Files
html/wevia-agent.html

429 lines
25 KiB
HTML

<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>WEVIA Agent · Plan → Execute</title>
<style>
:root{
--bg:#0a0a0f;--panel:rgba(18,18,26,0.7);--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;
--font-sans:"Inter",-apple-system,system-ui,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(167,139,250,0.06) 0%,transparent 60%),
radial-gradient(ellipse at 80% 70%,rgba(78,205,196,0.04) 0%,transparent 60%);
display:flex;flex-direction:column;align-items:center;padding:0}
.app{width:100%;max-width:1100px;display:flex;flex-direction:column;height:100vh}
header{display:flex;align-items:center;justify-content:space-between;padding:16px 24px;border-bottom:1px solid var(--border);background:rgba(10,10,15,0.85);backdrop-filter:blur(24px)}
.brand{display:flex;align-items:center;gap:12px;font-size:13px;letter-spacing:.2em;text-transform:uppercase;color:var(--violet);font-weight:600}
.brand::before{content:"⚡";font-size:18px;animation:pulse 2s ease infinite}
@keyframes pulse{50%{opacity:.5}}
.nav-links{display:flex;gap:8px}
.nav-links a{padding:6px 12px;background:transparent;border:1px solid var(--border);color:var(--ink-dim);font-size:10px;letter-spacing:.14em;text-transform:uppercase;text-decoration:none;border-radius:4px;transition:.2s;font-family:var(--font-mono)}
.nav-links a:hover{border-color:var(--gold);color:var(--gold)}
.transcript{flex:1;overflow-y:auto;padding:24px;display:flex;flex-direction:column;gap:12px}
.welcome{text-align:center;padding:80px 20px;color:var(--ink-faint)}
.welcome h2{color:var(--violet);font-weight:300;margin-bottom:14px;font-size:24px}
.welcome p{font-size:13px;line-height:1.6;margin-bottom:8px}
.examples{margin-top:24px;display:flex;flex-direction:column;gap:8px;max-width:600px;margin-left:auto;margin-right:auto}
.ex-btn{padding:10px 14px;background:rgba(167,139,250,0.06);border:1px solid rgba(167,139,250,0.2);border-radius:4px;color:var(--ink-dim);text-align:left;cursor:pointer;font-size:12px;transition:.2s;font-family:var(--font-mono)}
.ex-btn:hover{border-color:var(--violet);color:var(--violet);background:rgba(167,139,250,0.1)}
.event{padding:12px 16px;border-radius:6px;background:var(--panel);border:1px solid var(--border);font-size:13px;line-height:1.5;animation:in .4s ease}
@keyframes in{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}
.event-header{display:flex;align-items:center;gap:10px;margin-bottom:8px;font-size:10px;letter-spacing:.15em;text-transform:uppercase;font-family:var(--font-mono);color:var(--ink-faint)}
.event-icon{font-size:14px}
.event.user{background:rgba(78,205,196,0.06);border-color:rgba(78,205,196,0.3);align-self:flex-end;max-width:80%}
.event.thinking{background:rgba(167,139,250,0.04);border-color:rgba(167,139,250,0.2)}
.event.thinking .event-header{color:var(--violet)}
.event.plan{background:rgba(246,213,114,0.04);border-left:3px solid var(--gold)}
.event.plan .event-header{color:var(--gold)}
.event.exec{background:rgba(0,0,0,0.4);border-color:rgba(78,205,196,0.2);font-family:var(--font-mono);font-size:11px}
.event.exec .event-header{color:var(--cyan)}
.event.exec .cmd{color:var(--cyan);background:rgba(78,205,196,0.06);padding:6px 10px;border-radius:3px;margin:4px 0;display:block;word-break:break-all;border-left:2px solid var(--cyan)}
.event.exec .output{color:var(--ink-dim);background:rgba(0,0,0,0.5);padding:8px 12px;border-radius:3px;margin-top:6px;white-space:pre-wrap;max-height:300px;overflow-y:auto;font-size:11px}
.event.exec.failed .cmd{border-left-color:var(--coral)}
.event.exec.failed .output{color:var(--coral)}
.event.recovery{background:rgba(246,213,114,0.06);border-left:3px solid var(--gold);font-style:italic}
.event.summary{background:rgba(92,219,149,0.06);border-left:3px solid var(--mint);padding:16px 20px}
.event.summary .event-header{color:var(--mint)}
.event.summary .content{color:var(--ink);line-height:1.6;white-space:pre-wrap}
.event.error{background:rgba(255,107,107,0.06);border-left:3px solid var(--coral)}
.event.done{background:rgba(92,219,149,0.04);text-align:center;color:var(--mint);font-size:11px;font-family:var(--font-mono);letter-spacing:.15em;text-transform:uppercase}
.plan-list{list-style:none;padding:0;margin:8px 0}
.plan-list li{padding:6px 0 6px 22px;position:relative;color:var(--ink);font-size:12px}
.plan-list li::before{content:"";position:absolute;left:6px;top:11px;width:6px;height:6px;border-radius:50%;background:var(--gold)}
.commands-preview{margin-top:10px;font-family:var(--font-mono);font-size:11px;color:var(--cyan);padding:8px 10px;background:rgba(0,0,0,0.4);border-radius:3px;display:block;word-break:break-all}
.composer{padding:16px 24px;border-top:1px solid var(--border);background:rgba(10,10,15,0.95);backdrop-filter:blur(24px)}
.composer-row{display:flex;gap:10px;align-items:flex-end}
.composer textarea{flex:1;min-height:54px;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;line-height:1.4;outline:none;transition:.2s}
.composer textarea:focus{border-color:var(--violet);background:rgba(18,18,26,0.9)}
.composer-btn{padding:14px 22px;background:var(--violet);color:#0a0a0f;border:none;border-radius:6px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;font-size:11px;cursor:pointer;transition:.2s;font-family:var(--font-mono)}
.composer-btn:hover{transform:translateY(-1px);box-shadow:0 8px 20px rgba(167,139,250,0.4)}
.composer-btn:disabled{opacity:.4;cursor:not-allowed}
.composer-opts{display:flex;gap:16px;margin-top:8px;font-size:10px;color:var(--ink-faint);font-family:var(--font-mono);align-items:center}
.composer-opts label{display:flex;align-items:center;gap:6px;cursor:pointer}
.composer-opts input[type="number"]{width:38px;padding:2px 4px;background:rgba(0,0,0,0.4);border:1px solid var(--border);color:var(--violet);border-radius:3px;font-family:var(--font-mono)}
.loader{display:inline-block;width:8px;height:8px;border:2px solid var(--border);border-top-color:var(--violet);border-radius:50%;animation:spin .8s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
.badge{display:inline-block;padding:2px 6px;background:rgba(167,139,250,0.15);color:var(--violet);font-size:9px;border-radius:2px;margin-left:6px;font-family:var(--font-mono);letter-spacing:.1em}
.event.consensus_resp{background:rgba(78,205,196,0.04);border-left:3px solid var(--cyan);font-size:12px}
.event.consensus_resp .vote-approve{color:var(--mint);font-weight:700}
.event.consensus_resp .vote-reject{color:var(--coral);font-weight:700}
.event.consensus_resp .vote-modify{color:var(--gold);font-weight:700}
.event.consensus_resp .vote-error,.event.consensus_resp .vote-unparseable{color:var(--ink-faint);font-style:italic}
.event.consensus_decision{background:rgba(167,139,250,0.08);border-left:3px solid var(--violet);padding:14px 18px}
.event.consensus_decision .tally{display:flex;gap:14px;margin:8px 0;font-family:var(--font-mono);font-size:12px}
.event.consensus_decision .tally .t{padding:4px 10px;background:rgba(0,0,0,0.4);border-radius:3px}
.event.consensus_decision .tally .t b{font-size:14px}
.event.consensus_decision .tally .approve b{color:var(--mint)}
.event.consensus_decision .tally .reject b{color:var(--coral)}
.event.consensus_decision .tally .modify b{color:var(--gold)}
.event.consensus_decision .decision-badge{display:inline-block;padding:4px 10px;border-radius:3px;font-size:11px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;font-family:var(--font-mono);margin-bottom:8px}
.event.consensus_decision .decision-approved{background:rgba(92,219,149,0.15);color:var(--mint)}
.event.consensus_decision .decision-rejected_by_majority{background:rgba(255,107,107,0.15);color:var(--coral)}
.event.consensus_decision .decision-modify_recommended{background:rgba(246,213,114,0.15);color:var(--gold)}
.event.consensus_decision .decision-no_quorum{background:rgba(139,134,128,0.15);color:var(--ink-faint)}
.event.aborted{background:rgba(255,107,107,0.08);border-left:3px solid var(--coral);padding:14px}
.event.aborted .event-header{color:var(--coral)}
.event.plan_revised{background:rgba(246,213,114,0.06);border-left:3px solid var(--gold);padding:12px 16px}
.event.plan_revised .event-header{color:var(--gold)}
</style>
<!-- DOCTRINE-60-UX-ENRICH direct-inject-20260424-143216 -->
<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>
</head>
<body>
<div class="app">
<header>
<div class="brand">WEVIA Agent · Plan → Execute</div>
<div class="nav-links">
<a href="/ai-multichat.html">💬 Multi-Chat</a>
<a href="/vnc-picker.html">VNC Picker</a>
<a href="/weval-technology-platform.html">← WTP</a>
</div>
</header>
<div class="transcript" id="transcript">
<div class="welcome">
<h2>⚡ Agent autonome plan → execute</h2>
<p>WEVIA Master agit comme Opus directement sur S204 : analyse ta demande, génère un plan,<br>exécute des commandes bash en mode autonome, et te résume le résultat.</p>
<p style="color:var(--ink-faint);font-size:11px">Powered by Cerebras-think (planning) + Cerebras-fast (recovery & summary) · Sovereign API · 0€ cost</p>
<div class="examples">
<button class="ex-btn" onclick="useExample(this)">Combien de pages HTML dans /var/www/html et taille totale ?</button>
<button class="ex-btn" onclick="useExample(this)">Vérifie quels intents PHP sont activés dans /var/www/html/api/wired-pending</button>
<button class="ex-btn" onclick="useExample(this)">Trouve les 5 plus gros fichiers .log de /var/log et leur date modification</button>
<button class="ex-btn" onclick="useExample(this)">Test si les 8 ports CDP 9222-9229 répondent et lesquels sont down</button>
<button class="ex-btn" onclick="useExample(this)">Combien de commits git sur les 24 dernières heures et leurs messages</button>
</div>
</div>
</div>
<div class="composer">
<div class="composer-row">
<textarea id="task-input" placeholder="Décris ta tâche en langage naturel... (ex: 'Vérifie que les 8 chrome CDP tournent et redémarre ceux qui sont down')" rows="1"></textarea>
<button class="composer-btn" id="send-btn" onclick="executeTask()">Execute</button>
</div>
<div class="composer-opts">
<label><input type="checkbox" id="opt-dry-run"> Dry run (plan seul)</label>
<label><input type="checkbox" id="opt-consensus"> 🗳 Consensus N IA avant exec</label>
<label>Max steps: <input type="number" id="opt-max-steps" min="1" max="10" value="5"></label>
<label style="color:var(--ink-faint)">Enter · Shift+Enter</label>
</div>
</div>
</div>
<script>
const transcript = document.getElementById('transcript');
const input = document.getElementById('task-input');
function useExample(btn) {
input.value = btn.textContent;
input.focus();
input.style.height = 'auto';
input.style.height = Math.min(input.scrollHeight, 180) + 'px';
}
function clearWelcome() {
const w = transcript.querySelector('.welcome');
if (w) w.remove();
}
function addEvent(type, content, meta) {
const ev = document.createElement('div');
ev.className = `event ${type}`;
let header = '';
let body = '';
switch(type) {
case 'user':
header = `<span class="event-icon">👤</span><span>You</span>`;
body = `<div>${escapeHtml(content)}</div>`;
break;
case 'thinking':
header = `<span class="event-icon"><span class="loader"></span></span><span>Thinking</span>`;
body = `<div style="color:var(--violet);font-style:italic">${escapeHtml(content)}</div>`;
break;
case 'plan':
header = `<span class="event-icon">📋</span><span>Plan</span>${meta?.provider ? '<span class="badge">'+meta.provider+'</span>' : ''}`;
body = `<ul class="plan-list">${(content.plan || []).map(p => `<li>${escapeHtml(p)}</li>`).join('')}</ul>
<div style="margin-top:10px;font-size:10px;color:var(--ink-faint);text-transform:uppercase;letter-spacing:.15em">Commands</div>
${(content.commands || []).map(c => `<code class="commands-preview">$ ${escapeHtml(c)}</code>`).join('')}`;
break;
case 'exec':
const failed = !content.ok || (content.output && /^find:.*Permission/i.test(content.output) && !/\d/.test(content.output));
ev.className = `event exec${failed ? ' failed' : ''}`;
header = `<span class="event-icon">${content.ok ? '✓' : '✗'}</span><span>Step ${content.step}</span>${content.duration_ms ? '<span style="color:var(--gold);margin-left:auto">'+content.duration_ms+'ms</span>' : ''}`;
body = `<code class="cmd">$ ${escapeHtml(content.cmd)}</code>${content.output ? '<div class="output">'+escapeHtml(content.output)+'</div>' : ''}${content.error ? '<div class="output" style="color:var(--coral)">'+escapeHtml(content.error)+'</div>' : ''}`;
break;
case 'recovery':
header = `<span class="event-icon">🔧</span><span>Recovery</span>`;
body = `<code class="cmd">$ ${escapeHtml(content.fix_cmd || content.cmd || '')}</code>${content.output ? '<div class="output">'+escapeHtml(content.output)+'</div>' : ''}`;
break;
case 'summary':
header = `<span class="event-icon">📊</span><span>Summary</span>${meta?.provider ? '<span class="badge">'+meta.provider+'</span>' : ''}`;
body = `<div class="content">${escapeHtml(content.content || '').replace(/\*\*(.+?)\*\*/g, '<b>$1</b>').replace(/\n/g, '<br>')}</div>`;
break;
case 'error':
header = `<span class="event-icon">⚠</span><span>Error</span>`;
body = `<div style="color:var(--coral)">${escapeHtml(content.msg || JSON.stringify(content))}</div>`;
break;
case 'consensus_resp':
const voteCls = 'vote-' + (content.vote || 'unknown');
header = `<span class="event-icon">🗳</span><span>${content.model || "?"}</span>${content.provider ? '<span class="badge">'+content.provider+'</span>' : ''}<span style="margin-left:auto" class="${voteCls}">${(content.vote || "?").toUpperCase()}${content.confidence ? ' ('+content.confidence+'/10)' : ''}</span>`;
body = `<div>${escapeHtml(content.rationale || "")}</div>${(content.concerns || []).length ? '<div style="margin-top:6px;font-size:11px;color:var(--ink-faint)">⚠ '+content.concerns.map(c=>escapeHtml(c)).join(' · ')+'</div>' : ''}${(content.suggested_changes || []).length ? '<div style="margin-top:6px;font-size:11px;color:var(--gold)">💡 '+content.suggested_changes.map(s=>escapeHtml(s)).join(' / ')+'</div>' : ''}`;
break;
case 'consensus_decision':
const dec = content.decision || 'unknown';
const t = content.tally || {};
header = `<span class="event-icon">⚖️</span><span>Consensus Decision</span>`;
body = `<div class="decision-badge decision-${dec}">${dec.replace(/_/g, ' ')}</div>
<div class="tally">
<div class="t approve"><b>${t.approve||0}</b> approve</div>
<div class="t reject"><b>${t.reject||0}</b> reject</div>
<div class="t modify"><b>${t.modify||0}</b> modify</div>
</div>
${(content.concerns_aggregated || []).length ? '<div style="margin-top:8px;font-size:12px"><b style="color:var(--coral)">Concerns agrégés:</b><br>'+content.concerns_aggregated.map(c=>'• '+escapeHtml(c)).join('<br>')+'</div>' : ''}
${(content.suggestions_aggregated || []).length ? '<div style="margin-top:8px;font-size:12px"><b style="color:var(--gold)">Suggestions agrégées:</b><br>'+content.suggestions_aggregated.map(s=>'• '+escapeHtml(s)).join('<br>')+'</div>' : ''}`;
break;
case 'plan_revised':
header = `<span class="event-icon">🔄</span><span>Plan révisé (consensus)</span>`;
body = `<div style="font-size:11px;color:var(--ink-faint);margin-bottom:6px">${content.integrated_suggestions||0} suggestions intégrées</div><ul class="plan-list">${(content.plan || []).map(p => '<li>'+escapeHtml(p)+'</li>').join('')}</ul>${(content.commands || []).map(c => '<code class="commands-preview">$ '+escapeHtml(c)+'</code>').join('')}`;
break;
case 'aborted':
header = `<span class="event-icon">🛑</span><span>Execution Aborted</span>`;
body = `<div><b>Reason:</b> ${escapeHtml(content.reason || "unknown")}</div>${(content.concerns || []).length ? '<div style="margin-top:6px"><b>Concerns:</b><br>'+content.concerns.map(c=>'• '+escapeHtml(c)).join('<br>')+'</div>' : ''}`;
break;
case 'done':
header = '';
body = `✓ Done · ${content.steps_executed || 0} steps · ${content.success_count || 0} success${content.consensus_decision ? ' · consensus: '+content.consensus_decision : ''}${content.aborted ? ' · ABORTED' : ''}`;
break;
}
ev.innerHTML = (header ? `<div class="event-header">${header}</div>` : '') + body;
transcript.appendChild(ev);
transcript.scrollTop = transcript.scrollHeight;
return ev;
}
function escapeHtml(s) {
return String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
}
let activeStream = null;
let pendingThinking = null;
async function executeTask() {
const task = input.value.trim();
if (!task) return;
clearWelcome();
const dry = document.getElementById('opt-dry-run').checked;
const maxSteps = parseInt(document.getElementById('opt-max-steps').value || '5');
const btn = document.getElementById('send-btn');
btn.disabled = true;
btn.textContent = 'Running...';
addEvent('user', task);
input.value = '';
input.style.height = 'auto';
try {
const resp = await fetch('/api/wevia-agent-exec.php', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({task, dry_run: dry, max_steps: maxSteps, consensus: document.getElementById('opt-consensus').checked, consensus_models: ['cerebras-fast','groq','gemini','mistral','cloudflare-ai']})
});
if (!resp.ok) {
addEvent('error', {msg: 'HTTP ' + resp.status});
return;
}
const reader = resp.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const {value, done} = await reader.read();
if (done) break;
buffer += decoder.decode(value, {stream: true});
const events = buffer.split('\n\n');
buffer = events.pop() || '';
for (const block of events) {
if (!block.trim()) continue;
const lines = block.split('\n');
let eventType = 'message', dataStr = '';
for (const line of lines) {
if (line.startsWith('event: ')) eventType = line.slice(7);
if (line.startsWith('data: ')) dataStr = line.slice(6);
}
let data;
try { data = JSON.parse(dataStr); } catch(e) { continue; }
// Remove pending thinking before showing concrete result
if (['plan','exec_result','summary','error'].includes(eventType) && pendingThinking) {
pendingThinking.remove();
pendingThinking = null;
}
if (eventType === 'start') {
// Skip - already shown user
} else if (eventType === 'thinking' || eventType === 'recovery_thinking') {
pendingThinking = addEvent('thinking', data.msg || 'Processing...');
} else if (eventType === 'plan') {
addEvent('plan', data, {provider: data.provider});
} else if (eventType === 'consensus_query') {
pendingThinking = addEvent('thinking', '🗳 Interroge ' + data.model + '...');
} else if (eventType === 'consensus_response') {
if (pendingThinking) { pendingThinking.remove(); pendingThinking = null; }
addEvent('consensus_resp', data);
} else if (eventType === 'consensus_decision') {
addEvent('consensus_decision', data);
} else if (eventType === 'plan_revised') {
addEvent('plan_revised', data);
} else if (eventType === 'aborted') {
addEvent('aborted', data);
} else if (eventType === 'exec_start') {
// Skip - covered by exec_result
} else if (eventType === 'exec_result') {
addEvent('exec', data);
} else if (eventType === 'recovery_exec' || eventType === 'recovery_result') {
addEvent('recovery', data);
} else if (eventType === 'summary') {
addEvent('summary', data, {provider: data.provider});
} else if (eventType === 'done') {
addEvent('done', data);
} else if (eventType === 'consensus_query') {
pendingThinking = addEvent('thinking', '🗳 Vote ' + data.model + '...');
} else if (eventType === 'consensus_response') {
const voteIcon = data.vote === 'approve' ? '✅' : (data.vote === 'reject' ? '❌' : (data.vote === 'modify' ? '✏️' : '⚠️'));
const ev = document.createElement('div');
ev.className = 'event plan';
ev.innerHTML = '<div class="event-header"><span class="event-icon">' + voteIcon + '</span><span>Vote ' + data.model + '</span><span class="badge">' + (data.provider || '?') + '</span><span style="margin-left:auto;color:var(--gold)">conf ' + (data.confidence||'?') + '/10</span></div>' +
'<div style="font-weight:700;color:var(--violet);margin-bottom:6px">' + data.vote.toUpperCase() + '</div>' +
(data.rationale ? '<div style="font-size:11px;color:var(--ink-dim);margin-bottom:6px">' + escapeHtml(data.rationale) + '</div>' : '') +
(data.concerns && data.concerns.length ? '<div style="font-size:11px;color:var(--coral);margin-top:6px">⚠ ' + data.concerns.map(escapeHtml).join('<br>⚠ ') + '</div>' : '') +
(data.suggested_changes && data.suggested_changes.length ? '<div style="font-size:10px;color:var(--cyan);margin-top:6px;font-family:var(--font-mono)">↻ ' + data.suggested_changes.map(escapeHtml).join('<br>↻ ') + '</div>' : '');
transcript.appendChild(ev);
transcript.scrollTop = transcript.scrollHeight;
} else if (eventType === 'consensus_decision') {
const decisionIcon = data.decision === 'approved' ? '✅' : (data.decision === 'rejected_by_majority' ? '❌' : (data.decision === 'modify_recommended' ? '✏️' : '⚠️'));
addEvent('summary', {content: decisionIcon + ' DÉCISION CONSENSUS: ' + data.decision.toUpperCase() + '\n\nVotes: approve=' + data.tally.approve + ' · reject=' + data.tally.reject + ' · modify=' + data.tally.modify + ' / ' + data.tally.total + '\n\n' + (data.concerns_aggregated.length ? 'Concerns:\n• ' + data.concerns_aggregated.join('\n• ') + '\n\n' : '') + (data.suggestions_aggregated.length ? 'Suggestions intégrées:\n• ' + data.suggestions_aggregated.join('\n• ') : '')});
} else if (eventType === 'plan_revised') {
addEvent('plan', data, {provider: 'WEVIA-revised'});
} else if (eventType === 'aborted') {
addEvent('error', {msg: '🛑 ABORTED: ' + data.reason + '\n\nConcerns: ' + (data.concerns||[]).join(' · ')});
} else if (eventType === 'error') {
addEvent('error', data);
}
}
}
} catch(e) {
addEvent('error', {msg: 'Network error: ' + e.message});
} finally {
btn.disabled = false;
btn.textContent = 'Execute';
if (pendingThinking) { pendingThinking.remove(); pendingThinking = null; }
}
}
// Input handling
input.addEventListener('input', () => {
input.style.height = 'auto';
input.style.height = Math.min(input.scrollHeight, 180) + 'px';
});
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
executeTask();
}
});
</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>