limina-engine / report_template.html
sdawdsdw's picture
Upload 8 files
31f2a28 verified
Raw
History Blame Contribute Delete
42.4 kB
<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Agent DAG Inspector — Trajectory Debug</title>
<script src="https://cdn.tailwindcss.com?plugins=typography"></script>
<script src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<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=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
mono: ['JetBrains Mono', 'monospace']
}
}
}
}
</script>
<style>
:root {
--bg-base: #09090b;
--bg-elevated: #131316;
--bg-card: #1a1a1e;
--border: #27272a;
--border-strong: #3f3f46;
--text-primary: #fafafa;
--text-secondary: #a1a1aa;
--text-muted: #71717a;
--accent-emerald: #10b981;
--accent-red: #ef4444;
--accent-amber: #f59e0b;
--accent-blue: #3b82f6;
}
* { box-sizing: border-box; }
html, body { height: 100%; }
body {
background: var(--bg-base);
color: var(--text-primary);
font-family: 'Inter', system-ui, sans-serif;
overflow: hidden;
-webkit-font-smoothing: antialiased;
}
body::before {
content: '';
position: fixed;
inset: 0;
background-image:
radial-gradient(circle at 18% 8%, rgba(59, 130, 246, 0.05), transparent 42%),
radial-gradient(circle at 82% 92%, rgba(16, 185, 129, 0.04), transparent 42%);
pointer-events: none;
z-index: 0;
}
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #27272a; border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: #3f3f46; }
#graph-container { width: 100%; height: 100%; }
.vis-network { outline: none; }
.vis-network canvas { cursor: grab; }
.drawer {
transform: translateX(100%);
transition: transform 340ms cubic-bezier(0.32, 0.72, 0, 1);
box-shadow: -20px 0 60px -20px rgba(0,0,0,0.6);
}
.drawer.open { transform: translateX(0); }
.drawer-backdrop {
opacity: 0;
pointer-events: none;
transition: opacity 320ms ease;
}
.drawer-backdrop.open { opacity: 1; pointer-events: auto; }
@keyframes pulse-dot {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.55; transform: scale(0.85); }
}
.pulse-dot { animation: pulse-dot 2s ease-in-out infinite; }
.health-ring circle { transition: stroke-dashoffset 1.1s cubic-bezier(0.16, 1, 0.3, 1), stroke 400ms ease; }
.stat-card { transition: border-color 200ms ease, background-color 200ms ease; }
.stat-card:hover { border-color: var(--border-strong); background-color: rgba(255,255,255,0.015); }
.badge-healthy { background: rgba(16, 185, 129, 0.1); color: #34d399; border: 1px solid rgba(16, 185, 129, 0.25); }
.badge-error { background: rgba(239, 68, 68, 0.1); color: #f87171; border: 1px solid rgba(239, 68, 68, 0.25); }
.badge-warning { background: rgba(245, 158, 11, 0.1); color: #fbbf24; border: 1px solid rgba(245, 158, 11, 0.25); }
.bar-track { background: rgba(255, 255, 255, 0.05); }
.bar-fill { transition: width 1s cubic-bezier(0.16, 1, 0.3, 1), background 400ms ease; }
.btn-icon { transition: background-color 150ms ease, color 150ms ease; }
.btn-icon:hover { background: rgba(255, 255, 255, 0.06); color: var(--text-primary); }
@keyframes fade-in-up {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
.drawer.open .drawer-content > * {
animation: fade-in-up 340ms ease-out backwards;
}
.drawer.open .drawer-content > *:nth-child(1) { animation-delay: 60ms; }
.drawer.open .drawer-content > *:nth-child(2) { animation-delay: 110ms; }
.drawer.open .drawer-content > *:nth-child(3) { animation-delay: 160ms; }
.drawer.open .drawer-content > *:nth-child(4) { animation-delay: 210ms; }
.drawer.open .drawer-content > *:nth-child(5) { animation-delay: 260ms; }
.empty-state {
display: flex; align-items: center; justify-content: center;
flex-direction: column; height: 100%; color: var(--text-muted);
}
.line-clamp-2 {
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
overflow: hidden;
}
.modal-enter { animation: modal-in 220ms cubic-bezier(0.16, 1, 0.3, 1); }
@keyframes modal-in {
from { opacity: 0; transform: scale(0.97) translateY(8px); }
to { opacity: 1; transform: scale(1) translateY(0); }
}
</style>
</head>
<body class="text-zinc-100">
<div class="relative z-10 h-screen flex flex-col">
<!-- ============ TOP BAR / EXECUTIVE SUMMARY ============ -->
<header class="border-b border-zinc-800/80 bg-zinc-950/80 backdrop-blur-xl flex-shrink-0">
<div class="flex items-center justify-between px-6 py-3 border-b border-zinc-800/50">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded-lg bg-gradient-to-br from-emerald-500 to-blue-600 flex items-center justify-center shadow-lg shadow-emerald-500/20">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="6" cy="6" r="2"/><circle cx="18" cy="6" r="2"/>
<circle cx="6" cy="18" r="2"/><circle cx="18" cy="18" r="2"/>
<path d="M8 6h8M6 8v8M18 8v8M8 18h8"/>
</svg>
</div>
<div>
<h1 class="text-sm font-semibold tracking-tight">Limina AI — Agent DAG Inspector</h1>
<p class="text-[11px] text-zinc-500 font-mono">trajectory_debug_session_4821</p>
</div>
</div>
<div class="flex items-center gap-4">
<div class="flex items-center gap-2 text-xs">
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500 pulse-dot"></span>
<span class="text-zinc-400">Live session</span>
</div>
<div class="text-xs text-zinc-500 font-mono"><span id="timestamp"></span></div>
</div>
</div>
<!-- UPGRADE: 4-Column Grid, adaugat cardul strategic Session Cost deasupra -->
<div class="grid grid-cols-4 divide-x divide-zinc-800/60">
<!-- Health Rating -->
<div class="stat-card px-6 py-4">
<div class="flex items-center justify-between mb-2">
<span class="text-[11px] uppercase tracking-wider text-zinc-500 font-medium">Health Rating</span>
<span class="text-[10px] font-mono text-zinc-600">/100</span>
</div>
<div class="flex items-center gap-4">
<div class="relative w-12 h-12">
<svg class="health-ring w-12 h-12 -rotate-90" viewBox="0 0 48 48">
<circle cx="24" cy="24" r="20" fill="none" stroke="rgba(255,255,255,0.06)" stroke-width="4"/>
<circle id="health-circle" cx="24" cy="24" r="20" fill="none" stroke="#10b981" stroke-width="4" stroke-linecap="round" stroke-dasharray="125.6" stroke-dashoffset="125.6"/>
</svg>
<div class="absolute inset-0 flex items-center justify-center">
<span id="health-value" class="text-sm font-semibold text-emerald-400 tabular-nums">0</span>
</div>
</div>
<div class="flex-1 min-w-0">
<div id="health-label" class="text-sm font-semibold text-zinc-100"></div>
<div id="health-sub" class="text-xs text-zinc-500 truncate">Analyzing...</div>
</div>
</div>
</div>
<!-- Success Rate -->
<div class="stat-card px-6 py-4">
<div class="flex items-center justify-between mb-2">
<span class="text-[11px] uppercase tracking-wider text-zinc-500 font-medium">Success Rate</span>
<span id="success-total" class="text-[10px] font-mono text-zinc-600">— nodes</span>
</div>
<div class="flex items-baseline gap-2 mb-2">
<span id="success-value" class="text-2xl font-bold tabular-nums">0%</span>
</div>
<div class="bar-track h-1.5 rounded-full overflow-hidden">
<div id="success-bar" class="bar-fill h-full bg-gradient-to-r from-emerald-500 to-emerald-400 rounded-full" style="width: 0%"></div>
</div>
</div>
<!-- UPGRADE: Session Cost Card (Amplasament strategic de business) -->
<div class="stat-card px-6 py-4">
<div class="flex items-center justify-between mb-2">
<span class="text-[11px] uppercase tracking-wider text-zinc-500 font-medium">Session Cost</span>
<span id="session-tokens" class="text-[10px] font-mono text-zinc-400">0 tokens</span>
</div>
<div class="flex items-baseline gap-2 mb-1">
<span id="session-cost-value" class="text-2xl font-bold text-emerald-400 tabular-nums">$0.00000</span>
<span class="text-[10px] text-zinc-500">simulated USD</span>
</div>
<div class="text-[10px] text-zinc-500 font-mono">Based on standard B2B rates</div>
</div>
<!-- Actionable Advice -->
<div class="stat-card px-6 py-4">
<div class="flex items-center justify-between mb-2">
<span class="text-[11px] uppercase tracking-wider text-zinc-500 font-medium">Actionable Advice</span>
<span class="badge-warning text-[10px] px-1.5 py-0.5 rounded font-medium">PRIORITY</span>
</div>
<p id="advice-text" class="text-xs text-zinc-300 leading-relaxed line-clamp-2">No advice available.</p>
<button id="advice-expand" class="mt-1.5 text-[11px] text-blue-400 hover:text-blue-300 transition-colors">View full analysis →</button>
</div>
</div>
</header>
<!-- ============ MAIN AREA ============ -->
<main class="flex-1 flex relative overflow-hidden">
<!-- Graph panel -->
<section class="flex-1 flex flex-col relative">
<!-- Toolbar -->
<div class="flex items-center justify-between px-4 py-2 border-b border-zinc-800/60 bg-zinc-950/40">
<div class="flex items-center gap-3">
<span class="text-xs text-zinc-400 font-medium pl-2">Trajectory Graph</span>
<span class="text-zinc-700">·</span>
<!-- Dropdown dinamic pentru selectarea sesiunilor -->
<select id="session-select" class="bg-zinc-900 border border-zinc-800 text-xs text-zinc-300 rounded-md px-2.5 py-1 focus:outline-none focus:border-zinc-700 cursor-pointer font-mono"></select>
<span class="text-zinc-700">·</span>
<span id="node-count" class="text-xs text-zinc-500 font-mono">0 nodes</span>
<span class="text-zinc-700">·</span>
<span id="edge-count" class="text-xs text-zinc-500 font-mono">0 edges</span>
</div>
<div class="flex items-center gap-1">
<button id="zoom-fit" class="btn-icon text-zinc-400 p-1.5 rounded-md" title="Fit to view">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 7V5a2 2 0 0 1 2-2h2M17 3h2a2 2 0 0 1 2 2v2M21 17v2a2 2 0 0 1-2 2h-2M7 21H5a2 2 0 0 1-2-2v-2"/>
</svg>
</button>
<button id="zoom-in" class="btn-icon text-zinc-400 p-1.5 rounded-md" title="Zoom in">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="11" y1="8" x2="11" y2="14"/><line x1="8" y1="11" x2="14" y2="11"/>
</svg>
</button>
<button id="zoom-out" class="btn-icon text-zinc-400 p-1.5 rounded-md" title="Zoom out">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="8" y1="11" x2="14" y2="11"/>
</svg>
</button>
<div class="w-px h-5 bg-zinc-800 mx-1"></div>
<button id="layout-toggle" class="btn-icon text-zinc-400 p-1.5 rounded-md" title="Toggle layout direction">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 6h18M3 12h18M3 18h18"/>
</svg>
</button>
</div>
</div>
<!-- Graph canvas -->
<div class="flex-1 relative">
<div id="graph-container"></div>
<!-- Empty state -->
<div id="empty-state" class="empty-state absolute inset-0 pointer-events-none" style="display:none">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="text-zinc-700 mb-3">
<circle cx="6" cy="6" r="2"/><circle cx="18" cy="6" r="2"/>
<circle cx="6" cy="18" r="2"/><circle cx="18" cy="18" r="2"/>
<path d="M8 6h8M6 8v8M18 8v8M8 18h8"/>
</svg>
<p class="text-sm text-zinc-500">No trajectory data loaded</p>
<p class="text-xs text-zinc-600 mt-1">Waiting for <code class="font-mono text-zinc-400">reportData</code></p>
</div>
<!-- Legend -->
<div class="absolute bottom-4 left-4 bg-zinc-950/80 backdrop-blur-md border border-zinc-800 rounded-lg p-3 text-xs">
<div class="text-[10px] uppercase tracking-wider text-zinc-500 font-medium mb-2">Legend</div>
<div class="space-y-1.5">
<div class="flex items-center gap-2">
<span class="w-2.5 h-2.5 rounded-full bg-emerald-500 ring-2 ring-emerald-500/30"></span>
<span class="text-zinc-300">Healthy</span>
<span class="text-zinc-600 text-[10px] font-mono">stable &amp; secure</span>
</div>
<div class="flex items-center gap-2">
<span class="w-2.5 h-2.5 rounded-full bg-amber-500 ring-2 ring-amber-500/30"></span>
<span class="text-zinc-300">Unstable / Warn</span>
<span class="text-zinc-600 text-[10px] font-mono">instability / low robustness</span>
</div>
<div class="flex items-center gap-2">
<span class="w-2.5 h-2.5 rounded-full bg-red-500 ring-2 ring-red-500/30"></span>
<span class="text-zinc-300">Error / Breach</span>
<span class="text-zinc-600 text-[10px] font-mono">contradiction / format fail / leak</span>
</div>
</div>
</div>
<!-- Hint -->
<div id="graph-hint" class="absolute top-4 left-1/2 -translate-x-1/2 bg-zinc-950/80 backdrop-blur-md border border-zinc-800 rounded-full px-3 py-1.5 text-xs text-zinc-400 flex items-center gap-2 transition-opacity duration-500" style="display:none">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M9 11.24V7.5a2.5 2.5 0 0 1 5 0v3.74M7 11h10v8a2 2 0 0 1-2 2H9a2 2 0 0 1-2-2v-8z"/>
</svg>
<span>Click any node to inspect diagnostics</span>
</div>
</div>
</section>
<!-- Drawer backdrop -->
<div id="drawer-backdrop" class="drawer-backdrop absolute inset-0 bg-black/40 z-20"></div>
<!-- ============ DIAGNOSTIC DRAWER ============ -->
<aside id="drawer" class="drawer absolute right-0 top-0 bottom-0 w-[420px] bg-zinc-950 border-l border-zinc-800 z-30 flex flex-col">
<div class="flex items-center justify-between px-5 py-4 border-b border-zinc-800">
<div class="flex items-center gap-2">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="text-zinc-400">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/>
</svg>
<h2 class="text-sm font-semibold tracking-tight">Node Diagnostics</h2>
</div>
<button id="drawer-close" class="btn-icon text-zinc-400 p-1.5 rounded-md" title="Close (Esc)">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
</div>
<div class="drawer-content flex-1 overflow-y-auto p-5 space-y-5">
<!-- Identity -->
<div>
<div class="flex items-center gap-2 mb-2">
<span id="drawer-status-badge" class="badge-healthy text-[10px] px-2 py-0.5 rounded-full font-medium uppercase tracking-wider">Healthy</span>
<span id="drawer-type" class="text-[11px] text-zinc-500 font-mono">type</span>
</div>
<h3 id="drawer-label" class="text-lg font-semibold text-zinc-100"></h3>
<div class="flex items-center gap-2 mt-1">
<span class="text-[11px] text-zinc-600 font-mono">id:</span>
<code id="drawer-id" class="text-[11px] text-zinc-400 font-mono"></code>
</div>
</div>
<!-- UPGRADE: 3-Column Metrics (Exec Time, Instability, Cost/Tokens) -->
<div class="grid grid-cols-3 gap-1.5">
<div class="bg-zinc-900/50 border border-zinc-800 rounded-lg p-2.5">
<div class="text-[9px] uppercase tracking-wider text-zinc-500 font-medium mb-1">Exec Time</div>
<div class="flex items-baseline gap-0.5">
<span id="drawer-exec-time" class="text-sm font-semibold tabular-nums text-zinc-100"></span>
<span class="text-[9px] text-zinc-500">ms</span>
</div>
</div>
<div class="bg-zinc-900/50 border border-zinc-800 rounded-lg p-2.5">
<div class="text-[9px] uppercase tracking-wider text-zinc-500 font-medium mb-1">Instability</div>
<div class="flex items-baseline gap-0.5">
<span id="drawer-instability" class="text-sm font-semibold tabular-nums text-zinc-100"></span>
<span class="text-[9px] text-zinc-500">idx</span>
</div>
<div class="mt-2 bar-track h-1 rounded-full overflow-hidden">
<div id="drawer-instability-bar" class="bar-fill h-full rounded-full" style="width: 0%"></div>
</div>
</div>
<div class="bg-zinc-900/50 border border-zinc-800 rounded-lg p-2.5">
<div class="text-[9px] uppercase tracking-wider text-zinc-500 font-medium mb-1">Session Cost</div>
<div class="flex flex-col">
<span id="drawer-cost" class="text-sm font-semibold text-emerald-400 tabular-nums">$0.00</span>
<span id="drawer-tokens" class="text-[9px] text-zinc-500 font-mono">0 tokens</span>
</div>
</div>
</div>
<!-- Validation errors list -->
<div id="drawer-error-section" class="hidden">
<div class="text-[10px] uppercase tracking-wider text-zinc-500 font-medium mb-2">Validation Errors</div>
<div id="drawer-error" class="space-y-2"></div>
</div>
<!-- Node text with 1-Click Copy -->
<div>
<div class="flex items-center justify-between mb-2">
<span class="text-[10px] uppercase tracking-wider text-zinc-500 font-medium">Node Output / Text</span>
<!-- UPGRADE: 1-Click Copy Button -->
<button id="copy-payload" class="flex items-center gap-1.5 text-[11px] text-blue-400 hover:text-blue-300 transition-colors">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
</svg>
<span id="copy-text">Copy</span>
</button>
</div>
<div class="bg-zinc-900/50 border border-zinc-800 rounded-lg p-3">
<p id="drawer-text" class="text-xs text-zinc-300 leading-relaxed font-mono break-words select-all"></p>
</div>
</div>
<!-- Connected edges -->
<div>
<div class="flex items-center justify-between mb-2">
<span class="text-[10px] uppercase tracking-wider text-zinc-500 font-medium">Connected Edges</span>
<span id="drawer-edges-count" class="text-[10px] text-zinc-600 font-mono">0</span>
</div>
<div id="drawer-edges" class="space-y-2"></div>
</div>
</div>
</aside>
</main>
</div>
<!-- Advice modal -->
<div id="advice-modal" class="fixed inset-0 z-50 hidden items-center justify-center p-6 bg-black/60 backdrop-blur-sm">
<div class="modal-enter bg-zinc-950 border border-zinc-800 rounded-xl max-w-xl w-full max-h-[85vh] flex flex-col p-6 shadow-2xl">
<div class="flex items-start justify-between mb-4">
<div>
<h3 class="text-base font-semibold">Diagnostic Analysis</h3>
<p class="text-xs text-zinc-500 mt-0.5">Generated locally by Limina Cognitive Engine</p>
</div>
<button id="advice-close" class="btn-icon text-zinc-400 p-1.5 rounded-md">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
</div>
<!-- UPGRADE: Clasa prose si prose-invert pentru formatarea corecta a Markdown-ului -->
<div id="advice-full" class="text-sm text-zinc-300 leading-relaxed prose prose-invert max-w-none overflow-y-auto flex-1 pr-2 prose-p:text-sm prose-li:text-sm prose-code:text-xs prose-code:bg-zinc-900 prose-code:px-1 prose-code:py-0.5 prose-code:rounded"></div>
</div>
</div>
<script>
// =====================================================================
// PLACEHOLDER — populated externally by the report generator.
// Expected shape:
// reportData = {
// executive_summary: { health_rating, success_rate, actionable_advice, total_nodes, errors_detected },
// enriched_graph: { nodes: [...], edges: [...] }
// }
// =====================================================================
const reportData = /* INSERT_REPORT_DATA_HERE */;
// ---- Demo payload (ONLY runs if reportData is empty) ----
if (!reportData || Object.keys(reportData).length === 0) {
Object.assign(reportData, {
executive_summary: {
health_rating: 72,
success_rate: 71.4,
actionable_advice: "Node 'validator' shows critical instability (24.8)...",
total_nodes: 7,
errors_detected: 2
},
enriched_graph: {
nodes: [
{ id: "plan", type: "planner", label: "Planner", text: "Decomposed user query...", execution_time_ms: 142, instability_index: 4.2, validation_error: null },
{ id: "validate", type: "validator", label: "Validator", text: "NLI consistency check...", execution_time_ms: 95, instability_index: 24.8, validation_error: "ContradictionDetected" }
],
edges: [
{ from: "plan", to: "validate", drift_score: 0.67, nli_label: "contradiction" }
]
}
});
}
// =================== Application ===================
let network = null;
let nodesDataset = null;
let edgesDataset = null;
let layoutVertical = true;
let hintDismissed = false;
let currentSessionIdx = 0;
const $ = (sel) => document.querySelector(sel);
const drawer = $('#drawer');
const drawerBackdrop = $('#drawer-backdrop');
const emptyState = $('#empty-state');
const graphHint = $('#graph-hint');
// UPGRADE: Evaluam starea nodului pe baza listei complete de failures din sesiune (SOTA)
function getNodeStatus(node) {
const session = reportData.results[currentSessionIdx];
if (!session) return 'healthy';
// 1. Daca are o eroare critica de conformitate, formatare, securitate sau contradictie
const hasCriticalFailure = session.failures.some(f =>
f.to_node === node.id &&
f.failure_type !== "SEMANTIC_INSTABILITY" &&
f.failure_type !== "LOW_ROBUSTNESS"
);
if (hasCriticalFailure || node.validation_error) {
return 'error';
}
// 2. Daca are o eroare de instabilitate sau robustete scazuta (atribuita acum nodului corect USER sau AGENT)
const hasWarningFailure = session.failures.some(f =>
(f.to_node === node.id || f.from_node === node.id) &&
(f.failure_type === "SEMANTIC_INSTABILITY" || f.failure_type === "LOW_ROBUSTNESS")
);
if (hasWarningFailure || (node.instability_index != null && node.instability_index >= 15)) {
return 'warning';
}
return 'healthy';
}
function statusColor(s) {
return ({ healthy: '#10b981', error: '#ef4444', warning: '#f59e0b' })[s];
}
function nliColor(label) {
return ({ entailment: '#10b981', neutral: '#a1a1aa', contradiction: '#ef4444' })[label] || '#a1a1aa';
}
function driftColor(score) {
if (score < 0.15) return '#10b981';
if (score < 0.25) return '#fbbf24';
if (score < 0.40) return '#f59e0b';
return '#ef4444';
}
function escapeHtml(s) {
const div = document.createElement('div');
div.textContent = String(s ?? '');
return div.innerHTML;
}
// ===== Executive summary render =====
function renderSummary() {
const s = reportData.executive_summary;
if (!s) return;
const health = Math.max(0, Math.min(100, s.health_rating || 0));
const circumference = 2 * Math.PI * 20;
const offset = circumference * (1 - health / 100);
const ring = $('#health-circle');
ring.setAttribute('stroke-dasharray', circumference);
ring.setAttribute('stroke-dashoffset', offset);
let color = '#10b981', label = 'Excellent';
if (health < 50) { color = '#ef4444'; label = 'Critical'; }
else if (health < 75) { color = '#f59e0b'; label = 'Degraded'; }
else if (health < 90) { color = '#10b981'; label = 'Good'; }
ring.setAttribute('stroke', color);
const hv = $('#health-value');
hv.textContent = health;
hv.style.color = color;
$('#health-label').textContent = label;
$('#health-sub').textContent = `${s.errors_detected || 0} validation issues detected`;
const sr = s.success_rate_percentage != null ? s.success_rate_percentage : s.success_rate || 0;
$('#success-value').textContent = sr.toFixed(1) + '%';
$('#success-bar').style.width = sr + '%';
$('#success-total').textContent = `${s.total_nodes || 0} nodes`;
let barColor = 'from-emerald-500 to-emerald-400';
if (sr < 50) barColor = 'from-red-500 to-red-400';
else if (sr < 75) barColor = 'from-amber-500 to-amber-400';
$('#success-bar').className = `bar-fill h-full bg-gradient-to-r ${barColor} rounded-full`;
$('#advice-text').textContent = s.actionable_advice || 'No advice available.';
}
// ===== Dropdown session population =====
function populateSessionSelect() {
const select = $('#session-select');
if (!select || !reportData.results) return;
select.innerHTML = '';
reportData.results.forEach((r, idx) => {
const opt = document.createElement('option');
opt.value = idx;
// UPGRADE: "Red Dot Indicator" - Adaugam automat emoji 🔴 sau 🟢 in functie de statusul sesiunii!
const statusEmoji = r.status === "FAILED" ? "🔴" : "🟢";
opt.textContent = `${statusEmoji} ${r.session_id} (${r.status})`;
select.appendChild(opt);
});
select.addEventListener('change', (e) => {
currentSessionIdx = parseInt(e.target.value);
closeDrawer();
renderGraph();
});
}
// ===== Graph render =====
function renderGraph() {
const session = reportData.results[currentSessionIdx];
const g = session ? session.enriched_graph : null;
if (!g || !g.nodes || g.nodes.length === 0) {
emptyState.style.display = 'flex';
graphHint.style.display = 'none';
return;
}
emptyState.style.display = 'none';
if (!hintDismissed) graphHint.style.display = 'flex';
const visNodes = g.nodes.map(n => {
const status = getNodeStatus(n);
const color = statusColor(status);
return {
id: n.id,
label: n.label || n.id,
shape: 'dot',
size: 18,
color: {
background: color,
border: color,
highlight: { background: color, border: '#ffffff' },
hover: { background: color, border: '#ffffff' }
},
font: { color: '#e4e4e7', size: 13, face: 'Inter', strokeWidth: 0 },
borderWidth: 2,
shadow: { enabled: true, color: color + '55', size: 14, x: 0, y: 0 },
_data: n
};
});
const visEdges = g.edges.map((e, i) => {
const drift = e.drift_score != null ? e.drift_score : 0;
return {
id: 'e' + i,
from: e.from, to: e.to,
color: { color: driftColor(drift), highlight: '#ffffff', hover: '#ffffff' },
width: 1.5,
smooth: { enabled: true, type: 'cubicBezier', roundness: 0.5 },
arrows: { to: { enabled: true, scaleFactor: 0.6 } },
_data: e
};
});
nodesDataset = new vis.DataSet(visNodes);
edgesDataset = new vis.DataSet(visEdges);
const container = document.getElementById('graph-container');
const options = {
layout: {
hierarchical: {
enabled: true,
direction: layoutVertical ? 'UD' : 'LR',
sortMethod: 'directed',
levelSeparation: 120,
nodeSpacing: 110,
treeSpacing: 160
}
},
physics: { enabled: false },
interaction: { hover: true, dragNodes: true, dragView: true, zoomView: true, tooltipDelay: 100 }
};
if (network) network.destroy();
network = new vis.Network(container, { nodes: nodesDataset, edges: edgesDataset }, options);
network.on('click', (params) => {
if (params.nodes.length > 0) {
const node = g.nodes.find(n => n.id === params.nodes[0]);
if (node) {
openDrawer(node);
dismissHint();
}
} else {
closeDrawer();
}
});
network.on('hoverNode', () => { if (network.canvas.frame) network.canvas.frame.canvas.style.cursor = 'pointer'; });
network.on('blurNode', () => { if (network.canvas.frame) network.canvas.frame.canvas.style.cursor = 'default'; });
$('#node-count').textContent = `${g.nodes.length} nodes`;
$('#edge-count').textContent = `${g.edges.length} edges`;
// Upgradam datele generale de cost din top-bar pentru sesiunea curenta
$('#session-cost-value').textContent = session.estimated_cost_usd != null ? ('$' + session.estimated_cost_usd.toFixed(5)) : '$0.00000';
$('#session-tokens').textContent = `${session.total_tokens || 0} tokens`;
setTimeout(() => network && network.fit({ animation: { duration: 500, easingFunction: 'easeInOutQuad' } }), 80);
}
function dismissHint() {
if (hintDismissed) return;
hintDismissed = true;
graphHint.style.opacity = '0';
setTimeout(() => { graphHint.style.display = 'none'; }, 500);
}
// ===== Drawer =====
function openDrawer(node) {
const session = reportData.results[currentSessionIdx];
const g = session ? session.enriched_graph : null;
if (!g) return;
const status = getNodeStatus(node);
const badge = $('#drawer-status-badge');
const badgeClass = status === 'healthy' ? 'badge-healthy' : status === 'error' ? 'badge-error' : 'badge-warning';
badge.className = `${badgeClass} text-[10px] px-2 py-0.5 rounded-full font-medium uppercase tracking-wider`;
badge.textContent = status === 'healthy' ? 'Healthy' : status === 'error' ? 'Error' : 'Unstable';
$('#drawer-type').textContent = node.type || 'unknown';
$('#drawer-label').textContent = node.label || node.id;
$('#drawer-id').textContent = node.id;
$('#drawer-exec-time').textContent = (node.execution_time_ms != null ? node.execution_time_ms : '—');
const instability = node.instability_index != null ? node.instability_index : 0;
$('#drawer-instability').textContent = instability.toFixed(1);
const instBar = $('#drawer-instability-bar');
instBar.style.width = Math.min(100, (instability / 30) * 100) + '%';
instBar.style.background = statusColor(status);
// Upgradam datele specifice de cost ale acestei sesiuni in drawer
$('#drawer-cost').textContent = session.estimated_cost_usd != null ? ('$' + session.estimated_cost_usd.toFixed(5)) : '$0.00';
$('#drawer-tokens').textContent = session.total_tokens != null ? (session.total_tokens + ' tokens') : '0 tokens';
// UPGRADE: Randeram o LISTA de erori pe nod (nu doar pe prima) si adaugam "Oglinda Minciunii" (Side-by-Side)
const errorSection = $('#drawer-error-section');
const errorContainer = $('#drawer-error');
const nodeFailures = session.failures.filter(f =>
f.to_node === node.id ||
(f.from_node === node.id && f.failure_type === "LOW_ROBUSTNESS")
);
if (nodeFailures.length > 0 || node.validation_error) {
errorSection.classList.remove('hidden');
let errorsHtml = '';
if (node.validation_error) {
errorsHtml += `
<div class="bg-red-950/20 border border-red-500/30 rounded-lg p-3">
<code class="text-xs text-red-200 font-mono leading-relaxed break-words">
<strong class="text-red-400">AST_FORMAT_ERROR:</strong> ${escapeHtml(node.validation_error)}
</code>
</div>
`;
}
nodeFailures.forEach(f => {
const detailText = f.details ? ` (${f.details})` : '';
// UPGRADE: "Oglinda Minciunii" (Side-by-Side comparison) pentru contradictii si scurgeri de date
let diffHtml = '';
if (f.failure_type === "GENERATION_CONTRADICTION" || f.failure_type === "SECURITY_LEAK" || f.failure_type === "RELEVANCE_VIOLATION") {
// Creăm o etichetă dinamică în funcție de tipul de eroare
const sourceLabel = f.failure_type === "RELEVANCE_VIOLATION" ? "🔵 Initial User Input (Prompt)" : "🔵 Source Database / Context (Premise)";
diffHtml = `
<div class="mt-3 space-y-2 border-t border-zinc-800/80 pt-2.5">
<div class="bg-red-950/20 border border-red-500/30 rounded-lg p-2.5">
<div class="text-[9px] uppercase tracking-wider text-red-400 font-medium mb-1">🔴 Agent Generated (Target)</div>
<p class="text-xs font-mono text-zinc-300 leading-relaxed">${escapeHtml(f.error_node_text)}</p>
</div>
<div class="bg-blue-950/20 border border-blue-500/30 rounded-lg p-2.5">
<div class="text-[9px] uppercase tracking-wider text-blue-400 font-medium mb-1">${escapeHtml(sourceLabel)}</div>
<p class="text-xs font-mono text-zinc-300 leading-relaxed">${escapeHtml(f.source_node_text || node.text || '—')}</p>
</div>
</div>
`;
}
errorsHtml += `
<div class="bg-red-950/20 border border-red-500/30 rounded-lg p-3">
<code class="text-xs text-red-200 font-mono leading-relaxed break-words">
<strong class="text-red-400">${escapeHtml(f.failure_type)}:</strong> ${escapeHtml(f.reason)}${escapeHtml(detailText)}
</code>
${diffHtml}
</div>
`;
});
errorContainer.innerHTML = errorsHtml;
} else {
errorSection.classList.add('hidden');
errorContainer.innerHTML = '';
}
$('#drawer-text').textContent = node.text || '(no output captured)';
// Connected edges
const connected = g.edges.filter(e => e.from === node.id || e.to === node.id);
const connContainer = $('#drawer-edges');
connContainer.innerHTML = '';
$('#drawer-edges-count').textContent = connected.length;
if (connected.length === 0) {
connContainer.innerHTML = '<div class="text-xs text-zinc-600 italic py-2">No connected edges</div>';
} else {
connected.forEach(edge => {
const isIncoming = edge.to === node.id;
const otherId = isIncoming ? edge.from : edge.to;
const otherNode = g.nodes.find(n => n.id === otherId);
const otherLabel = otherNode ? (otherNode.label || otherNode.id) : otherId;
const drift = edge.drift_score != null ? edge.drift_score : 0;
const nli = edge.nli_label || 'unknown';
const el = document.createElement('div');
el.className = 'bg-zinc-900/50 border border-zinc-800 rounded-lg p-3 hover:border-zinc-700 transition-colors cursor-pointer';
el.innerHTML = `
<div class="flex items-center justify-between mb-2">
<div class="flex items-center gap-1.5 text-[11px]">
<span class="text-zinc-500">${isIncoming ? 'from' : 'to'}</span>
<span class="text-zinc-300 font-medium">${escapeHtml(otherLabel)}</span>
</div>
</div>
<div class="flex items-center justify-between text-[11px] mb-1">
<span class="text-zinc-500">Relative Drift</span>
<span class="font-mono font-medium tabular-nums" style="color:${driftColor(drift)}">${(drift * 100).toFixed(2)}%</span>
</div>
<div class="bar-track h-1 rounded-full overflow-hidden">
<div class="h-full rounded-full" style="width:${(drift * 100)}%;background:${driftColor(drift)}"></div>
</div>
`;
el.addEventListener('click', () => {
if (otherNode) openDrawer(otherNode);
if (network) {
network.selectNodes([otherId]);
network.focus(otherId, { scale: 1.2, animation: { duration: 400, easingFunction: 'easeInOutQuad' } });
}
});
connContainer.appendChild(el);
});
}
drawer.classList.add('open');
drawerBackdrop.classList.add('open');
}
function closeDrawer() {
drawer.classList.remove('open');
drawerBackdrop.classList.remove('open');
if (network) network.unselectAll();
}
// ===== Toolbar / global handlers =====
$('#zoom-fit').addEventListener('click', () => network && network.fit({ animation: { duration: 400, easingFunction: 'easeInOutQuad' } }));
$('#zoom-in').addEventListener('click', () => network && network.moveTo({ scale: network.getScale() * 1.3, animation: { duration: 200 } }));
$('#zoom-out').addEventListener('click', () => network && network.moveTo({ scale: network.getScale() / 1.3, animation: { duration: 200 } }));
$('#layout-toggle').addEventListener('click', () => { layoutVertical = !layoutVertical; renderGraph(); });
$('#drawer-close').addEventListener('click', closeDrawer);
drawerBackdrop.addEventListener('click', closeDrawer);
// UPGRADE: 1-Click Copy function with visual feedback
$('#copy-payload').addEventListener('click', () => {
const textToCopy = $('#drawer-text').textContent;
navigator.clipboard.writeText(textToCopy).then(() => {
const copyText = $('#copy-text');
copyText.textContent = 'Copied!';
copyText.className = 'text-emerald-400 transition-colors font-medium';
setTimeout(() => {
copyText.textContent = 'Copy';
copyText.className = 'text-blue-400 transition-colors';
}, 1500);
});
});
$('#advice-expand').addEventListener('click', () => {
const m = $('#advice-modal');
m.classList.remove('hidden');
m.classList.add('flex');
// UPGRADE: Renderam raportul generativ local complex (Markdown) utilizand marked.js
const markdownReport = reportData.narrative_report || reportData.executive_summary.actionable_advice;
$('#advice-full').innerHTML = marked.parse(markdownReport);
});
function closeAdvice() {
const m = $('#advice-modal');
m.classList.add('hidden');
m.classList.remove('flex');
}
$('#advice-close').addEventListener('click', closeAdvice);
$('#advice-modal').addEventListener('click', (e) => { if (e.target.id === 'advice-modal') closeAdvice(); });
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') { closeDrawer(); closeAdvice(); }
});
// ===== Init =====
function init() {
if (!reportData || !reportData.executive_summary) {
emptyState.style.display = 'flex';
return;
}
renderSummary();
populateSessionSelect();
renderGraph();
}
window.addEventListener('load', () => setTimeout(init, 60));
</script>
</body>
</html>