| 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, |
| ) |
|
|
| |
| |
| |
| 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; |
| } |
| """ |
|
|
| |
| APP_HEAD = """ |
| <meta name="color-scheme" content="light"> |
| <script> |
| (function () { |
| var root = document.documentElement; |
| root.classList.remove('dark'); |
| root.style.colorScheme = 'light'; |
| })(); |
| </script> |
| """ |
|
|
| 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 = """ |
| <nav class="top-nav"> |
| <div class="top-nav-inner"> |
| <div class="top-nav-brand"> |
| <span class="icon">✨</span> |
| <span>Piper · Paperain Studio</span> |
| </div> |
| <div class="top-nav-meta">Build Small Hackathon 2026 · Qwen 2.5-7B</div> |
| </div> |
| </nav> |
| """ |
|
|
| HERO_HTML = """ |
| <div class="app-shell"> |
| <div class="hero-section"> |
| <div class="hero-grid"> |
| <div class="hero-copy"> |
| <h1>Paperain Studio</h1> |
| <p class="hero-tagline"> |
| Turn messy purchase orders from 25 partner stores into organised, print-ready sticker restocks — powered by Piper AI. |
| </p> |
| <div class="hero-meta"> |
| <span class="hero-pill">Piper AI</span> |
| <span class="hero-pill blue">3-Stage Parser</span> |
| <span class="hero-pill">Code-Mixed ID/EN</span> |
| <span class="hero-pill muted">Qwen 2.5-7B · Yogyakarta</span> |
| </div> |
| </div> |
| <div class="stats-row"> |
| <div class="stat"><span class="number">25</span><span class="label">Partner Stores</span></div> |
| <div class="stat"><span class="number">150</span><span class="label">Sticker Designs</span></div> |
| <div class="stat"><span class="number">7B</span><span class="label">Parameters</span></div> |
| <div class="stat stat-hours"><span class="number">3-4</span><span class="label">hours saved / Week</span></div> |
| </div> |
| </div> |
| <div class="step-flow"> |
| <span class="step active">1 · Paste messy PO</span> |
| <span class="arrow">→</span> |
| <span class="step">2 · Aggregate demand</span> |
| <span class="arrow">→</span> |
| <span class="step">3 · Calculate print</span> |
| <span class="arrow">→</span> |
| <span class="step">4 · Delivery docs</span> |
| </div> |
| </div> |
| </div> |
| """ |
|
|
| PIPER_HTML = """ |
| <div class="piper-container" title="Piper — your sticker desk assistant"> |
| <div class="piper-bubble">Hi! Paste a PO and I'll parse it ✨</div> |
| <div class="piper-body"> |
| <div class="piper-face"> |
| <div class="piper-eyes"> |
| <div class="piper-eye"></div> |
| <div class="piper-eye"></div> |
| </div> |
| <div class="piper-mouth"></div> |
| </div> |
| <div class="piper-cheek left"></div> |
| <div class="piper-cheek right"></div> |
| </div> |
| <div class="piper-nametag">PIPER</div> |
| </div> |
| """ |
|
|
| FOOTER_HTML = ( |
| '<div class="app-shell"><div class="paperain-footer">' |
| "Paperain Studio · Build Small Hackathon 2026 · Qwen 2.5-7B · Yogyakarta, Indonesia" |
| "</div></div>" |
| ) |
|
|
| TABS_OPEN = '<div class="app-shell">' |
|
|
|
|
| def build_ui() -> gr.Blocks: |
| with gr.Blocks( |
| title="Paperain Studio — Sticker Restock Manager", |
| fill_width=True, |
| theme=THEME, |
| css=CUSTOM_CSS, |
| head=APP_HEAD, |
| ) as demo: |
|
|
| all_pos_state = gr.State(value={}) |
| demand_state = gr.State(value=None) |
|
|
| gr.HTML(NAV_HTML) |
| gr.HTML(HERO_HTML) |
| gr.HTML(TABS_OPEN) |
|
|
| |
| with gr.Tab("PO Intake"): |
| gr.HTML( |
| '<div class="tab-hint"><strong>Step 1:</strong> Paste a store PO below — ' |
| "any format works. Click a sample to try, then hit <strong>Parse PO with Piper</strong>.</div>" |
| ) |
| with gr.Row(equal_height=True): |
| with gr.Column(scale=2, elem_classes=["panel-input"]): |
| po_store = gr.Dropdown( |
| choices=STORE_NAMES, |
| value=STORE_NAMES[0], |
| label="Store", |
| allow_custom_value=True, |
| ) |
| po_text = gr.Textbox( |
| label="Paste PO Text", |
| lines=12, |
| placeholder=( |
| "e.g.\nPesanan bulan Juni:\n" |
| "- Stiker kucing hologram 20 pcs\n" |
| "- Stiker sakura 15 pcs\n" |
| "- Rainbow unicorn 10\n" |
| "Tolong kirim sebelum tanggal 5" |
| ), |
| ) |
| with gr.Row(): |
| parse_btn = gr.Button("Parse PO with Piper", variant="primary", scale=2) |
| save_btn = gr.Button("Save Edits", variant="secondary", scale=1) |
| po_status = gr.Textbox(label="Status", interactive=False, max_lines=3) |
| with gr.Column(scale=3, elem_classes=["panel-output"]): |
| po_table = gr.Dataframe( |
| headers=["Product", "Qty", "Notes"], |
| datatype=["str", "number", "str"], |
| interactive=True, |
| label="Piper's Interpretation (editable)", |
| ) |
|
|
| with gr.Accordion("All Parsed POs", open=False): |
| po_log_md = gr.Markdown("*No POs parsed yet.*") |
| export_all_btn = gr.Button("Export All POs as CSV", variant="secondary") |
| export_all_file = gr.File(label="Download CSV") |
|
|
| gr.Markdown("#### Try These Sample POs") |
| gr.Examples( |
| examples=SAMPLE_PO_EXAMPLES, |
| inputs=[po_store, po_text], |
| label="Click an example, then Parse", |
| ) |
|
|
| |
| with gr.Tab("Stock & Demand"): |
| gr.HTML( |
| '<div class="tab-hint"><strong>Step 2:</strong> After parsing POs, click ' |
| "<strong>Refresh Demand</strong> to see total demand vs your home stock.</div>" |
| ) |
| with gr.Row(equal_height=True): |
| with gr.Column(scale=1, elem_classes=["panel-input"]): |
| gr.Markdown("#### Home Stock") |
| stock_table = gr.Dataframe( |
| value=make_default_stock(), |
| headers=["Product", "In Stock"], |
| datatype=["str", "number"], |
| interactive=True, |
| label="Your inventory", |
| ) |
| with gr.Column(scale=1, elem_classes=["panel-output"]): |
| gr.Markdown("#### Demand vs Stock") |
| refresh_demand_btn = gr.Button("Refresh Demand", variant="primary") |
| workflow_btn = gr.Button("Run Full Workflow (Demand + Print)", variant="secondary") |
| demand_summary = gr.Textbox(label="Summary", interactive=False, lines=2) |
| demand_table = gr.Dataframe( |
| headers=["Product", "Total Demand", "In Stock", "Shortage"], |
| datatype=["str", "number", "number", "number"], |
| interactive=False, |
| label="Aggregated demand", |
| ) |
|
|
| |
| with gr.Tab("Print Calculator"): |
| gr.HTML( |
| '<div class="tab-hint"><strong>Step 3:</strong> Each A3 sheet fits 8 A5 stickers. ' |
| "Only products with shortages appear here.</div>" |
| ) |
| with gr.Column(elem_classes=["panel-output"]): |
| calc_btn = gr.Button("Calculate Print Order", variant="primary") |
| print_summary = gr.Textbox(label="Summary", interactive=False, lines=2) |
| print_table = gr.Dataframe( |
| headers=["Product", "Qty to Print", "A3 Sheets (8 per sheet)"], |
| datatype=["str", "number", "number"], |
| interactive=False, |
| label="Print order", |
| ) |
| print_csv = gr.File(label="Download Print Order CSV") |
|
|
| |
| with gr.Tab("Delivery Docs"): |
| gr.HTML( |
| '<div class="tab-hint"><strong>Step 4:</strong> Generate a packing list for a specific store. ' |
| "Parse their PO first in Tab 1.</div>" |
| ) |
| with gr.Column(elem_classes=["panel-input"]): |
| with gr.Row(): |
| del_store = gr.Dropdown( |
| choices=STORE_NAMES, |
| value=STORE_NAMES[0], |
| label="Store", |
| allow_custom_value=True, |
| ) |
| del_lang = gr.Radio(["English", "Indonesian"], value="English", label="Language") |
| del_btn = gr.Button("Generate Delivery Document", variant="primary") |
| del_output = gr.Textbox(label="Delivery Document", lines=16, interactive=False) |
|
|
| |
| with gr.Tab("Best Sellers"): |
| gr.HTML( |
| '<div class="tab-hint">Piper analyses demand across all parsed POs and recommends ' |
| "what to stock — share with stores that ask.</div>" |
| ) |
| with gr.Column(elem_classes=["panel-output"]): |
| with gr.Row(): |
| bs_lang = gr.Radio(["English", "Indonesian"], value="English", label="Language") |
| bs_btn = gr.Button("Generate Report", variant="primary") |
| bs_output = gr.Textbox(label="Best Seller Report", lines=16, interactive=False) |
|
|
| gr.HTML('</div>') |
| gr.HTML(FOOTER_HTML) |
| gr.HTML(PIPER_HTML) |
|
|
| |
| def parse_and_refresh(store, text, all_pos, stock): |
| po_table_out, status, all_pos_out = parse_po(store, text, all_pos) |
| demand_df, demand_sum = build_demand_table(all_pos_out, stock) |
| log_md = format_po_log(all_pos_out) |
| return po_table_out, status, all_pos_out, demand_df, demand_sum, log_md, demand_df |
|
|
| parse_btn.click( |
| fn=parse_and_refresh, |
| inputs=[po_store, po_text, all_pos_state, stock_table], |
| outputs=[po_table, po_status, all_pos_state, demand_table, demand_summary, po_log_md, demand_state], |
| ) |
|
|
| save_btn.click( |
| fn=save_po, |
| inputs=[po_store, po_table, all_pos_state], |
| outputs=[all_pos_state, po_status], |
| ).then( |
| fn=format_po_log, |
| inputs=[all_pos_state], |
| outputs=[po_log_md], |
| ).then( |
| fn=build_demand_table, |
| inputs=[all_pos_state, stock_table], |
| outputs=[demand_table, demand_summary], |
| ).then( |
| fn=lambda d, s: d, |
| inputs=[demand_table, demand_summary], |
| outputs=[demand_state], |
| ) |
|
|
| export_all_btn.click( |
| fn=export_all_pos_csv, |
| inputs=[all_pos_state], |
| outputs=[export_all_file], |
| ) |
|
|
| refresh_demand_btn.click( |
| fn=build_demand_table, |
| inputs=[all_pos_state, stock_table], |
| outputs=[demand_table, demand_summary], |
| ).then( |
| fn=lambda d, s: d, |
| inputs=[demand_table, demand_summary], |
| outputs=[demand_state], |
| ) |
|
|
| workflow_btn.click( |
| fn=run_full_workflow, |
| inputs=[all_pos_state, stock_table], |
| outputs=[demand_table, demand_summary, print_table, print_summary, print_csv], |
| ).then( |
| fn=lambda d, *_: d, |
| inputs=[demand_table, demand_summary, print_table, print_summary, print_csv], |
| outputs=[demand_state], |
| ) |
|
|
| calc_btn.click( |
| fn=calculate_printing, |
| inputs=[demand_table], |
| outputs=[print_table, print_summary, print_csv], |
| ) |
|
|
| del_btn.click( |
| fn=generate_delivery_doc, |
| inputs=[del_store, all_pos_state, del_lang], |
| outputs=[del_output], |
| ) |
|
|
| bs_btn.click( |
| fn=generate_bestseller_report, |
| inputs=[all_pos_state, bs_lang], |
| outputs=[bs_output], |
| ) |
|
|
| return demo |
|
|