/** * GMIIE Voice Desk — Listen | Ask | Voice across xxxiii.io hub pages. * Depends on grok-voice.js for TTS (institutional HD voice synthesis /api/tts + speechSynthesis fallback). */ (function (global) { const DEFAULTS = { askEndpoint: '/api/gmiie/ask', ttsEndpoint: '/api/tts', contentSelector: 'main', voiceId: 'daniel', }; let cfg = { ...DEFAULTS }; let panelEl = null; let fabEl = null; let narratives = null; let recognition = null; let listening = false; function injectStyles() { if (document.getElementById('gvd-styles')) return; const s = document.createElement('style'); s.id = 'gvd-styles'; s.textContent = ` .gvd-fab{position:fixed;bottom:20px;right:20px;z-index:9998;font-family:var(--mono,'JetBrains Mono',Consolas,monospace);font-size:9px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;background:var(--band,#1a1a1a);color:var(--band-ink,rgba(255,255,255,.9));border:2px solid var(--gold,#b8953a);padding:12px 16px;cursor:pointer;box-shadow:0 8px 32px rgba(0,0,0,.35);transition:transform .2s,border-color .2s;} .gvd-fab:hover{transform:translateY(-2px);border-color:var(--gold2,#d4af55);} .gvd-panel{position:fixed;bottom:72px;right:20px;width:min(380px,calc(100vw - 32px));max-height:min(520px,calc(100vh - 100px));z-index:9999;background:var(--surface,var(--paper2,#f3f0e8));border:2px solid var(--rule,#d4c9a8);box-shadow:0 16px 48px rgba(0,0,0,.25);display:none;flex-direction:column;font-family:var(--sans,'Helvetica Neue',sans-serif);color:var(--ink,#1a1a1a);} .gvd-panel.open{display:flex;} .gvd-hd{padding:12px 14px;border-bottom:1px solid var(--rule,#d4c9a8);display:flex;align-items:center;justify-content:space-between;gap:8px;background:var(--paper2,var(--surface,#ece8dc));} .gvd-title{font-family:var(--mono,'JetBrains Mono',monospace);font-size:9px;font-weight:700;letter-spacing:.14em;text-transform:uppercase;color:var(--gold,#b8953a);} .gvd-close{background:none;border:1px solid var(--rule,#d4c9a8);cursor:pointer;font-size:14px;line-height:1;padding:2px 8px;color:var(--ink3,#777);} .gvd-tabs{display:flex;border-bottom:1px solid var(--rule,#d4c9a8);} .gvd-tab{flex:1;font-family:var(--mono,'JetBrains Mono',monospace);font-size:8px;font-weight:700;letter-spacing:.1em;text-transform:uppercase;padding:10px 6px;background:transparent;border:none;border-bottom:2px solid transparent;cursor:pointer;color:var(--ink3,#777);} .gvd-tab.active{color:var(--ink,#1a1a1a);border-bottom-color:var(--gold,#b8953a);} .gvd-body{padding:14px;overflow-y:auto;flex:1;font-size:13px;line-height:1.6;} .gvd-pane{display:none;}.gvd-pane.active{display:block;} .gvd-btn{font-family:var(--mono,'JetBrains Mono',monospace);font-size:8px;font-weight:700;letter-spacing:.1em;text-transform:uppercase;padding:8px 12px;cursor:pointer;border:1px solid var(--rule,#d4c9a8);background:var(--paper,var(--paper2,#faf8f3));color:var(--ink,#1a1a1a);margin:4px 4px 4px 0;} .gvd-btn.primary{background:var(--fraud,#7a1f2e);color:#fff;border-color:var(--fraud,#7a1f2e);} .gvd-btn.on,.gvd-listen-btn.on{background:var(--gold-bg,rgba(184,149,58,.15));border-color:var(--gold,#b8953a);color:var(--gold,#b8953a);} .gvd-listen-btn,.gvd-ask-btn{font-family:var(--mono,'JetBrains Mono',monospace);font-size:7px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;padding:4px 8px;cursor:pointer;border:1px solid var(--rule,#d4c9a8);background:var(--paper2,#f3f0e8);color:var(--ink2,#444);margin-top:6px;margin-right:6px;} .gvd-input{width:100%;min-height:72px;padding:10px;border:1px solid var(--rule,#d4c9a8);background:var(--paper,#faf8f3);font-family:var(--sans);font-size:13px;color:var(--ink,#1a1a1a);resize:vertical;} .gvd-answer{margin-top:12px;padding:10px;border-left:3px solid var(--gold,#b8953a);background:var(--gold-bg,rgba(184,149,58,.08));font-size:12px;line-height:1.65;white-space:pre-wrap;} .gvd-meta{font-family:var(--mono,'JetBrains Mono',monospace);font-size:7px;color:var(--ink3,#777);margin-top:8px;} .gvd-mic{width:48px;height:48px;border-radius:50%;border:2px solid var(--gold,#b8953a);background:var(--band,#1a1a1a);color:var(--gold,#b8953a);font-size:18px;cursor:pointer;display:flex;align-items:center;justify-content:center;margin:12px auto;} .gvd-mic.listening{border-color:var(--red,#c0392b);color:var(--red,#c0392b);animation:gvd-pulse 1.2s infinite;} @keyframes gvd-pulse{0%,100%{opacity:1}50%{opacity:.5}} .gvd-report-actions{margin-top:12px;display:flex;flex-wrap:wrap;gap:8px;} .gvd-voice-label{font-family:var(--mono,'JetBrains Mono',monospace);font-size:7px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--ink3,#777);margin-left:8px;} .gvd-narrative-actions{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px;} `; document.head.appendChild(s); } function narrativeToSpeech(n) { if (!n) return ''; const parts = []; if (n.headline) parts.push(n.headline + '.'); if (n.category) parts.push('Category: ' + n.category + '.'); if (n.summary) parts.push(n.summary); if (n.what_happened) parts.push('What happened. ' + n.what_happened); if (n.what_we_found && n.what_we_found.length) { parts.push('What we found. ' + n.what_we_found.join('. ') + '.'); } if (n.awareness && n.awareness.length) { parts.push('Awareness. ' + n.awareness.join('. ') + '.'); } if (n.gmiie_signal) parts.push('GMIIE signal. ' + n.gmiie_signal); if (n.lesson) parts.push('Lesson. ' + n.lesson); return parts.join('\n\n'); } function extractElementText(el) { if (!el) return ''; const clone = el.cloneNode(true); clone.querySelectorAll('script,style,button,.gvd-fab,.gvd-panel').forEach((n) => n.remove()); return (clone.innerText || '').replace(/\s+/g, ' ').trim(); } function pageContext() { const page = cfg.page || document.body.getAttribute('data-gmiie-page') || location.pathname.replace(/^\//, '').replace(/\.html$/, '') || 'hub'; const sel = global.getSelection?.(); const selectedText = sel && !sel.isCollapsed ? String(sel.toString()).trim() : ''; const ctx = { page, url: location.pathname }; if (selectedText) ctx.selectedText = selectedText.slice(0, 2000); if (cfg.fraudScore != null) ctx.fraudScore = cfg.fraudScore; if (cfg.fraudLevel) ctx.fraudLevel = cfg.fraudLevel; if (cfg.engine) ctx.engine = cfg.engine; return ctx; } function narrationContext(overrides) { const page = cfg.page || document.body.getAttribute('data-gmiie-page') || location.pathname.replace(/^\//, '').replace(/\.html$/, '') || 'hub'; const base = { page, url: location.pathname }; if (cfg.fraudScore != null) base.score = cfg.fraudScore; if (cfg.fraudLevel) base.level = cfg.fraudLevel; if (cfg.engine) base.engine = cfg.engine; if (cfg.typology) base.typology = cfg.typology; if (cfg.scamFamily) base.scam_family = cfg.scamFamily; return { ...base, ...(overrides || {}) }; } function speak(text, btn, speakOpts) { if (!text) return; const ctx = speakOpts?.context || narrationContext({ type: inferNarrationType(speakOpts) }); if (global.GrokVoice) { global.GrokVoice.speak(text, btn, { voice_id: cfg.voiceId, endpoint: cfg.ttsEndpoint, provider: 'auto', smart: speakOpts?.smart !== false, context: ctx, }); return; } const u = new SpeechSynthesisUtterance(String(text).slice(0, 5000)); u.rate = 0.87; global.speechSynthesis?.speak(u); } function inferNarrationType(speakOpts) { if (speakOpts?.type) return speakOpts.type; if (speakOpts?.narrativeId || speakOpts?.narrative) return 'narrative'; if (cfg.fraudScore != null || speakOpts?.report) return 'fraud_report'; if (cfg.page === 'legislative' || location.pathname.includes('legislative')) return 'legislative'; if (cfg.page === 'global-governance' || location.pathname.includes('global-governance')) return 'legislative'; if (cfg.page === 'fraud' || location.pathname.includes('fraud')) return 'fraud_report'; return 'narrative'; } async function ask(question, extraContext, answerEl, metaEl) { if (!question) return; if (answerEl) answerEl.textContent = 'Analyzing…'; if (metaEl) metaEl.textContent = ''; const context = { ...pageContext(), ...(extraContext || {}) }; try { const r = await fetch(cfg.askEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ question, context, mode: 'text' }), signal: AbortSignal.timeout(35000), }); const data = await r.json().catch(() => ({})); if (!r.ok) throw new Error(data.error || 'Ask failed ' + r.status); if (answerEl) answerEl.textContent = data.answer || 'No answer returned.'; if (metaEl) { const src = (data.sources || []).map((s) => s.ref || s.type).filter(Boolean).slice(0, 4); metaEl.textContent = (data.model || 'GMIIE') + (data.tools?.length ? ' · tools: ' + data.tools.join(', ') : '') + (src.length ? ' · sources: ' + src.join(', ') : ''); } return data; } catch (e) { if (answerEl) answerEl.textContent = 'Could not reach GMIIE agent. ' + (e.message || e); return null; } } function getNarrativeById(id) { if (!narratives || !id) return null; return (narratives.narratives || []).find((n) => n.id === id) || null; } function loadNarratives() { if (narratives) return Promise.resolve(narratives); return fetch('/data/fraud-narratives.json') .then((r) => (r.ok ? r.json() : null)) .then((d) => { narratives = d; return d; }) .catch(() => null); } function switchTab(name) { if (!panelEl) return; panelEl.querySelectorAll('.gvd-tab').forEach((t) => { t.classList.toggle('active', t.dataset.tab === name); }); panelEl.querySelectorAll('.gvd-pane').forEach((p) => { p.classList.toggle('active', p.dataset.pane === name); }); } function buildPanel() { injectStyles(); fabEl = document.createElement('button'); fabEl.type = 'button'; fabEl.className = 'gvd-fab'; fabEl.setAttribute('aria-label', 'Open GMIIE Voice Desk'); fabEl.textContent = 'GMIIE Voice Desk'; fabEl.addEventListener('click', () => { panelEl.classList.toggle('open'); }); panelEl = document.createElement('div'); panelEl.className = 'gvd-panel'; panelEl.setAttribute('role', 'dialog'); panelEl.setAttribute('aria-label', 'GMIIE Voice Desk'); panelEl.innerHTML = `
GMIIE Voice DeskGMIIE Analyst Voice

Read aloud page content, case narratives, or your text selection.

Speak your question — answers play via GMIIE Analyst Voice (institutional HD voice synthesis).

`; document.body.appendChild(fabEl); document.body.appendChild(panelEl); panelEl.querySelector('.gvd-close').addEventListener('click', () => panelEl.classList.remove('open')); panelEl.querySelectorAll('.gvd-tab').forEach((tab) => { tab.addEventListener('click', () => switchTab(tab.dataset.tab)); }); panelEl.querySelector('#gvd-listen-page').addEventListener('click', (ev) => { const main = document.querySelector(cfg.contentSelector) || document.body; speak(extractElementText(main).slice(0, 12000), ev.currentTarget); }); panelEl.querySelector('#gvd-listen-selection').addEventListener('click', (ev) => { const sel = global.getSelection?.()?.toString()?.trim(); if (!sel) { alert('Select text on the page first.'); return; } speak(sel, ev.currentTarget); }); panelEl.querySelector('#gvd-stop').addEventListener('click', () => { if (global.GrokVoice) global.GrokVoice.stop(); else global.speechSynthesis?.cancel(); }); const answerEl = panelEl.querySelector('#gvd-answer'); const metaEl = panelEl.querySelector('#gvd-ask-meta'); panelEl.querySelector('#gvd-ask-submit').addEventListener('click', () => { const q = panelEl.querySelector('#gvd-question').value.trim(); if (!q) return; answerEl.style.display = 'block'; ask(q, {}, answerEl, metaEl); }); setupVoiceInput(); setupDelegatedActions(); } function setupVoiceInput() { const SpeechRecognition = global.SpeechRecognition || global.webkitSpeechRecognition; const mic = panelEl.querySelector('#gvd-mic'); const transcriptEl = panelEl.querySelector('#gvd-voice-transcript'); const voiceAnswerEl = panelEl.querySelector('#gvd-voice-answer'); const fallbackEl = panelEl.querySelector('#gvd-voice-fallback'); const helpEl = panelEl.querySelector('#gvd-mic-help'); const askFallbackBtn = panelEl.querySelector('#gvd-use-ask'); function showAskFallback(msg, help) { if (transcriptEl) transcriptEl.textContent = msg || 'Voice input unavailable.'; if (fallbackEl) fallbackEl.style.display = 'block'; if (helpEl && help) helpEl.textContent = help; mic.classList.remove('listening'); listening = false; } askFallbackBtn?.addEventListener('click', () => switchTab('ask')); const isSecure = global.isSecureContext || location.protocol === 'https:' || location.hostname === 'localhost' || location.hostname === '127.0.0.1'; if (!isSecure) { mic.disabled = true; showAskFallback( 'Microphone requires HTTPS.', 'Open xxxiii.io over HTTPS or use Ask mode to type your question.' ); return; } if (!SpeechRecognition) { mic.disabled = true; showAskFallback( 'Speech recognition not supported.', 'Use Chrome or Edge on desktop, or switch to Ask mode.' ); return; } async function checkMicPermission() { if (!navigator.permissions?.query) return 'unknown'; try { const st = await navigator.permissions.query({ name: 'microphone' }); return st.state; } catch { return 'unknown'; } } const MIC_ERRORS = { 'not-allowed': 'Microphone blocked — allow mic for this site in browser settings.', denied: 'Microphone blocked — allow mic for this site in browser settings.', 'service-not-allowed': 'Speech service blocked — try Chrome/Edge or use Ask mode.', 'audio-capture': 'No microphone detected — connect a mic or use Ask mode.', 'no-speech': 'No speech heard — tap mic and speak clearly.', aborted: 'Voice input cancelled.', network: 'Network error during speech recognition — try Ask mode.', }; recognition = new SpeechRecognition(); recognition.lang = navigator.language || 'en-US'; recognition.interimResults = true; recognition.continuous = false; recognition.maxAlternatives = 1; mic.addEventListener('click', async () => { if (listening) { recognition.stop(); return; } if (fallbackEl) fallbackEl.style.display = 'none'; const perm = await checkMicPermission(); if (perm === 'denied') { showAskFallback('Microphone permission denied.', MIC_ERRORS.denied); return; } try { recognition.start(); } catch (err) { showAskFallback('Could not start microphone.', err.message || 'Use Ask mode.'); } }); recognition.onstart = () => { listening = true; mic.classList.add('listening'); transcriptEl.textContent = 'Listening… speak your question'; if (fallbackEl) fallbackEl.style.display = 'none'; }; recognition.onend = () => { listening = false; mic.classList.remove('listening'); }; recognition.onerror = (ev) => { const code = ev.error || 'unknown'; const msg = MIC_ERRORS[code] || 'Mic error — check permissions or try Ask mode.'; if (code === 'no-speech') { transcriptEl.textContent = msg; return; } showAskFallback(msg, 'Tip: Click "Use Ask mode (type)" to type your question instead.'); }; recognition.onresult = async (ev) => { let text = ''; for (let i = ev.resultIndex; i < ev.results.length; i++) { if (ev.results[i].isFinal) { text = ev.results[i][0].transcript; } else { transcriptEl.textContent = ev.results[i][0].transcript + '…'; } } if (!text) return; transcriptEl.textContent = '"' + text + '"'; voiceAnswerEl.style.display = 'block'; voiceAnswerEl.textContent = 'Thinking…'; const data = await ask(text, {}, voiceAnswerEl, null); if (data?.answer) { speak(data.answer, mic, { type: 'fraud_report', report: true }); } }; } function setupDelegatedActions() { document.addEventListener('click', (ev) => { const listenBtn = ev.target.closest('.gvd-listen-btn'); if (listenBtn) { ev.preventDefault(); const nid = listenBtn.getAttribute('data-narrative-id'); const section = listenBtn.getAttribute('data-gmiie-section'); if (nid) { loadNarratives().then(() => { const n = getNarrativeById(nid); speak(narrativeToSpeech(n), listenBtn, { type: 'narrative', narrativeId: nid, context: { type: 'narrative', narrative_id: nid, category: n?.category, typology: n?.typology_id, }, }); }); return; } if (section === 'case-study') { const block = document.querySelector('.case-study'); speak(extractElementText(block).slice(0, 12000), listenBtn, { type: 'case_study' }); return; } if (listenBtn.getAttribute('data-gmiie-report')) { const reportText = listenBtn.getAttribute('data-report-text') || extractElementText(document.getElementById('fraud-results') || document.getElementById('check-results')); const score = listenBtn.getAttribute('data-fraud-score'); const level = listenBtn.getAttribute('data-fraud-level'); const typology = listenBtn.getAttribute('data-fraud-typology'); const scamFamily = listenBtn.getAttribute('data-scam-family'); speak(reportText.slice(0, 8000), listenBtn, { type: 'fraud_report', report: true, context: { type: 'fraud_report', score: score ? parseInt(score, 10) : cfg.fraudScore, level: level || cfg.fraudLevel, typology: typology || cfg.typology, scam_family: scamFamily || cfg.scamFamily, }, }); return; } const host = listenBtn.closest('[data-gmiie-speak]'); if (host) { speak(host.getAttribute('data-gmiie-speak') || extractElementText(host), listenBtn); } } const askBtn = ev.target.closest('.gvd-ask-btn'); if (askBtn) { ev.preventDefault(); panelEl.classList.add('open'); switchTab('ask'); const qInput = panelEl.querySelector('#gvd-question'); const answerEl = panelEl.querySelector('#gvd-answer'); const metaEl = panelEl.querySelector('#gvd-ask-meta'); answerEl.style.display = 'block'; const nid = askBtn.getAttribute('data-narrative-id'); if (nid) { qInput.value = 'Summarize narrative ' + nid + ' and key awareness points.'; ask(qInput.value, { narrativeIds: [nid] }, answerEl, metaEl); return; } if (askBtn.getAttribute('data-gmiie-ask-score')) { const score = askBtn.getAttribute('data-fraud-score'); const level = askBtn.getAttribute('data-fraud-level'); cfg.fraudScore = score ? parseInt(score, 10) : cfg.fraudScore; cfg.fraudLevel = level || cfg.fraudLevel; qInput.value = 'Explain this fraud score and what I should do next.'; ask(qInput.value, { fraudScore: cfg.fraudScore, fraudLevel: cfg.fraudLevel }, answerEl, metaEl); } } }); } function setFraudReportContext(score, level, engine, reportSummary, extras) { cfg.fraudScore = score; cfg.fraudLevel = level; cfg.engine = engine; cfg.reportSummary = reportSummary; if (extras?.typology) cfg.typology = extras.typology; if (extras?.scamFamily) cfg.scamFamily = extras.scamFamily; } function init(opts) { cfg = { ...DEFAULTS, ...opts }; if (document.body) buildPanel(); else document.addEventListener('DOMContentLoaded', buildPanel); if (cfg.page === 'fraud' || location.pathname.includes('fraud')) { loadNarratives(); } } global.GMIIEVoiceDesk = { init, speak, ask, narrativeToSpeech, setFraudReportContext, loadNarratives, }; if (document.body?.hasAttribute('data-gmiie-desk')) { init({ page: document.body.getAttribute('data-gmiie-page') || undefined, }); } })(typeof window !== 'undefined' ? window : globalThis);