"""After-Shift Admin Assistant — Gradio app. Flow: record/upload -> transcribe -> extract -> EDIT & CONFIRM -> save + outputs The confirm step shows every extracted field in an editable table before anything is generated or saved. Money is on the line, so she sees what the AI heard and can fix it in two seconds — which also means a live demo can't be derailed by one misheard word. """ from __future__ import annotations import html as html_lib import inspect import math import os from datetime import datetime # Off the Grid: disable Gradio's telemetry before importing it, so nothing phones home. os.environ.setdefault("GRADIO_ANALYTICS_ENABLED", "False") import gradio as gr import database as db from config import CURRENCY from extractor import extract from generators import generate_all, money from transcriber import transcribe # Gradio can't create components dynamically, so we pre-build this many editable # order cards and show only as many as extraction (or "Add order manually") needs. MAX_ORDERS = 5 CARD_FIELDS = 8 # customer, items, total, paid, method, pickup, ingredients, notes APP_CSS = """ :root { /* Warm Bakery Theme — flour-dusted parchment, toasted caramel */ --asa-bg: #f1e8d8; --asa-surface: rgba(255, 253, 250, 0.96); --asa-surface-solid: #fffdfa; --asa-surface-soft: rgba(252, 247, 239, 0.95); --asa-text: #33271d; --asa-muted: #6e5f50; /* >= 4.5:1 on the cream surfaces (WCAG AA) */ --asa-deep: #8a4f12; --asa-serif: "Iowan Old Style", "Palatino", Georgia, "Times New Roman", serif; --asa-line: rgba(120, 85, 45, 0.16); --asa-line-strong: rgba(120, 85, 45, 0.26); --asa-primary: #c8791f; --asa-primary-hover: #b56c16; --asa-fill: rgba(120, 85, 45, 0.07); --asa-fill-hover: rgba(120, 85, 45, 0.13); --asa-radius: 20px; --asa-radius-sm: 12px; --asa-shadow: 0 1px 2px rgba(60,40,20,0.05), 0 14px 40px -14px rgba(90,60,25,0.22); --asa-ease: cubic-bezier(0.32, 0.72, 0, 1); --asa-font: -apple-system, BlinkMacSystemFont, "SF Pro Display", "SF Pro Text", "Helvetica Neue", sans-serif; } body, .gradio-container, .gradio-container * { font-family: var(--asa-font) !important; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } html, body, .gradio-container { min-height: 100vh !important; margin: 0; } body, .gradio-container { color: var(--asa-text) !important; background: radial-gradient(1200px 620px at 8% -10%, #f8efdd 0%, rgba(248,239,221,0) 55%), radial-gradient(1050px 560px at 100% 4%, #f0e6d2 0%, rgba(240,230,210,0) 50%), linear-gradient(180deg, #f5eee0 0%, #ece1cd 100%) !important; background-attachment: fixed !important; background-size: cover !important; } /* The host paints solid white full-width over the body gradient. Make it (and the html root) transparent so the warm wash fills the whole screen. */ html { background: transparent !important; } gradio-app { background: transparent !important; display: block; min-height: 100vh; } /* Hide Gradio default footer and padding */ footer { display: none !important; } .gradio-container { max-width: 1080px !important; margin: 0 auto !important; padding: 40px 24px 80px !important; /* Neutralize Gradio's white component wrappers so warm panels show through */ --block-background-fill: transparent; --block-border-color: transparent; --block-shadow: none; --panel-background-fill: transparent; --background-fill-primary: transparent; --background-fill-secondary: transparent; --border-color-primary: var(--asa-line); --input-background-fill: var(--asa-fill); --input-border-color: transparent; --body-text-color: var(--asa-text); --block-label-text-color: var(--asa-muted); --block-info-text-color: var(--asa-muted); } /* Belt-and-braces: blank out any remaining Gradio block/form wrappers */ .gradio-container .block, .gradio-container .form, .gradio-container [data-testid="block"] { background: transparent !important; border: none !important; box-shadow: none !important; } /* ---------- Typography & Spacing ---------- */ h1, h2, h3, h4, h5, h6 { letter-spacing: -0.02em !important; } p, span, div { letter-spacing: -0.01em; } /* ---------- Hero ---------- */ .app-header { display: grid; grid-template-columns: 1fr; background: linear-gradient(155deg, rgba(255,250,242,0.94) 0%, rgba(247,233,213,0.88) 100%); backdrop-filter: blur(40px) saturate(118%); -webkit-backdrop-filter: blur(40px) saturate(118%); border: 1px solid var(--asa-line); box-shadow: var(--asa-shadow); border-radius: 32px; padding: 48px; margin-bottom: 24px; transition: all 0.5s var(--asa-ease); } .app-kicker { font-size: 14px; font-weight: 600; letter-spacing: 0.04em !important; color: var(--asa-primary) !important; text-transform: uppercase; margin: 0 0 12px; } .app-header h1 { color: var(--asa-text) !important; font-family: var(--asa-serif) !important; font-size: 54px; font-weight: 600; line-height: 1.04; letter-spacing: -0.015em !important; margin: 0 0 16px; } .app-header p { color: var(--asa-muted) !important; font-size: 19px; font-weight: 400; line-height: 1.5; margin: 0; max-width: 540px; } .hero-tagline { margin-top: 18px !important; font-size: 14px !important; font-weight: 600; letter-spacing: .02em; color: var(--asa-deep) !important; } /* ---------- Stat cards ---------- */ .summary-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 16px; margin: 24px 0 32px; } .stat-card { background: var(--asa-surface); backdrop-filter: blur(30px) saturate(118%); -webkit-backdrop-filter: blur(30px) saturate(118%); border: 1px solid var(--asa-line); border-radius: var(--asa-radius); padding: 24px; min-height: 110px; box-shadow: var(--asa-shadow); transition: transform 0.4s var(--asa-ease), box-shadow 0.4s var(--asa-ease), background 0.4s var(--asa-ease); } .stat-card:hover { transform: scale(1.02) translateY(-2px); box-shadow: 0 12px 32px rgba(0,0,0,0.08), 0 2px 4px rgba(0,0,0,0.04); } .stat-label { color: var(--asa-muted); font-size: 14px; font-weight: 500; margin-bottom: 12px; } .stat-value { color: var(--asa-text); font-family: var(--asa-serif) !important; font-size: 38px; line-height: 1.08; font-weight: 600; letter-spacing: -0.01em; } .stat-card:first-child .stat-value { color: var(--asa-deep); } .stat-note { color: var(--asa-muted); font-size: 13px; margin-top: 8px; } /* ---------- Panels ---------- */ .panel { background: var(--asa-surface); backdrop-filter: blur(40px) saturate(118%); -webkit-backdrop-filter: blur(40px) saturate(118%); border: 1px solid var(--asa-line); border-radius: 28px; padding: 32px; box-shadow: var(--asa-shadow); } .panel-soft { background: var(--asa-surface-soft); } .step-head { display: grid; grid-template-columns: 36px 1fr; gap: 16px; align-items: start; margin-bottom: 24px; } .step-num { display: inline-grid; place-items: center; width: 36px; height: 36px; border-radius: 50%; background: var(--asa-primary); color: #fff; font-weight: 600; box-shadow: 0 4px 16px rgba(200, 121, 31, 0.4); } .step-head h2 { font-family: var(--asa-serif) !important; font-size: 26px; font-weight: 600; line-height: 1.15; letter-spacing: -0.01em; margin: 0 0 6px; } .step-head p { color: var(--asa-muted); font-size: 15px; margin: 0; line-height: 1.5; } .sample-note { background: var(--asa-fill); border-radius: var(--asa-radius-sm); padding: 16px; color: var(--asa-text); font-size: 14px; line-height: 1.5; margin-top: 16px; } .sample-note strong { color: var(--asa-text); font-weight: 600; } .status-line { background: var(--asa-surface-solid); border-radius: var(--asa-radius-sm); padding: 13px 16px; font-size: 15px; font-weight: 500; color: var(--asa-text) !important; margin-bottom: 16px; border: 1px solid var(--asa-line) !important; border-left: 4px solid var(--asa-primary) !important; } .status-line * { color: var(--asa-text) !important; } /* Readable placeholders + intentional selection color */ .gradio-container ::placeholder { color: #7d6d5c !important; opacity: 1 !important; } ::selection { background: rgba(200, 121, 31, 0.28); } /* ---------- Buttons: native bounce ---------- */ .gradio-container button { border-radius: 980px !important; font-weight: 500 !important; letter-spacing: -0.01em; cursor: pointer; transition: transform 0.3s var(--asa-ease), background-color 0.2s ease, box-shadow 0.3s var(--asa-ease), opacity 0.2s ease !important; will-change: transform; } /* Secondary = ghost/outlined, clearly distinct from the solid primary CTAs */ .gradio-container button.secondary { background: transparent !important; color: var(--asa-text) !important; border: 1.5px solid var(--asa-line-strong) !important; } .gradio-container button.secondary:hover { background: var(--asa-fill-hover) !important; border-color: var(--asa-primary) !important; } .gradio-container button.secondary:active { transform: scale(0.96); } /* Disabled is unmistakably disabled — enabled ghosts keep full-contrast text */ .gradio-container button:disabled { opacity: 0.4 !important; cursor: not-allowed !important; transform: none !important; box-shadow: none !important; } .gradio-container button.primary { background: var(--asa-primary) !important; color: #fff !important; border: none !important; box-shadow: 0 4px 14px rgba(200, 121, 31, 0.4) !important; } .gradio-container button.primary:hover { background: var(--asa-primary-hover) !important; box-shadow: 0 6px 20px rgba(200, 121, 31, 0.5) !important; } .gradio-container button.primary:active { transform: scale(0.96); box-shadow: 0 2px 8px rgba(200, 121, 31, 0.3) !important; } /* ---------- Tabs as Segmented Controls ---------- */ .gradio-container .tab-nav { border: none !important; gap: 4px; background: var(--asa-fill); padding: 4px; border-radius: 12px; width: fit-content; margin-bottom: 24px; } .gradio-container .tab-nav button { border: none !important; background: transparent !important; border-radius: 9px !important; color: var(--asa-muted) !important; padding: 9px 18px !important; font-size: 12.5px !important; font-weight: 600 !important; text-transform: uppercase !important; letter-spacing: .05em !important; transition: all 0.2s ease !important; } .gradio-container .tab-nav button:hover { color: var(--asa-text) !important; background: var(--asa-fill-hover) !important; } .gradio-container .tab-nav button.selected { background: var(--asa-surface-solid) !important; color: var(--asa-deep) !important; box-shadow: 0 1px 3px rgba(90,60,25,0.12), 0 0 0 1px var(--asa-line) !important; } /* ---------- Inputs / Table ---------- */ .gradio-container textarea, .gradio-container input, .gradio-container .gr-box { background-color: var(--asa-fill) !important; border-radius: var(--asa-radius-sm) !important; border: 1px solid transparent !important; color: var(--asa-text) !important; transition: all 0.2s var(--asa-ease) !important; } .gradio-container textarea:focus, .gradio-container input:focus { background-color: var(--asa-surface-solid) !important; border-color: var(--asa-primary) !important; box-shadow: 0 0 0 4px rgba(200, 121, 31, 0.2) !important; } /* Make tables blend perfectly */ .gradio-container table { font-size: 14px; border-radius: var(--asa-radius-sm); overflow: hidden; border: none !important; } .gradio-container table thead th { background: var(--asa-fill) !important; font-weight: 600 !important; color: var(--asa-muted) !important; border: none !important; } /* Hide useless ellipsis/sort buttons in table headers */ .gradio-container table thead th button, .gradio-container table thead th svg { display: none !important; } /* Completely hide maximize/fullscreen buttons as they break our native table styling */ .gradio-container button[aria-label="Fullscreen"], .gradio-container button[aria-label="Expand"], .gradio-container button[title="fullscreen"], .gradio-container button[aria-label*="ullscreen"], .gradio-container button svg.feather-maximize, .gradio-container [data-testid="dataframe"] .icon-button { display: none !important; } .gradio-container table tbody tr { transition: background-color 0.2s ease; border-bottom: 1px solid var(--asa-line) !important; } .gradio-container table tbody tr:hover { background: var(--asa-fill) !important; } .gradio-container table tbody td { border: none !important; color: var(--asa-text) !important; } /* Clean up markdown links / specific Gradio overrides */ .gradio-container a { color: var(--asa-primary) !important; text-decoration: none !important; } .gradio-container a:hover { text-decoration: underline !important; } /* Markdown tables */ .prose table { width: 100%; } .align-end { align-items: flex-end !important; } /* ---------- Step 1: the recorder is the hero ---------- */ .recorder-zone .audio-container, .recorder-zone .component-wrapper { min-height: 160px; } .recorder-zone button { font-size: 15px !important; } /* Clean white inset card around the audio widget so it feels deliberate */ .recorder-zone .audio-container { background: var(--asa-surface-solid) !important; border: 1px solid var(--asa-line) !important; border-radius: 16px !important; padding: 18px !important; } /* Big, unmistakable record control (Gradio class names vary across versions) */ .recorder-zone .record-button, .recorder-zone button.record, .recorder-zone button[aria-label*="ecord"] { transform: scale(1.18); transform-origin: left center; font-weight: 600 !important; } /* Pulse while recording (stop button is shown during capture) */ @keyframes asa-pulse { 0% { box-shadow: 0 0 0 0 rgba(196, 64, 38, 0.45); } 70% { box-shadow: 0 0 0 14px rgba(196, 64, 38, 0); } 100% { box-shadow: 0 0 0 0 rgba(196, 64, 38, 0); } } .recorder-zone .stop-button, .recorder-zone button.stop, .recorder-zone button[aria-label*="top recording"] { animation: asa-pulse 1.6s infinite; } /* Never truncate the mic status/permission message (it's a narrow