"""app/phases/documents.py — Phase 5 document package screen (T051/T052). Matches mockup.html #phase-5: a document preview (with amber-tint highlights on pre-filled fields), an evidence checklist (pre-checked from ``session.interview.documents_available``), the four generated files, a "Download all" primary action, and a "Start over" ghost button. The PDFs are generated by ``agent.tools.doc_generator`` from real session data. """ from __future__ import annotations from dataclasses import dataclass import gradio as gr from agent.tools.doc_generator import all_files, generate, preview_statement_html, zip_package from app.state.session import SessionState # Evidence checklist: (label, sublabel, document_available key or None). CHECKLIST = [ ("Personal statement", "Drafted by Fugee · ready", "_always"), ("Identity document or sworn statement", "Passport, national ID, or a sworn statement", "passport"), ("Proof of journey / entry", "Tickets, stamps, or a written account", "travel"), ("Photos or messages showing the threat", "Optional, but strengthens your claim", "evidence"), ("Names of witnesses", "People who can confirm what happened", "witnesses"), ("Medical or police reports", "If you have any", "medical"), ] DOCUMENTS_CSS = """ #docs-screen { background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-lg); box-shadow: var(--shadow-md); overflow: hidden; max-width: 1180px; margin: 0 auto; } .pkg { display:grid; grid-template-columns:1.1fr .9fr; gap:0; } @media(max-width:820px){ .pkg { grid-template-columns:1fr; } } .pkg__preview { padding:clamp(18px,4vw,28px); border-right:1px solid var(--line); background:var(--surface-2); } @media(max-width:820px){ .pkg__preview { border-right:0; border-bottom:1px solid var(--line); } } .pkg__side { padding:clamp(18px,4vw,28px); background:var(--surface); } .pkg__lbl { font-size:12px; letter-spacing:.06em; text-transform:uppercase; color:var(--text-muted); font-weight:600; margin:0 0 12px; } .doc { background:#fff; border:1px solid var(--line-strong); border-radius:var(--r-md); box-shadow:var(--shadow-sm); padding:26px 28px; max-height:360px; overflow:hidden; position:relative; } .doc::after { content:""; position:absolute; left:0; right:0; bottom:0; height:70px; background:linear-gradient(180deg,rgba(255,255,255,0),#fff); } .doc h4 { font-family:var(--font-display); font-size:18px; font-weight:600; margin:0 0 2px; } .doc .doc__sub { font-size:12px; color:var(--text-muted); margin:0 0 16px; } .doc p { font-size:13px; line-height:1.7; color:#33373f; margin:0 0 11px; } .doc .fill { background:var(--accent-tint); color:#7a431a; padding:0 4px; border-radius:3px; font-weight:600; } .doc .blank { display:inline-block; min-width:90px; border-bottom:1px solid #aaa; } .doc__divider { height:1px; background:var(--line); margin:14px 0; } #docs-checklist .wrap label { font-size:14.5px !important; } #docs-files .download-link, #docs-files label { font-size:13.5px; } #docs-download, #docs-download button { background:var(--accent) !important; color:var(--on-accent) !important; box-shadow:0 2px 0 var(--accent-deep) !important; border:0 !important; font-weight:600 !important; border-radius:var(--r-md) !important; padding:14px 22px !important; } #docs-download:hover, #docs-download button:hover { background:var(--accent-deep) !important; } #docs-startover, #docs-startover button { background:transparent !important; color:var(--primary-deep) !important; border:0 !important; box-shadow:none !important; font-weight:600 !important; } #docs-startover:hover, #docs-startover button:hover { background:var(--primary-tint) !important; } """ @dataclass class DocumentsUI: column: gr.Column populate: callable # (session) -> list of updates for render_outputs render_outputs: list start_over: gr.Button session: gr.State def _checklist_values(session: SessionState) -> list[str]: """Labels that should be pre-checked, from documents_available (SC-043).""" available = {d.lower() for d in (session.interview.documents_available or [])} checked = [] for label, _sub, key in CHECKLIST: if key == "_always" or (key and key in available): checked.append(label) return checked def build(visible: bool = False, session_st: gr.State | None = None) -> DocumentsUI: session_st = session_st or gr.State(None) with gr.Column(visible=visible, elem_classes=["screen-wrap"]) as column: with gr.Column(elem_id="docs-screen"): with gr.Row(elem_classes=["pkg"]): with gr.Column(elem_classes=["pkg__preview"]): gr.HTML('
Preview · Personal statement
') preview = gr.HTML('') with gr.Column(elem_classes=["pkg__side"]): gr.HTML('Evidence to gather
') checklist = gr.CheckboxGroup( choices=[label for label, _s, _k in CHECKLIST], value=[], label="", show_label=False, elem_id="docs-checklist", ) gr.HTML('Your files · Word (editable) + PDF
') files = gr.File(label="", show_label=False, interactive=False, elem_id="docs-files") download_all = gr.DownloadButton( "⤓ Download all (Word + PDF)", elem_id="docs-download", visible=False ) start_over = gr.Button( "↺ Start over for a different country", elem_id="docs-startover" ) render_outputs = [preview, checklist, files, download_all] async def populate(session: SessionState) -> list: # The agent drafts the personal statement (LLM), then we render PDF + Word. from agent.drafting import draft_personal_statement try: statement = await draft_personal_statement(session) except Exception: statement = None if not statement: from agent.drafting import fallback_statement statement = fallback_statement(session) docs = generate(session, statement=statement) zip_path = zip_package(docs) return [ gr.update(value=preview_statement_html(session, statement)), gr.update(value=_checklist_values(session)), gr.update(value=all_files(docs)), gr.update(value=str(zip_path), visible=True), ] return DocumentsUI( column=column, populate=populate, render_outputs=render_outputs, start_over=start_over, session=session_st, ) __all__ = ["build", "DocumentsUI", "DOCUMENTS_CSS", "CHECKLIST"]