Skip to content

feat: choose an AI recruiter persona before a simulation#175

Open
BhuvanArn wants to merge 16 commits into
stagingfrom
worktree-persona-picker-modal
Open

feat: choose an AI recruiter persona before a simulation#175
BhuvanArn wants to merge 16 commits into
stagingfrom
worktree-persona-picker-modal

Conversation

@BhuvanArn

Copy link
Copy Markdown
Owner

What

Before a simulation, the user now chooses which AI recruiter will interview them. The choice drives the recruiter's displayed identity (name + role) across the avatar panel and the info box, so picking Marc Bernard visibly changes who you face.

Four personas, ranked by difficulty: Sophie Martin (easy) · Thomas Leroy (medium) · Claire Dubois (medium) · Marc Bernard (hard).

Scope — web only, by design

The AI pipeline wiring is deliberately NOT here. Sending personaId to the backend and resolving each persona's systemPrompt into the LLM prompt is a separate task for the AI developer. This PR changes no DTO, no createInterview payload, and nothing under server/ or ai/. No new dependency.

The roster's French systemPrompt strings are authored now (so that follow-up is a wiring change, not a data-authoring one) and are intentionally unconsumed by the web app today.

Note for whoever picks that up: the recruiter persona is hardcoded in two independent prompt builderssimulation-context.service.ts:24-39 and its Python twin simulation_brief.py:18-46. They share no source, so it is two wiring tasks, not one.

Design decisions

  • The simulation page owns the modal, both routes. The roadmap's "Start simulation" stays a plain <Link>. One rule: on mount, if nothing is selected, ask.
  • sessionStorage, not localStorage. The choice must not outlive the tab — otherwise a user returning days later gets silently interviewed by a forgotten pick with no modal shown. Cleared when an interview ends, so the next run re-asks.
  • Dismiss is non-trapping. Esc / scrim / X / Skip — use Sophie all fall back to Sophie Martin — today's shipped hardcoded identity — so dismissing reproduces current behaviour exactly.
  • Role labels stay French (Recruteuse IT, DRH, Directeur): in-fiction content like the names themselves, and Recruteuse IT is already live. Anglicising would regress shipped UI.

The correctness invariant

No persona copy claims the AI interrupts, rushes, or talks over you — the speech-to-speech pipeline is strict turn-taking (VAD only trims silence; there is no barge-in), so such copy would describe behaviour the system cannot produce. Difficulty is expressed through what gets asked — question depth, follow-ups, scrutiny — never pacing. The modal says so outright: "you always get to finish your answer."

This is enforced by a regex guard test over both description and systemPrompt, so it can't be quietly reintroduced.

Accessibility

The codebase had no focus-trap primitive, so useFocusTrap is new and shared. The gauge never carries meaning alone — the level word (EASY/MEDIUM/HARD) always renders beside it, and the bar is aria-hidden, so it survives greyscale and colour-blindness. Cards are a proper radiogroup with roving tabindex and arrow-key navigation.

Two bugs caught before merge, worth knowing about

  • E2E caught a regression this feature caused: the modal's full-viewport scrim covered the "Back to roadmap" link (PR feat: back-to-roadmap link on the application simulation page #165 / #195) on /applications/:id/simulations, because that link is rendered by the route, outside the workspace where the modal lives. Fixed with an optional back control inside the dialog, so aria-modal stays honest. The #195 guard was net strengthened — both paths to /roadmap are now covered where one was before.
  • The final whole-branch review caught a seam no per-task review could see: clearing the persona on interview end never reopened the picker, because open-state was snapshotted into a useState initializer that only runs at mount. The info box silently flipped Marc → Sophie and the next interview ran against a recruiter the user never chose. Fixed by deriving the open state; regression test verified to fail against the unfixed code.

Testing

  • Web unit: 1254 passing (127 files), including a guard that Change recruiter hides on isCallActive and stays visible during isAwaitingAiResponse (gating on the per-turn flag would flicker it every turn).
  • Server: 887 passing — untouched, verified.
  • E2E (Chromium): persona-picker and application-simulation-back pass. The modal was verified in a real browser in light and dark mode.
  • Gate: lint, prettier, tsc -b all clean.

Follow-ups (not this PR)

  1. AI dev: send personaId; resolve systemPrompt in both prompt builders (see note above).
  2. Pre-existing bug, deliberately not widened here: clearInterviewStorage() at useInterviewSession.ts:388 sits inside a try after an await, so a rejected updateInterview skips it. clearPersona() was placed in a finally specifically to avoid inheriting it.
  3. Per-persona avatar assets — the repo has exactly one GLB, so all personas currently render the same 3D avatar.
  4. Move the roster server-side once it needs editing without a deploy.

🤖 Generated with Claude Code

@vercel

vercel Bot commented Jul 15, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
talk-up-ai-dev Ready Ready Preview, Comment Jul 15, 2026 12:27pm

@railway-app
railway-app Bot temporarily deployed to talk-up-ai / TalkUp.AI-pr-175 July 15, 2026 11:02 Destroyed
@railway-app

railway-app Bot commented Jul 15, 2026

Copy link
Copy Markdown

🚅 Deployed to the TalkUp.AI-pr-175 environment in talk-up-ai

Service Status Web Updated (UTC)
Backend ✅ Success (View Logs) Jul 15, 2026 at 12:27 pm

@BhuvanArn

Copy link
Copy Markdown
Owner Author

Follow-up tracked: EpitechPromo2027/G-EIP-600-NAN-6-1-eip-tugdual.de-reviers#205 — end-of-simulation navigation (confirm early exit, auto-return to the roadmap on completion, and drop the standalone /simulations route so a simulation always runs in an application's context).

Kept out of this PR deliberately: different feature, touches shared files (useInterviewSession, both simulation routes), and dropping /simulations is a product decision — it's a live sidebar entry, not a dead route.

Worth noting for reviewers: the reopen-on-clear fix in this PR stays relevant even after #205 lands. clearPersona() fires while the workspace is still mounted, so during the redirect window the old snapshot behaviour would silently swap the displayed recruiter from the user's pick back to the default.

@BhuvanArn BhuvanArn self-assigned this Jul 15, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants