import gradio as gr from data import STORE_NAMES, SAMPLE_PO_EXAMPLES, make_default_stock from services import ( parse_po, save_po, build_demand_table, calculate_printing, generate_delivery_doc, generate_bestseller_report, format_po_log, export_all_pos_csv, run_full_workflow, ) # --------------------------------------------------------------------------- # Design system — Paperain light mode (high contrast, cream + dark text) # --------------------------------------------------------------------------- CUSTOM_CSS = """ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Montserrat:wght@400;600;700;800&display=swap'); :root { --primary: #2D2D2D; --primary-container: #FFF4D6; --on-primary-container: #1A1A1A; --secondary: #5C5C5C; --secondary-container: #F0F0EE; --on-secondary-container: #1A1A1A; --surface: #FFFFFF; --surface-low: #F5F4F1; --background: #FFFFFF; --on-surface: #1A1A1A; --on-surface-variant:#3D3D3D; --outline: #E0DDD6; --honey: #C9A227; --radius: 16px; --radius-lg: 24px; --radius-pill: 999px; --shadow-soft: 0 4px 24px rgba(104, 95, 48, 0.08); --shadow-card: 0 2px 12px rgba(28, 27, 27, 0.05); color-scheme: light !important; --body-background-fill: var(--background) !important; --background-fill-primary: var(--surface) !important; --background-fill-secondary: var(--surface-low) !important; --block-background-fill: transparent !important; --block-border-color: transparent !important; --body-text-color: #1A1A1A !important; --body-text-color-subdued: #444444 !important; --block-label-text-color: #1A1A1A !important; --input-background-fill: #FFFFFF !important; --input-text-color: #1A1A1A !important; --input-border-color: var(--outline) !important; --border-color-primary: var(--outline) !important; --table-text-color: #1A1A1A !important; --table-background-fill: #FFFFFF !important; --table-even-background-fill: #F5F4F1 !important; --table-odd-background-fill: #FFFFFF !important; --table-border-color: var(--outline) !important; --checkbox-label-text-color: #1A1A1A !important; --radio-label-text-color: #1A1A1A !important; --block-label-background-fill: #F5F4F1 !important; --block-title-background-fill: #F5F4F1 !important; --checkbox-label-background-fill: #FFFFFF !important; --checkbox-label-border-color: #E0DDD6 !important; } /* ── Force readable dark text on labels/markdown (not buttons or tables) ── */ .gradio-container label, .gradio-container .label-wrap, .gradio-container .label-wrap span, .gradio-container .block-label, .gradio-container fieldset legend, .gradio-container .markdown, .gradio-container .prose, .gradio-container .prose p, .gradio-container .markdown p, .gradio-container .tab-hint, .gradio-container .tab-hint span { color: #1A1A1A !important; } .gradio-container textarea, .gradio-container input, .gradio-container select, .gradio-container .gr-textbox textarea, .gradio-container .gr-textbox input, .gradio-container [contenteditable] { color: #1A1A1A !important; -webkit-text-fill-color: #1A1A1A !important; background: #FFFFFF !important; } .gradio-container table, .gradio-container td, .gradio-container th, .gradio-container .table-wrap, .gradio-container .dataframe { color: #1A1A1A !important; background-color: #FFFFFF !important; } .gradio-container table thead th { background: #F0EDE8 !important; color: #1A1A1A !important; } .gradio-container table tbody td { background: #FFFFFF !important; color: #1A1A1A !important; } .gradio-container table tbody tr:nth-child(even) td { background: #F5F4F1 !important; } .gradio-container .wrap > label, .gradio-container .form > label, .gradio-container .svelte-1gfkn6j { color: #1A1A1A !important; } /* ── Full-width: kill HF Spaces dark gutters ── */ html, body, #root, .app, main, .wrap, .contain, footer.svelte-1ax1toq { background: var(--background) !important; color-scheme: light !important; } .contain { max-width: 100% !important; padding: 0 !important; margin: 0 !important; } .gradio-container { max-width: 100% !important; width: 100% !important; margin: 0 !important; padding: 0 0 72px !important; font-family: 'Inter', system-ui, sans-serif !important; background: var(--background) !important; color: var(--on-surface) !important; } .gradio-container .block, .gradio-container .form, .gradio-container .column, .gradio-container .row { background: transparent !important; border: none !important; box-shadow: none !important; } .gradio-container h1, .gradio-container h2, .gradio-container h3, .gradio-container .prose h1, .gradio-container .prose h2, .gradio-container .prose h3 { font-family: 'Montserrat', sans-serif !important; color: var(--on-surface) !important; letter-spacing: -0.02em; } .gradio-container p, .gradio-container span, .gradio-container label, .gradio-container .markdown, .gradio-container .prose { font-family: 'Inter', sans-serif !important; color: var(--on-surface-variant) !important; line-height: 1.6; } /* ── Top nav ── */ .top-nav { position: sticky; top: 0; z-index: 100; background: rgba(250, 250, 248, 0.95); backdrop-filter: blur(12px); border-bottom: 1px solid var(--outline); margin-bottom: 0; } .top-nav-inner { max-width: 1400px; margin: 0 auto; padding: 14px 40px; display: flex; align-items: center; justify-content: space-between; gap: 16px; } .top-nav-brand { font-family: 'Montserrat', sans-serif; font-weight: 700; font-size: 1.15rem; color: var(--primary); display: flex; align-items: center; gap: 8px; } .top-nav-brand span.icon { font-size: 1.3rem; } .top-nav-meta { font-size: 0.75rem; font-weight: 600; color: var(--on-surface-variant); letter-spacing: 0.04em; text-transform: uppercase; } @media (max-width: 768px) { .top-nav-inner { padding: 12px 16px; } .top-nav-meta { display: none; } } /* ── App shell ── */ .app-shell { max-width: 1400px; margin: 0 auto; padding: 32px 40px 0; width: 100%; box-sizing: border-box; } @media (max-width: 768px) { .app-shell { padding: 20px 16px 0; } } /* ── Hero ── */ .hero-section { background: var(--surface); border: 1px solid var(--outline); border-radius: var(--radius-lg); padding: 40px 44px 36px; margin-bottom: 24px; box-shadow: var(--shadow-soft); } .hero-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 32px; align-items: start; } @media (max-width: 900px) { .hero-grid { grid-template-columns: 1fr; } } .hero-section h1 { font-size: clamp(2rem, 4vw, 2.75rem); margin: 0 0 10px; color: var(--on-surface) !important; line-height: 1.15; } .hero-tagline { font-size: 1.05rem; color: var(--on-surface-variant); margin: 0 0 20px; line-height: 1.55; max-width: 520px; } .hero-meta { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; } .hero-pill { display: inline-flex; align-items: center; gap: 5px; background: var(--primary-container); color: var(--on-primary-container); font-size: 0.72rem; font-weight: 700; padding: 6px 14px; border-radius: var(--radius-pill); letter-spacing: 0.03em; text-transform: uppercase; white-space: nowrap; } .hero-pill.blue { background: var(--secondary-container); color: var(--on-secondary-container); } .hero-pill.muted { background: var(--surface-low); color: var(--on-surface-variant); font-weight: 600; text-transform: none; letter-spacing: 0; } .stats-row { display: grid; grid-template-columns: repeat(2, 1fr); gap: 14px; } @media (min-width: 600px) { .stats-row { grid-template-columns: repeat(4, 1fr); } } .stat { background: var(--surface-low); border: 1px solid var(--outline); border-radius: var(--radius); padding: 20px 14px; text-align: center; transition: transform 0.15s, box-shadow 0.15s; } .stat:hover { transform: translateY(-2px); box-shadow: var(--shadow-card); } .stat:nth-child(1) { border-left: 4px solid var(--honey); background: #FFF8E7; } .stat:nth-child(2) { border-left: 4px solid #8B9DAF; background: #F0F4F8; } .stat:nth-child(3) { border-left: 4px solid #6B6B6B; background: #F5F4F1; } .stat:nth-child(4) { border-left: 4px solid #7CB342; background: #F0F7EA; } .stat .number { font-family: 'Montserrat', sans-serif; font-size: 1.85rem; font-weight: 800; color: var(--on-surface) !important; line-height: 1.1; display: block; white-space: nowrap; } .stat .label { font-size: 0.68rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; color: var(--on-surface-variant) !important; margin-top: 6px; display: block; } /* ── Workflow steps ── */ .step-flow { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-top: 28px; padding-top: 24px; border-top: 1px solid var(--outline); } .step-flow .step { background: var(--surface-low); border: 1px solid var(--outline); border-radius: var(--radius-pill); padding: 8px 16px; font-size: 0.8rem; font-weight: 600; color: var(--on-surface-variant); } .step-flow .step.active { background: var(--primary-container); border-color: var(--primary); color: var(--on-surface); } .step-flow .arrow { color: var(--outline); font-size: 0.85rem; } /* ── Tabs (Gradio 6 — force dark labels) ── */ .tabs, .tab-nav, .tabitem { border: none !important; background: transparent !important; } .tab-nav { border-bottom: 2px solid #D0D0D0 !important; gap: 4px !important; margin-bottom: 0 !important; padding: 0 !important; flex-wrap: wrap !important; } .gradio-container .tabs button, .gradio-container .tab-nav button, .gradio-container button[role="tab"], .gradio-container .tab-nav > button { font-family: 'Inter', sans-serif !important; font-weight: 600 !important; font-size: 0.9rem !important; border-radius: 8px 8px 0 0 !important; border: 1px solid #D0D0D0 !important; border-bottom: none !important; background: #F5F5F5 !important; color: #111111 !important; -webkit-text-fill-color: #111111 !important; padding: 10px 18px !important; margin-bottom: -1px !important; } .gradio-container .tabs button:hover, .gradio-container .tab-nav button:hover { background: #EEEEEE !important; color: #000000 !important; } .gradio-container .tabs button.selected, .gradio-container .tab-nav button.selected, .gradio-container button[role="tab"][aria-selected="true"] { background: #FFFFFF !important; color: #000000 !important; -webkit-text-fill-color: #000000 !important; border-color: #D0D0D0 !important; border-bottom: 2px solid #FFFFFF !important; box-shadow: inset 0 -3px 0 #111111 !important; } .tabitem { background: var(--surface) !important; border: 1px solid var(--outline) !important; border-radius: var(--radius-lg) !important; padding: 28px !important; margin-top: 4px !important; box-shadow: var(--shadow-card) !important; } /* ── Panels (PO intake split view) ── */ .panel-input { background: rgba(255, 255, 255, 0.7) !important; border: 1px solid var(--outline) !important; border-radius: var(--radius) !important; padding: 20px !important; } .panel-output { background: var(--surface-low) !important; border: 1px solid var(--outline) !important; border-radius: var(--radius) !important; padding: 20px !important; } /* ── Buttons (Gradio 6 variant classes + inner spans) ── */ .gradio-container button.primary, .gradio-container button.secondary, .gradio-container .gr-button-primary, .gradio-container .gr-button-secondary, .gradio-container button[class*="primary"], .gradio-container button[class*="secondary"] { border-radius: var(--radius-pill) !important; font-family: 'Inter', sans-serif !important; font-weight: 600 !important; font-size: 0.88rem !important; padding: 11px 22px !important; transition: transform 0.12s, box-shadow 0.12s, background 0.12s !important; } .gradio-container button.primary, .gradio-container .gr-button-primary, .gradio-container button[class*="primary"]:not([class*="secondary"]) { background: #2D2D2D !important; background-color: #2D2D2D !important; color: #FFFFFF !important; -webkit-text-fill-color: #FFFFFF !important; border: none !important; box-shadow: 0 4px 14px rgba(0, 0, 0, 0.12) !important; font-weight: 700 !important; } .gradio-container button.primary *, .gradio-container .gr-button-primary *, .gradio-container button[class*="primary"]:not([class*="secondary"]) * { color: #FFFFFF !important; -webkit-text-fill-color: #FFFFFF !important; } .gradio-container button.primary:hover, .gradio-container .gr-button-primary:hover { transform: translateY(-1px) !important; background: #2D2D2D !important; background-color: #2D2D2D !important; color: #FFFFFF !important; } .gradio-container button.secondary, .gradio-container .gr-button-secondary, .gradio-container button[class*="secondary"] { background: #FFFFFF !important; background-color: #FFFFFF !important; color: #111111 !important; -webkit-text-fill-color: #111111 !important; border: 1px solid #CCCCCC !important; } .gradio-container button.secondary *, .gradio-container .gr-button-secondary *, .gradio-container button[class*="secondary"] * { color: #111111 !important; -webkit-text-fill-color: #111111 !important; } .gradio-container button.secondary:hover, .gradio-container .gr-button-secondary:hover { background: #F5F5F5 !important; background-color: #F5F5F5 !important; color: #000000 !important; } /* ── Inputs ── */ textarea, input[type="text"], input[type="number"], select { font-family: 'Inter', sans-serif !important; border: 1px solid var(--outline) !important; border-radius: var(--radius) !important; font-size: 0.92rem !important; background: var(--surface) !important; } textarea:focus, input:focus, select:focus { border-color: var(--primary) !important; box-shadow: 0 0 0 3px rgba(255, 241, 181, 0.6) !important; outline: none !important; } /* ── Dataframes / tables (standard light spreadsheet) ── */ .gradio-container .table-wrap, .gradio-container .dataframe-wrap, .gradio-container [class*="dataframe"] { border: 1px solid #CCCCCC !important; border-radius: 8px !important; overflow: hidden !important; background: #FFFFFF !important; } .gradio-container table, .gradio-container table thead, .gradio-container table tbody, .gradio-container table tr, .gradio-container table th, .gradio-container table td, .gradio-container .table-wrap table *, .gradio-container [class*="dataframe"] table * { color: #111111 !important; -webkit-text-fill-color: #111111 !important; font-size: 0.9rem !important; } .gradio-container table thead th, .gradio-container table th { font-family: 'Inter', sans-serif !important; font-weight: 700 !important; background: #E8E8E8 !important; background-color: #E8E8E8 !important; color: #111111 !important; font-size: 0.78rem !important; text-transform: uppercase !important; letter-spacing: 0.04em !important; border-bottom: 1px solid #CCCCCC !important; } .gradio-container table tbody td, .gradio-container table td { background: #FFFFFF !important; background-color: #FFFFFF !important; color: #111111 !important; border-bottom: 1px solid #EEEEEE !important; } .gradio-container table tbody tr:nth-child(even) td { background: #F9F9F9 !important; background-color: #F9F9F9 !important; } .gradio-container table tbody tr:hover td { background: #F0F4FF !important; background-color: #F0F4FF !important; } /* ── Gradio 6 Dataframe (div cells + CSS variables, incl. dark-mode class) ── */ .gradio-container, .gradio-container .gradio-dataframe, .gradio-container .gradio-dataframe-standalone, .gradio-container [class*="dataframe"] { --gr-df-table-bg-even: #F5F4F1 !important; --gr-df-table-bg-odd: #FFFFFF !important; --gr-df-table-text: #111111 !important; --gr-df-table-border: #E0DDD6 !important; --gr-df-accent: #4A6FA5 !important; --gr-df-accent-soft: #F0F4FF !important; --table-even-background-fill: #F5F4F1 !important; --table-odd-background-fill: #FFFFFF !important; --table-text-color: #111111 !important; --table-row-focus: #F0F4FF !important; --table-border-color: #E0DDD6 !important; --df-table-even-background-fill: #F5F4F1 !important; --df-table-odd-background-fill: #FFFFFF !important; --df-body-text-color: #111111 !important; --df-background-fill-primary: #FFFFFF !important; --df-background-fill-secondary: #F5F4F1 !important; --df-block-background-fill: #FFFFFF !important; --background-fill-primary: #FFFFFF !important; --background-fill-secondary: #F5F4F1 !important; --body-text-color: #111111 !important; } html.dark .gradio-container, .dark .gradio-container, .gradio-container.dark { --gr-df-table-bg-even: #F5F4F1 !important; --gr-df-table-bg-odd: #FFFFFF !important; --gr-df-table-text: #111111 !important; --table-even-background-fill: #F5F4F1 !important; --table-odd-background-fill: #FFFFFF !important; --table-text-color: #111111 !important; --df-table-even-background-fill: #F5F4F1 !important; --df-table-odd-background-fill: #FFFFFF !important; --df-body-text-color: #111111 !important; --df-background-fill-primary: #FFFFFF !important; --df-background-fill-secondary: #F5F4F1 !important; --background-fill-primary: #FFFFFF !important; --background-fill-secondary: #F5F4F1 !important; --body-text-color: #111111 !important; } .gradio-container .cell-wrap, .gradio-container .table-wrap .cell-wrap, .gradio-container [class*="dataframe"] .cell-wrap { color: #111111 !important; -webkit-text-fill-color: #111111 !important; background-color: #FFFFFF !important; } .gradio-container .cell-wrap input, .gradio-container .cell-wrap textarea { color: #111111 !important; -webkit-text-fill-color: #111111 !important; background: #FFFFFF !important; } .gradio-container .header-row .cell-wrap, .gradio-container thead .cell-wrap, .gradio-container [class*="dataframe"] .header-row .cell-wrap { background-color: #E8E8E8 !important; color: #111111 !important; font-weight: 700 !important; } .gradio-container tr:nth-child(even) .cell-wrap, .gradio-container .row-even .cell-wrap { background-color: #F5F4F1 !important; } .gradio-container tr:hover .cell-wrap, .gradio-container .row-selected .cell-wrap, .gradio-container .selected .cell-wrap { background-color: #F0F4FF !important; color: #111111 !important; } /* ── Tab hints ── */ .tab-hint { background: #FFFFFF; border: 1px solid var(--outline); border-left: 4px solid var(--honey); border-radius: 0 var(--radius) var(--radius) 0; padding: 14px 18px; margin-bottom: 20px; font-size: 0.88rem; color: #3D3D3D !important; } .tab-hint strong { color: #1A1A1A !important; } /* ── Block labels, field labels, radio/checkbox pills ── */ .gradio-container .block-label, .gradio-container .block-label span, .gradio-container .block-title, .gradio-container .label-wrap span:not(.icon) { background: #F5F4F1 !important; background-color: #F5F4F1 !important; color: #1A1A1A !important; -webkit-text-fill-color: #1A1A1A !important; border-color: #E0DDD6 !important; } .gradio-container fieldset label, .gradio-container .wrap > label, .gradio-container .form label { background: #FFFFFF !important; background-color: #FFFFFF !important; color: #1A1A1A !important; -webkit-text-fill-color: #1A1A1A !important; border-color: #E0DDD6 !important; box-shadow: none !important; } .gradio-container fieldset label:hover, .gradio-container .wrap > label:hover { background: #F5F4F1 !important; background-color: #F5F4F1 !important; color: #1A1A1A !important; } .gradio-container fieldset label.selected, .gradio-container .wrap > label.selected { background: #FFF4D6 !important; background-color: #FFF4D6 !important; color: #1A1A1A !important; border-color: var(--honey) !important; } /* ── Accordion ── */ .gradio-container .label-wrap { font-family: 'Montserrat', sans-serif !important; font-weight: 600 !important; font-size: 0.85rem !important; color: var(--on-surface-variant) !important; text-transform: uppercase !important; letter-spacing: 0.04em !important; background: transparent !important; } /* ── Piper mascot ── */ .piper-container { position: fixed !important; bottom: 24px !important; right: 24px !important; z-index: 99999 !important; animation: piperFloat 4s ease-in-out infinite; } @keyframes piperFloat { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-5px); } } .piper-body { width: 60px; height: 68px; background: var(--primary-container); border-radius: 12px 12px 12px 20px; position: relative; box-shadow: var(--shadow-soft); border: 2px solid var(--primary); } .piper-face { position: absolute; top: 18px; left: 50%; transform: translateX(-50%); width: 44px; text-align: center; } .piper-eyes { display: flex; justify-content: center; gap: 9px; margin-bottom: 3px; } .piper-eye { width: 7px; height: 8px; background: var(--on-surface); border-radius: 50%; animation: piperBlink 4s ease-in-out infinite; } @keyframes piperBlink { 0%, 42%, 48%, 100% { transform: scaleY(1); } 45% { transform: scaleY(0.1); } } .piper-mouth { width: 11px; height: 5px; border-bottom: 2px solid var(--on-surface); border-radius: 0 0 8px 8px; margin: 2px auto 0; } .piper-cheek { position: absolute; top: 28px; width: 8px; height: 5px; background: #fadadd; border-radius: 50%; opacity: 0.7; } .piper-cheek.left { left: 6px; } .piper-cheek.right { right: 6px; } .piper-nametag { text-align: center; font-family: 'Inter', sans-serif; font-size: 0.65rem; font-weight: 800; color: #111111 !important; margin-top: 5px; letter-spacing: 0.08em; } .piper-bubble { position: absolute; bottom: 78px; right: 0; background: var(--surface); border: 1px solid var(--outline); border-radius: 14px 14px 4px 14px; padding: 8px 13px; font-size: 0.74rem; font-weight: 600; color: var(--on-surface-variant); white-space: nowrap; box-shadow: var(--shadow-card); opacity: 0; transform: translateY(4px); transition: all 0.25s ease; } .piper-container:hover .piper-bubble { opacity: 1; transform: translateY(0); } .paperain-footer { text-align: center; padding: 28px 0 12px; font-size: 0.78rem; font-weight: 500; color: var(--on-surface-variant); letter-spacing: 0.02em; } /* ── Keep light surfaces if Gradio applies .dark (system theme) ── */ html.dark, html.dark body, html.dark .gradio-container, :root.dark, :root.dark .gradio-container { color-scheme: light !important; background-color: #FAFAF8 !important; --body-background-fill: #FAFAF8 !important; --block-background-fill: #FFFFFF !important; --background-fill-primary: #FFFFFF !important; --background-fill-secondary: #F5F4F1 !important; --body-text-color: #1A1A1A !important; --gr-df-table-bg-even: #F5F4F1 !important; --gr-df-table-bg-odd: #FFFFFF !important; --gr-df-table-text: #1A1A1A !important; --block-label-background-fill: #F5F4F1 !important; --block-label-text-color: #1A1A1A !important; --checkbox-label-background-fill: #FFFFFF !important; --checkbox-label-text-color: #1A1A1A !important; } .app-shell { background: transparent !important; } """ # One-time head script: prefer light theme, no MutationObserver (avoids layout thrash). APP_HEAD = """ """ THEME = gr.themes.Soft( primary_hue=gr.themes.colors.neutral, secondary_hue=gr.themes.colors.neutral, font=gr.themes.GoogleFont("Inter"), ).set( body_background_fill="#FAFAF8", body_text_color="#1A1A1A", body_text_color_subdued="#444444", block_background_fill="#FFFFFF", block_border_width="0px", block_shadow="none", block_label_background_fill="#F5F4F1", block_label_text_color="#1A1A1A", block_title_background_fill="#F5F4F1", block_title_text_color="#1A1A1A", input_background_fill="#FFFFFF", input_border_color="#E0DDD6", button_primary_background_fill="#2D2D2D", button_primary_text_color="#FFFFFF", button_secondary_background_fill="#FFFFFF", button_secondary_text_color="#1A1A1A", checkbox_label_background_fill="#FFFFFF", checkbox_label_text_color="#1A1A1A", checkbox_label_border_color="#E0DDD6", checkbox_label_background_fill_selected="#FFF4D6", checkbox_label_text_color_selected="#1A1A1A", checkbox_label_border_color_selected="#C9A227", table_even_background_fill="#F5F4F1", table_odd_background_fill="#FFFFFF", table_border_color="#E0DDD6", table_row_focus="#F0F4FF", ) NAV_HTML = """ """ HERO_HTML = """
Turn messy purchase orders from 25 partner stores into organised, print-ready sticker restocks — powered by Piper AI.