574 lines
15 KiB
HTML
574 lines
15 KiB
HTML
<!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>
|