add quiz and progress - #16
Conversation
📝 WalkthroughWalkthroughAdds roadmap node ordering, persisted quiz and completion data, AI-generated French quizzes, pass-based unlocking, progress-aware node visuals, and a dialog-driven quiz interaction flow. ChangesRoadmap progress and quizzes
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Learner
participant RoadmapDisplaySection
participant NodeActionDialog
participant getOrCreateNodeQuiz
participant submitNodeQuiz
Learner->>RoadmapDisplaySection: select unlocked roadmap node
RoadmapDisplaySection->>NodeActionDialog: open node menu
NodeActionDialog->>getOrCreateNodeQuiz: load node quiz
getOrCreateNodeQuiz-->>NodeActionDialog: return questions
Learner->>NodeActionDialog: submit answers
NodeActionDialog->>submitNodeQuiz: evaluate answers
submitNodeQuiz-->>NodeActionDialog: return score and pass status
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration. 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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 7
🧹 Nitpick comments (5)
src/modules/roadmap/stores/quiz-ui-store.ts (1)
41-47: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
isSubmittingisn't reset on close.Every other field is cleared here and in
backToMenu, butisSubmittingis left as-is. It's recovered by thefinallyinhandleSubmittoday; resetting it here makes the store self-consistent regardless of caller.🤖 Prompt for 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. In `@src/modules/roadmap/stores/quiz-ui-store.ts` around lines 41 - 47, Update closeDialog to reset isSubmitting to false alongside the other cleared quiz UI state, matching the reset behavior in backToMenu and keeping the store consistent regardless of how the dialog closes.convex/roadmap/action.ts (2)
132-136: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winNo error handling or timeout around the model call.
Unlike
generateQuestionsAction/submitAnswersAndGenerateRoadmap, this path lets provider errors propagate raw to the client. The dialog's.then(...).finally(...)has no.catch, so a failure leaves an empty quiz with no feedback. Wrap in try/catch and throw aConvexErrorwith a user-facing French message.🤖 Prompt for 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. In `@convex/roadmap/action.ts` around lines 132 - 136, Wrap the generateObject call in the quiz-generation flow with try/catch, including an appropriate timeout consistent with the existing model-call patterns. Catch provider failures and throw a ConvexError containing a clear user-facing French message so the dialog receives actionable feedback instead of a raw error.
82-92: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winConstrain
correctIndexto an integer.
z.number().min(0).max(3)accepts non-integer values like1.5; switch toz.int().min(0).max(3)so the generated quiz only accepts a valid option index.♻️ Proposed change
- correctIndex: z.number().min(0).max(3), + correctIndex: z.int().min(0).max(3),🤖 Prompt for 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. In `@convex/roadmap/action.ts` around lines 82 - 92, Update the correctIndex field in quizSchema to use Zod’s integer validator while preserving the existing 0–3 bounds, so only valid option indices are accepted.convex/roadmap/mutation.ts (1)
57-73: 🗄️ Data Integrity & Integration | 🔵 Trivial | 💤 Low valueScope the completion lookup by user.
The
by_roadmap_and_nodelookup ignoresuserIdeven though the inserted row carries one. It works today only because a roadmap has a single owner; if roadmaps ever become shared, one user's completion silently suppresses another's. Consider an index keyed onuserIdtoo.🤖 Prompt for 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. In `@convex/roadmap/mutation.ts` around lines 57 - 73, Update the completion lookup in the passed branch to scope roadmapNodeProgress by userId as well as roadmapId and nodeId. Use an index and query predicate that include identity.subject, and update the schema/index definition if needed; preserve the existing insert and duplicate-prevention behavior for each individual user.src/modules/roadmap/ui/components/node-action-dialog.tsx (1)
207-230: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAnswer options lack radio-group semantics.
Each option is a plain
<button>; the selected state is conveyed only by border/background colour, so assistive tech can't announce which option is chosen, and there's no grouping between a question and its options. Userole="radiogroup"withrole="radio"+aria-checked, and label the group with the question.♻️ Sketch
- <div key={questionIndex} className="flex flex-col gap-y-2"> - <p className="text-sm font-medium"> + <fieldset key={questionIndex} className="flex flex-col gap-y-2"> + <legend className="text-sm font-medium"> {questionIndex + 1}. {q.question} - </p> - <div className="flex flex-col gap-y-1.5"> + </legend> + <div role="radiogroup" className="flex flex-col gap-y-1.5"> {q.options.map((option, optionIndex) => ( <button key={optionIndex} type="button" + role="radio" + aria-checked={answers[questionIndex] === optionIndex} onClick={() => setAnswer(questionIndex, optionIndex)}🤖 Prompt for 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. In `@src/modules/roadmap/ui/components/node-action-dialog.tsx` around lines 207 - 230, Update the question/options markup in the questions.map block to expose radio-group semantics: label each options container with its question, assign role="radiogroup" to that container, and assign role="radio" with aria-checked reflecting answers[questionIndex] to each option button. Preserve the existing selection handler and visual styling.
🤖 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 `@convex/roadmap/internal.ts`:
- Around line 22-35: The saveNodeQuiz mutation must be idempotent for each
roadmapId/nodeId pair: in convex/roadmap/internal.ts lines 22-35, query the
by_roadmap_and_node index and return the existing record’s _id before inserting;
in convex/roadmap/action.ts lines 117-145, preserve the fast-path read but use
the mutation’s returned id rather than assuming a new row was inserted.
In `@convex/roadmap/mutation.ts`:
- Around line 30-46: Enforce ordered unlocking on both server entry points: in
convex/roadmap/mutation.ts at lines 30-46, derive the unlocked set using the
same logic as getRoadmapProgress and reject nodeId before scoring when it is not
unlocked; in convex/roadmap/action.ts at lines 107-120, apply the same check
before invoking the model so locked nodes cannot trigger generation. Preserve
the existing ownership and quiz validation.
In `@convex/roadmap/progress.ts`:
- Around line 11-16: Update saveNodeQuiz to query the by_roadmap_and_node index
for an existing quiz matching roadmapId and nodeId, return that record without
inserting when found, and insert only when no record exists so concurrent calls
do not create duplicate cached quizzes.
In `@src/modules/my-roadmap/ui/sections/roadmap-display-section.tsx`:
- Around line 59-78: Update both direct onNodeClick calls in handleNodeClick to
pass a narrowed RoadmapNodeSelection-shaped object with node.id and a data.label
string, rather than the full `@xyflow/react` Node. Apply this in the completedSet
branch and the fallback branch while preserving the existing menu behavior and
click flow.
In `@src/modules/roadmap/lib/roadmap-node-selection.ts`:
- Line 1: Update the stale source path reference associated with
roadmap-node-selection.ts so it uses src/modules/roadmap/lib/ rather than
src/modules/my-roadmap/lib/. Preserve the file name and remaining path
unchanged.
In `@src/modules/roadmap/ui/components/node-action-dialog.tsx`:
- Around line 60-69: Add error state to the dialog component and handle failures
in both the quiz-loading promise after getOrCreateNodeQuiz and the handleSubmit
try block. Capture each error, update the error state with a user-visible
message, and retain the existing loading cleanup in finally so failures do not
leave unhandled rejections or silent empty states.
- Around line 53-58: Update the quiz state reset in the useEffect so result is
cleared whenever entering quiz mode, before the quizId early-return guard can
skip it. Preserve the existing quizId loading guard and ensure re-entering the
quiz renders questions instead of the previous score screen.
---
Nitpick comments:
In `@convex/roadmap/action.ts`:
- Around line 132-136: Wrap the generateObject call in the quiz-generation flow
with try/catch, including an appropriate timeout consistent with the existing
model-call patterns. Catch provider failures and throw a ConvexError containing
a clear user-facing French message so the dialog receives actionable feedback
instead of a raw error.
- Around line 82-92: Update the correctIndex field in quizSchema to use Zod’s
integer validator while preserving the existing 0–3 bounds, so only valid option
indices are accepted.
In `@convex/roadmap/mutation.ts`:
- Around line 57-73: Update the completion lookup in the passed branch to scope
roadmapNodeProgress by userId as well as roadmapId and nodeId. Use an index and
query predicate that include identity.subject, and update the schema/index
definition if needed; preserve the existing insert and duplicate-prevention
behavior for each individual user.
In `@src/modules/roadmap/stores/quiz-ui-store.ts`:
- Around line 41-47: Update closeDialog to reset isSubmitting to false alongside
the other cleared quiz UI state, matching the reset behavior in backToMenu and
keeping the store consistent regardless of how the dialog closes.
In `@src/modules/roadmap/ui/components/node-action-dialog.tsx`:
- Around line 207-230: Update the question/options markup in the questions.map
block to expose radio-group semantics: label each options container with its
question, assign role="radiogroup" to that container, and assign role="radio"
with aria-checked reflecting answers[questionIndex] to each option button.
Preserve the existing selection handler and visual styling.
🪄 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: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: f55cbf31-a981-4b48-b3ef-a550bcc5a055
⛔ Files ignored due to path filters (1)
convex/_generated/api.d.tsis excluded by!**/_generated/**
📒 Files selected for processing (18)
convex/lib/nodeOrder.tsconvex/roadmap/action.tsconvex/roadmap/internal.tsconvex/roadmap/mutation.tsconvex/roadmap/progress.tsconvex/roadmap/queries.tsconvex/schema.tssrc/app/globals.csssrc/modules/my-roadmap/ui/sections/learn-chat-section.tsxsrc/modules/my-roadmap/ui/sections/roadmap-display-section.tsxsrc/modules/my-roadmap/ui/views/roadmap-learn-view.tsxsrc/modules/roadmap/lib/build-roadmap-flow.tssrc/modules/roadmap/lib/roadmap-flow-node-animation.tssrc/modules/roadmap/lib/roadmap-node-selection.tssrc/modules/roadmap/stores/quiz-ui-store.tssrc/modules/roadmap/ui/components/node-action-dialog.tsxsrc/modules/roadmap/ui/components/roadmap-flow-nodes.tsxsrc/modules/roadmap/ui/components/roadmap-flow.tsx
| export const saveNodeQuiz = internalMutation({ | ||
| args: { | ||
| roadmapId: v.id("roadmaps"), | ||
| nodeId: v.string(), | ||
| questions: v.array(quizQuestionValidator), | ||
| }, | ||
| handler: async (ctx, { roadmapId, nodeId, questions }) => { | ||
| return await ctx.db.insert("roadmapNodeQuizzes", { | ||
| roadmapId, | ||
| nodeId, | ||
| questions, | ||
| }); | ||
| }, | ||
| }); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🔴 Critical | 🏗️ Heavy lift
Non-idempotent saveNodeQuiz lets concurrent quiz requests create duplicate rows. The action reads and writes in two separate transactions, so overlapping requests for the same (roadmapId, nodeId) both miss and both insert; afterwards getNodeQuiz's .unique() throws permanently for that node.
convex/roadmap/internal.ts#L22-L35: re-queryby_roadmap_and_nodeinsidesaveNodeQuizand return the existing_idinstead of inserting.convex/roadmap/action.ts#L117-L145: keep the fast-path read, but rely on the idempotent mutation's returned id rather than assuming the insert created a fresh row.
📍 Affects 2 files
convex/roadmap/internal.ts#L22-L35(this comment)convex/roadmap/action.ts#L117-L145
🤖 Prompt for 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.
In `@convex/roadmap/internal.ts` around lines 22 - 35, The saveNodeQuiz mutation
must be idempotent for each roadmapId/nodeId pair: in convex/roadmap/internal.ts
lines 22-35, query the by_roadmap_and_node index and return the existing
record’s _id before inserting; in convex/roadmap/action.ts lines 117-145,
preserve the fast-path read but use the mutation’s returned id rather than
assuming a new row was inserted.
| handler: async (ctx, { roadmapId, nodeId, quizId, answers }) => { | ||
| const identity = await ctx.auth.getUserIdentity(); | ||
| if (!identity) throw new Error("Non authentifié"); | ||
|
|
||
| const roadmap = await ctx.db.get(roadmapId); | ||
| if (!roadmap || roadmap.userId !== identity.subject) { | ||
| throw new Error("Introuvable"); | ||
| } | ||
|
|
||
| const quiz = await ctx.db.get(quizId); | ||
| if (!quiz || quiz.roadmapId !== roadmapId || quiz.nodeId !== nodeId) { | ||
| throw new Error("Quiz introuvable"); | ||
| } | ||
|
|
||
| if (answers.length !== quiz.questions.length) { | ||
| throw new Error("Réponses incomplètes"); | ||
| } |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift
Ordered unlocking is enforced only in the UI. Both server entry points verify roadmap ownership but never check that nodeId exists in the roadmap and is currently unlocked, so a direct API call can generate and pass quizzes for arbitrary future nodes.
convex/roadmap/mutation.ts#L30-L46: before scoring, derive the unlocked set (same logic backinggetRoadmapProgress) and rejectnodeIds that aren't unlocked.convex/roadmap/action.ts#L107-L120: apply the same unlocked-node check before calling the model, so locked nodes can't trigger paid generation.
📍 Affects 2 files
convex/roadmap/mutation.ts#L30-L46(this comment)convex/roadmap/action.ts#L107-L120
🤖 Prompt for 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.
In `@convex/roadmap/mutation.ts` around lines 30 - 46, Enforce ordered unlocking
on both server entry points: in convex/roadmap/mutation.ts at lines 30-46,
derive the unlocked set using the same logic as getRoadmapProgress and reject
nodeId before scoring when it is not unlocked; in convex/roadmap/action.ts at
lines 107-120, apply the same check before invoking the model so locked nodes
cannot trigger generation. Preserve the existing ownership and quiz validation.
| // Quiz généré une seule fois par nœud (mis en cache, jamais régénéré) | ||
| export const roadmapNodeQuizzes = defineTable({ | ||
| roadmapId: v.id("roadmaps"), | ||
| nodeId: v.string(), | ||
| questions: v.array(quizQuestionValidator), | ||
| }).index("by_roadmap_and_node", ["roadmapId", "nodeId"]); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Make quiz persistence idempotent.
The index does not enforce uniqueness, while the supplied saveNodeQuiz mutation always inserts. Concurrent generation can create multiple cached quizzes for one node, making retrieval ambiguous. Query by by_roadmap_and_node and return the existing record before inserting, within the same mutation.
🤖 Prompt for 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.
In `@convex/roadmap/progress.ts` around lines 11 - 16, Update saveNodeQuiz to
query the by_roadmap_and_node index for an existing quiz matching roadmapId and
nodeId, return that record without inserting when found, and insert only when no
record exists so concurrent calls do not create duplicate cached quizzes.
| const handleNodeClick = (node: Node) => { | ||
| const locked = Boolean(node.data.locked); | ||
| if (locked) return; | ||
|
|
||
| if (node.type === "topic" || node.type === "choice") { | ||
| const label = String(node.data.label ?? ""); | ||
|
|
||
| if (completedSet.has(node.id)) { | ||
| // Déjà validé : plus besoin du menu, on va direct à l'explication. | ||
| onNodeClick(node); | ||
| return; | ||
| } | ||
|
|
||
| openNodeMenu(node.id, label); | ||
| return; | ||
| } | ||
|
|
||
| // "option" et "center" : jamais gatés, comportement normal | ||
| onNodeClick(node); | ||
| }; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Confirm whether tsc flags these assignments and inspect the Node data typing in use.
fd -t f 'tsconfig*.json' --exec cat {}
rg -nP --type=ts -C3 'RoadmapNodeSelection' src
rg -nP --type=ts -C5 'data\.label' src/modules/roadmap/libRepository: MiradoAndreas/learning-plateform-ai
Length of output: 5851
🏁 Script executed:
#!/bin/bash
set -u
echo "== roadmap-display-section outline =="
ast-grep outline src/modules/my-roadmap/ui/sections/roadmap-display-section.tsx --view expanded || true
echo "== roadmap-display-section relevant =="
sed -n '1,120p' src/modules/my-roadmap/ui/sections/roadmap-display-section.tsx | cat -n
echo "== roadmap-node-selection =="
cat -n src/modules/roadmap/lib/roadmap-node-selection.ts
echo "== onNodeClick usages in roadmap-learn-view =="
sed -n '1,80p' src/modules/my-roadmap/ui/views/roadmap-learn-view.tsx | cat -n
echo "== node labels and data in roadmap =="
rg -nP --type=ts -C4 'label|data:' src/modules my-roadmap 2>/dev/null || true
echo "== `@xyflow/react` deps =="
for f in package.json **/package.json; do [ -f "$f" ] && { echo "--- $f"; jq -c '.dependencies, .devDependencies' "$f" | rg -n '`@xyflow/react`|xyflow' || true; }; done
fd -t f 'package.json' -x sh -c 'echo "--- $1"; jq -c "\(.dependencies // {}) + \(.devDependencies // {})" "$1" | rg -n "`@xyflow/react`|xyflow" || true' sh {}Repository: MiradoAndreas/learning-plateform-ai
Length of output: 50392
🏁 Script executed:
#!/bin/bash
set -u
echo "== roadmap-display-section outline =="
ast-grep outline src/modules/my-roadmap/ui/sections/roadmap-display-section.tsx --view expanded || true
echo "== roadmap-display-section relevant =="
sed -n '1,120p' src/modules/my-roadmap/ui/sections/roadmap-display-section.tsx | cat -n
echo "== roadmap-node-selection =="
cat -n src/modules/roadmap/lib/roadmap-node-selection.ts
echo "== learn-view node click relevant =="
sed -n '1,80p' src/modules/my-roadmap/ui/views/roadmap-learn-view.tsx | cat -n
echo "== onNodeClick/callgraph-ish usages in roadmap sections and views =="
rg -n --type=ts 'onNodeClick|handleNodeClick|RoadmapNodeSelection' src/modules/my-roadmap src/modules/roadmap 2>/dev/null | sed -n '1,160p'
echo "== package versions =="
python3 - <<'PY'
import json, pathlib
for p in pathlib.Path('.').glob('**/package.json'):
if 'node_modules' in p.parts:
continue
try:
data=json.loads(p.read_text())
deps={**data.get('dependencies',{}), **data.get('devDependencies',{})}
if any(k.startswith('`@xyflow/`') for k in deps):
print(f'--- {p}')
for k,v in deps.items():
if k.startswith('`@xyflow/`'):
print(f'{k}: {v}')
except Exception as e:
...
PY
echo "== check local lock/installed xyflow typing if present =="
fd -t f 'package-lock.json|pnpm-lock.yaml|yarn.lock|bun.lock' -x sh -c 'echo "--- $1"; rg -n "`@xyflow/react`|`@xyflow/system`" "$1" | sed -n "1,80p"' sh {}
find . -path '*/node_modules/@xyflow/*' -maxdepth 4 2>/dev/null | head -80Repository: MiradoAndreas/learning-plateform-ai
Length of output: 10758
🏁 Script executed:
#!/bin/bash
set -u
echo "== roadmap-flow relevant =="
sed -n '1,100p' src/modules/roadmap/ui/components/roadmap-flow.tsx | cat -n
echo "== package manager files =="
ls -la package.json pnpm-lock.yaml 2>/dev/null | sed -n '1,40p'
echo "== xyflow typing in lock entries =="
python3 - <<'PY'
from pathlib import Path
p=Path('pnpm-lock.yaml')
text=p.read_text().splitlines()
for i,line in enumerate(text):
if line.startswith(' `@xyflow/react`@12.11.2'):
print('\n-- `@xyflow/react`@12.11.2 block --')
for j in range(i, min(i+40, len(text))):
print(f'{j+1}: {text[j]}')
break
for i,line in enumerate(text):
if line.startswith(' `@xyflow/system`@0.0.79'):
print('\n-- `@xyflow/system`@0.0.79 block --')
for j in range(i, min(i+40, len(text))):
print(f'{j+1}: {text[j]}')
break
PY
echo "== Node data declaration in installed xyflow if present =="
python3 - <<'PY'
from pathlib import Path
roots=[Path('node_modules')]
for root in roots:
if not root.exists():
continue
for p in Path(root).glob('`@xyflow/react`*/dist/**/*.d.*'):
if 'xy' in str(p).lower():
continue
for p in list(root.rglob('types.d.ts')) + list(root.rglob('index.d.ts')):
if '`@xyflow`' not in str(p):
continue
txt=p.read_text(errors='ignore')
if 'interface Node' in txt or 'type Node' in txt:
print(f'-- {p} --')
for i,line in enumerate(txt.splitlines(), 1):
if 'interface Node' in line or 'type Node' in line:
for j in range(max(1,i-20), min(i+80, len(txt.splitlines())+1)):
print(f'{j}: {txt.splitlines()[j-1]}')
break
PYRepository: MiradoAndreas/learning-plateform-ai
Length of output: 2566
🌐 Web query:
@xyflow/react v12.11.2 Node type data Record<string, unknown>
💡 Result:
In @xyflow/react v12.11.2, the Node type is defined with a generic constraint on its data property: NodeData extends Record<string, unknown> = Record<string, unknown> [1][2]. This means the data object must be an object-like structure that satisfies the Record<string, unknown> constraint [3]. Key points regarding this requirement: - Use type aliases over interfaces: Because of the Record<string, unknown> constraint, TypeScript may throw an error if you attempt to use an interface for your node data directly (as interfaces are open and may not strictly satisfy the index signature requirements in all contexts) [3]. Using a type alias is the recommended approach for defining custom node data [4][3]. - Constraint behavior: If you define a type alias and try to pass it to the Node generic, it must be compatible with Record<string, unknown>. If you encounter errors, ensure your data type does not contain properties that violate this constraint [3]. - Standard Usage: You define your custom node data by passing it as the first generic argument to the Node type [4][5]: type MyNodeData = { label: string; value: number }; type MyNode = Node<MyNodeData, 'my-custom-type'>; If you have complex requirements that make it difficult to satisfy Record<string, unknown>, you may need to ensure your type explicitly extends it or is structurally compatible [3].
Citations:
- 1: https://github.com/xyflow/xyflow/blob/main/packages/react/src/types/nodes.ts
- 2: https://github.com/xyflow/xyflow/blob/main/packages/system/src/types/nodes.ts
- 3: https://github.com/xyflow/web/issues/486
- 4: https://reactflow.dev/learn/advanced-use/typescript
- 5: https://reactflow.dev/learn/troubleshooting/migrate-to-v12
🌐 Web query:
TS2741 required property not present in assignment @xyflow/reactv12.11.2@xyflow/system v0.0.79
💡 Result:
The TypeScript error TS2741 indicates that you are attempting to assign an object to a variable or pass it as a prop that requires a specific property which is currently missing from your object [1]. In the context of @xyflow/react, this is a standard TypeScript error and not necessarily a bug specific to versions 12.11.2 or @xyflow/system 0.0.79. It occurs because the TypeScript definitions for React Flow components or hooks enforce that all mandatory properties (those without a? modifier) must be present [1]. To resolve this, check the following common causes: 1. Missing Required Property: Review the interface of the component or function you are using [1]. For example, if you are passing a node or edge, ensure you have provided all mandatory fields (such as id, position, or data, depending on the specific component's requirements). 2. Typo in Property Name: A common cause of TS2741 is a simple naming mismatch (e.g., using dataset instead of data) [2]. Ensure your property keys match the expected type definition exactly. 3. Partial Data Initialization: If you are building objects incrementally, the object may lack required properties during intermediate steps [3][1]. You can resolve this by: - Providing all required properties upfront [1]. - Using Partial while building the object and casting it to the final type only when it is complete [1]. - Marking the properties as optional in your own custom interfaces if they are intended to be optional [1]. If you are using specific hooks or components that were recently updated in the @xyflow/react v12.x series, verify that your implementation aligns with the latest exported types [4][5]. You can inspect the source types in your node_modules/@xyflow/react/dist directory to confirm exactly which property TypeScript expects [6].
Citations:
- 1: https://js2ts.com/typescript-error/ts2741
- 2: https://stackoverflow.com/questions/64895950/property-x-is-missing-in-type-but-required-in-type-pickinterface-x
- 3: https://deadends.dev/typescript/ts2741-missing-property/
- 4: Bump @xyflow/react from 12.10.0 to 12.11.0 xyflow/vite-react-flow-template#65
- 5: https://github.com/samoletovs/agentFlow/pull/20
- 6: https://app.unpkg.com/@xyflow/react@12.10.2/files/dist/umd
🌐 Web query:
"@xyflow/system@0.0.79" 0.0.79
💡 Result:
@xyflow/system version 0.0.79 is a core shared helper library for the xyflow ecosystem, which powers React Flow and Svelte Flow [1][2]. It was published on July 6, 2026 [3][1]. This release included several patch changes and improvements [4][5]: - Zoom pane extent is now cached to prevent panning and pinching from forcing synchronous layouts [4]. - The background pattern fill color was corrected for dark mode [4][5]. - An issue where fitView ignored hidden nodes with declared intrinsic sizes was resolved [4]. - A fix was implemented to prevent errors when clamping positions if a parent node is not in the node lookup [4]. - The onPanZoomEnd event is now consistently fired when triggered by pan-on-scroll wheel ticks [4]. - Improved safety checks were added for element existence and type in node/edge typeguards [4][5].
Citations:
- 1: https://www.npmjs.com/package/@xyflow/system
- 2: https://github.com/xyflow/xyflow
- 3: https://github.com/xyflow/xyflow/releases/tag/%40xyflow%2Fsystem%400.0.79
- 4: https://newreleases.io/project/github/xyflow/xyflow/release/@xyflow%2Fsystem@0.0.79
- 5: Release packages xyflow/xyflow#5838
Fix the assignment shape in the direct node-click callbacks.
RoadmapNodeSelection requires { id: string; data: { label: string; unknown keys } }, while @xyflow/react’s default Node uses data: Record<string, unknown>. Pass a narrowed selection object at lines 68 and 77 to avoid requiring a manual // ts-ignore around onNodeClick(node).
Suggested change
- onNodeClick(node);
+ onNodeClick({ id: node.id, data: { ...node.data, label } });
return;
}
openNodeMenu(node.id, label);
return;
}
// "option" et "center" : jamais gatés, comportement normal
- onNodeClick(node);
+ onNodeClick({
+ id: node.id,
+ data: { ...node.data, label: String(node.data.label ?? "") },
+ });📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const handleNodeClick = (node: Node) => { | |
| const locked = Boolean(node.data.locked); | |
| if (locked) return; | |
| if (node.type === "topic" || node.type === "choice") { | |
| const label = String(node.data.label ?? ""); | |
| if (completedSet.has(node.id)) { | |
| // Déjà validé : plus besoin du menu, on va direct à l'explication. | |
| onNodeClick(node); | |
| return; | |
| } | |
| openNodeMenu(node.id, label); | |
| return; | |
| } | |
| // "option" et "center" : jamais gatés, comportement normal | |
| onNodeClick(node); | |
| }; | |
| const handleNodeClick = (node: Node) => { | |
| const locked = Boolean(node.data.locked); | |
| if (locked) return; | |
| if (node.type === "topic" || node.type === "choice") { | |
| const label = String(node.data.label ?? ""); | |
| if (completedSet.has(node.id)) { | |
| // Déjà validé : plus besoin du menu, on va direct à l'explication. | |
| onNodeClick({ id: node.id, data: { ...node.data, label } }); | |
| return; | |
| } | |
| openNodeMenu(node.id, label); | |
| return; | |
| } | |
| // "option" et "center" : jamais gatés, comportement normal | |
| onNodeClick({ | |
| id: node.id, | |
| data: { ...node.data, label: String(node.data.label ?? "") }, | |
| }); | |
| }; |
🤖 Prompt for 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.
In `@src/modules/my-roadmap/ui/sections/roadmap-display-section.tsx` around lines
59 - 78, Update both direct onNodeClick calls in handleNodeClick to pass a
narrowed RoadmapNodeSelection-shaped object with node.id and a data.label
string, rather than the full `@xyflow/react` Node. Apply this in the completedSet
branch and the fallback branch while preserving the existing menu behavior and
click flow.
| @@ -0,0 +1,16 @@ | |||
| // src/modules/my-roadmap/lib/roadmap-node-selection.ts | |||
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Path comment doesn't match the file location — it says src/modules/my-roadmap/lib/... but the file lives at src/modules/roadmap/lib/.
✏️ Fix
-// src/modules/my-roadmap/lib/roadmap-node-selection.ts
+// src/modules/roadmap/lib/roadmap-node-selection.ts📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // src/modules/my-roadmap/lib/roadmap-node-selection.ts | |
| // src/modules/roadmap/lib/roadmap-node-selection.ts |
🤖 Prompt for 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.
In `@src/modules/roadmap/lib/roadmap-node-selection.ts` at line 1, Update the
stale source path reference associated with roadmap-node-selection.ts so it uses
src/modules/roadmap/lib/ rather than src/modules/my-roadmap/lib/. Preserve the
file name and remaining path unchanged.
| useEffect(() => { | ||
| if (mode !== "quiz" || !activeNodeId || !activeNodeLabel) return; | ||
| if (quizId) return; // déjà chargé pour ce passage | ||
|
|
||
| setResult(null); | ||
| setIsLoadingQuiz(true); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Stale result screen when re-entering the quiz.
result is only cleared inside this effect, but the if (quizId) return; guard on Line 55 short-circuits before setResult(null). So after finishing a quiz → "Retour" → "Passer le quiz", mode === "quiz" with a truthy result renders the old score screen instead of the questions.
Clear result when leaving quiz mode (or move the reset above the quizId guard).
🐛 Proposed fix
useEffect(() => {
if (mode !== "quiz" || !activeNodeId || !activeNodeLabel) return;
+ setResult(null);
if (quizId) return; // déjà chargé pour ce passage
- setResult(null);
setIsLoadingQuiz(true);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| useEffect(() => { | |
| if (mode !== "quiz" || !activeNodeId || !activeNodeLabel) return; | |
| if (quizId) return; // déjà chargé pour ce passage | |
| setResult(null); | |
| setIsLoadingQuiz(true); | |
| useEffect(() => { | |
| if (mode !== "quiz" || !activeNodeId || !activeNodeLabel) return; | |
| setResult(null); | |
| if (quizId) return; // déjà chargé pour ce passage | |
| setIsLoadingQuiz(true); |
🤖 Prompt for 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.
In `@src/modules/roadmap/ui/components/node-action-dialog.tsx` around lines 53 -
58, Update the quiz state reset in the useEffect so result is cleared whenever
entering quiz mode, before the quizId early-return guard can skip it. Preserve
the existing quizId loading guard and ensure re-entering the quiz renders
questions instead of the previous score screen.
| getOrCreateNodeQuiz({ | ||
| roadmapId, | ||
| nodeId: activeNodeId, | ||
| nodeLabel: activeNodeLabel, | ||
| }) | ||
| .then((res) => { | ||
| setQuizId(res.quizId); | ||
| setQuestions(res.questions); | ||
| }) | ||
| .finally(() => setIsLoadingQuiz(false)); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
No error handling on either async path.
getOrCreateNodeQuiz(...) uses .then(...).finally(...) with no .catch, and handleSubmit has try/finally with no catch. A backend failure (very plausible: the model call is unguarded server-side) produces an unhandled rejection and leaves the dialog showing an empty question list or silently doing nothing on submit.
Add error state and surface a message.
🛠️ Sketch
.then((res) => {
setQuizId(res.quizId);
setQuestions(res.questions);
})
+ .catch(() => setLoadError("Impossible de charger le quiz. Réessayez."))
.finally(() => setIsLoadingQuiz(false)); setResult(res);
+ } catch {
+ setSubmitError("Échec de l'envoi du quiz. Réessayez.");
} finally {
setSubmitting(false);
}Also applies to: 94-112
🤖 Prompt for 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.
In `@src/modules/roadmap/ui/components/node-action-dialog.tsx` around lines 60 -
69, Add error state to the dialog component and handle failures in both the
quiz-loading promise after getOrCreateNodeQuiz and the handleSubmit try block.
Capture each error, update the error state with a user-visible message, and
retain the existing loading cleanup in finally so failures do not leave
unhandled rejections or silent empty states.
Summary by CodeRabbit