Spaces:
Sleeping
Sleeping
Claude commited on
Redesign UI as a single guided flow with a modern, professional theme
Browse filesReplaces the two-tab layout with one linear page (Step 1: describe what
to extract via chat or quick-start buttons -> Step 2: upload multiple
PDFs/images -> Step 3: run and review results in tabs), so a business
owner can tell the assistant exactly what data it's looking for before
uploading documents for review. Adds an indigo Soft theme, card-style
sections, and one-click category starting points (Construction/SAP,
Compliance, Finance/AR) that pre-fill and send a chat prompt. Manual
settings and the HF token field are tucked into collapsed accordions
since the chat is now the primary path. Verified the full render and
quick-start -> chat interaction in a real browser.
app.py
CHANGED
|
@@ -7,7 +7,6 @@ amounts plus any custom labels the user names), and export everything to
|
|
| 7 |
CSV as tidy (one-fact-per-row) tables, alongside an audit record of the
|
| 8 |
settings that produced the run.
|
| 9 |
"""
|
| 10 |
-
import json
|
| 11 |
import os
|
| 12 |
import tempfile
|
| 13 |
import uuid
|
|
@@ -42,6 +41,32 @@ METADATA_COLUMNS = [
|
|
| 42 |
"custom_fields", "only_matches_filter", "chat_model", "chat_transcript",
|
| 43 |
]
|
| 44 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
def _file_path(f):
|
| 47 |
return f if isinstance(f, str) else f.name
|
|
@@ -186,83 +211,90 @@ def handle_chat(history, message, settings, hf_token):
|
|
| 186 |
)
|
| 187 |
|
| 188 |
|
| 189 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
gr.Markdown(
|
| 191 |
-
"# OCR Notification & Data Extractor\n"
|
| 192 |
-
"
|
| 193 |
-
"
|
| 194 |
-
"
|
| 195 |
-
"
|
| 196 |
-
"pick, plus any custom labels you name (e.g. \"gross weight\", "
|
| 197 |
-
"\"delivery date\"). Every run exports three tidy CSVs: extracted "
|
| 198 |
-
"fields, notification hits, and a metadata/audit record of exactly "
|
| 199 |
-
"what settings produced them."
|
| 200 |
)
|
| 201 |
|
| 202 |
settings_state = gr.State(dict(DEFAULT_SETTINGS))
|
| 203 |
|
| 204 |
-
with gr.
|
|
|
|
| 205 |
gr.Markdown(
|
| 206 |
-
"Describe what
|
| 207 |
-
"
|
| 208 |
-
"invoices and non-compliance notices, and pull the reference "
|
| 209 |
-
"number, gross weight, and delivery date\"*. This updates the "
|
| 210 |
-
"settings used in **Upload & Run** - you can still edit them by "
|
| 211 |
-
"hand there too."
|
| 212 |
-
)
|
| 213 |
-
hf_token_input = gr.Textbox(
|
| 214 |
-
label="Hugging Face token (optional if HF_TOKEN is set as a Space secret)",
|
| 215 |
-
type="password",
|
| 216 |
-
placeholder="hf_...",
|
| 217 |
)
|
| 218 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
with gr.Row():
|
| 220 |
chat_input = gr.Textbox(
|
| 221 |
-
label="Message", scale=4,
|
| 222 |
placeholder="e.g. Flag overdue invoices; pull reference number, gross weight, delivery date",
|
| 223 |
)
|
| 224 |
chat_send = gr.Button("Send", scale=1, variant="primary")
|
| 225 |
-
settings_display = gr.JSON(label="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 226 |
|
| 227 |
-
with gr.
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
label="Custom notification keywords (comma-separated)",
|
| 242 |
-
placeholder="e.g. re-inspection, warranty claim, hold payment",
|
| 243 |
-
info="Phrases that just raise a flag - not extracted as values.",
|
| 244 |
-
)
|
| 245 |
-
custom_fields_input = gr.Textbox(
|
| 246 |
-
label="Custom fields to extract as data (comma-separated labels)",
|
| 247 |
-
placeholder="e.g. reference number, gross weight, delivery date",
|
| 248 |
-
info="Labels looked up in the document; the value after each label is captured.",
|
| 249 |
-
)
|
| 250 |
-
only_matches_input = gr.Checkbox(
|
| 251 |
-
value=True,
|
| 252 |
-
label="Only export pages with a notification hit or extracted field",
|
| 253 |
-
)
|
| 254 |
-
run_button = gr.Button("Run OCR & Extract", variant="primary")
|
| 255 |
-
|
| 256 |
-
with gr.Column(scale=2):
|
| 257 |
-
status_output = gr.Textbox(label="Status", lines=3, interactive=False)
|
| 258 |
-
fields_table_output = gr.Dataframe(label="Extracted fields (tidy)", headers=FIELDS_COLUMNS, wrap=True)
|
| 259 |
-
notifications_table_output = gr.Dataframe(
|
| 260 |
-
label="Notification hits (tidy)", headers=NOTIFICATIONS_COLUMNS, wrap=True
|
| 261 |
-
)
|
| 262 |
-
csv_output = gr.Files(label="Download CSVs (fields, notifications, run metadata)")
|
| 263 |
-
run_metadata_output = gr.JSON(label="This run's settings (audit record)")
|
| 264 |
-
with gr.Accordion("Full OCR text (debug)", open=False):
|
| 265 |
-
text_output = gr.Textbox(label="Raw extracted text", lines=20)
|
| 266 |
|
| 267 |
chat_outputs = [
|
| 268 |
chatbot, chat_input, settings_state, settings_display,
|
|
@@ -271,6 +303,11 @@ with gr.Blocks(title="OCR Notification & Data Extractor") as demo:
|
|
| 271 |
chat_send.click(fn=handle_chat, inputs=[chatbot, chat_input, settings_state, hf_token_input], outputs=chat_outputs)
|
| 272 |
chat_input.submit(fn=handle_chat, inputs=[chatbot, chat_input, settings_state, hf_token_input], outputs=chat_outputs)
|
| 273 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 274 |
run_button.click(
|
| 275 |
fn=process_files,
|
| 276 |
inputs=[files_input, categories_input, custom_keywords_input, custom_fields_input, only_matches_input, chatbot],
|
|
|
|
| 7 |
CSV as tidy (one-fact-per-row) tables, alongside an audit record of the
|
| 8 |
settings that produced the run.
|
| 9 |
"""
|
|
|
|
| 10 |
import os
|
| 11 |
import tempfile
|
| 12 |
import uuid
|
|
|
|
| 41 |
"custom_fields", "only_matches_filter", "chat_model", "chat_transcript",
|
| 42 |
]
|
| 43 |
|
| 44 |
+
QUICK_PROMPTS = {
|
| 45 |
+
"🏗️ Construction / SAP": (
|
| 46 |
+
"Flag change orders, delay notices, and safety violations. "
|
| 47 |
+
"Pull PO numbers, SAP document numbers, and reference numbers."
|
| 48 |
+
),
|
| 49 |
+
"✅ Compliance": (
|
| 50 |
+
"Flag non-compliance, violations, and expired certifications. "
|
| 51 |
+
"Pull certification codes and expiration dates."
|
| 52 |
+
),
|
| 53 |
+
"💵 Finance / AR": (
|
| 54 |
+
"Flag overdue and past-due invoices, credit holds, and disputes. "
|
| 55 |
+
"Pull invoice numbers, amounts, and due dates."
|
| 56 |
+
),
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
CUSTOM_CSS = """
|
| 60 |
+
#hero { text-align: center; padding: 8px 8px 4px 8px; }
|
| 61 |
+
#hero h1 { font-size: 1.9rem; margin-bottom: 2px; }
|
| 62 |
+
#hero p { color: var(--body-text-color-subdued); max-width: 720px; margin: 0 auto; }
|
| 63 |
+
.step-card { border-radius: 16px !important; padding: 22px !important; margin-bottom: 18px; }
|
| 64 |
+
.step-kicker { font-weight: 600; font-size: 1.05rem; margin-bottom: 2px; }
|
| 65 |
+
.step-subtitle { color: var(--body-text-color-subdued); margin-bottom: 14px; font-size: 0.92rem; }
|
| 66 |
+
#run-button button { font-size: 1.05rem !important; padding: 14px !important; }
|
| 67 |
+
#quick-prompts button { font-size: 0.85rem !important; }
|
| 68 |
+
"""
|
| 69 |
+
|
| 70 |
|
| 71 |
def _file_path(f):
|
| 72 |
return f if isinstance(f, str) else f.name
|
|
|
|
| 211 |
)
|
| 212 |
|
| 213 |
|
| 214 |
+
theme = gr.themes.Soft(
|
| 215 |
+
primary_hue="indigo",
|
| 216 |
+
secondary_hue="slate",
|
| 217 |
+
neutral_hue="slate",
|
| 218 |
+
font=[gr.themes.GoogleFont("Inter"), "ui-sans-serif", "system-ui", "sans-serif"],
|
| 219 |
+
)
|
| 220 |
+
|
| 221 |
+
with gr.Blocks(title="OCR Notification & Data Extractor", theme=theme, css=CUSTOM_CSS) as demo:
|
| 222 |
gr.Markdown(
|
| 223 |
+
"# 🧾 OCR Notification & Data Extractor\n"
|
| 224 |
+
"Tell it what you need, upload your documents, and get a clean, "
|
| 225 |
+
"audit-ready CSV back — built for construction/SAP, compliance, "
|
| 226 |
+
"and finance teams.",
|
| 227 |
+
elem_id="hero",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
)
|
| 229 |
|
| 230 |
settings_state = gr.State(dict(DEFAULT_SETTINGS))
|
| 231 |
|
| 232 |
+
with gr.Group(elem_classes=["step-card"]):
|
| 233 |
+
gr.Markdown('<div class="step-kicker">Step 1 · Tell us what you need</div>')
|
| 234 |
gr.Markdown(
|
| 235 |
+
'<div class="step-subtitle">Describe what to flag and what to pull as data, '
|
| 236 |
+
"in your own words — or tap a starting point below.</div>"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 237 |
)
|
| 238 |
+
with gr.Row(elem_id="quick-prompts"):
|
| 239 |
+
quick_buttons = {label: gr.Button(label, size="sm") for label in QUICK_PROMPTS}
|
| 240 |
+
|
| 241 |
+
with gr.Accordion("Use your own Hugging Face token (optional)", open=False):
|
| 242 |
+
gr.Markdown("Only needed if this Space's `HF_TOKEN` secret isn't set, or you want to use your own.")
|
| 243 |
+
hf_token_input = gr.Textbox(label="Hugging Face token", type="password", placeholder="hf_...", show_label=False)
|
| 244 |
+
|
| 245 |
+
chatbot = gr.Chatbot(height=320, label="Extraction assistant")
|
| 246 |
with gr.Row():
|
| 247 |
chat_input = gr.Textbox(
|
| 248 |
+
label="Message", scale=4, show_label=False, container=False,
|
| 249 |
placeholder="e.g. Flag overdue invoices; pull reference number, gross weight, delivery date",
|
| 250 |
)
|
| 251 |
chat_send = gr.Button("Send", scale=1, variant="primary")
|
| 252 |
+
settings_display = gr.JSON(label="What we'll extract", value=DEFAULT_SETTINGS)
|
| 253 |
+
|
| 254 |
+
with gr.Group(elem_classes=["step-card"]):
|
| 255 |
+
gr.Markdown('<div class="step-kicker">Step 2 · Upload your documents</div>')
|
| 256 |
+
gr.Markdown('<div class="step-subtitle">Upload as many PDFs or images as you need reviewed at once.</div>')
|
| 257 |
+
files_input = gr.Files(
|
| 258 |
+
label="PDF or image files",
|
| 259 |
+
file_types=[".pdf", ".png", ".jpg", ".jpeg", ".tif", ".tiff", ".bmp"],
|
| 260 |
+
file_count="multiple",
|
| 261 |
+
)
|
| 262 |
+
with gr.Accordion("Advanced: edit settings manually", open=False):
|
| 263 |
+
categories_input = gr.CheckboxGroup(
|
| 264 |
+
choices=CATEGORY_CHOICES,
|
| 265 |
+
value=CATEGORY_CHOICES,
|
| 266 |
+
label="Notification categories",
|
| 267 |
+
info="Also scopes which standard fields (PO/SAP/invoice/cert) get extracted.",
|
| 268 |
+
)
|
| 269 |
+
custom_keywords_input = gr.Textbox(
|
| 270 |
+
label="Custom notification keywords (comma-separated)",
|
| 271 |
+
placeholder="e.g. re-inspection, warranty claim, hold payment",
|
| 272 |
+
info="Phrases that just raise a flag - not extracted as values.",
|
| 273 |
+
)
|
| 274 |
+
custom_fields_input = gr.Textbox(
|
| 275 |
+
label="Custom fields to extract as data (comma-separated labels)",
|
| 276 |
+
placeholder="e.g. reference number, gross weight, delivery date",
|
| 277 |
+
info="Labels looked up in the document; the value after each label is captured.",
|
| 278 |
+
)
|
| 279 |
+
only_matches_input = gr.Checkbox(
|
| 280 |
+
value=True,
|
| 281 |
+
label="Only export pages with a notification hit or extracted field",
|
| 282 |
+
)
|
| 283 |
|
| 284 |
+
with gr.Group(elem_classes=["step-card"]):
|
| 285 |
+
gr.Markdown('<div class="step-kicker">Step 3 · Run & review</div>')
|
| 286 |
+
run_button = gr.Button("Run OCR & Extract", variant="primary", elem_id="run-button")
|
| 287 |
+
status_output = gr.Textbox(label="Status", lines=2, interactive=False)
|
| 288 |
+
csv_output = gr.Files(label="Download CSVs (fields, notifications, run metadata)")
|
| 289 |
+
with gr.Tabs():
|
| 290 |
+
with gr.Tab("Extracted fields"):
|
| 291 |
+
fields_table_output = gr.Dataframe(headers=FIELDS_COLUMNS, wrap=True)
|
| 292 |
+
with gr.Tab("Notification hits"):
|
| 293 |
+
notifications_table_output = gr.Dataframe(headers=NOTIFICATIONS_COLUMNS, wrap=True)
|
| 294 |
+
with gr.Tab("Run audit"):
|
| 295 |
+
run_metadata_output = gr.JSON(label="Exactly what settings produced this run")
|
| 296 |
+
with gr.Tab("Raw OCR text (debug)"):
|
| 297 |
+
text_output = gr.Textbox(label="Raw extracted text", lines=20, show_label=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 298 |
|
| 299 |
chat_outputs = [
|
| 300 |
chatbot, chat_input, settings_state, settings_display,
|
|
|
|
| 303 |
chat_send.click(fn=handle_chat, inputs=[chatbot, chat_input, settings_state, hf_token_input], outputs=chat_outputs)
|
| 304 |
chat_input.submit(fn=handle_chat, inputs=[chatbot, chat_input, settings_state, hf_token_input], outputs=chat_outputs)
|
| 305 |
|
| 306 |
+
for label, prompt in QUICK_PROMPTS.items():
|
| 307 |
+
quick_buttons[label].click(fn=lambda p=prompt: p, outputs=chat_input).then(
|
| 308 |
+
fn=handle_chat, inputs=[chatbot, chat_input, settings_state, hf_token_input], outputs=chat_outputs
|
| 309 |
+
)
|
| 310 |
+
|
| 311 |
run_button.click(
|
| 312 |
fn=process_files,
|
| 313 |
inputs=[files_input, categories_input, custom_keywords_input, custom_fields_input, only_matches_input, chatbot],
|