Replace browser confirm() with Radix AlertDialog
Implement the following plan:
Replace browser confirm() with Radix AlertDialog
Summary
Replace the two confirm() calls in activity-stage-instructor.tsx (reset and stop actions) with a Radix UI AlertDialog component, matching the existing UI component patterns.
Current State
handleReset(line 242):confirm("Reset this session? This will clear all student progress.")handleStop(line 274):confirm("Stop this session? Student progress will be preserved but the session will end.")- Codebase already uses Radix UI (
@radix-ui/react-dropdown-menu,@radix-ui/react-avatar) - UI components in
apps/web/components/ui/follow a consistent wrapper pattern (seedropdown-menu.tsx) - E2E tests across 6 files use
page.once("dialog", (dialog) => dialog.accept())to handle these confirms
Plan
Step 1: Install @radix-ui/react-alert-dialog
cd apps/web && bun add @radix-ui/react-alert-dialog
Pin to exact version per project conventions.
Step 2: Create apps/web/components/ui/alert-dialog.tsx
Radix AlertDialog wrapper following the same pattern as dropdown-menu.tsx:
- Import
@radix-ui/react-alert-dialogprimitives - Export styled wrappers:
AlertDialog,AlertDialogTrigger,AlertDialogContent,AlertDialogHeader,AlertDialogFooter,AlertDialogTitle,AlertDialogDescription,AlertDialogAction,AlertDialogCancel - Use existing theme variables (
--background,--foreground,--destructive,--border) - Overlay with
z-50, backdrop blur, fade animations
Step 3: Refactor activity-stage-instructor.tsx
- Add two state variables:
showResetConfirmandshowStopConfirm - Remove the
confirm()calls fromhandleResetandhandleStop - Button clicks set state to
trueinstead of calling confirm - Add
AlertDialogcomponents in JSX that call the actual action on confirm - Add
data-testidattributes to dialog elements for E2E testing:confirm-dialog-reset/confirm-dialog-stopon the dialog contentconfirm-dialog-reset-confirm/confirm-dialog-stop-confirmon confirm buttonsconfirm-dialog-reset-cancel/confirm-dialog-stop-cancelon cancel buttons
Step 4: Update E2E tests
Replace all page.once("dialog", (dialog) => dialog.accept()) patterns for reset/stop with clicking the new confirm button via testid. Affected files:
| File | Occurrences |
|---|---|
tests/activity-stage-timing.spec.ts | 5 (1 in helper, 4 in tests) |
tests/activity-stages.spec.ts | 2 |
tests/full-activity-flow.spec.ts | 3 |
tests/auto-advance-simple.spec.ts | 1 |
tests/meeting-2026-02-03.spec.ts | 2 |
tests/mixed-stages.spec.ts | 4 |
Pattern change:
// Before
page.once("dialog", (dialog) => dialog.accept());
await page.getByTestId("activity-stage-reset-button").click();
// After
await page.getByTestId("activity-stage-reset-button").click();
await page.getByTestId("confirm-dialog-reset-confirm").click();
Same pattern for stop button with confirm-dialog-stop-* testids.
Files modified
| File | Change |
|---|---|
apps/web/package.json | Add @radix-ui/react-alert-dialog dependency |
apps/web/components/ui/alert-dialog.tsx | New: Radix AlertDialog wrapper |
apps/web/components/mdx/activity-stage-instructor.tsx | Replace confirm() with AlertDialog state + JSX |
testing/e2e/tests/activity-stage-timing.spec.ts | Update dialog handling |
testing/e2e/tests/activity-stages.spec.ts | Update dialog handling |
testing/e2e/tests/full-activity-flow.spec.ts | Update dialog handling |
testing/e2e/tests/auto-advance-simple.spec.ts | Update dialog handling |
testing/e2e/tests/meeting-2026-02-03.spec.ts | Update dialog handling |
testing/e2e/tests/mixed-stages.spec.ts | Update dialog handling |
Verification
bun run tsc
cd testing/e2e && CI=true bunx playwright test --project=activity-stage-timing
cd testing/e2e && CI=true bunx playwright test --project=activity-stages
cd testing/e2e && CI=true bunx playwright test --project=full-activity-flow
cd testing/e2e && CI=true bunx playwright test --project=auto-advance-simple
If you need specific details from before exiting plan mode (like exact code snippets, error messages, or content you generated), read the full transcript at: ~/.claude/projects/-Users-challen-www-usingandunderstanding-ai/c169111f-c38c-4a76-a38c-d8121a745090.jsonl
I'll start by reading the key files I need to modify, then implement the changes.
Now let me read the E2E test files in parallel.
Now I have all the context I need. Let me create the tasks and start implementing.
Now let me check what version was installed and make sure it's exact.
Bun added it with ^ prefix. Need to pin it to an exact version.
Now create the alert-dialog.tsx component.
Now refactor activity-stage-instructor.tsx to replace confirm() calls with AlertDialog.
Now add the state variables for the confirm dialogs:
Now replace the handleReset function to remove the confirm() call:
Now replace the handleStop function to remove the confirm() call:
Now update the reset button to open the dialog instead of calling handleReset directly:
Now update the stop button to open the dialog:
Now add the AlertDialog components at the end of the JSX, just before the closing </div>:
Now update all E2E tests. Let me do all the edits in parallel.
Now update the stop dialog occurrences in this file. Let me find them.
Now update activity-stages.spec.ts. There are two occurrences - one in the helper and one in the "instructor can reset session" test.
Now update full-activity-flow.spec.ts. There are 3 occurrences - helper, session reset test, and cleanup.
Now update auto-advance-simple.spec.ts - 1 occurrence in helper.
Now update meeting-2026-02-03.spec.ts - 2 occurrences (helper and cleanup).
Now update mixed-stages.spec.ts - 4 occurrences (helper plus 3 cleanup/test spots).
Let me verify there are no remaining dialog patterns anywhere in these test files.
All browser dialog handlers have been removed. Also verify no remaining confirm( calls in the instructor component.
TypeScript compilation passes with no errors.
All done. Here's a summary of the changes:
Files modified:
apps/web/package.json- Added@radix-ui/react-alert-dialog: 1.1.15(exact version)apps/web/components/ui/alert-dialog.tsx(new) - Radix AlertDialog wrapper following the same pattern asdropdown-menu.tsx, exporting styled primitivesapps/web/components/mdx/activity-stage-instructor.tsx- Replaced twoconfirm()calls withAlertDialogstate management:- Added
showResetConfirmandshowStopConfirmstate - Reset/stop buttons now set state to open the dialog
handleReset/handleStopno longer callconfirm()- they close the dialog and proceed- Two
AlertDialogcomponents in JSX withdata-testidattributes for E2E testing
- Added
- 6 E2E test files - Replaced all
page.once("dialog", (dialog) => dialog.accept())patterns with clicking the new confirm button viadata-testid:activity-stage-timing.spec.ts(4 occurrences)activity-stages.spec.ts(2 occurrences)full-activity-flow.spec.ts(3 occurrences)auto-advance-simple.spec.ts(1 occurrence)meeting-2026-02-03.spec.ts(2 occurrences)mixed-stages.spec.ts(4 occurrences)
Read the output file to retrieve the result: [tmp-path]
That was just a background task notification for a killed dev server process — not related to the current work. All changes are complete and TypeScript compiles cleanly.
Your task
First, run git status, git diff, and git log --oneline -10 to understand the current state.
Then add and commit all modified files with a descriptive message. Break the changes into multiple commits if appropriate. If there are untracked files, ask if they should be added or added to the appropriate .gitignore file. The user may also want to take over at this point, so provide an abort option as well. Always check changes for unnecessary debugging statements or logging that might have been accidentally included. Prompt the user if you find any, and offer to remove each one by one. Do not use interactive git staging (git add -p) as it does not work.
No debugging statements or unnecessary logging in the changes. One untracked file (apps/web/components/ui/alert-dialog.tsx) which is the new component and should be committed.
This is a single logical change, so one commit is appropriate.
Committed as f54e558 on branch fixes. Working tree is clean.
Let's remove that TODO.
(no content)
Done. Removed the "Use modal instead of browser confirm for session stop/reset" TODO from TODOS.md.
Your task
First, run git status, git diff, and git log --oneline -10 to understand the current state.
Then add and commit all modified files with a descriptive message. Break the changes into multiple commits if appropriate. If there are untracked files, ask if they should be added or added to the appropriate .gitignore file. The user may also want to take over at this point, so provide an abort option as well. Always check changes for unnecessary debugging statements or logging that might have been accidentally included. Prompt the user if you find any, and offer to remove each one by one. Do not use interactive git staging (git add -p) as it does not work.
Clean change — just removing the completed TODO entry. No debugging or logging issues.
Committed as f135295.
Let's check TextSubmission visible to all group members instead of just observer. Is that an issue with how the lesson MDX file is set up or something else?
(no content)
This is an MDX content structure issue, not a component bug. The components all work correctly.
The problem is in content/spring2026/meetings/2026-02-03-assessments-and-agents.mdx. That meeting uses roles={{ partner: "*" }}, meaning both group members get the "partner" role. The TextSubmission is placed inside <GroupRoleContent role="partner" index={1}>, which correctly limits it to only the index-1 partner — so only one of the two partners sees it.
Contrast with 2026-01-27-ai-scavenger-hunt.mdx, which uses roles={{ driver: 1, observer: "*" }} and places TextSubmission inside <GroupRoleContent role="observer"> — cleanly observer-only.
So the TODO's description ("visible to all group participants") doesn't match what's actually happening. The index={1} filtering does restrict visibility to one partner. The E2E test in meeting-2026-02-03.spec.ts explicitly verifies this at lines 154-160 — it confirms the index-1 partner sees the TextSubmission and the index-0 partner does not.
This TODO may have been filed before the index-based partner content routing was implemented (commit 8b1a77f), which would have resolved it. Want to remove this TODO as already fixed?
Yes.
Done. Removed the TextSubmission visibility TODO — it was resolved by the index-based partner content routing.
Your task
First, run git status, git diff, and git log --oneline -10 to understand the current state.
Then add and commit all modified files with a descriptive message. Break the changes into multiple commits if appropriate. If there are untracked files, ask if they should be added or added to the appropriate .gitignore file. The user may also want to take over at this point, so provide an abort option as well. Always check changes for unnecessary debugging statements or logging that might have been accidentally included. Prompt the user if you find any, and offer to remove each one by one. Do not use interactive git staging (git add -p) as it does not work.
Clean removal, no issues.
Committed as e561f34.
How about this one: Text submissions may not be associated with correct stage
This one is also not a real bug. The architecture handles it correctly through React Context isolation.
Each ActivityStageClient wraps its children with <CurrentStageIndexProvider stageIndex={index}> (activity-stage-client.tsx:114,249), where index is the static position of that stage in the MDX (0, 1, 2, etc.). When TextSubmissionClient calls useCurrentStageIndex() (text-submission-client.tsx:48), it reads from this local context provider — not from the global session state that tracks the instructor's current stage.
So a TextSubmission in stage 2 always submits with stageIndex: 2, regardless of whether the instructor has advanced to stage 3. The concern in the TODO was valid to raise but the implementation is correct.
There is a separate minor TODO at text-submission-client.tsx:115 — sessionId: null, // TODO: Get actual session ID from context if needed — but that's about session tracking, not stage association.
Want to remove this TODO as well?
Yes.
Done. Removed the text submission stage association TODO — the CurrentStageIndexProvider context isolation handles it correctly.
Your task
First, run git status, git diff, and git log --oneline -10 to understand the current state.
Then add and commit all modified files with a descriptive message. Break the changes into multiple commits if appropriate. If there are untracked files, ask if they should be added or added to the appropriate .gitignore file. The user may also want to take over at this point, so provide an abort option as well. Always check changes for unnecessary debugging statements or logging that might have been accidentally included. Prompt the user if you find any, and offer to remove each one by one. Do not use interactive git staging (git add -p) as it does not work.
Clean change, no issues.
Committed as e02135a.
What about showing the group counts not individuals? I think that was still broken today.
(no content)
This is still a real bug. The completion count on the instructor panel counts individual students, not groups.
Current behavior: If a pair of students completes a grouped stage, the instructor sees "2 completed" — one count per student, since each student's client independently calls the completion endpoint.
Expected behavior: It should show "1 completed" for that pair, since they're one group.
The root cause is in apps/api/src/routes/admin/activity-stages.ts:233-241 — the query simply does count() on activityStageCompletions rows grouped by stageIndex. There's no join to activityGroupMembers to deduplicate by group.
The fix isn't trivial though — the activityStageCompletions table has no groupId column and there's no direct link between a completion record and a group. It would require either:
- Joining through users to groups: For grouped stages, count distinct groups where at least one member completed, rather than counting individual completions
- Adding a
groupIdto completions: Store the group association at completion time so the count query canCOUNT(DISTINCT groupId)
The challenge is the system doesn't currently know at the API level which stages are grouped vs individual — that information lives in the MDX content. Want me to dig into a specific approach for fixing this?
Yes. But I'm also confused. What if both group roles don't have a submission?
(no content)
Sorry, I mean what if we don't have a submission dialog from both group roles? We could have partners where one drives and the other submits. So we need to handle that as well.
(no content)
Good point. Let me dig into how completion actually works across the different stage configurations — auto-complete on group formation, manual complete buttons, and whether both roles always get the complete button.
We may need to extract some information about the MDX content at build time and sent it to the backend, possibly using a special token provided to the frontend build system. I've used this approach on the cs124.org website. It's somewhat gross, but it works. I guess the other option is to extract the information at build time, stick it in the web container, and have the web container push it to the API server when it starts.
(no content)
Essentially we need to get information from the build stage to the API server.
There might be a simpler path that avoids the build-to-API pipeline entirely. The client already has the group context when it triggers a stage completion — group-activity-client.tsx calls activityStages.completeStage(currentStageIndex) and has access to the group ID at that point.
So the approach could be:
- Add a nullable
groupIdcolumn toactivityStageCompletions - Pass the
groupIdthroughcompleteStage()when completing from within a group context (individual stages would leave it null) - In the count query, use
COUNT(DISTINCT groupId)whengroupIdis present, fall back toCOUNT(*)for null (individual stages)
That way the API doesn't need to know anything about the MDX structure — the client tells it "this completion belongs to group X" at the time of submission. No build-time extraction needed.
The one thing this doesn't cover is knowing the total number of groups (for showing "3 of 12 groups completed"), but enrollment count has the same limitation today for individuals.
Want me to plan out this approach, or do you see a reason the build-time metadata route is necessary?
Does that make sense?
If we can avoid the build-time metadata that would be great. But why don't we know how many groups exist? We know how many have been formed, right?
Right — the activityGroups table tracks every group that's been formed. So we could show "3 of 12 groups completed" by querying activityGroups filtered by the relevant activityId and semester. The total group count is already in the database.
The missing piece is linking a stage's completions to the right set of groups. We'd need to know which activityId (the MDX id prop on GroupActivity) corresponds to which stage. But that relationship is also knowable at runtime — when the client sends the groupId with the completion, we can look up that group's activityId and use it to count total groups for that stage.
So the instructor display could show:
- Individual stage: "4 completed" (count of students)
- Grouped stage: "3 of 12 completed" (distinct groups completed / total groups for that activity)
Want me to plan this out?
Yes. And for an individual stage, maybe we need some equivalent of group entry, even if it's just "Click here to start" and then uncover the content, just so we know how many individuals are participating.
(no content)
Now let me read a few critical files to confirm specific details before writing the plan.
Now let me check the student status endpoint response shape more closely, and look at the activity-stage-context interface.
Good, I have a clear picture now. Let me ask one clarifying question before writing the plan.
Now let me write the plan.