Skip to content

quiz generation - #29

Merged
flvvius merged 6 commits into
mainfrom
feat/quiz
May 12, 2026
Merged

quiz generation#29
flvvius merged 6 commits into
mainfrom
feat/quiz

Conversation

@flvvius

@flvvius flvvius commented May 12, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features

    • Daily news quiz page with per-question grading, review, sharing, and persisted attempts that update streaks.
    • Early-access waitlist sign-up card.
  • UI

    • Quiz added to header and mobile tabs; CTAs on feed and activity dashboard.
    • Minor layout and style tweaks across media cards, loading states, and sign-in prompts.
  • Localization

    • English and Romanian quiz strings added; calendar wording changed to “activity/activities”.
  • Backend

    • Quiz storage, attempt APIs, generation job, and streak-counting updates.

Review Change Stack

@vercel

vercel Bot commented May 12, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
news Error Error May 12, 2026 1:51pm
news-web Ready Ready Preview, Comment May 12, 2026 1:51pm

@coderabbitai

coderabbitai Bot commented May 12, 2026

Copy link
Copy Markdown
Contributor

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 44a6a29d-9a17-4b85-bbfd-c4e2b8e2c7b8

📥 Commits

Reviewing files that changed from the base of the PR and between f775639 and b4603d9.

📒 Files selected for processing (4)
  • apps/web/src/components/early-access-apply-card.tsx
  • apps/web/src/routes/__root.tsx
  • packages/backend/convex/quiz.ts
  • packages/backend/convex/quizNode.ts

Walkthrough

This PR adds a full daily news quiz: Convex schema and APIs, a server-side generation action using OpenAI with strict schema validation and sanitization, scheduling and streak integration, a frontend /quiz route with grading/submission/share/review UI, navigation and i18n wiring (desktop and mobile), and several UI/type polish changes.

Changes

Daily News Quiz Feature

Layer / File(s) Summary
Backend data schema and types
packages/backend/convex/schema.ts, packages/backend/convex/lib/aiCall.ts
New dailyQuizzes and quizAttempts tables store multilingual question structures and user attempts; AICallType adds "quiz_generation".
Backend quiz operations and API
packages/backend/convex/quiz.ts
Exports: getTodayQuiz, getMyTodayAttempt, submitQuizAttempt, gradeQuizQuestion; includes internal helpers getQuizGenerationInput and replaceDailyQuiz, builds reviews and updates streak stats.
Backend quiz generation engine
packages/backend/convex/quizNode.ts
Internal action generateDailyQuiz: builds prompt, enforces JSON schema on model output, sanitizes/validates questions against events/sources, computes input signature to skip identical runs, and persists ready/failed quizzes.
Backend integration and scheduling
packages/backend/convex/interactions.ts, packages/backend/convex/crons.ts, packages/backend/convex/lib/streaks.ts, packages/backend/convex/lib/userProfile.ts
Centralized streak computation (computeStreakUpdate), user-profile helpers moved to lib/userProfile, getDashboardOverview now aggregates quizAttempts per UTC day into activityCount, and a daily cron triggers generation at 06:00 UTC.
Frontend navigation and i18n wiring
apps/web/src/components/header.tsx, apps/web/src/components/layout/MobileTabBar.tsx, apps/web/src/lib/i18n/strings.ts
Adds /quiz to header and mobile tab bar with BrainCircuit icon, expands mobile tabs grid to 5 columns, and adds tabs.quiz and quiz.* localization keys in Romanian and English; calendar wording updated to “activity/activities”.
Frontend /quiz route and interactive experience
apps/web/src/routes/quiz.tsx, apps/web/src/lib/i18n/strings.ts
New /quiz route with QuizRoute loader and QuizExperience: per-question grading, submission handling, sharing (native/clipboard), results and per-question review UI, and localized route metadata.
Frontend quiz CTAs & streak calendar
apps/web/src/routes/activitate.tsx, apps/web/src/routes/feed.tsx, apps/web/src/components/streak-activity-calendar.tsx
Adds quiz CTA banners in activity dashboard and feed linking to /quiz and updates streak calendar cells to use activityCount (views + quiz attempts) with updated labels/tooltips.
Type annotations & small polish
packages/backend/convex/clustering.ts, packages/backend/convex/enrichmentNode.ts, apps/web/src/lib/auth-redirect.ts
Adds explicit parameter typings in several reduce/map callbacks and extends AuthRedirectPath to include "/quiz".
Misc frontend components & tooling
apps/web/src/components/early-access-apply-card.tsx, apps/web/src/components/*, .vscode/tasks.json
Adds EarlyAccessApplyCard component, minor Tailwind fixes (aspect ratios, gradients, truncation), sign-in typing tweak, lazy devtools loading, user-menu width change, and a VS Code web build task.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • flvvius/news#29: Likely the same daily-quiz feature implementation across backend and frontend.
  • flvvius/news#23: Related changes to interactions/streak handling used by dashboard aggregation.
  • flvvius/news#26: Related frontend auth and devtools changes overlapping auth UI and __root.tsx.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'quiz generation' directly summarizes the main change: a comprehensive new quiz feature with generation, storage, and user-facing quiz experience.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/quiz

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 8

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/src/routes/quiz.tsx`:
- Around line 357-380: The feedback container rendered when currentFeedback
exists should be made accessible by adding ARIA live region attributes to the
wrapping div (the element that currently uses cn(...) and renders the
CheckCircle2/XCircle and localize(currentFeedback.explanation, locale)); update
that div to include aria-live="polite" and role="status" (and optionally
aria-atomic="true") so screen readers will announce the success/error text when
currentFeedback changes.

In `@packages/backend/convex/interactions.ts`:
- Around line 813-817: The returned object currently exposes readCount but it
actually sums article views and quiz attempts; rename this field to a clearer
name like activityCount or dailyActivityCount in the return value (in the
function that currently returns { timestamp, readCount: (activeSet?.size ?? 0) +
quizCount, isToday }) and update all related type/interface definitions, usages,
and tests in the codebase (e.g., any callers expecting readCount) to the new
field name to keep types and runtime behavior consistent; alternatively, if you
prefer separate metrics, return both readCount: (activeSet?.size ?? 0) and
quizCount as distinct fields and update types/usages accordingly.
- Around line 708-713: The quizAttempts query currently collects all records via
ctx.db.query("quizAttempts").withIndex("by_user_date", (q) => q.eq("userId",
user._id)).order("desc").collect(), which can load unbounded history; restrict
it by either adding a take() (e.g., .take(200)) or by filtering to a recent date
range (compute a cutoff like 90 days ago and change the index/filter to
.withIndex("by_user_date", q => q.eq("userId", user._id).gte("date", cutoff)) or
equivalent) so the dashboard only fetches recent attempts (>=84 days) and avoids
loading thousands of rows.

In `@packages/backend/convex/quiz.ts`:
- Around line 117-157: getUserProfileByAuthUserId and
ensureUserProfileForAuthUser are duplicated here and in interactions.ts; extract
them into a single shared module (e.g., lib/userProfile.ts) that exports
getUserProfileByAuthUserId and ensureUserProfileForAuthUser, keep the same
signatures (using QueryCtx/MutationCtx and authUser shape) and logic (including
normalizeEmail and inserting userStats), then replace the local definitions in
both quiz.ts and interactions.ts with imports from that new module and update
callers to use the imported functions so there is one source of truth.
- Around line 159-205: The streak calculation in updateUserStatsForDailyQuiz
duplicates the logic found in updateUserStatsForView (interactions.ts); extract
that core logic into a shared helper (e.g., calculateNextStreak or
computeStreakUpdate) that accepts the current stats record and a timestamp
(completedAt), returns the new currentStreak, longestStreak delta or value, and
the updated lastActiveAt, then replace the duplicated block in
updateUserStatsForDailyQuiz and the corresponding block in
updateUserStatsForView to call this helper; ensure the helper lives in a common
util module, update imports, and preserve existing behavior (handling undefined
lastActiveAt, startOfUtcDay and DAY_MS comparisons) and unit tests.
- Around line 389-406: In the candidates.map async handler, avoid fetching
claims and articles until you confirm the event is published: first await
ctx.db.get(preview.eventId) into the existing event variable, check if (!event
|| event.status !== "published") and return null immediately, and only if
published run the two ctx.db.query(...) calls to load claims and articles;
update the Promise.all structure so the initial db.get is awaited separately
before running the parallel queries for "eventClaims" and "articles".
- Around line 306-315: The code currently returns the previous attempt when an
existingAttempt is found (using buildReview(quiz, existingAttempt.answers)),
which silently ignores the new args.answers; change this to either reject
duplicate submissions by throwing a ConvexError (e.g., throw new
ConvexError("You have already completed this quiz")) or explicitly update the
stored attempt with the new answers before returning (update existingAttempt
with args.answers and recompute buildReview(quiz, updatedAnswers)); locate the
handling around existingAttempt and buildReview in the quiz submission mutation
and implement one of these behaviors so the new submission is not silently
dropped.

In `@packages/backend/convex/quizNode.ts`:
- Line 285: Extract the magic number 0.45 used in the tokenOverlap check into a
named constant (e.g., TOKEN_OVERLAP_THRESHOLD) near the top of the module or the
surrounding function, add a brief comment explaining why 0.45 was chosen (or how
it was derived), and replace the literal in the tokenOverlap(normalizedClaim,
normalizedText) >= 0.45 expression with the constant; ensure the constant name
and comment are placed close to related helpers (e.g., tokenOverlap) so future
maintainers can find and adjust it.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7fe4bd0a-6cd8-46d6-bce9-693a1f896827

📥 Commits

Reviewing files that changed from the base of the PR and between e5e3561 and f0704e0.

⛔ Files ignored due to path filters (2)
  • apps/web/src/routeTree.gen.ts is excluded by !**/routeTree.gen.ts
  • packages/backend/convex/_generated/api.d.ts is excluded by !**/_generated/**, !**/_generated/**
📒 Files selected for processing (14)
  • apps/web/src/components/header.tsx
  • apps/web/src/components/layout/MobileTabBar.tsx
  • apps/web/src/lib/i18n/strings.ts
  • apps/web/src/routes/activitate.tsx
  • apps/web/src/routes/feed.tsx
  • apps/web/src/routes/quiz.tsx
  • packages/backend/convex/clustering.ts
  • packages/backend/convex/crons.ts
  • packages/backend/convex/enrichmentNode.ts
  • packages/backend/convex/interactions.ts
  • packages/backend/convex/lib/aiCall.ts
  • packages/backend/convex/quiz.ts
  • packages/backend/convex/quizNode.ts
  • packages/backend/convex/schema.ts

Comment thread apps/web/src/routes/quiz.tsx
Comment thread packages/backend/convex/interactions.ts
Comment thread packages/backend/convex/interactions.ts
Comment thread packages/backend/convex/quiz.ts Outdated
Comment thread packages/backend/convex/quiz.ts
Comment thread packages/backend/convex/quiz.ts
Comment thread packages/backend/convex/quiz.ts Outdated
Comment thread packages/backend/convex/quizNode.ts Outdated

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 8

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/src/routes/quiz.tsx`:
- Around line 162-165: The component must not render the interactive quiz while
the Convex query existingAttempt (from useQuery(api.quiz.getMyTodayAttempt,
...)) is still loading (it is undefined for authenticated users); update the
render logic to gate the quiz UI: if isAuthenticated && existingAttempt ===
undefined, return a loading state (or null) instead of treating it as “no
attempt”. Apply this same guard wherever the code later assumes existingAttempt
(the block around the later quiz rendering/submit logic) so the UI only allows
answering/submitting once existingAttempt is resolved.

In `@packages/backend/convex/quiz.ts`:
- Around line 273-301: gradeQuizQuestion currently returns correctChoiceId
allowing clients to learn answers and then cheat when calling submitQuizAttempt;
remove correctChoiceId (and any other answer-key fields like isCorrect) from
gradeQuizQuestion's response and instead implement server-side grading inside
submitQuizAttempt (or require a persisted attempt record) so the server computes
correctness from the authoritative quiz data and the submitted answers. Update
gradeQuizQuestion to only return non-sensitive metadata (questionId,
explanation/attribution only if safe) and modify submitQuizAttempt to fetch the
quiz (from ctx.db.get using quizId), compare each submitted choice against
question.correctChoiceId on the server, compute score/streak, persist the
attempt, and ignore any client-supplied correctness fields.
- Around line 423-458: The replaceDailyQuiz handler currently overwrites an
existing ready quiz with a failed rerun; modify the handler in replaceDailyQuiz
so that when an existing row exists and existing.status === "ready" and
args.status === "failed" you do not clobber the published quiz data—either
return early (no patch) or patch only non-destructive fields (e.g., update
lastError and generatedAt but leave status, questions, and publishedAt intact).
Concretely, detect the condition after loading existing and before
building/applying row, and when it matches, skip updating
status/questions/publishedAt (or simply return { quizId: existing._id, replaced:
false } or similar), otherwise continue with the existing patch/insert logic;
reference replaceDailyQuiz, existing, row, publishedAt, questions, and lastError
when making the change.
- Around line 217-259: The server currently filters invalid answers into
normalizedAnswers but doesn't verify the caller answered every question once,
allowing partial submissions to be saved and block future attempts; after
computing normalizedAnswers (and/or before inserting into quizAttempts) check
that normalizedAnswers.length === quiz.questions.length and that every
questionId from quiz.questions is present exactly once (you can use
validQuestionIds and compare sets), and if not, return the same non-saved
response shape used for unauthenticated users (saved: false, completedAt:
Date.now(), quizId: quiz._id, dateKey: quiz.dateKey, ...result) instead of
inserting; ensure this check happens before creating existingAttempt/ inserting
into ctx.db.insert("quizAttempts") so partial attempts are never persisted.

In `@packages/backend/convex/quizNode.ts`:
- Around line 492-523: callOpenAI(...) can throw and currently that exception
bypasses the failure persistence; wrap the call to callOpenAI in a try/catch
around the existing callOpenAI<RawQuizResponse> invocation and in the catch
block call ctx.runMutation(internal.quiz.replaceDailyQuiz, { dateKey, status:
"failed", questions: [], sourceEventIds, inputSignature, model: settings.model,
lastError: errorOrMessage }) to persist the failed row (include the caught error
message/details), then return the same failure shape ({ status: "failed",
reason: errorOrMessage, questionCount: 0 }) or rethrow if desired; ensure you
reference callOpenAI, internal.quiz.replaceDailyQuiz, dateKey, inputSignature
and preserve existing fields (model, sourceEventIds) when recording the failure.
- Around line 399-431: The attribution fields are still taken directly from the
model output (raw.sourceNames[0], raw.sourceUrl) even though you validated
sources via eventSourceNames and computed sourceIds; change attribution to use
the validated event source instead: build a lookup of event.sources by _id (or
find the matching source by sourceIds[0]) and set attribution.sourceName and
attribution.sourceUrl from that event source (trimmed) or undefined if none,
rather than using raw.sourceNames/raw.sourceUrl; update the sanitized push where
attribution is set (reference: eventSourceNames, sourceIds, sanitized,
raw.sourceNames, raw.sourceUrl) to derive attribution from the matched event
source.
- Around line 589-604: Compute targetQuestions before minQuestions and ensure
minQuestions is clamped to not exceed targetQuestions; either call safeInteger
for minQuestions using targetQuestions as the max argument or, after both values
are computed, set minQuestions = Math.min(minQuestions, targetQuestions). Update
the QuizGenerationSettings construction (symbols: QuizGenerationSettings,
settings, targetQuestions, minQuestions, safeInteger,
quiz_generation_target_questions, quiz_generation_min_questions) so minQuestions
can never be greater than targetQuestions.
- Around line 386-409: The sanitization currently only rejects duplicate choice
text; add checks to reject duplicate question IDs and duplicate choice IDs: for
each raw, ensure that if raw.id exists it is not already seen by maintaining a
questionIds Set (and if raw.id is missing, generate an id and ensure it is
unique against questionIds), and ensure the set of raw.choices.map(c=>c.id) has
the same size as raw.choices.length to detect duplicate choice.id values; if
either duplicate-question-id or duplicate-choice-id is detected, skip/continue
and do not push to sanitized. Update code around choiceIds, raw.id, and the
point before sanitized.push to perform these checks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 27149d19-8dfa-45e2-8dfd-6e8de7519aed

📥 Commits

Reviewing files that changed from the base of the PR and between f0704e0 and 97090e7.

⛔ Files ignored due to path filters (1)
  • packages/backend/convex/_generated/api.d.ts is excluded by !**/_generated/**, !**/_generated/**
📒 Files selected for processing (7)
  • apps/web/src/components/streak-activity-calendar.tsx
  • apps/web/src/routes/quiz.tsx
  • packages/backend/convex/interactions.ts
  • packages/backend/convex/lib/streaks.ts
  • packages/backend/convex/lib/userProfile.ts
  • packages/backend/convex/quiz.ts
  • packages/backend/convex/quizNode.ts

Comment thread apps/web/src/routes/quiz.tsx
Comment thread packages/backend/convex/quiz.ts
Comment thread packages/backend/convex/quiz.ts
Comment thread packages/backend/convex/quiz.ts
Comment thread packages/backend/convex/quizNode.ts
Comment thread packages/backend/convex/quizNode.ts
Comment thread packages/backend/convex/quizNode.ts Outdated
Comment thread packages/backend/convex/quizNode.ts

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/src/components/early-access-apply-card.tsx`:
- Around line 67-73: The onSubmit handler allows whitespace-only emails to
proceed; compute a normalized email (const normalizedEmail =
email.trim().toLowerCase()) inside the onSubmit, guard with if
(!normalizedEmail) { setMessage("Please enter a valid email"); return; } and
only then call addToWaitlist.mutate with email: normalizedEmail and name:
name.trim() || undefined; this prevents sending empty emails and provides user
feedback.
- Around line 108-115: The status paragraph that uses addToWaitlist.isError to
set role should also include an explicit aria-live attribute for consistent
screen reader announcements; update the <p> (the status region controlled by
addToWaitlist.isError) to add aria-live="assertive" when addToWaitlist.isError
is true and aria-live="polite" (or "off") when it's a non-error status,
preserving the existing role logic so screen readers receive correct live region
semantics.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6b24ba1c-5e63-491f-bb34-bf3e56bf41a3

📥 Commits

Reviewing files that changed from the base of the PR and between 97090e7 and f775639.

📒 Files selected for processing (16)
  • .vscode/tasks.json
  • apps/web/src/components/SignInPrompt.tsx
  • apps/web/src/components/early-access-apply-card.tsx
  • apps/web/src/components/feed/event-card.tsx
  • apps/web/src/components/sign-in-form.tsx
  • apps/web/src/components/ui/page-loading-state.tsx
  • apps/web/src/components/user-menu.tsx
  • apps/web/src/lib/auth-redirect.ts
  • apps/web/src/routes/event.$slug.tsx
  • apps/web/src/routes/feed.tsx
  • apps/web/src/routes/quiz.tsx
  • apps/web/src/routes/source.$sourceId.tsx
  • packages/backend/convex/interactions.ts
  • packages/backend/convex/lib/streaks.ts
  • packages/backend/convex/quiz.ts
  • packages/backend/convex/quizNode.ts

Comment thread apps/web/src/components/early-access-apply-card.tsx
Comment thread apps/web/src/components/early-access-apply-card.tsx
@flvvius
flvvius merged commit 902cb8e into main May 12, 2026
2 of 4 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Jul 4, 2026
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.

1 participant