Claude commited on
Commit
9e437ab
·
unverified ·
1 Parent(s): 1d905fa

Redesign UI as a single guided flow with a modern, professional theme

Browse files

Replaces 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.

Files changed (1) hide show
  1. app.py +102 -65
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
- with gr.Blocks(title="OCR Notification & Data Extractor") as demo:
 
 
 
 
 
 
 
190
  gr.Markdown(
191
- "# OCR Notification & Data Extractor\n"
192
- "Upload scanned PDFs or images (construction reports, compliance "
193
- "certificates, invoices, SAP printouts, etc.). The app OCRs each "
194
- "page, flags notification-worthy keywords per domain, and pulls out "
195
- "labeled data fields - standard ones scoped to the categories you "
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.Tab("1. Tailor extraction (chat)"):
 
205
  gr.Markdown(
206
- "Describe what you want **flagged** (notifications) vs. **pulled "
207
- "as data** (fields), in plain language, e.g. *\"flag overdue "
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
- chatbot = gr.Chatbot(height=350, label="Extraction assistant")
 
 
 
 
 
 
 
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="Active extraction settings", value=DEFAULT_SETTINGS)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
226
 
227
- with gr.Tab("2. Upload & Run"):
228
- with gr.Row():
229
- with gr.Column(scale=1):
230
- files_input = gr.Files(
231
- label="PDF or image files",
232
- file_types=[".pdf", ".png", ".jpg", ".jpeg", ".tif", ".tiff", ".bmp"],
233
- )
234
- categories_input = gr.CheckboxGroup(
235
- choices=CATEGORY_CHOICES,
236
- value=CATEGORY_CHOICES,
237
- label="Notification categories",
238
- info="Also scopes which standard fields (PO/SAP/invoice/cert) get extracted - see CATEGORY_FIELD_MAP.",
239
- )
240
- custom_keywords_input = gr.Textbox(
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 &amp; 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],