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