feat: real-time reasoning speech, dynamic voice tags, and unified web UI bubbles
This commit is contained in:
+61
-45
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user