diff --git a/armoury/lab.html b/armoury/lab.html index bab3ce6..0ef5f6e 100644 --- a/armoury/lab.html +++ b/armoury/lab.html @@ -202,5 +202,6 @@ + \ No newline at end of file diff --git a/css/agent-chat.css b/css/agent-chat.css index 97a10f0..36e4da5 100644 --- a/css/agent-chat.css +++ b/css/agent-chat.css @@ -198,3 +198,177 @@ padding: 0; } .chat-msg-body li { margin: 0.15rem 0; list-style: disc; } + +/* ═══ Phase 4: Rich tool cards ═══ */ +.chat-msg-body strong { color: var(--accent, #00ffc8); } +.chat-msg-body em { color: var(--text-dim, #9ab); font-style: italic; } +.chat-msg-body code { + background: rgba(0,255,200,0.08); + padding: 0 0.3rem; + border-radius: 2px; + color: var(--accent, #00ffc8); + font-family: 'JetBrains Mono', ui-monospace, monospace; + font-size: 0.88em; +} +.chat-msg-body pre { + background: rgba(0,0,0,0.5); + padding: 0.6rem; + border-radius: 4px; + overflow-x: auto; + border-left: 2px solid var(--accent, #00ffc8); + margin: 0.4rem 0; + font-size: 0.82em; + color: #dfe; +} +.chat-msg-body a { color: var(--accent, #00ffc8); text-decoration: underline; } +.chat-msg-body h2, .chat-msg-body h3, .chat-msg-body h4 { + color: var(--accent, #00ffc8); + margin: 0.5rem 0 0.25rem; + font-family: 'JetBrains Mono', ui-monospace, monospace; + letter-spacing: 0.02em; +} +.chat-msg-body ul, .chat-msg-body ol { margin-left: 1.2rem; } + +.tool-card { + margin: 0.4rem 0 0.2rem; + padding: 0.55rem 0.7rem; + background: linear-gradient(180deg, rgba(0,255,200,0.05), rgba(0,255,200,0.02)); + border: 1px solid rgba(0,255,200,0.25); + border-left: 3px solid var(--accent, #00ffc8); + border-radius: 4px; + font-size: 0.85em; + color: #dfe; +} +.tool-card .tc-head { font-weight: 600; color: var(--accent, #00ffc8); margin-bottom: 0.3rem; font-family: 'JetBrains Mono', monospace; letter-spacing: 0.02em; } +.tool-card .tc-sub { color: #9ab; font-size: 0.9em; } +.tool-card .tc-sub.pos { color: #14f195; } +.tool-card .tc-sub.neg { color: #ff5577; } +.tool-card .tc-list { list-style: none; padding: 0; margin: 0; } +.tool-card .tc-list li { padding: 0.35rem 0; border-bottom: 1px dashed rgba(0,255,200,0.12); } +.tool-card .tc-list li:last-child { border-bottom: 0; } +.tool-card .tc-tag { + display: inline-block; + background: rgba(0,255,200,0.12); + color: var(--accent, #00ffc8); + padding: 1px 6px; + border-radius: 2px; + font-size: 0.75em; + margin-left: 0.3rem; + text-transform: uppercase; + letter-spacing: 0.05em; +} +.tool-card .tc-snippet { color: #9ab; font-size: 0.88em; margin-top: 0.15rem; } +.tool-card .tc-link { display: inline-block; margin-top: 0.35rem; color: var(--accent, #00ffc8); font-size: 0.85em; } +.tool-card .tc-metrics { display: flex; gap: 0.9rem; margin-top: 0.3rem; flex-wrap: wrap; } +.tool-card .tc-metric { display: flex; flex-direction: column; align-items: center; } +.tool-card .tc-metric .tc-m-num { font-size: 1.15rem; color: var(--accent, #00ffc8); font-weight: 600; } +.tool-card .tc-metric .tc-m-lbl { font-size: 0.7em; color: #89a; text-transform: uppercase; letter-spacing: 0.08em; } +.tool-card.tool-price { display: flex; align-items: center; gap: 0.6rem; } +.tool-card.tool-price .tc-icon { font-size: 1.3rem; } +.tool-card.tool-price .tc-main { flex: 1; font-size: 1rem; } +.tool-card.tool-price .tc-label { color: #9ab; font-size: 0.8em; margin-right: 0.3rem; } +.tool-card.tool-effect, .tool-card.tool-fortune, .tool-card.tool-stats { padding: 0.5rem 0.7rem; } +.tool-card.tool-ascii pre { background: transparent; color: var(--accent, #00ffc8); border: 0; padding: 0; font-size: 0.75em; line-height: 1.1; } +.tool-card .tc-badge { display: inline-block; padding: 2px 8px; border-radius: 2px; font-size: 0.75em; font-weight: 700; margin-left: 0.5rem; } +.tool-card .tc-badge.taken { background: rgba(255,85,119,0.2); color: #ff5577; } +.tool-card .tc-badge.available { background: rgba(20,241,149,0.2); color: #14f195; } +.tool-card .tc-ver { color: var(--accent, #00ffc8); font-family: 'JetBrains Mono', monospace; font-weight: 600; } +.tool-card .tc-date { color: #678; font-size: 0.8em; margin-left: 0.3rem; } +.tool-card .tc-title { font-weight: 500; margin-top: 0.15rem; } +.atc-details { margin-top: 0.3rem; } +.atc-details summary { cursor: pointer; color: #678; font-size: 0.8em; } +.atc-status.err { color: #ff5577; font-size: 0.9em; } + +/* ═══ Phase 4: Voice mode ═══ */ +.chat-mic-btn, .chat-voice-settings-btn { + background: transparent; + border: 1px solid rgba(0,255,200,0.3); + color: var(--accent, #00ffc8); + width: 36px; height: 36px; + border-radius: 4px; + cursor: pointer; + font-size: 1rem; + margin-right: 4px; + transition: all 0.15s; + display: inline-flex; align-items: center; justify-content: center; +} +.chat-mic-btn:hover, .chat-voice-settings-btn:hover { + background: rgba(0,255,200,0.1); + border-color: var(--accent, #00ffc8); +} +.chat-mic-btn.listening { + background: rgba(255,60,90,0.3); + border-color: #ff3c5a; + color: #ff3c5a; + animation: jae-mic-pulse 1s infinite; +} +@keyframes jae-mic-pulse { + 0%, 100% { box-shadow: 0 0 0 0 rgba(255,60,90,0.6); } + 50% { box-shadow: 0 0 0 6px rgba(255,60,90,0); } +} +.jae-speaking .agent-chat-header::after { + content: '🔊'; + margin-left: 0.5rem; + animation: jae-wave 0.9s infinite; +} +@keyframes jae-wave { + 0%, 100% { opacity: 0.5; } + 50% { opacity: 1; } +} + +.jae-voice-modal { + position: fixed; inset: 0; + background: rgba(0,0,0,0.7); + display: flex; align-items: center; justify-content: center; + z-index: 10001; + backdrop-filter: blur(4px); +} +.jae-voice-modal .jvm-panel { + background: #0a0f12; + border: 1px solid var(--accent, #00ffc8); + border-radius: 6px; + padding: 1.2rem 1.4rem; + min-width: 340px; + max-width: 90vw; + color: #dfe; + font-family: 'JetBrains Mono', monospace; + box-shadow: 0 0 30px rgba(0,255,200,0.3); +} +.jvm-head { + color: var(--accent, #00ffc8); + font-weight: 700; + letter-spacing: 0.1em; + display: flex; + justify-content: space-between; + margin-bottom: 1rem; +} +.jvm-close { + background: transparent; border: 0; color: #9ab; cursor: pointer; font-size: 1rem; +} +.jvm-row { display: flex; flex-direction: column; gap: 0.3rem; margin: 0.8rem 0; font-size: 0.85em; } +.jvm-row select, .jvm-row input[type=range] { + background: #05080a; + border: 1px solid rgba(0,255,200,0.25); + color: #dfe; + padding: 0.3rem; + font-family: inherit; +} +.jvm-row input[type=checkbox] { accent-color: var(--accent, #00ffc8); } +.jvm-actions { flex-direction: row; justify-content: flex-end; gap: 0.5rem; } +.jvm-actions button { + background: rgba(0,255,200,0.1); + border: 1px solid var(--accent, #00ffc8); + color: var(--accent, #00ffc8); + padding: 0.4rem 0.9rem; + border-radius: 3px; + cursor: pointer; + font-family: inherit; +} +.jvm-actions button:hover { background: rgba(0,255,200,0.2); } + +/* mobile */ +@media (max-width: 768px) { + .chat-mic-btn, .chat-voice-settings-btn { width: 32px; height: 32px; font-size: 0.85rem; } + .tool-card { font-size: 0.8em; padding: 0.45rem 0.55rem; } + .tool-card .tc-metrics { gap: 0.6rem; } +} diff --git a/css/sitewide-effects.css b/css/sitewide-effects.css index a87c757..0dc75ec 100644 --- a/css/sitewide-effects.css +++ b/css/sitewide-effects.css @@ -329,3 +329,47 @@ html.fx-hacker body > *:not(canvas):not(script):not(style) { animation: none !important; } } + +/* ═══ Broadcast banner (Phase 3) ═══ */ +.jae-broadcast { + position: fixed; + top: 0; left: 0; right: 0; + background: linear-gradient(90deg, #5a0010 0%, #7a001a 50%, #5a0010 100%); + color: #ffecec; + padding: 0.55rem 2.5rem 0.55rem 1rem; + font-family: 'JetBrains Mono', ui-monospace, monospace; + font-size: 0.85rem; + font-weight: 500; + z-index: 10000; + display: flex; + align-items: center; + gap: 0.6rem; + border-bottom: 1px solid #ff3c5a; + box-shadow: 0 2px 12px rgba(255,60,90,0.4); + animation: jbb-slide-down 0.4s ease-out; +} +@keyframes jbb-slide-down { + from { transform: translateY(-100%); } + to { transform: translateY(0); } +} +.jae-broadcast .jbb-icon { font-size: 1rem; } +.jae-broadcast .jbb-msg { flex: 1; letter-spacing: 0.02em; } +.jae-broadcast .jbb-close { + background: transparent; + border: 1px solid rgba(255,255,255,0.3); + color: #ffecec; + width: 24px; height: 24px; + border-radius: 3px; + cursor: pointer; + font-size: 0.85rem; + padding: 0; + display: inline-flex; align-items: center; justify-content: center; +} +.jae-broadcast .jbb-close:hover { + background: rgba(255,255,255,0.15); +} +body.has-broadcast { padding-top: 38px; } +@media (max-width: 768px) { + .jae-broadcast { font-size: 0.78rem; padding: 0.45rem 2.2rem 0.45rem 0.8rem; } + body.has-broadcast { padding-top: 44px; } +} diff --git a/depot/contraband.html b/depot/contraband.html index 17214e6..01eb41d 100644 --- a/depot/contraband.html +++ b/depot/contraband.html @@ -68,5 +68,6 @@ + diff --git a/depot/recon.html b/depot/recon.html index d7d4237..9a15671 100644 --- a/depot/recon.html +++ b/depot/recon.html @@ -65,5 +65,6 @@ + diff --git a/hq/briefing.html b/hq/briefing.html index 235af31..7b11fa1 100644 --- a/hq/briefing.html +++ b/hq/briefing.html @@ -68,5 +68,6 @@ + \ No newline at end of file diff --git a/hq/index.html b/hq/index.html index 4c8b573..f84a1b5 100644 --- a/hq/index.html +++ b/hq/index.html @@ -80,5 +80,6 @@ + diff --git a/hq/leaderboards.html b/hq/leaderboards.html index 7aae011..4ab6c76 100644 --- a/hq/leaderboards.html +++ b/hq/leaderboards.html @@ -114,5 +114,6 @@ + diff --git a/hq/logs.html b/hq/logs.html index 47a9e32..f5bfb14 100644 --- a/hq/logs.html +++ b/hq/logs.html @@ -55,5 +55,6 @@ + diff --git a/hq/profile.html b/hq/profile.html index 36daf54..edb27c7 100644 --- a/hq/profile.html +++ b/hq/profile.html @@ -68,5 +68,6 @@ + \ No newline at end of file diff --git a/hq/telemetry.html b/hq/telemetry.html index dfceafd..6337623 100644 --- a/hq/telemetry.html +++ b/hq/telemetry.html @@ -228,5 +228,6 @@ + diff --git a/index.html b/index.html index c1fa0e2..252c1fe 100644 --- a/index.html +++ b/index.html @@ -596,9 +596,11 @@ + + diff --git a/js/broadcast.js b/js/broadcast.js new file mode 100644 index 0000000..74d8449 --- /dev/null +++ b/js/broadcast.js @@ -0,0 +1,53 @@ +// Broadcast banner — polls /api/broadcast/current every 60s (Phase 3/4) +(function () { + 'use strict'; + const POLL_MS = 60000; + const DISMISS_KEY = 'jae-broadcast-dismissed'; + + function ready(fn) { if (document.readyState !== 'loading') fn(); else document.addEventListener('DOMContentLoaded', fn); } + + function renderBanner(data) { + const dismissed = localStorage.getItem(DISMISS_KEY); + const fingerprint = (data.created_at || '') + '|' + (data.message || ''); + if (dismissed === fingerprint) return; + let el = document.getElementById('jaeBroadcastBanner'); + if (!el) { + el = document.createElement('div'); + el.id = 'jaeBroadcastBanner'; + el.className = 'jae-broadcast'; + document.body.appendChild(el); + } + el.innerHTML = ` + 📣 + + `; + el.querySelector('.jbb-msg').textContent = data.message || ''; + el.querySelector('.jbb-close').onclick = () => { + localStorage.setItem(DISMISS_KEY, fingerprint); + el.remove(); + document.body.classList.remove('has-broadcast'); + }; + document.body.classList.add('has-broadcast'); + } + + function hideBanner() { + const el = document.getElementById('jaeBroadcastBanner'); + if (el) el.remove(); + document.body.classList.remove('has-broadcast'); + } + + async function check() { + try { + const res = await fetch('/api/broadcast/current', { cache: 'no-store' }); + if (res.status === 204) { hideBanner(); return; } + if (!res.ok) return; + const data = await res.json(); + if (data && data.message) renderBanner(data); else hideBanner(); + } catch (e) { /* offline: ignore */ } + } + + ready(function () { + check(); + setInterval(check, POLL_MS); + }); +})(); diff --git a/js/chat.js b/js/chat.js index 5924ba8..ef9085d 100644 --- a/js/chat.js +++ b/js/chat.js @@ -81,31 +81,175 @@ return body; } + // ─── Per-tool rich card renderers (Phase 4) ────────────────────────── + const TOOL_RENDERERS = { + get_sol_price: (r) => { + if (r.error) return null; + const pct = r.change_24h_pct || 0; + const cls = pct >= 0 ? 'pos' : 'neg'; + const arrow = pct >= 0 ? '▲' : '▼'; + return `
+ 💰 +
SOL $${(r.price_usd||0).toFixed(2)}
+
${arrow} ${Math.abs(pct).toFixed(2)}%
+
`; + }, + get_crypto_price: (r) => { + if (r.error) return null; + const pct = r.change_24h_pct || 0; + const cls = pct >= 0 ? 'pos' : 'neg'; + const arrow = pct >= 0 ? '▲' : '▼'; + return `
+ 📊 +
${escapeHtml(r.symbol||'')} $${(r.price_usd||0).toFixed(4)}
+
${arrow} ${Math.abs(pct).toFixed(2)}%
+
`; + }, + search_site: (r) => { + if (!Array.isArray(r.results) || !r.results.length) return null; + const items = r.results.slice(0, 5).map(x => + `
  • ${escapeHtml(x.title||x.source||'')}
    ${escapeHtml((x.snippet||'').slice(0,140))}
  • ` + ).join(''); + return ``; + }, + search_contraband: (r) => { + if (!Array.isArray(r.results) || !r.results.length) return null; + const items = r.results.slice(0, 5).map(x => + `
  • ${escapeHtml(x.title||'')} ${escapeHtml(x.category||'')}
    ${escapeHtml((x.description||'').slice(0,140))}
  • ` + ).join(''); + return ``; + }, + search_awesomelist: (r) => { + if (!Array.isArray(r.results) || !r.results.length) return null; + const items = r.results.slice(0, 5).map(x => + `
  • ${escapeHtml(x.name||'')} ${escapeHtml(x.type||'')}
    ${escapeHtml((x.description||'').slice(0,140))}
  • ` + ).join(''); + return ``; + }, + search_unredacted: (r) => { + if (!Array.isArray(r.results) || !r.results.length) return null; + const items = r.results.slice(0, 5).map(x => + `
  • ${escapeHtml(x.title||'')} ${escapeHtml(x.collection||'unredacted')}
    ${escapeHtml((x.summary||'').slice(0,140))}
  • ` + ).join(''); + return ``; + }, + search_crimescene: (r) => { + if (!Array.isArray(r.results) || !r.results.length) return null; + const items = r.results.slice(0, 5).map(x => + `
  • ${escapeHtml(x.case||'')} ${escapeHtml(x.year||'')}
    ${escapeHtml((x.summary||'').slice(0,140))}
  • ` + ).join(''); + return ``; + }, + search_radar: (r) => { + if (!Array.isArray(r.results) || !r.results.length) return null; + const items = r.results.slice(0, 5).map(x => + `
  • ${escapeHtml(x.title||'')} ${escapeHtml(x.source||'')}
  • ` + ).join(''); + return ``; + }, + search_docs: (r) => { + if (!Array.isArray(r.results) || !r.results.length) return null; + const items = r.results.slice(0, 10).map(x => + `
  • ${escapeHtml(x.title||x.case||'')} ${escapeHtml(x.source||'')}
  • ` + ).join(''); + return ``; + }, + get_changelog: (r) => { + if (!Array.isArray(r.entries) || !r.entries.length) return null; + const items = r.entries.slice(0, 8).map(e => + `
  • v${escapeHtml(e.version||'')} ${escapeHtml(e.category||'')} ${escapeHtml(e.date||'')}
    ${escapeHtml(e.title||'')}
  • ` + ).join(''); + return `
    📜 Last ${r.entries.length} of ${r.total_available||'?'}
    `; + }, + wallet_xray: (r) => { + if (r.error) return null; + return `
    +
    👁 Wallet X-Ray ${escapeHtml((r.address||'').slice(0,4))}…${escapeHtml((r.address||'').slice(-4))}
    +
    +
    ${r.balance_sol ?? '?'}SOL
    +
    ${r.non_zero_tokens ?? '?'}Tokens
    +
    ${(r.recent_txs||[]).length}Recent TXs
    +
    + ${r.solscan_url ? `solscan →` : ''} +
    `; + }, + get_my_wallet_summary: (r) => (TOOL_RENDERERS.wallet_xray ? TOOL_RENDERERS.wallet_xray(r) : null), + lookup_sol_domain: (r) => { + if (r.error) return null; + const badge = r.registered ? `TAKEN` : `AVAILABLE`; + const link = r.register_url || r.solscan_url; + return `
    +
    🌐 ${escapeHtml(r.name||'')} ${badge}
    + ${r.owner ? `
    owner: ${escapeHtml((r.owner||'').slice(0,4))}…${escapeHtml((r.owner||'').slice(-4))}
    ` : ''} + ${link ? `sns.id →` : ''} +
    `; + }, + trigger_effect: (r) => { + if (!r.effect) return null; + return `
    ⚡ Effect ${escapeHtml(r.effect)} triggered
    `; + }, + get_server_status: (r) => { + if (r.error) return null; + return `
    +
    🖥️ Server status
    +
    +
    ${(r.cpu_pct ?? 0).toFixed(1)}%CPU
    +
    ${(r.ram_pct ?? 0).toFixed(1)}%RAM
    +
    ${(r.disk_pct ?? 0).toFixed(1)}%Disk
    +
    +
    `; + }, + random_fortune: (r) => r.fortune ? `
    🔮 ${escapeHtml(r.fortune)}
    ` : null, + ascii_banner: (r) => r.banner ? `
    ${escapeHtml(r.banner)}
    ` : null, + get_gov_domains_stats: (r) => r.error ? null : `
    🏛️ Gov domains: ${r.total_domains} tracked · ${r.added_last_24h} new in 24h
    `, + get_leaderboards: (r) => { + if (r.error) return null; + const top = (r.top_countries||[]).slice(0,3).map(c => `
  • ${escapeHtml(c.country||c.code||'')} ${c.count||c.visits||0}
  • `).join(''); + return `
    🏆 Top countries
    `; + }, + get_network_graph_data: (r) => `
    🌐 Recent arcs: ${r.total_arcs||0} · Top: ${(r.top_countries||[]).slice(0,3).map(c=>escapeHtml(c.code)).join(', ')}
    `, + get_guestbook: (r) => { + if (!Array.isArray(r.entries) || !r.entries.length) return `
    📖 Guestbook is empty
    `; + const items = r.entries.slice(0, 5).map(e => `
  • ${escapeHtml(e.truncated_address||'')} ${escapeHtml((e.message||'').slice(0,140))}
  • `).join(''); + return ``; + }, + list_memories: (r) => { + if (!Array.isArray(r.memories) || !r.memories.length) return `
    🧠 No memories stored yet
    `; + const items = r.memories.slice(-10).map(m => `
  • #${m.id} · ${escapeHtml((m.fact||'').slice(0,140))}
  • `).join(''); + return ``; + }, + save_memory: (r) => r.saved ? `
    🧠 Memory saved (#${r.id} · total ${r.total})
    ` : null, + delete_memory: (r) => r.deleted ? `
    🗑️ Forgot: ${escapeHtml(r.removed_fact||'')}
    ` : null, + get_sitrep: (r) => r.error ? null : `
    📋 SITREP ${escapeHtml(r.date||'')}
    ${escapeHtml((r.summary||'').slice(0,240))}
    view full →
    `, + }; + function addToolCallCard(call) { const wrap = document.createElement('div'); wrap.className = 'agent-tool-call'; - const argsStr = escapeHtml(JSON.stringify(call.args || {}, null, 0)); const result = call.result || {}; const errored = !!result.error; - const statusIcon = errored ? '❌' : '✅'; - let summary = errored ? (result.error || 'error') : 'ok'; - if (!errored) { - if (Array.isArray(result.results)) summary = `${result.results.length} result${result.results.length === 1 ? '' : 's'}`; - else if (Array.isArray(result.entries)) summary = `${result.entries.length} entries`; - else if (typeof result.price_usd === 'number') summary = `$${result.price_usd.toFixed(2)} (${result.change_24h_pct > 0 ? '+' : ''}${(result.change_24h_pct || 0).toFixed(2)}%)`; - else if (result.effect) summary = `effect: ${result.effect}`; - else if (result.balance_sol !== undefined) summary = `${result.balance_sol} SOL`; - } - const resultStr = escapeHtml(JSON.stringify(result, null, 2).slice(0, 1400)); - wrap.innerHTML = ` -
    - 🔧 - ${escapeHtml(call.name)} - (${argsStr}) -
    -
    ├─ ${statusIcon} ${escapeHtml(summary)}
    -
    └─ (click to expand)
    ${resultStr}
    - `; + const argsStr = escapeHtml(JSON.stringify(call.args || {}, null, 0)); + + // Try rich renderer first + let richBody = ''; + try { + const renderer = TOOL_RENDERERS[call.name]; + if (renderer && !errored) richBody = renderer(result) || ''; + } catch (e) { console.warn('tool renderer error', call.name, e); } + + // Header always shown + const header = `
    + ${errored ? '❌' : '🔧'} + ${escapeHtml(call.name)} + (${argsStr}) +
    `; + + const resultStr = escapeHtml(JSON.stringify(result, null, 2).slice(0, 1600)); + const fallback = errored + ? `
    └─ ❌ ${escapeHtml(result.error || 'error')}
    ` + : `
    └─ raw payload
    ${resultStr}
    `; + + wrap.innerHTML = header + (richBody || '') + fallback; chatMessages.appendChild(wrap); chatMessages.scrollTop = chatMessages.scrollHeight; } diff --git a/js/voice-mode.js b/js/voice-mode.js new file mode 100644 index 0000000..aa202ac --- /dev/null +++ b/js/voice-mode.js @@ -0,0 +1,203 @@ +// JAE-AI Voice Mode — Web Speech API (Phase 4) +// Adds mic button + auto-speak of assistant replies. +(function () { + 'use strict'; + const SR = window.SpeechRecognition || window.webkitSpeechRecognition; + const SY = window.speechSynthesis; + if (!SR || !SY) { console.warn('[voice] Web Speech API unsupported in this browser'); return; } + + // Wait for DOM ready + function ready(fn) { if (document.readyState !== 'loading') fn(); else document.addEventListener('DOMContentLoaded', fn); } + + const LS_AUTO = 'jae-voice-auto-speak'; + const LS_VOICE = 'jae-voice-name'; + const LS_RATE = 'jae-voice-rate'; + const LS_LANG = 'jae-voice-lang'; + + let recognition = null; + let recognizing = false; + let silenceTimer = null; + let finalBuffer = ''; + let voices = []; + + function loadVoices() { + voices = SY.getVoices(); + if (!voices.length) setTimeout(loadVoices, 250); + } + if (typeof SY.onvoiceschanged !== 'undefined') SY.onvoiceschanged = loadVoices; + loadVoices(); + + function stripMarkdownForSpeech(txt) { + if (!txt) return ''; + return txt + .replace(/```[\s\S]*?```/g, ' code block ') + .replace(/`([^`]+)`/g, '$1') + .replace(/\*\*([^*]+)\*\*/g, '$1') + .replace(/\*([^*]+)\*/g, '$1') + .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') + .replace(/[#>_~]/g, ' ') + .replace(/<[^>]+>/g, ' ') + .replace(/\s+/g, ' ') + .trim(); + } + + function chooseVoice() { + const preferName = localStorage.getItem(LS_VOICE); + const lang = localStorage.getItem(LS_LANG) || 'en-GB'; + if (preferName) { + const m = voices.find(v => v.name === preferName); + if (m) return m; + } + const byLang = voices.find(v => v.lang === lang) || voices.find(v => v.lang.startsWith('en')); + return byLang || voices[0] || null; + } + + function speak(text) { + if (!text) return; + try { SY.cancel(); } catch (e) {} + const clean = stripMarkdownForSpeech(text).slice(0, 600); + if (!clean) return; + const utter = new SpeechSynthesisUtterance(clean); + const voice = chooseVoice(); + if (voice) { utter.voice = voice; utter.lang = voice.lang; } + utter.rate = parseFloat(localStorage.getItem(LS_RATE) || '1.05'); + utter.pitch = 1.0; + utter.onstart = () => document.body.classList.add('jae-speaking'); + utter.onend = () => document.body.classList.remove('jae-speaking'); + utter.onerror = () => document.body.classList.remove('jae-speaking'); + SY.speak(utter); + } + + function findInput() { + return document.getElementById('chatInput') || document.querySelector('.chat-input textarea, .chat-input input'); + } + function findSendBtn() { + return document.getElementById('chatSendBtn') || document.querySelector('.chat-send-btn, button[data-chat-send]'); + } + function submitChat() { + const input = findInput(); + const send = findSendBtn(); + if (input && (input.value || '').trim() && send) send.click(); + } + + function setListeningUI(on) { + const btn = document.getElementById('chatMicBtn'); + if (btn) btn.classList.toggle('listening', on); + document.body.classList.toggle('jae-listening', on); + } + + function startListening() { + if (recognizing) return; + try { + recognition = new SR(); + recognition.lang = localStorage.getItem(LS_LANG) || 'en-GB'; + recognition.continuous = true; + recognition.interimResults = true; + finalBuffer = ''; + recognition.onstart = () => { recognizing = true; setListeningUI(true); }; + recognition.onresult = (ev) => { + let interim = ''; + for (let i = ev.resultIndex; i < ev.results.length; i++) { + const r = ev.results[i]; + if (r.isFinal) finalBuffer += r[0].transcript + ' '; + else interim += r[0].transcript; + } + const input = findInput(); + if (input) input.value = (finalBuffer + interim).trim(); + // auto-submit after 1.5s of silence (no new results) + if (silenceTimer) clearTimeout(silenceTimer); + silenceTimer = setTimeout(() => { + stopListening(); + setTimeout(submitChat, 120); + }, 1500); + }; + recognition.onerror = (e) => { console.warn('[voice] err', e.error); stopListening(); }; + recognition.onend = () => { recognizing = false; setListeningUI(false); }; + recognition.start(); + } catch (e) { + console.warn('[voice] start failed', e); + } + } + function stopListening() { + if (silenceTimer) { clearTimeout(silenceTimer); silenceTimer = null; } + try { if (recognition) recognition.stop(); } catch (e) {} + recognizing = false; + setListeningUI(false); + } + + function injectMicButton() { + const send = findSendBtn(); + if (!send || document.getElementById('chatMicBtn')) return false; + const mic = document.createElement('button'); + mic.id = 'chatMicBtn'; + mic.className = 'chat-mic-btn'; + mic.type = 'button'; + mic.title = 'Voice mode (click to talk, auto-submits after 1.5s silence)'; + mic.innerHTML = '🎙'; + mic.addEventListener('click', () => { recognizing ? stopListening() : startListening(); }); + send.parentNode.insertBefore(mic, send); + + const gear = document.createElement('button'); + gear.id = 'chatVoiceSettingsBtn'; + gear.className = 'chat-voice-settings-btn'; + gear.type = 'button'; + gear.title = 'Voice settings'; + gear.innerHTML = '⚙'; + gear.addEventListener('click', openSettingsModal); + send.parentNode.insertBefore(gear, send); + return true; + } + + function openSettingsModal() { + if (document.getElementById('jaeVoiceModal')) return; + const vs = (voices.length ? voices : SY.getVoices()).filter(v => (v.lang || '').toLowerCase().startsWith('en')); + const curName = localStorage.getItem(LS_VOICE) || ''; + const curRate = parseFloat(localStorage.getItem(LS_RATE) || '1.05'); + const autoSpeak = localStorage.getItem(LS_AUTO) === '1'; + const overlay = document.createElement('div'); + overlay.id = 'jaeVoiceModal'; + overlay.className = 'jae-voice-modal'; + overlay.innerHTML = ` +
    +
    VOICE SETTINGS
    + + + +
    + + +
    +
    `; + document.body.appendChild(overlay); + const close = () => overlay.remove(); + overlay.querySelector('.jvm-close').onclick = close; + overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); }); + const rateEl = overlay.querySelector('#jvm-rate'); + const rateVal = overlay.querySelector('#jvm-rate-val'); + rateEl.oninput = () => { rateVal.textContent = parseFloat(rateEl.value).toFixed(2); }; + overlay.querySelector('#jvm-test').onclick = () => { + localStorage.setItem(LS_VOICE, overlay.querySelector('#jvm-voice').value); + localStorage.setItem(LS_RATE, rateEl.value); + speak('Voice check. JAE-AI online. All systems nominal.'); + }; + overlay.querySelector('#jvm-save').onclick = () => { + localStorage.setItem(LS_AUTO, overlay.querySelector('#jvm-auto').checked ? '1' : '0'); + localStorage.setItem(LS_VOICE, overlay.querySelector('#jvm-voice').value); + localStorage.setItem(LS_RATE, rateEl.value); + close(); + }; + } + + ready(function () { + // Try inject now; if chat not yet rendered, observe. + if (!injectMicButton()) { + const obs = new MutationObserver(() => { if (injectMicButton()) obs.disconnect(); }); + obs.observe(document.body, { childList: true, subtree: true }); + } + document.addEventListener('jae-agent-reply', (e) => { + if (localStorage.getItem(LS_AUTO) === '1') speak(e.detail && e.detail.text); + }); + }); + + window.__jaeVoice = { speak, start: startListening, stop: stopListening, openSettings: openSettingsModal }; +})(); diff --git a/recon/index.html b/recon/index.html index bacd35b..2ecfd98 100644 --- a/recon/index.html +++ b/recon/index.html @@ -10,5 +10,6 @@

    Redirecting to RECON...

    + diff --git a/transmissions/radar.html b/transmissions/radar.html index 85573f7..f034ac3 100644 --- a/transmissions/radar.html +++ b/transmissions/radar.html @@ -141,5 +141,6 @@ +