diff --git a/shared/components/rstack-mi.ts b/shared/components/rstack-mi.ts index 24cdf18e..71d70ec6 100644 --- a/shared/components/rstack-mi.ts +++ b/shared/components/rstack-mi.ts @@ -35,8 +35,6 @@ export class RStackMi extends HTMLElement { #shadow: ShadowRoot; #messages: MiMessage[] = []; #abortController: AbortController | null = null; - #dictation: SpeechDictation | null = null; - #interimText = ""; #preferredModel: string = ""; #minimized = false; #availableModels: MiModelConfig[] = []; @@ -154,8 +152,7 @@ export class RStackMi extends HTMLElement { - ${SpeechDictation.isSupported() ? '' : ''} - ${SpeechDictation.isSupported() ? '' : ''} + ${SpeechDictation.isSupported() ? '' : ''}
@@ -163,7 +160,6 @@ export class RStackMi extends HTMLElement { mi
- ${SpeechDictation.isSupported() ? '' : ''}
@@ -315,51 +311,11 @@ export class RStackMi extends HTMLElement { } }); - // Voice dictation - const micBtn = this.#shadow.getElementById("mi-mic") as HTMLButtonElement | null; - if (micBtn) { - let baseText = ""; - this.#dictation = new SpeechDictation({ - onInterim: (text) => { - this.#interimText = text; - barInput.value = baseText + (baseText ? " " : "") + text; - }, - onFinal: (text) => { - this.#interimText = ""; - baseText += (baseText ? " " : "") + text; - barInput.value = baseText; - }, - onStateChange: (recording) => { - micBtn.classList.toggle("recording", recording); - if (!recording) { - baseText = barInput.value; - this.#interimText = ""; - } - }, - onError: (err) => console.warn("MI dictation:", err), - }); - - micBtn.addEventListener("click", (e) => { - e.stopPropagation(); - if (this.#voiceMode) return; // Bar mic disabled during voice mode - if (!this.#dictation!.isRecording) { - baseText = barInput.value; - } - this.#dictation!.toggle(); - barInput.focus(); - }); - } - - // Voice mode buttons - const voiceBarBtn = this.#shadow.getElementById("mi-voice-btn"); - const voicePanelBtn = this.#shadow.getElementById("mi-voice-panel-btn"); + // Voice mode — single mic toggle (STT input → auto-submit → TTS response) + const voiceBtn = this.#shadow.getElementById("mi-voice-btn"); const voiceStopBtn = this.#shadow.getElementById("mi-voice-stop"); - voiceBarBtn?.addEventListener("click", (e) => { - e.stopPropagation(); - this.#toggleVoiceMode(); - }); - voicePanelBtn?.addEventListener("click", (e) => { + voiceBtn?.addEventListener("click", (e) => { e.stopPropagation(); this.#toggleVoiceMode(); }); @@ -487,9 +443,6 @@ export class RStackMi extends HTMLElement { } #activateVoiceMode() { - // Stop existing bar dictation if recording - if (this.#dictation?.isRecording) this.#dictation.stop(); - this.#voiceMode = true; this.#voiceAccumulated = ""; @@ -558,8 +511,7 @@ export class RStackMi extends HTMLElement { const strip = this.#shadow.getElementById("mi-voice-strip"); const label = this.#shadow.getElementById("mi-voice-label"); - const barBtn = this.#shadow.getElementById("mi-voice-btn"); - const panelBtn = this.#shadow.getElementById("mi-voice-panel-btn"); + const btn = this.#shadow.getElementById("mi-voice-btn"); // Update strip if (strip) { @@ -576,9 +528,8 @@ export class RStackMi extends HTMLElement { label.textContent = labels[state]; } - // Update buttons - for (const btn of [barBtn, panelBtn]) { - if (!btn) continue; + // Update button + if (btn) { btn.classList.remove("v-listening", "v-thinking", "v-speaking", "v-active"); if (this.#voiceMode) { btn.classList.add("v-active"); @@ -646,10 +597,10 @@ export class RStackMi extends HTMLElement { .replace(/\s+/g, " ") .trim(); - // Truncate to ~4 sentences for snappy TTS + // Truncate to ~2 sentences for succinct TTS const sentences = stripped.match(/[^.!?]+[.!?]+/g) || [stripped]; - if (sentences.length > 4) { - stripped = sentences.slice(0, 4).join(" ").trim(); + if (sentences.length > 2) { + stripped = sentences.slice(0, 2).join(" ").trim(); } return stripped; } @@ -1065,18 +1016,20 @@ const STYLES = ` .mi-input-bar::placeholder { color: var(--rs-text-muted); } .mi-mic-btn { - background: none; border: none; cursor: pointer; padding: 2px 4px; - font-size: 0.85rem; border-radius: 6px; transition: all 0.2s; - flex-shrink: 0; line-height: 1; + background: none; border: none; cursor: pointer; padding: 2px 6px; + font-size: 0.9rem; border-radius: 6px; transition: all 0.2s; + flex-shrink: 0; line-height: 1; opacity: 0.7; } -.mi-mic-btn:hover, .mi-mic-btn:active { background: var(--rs-bg-hover); } -.mi-mic-btn.recording { - animation: micPulse 1.5s infinite; - filter: saturate(2) brightness(1.1); +.mi-mic-btn:hover, .mi-mic-btn:active { background: var(--rs-bg-hover); opacity: 1; } +.mi-mic-btn.v-active { opacity: 1; } +.mi-mic-btn.v-listening { + animation: voicePulseRed 1.5s infinite; } -@keyframes micPulse { - 0%, 100% { transform: scale(1); } - 50% { transform: scale(1.15); } +.mi-mic-btn.v-thinking { + animation: voiceSpinAmber 2s linear infinite; +} +.mi-mic-btn.v-speaking { + animation: voicePulseCyan 2s infinite; } /* ── Rich panel ── */ @@ -1313,24 +1266,6 @@ const STYLES = ` -webkit-background-clip: text; -webkit-text-fill-color: transparent; } -/* ── Voice mode buttons ── */ -.mi-voice-btn, .mi-voice-panel-btn { - background: none; border: none; cursor: pointer; padding: 2px 4px; - font-size: 0.85rem; border-radius: 6px; transition: all 0.2s; - flex-shrink: 0; line-height: 1; opacity: 0.7; -} -.mi-voice-btn:hover, .mi-voice-panel-btn:hover { background: var(--rs-bg-hover); opacity: 1; } -.mi-voice-btn.v-active, .mi-voice-panel-btn.v-active { opacity: 1; } -.mi-voice-btn.v-listening, .mi-voice-panel-btn.v-listening { - animation: voicePulseRed 1.5s infinite; -} -.mi-voice-btn.v-thinking, .mi-voice-panel-btn.v-thinking { - animation: voiceSpinAmber 2s linear infinite; -} -.mi-voice-btn.v-speaking, .mi-voice-panel-btn.v-speaking { - animation: voicePulseCyan 2s infinite; -} - @keyframes voicePulseRed { 0%, 100% { filter: drop-shadow(0 0 2px transparent); } 50% { filter: drop-shadow(0 0 6px rgba(239,68,68,0.7)); }