903 lines
28 KiB
HTML
903 lines
28 KiB
HTML
|
|
<!DOCTYPE html>
|
||
|
|
<html lang="en" data-theme="dark">
|
||
|
|
<head>
|
||
|
|
<meta charset="UTF-8">
|
||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
|
|
<title>JARVIS</title>
|
||
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
|
|
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Inter:wght@300;400;500&display=swap" rel="stylesheet">
|
||
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r134/three.min.js"></script>
|
||
|
|
<style>
|
||
|
|
:root {
|
||
|
|
--font-display: 'Orbitron', monospace;
|
||
|
|
--font-body: 'Inter', sans-serif;
|
||
|
|
--text-xs: clamp(0.65rem, 0.6rem + 0.2vw, 0.75rem);
|
||
|
|
--text-sm: clamp(0.75rem, 0.7rem + 0.25vw, 0.875rem);
|
||
|
|
--text-base: clamp(0.875rem, 0.8rem + 0.3vw, 1rem);
|
||
|
|
--text-lg: clamp(1rem, 0.9rem + 0.5vw, 1.25rem);
|
||
|
|
--text-xl: clamp(1.25rem, 1rem + 1vw, 1.75rem);
|
||
|
|
--space-1: 0.25rem; --space-2: 0.5rem; --space-3: 0.75rem;
|
||
|
|
--space-4: 1rem; --space-5: 1.25rem; --space-6: 1.5rem;
|
||
|
|
--space-8: 2rem; --space-10: 2.5rem; --space-12: 3rem;
|
||
|
|
--radius-sm: 4px; --radius-md: 8px; --radius-lg: 12px; --radius-xl: 16px; --radius-full: 9999px;
|
||
|
|
--transition: 200ms cubic-bezier(0.16, 1, 0.3, 1);
|
||
|
|
|
||
|
|
--color-bg: #0a0a0c;
|
||
|
|
--color-surface: #0f0f14;
|
||
|
|
--color-surface-2: #14141a;
|
||
|
|
--color-border: rgba(120,180,255,0.08);
|
||
|
|
--color-text: #c8d4e8;
|
||
|
|
--color-text-muted: #6a7a94;
|
||
|
|
--color-text-faint: #3a4a5e;
|
||
|
|
--color-accent: #4f9eff;
|
||
|
|
--color-accent-2: #a78bfa;
|
||
|
|
--color-accent-gold: #f5c842;
|
||
|
|
--color-accent-green: #34d399;
|
||
|
|
--color-accent-orange: #fb923c;
|
||
|
|
--glow-blue: rgba(79,158,255,0.15);
|
||
|
|
--glow-purple: rgba(167,139,250,0.12);
|
||
|
|
}
|
||
|
|
|
||
|
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||
|
|
html { -webkit-font-smoothing: antialiased; scroll-behavior: smooth; }
|
||
|
|
|
||
|
|
body {
|
||
|
|
font-family: var(--font-body);
|
||
|
|
background: var(--color-bg);
|
||
|
|
color: var(--color-text);
|
||
|
|
min-height: 100dvh;
|
||
|
|
overflow: hidden;
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ─── Canvas ─────────────────────────────── */
|
||
|
|
#orb-canvas {
|
||
|
|
position: fixed;
|
||
|
|
inset: 0;
|
||
|
|
z-index: 0;
|
||
|
|
pointer-events: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ─── Layout ─────────────────────────────── */
|
||
|
|
.app {
|
||
|
|
position: relative;
|
||
|
|
z-index: 10;
|
||
|
|
display: grid;
|
||
|
|
grid-template-rows: auto 1fr auto;
|
||
|
|
height: 100dvh;
|
||
|
|
padding: var(--space-4);
|
||
|
|
gap: var(--space-4);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ─── Header ─────────────────────────────── */
|
||
|
|
.header {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: space-between;
|
||
|
|
padding: var(--space-3) var(--space-5);
|
||
|
|
background: rgba(15,15,20,0.6);
|
||
|
|
backdrop-filter: blur(20px);
|
||
|
|
border: 1px solid var(--color-border);
|
||
|
|
border-radius: var(--radius-xl);
|
||
|
|
}
|
||
|
|
|
||
|
|
.logo {
|
||
|
|
font-family: var(--font-display);
|
||
|
|
font-size: var(--text-lg);
|
||
|
|
font-weight: 700;
|
||
|
|
letter-spacing: 0.2em;
|
||
|
|
color: var(--color-accent);
|
||
|
|
text-shadow: 0 0 20px rgba(79,158,255,0.5);
|
||
|
|
}
|
||
|
|
|
||
|
|
.logo span {
|
||
|
|
font-size: var(--text-xs);
|
||
|
|
display: block;
|
||
|
|
color: var(--color-text-muted);
|
||
|
|
letter-spacing: 0.3em;
|
||
|
|
font-weight: 400;
|
||
|
|
margin-top: 1px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.header-status {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: var(--space-3);
|
||
|
|
}
|
||
|
|
|
||
|
|
.status-dot {
|
||
|
|
width: 8px; height: 8px;
|
||
|
|
border-radius: var(--radius-full);
|
||
|
|
background: var(--color-accent-green);
|
||
|
|
box-shadow: 0 0 8px var(--color-accent-green);
|
||
|
|
animation: pulse-dot 2s ease-in-out infinite;
|
||
|
|
}
|
||
|
|
|
||
|
|
@keyframes pulse-dot {
|
||
|
|
0%, 100% { opacity: 1; transform: scale(1); }
|
||
|
|
50% { opacity: 0.6; transform: scale(0.85); }
|
||
|
|
}
|
||
|
|
|
||
|
|
.status-label {
|
||
|
|
font-size: var(--text-xs);
|
||
|
|
color: var(--color-text-muted);
|
||
|
|
font-family: var(--font-display);
|
||
|
|
letter-spacing: 0.15em;
|
||
|
|
text-transform: uppercase;
|
||
|
|
}
|
||
|
|
|
||
|
|
.header-time {
|
||
|
|
font-family: var(--font-display);
|
||
|
|
font-size: var(--text-sm);
|
||
|
|
color: var(--color-text-muted);
|
||
|
|
letter-spacing: 0.1em;
|
||
|
|
text-align: right;
|
||
|
|
}
|
||
|
|
.header-time .date {
|
||
|
|
font-size: var(--text-xs);
|
||
|
|
color: var(--color-text-faint);
|
||
|
|
margin-top: 2px;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ─── Main body ────────────────────────────── */
|
||
|
|
.main {
|
||
|
|
display: grid;
|
||
|
|
grid-template-columns: 200px 1fr 200px;
|
||
|
|
gap: var(--space-4);
|
||
|
|
align-items: stretch;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ─── Side panels ─────────────────────────── */
|
||
|
|
.panel {
|
||
|
|
background: rgba(15,15,20,0.55);
|
||
|
|
backdrop-filter: blur(20px);
|
||
|
|
border: 1px solid var(--color-border);
|
||
|
|
border-radius: var(--radius-xl);
|
||
|
|
padding: var(--space-4);
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
gap: var(--space-3);
|
||
|
|
}
|
||
|
|
|
||
|
|
.panel-title {
|
||
|
|
font-family: var(--font-display);
|
||
|
|
font-size: var(--text-xs);
|
||
|
|
letter-spacing: 0.2em;
|
||
|
|
text-transform: uppercase;
|
||
|
|
color: var(--color-accent);
|
||
|
|
padding-bottom: var(--space-2);
|
||
|
|
border-bottom: 1px solid var(--color-border);
|
||
|
|
opacity: 0.8;
|
||
|
|
}
|
||
|
|
|
||
|
|
.panel-item {
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
gap: 2px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.panel-item-label {
|
||
|
|
font-size: var(--text-xs);
|
||
|
|
color: var(--color-text-faint);
|
||
|
|
letter-spacing: 0.1em;
|
||
|
|
}
|
||
|
|
|
||
|
|
.panel-item-value {
|
||
|
|
font-family: var(--font-display);
|
||
|
|
font-size: var(--text-sm);
|
||
|
|
color: var(--color-text);
|
||
|
|
}
|
||
|
|
|
||
|
|
.panel-item-value.accent { color: var(--color-accent); }
|
||
|
|
.panel-item-value.green { color: var(--color-accent-green); }
|
||
|
|
.panel-item-value.gold { color: var(--color-accent-gold); }
|
||
|
|
.panel-item-value.purple { color: var(--color-accent-2); }
|
||
|
|
|
||
|
|
.capability-chip {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: var(--space-2);
|
||
|
|
padding: var(--space-1) var(--space-2);
|
||
|
|
border-radius: var(--radius-full);
|
||
|
|
background: rgba(79,158,255,0.06);
|
||
|
|
border: 1px solid rgba(79,158,255,0.12);
|
||
|
|
font-size: var(--text-xs);
|
||
|
|
color: var(--color-text-muted);
|
||
|
|
letter-spacing: 0.05em;
|
||
|
|
transition: all var(--transition);
|
||
|
|
}
|
||
|
|
|
||
|
|
.capability-chip .dot {
|
||
|
|
width: 5px; height: 5px;
|
||
|
|
border-radius: var(--radius-full);
|
||
|
|
background: var(--color-text-faint);
|
||
|
|
flex-shrink: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.capability-chip.active .dot { background: var(--color-accent-green); box-shadow: 0 0 6px var(--color-accent-green); }
|
||
|
|
.capability-chip.active { color: var(--color-text); border-color: rgba(52,211,153,0.2); background: rgba(52,211,153,0.04); }
|
||
|
|
|
||
|
|
/* ─── Orb Center ──────────────────────────── */
|
||
|
|
.orb-center {
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: center;
|
||
|
|
gap: var(--space-4);
|
||
|
|
position: relative;
|
||
|
|
}
|
||
|
|
|
||
|
|
.orb-status-text {
|
||
|
|
font-family: var(--font-display);
|
||
|
|
font-size: var(--text-base);
|
||
|
|
letter-spacing: 0.3em;
|
||
|
|
text-transform: uppercase;
|
||
|
|
color: var(--color-accent);
|
||
|
|
text-shadow: 0 0 20px rgba(79,158,255,0.4);
|
||
|
|
text-align: center;
|
||
|
|
min-height: 1.5em;
|
||
|
|
transition: opacity 0.4s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
.orb-subtitle {
|
||
|
|
font-size: var(--text-xs);
|
||
|
|
color: var(--color-text-faint);
|
||
|
|
letter-spacing: 0.25em;
|
||
|
|
text-transform: uppercase;
|
||
|
|
text-align: center;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Audio waveform bars (idle animation) */
|
||
|
|
.waveform {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 3px;
|
||
|
|
height: 40px;
|
||
|
|
opacity: 0;
|
||
|
|
transition: opacity 0.4s ease;
|
||
|
|
}
|
||
|
|
.waveform.active { opacity: 1; }
|
||
|
|
.waveform .bar {
|
||
|
|
width: 3px;
|
||
|
|
border-radius: var(--radius-full);
|
||
|
|
background: var(--color-accent);
|
||
|
|
box-shadow: 0 0 6px var(--color-accent);
|
||
|
|
animation: wave-bar 1.2s ease-in-out infinite;
|
||
|
|
}
|
||
|
|
.waveform .bar:nth-child(1) { animation-delay: 0ms; }
|
||
|
|
.waveform .bar:nth-child(2) { animation-delay: 80ms; }
|
||
|
|
.waveform .bar:nth-child(3) { animation-delay: 160ms; }
|
||
|
|
.waveform .bar:nth-child(4) { animation-delay: 240ms; }
|
||
|
|
.waveform .bar:nth-child(5) { animation-delay: 320ms; }
|
||
|
|
.waveform .bar:nth-child(6) { animation-delay: 400ms; }
|
||
|
|
.waveform .bar:nth-child(7) { animation-delay: 320ms; }
|
||
|
|
.waveform .bar:nth-child(8) { animation-delay: 240ms; }
|
||
|
|
.waveform .bar:nth-child(9) { animation-delay: 160ms; }
|
||
|
|
.waveform .bar:nth-child(10) { animation-delay: 80ms; }
|
||
|
|
.waveform .bar:nth-child(11) { animation-delay: 0ms; }
|
||
|
|
|
||
|
|
@keyframes wave-bar {
|
||
|
|
0%, 100% { height: 6px; opacity: 0.3; }
|
||
|
|
50% { height: 32px; opacity: 1; }
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ─── Footer / Input Bar ─────────────────── */
|
||
|
|
.footer {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: var(--space-3);
|
||
|
|
padding: var(--space-3) var(--space-5);
|
||
|
|
background: rgba(15,15,20,0.6);
|
||
|
|
backdrop-filter: blur(20px);
|
||
|
|
border: 1px solid var(--color-border);
|
||
|
|
border-radius: var(--radius-xl);
|
||
|
|
}
|
||
|
|
|
||
|
|
.transcript-display {
|
||
|
|
flex: 1;
|
||
|
|
font-family: var(--font-display);
|
||
|
|
font-size: var(--text-sm);
|
||
|
|
color: var(--color-text-muted);
|
||
|
|
letter-spacing: 0.05em;
|
||
|
|
min-height: 1.4em;
|
||
|
|
transition: color 0.3s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
.transcript-display.speaking { color: var(--color-text); }
|
||
|
|
|
||
|
|
.mic-btn {
|
||
|
|
width: 48px; height: 48px;
|
||
|
|
border-radius: var(--radius-full);
|
||
|
|
border: 1px solid rgba(79,158,255,0.3);
|
||
|
|
background: rgba(79,158,255,0.08);
|
||
|
|
cursor: pointer;
|
||
|
|
display: flex; align-items: center; justify-content: center;
|
||
|
|
transition: all var(--transition);
|
||
|
|
position: relative;
|
||
|
|
flex-shrink: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.mic-btn:hover {
|
||
|
|
background: rgba(79,158,255,0.15);
|
||
|
|
border-color: rgba(79,158,255,0.5);
|
||
|
|
box-shadow: 0 0 20px rgba(79,158,255,0.2);
|
||
|
|
}
|
||
|
|
|
||
|
|
.mic-btn.recording {
|
||
|
|
background: rgba(251,146,60,0.15);
|
||
|
|
border-color: rgba(251,146,60,0.5);
|
||
|
|
box-shadow: 0 0 24px rgba(251,146,60,0.3);
|
||
|
|
animation: mic-pulse 1s ease-in-out infinite;
|
||
|
|
}
|
||
|
|
|
||
|
|
@keyframes mic-pulse {
|
||
|
|
0%, 100% { box-shadow: 0 0 20px rgba(251,146,60,0.3); }
|
||
|
|
50% { box-shadow: 0 0 40px rgba(251,146,60,0.5); }
|
||
|
|
}
|
||
|
|
|
||
|
|
.mic-btn svg { pointer-events: none; }
|
||
|
|
|
||
|
|
.close-btn {
|
||
|
|
width: 40px; height: 40px;
|
||
|
|
border-radius: var(--radius-full);
|
||
|
|
border: 1px solid var(--color-border);
|
||
|
|
background: transparent;
|
||
|
|
cursor: pointer;
|
||
|
|
display: flex; align-items: center; justify-content: center;
|
||
|
|
color: var(--color-text-faint);
|
||
|
|
transition: all var(--transition);
|
||
|
|
flex-shrink: 0;
|
||
|
|
}
|
||
|
|
.close-btn:hover { color: var(--color-text-muted); border-color: rgba(255,255,255,0.15); }
|
||
|
|
|
||
|
|
/* ─── Response output ─────────────────────── */
|
||
|
|
.response-panel {
|
||
|
|
position: fixed;
|
||
|
|
bottom: 100px;
|
||
|
|
left: 50%;
|
||
|
|
transform: translateX(-50%);
|
||
|
|
width: min(600px, calc(100vw - 2rem));
|
||
|
|
background: rgba(15,15,20,0.85);
|
||
|
|
backdrop-filter: blur(24px);
|
||
|
|
border: 1px solid rgba(79,158,255,0.15);
|
||
|
|
border-radius: var(--radius-xl);
|
||
|
|
padding: var(--space-5);
|
||
|
|
z-index: 50;
|
||
|
|
opacity: 0;
|
||
|
|
pointer-events: none;
|
||
|
|
transition: opacity 0.4s ease, transform 0.4s cubic-bezier(0.16,1,0.3,1);
|
||
|
|
transform: translateX(-50%) translateY(10px);
|
||
|
|
}
|
||
|
|
|
||
|
|
.response-panel.visible {
|
||
|
|
opacity: 1;
|
||
|
|
pointer-events: auto;
|
||
|
|
transform: translateX(-50%) translateY(0);
|
||
|
|
}
|
||
|
|
|
||
|
|
.response-label {
|
||
|
|
font-family: var(--font-display);
|
||
|
|
font-size: var(--text-xs);
|
||
|
|
color: var(--color-accent);
|
||
|
|
letter-spacing: 0.2em;
|
||
|
|
margin-bottom: var(--space-3);
|
||
|
|
text-transform: uppercase;
|
||
|
|
}
|
||
|
|
|
||
|
|
.response-text {
|
||
|
|
font-size: var(--text-base);
|
||
|
|
color: var(--color-text);
|
||
|
|
line-height: 1.7;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ─── Scrollable transcript log ──────────── */
|
||
|
|
.log-panel {
|
||
|
|
position: fixed;
|
||
|
|
right: var(--space-4);
|
||
|
|
top: 50%;
|
||
|
|
transform: translateY(-50%);
|
||
|
|
width: 220px;
|
||
|
|
max-height: 60vh;
|
||
|
|
overflow-y: auto;
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
gap: var(--space-2);
|
||
|
|
z-index: 30;
|
||
|
|
padding: var(--space-3);
|
||
|
|
background: rgba(15,15,20,0.5);
|
||
|
|
backdrop-filter: blur(16px);
|
||
|
|
border: 1px solid var(--color-border);
|
||
|
|
border-radius: var(--radius-xl);
|
||
|
|
opacity: 0;
|
||
|
|
pointer-events: none;
|
||
|
|
transition: opacity 0.3s;
|
||
|
|
}
|
||
|
|
|
||
|
|
.log-entry {
|
||
|
|
font-size: var(--text-xs);
|
||
|
|
padding: var(--space-2) var(--space-3);
|
||
|
|
border-radius: var(--radius-md);
|
||
|
|
line-height: 1.5;
|
||
|
|
}
|
||
|
|
|
||
|
|
.log-entry.user { background: rgba(79,158,255,0.08); color: var(--color-accent); }
|
||
|
|
.log-entry.assistant { background: rgba(167,139,250,0.08); color: var(--color-accent-2); }
|
||
|
|
|
||
|
|
/* ─── Background grid ─────────────────────── */
|
||
|
|
.bg-grid {
|
||
|
|
position: fixed;
|
||
|
|
inset: 0;
|
||
|
|
z-index: 1;
|
||
|
|
pointer-events: none;
|
||
|
|
background-image:
|
||
|
|
linear-gradient(rgba(79,158,255,0.025) 1px, transparent 1px),
|
||
|
|
linear-gradient(90deg, rgba(79,158,255,0.025) 1px, transparent 1px);
|
||
|
|
background-size: 60px 60px;
|
||
|
|
mask-image: radial-gradient(ellipse 70% 70% at 50% 50%, black, transparent);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ─── Horizon glow ─────────────────────────── */
|
||
|
|
.horizon-glow {
|
||
|
|
position: fixed;
|
||
|
|
bottom: -80px;
|
||
|
|
left: 50%;
|
||
|
|
transform: translateX(-50%);
|
||
|
|
width: 80vw;
|
||
|
|
height: 200px;
|
||
|
|
background: radial-gradient(ellipse at 50% 100%, rgba(79,158,255,0.06), transparent 70%);
|
||
|
|
pointer-events: none;
|
||
|
|
z-index: 2;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ─── Scanning ring ────────────────────────── */
|
||
|
|
.scan-ring {
|
||
|
|
position: absolute;
|
||
|
|
width: 320px; height: 320px;
|
||
|
|
border-radius: var(--radius-full);
|
||
|
|
border: 1px solid rgba(79,158,255,0.06);
|
||
|
|
animation: scan-rotate 20s linear infinite;
|
||
|
|
pointer-events: none;
|
||
|
|
}
|
||
|
|
.scan-ring::after {
|
||
|
|
content: '';
|
||
|
|
position: absolute;
|
||
|
|
top: -1px; left: 20%;
|
||
|
|
width: 30%; height: 1px;
|
||
|
|
background: linear-gradient(90deg, transparent, rgba(79,158,255,0.4), transparent);
|
||
|
|
}
|
||
|
|
.scan-ring-2 {
|
||
|
|
width: 280px; height: 280px;
|
||
|
|
animation-duration: 15s;
|
||
|
|
animation-direction: reverse;
|
||
|
|
border-color: rgba(167,139,250,0.05);
|
||
|
|
}
|
||
|
|
|
||
|
|
@keyframes scan-rotate { to { transform: rotate(360deg); } }
|
||
|
|
|
||
|
|
/* ─── Scrollbar ────────────────────────────── */
|
||
|
|
::-webkit-scrollbar { width: 4px; }
|
||
|
|
::-webkit-scrollbar-track { background: transparent; }
|
||
|
|
::-webkit-scrollbar-thumb { background: var(--color-border); border-radius: var(--radius-full); }
|
||
|
|
</style>
|
||
|
|
</head>
|
||
|
|
<body>
|
||
|
|
<canvas id="orb-canvas"></canvas>
|
||
|
|
<div class="bg-grid"></div>
|
||
|
|
<div class="horizon-glow"></div>
|
||
|
|
|
||
|
|
<div class="app">
|
||
|
|
<!-- Header -->
|
||
|
|
<header class="header">
|
||
|
|
<div class="logo">
|
||
|
|
JARVIS
|
||
|
|
<span>Just A Rather Very Intelligent System</span>
|
||
|
|
</div>
|
||
|
|
<div class="header-status">
|
||
|
|
<div class="status-dot"></div>
|
||
|
|
<div class="status-label" id="sys-status">Online</div>
|
||
|
|
</div>
|
||
|
|
<div class="header-time">
|
||
|
|
<div id="clock">--:--:--</div>
|
||
|
|
<div class="date" id="date-label">---</div>
|
||
|
|
</div>
|
||
|
|
</header>
|
||
|
|
|
||
|
|
<!-- Main -->
|
||
|
|
<main class="main">
|
||
|
|
<!-- Left panel -->
|
||
|
|
<aside class="panel">
|
||
|
|
<div class="panel-title">System</div>
|
||
|
|
<div class="panel-item">
|
||
|
|
<div class="panel-item-label">Backend</div>
|
||
|
|
<div class="panel-item-value accent">FastAPI</div>
|
||
|
|
</div>
|
||
|
|
<div class="panel-item">
|
||
|
|
<div class="panel-item-label">Voice Engine</div>
|
||
|
|
<div class="panel-item-value green">Fish Audio TTS</div>
|
||
|
|
</div>
|
||
|
|
<div class="panel-item">
|
||
|
|
<div class="panel-item-label">Intelligence</div>
|
||
|
|
<div class="panel-item-value purple">Claude API</div>
|
||
|
|
</div>
|
||
|
|
<div class="panel-item">
|
||
|
|
<div class="panel-item-label">STT Model</div>
|
||
|
|
<div class="panel-item-value">Whisper Base</div>
|
||
|
|
</div>
|
||
|
|
<div class="panel-item">
|
||
|
|
<div class="panel-item-label">Transport</div>
|
||
|
|
<div class="panel-item-value">WebSocket</div>
|
||
|
|
</div>
|
||
|
|
<div class="panel-item">
|
||
|
|
<div class="panel-item-label">Workspace</div>
|
||
|
|
<div class="panel-item-value gold">Google Suite</div>
|
||
|
|
</div>
|
||
|
|
</aside>
|
||
|
|
|
||
|
|
<!-- Center orb -->
|
||
|
|
<div class="orb-center">
|
||
|
|
<div class="scan-ring"></div>
|
||
|
|
<div class="scan-ring scan-ring-2"></div>
|
||
|
|
<div class="orb-status-text" id="orb-text">STANDBY</div>
|
||
|
|
<div class="waveform" id="waveform">
|
||
|
|
<div class="bar"></div><div class="bar"></div><div class="bar"></div>
|
||
|
|
<div class="bar"></div><div class="bar"></div><div class="bar"></div>
|
||
|
|
<div class="bar"></div><div class="bar"></div><div class="bar"></div>
|
||
|
|
<div class="bar"></div><div class="bar"></div>
|
||
|
|
</div>
|
||
|
|
<div class="orb-subtitle" id="orb-subtitle">Tap microphone to speak</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Right panel -->
|
||
|
|
<aside class="panel">
|
||
|
|
<div class="panel-title">Capabilities</div>
|
||
|
|
<div class="capability-chip active"><div class="dot"></div>Screen Vision</div>
|
||
|
|
<div class="capability-chip active"><div class="dot"></div>Gmail</div>
|
||
|
|
<div class="capability-chip active"><div class="dot"></div>Calendar</div>
|
||
|
|
<div class="capability-chip active"><div class="dot"></div>G. Tasks</div>
|
||
|
|
<div class="capability-chip active"><div class="dot"></div>G. Keep</div>
|
||
|
|
<div class="capability-chip active"><div class="dot"></div>Google Drive</div>
|
||
|
|
<div class="capability-chip active"><div class="dot"></div>Terminal</div>
|
||
|
|
<div class="capability-chip active"><div class="dot"></div>Chrome</div>
|
||
|
|
<div class="capability-chip active"><div class="dot"></div>VS Code</div>
|
||
|
|
<div class="capability-chip active"><div class="dot"></div>Git</div>
|
||
|
|
</aside>
|
||
|
|
</main>
|
||
|
|
|
||
|
|
<!-- Footer input bar -->
|
||
|
|
<footer class="footer">
|
||
|
|
<button class="close-btn" title="Clear" onclick="clearSession()">
|
||
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||
|
|
</button>
|
||
|
|
<div class="transcript-display" id="transcript">Say something…</div>
|
||
|
|
<button class="mic-btn" id="mic-btn" title="Hold to speak" onclick="toggleMic()">
|
||
|
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
|
|
<path d="M12 2a3 3 0 0 1 3 3v7a3 3 0 0 1-6 0V5a3 3 0 0 1 3-3z"/>
|
||
|
|
<path d="M19 10v2a7 7 0 0 1-14 0v-2"/>
|
||
|
|
<line x1="12" y1="19" x2="12" y2="22"/>
|
||
|
|
<line x1="8" y1="22" x2="16" y2="22"/>
|
||
|
|
</svg>
|
||
|
|
</button>
|
||
|
|
</footer>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Response panel -->
|
||
|
|
<div class="response-panel" id="response-panel">
|
||
|
|
<div class="response-label">JARVIS Response</div>
|
||
|
|
<div class="response-text" id="response-text"></div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<script type="module">
|
||
|
|
// ─── Clock ────────────────────────────────────────
|
||
|
|
function updateClock() {
|
||
|
|
const now = new Date();
|
||
|
|
document.getElementById('clock').textContent = now.toLocaleTimeString('en-US', { hour12: false });
|
||
|
|
document.getElementById('date-label').textContent = now.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' });
|
||
|
|
}
|
||
|
|
updateClock();
|
||
|
|
setInterval(updateClock, 1000);
|
||
|
|
|
||
|
|
// ─── Three.js Particle Orb ─────────────────────────
|
||
|
|
const canvas = document.getElementById('orb-canvas');
|
||
|
|
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
|
||
|
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
||
|
|
renderer.setClearColor(0x000000, 0);
|
||
|
|
|
||
|
|
const scene = new THREE.Scene();
|
||
|
|
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 100);
|
||
|
|
camera.position.z = 3.2;
|
||
|
|
|
||
|
|
const PARTICLE_COUNT = 4000;
|
||
|
|
const geometry = new THREE.BufferGeometry();
|
||
|
|
const positions = new Float32Array(PARTICLE_COUNT * 3);
|
||
|
|
const colors = new Float32Array(PARTICLE_COUNT * 3);
|
||
|
|
const sizes = new Float32Array(PARTICLE_COUNT);
|
||
|
|
const originalPositions = new Float32Array(PARTICLE_COUNT * 3);
|
||
|
|
const velocities = new Float32Array(PARTICLE_COUNT * 3);
|
||
|
|
|
||
|
|
// Distribute particles on sphere surface using Fibonacci sphere
|
||
|
|
for (let i = 0; i < PARTICLE_COUNT; i++) {
|
||
|
|
const goldenAngle = Math.PI * (3 - Math.sqrt(5));
|
||
|
|
const theta = goldenAngle * i;
|
||
|
|
const y = 1 - (i / (PARTICLE_COUNT - 1)) * 2;
|
||
|
|
const radius = Math.sqrt(1 - y * y);
|
||
|
|
const r = 1.0 + (Math.random() - 0.5) * 0.25;
|
||
|
|
|
||
|
|
const x = Math.cos(theta) * radius * r;
|
||
|
|
const z = Math.sin(theta) * radius * r;
|
||
|
|
const yr = y * r;
|
||
|
|
|
||
|
|
positions[i * 3] = x;
|
||
|
|
positions[i * 3 + 1] = yr;
|
||
|
|
positions[i * 3 + 2] = z;
|
||
|
|
originalPositions[i * 3] = x;
|
||
|
|
originalPositions[i * 3 + 1] = yr;
|
||
|
|
originalPositions[i * 3 + 2] = z;
|
||
|
|
|
||
|
|
velocities[i * 3] = (Math.random() - 0.5) * 0.002;
|
||
|
|
velocities[i * 3 + 1] = (Math.random() - 0.5) * 0.002;
|
||
|
|
velocities[i * 3 + 2] = (Math.random() - 0.5) * 0.002;
|
||
|
|
|
||
|
|
// Warm amber-to-gold color scheme matching attachment
|
||
|
|
const t = Math.random();
|
||
|
|
colors[i * 3] = 0.85 + t * 0.15; // R - warm
|
||
|
|
colors[i * 3 + 1] = 0.45 + t * 0.35; // G - amber
|
||
|
|
colors[i * 3 + 2] = 0.05 + t * 0.1; // B - minimal
|
||
|
|
|
||
|
|
sizes[i] = Math.random() * 1.5 + 0.4;
|
||
|
|
}
|
||
|
|
|
||
|
|
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
||
|
|
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
|
||
|
|
geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
|
||
|
|
|
||
|
|
const material = new THREE.ShaderMaterial({
|
||
|
|
uniforms: {
|
||
|
|
uTime: { value: 0 },
|
||
|
|
uAudioLevel: { value: 0 },
|
||
|
|
uState: { value: 0 }, // 0=idle, 1=listening, 2=speaking
|
||
|
|
uPixelRatio: { value: renderer.getPixelRatio() }
|
||
|
|
},
|
||
|
|
vertexShader: `
|
||
|
|
attribute float size;
|
||
|
|
attribute vec3 color;
|
||
|
|
varying vec3 vColor;
|
||
|
|
uniform float uTime;
|
||
|
|
uniform float uAudioLevel;
|
||
|
|
uniform float uState;
|
||
|
|
uniform float uPixelRatio;
|
||
|
|
|
||
|
|
float noise(vec3 p) {
|
||
|
|
return fract(sin(dot(p, vec3(127.1, 311.7, 74.7))) * 43758.5453);
|
||
|
|
}
|
||
|
|
|
||
|
|
void main() {
|
||
|
|
vColor = color;
|
||
|
|
vec3 pos = position;
|
||
|
|
float n = noise(pos * 2.0 + uTime * 0.3);
|
||
|
|
float dist = length(pos);
|
||
|
|
float breathe = 1.0 + sin(uTime * 0.8 + n * 6.28) * 0.03;
|
||
|
|
float audio = uAudioLevel * 0.3;
|
||
|
|
float stateMod = mix(0.0, 0.08, step(0.5, uState));
|
||
|
|
pos *= breathe + audio + stateMod * n;
|
||
|
|
|
||
|
|
// Listening: particles scatter slightly outward
|
||
|
|
if (uState > 0.5 && uState < 1.5) {
|
||
|
|
pos += normalize(pos) * sin(uTime * 3.0 + n * 10.0) * 0.04 * uAudioLevel;
|
||
|
|
}
|
||
|
|
// Speaking: pulsating ring
|
||
|
|
if (uState > 1.5) {
|
||
|
|
float ring = abs(pos.y) < 0.3 ? 1.0 : 0.0;
|
||
|
|
pos += normalize(pos) * ring * sin(uTime * 6.0 + n * 8.0) * 0.06;
|
||
|
|
}
|
||
|
|
|
||
|
|
vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0);
|
||
|
|
gl_PointSize = size * uPixelRatio * (280.0 / -mvPosition.z);
|
||
|
|
gl_Position = projectionMatrix * mvPosition;
|
||
|
|
}
|
||
|
|
`,
|
||
|
|
fragmentShader: `
|
||
|
|
varying vec3 vColor;
|
||
|
|
uniform float uState;
|
||
|
|
void main() {
|
||
|
|
vec2 uv = gl_PointCoord - 0.5;
|
||
|
|
float dist = length(uv);
|
||
|
|
if (dist > 0.5) discard;
|
||
|
|
float alpha = 1.0 - smoothstep(0.2, 0.5, dist);
|
||
|
|
|
||
|
|
// Tint color per state
|
||
|
|
vec3 col = vColor;
|
||
|
|
if (uState > 0.5 && uState < 1.5) {
|
||
|
|
col = mix(col, vec3(0.3, 0.6, 1.0), 0.4); // Blue-listening
|
||
|
|
} else if (uState > 1.5) {
|
||
|
|
col = mix(col, vec3(0.6, 1.0, 0.7), 0.35); // Green-speaking
|
||
|
|
}
|
||
|
|
|
||
|
|
gl_FragColor = vec4(col, alpha * 0.85);
|
||
|
|
}
|
||
|
|
`,
|
||
|
|
transparent: true,
|
||
|
|
vertexColors: true,
|
||
|
|
depthWrite: false,
|
||
|
|
blending: THREE.AdditiveBlending
|
||
|
|
});
|
||
|
|
|
||
|
|
const particles = new THREE.Points(geometry, material);
|
||
|
|
scene.add(particles);
|
||
|
|
|
||
|
|
// State
|
||
|
|
let state = 0; // 0=idle, 1=listening, 2=speaking
|
||
|
|
let targetAudioLevel = 0;
|
||
|
|
let currentAudioLevel = 0;
|
||
|
|
let speaking = false;
|
||
|
|
let rotSpeed = 0.001;
|
||
|
|
|
||
|
|
function setOrbState(s) {
|
||
|
|
state = s;
|
||
|
|
material.uniforms.uState.value = s;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Simulate speaking audio pulses
|
||
|
|
function simulateAudio(duration = 3000) {
|
||
|
|
const interval = setInterval(() => {
|
||
|
|
targetAudioLevel = Math.random() * 0.8 + 0.2;
|
||
|
|
}, 80);
|
||
|
|
setTimeout(() => {
|
||
|
|
clearInterval(interval);
|
||
|
|
targetAudioLevel = 0;
|
||
|
|
}, duration);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Animation loop
|
||
|
|
let clock = { t: 0, last: performance.now() };
|
||
|
|
function animate() {
|
||
|
|
requestAnimationFrame(animate);
|
||
|
|
const now = performance.now();
|
||
|
|
const delta = (now - clock.last) / 1000;
|
||
|
|
clock.last = now;
|
||
|
|
clock.t += delta;
|
||
|
|
|
||
|
|
material.uniforms.uTime.value = clock.t;
|
||
|
|
currentAudioLevel += (targetAudioLevel - currentAudioLevel) * 0.1;
|
||
|
|
material.uniforms.uAudioLevel.value = currentAudioLevel;
|
||
|
|
|
||
|
|
particles.rotation.y += rotSpeed + currentAudioLevel * 0.002;
|
||
|
|
particles.rotation.x += rotSpeed * 0.3;
|
||
|
|
|
||
|
|
renderer.render(scene, camera);
|
||
|
|
}
|
||
|
|
animate();
|
||
|
|
|
||
|
|
window.addEventListener('resize', () => {
|
||
|
|
camera.aspect = window.innerWidth / window.innerHeight;
|
||
|
|
camera.updateProjectionMatrix();
|
||
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
||
|
|
material.uniforms.uPixelRatio.value = renderer.getPixelRatio();
|
||
|
|
});
|
||
|
|
|
||
|
|
// ─── Mic & State Machine ──────────────────────────
|
||
|
|
let recording = false;
|
||
|
|
const micBtn = document.getElementById('mic-btn');
|
||
|
|
const orbText = document.getElementById('orb-text');
|
||
|
|
const orbSubtitle = document.getElementById('orb-subtitle');
|
||
|
|
const waveform = document.getElementById('waveform');
|
||
|
|
const transcript = document.getElementById('transcript');
|
||
|
|
const responsePanel = document.getElementById('response-panel');
|
||
|
|
const responseText = document.getElementById('response-text');
|
||
|
|
|
||
|
|
const DEMO_RESPONSES = [
|
||
|
|
"I've checked your Google Calendar. You have 3 meetings today — a standup at 9 AM, a design review at 2 PM, and a 1:1 at 4:30 PM.",
|
||
|
|
"I pulled up your Gmail. You have 7 unread messages, 2 of which appear to be marked high priority from Jason.Doe@company.com.",
|
||
|
|
"I found 4 open tasks in Google Tasks. The highest priority item is 'Deploy JARVIS backend' due tomorrow.",
|
||
|
|
"Running git status on your current project — 3 modified files, 1 untracked. Want me to open VS Code for a diff view?",
|
||
|
|
"I've taken a screenshot of your current screen and identified Chrome, VS Code, and Windows Terminal as your open applications.",
|
||
|
|
"I've created a new note in Google Keep titled 'JARVIS Ideas' with your content. It's been synced to your account."
|
||
|
|
];
|
||
|
|
|
||
|
|
const DEMO_QUERIES = [
|
||
|
|
"What's on my calendar today?",
|
||
|
|
"Check my Gmail for unread messages",
|
||
|
|
"Show me my pending tasks",
|
||
|
|
"Check git status on current project",
|
||
|
|
"What apps do I have open?",
|
||
|
|
"Create a note in Google Keep"
|
||
|
|
];
|
||
|
|
|
||
|
|
let demoIndex = 0;
|
||
|
|
|
||
|
|
window.toggleMic = function() {
|
||
|
|
if (recording) {
|
||
|
|
stopRecording();
|
||
|
|
} else {
|
||
|
|
startRecording();
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
window.clearSession = function() {
|
||
|
|
responsePanel.classList.remove('visible');
|
||
|
|
transcript.textContent = 'Say something\u2026';
|
||
|
|
transcript.classList.remove('speaking');
|
||
|
|
orbText.textContent = 'STANDBY';
|
||
|
|
orbSubtitle.textContent = 'Tap microphone to speak';
|
||
|
|
waveform.classList.remove('active');
|
||
|
|
setOrbState(0);
|
||
|
|
targetAudioLevel = 0;
|
||
|
|
rotSpeed = 0.001;
|
||
|
|
};
|
||
|
|
|
||
|
|
function startRecording() {
|
||
|
|
recording = true;
|
||
|
|
micBtn.classList.add('recording');
|
||
|
|
setOrbState(1);
|
||
|
|
rotSpeed = 0.003;
|
||
|
|
|
||
|
|
const query = DEMO_QUERIES[demoIndex % DEMO_QUERIES.length];
|
||
|
|
transcript.textContent = 'Listening…';
|
||
|
|
transcript.classList.add('speaking');
|
||
|
|
orbText.textContent = 'LISTENING';
|
||
|
|
orbSubtitle.textContent = 'Processing audio input';
|
||
|
|
waveform.classList.add('active');
|
||
|
|
targetAudioLevel = 0.5;
|
||
|
|
|
||
|
|
// Simulate live transcription
|
||
|
|
let charIdx = 0;
|
||
|
|
const typeInterval = setInterval(() => {
|
||
|
|
if (charIdx <= query.length) {
|
||
|
|
transcript.textContent = query.slice(0, charIdx) + (charIdx < query.length ? '|' : '');
|
||
|
|
charIdx++;
|
||
|
|
} else {
|
||
|
|
clearInterval(typeInterval);
|
||
|
|
setTimeout(stopRecording, 400);
|
||
|
|
}
|
||
|
|
}, 60);
|
||
|
|
}
|
||
|
|
|
||
|
|
function stopRecording() {
|
||
|
|
recording = false;
|
||
|
|
micBtn.classList.remove('recording');
|
||
|
|
setOrbState(2);
|
||
|
|
rotSpeed = 0.002;
|
||
|
|
|
||
|
|
const query = DEMO_QUERIES[demoIndex % DEMO_QUERIES.length];
|
||
|
|
transcript.textContent = query;
|
||
|
|
orbText.textContent = 'PROCESSING';
|
||
|
|
orbSubtitle.textContent = 'Querying Claude API';
|
||
|
|
targetAudioLevel = 0.3;
|
||
|
|
|
||
|
|
setTimeout(() => {
|
||
|
|
const response = DEMO_RESPONSES[demoIndex % DEMO_RESPONSES.length];
|
||
|
|
demoIndex++;
|
||
|
|
|
||
|
|
orbText.textContent = 'SPEAKING';
|
||
|
|
orbSubtitle.textContent = 'Synthesizing response';
|
||
|
|
simulateAudio(response.length * 25);
|
||
|
|
|
||
|
|
responseText.textContent = '';
|
||
|
|
responsePanel.classList.add('visible');
|
||
|
|
|
||
|
|
// Typewriter effect
|
||
|
|
let i = 0;
|
||
|
|
const typeRes = setInterval(() => {
|
||
|
|
if (i <= response.length) {
|
||
|
|
responseText.textContent = response.slice(0, i);
|
||
|
|
i++;
|
||
|
|
} else {
|
||
|
|
clearInterval(typeRes);
|
||
|
|
setTimeout(() => {
|
||
|
|
orbText.textContent = 'STANDBY';
|
||
|
|
orbSubtitle.textContent = 'Tap microphone to speak';
|
||
|
|
waveform.classList.remove('active');
|
||
|
|
setOrbState(0);
|
||
|
|
targetAudioLevel = 0;
|
||
|
|
rotSpeed = 0.001;
|
||
|
|
}, 4000);
|
||
|
|
}
|
||
|
|
}, 18);
|
||
|
|
}, 1200);
|
||
|
|
}
|
||
|
|
</script>
|
||
|
|
</body>
|
||
|
|
</html>
|