Spaces:
Running
Running
| """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; } | |
| """ | |
| 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('<p class="pkg__lbl">Preview · Personal statement</p>') | |
| preview = gr.HTML('<article class="doc"></article>') | |
| with gr.Column(elem_classes=["pkg__side"]): | |
| gr.HTML('<p class="pkg__lbl">Evidence to gather</p>') | |
| checklist = gr.CheckboxGroup( | |
| choices=[label for label, _s, _k in CHECKLIST], | |
| value=[], label="", show_label=False, elem_id="docs-checklist", | |
| ) | |
| gr.HTML('<p class="pkg__lbl">Your files · Word (editable) + PDF</p>') | |
| 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"] | |