feat(workspaces): quiz study items, self-describing AI addresses, and shared study UI - #788
Conversation
Multiple-choice quizzes as a first-class item: the model authors question + correctAnswer + distractors + explanation, the server shuffles the answer in and stores the option order, and a first-pick- wins immediate-feedback viewer persists per-user answers. Extracts the shared ordered-entry edit engine and entry rich-text dialect from flashcards so both study types run on one mechanism, and generalizes the study toolbar and creation dialog. Part 1 of a stacked series; builds together with the address refactor it was developed alongside.
Every workspace item gets a stable 8-character refKey, minted on insert and backfilled by migration from a ~47-bit alphanumeric space with a dedupe pass before the unique index. This is the model-facing handle the address system resolves statelessly, so nothing per-turn has to be minted or kept alive for a reference to keep working. Part 2 of the stacked series.
Deletes the per-turn wr_ reference registry, the four-kind read cursor union, and the byte-budget latch. Reads follow an outline-and-units model: every item is numbered units (blocks, pages, cards, questions), every result names the units it covered plus the total, and continuation is just the next range. Refs become stateless addresses - refKey[/unit][.r_revision] - so edits, citations, and re-reads resolve from the string alone, survive compaction, and need no Durable Object turn state or transcript reconciliation. Part 3 of the stacked series.
Entry-range failures carry their real codes and a recovery message, the range description no longer teaches comma lists to documents, and the simplification pass lands: one shared Fisher-Yates, alias chains and the leftover adapter factory removed, dead userId branches and unused exports deleted. Part 4 of the stacked series; tree is byte-identical to the verified add-quiz-item-type branch.
The flashcard and quiz viewers each carried their own copy of the same pieces: a read-only Tiptap render, the amber "ask AI" button, the prev/next arrows, and the progress strip. Same look, four chances to drift. Extracts them into components/study: StudyRichText, StudyAiActionButton, StudyNavButton, and StudyProgressStrip, whose segments now carry a tone so each item type maps its own state onto one strip.
Centred text is hard to read past a line or two, and it forced a display: inline-block hack on lists to keep them from centring with it. The prose block still centres inside the face; only the text aligns left.
Same shell as flashcards — margins, scrollable body, progress strip, footer — so the two study surfaces read as one product. Answer choices become Khan-style rows in a single list rather than a card each, picking one only selects it, and a Submit button in the footer grades. Four layout bugs went with it, each fixed by a rule rather than an offset, and each measured in the browser: - Rows shifted when the outline moved, because the hairlines between them unmounted. They now stay in the flow and hide by colour. - Everything shifted up on submit, because the row holding the Hint button collapsed when grading removed it. The button sits in a slot that keeps its height. - Option text sat above its letter, because the prose rule that gives an empty paragraph a click target while editing also padded read-only renders. Scoped that min-height to editable prose. - Scroll position survived question changes, dropping you past the next stem. The scroller is keyed by question.
The allowlist earns its keep at the model boundary: study viewers render with the full document schema, so an unchecked widget really would embed a sandboxed iframe inside a card face. It earns nothing on the way out, where re-running it over content that already passed can only turn one odd entry into an item that will not open. Storage keeps the structural check and drops the allowlist. The parser factory and its six message strings collapse into two functions taking the item's label.
Chat could already draw mermaid. Documents, flashcards, and quizzes could not, which left diagrams out of exactly the surfaces that explain things. A mermaid block is an ordinary code block, so cards and questions get diagrams without opening their allowlist to widgets, which stay documents-only. AiChatMermaidDiagram becomes a shared MermaidDiagram taking isIncomplete as a prop, since only chat can see a half-written fence, and caches rendered SVGs so a card flipped back and forth does not re-render its artwork. CodeBlockNodeView draws the diagram in place of the source and selects the whole block on click, the way an image behaves. PDF export drops mermaid blocks rather than printing authoring syntax. Tool schemas gain one diagram instruction shared by all three item types, plus a note that a diagram usually belongs in a quiz stem or explanation, since one option taller than the rest hints at the answer.
The skeleton still drew one big rounded card, which the quiz stopped being when the answer rows moved out into a plain list. It now traces the real shape: a stem line with the Hint slot beside it, four option rows with their letter badges and hairlines, then the progress strip and footer.
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
|
Too many files changed for review (109 files, 100 file limit). Bypass the limit by tagging |
|
Important Review skippedToo many files! This PR contains 109 files, which is 9 over the limit of 100. To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch. Upgrade to a paid plan to raise the limit. This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (109)
You can disable this status message by setting the Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
23 issues found across 109 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/features/workspaces/documents/document-session.ts">
<violation number="1" location="src/features/workspaces/documents/document-session.ts:357">
P3: `readHtmlChunk` no longer uses the snapshot revision, but `getReferencedDocumentSnapshot()` still computes and returns `stateVector` for every read. Remove that field and its `Y.encodeStateVector` computation to avoid dead work on each document read.</violation>
</file>
<file name="src/features/workspaces/content/entry-rich-text.ts">
<violation number="1" location="src/features/workspaces/content/entry-rich-text.ts:15">
P2: When an entry field is syntactically nonempty but renders no content, such as `<p></p>`, this schema accepts it and `parseEntryRichTextHtml` stores a blank rich-text document. That permits blank quiz answers, stems, explanations, or flashcard sides; validate the normalized document for meaningful content at the model boundary (and in the parser used by direct callers).</violation>
</file>
<file name="src/features/workspaces/quizzes/quiz-queries.ts">
<violation number="1" location="src/features/workspaces/quizzes/quiz-queries.ts:16">
P3: This module duplicates the complete study-query lifecycle from `flashcard-queries.ts`, so fixes to cancellation, optimistic rollback, or cache-version handling must be made twice. Extract a shared study-cache helper and pass the item-specific query and state functions into it.</violation>
<violation number="2" location="src/features/workspaces/quizzes/quiz-queries.ts:58">
P2: When an older `updatedAt` variant remains cached and a quiz mutation fails, the optimistic answer is rolled back only for the current variant. The older variant can show the rejected answer when reopened until its refetch completes; snapshot and restore every key modified optimistically, or update only the exact key and invalidate the others.</violation>
</file>
<file name="src/features/workspaces/quizzes/quiz-persistence.ts">
<violation number="1" location="src/features/workspaces/quizzes/quiz-persistence.ts:18">
P3: This file duplicates the complete structured-item persistence flow from `flashcard-persistence.ts`, so future fixes to locking, revisions, or notifications can diverge between flashcards and quizzes. Extract the shared transaction and notification logic into a generic structured-item persistence helper, leaving only type-specific parsing and serialization in each module.</violation>
</file>
<file name="src/features/workspaces/documents/code-block-shiki/CodeBlockNodeView.tsx">
<violation number="1" location="src/features/workspaces/documents/code-block-shiki/CodeBlockNodeView.tsx:25">
P2: Custom agent: **Flag AI Slop and Fabricated Changes**
The PR adds mermaid diagram rendering in documents, flashcards, and quizzes, but the new document rendering path has no automated tests. Rule 1 requires behavior changes to be exercised by tests when a regression-style assertion is practical. Add a test that verifies a mermaid `codeBlock` renders `<MermaidDiagram>` (or the `.workspace-document-diagram` wrapper) instead of the usual `<NodeViewWrapper className="workspace-document-code-block">` path.</violation>
<violation number="2" location="src/features/workspaces/documents/code-block-shiki/CodeBlockNodeView.tsx:54">
P2: Clicking a Mermaid block selects it but does not make it draggable, so users cannot move the block as the new image-like behavior promises. Mark the code-block node as draggable or add equivalent drag handling.</violation>
</file>
<file name="src/features/workspaces/model/workspace-page.ts">
<violation number="1" location="src/features/workspaces/model/workspace-page.ts:43">
P2: The optimistic `refKey` is a random client-minted placeholder that deliberately differs from the server-minted key (`createWorkspaceItemRefKey()` at workspace-items.ts:241 mints an independent value, and the onSuccess/realtime upsert replaces the item by id). During the window before that upsert lands, any client-built address that copies this placeholder `refKey` (e.g. a citation or reference authored against the freshly created item, resolved via `workspace-location-context.tsx:69` or server-side `getWorkspaceItemByRefKey`) is persisted with a key that never resolves after the placeholder is discarded. Because the refKey is the durable model-facing handle, consider minting the real key on the client reject path or deferring refKey-dependent reads until the server-minted key arrives, rather than exposing a placeholder that can be captured into durable content.</violation>
</file>
<file name="src/features/workspaces/components/study/StudySessionControls.tsx">
<violation number="1" location="src/features/workspaces/components/study/StudySessionControls.tsx:32">
P3: The unconditional `onKeyDown={(event) => event.stopPropagation()}` on StudyAiActionButton swallows every key press from the Hint/Explain button, so when it holds focus the parent `<section>`'s ArrowLeft/ArrowRight navigation (QuizViewer.tsx:305 and FlashcardViewer.tsx:460) stops working. This is inconsistent with StudyNavButton, which intentionally lets arrows bubble to the same section handler, and it breaks the arrow-key shortcuts advertised in the nav button title. Drop the onKeyDown handler — `onClick` already stops the click from reaching the flashcard flip handler, and the flip-on-space path is separately guarded by `closest("button, a")` — so arrow navigation behaves identically regardless of which control has focus.</violation>
</file>
<file name="src/features/workspaces/components/quizzes/QuizViewer.tsx">
<violation number="1" location="src/features/workspaces/components/quizzes/QuizViewer.tsx:192">
P2: When switching to Missed after all questions are correct, `currentQuestion` becomes undefined and this effect returns without clearing the last view state. AI context continues reporting the previous question while the viewer shows “No missed questions”; clear this view instance when no current question exists.</violation>
</file>
<file name="src/features/workspaces/locations/workspace-location.ts">
<violation number="1" location="src/features/workspaces/locations/workspace-location.ts:52">
P2: The shared entry schema allows flashcard locations to carry `q_` IDs and quiz locations to carry `c_` IDs. Enforce the `c_` prefix for flashcards and `q_` for quiz questions while retaining UUID support for legacy content, so malformed persisted content and cross-type citations cannot validate.</violation>
</file>
<file name="src/features/workspaces/components/study/StudyRichText.tsx">
<violation number="1" location="src/features/workspaces/components/study/StudyRichText.tsx:27">
P2: Read-only study content still exposes the code-block language selector, and selecting a language mutates the editor because `CodeBlockNodeView` does not guard `updateAttributes` with `editor.isEditable`. Hide or disable that selector for read-only editors so study content remains read-only.</violation>
</file>
<file name="src/features/workspaces/documents/document-html-chunk.ts">
<violation number="1" location="src/features/workspaces/documents/document-html-chunk.ts:42">
P2: When `maxBlocks` is zero or negative, `readDocumentHtmlChunk` returns an empty ready chunk whose `nextOffset` repeats the current offset. Validate that `maxBlocks` is a positive integer and reject invalid values before entering the loop.</violation>
</file>
<file name="src/features/workspaces/components/study/StudyProgressStrip.tsx">
<violation number="1" location="src/features/workspaces/components/study/StudyProgressStrip.tsx:49">
P2: Keyboard users cannot tab to another progress segment because every non-current button has `tabIndex={-1}`, but the strip does not implement roving arrow-key focus. Make all segment buttons tabbable with a visible focus style, or move focus to the newly selected button when navigating with the keyboard.</violation>
</file>
<file name="eval/datasets/workspace-tools.cases.ts">
<violation number="1" location="eval/datasets/workspace-tools.cases.ts:41">
P2: The case name claims 'without leaking the answer key into the stems', but the eval cannot enforce that. The harness stubs tool execution and grades only tool selection and schema validity (`scoreExpectedTools`/`scoreNoForbiddenTools`/`scoreToolInputsValid`), so this case passes as long as the model calls `workspace_create_items` once and avoids `workspace_delete_items` — it never checks that 3 questions were created or that the correct answer isn't embedded among the stems. The other cases whose concern can't be verified deterministically (read-only denial, general question) set `qualityRubric`; this one has none. Either rename the case to what it actually measures (e.g. 'creates a quiz and does not delete items') or add a `qualityRubric` that grades the answer for the 3-question count and no answer-key leakage.</violation>
</file>
<file name="src/features/workspaces/export/workspace-document-pdf-html.ts">
<violation number="1" location="src/features/workspaces/export/workspace-document-pdf-html.ts:119">
P2: When a Mermaid block is the only content inside a block container, this removes only `<pre>` and leaves the empty parent visible in the PDF. Prune empty ancestors after removal, or remove the Mermaid node before HTML serialization so the diagram leaves no layout artifact.</violation>
</file>
<file name="src/features/workspaces/quizzes/quiz-content.ts">
<violation number="1" location="src/features/workspaces/quizzes/quiz-content.ts:144">
P2: A `replace_text` edit can make one option exactly duplicate another, but `parseQuizSetContent` accepts the resulting question because it never enforces the distinct-option invariant. The quiz then stores indistinguishable choices; enforce `assertDistinctOptions` for every edited question before persistence and reject duplicate stored content at this boundary.</violation>
</file>
<file name="src/components/code-block/mermaid-diagram.tsx">
<violation number="1" location="src/components/code-block/mermaid-diagram.tsx:208">
P2: When a cached SVG fails `<img>` decoding, the cache retains the broken image and later remounts skip Mermaid rendering because `cachedImage` is present. Remove the matching cache entry when `onImageError` fires so the next mount can retry and show the normal error fallback.</violation>
</file>
<file name="src/features/workspaces/operations/document-citations.ts">
<violation number="1" location="src/features/workspaces/operations/document-citations.ts:39">
P2: When the model emits a valid-looking but nonexistent card or question ID, this line treats the address as resolved and persists a broken citation. Verify the unit exists in the item's content before adding its location, or leave the citation unresolved so `parseDocumentAiHtml` removes it.</violation>
</file>
<file name="src/features/workspaces/content/workspace-content-reader.ts">
<violation number="1" location="src/features/workspaces/content/workspace-content-reader.ts:117">
P2: When a file read hits the batch budget, this failure omits `type: "file"`, so `recordWorkspaceFileReadOutcomes` drops the event. Preserve the file type on budget failures so file-read observability continues to capture them.</violation>
<violation number="2" location="src/features/workspaces/content/workspace-content-reader.ts:135">
P2: An explicit `entries` read can return more than the 200,000-character batch ceiling because this code accounts for the current result only after reading it. Reject an oversized result before adding it to `results` and `readyResults`, while preserving `read_budget_exceeded` for subsequent requests.</violation>
<violation number="3" location="src/features/workspaces/content/workspace-content-reader.ts:506">
P2: Document `entries` reads do not enforce the documented 20-entry maximum, so a range such as `1-100` can return an unexpectedly large response. Reject contiguous ranges containing more than 20 blocks before calling `readHtmlChunk`.</violation>
</file>
<file name="src/features/workspaces/content/workspace-content-contract.ts">
<violation number="1" location="src/features/workspaces/content/workspace-content-contract.ts:152">
P2: Malformed quiz results with zero or multiple correct options pass this output contract despite the documented invariant. Enforce that exactly one option has `correct: true` so invalid quiz data is rejected at the boundary.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| } from "#/features/workspaces/documents/code-block-shiki/highlighter"; | ||
|
|
||
| export function CodeBlockNodeView({ node, updateAttributes }: ReactNodeViewProps) { | ||
| export function CodeBlockNodeView({ |
There was a problem hiding this comment.
P2: Custom agent: Flag AI Slop and Fabricated Changes
The PR adds mermaid diagram rendering in documents, flashcards, and quizzes, but the new document rendering path has no automated tests. Rule 1 requires behavior changes to be exercised by tests when a regression-style assertion is practical. Add a test that verifies a mermaid codeBlock renders <MermaidDiagram> (or the .workspace-document-diagram wrapper) instead of the usual <NodeViewWrapper className="workspace-document-code-block"> path.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/documents/code-block-shiki/CodeBlockNodeView.tsx, line 25:
<comment>The PR adds mermaid diagram rendering in documents, flashcards, and quizzes, but the new document rendering path has no automated tests. Rule 1 requires behavior changes to be exercised by tests when a regression-style assertion is practical. Add a test that verifies a mermaid `codeBlock` renders `<MermaidDiagram>` (or the `.workspace-document-diagram` wrapper) instead of the usual `<NodeViewWrapper className="workspace-document-code-block">` path.</comment>
<file context>
@@ -13,18 +13,56 @@ import {
} from "#/features/workspaces/documents/code-block-shiki/highlighter";
-export function CodeBlockNodeView({ node, updateAttributes }: ReactNodeViewProps) {
+export function CodeBlockNodeView({
+ editor,
+ getPos,
</file context>
| itemId: workspaceLocationItemIdSchema, | ||
| kind: z.literal("flashcard"), | ||
| cardId: z.uuid(), | ||
| cardId: workspaceEntryIdSchema, |
There was a problem hiding this comment.
P2: The shared entry schema allows flashcard locations to carry q_ IDs and quiz locations to carry c_ IDs. Enforce the c_ prefix for flashcards and q_ for quiz questions while retaining UUID support for legacy content, so malformed persisted content and cross-type citations cannot validate.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/locations/workspace-location.ts, line 52:
<comment>The shared entry schema allows flashcard locations to carry `q_` IDs and quiz locations to carry `c_` IDs. Enforce the `c_` prefix for flashcards and `q_` for quiz questions while retaining UUID support for legacy content, so malformed persisted content and cross-type citations cannot validate.</comment>
<file context>
@@ -38,135 +49,103 @@ export const workspaceLocationSchema = z.discriminatedUnion("kind", [
itemId: workspaceLocationItemIdSchema,
kind: z.literal("flashcard"),
- cardId: z.uuid(),
+ cardId: workspaceEntryIdSchema,
+ version: z.literal(1),
+ }),
</file context>
| content: content as unknown as JSONContent, | ||
| editable: false, | ||
| immediatelyRender: false, | ||
| extensions: getTiptapDocumentBaseExtensions(), |
There was a problem hiding this comment.
P2: Read-only study content still exposes the code-block language selector, and selecting a language mutates the editor because CodeBlockNodeView does not guard updateAttributes with editor.isEditable. Hide or disable that selector for read-only editors so study content remains read-only.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/components/study/StudyRichText.tsx, line 27:
<comment>Read-only study content still exposes the code-block language selector, and selecting a language mutates the editor because `CodeBlockNodeView` does not guard `updateAttributes` with `editor.isEditable`. Hide or disable that selector for read-only editors so study content remains read-only.</comment>
<file context>
@@ -0,0 +1,36 @@
+ content: content as unknown as JSONContent,
+ editable: false,
+ immediatelyRender: false,
+ extensions: getTiptapDocumentBaseExtensions(),
+ editorProps: {
+ attributes: { class: cn("workspace-document-prose outline-none", proseClassName) },
</file context>
| let characters = 0; | ||
| let endOffset = offset; | ||
| while (endOffset < document.childCount) { | ||
| if (maxBlocks !== undefined && endOffset - offset >= maxBlocks) break; |
There was a problem hiding this comment.
P2: When maxBlocks is zero or negative, readDocumentHtmlChunk returns an empty ready chunk whose nextOffset repeats the current offset. Validate that maxBlocks is a positive integer and reject invalid values before entering the loop.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/documents/document-html-chunk.ts, line 42:
<comment>When `maxBlocks` is zero or negative, `readDocumentHtmlChunk` returns an empty ready chunk whose `nextOffset` repeats the current offset. Validate that `maxBlocks` is a positive integer and reject invalid values before entering the loop.</comment>
<file context>
@@ -38,6 +39,7 @@ export async function readDocumentHtmlChunk(
let characters = 0;
let endOffset = offset;
while (endOffset < document.childCount) {
+ if (maxBlocks !== undefined && endOffset - offset >= maxBlocks) break;
const block = await serializeTiptapNodeToAiHtml(document.child(endOffset));
const separatorCharacters = content.length > 0 ? 1 : 0;
</file context>
| if (maxBlocks !== undefined && endOffset - offset >= maxBlocks) break; | |
| if (maxBlocks !== undefined && (!Number.isInteger(maxBlocks) || maxBlocks <= 0)) { | |
| return undefined; | |
| } | |
| if (maxBlocks !== undefined && endOffset - offset >= maxBlocks) break; |
| if (input.expectedRevision && input.expectedRevision !== revision) { | ||
| return { status: "content_changed" }; | ||
| } | ||
| const { document } = await this.getReferencedDocumentSnapshot(); |
There was a problem hiding this comment.
P3: readHtmlChunk no longer uses the snapshot revision, but getReferencedDocumentSnapshot() still computes and returns stateVector for every read. Remove that field and its Y.encodeStateVector computation to avoid dead work on each document read.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/documents/document-session.ts, line 357:
<comment>`readHtmlChunk` no longer uses the snapshot revision, but `getReferencedDocumentSnapshot()` still computes and returns `stateVector` for every read. Remove that field and its `Y.encodeStateVector` computation to avoid dead work on each document read.</comment>
<file context>
@@ -354,14 +354,10 @@ export class DocumentSession extends YServer {
- if (input.expectedRevision && input.expectedRevision !== revision) {
- return { status: "content_changed" };
- }
+ const { document } = await this.getReferencedDocumentSnapshot();
- const chunk = await readDocumentHtmlChunk(document, input.offset);
</file context>
|
|
||
| type QuizViewerData = Awaited<ReturnType<typeof getQuizViewerFn>>; | ||
|
|
||
| export function quizViewerQueryOptions(input: { |
There was a problem hiding this comment.
P3: This module duplicates the complete study-query lifecycle from flashcard-queries.ts, so fixes to cancellation, optimistic rollback, or cache-version handling must be made twice. Extract a shared study-cache helper and pass the item-specific query and state functions into it.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/quizzes/quiz-queries.ts, line 16:
<comment>This module duplicates the complete study-query lifecycle from `flashcard-queries.ts`, so fixes to cancellation, optimistic rollback, or cache-version handling must be made twice. Extract a shared study-cache helper and pass the item-specific query and state functions into it.</comment>
<file context>
@@ -0,0 +1,110 @@
+
+type QuizViewerData = Awaited<ReturnType<typeof getQuizViewerFn>>;
+
+export function quizViewerQueryOptions(input: {
+ itemId: string;
+ updatedAt: string;
</file context>
| } from "#/features/workspaces/persistence/workspace-postgres-support"; | ||
| import { notifyWorkspaceRoom } from "#/features/workspaces/realtime/workspace-room-notifier"; | ||
|
|
||
| export async function readQuizSet(input: { itemId: string; workspaceId: string }) { |
There was a problem hiding this comment.
P3: This file duplicates the complete structured-item persistence flow from flashcard-persistence.ts, so future fixes to locking, revisions, or notifications can diverge between flashcards and quizzes. Extract the shared transaction and notification logic into a generic structured-item persistence helper, leaving only type-specific parsing and serialization in each module.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/quizzes/quiz-persistence.ts, line 18:
<comment>This file duplicates the complete structured-item persistence flow from `flashcard-persistence.ts`, so future fixes to locking, revisions, or notifications can diverge between flashcards and quizzes. Extract the shared transaction and notification logic into a generic structured-item persistence helper, leaving only type-specific parsing and serialization in each module.</comment>
<file context>
@@ -0,0 +1,96 @@
+} from "#/features/workspaces/persistence/workspace-postgres-support";
+import { notifyWorkspaceRoom } from "#/features/workspaces/realtime/workspace-room-notifier";
+
+export async function readQuizSet(input: { itemId: string; workspaceId: string }) {
+ return await withDb(async (db) => {
+ const [row] = await db
</file context>
| event.stopPropagation(); | ||
| onSend(); | ||
| }} | ||
| onKeyDown={(event) => event.stopPropagation()} |
There was a problem hiding this comment.
P3: The unconditional onKeyDown={(event) => event.stopPropagation()} on StudyAiActionButton swallows every key press from the Hint/Explain button, so when it holds focus the parent <section>'s ArrowLeft/ArrowRight navigation (QuizViewer.tsx:305 and FlashcardViewer.tsx:460) stops working. This is inconsistent with StudyNavButton, which intentionally lets arrows bubble to the same section handler, and it breaks the arrow-key shortcuts advertised in the nav button title. Drop the onKeyDown handler — onClick already stops the click from reaching the flashcard flip handler, and the flip-on-space path is separately guarded by closest("button, a") — so arrow navigation behaves identically regardless of which control has focus.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/components/study/StudySessionControls.tsx, line 32:
<comment>The unconditional `onKeyDown={(event) => event.stopPropagation()}` on StudyAiActionButton swallows every key press from the Hint/Explain button, so when it holds focus the parent `<section>`'s ArrowLeft/ArrowRight navigation (QuizViewer.tsx:305 and FlashcardViewer.tsx:460) stops working. This is inconsistent with StudyNavButton, which intentionally lets arrows bubble to the same section handler, and it breaks the arrow-key shortcuts advertised in the nav button title. Drop the onKeyDown handler — `onClick` already stops the click from reaching the flashcard flip handler, and the flip-on-space path is separately guarded by `closest("button, a")` — so arrow navigation behaves identically regardless of which control has focus.</comment>
<file context>
@@ -0,0 +1,66 @@
+ event.stopPropagation();
+ onSend();
+ }}
+ onKeyDown={(event) => event.stopPropagation()}
+ >
+ <Lightbulb />
</file context>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4164e7e571
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| returnedCharacters += | ||
| "content" in read | ||
| ? read.content.length | ||
| : JSON.stringify("cards" in read ? read.cards : read.questions).length; |
There was a problem hiding this comment.
Enforce the read limit before returning each result
When a request selects a large payload—such as 20 text-heavy PDF pages or quiz entries—the code adds its size only after the pre-read budget check and returns it unconditionally. A single result can therefore exceed the intended 200,000-character ceiling by an arbitrary amount and overflow the model context; calculate the prospective total here and return read_budget_exceeded instead of appending an over-budget result.
Useful? React with 👍 / 👎.
| const location = resolveAddress(citationProps.node?.properties?.ref); | ||
| return location ? <WorkspaceCitation location={location} /> : null; |
There was a problem hiding this comment.
Preserve resolution of citations from existing chat history
When rendering any assistant message saved before this upgrade, its citation refs use the wr_XXXXXXXX format and the message's hidden data-workspace-references part contains the durable location. resolveAddress accepts only the new eight-character item addresses, while this change removes the code that read those persisted records, so every historical citation now resolves to undefined and disappears from the transcript despite its target still being available.
Useful? React with 👍 / 👎.
|
@greptile-apps review |
Both viewers key the current entry's subtree so navigation remounts it, and a remount that held the focused element drops focus to <body>, dead to the section's keydown handler. The section now reclaims focus when a navigation strands it there.
The node view assumed its host was the editable document editor; study viewers now render it read-only, where the language dropdown is editing chrome with nothing to edit.
getWorkspaceItemByRefKey read the whole workspace page per call, and both callers called it in a loop — N citations or ref reads cost N serial page reads. It is now getWorkspaceItemRefKeyIndex: one page read, however many keys the operation resolves.
The prose listed the union's members and went stale the moment quizzes joined; the schema already names every type, so the description no longer repeats them.
There was a problem hiding this comment.
1 issue found across 9 files (changes from recent commits).
You’re at about 93% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/features/workspaces/documents/code-block-shiki/CodeBlockNodeView.tsx">
<violation number="1" location="src/features/workspaces/documents/code-block-shiki/CodeBlockNodeView.tsx:104">
P2: When `language` is null, the read-only branch displays `Code` instead of the existing `Plain text` label. Preserve the plain-text fallback and label styling in this branch so read-only and editable code blocks remain consistent.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| </CodeBlockLanguageSelectorContent> | ||
| </CodeBlockLanguageSelector> | ||
| ) : ( | ||
| getCodeLanguageLabel(language) |
There was a problem hiding this comment.
P2: When language is null, the read-only branch displays Code instead of the existing Plain text label. Preserve the plain-text fallback and label styling in this branch so read-only and editable code blocks remain consistent.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/documents/code-block-shiki/CodeBlockNodeView.tsx, line 104:
<comment>When `language` is null, the read-only branch displays `Code` instead of the existing `Plain text` label. Preserve the plain-text fallback and label styling in this branch so read-only and editable code blocks remain consistent.</comment>
<file context>
@@ -71,32 +71,38 @@ export function CodeBlockNodeView({
+ </CodeBlockLanguageSelectorContent>
+ </CodeBlockLanguageSelector>
+ ) : (
+ getCodeLanguageLabel(language)
+ )}
</CodeBlockTitle>
</file context>
| getCodeLanguageLabel(language) | |
| <span className="truncate font-medium">{language ? getCodeLanguageLabel(language) : "Plain text"}</span> |
…-split # Conflicts: # src/features/workspaces/operations/__snapshots__/workspace-tool-surface.test.ts.snap # src/features/workspaces/operations/create-items.ts # src/features/workspaces/operations/workspace-tool-definitions.ts # src/features/workspaces/operations/workspace-tool-schemas.ts
|
React Doctor found 3 new issues in 2 files · 3 warnings · score 85 / 100 (Great) · 1 fixed · vs 3 warnings
Reviewed by React Doctor for commit |
| @@ -178,9 +185,17 @@ function FlashcardStudySession({ | |||
| }, [resetProgress, startSession]); | |||
| const toolbar = useMemo( | |||
There was a problem hiding this comment.
React Doctor · react-doctor/rerender-memo-before-early-return (warning)
This rebuilds the JSX whenever its dependencies change even on renders that take the early return, so move the JSX into a child component rendered after the early return to skip it
Fix → Move the JSX into a child component rendered after the early return, so renders that take the early return never build it
| ); | ||
| } | ||
|
|
||
| function QuizStudySession({ |
There was a problem hiding this comment.
React Doctor · react-doctor/no-giant-component (warning)
Component "QuizStudySession" is over 300 lines long, which is hard to read & change. Split it into a few smaller components.
Fix → Pull each section into its own component so the parent is easier to read, test, and change.
| startSession("all", false); | ||
| resetProgress(); | ||
| }, [resetProgress, startSession]); | ||
| const toolbar = useMemo( |
There was a problem hiding this comment.
React Doctor · react-doctor/rerender-memo-before-early-return (warning)
This rebuilds the JSX whenever its dependencies change even on renders that take the early return, so move the JSX into a child component rendered after the early return to skip it
Fix → Move the JSX into a child component rendered after the early return, so renders that take the early return never build it
Adds quizzes as a workspace item type, replaces the AI's per-turn reference registry with self-describing addresses, and brings the flashcard and quiz viewers onto one shared study UI.
Ten commits, each reviewable on its own. The first four are the feature; the last six are the UI and validation work that followed.
What changes
Quiz items. Multiple choice, room for free response later. The model authors
question + correctAnswer + distractors + explanation; the server shuffles the options and records which one is correct, so option order can never leak the answer. Answers lock on first pick. Citations resolve to a single question, the way flashcard citations resolve to a card.Addresses replace
wr_refs. An address isrefKey[/unit][.r_revision]— an 8-character per-item key, an optional unit (p5,b_…, a card or question id), and a 6-character content-hash revision. Resolution is stateless, so nothing has to be handed to the model and kept in sync per turn. Reads reportreturned/totaland continue by asking for the next range instead of carrying a cursor.One study UI. The quiz viewer now uses the flashcard shell — same margins, scrollable body, progress strip, footer. Answer choices are Khan-style rows in one list, picking only selects, and a Submit button grades. Four layout bugs were fixed at the root and measured in the browser rather than eyeballed: unmounting hairlines, a collapsing Hint slot, an editor-only paragraph min-height leaking into read-only prose, and scroll position surviving question changes.
Mermaid diagrams render in documents, flashcards, and quizzes, not just chat. A mermaid block is an ordinary code block, so study items get diagrams without opening their allowlist to widgets, which stay documents-only. PDF export drops them rather than printing authoring syntax.
Entry rich text is validated at the model boundary only. Re-running the allowlist over stored content could only turn one odd entry into an item that will not open.
Review notes
correctOptionIdand the explanation reach the client before you answer. Fine for self-study; needs server-side grading if quiz scores ever become a signal worth trusting.packageManagerbump sitting in the working tree is deliberately not here.Test plan
pnpm verify(check + test + build) green on this tree.Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.