Skip to content

Ux improvements#131

Merged
urjitc merged 18 commits intomainfrom
ux-improvements
Jan 28, 2026
Merged

Ux improvements#131
urjitc merged 18 commits intomainfrom
ux-improvements

Conversation

@urjitc
Copy link
Member

@urjitc urjitc commented Jan 28, 2026

Important

This PR enhances UX with new UI components, redesigns, improved error handling, and performance optimizations, while updating navigation and layout configurations.

  • New Features:
    • Add ToolGroup UI component for better tool organization in thread.tsx and tool-group.tsx.
    • Redesign home page with floating cards in HomeContent.tsx.
    • Add "Currently viewing" indicator for cards in panel mode in WorkspaceCard.tsx.
  • Bug Fixes:
    • Improve error handling for quiz and flashcard generation in CreateFlashcardToolUI.tsx and CreateQuizToolUI.tsx.
    • Enhance URL and file processing with better error recovery in process-files.ts and process-urls.ts.
  • Performance Improvements:
    • Enable parallel processing for file and URL handling in process-files.ts and process-urls.ts.
    • Optimize workspace operations for faster responses in workspace-worker.ts.
  • Changes:
    • Migrate workspace navigation routes from /dashboard to /workspace in route.ts and page.tsx.
    • Reduce default quiz size from 10 to 5 questions in quiz-tools.ts.
    • Update sidebar to collapse by default and remove persistence storage in sidebar.tsx.
    • Adjust panel layout ratio for better workspace viewing in layout-constants.ts.
    • Simplify note creation by removing tags support in CreateNoteToolUI.tsx.
    • Restructure flashcard input format for better validation in flashcard-tools.ts.

This description was created by Ellipsis for ed38d93. You can customize this summary. It will automatically update as commits are pushed.


Summary by CodeRabbit

  • New Features

    • ToolGroup UI for better assistant tool grouping
    • Redesigned Home with floating workspace cards and refreshed hero
    • "Currently viewing" indicator when opening cards in panel mode
  • Bug Fixes

    • Improved error handling for quiz and flashcard generation
    • More robust URL and file processing with per-item recovery
  • Performance Improvements

    • Parallel processing for files/URLs
    • Faster workspace operations with parallel-friendly handling
  • Changes

    • Navigation consolidated to /workspace routes
    • Quiz default reduced to 5 questions
    • Sidebar now closed by default and no longer persisted
    • Note creation no longer supports tags
    • Flashcard/quiz input validation tightened and clarified
    • Panel split ratio adjusted for better layout

✏️ Tip: You can customize this high-level summary in your review settings.

urjitc added 17 commits January 28, 2026 05:27
- Set sidebar to closed by default (removed cookie persistence)
- Change 'Create' button to 'New' in workspace header
- Reduce breadcrumb font size to text-xs to match save indicator
- Remove font-medium from breadcrumb text for consistent weight
- Remove SidebarQuickActions component from workspace sidebar
- Updated home page prompt input styling with sidebar background color
- Adjusted focus border opacity and thickness for better visibility
- Increased spacing between 'What's on your mind?' heading and input
- Made floating cards more transparent on home route
- Added extra cards to bottom rows (home route only)
- Adjusted sidebar default open state behavior
- Created new /workspace routes (page.tsx and [slug]/page.tsx)
- Added redirects from /dashboard routes to /workspace for backwards compatibility
- Updated all code references from /dashboard to /workspace
- Updated robots.txt to disallow both /workspace and /dashboard routes
- Maintained backwards compatibility in WorkspaceContext for pathname detection
- Refactored placeholder options to align with ThinkEx's actual use cases
- Made options more specific with real subjects (calculus, chemistry, etc.)
- Balanced academic and professional use cases
- Shortened overly long options for better readability
- Reduced CS/tech focus, added more diverse examples (history, literature, business)
- Made options more relatable to college students
- Wrap workspace grid in sidebar background container with rounded corners
- Add 'Recent workspaces' heading with muted text color
- Reduce font weight of workspace titles and 'New workspace' text
- Move sign-in message from sidebar footer to below home prompt input
- Add radial blur effect behind sign-in message
- Hide sidebar sign-in message on home route
- Reduced workspace card background opacity from 0.25 to 0.1
- Makes workspace colors more subtle and transparent
- Removed ghost card that showed at top when panel is open
- Removed logic that hides cards from grid when panel is open
- Updated WorkspaceCard to show 'Currently viewing' state when panel is open
- Card now displays card name with X icon when panel is open
- Clicking card when panel is open closes the panel
- Removed unused imports and variables
- Increased ITEM_PANEL_SPLIT_RATIO from 0.6 to 0.7
- Workspace now gets 30% of available space (down from 40%) when panel is open
- Panel gets 70% of available space (up from 60%)
- Makes workspace smaller by default when panels are open, similar to resizable panels
- Add clearPlayingYouTubeCards function to UI store
- Clear playing videos when workspace view mounts
- Clear playing videos when workspace ID changes
- Prevents videos from auto-playing when returning from home route
- Update CreateQuizToolUI to include all tool fields (contextContent, sourceCardIds, sourceCardNames)
- Update UpdateQuizToolUI to include all tool fields (topic, contextContent, sourceCardIds, sourceCardNames)
- Update CreateFlashcardToolUI and UpdateFlashcardToolUI to accept string | object to match z.any() tool schema
- Update CreateNoteToolUI to remove unsupported tags field
- Update SelectCardsToolUI type documentation to clarify cardIds is client-side only
- Add tool-schema-comparison.md documenting all schema alignments
- Replace z.any() with proper Zod schemas using .passthrough() for streaming compatibility
- Convert flashcard tools from string parsing to structured JSON format
- Remove unused parseFlashcardText function (no longer needed with structured schemas)
- Update tool descriptions to reflect new structured format
- Improve type safety and validation for flashcard and quiz tools
Auto-open tool groups while a tool-call is streaming, then collapse when done, while still allowing manual expand/collapse.
@vercel
Copy link

vercel bot commented Jan 28, 2026

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

Project Deployment Review Updated (UTC)
thinkex Ready Ready Preview, Comment Jan 28, 2026 6:27pm

@coderabbitai
Copy link

coderabbitai bot commented Jan 28, 2026

Caution

Review failed

The pull request is closed.

📝 Walkthrough

Walkthrough

Migrates dashboard routes to /workspace paths, adds a ToolGroup UI, restructures home/onboarding flows, replaces free-form AI tool inputs with structured Zod schemas and parallel processing, adds workspace-worker version-conflict retry and allowParallel option, and tweaks sidebar and UI state behaviors.

Changes

Cohort / File(s) Summary
Route migration & routing changes
public/robots.txt, src/app/dashboard/page.tsx, src/app/dashboard/[slug]/page.tsx, src/app/workspace/[slug]/page.tsx, src/app/workspace/page.tsx, src/app/onboarding/page.tsx, src/app/api/user/onboarding/route.ts, src/app/guest-setup/page.tsx, src/app/share/[id]/page.tsx, src/contexts/WorkspaceContext.tsx
Redirects/targets changed from /dashboard/* to /workspace/*; added workspace app routes and client-side redirects; robots.txt updated to disallow /workspace/ and retain legacy /dashboard/ comment.
Home / landing UI
src/components/home/*, src/components/landing/FloatingWorkspaceCards.tsx, src/app/home/page.tsx
Home layout and hero reworked (floating cards, centered hero); prompt placeholders changed; FloatingWorkspaceCards made prop-driven (opacity, includeExtraCards); redirects now point to /workspace/{slug}.
Sidebar & session handling
src/components/ui/sidebar.tsx, src/components/layout/SessionHandler.tsx
Removed cookie-based sidebar persistence; changed SidebarProvider defaultOpen from true → false; SidebarCoordinator accepts optional defaultOpen.
Workspace canvas / grid / header / sidebar
src/components/workspace-canvas/WorkspaceCard.tsx, src/components/workspace-canvas/WorkspaceGrid.tsx, src/components/workspace-canvas/WorkspaceHeader.tsx, src/components/workspace-canvas/WorkspaceSidebar.tsx
Panel-mode behavior refactored: card previews gated by panel state with “Currently viewing” display; ghost-panel UI removed and grid always shows items; header typography reduced; quick actions hidden on home route.
Assistant UI / ToolGroup
src/components/assistant-ui/tool-group.tsx, src/components/assistant-ui/thread.tsx
New ToolGroup component (Root/Trigger/Content) with streaming-aware auto-open, collapsible behavior, and styling variants; ToolGroup wired into assistant message parts.
Assistant tool UI types & receipts
src/components/assistant-ui/*.tsx (CreateFlashcard, UpdateFlashcard, CreateQuiz, UpdateQuiz, CreateNote, SelectCards)
Tool arg shapes expanded/changed: structured object support and type guards added for flashcard/update tools; CreateNoteArgs dropped tags; CreateQuiz/UpdateQuiz args expanded; SelectCardsArgs.cardTitles made required; CreateFlashcardReceipt accepts lifecycle status props.
AI tool implementations
src/lib/ai/tools/flashcard-tools.ts, src/lib/ai/tools/quiz-tools.ts, src/lib/ai/tools/process-files.ts, src/lib/ai/tools/process-urls.ts
Replaced free-form text parsing with explicit zodSchema inputs (flashcards, quizzes); quiz size default reduced 10→5; file and URL processing changed to parallel Promise.all patterns with per-item error handling and richer per-URL results.
Tool result parsing & schema docs
src/lib/ai/tool-result-schemas.ts, tool-schema-comparison.md
parseQuizResult/parseFlashcardResult now coerce non-object inputs safely before schema parsing; added tool-schema-comparison.md documenting schema matches/mismatches.
Workers & workspace operations
src/lib/ai/workers/common.ts, src/lib/ai/workers/workspace-worker.ts, src/lib/ai/workers/quiz-worker.ts
Added allowParallel option to executeWorkspaceOperation to bypass serialization; workspace-worker gains version-conflict retry loop for create/append events (up to retries); quiz-worker introduces DEFAULT_CHAT_MODEL_ID and aligns question defaults to 5.
Layout constants & UI store
src/lib/layout-constants.ts, src/lib/stores/ui-store.ts
ITEM_PANEL_SPLIT_RATIO adjusted 0.6→0.7; UI store adds clearPlayingYouTubeCards() action.
Workspace creation UI
src/components/workspace/CreateWorkspaceModal.tsx, src/components/workspace/SharedWorkspaceModal.tsx, src/app/guest-setup/page.tsx
Post-create navigation changed from /dashboard/{slug}/workspace/{slug}.
Other UI tweaks
src/components/home/HomePromptInput.tsx, src/components/home/WorkspaceGrid.tsx, src/components/workspace/*
Styling and minor behavior changes: sidebar defaultOpen usage, workspace card typography and opacity adjustments, minor breadcrumb/label style reductions.

Sequence Diagram(s)

mermaid
sequenceDiagram
autonumber
participant Client
participant API as "API /route"
participant Worker as "WorkspaceWorker"
participant DB as "DB (append_workspace_event)"
participant Queue as "serialize queue (common.executeWorkspaceOperation)"

Client->>API: create workspace request
API->>Worker: call create workspace (allowParallel? false/true)
alt allowParallel = true
Worker->>DB: append_workspace_event(baseVersion)
DB-->>Worker: success | conflict (returns version)
else allowParallel = false
API->>Queue: enqueue operation
Queue->>Worker: run operation
Worker->>DB: append_workspace_event(baseVersion)
DB-->>Worker: success | conflict (returns version)
end
alt DB conflict
Worker->>DB: retry with updated version (up to 2 retries)
DB-->>Worker: success | conflict
end
Worker-->>API: result (success or error)
API-->>Client: HTTP redirect to /workspace/{slug} or error

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

enhancement

Poem

🐰
From dashboard burrows to workspace light,
I hop through routes and tidy the sight,
ToolGroups gather, workers retry,
Cards float and sidebars wink goodbye,
A rabbit cheers: new paths take flight!

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Title check ⚠️ Warning The title "Ux improvements" is too vague and generic; it does not convey specific information about the actual changes in the pull request, which involve significant restructuring of routing paths from /dashboard/ to /workspace/, addition of new UI components (ToolGroup), and multiple tool input schema changes. Replace the generic title with a specific, concise description of the primary change, such as "Migrate dashboard routes to workspace routes and introduce ToolGroup component" or "Refactor routing from /dashboard to /workspace with ToolGroup UI additions".
Docstring Coverage ⚠️ Warning Docstring coverage is 46.51% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing touches
  • 📝 Generate docstrings

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

❤️ Share

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

Copy link
Contributor

@ellipsis-dev ellipsis-dev bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

Looks good to me! 👍

Reviewed everything up to b06eb59 in 39 seconds. Click for details.
  • Reviewed 2373 lines of code in 42 files
  • Skipped 0 files when reviewing.
  • Skipped posting 0 draft comments. View those below.
  • Modify your settings and rules to customize what types of comments Ellipsis leaves. And don't forget to react with 👍 or 👎 to teach Ellipsis.

Workflow ID: wflow_CtCNPm3ZlllfZdLi

You can customize Ellipsis by changing your verbosity settings, reacting with 👍 or 👎, replying to comments, or adding code review rules.

// read the same baseVersion before the lock, causing conflicts. Retry with the
// conflict version (which the DB returns) to handle this gracefully.
let baseVersion = 0;
let appendResult: { version: number; conflict: boolean } = { version: 0, conflict: false };
const results = await Promise.all(urlProcessingPromises);

// Aggregate results
const successfulResults = results.filter(r => r.success);
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Jan 28, 2026

Greptile Overview

Greptile Summary

This PR implements UX improvements focused on better tool handling, performance optimization, and enhanced visual design.

Major Changes:

  • Refactored flashcard and quiz tools from text parsing to structured Zod schemas, improving type safety and reliability
  • Added new collapsible ToolGroup component with auto-expand/collapse during streaming for better tool call visualization
  • Optimized file and URL processing to run in parallel instead of sequentially, improving performance
  • Enhanced home page with floating workspace card backgrounds and sign-in CTAs for anonymous users
  • Improved workspace card panel state handling with "Currently viewing" indicator
  • Added result coercion functions to handle string/non-object tool results gracefully

Technical Improvements:

  • Tool schemas now use .passthrough() to handle streaming properties from AI models without validation errors
  • Parallel processing with Promise.all() reduces latency for multi-file/URL operations
  • Better error handling and logging throughout workspace operations
  • Documentation added comparing tool schemas with UI components

Confidence Score: 5/5

  • This PR is safe to merge with high confidence
  • The changes are well-structured refactorings and UX improvements. The code follows consistent patterns, includes proper error handling, and the parallel processing optimizations are correctly implemented. The schema refactoring from text parsing to structured objects is a significant improvement in type safety. No security issues or logical errors detected.
  • No files require special attention

Important Files Changed

Filename Overview
src/components/assistant-ui/tool-group.tsx New collapsible tool group component with auto-expand/collapse during streaming
src/lib/ai/tools/flashcard-tools.ts Refactored from text parsing to structured object input with Zod schema validation
src/lib/ai/tools/quiz-tools.ts Refactored to use proper Zod schemas with .passthrough() for streaming compatibility
src/components/workspace-canvas/WorkspaceCard.tsx Improved panel state handling and added "Currently viewing" state for open panels
src/lib/ai/tools/process-files.ts Optimized file processing to run different file types in parallel using Promise.all()
src/lib/ai/tools/process-urls.ts Refactored to process multiple URLs in parallel instead of sequentially

Sequence Diagram

sequenceDiagram
    participant User
    participant HomeContent
    participant Assistant
    participant ToolGroup
    participant Tools
    participant WorkspaceWorker
    participant Database

    User->>HomeContent: View home page
    HomeContent->>HomeContent: Check session.user.isAnonymous
    alt Anonymous User
        HomeContent->>User: Show sign-in CTA with blur overlay
    end

    User->>Assistant: Create flashcards/quiz
    Assistant->>Tools: Execute flashcard-tools/quiz-tools
    Note over Tools: Uses structured Zod schemas<br/>with .passthrough() for streaming
    Tools->>WorkspaceWorker: workspaceWorker("create", params)
    WorkspaceWorker->>Database: append_workspace_event()
    Database-->>WorkspaceWorker: {version, conflict}
    WorkspaceWorker-->>Tools: {success, itemId, event}
    Tools-->>Assistant: Return result
    Assistant->>ToolGroup: Stream tool calls
    ToolGroup->>ToolGroup: Auto-expand during streaming
    ToolGroup->>ToolGroup: Auto-collapse when complete
    ToolGroup-->>User: Show collapsible tool results

    User->>Assistant: Process files/URLs
    Assistant->>Tools: processFiles() or processUrls()
    par Parallel Processing
        Tools->>Tools: Process file type 1
        Tools->>Tools: Process file type 2
        Tools->>Tools: Process file type 3
    end
    Note over Tools: All file types/URLs<br/>processed in parallel
    Tools-->>Assistant: Combined results
    Assistant-->>User: Display content

    User->>WorkspaceCard: Click card in panel
    WorkspaceCard->>WorkspaceCard: Check isOpenInPanel
    alt Already open
        WorkspaceCard->>WorkspaceCard: closePanel(item.id)
        WorkspaceCard-->>User: Show "Currently viewing" state
    else Not open
        WorkspaceCard->>WorkspaceCard: Open in panel
    end
Loading

@sonarqubecloud
Copy link

Copy link
Contributor

@ellipsis-dev ellipsis-dev bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

Looks good to me! 👍

Reviewed ed38d93 in 46 seconds. Click for details.
  • Reviewed 13 lines of code in 1 files
  • Skipped 0 files when reviewing.
  • Skipped posting 0 draft comments. View those below.
  • Modify your settings and rules to customize what types of comments Ellipsis leaves. And don't forget to react with 👍 or 👎 to teach Ellipsis.

Workflow ID: wflow_CQRqN5pPpPOyIslr

You can customize Ellipsis by changing your verbosity settings, reacting with 👍 or 👎, replying to comments, or adding code review rules.

@urjitc urjitc merged commit e1a1904 into main Jan 28, 2026
6 of 7 checks passed
@urjitc urjitc deleted the ux-improvements branch January 28, 2026 18:26
@github-project-automation github-project-automation bot moved this from Backlog to Done in Dev Board Jan 28, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/lib/ai/tools/flashcard-tools.ts (1)

191-198: Fuzzy matching could cause AI to select unintended flashcard decks.

This is an AI-driven tool where the model generates the deckName input. The three-stage fuzzy matching (exact → deck contains term → term contains deck) stops at the first match without informing the AI that multiple decks could match at the same stage. For example, searching "Bio" would match the first deck containing "bio" (potentially "Biodegrade" instead of "Biology"), with no mechanism for the AI to see alternatives or be prompted to clarify.

Since the AI cannot see partial matches or request disambiguation, it may silently add cards to the wrong deck. Consider tracking and logging when multiple decks match at a given stage, or requiring an exact match when ambiguity exists (removing the broader "contains" stages).

🤖 Fix all issues with AI agents
In `@src/app/workspace/`[slug]/page.tsx:
- Around line 23-29: The WorkspacePage component accepts WorkspacePageProps with
params.slug but doesn't use it; either remove the unused prop or explicitly
document why it's accepted: update the WorkspacePage signature (remove
WorkspacePageProps and the params param) or keep it and add a brief inline
comment above WorkspacePage explaining that current slug is derived from
usePathname() inside WorkspaceProvider and consumers should use
useWorkspaceContext().currentSlug; reference WorkspacePage, WorkspacePageProps,
and params.slug so reviewers can locate the change.

In `@src/components/home/HomeContent.tsx`:
- Around line 72-88: The Link wrapping Buttons creates nested interactive
elements; change each usage to render the Link as a child of the Button by using
the Button prop asChild (e.g., replace <Link href="..."><Button
...>...</Button></Link> with <Button asChild ...><Link
href="...">...</Link></Button>), keeping the Button's variant/size/className
props on Button and the href on Link so the visual styling remains while
avoiding nested interactive elements in HomeContent.tsx and referencing the
Button and Link components.
- Around line 18-21: The FloatingWorkspaceCards usage passes an unsupported
Tailwind class "opacity-15"; update the prop passed to the opacity prop (in the
FloatingWorkspaceCards JSX) to use a supported value such as "opacity-10
md:opacity-20" (or replace only the unsupported token with "opacity-20") so
Tailwind v4 will apply the intended opacity correctly.

In `@src/components/workspace-canvas/WorkspaceCard.tsx`:
- Around line 783-784: In WorkspaceCard (component WorkspaceCard or file
WorkspaceCard.tsx) remove the duplicated JSX comment "Subtle type label for
narrow cards without preview" so only a single instance remains; locate the two
adjacent comment nodes around the narrow-card label render and delete the
redundant line to avoid repeated comments.

In `@src/lib/ai/tools/flashcard-tools.ts`:
- Around line 134-136: In updateFlashcards, remove the redundant fallback that
sets const cardsToAdd = input.cards || []; because the input schema enforces
.min(1); instead reference input.cards directly (e.g., const cardsToAdd =
input.cards) or use input.cards everywhere to avoid masking missing-data bugs;
update the function body around updateFlashcards/execute and the cardsToAdd
binding to stop using the || [] pattern and rely on the schema-validated
input.cards.
- Around line 51-52: The code uses a fallback `input.cards || []` which can mask
schema violations and makes the subsequent `if (cards.length === 0)` check
redundant; instead, trust the Zod-validated `input.cards` by assigning `const
cards = input.cards;` (keep `const title = input.title || "Flashcard Deck";` if
you still want a fallback for title) and remove the unreachable `if
(cards.length === 0)` guard, or alternatively keep defensive validation but
remove the `|| []` fallback so that a missing `cards` will surface as an error;
update references to `cards` accordingly.

In `@src/lib/ai/tools/process-urls.ts`:
- Around line 124-151: combinedText currently concatenates text from the entire
results array, which can include error strings from failedResults; change the
construction of combinedText in process-urls.ts to only aggregate texts from
successful result objects (filter out failedResults or use a success flag on
entries in results), and then append or return a separate concise failure
summary (e.g., list of failedResults.map(r => r.url) and short error messages)
under a different field so errors are not mixed into combinedText; update any
callers expecting combinedText to remain pure content-only.

In `@src/lib/ai/workers/common.ts`:
- Around line 12-25: The parallel branch in executeWorkspaceOperation currently
runs operation() without recording it, allowing later ops to run before the
create finishes; change it to start the promise (call operation()) and
immediately register that in the workspace's queue/registry so subsequent calls
serialize behind this in-flight promise (i.e., enqueue the promise for
workspaceId but do not await previous queue entries before starting it). Use the
existing executeWorkspaceOperation, operation and options.allowParallel symbols
to locate the logic and ensure the enqueuing API you have for non-parallel paths
(the workspace queue/map enqueue/register function) is used to store the
returned promise so later operations see it and wait.

In `@src/lib/ai/workers/quiz-worker.ts`:
- Around line 7-8: DEFAULT_CHAT_MODEL_ID is set to the preview model
"gemini-3-flash-preview"; replace it with a stable model identifier or make the
model configurable and document the choice. Update the DEFAULT_CHAT_MODEL_ID
constant (and any code that imports it) to either a GA model (e.g., "gemini-3"
or another stable model your org approves) or read from an environment/config
value with a clear fallback to allow switching without code changes; also add a
brief comment near DEFAULT_CHAT_MODEL_ID explaining why a preview is used if you
intentionally keep it and note the risks (rate limits, breaking changes,
deprecation).
🧹 Nitpick comments (16)
src/components/assistant-ui/tool-group.tsx (1)

216-221: Clarify the user toggle behavior in the comment.

The current comment states "Still allows manual user toggle when not streaming," but the useEffect will override any user toggle when isToolGroupStreaming changes. If a user manually opens the group while not streaming, and then streaming starts and ends, the group will be forced closed regardless of user preference.

If this is the intended behavior (auto-collapse when streaming completes), the comment should be updated to reflect this more accurately. Otherwise, consider preserving user preference when the user has manually interacted.

✏️ Suggested comment clarification
-  // Auto-open while streaming; auto-close when streaming ends.
-  // Still allows manual user toggle when not streaming.
+  // Auto-open while streaming; auto-close when streaming ends.
+  // User can toggle manually, but streaming state changes will override the toggle.
   const [open, setOpen] = useState(false);
   useEffect(() => {
     setOpen(isToolGroupStreaming);
   }, [isToolGroupStreaming]);
src/components/assistant-ui/SelectCardsToolUI.tsx (4)

64-80: Duplicated title resolution logic.

The title-to-ID resolution algorithm (exact match, then contains match) is duplicated here and in the selectedCards useMemo (lines 111-120). Consider extracting this into a shared helper function to ensure consistency and reduce maintenance burden.

♻️ Suggested refactor
// Extract helper function (place before the component)
function resolveCardTitlesToIds(
  titles: string[],
  items: { id: string; name: string }[],
  availableIds: Set<string>
): string[] {
  const resolvedIds: string[] = [];
  titles.forEach((title) => {
    const searchTitle = title.toLowerCase().trim();
    let match = items.find((item) => item.name.toLowerCase().trim() === searchTitle);
    if (!match) {
      match = items.find((item) => item.name.toLowerCase().includes(searchTitle));
    }
    if (match && availableIds.has(match.id) && !resolvedIds.includes(match.id)) {
      resolvedIds.push(match.id);
    }
  });
  return resolvedIds;
}

92-92: Potentially misleading dependency array.

Including currentSelectedIds in the dependency array causes the effect to re-evaluate whenever selection changes, but hasSelectedRef blocks actual execution. This is semantically confusing and could hide bugs if the ref logic changes. Consider whether currentSelectedIds truly needs to be a dependency or if it can be accessed via a ref to avoid unnecessary re-evaluations.


122-125: Avoid any[] type assertion.

The type assertion as any[] loses type safety. Use a type predicate in the filter to properly narrow the type.

♻️ Proposed fix
       return Array.from(resolvedIds)
         .map((id) => state.items.find((item) => item.id === id))
-        .filter((item) => item !== undefined) as any[]; // Type assertion needed for strict mode
+        .filter((item): item is NonNullable<typeof item> => item !== undefined);

130-130: Clarify intent of unused parse result.

The parsed result from parseSelectCardsResult is discarded. If this is intentional validation (throwing on invalid schema), consider adding a brief comment to clarify, or use the parsed/validated result instead of the raw result for type safety downstream.

♻️ Suggested clarification
-    if (result != null) parseSelectCardsResult(result);
+    // Validate result schema (throws on invalid structure)
+    if (result != null) parseSelectCardsResult(result);
src/lib/ai/tools/quiz-tools.ts (2)

22-46: Schema mismatch: topic is required in inputSchema but optional in createQuizSchema.

The inputSchema declares topic as z.string() (required), while the internal createQuizSchema declares it as z.string().optional(). This inconsistency could cause confusion about the actual contract. Consider aligning them—likely both should be optional since line 56 handles the case where neither is provided.

Additionally, defining similar schemas twice (for inputSchema and execute validation) adds maintenance burden. Consider extracting a shared base schema.

♻️ Suggested refactor to align schemas
+// Shared schema for createQuiz arguments
+const createQuizArgsSchema = z.object({
+    topic: z.string().optional().describe("The topic for the quiz - extract from user's message"),
+    contextContent: z.string().optional().describe("Content from selected cards in system context if available"),
+    sourceCardIds: z.array(z.string()).optional().describe("IDs of source cards"),
+    sourceCardNames: z.array(z.string()).optional().describe("Names of source cards"),
+    difficulty: z.enum(["easy", "medium", "hard"]).optional().default("medium").describe("Difficulty level"),
+});

 export function createQuizTool(ctx: WorkspaceToolContext) {
     return {
         description: "...",
-        inputSchema: zodSchema(
-            z.object({
-                topic: z.string().describe("The topic for the quiz - REQUIRED: extract from user's message"),
-                contextContent: z.string().optional().describe("Content from selected cards in system context if available"),
-                sourceCardIds: z.array(z.string()).optional().describe("IDs of source cards"),
-                sourceCardNames: z.array(z.string()).optional().describe("Names of source cards"),
-                difficulty: z.enum(["easy", "medium", "hard"]).optional().default("medium").describe("Difficulty level"),
-            }).passthrough()
-        ),
+        inputSchema: zodSchema(createQuizArgsSchema.passthrough()),
         execute: async (args: unknown) => {
-            const createQuizSchema = z.object({
-                topic: z.string().optional(),
-                contextContent: z.string().optional(),
-                sourceCardIds: z.array(z.string()).optional(),
-                sourceCardNames: z.array(z.string()).optional(),
-                difficulty: z.enum(["easy", "medium", "hard"]).optional().default("medium"),
-            }).passthrough();
-            
-            const parsedArgs = createQuizSchema.parse(args);
+            const parsedArgs = createQuizArgsSchema.passthrough().parse(args);

135-154: Same schema duplication pattern in createUpdateQuizTool.

Similar to createQuizTool, consider extracting a shared schema to avoid duplication between inputSchema and the internal validation schema.

src/app/workspace/page.tsx (1)

1-13: Prefer a server redirect to avoid client-side flash.

A server redirect("/home") avoids hydration and renders no UI.

♻️ Proposed refactor
-"use client";
-
-import { useRouter } from "next/navigation";
-import { useEffect } from "react";
-
 export default function WorkspacePage() {
-  const router = useRouter();
-
-  useEffect(() => {
-    router.replace("/home");
-  }, [router]);
-
-  return null;
+  redirect("/home");
 }
+import { redirect } from "next/navigation";
src/components/workspace-canvas/WorkspaceSidebar.tsx (1)

71-73: Broaden the home-route check to cover subroutes/trailing slash.

This ensures the anonymous footer stays hidden across /home subpaths.

♻️ Proposed tweak
-  const isHomeRoute = pathname === "/home";
+  const isHomeRoute = pathname === "/home" || pathname.startsWith("/home/");

Also applies to: 277-278

src/components/assistant-ui/CreateFlashcardToolUI.tsx (2)

42-51: Consider using stricter types instead of any.

The status prop and allItems array are typed as any, which loses type safety. While this may be acceptable for UI components consuming external data, consider using more specific types if available from @assistant-ui/react.

♻️ Suggested improvement
 interface CreateFlashcardReceiptProps {
     args: CreateFlashcardArgs;
     result: FlashcardResult;
-    status: any;
+    status: { type: "running" | "complete" | "incomplete"; reason?: string };
     moveItemToFolder?: (itemId: string, folderId: string | null) => void;
-    allItems?: any[];
+    allItems?: Item[];
     workspaceName?: string;
     workspaceIcon?: string | null;
     workspaceColor?: string | null;
 }

134-135: Fallback chain is well-structured but has repetition.

The fallback pattern result.cardCount || result.cards?.length || argsCardsLen || '?' is used multiple times. Consider extracting to a computed variable for DRY compliance and consistency.

♻️ Suggested improvement
     const argsObj = isCreateFlashcardArgsObject(args) ? args : null;
     const argsTitle = argsObj?.title;
     const argsCardsLen = argsObj?.cards?.length;
+    
+    // Computed card count with fallback chain
+    const cardCount = result.cardCount || result.cards?.length || argsCardsLen || 0;
+    const cardCountDisplay = cardCount || '?';

Then use cardCountDisplay in the UI and cardCount for the plural check.

Also applies to: 157-157, 159-159

src/lib/ai/tools/flashcard-tools.ts (3)

37-47: Schema uses .passthrough() which may allow unexpected fields.

Using .passthrough() allows extra fields to pass through validation without stripping them. While this provides flexibility, it may leak unexpected data to downstream workers. Consider if .strict() or no modifier (default strips unknown keys) would be more appropriate for security.

♻️ Consider removing .passthrough()
         inputSchema: zodSchema(
             z.object({
                 title: z.string().optional().describe("The title of the flashcard deck (defaults to 'Flashcard Deck' if not provided)"),
                 cards: z.array(
                     z.object({
                         front: z.string().describe("The question or term on the front of the card"),
                         back: z.string().describe("The answer or definition on the back of the card"),
                     })
                 ).min(1).describe("Array of flashcard objects, each with 'front' and 'back' properties"),
-            }).passthrough()
+            })
         ),

123-133: Same .passthrough() concern applies to updateFlashcards schema.

Consistent with the create tool, consider removing .passthrough() for tighter input validation.


165-166: Dynamic imports may impact cold-start performance.

The dynamic import() calls for @/lib/db/client and drizzle-orm on every tool execution could add latency. Consider hoisting these imports to the module level if the tool is frequently called.

♻️ Suggested improvement

Move imports to the top of the file:

import { db, workspaces } from "@/lib/db/client";
import { eq } from "drizzle-orm";

This avoids the overhead of dynamic imports on each execution while maintaining the same functionality.

src/components/workspace-canvas/WorkspaceCard.tsx (2)

551-551: Unused variable setOpenModalItemId in dependency array.

The handleCardClick callback includes setOpenModalItemId in its dependency array but doesn't use it in the function body anymore (it was replaced by closePanel). This could cause unnecessary re-renders if the store reference changes.

♻️ Remove unused dependency
-  }, [isEditingTitle, isOpenInPanel, item.id, item.type, onOpenModal, setOpenModalItemId, openPanelIds, isYouTubePlaying, setCardPlaying, maximizedItemId]);
+  }, [isEditingTitle, isOpenInPanel, item.id, item.type, onOpenModal, closePanel, openPanelIds, isYouTubePlaying, setCardPlaying, maximizedItemId, onToggleSelection]);

Note: closePanel should be added since it's now used, and onToggleSelection is used but missing from the array.


1090-1145: Memo comparison function is thorough but expensive for large data.

The JSON.stringify comparisons for item data (lines 1102, 1107, 1112, 1117, 1122) can be expensive for items with large content. This is a known tradeoff between re-render prevention and comparison cost.

For items with very large blockContent arrays (notes) or many flashcards, consider a hash-based approach or comparing only key fields that actually trigger visual changes.

Comment on lines +23 to +29
interface WorkspacePageProps {
params: { slug: string };
}

export default function WorkspacePage({ params }: WorkspacePageProps) {
return <DashboardShell />;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's find the WorkspaceProvider implementation
rg -n 'export.*WorkspaceProvider' --type=tsx --type=ts

# Check WorkspaceContext for useParams usage
rg -n 'useParams' --type=tsx --type=ts

Repository: ThinkEx-OSS/thinkex

Length of output: 118


🏁 Script executed:

#!/bin/bash

# Find files related to WorkspaceContext/WorkspaceProvider
fd -e tsx -e ts | grep -i 'context\|provider' | head -20

# Search for WorkspaceProvider across all TypeScript files
rg 'WorkspaceProvider' -A 10

# Search for useParams to see how params are consumed
rg 'useParams'

# Check the specific file mentioned in the review
cat -n src/app/workspace/[slug]/page.tsx

Repository: ThinkEx-OSS/thinkex

Length of output: 7176


🏁 Script executed:

#!/bin/bash

# Get the full WorkspaceContext.tsx file
cat -n src/contexts/WorkspaceContext.tsx

# Also check the DashboardShell component to understand what it expects
rg -n 'export.*DashboardShell' -A 20

Repository: ThinkEx-OSS/thinkex

Length of output: 10006


The slug parameter is unused, but this appears intentional.

The params.slug is not used because WorkspaceProvider derives the current slug directly from the URL pathname via usePathname() (see src/contexts/WorkspaceContext.tsx lines 48-58). Any component needing the current slug can access it via useWorkspaceContext().currentSlug.

Consider either removing the unused parameter or adding a comment explaining why it's accepted but not used.

🤖 Prompt for AI Agents
In `@src/app/workspace/`[slug]/page.tsx around lines 23 - 29, The WorkspacePage
component accepts WorkspacePageProps with params.slug but doesn't use it; either
remove the unused prop or explicitly document why it's accepted: update the
WorkspacePage signature (remove WorkspacePageProps and the params param) or keep
it and add a brief inline comment above WorkspacePage explaining that current
slug is derived from usePathname() inside WorkspaceProvider and consumers should
use useWorkspaceContext().currentSlug; reference WorkspacePage,
WorkspacePageProps, and params.slug so reviewers can locate the change.

Comment on lines +18 to +21
<FloatingWorkspaceCards
bottomGradientHeight="40%"
opacity="opacity-10 md:opacity-15"
includeExtraCards={true}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for opacity-15 usage
echo "=== Searching for opacity-15 usage ==="
rg "opacity-15" -S

echo -e "\n=== Locating Tailwind config files ==="
fd -a "tailwind\.(js|cjs|mjs|ts)$|tailwind\.config\.(js|cjs|mjs|ts)$"

echo -e "\n=== Checking for Tailwind extend config in other files ==="
rg -l "tailwindConfig|extend.*opacity" --type js --type ts

Repository: ThinkEx-OSS/thinkex

Length of output: 282


🏁 Script executed:

#!/bin/bash
# Check package.json for Tailwind version
echo "=== Checking package.json for Tailwind ==="
cat package.json | grep -A 2 -B 2 "tailwind"

echo -e "\n=== Checking for PostCSS or other CSS config ==="
fd -a "postcss\.config\.(js|cjs|mjs|ts)$|next\.config\.(js|ts)$|vite\.config\.(js|ts)$"

echo -e "\n=== Checking tsconfig for CSS/Tailwind hints ==="
fd -a "tsconfig.json$" -exec cat {} \;

echo -e "\n=== Checking for CSS files with Tailwind directives ==="
rg "@tailwind|@apply" --type css --type tsx -l

Repository: ThinkEx-OSS/thinkex

Length of output: 1012


🏁 Script executed:

#!/bin/bash
echo "=== PostCSS config ==="
cat postcss.config.mjs

echo -e "\n=== Next.js config (first 100 lines) ==="
head -100 next.config.ts

echo -e "\n=== Search for Tailwind theme/config in codebase ==="
rg -i "theme|opacity" --type ts --type js -B 2 -A 2 | head -50

Repository: ThinkEx-OSS/thinkex

Length of output: 4430


🏁 Script executed:

#!/bin/bash
echo "=== Check for any custom Tailwind CSS output or tailwind imports ==="
find . -name "*.css" -type f | head -20

echo -e "\n=== Look for globals.css or main CSS file ==="
fd -e css | grep -E "(global|main|layout|app)" | head -20

echo -e "\n=== Verify CSS processing - check for generated CSS ==="
ls -la src/ | grep -i css || echo "No CSS files in src/"

echo -e "\n=== Check if opacity-15 is used anywhere else ==="
rg "opacity-15|opacity-\[" --type tsx --type ts -B 2 -A 2

Repository: ThinkEx-OSS/thinkex

Length of output: 561


🏁 Script executed:

#!/bin/bash
echo "=== Check globals.css for Tailwind setup and custom opacity ==="
cat src/app/globals.css

Repository: ThinkEx-OSS/thinkex

Length of output: 34379


Remove opacity-15 — it's not in Tailwind v4's default opacity scale.

Tailwind v4.1.18's default opacity values are: 0, 5, 10, 20, 25, 30, 40, 50, 60, 70, 75, 80, 90, 95, 100. The class opacity-15 will be silently ignored since 15% is not defined. Use opacity-20 instead for a standard supported value.

Suggested fix
-          opacity="opacity-10 md:opacity-15"
+          opacity="opacity-10 md:opacity-20"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<FloatingWorkspaceCards
bottomGradientHeight="40%"
opacity="opacity-10 md:opacity-15"
includeExtraCards={true}
<FloatingWorkspaceCards
bottomGradientHeight="40%"
opacity="opacity-10 md:opacity-20"
includeExtraCards={true}
🤖 Prompt for AI Agents
In `@src/components/home/HomeContent.tsx` around lines 18 - 21, The
FloatingWorkspaceCards usage passes an unsupported Tailwind class "opacity-15";
update the prop passed to the opacity prop (in the FloatingWorkspaceCards JSX)
to use a supported value such as "opacity-10 md:opacity-20" (or replace only the
unsupported token with "opacity-20") so Tailwind v4 will apply the intended
opacity correctly.

Comment on lines +72 to +88
<Link href="/auth/sign-in">
<Button
variant="outline"
size="sm"
className="h-8 text-xs"
>
Sign in
</Button>
</Link>
<Link href="/auth/sign-up">
<Button
size="sm"
className="h-8 text-xs"
>
Sign up
</Button>
</Link>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's check the structure and locate the HomeContent file
find . -name "HomeContent.tsx" -type f

Repository: ThinkEx-OSS/thinkex

Length of output: 100


🏁 Script executed:

# Look for Button component definitions
fd -e tsx -e ts . src/components | grep -i button | head -20

Repository: ThinkEx-OSS/thinkex

Length of output: 339


🏁 Script executed:

# Check what's actually at lines 72-88 in HomeContent.tsx
cat -n src/components/home/HomeContent.tsx | sed -n '70,90p'

Repository: ThinkEx-OSS/thinkex

Length of output: 892


🏁 Script executed:

# Search for Button component imports and usage patterns
rg "from.*Button|import.*Button" src/components/home/ -A 2

Repository: ThinkEx-OSS/thinkex

Length of output: 822


🏁 Script executed:

cat -n src/components/ui/button.tsx

Repository: ThinkEx-OSS/thinkex

Length of output: 2639


🏁 Script executed:

# Check dependencies and see if using Radix UI
rg "asChild|@radix-ui" src/components/ui/button.tsx

Repository: ThinkEx-OSS/thinkex

Length of output: 188


🏁 Script executed:

# Check package.json for the Button library being used
cat package.json | grep -A 50 -B 5 "dependencies"

Repository: ThinkEx-OSS/thinkex

Length of output: 2295


Avoid wrapping <Button> inside <Link>.
This creates nested interactive elements; the Button component supports asChild which allows you to reverse the nesting: place the <Link> as a child of <Button asChild> instead.

✅ Suggested fix (Button asChild)
-                  <Link href="/auth/sign-in">
-                    <Button
-                      variant="outline"
-                      size="sm"
-                      className="h-8 text-xs"
-                    >
-                      Sign in
-                    </Button>
-                  </Link>
+                  <Button
+                    asChild
+                    variant="outline"
+                    size="sm"
+                    className="h-8 text-xs"
+                  >
+                    <Link href="/auth/sign-in">Sign in</Link>
+                  </Button>
-                  <Link href="/auth/sign-up">
-                    <Button
-                      size="sm"
-                      className="h-8 text-xs"
-                    >
-                      Sign up
-                    </Button>
-                  </Link>
+                  <Button asChild size="sm" className="h-8 text-xs">
+                    <Link href="/auth/sign-up">Sign up</Link>
+                  </Button>
🤖 Prompt for AI Agents
In `@src/components/home/HomeContent.tsx` around lines 72 - 88, The Link wrapping
Buttons creates nested interactive elements; change each usage to render the
Link as a child of the Button by using the Button prop asChild (e.g., replace
<Link href="..."><Button ...>...</Button></Link> with <Button asChild ...><Link
href="...">...</Link></Button>), keeping the Button's variant/size/className
props on Button and the href on Link so the visual styling remains while
avoiding nested interactive elements in HomeContent.tsx and referencing the
Button and Link components.

Comment on lines +783 to +784
{/* Subtle type label for narrow cards without preview */}
{/* Subtle type label for narrow cards without preview */}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Duplicate comment line.

Lines 783-784 have the same comment repeated:

{/* Subtle type label for narrow cards without preview */}
{/* Subtle type label for narrow cards without preview */}
🧹 Remove duplicate comment
-              {/* Subtle type label for narrow cards without preview */}
               {/* Subtle type label for narrow cards without preview */}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{/* Subtle type label for narrow cards without preview */}
{/* Subtle type label for narrow cards without preview */}
{/* Subtle type label for narrow cards without preview */}
🤖 Prompt for AI Agents
In `@src/components/workspace-canvas/WorkspaceCard.tsx` around lines 783 - 784, In
WorkspaceCard (component WorkspaceCard or file WorkspaceCard.tsx) remove the
duplicated JSX comment "Subtle type label for narrow cards without preview" so
only a single instance remains; locate the two adjacent comment nodes around the
narrow-card label render and delete the redundant line to avoid repeated
comments.

Comment on lines +51 to +52
const title = input.title || "Flashcard Deck";
const cards = input.cards || [];
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Redundant validation after Zod schema with .min(1).

The schema already enforces cards.min(1), so the check on line 54 if (cards.length === 0) should never be true if Zod validation passed. However, keeping it as defensive coding is reasonable since line 52 uses input.cards || [] which could theoretically bypass validation.

The || [] fallback on line 52 is concerning—if the schema enforces cards as required with .min(1), why would input.cards ever be undefined? This suggests a mismatch between the schema and the runtime expectation.

🔍 Suggested fix
-            const cards = input.cards || [];
+            const cards = input.cards;

If Zod validation passes, input.cards is guaranteed to be a non-empty array. The fallback masks potential upstream issues.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const title = input.title || "Flashcard Deck";
const cards = input.cards || [];
const title = input.title || "Flashcard Deck";
const cards = input.cards;
🤖 Prompt for AI Agents
In `@src/lib/ai/tools/flashcard-tools.ts` around lines 51 - 52, The code uses a
fallback `input.cards || []` which can mask schema violations and makes the
subsequent `if (cards.length === 0)` check redundant; instead, trust the
Zod-validated `input.cards` by assigning `const cards = input.cards;` (keep
`const title = input.title || "Flashcard Deck";` if you still want a fallback
for title) and remove the unreachable `if (cards.length === 0)` guard, or
alternatively keep defensive validation but remove the `|| []` fallback so that
a missing `cards` will surface as an error; update references to `cards`
accordingly.

Comment on lines +134 to +136
execute: async (input: { deckName: string; cards: Array<{ front: string; back: string }> }) => {
const deckName = input.deckName;
const cardsToAdd = input.cards || [];
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Same redundant fallback pattern in updateFlashcards.

Line 136 const cardsToAdd = input.cards || []; has the same issue—the schema enforces .min(1) so this fallback should never trigger.

🤖 Prompt for AI Agents
In `@src/lib/ai/tools/flashcard-tools.ts` around lines 134 - 136, In
updateFlashcards, remove the redundant fallback that sets const cardsToAdd =
input.cards || []; because the input schema enforces .min(1); instead reference
input.cards directly (e.g., const cardsToAdd = input.cards) or use input.cards
everywhere to avoid masking missing-data bugs; update the function body around
updateFlashcards/execute and the cardsToAdd binding to stop using the || []
pattern and rely on the schema-validated input.cards.

Comment on lines +124 to 151
// Combine text from all successful results
const combinedText = results
.map(r => `**${r.url}**\n\n${r.text}`)
.join('\n\n---\n\n');

// Combine metadata from all results
const allUrlMetadata = results
.flatMap(r => r.metadata.urlMetadata || [])
.filter((m): m is { retrievedUrl: string; urlRetrievalStatus: string } => m !== null);
const allGroundingChunks = results
.flatMap(r => r.metadata.groundingChunks || [])
.filter((c): c is any => c !== null);
const allSources = results
.flatMap(r => r.metadata.sources || [])
.filter((s): s is any => s !== null);

if (failedResults.length > 0) {
logger.warn(`🔗 [URL_TOOL] ${failedResults.length} URL(s) failed to process:`, failedResults.map(r => r.url));
}

return {
text,
text: combinedText,
metadata: {
urlMetadata: Array.isArray(urlMetadata) ? (urlMetadata as Array<{ retrievedUrl: string; urlRetrievalStatus: string }>) : null,
groundingChunks: Array.isArray(groundingChunks) ? (groundingChunks as Array<any>) : null,
sources: Array.isArray(sources) ? (sources as Array<any>) : null,
urlMetadata: allUrlMetadata.length > 0 ? allUrlMetadata : null,
groundingChunks: allGroundingChunks.length > 0 ? allGroundingChunks : null,
sources: allSources.length > 0 ? allSources : null,
},
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Avoid mixing failed-URL error text into the combined analysis.

Right now combinedText includes error strings from failed URLs, which can blur the analysis output and confuse downstream consumers. Consider only aggregating successful results and appending a short failure list separately.

🔧 Suggested adjustment
-                const combinedText = results
+                const combinedText = successfulResults
                     .map(r => `**${r.url}**\n\n${r.text}`)
                     .join('\n\n---\n\n');
+
+                const failedNotice =
+                    failedResults.length > 0
+                        ? `${combinedText ? '\n\n---\n\n' : ''}Failed URLs:\n${failedResults
+                              .map(r => `- ${r.url}`)
+                              .join('\n')}`
+                        : '';
 ...
                 return {
-                    text: combinedText,
+                    text: combinedText + failedNotice,
                     metadata: {
                         urlMetadata: allUrlMetadata.length > 0 ? allUrlMetadata : null,
                         groundingChunks: allGroundingChunks.length > 0 ? allGroundingChunks : null,
                         sources: allSources.length > 0 ? allSources : null,
                     },
                 };
🤖 Prompt for AI Agents
In `@src/lib/ai/tools/process-urls.ts` around lines 124 - 151, combinedText
currently concatenates text from the entire results array, which can include
error strings from failedResults; change the construction of combinedText in
process-urls.ts to only aggregate texts from successful result objects (filter
out failedResults or use a success flag on entries in results), and then append
or return a separate concise failure summary (e.g., list of failedResults.map(r
=> r.url) and short error messages) under a different field so errors are not
mixed into combinedText; update any callers expecting combinedText to remain
pure content-only.

Comment on lines +12 to +25
* For "create" operations, bypasses the queue to allow parallel execution
*/
export async function executeWorkspaceOperation<T>(
workspaceId: string,
operation: () => Promise<T>
operation: () => Promise<T>,
options?: { allowParallel?: boolean }
): Promise<T> {
// If allowParallel is true (for create operations), execute directly without queue
if (options?.allowParallel) {
logger.debug("⚡ [QUEUE] Executing create operation in parallel (bypassing queue):", {
workspaceId: workspaceId.substring(0, 8),
});
return operation();
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Prevent races between parallel creates and subsequent operations.

Because the parallel path doesn’t register the in-flight operation in the queue, later updates can execute before the create finishes. That can lead to “update-before-create” races for the same workspace. Consider enqueueing the parallel promise (without waiting on prior work) so later operations still serialize behind it.

🔧 Suggested adjustment (register parallel op in queue)
-    if (options?.allowParallel) {
-        logger.debug("⚡ [QUEUE] Executing create operation in parallel (bypassing queue):", {
-            workspaceId: workspaceId.substring(0, 8),
-        });
-        return operation();
-    }
+    if (options?.allowParallel) {
+        logger.debug("⚡ [QUEUE] Executing create operation in parallel (bypassing queue):", {
+            workspaceId: workspaceId.substring(0, 8),
+        });
+        const existingQueue = workspaceOperationQueues.get(workspaceId) || Promise.resolve();
+        const opPromise = operation();
+        const parallelQueue = Promise.allSettled([existingQueue, opPromise]).then(() => undefined);
+        workspaceOperationQueues.set(workspaceId, parallelQueue);
+        parallelQueue.finally(() => {
+            if (workspaceOperationQueues.get(workspaceId) === parallelQueue) {
+                workspaceOperationQueues.delete(workspaceId);
+                logger.debug("✅ [QUEUE] Cleaned up queue for workspace:", workspaceId.substring(0, 8));
+            }
+        });
+        return opPromise;
+    }
🤖 Prompt for AI Agents
In `@src/lib/ai/workers/common.ts` around lines 12 - 25, The parallel branch in
executeWorkspaceOperation currently runs operation() without recording it,
allowing later ops to run before the create finishes; change it to start the
promise (call operation()) and immediately register that in the workspace's
queue/registry so subsequent calls serialize behind this in-flight promise
(i.e., enqueue the promise for workspaceId but do not await previous queue
entries before starting it). Use the existing executeWorkspaceOperation,
operation and options.allowParallel symbols to locate the logic and ensure the
enqueuing API you have for non-parallel paths (the workspace queue/map
enqueue/register function) is used to store the returned promise so later
operations see it and wait.

Comment on lines +7 to +8
const DEFAULT_CHAT_MODEL_ID = "gemini-3-flash-preview";

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

Google Gemini 3 Flash preview model production use January 2026

💡 Result:

As of January 28, 2026, Gemini 3 Flash is still a public preview model (model ID: gemini-3-flash-preview, released December 17, 2025) on Vertex AI. That means it is not GA/stable and is covered by Google Cloud Pre‑GA / Preview terms (available “as is” with limited support, and subject to change). [1]

Can you use it in production? Google’s Gemini API documentation says preview models “may be used for production”, but they can have more restrictive rate limits and are deprecated with at least 2 weeks notice—so you should expect possible breaking changes compared with stable/GA models. [2]

Bottom line (Jan 2026): You can deploy gemini-3-flash-preview in a production app if you accept preview risk, but if you need GA/stability/SLA expectations, you should prefer a stable (non-preview) model until Gemini 3 Flash becomes GA. [1][2]

Sources: [1][2]


Consider using a stable model instead of the preview version.

The model gemini-3-flash-preview is a public preview model (released December 17, 2025) and not yet GA. While Google's documentation permits preview models in production, they come with limitations: more restrictive rate limits, possible breaking changes, and limited support, with deprecation possible on 2 weeks' notice. If production stability and predictable rate limits are important for this component, migrate to a stable Gemini model once available, or document the deliberate choice to use a preview version.

🤖 Prompt for AI Agents
In `@src/lib/ai/workers/quiz-worker.ts` around lines 7 - 8, DEFAULT_CHAT_MODEL_ID
is set to the preview model "gemini-3-flash-preview"; replace it with a stable
model identifier or make the model configurable and document the choice. Update
the DEFAULT_CHAT_MODEL_ID constant (and any code that imports it) to either a GA
model (e.g., "gemini-3" or another stable model your org approves) or read from
an environment/config value with a clear fallback to allow switching without
code changes; also add a brief comment near DEFAULT_CHAT_MODEL_ID explaining why
a preview is used if you intentionally keep it and note the risks (rate limits,
breaking changes, deprecation).

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

6 issues found across 42 files

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="src/components/home/HomeContent.tsx">

<violation number="1" location="src/components/home/HomeContent.tsx:72">
P2: Wrapping <Button> with <Link> nests a button inside a link, which is invalid HTML and can cause accessibility/interaction issues. Render the Link as the button instead (use `asChild`).</violation>

<violation number="2" location="src/components/home/HomeContent.tsx:81">
P2: Wrapping <Button> with <Link> nests a button inside a link, which is invalid HTML and can cause accessibility/interaction issues. Render the Link as the button instead (use `asChild`).</violation>
</file>

<file name="src/lib/ai/tools/quiz-tools.ts">

<violation number="1" location="src/lib/ai/tools/quiz-tools.ts:24">
P2: `inputSchema` requires `topic`, but the tool logic allows requests with only `contextContent`. This mismatch will reject valid context-only quiz creation. Make `topic` optional in the input schema to align with the runtime validation.</violation>
</file>

<file name="src/components/workspace-canvas/WorkspaceSidebar.tsx">

<violation number="1" location="src/components/workspace-canvas/WorkspaceSidebar.tsx:277">
P2: Anonymous users on `/home` fall through to the authenticated footer branch, exposing profile/account controls instead of hiding the footer. Add an explicit branch to render nothing (or the intended anonymous UI) when `isHomeRoute` is true for anonymous sessions.</violation>
</file>

<file name="src/lib/ai/workers/common.ts">

<violation number="1" location="src/lib/ai/workers/common.ts:20">
P2: Bypassing the queue for allowParallel operations ignores any existing queued update/delete for the same workspace, which can cause concurrent execution and increase version conflicts that update/delete do not retry. Consider waiting for the current queue when one exists so updates remain serialized with creates.</violation>
</file>

<file name="src/lib/ai/tools/process-urls.ts">

<violation number="1" location="src/lib/ai/tools/process-urls.ts:125">
P2: Error text from failed URLs is being mixed into `combinedText`. The code maps over `results` (all results including failures) rather than `successfulResults`. This can pollute the analysis output with error messages. Consider using `successfulResults` for the combined text and reporting failures separately.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Sign in to save your work and use unlimited AI
</p>
<div className="flex items-center gap-2">
<Link href="/auth/sign-in">
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Wrapping with nests a button inside a link, which is invalid HTML and can cause accessibility/interaction issues. Render the Link as the button instead (use asChild).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/components/home/HomeContent.tsx, line 72:

<comment>Wrapping <Button> with <Link> nests a button inside a link, which is invalid HTML and can cause accessibility/interaction issues. Render the Link as the button instead (use `asChild`).</comment>

<file context>
@@ -1,31 +1,109 @@
+                  Sign in to save your work and use unlimited AI
+                </p>
+                <div className="flex items-center gap-2">
+                  <Link href="/auth/sign-in">
+                    <Button
+                      variant="outline"
</file context>
Fix with Cubic

Sign in
</Button>
</Link>
<Link href="/auth/sign-up">
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Wrapping with nests a button inside a link, which is invalid HTML and can cause accessibility/interaction issues. Render the Link as the button instead (use asChild).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/components/home/HomeContent.tsx, line 81:

<comment>Wrapping <Button> with <Link> nests a button inside a link, which is invalid HTML and can cause accessibility/interaction issues. Render the Link as the button instead (use `asChild`).</comment>

<file context>
@@ -1,31 +1,109 @@
+                      Sign in
+                    </Button>
+                  </Link>
+                  <Link href="/auth/sign-up">
+                    <Button
+                      size="sm"
</file context>
Fix with Cubic

// This handles cases where Gemini sends properties in random order during streaming
inputSchema: zodSchema(
z.object({
topic: z.string().describe("The topic for the quiz - REQUIRED: extract from user's message"),
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: inputSchema requires topic, but the tool logic allows requests with only contextContent. This mismatch will reject valid context-only quiz creation. Make topic optional in the input schema to align with the runtime validation.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/lib/ai/tools/quiz-tools.ts, line 24:

<comment>`inputSchema` requires `topic`, but the tool logic allows requests with only `contextContent`. This mismatch will reject valid context-only quiz creation. Make `topic` optional in the input schema to align with the runtime validation.</comment>

<file context>
@@ -15,24 +16,34 @@ import type { QuizData } from "@/lib/workspace-state/types";
+        // This handles cases where Gemini sends properties in random order during streaming
+        inputSchema: zodSchema(
+            z.object({
+                topic: z.string().describe("The topic for the quiz - REQUIRED: extract from user's message"),
+                contextContent: z.string().optional().describe("Content from selected cards in system context if available"),
+                sourceCardIds: z.array(z.string()).optional().describe("IDs of source cards"),
</file context>
Suggested change
topic: z.string().describe("The topic for the quiz - REQUIRED: extract from user's message"),
topic: z.string().optional().describe("The topic for the quiz - REQUIRED: extract from user's message"),
Fix with Cubic

<SidebarFooter className="py-1.5">
{session?.user?.isAnonymous ? (
// Anonymous user footer - Sign in/Sign up
{session?.user?.isAnonymous && !isHomeRoute ? (
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Anonymous users on /home fall through to the authenticated footer branch, exposing profile/account controls instead of hiding the footer. Add an explicit branch to render nothing (or the intended anonymous UI) when isHomeRoute is true for anonymous sessions.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/components/workspace-canvas/WorkspaceSidebar.tsx, line 277:

<comment>Anonymous users on `/home` fall through to the authenticated footer branch, exposing profile/account controls instead of hiding the footer. Add an explicit branch to render nothing (or the intended anonymous UI) when `isHomeRoute` is true for anonymous sessions.</comment>

<file context>
@@ -281,8 +274,8 @@ function WorkspaceSidebar({
       <SidebarFooter className="py-1.5">
-        {session?.user?.isAnonymous ? (
-          // Anonymous user footer - Sign in/Sign up
+        {session?.user?.isAnonymous && !isHomeRoute ? (
+          // Anonymous user footer - Sign in/Sign up (hidden on home route)
           <div className="flex flex-col gap-2 px-2 py-2 w-full">
</file context>
Fix with Cubic

Comment on lines +20 to +25
if (options?.allowParallel) {
logger.debug("⚡ [QUEUE] Executing create operation in parallel (bypassing queue):", {
workspaceId: workspaceId.substring(0, 8),
});
return operation();
}
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Bypassing the queue for allowParallel operations ignores any existing queued update/delete for the same workspace, which can cause concurrent execution and increase version conflicts that update/delete do not retry. Consider waiting for the current queue when one exists so updates remain serialized with creates.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/lib/ai/workers/common.ts, line 20:

<comment>Bypassing the queue for allowParallel operations ignores any existing queued update/delete for the same workspace, which can cause concurrent execution and increase version conflicts that update/delete do not retry. Consider waiting for the current queue when one exists so updates remain serialized with creates.</comment>

<file context>
@@ -9,11 +9,22 @@ export const workspaceOperationQueues = new Map<string, Promise<any>>();
+    options?: { allowParallel?: boolean }
 ): Promise<T> {
+    // If allowParallel is true (for create operations), execute directly without queue
+    if (options?.allowParallel) {
+        logger.debug("⚡ [QUEUE] Executing create operation in parallel (bypassing queue):", {
+            workspaceId: workspaceId.substring(0, 8),
</file context>
Suggested change
if (options?.allowParallel) {
logger.debug("⚡ [QUEUE] Executing create operation in parallel (bypassing queue):", {
workspaceId: workspaceId.substring(0, 8),
});
return operation();
}
if (options?.allowParallel) {
const existingQueue = workspaceOperationQueues.get(workspaceId);
if (existingQueue) {
await existingQueue;
}
logger.debug("⚡ [QUEUE] Executing create operation in parallel (bypassing queue):", {
workspaceId: workspaceId.substring(0, 8),
});
return operation();
}
Fix with Cubic

const failedResults = results.filter(r => !r.success);

// Combine text from all successful results
const combinedText = results
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Error text from failed URLs is being mixed into combinedText. The code maps over results (all results including failures) rather than successfulResults. This can pollute the analysis output with error messages. Consider using successfulResults for the combined text and reporting failures separately.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/lib/ai/tools/process-urls.ts, line 125:

<comment>Error text from failed URLs is being mixed into `combinedText`. The code maps over `results` (all results including failures) rather than `successfulResults`. This can pollute the analysis output with error messages. Consider using `successfulResults` for the combined text and reporting failures separately.</comment>

<file context>
@@ -60,36 +60,93 @@ export function createProcessUrlsTool() {
+                const failedResults = results.filter(r => !r.success);
+
+                // Combine text from all successful results
+                const combinedText = results
+                    .map(r => `**${r.url}**\n\n${r.text}`)
+                    .join('\n\n---\n\n');
</file context>
Suggested change
const combinedText = results
const combinedText = successfulResults
Fix with Cubic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant