Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions gui/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,27 @@ 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');

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();
Expand Down Expand Up @@ -108,6 +129,7 @@ function addMessage(text, sender, cssClass) {
let typingEl = null;

function showTyping() {
setOrbState('thinking');
const wrapper = document.createElement('div');
wrapper.className = 'message-wrapper bot';
wrapper.id = 'typingWrapper';
Expand All @@ -123,6 +145,7 @@ function showTyping() {
}

function hideTyping() {
setOrbState('idle');
if (typingEl) {
typingEl.remove();
typingEl = null;
Expand Down
25 changes: 23 additions & 2 deletions gui/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,30 @@ <h1 class="header-title">Gathm AI</h1>
<!-- Avatar Section -->
<div class="avatar-section">
<div class="glow-bg"></div>
<div class="avatar">
<i data-lucide="bot" class="avatar-icon"></i>

<!-- Animated AI Orb -->
<div class="ai-orb idle" id="aiOrb">
<div class="orb-wrapper">
<div class="orb-ring ring-1"></div>
<div class="orb-ring ring-2"></div>
<div class="orb-ring ring-3"></div>
<div class="main-orb">
<div class="orb-swirl"></div>
<div class="orb-gloss"></div>
</div>
</div>
<div class="freq-bars">
<div class="fb"></div>
<div class="fb"></div>
<div class="fb"></div>
<div class="fb"></div>
<div class="fb"></div>
<div class="fb"></div>
<div class="fb"></div>
<div class="fb"></div>
</div>
</div>

<h2 class="bot-name">Gathm AI Agent</h2>
<div class="bot-status" id="botStatus">Checking...</div>
</div>
Expand Down
245 changes: 226 additions & 19 deletions gui/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -191,50 +191,257 @@ body {
/* Avatar Section */
.avatar-section {
width: 100%;
height: 180px;
padding: 16px 0 10px;
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
margin-top: -20px;
}

.glow-bg {
position: absolute;
width: 140px;
height: 140px;
background: radial-gradient(50% 50% at 50% 50%, rgba(74, 123, 247, 0.25) 0%, rgba(123, 92, 246, 0.12) 50%, rgba(0, 0, 0, 0) 100%);
filter: blur(20px);
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);
z-index: 0;
top: 10px;
top: 0;
}

.avatar {
width: 80px;
height: 80px;
background: linear-gradient(180deg, var(--accent-blue) 0%, var(--accent-purple) 100%);
border-radius: 50%;
/* ─── AI Orb ─────────────────────────────────────────────────── */

.ai-orb {
position: relative;
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
gap: 8px;
z-index: 1;
box-shadow: 0 0 30px 5px rgba(74, 123, 247, 0.3);
margin-bottom: 15px;
}

.avatar-icon {
width: 40px;
height: 40px;
color: #fff;
/* ── Orb wrapper – rings share the same space as the sphere ── */

.orb-wrapper {
position: relative;
width: 130px;
height: 130px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}

/* ── Main sphere ─────────────────────────────────────────────── */

.main-orb {
width: 130px;
height: 130px;
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(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);
}

/* Dark glass rim around the edge */
.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%);
pointer-events: none;
z-index: 3;
}

/* Continuously rotating swirl overlay */
.orb-swirl {
position: absolute;
width: 200%;
height: 200%;
top: -50%;
left: -50%;
background: conic-gradient(
from 0deg at 40% 55%,
transparent 0deg,
rgba(0, 200, 255, 0.2) 35deg,
transparent 75deg,
rgba(200, 45, 45, 0.2) 155deg,
transparent 200deg,
rgba(90, 55, 255, 0.15) 270deg,
transparent 320deg
);
z-index: 1;
animation: orbSwirl 10s linear infinite;
will-change: transform;
}

/* Static gloss highlight */
.orb-gloss {
position: absolute;
width: 42%;
height: 32%;
top: 10%;
left: 12%;
background: radial-gradient(ellipse, rgba(255,255,255,0.24) 0%, rgba(255,255,255,0.07) 50%, transparent 100%);
border-radius: 50%;
z-index: 2;
transform: rotate(-22deg);
pointer-events: none;
}

/* ── Expanding rings (speaking state) ──────────────────────── */

.orb-ring {
position: absolute;
inset: 0;
border-radius: 50%;
border: 1.5px solid rgba(74, 120, 255, 0.5);
opacity: 0;
pointer-events: none;
}

/* ── Frequency bars (speaking state) ───────────────────────── */

.freq-bars {
display: flex;
gap: 3px;
align-items: flex-end;
height: 22px;
opacity: 0;
transition: opacity 0.35s ease;
pointer-events: none;
}

.fb {
width: 3px;
height: 3px;
background: linear-gradient(to top, var(--accent-blue), var(--accent-purple));
border-radius: 2px;
--bar-h: 14px;
}

/* ── Keyframes ─────────────────────────────────────────────── */

@keyframes orbSwirl {
to { transform: rotate(360deg); }
}

@keyframes idleBreath {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.035); }
}

@keyframes thinkingGlow {
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);
}
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);
}
}

@keyframes speakingPulse {
0% { transform: scale(1); }
8% { transform: scale(1.08); }
15% { transform: scale(1.03); }
23% { transform: scale(1.11); }
31% { transform: scale(1.05); }
38% { transform: scale(1.13); }
46% { transform: scale(1.04); }
54% { transform: scale(1.09); }
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); }
100% { transform: scale(1); }
}

@keyframes ringExpand {
0% { transform: scale(1); opacity: 0.55; }
100% { transform: scale(1.9); opacity: 0; }
}

@keyframes freqBar {
from { height: 3px; }
to { height: var(--bar-h); }
}

/* ── State: idle ──────────────────────────────────────────── */

.ai-orb.idle .main-orb {
animation: idleBreath 5s ease-in-out infinite;
}

/* ── State: thinking (text chat — ChatGPT/Gemini style) ────── */

.ai-orb.thinking .main-orb {
animation: thinkingGlow 1.6s 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;
}

.ai-orb.speaking .orb-swirl {
animation-duration: 2.5s;
}

.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;
}

.ai-orb.speaking .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; }

/* ─────────────────────────────────────────────────────────────── */

.bot-name {
font-family: 'Plus Jakarta Sans', sans-serif;
font-weight: 700;
font-size: 16px;
z-index: 1;
margin-bottom: 4px;
}

.bot-status {
Expand Down
Loading