File size: 14,169 Bytes
3fa98f2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
<!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>