diff --git a/gui/app.js b/gui/app.js index 06a7b0e..6dd8bda 100644 --- a/gui/app.js +++ b/gui/app.js @@ -1,131 +1,79 @@ -// Gathm AI GUI — app.js -// Connects to the Gathm API server (default: http://127.0.0.1:8080) +// Gathm AI — app.js const API_BASE = window.GATHM_API_URL || 'http://127.0.0.1:8080'; -// Initialize Lucide icons lucide.createIcons(); -// ── Orb state management ─────────────────────────────────────── -const aiOrb = document.getElementById('aiOrb'); +// ── Element refs ─────────────────────────────────────────────── +const aiOrb = document.getElementById('aiOrb'); +const mainOrb = document.getElementById('mainOrb'); +const freqBars = document.getElementById('freqBars'); +const botStatus = document.getElementById('botStatus'); +const chatArea = document.getElementById('chatArea'); +const messageInput = document.getElementById('messageInput'); +const sendBtn = document.getElementById('sendBtn'); +const micBtn = document.getElementById('micBtn'); +// ── Orb state ────────────────────────────────────────────────── function setOrbState(state) { if (aiOrb) aiOrb.className = `ai-orb ${state}`; } -// Mic button → speaking state while held -const micBtn = document.querySelector('.mic-btn-outer'); -if (micBtn) { - const startSpeaking = () => setOrbState('speaking'); - const stopSpeaking = () => setOrbState('idle'); - micBtn.addEventListener('mousedown', startSpeaking); - micBtn.addEventListener('touchstart', startSpeaking, { passive: true }); - micBtn.addEventListener('mouseup', stopSpeaking); - micBtn.addEventListener('touchend', stopSpeaking); - micBtn.addEventListener('mouseleave', () => { - if (aiOrb && aiOrb.classList.contains('speaking')) stopSpeaking(); - }); -} - -// ── Clock ────────────────────────────────────────────────────────── -function updateClock() { - const now = new Date(); - let hours = now.getHours(); - let minutes = now.getMinutes(); - const ampm = hours >= 12 ? 'PM' : 'AM'; - hours = hours % 12 || 12; - minutes = minutes < 10 ? '0' + minutes : minutes; - document.getElementById('clock').textContent = hours + ':' + minutes; -} -setInterval(updateClock, 1000); -updateClock(); - -// ── Connectivity check ───────────────────────────────────────────── -const onlineDot = document.getElementById('onlineDot'); -const botStatus = document.getElementById('botStatus'); +// ── Connectivity ─────────────────────────────────────────────── let isOnline = false; async function checkConnectivity() { try { const res = await fetch(`${API_BASE}/api/v1/health`, { - method: 'GET', signal: AbortSignal.timeout(5000), }); isOnline = res.ok; } catch { isOnline = false; } - updateStatusUI(); + botStatus.textContent = isOnline ? 'Online · Voice & Text' : 'Offline · API not reachable'; } -function updateStatusUI() { - if (isOnline) { - onlineDot.classList.remove('offline'); - botStatus.textContent = 'Online · Voice & Text'; - } else { - onlineDot.classList.add('offline'); - botStatus.textContent = 'Offline · API not reachable'; - } -} - -// Check on load and every 30 seconds checkConnectivity(); setInterval(checkConnectivity, 30000); -// ── Scroll helper ────────────────────────────────────────────────── +// ── Scroll ───────────────────────────────────────────────────── function scrollToBottom() { - const scrollArea = document.getElementById('chatScroll'); - scrollArea.scrollTop = scrollArea.scrollHeight; + chatArea.scrollTop = chatArea.scrollHeight; } -// ── Layout padding for fixed input area ──────────────────────────── -document.addEventListener("DOMContentLoaded", () => { - const inputAreaHeight = document.querySelector('.input-area').offsetHeight; - document.getElementById('chatArea').style.paddingBottom = `${inputAreaHeight + 20}px`; - scrollToBottom(); -}); - -// ── Time formatting ──────────────────────────────────────────────── +// ── Time ─────────────────────────────────────────────────────── function formatTime() { - const now = new Date(); - let hours = now.getHours(); - let minutes = now.getMinutes(); - const ampm = hours >= 12 ? 'PM' : 'AM'; - hours = hours % 12 || 12; - minutes = minutes < 10 ? '0' + minutes : minutes; - return `${hours}:${minutes} ${ampm}`; + const d = new Date(); + let h = d.getHours(), m = d.getMinutes(); + const ampm = h >= 12 ? 'PM' : 'AM'; + h = h % 12 || 12; + return `${h}:${m < 10 ? '0' + m : m} ${ampm}`; } -// ── Message rendering ────────────────────────────────────────────── -const messageInput = document.getElementById('messageInput'); -const sendBtn = document.getElementById('sendBtn'); -const chatArea = document.getElementById('chatArea'); - +// ── Messages ─────────────────────────────────────────────────── function addMessage(text, sender, cssClass) { - const messageWrapper = document.createElement('div'); - messageWrapper.className = `message-wrapper ${sender}`; + const wrapper = document.createElement('div'); + wrapper.className = `message-wrapper ${sender}`; - const messageDiv = document.createElement('div'); - messageDiv.className = `message ${cssClass || sender + '-text'}`; + const msg = document.createElement('div'); + msg.className = `message ${cssClass || sender + '-text'}`; const p = document.createElement('p'); p.textContent = text; - messageDiv.appendChild(p); - - messageWrapper.appendChild(messageDiv); + msg.appendChild(p); + wrapper.appendChild(msg); - const timeDiv = document.createElement('div'); - timeDiv.className = `message-time ${sender}-time`; - timeDiv.textContent = formatTime(); + const time = document.createElement('div'); + time.className = `message-time ${sender}-time`; + time.textContent = formatTime(); - chatArea.appendChild(messageWrapper); - chatArea.appendChild(timeDiv); + chatArea.appendChild(wrapper); + chatArea.appendChild(time); scrollToBottom(); - - lucide.createIcons(); } -// ── Typing indicator ─────────────────────────────────────────────── +// ── Typing indicator ─────────────────────────────────────────── let typingEl = null; function showTyping() { @@ -133,11 +81,9 @@ function showTyping() { const wrapper = document.createElement('div'); wrapper.className = 'message-wrapper bot'; wrapper.id = 'typingWrapper'; - const indicator = document.createElement('div'); indicator.className = 'typing-indicator'; indicator.innerHTML = '
'; - wrapper.appendChild(indicator); chatArea.appendChild(wrapper); scrollToBottom(); @@ -146,20 +92,17 @@ function showTyping() { function hideTyping() { setOrbState('idle'); - if (typingEl) { - typingEl.remove(); - typingEl = null; - } + typingEl?.remove(); + typingEl = null; } -// ── Send message via API ─────────────────────────────────────────── +// ── Send via API ─────────────────────────────────────────────── let isSending = false; async function sendMessage() { const text = messageInput.value.trim(); if (!text || isSending) return; - // Add user message addMessage(text, 'user'); messageInput.value = ''; isSending = true; @@ -182,21 +125,18 @@ async function sendMessage() { } const data = await res.json(); - - // The agent returns either raw_output or structured response - const reply = data.raw_output - || data.output - || data.result + const reply = data.raw_output || data.output || data.result || JSON.stringify(data, null, 2); - addMessage(reply, 'bot'); + } catch (err) { hideTyping(); - if (!isOnline) { - addMessage('Cannot reach the Gathm API. Start the server: gathm-api --port 8080', 'bot', 'bot-error'); - } else { - addMessage(`Connection error: ${err.message}`, 'bot', 'bot-error'); - } + addMessage( + isOnline + ? `Connection error: ${err.message}` + : 'Cannot reach Gathm API. Start the server: gathm-api --port 8080', + 'bot', 'bot-error' + ); } finally { isSending = false; sendBtn.disabled = false; @@ -205,8 +145,86 @@ async function sendMessage() { } sendBtn.addEventListener('click', sendMessage); -messageInput.addEventListener('keypress', (e) => { - if (e.key === 'Enter') { - sendMessage(); +messageInput.addEventListener('keypress', e => { if (e.key === 'Enter') sendMessage(); }); + +// ══════════════════════════════════════════════════════════════ +// Voice mode — Web Audio API drives real frequency visualization +// ══════════════════════════════════════════════════════════════ + +let audioCtx = null; +let analyser = null; +let micStream = null; +let rafId = null; +let voiceActive = false; + +const bars = Array.from(freqBars.querySelectorAll('.fb')); + +async function startVoice() { + try { + micStream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false }); + } catch (err) { + botStatus.textContent = 'Microphone access denied'; + setTimeout(checkConnectivity, 3000); + return; } + + audioCtx = new (window.AudioContext || window.webkitAudioContext)(); + analyser = audioCtx.createAnalyser(); + analyser.fftSize = 64; // 32 frequency bins + analyser.smoothingTimeConstant = 0.75; + + const src = audioCtx.createMediaStreamSource(micStream); + src.connect(analyser); + + voiceActive = true; + aiOrb.setAttribute('data-live', 'true'); + setOrbState('speaking'); + micBtn.classList.add('active'); + botStatus.textContent = 'Listening…'; + + driveFrequency(); +} + +function stopVoice() { + voiceActive = false; + if (rafId) cancelAnimationFrame(rafId); + micStream?.getTracks().forEach(t => t.stop()); + audioCtx?.close(); + audioCtx = null; analyser = null; micStream = null; rafId = null; + + // Reset live transforms + mainOrb.style.transform = ''; + bars.forEach(b => { b.style.height = ''; }); + + aiOrb.removeAttribute('data-live'); + setOrbState('idle'); + micBtn.classList.remove('active'); + checkConnectivity(); +} + +function driveFrequency() { + if (!voiceActive || !analyser) return; + + const data = new Uint8Array(analyser.frequencyBinCount); // 32 values + analyser.getByteFrequencyData(data); + + // Overall energy → orb scale (1.0 – 1.18) + const avg = data.reduce((s, v) => s + v, 0) / data.length; + const scale = 1 + (avg / 255) * 0.18; + mainOrb.style.transform = `scale(${scale.toFixed(4)})`; + + // Per-band energy → bar heights (4 px – 38 px) + const step = Math.max(1, Math.floor(data.length / bars.length)); + bars.forEach((bar, i) => { + const val = data[i * step] ?? 0; + const h = 4 + (val / 255) * 34; + bar.style.height = `${h.toFixed(1)}px`; + }); + + rafId = requestAnimationFrame(driveFrequency); +} + +micBtn.addEventListener('click', () => { + if (voiceActive) stopVoice(); + else startVoice(); }); diff --git a/gui/index.html b/gui/index.html index 785a7ee..f3180df 100644 --- a/gui/index.html +++ b/gui/index.html @@ -3,7 +3,7 @@ - Gathm AI Agent + Gathm AI @@ -11,152 +11,55 @@ -
- -
-
9:41
-
- - - -
-
- - -
-
- -

Gathm AI

-
-
-
- - -
-
- - -
- -
-
- - -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -

Gathm AI Agent

-
Checking...
-
+
- -
- -
-
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
- 0:42 + +
+
+
+
+
+
+
+
+
+
-
10:32 AM
- - -
-
-

Can you explain how neural networks learn?

-
-
-
10:33 AM
- - -
-
-

Neural networks learn through a process called backpropagation. They adjust connection weights based on prediction errors, gradually improving accuracy over many training iterations.

-
-
-
10:33 AM
- - -
-
- 0:15 -
-
-
-
-
-
-
-
-
-
-
-
- -
+
+
+
+
+
+
+
+
+
-
10:35 AM
+

Gathm AI

+

Checking…

- + +
+ +
-
- - +
- -
-
- - Tap to speak
+
diff --git a/gui/styles.css b/gui/styles.css index b8d3475..1a312c8 100644 --- a/gui/styles.css +++ b/gui/styles.css @@ -1,276 +1,147 @@ +/* ═══════════════════════════════════════════════════════════════ + Gathm AI — full-screen orb UI + ═══════════════════════════════════════════════════════════════ */ + :root { - --accent-blue: #4A7BF7; - --accent-glow: #5B8DEF; - --accent-pink: #C471F5; + --accent-blue: #4A7BF7; --accent-purple: #7B5CF6; - --bg-card: #121830; - --bg-card-light: #1A2240; - --bg-dark: #0A0E1A; - --border-subtle: #1E2648; - --input-bg: #141A30; - --msg-bot-bg: #1E2648; - --msg-user-bg: #2A1E48; - --text-muted: #4A5278; - --text-primary: #EAEEFF; - --text-secondary: #7A84A6; + --accent-pink: #C471F5; + --bg: #050810; + --text-primary: #EAEEFF; + --text-secondary:#7A84A6; + --text-muted: #3A4268; + --input-bg: rgba(255,255,255,0.055); + --border: rgba(255,255,255,0.08); + --msg-bot: rgba(255,255,255,0.055); + --msg-user: rgba(100,80,220,0.18); } -* { - box-sizing: border-box; - margin: 0; - padding: 0; +* { box-sizing: border-box; margin: 0; padding: 0; } + +html, body { + height: 100%; + overflow: hidden; } body { - background-color: #000; + background: radial-gradient(ellipse at 50% 0%, #0b0d22 0%, #050810 55%, #020408 100%); color: var(--text-primary); font-family: 'Inter', sans-serif; display: flex; - justify-content: center; - align-items: center; - min-height: 100vh; + align-items: stretch; } -.chat-container { - width: 402px; - height: 874px; - background: linear-gradient(180deg, #060A18 0%, #0A1228 50%, #0E1A3A 100%); - position: relative; - display: flex; - flex-direction: column; - overflow: hidden; - /* Optional: mock device border */ - border-radius: 40px; - border: 8px solid #333; - box-shadow: 0 0 50px rgba(74, 123, 247, 0.2); -} +/* ── App shell ──────────────────────────────────────────────── */ -.scroll-area { - flex: 1; - overflow-y: auto; +.app { display: flex; flex-direction: column; - scrollbar-width: none; /* Firefox */ -} -.scroll-area::-webkit-scrollbar { - display: none; /* Safari and Chrome */ -} - -/* Status Bar */ -.status-bar { width: 100%; - height: 62px; - padding: 0 24px; - display: flex; - justify-content: space-between; - align-items: center; - z-index: 10; -} - -.time { - font-weight: 600; - font-size: 16px; -} - -.status-icons { - display: flex; - gap: 6px; - align-items: center; -} - -.icon-small { - width: 16px; - height: 16px; - color: var(--text-primary); -} - -.icon-medium { - width: 20px; - height: 16px; - color: var(--text-primary); -} - -/* Header */ -.header { - width: 100%; - height: 56px; - padding: 0 20px; - display: flex; - justify-content: space-between; - align-items: center; - z-index: 10; -} - -.header-left, .header-right { - display: flex; - gap: 12px; - align-items: center; -} - -.header-right { - gap: 16px; -} - -.icon { - width: 24px; - height: 24px; - color: var(--text-primary); - cursor: pointer; -} - -.icon-secondary { - width: 22px; - height: 22px; - color: var(--text-secondary); - cursor: pointer; -} - -.header-title { - font-family: 'Plus Jakarta Sans', sans-serif; - font-weight: 700; - font-size: 20px; -} - -.online-dot { - width: 8px; - height: 8px; - background-color: #22C55E; - border-radius: 50%; - transition: background-color 0.3s; + max-width: 680px; + margin: 0 auto; + height: 100%; + position: relative; } -.online-dot.offline { - background-color: #EF4444; -} +/* ── Orb scene ──────────────────────────────────────────────── */ -/* Typing indicator */ -.typing-indicator { +.orb-scene { display: flex; - gap: 4px; + flex-direction: column; align-items: center; - padding: 12px 16px; - background-color: var(--msg-bot-bg); - border-radius: 20px 20px 20px 4px; - max-width: 70px; -} - -.typing-indicator .dot { - width: 6px; - height: 6px; - background-color: var(--text-secondary); - border-radius: 50%; - animation: typingBounce 1.4s infinite ease-in-out both; -} - -.typing-indicator .dot:nth-child(1) { animation-delay: 0s; } -.typing-indicator .dot:nth-child(2) { animation-delay: 0.2s; } -.typing-indicator .dot:nth-child(3) { animation-delay: 0.4s; } - -@keyframes typingBounce { - 0%, 80%, 100% { transform: scale(0.6); opacity: 0.4; } - 40% { transform: scale(1); opacity: 1; } -} - -/* Error message style */ -.bot-error { - background-color: rgba(239, 68, 68, 0.15); - border: 1px solid rgba(239, 68, 68, 0.3); - border-radius: 20px 20px 20px 4px; + padding: clamp(24px, 5vh, 52px) 0 clamp(12px, 2vh, 24px); + flex-shrink: 0; + position: relative; } -.bot-error p { - color: #FCA5A5; -} +/* ── AI Orb root ────────────────────────────────────────────── */ -/* Disabled send button */ -.send-btn:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -/* Avatar Section */ -.avatar-section { - width: 100%; - padding: 16px 0 10px; +.ai-orb { position: relative; display: flex; flex-direction: column; align-items: center; - justify-content: center; - gap: 6px; - margin-top: -20px; + gap: 10px; } -.glow-bg { +/* Ambient background glow — state-driven */ +.orb-ambient { position: absolute; - width: 200px; - height: 200px; - background: radial-gradient(50% 50% at 50% 50%, rgba(74, 123, 247, 0.22) 0%, rgba(123, 92, 246, 0.1) 50%, rgba(0, 0, 0, 0) 100%); - filter: blur(28px); + width: 420px; + height: 420px; + top: 50%; + left: 50%; + transform: translate(-50%, -44%); + background: radial-gradient(circle, rgba(74,100,255,0.11) 0%, transparent 68%); + filter: blur(48px); + pointer-events: none; z-index: 0; - top: 0; + transition: opacity 0.6s ease; } -/* ─── AI Orb ─────────────────────────────────────────────────── */ +.ai-orb.thinking .orb-ambient { + animation: ambientPulse 1.6s ease-in-out infinite; +} -.ai-orb { - position: relative; - display: flex; - flex-direction: column; - align-items: center; - gap: 8px; - z-index: 1; +.ai-orb.speaking .orb-ambient { + background: radial-gradient(circle, rgba(100,80,255,0.18) 0%, rgba(200,50,50,0.06) 50%, transparent 68%); + animation: ambientPulse 0.9s ease-in-out infinite; +} + +@keyframes ambientPulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 1.6; } /* filter+opacity combo creates bloom */ } -/* ── Orb wrapper – rings share the same space as the sphere ── */ +/* ── Orb wrapper — hosts rings + sphere ─────────────────────── */ .orb-wrapper { position: relative; - width: 130px; - height: 130px; + width: clamp(160px, 28vmin, 220px); + height: clamp(160px, 28vmin, 220px); display: flex; align-items: center; justify-content: center; flex-shrink: 0; + z-index: 1; } -/* ── Main sphere ─────────────────────────────────────────────── */ +/* ── Main sphere ────────────────────────────────────────────── */ .main-orb { - width: 130px; - height: 130px; + width: 100%; + height: 100%; border-radius: 50%; position: relative; overflow: hidden; will-change: transform; - /* Iridescent sphere — matches the reference image */ background: - radial-gradient(circle at 68% 18%, rgba(255,255,255,0.48) 0%, rgba(255,255,255,0.12) 4%, transparent 9%), - radial-gradient(circle at 20% 25%, rgba(0, 228, 255, 0.92) 0%, rgba(0, 178, 232, 0.55) 16%, transparent 37%), - radial-gradient(circle at 67% 28%, rgba(80, 42, 255, 1) 0%, rgba(45, 8, 216, 0.72) 21%, transparent 45%), - radial-gradient(circle at 38% 63%, rgba(215, 50, 50, 0.95) 0%, rgba(168, 22, 22, 0.52) 23%, transparent 45%), - radial-gradient(circle at 60% 76%, rgba(0, 174, 212, 0.62) 0%, transparent 28%), - radial-gradient(circle at 74% 68%, rgba(180, 198, 255, 0.12) 0%, transparent 18%), + radial-gradient(circle at 68% 18%, rgba(255,255,255,0.48) 0%, rgba(255,255,255,0.12) 4%, transparent 9%), + radial-gradient(circle at 20% 25%, rgba(0, 228,255,0.92) 0%, rgba(0, 178,232,0.55) 16%, transparent 37%), + radial-gradient(circle at 67% 28%, rgba(80, 42,255,1) 0%, rgba(45, 8,216,0.72) 21%, transparent 45%), + radial-gradient(circle at 38% 63%, rgba(215, 50, 50,0.95) 0%, rgba(168, 22, 22,0.52) 23%, transparent 45%), + radial-gradient(circle at 60% 76%, rgba(0, 174,212,0.62) 0%, transparent 28%), + radial-gradient(circle at 74% 68%, rgba(180,198,255,0.12) 0%, transparent 18%), radial-gradient(ellipse at 48% 44%, #060025 0%, #0e0052 24%, #070028 55%, #020010 82%, #010006 100%); box-shadow: - 0 0 48px rgba(60, 100, 255, 0.26), - 0 0 96px rgba(60, 100, 255, 0.10); + 0 0 55px rgba(60,100,255,0.28), + 0 0 110px rgba(60,100,255,0.10); } -/* Dark glass rim around the edge */ +/* Glass rim */ .main-orb::after { content: ''; position: absolute; inset: 0; border-radius: 50%; - background: radial-gradient(circle at 50% 50%, transparent 62%, rgba(4, 3, 18, 0.92) 88%, rgba(2, 1, 9, 1) 100%); + background: radial-gradient(circle at 50% 50%, transparent 62%, rgba(4,3,18,0.92) 88%, rgba(2,1,9,1) 100%); pointer-events: none; z-index: 3; } -/* Continuously rotating swirl overlay */ +/* Rotating swirl */ .orb-swirl { position: absolute; width: 200%; @@ -280,11 +151,11 @@ body { background: conic-gradient( from 0deg at 40% 55%, transparent 0deg, - rgba(0, 200, 255, 0.2) 35deg, + rgba(0, 200,255,0.20) 35deg, transparent 75deg, - rgba(200, 45, 45, 0.2) 155deg, + rgba(200,45, 45,0.20) 155deg, transparent 200deg, - rgba(90, 55, 255, 0.15) 270deg, + rgba(90, 55,255,0.16) 270deg, transparent 320deg ); z-index: 1; @@ -306,346 +177,286 @@ body { pointer-events: none; } -/* ── Expanding rings (speaking state) ──────────────────────── */ +/* ── Expanding sound-wave rings ──────────────────────────────── */ .orb-ring { position: absolute; inset: 0; border-radius: 50%; - border: 1.5px solid rgba(74, 120, 255, 0.5); + border: 1.5px solid rgba(74,120,255,0.45); opacity: 0; pointer-events: none; } -/* ── Frequency bars (speaking state) ───────────────────────── */ +/* ── Frequency bars ──────────────────────────────────────────── */ .freq-bars { display: flex; - gap: 3px; + gap: 4px; align-items: flex-end; - height: 22px; + height: 40px; opacity: 0; transition: opacity 0.35s ease; pointer-events: none; + z-index: 1; } .fb { - width: 3px; - height: 3px; + width: 4px; + height: 4px; background: linear-gradient(to top, var(--accent-blue), var(--accent-purple)); - border-radius: 2px; + border-radius: 3px; --bar-h: 14px; } -/* ── Keyframes ─────────────────────────────────────────────── */ +/* ══════════════════════════════════════════════════════════════ + Keyframes + ══════════════════════════════════════════════════════════════ */ @keyframes orbSwirl { to { transform: rotate(360deg); } } -@keyframes idleBreath { - 0%, 100% { transform: scale(1); } - 50% { transform: scale(1.035); } +/* Organic float — 11 s, irregular stops */ +@keyframes orbFloat { + 0% { transform: translateY(0px); } + 28% { transform: translateY(-9px); } + 55% { transform: translateY(-4px); } + 78% { transform: translateY(-11px); } + 100% { transform: translateY(0px); } } -@keyframes thinkingGlow { +/* Organic breath — 7 s, irregular scale */ +@keyframes orbBreath { + 0% { transform: scale(1); } + 20% { transform: scale(1.055); } + 42% { transform: scale(1.02); } + 65% { transform: scale(1.045); } + 83% { transform: scale(1.015); } + 100% { transform: scale(1); } +} + +/* Thinking — faster glow pulse */ +@keyframes orbThink { 0%, 100% { transform: scale(1); - box-shadow: 0 0 48px rgba(60,100,255,0.26), 0 0 96px rgba(60,100,255,0.10); + box-shadow: 0 0 55px rgba(60,100,255,0.28), 0 0 110px rgba(60,100,255,0.10); } 50% { - transform: scale(1.065); - box-shadow: 0 0 72px rgba(90,140,255,0.46), 0 0 136px rgba(90,140,255,0.18); + transform: scale(1.07); + box-shadow: 0 0 80px rgba(90,140,255,0.50), 0 0 150px rgba(90,140,255,0.18); } } -@keyframes speakingPulse { +/* Speaking — irregular amplitude */ +@keyframes orbSpeak { 0% { transform: scale(1); } - 8% { transform: scale(1.08); } + 8% { transform: scale(1.09); } 15% { transform: scale(1.03); } - 23% { transform: scale(1.11); } + 23% { transform: scale(1.12); } 31% { transform: scale(1.05); } - 38% { transform: scale(1.13); } + 38% { transform: scale(1.14); } 46% { transform: scale(1.04); } - 54% { transform: scale(1.09); } + 54% { transform: scale(1.10); } 62% { transform: scale(1.02); } - 69% { transform: scale(1.10); } - 77% { transform: scale(1.06); } - 85% { transform: scale(1.12); } - 92% { transform: scale(1.03); } + 70% { transform: scale(1.11); } + 78% { transform: scale(1.06); } + 85% { transform: scale(1.13); } + 93% { transform: scale(1.03); } 100% { transform: scale(1); } } @keyframes ringExpand { - 0% { transform: scale(1); opacity: 0.55; } - 100% { transform: scale(1.9); opacity: 0; } + 0% { transform: scale(1); opacity: 0.50; } + 100% { transform: scale(2.1); opacity: 0; } } @keyframes freqBar { - from { height: 3px; } + from { height: 4px; } to { height: var(--bar-h); } } -/* ── State: idle ──────────────────────────────────────────── */ +/* ══════════════════════════════════════════════════════════════ + State classes + Two separate elements for float+scale so transforms don't collide. + Float (11 s) × Breath (7 s) → LCM 77 s of apparent randomness. + ══════════════════════════════════════════════════════════════ */ +/* idle */ +.ai-orb.idle .orb-wrapper { + animation: orbFloat 11s cubic-bezier(0.45,0.05,0.55,0.95) infinite; +} .ai-orb.idle .main-orb { - animation: idleBreath 5s ease-in-out infinite; + animation: orbBreath 7s cubic-bezier(0.4,0,0.6,1) infinite; +} +.ai-orb.idle .orb-swirl { + animation-duration: 10s; } -/* ── State: thinking (text chat — ChatGPT/Gemini style) ────── */ - +/* thinking */ +.ai-orb.thinking .orb-wrapper { + animation: orbFloat 11s cubic-bezier(0.45,0.05,0.55,0.95) infinite; +} .ai-orb.thinking .main-orb { - animation: thinkingGlow 1.6s ease-in-out infinite; + animation: orbThink 1.7s ease-in-out infinite; } - .ai-orb.thinking .orb-swirl { animation-duration: 4s; } -/* ── State: speaking (voice — frequency visualization) ──────── */ - -.ai-orb.speaking .main-orb { - animation: speakingPulse 2.2s ease-in-out infinite; +/* speaking — CSS fallback (no live mic) */ +.ai-orb.speaking:not([data-live]) .main-orb { + animation: orbSpeak 2.2s ease-in-out infinite; } - -.ai-orb.speaking .orb-swirl { +.ai-orb.speaking:not([data-live]) .orb-swirl { animation-duration: 2.5s; } +/* speaking — live mic drives transforms via JS */ +.ai-orb.speaking[data-live] .main-orb { animation: none; } +.ai-orb.speaking[data-live] .orb-swirl { animation-duration: 2s; } + +/* rings */ .ai-orb.speaking .orb-ring { animation: ringExpand 2s ease-out infinite; } .ai-orb.speaking .ring-2 { animation-delay: 0.67s; } .ai-orb.speaking .ring-3 { animation-delay: 1.33s; } -.ai-orb.speaking .freq-bars { - opacity: 1; -} +/* freq bars */ +.ai-orb.speaking .freq-bars { opacity: 1; } -.ai-orb.speaking .fb { +.ai-orb.speaking:not([data-live]) .fb { animation: freqBar 0.5s ease-in-out infinite alternate; } -/* Unique speed + height per bar for realistic waveform look */ -.ai-orb.speaking .fb:nth-child(1) { animation-duration: 0.52s; --bar-h: 10px; } -.ai-orb.speaking .fb:nth-child(2) { animation-duration: 0.36s; animation-delay: 0.06s; --bar-h: 18px; } -.ai-orb.speaking .fb:nth-child(3) { animation-duration: 0.68s; animation-delay: 0.12s; --bar-h: 12px; } -.ai-orb.speaking .fb:nth-child(4) { animation-duration: 0.42s; animation-delay: 0.08s; --bar-h: 20px; } -.ai-orb.speaking .fb:nth-child(5) { animation-duration: 0.58s; animation-delay: 0.16s; --bar-h: 15px; } -.ai-orb.speaking .fb:nth-child(6) { animation-duration: 0.33s; animation-delay: 0.04s; --bar-h: 20px; } -.ai-orb.speaking .fb:nth-child(7) { animation-duration: 0.62s; animation-delay: 0.10s; --bar-h: 11px; } -.ai-orb.speaking .fb:nth-child(8) { animation-duration: 0.45s; animation-delay: 0.14s; --bar-h: 17px; } +.ai-orb.speaking:not([data-live]) .fb:nth-child(1) { animation-duration: 0.52s; --bar-h: 10px; } +.ai-orb.speaking:not([data-live]) .fb:nth-child(2) { animation-duration: 0.36s; animation-delay:0.06s; --bar-h: 22px; } +.ai-orb.speaking:not([data-live]) .fb:nth-child(3) { animation-duration: 0.68s; animation-delay:0.12s; --bar-h: 14px; } +.ai-orb.speaking:not([data-live]) .fb:nth-child(4) { animation-duration: 0.42s; animation-delay:0.08s; --bar-h: 34px; } +.ai-orb.speaking:not([data-live]) .fb:nth-child(5) { animation-duration: 0.58s; animation-delay:0.16s; --bar-h: 18px; } +.ai-orb.speaking:not([data-live]) .fb:nth-child(6) { animation-duration: 0.33s; animation-delay:0.04s; --bar-h: 36px; } +.ai-orb.speaking:not([data-live]) .fb:nth-child(7) { animation-duration: 0.62s; animation-delay:0.10s; --bar-h: 12px; } +.ai-orb.speaking:not([data-live]) .fb:nth-child(8) { animation-duration: 0.45s; animation-delay:0.14s; --bar-h: 26px; } -/* ─────────────────────────────────────────────────────────────── */ +/* ── Orb labels ─────────────────────────────────────────────── */ -.bot-name { +.orb-name { font-family: 'Plus Jakarta Sans', sans-serif; font-weight: 700; - font-size: 16px; - z-index: 1; + font-size: clamp(18px, 2.5vw, 22px); + margin-top: 6px; + letter-spacing: -0.3px; } -.bot-status { +.orb-status { font-size: 13px; color: var(--text-secondary); - z-index: 1; } -/* Chat Area */ -.chat-area { - width: 100%; - padding: 8px 20px 20px; +/* ── Messages area ──────────────────────────────────────────── */ + +.messages-area { + flex: 1; + overflow-y: auto; + padding: 8px 24px 12px; display: flex; flex-direction: column; - gap: 10px; - justify-content: flex-end; + gap: 6px; + scrollbar-width: none; } +.messages-area::-webkit-scrollbar { display: none; } .message-wrapper { display: flex; width: 100%; } - -.message-wrapper.bot { - justify-content: flex-start; -} - -.message-wrapper.user { - justify-content: flex-end; -} +.message-wrapper.bot { justify-content: flex-start; } +.message-wrapper.user { justify-content: flex-end; } .message { - padding: 12px 16px; - max-width: 85%; - display: flex; + padding: 10px 16px; + max-width: 82%; + border-radius: 18px; } - .message p { font-size: 14px; - line-height: 1.5; + line-height: 1.55; } -.bot-voice { - background-color: var(--msg-bot-bg); - border-radius: 20px 20px 20px 4px; - gap: 10px; - align-items: center; +.bot-text { + background: var(--msg-bot); + border-radius: 4px 18px 18px 18px; } - .user-text { - background-color: var(--msg-user-bg); - border-radius: 20px 4px 20px 20px; + background: var(--msg-user); + border-radius: 18px 4px 18px 18px; } - -.bot-text { - background-color: var(--msg-bot-bg); - border-radius: 20px 20px 20px 4px; -} - -.user-voice { - background-color: var(--msg-user-bg); - border-radius: 20px 4px 20px 20px; - gap: 10px; - align-items: center; +.bot-error { + background: rgba(239,68,68,0.12); + border: 1px solid rgba(239,68,68,0.25); + border-radius: 4px 18px 18px 18px; } +.bot-error p { color: #FCA5A5; } .message-time { font-size: 11px; color: var(--text-muted); + margin-top: -2px; + margin-bottom: 6px; } +.bot-time { text-align: left; margin-left: 6px; } +.user-time { text-align: right; margin-right: 6px; } -.bot-time { - text-align: left; - margin-left: 4px; - margin-top: -4px; - margin-bottom: 8px; -} - -.user-time { - text-align: right; - margin-right: 4px; - margin-top: -4px; - margin-bottom: 8px; -} - -/* Audio Player Elements */ -.play-btn { - width: 32px; - height: 32px; - border-radius: 50%; +/* Typing dots */ +.typing-indicator { display: flex; - justify-content: center; + gap: 5px; align-items: center; - border: none; - cursor: pointer; -} - -.play-btn.accent-blue { - background-color: var(--accent-blue); + padding: 12px 16px; + background: var(--msg-bot); + border-radius: 4px 18px 18px 18px; } - -.play-btn.accent-purple { - background-color: var(--accent-purple); +.typing-indicator .dot { + width: 6px; height: 6px; + background: var(--text-secondary); + border-radius: 50%; + animation: typingBounce 1.4s infinite ease-in-out both; } - -.play-icon { - width: 14px; - height: 14px; - color: #fff; - margin-left: 2px; /* optical alignment */ +.typing-indicator .dot:nth-child(1) { animation-delay: 0s; } +.typing-indicator .dot:nth-child(2) { animation-delay: 0.2s; } +.typing-indicator .dot:nth-child(3) { animation-delay: 0.4s; } +@keyframes typingBounce { + 0%, 80%, 100% { transform: scale(0.6); opacity: 0.4; } + 40% { transform: scale(1); opacity: 1; } } -.wave { - display: flex; - gap: 2px; - align-items: center; - height: 24px; -} - -.wave .bar { - width: 3px; - background-color: var(--accent-blue); - border-radius: 2px; -} -/* Individual bar heights for bot voice */ -.wave#wave1 .bar-1 { height: 8px; opacity: 0.5; } -.wave#wave1 .bar-2 { height: 16px; opacity: 0.6; } -.wave#wave1 .bar-3 { height: 22px; opacity: 1; } -.wave#wave1 .bar-4 { height: 12px; opacity: 0.75; } -.wave#wave1 .bar-5 { height: 18px; opacity: 1; } -.wave#wave1 .bar-6 { height: 8px; opacity: 0.5; } -.wave#wave1 .bar-7 { height: 14px; opacity: 0.6; } -.wave#wave1 .bar-8 { height: 20px; opacity: 1; } -.wave#wave1 .bar-9 { height: 10px; opacity: 0.56; } -.wave#wave1 .bar-10 { height: 16px; opacity: 0.69; } -.wave#wave1 .bar-11 { height: 6px; opacity: 0.44; } -.wave#wave1 .bar-12 { height: 12px; opacity: 0.56; } - -/* Individual bar heights for user voice */ -.wave#wave2 { height: 20px; } -.wave#wave2 .bar { background-color: var(--accent-pink); } -.wave#wave2 .bar-1 { height: 6px; opacity: 0.44; } -.wave#wave2 .bar-2 { height: 14px; opacity: 0.63; } -.wave#wave2 .bar-3 { height: 18px; opacity: 1; } -.wave#wave2 .bar-4 { height: 10px; opacity: 0.69; } -.wave#wave2 .bar-5 { height: 16px; opacity: 1; } -.wave#wave2 .bar-6 { height: 8px; opacity: 0.5; } -.wave#wave2 .bar-7 { height: 14px; opacity: 0.63; } -.wave#wave2 .bar-8 { height: 20px; opacity: 1; } -.wave#wave2 .bar-9 { height: 6px; opacity: 0.44; } -.wave#wave2 .bar-10 { height: 12px; opacity: 0.56; } - -.duration { - font-size: 12px; - font-weight: 500; - color: var(--text-secondary); -} +/* ── Input area ─────────────────────────────────────────────── */ -/* Input Area */ .input-area { - width: 100%; - padding: 16px 20px 32px; - display: flex; - flex-direction: column; - gap: 16px; - align-items: center; - background: linear-gradient(180deg, rgba(10, 18, 40, 0) 0%, rgba(10, 18, 40, 1) 40%); - position: absolute; - bottom: 0; - left: 0; - z-index: 20; + padding: clamp(12px,2vh,20px) 20px clamp(16px,3vh,32px); + background: linear-gradient(to top, var(--bg) 55%, transparent); + flex-shrink: 0; } .input-row { - width: 100%; - height: 52px; display: flex; - gap: 12px; - align-items: center; -} - -.icon-btn { - width: 44px; - height: 44px; - border-radius: 22px; - background-color: var(--bg-card-light); - border: none; - display: flex; - justify-content: center; + gap: 10px; align-items: center; - cursor: pointer; - flex-shrink: 0; } .text-input-wrapper { flex: 1; - height: 48px; - background-color: var(--input-bg); - border-radius: 24px; - border: 1px solid var(--border-subtle); + height: 50px; + background: var(--input-bg); + border: 1px solid var(--border); + border-radius: 25px; + padding: 0 20px; display: flex; - justify-content: space-between; align-items: center; - padding: 0 16px; + transition: border-color 0.2s; +} +.text-input-wrapper:focus-within { + border-color: rgba(74,120,255,0.4); } .text-input-wrapper input { @@ -657,89 +468,45 @@ body { width: 100%; outline: none; } - .text-input-wrapper input::placeholder { color: var(--text-muted); } -.inside-input { - width: 20px; - height: 20px; - margin-left: 8px; -} - -.send-btn { - width: 44px; - height: 44px; - border-radius: 22px; - background: linear-gradient(180deg, var(--accent-blue) 0%, var(--accent-purple) 100%); +.action-btn { + width: 46px; + height: 46px; + border-radius: 50%; border: none; display: flex; - justify-content: center; align-items: center; - box-shadow: 0 4px 16px 2px rgba(74, 123, 247, 0.25); + justify-content: center; cursor: pointer; flex-shrink: 0; - transition: transform 0.2s, box-shadow 0.2s; -} - -.send-btn:active { - transform: scale(0.95); -} - -.icon-white { - width: 20px; - height: 20px; - color: #fff; - margin-left: -2px; - margin-top: 2px; + transition: transform 0.15s, box-shadow 0.2s, background 0.2s; } +.action-btn:active { transform: scale(0.92); } -/* Mic Section */ -.mic-section { - display: flex; - flex-direction: column; - gap: 8px; - align-items: center; - margin-top: 5px; -} - -.mic-btn-outer { - width: 64px; - height: 64px; - border-radius: 32px; - background: radial-gradient(50% 50% at 50% 50%, rgba(74, 123, 247, 0.18) 0%, rgba(123, 92, 246, 0.06) 100%); - border: 2px solid transparent; /* Replaced by background-clip hack or linear-gradient border */ - background-clip: padding-box; - position: relative; - display: flex; - justify-content: center; - align-items: center; - cursor: pointer; - transition: transform 0.2s; +.send-btn { + background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple)); + box-shadow: 0 4px 18px rgba(74,123,247,0.3); } - -.mic-btn-outer::before { - content: ''; - position: absolute; - top: -2px; right: -2px; bottom: -2px; left: -2px; - z-index: -1; - border-radius: 32px; - background: linear-gradient(180deg, var(--accent-blue) 0%, var(--accent-purple) 100%); +.send-btn:disabled { + opacity: 0.45; + cursor: not-allowed; } -.mic-btn-outer:active { - transform: scale(0.95); +.mic-btn { + background: rgba(255,255,255,0.07); + border: 1px solid var(--border); } - -.mic-icon-large { - width: 28px; - height: 28px; - color: var(--accent-blue); +.mic-btn.active { + background: linear-gradient(135deg, rgba(200,50,50,0.6), rgba(150,30,150,0.6)); + border-color: rgba(200,80,80,0.4); + box-shadow: 0 0 20px rgba(200,50,50,0.25); } -.mic-label { - font-size: 12px; - font-weight: 500; - color: var(--text-muted); +.btn-icon { + width: 18px; + height: 18px; + color: #fff; }