jarvis-local / designs /a-reacteur.html
PascalF53's picture
Sauvegarde JARVIS Local + guide d'installation
3fa98f2 verified
Raw History Blame Contribute Delete
13.9 kB
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>JARVIS · Proposition A — Réacteur</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Rajdhani:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
/* Direction A — "Réacteur" : l'esprit Iron Man, épuré.
Une seule couleur vive (cyan) + un accent ambre réservé aux alertes,
l'orbe au centre, tout le reste orbite autour en anneaux discrets. */
:root {
--bg: #03070d; --bg2: #071321;
--cyan: #5ee6ff; --cyan-2: #2aa8cc; --cyan-dim: rgba(94,230,255,.14);
--amber: #ffb547; --ok: #5dffb0; --err: #ff5f7a;
--text: #d8f4ff; --muted: #5f8aa3; --line: rgba(94,230,255,.16);
--hud: "Rajdhani", sans-serif; --mono: "JetBrains Mono", monospace;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; }
body {
background: radial-gradient(900px 700px at 50% 45%, var(--bg2), var(--bg) 70%);
color: var(--text); font-family: var(--hud); overflow: hidden;
}
/* fine concentric guides behind everything */
body::before {
content: ""; position: fixed; inset: 0; pointer-events: none;
background: repeating-radial-gradient(circle at 50% 45%, transparent 0 79px, rgba(94,230,255,.035) 80px);
}
/* ---------- top bar */
header {
position: fixed; top: 0; left: 0; right: 0; height: 56px; z-index: 5;
display: flex; align-items: center; gap: 22px; padding: 0 28px;
font-family: var(--mono); font-size: 11px; letter-spacing: .22em; color: var(--muted);
text-transform: uppercase; border-bottom: 1px solid var(--line);
background: linear-gradient(180deg, rgba(3,7,13,.9), transparent);
}
header .brand { color: var(--cyan); font-weight: 500; letter-spacing: .4em; }
header .brand b { color: var(--text); font-weight: 500; }
header .sep { flex: 1; }
header .pill { display: flex; align-items: center; gap: 7px; }
header .pill i { width: 6px; height: 6px; border-radius: 50%; background: var(--ok); box-shadow: 0 0 8px var(--ok); }
#clock { color: var(--text); }
/* ---------- stage */
main { position: fixed; inset: 56px 0 0 0; display: grid; place-items: center; }
#orbWrap { position: relative; width: min(62vh, 560px); aspect-ratio: 1; }
#orb { width: 100%; height: 100%; display: block; cursor: pointer; }
#state {
position: absolute; left: 50%; bottom: -6px; transform: translateX(-50%);
font-family: var(--mono); font-size: 11px; letter-spacing: .5em; text-transform: uppercase;
color: var(--cyan); text-shadow: 0 0 14px rgba(94,230,255,.7); white-space: nowrap;
}
#state small { color: var(--muted); letter-spacing: .2em; margin-left: 10px; }
/* subtitles: user line (dim) above, JARVIS line (bright) below */
#subs { position: absolute; left: 50%; bottom: 7vh; transform: translateX(-50%); width: min(820px, 80vw); text-align: center; }
#you { font-size: 15px; color: var(--muted); min-height: 22px; font-style: italic; }
#you:not(:empty)::before { content: "VOUS › "; font-family: var(--mono); font-style: normal; font-size: 10px; letter-spacing: .25em; color: var(--cyan-2); }
#say { margin-top: 8px; font-size: 22px; font-weight: 500; line-height: 1.35; min-height: 30px; text-shadow: 0 0 22px rgba(94,230,255,.25); }
/* ---------- left: holo cards */
#cards { position: fixed; left: 28px; top: 84px; width: 320px; display: flex; flex-direction: column; gap: 12px; z-index: 4; }
.card {
position: relative; padding: 14px 16px 14px 18px;
background: linear-gradient(120deg, rgba(94,230,255,.08), rgba(7,19,33,.85) 50%);
border: 1px solid var(--line); backdrop-filter: blur(8px);
clip-path: polygon(0 0, calc(100% - 14px) 0, 100% 14px, 100% 100%, 14px 100%, 0 calc(100% - 14px));
animation: in .5s cubic-bezier(.2,.9,.25,1);
}
.card::before { content: ""; position: absolute; left: 0; top: 14px; bottom: 14px; width: 2px; background: var(--cyan); box-shadow: 0 0 10px var(--cyan); }
.card.result::before { background: var(--ok); box-shadow: 0 0 10px var(--ok); }
.card h3 { font-family: var(--mono); font-size: 10px; letter-spacing: .3em; text-transform: uppercase; color: var(--cyan); margin-bottom: 8px; font-weight: 500; }
.card.result h3 { color: var(--ok); }
.card p, .card li { font-size: 14.5px; line-height: 1.5; }
.card ul { list-style: none; }
.card li { display: flex; justify-content: space-between; border-bottom: 1px dashed var(--line); padding: 3px 0; }
.card li b { font-family: var(--mono); font-weight: 500; color: var(--amber); font-size: 13px; }
@keyframes in { from { opacity: 0; transform: translateX(-30px); filter: brightness(2.5); } }
/* ---------- right: tasks as a vertical rail */
#rail { position: fixed; right: 28px; top: 84px; width: 300px; z-index: 4; }
#rail h2 { font-family: var(--mono); font-size: 10px; letter-spacing: .32em; color: var(--muted); text-transform: uppercase; margin-bottom: 14px; display: flex; justify-content: space-between; }
.task { position: relative; padding: 0 0 18px 22px; border-left: 1px solid var(--line); margin-left: 5px; animation: in .4s ease; }
.task::before { content: ""; position: absolute; left: -6px; top: 2px; width: 11px; height: 11px; border-radius: 50%; border: 2px solid var(--cyan); background: var(--bg); box-shadow: 0 0 10px var(--cyan); animation: pulse 1.2s infinite alternate; }
.task.done::before { border-color: var(--ok); background: var(--ok); box-shadow: 0 0 10px var(--ok); animation: none; }
.task .t { font-size: 15px; font-weight: 600; }
.task .m { font-family: var(--mono); font-size: 10.5px; color: var(--muted); margin-top: 3px; letter-spacing: .08em; }
.task .m .eng { color: var(--amber); }
.task .bar { height: 2px; background: var(--line); margin-top: 8px; overflow: hidden; }
.task .bar i { display: block; height: 100%; width: 35%; background: var(--cyan); animation: scan 1.4s linear infinite; }
.task.done .bar i { width: 100%; background: var(--ok); animation: none; }
@keyframes pulse { to { box-shadow: 0 0 2px var(--cyan); } }
@keyframes scan { from { transform: translateX(-100%); } to { transform: translateX(300%); } }
/* ---------- bottom-left: mic + telemetry */
footer {
position: fixed; left: 28px; bottom: 22px; z-index: 5; display: flex; gap: 18px; align-items: center;
font-family: var(--mono); font-size: 10.5px; color: var(--muted); letter-spacing: .12em;
}
select { background: transparent; color: var(--muted); border: 1px solid var(--line); padding: 5px 8px; font: inherit; }
footer b { color: var(--text); font-weight: 400; }
.badge { position: fixed; right: 28px; bottom: 22px; font-family: var(--mono); font-size: 10px; letter-spacing: .25em; color: var(--muted); }
</style>
</head>
<body>
<header>
<span class="brand">J.A.R.V.I.S. <b>· MK VIII</b></span>
<span class="pill"><i></i> 100 % local</span>
<span class="pill">qwen3 · whisper · kokoro</span>
<span class="sep"></span>
<span id="clock">--:--:--</span>
</header>
<main>
<div id="orbWrap">
<canvas id="orb"></canvas>
<div id="state">en veille</div>
</div>
<div id="subs"><div id="you"></div><div id="say"></div></div>
</main>
<section id="cards"></section>
<aside id="rail"><h2><span>Tâches</span><span id="count">0</span></h2><div id="tasks"></div></aside>
<footer>
<select><option>🎙 Micro — Shure MV7</option></select>
<span>latence <b id="lat">0,74 s</b></span>
<span>vram <b>24,1 / 32 Go</b></span>
</footer>
<div class="badge">PROPOSITION A · RÉACTEUR · démo</div>
<script>
"use strict";
/* ------------------------------------------------ orb: layered arc reactor */
const cv = document.getElementById("orb"), cx2 = cv.getContext("2d");
const fit = () => { const r = cv.getBoundingClientRect(); cv.width = r.width * devicePixelRatio; cv.height = r.height * devicePixelRatio; };
addEventListener("resize", fit); fit();
let level = 0, target = 0, mode = "idle", t = 0;
const TAU = Math.PI * 2;
const COL = { idle: [94,230,255], listening: [94,230,255], thinking: [255,181,71], speaking: [93,255,176] };
let col = [...COL.idle];
function draw() {
requestAnimationFrame(draw); t += 1 / 60;
level += (target - level) * 0.18;
col = col.map((c, i) => c + (COL[mode][i] - c) * 0.06);
const C = col.map(Math.round).join(",");
const w = cv.width, h = cv.height, cx = w / 2, cy = h / 2, d = devicePixelRatio;
const R = Math.min(w, h) * 0.17 * (1 + level * 0.12);
cx2.clearRect(0, 0, w, h);
// halo
let g = cx2.createRadialGradient(cx, cy, R * .3, cx, cy, R * 2.9);
g.addColorStop(0, `rgba(${C},${.22 + level * .25})`); g.addColorStop(1, "transparent");
cx2.fillStyle = g; cx2.fillRect(0, 0, w, h);
// radial spectrum (the voice made visible)
const bars = 96;
for (let i = 0; i < bars; i++) {
const a = i / bars * TAU - Math.PI / 2;
const n = Math.abs(Math.sin(i * 1.7 + t * 5) * Math.sin(i * .31 - t * 3));
const len = R * (.06 + level * .55 * n + (mode === "thinking" ? .05 * Math.sin(t * 6 + i / 4) ** 2 : 0));
const r0 = R * 1.32;
cx2.strokeStyle = `rgba(${C},${.35 + n * .5})`; cx2.lineWidth = 2.2 * d;
cx2.beginPath(); cx2.moveTo(cx + r0 * Math.cos(a), cy + r0 * Math.sin(a));
cx2.lineTo(cx + (r0 + len) * Math.cos(a), cy + (r0 + len) * Math.sin(a)); cx2.stroke();
}
// segmented coil ring
cx2.save(); cx2.translate(cx, cy); cx2.rotate(t * (mode === "thinking" ? .9 : .18));
for (let i = 0; i < 10; i++) {
const a0 = i / 10 * TAU;
cx2.strokeStyle = `rgba(${C},.85)`; cx2.lineWidth = 6 * d;
cx2.beginPath(); cx2.arc(0, 0, R * 1.16, a0, a0 + TAU / 10 * .7); cx2.stroke();
}
cx2.restore();
// outer graduated ring + rotating reticle
cx2.save(); cx2.translate(cx, cy); cx2.rotate(-t * .07);
for (let i = 0; i < 120; i++) {
const a = i / 120 * TAU, big = i % 10 === 0;
cx2.strokeStyle = big ? "rgba(255,181,71,.8)" : `rgba(${C},.22)`; cx2.lineWidth = (big ? 2 : 1) * d;
const r0 = R * 2.05, r1 = R * (big ? 2.18 : 2.11);
cx2.beginPath(); cx2.moveTo(r0 * Math.cos(a), r0 * Math.sin(a)); cx2.lineTo(r1 * Math.cos(a), r1 * Math.sin(a)); cx2.stroke();
}
cx2.restore();
[[1.9, .35, 1], [2.32, .12, -1.6]].forEach(([k, span, sp]) => {
cx2.save(); cx2.translate(cx, cy); cx2.rotate(t * sp * .4);
cx2.strokeStyle = `rgba(${C},.6)`; cx2.lineWidth = 1.6 * d;
cx2.beginPath(); cx2.arc(0, 0, R * k, 0, TAU * span); cx2.stroke(); cx2.restore();
});
// core
g = cx2.createRadialGradient(cx, cy, 0, cx, cy, R);
g.addColorStop(0, "rgba(245,255,255,1)"); g.addColorStop(.35, `rgba(${C},.95)`); g.addColorStop(1, "rgba(5,25,45,.95)");
cx2.fillStyle = g; cx2.beginPath(); cx2.arc(cx, cy, R * .95, 0, TAU); cx2.fill();
// triangle (MK signature)
cx2.save(); cx2.translate(cx, cy); cx2.rotate(-t * .05);
cx2.strokeStyle = "rgba(255,255,255,.55)"; cx2.lineWidth = 1.5 * d; cx2.beginPath();
for (let i = 0; i <= 3; i++) { const a = i / 3 * TAU - Math.PI / 2; const p = [R * .62 * Math.cos(a), R * .62 * Math.sin(a)]; i ? cx2.lineTo(...p) : cx2.moveTo(...p); }
cx2.stroke(); cx2.restore();
}
draw();
/* ------------------------------------------------ demo conversation */
const $ = id => document.getElementById(id);
setInterval(() => $("clock").textContent = new Date().toLocaleTimeString("fr-FR"), 1000);
const LABEL = { idle: "en veille", listening: "écoute", thinking: "réflexion", speaking: "jarvis parle" };
function setMode(m, sub = "") { mode = m; $("state").innerHTML = LABEL[m] + (sub ? `<small>${sub}</small>` : ""); }
const sleep = ms => new Promise(r => setTimeout(r, ms));
async function type(el, text, speed = 28) { el.textContent = ""; for (const ch of text) { el.textContent += ch; await sleep(speed); } }
setInterval(() => { target = mode === "listening" || mode === "speaking" ? .35 + Math.random() * .65 : mode === "thinking" ? .12 : 0; }, 90);
function card(title, html, kind = "") {
const c = document.createElement("div"); c.className = "card " + kind;
c.innerHTML = `<h3>${title}</h3>${html}`; $("cards").prepend(c);
while ($("cards").children.length > 3) $("cards").lastChild.remove();
}
function task(title, engine) {
const el = document.createElement("div"); el.className = "task";
el.innerHTML = `<div class="t">${title}</div><div class="m"><span class="eng">${engine}</span> · <span class="s">en cours</span> · <span class="c">0 s</span></div><div class="bar"><i></i></div>`;
$("tasks").prepend(el); $("count").textContent = $("tasks").children.length;
const t0 = Date.now(), iv = setInterval(() => el.querySelector(".c").textContent = Math.round((Date.now() - t0) / 1000) + " s", 1000);
return () => { clearInterval(iv); el.classList.add("done"); el.querySelector(".s").textContent = "terminé"; };
}
async function demo() {
for (;;) {
setMode("idle"); $("you").textContent = ""; $("say").textContent = ""; await sleep(1800);
setMode("listening"); await type($("you"), "Lance une veille sur les CVE critiques de la semaine.", 45);
setMode("thinking", "0,2 s"); await sleep(700);
setMode("speaking"); await type($("say"), "Très bien, Pascal. Je lance la veille, je vous préviens dès qu'elle est prête.");
const done = task("Veille CVE critiques", "local");
await sleep(1400); setMode("idle"); await sleep(3200);
done();
setMode("speaking"); $("you").textContent = "";
await type($("say"), "La veille est prête : trois vulnérabilités critiques, dont une activement exploitée.");
card("Veille CVE · semaine 40", `<ul><li>Exemple · passerelle VPN <b>9.8</b></li><li>Exemple · messagerie <b>9.1</b></li><li>Exemple · Kubernetes <b>8.8</b></li></ul>`, "result");
await sleep(2600);
}
}
demo();
</script>
</body>
</html>