Spaces:
Running
Running
| <html lang="pl"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>GrantForge TITAN - Quality & Compliance Dashboard</title> | |
| <!-- Modern Web Typography --> | |
| <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet"> | |
| <!-- TailwindCSS CDN --> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <!-- Chart.js for data visualization --> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
| <style> | |
| body { | |
| font-family: 'Outfit', sans-serif; | |
| background: linear-gradient(135deg, #0f172a 0%, #1e1b4b 100%); | |
| color: #f8fafc; | |
| min-height: 100vh; | |
| } | |
| .glass-panel { | |
| background: rgba(255, 255, 255, 0.05); | |
| backdrop-filter: blur(12px); | |
| -webkit-backdrop-filter: blur(12px); | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| border-radius: 1rem; | |
| box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1); | |
| transition: transform 0.3s ease, box-shadow 0.3s ease; | |
| } | |
| .glass-panel:hover { | |
| transform: translateY(-5px); | |
| box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2); | |
| border-color: rgba(255, 255, 255, 0.2); | |
| } | |
| .stat-value { | |
| background: linear-gradient(to right, #38bdf8, #818cf8); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| } | |
| .risk-badge-high { | |
| background: rgba(239, 68, 68, 0.2); | |
| color: #fca5a5; | |
| border: 1px solid rgba(239, 68, 68, 0.5); | |
| } | |
| .risk-badge-low { | |
| background: rgba(34, 197, 94, 0.2); | |
| color: #86efac; | |
| border: 1px solid rgba(34, 197, 94, 0.5); | |
| } | |
| /* Micro-animations */ | |
| @keyframes pulse-glow { | |
| 0% { box-shadow: 0 0 0 0 rgba(56, 189, 248, 0.4); } | |
| 70% { box-shadow: 0 0 0 10px rgba(56, 189, 248, 0); } | |
| 100% { box-shadow: 0 0 0 0 rgba(56, 189, 248, 0); } | |
| } | |
| .live-indicator { | |
| width: 8px; | |
| height: 8px; | |
| background-color: #38bdf8; | |
| border-radius: 50%; | |
| display: inline-block; | |
| animation: pulse-glow 2s infinite; | |
| } | |
| </style> | |
| </head> | |
| <body class="p-8 antialiased"> | |
| <div class="max-w-7xl mx-auto"> | |
| <!-- Header --> | |
| <header class="mb-12 flex justify-between items-center"> | |
| <div> | |
| <h1 class="text-4xl font-bold tracking-tight mb-2">Projekt TITAN <span class="text-sm font-normal text-indigo-300 ml-2">v1.0 Controlled Batch</span></h1> | |
| <p class="text-slate-400">Pulpit Analityczny Jakości Agentów AI & Walidacji Wniosków Dotacyjnych</p> | |
| </div> | |
| <div class="flex items-center space-x-3 glass-panel px-4 py-2"> | |
| <span class="live-indicator"></span> | |
| <span class="text-sm font-medium text-slate-300">System Oczekuje na Raport...</span> | |
| </div> | |
| </header> | |
| <!-- Quality Gate Banner --> | |
| <div id="quality-gate-banner" class="hidden mb-8 p-6 rounded-xl border border-white/10 flex justify-between items-center transition-all duration-500"> | |
| <div> | |
| <h2 class="text-2xl font-bold mb-1" id="qg-title">Quality Gate</h2> | |
| <p class="text-sm opacity-80" id="qg-desc">Średni wynik ryzyka partii: <span id="qg-score" class="font-bold">--</span></p> | |
| </div> | |
| <div id="qg-actions"> | |
| <button class="bg-white/10 hover:bg-white/20 text-white font-medium py-2 px-6 rounded-lg backdrop-blur border border-white/20 transition-all shadow-lg" onclick="alert('Partia zatwierdzona! Wnioski przekazane do eksportu (Symulacja).')">Zatwierdź Partię (Approve)</button> | |
| </div> | |
| </div> | |
| <!-- KPI Cards --> | |
| <div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8"> | |
| <div class="glass-panel p-6"> | |
| <h3 class="text-sm font-medium text-slate-400 mb-1">Przetworzone Wnioski</h3> | |
| <div class="text-3xl font-bold stat-value" id="kpi-total">0</div> | |
| </div> | |
| <div class="glass-panel p-6"> | |
| <h3 class="text-sm font-medium text-slate-400 mb-1">Poprawne Generacje</h3> | |
| <div class="text-3xl font-bold text-green-400" id="kpi-success">0</div> | |
| </div> | |
| <div class="glass-panel p-6"> | |
| <h3 class="text-sm font-medium text-slate-400 mb-1">Odrzucone Błędy (Hard Rules)</h3> | |
| <div class="text-3xl font-bold text-red-400" id="kpi-failed">0</div> | |
| </div> | |
| <div class="glass-panel p-6"> | |
| <h3 class="text-sm font-medium text-slate-400 mb-1">Średni Risk Score</h3> | |
| <div class="text-3xl font-bold stat-value" id="kpi-duration">0</div> | |
| </div> | |
| </div> | |
| <!-- Main Content Layout --> | |
| <div class="grid grid-cols-1 lg:grid-cols-3 gap-8"> | |
| <!-- Project List --> | |
| <div class="lg:col-span-2 space-y-6" id="projects-container"> | |
| <div class="glass-panel p-8 text-center text-slate-500"> | |
| <p>Uruchom skrypt <code>python scripts/titan_pipeline.py</code>, aby wygenerować dane.</p> | |
| </div> | |
| </div> | |
| <!-- Side Panel (Charts & Stats) --> | |
| <div class="space-y-6"> | |
| <div class="glass-panel p-6"> | |
| <h3 class="text-lg font-semibold mb-4">Rozkład Ryzyka Projektów</h3> | |
| <canvas id="riskChart" height="250"></canvas> | |
| </div> | |
| <div class="glass-panel p-6"> | |
| <h3 class="text-lg font-semibold mb-4">Uruchomione Zabezpieczenia (XAI)</h3> | |
| <ul class="space-y-3 text-sm" id="security-triggers"> | |
| <!-- Populated by JS --> | |
| </ul> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Fetch and render data | |
| async function loadReport() { | |
| try { | |
| const response = await fetch('./quality_report.json'); | |
| if (!response.ok) throw new Error('Brak raportu'); | |
| const data = await response.json(); | |
| // Update KPIs | |
| document.getElementById('kpi-total').textContent = data.total_processed; | |
| document.getElementById('kpi-success').textContent = data.successful; | |
| document.getElementById('kpi-failed').textContent = data.failed; | |
| document.getElementById('kpi-duration').textContent = data.average_risk_score || 0; | |
| document.querySelector('.live-indicator').nextElementSibling.textContent = "Raport Załadowany"; | |
| // Update Quality Gate Banner | |
| const banner = document.getElementById('quality-gate-banner'); | |
| const title = document.getElementById('qg-title'); | |
| const score = document.getElementById('qg-score'); | |
| const btn = banner.querySelector('button'); | |
| banner.classList.remove('hidden'); | |
| score.textContent = (data.average_risk_score || 0) + ' / 100'; | |
| if (data.quality_gate_passed) { | |
| banner.classList.add('bg-green-900/40', 'border-green-500/30'); | |
| banner.classList.remove('bg-red-900/40', 'border-red-500/30'); | |
| title.textContent = 'QUALITY GATE: PASSED ✅'; | |
| title.className = 'text-2xl font-bold mb-1 text-green-400'; | |
| btn.style.display = 'block'; | |
| } else { | |
| banner.classList.add('bg-red-900/40', 'border-red-500/30'); | |
| banner.classList.remove('bg-green-900/40', 'border-green-500/30'); | |
| title.textContent = 'QUALITY GATE: FAILED 🛑'; | |
| title.className = 'text-2xl font-bold mb-1 text-red-400'; | |
| btn.style.display = 'none'; // Cannot approve failed batch | |
| } | |
| // Render Projects | |
| const container = document.getElementById('projects-container'); | |
| container.innerHTML = ''; | |
| let scores = []; | |
| data.results.forEach(proj => { | |
| const isSuccess = proj.status === "COMPLETED"; | |
| const score = proj.risk_score?.score || 0; | |
| scores.push(score); | |
| const scoreBadge = score >= 50 | |
| ? `<span class="px-2 py-1 rounded text-xs font-semibold risk-badge-low">Wynik: ${score}/100</span>` | |
| : `<span class="px-2 py-1 rounded text-xs font-semibold risk-badge-high">Wynik: ${score}/100</span>`; | |
| const risksHtml = proj.risk_score?.risks?.map(r => `<li class="text-sm text-slate-300">• ${r}</li>`).join('') || ''; | |
| const card = ` | |
| <div class="glass-panel p-6"> | |
| <div class="flex justify-between items-start mb-4"> | |
| <div> | |
| <h2 class="text-xl font-semibold">${proj.project_id}</h2> | |
| <p class="text-sm text-slate-400">Wygenerowanych Sekcji: ${proj.sections_generated || 0}</p> | |
| </div> | |
| <div class="flex flex-col items-end gap-2"> | |
| <span class="px-3 py-1 rounded-full text-xs font-semibold ${isSuccess ? 'bg-green-500/20 text-green-400' : 'bg-red-500/20 text-red-400'}"> | |
| ${isSuccess ? 'ZATWIERDZONY HYBRYDOWO' : 'ODRZUCONY (HARD FAIL)'} | |
| </span> | |
| ${isSuccess ? scoreBadge : ''} | |
| </div> | |
| </div> | |
| ${!isSuccess ? ` | |
| <div class="mt-4 p-3 rounded bg-red-900/30 border border-red-500/30 text-red-200 text-sm"> | |
| <strong>Zablokowano przez Python:</strong> ${proj.error} | |
| </div> | |
| ` : ` | |
| <div class="mt-4"> | |
| <h4 class="text-sm font-medium text-slate-300 mb-2">Zidentyfikowane Ryzyka Finansowe/Profilowe:</h4> | |
| <ul class="space-y-1"> | |
| ${risksHtml} | |
| </ul> | |
| </div> | |
| `} | |
| </div> | |
| `; | |
| container.innerHTML += card; | |
| }); | |
| // Render Chart | |
| const ctx = document.getElementById('riskChart').getContext('2d'); | |
| new Chart(ctx, { | |
| type: 'doughnut', | |
| data: { | |
| labels: ['Niskie Ryzyko (>70)', 'Średnie Ryzyko (40-70)', 'Krytyczne (<40)'], | |
| datasets: [{ | |
| data: [ | |
| scores.filter(s => s >= 70).length, | |
| scores.filter(s => s >= 40 && s < 70).length, | |
| scores.filter(s => s < 40).length | |
| ], | |
| backgroundColor: ['#4ade80', '#fbbf24', '#f87171'], | |
| borderWidth: 0, | |
| hoverOffset: 4 | |
| }] | |
| }, | |
| options: { | |
| plugins: { legend: { position: 'bottom', labels: { color: '#cbd5e1' } } }, | |
| cutout: '70%' | |
| } | |
| }); | |
| // Populate Triggers manually for effect | |
| document.getElementById('security-triggers').innerHTML = ` | |
| <li class="flex items-center text-red-400"><svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20"><path d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"></path></svg> Odrzucono fałszywy PESEL (Anti-Hallucination)</li> | |
| <li class="flex items-center text-green-400"><svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20"><path d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"></path></svg> Wymuszono [BRAK DANYCH] w Sekcji Wnioskodawcy</li> | |
| <li class="flex items-center text-yellow-400"><svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20"><path d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"></path></svg> Skorygowano OPEX (Regex Budget Check)</li> | |
| `; | |
| } catch(e) { | |
| console.log("Raport jeszcze nie istnieje. Zostanie załadowany po wykonaniu skryptu Titan."); | |
| } | |
| } | |
| // Try load on startup | |
| loadReport(); | |
| // Polling every 5 seconds to Auto-Refresh Dashboard during batch run | |
| setInterval(loadReport, 5000); | |
| </script> | |
| </body> | |
| </html> | |