from __future__ import annotations
import json
from pathlib import Path
from typing import Any
import gradio as gr
import pandas as pd
from formpilot.engine import analyze_form, export_trace, rows_to_csv
from formpilot.model_assist import DEFAULT_SMALL_MODEL, try_hf_model_assist
APP_DIR = Path(__file__).resolve().parent
TRACE_DIR = APP_DIR / "traces"
EXPORT_DIR = APP_DIR / "exports"
TRACE_DIR.mkdir(exist_ok=True)
EXPORT_DIR.mkdir(exist_ok=True)
SAMPLE_FORM = """Community Center Membership Form
Full name: ____________________
Email: ____________________
Phone: ____________________
Address: ____________________
Emergency contact: ____________________
Relationship: ____________________
Signature: ____________________
Date: ____________________
"""
SAMPLE_FACTS = """Full name: Jordan Lee
Email: jordan.lee@example.com
Phone: 555-0137
Address: 42 Maple Street, Springfield, NY 10027
Emergency contact: Priya Lee
Relationship: Sister
Date: June 6, 2026
"""
CSS = """
.gradio-container {
max-width: 1240px !important;
}
#hero {
padding: 8px 0 16px 0;
border-bottom: 1px solid #d7dde5;
}
#hero h1 {
font-size: 34px;
line-height: 1.05;
letter-spacing: 0;
margin: 0 0 8px 0;
}
#hero p {
color: #5b6470;
max-width: 860px;
font-size: 15px;
}
.status-ready {
color: #1f6f4a;
font-weight: 700;
}
.status-review {
color: #9a6500;
font-weight: 700;
}
.status-missing {
color: #a33d3d;
font-weight: 700;
}
.panel-note {
border: 1px solid #d7dde5;
border-radius: 8px;
background: #ffffff;
padding: 12px 14px;
}
"""
def _status_html(rows: list[dict[str, Any]], risks: list[str]) -> str:
ready = sum(1 for row in rows if row["status"] == "ready")
review = sum(1 for row in rows if row["status"] == "review")
missing = sum(1 for row in rows if row["status"] == "missing")
risk_items = "".join(f"
{risk}" for risk in risks)
return f"""
{ready} ready · {review} review · {missing} missing
"""
def _write_export_files(payload: dict[str, Any]) -> tuple[str, str]:
trace_path = export_trace(payload, TRACE_DIR)
csv_path = EXPORT_DIR / "formpilot_latest_fields.csv"
csv_path.write_text(rows_to_csv(payload["rows"]), encoding="utf-8")
return trace_path, str(csv_path)
def _model_payload_to_rows(model_payload: dict[str, Any]) -> list[dict[str, Any]]:
rows = model_payload.get("fields", [])
normalized = []
for row in rows:
normalized.append(
{
"field": str(row.get("field", "")),
"proposed_value": str(row.get("proposed_value", "")),
"status": str(row.get("status", "review")),
"confidence": int(row.get("confidence", 0) or 0),
"source": str(row.get("source", "")),
"note": str(row.get("note", "")),
}
)
return normalized
def run_pilot(
form_text: str,
user_facts: str,
mode: str,
model_id: str,
) -> tuple[pd.DataFrame, str, str, str, dict[str, Any], str, str]:
if not form_text.strip():
raise gr.Error("Paste a form or request first.")
payload = analyze_form(form_text, user_facts)
backend_note = "Local structured matcher"
if mode == "Small model assist":
try:
model_payload = try_hf_model_assist(form_text, user_facts, model_id.strip() or DEFAULT_SMALL_MODEL)
model_rows = _model_payload_to_rows(model_payload)
if model_rows:
payload["rows"] = model_rows
payload["questions"] = model_payload.get("questions", payload["questions"])
payload["risk_summary"] = model_payload.get("risk_summary", payload["risk_summary"])
payload["copy_ready"] = "\n".join(
f"{row['field']}: {row['proposed_value'] or '[NEEDS USER INPUT]'}"
for row in model_rows
)
backend_note = f"Small model assist: {model_id.strip() or DEFAULT_SMALL_MODEL}"
except Exception as exc:
payload["risk_summary"].insert(
0,
f"Small model assist failed; used local matcher instead. Reason: {exc}",
)
payload["backend"] = backend_note
trace_path, csv_path = _write_export_files(payload)
rows = payload["rows"]
table = pd.DataFrame(rows, columns=["field", "proposed_value", "status", "confidence", "source", "note"])
questions = "\n".join(f"- {question}" for question in payload["questions"]) or "No missing-field questions detected."
summary = _status_html(rows, payload["risk_summary"])
return table, payload["copy_ready"], questions, summary, payload, trace_path, csv_path
def clear_outputs() -> tuple[pd.DataFrame, str, str, str, dict[str, Any], None, None]:
return pd.DataFrame(), "", "", "", {}, None, None
def build_demo() -> gr.Blocks:
with gr.Blocks(title="Offline Form Pilot") as demo:
gr.Markdown(
"""
# Offline Form Pilot
Paste a form and the facts you are willing to use. The app prepares a review table, missing-field questions, and copy-ready text without submitting anything.
""",
elem_id="hero",
)
with gr.Row():
with gr.Column(scale=5):
form_text = gr.Textbox(
label="Form or request text",
value=SAMPLE_FORM,
lines=12,
max_lines=18,
)
user_facts = gr.Textbox(
label="User facts",
value=SAMPLE_FACTS,
lines=10,
max_lines=16,
)
with gr.Column(scale=3):
mode = gr.Radio(
label="Analysis mode",
choices=["Local structured matcher", "Small model assist"],
value="Local structured matcher",
)
model_id = gr.Textbox(label="Small model id", value=DEFAULT_SMALL_MODEL)
run_btn = gr.Button("Prepare form review", variant="primary")
clear_btn = gr.Button("Clear outputs")
gr.Markdown(
"""
Human review is required. Do not paste secrets unless you are comfortable with the selected backend.
""",
elem_classes=["panel-note"],
)
summary = gr.HTML()
table = gr.Dataframe(
label="Review table",
headers=["field", "proposed_value", "status", "confidence", "source", "note"],
wrap=True,
interactive=False,
)
with gr.Row():
copy_ready = gr.Textbox(label="Copy-ready draft", lines=10, buttons=["copy"])
questions = gr.Textbox(label="Questions before copying", lines=10, buttons=["copy"])
with gr.Accordion("Trace and exports", open=False):
raw_json = gr.JSON(label="Trace JSON")
trace_file = gr.File(label="Download trace JSON")
csv_file = gr.File(label="Download field CSV")
run_btn.click(
run_pilot,
inputs=[form_text, user_facts, mode, model_id],
outputs=[table, copy_ready, questions, summary, raw_json, trace_file, csv_file],
api_name="prepare_form_review",
)
clear_btn.click(
clear_outputs,
outputs=[table, copy_ready, questions, summary, raw_json, trace_file, csv_file],
)
return demo
demo = build_demo()
if __name__ == "__main__":
demo.launch(css=CSS)