CarefulAI
  • AI Assurance Tools
  • Compliance-native AI Examples
  • AI Design Support Agents
  • AI User Protection
  • AI Safety Research
  • AI Research Checker
  • Critical AI on AI Podcast
  • Feedback
  • Contact Us
​"""
Prompt Rubric Dashboard
Requires: pip install plotly pandas
Run:      python rubric_dashboard.py
Output:   rubric_dashboard.html  — fully self-contained, works offline
"""

import pandas as pd
import plotly.graph_objects as go
import plotly.io as pio
from plotly.subplots import make_subplots

# ── Data ─────────────────────────────────────────────────────────────────────

DIMENSIONS = [
    ("instruction_clarity",     "Instruction Clarity"),
    ("constraint_completeness", "Constraint Completeness"),
    ("output_specificity",      "Output Specificity"),
    ("pii_handling",            "PII Token Handling"),
    ("clinical_safety",         "Clinical Safety"),
    ("duplication_risk",        "Duplication Risk*"),
    ("structural_guidance",     "Structural Guidance"),
    ("prompt_efficiency",       "Prompt Efficiency"),
]
DIM_IDS    = [d[0] for d in DIMENSIONS]
DIM_LABELS = [d[1] for d in DIMENSIONS]

PROMPTS = [
    dict(name="reason_for_referral", label="Reason for Referral", group="ADHD Initial",
         instruction_clarity=4, constraint_completeness=5, output_specificity=4, pii_handling=5,
         clinical_safety=5, duplication_risk=2, structural_guidance=5, prompt_efficiency=4,
         notes="Strong example phrase list aids extraction. Missing explicit word count in prompt body. Low duplication risk as referral context is distinct."),
    dict(name="history_of_presenting_complaint", label="History of Presenting Complaint", group="ADHD Initial",
         instruction_clarity=4, constraint_completeness=5, output_specificity=4, pii_handling=5,
         clinical_safety=4, duplication_risk=4, structural_guidance=4, prompt_efficiency=3,
         notes="Two-task structure discards Task 1 output — adds tokens without value. High duplication risk with Medical/Psychiatric History. Typo: 'transcriptd'."),
    dict(name="psychiatric_history", label="Psychiatric History", group="ADHD Initial",
         instruction_clarity=5, constraint_completeness=5, output_specificity=4, pii_handling=5,
         clinical_safety=4, duplication_risk=4, structural_guidance=4, prompt_efficiency=3,
         notes="Very thorough extraction criteria. Two-task structure discards Task 1. Overlap risk with Medical History and Drug & Alcohol History addressed downstream."),
    dict(name="medical_history", label="Medical History", group="ADHD Initial",
         instruction_clarity=4, constraint_completeness=4, output_specificity=4, pii_handling=5,
         clinical_safety=4, duplication_risk=4, structural_guidance=4, prompt_efficiency=4,
         notes="Clear scope. Boundary with Psychiatric History is a known cross-contamination risk addressed downstream by the deduplication prompt."),
    dict(name="family_medical_psychiatric_history", label="Family Medical & Psychiatric History", group="ADHD Initial",
         instruction_clarity=5, constraint_completeness=5, output_specificity=4, pii_handling=5,
         clinical_safety=4, duplication_risk=3, structural_guidance=5, prompt_efficiency=4,
         notes="SCAN reference is a strong clinical anchor. Clear exclusion of patient's own history. Duplication risk mainly from family conditions bleeding into patient Medical History."),
    dict(name="drug_alcohol_forensic", label="Drug, Alcohol, Forensic, Capacity & Safeguarding", group="ADHD Initial",
         instruction_clarity=4, constraint_completeness=4, output_specificity=3, pii_handling=5,
         clinical_safety=5, duplication_risk=3, structural_guidance=4, prompt_efficiency=3,
         notes="Four high-stakes sections in one prompt — failure in one affects all. Word count guidance is aggregate only. Typo: 'types od substances'."),
    dict(name="adhd_diagnostic_formulation", label="ADHD Diagnostic Formulation", group="ADHD Initial",
         instruction_clarity=5, constraint_completeness=5, output_specificity=5, pii_handling=5,
         clinical_safety=5, duplication_risk=2, structural_guidance=5, prompt_efficiency=4,
         notes="Strongest prompt in the set. Mandatory opening statement is excellent. Binary outcome structure is well-defined. DSM-5 severity anchoring is precise."),
    dict(name="adhd_follow_up", label="ADHD Follow-Up Review", group="ADHD Follow-Up",
         instruction_clarity=4, constraint_completeness=4, output_specificity=5, pii_handling=5,
         clinical_safety=3, duplication_risk=2, structural_guidance=5, prompt_efficiency=3,
         notes="Highly prescriptive paragraph-by-paragraph structure. Clinical safety weaker — no explicit safeguarding instruction. Female-only conditional is a code-level dependency."),
    dict(name="adhd_ia_deduplication", label="ADHD Deduplication Analysis", group="QA",
         instruction_clarity=5, constraint_completeness=5, output_specificity=5, pii_handling=3,
         clinical_safety=4, duplication_risk=1, structural_guidance=5, prompt_efficiency=4,
         notes="Excellent JSON schema with confidence scores. Cross-heading risk pairs are well-specified. PII less critical as it operates on already-generated output."),
    dict(name="asd_deduplication", label="ASD Deduplication Analysis", group="QA",
         instruction_clarity=5, constraint_completeness=5, output_specificity=5, pii_handling=3,
         clinical_safety=4, duplication_risk=1, structural_guidance=5, prompt_efficiency=4,
         notes="Near-identical to ADHD deduplication with ASD-specific heading pairs. Both could be unified into a single parameterised prompt."),
    dict(name="asd_dev_history_social_interaction", label="ASD Developmental History & Social Interaction", group="ASD Initial",
         instruction_clarity=4, constraint_completeness=4, output_specificity=4, pii_handling=5,
         clinical_safety=4, duplication_risk=4, structural_guidance=4, prompt_efficiency=3,
         notes="Rich reporting verb list is a nice touch. Combined heading reduces API calls but raises duplication risk. Typo: 'thourough'."),
    dict(name="asd_social_communication", label="ASD Social Communication", group="ASD Initial",
         instruction_clarity=4, constraint_completeness=4, output_specificity=4, pii_handling=5,
         clinical_safety=4, duplication_risk=4, structural_guidance=4, prompt_efficiency=3,
         notes="Overlap with Developmental History is a significant risk. Expand-most-detailed-example instruction could conflict with word count targets."),
    dict(name="asd_routines", label="ASD Routines / Repetitive Interests / Sensory", group="ASD Initial",
         instruction_clarity=5, constraint_completeness=4, output_specificity=4, pii_handling=5,
         clinical_safety=4, duplication_risk=3, structural_guidance=5, prompt_efficiency=4,
         notes="Good coverage of all sensory domains. Target word count (~500) could be more explicitly embedded in the prompt body."),
    dict(name="asd_diagnostic_formulation", label="ASD Diagnostic Formulation", group="ASD Initial",
         instruction_clarity=5, constraint_completeness=5, output_specificity=5, pii_handling=5,
         clinical_safety=5, duplication_risk=2, structural_guidance=5, prompt_efficiency=4,
         notes="Uses ADHD prefix (not ASD prefix) — potential bug. Otherwise mirrors the quality of the ADHD formulation prompt. DSM-5 Level 1/2/3 anchoring is well-specified."),
    dict(name="asd_dev_social_combined", label="ASD Dev History + Social Comm (Combined)", group="ASD Initial",
         instruction_clarity=4, constraint_completeness=4, output_specificity=4, pii_handling=5,
         clinical_safety=4, duplication_risk=5, structural_guidance=4, prompt_efficiency=2,
         notes="Highest duplication risk in the set. Combines two sections known to overlap. 1000-2000 word output makes accuracy hard to verify across both sections simultaneously."),
    dict(name="general_adult_dictation", label="General Adult Dictation", group="General Adult",
         instruction_clarity=4, constraint_completeness=4, output_specificity=3, pii_handling=4,
         clinical_safety=4, duplication_risk=2, structural_guidance=5, prompt_efficiency=3,
         notes="Most flexible prompt — 21 headings with blanks allowed. Output specificity necessarily lower given open-ended dictation context. Manual Instructions heading is a thoughtful edge case handler."),
    dict(name="general_adult_formulation", label="General Adult Diagnostic Formulation & Plan", group="General Adult",
         instruction_clarity=4, constraint_completeness=5, output_specificity=4, pii_handling=5,
         clinical_safety=5, duplication_risk=2, structural_guidance=4, prompt_efficiency=4,
         notes="Critical safety instruction (no diagnosis not explicitly stated) is well emphasised. Typo in guideline 4: 'DO NOT about the word transcript'."),
    dict(name="adhd_prompt_prefix", label="ADHD Prompt Prefix (Helper)", group="Helper",
         instruction_clarity=5, constraint_completeness=4, output_specificity=4, pii_handling=5,
         clinical_safety=3, duplication_risk=1, structural_guidance=3, prompt_efficiency=5,
         notes="Efficient and well-structured. Clinical safety intentionally minimal — constraints live in child prompts. Could add a safeguarding reminder as a safety net."),
    dict(name="asd_prompt_prefix", label="ASD Prompt Prefix (Helper)", group="Helper",
         instruction_clarity=5, constraint_completeness=4, output_specificity=4, pii_handling=5,
         clinical_safety=3, duplication_risk=1, structural_guidance=3, prompt_efficiency=5,
         notes="Well-adapted from ADHD prefix for multi-source input. Uses American 'anonymized' instead of British 'anonymised' — inconsistency with the style guide."),
    dict(name="remove_leading_tabs", label="Remove Leading Tabs (Helper)", group="Helper",
         instruction_clarity=5, constraint_completeness=5, output_specificity=5, pii_handling=5,
         clinical_safety=5, duplication_risk=1, structural_guidance=5, prompt_efficiency=5,
         notes="Utility function — not an LLM prompt. Scores reflect it performs a single, well-defined mechanical task correctly."),
]

GROUP_COLORS = {
    "ADHD Initial":   "#3b82f6",
    "ADHD Follow-Up": "#8b5cf6",
    "ASD Initial":    "#10b981",
    "QA":             "#f59e0b",
    "General Adult":  "#ef4444",
    "Helper":         "#94a3b8",
}

DARK_BG  = "#0f1117"
CARD_BG  = "#111827"
BORDER   = "#1e2a3a"
TXT_MAIN = "#e2e8f0"
TXT_SUB  = "#94a3b8"
TXT_DIM  = "#475569"

COLORSCALE = [
    [0.00, "#ef4444"],
    [0.25, "#f97316"],
    [0.50, "#fbbf24"],
    [0.75, "#34d399"],
    [1.00, "#10b981"],
]
SCORE_PAL = ["#ef4444", "#f97316", "#fbbf24", "#34d399", "#10b981"]

def score_color(val):
    return SCORE_PAL[max(0, min(4, round(val) - 1))]

def hex_to_rgb(h):
    h = h.lstrip("#")
    return int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16)

def adjusted_avg(row):
    total = sum((6 - row[d]) if d == "duplication_risk" else row[d] for d in DIM_IDS)
    return round(total / len(DIM_IDS), 2)

df = pd.DataFrame(PROMPTS)
df["adj_avg"] = df.apply(adjusted_avg, axis=1)

theta_closed = DIM_LABELS + [DIM_LABELS[0]]

# ── Figure 1: Heatmap ─────────────────────────────────────────────────────────
z      = df[DIM_IDS].values.tolist()
labels = df["label"].tolist()

fig_heatmap = go.Figure(go.Heatmap(
    z=z, x=DIM_LABELS, y=labels,
    text=[[str(v) for v in row] for row in z],
    texttemplate="%{text}",
    textfont=dict(size=11, color="white", family="monospace"),
    colorscale=COLORSCALE, zmin=1, zmax=5,
    colorbar=dict(
        title=dict(text="Score", font=dict(color=TXT_SUB)),
        tickfont=dict(color=TXT_SUB, family="monospace"),
        bgcolor=CARD_BG, bordercolor=BORDER, thickness=14,
    ),
    hovertemplate="<b>%{y}</b><br>%{x}: %{z}/5<extra></extra>",
))
fig_heatmap.update_layout(
    title=dict(text="Score Heatmap — All Prompts × All Dimensions",
               font=dict(color=TXT_MAIN, size=15, family="Georgia"), x=0.01),
    plot_bgcolor=CARD_BG, paper_bgcolor=CARD_BG,
    font=dict(color=TXT_SUB, family="monospace", size=10),
    xaxis=dict(tickangle=-35, showgrid=False, tickfont=dict(size=10, color=TXT_SUB)),
    yaxis=dict(showgrid=False, tickfont=dict(size=10, color=TXT_MAIN), autorange="reversed"),
    margin=dict(l=290, r=80, t=60, b=140), height=680,
)

# ── Figure 2: Adjusted average bar chart ─────────────────────────────────────
df_s = df.sort_values("adj_avg")
fig_bar = go.Figure(go.Bar(
    x=df_s["adj_avg"], y=df_s["label"], orientation="h",
    marker=dict(color=[GROUP_COLORS.get(g, "#6b7280") for g in df_s["group"]], line=dict(width=0)),
    text=df_s["adj_avg"].astype(str), textposition="outside",
    textfont=dict(color=TXT_SUB, size=10, family="monospace"),
    hovertemplate="<b>%{y}</b><br>Adj. avg: %{x}<extra></extra>",
))
for grp, col in GROUP_COLORS.items():
    fig_bar.add_trace(go.Scatter(x=[None], y=[None], mode="markers",
                                  marker=dict(size=10, color=col), name=grp))
fig_bar.update_layout(
    title=dict(text="Adjusted Average Score  (duplication risk inverted)",
               font=dict(color=TXT_MAIN, size=15, family="Georgia"), x=0.01),
    plot_bgcolor=CARD_BG, paper_bgcolor=CARD_BG,
    font=dict(color=TXT_SUB, family="monospace", size=10),
    xaxis=dict(range=[0, 5.8], showgrid=True, gridcolor=BORDER,
               tickfont=dict(size=10, color=TXT_SUB)),
    yaxis=dict(showgrid=False, tickfont=dict(size=10, color=TXT_MAIN)),
    legend=dict(bgcolor=CARD_BG, bordercolor=BORDER, borderwidth=1,
                font=dict(color=TXT_SUB, size=10)),
    margin=dict(l=290, r=60, t=60, b=40), height=620,
)

# ── Figure 3: Dimension averages ──────────────────────────────────────────────
dim_avgs = []
for dim, lbl in DIMENSIONS:
    avg_val = round((6 - df[dim]).mean() if dim == "duplication_risk" else df[dim].mean(), 2)
    dim_avgs.append((lbl + (" (inv.)" if dim == "duplication_risk" else ""), avg_val))
dim_avgs.sort(key=lambda x: x[1])

fig_dims = go.Figure(go.Bar(
    x=[v for _, v in dim_avgs], y=[n for n, _ in dim_avgs], orientation="h",
    marker=dict(color=[score_color(v) for _, v in dim_avgs], line=dict(width=0)),
    text=[str(v) for _, v in dim_avgs], textposition="outside",
    textfont=dict(color=TXT_SUB, size=11, family="monospace"),
    hovertemplate="<b>%{y}</b><br>Average: %{x}<extra></extra>",
))
fig_dims.update_layout(
    title=dict(text="Average Score per Dimension — All 20 Prompts",
               font=dict(color=TXT_MAIN, size=15, family="Georgia"), x=0.01),
    plot_bgcolor=CARD_BG, paper_bgcolor=CARD_BG,
    font=dict(color=TXT_SUB, family="monospace", size=11),
    xaxis=dict(range=[0, 5.6], showgrid=True, gridcolor=BORDER,
               tickfont=dict(size=10, color=TXT_SUB)),
    yaxis=dict(showgrid=False, tickfont=dict(size=11, color=TXT_MAIN)),
    margin=dict(l=230, r=60, t=60, b=40), height=400,
)

# ── Figure 4: Group radar overlay ─────────────────────────────────────────────
fig_radar = go.Figure()
for grp, col in GROUP_COLORS.items():
    gdf = df[df["group"] == grp]
    if gdf.empty:
        continue
    scores = [round(gdf[d].mean(), 2) for d in DIM_IDS]
    r_c    = scores + [scores[0]]
    r, g, b = hex_to_rgb(col)
    fig_radar.add_trace(go.Scatterpolar(
        r=r_c, theta=theta_closed, fill="toself",
        fillcolor=f"rgba({r},{g},{b},0.12)", line=dict(color=col, width=2),
        name=grp,
        hovertemplate="%{theta}: %{r:.2f}<extra>" + grp + "</extra>",
    ))
fig_radar.update_layout(
    title=dict(text="Group Average Radar — All Groups Overlaid",
               font=dict(color=TXT_MAIN, size=15, family="Georgia"), x=0.01),
    polar=dict(
        bgcolor="#0d1117",
        radialaxis=dict(visible=True, range=[0, 5], showticklabels=False,
                        gridcolor=BORDER, linecolor=BORDER),
        angularaxis=dict(tickfont=dict(color=TXT_SUB, size=10, family="monospace"),
                         gridcolor=BORDER, linecolor=BORDER),
    ),
    paper_bgcolor=CARD_BG,
    legend=dict(bgcolor=CARD_BG, bordercolor=BORDER, borderwidth=1,
                font=dict(color=TXT_SUB, size=11)),
    margin=dict(l=60, r=60, t=60, b=60), height=500,
)

# ── Figure 5: Per-prompt radar grid ──────────────────────────────────────────
COLS   = 4
N      = len(PROMPTS)
ROWS   = -(-N // COLS)
specs  = [[{"type": "polar"}] * COLS for _ in range(ROWS)]
titles = [p["label"] for p in PROMPTS] + [""] * (ROWS * COLS - N)

fig_grid = make_subplots(rows=ROWS, cols=COLS, specs=specs,
                          subplot_titles=titles,
                          vertical_spacing=0.08, horizontal_spacing=0.04)

for i, pd_ in enumerate(PROMPTS):
    ri = i // COLS + 1
    ci = i % COLS + 1
    scores  = [pd_[d] for d in DIM_IDS]
    r_c     = scores + [scores[0]]
    col     = GROUP_COLORS.get(pd_["group"], "#6b7280")
    r, g, b = hex_to_rgb(col)
    fig_grid.add_trace(go.Scatterpolar(
        r=r_c, theta=theta_closed, fill="toself",
        fillcolor=f"rgba({r},{g},{b},0.18)", line=dict(color=col, width=1.5),
        name=pd_["label"], showlegend=False,
        hovertemplate="%{theta}: %{r}<extra>" + pd_["label"] + "</extra>",
    ), row=ri, col=ci)

for i in range(1, ROWS * COLS + 1):
    key = f"polar{i}" if i > 1 else "polar"
    fig_grid.update_layout(**{key: dict(
        bgcolor="#0d1117",
        radialaxis=dict(visible=True, range=[0, 5], showticklabels=False,
                        gridcolor=BORDER, linecolor=BORDER),
        angularaxis=dict(tickfont=dict(color=TXT_DIM, size=7),
                         gridcolor=BORDER, linecolor=BORDER),
    )})
for ann in fig_grid.layout.annotations:
    ann.font = dict(color=TXT_SUB, size=9, family="monospace")
fig_grid.update_layout(
    title=dict(text="Individual Prompt Radar Grid",
               font=dict(color=TXT_MAIN, size=15, family="Georgia"), x=0.01),
    paper_bgcolor=CARD_BG, height=ROWS * 200 + 80,
    margin=dict(l=20, r=20, t=60, b=20),
)

# ── Reviewer notes table ──────────────────────────────────────────────────────
notes_rows_html = ""
for _, row in df.sort_values("adj_avg").iterrows():
    col    = GROUP_COLORS.get(row["group"], "#6b7280")
    avg    = row["adj_avg"]
    acol   = score_color(avg)
    notes_rows_html += f"""
    <tr>
      <td style="padding:10px 12px;border-bottom:1px solid {BORDER};">
        <span style="font-size:10px;color:{col};font-family:monospace;
          text-transform:uppercase;display:block;margin-bottom:3px;">{row['group']}</span>
        <span style="font-size:12px;color:{TXT_MAIN};">{row['label']}</span>
      </td>
      <td style="padding:10px 12px;border-bottom:1px solid {BORDER};font-size:18px;
          font-weight:bold;font-family:monospace;color:{acol};text-align:center;">{avg}</td>
      <td style="padding:10px 12px;border-bottom:1px solid {BORDER};font-size:12px;
          color:{TXT_SUB};line-height:1.6;font-family:'Helvetica Neue',sans-serif;">{row['notes']}</td>
    </tr>"""

# ── Build full HTML with Plotly embedded inline (no CDN needed) ───────────────

# Render each figure as a div (no plotly JS included yet)
def fig_div(fig):
    return fig.to_html(full_html=False, include_plotlyjs=False)

# Get the plotly.min.js source from the local package
import os, plotly
plotly_js_path = os.path.join(os.path.dirname(plotly.__file__),
                               "package_data", "plotly.min.js")
with open(plotly_js_path, "r", encoding="utf-8") as f:
    plotly_js = f.read()

HTML = f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Prompt Rubric Dashboard</title>
<script>{plotly_js}</script>
<style>
  *,*::before,*::after{{box-sizing:border-box;margin:0;padding:0;}}
  body {{background:{DARK_BG};color:{TXT_MAIN};font-family:Georgia,serif;}}
  header {{background:#1a1f2e;border-bottom:1px solid {BORDER};padding:28px 40px;}}
  header p.eyebrow {{color:{TXT_DIM};font-size:11px;letter-spacing:.2em;
    font-family:monospace;text-transform:uppercase;margin-bottom:6px;}}
  header h1 {{font-size:28px;font-weight:normal;color:{TXT_MAIN};margin-bottom:6px;}}
  header p.sub {{color:{TXT_DIM};font-size:12px;font-family:'Helvetica Neue',sans-serif;}}
  main {{max-width:1300px;margin:0 auto;padding:32px 40px;}}
  .card {{background:{CARD_BG};border:1px solid {BORDER};border-radius:10px;
    padding:20px;margin-bottom:24px;}}
  h2.st {{font-size:13px;font-weight:normal;color:{TXT_SUB};font-family:monospace;
    letter-spacing:.1em;text-transform:uppercase;margin-bottom:16px;}}
  .two-col {{display:grid;grid-template-columns:1fr 1fr;gap:24px;}}
  table {{width:100%;border-collapse:collapse;}}
  th {{text-align:left;padding:8px 12px;color:{TXT_DIM};font-size:11px;
    font-family:monospace;letter-spacing:.05em;border-bottom:2px solid {BORDER};}}
  @media(max-width:800px){{.two-col{{grid-template-columns:1fr;}}main{{padding:16px;}}}}
</style>
</head>
<body>
<header>
  <p class="eyebrow">Prompt Quality Assessment</p>
  <h1>Rubric Dashboard</h1>
  <p class="sub">20 prompts &nbsp;·&nbsp; 8 dimensions &nbsp;·&nbsp;
    * Duplication Risk is inverted — lower score = lower risk = better</p>
</header>
<main>
  <div class="card"><h2 class="st">Score Heatmap</h2>{fig_div(fig_heatmap)}</div>
  <div class="two-col">
    <div class="card"><h2 class="st">Adjusted Average by Prompt</h2>{fig_div(fig_bar)}</div>
    <div class="card"><h2 class="st">Dimension Averages — All Prompts</h2>{fig_div(fig_dims)}</div>
  </div>
  <div class="two-col">
    <div class="card"><h2 class="st">Group Radar Overlay</h2>{fig_div(fig_radar)}</div>
    <div class="card"><h2 class="st">Individual Prompt Radar Grid</h2>{fig_div(fig_grid)}</div>
  </div>
  <div class="card">
    <h2 class="st">Reviewer Notes</h2>
    <table>
      <thead><tr>
        <th style="width:240px;">Prompt</th>
        <th style="width:80px;text-align:center;">Adj. Avg</th>
        <th>Notes</th>
      </tr></thead>
      <tbody>{notes_rows_html}</tbody>
    </table>
  </div>
</main>
</body>
</html>"""

output_path = "rubric_dashboard.html"
with open(output_path, "w", encoding="utf-8") as f:
    f.write(HTML)

file_size = os.path.getsize(output_path) // 1024
print(f"Saved: {output_path}  ({file_size} KB)")

# ── Serve locally and open in Android browser ─────────────────────────────────
import subprocess
import threading
import http.server
import socketserver
import webbrowser
import signal

PORT      = 8080
directory = os.path.dirname(os.path.abspath(output_path))
filename  = os.path.basename(output_path)
url       = f"http://127.0.0.1:{PORT}/{filename}"

class Handler(http.server.SimpleHTTPRequestHandler):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, directory=directory, **kwargs)
    def log_message(self, format, *args):
        pass  # suppress request logs

socketserver.TCPServer.allow_reuse_address = True
httpd  = socketserver.TCPServer(("127.0.0.1", PORT), Handler)
thread = threading.Thread(target=httpd.serve_forever, daemon=True)
thread.start()

print(f"\nServer running at {url}")
print("Opening browser — if it does not launch automatically,")
print(f"open your browser and go to:  {url}")
print("Press Ctrl+C to stop the server when done.\n")

# Launch browser via Android activity manager
try:
    result = subprocess.run(
        ["am", "start", "--user", "0",
         "-a", "android.intent.action.VIEW",
         "-d", url],
        capture_output=True, text=True, timeout=5
    )
    if result.returncode != 0:
        raise RuntimeError(result.stderr.strip())
    print("Browser launched.")
except Exception:
    webbrowser.open(url)

# Keep server alive until Ctrl+C
try:
    signal.pause()          # efficient wait on Linux/Android
except (AttributeError, OSError):
    try:
        input("Press Enter to stop the server...")
    except EOFError:
        import time
        while True:
            time.sleep(1)   # fallback: keep alive indefinitely
finally:
    httpd.shutdown()
Privacy Policy       Terms of Service
  • AI Assurance Tools
  • Compliance-native AI Examples
  • AI Design Support Agents
  • AI User Protection
  • AI Safety Research
  • AI Research Checker
  • Critical AI on AI Podcast
  • Feedback
  • Contact Us