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() ? '' : ''}
@@ -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)); }