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 = """

Paperain Studio

Turn messy purchase orders from 25 partner stores into organised, print-ready sticker restocks — powered by Piper AI.

Piper AI 3-Stage Parser Code-Mixed ID/EN Qwen 2.5-7B · Yogyakarta
25Partner Stores
150Sticker Designs
7BParameters
3-4hours saved / Week
1 · Paste messy PO 2 · Aggregate demand 3 · Calculate print 4 · Delivery docs
""" PIPER_HTML = """
Hi! Paste a PO and I'll parse it ✨
PIPER
""" FOOTER_HTML = ( '
" ) TABS_OPEN = '
' 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) # ── Tab 1: PO Intake ── with gr.Tab("PO Intake"): gr.HTML( '
Step 1: Paste a store PO below — ' "any format works. Click a sample to try, then hit Parse PO with Piper.
" ) 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", ) # ── Tab 2: Stock & Demand ── with gr.Tab("Stock & Demand"): gr.HTML( '
Step 2: After parsing POs, click ' "Refresh Demand to see total demand vs your home stock.
" ) 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", ) # ── Tab 3: Print Calculator ── with gr.Tab("Print Calculator"): gr.HTML( '
Step 3: Each A3 sheet fits 8 A5 stickers. ' "Only products with shortages appear here.
" ) 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") # ── Tab 4: Delivery Docs ── with gr.Tab("Delivery Docs"): gr.HTML( '
Step 4: Generate a packing list for a specific store. ' "Parse their PO first in Tab 1.
" ) 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) # ── Tab 5: Best Sellers ── with gr.Tab("Best Sellers"): gr.HTML( '
Piper analyses demand across all parsed POs and recommends ' "what to stock — share with stores that ask.
" ) 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('
') # close app-shell gr.HTML(FOOTER_HTML) gr.HTML(PIPER_HTML) # ── Events ── 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