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;
#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 {
<span class="mi-icon">&#10023;</span>
<input class="mi-input-bar" id="mi-bar-input" type="text"
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-voice-btn" id="mi-voice-btn" title="miC voice conversation">🎙 miC</button>' : ''}
${SpeechDictation.isSupported() ? '<button class="mi-mic-btn" id="mi-voice-btn" title="Voice mode">🎤</button>' : ''}
</div>
<div class="mi-panel" id="mi-panel">
<div class="mi-panel-header">
@ -163,7 +160,6 @@ export class RStackMi extends HTMLElement {
<span class="mi-panel-title">mi</span>
<select class="mi-model-select" id="mi-model-select" title="Select AI model"></select>
<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-close" title="Close">&times;</button>
</div>
@ -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)); }