SIMON WORKSPACE
⚡ Connections
📊 Dashboard
SIMON READY
Speak a command — describe what you want to build, design, write, or analyze. SIMON routes your voice to the right agent automatically.
⬡ How do you want to proceed?
🎤 next · previous · undo · start over · modify ___
idle backend —
Always On
8
Total Agents
5
Active
2
Pending
0
Errors
88%
Global
Health
Filters
All Agents 8
Active 5
Pending 2
Categories
🎤 Voice I/O 2
🧠 Intelligence 3
🖥️ Output 2
Quick Actions
All
Active
Pending
Sort: Name ▾

Selected Agent

No Agent Selected
Click an agent card to view details, or start a voice session — SIMON output will appear here.
IDLE | backend —
Always On

⚡ Connection Center

Agents
Voice I/O
AI Core
Renderer
Deploy
Jobs

SIMON orchestrates specialized sub-agents. Each handles a distinct domain.

🎤
Voice Agent
Captures voice commands via STT, manages audio sessions.
Active
🧠
Brain Agent
Analyzes intent, injects context, routes to correct capability.
Active
🔀
Router Agent
Selects pipeline: general, generate_html, generate_page, or code.
Active
🖥️
Render Agent
Renders HTML, text, layouts and visual outputs in the workspace.
Active
🔊
Speaker Agent
Converts SIMON responses to natural speech via TTS.
Active
📡
Deploy Agent
Export, download, SFTP/FTP push and delivery of generated assets.
Pending
⚙️
Job Agent
Background queues, retry logic, multi-step chained workflows.
Pending
🎤
Speech-to-Text
Converts voice to text commands
Active
🔊
Text-to-Speech
SIMON speaks back to you
Active
Auto-speak all responses
⚠ Local use only. API keys entered here are stored in your browser's localStorage and sent directly from your browser. Do not share this file or deploy it publicly. For production use, route calls through your PHP backend at /simon/api/provider_router.php and store keys server-side.
Multi-Brain Router
Route tasks to the best AI automatically
Ready
Smart routing — image → DALL-E, analysis → Claude, fast → Grok
Prompt enhancement — auto-enrich voice commands before sending
🔑
Master Key Quick-Set
Paste one key here — SIMON auto-detects the provider (sk-proj = OpenAI, sk-ant = Claude, xai- = Grok)
⚙️
Carrier Routing Mode
How SIMON selects STT → Brain → TTS for each request
Current: Smart — routing matrix selects carrier chain based on detected intent
Smart matrix — generate HTML → OpenAI · analyze → Claude · fast reply → Grok
Manual — uses your AI provider / STT / TTS selections below
Fallback — if preferred carrier fails, auto-switches to next available, including Free AI local mode
🆓
Free AI · Local Fallback
No API key. No network call. Creates local drafts, checklists, summaries, and HTML scaffolds.
Ready
Use this when you want SIMON to respond without paid AI keys. For live web data, images, and deeper model reasoning, switch to OpenAI, Claude, Grok, or SIMON Backend.
🤖
OpenAI · GPT-4o + DALL-E
HTML/UI generation, code, images
No key
🔮
Claude · Anthropic
Long-form analysis, structure, research
No key
Grok · xAI
Fast responses, real-time, edgy style
No key
🧠
SIMON Backend · GS PHP
Your Gaylord Sinclair intelligence engine
⛓️
AI Pipeline Mode
Chain multiple brains for better output
Active
Single AI call. Fastest path — good for quick answers and simple pages.
🖥️
Display Engine
Renders generated content in workspace
Active
Auto-display when content is ready
Show scanner animation
📁
Export
Save generated content to files
Ready
⚙️
Job Queue
Background task execution engine
Ready
Enable async job processing
No jobs running.
`; } const lower = prompt.toLowerCase(); if (lower.includes('summary') || lower.includes('summarize')) { return 'SIMON Free AI summary mode: identify the main subject, reduce it to the key facts, list the action needed, and remove noise. Input: ' + prompt; } if (lower.includes('plan') || lower.includes('steps')) { return 'SIMON Free AI plan:\n1. Define the goal clearly.\n2. List the files, pages, or systems affected.\n3. Make the smallest safe change first.\n4. Test buttons, links, console errors, and mobile layout.\n5. Save a backup before replacing production.\n\nRequest: ' + prompt; } return 'SIMON Free AI is active. No API key is required. I can create local drafts, page scaffolds, checklists, summaries, and fallback responses. For deeper reasoning or live data, choose OpenAI, Claude, Grok, or SIMON Backend in AI Core.\n\nYour request: ' + prompt; } /* ─────────── SINGLE PROVIDER HELPER ─────────── */ async function callProvider(provider, promptText, sysPr) { if (provider === 'freeai') { return localFreeAIResponse(promptText, detectRoute(promptText)); } if (provider === 'openai' && CFG.openaiKey) { const r = await fetch(CFG.openaiEp || 'https://api.openai.com/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + CFG.openaiKey }, body: JSON.stringify({ model: CFG.openaiModel || 'gpt-4o', messages: [{ role: 'system', content: sysPr }, { role: 'user', content: promptText }], max_tokens: 4096, temperature: 0.7 }) }); if (!r.ok) { const e = await r.json().catch(() => ({})); throw new Error(e?.error?.message || 'OpenAI error ' + r.status); } const j = await r.json(); return j?.choices?.[0]?.message?.content || ''; } if (provider === 'claude' && CFG.claudeKey) { const r = await fetch('https://api.anthropic.com/v1/messages', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': CFG.claudeKey, 'anthropic-version': '2023-06-01', 'anthropic-dangerous-direct-browser-access': 'true' }, body: JSON.stringify({ model: CFG.claudeModel || 'claude-sonnet-4-6', max_tokens: 4096, system: sysPr, messages: [{ role: 'user', content: promptText }] }) }); if (!r.ok) { const e = await r.json().catch(() => ({})); throw new Error(e?.error?.message || 'Claude error ' + r.status); } const j = await r.json(); return j?.content?.[0]?.text || ''; } if (provider === 'grok' && CFG.grokKey) { const r = await fetch('https://api.x.ai/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + CFG.grokKey }, body: JSON.stringify({ model: CFG.grokModel || 'grok-3', messages: [{ role: 'system', content: sysPr }, { role: 'user', content: promptText }], max_tokens: 2048, temperature: 0.7 }) }); if (!r.ok) { const e = await r.json().catch(() => ({})); throw new Error(e?.error?.message || 'Grok error ' + r.status); } const j = await r.json(); return j?.choices?.[0]?.message?.content || ''; } // Fallback: try whichever provider has a key if (CFG.openaiKey) return callProvider('openai', promptText, sysPr); if (CFG.claudeKey) return callProvider('claude', promptText, sysPr); throw new Error('No API key configured. Add keys in Connections → AI Core.'); } /* ─────────── PIPELINE STEP DISPLAY ─────────── */ function showPipelineStep(steps, activeIdx) { const c = document.getElementById('wsRouting'); if (!c) return; c.innerHTML = ''; steps.forEach((step, i) => { const done = i < activeIdx; const active = i === activeIdx; const d = document.createElement('div'); d.className = 'rCard' + (active ? ' lit' : ''); d.style.animationDelay = (i * .08) + 's'; const icon = done ? '✓' : active ? '⚡' : '○'; const provColor = { openai: '#7c6bff', claude: '#e08040', grok: '#40c0ff', simon: '#50dc9f' }[step.provider] || '#8a9dc4'; d.innerHTML = `
${icon}
${step.label}
${step.desc}
${step.provider.toUpperCase()}
`; c.appendChild(d); }); } /* ─────────── PIPELINE ENGINE ─────────── */ async function runPipeline(text, route) { const mode = CFG.pipeline || 'quick'; const canOpenAI = !!CFG.openaiKey; const canClaude = !!CFG.claudeKey; /* ── STANDARD: Build → Polish ── */ if (mode === 'standard' && (canOpenAI || canClaude)) { const steps = [ { label: 'Step 1 — Build', desc: 'Generating output...', provider: canOpenAI ? 'openai' : 'claude' }, { label: 'Step 2 — Polish', desc: 'Refining quality and copy...', provider: canClaude ? 'claude' : 'openai' } ]; showPipelineStep(steps, 0); const builderSys = SYS_PROMPTS[route] || SYS_PROMPTS.general; const built = await callProvider(canOpenAI ? 'openai' : 'claude', (CFG.promptEnhance && route === 'generate_html' ? text + '\n\n[Build this as a complete, modern, visually stunning single-file HTML. Glassmorphism, animations, dark aesthetic.]' : text), builderSys); showPipelineStep(steps, 1); const polishSys = route === 'generate_html' ? 'You are a senior UI designer and copywriter. You are given an HTML page. Improve it: fix weak copy, tighten spacing, enhance colors/gradients, add micro-animations where missing, improve CTA clarity. Return ONLY the improved complete HTML file — no commentary.' : 'You are an expert editor. Improve the following response for clarity, structure, and quality. Return only the improved content — no meta-commentary.'; const polished = await callProvider(canClaude ? 'claude' : 'openai', `Here is the content to improve:\n\n${built}`, polishSys).catch(() => built); return polished || built; } /* ── PREMIUM: Plan → Build → Critique → Refine ── */ if (mode === 'premium' && (canOpenAI || canClaude)) { const steps = [ { label: 'Step 1 — Plan', desc: 'Analyzing request, creating spec...', provider: 'claude' }, { label: 'Step 2 — Build', desc: 'Generating output from spec...', provider: 'openai' }, { label: 'Step 3 — Critique', desc: 'Identifying weaknesses...', provider: 'claude' }, { label: 'Step 4 — Refine', desc: 'Applying improvements...', provider: 'openai' } ]; showPipelineStep(steps, 0); const planSys = 'You are a strategic planner. Analyze this creative request and return a tight spec (max 150 words): purpose, key sections/components, visual style, tone, and 3 must-have elements. Be specific and direct.'; const plan = await callProvider(canClaude ? 'claude' : 'openai', text, planSys).catch(() => ''); showPipelineStep(steps, 1); const buildSys = SYS_PROMPTS[route] || SYS_PROMPTS.general; const buildPrompt = plan ? `Original request: ${text}\n\nSpec to follow:\n${plan}` : text; const built = await callProvider(canOpenAI ? 'openai' : 'claude', CFG.promptEnhance && route === 'generate_html' ? buildPrompt + '\n\n[Single complete HTML file. Dark futuristic glassmorphism aesthetic. Embedded CSS and JS only.]' : buildPrompt, buildSys); showPipelineStep(steps, 2); const critSys = 'You are a brutal design critic and UX expert. Review this output and return ONLY a numbered list of 4-6 specific, actionable improvement points. Focus on: visual hierarchy, copy quality, layout gaps, missing interactions, and anything that looks generic or weak. Be blunt.'; const critique = await callProvider(canClaude ? 'claude' : 'openai', `Review this and give specific improvement notes:\n\n${built.slice(0, 3000)}`, critSys).catch(() => ''); showPipelineStep(steps, 3); const refineSys = SYS_PROMPTS[route] || SYS_PROMPTS.general; const refinePrompt = critique ? `Improve this based on the following critique feedback. Apply every point. Return ONLY the improved complete output:\n\nCRITIQUE:\n${critique}\n\nORIGINAL:\n${built}` : built; const refined = await callProvider(canOpenAI ? 'openai' : 'claude', refinePrompt, refineSys + ' Apply all critique improvements exactly. Return only the final output.').catch(() => built); return refined || built; } /* ── QUICK (default): single shot ── */ return null; // signals callSIMON to proceed with its own logic } /* ─────────── INTENT DETECTION: PATCH vs REBUILD vs NEW ─────────── */ const PATCH_WORDS = ['change', 'make', 'fix', 'update', 'adjust', 'modify', 'edit', 'replace', 'add', 'remove', 'move', 'darker', 'lighter', 'bigger', 'smaller', 'bolder', 'thinner', 'wider', 'taller', 'shorter', 'color', 'font', 'background', 'button', 'header', 'footer', 'hero', 'title', 'headline', 'more', 'less', 'different color', 'same but', 'instead of', 'keep the', 'leave the', 'only change', 'just change', 'just the', 'only the']; const REBUILD_WORDS = ['start over', 'start fresh', 'new page', 'completely different', 'redesign', 'build me a', 'create a', 'make me a new', 'from scratch', 'different concept']; function detectIntent(text) { const lo = text.toLowerCase(); if (REBUILD_WORDS.some(w => lo.includes(w))) return 'rebuild'; const hasArtifact = !!(MEMORY.session.activeArtifactId && MEMORY.artifacts[MEMORY.session.activeArtifactId]); if (hasArtifact && PATCH_WORDS.some(w => lo.includes(w))) return 'patch'; return 'new'; } /* System prompt for surgical patch mode */ const PATCH_SYS = `You are SIMON Intelligence, a surgical code editor. The user has an existing artifact they want to modify. RULES — follow these strictly: 1. Apply ONLY the specific change the user requested. Do NOT redesign, restructure, or rewrite anything else. 2. Return the COMPLETE modified artifact — not a diff, not a partial snippet. 3. Preserve all existing styles, layout, fonts, colors, animations, and content unless the user explicitly asked to change them. 4. If the user asks to change a headline, change ONLY the headline text. 5. If the user asks to change a color, change ONLY that color. 6. Think of yourself as a surgeon, not an architect. Minimal intervention, maximum precision.`; /* ─────────── UNDO / START OVER ─────────── */ /* ─────────── PROCEED PANEL ─────────── */ function showProceedPanel() { const p = document.getElementById('wsProceed'); if (!p || !lastOut) return; p.classList.add('show'); // hide the text input sub-panel until user picks Modify const pi = document.getElementById('wsProceedInput'); if (pi) pi.classList.remove('show'); // Update orb/wm text const ot = document.getElementById('orbTx'); const wt = document.getElementById('wmTx'); if (ot) ot.textContent = 'How to proceed?'; if (wt) wt.textContent = 'Choose how to continue…'; go('completed'); } function hideProceedPanel() { const p = document.getElementById('wsProceed'); if (p) p.classList.remove('show'); } function procModify() { // Show text input so user can type what to change const pi = document.getElementById('wsProceedInput'); if (pi) pi.classList.add('show'); const txt = document.getElementById('wsProceedTxt'); if (txt) txt.focus(); } function procGrow() { hideProceedPanel(); speak('Tell me what new element you want to add to the current design.', null, false, false); setTimeout(() => startListen(), 1200); } function procTalk() { hideProceedPanel(); startListen(); } function procSave() { hideProceedPanel(); // Trigger download of current iframe content const btn = document.getElementById('oDownload'); if (btn) btn.click(); setTimeout(() => { speak('Saved. Ready for a new creation. Clearing workspace.', null, false, false); setTimeout(doStartOver, 2200); }, 400); } function procSubmitText() { const txt = document.getElementById('wsProceedTxt'); if (!txt || !txt.value.trim()) return; const request = txt.value.trim(); txt.value = ''; hideProceedPanel(); callSIMON(request); } /* ─────────── HISTORY NAVIGATION ─────────── */ function doNext() { if (HISTORY_IDX >= HISTORY_STACK.length - 1) return speak('Already at the latest version.', null, false, false); if (historyRestore(HISTORY_IDX + 1)) speak('Next version.', null, false, false); } function doPrevious() { if (HISTORY_IDX <= 0 || HISTORY_STACK.length < 2) return speak('No previous version.', null, false, false); if (historyRestore(HISTORY_IDX - 1)) speak('Previous version.', null, false, false); } function doUndo() { if (HISTORY_IDX <= 0 || HISTORY_STACK.length < 2) return speak('No previous version to go back to.', null, false, false); if (historyRestore(HISTORY_IDX - 1)) speak('Restored previous version. Want another change?', null, false, true); } function doRedo() { if (HISTORY_IDX >= HISTORY_STACK.length - 1) return speak('Already at the latest version.', null, false, false); if (historyRestore(HISTORY_IDX + 1)) speak('Re-applied next version.', null, false, true); } function doStartOver() { hideProceedPanel(); // Clear in-session stack HISTORY_STACK = []; HISTORY_IDX = -1; // Clear persistent memory for active artifacts const id = MEMORY.session.activeArtifactId; if (id) delete MEMORY.artifacts[id]; MEMORY.session.activeArtifactId = null; MEMORY.session.activeImageId = null; MEMORY.session.lastPrompt = ''; lastOut = ''; lastOutType = 'text'; lastImageRevised = ''; saveMemory(); clearOut(); go('idle'); const wt = document.getElementById('wsTitle'); if (wt) wt.textContent = 'WORKSPACE'; speak('Session cleared. Ready for a new task.', null, false, false); } /* ─────────── API CALL ─────────── */ async function callSIMON(text) { currentPrompt = text; const route = detectRoute(text); // Resolve full carrier chain (STT→Brain→TTS) using carrier mesh const _chain = resolveCarrierChain(route); window.__SIMON_CARRIER_CHAIN = _chain; updateCarrierChip(_chain); // If no brain carrier available, surface the error rather than generating locally if (!_chain.brain?.id) { go('idle'); const noCarrierMsg = '⚠ No AI brain carrier available. Add an API key in Connections → AI Core.'; speak(noCarrierMsg, null, false, false); showAnswer && showAnswer(noCarrierMsg); return; } // Use resolved brain as provider (respects Manual/Smart/Fallback mode) const provider = _chain.brain.id !== 'simon' ? _chain.brain.id : (CFG.aiProvider||'openai'); go('thinking'); showRoutingCards('thinking', text); emitPts(.85); if (CFG.rScan) { const sc = document.getElementById('scanner'); if (sc) sc.classList.add('on'); } const wmTxEl = document.getElementById('wmTx'); const pipelineMode = CFG.pipeline || 'quick'; if (wmTxEl) wmTxEl.textContent = pipelineMode !== 'quick' ? '⚡ Pipeline: ' + pipelineMode + '...' : '⚡ Routing to ' + provider.toUpperCase() + '...'; let output = '', retRoute = route; const t0 = performance.now(); /* Intent: patch existing artifact vs fresh build */ const intent = detectIntent(text); const activeArt = MEMORY.artifacts[MEMORY.session.activeArtifactId]; let {sys, user} = enhancePrompt(text, route, provider); if (intent === 'patch' && activeArt && route !== 'generate_image') { // Surgical patch mode: override sys prompt and inject current artifact as context sys = PATCH_SYS; user = `CURRENT ARTIFACT (${activeArt.type}):\n\`\`\`\n${activeArt.content.slice(0, 6000)}\n\`\`\`\n\nUSER REQUEST: ${text}`; if (wmTxEl) wmTxEl.textContent = '🔧 Patching...'; } try { /* ────── PIPELINE (standard / premium) ────── */ if (pipelineMode !== 'quick' && route !== 'generate_image' && provider !== 'dalle') { const piped = await runPipeline(text, route); if (piped) { const lat = Math.round(performance.now() - t0); const lv = document.getElementById('latVal'); if (lv) lv.textContent = lat; const lc = document.getElementById('latChip'); if (lc) lc.style.display = ''; const wlv = document.getElementById('wmLatVal'); if (wlv) wlv.textContent = lat; const wlc = document.getElementById('wmVbLat'); if (wlc) wlc.style.display = ''; lastOut = piped; const isH = piped.trim().startsWith(' 200); lastOutType = isH ? 'html' : 'text'; historyPush(piped, lastOutType, text, 'pipeline_' + Date.now()); const pLabel = { quick: 'SIMON', standard: 'Build+Polish', premium: 'Plan→Build→Critique→Refine' }[pipelineMode]; go('executing'); setTimeout(() => { const sc = document.getElementById('scanner'); if (sc) sc.classList.remove('on'); go('completed', { type: lastOutType, content: piped, prompt: text }); const wt = document.getElementById('wsTitle'); if (wt) wt.textContent = pLabel + ' — ' + (text.length > 38 ? text.slice(0, 38) + '…' : text).toUpperCase(); if (isH) speak(pipelineMode === 'premium' ? "Four-step refinement complete. Here's your result." : "Two-brain pass complete. Here's what I built.", null, false, true); else speak('Pipeline complete. Your content is ready.', null, false, true); }, 600); return; } } /* ────── FREE LOCAL AI ────── */ if (provider === 'freeai') { output = localFreeAIResponse(user || text, route); retRoute = route; } else /* ────── DALL-E IMAGE ────── */ if (provider === 'dalle') { if (!CFG.openaiKey) throw new Error('OpenAI key required for images. Add it in Connections → AI Core.'); // If we have context from a prior image and this looks like a refinement // (no fresh "draw me / image of" trigger, short request, or modifier language), // inject the previous revised_prompt so DALL-E knows what to build on. const lo = text.toLowerCase(); const FRESH_TRIGGERS = ['draw me', 'picture of', 'image of', 'photo of', 'generate a', 'create a', 'make me a', 'dall-e', 'show me a']; const isFresh = FRESH_TRIGGERS.some(k => lo.includes(k)); let dallePrompt = text; if (lastImageRevised && !isFresh) { dallePrompt = `Continuation of a previous image. The previous image showed: "${lastImageRevised}". Apply this change to that image concept: ${text}`; } const r = await fetch('https://api.openai.com/v1/images/generations', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + CFG.openaiKey }, body: JSON.stringify({ model: 'dall-e-3', prompt: dallePrompt, n: 1, size: '1024x1024', quality: 'hd', style: 'vivid' }) }); if (!r.ok) { const e = await r.json().catch(() => ({})); throw new Error(e?.error?.message || 'DALL-E error ' + r.status); } const j = await r.json(); const imgUrl = j?.data?.[0]?.url || ''; const revised = j?.data?.[0]?.revised_prompt || dallePrompt; // Store revised prompt for follow-up continuations lastImageRevised = revised; output = `SIMON generated

${revised.replace(/ `; retRoute = 'generate_image'; } else /* ────── OPENAI ────── */ if (provider === 'openai' && CFG.openaiKey) { const r = await fetch(CFG.openaiEp || 'https://api.openai.com/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + CFG.openaiKey }, body: JSON.stringify({ model: CFG.openaiModel || 'gpt-4o', messages: [{ role: 'system', content: CFG.openaiSys || sys }, { role: 'user', content: user }], max_tokens: 4096, temperature: 0.7 }) }); if (!r.ok) { const e = await r.json().catch(() => ({})); throw new Error(e?.error?.message || 'OpenAI error ' + r.status); } const j = await r.json(); output = j?.choices?.[0]?.message?.content || ''; retRoute = route; } else /* ────── CLAUDE ────── */ if (provider === 'claude' && CFG.claudeKey) { const r = await fetch('https://api.anthropic.com/v1/messages', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': CFG.claudeKey, 'anthropic-version': '2023-06-01', 'anthropic-dangerous-direct-browser-access': 'true' }, body: JSON.stringify({ model: CFG.claudeModel || 'claude-sonnet-4-6', max_tokens: 4096, system: CFG.claudeSys || sys, messages: [{ role: 'user', content: user }] }) }); if (!r.ok) { const e = await r.json().catch(() => ({})); throw new Error(e?.error?.message || 'Claude error ' + r.status); } const j = await r.json(); output = j?.content?.[0]?.text || ''; retRoute = route; } else /* ────── GROK ────── */ if (provider === 'grok' && CFG.grokKey) { const r = await fetch('https://api.x.ai/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + CFG.grokKey }, body: JSON.stringify({ model: CFG.grokModel || 'grok-3', messages: [{ role: 'system', content: CFG.grokSys || sys }, { role: 'user', content: user }], max_tokens: 2048, temperature: 0.7 }) }); if (!r.ok) { const e = await r.json().catch(() => ({})); throw new Error(e?.error?.message || 'Grok error ' + r.status); } const j = await r.json(); output = j?.choices?.[0]?.message?.content || ''; retRoute = route; } else /* ────── SIMON BACKEND ────── */ { const r = await fetch(CFG.aiEp, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt: text, route, session: CFG.aiSess || 'voice_os' }) }); const raw = await r.text(); let j; try { j = JSON.parse(raw); } catch (e) { if (raw.trim().startsWith('<')) { output = raw; retRoute = 'generate_html'; } else throw new Error('Invalid backend response'); } if (j) { if (j.status !== 'success') throw new Error(j?.message || 'Backend failed'); output = j?.response?.data?.output || ''; retRoute = j?.response?.meta?.route || route; } } } catch (e) { const sc = document.getElementById('scanner'); if (sc) sc.classList.remove('on'); const errMsg = e.message || 'Unknown error'; const wsIt = document.getElementById('wsIdleTitle'); if (wsIt) { wsIt.textContent = '⚠ ' + errMsg.slice(0, 60); setTimeout(() => { if (S === 'idle') wsIt.textContent = 'SIMON READY'; }, 7000); } if (wmTxEl) { wmTxEl.textContent = '⚠ ' + errMsg.slice(0, 80); setTimeout(() => wmTxEl.textContent = '', 7000); } go('idle'); speak('There was an error. ' + errMsg.slice(0, 100)); return; } const lat = Math.round(performance.now() - t0); const lv = document.getElementById('latVal'); if (lv) lv.textContent = lat; const lc = document.getElementById('latChip'); if (lc) lc.style.display = ''; const wlv = document.getElementById('wmLatVal'); if (wlv) wlv.textContent = lat; const wlc = document.getElementById('wmVbLat'); if (wlc) wlc.style.display = ''; if (!output) { const sc = document.getElementById('scanner'); if (sc) sc.classList.remove('on'); go('idle'); speak('SIMON returned an empty response.'); return; } lastOut = output; const isImg = retRoute === 'generate_image'; const isH = isImg || retRoute === 'generate_3d' || retRoute === 'generate_html' || retRoute === 'generate_page' || output.trim().startsWith(' 200); lastOutType = isH ? 'html' : 'text'; const pLabel = { openai: 'GPT-4o', claude: 'Claude', grok: 'Grok', dalle: 'DALL-E 3', simon: 'SIMON', freeai: 'Free AI' }[provider] || provider.toUpperCase(); // Save to persistent memory const artId = isImg ? 'image_latest' : isH ? 'html_latest' : 'text_latest'; const revised = isImg ? (lastImageRevised || text) : text; memSaveArtifact(artId, isImg ? 'image' : isH ? 'html' : 'text', output, text, revised); memLogHistory(text, retRoute, provider, artId); historyPush(output, lastOutType, text, artId); try { const events = JSON.parse(localStorage.getItem('simon_carrier_events')||'[]'); events.push({ ts:Date.now(), event:'job_complete', chain:window.__SIMON_CARRIER_CHAIN, route:retRoute, provider, prompt:text.slice(0,80), latencyMs:Math.round(performance.now()-t0), fallback:!!(window.__SIMON_CARRIER_CHAIN?.brain?.fallback) }); if (events.length>100) events.splice(0,events.length-100); localStorage.setItem('simon_carrier_events', JSON.stringify(events)); } catch(e){} const speakVerb = intent === 'patch' ? 'Done. ' : ''; go('executing'); showRoutingCards('executing', text); setTimeout(() => { const sc = document.getElementById('scanner'); if (sc) sc.classList.remove('on'); go('completed', { type: lastOutType, content: output, prompt: text }); const wt = document.getElementById('wsTitle'); if (wt) wt.textContent = pLabel + ' — ' + (text.length > 40 ? text.slice(0, 40) + '…' : text).toUpperCase(); if (isImg) speak(speakVerb + 'Image generated. Say a follow-up to refine it, or ask for something new.', null, false, true); else if (retRoute === 'generate_3d') speak(speakVerb + 'Three D scene ready.', null, false, true); else if (isH) speak(speakVerb + (intent === 'patch' ? "I patched that for you. Want another change?" : "Here's what I built for you."), null, false, true); else if (output.length < 220) speak(output, null, false, true); else speak(speakVerb + 'Your content is ready.', null, false, true); }, 950); } /* ─────────── WORKSPACE OUTPUT ─────────── */ function renderOut({type, content}) { const ifr = document.getElementById('wsIframe'); const txt = document.getElementById('wsText'); if (type === 'html') { if (txt) txt.style.display = 'none'; if (ifr) { ifr.style.display = 'block'; ifr.srcdoc = content; } } else { if (ifr) ifr.style.display = 'none'; if (txt) { txt.style.display = 'block'; txt.textContent = content; } } } function clearOut() { const ifr = document.getElementById('wsIframe'); if (ifr) { ifr.srcdoc = ''; ifr.style.display = 'none'; } const txt = document.getElementById('wsText'); if (txt) { txt.textContent = ''; txt.style.display = 'none'; } const wo = document.getElementById('wsOutput'); if (wo) wo.classList.remove('show'); const lc = document.getElementById('latChip'); if (lc) lc.style.display = 'none'; const wlc = document.getElementById('wmVbLat'); if (wlc) wlc.style.display = 'none'; } /* ─────────── PIPELINE DESC HELPER (defined early to avoid TDZ issues) ─────────── */ const PIPELINE_DESCS = { quick: '⚡ Single AI call. Fastest — good for quick answers and simple pages.', standard: '✦ Two-step pass: GPT-4o builds → Claude polishes copy + design. Better output for important pages. ~2× slower.', premium: '🔥 Four-step adversarial refinement: Claude plans → GPT-4o builds → Claude critiques → GPT-4o refines. Best quality. Requires both OpenAI + Claude keys. ~4× slower.' }; function updatePipelineDesc(v) { const el = document.getElementById('pipeline-desc'); if (el) el.textContent = PIPELINE_DESCS[v] || ''; } /* ─────────── VOICE AGENT ─────────── */ function buildRec() { const SR = window.SpeechRecognition || window.webkitSpeechRecognition; if (!SR) return null; const r = new SR(); r.lang = CFG.sttLang || 'en-US'; r.interimResults = true; r.maxAlternatives = 1; r.continuous = false; r.onstart = () => { isListen = true; go('listening'); emitPts(.80); }; r.onresult = (ev) => { let fin = '', int = ''; for (let i = ev.resultIndex; i < ev.results.length; i++) { if (ev.results[i].isFinal) fin += ev.results[i][0].transcript; else int += ev.results[i][0].transcript; } const d = fin || int; if (d) { const ot = document.getElementById('orbTx'); if (ot) ot.textContent = d; const wt = document.getElementById('wmTx'); if (wt) wt.textContent = '"' + d + '"'; } if (fin) { const t = fin.trim(); const lo = t.toLowerCase(); // Voice control commands — intercept before AI routing if (lo === 'undo' || lo === 'go back' || lo === 'undo that' || lo === 'revert') { doUndo(); return; } if (lo === 'start over' || lo === 'clear' || lo === 'reset' || lo === 'new session') { doStartOver(); return; } if (lo === 'next' || lo === 'next page' || lo === 'show next') { doNext(); return; } if (lo === 'previous' || lo === 'prev' || lo === 'previous page' || lo === 'go previous' || lo === 'go back') { doPrevious(); return; } // Check for modify/grow intent keywords to keep proceed panel useful if (lo.startsWith('modify ') || lo.startsWith('remove ') || lo.startsWith('replace ') || lo.startsWith('change ') || lo.startsWith('add ') || lo.startsWith('update ')) { hideProceedPanel(); } callSIMON(t); } }; r.onerror = (e) => { isListen = false; go('idle'); const MIC_MSGS = { 'not-allowed': '⚠ Mic blocked — click the 🔒 lock in your browser bar and allow microphone.', 'no-speech': '⚠ Nothing heard. Tap Talk and speak clearly.', 'audio-capture': '⚠ No microphone detected.', 'network': '⚠ Network error. Check connection.', 'aborted': 'Voice stopped.', 'service-not-allowed': '⚠ Speech API blocked — try Chrome or Edge.' }; const msg = MIC_MSGS[e.error] || ('⚠ Voice error: ' + e.error); const ot = document.getElementById('orbTx'); if (ot) { ot.textContent = msg; setTimeout(() => ot.textContent = '', 6000); } const wt = document.getElementById('wmTx'); if (wt) { wt.textContent = msg; setTimeout(() => wt.textContent = '', 6000); } const wsIt = document.getElementById('wsIdleTitle'); if (wsIt && S === 'idle') { wsIt.textContent = msg; setTimeout(() => { if (S === 'idle') wsIt.textContent = 'SIMON READY'; }, 6000); } }; r.onend = () => { isListen = false; if (S === 'listening') go('idle'); const wt = document.getElementById('wmTx'); if (wt) setTimeout(() => { if (S !== 'thinking') wt.textContent = ''; }, 3000); }; return r; } /* Unlock browser audio context on first user gesture — required for async TTS to work */ function _unlockAudio() { if (_audioUnlocked) return; try { _audioCtx = _audioCtx || new (window.AudioContext || window.webkitAudioContext)(); if (_audioCtx.state === 'suspended') _audioCtx.resume(); // Play a 1-frame silent buffer so the context becomes trusted const buf = _audioCtx.createBuffer(1, 1, 22050); const src = _audioCtx.createBufferSource(); src.buffer = buf; src.connect(_audioCtx.destination); src.start(0); _audioUnlocked = true; } catch (e) {} // Also tap HTMLAudio — unlocks the audio element path used by OpenAI TTS try { const sil = new Audio('data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQAAAAA='); sil.volume = 0; sil.play().then(() => { _audioUnlocked = true; }).catch(() => {}); } catch (e) {} } function startListen() { if (isListen) return; // Unlock audio on this user gesture so async TTS plays work after the fetch _unlockAudio(); // Cancel any active TTS so mic doesn't echo or block window.speechSynthesis?.cancel(); clearTimeout(spkTimer); // Always rebuild rec — reusing a stale SpeechRecognition object causes // silent InvalidStateError after image/TTS cycles, leaving the mic dead rec = buildRec(); if (!rec) return; try { rec.start(); } catch (e) { // If start() still throws (e.g. device busy), retry once after a short delay setTimeout(() => { rec = buildRec(); try { rec?.start(); } catch (e2) {} }, 250); } } function stopAll() { if (rec && isListen) try { rec.stop(); } catch (e) {} window.speechSynthesis?.cancel(); if (currentAudio) { try { currentAudio.pause(); currentAudio.src = ''; } catch (e) {} currentAudio = null; } clearTimeout(spkTimer); go('idle'); const ot = document.getElementById('orbTx'); if (ot) ot.textContent = ''; const wt = document.getElementById('wmTx'); if (wt) wt.textContent = ''; } /* ─────────── SPEAKER AGENT ─────────── */ const OAI_TTS_VOICES = ['alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer']; function loadVoices() { const eng = CFG.ttsEng || 'browser'; const sel = document.getElementById('tts-voice'); if (!sel) return; sel.innerHTML = ''; if (eng === 'openai') { // OpenAI TTS voices — no API call needed, list is fixed OAI_TTS_VOICES.forEach(v => { const o = document.createElement('option'); o.value = v; const labels = { alloy: 'Alloy — balanced', echo: 'Echo — deep', fable: 'Fable — expressive ✦', onyx: 'Onyx — authoritative', nova: 'Nova — bright', shimmer: 'Shimmer — soft' }; o.textContent = labels[v] || v; sel.appendChild(o); }); sel.value = CFG.ttsVoice && OAI_TTS_VOICES.includes(CFG.ttsVoice) ? CFG.ttsVoice : 'fable'; } else { // Browser native voices if (!window.speechSynthesis) return; voices = window.speechSynthesis.getVoices(); sel.innerHTML = ''; voices.forEach(v => { const o = document.createElement('option'); o.value = v.voiceURI; o.textContent = `${v.name} (${v.lang})`; sel.appendChild(o); }); if (CFG.ttsVoice && !OAI_TTS_VOICES.includes(CFG.ttsVoice)) sel.value = CFG.ttsVoice; } } function getVoice() { return CFG.ttsVoice ? voices.find(v => v.voiceURI === CFG.ttsVoice) || null : null; } /* Shared post-speak logic — handles alwaysOn loop, one-shot image follow-up, and tap hint */ function _onSpeakEnd(cb, oneShot=false) { clearTimeout(spkTimer); cb?.(); if (S === 'speaking') go(lastOut ? 'completed' : 'idle'); if (!isListen) { if (alwaysOn) { // If we have rendered content, show proceed panel instead of auto-looping if (lastOut) { showProceedPanel(); } else { // Full conversational loop (no content): count down then re-listen let count = 3; const ot = document.getElementById('orbTx'); const wt = document.getElementById('wmTx'); const tick = () => { if (count <= 0) { if (ot) ot.textContent = ''; if (wt) wt.textContent = ''; startListen(); return; } const msg = 'Listening in ' + count + 's — or tap Talk'; if (ot) ot.textContent = msg; if (wt) wt.textContent = msg; count--; setTimeout(tick, 1000); }; tick(); } } else if (oneShot) { // After render: STOP and show "how to proceed" panel // DO NOT auto-relisten — this was burning API credits showProceedPanel(); } else if (S === 'completed') { // Passive hint only const ot = document.getElementById('orbTx'); const wt = document.getElementById('wmTx'); if (ot) { ot.textContent = 'Tap Talk to continue'; setTimeout(() => { if (ot.textContent === 'Tap Talk to continue') ot.textContent = ''; }, 5000); } if (wt) { wt.textContent = 'Tap Talk to ask a follow-up'; setTimeout(() => { if (wt.textContent === 'Tap Talk to ask a follow-up') wt.textContent = ''; }, 5000); } } } } /* Main speak() — routes to OpenAI TTS or browser depending on settings */ function speak(text, cb=null, force=false, oneShot=false) { if (!text) { cb?.(); return; } if (!CFG.ttsAuto && !cb && !force) return; // ── OpenAI TTS path ── if ((CFG.ttsEng === 'openai') && CFG.openaiKey) { // Cancel any active browser TTS and prior audio window.speechSynthesis?.cancel(); if (currentAudio) { try { currentAudio.pause(); currentAudio.src = ''; } catch (e) {} currentAudio = null; } const voice = CFG.ttsVoice && OAI_TTS_VOICES.includes(CFG.ttsVoice) ? CFG.ttsVoice : 'fable'; if (S !== 'executing' && S !== 'completed') go('speaking'); spkPulse(); fetch('https://api.openai.com/v1/audio/speech', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + CFG.openaiKey }, body: JSON.stringify({ model: 'tts-1', input: text, voice, response_format: 'mp3' }) }).then(r => { if (!r.ok) throw new Error('OpenAI TTS ' + r.status); return r.blob(); }).then(blob => { const url = URL.createObjectURL(blob); const aud = new Audio(url); currentAudio = aud; aud.onended = () => { URL.revokeObjectURL(url); currentAudio = null; _onSpeakEnd(cb, oneShot); }; aud.onerror = () => { URL.revokeObjectURL(url); currentAudio = null; clearTimeout(spkTimer); cb?.(); // Fallback to browser TTS on audio error _speakBrowser(text, cb, oneShot); }; aud.play().catch(() => { // Autoplay blocked — fall through to browser URL.revokeObjectURL(url); currentAudio = null; _speakBrowser(text, cb, oneShot); }); }).catch(() => { // Network / API error — fall back to browser TTS _speakBrowser(text, cb, oneShot); }); return; } // ── Browser TTS path ── _speakBrowser(text, cb, oneShot); } function _speakBrowser(text, cb=null, oneShot=false) { if (!window.speechSynthesis) { _speakFallback(text, cb, oneShot); return; } window.speechSynthesis.cancel(); // Resume suspended synthesis (common after tab switch or long idle) window.speechSynthesis.resume(); const u = new SpeechSynthesisUtterance(text); const v = getVoice(); if (v) u.voice = v; u.rate = 0.92; u.pitch = 1; let started = false; const watchdog = setTimeout(() => { // If onstart hasn't fired within 2.5s, synthesis is stuck — show fallback if (!started) { window.speechSynthesis.cancel(); _speakFallback(text, cb, oneShot); } }, 2500); u.onstart = () => { started = true; clearTimeout(watchdog); if (S !== 'executing' && S !== 'completed') go('speaking'); spkPulse(); }; u.onend = () => { clearTimeout(watchdog); _onSpeakEnd(cb, oneShot); }; u.onerror = (e) => { clearTimeout(watchdog); if (e.error === 'not-allowed' || e.error === 'audio-busy') { _speakFallback(text, cb, oneShot); return; } clearTimeout(spkTimer); cb?.(); }; window.speechSynthesis.speak(u); } /* Visual fallback: shows response text as an on-screen toast when all TTS paths fail */ function _speakFallback(text, cb=null, oneShot=false) { clearTimeout(spkTimer); if (S !== 'executing' && S !== 'completed') go(lastOut ? 'completed' : 'idle'); // Flash the text in the UI so user can read it const ot = document.getElementById('orbTx'); const wt = document.getElementById('wmTx'); const SHORT = text.length < 160 ? text : text.slice(0, 157) + '…'; if (ot) { ot.textContent = '🔇 ' + SHORT; setTimeout(() => { if (ot.textContent.startsWith('🔇')) ot.textContent = ''; }, 7000); } if (wt) { wt.textContent = '🔇 ' + SHORT; setTimeout(() => { if (wt.textContent.startsWith('🔇')) wt.textContent = ''; }, 7000); } cb?.(); if (oneShot && !isListen) { setTimeout(() => { if (!isListen && S !== 'thinking' && S !== 'executing') startListen(); }, 2000); } } /* ─────────── MODE TOGGLE ─────────── */ function setMode(mode) { const isWork = mode === 'work'; // Safe: toggle only the m-* mode class, never touch s-* state classes document.body.classList.remove('m-orb', 'm-work'); document.body.classList.add('m-' + mode); document.getElementById('mBtnOrb').classList.toggle('on', !isWork); document.getElementById('mBtnWork').classList.toggle('on', isWork); if (isWork && document.getElementById('wmGrid').children.length === 0) buildAgentCards(); localStorage.setItem('simon_mode', mode); } document.getElementById('mBtnOrb').addEventListener('click', () => setMode('orb')); document.getElementById('mBtnWork').addEventListener('click', () => setMode('work')); /* ─────────── CONTROLS — ORB MODE ─────────── */ document.getElementById('omTalkBtn').addEventListener('click', () => startListen()); document.getElementById('omStopBtn').addEventListener('click', () => stopAll()); document.getElementById('wsReset').addEventListener('click', () => { lastOut = ''; lastOutType = 'text'; hideProceedPanel(); clearOut(); go('idle'); }); document.getElementById('oClear').addEventListener('click', () => { lastOut = ''; lastOutType = 'text'; hideProceedPanel(); clearOut(); go('idle'); }); document.getElementById('oSpeak').addEventListener('click', () => { if (lastOut) speak(lastOut, null, true); }); document.getElementById('oDownload').addEventListener('click', () => { if (!lastOut) return; const a = document.createElement('a'); a.href = URL.createObjectURL(new Blob([lastOut], { type: lastOutType === 'html' ? 'text/html' : 'text/plain' })); a.download = 'simon-output.' + (lastOutType === 'html' ? 'html' : 'txt'); a.click(); }); document.getElementById('aoToggle').addEventListener('change', function() { alwaysOn = this.checked; document.getElementById('wmAoToggle').checked = this.checked; if (alwaysOn) startListen(); }); document.getElementById('connBtn').addEventListener('click', () => document.getElementById('cc').classList.add('open')); document.getElementById('orbWrap').addEventListener('click', () => { if (S === 'idle') startListen(); else if (S === 'completed') speak(lastOut.length < 280 ? lastOut : 'Content is ready in the workspace.'); }); /* ─────────── CONTROLS — WORK MODE ─────────── */ document.getElementById('wmTalkBtn').addEventListener('click', () => startListen()); document.getElementById('wmTalkBtn2').addEventListener('click', () => startListen()); document.getElementById('wmStopBtn').addEventListener('click', () => stopAll()); document.getElementById('wmConnBtn').addEventListener('click', () => document.getElementById('cc').classList.add('open')); document.getElementById('sbConnBtn').addEventListener('click', () => document.getElementById('cc').classList.add('open')); document.getElementById('sbRunSession').addEventListener('click', () => startListen()); document.getElementById('sbCheckAgents').addEventListener('click', () => { buildAgentCards(); const wt = document.getElementById('wmTx'); if (wt) { wt.textContent = 'All agents checked ✓'; setTimeout(() => wt.textContent = '', 2500); } }); document.getElementById('wmAoToggle').addEventListener('change', function() { alwaysOn = this.checked; document.getElementById('aoToggle').checked = this.checked; if (alwaysOn) startListen(); }); document.getElementById('wmPanelClose').addEventListener('click', () => { selectedAgent = null; document.querySelectorAll('.agCard').forEach(c => c.classList.remove('selected')); const t = document.getElementById('wmPanelTitle'); if (t) t.textContent = 'Selected Agent'; const pc = document.getElementById('wmPanelContent'); if (pc) { pc.innerHTML = ''; const emp = document.createElement('div'); emp.className = 'wpEmpty'; emp.innerHTML = '

No Agent Selected
Click an agent card to view details.
'; pc.appendChild(emp); } const b = document.getElementById('wmPanelBar'); if (b) b.style.display = 'none'; }); document.getElementById('wpSpeak').addEventListener('click', () => { if (lastOut) speak(lastOut, null, true); }); document.getElementById('wpSave').addEventListener('click', () => { if (!lastOut) return; const a = document.createElement('a'); a.href = URL.createObjectURL(new Blob([lastOut], { type: 'text/html' })); a.download = 'simon-output.html'; a.click(); }); document.getElementById('wpClear').addEventListener('click', () => { go('idle'); const t = document.getElementById('wmPanelTitle'); if (t) t.textContent = 'Selected Agent'; const pc = document.getElementById('wmPanelContent'); if (pc) pc.innerHTML = '
Cleared
'; const b = document.getElementById('wmPanelBar'); if (b) b.style.display = 'none'; }); // Sidebar filter items document.querySelectorAll('.sbItem[data-filter]').forEach(item => { item.addEventListener('click', () => { document.querySelectorAll('.sbItem').forEach(i => i.classList.remove('on')); item.classList.add('on'); }); }); // Filter bar badges document.querySelectorAll('.wFBadge[data-f]').forEach(badge => { badge.addEventListener('click', () => { document.querySelectorAll('.wFBadge').forEach(b => b.classList.remove('on')); badge.classList.add('on'); }); }); /* ─────────── KEYBOARD ─────────── */ window.addEventListener('keydown', e => { if (e.key === ' ' && document.activeElement.tagName !== 'INPUT' && document.activeElement.tagName !== 'TEXTAREA') { e.preventDefault(); if (S === 'idle') startListen(); } if (e.key === 'Escape') { if (document.getElementById('cc').classList.contains('open')) document.getElementById('cc').classList.remove('open'); else stopAll(); } }); /* ─────────── CONNECTION CENTER ─────────── */ document.getElementById('ccX').addEventListener('click', () => document.getElementById('cc').classList.remove('open')); document.getElementById('ccBg').addEventListener('click', e => { if (e.target.id === 'ccBg') document.getElementById('cc').classList.remove('open'); }); document.querySelectorAll('.cTab').forEach(tab => { tab.addEventListener('click', () => { document.querySelectorAll('.cTab').forEach(t => t.classList.remove('on')); document.querySelectorAll('.cSec').forEach(s => s.classList.remove('on')); tab.classList.add('on'); const sec = document.getElementById('t-' + tab.dataset.t); if (sec) sec.classList.add('on'); }); }); const _pmEl = document.getElementById('pipeline-mode'); if (_pmEl) _pmEl.addEventListener('change', function() { CFG.pipeline = this.value; updatePipelineDesc(this.value); }); document.getElementById('ai-provider').addEventListener('change', function() { CFG.aiProvider = this.value; updateProviderUI(); }); ['oai-key', 'cla-key', 'grk-key'].forEach(id => { const el = document.getElementById(id); if (el) el.addEventListener('input', () => updateProviderUI()); }); document.getElementById('smart-route').addEventListener('change', function() { CFG.smartRoute = this.checked; }); document.getElementById('prompt-enhance').addEventListener('change', function() { CFG.promptEnhance = this.checked; }); document.getElementById('stt-eng').addEventListener('change', function() { document.getElementById('stt-ep-f').style.display = this.value !== 'browser' ? 'flex' : 'none'; document.getElementById('stt-key-f').style.display = (this.value === 'whisper' || this.value === 'deepgram') ? 'flex' : 'none'; }); document.getElementById('tts-eng').addEventListener('change', function() { CFG.ttsEng = this.value; document.getElementById('tts-key-f').style.display = this.value !== 'browser' ? 'flex' : 'none'; // Refresh voice list to match engine (OpenAI voices vs browser voices) loadVoices(); }); /* ─── Carrier Mode Controller ─── */ function setCarrierMode(mode) { CFG.carrierMode = mode; saveCfg(); const labels = { manual: 'Manual — your explicit STT / Brain / TTS selection is used', smart: 'Smart — routing matrix selects carrier chain based on detected intent', fallback: 'Fallback — preferred carrier tried first, auto-switches if unavailable' }; const el = document.getElementById('carrierModeStatus'); if (el) el.innerHTML = 'Current: ' + mode.charAt(0).toUpperCase()+mode.slice(1) + ' — ' + (labels[mode]||mode); ['manual','smart','fallback'].forEach(m => { const btn = document.getElementById('cm'+m.charAt(0).toUpperCase()+m.slice(1)); if (!btn) return; if (m === mode) { btn.style.border = '1px solid rgba(255,200,80,.5)'; btn.style.background = 'rgba(255,200,80,.12)'; btn.style.color = '#eef4ff'; } else { btn.style.border = '1px solid rgba(255,255,255,.10)'; btn.style.background = 'rgba(0,0,0,.22)'; btn.style.color = '#8fa0c8'; } }); try { const events = JSON.parse(localStorage.getItem('simon_carrier_events')||'[]'); events.push({ ts:Date.now(), event:'mode_change', mode }); if (events.length > 100) events.splice(0, events.length-100); localStorage.setItem('simon_carrier_events', JSON.stringify(events)); } catch(e){} } /* ── Master Key Quick-Set ── */ document.getElementById('masterKeyApply').addEventListener('click', () => { const raw = document.getElementById('master-key').value.trim(); const st = document.getElementById('masterKeyStatus'); if (!raw) { if(st) st.textContent = '⚠ Paste a key first.'; return; } let matched = ''; if (raw.startsWith('sk-proj-') || raw.startsWith('sk-')) { document.getElementById('oai-key').value = raw; CFG.openaiKey = raw; matched = 'OpenAI'; } else if (raw.startsWith('sk-ant-')) { document.getElementById('cla-key').value = raw; CFG.claudeKey = raw; matched = 'Claude (Anthropic)'; } else if (raw.startsWith('xai-')) { document.getElementById('grk-key').value = raw; CFG.grokKey = raw; matched = 'Grok (xAI)'; } else { // Unknown prefix — ask user which provider const choice = prompt('Key prefix not recognized. Which provider?\n1 = OpenAI\n2 = Claude\n3 = Grok'); if (choice === '1') { document.getElementById('oai-key').value = raw; CFG.openaiKey = raw; matched = 'OpenAI'; } else if (choice === '2') { document.getElementById('cla-key').value = raw; CFG.claudeKey = raw; matched = 'Claude'; } else if (choice === '3') { document.getElementById('grk-key').value = raw; CFG.grokKey = raw; matched = 'Grok'; } else { if(st) st.textContent = '⚠ Cancelled.'; return; } } saveCfg(); if (st) st.textContent = '✓ ' + matched + ' key applied & saved.'; document.getElementById('master-key').value = ''; setTimeout(() => { if(st) st.textContent = ''; }, 3500); }); /* enter key in master key input */ document.getElementById('master-key').addEventListener('keydown', (e) => { if (e.key === 'Enter') document.getElementById('masterKeyApply').click(); }); /* enter key in proceed text input */ document.getElementById('wsProceedTxt').addEventListener('keydown', (e) => { if (e.key === 'Enter') procSubmitText(); }); document.getElementById('saveConn').addEventListener('click', () => { CFG.aiProvider = document.getElementById('ai-provider').value; CFG.smartRoute = document.getElementById('smart-route').checked; CFG.promptEnhance = document.getElementById('prompt-enhance').checked; CFG.pipeline = document.getElementById('pipeline-mode').value; CFG.openaiKey = document.getElementById('oai-key').value.trim(); CFG.openaiModel = document.getElementById('oai-model').value; CFG.openaiEp = document.getElementById('oai-ep').value.trim() || DFLT.openaiEp; CFG.openaiSys = document.getElementById('oai-sys').value.trim(); CFG.claudeKey = document.getElementById('cla-key').value.trim(); CFG.claudeModel = document.getElementById('cla-model').value; CFG.claudeSys = document.getElementById('cla-sys').value.trim(); CFG.grokKey = document.getElementById('grk-key').value.trim(); CFG.grokModel = document.getElementById('grk-model').value; CFG.grokSys = document.getElementById('grk-sys').value.trim(); CFG.aiEp = document.getElementById('ai-ep').value.trim() || DFLT.aiEp; CFG.aiStat = document.getElementById('ai-stat').value.trim() || DFLT.aiStat; CFG.aiSess = document.getElementById('ai-sess').value.trim() || DFLT.aiSess; CFG.aiRoute = document.getElementById('ai-route').value; CFG.sttEng = document.getElementById('stt-eng').value; CFG.sttEp = document.getElementById('stt-ep').value.trim(); CFG.sttKey = document.getElementById('stt-key').value.trim(); CFG.sttLang = document.getElementById('stt-lang').value; CFG.ttsEng = document.getElementById('tts-eng').value; CFG.ttsKey = document.getElementById('tts-key').value.trim(); CFG.ttsVoice = document.getElementById('tts-voice').value; CFG.ttsAuto = document.getElementById('tts-auto').checked; CFG.rMode = document.getElementById('r-mode').value; CFG.rAuto = document.getElementById('r-auto').checked; CFG.rScan = document.getElementById('r-scan').checked; CFG.exEp = document.getElementById('ex-ep').value.trim(); CFG.exFmt = document.getElementById('ex-fmt').value; saveCfg(); rec = buildRec(); startPolling(); updateProviderUI(); document.getElementById('cc').classList.remove('open'); const wt = document.getElementById('wsIdleTitle'); if (wt) { wt.textContent = 'CONNECTIONS SAVED ✓'; setTimeout(() => { if (S === 'idle') wt.textContent = 'SIMON READY'; }, 2400); } }); function updateProviderUI() { const p = CFG.aiProvider; // Show all blocks — each shows its status independently ['freeai', 'openai', 'claude', 'grok', 'simon'].forEach(id => { const bl = document.getElementById('ai-' + id + '-block'); if (bl) bl.style.display = ''; }); // Key status indicators const oaiKey = CFG.openaiKey || document.getElementById('oai-key')?.value || ''; const claKey = CFG.claudeKey || document.getElementById('cla-key')?.value || ''; const grkKey = CFG.grokKey || document.getElementById('grk-key')?.value || ''; const upd = (sdId, slId, hasKey, label) => { const sd = document.getElementById(sdId); const sl = document.getElementById(slId); if (sd) sd.className = 'sd ' + (hasKey ? 'on' : ''); if (sl) sl.textContent = hasKey ? label + ' ✓' : 'No key'; }; upd('oai-sd', 'oai-sl', !!oaiKey, 'OpenAI'); upd('cla-sd', 'cla-sl', !!claKey, 'Claude'); upd('grk-sd', 'grk-sl', !!grkKey, 'Grok'); // Brain badge const bsd = document.getElementById('brain-sd'); const bsl = document.getElementById('brain-sl'); const pNames = { openai: 'GPT-4o', claude: 'Claude', grok: 'Grok', simon: 'SIMON', freeai: 'Free AI', auto: 'Auto-route' }; if (bsd) bsd.className = 'sd on'; if (bsl) bsl.textContent = pNames[p] || p; } function bindUI() { document.getElementById('ai-provider').value = CFG.aiProvider || 'freeai'; document.getElementById('smart-route').checked = !!CFG.smartRoute; document.getElementById('prompt-enhance').checked = !!CFG.promptEnhance; document.getElementById('pipeline-mode').value = CFG.pipeline || 'quick'; updatePipelineDesc(CFG.pipeline || 'quick'); document.getElementById('oai-key').value = CFG.openaiKey || ''; document.getElementById('oai-model').value = CFG.openaiModel || 'gpt-4o'; document.getElementById('oai-ep').value = CFG.openaiEp || DFLT.openaiEp; document.getElementById('oai-sys').value = CFG.openaiSys || ''; document.getElementById('cla-key').value = CFG.claudeKey || ''; document.getElementById('cla-model').value = CFG.claudeModel || 'claude-sonnet-4-6'; document.getElementById('cla-sys').value = CFG.claudeSys || ''; document.getElementById('grk-key').value = CFG.grokKey || ''; document.getElementById('grk-model').value = CFG.grokModel || 'grok-3'; document.getElementById('grk-sys').value = CFG.grokSys || ''; document.getElementById('ai-ep').value = CFG.aiEp; document.getElementById('ai-stat').value = CFG.aiStat; document.getElementById('ai-sess').value = CFG.aiSess; document.getElementById('ai-route').value = CFG.aiRoute; document.getElementById('stt-eng').value = CFG.sttEng; document.getElementById('stt-ep').value = CFG.sttEp; document.getElementById('stt-lang').value = CFG.sttLang; document.getElementById('tts-eng').value = CFG.ttsEng; document.getElementById('tts-auto').checked = CFG.ttsAuto; document.getElementById('r-mode').value = CFG.rMode; document.getElementById('r-auto').checked = CFG.rAuto; document.getElementById('r-scan').checked = CFG.rScan; document.getElementById('ex-fmt').value = CFG.exFmt; document.getElementById('stt-ep-f').style.display = CFG.sttEng !== 'browser' ? 'flex' : 'none'; document.getElementById('stt-key-f').style.display = (CFG.sttEng === 'whisper' || CFG.sttEng === 'deepgram') ? 'flex' : 'none'; document.getElementById('tts-key-f').style.display = CFG.ttsEng !== 'browser' ? 'flex' : 'none'; updateProviderUI(); } /* ─────────── STATUS POLLING ─────────── */ async function pollStatus() { const p = CFG.aiProvider; let lbl = '', dotCls = 'red'; if (p === 'simon') { // Only provider that needs real network polling try { const r = await fetch(CFG.aiStat, { cache: 'no-store' }); const j = await r.json(); const ok = j?.status === 'online'; dotCls = ok ? 'green' : 'red'; lbl = 'SIMON ' + (ok ? 'online' : 'offline'); } catch (e) { dotCls = 'red'; lbl = 'SIMON offline'; } } else if (p === 'openai') { const ok = !!CFG.openaiKey; dotCls = ok ? 'green' : 'red'; lbl = ok ? 'GPT-4o ready' : 'OpenAI: add key'; } else if (p === 'claude') { const ok = !!CFG.claudeKey; dotCls = ok ? 'green' : 'red'; lbl = ok ? 'Claude ready' : 'Claude: add key'; } else if (p === 'grok') { const ok = !!CFG.grokKey; dotCls = ok ? 'green' : 'red'; lbl = ok ? 'Grok ready' : 'Grok: add key'; } else if (p === 'auto') { // Show all connected brains const brains = [CFG.openaiKey && 'GPT-4o', CFG.claudeKey && 'Claude', CFG.grokKey && 'Grok'].filter(Boolean); const ok = brains.length > 0; dotCls = ok ? 'green' : 'red'; lbl = ok ? brains.join(' · ') + ' active' : 'Auto: no keys set'; } else { dotCls = 'red'; lbl = 'Unknown provider'; } const bd = document.getElementById('beDot'); if (bd) bd.className = 'dot ' + dotCls; const bl = document.getElementById('beLbl'); if (bl) bl.textContent = lbl; const wd = document.getElementById('wmBeDot'); if (wd) wd.className = 'dot ' + dotCls; const wl = document.getElementById('wmBeLbl'); if (wl) wl.textContent = lbl; } function startPolling() { if (poller) clearInterval(poller); pollStatus(); poller = setInterval(pollStatus, CFG.pollMs || 3000); } /* ─────────── INIT ─────────── */ // loadVoices() checks CFG.ttsEng — for OpenAI it fills fixed list, for browser it waits for voices if (window.speechSynthesis) { window.speechSynthesis.onvoiceschanged = loadVoices; } loadVoices(); bindUI(); setCarrierMode(CFG.carrierMode||'smart'); rec = buildRec(); if (!rec) { ['omTalkBtn', 'wmTalkBtn', 'wmTalkBtn2'].forEach(id => { const b = document.getElementById(id); if (b) b.disabled = true; }); } startPolling(); const savedMode = localStorage.getItem('simon_mode') || 'orb'; setMode(savedMode); go('idle');