feat: guess who developer-persona quiz with LLM follow-up#6001
Open
davidercruz wants to merge 22 commits into
Open
feat: guess who developer-persona quiz with LLM follow-up#6001davidercruz wants to merge 22 commits into
davidercruz wants to merge 22 commits into
Conversation
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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
- 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>
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Changes
A new
/guess-whopage 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 newguessWhoQuizStepGraphQL mutation, which proxies to bragi (GuessWhoQuizpipeline) and either returns one more clarifying question or finalises a persona; on finalisation, daily-api also callsrecswipe.extractTagson the persona description so the response includes{ name, description, tags }. The frontend wires this via auseGuessWhoQuizTanStack mutation hook plusLlmPhase(loading/question/result/error states),LlmQuestionCard, and aPersonaResultreveal with tag chips.Events
Did you introduce any new tracking events?
origin: "guess who quiz"target_id: <questionId>,origin: "guess who quiz",extra: { optionId }origin: "guess who quiz",extra: { answer }extra: { persona, tagCount }ENG-1389
Preview domain
https://eng-1389-llm-quiz.preview.app.daily.dev