Initial commit from agent

This commit is contained in:
2026-03-24 00:11:34 -05:00
commit 0c777488d3
69 changed files with 4253 additions and 0 deletions

View File

@@ -0,0 +1,902 @@
<!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&hellip;</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>