Merge branch 'dev'
CI/CD / deploy (push) Failing after 2m37s Details

This commit is contained in:
Jeff Emmett 2026-04-13 09:03:36 -04:00
commit 95585a7f58
1 changed files with 22 additions and 87 deletions

View File

@ -35,8 +35,6 @@ export class RStackMi extends HTMLElement {
#shadow: ShadowRoot; #shadow: ShadowRoot;
#messages: MiMessage[] = []; #messages: MiMessage[] = [];
#abortController: AbortController | null = null; #abortController: AbortController | null = null;
#dictation: SpeechDictation | null = null;
#interimText = "";
#preferredModel: string = ""; #preferredModel: string = "";
#minimized = false; #minimized = false;
#availableModels: MiModelConfig[] = []; #availableModels: MiModelConfig[] = [];
@ -154,8 +152,7 @@ export class RStackMi extends HTMLElement {
<span class="mi-icon">&#10023;</span> <span class="mi-icon">&#10023;</span>
<input class="mi-input-bar" id="mi-bar-input" type="text" <input class="mi-input-bar" id="mi-bar-input" type="text"
placeholder="Ask mi anything..." autocomplete="off" /> placeholder="Ask mi anything..." autocomplete="off" />
${SpeechDictation.isSupported() ? '<button class="mi-mic-btn" id="mi-mic" title="Voice dictation">🎤</button>' : ''} ${SpeechDictation.isSupported() ? '<button class="mi-mic-btn" id="mi-voice-btn" title="Voice mode">🎤</button>' : ''}
${SpeechDictation.isSupported() ? '<button class="mi-voice-btn" id="mi-voice-btn" title="miC voice conversation">🎙 miC</button>' : ''}
</div> </div>
<div class="mi-panel" id="mi-panel"> <div class="mi-panel" id="mi-panel">
<div class="mi-panel-header"> <div class="mi-panel-header">
@ -163,7 +160,6 @@ export class RStackMi extends HTMLElement {
<span class="mi-panel-title">mi</span> <span class="mi-panel-title">mi</span>
<select class="mi-model-select" id="mi-model-select" title="Select AI model"></select> <select class="mi-model-select" id="mi-model-select" title="Select AI model"></select>
<div class="mi-panel-spacer"></div> <div class="mi-panel-spacer"></div>
${SpeechDictation.isSupported() ? '<button class="mi-voice-panel-btn" id="mi-voice-panel-btn" title="miC voice mode">🎙 miC</button>' : ''}
<button class="mi-panel-btn" id="mi-minimize" title="Minimize (Escape)">&#8722;</button> <button class="mi-panel-btn" id="mi-minimize" title="Minimize (Escape)">&#8722;</button>
<button class="mi-panel-btn" id="mi-close" title="Close">&times;</button> <button class="mi-panel-btn" id="mi-close" title="Close">&times;</button>
</div> </div>
@ -315,51 +311,11 @@ export class RStackMi extends HTMLElement {
} }
}); });
// Voice dictation // Voice mode — single mic toggle (STT input → auto-submit → TTS response)
const micBtn = this.#shadow.getElementById("mi-mic") as HTMLButtonElement | null; const voiceBtn = this.#shadow.getElementById("mi-voice-btn");
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");
const voiceStopBtn = this.#shadow.getElementById("mi-voice-stop"); const voiceStopBtn = this.#shadow.getElementById("mi-voice-stop");
voiceBarBtn?.addEventListener("click", (e) => { voiceBtn?.addEventListener("click", (e) => {
e.stopPropagation();
this.#toggleVoiceMode();
});
voicePanelBtn?.addEventListener("click", (e) => {
e.stopPropagation(); e.stopPropagation();
this.#toggleVoiceMode(); this.#toggleVoiceMode();
}); });
@ -487,9 +443,6 @@ export class RStackMi extends HTMLElement {
} }
#activateVoiceMode() { #activateVoiceMode() {
// Stop existing bar dictation if recording
if (this.#dictation?.isRecording) this.#dictation.stop();
this.#voiceMode = true; this.#voiceMode = true;
this.#voiceAccumulated = ""; this.#voiceAccumulated = "";
@ -558,8 +511,7 @@ export class RStackMi extends HTMLElement {
const strip = this.#shadow.getElementById("mi-voice-strip"); const strip = this.#shadow.getElementById("mi-voice-strip");
const label = this.#shadow.getElementById("mi-voice-label"); const label = this.#shadow.getElementById("mi-voice-label");
const barBtn = this.#shadow.getElementById("mi-voice-btn"); const btn = this.#shadow.getElementById("mi-voice-btn");
const panelBtn = this.#shadow.getElementById("mi-voice-panel-btn");
// Update strip // Update strip
if (strip) { if (strip) {
@ -576,9 +528,8 @@ export class RStackMi extends HTMLElement {
label.textContent = labels[state]; label.textContent = labels[state];
} }
// Update buttons // Update button
for (const btn of [barBtn, panelBtn]) { if (btn) {
if (!btn) continue;
btn.classList.remove("v-listening", "v-thinking", "v-speaking", "v-active"); btn.classList.remove("v-listening", "v-thinking", "v-speaking", "v-active");
if (this.#voiceMode) { if (this.#voiceMode) {
btn.classList.add("v-active"); btn.classList.add("v-active");
@ -646,10 +597,10 @@ export class RStackMi extends HTMLElement {
.replace(/\s+/g, " ") .replace(/\s+/g, " ")
.trim(); .trim();
// Truncate to ~4 sentences for snappy TTS // Truncate to ~2 sentences for succinct TTS
const sentences = stripped.match(/[^.!?]+[.!?]+/g) || [stripped]; const sentences = stripped.match(/[^.!?]+[.!?]+/g) || [stripped];
if (sentences.length > 4) { if (sentences.length > 2) {
stripped = sentences.slice(0, 4).join(" ").trim(); stripped = sentences.slice(0, 2).join(" ").trim();
} }
return stripped; return stripped;
} }
@ -1065,18 +1016,20 @@ const STYLES = `
.mi-input-bar::placeholder { color: var(--rs-text-muted); } .mi-input-bar::placeholder { color: var(--rs-text-muted); }
.mi-mic-btn { .mi-mic-btn {
background: none; border: none; cursor: pointer; padding: 2px 4px; background: none; border: none; cursor: pointer; padding: 2px 6px;
font-size: 0.85rem; border-radius: 6px; transition: all 0.2s; font-size: 0.9rem; border-radius: 6px; transition: all 0.2s;
flex-shrink: 0; line-height: 1; 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:hover, .mi-mic-btn:active { background: var(--rs-bg-hover); opacity: 1; }
.mi-mic-btn.recording { .mi-mic-btn.v-active { opacity: 1; }
animation: micPulse 1.5s infinite; .mi-mic-btn.v-listening {
filter: saturate(2) brightness(1.1); animation: voicePulseRed 1.5s infinite;
} }
@keyframes micPulse { .mi-mic-btn.v-thinking {
0%, 100% { transform: scale(1); } animation: voiceSpinAmber 2s linear infinite;
50% { transform: scale(1.15); } }
.mi-mic-btn.v-speaking {
animation: voicePulseCyan 2s infinite;
} }
/* ── Rich panel ── */ /* ── Rich panel ── */
@ -1313,24 +1266,6 @@ const STYLES = `
-webkit-background-clip: text; -webkit-text-fill-color: transparent; -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 { @keyframes voicePulseRed {
0%, 100% { filter: drop-shadow(0 0 2px transparent); } 0%, 100% { filter: drop-shadow(0 0 2px transparent); }
50% { filter: drop-shadow(0 0 6px rgba(239,68,68,0.7)); } 50% { filter: drop-shadow(0 0 6px rgba(239,68,68,0.7)); }