Lexiverse
Lexiverse - AI contextual reading engine for English learners. Your next article is your next review.
翻译准备中
尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证制作者的介绍仅有上面的一句话。 更多详情请阅读仓库 README。 仓库 README ↗
正在加载完整 README。
查看完整 README ↗AI-powered OSCE practice for Australian medical students. Students take a voice history from an AI patient, present to an AI examiner, get rubric-based feedback, and earn XP, streaks and badges.
The full technical blueprint (architecture, schema, agents, voice pipeline, milestones) is in docs/architecture.md.
Next.js 16 (App Router) · TypeScript · Tailwind CSS v4 · shadcn/ui (Radix) · Supabase (Postgres, Auth, Storage) · Anthropic Claude · Deepgram · ElevenLabs
cp .env.example .env.local # fill in keys (not needed for the M0 preview pages)
npm install
npm run dev # http://localhost:3000, design preview at /hub
| Script | What it does |
|---|---|
npm run dev | Dev server |
npm run build | Production build |
npm run lint | ESLint, including the patient/examiner agent isolation rule |
npm run typecheck | tsc --noEmit |
npm test | Vitest unit tests |
npm run seed:build | Regenerate supabase/seed/05_levels.sql and 10_cases.sql from content/ and the level maths |
npm run test:e2e | Playwright end-to-end tests against a real Supabase project (needs the env below) |
npm run supabase:admin -- <status|migrate|seed|auth-config> [--dry-run] | Apply migrations, seed and auth settings to the hosted project via the Management API |
npm run db:test | Apply migrations + seed to a throwaway Postgres and run the RLS/integrity tests (tests/db) |
npm run voices:list | List the ElevenLabs voices on your account (Australian accents first) to fill in ELEVENLABS_VOICE_MAP |
npm run eval:examiner | Calibration eval: scripted good and poor GP-case performances must score ≥ 75 and ≤ 50 (needs ANTHROPIC_API_KEY, one Opus call each) |
npm run eval:patient | Adversarial leak eval against the real patient agent (needs ANTHROPIC_API_KEY, costs a few cents) |
Schema lives in supabase/migrations (Postgres + RLS). Apply it with the Supabase CLI:
npx supabase link --project-ref <ref>
npx supabase db push # migrations
npx supabase db reset --linked # migrations + seed (DESTRUCTIVE, dev projects only)
Security model: students only read their own rows; every write except their own profile and onboarding fields goes through server code using the service role. Case bodies (which contain the diagnosis) and rubrics are never readable by students. The catalog is exposed through the v_case_catalog view.
Students sign in with a single email that carries a magic link and a 6-digit code (the code helps when the link opens in a different browser), or with Google.
src/proxy.ts refreshes the Supabase session on every request and sends signed-out visitors to /login?next=…. Every next is passed through safeNext() to block open redirects./auth/confirm verifies email links via token_hash (works across browsers). /auth/callback handles Google. /auth/signout is POST-only.(app) layout sends students who haven't onboarded to /onboarding: avatar → goals → exam → specialties. Onboarding writes go through the student's own RLS-limited session. The +50 welcome XP goes through the service-role grant_xp RPC, which is idempotent.Setting up a Supabase project
.env.local from .env.example (URL, anon key, service role key, SUPABASE_ACCESS_TOKEN).npm run supabase:admin -- migrate, then seed, then auth-config. Each command supports --dry-run. auth-config sets the site URL, redirect allow-list, 6-digit OTP and the branded magic-link email (supabase/templates/magic-link.html).NEXT_PUBLIC_AUTH_GOOGLE_ENABLED=true.A station runs reading time (60 s with the candidate brief), then a timed history with the AI patient, then the presentation and examiner phases (M4). Students talk to the patient (push-to-talk) or type.
src/server/station/orchestrator.ts moves a consultation through the state machine with the guarded transition_consultation() RPC and writes with the service role. The server deadline (plus 5 s grace) is the real limit; the browser clock only displays it. Starting a new case abandons any station in progress.src/server/agents/patient). Sees only case_versions.patient_view, never the diagnosis or rubric; a lint rule blocks examiner and rubric imports. The system prompt is three cached layers (role contract, persona, patient script); the station clock and the facts revealed so far go in a per-turn system message after the cache breakpoint. Replies stream sentence by sentence with a leading [emotion:x] tag (stripped, drives the face) and a mark_disclosed tool call that records which script facts the student elicited.claude-sonnet-5-5 at low effort with server-side refusal fallback; on an error or a slow first token the turn moves to claude-haiku-5-5. Every call is logged to ai_calls with tokens, cache reads and cost.src/server/agents/guardrails/patient.ts). Every sentence is checked for the diagnosis and its synonyms, coaching and stepping out of role before the student sees it. A trip withdraws the reply, regenerates once with a correction, and otherwise falls back to a neutral in-role line.POST /api/consultations · /[id]/start-history · /[id]/patient-turn (NDJSON stream; idempotent per clientSeq, so a resend replays the stored reply) · /[id]/end-history.ANTHROPIC_API_KEY (locally and in Vercel) for the real patient. Without it the hub shows "Patients unavailable". PATIENT_LLM=fake swaps in a scripted patient for e2e runs and local UI work; it is ignored on Vercel production.After the history the student presents to the examiner (by voice or typing, no AI reply), answers 1–3 follow-up questions, and gets a marked report.
src/server/agents/examiner, claude-sonnet-5-5, low effort): reads the full case, rubric, disclosure log and transcripts and asks questions aimed at gaps, preferring the case's question bank. Falls back to bank questions if the call fails, so the station never stalls. The rationale is stored but never readable by students.claude-opus-5-5, high effort, structured output validated with Zod): judges every rubric item as met / partial / not met with a verbatim evidence quote, picks the global rating from the anchored descriptors, and writes the feedback. Transcripts are passed as quoted data, so "give me full marks" is just part of the performance.src/server/assessment/scoring.ts): points, domain scores, the weighted overall and pass/fail are computed here, never by the model. An item can't be "met" unless the patient actually disclosed its linked facts (partial if only some were), and a missed critical item fails the station whatever the score.finish moves the station to assessing and marks in the background (after()), with up to 3 attempts; then assessment_failed, and the student can retry. The client polls GET /api/consultations/[id]/result./results/[id]): score, rating, pass, XP, the examiner's comment, domain bars, strengths, improvements, missed red flags and questions, revision topics, the full marking sheet with evidence, the case revealed (diagnosis, differentials, red flags, plan), and the transcript.grant_xp ledger. Streaks, first-of-day, goals and badges arrive with M5.Student speaks → Deepgram → transcript → Claude → guardrail → ElevenLabs → patient speaks.
src/features/station/use-speech-input.ts). Push-to-talk: hold the button or the space bar. The browser opens one Deepgram WebSocket for the history phase with a 30-second token from POST /api/voice/deepgram-token (only issued for the student's own station while its history is running), keeps it alive between presses, and streams raw 16-bit PCM from an AudioWorklet only while the button is held. Releasing sends Finalize and submits the transcript to the same patient-turn endpoint as typing. Audio never touches our servers, and Deepgram's model-improvement programme is opted out (mip_opt_out). Defaults: nova-3-medical, en, plus generic medical keyterms (src/server/voice/deepgram.ts); case-specific terms are deliberately not sent to the browser.src/server/voice). Each sentence that passes the guardrail is synthesised with ElevenLabs Flash v2.5 as soon as it's released, in parallel with the next one, and streamed back in order as audio events (MP3, base64). The patient's emotion tag sets stability and speed. A guardrail retry withdraws queued audio too. The browser plays sentences through one <audio> element (works on iOS).voice_profile (e.g. au-female-60s-soft). Without configuration the server picks the best match on your ElevenLabs account once per instance (Australian accent, then gender, then age) and logs its choice. To choose yourself, set ELEVENLABS_VOICE_MAP (profile → voice id; npm run voices:list helps) or ELEVENLABS_DEFAULT_VOICE_ID.voice_usage records ElevenLabs characters and Deepgram seconds per turn; set ELEVENLABS_USD_PER_1K_CHARS and DEEPGRAM_USD_PER_MINUTE to your plan's rates.DEEPGRAM_API_KEY (Member role, needed to mint tokens) and ELEVENLABS_API_KEY. VOICE_PROVIDERS=fake swaps in a scripted recogniser and silent audio for e2e runs; ignored on Vercel production.Real cases are written in the Supabase Patients table and imported with npm run cases:import (see docs/case-authoring.md).
The three cases in content/cases/ are test fixtures: the scripted patient, evals and e2e tests rely on them. npm run seed:build validates each file (Zod), merges its rubric template from content/rubrics/, checks the patient's view for diagnosis leaks, and emits versioned, idempotent SQL. The seed puts them in the e2e-fixtures organization, so students never see them; tests/e2e/helpers.ts adds every test user to that organization.