Spaces:
Running
Running
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
# StageΒ 1Β UXΒ Shell β FormPilot (no backend yet) | |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
import streamlit as st | |
import pandas as pd | |
from pathlib import Path | |
from io import BytesIO | |
import os | |
from dotenv import load_dotenv | |
load_dotenv() | |
from qdrant_client import QdrantClient | |
from langchain_community.vectorstores import Qdrant | |
from langchain_openai import OpenAIEmbeddings | |
from rag.qa_chain import get_answer | |
from rag.ocr_azure import parse_passport_azure | |
if "AZURE_DOC_KEY" not in os.environ: | |
st.warning("β οΈ OCR disabled β set AZURE_DOC_KEY & AZURE_DOC_ENDPOINT") | |
st.set_page_config( | |
page_title="FormPilot β Immigration Paralegal Copilot", | |
page_icon="π‘οΈ", | |
layout="wide", | |
) | |
# Add custom CSS for better display of extracted information | |
st.markdown(""" | |
<style> | |
.extracted-info { | |
background: #f7f7f7; | |
padding: 12px; | |
border-radius: 5px; | |
border-left: 3px solid #4CAF50; | |
margin-bottom: 15px; | |
} | |
.field-label { | |
font-weight: bold; | |
color: #333; | |
} | |
.field-value { | |
color: #1E88E5; | |
} | |
.success-value { | |
color: #4CAF50; | |
font-weight: bold; | |
} | |
.missing-value { | |
color: #F44336; | |
font-style: italic; | |
} | |
.verification-checklist { | |
background-color: #f9f9f9; | |
padding: 15px; | |
border-radius: 8px; | |
margin-top: 20px; | |
} | |
</style> | |
""", unsafe_allow_html=True) | |
# ------------------------------------------------------------ | |
# Helper: generate checklist table with extracted information | |
# ------------------------------------------------------------ | |
def build_checklist(profile=None): | |
fields = [ | |
("Iβ485 Part 1 β Full Name", "pending"), | |
("Iβ485 Part 1 β AβNumber", "pending"), | |
("Iβ485 Part 1 β Date of Birth", "pending"), | |
("Iβ485 Part 2 β Basis of Application", "pending"), | |
] | |
df = pd.DataFrame(fields, columns=["Field", "Status"]) | |
# Update with extracted information if available | |
if profile: | |
# Full Name | |
if profile.get('FirstName') and profile.get('LastName'): | |
full_name = f"{profile.get('FirstName')} {profile.get('LastName')}" | |
df.loc[df["Field"].str.contains("Full Name"), "Status"] = full_name | |
# A-Number | |
if profile.get('ANumber'): | |
df.loc[df["Field"].str.contains("AβNumber"), "Status"] = profile.get('ANumber') | |
# Date of Birth | |
if profile.get('DateOfBirth'): | |
df.loc[df["Field"].str.contains("Date of Birth"), "Status"] = profile.get('DateOfBirth') | |
return df | |
# ------------------------------------------------------------ | |
# Helper: Format extracted information for display | |
# ------------------------------------------------------------ | |
def display_extracted_info(profile): | |
if not profile: | |
return st.info("No information extracted from documents.") | |
st.markdown('<div class="verification-checklist">', unsafe_allow_html=True) | |
# Display First Name | |
st.markdown( | |
f'<div class="extracted-info">' | |
f'<span class="field-label">First Name:</span> ' | |
f'<span class="{"success-value" if profile.get("FirstName") else "missing-value"}">' | |
f'{profile.get("FirstName", "Not found")}</span>' | |
f'</div>', | |
unsafe_allow_html=True | |
) | |
# Display Last Name | |
st.markdown( | |
f'<div class="extracted-info">' | |
f'<span class="field-label">Last Name:</span> ' | |
f'<span class="{"success-value" if profile.get("LastName") else "missing-value"}">' | |
f'{profile.get("LastName", "Not found")}</span>' | |
f'</div>', | |
unsafe_allow_html=True | |
) | |
# Display Date of Birth | |
st.markdown( | |
f'<div class="extracted-info">' | |
f'<span class="field-label">Date of Birth:</span> ' | |
f'<span class="{"success-value" if profile.get("DateOfBirth") else "missing-value"}">' | |
f'{profile.get("DateOfBirth", "Not found")}</span>' | |
f'</div>', | |
unsafe_allow_html=True | |
) | |
# Display A-Number (if available) | |
st.markdown( | |
f'<div class="extracted-info">' | |
f'<span class="field-label">A-Number:</span> ' | |
f'<span class="{"success-value" if profile.get("ANumber") else "missing-value"}">' | |
f'{profile.get("ANumber", "Not applicable")}</span>' | |
f'</div>', | |
unsafe_allow_html=True | |
) | |
st.markdown('</div>', unsafe_allow_html=True) | |
# ------------------------------------------------------------ | |
# SidebarΒ β navigation + brand | |
# ------------------------------------------------------------ | |
with st.sidebar: | |
st.title("βοΈ+π+π‘οΈ+πΒ FormPilot") | |
if "stage" not in st.session_state: | |
st.session_state.stage = "home" | |
st.markdown("---") | |
if st.button("π Β Home"): | |
st.session_state.stage = "home" | |
if st.button("πΒ Draft Packet"): | |
if "uploaded_files" in st.session_state: | |
st.session_state.stage = "draft" | |
# ------------------------------------------------------------ | |
# StageΒ AΒ β Upload page | |
# ------------------------------------------------------------ | |
if st.session_state.stage == "home": | |
st.header("New Case β Build Iβ485 Package") | |
uploaded_files = st.file_uploader( | |
"Upload client documents (passport, visa, etc.)", | |
type=["pdf", "jpg", "jpeg", "png"], | |
accept_multiple_files=True, | |
) | |
form_choice = st.selectbox( | |
"Select USCIS Form to prepare", | |
options=["Iβ485 (Adjustment of Status)"], | |
) | |
# Store uploads in session so we can view later | |
if st.button("π Build Package"): | |
if not uploaded_files: | |
st.warning("Please upload at least one document.") | |
else: | |
st.session_state.uploaded_files = uploaded_files | |
st.session_state.form_choice = form_choice | |
st.session_state.stage = "draft" | |
st.rerun() | |
st.info( | |
""" | |
**What happens next?** | |
In StageΒ 2+ the AI will ingest your uploads, retrieve instructions, | |
and draft the packet. For now, we jump to a placeholder Draft view. | |
""" | |
) | |
# ------------------------------------------------------------ | |
# StageΒ BΒ β Draft Packet page | |
# ------------------------------------------------------------ | |
elif st.session_state.stage == "draft": | |
st.header("Draft Packet (Stage 3 β Retrieval MVP))") | |
# Two-column layout: preview & checklist | |
col_left, col_right = st.columns([2, 1]) | |
with col_left: | |
st.subheader("πΒ PDF Preview") | |
st.write( | |
"A preview of the filled Iβ485 will appear here once AI preβfill is ready." | |
) | |
st.image( | |
"https://placehold.co/600x800?text=PDF+Preview", | |
caption="Static placeholder preview", | |
) | |
with col_right: | |
st.subheader("β Document Information") | |
profile = {} | |
# Try to get profile from session state first | |
if "profile" in st.session_state: | |
profile = st.session_state.profile | |
# Otherwise, try to process the first uploaded file | |
elif "uploaded_files" in st.session_state and st.session_state.uploaded_files: | |
with st.spinner("Extracting information..."): | |
data = st.session_state.uploaded_files[0].getvalue() | |
try: | |
profile = parse_passport_azure(data) | |
st.session_state.profile = profile | |
except Exception as e: | |
st.error(f"Error extracting information: {str(e)}") | |
# Display nicely formatted extracted information | |
if profile and (profile.get("FirstName") or profile.get("LastName") or profile.get("DateOfBirth")): | |
display_extracted_info(profile) | |
# Show checklist with auto-filled fields | |
st.subheader("Form Field Checklist") | |
df = build_checklist(profile) | |
st.dataframe(df, hide_index=True, width=350) | |
# Show the raw JSON for debugging | |
with st.expander("Raw Extracted Data"): | |
st.json(profile) | |
else: | |
st.warning("No profile information extracted from documents.") | |
df = build_checklist() | |
st.dataframe(df, hide_index=True, width=350) | |
st.markdown("---") | |
st.subheader("Ask about Iβ485 instructions") | |
q = st.text_input("Your question", key="qa") | |
if q: | |
with st.spinner("Retrieving..."): | |
ans, cites = get_answer(q) | |
st.success(ans) | |
st.caption("Sources: " + ", ".join(sorted(cites))) | |
# ------------------------------------------------------------ | |
# (Optional) Future stage for OCR parsing | |
# ------------------------------------------------------------ | |
# Example usage of doc_parse_stub | |
if False: | |
for file in st.session_state.get("uploaded_files", []): | |
data = file.read() | |
parsed = parse_passport(data) | |
st.write(parsed) | |