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 setsStudyPlan.CompanyLeetCodewithIsCuratedMatch = true, the verified URL, andSubscribeUrl = "https://leetcode.com/subscribe/". - No curated match → still sets
StudyPlan.CompanyLeetCode, but pointsCompanyProblemListUrlat LeetCode's own company-tags entry point (https://leetcode.com/company/) instead of guessing a slug, withIsCuratedMatch = 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:
- Job description and fit summary verbatim from
JobPosting.Fit. - Gap keywords (
Fit.GapKeywords) the user should close before the interview. - Detected ATS/assessment platform —
JobPosting.Ats.Kind(Workday, Greenhouse, Lever, iCIMS, UKG, ADP, Paycom...) and tenant host when known; an honest "not yet detected" line whenAtsKind.Unknown, rather than guessing. - Company-specific coding practice — the Part A LeetCode recommendation, when present.
- 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.
- Full study-plan resource list (every
StudyResource, one line each). - 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-separatedFront\tBack, one line per card, with a#separator:tab/#html:falseheader per Anki's plain-text import format.RenderQuizletTsv— tab-separatedterm\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 (BuildClaudeProjectBriefgap 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.