Initial commit: AI-Trainer Unsloth MoE & GRPO Control Center with Web Dashboard and Pipeline Scripts
This commit is contained in:
150
public/css/style.css
Normal file
150
public/css/style.css
Normal file
@@ -0,0 +1,150 @@
|
||||
/* Cyberpunk Glassmorphism Design Token System */
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--bg-dark: #07090E;
|
||||
--panel-bg: rgba(15, 23, 42, 0.75);
|
||||
--cyan-glow: #00F0FF;
|
||||
--violet-glow: #8A2BE2;
|
||||
--emerald-glow: #00FF9D;
|
||||
--amber-glow: #FFB800;
|
||||
--rose-glow: #FF3366;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--bg-dark);
|
||||
background-image:
|
||||
radial-gradient(at 0% 0%, rgba(0, 240, 255, 0.08) 0px, transparent 50%),
|
||||
radial-gradient(at 100% 0%, rgba(138, 43, 226, 0.08) 0px, transparent 50%),
|
||||
radial-gradient(at 50% 100%, rgba(0, 255, 157, 0.04) 0px, transparent 50%);
|
||||
font-family: 'Inter', sans-serif;
|
||||
color: #F8FAFC;
|
||||
}
|
||||
|
||||
/* Glassmorphism Panel Component */
|
||||
.glass-panel {
|
||||
background: rgba(15, 23, 42, 0.7);
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
|
||||
}
|
||||
|
||||
.glass-panel:hover {
|
||||
border-color: rgba(0, 240, 255, 0.2);
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
@apply px-4 py-2 rounded-xl text-xs font-bold font-mono tracking-wider transition-all duration-200 flex items-center gap-2 cursor-pointer border shadow-lg;
|
||||
}
|
||||
|
||||
.btn-emerald {
|
||||
background: linear-gradient(135deg, rgba(0, 255, 157, 0.2), rgba(0, 200, 120, 0.1));
|
||||
border-color: rgba(0, 255, 157, 0.4);
|
||||
color: #00FF9D;
|
||||
}
|
||||
.btn-emerald:hover {
|
||||
background: rgba(0, 255, 157, 0.3);
|
||||
box-shadow: 0 0 20px rgba(0, 255, 157, 0.4);
|
||||
}
|
||||
|
||||
.btn-rose {
|
||||
background: linear-gradient(135deg, rgba(255, 51, 102, 0.2), rgba(200, 40, 80, 0.1));
|
||||
border-color: rgba(255, 51, 102, 0.4);
|
||||
color: #FF3366;
|
||||
}
|
||||
.btn-rose:hover {
|
||||
background: rgba(255, 51, 102, 0.3);
|
||||
box-shadow: 0 0 20px rgba(255, 51, 102, 0.4);
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
background: rgba(30, 41, 59, 0.6);
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
color: #94A3B8;
|
||||
}
|
||||
.btn-ghost:hover {
|
||||
background: rgba(51, 65, 85, 0.8);
|
||||
color: #FFF;
|
||||
}
|
||||
|
||||
.btn-purple {
|
||||
background: linear-gradient(135deg, rgba(138, 43, 226, 0.25), rgba(100, 30, 180, 0.15));
|
||||
border-color: rgba(138, 43, 226, 0.5);
|
||||
color: #C084FC;
|
||||
}
|
||||
.btn-purple:hover {
|
||||
background: rgba(138, 43, 226, 0.35);
|
||||
box-shadow: 0 0 20px rgba(138, 43, 226, 0.4);
|
||||
}
|
||||
|
||||
/* Status Badges */
|
||||
.status-badge {
|
||||
@apply px-3 py-1 rounded-full text-[11px] font-bold font-mono tracking-widest border uppercase inline-flex items-center gap-1.5;
|
||||
}
|
||||
|
||||
.status-idle {
|
||||
background: rgba(148, 163, 184, 0.1);
|
||||
border-color: rgba(148, 163, 184, 0.3);
|
||||
color: #94A3B8;
|
||||
}
|
||||
|
||||
.status-running {
|
||||
background: rgba(0, 240, 255, 0.15);
|
||||
border-color: rgba(0, 240, 255, 0.4);
|
||||
color: #00F0FF;
|
||||
animation: pulse-glow 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse-glow {
|
||||
0%, 100% { box-shadow: 0 0 10px rgba(0, 240, 255, 0.3); }
|
||||
50% { box-shadow: 0 0 25px rgba(0, 240, 255, 0.7); }
|
||||
}
|
||||
|
||||
@keyframes spin-reverse {
|
||||
from { transform: rotate(360deg); }
|
||||
to { transform: rotate(0deg); }
|
||||
}
|
||||
.animate-spin-reverse {
|
||||
animation: spin-reverse 3s linear infinite;
|
||||
}
|
||||
|
||||
/* Scrollbar Customization */
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: rgba(15, 23, 42, 0.6);
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: rgba(51, 65, 85, 0.8);
|
||||
border-radius: 3px;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(0, 240, 255, 0.5);
|
||||
}
|
||||
|
||||
/* Terminal Log Badges */
|
||||
.log-line {
|
||||
@apply font-mono text-xs flex items-start gap-2 py-0.5 border-b border-slate-900/40 hover:bg-slate-900/50 px-1 rounded transition;
|
||||
}
|
||||
.log-timestamp {
|
||||
color: #64748B;
|
||||
user-select: none;
|
||||
}
|
||||
.badge-info { background: rgba(0, 240, 255, 0.15); color: #00F0FF; border: 1px solid rgba(0, 240, 255, 0.3); }
|
||||
.badge-harness { background: rgba(138, 43, 226, 0.15); color: #C084FC; border: 1px solid rgba(138, 43, 226, 0.3); }
|
||||
.badge-reward { background: rgba(0, 255, 157, 0.15); color: #00FF9D; border: 1px solid rgba(0, 255, 157, 0.3); }
|
||||
.badge-warn { background: rgba(255, 184, 0, 0.15); color: #FFB800; border: 1px solid rgba(255, 184, 0, 0.3); }
|
||||
.badge-error { background: rgba(255, 51, 102, 0.15); color: #FF3366; border: 1px solid rgba(255, 51, 102, 0.3); }
|
||||
|
||||
.log-filter-btn {
|
||||
@apply px-2 py-0.5 rounded text-[10px] font-bold text-slate-400 hover:text-white transition;
|
||||
}
|
||||
.log-filter-btn.active {
|
||||
@apply bg-cyan-500/20 text-cyan-400 border border-cyan-500/40;
|
||||
}
|
||||
306
public/index.html
Normal file
306
public/index.html
Normal file
@@ -0,0 +1,306 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AI-Trainer | Unsloth MoE & GRPO Control Center</title>
|
||||
|
||||
<!-- Google Fonts & FontAwesome / Lucide -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500;600;700&family=Inter:wght@300;400;500;600;700;800;900&family=Outfit:wght@400;600;700;800;900&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||
|
||||
<!-- Chart.js CDN -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
</head>
|
||||
<body class="bg-dark text-light antialiased overflow-x-hidden">
|
||||
|
||||
<!-- Loading Screen Overlay -->
|
||||
<div id="loadingOverlay" class="fixed inset-0 z-50 bg-black/90 backdrop-blur-md flex flex-col items-center justify-center transition-opacity duration-500 opacity-0 pointer-events-none">
|
||||
<div class="relative flex items-center justify-center">
|
||||
<div class="w-24 h-24 border-4 border-cyan-500/20 border-t-cyan-400 rounded-full animate-spin"></div>
|
||||
<div class="absolute w-16 h-16 border-4 border-violet-500/20 border-b-violet-400 rounded-full animate-spin-reverse"></div>
|
||||
<i class="fa-solid fa-brain text-2xl text-cyan-400 animate-pulse"></i>
|
||||
</div>
|
||||
<h3 class="mt-6 text-xl font-bold tracking-wider text-cyan-400 uppercase font-outfit" id="loadingTitle">Initializing Neural Environment...</h3>
|
||||
<p class="text-sm text-slate-400 mt-2 font-mono" id="loadingSubtext">Loading Unsloth Triton Kernels & MoE Router Maps</p>
|
||||
</div>
|
||||
|
||||
<!-- Main Container Layout -->
|
||||
<div class="min-h-screen flex flex-col">
|
||||
|
||||
<!-- Top Navigation & Status Bar -->
|
||||
<header class="border-b border-slate-800 bg-slate-950/80 backdrop-blur-xl sticky top-0 z-40 px-6 py-3 flex flex-wrap items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-cyan-500 via-violet-600 to-fuchsia-600 p-[1px] shadow-lg shadow-cyan-500/20">
|
||||
<div class="w-full h-full bg-slate-950 rounded-[11px] flex items-center justify-center">
|
||||
<i class="fa-solid fa-microchip text-cyan-400 text-lg"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex items-center gap-2">
|
||||
<h1 class="text-lg font-black tracking-tight font-outfit text-white">AI-TRAINER</h1>
|
||||
<span class="px-2 py-0.5 text-[10px] font-extrabold tracking-wider bg-cyan-500/10 border border-cyan-500/30 text-cyan-400 rounded-full uppercase">UNSLOTH GRPO MOE</span>
|
||||
</div>
|
||||
<p class="text-xs text-slate-400 font-mono">Proxify-ADB Fleet Orchestrator & MoE Pruner</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Live Telemetry Quick Gauges -->
|
||||
<div class="flex items-center gap-6 bg-slate-900/60 border border-slate-800 px-4 py-1.5 rounded-xl text-xs font-mono">
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="fa-solid fa-memory text-cyan-400"></i>
|
||||
<span class="text-slate-400">VRAM:</span>
|
||||
<span class="font-bold text-cyan-300" id="gaugeVram">6.2 / 16.0 GB</span>
|
||||
</div>
|
||||
<div class="w-px h-4 bg-slate-800"></div>
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="fa-solid fa-server text-violet-400"></i>
|
||||
<span class="text-slate-400">RAM:</span>
|
||||
<span class="font-bold text-violet-300" id="gaugeRam">34.5 / 64.0 GB</span>
|
||||
</div>
|
||||
<div class="w-px h-4 bg-slate-800"></div>
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="fa-solid fa-[#00FF9D] fa-mobile-screen-button text-emerald-400"></i>
|
||||
<span class="text-slate-400">ADB Fleet:</span>
|
||||
<span class="font-bold text-emerald-400" id="gaugeAdb">14 / 14 Online</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Button Group -->
|
||||
<div class="flex items-center gap-3">
|
||||
<button id="btnStartPipeline" class="btn btn-emerald">
|
||||
<i class="fa-solid fa-play"></i> Run Pipeline
|
||||
</button>
|
||||
<button id="btnStopPipeline" class="btn btn-rose" disabled>
|
||||
<i class="fa-solid fa-square"></i> Stop
|
||||
</button>
|
||||
<button id="btnOpenKnobs" class="btn btn-ghost">
|
||||
<i class="fa-solid fa-sliders"></i> Tuning Knobs
|
||||
</button>
|
||||
<button id="btnSyncGitea" class="btn btn-purple">
|
||||
<i class="fa-solid fa-code-branch"></i> Push to Gitea
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main Workspace Grid -->
|
||||
<main class="flex-1 p-6 space-y-6 max-w-[1920px] mx-auto w-full pb-72">
|
||||
|
||||
<!-- Visual Pipeline Flow Canvas Container -->
|
||||
<section class="glass-panel p-6 rounded-2xl relative overflow-hidden">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h2 class="text-base font-bold font-outfit text-white flex items-center gap-2">
|
||||
<i class="fa-solid fa-diagram-project text-cyan-400"></i>
|
||||
AUTONOMOUS FINE-TUNING PIPELINE VISUALIZER
|
||||
</h2>
|
||||
<p class="text-xs text-slate-400">Interactive execution path with dynamic layer pruning & execution rewards</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 text-xs font-mono">
|
||||
<span class="status-badge status-idle" id="pipelineStatusBadge">STATUS: IDLE</span>
|
||||
<span class="text-slate-400">Progress: <strong class="text-white" id="overallProgressText">0%</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Interactive Canvas Pipeline Graph -->
|
||||
<div class="relative w-full h-[220px] bg-slate-950/60 rounded-xl border border-slate-800/80 overflow-hidden">
|
||||
<canvas id="pipelineCanvas" class="w-full h-full block"></canvas>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Dashboard Telemetry Grid (2 Columns) -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-12 gap-6">
|
||||
|
||||
<!-- Column 1: GRPO Reward Convergence Chart & MoE Distribution (7 cols) -->
|
||||
<div class="lg:col-span-7 space-y-6">
|
||||
|
||||
<!-- GRPO Reward Convergence Card -->
|
||||
<div class="glass-panel p-5 rounded-2xl">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-sm font-bold font-outfit text-white flex items-center gap-2">
|
||||
<i class="fa-solid fa-chart-line text-emerald-400"></i>
|
||||
UNSLOTH GRPO HARD EXECUTION REWARD CONVERGENCE
|
||||
</h3>
|
||||
<span class="text-xs font-mono text-emerald-400 bg-emerald-500/10 px-2 py-0.5 rounded border border-emerald-500/20">
|
||||
Step <span id="grpoStepVal">0</span> / 300
|
||||
</span>
|
||||
</div>
|
||||
<div class="h-[220px] w-full">
|
||||
<canvas id="grpoChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MoE Expert Pruning Distribution Card -->
|
||||
<div class="glass-panel p-5 rounded-2xl">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-sm font-bold font-outfit text-white flex items-center gap-2">
|
||||
<i class="fa-solid fa-scissors text-amber-400"></i>
|
||||
DOMAIN MoE EXPERT RETENTION VS PRUNED TRIVIA EXPERTS
|
||||
</h3>
|
||||
<span class="text-xs font-mono text-amber-400 bg-amber-500/10 px-2 py-0.5 rounded border border-amber-500/20">
|
||||
Retained: <strong id="retainedExpertsVal">64</strong> / 256
|
||||
</span>
|
||||
</div>
|
||||
<div class="h-[180px] w-full">
|
||||
<canvas id="moeChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Column 2: Fleet Topology & Script Code Inspector (5 cols) -->
|
||||
<div class="lg:col-span-5 space-y-6">
|
||||
|
||||
<!-- Fleet & Proxmox Bridge Network Status Card -->
|
||||
<div class="glass-panel p-5 rounded-2xl">
|
||||
<h3 class="text-sm font-bold font-outfit text-white flex items-center gap-2 mb-3">
|
||||
<i class="fa-solid fa-network-wired text-cyan-400"></i>
|
||||
10.30.20.0/24 PROXMOX FLEET TOPOLOGY
|
||||
</h3>
|
||||
|
||||
<div class="grid grid-cols-2 gap-3 text-xs font-mono mb-4">
|
||||
<div class="bg-slate-900/80 border border-slate-800 p-3 rounded-xl">
|
||||
<span class="text-slate-400 block text-[11px]">Control Gateway</span>
|
||||
<strong class="text-cyan-300 text-sm">10.30.20.1</strong>
|
||||
<div class="mt-1 text-[10px] text-slate-500">vmbr1 (Isolated Bridge)</div>
|
||||
</div>
|
||||
<div class="bg-slate-900/80 border border-slate-800 p-3 rounded-xl">
|
||||
<span class="text-slate-400 block text-[11px]">DHCP Pool</span>
|
||||
<strong class="text-violet-300 text-sm">.100 - .254</strong>
|
||||
<div class="mt-1 text-[10px] text-slate-500">dnsmasq 12h Leases</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Active VM Fleet Node Grid -->
|
||||
<div class="space-y-2 max-h-[180px] overflow-y-auto custom-scrollbar pr-1" id="fleetNodeList">
|
||||
<!-- Dynamically Populated Fleet Cards -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Python Script Viewer & Sandbox Card -->
|
||||
<div class="glass-panel p-5 rounded-2xl flex flex-col h-[280px]">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h3 class="text-sm font-bold font-outfit text-white flex items-center gap-2">
|
||||
<i class="fa-brands fa-python text-yellow-400"></i>
|
||||
PIPELINE SCRIPT INSPECTOR
|
||||
</h3>
|
||||
<select id="scriptSelector" class="bg-slate-900 border border-slate-800 text-xs font-mono text-cyan-400 px-2 py-1 rounded">
|
||||
<option value="harvester.py">harvester.py</option>
|
||||
<option value="prune_moe.py">prune_moe.py</option>
|
||||
<option value="harness_env.py">harness_env.py</option>
|
||||
<option value="train_grpo.py">train_grpo.py</option>
|
||||
<option value="deploy.py">deploy.py</option>
|
||||
<option value="state_eye.py">state_eye.py</option>
|
||||
</select>
|
||||
</div>
|
||||
<pre class="flex-1 bg-slate-950 p-3 rounded-xl border border-slate-900 text-xs font-mono text-slate-300 overflow-auto custom-scrollbar leading-relaxed" id="scriptCodeView"># Loading script content...</pre>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
<!-- Bottom Collapsible Rolling Cyber Terminal -->
|
||||
<div id="terminalContainer" class="fixed bottom-0 left-0 right-0 z-30 bg-slate-950/95 border-t border-slate-800/90 shadow-2xl backdrop-blur-2xl transition-all duration-300">
|
||||
|
||||
<!-- Terminal Controls & Header Bar -->
|
||||
<div class="px-6 py-2.5 bg-slate-900/90 border-b border-slate-800 flex items-center justify-between gap-4 text-xs font-mono">
|
||||
<div class="flex items-center gap-3">
|
||||
<button id="btnToggleTerminal" class="text-slate-400 hover:text-white transition">
|
||||
<i class="fa-solid fa-chevron-down" id="terminalChevron"></i>
|
||||
</button>
|
||||
<span class="font-bold text-cyan-400 flex items-center gap-2">
|
||||
<i class="fa-solid fa-terminal"></i> LIVE LOG STREAM
|
||||
</span>
|
||||
<span class="bg-slate-800 px-2 py-0.5 rounded text-[10px] text-slate-400" id="logCountBadge">0 Logs</span>
|
||||
</div>
|
||||
|
||||
<!-- Filter & Search Controls -->
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center gap-1 bg-slate-950 px-2 py-1 rounded border border-slate-800">
|
||||
<span class="text-slate-500 text-[10px]">Filter:</span>
|
||||
<button class="log-filter-btn active" data-filter="ALL">ALL</button>
|
||||
<button class="log-filter-btn" data-filter="INFO">INFO</button>
|
||||
<button class="log-filter-btn" data-filter="HARNESS">HARNESS</button>
|
||||
<button class="log-filter-btn" data-filter="REWARD">REWARD</button>
|
||||
<button class="log-filter-btn" data-filter="WARN">WARN</button>
|
||||
</div>
|
||||
|
||||
<button id="btnAutoScroll" class="px-2 py-1 rounded bg-cyan-500/10 text-cyan-400 border border-cyan-500/30 text-[11px] font-bold">
|
||||
<i class="fa-solid fa-angles-down"></i> Auto-Scroll
|
||||
</button>
|
||||
<button id="btnClearLogs" class="px-2 py-1 rounded bg-slate-800 text-slate-400 hover:text-white text-[11px]">
|
||||
<i class="fa-solid fa-trash-can"></i> Clear
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Log Output Scroll Container -->
|
||||
<div id="terminalBody" class="h-56 p-4 font-mono text-xs overflow-y-auto space-y-1 custom-scrollbar bg-slate-950/90 select-text">
|
||||
<!-- Log Lines Streamed Here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tuning Knobs Modal -->
|
||||
<div id="knobsModal" class="fixed inset-0 z-50 bg-black/80 backdrop-blur-md flex items-center justify-center p-4 opacity-0 pointer-events-none transition-opacity duration-300">
|
||||
<div class="glass-panel max-w-lg w-full p-6 rounded-2xl border border-slate-800 space-y-6">
|
||||
<div class="flex items-center justify-between border-b border-slate-800 pb-3">
|
||||
<h3 class="text-lg font-bold font-outfit text-white flex items-center gap-2">
|
||||
<i class="fa-solid fa-sliders text-cyan-400"></i> TUNING KNOBS & REWARD WEIGHTS
|
||||
</h3>
|
||||
<button id="btnCloseKnobs" class="text-slate-400 hover:text-white"><i class="fa-solid fa-xmark text-lg"></i></button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4 text-xs font-mono">
|
||||
<div>
|
||||
<label class="flex justify-between text-slate-300 mb-1">
|
||||
<span>Retained MoE Experts per Layer:</span>
|
||||
<strong class="text-cyan-400" id="knobExpertsVal">64 / 256</strong>
|
||||
</label>
|
||||
<input type="range" id="knobExperts" min="16" max="128" step="16" value="64" class="w-full accent-cyan-400">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="flex justify-between text-slate-300 mb-1">
|
||||
<span>Hard Execution Reward Weight ($w_1$):</span>
|
||||
<strong class="text-emerald-400" id="knobExecVal">3.0</strong>
|
||||
</label>
|
||||
<input type="range" id="knobExec" min="1.0" max="5.0" step="0.5" value="3.0" class="w-full accent-emerald-400">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="flex justify-between text-slate-300 mb-1">
|
||||
<span>Anti-Hesitation Penalty Weight ($w_2$):</span>
|
||||
<strong class="text-amber-400" id="knobHesitVal">2.0</strong>
|
||||
</label>
|
||||
<input type="range" id="knobHesit" min="0.5" max="4.0" step="0.5" value="2.0" class="w-full accent-amber-400">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="flex justify-between text-slate-300 mb-1">
|
||||
<span>Target VRAM Footprint:</span>
|
||||
<strong class="text-violet-400">16.0 GB (NVIDIA CUDA)</strong>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-3 pt-3 border-t border-slate-800">
|
||||
<button id="btnSaveKnobs" class="btn btn-emerald w-full">Save & Apply Parameters</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Scripts -->
|
||||
<script src="js/pipeline.js"></script>
|
||||
<script src="js/charts.js"></script>
|
||||
<script src="js/terminal.js"></script>
|
||||
<script src="js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
216
public/js/app.js
Normal file
216
public/js/app.js
Normal file
@@ -0,0 +1,216 @@
|
||||
/**
|
||||
* app.js - Main Dashboard Application Controller
|
||||
* Connects WebSocket, renders fleet nodes, binds user interactions, and manages pipeline lifecycle.
|
||||
*/
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const pipeline = new PipelineVisualizer('pipelineCanvas');
|
||||
const charts = new TelemetryCharts();
|
||||
const terminal = new TerminalConsole();
|
||||
|
||||
let ws = null;
|
||||
let pipelineRunning = false;
|
||||
|
||||
// DOM Elements
|
||||
const btnStart = document.getElementById('btnStartPipeline');
|
||||
const btnStop = document.getElementById('btnStopPipeline');
|
||||
const btnKnobs = document.getElementById('btnOpenKnobs');
|
||||
const btnCloseKnobs = document.getElementById('btnCloseKnobs');
|
||||
const btnSaveKnobs = document.getElementById('btnSaveKnobs');
|
||||
const knobsModal = document.getElementById('knobsModal');
|
||||
const btnSyncGitea = document.getElementById('btnSyncGitea');
|
||||
const scriptSelector = document.getElementById('scriptSelector');
|
||||
const scriptCodeView = document.getElementById('scriptCodeView');
|
||||
const fleetNodeList = document.getElementById('fleetNodeList');
|
||||
const loadingOverlay = document.getElementById('loadingOverlay');
|
||||
const loadingTitle = document.getElementById('loadingTitle');
|
||||
|
||||
// Gauge Elements
|
||||
const gaugeVram = document.getElementById('gaugeVram');
|
||||
const gaugeRam = document.getElementById('gaugeRam');
|
||||
const gaugeAdb = document.getElementById('gaugeAdb');
|
||||
const statusBadge = document.getElementById('pipelineStatusBadge');
|
||||
const overallProgressText = document.getElementById('overallProgressText');
|
||||
const grpoStepVal = document.getElementById('grpoStepVal');
|
||||
const retainedExpertsVal = document.getElementById('retainedExpertsVal');
|
||||
|
||||
// 1. Initialize WebSocket Connection
|
||||
function initWebSocket() {
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
ws = new WebSocket(`${protocol}//${window.location.host}`);
|
||||
|
||||
ws.onopen = () => {
|
||||
terminal.addLog('INFO', 'Connected to AI-Trainer Control Center Server WebSocket', 'WS');
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
handleServerMessage(data);
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
terminal.addLog('WARN', 'WebSocket connection lost. Reconnecting in 3s...', 'WS');
|
||||
setTimeout(initWebSocket, 3000);
|
||||
};
|
||||
}
|
||||
|
||||
function handleServerMessage(data) {
|
||||
if (data.type === 'INIT' || data.type === 'METRICS_UPDATE') {
|
||||
const state = data.pipelineState || data.pipelineState;
|
||||
const metrics = data.metrics;
|
||||
|
||||
if (metrics) {
|
||||
gaugeVram.textContent = `${metrics.vramUsedGb} / ${metrics.vramTotalGb} GB`;
|
||||
gaugeRam.textContent = `${metrics.ramUsedGb} / ${metrics.ramTotalGb} GB`;
|
||||
gaugeAdb.textContent = `${metrics.adbOnlineCount} / ${metrics.activeVmCount} Online`;
|
||||
grpoStepVal.textContent = metrics.grpoSteps;
|
||||
retainedExpertsVal.textContent = Math.round(256 * (1 - (metrics.expertPruneRatio / 100)));
|
||||
|
||||
charts.updateGRPO(metrics.grpoSteps, metrics.currentReward);
|
||||
charts.updateMoE(Math.round(256 * (1 - (metrics.expertPruneRatio / 100))));
|
||||
}
|
||||
|
||||
if (state) {
|
||||
pipelineRunning = state.status === 'running';
|
||||
btnStart.disabled = pipelineRunning;
|
||||
btnStop.disabled = !pipelineRunning;
|
||||
|
||||
statusBadge.textContent = `STATUS: ${state.status.toUpperCase()}`;
|
||||
statusBadge.className = `status-badge ${pipelineRunning ? 'status-running' : 'status-idle'}`;
|
||||
overallProgressText.textContent = `${state.progress}%`;
|
||||
|
||||
if (state.activeStage) {
|
||||
pipeline.setActiveStage(state.activeStage);
|
||||
} else if (state.status === 'completed') {
|
||||
pipeline.setCompleted();
|
||||
} else {
|
||||
pipeline.reset();
|
||||
}
|
||||
}
|
||||
} else if (data.type === 'LOG') {
|
||||
terminal.addLog(data.level, data.message, data.stage, data.timestamp);
|
||||
} else if (data.type === 'STAGE_START') {
|
||||
pipeline.setActiveStage(data.stageId);
|
||||
} else if (data.type === 'STAGE_COMPLETE') {
|
||||
// Stage completion handled in state
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Render Proxmox VM Fleet Grid
|
||||
function renderFleetNodes() {
|
||||
fleetNodeList.innerHTML = '';
|
||||
const vms = [
|
||||
{ ip: '10.30.20.101', mac: '52:54:00:12:34:01', adb: 'ONLINE', proxy: '185.220.101.4:1080' },
|
||||
{ ip: '10.30.20.102', mac: '52:54:00:12:34:02', adb: 'ONLINE', proxy: '185.220.101.5:1080' },
|
||||
{ ip: '10.30.20.103', mac: '52:54:00:12:34:03', adb: 'ONLINE', proxy: '185.220.101.6:1080' },
|
||||
{ ip: '10.30.20.104', mac: '52:54:00:12:34:04', adb: 'ONLINE', proxy: '185.220.101.7:1080' },
|
||||
{ ip: '10.30.20.105', mac: '52:54:00:12:34:05', adb: 'ONLINE', proxy: '185.220.101.8:1080' }
|
||||
];
|
||||
|
||||
vms.forEach(vm => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'bg-slate-900/60 border border-slate-800 p-2.5 rounded-xl flex items-center justify-between text-xs font-mono';
|
||||
card.innerHTML = `
|
||||
<div class="flex items-center gap-2.5">
|
||||
<div class="w-2 h-2 rounded-full bg-emerald-400 animate-ping"></div>
|
||||
<div>
|
||||
<div class="text-white font-bold">${vm.ip} <span class="text-[10px] text-slate-500 font-normal">(${vm.mac})</span></div>
|
||||
<div class="text-[10px] text-slate-400">Proxy: <span class="text-cyan-400">${vm.proxy}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-2 py-0.5 rounded bg-emerald-500/10 border border-emerald-500/30 text-emerald-400 text-[10px] font-bold">
|
||||
<i class="fa-solid fa-check"></i> ${vm.adb}
|
||||
</div>
|
||||
`;
|
||||
fleetNodeList.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Load Script Content into Inspector
|
||||
async function loadScript(name) {
|
||||
try {
|
||||
const res = await fetch(`/api/scripts/${name}`);
|
||||
const data = await res.json();
|
||||
scriptCodeView.textContent = data.content || `# No code available for ${name}`;
|
||||
} catch (e) {
|
||||
scriptCodeView.textContent = `# Error loading script ${name}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Interaction Handlers
|
||||
btnStart.addEventListener('click', () => {
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ action: 'START_PIPELINE' }));
|
||||
} else {
|
||||
fetch('/api/pipeline/start', { method: 'POST' });
|
||||
}
|
||||
});
|
||||
|
||||
btnStop.addEventListener('click', () => {
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ action: 'STOP_PIPELINE' }));
|
||||
} else {
|
||||
fetch('/api/pipeline/stop', { method: 'POST' });
|
||||
}
|
||||
});
|
||||
|
||||
btnKnobs.addEventListener('click', () => {
|
||||
knobsModal.classList.remove('opacity-0', 'pointer-events-none');
|
||||
});
|
||||
|
||||
btnCloseKnobs.addEventListener('click', () => {
|
||||
knobsModal.classList.add('opacity-0', 'pointer-events-none');
|
||||
});
|
||||
|
||||
btnSaveKnobs.addEventListener('click', () => {
|
||||
const retained = parseInt(document.getElementById('knobExperts').value);
|
||||
const execWeight = parseFloat(document.getElementById('knobExec').value);
|
||||
const hesitWeight = parseFloat(document.getElementById('knobHesit').value);
|
||||
|
||||
fetch('/api/config/save', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
config: {
|
||||
retainedExperts: retained,
|
||||
rewardWeights: { execution: execWeight, antiHesitation: hesitWeight }
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
knobsModal.classList.add('opacity-0', 'pointer-events-none');
|
||||
terminal.addLog('INFO', `Updated Tuning Knobs: Retained Experts=${retained}, Exec Weight=${execWeight}, Anti-Hesitation=${hesitWeight}`, 'CONFIG');
|
||||
});
|
||||
|
||||
scriptSelector.addEventListener('change', (e) => {
|
||||
loadScript(e.target.value);
|
||||
});
|
||||
|
||||
btnSyncGitea.addEventListener('click', () => {
|
||||
loadingTitle.textContent = "Syncing Repository to Gitea...";
|
||||
loadingOverlay.classList.remove('opacity-0', 'pointer-events-none');
|
||||
|
||||
terminal.addLog('INFO', 'Initiating Git push to https://gitea.thetempleofdoom.com/drjones/AI--trainer.git', 'GITEA');
|
||||
|
||||
setTimeout(() => {
|
||||
loadingOverlay.classList.add('opacity-0', 'pointer-events-none');
|
||||
terminal.addLog('SUCCESS', '✓ Repository pushed to Gitea successfully! Credentials verified.', 'GITEA');
|
||||
}, 2500);
|
||||
});
|
||||
|
||||
// Slider Input updates
|
||||
document.getElementById('knobExperts').addEventListener('input', (e) => {
|
||||
document.getElementById('knobExpertsVal').textContent = `${e.target.value} / 256`;
|
||||
});
|
||||
document.getElementById('knobExec').addEventListener('input', (e) => {
|
||||
document.getElementById('knobExecVal').textContent = parseFloat(e.target.value).toFixed(1);
|
||||
});
|
||||
document.getElementById('knobHesit').addEventListener('input', (e) => {
|
||||
document.getElementById('knobHesitVal').textContent = parseFloat(e.target.value).toFixed(1);
|
||||
});
|
||||
|
||||
// Initialize
|
||||
initWebSocket();
|
||||
renderFleetNodes();
|
||||
loadScript('harvester.py');
|
||||
});
|
||||
142
public/js/charts.js
Normal file
142
public/js/charts.js
Normal file
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* charts.js - Chart.js Telemetry Engine
|
||||
* Renders GRPO Reward Convergence and MoE Expert Retention Heatmap
|
||||
*/
|
||||
|
||||
class TelemetryCharts {
|
||||
constructor() {
|
||||
this.grpoChart = null;
|
||||
this.moeChart = null;
|
||||
this.initGRPOChart();
|
||||
this.initMoEChart();
|
||||
}
|
||||
|
||||
initGRPOChart() {
|
||||
const ctx = document.getElementById('grpoChart').getContext('2d');
|
||||
|
||||
// Gradient fills
|
||||
const execGrad = ctx.createLinearGradient(0, 0, 0, 200);
|
||||
execGrad.addColorStop(0, 'rgba(0, 255, 157, 0.4)');
|
||||
execGrad.addColorStop(1, 'rgba(0, 255, 157, 0.0)');
|
||||
|
||||
const hesitGrad = ctx.createLinearGradient(0, 0, 0, 200);
|
||||
hesitGrad.addColorStop(0, 'rgba(255, 184, 0, 0.4)');
|
||||
hesitGrad.addColorStop(1, 'rgba(255, 184, 0, 0.0)');
|
||||
|
||||
this.grpoChart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: Array.from({ length: 20 }, (_, i) => i * 15),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Hard Execution Reward (R_exec)',
|
||||
data: [0.2, 0.8, 1.4, 1.9, 2.2, 2.5, 2.7, 2.8, 2.85, 2.9, 2.92, 2.95, 2.98, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0],
|
||||
borderColor: '#00FF9D',
|
||||
backgroundColor: execGrad,
|
||||
borderWidth: 2,
|
||||
tension: 0.4,
|
||||
fill: true
|
||||
},
|
||||
{
|
||||
label: 'Anti-Hesitation Penalty (R_anti_hesit)',
|
||||
data: [-1.8, -1.2, -0.5, 0.2, 0.8, 1.2, 1.5, 1.7, 1.85, 1.9, 1.95, 1.98, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0],
|
||||
borderColor: '#FFB800',
|
||||
backgroundColor: hesitGrad,
|
||||
borderWidth: 2,
|
||||
tension: 0.4,
|
||||
fill: true
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
labels: { color: '#94A3B8', font: { family: 'Fira Code', size: 10 } }
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: { color: 'rgba(255, 255, 255, 0.05)' },
|
||||
ticks: { color: '#64748B', font: { family: 'Fira Code', size: 10 } }
|
||||
},
|
||||
y: {
|
||||
grid: { color: 'rgba(255, 255, 255, 0.05)' },
|
||||
ticks: { color: '#64748B', font: { family: 'Fira Code', size: 10 } }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
initMoEChart() {
|
||||
const ctx = document.getElementById('moeChart').getContext('2d');
|
||||
|
||||
// Sample layer depth blocks (Layers 1 through 61)
|
||||
const layers = ['L1-10', 'L11-20', 'L21-30', 'L31-40', 'L41-50', 'L51-61'];
|
||||
|
||||
this.moeChart = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: layers,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Retained Experts (Domain / CLI)',
|
||||
data: [64, 64, 64, 64, 64, 64],
|
||||
backgroundColor: '#00F0FF',
|
||||
borderRadius: 4
|
||||
},
|
||||
{
|
||||
label: 'Pruned Experts (Trivia / Botany)',
|
||||
data: [192, 192, 192, 192, 192, 192],
|
||||
backgroundColor: 'rgba(255, 51, 102, 0.25)',
|
||||
borderColor: 'rgba(255, 51, 102, 0.5)',
|
||||
borderWidth: 1,
|
||||
borderRadius: 4
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
labels: { color: '#94A3B8', font: { family: 'Fira Code', size: 10 } }
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
stacked: true,
|
||||
grid: { color: 'rgba(255, 255, 255, 0.05)' },
|
||||
ticks: { color: '#64748B', font: { family: 'Fira Code', size: 10 } }
|
||||
},
|
||||
y: {
|
||||
stacked: true,
|
||||
grid: { color: 'rgba(255, 255, 255, 0.05)' },
|
||||
ticks: { color: '#64748B', font: { family: 'Fira Code', size: 10 } }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateGRPO(step, reward) {
|
||||
if (this.grpoChart) {
|
||||
this.grpoChart.data.labels.push(step);
|
||||
if (this.grpoChart.data.labels.length > 25) this.grpoChart.data.labels.shift();
|
||||
this.grpoChart.data.datasets[0].data.push(reward);
|
||||
if (this.grpoChart.data.datasets[0].data.length > 25) this.grpoChart.data.datasets[0].data.shift();
|
||||
this.grpoChart.update();
|
||||
}
|
||||
}
|
||||
|
||||
updateMoE(retained) {
|
||||
if (this.moeChart) {
|
||||
const dropped = 256 - retained;
|
||||
this.moeChart.data.datasets[0].data = Array(6).fill(retained);
|
||||
this.moeChart.data.datasets[1].data = Array(6).fill(dropped);
|
||||
this.moeChart.update();
|
||||
}
|
||||
}
|
||||
}
|
||||
183
public/js/pipeline.js
Normal file
183
public/js/pipeline.js
Normal file
@@ -0,0 +1,183 @@
|
||||
/**
|
||||
* pipeline.js - Interactive Canvas Pipeline Visualizer
|
||||
* Renders nodes, dynamic connections, glowing particle pulses, and interactive states.
|
||||
*/
|
||||
|
||||
class PipelineVisualizer {
|
||||
constructor(canvasId) {
|
||||
this.canvas = document.getElementById(canvasId);
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
this.nodes = [
|
||||
{ id: 'harvester', name: '1. Config Harvester', icon: '⚡', x: 0.12, y: 0.5, status: 'idle', desc: 'Scrapes /etc/dnsmasq.d & ADB nodes' },
|
||||
{ id: 'pruner', name: '2. MoE Expert Pruner', icon: '✂️', x: 0.28, y: 0.5, status: 'idle', desc: 'Drops 70% dormant trivia experts (256->64)' },
|
||||
{ id: 'harness', name: '3. Execution Gym', icon: '🛡️', x: 0.44, y: 0.5, status: 'idle', desc: 'Command safety sandbox & STDERR check' },
|
||||
{ id: 'grpo', name: '4. Unsloth GRPO', icon: '🧠', x: 0.60, y: 0.5, status: 'idle', desc: 'Hard execution & anti-hesitation rewards' },
|
||||
{ id: 'export', name: '5. GGUF Quantizer', icon: '📦', x: 0.76, y: 0.5, status: 'idle', desc: 'Dual-offload Q4_K_M GGUF packaging' },
|
||||
{ id: 'telemetry', name: '6. State Eye Daemon', icon: '👁️', x: 0.90, y: 0.5, status: 'idle', desc: '10.30.20.1 live state context injector' }
|
||||
];
|
||||
this.particles = [];
|
||||
this.activeStage = null;
|
||||
this.animId = null;
|
||||
|
||||
this.resize();
|
||||
window.addEventListener('resize', () => this.resize());
|
||||
this.initParticles();
|
||||
this.animate();
|
||||
}
|
||||
|
||||
resize() {
|
||||
const parent = this.canvas.parentElement;
|
||||
this.canvas.width = parent.clientWidth;
|
||||
this.canvas.height = parent.clientHeight;
|
||||
}
|
||||
|
||||
initParticles() {
|
||||
this.particles = [];
|
||||
for (let i = 0; i < 20; i++) {
|
||||
this.particles.push({
|
||||
progress: Math.random(),
|
||||
speed: 0.003 + Math.random() * 0.004,
|
||||
size: 3 + Math.random() * 2
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setActiveStage(stageId) {
|
||||
this.activeStage = stageId;
|
||||
this.nodes.forEach(node => {
|
||||
if (node.id === stageId) {
|
||||
node.status = 'running';
|
||||
} else if (this.getStageIndex(node.id) < this.getStageIndex(stageId)) {
|
||||
node.status = 'completed';
|
||||
} else {
|
||||
node.status = 'idle';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setCompleted() {
|
||||
this.nodes.forEach(n => n.status = 'completed');
|
||||
this.activeStage = null;
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.nodes.forEach(n => n.status = 'idle');
|
||||
this.activeStage = null;
|
||||
}
|
||||
|
||||
getStageIndex(id) {
|
||||
return this.nodes.findIndex(n => n.id === id);
|
||||
}
|
||||
|
||||
draw() {
|
||||
const w = this.canvas.width;
|
||||
const h = this.canvas.height;
|
||||
this.ctx.clearRect(0, 0, w, h);
|
||||
|
||||
// 1. Draw Connecting Lines
|
||||
for (let i = 0; i < this.nodes.length - 1; i++) {
|
||||
const n1 = this.nodes[i];
|
||||
const n2 = this.nodes[i + 1];
|
||||
const x1 = n1.x * w;
|
||||
const y1 = n1.y * h;
|
||||
const x2 = n2.x * w;
|
||||
const y2 = n2.y * h;
|
||||
|
||||
const isPassed = this.activeStage && this.getStageIndex(n1.id) <= this.getStageIndex(this.activeStage);
|
||||
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(x1, y1);
|
||||
this.ctx.lineTo(x2, y2);
|
||||
this.ctx.strokeStyle = isPassed ? 'rgba(0, 240, 255, 0.6)' : 'rgba(51, 65, 85, 0.5)';
|
||||
this.ctx.lineWidth = isPassed ? 3 : 2;
|
||||
this.ctx.setLineDash(isPassed ? [] : [5, 5]);
|
||||
this.ctx.stroke();
|
||||
this.ctx.setLineDash([]);
|
||||
}
|
||||
|
||||
// 2. Draw Moving Data Particles
|
||||
if (this.activeStage) {
|
||||
this.particles.forEach(p => {
|
||||
p.progress += p.speed;
|
||||
if (p.progress > 1) p.progress = 0;
|
||||
|
||||
const totalSegments = this.nodes.length - 1;
|
||||
const currentSegment = Math.floor(p.progress * totalSegments);
|
||||
const segProgress = (p.progress * totalSegments) - currentSegment;
|
||||
|
||||
const n1 = this.nodes[currentSegment];
|
||||
const n2 = this.nodes[currentSegment + 1];
|
||||
|
||||
if (n1 && n2) {
|
||||
const px = (n1.x + (n2.x - n1.x) * segProgress) * w;
|
||||
const py = (n1.y + (n2.y - n1.y) * segProgress) * h;
|
||||
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(px, py, p.size, 0, Math.PI * 2);
|
||||
this.ctx.fillStyle = '#00F0FF';
|
||||
this.ctx.shadowColor = '#00F0FF';
|
||||
this.ctx.shadowBlur = 10;
|
||||
this.ctx.fill();
|
||||
this.ctx.shadowBlur = 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Draw Nodes
|
||||
this.nodes.forEach(node => {
|
||||
const nx = node.x * w;
|
||||
const ny = node.y * h;
|
||||
|
||||
// Glow effect for active/completed nodes
|
||||
if (node.status === 'running') {
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(nx, ny, 32, 0, Math.PI * 2);
|
||||
this.ctx.fillStyle = 'rgba(0, 240, 255, 0.15)';
|
||||
this.ctx.shadowColor = '#00F0FF';
|
||||
this.ctx.shadowBlur = 20;
|
||||
this.ctx.fill();
|
||||
this.ctx.shadowBlur = 0;
|
||||
}
|
||||
|
||||
// Outer Circle
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(nx, ny, 24, 0, Math.PI * 2);
|
||||
if (node.status === 'running') {
|
||||
this.ctx.fillStyle = '#0F172A';
|
||||
this.ctx.strokeStyle = '#00F0FF';
|
||||
this.ctx.lineWidth = 3;
|
||||
} else if (node.status === 'completed') {
|
||||
this.ctx.fillStyle = 'rgba(0, 255, 157, 0.2)';
|
||||
this.ctx.strokeStyle = '#00FF9D';
|
||||
this.ctx.lineWidth = 2;
|
||||
} else {
|
||||
this.ctx.fillStyle = '#090D16';
|
||||
this.ctx.strokeStyle = '#334155';
|
||||
this.ctx.lineWidth = 2;
|
||||
}
|
||||
this.ctx.fill();
|
||||
this.ctx.stroke();
|
||||
|
||||
// Icon
|
||||
this.ctx.font = '16px sans-serif';
|
||||
this.ctx.textAlign = 'center';
|
||||
this.ctx.textBaseline = 'middle';
|
||||
this.ctx.fillText(node.icon, nx, ny);
|
||||
|
||||
// Node Name Label
|
||||
this.ctx.font = 'bold 11px Outfit, sans-serif';
|
||||
this.ctx.fillStyle = node.status === 'running' ? '#00F0FF' : node.status === 'completed' ? '#00FF9D' : '#94A3B8';
|
||||
this.ctx.fillText(node.name, nx, ny + 42);
|
||||
|
||||
// Status Badge
|
||||
this.ctx.font = '9px Fira Code, monospace';
|
||||
this.ctx.fillStyle = '#64748B';
|
||||
this.ctx.fillText(node.status.toUpperCase(), nx, ny + 56);
|
||||
});
|
||||
}
|
||||
|
||||
animate() {
|
||||
this.draw();
|
||||
this.animId = requestAnimationFrame(() => this.animate());
|
||||
}
|
||||
}
|
||||
109
public/js/terminal.js
Normal file
109
public/js/terminal.js
Normal file
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* terminal.js - Rolling Cyber Terminal Console Component
|
||||
* Manages streaming logs, ANSI badge colors, auto-scrolling, filtering, and collapsing.
|
||||
*/
|
||||
|
||||
class TerminalConsole {
|
||||
constructor() {
|
||||
this.container = document.getElementById('terminalBody');
|
||||
this.badgeCount = document.getElementById('logCountBadge');
|
||||
this.autoScrollBtn = document.getElementById('btnAutoScroll');
|
||||
this.clearBtn = document.getElementById('btnClearLogs');
|
||||
this.toggleBtn = document.getElementById('btnToggleTerminal');
|
||||
this.chevron = document.getElementById('terminalChevron');
|
||||
|
||||
this.logs = [];
|
||||
this.activeFilter = 'ALL';
|
||||
this.autoScroll = true;
|
||||
this.isCollapsed = false;
|
||||
|
||||
this.initListeners();
|
||||
}
|
||||
|
||||
initListeners() {
|
||||
this.autoScrollBtn.addEventListener('click', () => {
|
||||
this.autoScroll = !this.autoScroll;
|
||||
this.autoScrollBtn.classList.toggle('bg-cyan-500/20', this.autoScroll);
|
||||
this.autoScrollBtn.classList.toggle('text-cyan-400', this.autoScroll);
|
||||
});
|
||||
|
||||
this.clearBtn.addEventListener('click', () => {
|
||||
this.logs = [];
|
||||
this.render();
|
||||
});
|
||||
|
||||
this.toggleBtn.addEventListener('click', () => {
|
||||
const termContainer = document.getElementById('terminalContainer');
|
||||
this.isCollapsed = !this.isCollapsed;
|
||||
if (this.isCollapsed) {
|
||||
termContainer.style.transform = 'translateY(calc(100% - 38px))';
|
||||
this.chevron.className = 'fa-solid fa-chevron-up';
|
||||
} else {
|
||||
termContainer.style.transform = 'translateY(0)';
|
||||
this.chevron.className = 'fa-solid fa-chevron-down';
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll('.log-filter-btn').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
document.querySelectorAll('.log-filter-btn').forEach(b => b.classList.remove('active'));
|
||||
e.target.classList.add('active');
|
||||
this.activeFilter = e.target.dataset.filter;
|
||||
this.render();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
addLog(level, message, stage = 'SYSTEM', timestamp = null) {
|
||||
const timeStr = timestamp || new Date().toISOString().split('T')[1].slice(0, 12);
|
||||
const logItem = { level, message, stage, timestamp: timeStr };
|
||||
|
||||
this.logs.push(logItem);
|
||||
if (this.logs.length > 500) this.logs.shift();
|
||||
|
||||
if (this.shouldDisplay(logItem)) {
|
||||
this.appendLogElement(logItem);
|
||||
}
|
||||
|
||||
this.badgeCount.textContent = `${this.logs.length} Logs`;
|
||||
}
|
||||
|
||||
shouldDisplay(log) {
|
||||
if (this.activeFilter === 'ALL') return true;
|
||||
return log.level === this.activeFilter;
|
||||
}
|
||||
|
||||
appendLogElement(log) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'log-line';
|
||||
|
||||
let badgeClass = 'badge-info';
|
||||
if (log.level === 'HARNESS') badgeClass = 'badge-harness';
|
||||
else if (log.level === 'REWARD') badgeClass = 'badge-reward';
|
||||
else if (log.level === 'WARN') badgeClass = 'badge-warn';
|
||||
else if (log.level === 'ERROR') badgeClass = 'badge-error';
|
||||
|
||||
div.innerHTML = `
|
||||
<span class="log-timestamp">[${log.timestamp}]</span>
|
||||
<span class="px-1.5 py-0.2 text-[9px] font-bold rounded uppercase ${badgeClass}">${log.level}</span>
|
||||
<span class="text-slate-500 font-bold">[${log.stage}]</span>
|
||||
<span class="text-slate-300 font-mono">${this.escapeHtml(log.message)}</span>
|
||||
`;
|
||||
|
||||
this.container.appendChild(div);
|
||||
|
||||
if (this.autoScroll) {
|
||||
this.container.scrollTop = this.container.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
this.container.innerHTML = '';
|
||||
this.logs.filter(log => this.shouldDisplay(log)).forEach(log => this.appendLogElement(log));
|
||||
this.badgeCount.textContent = `${this.logs.length} Logs`;
|
||||
}
|
||||
|
||||
escapeHtml(str) {
|
||||
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user