Spaces:
Running on Zero
Running on Zero
| <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 & 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> |