fix(dashboard): don't seed course name as tutor topic on node click (#319) - #369

Merged
AndresL230 merged 2 commits into
mainfrom
fix/319-course-node-topic
Jul 29, 2026
Merged

fix(dashboard): don't seed course name as tutor topic on node click (#319)#369
AndresL230 merged 2 commits into
mainfrom
fix/319-course-node-topic

Conversation

@Darkest-Teddy

@Darkest-TeddyDarkest-Teddy commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Closes#319.

Problem

Clicking a course node on the homepage knowledge graph redirected to the AI Tutor with the course name pre-filled as the session topic. The node-click handlers unconditionally set topic=n.name; for a subject-root (course) node that name is the course.

Fix

  • New shared helper learnHrefForNode(node) in frontend/src/lib/data.ts:
    • omits topic for subject-root nodes → tutor opens scoped to the course with an empty topic picker so the learner chooses a concept;
    • seeds topic=name only for concept nodes.
  • Wired into both Dashboard.tsx graph handlers (compact + fullscreen) and Tree.tsx's "Learn this", replacing duplicated inline URL building.
  • Latent bug also fixed: the handlers passed the course as course_id, but the Learn screen reads searchParams.get("course") — so course scope was silently dropped on every node click. The helper now passes course.

Tests

Base

Stacked on feat/semester-scoped-learning (PR #360): the homepage-graph handlers this touches only exist in this form on that branch.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Improved navigation from knowledge graph concepts to the appropriate Learn experience.
    • Added support for preserving course context and selecting learning modes, including flashcards.
    • Updated quiz navigation so subject-level entries open the quiz directly, while specific concepts retain their topic.
  • Bug Fixes

    • Standardized Learn links across dashboard and tree views for more reliable routing.
  • Tests

    • Added coverage for topic, course, and learning-mode URL parameters.

@coderabbitai

coderabbitaiBot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Dashboard and tree navigation now use a shared helper for Learn URLs. Subject-root nodes omit topic parameters, course scope uses course, Quiz routes no longer include course_id, and URL behavior is covered by tests.

Changes

Learning navigation

Layer / File(s)Summary
Learn URL helper and coverage
frontend/src/lib/data.ts, frontend/src/lib/data.test.ts
Adds learnHrefForNode and tests topic, mode, and course query construction.
Dashboard graph navigation
frontend/src/components/screens/Dashboard.tsx
Routes normal and fullscreen graph selections through the shared Learn URL helper.
Tree Learn and Quiz navigation
frontend/src/components/screens/Tree.tsx
Centralizes Learn navigation and routes subject-root Quiz selections to /quiz, with topic-based routes for other nodes.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

  • SaplingLearn/Sapling#53: Updates backend Learn endpoint and session handling to use course_id in the related deep-link flow.

Suggested reviewers:jose-gael-cruz-lopez

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Out of Scope Changes check⚠️ WarningTree.tsx's quiz-routing changes go beyond #319's tutor-topic bug fix and appear unrelated to the linked issue scope.Either move the quiz-routing change to a separate PR or add a linked issue and scope note if it is intended to be part of this change.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Linked Issues check✅ PassedThe changes stop course nodes from seeding the tutor topic and switch to the course query param, which satisfies #319.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check✅ PassedThe title is concise and describes the main fix, though it only mentions the dashboard side of a broader navigation change.
Description check✅ PassedThe PR description covers the problem, fix, related issue, tests, and context, even though it doesn't follow the template headings exactly.
✨ Finishing Touches 💡 1
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch fix/319-course-node-topic
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/319-course-node-topic

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.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jul 21, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging0062744Commit Preview URL

Branch Preview URL
Jul 29 2026, 10:59 AM

Darkest-Teddyand others added 2 commits July 29, 2026 03:54
…319)
Clicking a course (subject-root) node on the homepage graph opened the AI
Tutor with the course name pre-filled as the session topic. The node-click
handlers unconditionally set `topic=n.name`; for a course node that name is
the course itself.
Add a shared `learnHrefForNode` helper in lib/data.ts that omits the topic
for subject-root nodes (scoping the tutor to the course with an empty topic
picker instead) and seeds `topic` only for concept nodes. Wire it into both
Dashboard graph handlers and Tree's "Learn this".
Also fixes a latent scope-drop in the same handlers: the course was passed
as `course_id`, but the Learn screen reads the `course` param, so course
scope was silently lost on every node click.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…319)
Follow-up to the tutor fix. The Tree detail panel's "Quick quiz" button ran
onQuiz for any node, including subject-root (course) nodes, passing
`topic=<course name>`. Unlike the tutor, the Quiz screen resolves `topic`
against concepts (roots excluded) so the course name never actually became
the quiz topic — but the param was dead and misleading.
Guard onQuiz so course nodes open the quiz picker with no seeded topic
(mirrors the tutor behavior). Also drop the `course_id` query param: the
Quiz screen only reads `topic`/`concept`, so it was dead.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@AndresL230
AndresL230force-pushed the fix/319-course-node-topic branch from 4a17bc7 to 0062744CompareJuly 29, 2026 10:57
@AndresL230
AndresL230 changed the base branch from feat/semester-scoped-learning to mainJuly 29, 2026 10:57
@AndresL230
AndresL230 marked this pull request as ready for review July 29, 2026 10:57
@AndresL230
AndresL230 merged commit 0736e43 into mainJul 29, 2026
5 checks passed
AndresL230 added a commit that referenced this pull request Jul 29, 2026
Rework of PR #360 (feat/semester-scoped-learning) replayed onto current
main as a single commit, resolving the drift accumulated since the
merge-base (~40 commits, incl. the #349 Learn.tsx SSE rewrite, the #355
subject-root dedup, the #352 content-hash chunk ids, and the #140
current/archive dashboard split, which the semester tabs supersede per
the branch's own reconciliation).
Backend semester scoping:
- services/academics.py: term_id_for_label (semester label -> term id,
with term-id fallback) + user_course_ids_for_term (enrollments ->
offerings -> term-filtered course ids).
- services/graph_service.py: get_graph/get_recommendations accept an
optional semester label; nodes/edges/stats and the synthesized subject
roots are restricted to that term's courses (composes with the #355
per-course subject-root dedup — roots are built from the already-
filtered enrollment list).
- add_course: no-retake rule across ALL terms (returns already_existed
with the existing term label) and an optional term label so the hub
enrolls into the tab being viewed, falling back to the current term.
- routes/graph.py: optional ?semester= on GET /api/graph/{user_id} and
/recommendations; AddCourseBody gains term.
- routes/gradebook.py: private _term_id_for_semester duplicate removed;
call sites now use the shared academics.term_id_for_label (follow-up
noted on the original PR).
Frontend semester scoping:
- lib/useActiveSemester.ts: localStorage-backed active-semester hook
(cross-tab sync, hydration flag so first fetches are scoped once).
- Dashboard/Tree/Learn/Quiz/Study fetch the graph scoped to the active
semester and scope course/concept pickers to it; Learn's scoping was
redone by hand against main's SSE-streaming Learn.tsx (pass the active
semester into the bootstrap getGraph, term on concepts, scopedCourses
for the course select, TopicPicker term filter — main's own
suggest/highlight logic kept, not duplicated).
- ManageCoursesModal becomes the Courses & Semesters hub (per-term tabs,
enroll-into-tab, no-retake feedback); the Dashboard archive rail from
#140 is replaced by the active-semester scoping.
- vitest.setup.ts: install an in-memory localStorage when the jsdom
environment exposes none (jsdom 29 under Vitest leaves
window.localStorage undefined), so useActiveSemester/useLayoutPref
are testable in DOM tests.
Per-type document chunking + tutor integrity:
- services/chunker.py: chunk_for_category routes prose-like categories
(essays/assignments) to a sentence-aware prose chunker, others to the
existing chunker; routes/documents.py passes the doc category through;
backfill script follows. Composes with #352's content-addressed chunk
ids (sha256 over chunk text — only boundaries change).
- prompts/preamble.txt: academic-integrity rule for the tutor (guide,
never hand over graded-work answers).
Conflict resolutions: test_document_indexing.py keeps both main's #439
relevance-gate test (repointed at the chunk_for_category seam) and the
branch's category-passthrough test; Dashboard.tsx keeps main's
IS_TEST_MODE/now + #369 learnHrefForNode imports alongside
courseTermLabels; eslint-suppressions.json re-baselined (Dashboard
no-restricted-syntax count drops with the removed archive UI).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AndresL230 added a commit that referenced this pull request Jul 29, 2026
Rework of PR #360 (feat/semester-scoped-learning) replayed onto current
main as a single commit, resolving the drift accumulated since the
merge-base (~40 commits, incl. the #349 Learn.tsx SSE rewrite, the #355
subject-root dedup, the #352 content-hash chunk ids, and the #140
current/archive dashboard split, which the semester tabs supersede per
the branch's own reconciliation).
Backend semester scoping:
- services/academics.py: term_id_for_label (semester label -> term id,
with term-id fallback) + user_course_ids_for_term (enrollments ->
offerings -> term-filtered course ids).
- services/graph_service.py: get_graph/get_recommendations accept an
optional semester label; nodes/edges/stats and the synthesized subject
roots are restricted to that term's courses (composes with the #355
per-course subject-root dedup — roots are built from the already-
filtered enrollment list).
- add_course: no-retake rule across ALL terms (returns already_existed
with the existing term label) and an optional term label so the hub
enrolls into the tab being viewed, falling back to the current term.
- routes/graph.py: optional ?semester= on GET /api/graph/{user_id} and
/recommendations; AddCourseBody gains term.
- routes/gradebook.py: private _term_id_for_semester duplicate removed;
call sites now use the shared academics.term_id_for_label (follow-up
noted on the original PR).
Frontend semester scoping:
- lib/useActiveSemester.ts: localStorage-backed active-semester hook
(cross-tab sync, hydration flag so first fetches are scoped once).
- Dashboard/Tree/Learn/Quiz/Study fetch the graph scoped to the active
semester and scope course/concept pickers to it; Learn's scoping was
redone by hand against main's SSE-streaming Learn.tsx (pass the active
semester into the bootstrap getGraph, term on concepts, scopedCourses
for the course select, TopicPicker term filter — main's own
suggest/highlight logic kept, not duplicated).
- ManageCoursesModal becomes the Courses & Semesters hub (per-term tabs,
enroll-into-tab, no-retake feedback); the Dashboard archive rail from
#140 is replaced by the active-semester scoping.
- vitest.setup.ts: install an in-memory localStorage when the jsdom
environment exposes none (jsdom 29 under Vitest leaves
window.localStorage undefined), so useActiveSemester/useLayoutPref
are testable in DOM tests.
Per-type document chunking + tutor integrity:
- services/chunker.py: chunk_for_category routes prose-like categories
(essays/assignments) to a sentence-aware prose chunker, others to the
existing chunker; routes/documents.py passes the doc category through;
backfill script follows. Composes with #352's content-addressed chunk
ids (sha256 over chunk text — only boundaries change).
- prompts/preamble.txt: academic-integrity rule for the tutor (guide,
never hand over graded-work answers).
Conflict resolutions: test_document_indexing.py keeps both main's #439
relevance-gate test (repointed at the chunk_for_category seam) and the
branch's category-passthrough test; Dashboard.tsx keeps main's
IS_TEST_MODE/now + #369 learnHrefForNode imports alongside
courseTermLabels; eslint-suppressions.json re-baselined (Dashboard
no-restricted-syntax count drops with the removed archive UI).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AndresL230 added a commit that referenced this pull request Jul 29, 2026
* feat: semester-scoped learning + Courses & Semesters hub (#360, rebased)
Rework of PR #360 (feat/semester-scoped-learning) replayed onto current
main as a single commit, resolving the drift accumulated since the
merge-base (~40 commits, incl. the #349 Learn.tsx SSE rewrite, the #355
subject-root dedup, the #352 content-hash chunk ids, and the #140
current/archive dashboard split, which the semester tabs supersede per
the branch's own reconciliation).
Backend semester scoping:
- services/academics.py: term_id_for_label (semester label -> term id,
with term-id fallback) + user_course_ids_for_term (enrollments ->
offerings -> term-filtered course ids).
- services/graph_service.py: get_graph/get_recommendations accept an
optional semester label; nodes/edges/stats and the synthesized subject
roots are restricted to that term's courses (composes with the #355
per-course subject-root dedup — roots are built from the already-
filtered enrollment list).
- add_course: no-retake rule across ALL terms (returns already_existed
with the existing term label) and an optional term label so the hub
enrolls into the tab being viewed, falling back to the current term.
- routes/graph.py: optional ?semester= on GET /api/graph/{user_id} and
/recommendations; AddCourseBody gains term.
- routes/gradebook.py: private _term_id_for_semester duplicate removed;
call sites now use the shared academics.term_id_for_label (follow-up
noted on the original PR).
Frontend semester scoping:
- lib/useActiveSemester.ts: localStorage-backed active-semester hook
(cross-tab sync, hydration flag so first fetches are scoped once).
- Dashboard/Tree/Learn/Quiz/Study fetch the graph scoped to the active
semester and scope course/concept pickers to it; Learn's scoping was
redone by hand against main's SSE-streaming Learn.tsx (pass the active
semester into the bootstrap getGraph, term on concepts, scopedCourses
for the course select, TopicPicker term filter — main's own
suggest/highlight logic kept, not duplicated).
- ManageCoursesModal becomes the Courses & Semesters hub (per-term tabs,
enroll-into-tab, no-retake feedback); the Dashboard archive rail from
#140 is replaced by the active-semester scoping.
- vitest.setup.ts: install an in-memory localStorage when the jsdom
environment exposes none (jsdom 29 under Vitest leaves
window.localStorage undefined), so useActiveSemester/useLayoutPref
are testable in DOM tests.
Per-type document chunking + tutor integrity:
- services/chunker.py: chunk_for_category routes prose-like categories
(essays/assignments) to a sentence-aware prose chunker, others to the
existing chunker; routes/documents.py passes the doc category through;
backfill script follows. Composes with #352's content-addressed chunk
ids (sha256 over chunk text — only boundaries change).
- prompts/preamble.txt: academic-integrity rule for the tutor (guide,
never hand over graded-work answers).
Conflict resolutions: test_document_indexing.py keeps both main's #439
relevance-gate test (repointed at the chunk_for_category seam) and the
branch's category-passthrough test; Dashboard.tsx keeps main's
IS_TEST_MODE/now + #369 learnHrefForNode imports alongside
courseTermLabels; eslint-suppressions.json re-baselined (Dashboard
no-restricted-syntax count drops with the removed archive UI).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(360): review fixes — race-safe add_course, honest add toasts, single scoped dashboard fetch
Review follow-ups on the semester-scoped learning rework (#360):
- add_course race (A): new partial unique index (migration 0036) closes the
NULL-section gap in course_offerings_unique; academics.resolve_offering
(create=True) now catches the 409 from a lost create race and re-selects
the winner's offering; the ManageCoursesModal Add button disables while an
add request is in flight. The now-unreachable per-offering already-enrolled
check in add_course is removed and the single return contract documented.
- add toast honesty (B): handleAdd reads the response — already_existed shows
an informational "Already taken in <term>" toast instead of a false success;
success only toasts when a row was created.
- CoursesKey empty state (C): "Nothing enrolled this semester." is reachable —
the key stays rendered when courses exist but the active semester scopes to
none; still null when there are no courses at all.
- dashboard first load (D): with no stored semester, resolve + persist the
default (courses + semesters fetch) BEFORE the scoped fetch and early-return;
the effect re-run performs the single scoped fetch. Zero-courses stays a
single unscoped pass.
- cross-screen default (E): new useActiveSemester.ensureDefaultActiveSemester
(termLabels) persists a default when none is stored, called from
Learn/Quiz/Tree/Study once course term labels are in hand.
- prune (F): drop orphaned partitionCurrentAndArchive, groupCoursesByTerm,
UNKNOWN_TERM_LABEL, CoursePartition (+ TermGroup) and their test blocks.
Tests: resolve_offering conflict-retry + non-409 propagation units; add_course
duplicate-path contract; ManageCoursesModal component tests for the
already-existed/success/in-flight paths; Dashboard single-scoped-fetch,
zero-courses and CoursesKey empty-state renders; ensureDefaultActiveSemester
units.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* rework(360): default = All semesters — semester scoping is opt-in (e2e veto)
The e2e lane vetoed the auto-default: the rich seed spans Fall 2025 /
Spring 2026 / Summer 2026 and 7 of 11 journeys failed because the
auto-resolved term silently hid cross-term fixtures (dashboard lost
MATH210; the graph shrank to 1 of 17 nodes). New semantics:
- Default = ALL SEMESTERS (unscoped). An empty stored active-semester
value IS the default and means "All semesters": removed
ensureDefaultActiveSemester + its Learn/Quiz/Tree/Study call sites,
Dashboard's default-resolution early-return pass (deferredToScopedPass)
and its getSemesters/courseTermLabels/resolveActiveSemester usage.
resolveActiveSemester itself is deleted (orphaned — the hub now reads
the stored value raw). Hook, change events, and hydration gating stay.
- Hub UI: ManageCoursesModal grows an explicit "All semesters" tab —
active when the stored value is empty, clicking it clears the value;
term tabs unchanged (both carry aria-pressed). A picked term still
persists and scopes every surface.
- Kept from the review batch: CoursesKey empty-state reachability, the
already_existed info toast, in-flight Add disable, the 0036 partial
unique index + resolve_offering conflict retry, semesters.ts pruning.
- New journey frontend/e2e/semester-scope.spec.ts: default shows
cross-term courses together (MATH210 + BIO110), picking Fall 2025 in
the hub hides Spring-only MATH210, "All semesters" restores it. Hub
opens via new dashboard-courses-manage testid (documented in
docs/frontend-testids.md); tabs selected by role/name.
- Tests reworked: Dashboard default test now asserts one unscoped fetch
with both terms visible; /api/semesters-failure test dropped (no
semesters fetch remains); ensureDefault unit tests replaced by hub tab
component tests (All active by default / term persists / All clears).
- Design doc amendment records the veto and the opt-in scoping decision.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: AndresL230 <190146319+AndresL230@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@AndresL230
AndresL230 deleted the fix/319-course-node-topic branch August 2, 2026 18:30
Sign up for freeto 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.

Clicking on a course node will result in the course name being selected as a course topic automatically

2 participants

@Darkest-Teddy@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(dashboard): don't seed course name as tutor topic on node click (#319) - #369

Merged
AndresL230 merged 2 commits into
mainfrom
fix/319-course-node-topic
Jul 29, 2026
Merged

fix(dashboard): don't seed course name as tutor topic on node click (#319)#369
AndresL230 merged 2 commits into
mainfrom
fix/319-course-node-topic

Conversation

@Darkest-Teddy

@Darkest-TeddyDarkest-Teddy commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Closes#319.

Problem

Clicking a course node on the homepage knowledge graph redirected to the AI Tutor with the course name pre-filled as the session topic. The node-click handlers unconditionally set topic=n.name; for a subject-root (course) node that name is the course.

Fix

  • New shared helper learnHrefForNode(node) in frontend/src/lib/data.ts:
    • omits topic for subject-root nodes → tutor opens scoped to the course with an empty topic picker so the learner chooses a concept;
    • seeds topic=name only for concept nodes.
  • Wired into both Dashboard.tsx graph handlers (compact + fullscreen) and Tree.tsx's "Learn this", replacing duplicated inline URL building.
  • Latent bug also fixed: the handlers passed the course as course_id, but the Learn screen reads searchParams.get("course") — so course scope was silently dropped on every node click. The helper now passes course.

Tests

Base

Stacked on feat/semester-scoped-learning (PR #360): the homepage-graph handlers this touches only exist in this form on that branch.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Improved navigation from knowledge graph concepts to the appropriate Learn experience.
    • Added support for preserving course context and selecting learning modes, including flashcards.
    • Updated quiz navigation so subject-level entries open the quiz directly, while specific concepts retain their topic.
  • Bug Fixes

    • Standardized Learn links across dashboard and tree views for more reliable routing.
  • Tests

    • Added coverage for topic, course, and learning-mode URL parameters.

@coderabbitai

coderabbitaiBot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Dashboard and tree navigation now use a shared helper for Learn URLs. Subject-root nodes omit topic parameters, course scope uses course, Quiz routes no longer include course_id, and URL behavior is covered by tests.

Changes

Learning navigation

Layer / File(s)Summary
Learn URL helper and coverage
frontend/src/lib/data.ts, frontend/src/lib/data.test.ts
Adds learnHrefForNode and tests topic, mode, and course query construction.
Dashboard graph navigation
frontend/src/components/screens/Dashboard.tsx
Routes normal and fullscreen graph selections through the shared Learn URL helper.
Tree Learn and Quiz navigation
frontend/src/components/screens/Tree.tsx
Centralizes Learn navigation and routes subject-root Quiz selections to /quiz, with topic-based routes for other nodes.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

  • SaplingLearn/Sapling#53: Updates backend Learn endpoint and session handling to use course_id in the related deep-link flow.

Suggested reviewers:jose-gael-cruz-lopez

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Out of Scope Changes check⚠️ WarningTree.tsx's quiz-routing changes go beyond #319's tutor-topic bug fix and appear unrelated to the linked issue scope.Either move the quiz-routing change to a separate PR or add a linked issue and scope note if it is intended to be part of this change.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Linked Issues check✅ PassedThe changes stop course nodes from seeding the tutor topic and switch to the course query param, which satisfies #319.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check✅ PassedThe title is concise and describes the main fix, though it only mentions the dashboard side of a broader navigation change.
Description check✅ PassedThe PR description covers the problem, fix, related issue, tests, and context, even though it doesn't follow the template headings exactly.
✨ Finishing Touches 💡 1
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch fix/319-course-node-topic
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/319-course-node-topic

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.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jul 21, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging0062744Commit Preview URL

Branch Preview URL
Jul 29 2026, 10:59 AM

Darkest-Teddyand others added 2 commits July 29, 2026 03:54
…319)
Clicking a course (subject-root) node on the homepage graph opened the AI
Tutor with the course name pre-filled as the session topic. The node-click
handlers unconditionally set `topic=n.name`; for a course node that name is
the course itself.
Add a shared `learnHrefForNode` helper in lib/data.ts that omits the topic
for subject-root nodes (scoping the tutor to the course with an empty topic
picker instead) and seeds `topic` only for concept nodes. Wire it into both
Dashboard graph handlers and Tree's "Learn this".
Also fixes a latent scope-drop in the same handlers: the course was passed
as `course_id`, but the Learn screen reads the `course` param, so course
scope was silently lost on every node click.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…319)
Follow-up to the tutor fix. The Tree detail panel's "Quick quiz" button ran
onQuiz for any node, including subject-root (course) nodes, passing
`topic=<course name>`. Unlike the tutor, the Quiz screen resolves `topic`
against concepts (roots excluded) so the course name never actually became
the quiz topic — but the param was dead and misleading.
Guard onQuiz so course nodes open the quiz picker with no seeded topic
(mirrors the tutor behavior). Also drop the `course_id` query param: the
Quiz screen only reads `topic`/`concept`, so it was dead.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@AndresL230
AndresL230force-pushed the fix/319-course-node-topic branch from 4a17bc7 to 0062744CompareJuly 29, 2026 10:57
@AndresL230
AndresL230 changed the base branch from feat/semester-scoped-learning to mainJuly 29, 2026 10:57
@AndresL230
AndresL230 marked this pull request as ready for review July 29, 2026 10:57
@AndresL230
AndresL230 merged commit 0736e43 into mainJul 29, 2026
5 checks passed
AndresL230 added a commit that referenced this pull request Jul 29, 2026
Rework of PR #360 (feat/semester-scoped-learning) replayed onto current
main as a single commit, resolving the drift accumulated since the
merge-base (~40 commits, incl. the #349 Learn.tsx SSE rewrite, the #355
subject-root dedup, the #352 content-hash chunk ids, and the #140
current/archive dashboard split, which the semester tabs supersede per
the branch's own reconciliation).
Backend semester scoping:
- services/academics.py: term_id_for_label (semester label -> term id,
with term-id fallback) + user_course_ids_for_term (enrollments ->
offerings -> term-filtered course ids).
- services/graph_service.py: get_graph/get_recommendations accept an
optional semester label; nodes/edges/stats and the synthesized subject
roots are restricted to that term's courses (composes with the #355
per-course subject-root dedup — roots are built from the already-
filtered enrollment list).
- add_course: no-retake rule across ALL terms (returns already_existed
with the existing term label) and an optional term label so the hub
enrolls into the tab being viewed, falling back to the current term.
- routes/graph.py: optional ?semester= on GET /api/graph/{user_id} and
/recommendations; AddCourseBody gains term.
- routes/gradebook.py: private _term_id_for_semester duplicate removed;
call sites now use the shared academics.term_id_for_label (follow-up
noted on the original PR).
Frontend semester scoping:
- lib/useActiveSemester.ts: localStorage-backed active-semester hook
(cross-tab sync, hydration flag so first fetches are scoped once).
- Dashboard/Tree/Learn/Quiz/Study fetch the graph scoped to the active
semester and scope course/concept pickers to it; Learn's scoping was
redone by hand against main's SSE-streaming Learn.tsx (pass the active
semester into the bootstrap getGraph, term on concepts, scopedCourses
for the course select, TopicPicker term filter — main's own
suggest/highlight logic kept, not duplicated).
- ManageCoursesModal becomes the Courses & Semesters hub (per-term tabs,
enroll-into-tab, no-retake feedback); the Dashboard archive rail from
#140 is replaced by the active-semester scoping.
- vitest.setup.ts: install an in-memory localStorage when the jsdom
environment exposes none (jsdom 29 under Vitest leaves
window.localStorage undefined), so useActiveSemester/useLayoutPref
are testable in DOM tests.
Per-type document chunking + tutor integrity:
- services/chunker.py: chunk_for_category routes prose-like categories
(essays/assignments) to a sentence-aware prose chunker, others to the
existing chunker; routes/documents.py passes the doc category through;
backfill script follows. Composes with #352's content-addressed chunk
ids (sha256 over chunk text — only boundaries change).
- prompts/preamble.txt: academic-integrity rule for the tutor (guide,
never hand over graded-work answers).
Conflict resolutions: test_document_indexing.py keeps both main's #439
relevance-gate test (repointed at the chunk_for_category seam) and the
branch's category-passthrough test; Dashboard.tsx keeps main's
IS_TEST_MODE/now + #369 learnHrefForNode imports alongside
courseTermLabels; eslint-suppressions.json re-baselined (Dashboard
no-restricted-syntax count drops with the removed archive UI).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AndresL230 added a commit that referenced this pull request Jul 29, 2026
Rework of PR #360 (feat/semester-scoped-learning) replayed onto current
main as a single commit, resolving the drift accumulated since the
merge-base (~40 commits, incl. the #349 Learn.tsx SSE rewrite, the #355
subject-root dedup, the #352 content-hash chunk ids, and the #140
current/archive dashboard split, which the semester tabs supersede per
the branch's own reconciliation).
Backend semester scoping:
- services/academics.py: term_id_for_label (semester label -> term id,
with term-id fallback) + user_course_ids_for_term (enrollments ->
offerings -> term-filtered course ids).
- services/graph_service.py: get_graph/get_recommendations accept an
optional semester label; nodes/edges/stats and the synthesized subject
roots are restricted to that term's courses (composes with the #355
per-course subject-root dedup — roots are built from the already-
filtered enrollment list).
- add_course: no-retake rule across ALL terms (returns already_existed
with the existing term label) and an optional term label so the hub
enrolls into the tab being viewed, falling back to the current term.
- routes/graph.py: optional ?semester= on GET /api/graph/{user_id} and
/recommendations; AddCourseBody gains term.
- routes/gradebook.py: private _term_id_for_semester duplicate removed;
call sites now use the shared academics.term_id_for_label (follow-up
noted on the original PR).
Frontend semester scoping:
- lib/useActiveSemester.ts: localStorage-backed active-semester hook
(cross-tab sync, hydration flag so first fetches are scoped once).
- Dashboard/Tree/Learn/Quiz/Study fetch the graph scoped to the active
semester and scope course/concept pickers to it; Learn's scoping was
redone by hand against main's SSE-streaming Learn.tsx (pass the active
semester into the bootstrap getGraph, term on concepts, scopedCourses
for the course select, TopicPicker term filter — main's own
suggest/highlight logic kept, not duplicated).
- ManageCoursesModal becomes the Courses & Semesters hub (per-term tabs,
enroll-into-tab, no-retake feedback); the Dashboard archive rail from
#140 is replaced by the active-semester scoping.
- vitest.setup.ts: install an in-memory localStorage when the jsdom
environment exposes none (jsdom 29 under Vitest leaves
window.localStorage undefined), so useActiveSemester/useLayoutPref
are testable in DOM tests.
Per-type document chunking + tutor integrity:
- services/chunker.py: chunk_for_category routes prose-like categories
(essays/assignments) to a sentence-aware prose chunker, others to the
existing chunker; routes/documents.py passes the doc category through;
backfill script follows. Composes with #352's content-addressed chunk
ids (sha256 over chunk text — only boundaries change).
- prompts/preamble.txt: academic-integrity rule for the tutor (guide,
never hand over graded-work answers).
Conflict resolutions: test_document_indexing.py keeps both main's #439
relevance-gate test (repointed at the chunk_for_category seam) and the
branch's category-passthrough test; Dashboard.tsx keeps main's
IS_TEST_MODE/now + #369 learnHrefForNode imports alongside
courseTermLabels; eslint-suppressions.json re-baselined (Dashboard
no-restricted-syntax count drops with the removed archive UI).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AndresL230 added a commit that referenced this pull request Jul 29, 2026
* feat: semester-scoped learning + Courses & Semesters hub (#360, rebased)
Rework of PR #360 (feat/semester-scoped-learning) replayed onto current
main as a single commit, resolving the drift accumulated since the
merge-base (~40 commits, incl. the #349 Learn.tsx SSE rewrite, the #355
subject-root dedup, the #352 content-hash chunk ids, and the #140
current/archive dashboard split, which the semester tabs supersede per
the branch's own reconciliation).
Backend semester scoping:
- services/academics.py: term_id_for_label (semester label -> term id,
with term-id fallback) + user_course_ids_for_term (enrollments ->
offerings -> term-filtered course ids).
- services/graph_service.py: get_graph/get_recommendations accept an
optional semester label; nodes/edges/stats and the synthesized subject
roots are restricted to that term's courses (composes with the #355
per-course subject-root dedup — roots are built from the already-
filtered enrollment list).
- add_course: no-retake rule across ALL terms (returns already_existed
with the existing term label) and an optional term label so the hub
enrolls into the tab being viewed, falling back to the current term.
- routes/graph.py: optional ?semester= on GET /api/graph/{user_id} and
/recommendations; AddCourseBody gains term.
- routes/gradebook.py: private _term_id_for_semester duplicate removed;
call sites now use the shared academics.term_id_for_label (follow-up
noted on the original PR).
Frontend semester scoping:
- lib/useActiveSemester.ts: localStorage-backed active-semester hook
(cross-tab sync, hydration flag so first fetches are scoped once).
- Dashboard/Tree/Learn/Quiz/Study fetch the graph scoped to the active
semester and scope course/concept pickers to it; Learn's scoping was
redone by hand against main's SSE-streaming Learn.tsx (pass the active
semester into the bootstrap getGraph, term on concepts, scopedCourses
for the course select, TopicPicker term filter — main's own
suggest/highlight logic kept, not duplicated).
- ManageCoursesModal becomes the Courses & Semesters hub (per-term tabs,
enroll-into-tab, no-retake feedback); the Dashboard archive rail from
#140 is replaced by the active-semester scoping.
- vitest.setup.ts: install an in-memory localStorage when the jsdom
environment exposes none (jsdom 29 under Vitest leaves
window.localStorage undefined), so useActiveSemester/useLayoutPref
are testable in DOM tests.
Per-type document chunking + tutor integrity:
- services/chunker.py: chunk_for_category routes prose-like categories
(essays/assignments) to a sentence-aware prose chunker, others to the
existing chunker; routes/documents.py passes the doc category through;
backfill script follows. Composes with #352's content-addressed chunk
ids (sha256 over chunk text — only boundaries change).
- prompts/preamble.txt: academic-integrity rule for the tutor (guide,
never hand over graded-work answers).
Conflict resolutions: test_document_indexing.py keeps both main's #439
relevance-gate test (repointed at the chunk_for_category seam) and the
branch's category-passthrough test; Dashboard.tsx keeps main's
IS_TEST_MODE/now + #369 learnHrefForNode imports alongside
courseTermLabels; eslint-suppressions.json re-baselined (Dashboard
no-restricted-syntax count drops with the removed archive UI).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(360): review fixes — race-safe add_course, honest add toasts, single scoped dashboard fetch
Review follow-ups on the semester-scoped learning rework (#360):
- add_course race (A): new partial unique index (migration 0036) closes the
NULL-section gap in course_offerings_unique; academics.resolve_offering
(create=True) now catches the 409 from a lost create race and re-selects
the winner's offering; the ManageCoursesModal Add button disables while an
add request is in flight. The now-unreachable per-offering already-enrolled
check in add_course is removed and the single return contract documented.
- add toast honesty (B): handleAdd reads the response — already_existed shows
an informational "Already taken in <term>" toast instead of a false success;
success only toasts when a row was created.
- CoursesKey empty state (C): "Nothing enrolled this semester." is reachable —
the key stays rendered when courses exist but the active semester scopes to
none; still null when there are no courses at all.
- dashboard first load (D): with no stored semester, resolve + persist the
default (courses + semesters fetch) BEFORE the scoped fetch and early-return;
the effect re-run performs the single scoped fetch. Zero-courses stays a
single unscoped pass.
- cross-screen default (E): new useActiveSemester.ensureDefaultActiveSemester
(termLabels) persists a default when none is stored, called from
Learn/Quiz/Tree/Study once course term labels are in hand.
- prune (F): drop orphaned partitionCurrentAndArchive, groupCoursesByTerm,
UNKNOWN_TERM_LABEL, CoursePartition (+ TermGroup) and their test blocks.
Tests: resolve_offering conflict-retry + non-409 propagation units; add_course
duplicate-path contract; ManageCoursesModal component tests for the
already-existed/success/in-flight paths; Dashboard single-scoped-fetch,
zero-courses and CoursesKey empty-state renders; ensureDefaultActiveSemester
units.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* rework(360): default = All semesters — semester scoping is opt-in (e2e veto)
The e2e lane vetoed the auto-default: the rich seed spans Fall 2025 /
Spring 2026 / Summer 2026 and 7 of 11 journeys failed because the
auto-resolved term silently hid cross-term fixtures (dashboard lost
MATH210; the graph shrank to 1 of 17 nodes). New semantics:
- Default = ALL SEMESTERS (unscoped). An empty stored active-semester
value IS the default and means "All semesters": removed
ensureDefaultActiveSemester + its Learn/Quiz/Tree/Study call sites,
Dashboard's default-resolution early-return pass (deferredToScopedPass)
and its getSemesters/courseTermLabels/resolveActiveSemester usage.
resolveActiveSemester itself is deleted (orphaned — the hub now reads
the stored value raw). Hook, change events, and hydration gating stay.
- Hub UI: ManageCoursesModal grows an explicit "All semesters" tab —
active when the stored value is empty, clicking it clears the value;
term tabs unchanged (both carry aria-pressed). A picked term still
persists and scopes every surface.
- Kept from the review batch: CoursesKey empty-state reachability, the
already_existed info toast, in-flight Add disable, the 0036 partial
unique index + resolve_offering conflict retry, semesters.ts pruning.
- New journey frontend/e2e/semester-scope.spec.ts: default shows
cross-term courses together (MATH210 + BIO110), picking Fall 2025 in
the hub hides Spring-only MATH210, "All semesters" restores it. Hub
opens via new dashboard-courses-manage testid (documented in
docs/frontend-testids.md); tabs selected by role/name.
- Tests reworked: Dashboard default test now asserts one unscoped fetch
with both terms visible; /api/semesters-failure test dropped (no
semesters fetch remains); ensureDefault unit tests replaced by hub tab
component tests (All active by default / term persists / All clears).
- Design doc amendment records the veto and the opt-in scoping decision.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: AndresL230 <190146319+AndresL230@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@AndresL230
AndresL230 deleted the fix/319-course-node-topic branch August 2, 2026 18:30
Sign up for freeto 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.

Clicking on a course node will result in the course name being selected as a course topic automatically

2 participants

@Darkest-Teddy@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(dashboard): don't seed course name as tutor topic on node click (#319) - #369

Merged
AndresL230 merged 2 commits into
mainfrom
fix/319-course-node-topic
Jul 29, 2026
Merged

fix(dashboard): don't seed course name as tutor topic on node click (#319)#369
AndresL230 merged 2 commits into
mainfrom
fix/319-course-node-topic

Conversation

@Darkest-Teddy

@Darkest-TeddyDarkest-Teddy commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Closes#319.

Problem

Clicking a course node on the homepage knowledge graph redirected to the AI Tutor with the course name pre-filled as the session topic. The node-click handlers unconditionally set topic=n.name; for a subject-root (course) node that name is the course.

Fix

  • New shared helper learnHrefForNode(node) in frontend/src/lib/data.ts:
    • omits topic for subject-root nodes → tutor opens scoped to the course with an empty topic picker so the learner chooses a concept;
    • seeds topic=name only for concept nodes.
  • Wired into both Dashboard.tsx graph handlers (compact + fullscreen) and Tree.tsx's "Learn this", replacing duplicated inline URL building.
  • Latent bug also fixed: the handlers passed the course as course_id, but the Learn screen reads searchParams.get("course") — so course scope was silently dropped on every node click. The helper now passes course.

Tests

Base

Stacked on feat/semester-scoped-learning (PR #360): the homepage-graph handlers this touches only exist in this form on that branch.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Improved navigation from knowledge graph concepts to the appropriate Learn experience.
    • Added support for preserving course context and selecting learning modes, including flashcards.
    • Updated quiz navigation so subject-level entries open the quiz directly, while specific concepts retain their topic.
  • Bug Fixes

    • Standardized Learn links across dashboard and tree views for more reliable routing.
  • Tests

    • Added coverage for topic, course, and learning-mode URL parameters.

@coderabbitai

coderabbitaiBot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Dashboard and tree navigation now use a shared helper for Learn URLs. Subject-root nodes omit topic parameters, course scope uses course, Quiz routes no longer include course_id, and URL behavior is covered by tests.

Changes

Learning navigation

Layer / File(s)Summary
Learn URL helper and coverage
frontend/src/lib/data.ts, frontend/src/lib/data.test.ts
Adds learnHrefForNode and tests topic, mode, and course query construction.
Dashboard graph navigation
frontend/src/components/screens/Dashboard.tsx
Routes normal and fullscreen graph selections through the shared Learn URL helper.
Tree Learn and Quiz navigation
frontend/src/components/screens/Tree.tsx
Centralizes Learn navigation and routes subject-root Quiz selections to /quiz, with topic-based routes for other nodes.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

  • SaplingLearn/Sapling#53: Updates backend Learn endpoint and session handling to use course_id in the related deep-link flow.

Suggested reviewers:jose-gael-cruz-lopez

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Out of Scope Changes check⚠️ WarningTree.tsx's quiz-routing changes go beyond #319's tutor-topic bug fix and appear unrelated to the linked issue scope.Either move the quiz-routing change to a separate PR or add a linked issue and scope note if it is intended to be part of this change.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Linked Issues check✅ PassedThe changes stop course nodes from seeding the tutor topic and switch to the course query param, which satisfies #319.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check✅ PassedThe title is concise and describes the main fix, though it only mentions the dashboard side of a broader navigation change.
Description check✅ PassedThe PR description covers the problem, fix, related issue, tests, and context, even though it doesn't follow the template headings exactly.
✨ Finishing Touches 💡 1
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch fix/319-course-node-topic
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/319-course-node-topic

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.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jul 21, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging0062744Commit Preview URL

Branch Preview URL
Jul 29 2026, 10:59 AM

Darkest-Teddyand others added 2 commits July 29, 2026 03:54
…319)
Clicking a course (subject-root) node on the homepage graph opened the AI
Tutor with the course name pre-filled as the session topic. The node-click
handlers unconditionally set `topic=n.name`; for a course node that name is
the course itself.
Add a shared `learnHrefForNode` helper in lib/data.ts that omits the topic
for subject-root nodes (scoping the tutor to the course with an empty topic
picker instead) and seeds `topic` only for concept nodes. Wire it into both
Dashboard graph handlers and Tree's "Learn this".
Also fixes a latent scope-drop in the same handlers: the course was passed
as `course_id`, but the Learn screen reads the `course` param, so course
scope was silently lost on every node click.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…319)
Follow-up to the tutor fix. The Tree detail panel's "Quick quiz" button ran
onQuiz for any node, including subject-root (course) nodes, passing
`topic=<course name>`. Unlike the tutor, the Quiz screen resolves `topic`
against concepts (roots excluded) so the course name never actually became
the quiz topic — but the param was dead and misleading.
Guard onQuiz so course nodes open the quiz picker with no seeded topic
(mirrors the tutor behavior). Also drop the `course_id` query param: the
Quiz screen only reads `topic`/`concept`, so it was dead.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@AndresL230
AndresL230force-pushed the fix/319-course-node-topic branch from 4a17bc7 to 0062744CompareJuly 29, 2026 10:57
@AndresL230
AndresL230 changed the base branch from feat/semester-scoped-learning to mainJuly 29, 2026 10:57
@AndresL230
AndresL230 marked this pull request as ready for review July 29, 2026 10:57
@AndresL230
AndresL230 merged commit 0736e43 into mainJul 29, 2026
5 checks passed
AndresL230 added a commit that referenced this pull request Jul 29, 2026
Rework of PR #360 (feat/semester-scoped-learning) replayed onto current
main as a single commit, resolving the drift accumulated since the
merge-base (~40 commits, incl. the #349 Learn.tsx SSE rewrite, the #355
subject-root dedup, the #352 content-hash chunk ids, and the #140
current/archive dashboard split, which the semester tabs supersede per
the branch's own reconciliation).
Backend semester scoping:
- services/academics.py: term_id_for_label (semester label -> term id,
with term-id fallback) + user_course_ids_for_term (enrollments ->
offerings -> term-filtered course ids).
- services/graph_service.py: get_graph/get_recommendations accept an
optional semester label; nodes/edges/stats and the synthesized subject
roots are restricted to that term's courses (composes with the #355
per-course subject-root dedup — roots are built from the already-
filtered enrollment list).
- add_course: no-retake rule across ALL terms (returns already_existed
with the existing term label) and an optional term label so the hub
enrolls into the tab being viewed, falling back to the current term.
- routes/graph.py: optional ?semester= on GET /api/graph/{user_id} and
/recommendations; AddCourseBody gains term.
- routes/gradebook.py: private _term_id_for_semester duplicate removed;
call sites now use the shared academics.term_id_for_label (follow-up
noted on the original PR).
Frontend semester scoping:
- lib/useActiveSemester.ts: localStorage-backed active-semester hook
(cross-tab sync, hydration flag so first fetches are scoped once).
- Dashboard/Tree/Learn/Quiz/Study fetch the graph scoped to the active
semester and scope course/concept pickers to it; Learn's scoping was
redone by hand against main's SSE-streaming Learn.tsx (pass the active
semester into the bootstrap getGraph, term on concepts, scopedCourses
for the course select, TopicPicker term filter — main's own
suggest/highlight logic kept, not duplicated).
- ManageCoursesModal becomes the Courses & Semesters hub (per-term tabs,
enroll-into-tab, no-retake feedback); the Dashboard archive rail from
#140 is replaced by the active-semester scoping.
- vitest.setup.ts: install an in-memory localStorage when the jsdom
environment exposes none (jsdom 29 under Vitest leaves
window.localStorage undefined), so useActiveSemester/useLayoutPref
are testable in DOM tests.
Per-type document chunking + tutor integrity:
- services/chunker.py: chunk_for_category routes prose-like categories
(essays/assignments) to a sentence-aware prose chunker, others to the
existing chunker; routes/documents.py passes the doc category through;
backfill script follows. Composes with #352's content-addressed chunk
ids (sha256 over chunk text — only boundaries change).
- prompts/preamble.txt: academic-integrity rule for the tutor (guide,
never hand over graded-work answers).
Conflict resolutions: test_document_indexing.py keeps both main's #439
relevance-gate test (repointed at the chunk_for_category seam) and the
branch's category-passthrough test; Dashboard.tsx keeps main's
IS_TEST_MODE/now + #369 learnHrefForNode imports alongside
courseTermLabels; eslint-suppressions.json re-baselined (Dashboard
no-restricted-syntax count drops with the removed archive UI).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AndresL230 added a commit that referenced this pull request Jul 29, 2026
Rework of PR #360 (feat/semester-scoped-learning) replayed onto current
main as a single commit, resolving the drift accumulated since the
merge-base (~40 commits, incl. the #349 Learn.tsx SSE rewrite, the #355
subject-root dedup, the #352 content-hash chunk ids, and the #140
current/archive dashboard split, which the semester tabs supersede per
the branch's own reconciliation).
Backend semester scoping:
- services/academics.py: term_id_for_label (semester label -> term id,
with term-id fallback) + user_course_ids_for_term (enrollments ->
offerings -> term-filtered course ids).
- services/graph_service.py: get_graph/get_recommendations accept an
optional semester label; nodes/edges/stats and the synthesized subject
roots are restricted to that term's courses (composes with the #355
per-course subject-root dedup — roots are built from the already-
filtered enrollment list).
- add_course: no-retake rule across ALL terms (returns already_existed
with the existing term label) and an optional term label so the hub
enrolls into the tab being viewed, falling back to the current term.
- routes/graph.py: optional ?semester= on GET /api/graph/{user_id} and
/recommendations; AddCourseBody gains term.
- routes/gradebook.py: private _term_id_for_semester duplicate removed;
call sites now use the shared academics.term_id_for_label (follow-up
noted on the original PR).
Frontend semester scoping:
- lib/useActiveSemester.ts: localStorage-backed active-semester hook
(cross-tab sync, hydration flag so first fetches are scoped once).
- Dashboard/Tree/Learn/Quiz/Study fetch the graph scoped to the active
semester and scope course/concept pickers to it; Learn's scoping was
redone by hand against main's SSE-streaming Learn.tsx (pass the active
semester into the bootstrap getGraph, term on concepts, scopedCourses
for the course select, TopicPicker term filter — main's own
suggest/highlight logic kept, not duplicated).
- ManageCoursesModal becomes the Courses & Semesters hub (per-term tabs,
enroll-into-tab, no-retake feedback); the Dashboard archive rail from
#140 is replaced by the active-semester scoping.
- vitest.setup.ts: install an in-memory localStorage when the jsdom
environment exposes none (jsdom 29 under Vitest leaves
window.localStorage undefined), so useActiveSemester/useLayoutPref
are testable in DOM tests.
Per-type document chunking + tutor integrity:
- services/chunker.py: chunk_for_category routes prose-like categories
(essays/assignments) to a sentence-aware prose chunker, others to the
existing chunker; routes/documents.py passes the doc category through;
backfill script follows. Composes with #352's content-addressed chunk
ids (sha256 over chunk text — only boundaries change).
- prompts/preamble.txt: academic-integrity rule for the tutor (guide,
never hand over graded-work answers).
Conflict resolutions: test_document_indexing.py keeps both main's #439
relevance-gate test (repointed at the chunk_for_category seam) and the
branch's category-passthrough test; Dashboard.tsx keeps main's
IS_TEST_MODE/now + #369 learnHrefForNode imports alongside
courseTermLabels; eslint-suppressions.json re-baselined (Dashboard
no-restricted-syntax count drops with the removed archive UI).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AndresL230 added a commit that referenced this pull request Jul 29, 2026
* feat: semester-scoped learning + Courses & Semesters hub (#360, rebased)
Rework of PR #360 (feat/semester-scoped-learning) replayed onto current
main as a single commit, resolving the drift accumulated since the
merge-base (~40 commits, incl. the #349 Learn.tsx SSE rewrite, the #355
subject-root dedup, the #352 content-hash chunk ids, and the #140
current/archive dashboard split, which the semester tabs supersede per
the branch's own reconciliation).
Backend semester scoping:
- services/academics.py: term_id_for_label (semester label -> term id,
with term-id fallback) + user_course_ids_for_term (enrollments ->
offerings -> term-filtered course ids).
- services/graph_service.py: get_graph/get_recommendations accept an
optional semester label; nodes/edges/stats and the synthesized subject
roots are restricted to that term's courses (composes with the #355
per-course subject-root dedup — roots are built from the already-
filtered enrollment list).
- add_course: no-retake rule across ALL terms (returns already_existed
with the existing term label) and an optional term label so the hub
enrolls into the tab being viewed, falling back to the current term.
- routes/graph.py: optional ?semester= on GET /api/graph/{user_id} and
/recommendations; AddCourseBody gains term.
- routes/gradebook.py: private _term_id_for_semester duplicate removed;
call sites now use the shared academics.term_id_for_label (follow-up
noted on the original PR).
Frontend semester scoping:
- lib/useActiveSemester.ts: localStorage-backed active-semester hook
(cross-tab sync, hydration flag so first fetches are scoped once).
- Dashboard/Tree/Learn/Quiz/Study fetch the graph scoped to the active
semester and scope course/concept pickers to it; Learn's scoping was
redone by hand against main's SSE-streaming Learn.tsx (pass the active
semester into the bootstrap getGraph, term on concepts, scopedCourses
for the course select, TopicPicker term filter — main's own
suggest/highlight logic kept, not duplicated).
- ManageCoursesModal becomes the Courses & Semesters hub (per-term tabs,
enroll-into-tab, no-retake feedback); the Dashboard archive rail from
#140 is replaced by the active-semester scoping.
- vitest.setup.ts: install an in-memory localStorage when the jsdom
environment exposes none (jsdom 29 under Vitest leaves
window.localStorage undefined), so useActiveSemester/useLayoutPref
are testable in DOM tests.
Per-type document chunking + tutor integrity:
- services/chunker.py: chunk_for_category routes prose-like categories
(essays/assignments) to a sentence-aware prose chunker, others to the
existing chunker; routes/documents.py passes the doc category through;
backfill script follows. Composes with #352's content-addressed chunk
ids (sha256 over chunk text — only boundaries change).
- prompts/preamble.txt: academic-integrity rule for the tutor (guide,
never hand over graded-work answers).
Conflict resolutions: test_document_indexing.py keeps both main's #439
relevance-gate test (repointed at the chunk_for_category seam) and the
branch's category-passthrough test; Dashboard.tsx keeps main's
IS_TEST_MODE/now + #369 learnHrefForNode imports alongside
courseTermLabels; eslint-suppressions.json re-baselined (Dashboard
no-restricted-syntax count drops with the removed archive UI).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(360): review fixes — race-safe add_course, honest add toasts, single scoped dashboard fetch
Review follow-ups on the semester-scoped learning rework (#360):
- add_course race (A): new partial unique index (migration 0036) closes the
NULL-section gap in course_offerings_unique; academics.resolve_offering
(create=True) now catches the 409 from a lost create race and re-selects
the winner's offering; the ManageCoursesModal Add button disables while an
add request is in flight. The now-unreachable per-offering already-enrolled
check in add_course is removed and the single return contract documented.
- add toast honesty (B): handleAdd reads the response — already_existed shows
an informational "Already taken in <term>" toast instead of a false success;
success only toasts when a row was created.
- CoursesKey empty state (C): "Nothing enrolled this semester." is reachable —
the key stays rendered when courses exist but the active semester scopes to
none; still null when there are no courses at all.
- dashboard first load (D): with no stored semester, resolve + persist the
default (courses + semesters fetch) BEFORE the scoped fetch and early-return;
the effect re-run performs the single scoped fetch. Zero-courses stays a
single unscoped pass.
- cross-screen default (E): new useActiveSemester.ensureDefaultActiveSemester
(termLabels) persists a default when none is stored, called from
Learn/Quiz/Tree/Study once course term labels are in hand.
- prune (F): drop orphaned partitionCurrentAndArchive, groupCoursesByTerm,
UNKNOWN_TERM_LABEL, CoursePartition (+ TermGroup) and their test blocks.
Tests: resolve_offering conflict-retry + non-409 propagation units; add_course
duplicate-path contract; ManageCoursesModal component tests for the
already-existed/success/in-flight paths; Dashboard single-scoped-fetch,
zero-courses and CoursesKey empty-state renders; ensureDefaultActiveSemester
units.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* rework(360): default = All semesters — semester scoping is opt-in (e2e veto)
The e2e lane vetoed the auto-default: the rich seed spans Fall 2025 /
Spring 2026 / Summer 2026 and 7 of 11 journeys failed because the
auto-resolved term silently hid cross-term fixtures (dashboard lost
MATH210; the graph shrank to 1 of 17 nodes). New semantics:
- Default = ALL SEMESTERS (unscoped). An empty stored active-semester
value IS the default and means "All semesters": removed
ensureDefaultActiveSemester + its Learn/Quiz/Tree/Study call sites,
Dashboard's default-resolution early-return pass (deferredToScopedPass)
and its getSemesters/courseTermLabels/resolveActiveSemester usage.
resolveActiveSemester itself is deleted (orphaned — the hub now reads
the stored value raw). Hook, change events, and hydration gating stay.
- Hub UI: ManageCoursesModal grows an explicit "All semesters" tab —
active when the stored value is empty, clicking it clears the value;
term tabs unchanged (both carry aria-pressed). A picked term still
persists and scopes every surface.
- Kept from the review batch: CoursesKey empty-state reachability, the
already_existed info toast, in-flight Add disable, the 0036 partial
unique index + resolve_offering conflict retry, semesters.ts pruning.
- New journey frontend/e2e/semester-scope.spec.ts: default shows
cross-term courses together (MATH210 + BIO110), picking Fall 2025 in
the hub hides Spring-only MATH210, "All semesters" restores it. Hub
opens via new dashboard-courses-manage testid (documented in
docs/frontend-testids.md); tabs selected by role/name.
- Tests reworked: Dashboard default test now asserts one unscoped fetch
with both terms visible; /api/semesters-failure test dropped (no
semesters fetch remains); ensureDefault unit tests replaced by hub tab
component tests (All active by default / term persists / All clears).
- Design doc amendment records the veto and the opt-in scoping decision.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: AndresL230 <190146319+AndresL230@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@AndresL230
AndresL230 deleted the fix/319-course-node-topic branch August 2, 2026 18:30
Sign up for freeto 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.

Clicking on a course node will result in the course name being selected as a course topic automatically

2 participants

@Darkest-Teddy@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(dashboard): don't seed course name as tutor topic on node click (#319) - #369

Merged
AndresL230 merged 2 commits into
mainfrom
fix/319-course-node-topic
Jul 29, 2026
Merged

fix(dashboard): don't seed course name as tutor topic on node click (#319)#369
AndresL230 merged 2 commits into
mainfrom
fix/319-course-node-topic

Conversation

@Darkest-Teddy

@Darkest-TeddyDarkest-Teddy commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Closes#319.

Problem

Clicking a course node on the homepage knowledge graph redirected to the AI Tutor with the course name pre-filled as the session topic. The node-click handlers unconditionally set topic=n.name; for a subject-root (course) node that name is the course.

Fix

  • New shared helper learnHrefForNode(node) in frontend/src/lib/data.ts:
    • omits topic for subject-root nodes → tutor opens scoped to the course with an empty topic picker so the learner chooses a concept;
    • seeds topic=name only for concept nodes.
  • Wired into both Dashboard.tsx graph handlers (compact + fullscreen) and Tree.tsx's "Learn this", replacing duplicated inline URL building.
  • Latent bug also fixed: the handlers passed the course as course_id, but the Learn screen reads searchParams.get("course") — so course scope was silently dropped on every node click. The helper now passes course.

Tests

Base

Stacked on feat/semester-scoped-learning (PR #360): the homepage-graph handlers this touches only exist in this form on that branch.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Improved navigation from knowledge graph concepts to the appropriate Learn experience.
    • Added support for preserving course context and selecting learning modes, including flashcards.
    • Updated quiz navigation so subject-level entries open the quiz directly, while specific concepts retain their topic.
  • Bug Fixes

    • Standardized Learn links across dashboard and tree views for more reliable routing.
  • Tests

    • Added coverage for topic, course, and learning-mode URL parameters.

@coderabbitai

coderabbitaiBot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Dashboard and tree navigation now use a shared helper for Learn URLs. Subject-root nodes omit topic parameters, course scope uses course, Quiz routes no longer include course_id, and URL behavior is covered by tests.

Changes

Learning navigation

Layer / File(s)Summary
Learn URL helper and coverage
frontend/src/lib/data.ts, frontend/src/lib/data.test.ts
Adds learnHrefForNode and tests topic, mode, and course query construction.
Dashboard graph navigation
frontend/src/components/screens/Dashboard.tsx
Routes normal and fullscreen graph selections through the shared Learn URL helper.
Tree Learn and Quiz navigation
frontend/src/components/screens/Tree.tsx
Centralizes Learn navigation and routes subject-root Quiz selections to /quiz, with topic-based routes for other nodes.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

  • SaplingLearn/Sapling#53: Updates backend Learn endpoint and session handling to use course_id in the related deep-link flow.

Suggested reviewers:jose-gael-cruz-lopez

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Out of Scope Changes check⚠️ WarningTree.tsx's quiz-routing changes go beyond #319's tutor-topic bug fix and appear unrelated to the linked issue scope.Either move the quiz-routing change to a separate PR or add a linked issue and scope note if it is intended to be part of this change.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Linked Issues check✅ PassedThe changes stop course nodes from seeding the tutor topic and switch to the course query param, which satisfies #319.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check✅ PassedThe title is concise and describes the main fix, though it only mentions the dashboard side of a broader navigation change.
Description check✅ PassedThe PR description covers the problem, fix, related issue, tests, and context, even though it doesn't follow the template headings exactly.
✨ Finishing Touches 💡 1
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch fix/319-course-node-topic
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/319-course-node-topic

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.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jul 21, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging0062744Commit Preview URL

Branch Preview URL
Jul 29 2026, 10:59 AM

Darkest-Teddyand others added 2 commits July 29, 2026 03:54
…319)
Clicking a course (subject-root) node on the homepage graph opened the AI
Tutor with the course name pre-filled as the session topic. The node-click
handlers unconditionally set `topic=n.name`; for a course node that name is
the course itself.
Add a shared `learnHrefForNode` helper in lib/data.ts that omits the topic
for subject-root nodes (scoping the tutor to the course with an empty topic
picker instead) and seeds `topic` only for concept nodes. Wire it into both
Dashboard graph handlers and Tree's "Learn this".
Also fixes a latent scope-drop in the same handlers: the course was passed
as `course_id`, but the Learn screen reads the `course` param, so course
scope was silently lost on every node click.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…319)
Follow-up to the tutor fix. The Tree detail panel's "Quick quiz" button ran
onQuiz for any node, including subject-root (course) nodes, passing
`topic=<course name>`. Unlike the tutor, the Quiz screen resolves `topic`
against concepts (roots excluded) so the course name never actually became
the quiz topic — but the param was dead and misleading.
Guard onQuiz so course nodes open the quiz picker with no seeded topic
(mirrors the tutor behavior). Also drop the `course_id` query param: the
Quiz screen only reads `topic`/`concept`, so it was dead.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@AndresL230
AndresL230force-pushed the fix/319-course-node-topic branch from 4a17bc7 to 0062744CompareJuly 29, 2026 10:57
@AndresL230
AndresL230 changed the base branch from feat/semester-scoped-learning to mainJuly 29, 2026 10:57
@AndresL230
AndresL230 marked this pull request as ready for review July 29, 2026 10:57
@AndresL230
AndresL230 merged commit 0736e43 into mainJul 29, 2026
5 checks passed
AndresL230 added a commit that referenced this pull request Jul 29, 2026
Rework of PR #360 (feat/semester-scoped-learning) replayed onto current
main as a single commit, resolving the drift accumulated since the
merge-base (~40 commits, incl. the #349 Learn.tsx SSE rewrite, the #355
subject-root dedup, the #352 content-hash chunk ids, and the #140
current/archive dashboard split, which the semester tabs supersede per
the branch's own reconciliation).
Backend semester scoping:
- services/academics.py: term_id_for_label (semester label -> term id,
with term-id fallback) + user_course_ids_for_term (enrollments ->
offerings -> term-filtered course ids).
- services/graph_service.py: get_graph/get_recommendations accept an
optional semester label; nodes/edges/stats and the synthesized subject
roots are restricted to that term's courses (composes with the #355
per-course subject-root dedup — roots are built from the already-
filtered enrollment list).
- add_course: no-retake rule across ALL terms (returns already_existed
with the existing term label) and an optional term label so the hub
enrolls into the tab being viewed, falling back to the current term.
- routes/graph.py: optional ?semester= on GET /api/graph/{user_id} and
/recommendations; AddCourseBody gains term.
- routes/gradebook.py: private _term_id_for_semester duplicate removed;
call sites now use the shared academics.term_id_for_label (follow-up
noted on the original PR).
Frontend semester scoping:
- lib/useActiveSemester.ts: localStorage-backed active-semester hook
(cross-tab sync, hydration flag so first fetches are scoped once).
- Dashboard/Tree/Learn/Quiz/Study fetch the graph scoped to the active
semester and scope course/concept pickers to it; Learn's scoping was
redone by hand against main's SSE-streaming Learn.tsx (pass the active
semester into the bootstrap getGraph, term on concepts, scopedCourses
for the course select, TopicPicker term filter — main's own
suggest/highlight logic kept, not duplicated).
- ManageCoursesModal becomes the Courses & Semesters hub (per-term tabs,
enroll-into-tab, no-retake feedback); the Dashboard archive rail from
#140 is replaced by the active-semester scoping.
- vitest.setup.ts: install an in-memory localStorage when the jsdom
environment exposes none (jsdom 29 under Vitest leaves
window.localStorage undefined), so useActiveSemester/useLayoutPref
are testable in DOM tests.
Per-type document chunking + tutor integrity:
- services/chunker.py: chunk_for_category routes prose-like categories
(essays/assignments) to a sentence-aware prose chunker, others to the
existing chunker; routes/documents.py passes the doc category through;
backfill script follows. Composes with #352's content-addressed chunk
ids (sha256 over chunk text — only boundaries change).
- prompts/preamble.txt: academic-integrity rule for the tutor (guide,
never hand over graded-work answers).
Conflict resolutions: test_document_indexing.py keeps both main's #439
relevance-gate test (repointed at the chunk_for_category seam) and the
branch's category-passthrough test; Dashboard.tsx keeps main's
IS_TEST_MODE/now + #369 learnHrefForNode imports alongside
courseTermLabels; eslint-suppressions.json re-baselined (Dashboard
no-restricted-syntax count drops with the removed archive UI).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AndresL230 added a commit that referenced this pull request Jul 29, 2026
Rework of PR #360 (feat/semester-scoped-learning) replayed onto current
main as a single commit, resolving the drift accumulated since the
merge-base (~40 commits, incl. the #349 Learn.tsx SSE rewrite, the #355
subject-root dedup, the #352 content-hash chunk ids, and the #140
current/archive dashboard split, which the semester tabs supersede per
the branch's own reconciliation).
Backend semester scoping:
- services/academics.py: term_id_for_label (semester label -> term id,
with term-id fallback) + user_course_ids_for_term (enrollments ->
offerings -> term-filtered course ids).
- services/graph_service.py: get_graph/get_recommendations accept an
optional semester label; nodes/edges/stats and the synthesized subject
roots are restricted to that term's courses (composes with the #355
per-course subject-root dedup — roots are built from the already-
filtered enrollment list).
- add_course: no-retake rule across ALL terms (returns already_existed
with the existing term label) and an optional term label so the hub
enrolls into the tab being viewed, falling back to the current term.
- routes/graph.py: optional ?semester= on GET /api/graph/{user_id} and
/recommendations; AddCourseBody gains term.
- routes/gradebook.py: private _term_id_for_semester duplicate removed;
call sites now use the shared academics.term_id_for_label (follow-up
noted on the original PR).
Frontend semester scoping:
- lib/useActiveSemester.ts: localStorage-backed active-semester hook
(cross-tab sync, hydration flag so first fetches are scoped once).
- Dashboard/Tree/Learn/Quiz/Study fetch the graph scoped to the active
semester and scope course/concept pickers to it; Learn's scoping was
redone by hand against main's SSE-streaming Learn.tsx (pass the active
semester into the bootstrap getGraph, term on concepts, scopedCourses
for the course select, TopicPicker term filter — main's own
suggest/highlight logic kept, not duplicated).
- ManageCoursesModal becomes the Courses & Semesters hub (per-term tabs,
enroll-into-tab, no-retake feedback); the Dashboard archive rail from
#140 is replaced by the active-semester scoping.
- vitest.setup.ts: install an in-memory localStorage when the jsdom
environment exposes none (jsdom 29 under Vitest leaves
window.localStorage undefined), so useActiveSemester/useLayoutPref
are testable in DOM tests.
Per-type document chunking + tutor integrity:
- services/chunker.py: chunk_for_category routes prose-like categories
(essays/assignments) to a sentence-aware prose chunker, others to the
existing chunker; routes/documents.py passes the doc category through;
backfill script follows. Composes with #352's content-addressed chunk
ids (sha256 over chunk text — only boundaries change).
- prompts/preamble.txt: academic-integrity rule for the tutor (guide,
never hand over graded-work answers).
Conflict resolutions: test_document_indexing.py keeps both main's #439
relevance-gate test (repointed at the chunk_for_category seam) and the
branch's category-passthrough test; Dashboard.tsx keeps main's
IS_TEST_MODE/now + #369 learnHrefForNode imports alongside
courseTermLabels; eslint-suppressions.json re-baselined (Dashboard
no-restricted-syntax count drops with the removed archive UI).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AndresL230 added a commit that referenced this pull request Jul 29, 2026
* feat: semester-scoped learning + Courses & Semesters hub (#360, rebased)
Rework of PR #360 (feat/semester-scoped-learning) replayed onto current
main as a single commit, resolving the drift accumulated since the
merge-base (~40 commits, incl. the #349 Learn.tsx SSE rewrite, the #355
subject-root dedup, the #352 content-hash chunk ids, and the #140
current/archive dashboard split, which the semester tabs supersede per
the branch's own reconciliation).
Backend semester scoping:
- services/academics.py: term_id_for_label (semester label -> term id,
with term-id fallback) + user_course_ids_for_term (enrollments ->
offerings -> term-filtered course ids).
- services/graph_service.py: get_graph/get_recommendations accept an
optional semester label; nodes/edges/stats and the synthesized subject
roots are restricted to that term's courses (composes with the #355
per-course subject-root dedup — roots are built from the already-
filtered enrollment list).
- add_course: no-retake rule across ALL terms (returns already_existed
with the existing term label) and an optional term label so the hub
enrolls into the tab being viewed, falling back to the current term.
- routes/graph.py: optional ?semester= on GET /api/graph/{user_id} and
/recommendations; AddCourseBody gains term.
- routes/gradebook.py: private _term_id_for_semester duplicate removed;
call sites now use the shared academics.term_id_for_label (follow-up
noted on the original PR).
Frontend semester scoping:
- lib/useActiveSemester.ts: localStorage-backed active-semester hook
(cross-tab sync, hydration flag so first fetches are scoped once).
- Dashboard/Tree/Learn/Quiz/Study fetch the graph scoped to the active
semester and scope course/concept pickers to it; Learn's scoping was
redone by hand against main's SSE-streaming Learn.tsx (pass the active
semester into the bootstrap getGraph, term on concepts, scopedCourses
for the course select, TopicPicker term filter — main's own
suggest/highlight logic kept, not duplicated).
- ManageCoursesModal becomes the Courses & Semesters hub (per-term tabs,
enroll-into-tab, no-retake feedback); the Dashboard archive rail from
#140 is replaced by the active-semester scoping.
- vitest.setup.ts: install an in-memory localStorage when the jsdom
environment exposes none (jsdom 29 under Vitest leaves
window.localStorage undefined), so useActiveSemester/useLayoutPref
are testable in DOM tests.
Per-type document chunking + tutor integrity:
- services/chunker.py: chunk_for_category routes prose-like categories
(essays/assignments) to a sentence-aware prose chunker, others to the
existing chunker; routes/documents.py passes the doc category through;
backfill script follows. Composes with #352's content-addressed chunk
ids (sha256 over chunk text — only boundaries change).
- prompts/preamble.txt: academic-integrity rule for the tutor (guide,
never hand over graded-work answers).
Conflict resolutions: test_document_indexing.py keeps both main's #439
relevance-gate test (repointed at the chunk_for_category seam) and the
branch's category-passthrough test; Dashboard.tsx keeps main's
IS_TEST_MODE/now + #369 learnHrefForNode imports alongside
courseTermLabels; eslint-suppressions.json re-baselined (Dashboard
no-restricted-syntax count drops with the removed archive UI).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(360): review fixes — race-safe add_course, honest add toasts, single scoped dashboard fetch
Review follow-ups on the semester-scoped learning rework (#360):
- add_course race (A): new partial unique index (migration 0036) closes the
NULL-section gap in course_offerings_unique; academics.resolve_offering
(create=True) now catches the 409 from a lost create race and re-selects
the winner's offering; the ManageCoursesModal Add button disables while an
add request is in flight. The now-unreachable per-offering already-enrolled
check in add_course is removed and the single return contract documented.
- add toast honesty (B): handleAdd reads the response — already_existed shows
an informational "Already taken in <term>" toast instead of a false success;
success only toasts when a row was created.
- CoursesKey empty state (C): "Nothing enrolled this semester." is reachable —
the key stays rendered when courses exist but the active semester scopes to
none; still null when there are no courses at all.
- dashboard first load (D): with no stored semester, resolve + persist the
default (courses + semesters fetch) BEFORE the scoped fetch and early-return;
the effect re-run performs the single scoped fetch. Zero-courses stays a
single unscoped pass.
- cross-screen default (E): new useActiveSemester.ensureDefaultActiveSemester
(termLabels) persists a default when none is stored, called from
Learn/Quiz/Tree/Study once course term labels are in hand.
- prune (F): drop orphaned partitionCurrentAndArchive, groupCoursesByTerm,
UNKNOWN_TERM_LABEL, CoursePartition (+ TermGroup) and their test blocks.
Tests: resolve_offering conflict-retry + non-409 propagation units; add_course
duplicate-path contract; ManageCoursesModal component tests for the
already-existed/success/in-flight paths; Dashboard single-scoped-fetch,
zero-courses and CoursesKey empty-state renders; ensureDefaultActiveSemester
units.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* rework(360): default = All semesters — semester scoping is opt-in (e2e veto)
The e2e lane vetoed the auto-default: the rich seed spans Fall 2025 /
Spring 2026 / Summer 2026 and 7 of 11 journeys failed because the
auto-resolved term silently hid cross-term fixtures (dashboard lost
MATH210; the graph shrank to 1 of 17 nodes). New semantics:
- Default = ALL SEMESTERS (unscoped). An empty stored active-semester
value IS the default and means "All semesters": removed
ensureDefaultActiveSemester + its Learn/Quiz/Tree/Study call sites,
Dashboard's default-resolution early-return pass (deferredToScopedPass)
and its getSemesters/courseTermLabels/resolveActiveSemester usage.
resolveActiveSemester itself is deleted (orphaned — the hub now reads
the stored value raw). Hook, change events, and hydration gating stay.
- Hub UI: ManageCoursesModal grows an explicit "All semesters" tab —
active when the stored value is empty, clicking it clears the value;
term tabs unchanged (both carry aria-pressed). A picked term still
persists and scopes every surface.
- Kept from the review batch: CoursesKey empty-state reachability, the
already_existed info toast, in-flight Add disable, the 0036 partial
unique index + resolve_offering conflict retry, semesters.ts pruning.
- New journey frontend/e2e/semester-scope.spec.ts: default shows
cross-term courses together (MATH210 + BIO110), picking Fall 2025 in
the hub hides Spring-only MATH210, "All semesters" restores it. Hub
opens via new dashboard-courses-manage testid (documented in
docs/frontend-testids.md); tabs selected by role/name.
- Tests reworked: Dashboard default test now asserts one unscoped fetch
with both terms visible; /api/semesters-failure test dropped (no
semesters fetch remains); ensureDefault unit tests replaced by hub tab
component tests (All active by default / term persists / All clears).
- Design doc amendment records the veto and the opt-in scoping decision.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: AndresL230 <190146319+AndresL230@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@AndresL230
AndresL230 deleted the fix/319-course-node-topic branch August 2, 2026 18:30
Sign up for freeto 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.

Clicking on a course node will result in the course name being selected as a course topic automatically

2 participants

@Darkest-Teddy@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(dashboard): don't seed course name as tutor topic on node click (#319) - #369

Merged
AndresL230 merged 2 commits into
mainfrom
fix/319-course-node-topic
Jul 29, 2026
Merged

fix(dashboard): don't seed course name as tutor topic on node click (#319)#369
AndresL230 merged 2 commits into
mainfrom
fix/319-course-node-topic

Conversation

@Darkest-Teddy

@Darkest-TeddyDarkest-Teddy commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Closes#319.

Problem

Clicking a course node on the homepage knowledge graph redirected to the AI Tutor with the course name pre-filled as the session topic. The node-click handlers unconditionally set topic=n.name; for a subject-root (course) node that name is the course.

Fix

  • New shared helper learnHrefForNode(node) in frontend/src/lib/data.ts:
    • omits topic for subject-root nodes → tutor opens scoped to the course with an empty topic picker so the learner chooses a concept;
    • seeds topic=name only for concept nodes.
  • Wired into both Dashboard.tsx graph handlers (compact + fullscreen) and Tree.tsx's "Learn this", replacing duplicated inline URL building.
  • Latent bug also fixed: the handlers passed the course as course_id, but the Learn screen reads searchParams.get("course") — so course scope was silently dropped on every node click. The helper now passes course.

Tests

Base

Stacked on feat/semester-scoped-learning (PR #360): the homepage-graph handlers this touches only exist in this form on that branch.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Improved navigation from knowledge graph concepts to the appropriate Learn experience.
    • Added support for preserving course context and selecting learning modes, including flashcards.
    • Updated quiz navigation so subject-level entries open the quiz directly, while specific concepts retain their topic.
  • Bug Fixes

    • Standardized Learn links across dashboard and tree views for more reliable routing.
  • Tests

    • Added coverage for topic, course, and learning-mode URL parameters.

@coderabbitai

coderabbitaiBot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Dashboard and tree navigation now use a shared helper for Learn URLs. Subject-root nodes omit topic parameters, course scope uses course, Quiz routes no longer include course_id, and URL behavior is covered by tests.

Changes

Learning navigation

Layer / File(s)Summary
Learn URL helper and coverage
frontend/src/lib/data.ts, frontend/src/lib/data.test.ts
Adds learnHrefForNode and tests topic, mode, and course query construction.
Dashboard graph navigation
frontend/src/components/screens/Dashboard.tsx
Routes normal and fullscreen graph selections through the shared Learn URL helper.
Tree Learn and Quiz navigation
frontend/src/components/screens/Tree.tsx
Centralizes Learn navigation and routes subject-root Quiz selections to /quiz, with topic-based routes for other nodes.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

  • SaplingLearn/Sapling#53: Updates backend Learn endpoint and session handling to use course_id in the related deep-link flow.

Suggested reviewers:jose-gael-cruz-lopez

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Out of Scope Changes check⚠️ WarningTree.tsx's quiz-routing changes go beyond #319's tutor-topic bug fix and appear unrelated to the linked issue scope.Either move the quiz-routing change to a separate PR or add a linked issue and scope note if it is intended to be part of this change.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Linked Issues check✅ PassedThe changes stop course nodes from seeding the tutor topic and switch to the course query param, which satisfies #319.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check✅ PassedThe title is concise and describes the main fix, though it only mentions the dashboard side of a broader navigation change.
Description check✅ PassedThe PR description covers the problem, fix, related issue, tests, and context, even though it doesn't follow the template headings exactly.
✨ Finishing Touches 💡 1
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch fix/319-course-node-topic
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/319-course-node-topic

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.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jul 21, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging0062744Commit Preview URL

Branch Preview URL
Jul 29 2026, 10:59 AM

Darkest-Teddyand others added 2 commits July 29, 2026 03:54
…319)
Clicking a course (subject-root) node on the homepage graph opened the AI
Tutor with the course name pre-filled as the session topic. The node-click
handlers unconditionally set `topic=n.name`; for a course node that name is
the course itself.
Add a shared `learnHrefForNode` helper in lib/data.ts that omits the topic
for subject-root nodes (scoping the tutor to the course with an empty topic
picker instead) and seeds `topic` only for concept nodes. Wire it into both
Dashboard graph handlers and Tree's "Learn this".
Also fixes a latent scope-drop in the same handlers: the course was passed
as `course_id`, but the Learn screen reads the `course` param, so course
scope was silently lost on every node click.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…319)
Follow-up to the tutor fix. The Tree detail panel's "Quick quiz" button ran
onQuiz for any node, including subject-root (course) nodes, passing
`topic=<course name>`. Unlike the tutor, the Quiz screen resolves `topic`
against concepts (roots excluded) so the course name never actually became
the quiz topic — but the param was dead and misleading.
Guard onQuiz so course nodes open the quiz picker with no seeded topic
(mirrors the tutor behavior). Also drop the `course_id` query param: the
Quiz screen only reads `topic`/`concept`, so it was dead.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@AndresL230
AndresL230force-pushed the fix/319-course-node-topic branch from 4a17bc7 to 0062744CompareJuly 29, 2026 10:57
@AndresL230
AndresL230 changed the base branch from feat/semester-scoped-learning to mainJuly 29, 2026 10:57
@AndresL230
AndresL230 marked this pull request as ready for review July 29, 2026 10:57
@AndresL230
AndresL230 merged commit 0736e43 into mainJul 29, 2026
5 checks passed
AndresL230 added a commit that referenced this pull request Jul 29, 2026
Rework of PR #360 (feat/semester-scoped-learning) replayed onto current
main as a single commit, resolving the drift accumulated since the
merge-base (~40 commits, incl. the #349 Learn.tsx SSE rewrite, the #355
subject-root dedup, the #352 content-hash chunk ids, and the #140
current/archive dashboard split, which the semester tabs supersede per
the branch's own reconciliation).
Backend semester scoping:
- services/academics.py: term_id_for_label (semester label -> term id,
with term-id fallback) + user_course_ids_for_term (enrollments ->
offerings -> term-filtered course ids).
- services/graph_service.py: get_graph/get_recommendations accept an
optional semester label; nodes/edges/stats and the synthesized subject
roots are restricted to that term's courses (composes with the #355
per-course subject-root dedup — roots are built from the already-
filtered enrollment list).
- add_course: no-retake rule across ALL terms (returns already_existed
with the existing term label) and an optional term label so the hub
enrolls into the tab being viewed, falling back to the current term.
- routes/graph.py: optional ?semester= on GET /api/graph/{user_id} and
/recommendations; AddCourseBody gains term.
- routes/gradebook.py: private _term_id_for_semester duplicate removed;
call sites now use the shared academics.term_id_for_label (follow-up
noted on the original PR).
Frontend semester scoping:
- lib/useActiveSemester.ts: localStorage-backed active-semester hook
(cross-tab sync, hydration flag so first fetches are scoped once).
- Dashboard/Tree/Learn/Quiz/Study fetch the graph scoped to the active
semester and scope course/concept pickers to it; Learn's scoping was
redone by hand against main's SSE-streaming Learn.tsx (pass the active
semester into the bootstrap getGraph, term on concepts, scopedCourses
for the course select, TopicPicker term filter — main's own
suggest/highlight logic kept, not duplicated).
- ManageCoursesModal becomes the Courses & Semesters hub (per-term tabs,
enroll-into-tab, no-retake feedback); the Dashboard archive rail from
#140 is replaced by the active-semester scoping.
- vitest.setup.ts: install an in-memory localStorage when the jsdom
environment exposes none (jsdom 29 under Vitest leaves
window.localStorage undefined), so useActiveSemester/useLayoutPref
are testable in DOM tests.
Per-type document chunking + tutor integrity:
- services/chunker.py: chunk_for_category routes prose-like categories
(essays/assignments) to a sentence-aware prose chunker, others to the
existing chunker; routes/documents.py passes the doc category through;
backfill script follows. Composes with #352's content-addressed chunk
ids (sha256 over chunk text — only boundaries change).
- prompts/preamble.txt: academic-integrity rule for the tutor (guide,
never hand over graded-work answers).
Conflict resolutions: test_document_indexing.py keeps both main's #439
relevance-gate test (repointed at the chunk_for_category seam) and the
branch's category-passthrough test; Dashboard.tsx keeps main's
IS_TEST_MODE/now + #369 learnHrefForNode imports alongside
courseTermLabels; eslint-suppressions.json re-baselined (Dashboard
no-restricted-syntax count drops with the removed archive UI).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AndresL230 added a commit that referenced this pull request Jul 29, 2026
Rework of PR #360 (feat/semester-scoped-learning) replayed onto current
main as a single commit, resolving the drift accumulated since the
merge-base (~40 commits, incl. the #349 Learn.tsx SSE rewrite, the #355
subject-root dedup, the #352 content-hash chunk ids, and the #140
current/archive dashboard split, which the semester tabs supersede per
the branch's own reconciliation).
Backend semester scoping:
- services/academics.py: term_id_for_label (semester label -> term id,
with term-id fallback) + user_course_ids_for_term (enrollments ->
offerings -> term-filtered course ids).
- services/graph_service.py: get_graph/get_recommendations accept an
optional semester label; nodes/edges/stats and the synthesized subject
roots are restricted to that term's courses (composes with the #355
per-course subject-root dedup — roots are built from the already-
filtered enrollment list).
- add_course: no-retake rule across ALL terms (returns already_existed
with the existing term label) and an optional term label so the hub
enrolls into the tab being viewed, falling back to the current term.
- routes/graph.py: optional ?semester= on GET /api/graph/{user_id} and
/recommendations; AddCourseBody gains term.
- routes/gradebook.py: private _term_id_for_semester duplicate removed;
call sites now use the shared academics.term_id_for_label (follow-up
noted on the original PR).
Frontend semester scoping:
- lib/useActiveSemester.ts: localStorage-backed active-semester hook
(cross-tab sync, hydration flag so first fetches are scoped once).
- Dashboard/Tree/Learn/Quiz/Study fetch the graph scoped to the active
semester and scope course/concept pickers to it; Learn's scoping was
redone by hand against main's SSE-streaming Learn.tsx (pass the active
semester into the bootstrap getGraph, term on concepts, scopedCourses
for the course select, TopicPicker term filter — main's own
suggest/highlight logic kept, not duplicated).
- ManageCoursesModal becomes the Courses & Semesters hub (per-term tabs,
enroll-into-tab, no-retake feedback); the Dashboard archive rail from
#140 is replaced by the active-semester scoping.
- vitest.setup.ts: install an in-memory localStorage when the jsdom
environment exposes none (jsdom 29 under Vitest leaves
window.localStorage undefined), so useActiveSemester/useLayoutPref
are testable in DOM tests.
Per-type document chunking + tutor integrity:
- services/chunker.py: chunk_for_category routes prose-like categories
(essays/assignments) to a sentence-aware prose chunker, others to the
existing chunker; routes/documents.py passes the doc category through;
backfill script follows. Composes with #352's content-addressed chunk
ids (sha256 over chunk text — only boundaries change).
- prompts/preamble.txt: academic-integrity rule for the tutor (guide,
never hand over graded-work answers).
Conflict resolutions: test_document_indexing.py keeps both main's #439
relevance-gate test (repointed at the chunk_for_category seam) and the
branch's category-passthrough test; Dashboard.tsx keeps main's
IS_TEST_MODE/now + #369 learnHrefForNode imports alongside
courseTermLabels; eslint-suppressions.json re-baselined (Dashboard
no-restricted-syntax count drops with the removed archive UI).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AndresL230 added a commit that referenced this pull request Jul 29, 2026
* feat: semester-scoped learning + Courses & Semesters hub (#360, rebased)
Rework of PR #360 (feat/semester-scoped-learning) replayed onto current
main as a single commit, resolving the drift accumulated since the
merge-base (~40 commits, incl. the #349 Learn.tsx SSE rewrite, the #355
subject-root dedup, the #352 content-hash chunk ids, and the #140
current/archive dashboard split, which the semester tabs supersede per
the branch's own reconciliation).
Backend semester scoping:
- services/academics.py: term_id_for_label (semester label -> term id,
with term-id fallback) + user_course_ids_for_term (enrollments ->
offerings -> term-filtered course ids).
- services/graph_service.py: get_graph/get_recommendations accept an
optional semester label; nodes/edges/stats and the synthesized subject
roots are restricted to that term's courses (composes with the #355
per-course subject-root dedup — roots are built from the already-
filtered enrollment list).
- add_course: no-retake rule across ALL terms (returns already_existed
with the existing term label) and an optional term label so the hub
enrolls into the tab being viewed, falling back to the current term.
- routes/graph.py: optional ?semester= on GET /api/graph/{user_id} and
/recommendations; AddCourseBody gains term.
- routes/gradebook.py: private _term_id_for_semester duplicate removed;
call sites now use the shared academics.term_id_for_label (follow-up
noted on the original PR).
Frontend semester scoping:
- lib/useActiveSemester.ts: localStorage-backed active-semester hook
(cross-tab sync, hydration flag so first fetches are scoped once).
- Dashboard/Tree/Learn/Quiz/Study fetch the graph scoped to the active
semester and scope course/concept pickers to it; Learn's scoping was
redone by hand against main's SSE-streaming Learn.tsx (pass the active
semester into the bootstrap getGraph, term on concepts, scopedCourses
for the course select, TopicPicker term filter — main's own
suggest/highlight logic kept, not duplicated).
- ManageCoursesModal becomes the Courses & Semesters hub (per-term tabs,
enroll-into-tab, no-retake feedback); the Dashboard archive rail from
#140 is replaced by the active-semester scoping.
- vitest.setup.ts: install an in-memory localStorage when the jsdom
environment exposes none (jsdom 29 under Vitest leaves
window.localStorage undefined), so useActiveSemester/useLayoutPref
are testable in DOM tests.
Per-type document chunking + tutor integrity:
- services/chunker.py: chunk_for_category routes prose-like categories
(essays/assignments) to a sentence-aware prose chunker, others to the
existing chunker; routes/documents.py passes the doc category through;
backfill script follows. Composes with #352's content-addressed chunk
ids (sha256 over chunk text — only boundaries change).
- prompts/preamble.txt: academic-integrity rule for the tutor (guide,
never hand over graded-work answers).
Conflict resolutions: test_document_indexing.py keeps both main's #439
relevance-gate test (repointed at the chunk_for_category seam) and the
branch's category-passthrough test; Dashboard.tsx keeps main's
IS_TEST_MODE/now + #369 learnHrefForNode imports alongside
courseTermLabels; eslint-suppressions.json re-baselined (Dashboard
no-restricted-syntax count drops with the removed archive UI).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(360): review fixes — race-safe add_course, honest add toasts, single scoped dashboard fetch
Review follow-ups on the semester-scoped learning rework (#360):
- add_course race (A): new partial unique index (migration 0036) closes the
NULL-section gap in course_offerings_unique; academics.resolve_offering
(create=True) now catches the 409 from a lost create race and re-selects
the winner's offering; the ManageCoursesModal Add button disables while an
add request is in flight. The now-unreachable per-offering already-enrolled
check in add_course is removed and the single return contract documented.
- add toast honesty (B): handleAdd reads the response — already_existed shows
an informational "Already taken in <term>" toast instead of a false success;
success only toasts when a row was created.
- CoursesKey empty state (C): "Nothing enrolled this semester." is reachable —
the key stays rendered when courses exist but the active semester scopes to
none; still null when there are no courses at all.
- dashboard first load (D): with no stored semester, resolve + persist the
default (courses + semesters fetch) BEFORE the scoped fetch and early-return;
the effect re-run performs the single scoped fetch. Zero-courses stays a
single unscoped pass.
- cross-screen default (E): new useActiveSemester.ensureDefaultActiveSemester
(termLabels) persists a default when none is stored, called from
Learn/Quiz/Tree/Study once course term labels are in hand.
- prune (F): drop orphaned partitionCurrentAndArchive, groupCoursesByTerm,
UNKNOWN_TERM_LABEL, CoursePartition (+ TermGroup) and their test blocks.
Tests: resolve_offering conflict-retry + non-409 propagation units; add_course
duplicate-path contract; ManageCoursesModal component tests for the
already-existed/success/in-flight paths; Dashboard single-scoped-fetch,
zero-courses and CoursesKey empty-state renders; ensureDefaultActiveSemester
units.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* rework(360): default = All semesters — semester scoping is opt-in (e2e veto)
The e2e lane vetoed the auto-default: the rich seed spans Fall 2025 /
Spring 2026 / Summer 2026 and 7 of 11 journeys failed because the
auto-resolved term silently hid cross-term fixtures (dashboard lost
MATH210; the graph shrank to 1 of 17 nodes). New semantics:
- Default = ALL SEMESTERS (unscoped). An empty stored active-semester
value IS the default and means "All semesters": removed
ensureDefaultActiveSemester + its Learn/Quiz/Tree/Study call sites,
Dashboard's default-resolution early-return pass (deferredToScopedPass)
and its getSemesters/courseTermLabels/resolveActiveSemester usage.
resolveActiveSemester itself is deleted (orphaned — the hub now reads
the stored value raw). Hook, change events, and hydration gating stay.
- Hub UI: ManageCoursesModal grows an explicit "All semesters" tab —
active when the stored value is empty, clicking it clears the value;
term tabs unchanged (both carry aria-pressed). A picked term still
persists and scopes every surface.
- Kept from the review batch: CoursesKey empty-state reachability, the
already_existed info toast, in-flight Add disable, the 0036 partial
unique index + resolve_offering conflict retry, semesters.ts pruning.
- New journey frontend/e2e/semester-scope.spec.ts: default shows
cross-term courses together (MATH210 + BIO110), picking Fall 2025 in
the hub hides Spring-only MATH210, "All semesters" restores it. Hub
opens via new dashboard-courses-manage testid (documented in
docs/frontend-testids.md); tabs selected by role/name.
- Tests reworked: Dashboard default test now asserts one unscoped fetch
with both terms visible; /api/semesters-failure test dropped (no
semesters fetch remains); ensureDefault unit tests replaced by hub tab
component tests (All active by default / term persists / All clears).
- Design doc amendment records the veto and the opt-in scoping decision.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: AndresL230 <190146319+AndresL230@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@AndresL230
AndresL230 deleted the fix/319-course-node-topic branch August 2, 2026 18:30
Sign up for freeto 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.

Clicking on a course node will result in the course name being selected as a course topic automatically

2 participants

@Darkest-Teddy@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(dashboard): don't seed course name as tutor topic on node click (#319) - #369

Merged
AndresL230 merged 2 commits into
mainfrom
fix/319-course-node-topic
Jul 29, 2026
Merged

fix(dashboard): don't seed course name as tutor topic on node click (#319)#369
AndresL230 merged 2 commits into
mainfrom
fix/319-course-node-topic

Conversation

@Darkest-Teddy

@Darkest-TeddyDarkest-Teddy commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Closes#319.

Problem

Clicking a course node on the homepage knowledge graph redirected to the AI Tutor with the course name pre-filled as the session topic. The node-click handlers unconditionally set topic=n.name; for a subject-root (course) node that name is the course.

Fix

  • New shared helper learnHrefForNode(node) in frontend/src/lib/data.ts:
    • omits topic for subject-root nodes → tutor opens scoped to the course with an empty topic picker so the learner chooses a concept;
    • seeds topic=name only for concept nodes.
  • Wired into both Dashboard.tsx graph handlers (compact + fullscreen) and Tree.tsx's "Learn this", replacing duplicated inline URL building.
  • Latent bug also fixed: the handlers passed the course as course_id, but the Learn screen reads searchParams.get("course") — so course scope was silently dropped on every node click. The helper now passes course.

Tests

Base

Stacked on feat/semester-scoped-learning (PR #360): the homepage-graph handlers this touches only exist in this form on that branch.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Improved navigation from knowledge graph concepts to the appropriate Learn experience.
    • Added support for preserving course context and selecting learning modes, including flashcards.
    • Updated quiz navigation so subject-level entries open the quiz directly, while specific concepts retain their topic.
  • Bug Fixes

    • Standardized Learn links across dashboard and tree views for more reliable routing.
  • Tests

    • Added coverage for topic, course, and learning-mode URL parameters.

@coderabbitai

coderabbitaiBot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Dashboard and tree navigation now use a shared helper for Learn URLs. Subject-root nodes omit topic parameters, course scope uses course, Quiz routes no longer include course_id, and URL behavior is covered by tests.

Changes

Learning navigation

Layer / File(s)Summary
Learn URL helper and coverage
frontend/src/lib/data.ts, frontend/src/lib/data.test.ts
Adds learnHrefForNode and tests topic, mode, and course query construction.
Dashboard graph navigation
frontend/src/components/screens/Dashboard.tsx
Routes normal and fullscreen graph selections through the shared Learn URL helper.
Tree Learn and Quiz navigation
frontend/src/components/screens/Tree.tsx
Centralizes Learn navigation and routes subject-root Quiz selections to /quiz, with topic-based routes for other nodes.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

  • SaplingLearn/Sapling#53: Updates backend Learn endpoint and session handling to use course_id in the related deep-link flow.

Suggested reviewers:jose-gael-cruz-lopez

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Out of Scope Changes check⚠️ WarningTree.tsx's quiz-routing changes go beyond #319's tutor-topic bug fix and appear unrelated to the linked issue scope.Either move the quiz-routing change to a separate PR or add a linked issue and scope note if it is intended to be part of this change.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Linked Issues check✅ PassedThe changes stop course nodes from seeding the tutor topic and switch to the course query param, which satisfies #319.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check✅ PassedThe title is concise and describes the main fix, though it only mentions the dashboard side of a broader navigation change.
Description check✅ PassedThe PR description covers the problem, fix, related issue, tests, and context, even though it doesn't follow the template headings exactly.
✨ Finishing Touches 💡 1
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch fix/319-course-node-topic
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/319-course-node-topic

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.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jul 21, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging0062744Commit Preview URL

Branch Preview URL
Jul 29 2026, 10:59 AM

Darkest-Teddyand others added 2 commits July 29, 2026 03:54
…319)
Clicking a course (subject-root) node on the homepage graph opened the AI
Tutor with the course name pre-filled as the session topic. The node-click
handlers unconditionally set `topic=n.name`; for a course node that name is
the course itself.
Add a shared `learnHrefForNode` helper in lib/data.ts that omits the topic
for subject-root nodes (scoping the tutor to the course with an empty topic
picker instead) and seeds `topic` only for concept nodes. Wire it into both
Dashboard graph handlers and Tree's "Learn this".
Also fixes a latent scope-drop in the same handlers: the course was passed
as `course_id`, but the Learn screen reads the `course` param, so course
scope was silently lost on every node click.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…319)
Follow-up to the tutor fix. The Tree detail panel's "Quick quiz" button ran
onQuiz for any node, including subject-root (course) nodes, passing
`topic=<course name>`. Unlike the tutor, the Quiz screen resolves `topic`
against concepts (roots excluded) so the course name never actually became
the quiz topic — but the param was dead and misleading.
Guard onQuiz so course nodes open the quiz picker with no seeded topic
(mirrors the tutor behavior). Also drop the `course_id` query param: the
Quiz screen only reads `topic`/`concept`, so it was dead.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@AndresL230
AndresL230force-pushed the fix/319-course-node-topic branch from 4a17bc7 to 0062744CompareJuly 29, 2026 10:57
@AndresL230
AndresL230 changed the base branch from feat/semester-scoped-learning to mainJuly 29, 2026 10:57
@AndresL230
AndresL230 marked this pull request as ready for review July 29, 2026 10:57
@AndresL230
AndresL230 merged commit 0736e43 into mainJul 29, 2026
5 checks passed
AndresL230 added a commit that referenced this pull request Jul 29, 2026
Rework of PR #360 (feat/semester-scoped-learning) replayed onto current
main as a single commit, resolving the drift accumulated since the
merge-base (~40 commits, incl. the #349 Learn.tsx SSE rewrite, the #355
subject-root dedup, the #352 content-hash chunk ids, and the #140
current/archive dashboard split, which the semester tabs supersede per
the branch's own reconciliation).
Backend semester scoping:
- services/academics.py: term_id_for_label (semester label -> term id,
with term-id fallback) + user_course_ids_for_term (enrollments ->
offerings -> term-filtered course ids).
- services/graph_service.py: get_graph/get_recommendations accept an
optional semester label; nodes/edges/stats and the synthesized subject
roots are restricted to that term's courses (composes with the #355
per-course subject-root dedup — roots are built from the already-
filtered enrollment list).
- add_course: no-retake rule across ALL terms (returns already_existed
with the existing term label) and an optional term label so the hub
enrolls into the tab being viewed, falling back to the current term.
- routes/graph.py: optional ?semester= on GET /api/graph/{user_id} and
/recommendations; AddCourseBody gains term.
- routes/gradebook.py: private _term_id_for_semester duplicate removed;
call sites now use the shared academics.term_id_for_label (follow-up
noted on the original PR).
Frontend semester scoping:
- lib/useActiveSemester.ts: localStorage-backed active-semester hook
(cross-tab sync, hydration flag so first fetches are scoped once).
- Dashboard/Tree/Learn/Quiz/Study fetch the graph scoped to the active
semester and scope course/concept pickers to it; Learn's scoping was
redone by hand against main's SSE-streaming Learn.tsx (pass the active
semester into the bootstrap getGraph, term on concepts, scopedCourses
for the course select, TopicPicker term filter — main's own
suggest/highlight logic kept, not duplicated).
- ManageCoursesModal becomes the Courses & Semesters hub (per-term tabs,
enroll-into-tab, no-retake feedback); the Dashboard archive rail from
#140 is replaced by the active-semester scoping.
- vitest.setup.ts: install an in-memory localStorage when the jsdom
environment exposes none (jsdom 29 under Vitest leaves
window.localStorage undefined), so useActiveSemester/useLayoutPref
are testable in DOM tests.
Per-type document chunking + tutor integrity:
- services/chunker.py: chunk_for_category routes prose-like categories
(essays/assignments) to a sentence-aware prose chunker, others to the
existing chunker; routes/documents.py passes the doc category through;
backfill script follows. Composes with #352's content-addressed chunk
ids (sha256 over chunk text — only boundaries change).
- prompts/preamble.txt: academic-integrity rule for the tutor (guide,
never hand over graded-work answers).
Conflict resolutions: test_document_indexing.py keeps both main's #439
relevance-gate test (repointed at the chunk_for_category seam) and the
branch's category-passthrough test; Dashboard.tsx keeps main's
IS_TEST_MODE/now + #369 learnHrefForNode imports alongside
courseTermLabels; eslint-suppressions.json re-baselined (Dashboard
no-restricted-syntax count drops with the removed archive UI).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AndresL230 added a commit that referenced this pull request Jul 29, 2026
Rework of PR #360 (feat/semester-scoped-learning) replayed onto current
main as a single commit, resolving the drift accumulated since the
merge-base (~40 commits, incl. the #349 Learn.tsx SSE rewrite, the #355
subject-root dedup, the #352 content-hash chunk ids, and the #140
current/archive dashboard split, which the semester tabs supersede per
the branch's own reconciliation).
Backend semester scoping:
- services/academics.py: term_id_for_label (semester label -> term id,
with term-id fallback) + user_course_ids_for_term (enrollments ->
offerings -> term-filtered course ids).
- services/graph_service.py: get_graph/get_recommendations accept an
optional semester label; nodes/edges/stats and the synthesized subject
roots are restricted to that term's courses (composes with the #355
per-course subject-root dedup — roots are built from the already-
filtered enrollment list).
- add_course: no-retake rule across ALL terms (returns already_existed
with the existing term label) and an optional term label so the hub
enrolls into the tab being viewed, falling back to the current term.
- routes/graph.py: optional ?semester= on GET /api/graph/{user_id} and
/recommendations; AddCourseBody gains term.
- routes/gradebook.py: private _term_id_for_semester duplicate removed;
call sites now use the shared academics.term_id_for_label (follow-up
noted on the original PR).
Frontend semester scoping:
- lib/useActiveSemester.ts: localStorage-backed active-semester hook
(cross-tab sync, hydration flag so first fetches are scoped once).
- Dashboard/Tree/Learn/Quiz/Study fetch the graph scoped to the active
semester and scope course/concept pickers to it; Learn's scoping was
redone by hand against main's SSE-streaming Learn.tsx (pass the active
semester into the bootstrap getGraph, term on concepts, scopedCourses
for the course select, TopicPicker term filter — main's own
suggest/highlight logic kept, not duplicated).
- ManageCoursesModal becomes the Courses & Semesters hub (per-term tabs,
enroll-into-tab, no-retake feedback); the Dashboard archive rail from
#140 is replaced by the active-semester scoping.
- vitest.setup.ts: install an in-memory localStorage when the jsdom
environment exposes none (jsdom 29 under Vitest leaves
window.localStorage undefined), so useActiveSemester/useLayoutPref
are testable in DOM tests.
Per-type document chunking + tutor integrity:
- services/chunker.py: chunk_for_category routes prose-like categories
(essays/assignments) to a sentence-aware prose chunker, others to the
existing chunker; routes/documents.py passes the doc category through;
backfill script follows. Composes with #352's content-addressed chunk
ids (sha256 over chunk text — only boundaries change).
- prompts/preamble.txt: academic-integrity rule for the tutor (guide,
never hand over graded-work answers).
Conflict resolutions: test_document_indexing.py keeps both main's #439
relevance-gate test (repointed at the chunk_for_category seam) and the
branch's category-passthrough test; Dashboard.tsx keeps main's
IS_TEST_MODE/now + #369 learnHrefForNode imports alongside
courseTermLabels; eslint-suppressions.json re-baselined (Dashboard
no-restricted-syntax count drops with the removed archive UI).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AndresL230 added a commit that referenced this pull request Jul 29, 2026
* feat: semester-scoped learning + Courses & Semesters hub (#360, rebased)
Rework of PR #360 (feat/semester-scoped-learning) replayed onto current
main as a single commit, resolving the drift accumulated since the
merge-base (~40 commits, incl. the #349 Learn.tsx SSE rewrite, the #355
subject-root dedup, the #352 content-hash chunk ids, and the #140
current/archive dashboard split, which the semester tabs supersede per
the branch's own reconciliation).
Backend semester scoping:
- services/academics.py: term_id_for_label (semester label -> term id,
with term-id fallback) + user_course_ids_for_term (enrollments ->
offerings -> term-filtered course ids).
- services/graph_service.py: get_graph/get_recommendations accept an
optional semester label; nodes/edges/stats and the synthesized subject
roots are restricted to that term's courses (composes with the #355
per-course subject-root dedup — roots are built from the already-
filtered enrollment list).
- add_course: no-retake rule across ALL terms (returns already_existed
with the existing term label) and an optional term label so the hub
enrolls into the tab being viewed, falling back to the current term.
- routes/graph.py: optional ?semester= on GET /api/graph/{user_id} and
/recommendations; AddCourseBody gains term.
- routes/gradebook.py: private _term_id_for_semester duplicate removed;
call sites now use the shared academics.term_id_for_label (follow-up
noted on the original PR).
Frontend semester scoping:
- lib/useActiveSemester.ts: localStorage-backed active-semester hook
(cross-tab sync, hydration flag so first fetches are scoped once).
- Dashboard/Tree/Learn/Quiz/Study fetch the graph scoped to the active
semester and scope course/concept pickers to it; Learn's scoping was
redone by hand against main's SSE-streaming Learn.tsx (pass the active
semester into the bootstrap getGraph, term on concepts, scopedCourses
for the course select, TopicPicker term filter — main's own
suggest/highlight logic kept, not duplicated).
- ManageCoursesModal becomes the Courses & Semesters hub (per-term tabs,
enroll-into-tab, no-retake feedback); the Dashboard archive rail from
#140 is replaced by the active-semester scoping.
- vitest.setup.ts: install an in-memory localStorage when the jsdom
environment exposes none (jsdom 29 under Vitest leaves
window.localStorage undefined), so useActiveSemester/useLayoutPref
are testable in DOM tests.
Per-type document chunking + tutor integrity:
- services/chunker.py: chunk_for_category routes prose-like categories
(essays/assignments) to a sentence-aware prose chunker, others to the
existing chunker; routes/documents.py passes the doc category through;
backfill script follows. Composes with #352's content-addressed chunk
ids (sha256 over chunk text — only boundaries change).
- prompts/preamble.txt: academic-integrity rule for the tutor (guide,
never hand over graded-work answers).
Conflict resolutions: test_document_indexing.py keeps both main's #439
relevance-gate test (repointed at the chunk_for_category seam) and the
branch's category-passthrough test; Dashboard.tsx keeps main's
IS_TEST_MODE/now + #369 learnHrefForNode imports alongside
courseTermLabels; eslint-suppressions.json re-baselined (Dashboard
no-restricted-syntax count drops with the removed archive UI).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(360): review fixes — race-safe add_course, honest add toasts, single scoped dashboard fetch
Review follow-ups on the semester-scoped learning rework (#360):
- add_course race (A): new partial unique index (migration 0036) closes the
NULL-section gap in course_offerings_unique; academics.resolve_offering
(create=True) now catches the 409 from a lost create race and re-selects
the winner's offering; the ManageCoursesModal Add button disables while an
add request is in flight. The now-unreachable per-offering already-enrolled
check in add_course is removed and the single return contract documented.
- add toast honesty (B): handleAdd reads the response — already_existed shows
an informational "Already taken in <term>" toast instead of a false success;
success only toasts when a row was created.
- CoursesKey empty state (C): "Nothing enrolled this semester." is reachable —
the key stays rendered when courses exist but the active semester scopes to
none; still null when there are no courses at all.
- dashboard first load (D): with no stored semester, resolve + persist the
default (courses + semesters fetch) BEFORE the scoped fetch and early-return;
the effect re-run performs the single scoped fetch. Zero-courses stays a
single unscoped pass.
- cross-screen default (E): new useActiveSemester.ensureDefaultActiveSemester
(termLabels) persists a default when none is stored, called from
Learn/Quiz/Tree/Study once course term labels are in hand.
- prune (F): drop orphaned partitionCurrentAndArchive, groupCoursesByTerm,
UNKNOWN_TERM_LABEL, CoursePartition (+ TermGroup) and their test blocks.
Tests: resolve_offering conflict-retry + non-409 propagation units; add_course
duplicate-path contract; ManageCoursesModal component tests for the
already-existed/success/in-flight paths; Dashboard single-scoped-fetch,
zero-courses and CoursesKey empty-state renders; ensureDefaultActiveSemester
units.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* rework(360): default = All semesters — semester scoping is opt-in (e2e veto)
The e2e lane vetoed the auto-default: the rich seed spans Fall 2025 /
Spring 2026 / Summer 2026 and 7 of 11 journeys failed because the
auto-resolved term silently hid cross-term fixtures (dashboard lost
MATH210; the graph shrank to 1 of 17 nodes). New semantics:
- Default = ALL SEMESTERS (unscoped). An empty stored active-semester
value IS the default and means "All semesters": removed
ensureDefaultActiveSemester + its Learn/Quiz/Tree/Study call sites,
Dashboard's default-resolution early-return pass (deferredToScopedPass)
and its getSemesters/courseTermLabels/resolveActiveSemester usage.
resolveActiveSemester itself is deleted (orphaned — the hub now reads
the stored value raw). Hook, change events, and hydration gating stay.
- Hub UI: ManageCoursesModal grows an explicit "All semesters" tab —
active when the stored value is empty, clicking it clears the value;
term tabs unchanged (both carry aria-pressed). A picked term still
persists and scopes every surface.
- Kept from the review batch: CoursesKey empty-state reachability, the
already_existed info toast, in-flight Add disable, the 0036 partial
unique index + resolve_offering conflict retry, semesters.ts pruning.
- New journey frontend/e2e/semester-scope.spec.ts: default shows
cross-term courses together (MATH210 + BIO110), picking Fall 2025 in
the hub hides Spring-only MATH210, "All semesters" restores it. Hub
opens via new dashboard-courses-manage testid (documented in
docs/frontend-testids.md); tabs selected by role/name.
- Tests reworked: Dashboard default test now asserts one unscoped fetch
with both terms visible; /api/semesters-failure test dropped (no
semesters fetch remains); ensureDefault unit tests replaced by hub tab
component tests (All active by default / term persists / All clears).
- Design doc amendment records the veto and the opt-in scoping decision.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: AndresL230 <190146319+AndresL230@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@AndresL230
AndresL230 deleted the fix/319-course-node-topic branch August 2, 2026 18:30
Sign up for freeto 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.

Clicking on a course node will result in the course name being selected as a course topic automatically

2 participants

@Darkest-Teddy@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(dashboard): don't seed course name as tutor topic on node click (#319) - #369

Merged
AndresL230 merged 2 commits into
mainfrom
fix/319-course-node-topic
Jul 29, 2026
Merged

fix(dashboard): don't seed course name as tutor topic on node click (#319)#369
AndresL230 merged 2 commits into
mainfrom
fix/319-course-node-topic

Conversation

@Darkest-Teddy

@Darkest-TeddyDarkest-Teddy commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Closes#319.

Problem

Clicking a course node on the homepage knowledge graph redirected to the AI Tutor with the course name pre-filled as the session topic. The node-click handlers unconditionally set topic=n.name; for a subject-root (course) node that name is the course.

Fix

  • New shared helper learnHrefForNode(node) in frontend/src/lib/data.ts:
    • omits topic for subject-root nodes → tutor opens scoped to the course with an empty topic picker so the learner chooses a concept;
    • seeds topic=name only for concept nodes.
  • Wired into both Dashboard.tsx graph handlers (compact + fullscreen) and Tree.tsx's "Learn this", replacing duplicated inline URL building.
  • Latent bug also fixed: the handlers passed the course as course_id, but the Learn screen reads searchParams.get("course") — so course scope was silently dropped on every node click. The helper now passes course.

Tests

Base

Stacked on feat/semester-scoped-learning (PR #360): the homepage-graph handlers this touches only exist in this form on that branch.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Improved navigation from knowledge graph concepts to the appropriate Learn experience.
    • Added support for preserving course context and selecting learning modes, including flashcards.
    • Updated quiz navigation so subject-level entries open the quiz directly, while specific concepts retain their topic.
  • Bug Fixes

    • Standardized Learn links across dashboard and tree views for more reliable routing.
  • Tests

    • Added coverage for topic, course, and learning-mode URL parameters.

@coderabbitai

coderabbitaiBot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Dashboard and tree navigation now use a shared helper for Learn URLs. Subject-root nodes omit topic parameters, course scope uses course, Quiz routes no longer include course_id, and URL behavior is covered by tests.

Changes

Learning navigation

Layer / File(s)Summary
Learn URL helper and coverage
frontend/src/lib/data.ts, frontend/src/lib/data.test.ts
Adds learnHrefForNode and tests topic, mode, and course query construction.
Dashboard graph navigation
frontend/src/components/screens/Dashboard.tsx
Routes normal and fullscreen graph selections through the shared Learn URL helper.
Tree Learn and Quiz navigation
frontend/src/components/screens/Tree.tsx
Centralizes Learn navigation and routes subject-root Quiz selections to /quiz, with topic-based routes for other nodes.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

  • SaplingLearn/Sapling#53: Updates backend Learn endpoint and session handling to use course_id in the related deep-link flow.

Suggested reviewers:jose-gael-cruz-lopez

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Out of Scope Changes check⚠️ WarningTree.tsx's quiz-routing changes go beyond #319's tutor-topic bug fix and appear unrelated to the linked issue scope.Either move the quiz-routing change to a separate PR or add a linked issue and scope note if it is intended to be part of this change.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Linked Issues check✅ PassedThe changes stop course nodes from seeding the tutor topic and switch to the course query param, which satisfies #319.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check✅ PassedThe title is concise and describes the main fix, though it only mentions the dashboard side of a broader navigation change.
Description check✅ PassedThe PR description covers the problem, fix, related issue, tests, and context, even though it doesn't follow the template headings exactly.
✨ Finishing Touches 💡 1
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch fix/319-course-node-topic
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/319-course-node-topic

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.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jul 21, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging0062744Commit Preview URL

Branch Preview URL
Jul 29 2026, 10:59 AM

Darkest-Teddyand others added 2 commits July 29, 2026 03:54
…319)
Clicking a course (subject-root) node on the homepage graph opened the AI
Tutor with the course name pre-filled as the session topic. The node-click
handlers unconditionally set `topic=n.name`; for a course node that name is
the course itself.
Add a shared `learnHrefForNode` helper in lib/data.ts that omits the topic
for subject-root nodes (scoping the tutor to the course with an empty topic
picker instead) and seeds `topic` only for concept nodes. Wire it into both
Dashboard graph handlers and Tree's "Learn this".
Also fixes a latent scope-drop in the same handlers: the course was passed
as `course_id`, but the Learn screen reads the `course` param, so course
scope was silently lost on every node click.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…319)
Follow-up to the tutor fix. The Tree detail panel's "Quick quiz" button ran
onQuiz for any node, including subject-root (course) nodes, passing
`topic=<course name>`. Unlike the tutor, the Quiz screen resolves `topic`
against concepts (roots excluded) so the course name never actually became
the quiz topic — but the param was dead and misleading.
Guard onQuiz so course nodes open the quiz picker with no seeded topic
(mirrors the tutor behavior). Also drop the `course_id` query param: the
Quiz screen only reads `topic`/`concept`, so it was dead.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@AndresL230
AndresL230force-pushed the fix/319-course-node-topic branch from 4a17bc7 to 0062744CompareJuly 29, 2026 10:57
@AndresL230
AndresL230 changed the base branch from feat/semester-scoped-learning to mainJuly 29, 2026 10:57
@AndresL230
AndresL230 marked this pull request as ready for review July 29, 2026 10:57
@AndresL230
AndresL230 merged commit 0736e43 into mainJul 29, 2026
5 checks passed
AndresL230 added a commit that referenced this pull request Jul 29, 2026
Rework of PR #360 (feat/semester-scoped-learning) replayed onto current
main as a single commit, resolving the drift accumulated since the
merge-base (~40 commits, incl. the #349 Learn.tsx SSE rewrite, the #355
subject-root dedup, the #352 content-hash chunk ids, and the #140
current/archive dashboard split, which the semester tabs supersede per
the branch's own reconciliation).
Backend semester scoping:
- services/academics.py: term_id_for_label (semester label -> term id,
with term-id fallback) + user_course_ids_for_term (enrollments ->
offerings -> term-filtered course ids).
- services/graph_service.py: get_graph/get_recommendations accept an
optional semester label; nodes/edges/stats and the synthesized subject
roots are restricted to that term's courses (composes with the #355
per-course subject-root dedup — roots are built from the already-
filtered enrollment list).
- add_course: no-retake rule across ALL terms (returns already_existed
with the existing term label) and an optional term label so the hub
enrolls into the tab being viewed, falling back to the current term.
- routes/graph.py: optional ?semester= on GET /api/graph/{user_id} and
/recommendations; AddCourseBody gains term.
- routes/gradebook.py: private _term_id_for_semester duplicate removed;
call sites now use the shared academics.term_id_for_label (follow-up
noted on the original PR).
Frontend semester scoping:
- lib/useActiveSemester.ts: localStorage-backed active-semester hook
(cross-tab sync, hydration flag so first fetches are scoped once).
- Dashboard/Tree/Learn/Quiz/Study fetch the graph scoped to the active
semester and scope course/concept pickers to it; Learn's scoping was
redone by hand against main's SSE-streaming Learn.tsx (pass the active
semester into the bootstrap getGraph, term on concepts, scopedCourses
for the course select, TopicPicker term filter — main's own
suggest/highlight logic kept, not duplicated).
- ManageCoursesModal becomes the Courses & Semesters hub (per-term tabs,
enroll-into-tab, no-retake feedback); the Dashboard archive rail from
#140 is replaced by the active-semester scoping.
- vitest.setup.ts: install an in-memory localStorage when the jsdom
environment exposes none (jsdom 29 under Vitest leaves
window.localStorage undefined), so useActiveSemester/useLayoutPref
are testable in DOM tests.
Per-type document chunking + tutor integrity:
- services/chunker.py: chunk_for_category routes prose-like categories
(essays/assignments) to a sentence-aware prose chunker, others to the
existing chunker; routes/documents.py passes the doc category through;
backfill script follows. Composes with #352's content-addressed chunk
ids (sha256 over chunk text — only boundaries change).
- prompts/preamble.txt: academic-integrity rule for the tutor (guide,
never hand over graded-work answers).
Conflict resolutions: test_document_indexing.py keeps both main's #439
relevance-gate test (repointed at the chunk_for_category seam) and the
branch's category-passthrough test; Dashboard.tsx keeps main's
IS_TEST_MODE/now + #369 learnHrefForNode imports alongside
courseTermLabels; eslint-suppressions.json re-baselined (Dashboard
no-restricted-syntax count drops with the removed archive UI).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AndresL230 added a commit that referenced this pull request Jul 29, 2026
Rework of PR #360 (feat/semester-scoped-learning) replayed onto current
main as a single commit, resolving the drift accumulated since the
merge-base (~40 commits, incl. the #349 Learn.tsx SSE rewrite, the #355
subject-root dedup, the #352 content-hash chunk ids, and the #140
current/archive dashboard split, which the semester tabs supersede per
the branch's own reconciliation).
Backend semester scoping:
- services/academics.py: term_id_for_label (semester label -> term id,
with term-id fallback) + user_course_ids_for_term (enrollments ->
offerings -> term-filtered course ids).
- services/graph_service.py: get_graph/get_recommendations accept an
optional semester label; nodes/edges/stats and the synthesized subject
roots are restricted to that term's courses (composes with the #355
per-course subject-root dedup — roots are built from the already-
filtered enrollment list).
- add_course: no-retake rule across ALL terms (returns already_existed
with the existing term label) and an optional term label so the hub
enrolls into the tab being viewed, falling back to the current term.
- routes/graph.py: optional ?semester= on GET /api/graph/{user_id} and
/recommendations; AddCourseBody gains term.
- routes/gradebook.py: private _term_id_for_semester duplicate removed;
call sites now use the shared academics.term_id_for_label (follow-up
noted on the original PR).
Frontend semester scoping:
- lib/useActiveSemester.ts: localStorage-backed active-semester hook
(cross-tab sync, hydration flag so first fetches are scoped once).
- Dashboard/Tree/Learn/Quiz/Study fetch the graph scoped to the active
semester and scope course/concept pickers to it; Learn's scoping was
redone by hand against main's SSE-streaming Learn.tsx (pass the active
semester into the bootstrap getGraph, term on concepts, scopedCourses
for the course select, TopicPicker term filter — main's own
suggest/highlight logic kept, not duplicated).
- ManageCoursesModal becomes the Courses & Semesters hub (per-term tabs,
enroll-into-tab, no-retake feedback); the Dashboard archive rail from
#140 is replaced by the active-semester scoping.
- vitest.setup.ts: install an in-memory localStorage when the jsdom
environment exposes none (jsdom 29 under Vitest leaves
window.localStorage undefined), so useActiveSemester/useLayoutPref
are testable in DOM tests.
Per-type document chunking + tutor integrity:
- services/chunker.py: chunk_for_category routes prose-like categories
(essays/assignments) to a sentence-aware prose chunker, others to the
existing chunker; routes/documents.py passes the doc category through;
backfill script follows. Composes with #352's content-addressed chunk
ids (sha256 over chunk text — only boundaries change).
- prompts/preamble.txt: academic-integrity rule for the tutor (guide,
never hand over graded-work answers).
Conflict resolutions: test_document_indexing.py keeps both main's #439
relevance-gate test (repointed at the chunk_for_category seam) and the
branch's category-passthrough test; Dashboard.tsx keeps main's
IS_TEST_MODE/now + #369 learnHrefForNode imports alongside
courseTermLabels; eslint-suppressions.json re-baselined (Dashboard
no-restricted-syntax count drops with the removed archive UI).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AndresL230 added a commit that referenced this pull request Jul 29, 2026
* feat: semester-scoped learning + Courses & Semesters hub (#360, rebased)
Rework of PR #360 (feat/semester-scoped-learning) replayed onto current
main as a single commit, resolving the drift accumulated since the
merge-base (~40 commits, incl. the #349 Learn.tsx SSE rewrite, the #355
subject-root dedup, the #352 content-hash chunk ids, and the #140
current/archive dashboard split, which the semester tabs supersede per
the branch's own reconciliation).
Backend semester scoping:
- services/academics.py: term_id_for_label (semester label -> term id,
with term-id fallback) + user_course_ids_for_term (enrollments ->
offerings -> term-filtered course ids).
- services/graph_service.py: get_graph/get_recommendations accept an
optional semester label; nodes/edges/stats and the synthesized subject
roots are restricted to that term's courses (composes with the #355
per-course subject-root dedup — roots are built from the already-
filtered enrollment list).
- add_course: no-retake rule across ALL terms (returns already_existed
with the existing term label) and an optional term label so the hub
enrolls into the tab being viewed, falling back to the current term.
- routes/graph.py: optional ?semester= on GET /api/graph/{user_id} and
/recommendations; AddCourseBody gains term.
- routes/gradebook.py: private _term_id_for_semester duplicate removed;
call sites now use the shared academics.term_id_for_label (follow-up
noted on the original PR).
Frontend semester scoping:
- lib/useActiveSemester.ts: localStorage-backed active-semester hook
(cross-tab sync, hydration flag so first fetches are scoped once).
- Dashboard/Tree/Learn/Quiz/Study fetch the graph scoped to the active
semester and scope course/concept pickers to it; Learn's scoping was
redone by hand against main's SSE-streaming Learn.tsx (pass the active
semester into the bootstrap getGraph, term on concepts, scopedCourses
for the course select, TopicPicker term filter — main's own
suggest/highlight logic kept, not duplicated).
- ManageCoursesModal becomes the Courses & Semesters hub (per-term tabs,
enroll-into-tab, no-retake feedback); the Dashboard archive rail from
#140 is replaced by the active-semester scoping.
- vitest.setup.ts: install an in-memory localStorage when the jsdom
environment exposes none (jsdom 29 under Vitest leaves
window.localStorage undefined), so useActiveSemester/useLayoutPref
are testable in DOM tests.
Per-type document chunking + tutor integrity:
- services/chunker.py: chunk_for_category routes prose-like categories
(essays/assignments) to a sentence-aware prose chunker, others to the
existing chunker; routes/documents.py passes the doc category through;
backfill script follows. Composes with #352's content-addressed chunk
ids (sha256 over chunk text — only boundaries change).
- prompts/preamble.txt: academic-integrity rule for the tutor (guide,
never hand over graded-work answers).
Conflict resolutions: test_document_indexing.py keeps both main's #439
relevance-gate test (repointed at the chunk_for_category seam) and the
branch's category-passthrough test; Dashboard.tsx keeps main's
IS_TEST_MODE/now + #369 learnHrefForNode imports alongside
courseTermLabels; eslint-suppressions.json re-baselined (Dashboard
no-restricted-syntax count drops with the removed archive UI).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(360): review fixes — race-safe add_course, honest add toasts, single scoped dashboard fetch
Review follow-ups on the semester-scoped learning rework (#360):
- add_course race (A): new partial unique index (migration 0036) closes the
NULL-section gap in course_offerings_unique; academics.resolve_offering
(create=True) now catches the 409 from a lost create race and re-selects
the winner's offering; the ManageCoursesModal Add button disables while an
add request is in flight. The now-unreachable per-offering already-enrolled
check in add_course is removed and the single return contract documented.
- add toast honesty (B): handleAdd reads the response — already_existed shows
an informational "Already taken in <term>" toast instead of a false success;
success only toasts when a row was created.
- CoursesKey empty state (C): "Nothing enrolled this semester." is reachable —
the key stays rendered when courses exist but the active semester scopes to
none; still null when there are no courses at all.
- dashboard first load (D): with no stored semester, resolve + persist the
default (courses + semesters fetch) BEFORE the scoped fetch and early-return;
the effect re-run performs the single scoped fetch. Zero-courses stays a
single unscoped pass.
- cross-screen default (E): new useActiveSemester.ensureDefaultActiveSemester
(termLabels) persists a default when none is stored, called from
Learn/Quiz/Tree/Study once course term labels are in hand.
- prune (F): drop orphaned partitionCurrentAndArchive, groupCoursesByTerm,
UNKNOWN_TERM_LABEL, CoursePartition (+ TermGroup) and their test blocks.
Tests: resolve_offering conflict-retry + non-409 propagation units; add_course
duplicate-path contract; ManageCoursesModal component tests for the
already-existed/success/in-flight paths; Dashboard single-scoped-fetch,
zero-courses and CoursesKey empty-state renders; ensureDefaultActiveSemester
units.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* rework(360): default = All semesters — semester scoping is opt-in (e2e veto)
The e2e lane vetoed the auto-default: the rich seed spans Fall 2025 /
Spring 2026 / Summer 2026 and 7 of 11 journeys failed because the
auto-resolved term silently hid cross-term fixtures (dashboard lost
MATH210; the graph shrank to 1 of 17 nodes). New semantics:
- Default = ALL SEMESTERS (unscoped). An empty stored active-semester
value IS the default and means "All semesters": removed
ensureDefaultActiveSemester + its Learn/Quiz/Tree/Study call sites,
Dashboard's default-resolution early-return pass (deferredToScopedPass)
and its getSemesters/courseTermLabels/resolveActiveSemester usage.
resolveActiveSemester itself is deleted (orphaned — the hub now reads
the stored value raw). Hook, change events, and hydration gating stay.
- Hub UI: ManageCoursesModal grows an explicit "All semesters" tab —
active when the stored value is empty, clicking it clears the value;
term tabs unchanged (both carry aria-pressed). A picked term still
persists and scopes every surface.
- Kept from the review batch: CoursesKey empty-state reachability, the
already_existed info toast, in-flight Add disable, the 0036 partial
unique index + resolve_offering conflict retry, semesters.ts pruning.
- New journey frontend/e2e/semester-scope.spec.ts: default shows
cross-term courses together (MATH210 + BIO110), picking Fall 2025 in
the hub hides Spring-only MATH210, "All semesters" restores it. Hub
opens via new dashboard-courses-manage testid (documented in
docs/frontend-testids.md); tabs selected by role/name.
- Tests reworked: Dashboard default test now asserts one unscoped fetch
with both terms visible; /api/semesters-failure test dropped (no
semesters fetch remains); ensureDefault unit tests replaced by hub tab
component tests (All active by default / term persists / All clears).
- Design doc amendment records the veto and the opt-in scoping decision.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: AndresL230 <190146319+AndresL230@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@AndresL230
AndresL230 deleted the fix/319-course-node-topic branch August 2, 2026 18:30
Sign up for freeto 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.

Clicking on a course node will result in the course name being selected as a course topic automatically

2 participants

@Darkest-Teddy@AndresL230
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(dashboard): don't seed course name as tutor topic on node click (#319) - #369

Merged
AndresL230 merged 2 commits into
mainfrom
fix/319-course-node-topic
Jul 29, 2026
Merged

fix(dashboard): don't seed course name as tutor topic on node click (#319)#369
AndresL230 merged 2 commits into
mainfrom
fix/319-course-node-topic

Conversation

@Darkest-Teddy

@Darkest-TeddyDarkest-Teddy commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Closes#319.

Problem

Clicking a course node on the homepage knowledge graph redirected to the AI Tutor with the course name pre-filled as the session topic. The node-click handlers unconditionally set topic=n.name; for a subject-root (course) node that name is the course.

Fix

  • New shared helper learnHrefForNode(node) in frontend/src/lib/data.ts:
    • omits topic for subject-root nodes → tutor opens scoped to the course with an empty topic picker so the learner chooses a concept;
    • seeds topic=name only for concept nodes.
  • Wired into both Dashboard.tsx graph handlers (compact + fullscreen) and Tree.tsx's "Learn this", replacing duplicated inline URL building.
  • Latent bug also fixed: the handlers passed the course as course_id, but the Learn screen reads searchParams.get("course") — so course scope was silently dropped on every node click. The helper now passes course.

Tests

Base

Stacked on feat/semester-scoped-learning (PR #360): the homepage-graph handlers this touches only exist in this form on that branch.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Improved navigation from knowledge graph concepts to the appropriate Learn experience.
    • Added support for preserving course context and selecting learning modes, including flashcards.
    • Updated quiz navigation so subject-level entries open the quiz directly, while specific concepts retain their topic.
  • Bug Fixes

    • Standardized Learn links across dashboard and tree views for more reliable routing.
  • Tests

    • Added coverage for topic, course, and learning-mode URL parameters.

@coderabbitai

coderabbitaiBot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Dashboard and tree navigation now use a shared helper for Learn URLs. Subject-root nodes omit topic parameters, course scope uses course, Quiz routes no longer include course_id, and URL behavior is covered by tests.

Changes

Learning navigation

Layer / File(s)Summary
Learn URL helper and coverage
frontend/src/lib/data.ts, frontend/src/lib/data.test.ts
Adds learnHrefForNode and tests topic, mode, and course query construction.
Dashboard graph navigation
frontend/src/components/screens/Dashboard.tsx
Routes normal and fullscreen graph selections through the shared Learn URL helper.
Tree Learn and Quiz navigation
frontend/src/components/screens/Tree.tsx
Centralizes Learn navigation and routes subject-root Quiz selections to /quiz, with topic-based routes for other nodes.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

  • SaplingLearn/Sapling#53: Updates backend Learn endpoint and session handling to use course_id in the related deep-link flow.

Suggested reviewers:jose-gael-cruz-lopez

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Out of Scope Changes check⚠️ WarningTree.tsx's quiz-routing changes go beyond #319's tutor-topic bug fix and appear unrelated to the linked issue scope.Either move the quiz-routing change to a separate PR or add a linked issue and scope note if it is intended to be part of this change.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Linked Issues check✅ PassedThe changes stop course nodes from seeding the tutor topic and switch to the course query param, which satisfies #319.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check✅ PassedThe title is concise and describes the main fix, though it only mentions the dashboard side of a broader navigation change.
Description check✅ PassedThe PR description covers the problem, fix, related issue, tests, and context, even though it doesn't follow the template headings exactly.
✨ Finishing Touches 💡 1
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch fix/319-course-node-topic
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/319-course-node-topic

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.

@cloudflare-workers-and-pages

cloudflare-workers-and-pagesBot commented Jul 21, 2026

Copy link
Copy Markdown

Deploying with Cloudflare Workers Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

StatusNameLatest CommitPreview URLUpdated (UTC)
✅ Deployment successful!
View logs
frontend-staging0062744Commit Preview URL

Branch Preview URL
Jul 29 2026, 10:59 AM

Darkest-Teddyand others added 2 commits July 29, 2026 03:54
…319)
Clicking a course (subject-root) node on the homepage graph opened the AI
Tutor with the course name pre-filled as the session topic. The node-click
handlers unconditionally set `topic=n.name`; for a course node that name is
the course itself.
Add a shared `learnHrefForNode` helper in lib/data.ts that omits the topic
for subject-root nodes (scoping the tutor to the course with an empty topic
picker instead) and seeds `topic` only for concept nodes. Wire it into both
Dashboard graph handlers and Tree's "Learn this".
Also fixes a latent scope-drop in the same handlers: the course was passed
as `course_id`, but the Learn screen reads the `course` param, so course
scope was silently lost on every node click.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…319)
Follow-up to the tutor fix. The Tree detail panel's "Quick quiz" button ran
onQuiz for any node, including subject-root (course) nodes, passing
`topic=<course name>`. Unlike the tutor, the Quiz screen resolves `topic`
against concepts (roots excluded) so the course name never actually became
the quiz topic — but the param was dead and misleading.
Guard onQuiz so course nodes open the quiz picker with no seeded topic
(mirrors the tutor behavior). Also drop the `course_id` query param: the
Quiz screen only reads `topic`/`concept`, so it was dead.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@AndresL230
AndresL230force-pushed the fix/319-course-node-topic branch from 4a17bc7 to 0062744CompareJuly 29, 2026 10:57
@AndresL230
AndresL230 changed the base branch from feat/semester-scoped-learning to mainJuly 29, 2026 10:57
@AndresL230
AndresL230 marked this pull request as ready for review July 29, 2026 10:57
@AndresL230
AndresL230 merged commit 0736e43 into mainJul 29, 2026
5 checks passed
AndresL230 added a commit that referenced this pull request Jul 29, 2026
Rework of PR #360 (feat/semester-scoped-learning) replayed onto current
main as a single commit, resolving the drift accumulated since the
merge-base (~40 commits, incl. the #349 Learn.tsx SSE rewrite, the #355
subject-root dedup, the #352 content-hash chunk ids, and the #140
current/archive dashboard split, which the semester tabs supersede per
the branch's own reconciliation).
Backend semester scoping:
- services/academics.py: term_id_for_label (semester label -> term id,
with term-id fallback) + user_course_ids_for_term (enrollments ->
offerings -> term-filtered course ids).
- services/graph_service.py: get_graph/get_recommendations accept an
optional semester label; nodes/edges/stats and the synthesized subject
roots are restricted to that term's courses (composes with the #355
per-course subject-root dedup — roots are built from the already-
filtered enrollment list).
- add_course: no-retake rule across ALL terms (returns already_existed
with the existing term label) and an optional term label so the hub
enrolls into the tab being viewed, falling back to the current term.
- routes/graph.py: optional ?semester= on GET /api/graph/{user_id} and
/recommendations; AddCourseBody gains term.
- routes/gradebook.py: private _term_id_for_semester duplicate removed;
call sites now use the shared academics.term_id_for_label (follow-up
noted on the original PR).
Frontend semester scoping:
- lib/useActiveSemester.ts: localStorage-backed active-semester hook
(cross-tab sync, hydration flag so first fetches are scoped once).
- Dashboard/Tree/Learn/Quiz/Study fetch the graph scoped to the active
semester and scope course/concept pickers to it; Learn's scoping was
redone by hand against main's SSE-streaming Learn.tsx (pass the active
semester into the bootstrap getGraph, term on concepts, scopedCourses
for the course select, TopicPicker term filter — main's own
suggest/highlight logic kept, not duplicated).
- ManageCoursesModal becomes the Courses & Semesters hub (per-term tabs,
enroll-into-tab, no-retake feedback); the Dashboard archive rail from
#140 is replaced by the active-semester scoping.
- vitest.setup.ts: install an in-memory localStorage when the jsdom
environment exposes none (jsdom 29 under Vitest leaves
window.localStorage undefined), so useActiveSemester/useLayoutPref
are testable in DOM tests.
Per-type document chunking + tutor integrity:
- services/chunker.py: chunk_for_category routes prose-like categories
(essays/assignments) to a sentence-aware prose chunker, others to the
existing chunker; routes/documents.py passes the doc category through;
backfill script follows. Composes with #352's content-addressed chunk
ids (sha256 over chunk text — only boundaries change).
- prompts/preamble.txt: academic-integrity rule for the tutor (guide,
never hand over graded-work answers).
Conflict resolutions: test_document_indexing.py keeps both main's #439
relevance-gate test (repointed at the chunk_for_category seam) and the
branch's category-passthrough test; Dashboard.tsx keeps main's
IS_TEST_MODE/now + #369 learnHrefForNode imports alongside
courseTermLabels; eslint-suppressions.json re-baselined (Dashboard
no-restricted-syntax count drops with the removed archive UI).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AndresL230 added a commit that referenced this pull request Jul 29, 2026
Rework of PR #360 (feat/semester-scoped-learning) replayed onto current
main as a single commit, resolving the drift accumulated since the
merge-base (~40 commits, incl. the #349 Learn.tsx SSE rewrite, the #355
subject-root dedup, the #352 content-hash chunk ids, and the #140
current/archive dashboard split, which the semester tabs supersede per
the branch's own reconciliation).
Backend semester scoping:
- services/academics.py: term_id_for_label (semester label -> term id,
with term-id fallback) + user_course_ids_for_term (enrollments ->
offerings -> term-filtered course ids).
- services/graph_service.py: get_graph/get_recommendations accept an
optional semester label; nodes/edges/stats and the synthesized subject
roots are restricted to that term's courses (composes with the #355
per-course subject-root dedup — roots are built from the already-
filtered enrollment list).
- add_course: no-retake rule across ALL terms (returns already_existed
with the existing term label) and an optional term label so the hub
enrolls into the tab being viewed, falling back to the current term.
- routes/graph.py: optional ?semester= on GET /api/graph/{user_id} and
/recommendations; AddCourseBody gains term.
- routes/gradebook.py: private _term_id_for_semester duplicate removed;
call sites now use the shared academics.term_id_for_label (follow-up
noted on the original PR).
Frontend semester scoping:
- lib/useActiveSemester.ts: localStorage-backed active-semester hook
(cross-tab sync, hydration flag so first fetches are scoped once).
- Dashboard/Tree/Learn/Quiz/Study fetch the graph scoped to the active
semester and scope course/concept pickers to it; Learn's scoping was
redone by hand against main's SSE-streaming Learn.tsx (pass the active
semester into the bootstrap getGraph, term on concepts, scopedCourses
for the course select, TopicPicker term filter — main's own
suggest/highlight logic kept, not duplicated).
- ManageCoursesModal becomes the Courses & Semesters hub (per-term tabs,
enroll-into-tab, no-retake feedback); the Dashboard archive rail from
#140 is replaced by the active-semester scoping.
- vitest.setup.ts: install an in-memory localStorage when the jsdom
environment exposes none (jsdom 29 under Vitest leaves
window.localStorage undefined), so useActiveSemester/useLayoutPref
are testable in DOM tests.
Per-type document chunking + tutor integrity:
- services/chunker.py: chunk_for_category routes prose-like categories
(essays/assignments) to a sentence-aware prose chunker, others to the
existing chunker; routes/documents.py passes the doc category through;
backfill script follows. Composes with #352's content-addressed chunk
ids (sha256 over chunk text — only boundaries change).
- prompts/preamble.txt: academic-integrity rule for the tutor (guide,
never hand over graded-work answers).
Conflict resolutions: test_document_indexing.py keeps both main's #439
relevance-gate test (repointed at the chunk_for_category seam) and the
branch's category-passthrough test; Dashboard.tsx keeps main's
IS_TEST_MODE/now + #369 learnHrefForNode imports alongside
courseTermLabels; eslint-suppressions.json re-baselined (Dashboard
no-restricted-syntax count drops with the removed archive UI).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AndresL230 added a commit that referenced this pull request Jul 29, 2026
* feat: semester-scoped learning + Courses & Semesters hub (#360, rebased)
Rework of PR #360 (feat/semester-scoped-learning) replayed onto current
main as a single commit, resolving the drift accumulated since the
merge-base (~40 commits, incl. the #349 Learn.tsx SSE rewrite, the #355
subject-root dedup, the #352 content-hash chunk ids, and the #140
current/archive dashboard split, which the semester tabs supersede per
the branch's own reconciliation).
Backend semester scoping:
- services/academics.py: term_id_for_label (semester label -> term id,
with term-id fallback) + user_course_ids_for_term (enrollments ->
offerings -> term-filtered course ids).
- services/graph_service.py: get_graph/get_recommendations accept an
optional semester label; nodes/edges/stats and the synthesized subject
roots are restricted to that term's courses (composes with the #355
per-course subject-root dedup — roots are built from the already-
filtered enrollment list).
- add_course: no-retake rule across ALL terms (returns already_existed
with the existing term label) and an optional term label so the hub
enrolls into the tab being viewed, falling back to the current term.
- routes/graph.py: optional ?semester= on GET /api/graph/{user_id} and
/recommendations; AddCourseBody gains term.
- routes/gradebook.py: private _term_id_for_semester duplicate removed;
call sites now use the shared academics.term_id_for_label (follow-up
noted on the original PR).
Frontend semester scoping:
- lib/useActiveSemester.ts: localStorage-backed active-semester hook
(cross-tab sync, hydration flag so first fetches are scoped once).
- Dashboard/Tree/Learn/Quiz/Study fetch the graph scoped to the active
semester and scope course/concept pickers to it; Learn's scoping was
redone by hand against main's SSE-streaming Learn.tsx (pass the active
semester into the bootstrap getGraph, term on concepts, scopedCourses
for the course select, TopicPicker term filter — main's own
suggest/highlight logic kept, not duplicated).
- ManageCoursesModal becomes the Courses & Semesters hub (per-term tabs,
enroll-into-tab, no-retake feedback); the Dashboard archive rail from
#140 is replaced by the active-semester scoping.
- vitest.setup.ts: install an in-memory localStorage when the jsdom
environment exposes none (jsdom 29 under Vitest leaves
window.localStorage undefined), so useActiveSemester/useLayoutPref
are testable in DOM tests.
Per-type document chunking + tutor integrity:
- services/chunker.py: chunk_for_category routes prose-like categories
(essays/assignments) to a sentence-aware prose chunker, others to the
existing chunker; routes/documents.py passes the doc category through;
backfill script follows. Composes with #352's content-addressed chunk
ids (sha256 over chunk text — only boundaries change).
- prompts/preamble.txt: academic-integrity rule for the tutor (guide,
never hand over graded-work answers).
Conflict resolutions: test_document_indexing.py keeps both main's #439
relevance-gate test (repointed at the chunk_for_category seam) and the
branch's category-passthrough test; Dashboard.tsx keeps main's
IS_TEST_MODE/now + #369 learnHrefForNode imports alongside
courseTermLabels; eslint-suppressions.json re-baselined (Dashboard
no-restricted-syntax count drops with the removed archive UI).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(360): review fixes — race-safe add_course, honest add toasts, single scoped dashboard fetch
Review follow-ups on the semester-scoped learning rework (#360):
- add_course race (A): new partial unique index (migration 0036) closes the
NULL-section gap in course_offerings_unique; academics.resolve_offering
(create=True) now catches the 409 from a lost create race and re-selects
the winner's offering; the ManageCoursesModal Add button disables while an
add request is in flight. The now-unreachable per-offering already-enrolled
check in add_course is removed and the single return contract documented.
- add toast honesty (B): handleAdd reads the response — already_existed shows
an informational "Already taken in <term>" toast instead of a false success;
success only toasts when a row was created.
- CoursesKey empty state (C): "Nothing enrolled this semester." is reachable —
the key stays rendered when courses exist but the active semester scopes to
none; still null when there are no courses at all.
- dashboard first load (D): with no stored semester, resolve + persist the
default (courses + semesters fetch) BEFORE the scoped fetch and early-return;
the effect re-run performs the single scoped fetch. Zero-courses stays a
single unscoped pass.
- cross-screen default (E): new useActiveSemester.ensureDefaultActiveSemester
(termLabels) persists a default when none is stored, called from
Learn/Quiz/Tree/Study once course term labels are in hand.
- prune (F): drop orphaned partitionCurrentAndArchive, groupCoursesByTerm,
UNKNOWN_TERM_LABEL, CoursePartition (+ TermGroup) and their test blocks.
Tests: resolve_offering conflict-retry + non-409 propagation units; add_course
duplicate-path contract; ManageCoursesModal component tests for the
already-existed/success/in-flight paths; Dashboard single-scoped-fetch,
zero-courses and CoursesKey empty-state renders; ensureDefaultActiveSemester
units.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* rework(360): default = All semesters — semester scoping is opt-in (e2e veto)
The e2e lane vetoed the auto-default: the rich seed spans Fall 2025 /
Spring 2026 / Summer 2026 and 7 of 11 journeys failed because the
auto-resolved term silently hid cross-term fixtures (dashboard lost
MATH210; the graph shrank to 1 of 17 nodes). New semantics:
- Default = ALL SEMESTERS (unscoped). An empty stored active-semester
value IS the default and means "All semesters": removed
ensureDefaultActiveSemester + its Learn/Quiz/Tree/Study call sites,
Dashboard's default-resolution early-return pass (deferredToScopedPass)
and its getSemesters/courseTermLabels/resolveActiveSemester usage.
resolveActiveSemester itself is deleted (orphaned — the hub now reads
the stored value raw). Hook, change events, and hydration gating stay.
- Hub UI: ManageCoursesModal grows an explicit "All semesters" tab —
active when the stored value is empty, clicking it clears the value;
term tabs unchanged (both carry aria-pressed). A picked term still
persists and scopes every surface.
- Kept from the review batch: CoursesKey empty-state reachability, the
already_existed info toast, in-flight Add disable, the 0036 partial
unique index + resolve_offering conflict retry, semesters.ts pruning.
- New journey frontend/e2e/semester-scope.spec.ts: default shows
cross-term courses together (MATH210 + BIO110), picking Fall 2025 in
the hub hides Spring-only MATH210, "All semesters" restores it. Hub
opens via new dashboard-courses-manage testid (documented in
docs/frontend-testids.md); tabs selected by role/name.
- Tests reworked: Dashboard default test now asserts one unscoped fetch
with both terms visible; /api/semesters-failure test dropped (no
semesters fetch remains); ensureDefault unit tests replaced by hub tab
component tests (All active by default / term persists / All clears).
- Design doc amendment records the veto and the opt-in scoping decision.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: AndresL230 <190146319+AndresL230@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@AndresL230
AndresL230 deleted the fix/319-course-node-topic branch August 2, 2026 18:30
Sign up for freeto 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.

Clicking on a course node will result in the course name being selected as a course topic automatically

2 participants

@Darkest-Teddy@AndresL230