From 49f8f49b65bc1ea021b9de3c9b8d34e3e4080887 Mon Sep 17 00:00:00 2001 From: Vasanth Date: Fri, 16 Oct 2026 13:33:03 +0530 Subject: [PATCH] Initial commit: plan, spec, tickets and dashboard skill templates Co-Authored-By: Claude Opus 5.5 --- .gitignore | 4 + .../issues/01-no-ai-tracer-empty-project.md | 30 + .../issues/02-no-ai-desktop-bot-wiring.md | 22 + .../03-existing-frontend-keep-or-restyle.md | 15 + .../issues/04-optional-sections.md | 20 + .../05-ai-tracer-bands-review-approve.md | 27 + .../issues/06-ai-cost-roi-page.md | 22 + .../issues/07-ai-desktop-plain-html.md | 15 + .scratch/rpa-dashboard-skills/spec.md | 237 ++ CLAUDE.md | 13 + PLAN.md | 115 + docs/agents/domain.md | 51 + docs/agents/issue-tracker.md | 30 + .../templates/reference/backend/bands.py | 64 + .../reference/backend/gemini_cost.py | 60 + .../templates/reference/web/DESIGN.md | 451 ++++ .../templates/reference/web/api.ts | 364 +++ .../templates/reference/web/bands.ts | 74 + .../reference/web/components/BrandHeader.tsx | 125 + .../reference/web/components/Donut.tsx | 94 + .../reference/web/components/PdfPane.tsx | 536 +++++ .../reference/web/components/ReviewPane.tsx | 1118 +++++++++ .../reference/web/components/RoiTab.tsx | 988 ++++++++ .../reference/web/components/RunPanel.tsx | 277 +++ .../reference/web/components/Sidebar.tsx | 300 +++ .../reference/web/components/SplitPane.tsx | 146 ++ .../reference/web/components/StatsBar.tsx | 100 + .../reference/web/components/Tooltip.tsx | 133 ++ .../reference/web/components/icons.tsx | 159 ++ .../templates/reference/web/index.css | 1764 ++++++++++++++ .../templates/reference/web/index.html | 35 + .../templates/reference/web/theme.tsx | 90 + .../templates/reference/web/types.ts | 346 +++ rpa-dashboard-no-ai/templates/bridge.py | 81 + rpa-dashboard-no-ai/templates/contract.md | 31 + rpa-dashboard-no-ai/templates/dashboard.html | 273 +++ .../templates/reference/tebra_bridge_part1.py | 251 ++ .../templates/reference/tebra_bridge_part2.py | 171 ++ .../templates/reference/tebra_dashboard.html | 2104 +++++++++++++++++ 39 files changed, 10736 insertions(+) create mode 100644 .gitignore create mode 100644 .scratch/rpa-dashboard-skills/issues/01-no-ai-tracer-empty-project.md create mode 100644 .scratch/rpa-dashboard-skills/issues/02-no-ai-desktop-bot-wiring.md create mode 100644 .scratch/rpa-dashboard-skills/issues/03-existing-frontend-keep-or-restyle.md create mode 100644 .scratch/rpa-dashboard-skills/issues/04-optional-sections.md create mode 100644 .scratch/rpa-dashboard-skills/issues/05-ai-tracer-bands-review-approve.md create mode 100644 .scratch/rpa-dashboard-skills/issues/06-ai-cost-roi-page.md create mode 100644 .scratch/rpa-dashboard-skills/issues/07-ai-desktop-plain-html.md create mode 100644 .scratch/rpa-dashboard-skills/spec.md create mode 100644 CLAUDE.md create mode 100644 PLAN.md create mode 100644 docs/agents/domain.md create mode 100644 docs/agents/issue-tracker.md create mode 100644 rpa-dashboard-ai/templates/reference/backend/bands.py create mode 100644 rpa-dashboard-ai/templates/reference/backend/gemini_cost.py create mode 100644 rpa-dashboard-ai/templates/reference/web/DESIGN.md create mode 100644 rpa-dashboard-ai/templates/reference/web/api.ts create mode 100644 rpa-dashboard-ai/templates/reference/web/bands.ts create mode 100644 rpa-dashboard-ai/templates/reference/web/components/BrandHeader.tsx create mode 100644 rpa-dashboard-ai/templates/reference/web/components/Donut.tsx create mode 100644 rpa-dashboard-ai/templates/reference/web/components/PdfPane.tsx create mode 100644 rpa-dashboard-ai/templates/reference/web/components/ReviewPane.tsx create mode 100644 rpa-dashboard-ai/templates/reference/web/components/RoiTab.tsx create mode 100644 rpa-dashboard-ai/templates/reference/web/components/RunPanel.tsx create mode 100644 rpa-dashboard-ai/templates/reference/web/components/Sidebar.tsx create mode 100644 rpa-dashboard-ai/templates/reference/web/components/SplitPane.tsx create mode 100644 rpa-dashboard-ai/templates/reference/web/components/StatsBar.tsx create mode 100644 rpa-dashboard-ai/templates/reference/web/components/Tooltip.tsx create mode 100644 rpa-dashboard-ai/templates/reference/web/components/icons.tsx create mode 100644 rpa-dashboard-ai/templates/reference/web/index.css create mode 100644 rpa-dashboard-ai/templates/reference/web/index.html create mode 100644 rpa-dashboard-ai/templates/reference/web/theme.tsx create mode 100644 rpa-dashboard-ai/templates/reference/web/types.ts create mode 100644 rpa-dashboard-no-ai/templates/bridge.py create mode 100644 rpa-dashboard-no-ai/templates/contract.md create mode 100644 rpa-dashboard-no-ai/templates/dashboard.html create mode 100644 rpa-dashboard-no-ai/templates/reference/tebra_bridge_part1.py create mode 100644 rpa-dashboard-no-ai/templates/reference/tebra_bridge_part2.py create mode 100644 rpa-dashboard-no-ai/templates/reference/tebra_dashboard.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7f93586 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +__pycache__/ +*.pyc +node_modules/ +.venv/ diff --git a/.scratch/rpa-dashboard-skills/issues/01-no-ai-tracer-empty-project.md b/.scratch/rpa-dashboard-skills/issues/01-no-ai-tracer-empty-project.md new file mode 100644 index 0000000..196a492 --- /dev/null +++ b/.scratch/rpa-dashboard-skills/issues/01-no-ai-tracer-empty-project.md @@ -0,0 +1,30 @@ +# 01: No-AI skill, first full path: empty project → working dashboard with sample data + +Part of: ../spec.md + +**What to build:** A developer runs the no-AI skill in an empty project. The skill: +- scans the project and finds no frontend and no bot, +- asks for colours (Tebra Sky, Superbill Ember, or a custom brand hex), +- asks desktop or web, and which optional sections to build, +- produces a working dashboard page with the shell, KPI cards, status donut, run panel and report table, running on sample data. + +An automated page check proves the page renders the standard data contract correctly. Before any of that, fix the desktop host template so it stays valid Python until its placeholders are filled. + +**Blocked by:** None (can start immediately) + +**Status:** ready-for-agent + +- [ ] The desktop host template passes the lint hook unfilled. Its placeholders go inside string literals. +- [ ] The no-AI skill file exists with a trigger description ("RPA dashboard", "automation dashboard", "bot dashboard", "without AI") and the workflow in spec order: scan → report/keep-or-restyle → colours → stack → optional sections → build → wire → verify. +- [ ] The theme presets file exists with Tebra Sky and Superbill Ember, light and dark tokens for each. +- [ ] Custom palette: given one brand hex, the skill derives the rest of the palette and checks text contrast in light and dark. +- [ ] With no bot present, the generated page uses sample data and renders on first open. +- [ ] A headless-browser page check (Seam 1) pushes contract envelopes through the page's public entry point and asserts visible results: + - KPI counts match batch states; + - donut and legend counts match; + - the step list follows the step label; + - log lines appear with their type; + - report search filters rows and shows the correct bucket pill; + - the dark toggle flips the theme. +- [ ] The page check runs in light, dark and narrow width (≤820px). +- [ ] Skill eval on test project (a), an empty project: the skill asks colours, stack and sections, never reports a frontend, and its output passes the page check. diff --git a/.scratch/rpa-dashboard-skills/issues/02-no-ai-desktop-bot-wiring.md b/.scratch/rpa-dashboard-skills/issues/02-no-ai-desktop-bot-wiring.md new file mode 100644 index 0000000..06e3750 --- /dev/null +++ b/.scratch/rpa-dashboard-skills/issues/02-no-ai-desktop-bot-wiring.md @@ -0,0 +1,22 @@ +# 02: No-AI desktop path: PyQt bot connected to the page + +Part of: ../spec.md + +**What to build:** A developer runs the no-AI skill in a PyQt project that already has a bot printing step markers. The skill chooses the desktop path and connects the bot to the dashboard: +- The bot runs as its own child process, and its output is read line by line. +- Specific step markers move the progress bar and step list. +- A friendly-rules table turns raw output lines into plain-English log lines. +- Error lines always appear, even when no rule matches. +- Dark mode is remembered between launches. + +**Blocked by:** 01 + +**Status:** ready-for-agent + +- [ ] The skill detects the PyQt project and the bot entry point, and fills in the host template's bot command, steps, step markers and friendly rules from the bot's real output. +- [ ] Clicking Run starts the bot in a separate process. The Run button disables itself while running, and the status dot goes ready → running → done/error. +- [ ] Step markers match on specific substrings and the first match wins. An unrelated line containing a common word does not move progress. +- [ ] Unmatched error lines are always shown. Unmatched non-error lines are hidden. +- [ ] The dark-mode choice persists across app restarts. +- [ ] A Python assert-based self-check covers line classification, step-marker matching and friendly-rule matching, including the "never hide an error" case. +- [ ] Skill eval on test project (b), a PyQt project with a step-marker bot: the skill doesn't ask the stack question, and the running page shows real steps and log lines. diff --git a/.scratch/rpa-dashboard-skills/issues/03-existing-frontend-keep-or-restyle.md b/.scratch/rpa-dashboard-skills/issues/03-existing-frontend-keep-or-restyle.md new file mode 100644 index 0000000..b1a27c5 --- /dev/null +++ b/.scratch/rpa-dashboard-skills/issues/03-existing-frontend-keep-or-restyle.md @@ -0,0 +1,15 @@ +# 03: Existing frontend: report, then keep or restyle + +Part of: ../spec.md + +**What to build:** A developer runs the no-AI skill in a web project that already has a frontend with its own colours. Before changing anything, the skill reports where the frontend is, what stack it uses and what colours it uses, then asks: keep the existing look or restyle. "Keep" adds the dashboard page in the existing tokens, fonts and components. "Restyle" asks for a preset or custom colour and swaps only the theme tokens; layout and logic stay untouched. + +**Blocked by:** 01 + +**Status:** ready-for-agent + +- [ ] Detection covers plain HTML, React/Vue projects and an embedded PyQt web view, and reports location, stack and the main colour tokens. +- [ ] "Keep": the new dashboard page uses the project's existing tokens and fonts. No existing styles are changed. +- [ ] "Restyle": only token blocks change. A diff shows no layout or logic edits. +- [ ] The colour question is skipped when the user keeps the existing look. The stack question is skipped whenever a frontend exists. +- [ ] Skill eval on test project (c), a web project with an existing frontend and its own colours: the skill reports the frontend correctly. Both branches produce a page that passes the page check. diff --git a/.scratch/rpa-dashboard-skills/issues/04-optional-sections.md b/.scratch/rpa-dashboard-skills/issues/04-optional-sections.md new file mode 100644 index 0000000..0b055c8 --- /dev/null +++ b/.scratch/rpa-dashboard-skills/issues/04-optional-sections.md @@ -0,0 +1,20 @@ +# 04: Optional sections: checkpoint tracker, admin/roles, login page + +Part of: ../spec.md + +**What to build:** The skill asks which optional sections to build and builds only those: +- **Checkpoint tracker:** numbered boxes that go pending → active → done, or stuck. +- **Admin/roles screen:** admin or operator. The admin nav item is hidden from operators. +- **Login page:** UI only. It calls whatever auth the project already has. + +Sections that aren't picked leave no code behind. + +**Blocked by:** 01 + +**Status:** ready-for-agent + +- [ ] The skill asks for the optional sections with a multi-select and builds only the chosen ones. +- [ ] Checkpoint tracker: the page check pushes checkpoint-state events and asserts each box shows pending, active, done or stuck. +- [ ] Admin/roles: the page check asserts the admin nav and screen appear for the admin role and are hidden for operators. +- [ ] Login page: renders in the chosen theme, light and dark, and hands off to the project's auth or a stub. +- [ ] With no optional sections picked, the output contains no leftover markup, scripts or styles for them. diff --git a/.scratch/rpa-dashboard-skills/issues/05-ai-tracer-bands-review-approve.md b/.scratch/rpa-dashboard-skills/issues/05-ai-tracer-bands-review-approve.md new file mode 100644 index 0000000..52a5843 --- /dev/null +++ b/.scratch/rpa-dashboard-skills/issues/05-ai-tracer-bands-review-approve.md @@ -0,0 +1,27 @@ +# 05: AI skill, first full path: confidence bands + review and approval (web, sample data) + +Part of: ../spec.md + +**What to build:** A developer runs the AI skill in a web project that has an LLM extraction call but no frontend. The skill follows the same question flow as the no-AI skill and builds a React + Vite + Tailwind dashboard with everything from the no-AI page, plus: +- **Confidence bands:** green ≥0.95, amber 0.90–0.95, red <0.90. A field takes its worst word score, and a blank mandatory field is always flagged. The legend shows the caveat text. +- **Review split pane:** the source document next to the extracted fields. Fields can be filtered by band. Edits stay drafts until Save, and leaving with unsaved edits asks first. Each field has Verify, and there's a "verify remaining" action. +- **Approve gate:** the server refuses approval while any required field is unverified. + +**Blocked by:** 01 + +**Status:** ready-for-agent + +- [ ] The AI skill file exists with a trigger description ("AI RPA dashboard", "extraction dashboard", "LLM/OCR review dashboard") and the shared workflow. +- [ ] An AI contract extends the no-AI contract. These shapes came from the Superbill prototype: + `Run{job_id,status:pending|queued|running|done|failed,model,pages_done,pages_total,summary:{green,amber,red,unscored}}` + `ReviewRow{row_id,field,value,band:green|amber|red|blank|unscored,score,reason,verified,corrected_value}`. + Actions: patch a review row, approve a run (server gate). A busy run is polled every ≈1.5s. +- [ ] Reference code: Superbill-only names (ECW/Tebra project split, table prefixes, patient/ticket wording) are stripped or marked as examples. A scan finds no patient data, internal IPs, credentials or keys. +- [ ] A band self-check asserts the thresholds, the worst-word rule and the blank-mandatory flag. +- [ ] Page check, AI cases: + - band counts and legend match; + - band filters show the right rows; + - a draft edit isn't saved until Save; + - approve stays blocked while rows are unverified (the server stub refuses); + - "page X of Y" updates while running. +- [ ] Skill eval on test project (d), a project with an LLM extraction call: the skill picks the AI path, asks the shared questions, and its output passes the page check with sample data. diff --git a/.scratch/rpa-dashboard-skills/issues/06-ai-cost-roi-page.md b/.scratch/rpa-dashboard-skills/issues/06-ai-cost-roi-page.md new file mode 100644 index 0000000..9a99908 --- /dev/null +++ b/.scratch/rpa-dashboard-skills/issues/06-ai-cost-roi-page.md @@ -0,0 +1,22 @@ +# 06: AI cost page: tokens, cost, monthly spend, model choice + +Part of: ../spec.md + +**What to build:** The AI dashboard gets a cost/ROI page. It shows: +- input, output and thinking tokens and cost per run; +- total spend and this month's spend, with a month-by-month breakdown; +- a coverage pill ("N of M documents priced"); +- an "against doing it by hand" panel with editable assumptions, clearly labelled as assumptions. + +Cost comes from a pricing table that records its source and its as-of date. Optionally, the skill also adds a model picker for each batch and retry-on-another-model for failed runs. + +**Blocked by:** 05 + +**Status:** ready-for-agent + +- [ ] Cost per call = input tokens × input rate + (output + thinking tokens) × output rate, per million tokens. Long-context rates apply above the threshold. +- [ ] An unknown model raises an error that names the known models. Cost never silently defaults to zero. +- [ ] A Python self-check covers normal, long-context, thinking-token and unknown-model cases. +- [ ] Contract: `RoiRun{model,input_tokens,output_tokens,thinking_tokens,total_cost}` and a fetch-ROI action. (Shape from the Superbill prototype.) +- [ ] The page check asserts that the spend tiles sum the runs, the coverage pill counts priced vs. total documents, and the month breakdown groups runs correctly. +- [ ] The model picker and retry-on-other-model are offered as optional sections and built only when chosen. diff --git a/.scratch/rpa-dashboard-skills/issues/07-ai-desktop-plain-html.md b/.scratch/rpa-dashboard-skills/issues/07-ai-desktop-plain-html.md new file mode 100644 index 0000000..c6d4484 --- /dev/null +++ b/.scratch/rpa-dashboard-skills/issues/07-ai-desktop-plain-html.md @@ -0,0 +1,15 @@ +# 07: AI desktop path: AI sections in plain HTML on the no-AI page + +Part of: ../spec.md + +**What to build:** A developer runs the AI skill in a PyQt project. The skill builds the Tebra-style no-AI page and adds plain-HTML versions of the AI sections: band legend, review split pane with drafts, Verify and approve gate, and the cost tiles. They're connected through the same Python bridge as the no-AI desktop path, so a desktop AI bot gets the full AI dashboard with no web server. + +**Blocked by:** 02, 05 + +**Status:** ready-for-agent + +- [ ] Plain-HTML AI sections exist, use the shared theme tokens, and render in light, dark and narrow width. +- [ ] The AI events and actions travel over the desktop bridge using the same envelope as the no-AI path. +- [ ] Approval is still enforced in Python on the bridge side. Changing the page cannot bypass it. +- [ ] The page check runs the AI cases from tickets 05 and 06 against the desktop page and they pass. +- [ ] Skill eval on a PyQt test project with an LLM extraction call: the skill picks the desktop AI path, and its output passes the page check. diff --git a/.scratch/rpa-dashboard-skills/spec.md b/.scratch/rpa-dashboard-skills/spec.md new file mode 100644 index 0000000..9ed68b1 --- /dev/null +++ b/.scratch/rpa-dashboard-skills/spec.md @@ -0,0 +1,237 @@ +# Spec: RPA dashboard skills (no-AI and AI) + +Status: ready-for-agent + +## Problem Statement + +Every new RPA project at Ecare needs a dashboard: what the bot is doing now, how many batches finished or failed, a log people can read, and a report they can export. For projects that use AI extraction, it also needs a way for a person to review low-confidence fields and approve them, plus a view of what the AI is costing. + +Today, each dashboard is built from scratch. Two good dashboards already exist, Tebra ERA (no AI) and Superbill Review (AI), but their designs, colours and data plumbing sit inside those projects. They can't be reused without hours of copy-and-strip work. + +As a result: +- Each new project looks different. +- Each one re-learns the same lessons: never hide error lines, run the bot in its own process, poll only while busy, enforce the approval gate on the server. +- Developers spend days on UI instead of on the automation. + +## Solution + +Build two Claude skills that a developer runs inside any new RPA project to produce a working dashboard page: + +- **`rpa-dashboard-no-ai`** for plain RPA bots. Based on the Tebra ERA dashboard. +- **`rpa-dashboard-ai`** for RPA bots that use an LLM or OCR. Based on Superbill Review. It includes everything in the no-AI skill, plus confidence bands, a review-and-approve screen, and a cost/ROI page. + +When the developer runs a skill, it: +1. Scans the project. +2. Tells them whether a frontend already exists: where it is, what stack it uses, and what colours it uses. +3. Asks whether to keep that look or restyle it. +4. Asks for colours: a preset (Tebra Sky or Superbill Ember) or their own brand colour. +5. Asks desktop or web, if there's no frontend. +6. Asks which optional sections they want. +7. Builds the page and wires it to the bot using one standard data contract. If no bot exists yet, it uses sample data so the page works on day one. + +## User Stories + +1. As an RPA developer, I want to run one skill in a new project and get a working dashboard page, so that I don't build the UI from scratch. +2. As an RPA developer, I want to choose between a no-AI skill and an AI skill, so that the dashboard matches what my bot actually does. +3. As an RPA developer, I want the skill to scan my project before asking anything, so that it doesn't ask questions the code already answers. +4. As an RPA developer, I want the skill to tell me whether a frontend already exists, with its location, stack and colours, so that I know what it found before it changes anything. +5. As an RPA developer with an existing frontend, I want to choose between keeping its look and restyling it, so that the skill never overwrites my brand without asking. +6. As an RPA developer who keeps the existing look, I want the new dashboard page to match the existing tokens, fonts and components, so that it feels like part of the same app. +7. As an RPA developer who restyles, I want the skill to replace only the theme tokens, so that my layout and logic stay untouched. +8. As an RPA developer with no frontend, I want to pick the Tebra Sky preset, so that I get the blue glassmorphism look quickly. +9. As an RPA developer with no frontend, I want to pick the Superbill Ember preset, so that I get the red-orange brand gradient look quickly. +10. As an RPA developer, I want to give one brand hex and have the skill derive the rest of the palette, so that a client-branded dashboard needs no design work. +11. As an RPA developer, I want every palette to come with a matching dark mode, so that dark mode never looks broken. +12. As an RPA developer, I want the skill to check text contrast for a custom palette, so that the dashboard stays readable. +13. As an RPA developer with no frontend, I want to choose desktop (PyQt with an embedded web page) or web, so that the dashboard fits how the bot is deployed. +14. As an RPA developer on desktop, I want the page to talk to Python through a bridge, so that I don't need a web server. +15. As an RPA developer on web, I want the same page to poll the backend, so that one design serves both deployment styles. +16. As an RPA developer, I want to choose which optional sections get built (checkpoint tracker, admin/roles, login), so that I don't delete unused code afterwards. +17. As an RPA developer, I want the sidebar, top bar, dark toggle and status dot built every time, so that every dashboard has the same shell. +18. As an RPA developer, I want KPI cards for total, completed, in-progress and not-completed batches, so that the overview always answers "how are we doing?". +19. As an RPA developer, I want a status donut with a legend, so that the batch split is visible at a glance. +20. As an RPA developer, I want a run panel with a progress bar, a step list and a live log, so that operators can watch a run happen. +21. As an RPA developer, I want a report table with search and Excel export, so that results can be handed to the billing team. +22. As an RPA developer, I want one documented data contract (events in, actions out), so that I know exactly what my bot must send. +23. As an RPA developer, I want the skill to wire the contract to my existing bot, so that the dashboard shows real data immediately. +24. As an RPA developer without a bot yet, I want the page to ship with sample data, so that I can demo and style it before the bot exists. +25. As an RPA developer, I want my bot's raw output lines turned into friendly log messages by a rules table, so that operators see plain English. +26. As an RPA developer, I want error lines always shown even when no friendly rule matches, so that a real failure is never invisible. +27. As an RPA developer, I want step progress driven by specific markers in the bot's output, so that progress doesn't jump on unrelated lines. +28. As an RPA developer on desktop, I want the bot to run in its own process with its output tailed, so that UI automation reads stay reliable. +29. As an RPA developer on web, I want polling to speed up only while a run is busy, so that an idle dashboard doesn't hammer the backend. +30. As an operator, I want a status dot that says ready, running, done or error, so that I know at a glance whether the bot is working. +31. As an operator, I want a Run button that disables itself while a run is going, so that I can't start two runs by accident. +32. As an operator, I want my dark-mode choice remembered, so that I don't toggle it every time. +33. As an operator, I want each report row to show a coloured success, attention or failed pill, so that I can spot problems fast. +34. As an operator, I want the dashboard to stay usable on a narrow window, so that it works on a small laptop screen. +35. As an admin, I want an optional roles screen (admin/operator), so that only the right people see admin features. +36. As an AI-project developer, I want the AI skill to include everything from the no-AI skill, so that I don't have to merge two outputs. +37. As an AI-project developer, I want confidence bands (green ≥0.95, amber 0.90–0.95, red <0.90), so that reviewers only look at fields that need them. +38. As an AI-project developer, I want a field's band to come from its worst word score, so that one shaky word can't hide inside a clean-looking field. +39. As an AI-project developer, I want a blank mandatory field always flagged, so that a missing code never passes silently. +40. As an AI-project developer, I want the band caveat shown in the UI ("colour reflects print clarity, not correctness"), so that reviewers don't over-trust green. +41. As a reviewer, I want the source document next to the extracted fields, so that I can check each value against the original. +42. As a reviewer, I want to filter fields by band (all, red, amber, missing, unscored), so that I work the riskiest fields first. +43. As a reviewer, I want to correct a value and keep it as a draft until I save, so that I don't commit half-finished edits. +44. As a reviewer, I want a warning if I leave with unsaved edits, so that I don't lose work. +45. As a reviewer, I want to verify each field, so that there's a record of who checked what. +46. As a reviewer, I want a "verify remaining" action, so that I can finish a clean document quickly. +47. As a reviewer, I want approval to be refused by the server until every required field is verified, so that the gate can't be bypassed from the browser. +48. As a reviewer, I want to see "page X of Y" while extraction runs, so that I know how long to wait. +49. As a reviewer, I want to retry a failed run on the same or a different model, so that a quota or overload error doesn't block me. +50. As an AI-project developer, I want an optional model picker, so that users can choose a cheaper or stronger model per batch. +51. As a manager, I want a cost page with input, output and thinking tokens and cost per run, so that I know what the AI spends. +52. As a manager, I want this month's spend and a month-by-month breakdown, so that I can track the budget. +53. As a manager, I want a coverage pill ("N of M documents priced"), so that I know whether the totals are complete. +54. As a manager, I want an "against doing it by hand" panel with editable assumptions, clearly labelled as assumptions, so that the ROI story is honest. +55. As an AI-project developer, I want a pricing table with a clear error when a model is missing, so that cost is never silently wrong. +56. As an AI-project developer on desktop, I want the AI sections available as plain HTML, so that a PyQt AI project gets them too. +57. As an AI-project developer on web, I want the AI dashboard built in React with Vite and Tailwind, adapted from the Superbill components, so that it matches the proven design. +58. As a skill maintainer, I want the reference code bundled inside each skill, so that the skills work on any PC even if the source projects move. +59. As a skill maintainer, I want the bundled references free of patient data, internal IPs and keys, so that the skills are safe to share. +60. As a skill maintainer, I want project-specific names (ECW, Tebra, table prefixes, patient/ticket wording) stripped or clearly marked, so that new projects don't inherit them. +61. As a skill maintainer, I want an automated check that the generated page renders contract events correctly, so that I can change templates without fear. +62. As a skill maintainer, I want an eval that runs each skill on small fixture projects, so that I know the question flow and output still work. +63. As a team member, I want to install the skills by copying their folders into Claude's skills directory, so that setup is trivial. + +## Implementation Decisions + +**Modules** +- **Two skills, each self-contained:** a skill file holding the workflow and trigger description, plus a templates folder. The AI skill's desktop path reuses the no-AI dashboard page, so both skills must be installed together. +- **No-AI templates:** + - a single-file dashboard page (theme tokens, shell, KPI, donut, run panel, report); + - a minimal PyQt host with bridge and process tailing; + - the data contract; + - theme presets; + - raw Tebra extracts as reference. +- **AI templates:** + - an AI data contract that extends the no-AI one; + - plain-HTML AI sections for desktop (band legend, review split pane, ROI tiles); + - theme presets; + - Superbill reference components, styles, band logic and the pricing/cost function. + +**Workflow order, identical in both skills** +1. Scan. +2. Report the existing frontend, then ask keep or restyle. +3. Ask colours. Skipped if the user keeps the existing theme. +4. Ask the stack question. Only when there's no frontend. +5. Ask for optional sections. +6. Build. +7. Wire the data. +8. Verify. + +**Theme rules** +- The theme lives only in the token blocks, light plus dark. Restyling swaps tokens and nothing else. +- Presets: + - **Tebra Sky:** sky gradient, glass cards, accent `#3d9bd0`, Segoe UI. + - **Superbill Ember:** flat surfaces, gradient `#ff3049`→`#f26b21`, used only on the header, primary buttons, the active nav row and the progress fill; Sora and IBM Plex fonts. + - **Custom:** derived from one brand hex. +- Band colours are semantic only and never used on buttons or headings. + +**Transport** +- One envelope in both directions: `{event, data}`. +- **Desktop:** Python pushes envelopes into the page; the page calls named bridge slots. +- **Web:** the page polls a dashboard endpoint and a run-status endpoint. Run-status returns `{running, events[]}`. Polling is fast (≈2s) only while running, otherwise slow. +- The same page file detects which transport is present. + +**No-AI contract (events to the page)** +- `status {state: ready|running|done|error, msg}` +- `step {label, pct}` +- `log {msg, type: info|success|warn|error}` +- `dashboard_data {batches: [{batch_id, date, state: completed|inprogress|notcompleted|cleared, items_total, items_done}]}` +- `report_data {rows: [... , status, bucket: success|attention|failed]}` +- `set_user {username, emp_id, role}` +- `apply_dark {on}` +- Optional: `cpstate {kind: pending|active|done|stuck, cp}` + +**No-AI contract (actions from the page)** +- `load_dashboard`, `start_automation`, `request_stop`, `load_latest_report`, `download_report`, `set_dark`, `logout` + +**Derived rules** +- A Pending batch from today is `inprogress`; one from an older day is `notcompleted`. +- Report buckets map {processed successfully, posted, completed} to success and {error, failed} to failed; everything else is attention. + +**AI contract additions.** These type shapes came from the Superbill prototype; trimmed: + +``` +Run { job_id, status: pending|queued|running|done|failed, model, + pages_done, pages_total, summary: { green, amber, red, unscored } } +ReviewRow { row_id, field, value, band: green|amber|red|blank|unscored, + score, reason, verified, corrected_value } +RoiRun { model, input_tokens, output_tokens, thinking_tokens, total_cost } +``` + +The AI skill adds these actions: patch one review row (corrected value, verified), approve a run (the server enforces the gate), and fetch ROI. While a single run is busy, it's polled every ≈1.5s. + +**Banding** +- Green ≥0.95, amber 0.90–0.95, red <0.90. +- A multi-word field takes the minimum word score. +- A blank mandatory field is always flagged. +- The caveat text is shown next to the legend. + +**Cost** +- Cost per call = input tokens × input rate + (output + thinking tokens) × output rate, per million tokens. +- The pricing table records its source and its as-of date. An unknown model raises an error naming the known models; it never defaults to zero. + +**Bot integration on desktop** +- The bot runs as a child process, and its stdout is tailed. +- The raw line is kept for audit. +- The step-marker table and the friendly-rules table use specific substrings, and the first match wins. +- Unmatched non-error lines are hidden; unmatched error lines are always shown. + +**Fill-in points** +- Template placeholders are filled by the skill: app name, subtitle, logo, steps, report columns, API base, bot module. +- Python templates must stay valid Python before they're filled, so placeholders go inside string literals. The current host template breaks this rule and is known lint debt. + +**Safety** +- Bundled references contain no PHI, internal IPs, credentials or keys. +- Project-specific names are removed or marked as examples. + +## Testing Decisions + +- **Good tests check external behaviour only:** what the user sees on the page for a given set of contract events, and what files and questions a skill produces for a given project. They don't check internal function names, CSS class names or the template's internal structure. +- **Seam 1: the data contract (primary).** + - Load a generated dashboard page in a headless browser with no backend. + - Push contract envelopes through the page's public entry point. + - Assert on the visible result. KPI numbers match the batch states; donut segments and legend counts match; the active and done steps match the step label; log lines appear with the right type; error lines from an unmatched bot line appear; report rows filter on search and show the right bucket pill; dark mode flips the theme. + - The AI variant also checks: band counts and legend; review rows filtered by band; approve stays blocked while rows are unverified (server stub returns refusal); ROI tiles sum the runs; a missing-price model surfaces an error. + - Run each case in light and dark, and at a narrow width (≤820px). +- **Seam 2: skill eval.** + - Run each skill against small fixture projects: + - (a) empty project, no frontend, no bot; + - (b) PyQt project with a bot that prints step markers; + - (c) web project with an existing frontend and its own colours; + - (d) for the AI skill, a project with an LLM extraction call. + - Assert that the skill reports the existing frontend only when there is one, and asks keep-or-restyle in that case. + - Assert that it asks for colours and stack only when appropriate, and asks for optional sections. + - Assert that it produces a page that passes Seam 1 with sample data, or with the fixture bot's real output in case (b). + - Use the skill-creator eval tooling, or `claude plugin eval`. +- **Python logic tests:** the pure Python pieces bundled in the templates get a small assert-based self-check: line classification and friendly-rule matching, band assignment, and cost calculation. The prior art is the band and cost functions in Superbill and Tebra's pytest suite for report categorisation and batch state. +- **Prior art:** Tebra has pytest files covering batch clear, report scope and status categorisation. Superbill's band thresholds and its server-enforced approval gate define the expected AI behaviour. + +## Out of Scope + +- **Backend generation:** database tables, stored procedures, worker queues, and auth/login services. The skills wire to what exists or ship sample data; they don't build the backend. (The optional login page is UI only.) +- **The automation itself:** UI automation steps, SOAP/API posting, and extraction prompts and templates. +- **Deployment:** the auto-update launcher and Dokploy. +- **Frameworks other than plain HTML (desktop/web) and React (AI web).** For an existing Vue/Angular frontend, the skill ports sections using the HTML as reference, best effort, with no dedicated template. +- **Exact ports of every original feature:** PDF zoom/fullscreen polish, drag-to-stage in the sidebar, the animated extraction progress, demography and insurance lookups, and the batch-clear approval flow. +- **Installing the skills into Claude.** The user copies the folders. + +## Further Notes + +- **Source projects:** Tebra ERA, a PyQt6 desktop app whose dashboard is one embedded HTML page; and Superbill Review, FastAPI with a React 19 + Vite + Tailwind v4 front end and Gemini on Vertex AI. Their design docs (Superbill's design and product notes, Tebra's context notes) explain the reasoning behind the visual rules and should be read before changing presets. +- **Already done in this folder:** + - the no-AI dashboard page, the PyQt host and the no-AI contract; + - reference extracts for both skills. +- **Still to do:** + - both skill files; + - both theme preset files; + - the AI contract; + - the AI desktop sections; + - stripping project names from the AI references; + - fixing the host template's placeholder lint; + - both test seams. +- **Plan:** the planning record with every grill decision is the plan file at the repo root. +- No glossary or ADRs exist yet for this folder. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..7827378 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,13 @@ +# RPA SKILLS + +Shared Claude skills for building RPA dashboards: `rpa-dashboard-no-ai` and `rpa-dashboard-ai`. See `PLAN.md`. + +## Agent skills + +### Issue tracker + +Issues and specs live as local markdown under `.scratch//`. See `docs/agents/issue-tracker.md`. + +### Domain docs + +Single-context: `GLOSSARY.md` + `docs/adr/` at the repo root. See `docs/agents/domain.md`. diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 0000000..c479f45 --- /dev/null +++ b/PLAN.md @@ -0,0 +1,115 @@ +# Plan: two RPA dashboard skills + +Goal: build two Claude skills. Future RPA projects use them to create a dashboard page. +- **`rpa-dashboard-no-ai`**: for RPA projects with no AI. Source project: `D:\Tebra_Auto_Post`. +- **`rpa-dashboard-ai`**: for RPA projects that use AI (LLM/OCR extraction). Source project: `D:\Superbill_extRACTION`. + +Build both in `D:\RPA SKILLS\\`. The user installs them into Claude later. + +## Decisions (from the grill) + +| # | Question | Decision | +|---|---|---| +| Q1 | Where the skills live | `D:\RPA SKILLS` only. The user installs them into Claude later. | +| Q2 | Tech stack | **Detect, then adapt.** Scan the project first. If a frontend exists, use its stack. If not, ask the user: Desktop (PyQt6 + QWebEngine HTML) or Web. Both skills support both. | +| Q3 | Colours when there's no frontend | **Offer presets + custom:** 1) Tebra Sky, 2) Superbill Ember, 3) Custom (user gives a primary/brand hex). Always produce light and dark token blocks. | +| Q4 | Frontend already exists | **Report, then ask.** Say "Frontend found at X, stack Y, colours Z". Then ask: keep the existing theme and add the dashboard in the same style, or restyle to a preset/custom palette. | +| Q5 | Sections | **Core + ask for extras** (see below). | +| Q6 | Reference code | **Bundle templates** inside each skill (`templates/`). Do not read the D:\ source projects at runtime. | +| Q7 | Data | **Standard contract + wire it.** One event/JSON contract. Wire it to the project's bot if one exists. Otherwise ship sample data so the page works on day one. | + +## Sections + +**Both skills, always built:** +- Shell: sidebar, topbar, dark toggle, status dot +- KPI cards +- Status donut +- Run panel: progress bar, steps and live log +- Report table with search and Excel export + +**Both skills, optional (ask the user):** +- Checkpoint tracker +- Admin/roles +- Login page + +**AI skill, also always built:** +- Confidence bands. Scoring: green ≥0.95, amber 0.90–0.95, red <0.90. A field gets the worst word score. The band caveat text is shown to the user. +- Review split pane: source document next to the extracted fields. Edits stay drafts until Save. Each field has Verify. Approve is a gate enforced on the server. +- ROI/cost page: tokens (input/output/thinking) per run, cost per run, monthly spend, coverage pill, "vs by hand" assumptions. + +**AI skill, also optional:** +- Model picker +- Retry on the other model + +## Skill workflow (each SKILL.md must follow this order) +1. **Scan the project.** Look for an existing frontend: `*.html`, `web/`, `package.json` with react/vue, a PyQt `QWebEngineView`. Also find the bot entry point and any status/log output. +2. **Report what you found, then ask Q4.** Skip this step if there is no frontend. +3. **Ask for colours (Q3).** Skip this step if the user chose to keep the existing theme. +4. **Ask the stack question (Q2).** Only if there is no frontend. +5. **Ask which optional sections to build (Q5).** +6. **Build.** Copy the template, fill the `{{PLACEHOLDERS}}`, apply the tokens, remove the sections the user didn't choose. +7. **Wire the data.** Connect to the bot using the contract. If there is no bot yet, use sample data. +8. **Verify.** Open the page, check both light and dark mode, and check width ≤820px. + +## Theme presets (put in `templates/themes.md` in BOTH skills) + +**Tebra Sky.** Glass cards on a gradient background. Segoe UI font. +- Light: `--bg-top:#8fc9e8; --bg-mid:#bfe3f2; --bg-bottom:#eaf6fb; --ink:#1c2b33; --muted:#5c7382; --accent-dark:#16202a; --accent:#3d9bd0; --accent-2:#5cb2e2; --ok:#1f9d57; --warn:#b7791f; --bad:#c0392b` +- Dark: `--bg-top:#0f1b26; --bg-mid:#13212e; --bg-bottom:#182a38; --ink:#e6eef4; --muted:#8fa6b4; --accent-dark:#2e5d80; --ok:#2fbf72; --warn:#d9a13c; --bad:#e05a4b` + +**Superbill Ember.** Flat surfaces. The brand gradient appears only on the header bar, primary buttons, the active nav row and the progress fill. Fonts: Sora for display, IBM Plex Sans for body. +- Light: `--ground:#f4f3f7; --surface:#ffffff; --surface-2:#f7f6fa; --line:#e2dfe9; --ink:#18151f; --ink-2:#3d3850; --muted:#66607a; --accent-1:#ff3049; --accent-2:#f26b21` +- Dark: `--ground:#0a0912; --surface:#151220; --surface-2:#1b1727; --line:#2a2438; --ink:#f3f1f8; --ink-2:#c9c4d8; --muted:#8d87a0` +- Band colours (AI only, semantic, never used on buttons): `--clean:#51a68f; --verify:#e39b34; --review:#de5243; --absent:#6b6580` + +**Custom.** Take the user's primary hex and derive from it: +- `--accent` +- `--accent-2`: lighter +- `--accent-dark`: darker +- `--accent-soft`: tint +- Keep ok/warn/bad unless the user gives their own. +- Check text contrast in both light and dark. + +## Status: what already exists + +``` +rpa-dashboard-no-ai/ + templates/dashboard.html DONE: generic page, works in Qt (QWebChannel) or web (polling) + templates/contract.md DONE: event/JSON contract + Tebra lessons + templates/bridge.py DONE: minimal PyQt6 host + subprocess log tailing + templates/reference/ DONE: tebra_dashboard.html, tebra_bridge_part1/2.py (raw extracts) +rpa-dashboard-ai/ + templates/reference/web/ DONE: Superbill index.css, DESIGN.md, bands.ts, theme.tsx, types.ts, api.ts, + components (BrandHeader, Sidebar, StatsBar, Donut, SplitPane, + RunPanel, ReviewPane, RoiTab, PdfPane, Tooltip, icons) + templates/reference/backend/ DONE: bands.py, gemini_cost.py (PRICING table + cost_for_call) +``` + +## TODO for the implement skill +1. **Fix the lint error in `rpa-dashboard-no-ai/templates/bridge.py:15`.** `STEPS = {{STEPS_PY}}` is not valid Python, so the lint hook flags it (F821). Change it to `STEPS = ["{{STEP_1}}", "{{STEP_2}}"]` or similar. +2. **Write `rpa-dashboard-no-ai/SKILL.md`.** + - Frontmatter `name` and `description`. The trigger should cover "RPA dashboard", "automation dashboard", "bot dashboard", and "without AI". + - The workflow above. + - Point to `templates/*`. +3. **Write `rpa-dashboard-ai/SKILL.md`.** + - Trigger: "AI RPA dashboard", "extraction dashboard", "LLM/OCR review dashboard". + - The workflow above. + - Web path: React + Vite + Tailwind v4, adapted from `reference/web`. + - Desktop path: copy the no-AI `dashboard.html`, then add the AI sections. +4. **Write `templates/themes.md` in both skills.** Content is in the Theme presets section above. +5. **Write `rpa-dashboard-ai/templates/contract.md`.** It extends the no-AI contract with: + - `Run{job_id,status:pending|queued|running|done|failed,model,pages_done,pages_total,summary:{green,amber,red,unscored}}` + - `ReviewRow{row_id,field,value,band,score,reason,verified,corrected_value}` + - `RoiRun{model,input_tokens,output_tokens,thinking_tokens,total_cost}` + - Endpoints: `PATCH /review/{row_id}`, `POST /runs/{id}/approve` (server gate), `GET /roi`. + - Polling: 1.5–2s only while a run is busy. +6. **Write `rpa-dashboard-ai/templates/ai-sections.html`.** Plain HTML/CSS/JS versions of the band legend, the review split pane and the ROI tiles, for the desktop path. +7. **Strip project-specific bits from the AI reference files.** Remove ECW/Tebra project names, SPB_* tables and patient/ticket wording, or mark them as such. **Never** ship real patient data (PHI), internal IPs or keys. The reference copies have already been checked for these, but check again after editing. +8. **Verify.** Open `dashboard.html` in a browser using sample data. Check light, dark and narrow width. Load each SKILL.md and make sure every path it references exists. + +## Done when +- Both folders have a SKILL.md and all the templates it references. +- In a fresh test project, each skill: + - asks the questions in order, + - reports any existing frontend, + - produces a working dashboard page that renders in light and dark. diff --git a/docs/agents/domain.md b/docs/agents/domain.md new file mode 100644 index 0000000..4032de0 --- /dev/null +++ b/docs/agents/domain.md @@ -0,0 +1,51 @@ +# Domain Docs + +How the engineering skills should consume this repo's domain documentation when exploring the codebase. + +## Before exploring, read these + +- **`GLOSSARY.md`** at the repo root, or +- **`GLOSSARY-MAP.md`** at the repo root if it exists: it points at one `GLOSSARY.md` per context. Read each one relevant to the topic. +- **`docs/adr/`**: read ADRs that touch the area you're about to work in. In multi-context repos, also check `src//docs/adr/` for context-scoped decisions. + +If any of these files don't exist, **proceed silently**. Don't flag their absence; don't suggest creating them upfront. The `/domain-modeling` skill (reached via `/grill-with-docs` and `/improve-codebase-architecture`) creates them lazily when terms or decisions actually get resolved. + +## File structure + +Single-context repo (most repos): + +``` +/ +├── GLOSSARY.md +├── docs/adr/ +│ ├── 0001-event-sourced-orders.md +│ └── 0002-postgres-for-write-model.md +└── src/ +``` + +Multi-context repo (presence of `GLOSSARY-MAP.md` at the root): + +``` +/ +├── GLOSSARY-MAP.md +├── docs/adr/ ← system-wide decisions +└── src/ + ├── ordering/ + │ ├── GLOSSARY.md + │ └── docs/adr/ ← context-specific decisions + └── billing/ + ├── GLOSSARY.md + └── docs/adr/ +``` + +## Use the glossary's vocabulary + +When your output names a domain concept (in an issue title, a refactor proposal, a hypothesis, a test name), use the term as defined in `GLOSSARY.md`. Don't drift to synonyms the glossary explicitly avoids. + +If the concept you need isn't in the glossary yet, that's a signal: either you're inventing language the project doesn't use (reconsider) or there's a real gap (note it for `/domain-modeling`). + +## Flag ADR conflicts + +If your output contradicts an existing ADR, surface it explicitly rather than silently overriding: + +> _Contradicts ADR-0007 (event-sourced orders), but worth reopening because…_ diff --git a/docs/agents/issue-tracker.md b/docs/agents/issue-tracker.md new file mode 100644 index 0000000..a13b77d --- /dev/null +++ b/docs/agents/issue-tracker.md @@ -0,0 +1,30 @@ +# Issue tracker: Local Markdown + +Issues and specs for this repo live as markdown files in `.scratch/`. + +## Conventions + +- One feature per directory: `.scratch//` +- The spec is `.scratch//spec.md` +- Implementation issues are one file per ticket at `.scratch//issues/-.md`, numbered from `01`, never a single combined tickets file +- Triage state is recorded as a `Status:` line near the top of each issue file, using one of: `needs-triage`, `needs-info`, `ready-for-agent`, `ready-for-human`, `wontfix` +- Comments and conversation history append to the bottom of the file under a `## Comments` heading + +## When a skill says "publish to the issue tracker" + +Create a new file under `.scratch//` (creating the directory if needed). + +## When a skill says "fetch the relevant ticket" + +Read the file at the referenced path. The user will normally pass the path or the issue number directly. + +## Wayfinding operations + +Used by `/wayfinder`. The **map** is a file with one **child** file per ticket. + +- **Map**: `.scratch//map.md` (the Notes / Decisions-so-far / Fog body). +- **Child ticket**: `.scratch//issues/NN-.md`, numbered from `01`, with the question in the body. A `Type:` line records the ticket type (`research`/`prototype`/`grilling`/`task`); a `Status:` line records `claimed`/`resolved`. +- **Blocking**: a `Blocked by: NN, NN` line near the top. A ticket is unblocked when every file it lists is `resolved`. +- **Frontier**: scan `.scratch//issues/` for files that are open, unblocked, and unclaimed; first by number wins. +- **Claim**: set `Status: claimed` and save before any work. +- **Resolve**: append the answer under an `## Answer` heading, set `Status: resolved`, then append a context pointer (gist + link) to the map's Decisions-so-far in `map.md`. diff --git a/rpa-dashboard-ai/templates/reference/backend/bands.py b/rpa-dashboard-ai/templates/reference/backend/bands.py new file mode 100644 index 0000000..5e447e3 --- /dev/null +++ b/rpa-dashboard-ai/templates/reference/backend/bands.py @@ -0,0 +1,64 @@ +"""Confidence-band classification — single source of truth for the cut points. + +The workbook colours and the review queue MUST agree about what counts as red. +If each applied its own threshold they would drift the first time one is tuned, +and a reviewer would see a red cell the queue never asked them to check. +Everything that needs a band imports from here. +""" +from __future__ import annotations + +from typing import Optional + +# --------------------------------------------------------------------------- +# Cut points +# --------------------------------------------------------------------------- +# Calibrated against the measured distribution over all 225 extracted rows of +# the six sample documents: +# +# 1.00 69.8% crisp print +# 0.90-0.99 18.2% crisp print +# 0.80-0.89 11.0% almost entirely handwritten CPT and modifiers +# 0.70-0.79 0.0% <- empty; the natural gap in this data +# <0.70 1.1% handwritten modifiers +# +# 0.80 left only 1.1% of cells red so nearly everything went green and the +# colour carried no information. 0.90 sits in the empty band and separates +# the handwriting cluster from printed text. +CONFIDENCE_THRESHOLD: float = 0.90 + +# Above CONFIDENCE_THRESHOLD the 0.90-0.99 band is still 18.2% of cells, and +# routing treats it differently from a clean 1.00: reviewers sample it rather +# than either trusting it outright or checking every one. A second cut at +# 0.95 names that band so the sheet shows what the routing rule already assumes. +# +# GREEN >= 0.95 clean print, straight through +# AMBER 0.90-0.95 slightly unclear, sampled +# RED < 0.90 handwriting cluster, always reviewed +GREEN_THRESHOLD: float = 0.95 + +# --------------------------------------------------------------------------- +# Band name constants +# --------------------------------------------------------------------------- +BAND_GREEN = "green" +BAND_AMBER = "amber" +BAND_RED = "red" + + +def band_of(score: Optional[float]) -> Optional[str]: + """Return the confidence band name, or ``None`` when the value was never scored. + + WHAT THE BAND CLAIMS: green means the source text this value came from was + transcribed cleanly. It does NOT mean the value is correct. The score is + the model's own legibility rating, so a confident misread comes back scored + high. Verifying correctness needs a second, independent read. Red is the + useful direction: it reliably surfaces handwriting. + + Amber does not mean "probably fine". It means the reading was good enough to + escape the handwriting cluster but not clean enough to trust unsampled, which + is a routing decision, not a quality claim. + """ + if score is None: + return None + if score >= GREEN_THRESHOLD: + return BAND_GREEN + return BAND_AMBER if score >= CONFIDENCE_THRESHOLD else BAND_RED diff --git a/rpa-dashboard-ai/templates/reference/backend/gemini_cost.py b/rpa-dashboard-ai/templates/reference/backend/gemini_cost.py new file mode 100644 index 0000000..7780a4a --- /dev/null +++ b/rpa-dashboard-ai/templates/reference/backend/gemini_cost.py @@ -0,0 +1,60 @@ +INCLUDE_THINKING_IN_COST = True # thinking tokens bill at output rate + +# ═════════════════════════════════════════════════════════════════════════════ +# GEMINI PRICING TABLE +# ═════════════════════════════════════════════════════════════════════════════ +PRICING_SOURCE = "https://ai.google.dev/gemini-api/docs/pricing" +PRICING_AS_OF = "2026-08-11" + +GEMINI_PRICING = { + # ---- Gemini 3.x family -------------------------------------------------- + "gemini-3.6-flash": {"input": 1.50, "output": 7.50, "free_tier": True}, + "gemini-3.5-flash": {"input": 1.50, "output": 9.00, "free_tier": True}, + "gemini-3.5-flash-lite": {"input": 0.30, "output": 2.50, "free_tier": True}, + "gemini-3-flash-preview": {"input": 0.50, "output": 3.00, "free_tier": True}, + "gemini-3.1-flash-lite": {"input": 0.25, "output": 1.50, "free_tier": True}, + "gemini-3.1-pro-preview": { + "input": 2.00, "output": 12.00, "free_tier": False, + "long_threshold": 200_000, "input_long": 4.00, "output_long": 18.00, + }, + # ---- Gemini 2.5 family -------------------------------------------------- + "gemini-2.5-pro": { + "input": 1.25, "output": 10.00, "free_tier": True, + "long_threshold": 200_000, "input_long": 2.50, "output_long": 15.00, + }, + "gemini-2.5-flash": {"input": 0.30, "output": 2.50, "free_tier": True}, + "gemini-2.5-flash-lite": {"input": 0.10, "output": 0.40, "free_tier": True}, + "gemini-2.5-flash-lite-preview-09-2025": {"input": 0.10, "output": 0.40, "free_tier": True}, + # ---- Gemini 2.0 family (DEPRECATED — shut down 2026-06-01) -------------- + "gemini-2.0-flash": {"input": 0.10, "output": 0.40, "free_tier": True, "deprecated": True}, + "gemini-2.0-flash-lite": {"input": 0.075, "output": 0.30, "free_tier": True, "deprecated": True}, +} + + +def resolve_pricing(model: str, input_tokens: int): + if model not in GEMINI_PRICING: + raise KeyError( + f"No pricing entry for model '{model}'. Add it to GEMINI_PRICING " + f"from {PRICING_SOURCE}. Known models: {', '.join(GEMINI_PRICING)}" + ) + p = GEMINI_PRICING[model] + note = "" + in_rate, out_rate = p["input"], p["output"] + threshold = p.get("long_threshold") + if threshold and input_tokens > threshold: + in_rate = p.get("input_long", in_rate) + out_rate = p.get("output_long", out_rate) + note = f"long-context rate (>{threshold:,} input tokens)" + return in_rate, out_rate, note + + +def cost_for_call(model: str, usage: dict): + in_rate, out_rate, note = resolve_pricing(model, usage["input"]) + billable_output = usage["output"] + if INCLUDE_THINKING_IN_COST: + billable_output += usage.get("thinking", 0) + input_cost = usage["input"] / 1_000_000 * in_rate + output_cost = billable_output / 1_000_000 * out_rate + return input_cost + output_cost, input_cost, output_cost, note + + diff --git a/rpa-dashboard-ai/templates/reference/web/DESIGN.md b/rpa-dashboard-ai/templates/reference/web/DESIGN.md new file mode 100644 index 0000000..a20252d --- /dev/null +++ b/rpa-dashboard-ai/templates/reference/web/DESIGN.md @@ -0,0 +1,451 @@ +--- +name: Superbill Extraction and Automation Tool +description: Extract billing fields from a scan, verify them against the page, post to eClinicalWorks. +# ── Colour ─────────────────────────────────────────────────────────────────── +# Two appearances, light and dark, declared as CSS variables in web/src/index.css +# and registered as Tailwind utilities there via `@theme inline`. A component +# names a token (`bg-surface`, `text-muted`); it never names a hex. +colors: + # Surfaces and ink — light + ground: "#F4F3F7" + rail: "#FFFFFF" + surface: "#FFFFFF" + surface-2: "#F7F6FA" + line: "#E2DFE9" + line-soft: "#ECEAF1" + ink: "#18151F" + ink-2: "#3D3850" + muted: "#66607A" + # Surfaces and ink — dark + dark-ground: "#0A0912" + dark-rail: "#100E1A" + dark-surface: "#151220" + dark-surface-2: "#1B1727" + dark-line: "#2A2438" + dark-line-soft: "#221D31" + dark-ink: "#F3F1F8" + dark-ink-2: "#C9C4D8" + dark-muted: "#8D87A0" + # The brand ramp. Identical in both appearances, and the only saturated thing + # on a page: header bar, primary buttons, active nav bar, super-admin badge, + # progress fill. Nothing else gets the gradient. + accent-1: "#FF3049" + accent-2: "#F26B21" + # Grading. Semantic, never decorative — print clarity, not correctness. + clean: "#51A68F" + verify: "#E39B34" + review: "#DE5243" + absent: "#6B6580" + # Artwork. The logo shield is the same mark in both appearances. + shield: "#21395A" + # The workbook's own cell fills. NOT interface colours — these are the + # highlight chips drawn over the scanned page, matching what + # patient_extractor_v2.py writes into the spreadsheet, so a reviewer moving + # between the sheet and the screen sees one language. + confidence-green: "#B6E2B6" + confidence-green-border: "#9AC79A" + confidence-amber: "#F4E2B6" + confidence-amber-border: "#D9C79A" + confidence-red: "#F4B6B6" + confidence-red-border: "#D99A9A" + confidence-unscored: "#FFFFFF" + confidence-unscored-border: "#D0D0D0" +# ── Type ───────────────────────────────────────────────────────────────────── +# Two families, both loaded from Google Fonts with display=swap and a full +# system fallback. Sora is display — headings, figures, brand name, buttons, +# batch ids. IBM Plex Sans is everything read as prose — body, labels, table +# cells, captions. Numbers that sit in a column carry tabular-nums. +typography: + display-page: { fontFamily: "'Sora'", fontSize: "22px", fontWeight: 700, lineHeight: 1.2 } + display-figure: { fontFamily: "'Sora'", fontSize: "21px", fontWeight: 700, lineHeight: 1.1 } + display-spend: { fontFamily: "'Sora'", fontSize: "24px", fontWeight: 700, lineHeight: 1.1 } + display-appbar: { fontFamily: "'Sora'", fontSize: "17px", fontWeight: 700, lineHeight: 1.15 } + display-runbar: { fontFamily: "'Sora'", fontSize: "17px", fontWeight: 700, lineHeight: 1.2 } + display-drop: { fontFamily: "'Sora'", fontSize: "16px", fontWeight: 700, lineHeight: 1.2 } + display-panel: { fontFamily: "'Sora'", fontSize: "14px", fontWeight: 600, lineHeight: 1.25 } + display-group: { fontFamily: "'Sora'", fontSize: "11.5px", fontWeight: 600, lineHeight: 1.25 } + button: { fontFamily: "'Sora'", fontSize: "13px", fontWeight: 600, lineHeight: 1.2 } + button-sm: { fontFamily: "'Sora'", fontSize: "12px", fontWeight: 600, lineHeight: 1.2 } + body: { fontFamily: "'IBM Plex Sans'", fontSize: "14px", fontWeight: 400, lineHeight: 1.5 } + body-doc: { fontFamily: "'IBM Plex Sans'", fontSize: "13.5px", fontWeight: 600, lineHeight: 1.3 } + body-table: { fontFamily: "'IBM Plex Sans'", fontSize: "13px", fontWeight: 400, lineHeight: 1.4 } + nav: { fontFamily: "'IBM Plex Sans'", fontSize: "13px", fontWeight: 500, lineHeight: 1.3 } + meta: { fontFamily: "'IBM Plex Sans'", fontSize: "12.5px", fontWeight: 400, lineHeight: 1.4 } + caption: { fontFamily: "'IBM Plex Sans'", fontSize: "11.5px", fontWeight: 400, lineHeight: 1.4 } + label: { fontFamily: "'IBM Plex Sans'", fontSize: "12px", fontWeight: 600, lineHeight: 1.3 } + # Uppercase, letter-spaced. Table headers and section eyebrows. + eyebrow: { fontFamily: "'IBM Plex Sans'", fontSize: "10.5px", fontWeight: 600, lineHeight: 1.3, textTransform: "uppercase", letterSpacing: "0.1em" } + badge: { fontFamily: "'IBM Plex Sans'", fontSize: "11px", fontWeight: 600, lineHeight: 1.2 } + # ── Sign-in screen only ── + # Its own scale, deliberately an island — see Typography > "Sign-in scale". + signin-title: { fontFamily: "'Sora'", fontSize: "clamp(22px, 3.2vw, 26px)", fontWeight: 700, lineHeight: 1.2 } + signin-brand: { fontFamily: "'Sora'", fontSize: "16px", fontWeight: 700, lineHeight: 1.2 } + signin-body: { fontFamily: "'IBM Plex Sans'", fontSize: "14.5px", fontWeight: 400, lineHeight: 1.55 } + signin-label: { fontFamily: "'IBM Plex Sans'", fontSize: "12.5px", fontWeight: 600, lineHeight: 1.3, textTransform: "uppercase", letterSpacing: "0.06em" } +# ── Shape ──────────────────────────────────────────────────────────────────── +# ONE card radius. Everything that is a card, panel, table container, dropzone +# or empty state is 14px; the smaller steps are for controls inside them. +rounded: + card: "14px" + field: "11px" + control: "9px" + input: "8px" + icon: "7px" + chip: "10px" + swatch: "3px" + full: "9999px" + # Sign-in only. + signin-card: "20px" + signin-control: "10px" +spacing: + xs: "4px" + sm: "8px" + md: "12px" + lg: "16px" + xl: "18px" + xxl: "22px" +components: + app-bar: + background: "linear-gradient(100deg, {colors.accent-1}, {colors.accent-2})" + textColor: "#FFFFFF" + padding: "12px 18px" + nav-item-active: + borderLeft: "3px solid {colors.accent-1}" + background: "linear-gradient(90deg, color-mix(in srgb, {colors.accent-1} 12%, transparent), transparent)" + button-primary: + background: "linear-gradient(115deg, {colors.accent-1}, {colors.accent-2})" + textColor: "#FFFFFF" + rounded: "{rounded.control}" + padding: "9px 16px" + button-quiet: + backgroundColor: "{colors.surface-2}" + textColor: "{colors.ink}" + border: "1px solid {colors.line}" + rounded: "{rounded.control}" + card: + backgroundColor: "{colors.surface}" + border: "1px solid {colors.line}" + rounded: "{rounded.card}" + padding: "14px 16px" + pill-clean: + backgroundColor: "color-mix(in srgb, {colors.clean} 22%, transparent)" + textColor: "{colors.clean}" + rounded: "{rounded.full}" + pill-verify: + backgroundColor: "color-mix(in srgb, {colors.verify} 20%, transparent)" + textColor: "{colors.verify}" + rounded: "{rounded.full}" + pill-review: + backgroundColor: "color-mix(in srgb, {colors.review} 20%, transparent)" + textColor: "{colors.review}" + rounded: "{rounded.full}" + badge-super-admin: + background: "linear-gradient(115deg, {colors.accent-1}, {colors.accent-2})" + textColor: "#FFFFFF" + rounded: "{rounded.full}" + field-row: + backgroundColor: "{colors.surface}" + border: "1px solid {colors.line}" + rounded: "{rounded.field}" + gridTemplateColumns: "78px minmax(0,1fr) 42px 70px" +--- + +# Design System: Superbill Review + +## Overview + +**Creative North Star: "The Triage Desk"** + +Superbill Review reads like an emergency triage desk, not a spreadsheet. Every +extracted field arrives already sorted by how urgently it needs attention — +green passes through untouched, amber gets a spot-check, red stops for a full +look — so a reviewer's eye lands exactly where judgment is actually required +and nowhere else. The interface itself stays out of the way: a dark, quiet +"console" surface with almost no decoration, so the one signal that is allowed +to be loud — the confidence band on a field — is never competing with anything +else for attention. + +The system is calm, precise, and unhurried by design, because the job it +supports is not: a reviewer working through dozens of patients per document +needs an interface that does not add its own visual noise on top of already +noisy handwritten source material. Confirmed rejection: no bright accent +palette, no illustrative flourishes, no "friendly SaaS" warmth — this is an +operations tool for people doing careful, repetitive verification work, and it +should feel like a well-run night shift, not a marketing site. + +**Key Characteristics:** +- One loud signal only — the green/amber/red confidence band — everything else recedes +- Flat by default; depth appears only as a response to state, not as decoration +- Dense, scannable typography tuned for many short fields in a row, not prose +- Every screen answers "what needs me right now" before anything else + +## Colors + +Two appearances — **Light and Dark, and only those two** — declared once as CSS +variables in `web/src/index.css` and registered there as Tailwind utilities via +`@theme inline`. A component names a token (`bg-surface`, `text-muted`, +`border-line`); it never names a hex. If a colour decision is not in that file, +it is not a colour decision this product makes. + +`@theme inline` rather than plain `@theme` is load-bearing: inline emits each +utility as `var(--surface)` at the use site instead of snapshotting the value, +which is what lets `.dark` reassign a token and repaint every utility built on +it. Plain `@theme` would freeze the light value into the stylesheet. + +### Surfaces and ink + +| token | light | dark | what it is | +|---|---|---|---| +| `ground` | `#F4F3F7` | `#0A0912` | the page behind everything | +| `rail` | `#FFFFFF` | `#100E1A` | the sidebar, and the review screen's own strips | +| `surface` | `#FFFFFF` | `#151220` | cards, panels, tables | +| `surface-2` | `#F7F6FA` | `#1B1727` | inset things: inputs, chips, wells | +| `line` | `#E2DFE9` | `#2A2438` | the one border colour | +| `line-soft` | `#ECEAF1` | `#221D31` | the divider between rows of a list | +| `ink` | `#18151F` | `#F3F1F8` | anything being read | +| `ink-2` | `#3D3850` | `#C9C4D8` | text that supports it | +| `muted` | `#66607A` | `#8D87A0` | labels and captions, found rather than read | + +The ink ramp is three steps on purpose. Anything reaching for a fourth is +usually reaching for the wrong one of the three. + +### Brand ramp — the only saturated thing on a page + +- **Accent 1** (`#FF3049`) → **Accent 2** (`#F26B21`), identical in both + appearances, and the same two hues the sign-in screen uses — so signing in is + a continuation rather than a change of product. +- `linear-gradient(115deg, …)` on controls, `100deg` on the wide header band. +- It appears on exactly six things: the header bar, primary buttons, the active + nav row's 3px bar and 12% wash, the super-admin badge, the review progress + fill, and a batch id. **Nothing else gets the gradient.** + +### Grading — semantic, never decorative + +- **Clean** (`#51A68F`): legibility ≥0.95 — clean print, passes straight through. +- **Verify** (`#E39B34`): 0.90–0.95 — sampled rather than checked one by one. +- **Review** (`#DE5243`): <0.90 — handwritten or hard to read, always reviewed. +- **Absent** (`#6B6580`): no legibility signal at all. Grey because nothing was + read, so there is no clarity to grade — distinct from "read badly". + +These appear on status pills, the 3px grade rule down a field input, legend +swatches, donut segments and quality meters. **Never on a button, never on a +heading.** The moment green also means "primary action", neither reading +survives. `BAND_VAR` in `web/src/bands.ts` is the one map from a band to its +token. + +### The workbook fills are not interface colours + +`confidence-green/amber/red` (`#B6E2B6` / `#F4E2B6` / `#F4B6B6`) and their +borders are still in the token list and still used — but only for the highlight +chips drawn **over the scanned page**. They mirror what +`patient_extractor_v2.py` fills spreadsheet cells with, so a reviewer moving +between the sheet and this screen sees one language. Interface tokens would +break that, and these pastels would be invisible on a white scan besides. + +### Named Rules + +**The One Loud Colour Rule.** The brand ramp is the only saturated thing on a +page, and grading is the only colour carrying meaning. A third loud hue would +compete with the one signal the product exists to surface. + +**Grading Never Becomes Chrome.** A grading colour on a button or a heading +destroys both readings at once. The only adjacent case is a destructive action +(`Remove` on the People screen), which uses `review` as text — not as a fill. + +**One Border, One Radius.** Every card, panel, table container, dropzone and +empty state is `1px solid line` at `14px`. Hierarchy comes from spacing, not +from stacking shadows: a page of cards at three elevations reads as a pile, and +every screen in this product is a list of peers. + +**There Is No Third Appearance.** An `eye` mode — the light palette under a +warm sepia filter — was removed. It was a third surface to keep in sync +forever, it tinted the scanned page a reviewer is trying to read accurately, +and both the OS and the monitor already offer the same warmth system-wide. + +## Typography + +**Display Font:** Sora 600/700 — page titles, panel headings, figures, the app-bar title, buttons, batch ids, group headers in the rail. + +**Body Font:** IBM Plex Sans 400/500/600 — everything read as prose: body copy, nav labels, table cells, captions, form labels, metadata. + +Both load from Google Fonts with `display=swap`, and every rule that names one carries the full system stack (`ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Arial, sans-serif`) behind it. + +**This changed.** The interface used to run entirely on the system stack — a density argument for 11–13px lists: at label size, Inter beside Segoe UI is a distinction nobody reading a field list notices, and it cost a network dependency on an internal deployment. That argument was sound in isolation and wrong in aggregate. Once the sign-in screen was rebuilt on Sora/IBM Plex, signing in read as leaving one product and arriving at another. Two families across the whole product is the cheaper inconsistency. + +**Why these two.** They are the sign-in screen's pairing, which is the approved mockup's pairing, carried inward rather than re-chosen. Sora has a real 600 and 700 so weights are drawn rather than synthesised, and its figures hold up at 21–24px where this product puts its money. IBM Plex Sans was drawn for interfaces and stays legible at the 10.5–13px the tables and rails live at. + +**The rule that survives.** A blocked CDN must cost voice and nothing else. No layout figure may depend on either font's metrics without a re-measurement noted beside it, and adding a third family needs a reason written down here first. + +**Numbers.** Anything that sits in a column — money, counts, timestamps, confidence figures, percentages — carries `tabular-nums` (the `tnum` class). Figures that shift width as they update are the single most common reason a dashboard looks unstable. + +### Sign-in scale — a documented exception + +The sign-in screen shares the two families but not the size or radius scales. It carries the mockup's own geometry so implementation and mockup stay comparable: **radius** 20px (card) and 10px (fields, button), **type** `clamp(22px, 3.2vw, 26px)` headline, 16px brand name, 14.5px lede/fields/button, 12.5px uppercase labels. + +This is a deliberate island, not drift. It is the only surface that is neither a list nor a board, it is seen once per session, and it never renders beside a component built from the scales below — so there is nothing for it to be inconsistent *with*. A value from that list appearing on any other surface is a bug. + +### Hierarchy +- **Figure Lead** (700 weight, 26px, 1.15 line-height): the one dominant quantity on a reporting surface — total spend on the ROI board. At most one per screen; a second one means neither is the answer. +- **Figure** (700 weight, 20px, 1.2 line-height): a supporting quantity that still has to be read before its own label — the settled-field percentage, each side of the by-hand comparison. +- **Heading** (700 weight, 16px, 1.25 line-height): patient name in the review header, document title, the brand wordmark. +- **Control** (600 weight, 13px, 1.3 line-height): navigation that has to be found before it is read — the Extraction/Automation tabs, a batch card's name, a primary button on the batch boards. +- **Body** (400 weight, 12px, 1.4 line-height): field values, document metadata, most UI copy. +- **Label** (500 weight, 11px, 1.3 line-height): field names, filter pills, band captions, timestamps. + +### Named Rules +**The Flat Hierarchy Problem — partially addressed.** The shipped UI used only 11px, 12px, and 16px, roughly 1.45:1 from smallest to heading. A hierarchy that flat asks color and weight to do all the work vision normally gets from size. The **Control** step (13px/600) was added for the batch-board and tab-bar chrome, which sits above the data-dense lists and needs to be found without competing with a heading. The ramp is now 11/12/13/16/20/26. 16px stays the ceiling inside any data-dense list, reserved for the one heading that anchors the view; 20px and 26px appear only on surfaces that are not lists — today that is the ROI board, which carries both. The sign-in screen no longer counts toward this ramp: it runs on the `signin-*` steps instead, for the reasons in Typography > "Sign-in scale". + +### Sign-in screen + +The one screen with nothing else on it, so the brand sweep carries the whole +left panel rather than a bar across the top — the only place in the product +where the palette is decorative rather than functional. Below `lg` that panel +collapses entirely and the form takes the full width: a half-screen of +decoration would push the fields under the fold on a laptop. Type runs +26px headline / 20px title / 13px controls / 12px supporting copy. + +**Display — removed.** There used to be a 44–72px step +(`clamp(2.75rem, 4.6vw, 4.5rem)`) for a product wordmark set across the waist +of a viewport-tall monogram. Both are gone: the letter spanned the screen with +the card sitting on its spine, so the parts that showed around the card read as +a ring rather than as a letter. Nothing now exceeds the 26px ceiling, and the +sign-in screen's own type is the `signin-*` steps in the token block above. + +**The rule divides the lockup.** The name sits above the brand rule and +`AUTOMATION` below it, so the rule that cuts the S is also the line of the +wordmark — one gesture doing both jobs. The whole block is therefore rotated +onto the rule's own −8° axis: set horizontal, the rule climbs ~53px across the +width of the name and clips a letter at one end or the other, and the dodge +(parking the wordmark in the clear space above) reads as a wordmark avoiding a +line. Parallel, the gap stays constant at every viewport width. The tilt is +structural, not stylistic — it is the only reason the geometry holds without +pinning a gap to a text width the font resolves at runtime. + +Implementation note: the lockup's container is `h-0` at `top-1/2`, so it shares +the rule's rotation pivot and its own axis *is* the rule. The two lines are then +offset by fixed distances (0.5rem / 0.85rem) rather than by anything computed +per breakpoint. The offsets are deliberately unequal because cap height is not +box height — uppercase leaves ~8px of empty descender space below the 72px line +and only ~2px above the 20px one, so equal offsets read as unequal air. + +It is sized like a headline but weighted like a background: white at 88% +stepping to 52% across a gradient raked to match the light behind it, so the +card's 26px pure-white headline is still the loudest type on the screen. That +the sign-in headline leads is unchanged — the wordmark is the room, not the +sentence. A wordmark at full white here reverses that and the screen becomes a +poster rather than a sign-in. + +Gated at `xl`, not `lg`. Between the two the card's left edge lands roughly +where the wordmark ends; below `xl` the S carries the screen alone and the +card's own header names the product. + +**The reveal covers the whole hero, not just the letter.** The cursor torch +lights the monogram and the wordmark together, as one field: the letter goes +brand red, the name goes red-into-orange on the same 150° rake as its resting +white gradient. It is one mask on one unrotated, full-size layer that carries +both marks — which is the only arrangement that works, since a mask on the +wordmark's own container would rotate with it and the torch would slide off the +cursor. Everything in the hero is therefore rendered twice, once per layer. + +**Where the name overlaps the letter, a halo separates them.** The name is +placed to clear the monogram's painted edge where the screen is wide enough, +but on squarer screens the letter grows until no channel remains between it and +the card, and the name sits on the letter instead. A single tight dark halo +behind the glyphs keeps the two from tangling. It stays tight on purpose: a +wider one reaches the brand rule ~16px below and veils the very gesture the +lockup is built on. The halo is a `filter`, never a `text-shadow` — these glyphs +are a background clipped to text, and a text shadow on transparent text paints +straight through the letterform. + +## Layout + +The document list is a vertical stack of full-width rows on a light `Paper` +background, each row a single click target. Opening a document replaces the +list entirely with a two-pane, full-window split (draggable divider, ratio +persisted in `localStorage`): the source PDF on the left inside a `Midnight +Console Deep` canvas, the field-by-field review queue on the right on +`Midnight Console`. Density is high by design — the review pane favors many +short rows over few tall ones, since the job is scanning many fields fast, not +reading long-form content. + +Within the review pane, navigation is patient-by-patient rather than a long +scroll: one patient's full field record fills the pane at a time, with +Prev/Next controls, a jump dropdown, and left/right arrow-key navigation. The +PDF pane follows the active patient's page automatically. + +## Elevation & Depth + +Flat by default. Surfaces sit at the same visual plane at rest — no ambient +drop shadows on cards, panels, or list rows. Depth appears only as a direct +response to state: the currently-open patient card gets `shadow-lg +shadow-black/20` to read as "in focus," and the PDF page itself gets +`shadow-xl shadow-black/40` specifically to read as a physical page sitting on +the dark canvas rather than blending into it. Nothing gets a shadow simply for +decoration. + +### Named Rules +**The Flat-at-Rest Rule.** Depth is a state signal, not a texture. If a shadow is not communicating "this is the thing that's active/focused/physical right now," it does not belong. + +## Shapes + +Corners are consistently soft but modest: `rounded-md` (6px) is the default for +inputs, value chips, and buttons; `rounded-lg` (8px) for cards and the larger +patient/document containers; `rounded-full` for pill-shaped filter buttons and +status badges. No sharp corners, no aggressively rounded (>8px) surfaces — +the form language stays understated rather than either clinical-sharp or +playful-round. + +## Components + +Every interactive element is restrained and functional: minimal ornament, a +single clear affordance, and just enough state feedback to confirm what +happened. Nothing asks to be noticed on its own — the confidence bands already +carry that job. + +### Buttons +- **Shape:** `rounded-md` (6px), except pill-shaped filters (`rounded-full`). +- **Primary (Confirm/OK/Approve):** `accent-confirm` emerald background, dark text, used for "Verify," "OK," and "Approve for posting." +- **Secondary/Neutral:** `bg-slate-800` with `text-slate-300`, used for Undo, Retry, and non-destructive secondary actions. +- **Hover / Focus:** background lightens one step (`hover:bg-slate-700`, `hover:bg-emerald-400`); no transform or shadow added on hover, consistent with the flat-at-rest rule. +- **Disabled:** `opacity-40` plus `cursor-not-allowed`, used specifically to make the Approve gate's "you can't do this yet" state legible. + +### Filter Pills +- **Style:** pill (`rounded-full`), `bg-slate-800`/`text-slate-300` at rest, inverted to `bg-slate-200`/`text-slate-900` when active. +- **State:** exactly one pill active at a time (Red / Amber / Missing code / Unscored / All). + +### Cards / Containers (Patient record) +- **Corner Style:** `rounded-lg` (8px). +- **Background:** `bg-slate-900` (open) / `bg-slate-900/60` (collapsed, in the list-style views). +- **Shadow Strategy:** none at rest; `shadow-lg shadow-black/20` only on the currently-open item. +- **Border:** 1px, `border-slate-800` at rest, `border-slate-700` when open or hovered. +- **Internal Padding:** `px-3.5 py-3` header, `px-2.5 py-2` per field row. + +### Inputs / Fields (correction input) +- **Style:** `rounded-md`, `border-slate-700`, `bg-slate-800`, `text-slate-100`. +- **Focus:** border shifts to `border-slate-500`; no glow/ring — consistent with the flat, restrained component philosophy. +- **Placeholder:** `text-slate-500`, always "correct value" — never blank, so an empty input still communicates its purpose. + +### Confidence Band Chip (signature component) +The one component that is allowed to be visually loud. A field's extracted +value renders inside a solid chip colored by its band (`confidence-green` / +`-amber` / `-red`), dark text for contrast, with a 1px border one shade darker +than the fill. On dark surfaces, `unscored`/`blank` never uses the literal +white token — see the Literal-White Trap rule under Colors. + +### Navigation (document-open header) +- Back control (`← All documents`, bordered, neutral) plus the current document's name and template, band legend right-aligned on wide viewports. +- No persistent sidebar or tab bar — this is a single-task, single-document focus model, not a multi-section app shell. + +## Do's and Don'ts + +### Do: +- **Do** treat the confidence band as the only color allowed to demand attention on a review screen. +- **Do** render `unscored`/`blank` fields as a dashed muted chip on dark surfaces, never as literal white. +- **Do** use shadow only as a state signal (open/active/physical), never as ambient decoration. +- **Do** keep every button, pill, and input restrained — one clear affordance, minimal ornament. +- **Do** animate `transform`/`opacity` for progress and state changes, not `width`/`height` — a `width` transition re-triggers layout on every frame, a `transform: scaleX()` does not. + +### Don't: +- **Don't** introduce a second loud accent hue anywhere near the confidence bands — it competes with the one signal the product depends on. +- **Don't** add ambient drop shadows to cards or panels at rest; depth is earned by state, not applied by default. +- **Don't** rely on a single font family across every size and weight without also varying size deliberately — see the Flat Hierarchy Problem: the current 11/12/16px scale is undersized for a system this data-dense and should widen (e.g., toward a 11/13/16/20px, ~1.8:1 progression) the next time typography is revisited. diff --git a/rpa-dashboard-ai/templates/reference/web/api.ts b/rpa-dashboard-ai/templates/reference/web/api.ts new file mode 100644 index 0000000..737a20c --- /dev/null +++ b/rpa-dashboard-ai/templates/reference/web/api.ts @@ -0,0 +1,364 @@ +import type { + AuditEntry, Batch, BatchStage, DemographyRecord, DemographyRun, InsuranceMasterRow, LastRun, + ManagedUser, + Project, ResultRow, ReviewProgress, ReviewRow, RoiMonth, RoiRun, RoiTotals, Run, +} from './types' + +/** + * Every call goes to a same-origin /api path. In development Vite proxies that + * to the FastAPI port, in production both are served from one origin — so no + * base URL is configured anywhere and there is nothing to get wrong per + * environment. + */ +/** + * FastAPI's `detail` in readable form. + * + * Most of this API refuses with a plain sentence, and for those this is the + * identity function. A 422 is the exception: validation errors arrive as an + * array of `{loc, msg}` objects, and handing that to `new Error()` produced + * the literal string "[object Object]" — which is how a missing request field + * spent a while looking like an unexplained failure. + */ +function errorText(detail: unknown): string | null { + if (typeof detail === 'string') return detail + if (Array.isArray(detail)) { + const parts = detail + .map((d) => { + if (typeof d === 'string') return d + const msg = (d as { msg?: string })?.msg + // `loc` is ["body", "role"]; the last hop is the field, and naming it + // is the difference between "field required" and knowing which one. + const loc = (d as { loc?: unknown[] })?.loc + const field = Array.isArray(loc) ? String(loc[loc.length - 1]) : null + return msg ? (field ? `${field}: ${msg}` : msg) : null + }) + .filter(Boolean) + return parts.length ? parts.join('; ') : null + } + if (detail && typeof detail === 'object') { + const msg = (detail as { msg?: string; message?: string }) + return msg.msg ?? msg.message ?? null + } + return null +} + +// The review screen's two reads give up after this long. fetch has no timeout +// of its own, and a response whose body stalls never rejects, so a stalled read +// left "Loading review queue…" up forever — react-query never got the failure +// its retry (QueryClient retry: 1) needs. Scoped to those two reads on purpose: +// they are a handful of queries each, while /api/batches and /api/roi scale with +// the data and could legitimately pass 20s, where a cap would turn "slow" into a +// false empty board. Writes are never capped — retrying a write is not safe. +const REVIEW_READ_TIMEOUT_MS = 20_000 +const reviewRead = () => ({ signal: AbortSignal.timeout(REVIEW_READ_TIMEOUT_MS) }) + +async function req(path: string, init?: RequestInit): Promise { + const res = await fetch(path, { + ...init, + // The session lives in a cookie, and a cross-origin dev request drops + // cookies unless asked to send them. Without this every call is anonymous + // and the whole app 401s behind the Vite proxy. + credentials: 'include', + headers: init?.body instanceof FormData + ? init.headers + : { 'Content-Type': 'application/json', ...init?.headers }, + }) + if (!res.ok) { + // The API says WHY it refused — an approve blocked by unverified fields + // returns 409 with the count. Surfacing that beats a generic failure. + const body = await res.json().catch(() => null) + throw new Error(errorText(body?.detail) ?? `${res.status} ${res.statusText}`) + } + return res.json() as Promise +} + +export const api = { + // ── batches ─────────────────────────────────────────────────────────────── + /** + * Upload a stack of files as ONE batch. Nothing starts extracting yet — the + * batch has its own Run button, so a reviewer who just dropped twenty files + * can look at the list before spending twenty documents' worth of quota. + */ + createBatch: (files: File[], model = 'gemini-3.5-flash', project = 'ecw') => { + const body = new FormData() + for (const f of files) body.append('files', f) + // `project` is which board received the drop. Nothing has detected a + // template yet, and the board a batch appears on is derived from that, so + // without it a Gulf Coast upload surfaces under ECW and reads as a failed + // upload on the Tebra board. + return req(`/api/batches?${new URLSearchParams({ model, project })}`, { + method: 'POST', + body, + }) + }, + + /** Gather loose documents into a new batch — the only route by which a + * document not uploaded as part of a batch can reach the automation board. */ + batchFromRuns: (jobIds: string[], stage: BatchStage = 'automation') => + req('/api/batches/from-runs', { + method: 'POST', + body: JSON.stringify({ job_ids: jobIds, stage }), + }), + + listBatches: () => req<{ batches: Batch[] }>('/api/batches'), + + getBatch: (batchId: string) => req(`/api/batches/${batchId}`), + + runBatch: (batchId: string, model?: string) => + req(`/api/batches/${batchId}/run`, { + method: 'POST', + body: JSON.stringify({ model: model ?? null }), + }), + + /** Tick or untick a batch as cleared to run. Admin and above. */ + setRunApproval: (batchId: string, approved: boolean) => + req(`/api/batches/${batchId}/run-approval`, { + method: 'PUT', + body: JSON.stringify({ approved }), + }), + + /** Every ticket's latest result, in reading order: the Download summary's + * rows (ADR 0003). */ + batchResults: (batchId: string) => + // counts: the header's three groups, one per encounter, as the Excel writes them. + req<{ rows: ResultRow[]; counts: Record; last_run: LastRun | null }>( + `/api/batches/${batchId}/results`, + ), + + /** Sign off every field of every document in the batch and clear it to + * run, so it can be posted without opening each one. */ + approveAll: (batchId: string) => + req(`/api/batches/${batchId}/approve-all`, { method: 'POST' }), + + setBatchStage: (batchId: string, stage: BatchStage) => + req(`/api/batches/${batchId}/stage`, { + method: 'POST', + body: JSON.stringify({ stage }), + }), + + /** 'precheck' reads every patient out of Tebra and posts nothing; + * 'post' creates the encounters. Defaults to the harmless one. */ + runAutomation: ( + batchId: string, + mode: 'precheck' | 'post' = 'precheck', + ticket?: string, + allowDuplicate = false, + ) => + req( + `/api/batches/${batchId}/run-automation`, + { + method: 'POST', + body: JSON.stringify({ mode, ticket, allow_duplicate: allowDuplicate }), + }, + ), + + // ── runs ────────────────────────────────────────────────────────────────── + startRun: (file: File, model = 'gemini-3.5-flash', thinking = false) => { + const body = new FormData() + body.append('file', file) + const q = new URLSearchParams({ model, thinking: String(thinking) }) + return req<{ job_id: string; status: string }>(`/api/runs?${q}`, { + method: 'POST', + body, + }) + }, + + listRuns: () => req<{ runs: Run[] }>('/api/runs'), + + rerunMissingPages: (jobId: string) => + req<{ job_id: string; status: string }>(`/api/runs/${jobId}/missing-pages`, { + method: 'POST', + }), + retryRun: (jobId: string, model?: string) => + req<{ job_id: string; status: string; model: string }>( + `/api/runs/${jobId}/retry`, + { method: 'POST', body: JSON.stringify({ model: model ?? null }) }, + ), + + getRun: (jobId: string) => req(`/api/runs/${jobId}`), + + deleteRun: (jobId: string) => + req<{ job_id: string; deleted: boolean }>( + `/api/runs/${jobId}`, + { method: 'DELETE' }, + ), + + getAllFields: (jobId: string) => + req<{ rows: ReviewRow[] }>(`/api/runs/${jobId}/all-fields`, reviewRead()), + + getReview: (jobId: string, opts?: { band?: string; verified?: boolean }) => { + const q = new URLSearchParams() + if (opts?.band) q.set('band', opts.band) + if (opts?.verified !== undefined) q.set('verified', String(opts.verified)) + const qs = q.toString() + return req<{ rows: ReviewRow[]; progress: ReviewProgress }>( + `/api/runs/${jobId}/review${qs ? `?${qs}` : ''}`, + reviewRead(), + ) + }, + + patchRow: (rowId: number, body: { + corrected_value?: string | null + verified: boolean + actor?: string + }) => + req(`/api/review/${rowId}`, { + method: 'PATCH', + body: JSON.stringify(body), + }), + + /** + * Correct any field, including one that was never flagged. + * + * Green means the print was legible, not that the value is right, so a + * cleanly-read field still has to be overridable. Those fields have no + * review row until somebody edits one, so they are addressed by the cell + * they occupy rather than by row_id. + */ + editField: (jobId: string, body: { + page: number | null + patient: string | null + field: string + value: string | null + band: string + score: number | null + corrected_value: string | null + verified: boolean + actor?: string + line_index?: number + }) => + req(`/api/runs/${jobId}/fields`, { + method: 'POST', + body: JSON.stringify(body), + }), + + pdfInfo: (jobId: string) => + req<{ pages: number; name: string }>(`/api/runs/${jobId}/pdf-info`), + + pageImage: (jobId: string, page: number, dpi: number) => + `/api/runs/${jobId}/page/${page}?dpi=${dpi}`, + + /** Sign off every remaining flagged field as extracted, in one deliberate + * decision. Satisfies the approve gate rather than bypassing it, and is + * audited as `bulk_verified` so it stays distinguishable afterwards. */ + verifyRemaining: (jobId: string, actor?: string) => + req<{ verified: number; progress: ReviewProgress }>( + `/api/runs/${jobId}/verify-remaining`, + { method: 'POST', body: JSON.stringify({ actor }) }, + ), + + approve: (jobId: string, actor?: string) => + req<{ ok: boolean; approved: number }>(`/api/runs/${jobId}/approve`, { + method: 'POST', + body: JSON.stringify({ actor }), + }), + + /** The cost ledger. Measured figures only — the saving is the client's own + * arithmetic over its own editable rate, and is never returned by the API. */ + roi: () => req<{ runs: RoiRun[]; totals: RoiTotals; monthly: RoiMonth[] }>('/api/roi'), + /** The same shape for demography runs: one total per run, no field figures. */ + demographyRoi: () => + req<{ runs: RoiRun[]; totals: RoiTotals; monthly: RoiMonth[] }>('/api/demography/roi'), + + // ── people and access ───────────────────────────────────────────────────── + /** `can_grant` is what the SERVER says this caller may grant. The client + * renders exactly those options rather than re-deriving the hierarchy. + * `project` names which screen is asking; a scoped admin's own project + * always wins server-side regardless of what is passed here. */ + /** `'all'` asks for the merged, both-projects list — the server only + * honours that for a super_admin or an admin of BOTH projects; anyone + * else gets their own single project's list regardless of what is sent + * here, so passing 'all' cannot widen a scoped admin's view. */ + listUsers: (project: Project | 'all') => + req<{ users: ManagedUser[]; can_grant: string[] }>( + `/api/users?project=${encodeURIComponent(project)}`, + ), + + /** Staff directory autocomplete. Returns [] rather than failing when the + * directory is unreachable, so the field stays usable by hand. */ + searchEmployees: (q: string) => + req<{ employees: { username: string; real_name: string | null }[] }>( + `/api/users/search?q=${encodeURIComponent(q)}`, + ), + + /** `project` is required by the server when `role` is 'admin', and ignored + * otherwise — passed unconditionally here so the caller never has to know + * which roles care. */ + /** `realName` is set only for a brand-new grant made through the directory + * picker; the server only ever uses it to fill a blank name, never to + * overwrite one it already has. */ + assignRole: (username: string, role: string, project?: Project, + realName?: string | null) => + req(`/api/users/${encodeURIComponent(username)}/role`, { + method: 'PUT', + body: JSON.stringify({ role, project, real_name: realName }), + }), + + /** Removes admin standing in ONE project only. A person still admin of the + * other project keeps that access — it does not stop them signing in + * either way; they return as an operator (of this project, at least). */ + removeAccess: (username: string, project: Project) => + req<{ ok: boolean; removed: string }>( + `/api/users/${encodeURIComponent(username)}?project=${encodeURIComponent(project)}`, + { method: 'DELETE' }, + ), + + audit: (jobId?: string) => + req<{ entries: AuditEntry[] }>( + `/api/audit${jobId ? `?job_id=${jobId}` : ''}`, + ), + + // ── demography ──────────────────────────────────────────────────────────── + /** Upload one facesheet. Unlike a batch, this starts immediately: OCR spends + * CPU rather than API quota, so there is nothing for an approval to guard. */ + startDemographyRun: (file: File) => { + const body = new FormData() + body.append('file', file) + return req<{ job_id: string; status: string }>('/api/demography/runs', { + method: 'POST', + body, + }) + }, + + /** `stage` omitted lists every run; passed, it filters server-side — + * a NULL stage counts as 'extraction' there, same as an unmoved batch. */ + listDemographyRuns: (stage?: 'extraction' | 'automation') => + req<{ runs: DemographyRun[] }>( + stage ? `/api/demography/runs?stage=${stage}` : '/api/demography/runs', + ), + + patchDemographyField: (jobId: string, rowId: number, correctedValue: string | null) => + req<{ row_id: number; corrected_value: string | null }>( + `/api/demography/runs/${jobId}/fields/${rowId}`, + { method: 'PATCH', body: JSON.stringify({ corrected_value: correctedValue }) }, + ), + + setDemographyStage: (jobId: string, stage: 'extraction' | 'automation') => + req(`/api/demography/runs/${jobId}/stage`, { + method: 'POST', + body: JSON.stringify({ stage }), + }), + + demographyRecords: (jobId: string) => + req<{ run: DemographyRun; records: DemographyRecord[] }>( + `/api/demography/runs/${jobId}/records`, + ), + + /** Every Insurance Master row, for the insurance finder. */ + insuranceMaster: () => + req<{ insurances: InsuranceMasterRow[] }>('/api/demography/insurance-master'), + + deleteDemographyRun: (jobId: string) => + req<{ job_id: string; deleted: boolean }>( + `/api/demography/runs/${jobId}`, + { method: 'DELETE' }, + ), + + /** A URL string, not a promise — used as an so the browser's own + * download handling takes over, the same way `pageImage` is used as a src. */ + demographyExportUrl: (jobId: string) => + `/api/demography/runs/${jobId}/export.xlsx`, + + /** The uploaded facesheet, shown in the browser's own PDF viewer. */ + demographyPdfUrl: (jobId: string) => `/api/demography/runs/${jobId}/pdf`, +} diff --git a/rpa-dashboard-ai/templates/reference/web/bands.ts b/rpa-dashboard-ai/templates/reference/web/bands.ts new file mode 100644 index 0000000..0208684 --- /dev/null +++ b/rpa-dashboard-ai/templates/reference/web/bands.ts @@ -0,0 +1,74 @@ +/** + * The confidence bands, and the exact colours the workbook paints. + * + * These hexes are not a design choice made here — they are the same values + * patient_extractor_v2.py fills cells with. A reviewer moves between the sheet + * and this dashboard looking at the same field; if the two disagreed about what + * red looks like, the colour would stop being a shared language. + * + * GREEN B6E2B6 green channel high + * AMBER F4E2B6 red + green high <- sits visually between the other two + * RED F4B6B6 red channel high + */ +export const BAND_COLOR = { + green: '#B6E2B6', + amber: '#F4E2B6', + red: '#F4B6B6', + blank: '#FFFFFF', + unscored: '#FFFFFF', +} as const + +/** Border, so a white "blank" cell is still visibly a cell. */ +export const BAND_BORDER = { + green: '#9AC79A', + amber: '#D9C79A', + red: '#D99A9A', + blank: '#D0D0D0', + unscored: '#D0D0D0', +} as const + +export const BAND_LABEL = { + green: 'Clean print', + amber: 'Slightly unclear', + red: 'Handwritten / messy', + blank: 'Billing code missing', + unscored: 'Not found in transcription', +} as const + +/** + * What the colour does NOT say. Kept next to the palette on purpose: the score + * rates how legibly the text was printed, not whether the value is right. A + * confident misread comes back green — measured on this document set, "J27.20" + * scored 0.9 while the correct "I27.20" sat on the same page. + */ +export const BAND_CAVEAT = + 'Colour reflects print clarity, not correctness. A cleanly printed value can still be misread.' + +export type Band = keyof typeof BAND_COLOR + +/** + * The band as a THEME TOKEN, for anything drawn in the interface rather than on + * the scan: status pills, the 3px grade rule down a field input, legend + * swatches, donut segments, quality meters. + * + * These reference the CSS variables rather than carrying hexes, so a band + * follows the appearance the same way every other surface does, and so the + * grading triple is declared in exactly one place (index.css). + * + * `blank` and `unscored` collapse to the same grey: nothing was read, so there + * is no print clarity to grade, and inventing a fourth hue for "no data" would + * put a colour on the legend that means nothing a reviewer can act on. + * + * BAND_COLOR / BAND_BORDER above are NOT replaced by this and are still the + * right choice in one place: the highlight chips drawn over the scanned page + * itself. Those sit on the document, mirror what the workbook fills, and are + * the shared language between the sheet and this dashboard — interface tokens + * would break that and would be unreadable on a white scan besides. + */ +export const BAND_VAR = { + green: 'var(--clean)', + amber: 'var(--verify)', + red: 'var(--review)', + blank: 'var(--absent)', + unscored: 'var(--absent)', +} as const diff --git a/rpa-dashboard-ai/templates/reference/web/components/BrandHeader.tsx b/rpa-dashboard-ai/templates/reference/web/components/BrandHeader.tsx new file mode 100644 index 0000000..a8459e6 --- /dev/null +++ b/rpa-dashboard-ai/templates/reference/web/components/BrandHeader.tsx @@ -0,0 +1,125 @@ +import { ThemeSwitcher } from '../theme' +import { ROLE_LABEL } from '../auth' + +/** + * The app shell's top bar: brand mark, title, and the signed-in user's + * controls. The board tab strip that used to live below it is now the left + * sidebar (`Sidebar.tsx`) — this component no longer owns navigation. + * + * The gradient is the one saturated surface in the product. Everything on it + * is white or a white alpha rather than a palette token, because a + * token-coloured control on the band reads as a hole punched through it — + * see `.sb-appbar` and `.sb-ghost-btn` in index.css. + */ +/** 'admin' is never a single global role any more — computed here from the + * two per-project flags so the header names exactly what this account can + * reach, rather than falling back to "Operator" for someone who is in fact + * an admin of one or both projects. */ +function roleLabel(user: { + role: string + ecw_role?: string | null + tebra_role?: string | null +}): string { + if (user.role === 'super_admin') return ROLE_LABEL.super_admin + const ecw = user.ecw_role === 'admin' + const tebra = user.tebra_role === 'admin' + if (ecw && tebra) return 'Admin · Both' + if (ecw) return 'Admin · ECW' + if (tebra) return 'Admin · Tebra' + return ROLE_LABEL.operator +} + +export function BrandHeader({ + user, + onLogout, + sidebarOpen, + onToggleSidebar, +}: { + user: { + username: string + real_name: string | null + role: string + ecw_role?: string | null + tebra_role?: string | null + } + onLogout: () => void + /** Present only on screens that have the left sidebar. */ + sidebarOpen?: boolean + onToggleSidebar?: () => void +}) { + return ( +
+ + {onToggleSidebar && ( + + )} + {/* What the tool is, matching the browser tab and the sign-in screen. + The brand and EMR edition — "Superbill Automation for ECW" — live in + the sidebar's own group headers instead (`Sidebar.tsx`), which is + where the rail has room to state them without competing here. */} +
+
Extraction and Automation Tool
+
Extract · correct · post to eClinicalWorks
+
+
+
+ {user.real_name ?? user.username} + {roleLabel(user)} +
+ + +
+
+ ) +} + +/** + * The product mark: shield plus three ascending bars, drawn as vector. + * + * The same artwork the sign-in screen carries, so the two screens read as one + * product — but flat here, with no bob and no glow: on the sign-in screen it + * is the subject, and in a header it is a label for the page. + * + * Vector rather than the `logo-mark.png` raster it replaced. The raster could + * not take the header's drop-shadow without fringing along its antialiased + * edge, and it could not follow a theme. + * + * Colours are literal on purpose. The shield and its bars are artwork — the + * mark is the same mark in light and dark, the way a printed logo is — so + * these four values are deliberately NOT theme tokens. `--shield`, `--clean`, + * `--verify` and `--review` hold the same hexes for anything that needs to + * reference them. + * + * `aria-hidden` because the product is named in text immediately beside it. + */ +export function ShieldMark({ size = 34 }: { size?: number }) { + return ( + + + + ) +} diff --git a/rpa-dashboard-ai/templates/reference/web/components/Donut.tsx b/rpa-dashboard-ai/templates/reference/web/components/Donut.tsx new file mode 100644 index 0000000..51f8e8e --- /dev/null +++ b/rpa-dashboard-ai/templates/reference/web/components/Donut.tsx @@ -0,0 +1,94 @@ +/** + * A proportional ring. + * + * Earns its place only because the quantity it draws is genuinely a share of a + * whole — the split of every graded field across the confidence bands. A ring + * standing in for a single number would be decoration; this one is the only + * shape on the board that shows three parts against their total at once. + * + * It is `aria-hidden`, and that is deliberate rather than lazy: the legend + * beside it states every segment's name and count as text, so a screen reader + * gets the data in full and is not read a list of arc lengths. The chart is the + * fast path for someone looking; the legend is the actual record. + */ + +export interface Slice { + key: string + label: string + value: number + color: string +} + +export function Donut({ + slices, + centerValue, + centerLabel, + size = 150, + thickness = 20, + /** Arc-length notch between segments, so two adjacent colours stay countable. */ + gap = 3, +}: { + slices: Slice[] + centerValue: string + centerLabel: string + size?: number + thickness?: number + gap?: number +}) { + const r = (size - thickness) / 2 + const c = 2 * Math.PI * r + const total = slices.reduce((n, s) => n + s.value, 0) + + // Running start angle, in arc length. Accumulated rather than derived per + // index so a zero-value slice consumes nothing at all instead of leaving a + // gap where no data is. + let at = 0 + const drawn = slices + .filter((s) => s.value > 0) + .map((s) => { + const len = (s.value / total) * c + const seg = { ...s, len: Math.max(len - gap, 0.5), offset: at } + at += len + return seg + }) + + return ( +
+ + +
+ + {centerValue} + + + {centerLabel} + +
+
+ ) +} diff --git a/rpa-dashboard-ai/templates/reference/web/components/PdfPane.tsx b/rpa-dashboard-ai/templates/reference/web/components/PdfPane.tsx new file mode 100644 index 0000000..ea6373e --- /dev/null +++ b/rpa-dashboard-ai/templates/reference/web/components/PdfPane.tsx @@ -0,0 +1,536 @@ +import { useCallback, useEffect, useRef, useState } from 'react' +import { useQuery } from '@tanstack/react-query' +import { api } from '../api' +import { BAND_VAR } from '../bands' +import type { ReviewActions } from './ReviewPane' + +/** + * The source page, as pixels the page owns. + * + * This used to be the browser's own PDF plugin in an