jarvis-local / designs /c-commandement.html
PascalF53's picture
Sauvegarde JARVIS Local + guide d'installation
3fa98f2 verified
Raw History Blame Contribute Delete
14.2 kB
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>JARVIS · Proposition C — Commandement</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=IBM+Plex+Sans+Condensed:wght@400;500;600&display=swap" rel="stylesheet">
<style>
/* Direction C — "Commandement" : console de SOC.
L'orbe devient un radar, la conversation un journal horodaté,
et on voit en permanence l'état de la machine (latences, VRAM, moteurs). */
:root {
--bg: #0a0d0d; --panel: #0f1414; --panel-2: #131a1a; --line: #1f2a29; --line-2: #2b3a38;
--green: #3df5a6; --green-d: #1c7a55; --amber: #ffc94a; --red: #ff5a5a; --blue: #58c6ff;
--text: #d6e4e1; --muted: #6b807c;
--mono: "IBM Plex Mono", monospace; --cond: "IBM Plex Sans Condensed", sans-serif;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; }
body {
background: var(--bg); color: var(--text); font-family: var(--cond); overflow: hidden;
display: grid; grid-template-rows: 44px 1fr 34px; grid-template-columns: 340px 1fr 340px;
grid-template-areas: "top top top" "log stage side" "bot bot bot"; gap: 1px; background-color: var(--line);
}
body > * { background: var(--bg); }
/* ---------- top status bar */
#top { grid-area: top; display: flex; align-items: center; gap: 26px; padding: 0 18px; font-family: var(--mono); font-size: 11.5px; color: var(--muted); }
#top .brand { color: var(--green); font-weight: 500; letter-spacing: .18em; }
#top .k b { color: var(--text); font-weight: 400; }
#top .k.ok b { color: var(--green); }
#top .sp { flex: 1; }
#top #clock { color: var(--amber); }
.ph { display: flex; justify-content: space-between; align-items: center; height: 34px; padding: 0 14px; border-bottom: 1px solid var(--line); font-family: var(--mono); font-size: 10.5px; letter-spacing: .16em; text-transform: uppercase; color: var(--muted); background: var(--panel); }
.ph b { color: var(--green); font-weight: 500; }
/* ---------- left: conversation journal */
#log { grid-area: log; display: flex; flex-direction: column; min-height: 0; }
#lines { flex: 1; overflow: hidden; padding: 12px 14px; display: flex; flex-direction: column; justify-content: flex-end; gap: 10px; font-size: 14px; line-height: 1.45; }
.ln { display: grid; grid-template-columns: 54px 1fr; gap: 8px; animation: fade .3s; }
.ln time { font-family: var(--mono); font-size: 11px; color: var(--muted); padding-top: 2px; }
.ln .who { font-family: var(--mono); font-size: 10.5px; letter-spacing: .12em; display: block; margin-bottom: 1px; }
.ln.you .who { color: var(--blue); } .ln.jv .who { color: var(--green); } .ln.sys .who { color: var(--amber); }
.ln.sys { color: var(--muted); font-size: 13px; }
.cursor::after { content: "▌"; color: var(--green); animation: blink .8s steps(1) infinite; }
@keyframes blink { 50% { opacity: 0; } }
@keyframes fade { from { opacity: 0; transform: translateY(6px); } }
/* ---------- centre: radar + oscilloscope */
#stage { grid-area: stage; position: relative; display: grid; grid-template-rows: 1fr 120px; min-height: 0; }
#radarBox { position: relative; display: grid; place-items: center; min-height: 0; }
#radar { width: min(100%, 64vh); aspect-ratio: 1; cursor: pointer; }
#mode { position: absolute; top: 14px; left: 16px; font-family: var(--mono); font-size: 11px; letter-spacing: .2em; }
#mode span { padding: 3px 8px; border: 1px solid var(--green-d); color: var(--green); }
#mode.thinking span { border-color: var(--amber); color: var(--amber); }
#scope { width: 100%; height: 120px; border-top: 1px solid var(--line); background: var(--panel); display: block; }
/* ---------- right: tasks + telemetry + findings */
#side { grid-area: side; display: flex; flex-direction: column; min-height: 0; overflow: hidden; }
table { width: 100%; border-collapse: collapse; font-size: 13px; }
td { padding: 8px 14px; border-bottom: 1px solid var(--line); vertical-align: top; }
td.st { font-family: var(--mono); font-size: 10.5px; width: 78px; }
.run { color: var(--amber); } .ok { color: var(--green); }
td small { display: block; font-family: var(--mono); font-size: 10.5px; color: var(--muted); margin-top: 2px; }
.meters { padding: 12px 14px; display: grid; gap: 10px; font-family: var(--mono); font-size: 11px; color: var(--muted); }
.meter { display: grid; grid-template-columns: 70px 1fr 54px; align-items: center; gap: 10px; }
.meter .b { height: 6px; background: var(--panel-2); border: 1px solid var(--line-2); }
.meter .b i { display: block; height: 100%; background: var(--green); transition: width .6s; }
.meter .v { color: var(--text); text-align: right; }
.finding { margin: 10px 14px; border: 1px solid var(--line-2); border-left: 3px solid var(--red); background: var(--panel); padding: 10px 12px; animation: fade .4s; }
.finding h4 { font-family: var(--mono); font-size: 10.5px; letter-spacing: .14em; color: var(--red); margin-bottom: 6px; font-weight: 500; }
.finding div { display: flex; justify-content: space-between; font-size: 13px; padding: 2px 0; }
.finding b { font-family: var(--mono); font-weight: 500; color: var(--amber); }
/* ---------- bottom */
#bot { grid-area: bot; display: flex; align-items: center; gap: 18px; padding: 0 18px; font-family: var(--mono); font-size: 11px; color: var(--muted); }
#bot kbd { border: 1px solid var(--line-2); padding: 1px 6px; color: var(--text); font: inherit; }
select { background: var(--panel); color: var(--text); border: 1px solid var(--line-2); padding: 3px 6px; font: inherit; }
#bot .sp { flex: 1; }
</style>
</head>
<body>
<div id="top">
<span class="brand">■ JARVIS // CONSOLE</span>
<span class="k ok">état <b>OPÉRATIONNEL</b></span>
<span class="k">llm <b>qwen3-30b-a3b</b></span>
<span class="k">agent <b>local + claude</b></span>
<span class="k">réseau <b>aucune sortie</b></span>
<span class="sp"></span>
<span id="clock">--:--:--</span>
</div>
<section id="log">
<div class="ph">journal de session <b id="cnt">0</b></div>
<div id="lines"></div>
</section>
<section id="stage">
<div id="radarBox">
<div id="mode"><span>VEILLE</span></div>
<canvas id="radar"></canvas>
</div>
<canvas id="scope"></canvas>
</section>
<aside id="side">
<div class="ph">tâches <b id="tc">0 actives</b></div>
<table><tbody id="tasks"></tbody></table>
<div class="ph">télémétrie</div>
<div class="meters">
<div class="meter"><span>oreille</span><div class="b"><i id="m1" style="width:12%"></i></div><span class="v" id="v1">0,18 s</span></div>
<div class="meter"><span>cerveau</span><div class="b"><i id="m2" style="width:30%"></i></div><span class="v" id="v2">0,31 s</span></div>
<div class="meter"><span>voix</span><div class="b"><i id="m3" style="width:18%"></i></div><span class="v" id="v3">0,22 s</span></div>
<div class="meter"><span>vram</span><div class="b"><i style="width:75%;background:var(--amber)"></i></div><span class="v">24/32 Go</span></div>
</div>
<div id="finds"></div>
</aside>
<div id="bot">
<select><option>MIC · Shure MV7</option></select>
<span><kbd>Espace</kbd> micro on/off</span>
<span><kbd>Échap</kbd> interrompre</span>
<span class="sp"></span>
<span>PROPOSITION C · COMMANDEMENT · démo</span>
</div>
<script>
"use strict";
const $ = id => document.getElementById(id);
const TAU = Math.PI * 2;
let mode = "idle", level = 0, target = 0, t = 0;
/* ------------------------------------------------ radar */
const rc = $("radar"), r = rc.getContext("2d");
const sc = $("scope"), s = sc.getContext("2d");
const fit = () => [rc, sc].forEach(c => { const b = c.getBoundingClientRect(); c.width = b.width * devicePixelRatio; c.height = b.height * devicePixelRatio; });
addEventListener("resize", fit); fit();
const blips = [];
const wave = new Array(240).fill(0);
function frame() {
requestAnimationFrame(frame); t += 1 / 60; level += (target - level) * .2;
const w = rc.width, h = rc.height, cx = w / 2, cy = h / 2, R = Math.min(w, h) * .44, d = devicePixelRatio;
const G = mode === "thinking" ? "255,201,74" : "61,245,166";
r.clearRect(0, 0, w, h);
// rings + crosshair
r.strokeStyle = "rgba(61,245,166,.16)"; r.lineWidth = 1 * d;
for (let i = 1; i <= 4; i++) { r.beginPath(); r.arc(cx, cy, R * i / 4, 0, TAU); r.stroke(); }
r.beginPath(); r.moveTo(cx - R, cy); r.lineTo(cx + R, cy); r.moveTo(cx, cy - R); r.lineTo(cx, cy + R); r.stroke();
// bearing ticks
for (let i = 0; i < 72; i++) {
const a = i / 72 * TAU, k = i % 6 === 0 ? .94 : .97;
r.beginPath(); r.moveTo(cx + R * k * Math.cos(a), cy + R * k * Math.sin(a)); r.lineTo(cx + R * Math.cos(a), cy + R * Math.sin(a)); r.stroke();
}
// sweep (faster while thinking)
const sweep = t * (mode === "thinking" ? 3.2 : 1.1);
const grad = r.createConicGradient ? r.createConicGradient(sweep - .9, cx, cy) : null;
if (grad) {
grad.addColorStop(0, `rgba(${G},0)`); grad.addColorStop(.14, `rgba(${G},.28)`); grad.addColorStop(.143, `rgba(${G},0)`);
r.fillStyle = grad; r.beginPath(); r.arc(cx, cy, R, 0, TAU); r.fill();
}
// voice = pulsing core rings
const core = R * (.16 + level * .12);
r.fillStyle = `rgba(${G},.12)`; r.beginPath(); r.arc(cx, cy, core * 1.8, 0, TAU); r.fill();
r.fillStyle = `rgba(${G},.9)`; r.beginPath(); r.arc(cx, cy, core * .55, 0, TAU); r.fill();
r.strokeStyle = `rgba(${G},.7)`; r.lineWidth = 1.5 * d; r.beginPath(); r.arc(cx, cy, core, 0, TAU); r.stroke();
// blips = running / finished tasks
blips.forEach(b => {
const x = cx + R * b.r * Math.cos(b.a), y = cy + R * b.r * Math.sin(b.a);
const age = ((sweep - b.a) % TAU + TAU) % TAU, glow = Math.max(.25, 1 - age / TAU);
r.fillStyle = b.done ? `rgba(61,245,166,${glow})` : `rgba(255,201,74,${glow})`;
r.beginPath(); r.arc(x, y, 5 * d, 0, TAU); r.fill();
r.font = `${11 * d}px IBM Plex Mono`; r.fillText(b.label, x + 10 * d, y + 4 * d);
});
// oscilloscope
wave.push(mode === "listening" || mode === "speaking" ? (Math.random() - .5) * level * 1.6 : (Math.random() - .5) * .03);
wave.shift();
const W = sc.width, H = sc.height;
s.clearRect(0, 0, W, H);
s.strokeStyle = "rgba(61,245,166,.08)"; s.lineWidth = 1;
for (let x = 0; x < W; x += 40 * d) { s.beginPath(); s.moveTo(x, 0); s.lineTo(x, H); s.stroke(); }
s.strokeStyle = mode === "listening" ? "#58c6ff" : `rgb(${G})`; s.lineWidth = 1.6 * d; s.beginPath();
wave.forEach((v, i) => { const x = i / (wave.length - 1) * W, y = H / 2 + v * H * .9; i ? s.lineTo(x, y) : s.moveTo(x, y); });
s.stroke();
}
frame();
/* ------------------------------------------------ demo */
setInterval(() => $("clock").textContent = new Date().toLocaleTimeString("fr-FR"), 1000);
setInterval(() => { target = mode === "listening" || mode === "speaking" ? .3 + Math.random() * .7 : mode === "thinking" ? .1 : 0; }, 80);
const sleep = ms => new Promise(res => setTimeout(res, ms));
const now = () => new Date().toLocaleTimeString("fr-FR");
const MODES = { idle: "VEILLE", listening: "ÉCOUTE", thinking: "ANALYSE", speaking: "RÉPONSE" };
function setMode(m) { mode = m; $("mode").className = m; $("mode").firstChild.textContent = MODES[m]; }
function line(kind, who, text = "") {
const el = document.createElement("div"); el.className = "ln " + kind;
el.innerHTML = `<time>${now().slice(0, 5)}</time><div><span class="who">${who}</span><span class="tx">${text}</span></div>`;
$("lines").append(el); while ($("lines").children.length > 14) $("lines").firstChild.remove();
$("cnt").textContent = +$("cnt").textContent + 1;
return el.querySelector(".tx");
}
async function type(el, text, speed = 26) { el.classList.add("cursor"); for (const ch of text) { el.textContent += ch; await sleep(speed); } el.classList.remove("cursor"); }
function meters(a, b, c) {
[[a, "m1", "v1"], [b, "m2", "v2"], [c, "m3", "v3"]].forEach(([v, m, l]) => { $(m).style.width = Math.min(100, v * 100) + "%"; $(l).textContent = v.toFixed(2).replace(".", ",") + " s"; });
}
let active = 0;
function task(title, engine) {
const tr = document.createElement("tr");
tr.innerHTML = `<td class="st run">● RUN</td><td>${title}<small>${engine} · ${now()}</small></td>`;
$("tasks").prepend(tr); active++; $("tc").textContent = active + " active" + (active > 1 ? "s" : "");
const b = { a: Math.random() * TAU, r: .5 + Math.random() * .35, label: title.split(" ")[0].toUpperCase(), done: false };
blips.push(b); if (blips.length > 4) blips.shift();
return () => { tr.firstChild.className = "st ok"; tr.firstChild.textContent = "✓ OK"; b.done = true; active--; $("tc").textContent = active + " actives"; };
}
async function demo() {
for (;;) {
setMode("idle"); await sleep(1600);
setMode("listening"); const u = line("you", "PASCAL"); await type(u, "Lance une veille sur les CVE critiques de la semaine.", 40);
setMode("thinking"); await sleep(600); meters(.18 + Math.random() * .1, .3 + Math.random() * .15, .2 + Math.random() * .08);
setMode("speaking"); const j = line("jv", "JARVIS"); await type(j, "Très bien, Pascal. Je lance la veille.");
const done = task("Veille CVE critiques", "local · qwen3");
line("sys", "SYSTÈME", "tâche lancée sur le modèle local");
setMode("idle"); await sleep(4200);
done(); line("sys", "SYSTÈME", "tâche terminée · notification envoyée");
setMode("speaking"); const j2 = line("jv", "JARVIS"); await type(j2, "Trois vulnérabilités critiques, dont une activement exploitée.");
const f = document.createElement("div"); f.className = "finding";
f.innerHTML = `<h4>▲ VEILLE CVE · SEMAINE 40</h4><div><span>Exemple · passerelle VPN</span><b>9.8</b></div><div><span>Exemple · messagerie</span><b>9.1</b></div><div><span>Exemple · Kubernetes</span><b>8.8</b></div>`;
$("finds").prepend(f); while ($("finds").children.length > 2) $("finds").lastChild.remove();
await sleep(2600);
}
}
demo();
</script>
</body>
</html>