Repository navigation
Add /ai-debate with a no-login two-turn mini AI round (draft) - #298
Merged
Merged
Conversation
/ai-debate is a prerendered landing page for "AI debate partner" searches. At the top, a visitor picks a motion and a side, writes an argument, and gets one AI rebuttal with a one-line judge note, then one follow-up. After the second turn the page offers the 3-day trial. Without JavaScript, or while the demo is off, the same spot shows a static sample exchange. The demo-rebuttal Edge Function serves the round. It stays off unless DEMO_ENABLED=true and TURNSTILE_SECRET_KEY is set, accepts only our origins, checks a Turnstile token before a session starts, and reserves each model call through claim_demo_round_turn: 3 sessions per IP per UTC day, 2 turns per session, and DEMO_DAILY_LIMIT calls per day overall (500 by default). It stores a salted per-day IP hash and counts, nothing the visitor typed. Each turn is one Claude Haiku 5.5 call with max_tokens 450. The client shows the live form only when the build sets VITE_DEMO_ENABLED and VITE_TURNSTILE_SITE_KEY. The CSP allows challenges.cloudflare.com for the Turnstile script and frame. The page has WebApplication, FAQPage and BreadcrumbList data and its own share image, and is linked from the hero (English), the footer, the home page's Practice group and llms.txt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A follow-up from a different IP (a phone moving between networks) cannot continue its session. It used to show the generic "unavailable" message under a reply box that could never succeed; it now says why and ends the round. The e2e also checks that the FAQ heading renders. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: true
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. |
|
This PR changes |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Task 1 from the 2026-11-02 organic spec. Draft: do not merge. Adam approved a draft PR only. Nothing here is deployed: no migration applied, no Edge Function deployed, no env vars set on Supabase or Vercel.
What it adds
/ai-debateis a prerendered landing page for "AI debate partner" searches. It uses the spec's title, meta description, H1 and first sentence.At the top is a mini round. The visitor picks a motion (5 presets or their own, up to 200 characters) and a side, then writes an argument (up to 600 characters). They get one AI rebuttal (capped at 150 words) and a one-line judge note naming the weakest link in their argument, then one follow-up reply. After the second turn the page shows "Run a full timed round: start your 3-day trial", linking to
/login, with "Then $5/month + tax. Cancel anytime." under it.Without JavaScript, or while the demo is off, the same spot shows a static sample exchange and the trial button.
Below it: how it works, a worked example on an invented motion about an invented town, what the full app adds (Sparring, Crossfire, Case generator, Case attacker), the formats Sparring supports (School 4-6-6-3, Lincoln-Douglas, Public Forum, checked in
packages/core/src/sparring/formats.ts), a paragraph on general chatbots linking to/best-ai-for-debate, and the 5-question FAQ. The FAQ says the demo is free and the app is a 3-day trial, then $5/month. It never calls the demo a free plan.Schema: WebApplication (EducationalApplication; Offer at 5 USD, with the trial in the offer description), WebPage, FAQPage and BreadcrumbList. The page has its own 1200x630 share image,
public/og/ai-debate.png.scripts/prerender.mjsnow swapsog:imageandtwitter:imagefor any page whose head names an image.Links to it: a secondary "Try an AI debate" button in the hero on the English home page only (
/huand/eskeep one button, since this page is English), a footer link, the first chip in the home page's "Practice with AI" group, andllms.txt. The sitemap picks the page up fromdist/. The page has 24 internal inlinks.Server side: the
demo-rebuttalEdge FunctionThe spec asked for
POST /api/demo/rebuttal. The site is a static Vite build on Vercel with no API routes, and every server call already goes through Supabase Edge Functions, so this uses an Edge Function instead.Limits
DEMO_ENABLEDturns the function on only when it is exactlytrue. The default is off.VITE_DEMO_ENABLED=trueandVITE_TURNSTILE_SITE_KEY. The default is off, and the page shows the sample exchange.TURNSTILE_SECRET_KEYthe function stays off._shared/cors.tsget through; anything else gets 403.DEMO_DAILY_LIMITmodel calls per UTC day across all visitors. The default is 500.DEFAULT_MODEL(Claude Haiku 5.5), routed throughopenRouterProviderOptionsso it stays on the Anthropic BYOK key, withmax_tokens450, no reasoning and no retries.The per-IP, per-session and global checks happen in one transaction in
claim_demo_round_turn, a service-role-only RPC. It takes a per-IP advisory lock, so parallel requests can't both take the last slot, and it writes nothing when it refuses. A turn counts against the limits before the model call, so a failed call still uses up its slot and can never push spend past the cap.Moderation. The model classifies each input in the same call it answers. Sexual content, violence, self-harm, hate against a protected group, targeting a private person, or text that isn't a debate argument returns
allowed: false, which the function turns into a 422 and the page into "That motion or argument cannot be used in the demo". On top of that: the length caps, an 8 KB body cap, and Anthropic's own safety. There is no separate moderation API.Privacy. The tables hold a salted SHA-256 of
day + IPand counts. Nothing the visitor types is stored. Logs record only the outcome and the turn number ([DEMO] status=ok turn=1). The salt isSPARRING_INTEGRITY_SECRET, or the service-role key when that isn't set, the same fallback theaifunction uses. Sessions older than yesterday are deleted on every claim.Cost per call
OpenRouter's listed price for
anthropic/claude-haiku-5.5, fetched today: $0.10 per million input tokens, $0.50 per million output tokens.max_tokens450 = at most $0.000225.Billing goes to the Anthropic BYOK key at Anthropic's price. Past the monthly BYOK allowance OpenRouter adds its fee, which these numbers don't include.
Deploy steps (not done here)
supabase/migrations/20261102120000_demo_rounds.sql(staging first, then production).debateowl.com(plus the staging and preview hosts if wanted). This creates a secret, so Adam does it.TURNSTILE_SECRET_KEY,DEMO_ENABLED=true, and optionallyDEMO_DAILY_LIMIT(500 if unset).npx supabase functions deploy demo-rebuttal.supabase/config.tomlsetsverify_jwt = falsefor it.VITE_DEMO_ENABLED=trueandVITE_TURNSTILE_SITE_KEY, then redeploy.The CSP in
vercel.jsonalready allowshttps://challenges.cloudflare.comforscript-srcandframe-src, and the CSP guard test is updated to match.Kill switch. Set
DEMO_ENABLEDto anything buttrue. The function answers 503demo_disabledand the page switches back to the sample exchange without a redeploy. UnsettingVITE_DEMO_ENABLEDhides the form too, but needs a redeploy.Before merging. The meta description, intro and FAQ promise "one free exchange with no account" in the static HTML. If this merges while the demo is off, the page promises something it can't deliver. Do steps 1 to 5 before or right at merge.
Checks
Run locally:
npm run verifyat the final head: all 9 steps pass (docs, jargon, lint with knip and squawk, typecheck, guards, Edge tests, smoke, build with prerender, site and link checks, service-worker offline)../.superset/db-gate.sh --db-only: pgTAP passes, including the newsupabase/tests/demo_rounds_test.sql. It covers the rate limits: 3 sessions per IP and a 4th refused, 2 turns and a 3rd refused, a session not continuable from another IP, the global cap refusing without counting, client roles with no access, and old rows deleted.e2e/ai-debate.spec.ts, desktop and mobile Chromium, with Turnstile and the function mocked: two turns end in the trial button, and a 503demo_disabledfalls back to the sample exchange. Not tagged@smoke, so CI doesn't run it.deno run: a wrong origin gets 403, the off state 503, a bad body 400, a missing Turnstile token 403. With Cloudflare's always-pass test secret, a real siteverify call passed and the request failed closed at the database step, since there was no database.vite previewof/ai-debate: SEO 100, accessibility 100.dist/ai-debate/index.htmlcontains the sample exchange and no form, so the page works without JavaScript.Not run:
Notes
🤖 Generated with Claude Code