JARVIS / static /index.html
Khanna, Videh Rakesh Rakesh
feat: security hardening, transcription mode, 15 new features, Gradio UI
0191dfa
Raw
History Blame Contribute Delete
61.4 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="JARVIS">
<meta name="mobile-web-app-capable" content="yes">
<meta name="theme-color" content="#0a0e17">
<meta name="format-detection" content="telephone=no">
<link rel="manifest" href="/static/manifest.json">
<!-- Apple touch icons for iPhone / iPad -->
<link rel="apple-touch-icon" href="/static/apple-touch-icon.png">
<link rel="apple-touch-icon" sizes="152x152" href="/static/apple-touch-icon-152.png">
<link rel="apple-touch-icon" sizes="167x167" href="/static/apple-touch-icon-167.png">
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
<!-- iOS splash screens -->
<meta name="apple-mobile-web-app-orientations" content="portrait">
<title>J.A.R.V.I.S.</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--bg: #0a0e17;
--surface: #111827;
--surface2: #1a2332;
--accent: #00b4d8;
--accent2: #0077b6;
--accent-glow: rgba(0, 180, 216, 0.3);
--text: #e0e6ed;
--text2: #8899aa;
--success: #00e676;
--warning: #ffab00;
--danger: #ff5252;
--purple: #bb86fc;
}
body {
background: var(--bg);
color: var(--text);
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', 'JetBrains Mono', monospace;
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* Header */
.header {
background: linear-gradient(180deg, var(--surface) 0%, transparent 100%);
padding: 12px 20px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid rgba(0, 180, 216, 0.15);
z-index: 10;
}
.logo {
display: flex;
align-items: center;
gap: 12px;
}
.arc-reactor {
width: 36px;
height: 36px;
border-radius: 50%;
background: radial-gradient(circle, var(--accent) 0%, var(--accent2) 40%, transparent 70%);
box-shadow: 0 0 20px var(--accent-glow), 0 0 40px rgba(0, 180, 216, 0.15);
animation: pulse 2s ease-in-out infinite;
position: relative;
}
.arc-reactor::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 12px;
height: 12px;
border-radius: 50%;
background: white;
box-shadow: 0 0 10px var(--accent);
}
@keyframes pulse {
0%, 100% { opacity: 0.8; transform: scale(1); }
50% { opacity: 1; transform: scale(1.05); }
}
.logo h1 {
font-size: 18px;
font-weight: 700;
letter-spacing: 4px;
color: var(--accent);
text-shadow: 0 0 20px var(--accent-glow);
}
.logo .subtitle {
font-size: 9px;
color: var(--text2);
letter-spacing: 2px;
}
.controls {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
justify-content: flex-end;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--success);
box-shadow: 0 0 8px var(--success);
animation: pulse 2s ease-in-out infinite;
}
.ctrl-btn {
background: var(--surface2);
border: 1px solid rgba(0, 180, 216, 0.2);
color: var(--text);
padding: 4px 10px;
border-radius: 12px;
font-size: 10px;
cursor: pointer;
font-family: inherit;
transition: all 0.2s;
letter-spacing: 0.5px;
}
.ctrl-btn:hover {
border-color: var(--accent);
box-shadow: 0 0 10px var(--accent-glow);
}
.ctrl-btn.active {
background: rgba(0, 180, 216, 0.15);
border-color: var(--accent);
color: var(--accent);
}
.ctrl-btn.multi {
border-color: rgba(187, 134, 252, 0.3);
color: var(--purple);
}
.ctrl-btn.multi.active {
background: rgba(187, 134, 252, 0.15);
border-color: var(--purple);
box-shadow: 0 0 10px rgba(187, 134, 252, 0.3);
}
/* Settings Panel */
.settings-panel {
position: fixed;
top: 50px;
right: 16px;
width: 380px;
max-height: 80vh;
overflow-y: auto;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: 20px;
z-index: 1000;
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
}
.settings-group {
margin-bottom: 14px;
}
.settings-group label {
display: block;
font-size: 11px;
color: var(--text2);
margin-bottom: 4px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.settings-group select, .settings-group input[type="range"] {
width: 100%;
padding: 6px 10px;
background: var(--bg2);
border: 1px solid var(--border);
color: var(--text);
border-radius: 6px;
font-size: 13px;
font-family: inherit;
}
.settings-group select:focus, .settings-group input:focus {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
/* Skip link for accessibility */
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: var(--accent);
color: #000;
padding: 8px 16px;
z-index: 9999;
border-radius: 0 0 8px 0;
font-weight: 600;
}
.skip-link:focus {
top: 0;
}
select.ctrl-select {
background: var(--surface2);
border: 1px solid rgba(0, 180, 216, 0.2);
color: var(--text);
padding: 4px 8px;
border-radius: 12px;
font-size: 10px;
font-family: inherit;
cursor: pointer;
outline: none;
}
select.ctrl-select:focus {
border-color: var(--accent);
}
/* AutoTune indicator */
.autotune-badge {
display: none;
align-items: center;
gap: 4px;
padding: 2px 8px;
border-radius: 8px;
font-size: 9px;
letter-spacing: 0.5px;
background: rgba(255, 171, 0, 0.1);
border: 1px solid rgba(255, 171, 0, 0.2);
color: var(--warning);
}
.autotune-badge.visible {
display: flex;
}
/* Chat Area */
.chat-container {
flex: 1;
overflow-y: auto;
padding: 20px;
scroll-behavior: smooth;
}
.chat-container::-webkit-scrollbar { width: 4px; }
.chat-container::-webkit-scrollbar-track { background: transparent; }
.chat-container::-webkit-scrollbar-thumb { background: var(--accent2); border-radius: 2px; }
.message {
max-width: 85%;
margin-bottom: 16px;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
.message.user { margin-left: auto; }
.message.assistant { margin-right: auto; }
.message-bubble {
padding: 12px 16px;
border-radius: 16px;
font-size: 14px;
line-height: 1.6;
word-wrap: break-word;
}
.message.user .message-bubble {
background: linear-gradient(135deg, var(--accent2), var(--accent));
color: white;
border-bottom-right-radius: 4px;
}
.message.assistant .message-bubble {
background: var(--surface2);
border: 1px solid rgba(0, 180, 216, 0.1);
border-bottom-left-radius: 4px;
}
.message-label {
font-size: 10px;
color: var(--text2);
margin-bottom: 4px;
letter-spacing: 1px;
text-transform: uppercase;
display: flex;
align-items: center;
gap: 6px;
}
.message.user .message-label { text-align: right; justify-content: flex-end; }
.model-tag {
font-size: 8px;
padding: 1px 6px;
border-radius: 6px;
background: rgba(0, 180, 216, 0.1);
border: 1px solid rgba(0, 180, 216, 0.15);
color: var(--accent);
}
/* Tool execution indicator */
.tool-indicator {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 14px;
background: rgba(0, 180, 216, 0.08);
border: 1px solid rgba(0, 180, 216, 0.15);
border-radius: 8px;
font-size: 12px;
color: var(--accent);
margin-bottom: 12px;
animation: fadeIn 0.3s ease;
}
.tool-spinner {
width: 14px;
height: 14px;
border: 2px solid rgba(0, 180, 216, 0.2);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* STM indicator */
.stm-indicator {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
background: rgba(0, 230, 118, 0.08);
border: 1px solid rgba(0, 230, 118, 0.15);
border-radius: 6px;
font-size: 10px;
color: var(--success);
margin-bottom: 8px;
animation: fadeIn 0.3s ease;
}
/* Code blocks */
.message-bubble pre {
background: var(--bg);
padding: 10px;
border-radius: 8px;
overflow-x: auto;
margin: 8px 0;
font-size: 12px;
border: 1px solid rgba(0, 180, 216, 0.1);
}
.message-bubble code { font-family: inherit; font-size: 13px; }
.message-bubble p { margin-bottom: 8px; }
.message-bubble p:last-child { margin-bottom: 0; }
.message-bubble strong { color: var(--accent); }
/* Input Area */
.input-area {
padding: 16px 20px;
background: var(--surface);
border-top: 1px solid rgba(0, 180, 216, 0.15);
}
.input-row {
display: flex;
gap: 10px;
align-items: flex-end;
max-width: 900px;
margin: 0 auto;
}
.input-wrapper { flex: 1; position: relative; }
#message-input {
width: 100%;
background: var(--surface2);
border: 1px solid rgba(0, 180, 216, 0.2);
color: var(--text);
padding: 12px 16px;
border-radius: 16px;
font-family: inherit;
font-size: 14px;
resize: none;
max-height: 120px;
outline: none;
transition: border-color 0.2s;
}
#message-input:focus {
border-color: var(--accent);
box-shadow: 0 0 15px var(--accent-glow);
}
#message-input::placeholder { color: var(--text2); }
.btn {
width: 44px;
height: 44px;
border-radius: 50%;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
flex-shrink: 0;
}
.btn-send {
background: linear-gradient(135deg, var(--accent2), var(--accent));
color: white;
}
.btn-send:hover { transform: scale(1.05); box-shadow: 0 0 20px var(--accent-glow); }
.btn-send:disabled { opacity: 0.4; cursor: not-allowed; transform: none; }
.btn-voice {
background: var(--surface2);
border: 1px solid rgba(0, 180, 216, 0.2);
color: var(--text2);
}
.btn-voice:hover { border-color: var(--accent); color: var(--accent); }
.btn-voice.listening {
background: rgba(255, 82, 82, 0.15);
border-color: var(--danger);
color: var(--danger);
animation: pulse 1s ease-in-out infinite;
}
.btn svg { width: 20px; height: 20px; fill: currentColor; }
/* Welcome screen */
.welcome {
text-align: center;
padding: 60px 20px;
animation: fadeIn 0.8s ease;
}
.welcome .arc-reactor-lg {
width: 80px;
height: 80px;
border-radius: 50%;
background: radial-gradient(circle, var(--accent) 0%, var(--accent2) 40%, transparent 70%);
box-shadow: 0 0 40px var(--accent-glow), 0 0 80px rgba(0, 180, 216, 0.1);
margin: 0 auto 24px;
animation: pulse 2s ease-in-out infinite;
position: relative;
}
.welcome .arc-reactor-lg::after {
content: '';
position: absolute;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
width: 24px; height: 24px;
border-radius: 50%;
background: white;
box-shadow: 0 0 15px var(--accent);
}
.welcome h2 { font-size: 24px; color: var(--accent); letter-spacing: 6px; margin-bottom: 8px; }
.welcome p { color: var(--text2); font-size: 13px; line-height: 1.8; }
.quick-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: center;
margin-top: 24px;
}
.quick-btn {
background: var(--surface2);
border: 1px solid rgba(0, 180, 216, 0.15);
color: var(--text2);
padding: 8px 16px;
border-radius: 20px;
font-size: 12px;
cursor: pointer;
font-family: inherit;
transition: all 0.2s;
}
.quick-btn:hover {
border-color: var(--accent);
color: var(--accent);
box-shadow: 0 0 10px var(--accent-glow);
}
/* Wake word indicator */
.wakeword-overlay {
display: none;
position: fixed;
bottom: 100px;
left: 50%;
transform: translateX(-50%);
padding: 8px 20px;
background: rgba(0, 230, 118, 0.1);
border: 1px solid rgba(0, 230, 118, 0.3);
border-radius: 20px;
font-size: 11px;
color: var(--success);
z-index: 100;
animation: pulse 2s ease-in-out infinite;
letter-spacing: 1px;
}
.wakeword-overlay.active { display: block; }
.wakeword-overlay.heard {
background: rgba(0, 180, 216, 0.15);
border-color: var(--accent);
color: var(--accent);
animation: none;
}
/* Mobile responsive */
@media (max-width: 600px) {
.header { padding: 10px 14px; flex-wrap: wrap; gap: 8px; }
.logo h1 { font-size: 15px; letter-spacing: 3px; }
.logo .subtitle { display: none; }
.controls { gap: 4px; }
.chat-container { padding: 12px; }
.message { max-width: 92%; }
.input-area { padding: 12px 14px; }
.welcome h2 { font-size: 20px; }
.quick-actions { gap: 6px; }
}
/* iOS safe areas β€” notch / Dynamic Island / home indicator */
@supports (padding-top: env(safe-area-inset-top)) {
.header {
padding-top: calc(12px + env(safe-area-inset-top));
}
.input-area {
padding-bottom: calc(16px + env(safe-area-inset-bottom));
}
}
/* iPad specific β€” wider layout */
@media (min-width: 768px) and (max-width: 1024px) {
.message { max-width: 75%; }
.input-row { max-width: 700px; }
}
</style>
</head>
<body>
<!-- Accessibility: Skip to main content -->
<a href="#message-input" class="skip-link">Skip to chat input</a>
<!-- Header -->
<div class="header">
<div class="logo">
<div class="arc-reactor"></div>
<div>
<h1>J.A.R.V.I.S.</h1>
<div class="subtitle">JUST A RATHER VERY INTELLIGENT SYSTEM</div>
</div>
</div>
<div class="controls">
<div class="status-dot" id="status-dot"></div>
<select class="ctrl-select" id="backend-select" onchange="setBackend(this.value)">
<option value="auto">AUTO</option>
<option value="claude">CLAUDE</option>
<option value="ollama">OLLAMA</option>
<option value="openrouter">OPENROUTER</option>
<option value="multi">MULTI-MODEL</option>
</select>
<button class="ctrl-btn active" id="stm-btn" onclick="toggleSTM()" title="Semantic Transform β€” strips filler & hedging">STM</button>
<button class="ctrl-btn multi" id="multi-btn" onclick="setMultiModel()" title="Race 3 models, pick the best">RACE</button>
<div class="autotune-badge" id="autotune-badge">AT: <span id="autotune-type">β€”</span></div>
<button class="ctrl-btn" id="wakeword-btn" onclick="toggleWakeWord()" title="Always-on wake word: say 'Hey JARVIS'">WAKE</button>
<button class="ctrl-btn" onclick="newChat()" title="New Chat">NEW</button>
<button class="ctrl-btn" onclick="toggleSettings()" title="Settings" aria-label="Settings">βš™</button>
</div>
</div>
<!-- Settings Panel -->
<div class="settings-panel" id="settings-panel" role="dialog" aria-label="Settings" style="display:none;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">
<h3 style="margin:0;color:var(--accent);">Settings</h3>
<button onclick="toggleSettings()" style="background:none;border:none;color:var(--text2);font-size:20px;cursor:pointer;" aria-label="Close settings">&times;</button>
</div>
<div class="settings-group">
<label for="setting-language">Language</label>
<select id="setting-language" aria-label="Language">
<option value="en">English</option>
<option value="es">Spanish</option>
<option value="fr">French</option>
<option value="de">German</option>
<option value="it">Italian</option>
<option value="pt">Portuguese</option>
<option value="ja">Japanese</option>
<option value="ko">Korean</option>
<option value="zh">Chinese</option>
<option value="hi">Hindi</option>
<option value="ar">Arabic</option>
</select>
</div>
<div class="settings-group">
<label for="setting-tts">TTS Voice</label>
<select id="setting-tts" aria-label="TTS Voice">
<option value="Daniel">Daniel (Default)</option>
<option value="Samantha">Samantha</option>
<option value="Alex">Alex</option>
<option value="Karen">Karen</option>
<option value="Moira">Moira</option>
<option value="Tessa">Tessa</option>
<option value="elevenlabs">ElevenLabs (Premium)</option>
</select>
</div>
<div class="settings-group">
<label for="setting-rate">Speech Rate</label>
<input type="range" id="setting-rate" min="120" max="250" value="180" aria-label="Speech rate">
<span id="rate-value">180 WPM</span>
</div>
<div class="settings-group">
<label>Scheduled Jobs</label>
<div id="scheduled-jobs-list" style="font-size:12px;color:var(--text2);margin-top:4px;">Loading...</div>
<div style="display:flex;gap:6px;margin-top:8px;">
<input type="text" id="job-name" placeholder="Job name" style="flex:1;padding:4px 8px;background:var(--bg2);border:1px solid var(--border);color:var(--text);border-radius:4px;font-size:12px;">
<input type="text" id="job-command" placeholder="Command" style="flex:2;padding:4px 8px;background:var(--bg2);border:1px solid var(--border);color:var(--text);border-radius:4px;font-size:12px;">
<input type="text" id="job-time" placeholder="HH:MM" style="width:60px;padding:4px 8px;background:var(--bg2);border:1px solid var(--border);color:var(--text);border-radius:4px;font-size:12px;">
<button onclick="addScheduledJob()" style="padding:4px 10px;background:var(--accent);border:none;border-radius:4px;cursor:pointer;font-size:12px;color:#000;">Add</button>
</div>
</div>
<div class="settings-group">
<label>Automation Rules</label>
<div id="automation-rules-list" style="font-size:12px;color:var(--text2);margin-top:4px;">Loading...</div>
</div>
<div class="settings-group" style="margin-top:12px;">
<button onclick="saveSettings()" style="width:100%;padding:8px;background:var(--accent);border:none;border-radius:6px;cursor:pointer;font-weight:600;color:#000;">Save Settings</button>
</div>
</div>
<!-- Chat -->
<div class="chat-container" id="chat" role="log" aria-label="Chat messages" aria-live="polite">
<div class="welcome" id="welcome">
<div class="arc-reactor-lg"></div>
<h2>J.A.R.V.I.S.</h2>
<p>Good to see you. How may I assist you today?</p>
<p style="margin-top:8px; font-size:11px; color: var(--text2);">
Multi-model racing / STM filtering / AutoTune enabled
</p>
<div class="quick-actions">
<button class="quick-btn" onclick="quickAction('What can you do?')">What can you do?</button>
<button class="quick-btn" onclick="quickAction('Search the web for latest AI news')">AI News</button>
<button class="quick-btn" onclick="quickAction('What is my system status?')">System Status</button>
<button class="quick-btn" onclick="quickAction('What is the weather in New York?')">Weather</button>
<button class="quick-btn" onclick="quickAction('Write a Python function to sort a list')">Code</button>
<button class="quick-btn" onclick="quickAction('Compare React vs Vue for a startup')">Analysis</button>
</div>
</div>
</div>
<!-- Wake Word Overlay -->
<div class="wakeword-overlay" id="wakeword-overlay">LISTENING FOR "HEY JARVIS"...</div>
<!-- Input -->
<div class="input-area">
<div class="input-row">
<button class="btn btn-voice" id="voice-btn" onclick="toggleVoice()" title="Voice Input">
<svg viewBox="0 0 24 24"><path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm-1-9c0-.55.45-1 1-1s1 .45 1 1v6c0 .55-.45 1-1 1s-1-.45-1-1V5zm6 6c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z"/></svg>
</button>
<div class="input-wrapper">
<textarea id="message-input" rows="1" placeholder="Talk to JARVIS..."
onkeydown="handleKeydown(event)" oninput="autoResize(this)"></textarea>
</div>
<button class="btn btn-send" id="send-btn" onclick="sendMessage()" title="Send">
<svg viewBox="0 0 24 24"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>
</button>
</div>
</div>
<script>
// State
let ws = null;
let isStreaming = false;
let currentBackend = 'auto';
let stmEnabled = true;
let currentStreamEl = null;
let currentStreamText = '';
let recognition = null;
let isListening = false;
let ttsEnabled = true;
// ─── Auth ────────────────────────────────────
function getToken() {
return document.cookie.split(';').map(c => c.trim()).find(c => c.startsWith('jarvis_token='))?.split('=')[1] || '';
}
async function checkAuth() {
try {
const resp = await fetch('/api/status');
if (resp.status === 401) {
showLoginScreen();
return false;
}
return true;
} catch(e) {
return true;
}
}
function showLoginScreen() {
document.getElementById('chat').innerHTML = `
<div class="welcome" id="welcome">
<div class="arc-reactor-lg"></div>
<h2>J.A.R.V.I.S.</h2>
<p style="margin-top:16px;">Authentication required.</p>
<div style="margin-top:20px; display:flex; gap:8px; justify-content:center; align-items:center;">
<input type="password" id="auth-input" placeholder="Enter access token"
style="background:var(--surface2); border:1px solid rgba(0,180,216,0.3); color:var(--text);
padding:10px 16px; border-radius:12px; font-family:inherit; font-size:14px; outline:none; width:260px;"
onkeydown="if(event.key==='Enter') submitToken()">
<button onclick="submitToken()" class="ctrl-btn" style="padding:10px 20px; font-size:12px;">AUTHENTICATE</button>
</div>
<p id="auth-error" style="margin-top:10px; color:var(--danger); font-size:12px; display:none;"></p>
</div>`;
}
async function submitToken() {
const input = document.getElementById('auth-input');
const token = input.value.trim();
if (!token) return;
try {
const resp = await fetch('/api/auth/token', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({token}),
});
if (resp.ok) {
location.reload();
} else {
const err = document.getElementById('auth-error');
err.textContent = 'Invalid token. Check the server console for the correct token.';
err.style.display = 'block';
}
} catch(e) {
const err = document.getElementById('auth-error');
err.textContent = 'Cannot reach server.';
err.style.display = 'block';
}
}
// Connect WebSocket
function connect() {
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
ws = new WebSocket(`${proto}//${location.host}/ws`);
ws.onopen = () => {
document.getElementById('status-dot').style.background = 'var(--success)';
fetchStatus();
};
ws.onclose = () => {
document.getElementById('status-dot').style.background = 'var(--danger)';
setTimeout(connect, 3000);
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
handleMessage(data);
};
}
async function fetchStatus() {
try {
const resp = await fetch('/api/status');
const data = await resp.json();
currentBackend = data.backend;
// Update dropdown to show active backend
document.getElementById('backend-select').value = 'auto';
} catch(e) {}
}
function setBackend(value) {
currentBackend = value;
if (value === 'multi') {
document.getElementById('multi-btn').classList.add('active');
} else {
document.getElementById('multi-btn').classList.remove('active');
}
}
function setMultiModel() {
const btn = document.getElementById('multi-btn');
const select = document.getElementById('backend-select');
if (currentBackend === 'multi') {
currentBackend = 'auto';
select.value = 'auto';
btn.classList.remove('active');
} else {
currentBackend = 'multi';
select.value = 'multi';
btn.classList.add('active');
}
}
function toggleSTM() {
stmEnabled = !stmEnabled;
const btn = document.getElementById('stm-btn');
btn.classList.toggle('active', stmEnabled);
}
// Handle incoming messages
function handleMessage(data) {
const chat = document.getElementById('chat');
switch(data.type) {
case 'stream_start':
if (!currentStreamEl) {
hideWelcome();
const msg = createMessage('assistant');
currentStreamEl = msg.querySelector('.message-bubble');
currentStreamText = '';
chat.appendChild(msg);
}
break;
case 'stream':
if (currentStreamEl) {
currentStreamText += data.content;
currentStreamEl.innerHTML = formatMarkdown(currentStreamText);
scrollToBottom();
}
break;
case 'stream_end':
if (currentStreamEl && ttsEnabled) {
speak(stripMarkdown(currentStreamText));
}
currentStreamEl = null;
currentStreamText = '';
isStreaming = false;
updateSendButton();
break;
case 'autotune':
const badge = document.getElementById('autotune-badge');
const typeEl = document.getElementById('autotune-type');
typeEl.textContent = `${data.query_type} (t=${data.temperature})`;
badge.classList.add('visible');
setTimeout(() => badge.classList.remove('visible'), 5000);
break;
case 'stm_applied':
const saved = data.original_length - data.cleaned_length;
if (saved > 10) {
const indicator = document.createElement('div');
indicator.className = 'stm-indicator';
indicator.innerHTML = `&#9889; STM: Removed ${saved} chars of filler`;
chat.appendChild(indicator);
}
break;
case 'tool_start':
break;
case 'tool_exec':
hideWelcome();
const toolInd = document.createElement('div');
toolInd.className = 'tool-indicator';
toolInd.innerHTML = `<div class="tool-spinner"></div> Executing: ${data.tool}(${JSON.stringify(data.args).slice(0, 80)})`;
chat.appendChild(toolInd);
scrollToBottom();
break;
case 'tool_result':
const indicators = chat.querySelectorAll('.tool-indicator');
if (indicators.length) {
const last = indicators[indicators.length - 1];
last.innerHTML = `&#10003; ${data.tool}: Done`;
last.style.borderColor = 'rgba(0, 230, 118, 0.3)';
last.style.color = 'var(--success)';
}
currentStreamEl = null;
break;
case 'error':
const errMsg = createMessage('assistant');
errMsg.querySelector('.message-bubble').textContent = data.content;
errMsg.querySelector('.message-bubble').style.borderColor = 'var(--danger)';
chat.appendChild(errMsg);
isStreaming = false;
updateSendButton();
break;
case 'cleared':
chat.innerHTML = '';
showWelcome();
break;
}
}
// Message helpers
function createMessage(role) {
const div = document.createElement('div');
div.className = `message ${role}`;
const label = role === 'user' ? 'YOU' : 'JARVIS';
const modelTag = role === 'assistant' && currentBackend !== 'auto'
? `<span class="model-tag">${currentBackend.toUpperCase()}</span>` : '';
div.innerHTML = `
<div class="message-label">${label} ${modelTag}</div>
<div class="message-bubble"></div>
`;
return div;
}
function addUserMessage(text) {
hideWelcome();
const msg = createMessage('user');
msg.querySelector('.message-bubble').textContent = text;
document.getElementById('chat').appendChild(msg);
scrollToBottom();
}
// Send message
function sendMessage() {
const input = document.getElementById('message-input');
const text = input.value.trim();
if (!text || isStreaming || !ws) return;
addUserMessage(text);
ws.send(JSON.stringify({
type: 'message',
content: text,
backend: currentBackend,
stm: stmEnabled,
}));
input.value = '';
input.style.height = 'auto';
isStreaming = true;
updateSendButton();
}
function quickAction(text) {
document.getElementById('message-input').value = text;
sendMessage();
}
function newChat() {
if (ws) ws.send(JSON.stringify({ type: 'clear' }));
document.getElementById('chat').innerHTML = '';
showWelcome();
}
// Voice
function toggleVoice() {
if (!('webkitSpeechRecognition' in window || 'SpeechRecognition' in window)) {
alert('Speech recognition not supported. Use Chrome or Edge.');
return;
}
if (isListening) { recognition.stop(); return; }
const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
recognition = new SR();
recognition.continuous = false;
recognition.interimResults = true;
recognition.lang = 'en-US';
recognition.onstart = () => {
isListening = true;
document.getElementById('voice-btn').classList.add('listening');
};
recognition.onresult = (event) => {
const transcript = Array.from(event.results).map(r => r[0].transcript).join('');
document.getElementById('message-input').value = transcript;
};
recognition.onend = () => {
isListening = false;
document.getElementById('voice-btn').classList.remove('listening');
if (document.getElementById('message-input').value.trim()) sendMessage();
};
recognition.onerror = () => {
isListening = false;
document.getElementById('voice-btn').classList.remove('listening');
};
recognition.start();
}
// TTS
function speak(text) {
if (!ttsEnabled || !text || text.length < 2) return;
window.speechSynthesis.cancel();
const utter = new SpeechSynthesisUtterance(text.slice(0, 500));
utter.rate = 1.05;
utter.pitch = 0.95;
const voices = window.speechSynthesis.getVoices();
const british = voices.find(v => v.lang === 'en-GB' && v.name.includes('Male'));
const english = voices.find(v => v.lang.startsWith('en'));
utter.voice = british || english || voices[0];
window.speechSynthesis.speak(utter);
}
window.speechSynthesis.onvoiceschanged = () => {};
// Formatting
function formatMarkdown(text) {
return text
.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>')
.replace(/`([^`]+)`/g, '<code>$1</code>')
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.+?)\*/g, '<em>$1</em>')
.replace(/\n/g, '<br>');
}
function stripMarkdown(text) {
return text
.replace(/```[\s\S]*?```/g, '')
.replace(/`[^`]+`/g, '')
.replace(/\*\*(.+?)\*\*/g, '$1')
.replace(/\*(.+?)\*/g, '$1')
.replace(/\[Tool.*?\][\s\S]*?(?=\n\n|\n[A-Z]|$)/g, '')
.replace(/```tool[\s\S]*?```/g, '')
.trim();
}
// UI helpers
function scrollToBottom() {
const chat = document.getElementById('chat');
chat.scrollTop = chat.scrollHeight;
}
function autoResize(el) {
el.style.height = 'auto';
el.style.height = Math.min(el.scrollHeight, 120) + 'px';
}
function handleKeydown(e) {
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); }
}
function updateSendButton() {
document.getElementById('send-btn').disabled = isStreaming;
}
function hideWelcome() {
const w = document.getElementById('welcome');
if (w) w.style.display = 'none';
}
function showWelcome() {
const chat = document.getElementById('chat');
chat.innerHTML = `
<div class="welcome" id="welcome">
<div class="arc-reactor-lg"></div>
<h2>J.A.R.V.I.S.</h2>
<p>Good to see you. How may I assist you today?</p>
<p style="margin-top:8px; font-size:11px; color: var(--text2);">
Multi-model racing / STM filtering / AutoTune enabled
</p>
<div class="quick-actions">
<button class="quick-btn" onclick="quickAction('What can you do?')">What can you do?</button>
<button class="quick-btn" onclick="quickAction('Search the web for latest AI news')">AI News</button>
<button class="quick-btn" onclick="quickAction('What is my system status?')">System Status</button>
<button class="quick-btn" onclick="quickAction('What is the weather in New York?')">Weather</button>
<button class="quick-btn" onclick="quickAction('Write a Python function to sort a list')">Code</button>
<button class="quick-btn" onclick="quickAction('Compare React vs Vue for a startup')">Analysis</button>
</div>
</div>`;
}
// Wake Word Detection β€” always-on "Hey JARVIS"
let wakeWordActive = false;
let wakeRecognition = null;
function toggleWakeWord() {
if (wakeWordActive) {
stopWakeWord();
} else {
startWakeWord();
}
}
function startWakeWord() {
if (!('webkitSpeechRecognition' in window || 'SpeechRecognition' in window)) {
alert('Speech recognition not supported. Use Chrome or Edge.');
return;
}
wakeWordActive = true;
document.getElementById('wakeword-btn').classList.add('active');
document.getElementById('wakeword-overlay').classList.add('active');
listenForWakeWord();
}
function stopWakeWord() {
wakeWordActive = false;
document.getElementById('wakeword-btn').classList.remove('active');
document.getElementById('wakeword-overlay').classList.remove('active');
if (wakeRecognition) {
wakeRecognition.abort();
wakeRecognition = null;
}
}
function listenForWakeWord() {
if (!wakeWordActive) return;
const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
wakeRecognition = new SR();
wakeRecognition.continuous = true;
wakeRecognition.interimResults = true;
wakeRecognition.lang = 'en-US';
wakeRecognition.onresult = (event) => {
for (let i = event.resultIndex; i < event.results.length; i++) {
const transcript = event.results[i][0].transcript.toLowerCase().trim();
// Check for wake word
const wakeWords = ['hey jarvis', 'jarvis', 'hey j.a.r.v.i.s', 'okay jarvis', 'yo jarvis'];
let triggered = false;
let command = '';
for (const wake of wakeWords) {
if (transcript.includes(wake)) {
triggered = true;
// Extract command after wake word
const idx = transcript.indexOf(wake) + wake.length;
command = transcript.slice(idx).trim().replace(/^[,.\s]+/, '');
break;
}
}
if (triggered && event.results[i].isFinal) {
// Stop wake word listening
wakeRecognition.stop();
const overlay = document.getElementById('wakeword-overlay');
overlay.classList.add('heard');
overlay.textContent = 'JARVIS ACTIVATED';
// Play activation sound
try {
const ctx = new (window.AudioContext || window.webkitAudioContext)();
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.connect(gain);
gain.connect(ctx.destination);
osc.frequency.value = 880;
osc.type = 'sine';
gain.gain.value = 0.1;
osc.start();
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.3);
osc.stop(ctx.currentTime + 0.3);
} catch(e) {}
if (command) {
// Already have command, send it
document.getElementById('message-input').value = command;
sendMessage();
setTimeout(() => {
overlay.classList.remove('heard');
overlay.textContent = 'LISTENING FOR "HEY JARVIS"...';
listenForWakeWord();
}, 2000);
} else {
// Speak "Yes sir?" and listen for command
speak('Yes, sir?');
setTimeout(() => {
listenForCommand();
}, 1500);
}
return;
}
}
};
wakeRecognition.onend = () => {
// Auto-restart if still active
if (wakeWordActive && !isStreaming) {
setTimeout(listenForWakeWord, 300);
}
};
wakeRecognition.onerror = (e) => {
if (e.error !== 'no-speech' && e.error !== 'aborted') {
console.log('Wake word error:', e.error);
}
if (wakeWordActive) {
setTimeout(listenForWakeWord, 1000);
}
};
wakeRecognition.start();
}
function listenForCommand() {
const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
const cmdRecognition = new SR();
cmdRecognition.continuous = false;
cmdRecognition.interimResults = true;
cmdRecognition.lang = 'en-US';
const overlay = document.getElementById('wakeword-overlay');
overlay.textContent = 'LISTENING FOR COMMAND...';
let finalTranscript = '';
cmdRecognition.onresult = (event) => {
finalTranscript = '';
for (let i = 0; i < event.results.length; i++) {
finalTranscript += event.results[i][0].transcript;
}
document.getElementById('message-input').value = finalTranscript;
};
cmdRecognition.onend = () => {
if (finalTranscript.trim()) {
sendMessage();
}
// Wait for response to finish, then resume wake word
const checkDone = setInterval(() => {
if (!isStreaming) {
clearInterval(checkDone);
overlay.classList.remove('heard');
overlay.textContent = 'LISTENING FOR "HEY JARVIS"...';
setTimeout(listenForWakeWord, 1000);
}
}, 500);
};
cmdRecognition.onerror = () => {
overlay.classList.remove('heard');
overlay.textContent = 'LISTENING FOR "HEY JARVIS"...';
listenForWakeWord();
};
cmdRecognition.start();
}
// ─── PWA Install ─────────────────────────────
let deferredPrompt = null;
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
deferredPrompt = e;
showInstallBanner();
});
function showInstallBanner() {
if (document.getElementById('install-banner')) return;
const banner = document.createElement('div');
banner.id = 'install-banner';
banner.style.cssText = `
position: fixed; bottom: 80px; left: 50%; transform: translateX(-50%);
background: var(--surface2); border: 1px solid var(--accent);
border-radius: 16px; padding: 12px 20px; z-index: 1000;
display: flex; align-items: center; gap: 12px;
box-shadow: 0 0 30px var(--accent-glow); animation: fadeIn 0.5s ease;
max-width: 90vw;
`;
banner.innerHTML = `
<span style="font-size:13px; color: var(--text);">Install JARVIS as an app?</span>
<button onclick="installPWA()" style="
background: var(--accent); color: white; border: none;
padding: 6px 16px; border-radius: 10px; font-family: inherit;
font-size: 12px; cursor: pointer; font-weight: bold;
">INSTALL</button>
<button onclick="this.parentElement.remove()" style="
background: none; border: none; color: var(--text2);
font-size: 18px; cursor: pointer; padding: 0 4px;
">&times;</button>
`;
document.body.appendChild(banner);
}
async function installPWA() {
if (!deferredPrompt) return;
deferredPrompt.prompt();
const result = await deferredPrompt.userChoice;
if (result.outcome === 'accepted') {
document.getElementById('install-banner')?.remove();
}
deferredPrompt = null;
}
// iOS / iPadOS install hint
function checkiOSInstall() {
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) ||
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1); // iPad with desktop UA
const isStandalone = window.navigator.standalone;
const dismissed = localStorage.getItem('jarvis_ios_install_dismissed');
if (isIOS && !isStandalone && !dismissed) {
const isIPad = /iPad/.test(navigator.userAgent) ||
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
const deviceName = isIPad ? 'iPad' : 'iPhone';
const hint = document.createElement('div');
hint.id = 'ios-install-hint';
hint.style.cssText = `
position: fixed; bottom: 80px; left: 50%; transform: translateX(-50%);
background: var(--surface2); border: 1px solid var(--accent);
border-radius: 16px; padding: 16px 24px; z-index: 1000;
box-shadow: 0 0 30px var(--accent-glow); animation: fadeIn 0.5s ease;
max-width: 90vw; text-align: center; font-size: 13px; color: var(--text);
`;
hint.innerHTML = `
<div style="margin-bottom:8px; font-weight:bold; color: var(--accent); font-size:15px;">Install JARVIS on ${deviceName}</div>
<div style="margin-bottom:6px;">Tap <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="vertical-align:middle;"><path d="M16 5l-1.42 1.42-1.59-1.59V16h-1.98V4.83L9.42 6.42 8 5l4-4 4 4zm4 5v11c0 1.1-.9 2-2 2H6c-1.11 0-2-.9-2-2V10c0-1.11.89-2 2-2h3v2H6v11h12V10h-3V8h3c1.1 0 2 .89 2 2z"/></svg> Share β†’ <strong>Add to Home Screen</strong></div>
<div style="color: var(--text2); font-size:11px; margin-bottom:10px;">JARVIS will run like a native app β€” fullscreen, with mic &amp; voice</div>
<div style="display:flex; gap:8px; justify-content:center;">
<button onclick="this.closest('#ios-install-hint').remove()" style="
background:none; border:1px solid var(--accent);
color:var(--accent); padding:6px 16px; border-radius:10px;
font-family:inherit; font-size:12px; cursor:pointer;
">GOT IT</button>
<button onclick="localStorage.setItem('jarvis_ios_install_dismissed','1'); this.closest('#ios-install-hint').remove()" style="
background:none; border:1px solid var(--text2);
color:var(--text2); padding:6px 16px; border-radius:10px;
font-family:inherit; font-size:11px; cursor:pointer;
">DON'T SHOW AGAIN</button>
</div>
`;
document.body.appendChild(hint);
}
}
// ─── Auto Wake Word on Launch ────────────────
function autoStartWakeWord() {
// If opened as installed PWA, auto-start wake word
const isStandalone = window.matchMedia('(display-mode: standalone)').matches
|| window.navigator.standalone
|| document.referrer.includes('android-app://');
if (isStandalone) {
// Small delay to let everything initialize
setTimeout(() => {
startWakeWord();
}, 1500);
}
}
// ─── Keep Alive (prevent phone from killing the tab) ────
function startKeepAlive() {
// Play silent audio to keep the app alive on mobile
const AudioContext = window.AudioContext || window.webkitAudioContext;
if (!AudioContext) return;
const ctx = new AudioContext();
const oscillator = ctx.createOscillator();
const gain = ctx.createGain();
gain.gain.value = 0.001; // Nearly silent
oscillator.connect(gain);
gain.connect(ctx.destination);
oscillator.start();
// Also prevent screen dimming via Wake Lock API
async function requestWakeLock() {
try {
if ('wakeLock' in navigator) {
await navigator.wakeLock.request('screen');
}
} catch(e) {}
}
requestWakeLock();
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
requestWakeLock();
// Restart wake word if it died
if (wakeWordActive && !wakeRecognition) {
listenForWakeWord();
}
}
});
}
// ─── Service Worker Registration ─────────────
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/static/sw.js').catch(() => {});
}
// ─── Request Permissions Upfront ──────────────
async function requestPermissions() {
// Mic permission
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
stream.getTracks().forEach(t => t.stop());
} catch(e) {
console.log('Mic permission denied');
}
// Notification permission
if ('Notification' in window && Notification.permission === 'default') {
Notification.requestPermission();
}
}
// ── Settings Panel ──────────────────────────────
function toggleSettings() {
const panel = document.getElementById('settings-panel');
const visible = panel.style.display !== 'none';
panel.style.display = visible ? 'none' : 'block';
if (!visible) loadSettings();
}
async function loadSettings() {
try {
const resp = await fetch('/api/settings', { headers: authHeaders() });
const data = await resp.json();
document.getElementById('setting-language').value = data.language || 'en';
document.getElementById('setting-tts').value = data.tts_voice || 'Daniel';
document.getElementById('setting-rate').value = data.tts_rate || 180;
document.getElementById('rate-value').textContent = (data.tts_rate || 180) + ' WPM';
} catch(e) { console.log('Settings load error', e); }
// Load scheduled jobs
try {
const resp = await fetch('/api/scheduler/jobs', { headers: authHeaders() });
const data = await resp.json();
const el = document.getElementById('scheduled-jobs-list');
if (data.jobs && data.jobs.length > 0) {
el.innerHTML = data.jobs.map(j =>
`<div style="display:flex;justify-content:space-between;align-items:center;padding:3px 0;">` +
`<span>${j.name} β€” <em>${j.command}</em> ${j.run_at ? '@ '+j.run_at : j.interval_seconds+'s'}</span>` +
`<button onclick="deleteJob(${j.id})" style="background:none;border:none;color:#f44;cursor:pointer;font-size:14px;">&times;</button></div>`
).join('');
} else {
el.textContent = 'No scheduled jobs.';
}
} catch(e) { document.getElementById('scheduled-jobs-list').textContent = 'Could not load jobs.'; }
// Load automation rules
try {
const resp = await fetch('/api/automations', { headers: authHeaders() });
const data = await resp.json();
const el = document.getElementById('automation-rules-list');
if (data.rules && data.rules.length > 0) {
el.innerHTML = data.rules.map(r =>
`<div style="padding:3px 0;">${r.name}: when ${r.trigger}="${r.condition}" β†’ ${r.action}</div>`
).join('');
} else {
el.textContent = 'No automation rules.';
}
} catch(e) { document.getElementById('automation-rules-list').textContent = 'Could not load rules.'; }
}
// Speech rate slider live update
document.getElementById('setting-rate').addEventListener('input', function() {
document.getElementById('rate-value').textContent = this.value + ' WPM';
});
async function saveSettings() {
const prefs = [
{ key: 'language', value: document.getElementById('setting-language').value },
{ key: 'tts_voice', value: document.getElementById('setting-tts').value },
{ key: 'tts_rate', value: document.getElementById('setting-rate').value },
];
for (const p of prefs) {
await fetch('/api/settings', {
method: 'PUT',
headers: { ...authHeaders(), 'Content-Type': 'application/json' },
body: JSON.stringify(p),
});
}
toggleSettings();
}
async function addScheduledJob() {
const name = document.getElementById('job-name').value.trim();
const command = document.getElementById('job-command').value.trim();
const runAt = document.getElementById('job-time').value.trim();
if (!name || !command) return alert('Name and command are required.');
await fetch('/api/scheduler/jobs', {
method: 'POST',
headers: { ...authHeaders(), 'Content-Type': 'application/json' },
body: JSON.stringify({ name, command, run_at: runAt, repeat_daily: !!runAt }),
});
document.getElementById('job-name').value = '';
document.getElementById('job-command').value = '';
document.getElementById('job-time').value = '';
loadSettings();
}
async function deleteJob(id) {
await fetch('/api/scheduler/jobs/' + id, { method: 'DELETE', headers: authHeaders() });
loadSettings();
}
function authHeaders() {
const token = localStorage.getItem('jarvis_token') || '';
return token ? { 'Authorization': 'Bearer ' + token } : {};
}
// Initialize
checkAuth().then(ok => {
if (ok) {
connect();
requestPermissions();
startKeepAlive();
autoStartWakeWord();
setTimeout(checkiOSInstall, 3000);
}
});
</script>
</body>
</html>