Yacine: UNFMRISER UX TOUT COMME WTP (propagation apres wave 320 Registry) Pages unifiees: - paperclip-dashboard.html - vnc-picker.html - ai-multichat.html - wevia-agent.html - wevia-cockpit.html Chaque page recoit: 1. Portal banner sticky top (7 links: WTP Master, WEVIA, Cockpit, All-IA Hub, Orchestrator, Paperclip, Registry + badge W321 UX UNIFIED) 2. /css/wevia-portal-consistency.css (shared tokens wave 221) 3. w321-ux-unif-tokens CSS override (WTP colors/radius/trans/Inter font) 4. Focus-visible outline consistent (indigo 2px) 5. Scroll-behavior smooth Zero regression (CSS additive uniquement, banner avant contenu) Zero ecrasement (str_replace + preg_replace surgical) chattr +i toggle workflow (unlock -> patch -> re-lock) GOLD backup par fichier (gold_w321_<page>_<ts>) CF purge bulk 6 URLs Doctrine UX uniform: all portails meme look-n-feel WTP master reference Waves 320+321 = registry + 5 pages = TOUTES pages portail WEVAL unifiees
441 lines
21 KiB
HTML
441 lines
21 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="fr">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>AI Multi-Chat · WEVAL · 8 Provider CDP Cross-IA</title>
|
|
<style>
|
|
:root{
|
|
--bg:#0a0a0f;--panel:rgba(18,18,26,0.6);--border:rgba(255,255,255,0.08);
|
|
--ink:#e8e6e3;--ink-dim:#8b8680;--ink-faint:#5c5852;
|
|
--gold:#f6d572;--mint:#5cdb95;--coral:#ff6b6b;--cyan:#4ecdc4;--violet:#a78bfa;
|
|
--ease:cubic-bezier(.34,1.56,.64,1);
|
|
--font-sans:"Inter",-apple-system,BlinkMacSystemFont,sans-serif;
|
|
--font-mono:"SF Mono",Monaco,"Cascadia Code",monospace;
|
|
}
|
|
*{box-sizing:border-box;margin:0;padding:0}
|
|
body{background:var(--bg);color:var(--ink);font-family:var(--font-sans);min-height:100vh;
|
|
background-image:
|
|
radial-gradient(ellipse at 20% 30%,rgba(246,213,114,0.04) 0%,transparent 50%),
|
|
radial-gradient(ellipse at 80% 70%,rgba(167,139,250,0.03) 0%,transparent 50%);
|
|
}
|
|
.app{display:grid;grid-template-columns:260px 1fr;grid-template-rows:60px 1fr;height:100vh;overflow:hidden}
|
|
.header{grid-column:1/3;display:flex;align-items:center;justify-content:space-between;padding:0 24px;background:rgba(10,10,15,0.85);backdrop-filter:blur(24px);border-bottom:1px solid var(--border);z-index:10}
|
|
.brand{display:flex;align-items:center;gap:14px;font-size:13px;letter-spacing:.2em;text-transform:uppercase;color:var(--gold);font-weight:500}
|
|
.brand::before{content:"";width:6px;height:6px;border-radius:50%;background:var(--mint);animation:pulse 2s ease infinite}
|
|
@keyframes pulse{50%{opacity:.4}}
|
|
.cdp-summary{display:flex;gap:12px;align-items:center;font-size:11px;color:var(--ink-dim);font-family:var(--font-mono)}
|
|
.cdp-summary b{color:var(--mint);font-size:13px}
|
|
.cdp-summary .warn{color:var(--gold)}
|
|
.cdp-summary .err{color:var(--coral)}
|
|
|
|
.sidebar{background:var(--panel);border-right:1px solid var(--border);overflow-y:auto;padding:16px 12px}
|
|
.side-title{font-size:10px;letter-spacing:.28em;text-transform:uppercase;color:var(--ink-faint);margin:14px 8px 10px;font-weight:500}
|
|
.provider-card{padding:10px 12px;border:1px solid var(--border);border-radius:4px;margin-bottom:6px;cursor:pointer;transition:all .25s var(--ease);display:flex;align-items:center;gap:10px}
|
|
.provider-card:hover{border-color:var(--gold);background:rgba(246,213,114,0.05)}
|
|
.provider-card.selected{border-color:var(--mint);background:rgba(92,219,149,0.08)}
|
|
.provider-card.offline{opacity:.4;cursor:not-allowed}
|
|
.pc-check{width:14px;height:14px;border:1.5px solid var(--ink-faint);border-radius:2px;flex-shrink:0;display:flex;align-items:center;justify-content:center;transition:all .2s}
|
|
.provider-card.selected .pc-check{background:var(--mint);border-color:var(--mint)}
|
|
.provider-card.selected .pc-check::after{content:"✓";color:#0a0a0f;font-size:10px;font-weight:700}
|
|
.pc-icon{width:22px;height:22px;border-radius:4px;display:flex;align-items:center;justify-content:center;font-size:13px;background:rgba(255,255,255,0.04);font-weight:600;color:var(--gold)}
|
|
.pc-info{flex:1;min-width:0}
|
|
.pc-name{font-size:12px;color:var(--ink);font-weight:500;margin-bottom:2px}
|
|
.pc-status{font-size:10px;color:var(--ink-faint);font-family:var(--font-mono);display:flex;align-items:center;gap:5px}
|
|
.pc-dot{width:5px;height:5px;border-radius:50%;background:var(--ink-faint)}
|
|
.pc-dot.running{background:var(--mint);box-shadow:0 0 6px var(--mint)}
|
|
.pc-dot.offline{background:var(--coral);opacity:.5}
|
|
|
|
.side-actions{display:flex;flex-direction:column;gap:6px;margin-top:12px}
|
|
.side-btn{padding:8px 12px;background:transparent;border:1px solid var(--border);color:var(--ink-dim);font-size:10px;letter-spacing:.14em;text-transform:uppercase;cursor:pointer;transition:all .25s var(--ease);border-radius:4px;font-family:var(--font-mono);text-align:center}
|
|
.side-btn:hover{border-color:var(--gold);color:var(--gold)}
|
|
.side-btn.primary{background:var(--gold);color:#0a0a0f;border-color:var(--gold);font-weight:600}
|
|
|
|
.main{display:grid;grid-template-rows:1fr auto;overflow:hidden}
|
|
.transcript{overflow-y:auto;padding:20px 32px;display:flex;flex-direction:column;gap:14px}
|
|
.msg{display:flex;gap:12px;max-width:85%;animation:msgIn .4s var(--ease)}
|
|
@keyframes msgIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
|
|
.msg.me{align-self:flex-end;flex-direction:row-reverse}
|
|
.msg-avatar{width:32px;height:32px;border-radius:4px;flex-shrink:0;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:12px;background:rgba(246,213,114,0.1);color:var(--gold)}
|
|
.msg.me .msg-avatar{background:rgba(78,205,196,0.15);color:var(--cyan)}
|
|
.msg-bubble{padding:12px 16px;border-radius:6px;background:var(--panel);border:1px solid var(--border);line-height:1.5;font-size:13px}
|
|
.msg.me .msg-bubble{background:rgba(78,205,196,0.06);border-color:rgba(78,205,196,0.25)}
|
|
.msg-header{font-size:10px;color:var(--ink-faint);letter-spacing:.12em;text-transform:uppercase;margin-bottom:6px;font-family:var(--font-mono);display:flex;gap:10px;align-items:center}
|
|
.msg-header .latency{color:var(--gold)}
|
|
.msg-header .provider{color:var(--violet)}
|
|
.msg-empty{color:var(--ink-faint);text-align:center;padding:80px 20px;font-size:13px}
|
|
.msg-empty .hint{display:block;margin-top:12px;font-size:11px;color:var(--ink-faint);font-family:var(--font-mono)}
|
|
|
|
.composer{padding:16px 24px;background:rgba(10,10,15,0.9);backdrop-filter:blur(24px);border-top:1px solid var(--border)}
|
|
.composer-row{display:flex;gap:10px;align-items:flex-end}
|
|
.composer textarea{flex:1;min-height:52px;max-height:180px;padding:14px 16px;background:rgba(18,18,26,0.6);border:1px solid var(--border);border-radius:6px;color:var(--ink);font-family:var(--font-sans);font-size:13px;resize:none;transition:all .2s;line-height:1.4}
|
|
.composer textarea:focus{outline:none;border-color:var(--gold);background:rgba(18,18,26,0.9)}
|
|
.composer-btn{padding:14px 22px;background:var(--gold);color:#0a0a0f;border:none;border-radius:6px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;font-size:11px;cursor:pointer;transition:all .25s;font-family:var(--font-mono)}
|
|
.composer-btn:hover{background:var(--ink);color:#0a0a0f;transform:translateY(-1px);box-shadow:0 6px 20px rgba(246,213,114,0.3)}
|
|
.composer-btn:disabled{opacity:.4;cursor:not-allowed}
|
|
.composer-hint{font-size:10px;color:var(--ink-faint);margin-top:8px;font-family:var(--font-mono);display:flex;gap:16px}
|
|
.composer-hint .k{color:var(--gold);padding:1px 6px;background:rgba(246,213,114,0.08);border-radius:2px;font-weight:600}
|
|
|
|
.loader{display:inline-block;width:10px;height:10px;border:2px solid var(--border);border-top-color:var(--gold);border-radius:50%;animation:spin .8s linear infinite;margin-right:6px;vertical-align:middle}
|
|
@keyframes spin{to{transform:rotate(360deg)}}
|
|
.pending{color:var(--gold);font-style:italic}
|
|
.err-msg{color:var(--coral);font-size:11px;margin-top:4px;font-family:var(--font-mono)}
|
|
|
|
/* Toast - BOTTOM LEFT doctrine zero overlap */
|
|
.toast-stack{position:fixed;bottom:24px;left:24px;z-index:9999;display:flex;flex-direction:column-reverse;gap:8px;pointer-events:none;max-width:360px}
|
|
.toast{padding:12px 16px;background:rgba(18,18,26,0.97);border:1px solid var(--border);border-left:2px solid var(--gold);border-radius:4px;font-size:11px;color:var(--ink-dim);backdrop-filter:blur(20px);animation:toastIn .4s var(--ease)}
|
|
.toast.success{border-left-color:var(--mint)}.toast.success .t{color:var(--mint)}
|
|
.toast.error{border-left-color:var(--coral)}.toast.error .t{color:var(--coral)}
|
|
.toast .t{color:var(--gold);font-size:9px;letter-spacing:.2em;text-transform:uppercase;margin-bottom:4px;font-weight:600}
|
|
@keyframes toastIn{from{opacity:0;transform:translateX(-20px)}to{opacity:1;transform:translateX(0)}}
|
|
|
|
/* Responsive */
|
|
@media(max-width:900px){
|
|
.app{grid-template-columns:1fr;grid-template-rows:60px auto 1fr}
|
|
.sidebar{max-height:260px;border-right:0;border-bottom:1px solid var(--border)}
|
|
}
|
|
</style>
|
|
<!-- DOCTRINE-60-UX-ENRICH direct-inject-20260424-143213 -->
|
|
<style id="doctrine60-ux-direct">
|
|
|
|
/* DOCTRINE-60-UX-ENRICH injected-direct */
|
|
body::before {
|
|
content: '';
|
|
position: fixed;
|
|
top: 0; left: 0; width: 100vw; height: 100vh;
|
|
background: radial-gradient(circle at 50% 50%, rgba(100,180,255,0.08), transparent 60%);
|
|
pointer-events: none;
|
|
z-index: -1;
|
|
}
|
|
.card, .kpi, .panel, .btn {
|
|
transition: all 0.3s cubic-bezier(0.2,0,0.1,1);
|
|
}
|
|
.card:hover, .kpi:hover, .panel:hover {
|
|
box-shadow: 0 4px 20px rgba(100,180,255,0.2);
|
|
border-color: rgba(100,180,255,0.5);
|
|
}
|
|
@keyframes pulseD60 {
|
|
0%,100% { opacity: 1; transform: scale(1); }
|
|
50% { opacity: 0.7; transform: scale(1.05); }
|
|
}
|
|
.pulse, .live-indicator, .active, .online {
|
|
animation: pulseD60 3s ease-in-out infinite;
|
|
}
|
|
.modal, .chat, .speech, .overlay {
|
|
backdrop-filter: blur(12px);
|
|
-webkit-backdrop-filter: blur(12px);
|
|
}
|
|
.enter-stagger {
|
|
animation: enterStagD60 0.5s cubic-bezier(0.2,0,0.1,1) forwards;
|
|
}
|
|
@keyframes enterStagD60 {
|
|
from { opacity: 0; transform: translateY(20px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
|
|
</style>
|
|
<style id="w321-ux-unif-tokens">
|
|
/* W321 UX Unification - align WTP master tokens */
|
|
:root{
|
|
--wtp-bg-card:#0e111c;
|
|
--wtp-border:#1f2436;
|
|
--wtp-border-hover:#3a425f;
|
|
--wtp-accent:#6366f1;
|
|
--wtp-accent-hover:#818cf8;
|
|
--wtp-success:#10b981;
|
|
--wtp-warning:#f59e0b;
|
|
--wtp-danger:#ef4444;
|
|
--wtp-info:#06b6d4;
|
|
--wtp-purple:#a855f7;
|
|
--wtp-radius:12px;
|
|
--wtp-radius-sm:8px;
|
|
--wtp-trans:.18s cubic-bezier(.4,0,.2,1);
|
|
--wtp-sans:'Inter',-apple-system,BlinkMacSystemFont,system-ui,sans-serif;
|
|
--wtp-mono:'JetBrains Mono','SF Mono','Fira Code',monospace;
|
|
}
|
|
/* Smooth scroll + consistent focus ring */
|
|
html{scroll-behavior:smooth}
|
|
*:focus-visible{outline:2px solid var(--wtp-accent)!important;outline-offset:2px;border-radius:4px}
|
|
/* Banner spacing */
|
|
.wevia-portal-banner + *{margin-top:0!important}
|
|
</style>
|
|
<link rel="stylesheet" href="/css/wevia-portal-consistency.css?v=w321">
|
|
</head>
|
|
<body>
|
|
<div class="wevia-portal-banner" style="position:sticky;top:0;z-index:10000">
|
|
<span class="wevia-portal-banner-label">WEVAL PORTAL</span>
|
|
<a class="wevia-portal-banner-link" href="/weval-technology-platform.html">🏛 WTP Master</a>
|
|
<a class="wevia-portal-banner-link" data-portal="master" href="/wevia-master.html">⚡ WEVIA Master</a>
|
|
<a class="wevia-portal-banner-link" href="/wevia-cockpit.html">🎯 Cockpit</a>
|
|
<a class="wevia-portal-banner-link" href="/all-ia-hub.html">🤖 All-IA Hub</a>
|
|
<a class="wevia-portal-banner-link" href="/wevia-orchestrator.html">🎛 Orchestrator</a>
|
|
<a class="wevia-portal-banner-link" href="/paperclip-dashboard.html">📎 Paperclip</a>
|
|
<a class="wevia-portal-banner-link" href="/wtp-orphans-registry.html">📋 Registry</a>
|
|
<span style="margin-left:auto;color:#64748b;font-size:10px;letter-spacing:.4px">W321 UX UNIFIED</span>
|
|
</div>
|
|
<div class="app">
|
|
<header class="header">
|
|
<div class="brand">AI Multi-Chat · WEVAL</div>
|
|
<a href="/wevia-agent.html" style="padding:6px 14px;background:rgba(167,139,250,.15);border:1px solid rgba(167,139,250,.4);border-radius:4px;color:#a78bfa;text-decoration:none;font-size:11px;letter-spacing:.14em;text-transform:uppercase;font-family:var(--font-mono);margin-right:14px">⚡ Agent Exec</a><div class="cdp-summary" id="cdp-summary">
|
|
<span>CDP Live:</span>
|
|
<b id="cdp-running">—/8</b>
|
|
<span>·</span>
|
|
<span id="cdp-coverage">0%</span>
|
|
</div>
|
|
</header>
|
|
|
|
<aside class="sidebar">
|
|
<div class="side-title">Providers CDP</div>
|
|
<div id="providers-list">
|
|
<!-- Cards injected by JS -->
|
|
</div>
|
|
<div class="side-actions">
|
|
<button class="side-btn" onclick="selectAll(true)">Select all</button>
|
|
<button class="side-btn" onclick="selectAll(false)">Deselect</button>
|
|
<button class="side-btn" onclick="refreshStatus()">↻ Refresh CDP</button>
|
|
<a class="side-btn" href="/vnc-picker.html">VNC Picker</a>
|
|
<a class="side-btn" href="/weval-technology-platform.html">← WTP</a>
|
|
</div>
|
|
</aside>
|
|
|
|
<main class="main">
|
|
<div class="transcript" id="transcript">
|
|
<div class="msg-empty">
|
|
💬 Chat unifié avec les 8 IA via CDP<br>
|
|
<span class="hint">Sélectionnez 1 ou plusieurs providers dans le panneau de gauche, puis tapez votre message ci-dessous. Le message sera broadcast en parallèle via Chrome DevTools Protocol.</span>
|
|
</div>
|
|
</div>
|
|
<div class="composer">
|
|
<div class="composer-row">
|
|
<textarea id="composer-input" placeholder="Tapez votre message... (Shift+Enter pour retour à la ligne, Enter pour envoyer)" rows="1"></textarea>
|
|
<button class="composer-btn" id="send-btn" onclick="sendBroadcast()">Broadcast</button>
|
|
</div>
|
|
<div class="composer-hint">
|
|
<span><span class="k">Enter</span> Send</span>
|
|
<span><span class="k">Shift+Enter</span> Newline</span>
|
|
<span id="selected-count">0 providers selected</span>
|
|
<label style="display:flex;align-items:center;gap:5px;cursor:pointer"><input type="checkbox" id="opt-kb" checked style="cursor:pointer"> <span class="k">KB+</span> WEVIA augment</label>
|
|
<label style="display:flex;align-items:center;gap:5px;cursor:pointer"><input type="checkbox" id="opt-fallback" checked style="cursor:pointer"> <span class="k">↩</span> Fallback sovereign</label>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
<div class="toast-stack" id="toast-stack"></div>
|
|
|
|
<script>
|
|
// Provider metadata
|
|
const PROVIDERS = [
|
|
{ slug: 'wevia', name: 'WEVIA Master', icon: 'W', port: 4000, url: 'sovereign-api', is_master: true },
|
|
{ slug: 'openai', name: 'ChatGPT', icon: 'C', port: 9222, url: 'https://chat.openai.com/' },
|
|
{ slug: 'anthropic', name: 'Claude.ai', icon: 'A', port: 9223, url: 'https://claude.ai/' },
|
|
{ slug: 'google', name: 'Gemini', icon: 'G', port: 9224, url: 'https://gemini.google.com/' },
|
|
{ slug: 'deepseek', name: 'DeepSeek', icon: 'D', port: 9225, url: 'https://chat.deepseek.com/' },
|
|
{ slug: 'mistral', name: 'Mistral', icon: 'M', port: 9226, url: 'https://chat.mistral.ai/' },
|
|
{ slug: 'poe', name: 'Poe', icon: 'P', port: 9227, url: 'https://poe.com/' },
|
|
{ slug: 'perplexity', name: 'Perplexity', icon: 'Px', port: 9228, url: 'https://www.perplexity.ai/' },
|
|
{ slug: 'hf', name: 'HuggingFace', icon: 'H', port: 9229, url: 'https://huggingface.co/chat/' }
|
|
];
|
|
|
|
const selected = new Set();
|
|
let cdpStatus = {};
|
|
|
|
// Build provider cards
|
|
function buildProviders() {
|
|
const container = document.getElementById('providers-list');
|
|
container.innerHTML = PROVIDERS.map(p => {
|
|
const running = p.is_master ? true : cdpStatus[p.slug]?.cdp_listening;
|
|
return `<div class="provider-card${selected.has(p.slug) ? ' selected' : ''}${running === false ? ' offline' : ''}" onclick="toggleProvider('${p.slug}')" data-slug="${p.slug}">
|
|
<div class="pc-check"></div>
|
|
<div class="pc-icon">${p.icon}</div>
|
|
<div class="pc-info">
|
|
<div class="pc-name">${p.name}${p.is_master ? ' <span style="color:var(--violet);font-size:9px;letter-spacing:.15em;background:rgba(167,139,250,.15);padding:2px 5px;border-radius:2px;margin-left:4px">PIVOT</span>' : ''}</div>
|
|
<div class="pc-status">
|
|
<span class="pc-dot ${running ? 'running' : 'offline'}"></span>
|
|
<span>${running ? 'CDP :' + p.port : 'offline'}</span>
|
|
</div>
|
|
</div>
|
|
</div>`;
|
|
}).join('');
|
|
updateSelectedCount();
|
|
}
|
|
|
|
function toggleProvider(slug) {
|
|
const card = document.querySelector(`[data-slug="${slug}"]`);
|
|
if (card.classList.contains('offline')) {
|
|
toast('warn', 'Provider offline', `${slug} CDP not reachable`);
|
|
return;
|
|
}
|
|
if (selected.has(slug)) {
|
|
selected.delete(slug);
|
|
card.classList.remove('selected');
|
|
} else {
|
|
selected.add(slug);
|
|
card.classList.add('selected');
|
|
}
|
|
updateSelectedCount();
|
|
}
|
|
|
|
function selectAll(on) {
|
|
PROVIDERS.forEach(p => {
|
|
const running = p.is_master ? true : cdpStatus[p.slug]?.cdp_listening;
|
|
if (!running) return;
|
|
if (on) selected.add(p.slug);
|
|
else selected.delete(p.slug);
|
|
});
|
|
buildProviders();
|
|
}
|
|
|
|
function updateSelectedCount() {
|
|
document.getElementById('selected-count').textContent = `${selected.size} provider${selected.size !== 1 ? 's' : ''} selected`;
|
|
document.getElementById('send-btn').disabled = selected.size === 0;
|
|
}
|
|
|
|
async function refreshStatus() {
|
|
try {
|
|
const r = await fetch('/api/cdp-status.php?cb=' + Date.now());
|
|
const d = await r.json();
|
|
cdpStatus = {};
|
|
(d.providers || []).forEach(p => cdpStatus[p.slug] = p);
|
|
document.getElementById('cdp-running').textContent = `${d.summary.running}/${d.summary.total}`;
|
|
document.getElementById('cdp-coverage').textContent = d.summary.coverage_pct + '%';
|
|
const b = document.getElementById('cdp-running');
|
|
b.className = d.summary.running === 8 ? '' : (d.summary.running > 4 ? 'warn' : 'err');
|
|
buildProviders();
|
|
} catch (e) {
|
|
toast('error', 'Status refresh failed', e.message);
|
|
}
|
|
}
|
|
|
|
function addMsg(role, content, meta) {
|
|
const t = document.getElementById('transcript');
|
|
// Remove empty placeholder
|
|
const empty = t.querySelector('.msg-empty');
|
|
if (empty) empty.remove();
|
|
const msg = document.createElement('div');
|
|
msg.className = `msg ${role}`;
|
|
const avatarLetter = role === 'me' ? 'Y' : (meta?.icon || '?');
|
|
msg.innerHTML = `
|
|
<div class="msg-avatar">${avatarLetter}</div>
|
|
<div class="msg-bubble">
|
|
${meta ? `<div class="msg-header">
|
|
<span class="provider">${meta.provider || ''}</span>
|
|
${meta.latency ? `<span class="latency">${meta.latency}ms</span>` : ''}
|
|
${meta.status ? `<span>${meta.status}</span>` : ''}
|
|
</div>` : ''}
|
|
<div class="msg-content">${content}</div>
|
|
</div>`;
|
|
t.appendChild(msg);
|
|
t.scrollTop = t.scrollHeight;
|
|
return msg;
|
|
}
|
|
|
|
async function sendBroadcast() {
|
|
const input = document.getElementById('composer-input');
|
|
const message = input.value.trim();
|
|
if (!message || selected.size === 0) return;
|
|
|
|
// Add user message
|
|
addMsg('me', message.replace(/</g, '<').replace(/\n/g, '<br>'));
|
|
input.value = '';
|
|
input.style.height = 'auto';
|
|
|
|
// Create pending placeholders
|
|
const pending = {};
|
|
for (const slug of selected) {
|
|
const p = PROVIDERS.find(x => x.slug === slug);
|
|
pending[slug] = addMsg('ai', `<span class="loader"></span><span class="pending">Sending to ${p.name} via CDP...</span>`, { provider: p.name, icon: p.icon, status: 'pending' });
|
|
}
|
|
|
|
// Broadcast to backend
|
|
try {
|
|
const r = await fetch('/api/cdp-broadcast.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ message, providers: Array.from(selected), augment_with_kb: document.getElementById('opt-kb').checked, fallback_sovereign: document.getElementById('opt-fallback').checked })
|
|
});
|
|
const d = await r.json();
|
|
|
|
if (!d.ok) {
|
|
toast('error', 'Broadcast failed', d.error || 'Unknown error');
|
|
// Update pending with error
|
|
Object.values(pending).forEach(el => {
|
|
el.querySelector('.msg-content').innerHTML = `<span class="err-msg">❌ Backend error: ${d.error || 'unknown'}</span>`;
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Update each pending with actual response
|
|
(d.responses || []).forEach(resp => {
|
|
const el = pending[resp.slug];
|
|
if (!el) return;
|
|
const p = PROVIDERS.find(x => x.slug === resp.slug);
|
|
const content = el.querySelector('.msg-content');
|
|
const header = el.querySelector('.msg-header');
|
|
if (resp.ok) {
|
|
content.innerHTML = (resp.response || '(empty response)').replace(/</g, '<').replace(/\n/g, '<br>');
|
|
header.innerHTML = `<span class="provider">${p.name}</span><span class="latency">${resp.latency_ms}ms</span><span style="color:var(--mint)">✓ OK</span>`;
|
|
} else {
|
|
content.innerHTML = `<span class="err-msg">⚠ ${resp.error || 'Failed'}</span>`;
|
|
header.innerHTML = `<span class="provider">${p.name}</span><span style="color:var(--coral)">✗ FAILED</span>`;
|
|
}
|
|
});
|
|
|
|
toast('success', `Broadcast complete`, `${d.summary.ok}/${d.summary.total} providers answered`);
|
|
} catch (e) {
|
|
toast('error', 'Network error', e.message);
|
|
Object.values(pending).forEach(el => {
|
|
el.querySelector('.msg-content').innerHTML = `<span class="err-msg">❌ ${e.message}</span>`;
|
|
});
|
|
}
|
|
}
|
|
|
|
function toast(type, title, content) {
|
|
const stack = document.getElementById('toast-stack');
|
|
const t = document.createElement('div');
|
|
t.className = `toast ${type === 'success' ? 'success' : type === 'error' ? 'error' : ''}`;
|
|
t.innerHTML = `<div class="t">${title}</div><div>${content}</div>`;
|
|
stack.appendChild(t);
|
|
setTimeout(() => t.style.opacity = '0', 5000);
|
|
setTimeout(() => t.remove(), 5500);
|
|
}
|
|
|
|
// Input handling
|
|
const ta = document.getElementById('composer-input');
|
|
ta.addEventListener('input', () => {
|
|
ta.style.height = 'auto';
|
|
ta.style.height = Math.min(ta.scrollHeight, 180) + 'px';
|
|
});
|
|
ta.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
e.preventDefault();
|
|
sendBroadcast();
|
|
}
|
|
});
|
|
|
|
// Init
|
|
refreshStatus();
|
|
setInterval(refreshStatus, 10000);
|
|
|
|
// Auto-select WEVIA Master on load (it's the patron)
|
|
setTimeout(() => { selected.add('wevia'); buildProviders(); }, 500); // poll every 10s
|
|
</script>
|
|
<!-- DOCTRINE-60-UX-JS --><script id="doctrine60-ux-js-direct">
|
|
|
|
// DOCTRINE-60-UX-JS staggered entrance
|
|
(function(){
|
|
if (!('IntersectionObserver' in window)) return;
|
|
const obs = new IntersectionObserver((entries) => {
|
|
entries.forEach((e, i) => {
|
|
if (e.isIntersecting) {
|
|
setTimeout(() => e.target.classList.add('enter-stagger'), i * 80);
|
|
obs.unobserve(e.target);
|
|
}
|
|
});
|
|
});
|
|
document.querySelectorAll('.card, .kpi, .panel').forEach(el => obs.observe(el));
|
|
})();
|
|
|
|
</script>
|
|
</body>
|
|
</html>
|