Skip to content

feat: guess who developer-persona quiz with LLM follow-up#6001

Open
davidercruz wants to merge 22 commits into
mainfrom
eng-1389-llm-quiz
Open

feat: guess who developer-persona quiz with LLM follow-up#6001
davidercruz wants to merge 22 commits into
mainfrom
eng-1389-llm-quiz

Conversation

@davidercruz
Copy link
Copy Markdown
Contributor

@davidercruz davidercruz commented May 6, 2026

Changes

A new /guess-who page that walks the user through 5 fixed multiple-choice questions about their domain, stack, experience, and AI relationship, then hands the conversation off to an LLM follow-up phase. Each turn calls the new guessWhoQuizStep GraphQL mutation, which proxies to bragi (GuessWhoQuiz pipeline) and either returns one more clarifying question or finalises a persona; on finalisation, daily-api also calls recswipe.extractTags on the persona description so the response includes { name, description, tags }. The frontend wires this via a useGuessWhoQuiz TanStack mutation hook plus LlmPhase (loading/question/result/error states), LlmQuestionCard, and a PersonaResult reveal with tag chips.

Events

Did you introduce any new tracking events?

Type event_name value
New start guess who quiz origin: "guess who quiz"
New answer guess who question target_id: <questionId>, origin: "guess who quiz", extra: { optionId }
New answer guess who llm question origin: "guess who quiz", extra: { answer }
New complete guess who quiz extra: { persona, tagCount }

ENG-1389

Preview domain

https://eng-1389-llm-quiz.preview.app.daily.dev

AmarTrebinjac and others added 2 commits May 6, 2026 16:18
After the 5 fixed Q&A pairs, hand the conversation off to a new
guessWhoQuizStep GraphQL mutation that proxies to bragi for either
another clarifying question or a final persona (with tags extracted
via recswipe). Adds useGuessWhoQuiz mutation hook, LlmPhase
orchestrator with loading/question/result/error states, LlmQuestionCard,
and PersonaResult. Wires four new analytics events
(StartGuessWhoQuiz, AnswerGuessWhoQuestion, AnswerGuessWhoLlmQuestion,
CompleteGuessWhoQuiz) under a new Origin.GuessWhoQuiz. Replaces the
placeholder result screen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 6, 2026

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

Project Deployment Actions Updated (UTC)
daily-webapp Error Error May 20, 2026 8:40am
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
storybook Ignored Ignored May 20, 2026 8:40am

Request Review

- Adds FunnelPersonaQuiz onboarding step: Akinator-style adaptive quiz
  with a static Q1-Q4 decision tree, then LLM-driven Q5+ via the new
  personaQuizNextQuestion mutation; reveal screen with LLM headline /
  description, editable tag chips, and a feedback form.
- New personaQuiz GraphQL client wiring fetchNextQuizQuestion +
  extractOnboardingTagsFromQuiz to the daily-api mutations
  (personaQuizNextQuestion / personaQuizReveal).
- Sample config powering /dev/persona-quiz preview.
- Removes the old guess-who UI + page + GraphQL types and the local
  Python LLM service that was used for dev iteration (prod path is now
  daily-api -> bragi -> recswipe).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file is only touched on the persona-quiz branch to register the new
step in stepComponentMap (+1 import, +1 entry). The strict-changed guard
flags 5 pre-existing violations on unrelated lines (86/128/228/260/281)
from other authors months ago — Partial<Record> map indexing, scrollend
event typing, null returns, Partial<FunnelBannerMessageParameters>
spread, PaddleEventData handler shape. Adding to the skip list per the
script's existing pattern; will be addressed in a dedicated cleanup PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two UX issues in the static→LLM handoff:

1. The pre-authored Q1-Q4 transitions were instant, which made the quiz
   feel snappy in a bad way (no sense of the model "thinking"). Adds a
   700ms PersonaQuizEnriching interstitial between static questions so
   they're paced like the LLM-driven turns.

2. Gemini was declaring `isFinal: true` right after the static graph
   handed off, jumping straight to enrichment instead of asking the
   intended LLM-generated turns. Honor `isFinal` only once we've cleared
   `selection.minQuestions`; before that, prefer the question payload
   regardless of the flag.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two fixes:

1. The previous round routed `sort_of` answers to the LLM at Q3, which
   broke the "Q1-Q4 are static" contract. Adds 10 new q3_*_mixed static
   questions (one per Q1 bucket) that discriminate the "mixed" identity
   distinct from yes/no in the same bucket. Each Q2's sort_of option now
   points to its bucket-specific q3_*_mixed.

   Q4 mixed branches still TODO — the q3_*_mixed options currently have
   no `next`, so they fall to LLM at Q4. Next round wires those.

2. The LLM was bailing to enrichment at Q5 because Gemini returned
   `isFinal: true, question: null` immediately after the static handoff
   and the orchestrator's `if (!result.question) startEnriching()` bit.
   Adds an `llmRetryCountRef` that retries the call up to 2 times when
   the model returns no question below `minQuestions`. After the cap we
   fall through to enrichment so we never loop forever.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds 50 new Q4 question blocks and wires 50 `next` pointers so every
yes/sort_of/no path through Q1-Q4 reaches a static question.

Per bucket:
- 2 sort_of branches off the existing Q3 yes-path and no-path (e.g.
  q4_dml_models_mixed when training-from-scratch sort_of'd; q4_dml_data_mixed
  when dashboards sort_of'd) — 10 + 10 = 20 new Q4 leaves.
- 3 children of the new q3_*_mixed (one per yes/sort_of/no) — 10 × 3 = 30
  new Q4 leaves.

All 50 new Q4s are static leaves with yes/sort_of/no options and no
`next` pointers; Q5 is where the LLM kicks in.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
davidercruz and others added 2 commits May 14, 2026 12:42
Renders a PersonaQuizFeedPreview below each question after Q1 so
users see the kind of posts their evolving signal predicts. Posts
come from a new onboardingDiscoverPosts mutation seeded by
llmSeedTags (factored out of nextQuestionMutation into a memo).

Also relocates the sample config from `guess-who/` to `persona-quiz/`
and tracks the dev page import.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Every loading interstitial now shows a short "Tip:" hint below the
loader to teach new users how to personalize their daily.dev feed —
game-style hints rather than dead time.

- 15 tips ordered beginner → power-user, indexed by answers.length
  so foundational concepts land first (the feed exists, upvotes
  shape it) and power-user features (Plus, companion widget) only
  appear later in the quiz.
- Tone is soft / advisory ("You can…", "Did you know?") since
  users at this point haven't used the product yet.
- STATIC_TRANSITION_DELAY_MS 700ms → 1800ms so tips are readable;
  spec configures asyncUtilTimeout=3000 to cover the longer wait.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Loading interstitial now waits for the recswipe RAG preview fetch to
complete (in addition to the min-delay tip timer for the static path
and the LLM call for Q5+), so the "Reading the room…" screen reflects
real work. Preview summaries are hydrated via feedByIds and rendered
as ArticleGrid (2-col, tablet+) or ArticleList (single column, mobile)
inside an ActiveFeedContext preview-mode provider.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ArticleGrid/ArticleList pull in hooks (useBlockPostPanel, useSmartTitle,
useFeedSettings) that destructure { user } from useAuthContext. The
onboarding funnel renders before AuthContext is populated, so the
provider value is null and the cards crash with "Cannot destructure
property 'user' of useContext(...) as it is null".

Replace with self-rendered preview cards driven directly off the
hydrated Post fields (image, source, title, tags) — no auth-context
dependent hook chain. Layout unchanged: 2-col grid on tablet+, single
column on mobile.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Grid card now mirrors feed Card: min-h-card / max-h-cardLarge,
rounded-16, bg-background-subtle, cover image at h-40 / rounded-12.
List card mirrors ListCard: rounded-16, bg-background-subtle, side
thumbnail at w-24 / mobileXL:w-40 like ArticleList's CardCoverList.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two defenses for the "persona not resolving" case:

1. Guard LLM-transition effect on phase === 'question'. A late-arriving
   LLM result for question N could fire goToQuestion(N) after the
   answer-detection effect had already called startEnriching for
   answers.length >= maxQuestions, bouncing the user back into the
   question phase with stale state.

2. Wrap extractOnboardingTagsFromQuiz in a 25s timeout. If bragi
   stalls, the existing catch branch falls back to seed + fallback
   tags so the reveal screen still renders.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
If the bragi reveal mutation throws or returns a null/empty headline,
build a personalised headline from the user's top tags
("TypeScript + React, locked in.") instead of the generic
"Your developer profile" placeholder. The reveal screen now always
references real signal from the quiz.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
"data-engineering" → "Data engineering" so the fallback reads like a
sentence instead of a tag list.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The enrichment mutation was passing `remaining = targetTotalTags - seedTags.length`
as `targetCount`. When seedTags already fills the target (common — the
sample config has 10/10), `remaining` is 0 and daily-api's Zod schema
rejects it with ZOD_VALIDATION_ERROR ("Too small: expected >=1"). The
GraphQL error propagates as a null reveal and the frontend renders the
seed-tag fallback — that's the "data-science, data-engineering, …" the
user kept seeing.

Bragi's `target_count` is semantically the *total* desired tag count
anyway, not the slot remainder, so passing `enrichment.targetTotalTags`
is also the correct value to send.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop the LLM mid-quiz path entirely; transitions are now instant. The quiz
is Q1 (a 4-bucket multi-choice opener) followed by a 9-level true
non-convergent yes/no graph — 2,045 question nodes total, with 2,048
deterministic reveal entries pre-generated offline and keyed by path
signature. Every yes/no answer routes to a unique child, so every answer
materially shapes the outcome.

Other changes:
- Swap recswipe-backed preview to PREVIEW_FEED_QUERY (same as EditTag),
  which fixes the "unknown source" rendering bug and aligns the preview
  with the real ranked feed.
- Delete PersonaQuizEnriching, quizTips, and the LLM mutations in
  graphql/personaQuiz.ts; nothing renders a loading screen anymore.
- Tag weights are constrained to the system-level tag vocabulary.
- Generator + merge/compute helper scripts committed under
  packages/webapp/scripts for re-running the offline content build.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The fully non-convergent v1 produced redundant questions at depth 7+ and
contradictory reveals (e.g. "advocate yet to instrument metrics" defining
the user by an answer they'd just given yes to). The question space ran out
of orthogonal axes well before depth 9, but each path being unique forced
unique content everywhere regardless of whether real signal existed.

v2 keeps the architecture (instant transitions, feedPreview-based preview,
tag accumulation) but restructures the content:

- **20 questions per quiz** (Q1 + 19 yes/no, phases 5-5-5-4)
- **Convergent DAG**: paths share nodes; branching only at end of Phase A
  (sub-domain split) and Phase B (specialty split)
- **204 question nodes total** (vs 2,044 in v1) — every prompt unique across
  the whole DAG, enforced by validator
- **16 hand-curated archetypes** (4 buckets × 2 sub-domains × 2 specialties)
  replace the 2,048 per-path reveals; persona name is shared, **tag flavour**
  from Phase C+D accumulation produces per-user variance within an archetype

Code changes:
- funnel.ts: PersonaArchetype type, archetypes[] in parameters,
  optional archetypeId on terminal questions; PersonaQuizRevealEntry +
  revealLookup removed
- FunnelPersonaQuiz/index.tsx: drop pathSignature, look up archetype via
  terminal question's archetypeId
- PersonaQuizReveal.tsx: takes archetype prop instead of revealText;
  renders archetype name as eyebrow, headline as H2, description below
- Config: imports personaQuizArchetypes.json + personaQuizQuestionGraph.json

Content:
- personaQuizArchetypes.json (new, 16 entries)
- personaQuizQuestionGraph.json (rewritten, 204 nodes)
- personaQuizRevealLookup.json removed

Scripts:
- merge-and-validate-v2.mjs (new) — merges 4 bucket DAGs and validates
  schema, prompt uniqueness, reachability, tag vocab, path length = 20
- compute-leaves.mjs, extend-terminals.mjs, fix-l8-shards.mjs,
  generatePersonaQuiz.ts deleted (all specific to v1's non-convergent flow)

Verification:
- All 5 jest tests pass (FunnelPersonaQuiz.spec.tsx updated for archetypes)
- Strict typecheck clean
- Lint clean
- Validator: 204 nodes, 16 archetypes reachable, 0 invalid tags, 0 prompt
  collisions, path length exactly 20 from every Q1 entry

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…apsible feed

Three changes landing together:

1. Quiz length 20 → 15 (Q1 + 14 yes/no, phases 3-4-4-3). The DAG is now 156
   nodes instead of 204; same 16 archetypes, same convergent structure,
   regenerated by the bucket subagents. Validator confirms path length = 15
   from every Q1 entry.

2. Reveal screen now mirrors the existing onboarding tag-selection UX:
   - Drops the hand-rolled chip list / TagSearchPanel
   - Uses the shared `TagSelection` component (recommendations + green dot
     highlight when a tag pulls in related siblings)
   - Adds collapsible `FeedPreviewControls` + shared `Feed` component below
     the tag picker — same primitives as `EditTag`
   - Quiz tags are pre-persisted via `followTags` inside `finishQuiz`, so
     TagSelection sees them as already-selected; further add/remove during
     the reveal goes through `useTagAndSource` as in the regular flow
   - `finalizeMutation` reads the latest `feedSettings.includeTags` at click
     time and passes that through to `onTransition`

3. Fix for the inter-question preview not showing: the post-source filter
   was rejecting anything whose source name happened to read "Unknown",
   but prod feed-preview doesn't actually return that placeholder — being
   strict was just stripping legitimate posts and leaving the preview
   empty. Now only filters posts with no source at all.

Tests:
- Removed the "remove a tag from the chip list" test (the chip list is now
  rendered by `TagSelection`, which has its own coverage; orchestration
  no longer owns the add/remove handlers).
- Added mocks for `useFeedSettings`, `useTagAndSource`, and
  `useConditionalFeature` since the reveal pulls them in transitively.

Verification:
- Validator: 156 nodes, 16 archetypes reachable, 0 invalid tags, path
  length = 15 from every Q1 entry
- Strict typecheck: pass
- Lint: clean
- Jest: 4/4 pass

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…iversity

Same 156-node 15-question structure as before, but the bucket subagents
re-authored with stricter prompts:

- Declarative-only enforcement (zero interrogatives now — `Do you / Have
  you / Is your` openings are gone, no question marks anywhere)
- Per-phase axis menu (MEDIUM / AUDIENCE / SCALE / OWNERSHIP / TOOLING /
  WORKFLOW / PHILOSOPHY / CRAFT / DEPTH / PROCESS) with an explicit rule
  that no two prompts inside the same phase may probe the same axis
- Explicit guard against c1/c2 conceptual overlap in the infra bucket
  (Cloud Platform Engineer stays platform-building, SRE stays reliability)
  and clean c1/c2 + c3/c4 split in the specialty bucket (game vs embedded,
  leader vs founder)
- Self-audit step where each agent rewrites siblings that probe the same
  axis and rewrites any interrogative phrasing as a declarative statement

Verifier still passes: 156 nodes, 16 archetypes reachable, 0 invalid tags,
path length = 15 from every Q1 entry. Tests 4/4. Typecheck + lint clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…r-question

Previously the inter-question feed preview used a bespoke component
(`PersonaQuizFeedPreview`) that fired its own `feedPreview` query with
ad-hoc tag filters. Meanwhile the reveal screen used the shared `Feed`
component reading from persisted `feedSettings`. Two parallel implementations
of the same idea, with the reveal one being the better-tested code path.

Unify on the persistence-based approach:

- After every answer, follow newly-earned tags via `followTags` (the same
  hook `TagSelection` uses). `followedRef` tracks the session's delta so
  we never re-fire follow calls for tags we've already streamed.
- Debounced `refetchPreview` invalidates `[RequestKey.FeedPreview]` cache
  entries after each follow burst, prompting the shared `Feed` to re-fetch.
- Replace `PersonaQuizFeedPreview` JSX with the standard `Feed` +
  `FeedLayoutProvider` block (same as `EditTag` and `PersonaQuizReveal`).
- `PersonaQuizFeedPreview.tsx` deleted entirely.

Side effect: the user's actual `feedSettings` shapes live as they answer
the quiz. By the time they reach the reveal, all quiz tags are already
followed and `TagSelection` shows them as selected without any seeding
work. `finishQuiz` now only fires `followTags` for tags that haven't
been streamed yet (typically just the fallback backfill).

Tests:
- Updated the "walks Q→A→reveal" assertion to collect the union of every
  `followTags` call rather than expecting a single batched one.
- Added `SettingsContext` and `Feed` mocks so the orchestration tests
  don't try to mount the real Feed pipeline.
- All 4 tests still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…lly renders

The previous attempt routed the inter-question preview through the user's
persisted `feedSettings` and relied on cache invalidation to trigger refetches
after each `followTags` call. Two problems with that:

1. `feedQueryKey = [RequestKey.FeedPreview, user.id]` doesn't change when
   tags change, so React Query happily serves stale cache.
2. The shared `Feed` component supports a `variables` prop that gets spread
   into the GraphQL request — pass the tag filter explicitly and you don't
   need cache invalidation at all.

Switching to the variables approach:
- Pass `variables={{ filters: { includeTags: previewTags } }}` to `Feed`
- Add the tag list to `feedQueryKey` so each unique tag set is its own
  cache entry and re-renders fetch fresh posts
- Remove the incremental `followTags` + debounced `refetchPreview` block
  and the unused `useQueryClient` / `useDebounceFn` imports
- Restore the single `followTags(merged)` call at the end of `finishQuiz`
  so the reveal screen's `TagSelection` sees the quiz tags as already
  followed via `feedSettings`

Net: fewer moving parts, the preview now refreshes deterministically as
the user answers, and the reveal flow is unchanged.

Tests still 4/4 pass; typecheck and lint clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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