v3.2.2: 폴더블 폰 모바일 패널 토글 전면 수정 + 탐정앱 네트워크 분석 + 음성 유틸
- mobToggle 가드 640→1024: 음악·슬라이드·코드·날씨·언어·치과·ESP32·아두이노 앱 (CSS max-width:1024px인데 JS guard가 >640으로 남아 폴드 내부화면에서 토글 불가) - #mob-tabbar z-index 400/490→600, 패널 bottom 0→52px, backdrop inset 0 0 52px 0 (backdrop이 탭바를 가려 열린 상태에서 다른 탭 전환 불가 문제 해결) - 모든 innerWidth<=640 자동트리거 →<=1024 통일 (9개 앱) - 탐정앱: 네트워크 분석 패널 (IP조회/Traceroute/Nmap/DNS/WHOIS/SSL/취약점/해시/웹스캔/배너/CVE) - voice-utils.js 분리 — 서브앱 공통 음성입력+TTS 유틸 - app.js: 컨텍스트 토큰 게이지 (세션 목록에 tok/max 표시), usage 이벤트 처리 - routes-music.ts: Demucs 보컬분리 + YouTube 다운로드 잡 관리 (TTL 자동정리) - server-v2.ts: 탐정 네트워크 API (traceroute/dns/whois/ssl/nmap/vuln/hash/webscan/cve) - session.ts: resolveNumCtx export, /api/agent/session contextTokenEstimate 노출 - config.json: 카카오 채널 설정, ITS 교통 API 키 vault 참조 - styles.css: app-mic-btn / app-tts-btn 공통 스타일 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
// Shared voice input + TTS for sub-apps. Requires authH() to be defined by the host page.
|
||||
var _appSendFn = null;
|
||||
var _isRecording = false;
|
||||
var _speechRecog = null;
|
||||
var _micAutosend = true;
|
||||
var _ttsAudio = null;
|
||||
var _ttsBtnActive = null;
|
||||
|
||||
function initAppVoice(sendFn) {
|
||||
_appSendFn = sendFn || null;
|
||||
}
|
||||
|
||||
function toggleMicRecording() {
|
||||
if (_isRecording) stopMicRecording();
|
||||
else startMicRecording();
|
||||
}
|
||||
|
||||
function startMicRecording() {
|
||||
var SpeechRecog = window.SpeechRecognition || window.webkitSpeechRecognition;
|
||||
if (!SpeechRecog) { alert('이 브라우저는 음성 인식을 지원하지 않습니다.\nChrome을 사용해주세요.'); return; }
|
||||
_speechRecog = new SpeechRecog();
|
||||
_speechRecog.lang = 'ko-KR';
|
||||
_speechRecog.interimResults = false;
|
||||
_speechRecog.maxAlternatives = 1;
|
||||
_speechRecog.continuous = false;
|
||||
var btn = document.getElementById('mic-btn');
|
||||
_speechRecog.onstart = function() {
|
||||
_isRecording = true;
|
||||
if (btn) { btn.textContent = '⏹'; btn.title = '말하는 중… (클릭 시 중지)'; btn.classList.add('recording'); }
|
||||
};
|
||||
_speechRecog.onresult = function(e) {
|
||||
var transcript = e.results[0][0].transcript;
|
||||
var input = document.getElementById('chat-input');
|
||||
if (input && transcript) {
|
||||
input.value = (input.value ? input.value + ' ' : '') + transcript;
|
||||
input.focus();
|
||||
input.dispatchEvent(new Event('input'));
|
||||
if (_micAutosend && _appSendFn) setTimeout(function() { _appSendFn(); }, 50);
|
||||
}
|
||||
};
|
||||
_speechRecog.onerror = function(e) {
|
||||
if (e.error !== 'no-speech') alert('음성 인식 오류: ' + e.error);
|
||||
};
|
||||
_speechRecog.onend = function() {
|
||||
_isRecording = false;
|
||||
_speechRecog = null;
|
||||
if (btn) { btn.textContent = '🎤'; btn.title = '음성 입력'; btn.classList.remove('recording'); }
|
||||
};
|
||||
_speechRecog.start();
|
||||
}
|
||||
|
||||
function stopMicRecording() {
|
||||
if (_speechRecog) _speechRecog.stop();
|
||||
}
|
||||
|
||||
function playTTSText(btn, text) {
|
||||
text = (text || '').replace(/```[\s\S]*?```/g, '').replace(/[#*`_~>]/g, ' ').replace(/\s+/g, ' ').trim();
|
||||
if (!text) return;
|
||||
if (_ttsBtnActive === btn && _ttsAudio) {
|
||||
_ttsAudio.pause(); _ttsAudio = null;
|
||||
btn.textContent = '🔊'; btn.classList.remove('playing');
|
||||
_ttsBtnActive = null; return;
|
||||
}
|
||||
if (_ttsAudio) { _ttsAudio.pause(); _ttsAudio = null; }
|
||||
if (_ttsBtnActive) { _ttsBtnActive.textContent = '🔊'; _ttsBtnActive.classList.remove('playing'); }
|
||||
btn.textContent = '⌛'; btn.classList.add('playing');
|
||||
_ttsBtnActive = btn;
|
||||
fetch('/api/voice/tts', {
|
||||
method: 'POST',
|
||||
headers: Object.assign({ 'Content-Type': 'application/json' }, typeof authH === 'function' ? authH() : {}),
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ text: text.slice(0, 2000) }),
|
||||
})
|
||||
.then(function(r) { if (!r.ok) throw new Error(r.status); return r.blob(); })
|
||||
.then(function(blob) {
|
||||
var url = URL.createObjectURL(blob);
|
||||
_ttsAudio = new Audio(url);
|
||||
_ttsAudio.onended = function() { URL.revokeObjectURL(url); _ttsAudio = null; btn.textContent = '🔊'; btn.classList.remove('playing'); _ttsBtnActive = null; };
|
||||
_ttsAudio.onerror = function() { URL.revokeObjectURL(url); _ttsAudio = null; btn.textContent = '🔊'; btn.classList.remove('playing'); _ttsBtnActive = null; };
|
||||
btn.textContent = '⏹'; _ttsAudio.play();
|
||||
})
|
||||
.catch(function() { btn.textContent = '🔊'; btn.classList.remove('playing'); _ttsBtnActive = null; });
|
||||
}
|
||||
Reference in New Issue
Block a user