10 more alive-feeling effects: HUD corners, data stream, topology, boot seq, vignette, EKG, audio ring, typewriter, static burst, color cycle

① HUD corner rings: animated scanning arcs in all 4 screen corners with tick marks, edge lines, moving dot
② Data stream columns: 22 subtle falling hex/glyph columns behind everything (Matrix-adjacent, very low opacity)
③ Network topology canvas: live node constellation above the network list with pulsing travel dots on connections
④ HUD boot sequence: topbar/panels slide in from edges with staggered timing on every login
⑤ Breathing vignette: screen edges slowly pulse + shifts to red when alerts are active
⑥ EKG heartbeat: scrolling ECG waveform in bottom bar (P-QRS-T complex, green glow)
⑦ Audio waveform ring: animated bar ring around the arc reactor during mic/TTS activity
⑧ Typewriter: JARVIS chat responses type out character by character with adaptive speed + cursor blink
⑨ Static noise burst: random panel gets 280ms noise overlay every 75-130 seconds
⑩ Ambient color cycle: --cyan slowly drifts between blue-cyan and green-cyan over 70s loop
This commit is contained in:
2026-06-02 00:27:27 +00:00
parent c34d497e9d
commit c8e002091e
+381 -7
View File
@@ -759,11 +759,60 @@ body::after{
.text-orange{color:var(--orange)} .text-orange{color:var(--orange)}
.text-red{color:var(--red)} .text-red{color:var(--red)}
.text-dim{color:var(--text-dim)} .text-dim{color:var(--text-dim)}
/* ① HUD CORNER RINGS */
#hudCornersCanvas{position:fixed;inset:0;z-index:3;pointer-events:none}
/* ② DATA STREAM COLUMNS */
#dataStreamCanvas{position:fixed;inset:0;z-index:0;pointer-events:none}
/* ③ NETWORK TOPOLOGY */
#topoCanvas{display:block;width:100%;flex-shrink:0;cursor:default;border-bottom:1px solid var(--panel-border);margin-bottom:6px}
/* ④ BOOT SEQUENCE */
@keyframes bootLeft{0%{opacity:0;transform:translateX(-70px)}100%{opacity:1;transform:none}}
@keyframes bootRight{0%{opacity:0;transform:translateX(70px)}100%{opacity:1;transform:none}}
@keyframes bootDown{0%{opacity:0;transform:translateY(-18px)}100%{opacity:1;transform:none}}
@keyframes bootCenter{0%{opacity:0;transform:scale(0.94) translateY(14px)}100%{opacity:1;transform:none}}
.boot-left{animation:bootLeft 0.55s cubic-bezier(0.4,0,0.2,1) both}
.boot-right{animation:bootRight 0.55s cubic-bezier(0.4,0,0.2,1) both}
.boot-top{animation:bootDown 0.4s ease both}
.boot-center{animation:bootCenter 0.65s cubic-bezier(0.4,0,0.2,1) both}
/* ⑤ BREATHING EDGE VIGNETTE */
#vignetteOverlay{position:fixed;inset:0;pointer-events:none;z-index:1;
background:radial-gradient(ellipse at 50% 50%,transparent 32%,rgba(0,2,18,0.6) 100%);
animation:vignettePulse 5s ease-in-out infinite}
#vignetteOverlay.alert-vignette{background:radial-gradient(ellipse at 50% 50%,transparent 32%,rgba(20,0,8,0.65) 100%)}
@keyframes vignettePulse{0%,100%{opacity:0.75}50%{opacity:1}}
/* ⑥ EKG HEARTBEAT */
#ekgWrap{flex:1;max-width:180px;display:flex;align-items:center;overflow:hidden}
#ekgCanvas{display:block;width:100%;height:22px;opacity:0.8}
/* ⑦ AUDIO RING */
.tb-reactor{position:relative}
#audioRingCanvas{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);
width:60px;height:60px;pointer-events:none;z-index:4}
/* ⑧ TYPEWRITER CURSOR */
@keyframes cursorBlink{0%,100%{opacity:1}49%{opacity:1}50%,99%{opacity:0}}
.type-cursor{display:inline-block;width:6px;height:0.82em;background:var(--cyan);margin-left:1px;
vertical-align:text-bottom;animation:cursorBlink 0.7s step-end infinite}
/* ⑨ STATIC NOISE BURST */
@keyframes staticBurst{0%{opacity:0}10%{opacity:1}90%{opacity:1}100%{opacity:0}}
.panel-noise-layer{position:absolute;inset:0;pointer-events:none;z-index:20;border-radius:var(--r);
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='4'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.1'/%3E%3C/svg%3E");
background-size:100% 100%;mix-blend-mode:screen;animation:staticBurst 0.28s ease forwards}
</style> </style>
</head> </head>
<body> <body>
<canvas id="particleCanvas"></canvas> <canvas id="particleCanvas"></canvas>
<canvas id="dataStreamCanvas"></canvas>
<canvas id="hudCornersCanvas"></canvas>
<div id="alertOverlay"></div> <div id="alertOverlay"></div>
<div id="vignetteOverlay"></div>
<div id="faceScanOverlay"> <div id="faceScanOverlay">
<div class="fso-ring"></div> <div class="fso-ring"></div>
<div class="fso-tr"></div> <div class="fso-tr"></div>
@@ -797,7 +846,7 @@ body::after{
<!-- Top Bar --> <!-- Top Bar -->
<div id="topBar"> <div id="topBar">
<div class="tb-logo"> <div class="tb-logo">
<div class="tb-reactor"><div class="tbr-ring tbr-r1"></div><div class="tbr-ring tbr-r2"></div><div class="tbr-core"></div></div> <div class="tb-reactor"><div class="tbr-ring tbr-r1"></div><div class="tbr-ring tbr-r2"></div><div class="tbr-core"></div><canvas id="audioRingCanvas" width="60" height="60"></canvas></div>
<span class="tb-logo-text" data-text="JARVIS SYSTEM">JARVIS SYSTEM</span> <span class="tb-logo-text" data-text="JARVIS SYSTEM">JARVIS SYSTEM</span>
</div> </div>
<div class="tb-center"> <div class="tb-center">
@@ -940,6 +989,7 @@ body::after{
<!-- Network Status --> <!-- Network Status -->
<div class="panel" style="flex:0 1 auto;max-height:35%;display:flex;flex-direction:column;min-height:100px"> <div class="panel" style="flex:0 1 auto;max-height:35%;display:flex;flex-direction:column;min-height:100px">
<div class="panel-title">NETWORK STATUS <div class="indicator"></div><span id="net-agent-count" style="font-size:0.6rem;color:var(--cyan);margin-left:auto"></span><button onclick="addNetworkDevice()" title="Add device" style="background:none;border:none;color:var(--cyan);cursor:pointer;font-size:1rem;padding:0 4px;margin-left:4px;line-height:1">+</button></div> <div class="panel-title">NETWORK STATUS <div class="indicator"></div><span id="net-agent-count" style="font-size:0.6rem;color:var(--cyan);margin-left:auto"></span><button onclick="addNetworkDevice()" title="Add device" style="background:none;border:none;color:var(--cyan);cursor:pointer;font-size:1rem;padding:0 4px;margin-left:4px;line-height:1">+</button></div>
<canvas id="topoCanvas" height="100"></canvas>
<div id="network-list" style="overflow-y:auto;flex:1;padding-right:2px"> <div id="network-list" style="overflow-y:auto;flex:1;padding-right:2px">
<div class="loading-shimmer" style="margin-bottom:6px"></div> <div class="loading-shimmer" style="margin-bottom:6px"></div>
<div class="loading-shimmer" style="margin-bottom:6px"></div> <div class="loading-shimmer" style="margin-bottom:6px"></div>
@@ -1001,7 +1051,8 @@ body::after{
<span>AGENTS</span> <span id="bb-agent-status">--</span> <span>AGENTS</span> <span id="bb-agent-status">--</span>
</div> </div>
<div style="margin-left:auto;font-size:0.65rem"> <div id="ekgWrap" style="margin-left:auto"><canvas id="ekgCanvas"></canvas></div>
<div style="font-size:0.65rem;flex-shrink:0">
JARVIS v2.0 · SECURITY LEVEL ALPHA · UPDATED <span id="last-refresh">--:--:--</span> JARVIS v2.0 · SECURITY LEVEL ALPHA · UPDATED <span id="last-refresh">--:--:--</span>
</div> </div>
</div> </div>
@@ -1218,8 +1269,9 @@ function flashPanel(panelEl) {
// ── ALERT PULSE ─────────────────────────────────────────────────────── // ── ALERT PULSE ───────────────────────────────────────────────────────
function setAlertState(hasAlerts) { function setAlertState(hasAlerts) {
const ov = document.getElementById('alertOverlay'); const ov = document.getElementById('alertOverlay');
if (!ov) return; if (ov) ov.style.display = hasAlerts ? 'block' : 'none';
ov.style.display = hasAlerts ? 'block' : 'none'; const vg = document.getElementById('vignetteOverlay');
if (vg) vg.classList.toggle('alert-vignette', hasAlerts);
} }
// ── FACE TRACKING — reactor follows face position ───────────────────── // ── FACE TRACKING — reactor follows face position ─────────────────────
@@ -1306,13 +1358,299 @@ function stopFaceTracking() {
if (!el) return; if (!el) return;
el.classList.add('glitching'); el.classList.add('glitching');
setTimeout(() => el.classList.remove('glitching'), 280); setTimeout(() => el.classList.remove('glitching'), 280);
// Schedule next glitch: 35-60s random interval
setTimeout(triggerGlitch, 35000 + Math.random() * 25000); setTimeout(triggerGlitch, 35000 + Math.random() * 25000);
} }
// First glitch after 20s
setTimeout(triggerGlitch, 20000); setTimeout(triggerGlitch, 20000);
})(); })();
// ① HUD CORNER RINGS ──────────────────────────────────────────────────
(function initHudCorners() {
const canvas = document.getElementById('hudCornersCanvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
let W, H, t = 0;
function resize() { W = canvas.width = window.innerWidth; H = canvas.height = window.innerHeight; }
function drawCorner(cx, cy, a0, a1) {
const R = 62, R2 = R + 16;
// Edge lines
ctx.strokeStyle = 'rgba(0,212,255,0.35)'; ctx.lineWidth = 1;
const edgeLen = 28;
// two short lines along the screen edges from the corner point
const midA = (a0 + a1) / 2;
const ax = Math.cos(a0), ay = Math.sin(a0);
const bx = Math.cos(a1), by = Math.sin(a1);
ctx.beginPath(); ctx.moveTo(cx + ax*(R+6), cy + ay*(R+6)); ctx.lineTo(cx + ax*(R+6+edgeLen), cy + ay*(R+6+edgeLen)); ctx.stroke();
ctx.beginPath(); ctx.moveTo(cx + bx*(R+6), cy + by*(R+6)); ctx.lineTo(cx + bx*(R+6+edgeLen), cy + by*(R+6+edgeLen)); ctx.stroke();
// Primary arc
ctx.beginPath(); ctx.arc(cx, cy, R, a0, a1);
ctx.strokeStyle = 'rgba(0,212,255,0.5)'; ctx.lineWidth = 1.2; ctx.stroke();
// Outer arc
ctx.beginPath(); ctx.arc(cx, cy, R2, a0 + 0.12, a1 - 0.12);
ctx.strokeStyle = 'rgba(0,212,255,0.18)'; ctx.lineWidth = 0.6; ctx.stroke();
// Tick marks
const ticks = 14;
for (let i = 0; i <= ticks; i++) {
const a = a0 + (a1 - a0) * (i / ticks);
const big = i % 7 === 0;
const len = big ? 9 : (i % 2 === 0 ? 5 : 3);
ctx.beginPath();
ctx.moveTo(cx + Math.cos(a) * (R - 2), cy + Math.sin(a) * (R - 2));
ctx.lineTo(cx + Math.cos(a) * (R - 2 - len), cy + Math.sin(a) * (R - 2 - len));
ctx.strokeStyle = big ? 'rgba(0,212,255,0.8)' : 'rgba(0,212,255,0.3)';
ctx.lineWidth = big ? 1 : 0.5; ctx.stroke();
}
// Animated scanning dot
const dotA = a0 + ((a1 - a0) * ((t * 0.35) % 1));
ctx.beginPath(); ctx.arc(cx + Math.cos(dotA) * R, cy + Math.sin(dotA) * R, 2.5, 0, Math.PI*2);
ctx.fillStyle = 'rgba(0,212,255,1)';
ctx.shadowColor = 'rgba(0,212,255,0.9)'; ctx.shadowBlur = 8; ctx.fill(); ctx.shadowBlur = 0;
// Small numeric labels
ctx.font = '7px Share Tech Mono,monospace'; ctx.fillStyle = 'rgba(0,212,255,0.55)';
ctx.fillText(Math.round((Math.abs(a0) / (Math.PI*2)) * 360) + '°',
cx + Math.cos(a0) * (R + 20), cy + Math.sin(a0) * (R + 20));
}
function draw() {
ctx.clearRect(0, 0, W, H); t += 0.01;
drawCorner(0, 0, 0, Math.PI*0.5);
drawCorner(W, 0, Math.PI*0.5, Math.PI);
drawCorner(0, H, Math.PI*1.5, Math.PI*2);
drawCorner(W, H, Math.PI, Math.PI*1.5);
requestAnimationFrame(draw);
}
resize(); draw();
window.addEventListener('resize', resize);
})();
// ② DATA STREAM COLUMNS ───────────────────────────────────────────────
(function initDataStream() {
const canvas = document.getElementById('dataStreamCanvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
let W, H;
const CHARS = '0123456789ABCDEF◈◉▸▹⟩⟨⬡░▒';
const COL_COUNT = 22;
let cols = [];
function resize() {
W = canvas.width = window.innerWidth;
H = canvas.height = window.innerHeight;
cols = [];
for (let i = 0; i < COL_COUNT; i++) {
const x = (i / COL_COUNT) * W + Math.random() * (W / COL_COUNT) * 0.6;
cols.push({ x, y: Math.random() * H, speed: Math.random() * 0.7 + 0.25,
len: Math.floor(Math.random() * 14 + 5), chars: [],
alpha: Math.random() * 0.035 + 0.015, tick: 0 });
}
}
function draw() {
ctx.clearRect(0, 0, W, H);
ctx.font = '11px Share Tech Mono,monospace';
for (const c of cols) {
c.y += c.speed;
if (c.y - c.len * 14 > H) { c.y = -c.len * 14; c.alpha = Math.random() * 0.035 + 0.015; }
if (++c.tick > 7) { c.tick = 0; c.chars[0] = CHARS[Math.floor(Math.random() * CHARS.length)]; }
for (let i = 0; i < c.len; i++) {
if (!c.chars[i]) c.chars[i] = CHARS[Math.floor(Math.random() * CHARS.length)];
const cy = c.y - i * 14;
if (cy < -14 || cy > H + 14) continue;
const a = c.alpha * (1 - i / c.len);
ctx.fillStyle = i === 0 ? `rgba(180,240,255,${Math.min(a*4,0.15)})` : `rgba(0,185,225,${a})`;
ctx.fillText(c.chars[i], c.x, cy);
}
}
requestAnimationFrame(draw);
}
resize(); draw();
window.addEventListener('resize', resize);
})();
// ③ NETWORK TOPOLOGY ──────────────────────────────────────────────────
let _topoNodes = [], _topoT = 0, _topoRunning = false;
function renderTopology(devices) {
const canvas = document.getElementById('topoCanvas');
if (!canvas) return;
const W = canvas.parentElement?.clientWidth || 260;
canvas.width = W; canvas.height = 100;
_topoNodes = devices.slice(0, 18).map((d, i, arr) => {
const angle = (i / arr.length) * Math.PI * 2 - Math.PI / 2;
const rx = W * 0.36, ry = 36;
return {
x: W/2 + Math.cos(angle) * rx * (0.6 + (i%3)*0.18),
y: 50 + Math.sin(angle) * ry * (0.7 + (i%2)*0.3),
label: (d.name || d.ip || '?').split('.')[0].substring(0, 9),
on: !!(d.alive || d.status === 'online'),
agent: d.source === 'agent',
phase: Math.random() * Math.PI * 2,
};
});
if (!_topoRunning) { _topoRunning = true; _drawTopo(); }
}
function _drawTopo() {
requestAnimationFrame(_drawTopo);
const canvas = document.getElementById('topoCanvas');
if (!canvas || !_topoNodes.length) return;
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
_topoT += 0.018;
ctx.clearRect(0, 0, W, H);
// Connections
for (let i = 0; i < _topoNodes.length; i++) {
for (let j = i+1; j < _topoNodes.length; j++) {
const a = _topoNodes[i], b = _topoNodes[j];
if (!a.on || !b.on) continue;
const dx = b.x-a.x, dy = b.y-a.y, dist = Math.hypot(dx,dy);
if (dist > W * 0.55) continue;
ctx.beginPath(); ctx.moveTo(a.x,a.y); ctx.lineTo(b.x,b.y);
ctx.strokeStyle = 'rgba(0,180,220,0.13)'; ctx.lineWidth = 0.5; ctx.stroke();
// travelling pulse
const p = (_topoT * 0.4 + a.phase) % 1;
ctx.beginPath(); ctx.arc(a.x+dx*p, a.y+dy*p, 1.5, 0, Math.PI*2);
ctx.fillStyle = 'rgba(0,212,255,0.7)'; ctx.fill();
}
}
// Nodes
for (const n of _topoNodes) {
const pulse = 0.5 + Math.sin(_topoT*1.8 + n.phase) * 0.3;
const col = n.on ? (n.agent ? '0,255,136' : '0,212,255') : '255,50,80';
const a = n.on ? pulse * 0.7 : 0.2;
if (n.on) {
const g = ctx.createRadialGradient(n.x,n.y,0,n.x,n.y,9);
g.addColorStop(0,`rgba(${col},${(a*0.5).toFixed(2)})`); g.addColorStop(1,`rgba(${col},0)`);
ctx.beginPath(); ctx.arc(n.x,n.y,9,0,Math.PI*2); ctx.fillStyle=g; ctx.fill();
}
ctx.beginPath(); ctx.arc(n.x,n.y, n.agent ? 4 : 3, 0, Math.PI*2);
ctx.fillStyle = `rgba(${col},${a.toFixed(2)})`; ctx.fill();
ctx.font = '6.5px Share Tech Mono,monospace';
ctx.fillStyle = n.on ? 'rgba(180,230,255,0.65)' : 'rgba(255,100,100,0.45)';
ctx.fillText(n.label, n.x+5, n.y+3);
}
}
// ④ EKG HEARTBEAT ────────────────────────────────────────────────────
(function initEKG() {
const canvas = document.getElementById('ekgCanvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
let W, H, data = [], phase = 0;
function resize() {
const wrap = document.getElementById('ekgWrap');
W = canvas.width = wrap ? Math.max(100, wrap.clientWidth) : 180;
H = canvas.height = 22;
data = new Array(W).fill(0);
}
function ekgVal(p) {
const c = p % 1;
if (c < 0.28) return 0;
if (c < 0.38) return Math.sin((c-0.28)/0.10*Math.PI) * 0.22; // P bump
if (c < 0.43) return 0; // PR flat
if (c < 0.45) return -(c-0.43)/0.02 * 0.18; // Q dip
if (c < 0.465){ const f=(c-0.45)/0.015; return f<0.5?f*2*0.95:(2-f*2)*0.95; } // R spike
if (c < 0.49) return -(c-0.465)/0.025 * 0.22; // S dip
if (c < 0.52) return -(0.22-(c-0.49)/0.03*0.22); // back to baseline
if (c < 0.70) return Math.sin((c-0.52)/0.18*Math.PI) * 0.28; // T wave
return 0;
}
function draw() {
phase += 0.0038;
data.shift(); data.push(ekgVal(phase));
ctx.clearRect(0, 0, W, H);
// Glow layer
ctx.beginPath();
data.forEach((v,i) => { const y=H*0.5-v*H*0.44; i===0?ctx.moveTo(i,y):ctx.lineTo(i,y); });
ctx.strokeStyle='rgba(0,220,100,0.2)'; ctx.lineWidth=4; ctx.stroke();
// Line
ctx.beginPath();
data.forEach((v,i) => { const y=H*0.5-v*H*0.44; i===0?ctx.moveTo(i,y):ctx.lineTo(i,y); });
ctx.strokeStyle='rgba(0,255,120,0.85)'; ctx.lineWidth=1.2; ctx.stroke();
requestAnimationFrame(draw);
}
resize(); draw();
window.addEventListener('resize', resize);
})();
// ⑤ AUDIO WAVEFORM RING ───────────────────────────────────────────────
(function initAudioRing() {
const canvas = document.getElementById('audioRingCanvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
const CX = 30, CY = 30, BARS = 28, INNER = 20;
let t = 0;
function draw() {
t += 0.055;
ctx.clearRect(0, 0, 60, 60);
const listening = window.isListening;
const speaking = window.isSpeaking;
if (!listening && !speaking) { requestAnimationFrame(draw); return; }
for (let i = 0; i < BARS; i++) {
const angle = (i / BARS) * Math.PI * 2;
let amp;
if (speaking) {
amp = 0.35 + Math.abs(Math.sin(t*4.1+i*0.9))*0.5 + Math.abs(Math.sin(t*9+i*1.7))*0.15;
} else {
amp = 0.08 + Math.abs(Math.sin(t*1.1+i*0.6))*0.18;
}
const barLen = 3 + amp * 11;
ctx.beginPath();
ctx.moveTo(CX+Math.cos(angle)*INNER, CY+Math.sin(angle)*INNER);
ctx.lineTo(CX+Math.cos(angle)*(INNER+barLen), CY+Math.sin(angle)*(INNER+barLen));
const alpha = speaking ? 0.55+amp*0.45 : 0.25+amp*0.35;
ctx.strokeStyle = speaking ? `rgba(0,255,175,${alpha})` : `rgba(0,212,255,${alpha})`;
ctx.lineWidth = 1.8; ctx.stroke();
}
requestAnimationFrame(draw);
}
draw();
})();
// ⑥ STATIC NOISE BURSTS ───────────────────────────────────────────────
(function initStaticBursts() {
function burst() {
const panels = document.querySelectorAll('#app .panel');
if (panels.length) {
const p = panels[Math.floor(Math.random() * panels.length)];
const n = document.createElement('div');
n.className = 'panel-noise-layer';
p.appendChild(n);
setTimeout(() => n.remove(), 320);
}
setTimeout(burst, 75000 + Math.random() * 55000);
}
setTimeout(burst, 40000 + Math.random() * 30000);
})();
// ⑦ AMBIENT COLOR CYCLE ───────────────────────────────────────────────
(function initAmbientColor() {
let t = 0;
const root = document.documentElement;
function tick() {
t += 0.00025;
const g = Math.round(210 + Math.sin(t) * 22);
const b = Math.round(248 + Math.cos(t * 1.15) * 28);
root.style.setProperty('--cyan', `rgb(0,${g},${b})`);
root.style.setProperty('--cyan2', `rgb(0,${Math.round(g*.82)},${Math.round(b*.87)})`);
requestAnimationFrame(tick);
}
tick();
})();
// ── GLOBALS ────────────────────────────────────────────────────────── // ── GLOBALS ──────────────────────────────────────────────────────────
let sessionToken = ''; let sessionToken = '';
let sessionUser = ''; let sessionUser = '';
@@ -1405,6 +1743,20 @@ function showApp(name, greeting, silent = false) {
const app = document.getElementById('app'); const app = document.getElementById('app');
app.style.display = 'flex'; app.style.display = 'flex';
// HUD boot sequence — staggered slide-in
const topBar = document.getElementById('topBar');
const leftPanel = document.getElementById('leftPanel');
const rightPanel = document.getElementById('rightPanel');
const centerPanel= document.getElementById('centerPanel');
[topBar, leftPanel, rightPanel, centerPanel].forEach(el => el && (el.style.opacity = '0'));
requestAnimationFrame(() => {
if (topBar) { topBar.style.opacity=''; topBar.style.animationDelay='0s'; topBar.classList.add('boot-top'); }
setTimeout(()=>{ if(leftPanel) { leftPanel.style.opacity=''; leftPanel.style.animationDelay='0s'; leftPanel.classList.add('boot-left'); }}, 120);
setTimeout(()=>{ if(rightPanel) { rightPanel.style.opacity=''; rightPanel.style.animationDelay='0s'; rightPanel.classList.add('boot-right'); }}, 180);
setTimeout(()=>{ if(centerPanel){ centerPanel.style.opacity='';centerPanel.style.animationDelay='0s';centerPanel.classList.add('boot-center');}}, 240);
setTimeout(()=>{ [topBar,leftPanel,rightPanel,centerPanel].forEach(el=>el?.classList.remove('boot-top','boot-left','boot-right','boot-center')); }, 1200);
});
if (!silent) { if (!silent) {
if (greeting) { if (greeting) {
addMessage('jarvis', greeting); addMessage('jarvis', greeting);
@@ -1779,6 +2131,7 @@ async function loadNetwork() {
// ── RENDER: NETWORK ─────────────────────────────────────────────────── // ── RENDER: NETWORK ───────────────────────────────────────────────────
function renderNetworkStatus(n) { function renderNetworkStatus(n) {
if (!n) return; if (!n) return;
renderTopology(n.devices || []);
const el = document.getElementById('network-list'); const el = document.getElementById('network-list');
if (!el) return; if (!el) return;
const devices = n.devices || []; const devices = n.devices || [];
@@ -2153,8 +2506,29 @@ function addMessage(role, text) {
const log = document.getElementById('chatLog'); const log = document.getElementById('chatLog');
const div = document.createElement('div'); const div = document.createElement('div');
div.className = 'msg ' + role; div.className = 'msg ' + role;
div.textContent = text;
log.appendChild(div); log.appendChild(div);
if (role === 'jarvis' && text && text.length > 0) {
// Adaptive speed: fast for short, slower for long (feels intentional either way)
const msPerChar = Math.max(9, Math.min(25, 1600 / text.length));
const cursor = document.createElement('span');
cursor.className = 'type-cursor';
div.appendChild(cursor);
let i = 0;
const type = () => {
if (i < text.length) {
cursor.insertAdjacentText('beforebegin', text[i++]);
log.scrollTop = log.scrollHeight;
setTimeout(type, msPerChar + (text[i-1] === '.' || text[i-1] === ',' ? msPerChar * 4 : 0));
} else {
cursor.remove();
}
};
setTimeout(type, 0);
} else {
div.textContent = text;
}
log.scrollTop = log.scrollHeight; log.scrollTop = log.scrollHeight;
return div; return div;
} }