Skip to content

WorkWingman — Prep Studio (Technical)

The interview-prep studio extends the existing per-job Study & prep hub (StudyPlanService, src/WorkWingman.Infrastructure/Services/StudyPlanService.cs) with three additions, all built to the same hard rules the rest of WorkWingman follows: nothing auto-subscribes or auto-pays on the user's behalf, everything is a deep-link the user follows themselves (BYO credential/subscription), and nothing about the user leaves the machine except files the user explicitly chooses to move.

Plain-language version: ../plain/prep-studio.md

Part A — Company LeetCode libraries + Premium

LeetCode publishes per-company problem lists at a URL pattern verified against a live page (leetcode.com/company/google/, returned in a July 2026 web search as the canonical URL shape; the JS-rendered page itself blocks unauthenticated WebFetch/bot traffic, consistent with LeetCode's own scraping-defense posture) — the pattern is https://leetcode.com/company/{slug}/. Seeing the full company-tagged problem list is a LeetCode Premium feature; the page itself may render without a login but the complete company-specific list is gated.

LeetCodeCompanyMap (src/WorkWingman.Core/Models/LeetCodeCompanyMap.cs) is a curated, static dictionary of ~65 common employers (Microsoft, Google, Amazon, Meta, Apple, Netflix, Uber, Salesforce, Bloomberg, Airbnb, Goldman Sachs, Nvidia, Stripe, Databricks, Palantir, ...) to their LeetCode company slug. Matching is case-insensitive, tolerant of common corporate suffixes ("Microsoft Corporation" → "Microsoft"), and uses whole-word substring containment so a name like "Meta Platforms Inc." resolves to "Meta" — but a short key can never false-positive on an unrelated name that merely contains the same letters (e.g. the single-letter key "X" for X/Twitter cannot fire on "...Xyz Startup", because matching requires a whole word, not a raw Contains).

StudyPlanService.BuildPlan calls LeetCodeCompanyMap.TryGetCompanyUrl(job.Company):

  • Curated match → adds a StudyResource (Kind = LeetCode, title "Practice {Company}'s frequently-asked problems on LeetCode") and sets StudyPlan.CompanyLeetCode with IsCuratedMatch = true, the verified URL, and SubscribeUrl = "https://leetcode.com/subscribe/".
  • No curated match → still sets StudyPlan.CompanyLeetCode, but points CompanyProblemListUrl at LeetCode's own company-tags entry point (https://leetcode.com/company/) instead of guessing a slug, with IsCuratedMatch = false.

Never auto-subscribes. There is no code path in LeetCodeCompanyMap or StudyPlanService that creates a LeetCode account, submits payment details, or calls any billing API — the recommendation is surfaced with a <a href> deep-link the user clicks themselves, mirroring the convention already documented for every other linked account (Udemy, Google Cloud Skills Boost, AWS Skill Builder, Microsoft Learn) in connections-and-sso.md. The Study screen renders both the free problem list link and a separate, explicitly labeled "Get LeetCode Premium →" button so the paid nature of the subscription is never hidden.

Part B — Richer Claude Project generation

StudyPlanService.BuildClaudeProjectBrief(JobPosting, StudyPlan) assembles the structured prep material a generated Claude Project is seeded with. It is a public static method (independently unit-testable without a live Claude API call) that composes, purely from fields already on JobPosting/StudyPlan — nothing fabricated:

  1. Job description and fit summary verbatim from JobPosting.Fit.
  2. Gap keywords (Fit.GapKeywords) the user should close before the interview.
  3. Detected ATS/assessment platformJobPosting.Ats.Kind (Workday, Greenhouse, Lever, iCIMS, UKG, ADP, Paycom...) and tenant host when known; an honest "not yet detected" line when AtsKind.Unknown, rather than guessing.
  4. Company-specific coding practice — the Part A LeetCode recommendation, when present.
  5. System design — added only when the job title signals a senior-leveled role (contains "senior", "staff", "principal", or "lead", case-insensitive), since junior interview loops rarely include a system-design round.
  6. Full study-plan resource list (every StudyResource, one line each).
  7. Behavioral/story prep — a pointer to the user's own Story profile (StoryProfile, src/WorkWingman.Core/Models/StoryProfile.cs) plus a fixed set of common prompts ("Tell me about yourself", "Why this company", a disagreement story, a proudest-decision story, a failure story) so the generated project can quiz against the user's real answers instead of generic STAR templates.

GenerateClaudeStudyProjectAsync now populates StudyPlan.ClaudeProjectBrief with this text before minting the (currently stubbed) project URL — the brief is what a real Claude Projects API call would upload as project knowledge alongside the resume (Claude Projects docs).

Part C — Flashcards + phone-portable audio review

Generation

FlashcardService.BuildDeck(JobPosting, StudyPlan) (src/WorkWingman.Infrastructure/Services/FlashcardService.cs) is deterministic and template-based — the same "no LLM call required" convention as TemplateDrafter (IClaudeDrafter's offline fallback, src/WorkWingman.Infrastructure/Services/DocumentService.cs). Every card traces to a concrete source:

Category Source
TechConcept one card per Fit.MatchedKeywords entry
GapKeyword one card per Fit.GapKeywords entry, framed as a "how do you answer this gap" prompt
CompanySpecific one card per LeetCode study resource (title + MatchedTo, deliberately no URL — see below)
SystemDesign one card, only for senior-leveled titles (same heuristic as the Claude brief)
Behavioral a fixed set of 7 common prompts, always included

Primary export: self-contained HTML, Web Speech API audio

FlashcardService.RenderSelfContainedHtml(FlashcardDeck) returns one HTML string with the deck's cards JSON-serialized and embedded directly in an inline <script> block — no <link>, no <script src=...>, no CDN, no fetch/XHR/WebSocket call anywhere in the page. The mobile review UI is a flip-card with a lightweight in-session spaced-review queue (cards rated "Again"/"Hard" requeue for another pass; "Good"/"Easy" complete the card and remove it from the queue, so rating every card Good always finishes the deck — not full SM-2, since the file is disposable and has no persistence by design) and three controls: Flip, Speak, Stop.

Audio is the browser-native Web Speech API (window.speechSynthesis / SpeechSynthesisUtterance) — free, no API key, no network call: the device's own on-device or OS-provided TTS engine renders speech locally. This is "Baseline widely available" per MDN (supported since 2018) in iOS Safari and Android Chrome.

iOS quirk, verified via web search and handled: iOS Safari only starts speechSynthesis.speak() from inside a user-gesture event handler (a tap/click) — calling it from a timer, promise callback, or on page load is silently dropped. The template wires Speak with document.getElementById("speak").addEventListener("click", speak), i.e. directly on a click listener, never from setTimeout/async chains, and shows an on-page hint the first time ("Tip (iPhone/iPad): tap Speak once..."). getVoices() can return empty until Safari's voiceschanged event fires — the template doesn't depend on a pre-populated voice list; it lets speechSynthesis.speak() use the OS default voice for the utterance's language.

Zero external network calls — enforced, not just claimed: FlashcardServiceTests.RenderSelfContainedHtml_HasNoExternalHttpOrHttpsReferences asserts the rendered HTML contains no http:///https:// substring anywhere (regex https?:// across the whole document), and separately asserts no <link, no script src, no fetch(, no XMLHttpRequest, no WebSocket. To make that guarantee hold structurally rather than by luck, FlashcardService.BuildDeck deliberately never puts a resource URL into card text — the LeetCode-resource cards reference the resource by title and MatchedTo only ("find the link on the Study tab's Coding interview card"); the actual clickable link lives one tap away on the Study tab, which is a normal networked page, not the offline export. This means the "no external references" guarantee holds regardless of what a future job's JD/company text contains.

Once downloaded, the file is meant to be moved to a phone by the user's own means (email, AirDrop, a cloud drive, or a self-generated QR code) and opened directly in Safari/Chrome — no app store, no install, no account.

Secondary exports: Anki and Quizlet (optional, BYO account)

  • RenderAnkiTsv — tab-separated Front\tBack, one line per card, with a #separator:tab / #html:false header per Anki's plain-text import format.
  • RenderQuizletTsv — tab-separated term\tdefinition, no header, per Quizlet's "import from Word/Excel" flow.

Both are explicitly optional and documented as involving the user's own pre-existing account on those third-party services — WorkWingman never creates an Anki or Quizlet account on the user's behalf, and these exports make no network call themselves (they're static file downloads the user then uploads to whichever service they already use).

Endpoints

FlashcardsController (src/WorkWingman.Api/Controllers/FlashcardsController.cs), routed under api/study/{jobId}/flashcards:

Route Behavior
GET / Returns the persisted deck, or 404 if never generated
POST /generate Builds (or rebuilds) the deck
GET /export/html Downloads the self-contained HTML file
GET /export/anki Downloads the Anki-importable TSV
GET /export/quizlet Downloads the Quizlet-importable TSV

Frontend

The Study screen's "Take it with you" panel (frontend/src/app/features/study/study.html) generates the deck and surfaces the three downloads with a short how-to, plus the LeetCode Premium recommendation card (Part A) rendered above the existing resource grid.

Optional future note (not built)

A paid, higher-fidelity TTS API (BYO key, e.g. ElevenLabs or a cloud provider's TTS) could eventually replace or supplement the browser's built-in voice for a nicer listening experience. This is deliberately not implemented — the default and only shipped path is the free, offline, zero-config Web Speech API, which is sufficient for review purposes and preserves the "no network call, nothing leaves the device" guarantee that a paid API would break. If ever added, it would need to be config-gated and off by default, with the self-contained HTML export remaining fully functional (Web Speech fallback) when no key is configured.

Testing

  • LeetCodeCompanyMapTests.cs — slug/URL resolution, corporate-suffix tolerance, whole-word substring matching (including the short-key false-positive regression), the verified URL pattern, and the subscribe URL.
  • StudyPlanServiceTests.cs — Part A (curated match, fallback, never-auto-subscribe) and Part B (BuildClaudeProjectBrief gap keywords, ATS detection, senior/junior system-design gating, behavioral prompts, company LeetCode inclusion).
  • FlashcardServiceTests.cs — deck generation from matched/gap keywords, behavioral prompts, senior-role system-design gating, the no-raw-URL-in-cards guarantee, the self-contained HTML's zero-external-reference assertion, Web Speech API usage, the click-gesture wiring, XSS-safe title encoding, and Anki/Quizlet TSV format correctness (tab-separation, escaping).

Related: job-signals.md, connections-and-sso.md, references.md.