feat: real-time reasoning speech, dynamic voice tags, and unified web UI bubbles

This commit is contained in:
Adolfo Reyna
2026-08-12 10:17:47 -04:00
parent b5034b4b16
commit 404a7071fb
8 changed files with 734 additions and 94 deletions
+61 -45
View File
@@ -729,6 +729,12 @@ HTML_INDEX = """<!DOCTYPE html>
<h1>VoiceAgent Companion</h1>
</div>
<div class="controls">
<div class="pill" id="hermesModePill" title="Hermes Mode: API (Daemon) vs CLI (Subprocess)">
Hermes: <strong id="hermesModeText" style="color: #10b981;">API (9119)</strong>
</div>
<div class="pill" id="latencyPill" title="Turn Latency Profiling Metric">
Latency: <strong id="latencyText" style="color: #818cf8;">-- ms</strong>
</div>
<div class="pill" id="modelPill" onclick="openModelPicker()">
Model: <strong id="modelName">Loading...</strong>
</div>
@@ -845,6 +851,18 @@ HTML_INDEX = """<!DOCTYPE html>
} else if (data.type === 'status_change') {
if (data.model) { modelNameEl.textContent = data.model; activeModelId = data.model; }
if (data.voice) voiceNameEl.textContent = data.voice;
} else if (data.type === 'hermes_status') {
const hermesEl = document.getElementById('hermesModeText');
if (hermesEl && data.mode) {
hermesEl.textContent = data.mode;
hermesEl.style.color = data.is_api ? '#10b981' : '#f59e0b';
}
} else if (data.type === 'profiling') {
const latEl = document.getElementById('latencyText');
if (latEl && data.total_ms !== undefined) {
latEl.textContent = `${data.total_ms} ms (${data.mode || ''})`;
latEl.style.color = data.total_ms < 1500 ? '#10b981' : '#818cf8';
}
} else if (data.type === 'show_file') {
if (data.name && data.content) {
drawerFileName.textContent = data.name;
@@ -863,6 +881,10 @@ HTML_INDEX = """<!DOCTYPE html>
function renderEvent(data) {
if (data.type === 'heard' && data.text) {
appendUserMessage(data.text, data.at);
} else if (data.type === 'fast_reply' && data.text) {
appendFastReply(data.text, data.is_complete, data.at);
} else if (data.type === 'thinking' && data.text) {
appendThinkingStep(data.text, data.at);
} else if (data.type === 'partial_reply' && data.text) {
appendPartialReply(data.text, data.at);
} else if (data.type === 'reply' && data.text) {
@@ -872,42 +894,25 @@ HTML_INDEX = """<!DOCTYPE html>
}
}
function sendMessage() {
const text = userInputEl.value.trim();
if (!text) return;
userInputEl.value = '';
let currentThinkingContent = null;
fetch('/api/send', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({text: text})
})
.then(r => r.json())
.then(res => {
if (res.error) alert(res.error);
})
.catch(err => console.error('Failed to send message:', err));
}
async function resetSession() {
if (!confirm('Start a fresh Hermes conversation session?')) return;
try {
const res = await fetch('/api/session/reset', { method: 'POST' });
const data = await res.json();
if (data.success) {
appendToolStep('System', data.message || 'Session reset.');
} else {
alert('Failed to reset session: ' + (data.error || 'Unknown error'));
}
} catch (err) {
alert('Error resetting session: ' + err.message);
}
}
function handleKeyDown(e) {
if (e.key === 'Enter') {
sendMessage();
function appendThinkingStep(text, timestamp) {
ensureAssistantCard(timestamp);
if (!currentThinkingContent) {
const step = document.createElement('div');
step.className = 'thinking-step';
step.style.cssText = 'margin-bottom: 10px; padding: 12px 16px; background: linear-gradient(135deg, rgba(168, 85, 247, 0.12), rgba(126, 34, 206, 0.06)); border: 1px solid rgba(168, 85, 247, 0.35); border-left: 4px solid #a855f7; border-radius: 10px; box-shadow: 0 4px 14px rgba(168, 85, 247, 0.15); font-size: 13.5px; color: #e9d5ff; backdrop-filter: blur(8px);';
step.innerHTML = `
<div style="font-weight: 700; text-transform: uppercase; font-size: 11px; letter-spacing: 0.8px; color: #c084fc; margin-bottom: 6px; display: flex; align-items: center; gap: 6px;">
<span>🧠 REASONING PROCESS</span>
</div>
<div class="thinking-text" style="white-space: pre-wrap; font-family: 'JetBrains Mono', monospace; font-size: 13px; line-height: 1.5; color: #f3e8ff;"></div>
`;
currentToolsContainer.appendChild(step);
currentThinkingContent = step.querySelector('.thinking-text');
}
currentThinkingContent.textContent += text;
feed.scrollTop = feed.scrollHeight;
}
function appendUserMessage(text, timestamp) {
@@ -915,6 +920,7 @@ HTML_INDEX = """<!DOCTYPE html>
currentAssistantCard = null;
currentToolsContainer = null;
currentTextContent = null;
currentThinkingContent = null;
currentHasPartialText = false;
const card = document.createElement('div');
@@ -950,9 +956,12 @@ HTML_INDEX = """<!DOCTYPE html>
ensureAssistantCard(timestamp);
const step = document.createElement('div');
step.className = 'tool-step';
step.style.cssText = 'margin-bottom: 10px; padding: 12px 16px; background: linear-gradient(135deg, rgba(6, 182, 212, 0.12), rgba(14, 116, 144, 0.06)); border: 1px solid rgba(6, 182, 212, 0.35); border-left: 4px solid #06b6d4; border-radius: 10px; box-shadow: 0 4px 14px rgba(6, 182, 212, 0.15); backdrop-filter: blur(8px);';
step.innerHTML = `
<div class="tool-step-header">⚡ Tool Executed: ${escapeHtml(name)}</div>
${detail ? `<div class="tool-step-body">${escapeHtml(detail)}</div>` : ''}
<div style="font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.8px; color: #22d3ee; margin-bottom: 6px; display: flex; align-items: center; gap: 6px;">
<span>⚡ TOOL EXECUTED: ${escapeHtml(name)}</span>
</div>
${detail ? `<div style="margin-top: 6px; padding: 8px 10px; background: rgba(0,0,0,0.4); border: 1px solid rgba(6, 182, 212, 0.2); border-radius: 6px; font-family: 'JetBrains Mono', monospace; font-size: 12.5px; color: #67e8f9; max-height: 180px; overflow-y: auto; white-space: pre-wrap; word-break: break-all;">${escapeHtml(detail)}</div>` : ''}
`;
currentToolsContainer.appendChild(step);
feed.scrollTop = feed.scrollHeight;
@@ -960,22 +969,29 @@ HTML_INDEX = """<!DOCTYPE html>
function appendPartialReply(text, timestamp) {
ensureAssistantCard(timestamp);
const formatted = linkifyFiles(escapeHtml(text));
if (currentTextContent.innerHTML) {
currentTextContent.innerHTML += ' ' + formatted;
} else {
currentTextContent.innerHTML = formatted;
currentTextContent.style.display = 'block';
currentTextContent.style.cssText = 'display: block; margin-top: 6px; font-size: 14.5px; line-height: 1.6; color: #f3f4f6; white-space: pre-wrap;';
if (currentTextContent.textContent.length > 0 && !currentTextContent.textContent.endsWith(' ') && !text.startsWith(' ')) {
currentTextContent.appendChild(document.createTextNode(' '));
}
const span = document.createElement('span');
span.innerHTML = linkifyFiles(escapeHtml(text));
span.style.cssText = 'opacity: 0; transition: opacity 0.2s ease-in;';
currentTextContent.appendChild(span);
setTimeout(() => { span.style.opacity = '1'; }, 10);
currentHasPartialText = true;
feed.scrollTop = feed.scrollHeight;
}
function appendFinalReply(text, timestamp) {
ensureAssistantCard(timestamp);
if (!currentHasPartialText) {
const formatted = linkifyFiles(escapeHtml(text));
currentTextContent.innerHTML = formatted;
}
const formatted = linkifyFiles(escapeHtml(text));
currentTextContent.style.display = 'block';
currentTextContent.style.cssText = 'display: block; margin-top: 6px; font-size: 14.5px; line-height: 1.6; color: #f3f4f6; white-space: pre-wrap;';
currentTextContent.innerHTML = formatted;
feed.scrollTop = feed.scrollHeight;
}