Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
df0fe42
Add DebateOwl tokens, Fredoka and Nunito, light by default
Programcode Oct 24, 2026
ab96c87
Add the owl: pose cutouts, app mark and icons
Programcode Oct 24, 2026
28b1c92
Restyle shared components to the DebateOwl style guide
Programcode Oct 24, 2026
8b15cb4
Put button labels in sentence case
Programcode Oct 24, 2026
8b67a64
Owl loaders, Ask coach and Sparring opponent
Programcode Oct 24, 2026
6fa5b95
Restyle the landing page, Home and Library
Programcode Oct 24, 2026
f0874ea
Owl on the 404 and error pages; busy tool buttons keep their face
Programcode Oct 24, 2026
27eddf9
Owl on the Sparring verdict, onboarding tour in the new style
Programcode Oct 24, 2026
a4d502a
Success is never colour alone; amber text passes AA
Programcode Oct 24, 2026
433af86
Document the DebateOwl tokens, fonts and owl component
Programcode Oct 24, 2026
1482c73
Use Nunito and Fredoka in the embeddable timer too
Programcode Oct 24, 2026
51a0f01
Timers in JetBrains Mono; offline check follows the owl logo
Programcode Oct 24, 2026
5afdcf0
Reduced motion stops new entrances; owls work offline; guide pages re…
Programcode Oct 24, 2026
0990051
Recipes that compare themes switch the account to System first
Programcode Oct 24, 2026
c9cf5dd
Bold in answers stands out from Nunito body text; toast close button …
Programcode Oct 24, 2026
ec6c1a0
Before screenshots of the Ask and Generate menus
Programcode Oct 24, 2026
03e3f43
Ask Options join the mode row; Generate actions follow their purpose
Programcode Oct 24, 2026
56097ef
After screenshots of the Ask and Generate menus
Programcode Oct 24, 2026
aa92de4
Merge pull request #229 from Programcode/design/menu-layout-cleanup
Programcode Oct 24, 2026
41f5cb3
Generate toolbar rows split evenly on tablets and desktop
Programcode Oct 24, 2026
67628a8
Owls are traced SVGs: six new poses, WebP cutouts gone
Programcode Oct 24, 2026
42cb530
One owl on each key screen
Programcode Oct 24, 2026
e66cf0b
Spanish coach alt text says coach, as the Ask title does
Programcode Oct 24, 2026
613b2c7
Merge pull request #232 from Programcode/design/debateowl-owls
Programcode Oct 24, 2026
5649e10
Before and after screenshots of the Generate toolbar rows
Programcode Oct 24, 2026
894e6d1
Merge pull request #231 from Programcode/design/generate-toolbar-even…
Programcode Oct 24, 2026
748f3c9
One-time DebateOwl welcome on the first app open
Programcode Oct 26, 2026
715b2e5
Merge remote-tracking branch 'origin/main' into design/debateowl-restyle
Programcode Oct 26, 2026
5e5bc44
Welcome and rename notice in one dialog with one dismiss
Programcode Oct 26, 2026
b2fd202
Merge remote-tracking branch 'origin/design/debateowl-restyle' into d…
Programcode Oct 26, 2026
8917c29
Keep the owl nameless until a name is picked
Programcode Oct 26, 2026
e21d8ee
The owl is called Ollie
Programcode Oct 26, 2026
4778c66
Service worker offline test marks the welcome seen
Programcode Oct 26, 2026
c1fe62f
Ollie's greeting wraps evenly on phones
Programcode Oct 26, 2026
e5c6efc
Merge pull request #237 from Programcode/design/debateowl-intro
Programcode Oct 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .claude/skills/verify-debatelab/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,13 @@ It also prints the commit the stack started at and whether there are uncommitted
- `admin`: a service-role client for reading rows as a second view.
- `fake` / `providerCall(feature)`: what the Edge Function actually sent upstream. Use `fake.fail(feature, status)` to force a provider error and `fake.truncate(feature)` (fake mode only) to cut the next reply off at its length cap. Each call records the `maxTokens` it asked for.
- `aiResponse(page, feature, operation?)`: the parsed body of the next `ai` Edge Function response. Pages ask for live generation, so most `ai` responses are event streams; `aiResponse` and `readAIResponse(response)` read the final `result` event (or a plain JSON body) for you, so never call `response.json()` on an `ai` response. Assert that its fields reach the screen, never on model wording, so a recipe passes in both modes. `LIVE` and `AI_TIMEOUT` (30 s fake, 240 s live) let a recipe branch where a check only makes sense live, and a fake-mode branch adds an `unverified` annotation instead of passing silently.
- `followSystemTheme(page)`: new accounts start on the light theme. A recipe that compares light and dark calls this first; it picks System in Settings, so `page.emulateMedia({ colorScheme })` then drives the theme.
- `expectStreamed([...])`: every provider call for those features asked the model to stream, which proves the page showed the answer live.
- `expectProviderCalls([...])`: what reached the provider. Each test writes `provider-calls.json`, and in live mode the fixture fails the test if any chat call was answered by a model other than the pin.
- `evidence`: `step(line)`, `aria(name, locator?)`, `shot(name)` and `json(name, value)`.
- Automatic failure on any uncaught page error, horizontal overflow, or remote Supabase request.

Every result, source and practice round belongs to a Current Debate or is explicitly one-off. A tool given a motion when no debate exists opens `Start a debate with this motion?`; a motion that differs from the Current Debate's opens `This is a different motion`. Both open after the submit click and before the AI request, so create the `aiResponse()` promise, click, answer with `chooseForMotion`, then await. Or call `startDebate` first, after which tools prefill its motion and open no dialog. The Library's `Add Source` is disabled until a debate exists. See `features/current-debate.md`.
Every result, source and practice round belongs to a Current Debate or is explicitly one-off. A tool given a motion when no debate exists opens `Start a debate with this motion?`; a motion that differs from the Current Debate's opens `This is a different motion`. Both open after the submit click and before the AI request, so create the `aiResponse()` promise, click, answer with `chooseForMotion`, then await. Or call `startDebate` first, after which tools prefill its motion and open no dialog. The Library's `Add source` is disabled until a debate exists. See `features/current-debate.md`.

```bash
$H drive all # every recipe, both viewports
Expand All @@ -105,9 +106,9 @@ Fake mode also runs `e2e/full-stack/`. Live mode runs the recipes, `e2e/live/`,

To prove a feature without a recipe, copy `recipes/case-generation.spec.ts` to `recipes/<feature>.spec.ts` and use the selectors from the matching file in `features/`. Recipes are maintained files. Keep the ones worth keeping.

**Interactive.** Run `$H user` to mint a code. It prints `DEMO_CODE=...`, and `down` deletes that user. Then point a browser tool (Playwright MCP `browser_navigate` etc.) at `http://127.0.0.1:18081/login`, choose the `Demo code` tab, fill the `Demo code` textbox, and click `Sign in with demo code`. Save your own screenshots into `.verify/artifacts/<run-id>/`. A new account shows the onboarding tour on `/app`. Its first step offers `Start debate`; to skip the tour, click `Skip Tour` (`[data-test-id="button-skip"]`). Then start a debate from the header (`Start debate`, test id `current-debate-desktop` or `current-debate-mobile`) or answer the motion dialog the first tool opens: `Start debate` files the work in a new debate, `Not now, use it once` keeps it one-off. Interactive runs have no remote-Supabase guard. Doctor's local-mode check is what protects you.
**Interactive.** Run `$H user` to mint a code. It prints `DEMO_CODE=...`, and `down` deletes that user. Then point a browser tool (Playwright MCP `browser_navigate` etc.) at `http://127.0.0.1:18081/login`, choose the `Demo code` tab, fill the `Demo code` textbox, and click `Sign in with demo code`. Save your own screenshots into `.verify/artifacts/<run-id>/`. The one-time DebateOwl welcome never opens on this stack (`VITE_LOCAL_PREVIEW`) or under Playwright (`VITE_E2E`); open `/app?intro=show` to see it, and close it with `Got it` or Esc. A new account shows the onboarding tour on `/app`. Its first step offers `Start debate`; to skip the tour, click `Skip tour` (`[data-test-id="button-skip"]`). Then start a debate from the header (`Start debate`, test id `current-debate-desktop` or `current-debate-mobile`) or answer the motion dialog the first tool opens: `Start debate` files the work in a new debate, `Not now, use it once` keeps it one-off. Interactive runs have no remote-Supabase guard. Doctor's local-mode check is what protects you.

Selectors: use accessible names and roles as the feature files list them (`getByRole("button", { name: "Generate Debate Case" })`). The exceptions use `data-testid`: Crossfire's controls, the Current Debate control, strip and dashboard, and the voice controls (`voice-mode`, `voice-mic`). Use `exact: true` for short names such as `Start` or `Save`, because the header's `Start debate` also matches `Start`. Never use coordinates.
Selectors: use accessible names and roles as the feature files list them (`getByRole("button", { name: "Generate debate case" })`). The exceptions use `data-testid`: Crossfire's controls, the Current Debate control, strip and dashboard, and the voice controls (`voice-mode`, `voice-mic`). Use `exact: true` for short names such as `Start` or `Save`, because the header's `Start debate` also matches `Start`. Never use coordinates.

## Agentic tests and exploration

Expand Down
4 changes: 2 additions & 2 deletions .claude/skills/verify-debatelab/features/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ This directory is the maintained source for verifying DebateLab's user-facing be
- `bin/verify.sh up` has printed `READY http://127.0.0.1:18081`, and `bin/verify.sh doctor` exits 0.
- The browser client is the `up` instance at `http://127.0.0.1:18081`. Never use the `debatelab` container on `:8080` or a plain `npm run dev`, because both reach production.
- Each scripted test signs in with its own fresh demo-code user (a Pro account with a daily limit of 999) and deletes it afterwards. The fixture marks the onboarding tour as seen, and the user starts with no Current Debate.
- Most tools work inside a Current Debate (see [current-debate](./current-debate.md)). Start one with `startDebate(page, motion)` first, or answer the motion dialog that opens after submit with `chooseForMotion(page, "create" | "once")`. The Library's `Add Source` stays disabled until a debate exists.
- Most tools work inside a Current Debate (see [current-debate](./current-debate.md)). Start one with `startDebate(page, motion)` first, or answer the motion dialog that opens after submit with `chooseForMotion(page, "create" | "once")`. The Library's `Add source` stays disabled until a debate exists.
- In fake mode, model answers come from the fake provider and contain the marker `FAKE-PROVIDER`. In live mode (`up --live`), every chat call goes to real OpenRouter pinned to `anthropic/claude-haiku-5.5` on the Anthropic BYOK key, research uses real Exa, and answers are real model text.
- The feature files quote fake-mode text such as `FAKE-PROVIDER case thesis`. Recipes never assert it: they read the `ai` response body with `aiResponse()` and check that its fields are on screen, so the same recipe passes in both modes.

Expand Down Expand Up @@ -49,7 +49,7 @@ Each feature file starts with an H1 title and one paragraph describing the user-
- [Practice](./practice.md): the Sparring setup's whole-round cost and Start's pending state, a full Lincoln-Douglas sparring round with verdict (and its Hungarian chart labels), and Crossfire in both directions with debrief, each filed in its debate (`recipes/practice.spec.ts`).
- [Voice rounds](./voice-rounds.md): spoken Sparring and Crossfire on the browser voice, with no ElevenLabs key (`recipes/voice-rounds.spec.ts`).
- [Workspace autosave](./workspace-autosave.md): a slow Crossfire or Sparring save from a Workspace never overwrites newer text (`recipes/workspace-autosave.spec.ts`). Needs `up --collab`; skips without it.
- [Account data](./account-data.md): Download my data and Delete account, each proven against a bystander (`recipes/account-data.spec.ts`); Delete All My Data scoped against a bystander (`e2e/full-stack/account-data.spec.ts`).
- [Account data](./account-data.md): Download my data and Delete account, each proven against a bystander (`recipes/account-data.spec.ts`); Delete all my data scoped against a bystander (`e2e/full-stack/account-data.spec.ts`).
- Account: output language reaching the model from a new device, flowchart node delete (at once, with an `Undo` toast; no confirm dialog), save and load, and the speech timer (`R` resets only while paused) (`recipes/account.spec.ts`).
- [Usage limits](./usage-limits.md): Opus refused when its weight does not fit today while Haiku still runs, and the weekly AI budget dialog (`recipes/usage-limits.spec.ts`).
- Billing: expired account blocked, promo code restores access, and a discount code claimed once (a second Apply does not count again) stays in the pricing dialog and is sent with the upgrade (`recipes/billing.spec.ts`); each billing state (trial, extended access, active Pro, Pro set to cancel, promo Pro without Stripe, payment failed, subscription ended) shown the same way by Settings, the pricing dialog (its one plan button and Current badge) and a refused AI request, in English and Hungarian, light and dark, at 360 and 375 px (mobile project) and 768 and 1280 px (desktop project); the usage line on every tool page matching Settings and updating after a run; the trial banner; and `/app/billing` landing on the Settings subscription card (`recipes/billing-states.spec.ts`). The pricing dialog's Manage path stubs `create-portal` with its own no-subscription answer, since the portal needs Stripe; signed, forged, and replayed Stripe webhooks, including a failed renewal that keeps Pro (`e2e/full-stack/billing.spec.ts`).
Expand Down
6 changes: 3 additions & 3 deletions .claude/skills/verify-debatelab/features/account-data.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
# Account data: export and deletion

From Settings, a signed-in user can download everything saved to the account as one JSON file, or delete the account permanently. Both go through the `account` Edge Function, which acts only on the caller's own session and ignores any user id in the request. Deletion first checks Stripe for the user's customers and cancels any subscription, then removes every row that references the user. `Delete All My Data` is the older, narrower erase: it keeps the account and removes its saved work.
From Settings, a signed-in user can download everything saved to the account as one JSON file, or delete the account permanently. Both go through the `account` Edge Function, which acts only on the caller's own session and ignores any user id in the request. Deletion first checks Stripe for the user's customers and cancels any subscription, then removes every row that references the user. `Delete all my data` is the older, narrower erase: it keeps the account and removes its saved work.

## Sub-features

- `acct-export`: `Download my data` saves `debatelab-data-YYYY-MM-DD.json` with `format: "debatelab-account-export"`, the caller's `account` (`id`, `email`), and `data` (debates, sources, and saved results). Another account's rows never appear, even when the request names that account's id.
- `acct-delete`: `Delete account` opens `Delete your account?`. Its confirm button stays disabled until the field labelled `Type your email to confirm` holds the account email. After deletion, the toast reads `Account deleted` and the app returns to `/`. The auth user and every row mentioning its id, in the `public` and `private` schemas, are gone. Nobody else's rows change, and the old session gets `401`.
- `acct-erase`: `Delete All My Data` with the same email confirmation shows `Data Deleted` and empties the account's sources and debates. A bystander's rows stay. This is covered by `e2e/full-stack/account-data.spec.ts`, not by a recipe.
- `acct-erase`: `Delete all my data` with the same email confirmation shows `Data Deleted` and empties the account's sources and debates. A bystander's rows stay. This is covered by `e2e/full-stack/account-data.spec.ts`, not by a recipe.

## How to get to it (user POV)

- Settings: the gear link named `Settings` in the header, or `/app/settings`. `Download my data` is under `Your data` in the Account card. `Delete All My Data` and `Delete account` are in the Danger Zone card.
- Settings: the gear link named `Settings` in the header, or `/app/settings`. `Download my data` is under `Your data` in the Account card. `Delete all my data` and `Delete account` are in the Danger Zone card.

## Driving it with verify.sh + Playwright

Expand Down
4 changes: 2 additions & 2 deletions .claude/skills/verify-debatelab/features/ai-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ Preconditions:

- Doctor passes. The user is signed in.

- **Attack.** On `/app/attack`, fill the field labelled `Your Argument *` and click `Find Weaknesses`. The text `FAKE-PROVIDER weakness` appears. `providerCall("attack").user` contains the argument.
- **Brainstorm.** On `/app/brainstorm`, fill the field labelled `Motion / Topic *` and click `Start Brainstorming`. With no Current Debate, the dialog `Start a debate with this motion?` opens first: `chooseForMotion(page, "create")` (or `"once"`). The text `FAKE-PROVIDER idea 1` appears. `providerCall("brainstorm").user` contains `Motion: <motion>`, and the `brainstorms` row is filed in the new debate. With a Current Debate, the field is prefilled and no dialog opens unless the motion differs.
- **Attack.** On `/app/attack`, fill the field labelled `Your Argument *` and click `Find weaknesses`. The text `FAKE-PROVIDER weakness` appears. `providerCall("attack").user` contains the argument.
- **Brainstorm.** On `/app/brainstorm`, fill the field labelled `Motion / Topic *` and click `Start brainstorming`. With no Current Debate, the dialog `Start a debate with this motion?` opens first: `chooseForMotion(page, "create")` (or `"once"`). The text `FAKE-PROVIDER idea 1` appears. `providerCall("brainstorm").user` contains `Motion: <motion>`, and the `brainstorms` row is filed in the new debate. With a Current Debate, the field is prefilled and no dialog opens unless the motion differs.
- **Factoids.** On `/app/factoids`, fill the field labelled `Statement *` and click `Find Factoids`. The text `FAKE-PROVIDER search suggestion` appears.
- **Ask, streamed.** On `/app/ask`, fill the placeholder `Ask your debate coach...` and click the button named `Send message`. The text `FAKE-PROVIDER streamed answer: ...` appears. `fake.requests()` features contain `ask-stream` and not `ask`. `ai_requests` gets `{feature: "ask", status: "success"}`.
- **Ask, cut off.** Call `await fake.truncate("ask-stream")` and send. The stream ends with `finish_reason: "length"`, its `final` event's output carries `replyTruncated: true`, and the partial answer stays with `This answer hit its length limit and stops early...` under `Uncertainties:`. No `Response was interrupted` toast. Fake mode only.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ Preconditions:

## Gotchas

- A first sign-in on a new account starts the onboarding tour on `/app`. Its first step (`Welcome to DebateLab!`, `Start your first debate here.`) offers `Start debate`. The fixtures pre-seed `debate-trainer-onboarding` to skip it. The tour is tracked per account (store version 1, `completedUserIds`). A version 0 seed with `hasCompletedOnboarding: true` covers only the next account that signs in, so a second account in the same browser sees the tour. Interactive runs click `Skip Tour` (`[data-test-id="button-skip"]`). The tour itself is covered in [current-debate](./current-debate.md).
- A first sign-in on a new account starts the onboarding tour on `/app`. Its first step (`Welcome to DebateLab!`, `Start your first debate here.`) offers `Start debate`. The fixtures pre-seed `debate-trainer-onboarding` to skip it. The tour is tracked per account (store version 1, `completedUserIds`). A version 0 seed with `hasCompletedOnboarding: true` covers only the next account that signs in, so a second account in the same browser sees the tour. Interactive runs click `Skip tour` (`[data-test-id="button-skip"]`). The tour itself is covered in [current-debate](./current-debate.md).
- Sessions are stored under the `debate-auth-local` localStorage key in local-preview mode. Don't reuse stub-session helpers (`e2e/helpers/auth.ts`). They fake auth and prove rendering only.
- Google sign-in is not wired to the local stack. Demo codes, email codes and the email link work here.
- The revoked-session check forces one token refresh before it signs out. An Auth outage (network error, 5xx, 429) keeps the session, and that path is covered by `src/test/auth/session-expiry.test.ts`, not by a recipe.
4 changes: 2 additions & 2 deletions .claude/skills/verify-debatelab/features/case-generation.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Preconditions:
- Doctor passes. The recipe `recipes/case-generation.spec.ts` covers every sub-feature below: `bin/verify.sh drive recipes/case-generation`.

- **Start the debate.** `await startDebate(page, motion)`. On `/app/generate`, the textbox named `Motion / Topic *` already holds the motion.
- **Generate.** Click the button named `Generate Debate Case`. The text `FAKE-PROVIDER case thesis` and `FAKE-PROVIDER contention 3` appear, along with a toast `Generate complete`. Without a Current Debate, fill the motion yourself and answer the dialog `Start a debate with this motion?` with `chooseForMotion`.
- **Generate.** Click the button named `Generate debate case`. The text `FAKE-PROVIDER case thesis` and `FAKE-PROVIDER contention 3` appear, along with a toast `Generate complete`. Without a Current Debate, fill the motion yourself and answer the dialog `Start a debate with this motion?` with `chooseForMotion`.
- **Provider side effect.** `providerCall("generate")`'s `user` contains `Motion: <motion>`. `ai_requests` for the user contains `{feature: "generate", status: "success"}`.
- **Filed in the debate.** `cases` for the user is one row whose `debates(motion)` is the motion. On a fresh browser context, `/app/history` shows it as a heading in the `Cases` tab panel, and `Load` reopens it.
- **Save (one-off).** Generate with no debate and choose `chooseForMotion(page, "once")`. Click the button named `Save` (use `exact`), fill the dialog field labelled `Case title`, and click the dialog's `Save` button. `cases` for the user is `[{title, debate_id: null}]`. On a fresh browser context, `/app/history` shows `One-off and unsorted work` with a list item holding the title.
Expand All @@ -39,7 +39,7 @@ Preconditions:

## Gotchas

- While any generation is running, the form's submit button reads `Generating Case...`, not `Generate Debate Case`. Find it by `[data-tour="generate-button"]` then.
- While any generation is running, the form's submit button reads `Generating case...`, not `Generate debate case`. Find it by `[data-tour="generate-button"]` then.

- The Generate page shows the heading `Ready to Generate` before the first run. Wait for it after navigating, because lazy route chunks load slowly on a cold Vite.
- `Save` appears both on the result toolbar and in the dialog. Scope the second click to `getByRole("dialog")`. The toolbar `Save` exists only for one-off results.
Expand Down
Loading
Loading