From 657e6cc7c1015a59c36936aef2dc2234de005a6e Mon Sep 17 00:00:00 2001 From: Kevin Reznicek Date: Wed, 17 Jun 2026 09:37:44 -0700 Subject: [PATCH 1/2] Replace Streamlit dashboard with B1 HTMX front-end MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rebuild the operator UI as server-rendered Jinja2 + HTMX served by the existing FastAPI app, implementing the B1 "Concrete · Amber Dark" design (light default + working dark/light toggle). Removes Streamlit entirely. - web/: shell (sidebar + topbar), b1.css design system (both palettes as data-theme tokens), vendored htmx, app.js (toggle, dropdowns, master/detail, removable tags, aspect->size). - Four designed screens wired to real data: Trend Inbox, Research Reports (markdown TOC + trend-derived score), Etsy Posting (pipeline + listing editor + readiness), Design Studio (composer + lineage library + focus). - Group B screens restyled in B1: Product Drafts, Printful Workshop, Analytics, Agent Runs - Logs. Approval Queue dropped; Etsy Listings folded into the 8-item nav. - Cutover: delete dashboard/, drop streamlit dep, remove the dashboard compose service, update Makefile/Dockerfile (UI now served by api at :8000). - 16 DB-backed web smoke tests; ruff + mypy --strict clean. Co-Authored-By: Claude Opus 4.8 --- Dockerfile | 2 +- Makefile | 9 +- .../B1 Workshop.html | 38 + assets/design_handoff_b1_workshop/README.md | 105 +++ .../design_handoff_b1_workshop/b1-mount.jsx | 49 ++ .../docs/01-design-tokens.md | 165 +++++ .../docs/02-components.md | 131 ++++ .../docs/03-screens.md | 143 ++++ .../docs/04-interactions-and-state.md | 88 +++ .../docs/05-data-model.md | 127 ++++ .../docs/06-implementation-guide.md | 106 +++ .../screen-etsy.jsx | 203 ++++++ .../screen-inbox.jsx | 176 +++++ .../screen-reports.jsx | 167 +++++ .../screenshots/design-studio-dark.png | Bin 0 -> 44419 bytes .../screenshots/design-studio-light.png | Bin 0 -> 44419 bytes .../screenshots/etsy-posting-dark.png | Bin 0 -> 43782 bytes .../screenshots/etsy-posting-light.png | Bin 0 -> 43782 bytes .../screenshots/research-reports-dark.png | Bin 0 -> 46603 bytes .../screenshots/research-reports-light.png | Bin 0 -> 46603 bytes .../screenshots/trend-inbox-dark.png | Bin 0 -> 47605 bytes .../screenshots/trend-inbox-light.png | Bin 0 -> 47605 bytes assets/design_handoff_b1_workshop/studio.jsx | 611 +++++++++++++++++ .../design_handoff_b1_workshop/theme.b1.jsx | 154 +++++ .../design_handoff_b1_workshop/wschrome.jsx | 291 ++++++++ docker-compose.yml | 16 +- pyproject.toml | 7 +- src/ecommerce_agent/api/main.py | 9 + src/ecommerce_agent/dashboard/__init__.py | 1 - src/ecommerce_agent/dashboard/app.py | 32 - src/ecommerce_agent/dashboard/client.py | 87 --- .../dashboard/pages/1_Trend_Inbox.py | 98 --- .../dashboard/pages/2_Research_Reports.py | 189 ----- .../dashboard/pages/4_Design_Studio.py | 647 ------------------ .../dashboard/pages/6_Etsy_Listings.py | 330 --------- .../pages/6_Etsy_Posting_Workflow.py | 572 ---------------- .../dashboard/pages/6_Printful_Workshop.py | 456 ------------ .../dashboard/pages/8_Agent_Runs_Logs.py | 15 - src/ecommerce_agent/dashboard/ui.py | 48 -- src/ecommerce_agent/web/__init__.py | 11 + .../{dashboard => web}/design_lineage.py | 0 src/ecommerce_agent/web/design_studio.py | 166 +++++ src/ecommerce_agent/web/etsy_posting.py | 253 +++++++ src/ecommerce_agent/web/group_b.py | 158 +++++ src/ecommerce_agent/web/research_reports.py | 229 +++++++ src/ecommerce_agent/web/routes.py | 150 ++++ src/ecommerce_agent/web/static/app.js | 152 ++++ src/ecommerce_agent/web/static/b1.css | 569 +++++++++++++++ src/ecommerce_agent/web/static/htmx.min.js | 1 + .../web/templates/agent_runs.html | 6 + .../web/templates/analytics.html | 28 + src/ecommerce_agent/web/templates/base.html | 34 + .../web/templates/design_studio.html | 113 +++ .../web/templates/etsy_posting.html | 43 ++ .../templates/partials/_listing_editor.html | 102 +++ .../web/templates/partials/_macros.html | 74 ++ .../web/templates/partials/_report_list.html | 38 + .../templates/partials/_report_reader.html | 56 ++ .../web/templates/partials/_runs_table.html | 18 + .../web/templates/partials/_sidebar.html | 34 + .../web/templates/partials/_signals_meta.html | 3 + .../web/templates/partials/_studio_focus.html | 45 ++ .../web/templates/partials/_topbar.html | 18 + .../web/templates/partials/_trend_detail.html | 79 +++ .../web/templates/partials/_trend_list.html | 38 + .../web/templates/printful_workshop.html | 28 + .../web/templates/product_drafts.html | 22 + .../web/templates/research_reports.html | 9 + .../web/templates/trend_inbox.html | 9 + src/ecommerce_agent/web/trend_inbox.py | 151 ++++ tests/smoke/test_api_and_dashboard.py | 10 +- tests/unit/test_dashboard_client.py | 28 - tests/unit/test_design_lineage.py | 2 +- tests/web/conftest.py | 43 ++ tests/web/test_design_studio.py | 70 ++ tests/web/test_etsy_posting.py | 75 ++ tests/web/test_group_b.py | 78 +++ tests/web/test_research_reports.py | 67 ++ tests/web/test_trend_inbox.py | 85 +++ 79 files changed, 5628 insertions(+), 2539 deletions(-) create mode 100644 assets/design_handoff_b1_workshop/B1 Workshop.html create mode 100644 assets/design_handoff_b1_workshop/README.md create mode 100644 assets/design_handoff_b1_workshop/b1-mount.jsx create mode 100644 assets/design_handoff_b1_workshop/docs/01-design-tokens.md create mode 100644 assets/design_handoff_b1_workshop/docs/02-components.md create mode 100644 assets/design_handoff_b1_workshop/docs/03-screens.md create mode 100644 assets/design_handoff_b1_workshop/docs/04-interactions-and-state.md create mode 100644 assets/design_handoff_b1_workshop/docs/05-data-model.md create mode 100644 assets/design_handoff_b1_workshop/docs/06-implementation-guide.md create mode 100644 assets/design_handoff_b1_workshop/screen-etsy.jsx create mode 100644 assets/design_handoff_b1_workshop/screen-inbox.jsx create mode 100644 assets/design_handoff_b1_workshop/screen-reports.jsx create mode 100644 assets/design_handoff_b1_workshop/screenshots/design-studio-dark.png create mode 100644 assets/design_handoff_b1_workshop/screenshots/design-studio-light.png create mode 100644 assets/design_handoff_b1_workshop/screenshots/etsy-posting-dark.png create mode 100644 assets/design_handoff_b1_workshop/screenshots/etsy-posting-light.png create mode 100644 assets/design_handoff_b1_workshop/screenshots/research-reports-dark.png create mode 100644 assets/design_handoff_b1_workshop/screenshots/research-reports-light.png create mode 100644 assets/design_handoff_b1_workshop/screenshots/trend-inbox-dark.png create mode 100644 assets/design_handoff_b1_workshop/screenshots/trend-inbox-light.png create mode 100644 assets/design_handoff_b1_workshop/studio.jsx create mode 100644 assets/design_handoff_b1_workshop/theme.b1.jsx create mode 100644 assets/design_handoff_b1_workshop/wschrome.jsx delete mode 100644 src/ecommerce_agent/dashboard/__init__.py delete mode 100644 src/ecommerce_agent/dashboard/app.py delete mode 100644 src/ecommerce_agent/dashboard/client.py delete mode 100644 src/ecommerce_agent/dashboard/pages/1_Trend_Inbox.py delete mode 100644 src/ecommerce_agent/dashboard/pages/2_Research_Reports.py delete mode 100644 src/ecommerce_agent/dashboard/pages/4_Design_Studio.py delete mode 100644 src/ecommerce_agent/dashboard/pages/6_Etsy_Listings.py delete mode 100644 src/ecommerce_agent/dashboard/pages/6_Etsy_Posting_Workflow.py delete mode 100644 src/ecommerce_agent/dashboard/pages/6_Printful_Workshop.py delete mode 100644 src/ecommerce_agent/dashboard/pages/8_Agent_Runs_Logs.py delete mode 100644 src/ecommerce_agent/dashboard/ui.py create mode 100644 src/ecommerce_agent/web/__init__.py rename src/ecommerce_agent/{dashboard => web}/design_lineage.py (100%) create mode 100644 src/ecommerce_agent/web/design_studio.py create mode 100644 src/ecommerce_agent/web/etsy_posting.py create mode 100644 src/ecommerce_agent/web/group_b.py create mode 100644 src/ecommerce_agent/web/research_reports.py create mode 100644 src/ecommerce_agent/web/routes.py create mode 100644 src/ecommerce_agent/web/static/app.js create mode 100644 src/ecommerce_agent/web/static/b1.css create mode 100644 src/ecommerce_agent/web/static/htmx.min.js create mode 100644 src/ecommerce_agent/web/templates/agent_runs.html create mode 100644 src/ecommerce_agent/web/templates/analytics.html create mode 100644 src/ecommerce_agent/web/templates/base.html create mode 100644 src/ecommerce_agent/web/templates/design_studio.html create mode 100644 src/ecommerce_agent/web/templates/etsy_posting.html create mode 100644 src/ecommerce_agent/web/templates/partials/_listing_editor.html create mode 100644 src/ecommerce_agent/web/templates/partials/_macros.html create mode 100644 src/ecommerce_agent/web/templates/partials/_report_list.html create mode 100644 src/ecommerce_agent/web/templates/partials/_report_reader.html create mode 100644 src/ecommerce_agent/web/templates/partials/_runs_table.html create mode 100644 src/ecommerce_agent/web/templates/partials/_sidebar.html create mode 100644 src/ecommerce_agent/web/templates/partials/_signals_meta.html create mode 100644 src/ecommerce_agent/web/templates/partials/_studio_focus.html create mode 100644 src/ecommerce_agent/web/templates/partials/_topbar.html create mode 100644 src/ecommerce_agent/web/templates/partials/_trend_detail.html create mode 100644 src/ecommerce_agent/web/templates/partials/_trend_list.html create mode 100644 src/ecommerce_agent/web/templates/printful_workshop.html create mode 100644 src/ecommerce_agent/web/templates/product_drafts.html create mode 100644 src/ecommerce_agent/web/templates/research_reports.html create mode 100644 src/ecommerce_agent/web/templates/trend_inbox.html create mode 100644 src/ecommerce_agent/web/trend_inbox.py delete mode 100644 tests/unit/test_dashboard_client.py create mode 100644 tests/web/conftest.py create mode 100644 tests/web/test_design_studio.py create mode 100644 tests/web/test_etsy_posting.py create mode 100644 tests/web/test_group_b.py create mode 100644 tests/web/test_research_reports.py create mode 100644 tests/web/test_trend_inbox.py diff --git a/Dockerfile b/Dockerfile index e7ee793..0ab989a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,6 +20,6 @@ COPY alembic.ini ./ COPY migrations ./migrations RUN mkdir -p /app/assets/generated -EXPOSE 8000 8501 +EXPOSE 8000 CMD ["uvicorn", "ecommerce_agent.api.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/Makefile b/Makefile index 92e0031..5e42508 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ TEST_DATABASE_URL ?= postgresql+asyncpg://ecommerce:ecommerce@postgres:5432/$(TE .DEFAULT_GOAL := help -.PHONY: help env install install-hooks lock up down restart build ps logs logs-api logs-dashboard \ +.PHONY: help env install install-hooks lock up down restart build ps logs logs-api \ migrate lint typecheck format format-check test test-unit \ test-smoke test-integration test-all shell-api db-shell health openapi \ reset-local-data @@ -59,9 +59,6 @@ logs: ## Follow logs for all services. logs-api: ## Follow FastAPI logs. $(COMPOSE) logs -f --tail=200 api -logs-dashboard: ## Follow Streamlit logs. - $(COMPOSE) logs -f --tail=200 dashboard - migrate: env ## Apply application database migrations. $(COMPOSE) run --rm migrate alembic upgrade head @@ -115,10 +112,10 @@ shell-api: ## Open a shell in a one-off API container. db-shell: ## Open psql in the local application database. $(COMPOSE) exec postgres psql -U ecommerce -d ecommerce -health: ## Check local API and dashboard HTTP endpoints. +health: ## Check the local API and web UI HTTP endpoints. curl -fsS http://127.0.0.1:8000/api/health @printf "\n" - curl -fsSI http://127.0.0.1:8501 | head -n 1 + curl -fsSI http://127.0.0.1:8000/trend-inbox | head -n 1 openapi: ## Write the current OpenAPI schema to /tmp/ecommerce-agent-openapi.json. @test -x "$(PYTHON)" || { echo "Run 'make install' first."; exit 1; } diff --git a/assets/design_handoff_b1_workshop/B1 Workshop.html b/assets/design_handoff_b1_workshop/B1 Workshop.html new file mode 100644 index 0000000..67ba1f9 --- /dev/null +++ b/assets/design_handoff_b1_workshop/B1 Workshop.html @@ -0,0 +1,38 @@ + + + + + +Workshop — B1 (Concrete · Amber Dark) · Handoff + + + + + + +
+ + + + + + + + + + + + + + + + diff --git a/assets/design_handoff_b1_workshop/README.md b/assets/design_handoff_b1_workshop/README.md new file mode 100644 index 0000000..b698cb6 --- /dev/null +++ b/assets/design_handoff_b1_workshop/README.md @@ -0,0 +1,105 @@ +# Workshop — B1 (Concrete · Amber Dark) · Claude Code Handoff + +This folder is a **complete handoff package** for implementing the Workshop B1 design in a +real codebase. It is written so a developer (or Claude Code) who was **not** part of the +original design conversation can build the product from these documents alone. + +--- + +## What Workshop is +An internal, single-operator tool for an AI-assisted print-on-demand business. It runs a +human-in-the-loop pipeline: surface market **trends** → generate **research reports** → +**design** print artwork → push **products** to Etsy/Printful. Tagline: *Human-driven commerce.* + +## What "B1" is +B1 (code-named **Concrete · Amber Dark**) is the chosen visual direction: a **brutalist**, +monospace-accented UI — hard 0-radius corners, 1.5px structural borders, solid offset +"hard shadows," a single **vermilion** signal color (`#fb5a28`) over **warm amber-orange +near-blacks**. It ships **dark by default** but is fully **light/dark compatible** (a toggle +lives in every topbar). The other directions explored earlier have been intentionally +stripped from this package. + +--- + +## How to use this package + +1. Open **`B1 Workshop.html`** in a browser to see all four screens live (stacked, dark mode; + use the LIGHT/DARK toggle in any topbar to flip the whole study). This is your visual + source of truth. +2. Read the docs below **in order**. They are the build spec. +3. The `.jsx`/`.html` files are **design references**, not production code — see *Fidelity* below. + +### Documents +| Doc | Contents | +|---|---| +| [`docs/01-design-tokens.md`](docs/01-design-tokens.md) | Both color palettes (dark + light), type scale, geometry, spacing, shadows, image placeholders. Start here. | +| [`docs/02-components.md`](docs/02-components.md) | The app shell (sidebar, topbar), every shared primitive, and reusable patterns with exact specs. | +| [`docs/03-screens.md`](docs/03-screens.md) | Per-screen layout breakdowns: Design Studio, Trend Inbox, Research Reports, Etsy Posting. | +| [`docs/04-interactions-and-state.md`](docs/04-interactions-and-state.md) | Hover/active/selection behavior, transitions, theming mechanism, and per-screen state. | +| [`docs/05-data-model.md`](docs/05-data-model.md) | The shape of all mock content (`WS_DATA`) — your provisional data contract. | +| [`docs/06-implementation-guide.md`](docs/06-implementation-guide.md) | Recommended build approach, component tree, theming strategy, what's mocked, acceptance checklist. | + +### Reference screenshots +`screenshots/` contains each screen rendered in **both** dark and light mode — use them to +sanity-check your implementation: +- `design-studio-{dark,light}.png` +- `trend-inbox-{dark,light}.png` +- `research-reports-{dark,light}.png` +- `etsy-posting-{dark,light}.png` + +(These are captured at the design width; the topbar's right edge may be cropped — see the live +`B1 Workshop.html` for the full topbar cluster.) + +--- + +## About the design files (read this) +The `.jsx` and `.html` files are **design references created in HTML/React** — prototypes that +faithfully show the intended look and behavior. They are **not production code to copy +directly.** Your task is to **recreate these designs in your target codebase** using its +existing framework, component library, state management, and conventions (React, Vue, +SwiftUI, native, etc.). If no environment exists yet, choose the most appropriate framework +and implement them there. + +The prototype takes shortcuts a real app must not: all content is hard-coded mock data, there +is no routing, and detail panes sometimes show shared sample content regardless of the list +selection. Treat the **tokens, layouts, components, and interactions** as the spec; treat the +running HTML as the pixel reference. + +## Fidelity +**High-fidelity.** Colors, typography, spacing, borders, and interactions are final. Recreate +the UI pixel-accurately using your codebase's libraries and patterns. Exact values are in +`docs/01-design-tokens.md`. + +--- + +## File map +``` +design_handoff_b1_workshop/ +├── README.md ← you are here (index + overview) +├── docs/ +│ ├── 01-design-tokens.md +│ ├── 02-components.md +│ ├── 03-screens.md +│ ├── 04-interactions-and-state.md +│ ├── 05-data-model.md +│ └── 06-implementation-guide.md +├── screenshots/ ← reference renders (dark + light) +│ +├── B1 Workshop.html ← entry point; mounts all four screens +├── b1-mount.jsx ← handoff mount: stacks the screens, owns the theme toggle +├── theme.b1.jsx ← WS_B1 theme object + all mock content (WS_DATA) +├── wschrome.jsx ← shared app shell (WSChrome) + shared primitives + icons +├── studio.jsx ← Design Studio screen + the one-time interaction CSS +├── screen-inbox.jsx ← Trend Inbox screen +├── screen-reports.jsx ← Research Reports screen +└── screen-etsy.jsx ← Etsy Posting Workflow screen +``` + +## Screens at a glance +1. **Design Studio** — compose & version print artwork (composer + library + focus detail). +2. **Trend Inbox** — triage agent-surfaced market signals (master/detail). +3. **Research Reports** — read agent-written market reports (list + reading pane + TOC). +4. **Etsy Posting Workflow** — pipeline kanban + listing editor. + +> All screens are **desktop layouts** sized as **1440 × 1180** artboards. No responsive +> breakpoints were designed — define those in implementation if the product needs them. diff --git a/assets/design_handoff_b1_workshop/b1-mount.jsx b/assets/design_handoff_b1_workshop/b1-mount.jsx new file mode 100644 index 0000000..4053c03 --- /dev/null +++ b/assets/design_handoff_b1_workshop/b1-mount.jsx @@ -0,0 +1,49 @@ +// b1-mount.jsx — handoff entry. Renders the four B1 (CONCRETE · AMBER DARK) +// screens stacked, fixed dark mode. No design canvas, no tweaks, no other +// directions — just the B1 designs a developer needs to recreate. + +const B1_SCREENS = [ + { id: 'studio', label: 'Design Studio', Comp: window.StudioScreen }, + { id: 'inbox', label: 'Trend Inbox', Comp: window.TrendInboxScreen }, + { id: 'reports', label: 'Research Reports', Comp: window.ResearchReportsScreen }, + { id: 'etsy', label: 'Etsy Posting Workflow', Comp: window.EtsyPostingScreen }, +]; + +function B1Handoff() { + const theme = window.WS_B1; + const [mode, setMode] = React.useState(() => localStorage.getItem('ws-b1-mode') || 'dark'); + const c = mode === 'dark' ? theme.dark : theme.light; + const onSetMode = (m) => { setMode(m); localStorage.setItem('ws-b1-mode', m); }; + + React.useEffect(() => { document.body.style.background = c.bg; }, [c.bg]); + + return ( +
+
+
+ Workshop · B1 — Concrete · Amber Dark +
+
+ Design reference · {B1_SCREENS.length} screens · see README.md for the build spec +
+
+ + {B1_SCREENS.map(({ id, label, Comp }) => ( +
+
+ + {label} +
+
+ +
+
+ ))} +
+ ); +} + +ReactDOM.createRoot(document.getElementById('root')).render(); diff --git a/assets/design_handoff_b1_workshop/docs/01-design-tokens.md b/assets/design_handoff_b1_workshop/docs/01-design-tokens.md new file mode 100644 index 0000000..a809cb7 --- /dev/null +++ b/assets/design_handoff_b1_workshop/docs/01-design-tokens.md @@ -0,0 +1,165 @@ +# 01 · Design Tokens + +All values are exact. The product ships **dark by default**; both palettes are required because +the UI is light/dark switchable. + +--- + +## Color + +Tokens are the same names across both palettes — swap the values by mode. In the prototype they +live on the theme object as `theme.dark` / `theme.light` (see `theme.b1.jsx`). Implement them as +CSS custom properties or your framework's theming layer. + +### Dark palette — `ws.dark` (default / signature mode) +| Token | Hex | Role | +|---|---|---| +| `bg` | `#1c1108` | App background — warm near-black | +| `panel` | `#271810` | Sidebar, cards, primary surfaces | +| `panel2` | `#312115` | Recessed columns, table headers, inset wells | +| `ink` | `#f4ece1` | Primary text **and** structural borders | +| `sub` | `#a89a84` | Secondary text, labels | +| `faint` | `#6e624d` | Tertiary text, eyebrows, meta | +| `hairline` | `rgba(244,236,225,0.13)` | Subtle internal dividers (within a bordered box) | +| `accent` | `#fb5a28` | THE signal — primary actions, active nav, scores, selection, highlights | +| `accent2` | `#f4ece1` | Secondary accent. In B1 this equals `ink` — **B1 is a single-accent system** | +| `ok` | `#6cae66` | Success / "live" / low competition | +| `warn` | `#e0a24a` | Caution / "in review" / destructive / "mock" | +| `onAccent` | `#1c1108` | Text & icons placed on an `accent` fill | +| `hover` | `rgba(244,236,225,0.06)` | Hover wash on nav items / segments | +| `thumbBg` | `#382616` | Image-placeholder fill | + +### Light palette — `ws.light` (bone / clay) +| Token | Hex | Role | +|---|---|---| +| `bg` | `#e9e6dd` | App background — warm bone | +| `panel` | `#f7f5ef` | Surfaces | +| `panel2` | `#efece3` | Recessed | +| `ink` | `#16130d` | Primary text + borders | +| `sub` | `#736d60` | Secondary | +| `faint` | `#a39c8c` | Tertiary | +| `hairline` | `rgba(22,19,13,0.14)` | Dividers | +| `accent` | `#e23c0c` | Signal | +| `accent2` | `#16130d` | = ink | +| `ok` | `#3f7a44` | Success | +| `warn` | `#b8761f` | Caution | +| `onAccent` | `#f7f5ef` | On-accent text | +| `hover` | `rgba(22,19,13,0.06)` | Hover wash | +| `thumbBg` | `#dedace` | Placeholder fill | + +**Notes** +- `ink` doubles as the structural **border** color — a defining brutalist trait. Borders are + full-strength ink, not a tint. `hairline` is the lighter divider used *inside* a bordered box. +- There is one accent. `accent2` exists only so multi-accent themes could plug in; in B1 it is + ink. Do not introduce a second hue. +- `theme.id` is `'concrete'`. Some prototype code branches on other ids (`'phosphor'`, + `'greenhouse'`) for glow/shadow variants — **ignore those branches**, they belong to discarded + directions. B1 has no glow (`c.glow` is falsy). + +--- + +## Typography + +Two families, loaded from Google Fonts. + +| Family | Use | Weights | +|---|---|---| +| **Archivo** | Display headings + body copy | 400–900 | +| **JetBrains Mono** | Labels, eyebrows, metrics, chips, buttons, all-caps meta | 400–700 | + +### Display style (headings) +``` +font-family: Archivo; +font-weight: 800; +text-transform: uppercase; +letter-spacing: -0.02em; +line-height: 0.98; +color: ink; +``` +Applied at: screen titles ~26–30px, panel titles 16–17px, big numeric readouts 34–48px +(weight 800, often in `accent`). + +### Body copy +Archivo, weight 400–600. Paragraph text 13–15px / `line-height` 1.5–1.6. Dense list text +13–14.5px / `line-height` ~1.2. `text-overflow: ellipsis` + `white-space: nowrap` on +single-line titles in cards. + +### Mono / label style +JetBrains Mono, 9–13px, usually `text-transform: uppercase`, `letter-spacing` 0.04–0.1em. +Used for eyebrows, field labels, chips, button text, table headers, metrics, and all numeric +values (often `font-variant-numeric: tabular-nums`). + +--- + +## Geometry + +| Token | Value | Notes | +|---|---|---| +| `radius` | **0** | Hard corners everywhere. The one exception is small status dots (full circle). | +| `borderW` | **1.5px** | Structural border weight (color = `ink`). | +| hairline border | **1px** | Inner dividers (color = `hairline`). | + +--- + +## Elevation / shadow + +There is no soft blur. Elevation is a **solid offset block** in the accent color: + +``` +--ws-hardshadow: 5px 5px 0 ; /* hover lift on cards/buttons */ +4px 4px 0 /* selected list cards */ +6px 6px 0 /* open dropdown menus */ +``` + +Applied on hover/selection only; the resting state is flat. + +--- + +## Spacing + +No formal numeric scale was defined; values cluster consistently: + +| Context | Padding / gap | +|---|---| +| Controls (buttons, chips, dropdowns) | `6–11px` vertical, `9–16px` horizontal | +| Cards | `10–14px` | +| Panel headers | `16px 20–24px` | +| Section body padding | `18–30px` | +| Card grid / list gaps | `8–16px` | +| Section-to-section gaps | `16–26px` | + +### Fixed dimensions +| Element | Size | +|---|---| +| Artboard | 1440 × 1180 | +| Sidebar | width **250px** | +| Topbar | height **74px** | +| Trend Inbox signal list | width **452px** | +| Reports list column | width **364px**; TOC nav **188px** | +| Etsy pipeline column | width **372px** | +| Studio composer column | width **396px** | +| Studio focus thumbnail rail | width **220px** | + +--- + +## Imagery / placeholders + +The prototype ships **no real images**. Every image surface is a placeholder you will replace +with real generated artwork / product photography: + +- Fill: `thumbBg`. +- Overlaid hatch: `repeating-linear-gradient(45deg, 14 0 2px, transparent 2px 9px)` + (i.e. ink at ~8% alpha, 2px stripes every 9px, 45°). +- Optional mono caption top-left (e.g. `ref`, `1024²`) on a `thumbBg` chip with a hairline border. +- Optional accent label bottom-right (e.g. `REF·1`, `v3`, `LISTING`), weight 700. +- 1.5px ink border, 0 radius. Hover scales to `1.04` with the hard shadow. + +--- + +## Iconography + +Icons are **inline 20×20 stroke SVGs** (`stroke-width` ~1.6, `currentColor`), defined as a path +map in `wschrome.jsx` (`WSIcon`) and `studio.jsx` (`Icon`). No icon font / no SVG illustration. +Map them to your codebase's icon set. Names in use: `inbox, doc, check, image, box, shirt, tag, +chart, pulse, plus, search, bolt, eye, down, branch, trash, arrow, clock, flame, cal, star, +sliders, ext, edit, x, layers, dollar`. diff --git a/assets/design_handoff_b1_workshop/docs/02-components.md b/assets/design_handoff_b1_workshop/docs/02-components.md new file mode 100644 index 0000000..f8079f0 --- /dev/null +++ b/assets/design_handoff_b1_workshop/docs/02-components.md @@ -0,0 +1,131 @@ +# 02 · Components + +Specs for the shared shell and every reusable primitive. Colors are referenced by token name +(see `01-design-tokens.md`); they resolve against the active mode. `radius` = 0, `borderW` = +1.5px, `border` = `1.5px solid ink`, `hairBorder` = `1px solid hairline`. + +--- + +## App shell — `WSChrome` +The frame every workflow screen renders inside. Three of the four screens use it directly +(Trend Inbox, Research Reports, Etsy Posting). **Design Studio reimplements the same shell +inline** (historical reason) — keep them visually identical in your build by extracting one +shared layout component. + +Structure: a flex row — **Sidebar (250px, fixed)** + **main column** (Topbar 74px + content). +The content area is itself a flex row that the screen fills with its own panels. + +### Sidebar (250px) +Top → bottom: +1. **Brand block** (padding `22px 20px`, bottom border): + - 26×26 `accent` tile, `onAccent` bolt icon. + - Wordmark `WORKSHOP` in display style, 19px. + - Sub-line: `Human-driven commerce` — mono 10.5px, `faint`, uppercase, letter-spacing 0.08em. +2. **Nav list** (padding `12px`, gap 2): 9 items. Each item is a flex row (gap 11, padding + `9px 11px`): 16px line-icon · label (Archivo 13.5px) · optional count badge. + - **Active item:** background `accent`, text `onAccent`, weight 600. + - **Inactive:** text `sub`, weight 500, icon opacity 0.85. + - **Badge:** mono 10.5px weight 700, padding `2px 5px`; active item inverts (bg `onAccent`, + text `accent`), otherwise bg `accent` text `onAccent`. + - Nav items: `Trend Inbox` (badge 6), `Research Reports`, `Approval Queue` (badge 3), + `Design Studio`, `Product Drafts`, `Printful Workshop`, `Etsy Posting`, `Analytics`, + `Agent Runs · Logs`. +3. **Footer** (top border, padding `14px 16px`, gap 9): + - "Image budget" row: mono label + `$3.84 / $25`. + - Budget meter: 5px track (`panel2`, hairline border) filled 15% in `accent`. + - Operator row: 28×28 `KR` monogram tile (border, `panel2`) + "Operator" / "local · no auth". + +### Topbar (74px, bottom border, bg `panel`, padding `0 24px`, gap 20) +Left: **screen title** (display style, but most screens render the title in their own header +band — see each screen) + subtitle. Right cluster (flex, gap 7, vertically centered), +**in this order**: +1. `LIVE` status chip (`WSChip tone="live"`). +2. **TXT** model dropdown (`ModelDropdown`) — default `gpt-5`. +3. **IMG** model dropdown — default `gpt-image-2`. +4. **Light/Dark toggle** (`ModeToggle`). +5. `cap $0.50` chip (`WSChip`, plain). + +--- + +## Shared primitives + +### `WSIcon` / `Icon` +`` rendering one path from the +icon map. Props: `name`, `size` (default 16), `sw` (stroke width, default 1.6). + +### `WSChip` — status pill +Inline-flex, gap 5, mono 11px, uppercase, letter-spacing 0.04em, padding `3px 7px`, 0 radius, +`white-space: nowrap`. Border = `1px solid` (tone color if toned, else `hairline`). +- Optional leading **dot**: 6×6 full-circle in the tone color (omitted for plain/`tone="plain"`). +- Tone → color: `live`/`accent` → `accent`; `ok` → `ok`; `warn` → `warn`; default/none → `sub`. + +### `ModelDropdown` +A button that opens a menu; only one open at a time (managed by `openDD` state); closes on +outside `pointerdown`. +- **Button** (`.wsx-dd`): inline-flex gap 7, mono 11px uppercase letter-spacing 0.04em, padding + `6px 9px`, 0 radius, `1px solid hairline` (→ `accent` when open), bg transparent (→ `hover` + when open). Content: dim `label` (`faint`) + bold `value` (`ink`) + chevron that rotates 180° + when open. +- **Menu** (`6px 6px 0 accent` shadow, `1.5px solid accent` border, bg `panel`, padding 4, + min-width 210, z 60): a faint "
`. Use real ` +
+ changes saved · draft + + +
+ + ); + + return ( + + {Pipeline}{Editor} + + ); + } + + window.EtsyPostingScreen = EtsyPostingScreen; +})(); diff --git a/assets/design_handoff_b1_workshop/screen-inbox.jsx b/assets/design_handoff_b1_workshop/screen-inbox.jsx new file mode 100644 index 0000000..07fb36f --- /dev/null +++ b/assets/design_handoff_b1_workshop/screen-inbox.jsx @@ -0,0 +1,176 @@ +// screen-inbox.jsx — Trend Inbox workflow screen. Exports: TrendInboxScreen. +(function () { + const { WSChrome, WSIcon, WSChip, WSScore, WSThumb, wsBtn, wsBorder, wsDisplay } = window; + + function TrendInboxScreen({ theme, mode, scan = true, onSetMode }) { + const c = mode === 'dark' ? theme.dark : theme.light; + const F = theme.fonts; + const D = window.WS_DATA.inbox; + const border = wsBorder(theme, c); + const display = wsDisplay(theme, c); + const [filter, setFilter] = React.useState(D.filters[0]); + const [sel, setSel] = React.useState(D.trends[0].id); + const t = D.trends.find((x) => x.id === sel) || D.trends[0]; + + const stateTone = (s) => (s === 'RISING' || s === 'NEW' ? c.accent : s === 'COOLING' ? c.sub : c.warn); + + const Spark = ({ data, color, h = 30, w = 92 }) => { + const max = Math.max(...data); + const bw = w / data.length; + return ( + + {data.map((v, i) => ( + + ))} + + ); + }; + + // ── Left: signal queue ────────────────────────────────── + const List = ( +
+
+
+ Signals + {D.trends.length} new · agent · 11m ago +
+
+ {D.filters.map((f, i) => { + const on = f === filter; + return ( +
setFilter(f)} style={{ + fontFamily: F.mono, fontSize: 10.5, fontWeight: 600, padding: '8px 10px', cursor: 'pointer', + border, borderLeft: i === 0 ? border : 'none', background: on ? c.accent : c.panel, color: on ? c.onAccent : c.sub, + whiteSpace: 'nowrap' }}>{f}
+ ); + })} +
+
+
+ {D.trends.map((x) => { + const on = x.id === sel; + return ( +
setSel(x.id)} style={{ + background: c.panel, border: on ? `${theme.borderW}px solid ${c.accent}` : border, + boxShadow: on ? `4px 4px 0 ${c.accent}` : 'none', borderRadius: theme.radius, padding: '13px 14px', + display: 'flex', gap: 13, alignItems: 'center' }}> +
+
{x.momentum}
+
mom.
+
+
+
{x.title}
+
+ {x.state} + {x.delta} + · + {x.niche} +
+
+
+
+ ); + })} +
+
+ ); + + // ── Right: trend detail ───────────────────────────────── + const Detail = ( +
+
+
+
+
+ {t.state} + {t.niche} +
+

{t.title}

+
+
+
{t.momentum}
+
momentum {t.delta}
+
+
+ + {/* metric row */} +
+ {[['Search volume', t.vol], ['First seen', t.age + ' ago'], ['Sources', t.sources.length + ' channels']].map(([k, v], i) => ( +
+
{k}
+
{v}
+
+ ))} +
+ +
+
Why it matters
+
{t.summary}
+
+ +
+
+
+ Keyword openings + vol · competition +
+
+ {t.keywords.map((kw, i) => ( +
+ {kw[0]} + {kw[1]} + {kw[2]} +
+ ))} +
+
+
+
Momentum · 7 wk
+
+ +
+ 7w agonow +
+
+
+
+ +
+
+ Suggested design directions + agent · from this signal +
+
+ {[['Linework', 'hand-drawn'], ['Duotone', '2-colour'], ['Specimen', 'herbarium']].map(([lab, sub]) => ( +
+ +
+ ))} +
+
+
+ +
+ + +
+ +
+
+ ); + + return ( + + {List}{Detail} + + ); + } + + window.TrendInboxScreen = TrendInboxScreen; +})(); diff --git a/assets/design_handoff_b1_workshop/screen-reports.jsx b/assets/design_handoff_b1_workshop/screen-reports.jsx new file mode 100644 index 0000000..1cb01f4 --- /dev/null +++ b/assets/design_handoff_b1_workshop/screen-reports.jsx @@ -0,0 +1,167 @@ +// screen-reports.jsx — Research Reports workflow screen. Exports: ResearchReportsScreen. +(function () { + const { WSChrome, WSIcon, WSChip, WSScore, wsBtn, wsBorder, wsDisplay } = window; + + function ResearchReportsScreen({ theme, mode, scan = true, onSetMode }) { + const c = mode === 'dark' ? theme.dark : theme.light; + const F = theme.fonts; + const D = window.WS_DATA.reports; + const border = wsBorder(theme, c); + const display = wsDisplay(theme, c); + const [sel, setSel] = React.useState(D.list[0].id); + const [section, setSection] = React.useState(D.focus.toc[0]); + const r = D.focus; // detail content is shared mock; selection drives the header + + const selItem = D.list.find((x) => x.id === sel) || D.list[0]; + + const SectionHead = ({ children }) => ( +
+ {children} +
+ ); + const Body = ({ children }) => ( +
{children}
+ ); + + // ── Left: report list ─────────────────────────────────── + const List = ( +
+
+ Reports + {D.list.length} total +
+
+ {D.list.map((x) => { + const on = x.id === sel; + return ( +
setSel(x.id)} style={{ + background: c.panel, border: on ? `${theme.borderW}px solid ${c.accent}` : border, + boxShadow: on ? `4px 4px 0 ${c.accent}` : 'none', borderRadius: theme.radius, padding: 14, + display: 'flex', flexDirection: 'column', gap: 10 }}> +
+
+
{x.title}
+
{x.niche} · {x.secs} sections
+
+ {x.status} +
+
+ + {x.date} +
+
+ ); + })} +
+
+ +
+
+ ); + + // ── Right: reading pane ───────────────────────────────── + const Reader = ( +
+ {/* report header */} +
+
+
+
+ {selItem.status} + {r.model} · {r.words} words · {r.date} +
+

{selItem.title}

+
+
+
{selItem.score}
+
opportunity
+
+
+
+ +
+ {/* TOC */} + + + {/* body */} +
+
+ Market summary + {r.summary} +
+
+ Competitive landscape + {r.competition} +
+
+ Keyword opportunities +
+
+ KeywordVol + CompCPC +
+ {r.keywords.map((kw) => ( +
+ {kw[0]} + {kw[1]} + {kw[2]} + {kw[3]} +
+ ))} +
+
+
+ Recommended products +
+ {r.products.map((p) => ( + {p} + ))} +
+
+
+ Risks & cautions + {r.risks} +
+
+
+ +
+ + +
+ +
+
+ ); + + return ( + + {List}{Reader} + + ); + } + + window.ResearchReportsScreen = ResearchReportsScreen; +})(); diff --git a/assets/design_handoff_b1_workshop/screenshots/design-studio-dark.png b/assets/design_handoff_b1_workshop/screenshots/design-studio-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..a8b0b1cb66571833e50196ac67f336ba83bc744e GIT binary patch literal 44419 zcmeFZ1z1(v);GR34I&6^1f;u>ZrFsI5Rh&>bSm8MJZ(^c_}%#nt`jCnzp?TT+SlI(%#d@ zKfwQLZLuSM}^P@*9bG+r19J;YD{C8DJ6AV%4^J=eM}bd+f}e$ihQ-AvBqlviPD#zl&C4$+ zEGjOks;;T6t8Zw0{q|i)XIFPmZ{Nu1*!aZc$4^sV=NA^2mRDBS)^~RI_74t^j!#a1 z^a}!@{?;w<-`_g+hkg-&{X#)QLq)^<(Ju&!CwQR}prO<9U=T{HW12b>(ee6YL1kiJ zRJLQ&^J#o1Hgg%qxyiu)m2u}s*M9BUzprBf|EZq+qhr7KYYMI!{s4Xn z1gC5V6R227zql^*^#xg)XT(u0)p&OIg$1fHzN_wX9^!B&DU7iaFEq zo=qiDMv@mtYGJ&C^c0I19VUKkQhn&Gw3L0nN!PJJI2)&U*LE@c9YHTN9*v;)7LA%r zdf=EP5JT#Zf0GpzSXsET_eo zaK3RaJC{TnW2E#{y(YqWG%UmXj>sKt<{=F~2m>U3>~siS54nljn*4bpHJiQkHl7vD zLNS?0sTn`69$T=)PCp924hOL;RUV!V3SsEskVl1w^-jM$4OwFAQqJq314`KGf%W@n zkFD^gNs5sKnf9=NcW*?pG1cKv;D5t4yK(XLWVi*<3=>U-9VjAK3)RqlfB&r#4(QIc z&QWKOPoBNoD~105`JpyP>%xlKCGgV!63B`3qyDwiLNAY=t?001F#{Y%2@er{de=&Topc`2Xn}x2M;jpoXCzqS!i*0H+;bE_QFZn!u6SnvH z5}m(IxOu?-Ce9oIAI^O%yO>WzbTr`XGh+zy-l3vM}?cuW+}u zp$QJvCi@SaU0PL_Ky-#I@(m_3>~8%ZHVEhN4<4s~`sO8GJcT2_!@R72HT6Vw&Q5UV z_DzMA5bAkLH2j_+Oj)?q`lInTX^yzter`oN>?z-{}*6_j&bLYp9 z0pE*~r^wq?QD(7l{=Sy`QmEhi0XKdf_qtkJPg8*z^b}K>fv2Xy5D6HLM||n@`mk7g zqdp!PdC^h!wS67;g39Z2g%SbsG;ueSc)IYgD4w`;R#UrW%r z-Ws;KlwzJrR{kz4aR&DZe|;uvZT0J^f}5&l_O?Nw<^z91aWhaTr*R{i%6}O!+EgWdE$7`@LE16=B$B0nbUK}OesYp-zod( zbA;g~FsY3+s}aB^#Cn`lG_Woh3{;`^ifp9E>mR~)E*vj``?@D#!Es5!mGOy!pFZjM zW|k9=Z;ii98g8jP2`32P8$)Csyf8w9FxcPL&$PfKNy_Jyc!ZF?zY=2q>A}QNosf`C zRH?g(>qP&ga4oU7(^3nXJ+@J{h{AGMQR0-st%hbHZV zA|BBsSj}NvW2Qi0!!oa<^3&{ytB5<#PN~{dUGy?gvzFc}s7rMp)42q!le`)4_t7sN z?3Y~vB=-$f3g8qj3{}m3*|PCb-X&A7XS&PR*zvj=^|bK$#FSho`~+*ZT&|rZ)$yn} zI^{|ll?K?!MEl9uzXzU_VLLWY~u@JH81;`n>rkDK?;K$ zQ_1*r6_IE>X%cLVuP{;)Paz!}vPi`vAF`ivU_-0q8F`n7`2&N;Ax8D$kU-kFg5v?{ zhilP^ypMcL)G#ta{>*5U=GD$EanplquD4=iFdfg9{bSZ|S>;=oOBNR7sJ4~}sdwR1e^n~5 zi<#MAr$nZ6)}pq>$EZ?bNeGvcOi5;^hR?0(J|I^-%8f7l+MYMMFV?!nQ?y=>&GG(7 zThy&v90zC{{WPjLiROzhQv^g5J@Ys%JUKx66v^p$J`bk zMamvjEX9Q^H3_>igPYZbeBA}cax|{#F!f^-WCG-uiS-fqxLN&Z=79MFcEuFztx?;x ziRzk{HEXZrOWU7^D43;4G+YAIgTy0x$G%$+9q+_rJ96b(aJl-&_#AAEjh%AEOc-b- z>1G-Q$eGE+Nuxn#aDeXbi@mP%;cGb}iUgk&Nx!_Fcph34dcY81s8OJ&ctX111Wpc) z)w8cBHRT$%A7YzvgNV?!*Rcjkw{&XzneM~Q;-*~DJDAyAoSrmf%W)2ri<0K-U?h~2 ziVKr=7gI*B7*a7cxR7nS)Qyem+&hZ;zuS)_B{okZ4xY z`CE;gh7>eWd2gKjOLz6oT?GHMgYF|O2!m)27V>o%i62Fjj#p%Q=iwiqF?Xj z$-z)Ms7GF_kVpxV-1hbq%5wX(@@i0XP*Zuh2AgBzRBRL)JLAXwbbnXRFSd3oMPcgYU&x+GiDtb7c^arbqd~!q^51n4-=DK;w3+uB)+T;q!EsOVh$_)6TlE;3 zhHPZsd!*cOU|O|-^pU*;o~T{|#B%Rno^0V?oWRbtHO7#$heeXTP1cXy27J!6-y=B+ zE&-36vl9~Qf>L#l`-njs#JAo{V7};+`z0Xp;u2_Oz68RGwwLOY`plQ_tV55`P@`bC zBBz8ozcf5$y@N)X!7X2ZCZ%bQqVn<>J@UEn-W2uOF5UUl9yv^YpVJOn4>YHH=O0_L2OO-jJR_VMqlTjmmqRo;M$q zEGB}QkK&Z-d}wjZzhMVHNzT3|_Tw9K%`O4ACtB+P>aMLvDpa2j{rdqW>eD>%SIy$e zOQ1yc2rlZ!+YG-1s93=>U4B=ff6^6hDr&TSB*rrDo9_M48FzE5@nbdB;^J7{?v(zTK;7Nn+(PB@}07agl>@ zwXhk6dsV!!v5rtAch}SymLf|^#YnRjP9;ao-DcAdH(yJw6$q+1a=x!bwKo5i9Atca zVbyRzThzU{^~^IdJl)*OjIT@78YBF^=)~Z_nYi@hg%n=J83c8?L5?3wd!F8OI|qTG zKeW~N+D**w%wfTw-%VN8ytUa1J`lP8K@Yzzbu7n^T_C9XGvkJQ+SB_bXVIvcpSoS| z9NwH1;+|2VS~C@itS5+`C>xf7zT7XBP;#95il>{4B267qsApT3>cTrELqB^IrO3P^ z;1C9s+nU;2jHKBdk{P<5cPt{A=gNlv?M@@sc#o8H$3H!$K4oQ_{YkcN8!l_8N1 zXzaQNpfVs)LKYQuT_~cm?uI?i@6brJg6nA3SVSzOKOv?F9ONM&Dj|E~fW;x0sk$nX zZP~{zVxDl{MUQ|`7R$c3XMq-$4GYYzEr5+!*@MO?45@R)wn7~PhAd$%`==dJSZ&q_3Ez_YrNz4F)`e0eXU#RlbqScWAJ9u&hJ8;-#&`Hbw`86GCu1J0`z1?fkNow4}djS)Yt zrZSs>Zk+7+{X+M$jWNzczh3%kCb}tXN{N~!cm;}k=`QkhmZ;ca6!aRc4E=G-2@-5Cf68hq+tG&TegsiHW(PKMnJy4Go+TN6=r)?ZKyrdrx8Qw>ugV*QmDAskvhhZ z>b3>43p*iw@uK$}5q){+Y~A8JJ*&R`fbFodB5IQQI3D@O1=vI}gtkQo>;2hV@+HTn zz`)Gpwa#BBK1Mz(IY8==8u|JK1#dSR6V1GiU+}<_Lkzy-(XCkcRE={_2_4OIG}qXT zIX|vojZSHDKeXt*{rnOV9soP<72ZwG2tXb{0;cU@$2)^lJ$ZGp?3m7ZNEemhFC#P^8#a1zV7`#WQ$($_68|c^U zxfaz}0LPZS7G$z)Jv+IbG3)zU4gK_?Llupih~`M?jGwhET@$?K3k&o0V{I6lF&oX- z@jH6dO;)s3u0s)236OGyGExf^5&GD$T)Kt^Iod_X1eZKt1$zJO>r4|fZ|uYh)+E&@ z#Ttb7!GTlPuiGJu6?(?LMB(yi`{1B-Vkxp&PWjtMA?pffDMApu#A3+_1OC~?R!v6V zDx^em?~ZXvZ`?ejG`1zaDvzaaFB`)QJ!8 z9bV+kNp~?2R!945NbA%*N?YcZQ^QTd%8tz#)>vp zy@{<#r}SlQ@t>1&CUJZ?Phwb%*L-Na9o^?g2^ae9DEp1Xx&+3V&vG(UqAxTMuUW?+ z6a(tg>RHJkPSBYc&Lzi3YecO~q{$oHCFkU-!b6K@UOe0WO{zzHO%ZU7k-zth0aezB zSV7SeJ%P`T7ERk~DJV8?;TMAf- zGg1SF3_OFB{_Phm-qfaSs=W53sKRR+WeQlghOs!M zId4tm*^DRi49Ghvf#U5i64W!HjH>+4df>D1@j>WG*T!w4eLte^{U6S<*5o z6k*c?_}aP<8Nw71+{?Q7qqvWuvbWKeo%0c9Qe`*Ox$BF{raj zUL|TlwdV}Y`1jpY^b_^#Ub@=WUk(3{4K*6OJ8zM?smm8!jO$eRt+$m9K@4M+5@eFT z;JWD?PyU;*q6}B910fNqjhAQZw~<`YSBdgrDPqY_unW|R)^A0v$FHy6*TVs=GmA@L z9}iq9Z*o9Ry81J@J`06tZpB{$L!ukV)9_0mJohK6)J+ZjucrSi1HMivF{Su2CD$f2 z{c)6>{PyTj?CC_LdJV`5N-u%aZP>ns8t*t%T>+==bz=FVOD|g5(>u>d+o^+eRe3Uz&dBmBz@YQSN8H~n=;?A;}SS1h23kZscD%AS_~-}%}Q+K;Qh4F0z8~qKh$|8 z%OAHa@@9dCGpjF-(yGsfDaC@Ff`-IereRRE+{G^LU4d=H;Ntdc>&O|Wz@_$6&+^h| z!7GZdl`d4c%+V3vDEPDKJ24WgsB(k))y?U$M6Y6lweRC>Q++DzF4gaSz}b`bkcUU! z3e6-Mgrv+q&8grOTR%J!oQiP3lGi3*AU)X9_g6h67tt8%E=r1lOrxG7`61&54?Kk8 zL(CFF$rJJNJ$?B!4b}xGGd*LO7OvSlM-b>urut>+R4Qp&yG1NmL94rjdj}nnwN_KI z&JOjd83p<|4Q@%2{nXTbexe&r5xc127t2WZ>VNHpq-)CX z{9b@{q&D=c%mHJI^gglK3)SO`=g7vwJv-&11AIEwlhd?aRU}hvjyu_dMyUf@*l}up zss60ddm<^9Y`576YJ=7#lG@!o`;LMMN6n~OcAfD^%D8(Q#9=Y}Cxp9Ue1BO7cQKrP3Mghj?^GA1ZsuI<(Ea|hlf{HT3k)+1| zXgvBKcUjD7mjFi{lE3oX?qq-0`QwjksUA$*lQfo7UvHgC^$$^|z>c?s*qaLv10AW~ zCGVXVllDS)w`BAeHLzu*jUbbCBK#kZFj5o~2nU09k($Q~r(-iCQ^u#~p)!OJlByf; zXwK84&z4ik$n>A9n~@Y_NW$*Eg!0OnsYJfI)8RZ6!wZA0s6wXRo7n2N-J=pWkKN?E zyRdZUE4Ix1p-f=LJ1dw(O>`YM?(@wTjpPyLuzb1UzMBel4&IDtb=-O8`)fZ|30JRV zo4JKpY=EE07w#??jwkW+1Jlk}ZT(sdoLOw1FUUlipdPgQebR^E*ylF(ypF6_$Th#- zlhza^daI_nNPK^rN2{LhUa!E#6EvN&e*A&X5D%kt*I@ekk%A(7*jW9A8^~Z~1q1{t zd5=>xe-;7+41cEidwY#KcpINl_58O&AaeT)tR_6m$bfYl4K%fU8yHafc$8(VSnUHinPAK89x?OgRs6^ ziw^`&`w8w;cj*Z#Su7N=@s!Pm#4W4vO4}X3V-J2vb!LNV71kGT6wImr&{Cyj9jDJQ zBGWIzS@T`d$?KNRXNl2_sE)If!duagK=ec{USszl8RTT_k`_Ma{j;Jw5kBh0>&BnH zJ$$2^veH}q&D^o|F3<`G^>6zbiOxkQJ?xvqAs%}HJA=aAl{v;YAeOey*Q*sAjI!l? z8$`N|ZUIv{)tt4~_ELJYQ?<>TP4O#uq^>i**WvCJ<#h#|;sL|Kcf38n&^n}>`3bU= z&1!1-U~id*eIE1)(3VfBM>GF;I>uhEv^?m@7|uAOei8uVzOnGpfAV|w^)Pm_b6Ju@ z_m&UpWQ-Yy9JGzIGuG6qA9UWvY25?_MSv9yzaKg$N*F6PYH>FDKhf7v6#f7nbU@YB zRh*%ud&Umi>1t>-T52%e85q%9a zZ5CfF4)_TnKn&jq*eok?;u!BMO9?CyC{Xh!swY&3=|!25!CWt{ErE8|HPlSM(=nF^ zBs+99F?U|PwxHfA%g`${&o?a?@$EllOG@-++^SY&QETrz&y5N^x7)JG7$1I`Gk>C| zO|E);+6u~{3zq<#@s|?8tepb{xD&cTzY#M|5KHol5~d9$qYLAgV@T8nh}3I?(&S=m z^U#UJw7R$(H~go{)TIbyA2>RYN~5V`oVDNJdeV?*sO0X~_UaC;ggKUA*4u&{KLP@C z_#L&9T!>T~O17wPnu5gx1>C2Lm60y070#fS7FI!ReS^0CmoT+fH&Kl4QT|x{skarC zhO)OB-UUga`uf`0v7lgal-noQwlu5rtg8e3`*i^(33@q7vN4#)v=Y?NZs;3K z^E~kJ$}u0y0c>~Lp|nrc=`ci0N-$K5i&?TU;J9iu3g!w!2pksFUFhlmL9NbqPjR@V zkR*s3*2Uz|K|3D_U0~U`P9)k}Dwa8=AHaN?)1Wm%!)IOQ2);^#;-lRFn~*Ku4sh4sU+@ zKV6)-ir?9Er=gl>%0^B{Y#u;HT!^EE~7KN)6F~y8|_PM8J!G z)sfE9o5?#lhiyf`V12l(##h22Q2vd}CZ;wfbp^U7zZ#~HxnRbjVY?NVBg_LgRzb06 z&~c#J%*U;^Y1dv{!Sk_i_?&$Y zCT+bGQ)qD%w;=%ad*U&5DT;|wPvYn>`<3+AThm`+_$J-?bXo(|k^QM3;{sM$d%DFD zk?v46Ov3)cNG0w2GK#uxg(eYMY4MrqkKFUqws_YbY0OwLoymRdxu18NRJdXy3|QLq z8$FkG5+M6U|0dl~-^#I6?*-qISLEF#0J&G~o#Zr zct>FLhmB~Gft8KdxvfxL>W1MWnKattS!6(#@Wq@j7OV(d@mLnIvT~jk*oXTYQXYGl z{V_%rJk!JiOy1r65HuGt48|eKyf0a=?Gy}M(&L<*Ghz9PaDd)AvSH)%2nbZ6ptEp= zpgFJ)@=K{Hvc15}GCCxd_|-oa!th6bw?d9!_G*sUS0P+&3uw5R_e4*%`&dxYHOL$~ zD!aPqny#e-PF}(r8@eDa#=7pDtM}nUiN4yG*q%Ihc6TUueOyFDf2BNwSp{!tv94O* zqX6FP|dnm^8xF2{xkc0@9}n(a(S$GbJ>twSzx2(5&Ay(5poGumVoj zgDj-~Bvr9mZQ~%%Zpc$LMn<#bxR{vZYL(d-U<35)8dj8gucahDGvhI zs!dwfO;#}Luz1;5Sb^Dk&!#SE*=D)-DQ_eEZMp@OYd>I^Q^Yviefan}7hMmT8f4RX zP(WbwnWB8FtEJ*AHmze!B!#mZvWP2zZ6(z6WLF ztTe}SOh+Ww4|Gku=@K|OsROZAj~@uy&u{;Vka1lC$S3x#-zh=-?dP|D1=y}u7=88K zANbtQ)&14@{}aPbDF2&F|KC0QX-eWcDJI+Q`nQGC5PZ{F$2NODNZ8SPv_>=kfM7Ju`b{=1B

h(%yTwZl7BRpjGW{jwG`UUNLHeIAvd$xvMU0G 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 literal 0 HcmV?d00001 diff --git a/assets/design_handoff_b1_workshop/screenshots/design-studio-light.png b/assets/design_handoff_b1_workshop/screenshots/design-studio-light.png new file mode 100644 index 0000000000000000000000000000000000000000..a8b0b1cb66571833e50196ac67f336ba83bc744e GIT binary patch literal 44419 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

h(%yTwZl7BRpjGW{jwG`UUNLHeIAvd$xvMU0G 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 literal 0 HcmV?d00001 diff --git a/assets/design_handoff_b1_workshop/screenshots/etsy-posting-dark.png b/assets/design_handoff_b1_workshop/screenshots/etsy-posting-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..8720256dbd431f54180c4f6ece3c2f40783c2d6c GIT binary patch literal 43782 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 v9S0Hx)|)Xl { + const p = { + inbox: 'M3 13h4l1 2h4l1-2h4M4 13l2-8h8l2 8', + doc: 'M5 2h7l3 3v11H5zM12 2v3h3', + check: 'M4 9l3 3 6-7', + image: 'M3 4h14v12H3zM3 13l4-4 3 3 3-4 4 5', + box: 'M3 6l7-3 7 3-7 3zM3 6v8l7 3 7-3V6M10 9v8', + shirt: 'M6 3l4 2 4-2 3 3-2 2v8H5V8L3 6z', + tag: 'M3 3h6l8 8-6 6-8-8zM6.5 6.5h.01', + chart: 'M3 16V4M3 16h13M7 13V9M11 13V6M15 13v-3', + pulse: 'M3 10h3l2-5 3 10 2-5h3', + plus: 'M10 4v12M4 10h12', + search: 'M9 3a6 6 0 100 12A6 6 0 009 3zM14 14l3 3', + ref: 'M4 14l4-5 3 3 2-3 3 5zM4 4h12v10H4z', + bolt: 'M11 2L4 11h5l-1 7 7-9h-5z', + eye: 'M2 10s3-5 8-5 8 5 8 5-3 5-8 5-8-5-8-5z M10 8a2 2 0 100 4 2 2 0 000-4z', + down: 'M10 3v10M5 9l5 5 5-5M4 17h12', + branch: 'M6 4v8a3 3 0 003 3h2M6 4a2 2 0 100-.1zM14 4a2 2 0 100-.1zM12 15a2 2 0 100 .1z', + trash: 'M4 5h12M8 5V3h4v2M6 5l1 12h6l1-12', + dot: 'M10 7a3 3 0 100 6 3 3 0 000-6z', + }[name] || ''; + return ( + + + + ); + }; + + // Hatched / dithered design-thumbnail placeholder. Pattern keys off theme. + const Thumb = ({ c, theme, label, sub, ratio = 1, big = false }) => { + const seedHue = (label.charCodeAt(0) * 37) % 360; + let bg; + if (theme.texture === 'scan') { + bg = `repeating-linear-gradient(0deg, ${c.accent}22 0 1px, transparent 1px 3px), radial-gradient(circle at 30% 30%, ${c.accent}1f, transparent 60%)`; + } else if (theme.id === 'concrete') { + bg = theme.duo + ? `radial-gradient(circle at 72% 26%, ${c.accent2}2b, transparent 56%), repeating-linear-gradient(45deg, ${c.ink}14 0 2px, transparent 2px 9px)` + : `repeating-linear-gradient(45deg, ${c.ink}14 0 2px, transparent 2px 9px)`; + } else if (theme.id === 'greenhouse') { + bg = `radial-gradient(circle at 50% 40%, ${c.accent}26, transparent 62%), repeating-linear-gradient(90deg, ${c.ink}0d 0 1px, transparent 1px 12px)`; + } else { + bg = `linear-gradient(135deg, ${c.accent}26, ${c.accent2}1f), repeating-linear-gradient(45deg, ${c.ink}10 0 3px, transparent 3px 12px)`; + } + return ( +
+
+
{sub}
+
{label}
+
+ ); + }; + + const Chip = ({ c, theme, children, tone }) => { + const col = tone === 'live' ? c.accent : tone === 'ok' ? c.ok : c.sub; + return ( + + {tone && } + {children} + + ); + }; + + const FieldLabel = ({ c, theme, children, hint }) => ( +
+ {children} + {hint && {hint}} +
+ ); + + const ScoreBar = ({ c, theme, score }) => { + const sc = theme.duo ? c.accent2 : c.accent; + return ( +
+
+
+
+ {score} +
+ ); + }; + + function StudioScreen({ theme, mode, scan = true, onNav, onSetMode }) { + const c = mode === 'dark' ? theme.dark : theme.light; + const D = window.WS_DATA; + const F = theme.fonts; + + // ── interactive state ──────────────────────────────────── + const [activeNav, setActiveNav] = React.useState( + (D.nav.find((n) => n.active) || D.nav[0]).label); + const [aspect, setAspect] = React.useState(D.composer.aspect); + const [version, setVersion] = React.useState(D.focus.version); + const [bgFriendly, setBgFriendly] = React.useState(true); + const [selCard, setSelCard] = React.useState(D.focus.line); + const [textModel, setTextModel] = React.useState(D.status.textModel); + const [imgModel, setImgModel] = React.useState(D.status.model); + const [openDD, setOpenDD] = React.useState(null); + React.useEffect(() => { + if (!openDD) return; + const off = () => setOpenDD(null); + document.addEventListener('pointerdown', off); + return () => document.removeEventListener('pointerdown', off); + }, [openDD]); + + // interaction vars + const hard = theme.id === 'greenhouse' + ? `0 10px 26px ${mode === 'dark' ? 'rgba(0,0,0,.5)' : 'rgba(21,36,27,.16)'}` + : theme.id === 'phosphor' + ? `0 0 0 1.5px ${c.accent}, 0 0 20px ${c.accent}55` + : `5px 5px 0 ${c.accent}`; + + const rootVars = { + '--ws-hover': c.hover, '--ws-accent': c.accent, '--ws-ink': c.ink, + '--ws-hardshadow': hard, + }; + + const border = `${theme.borderW}px solid ${c.line}`; + const hairBorder = `1px solid ${c.hairline}`; + + const displayStyle = { + fontFamily: F.display, fontWeight: theme.displayWeight, + textTransform: theme.displayTransform, letterSpacing: theme.displayTracking, + color: c.ink, margin: 0, lineHeight: 0.98, + }; + + const btn = (variant) => ({ + fontFamily: F.mono, fontSize: 12, fontWeight: 600, letterSpacing: '0.06em', + textTransform: 'uppercase', padding: '11px 16px', borderRadius: theme.radius, + display: 'inline-flex', alignItems: 'center', gap: 7, lineHeight: 1, + border, cursor: 'pointer', + ...(variant === 'primary' + ? { background: c.accent, color: c.onAccent, borderColor: c.accent, + boxShadow: c.glow ? `0 0 16px ${c.accent}66` : 'none' } + : { background: 'transparent', color: c.ink }), + }); + + // ── Model dropdown (text / image generation models) ────── + const ModelDropdown = ({ ddKey, label, value, options, onSelect }) => { + const open = openDD === ddKey; + return ( +
e.stopPropagation()}> + + {open && ( +
+
{label} model
+ {options.map((o) => { + const on = o === value; + return ( + + ); + })} +
+ )} +
+ ); + }; + + // ── Sidebar ────────────────────────────────────────────── + const Sidebar = ( + + ); + + // ── Composer ───────────────────────────────────────────── + const cm = D.composer; + const inputBox = { + width: '100%', background: c.panel, border, borderRadius: theme.radius, + color: c.ink, fontFamily: F.body, fontSize: 13.5, padding: '10px 12px', outline: 'none', + }; + const Composer = ( +
+
+ New design + v1 → generate +
+ +
+
+ Title +
{cm.title}
+
+ +
+ Prompt +
{cm.prompt}
+
+ +
+ Reference images +
+ + +
+ + DROP +
+
+
+ +
+ Aspect ratio +
+ {['Square 1:1', 'Portrait 4:5', 'Landscape 2:1', 'Custom'].map((a, i) => { + const on = a === aspect; + return ( +
setAspect(a)} style={{ + fontFamily: F.mono, fontSize: 10.5, fontWeight: 600, letterSpacing: '0.02em', + textAlign: 'center', padding: '9px 4px', cursor: 'pointer', + border, borderLeft: i === 0 ? border : 'none', + background: on ? c.accent : c.panel, color: on ? c.onAccent : c.sub, + }}>{a.split(' ')[1] || a}
+ ); + })} +
+
+ +
+
+ Format +
+ {cm.format}▾ +
+
+
+ Est. cost +
+ ${cm.estCost} + under cap +
+
+
+ + +
+ +
+ + +
+
+ ); + + // ── Library + focus detail ─────────────────────────────── + const fo = D.focus; + const LibCard = (item) => { + const sel = selCard === item.title; + return ( +
setSelCard(item.title)} + style={{ background: c.panel, + border: sel ? `${theme.borderW}px solid ${c.accent}` : border, + boxShadow: sel ? (theme.id === 'phosphor' ? `0 0 0 1px ${c.accent}` : `4px 4px 0 ${c.accent}`) : 'none', + borderRadius: theme.radius, + padding: 14, display: 'flex', flexDirection: 'column', gap: 11 }}> +
+
+
{item.title}
+
{item.niche}
+
+
+ {item.latest} + +
+
+ +
+ {Array.from({ length: Math.min(item.versions, 4) }).map((_, i) => ( + + ))} +
+ +
+
+ WORKSHOP SCORE + {item.versions} ver · {item.updated} +
+ +
+ +
+ + {item.lineage} + LINEAGE → +
+
+ ); + }; + + const Library = ( +
+
+
+ Design Library + 4 lines · 11 versions +
+
+
+ filter lines +
+ all succeeded +
+
+ +
+
+ {D.library.map((it) => {LibCard(it)})} +
+ + {/* Focused version detail */} +
+
+ +
+ {['v1', 'v2', 'v3'].map((v) => ( +
setVersion(v)} style={{ flex: 1, textAlign: 'center', fontFamily: F.mono, fontSize: 10.5, + fontWeight: 700, padding: '5px 0', border, + background: v === version ? c.accent : 'transparent', + color: v === version ? c.onAccent : c.sub, cursor: 'pointer' }}>{v}
+ ))} +
+
+ +
+
+
+
Selected version
+
{fo.line} · {version}
+
+
+
86
+
/ 100 score
+
+
+ +
{fo.prompt}
+ +
+ {[fo.id, imgModel, fo.quality, fo.size, fo.format, '$' + fo.estCost].map((m) => ( + {m} + ))} + {fo.bgFriendly && bg-removal friendly} +
+ +
+ + + + +
+
+
+
+
+ ); + + // ── Topbar ─────────────────────────────────────────────── + const st = D.status; + const Topbar = ( +
+
+

Design Studio

+
+ Create standalone designs, edit versions, and track each design line.
+
+
+
+ {st.integrations} + + + {window.ModeToggle && } + cap ${st.capUsd} +
+ +
+ ); + + return ( +
+ {Sidebar} +
+ {Topbar} +
+ {Composer} + {Library} +
+
+ + {/* retro CRT texture overlay */} + {theme.texture === 'scan' && scan && ( +
+
+
+ )} +
+ ); + } + + window.StudioScreen = StudioScreen; +})(); diff --git a/assets/design_handoff_b1_workshop/theme.b1.jsx b/assets/design_handoff_b1_workshop/theme.b1.jsx new file mode 100644 index 0000000..564984e --- /dev/null +++ b/assets/design_handoff_b1_workshop/theme.b1.jsx @@ -0,0 +1,154 @@ +// theme.b1.jsx — the single B1 (CONCRETE · AMBER DARK) theme + shared mock +// content for the Workshop handoff. Trimmed from the exploration: this is the +// ONLY theme, no canvas / tweaks / other directions. +// Exports to window: WS_B1, WS_DATA. + +const WS_B1 = { + id: 'concrete', + code: 'B1', + name: 'CONCRETE · AMBER DARK', + tag: 'Single vermilion signal · warm amber-orange darks', + duo: false, + fonts: { + display: '"Archivo", system-ui, sans-serif', + body: '"Archivo", system-ui, sans-serif', + mono: '"JetBrains Mono", ui-monospace, monospace', + }, + displayWeight: 800, + displayTransform: 'uppercase', + displayTracking: '-0.02em', + radius: 0, + borderW: 1.5, + texture: 'none', + // Light palette (bone / clay). The product ships dark by default; light is + // included for completeness. + light: { + bg: '#e9e6dd', panel: '#f7f5ef', panel2: '#efece3', ink: '#16130d', + sub: '#736d60', faint: '#a39c8c', line: '#16130d', hairline: 'rgba(22,19,13,0.14)', + accent: '#e23c0c', accent2: '#16130d', ok: '#3f7a44', warn: '#b8761f', + onAccent: '#f7f5ef', hover: 'rgba(22,19,13,0.06)', thumbBg: '#dedace', + }, + // Dark palette (warm amber-orange near-blacks) — the signature B1 mode. + dark: { + bg: '#1c1108', panel: '#271810', panel2: '#312115', ink: '#f4ece1', + sub: '#a89a84', faint: '#6e624d', line: '#f4ece108', hairline: 'rgba(244,236,225,0.13)', + accent: '#fb5a28', accent2: '#f4ece1', ok: '#6cae66', warn: '#e0a24a', + onAccent: '#1c1108', hover: 'rgba(244,236,225,0.06)', thumbBg: '#382616', + }, +}; + +const WS_DATA = { + app: 'WORKSHOP', + nav: [ + { label: 'Trend Inbox', glyph: 'inbox', badge: '6' }, + { label: 'Research Reports', glyph: 'doc' }, + { label: 'Approval Queue', glyph: 'check', badge: '3' }, + { label: 'Design Studio', glyph: 'image', active: true }, + { label: 'Product Drafts', glyph: 'box' }, + { label: 'Printful Workshop', glyph: 'shirt' }, + { label: 'Etsy Posting', glyph: 'tag' }, + { label: 'Analytics', glyph: 'chart' }, + { label: 'Agent Runs · Logs', glyph: 'pulse' }, + ], + status: { + integrations: 'LIVE', openai: 'real', model: 'gpt-image-2', textModel: 'gpt-5', + quality: 'medium', capUsd: '0.50', etsy: 'mock', printful: 'real', + }, + models: { + text: ['gpt-5', 'gpt-5-mini', 'claude-opus-4.5', 'claude-sonnet-4.5', 'gemini-2.5-pro', 'llama-4-70b'], + image: ['gpt-image-2', 'gpt-image-2-mini', 'dall-e-3', 'flux-1.1-pro', 'imagen-4', 'sd-3.5-large'], + }, + composer: { + title: 'Cyanotype Botanical Study', + prompt: + 'Detailed cyanotype-style botanical print of a wild fennel frond, deep Prussian blue on bone-white, fine herbarium linework, centered, generous negative space. Refer to Reference 1 for leaf structure.', + aspect: 'Portrait 4:5', size: '1024 × 1280', format: 'PNG', bgFriendly: true, refs: 2, estCost: '0.04', + }, + library: [ + { title: 'Cottagecore Mushroom Forager', niche: 'apparel · cottagecore', versions: 3, latest: 'v3', updated: '2h ago', lineage: 'v1 → v2 → v3', score: 86 }, + { title: 'Retro Sunset Trail Runner', niche: 'apparel · outdoors', versions: 2, latest: 'v2', updated: 'yesterday', lineage: 'v1 → v2', score: 78 }, + { title: 'Brutalist Type Specimen Tee', niche: 'apparel · typographic', versions: 4, latest: 'v2·b', updated: '3d ago', lineage: 'Fork 2 of 2 · v1 → v2', score: 71 }, + { title: 'Vintage Seed Packet · Heirloom Tomato', niche: 'apparel · garden', versions: 2, latest: 'v2', updated: '4d ago', lineage: 'v1 → v2', score: 64 }, + ], + focus: { + line: 'Cottagecore Mushroom Forager', version: 'v3', id: 'gen_8f2a91c4', + model: 'gpt-image-2', quality: 'medium', size: '1024 × 1024', format: 'PNG', estCost: '0.04', bgFriendly: true, + prompt: + 'Hand-drawn cottagecore illustration of a fox foraging amanita mushrooms among ferns, muted sage + ochre palette, vintage storybook linework, isolated on cream for background removal.', + }, + + inbox: { + filters: ['All signals', 'Rising', 'Peaking', 'New'], + trends: [ + { id: 't1', title: 'Mushroom foraging core', niche: 'cottagecore · apparel', momentum: 92, + delta: '+34%', state: 'RISING', age: '2h', sources: ['etsy', 'pinterest', 'reddit'], vol: '18.4k/mo', + spark: [3, 4, 5, 6, 8, 9, 12], + summary: 'Forager aesthetic is spiking off late-summer mushroom season; amanita + fern motifs over-index on tees and totes. Hand-drawn linework faces low competition versus stock photography.', + keywords: [['amanita tee', '6.2k', 'low'], ['mushroom forager', '4.1k', 'med'], ['mycology gift', '3.0k', 'low'], ['cottagecore fungi', '2.4k', 'low']] }, + { id: 't2', title: 'Retro national parks', niche: 'outdoors · apparel', momentum: 81, + delta: '+12%', state: 'RISING', age: '5h', sources: ['etsy', 'google'], vol: '22.7k/mo', + spark: [6, 6, 7, 7, 8, 9, 10], + summary: 'Evergreen outdoors demand lifting on early fall travel planning. Crowded category — win on a distinctive limited palette and typographic system rather than generic mountain scenes.', + keywords: [['national park shirt', '9.1k', 'high'], ['hiking tee', '5.4k', 'med'], ['retro camping', '3.2k', 'med'], ['trail runner gift', '1.8k', 'low']] }, + { id: 't3', title: 'Brutalist type specimens', niche: 'typographic · apparel', momentum: 74, + delta: '+8%', state: 'PEAKING', age: 'yesterday', sources: ['pinterest', 'reddit'], vol: '9.3k/mo', + spark: [8, 9, 9, 10, 9, 9, 8], + summary: 'Design-literate buyers responding to stark grotesk lockups and grid systems. Smaller but high-intent audience; strong repeat-purchase and gifting within design community.', + keywords: [['type specimen tee', '3.1k', 'low'], ['helvetica shirt', '2.6k', 'med'], ['brutalist poster', '2.0k', 'low'], ['graphic design gift', '1.6k', 'med']] }, + { id: 't4', title: 'Heirloom seed packets', niche: 'garden · apparel', momentum: 67, + delta: '+5%', state: 'PEAKING', age: '1d', sources: ['etsy'], vol: '7.1k/mo', + spark: [5, 6, 7, 7, 8, 7, 7], + summary: 'Vintage seed-packet illustration carries from spring into fall planting. Nostalgic palette and botanical accuracy drive conversion; pairs well with mug and tote extensions.', + keywords: [['seed packet art', '2.8k', 'low'], ['heirloom tomato', '2.2k', 'low'], ['garden lover gift', '4.0k', 'med'], ['vintage botanical', '3.5k', 'med']] }, + { id: 't5', title: 'Coastal grandmother', niche: 'lifestyle · apparel', momentum: 58, + delta: '-3%', state: 'COOLING', age: '2d', sources: ['pinterest'], vol: '11.2k/mo', + spark: [10, 9, 9, 8, 7, 6, 6], + summary: 'Past peak but still high volume. Enter only with a sharply differentiated take — saturated competition and falling momentum make generic entries unprofitable.', + keywords: [['coastal grandmother', '6.4k', 'high'], ['linen aesthetic', '2.1k', 'med'], ['hamptons style', '1.4k', 'low'], ['beach house decor', '3.3k', 'high']] }, + { id: 't6', title: 'Cyanotype botanicals', niche: 'art · apparel', momentum: 54, + delta: 'NEW', state: 'NEW', age: '3d', sources: ['google'], vol: '3.8k/mo', + spark: [1, 2, 2, 3, 4, 5, 6], + summary: 'Early signal — Prussian-blue herbarium prints emerging from the art-print crowd into apparel. Untapped on Etsy; first-mover window open for a clean linework series.', + keywords: [['cyanotype print', '2.4k', 'low'], ['botanical blue', '1.1k', 'low'], ['herbarium art', '0.9k', 'low'], ['blueprint flower', '0.7k', 'low']] }, + ], + }, + + reports: { + list: [ + { id: 'r1', title: 'Cottagecore mushroom apparel', niche: 'cottagecore', status: 'FINAL', score: 86, date: 'Jun 14', secs: 5 }, + { id: 'r2', title: 'Retro national parks revival', niche: 'outdoors', status: 'FINAL', score: 78, date: 'Jun 12', secs: 5 }, + { id: 'r3', title: 'Brutalist type specimen tees', niche: 'typographic', status: 'DRAFT', score: 71, date: 'Jun 11', secs: 4 }, + { id: 'r4', title: 'Heirloom seed packet prints', niche: 'garden', status: 'DRAFT', score: 64, date: 'Jun 09', secs: 5 }, + ], + focus: { + title: 'Cottagecore mushroom apparel', niche: 'cottagecore · apparel', status: 'FINAL', + score: 86, date: 'Jun 14, 2026', model: 'gpt-5', words: '1,240', + toc: ['Market summary', 'Competitive landscape', 'Keyword opportunities', 'Recommended products', 'Risks & cautions'], + summary: 'Demand for hand-drawn mushroom and forager motifs is rising sharply into late summer, with strong cross-over between cottagecore and mycology-hobbyist audiences. Competition is concentrated on photographic prints, leaving a clear opening for original linework in a tight, limited palette.', + competition: '37 established shops rank for the core terms; top listings lean on stock photography and generic clip-art. Differentiation through original illustration and a 2–3 colour palette is achievable within a single design cycle.', + keywords: [['amanita tee', '6.2k', 'low', '$0.41'], ['mushroom forager', '4.1k', 'med', '$0.58'], ['mycology gift', '3.0k', 'low', '$0.36'], ['cottagecore fungi', '2.4k', 'low', '$0.33'], ['forager shirt', '1.9k', 'med', '$0.49']], + products: ['Unisex heavyweight tee', 'Organic cotton tote', 'Ceramic mug', 'Die-cut sticker sheet'], + risks: 'Seasonal demand likely tapers after September — front-load launches. Frame amanita as decorative only; avoid foraging or edibility claims in listing copy.', + }, + }, + + etsy: { + stages: [ + { name: 'Draft', tone: 'sub', items: [ { id: 'e1', title: 'Amanita Forager Tee', price: '28.00' }, { id: 'e2', title: 'Mycology Society Tote', price: '22.00' } ] }, + { name: 'In review', tone: 'warn', items: [ { id: 'e3', title: 'Cottagecore Fox & Ferns', price: '26.00' } ] }, + { name: 'Scheduled', tone: 'accent', items: [ { id: 'e4', title: 'Heirloom Tomato Packet', price: '24.00' } ] }, + { name: 'Live', tone: 'ok', items: [ { id: 'e5', title: 'Retro Trail Runner', price: '29.00' }, { id: 'e6', title: 'Brutalist Specimen Tee', price: '27.00' } ] }, + ], + focus: { + title: 'Amanita Forager Heavyweight Tee', stage: 'Draft', status: 'mock', price: '28.00', qty: 25, + category: 'Clothing › Tops & Tees › T-shirts', section: 'Cottagecore Apparel', + whoMade: 'I did', whenMade: 'Made to order', renewal: 'Automatic', images: 4, + materials: ['Ringspun cotton', 'Water-based ink', 'DTG print'], + tags: ['amanita tee', 'mushroom forager', 'cottagecore', 'mycology gift', 'foraging shirt', 'botanical tee', 'dark academia', 'goblincore', 'witchy apparel', 'fungi art', 'nature lover'], + tagCap: 13, + summary: 'Hand-drawn amanita muscaria with fern detailing, screen-print look on a heavyweight unisex tee. Background-removed art on a bone-white blank.', + }, + }, +}; + +Object.assign(window, { WS_B1, WS_DATA }); diff --git a/assets/design_handoff_b1_workshop/wschrome.jsx b/assets/design_handoff_b1_workshop/wschrome.jsx new file mode 100644 index 0000000..a4112b7 --- /dev/null +++ b/assets/design_handoff_b1_workshop/wschrome.jsx @@ -0,0 +1,291 @@ +// wschrome.jsx — shared Workshop app shell (B1 and any theme). +// Sidebar + topbar frame + interactive model dropdowns + brutalist +// primitives, so each workflow screen only writes its own main content. +// Reuses the .wsx-* interaction CSS injected by studio.jsx. +// Exports to window: WSChrome, WSIcon, WSChip, WSField, WSScore, WSThumb, +// wsBtn, wsBorder, wsDisplay. + +(function () { + const ICONS = { + inbox: 'M3 13h4l1 2h4l1-2h4M4 13l2-8h8l2 8', + doc: 'M5 2h7l3 3v11H5zM12 2v3h3', + check: 'M4 9l3 3 6-7', + image: 'M3 4h14v12H3zM3 13l4-4 3 3 3-4 4 5', + box: 'M3 6l7-3 7 3-7 3zM3 6v8l7 3 7-3V6M10 9v8', + shirt: 'M6 3l4 2 4-2 3 3-2 2v8H5V8L3 6z', + tag: 'M3 3h6l8 8-6 6-8-8zM6.5 6.5h.01', + chart: 'M3 16V4M3 16h13M7 13V9M11 13V6M15 13v-3', + pulse: 'M3 10h3l2-5 3 10 2-5h3', + plus: 'M10 4v12M4 10h12', + search: 'M9 3a6 6 0 100 12A6 6 0 009 3zM14 14l3 3', + bolt: 'M11 2L4 11h5l-1 7 7-9h-5z', + eye: 'M2 10s3-5 8-5 8 5 8 5-3 5-8 5-8-5-8-5z M10 8a2 2 0 100 4 2 2 0 000-4z', + down: 'M10 3v10M5 9l5 5 5-5M4 17h12', + branch: 'M6 4v8a3 3 0 003 3h2M6 4a2 2 0 100-.1zM14 4a2 2 0 100-.1zM12 15a2 2 0 100 .1z', + trash: 'M4 5h12M8 5V3h4v2M6 5l1 12h6l1-12', + arrow: 'M4 10h12M11 5l5 5-5 5', + clock: 'M10 5v5l3 2M10 3a7 7 0 100 14 7 7 0 000-14z', + flame: 'M10 2c1 3 4 4 4 8a4 4 0 11-8 0c0-2 1-3 2-4 0 2 1 2 2 1 0-2-1-4-0-6z', + cal: 'M3 5h14v12H3zM3 8h14M7 3v3M13 3v3', + star: 'M10 2l2.4 5 5.6.6-4 3.8 1 5.6L10 14.8 4 17l1-5.6-4-3.8 5.6-.6z', + sliders: 'M4 6h8M14 6h2M4 14h2M8 14h8M11 4v4M6 12v4', + ext: 'M8 4H4v12h12v-4M12 4h4v4M16 4l-7 7', + edit: 'M4 13l9-9 3 3-9 9H4z', + x: 'M5 5l10 10M15 5L5 15', + layers: 'M10 3l7 4-7 4-7-4zM3 11l7 4 7-4', + dollar: 'M10 3v14M13 6c0-1.5-1.5-2-3-2s-3 .5-3 2 1.5 2 3 2 3 .5 3 2-1.5 2-3 2-3-.5-3-2', + }; + + const WSIcon = ({ name, size = 16, sw = 1.6 }) => ( + + + + ); + + // Light/dark toggle — single button, sized to match the TXT/IMG dropdowns. + const ModeToggle = ({ c, theme, F, mode, onSetMode }) => { + const next = mode === 'dark' ? 'light' : 'dark'; + return ( + + ); + }; + + const wsBorder = (theme, c) => `${theme.borderW}px solid ${c.line}`; + const wsDisplay = (theme, c) => ({ + fontFamily: theme.fonts.display, fontWeight: theme.displayWeight, + textTransform: theme.displayTransform, letterSpacing: theme.displayTracking, + color: c.ink, margin: 0, lineHeight: 0.98, + }); + const wsBtn = (theme, c, variant) => ({ + fontFamily: theme.fonts.mono, fontSize: 12, fontWeight: 600, letterSpacing: '0.06em', + textTransform: 'uppercase', padding: '11px 16px', borderRadius: theme.radius, + display: 'inline-flex', alignItems: 'center', gap: 7, lineHeight: 1, + border: wsBorder(theme, c), cursor: 'pointer', + ...(variant === 'primary' + ? { background: c.accent, color: c.onAccent, borderColor: c.accent, + boxShadow: c.glow ? `0 0 16px ${c.accent}66` : 'none' } + : { background: 'transparent', color: c.ink }), + }); + + const WSChip = ({ c, theme, children, tone }) => { + const col = tone === 'live' || tone === 'accent' ? c.accent : tone === 'ok' ? c.ok + : tone === 'warn' ? c.warn : c.sub; + return ( + + {tone && tone !== 'plain' && } + {children} + + ); + }; + + const WSField = ({ c, theme, children, hint }) => ( +
+ {children} + {hint && {hint}} +
+ ); + + const WSScore = ({ c, theme, score, w = 80 }) => ( +
+
+
+
+ {score} +
+ ); + + const WSThumb = ({ c, theme, label, sub, ratio = 1, big = false }) => { + const bg = `repeating-linear-gradient(45deg, ${c.ink}14 0 2px, transparent 2px 9px)`; + return ( +
+
+ {sub &&
{sub}
} + {label &&
{label}
} +
+ ); + }; + + // ── The shell ──────────────────────────────────────────────────────────── + function WSChrome({ theme, mode, scan = true, active, title, subtitle, primary, children, onNav, onSetMode }) { + const c = mode === 'dark' ? theme.dark : theme.light; + const D = window.WS_DATA; + const F = theme.fonts; + const border = wsBorder(theme, c); + const hairBorder = `1px solid ${c.hairline}`; + const display = wsDisplay(theme, c); + + const [activeNav, setActiveNav] = React.useState(active); + const [textModel, setTextModel] = React.useState(D.status.textModel); + const [imgModel, setImgModel] = React.useState(D.status.model); + const [openDD, setOpenDD] = React.useState(null); + React.useEffect(() => { + if (!openDD) return; + const off = () => setOpenDD(null); + document.addEventListener('pointerdown', off); + return () => document.removeEventListener('pointerdown', off); + }, [openDD]); + + const hard = theme.id === 'phosphor' ? `0 0 0 1.5px ${c.accent}, 0 0 20px ${c.accent}55` + : `5px 5px 0 ${c.accent}`; + const rootVars = { '--ws-hover': c.hover, '--ws-accent': c.accent, '--ws-ink': c.ink, '--ws-hardshadow': hard }; + + const ModelDropdown = ({ ddKey, label, value, options, onSelect }) => { + const open = openDD === ddKey; + return ( +
e.stopPropagation()}> + + {open && ( +
+
{label} model
+ {options.map((o) => { + const on = o === value; + return ( + + ); + })} +
+ )} +
+ ); + }; + + const Sidebar = ( + + ); + + const st = D.status; + const Topbar = ( +
+
+

{title}

+ {subtitle &&
{subtitle}
} +
+
+
+ {st.integrations} + + + + cap ${st.capUsd} +
+ {primary && ( + + )} +
+ ); + + return ( +
+ {Sidebar} +
+ {Topbar} +
{children}
+
+
+ ); + } + + Object.assign(window, { WSChrome, WSIcon, WSChip, WSField, WSScore, WSThumb, wsBtn, wsBorder, wsDisplay, ModeToggle }); +})(); diff --git a/docker-compose.yml b/docker-compose.yml index 28510d9..484766f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -35,21 +35,7 @@ services: volumes: - generated_assets:/app/assets/generated restart: unless-stopped - - dashboard: - build: . - env_file: .env - command: > - streamlit run src/ecommerce_agent/dashboard/app.py - --server.address=0.0.0.0 - --server.port=8501 - --server.headless=true - depends_on: - api: - condition: service_started - ports: - - "127.0.0.1:8501:8501" - restart: unless-stopped + # The B1 web UI (Jinja + HTMX) is served by this same service at http://localhost:8000/ volumes: postgres_data: diff --git a/pyproject.toml b/pyproject.toml index fb69a5c..ec76548 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,11 +15,11 @@ dependencies = [ "cryptography>=44,<46", "fastapi>=0.115,<1", "httpx>=0.28,<1", + "jinja2>=3,<4", "pillow>=11,<13", "pydantic-settings>=2.7,<3", "python-multipart>=0.0.20,<1", "sqlalchemy[asyncio]>=2.0.36,<3", - "streamlit>=1.41,<2", "uvicorn[standard]>=0.34,<1", ] @@ -51,11 +51,6 @@ markers = [ python_version = "3.12" strict = true -[[tool.mypy.overrides]] -module = "ecommerce_agent.dashboard.*" -disallow_untyped_defs = false -disallow_untyped_calls = false - [tool.ruff] line-length = 100 target-version = "py312" diff --git a/src/ecommerce_agent/api/main.py b/src/ecommerce_agent/api/main.py index 064ced1..e42d58c 100644 --- a/src/ecommerce_agent/api/main.py +++ b/src/ecommerce_agent/api/main.py @@ -26,6 +26,8 @@ from ecommerce_agent.services.openai_images import OpenAIImageError from ecommerce_agent.services.printful import PrintfulError from ecommerce_agent.services.research import ResearchProviderError +from ecommerce_agent.web import router as web_router +from ecommerce_agent.web.routes import _TEMPLATES_DIR def create_app(settings: Settings | None = None) -> FastAPI: @@ -78,6 +80,13 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]: app.include_router(research_runs_router, prefix="/api") app.include_router(research_router, prefix="/api") + app.mount( + "/static", + StaticFiles(directory=str(_TEMPLATES_DIR.parent / "static")), + name="static", + ) + app.include_router(web_router) + @app.exception_handler(NotFoundError) async def not_found_handler(request: Request, error: NotFoundError) -> JSONResponse: return JSONResponse(status_code=404, content={"detail": str(error)}) diff --git a/src/ecommerce_agent/dashboard/__init__.py b/src/ecommerce_agent/dashboard/__init__.py deleted file mode 100644 index 5a302f9..0000000 --- a/src/ecommerce_agent/dashboard/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Streamlit admin dashboard.""" diff --git a/src/ecommerce_agent/dashboard/app.py b/src/ecommerce_agent/dashboard/app.py deleted file mode 100644 index c978186..0000000 --- a/src/ecommerce_agent/dashboard/app.py +++ /dev/null @@ -1,32 +0,0 @@ -import streamlit as st - -from ecommerce_agent.dashboard.client import ApiError, get, post -from ecommerce_agent.dashboard.ui import page_header - -page_header( - "Ecommerce Agent", - "A human-approved workflow for Etsy and Printful product creation.", -) - -st.markdown( - """ -Use the pages in the sidebar to review research, approve designs and copy, -inspect product drafts, curate Printful products, and monitor published listings. -Each provider can be enabled independently behind explicit integration switches. -""" -) - -col1, col2 = st.columns(2) -with col1: - if st.button("Start trend research", type="primary"): - try: - run = post("/trend-discovery-runs") - st.success(f"Trend discovery {run['id']} was queued.") - except ApiError as error: - st.error(str(error)) -with col2: - try: - health = get("/health") - st.metric("API", health["status"]) - except ApiError: - st.metric("API", "unavailable") diff --git a/src/ecommerce_agent/dashboard/client.py b/src/ecommerce_agent/dashboard/client.py deleted file mode 100644 index a5798de..0000000 --- a/src/ecommerce_agent/dashboard/client.py +++ /dev/null @@ -1,87 +0,0 @@ -import os -from typing import Any - -import httpx - -API_BASE_URL = os.getenv("API_BASE_URL", "http://localhost:8000").rstrip("/") - - -class ApiError(RuntimeError): - pass - - -def _error_detail(response: httpx.Response) -> str: - try: - payload = response.json() - except (TypeError, ValueError): - payload = None - if isinstance(payload, dict) and payload.get("detail"): - return str(payload["detail"]) - text = response.text.strip() - if text and not text.lstrip().startswith("<"): - return text - return f"API request failed with status {response.status_code}." - - -def request(method: str, path: str, **kwargs: Any) -> Any: - timeout = kwargs.pop("timeout", 20) - try: - response = httpx.request( - method, - f"{API_BASE_URL}/api{path}", - timeout=timeout, - **kwargs, - ) - response.raise_for_status() - except httpx.HTTPStatusError as error: - raise ApiError(_error_detail(error.response)) from error - except httpx.HTTPError as error: - raise ApiError(f"API is unavailable: {error}") from error - return None if response.status_code == 204 else response.json() - - -def get(path: str) -> Any: - return request("GET", path) - - -def get_text(path: str) -> str: - try: - response = httpx.get(f"{API_BASE_URL}/api{path}", timeout=20) - response.raise_for_status() - except httpx.HTTPStatusError as error: - raise ApiError(_error_detail(error.response)) from error - except httpx.HTTPError as error: - raise ApiError(f"API is unavailable: {error}") from error - return response.text - - -def post(path: str, payload: dict[str, Any] | None = None, *, timeout: float = 20) -> Any: - return request("POST", path, json=payload or {}, timeout=timeout) - - -def patch(path: str, payload: dict[str, Any], *, timeout: float = 20) -> Any: - return request("PATCH", path, json=payload, timeout=timeout) - - -def post_multipart( - path: str, - *, - data: dict[str, str], - files: list[tuple[str, tuple[str, bytes, str]]] | None = None, - timeout: float = 20, -) -> Any: - return request("POST", path, data=data, files=files or [], timeout=timeout) - - -def delete(path: str) -> None: - request("DELETE", path) - - -def get_asset(uri: str) -> bytes: - url = uri if uri.startswith(("http://", "https://")) else f"{API_BASE_URL}{uri}" - try: - response = httpx.get(url, timeout=20) - response.raise_for_status() - except httpx.HTTPError as error: - raise ApiError(f"Generated asset is unavailable: {error}") from error - return response.content diff --git a/src/ecommerce_agent/dashboard/pages/1_Trend_Inbox.py b/src/ecommerce_agent/dashboard/pages/1_Trend_Inbox.py deleted file mode 100644 index f342b62..0000000 --- a/src/ecommerce_agent/dashboard/pages/1_Trend_Inbox.py +++ /dev/null @@ -1,98 +0,0 @@ -import streamlit as st - -from ecommerce_agent.dashboard.client import get, post -from ecommerce_agent.dashboard.ui import page_header, show_error - -page_header( - "Trend Inbox", - "Discover emerging apparel themes, inspect the evidence, and start deeper research.", -) - -try: - sources = get("/research-sources/status") - with st.expander("Research source status"): - columns = st.columns(5) - columns[0].metric( - "Web research", - "Live" if sources["web_research"]["enabled"] else "Mock", - ) - columns[1].metric( - "Etsy API", - ( - "Connected" - if sources["etsy_api"]["connected"] - else ("Configured" if sources["etsy_api"]["enabled"] else "Mock") - ), - ) - columns[2].metric("Etsy Stats imports", sources["etsy_stats"]["import_count"]) - columns[3].metric( - "GA4", - "Connected" if sources["google_analytics"]["connected"] else "Optional", - ) - columns[4].metric( - "Printful products", - sources["printful_catalog"]["cached_product_count"], - ) - st.caption( - "Google Trends is not enabled because its official API remains limited-access alpha." - ) - - action, refresh = st.columns([1, 1]) - with action: - if st.button("Run trend research", type="primary"): - run = post("/trend-discovery-runs") - st.success(f"Trend discovery {run['id']} was queued.") - st.rerun() - with refresh: - st.button("Refresh") - - runs = get("/trend-discovery-runs") - if runs: - latest = runs[0] - status = latest["status"].replace("_", " ").title() - st.caption( - f"Latest discovery: {status} · {latest['trend_count']} trend(s) · " - f"{latest['created_at']}" - ) - if latest.get("error_message"): - st.error(latest["error_message"]) - - trends = get("/trends") - discovered = [trend for trend in trends if trend["status"] == "discovered"] - if not discovered: - st.info("Run trend research to populate the inbox.") - for trend in discovered: - with st.container(border=True): - heading, score, confidence = st.columns([5, 1, 1]) - heading.subheader(trend["title"]) - heading.caption(trend["niche"]) - score.metric( - "Apparel", - f"{float(trend['apparel_score']):.1f}/5" - if trend.get("apparel_score") is not None - else "—", - ) - confidence.metric( - "Confidence", - f"{float(trend['confidence']):.0%}" if trend.get("confidence") is not None else "—", - ) - st.write(trend.get("blurb") or "") - if trend.get("evidence"): - st.markdown("**Evidence**") - for evidence in trend["evidence"]: - st.markdown(f"- {evidence}") - if trend.get("risk_warnings"): - st.warning("\n\n".join(trend["risk_warnings"])) - sources_for_trend = trend.get("verified_sources") or [] - if sources_for_trend: - st.markdown( - "**Sources:** " - + " · ".join( - f"[{source['title']}]({source['url']})" for source in sources_for_trend - ) - ) - if st.button("Research", key=f"research-{trend['id']}"): - st.query_params["trend_id"] = trend["id"] - st.switch_page("pages/2_Research_Reports.py") -except Exception as error: - show_error(error) diff --git a/src/ecommerce_agent/dashboard/pages/2_Research_Reports.py b/src/ecommerce_agent/dashboard/pages/2_Research_Reports.py deleted file mode 100644 index 4473881..0000000 --- a/src/ecommerce_agent/dashboard/pages/2_Research_Reports.py +++ /dev/null @@ -1,189 +0,0 @@ -import streamlit as st - -from ecommerce_agent.dashboard.client import ( - API_BASE_URL, - get, - get_text, - post, - post_multipart, -) -from ecommerce_agent.dashboard.ui import page_header, show_error - -page_header( - "Research Reports", - "Run in-depth topic research and review consistent, downloadable Markdown reports.", -) - -try: - trends = get("/trends") - trend_id = st.query_params.get("trend_id") - selected_trend = next( - (trend for trend in trends if trend["id"] == trend_id), - None, - ) - - with st.expander( - "Start research" + (f": {selected_trend['title']}" if selected_trend else ""), - expanded=bool(selected_trend), - ): - with st.form("research-topic-form"): - topic_title = st.text_input( - "Topic title", - value=selected_trend["title"] if selected_trend else "", - ) - default_context = "" - if selected_trend: - default_context = ( - f"Trend summary: {selected_trend.get('blurb', '')}\n\n" - f"Niche: {selected_trend.get('niche', '')}\n\n" - "Please validate the current momentum and focus on wearable, differentiated " - "ideas suitable for hats, shirts, and hoodies." - ) - additional_context = st.text_area( - "Additional context", - value=default_context, - height=180, - help="Optional guidance about audience, style, products, exclusions, or timing.", - ) - if st.form_submit_button("Submit", type="primary"): - run = post( - "/research-runs", - { - "topic_title": topic_title, - "additional_context": additional_context, - "trend_id": selected_trend["id"] if selected_trend else None, - }, - ) - st.query_params.clear() - st.query_params["run_id"] = run["id"] - st.success("Research run queued.") - st.rerun() - - with st.expander("Etsy Stats search-term import"): - st.markdown( - """ -Etsy keeps shopper search terms inside Shop Manager rather than its current public API. - -1. Open **Etsy.com → Shop Manager → Stats**. -2. Choose the date range you want to analyze. -3. Under **How shoppers found you**, select **Etsy search**. -4. Copy each visible search term and its visit count into the Workshop CSV template. -5. Add a listing ID or title when you know which listing the term belongs to, then upload it here. - -Use the same date range on every row from one export. Workshop keeps import history and rejects -an identical file if it has already been uploaded. -""" - ) - template = get_text("/research-sources/etsy-stats-imports/template") - st.download_button( - "Download Etsy Stats CSV template", - data=template, - file_name="workshop-etsy-stats-template.csv", - mime="text/csv", - ) - upload = st.file_uploader("Upload completed CSV", type=["csv"]) - if upload is not None and st.button("Import Etsy Stats"): - result = post_multipart( - "/research-sources/etsy-stats-imports", - data={}, - files=[ - ( - "upload", - ( - upload.name, - upload.getvalue(), - "text/csv", - ), - ) - ], - ) - st.success(f"Imported {result['row_count']} Etsy search-term rows.") - if result["warnings"]: - st.warning("\n".join(result["warnings"])) - imports = get("/research-sources/etsy-stats-imports") - if imports: - st.dataframe(imports, use_container_width=True, hide_index=True) - - with st.expander("Google Analytics 4"): - source_status = get("/research-sources/status")["google_analytics"] - if not source_status["configured"]: - st.info( - "Set GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, and " - "GOOGLE_TOKEN_ENCRYPTION_KEY to enable GA4." - ) - elif not source_status["connected"]: - st.link_button( - "Connect Google Analytics", - f"{API_BASE_URL}/api/research-sources/google-analytics/oauth/start", - ) - else: - st.success(f"Connected as {source_status['account_id']}.") - properties = get("/research-sources/google-analytics/properties") - if properties: - labels = { - f"{item['display_name']} · {item['property_id']}": item for item in properties - } - selected_label = st.selectbox("GA4 property", list(labels)) - selected_property = labels[selected_label] - if st.button("Use this GA4 property"): - post( - "/research-sources/google-analytics/property", - selected_property, - ) - st.success("GA4 property saved.") - st.rerun() - else: - st.warning("No accessible GA4 properties were returned.") - if st.button("Disconnect Google Analytics"): - post("/research-sources/google-analytics/disconnect") - st.rerun() - - st.divider() - heading, refresh = st.columns([4, 1]) - heading.subheader("Research history") - refresh.button("Refresh") - runs = get("/research-runs") - if not runs: - st.info("No research runs yet.") - else: - run_labels = { - f"{run['topic_title']} · {run['status'].replace('_', ' ')} · {run['created_at']}": run - for run in runs - } - requested_run_id = st.query_params.get("run_id") - default_index = next( - ( - index - for index, run in enumerate(run_labels.values()) - if run["id"] == requested_run_id - ), - 0, - ) - selected_label = st.selectbox( - "Research run", - list(run_labels), - index=default_index, - ) - selected_run = get(f"/research-runs/{run_labels[selected_label]['id']}") - status_value = selected_run["status"].replace("_", " ").title() - st.caption( - f"Status: {status_value} · Created {selected_run['created_at']} · " - f"Run `{selected_run['id']}`" - ) - if selected_run.get("error_message"): - st.error(selected_run["error_message"]) - if st.button("Retry research run"): - post(f"/research-runs/{selected_run['id']}/retry") - st.rerun() - elif selected_run["status"] in {"queued", "running"}: - st.info("Research is running in the background. Use Refresh to check progress.") - elif selected_run.get("markdown_report"): - st.download_button( - "Download Markdown", - data=selected_run["markdown_report"], - file_name=f"{selected_run['topic_title']}.md", - mime="text/markdown", - ) - st.markdown(selected_run["markdown_report"]) -except Exception as error: - show_error(error) diff --git a/src/ecommerce_agent/dashboard/pages/4_Design_Studio.py b/src/ecommerce_agent/dashboard/pages/4_Design_Studio.py deleted file mode 100644 index 9bd4526..0000000 --- a/src/ecommerce_agent/dashboard/pages/4_Design_Studio.py +++ /dev/null @@ -1,647 +0,0 @@ -from math import gcd -from typing import Any, Literal - -import streamlit as st - -from ecommerce_agent.dashboard.client import delete, get, get_asset, patch, post_multipart -from ecommerce_agent.dashboard.design_lineage import branch_label, lineage_paths, version_label -from ecommerce_agent.dashboard.ui import page_header, show_error -from ecommerce_agent.domain.dtos import ImageGenerationRequest, ImageReference -from ecommerce_agent.services.openai_images import build_effective_prompt - -OUTPUT_FORMATS = { - "PNG": "png", - "JPEG": "jpeg", - "WebP": "webp", -} -OUTPUT_DOWNLOADS = { - "png": ("png", "image/png"), - "jpeg": ("jpg", "image/jpeg"), - "webp": ("webp", "image/webp"), -} -ASPECT_OPTIONS = ("Square 1:1", "Portrait 4:5", "Landscape 2:1", "Custom") -EDIT_ASPECT_OPTIONS = ("Same as source", *ASPECT_OPTIONS) - -page_header( - "Design Studio", - "Create standalone image designs, edit versions, and track each design line.", -) - -DEFAULT_STATE = { - "design_title": "Untitled design", - "design_prompt": "", - "design_output_format": "PNG", - "design_background_friendly": True, - "design_aspect": "Square 1:1", - "design_custom_width": 4, - "design_custom_height": 5, - "focused_generation_id": None, - "focused_lineage_leaf_id": None, - "editing_generation_id": None, - "deleting_generation_id": None, - "deleting_lineage_leaf_id": None, -} -for key, value in DEFAULT_STATE.items(): - if key not in st.session_state: - st.session_state[key] = value - - -def reuse_generation(generation: dict[str, Any]) -> None: - st.session_state.design_title = generation.get("title") or "Untitled design" - st.session_state.design_prompt = generation["prompt"] - st.session_state.design_output_format = _format_label(generation["output_format"]) - st.session_state.design_background_friendly = generation["background_removal_friendly"] - st.session_state.design_aspect = _aspect_label(generation["width"], generation["height"]) - st.session_state.design_custom_width = generation["width"] - st.session_state.design_custom_height = generation["height"] - - -def start_edit(generation_id: str) -> None: - st.session_state.editing_generation_id = generation_id - - -def cancel_edit() -> None: - st.session_state.editing_generation_id = None - - -def start_delete(generation_id: str) -> None: - st.session_state.deleting_generation_id = generation_id - - -def cancel_delete() -> None: - st.session_state.deleting_generation_id = None - - -def confirm_delete(generation_id: str) -> None: - delete(f"/image-generations/{generation_id}") - if st.session_state.editing_generation_id == generation_id: - st.session_state.editing_generation_id = None - st.session_state.deleting_generation_id = None - - -def start_delete_lineage(leaf_id: str) -> None: - st.session_state.deleting_lineage_leaf_id = leaf_id - - -def cancel_delete_lineage() -> None: - st.session_state.deleting_lineage_leaf_id = None - - -def confirm_delete_lineage(path: list[dict[str, Any]]) -> None: - for generation in reversed(path): - delete(f"/image-generations/{generation['id']}") - if st.session_state.editing_generation_id in {item["id"] for item in path}: - st.session_state.editing_generation_id = None - st.session_state.deleting_lineage_leaf_id = None - - -def upload_files(uploaded: list[Any]) -> list[tuple[str, tuple[str, bytes, str]]]: - return [ - ( - "reference_images", - ( - item.name, - item.getvalue(), - item.type or "application/octet-stream", - ), - ) - for item in uploaded - ] - - -def show_upload_previews(uploaded: list[Any]) -> None: - if not uploaded: - return - st.caption("Refer to these in your prompt as Reference 1, Reference 2, and so on.") - columns = st.columns(min(len(uploaded), 4)) - for index, item in enumerate(uploaded): - with columns[index % len(columns)]: - st.image(item.getvalue(), caption=f"Reference {index + 1}", width=140) - - -def show_generated_image( - asset_uri: str, *, width: Literal["stretch", "content"] | int = "stretch" -) -> bytes: - content = get_asset(asset_uri) - st.image(content, width=width) - return content - - -def line_title(path: list[dict[str, Any]]) -> str: - leaf = path[-1] - return str(leaf.get("title") or "Untitled design") - - -def save_line_title(path: list[dict[str, Any]], title: str) -> None: - clean_title = title.strip() or "Untitled design" - patch(f"/image-generations/{path[-1]['id']}", {"title": clean_title}) - - -def aspect_dimensions( - choice: str, - custom_width: int, - custom_height: int, - *, - source: dict[str, Any] | None = None, -) -> tuple[int, int]: - if choice == "Same as source" and source is not None: - return int(source["width"]), int(source["height"]) - if choice == "Square 1:1": - return 1024, 1024 - if choice == "Portrait 4:5": - return 1024, 1280 - if choice == "Landscape 2:1": - return 1536, 768 - ratio = max(custom_width, 1) / max(custom_height, 1) - if ratio >= 1: - width = 1536 - height = int(round(width / ratio)) - else: - height = 1536 - width = int(round(height * ratio)) - return _round_dimension(width), _round_dimension(height) - - -def _round_dimension(value: int) -> int: - return max(64, min(4096, round(value / 64) * 64)) - - -def _aspect_label(width: int, height: int) -> str: - ratio = round(width / height, 2) - if ratio == 1: - return "Square 1:1" - if ratio == 0.8: - return "Portrait 4:5" - if ratio == 2: - return "Landscape 2:1" - return "Custom" - - -def ratio_parts(width: int, height: int) -> tuple[int, int]: - divisor = gcd(width, height) - return min(width // divisor, 20), min(height // divisor, 20) - - -def prompt_preview( - *, - prompt: str, - width: int, - height: int, - output_format: str, - background_removal_friendly: bool, - reference_count: int, -) -> str: - references = tuple( - ImageReference(f"reference-{index}.png", "image/png", b"") - for index in range(1, reference_count + 1) - ) - effective_prompt = build_effective_prompt( - ImageGenerationRequest( - prompt=prompt, - width=width, - height=height, - references=references, - output_format=output_format, - background_removal_friendly=background_removal_friendly, - product_concept_id="preview", - ) - ) - return ( - f"size: {width}x{height}\n" - f"output_format: {output_format}\n" - f"reference_images: {reference_count}\n\n" - f"prompt:\n{effective_prompt}" - ) - - -def _format_label(output_format: str) -> str: - return {"png": "PNG", "jpeg": "JPEG", "webp": "WebP"}[output_format] - - -def show_details(generation: dict[str, Any]) -> None: - st.caption( - f"{generation['id']} · {generation['model']} · {generation['quality']} · " - f"{generation['width']}x{generation['height']} · " - f"{generation['output_format'].upper()} · " - f"estimated ${generation['estimated_cost_usd']}" - ) - if generation["background_removal_friendly"]: - st.caption("Background-removal friendly") - if generation.get("usage"): - st.json(generation["usage"], expanded=False) - if generation.get("error_message"): - st.error(generation["error_message"]) - - -def thumbnail_rows( - path: list[dict[str, Any]], *, key_prefix: str, open_dialog: bool = True -) -> None: - for row_start in range(0, len(path), 4): - row = path[row_start : row_start + 4] - widths = [1] * len(row) - if len(row) < 4: - widths.append(4 - len(row)) - columns = st.columns(widths, gap="small") - for column, generation in zip(columns, row, strict=False): - with column: - if generation.get("asset_uri"): - show_generated_image(generation["asset_uri"], width=120) - else: - st.caption(generation["status"]) - label = version_label(path, generation) - st.caption(label) - if open_dialog: - if st.button("Open", key=f"{key_prefix}-open-{generation['id']}"): - version_dialog(path, generation) - elif st.button("Select", key=f"{key_prefix}-select-{generation['id']}"): - st.session_state.focused_generation_id = generation["id"] - - -def version_actions( - path: list[dict[str, Any]], generation: dict[str, Any], *, key_prefix: str -) -> None: - extension, content_type = OUTPUT_DOWNLOADS[generation["output_format"]] - image_content = get_asset(generation["asset_uri"]) if generation.get("asset_uri") else b"" - actions = st.columns(4) - with actions[0]: - if st.button( - f"Edit from {version_label(path, generation)}", - key=f"{key_prefix}-edit-{generation['id']}", - disabled=generation["status"] != "succeeded" or not generation.get("asset_uri"), - ): - start_edit(generation["id"]) - st.rerun() - with actions[1]: - if st.button("Reuse prompt", key=f"{key_prefix}-reuse-{generation['id']}"): - reuse_generation(generation) - st.success("Prompt copied to New design.") - with actions[2]: - st.download_button( - "Download", - data=image_content, - file_name=( - f"{line_title(path).lower().replace(' ', '-')}-" - f"{version_label(path, generation)}.{extension}" - ), - mime=content_type, - key=f"{key_prefix}-download-{generation['id']}", - disabled=not bool(image_content), - on_click="ignore", - ) - with actions[3]: - if st.button("Delete", key=f"{key_prefix}-delete-{generation['id']}"): - start_delete(generation["id"]) - - if st.session_state.deleting_generation_id == generation["id"]: - st.warning("Delete this version? Edited descendants will be preserved.") - confirm, cancel = st.columns(2) - with confirm: - if st.button( - "Confirm delete", - key=f"{key_prefix}-confirm-delete-{generation['id']}", - type="primary", - ): - confirm_delete(generation["id"]) - st.rerun() - with cancel: - if st.button("Cancel", key=f"{key_prefix}-cancel-delete-{generation['id']}"): - cancel_delete() - st.rerun() - - -@st.dialog("Design version", width="large") -def version_dialog(path: list[dict[str, Any]], generation: dict[str, Any]) -> None: - title = line_title(path) - label = version_label(path, generation) - st.subheader(f"{title} · {label}") - image_col, info_col = st.columns([1, 1]) - with image_col: - if generation.get("asset_uri"): - show_generated_image(generation["asset_uri"]) - else: - st.caption(f"Generation status: {generation['status']}") - with info_col: - st.write(generation["prompt"]) - show_details(generation) - version_actions(path, generation, key_prefix=f"version-dialog-{generation['id']}") - - -@st.dialog("Design lineage", width="large") -def lineage_dialog(path: list[dict[str, Any]], lineage_summary: str) -> None: - title = line_title(path) - st.subheader(f"{title} · Lineage") - st.caption(lineage_summary) - rename, save = st.columns([3, 1]) - with rename: - new_title = st.text_input("Design title", value=title, key=f"title-{path[-1]['id']}") - with save: - st.write("") - if st.button("Save title", key=f"save-title-{path[-1]['id']}"): - save_line_title(path, new_title) - st.rerun() - - thumbnail_rows(path, key_prefix=f"lineage-dialog-{path[-1]['id']}", open_dialog=False) - - selected = next( - (item for item in path if item["id"] == st.session_state.focused_generation_id), - path[-1], - ) - st.divider() - st.caption(f"Selected: {version_label(path, selected)}") - image_col, info_col = st.columns([1, 1]) - with image_col: - if selected.get("asset_uri"): - show_generated_image(selected["asset_uri"]) - else: - st.caption(f"Generation status: {selected['status']}") - with info_col: - st.write(selected["prompt"]) - show_details(selected) - version_actions(path, selected, key_prefix=f"lineage-selected-{selected['id']}") - - -@st.dialog("Edit design", width="large") -def edit_dialog(selected: dict[str, Any], path: list[dict[str, Any]] | None) -> None: - label = version_label(path, selected) if path is not None and selected in path else "version" - st.subheader(f"Edit from {selected.get('title') or 'Untitled design'} · {label}") - edit_left, edit_right = st.columns([1, 1]) - with edit_left: - show_generated_image(selected["asset_uri"]) - st.caption(f"Source {selected['width']}x{selected['height']}") - with edit_right: - edit_instruction = st.text_area( - "Edit instruction", - key=f"edit-instruction-{selected['id']}", - height=150, - placeholder="Describe what should change while preserving the rest...", - ) - edit_references = st.file_uploader( - "Additional reference images", - type=["png", "jpg", "jpeg", "webp"], - accept_multiple_files=True, - key=f"edit-references-{selected['id']}", - help="Upload up to three additional references.", - ) - show_upload_previews(edit_references) - edit_aspect = st.radio( - "Aspect ratio", - EDIT_ASPECT_OPTIONS, - key=f"edit-aspect-{selected['id']}", - horizontal=True, - ) - if edit_aspect == "Custom": - ratio_width, ratio_height = ratio_parts(selected["width"], selected["height"]) - custom_left, custom_right = st.columns(2) - with custom_left: - edit_custom_width = st.number_input( - "Ratio width", - min_value=1, - max_value=20, - value=ratio_width, - key=f"edit-custom-width-{selected['id']}", - ) - with custom_right: - edit_custom_height = st.number_input( - "Ratio height", - min_value=1, - max_value=20, - value=ratio_height, - key=f"edit-custom-height-{selected['id']}", - ) - else: - edit_custom_width = selected["width"] - edit_custom_height = selected["height"] - edit_width, edit_height = aspect_dimensions( - edit_aspect, - edit_custom_width, - edit_custom_height, - source=selected, - ) - st.caption(f"Output size: {edit_width}x{edit_height}") - edit_format_label = st.selectbox( - "Edited output format", - options=list(OUTPUT_FORMATS), - index=list(OUTPUT_FORMATS).index(_format_label(selected["output_format"])), - key=f"edit-format-{selected['id']}", - ) - edit_background = st.checkbox( - "Background-removal friendly", - value=selected["background_removal_friendly"], - key=f"edit-background-{selected['id']}", - ) - too_many_edit = len(edit_references) > 3 - if too_many_edit: - st.error("A maximum of three additional reference images is allowed.") - preview_edit, render, cancel = st.columns(3) - with preview_edit: - show_edit_preview = st.button( - "Preview full prompt", - disabled=not edit_instruction.strip(), - key=f"preview-edit-prompt-{selected['id']}", - ) - with render: - render_edit = st.button( - f"Generate from {label}", - type="primary", - disabled=not edit_instruction.strip() or too_many_edit, - ) - with cancel: - if st.button("Cancel", on_click=cancel_edit): - st.rerun() - if show_edit_preview: - st.code( - prompt_preview( - prompt=edit_instruction.strip(), - width=edit_width, - height=edit_height, - output_format=OUTPUT_FORMATS[edit_format_label], - background_removal_friendly=edit_background, - reference_count=len(edit_references) + 1, - ) - ) - if render_edit: - with st.spinner("Rendering edit with OpenAI..."): - result = post_multipart( - f"/image-generations/{selected['id']}/edits", - data={ - "prompt": edit_instruction.strip(), - "width": str(edit_width), - "height": str(edit_height), - "output_format": OUTPUT_FORMATS[edit_format_label], - "background_removal_friendly": str(edit_background).lower(), - }, - files=upload_files(edit_references), - timeout=150, - ) - if result["status"] == "succeeded": - st.success("Edited version generated and saved.") - st.session_state.editing_generation_id = None - elif result["status"] == "unknown": - st.warning(result["error_message"]) - else: - st.error(result["error_message"]) - st.rerun() - - -try: - generations = get("/image-generations") - - with st.expander("New design", expanded=not generations): - title = st.text_input("Title", key="design_title") - prompt = st.text_area( - "Prompt", - key="design_prompt", - height=180, - placeholder="Describe the image you want to generate...", - ) - initial_references = st.file_uploader( - "Reference images", - type=["png", "jpg", "jpeg", "webp"], - accept_multiple_files=True, - help="Upload up to four images. Upload order determines reference numbering.", - ) - show_upload_previews(initial_references) - - aspect = st.radio("Aspect ratio", ASPECT_OPTIONS, key="design_aspect", horizontal=True) - if aspect == "Custom": - custom_left, custom_right = st.columns(2) - with custom_left: - custom_width = st.number_input( - "Ratio width", min_value=1, max_value=20, key="design_custom_width" - ) - with custom_right: - custom_height = st.number_input( - "Ratio height", min_value=1, max_value=20, key="design_custom_height" - ) - else: - custom_width = st.session_state.design_custom_width - custom_height = st.session_state.design_custom_height - width, height = aspect_dimensions(aspect, custom_width, custom_height) - st.caption(f"Output size: {width}x{height}") - - control_left, control_right = st.columns(2) - with control_left: - output_label = st.selectbox( - "Output format", - options=list(OUTPUT_FORMATS), - key="design_output_format", - ) - with control_right: - background_friendly = st.checkbox( - "Background-removal friendly", - key="design_background_friendly", - help="Requests an isolated subject on a uniform, high-contrast background.", - ) - - too_many_initial = len(initial_references) > 4 - if too_many_initial: - st.error("A maximum of four reference images is allowed.") - preview, generate = st.columns([1, 1]) - with preview: - show_preview = st.button( - "Preview full prompt", - disabled=not prompt.strip(), - key="preview-create-prompt", - ) - with generate: - render_image = st.button( - "Generate v1", - type="primary", - disabled=not prompt.strip() or too_many_initial, - ) - if show_preview: - st.code( - prompt_preview( - prompt=prompt.strip(), - width=width, - height=height, - output_format=OUTPUT_FORMATS[output_label], - background_removal_friendly=background_friendly, - reference_count=len(initial_references), - ) - ) - if render_image: - with st.spinner("Generating with OpenAI..."): - result = post_multipart( - "/image-generations", - data={ - "title": title.strip() or "Untitled design", - "prompt": prompt.strip(), - "width": str(width), - "height": str(height), - "output_format": OUTPUT_FORMATS[output_label], - "background_removal_friendly": str(background_friendly).lower(), - }, - files=upload_files(initial_references), - timeout=150, - ) - if result["status"] == "succeeded": - st.success("Image generated and saved.") - elif result["status"] == "unknown": - st.warning(result["error_message"]) - else: - st.error(result["error_message"]) - st.rerun() - - paths = lineage_paths(generations) - selected = next( - (item for item in generations if item["id"] == st.session_state.editing_generation_id), - None, - ) - if selected is not None: - selected_path = next((path for path in paths if selected in path), None) - edit_dialog(selected, selected_path) - - st.divider() - st.subheader("Design Library") - if not generations: - st.info("No designs have been generated.") - - root_counts: dict[str, int] = {} - for path in paths: - root_counts[str(path[0]["id"])] = root_counts.get(str(path[0]["id"]), 0) + 1 - root_seen: dict[str, int] = {} - for path in paths: - latest = path[-1] - title = line_title(path) - root_id = str(path[0]["id"]) - root_seen[root_id] = root_seen.get(root_id, 0) + 1 - lineage_summary = branch_label(path) - if root_counts[root_id] > 1: - lineage_summary = ( - f"{lineage_summary} · Fork {root_seen[root_id]} of {root_counts[root_id]}" - ) - with st.container(border=True): - st.markdown(f"**{title}**") - st.caption( - f"{len(path)} version{'s' if len(path) != 1 else ''} · " - f"Latest {version_label(path, latest)} · Updated {latest['created_at']}" - ) - st.caption(lineage_summary) - thumbnail_rows(path, key_prefix=f"library-{latest['id']}") - inspect_col, delete_col = st.columns([1, 1]) - with inspect_col: - if st.button("View lineage", key=f"view-lineage-{latest['id']}"): - st.session_state.focused_lineage_leaf_id = latest["id"] - st.session_state.focused_generation_id = latest["id"] - lineage_dialog(path, lineage_summary) - with delete_col: - if st.session_state.deleting_lineage_leaf_id == latest["id"]: - if st.button( - "Confirm delete", - key=f"confirm-delete-lineage-{latest['id']}", - type="primary", - ): - confirm_delete_lineage(path) - st.rerun() - if st.button("Cancel", key=f"cancel-delete-lineage-{latest['id']}"): - cancel_delete_lineage() - st.rerun() - elif st.button( - "Delete...", - key=f"delete-lineage-{latest['id']}", - ): - start_delete_lineage(latest["id"]) - st.rerun() -except Exception as error: - show_error(error) diff --git a/src/ecommerce_agent/dashboard/pages/6_Etsy_Listings.py b/src/ecommerce_agent/dashboard/pages/6_Etsy_Listings.py deleted file mode 100644 index 13f09ff..0000000 --- a/src/ecommerce_agent/dashboard/pages/6_Etsy_Listings.py +++ /dev/null @@ -1,330 +0,0 @@ -from datetime import UTC, datetime -from typing import Any - -import streamlit as st - -from ecommerce_agent.dashboard.client import get, patch, post, request -from ecommerce_agent.dashboard.ui import page_header, show_error - -page_header("Etsy Listings", "Draft and published listings from Etsy.") - - -def money(value: Any) -> str: - if isinstance(value, dict): - amount = value.get("amount") - divisor = value.get("divisor") or 100 - currency = value.get("currency_code") or "" - if amount is not None: - return f"{amount / divisor:.2f} {currency}".strip() - if value not in (None, ""): - return str(value) - return "" - - -def price_number(value: Any) -> float: - if isinstance(value, dict): - amount = value.get("amount") - divisor = value.get("divisor") or 100 - return float(amount or 0) / float(divisor) - return float(value or 0) - - -def image_url(listing: dict[str, Any]) -> str: - images = listing.get("images") or [] - if not images: - return "" - image = sorted(images, key=lambda row: int(row.get("rank") or 999))[0] - return str(image.get("url_170x135") or image.get("url_fullxfull") or "") - - -def timestamp(value: Any) -> str: - if not value: - return "" - return datetime.fromtimestamp(int(value), UTC).strftime("%Y-%m-%d") - - -def tags_text(value: list[str] | None) -> str: - return ", ".join(value or []) - - -def split_text(value: str) -> list[str]: - return [item.strip() for item in value.split(",") if item.strip()] - - -def first_offering(inventory: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any]] | None: - products = inventory.get("products") or [] - if not products: - return None - product = products[0] - offerings = product.get("offerings") or [] - if not offerings: - return None - return product, offerings[0] - - -def listing_rows(listings: list[dict[str, Any]]) -> list[dict[str, Any]]: - return [ - { - "thumbnail": image_url(listing), - "title": listing.get("title"), - "listing_id": str(listing.get("listing_id")), - "price": money(listing.get("price")), - "quantity": listing.get("quantity"), - "state": listing.get("state"), - "favorites": listing.get("num_favorers"), - "views": listing.get("views"), - "updated": timestamp( - listing.get("updated_timestamp") or listing.get("last_modified_timestamp") - ), - } - for listing in listings - ] - - -def filtered(listings: list[dict[str, Any]], search: str) -> list[dict[str, Any]]: - if not search.strip(): - return listings - needle = search.casefold() - return [ - listing - for listing in listings - if needle - in " ".join( - [ - str(listing.get("listing_id", "")), - str(listing.get("title", "")), - " ".join(listing.get("tags") or []), - ] - ).casefold() - ] - - -def changed(original: dict[str, Any], edited: dict[str, Any]) -> dict[str, Any]: - return {key: value for key, value in edited.items() if original.get(key) != value} - - -def edit_listing(summary: dict[str, Any]) -> None: - detail = get(f"/etsy/listings/{summary['listing_id']}") - url = image_url(detail) - if url: - st.image(url, width="stretch") - if detail.get("url"): - st.link_button("Open in Etsy", detail["url"]) - - metrics = st.columns(3) - metrics[0].metric("Favorites", detail.get("num_favorers") or 0) - metrics[1].metric("Views", detail.get("views") or 0) - metrics[2].metric("Quantity", detail.get("quantity") or 0) - - with st.form(f"listing-{detail['listing_id']}"): - st.subheader("Copy") - title = st.text_input("Title", value=detail.get("title") or "", max_chars=140) - description = st.text_area("Description", value=detail.get("description") or "", height=220) - tags = st.text_input("Tags", value=tags_text(detail.get("tags"))) - materials = st.text_input("Materials", value=tags_text(detail.get("materials"))) - - st.subheader("Setup") - setup_columns = st.columns(2) - with setup_columns[0]: - taxonomy_id = st.number_input( - "Taxonomy ID", - min_value=0, - value=int(detail.get("taxonomy_id") or 0), - step=1, - ) - shipping_profile_id = st.number_input( - "Shipping profile ID", - min_value=0, - value=int(detail.get("shipping_profile_id") or 0), - step=1, - ) - return_policy_id = st.number_input( - "Return policy ID", - min_value=0, - value=int(detail.get("return_policy_id") or 0), - step=1, - ) - readiness_state_id = st.number_input( - "Readiness state ID", - min_value=0, - value=int(detail.get("readiness_state_id") or 0), - step=1, - ) - with setup_columns[1]: - who_made = st.selectbox( - "Who made it", - ["i_did", "someone_else", "collective"], - index=["i_did", "someone_else", "collective"].index( - detail.get("who_made") - if detail.get("who_made") in {"i_did", "someone_else", "collective"} - else "someone_else" - ), - ) - when_options = [ - "made_to_order", - "2020_2026", - "2010_2019", - "2007_2009", - "before_2007", - "2000_2006", - "1990s", - "1980s", - "1970s", - "1960s", - "1950s", - "1940s", - "1930s", - "1920s", - "1910s", - "1900s", - "1800s", - "1700s", - "before_1700", - ] - when_made = st.selectbox( - "When made", - when_options, - index=when_options.index( - detail.get("when_made") - if detail.get("when_made") in set(when_options) - else "made_to_order" - ), - ) - is_supply = st.checkbox("Supply", value=bool(detail.get("is_supply"))) - should_auto_renew = st.checkbox( - "Auto renew", value=bool(detail.get("should_auto_renew")) - ) - production_partner_ids = st.text_input( - "Production partner IDs", - value=", ".join(str(value) for value in detail.get("production_partner_ids", [])), - ) - - st.subheader("Inventory") - inventory = detail.get("inventory") or {} - first = first_offering(inventory) - inventory_changed = False - if first is None: - st.info("No editable inventory returned.") - else: - product, offering = first - inventory_columns = st.columns(4) - sku = inventory_columns[0].text_input("SKU", value=str(product.get("sku") or "")) - price = inventory_columns[1].number_input( - "Price", - min_value=0.01, - value=price_number(offering.get("price")) or 1.0, - step=1.0, - ) - quantity = inventory_columns[2].number_input( - "Quantity", - min_value=1, - value=int(offering.get("quantity") or 1), - step=1, - ) - enabled = inventory_columns[3].checkbox( - "Enabled", value=bool(offering.get("is_enabled", True)) - ) - inventory_changed = ( - product.get("sku") != sku - or price_number(offering.get("price")) != price - or int(offering.get("quantity") or 0) != quantity - or bool(offering.get("is_enabled", True)) != enabled - ) - - st.subheader("Images") - for image in sorted(detail.get("images") or [], key=lambda row: int(row.get("rank") or 0)): - st.caption(f"#{image.get('rank')} | {image.get('alt_text') or 'No alt text'}") - if image.get("url_170x135") or image.get("url_fullxfull"): - st.image(image.get("url_170x135") or image.get("url_fullxfull"), width=170) - - saved = st.form_submit_button("Save changes", type="primary") - - if not saved: - with st.expander("Raw Etsy JSON"): - st.json(detail) - return - - listing_payload = changed( - detail, - { - "title": title.strip(), - "description": description, - "tags": split_text(tags), - "materials": split_text(materials), - "taxonomy_id": int(taxonomy_id) or None, - "shipping_profile_id": int(shipping_profile_id) or None, - "return_policy_id": int(return_policy_id) or None, - "readiness_state_id": int(readiness_state_id) or None, - "who_made": who_made, - "when_made": when_made, - "is_supply": is_supply, - "should_auto_renew": should_auto_renew, - "production_partner_ids": [int(value) for value in split_text(production_partner_ids)], - }, - ) - if listing_payload: - patch(f"/etsy/listings/{detail['listing_id']}", listing_payload, timeout=45) - if first is not None and inventory_changed: - product, offering = first - product["sku"] = sku - offering["price"] = price - offering["quantity"] = quantity - offering["is_enabled"] = enabled - request( - "PUT", f"/etsy/listings/{detail['listing_id']}/inventory", json=inventory, timeout=45 - ) - st.success("Listing saved.") - st.rerun() - - -try: - auth = get("/etsy/oauth/status") - status_columns = st.columns([3, 1, 1]) - with status_columns[0]: - if auth["connected"]: - st.caption(f"Connected to Etsy shop {auth['shop_id']}.") - else: - st.warning("Etsy OAuth is not connected. Mock mode can still show sample listings.") - with status_columns[1]: - if st.button("Refresh OAuth", width="stretch", disabled=not auth["connected"]): - post("/etsy/oauth/refresh") - st.rerun() - with status_columns[2]: - st.link_button( - "Connect Etsy", "http://localhost:8000/api/etsy/oauth/start", width="stretch" - ) - - search = st.text_input("Search listings", placeholder="Title, listing ID, or tag") - tabs = st.tabs(["Draft", "Published"]) - for tab, state in zip(tabs, ["draft", "active"], strict=True): - with tab: - listings = filtered(get(f"/etsy/listings?state={state}"), search) - left, right = st.columns([3, 2]) - with left: - rows = listing_rows(listings) - st.dataframe( - rows, - hide_index=True, - width="stretch", - column_config={ - "thumbnail": st.column_config.ImageColumn(""), - "title": "Title", - "listing_id": "Listing ID", - }, - ) - if rows: - labels = {row["listing_id"]: row["title"] for row in rows} - selected_id = st.radio( - "Selected listing", - [row["listing_id"] for row in rows], - format_func=labels.__getitem__, - key=f"selected-{state}", - ) - else: - st.info("No listings found.") - selected_id = None - with right: - if selected_id: - edit_listing({"listing_id": selected_id}) -except Exception as error: - show_error(error) diff --git a/src/ecommerce_agent/dashboard/pages/6_Etsy_Posting_Workflow.py b/src/ecommerce_agent/dashboard/pages/6_Etsy_Posting_Workflow.py deleted file mode 100644 index 264ba86..0000000 --- a/src/ecommerce_agent/dashboard/pages/6_Etsy_Posting_Workflow.py +++ /dev/null @@ -1,572 +0,0 @@ -from typing import Any - -import streamlit as st - -from ecommerce_agent.dashboard.client import get, get_asset, patch, post -from ecommerce_agent.dashboard.ui import page_header, show_error - -STEPS = [ - ("select_products", "Select products"), - ("review_copy", "Review copy"), - ("build_images", "Build images"), - ("final_review", "Final review"), - ("submit_drafts", "Submit drafts"), -] - -page_header( - "Etsy Posting Workflow", - "Turn Printful product templates into reviewed Etsy draft listings.", -) - - -def results(value: Any) -> list[dict[str, Any]]: - if isinstance(value, dict): - rows = value.get("results", []) - return rows if isinstance(rows, list) else [] - return value if isinstance(value, list) else [] - - -def taxonomy_options(value: Any) -> list[tuple[int, str]]: - options: list[tuple[int, str]] = [] - - def walk(node: dict[str, Any], parents: list[str]) -> None: - path = [*parents, str(node["name"])] - children = node.get("children", []) - if children: - for child in children: - walk(child, path) - else: - options.append((int(node["id"]), " > ".join(path))) - - for root in results(value): - walk(root, []) - return options - - -def remote_or_local_image(image: dict[str, Any]) -> None: - if image.get("asset_uri"): - st.image(get_asset(image["asset_uri"]), width="stretch") - elif image.get("source_url"): - st.image(image["source_url"], width="stretch") - - -def move(session: dict[str, Any], step: str) -> None: - patch( - f"/posting/sessions/{session['id']}", - {"version": session["version"], "current_step": step}, - ) - st.rerun() - - -try: - auth = get("/etsy/oauth/status") - if not auth["connected"]: - st.error("Connect Etsy before using the posting workflow.") - st.link_button("Connect Etsy", "http://localhost:8000/api/etsy/oauth/start") - st.stop() - st.caption(f"Connected to Etsy shop {auth['shop_id']} with all required scopes.") - - sessions = get("/posting/sessions") - new_tab, resume_tab = st.tabs(["New session", "Resume session"]) - with new_tab: - source = get("/posting/templates") - templates = source["templates"] - selected_ids: list[int] = [] - st.subheader("Printful product templates") - for template in templates: - with st.container(border=True): - preview, detail = st.columns([1, 2]) - with preview: - st.image(template["mockup_file_url"], width="stretch") - with detail: - selected = st.checkbox( - template["title"], - key=f"template-{template['id']}", - ) - if selected: - selected_ids.append(int(template["id"])) - swatches = " ".join( - f"`{color['color_name']}`" for color in template.get("colors", []) - ) - st.markdown(swatches or "No colors reported") - st.caption( - f"Sizes: {', '.join(template.get('sizes', []))} | " - f"Variants: {len(template.get('available_variant_ids', []))}" - ) - placements = template.get("placements", []) - if placements: - st.caption( - " | ".join( - f"{row.get('display_name')}: {row.get('technique_display_name')}" - for row in placements - ) - ) - price = st.number_input( - "Default retail price", - min_value=1.0, - value=34.99, - step=1.0, - ) - if st.button( - "Create posting session", - type="primary", - disabled=not selected_ids, - ): - created = post( - "/posting/sessions", - { - "name": "Etsy posting session", - "template_ids": selected_ids, - "retail_price": price, - }, - timeout=180, - ) - st.session_state.posting_session_id = created["id"] - st.rerun() - - with st.expander("Existing Etsy-synced Printful products"): - synced = source["etsy_synced_products"] - if synced: - st.dataframe(synced, hide_index=True, width="stretch") - else: - st.info("No existing synced products were returned.") - - with resume_tab: - if sessions: - selected_session = st.selectbox( - "Posting session", - [row["id"] for row in sessions], - format_func=lambda value: next( - f"{row['name']} | {row['status']} | {row['updated_at']}" - for row in sessions - if row["id"] == value - ), - ) - if st.button("Open session"): - st.session_state.posting_session_id = selected_session - st.rerun() - else: - st.info("No posting sessions exist yet.") - - session_id = st.session_state.get("posting_session_id") - if not session_id: - st.stop() - posting = get(f"/posting/sessions/{session_id}") - st.divider() - step_labels = [label for _, label in STEPS] - current_index = [key for key, _ in STEPS].index(posting["current_step"]) - st.progress((current_index + 1) / len(STEPS), text=step_labels[current_index]) - - if posting["current_step"] == "select_products": - st.subheader("Selected products") - for item in posting["items"]: - left, right = st.columns([1, 2]) - with left: - st.image(item["template"]["mockup_file_url"], width="stretch") - with right: - st.write(item["template"]["title"]) - st.write("Colors:", ", ".join(item["selected_colors"])) - st.write("Sizes:", ", ".join(item["selected_sizes"])) - if st.button("Continue to copy", type="primary"): - move(posting, "review_copy") - - elif posting["current_step"] == "review_copy": - designs = get("/printful/designs") - designs_by_id = {design["id"]: design for design in designs} - for item in posting["items"]: - with st.expander(item["template"]["title"], expanded=True): - brief_col, generate_col = st.columns([4, 1], vertical_alignment="bottom") - with brief_col: - copy_context = st.text_area( - "Product and design context", - value=item["copy_context"] or "", - placeholder=( - "Hat with an embroidered blue BMW M5 viewed from an angle, " - "near-isometric perspective" - ), - help=( - "Describe the artwork, subject, perspective, audience, mood, and " - "important details the listing copy should emphasize." - ), - max_chars=4000, - key=f"context-{item['id']}", - ) - with generate_col: - generate_clicked = st.button( - "Generate copy", - key=f"generate-{item['id']}", - type="primary" if not item["title"] else "secondary", - width="stretch", - ) - - design_ids = [None, *designs_by_id] - current_design_id = item["copy_design_id"] - design_index = ( - design_ids.index(current_design_id) if current_design_id in design_ids else 0 - ) - selected_design_id = st.selectbox( - "Original design reference", - design_ids, - index=design_index, - format_func=lambda value: ( - "No linked Workshop design" - if value is None - else designs_by_id[value]["title"] - ), - help=( - "Workshop auto-selects a design when this Printful template was created " - "from a linked draft. For legacy templates, select the matching design." - ), - key=f"copy-design-{item['id']}", - ) - if selected_design_id is None: - st.info( - "Printful does not expose the reusable artwork for this legacy template. " - "Select the matching Workshop design above if it is in the design library." - ) - - candidates = item["copy_reference_candidates"] - candidate_labels = {candidate["id"]: candidate["label"] for candidate in candidates} - available_candidate_ids = list(candidate_labels) - selected_reference_ids = st.multiselect( - "Additional product references", - available_candidate_ids, - default=[ - reference_id - for reference_id in item["copy_reference_ids"] - if reference_id in candidate_labels - ], - max_selections=2, - format_func=candidate_labels.__getitem__, - help=( - "The primary Printful mockup is always included. Select up to two more " - "catalog, color, or product images when available." - ), - key=f"copy-references-{item['id']}", - ) - if not candidates: - st.caption( - "No additional Printful product images are available for this template." - ) - - st.caption("Images sent to ChatGPT") - preview_items: list[tuple[str, Any]] = [] - if selected_design_id is not None: - preview_items.append( - ( - "Original design", - get_asset(designs_by_id[selected_design_id]["asset_uri"]), - ) - ) - preview_items.append( - ( - "Primary Printful mockup (always included)", - item["template"]["mockup_file_url"], - ) - ) - selected_candidates = {candidate["id"]: candidate for candidate in candidates} - preview_items.extend( - ( - selected_candidates[reference_id]["label"], - selected_candidates[reference_id]["url"], - ) - for reference_id in selected_reference_ids - ) - preview_columns = st.columns(min(len(preview_items), 4)) - for index, (label, image_source) in enumerate(preview_items): - with preview_columns[index]: - st.image(image_source, width="stretch") - st.caption(label) - - generation_payload = { - "copy_context": copy_context.strip(), - "copy_design_id": selected_design_id, - "copy_reference_ids": selected_reference_ids, - } - save_inputs, reference_count = st.columns([1, 3], vertical_alignment="center") - if save_inputs.button( - "Save generation inputs", - key=f"save-generation-inputs-{item['id']}", - ): - patch( - f"/posting/items/{item['id']}", - {"version": item["version"], **generation_payload}, - ) - st.rerun() - reference_count.caption( - f"{len(preview_items)} image reference" - f"{'' if len(preview_items) == 1 else 's'} selected" - ) - if generate_clicked: - post( - f"/posting/items/{item['id']}/generate-copy", - generation_payload, - timeout=180, - ) - st.rerun() - - with st.form(f"copy-{item['id']}"): - title = st.text_input("Title", value=item["title"] or "", max_chars=140) - description = st.text_area( - "Description", value=item["description"] or "", height=260 - ) - tags = st.text_input("Tags, comma separated", value=", ".join(item["tags"])) - materials = st.text_input( - "Materials, comma separated", value=", ".join(item["materials"]) - ) - alt = st.text_input( - "Image alt text", value=item["image_alt_text"] or "", max_chars=500 - ) - price = st.number_input( - "Retail price", - value=float(item["retail_price"]), - min_value=1.0, - key=f"price-{item['id']}", - ) - if st.form_submit_button("Save copy", type="primary"): - patch( - f"/posting/items/{item['id']}", - { - "version": item["version"], - "title": title, - "description": description, - "tags": [ - value.strip() for value in tags.split(",") if value.strip() - ], - "materials": [ - value.strip() for value in materials.split(",") if value.strip() - ], - "image_alt_text": alt, - "retail_price": price, - }, - ) - st.success("Saved.") - st.rerun() - st.caption(f"Saved version {item['version']}") - nav = st.columns(2) - nav[0].button( - "Back", - on_click=move, - args=(posting, "select_products"), - ) - if nav[1].button( - "Continue to images", - type="primary", - disabled=any(not item["title"] for item in posting["items"]), - ): - move(posting, "build_images") - - elif posting["current_step"] == "build_images": - for item in posting["items"]: - st.subheader(item["title"] or item["template"]["title"]) - columns = st.columns(min(max(len(item["images"]), 1), 4)) - for index, image in enumerate(item["images"]): - with columns[index % len(columns)]: - remote_or_local_image(image) - st.caption( - "Accurate Printful mockup" - if image["source"] == "printful" - else "AI marketing image: verify design details" - ) - approved = st.checkbox( - "Approved", - value=image["approved"], - key=f"approved-{image['id']}", - ) - drift = image["ai_drift_acknowledged"] - if image["source"] == "ai_lifestyle": - drift = st.checkbox( - "I checked for design drift", - value=drift, - key=f"drift-{image['id']}", - ) - if approved != image["approved"] or drift != image["ai_drift_acknowledged"]: - patch( - f"/posting/images/{image['id']}", - { - "version": image["version"], - "approved": approved, - "ai_drift_acknowledged": drift, - }, - ) - st.rerun() - with st.form(f"lifestyle-{item['id']}"): - prompt = st.text_input( - "Optional lifestyle scene", - placeholder="On a clean studio table beside tasteful gift packaging", - ) - if st.form_submit_button("Generate AI lifestyle image"): - if not prompt.strip(): - st.warning("Describe the lifestyle scene before generating an image.") - else: - post( - f"/posting/items/{item['id']}/lifestyle-images", - {"prompt": prompt.strip()}, - timeout=360, - ) - st.rerun() - back, next_col = st.columns(2) - if back.button("Back to copy"): - move(posting, "review_copy") - if next_col.button("Continue to final review", type="primary"): - move(posting, "final_review") - - elif posting["current_step"] == "final_review": - metadata = get("/posting/metadata") - defaults = posting["defaults"] - shipping = results(metadata["shipping_profiles"]) - returns = results(metadata["return_policies"]) - processing = results(metadata["processing_profiles"]) - partners = results(metadata["production_partners"]) - taxonomy = taxonomy_options(metadata["seller_taxonomy"]) - shipping_ids = [row["shipping_profile_id"] for row in shipping] - return_ids = [row["return_policy_id"] for row in returns] - readiness_ids = [row["readiness_state_id"] for row in processing] - partner_ids = [row["production_partner_id"] for row in partners] - taxonomy_ids = [value for value, _ in taxonomy] - shipping_labels = { - row["shipping_profile_id"]: str(row.get("title") or row["shipping_profile_id"]) - for row in shipping - } - readiness_labels = { - row["readiness_state_id"]: str( - row.get("processing_days_display_label") - or row.get("display_name") - or row["readiness_state_id"] - ) - for row in processing - } - partner_labels = { - row["production_partner_id"]: str( - row.get("partner_name") or row["production_partner_id"] - ) - for row in partners - } - taxonomy_labels = dict(taxonomy) - if not returns and not metadata["shop"].get("is_using_structured_policies"): - st.info( - "This shop does not use Etsy structured return policies. " - "A return policy ID is not required for its draft listings." - ) - with st.form("listing-defaults"): - shipping_id = st.selectbox( - "Shipping profile", - [None, *shipping_ids], - index=( - shipping_ids.index(defaults["shipping_profile_id"]) + 1 - if defaults.get("shipping_profile_id") in shipping_ids - else 0 - ), - format_func=lambda value: ( - "Select a shipping profile" if value is None else shipping_labels[value] - ), - ) - return_id = st.selectbox( - "Return policy", - [None, *return_ids], - index=( - return_ids.index(defaults["return_policy_id"]) + 1 - if defaults.get("return_policy_id") in return_ids - else 0 - ), - format_func=lambda value: ( - "No structured return policy" if value is None else str(value) - ), - ) - readiness_id = st.selectbox( - "Processing profile", - [None, *readiness_ids], - index=( - readiness_ids.index(defaults["readiness_state_id"]) + 1 - if defaults.get("readiness_state_id") in readiness_ids - else (1 if len(readiness_ids) == 1 else 0) - ), - format_func=lambda value: ( - "Select a processing profile" if value is None else readiness_labels[value] - ), - ) - partner_id = st.selectbox( - "Production partner", - [None, *partner_ids], - index=( - partner_ids.index(defaults["production_partner_id"]) + 1 - if defaults.get("production_partner_id") in partner_ids - else (1 if len(partner_ids) == 1 else 0) - ), - format_func=lambda value: ( - "Select a production partner" if value is None else partner_labels[value] - ), - ) - taxonomy_id = st.selectbox( - "Etsy category", - [None, *taxonomy_ids], - index=( - taxonomy_ids.index(defaults["taxonomy_id"]) + 1 - if defaults.get("taxonomy_id") in taxonomy_ids - else 0 - ), - format_func=lambda value: ( - "Select an Etsy category" if value is None else taxonomy_labels[value] - ), - ) - if st.form_submit_button("Save listing defaults", type="primary"): - patch( - f"/posting/sessions/{posting['id']}", - { - "version": posting["version"], - "defaults": { - "shipping_profile_id": shipping_id, - "return_policy_id": return_id, - "readiness_state_id": readiness_id, - "production_partner_id": partner_id, - "taxonomy_id": taxonomy_id, - }, - }, - ) - post(f"/posting/sessions/{posting['id']}/validate") - st.rerun() - for item in posting["items"]: - with st.expander(item["title"] or item["template"]["title"], expanded=True): - st.write(item["description"]) - st.write("Tags:", item["tags"]) - st.write( - f"${item['retail_price']} | {len(item['selected_variant_ids'])} variants | " - f"{len([image for image in item['images'] if image['approved']])} images" - ) - for error in item["validation_errors"]: - st.error(error) - for error in posting["validation_errors"]: - st.error(error) - back, next_col = st.columns(2) - if back.button("Back to images"): - move(posting, "build_images") - if next_col.button( - "Continue to submission", - type="primary", - disabled=bool( - posting["validation_errors"] - or any(item["validation_errors"] for item in posting["items"]) - ), - ): - move(posting, "submit_drafts") - - else: - if st.button("Submit Etsy drafts", type="primary"): - post(f"/posting/sessions/{posting['id']}/submit", timeout=180) - st.rerun() - for item in posting["items"]: - with st.container(border=True): - st.write(item["title"]) - st.write(f"Etsy listing: {item['etsy_listing_id'] or 'not created'}") - st.write(f"Status: {item['status']}") - st.write(f"Printful native sync: {item['printful_sync_status']}") - if item["etsy_listing_id"] and st.button( - "Refresh Printful sync", - key=f"sync-{item['id']}", - ): - post(f"/posting/items/{item['id']}/sync-status", timeout=60) - st.rerun() - if item["error_message"]: - st.error(item["error_message"]) -except Exception as error: - show_error(error) diff --git a/src/ecommerce_agent/dashboard/pages/6_Printful_Workshop.py b/src/ecommerce_agent/dashboard/pages/6_Printful_Workshop.py deleted file mode 100644 index 4b14825..0000000 --- a/src/ecommerce_agent/dashboard/pages/6_Printful_Workshop.py +++ /dev/null @@ -1,456 +0,0 @@ -from decimal import Decimal -from typing import Any -from urllib.parse import urlencode - -import streamlit as st - -from ecommerce_agent.dashboard.client import get, get_asset, post -from ecommerce_agent.dashboard.ui import page_header, show_error - -page_header( - "Printful Workshop", - "Curate Printful blanks, pair them with reusable designs, preview mockups, " - "and create sync products.", -) - - -def product_label(product: dict[str, Any]) -> str: - price = product.get("cheapest_us_price") or "price unavailable" - return f"{product['title']} · score {product['workshop_score']} · from ${price}" - - -def draft_label(draft: dict[str, Any]) -> str: - return f"{draft['title']} · {draft['status']} · {draft['id'][:8]}" - - -try: - status_tab, catalog_tab, designs_tab, drafts_tab, templates_tab, sync_tab = st.tabs( - ["Status", "Catalog", "Designs", "Drafts", "Templates", "Sync products"] - ) - - with status_tab: - connection = get("/printful/status") - store = connection["store"] - metrics = st.columns(4) - metrics[0].metric("Mode", connection["mode"]) - metrics[1].metric("Store", store.get("name", store.get("id"))) - metrics[2].metric("Writes", "enabled" if connection["writes_enabled"] else "disabled") - metrics[3].metric( - "Asset delivery", - connection["asset_delivery_mode"] or "not configured", - ) - st.caption(f"Store ID {store.get('id')} · type {store.get('type')}") - if not connection["submission_ready"]: - st.warning( - "Catalog and template reads are live. Sync submission requires " - "`PRINTFUL_WRITES_ENABLED=true` plus signed public asset delivery." - ) - with st.expander("Token scopes"): - st.write(connection.get("scopes", [])) - - with catalog_tab: - refresh_col, note_col = st.columns([1, 4]) - with refresh_col: - if st.button("Refresh catalog", type="primary"): - with st.spinner("Reading Printful catalog and variant pricing..."): - result = post("/printful/catalog/refresh", {"category_ids": None}, timeout=180) - st.success(f"Refreshed {result['refreshed']} products.") - st.rerun() - with note_col: - st.caption( - "Price points compare the cheapest in-stock US variant only against " - "products in the same leaf category." - ) - - categories = get("/printful/categories") - category_options = {item["catalog_category_id"]: item["title"] for item in categories} - filters = st.columns([2, 2, 1]) - with filters[0]: - selected_category = st.selectbox( - "Category", - options=[None, *category_options], - format_func=lambda value: ( - "All categories" if value is None else category_options[value] - ), - ) - with filters[1]: - search = st.text_input("Search products") - with filters[2]: - favorites_only = st.checkbox("Favorites only") - query = urlencode( - { - key: value - for key, value in { - "category_id": selected_category, - "search": search.strip() or None, - "favorites_only": str(favorites_only).lower(), - }.items() - if value is not None - } - ) - catalog = get(f"/printful/catalog?{query}") - if not catalog: - st.info("Refresh the catalog or broaden the filters.") - else: - st.dataframe( - [ - { - "★": "★" if item["favorite"] else "", - "Product": item["title"], - "Brand": item["brand"], - "Price": item["cheapest_us_price"], - "Rating": item["manual_rating"], - "Score": item["workshop_score"], - "US variants": item["variant_count"], - } - for item in catalog - ], - hide_index=True, - width="stretch", - ) - selected_product_id = st.selectbox( - "Curate product", - options=[item["catalog_product_id"] for item in catalog], - format_func=lambda value: product_label( - next(item for item in catalog if item["catalog_product_id"] == value) - ), - ) - selected_product = get(f"/printful/catalog/{selected_product_id}") - detail = selected_product["raw_payload"]["detail"] - variants = detail.get("variants", []) - product_data = detail.get("product", {}) - left, right = st.columns([1, 2]) - with left: - if selected_product.get("image_url"): - st.image(selected_product["image_url"], width="stretch") - st.metric("Workshop score", selected_product["workshop_score"]) - st.json(selected_product["score_breakdown"], expanded=False) - with right: - with st.form(f"curate-{selected_product_id}"): - favorite = st.checkbox("Favorite", value=selected_product["favorite"]) - rating = st.select_slider( - "Manual rating", - options=[0, 1, 2, 3, 4, 5], - value=selected_product["manual_rating"] or 0, - help="0 means unrated and receives the neutral 15-point score.", - ) - notes = st.text_area("Notes", value=selected_product["notes"] or "") - variant_labels = { - int(item["id"]): ( - f"{item.get('color')} · {item.get('size')} · ${item.get('price', '?')}" - ) - for item in variants - } - preferred_variants = st.multiselect( - "Preferred variants", - options=list(variant_labels), - default=[ - item - for item in selected_product["preferred_variant_ids"] - if item in variant_labels - ], - format_func=lambda value: variant_labels[value], - ) - placements = [ - str(item["type"]) - for item in product_data.get("files", []) - if item.get("type") != "mockup" - ] - preferred_placement = st.selectbox( - "Preferred placement", - options=["", *placements], - index=( - placements.index(selected_product["preferred_placement"]) + 1 - if selected_product["preferred_placement"] in placements - else 0 - ), - ) - if st.form_submit_button("Save curation", type="primary"): - post( - f"/printful/catalog/{selected_product_id}/curation", - { - "favorite": favorite, - "manual_rating": rating or None, - "clear_rating": rating == 0, - "notes": notes.strip() or None, - "preferred_variant_ids": preferred_variants, - "preferred_placement": preferred_placement or None, - }, - ) - st.success("Curation and score updated.") - st.rerun() - - with designs_tab: - st.subheader("Promote a Design Studio image") - generations = [ - item - for item in get("/image-generations") - if item["status"] == "succeeded" and item.get("asset_uri") - ] - promoted = get("/printful/designs") - promoted_sources = {(item["source_type"], item["source_id"]) for item in promoted} - for generation in generations: - with st.container(border=True): - preview, info = st.columns([1, 2]) - with preview: - st.image(get_asset(generation["asset_uri"]), width="stretch") - with info: - st.write(generation["prompt"]) - is_promoted = ( - "image_generation", - generation["id"], - ) in promoted_sources - if st.button( - "Promoted" if is_promoted else "Promote to merch library", - key=f"promote-generation-{generation['id']}", - disabled=is_promoted, - ): - post(f"/printful/designs/from-generation/{generation['id']}") - st.rerun() - - st.subheader("Promote an approved workflow design") - workflow_designs = [item for item in get("/designs") if item["approved"] is True] - for design in workflow_designs: - is_promoted = ("design_asset", design["id"]) in promoted_sources - if st.button( - f"{'Promoted' if is_promoted else 'Promote'} workflow design {design['id'][:8]}", - key=f"promote-workflow-{design['id']}", - disabled=is_promoted, - ): - post(f"/printful/designs/from-workflow/{design['id']}") - st.rerun() - - st.subheader("Merch design library") - promoted = get("/printful/designs") - for design in promoted: - with st.container(border=True): - image_col, data_col = st.columns([1, 2]) - with image_col: - st.image(get_asset(design["asset_uri"]), width="stretch") - with data_col: - st.write(design["title"]) - st.caption( - f"{design['width']}×{design['height']} · {design['source_type']} · " - f"{'Printful-ready' if design['print_asset_uri'] else 'local only'}" - ) - - with drafts_tab: - designs = get("/printful/designs") - catalog = get("/printful/catalog") - st.subheader("Create design × product drafts") - if not designs or not catalog: - st.info("Promote at least one design and refresh the catalog first.") - else: - selected_designs = st.multiselect( - "Designs", - options=[item["id"] for item in designs], - format_func=lambda value: next( - item["title"] for item in designs if item["id"] == value - ), - ) - selected_products = st.multiselect( - "Products", - options=[item["catalog_product_id"] for item in catalog], - default=[206] if any(item["catalog_product_id"] == 206 for item in catalog) else [], - format_func=lambda value: product_label( - next(item for item in catalog if item["catalog_product_id"] == value) - ), - ) - retail_price = st.number_input("Retail price", min_value=0.01, value=34.99, step=1.00) - variant_ids_by_product: dict[str, list[int]] = {} - placement_by_product: dict[str, str] = {} - technique_by_product: dict[str, str] = {} - for product_id in selected_products: - item = get(f"/printful/catalog/{product_id}") - detail = item["raw_payload"]["detail"] - variants = [ - variant - for variant in detail.get("variants", []) - if any( - status.get("region") == "US" and status.get("status") == "in_stock" - for status in variant.get("availability_status", []) - ) - ] - labels = { - int(variant["id"]): ( - f"{variant.get('color')} · {variant.get('size')} · " - f"${variant.get('price', '?')}" - ) - for variant in variants - } - default_variants = item["preferred_variant_ids"] - if product_id == 206 and 12736 in labels: - default_variants = [12736] - variant_ids_by_product[str(product_id)] = st.multiselect( - f"Variants · {item['title']}", - options=list(labels), - default=[value for value in default_variants if value in labels], - format_func=labels.__getitem__, - key=f"draft-variants-{product_id}", - ) - placements = [ - str(file["type"]) - for file in detail.get("product", {}).get("files", []) - if file.get("type") != "mockup" - ] - default_placement = item["preferred_placement"] - if product_id == 206 and "embroidery_front_large" in placements: - default_placement = "embroidery_front_large" - placement_by_product[str(product_id)] = st.selectbox( - f"Placement · {item['title']}", - options=placements, - index=placements.index(default_placement) - if default_placement in placements - else 0, - key=f"draft-placement-{product_id}", - ) - techniques = detail.get("product", {}).get("techniques", []) - technique_keys = [str(item["key"]) for item in techniques] - default_technique = next( - (str(item["key"]) for item in techniques if item.get("is_default")), - technique_keys[0] if technique_keys else "EMBROIDERY", - ) - technique_by_product[str(product_id)] = st.selectbox( - f"Technique · {item['title']}", - options=technique_keys or ["EMBROIDERY"], - index=(technique_keys or ["EMBROIDERY"]).index(default_technique), - key=f"draft-technique-{product_id}", - ) - if st.button( - "Create drafts", - type="primary", - disabled=not selected_designs or not selected_products, - ): - created = post( - "/printful/drafts", - { - "design_ids": selected_designs, - "catalog_product_ids": selected_products, - "variant_ids_by_product": variant_ids_by_product, - "placement_by_product": placement_by_product, - "technique_by_product": technique_by_product, - "retail_price": str(Decimal(str(retail_price))), - }, - timeout=120, - ) - st.success(f"Created {len(created)} draft(s).") - st.rerun() - - st.subheader("Prepared drafts") - drafts = get("/printful/drafts") - products = get("/concepts") - for draft in drafts: - with st.expander(draft_label(draft), expanded=draft["status"] != "succeeded"): - validation = draft["validation"] - metrics = st.columns(4) - metrics[0].metric("Base cost", validation.get("base_cost") or "unknown") - metrics[1].metric("Decoration", validation.get("decoration_surcharge") or "0") - metrics[2].metric("Margin", validation.get("estimated_margin") or "unknown") - metrics[3].metric("Effective DPI", validation.get("effective_dpi")) - for warning in validation.get("warnings", []): - st.warning(warning) - for error in validation.get("errors", []): - st.error(error) - if draft.get("mockup_url"): - st.image(draft["mockup_url"], width=420) - actions = st.columns(4) - with actions[0]: - if st.button( - "Generate mockup", - key=f"mockup-{draft['id']}", - disabled=not validation.get("valid") or not connection["submission_ready"], - ): - with st.spinner("Generating Printful mockup..."): - post(f"/printful/drafts/{draft['id']}/mockup", timeout=90) - st.rerun() - with actions[1]: - if st.button( - "Submit", - type="primary", - key=f"submit-{draft['id']}", - disabled=not validation.get("valid") - or not connection["submission_ready"] - or draft["status"] in {"queued", "submitting", "succeeded"}, - ): - post("/printful/drafts/submit", {"draft_ids": [draft["id"]]}) - st.rerun() - with actions[2]: - if st.button( - "Retry", - key=f"retry-{draft['id']}", - disabled=draft["status"] != "failed" or not connection["submission_ready"], - ): - post(f"/printful/drafts/{draft['id']}/retry") - st.rerun() - with actions[3]: - attachable = [ - item - for item in products - if item["status"] in {"concept", "copy_review", "awaiting_printful_product"} - ] - if attachable: - attach_labels = {str(item["id"]): str(item["title"]) for item in attachable} - product_id = st.selectbox( - "Attach to workflow", - options=list(attach_labels), - format_func=attach_labels.__getitem__, - key=f"attach-select-{draft['id']}", - label_visibility="collapsed", - ) - if st.button("Attach", key=f"attach-{draft['id']}"): - post( - f"/printful/drafts/{draft['id']}/attach", - {"product_id": product_id}, - ) - st.rerun() - if draft.get("error_message"): - st.error(draft["error_message"]) - - with templates_tab: - templates = get("/printful/templates") - designs = get("/printful/designs") - if not templates: - st.info("No Printful Product Templates are available.") - for template in templates: - with st.expander(template["title"]): - if template.get("mockup_file_url"): - st.image(template["mockup_file_url"], width=320) - st.caption( - f"Catalog product {template['product_id']} · " - f"{len(template.get('available_variant_ids', []))} variants" - ) - selected = st.multiselect( - "Designs to apply", - options=[item["id"] for item in designs], - format_func=lambda value: next( - item["title"] for item in designs if item["id"] == value - ), - key=f"template-designs-{template['id']}", - ) - price = st.number_input( - "Retail price", - value=34.99, - min_value=0.01, - key=f"template-price-{template['id']}", - ) - if st.button( - "Use as starting point", - key=f"use-template-{template['id']}", - disabled=not selected, - ): - post( - f"/printful/templates/{template['id']}/drafts", - {"design_ids": selected, "retail_price": str(price)}, - timeout=120, - ) - st.success("Drafts created from the template.") - st.rerun() - - with sync_tab: - sync_products = get("/printful/sync-products") - if sync_products: - st.dataframe(sync_products, hide_index=True, width="stretch") - else: - st.info("No sync products exist in the configured Printful store.") -except Exception as error: - show_error(error) diff --git a/src/ecommerce_agent/dashboard/pages/8_Agent_Runs_Logs.py b/src/ecommerce_agent/dashboard/pages/8_Agent_Runs_Logs.py deleted file mode 100644 index 8f9dbfb..0000000 --- a/src/ecommerce_agent/dashboard/pages/8_Agent_Runs_Logs.py +++ /dev/null @@ -1,15 +0,0 @@ -import streamlit as st - -from ecommerce_agent.dashboard.client import get -from ecommerce_agent.dashboard.ui import page_header, show_error, table - -page_header("Agent Runs / Logs", "Workflow status, checkpoints, and audit events.") -try: - runs = get("/runs") - table(runs) - if runs: - run_id = st.selectbox("Run details", [run["id"] for run in runs]) - st.subheader("Events") - table(get(f"/runs/{run_id}/events")) -except Exception as error: - show_error(error) diff --git a/src/ecommerce_agent/dashboard/ui.py b/src/ecommerce_agent/dashboard/ui.py deleted file mode 100644 index 79458ec..0000000 --- a/src/ecommerce_agent/dashboard/ui.py +++ /dev/null @@ -1,48 +0,0 @@ -from typing import Any - -import streamlit as st - -from ecommerce_agent.dashboard.client import ApiError - - -def page_header(title: str, description: str) -> None: - st.set_page_config(page_title=title, layout="wide") - st.markdown( - """ - - """, - unsafe_allow_html=True, - ) - st.title(title) - st.caption(description) - - -def show_error(error: Exception) -> None: - if isinstance(error, ApiError): - st.error(str(error)) - else: - st.exception(error) - - -def table(rows: list[dict[str, Any]], *, empty: str = "Nothing here yet.") -> None: - if rows: - st.dataframe(rows, use_container_width=True, hide_index=True) - else: - st.info(empty) diff --git a/src/ecommerce_agent/web/__init__.py b/src/ecommerce_agent/web/__init__.py new file mode 100644 index 0000000..1275202 --- /dev/null +++ b/src/ecommerce_agent/web/__init__.py @@ -0,0 +1,11 @@ +# Import screen modules for their side-effecting route registrations. +from ecommerce_agent.web import ( # noqa: E402,F401 + design_studio, + etsy_posting, + group_b, + research_reports, + trend_inbox, +) +from ecommerce_agent.web.routes import router, templates + +__all__ = ["router", "templates"] diff --git a/src/ecommerce_agent/dashboard/design_lineage.py b/src/ecommerce_agent/web/design_lineage.py similarity index 100% rename from src/ecommerce_agent/dashboard/design_lineage.py rename to src/ecommerce_agent/web/design_lineage.py diff --git a/src/ecommerce_agent/web/design_studio.py b/src/ecommerce_agent/web/design_studio.py new file mode 100644 index 0000000..f874757 --- /dev/null +++ b/src/ecommerce_agent/web/design_studio.py @@ -0,0 +1,166 @@ +"""Design Studio screen — composer + version-tracked library + focus detail. + +Library "lines" are lineage paths over image generations (reusing the lineage +helpers). Per the reconciliation inventory, WORKSHOP SCORE and niche have no +backing data, so they render as explicit placeholders rather than fabricated +values. Everything else (versions, prompt, model/size/cost meta, delete) is real. +""" + +import uuid +from typing import Annotated, Any + +from fastapi import Depends, Request +from fastapi.responses import HTMLResponse, RedirectResponse +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from ecommerce_agent.api.serialization import serialize_model +from ecommerce_agent.db.models import ImageGeneration +from ecommerce_agent.db.session import get_session +from ecommerce_agent.web.design_lineage import branch_label, lineage_paths +from ecommerce_agent.web.routes import asset_url, router, shell_context, templates + +SessionDep = Annotated[AsyncSession, Depends(get_session)] + +SUBTITLE = "Create standalone designs, edit versions, and track each design line." + + +def _fmt_date(value: Any) -> str: + text = str(value or "") + return text[:10] if text else "—" + + +def _thumbs(path: list[dict[str, Any]]) -> list[dict[str, Any]]: + out = [] + for index, gen in enumerate(path[:4]): + out.append( + { + "label": f"v{index + 1}", + "url": asset_url(gen.get("asset_uri")), + "status": gen.get("status"), + } + ) + return out + + +def _line_view(path: list[dict[str, Any]]) -> dict[str, Any]: + leaf = path[-1] + return { + "line_id": leaf["id"], + "title": leaf.get("title") or "Untitled design", + "eyebrow": (leaf.get("model") or "design").upper(), + "versions": len(path), + "latest": f"v{len(path)}", + "updated": _fmt_date(leaf.get("created_at")), + "lineage": branch_label(path).replace("->", "→"), + "thumbs": _thumbs(path), + } + + +def _focus_view(path: list[dict[str, Any]], version_index: int) -> dict[str, Any]: + version_index = max(0, min(version_index, len(path) - 1)) + gen = path[version_index] + return { + "line_id": path[-1]["id"], + "line_title": path[-1].get("title") or "Untitled design", + "version": f"v{version_index + 1}", + "version_count": len(path), + "versions": [{"label": f"v{i + 1}", "index": i} for i in range(len(path))], + "active_index": version_index, + "thumb": asset_url(gen.get("asset_uri")), + "prompt": gen.get("prompt") or "", + "status": gen.get("status"), + "meta": [ + str(gen.get("id", ""))[:12], + gen.get("model") or "—", + gen.get("quality") or "—", + f"{gen.get('width', '?')} × {gen.get('height', '?')}", + (gen.get("output_format") or "").upper(), + f"${gen.get('estimated_cost_usd', '0')}", + ], + "bg_friendly": gen.get("background_removal_friendly"), + "gen_id": str(gen.get("id")), + "download_url": asset_url(gen.get("asset_uri")), + } + + +async def _all_paths(session: AsyncSession) -> list[list[dict[str, Any]]]: + rows = ( + await session.scalars( + select(ImageGeneration).order_by(ImageGeneration.created_at.desc()).limit(500) + ) + ).all() + gens = [serialize_model(r) for r in rows] + return lineage_paths(gens) + + +@router.get("/design-studio", response_class=HTMLResponse) +async def design_studio( + request: Request, + session: SessionDep, + sel: str | None = None, + prompt: str = "", + title: str = "", +) -> HTMLResponse: + paths = await _all_paths(session) + lines = [_line_view(p) for p in paths] + selected_path = next((p for p in paths if p[-1]["id"] == sel), paths[0] if paths else None) + focus = _focus_view(selected_path, len(selected_path) - 1) if selected_path else None + total_versions = sum(len(p) for p in paths) + ctx = shell_context( + request, + active="Design Studio", + title="Design Studio", + subtitle=SUBTITLE, + primary={"label": "New design", "icon": "plus"}, + lines=lines, + focus=focus, + selected_line=focus["line_id"] if focus else None, + line_count=len(paths), + version_count=total_versions, + prefill_prompt=prompt, + prefill_title=title, + ) + return templates.TemplateResponse(request=request, name="design_studio.html", context=ctx) + + +@router.get("/design-studio/focus/{leaf_id}", response_class=HTMLResponse) +async def studio_focus( + request: Request, session: SessionDep, leaf_id: str, v: int = -1 +) -> HTMLResponse: + paths = await _all_paths(session) + path = next((p for p in paths if p[-1]["id"] == leaf_id), None) + focus = None + if path is not None: + focus = _focus_view(path, v if v >= 0 else len(path) - 1) + return templates.TemplateResponse( + request=request, + name="partials/_studio_focus.html", + context={"request": request, "focus": focus}, + ) + + +@router.post("/design-studio/generate", response_class=HTMLResponse) +async def generate(request: Request, session: SessionDep) -> RedirectResponse: + services = getattr(request.app.state, "services", None) + if services is not None: + from ecommerce_agent.api.routes import create_image_generation + + try: + await create_image_generation(request=request, session=session, services=services) + except Exception: + pass + return RedirectResponse(url="/design-studio", status_code=303) + + +@router.post("/design-studio/lines/{leaf_id}/delete", response_class=HTMLResponse) +async def delete_line(request: Request, session: SessionDep, leaf_id: str) -> RedirectResponse: + paths = await _all_paths(session) + path = next((p for p in paths if p[-1]["id"] == leaf_id), None) + if path is not None: + for gen in reversed(path): # leaf → root, respecting the self-FK + obj = await session.get(ImageGeneration, uuid.UUID(gen["id"])) + if obj is not None: + await session.delete(obj) + await session.commit() + return RedirectResponse(url="/design-studio", status_code=303) diff --git a/src/ecommerce_agent/web/etsy_posting.py b/src/ecommerce_agent/web/etsy_posting.py new file mode 100644 index 0000000..53ad8e1 --- /dev/null +++ b/src/ecommerce_agent/web/etsy_posting.py @@ -0,0 +1,253 @@ +"""Etsy Posting screen — pipeline + listing editor over real posting sessions. + +The real workflow is a multi-step wizard (select → copy → images → review → +submit); the B1 design collapses it into a pipeline + single editor. We keep the +underlying data/actions: editing item copy, validating, and submitting. Pipeline +stages map real item statuses onto the design's Draft / In review / Scheduled / Live. +""" + +import uuid +from decimal import Decimal, InvalidOperation +from typing import Annotated, Any + +from fastapi import Depends, Form, Request +from fastapi.responses import HTMLResponse, RedirectResponse +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import selectinload + +from ecommerce_agent.api.posting_routes import _validate_item +from ecommerce_agent.db.models import PostingItem, PostingSession +from ecommerce_agent.db.session import get_session +from ecommerce_agent.web.routes import asset_url, router, shell_context, templates + +SessionDep = Annotated[AsyncSession, Depends(get_session)] + +SUBTITLE = "Move approved designs through draft, review, and scheduling onto Etsy listings." +TAG_CAP = 13 +IMAGE_CAP = 10 + +# Design pipeline stages ← real PostingItemStatus. +STAGES = [("Draft", "sub"), ("In review", "warn"), ("Scheduled", "accent"), ("Live", "ok")] +STAGE_FOR = { + "selected": "Draft", + "copy_ready": "Draft", + "images_ready": "Draft", + "ready": "In review", + "failed": "In review", + "unknown": "In review", + "submitting": "Scheduled", + "submitted": "Live", +} + + +def _item_title(item: PostingItem) -> str: + return item.title or item.template_snapshot.get("name") or "Untitled listing" + + +def _first_image(item: PostingItem) -> str: + for image in sorted(item.images, key=lambda i: i.position): + url = image.source_url or image.asset_uri + if url: + return asset_url(url) + return "" + + +def _card_view(item: PostingItem) -> dict[str, Any]: + return { + "id": str(item.id), + "title": _item_title(item), + "price": f"{item.retail_price:.2f}", + "stage": STAGE_FOR.get(item.status.value, "Draft"), + "image": _first_image(item), + } + + +def _readiness(item: PostingItem) -> list[dict[str, Any]]: + approved = [i for i in item.images if i.approved] + title = item.title or "" + title_ok = bool(title) and len(title) <= 140 + tags_ok = 1 <= len(item.tags) <= TAG_CAP + img_ok = len(approved) >= 1 + cat_ok = item.taxonomy_id is not None + desc_ok = bool(item.description) + desc_words = len((item.description or "").split()) + return [ + {"name": "Title", "ok": title_ok, "value": f"{len(item.title or '')} chars"}, + {"name": "Tags", "ok": tags_ok, "value": f"{len(item.tags)}/{TAG_CAP}"}, + {"name": "Images", "ok": img_ok, "value": f"{len(item.images)} of {IMAGE_CAP}"}, + {"name": "Category", "ok": cat_ok, "value": str(item.taxonomy_id or "—")}, + {"name": "Description", "ok": desc_ok, "value": f"{desc_words} words"}, + ] + + +def _editor_view(item: PostingItem) -> dict[str, Any]: + images = sorted(item.images, key=lambda i: i.position) + return { + "id": str(item.id), + "session_id": str(item.session_id), + "title": _item_title(item), + "raw_title": item.title or "", + "stage": STAGE_FOR.get(item.status.value, "Draft"), + "stage_tone": dict(STAGES).get(STAGE_FOR.get(item.status.value, "Draft"), "sub"), + "status": item.status.value, + "price": f"{item.retail_price:.2f}", + "quantity": item.quantity, + "taxonomy_id": item.taxonomy_id or "", + "category": item.template_snapshot.get("category") or "Clothing › Tops & Tees › T-shirts", + "tags": item.tags, + "tag_cap": TAG_CAP, + "description": item.description or "", + "materials": item.materials, + "images": [ + { + "url": asset_url(i.source_url or i.asset_uri), + "approved": i.approved, + "source": i.source.value, + } + for i in images + ], + "image_count": len(images), + "image_cap": IMAGE_CAP, + "readiness": _readiness(item), + "errors": item.validation_errors, + } + + +async def _load_items(session: AsyncSession) -> list[PostingItem]: + sessions = ( + await session.scalars( + select(PostingSession) + .options(selectinload(PostingSession.items).selectinload(PostingItem.images)) + .order_by(PostingSession.updated_at.desc()) + ) + ).all() + items: list[PostingItem] = [] + for s in sessions: + items.extend(s.items) + return items + + +async def _get_item(session: AsyncSession, item_id: str) -> PostingItem | None: + try: + pk = uuid.UUID(item_id) + except ValueError: + return None + stmt = select(PostingItem).options(selectinload(PostingItem.images)).where(PostingItem.id == pk) + return (await session.scalars(stmt)).first() + + +def _pipeline(items: list[PostingItem]) -> list[dict[str, Any]]: + cards = [_card_view(i) for i in items] + groups = [] + for name, tone in STAGES: + members = [c for c in cards if c["stage"] == name] + groups.append({"name": name, "tone": tone, "cards": members}) + return groups + + +@router.get("/etsy-posting", response_class=HTMLResponse) +async def etsy_posting( + request: Request, session: SessionDep, sel: str | None = None +) -> HTMLResponse: + items = await _load_items(session) + selected_item = next((i for i in items if str(i.id) == sel), items[0] if items else None) + selected = _editor_view(selected_item) if selected_item else None + ctx = shell_context( + request, + active="Etsy Posting", + title="Etsy Posting", + subtitle=SUBTITLE, + primary={"label": "New listing", "icon": "plus"}, + pipeline=_pipeline(items), + selected=selected, + has_items=bool(items), + ) + return templates.TemplateResponse(request=request, name="etsy_posting.html", context=ctx) + + +@router.get("/etsy-posting/detail/{item_id}", response_class=HTMLResponse) +async def etsy_detail(request: Request, session: SessionDep, item_id: str) -> HTMLResponse: + item = await _get_item(session, item_id) + return templates.TemplateResponse( + request=request, + name="partials/_listing_editor.html", + context={"request": request, "selected": _editor_view(item) if item else None}, + ) + + +@router.post("/etsy-posting/items/{item_id}/save", response_class=HTMLResponse) +async def save_item( + request: Request, + session: SessionDep, + item_id: str, + title: Annotated[str, Form()] = "", + description: Annotated[str, Form()] = "", + retail_price: Annotated[str, Form()] = "", + quantity: Annotated[int, Form()] = 999, + taxonomy_id: Annotated[str, Form()] = "", + tags: Annotated[list[str], Form()] = [], # noqa: B006 + materials: Annotated[list[str], Form()] = [], # noqa: B006 +) -> HTMLResponse: + item = await _get_item(session, item_id) + if item is not None: + item.title = title.strip() or None + item.description = description.strip() or None + try: + item.retail_price = Decimal(retail_price) if retail_price else item.retail_price + except InvalidOperation: + pass + item.quantity = quantity + item.taxonomy_id = int(taxonomy_id) if taxonomy_id.strip().isdigit() else None + item.tags = [t.strip() for t in tags if t.strip()][:TAG_CAP] + item.materials = [m.strip() for m in materials if m.strip()] + await session.commit() + item = await _get_item(session, item_id) + return templates.TemplateResponse( + request=request, + name="partials/_listing_editor.html", + context={ + "request": request, + "selected": _editor_view(item) if item else None, + "saved": True, + }, + ) + + +@router.post("/etsy-posting/sessions/{session_id}/validate", response_class=HTMLResponse) +async def validate_session( + request: Request, session: SessionDep, session_id: str +) -> RedirectResponse: + stmt = ( + select(PostingSession) + .options(selectinload(PostingSession.items).selectinload(PostingItem.images)) + .where(PostingSession.id == uuid.UUID(session_id)) + ) + posting = (await session.scalars(stmt)).first() + if posting is not None: + for item in posting.items: + item.validation_errors = _validate_item(item, posting.defaults) + await session.commit() + return RedirectResponse(url="/etsy-posting", status_code=303) + + +@router.post("/etsy-posting/sessions/{session_id}/submit", response_class=HTMLResponse) +async def submit_session( + request: Request, session: SessionDep, session_id: str +) -> RedirectResponse: + # Delegate to the real submit pipeline (needs the runtime service container). + from ecommerce_agent.api.posting_routes import submit_posting_session + from ecommerce_agent.api.schemas import PostingSubmitRequest + + services = getattr(request.app.state, "services", None) + if services is not None: + try: + await submit_posting_session( + session_id=uuid.UUID(session_id), + body=PostingSubmitRequest(item_ids=None), + session=session, + services=services, + ) + except Exception: # surfaced on the page via validation_errors / status + pass + return RedirectResponse(url="/etsy-posting", status_code=303) diff --git a/src/ecommerce_agent/web/group_b.py b/src/ecommerce_agent/web/group_b.py new file mode 100644 index 0000000..9648334 --- /dev/null +++ b/src/ecommerce_agent/web/group_b.py @@ -0,0 +1,158 @@ +"""Group B screens (no design reference) — restyled in the B1 language. + +Product Drafts, Analytics, Agent Runs · Logs, Printful Workshop. These reuse the +JSON layer's queries and render B1 cards/tables. +""" + +from typing import Annotated, Any + +from fastapi import Depends, Request +from fastapi.responses import HTMLResponse +from sqlalchemy import desc, select +from sqlalchemy.ext.asyncio import AsyncSession + +from ecommerce_agent.db.models import AnalyticsSnapshot, MerchDesign, Product +from ecommerce_agent.db.session import get_session +from ecommerce_agent.web.routes import router, shell_context, templates + +SessionDep = Annotated[AsyncSession, Depends(get_session)] + +_STATUS_TONE = { + "published": "ok", + "ready_for_manual_publish": "accent", + "rejected": "warn", + "failed": "warn", +} + + +def _tone(status: str) -> str: + return _STATUS_TONE.get(status, "sub") + + +@router.get("/product-drafts", response_class=HTMLResponse) +async def product_drafts(request: Request, session: SessionDep) -> HTMLResponse: + stmt = select(Product).order_by(desc(Product.created_at)).limit(200) + rows = (await session.scalars(stmt)).all() + drafts = [ + { + "id": str(p.id), + "title": (p.listing_copy or {}).get("title") or p.title, + "audience": p.audience, + "product_type": p.product_type, + "status": p.status.value, + "tone": _tone(p.status.value), + "price": f"{p.retail_price:.2f}", + "currency": p.currency, + } + for p in rows + if p.listing_copy is not None + ] + ctx = shell_context( + request, + active="Product Drafts", + title="Product Drafts", + subtitle="Concepts with generated listing copy, in progress toward Etsy.", + drafts=drafts, + ) + return templates.TemplateResponse(request=request, name="product_drafts.html", context=ctx) + + +@router.get("/analytics", response_class=HTMLResponse) +async def analytics(request: Request, session: SessionDep) -> HTMLResponse: + rows = ( + await session.scalars( + select(AnalyticsSnapshot).order_by(desc(AnalyticsSnapshot.captured_at)).limit(200) + ) + ).all() + snapshots = [] + for s in rows: + product = await session.get(Product, s.product_id) + snapshots.append( + { + "title": ((product.listing_copy or {}).get("title") if product else None) + or (product.title if product else "—"), + "captured_at": s.captured_at.strftime("%b %d") if s.captured_at else "—", + "views": s.views, + "favorites": s.favorites, + "orders": s.orders, + "revenue": f"{s.revenue:.2f}", + "conversion": f"{float(s.conversion_rate) * 100:.1f}%", + "fulfillment": s.fulfillment_status, + } + ) + ctx = shell_context( + request, + active="Analytics", + title="Analytics", + subtitle="Listing performance snapshots captured from live listings.", + snapshots=snapshots, + ) + return templates.TemplateResponse(request=request, name="analytics.html", context=ctx) + + +async def _runs(session: AsyncSession) -> list[dict[str, Any]]: + from ecommerce_agent.api.routes import list_runs + + items = await list_runs(session) + for item in items: + last = str(item.get("last_event", "")) + item["tone"] = ( + "warn" + if last in {"failed", "error"} + else ( + "ok" + if last in {"completed", "succeeded"} + else "accent" + if last == "running" + else "sub" + ) + ) + item["updated"] = str(item.get("updated_at", ""))[:19].replace("T", " ") + return items + + +@router.get("/agent-runs", response_class=HTMLResponse) +async def agent_runs(request: Request, session: SessionDep) -> HTMLResponse: + ctx = shell_context( + request, + active="Agent Runs · Logs", + title="Agent Runs · Logs", + subtitle="Active jobs and the recent pipeline event timeline.", + runs=await _runs(session), + ) + return templates.TemplateResponse(request=request, name="agent_runs.html", context=ctx) + + +@router.get("/agent-runs/table", response_class=HTMLResponse) +async def agent_runs_table(request: Request, session: SessionDep) -> HTMLResponse: + return templates.TemplateResponse( + request=request, + name="partials/_runs_table.html", + context={"request": request, "runs": await _runs(session)}, + ) + + +@router.get("/printful-workshop", response_class=HTMLResponse) +async def printful_workshop(request: Request, session: SessionDep) -> HTMLResponse: + stmt = select(MerchDesign).order_by(desc(MerchDesign.created_at)).limit(200) + rows = (await session.scalars(stmt)).all() + designs = [ + { + "title": d.title, + "asset": d.asset_uri, + "dims": f"{d.width} × {d.height}", + "source": d.source_type, + "print_ready": bool(d.print_asset_uri), + } + for d in rows + ] + settings = request.app.state.settings + ctx = shell_context( + request, + active="Printful Workshop", + title="Printful Workshop", + subtitle="Curate the merch design library and prepare Printful drafts.", + designs=designs, + printful_mode=getattr(settings, "printful_mode", "mock"), + ) + return templates.TemplateResponse(request=request, name="printful_workshop.html", context=ctx) diff --git a/src/ecommerce_agent/web/research_reports.py b/src/ecommerce_agent/web/research_reports.py new file mode 100644 index 0000000..3e7e808 --- /dev/null +++ b/src/ecommerce_agent/web/research_reports.py @@ -0,0 +1,229 @@ +"""Research Reports screen — list + reading pane over real research runs. + +The agent writes freeform markdown; we derive the TOC and section heads from its +headings and render the body with a minimal markdown subset (no new dependency). +Opportunity score is derived from the linked trend's apparel score where present. +""" + +import html as _html +import re +import uuid +from typing import Annotated, Any + +from fastapi import Depends, Form, Request +from fastapi.responses import HTMLResponse, RedirectResponse +from sqlalchemy import desc, select +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import selectinload + +from ecommerce_agent.db.models import ResearchRun, Trend +from ecommerce_agent.db.session import get_session +from ecommerce_agent.domain.enums import ResearchRunStatus +from ecommerce_agent.jobs.queue import enqueue +from ecommerce_agent.web.routes import router, shell_context, templates + +SessionDep = Annotated[AsyncSession, Depends(get_session)] + +SUBTITLE = "Agent-written market reports — read, score, and turn opportunities into designs." +_NEW_REPORT_ATTRS = "onclick=\"document.getElementById('new-report').open=true\"" + +_STATUS = { + "completed": ("FINAL", "ok"), + "queued": ("QUEUED", "warn"), + "running": ("RUNNING", "warn"), + "failed": ("FAILED", "warn"), +} + + +def _inline(text: str) -> str: + out = _html.escape(text) + out = re.sub(r"\*\*(.+?)\*\*", r"\1", out) + out = re.sub(r"`(.+?)`", r"\1", out) + return out + + +def _render_body(lines: list[str]) -> str: + html: list[str] = [] + para: list[str] = [] + bullets: list[str] = [] + + def flush_para() -> None: + if para: + html.append("

" + " ".join(_inline(p) for p in para) + "

") + para.clear() + + def flush_list() -> None: + if bullets: + html.append("
    " + "".join(f"
  • {_inline(b)}
  • " for b in bullets) + "
") + bullets.clear() + + for raw in lines: + line = raw.rstrip() + if not line.strip(): + flush_para() + flush_list() + elif re.match(r"^\s*[-*]\s+", line): + flush_para() + bullets.append(re.sub(r"^\s*[-*]\s+", "", line)) + else: + flush_list() + para.append(line.strip()) + flush_para() + flush_list() + return "".join(html) + + +def parse_report(md: str | None) -> dict[str, Any]: + """Split markdown into (toc, sections, word count).""" + if not md: + return {"toc": [], "sections": [], "words": 0} + sections: list[dict[str, Any]] = [] + current: dict[str, Any] = {"title": "", "lines": []} + for line in md.splitlines(): + heading = re.match(r"^#{1,6}\s+(.*)$", line) + if heading: + if current["title"] or current["lines"]: + sections.append(current) + current = {"title": heading.group(1).strip(), "lines": []} + else: + current["lines"].append(line) + if current["title"] or current["lines"]: + sections.append(current) + rendered = [ + {"title": s["title"] or "Overview", "html": _render_body(s["lines"])} for s in sections + ] + toc = [s["title"] for s in rendered] + return {"toc": toc, "sections": rendered, "words": len(md.split())} + + +def _score_from_trend(trend: Trend | None) -> int | None: + if trend is None or trend.apparel_score is None: + return None + return round(float(trend.apparel_score) / 5 * 100) + + +def _list_view(run: ResearchRun) -> dict[str, Any]: + label, tone = _STATUS.get(run.status.value, (run.status.value.upper(), "sub")) + md = run.markdown_report or "" + return { + "id": str(run.id), + "title": run.topic_title or "Untitled report", + "niche": run.trend.niche if run.trend else "—", + "status_label": label, + "status_tone": tone, + "score": _score_from_trend(run.trend), + "date": run.created_at.strftime("%b %d") if run.created_at else "—", + "sections": len(re.findall(r"^#{1,6}\s+", md, flags=re.MULTILINE)), + } + + +def _read_view(run: ResearchRun) -> dict[str, Any]: + parsed = parse_report(run.markdown_report) + base = _list_view(run) + base.update( + { + "words": parsed["words"], + "toc": parsed["toc"], + "report_sections": parsed["sections"], + "error": run.error_message, + "can_download": run.status == ResearchRunStatus.COMPLETED and bool(run.markdown_report), + "can_retry": run.status == ResearchRunStatus.FAILED, + "context": run.additional_context or "", + } + ) + return base + + +async def _load(session: AsyncSession) -> list[ResearchRun]: + return list( + ( + await session.scalars( + select(ResearchRun) + .options(selectinload(ResearchRun.trend)) + .order_by(desc(ResearchRun.created_at)) + .limit(200) + ) + ).all() + ) + + +@router.get("/research-reports", response_class=HTMLResponse) +async def research_reports( + request: Request, session: SessionDep, sel: str | None = None, trend_id: str | None = None +) -> HTMLResponse: + runs = await _load(session) + views = [_list_view(r) for r in runs] + selected_run = next((r for r in runs if str(r.id) == sel), runs[0] if runs else None) + selected = _read_view(selected_run) if selected_run else None + + prefill = "" + if trend_id: + try: + trend = await session.get(Trend, uuid.UUID(trend_id)) + prefill = trend.title if trend else "" + except (ValueError, TypeError): + prefill = "" + + ctx = shell_context( + request, + active="Research Reports", + title="Research Reports", + subtitle=SUBTITLE, + primary={"label": "New report", "icon": "plus", "attrs": _NEW_REPORT_ATTRS}, + reports=views, + selected=selected, + prefill=prefill, + ) + return templates.TemplateResponse(request=request, name="research_reports.html", context=ctx) + + +@router.get("/research-reports/detail/{run_id}", response_class=HTMLResponse) +async def research_detail(request: Request, session: SessionDep, run_id: str) -> HTMLResponse: + try: + pk = uuid.UUID(run_id) + except ValueError: + pk = None + run = None + if pk: + stmt = select(ResearchRun).options(selectinload(ResearchRun.trend)) + run = (await session.scalars(stmt.where(ResearchRun.id == pk))).first() + return templates.TemplateResponse( + request=request, + name="partials/_report_reader.html", + context={"request": request, "selected": _read_view(run) if run else None}, + ) + + +@router.post("/research-reports/new", response_class=HTMLResponse) +async def new_report( + request: Request, + session: SessionDep, + topic_title: Annotated[str, Form()], + additional_context: Annotated[str, Form()] = "", + trend_id: Annotated[str, Form()] = "", +) -> RedirectResponse: + title = topic_title.strip() + if title: + run = ResearchRun( + topic_title=title, + additional_context=additional_context.strip(), + status=ResearchRunStatus.QUEUED, + trend_id=uuid.UUID(trend_id) if trend_id else None, + ) + session.add(run) + await session.flush() + await enqueue(session, "generate_research_report", run.id) + await session.commit() + return RedirectResponse(url="/research-reports", status_code=303) + + +@router.post("/research-reports/{run_id}/retry", response_class=HTMLResponse) +async def retry_report(request: Request, session: SessionDep, run_id: str) -> RedirectResponse: + run = await session.get(ResearchRun, uuid.UUID(run_id)) + if run and run.status == ResearchRunStatus.FAILED: + run.status = ResearchRunStatus.QUEUED + run.error_message = None + run.finished_at = None + await enqueue(session, "generate_research_report", run.id) + await session.commit() + return RedirectResponse(url=f"/research-reports?sel={run_id}", status_code=303) diff --git a/src/ecommerce_agent/web/routes.py b/src/ecommerce_agent/web/routes.py new file mode 100644 index 0000000..245ccd7 --- /dev/null +++ b/src/ecommerce_agent/web/routes.py @@ -0,0 +1,150 @@ +"""B1 Workshop web UI — server-rendered Jinja2 templates + HTMX. + +This router renders HTML for the human operator. It shares the FastAPI app's +service layer (via `get_services`) rather than calling the JSON API over HTTP. +""" + +from pathlib import Path +from typing import Any + +from fastapi import APIRouter, Request +from fastapi.responses import HTMLResponse, RedirectResponse +from fastapi.templating import Jinja2Templates +from markupsafe import Markup + +_TEMPLATES_DIR = Path(__file__).parent / "templates" +templates = Jinja2Templates(directory=str(_TEMPLATES_DIR)) + +router = APIRouter(include_in_schema=False) + +# ── Icons ──────────────────────────────────────────────────────────────────── +# Inline 20×20 stroke SVG paths, lifted verbatim from the B1 prototype +# (wschrome.jsx / studio.jsx) so the icon set matches pixel-for-pixel. +ICON_PATHS: dict[str, str] = { + "inbox": "M3 13h4l1 2h4l1-2h4M4 13l2-8h8l2 8", + "doc": "M5 2h7l3 3v11H5zM12 2v3h3", + "check": "M4 9l3 3 6-7", + "image": "M3 4h14v12H3zM3 13l4-4 3 3 3-4 4 5", + "box": "M3 6l7-3 7 3-7 3zM3 6v8l7 3 7-3V6M10 9v8", + "shirt": "M6 3l4 2 4-2 3 3-2 2v8H5V8L3 6z", + "tag": "M3 3h6l8 8-6 6-8-8zM6.5 6.5h.01", + "chart": "M3 16V4M3 16h13M7 13V9M11 13V6M15 13v-3", + "pulse": "M3 10h3l2-5 3 10 2-5h3", + "plus": "M10 4v12M4 10h12", + "search": "M9 3a6 6 0 100 12A6 6 0 009 3zM14 14l3 3", + "ref": "M4 14l4-5 3 3 2-3 3 5zM4 4h12v10H4z", + "bolt": "M11 2L4 11h5l-1 7 7-9h-5z", + "eye": "M2 10s3-5 8-5 8 5 8 5-3 5-8 5-8-5-8-5z M10 8a2 2 0 100 4 2 2 0 000-4z", + "down": "M10 3v10M5 9l5 5 5-5M4 17h12", + "branch": "M6 4v8a3 3 0 003 3h2M6 4a2 2 0 100-.1zM14 4a2 2 0 100-.1zM12 15a2 2 0 100 .1z", + "trash": "M4 5h12M8 5V3h4v2M6 5l1 12h6l1-12", + "arrow": "M4 10h12M11 5l5 5-5 5", + "clock": "M10 5v5l3 2M10 3a7 7 0 100 14 7 7 0 000-14z", + "flame": "M10 2c1 3 4 4 4 8a4 4 0 11-8 0c0-2 1-3 2-4 0 2 1 2 2 1 0-2-1-4-0-6z", + "cal": "M3 5h14v12H3zM3 8h14M7 3v3M13 3v3", + "star": "M10 2l2.4 5 5.6.6-4 3.8 1 5.6L10 14.8 4 17l1-5.6-4-3.8 5.6-.6z", + "sliders": "M4 6h8M14 6h2M4 14h2M8 14h8M11 4v4M6 12v4", + "ext": "M8 4H4v12h12v-4M12 4h4v4M16 4l-7 7", + "edit": "M4 13l9-9 3 3-9 9H4z", + "x": "M5 5l10 10M15 5L5 15", + "layers": "M10 3l7 4-7 4-7-4zM3 11l7 4 7-4", + "dollar": "M10 3v14M13 6c0-1.5-1.5-2-3-2s-3 .5-3 2 1.5 2 3 2 3 .5 3 2-1.5 2-3 2-3-.5-3-2", +} + + +def render_icon(name: str, size: int = 16, sw: float = 1.6) -> Markup: + path = ICON_PATHS.get(name, "") + return Markup( + f'' + ) + + +templates.env.globals["icon"] = render_icon + + +def asset_url(uri: str | None) -> str: + """Resolve a stored asset reference to a browser URL (mirrors the old client).""" + if not uri: + return "" + if uri.startswith(("http://", "https://", "/")): + return uri + return f"/api/assets/{uri}" + + +templates.env.globals["asset_url"] = asset_url + +# ── Navigation ─────────────────────────────────────────────────────────────── +# Design's 9-item nav minus Approval Queue (dropped per product decision). +NAV: list[dict[str, str]] = [ + {"label": "Trend Inbox", "slug": "trend-inbox", "icon": "inbox"}, + {"label": "Research Reports", "slug": "research-reports", "icon": "doc"}, + {"label": "Design Studio", "slug": "design-studio", "icon": "image"}, + {"label": "Product Drafts", "slug": "product-drafts", "icon": "box"}, + {"label": "Printful Workshop", "slug": "printful-workshop", "icon": "shirt"}, + {"label": "Etsy Posting", "slug": "etsy-posting", "icon": "tag"}, + {"label": "Analytics", "slug": "analytics", "icon": "chart"}, + {"label": "Agent Runs · Logs", "slug": "agent-runs", "icon": "pulse"}, +] + +# Topbar model dropdowns are cosmetic selectors (the app uses a fixed +# server-side model). Options mirror the prototype's WS_DATA.models. +TEXT_MODELS = [ + "gpt-5", + "gpt-5-mini", + "claude-opus-4.5", + "claude-sonnet-4.5", + "gemini-2.5-pro", + "llama-4-70b", +] +IMAGE_MODELS = [ + "gpt-image-2", + "gpt-image-2-mini", + "dall-e-3", + "flux-1.1-pro", + "imagen-4", + "sd-3.5-large", +] + + +def shell_context( + request: Request, + *, + active: str, + title: str, + subtitle: str = "", + primary: dict[str, str] | None = None, + badges: dict[str, str] | None = None, + **extra: Any, +) -> dict[str, Any]: + """Build the template context shared by every screen (sidebar + topbar).""" + settings = request.app.state.settings + cap = getattr(settings, "image_generation_cap_usd", None) or getattr(settings, "cap_usd", None) + ctx: dict[str, Any] = { + "request": request, + "app_name": "WORKSHOP", + "nav": NAV, + "active": active, + "title": title, + "subtitle": subtitle, + "primary": primary, + "badges": badges or {}, + "text_models": TEXT_MODELS, + "image_models": IMAGE_MODELS, + "text_model": TEXT_MODELS[0], + "image_model": IMAGE_MODELS[0], + "integrations": "LIVE", + "cap_usd": f"{float(cap):.2f}" if cap is not None else "0.50", + } + ctx.update(extra) + return ctx + + +# ── Routes ─────────────────────────────────────────────────────────────────-- +# Screen routes live in their own modules (imported by web/__init__). + + +@router.get("/", response_class=HTMLResponse) +async def home() -> RedirectResponse: + return RedirectResponse(url="/trend-inbox") diff --git a/src/ecommerce_agent/web/static/app.js b/src/ecommerce_agent/web/static/app.js new file mode 100644 index 0000000..8934125 --- /dev/null +++ b/src/ecommerce_agent/web/static/app.js @@ -0,0 +1,152 @@ +// B1 Workshop — minimal client behavior: theme toggle + topbar dropdowns. + +(function () { + // ── Theme toggle ────────────────────────────────────────────────────────── + function currentTheme() { + return document.documentElement.getAttribute("data-theme") || "light"; + } + function syncToggleLabels() { + var mode = currentTheme(); + document.querySelectorAll(".ws-toggle .ws-toggle-label").forEach(function (el) { + el.textContent = mode; + }); + } + document.addEventListener("click", function (e) { + var toggle = e.target.closest(".ws-toggle"); + if (!toggle) return; + var next = currentTheme() === "dark" ? "light" : "dark"; + document.documentElement.setAttribute("data-theme", next); + try { localStorage.setItem("ws-theme", next); } catch (err) {} + syncToggleLabels(); + }); + + // ── Dropdowns (one open at a time; close on outside click) ───────────────── + function closeAllMenus(except) { + document.querySelectorAll(".ws-dd-wrap").forEach(function (wrap) { + if (wrap === except) return; + var menu = wrap.querySelector(".ws-dd-menu"); + var btn = wrap.querySelector(".ws-dd"); + if (menu) menu.hidden = true; + if (btn) btn.classList.remove("is-open"); + }); + } + document.addEventListener("click", function (e) { + var btn = e.target.closest(".ws-dd-wrap > .ws-dd"); + if (btn) { + var wrap = btn.parentElement; + var menu = wrap.querySelector(".ws-dd-menu"); + var open = menu && menu.hidden; + closeAllMenus(wrap); + if (menu) { menu.hidden = !open; btn.classList.toggle("is-open", open); } + return; + } + var opt = e.target.closest(".ws-opt"); + if (opt) { + var wrap2 = opt.closest(".ws-dd-wrap"); + var value = opt.getAttribute("data-value"); + wrap2.querySelectorAll(".ws-opt").forEach(function (o) { + o.classList.toggle("is-on", o === opt); + var active = o.querySelector(".ws-opt-active"); + if (o === opt && !active) { + var span = document.createElement("span"); + span.className = "ws-opt-active"; + span.textContent = "ACTIVE"; + o.appendChild(span); + } else if (o !== opt && active) { + active.remove(); + } + }); + var valueEl = wrap2.querySelector(".ws-dd-value"); + if (valueEl) valueEl.textContent = value; + var key = wrap2.getAttribute("data-dd"); + try { if (key) localStorage.setItem("ws-model-" + key, value); } catch (err) {} + closeAllMenus(null); + return; + } + // click elsewhere closes any open menu + if (!e.target.closest(".ws-dd-menu")) closeAllMenus(null); + }); + + // ── Master/detail selection highlight ───────────────────────────────────── + document.addEventListener("click", function (e) { + var card = e.target.closest("[data-detail]"); + if (!card) return; + var master = card.closest("[data-master]"); + if (!master) return; + master.querySelectorAll("[data-detail].is-selected").forEach(function (el) { + el.classList.remove("is-selected"); + }); + card.classList.add("is-selected"); + }); + + // ── Composer aspect → width/height ──────────────────────────────────────── + document.addEventListener("change", function (e) { + var radio = e.target.closest("[data-aspect-form] input[name='aspect']"); + if (!radio) return; + var form = radio.closest("[data-aspect-form]"); + form.querySelectorAll(".ws-aspect .ws-seg-cell").forEach(function (cell) { + cell.classList.toggle("is-on", cell.contains(radio)); + }); + var w = form.querySelector("[data-ds-width]"); + var h = form.querySelector("[data-ds-height]"); + if (w) w.value = radio.getAttribute("data-w"); + if (h) h.value = radio.getAttribute("data-h"); + }); + + // ── Removable tags editor ───────────────────────────────────────────────── + function updateTagCount(form) { + var count = form.querySelectorAll("[data-tag-list] .ws-tag:not(.ws-tag-add)").length; + var el = form.querySelector("[data-tag-count]"); + if (el) el.textContent = count; + } + document.addEventListener("click", function (e) { + var rm = e.target.closest("[data-remove-tag]"); + if (!rm) return; + var form = rm.closest("[data-tags]"); + rm.closest(".ws-tag").remove(); + if (form) updateTagCount(form); + }); + document.addEventListener("keydown", function (e) { + var input = e.target.closest("[data-tag-input]"); + if (!input || e.key !== "Enter") return; + e.preventDefault(); + var value = input.value.trim(); + if (!value) return; + var form = input.closest("[data-tags]"); + var addCell = input.closest(".ws-tag-add"); + var tag = document.createElement("span"); + tag.className = "ws-tag"; + var hidden = document.createElement("input"); + hidden.type = "hidden"; + hidden.name = "tags"; + hidden.value = value; + tag.appendChild(hidden); + tag.appendChild(document.createTextNode(value)); + var x = document.createElement("button"); + x.type = "button"; + x.className = "ws-tag-x"; + x.setAttribute("data-remove-tag", ""); + x.textContent = "×"; + tag.appendChild(x); + addCell.parentNode.insertBefore(tag, addCell); + input.value = ""; + if (form) updateTagCount(form); + }); + + // restore persisted model selections + theme label on load + function restore() { + document.querySelectorAll(".ws-dd-wrap[data-dd]").forEach(function (wrap) { + var key = wrap.getAttribute("data-dd"); + var saved; + try { saved = localStorage.getItem("ws-model-" + key); } catch (err) {} + if (!saved) return; + var match = wrap.querySelector('.ws-opt[data-value="' + saved + '"]'); + if (match) match.click(); + var menu = wrap.querySelector(".ws-dd-menu"); + if (menu) menu.hidden = true; + }); + syncToggleLabels(); + } + if (document.readyState !== "loading") restore(); + else document.addEventListener("DOMContentLoaded", restore); +})(); diff --git a/src/ecommerce_agent/web/static/b1.css b/src/ecommerce_agent/web/static/b1.css new file mode 100644 index 0000000..024f0eb --- /dev/null +++ b/src/ecommerce_agent/web/static/b1.css @@ -0,0 +1,569 @@ +/* B1 "Concrete · Amber Dark" design system. + Tokens per docs/01; light is default, dark via [data-theme="dark"]. + `--line` is the structural border (full ink in light, faint in dark — matching + the prototype's `c.line`); `--ink` stays the text color. */ + +:root, +:root[data-theme="light"] { + --bg: #e9e6dd; + --panel: #f7f5ef; + --panel2: #efece3; + --ink: #16130d; + --sub: #736d60; + --faint: #a39c8c; + --line: #16130d; + --hairline: rgba(22, 19, 13, 0.14); + --accent: #e23c0c; + --ok: #3f7a44; + --warn: #b8761f; + --on-accent: #f7f5ef; + --hover: rgba(22, 19, 13, 0.06); + --thumb-bg: #dedace; +} + +:root[data-theme="dark"] { + --bg: #1c1108; + --panel: #271810; + --panel2: #312115; + --ink: #f4ece1; + --sub: #a89a84; + --faint: #6e624d; + --line: rgba(244, 236, 225, 0.03); + --hairline: rgba(244, 236, 225, 0.13); + --accent: #fb5a28; + --ok: #6cae66; + --warn: #e0a24a; + --on-accent: #1c1108; + --hover: rgba(244, 236, 225, 0.06); + --thumb-bg: #382616; +} + +:root { + --radius: 0; + --border-w: 1.5px; + --border: var(--border-w) solid var(--line); + --hair: 1px solid var(--hairline); + --hardshadow: 5px 5px 0 var(--accent); + --display: "Archivo", system-ui, sans-serif; + --body: "Archivo", system-ui, sans-serif; + --mono: "JetBrains Mono", ui-monospace, monospace; +} + +* { box-sizing: border-box; } + +html, body { height: 100%; } + +body { + margin: 0; + background: var(--bg); + color: var(--ink); + font-family: var(--body); + -webkit-font-smoothing: antialiased; + font-size: 14px; +} + +.ws-display { + font-family: var(--display); + font-weight: 800; + text-transform: uppercase; + letter-spacing: -0.02em; + line-height: 0.98; + color: var(--ink); + margin: 0; +} + +.ws-mono-label { + font-family: var(--mono); + font-size: 10.5px; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--faint); +} + +.ws-icon { display: block; } + +/* ── App shell ───────────────────────────────────────────────────────────── */ +.ws-app { display: flex; height: 100vh; overflow: hidden; } + +.ws-sidebar { + width: 250px; + flex-shrink: 0; + background: var(--panel); + border-right: var(--border); + display: flex; + flex-direction: column; +} + +.ws-brand { padding: 22px 20px; border-bottom: var(--border); } +.ws-brand-row { display: flex; align-items: center; gap: 9px; } +.ws-brand-tile { + width: 26px; height: 26px; flex-shrink: 0; + background: var(--accent); color: var(--on-accent); + display: flex; align-items: center; justify-content: center; +} +.ws-brand-mark { + font-family: var(--display); font-weight: 800; text-transform: uppercase; + letter-spacing: 0.02em; font-size: 19px; line-height: 0.98; +} +.ws-brand-sub { + font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.08em; + color: var(--faint); margin-top: 8px; text-transform: uppercase; +} + +.ws-nav { padding: 12px; flex: 1; display: flex; flex-direction: column; gap: 2px; overflow-y: auto; } +.ws-nav-item { + display: flex; align-items: center; gap: 11px; padding: 9px 11px; + color: var(--sub); text-decoration: none; cursor: pointer; + font-family: var(--body); font-size: 13.5px; font-weight: 500; + transition: background .12s, color .12s, transform .1s; +} +.ws-nav-item .ws-nav-icon { display: flex; opacity: 0.85; } +.ws-nav-label { flex: 1; } +.ws-nav-item:hover { background: var(--hover); color: var(--ink); } +.ws-nav-item:not(.is-active):hover { transform: translateX(2px); } +.ws-nav-item:active { transform: translateX(2px) scale(.99); } +.ws-nav-item.is-active { + background: var(--accent); color: var(--on-accent); font-weight: 600; +} +.ws-nav-item.is-active .ws-nav-icon { opacity: 1; } +.ws-nav-badge { + font-family: var(--mono); font-size: 10.5px; font-weight: 700; line-height: 1; + padding: 2px 5px; background: var(--accent); color: var(--on-accent); +} +.ws-nav-item.is-active .ws-nav-badge { background: var(--on-accent); color: var(--accent); } + +.ws-side-footer { padding: 14px 16px; border-top: var(--border); display: flex; flex-direction: column; gap: 9px; } +.ws-budget-row { display: flex; justify-content: space-between; align-items: center; } +.ws-budget-val { font-family: var(--mono); font-size: 11px; color: var(--sub); } +.ws-meter { height: 5px; background: var(--panel2); border: var(--hair); } +.ws-meter-fill { height: 100%; background: var(--accent); } +.ws-operator { display: flex; align-items: center; gap: 9px; margin-top: 4px; } +.ws-operator-tile { + width: 28px; height: 28px; border: var(--border); background: var(--panel2); + display: flex; align-items: center; justify-content: center; + font-family: var(--mono); font-size: 12px; font-weight: 700; color: var(--ink); +} +.ws-operator-text { line-height: 1.2; } +.ws-operator-name { font-size: 12.5px; font-weight: 600; color: var(--ink); } +.ws-operator-meta { font-family: var(--mono); font-size: 10.5px; color: var(--faint); } + +.ws-main { flex: 1; display: flex; flex-direction: column; overflow: hidden; min-width: 0; } + +.ws-topbar { + height: 74px; flex-shrink: 0; border-bottom: var(--border); background: var(--panel); + display: flex; align-items: center; padding: 0 24px; gap: 20px; +} +.ws-topbar-title { min-width: 0; } +.ws-h1 { font-size: 23px; } +.ws-subtitle { font-family: var(--body); font-size: 12.5px; color: var(--sub); margin-top: 3px; } +.ws-topbar-spacer { flex: 1; } +.ws-topbar-cluster { display: flex; gap: 7px; align-items: center; } + +.ws-content { flex: 1; display: flex; overflow: hidden; min-height: 0; } + +/* ── Chip ────────────────────────────────────────────────────────────────── */ +.ws-chip { + display: inline-flex; align-items: center; gap: 5px; + font-family: var(--mono); font-size: 11px; letter-spacing: 0.04em; + text-transform: uppercase; color: var(--sub); + padding: 3px 7px; border: 1px solid var(--hairline); line-height: 1; white-space: nowrap; +} +.ws-chip-dot { width: 6px; height: 6px; border-radius: 6px; background: currentColor; } +.ws-chip.tone-live, .ws-chip.tone-accent { color: var(--accent); border-color: var(--accent); } +.ws-chip.tone-ok { color: var(--ok); border-color: var(--ok); } +.ws-chip.tone-warn { color: var(--warn); border-color: var(--warn); } +.ws-chip.tone-sub { color: var(--sub); border-color: var(--sub); } + +/* ── Buttons ─────────────────────────────────────────────────────────────── */ +.ws-btn { + font-family: var(--mono); font-size: 12px; font-weight: 600; letter-spacing: 0.06em; + text-transform: uppercase; padding: 11px 16px; line-height: 1; + display: inline-flex; align-items: center; gap: 7px; + border: var(--border); cursor: pointer; user-select: none; background: transparent; color: var(--ink); + transition: background .14s, color .14s, border-color .14s, transform .08s, filter .14s, box-shadow .12s; +} +.ws-btn:active { transform: translateY(1px) scale(.985); } +.ws-primary { background: var(--accent); color: var(--on-accent); border-color: var(--accent); } +.ws-primary:hover { filter: saturate(1.1) brightness(.97); transform: translateY(-1px); box-shadow: var(--hardshadow); } +.ws-primary:active { transform: translateY(0) scale(.985); filter: saturate(1.12) brightness(.9); box-shadow: none; } +.ws-ghost:hover { background: var(--hover); border-color: var(--accent); color: var(--ink); } +.ws-ghost:active { background: var(--hover); } +.ws-destructive { color: var(--warn); border-color: var(--warn); } +.ws-destructive:hover { background: var(--hover); border-color: var(--warn); color: var(--warn); } +/* compact icon-only button */ +.ws-icon-btn { + display: inline-flex; align-items: center; justify-content: center; padding: 6px; + border: 1px solid var(--hairline); background: transparent; cursor: pointer; line-height: 1; + color: var(--ink); +} +.ws-icon-btn.is-warn { color: var(--warn); } + +/* ── Field label + input ─────────────────────────────────────────────────── */ +.ws-field-label { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 7px; gap: 10px; } +.ws-field-name { + font-family: var(--mono); font-size: 11px; letter-spacing: 0.07em; text-transform: uppercase; + color: var(--sub); font-weight: 600; white-space: nowrap; +} +.ws-field-hint { font-family: var(--mono); font-size: 11px; color: var(--faint); white-space: nowrap; } +.ws-input { + width: 100%; background: var(--panel); border: var(--border); color: var(--ink); + font-family: var(--body); font-size: 13.5px; padding: 10px 12px; outline: none; +} +textarea.ws-input { line-height: 1.5; font-size: 13px; resize: vertical; } +.ws-input::placeholder { color: var(--faint); } + +/* ── Score bar ───────────────────────────────────────────────────────────── */ +.ws-score { display: flex; align-items: center; gap: 8px; } +.ws-score-track { flex: 1; min-width: 70px; height: 6px; background: var(--panel2); border: var(--hair); overflow: hidden; } +.ws-score-fill { height: 100%; background: var(--accent); } +.ws-score-num { font-family: var(--mono); font-size: 12px; font-weight: 700; color: var(--ink); font-variant-numeric: tabular-nums; } + +/* ── Thumb placeholder ───────────────────────────────────────────────────── */ +.ws-thumb { + position: relative; width: 100%; background: var(--thumb-bg); + border: var(--border); overflow: hidden; cursor: pointer; + transition: transform .18s, filter .18s, box-shadow .18s; +} +.ws-thumb-hatch { + position: absolute; inset: 0; + background: repeating-linear-gradient(45deg, color-mix(in srgb, var(--ink) 8%, transparent) 0 2px, transparent 2px 9px); +} +.ws-thumb:hover { transform: scale(1.04); z-index: 3; box-shadow: var(--hardshadow); } +.ws-thumb:active { transform: scale(.99); } +.ws-thumb-sub { + position: absolute; left: 7px; top: 6px; font-family: var(--mono); font-size: 9px; + letter-spacing: 0.06em; color: var(--sub); background: var(--thumb-bg); padding: 1px 4px; border: var(--hair); +} +.ws-thumb-label { + position: absolute; right: 7px; bottom: 6px; font-family: var(--mono); font-weight: 700; + font-size: 11px; color: var(--accent); +} +.ws-thumb.is-big .ws-thumb-sub { font-size: 11px; } +.ws-thumb.is-big .ws-thumb-label { font-size: 13px; } +.ws-thumb-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; } + +/* ── Checkbox ────────────────────────────────────────────────────────────── */ +.ws-check-row { display: flex; align-items: center; gap: 10px; cursor: pointer; } +.ws-check { + width: 18px; height: 18px; border: var(--border); background: transparent; color: var(--on-accent); + display: flex; align-items: center; justify-content: center; flex-shrink: 0; + transition: transform .08s, filter .12s; +} +.ws-check.is-on { background: var(--accent); } +.ws-check-row:hover .ws-check { filter: brightness(1.06); } +.ws-check-row:active .ws-check { transform: scale(.92); } +.ws-check-label { font-size: 13px; color: var(--ink); } + +/* ── Segmented control ───────────────────────────────────────────────────── */ +.ws-seg { display: flex; } +.ws-seg.is-grid { display: grid; } +.ws-seg-cell { + flex: 1; text-align: center; font-family: var(--mono); font-size: 10.5px; font-weight: 600; + letter-spacing: 0.02em; padding: 9px 4px; cursor: pointer; user-select: none; + border: var(--border); border-left: none; background: var(--panel); color: var(--sub); + transition: background .12s, color .12s, box-shadow .1s; +} +.ws-seg-cell:first-child { border-left: var(--border); } +.ws-seg-cell:not(.is-on):hover { background: var(--hover); color: var(--ink); } +.ws-seg-cell:active { box-shadow: inset 0 2px 5px rgba(0,0,0,.18); } +.ws-seg-cell.is-on { background: var(--accent); color: var(--on-accent); } + +/* ── Dropdown + mode toggle ──────────────────────────────────────────────── */ +.ws-dd-wrap { position: relative; } +.ws-dd { + display: inline-flex; align-items: center; gap: 7px; line-height: 1; font-family: var(--mono); + font-size: 11px; letter-spacing: 0.04em; text-transform: uppercase; padding: 6px 9px; + border: 1px solid var(--hairline); background: transparent; color: var(--sub); cursor: pointer; user-select: none; + transition: background .12s, border-color .12s, color .12s, transform .08s; +} +.ws-dd:hover { border-color: var(--accent); color: var(--ink); } +.ws-dd:active { transform: translateY(1px); } +.ws-dd.is-open { border-color: var(--accent); background: var(--hover); } +.ws-dd-label { color: var(--faint); } +.ws-dd-value { color: var(--ink); font-weight: 700; } +.ws-dd-chev { opacity: 0.7; transition: transform .15s; } +.ws-dd.is-open .ws-dd-chev { transform: rotate(180deg); } +.ws-dd-menu { + position: absolute; top: calc(100% + 6px); right: 0; min-width: 210px; z-index: 60; + background: var(--panel); border: var(--border-w) solid var(--accent); padding: 4px; box-shadow: 6px 6px 0 var(--accent); +} +.ws-dd-head { + font-family: var(--mono); font-size: 9.5px; letter-spacing: 0.1em; color: var(--faint); + text-transform: uppercase; padding: 4px 8px 6px; +} +.ws-opt { + display: flex; align-items: center; gap: 9px; width: 100%; text-align: left; padding: 8px 9px; + border: none; background: transparent; color: var(--ink); cursor: pointer; + font-family: var(--mono); font-size: 12px; line-height: 1; transition: background .1s, color .1s; +} +.ws-opt:hover { background: var(--hover); } +.ws-opt-mark { width: 6px; height: 6px; flex-shrink: 0; background: var(--accent); } +.ws-opt-name { flex: 1; } +.ws-opt-active { font-size: 9.5px; letter-spacing: 0.08em; opacity: 0.8; } +.ws-opt.is-on { background: var(--accent); color: var(--on-accent); } +.ws-opt.is-on .ws-opt-mark { background: var(--on-accent); } +.ws-toggle-glyph { color: var(--sub); } +.ws-toggle-label { color: var(--ink); font-weight: 700; } + +/* ── Selectable card ─────────────────────────────────────────────────────── */ +.ws-card { + background: var(--panel); border: var(--border); cursor: pointer; + transition: transform .16s cubic-bezier(.2,.7,.3,1), box-shadow .16s, border-color .16s; +} +.ws-card:hover { transform: translate(-2px,-2px); box-shadow: var(--hardshadow); border-color: var(--accent); } +.ws-card:active { transform: translate(0,0); box-shadow: none; } +.ws-card.is-selected { border: var(--border-w) solid var(--accent); box-shadow: 4px 4px 0 var(--accent); } + +/* ── Dashed drop zone ────────────────────────────────────────────────────── */ +.ws-drop { + border: 1.5px dashed var(--hairline); display: flex; flex-direction: column; align-items: center; + justify-content: center; gap: 4px; color: var(--faint); cursor: pointer; width: 100%; + transition: border-color .15s, background .15s; +} +.ws-drop:hover { border-color: var(--accent); background: var(--hover); } +.ws-drop-label { font-family: var(--mono); font-size: 9.5px; letter-spacing: 0.04em; } + +/* ── Link ────────────────────────────────────────────────────────────────── */ +.ws-link { color: var(--accent); font-weight: 600; cursor: pointer; transition: opacity .12s; text-decoration: none; } +.ws-link:hover { opacity: .55; } + +/* ── Placeholder screen ──────────────────────────────────────────────────── */ +.ws-placeholder { flex: 1; display: flex; align-items: center; justify-content: center; padding: 40px; } +.ws-placeholder-card { border: var(--border); background: var(--panel); padding: 30px 36px; max-width: 420px; } +.ws-placeholder-card h2 { font-size: 26px; margin: 8px 0 12px; } +.ws-placeholder-note { color: var(--sub); font-size: 13.5px; line-height: 1.5; margin: 0; } + +/* ── Master / detail layout ──────────────────────────────────────────────── */ +.ws-col-master { + width: var(--master-w, 420px); flex-shrink: 0; background: var(--panel2); + border-right: var(--border); display: flex; flex-direction: column; overflow: hidden; +} +.ws-col-detail { flex: 1; overflow-y: auto; min-width: 0; } +.ws-list { display: flex; flex-direction: column; height: 100%; overflow: hidden; } +.ws-list-head { padding: 16px 24px; border-bottom: var(--border); flex-shrink: 0; } +.ws-list-head-row { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; margin-bottom: 12px; } +.ws-list-title { font-size: 17px; } +.ws-list-meta { font-family: var(--mono); font-size: 11px; color: var(--faint); white-space: nowrap; } +.ws-filter .ws-seg-cell { display: flex; align-items: center; justify-content: center; text-decoration: none; } +.ws-list-body { flex: 1; overflow-y: auto; padding: 14px 24px; display: flex; flex-direction: column; gap: 10px; } +.ws-empty, .ws-empty-detail { color: var(--faint); font-size: 13px; padding: 30px 6px; line-height: 1.5; } +.ws-empty-detail { display: flex; align-items: center; justify-content: center; height: 100%; } + +/* ── Signal card ─────────────────────────────────────────────────────────── */ +.ws-signal { display: flex; align-items: center; gap: 12px; padding: 12px 14px; } +.ws-signal-mom { width: 46px; flex-shrink: 0; text-align: center; } +.ws-signal-mom-num { font-family: var(--display); font-weight: 800; font-size: 30px; line-height: 0.9; color: var(--ink); } +.ws-signal-mom-label { font-family: var(--mono); font-size: 9px; letter-spacing: 0.04em; color: var(--faint); text-transform: uppercase; margin-top: 2px; } +.ws-signal-main { flex: 1; min-width: 0; } +.ws-signal-title { font-size: 14px; font-weight: 600; color: var(--ink); line-height: 1.2; margin-bottom: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.ws-signal-meta { display: flex; align-items: center; gap: 8px; font-family: var(--mono); font-size: 11px; min-width: 0; } +.ws-signal-niche { color: var(--faint); text-transform: uppercase; letter-spacing: 0.03em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.ws-state { font-family: var(--mono); font-size: 11px; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; white-space: nowrap; } +.ws-state.tone-live, .ws-state.tone-accent { color: var(--accent); } +.ws-state.tone-ok { color: var(--ok); } +.ws-state.tone-warn { color: var(--warn); } +.ws-state.tone-sub { color: var(--sub); } + +/* ── Sparkline ───────────────────────────────────────────────────────────── */ +.ws-spark { display: flex; align-items: flex-end; gap: 2px; height: 28px; flex-shrink: 0; } +.ws-spark span { width: 4px; background: var(--accent); opacity: 0.85; } +.ws-spark-lg { height: 60px; gap: 3px; } +.ws-spark-lg span { width: 7px; } +.ws-spark-axis { display: flex; justify-content: space-between; font-family: var(--mono); font-size: 9px; color: var(--faint); margin-top: 6px; text-transform: uppercase; letter-spacing: 0.06em; } + +/* ── Trend detail ────────────────────────────────────────────────────────── */ +.ws-detail-inner { padding: 24px 30px; display: flex; flex-direction: column; gap: 20px; } +.ws-detail-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 20px; } +.ws-eyebrow { display: flex; align-items: center; gap: 9px; margin-bottom: 10px; } +.ws-eyebrow-niche { font-family: var(--mono); font-size: 11px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--faint); } +.ws-detail-title { font-size: 30px; } +.ws-mom-readout { text-align: right; flex-shrink: 0; } +.ws-mom-big { font-family: var(--display); font-weight: 800; font-size: 48px; line-height: 0.9; color: var(--accent); } +.ws-metric-grid { display: grid; grid-template-columns: repeat(3, 1fr); border: var(--border); } +.ws-metric { padding: 14px 16px; border-left: var(--border); } +.ws-metric:first-child { border-left: none; } +.ws-metric-val { font-family: var(--mono); font-size: 15px; color: var(--ink); margin-top: 6px; font-variant-numeric: tabular-nums; } +.ws-section { display: flex; flex-direction: column; gap: 8px; } +.ws-prose { font-size: 13.5px; line-height: 1.55; color: var(--sub); margin: 0; max-width: 640px; } +.ws-prose.ws-faint { color: var(--faint); } +.ws-detail-cols { display: grid; grid-template-columns: 1fr 200px; gap: 16px; align-items: start; } +.ws-section-grow { min-width: 0; } +.ws-momentum-card { border: var(--border); padding: 14px; } +.ws-evidence { margin: 0; padding-left: 18px; font-size: 13px; line-height: 1.5; color: var(--sub); } +.ws-evidence li { margin-bottom: 4px; } +.ws-source-links { display: flex; flex-wrap: wrap; gap: 12px; } +.ws-source-links .ws-link { font-family: var(--mono); font-size: 11px; display: inline-flex; align-items: center; gap: 4px; } +.ws-risks .ws-prose { color: var(--warn); } +.ws-thumb-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; max-width: 420px; } +.ws-detail-actions { display: flex; align-items: center; gap: 9px; flex-wrap: wrap; padding-top: 4px; } +.ws-spacer { flex: 1; } + +/* ── List footer + disclosure form ───────────────────────────────────────── */ +.ws-list-footer { padding: 14px 24px; border-top: var(--border); flex-shrink: 0; } +.ws-full { width: 100%; justify-content: center; } +.ws-disclosure summary { list-style: none; cursor: pointer; } +.ws-disclosure summary::-webkit-details-marker { display: none; } +.ws-form { display: flex; flex-direction: column; gap: 10px; margin-top: 10px; } +.ws-faint-mono { font-family: var(--mono); font-size: 11px; color: var(--faint); } + +/* ── Report list card ────────────────────────────────────────────────────── */ +.ws-report { padding: 13px 14px; display: flex; flex-direction: column; gap: 8px; } +.ws-report-top { display: flex; justify-content: space-between; align-items: flex-start; gap: 8px; } +.ws-report-title { font-size: 14px; font-weight: 600; color: var(--ink); line-height: 1.25; } +.ws-report-sub { font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.03em; text-transform: uppercase; color: var(--faint); } +.ws-report-row { display: flex; align-items: center; gap: 12px; } +.ws-report-date { font-family: var(--mono); font-size: 11px; color: var(--faint); margin-left: auto; } + +/* ── Reading pane ────────────────────────────────────────────────────────── */ +.ws-display-normal { font-family: var(--display); font-weight: 800; text-transform: none; letter-spacing: -0.01em; line-height: 1.0; margin: 0; color: var(--ink); } +.ws-reader { display: flex; flex-direction: column; } +.ws-reader-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 20px; padding: 22px 30px; border-bottom: var(--border); } +.ws-reader-meta { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; } +.ws-reader-title { font-size: 26px; } +.ws-score-big { font-family: var(--display); font-weight: 800; font-size: 42px; line-height: 0.9; color: var(--accent); } +.ws-alert { margin: 16px 30px 0; padding: 10px 14px; border: 1px solid var(--warn); color: var(--warn); font-size: 13px; } +.ws-reader-body { display: grid; grid-template-columns: 188px 1fr; gap: 0; align-items: start; } +.ws-toc { padding: 22px 18px 22px 30px; position: sticky; top: 0; display: flex; flex-direction: column; gap: 4px; } +.ws-toc .ws-mono-label { margin-bottom: 8px; } +.ws-toc-link { display: block; padding: 5px 0 5px 10px; color: var(--sub); text-decoration: none; font-size: 13px; line-height: 1.3; border-left: 2px solid transparent; transition: color .12s, border-color .12s; } +.ws-toc-link:hover { color: var(--ink); } +.ws-toc-link.is-active { color: var(--ink); border-left-color: var(--accent); font-weight: 600; } +.ws-article { padding: 22px 30px; border-left: var(--border); min-width: 0; } +.ws-article-sec { margin-bottom: 24px; } +.ws-section-head { font-family: var(--mono); font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--accent); display: flex; align-items: center; gap: 8px; margin-bottom: 10px; } +.ws-section-head::before { content: ""; width: 16px; height: 2px; background: var(--accent); display: inline-block; flex-shrink: 0; } +.ws-article-body { font-size: 13.5px; line-height: 1.6; color: var(--sub); max-width: 680px; } +.ws-article-body p { margin: 0 0 12px; } +.ws-article-body ul { margin: 0 0 12px; padding-left: 18px; } +.ws-article-body li { margin-bottom: 4px; } +.ws-article-body strong { color: var(--ink); font-weight: 700; } +.ws-article-body code { font-family: var(--mono); font-size: 12px; background: var(--panel2); padding: 1px 4px; } +.ws-reader .ws-detail-actions { padding: 16px 30px; border-top: var(--border); } + +/* ── Etsy pipeline ───────────────────────────────────────────────────────── */ +.ws-stage { margin-bottom: 18px; } +.ws-stage-head { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; } +.ws-state-dot { width: 8px; height: 8px; border-radius: 8px; background: var(--sub); flex-shrink: 0; } +.ws-state-dot.tone-warn { background: var(--warn); } +.ws-state-dot.tone-accent { background: var(--accent); } +.ws-state-dot.tone-ok { background: var(--ok); } +.ws-state-dot.tone-sub { background: var(--sub); } +.ws-stage-name { font-family: var(--mono); font-size: 11px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink); } +.ws-stage-count { font-family: var(--mono); font-size: 11px; color: var(--faint); } +.ws-stage-rule { flex: 1; height: 0; border-top: var(--hair); } +.ws-pcard { display: flex; align-items: center; gap: 12px; padding: 10px 12px; margin-bottom: 8px; } +.ws-pcard-thumb { width: 38px; height: 38px; flex-shrink: 0; border: var(--border); background: var(--thumb-bg); position: relative; overflow: hidden; } +.ws-pcard-main { flex: 1; min-width: 0; } +.ws-pcard-title { font-size: 13.5px; font-weight: 600; color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.ws-pcard-price { font-family: var(--mono); font-size: 12px; color: var(--sub); } +.ws-pcard-arrow { color: var(--faint); display: flex; } + +/* ── Listing editor ──────────────────────────────────────────────────────── */ +.ws-editor { display: flex; flex-direction: column; } +.ws-editor-head { display: flex; justify-content: space-between; align-items: center; gap: 16px; padding: 18px 30px; border-bottom: var(--border); } +.ws-editor-title { font-size: 22px; } +.ws-editor-body { display: grid; grid-template-columns: 210px 1fr; gap: 24px; padding: 22px 30px; } +.ws-editor-images { display: flex; flex-direction: column; gap: 10px; } +.ws-listing-thumb { aspect-ratio: 1; } +.ws-img-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; } +.ws-img-cell { aspect-ratio: 1; } +.ws-img-count { text-align: center; } +.ws-editor-fields { display: flex; flex-direction: column; gap: 16px; min-width: 0; } +.ws-field-row3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; } +.ws-field-row2 { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; } +.ws-mono-input { font-family: var(--mono); font-size: 12px; } +.ws-tags-hint { float: right; margin-top: -26px; } +.ws-seg-tags { display: flex; flex-wrap: wrap; gap: 6px; } +.ws-tag { display: inline-flex; align-items: center; gap: 6px; font-family: var(--mono); font-size: 11px; padding: 5px 8px; border: var(--border); color: var(--ink); text-transform: lowercase; } +.ws-tag-x { display: inline-flex; border: none; background: transparent; color: var(--warn); cursor: pointer; padding: 0; line-height: 1; } +.ws-tag-add { border-style: dashed; border-color: var(--accent); color: var(--accent); } +.ws-tag-add input { border: none; background: transparent; outline: none; font-family: var(--mono); font-size: 11px; color: var(--ink); width: 70px; } +.ws-chip-list { display: flex; flex-wrap: wrap; gap: 6px; } +.ws-readiness { display: grid; grid-template-columns: repeat(5, 1fr); border: var(--border); } +.ws-ready-cell { padding: 12px; border-left: var(--border); } +.ws-ready-cell:first-child { border-left: none; } +.ws-ready-mark { display: inline-flex; align-items: center; justify-content: center; width: 18px; height: 18px; background: var(--ok); color: var(--on-accent); margin-bottom: 8px; } +.ws-ready-cell.is-warn .ws-ready-mark { background: transparent; border: 1px solid var(--warn); color: var(--warn); } +.ws-ready-name { font-family: var(--mono); font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.04em; color: var(--sub); } +.ws-ready-val { font-family: var(--mono); font-size: 12px; color: var(--ink); margin-top: 3px; } +.ws-ready-cell.is-warn .ws-ready-val { color: var(--warn); } +.ws-editor-foot { display: flex; align-items: center; gap: 9px; padding: 16px 30px; border-top: var(--border); } + +/* ── Design Studio: composer ─────────────────────────────────────────────── */ +.ws-col-composer { width: 396px; flex-shrink: 0; background: var(--panel2); border-right: var(--border); display: flex; flex-direction: column; overflow: hidden; } +.ws-composer-head { display: flex; align-items: center; justify-content: space-between; padding: 16px 20px; border-bottom: var(--border); } +.ws-composer-body { padding: 18px 20px; display: flex; flex-direction: column; gap: 16px; overflow-y: auto; flex: 1; } +.ws-composer-prompt { min-height: 110px; } +.ws-cost { display: flex; justify-content: space-between; align-items: center; font-family: var(--mono); } +.ws-ok-note { color: var(--ok); font-size: 11px; } +.ws-drop { aspect-ratio: 3.4; } +.ws-aspect .ws-seg-cell { cursor: pointer; } +.ws-composer-foot { margin-top: auto; padding-top: 14px; border-top: var(--border); display: flex; gap: 10px; } +.ws-grow { flex: 1; justify-content: center; } + +/* ── Design Studio: library ──────────────────────────────────────────────── */ +.ws-col-library { flex: 1; display: flex; flex-direction: column; overflow: hidden; min-width: 0; } +.ws-lib-head { display: flex; align-items: center; justify-content: space-between; padding: 16px 24px; border-bottom: var(--border); } +.ws-lib-head-l { display: flex; align-items: baseline; gap: 12px; } +.ws-lib-head-r { display: flex; align-items: center; gap: 8px; } +.ws-pseudo-input { display: flex; align-items: center; gap: 7px; border: var(--border); padding: 7px 11px; color: var(--faint); font-family: var(--mono); font-size: 11.5px; } +.ws-lib-scroll { flex: 1; overflow-y: auto; padding: 18px 24px; display: flex; flex-direction: column; gap: 16px; } +.ws-lib-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; } +.ws-libcard { padding: 14px; display: flex; flex-direction: column; gap: 11px; } +.ws-libcard-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 8px; } +.ws-libcard-titles { min-width: 0; } +.ws-libcard-title { font-size: 14.5px; font-weight: 600; color: var(--ink); line-height: 1.2; margin-bottom: 3px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.ws-libcard-eyebrow { font-family: var(--mono); font-size: 10.5px; color: var(--faint); letter-spacing: 0.03em; text-transform: uppercase; } +.ws-libcard-actions { display: flex; align-items: center; gap: 6px; flex-shrink: 0; } +.ws-libcard-thumbs { display: flex; gap: 6px; } +.ws-libcard-thumbs .ws-thumb { aspect-ratio: 1; flex: 1; } +.ws-libscore-row { display: flex; justify-content: space-between; margin-bottom: 5px; } +.ws-lineage { display: flex; align-items: center; gap: 6px; font-family: var(--mono); font-size: 11px; color: var(--faint); } +.ws-lineage-text { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + +/* ── Design Studio: focus detail ─────────────────────────────────────────── */ +.ws-focus { border: var(--border); background: var(--panel); display: flex; overflow: hidden; } +.ws-focus-rail { width: 220px; flex-shrink: 0; padding: 16px; border-right: var(--border); background: var(--panel2); display: flex; flex-direction: column; gap: 12px; } +.ws-focus-thumb { aspect-ratio: 1; } +.ws-focus-versions .ws-seg-cell { padding: 5px 0; font-weight: 700; cursor: pointer; text-decoration: none; display: flex; align-items: center; justify-content: center; } +.ws-focus-main { flex: 1; padding: 18px; display: flex; flex-direction: column; gap: 12px; min-width: 0; } +.ws-focus-top { display: flex; justify-content: space-between; align-items: flex-start; } +.ws-focus-title { font-family: var(--display); font-weight: 800; font-size: 18px; line-height: 1; } +.ws-accent { color: var(--accent); } +.ws-clamp2 { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } +.ws-meta-chips { display: flex; flex-wrap: wrap; gap: 6px; } +.ws-meta-chip { font-family: var(--mono); font-size: 10.5px; color: var(--sub); padding: 3px 7px; background: var(--panel2); border: var(--hair); } +.ws-meta-chip.is-ok { color: var(--ok); border: 1px solid var(--ok); background: transparent; } +.ws-focus-actions { margin-top: auto; display: flex; gap: 9px; flex-wrap: wrap; align-items: center; } +.ws-ml-auto { margin-left: auto; } +.ws-faint { color: var(--faint); } + +/* ── Group B: single-column screens ──────────────────────────────────────── */ +.ws-screen { flex: 1; overflow-y: auto; padding: 24px 30px; min-width: 0; } +.ws-status-row { display: flex; gap: 8px; margin-bottom: 20px; } +.ws-grid3 { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 14px; } +.ws-grid4 { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 14px; } +.ws-info-card { padding: 16px; display: flex; flex-direction: column; gap: 8px; cursor: default; } +.ws-info-card:hover { transform: none; box-shadow: none; border-color: var(--line); } +.ws-info-top { display: flex; justify-content: space-between; align-items: flex-start; gap: 8px; } +.ws-info-title { font-size: 15px; font-weight: 600; color: var(--ink); line-height: 1.25; } +.ws-info-meta { font-family: var(--mono); font-size: 11px; color: var(--faint); text-transform: uppercase; letter-spacing: 0.03em; } +.ws-info-price { font-family: var(--mono); font-size: 14px; color: var(--ink); font-variant-numeric: tabular-nums; } +.ws-design-card { padding: 12px; display: flex; flex-direction: column; gap: 10px; cursor: default; } +.ws-design-card:hover { transform: none; box-shadow: none; border-color: var(--line); } +.ws-design-thumb { aspect-ratio: 1; } +.ws-design-title { font-size: 13.5px; font-weight: 600; color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.ws-design-meta { display: flex; align-items: center; justify-content: space-between; gap: 8px; } + +/* ── Table ───────────────────────────────────────────────────────────────── */ +.ws-table { width: 100%; border-collapse: collapse; border: var(--border); } +.ws-table th { font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--sub); text-align: left; padding: 11px 14px; background: var(--panel2); border-bottom: var(--border); } +.ws-table td { font-size: 13px; color: var(--ink); padding: 11px 14px; border-bottom: var(--hair); } +.ws-table tr:last-child td { border-bottom: none; } +.ws-num { font-family: var(--mono); font-variant-numeric: tabular-nums; } +.ws-mono-cell { font-family: var(--mono); font-size: 12px; } + +@media (prefers-reduced-motion: reduce) { + * { transition: none !important; } + .ws-card:hover, .ws-thumb:hover, .ws-primary:hover { transform: none; } +} diff --git a/src/ecommerce_agent/web/static/htmx.min.js b/src/ecommerce_agent/web/static/htmx.min.js new file mode 100644 index 0000000..59937d7 --- /dev/null +++ b/src/ecommerce_agent/web/static/htmx.min.js @@ -0,0 +1 @@ +var htmx=function(){"use strict";const Q={onLoad:null,process:null,on:null,off:null,trigger:null,ajax:null,find:null,findAll:null,closest:null,values:function(e,t){const n=cn(e,t||"post");return n.values},remove:null,addClass:null,removeClass:null,toggleClass:null,takeClass:null,swap:null,defineExtension:null,removeExtension:null,logAll:null,logNone:null,logger:null,config:{historyEnabled:true,historyCacheSize:10,refreshOnHistoryMiss:false,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:true,indicatorClass:"htmx-indicator",requestClass:"htmx-request",addedClass:"htmx-added",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:true,allowScriptTags:true,inlineScriptNonce:"",inlineStyleNonce:"",attributesToSettle:["class","style","width","height"],withCredentials:false,timeout:0,wsReconnectDelay:"full-jitter",wsBinaryType:"blob",disableSelector:"[hx-disable], [data-hx-disable]",scrollBehavior:"instant",defaultFocusScroll:false,getCacheBusterParam:false,globalViewTransitions:false,methodsThatUseUrlParams:["get","delete"],selfRequestsOnly:true,ignoreTitle:false,scrollIntoViewOnBoost:true,triggerSpecsCache:null,disableInheritance:false,responseHandling:[{code:"204",swap:false},{code:"[23]..",swap:true},{code:"[45]..",swap:false,error:true}],allowNestedOobSwaps:true},parseInterval:null,_:null,version:"2.0.4"};Q.onLoad=j;Q.process=kt;Q.on=ye;Q.off=be;Q.trigger=he;Q.ajax=Rn;Q.find=u;Q.findAll=x;Q.closest=g;Q.remove=z;Q.addClass=K;Q.removeClass=G;Q.toggleClass=W;Q.takeClass=Z;Q.swap=$e;Q.defineExtension=Fn;Q.removeExtension=Bn;Q.logAll=V;Q.logNone=_;Q.parseInterval=d;Q._=e;const n={addTriggerHandler:St,bodyContains:le,canAccessLocalStorage:B,findThisElement:Se,filterValues:hn,swap:$e,hasAttribute:s,getAttributeValue:te,getClosestAttributeValue:re,getClosestMatch:o,getExpressionVars:En,getHeaders:fn,getInputValues:cn,getInternalData:ie,getSwapSpecification:gn,getTriggerSpecs:st,getTarget:Ee,makeFragment:P,mergeObjects:ce,makeSettleInfo:xn,oobSwap:He,querySelectorExt:ae,settleImmediately:Kt,shouldCancel:ht,triggerEvent:he,triggerErrorEvent:fe,withExtensions:Ft};const r=["get","post","put","delete","patch"];const H=r.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");function d(e){if(e==undefined){return undefined}let t=NaN;if(e.slice(-2)=="ms"){t=parseFloat(e.slice(0,-2))}else if(e.slice(-1)=="s"){t=parseFloat(e.slice(0,-1))*1e3}else if(e.slice(-1)=="m"){t=parseFloat(e.slice(0,-1))*1e3*60}else{t=parseFloat(e)}return isNaN(t)?undefined:t}function ee(e,t){return e instanceof Element&&e.getAttribute(t)}function s(e,t){return!!e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function te(e,t){return ee(e,t)||ee(e,"data-"+t)}function c(e){const t=e.parentElement;if(!t&&e.parentNode instanceof ShadowRoot)return e.parentNode;return t}function ne(){return document}function m(e,t){return e.getRootNode?e.getRootNode({composed:t}):ne()}function o(e,t){while(e&&!t(e)){e=c(e)}return e||null}function i(e,t,n){const r=te(t,n);const o=te(t,"hx-disinherit");var i=te(t,"hx-inherit");if(e!==t){if(Q.config.disableInheritance){if(i&&(i==="*"||i.split(" ").indexOf(n)>=0)){return r}else{return null}}if(o&&(o==="*"||o.split(" ").indexOf(n)>=0)){return"unset"}}return r}function re(t,n){let r=null;o(t,function(e){return!!(r=i(t,ue(e),n))});if(r!=="unset"){return r}}function h(e,t){const n=e instanceof Element&&(e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector);return!!n&&n.call(e,t)}function T(e){const t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;const n=t.exec(e);if(n){return n[1].toLowerCase()}else{return""}}function q(e){const t=new DOMParser;return t.parseFromString(e,"text/html")}function L(e,t){while(t.childNodes.length>0){e.append(t.childNodes[0])}}function A(e){const t=ne().createElement("script");se(e.attributes,function(e){t.setAttribute(e.name,e.value)});t.textContent=e.textContent;t.async=false;if(Q.config.inlineScriptNonce){t.nonce=Q.config.inlineScriptNonce}return t}function N(e){return e.matches("script")&&(e.type==="text/javascript"||e.type==="module"||e.type==="")}function I(e){Array.from(e.querySelectorAll("script")).forEach(e=>{if(N(e)){const t=A(e);const n=e.parentNode;try{n.insertBefore(t,e)}catch(e){O(e)}finally{e.remove()}}})}function P(e){const t=e.replace(/]*)?>[\s\S]*?<\/head>/i,"");const n=T(t);let r;if(n==="html"){r=new DocumentFragment;const i=q(e);L(r,i.body);r.title=i.title}else if(n==="body"){r=new DocumentFragment;const i=q(t);L(r,i.body);r.title=i.title}else{const i=q('");r=i.querySelector("template").content;r.title=i.title;var o=r.querySelector("title");if(o&&o.parentNode===r){o.remove();r.title=o.innerText}}if(r){if(Q.config.allowScriptTags){I(r)}else{r.querySelectorAll("script").forEach(e=>e.remove())}}return r}function oe(e){if(e){e()}}function t(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function k(e){return typeof e==="function"}function D(e){return t(e,"Object")}function ie(e){const t="htmx-internal-data";let n=e[t];if(!n){n=e[t]={}}return n}function M(t){const n=[];if(t){for(let e=0;e=0}function le(e){return e.getRootNode({composed:true})===document}function F(e){return e.trim().split(/\s+/)}function ce(e,t){for(const n in t){if(t.hasOwnProperty(n)){e[n]=t[n]}}return e}function S(e){try{return JSON.parse(e)}catch(e){O(e);return null}}function B(){const e="htmx:localStorageTest";try{localStorage.setItem(e,e);localStorage.removeItem(e);return true}catch(e){return false}}function U(t){try{const e=new URL(t);if(e){t=e.pathname+e.search}if(!/^\/$/.test(t)){t=t.replace(/\/+$/,"")}return t}catch(e){return t}}function e(e){return vn(ne().body,function(){return eval(e)})}function j(t){const e=Q.on("htmx:load",function(e){t(e.detail.elt)});return e}function V(){Q.logger=function(e,t,n){if(console){console.log(t,e,n)}}}function _(){Q.logger=null}function u(e,t){if(typeof e!=="string"){return e.querySelector(t)}else{return u(ne(),e)}}function x(e,t){if(typeof e!=="string"){return e.querySelectorAll(t)}else{return x(ne(),e)}}function E(){return window}function z(e,t){e=y(e);if(t){E().setTimeout(function(){z(e);e=null},t)}else{c(e).removeChild(e)}}function ue(e){return e instanceof Element?e:null}function $(e){return e instanceof HTMLElement?e:null}function J(e){return typeof e==="string"?e:null}function f(e){return e instanceof Element||e instanceof Document||e instanceof DocumentFragment?e:null}function K(e,t,n){e=ue(y(e));if(!e){return}if(n){E().setTimeout(function(){K(e,t);e=null},n)}else{e.classList&&e.classList.add(t)}}function G(e,t,n){let r=ue(y(e));if(!r){return}if(n){E().setTimeout(function(){G(r,t);r=null},n)}else{if(r.classList){r.classList.remove(t);if(r.classList.length===0){r.removeAttribute("class")}}}}function W(e,t){e=y(e);e.classList.toggle(t)}function Z(e,t){e=y(e);se(e.parentElement.children,function(e){G(e,t)});K(ue(e),t)}function g(e,t){e=ue(y(e));if(e&&e.closest){return e.closest(t)}else{do{if(e==null||h(e,t)){return e}}while(e=e&&ue(c(e)));return null}}function l(e,t){return e.substring(0,t.length)===t}function Y(e,t){return e.substring(e.length-t.length)===t}function ge(e){const t=e.trim();if(l(t,"<")&&Y(t,"/>")){return t.substring(1,t.length-2)}else{return t}}function p(t,r,n){if(r.indexOf("global ")===0){return p(t,r.slice(7),true)}t=y(t);const o=[];{let t=0;let n=0;for(let e=0;e"){t--}}if(n0){const r=ge(o.shift());let e;if(r.indexOf("closest ")===0){e=g(ue(t),ge(r.substr(8)))}else if(r.indexOf("find ")===0){e=u(f(t),ge(r.substr(5)))}else if(r==="next"||r==="nextElementSibling"){e=ue(t).nextElementSibling}else if(r.indexOf("next ")===0){e=pe(t,ge(r.substr(5)),!!n)}else if(r==="previous"||r==="previousElementSibling"){e=ue(t).previousElementSibling}else if(r.indexOf("previous ")===0){e=me(t,ge(r.substr(9)),!!n)}else if(r==="document"){e=document}else if(r==="window"){e=window}else if(r==="body"){e=document.body}else if(r==="root"){e=m(t,!!n)}else if(r==="host"){e=t.getRootNode().host}else{s.push(r)}if(e){i.push(e)}}if(s.length>0){const e=s.join(",");const c=f(m(t,!!n));i.push(...M(c.querySelectorAll(e)))}return i}var pe=function(t,e,n){const r=f(m(t,n)).querySelectorAll(e);for(let e=0;e=0;e--){const o=r[e];if(o.compareDocumentPosition(t)===Node.DOCUMENT_POSITION_FOLLOWING){return o}}};function ae(e,t){if(typeof e!=="string"){return p(e,t)[0]}else{return p(ne().body,e)[0]}}function y(e,t){if(typeof e==="string"){return u(f(t)||document,e)}else{return e}}function xe(e,t,n,r){if(k(t)){return{target:ne().body,event:J(e),listener:t,options:n}}else{return{target:y(e),event:J(t),listener:n,options:r}}}function ye(t,n,r,o){Vn(function(){const e=xe(t,n,r,o);e.target.addEventListener(e.event,e.listener,e.options)});const e=k(n);return e?n:r}function be(t,n,r){Vn(function(){const e=xe(t,n,r);e.target.removeEventListener(e.event,e.listener)});return k(n)?n:r}const ve=ne().createElement("output");function we(e,t){const n=re(e,t);if(n){if(n==="this"){return[Se(e,t)]}else{const r=p(e,n);if(r.length===0){O('The selector "'+n+'" on '+t+" returned no matches!");return[ve]}else{return r}}}}function Se(e,t){return ue(o(e,function(e){return te(ue(e),t)!=null}))}function Ee(e){const t=re(e,"hx-target");if(t){if(t==="this"){return Se(e,"hx-target")}else{return ae(e,t)}}else{const n=ie(e);if(n.boosted){return ne().body}else{return e}}}function Ce(t){const n=Q.config.attributesToSettle;for(let e=0;e0){s=e.substring(0,e.indexOf(":"));n=e.substring(e.indexOf(":")+1)}else{s=e}o.removeAttribute("hx-swap-oob");o.removeAttribute("data-hx-swap-oob");const r=p(t,n,false);if(r){se(r,function(e){let t;const n=o.cloneNode(true);t=ne().createDocumentFragment();t.appendChild(n);if(!Re(s,e)){t=f(n)}const r={shouldSwap:true,target:e,fragment:t};if(!he(e,"htmx:oobBeforeSwap",r))return;e=r.target;if(r.shouldSwap){qe(t);_e(s,e,e,t,i);Te()}se(i.elts,function(e){he(e,"htmx:oobAfterSwap",r)})});o.parentNode.removeChild(o)}else{o.parentNode.removeChild(o);fe(ne().body,"htmx:oobErrorNoTarget",{content:o})}return e}function Te(){const e=u("#--htmx-preserve-pantry--");if(e){for(const t of[...e.children]){const n=u("#"+t.id);n.parentNode.moveBefore(t,n);n.remove()}e.remove()}}function qe(e){se(x(e,"[hx-preserve], [data-hx-preserve]"),function(e){const t=te(e,"id");const n=ne().getElementById(t);if(n!=null){if(e.moveBefore){let e=u("#--htmx-preserve-pantry--");if(e==null){ne().body.insertAdjacentHTML("afterend","
");e=u("#--htmx-preserve-pantry--")}e.moveBefore(n,null)}else{e.parentNode.replaceChild(n,e)}}})}function Le(l,e,c){se(e.querySelectorAll("[id]"),function(t){const n=ee(t,"id");if(n&&n.length>0){const r=n.replace("'","\\'");const o=t.tagName.replace(":","\\:");const e=f(l);const i=e&&e.querySelector(o+"[id='"+r+"']");if(i&&i!==e){const s=t.cloneNode();Oe(t,i);c.tasks.push(function(){Oe(t,s)})}}})}function Ae(e){return function(){G(e,Q.config.addedClass);kt(ue(e));Ne(f(e));he(e,"htmx:load")}}function Ne(e){const t="[autofocus]";const n=$(h(e,t)?e:e.querySelector(t));if(n!=null){n.focus()}}function a(e,t,n,r){Le(e,n,r);while(n.childNodes.length>0){const o=n.firstChild;K(ue(o),Q.config.addedClass);e.insertBefore(o,t);if(o.nodeType!==Node.TEXT_NODE&&o.nodeType!==Node.COMMENT_NODE){r.tasks.push(Ae(o))}}}function Ie(e,t){let n=0;while(n0}function $e(e,t,r,o){if(!o){o={}}e=y(e);const i=o.contextElement?m(o.contextElement,false):ne();const n=document.activeElement;let s={};try{s={elt:n,start:n?n.selectionStart:null,end:n?n.selectionEnd:null}}catch(e){}const l=xn(e);if(r.swapStyle==="textContent"){e.textContent=t}else{let n=P(t);l.title=n.title;if(o.selectOOB){const u=o.selectOOB.split(",");for(let t=0;t0){E().setTimeout(c,r.settleDelay)}else{c()}}function Je(e,t,n){const r=e.getResponseHeader(t);if(r.indexOf("{")===0){const o=S(r);for(const i in o){if(o.hasOwnProperty(i)){let e=o[i];if(D(e)){n=e.target!==undefined?e.target:n}else{e={value:e}}he(n,i,e)}}}else{const s=r.split(",");for(let e=0;e0){const s=o[0];if(s==="]"){e--;if(e===0){if(n===null){t=t+"true"}o.shift();t+=")})";try{const l=vn(r,function(){return Function(t)()},function(){return true});l.source=t;return l}catch(e){fe(ne().body,"htmx:syntax:error",{error:e,source:t});return null}}}else if(s==="["){e++}if(tt(s,n,i)){t+="(("+i+"."+s+") ? ("+i+"."+s+") : (window."+s+"))"}else{t=t+s}n=o.shift()}}}function C(e,t){let n="";while(e.length>0&&!t.test(e[0])){n+=e.shift()}return n}function rt(e){let t;if(e.length>0&&Ye.test(e[0])){e.shift();t=C(e,Qe).trim();e.shift()}else{t=C(e,v)}return t}const ot="input, textarea, select";function it(e,t,n){const r=[];const o=et(t);do{C(o,w);const l=o.length;const c=C(o,/[,\[\s]/);if(c!==""){if(c==="every"){const u={trigger:"every"};C(o,w);u.pollInterval=d(C(o,/[,\[\s]/));C(o,w);var i=nt(e,o,"event");if(i){u.eventFilter=i}r.push(u)}else{const a={trigger:c};var i=nt(e,o,"event");if(i){a.eventFilter=i}C(o,w);while(o.length>0&&o[0]!==","){const f=o.shift();if(f==="changed"){a.changed=true}else if(f==="once"){a.once=true}else if(f==="consume"){a.consume=true}else if(f==="delay"&&o[0]===":"){o.shift();a.delay=d(C(o,v))}else if(f==="from"&&o[0]===":"){o.shift();if(Ye.test(o[0])){var s=rt(o)}else{var s=C(o,v);if(s==="closest"||s==="find"||s==="next"||s==="previous"){o.shift();const h=rt(o);if(h.length>0){s+=" "+h}}}a.from=s}else if(f==="target"&&o[0]===":"){o.shift();a.target=rt(o)}else if(f==="throttle"&&o[0]===":"){o.shift();a.throttle=d(C(o,v))}else if(f==="queue"&&o[0]===":"){o.shift();a.queue=C(o,v)}else if(f==="root"&&o[0]===":"){o.shift();a[f]=rt(o)}else if(f==="threshold"&&o[0]===":"){o.shift();a[f]=C(o,v)}else{fe(e,"htmx:syntax:error",{token:o.shift()})}C(o,w)}r.push(a)}}if(o.length===l){fe(e,"htmx:syntax:error",{token:o.shift()})}C(o,w)}while(o[0]===","&&o.shift());if(n){n[t]=r}return r}function st(e){const t=te(e,"hx-trigger");let n=[];if(t){const r=Q.config.triggerSpecsCache;n=r&&r[t]||it(e,t,r)}if(n.length>0){return n}else if(h(e,"form")){return[{trigger:"submit"}]}else if(h(e,'input[type="button"], input[type="submit"]')){return[{trigger:"click"}]}else if(h(e,ot)){return[{trigger:"change"}]}else{return[{trigger:"click"}]}}function lt(e){ie(e).cancelled=true}function ct(e,t,n){const r=ie(e);r.timeout=E().setTimeout(function(){if(le(e)&&r.cancelled!==true){if(!gt(n,e,Mt("hx:poll:trigger",{triggerSpec:n,target:e}))){t(e)}ct(e,t,n)}},n.pollInterval)}function ut(e){return location.hostname===e.hostname&&ee(e,"href")&&ee(e,"href").indexOf("#")!==0}function at(e){return g(e,Q.config.disableSelector)}function ft(t,n,e){if(t instanceof HTMLAnchorElement&&ut(t)&&(t.target===""||t.target==="_self")||t.tagName==="FORM"&&String(ee(t,"method")).toLowerCase()!=="dialog"){n.boosted=true;let r,o;if(t.tagName==="A"){r="get";o=ee(t,"href")}else{const i=ee(t,"method");r=i?i.toLowerCase():"get";o=ee(t,"action");if(o==null||o===""){o=ne().location.href}if(r==="get"&&o.includes("?")){o=o.replace(/\?[^#]+/,"")}}e.forEach(function(e){pt(t,function(e,t){const n=ue(e);if(at(n)){b(n);return}de(r,o,n,t)},n,e,true)})}}function ht(e,t){const n=ue(t);if(!n){return false}if(e.type==="submit"||e.type==="click"){if(n.tagName==="FORM"){return true}if(h(n,'input[type="submit"], button')&&(h(n,"[form]")||g(n,"form")!==null)){return true}if(n instanceof HTMLAnchorElement&&n.href&&(n.getAttribute("href")==="#"||n.getAttribute("href").indexOf("#")!==0)){return true}}return false}function dt(e,t){return ie(e).boosted&&e instanceof HTMLAnchorElement&&t.type==="click"&&(t.ctrlKey||t.metaKey)}function gt(e,t,n){const r=e.eventFilter;if(r){try{return r.call(t,n)!==true}catch(e){const o=r.source;fe(ne().body,"htmx:eventFilter:error",{error:e,source:o});return true}}return false}function pt(l,c,e,u,a){const f=ie(l);let t;if(u.from){t=p(l,u.from)}else{t=[l]}if(u.changed){if(!("lastValue"in f)){f.lastValue=new WeakMap}t.forEach(function(e){if(!f.lastValue.has(u)){f.lastValue.set(u,new WeakMap)}f.lastValue.get(u).set(e,e.value)})}se(t,function(i){const s=function(e){if(!le(l)){i.removeEventListener(u.trigger,s);return}if(dt(l,e)){return}if(a||ht(e,l)){e.preventDefault()}if(gt(u,l,e)){return}const t=ie(e);t.triggerSpec=u;if(t.handledFor==null){t.handledFor=[]}if(t.handledFor.indexOf(l)<0){t.handledFor.push(l);if(u.consume){e.stopPropagation()}if(u.target&&e.target){if(!h(ue(e.target),u.target)){return}}if(u.once){if(f.triggeredOnce){return}else{f.triggeredOnce=true}}if(u.changed){const n=event.target;const r=n.value;const o=f.lastValue.get(u);if(o.has(n)&&o.get(n)===r){return}o.set(n,r)}if(f.delayed){clearTimeout(f.delayed)}if(f.throttle){return}if(u.throttle>0){if(!f.throttle){he(l,"htmx:trigger");c(l,e);f.throttle=E().setTimeout(function(){f.throttle=null},u.throttle)}}else if(u.delay>0){f.delayed=E().setTimeout(function(){he(l,"htmx:trigger");c(l,e)},u.delay)}else{he(l,"htmx:trigger");c(l,e)}}};if(e.listenerInfos==null){e.listenerInfos=[]}e.listenerInfos.push({trigger:u.trigger,listener:s,on:i});i.addEventListener(u.trigger,s)})}let mt=false;let xt=null;function yt(){if(!xt){xt=function(){mt=true};window.addEventListener("scroll",xt);window.addEventListener("resize",xt);setInterval(function(){if(mt){mt=false;se(ne().querySelectorAll("[hx-trigger*='revealed'],[data-hx-trigger*='revealed']"),function(e){bt(e)})}},200)}}function bt(e){if(!s(e,"data-hx-revealed")&&X(e)){e.setAttribute("data-hx-revealed","true");const t=ie(e);if(t.initHash){he(e,"revealed")}else{e.addEventListener("htmx:afterProcessNode",function(){he(e,"revealed")},{once:true})}}}function vt(e,t,n,r){const o=function(){if(!n.loaded){n.loaded=true;he(e,"htmx:trigger");t(e)}};if(r>0){E().setTimeout(o,r)}else{o()}}function wt(t,n,e){let i=false;se(r,function(r){if(s(t,"hx-"+r)){const o=te(t,"hx-"+r);i=true;n.path=o;n.verb=r;e.forEach(function(e){St(t,e,n,function(e,t){const n=ue(e);if(g(n,Q.config.disableSelector)){b(n);return}de(r,o,n,t)})})}});return i}function St(r,e,t,n){if(e.trigger==="revealed"){yt();pt(r,n,t,e);bt(ue(r))}else if(e.trigger==="intersect"){const o={};if(e.root){o.root=ae(r,e.root)}if(e.threshold){o.threshold=parseFloat(e.threshold)}const i=new IntersectionObserver(function(t){for(let e=0;e0){t.polling=true;ct(ue(r),n,e)}else{pt(r,n,t,e)}}function Et(e){const t=ue(e);if(!t){return false}const n=t.attributes;for(let e=0;e", "+e).join(""));return o}else{return[]}}function Tt(e){const t=g(ue(e.target),"button, input[type='submit']");const n=Lt(e);if(n){n.lastButtonClicked=t}}function qt(e){const t=Lt(e);if(t){t.lastButtonClicked=null}}function Lt(e){const t=g(ue(e.target),"button, input[type='submit']");if(!t){return}const n=y("#"+ee(t,"form"),t.getRootNode())||g(t,"form");if(!n){return}return ie(n)}function At(e){e.addEventListener("click",Tt);e.addEventListener("focusin",Tt);e.addEventListener("focusout",qt)}function Nt(t,e,n){const r=ie(t);if(!Array.isArray(r.onHandlers)){r.onHandlers=[]}let o;const i=function(e){vn(t,function(){if(at(t)){return}if(!o){o=new Function("event",n)}o.call(t,e)})};t.addEventListener(e,i);r.onHandlers.push({event:e,listener:i})}function It(t){ke(t);for(let e=0;eQ.config.historyCacheSize){i.shift()}while(i.length>0){try{localStorage.setItem("htmx-history-cache",JSON.stringify(i));break}catch(e){fe(ne().body,"htmx:historyCacheError",{cause:e,cache:i});i.shift()}}}function Vt(t){if(!B()){return null}t=U(t);const n=S(localStorage.getItem("htmx-history-cache"))||[];for(let e=0;e=200&&this.status<400){he(ne().body,"htmx:historyCacheMissLoad",i);const e=P(this.response);const t=e.querySelector("[hx-history-elt],[data-hx-history-elt]")||e;const n=Ut();const r=xn(n);kn(e.title);qe(e);Ve(n,t,r);Te();Kt(r.tasks);Bt=o;he(ne().body,"htmx:historyRestore",{path:o,cacheMiss:true,serverResponse:this.response})}else{fe(ne().body,"htmx:historyCacheMissLoadError",i)}};e.send()}function Wt(e){zt();e=e||location.pathname+location.search;const t=Vt(e);if(t){const n=P(t.content);const r=Ut();const o=xn(r);kn(t.title);qe(n);Ve(r,n,o);Te();Kt(o.tasks);E().setTimeout(function(){window.scrollTo(0,t.scroll)},0);Bt=e;he(ne().body,"htmx:historyRestore",{path:e,item:t})}else{if(Q.config.refreshOnHistoryMiss){window.location.reload(true)}else{Gt(e)}}}function Zt(e){let t=we(e,"hx-indicator");if(t==null){t=[e]}se(t,function(e){const t=ie(e);t.requestCount=(t.requestCount||0)+1;e.classList.add.call(e.classList,Q.config.requestClass)});return t}function Yt(e){let t=we(e,"hx-disabled-elt");if(t==null){t=[]}se(t,function(e){const t=ie(e);t.requestCount=(t.requestCount||0)+1;e.setAttribute("disabled","");e.setAttribute("data-disabled-by-htmx","")});return t}function Qt(e,t){se(e.concat(t),function(e){const t=ie(e);t.requestCount=(t.requestCount||1)-1});se(e,function(e){const t=ie(e);if(t.requestCount===0){e.classList.remove.call(e.classList,Q.config.requestClass)}});se(t,function(e){const t=ie(e);if(t.requestCount===0){e.removeAttribute("disabled");e.removeAttribute("data-disabled-by-htmx")}})}function en(t,n){for(let e=0;en.indexOf(e)<0)}else{e=e.filter(e=>e!==n)}r.delete(t);se(e,e=>r.append(t,e))}}function on(t,n,r,o,i){if(o==null||en(t,o)){return}else{t.push(o)}if(tn(o)){const s=ee(o,"name");let e=o.value;if(o instanceof HTMLSelectElement&&o.multiple){e=M(o.querySelectorAll("option:checked")).map(function(e){return e.value})}if(o instanceof HTMLInputElement&&o.files){e=M(o.files)}nn(s,e,n);if(i){sn(o,r)}}if(o instanceof HTMLFormElement){se(o.elements,function(e){if(t.indexOf(e)>=0){rn(e.name,e.value,n)}else{t.push(e)}if(i){sn(e,r)}});new FormData(o).forEach(function(e,t){if(e instanceof File&&e.name===""){return}nn(t,e,n)})}}function sn(e,t){const n=e;if(n.willValidate){he(n,"htmx:validation:validate");if(!n.checkValidity()){t.push({elt:n,message:n.validationMessage,validity:n.validity});he(n,"htmx:validation:failed",{message:n.validationMessage,validity:n.validity})}}}function ln(n,e){for(const t of e.keys()){n.delete(t)}e.forEach(function(e,t){n.append(t,e)});return n}function cn(e,t){const n=[];const r=new FormData;const o=new FormData;const i=[];const s=ie(e);if(s.lastButtonClicked&&!le(s.lastButtonClicked)){s.lastButtonClicked=null}let l=e instanceof HTMLFormElement&&e.noValidate!==true||te(e,"hx-validate")==="true";if(s.lastButtonClicked){l=l&&s.lastButtonClicked.formNoValidate!==true}if(t!=="get"){on(n,o,i,g(e,"form"),l)}on(n,r,i,e,l);if(s.lastButtonClicked||e.tagName==="BUTTON"||e.tagName==="INPUT"&&ee(e,"type")==="submit"){const u=s.lastButtonClicked||e;const a=ee(u,"name");nn(a,u.value,o)}const c=we(e,"hx-include");se(c,function(e){on(n,r,i,ue(e),l);if(!h(e,"form")){se(f(e).querySelectorAll(ot),function(e){on(n,r,i,e,l)})}});ln(r,o);return{errors:i,formData:r,values:An(r)}}function un(e,t,n){if(e!==""){e+="&"}if(String(n)==="[object Object]"){n=JSON.stringify(n)}const r=encodeURIComponent(n);e+=encodeURIComponent(t)+"="+r;return e}function an(e){e=qn(e);let n="";e.forEach(function(e,t){n=un(n,t,e)});return n}function fn(e,t,n){const r={"HX-Request":"true","HX-Trigger":ee(e,"id"),"HX-Trigger-Name":ee(e,"name"),"HX-Target":te(t,"id"),"HX-Current-URL":ne().location.href};bn(e,"hx-headers",false,r);if(n!==undefined){r["HX-Prompt"]=n}if(ie(e).boosted){r["HX-Boosted"]="true"}return r}function hn(n,e){const t=re(e,"hx-params");if(t){if(t==="none"){return new FormData}else if(t==="*"){return n}else if(t.indexOf("not ")===0){se(t.slice(4).split(","),function(e){e=e.trim();n.delete(e)});return n}else{const r=new FormData;se(t.split(","),function(t){t=t.trim();if(n.has(t)){n.getAll(t).forEach(function(e){r.append(t,e)})}});return r}}else{return n}}function dn(e){return!!ee(e,"href")&&ee(e,"href").indexOf("#")>=0}function gn(e,t){const n=t||re(e,"hx-swap");const r={swapStyle:ie(e).boosted?"innerHTML":Q.config.defaultSwapStyle,swapDelay:Q.config.defaultSwapDelay,settleDelay:Q.config.defaultSettleDelay};if(Q.config.scrollIntoViewOnBoost&&ie(e).boosted&&!dn(e)){r.show="top"}if(n){const s=F(n);if(s.length>0){for(let e=0;e0?o.join(":"):null;r.scroll=u;r.scrollTarget=i}else if(l.indexOf("show:")===0){const a=l.slice(5);var o=a.split(":");const f=o.pop();var i=o.length>0?o.join(":"):null;r.show=f;r.showTarget=i}else if(l.indexOf("focus-scroll:")===0){const h=l.slice("focus-scroll:".length);r.focusScroll=h=="true"}else if(e==0){r.swapStyle=l}else{O("Unknown modifier in hx-swap: "+l)}}}}return r}function pn(e){return re(e,"hx-encoding")==="multipart/form-data"||h(e,"form")&&ee(e,"enctype")==="multipart/form-data"}function mn(t,n,r){let o=null;Ft(n,function(e){if(o==null){o=e.encodeParameters(t,r,n)}});if(o!=null){return o}else{if(pn(n)){return ln(new FormData,qn(r))}else{return an(r)}}}function xn(e){return{tasks:[],elts:[e]}}function yn(e,t){const n=e[0];const r=e[e.length-1];if(t.scroll){var o=null;if(t.scrollTarget){o=ue(ae(n,t.scrollTarget))}if(t.scroll==="top"&&(n||o)){o=o||n;o.scrollTop=0}if(t.scroll==="bottom"&&(r||o)){o=o||r;o.scrollTop=o.scrollHeight}}if(t.show){var o=null;if(t.showTarget){let e=t.showTarget;if(t.showTarget==="window"){e="body"}o=ue(ae(n,e))}if(t.show==="top"&&(n||o)){o=o||n;o.scrollIntoView({block:"start",behavior:Q.config.scrollBehavior})}if(t.show==="bottom"&&(r||o)){o=o||r;o.scrollIntoView({block:"end",behavior:Q.config.scrollBehavior})}}}function bn(r,e,o,i){if(i==null){i={}}if(r==null){return i}const s=te(r,e);if(s){let e=s.trim();let t=o;if(e==="unset"){return null}if(e.indexOf("javascript:")===0){e=e.slice(11);t=true}else if(e.indexOf("js:")===0){e=e.slice(3);t=true}if(e.indexOf("{")!==0){e="{"+e+"}"}let n;if(t){n=vn(r,function(){return Function("return ("+e+")")()},{})}else{n=S(e)}for(const l in n){if(n.hasOwnProperty(l)){if(i[l]==null){i[l]=n[l]}}}}return bn(ue(c(r)),e,o,i)}function vn(e,t,n){if(Q.config.allowEval){return t()}else{fe(e,"htmx:evalDisallowedError");return n}}function wn(e,t){return bn(e,"hx-vars",true,t)}function Sn(e,t){return bn(e,"hx-vals",false,t)}function En(e){return ce(wn(e),Sn(e))}function Cn(t,n,r){if(r!==null){try{t.setRequestHeader(n,r)}catch(e){t.setRequestHeader(n,encodeURIComponent(r));t.setRequestHeader(n+"-URI-AutoEncoded","true")}}}function On(t){if(t.responseURL&&typeof URL!=="undefined"){try{const e=new URL(t.responseURL);return e.pathname+e.search}catch(e){fe(ne().body,"htmx:badResponseUrl",{url:t.responseURL})}}}function R(e,t){return t.test(e.getAllResponseHeaders())}function Rn(t,n,r){t=t.toLowerCase();if(r){if(r instanceof Element||typeof r==="string"){return de(t,n,null,null,{targetOverride:y(r)||ve,returnPromise:true})}else{let e=y(r.target);if(r.target&&!e||r.source&&!e&&!y(r.source)){e=ve}return de(t,n,y(r.source),r.event,{handler:r.handler,headers:r.headers,values:r.values,targetOverride:e,swapOverride:r.swap,select:r.select,returnPromise:true})}}else{return de(t,n,null,null,{returnPromise:true})}}function Hn(e){const t=[];while(e){t.push(e);e=e.parentElement}return t}function Tn(e,t,n){let r;let o;if(typeof URL==="function"){o=new URL(t,document.location.href);const i=document.location.origin;r=i===o.origin}else{o=t;r=l(t,document.location.origin)}if(Q.config.selfRequestsOnly){if(!r){return false}}return he(e,"htmx:validateUrl",ce({url:o,sameHost:r},n))}function qn(e){if(e instanceof FormData)return e;const t=new FormData;for(const n in e){if(e.hasOwnProperty(n)){if(e[n]&&typeof e[n].forEach==="function"){e[n].forEach(function(e){t.append(n,e)})}else if(typeof e[n]==="object"&&!(e[n]instanceof Blob)){t.append(n,JSON.stringify(e[n]))}else{t.append(n,e[n])}}}return t}function Ln(r,o,e){return new Proxy(e,{get:function(t,e){if(typeof e==="number")return t[e];if(e==="length")return t.length;if(e==="push"){return function(e){t.push(e);r.append(o,e)}}if(typeof t[e]==="function"){return function(){t[e].apply(t,arguments);r.delete(o);t.forEach(function(e){r.append(o,e)})}}if(t[e]&&t[e].length===1){return t[e][0]}else{return t[e]}},set:function(e,t,n){e[t]=n;r.delete(o);e.forEach(function(e){r.append(o,e)});return true}})}function An(o){return new Proxy(o,{get:function(e,t){if(typeof t==="symbol"){const r=Reflect.get(e,t);if(typeof r==="function"){return function(){return r.apply(o,arguments)}}else{return r}}if(t==="toJSON"){return()=>Object.fromEntries(o)}if(t in e){if(typeof e[t]==="function"){return function(){return o[t].apply(o,arguments)}}else{return e[t]}}const n=o.getAll(t);if(n.length===0){return undefined}else if(n.length===1){return n[0]}else{return Ln(e,t,n)}},set:function(t,n,e){if(typeof n!=="string"){return false}t.delete(n);if(e&&typeof e.forEach==="function"){e.forEach(function(e){t.append(n,e)})}else if(typeof e==="object"&&!(e instanceof Blob)){t.append(n,JSON.stringify(e))}else{t.append(n,e)}return true},deleteProperty:function(e,t){if(typeof t==="string"){e.delete(t)}return true},ownKeys:function(e){return Reflect.ownKeys(Object.fromEntries(e))},getOwnPropertyDescriptor:function(e,t){return Reflect.getOwnPropertyDescriptor(Object.fromEntries(e),t)}})}function de(t,n,r,o,i,D){let s=null;let l=null;i=i!=null?i:{};if(i.returnPromise&&typeof Promise!=="undefined"){var e=new Promise(function(e,t){s=e;l=t})}if(r==null){r=ne().body}const M=i.handler||Dn;const X=i.select||null;if(!le(r)){oe(s);return e}const c=i.targetOverride||ue(Ee(r));if(c==null||c==ve){fe(r,"htmx:targetError",{target:te(r,"hx-target")});oe(l);return e}let u=ie(r);const a=u.lastButtonClicked;if(a){const L=ee(a,"formaction");if(L!=null){n=L}const A=ee(a,"formmethod");if(A!=null){if(A.toLowerCase()!=="dialog"){t=A}}}const f=re(r,"hx-confirm");if(D===undefined){const K=function(e){return de(t,n,r,o,i,!!e)};const G={target:c,elt:r,path:n,verb:t,triggeringEvent:o,etc:i,issueRequest:K,question:f};if(he(r,"htmx:confirm",G)===false){oe(s);return e}}let h=r;let d=re(r,"hx-sync");let g=null;let F=false;if(d){const N=d.split(":");const I=N[0].trim();if(I==="this"){h=Se(r,"hx-sync")}else{h=ue(ae(r,I))}d=(N[1]||"drop").trim();u=ie(h);if(d==="drop"&&u.xhr&&u.abortable!==true){oe(s);return e}else if(d==="abort"){if(u.xhr){oe(s);return e}else{F=true}}else if(d==="replace"){he(h,"htmx:abort")}else if(d.indexOf("queue")===0){const W=d.split(" ");g=(W[1]||"last").trim()}}if(u.xhr){if(u.abortable){he(h,"htmx:abort")}else{if(g==null){if(o){const P=ie(o);if(P&&P.triggerSpec&&P.triggerSpec.queue){g=P.triggerSpec.queue}}if(g==null){g="last"}}if(u.queuedRequests==null){u.queuedRequests=[]}if(g==="first"&&u.queuedRequests.length===0){u.queuedRequests.push(function(){de(t,n,r,o,i)})}else if(g==="all"){u.queuedRequests.push(function(){de(t,n,r,o,i)})}else if(g==="last"){u.queuedRequests=[];u.queuedRequests.push(function(){de(t,n,r,o,i)})}oe(s);return e}}const p=new XMLHttpRequest;u.xhr=p;u.abortable=F;const m=function(){u.xhr=null;u.abortable=false;if(u.queuedRequests!=null&&u.queuedRequests.length>0){const e=u.queuedRequests.shift();e()}};const B=re(r,"hx-prompt");if(B){var x=prompt(B);if(x===null||!he(r,"htmx:prompt",{prompt:x,target:c})){oe(s);m();return e}}if(f&&!D){if(!confirm(f)){oe(s);m();return e}}let y=fn(r,c,x);if(t!=="get"&&!pn(r)){y["Content-Type"]="application/x-www-form-urlencoded"}if(i.headers){y=ce(y,i.headers)}const U=cn(r,t);let b=U.errors;const j=U.formData;if(i.values){ln(j,qn(i.values))}const V=qn(En(r));const v=ln(j,V);let w=hn(v,r);if(Q.config.getCacheBusterParam&&t==="get"){w.set("org.htmx.cache-buster",ee(c,"id")||"true")}if(n==null||n===""){n=ne().location.href}const S=bn(r,"hx-request");const _=ie(r).boosted;let E=Q.config.methodsThatUseUrlParams.indexOf(t)>=0;const C={boosted:_,useUrlParams:E,formData:w,parameters:An(w),unfilteredFormData:v,unfilteredParameters:An(v),headers:y,target:c,verb:t,errors:b,withCredentials:i.credentials||S.credentials||Q.config.withCredentials,timeout:i.timeout||S.timeout||Q.config.timeout,path:n,triggeringEvent:o};if(!he(r,"htmx:configRequest",C)){oe(s);m();return e}n=C.path;t=C.verb;y=C.headers;w=qn(C.parameters);b=C.errors;E=C.useUrlParams;if(b&&b.length>0){he(r,"htmx:validation:halted",C);oe(s);m();return e}const z=n.split("#");const $=z[0];const O=z[1];let R=n;if(E){R=$;const Z=!w.keys().next().done;if(Z){if(R.indexOf("?")<0){R+="?"}else{R+="&"}R+=an(w);if(O){R+="#"+O}}}if(!Tn(r,R,C)){fe(r,"htmx:invalidPath",C);oe(l);return e}p.open(t.toUpperCase(),R,true);p.overrideMimeType("text/html");p.withCredentials=C.withCredentials;p.timeout=C.timeout;if(S.noHeaders){}else{for(const k in y){if(y.hasOwnProperty(k)){const Y=y[k];Cn(p,k,Y)}}}const H={xhr:p,target:c,requestConfig:C,etc:i,boosted:_,select:X,pathInfo:{requestPath:n,finalRequestPath:R,responsePath:null,anchor:O}};p.onload=function(){try{const t=Hn(r);H.pathInfo.responsePath=On(p);M(r,H);if(H.keepIndicators!==true){Qt(T,q)}he(r,"htmx:afterRequest",H);he(r,"htmx:afterOnLoad",H);if(!le(r)){let e=null;while(t.length>0&&e==null){const n=t.shift();if(le(n)){e=n}}if(e){he(e,"htmx:afterRequest",H);he(e,"htmx:afterOnLoad",H)}}oe(s);m()}catch(e){fe(r,"htmx:onLoadError",ce({error:e},H));throw e}};p.onerror=function(){Qt(T,q);fe(r,"htmx:afterRequest",H);fe(r,"htmx:sendError",H);oe(l);m()};p.onabort=function(){Qt(T,q);fe(r,"htmx:afterRequest",H);fe(r,"htmx:sendAbort",H);oe(l);m()};p.ontimeout=function(){Qt(T,q);fe(r,"htmx:afterRequest",H);fe(r,"htmx:timeout",H);oe(l);m()};if(!he(r,"htmx:beforeRequest",H)){oe(s);m();return e}var T=Zt(r);var q=Yt(r);se(["loadstart","loadend","progress","abort"],function(t){se([p,p.upload],function(e){e.addEventListener(t,function(e){he(r,"htmx:xhr:"+t,{lengthComputable:e.lengthComputable,loaded:e.loaded,total:e.total})})})});he(r,"htmx:beforeSend",H);const J=E?null:mn(p,r,w);p.send(J);return e}function Nn(e,t){const n=t.xhr;let r=null;let o=null;if(R(n,/HX-Push:/i)){r=n.getResponseHeader("HX-Push");o="push"}else if(R(n,/HX-Push-Url:/i)){r=n.getResponseHeader("HX-Push-Url");o="push"}else if(R(n,/HX-Replace-Url:/i)){r=n.getResponseHeader("HX-Replace-Url");o="replace"}if(r){if(r==="false"){return{}}else{return{type:o,path:r}}}const i=t.pathInfo.finalRequestPath;const s=t.pathInfo.responsePath;const l=re(e,"hx-push-url");const c=re(e,"hx-replace-url");const u=ie(e).boosted;let a=null;let f=null;if(l){a="push";f=l}else if(c){a="replace";f=c}else if(u){a="push";f=s||i}if(f){if(f==="false"){return{}}if(f==="true"){f=s||i}if(t.pathInfo.anchor&&f.indexOf("#")===-1){f=f+"#"+t.pathInfo.anchor}return{type:a,path:f}}else{return{}}}function In(e,t){var n=new RegExp(e.code);return n.test(t.toString(10))}function Pn(e){for(var t=0;t0){E().setTimeout(e,x.swapDelay)}else{e()}}if(f){fe(o,"htmx:responseError",ce({error:"Response Status Error Code "+s.status+" from "+i.pathInfo.requestPath},i))}}const Mn={};function Xn(){return{init:function(e){return null},getSelectors:function(){return null},onEvent:function(e,t){return true},transformResponse:function(e,t,n){return e},isInlineSwap:function(e){return false},handleSwap:function(e,t,n,r){return false},encodeParameters:function(e,t,n){return null}}}function Fn(e,t){if(t.init){t.init(n)}Mn[e]=ce(Xn(),t)}function Bn(e){delete Mn[e]}function Un(e,n,r){if(n==undefined){n=[]}if(e==undefined){return n}if(r==undefined){r=[]}const t=te(e,"hx-ext");if(t){se(t.split(","),function(e){e=e.replace(/ /g,"");if(e.slice(0,7)=="ignore:"){r.push(e.slice(7));return}if(r.indexOf(e)<0){const t=Mn[e];if(t&&n.indexOf(t)<0){n.push(t)}}})}return Un(ue(c(e)),n,r)}var jn=false;ne().addEventListener("DOMContentLoaded",function(){jn=true});function Vn(e){if(jn||ne().readyState==="complete"){e()}else{ne().addEventListener("DOMContentLoaded",e)}}function _n(){if(Q.config.includeIndicatorStyles!==false){const e=Q.config.inlineStyleNonce?` nonce="${Q.config.inlineStyleNonce}"`:"";ne().head.insertAdjacentHTML("beforeend"," ."+Q.config.indicatorClass+"{opacity:0} ."+Q.config.requestClass+" ."+Q.config.indicatorClass+"{opacity:1; transition: opacity 200ms ease-in;} ."+Q.config.requestClass+"."+Q.config.indicatorClass+"{opacity:1; transition: opacity 200ms ease-in;} ")}}function zn(){const e=ne().querySelector('meta[name="htmx-config"]');if(e){return S(e.content)}else{return null}}function $n(){const e=zn();if(e){Q.config=ce(Q.config,e)}}Vn(function(){$n();_n();let e=ne().body;kt(e);const t=ne().querySelectorAll("[hx-trigger='restored'],[data-hx-trigger='restored']");e.addEventListener("htmx:abort",function(e){const t=e.target;const n=ie(t);if(n&&n.xhr){n.xhr.abort()}});const n=window.onpopstate?window.onpopstate.bind(window):null;window.onpopstate=function(e){if(e.state&&e.state.htmx){Wt();se(t,function(e){he(e,"htmx:restored",{document:ne(),triggerEvent:he})})}else{if(n){n(e)}}};E().setTimeout(function(){he(e,"htmx:load",{});e=null},0)});return Q}(); \ No newline at end of file diff --git a/src/ecommerce_agent/web/templates/agent_runs.html b/src/ecommerce_agent/web/templates/agent_runs.html new file mode 100644 index 0000000..a023da3 --- /dev/null +++ b/src/ecommerce_agent/web/templates/agent_runs.html @@ -0,0 +1,6 @@ +{% extends "base.html" %} +{% block content %} +
+ {% include "partials/_runs_table.html" %} +
+{% endblock %} diff --git a/src/ecommerce_agent/web/templates/analytics.html b/src/ecommerce_agent/web/templates/analytics.html new file mode 100644 index 0000000..69131dd --- /dev/null +++ b/src/ecommerce_agent/web/templates/analytics.html @@ -0,0 +1,28 @@ +{% extends "base.html" %} +{% block content %} +
+ {% if snapshots %} + + + + + + {% for s in snapshots %} + + + + + + + + + + + {% endfor %} + +
ListingCapturedViewsFavoritesOrdersRevenueConv.Fulfillment
{{ s.title }}{{ s.captured_at }}{{ s.views }}{{ s.favorites }}{{ s.orders }}${{ s.revenue }}{{ s.conversion }}{{ s.fulfillment|replace("_", " ") }}
+ {% else %} +
No analytics snapshots yet.
+ {% endif %} +
+{% endblock %} diff --git a/src/ecommerce_agent/web/templates/base.html b/src/ecommerce_agent/web/templates/base.html new file mode 100644 index 0000000..1510b63 --- /dev/null +++ b/src/ecommerce_agent/web/templates/base.html @@ -0,0 +1,34 @@ + + + + + + {{ title }} · Workshop + + + + + + + + + +
+ {% include "partials/_sidebar.html" %} +
+ {% include "partials/_topbar.html" %} +
+ {% block content %}{% endblock %} +
+
+
+ + diff --git a/src/ecommerce_agent/web/templates/design_studio.html b/src/ecommerce_agent/web/templates/design_studio.html new file mode 100644 index 0000000..78028b5 --- /dev/null +++ b/src/ecommerce_agent/web/templates/design_studio.html @@ -0,0 +1,113 @@ +{% extends "base.html" %} +{% from "partials/_macros.html" import chip, button, field_label %} +{% block content %} +
+
+ New design + {{ chip("v1 → generate", "live") }} +
+
+
{{ field_label("Title") }}
+
{{ field_label("Prompt", "preview available") }}
+ +
+ {{ field_label("Reference images", "0 / 4") }} + +
+ +
+ {{ field_label("Aspect ratio", "1024 × 1280") }} +
+ + + + +
+ + +
+ +
+
{{ field_label("Format") }} + +
+
{{ field_label("Est. cost") }}
—under cap
+
+ + + +
+ + +
+
+
+ +
+
+
+ Design Library + {{ line_count }} lines · {{ version_count }} versions +
+
+
{{ icon("search", 13) }} filter lines
+ {{ chip("all succeeded", "ok") }} +
+
+
+
+ {% for line in lines %} +
+
+
+
{{ line.title }}
+
{{ line.eyebrow }}
+
+
+ {{ chip(line.latest) }} + +
+
+
+ {% for t in line.thumbs %} +
+ {% if t.url %}{% else %}
{% endif %} +
{{ t.label }}
+
+ {% endfor %} +
+
+
+ Workshop score + {{ line.versions }} ver · {{ line.updated }} +
+
n/a
+
+
+ {{ icon("branch", 13) }} + {{ line.lineage }} + LINEAGE → +
+
+ {% else %} +
No designs yet. Use the composer to generate v1.
+ {% endfor %} +
+
+ {% include "partials/_studio_focus.html" %} +
+
+
+{% endblock %} diff --git a/src/ecommerce_agent/web/templates/etsy_posting.html b/src/ecommerce_agent/web/templates/etsy_posting.html new file mode 100644 index 0000000..02167fe --- /dev/null +++ b/src/ecommerce_agent/web/templates/etsy_posting.html @@ -0,0 +1,43 @@ +{% extends "base.html" %} +{% from "partials/_macros.html" import chip %} +{% block content %} +
+
+
+
+ Pipeline + {{ chip("etsy · mock", "warn") }} +
+
+
+ {% for stage in pipeline %} +
+
+ + {{ stage.name }} + {{ stage.cards|length }} + +
+ {% for it in stage.cards %} +
+
+ {% if it.image %}{% else %}
{% endif %} +
+
+
{{ it.title }}
+
${{ it.price }}
+
+ {{ icon("arrow", 16) }} +
+ {% endfor %} +
+ {% endfor %} + {% if not has_items %}
No listings in the pipeline yet.
{% endif %} +
+
+
+
+ {% include "partials/_listing_editor.html" %} +
+{% endblock %} diff --git a/src/ecommerce_agent/web/templates/partials/_listing_editor.html b/src/ecommerce_agent/web/templates/partials/_listing_editor.html new file mode 100644 index 0000000..f48fab9 --- /dev/null +++ b/src/ecommerce_agent/web/templates/partials/_listing_editor.html @@ -0,0 +1,102 @@ +{% from "partials/_macros.html" import chip, button, field_label %} +{% if not selected %} +
Select a listing to edit.
+{% else %} +
+
+

{{ selected.title }}

+ {{ chip(selected.stage, selected.stage_tone) }} +
+ +
+
+
+ {% if selected.images and selected.images[0].url %}{% else %}
{% endif %} +
LISTING
+
+
+ {% for n in range(4) %} +
+ {% if selected.images|length > n and selected.images[n].url %}{% else %}
{% endif %} +
{{ n + 1 }}
+
+ {% endfor %} +
+
{{ selected.image_count }} of {{ selected.image_cap }} images
+
+ +
+
+
{{ field_label("Price") }}
+
{{ field_label("Quantity") }}
+
{{ field_label("Taxonomy") }}
+
+ +
{{ field_label("Category") }}
{{ selected.category }}
+ +
+ {{ field_label("Title", "≤ 140 chars") }} + +
+ +
+ {{ field_label("Tags", "") }}{{ selected.tags|length }} / {{ selected.tag_cap }} +
+ {% for t in selected.tags %} + {{ t }} + {% endfor %} + {{ icon("plus", 11, 2) }} +
+
+ +
{{ field_label("Description") }}
+ +
+
+ {{ field_label("Materials") }} +
+ {% for m in selected.materials %}{{ m }}{% endfor %} + {% if not selected.materials %}none{% endif %} +
+
+
+ {{ field_label("Auto-renew", "cosmetic") }} +
+ AutomaticManual +
+
+
+ +
+ {{ field_label("Listing readiness") }} +
+ {% for cell in selected.readiness %} +
+ {% if cell.ok %}{{ icon("check", 12, 2.4) }}{% else %}{{ icon("plus", 12, 2.4) }}{% endif %} +
{{ cell.name }}
+
{{ cell.value }}
+
+ {% endfor %} +
+
+ + {% if selected.errors %} +
{% for e in selected.errors %}
{{ e }}
{% endfor %}
+ {% endif %} +
+
+ +
+ {{ button("Save", "check", "ghost", 'type="submit"') }} +
+ {% if saved %}changes saved · {{ selected.stage|lower }}{% endif %} + + +
+
+{% endif %} diff --git a/src/ecommerce_agent/web/templates/partials/_macros.html b/src/ecommerce_agent/web/templates/partials/_macros.html new file mode 100644 index 0000000..da15196 --- /dev/null +++ b/src/ecommerce_agent/web/templates/partials/_macros.html @@ -0,0 +1,74 @@ +{# Reusable B1 primitives. Colors come from CSS tokens; geometry is fixed. #} + +{% macro chip(text, tone="") -%} + + {%- if tone and tone != "plain" %}{% endif -%} + {{ text }} + +{%- endmacro %} + +{% macro button(label, icon_name="", variant="ghost", attrs="") -%} + +{%- endmacro %} + +{% macro field_label(text, hint="") -%} +
+ {{ text }} + {% if hint %}{{ hint }}{% endif %} +
+{%- endmacro %} + +{% macro score_bar(score) -%} +
+
+ {{ score }} +
+{%- endmacro %} + +{% macro thumb(label="", sub="", ratio=1, big=false) -%} +
+
+ {% if sub %}
{{ sub }}
{% endif %} + {% if label %}
{{ label }}
{% endif %} +
+{%- endmacro %} + +{% macro checkbox(checked=false, label="") -%} + +{%- endmacro %} + +{% macro model_dropdown(label, value, options, key) -%} +
+ + +
+{%- endmacro %} + +{% macro mode_toggle() -%} + +{%- endmacro %} diff --git a/src/ecommerce_agent/web/templates/partials/_report_list.html b/src/ecommerce_agent/web/templates/partials/_report_list.html new file mode 100644 index 0000000..e44b3c0 --- /dev/null +++ b/src/ecommerce_agent/web/templates/partials/_report_list.html @@ -0,0 +1,38 @@ +{% from "partials/_macros.html" import chip, score_bar %} +
+
+
+ Reports + {{ reports|length }} total +
+
+
+ {% for r in reports %} +
+
+
{{ r.title }}
+ {{ chip(r.status_label, r.status_tone) }} +
+
{{ r.niche }} · {{ r.sections }} sections
+
+ {% if r.score is not none %}{{ score_bar(r.score) }}{% else %}no score{% endif %} + {{ r.date }} +
+
+ {% else %} +
No research reports yet.
+ {% endfor %} +
+ +
diff --git a/src/ecommerce_agent/web/templates/partials/_report_reader.html b/src/ecommerce_agent/web/templates/partials/_report_reader.html new file mode 100644 index 0000000..ce9a0ae --- /dev/null +++ b/src/ecommerce_agent/web/templates/partials/_report_reader.html @@ -0,0 +1,56 @@ +{% from "partials/_macros.html" import chip, button %} +{% if not selected %} +
Select a report to read.
+{% else %} +
+
+
+
+ {{ chip(selected.status_label, selected.status_tone) }} + {{ selected.words }} words · {{ selected.date }} +
+

{{ selected.title }}

+
+ {% if selected.score is not none %} +
+
{{ selected.score }}
+
opportunity
+
+ {% endif %} +
+ + {% if selected.error %}
{{ selected.error }}
{% endif %} + +
+ +
+ {% for s in selected.report_sections %} +
+
{{ s.title }}
+
{{ s.html|safe }}
+
+ {% else %} +

No report content yet{% if selected.status_label != "FINAL" %} — generation {{ selected.status_label|lower }}{% endif %}.

+ {% endfor %} +
+
+ +
+ {{ button("Generate designs from report", "image", "primary", "onclick=\"location.href='/design-studio'\"") }} + {% if selected.can_download %} + {{ icon("down", 14) }} Export + {% endif %} +
+ {% if selected.can_retry %} +
+ +
+ {% endif %} +
+
+{% endif %} diff --git a/src/ecommerce_agent/web/templates/partials/_runs_table.html b/src/ecommerce_agent/web/templates/partials/_runs_table.html new file mode 100644 index 0000000..8bf9c3a --- /dev/null +++ b/src/ecommerce_agent/web/templates/partials/_runs_table.html @@ -0,0 +1,18 @@ +{% from "partials/_macros.html" import chip %} +{% if runs %} + + + + {% for r in runs %} + + + + + + + {% endfor %} + +
SubjectStatusMessageUpdated
{{ r.subject_type }}{{ chip(r.last_event, r.tone) }}{{ r.last_message }}{{ r.updated }}
+{% else %} +
No active jobs or recent events.
+{% endif %} diff --git a/src/ecommerce_agent/web/templates/partials/_sidebar.html b/src/ecommerce_agent/web/templates/partials/_sidebar.html new file mode 100644 index 0000000..0ec4572 --- /dev/null +++ b/src/ecommerce_agent/web/templates/partials/_sidebar.html @@ -0,0 +1,34 @@ + diff --git a/src/ecommerce_agent/web/templates/partials/_signals_meta.html b/src/ecommerce_agent/web/templates/partials/_signals_meta.html new file mode 100644 index 0000000..cd5985c --- /dev/null +++ b/src/ecommerce_agent/web/templates/partials/_signals_meta.html @@ -0,0 +1,3 @@ + + {%- if queued -%}discovery queued · just now{%- else -%}{{ trends|length }} signals · agent{%- endif -%} + diff --git a/src/ecommerce_agent/web/templates/partials/_studio_focus.html b/src/ecommerce_agent/web/templates/partials/_studio_focus.html new file mode 100644 index 0000000..e632bef --- /dev/null +++ b/src/ecommerce_agent/web/templates/partials/_studio_focus.html @@ -0,0 +1,45 @@ +{% from "partials/_macros.html" import button %} +{% if not focus %} +
Select a design line to inspect its versions.
+{% else %} +
+
+
+ {% if focus.thumb %}{% else %}
{% endif %} +
1024²
+
{{ focus.version }}
+
+
+ {% for v in focus.versions %} + {{ v.label }} + {% endfor %} +
+
+
+
+
+
Selected version
+
{{ focus.line_title }} · {{ focus.version }}
+
+
+
—
+
/ 100 score
+
+
+

{{ focus.prompt }}

+
+ {% for m in focus.meta %}{{ m }}{% endfor %} + {% if focus.bg_friendly %}bg-removal friendly{% endif %} +
+ +
+
+{% endif %} diff --git a/src/ecommerce_agent/web/templates/partials/_topbar.html b/src/ecommerce_agent/web/templates/partials/_topbar.html new file mode 100644 index 0000000..80f5bd2 --- /dev/null +++ b/src/ecommerce_agent/web/templates/partials/_topbar.html @@ -0,0 +1,18 @@ +{% from "partials/_macros.html" import chip, model_dropdown, mode_toggle, button %} +
+
+

{{ title }}

+ {% if subtitle %}
{{ subtitle }}
{% endif %} +
+
+
+ {{ chip(integrations, "live") }} + {{ model_dropdown("TXT", text_model, text_models, "text") }} + {{ model_dropdown("IMG", image_model, image_models, "image") }} + {{ mode_toggle() }} + {{ chip("cap $" ~ cap_usd) }} +
+ {% if primary %} + {{ button(primary.label, primary.get("icon", "plus"), "primary", primary.get("attrs", "")) }} + {% endif %} +
diff --git a/src/ecommerce_agent/web/templates/partials/_trend_detail.html b/src/ecommerce_agent/web/templates/partials/_trend_detail.html new file mode 100644 index 0000000..70bed17 --- /dev/null +++ b/src/ecommerce_agent/web/templates/partials/_trend_detail.html @@ -0,0 +1,79 @@ +{% from "partials/_macros.html" import chip, button, thumb %} +{% if not selected %} +
Select a signal to inspect.
+{% else %} +
+
+
+
+ {{ chip(selected.state_label, "live" if selected.is_rising else "warn") }} + {{ selected.niche }} +
+

{{ selected.title }}

+
+
+
{{ selected.momentum if selected.momentum is not none else "—" }}
+
momentum
+
+
+ +
+
Search volume
{{ selected.volume }}
+
First seen
{{ selected.first_seen }}
+
Sources
{{ selected.sources|length }}
+
+ + {% if selected.blurb %} +
+
Why it matters
+

{{ selected.blurb }}

+
+ {% endif %} + +
+
+
Evidence
+ {% if selected.evidence %} +
    + {% for e in selected.evidence %}
  • {{ e }}
  • {% endfor %} +
+ {% else %}

No evidence captured.

{% endif %} +
+
+
Momentum · 7 wk
+
{% for v in selected.spark %}{% endfor %}
+
7w agonow
+
+
+ + {% if selected.sources %} +
+
Verified sources
+ +
+ {% endif %} + + {% if selected.risks %} +
+
Risks & cautions
+ {% for r in selected.risks %}

{{ r }}

{% endfor %} +
+ {% endif %} + +
+
Suggested design directions
+
+ {{ thumb("LINEWORK", "dir·1") }}{{ thumb("DUOTONE", "dir·2") }}{{ thumb("SPECIMEN", "dir·3") }} +
+
+ +
+ {{ button("Spin up research report", "doc", "primary", 'onclick="location.href=\'/research-reports?trend_id=' ~ selected.id ~ '\'"') }} + {{ button("Send to Design Studio", "image", "ghost", 'onclick="location.href=\'/design-studio\'"') }} +
+ {{ button("Dismiss", "x", "destructive", 'disabled title="Dismiss is not wired yet"') }} +
+
+{% endif %} diff --git a/src/ecommerce_agent/web/templates/partials/_trend_list.html b/src/ecommerce_agent/web/templates/partials/_trend_list.html new file mode 100644 index 0000000..15da4b4 --- /dev/null +++ b/src/ecommerce_agent/web/templates/partials/_trend_list.html @@ -0,0 +1,38 @@ +
+
+
+ Signals + {% include "partials/_signals_meta.html" %} +
+
+ {% for key, label, _status in filters %} + {{ label }} + {% endfor %} +
+
+
+ {% for t in trends %} +
+
+
{{ t.momentum if t.momentum is not none else "—" }}
+
mom.
+
+
+
{{ t.title }}
+
+ {{ t.state_label }} + {{ t.niche }} +
+
+
{% for v in t.spark %}{% endfor %}
+
+ {% else %} +
No signals yet. Use Refresh trends to discover.
+ {% endfor %} +
+
diff --git a/src/ecommerce_agent/web/templates/printful_workshop.html b/src/ecommerce_agent/web/templates/printful_workshop.html new file mode 100644 index 0000000..53f4ae5 --- /dev/null +++ b/src/ecommerce_agent/web/templates/printful_workshop.html @@ -0,0 +1,28 @@ +{% extends "base.html" %} +{% from "partials/_macros.html" import chip %} +{% block content %} +
+
+ {{ chip("printful · " ~ printful_mode, "warn" if printful_mode == "mock" else "ok") }} + {{ chip(designs|length ~ " designs") }} +
+ {% if designs %} +
+ {% for d in designs %} +
+
+ {% if d.asset %}{% else %}
{% endif %} +
+
{{ d.title }}
+
+ {{ d.dims }} + {% if d.print_ready %}{{ chip("print-ready", "ok") }}{% else %}{{ chip("display", "sub") }}{% endif %} +
+
+ {% endfor %} +
+ {% else %} +
No merch designs promoted yet. Promote a generation from Design Studio.
+ {% endif %} +
+{% endblock %} diff --git a/src/ecommerce_agent/web/templates/product_drafts.html b/src/ecommerce_agent/web/templates/product_drafts.html new file mode 100644 index 0000000..6601bbd --- /dev/null +++ b/src/ecommerce_agent/web/templates/product_drafts.html @@ -0,0 +1,22 @@ +{% extends "base.html" %} +{% from "partials/_macros.html" import chip %} +{% block content %} +
+ {% if drafts %} +
+ {% for d in drafts %} +
+
+
{{ d.title }}
+ {{ chip(d.status|replace("_", " "), d.tone) }} +
+
{{ d.product_type }} · {{ d.audience }}
+
{{ d.currency }} {{ d.price }}
+
+ {% endfor %} +
+ {% else %} +
No product drafts yet. Approve trends and designs to populate this list.
+ {% endif %} +
+{% endblock %} diff --git a/src/ecommerce_agent/web/templates/research_reports.html b/src/ecommerce_agent/web/templates/research_reports.html new file mode 100644 index 0000000..06f5b35 --- /dev/null +++ b/src/ecommerce_agent/web/templates/research_reports.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} +{% block content %} +
+ {% include "partials/_report_list.html" %} +
+
+ {% include "partials/_report_reader.html" %} +
+{% endblock %} diff --git a/src/ecommerce_agent/web/templates/trend_inbox.html b/src/ecommerce_agent/web/templates/trend_inbox.html new file mode 100644 index 0000000..d2a5d92 --- /dev/null +++ b/src/ecommerce_agent/web/templates/trend_inbox.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} +{% block content %} +
+ {% include "partials/_trend_list.html" %} +
+
+ {% include "partials/_trend_detail.html" %} +
+{% endblock %} diff --git a/src/ecommerce_agent/web/trend_inbox.py b/src/ecommerce_agent/web/trend_inbox.py new file mode 100644 index 0000000..a9573b7 --- /dev/null +++ b/src/ecommerce_agent/web/trend_inbox.py @@ -0,0 +1,151 @@ +"""Trend Inbox screen — master/detail over real trends.""" + +import uuid +from typing import Annotated, Any + +from fastapi import Depends, Request +from fastapi.responses import HTMLResponse +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from ecommerce_agent.db.models import Trend, TrendDiscoveryRun +from ecommerce_agent.db.session import get_session +from ecommerce_agent.domain.enums import ResearchRunStatus +from ecommerce_agent.jobs.queue import enqueue +from ecommerce_agent.web.routes import router, shell_context, templates + +SessionDep = Annotated[AsyncSession, Depends(get_session)] + +SUBTITLE = "Signals surfaced by the research agent — triage, then route to a report or a design." + +# Real trend lifecycle status → display state chip (no momentum-state data exists). +_STATE = { + "discovered": ("NEW", "live"), + "pending_review": ("IN REVIEW", "warn"), + "approved": ("APPROVED", "ok"), + "rejected": ("REJECTED", "sub"), +} +# Filter tabs map to real statuses (design's Rising/Peaking have no backing). +FILTERS = [ + ("all", "All signals", None), + ("new", "New", "discovered"), + ("review", "In review", "pending_review"), + ("approved", "Approved", "approved"), +] + + +def _spark(seed: str) -> list[int]: + """Deterministic placeholder sparkline (no time-series data yet).""" + h = abs(hash(seed)) + return [30 + (h >> (i * 3)) % 70 for i in range(7)] + + +def _view(t: Trend) -> dict[str, Any]: + score = float(t.apparel_score) if t.apparel_score is not None else None + momentum = round(score / 5 * 100) if score is not None else None + label, tone = _STATE.get(t.status.value, (t.status.value.upper(), "sub")) + return { + "id": str(t.id), + "title": t.title, + "niche": t.niche, + "momentum": momentum, + "state_label": label, + "state_tone": tone, + "blurb": t.blurb or "", + "evidence": t.evidence or [], + "risks": t.risk_warnings or [], + "sources": t.verified_sources or [], + "confidence": round(float(t.confidence) * 100) if t.confidence is not None else None, + "first_seen": t.created_at.strftime("%b %d") if t.created_at else "—", + "volume": "—", # placeholder: no search-volume data + "spark": _spark(str(t.id)), + "is_rising": t.status.value == "discovered", + } + + +async def _load(session: AsyncSession, status: str | None) -> list[dict[str, Any]]: + stmt = select(Trend).order_by(Trend.created_at.desc(), Trend.position.asc()) + if status: + stmt = stmt.where(Trend.status == status) + rows = (await session.scalars(stmt.limit(200))).all() + return [_view(t) for t in rows] + + +async def _new_count(session: AsyncSession) -> int: + rows = (await session.scalars(select(Trend).where(Trend.status == "discovered"))).all() + return len(rows) + + +@router.get("/trend-inbox", response_class=HTMLResponse) +async def trend_inbox( + request: Request, session: SessionDep, filter: str = "all", sel: str | None = None +) -> HTMLResponse: + status = dict((f[0], f[2]) for f in FILTERS).get(filter) + trends = await _load(session, status) + selected = next((t for t in trends if t["id"] == sel), trends[0] if trends else None) + ctx = shell_context( + request, + active="Trend Inbox", + title="Trend Inbox", + subtitle=SUBTITLE, + primary={ + "label": "Refresh trends", + "icon": "bolt", + "attrs": 'hx-post="/trend-inbox/refresh" hx-target="#signals-meta" hx-swap="outerHTML"', + }, + badges={"Trend Inbox": str(await _new_count(session))} if trends else {}, + trends=trends, + selected=selected, + filters=FILTERS, + active_filter=filter, + ) + return templates.TemplateResponse(request=request, name="trend_inbox.html", context=ctx) + + +@router.get("/trend-inbox/list", response_class=HTMLResponse) +async def trend_list(request: Request, session: SessionDep, filter: str = "all") -> HTMLResponse: + status = dict((f[0], f[2]) for f in FILTERS).get(filter) + trends = await _load(session, status) + return templates.TemplateResponse( + request=request, + name="partials/_trend_list.html", + context={ + "request": request, + "trends": trends, + "selected": trends[0] if trends else None, + "filters": FILTERS, + "active_filter": filter, + }, + ) + + +@router.get("/trend-inbox/detail/{trend_id}", response_class=HTMLResponse) +async def trend_detail(request: Request, session: SessionDep, trend_id: str) -> HTMLResponse: + try: + pk = uuid.UUID(trend_id) + except ValueError: + pk = None + trend = await session.get(Trend, pk) if pk else None + selected = _view(trend) if trend else None + return templates.TemplateResponse( + request=request, + name="partials/_trend_detail.html", + context={"request": request, "selected": selected}, + ) + + +@router.post("/trend-inbox/refresh", response_class=HTMLResponse) +async def refresh_trends(request: Request, session: SessionDep) -> HTMLResponse: + limit = request.app.state.settings.research_trend_limit + run = TrendDiscoveryRun( + status=ResearchRunStatus.QUEUED, input_snapshot={"requested_limit": limit} + ) + session.add(run) + await session.flush() + await enqueue(session, "discover_trends", run.id) + await session.commit() + return templates.TemplateResponse( + request=request, + name="partials/_signals_meta.html", + context={"request": request, "queued": True}, + ) diff --git a/tests/smoke/test_api_and_dashboard.py b/tests/smoke/test_api_and_dashboard.py index 69ec095..538a390 100644 --- a/tests/smoke/test_api_and_dashboard.py +++ b/tests/smoke/test_api_and_dashboard.py @@ -1,4 +1,3 @@ -import importlib import io import uuid from dataclasses import replace @@ -12,13 +11,13 @@ from ecommerce_agent.api.main import create_app from ecommerce_agent.config import Settings -from ecommerce_agent.dashboard.design_lineage import lineage_paths from ecommerce_agent.db.models import Job, ResearchRun from ecommerce_agent.db.session import get_session from ecommerce_agent.domain.enums import JobStatus, ResearchRunStatus from ecommerce_agent.jobs.worker import process_one from ecommerce_agent.services.assets import SignedLocalAssetStore from ecommerce_agent.services.research import MockResearchProvider +from ecommerce_agent.web.design_lineage import lineage_paths def make_settings(tmp_path) -> Settings: @@ -943,8 +942,5 @@ async def _get_session(): assert response.headers["cache-control"] == "private, no-store" -def test_dashboard_modules_import() -> None: - importlib.import_module("ecommerce_agent.dashboard.app") - importlib.import_module("ecommerce_agent.dashboard.client") - importlib.import_module("ecommerce_agent.dashboard.pages.4_Design_Studio") - importlib.import_module("ecommerce_agent.dashboard.pages.6_Printful_Workshop") +def test_web_modules_import() -> None: + import ecommerce_agent.web # noqa: F401 (registers all screen routes) diff --git a/tests/unit/test_dashboard_client.py b/tests/unit/test_dashboard_client.py deleted file mode 100644 index b2a198b..0000000 --- a/tests/unit/test_dashboard_client.py +++ /dev/null @@ -1,28 +0,0 @@ -import httpx -import pytest - -from ecommerce_agent.dashboard.client import ApiError, request - - -def test_api_client_handles_non_json_server_errors(monkeypatch) -> None: - response = httpx.Response( - 500, - text="Internal Server Error", - request=httpx.Request("POST", "http://api.test/api/printful/drafts"), - ) - monkeypatch.setattr(httpx, "request", lambda *args, **kwargs: response) - - with pytest.raises(ApiError, match="status 500"): - request("POST", "/printful/drafts", json={}) - - -def test_api_client_preserves_json_error_details(monkeypatch) -> None: - response = httpx.Response( - 409, - json={"detail": "Draft validation failed."}, - request=httpx.Request("POST", "http://api.test/api/printful/drafts"), - ) - monkeypatch.setattr(httpx, "request", lambda *args, **kwargs: response) - - with pytest.raises(ApiError, match="Draft validation failed"): - request("POST", "/printful/drafts", json={}) diff --git a/tests/unit/test_design_lineage.py b/tests/unit/test_design_lineage.py index 5d2c732..fafb723 100644 --- a/tests/unit/test_design_lineage.py +++ b/tests/unit/test_design_lineage.py @@ -1,4 +1,4 @@ -from ecommerce_agent.dashboard.design_lineage import branch_label, lineage_paths, version_label +from ecommerce_agent.web.design_lineage import branch_label, lineage_paths, version_label def generation(id_: str, parent: str | None = None, created: int = 0) -> dict[str, object]: diff --git a/tests/web/conftest.py b/tests/web/conftest.py new file mode 100644 index 0000000..685319e --- /dev/null +++ b/tests/web/conftest.py @@ -0,0 +1,43 @@ +from collections.abc import AsyncIterator +from dataclasses import dataclass + +import pytest +from httpx import ASGITransport, AsyncClient +from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine +from sqlalchemy.pool import StaticPool + +from ecommerce_agent.api.main import create_app +from ecommerce_agent.config import Settings +from ecommerce_agent.db.base import Base +from ecommerce_agent.db.session import get_session + + +@dataclass +class WebHarness: + client: AsyncClient + sessions: async_sessionmaker + + +@pytest.fixture +async def web(tmp_path) -> AsyncIterator[WebHarness]: + engine = create_async_engine( + "sqlite+aiosqlite://", + connect_args={"check_same_thread": False}, + poolclass=StaticPool, + ) + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + sessions = async_sessionmaker(engine, expire_on_commit=False) + + app = create_app( + Settings(_env_file=None, app_env="test", printful_mode="mock", asset_root=tmp_path) + ) + + async def _session() -> AsyncIterator: + async with sessions() as session: + yield session + + app.dependency_overrides[get_session] = _session + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://t") as client: + yield WebHarness(client=client, sessions=sessions) + await engine.dispose() diff --git a/tests/web/test_design_studio.py b/tests/web/test_design_studio.py new file mode 100644 index 0000000..1d4d0f2 --- /dev/null +++ b/tests/web/test_design_studio.py @@ -0,0 +1,70 @@ +from decimal import Decimal + +from ecommerce_agent.db.models import ImageGeneration +from ecommerce_agent.domain.enums import ImageGenerationStatus + + +async def _seed_line(web) -> str: + async with web.sessions() as session: + root = ImageGeneration( + title="Cottagecore Mushroom Forager", + prompt="A fox foraging amanita mushrooms among ferns.", + status=ImageGenerationStatus.SUCCEEDED, + asset_uri="gen/root.png", + width=1024, + height=1024, + provider="mock", + model="gpt-image-2", + quality="medium", + output_format="png", + estimated_cost_usd=Decimal("0.04"), + ) + session.add(root) + await session.flush() + child = ImageGeneration( + parent_generation_id=root.id, + title="Cottagecore Mushroom Forager", + prompt="Revised palette, sage and ochre.", + status=ImageGenerationStatus.SUCCEEDED, + asset_uri="gen/child.png", + width=1024, + height=1024, + provider="mock", + model="gpt-image-2", + quality="medium", + output_format="png", + estimated_cost_usd=Decimal("0.04"), + ) + session.add(child) + await session.commit() + return str(child.id) + + +async def test_library_and_focus_render(web) -> None: + leaf_id = await _seed_line(web) + resp = await web.client.get("/design-studio") + assert resp.status_code == 200 + body = resp.text + assert "Design Library" in body + assert "Cottagecore Mushroom Forager" in body + assert "1 lines · 2 versions" in body + assert "Workshop score" in body and ">n/a<" in body # placeholder, not a fake number + assert "Revised palette" in body # focus shows latest version prompt + + focus = await web.client.get(f"/design-studio/focus/{leaf_id}?v=0") + assert focus.status_code == 200 + assert "A fox foraging" in focus.text # v0 prompt + + +async def test_delete_line(web) -> None: + leaf_id = await _seed_line(web) + resp = await web.client.post(f"/design-studio/lines/{leaf_id}/delete", follow_redirects=False) + assert resp.status_code == 303 + page = await web.client.get("/design-studio") + assert "No designs yet" in page.text + + +async def test_empty_state(web) -> None: + resp = await web.client.get("/design-studio") + assert resp.status_code == 200 + assert "No designs yet" in resp.text diff --git a/tests/web/test_etsy_posting.py b/tests/web/test_etsy_posting.py new file mode 100644 index 0000000..ebbb889 --- /dev/null +++ b/tests/web/test_etsy_posting.py @@ -0,0 +1,75 @@ +from decimal import Decimal + +from ecommerce_agent.db.models import ListingImage, PostingItem, PostingSession +from ecommerce_agent.domain.enums import ListingImageSource, PostingItemStatus + + +async def _seed(web) -> str: + async with web.sessions() as session: + posting = PostingSession(name="Fall drop") + session.add(posting) + await session.flush() + item = PostingItem( + session_id=posting.id, + printful_template_id=101, + template_snapshot={"name": "Amanita Forager Tee"}, + status=PostingItemStatus.SELECTED, + title="Amanita Forager Heavyweight Tee", + description="Hand-drawn amanita tee.", + tags=["amanita tee", "cottagecore"], + materials=["Ringspun cotton"], + retail_price=Decimal("28.00"), + quantity=25, + taxonomy_id=1059, + ) + session.add(item) + await session.flush() + session.add( + ListingImage( + posting_item_id=item.id, + source=ListingImageSource.PRINTFUL, + source_url="https://example.com/mockup.png", + position=0, + approved=True, + ) + ) + await session.commit() + return str(item.id) + + +async def test_pipeline_and_editor_render(web) -> None: + await _seed(web) + resp = await web.client.get("/etsy-posting") + assert resp.status_code == 200 + body = resp.text + assert "Pipeline" in body and "Draft" in body + assert "Amanita Forager Heavyweight Tee" in body + assert "Listing readiness" in body + assert "Publish to Etsy" in body + assert "data-tag-count>2<" in body and "/ 13" in body # tag count + + +async def test_save_updates_item(web) -> None: + item_id = await _seed(web) + resp = await web.client.post( + f"/etsy-posting/items/{item_id}/save", + data={ + "title": "Renamed Tee", + "description": "Updated copy.", + "retail_price": "30.00", + "quantity": "10", + "taxonomy_id": "1059", + "tags": ["one", "two", "three"], + "materials": ["Cotton"], + }, + ) + assert resp.status_code == 200 + assert "Renamed Tee" in resp.text + assert "changes saved" in resp.text + assert "data-tag-count>3<" in resp.text + + +async def test_empty_pipeline(web) -> None: + resp = await web.client.get("/etsy-posting") + assert resp.status_code == 200 + assert "No listings in the pipeline yet." in resp.text diff --git a/tests/web/test_group_b.py b/tests/web/test_group_b.py new file mode 100644 index 0000000..0a3885d --- /dev/null +++ b/tests/web/test_group_b.py @@ -0,0 +1,78 @@ +import uuid +from datetime import UTC, datetime +from decimal import Decimal + +from ecommerce_agent.db.models import AnalyticsSnapshot, MerchDesign, Product +from ecommerce_agent.domain.enums import ProductStatus + + +async def test_product_drafts(web) -> None: + async with web.sessions() as session: + session.add( + Product( + title="Amanita Tee", + audience="cottagecore fans", + product_type="t-shirt", + status=ProductStatus.READY_FOR_MANUAL_PUBLISH, + listing_copy={"title": "Amanita Forager Tee"}, + retail_price=Decimal("28.00"), + currency="USD", + ) + ) + await session.commit() + resp = await web.client.get("/product-drafts") + assert resp.status_code == 200 + assert "Amanita Forager Tee" in resp.text + assert "USD 28.00" in resp.text + + +async def test_analytics(web) -> None: + async with web.sessions() as session: + product = Product(title="Tee", audience="a", product_type="t-shirt") + session.add(product) + await session.flush() + session.add( + AnalyticsSnapshot( + product_id=product.id, + captured_at=datetime.now(UTC), + views=120, + favorites=8, + orders=3, + revenue=Decimal("84.00"), + conversion_rate=Decimal("0.025"), + ) + ) + await session.commit() + resp = await web.client.get("/analytics") + assert resp.status_code == 200 + assert "120" in resp.text and "$84.00" in resp.text and "2.5%" in resp.text + + +async def test_printful_workshop(web) -> None: + async with web.sessions() as session: + session.add( + MerchDesign( + source_type="generation", + source_id=str(uuid.uuid4()), + title="Mushroom Forager", + asset_uri="gen/x.png", + print_asset_uri="gen/x-print.png", + checksum="abc", + width=1024, + height=1024, + ) + ) + await session.commit() + resp = await web.client.get("/printful-workshop") + assert resp.status_code == 200 + assert "Mushroom Forager" in resp.text + assert "print-ready" in resp.text + assert "printful · mock" in resp.text + + +async def test_agent_runs_empty(web) -> None: + resp = await web.client.get("/agent-runs") + assert resp.status_code == 200 + assert "No active jobs or recent events." in resp.text + table = await web.client.get("/agent-runs/table") + assert table.status_code == 200 diff --git a/tests/web/test_research_reports.py b/tests/web/test_research_reports.py new file mode 100644 index 0000000..f31ee65 --- /dev/null +++ b/tests/web/test_research_reports.py @@ -0,0 +1,67 @@ +import uuid +from decimal import Decimal + +from ecommerce_agent.db.models import ResearchRun, Trend +from ecommerce_agent.domain.enums import ResearchRunStatus, TrendStatus + +REPORT_MD = """# Market summary + +Demand for **mushroom** motifs is rising sharply. + +# Competitive landscape + +- 37 established shops +- mostly stock photography +""" + + +async def _seed(web) -> None: + async with web.sessions() as session: + trend = Trend( + research_batch_id=uuid.uuid4(), + title="Cottagecore mushroom apparel", + niche="cottagecore", + source="agent", + apparel_score=Decimal("4.3"), + status=TrendStatus.APPROVED, + ) + session.add(trend) + await session.flush() + session.add( + ResearchRun( + trend_id=trend.id, + topic_title="Cottagecore mushroom apparel", + status=ResearchRunStatus.COMPLETED, + markdown_report=REPORT_MD, + ) + ) + await session.commit() + + +async def test_reading_pane_renders_markdown_and_toc(web) -> None: + await _seed(web) + resp = await web.client.get("/research-reports") + assert resp.status_code == 200 + body = resp.text + assert "Cottagecore mushroom apparel" in body + assert "Market summary" in body and "Competitive landscape" in body # TOC + section heads + assert "mushroom" in body # inline markdown rendered + assert "
  • 37 established shops
  • " in body # list rendered + assert "86" in body # 4.3/5 → 86 opportunity score (derived from trend) + + +async def test_new_report_form_creates_run(web) -> None: + resp = await web.client.post( + "/research-reports/new", + data={"topic_title": "Retro parks", "additional_context": ""}, + follow_redirects=False, + ) + assert resp.status_code == 303 + listing = await web.client.get("/research-reports") + assert "Retro parks" in listing.text + + +async def test_empty_state(web) -> None: + resp = await web.client.get("/research-reports") + assert resp.status_code == 200 + assert "No research reports yet." in resp.text diff --git a/tests/web/test_trend_inbox.py b/tests/web/test_trend_inbox.py new file mode 100644 index 0000000..636fe43 --- /dev/null +++ b/tests/web/test_trend_inbox.py @@ -0,0 +1,85 @@ +import uuid +from collections.abc import AsyncIterator +from decimal import Decimal + +import pytest +from httpx import ASGITransport, AsyncClient +from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine +from sqlalchemy.pool import StaticPool + +from ecommerce_agent.api.main import create_app +from ecommerce_agent.config import Settings +from ecommerce_agent.db.base import Base +from ecommerce_agent.db.models import Trend +from ecommerce_agent.db.session import get_session +from ecommerce_agent.domain.enums import TrendStatus + + +@pytest.fixture +async def client(tmp_path) -> AsyncIterator[AsyncClient]: + engine = create_async_engine( + "sqlite+aiosqlite://", + connect_args={"check_same_thread": False}, + poolclass=StaticPool, + ) + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + sessionmaker = async_sessionmaker(engine, expire_on_commit=False) + + async with sessionmaker() as session: + session.add( + Trend( + research_batch_id=uuid.uuid4(), + title="Mushroom foraging core", + niche="cottagecore · apparel", + source="agent", + blurb="Forager aesthetic is spiking.", + apparel_score=Decimal("4.6"), + confidence=Decimal("0.78"), + status=TrendStatus.DISCOVERED, + evidence=["Etsy searches up 34%"], + verified_sources=[{"title": "Etsy Trends", "url": "https://example.com"}], + risk_warnings=["Avoid edibility claims."], + ) + ) + await session.commit() + + app = create_app( + Settings(_env_file=None, app_env="test", printful_mode="mock", asset_root=tmp_path) + ) + + async def _session() -> AsyncIterator: + async with sessionmaker() as session: + yield session + + app.dependency_overrides[get_session] = _session + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://t") as c: + yield c + await engine.dispose() + + +async def test_trend_inbox_renders_shell_and_signal(client: AsyncClient) -> None: + resp = await client.get("/trend-inbox") + assert resp.status_code == 200 + body = resp.text + assert 'data-theme="light"' in body + assert "ws-sidebar" in body and "ws-topbar" in body + assert "Mushroom foraging core" in body + assert "92" in body # 4.6/5 → 92 momentum + assert "Avoid edibility claims." in body + + +async def test_trend_filter_and_detail_fragments(client: AsyncClient) -> None: + listing = await client.get("/trend-inbox/list?filter=new") + assert listing.status_code == 200 + assert "Mushroom foraging core" in listing.text + + empty = await client.get("/trend-inbox/list?filter=approved") + assert empty.status_code == 200 + assert "No signals yet" in empty.text + + +async def test_home_redirects_to_trend_inbox(client: AsyncClient) -> None: + resp = await client.get("/", follow_redirects=False) + assert resp.status_code in (302, 307) + assert resp.headers["location"] == "/trend-inbox" From 8a3b7a9a3e15b7aadfc085047e3a25c0e5a56740 Mon Sep 17 00:00:00 2001 From: Kevin Reznicek Date: Wed, 17 Jun 2026 18:59:13 -0700 Subject: [PATCH 2/2] Fix stale dependency lockfile --- uv.lock | 460 +------------------------------------------------------- 1 file changed, 2 insertions(+), 458 deletions(-) diff --git a/uv.lock b/uv.lock index e45905d..81e0400 100644 --- a/uv.lock +++ b/uv.lock @@ -30,22 +30,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/29/6533c317b74f707ea28f8d633734dbda2119bbadfc61b2f3640ba835d0f7/alembic-1.18.4-py3-none-any.whl", hash = "sha256:a5ed4adcf6d8a4cb575f3d759f071b03cd6e5c7618eb796cb52497be25bfe19a", size = 263893, upload-time = "2026-02-10T16:00:49.997Z" }, ] -[[package]] -name = "altair" -version = "6.2.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jinja2" }, - { name = "jsonschema" }, - { name = "narwhals" }, - { name = "packaging" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/86/97/9a0dc61efd4f2dee29cb6d8edbbacdb789ce48cbffd98efa2b3ab145b297/altair-6.2.1.tar.gz", hash = "sha256:ca0298fa20b1a4fae22eff8847b95f74912bd90544013ad36af192119883ea64", size = 766468, upload-time = "2026-06-05T16:23:36.57Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/78/b556548d92b9e29ae68a86e7b416888820900809e189e39caf308c7d44a3/altair-6.2.1-py3-none-any.whl", hash = "sha256:bf2fee3733c3a31a588e45b857a2495a88d506970deb87f74e1613f0247446b1", size = 797498, upload-time = "2026-06-05T16:23:34.799Z" }, -] - [[package]] name = "annotated-doc" version = "0.0.4" @@ -101,24 +85,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/91/66/b25ccb84a246b470eb943b0107c07edcae51804912b824054b3413995a10/asyncpg-0.31.0-cp313-cp313-win_amd64.whl", hash = "sha256:dc5f2fa9916f292e5c5c8b2ac2813763bcd7f58e130055b4ad8a0531314201ab", size = 596569, upload-time = "2025-11-24T23:26:16.189Z" }, ] -[[package]] -name = "attrs" -version = "26.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, -] - -[[package]] -name = "blinker" -version = "1.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, -] - [[package]] name = "boto3" version = "1.43.29" @@ -147,15 +113,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/91/b1/aa410c22355f8f6c4ac2433db6a1c557dd959acf2953ccae4bfc37488119/botocore-1.43.29-py3-none-any.whl", hash = "sha256:5d62f2a03ed279a50207ca2824e009313df15f082b6bb591a095a4f04c7faef3", size = 15194135, upload-time = "2026-06-12T19:32:09.463Z" }, ] -[[package]] -name = "cachetools" -version = "7.1.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f4/8b/0d3945a13955303b81272f759a0331e54c5c793da455e6f5706b89d2639c/cachetools-7.1.4.tar.gz", hash = "sha256:437f55a4e0c1b01a4f3077cc470e6991d47430970e36fbcb77e2be0df4fc1cd6", size = 40085, upload-time = "2026-05-21T22:40:43.376Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8c/7b/1fc1c09cc0756cf25861a3be10565915953876da48bb228fb9a672b20a42/cachetools-7.1.4-py3-none-any.whl", hash = "sha256:323dc4127934744db5b54eb4924482d7edafbf9554e820d1531c2e08c0e4ef54", size = 16761, upload-time = "2026-05-21T22:40:41.845Z" }, -] - [[package]] name = "certifi" version = "2026.5.20" @@ -200,47 +157,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, ] -[[package]] -name = "charset-normalizer" -version = "3.4.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, - { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, - { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, - { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, - { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, - { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, - { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, - { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, - { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, - { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, - { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, - { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, - { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, - { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, - { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, - { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, - { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, - { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, - { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, - { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, - { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, - { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, - { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, - { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, - { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, - { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, - { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, - { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, - { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, - { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, - { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, - { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, - { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, -] - [[package]] name = "click" version = "8.4.1" @@ -362,11 +278,11 @@ dependencies = [ { name = "cryptography" }, { name = "fastapi" }, { name = "httpx" }, + { name = "jinja2" }, { name = "pillow" }, { name = "pydantic-settings" }, { name = "python-multipart" }, { name = "sqlalchemy", extra = ["asyncio"] }, - { name = "streamlit" }, { name = "uvicorn", extra = ["standard"] }, ] @@ -389,6 +305,7 @@ requires-dist = [ { name = "cryptography", specifier = ">=44,<46" }, { name = "fastapi", specifier = ">=0.115,<1" }, { name = "httpx", specifier = ">=0.28,<1" }, + { name = "jinja2", specifier = ">=3,<4" }, { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.14,<2" }, { name = "pillow", specifier = ">=11,<13" }, { name = "pydantic-settings", specifier = ">=2.7,<3" }, @@ -398,7 +315,6 @@ requires-dist = [ { name = "python-multipart", specifier = ">=0.0.20,<1" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.9,<1" }, { name = "sqlalchemy", extras = ["asyncio"], specifier = ">=2.0.36,<3" }, - { name = "streamlit", specifier = ">=1.41,<2" }, { name = "uvicorn", extras = ["standard"], specifier = ">=0.34,<1" }, ] provides-extras = ["dev"] @@ -419,30 +335,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e0/82/45359b62a067409bd929ae8a56b8ed13e5a8c8a61194b3c236920999ab83/fastapi-0.136.3-py3-none-any.whl", hash = "sha256:3d2a69bdf04b7e9f3afa292c3bc7a98816bbfafa10bc9b45f3f3700d2f761620", size = 117481, upload-time = "2026-05-23T18:53:16.924Z" }, ] -[[package]] -name = "gitdb" -version = "4.0.12" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "smmap" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" }, -] - -[[package]] -name = "gitpython" -version = "3.1.50" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "gitdb" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/33/f6/354ae6491228b5eb40e10d89c4d13c651fe1cf7556e35ebdded50cff57ce/gitpython-3.1.50.tar.gz", hash = "sha256:80da2d12504d52e1f998772dc5baf6e553f8d2fcfe1fcc226c9d9a2ee3372dcc", size = 219798, upload-time = "2026-05-06T04:01:26.571Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl", hash = "sha256:d352abe2908d07355014abdd21ddf798c2a961469239afec4962e9da884858f9", size = 212507, upload-time = "2026-05-06T04:01:23.799Z" }, -] - [[package]] name = "greenlet" version = "3.5.1" @@ -548,15 +440,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] -[[package]] -name = "itsdangerous" -version = "2.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, -] - [[package]] name = "jinja2" version = "3.1.6" @@ -578,33 +461,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, ] -[[package]] -name = "jsonschema" -version = "4.26.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "attrs" }, - { name = "jsonschema-specifications" }, - { name = "referencing" }, - { name = "rpds-py" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, -] - -[[package]] -name = "jsonschema-specifications" -version = "2025.9.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "referencing" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, -] - [[package]] name = "librt" version = "0.11.0" @@ -730,55 +586,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] -[[package]] -name = "narwhals" -version = "2.22.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/62/3c/c4ef2164a71c1a63d7f1ae411c4082c5fa872405106db60a4b7114989ad7/narwhals-2.22.1.tar.gz", hash = "sha256:d62920805a0a43b7ff8b54b0c0d3142d796f8a9301836ada37e573d6a33cbcd9", size = 647493, upload-time = "2026-06-05T12:34:34.051Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/48/ca/36339329c4604adbcc99c899b7eb1ce1a555c499b6a6860757dc9bfed36d/narwhals-2.22.1-py3-none-any.whl", hash = "sha256:60567d774edf77db53906f89d9fbd164e66e56d66d388e1e6990f17ac33cfb53", size = 454815, upload-time = "2026-06-05T12:34:32.289Z" }, -] - -[[package]] -name = "numpy" -version = "2.4.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/95/2a/3d7b5ac8aac24feaf9ad7ed58f45b0bbc06d37e4338ae84c9f2298b570f9/numpy-2.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1", size = 16689119, upload-time = "2026-05-18T23:33:54.065Z" }, - { url = "https://files.pythonhosted.org/packages/ea/12/92c4c131527599e8288d6918e888d88726f84d805d784b771f32408aeaef/numpy-2.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb", size = 14699246, upload-time = "2026-05-18T23:33:57.621Z" }, - { url = "https://files.pythonhosted.org/packages/ad/fe/c0a6b7b2ca128a8fb228575147073b660656734b8ebe4d76c8fd748dcc79/numpy-2.4.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41", size = 5204410, upload-time = "2026-05-18T23:34:00.302Z" }, - { url = "https://files.pythonhosted.org/packages/f3/d4/9770d14ba719432bb90a421bfd443872ed0f70f7264b64bec12ea363d5fd/numpy-2.4.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698", size = 6551240, upload-time = "2026-05-18T23:34:02.852Z" }, - { url = "https://files.pythonhosted.org/packages/c9/c6/50a46a6205feba2343f1d6d17438107c5dc491ed1c736e6ea68689fd906b/numpy-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f", size = 15671012, upload-time = "2026-05-18T23:34:05.485Z" }, - { url = "https://files.pythonhosted.org/packages/99/60/14115e6364fa676c5397c2ad3004e527e9aa487abf5d0706ec81bbd08529/numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853", size = 16645538, upload-time = "2026-05-18T23:34:09.265Z" }, - { url = "https://files.pythonhosted.org/packages/ae/c5/693cbe59e57db94d2231fa519ca3978dc9e19da5a8f088588f5c6e947ff2/numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a", size = 17020706, upload-time = "2026-05-18T23:34:13.053Z" }, - { url = "https://files.pythonhosted.org/packages/ef/fc/85b7c4eff9b4966ade25c2273cf7e7012e92366c032058653934b37de044/numpy-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2", size = 18368541, upload-time = "2026-05-18T23:34:17.024Z" }, - { url = "https://files.pythonhosted.org/packages/f6/81/e1b27545deedce7f4a0b348618c6b62d74e36a4dc9ccd42f3eb2f85eee32/numpy-2.4.6-cp312-cp312-win32.whl", hash = "sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45", size = 5962825, upload-time = "2026-05-18T23:34:20.3Z" }, - { url = "https://files.pythonhosted.org/packages/ab/ca/feab00bd44aa5fe1ad2c18f08b4d3bb92e26484b0b1d1443897809ed528c/numpy-2.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751", size = 12321687, upload-time = "2026-05-18T23:34:23.095Z" }, - { url = "https://files.pythonhosted.org/packages/63/cf/5a6d34850a39d1093558564f77ee8e8e0bee5061151b8f05a55711001ec7/numpy-2.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8", size = 10221482, upload-time = "2026-05-18T23:34:25.876Z" }, - { url = "https://files.pythonhosted.org/packages/fb/82/bdab26d7438c6791ca31b7c024ca37c1eab8b726ba236129005cd4a06e45/numpy-2.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0", size = 16684648, upload-time = "2026-05-18T23:34:29.41Z" }, - { url = "https://files.pythonhosted.org/packages/1b/30/a80189bcc7f5e4258b3fbc3968d909d1756f54d023299ecc39ad6fdb9ef8/numpy-2.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb", size = 14693902, upload-time = "2026-05-18T23:34:33.013Z" }, - { url = "https://files.pythonhosted.org/packages/97/12/70b5d0d7c15e1ebb8a6a84a8caa1d19e181d84fb58bb6d70aca29099dec1/numpy-2.4.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f", size = 5198992, upload-time = "2026-05-18T23:34:36.132Z" }, - { url = "https://files.pythonhosted.org/packages/ba/8c/ebd2a8f8a83541f8d38cc5667e8c2b69cecfd30da6e45693e8158857d44b/numpy-2.4.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3", size = 6546944, upload-time = "2026-05-18T23:34:38.484Z" }, - { url = "https://files.pythonhosted.org/packages/bb/c5/7b863a97a91671a0338f4253bd3b5a3d3852f0692dae91711c9f4a10e787/numpy-2.4.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b", size = 15669392, upload-time = "2026-05-18T23:34:41.257Z" }, - { url = "https://files.pythonhosted.org/packages/a5/9d/3584b9984ca4c047aea75214ce1a4c4c73d849bd71b604264b7f5653f8a8/numpy-2.4.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089", size = 16633220, upload-time = "2026-05-18T23:34:45.075Z" }, - { url = "https://files.pythonhosted.org/packages/05/ae/7c67fba23bd98caec7c99261f3a16072ade14813486b0282cb29846de832/numpy-2.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a", size = 17020800, upload-time = "2026-05-18T23:34:49.065Z" }, - { url = "https://files.pythonhosted.org/packages/d9/5d/3b6725cb31d983c5e66916f5d36f6d7e5521129e4c4404d64f918292a5b6/numpy-2.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605", size = 18357600, upload-time = "2026-05-18T23:34:52.709Z" }, - { url = "https://files.pythonhosted.org/packages/f7/da/2ccc6c2fe8898dee01d90c75c5f5f914a23daf99e3e0f59516a08760c8b5/numpy-2.4.6-cp313-cp313-win32.whl", hash = "sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91", size = 5961134, upload-time = "2026-05-18T23:34:55.618Z" }, - { url = "https://files.pythonhosted.org/packages/b5/cd/9cc4dc876fb065d5c220aae4d5e14826b2715331bb7618ce1fb07a679d99/numpy-2.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359", size = 12318598, upload-time = "2026-05-18T23:34:58.928Z" }, - { url = "https://files.pythonhosted.org/packages/39/1e/c0bcba1f8694116485fe28fd1be698c278fcda4141c5b0e53a2aed8b12a8/numpy-2.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778", size = 10222272, upload-time = "2026-05-18T23:35:02.167Z" }, - { url = "https://files.pythonhosted.org/packages/63/6d/cc5619247c8f4204e507f5883528372e4ac4bb189e579fb859a12e480b1f/numpy-2.4.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1", size = 14821197, upload-time = "2026-05-18T23:35:05.468Z" }, - { url = "https://files.pythonhosted.org/packages/00/58/f1c39161c87d9e9bed660f1ed4bafc0e403d5ec9650b6dd77aead07d489b/numpy-2.4.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe", size = 5326287, upload-time = "2026-05-18T23:35:08.693Z" }, - { url = "https://files.pythonhosted.org/packages/af/57/3917ab0fd97f271a8694513581b8a36c655f111c446852c302f04ccdb6fc/numpy-2.4.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997", size = 6646763, upload-time = "2026-05-18T23:35:11.459Z" }, - { url = "https://files.pythonhosted.org/packages/eb/0f/037e64c494b67581ae18193d770adef354c41f3f2c8ebf865602d949bf8f/numpy-2.4.6-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20", size = 15728070, upload-time = "2026-05-18T23:35:14.79Z" }, - { url = "https://files.pythonhosted.org/packages/21/a6/5d2bae9c9542eb4df16dc9c46dc79c186e9bad53805dfa5399a6023c6db0/numpy-2.4.6-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d", size = 16681752, upload-time = "2026-05-18T23:35:18.836Z" }, - { url = "https://files.pythonhosted.org/packages/92/14/23d1dfb410ae362cd59ce53e936b1513d545eb40db3949ced632e19a459e/numpy-2.4.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67", size = 17086024, upload-time = "2026-05-18T23:35:22.52Z" }, - { url = "https://files.pythonhosted.org/packages/4b/6e/23595a2c642cdf3bc567877064bdd7f91c8b0038a4453cf2daf7248eafe9/numpy-2.4.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd", size = 18403398, upload-time = "2026-05-18T23:35:26.398Z" }, - { url = "https://files.pythonhosted.org/packages/8a/90/0ac3bc947217e66dec77e7cbc6a1979d1af70b6461b82f620d3bccd5e4c8/numpy-2.4.6-cp313-cp313t-win32.whl", hash = "sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab", size = 6084971, upload-time = "2026-05-18T23:35:29.387Z" }, - { url = "https://files.pythonhosted.org/packages/77/71/5673e351671a1d2bd6063b91b44f70c0affea7d1516fa7a6572941ba4aa1/numpy-2.4.6-cp313-cp313t-win_amd64.whl", hash = "sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75", size = 12458532, upload-time = "2026-05-18T23:35:32.175Z" }, - { url = "https://files.pythonhosted.org/packages/3f/88/19d3503c5046e688f049274b27a3ef3d771152fa80d3ba3d01a3dff61abe/numpy-2.4.6-cp313-cp313t-win_arm64.whl", hash = "sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd", size = 10291881, upload-time = "2026-05-18T23:35:35.465Z" }, -] - [[package]] name = "packaging" version = "26.2" @@ -788,42 +595,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, ] -[[package]] -name = "pandas" -version = "3.0.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, - { name = "python-dateutil" }, - { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f8/87/4341c6252d1c47b08768c3d25ac487362bf403f0313ddae4a2a26c9b1b4c/pandas-3.0.3.tar.gz", hash = "sha256:696a4a00a2a2a35d4e5deb3fc946641b96c944f02230e4f76137fe35d806c4fc", size = 4651414, upload-time = "2026-05-11T18:54:29.21Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/24/f1/392f8c5bfc16f66a0d2d41561c01627c228fe7ed2a0d056ef11315042570/pandas-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fed2ff7fd9779120e388e285fc029bd5cf9490cdd2e4166a9ee22c0e49a9ab09", size = 10357846, upload-time = "2026-05-11T18:52:36.143Z" }, - { url = "https://files.pythonhosted.org/packages/cf/3d/b16412745651e855f357e5e66930248688378853a6e2698a214e331fba1f/pandas-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b168fc218fd80a6cbdbdbc1a97ddc7889ed057d7eb45f50d866ceab5f39904c4", size = 9899550, upload-time = "2026-05-11T18:52:38.976Z" }, - { url = "https://files.pythonhosted.org/packages/31/a8/fa2535168fffcedf67f4f6de28d2dd903a747ca7c8ea6989451aaeb3a92f/pandas-3.0.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0383c72c75cdcca61a9e116e611143902dbfd08bff356829c2f6d1cf40a9ca8c", size = 10412965, upload-time = "2026-05-11T18:52:41.915Z" }, - { url = "https://files.pythonhosted.org/packages/65/b6/09b01cdbc15224e2850365192d17b7bdebb8bdbd8780ed221fcdf0d9a515/pandas-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6dc0b3fd2169c9157deed50b4d519553a3655c8c6a96027136d654592be973a9", size = 10894600, upload-time = "2026-05-11T18:52:45.02Z" }, - { url = "https://files.pythonhosted.org/packages/c9/a4/2eb28f2fccb4ced4a2c79ab2a5dee9ade1ebf44922ebad6fea158c9f95d4/pandas-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7e65d5407dc0b394f509699650e4a2ec01c0514f21850f453fa60f3be79a5dbf", size = 11422824, upload-time = "2026-05-11T18:52:48.058Z" }, - { url = "https://files.pythonhosted.org/packages/f8/45/830bb57f533a4604b355e07edcb8ea18cf88b5f94e5fca92f27052d7c597/pandas-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8894dc474d648fe7b6ff0ca9b0bd73950d19952bc1a6534540762c5d79d305c", size = 11950889, upload-time = "2026-05-11T18:52:50.905Z" }, - { url = "https://files.pythonhosted.org/packages/b9/c5/fc1b368f303087d20e8c9bf3d6ceb186263cfac0ade735cd938538bea839/pandas-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:c7be265b62cef88e253a941e4698604973736dcfe242fdb5198f0f7bc473cdcc", size = 9755463, upload-time = "2026-05-11T18:52:53.386Z" }, - { url = "https://files.pythonhosted.org/packages/86/bd/fda8f9705b1b09c6ebe14bfc0fa0e4ec8584d54ea673628f157ff55131af/pandas-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:557409bc4178e70ee8d9ddb494798e51ebf6ea59330f6be22c51bab2a7db6c49", size = 9066158, upload-time = "2026-05-11T18:52:56.038Z" }, - { url = "https://files.pythonhosted.org/packages/c5/90/62d8302883c44308c477e222c3daf7c813a34c8e96985882fbd53d964352/pandas-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:67b3b64c11910cfa29f4e94a14d3bff9ee693b6fc76055e7cad549cee0aec5fa", size = 10331071, upload-time = "2026-05-11T18:52:58.838Z" }, - { url = "https://files.pythonhosted.org/packages/7f/ae/6a6493c783a101f165e4356953ba3c74d6f77f0042fa7d753da9dfbb640c/pandas-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:39436b377d56d2a2e52d0395bdbee171f01068e99af5250509aceeb929f765c7", size = 9875690, upload-time = "2026-05-11T18:53:01.431Z" }, - { url = "https://files.pythonhosted.org/packages/62/7c/5df8e9f56c69a2769fbe9382a5ef8f2658c007e376434e1e2cbb57ad895f/pandas-3.0.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4be06d68f9ddcfc645b87534911da79a8fbffc7573c80e0edcf42a5020624d8", size = 10381634, upload-time = "2026-05-11T18:53:04.393Z" }, - { url = "https://files.pythonhosted.org/packages/99/68/1237369725aa617bb358263d535803e3053fdbc593513ec5ed9c9896b5b6/pandas-3.0.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a4eeb6830daf35a71cc09649bd823e2b542dac246cdee9614c6e4bd65028cd6a", size = 10891243, upload-time = "2026-05-11T18:53:07.643Z" }, - { url = "https://files.pythonhosted.org/packages/25/93/77d108e8af7222b4a503ebde0e30215b1c2e4f8e53a526431890f22d5586/pandas-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1928e07221f82db493cd4af1e23c1bfca524a19a4699887975bff68f49a72bfb", size = 11388659, upload-time = "2026-05-11T18:53:10.634Z" }, - { url = "https://files.pythonhosted.org/packages/d0/bd/eff5b4399f332ac386c853f6cd2bd3fa2ca0061b9f36ecd9c4d7c4265649/pandas-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51b1fe551acb77dac643c6fda86084d8d446c10fe64b06a9cc29c4cc8540e7f2", size = 11942880, upload-time = "2026-05-11T18:53:13.536Z" }, - { url = "https://files.pythonhosted.org/packages/2c/20/559ace4200982c3887d0b86bfd0d856a2143ef8ddab63cc07934951a964c/pandas-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:a82d532a3351d435432cd913edbccaf8b8e01d4dd0e5ced5a8d2e8ecd94c7e44", size = 9757091, upload-time = "2026-05-11T18:53:16.306Z" }, - { url = "https://files.pythonhosted.org/packages/3a/66/69055a09fe200f29f922a3eeec4804611900b95f52d932ece3393c3c0c19/pandas-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:275c14e0fce14a2ec20eee474aecd305478ea3c1e6f6a9d8fe219a165542717e", size = 9057282, upload-time = "2026-05-11T18:53:18.768Z" }, - { url = "https://files.pythonhosted.org/packages/57/0e/efe801b0e6811e8e650cd21b7f2608e30f08a7067e2bf6e8752b0d56ee3c/pandas-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:46997386d528eb40376ecd6b033cf4a8a1e5282580f68f43de875b78cba2199d", size = 10767016, upload-time = "2026-05-11T18:53:21.227Z" }, - { url = "https://files.pythonhosted.org/packages/ea/dc/eb55135a1d5f0f0519f28da1f609a206d2cad1f9c35c32d51e38dd7261ae/pandas-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:261e308dfb22448384b7580cf719d2f998fe2966c92893c3e77d14008af1f066", size = 10420210, upload-time = "2026-05-11T18:53:23.982Z" }, - { url = "https://files.pythonhosted.org/packages/c6/3e/b1d5d955ce33ffecb407465a60bc32769d74fcf68224b7ae67ae11d4dea4/pandas-3.0.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd1a5d1def6a46002e964510bdc67c368aa0951df5d1d9f8365336f5a1f490cd", size = 10336126, upload-time = "2026-05-11T18:53:26.731Z" }, - { url = "https://files.pythonhosted.org/packages/f5/76/a01261711ab60a22d71b862f0de20e4c504bf80457270ad8cb42110f6abc/pandas-3.0.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d72828c20c6d6e83e1e22a6a3b47b326b71664112fa9705dcbccfd7a39b62085", size = 10728051, upload-time = "2026-05-11T18:53:29.125Z" }, - { url = "https://files.pythonhosted.org/packages/e9/21/ea191195e587b18cf682e97f433f81b2d0fbe341380e80a3e0d6e4403c8e/pandas-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d26cbe1fcfc12e8fd900e2454163e466b2d3af84f7c75481df7683ffc073d870", size = 11350796, upload-time = "2026-05-11T18:53:32.056Z" }, - { url = "https://files.pythonhosted.org/packages/64/69/f0eaaf54939f0e8c6768fd06be9af2cef9b36048b96dfb9e1b2c685a807e/pandas-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3e91cec1879ada0624fc3dc9953c5cbd60208e59c0db28f540c5d6d47502422f", size = 11799741, upload-time = "2026-05-11T18:53:34.985Z" }, - { url = "https://files.pythonhosted.org/packages/45/a4/865e0e510cae5fc2194de4db28be638952de942571ba9125934fd9c01d47/pandas-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:08d789b41f87e0905880e293cedf6197ce71fe67cc081358b1e148a491b9bd13", size = 10499958, upload-time = "2026-05-11T18:53:37.857Z" }, -] - [[package]] name = "pathspec" version = "1.1.1" @@ -886,50 +657,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] -[[package]] -name = "protobuf" -version = "7.35.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/60/fd/5b1491d9e4b586d621c54f4c36b888714164b6875f8d6afa3f9072906a51/protobuf-7.35.0.tar.gz", hash = "sha256:a2efd84605f41e559f1881b0912b44099d0a2ac9bf46b3474823f10fb393b0e6", size = 458677, upload-time = "2026-05-19T23:02:29.197Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/83/ee/93d06e358a4aa32280b00e722d3ea0a1f25fc3cc5778d80581c9cca2c10e/protobuf-7.35.0-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:66be6c513931c794fa92c080ffee41671390da3d79da219cf9c0c0907f035dda", size = 433225, upload-time = "2026-05-19T23:02:19.884Z" }, - { url = "https://files.pythonhosted.org/packages/8b/39/1c76c2da93f3c507e958e0aecee2391cc44d4625de6c728bbc555195b5a8/protobuf-7.35.0-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:fcbe42a4ac09d3ec9c987ddfcd956afd0b15f1ff613bd8371bde9405ffd5c8e5", size = 328847, upload-time = "2026-05-19T23:02:22.3Z" }, - { url = "https://files.pythonhosted.org/packages/91/1a/39f7ce90a238c1a987a4d81ec26379e02ca0aff367de68e4a1fa474215b9/protobuf-7.35.0-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:4cbf5cc286130e06a6c9bbefac442431173906dfcc979712183d4adcc01b37ee", size = 344030, upload-time = "2026-05-19T23:02:23.591Z" }, - { url = "https://files.pythonhosted.org/packages/70/5b/6baf9008817964454055ff3fe65f1de0b5f1e26c80c82f7fb108b7cd4ea3/protobuf-7.35.0-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:6c0f98f10c8a05ea30f8993dfef2de093d27b490fdae78bb60c8343795d55011", size = 327130, upload-time = "2026-05-19T23:02:24.637Z" }, - { url = "https://files.pythonhosted.org/packages/8e/e5/e46adb0badc388bfb84877a5f9f026aff63f60e611016cf64dbe77e05446/protobuf-7.35.0-cp310-abi3-win32.whl", hash = "sha256:4c4617b83ade0e279d1d2bfe04025a1adb87f9ed657de038620dc0ff959357f6", size = 428946, upload-time = "2026-05-19T23:02:25.741Z" }, - { url = "https://files.pythonhosted.org/packages/a7/ab/547fbd9e16d879dd13c167478f8ae0a83a428008ca07a5e06acdc23ad473/protobuf-7.35.0-cp310-abi3-win_amd64.whl", hash = "sha256:f05bcadf9a2a6b8dda047007075135fb7d08c73d9177aabc067e1be46881a201", size = 439996, upload-time = "2026-05-19T23:02:26.808Z" }, - { url = "https://files.pythonhosted.org/packages/b8/ef/50433d346c56657a70d27f156c7b349ac59a068b01de4eb796e747eecc43/protobuf-7.35.0-py3-none-any.whl", hash = "sha256:c13f325cf242bad135c350629eeb5d54b24228eb472fb3e2e9ebbd4c5dc20ca0", size = 171659, upload-time = "2026-05-19T23:02:27.842Z" }, -] - -[[package]] -name = "pyarrow" -version = "24.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/91/13/13e1069b351bdc3881266e11147ffccf687505dbb0ea74036237f5d454a5/pyarrow-24.0.0.tar.gz", hash = "sha256:85fe721a14dd823aca09127acbb06c3ca723efbd436c004f16bca601b04dcc83", size = 1180261, upload-time = "2026-04-21T10:51:25.837Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/a9/9686d9f07837f91f775e8932659192e02c74f9d8920524b480b85212cc68/pyarrow-24.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:6233c9ed9ab9d1db47de57d9753256d9dcffbf42db341576099f0fd9f6bf4810", size = 34981559, upload-time = "2026-04-21T10:47:22.17Z" }, - { url = "https://files.pythonhosted.org/packages/80/b6/0ddf0e9b6ead3474ab087ae598c76b031fc45532bf6a63f3a553440fb258/pyarrow-24.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:f7616236ec1bc2b15bfdec22a71ab38851c86f8f05ff64f379e1278cf20c634a", size = 36663654, upload-time = "2026-04-21T10:47:28.315Z" }, - { url = "https://files.pythonhosted.org/packages/7c/3b/926382efe8ce27ba729071d3566ade6dfb86bdf112f366000196b2f5780a/pyarrow-24.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:1617043b99bd33e5318ae18eb2919af09c71322ef1ca46566cdafc6e6712fb66", size = 45679394, upload-time = "2026-04-21T10:47:34.821Z" }, - { url = "https://files.pythonhosted.org/packages/b3/7a/829f7d9dfd37c207206081d6dad474d81dde29952401f07f2ba507814818/pyarrow-24.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:6165461f55ef6314f026de6638d661188e3455d3ec49834556a0ebbdbace18bb", size = 48863122, upload-time = "2026-04-21T10:47:42.056Z" }, - { url = "https://files.pythonhosted.org/packages/5f/e8/f88ce625fe8babaae64e8db2d417c7653adb3019b08aae85c5ed787dc816/pyarrow-24.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3b13dedfe76a0ad2d1d859b0811b53827a4e9d93a0bcb05cf59333ab4980cc7e", size = 49376032, upload-time = "2026-04-21T10:47:48.967Z" }, - { url = "https://files.pythonhosted.org/packages/36/7a/82c363caa145fff88fb475da50d3bf52bb024f61917be5424c3392eaf878/pyarrow-24.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:25ea65d868eb04015cd18e6df2fbe98f07e5bda2abefabcb88fce39a947716f6", size = 51929490, upload-time = "2026-04-21T10:47:55.981Z" }, - { url = "https://files.pythonhosted.org/packages/66/1c/e3e72c8014ad2743ca64a701652c733cc5cbcee15c0463a32a8c55518d9e/pyarrow-24.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:295f0a7f2e242dabd513737cf076007dc5b2d59237e3eca37b05c0c6446f3826", size = 27355660, upload-time = "2026-04-21T10:48:01.718Z" }, - { url = "https://files.pythonhosted.org/packages/6f/d3/a1abf004482026ddc17f4503db227787fa3cfe41ec5091ff20e4fea55e57/pyarrow-24.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:02b001b3ed4723caa44f6cd1af2d5c86aa2cf9971dacc2ffa55b21237713dfba", size = 34976759, upload-time = "2026-04-21T10:48:07.258Z" }, - { url = "https://files.pythonhosted.org/packages/4f/4a/34f0a36d28a2dd32225301b79daad44e243dc1a2bb77d43b60749be255c4/pyarrow-24.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:04920d6a71aabd08a0417709efce97d45ea8e6fb733d9ca9ecffb13c67839f68", size = 36658471, upload-time = "2026-04-21T10:48:13.347Z" }, - { url = "https://files.pythonhosted.org/packages/1f/78/543b94712ae8bb1a6023bcc1acf1a740fbff8286747c289cd9468fced2a5/pyarrow-24.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:a964266397740257f16f7bb2e4f08a0c81454004beab8ff59dd531b73610e9f2", size = 45675981, upload-time = "2026-04-21T10:48:20.201Z" }, - { url = "https://files.pythonhosted.org/packages/84/9f/8fb7c222b100d314137fa40ec050de56cd8c6d957d1cfff685ce72f15b17/pyarrow-24.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6f066b179d68c413374294bc1735f68475457c933258df594443bb9d88ddc2a0", size = 48859172, upload-time = "2026-04-21T10:48:27.541Z" }, - { url = "https://files.pythonhosted.org/packages/a7/d3/1ea72538e6c8b3b475ed78d1049a2c518e655761ea50fe1171fc855fcab7/pyarrow-24.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1183baeb14c5f587b1ec52831e665718ce632caab84b7cd6b85fd44f96114495", size = 49385733, upload-time = "2026-04-21T10:48:34.7Z" }, - { url = "https://files.pythonhosted.org/packages/c3/be/c3d8b06a1ba35f2260f8e1f771abbee7d5e345c0937aab90675706b1690a/pyarrow-24.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:806f24b4085453c197a5078218d1ee08783ebbba271badd153d1ae22a3ee804f", size = 51934335, upload-time = "2026-04-21T10:48:42.099Z" }, - { url = "https://files.pythonhosted.org/packages/9c/62/89e07a1e7329d2cde3e3c6994ba0839a24977a2beda8be6005ea3d860b99/pyarrow-24.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:e4505fc6583f7b05ab854934896bcac8253b04ac1171a77dfb73efef92076d91", size = 27271748, upload-time = "2026-04-21T10:49:42.532Z" }, - { url = "https://files.pythonhosted.org/packages/17/1a/cff3a59f80b5b1658549d46611b67163f65e0664431c076ad728bf9d5af4/pyarrow-24.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:1a4e45017efbf115032e4475ee876d525e0e36c742214fbe405332480ecd6275", size = 35238554, upload-time = "2026-04-21T10:48:48.526Z" }, - { url = "https://files.pythonhosted.org/packages/a8/99/cce0f42a327bfef2c420fb6078a3eb834826e5d6697bf3009fe11d2ad051/pyarrow-24.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:7986f1fa71cee060ad00758bcc79d3a93bab8559bf978fab9e53472a2e25a17b", size = 36782301, upload-time = "2026-04-21T10:48:55.181Z" }, - { url = "https://files.pythonhosted.org/packages/2a/66/8e560d5ff6793ca29aca213c53eec0dd482dd46cb93b2819e5aab52e4252/pyarrow-24.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:d3e0b61e8efb24ed38898e5cdc5fffa9124be480008d401a1f8071500494ae42", size = 45721929, upload-time = "2026-04-21T10:49:03.676Z" }, - { url = "https://files.pythonhosted.org/packages/27/0c/a26e25505d030716e078d9f16eb74973cbf0b33b672884e9f9da1c83b871/pyarrow-24.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:55a3bc1e3df3b5567b7d27ef551b2283f0c68a5e86f1cd56abc569da4f31335b", size = 48825365, upload-time = "2026-04-21T10:49:11.714Z" }, - { url = "https://files.pythonhosted.org/packages/5f/eb/771f9ecb0c65e73fe9dccdd1717901b9594f08c4515d000c7c62df573811/pyarrow-24.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:641f795b361874ac9da5294f8f443dfdbee355cf2bd9e3b8d97aaac2306b9b37", size = 49451819, upload-time = "2026-04-21T10:49:21.474Z" }, - { url = "https://files.pythonhosted.org/packages/48/da/61ae89a88732f5a785646f3ec6125dbb640fa98a540eb2b9889caa561403/pyarrow-24.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8adc8e6ce5fccf5dc707046ae4914fd537def529709cc0d285d37a7f9cd442ca", size = 51909252, upload-time = "2026-04-21T10:49:31.164Z" }, - { url = "https://files.pythonhosted.org/packages/cb/1a/8dd5cafab7b66573fa91c03d06d213356ad4edd71813aa75e08ce2b3a844/pyarrow-24.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:9b18371ad2f44044b81a8d23bc2d8a9b6a6226dca775e8e16cfee640473d6c5d", size = 27388127, upload-time = "2026-04-21T10:49:37.334Z" }, -] - [[package]] name = "pycparser" version = "3.0" @@ -1013,19 +740,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ae/8d/f1af3832f5e6eb13ba94ee809e72b8ecb5eef226d27ee0bef7d963d943c7/pydantic_settings-2.14.1-py3-none-any.whl", hash = "sha256:6e3c7edfd8277687cdc598f56e5cff0e9bfff0910a3749deaa8d4401c3a2b9de", size = 60964, upload-time = "2026-05-08T13:40:04.958Z" }, ] -[[package]] -name = "pydeck" -version = "0.9.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jinja2" }, - { name = "numpy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/eb/df/4e9e7f20f8034a37c6571c93809f6d22388c39978c98d174d656c1a18fd2/pydeck-0.9.2.tar.gz", hash = "sha256:c10d9035e81ead6385264cac8d19402471f6866a15ca1f7df1400f52142bcf87", size = 5849672, upload-time = "2026-04-16T18:30:30.089Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/24/b30ee7d723100fd822de1bb4c0adea62f3419884a75a536f35f355d1e7c0/pydeck-0.9.2-py2.py3-none-any.whl", hash = "sha256:8213dfeacc5f6bfe6825f61c8ee34e3850e8a31fc43924379ec98edb34a75b25", size = 11305615, upload-time = "2026-04-16T18:30:28.133Z" }, -] - [[package]] name = "pygments" version = "2.20.0" @@ -1135,87 +849,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, ] -[[package]] -name = "referencing" -version = "0.37.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "attrs" }, - { name = "rpds-py" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, -] - -[[package]] -name = "requests" -version = "2.34.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, -] - -[[package]] -name = "rpds-py" -version = "2026.5.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2e/43/25a8dcd3feedd735039a8f0b5b7e3b118232b5eae288c4fd9ab200d41094/rpds_py-2026.5.1.tar.gz", hash = "sha256:07b24fea40541e28570e5b795a4a38fbdcd12550c06bd0748005ecc8116ca256", size = 64459, upload-time = "2026-05-28T12:02:13.232Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/e7/a78582dc57caa592dcc7d4fb69b61390561e908eb3d2f5df5928a8e354c0/rpds_py-2026.5.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3abe24a66e57adcfa645d718063a5fa5103ecc71ddbf26d78af8f9368018ff1d", size = 353040, upload-time = "2026-05-28T11:59:12.531Z" }, - { url = "https://files.pythonhosted.org/packages/a3/43/35e3f136343aef451e545ce8c38d36c2f93c0ed88703db8b64ba2b205c68/rpds_py-2026.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:58b1d94308ddf0b1982f61f2eb54bf92997c9ece8a8093ef014250f4a517906c", size = 345775, upload-time = "2026-05-28T11:59:13.827Z" }, - { url = "https://files.pythonhosted.org/packages/20/e1/0f2160c5982d3157734d5cb3ed63d8b2d583a73c9864f77b666449f32cf8/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fa92420128dadce7f54bd73ba1825a273e9268fe9e35dbf7e6362890efa4e08", size = 376329, upload-time = "2026-05-28T11:59:15.271Z" }, - { url = "https://files.pythonhosted.org/packages/d0/11/ee0ba42aff83bf4effdbc576673c6be64c5e173978c3f6d537e94482f77d/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ca653c6546386227cd9800d1bef6a348099acf8db4250341da6d90f663d6dfcb", size = 383539, upload-time = "2026-05-28T11:59:16.665Z" }, - { url = "https://files.pythonhosted.org/packages/11/df/d94aa6a499d4ac40afe2d7620f2c597fd3c0f182e854ad7cf3f596a81cb6/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66c93681c4729e4e3ecba31b8179fae083ff3118841672835140338b4b9867c1", size = 494674, upload-time = "2026-05-28T11:59:17.991Z" }, - { url = "https://files.pythonhosted.org/packages/1f/75/33d30f43bb2f458de11979486a591b1bf6e5651765ed1704c6197c2dc773/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40ff257542e04796880e011e15cd4dc21c2599975df2aaa8f2c8495ca574e1a5", size = 389268, upload-time = "2026-05-28T11:59:19.434Z" }, - { url = "https://files.pythonhosted.org/packages/f4/1e/2c9096fc19d5fd084b0184ca2b651e659aa0a37e6fdbecf6ece47f147fe1/rpds_py-2026.5.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6825cc329b290e93c5f6a9be2393118a763f6ccf6abd83704e0c102ca583644", size = 376280, upload-time = "2026-05-28T11:59:21Z" }, - { url = "https://files.pythonhosted.org/packages/b9/e5/61ec9f8be8211ea7f48448195549e4aaf02004083475493b0e137702ecb2/rpds_py-2026.5.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:de42116e69cb53b911cc34aee5ab98f36c597b822545045d49e938818b99e5e4", size = 387233, upload-time = "2026-05-28T11:59:22.454Z" }, - { url = "https://files.pythonhosted.org/packages/0d/ca/bcec1005c4f4a234f92a29078631fee49206c7265ccae966f18fd332e80e/rpds_py-2026.5.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c0f920015df2a504bebaba6d4c31ccf3fcf942f92655c086da30b671aad19aa6", size = 405009, upload-time = "2026-05-28T11:59:23.845Z" }, - { url = "https://files.pythonhosted.org/packages/72/e6/4d5718c5cf26c522dc7c9999e238da1e77380b81d0c5d1df11e271ddfeb1/rpds_py-2026.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0408a24e44feb919423dc6d9da677cb5cddb894d2ca9e763967d156d9c60fab4", size = 553113, upload-time = "2026-05-28T11:59:25.184Z" }, - { url = "https://files.pythonhosted.org/packages/d4/25/2ee807bdb3e1f0b7eddf7782acd5665a8b5205a331a7d7244a52c4812fd9/rpds_py-2026.5.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cea68bcd53467561ae2f96a6bdad1544299ba97b5b0ddcd5ac3d376e5c781c24", size = 618838, upload-time = "2026-05-28T11:59:26.749Z" }, - { url = "https://files.pythonhosted.org/packages/6a/c1/7d4c26f167f8c41501cc073d30ee22082b16ce358cf5b00ec97cbc7804ea/rpds_py-2026.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4be8b1d2a705cc37d08256004e1d07de143fa0075c8e85a3df020b776f62b732", size = 582436, upload-time = "2026-05-28T11:59:28.11Z" }, - { url = "https://files.pythonhosted.org/packages/04/1d/9d12b0a337bab46f4769f8857f4007e3b2d639e14f9a44a0efe157696e64/rpds_py-2026.5.1-cp312-cp312-win32.whl", hash = "sha256:6736718bd4fc49cbcb538ba30516fdbef161522acefb739657d48b97bd864fed", size = 212734, upload-time = "2026-05-28T11:59:29.689Z" }, - { url = "https://files.pythonhosted.org/packages/c5/93/e4116f2de7f56bc7406a76033dc501811ddeb22b7f056b92d632871ebb0c/rpds_py-2026.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:0a7d1eec967df0e9b22614a5e177622e0c89611d03727fa0cb48e45028907870", size = 229045, upload-time = "2026-05-28T11:59:31.033Z" }, - { url = "https://files.pythonhosted.org/packages/cb/53/6c3419d85eb2ec5938a37627c585b42d76a63bb731d6e42ed4b079ebf486/rpds_py-2026.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:1841d067089e117142d79b98aa0df2f08b52f2ecc1819dd2700636c0db74a473", size = 223967, upload-time = "2026-05-28T11:59:32.318Z" }, - { url = "https://files.pythonhosted.org/packages/6c/32/14c961ad295f490eb0849ada8b79683e93a59b9de3afdd983eaf55fa6867/rpds_py-2026.5.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:efef4ac29c6ff495531eb17ee705b62841ecaa291b7c7077e848ea03e237164d", size = 352787, upload-time = "2026-05-28T11:59:33.655Z" }, - { url = "https://files.pythonhosted.org/packages/ca/bb/d1b85117967c11191441a7274ae616c65d93901d082c588f89a50a8da5ae/rpds_py-2026.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c39f5b67a8a2e67179ada2a954227d670fe65fa9098457f698f56ddf248709b3", size = 345179, upload-time = "2026-05-28T11:59:35Z" }, - { url = "https://files.pythonhosted.org/packages/7c/46/d84105f062e626a1b233f863907288a4708c2d833b8b4c6fb2764bc080c0/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5c30f3f04eef4fbd362226a6f31d7c8895ca4fbb6e0b790f6890a98d8da8559", size = 376173, upload-time = "2026-05-28T11:59:36.43Z" }, - { url = "https://files.pythonhosted.org/packages/e2/ae/469d7959ce5b1201e1de135dc735b86db3b35dd0d1734f6a44246d5f061c/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:277f6c82f0580848796c7ecc8a7173aa3bfb928e4ff831261c2f60a81dc270db", size = 383162, upload-time = "2026-05-28T11:59:37.995Z" }, - { url = "https://files.pythonhosted.org/packages/dc/a2/57853d31a1116a561aa072794602ad3f6341e18d70a8523f1bd5b9fc1e5a/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:63c2c4c213f1a4e3f3de28ecab029dbdee976324e729c0d7a55211be72576b02", size = 495093, upload-time = "2026-05-28T11:59:39.453Z" }, - { url = "https://files.pythonhosted.org/packages/99/63/3a8eabcad9314b7daf5c65f451d2c33d989235cd8a5762186cf2c3f5a4f8/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3350ec808fb538fe71a1f94dfaa0e29c598dfad805ce49f0caec5ae3183c652b", size = 389829, upload-time = "2026-05-28T11:59:40.896Z" }, - { url = "https://files.pythonhosted.org/packages/4b/25/05678d97fc25e2622df14dc530fb82023174ecfff6733991ed0d78f167bd/rpds_py-2026.5.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1b964e3ab599e718dc46c018d104b1ebc007cbc6567d827c94a687fca56d77e", size = 374786, upload-time = "2026-05-28T11:59:42.626Z" }, - { url = "https://files.pythonhosted.org/packages/88/d1/8c90b6431e80a3b91b284a5c7c8c0c4f9c006444d90477a740d6e0f9c694/rpds_py-2026.5.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:19cb09fab7b7fc96b2a6e28f2e34b72a3705ff27b37edb77455316e5d3f3dc9b", size = 386920, upload-time = "2026-05-28T11:59:44.124Z" }, - { url = "https://files.pythonhosted.org/packages/ff/99/4638f672ab356682d633ee0da9255f5b67ce6efd0b85eb94ad3e255e65a5/rpds_py-2026.5.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:abe76bcdba31e576cb83eeb8797aa0d882b738fef6dc65d0601fc753806a5b46", size = 405059, upload-time = "2026-05-28T11:59:47.177Z" }, - { url = "https://files.pythonhosted.org/packages/66/3f/3546524b6eb4cc2e1f363a3d638fa52f6c24faae3500c25fb488b02f1740/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8bff7073db3899158fff55ebf57b113a67030af26f80a18978f9f0aa60250ddf", size = 553030, upload-time = "2026-05-28T11:59:48.603Z" }, - { url = "https://files.pythonhosted.org/packages/c6/c3/7b3388c796fcf471bd17194242d4dc1a7608567c0fa422bcc1c5e79f9c1e/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8ba264fa49be666cd9cc56bf34ec7002fb3d27a4aee5bcb4d43d0d18feb1bb6f", size = 618975, upload-time = "2026-05-28T11:59:50.314Z" }, - { url = "https://files.pythonhosted.org/packages/61/1e/a3cb07f2795075d1d88efddae2f541359fde5f08c81ee114c29c2949c90a/rpds_py-2026.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4860b603ddda0475a8885499b3729e90229d480105b42651962a5397d995fa89", size = 581178, upload-time = "2026-05-28T11:59:51.673Z" }, - { url = "https://files.pythonhosted.org/packages/a1/74/e758c03a5ef46f04c37f2651a2893db846d569ba8a7bca469d4b58939bcd/rpds_py-2026.5.1-cp313-cp313-win32.whl", hash = "sha256:7944270ae71383f6e2657dd7d5ce4eeb4ac2d0059a6738f0510583d462ab4842", size = 212481, upload-time = "2026-05-28T11:59:53.148Z" }, - { url = "https://files.pythonhosted.org/packages/70/ec/a2aca432db9c7359b40fa393eeeaa0d166c2f70175be956e75fa24197c44/rpds_py-2026.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:88647f43a73c4e01be19b04ceef0c8d3a1958153604d13c773becd8016f2a0cf", size = 228519, upload-time = "2026-05-28T11:59:54.505Z" }, - { url = "https://files.pythonhosted.org/packages/29/60/a73bfdd45b096574556acf303bbd9fa9eed36ca8a818b514e2a5d5fe2b9d/rpds_py-2026.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:453895624ecf7db7063b1004e44037522bbaef9ff6a945e59bc71662d7a03abd", size = 223446, upload-time = "2026-05-28T11:59:56.081Z" }, - { url = "https://files.pythonhosted.org/packages/18/e2/408105fd611823f00882aea810f3989a30d26b1bab8b6beb20f98c724e0e/rpds_py-2026.5.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:b4e4bc98639ec915f512fde3aa7a95e0041d95d9c3cc86eea841fa63cb1e8600", size = 355287, upload-time = "2026-05-28T11:59:57.448Z" }, - { url = "https://files.pythonhosted.org/packages/8d/58/5c4a43436843c90d0f6d19f82c200c80e3843ca9fa07b237623327f6d384/rpds_py-2026.5.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cacedb7a6e167680acba45ad5716e89067d225dc80da0d7040cae8c81d4572fa", size = 347033, upload-time = "2026-05-28T11:59:58.881Z" }, - { url = "https://files.pythonhosted.org/packages/fb/c2/1a71acdacaf4e259b10278fb87b039ded3cf80041bcd89dd8a3ea702ded6/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68700371c5d7ae1412862ddfa719090925c93ecf351c566d66f09d04b136ea00", size = 376891, upload-time = "2026-05-28T12:00:00.516Z" }, - { url = "https://files.pythonhosted.org/packages/c2/c8/535f3d9b65addd8e28aa87b83c6e526799c3717a88273db8ea795beeef7a/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:296c799becfa849c779c8725494fe9ed94959ed886787df4364b058465bad7f0", size = 385646, upload-time = "2026-05-28T12:00:02.394Z" }, - { url = "https://files.pythonhosted.org/packages/1c/91/dc033f313345c354ade914dbe73cdb90b615a4409ea02430d5356794f3d8/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d3858b908218ee108d0bbfb2095ccc237648053c9bf98affad7cb079acaf1d97", size = 498830, upload-time = "2026-05-28T12:00:04.189Z" }, - { url = "https://files.pythonhosted.org/packages/27/fc/90fcbea459dbb8ddc18a2e0fd1de9412b48bc84ffff2db771cf714bacfd6/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4fb8d2e7cb2f850b169806d61d1b991738acec96500a75c30f49caf064ce7cef", size = 392830, upload-time = "2026-05-28T12:00:05.797Z" }, - { url = "https://files.pythonhosted.org/packages/b2/1d/46cd11a228c9750684a798d98f878be6f614aa762438da7378f035e79e35/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27b74c10ed6a8f190f4287f53bcfea348b92a84a9c9f70d30183d1e6172d580d", size = 379613, upload-time = "2026-05-28T12:00:07.433Z" }, - { url = "https://files.pythonhosted.org/packages/24/4a/d9b0c6af3a1de03eb93741bbe8be2bdce84d8fda8224f3005451d86df389/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:b9a6528956191c48c52294a592dbd4a8386d7048bdb25c0efcb6b966466c6d83", size = 388183, upload-time = "2026-05-28T12:00:09.227Z" }, - { url = "https://files.pythonhosted.org/packages/c5/b4/db7aaabdda6d020afc87d981bcc2f57a434c7dec60ecfc2ab3dd50b20351/rpds_py-2026.5.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:af03e34e860047bc7a352b842856fcf78798fbb81132cc98bd2f907ab4eb9cd2", size = 408578, upload-time = "2026-05-28T12:00:10.779Z" }, - { url = "https://files.pythonhosted.org/packages/08/d6/070f6a41cbb343e2ac4171859bf3f3623e0ab002f72619d6d505313ec2de/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fea6e836d10abbe191d557d33bd58bd5987725fe63aa1eefe557d230209855bd", size = 553573, upload-time = "2026-05-28T12:00:12.443Z" }, - { url = "https://files.pythonhosted.org/packages/75/ab/1a71ea3589c4345dac0a0518f0e6a031cb42689277851b683c46d27463a5/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:fc0c0f878ea770a0a8a462456c5ad36fc9fe6358e6b76fdadc7f17575e0b8bf1", size = 620861, upload-time = "2026-05-28T12:00:14.09Z" }, - { url = "https://files.pythonhosted.org/packages/8a/22/9bf80a56069c0c443fcfefac639a86a744550a2898817a6dfd3e26654924/rpds_py-2026.5.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e0b360f316d966b048b085857630b3cc51f3db2f07b06f440eac8f695374d1e3", size = 585633, upload-time = "2026-05-28T12:00:15.66Z" }, - { url = "https://files.pythonhosted.org/packages/da/68/3b2c0a75c9e04125696f84ebdbbf304acf5a40b58ba4481cdb98a922c3ba/rpds_py-2026.5.1-cp313-cp313t-win32.whl", hash = "sha256:a2999883eedf72fdfb7520b92c7d4ec2572a71ff40239377aa604cc529eecafc", size = 210074, upload-time = "2026-05-28T12:00:17.291Z" }, - { url = "https://files.pythonhosted.org/packages/e7/8b/609157d5a25d37d4f29f92840ba531f416907c34ae5c5739dd21fc2bef98/rpds_py-2026.5.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e07be2a9d7122bd6e82dea89814ef8dc893feb1aae97fec1630f3263bbb30e55", size = 228635, upload-time = "2026-05-28T12:00:18.73Z" }, -] - [[package]] name = "ruff" version = "0.15.16" @@ -1262,15 +895,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] -[[package]] -name = "smmap" -version = "5.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1f/ea/49c993d6dfdd7338c9b1000a0f36817ed7ec84577ae2e52f890d1a4ff909/smmap-5.0.3.tar.gz", hash = "sha256:4d9debb8b99007ae47165abc08670bd74cb74b5227dda7f643eccc4e9eb5642c", size = 22506, upload-time = "2026-03-09T03:43:26.1Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl", hash = "sha256:c106e05d5a61449cf6ba9a1e650227ecfb141590d2a98412103ff35d89fc7b2f", size = 24390, upload-time = "2026-03-09T03:43:24.361Z" }, -] - [[package]] name = "sqlalchemy" version = "2.0.50" @@ -1316,59 +940,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1c/54/196d0c1db10af76baa4f64894448505d60d3cdf70ef92cbb35f46a4e4c71/starlette-1.2.1-py3-none-any.whl", hash = "sha256:4de0082d08c8f6764a85a54cf1120d6939507a19905c7768acad2a9f875d2b89", size = 73350, upload-time = "2026-05-31T01:07:50.09Z" }, ] -[[package]] -name = "streamlit" -version = "1.58.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "altair" }, - { name = "anyio" }, - { name = "blinker" }, - { name = "cachetools" }, - { name = "click" }, - { name = "gitpython" }, - { name = "httptools" }, - { name = "itsdangerous" }, - { name = "numpy" }, - { name = "packaging" }, - { name = "pandas" }, - { name = "pillow" }, - { name = "protobuf" }, - { name = "pyarrow" }, - { name = "pydeck" }, - { name = "python-multipart" }, - { name = "requests" }, - { name = "starlette" }, - { name = "tenacity" }, - { name = "toml" }, - { name = "typing-extensions" }, - { name = "uvicorn" }, - { name = "watchdog", marker = "sys_platform != 'darwin'" }, - { name = "websockets" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/91/74/20dac6d6200d6ec0e1c230fb8eeb6a1a423645eacb76e8d802adfc246456/streamlit-1.58.0.tar.gz", hash = "sha256:78a22e7085b053af7ce544442bf4b670771e68c509ba1bdaa056ba0708f49c3d", size = 8721149, upload-time = "2026-05-28T18:02:44.606Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/df/84/14c36a92fb24f8e1cea452f53b0744b5da69d52cdd2fe22e71e6fbf765d5/streamlit-1.58.0-py3-none-any.whl", hash = "sha256:4ca8a7afc5bd16a5f176ccf4be1e34e8121cad0240becd127fb58a103ea3178d", size = 9219185, upload-time = "2026-05-28T18:02:41.993Z" }, -] - -[[package]] -name = "tenacity" -version = "9.1.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/47/c6/ee486fd809e357697ee8a44d3d69222b344920433d3b6666ccd9b374630c/tenacity-9.1.4.tar.gz", hash = "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a", size = 49413, upload-time = "2026-02-07T10:45:33.841Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55", size = 28926, upload-time = "2026-02-07T10:45:32.24Z" }, -] - -[[package]] -name = "toml" -version = "0.10.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253, upload-time = "2020-11-01T01:40:22.204Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" }, -] - [[package]] name = "typing-extensions" version = "4.15.0" @@ -1390,15 +961,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] -[[package]] -name = "tzdata" -version = "2026.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" }, -] - [[package]] name = "urllib3" version = "2.7.0" @@ -1452,24 +1014,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" }, ] -[[package]] -name = "watchdog" -version = "6.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, - { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, - { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, - { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, - { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, - { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, - { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, - { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, - { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, - { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, -] - [[package]] name = "watchfiles" version = "1.2.0"