| import os |
| import json |
| import time |
| from hashlib import sha256 |
| from typing import Any, Dict, List, Tuple |
|
|
| import gradio as gr |
|
|
| from modules.ocr import extract_text |
| from modules.llm_provider import extract_form_data, ask_llm |
| from modules.overview import generate_overview |
| from modules.checklist import generate_checklist |
| from modules.eligibility import check_eligibility |
| from modules.timeline import extract_timeline |
| from modules.risk_detector import generate_risks |
| from modules.verifier import verify_documents, verification_summary |
|
|
|
|
| def _read_css() -> str: |
| css_content = "" |
| css_path = os.path.join("assets", "style.css") |
| if os.path.exists(css_path): |
| with open(css_path, "r", encoding="utf-8") as f: |
| css_content = f.read() |
| return css_content |
|
|
|
|
| def _file_fingerprint(file_obj) -> str: |
| """Lightweight key for caching: file name + size + mtime if available.""" |
| if file_obj is None: |
| return "" |
| name = getattr(file_obj, "name", "") or "" |
| size = getattr(file_obj, "size", None) |
| mtime = getattr(file_obj, "last_modified", None) |
| raw = f"{name}|{size}|{mtime}".encode("utf-8", errors="ignore") |
| return sha256(raw).hexdigest() |
|
|
|
|
| |
| _CACHE: Dict[str, Dict[str, Any]] = {} |
|
|
|
|
| def analyze_document(file): |
| """Analyze uploaded form and return all outputs for the dashboard.""" |
| if file is None: |
| return ( |
| {}, |
| gr.update(value="ℹ Upload a form first.", visible=True), |
| gr.update(visible=False), |
| gr.update(value="ℹ Analyze a form to see results.", visible=True), |
| gr.update(visible=False), |
| gr.update(value="ℹ No timeline information found in this form.", visible=True), |
| gr.update(visible=False), |
| "ℹ No risks detected.", |
| gr.update(value="ℹ Upload supporting documents to verify.", visible=True), |
| gr.update(visible=False), |
| gr.update(value="", visible=False), |
| gr.update(value="ℹ Ready", visible=False), |
| gr.update(value=[], visible=False), |
| ) |
|
|
| key = _file_fingerprint(file) |
| cached = _CACHE.get(key) |
|
|
| if cached: |
| master_json = cached["master_json"] |
| overview = cached["overview"] |
| checklist = cached["checklist"] |
| timeline = cached["timeline"] |
| risks = cached["risks"] |
| status = f"✅ Loaded from cache ({time.strftime('%H:%M:%S')})" |
| else: |
| extracted_text = extract_text(file.name) |
| master_json = extract_form_data(extracted_text) |
| overview = generate_overview(master_json) |
| checklist = generate_checklist(master_json) |
| timeline = extract_timeline(extracted_text) |
| risks = generate_risks(master_json) |
| status = f"✅ Analysis complete ({time.strftime('%H:%M:%S')})" |
| _CACHE[key] = { |
| "master_json": master_json, |
| "overview": overview, |
| "checklist": checklist, |
| "timeline": timeline, |
| "risks": risks, |
| } |
|
|
| |
| if not checklist: |
| checklist_empty_update = gr.update(value="ℹ Analyze a form to see results.", visible=True) |
| checklist_output_update = gr.update(visible=False) |
| else: |
| checklist_empty_update = gr.update(visible=False) |
| checklist_output_update = gr.update(value=checklist, visible=True) |
|
|
| |
| if not timeline: |
| timeline_empty_update = gr.update(value="ℹ No timeline information found in this form.", visible=True) |
| timeline_output_update = gr.update(visible=False) |
| else: |
| timeline_empty_update = gr.update(visible=False) |
| timeline_output_update = gr.update(value=timeline, visible=True) |
|
|
| |
| if not risks or (isinstance(risks, str) and risks.strip() == "✅ No major issues detected."): |
| risks_display = "ℹ No risks detected." |
| else: |
| risks_display = risks |
|
|
| |
| verification_empty_update = gr.update(value="ℹ Upload supporting documents to verify.", visible=True) |
| verification_output_update = gr.update(visible=False) |
| verification_summary_update = gr.update(value="", visible=False) |
|
|
| |
| if not overview: |
| overview_empty_update = gr.update(value="ℹ Upload a form first.", visible=True) |
| overview_output_update = gr.update(visible=False) |
| else: |
| overview_empty_update = gr.update(value="", visible=False) |
| overview_output_update = gr.update(value=overview, visible=True) |
|
|
| completeness = 0 |
| if master_json: |
| completeness += 25 |
| if checklist: |
| completeness += 25 |
| if timeline: |
| completeness += 25 |
| if risks_display and "No risks" not in str(risks_display): |
| completeness += 25 |
|
|
| return ( |
| master_json, |
| overview_empty_update, |
| overview_output_update, |
| checklist_empty_update, |
| checklist_output_update, |
| timeline_empty_update, |
| timeline_output_update, |
| risks_display, |
| verification_empty_update, |
| verification_output_update, |
| verification_summary_update, |
| gr.update(value=f"✅ Ready · Completeness {completeness}%", visible=True), |
| gr.update(value=[], visible=False), |
| ) |
|
|
|
|
| def run_eligibility_check(income, master_json): |
| if not master_json: |
| return "ℹ Upload a form first." |
| return check_eligibility(master_json, income) |
|
|
|
|
| def run_verification(files, master_json): |
| |
| |
| if not master_json: |
| return ( |
| gr.update(value="ℹ Upload supporting documents to verify.", visible=True), |
| gr.update(visible=False), |
| gr.update(value="", visible=False), |
| ) |
|
|
| if files is None: |
| files_list = [] |
| elif isinstance(files, list): |
| files_list = files |
| else: |
| files_list = [files] |
|
|
| if len(files_list) == 0: |
| return ( |
| gr.update(value="ℹ Upload supporting documents to verify.", visible=True), |
| gr.update(visible=False), |
| gr.update(value="", visible=False), |
| ) |
|
|
|
|
| if not files: |
| return ( |
| gr.update(value="ℹ Upload supporting documents to verify.", visible=True), |
| gr.update(visible=False), |
| gr.update(value="", visible=False), |
| ) |
|
|
| results = verify_documents(files_list, master_json) |
|
|
| summary = verification_summary(results) |
|
|
| return ( |
| gr.update(visible=False), |
| gr.update(value=results, visible=True), |
| gr.update(value=summary, visible=True), |
| ) |
|
|
|
|
| def run_qa(question: str, master_json: dict, chat_history): |
| """Chat handler for gr.Chatbot. |
| |
| Gradio 6.x expects Chatbot value to be either: |
| - list[dict{"role": "user"|"assistant", "content": "..."}] |
| - or list[gr.ChatMessage] |
| |
| We normalize to dict format to avoid "messages format" errors. |
| """ |
|
|
| if not master_json: |
| assistant = "ℹ Upload a form first." |
| else: |
| prompt = json.dumps({"question": question, "master_json": master_json}) |
| assistant = ask_llm(prompt) |
|
|
| |
| history = chat_history if isinstance(chat_history, list) else [] |
|
|
| def _to_msg(msg): |
| if isinstance(msg, dict) and "role" in msg and "content" in msg: |
| return msg |
| |
| if isinstance(msg, (list, tuple)) and len(msg) == 2: |
| return {"role": "user", "content": str(msg[0])} |
| return None |
|
|
| norm_history: List[Dict[str, str]] = [] |
| for m in history: |
| if isinstance(m, dict) and "role" in m and "content" in m: |
| norm_history.append({"role": m["role"], "content": m["content"]}) |
|
|
| |
| norm_history.append({"role": "user", "content": str(question)}) |
| norm_history.append({"role": "assistant", "content": str(assistant)}) |
|
|
| |
| return norm_history, "" |
|
|
|
|
| with gr.Blocks(title="PaperPilot") as demo: |
| master_json_state = gr.State() |
|
|
| with gr.Group(elem_id="appbar"): |
| gr.HTML( |
| """ |
| <div class="appbar"> |
| <div class="appbar-left"> |
| <div class="brand-mark">📄</div> |
| <div class="brand-text"> |
| <div class="brand-title">PaperPilot</div> |
| <div class="brand-subtitle">AI-powered scholarship & form assistant</div> |
| </div> |
| </div> |
| <div class="appbar-right"> |
| <div class="status-active" id="status-pill" aria-label="Active">● Active</div> |
| |
| </div> |
| </div> |
| """ |
| ) |
|
|
| with gr.Tabs(elem_id="pp-tabs"): |
|
|
| with gr.Tab("1) Analyze"): |
| with gr.Row(equal_height=True): |
| with gr.Column(scale=1): |
| with gr.Group(elem_id="card-upload", elem_classes=["pp-card"]): |
| gr.HTML( |
| """ |
| <div class="card-header"> |
| <div class="card-title">📤 Upload & Analyze</div> |
| <div class="card-subtitle">Start here</div> |
| </div> |
| """ |
| ) |
| file_input = gr.File(label="Form file (PDF / image)", file_count="single") |
| analyze_btn = gr.Button("Analyze Form", variant="primary") |
| clear_btn = gr.Button("Clear", variant="secondary") |
| with gr.Accordion("ℹ What happens", open=False): |
| gr.Markdown( |
| """ |
| - Extract text (OCR/PDF) |
| - Build structured JSON |
| - Generate: overview, checklist, timeline, risk alerts |
| - Enable eligibility + document verification |
| """ |
| ) |
|
|
| with gr.Column(scale=2): |
| with gr.Group(elem_classes=["pp-card"], elem_id="card-overview"): |
| gr.HTML( |
| """ |
| <div class="card-header"> |
| <div class="card-title">📋 Overview</div> |
| <div class="card-subtitle">Extracted summary</div> |
| </div> |
| """ |
| ) |
| overview_empty = gr.Markdown( |
| "ℹ Upload a form first.", visible=True, elem_classes=["pp-empty"] |
| ) |
| overview_output = gr.Markdown("", visible=False) |
|
|
| with gr.Row(equal_height=True): |
| with gr.Group(elem_classes=["pp-card"], elem_id="card-risks"): |
| gr.HTML( |
| """ |
| <div class="card-header"> |
| <div class="card-title">⚠ Risk Alerts</div> |
| <div class="card-subtitle">Potential gaps</div> |
| </div> |
| """ |
| ) |
| risk_output = gr.Markdown("ℹ No risks detected.", elem_classes=["pp-empty"]) |
|
|
| with gr.Group(elem_classes=["pp-card"], elem_id="card-timeline"): |
| gr.HTML( |
| """ |
| <div class="card-header"> |
| <div class="card-title">📅 Timeline</div> |
| <div class="card-subtitle">Important dates</div> |
| </div> |
| """ |
| ) |
| timeline_empty = gr.Markdown( |
| "ℹ No timeline information found in this form.", |
| visible=True, |
| elem_classes=["pp-empty"], |
| ) |
| timeline_output = gr.DataFrame( |
| headers=["Date", "Event"], |
| label="", |
| visible=False, |
| ) |
|
|
| with gr.Tab("2) Checklist"): |
| with gr.Row(equal_height=True): |
| with gr.Column(scale=2): |
| with gr.Group(elem_classes=["pp-card"], elem_id="card-checklist"): |
| gr.HTML( |
| """ |
| <div class="card-header"> |
| <div class="card-title">📑 Document Checklist</div> |
| <div class="card-subtitle">From extracted requirements</div> |
| </div> |
| """ |
| ) |
| checklist_empty = gr.Markdown( |
| "ℹ Analyze a form to see results.", |
| visible=True, |
| elem_classes=["pp-empty"], |
| ) |
| checklist_output = gr.DataFrame( |
| headers=["Documents", "Status"], |
| label="", |
| visible=False, |
| ) |
|
|
| with gr.Tab("3) Eligibility & Verify"): |
| with gr.Row(equal_height=True): |
| with gr.Column(scale=1): |
| with gr.Group(elem_classes=["pp-card"], elem_id="card-eligibility"): |
| gr.HTML( |
| """ |
| <div class="card-header"> |
| <div class="card-title">💳 Eligibility</div> |
| <div class="card-subtitle">Income-based check</div> |
| </div> |
| """ |
| ) |
| income_input = gr.Number(label="Annual Family Income (₹)", precision=0) |
| check_btn = gr.Button("Check Eligibility", variant="primary") |
| eligibility_output = gr.Markdown("ℹ Upload a form first.", elem_classes=["pp-empty"]) |
|
|
| with gr.Column(scale=2): |
| with gr.Group(elem_classes=["pp-card"], elem_id="card-verification"): |
| gr.HTML( |
| """ |
| <div class="card-header"> |
| <div class="card-title">📁 Document Verification</div> |
| <div class="card-subtitle">Upload supporting files</div> |
| </div> |
| """ |
| ) |
| verify_upload = gr.File(file_count="multiple", label="Supporting documents") |
| verify_btn = gr.Button("Verify Documents", variant="primary") |
| verification_empty = gr.Markdown( |
| "ℹ Upload supporting documents to verify.", |
| visible=True, |
| elem_classes=["pp-empty"], |
| ) |
| verification_output = gr.DataFrame( |
| headers=["Document", "Status"], |
| label="", |
| visible=False, |
| ) |
| verification_summary_output = gr.Markdown(visible=False) |
|
|
| with gr.Tab("4) Ask PaperPilot"): |
| with gr.Group(elem_classes=["pp-card"], elem_id="card-qa"): |
| gr.HTML( |
| """ |
| <div class="card-header"> |
| <div class="card-title">💬 Ask PaperPilot</div> |
| <div class="card-subtitle">Questions based on your extracted form</div> |
| </div> |
| """ |
| ) |
| chat_output = gr.Chatbot(value=[], elem_id="qa-chat", height=320) |
|
|
| with gr.Row(equal_height=False): |
| question_input = gr.Textbox( |
| label="Your question", |
| placeholder="e.g., What documents are required? What is the deadline?", |
| scale=4, |
| ) |
| ask_btn = gr.Button("Send", variant="primary", scale=1) |
|
|
| gr.Markdown( |
| """ |
| **Try:** What is the eligibility criteria? What documents are needed? What should I do next? |
| """, |
| elem_classes=["pp-muted"], |
| ) |
|
|
| |
| gr.Markdown( |
| """ |
| <div class="custom-footer"> |
| Built with ❤️ using: Gradio • EasyOCR • PyMuPDF • Python |
| <br/><b>PaperPilot</b> – Helping users understand forms effortlessly. |
| </div> |
| """, |
| elem_classes=["custom-footer"], |
| ) |
|
|
| |
| analyze_btn.click( |
| fn=analyze_document, |
| inputs=[file_input], |
| outputs=[ |
| master_json_state, |
| overview_empty, |
| overview_output, |
| checklist_empty, |
| checklist_output, |
| timeline_empty, |
| timeline_output, |
| risk_output, |
| verification_empty, |
| verification_output, |
| verification_summary_output, |
| gr.State(), |
| gr.State(), |
| ], |
| ) |
|
|
| def _clear_all(): |
| return ( |
| {}, |
| gr.update(value="ℹ Upload a form first.", visible=True), |
| gr.update(visible=False), |
| gr.update(value="ℹ Analyze a form to see results.", visible=True), |
| gr.update(visible=False), |
| gr.update(value="ℹ No timeline information found in this form.", visible=True), |
| gr.update(visible=False), |
| "ℹ No risks detected.", |
| gr.update(value="ℹ Upload supporting documents to verify.", visible=True), |
| gr.update(visible=False), |
| gr.update(value="", visible=False), |
| gr.update(value="ℹ Ready", visible=True), |
| [], |
| ) |
|
|
| |
| |
|
|
| check_btn.click( |
| fn=run_eligibility_check, |
| inputs=[income_input, master_json_state], |
| outputs=eligibility_output, |
| ) |
|
|
| verify_btn.click( |
| fn=run_verification, |
| inputs=[verify_upload, master_json_state], |
| outputs=[verification_empty, verification_output, verification_summary_output], |
| ) |
|
|
| ask_btn.click( |
| fn=run_qa, |
| inputs=[question_input, master_json_state, chat_output], |
| outputs=[chat_output, question_input], |
| ) |
|
|
| question_input.submit( |
| fn=run_qa, |
| inputs=[question_input, master_json_state, chat_output], |
| outputs=[chat_output, question_input], |
| ) |
|
|
|
|
| demo.launch(theme=gr.themes.Soft(), css=_read_css()) |
|
|
|
|