From 16aadcae96d108fef95e1919b5f63c95044c51ec Mon Sep 17 00:00:00 2001 From: Shane Maynard Date: Sat, 29 Aug 2026 06:51:09 -0400 Subject: [PATCH] feat: redesign Hope voice experience --- web/app.js | 90 ++-- web/favicon.svg | 12 +- web/index.html | 154 ++++-- web/manifest.json | 10 +- web/style.css | 1307 ++++++++++++++++++++++++++++++--------------- 5 files changed, 1055 insertions(+), 518 deletions(-) diff --git a/web/app.js b/web/app.js index fc21ca0..8bc3ab7 100644 --- a/web/app.js +++ b/web/app.js @@ -82,7 +82,7 @@ function updateTasksBadge() { function renderTasks() { const items = [...tasks.values()].sort((a, b) => (b.created_at || 0) - (a.created_at || 0)); if (items.length === 0) { - tasksList.innerHTML = '

No tasks yet. Ask Hope to research something.

'; + tasksList.innerHTML = '
Nothing runningAsk Hope to research or compare something.
'; return; } tasksList.innerHTML = ""; @@ -124,8 +124,14 @@ function handleTaskEvent(data) { renderTasks(); } -tasksBtn.addEventListener("click", () => tasksPanel.classList.toggle("open")); -closeTasksBtn.addEventListener("click", () => tasksPanel.classList.remove("open")); +function setTasksOpen(open) { + tasksPanel.classList.toggle("open", open); + tasksBtn.setAttribute("aria-expanded", String(open)); +} + +tasksBtn.setAttribute("aria-expanded", "false"); +tasksBtn.addEventListener("click", () => setTasksOpen(!tasksPanel.classList.contains("open"))); +closeTasksBtn.addEventListener("click", () => setTasksOpen(false)); // ── Audio analysers (mic + remote agent audio) ───────────────────────────── let micCtx = null; @@ -196,11 +202,11 @@ function readAgentAudioLevel() { // ── State machine ─────────────────────────────────────────────────────────── const STATE_LABELS = { - idle: "Idle", - connecting: "Connecting", + idle: "Ready when you are", + connecting: "Opening a secure room", listening: "Listening", - thinking: "Thinking", - speaking: "Speaking", + thinking: "Thinking it through", + speaking: "Hope is speaking", }; function setState(next) { @@ -310,10 +316,21 @@ function persistHistory() { let history = loadHistory(); +const DELIVERY_MARKERS = /\[(?:appreciative|calm|concerned|curious|encouraging|excited|happy|joking|quiet|reassuring|reflective|sad|serious|surprised|sympathetic|urgent|whispering|laughter|sighing|breathing|neutral)\]\s*/gi; + +function cleanSpokenText(text) { + return String(text || "").replace(DELIVERY_MARKERS, "").trim(); +} + +function formatTimestamp(ts) { + if (!ts) return ""; + return new Date(ts).toLocaleTimeString([], { hour: "numeric", minute: "2-digit" }); +} + function renderMessage(msg) { const wrap = document.createElement("div"); wrap.className = `message-row ${msg.role}`; - wrap.dataset.ts = msg.ts || ""; + wrap.dataset.ts = formatTimestamp(msg.ts); if (msg.role === "agent") { const dot = document.createElement("span"); @@ -323,7 +340,7 @@ function renderMessage(msg) { const bubble = document.createElement("div"); bubble.className = "bubble"; - bubble.textContent = msg.text || ""; + bubble.textContent = cleanSpokenText(msg.text); if (msg.role === "agent") { const viz = document.createElement("span"); viz.className = "audio-viz"; @@ -350,7 +367,7 @@ function addPartial(role, text) { let last = null; for (const r of rows) last = r; if (last && last.dataset.role === role) { - last.querySelector(".bubble").textContent = text; + last.querySelector(".bubble").textContent = cleanSpokenText(text); messagesEl.scrollTop = messagesEl.scrollHeight; return; } @@ -364,7 +381,7 @@ function addPartial(role, text) { } const bubble = document.createElement("div"); bubble.className = "bubble partial"; - bubble.textContent = text; + bubble.textContent = cleanSpokenText(text); wrap.appendChild(bubble); messagesEl.appendChild(wrap); messagesEl.scrollTop = messagesEl.scrollHeight; @@ -451,19 +468,18 @@ function renderVoiceList() { name.className = "voice-name"; name.textContent = v.name + (v.tag ? ` (${v.tag})` : ""); - const previewBtn = document.createElement("button"); - previewBtn.className = "btn btn-ghost voice-preview"; - previewBtn.setAttribute("aria-label", `Preview ${v.name}`); - previewBtn.title = "Preview (coming soon)"; - // TODO: play a short Azure TTS sample for this voice. - previewBtn.addEventListener("click", (e) => { - e.stopPropagation(); - addMessage("system", `Preview for ${v.name} is not wired up yet.`); - }); - + const selected = v.id === active; + card.setAttribute("role", "radio"); + card.setAttribute("aria-checked", String(selected)); + card.tabIndex = 0; card.appendChild(name); - card.appendChild(previewBtn); card.addEventListener("click", () => selectVoice(v.id)); + card.addEventListener("keydown", (event) => { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + selectVoice(v.id); + } + }); voiceListEl.appendChild(card); } } @@ -482,16 +498,26 @@ function openSheet() { renderVoiceList(); sheetBackdrop.hidden = false; settingsSheet.classList.add("open"); + settingsBtn.setAttribute("aria-expanded", "true"); + settingsSheet.querySelector(".voice-card")?.focus(); } function closeSheet() { settingsSheet.classList.remove("open"); - sheetBackdrop.hidden = true; + settingsBtn.setAttribute("aria-expanded", "false"); + window.setTimeout(() => { sheetBackdrop.hidden = true; }, 240); + settingsBtn.focus(); } +settingsBtn.setAttribute("aria-expanded", "false"); settingsBtn.addEventListener("click", openSheet); closeSheetBtn.addEventListener("click", closeSheet); sheetBackdrop.addEventListener("click", closeSheet); +document.addEventListener("keydown", (event) => { + if (event.key !== "Escape") return; + if (settingsSheet.classList.contains("open")) closeSheet(); + if (tasksPanel.classList.contains("open")) setTasksOpen(false); +}); document.addEventListener("keydown", (e) => { if (e.key === "Escape") closeSheet(); }); @@ -536,7 +562,7 @@ function findAgent(rm) { function markAgentFound(rm, participant) { agentParticipant = participant; - statusEl.textContent = "Agent connected — speak now"; + statusEl.textContent = "Secure room · Microphone on"; // Send current voice selection to the agent sendVoiceMessage(currentVoiceId(), rm); } @@ -599,7 +625,7 @@ function createRoom(token) { // clobber UI state of the active one. newRoom.on(RoomEvent.ConnectionQualityChanged, (participant, quality) => { if (participant === newRoom.localParticipant && newRoom === room) { - statusEl.textContent = `Connected (${quality} quality)`; + statusEl.textContent = `Secure room · ${quality} connection`; } }); @@ -608,7 +634,7 @@ function createRoom(token) { teardownAudio(); stopVisualLoop(); releaseWakeLock(); - statusEl.textContent = "Disconnected"; + statusEl.textContent = "Conversation ended"; setState("idle"); startBtn.disabled = false; stopBtn.disabled = true; @@ -646,7 +672,7 @@ function waitForAgent(timeoutMs) { startBtn.addEventListener("click", async () => { try { setState("connecting"); - statusEl.textContent = "Connecting..."; + statusEl.textContent = "Opening your secure room…"; await acquireWakeLock(); // Get a signed access token from the token endpoint @@ -658,9 +684,9 @@ startBtn.addEventListener("click", async () => { // The agent is dispatched when we join. If it misses the dispatch // (e.g. the server just started and the worker wasn't ready yet), // reconnect once to trigger a fresh dispatch. - statusEl.textContent = "Waiting for assistant..."; + statusEl.textContent = "Inviting Hope into the room…"; if (!(await waitForAgent(10000))) { - statusEl.textContent = "Assistant not ready, retrying..."; + statusEl.textContent = "Hope is taking a moment · Retrying…"; const stale = room; room = null; // detach first so its Disconnected handler agentParticipant = null; // cannot clobber the UI @@ -672,10 +698,10 @@ startBtn.addEventListener("click", async () => { } } - statusEl.textContent = "Requesting microphone..."; + statusEl.textContent = "Waiting for microphone permission…"; await room.localParticipant.setMicrophoneEnabled(true); - statusEl.textContent = "Connected — speak now"; + statusEl.textContent = "Secure room · Microphone on"; startBtn.disabled = true; stopBtn.disabled = false; muteBtn.disabled = false; @@ -703,7 +729,7 @@ stopBtn.addEventListener("click", () => { stopVisualLoop(); releaseWakeLock(); document.querySelectorAll('[id^="audio-"]').forEach(el => el.remove()); - statusEl.textContent = "Disconnected"; + statusEl.textContent = "Conversation ended"; setState("idle"); startBtn.disabled = false; stopBtn.disabled = true; diff --git a/web/favicon.svg b/web/favicon.svg index 4511586..47f2201 100644 --- a/web/favicon.svg +++ b/web/favicon.svg @@ -1,9 +1,7 @@ - - - - - - - + + + + + diff --git a/web/index.html b/web/index.html index 059c3ef..09303c5 100644 --- a/web/index.html +++ b/web/index.html @@ -3,81 +3,127 @@ - - Hope + + Hope · Voice companion -
-
-

Hope

-

Your voice assistant

+ + +
+
+
+ +
+

Hope

+

Voice companion

+
+
+ +
+
+ + Private to Shane +
+ +
-
-
-
-
-
-
- Idle - - -
+
+
+
+

I'm here.

+

Speak naturally. I’ll listen, remember, and answer in real time.

+
+ +
+ +
+ Ready when you are + + +
+
+ +
+ + + + +
+ +
+
Secure connection ready
+ +
+ +

+ + Your memory stays in Shane’s private profile. +

+
+ +
+
+
+

Conversation

+

Live transcript

+
+ +
+
+
+
-
- - - - -
- -
Disconnected
- - -
-
- Conversation - -
-
-
-
- - - - -
+
-

Background Tasks

-
-

No tasks yet. Ask Hope to research something.

+
+ Nothing running + Ask Hope to research or compare something. +
diff --git a/web/manifest.json b/web/manifest.json index 02b18ae..de15600 100644 --- a/web/manifest.json +++ b/web/manifest.json @@ -1,11 +1,11 @@ { - "name": "Voice Assistant", - "short_name": "Voice", - "description": "Real-time voice assistant — Azure Speech + Gemma LLM", + "name": "Hope Voice Companion", + "short_name": "Hope", + "description": "A private, real-time voice companion with personal memory.", "start_url": "/", "display": "standalone", - "background_color": "#09090b", - "theme_color": "#09090b", + "background_color": "#08111f", + "theme_color": "#08111f", "icons": [ { "src": "favicon.svg", diff --git a/web/style.css b/web/style.css index af4cccb..a59eda9 100644 --- a/web/style.css +++ b/web/style.css @@ -1,421 +1,817 @@ :root { - --bg: #09090b; - --panel: rgba(255, 255, 255, 0.05); - --panel-strong: rgba(255, 255, 255, 0.08); - --border: rgba(255, 255, 255, 0.09); - --text: #e4e4e7; - --text-dim: #a1a1aa; - --text-faint: #71717a; - --accent-1: #60a5fa; - --accent-2: #a78bfa; - --user-bubble: rgba(96, 165, 250, 0.14); - --agent-bubble: rgba(167, 139, 250, 0.14); - --danger: #ef4444; + --canvas: #08111f; + --canvas-deep: #050b14; + --surface: #0d1929; + --surface-raised: #132238; + --surface-soft: #172941; + --line: rgba(183, 204, 235, 0.13); + --line-strong: rgba(183, 204, 235, 0.22); + --ink: #f4f7fb; + --ink-soft: #c5d1e2; + --ink-muted: #8395ad; + --iris: #8ba7ff; + --iris-strong: #6f8ff7; + --iris-deep: #3c5fc9; + --warm: #ffb38e; + --mint: #76d9b2; + --danger: #ff806f; + --user-bubble: #1b3150; + --agent-bubble: #14243a; + --focus: #b5c5ff; + --ease-out: cubic-bezier(0.22, 1, 0.36, 1); + --z-panel: 40; + --z-backdrop: 50; + --z-sheet: 60; color-scheme: dark; } -@media (prefers-color-scheme: light) { - :root { - --bg: #fafafa; - --panel: rgba(0, 0, 0, 0.04); - --panel-strong: rgba(0, 0, 0, 0.07); - --border: rgba(0, 0, 0, 0.1); - --text: #18181b; - --text-dim: #52525b; - --text-faint: #a1a1aa; - --user-bubble: rgba(37, 99, 235, 0.1); - --agent-bubble: rgba(124, 58, 237, 0.1); - color-scheme: light; - } +* { + box-sizing: border-box; + margin: 0; + padding: 0; } -* { margin: 0; padding: 0; box-sizing: border-box; } - -html, body { height: 100%; } +html { + min-height: 100%; + background: var(--canvas-deep); +} body { - font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; - background: var(--bg); - color: var(--text); min-height: 100vh; - display: flex; - justify-content: center; - align-items: flex-start; - padding: calc(2rem + env(safe-area-inset-top)) calc(1rem + env(safe-area-inset-right)) - calc(2rem + env(safe-area-inset-bottom)) calc(1rem + env(safe-area-inset-left)); + min-height: 100dvh; + padding: calc(1.25rem + env(safe-area-inset-top)) + calc(1.25rem + env(safe-area-inset-right)) + calc(1.25rem + env(safe-area-inset-bottom)) + calc(1.25rem + env(safe-area-inset-left)); + overflow-x: hidden; + color: var(--ink); + background: + radial-gradient(circle at 16% 8%, rgba(86, 115, 207, 0.16), transparent 34rem), + radial-gradient(circle at 88% 92%, rgba(255, 179, 142, 0.07), transparent 30rem), + var(--canvas); + font-family: "Avenir Next", "Segoe UI Variable", "Segoe UI", system-ui, -apple-system, sans-serif; + -webkit-font-smoothing: antialiased; } -.container { - width: 100%; - max-width: 680px; +button, +input, +select { + font: inherit; } -header { - text-align: center; - margin-bottom: 1.5rem; +button { + color: inherit; } -h1 { - font-size: 2rem; +[hidden] { + display: none !important; +} + +.skip-link { + position: fixed; + top: 0.75rem; + left: 0.75rem; + z-index: 100; + padding: 0.7rem 1rem; + border-radius: 8px; + color: var(--canvas); + background: var(--ink); + font-size: 0.85rem; font-weight: 700; - background: linear-gradient(135deg, var(--accent-1), var(--accent-2)); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; + transform: translateY(-160%); + transition: transform 180ms var(--ease-out); } -.subtitle { - color: var(--text-faint); - margin-top: 0.5rem; - font-size: 0.9rem; +.skip-link:focus { + transform: translateY(0); } -/* ── Orb ─────────────────────────────────────────────────────────────────── */ -.orb-stage { +.app-shell { + width: min(1180px, 100%); + margin: 0 auto; +} + +/* ── Header ─────────────────────────────────────────────────────────────── */ +.topbar { + min-height: 62px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 1.25rem; + margin-bottom: 1.25rem; + padding: 0.25rem 0.15rem; +} + +.brand, +.topbar-actions, +.identity-chip, +.tasks-trigger { + display: flex; + align-items: center; +} + +.brand { + gap: 0.8rem; +} + +.brand-glyph { + position: relative; + width: 38px; + height: 38px; + flex: 0 0 38px; + border: 1px solid rgba(139, 167, 255, 0.55); + border-radius: 50%; +} + +.brand-glyph::before, +.brand-glyph::after, +.brand-glyph i { + content: ""; + position: absolute; + border-radius: 50%; +} + +.brand-glyph::before { + inset: 7px; + border: 1px solid rgba(139, 167, 255, 0.35); +} + +.brand-glyph::after { + width: 8px; + height: 8px; + right: 4px; + top: 5px; + background: var(--warm); + box-shadow: 0 0 12px rgba(255, 179, 142, 0.52); +} + +.brand-glyph i { + width: 6px; + height: 6px; + inset: 0; + margin: auto; + background: var(--iris); +} + +.brand h1 { + color: var(--ink); + font-size: 1.18rem; + font-weight: 680; + letter-spacing: -0.02em; +} + +.brand p { + margin-top: 0.1rem; + color: var(--ink-muted); + font-size: 0.74rem; +} + +.topbar-actions { + justify-content: flex-end; + gap: 0.65rem; +} + +.identity-chip { + min-height: 38px; + gap: 0.55rem; + padding: 0 0.85rem; + border: 1px solid var(--line); + border-radius: 999px; + color: var(--ink-soft); + background: rgba(13, 25, 41, 0.72); + font-size: 0.78rem; +} + +.identity-chip strong { + color: var(--ink); + font-weight: 650; +} + +.presence-dot { + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--mint); + box-shadow: 0 0 0 4px rgba(118, 217, 178, 0.1); +} + +/* ── Main workspace ─────────────────────────────────────────────────────── */ +.workspace { + display: grid; + grid-template-columns: minmax(390px, 0.92fr) minmax(420px, 1.08fr); + height: min(700px, calc(100dvh - 108px)); + min-height: 610px; + border: 1px solid var(--line); + border-radius: 16px; + overflow: hidden; + background: var(--surface); + box-shadow: 0 8px 8px rgba(2, 7, 14, 0.18); +} + +.voice-console { + position: relative; display: flex; flex-direction: column; align-items: center; - margin-bottom: 1.75rem; + min-width: 0; + padding: clamp(2rem, 4vw, 3.5rem) clamp(1.5rem, 4vw, 3.25rem) 1.6rem; + overflow: hidden; + background: + radial-gradient(circle at 50% 38%, rgba(90, 121, 215, 0.14), transparent 18rem), + var(--surface); +} + +.voice-console::after { + content: ""; + position: absolute; + left: 10%; + right: 10%; + bottom: 0; + height: 1px; + background: linear-gradient(90deg, transparent, rgba(139, 167, 255, 0.34), transparent); +} + +.console-intro { + position: relative; + z-index: 2; + width: 100%; + text-align: center; +} + +.console-intro h2 { + color: var(--ink); + font-size: 2rem; + font-weight: 620; + letter-spacing: -0.035em; + text-wrap: balance; +} + +.console-intro p { + max-width: 38ch; + margin: 0.55rem auto 0; + color: var(--ink-muted); + font-size: 0.91rem; + line-height: 1.55; + text-wrap: pretty; +} + +/* ── Voice presence ─────────────────────────────────────────────────────── */ +.orb-stage { + position: relative; + z-index: 1; + flex: 1; + width: 100%; + min-height: 310px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding-top: 0.75rem; } .orb { --pulse-scale: 1; + --pulse-opacity: 0.28; position: relative; - width: 200px; - height: 200px; + width: 260px; + height: 260px; display: grid; place-items: center; + isolation: isolate; } .orb-core { - width: 130px; - height: 130px; + position: relative; + z-index: 3; + width: 116px; + height: 116px; + overflow: hidden; border-radius: 50%; - background: radial-gradient(circle at 35% 30%, var(--accent-1), var(--accent-2) 70%); + background: + radial-gradient(circle at 32% 24%, rgba(244, 247, 251, 0.96) 0 3%, transparent 20%), + radial-gradient(circle at 38% 30%, #b9c9ff, #6f8ff7 42%, #3d57ab 72%, #23305b 100%); box-shadow: - 0 0 60px 8px color-mix(in srgb, var(--accent-2) 45%, transparent), - inset 0 0 30px rgba(255, 255, 255, 0.15); + 0 0 0 1px rgba(213, 222, 255, 0.24), + 0 12px 28px rgba(2, 8, 20, 0.34), + 0 0 52px rgba(111, 143, 247, 0.32), + inset -12px -14px 26px rgba(10, 20, 50, 0.34); transform: scale(var(--pulse-scale)); - transition: box-shadow 0.3s ease; - animation: breathe 4s ease-in-out infinite; + transition: filter 220ms ease, box-shadow 220ms ease, transform 90ms linear; +} + +.orb-shimmer { + position: absolute; + width: 90px; + height: 34px; + left: 9px; + top: 18px; + border-radius: 50%; + border-top: 1px solid rgba(255, 255, 255, 0.52); + transform: rotate(-22deg); + opacity: 0.7; +} + +.orb-ring, +.orb-orbit { + position: absolute; + border-radius: 50%; + pointer-events: none; } .orb-ring { - position: absolute; - inset: 0; - border-radius: 50%; - border: 1px solid color-mix(in srgb, var(--accent-2) 35%, transparent); - opacity: var(--pulse-opacity, 0.35); + z-index: 2; + width: 164px; + height: 164px; + border: 1px solid rgba(139, 167, 255, 0.2); + opacity: var(--pulse-opacity); transform: scale(var(--pulse-scale)); - transition: opacity 0.2s ease; + transition: opacity 180ms ease, transform 90ms linear; +} + +.orb-orbit { + z-index: 1; + border: 1px solid rgba(139, 167, 255, 0.15); +} + +.orbit-one { + width: 214px; + height: 140px; + transform: rotate(-18deg); +} + +.orbit-one::after { + content: ""; + position: absolute; + width: 7px; + height: 7px; + top: 12px; + right: 37px; + border-radius: 50%; + background: var(--warm); + box-shadow: 0 0 13px rgba(255, 179, 142, 0.65); +} + +.orbit-two { + width: 230px; + height: 230px; + border-color: rgba(139, 167, 255, 0.08); } -/* Per-state glow */ .orb[data-state="idle"] .orb-core { - filter: saturate(0.5) brightness(0.75); - box-shadow: 0 0 30px 4px color-mix(in srgb, var(--accent-2) 25%, transparent); + filter: saturate(0.72) brightness(0.78); + box-shadow: + 0 0 0 1px rgba(213, 222, 255, 0.13), + 0 12px 28px rgba(2, 8, 20, 0.3), + 0 0 28px rgba(111, 143, 247, 0.16); +} + +.orb[data-state="connecting"] .orbit-one, +.orb[data-state="thinking"] .orbit-one { + animation: orbit-turn 3.5s linear infinite; } .orb[data-state="connecting"] .orb-core { - animation-duration: 1.6s; - filter: saturate(0.8) brightness(0.9); + animation: presence-breathe 1.7s ease-in-out infinite; } .orb[data-state="listening"] .orb-core { - box-shadow: 0 0 70px 12px color-mix(in srgb, var(--accent-1) 55%, transparent), - inset 0 0 30px rgba(255, 255, 255, 0.18); + box-shadow: + 0 0 0 1px rgba(213, 222, 255, 0.4), + 0 12px 28px rgba(2, 8, 20, 0.34), + 0 0 68px rgba(111, 143, 247, 0.56), + inset -12px -14px 26px rgba(10, 20, 50, 0.28); +} + +.orb[data-state="listening"] .orb-ring { + border-color: rgba(139, 167, 255, 0.5); } .orb[data-state="thinking"] .orb-core { - animation-duration: 2s; - filter: saturate(1.1) brightness(1.05); - box-shadow: 0 0 75px 14px color-mix(in srgb, var(--accent-2) 60%, transparent), - inset 0 0 30px rgba(255, 255, 255, 0.2); + background: + radial-gradient(circle at 32% 24%, rgba(255, 255, 255, 0.9) 0 3%, transparent 20%), + radial-gradient(circle at 38% 30%, #f3c9b5, #a783ca 42%, #5d68bd 72%, #29365f 100%); + box-shadow: 0 0 62px rgba(167, 131, 202, 0.48); } .orb[data-state="speaking"] .orb-core { - animation: none; - box-shadow: 0 0 90px 18px color-mix(in srgb, var(--accent-1) 65%, transparent), - inset 0 0 34px rgba(255, 255, 255, 0.22); + box-shadow: + 0 0 0 1px rgba(213, 222, 255, 0.45), + 0 12px 28px rgba(2, 8, 20, 0.34), + 0 0 78px rgba(111, 143, 247, 0.64), + 0 0 105px rgba(255, 179, 142, 0.11); } -@keyframes breathe { - 0%, 100% { transform: scale(calc(var(--pulse-scale) * 1)); } - 50% { transform: scale(calc(var(--pulse-scale) * 1.05)); } +.orb[data-state="speaking"] .orbit-one::after { + background: #ffd0b8; +} + +@keyframes presence-breathe { + 0%, 100% { transform: scale(calc(var(--pulse-scale) * 0.98)); } + 50% { transform: scale(calc(var(--pulse-scale) * 1.045)); } +} + +@keyframes orbit-turn { + to { transform: rotate(342deg); } } .orb-labels { + min-height: 46px; + margin-top: 0.2rem; display: flex; flex-direction: column; align-items: center; - gap: 0.4rem; - margin-top: 1rem; - min-height: 3.6rem; + gap: 0.35rem; } .state-label { - font-size: 0.85rem; + color: var(--ink-soft); + font-size: 0.83rem; font-weight: 600; - letter-spacing: 0.08em; - text-transform: uppercase; - color: var(--text-dim); -} - -.thinking-chip { - font-size: 0.75rem; - padding: 0.25rem 0.75rem; - border-radius: 999px; - background: var(--panel-strong); - border: 1px solid var(--border); - color: var(--text-dim); - animation: chipPulse 1.4s ease-in-out infinite; -} - -@keyframes chipPulse { - 0%, 100% { opacity: 0.55; } - 50% { opacity: 1; } } +.thinking-chip, .barge-hint { - font-size: 0.75rem; - color: var(--text-faint); - opacity: 0.6; + color: var(--ink-muted); + font-size: 0.73rem; } -/* ── Controls ────────────────────────────────────────────────────────────── */ +.thinking-chip::before { + content: ""; + display: inline-block; + width: 5px; + height: 5px; + margin: 0 0.42rem 0.1rem 0; + border-radius: 50%; + background: var(--warm); + vertical-align: middle; + animation: think-pulse 1.2s ease-in-out infinite; +} + +@keyframes think-pulse { + 50% { opacity: 0.28; transform: scale(0.75); } +} + +/* ── Buttons and controls ───────────────────────────────────────────────── */ .controls { + position: relative; + z-index: 2; display: flex; - gap: 0.75rem; - justify-content: center; align-items: center; - margin-bottom: 1.25rem; + justify-content: center; + flex-wrap: wrap; + gap: 0.6rem; + width: 100%; } .btn { - padding: 0.75rem 2rem; - border: none; - border-radius: 8px; - font-size: 1rem; - font-weight: 600; + min-height: 42px; + border: 0; + border-radius: 9px; + padding: 0.68rem 1rem; cursor: pointer; - transition: all 0.2s; - font-family: inherit; + font-size: 0.84rem; + font-weight: 650; + line-height: 1; + transition: color 180ms ease, background 180ms ease, border-color 180ms ease, transform 180ms var(--ease-out), opacity 180ms ease; +} + +.btn:hover:not(:disabled) { + transform: translateY(-1px); +} + +.btn:active:not(:disabled) { + transform: translateY(0); +} + +.btn:focus-visible, +.voice-card:focus-visible { + outline: 2px solid var(--focus); + outline-offset: 3px; } .btn:disabled { - opacity: 0.4; + opacity: 0.38; cursor: not-allowed; } .btn-primary { - background: #3b82f6; - color: white; + position: relative; + min-width: 180px; + color: #07101d; + background: var(--ink); } .btn-primary:hover:not(:disabled) { - background: #2563eb; - transform: translateY(-1px); + background: #dfe7f4; +} + +.button-spark { + display: inline-block; + width: 7px; + height: 7px; + margin-right: 0.5rem; + border-radius: 50%; + background: var(--iris-deep); + box-shadow: 0 0 0 4px rgba(60, 95, 201, 0.12); +} + +.btn-primary:disabled, +.btn-danger:disabled { + display: none; } .btn-danger { - background: var(--danger); - color: white; + color: #fff4f1; + background: #8f403a; } .btn-danger:hover:not(:disabled) { - background: #dc2626; + background: #a84b43; +} + +.btn-secondary { + color: var(--ink-soft); + background: var(--surface-raised); + border: 1px solid var(--line); +} + +.btn-secondary:hover:not(:disabled) { + color: var(--ink); + border-color: var(--line-strong); + background: var(--surface-soft); } .btn-icon { - display: grid; - place-items: center; - width: 44px; - height: 44px; + width: 42px; + min-width: 42px; + height: 42px; padding: 0; - border-radius: 50%; - background: var(--panel); - color: var(--text-dim); - border: 1px solid var(--border); + display: inline-grid; + place-items: center; + color: var(--ink-muted); + background: var(--surface-raised); + border: 1px solid var(--line); } -.btn-icon:hover { - color: var(--text); - background: var(--panel-strong); +.btn-icon:hover:not(:disabled) { + color: var(--ink); + border-color: var(--line-strong); + background: var(--surface-soft); } .btn-icon.muted { - color: #ef4444; - border-color: #ef4444; - background: rgba(239, 68, 68, 0.1); + color: #ffd1ca; + border-color: rgba(255, 128, 111, 0.5); + background: rgba(143, 64, 58, 0.4); } .btn-ghost { + min-height: 34px; + padding: 0.45rem 0.65rem; + color: var(--ink-muted); background: transparent; - color: var(--text-faint); - padding: 0.4rem 0.75rem; - font-size: 0.8rem; - border-radius: 6px; } -.btn-ghost:hover { - color: var(--text); - background: var(--panel-strong); +.btn-ghost:hover:not(:disabled) { + color: var(--ink); + background: var(--surface-raised); +} + +.tasks-trigger { + position: relative; + min-height: 38px; + gap: 0.45rem; + padding: 0 0.8rem; + font-size: 0.78rem; +} + +.tasks-badge { + min-width: 17px; + height: 17px; + display: inline-grid; + place-items: center; + padding: 0 4px; + border-radius: 999px; + color: #07101d; + background: var(--warm); + font-size: 0.64rem; + font-weight: 750; +} + +.connection-strip { + width: min(340px, 100%); + margin-top: 1rem; } -/* ── Status + mic meter ──────────────────────────────────────────────────── */ .status { + min-height: 20px; + color: var(--ink-muted); + font-size: 0.72rem; text-align: center; - padding: 0.75rem; - background: var(--panel); - border: 1px solid var(--border); - border-radius: 8px; - margin-bottom: 0.6rem; - font-size: 0.9rem; - color: var(--text-dim); } .mic-meter { - height: 4px; - background: var(--panel-strong); - border-radius: 2px; - margin-bottom: 1.5rem; + height: 3px; + margin-top: 0.55rem; overflow: hidden; + border-radius: 999px; + background: rgba(139, 167, 255, 0.1); } .mic-meter-fill { display: block; + width: 0; height: 100%; - width: 0%; - background: linear-gradient(90deg, var(--accent-1), var(--accent-2)); - border-radius: 2px; - transition: width 0.1s linear; + border-radius: inherit; + background: var(--iris); + transition: width 80ms linear; } -/* ── Transcript ──────────────────────────────────────────────────────────── */ +.privacy-note { + display: flex; + align-items: center; + gap: 0.4rem; + margin-top: 1.15rem; + color: var(--ink-muted); + font-size: 0.7rem; +} + +.privacy-note svg { + color: var(--mint); +} + +/* ── Conversation ───────────────────────────────────────────────────────── */ .transcript { - background: var(--panel); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - border: 1px solid var(--border); - border-radius: 12px; - padding: 1rem; - max-height: 50vh; - overflow-y: auto; + min-width: 0; + display: flex; + flex-direction: column; + padding: clamp(1.4rem, 3vw, 2.1rem); + border-left: 1px solid var(--line); + background: #0a1524; } .transcript-header { display: flex; align-items: center; justify-content: space-between; - font-weight: 700; - color: var(--text-dim); - margin-bottom: 1rem; - font-size: 0.85rem; - text-transform: uppercase; - letter-spacing: 0.05em; + gap: 1rem; + padding-bottom: 1.1rem; + border-bottom: 1px solid var(--line); +} + +.transcript-header h2 { + color: var(--ink); + font-size: 1rem; + font-weight: 650; + letter-spacing: -0.01em; +} + +.transcript-header p { + margin-top: 0.22rem; + color: var(--ink-muted); + font-size: 0.7rem; +} + +#messages { + flex: 1; + min-height: 0; + overflow-y: auto; + padding: 1.25rem 0.2rem 1rem 0; + scroll-behavior: smooth; +} + +#messages:empty { + display: grid; + place-items: center; +} + +#messages:empty::before { + content: "Your conversation will appear here.\A Start when you’re ready—there’s nothing to configure."; + max-width: 30ch; + color: var(--ink-muted); + font-size: 0.84rem; + line-height: 1.65; + text-align: center; + white-space: pre-wrap; } .message-row { + position: relative; display: flex; align-items: flex-end; - gap: 0.5rem; - margin-bottom: 0.6rem; - position: relative; - animation: msgIn 120ms ease-out; -} - -@keyframes msgIn { - from { opacity: 0; transform: translateY(6px); } - to { opacity: 1; transform: translateY(0); } + gap: 0.55rem; + margin-bottom: 0.85rem; + animation: message-in 180ms var(--ease-out); } .message-row.user { justify-content: flex-end; } +.message-row.system { + justify-content: center; +} + +.message-row.system .bubble { + padding: 0.35rem 0.65rem; + color: var(--ink-muted); + background: transparent; + font-size: 0.7rem; +} + +@keyframes message-in { + from { opacity: 0; transform: translateY(5px); } + to { opacity: 1; transform: translateY(0); } +} + .avatar-dot { - width: 24px; - height: 24px; - border-radius: 50%; - background: linear-gradient(135deg, var(--accent-1), var(--accent-2)); - flex-shrink: 0; + position: relative; + width: 27px; + height: 27px; + flex: 0 0 27px; margin-bottom: 2px; + border: 1px solid rgba(139, 167, 255, 0.38); + border-radius: 50%; +} + +.avatar-dot::after { + content: ""; + position: absolute; + inset: 8px; + border-radius: 50%; + background: var(--iris); } .bubble { - max-width: 78%; - padding: 0.6rem 0.9rem; - border-radius: 14px; - font-size: 0.95rem; - line-height: 1.5; - position: relative; + max-width: min(82%, 58ch); + padding: 0.72rem 0.9rem; + border-radius: 12px; + color: var(--ink-soft); + font-size: 0.9rem; + line-height: 1.55; + overflow-wrap: anywhere; } .message-row.user .bubble { + color: #e8efff; background: var(--user-bubble); - border-bottom-right-radius: 4px; + border-bottom-right-radius: 3px; } .message-row.agent .bubble { background: var(--agent-bubble); - border-bottom-left-radius: 4px; + border-bottom-left-radius: 3px; } +.message-row.partial, .bubble.partial { - opacity: 0.55; + opacity: 0.58; } -/* Timestamps on hover (CSS only, from data-ts attribute) */ .message-row::after { content: attr(data-ts); position: absolute; - bottom: -1.1rem; - font-size: 0.68rem; - color: var(--text-faint); + bottom: -0.9rem; + color: var(--ink-muted); + font-size: 0.62rem; opacity: 0; - transition: opacity 0.15s ease; pointer-events: none; + transition: opacity 150ms ease; } .message-row:hover::after { - opacity: 1; + opacity: 0.75; } -.message-row.user::after { - right: 0; -} +.message-row.user::after { right: 0; } +.message-row.agent::after { left: 2rem; } -.message-row.agent::after { - left: 2rem; -} - -/* ── Agent audio visualizer (bars on latest agent bubble) ─────────────────── */ .audio-viz { + height: 13px; + margin-left: 0.5rem; display: inline-flex; align-items: flex-end; gap: 2px; - height: 14px; - margin-left: 0.6rem; vertical-align: middle; } .audio-viz i { - width: 3px; + width: 2px; height: 15%; border-radius: 2px; - background: linear-gradient(180deg, var(--accent-2), var(--accent-1)); - opacity: 0.8; - transition: height 0.08s linear; + background: var(--iris); + opacity: 0.78; + transition: height 70ms linear; } -/* ── Settings sheet (voice picker) ───────────────────────────────────────── */ +/* ── Voice settings ─────────────────────────────────────────────────────── */ .sheet-backdrop { position: fixed; inset: 0; - background: rgba(0, 0, 0, 0.5); - z-index: 90; + z-index: var(--z-backdrop); + background: rgba(2, 7, 14, 0.68); opacity: 0; - transition: opacity 0.2s ease; + transition: opacity 200ms ease; } .sheet-backdrop:not([hidden]) { @@ -426,21 +822,18 @@ h1 { position: fixed; left: 50%; bottom: 0; - transform: translate(-50%, 100%); - width: 100%; - max-width: 680px; - max-height: 75vh; - background: var(--panel-strong); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - border: 1px solid var(--border); - border-bottom: none; - border-radius: 16px 16px 0 0; + z-index: var(--z-sheet); + width: min(660px, 100%); + max-height: min(76vh, 680px); padding: 0.75rem 1.25rem calc(1.25rem + env(safe-area-inset-bottom)); - z-index: 100; - transition: transform 0.25s cubic-bezier(0.32, 0.72, 0, 1); display: flex; flex-direction: column; + border: 1px solid var(--line-strong); + border-bottom: 0; + border-radius: 16px 16px 0 0; + background: #101e31; + transform: translate(-50%, 102%); + transition: transform 240ms var(--ease-out); } .settings-sheet.open { @@ -449,250 +842,324 @@ h1 { .sheet-handle { width: 36px; - height: 4px; - border-radius: 2px; - background: var(--border); - margin: 0 auto 0.75rem; -} - -.sheet-header { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 1rem; -} - -.sheet-header h2 { - font-size: 1.1rem; - font-weight: 700; -} - -.voice-list { - overflow-y: auto; - display: flex; - flex-direction: column; - gap: 0.5rem; - padding-right: 0.25rem; -} - -.voice-card { - display: flex; - align-items: center; - justify-content: space-between; - gap: 0.75rem; - padding: 0.8rem 1rem; - border-radius: 10px; - background: var(--panel); - border: 1px solid var(--border); - cursor: pointer; - transition: all 0.15s ease; -} - -.voice-card:hover { - background: var(--panel-strong); -} - -.voice-card.active { - border-color: color-mix(in srgb, var(--accent-2) 60%, transparent); - background: var(--agent-bubble); -} - -.voice-name { - font-size: 0.95rem; - font-weight: 500; -} - -/* ── Scrollbar ───────────────────────────────────────────────────────────── */ -.transcript::-webkit-scrollbar, -.voice-list::-webkit-scrollbar { width: 6px; } -.transcript::-webkit-scrollbar-track, -.voice-list::-webkit-scrollbar-track { background: transparent; } -.transcript::-webkit-scrollbar-thumb, -.voice-list::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; } - -/* ── Task panel ──────────────────────────────────────────────────────────── */ -.tasks-fab { - position: fixed; - bottom: calc(1.5rem + env(safe-area-inset-bottom)); - right: 1.5rem; - z-index: 100; -} - -.tasks-badge { - position: absolute; - top: -4px; - right: -4px; - background: #3b82f6; - color: #fff; - font-size: 0.65rem; - font-weight: 700; - min-width: 18px; - height: 18px; - border-radius: 9px; - display: grid; - place-items: center; - padding: 0 4px; -} - -.tasks-panel { - position: fixed; - top: 0; - right: -320px; - width: 320px; - max-width: 85vw; - height: 100dvh; - background: var(--panel); - border-left: 1px solid var(--border); - z-index: 200; - display: flex; - flex-direction: column; - transition: right 0.25s ease; -} - -.tasks-panel.open { - right: 0; + height: 3px; + margin: 0 auto 1rem; + border-radius: 999px; + background: var(--line-strong); } +.sheet-header, .tasks-header { display: flex; align-items: center; justify-content: space-between; - padding: 1rem; - border-bottom: 1px solid var(--border); + gap: 1rem; } +.sheet-header { + padding-bottom: 1rem; +} + +.sheet-header h2, .tasks-header h2 { + color: var(--ink); font-size: 1rem; - margin: 0; + font-weight: 650; +} + +.sheet-header p, +.tasks-header p { + margin-top: 0.25rem; + color: var(--ink-muted); + font-size: 0.72rem; +} + +.voice-list { + min-height: 0; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 0.4rem; + padding: 0.15rem 0.2rem 0 0; +} + +.voice-card { + min-height: 54px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.8rem; + padding: 0.65rem 0.75rem 0.65rem 1rem; + border: 1px solid transparent; + border-radius: 10px; + cursor: pointer; + color: var(--ink-soft); + background: rgba(23, 41, 65, 0.55); + transition: color 160ms ease, background 160ms ease, border-color 160ms ease; +} + +.voice-card:hover { + color: var(--ink); + background: var(--surface-soft); +} + +.voice-card.active { + color: var(--ink); + border-color: rgba(139, 167, 255, 0.42); + background: rgba(60, 95, 201, 0.18); +} + +.voice-name { + font-size: 0.86rem; + font-weight: 600; +} + +.voice-card.active .voice-name::after { + content: "Selected"; + display: inline-block; + margin-left: 0.55rem; + color: var(--iris); + font-size: 0.66rem; + font-weight: 550; +} + +.voice-preview { + min-height: 32px; + color: var(--ink-muted); +} + +.voice-preview::before { + content: "Listen"; +} + +/* ── Tasks ──────────────────────────────────────────────────────────────── */ +.tasks-panel { + position: fixed; + top: 0; + right: 0; + z-index: var(--z-panel); + width: min(380px, 92vw); + height: 100dvh; + display: flex; + flex-direction: column; + border-left: 1px solid var(--line-strong); + background: #0d1929; + transform: translateX(102%); + transition: transform 240ms var(--ease-out); +} + +.tasks-panel.open { + transform: translateX(0); +} + +.tasks-header { + padding: calc(1.1rem + env(safe-area-inset-top)) 1.1rem 1rem; + border-bottom: 1px solid var(--line); } .tasks-list { flex: 1; overflow-y: auto; - padding: 0.75rem; + padding: 0.9rem; display: flex; flex-direction: column; - gap: 0.75rem; + gap: 0.65rem; } .tasks-empty { - color: var(--text-faint); - font-size: 0.85rem; + min-height: 180px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 0.35rem; + color: var(--ink-muted); text-align: center; - margin-top: 2rem; + font-size: 0.78rem; +} + +.tasks-empty strong { + color: var(--ink-soft); + font-size: 0.86rem; } .task-card { - background: var(--panel-strong); - border: 1px solid var(--border); + padding: 0.85rem; border-radius: 10px; - padding: 0.75rem; - font-size: 0.82rem; + color: var(--ink-soft); + background: var(--surface-raised); + font-size: 0.8rem; } .task-card-header { display: flex; align-items: center; gap: 0.5rem; - margin-bottom: 0.4rem; + margin-bottom: 0.45rem; } .task-status-dot { - width: 8px; - height: 8px; + width: 7px; + height: 7px; + flex: 0 0 7px; border-radius: 50%; - background: #6b7280; - flex-shrink: 0; + background: var(--ink-muted); } .task-status-dot.running { - background: #3b82f6; - animation: pulse-dot 1.5s infinite; + background: var(--iris); + animation: status-pulse 1.4s ease-in-out infinite; } -.task-status-dot.completed { - background: #22c55e; -} +.task-status-dot.completed { background: var(--mint); } +.task-status-dot.failed { background: var(--danger); } -.task-status-dot.failed { - background: #ef4444; -} - -@keyframes pulse-dot { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.4; } +@keyframes status-pulse { + 50% { opacity: 0.3; } } .task-desc { - font-weight: 600; - color: var(--text); flex: 1; overflow: hidden; + color: var(--ink); + font-weight: 600; text-overflow: ellipsis; white-space: nowrap; } .task-steps { + max-height: 200px; margin-top: 0.5rem; - padding-left: 0; - list-style: none; + overflow-y: auto; display: flex; flex-direction: column; gap: 0.3rem; - max-height: 200px; - overflow-y: auto; + list-style: none; +} + +.task-step, +.task-result, +.task-error { + padding: 0.42rem 0.5rem; + border-radius: 6px; + font-size: 0.72rem; } .task-step { - font-size: 0.75rem; - color: var(--text-dim); - padding: 0.25rem 0.4rem; - border-radius: 4px; - background: rgba(0,0,0,0.15); + color: var(--ink-muted); + background: rgba(5, 11, 20, 0.3); } -.task-step.tool_call { - color: #60a5fa; -} +.task-step.tool_call { color: #a9bbff; } +.task-step.tool_result { color: var(--ink-muted); } +.task-step.text { color: var(--ink-soft); } +.task-result { margin-top: 0.5rem; color: #bcebd8; background: rgba(118, 217, 178, 0.1); } +.task-error { margin-top: 0.5rem; color: #ffc2ba; background: rgba(255, 128, 111, 0.1); } -.task-step.tool_result { - color: var(--text-faint); -} +/* ── Responsive ─────────────────────────────────────────────────────────── */ +@media (max-width: 880px) { + body { + padding: calc(0.85rem + env(safe-area-inset-top)) + calc(0.85rem + env(safe-area-inset-right)) + calc(0.85rem + env(safe-area-inset-bottom)) + calc(0.85rem + env(safe-area-inset-left)); + } -.task-step.text { - color: var(--text); - font-weight: 500; -} + .workspace { + grid-template-columns: 1fr; + height: auto; + min-height: auto; + } -.task-result { - margin-top: 0.5rem; - padding: 0.5rem; - background: rgba(34, 197, 94, 0.1); - border-radius: 6px; - font-size: 0.8rem; - color: var(--text); -} + .voice-console { + min-height: 620px; + } -.task-error { - margin-top: 0.5rem; - padding: 0.5rem; - background: rgba(239, 68, 68, 0.1); - border-radius: 6px; - font-size: 0.8rem; - color: #fca5a5; + .transcript { + min-height: 430px; + max-height: 62dvh; + border-top: 1px solid var(--line); + border-left: 0; + } } -/* ── Reduced motion ──────────────────────────────────────────────────────── */ +@media (max-width: 560px) { + body { + padding-right: env(safe-area-inset-right); + padding-left: env(safe-area-inset-left); + } + + .topbar { + padding-right: 0.85rem; + padding-left: 0.85rem; + margin-bottom: 0.65rem; + } + + .brand p, + .identity-chip { + display: none; + } + + .workspace { + border-right: 0; + border-left: 0; + border-radius: 0; + } + + .voice-console { + min-height: calc(100dvh - 82px); + padding: 2.1rem 1.15rem 1.25rem; + } + + .console-intro h2 { + font-size: 1.7rem; + } + + .orb-stage { + min-height: 300px; + } + + .orb { + width: 235px; + height: 235px; + transform: scale(0.94); + } + + .controls { + max-width: 330px; + } + + .btn-primary, + .btn-danger { + flex: 1 1 180px; + } + + .privacy-note { + margin-top: 1rem; + } + + .transcript { + min-height: 480px; + max-height: none; + padding: 1.25rem 1rem; + } + + .tasks-panel { + width: 100vw; + } + + .bubble { + max-width: 86%; + } +} + @media (prefers-reduced-motion: reduce) { - .orb-core, - .thinking-chip, - .message-row, - .settings-sheet, - .sheet-backdrop, - .tasks-panel, - .audio-viz i, - .mic-meter-fill { - animation: none !important; - transition: none !important; + *, + *::before, + *::after { + scroll-behavior: auto !important; + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; } }