Spaces:
Paused
Paused
| import json | |
| import os | |
| PROGRESS_FILE = "data/mintoak/eval_combined_progress.json" | |
| OUTPUT_HTML = "outputs/evaluation_report.html" | |
| CATEGORY_LABELS = { | |
| "greeting": "Greetings", | |
| "general_inquiry": "Inquiries", | |
| "out_of_scope": "Out of Scope", | |
| "guardrail_safety": "Safety", | |
| "guardrail_injection": "Injections", | |
| } | |
| CATEGORY_COLORS = { | |
| "greeting": "#6366f1", | |
| "general_inquiry": "#8b5cf6", | |
| "out_of_scope": "#f59e0b", | |
| "guardrail_safety": "#10b981", | |
| "guardrail_injection": "#ef4444", | |
| } | |
| def main(): | |
| if not os.path.exists(PROGRESS_FILE): | |
| print(f"Error: Progress file {PROGRESS_FILE} not found. Please run evaluation first.") | |
| return | |
| print(f"Loading results from {PROGRESS_FILE}...") | |
| with open(PROGRESS_FILE, "r") as f: | |
| state = json.load(f) | |
| results = state.get("results", []) | |
| # Always remap product_inquiry β general_inquiry at read-time | |
| CATEGORY_REMAP = {"product_inquiry": "general_inquiry"} | |
| for r in results: | |
| if r["category"] in CATEGORY_REMAP: | |
| r["category"] = CATEGORY_REMAP[r["category"]] | |
| passed_count = sum(1 for r in results if r["pass"]) | |
| total_cases = len(results) | |
| failed_count = total_cases - passed_count | |
| pass_rate = (passed_count / total_cases) * 100 if total_cases > 0 else 0.0 | |
| total_latency = state.get("total_latency", 0.0) | |
| llm_runs = state.get("llm_runs", 0) | |
| avg_latency = (total_latency / llm_runs) if llm_runs > 0 else 0.0 | |
| # Per-category stats | |
| cat_stats = {} | |
| for r in results: | |
| cat = r["category"] | |
| if cat not in cat_stats: | |
| cat_stats[cat] = {"total": 0, "passed": 0, "latencies": []} | |
| cat_stats[cat]["total"] += 1 | |
| if r["pass"]: | |
| cat_stats[cat]["passed"] += 1 | |
| if r["latency"] > 0: | |
| cat_stats[cat]["latencies"].append(r["latency"]) | |
| cat_order = ["greeting", "general_inquiry", "out_of_scope", "guardrail_safety", "guardrail_injection"] | |
| cat_order = [c for c in cat_order if c in cat_stats] + [c for c in cat_stats if c not in cat_order] | |
| # Build JS arrays for charts | |
| bar_labels = json.dumps([CATEGORY_LABELS.get(c, c) for c in cat_order]) | |
| bar_totals = json.dumps([cat_stats[c]["total"] for c in cat_order]) | |
| bar_passed = json.dumps([cat_stats[c]["passed"] for c in cat_order]) | |
| bar_failed = json.dumps([cat_stats[c]["total"] - cat_stats[c]["passed"] for c in cat_order]) | |
| bar_pass_rates = json.dumps([round(cat_stats[c]["passed"] / cat_stats[c]["total"] * 100, 1) for c in cat_order]) | |
| bar_colors = json.dumps([CATEGORY_COLORS.get(c, "#94a3b8") for c in cat_order]) | |
| avg_latencies = json.dumps([ | |
| round(sum(cat_stats[c]["latencies"]) / len(cat_stats[c]["latencies"]), 2) | |
| if cat_stats[c]["latencies"] else 0 | |
| for c in cat_order | |
| ]) | |
| # Category tile data for the summary strip | |
| cat_tiles_html = "" | |
| for cat in cat_order: | |
| s = cat_stats[cat] | |
| rate = round(s["passed"] / s["total"] * 100, 1) if s["total"] else 0 | |
| color = CATEGORY_COLORS.get(cat, "#94a3b8") | |
| label = CATEGORY_LABELS.get(cat, cat) | |
| cat_tiles_html += f""" | |
| <div class="cat-tile" onclick="filterCategory('{cat}', this)" data-cat="{cat}"> | |
| <div class="cat-dot" style="background:{color}"></div> | |
| <div class="cat-tile-inner"> | |
| <span class="cat-tile-label">{label}</span> | |
| <span class="cat-tile-count">{s['total']} cases</span> | |
| </div> | |
| <div class="cat-tile-rate" style="color:{color}">{rate}%</div> | |
| </div>""" | |
| # Build case cards HTML | |
| cards_html = "" | |
| for r in results: | |
| status_class = "pass" if r["pass"] else "fail" | |
| status_text = "Pass" if r["pass"] else "Fail" | |
| cat_color = CATEGORY_COLORS.get(r["category"], "#94a3b8") | |
| cat_label = CATEGORY_LABELS.get(r["category"], r["category"]) | |
| fail_section = "" if r["pass"] else f""" | |
| <div class="detail-row failure-row"> | |
| <div class="detail-label">β Failure Reason</div> | |
| <div class="detail-value" style="color:#dc2626;font-weight:600">{r["reason"]}</div> | |
| </div>""" | |
| lat_badge = f'{r["latency"]:.2f}s' if r["latency"] > 0 else "instant" | |
| cards_html += f""" | |
| <div class="case-card" data-category="{r['category']}" data-pass="{r['pass']}"> | |
| <div class="case-header" onclick="toggleDetails(this)"> | |
| <div class="case-title-group"> | |
| <span class="case-id">{r['id']}</span> | |
| <span class="case-category" style="background:{cat_color}20;color:{cat_color}">{cat_label}</span> | |
| <span class="case-query">{r['query']}</span> | |
| </div> | |
| <div class="case-meta"> | |
| <span class="lat-badge">β± {lat_badge}</span> | |
| <span class="status-pill {status_class}">{status_text}</span> | |
| <span class="chevron">βΊ</span> | |
| </div> | |
| </div> | |
| <div class="case-details"> | |
| <div class="detail-row"> | |
| <div class="detail-label">Query</div> | |
| <div class="detail-value" style="font-weight:500">{r['query']}</div> | |
| </div> | |
| {fail_section} | |
| <div class="detail-row"> | |
| <div class="detail-label">Assistant Response</div> | |
| <div class="detail-value code-box">{r['response']}</div> | |
| </div> | |
| </div> | |
| </div>""" | |
| html = f"""<!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Mintoak RAG Evaluation Dashboard</title> | |
| <meta name="description" content="Interactive evaluation report for Mintoak RAG Assistant β {total_cases} test cases, {pass_rate:.1f}% pass rate."> | |
| <link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&display=swap" rel="stylesheet"> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.2/dist/chart.umd.min.js"></script> | |
| <style> | |
| :root {{ | |
| --bg: #f8fafc; | |
| --surface: #ffffff; | |
| --border: #e2e8f0; | |
| --primary: #6366f1; | |
| --primary2: #8b5cf6; | |
| --success: #10b981; | |
| --danger: #ef4444; | |
| --warning: #f59e0b; | |
| --text: #0f172a; | |
| --muted: #64748b; | |
| --shadow-sm: 0 1px 3px rgba(0,0,0,0.06), 0 1px 2px rgba(0,0,0,0.04); | |
| --shadow: 0 4px 12px rgba(0,0,0,0.06); | |
| --shadow-lg: 0 12px 28px rgba(0,0,0,0.08); | |
| --radius: 16px; | |
| }} | |
| *, *::before, *::after {{ margin:0; padding:0; box-sizing:border-box; }} | |
| body {{ | |
| font-family:'Plus Jakarta Sans',sans-serif; | |
| background:var(--bg); | |
| color:var(--text); | |
| min-height:100vh; | |
| }} | |
| /* ββ Topbar ββ */ | |
| .topbar {{ | |
| background:var(--surface); | |
| border-bottom:1px solid var(--border); | |
| padding:1rem 2rem; | |
| display:flex; | |
| justify-content:space-between; | |
| align-items:center; | |
| position:sticky; | |
| top:0; | |
| z-index:100; | |
| box-shadow:var(--shadow-sm); | |
| }} | |
| .topbar-brand {{ display:flex; align-items:center; gap:0.75rem; }} | |
| .topbar-logo {{ | |
| width:36px; height:36px; border-radius:10px; | |
| background:linear-gradient(135deg,#6366f1,#8b5cf6); | |
| display:flex; align-items:center; justify-content:center; | |
| font-size:1.1rem; color:#fff; font-weight:800; | |
| }} | |
| .topbar-title {{ font-size:1.1rem; font-weight:700; letter-spacing:-0.02em; }} | |
| .topbar-sub {{ font-size:0.8rem; color:var(--muted); }} | |
| .topbar-badge {{ | |
| padding:0.4rem 0.9rem; border-radius:999px; font-size:0.8rem; | |
| font-weight:700; background:#f0fdf4; color:#059669; | |
| border:1px solid #bbf7d0; | |
| }} | |
| /* ββ Page layout ββ */ | |
| .page {{ max-width:1280px; margin:0 auto; padding:2rem; }} | |
| /* ββ Section heading ββ */ | |
| .section-heading {{ | |
| font-size:0.75rem; font-weight:700; text-transform:uppercase; | |
| letter-spacing:0.08em; color:var(--muted); margin-bottom:1rem; | |
| }} | |
| /* ββ Metric cards ββ */ | |
| .metrics-row {{ | |
| display:grid; | |
| grid-template-columns:repeat(auto-fit,minmax(170px,1fr)); | |
| gap:1rem; | |
| margin-bottom:2rem; | |
| }} | |
| .metric-card {{ | |
| background:var(--surface); | |
| border:1px solid var(--border); | |
| border-radius:var(--radius); | |
| padding:1.25rem 1.5rem; | |
| box-shadow:var(--shadow-sm); | |
| position:relative; | |
| overflow:hidden; | |
| transition:transform 0.2s, box-shadow 0.2s; | |
| }} | |
| .metric-card:hover {{ transform:translateY(-2px); box-shadow:var(--shadow); }} | |
| .metric-card::after {{ | |
| content:''; position:absolute; bottom:0; left:0; right:0; | |
| height:3px; background:linear-gradient(90deg,#6366f1,#8b5cf6); | |
| }} | |
| .metric-card.green::after {{ background:linear-gradient(90deg,#10b981,#34d399); }} | |
| .metric-card.red::after {{ background:linear-gradient(90deg,#ef4444,#f87171); }} | |
| .metric-card.amber::after {{ background:linear-gradient(90deg,#f59e0b,#fbbf24); }} | |
| .metric-card.sky::after {{ background:linear-gradient(90deg,#0ea5e9,#38bdf8); }} | |
| .metric-label {{ font-size:0.75rem; font-weight:600; text-transform:uppercase; letter-spacing:0.06em; color:var(--muted); }} | |
| .metric-value {{ font-size:2rem; font-weight:800; letter-spacing:-0.03em; margin-top:0.35rem; }} | |
| .metric-sub {{ font-size:0.75rem; color:var(--muted); margin-top:0.25rem; }} | |
| /* ββ Charts row ββ */ | |
| .charts-row {{ | |
| display:grid; | |
| grid-template-columns:280px 1fr 1fr; | |
| gap:1.25rem; | |
| margin-bottom:2rem; | |
| }} | |
| .chart-card {{ | |
| background:var(--surface); | |
| border:1px solid var(--border); | |
| border-radius:var(--radius); | |
| padding:1.5rem; | |
| box-shadow:var(--shadow-sm); | |
| }} | |
| .chart-title {{ | |
| font-size:0.9rem; font-weight:700; margin-bottom:1.25rem; color:var(--text); | |
| }} | |
| .chart-wrap {{ position:relative; }} | |
| .donut-center {{ | |
| position:absolute; top:50%; left:50%; | |
| transform:translate(-50%,-50%); | |
| text-align:center; pointer-events:none; | |
| }} | |
| .donut-pct {{ font-size:1.6rem; font-weight:800; color:var(--text); line-height:1; }} | |
| .donut-sub {{ font-size:0.7rem; color:var(--muted); margin-top:0.2rem; }} | |
| /* ββ Category tiles ββ */ | |
| .cat-grid {{ | |
| display:grid; | |
| grid-template-columns:repeat(auto-fill,minmax(200px,1fr)); | |
| gap:0.75rem; | |
| margin-bottom:2rem; | |
| }} | |
| .cat-tile {{ | |
| background:var(--surface); | |
| border:2px solid var(--border); | |
| border-radius:12px; | |
| padding:0.9rem 1rem; | |
| display:flex; | |
| align-items:center; | |
| gap:0.75rem; | |
| cursor:pointer; | |
| transition:all 0.18s; | |
| box-shadow:var(--shadow-sm); | |
| }} | |
| .cat-tile:hover {{ border-color:var(--primary); box-shadow:var(--shadow); }} | |
| .cat-tile.active {{ border-color:var(--primary); background:#f5f3ff; }} | |
| .cat-dot {{ width:10px; height:10px; border-radius:50%; flex-shrink:0; }} | |
| .cat-tile-inner {{ flex:1; min-width:0; }} | |
| .cat-tile-label {{ font-size:0.85rem; font-weight:700; display:block; }} | |
| .cat-tile-count {{ font-size:0.72rem; color:var(--muted); }} | |
| .cat-tile-rate {{ font-size:1rem; font-weight:800; flex-shrink:0; }} | |
| /* ββ Controls ββ */ | |
| .controls {{ | |
| display:flex; | |
| gap:0.75rem; | |
| margin-bottom:1.25rem; | |
| align-items:center; | |
| flex-wrap:wrap; | |
| }} | |
| .search-wrap {{ flex:1; min-width:260px; position:relative; }} | |
| .search-wrap svg {{ position:absolute; left:0.9rem; top:50%; transform:translateY(-50%); color:var(--muted); }} | |
| .search-input {{ | |
| width:100%; padding:0.7rem 1rem 0.7rem 2.5rem; | |
| border:1px solid var(--border); border-radius:10px; | |
| background:var(--surface); color:var(--text); | |
| font-size:0.9rem; outline:none; font-family:inherit; | |
| box-shadow:var(--shadow-sm); | |
| transition:border-color 0.2s, box-shadow 0.2s; | |
| }} | |
| .search-input:focus {{ border-color:var(--primary); box-shadow:0 0 0 3px rgba(99,102,241,0.1); }} | |
| .filter-select {{ | |
| padding:0.7rem 1rem; border:1px solid var(--border); border-radius:10px; | |
| background:var(--surface); color:var(--text); font-size:0.875rem; | |
| font-family:inherit; outline:none; cursor:pointer; box-shadow:var(--shadow-sm); | |
| }} | |
| .pass-filters {{ | |
| display:flex; gap:0.4rem; | |
| }} | |
| .pf-btn {{ | |
| padding:0.6rem 1rem; border-radius:10px; border:1px solid var(--border); | |
| background:var(--surface); color:var(--muted); font-size:0.82rem; | |
| font-weight:700; cursor:pointer; font-family:inherit; | |
| box-shadow:var(--shadow-sm); transition:all 0.18s; | |
| display:flex; align-items:center; gap:0.4rem; | |
| }} | |
| .pf-btn:hover {{ border-color:#94a3b8; }} | |
| .pf-btn.active {{ background:#f1f5f9; color:var(--text); border-color:#94a3b8; }} | |
| .pf-btn.pass.active {{ background:#ecfdf5; color:var(--success); border-color:#6ee7b7; }} | |
| .pf-btn.fail.active {{ background:#fef2f2; color:var(--danger); border-color:#fca5a5; }} | |
| .pf-count {{ | |
| padding:0.1rem 0.4rem; border-radius:999px; font-size:0.72rem; font-weight:800; | |
| background:rgba(0,0,0,0.07); | |
| }} | |
| .results-count {{ | |
| font-size:0.8rem; color:var(--muted); white-space:nowrap; | |
| }} | |
| /* ββ Case cards ββ */ | |
| .cases-list {{ display:flex; flex-direction:column; gap:0.75rem; }} | |
| .case-card {{ | |
| background:var(--surface); | |
| border:1px solid var(--border); | |
| border-radius:14px; | |
| overflow:hidden; | |
| box-shadow:var(--shadow-sm); | |
| transition:box-shadow 0.2s; | |
| }} | |
| .case-card:hover {{ box-shadow:var(--shadow); }} | |
| .case-header {{ | |
| padding:1rem 1.25rem; | |
| display:flex; | |
| justify-content:space-between; | |
| align-items:center; | |
| cursor:pointer; | |
| user-select:none; | |
| gap:1rem; | |
| }} | |
| .case-title-group {{ display:flex; align-items:center; gap:0.75rem; flex:1; min-width:0; }} | |
| .case-id {{ | |
| font-size:0.75rem; font-weight:700; color:var(--muted); | |
| background:#f1f5f9; padding:0.2rem 0.45rem; | |
| border-radius:5px; white-space:nowrap; | |
| }} | |
| .case-category {{ | |
| font-size:0.7rem; font-weight:700; text-transform:uppercase; | |
| letter-spacing:0.06em; padding:0.2rem 0.5rem; border-radius:5px; | |
| white-space:nowrap; | |
| }} | |
| .case-query {{ | |
| font-size:0.9rem; font-weight:600; color:var(--text); | |
| white-space:nowrap; overflow:hidden; text-overflow:ellipsis; | |
| }} | |
| .case-meta {{ display:flex; align-items:center; gap:0.6rem; flex-shrink:0; }} | |
| .lat-badge {{ | |
| font-size:0.72rem; color:var(--muted); background:#f8fafc; | |
| border:1px solid var(--border); padding:0.15rem 0.45rem; border-radius:5px; | |
| }} | |
| .status-pill {{ | |
| padding:0.25rem 0.6rem; border-radius:6px; | |
| font-size:0.7rem; font-weight:700; text-transform:uppercase; letter-spacing:0.05em; | |
| }} | |
| .status-pill.pass {{ background:#ecfdf5; color:#059669; }} | |
| .status-pill.fail {{ background:#fef2f2; color:#dc2626; }} | |
| .chevron {{ | |
| font-size:1.2rem; color:var(--muted); transition:transform 0.2s; line-height:1; | |
| }} | |
| .case-header.open .chevron {{ transform:rotate(90deg); }} | |
| .case-details {{ | |
| display:none; padding:1.25rem; border-top:1px solid var(--border); background:#f8fafc; | |
| }} | |
| .case-details.show {{ display:block; }} | |
| .detail-row {{ margin-bottom:0.9rem; }} | |
| .detail-row:last-child {{ margin-bottom:0; }} | |
| .detail-label {{ | |
| font-size:0.7rem; font-weight:700; text-transform:uppercase; | |
| letter-spacing:0.06em; color:var(--muted); margin-bottom:0.3rem; | |
| }} | |
| .detail-value {{ font-size:0.9rem; line-height:1.6; white-space:pre-wrap; }} | |
| .failure-row {{ border-left:3px solid #ef4444; padding-left:0.75rem; }} | |
| .code-box {{ | |
| background:#fff; border:1px solid var(--border); border-radius:8px; | |
| padding:0.9rem 1rem; font-family:monospace; font-size:0.85rem; | |
| color:#334155; box-shadow:inset 0 1px 3px rgba(0,0,0,0.03); | |
| }} | |
| .empty-state {{ | |
| text-align:center; padding:4rem 2rem; | |
| color:var(--muted); font-size:0.95rem; | |
| }} | |
| .empty-state .icon {{ font-size:3rem; margin-bottom:0.75rem; }} | |
| @media(max-width:900px) {{ | |
| .charts-row {{ grid-template-columns:1fr; }} | |
| .page {{ padding:1rem; }} | |
| }} | |
| </style> | |
| </head> | |
| <body> | |
| <div class="topbar"> | |
| <div class="topbar-brand"> | |
| <div class="topbar-logo">M</div> | |
| <div> | |
| <div class="topbar-title">Mintoak RAG Β· Evaluation Dashboard</div> | |
| <div class="topbar-sub">{total_cases} test cases Β· generated {__import__('datetime').datetime.now().strftime('%d %b %Y, %H:%M')}</div> | |
| </div> | |
| </div> | |
| <div class="topbar-badge">β {pass_rate:.1f}% Pass Rate</div> | |
| </div> | |
| <div class="page"> | |
| <!-- ββ Metric Cards ββ --> | |
| <div class="metrics-row"> | |
| <div class="metric-card"> | |
| <div class="metric-label">Total Cases</div> | |
| <div class="metric-value" id="metric-total">{total_cases}</div> | |
| <div class="metric-sub">Across all categories</div> | |
| </div> | |
| <div class="metric-card green"> | |
| <div class="metric-label">Passed</div> | |
| <div class="metric-value" id="metric-passed" style="color:var(--success)">{passed_count}</div> | |
| <div class="metric-sub" id="metric-passed-sub">of {total_cases} cases</div> | |
| </div> | |
| <div class="metric-card red"> | |
| <div class="metric-label">Failed</div> | |
| <div class="metric-value" id="metric-failed" style="color:var(--danger)">{failed_count}</div> | |
| <div class="metric-sub" id="metric-failed-sub">of {total_cases} cases</div> | |
| </div> | |
| <div class="metric-card amber"> | |
| <div class="metric-label">Pass Rate</div> | |
| <div class="metric-value" id="metric-rate">{pass_rate:.1f}%</div> | |
| <div class="metric-sub">Overall alignment</div> | |
| </div> | |
| <div class="metric-card sky"> | |
| <div class="metric-label">Avg Latency</div> | |
| <div class="metric-value">{avg_latency:.2f}s</div> | |
| <div class="metric-sub">LLM inference only</div> | |
| </div> | |
| </div> | |
| <!-- ββ Charts ββ --> | |
| <p class="section-heading">Analytics Overview</p> | |
| <div class="charts-row"> | |
| <!-- Donut: Pass vs Fail --> | |
| <div class="chart-card"> | |
| <div class="chart-title">Pass / Fail Ratio</div> | |
| <div class="chart-wrap"> | |
| <canvas id="donutChart" height="220"></canvas> | |
| <div class="donut-center"> | |
| <div class="donut-pct" id="donut-label">{pass_rate:.1f}%</div> | |
| <div class="donut-sub">pass rate</div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Bar: Cases per category --> | |
| <div class="chart-card"> | |
| <div class="chart-title">Cases by Category</div> | |
| <canvas id="barChart" height="220"></canvas> | |
| </div> | |
| <!-- Bar: Avg Latency per category --> | |
| <div class="chart-card"> | |
| <div class="chart-title">Avg Latency per Category (s)</div> | |
| <canvas id="latencyChart" height="220"></canvas> | |
| </div> | |
| </div> | |
| <!-- ββ Category tiles ββ --> | |
| <p class="section-heading">Filter by Category</p> | |
| <div class="cat-grid"> | |
| <div class="cat-tile active" onclick="filterCategory('all', this)" data-cat="all"> | |
| <div class="cat-dot" style="background:linear-gradient(135deg,#6366f1,#8b5cf6)"></div> | |
| <div class="cat-tile-inner"> | |
| <span class="cat-tile-label">All Categories</span> | |
| <span class="cat-tile-count">{total_cases} cases</span> | |
| </div> | |
| <div class="cat-tile-rate" style="color:var(--primary)">{pass_rate:.1f}%</div> | |
| </div> | |
| {cat_tiles_html} | |
| </div> | |
| <!-- ββ Controls ββ --> | |
| <div class="controls"> | |
| <div class="search-wrap"> | |
| <svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"> | |
| <circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/> | |
| </svg> | |
| <input class="search-input" id="searchInput" type="text" | |
| placeholder="Search queries, responses, test IDsβ¦" oninput="filterCases()"> | |
| </div> | |
| <div class="pass-filters"> | |
| <button class="pf-btn active" id="pf-all" onclick="setPassFilter('all', this)">All</button> | |
| <button class="pf-btn pass" id="pf-pass" onclick="setPassFilter('pass', this)">β Passed <span class="pf-count" id="pf-pass-count">{passed_count}</span></button> | |
| <button class="pf-btn fail" id="pf-fail" onclick="setPassFilter('fail', this)">β Failed <span class="pf-count" id="pf-fail-count">{failed_count}</span></button> | |
| </div> | |
| <span class="results-count" id="resultsCount">Showing {total_cases} cases</span> | |
| </div> | |
| <!-- ββ Case Cards ββ --> | |
| <div class="cases-list" id="casesList"> | |
| {cards_html} | |
| </div> | |
| <div class="empty-state" id="emptyState" style="display:none"> | |
| <div class="icon">π</div> | |
| No cases match your filters. | |
| </div> | |
| </div> | |
| <script> | |
| // ββ Data from Python ββ | |
| const barLabels = {bar_labels}; | |
| const barPassed = {bar_passed}; | |
| const barFailed = {bar_failed}; | |
| const barPassRates = {bar_pass_rates}; | |
| const barColors = {bar_colors}; | |
| const avgLatencies = {avg_latencies}; | |
| const totalPassed = {passed_count}; | |
| const totalFailed = {failed_count}; | |
| const passRate = {pass_rate:.2f}; | |
| // ββ Chart defaults ββ | |
| Chart.defaults.font.family = "'Plus Jakarta Sans', sans-serif"; | |
| Chart.defaults.color = "#64748b"; | |
| // ββ Donut ββ | |
| const donutCtx = document.getElementById('donutChart').getContext('2d'); | |
| const donutChart = new Chart(donutCtx, {{ | |
| type: 'doughnut', | |
| data: {{ | |
| labels: ['Passed', 'Failed'], | |
| datasets: [{{ | |
| data: [totalPassed, totalFailed], | |
| backgroundColor: ['#10b981', '#ef4444'], | |
| borderWidth: 3, | |
| borderColor: '#ffffff', | |
| hoverOffset: 6, | |
| }}] | |
| }}, | |
| options: {{ | |
| cutout: '72%', | |
| plugins: {{ | |
| legend: {{ | |
| position: 'bottom', | |
| labels: {{ padding: 16, boxWidth: 12, boxHeight: 12, borderRadius: 4 }} | |
| }}, | |
| tooltip: {{ | |
| callbacks: {{ | |
| label: ctx => ` ${{ctx.label}}: ${{ctx.raw}} cases (${{(ctx.raw/(totalPassed+totalFailed)*100).toFixed(1)}}%)` | |
| }} | |
| }} | |
| }}, | |
| animation: {{ animateRotate: true, duration: 900 }} | |
| }} | |
| }}); | |
| // ββ Bar: category breakdown ββ | |
| const barCtx = document.getElementById('barChart').getContext('2d'); | |
| new Chart(barCtx, {{ | |
| type: 'bar', | |
| data: {{ | |
| labels: barLabels, | |
| datasets: [ | |
| {{ | |
| label: 'Passed', | |
| data: barPassed, | |
| backgroundColor: barColors.map(c => c + 'cc'), | |
| borderRadius: 6, | |
| borderSkipped: false, | |
| }}, | |
| {{ | |
| label: 'Failed', | |
| data: barFailed, | |
| backgroundColor: '#ef444466', | |
| borderRadius: 6, | |
| borderSkipped: false, | |
| }} | |
| ] | |
| }}, | |
| options: {{ | |
| responsive: true, | |
| plugins: {{ | |
| legend: {{ position: 'bottom', labels: {{ padding: 16, boxWidth: 12, boxHeight: 12 }} }}, | |
| tooltip: {{ | |
| callbacks: {{ | |
| afterLabel: (ctx) => {{ | |
| const idx = ctx.dataIndex; | |
| return `Pass Rate: ${{barPassRates[idx]}}%`; | |
| }} | |
| }} | |
| }} | |
| }}, | |
| scales: {{ | |
| x: {{ grid: {{ display: false }}, stacked: false }}, | |
| y: {{ grid: {{ color: '#f1f5f9' }}, beginAtZero: true, ticks: {{ precision: 0 }} }} | |
| }}, | |
| animation: {{ duration: 900 }} | |
| }} | |
| }}); | |
| // ββ Bar: avg latency ββ | |
| const latCtx = document.getElementById('latencyChart').getContext('2d'); | |
| new Chart(latCtx, {{ | |
| type: 'bar', | |
| data: {{ | |
| labels: barLabels, | |
| datasets: [{{ | |
| label: 'Avg Latency (s)', | |
| data: avgLatencies, | |
| backgroundColor: barColors.map(c => c + 'aa'), | |
| borderRadius: 6, | |
| borderSkipped: false, | |
| }}] | |
| }}, | |
| options: {{ | |
| responsive: true, | |
| plugins: {{ | |
| legend: {{ display: false }}, | |
| tooltip: {{ | |
| callbacks: {{ | |
| label: ctx => ` ${{ctx.raw}}s average` | |
| }} | |
| }} | |
| }}, | |
| scales: {{ | |
| x: {{ grid: {{ display: false }} }}, | |
| y: {{ grid: {{ color: '#f1f5f9' }}, beginAtZero: true, ticks: {{ callback: v => v + 's' }} }} | |
| }}, | |
| animation: {{ duration: 900 }} | |
| }} | |
| }}); | |
| // ββ Filtering logic ββ | |
| let currentCat = 'all'; | |
| let currentPass = 'all'; | |
| function filterCategory(cat, el) {{ | |
| currentCat = cat; | |
| document.querySelectorAll('.cat-tile').forEach(t => t.classList.remove('active')); | |
| el.classList.add('active'); | |
| filterCases(); | |
| }} | |
| function setPassFilter(val, btn) {{ | |
| currentPass = val; | |
| document.querySelectorAll('.pf-btn').forEach(b => b.classList.remove('active')); | |
| btn.classList.add('active'); | |
| filterCases(); | |
| }} | |
| function filterCases() {{ | |
| const query = document.getElementById('searchInput').value.toLowerCase(); | |
| const cards = document.querySelectorAll('.case-card'); | |
| let total = 0, passed = 0, failed = 0, visible = 0; | |
| cards.forEach(card => {{ | |
| const cat = card.getAttribute('data-category'); | |
| const isPass = card.getAttribute('data-pass') === 'True'; | |
| const text = card.textContent.toLowerCase(); | |
| const matchCat = currentCat === 'all' || cat === currentCat; | |
| const matchSearch = text.includes(query); | |
| const matchPass = currentPass === 'all' || (currentPass === 'pass' && isPass) || (currentPass === 'fail' && !isPass); | |
| if (matchCat && matchSearch && matchPass) {{ | |
| card.style.display = ''; | |
| total++; | |
| if (isPass) passed++; else failed++; | |
| visible++; | |
| }} else {{ | |
| card.style.display = 'none'; | |
| }} | |
| }}); | |
| // Update metric cards | |
| document.getElementById('metric-total').textContent = total; | |
| document.getElementById('metric-passed').textContent = passed; | |
| document.getElementById('metric-failed').textContent = failed; | |
| document.getElementById('metric-passed-sub').textContent = `of ${{total}} cases`; | |
| document.getElementById('metric-failed-sub').textContent = `of ${{total}} cases`; | |
| const rate = total > 0 ? ((passed / total) * 100).toFixed(1) : '0.0'; | |
| document.getElementById('metric-rate').textContent = rate + '%'; | |
| // Update pill button counts | |
| document.getElementById('pf-pass-count').textContent = passed; | |
| document.getElementById('pf-fail-count').textContent = failed; | |
| // Update donut | |
| donutChart.data.datasets[0].data = [passed, failed]; | |
| document.getElementById('donut-label').textContent = rate + '%'; | |
| donutChart.update(); | |
| document.getElementById('resultsCount').textContent = `Showing ${{visible}} case${{visible !== 1 ? 's' : ''}}`; | |
| document.getElementById('emptyState').style.display = visible === 0 ? 'block' : 'none'; | |
| }} | |
| function toggleDetails(header) {{ | |
| const details = header.nextElementSibling; | |
| details.classList.toggle('show'); | |
| header.classList.toggle('open'); | |
| }} | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| os.makedirs("outputs", exist_ok=True) | |
| with open(OUTPUT_HTML, "w", encoding="utf-8") as f: | |
| f.write(html) | |
| print(f"Successfully generated HTML report at '{OUTPUT_HTML}'!") | |
| if __name__ == "__main__": | |
| main() | |