indirapravianti's picture
Deploy Piper sticker restock manager Gradio app
1a65785
Raw
History Blame Contribute Delete
37.9 kB
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 = """
<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)
# ── Tab 1: PO Intake ──
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",
)
# ── Tab 2: Stock & Demand ──
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",
)
# ── Tab 3: Print Calculator ──
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")
# ── Tab 4: Delivery Docs ──
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)
# ── Tab 5: Best Sellers ──
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>') # 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