Files
p5g-marvis/ai-slides.html
2026-04-23 13:50:31 -05:00

349 lines
18 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>P5G Marvis AI — Architecture</title>
<style>
:root {
--bg: #0c0f1a;
--surface: #131825;
--card: #1a2035;
--border: #252e48;
--text: #e2e8f0;
--muted: #64748b;
--purple: #7c3aed;
--blue: #3b82f6;
--green: #10b981;
--yellow: #f59e0b;
--red: #ef4444;
--font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; background: #07090f; font-family: var(--font); }
/* ── Deck navigation ──────────────────────────────────────────── */
.deck { width: 100%; height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 16px; }
.slide { display: none; width: 1100px; aspect-ratio: 16/9; background: var(--bg); border: 1px solid var(--border); border-radius: 18px; overflow: hidden; box-shadow: 0 30px 80px rgba(0,0,0,.7); position: relative; }
.slide.active { display: flex; flex-direction: column; }
.nav { display: flex; gap: 12px; align-items: center; }
.nav button { background: var(--card); border: 1px solid var(--border); color: var(--text); padding: 8px 22px; border-radius: 8px; font-size: 13px; cursor: pointer; font-family: var(--font); transition: all .15s; }
.nav button:hover { border-color: var(--blue); color: #fff; }
.nav button:disabled { opacity: .3; cursor: default; border-color: var(--border); }
.slide-indicator { color: var(--muted); font-size: 12px; min-width: 60px; text-align: center; }
/* ── Shared layout ──────────────────────────────────────────── */
.slide-header { padding: 28px 40px 0; display: flex; align-items: center; gap: 14px; flex-shrink: 0; }
.slide-pretitle { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .14em; color: var(--purple); }
.slide-title { font-size: 26px; font-weight: 800; letter-spacing: -.02em; color: var(--text); margin-top: 4px; }
.slide-subtitle { font-size: 13px; color: var(--muted); margin-top: 4px; }
.slide-body { flex: 1; padding: 22px 40px 28px; display: flex; gap: 20px; overflow: hidden; }
/* ── Slide 1 layout ──────────────────────────────────────────── */
#s1 .slide-body { flex-direction: column; gap: 16px; }
.flow { display: flex; align-items: stretch; gap: 0; flex: 1; }
.flow-col { display: flex; flex-direction: column; gap: 0; }
/* Nodes */
.node {
background: var(--card); border: 1px solid var(--border); border-radius: 10px;
padding: 10px 14px; position: relative; display: flex; flex-direction: column;
justify-content: center; font-size: 12px;
}
.node-label { font-size: 9px; font-weight: 700; text-transform: uppercase; letter-spacing: .1em; color: var(--muted); margin-bottom: 4px; }
.node-name { font-size: 13px; font-weight: 700; color: var(--text); }
.node-sub { font-size: 10px; color: var(--muted); margin-top: 2px; }
.node.purple { border-color: rgba(124,58,237,.5); background: rgba(124,58,237,.08); }
.node.blue { border-color: rgba(59,130,246,.5); background: rgba(59,130,246,.08); }
.node.green { border-color: rgba(16,185,129,.5); background: rgba(16,185,129,.08); }
.node.yellow { border-color: rgba(245,158,11,.5); background: rgba(245,158,11,.08); }
.node.red { border-color: rgba(239,68,68,.5); background: rgba(239,68,68,.08); }
/* Arrows */
.arrow { display: flex; align-items: center; justify-content: center; padding: 0 6px; color: var(--muted); font-size: 15px; flex-shrink: 0; position: relative; }
.arrow-label { position: absolute; top: -11px; font-size: 8.5px; font-weight: 600; color: var(--blue); white-space: nowrap; letter-spacing: .03em; }
.arrow-label.below { top: auto; bottom: -11px; }
.arrow svg { width: 20px; height: 20px; }
/* Main flow row */
.flow-row { display: flex; align-items: center; gap: 0; }
/* Context box */
.ctx-box {
border: 1px dashed var(--border); border-radius: 10px; padding: 10px 14px;
display: flex; gap: 10px; align-items: center; flex-wrap: wrap;
background: rgba(255,255,255,.02);
}
.ctx-tag {
font-size: 10px; padding: 3px 9px; border-radius: 20px; font-weight: 600;
background: var(--card); border: 1px solid var(--border); color: var(--muted);
display: flex; align-items: center; gap: 5px;
}
.ctx-tag b { color: var(--text); }
.ctx-label { font-size: 9px; text-transform: uppercase; letter-spacing: .1em; color: var(--muted); font-weight: 700; margin-bottom: 5px; }
/* ── Slide 2 layout ──────────────────────────────────────────── */
#s2 .slide-body { display: grid; grid-template-columns: 1fr 1px 1fr; gap: 0; }
.divider { background: var(--border); }
.s2-col { padding: 0 28px; display: flex; flex-direction: column; gap: 12px; }
.s2-col:first-child { padding-left: 0; }
.s2-col:last-child { padding-right: 0; }
.s2-section { font-size: 9px; font-weight: 700; text-transform: uppercase; letter-spacing: .12em; color: var(--purple); margin-bottom: 4px; }
.list-item { display: flex; gap: 10px; align-items: flex-start; }
.list-icon { font-size: 16px; flex-shrink: 0; width: 22px; text-align: center; margin-top: 1px; }
.list-text { flex: 1; }
.list-title { font-size: 13px; font-weight: 700; color: var(--text); }
.list-desc { font-size: 11px; color: var(--muted); margin-top: 2px; line-height: 1.5; }
.code-block {
background: #0a0d16; border: 1px solid var(--border); border-radius: 8px;
padding: 10px 14px; font-family: 'SF Mono', 'Fira Code', Consolas, monospace;
font-size: 10.5px; line-height: 1.7; color: #94a3b8; overflow: hidden;
}
.code-block .k { color: #7c3aed; }
.code-block .s { color: #10b981; }
.code-block .c { color: #4b5563; font-style: italic; }
.code-block .v { color: #f59e0b; }
.kv-row { display: flex; justify-content: space-between; align-items: center; padding: 5px 0; border-bottom: 1px solid var(--border); }
.kv-row:last-child { border-bottom: none; }
.kv-key { font-size: 11px; color: var(--muted); }
.kv-val { font-size: 11px; font-weight: 700; color: var(--text); }
.kv-val.green { color: var(--green); }
.kv-val.yellow { color: var(--yellow); }
.kv-val.blue { color: var(--blue); }
.kv-val.purple { color: var(--purple); }
/* Slide number watermark */
.slide-num { position: absolute; bottom: 16px; right: 22px; font-size: 10px; color: var(--border); font-weight: 600; }
/* Gradient accent line at top */
.slide::before { content:''; position:absolute; top:0; left:0; right:0; height:3px; background: linear-gradient(90deg, var(--purple), var(--blue), var(--green)); border-radius: 18px 18px 0 0; }
.badge { display: inline-flex; align-items: center; gap: 5px; font-size: 10px; font-weight: 700; padding: 3px 10px; border-radius: 20px; }
.badge.purple { background: rgba(124,58,237,.15); color: var(--purple); border: 1px solid rgba(124,58,237,.4); }
.badge.green { background: rgba(16,185,129,.12); color: var(--green); border: 1px solid rgba(16,185,129,.4); }
.badge.blue { background: rgba(59,130,246,.12); color: var(--blue); border: 1px solid rgba(59,130,246,.4); }
</style>
</head>
<body>
<div class="deck">
<!-- ═══════════════════ SLIDE 1 — Architecture ═════════════════════ -->
<div class="slide active" id="s1">
<div class="slide-header">
<div>
<div class="slide-pretitle">P5G Marvis · AI Integration</div>
<div class="slide-title">End-to-End Request Flow</div>
<div class="slide-subtitle">How a natural-language query becomes a network-aware AI response</div>
</div>
<div style="margin-left:auto;display:flex;gap:8px">
<span class="badge purple">On-Prem LLM</span>
<span class="badge green">No data leaves the network</span>
</div>
</div>
<div class="slide-body">
<!-- Main flow -->
<div class="flow-row" style="gap:0;align-items:stretch">
<!-- NCM Browser -->
<div class="node purple" style="width:155px;flex-shrink:0">
<div class="node-label">1 · Operator</div>
<div class="node-name">NCM Browser</div>
<div class="node-sub">React SPA (HPE NCM)</div>
<div class="node-sub" style="margin-top:6px;font-size:9px;color:#7c3aed">P5G Marvis AI pane<br>loaded in iframe</div>
</div>
<div class="arrow">
<span class="arrow-label">HTTPS query</span>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M13 6l6 6-6 6"/></svg>
</div>
<!-- Traefik -->
<div class="node blue" style="width:140px;flex-shrink:0">
<div class="node-label">2 · Proxy</div>
<div class="node-name">Traefik</div>
<div class="node-sub">172.27.0.159</div>
<div class="node-sub" style="margin-top:6px;font-size:9px;color:#3b82f6">/core/marvis/*<br>→ :8100 strip prefix</div>
</div>
<div class="arrow">
<span class="arrow-label">POST /api/query</span>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M13 6l6 6-6 6"/></svg>
</div>
<!-- FastAPI -->
<div class="node blue" style="width:170px;flex-shrink:0">
<div class="node-label">3 · Backend</div>
<div class="node-name">p5g-marvis</div>
<div class="node-sub">FastAPI · :8100</div>
<div class="node-sub" style="margin-top:6px;font-size:9px;color:#3b82f6">Fetches live NF status<br>+ active alerts<br>→ builds system prompt</div>
</div>
<div class="arrow">
<span class="arrow-label">OpenAI-compat API</span>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M13 6l6 6-6 6"/></svg>
</div>
<!-- LLM -->
<div class="node green" style="flex:1">
<div class="node-label">4 · Inference</div>
<div class="node-name">llama.cpp server</div>
<div class="node-sub">172.27.0.135:8001 · HTTPS · self-signed TLS</div>
<div style="margin-top:7px;display:flex;gap:6px;flex-wrap:wrap">
<span style="font-size:9px;background:rgba(16,185,129,.12);border:1px solid rgba(16,185,129,.35);color:#10b981;padding:2px 7px;border-radius:10px">Gemma 4 · 26B</span>
<span style="font-size:9px;background:rgba(16,185,129,.12);border:1px solid rgba(16,185,129,.35);color:#10b981;padding:2px 7px;border-radius:10px">Q4_K_S quant</span>
<span style="font-size:9px;background:rgba(16,185,129,.12);border:1px solid rgba(16,185,129,.35);color:#10b981;padding:2px 7px;border-radius:10px">Reasoning model</span>
</div>
</div>
</div>
<!-- Context enrichment box -->
<div>
<div class="ctx-label">Context injected into system prompt by p5g-marvis before every LLM call</div>
<div class="ctx-box">
<div class="ctx-tag"><span>📡</span> <b>12 NF states</b> (UDR, AMF, SMF, UPF…)</div>
<div class="ctx-tag"><span>🔴</span> <b>Active alerts</b> (name, severity, summary)</div>
<div class="ctx-tag"><span>🕐</span> <b>Timestamp</b></div>
<div class="ctx-tag"><span>📝</span> <b>User query</b> (natural language)</div>
<div style="flex:1"></div>
<div style="font-size:9px;color:var(--muted);text-align:right;line-height:1.6">
No training data leaves the site.<br>
Context window: ~1 024 tokens out · 120 s timeout.
</div>
</div>
</div>
<!-- Return path note -->
<div style="display:flex;align-items:center;gap:8px;font-size:10px;color:var(--muted)">
<span style="color:var(--green);font-weight:700">↩ Response path:</span>
LLM generates markdown analysis (via <code style="font-size:9.5px;background:#0a0d16;padding:1px 5px;border-radius:3px;color:#7c3aed">content</code> or <code style="font-size:9.5px;background:#0a0d16;padding:1px 5px;border-radius:3px;color:#7c3aed">reasoning_content</code> field) → FastAPI returns JSON → iframe renders markdown → operator reads actionable insight
</div>
</div>
<div class="slide-num">1 / 2</div>
</div>
<!-- ═══════════════════ SLIDE 2 — Components ══════════════════════ -->
<div class="slide" id="s2">
<div class="slide-header">
<div>
<div class="slide-pretitle">P5G Marvis · AI Integration</div>
<div class="slide-title">Configuration &amp; Design Choices</div>
<div class="slide-subtitle">How the integration is wired, and why</div>
</div>
<div style="margin-left:auto;display:flex;gap:8px">
<span class="badge blue">172.27.0.159 only</span>
<span class="badge purple">Rule-based fallback</span>
</div>
</div>
<div class="slide-body">
<!-- LEFT: Design choices -->
<div class="s2-col">
<div class="s2-section">Design Choices</div>
<div class="list-item">
<div class="list-icon">🔒</div>
<div class="list-text">
<div class="list-title">Fully air-gapped inference</div>
<div class="list-desc">LLM at 172.27.0.135 stays inside the private 5G network. No cloud API keys, no data egress. Self-signed TLS with verify=False for local trust boundary.</div>
</div>
</div>
<div class="list-item">
<div class="list-icon">🧠</div>
<div class="list-text">
<div class="list-title">Context-enriched prompt engineering</div>
<div class="list-desc">Every request carries live NF state and alert data. The model never sees a bare question — it always gets the full network picture, so answers are grounded in real telemetry.</div>
</div>
</div>
<div class="list-item">
<div class="list-icon"></div>
<div class="list-text">
<div class="list-title">Reasoning model handling</div>
<div class="list-desc">Gemma 4 returns a <code style="font-size:10px;background:#0a0d16;padding:1px 5px;border-radius:3px;color:#7c3aed">reasoning_content</code> field when <code style="font-size:10px;background:#0a0d16;padding:1px 5px;border-radius:3px;color:#7c3aed">content</code> is empty. The backend falls back gracefully so the thinking trace is surfaced rather than dropped.</div>
</div>
</div>
<div class="list-item">
<div class="list-icon">🛡️</div>
<div class="list-text">
<div class="list-title">Rule-based fallback</div>
<div class="list-desc">If the LLM is unreachable or times out, the backend falls through to a deterministic rule engine that still returns a formatted, accurate network health summary.</div>
</div>
</div>
</div>
<div class="divider"></div>
<!-- RIGHT: Config + key params -->
<div class="s2-col">
<div class="s2-section">Runtime Configuration (systemd env)</div>
<div class="code-block">
<span class="c"># /etc/systemd/system/p5g-marvis.service</span>
<span class="k">Environment</span>=<span class="v">MARVIS_AI_MODE</span>=<span class="s">openai</span>
<span class="k">Environment</span>=<span class="v">MARVIS_OPENAI_BASE_URL</span>=<span class="s">https://172.27.0.135:8001</span>
<span class="k">Environment</span>=<span class="v">MARVIS_OPENAI_MODEL</span>=<span class="s">gemma-4-26B-A4B-it-UD-Q4_K_S.gguf</span>
</div>
<div class="s2-section" style="margin-top:8px">Key Parameters</div>
<div style="background:var(--card);border:1px solid var(--border);border-radius:8px;padding:8px 14px;">
<div class="kv-row"><span class="kv-key">LLM endpoint</span><span class="kv-val blue">172.27.0.135:8001</span></div>
<div class="kv-row"><span class="kv-key">API format</span><span class="kv-val">/v1/chat/completions</span></div>
<div class="kv-row"><span class="kv-key">Auth header</span><span class="kv-val green">None (skipped if key empty)</span></div>
<div class="kv-row"><span class="kv-key">TLS verify</span><span class="kv-val yellow">Disabled (self-signed)</span></div>
<div class="kv-row"><span class="kv-key">max_tokens</span><span class="kv-val">1 024</span></div>
<div class="kv-row"><span class="kv-key">Timeout</span><span class="kv-val">120 s</span></div>
<div class="kv-row"><span class="kv-key">Hosts with LLM mode</span><span class="kv-val purple">172.27.0.159 only</span></div>
<div class="kv-row"><span class="kv-key">192.168.86.173 mode</span><span class="kv-val green">rule (deterministic)</span></div>
</div>
<div class="s2-section" style="margin-top:8px">Routing (Traefik)</div>
<div style="background:var(--card);border:1px solid var(--border);border-radius:8px;padding:8px 14px;">
<div class="kv-row"><span class="kv-key">NCM sidebar inject</span><span class="kv-val blue">patch-ncm.py → JS bundle</span></div>
<div class="kv-row"><span class="kv-key">Marvis iframe path</span><span class="kv-val">/core/marvis/ → :8100</span></div>
<div class="kv-row"><span class="kv-key">AI sub-page</span><span class="kv-val">/core/marvis/ai</span></div>
</div>
</div>
</div>
<div class="slide-num">2 / 2</div>
</div>
<!-- Navigation -->
<div class="nav">
<button id="prev" onclick="go(-1)" disabled>← Prev</button>
<span class="slide-indicator" id="indicator">1 / 2</span>
<button id="next" onclick="go(1)">Next →</button>
</div>
</div>
<script>
const slides = document.querySelectorAll('.slide');
let cur = 0;
function go(d) {
slides[cur].classList.remove('active');
cur = Math.max(0, Math.min(slides.length - 1, cur + d));
slides[cur].classList.add('active');
document.getElementById('indicator').textContent = `${cur+1} / ${slides.length}`;
document.getElementById('prev').disabled = cur === 0;
document.getElementById('next').disabled = cur === slides.length - 1;
}
document.addEventListener('keydown', e => {
if (e.key === 'ArrowRight') go(1);
if (e.key === 'ArrowLeft') go(-1);
});
</script>
</body>
</html>