Files
html/api/wevia-chat.html
2026-04-12 22:57:03 +02:00

574 lines
15 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>WEVIA Master AI</title>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--bg: #08080c;
--bg2: #0e0e14;
--bg3: #15151f;
--bg4: #1c1c2a;
--border: #252538;
--text: #e4e2f0;
--text2: #9896ab;
--text3: #5c5a6e;
--accent: #00e87b;
--accent2: #00e87b22;
--blue: #3d7dff;
--orange: #ff8833;
--red: #ff3355;
--purple: #9966ff;
--yellow: #ffc800;
--radius: 16px;
--font: 'Outfit', sans-serif;
--mono: 'IBM Plex Mono', monospace;
}
* { margin:0; padding:0; box-sizing:border-box; }
html, body { height:100%; overflow:hidden; }
body {
background: var(--bg);
color: var(--text);
font-family: var(--font);
display: flex;
flex-direction: column;
}
/* HEADER */
.header {
display: flex;
align-items: center;
padding: 12px 16px;
background: var(--bg2);
border-bottom: 1px solid var(--border);
flex-shrink: 0;
gap: 12px;
z-index: 10;
}
.logo {
width: 36px; height: 36px;
background: var(--accent);
border-radius: 10px;
display: flex; align-items: center; justify-content: center;
font-family: var(--mono);
font-weight: 600;
font-size: 14px;
color: #000;
flex-shrink: 0;
}
.header-info { flex:1; min-width:0; }
.header-title {
font-weight: 600;
font-size: 15px;
letter-spacing: -0.3px;
}
.header-status {
font-size: 11px;
color: var(--text2);
font-family: var(--mono);
display: flex;
align-items: center;
gap: 6px;
}
.status-dot {
width: 6px; height: 6px;
border-radius: 50%;
background: var(--accent);
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.4} }
.header-actions {
display: flex; gap: 6px;
}
.header-btn {
background: var(--bg3);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text2);
font-family: var(--mono);
font-size: 10px;
padding: 5px 10px;
cursor: pointer;
transition: all .2s;
}
.header-btn:hover { background: var(--bg4); color: var(--text); }
.header-btn.active { border-color: var(--accent); color: var(--accent); }
/* MESSAGES */
.messages {
flex: 1;
overflow-y: auto;
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
scroll-behavior: smooth;
}
.msg {
max-width: 85%;
animation: fadeIn .3s ease;
}
@keyframes fadeIn { from{opacity:0;transform:translateY(8px)} to{opacity:1;transform:translateY(0)} }
.msg.user {
align-self: flex-end;
}
.msg.ai {
align-self: flex-start;
}
.msg-bubble {
padding: 12px 16px;
border-radius: var(--radius);
font-size: 14px;
line-height: 1.65;
word-break: break-word;
}
.msg.user .msg-bubble {
background: var(--accent);
color: #000;
border-bottom-right-radius: 4px;
font-weight: 400;
}
.msg.ai .msg-bubble {
background: var(--bg3);
border: 1px solid var(--border);
border-bottom-left-radius: 4px;
}
.msg.ai .msg-bubble pre {
background: var(--bg);
border: 1px solid var(--border);
border-radius: 8px;
padding: 10px 12px;
margin: 8px 0;
overflow-x: auto;
font-family: var(--mono);
font-size: 12px;
line-height: 1.5;
}
.msg.ai .msg-bubble code {
font-family: var(--mono);
font-size: 12px;
background: var(--bg);
padding: 2px 5px;
border-radius: 4px;
}
.msg-meta {
display: flex;
align-items: center;
gap: 8px;
margin-top: 4px;
padding: 0 4px;
font-family: var(--mono);
font-size: 10px;
color: var(--text3);
flex-wrap: wrap;
}
.meta-tag {
padding: 1px 6px;
border-radius: 4px;
font-size: 9px;
font-weight: 500;
letter-spacing: 0.3px;
}
.tier0 { background: var(--accent2); color: var(--accent); }
.tier1 { background: #3d7dff22; color: var(--blue); }
.tier2 { background: #9966ff22; color: var(--purple); }
.rag-tag { background: #ffc80022; color: var(--yellow); }
/* TYPING */
.typing {
display: none;
align-self: flex-start;
padding: 14px 18px;
background: var(--bg3);
border: 1px solid var(--border);
border-radius: var(--radius);
border-bottom-left-radius: 4px;
}
.typing.active { display: flex; gap: 4px; align-items: center; }
.typing span {
width: 6px; height: 6px;
background: var(--text3);
border-radius: 50%;
animation: bounce 1.4s ease-in-out infinite;
}
.typing span:nth-child(2) { animation-delay: .2s; }
.typing span:nth-child(3) { animation-delay: .4s; }
@keyframes bounce { 0%,60%,100%{transform:translateY(0)} 30%{transform:translateY(-6px)} }
/* AGENT PANEL */
.agent-panel {
display: none;
padding: 8px 16px;
background: var(--bg2);
border-top: 1px solid var(--border);
flex-shrink: 0;
overflow-x: auto;
white-space: nowrap;
gap: 8px;
}
.agent-panel.visible { display: flex; }
.agent-btn {
background: var(--bg3);
border: 1px solid var(--border);
border-radius: 20px;
color: var(--text2);
font-family: var(--mono);
font-size: 11px;
padding: 6px 14px;
cursor: pointer;
transition: all .2s;
white-space: nowrap;
flex-shrink: 0;
}
.agent-btn:hover { border-color: var(--accent); color: var(--accent); }
/* INPUT */
.input-area {
padding: 12px 16px;
background: var(--bg2);
border-top: 1px solid var(--border);
flex-shrink: 0;
}
.input-wrap {
display: flex;
align-items: flex-end;
gap: 8px;
background: var(--bg3);
border: 1px solid var(--border);
border-radius: 14px;
padding: 8px 12px;
transition: border-color .2s;
}
.input-wrap:focus-within { border-color: var(--accent); }
.input-wrap textarea {
flex: 1;
background: transparent;
border: none;
outline: none;
color: var(--text);
font-family: var(--font);
font-size: 14px;
resize: none;
min-height: 20px;
max-height: 120px;
line-height: 1.5;
}
.send-btn {
width: 36px; height: 36px;
background: var(--accent);
border: none;
border-radius: 10px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: all .15s;
}
.send-btn:hover { transform: scale(1.05); }
.send-btn:active { transform: scale(0.95); }
.send-btn:disabled { opacity: .3; cursor: not-allowed; }
.send-btn svg { width: 18px; height: 18px; }
/* QUICK ACTIONS */
.quick-actions {
display: flex;
gap: 6px;
padding: 8px 0 0;
overflow-x: auto;
flex-wrap: wrap;
}
.quick-btn {
background: var(--bg4);
border: 1px solid var(--border);
border-radius: 20px;
color: var(--text2);
font-size: 12px;
padding: 5px 12px;
cursor: pointer;
transition: all .2s;
white-space: nowrap;
}
.quick-btn:hover { border-color: var(--accent); color: var(--text); }
/* SCROLLBAR */
::-webkit-scrollbar { width: 4px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
</style>
</head>
<body>
<!-- HEADER -->
<div class="header">
<div class="logo">W</div>
<div class="header-info">
<div class="header-title">WEVIA Master AI</div>
<div class="header-status">
<span class="status-dot" id="statusDot"></span>
<span id="statusText">Connecting...</span>
</div>
</div>
<div class="header-actions">
<button class="header-btn" onclick="toggleAgents()" id="agentToggle">Agents</button>
<button class="header-btn" onclick="openDashboard()">Stats</button>
</div>
</div>
<!-- AGENT PANEL -->
<div class="agent-panel" id="agentPanel">
<button class="agent-btn" onclick="runAgent('monitor','full health check')">Monitor</button>
<button class="agent-btn" onclick="runAgent('devops','check disk memory docker')">DevOps</button>
<button class="agent-btn" onclick="runAgent('ethica','count HCP top specialties')">Ethica</button>
<button class="agent-btn" onclick="runAgent('security','check SSL and ports')">Security</button>
</div>
<!-- MESSAGES -->
<div class="messages" id="messages">
<div class="msg ai">
<div class="msg-bubble">
Salut Yacine. WEVIA Master AI opérationnel — 18 providers à 0€, RAG Qdrant 14K vecteurs, 4 agents autonomes. Qu'est-ce qu'on attaque ?
</div>
<div class="msg-meta">
<span class="meta-tag tier1">SYSTEM</span>
<span>8 modèles Ollama + 11 cloud</span>
</div>
</div>
</div>
<!-- TYPING INDICATOR -->
<div class="typing" id="typing">
<span></span><span></span><span></span>
</div>
<!-- INPUT -->
<div class="input-area">
<div class="input-wrap">
<textarea id="input" placeholder="Message WEVIA..." rows="1" onkeydown="handleKey(event)" oninput="autoResize(this)"></textarea>
<button class="send-btn" id="sendBtn" onclick="sendMessage()">
<svg viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<line x1="22" y1="2" x2="11" y2="13"></line>
<polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
</svg>
</button>
</div>
<div class="quick-actions" id="quickActions">
<button class="quick-btn" onclick="quickSend('Status des 3 serveurs S204 S95 S151')">Status serveurs</button>
<button class="quick-btn" onclick="quickSend('Ethica: combien de HCP par spécialité?')">HCP Ethica</button>
<button class="quick-btn" onclick="quickSend('Quel est le disk usage et les containers Docker?')">Disk + Docker</button>
<button class="quick-btn" onclick="quickSend('Comment optimiser les performances WEVADS?')">Optim WEVADS</button>
</div>
</div>
<script>
const API = '/api/wevia-master-api.php';
const AGENT_API = '/api/wevia-agent-loop.php';
let isLoading = false;
let totalQueries = 0;
let totalCost = 0;
// Init: health check
async function init() {
try {
const r = await fetch(API + '?health');
const d = await r.json();
const models = d.ollama_models || 0;
const t1 = d.tier1_providers || 0;
const t2 = d.tier2_providers || 0;
const today = Object.values(d.stats || {})[0];
if (today && today.total) {
totalQueries = today.total;
}
document.getElementById('statusText').textContent =
`${models} local + ${t1+t2} cloud | ${totalQueries} req | 0€`;
document.getElementById('statusDot').style.background = 'var(--accent)';
} catch(e) {
document.getElementById('statusText').textContent = 'Offline';
document.getElementById('statusDot').style.background = 'var(--red)';
}
}
function handleKey(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
}
function autoResize(el) {
el.style.height = 'auto';
el.style.height = Math.min(el.scrollHeight, 120) + 'px';
}
function quickSend(text) {
document.getElementById('input').value = text;
sendMessage();
}
async function sendMessage() {
const input = document.getElementById('input');
const msg = input.value.trim();
if (!msg || isLoading) return;
input.value = '';
input.style.height = 'auto';
isLoading = true;
document.getElementById('sendBtn').disabled = true;
// Add user message
addMessage('user', msg);
// Show typing
const typing = document.getElementById('typing');
typing.classList.add('active');
scrollBottom();
const startTime = Date.now();
try {
const r = await fetch(API, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: msg })
});
const d = await r.json();
typing.classList.remove('active');
const content = d.content || 'Pas de réponse.';
const provider = d.provider || '?';
const model = d.model || '?';
const tier = d.tier ?? -1;
const latency = d.latency_ms || (Date.now() - startTime);
const routing = d.routing || {};
const ragCount = routing.rag?.results_count || 0;
const complexity = routing.complexity?.level || '?';
totalQueries++;
document.getElementById('statusText').textContent =
`${totalQueries} req | avg ${Math.round(latency)}ms | 0€`;
addMessage('ai', content, { provider, model, tier, latency, ragCount, complexity });
} catch(e) {
typing.classList.remove('active');
addMessage('ai', '⚠️ Erreur de connexion. Réessayez.', { provider: 'error', tier: -1, latency: Date.now()-startTime });
}
isLoading = false;
document.getElementById('sendBtn').disabled = false;
input.focus();
}
function addMessage(role, text, meta) {
const el = document.createElement('div');
el.className = `msg ${role}`;
// Format text (basic markdown)
let html = text
.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>')
.replace(/`([^`]+)`/g, '<code>$1</code>')
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
.replace(/\n/g, '<br>');
let metaHtml = '';
if (meta) {
const tierClass = meta.tier === 0 ? 'tier0' : meta.tier === 1 ? 'tier1' : 'tier2';
const tierLabel = meta.tier === 0 ? 'LOCAL' : meta.tier === 1 ? 'T1 FREE' : meta.tier === 2 ? 'T2 FREE' : 'ERR';
metaHtml = `<div class="msg-meta">
<span class="meta-tag ${tierClass}">${tierLabel}</span>
<span>${meta.provider}/${meta.model}</span>
<span>${meta.latency}ms</span>
${meta.ragCount > 0 ? `<span class="meta-tag rag-tag">RAG ×${meta.ragCount}</span>` : ''}
<span>0€</span>
</div>`;
}
el.innerHTML = `<div class="msg-bubble">${html}</div>${metaHtml}`;
document.getElementById('messages').appendChild(el);
scrollBottom();
}
function scrollBottom() {
const m = document.getElementById('messages');
setTimeout(() => m.scrollTop = m.scrollHeight, 50);
}
// Agents
function toggleAgents() {
const panel = document.getElementById('agentPanel');
const btn = document.getElementById('agentToggle');
panel.classList.toggle('visible');
btn.classList.toggle('active');
}
async function runAgent(agentId, goal) {
if (isLoading) return;
isLoading = true;
document.getElementById('sendBtn').disabled = true;
addMessage('user', `🤖 Agent ${agentId}: ${goal}`);
const typing = document.getElementById('typing');
typing.classList.add('active');
scrollBottom();
try {
const r = await fetch(`${AGENT_API}?agent=${agentId}&goal=${encodeURIComponent(goal)}`);
const d = await r.json();
typing.classList.remove('active');
const summary = d.summary || 'Agent terminé.';
const steps = d.steps_count || 0;
const time = d.total_time_ms || 0;
const status = d.status || '?';
let detail = `**Agent ${d.agent || agentId}** — ${steps} étapes en ${time}ms\n\n`;
detail += summary + '\n\n';
// Show step details
if (d.steps) {
for (const s of d.steps) {
const act = s.action || {};
if (act.cmd) detail += `\`${act.cmd}\`\n`;
if (s.result?.output) detail += `\`\`\`\n${s.result.output.substring(0, 300)}\n\`\`\`\n`;
}
}
addMessage('ai', detail, { provider: 'agent', model: agentId, tier: 1, latency: time, ragCount: 0 });
} catch(e) {
typing.classList.remove('active');
addMessage('ai', '⚠️ Agent timeout. Réessayez avec un objectif plus simple.', { provider: 'error', tier: -1, latency: 0 });
}
isLoading = false;
document.getElementById('sendBtn').disabled = false;
}
function openDashboard() {
window.open(API + '?dashboard', '_blank');
}
// Init
init();
document.getElementById('input').focus();
</script>
</body>
</html>