Skip to content

feat: enhance loading and error handling across components#11

Open
shivamhwp wants to merge 19 commits intomainfrom
feature/ux-improvements
Open

feat: enhance loading and error handling across components#11
shivamhwp wants to merge 19 commits intomainfrom
feature/ux-improvements

Conversation

@shivamhwp
Copy link
Member

@shivamhwp shivamhwp commented Aug 8, 2025

  • Introduced LoadingSpinner and ErrorState components for consistent loading and error feedback.
  • Updated DocumentDialog, AppSidebar, and various chat components to utilize new loading and error handling mechanisms.
  • Improved user experience by providing visual feedback during data fetching and error states.
  • Refactored useQuery hooks to include loading and error states for better data management.

Summary by CodeRabbit

  • New Features

    • Consistent loading spinners and friendly error UIs across chats, messages, documents, MCPs, toolbar, projects, panels, auth, and profile pages.
    • Document preview dialog now shows richer previews (images, PDFs, URLs, YouTube), loading/error feedback, and improved Download/Open actions.
    • Tooltip buttons can be disabled; top nav hides sidebar/panel on settings routes.
  • Refactor

    • Unified LoadingSpinner and ErrorState components for consistent status UI.
  • Documentation

    • Removed completed TODO from temp.md.
  • Chores

    • .gitignore adjustments.

- Introduced LoadingSpinner and ErrorState components for consistent loading and error feedback.
- Updated DocumentDialog, AppSidebar, and various chat components to utilize new loading and error handling mechanisms.
- Improved user experience by providing visual feedback during data fetching and error states.
- Refactored useQuery hooks to include loading and error states for better data management.
@coderabbitai
Copy link

coderabbitai bot commented Aug 8, 2025

Walkthrough

Adds reusable LoadingSpinner and ErrorState UI primitives, surfaces loading/error flags from many chat hooks, and wires explicit loading/error UI branches across numerous components, panels, and routes; DocumentDialog preview resolution and document-related flows were also expanded.

Changes

Cohort / File(s) Summary of changes
UI primitives (new)
src/components/ui/error-state.tsx, src/components/ui/loading-spinner.tsx
New ErrorState and LoadingSpinner components with variants, density, accessibility, and label support.
Hooks: loading/error exposure
src/hooks/chats/use-messages.ts, src/hooks/chats/use-stream.ts, src/hooks/chats/use-mcp.ts, src/hooks/chats/use-chats.ts, src/hooks/chats/use-documents.ts
Query hooks now return loading/error flags and error objects (aliases applied). useMessages/useStream add stream-error fields; use-documents removed an explicit enabled flag and added mutation onError handling.
Chat input components
src/components/chat/input/document-list.tsx, src/components/chat/input/toolbar/index.tsx, src/components/chat/input/toolbar/projects-dropdown.tsx
Added explicit loading/error UI branches using LoadingSpinner and ErrorState; DocumentList props destructuring gains default documentIds = [] and inline typing.
Messages rendering
src/components/chat/messages/index.tsx, src/components/chat/messages/user-message.tsx
Messages components now handle loading, general errors, and stream errors with LoadingSpinner/ErrorState branches before empty-state rendering.
Panels: MCP and Projects
src/components/chat/panels/mcp/index.tsx, src/components/chat/panels/projects/chat-list.tsx, src/components/chat/panels/projects/details.tsx, src/components/chat/panels/projects/document-list.tsx, src/components/chat/panels/projects/list.tsx
Added query status handling (loading/error) with spinner/error UIs; guarded list/card rendering; project-docs select-all mutation made fire-and-forget and disables while loading.
Document dialog
src/components/document-dialog.tsx
Added previewError state, per-type preview URL resolution (image/pdf/url/site/youtube/file), generateDownloadUrl mutation use, richer preview rendering (img/object/iframe), safer open/download behavior, and iframe title attributes.
Navigation and routes
src/components/topnav.tsx, src/routes/__root.tsx, src/routes/auth.tsx, src/routes/chat.$chatId.lazy.tsx, src/routes/settings/profile.tsx
Replaced ad-hoc loaders with LoadingSpinner; route-aware sidebar/panel visibility and user-store sync effect; route-level ErrorState UI on query errors.
UI minor
src/components/ui/accordion.tsx, src/components/ui/tooltip-button.tsx
AccordionTrigger prop destructuring reordered (no behavior change). TooltipButton gains optional disabled prop and updated Button import path.
Formatting / small edits
src/components/chat/messages/utils-bar/index.ts, src/components/chat/messages/utils-bar/user-utils-bar.tsx, other small files
Minor formatting, removal of non-null assertions, early-return refactor for editing UI, and small guard/disable improvements.
Misc / config / docs
temp.md, .gitignore
Removed a TODO line in temp.md; adjusted .gitignore entries for stats.html / bun.lock.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant U as User
  participant C as UI Component
  participant H as Data Hook
  participant Q as Convex Query

  U->>C: open view / navigate
  C->>H: call hook()
  H->>Q: fetch request
  Q-->>H: { data?, isLoading, isError, error }
  H-->>C: { data, isLoading, isError, error }

  alt isLoading
    C-->>U: render LoadingSpinner (compact/centered)
  else isError
    C-->>U: render ErrorState(title, error)
  else data empty
    C-->>U: render empty placeholder
  else data present
    C-->>U: render main content
  end
Loading
sequenceDiagram
  autonumber
  participant U as User
  participant D as DocumentDialog
  participant Q as getDocument
  participant S as Storage/Server

  U->>D: open dialog (id)
  D->>Q: fetch document
  Q-->>D: { doc, isLoading, isError, error }

  alt isLoading
    D-->>U: Dialog + LoadingSpinner
  else isError
    D-->>U: Dialog + ErrorState("Couldn't load document")
  else success
    D->>D: resolve preview URL by type
    alt needs download URL
      D->>S: generateDownloadUrl(key)
      S-->>D: url
    else external/url/site
      D-->>D: validate/use key as URL
    else youtube
      D-->>D: build embed URL
    end
    alt preview available
      D-->>U: render viewer (img/object/iframe)
    else preview error
      D-->>U: render ErrorState("Unable to preview")
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

imp and noice

Suggested reviewers

  • mantrakp04

Poem

I thump my paws at spinners bright,
Error carrots blink in gentle light 🥕✨
Previews open, panels hum anew,
Streams settle, actions follow through.
A rabbit hops — the UI feels right.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/ux-improvements

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

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: 10

🔭 Outside diff range comments (6)
src/routes/chat.$chatId.lazy.tsx (1)

47-56: Type-safety issue: chatId union vs cast; also gate the query with enabled

  • The component compares chatId !== "new" but chatId was cast to Id<"chats"> earlier. This loses the union type and weakens type safety.
  • Align with other hooks by adding enabled: chatId !== "new" for clarity and to avoid unnecessary fetches.
  const {
    data: queryChat,
    isError: isChatError,
    error: chatError,
  } = useQuery({
-    ...convexQuery(
+    ...convexQuery(
       api.chats.queries.get,
       chatId !== "new" ? { chatId: chatId as Id<"chats"> } : "skip"
     ),
+    enabled: chatId !== "new",
  });

Additionally, update the chatId typing (outside the selected range) to preserve the union and satisfy the project’s strict Id typing guideline:

// At Line 38 (outside selected range)
const rawChatId = params.chatId as string;
const chatId: Id<"chats"> | "new" =
  rawChatId === "new" ? "new" : (rawChatId as Id<"chats">);
src/components/chat/panels/projects/document-list.tsx (1)

47-55: Early return hides loading & error states

While isLoadingProjectDocs is true (or the query errored), projectDocuments is undefined, so this block executes and the user sees “No documents added yet” instead of a spinner or error.

- if (!projectDocuments || projectDocuments.projectDocuments.length === 0) {
+ if (!isLoadingProjectDocs &&
+     !isProjectDocumentsError &&
+     (!projectDocuments || projectDocuments.projectDocuments.length === 0)) {

Move the loading / error checks above this guard or incorporate them into the condition so the correct UI is shown.

src/components/chat/input/document-list.tsx (1)

105-112: Duplicate / early return null hides spinner & error UI

The first null-return executes while data is still loading ( documents is undefined ), so the new loading/error components never render.
Remove the premature check and keep a single “no documents” guard after the loading/error blocks.

-  if (!documents?.length) return null;
...
-  if (!documents?.length) return null;

Place one “no documents” check after Line 134 instead.

src/components/chat/messages/index.tsx (1)

20-41: Missing deps ⇒ stale UI on error changes

useMemo omits isError, error, isStreamError, and streamError from its dependency list, so entering/leaving error states won’t trigger a re-render.

- }, [isLoading, isEmpty, streamStatus, chatId, userLoadable]);
+ }, [
+   isLoading,
+   isEmpty,
+   isError,
+   error,
+   isStreamError,
+   streamError,
+   streamStatus,
+   chatId,
+   userLoadable,
+ ]);

Also applies to: 113-116

src/components/app-sidebar/index.tsx (1)

232-237: “No matching chats” never shown

The guard !isSearching makes this branch skip when searching, so users get no feedback for an empty search result set.

- {displayHistoryChats.length === 0 && !isSearching && (
+ {displayHistoryChats.length === 0 && (

Then keep the conditional text selection (isSearching ? ...) as-is.

src/components/chat/messages/user-message.tsx (1)

283-292: Show utils bar on focus for keyboard users, not only on hover

Add a focus-visible path to avoid hiding controls from non-pointer users.

-        <div className="opacity-0 flex gap-2 group-hover/message:opacity-100 transition-opacity">
+        <div className="opacity-0 flex gap-2 group-hover/message:opacity-100 group-focus-within/message:opacity-100 transition-opacity">
🧹 Nitpick comments (28)
src/hooks/chats/use-chats.ts (1)

149-150: isSearching semantics: clearer and less brittle

Using debouncedQuery.trim().length > 0 is a cleaner signal than comparing two strings. Good change.

If you want the UI to reflect “user is typing” immediately (before debounce triggers), consider also deriving a non-debounced flag for UX (optional).

src/components/ui/loading-spinner.tsx (1)

4-8: Make layout styling extensible without breaking current uses

Currently className applies to the icon. If callers need to style the wrapper, consider a non-breaking addition: containerClassName.

 interface LoadingSpinnerProps {
   sizeClassName?: string;
   className?: string;
   label?: string;
+  containerClassName?: string;
 }
 
 export function LoadingSpinner({
   sizeClassName = "h-5 w-5",
   className,
   label,
+  containerClassName,
 }: LoadingSpinnerProps) {
   return (
-    <div className="flex items-center justify-center gap-2">
+    <div className={cn("flex items-center justify-center gap-2", containerClassName)}>
       <Loader2
         aria-label="loading"
         className={cn(
           "animate-spin text-muted-foreground",
           sizeClassName,
           className
         )}
       />
-      <p className="text-muted-foreground">{label}</p>
+      {label ? <p className="text-muted-foreground">{label}</p> : null}
     </div>
   );
 }

Also applies to: 10-14

src/hooks/chats/use-documents.ts (1)

12-18: Avoid duplicated skip conditions (skip arg vs enabled)

You’re using both chatId !== "new" ? { chatId } : "skip" and enabled: chatId !== "new". One is sufficient. Using both is harmless but redundant and can introduce divergence if conditions drift.

Pick one approach (recommended: keep the skip sentinel only for consistency with other hooks in this PR) and drop the other.

src/routes/auth.tsx (1)

4-4: Consistent loading UI via LoadingSpinner

Good replacement; this standardizes the auth loading state with the rest of the app.

Optionally add a label for clarity/accessibility:

-        <LoadingSpinner sizeClassName="w-8 h-8" />
+        <LoadingSpinner sizeClassName="w-8 h-8" label="Checking authentication…" />

Also applies to: 20-20

src/components/chat/panels/projects/chat-list.tsx (2)

18-25: React Query state destructuring looks good

State exposure is clear. Note: React Query’s error is typed as unknown; ensure ErrorState can accept unknown or cast appropriately where passed.


27-33: Loading state handling is clear and accessible

Spinner + label is appropriate. Consider adding aria-busy on the container for a11y.

-  <div className="flex items-center justify-center py-10">
+  <div className="flex items-center justify-center py-10" aria-busy="true" aria-live="polite">
src/routes/chat.$chatId.lazy.tsx (1)

62-78: Good explicit error UI; consider adding loading handling

The error state is well-structured and consistent. Optionally add a loading spinner for the chat fetch to avoid brief content flashes while resolving the chat.

src/components/ui/accordion.tsx (1)

153-155: Avoid running rotation animation when icon is hidden

Currently the wrapper still animates even if the icon isn’t rendered. Minor optimization and avoids layout quirks by conditionally rendering the wrapper too.

-        <motion.div
-          animate={{ rotate: isOpen ? 0 : 180 }}
-          transition={{ duration: 0.25 }}
-          className="items-center flex justify-center "
-        >
-          {showIcon && (
-            <ChevronDownIcon className="text-muted-foreground pointer-events-none size-4" />
-          )}
-        </motion.div>
+        {showIcon && (
+          <motion.div
+            animate={{ rotate: isOpen ? 0 : 180 }}
+            transition={{ duration: 0.25 }}
+            className="items-center flex justify-center "
+          >
+            <ChevronDownIcon className="text-muted-foreground pointer-events-none size-4" />
+          </motion.div>
+        )}
src/components/chat/panels/projects/list.tsx (1)

44-53: Loading state UX is solid; add aria-busy for a11y

Minor accessibility enhancement.

-  <div className="flex flex-1 items-center justify-center">
+  <div className="flex flex-1 items-center justify-center" aria-busy="true" aria-live="polite">
src/routes/settings/profile.tsx (1)

40-48: Pass a non-undefined error to ErrorState

ErrorState receives error={userError}, but when isErrorUser is true and userError is still undefined, the component will not get any details.
Consider using error: userError ?? new Error("Unknown error") (or omit the prop) to avoid passing undefined.

src/components/chat/input/toolbar/index.tsx (1)

324-329: Include the underlying error for debuggability

ErrorState can show richer context if the error prop is supplied, mirroring other usages in the codebase.

- <ErrorState
+ <ErrorState
     density="compact"
     title="Failed to load project"
+    error={/* isProjectError object here */}
     className="h-9"
   />
src/components/topnav.tsx (1)

101-105: Spinner markup produces duplicate text

Inside a dropdown-sized area the <LoadingSpinner> already renders its own label; the sibling <span> repeats it.
Consider removing the extra <span> to avoid redundant content.

src/components/chat/input/toolbar/projects-dropdown.tsx (1)

59-112: Minor: inconsistency in loading UI

The dropdown renders both LoadingSpinner and a text node, but elsewhere in the codebase the spinner component already contains the label. Align with the rest of the app for consistency:

-              <DropdownMenuItem>
-                <LoadingSpinner
-                  className="h-4 w-4"
-                  label="Loading projects..."
-                />
-                <span className="text-muted-foreground">
-                  Loading projects...
-                </span>
-              </DropdownMenuItem>
+              <DropdownMenuItem>
+                <LoadingSpinner className="h-4 w-4" label="Loading projects..." />
+              </DropdownMenuItem>
src/components/document-dialog.tsx (3)

24-35: Redundant enabled + "skip" usage

enabled: !!documentDialogOpen already prevents the query from firing when the dialog is closed.
Returning "skip" in the query key is therefore unnecessary noise.


55-65: "file" handled twice

case "file" in this switch already fetches a download URL; the subsequent default‐branch check for document.type === "file" will never be reached.
Removing the duplicate simplifies control-flow and avoids an extra needless request if the tag ever mis-matches.

Also applies to: 75-83


180-189: Unreachable in-dialog error state

The component returns early for isDocumentError || documentError (lines 114-132), so this inner error block is never rendered.
Delete to avoid dead code and accidental divergence in future edits.

src/components/ui/error-state.tsx (1)

64-71: Early null-return can hide explicit titles

If the caller passes a custom title but neither error nor description, the early return null prevents the component from rendering despite valid props.

Consider dropping the guard or checking title separately.

src/components/chat/messages/user-message.tsx (5)

32-41: Good adoption of explicit loading/error states in DocumentButton; minor cleanups

Nicely aligns with the PR goal. Consider two small tweaks:

  • Rely on isDocumentError (it implies error exists) instead of isDocumentError || documentError.
  • Use consistent title-case microcopy.
-  if (isDocumentError || documentError) {
+  if (isDocumentError) {
     return (
       <div className="py-2 max-w-xs">
         <ErrorState
           density="compact"
-          title="error loading attached documents"
+          title="Error loading attached documents"
           error={documentError}
           showDescription={false}
         />
       </div>
     );
   }

Also applies to: 43-52, 54-65


43-52: Spinner copy/style consistency

Consider tightening copy to match other places and singular/plural cases.

-        <LoadingSpinner
-          sizeClassName="h-4 w-4"
-          label="Fetching attached documents"
-        />
+        <LoadingSpinner sizeClassName="h-4 w-4" label="Loading document…" />

67-74: Guard against missing doc name to avoid empty button

If the query returns no data or an unnamed doc, provide a safe fallback label.

-      {documentData?.name}
+      {documentData?.name ?? "View document"}

85-92: EditingDocumentList loading/error handling looks solid

The loading spinner and ErrorState simplify UX and match the PR’s consistency goal. Consider matching microcopy tone and adding a label for the loading state.

-      <div className="px-2 py-1 text-xs text-muted-foreground flex items-center gap-2">
-        <LoadingSpinner sizeClassName="h-3 w-3" /> Loading…
+      <div className="px-2 py-1 text-xs text-muted-foreground flex items-center gap-2">
+        <LoadingSpinner sizeClassName="h-3 w-3" label="Loading documents…" />
       </div>

Also applies to: 102-116


229-251: Optional: avoid N queries by batching document fetches

Rendering one useQuery per DocumentButton can fan out requests for messages with many attachments. Consider prefetching via a single getMultiple for all file_ids in content and pass names down (or render directly) to reduce request overhead and spinner churn.

Example approach:

  • Extract all file_ids from content in a useMemo.
  • useQuery({...convexQuery(api.documents.queries.getMultiple, { documentIds })}).
  • Build a Map<Id<"documents">, Document> and render names inline, avoiding per-item queries.

Also applies to: 242-246

temp.md (4)

5-8: Fix bare URL and clarify phrasing

Format the URL and tighten the copy for readability.

-- update these
-  tool-streaming, because of vibz mcp as it one shots the generation. so we need to live stream to the user all the changes.
-  https://github.com/0bs-chat/zerobs/tree/feat/message-queue : the message queue function.
+- Update tool streaming. Vibz MCP currently one-shots generation, so we need to live-stream updates to the user.
+- <https://github.com/0bs-chat/zerobs/tree/feat/message-queue> — the message queue function.

9-11: Consistency and capitalization (OAuth, MCP, UI); improve readability

-- vibe coding (better auth -> convex cloud migration -> streaming tool calls -> convex oauth integration -> revamp mcp templates to pass along the env vars)
-  custom ui for vibz mcp. (like artifacts, we will replace the panel content with the ui for vibz)(preview, dashboard (convex dashboard), code (vs code))
+- Vibe coding (Better Auth → Convex Cloud migration → streaming tool calls → Convex OAuth integration → revamp MCP templates to pass along env vars).
+  Custom UI for Vibz MCP (like Artifacts): replace the panel content with the Vibz UI (Preview, Dashboard (Convex dashboard), Code (VS Code)).

12-13: Grammar and style nits (capitalize proper nouns; UX; “etc.” with period)

-- migrate to better auth (when i get the green light from mantra after better-auth integrates.)
+- Migrate to Better Auth (when I get the green light from Mantra after Better Auth integrates).
 ...
-- improve ux overall with loading states and whatnot.
+- Improve UX overall with loading states and related polish.
-- google integration (the code is already there just need to setup oauth)
+- Google integration (the code is already there; just need to set up OAuth).
-- business related mcp with ability to autofill connection info (like auto fetching api key/oauth key for the headers in mcp using oauth, etc to reduce friction)
+- Business‑related MCP with ability to autofill connection info (e.g., auto‑fetching API key/OAuth key for MCP request headers using OAuth, etc.) to reduce friction.

Also applies to: 15-15, 21-23


25-27: Clarify actionable items

-- need message queue system
-- immediate send, wait for file to be processed check on the frontend instead of backend
+- Implement message queue system.
+- Immediate send: perform “wait for file to be processed” check on the frontend instead of the backend.
src/components/chat/panels/projects/details.tsx (2)

52-61: Spinner sizing and a11y nit

Prefer sizeClassName for sizing (avoids conflicting Tailwind classes), and add status/a11y attributes to the wrapper.

-  if (isLoadingProject) {
-    return (
-      <div className="flex flex-1 items-center justify-center py-8">
-        <LoadingSpinner
-          className="h-6 w-6"
-          label="Loading project details..."
-        />
-      </div>
-    );
-  }
+  if (isLoadingProject) {
+    return (
+      <div
+        className="flex flex-1 items-center justify-center py-8"
+        role="status"
+        aria-busy="true"
+        aria-live="polite"
+      >
+        <LoadingSpinner
+          sizeClassName="h-6 w-6"
+          label="Loading project details..."
+        />
+      </div>
+    );
+  }

63-73: Tighten error condition and unify layout with loading

  • Rely on isProjectError (React Query’s source of truth) and avoid the redundant || projectError.
  • Match the loading view’s centered layout. The current className=" " looks accidental.
-  if (isProjectError || projectError) {
+  if (isProjectError) {
     return (
-      <div className=" ">
+      <div className="flex flex-1 items-center justify-center py-8">
         <ErrorState
           className="h-full py-2.5"
           title="Error loading project details"
           error={projectError}
         />
       </div>
     );
   }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5d2b9ba and 795f287.

📒 Files selected for processing (27)
  • src/components/app-sidebar/index.tsx (8 hunks)
  • src/components/chat/input/document-list.tsx (3 hunks)
  • src/components/chat/input/toolbar/index.tsx (3 hunks)
  • src/components/chat/input/toolbar/projects-dropdown.tsx (3 hunks)
  • src/components/chat/messages/index.tsx (3 hunks)
  • src/components/chat/messages/user-message.tsx (7 hunks)
  • src/components/chat/panels/mcp/index.tsx (2 hunks)
  • src/components/chat/panels/projects/chat-list.tsx (2 hunks)
  • src/components/chat/panels/projects/details.tsx (3 hunks)
  • src/components/chat/panels/projects/document-list.tsx (5 hunks)
  • src/components/chat/panels/projects/list.tsx (3 hunks)
  • src/components/document-dialog.tsx (5 hunks)
  • src/components/topnav.tsx (4 hunks)
  • src/components/ui/accordion.tsx (2 hunks)
  • src/components/ui/error-state.tsx (1 hunks)
  • src/components/ui/loading-spinner.tsx (1 hunks)
  • src/hooks/chats/use-chats.ts (2 hunks)
  • src/hooks/chats/use-documents.ts (1 hunks)
  • src/hooks/chats/use-mcp.ts (2 hunks)
  • src/hooks/chats/use-messages.ts (2 hunks)
  • src/hooks/chats/use-stream.ts (3 hunks)
  • src/routes/__root.tsx (4 hunks)
  • src/routes/auth.tsx (2 hunks)
  • src/routes/chat.$chatId.lazy.tsx (3 hunks)
  • src/routes/settings/apiKeys.tsx (3 hunks)
  • src/routes/settings/profile.tsx (2 hunks)
  • temp.md (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.ts

📄 CodeRabbit Inference Engine (.cursor/rules/convex_rules.mdc)

**/*.ts: You can use the helper TypeScript type Id imported from './_generated/dataModel' to get the type of the id for a given table.
If you need to define a Record, provide the correct key and value types (e.g., Record<Id<'users'>, string>).
Be strict with types, particularly around ids of documents. Use Id<'table'> rather than string for document ids.
Always use as const for string literals in discriminated union types.
When using the Array type, always define arrays as const array: Array = [...];
When using the Record type, always define records as const record: Record<KeyType, ValueType> = {...};

Files:

  • src/hooks/chats/use-documents.ts
  • src/hooks/chats/use-mcp.ts
  • src/hooks/chats/use-stream.ts
  • src/hooks/chats/use-messages.ts
  • src/hooks/chats/use-chats.ts
🧠 Learnings (17)
📚 Learning: 2025-08-01T07:34:38.459Z
Learnt from: CR
PR: 0bs-chat/zerobs#0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-08-01T07:34:38.459Z
Learning: Applies to convex/**/*.{ts,js} : Try to use as few calls from actions to queries and mutations as possible to avoid race conditions.

Applied to files:

  • src/hooks/chats/use-documents.ts
  • src/components/chat/panels/projects/list.tsx
  • src/hooks/chats/use-mcp.ts
  • src/routes/chat.$chatId.lazy.tsx
  • src/routes/settings/apiKeys.tsx
  • src/hooks/chats/use-messages.ts
  • src/components/chat/panels/projects/document-list.tsx
  • src/components/chat/input/document-list.tsx
  • src/components/chat/panels/projects/details.tsx
  • src/components/chat/input/toolbar/projects-dropdown.tsx
  • src/components/document-dialog.tsx
  • temp.md
📚 Learning: 2025-08-01T07:34:38.459Z
Learnt from: CR
PR: 0bs-chat/zerobs#0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-08-01T07:34:38.459Z
Learning: Applies to convex/**/*.{ts,js} : Use ctx.db.replace to fully replace an existing document. This method will throw an error if the document does not exist.

Applied to files:

  • src/hooks/chats/use-documents.ts
📚 Learning: 2025-08-01T07:34:38.459Z
Learnt from: CR
PR: 0bs-chat/zerobs#0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-08-01T07:34:38.459Z
Learning: Applies to convex/**/*.{ts,js} : Convex queries do NOT support .delete(). Instead, .collect() the results, iterate, and call ctx.db.delete(row._id) on each result.

Applied to files:

  • src/hooks/chats/use-documents.ts
  • src/components/chat/panels/projects/details.tsx
  • src/components/chat/messages/user-message.tsx
📚 Learning: 2025-08-01T07:34:38.459Z
Learnt from: CR
PR: 0bs-chat/zerobs#0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-08-01T07:34:38.459Z
Learning: Applies to convex/**/*.{ts,js} : Use .unique() to get a single document from a query. This method will throw an error if there are multiple documents.

Applied to files:

  • src/hooks/chats/use-documents.ts
  • src/components/chat/input/document-list.tsx
  • src/components/chat/messages/user-message.tsx
📚 Learning: 2025-08-01T07:34:38.459Z
Learnt from: CR
PR: 0bs-chat/zerobs#0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-08-01T07:34:38.459Z
Learning: Applies to convex/**/* : Convex uses file-based routing; organize files with public query, mutation, or action functions within the convex/ directory.

Applied to files:

  • src/routes/settings/profile.tsx
  • src/routes/auth.tsx
  • src/components/chat/input/document-list.tsx
  • src/components/chat/panels/projects/details.tsx
  • src/components/chat/input/toolbar/projects-dropdown.tsx
  • src/components/document-dialog.tsx
  • temp.md
📚 Learning: 2025-08-01T07:34:38.459Z
Learnt from: CR
PR: 0bs-chat/zerobs#0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-08-01T07:34:38.459Z
Learning: Applies to convex/**/*.{ts,js} : Use ctx.runQuery to call a query, ctx.runMutation to call a mutation, and ctx.runAction to call an action. Only call an action from another action if you need to cross runtimes (e.g., from V8 to Node). Otherwise, pull out shared code into a helper async function.

Applied to files:

  • src/components/chat/panels/projects/list.tsx
  • src/hooks/chats/use-mcp.ts
  • src/routes/settings/apiKeys.tsx
  • src/components/chat/panels/projects/document-list.tsx
  • src/components/chat/input/document-list.tsx
  • src/components/chat/panels/projects/details.tsx
  • src/components/chat/input/toolbar/projects-dropdown.tsx
  • src/components/document-dialog.tsx
📚 Learning: 2025-08-01T07:34:38.459Z
Learnt from: CR
PR: 0bs-chat/zerobs#0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-08-01T07:34:38.459Z
Learning: Applies to convex/**/*.{ts,js} : Use internalQuery, internalMutation, and internalAction to register internal functions (private, only callable by other Convex functions). Use query, mutation, and action to register public functions (exposed to the public Internet). Do NOT use query, mutation, or action for sensitive internal functions.

Applied to files:

  • src/components/chat/panels/projects/list.tsx
  • src/routes/settings/apiKeys.tsx
  • src/components/chat/panels/projects/document-list.tsx
  • src/components/chat/input/document-list.tsx
  • src/components/chat/panels/projects/details.tsx
  • src/components/chat/input/toolbar/projects-dropdown.tsx
  • src/components/document-dialog.tsx
📚 Learning: 2025-08-01T07:34:38.459Z
Learnt from: CR
PR: 0bs-chat/zerobs#0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-08-01T07:34:38.459Z
Learning: Applies to convex/**/*.{ts,js} : ALWAYS use the new function syntax for Convex functions (using query/mutation/internalQuery/internalMutation/action/internalAction with args, returns, and handler).

Applied to files:

  • src/hooks/chats/use-mcp.ts
  • src/components/chat/panels/projects/document-list.tsx
  • src/components/chat/input/document-list.tsx
  • src/components/chat/panels/projects/details.tsx
  • src/components/chat/input/toolbar/projects-dropdown.tsx
  • src/components/document-dialog.tsx
📚 Learning: 2025-08-01T07:34:38.459Z
Learnt from: CR
PR: 0bs-chat/zerobs#0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-08-01T07:34:38.459Z
Learning: Applies to convex/**/*.{ts,js} : All ctx.runQuery, ctx.runMutation, and ctx.runAction calls take a FunctionReference. Do NOT pass the callee function directly.

Applied to files:

  • src/components/chat/panels/projects/document-list.tsx
  • src/components/chat/panels/projects/details.tsx
  • src/components/document-dialog.tsx
📚 Learning: 2025-08-01T07:34:38.459Z
Learnt from: CR
PR: 0bs-chat/zerobs#0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-08-01T07:34:38.459Z
Learning: Applies to convex/**/*.{ts,js} : Use the api object from convex/_generated/api.ts to call public functions and the internal object to call internal functions.

Applied to files:

  • src/components/chat/input/document-list.tsx
  • src/components/chat/panels/projects/details.tsx
  • src/components/chat/input/toolbar/projects-dropdown.tsx
📚 Learning: 2025-08-01T07:34:38.459Z
Learnt from: CR
PR: 0bs-chat/zerobs#0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-08-01T07:34:38.459Z
Learning: Applies to convex/schema.ts : Always import schema definition functions from convex/server in convex/schema.ts.

Applied to files:

  • src/components/chat/input/document-list.tsx
📚 Learning: 2025-08-01T07:34:38.459Z
Learnt from: CR
PR: 0bs-chat/zerobs#0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-08-01T07:34:38.459Z
Learning: Applies to convex/**/*.{ts,js} : Never use ctx.db inside of an action. Actions don't have access to the database.

Applied to files:

  • src/components/chat/input/document-list.tsx
📚 Learning: 2025-08-01T07:34:38.459Z
Learnt from: CR
PR: 0bs-chat/zerobs#0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-08-01T07:34:38.459Z
Learning: Applies to convex/**/*.{ts,js} : Always add "use node"; to the top of files containing actions that use Node.js built-in modules.

Applied to files:

  • src/components/chat/input/document-list.tsx
📚 Learning: 2025-08-01T07:34:38.459Z
Learnt from: CR
PR: 0bs-chat/zerobs#0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-08-01T07:34:38.459Z
Learning: Applies to convex/**/*.{ts,js} : Convex storage stores items as Blob objects. You must convert all items to/from a Blob when using Convex storage.

Applied to files:

  • src/components/chat/input/document-list.tsx
📚 Learning: 2025-08-01T07:34:38.459Z
Learnt from: CR
PR: 0bs-chat/zerobs#0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-08-01T07:34:38.459Z
Learning: Applies to convex/**/*.{ts,js} : When using ctx.runQuery, ctx.runMutation, or ctx.runAction to call a function in the same file, specify a type annotation on the return value to work around TypeScript circularity limitations.

Applied to files:

  • src/components/chat/panels/projects/details.tsx
  • src/components/chat/input/toolbar/projects-dropdown.tsx
  • src/components/document-dialog.tsx
📚 Learning: 2025-08-01T07:34:38.459Z
Learnt from: CR
PR: 0bs-chat/zerobs#0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-08-01T07:34:38.459Z
Learning: Applies to convex/**/*.{ts,js} : ALWAYS include argument and return validators for all Convex functions (query, internalQuery, mutation, internalMutation, action, internalAction). If a function doesn't return anything, include returns: v.null() as its output validator.

Applied to files:

  • src/components/chat/panels/projects/details.tsx
📚 Learning: 2025-08-01T07:34:38.459Z
Learnt from: CR
PR: 0bs-chat/zerobs#0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-08-01T07:34:38.459Z
Learning: Applies to convex/**/*.{ts,js} : When using async iteration, don't use .collect() or .take(n) on the result of a query. Use for await (const row of query) instead.

Applied to files:

  • src/components/chat/messages/user-message.tsx
🧬 Code Graph Analysis (13)
src/components/chat/panels/projects/chat-list.tsx (2)
src/components/ui/loading-spinner.tsx (1)
  • LoadingSpinner (10-28)
src/components/ui/error-state.tsx (1)
  • ErrorState (115-115)
src/routes/settings/profile.tsx (2)
src/components/ui/loading-spinner.tsx (1)
  • LoadingSpinner (10-28)
src/components/ui/error-state.tsx (1)
  • ErrorState (115-115)
src/components/chat/panels/projects/list.tsx (2)
src/components/ui/loading-spinner.tsx (1)
  • LoadingSpinner (10-28)
src/components/ui/error-state.tsx (1)
  • ErrorState (115-115)
src/routes/chat.$chatId.lazy.tsx (1)
src/components/ui/error-state.tsx (1)
  • ErrorState (115-115)
src/routes/auth.tsx (1)
src/components/ui/loading-spinner.tsx (1)
  • LoadingSpinner (10-28)
src/routes/settings/apiKeys.tsx (2)
src/components/ui/loading-spinner.tsx (1)
  • LoadingSpinner (10-28)
src/components/ui/error-state.tsx (1)
  • ErrorState (115-115)
src/components/chat/input/document-list.tsx (2)
src/store/chatStore.ts (1)
  • documentDialogOpenAtom (43-45)
src/components/ui/loading-spinner.tsx (1)
  • LoadingSpinner (10-28)
src/components/chat/input/toolbar/index.tsx (3)
src/components/ui/button.tsx (1)
  • Button (59-59)
src/components/ui/loading-spinner.tsx (1)
  • LoadingSpinner (10-28)
src/components/ui/error-state.tsx (1)
  • ErrorState (115-115)
src/components/chat/panels/mcp/index.tsx (2)
src/hooks/chats/use-mcp.ts (1)
  • useMCPs (42-143)
src/components/ui/loading-spinner.tsx (1)
  • LoadingSpinner (10-28)
src/components/chat/panels/projects/details.tsx (2)
src/components/ui/loading-spinner.tsx (1)
  • LoadingSpinner (10-28)
src/components/ui/error-state.tsx (1)
  • ErrorState (115-115)
src/components/chat/input/toolbar/projects-dropdown.tsx (3)
src/components/ui/error-state.tsx (1)
  • ErrorState (115-115)
src/components/ui/dropdown-menu.tsx (2)
  • DropdownMenuItem (248-248)
  • DropdownMenuSeparator (252-252)
src/components/ui/loading-spinner.tsx (1)
  • LoadingSpinner (10-28)
src/components/ui/loading-spinner.tsx (1)
src/lib/utils.ts (1)
  • cn (4-6)
src/components/ui/error-state.tsx (1)
src/lib/utils.ts (1)
  • cn (4-6)
🪛 Biome (2.1.2)
src/components/topnav.tsx

[error] 58-58: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

Hooks should not be called after an early return.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)

🪛 LanguageTool
temp.md

[style] ~23-~23: In American English, abbreviations like “etc.” require a period.
Context: ...key for the headers in mcp using oauth, etc to reduce friction) - need message que...

(ETC_PERIOD)

🪛 markdownlint-cli2 (0.17.2)
temp.md

7-7: Bare URL used

(MD034, no-bare-urls)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (13)
src/hooks/chats/use-mcp.ts (1)

43-48: Verify callsites for expanded return shape and data consistency

We’ve confirmed two usages of useMCPs() in the repo:

  • src/components/chat/panels/mcp/mcp-dialog.tsx
    Only handleCreate and validateMCP are destructured here—no impact from the added isLoading/isError/error fields.

  • src/components/chat/panels/mcp/index.tsx
    Destructures isError and error, then calls getAllMCPs() for the data.

Destructuring extra fields is non-breaking, but please verify that:

  • getAllMCPs() always returns an array (not null) to prevent rendering issues when there’s no data.
  • Consumers handle the new isLoading state where applicable.
src/hooks/chats/use-chats.ts (1)

129-134: Solid: expose search loading/error state directly from the query

This matches the new loading/error handling pattern and is consistent with other hooks. No functional issues spotted.

src/components/chat/panels/projects/chat-list.tsx (1)

8-9: LGTM: consistent UI imports for loading/error

Imports align with shared UI patterns introduced in this PR.

src/routes/chat.$chatId.lazy.tsx (1)

27-27: LGTM: ErrorState import

Matches the shared error UI introduced in this PR.

src/components/ui/accordion.tsx (2)

112-113: Prop addition is sensible and backwards compatible

Optional showIcon increases flexibility without breaking callers.


116-116: Default showIcon = true is appropriate

Sane default preserves existing visuals.

src/hooks/chats/use-messages.ts (1)

25-36: LGTM: explicit loading/error exposure from query

Clear and consistent with consumers like ChatMessages. Good use of enabled with the "new" sentinel.

src/components/chat/panels/projects/list.tsx (2)

5-6: LGTM: shared UI imports

Consistent with the cross-app loading/error pattern.


21-26: React Query state destructuring is clear

Good naming and exposure for downstream conditional UI.

src/routes/settings/apiKeys.tsx (1)

310-329: Loading / error handling looks solid

Nice, the new spinner and ErrorState branches make the UX clearer and the conditions (isLoading, isError || error) are correct.

src/hooks/chats/use-stream.ts (1)

182-184: Good addition

Exposing aggregated isError and error flags from the hook simplifies consumers.

src/components/chat/panels/projects/details.tsx (2)

6-7: New loading/error UI imports — LGTM

Imports are correct and align with the PR’s goal of consistent async UX.


24-29: Good: explicit query state exposure

Clear naming and explicit isLoading, isError, and error improve readability and UX handling.

Comment on lines 133 to 158
<Badge
key={doc._id}
variant="secondary"
className="flex items-center gap-1.5 pr-1 cursor-pointer"
variant="default"
className="group/document flex gap-1.5 py-1 cursor-pointer bg-accent/65 hover:bg-accent/100 hover:text-accent-foreground text-accent-foreground/90 rounded-md transition duration-300 items-center justify-center"
onClick={() => handlePreview(doc._id)}
>
<Icon className={`${IconClassName} h-3 w-3`} />
<span className="max-w-32 truncate">{doc.name}</span>
<Button
variant="ghost"
size="icon"
className="h-4 w-4 p-0.5 hover:bg-muted-foreground/20"
onClick={(e) => {
e.stopPropagation();
onRemove(doc._id);
}}
>
<XIcon className="w-3 h-3" />
</Button>
<div className="relative h-4 w-4">
<Icon
className={`${IconClassName} h-4 w-4 group-hover/document:opacity-0 transition duration-300`}
/>
<Button
variant="ghost"
size="icon"
className="absolute inset-0 h-4 w-4 opacity-0 group-hover/document:opacity-100 hover:bg-muted-foreground/20 cursor-pointer transition duration-300"
onClick={(e) => {
e.stopPropagation();
onRemove(doc._id);
}}
>
<XIcon className="w-3 h-3" />
</Button>
</div>
<span className="max-w-32 truncate text-xs cursor-pointer">
{doc.name}
</span>
</Badge>
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Make the document chip and remove button keyboard- and screen reader-accessible

Badge is clickable but not a native button; the icon-only remove button lacks a label; remove control is hover-only. Add role/tabIndex + keyboard handlers and aria-labels. Also expose visibility on focus.

-            <Badge
+            <Badge
               key={doc._id}
               variant="default"
-              className="group/document flex gap-1.5 py-1 cursor-pointer bg-accent/65 hover:bg-accent/100 hover:text-accent-foreground text-accent-foreground/90 rounded-md transition duration-300 items-center justify-center"
-              onClick={() => handlePreview(doc._id)}
+              className="group/document flex gap-1.5 py-1 cursor-pointer bg-accent/65 hover:bg-accent/100 hover:text-accent-foreground text-accent-foreground/90 rounded-md transition duration-300 items-center justify-center focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2"
+              role="button"
+              tabIndex={0}
+              aria-label={`Preview document ${doc.name}`}
+              onClick={() => handlePreview(doc._id)}
+              onKeyDown={(e) => {
+                if (e.key === "Enter" || e.key === " ") {
+                  e.preventDefault();
+                  handlePreview(doc._id);
+                }
+              }}
             >
               <div className="relative h-4 w-4">
                 <Icon
-                  className={`${IconClassName} h-4 w-4 group-hover/document:opacity-0 transition duration-300`}
+                  className={`${IconClassName} h-4 w-4 group-hover/document:opacity-0 group-focus-within/document:opacity-0 transition duration-300`}
                 />
                 <Button
                   variant="ghost"
                   size="icon"
-                  className="absolute inset-0 h-4 w-4 opacity-0 group-hover/document:opacity-100 hover:bg-muted-foreground/20 cursor-pointer transition duration-300"
+                  className="absolute inset-0 h-4 w-4 opacity-0 group-hover/document:opacity-100 group-focus-within/document:opacity-100 focus:opacity-100 hover:bg-muted-foreground/20 cursor-pointer transition duration-300"
+                  aria-label={`Remove document ${doc.name}`}
+                  title="Remove"
                   onClick={(e) => {
                     e.stopPropagation();
                     onRemove(doc._id);
                   }}
                 >
                   <XIcon className="w-3 h-3" />
                 </Button>
               </div>
📝 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
<Badge
key={doc._id}
variant="secondary"
className="flex items-center gap-1.5 pr-1 cursor-pointer"
variant="default"
className="group/document flex gap-1.5 py-1 cursor-pointer bg-accent/65 hover:bg-accent/100 hover:text-accent-foreground text-accent-foreground/90 rounded-md transition duration-300 items-center justify-center"
onClick={() => handlePreview(doc._id)}
>
<Icon className={`${IconClassName} h-3 w-3`} />
<span className="max-w-32 truncate">{doc.name}</span>
<Button
variant="ghost"
size="icon"
className="h-4 w-4 p-0.5 hover:bg-muted-foreground/20"
onClick={(e) => {
e.stopPropagation();
onRemove(doc._id);
}}
>
<XIcon className="w-3 h-3" />
</Button>
<div className="relative h-4 w-4">
<Icon
className={`${IconClassName} h-4 w-4 group-hover/document:opacity-0 transition duration-300`}
/>
<Button
variant="ghost"
size="icon"
className="absolute inset-0 h-4 w-4 opacity-0 group-hover/document:opacity-100 hover:bg-muted-foreground/20 cursor-pointer transition duration-300"
onClick={(e) => {
e.stopPropagation();
onRemove(doc._id);
}}
>
<XIcon className="w-3 h-3" />
</Button>
</div>
<span className="max-w-32 truncate text-xs cursor-pointer">
{doc.name}
</span>
</Badge>
<Badge
key={doc._id}
variant="default"
className="group/document flex gap-1.5 py-1 cursor-pointer bg-accent/65 hover:bg-accent/100 hover:text-accent-foreground text-accent-foreground/90 rounded-md transition duration-300 items-center justify-center focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2"
role="button"
tabIndex={0}
aria-label={`Preview document ${doc.name}`}
onClick={() => handlePreview(doc._id)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
handlePreview(doc._id);
}
}}
>
<div className="relative h-4 w-4">
<Icon
className={`${IconClassName} h-4 w-4 group-hover/document:opacity-0 group-focus-within/document:opacity-0 transition duration-300`}
/>
<Button
variant="ghost"
size="icon"
className="absolute inset-0 h-4 w-4 opacity-0 group-hover/document:opacity-100 group-focus-within/document:opacity-100 focus:opacity-100 hover:bg-muted-foreground/20 cursor-pointer transition duration-300"
aria-label={`Remove document ${doc.name}`}
title="Remove"
onClick={(e) => {
e.stopPropagation();
onRemove(doc._id);
}}
>
<XIcon className="w-3 h-3" />
</Button>
</div>
<span className="max-w-32 truncate text-xs cursor-pointer">
{doc.name}
</span>
</Badge>
🤖 Prompt for AI Agents
In src/components/chat/messages/user-message.tsx between lines 133 and 158, the
Badge component is clickable but not keyboard accessible and lacks proper ARIA
roles, and the remove Button is icon-only without an aria-label and only visible
on hover. To fix this, add role="button" and tabIndex={0} to the Badge,
implement keyboard event handlers for Enter and Space keys to trigger
handlePreview, add an aria-label to the remove Button describing its action, and
ensure the remove Button is visible on keyboard focus as well as hover by
adjusting its CSS classes accordingly.

Comment on lines +155 to 159
isSearching,
isLoadingSearch,
isSearchError,
searchError,
};
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Return type consistency for empty results

searchResults: searchResults || [] is pragmatic. If api.chats.queries.search returns chats, you may make the fallback explicit to keep strict typing happy in consumers:

-    searchResults: searchResults || [],
+    searchResults: (searchResults ?? []) as Array<Doc<"chats">>,

If unsure of the query’s return type, locate the Convex definition to confirm:


🏁 Script executed:

#!/bin/bash
# Try to find the search query and its return signature
fd -t f chats convex
rg -n --no-heading "queries\.search" -A 8 -B 4

Length of output: 854


🏁 Script executed:

#!/bin/bash
# Locate the Convex query definition for `search`
rg -n "export const search" -A 8 -B 4

Length of output: 610


🏁 Script executed:

#!/bin/bash
# Show the full definition of the `search` query to inspect its return
sed -n '80,140p' convex/chats/queries.ts

Length of output: 457


Enforce explicit typed fallback for searchResults

Verified that api.chats.queries.search returns Array<Doc<"chats">>, so we should use a nullish-coalescing fallback and cast to that type to keep consumers strictly typed.

Locations to update:

  • src/hooks/chats/use-chats.ts (in the object returned by your hook)

Suggested diff:

 return {
-  searchResults: searchResults || [],
+  searchResults: (searchResults ?? []) as Array<Doc<"chats">>,
   isSearching,
   isLoadingSearch,
   isSearchError,
   searchError,
 };
📝 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
isSearching,
isLoadingSearch,
isSearchError,
searchError,
};
return {
searchResults: (searchResults ?? []) as Array<Doc<"chats">>,
isSearching,
isLoadingSearch,
isSearchError,
searchError,
};
🤖 Prompt for AI Agents
In src/hooks/chats/use-chats.ts around lines 155 to 159, the returned object
from the hook includes searchResults without an explicit typed fallback. Fix
this by applying a nullish-coalescing operator to searchResults and casting it
to Array<Doc<"chats">> to ensure strict typing for consumers. Update the
returned object to include searchResults ?? [] as Array<Doc<"chats">>.

@shivamhwp shivamhwp changed the title feat: enhance document loading and error handling across components feat: enhance loading and error handling across components Aug 8, 2025
- Removed redundant loading and error handling logic from DocumentDialog and other components.
- Consolidated document download and opening functions for better clarity and maintainability.
- Enhanced user feedback during document loading and error states across various chat components.
- Updated LoadingSpinner and ErrorState components for consistent usage throughout the application.
@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Aug 9, 2025

Deploying zerobsv2 with  Cloudflare Pages  Cloudflare Pages

Latest commit: b5adc1c
Status: ✅  Deploy successful!
Preview URL: https://e327fed4.zerobsv2.pages.dev
Branch Preview URL: https://feature-ux-improvements.zerobsv2.pages.dev

View logs

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: 0

🔭 Outside diff range comments (3)
src/routes/settings/apiKeys.tsx (2)

245-258: Add error handling to toggle/remove mutations

These mutations currently assume success. Failures will silently do nothing beyond server logs, which contradicts the PR goal of improved error handling.

Wrap both in try/catch and surface failures via toasts:

   const handleToggle = async (config: ApiKeyConfig, enabled: boolean) => {
-    await updateApiKey({
-      key: config.key,
-      enabled,
-    });
-    toast.success(
-      `${config.title} ${enabled ? "enabled" : "disabled"} successfully`
-    );
+    try {
+      await updateApiKey({
+        key: config.key,
+        enabled,
+      });
+      toast.success(
+        `${config.title} ${enabled ? "enabled" : "disabled"} successfully`
+      );
+    } catch (error) {
+      toast.error(`Failed to ${enabled ? "enable" : "disable"} ${config.title}`);
+      console.error(error);
+    }
   };
 
   const handleRemove = async (config: ApiKeyConfig) => {
-    await removeApiKey({ key: config.key });
-    toast.success(`${config.title} removed successfully`);
+    try {
+      await removeApiKey({ key: config.key });
+      toast.success(`${config.title} removed successfully`);
+    } catch (error) {
+      toast.error(`Failed to remove ${config.title}`);
+      console.error(error);
+    }
   };

154-169: Align UI with the existing OPENAI_API_KEY

The backend and convex/langchain/models.ts exclusively use "OPENAI_API_KEY". To avoid user confusion and ensure the correct key is stored, update the UI copy and icon to reference OpenAI (Option A).

• Keep key: "OPENAI_API_KEY" unchanged.
• Update in src/routes/settings/apiKeys.tsx:

  • Change title from "OpenRouter API Key" to "OpenAI API Key".
  • Point the link at https://platform.openai.com/api-keys and link text to “OpenAI”.
  • Swap the icon from <Icons.OpenRouter /> to <Icons.OpenAI />.

Suggested diff:

@@ src/routes/settings/apiKeys.tsx
-    title: "OpenRouter API Key",
+    title: "OpenAI API Key",
     description: (
       <>
-        Required for GPT models and AI-powered features. Get your API key from{" "}
-        <ExternalLink href="https://openrouter.ai/settings/keys">
-          OpenRouter
-        </ExternalLink>
+        Required for GPT models and AI-powered features. Get your API key from{" "}
+        <ExternalLink href="https://platform.openai.com/api-keys">
+          OpenAI
+        </ExternalLink>
       </>
     ),
-    icon: <Icons.OpenRouter />,
+    icon: <Icons.OpenAI />,
src/routes/__root.tsx (1)

105-111: Use the provided open value in onOpenChange to avoid stale state

The SidebarProvider’s onOpenChange callback supplies the next open state, so toggling via !sidebarOpen can capture a stale value. Update to pass the new value directly:

• File: src/routes/__root.tsx
Lines: 105–111

-          <SidebarProvider
-            className="…"
-            open={sidebarOpen}
-            onOpenChange={() => {
-              setSidebarOpen(!sidebarOpen);
-            }}
-          >
+          <SidebarProvider
+            className="…"
+            open={sidebarOpen}
+            onOpenChange={(open: boolean) => {
+              setSidebarOpen(open);
+            }}
+          >
♻️ Duplicate comments (1)
src/routes/__root.tsx (1)

76-83: Resolved prior issue: state updates moved out of render

This addresses the earlier feedback about mutating state during render on settings routes. Thanks for fixing it.

🧹 Nitpick comments (4)
src/routes/settings/apiKeys.tsx (3)

27-31: Lazy-load favicons and mark as decorative to avoid redundant screen reader output

These provider icons are purely decorative (the provider name is rendered right next to them). To improve a11y and slightly reduce network/paint cost, consider lazy-loading and using empty alt text (or aria-hidden). Optionally, suppress the Referer header for privacy with referrerPolicy.

Apply this diff to each img tag:

-      <img
-        src="https://www.google.com/s2/favicons?sz=256&domain_url=https%3A%2F%2Fopenai.com%2F"
-        alt="OpenAI"
-        className="w-4 h-4 dark:invert"
-      />
+      <img
+        src="https://www.google.com/s2/favicons?sz=256&domain_url=https%3A%2F%2Fopenai.com%2F"
+        alt=""
+        aria-hidden="true"
+        loading="lazy"
+        decoding="async"
+        referrerPolicy="no-referrer"
+        className="w-4 h-4 dark:invert"
+      />

Repeat similarly for Google, Exa, and OpenRouter icons (keeping provider-specific classes intact).

Also applies to: 36-40, 45-49, 54-58


280-285: Disable toggle when no key value is set

Prevent enabling a provider with no stored key to avoid backend errors and confusing UX.

             <Switch
               id={`toggle-${config.key}`}
               checked={isEnabled}
               onCheckedChange={(checked) => handleToggle(config, checked)}
+              disabled={!existingKey?.value}
             />

131-141: Ensure masked key truncates properly and doesn’t blow layout

The mask can be very long and may overflow. Add min-w-0 to the flex child and apply truncate to the value span for reliable ellipsis in flex layouts.

-    <div className="flex items-center justify-between p-3 bg-input/80 rounded-md">
-      <div className="flex items-center gap-1 truncate">
+    <div className="flex items-center justify-between p-3 bg-input/80 rounded-md">
+      <div className="flex items-center gap-1 min-w-0">
         <span className="text-sm font-medium">Current Value:</span>
-        <span className="text-sm font-mono">
+        <span className="text-sm font-mono truncate">
           {config.isPassword ? maskKey(existingKey.value) : existingKey.value}
         </span>
       </div>
src/routes/__root.tsx (1)

76-83: Small polish: tighten effect dependencies and calls

You can remove sidebarOpen from deps and set both states unconditionally when entering settings. This avoids an extra effect run caused by toggling sidebarOpen to false.

-  useEffect(() => {
-    if (!isSettingsRoute) return;
-    sidebarOpen && setSidebarOpen(false);
-    setResizePanelOpen(false);
-  }, [isSettingsRoute, sidebarOpen, setSidebarOpen, setResizePanelOpen]);
+  useEffect(() => {
+    if (!isSettingsRoute) return;
+    setSidebarOpen(false);
+    setResizePanelOpen(false);
+  }, [isSettingsRoute, setSidebarOpen, setResizePanelOpen]);
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between c886bec and b87db9e.

📒 Files selected for processing (6)
  • src/components/chat/input/toolbar/index.tsx (3 hunks)
  • src/components/chat/panels/projects/details.tsx (3 hunks)
  • src/components/chat/panels/projects/document-list.tsx (5 hunks)
  • src/routes/__root.tsx (3 hunks)
  • src/routes/chat.$chatId.lazy.tsx (3 hunks)
  • src/routes/settings/apiKeys.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/components/chat/input/toolbar/index.tsx
  • src/components/chat/panels/projects/details.tsx
  • src/components/chat/panels/projects/document-list.tsx
  • src/routes/chat.$chatId.lazy.tsx
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/routes/__root.tsx (4)
src/routes/index.tsx (1)
  • Route (3-5)
src/store/chatStore.ts (1)
  • resizePanelOpenAtom (36-36)
src/components/ui/loading-spinner.tsx (1)
  • LoadingSpinner (11-35)
src/components/app-sidebar/index.tsx (1)
  • AppSidebar (35-246)
🔇 Additional comments (6)
src/routes/__root.tsx (6)

9-9: Adopted shared LoadingSpinner

Good move to standardize the loading UI.


12-12: Prepare store for right panel visibility control

Importing resizePanelOpenAtom here aligns the root route with panel state management.


17-17: Import useEffect to handle route-driven UI side-effects

Required for the settings route effect added below.


25-38: Head metadata formatting/branding changes look fine

Switching to double quotes and updating the title to "0bs" is consistent and non-functional otherwise.

Also applies to: 42-52, 56-59, 63-63


87-87: LGTM on spinner usage

Standardized spinner with size class keeps the loading screen consistent with the rest of the app.


112-118: Hide AppSidebar on settings routes

Conditional rendering here matches the effect that closes these panels; TopNav remains visible. Looks correct.

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: 0

🔭 Outside diff range comments (4)
src/hooks/chats/use-documents.ts (1)

111-118: Potential crash if chat.documents is undefined

Spreading possibly-undefined chat.documents will throw. Default to [] to be safe.

-          await updateChatMutation({
-            chatId,
-            updates: {
-              documents: [...chat.documents, ...documentIds],
-            },
-          });
+          await updateChatMutation({
+            chatId,
+            updates: {
+              documents: [...(chat.documents ?? []), ...documentIds],
+            },
+          });
src/components/chat/input/document-list.tsx (1)

105-121: Fix: Loading/error branches are unreachable due to early return ordering

The early return on Line 105 exits before loading/error UI, so users never see the spinner or error state. Reorder checks and remove the duplicate empty check.

-  if (!documents?.length) return null;
-
-  const selectedModel = models.find((m) => m.model_name === model);
-  const modalities = selectedModel?.modalities;
-
-  if (!documents?.length) return null;
-
-  if (isLoadingDocuments) {
-    return (
-      <div className="flex items-center justify-start p-2">
-        <div className="text-sm text-muted-foreground flex items-center gap-2">
-          <LoadingSpinner sizeClassName="h-4 w-4" />
-          Loading attached documents...
-        </div>
-      </div>
-    );
-  }
-
-  if (isDocumentsError || documentsError) {
+  if (isLoadingDocuments) {
+    return (
+      <div className="flex items-center justify-start p-2">
+        <div className="text-sm text-muted-foreground flex items-center gap-2">
+          <LoadingSpinner sizeClassName="h-4 w-4" />
+          Loading attached documents...
+        </div>
+      </div>
+    );
+  }
+
+  if (isDocumentsError || documentsError) {
     return (
       <div className="flex items-center justify-start p-2">
         <ErrorState
           title="Error loading documents"
           showDescription={false}
           className="p-2"
           density="compact"
         />
       </div>
     );
   }
+
+  if (!documents?.length) return null;
+
+  const selectedModel = models.find((m) => m.model_name === model);
+  const modalities = selectedModel?.modalities;
src/components/chat/messages/user-message.tsx (1)

25-31: Type safety: use Id<'documents'> for fileId instead of string

Aligns with project guidelines and removes the need for unsafe casts.

-const DocumentButton = ({
-  fileId,
-  setDocumentDialogOpen,
-}: {
-  fileId: string;
-  setDocumentDialogOpen: (id: Id<"documents"> | undefined) => void;
-}) => {
+const DocumentButton = ({
+  fileId,
+  setDocumentDialogOpen,
+}: {
+  fileId: Id<"documents">;
+  setDocumentDialogOpen: (id: Id<"documents"> | undefined) => void;
+}) => {
@@
   } = useQuery({
     ...convexQuery(api.documents.queries.get, {
-      documentId: fileId as Id<"documents">,
+      documentId: fileId,
     }),
   });
@@
-    <Button
-      className="py-6 cursor-pointer"
-      onClick={() => setDocumentDialogOpen(fileId as Id<"documents">)}
-    >
+    <Button
+      className="py-6 cursor-pointer"
+      onClick={() => setDocumentDialogOpen(fileId)}
+    >

Also applies to: 38-41, 68-73

src/components/app-sidebar/index.tsx (1)

232-237: Fix empty-state condition so “No matching chats found” can show

The current guard !isSearching prevents the search empty-state from rendering. Let the conditional depend only on the list being empty and choose the label based on isSearching.

-                {displayHistoryChats.length === 0 && !isSearching && (
-                  <div className="text-sm text-muted-foreground text-center py-2">
-                    {isSearching
-                      ? "No matching chats found"
-                      : "No chat history"}
-                  </div>
-                )}
+                {displayHistoryChats.length === 0 && (
+                  <div className="text-sm text-muted-foreground text-center py-2">
+                    {isSearching ? "No matching chats found" : "No chat history"}
+                  </div>
+                )}
♻️ Duplicate comments (3)
src/components/topnav.tsx (1)

44-46: Rules of Hooks violation: conditional return before hooks

Returning <Navigate /> conditionally means subsequent hooks (useEffect, useParams) aren’t called on that render. This breaks hook ordering between renders.

Remove the early return:

-  if (isErrorUser) {
-    return <Navigate to="/auth" />;
-  }

Add a side-effect redirect instead (place after hooks are declared), ensuring you don’t loop on the auth route and clearing stale user state:

useEffect(() => {
  if (!isErrorUser) return;

  // Optional: Only redirect on Unauthorized errors (see earlier comment)
  // if (!isUnauthorized(userError)) return;

  // Clear any stale user data before redirecting
  setUser(undefined);

  // Avoid redirect loops if TopNav also renders on the auth route
  if (location.pathname !== "/auth") {
    navigate({ to: "/auth" });
  }
  // Optionally include search/hash as needed
}, [isErrorUser, navigate, location.pathname, setUser /*, userError*/]);

While here, update the dependency array of the user-sync effect to include setUser (stable but satisfies lint):

useEffect(() => {
  if (user) setUser(user);
}, [user, setUser]);
```<!-- review_comment_end -->

</blockquote></details>
<details>
<summary>src/components/chat/messages/user-message.tsx (2)</summary><blockquote>

`136-161`: **Accessibility: Make document chip and remove button keyboard- and screen reader-accessible**

Same issue previously flagged: clickable Badge without role/tabIndex and icon-only remove without label.



```diff
-            <Badge
+            <Badge
               key={doc._id}
               variant="default"
-              className="group/document flex gap-1.5 py-1 cursor-pointer bg-accent/65 hover:bg-accent/100 hover:text-accent-foreground text-accent-foreground/90 rounded-md transition duration-300 items-center justify-center"
+              className="group/document flex gap-1.5 py-1 cursor-pointer bg-accent/65 hover:bg-accent/100 hover:text-accent-foreground text-accent-foreground/90 rounded-md transition duration-300 items-center justify-center focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2"
+              role="button"
+              tabIndex={0}
+              aria-label={`Preview document ${doc.name}`}
               onClick={() => handlePreview(doc._id)}
+              onKeyDown={(e) => {
+                if (e.key === "Enter" || e.key === " ") {
+                  e.preventDefault();
+                  handlePreview(doc._id);
+                }
+              }}
             >
               <div className="relative h-4 w-4">
                 <Icon
-                  className={`${IconClassName} h-4 w-4 group-hover/document:opacity-0 transition duration-300`}
+                  className={`${IconClassName} h-4 w-4 group-hover/document:opacity-0 group-focus-within/document:opacity-0 transition duration-300`}
                 />
                 <Button
                   variant="ghost"
                   size="icon"
-                  className="absolute inset-0 h-4 w-4 opacity-0 group-hover/document:opacity-100 hover:bg-muted-foreground/20 cursor-pointer transition duration-300"
+                  className="absolute inset-0 h-4 w-4 opacity-0 group-hover/document:opacity-100 group-focus-within/document:opacity-100 focus:opacity-100 hover:bg-muted-foreground/20 cursor-pointer transition duration-300"
+                  aria-label={`Remove document ${doc.name}`}
+                  title="Remove"
                   onClick={(e) => {
                     e.stopPropagation();
                     onRemove(doc._id);
                   }}
                 >
                   <XIcon className="w-3 h-3" />
                 </Button>
               </div>

265-265: Fix layout regression: min-w-[100vw] causes overflow

Replace with responsive width that respects parent constraints.

-              className="bg-transparent resize-none border-none min-w-[100vw] max-w-full ring-0 focus-visible:ring-0 focus-visible:ring-offset-0 outline-none focus-visible:outline-none text-base"
+              className="bg-transparent resize-none border-none w-full min-w-0 max-w-full ring-0 focus-visible:ring-0 focus-visible:ring-offset-0 outline-none focus-visible:outline-none text-base"
🧹 Nitpick comments (11)
src/components/topnav.tsx (3)

14-19: Remove <Navigate /> import if switching to imperative navigation

If you move the auth-redirect into an effect (recommended below), <Navigate /> becomes unused. Drop it to prevent lint failures.

 import {
-  Navigate,
   useLocation,
   useNavigate,
   useParams,
 } from "@tanstack/react-router";

36-40: Gate auth redirect on Unauthorized errors; also capture error from the query

Right now every fetch error redirects to /auth. That can misroute on transient/network errors. Capture the error object so you can conditionally redirect only for 401/Unauthorized. Consider also tuning retry behavior.

Apply within this block:

   const {
     data: user,
     isError: isErrorUser,
+    error: userError,
     isLoading: isLoadingUser,
   } = useQuery({
     ...convexQuery(api.auth.getUser, {}),
   });

Then, in an effect (see separate comment), redirect only when userError indicates Unauthorized. Please confirm the error shape from convex + tanstack-query in your setup (e.g., HTTP status, code, or message) before gating. If Unauthorized isn’t distinguishable, at least set retry: false on this query to avoid thrashing on auth failures.

Verification suggestions:

  • Check what userError looks like for an expired session vs. a network outage.
  • If it exposes an HTTP status, gate on 401 and keep the TopNav rendered otherwise (you could show an inline error or nothing).

If you want, I can wire the conditional retry function once you confirm the error shape.


102-115: Add an aria-label to the Settings button for better a11y

ModeToggle already has an sr-only label; mirror that for Settings to improve screen reader support.

         {!resizePanelOpen ? (
           <Button
             variant="ghost"
             size="icon"
             className="cursor-pointer"
+            aria-label="Open settings"
             onClick={() => {
               navigate({ to: "/settings/profile" });
             }}
           >
             <Settings2Icon className="h-6 w-6" />
           </Button>
         ) : null}
src/hooks/chats/use-documents.ts (2)

35-41: Add onError handling for the mutation to provide user feedback

Silent failures here can leave UI state stale. Add onError to surface issues to the user.

-      updateChatInputMutation({
-        chatId: chatId,
-        updates: {
-          documents: filteredDocuments,
-        },
-      });
+      updateChatInputMutation(
+        {
+          chatId,
+          updates: {
+            documents: filteredDocuments,
+          },
+        },
+        {
+          onError: (err) => {
+            toast("Failed to remove document", {
+              description: String(err),
+            });
+          },
+        },
+      );

73-96: Optional: parallelize uploads with bounded concurrency

Uploads are fully sequential; for many files this increases latency. Consider parallelizing with a small concurrency limit (e.g., 3–5) to reduce total time while avoiding server overload.

If you want, I can provide a throttled Promise pool snippet tailored to this hook.

Also applies to: 98-108

src/components/chat/input/document-list.tsx (1)

45-66: Accessibility: Make badge keyboard-focusable and label the remove button

The badge is clickable but not keyboard accessible; the icon-only remove button lacks an aria-label and isn’t focus-visible.

-      <Badge
-        variant="default"
-        className="group flex gap-1.5 py-1 cursor-pointer bg-accent/65 hover:bg-accent/100 hover:text-accent-foreground text-accent-foreground/90 rounded-md transition duration-300 items-center justify-center"
-        onClick={handlePreview}
-      >
+      <Badge
+        variant="default"
+        className="group/document flex gap-1.5 py-1 cursor-pointer bg-accent/65 hover:bg-accent/100 hover:text-accent-foreground text-accent-foreground/90 rounded-md transition duration-300 items-center justify-center focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2"
+        role="button"
+        tabIndex={0}
+        aria-label={`Preview document ${doc.name}`}
+        onClick={handlePreview}
+        onKeyDown={(e) => {
+          if (e.key === "Enter" || e.key === " ") {
+            e.preventDefault();
+            handlePreview();
+          }
+        }}
+      >
         <div className="relative h-4 w-4">
           <Icon
-            className={`${IconClassName} h-4 w-4 group-hover:opacity-0 transition duration-300`}
+            className={`${IconClassName} h-4 w-4 group-hover/document:opacity-0 group-focus-within/document:opacity-0 transition duration-300`}
           />
           <Button
             variant="ghost"
             size="icon"
-            className="absolute inset-0 h-4 w-4 opacity-0 group-hover:opacity-100 hover:bg-muted-foreground/20 cursor-pointer transition duration-300"
+            className="absolute inset-0 h-4 w-4 opacity-0 group-hover/document:opacity-100 group-focus-within/document:opacity-100 focus:opacity-100 hover:bg-muted-foreground/20 cursor-pointer transition duration-300"
+            aria-label={`Remove document ${doc.name}`}
+            title="Remove"
             onClick={handleRemove}
           >
             <XIcon className="w-3 h-3" />
           </Button>
         </div>

Also applies to: 50-61

src/components/document-dialog.tsx (2)

49-91: Optional: guard against setting state after unmount or document switch

The async preview loader can update state after the dialog closes or the doc changes. Add a cancellation flag.

   useEffect(() => {
-    setPreviewUrl(null);
+    let cancelled = false;
+    setPreviewUrl(null);
     const loadPreviewUrl = async () => {
       if (!document) return;
       try {
         switch (tag) {
@@
-            setPreviewUrl(url);
+            if (!cancelled) setPreviewUrl(url);
             break;
           }
@@
-            setPreviewUrl(document.key as string);
+            if (!cancelled) setPreviewUrl(document.key as string);
             break;
           }
@@
-            setPreviewUrl(`https://www.youtube.com/embed/${document.key}`);
+            if (!cancelled)
+              setPreviewUrl(`https://www.youtube.com/embed/${document.key}`);
             break;
           }
           default:
             if (["file", "text", "github"].includes(document.type)) {
               const url = await generateDownloadUrl({
                 documentId: document._id!,
               });
-              setPreviewUrl(url);
+              if (!cancelled) setPreviewUrl(url);
               break;
             } else {
-              setPreviewUrl(document.key as string);
+              if (!cancelled) setPreviewUrl(document.key as string);
             }
             break;
         }
       } catch (e) {
-        setPreviewUrl(null);
+        if (!cancelled) setPreviewUrl(null);
       }
     };
     loadPreviewUrl();
-  }, [document, tag, generateDownloadUrl]);
+    return () => {
+      cancelled = true;
+    };
+  }, [document, tag, generateDownloadUrl]);

122-127: Nit: use sizeClassName for spinner sizing for consistency

LoadingSpinner supports sizeClassName specifically for icon sizing.

-          <LoadingSpinner className="h-8 w-8" />
+          <LoadingSpinner sizeClassName="h-8 w-8" />
src/components/chat/messages/user-message.tsx (1)

110-118: Fix typo: double space in error title

Minor polish.

-        <ErrorState
-          title="Error loading attached  documents"
-          error={documentsError}
-        />
+        <ErrorState title="Error loading attached documents" error={documentsError} />
src/components/app-sidebar/index.tsx (2)

79-86: Optional: prevent repeated loadMore triggers

IntersectionObserver will call loadMore each time the sentinel re-enters view. Unobserve after the first trigger and re-observe once status flips back to "CanLoadMore".

     const observer = new IntersectionObserver(
       (entries) => {
         const [entry] = entries;
         if (entry.isIntersecting) {
-          loadMore(15);
+          observer.unobserve(loadMoreElement);
+          loadMore(15);
         }
       },

155-163: Nit: add accessible label to the clear-search button

Helps screen readers discover the control.

               <Button
                 variant="ghost"
                 size="icon"
                 onClick={() => setSearchQuery("")}
                 className="flex items-center justify-center size-5"
                 tabIndex={-1}
+                aria-label="Clear search"
+                title="Clear"
               >
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b87db9e and b062b96.

📒 Files selected for processing (15)
  • services/mcps (1 hunks)
  • src/components/app-sidebar/index.tsx (8 hunks)
  • src/components/chat/input/document-list.tsx (3 hunks)
  • src/components/chat/input/toolbar/index.tsx (3 hunks)
  • src/components/chat/messages/user-message.tsx (7 hunks)
  • src/components/chat/panels/projects/details.tsx (3 hunks)
  • src/components/chat/panels/projects/document-list.tsx (5 hunks)
  • src/components/document-dialog.tsx (5 hunks)
  • src/components/topnav.tsx (4 hunks)
  • src/components/ui/accordion.tsx (2 hunks)
  • src/hooks/chats/use-chats.ts (2 hunks)
  • src/hooks/chats/use-documents.ts (1 hunks)
  • src/hooks/chats/use-mcp.ts (2 hunks)
  • src/hooks/chats/use-messages.ts (2 hunks)
  • src/routes/chat.$chatId.lazy.tsx (3 hunks)
✅ Files skipped from review due to trivial changes (1)
  • services/mcps
🚧 Files skipped from review as they are similar to previous changes (8)
  • src/hooks/chats/use-messages.ts
  • src/hooks/chats/use-chats.ts
  • src/components/chat/panels/projects/details.tsx
  • src/components/chat/input/toolbar/index.tsx
  • src/routes/chat.$chatId.lazy.tsx
  • src/components/ui/accordion.tsx
  • src/hooks/chats/use-mcp.ts
  • src/components/chat/panels/projects/document-list.tsx
🧰 Additional context used
📓 Path-based instructions (1)
**/*.ts

📄 CodeRabbit Inference Engine (.cursor/rules/convex_rules.mdc)

**/*.ts: You can use the helper TypeScript type Id imported from './_generated/dataModel' to get the type of the id for a given table.
If you need to define a Record, provide the correct key and value types (e.g., Record<Id<'users'>, string>).
Be strict with types, particularly around ids of documents. Use Id<'table'> rather than string for document ids.
Always use as const for string literals in discriminated union types.
When using the Array type, always define arrays as const array: Array = [...];
When using the Record type, always define records as const record: Record<KeyType, ValueType> = {...};

Files:

  • src/hooks/chats/use-documents.ts
🧠 Learnings (1)
📚 Learning: 2025-08-01T07:34:38.459Z
Learnt from: CR
PR: 0bs-chat/zerobs#0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-08-01T07:34:38.459Z
Learning: Applies to convex/**/*.{ts,js} : Use the api object from convex/_generated/api.ts to call public functions and the internal object to call internal functions.

Applied to files:

  • src/components/chat/input/document-list.tsx
🧬 Code Graph Analysis (5)
src/components/chat/messages/user-message.tsx (4)
convex/_generated/dataModel.d.ts (1)
  • Id (48-49)
src/components/ui/loading-spinner.tsx (1)
  • LoadingSpinner (11-35)
src/components/ui/error-state.tsx (1)
  • ErrorState (122-122)
src/components/ui/autosize-textarea.tsx (1)
  • AutosizeTextarea (65-119)
src/components/app-sidebar/index.tsx (5)
src/hooks/chats/use-chats.ts (1)
  • useSearchChats (125-160)
src/components/ui/sidebar.tsx (4)
  • SidebarContent (703-703)
  • SidebarGroup (705-705)
  • SidebarGroupLabel (708-708)
  • SidebarGroupContent (707-707)
src/components/ui/accordion.tsx (2)
  • AccordionTrigger (234-234)
  • AccordionContent (235-235)
src/components/ui/loading-spinner.tsx (1)
  • LoadingSpinner (11-35)
src/components/ui/error-state.tsx (1)
  • ErrorState (122-122)
src/components/document-dialog.tsx (3)
convex/documents/mutations.ts (1)
  • generateDownloadUrl (22-39)
src/components/ui/loading-spinner.tsx (1)
  • LoadingSpinner (11-35)
src/components/ui/error-state.tsx (1)
  • ErrorState (122-122)
src/components/topnav.tsx (2)
src/components/ui/loading-spinner.tsx (1)
  • LoadingSpinner (11-35)
src/components/theme-provider.tsx (1)
  • ModeToggle (69-86)
src/components/chat/input/document-list.tsx (3)
src/store/chatStore.ts (1)
  • documentDialogOpenAtom (43-45)
src/components/ui/loading-spinner.tsx (1)
  • LoadingSpinner (11-35)
src/components/ui/error-state.tsx (1)
  • ErrorState (122-122)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (5)
src/components/topnav.tsx (3)

24-24: LGTM: standardized spinner import

Consistent with the new UI primitives. No issues.


59-59: LGTM: shortcut comment clarity

Comment accurately describes the scope of the shortcut. No code issues.


97-101: LGTM: unobtrusive loading indicator

Compact spinner placement and sizing align with the design system. Accessible via aria-label on the icon.

src/hooks/chats/use-documents.ts (1)

12-17: Conditional query gating via convexQuery "skip" looks correct

Passing "skip" when chatId is "new" cleanly disables the chat query. No issues here.

src/components/document-dialog.tsx (1)

224-235: Security review: sandbox flags for external iframes

The combination of allow-same-origin and allow-scripts grants substantial capability to embedded content. If you don’t need script execution, consider dropping allow-scripts; if you keep scripts, consider removing allow-same-origin to enforce opaque origin. Verify the UX trade-offs.

Would you like me to audit typical URLs and recommend the safest viable sandbox set for your use cases?

…r and useDocuments hooks

- Removed unnecessary line breaks and adjusted formatting for better readability.
- Enhanced observer logic in AppSidebar to unobserve the loadMoreElement when it is intersecting.
- Updated useRemoveDocument and useUploadDocuments hooks for consistent code style.
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: 0

🔭 Outside diff range comments (1)
src/components/app-sidebar/index.tsx (1)

233-239: Fix unreachable “No matching chats found” message (wrong condition).

The empty-state for search results is guarded by !isSearching, so “No matching chats found” will never render during a search. This contradicts both the intent and the AI summary, and it suppresses useful feedback to users.

Apply this diff to show:

  • “No matching chats found” only when searching, not loading, not error, and both pinned and history search lists are empty.
  • “No chat history” only when not searching and history is empty.
-                {displayHistoryChats.length === 0 && !isSearching && (
-                  <div className="text-sm text-muted-foreground text-center py-2">
-                    {isSearching
-                      ? "No matching chats found"
-                      : "No chat history"}
-                  </div>
-                )}
+                {isSearching &&
+                  !isLoadingSearch &&
+                  !isSearchError &&
+                  displayHistoryChats.length === 0 &&
+                  displayPinnedChats.length === 0 && (
+                  <div className="text-sm text-muted-foreground text-center py-2">
+                    No matching chats found
+                  </div>
+                )}
+                {!isSearching && displayHistoryChats.length === 0 && (
+                  <div className="text-sm text-muted-foreground text-center py-2">
+                    No chat history
+                  </div>
+                )}
🧹 Nitpick comments (6)
src/components/app-sidebar/index.tsx (6)

141-154: Add accessible name to the search input and drop redundant inline style.

Improve a11y by providing an explicit accessible name; the inline transparent background is redundant with the Tailwind classes.

             <Input
               placeholder="Search chats"
               className="flex-1 bg-transparent border-none focus:ring-0 focus:ring-offset-0 focus-visible:ring-0 focus-visible:ring-offset-0 outline-none text-base px-0"
               type="text"
+              aria-label="Search chats"
               value={searchQuery}
               onChange={(e) => setSearchQuery(e.target.value)}
               onKeyDown={(e) => {
                 if (e.key === "Escape") {
                   setSearchQuery("");
                   e.currentTarget.blur();
                 }
               }}
-              style={{ backgroundColor: "transparent" }}
             />

156-164: Add accessible name to the clear-search button.

Icon-only controls need an accessible label for screen readers.

               <Button
                 variant="ghost"
                 size="icon"
                 onClick={() => setSearchQuery("")}
                 className="flex items-center justify-center size-5"
                 tabIndex={-1}
+                aria-label="Clear search"
+                title="Clear search"
               >
                 <XIcon className="w-4 h-4" />
               </Button>

181-188: AccordionTrigger without children: add an accessible label (or make the whole header the trigger).

AccordionTrigger is rendered without children, so the control has no accessible name and only the chevron is clickable. At minimum, add an aria-label. Alternatively, consider making the entire header (icon + “Pinned”) the trigger for a larger tap target.

-                  <AccordionTrigger className="cursor-pointer p-0 mr-2" />
+                  <AccordionTrigger
+                    className="cursor-pointer p-0 mr-2"
+                    aria-label="Toggle pinned chats"
+                  />

171-176: Avoid mixing controlled and uncontrolled Accordion props.

value makes the Accordion controlled; defaultValue is ignored and can be removed for clarity.

             <Accordion
               type="single"
               collapsible
-              defaultValue="pinned"
               className="w-full"
               value={pinnedChatsAccordionOpen ? "pinned" : ""}

84-84: Replace magic number 15 with a named constant.

Removes duplication, clarifies intent, and centralizes changes to page size.

-          loadMore(15);
+          loadMore(PAGE_SIZE);
-                    onClick={() => loadMore(15)}
+                    onClick={() => loadMore(PAGE_SIZE)}

Add this near the top of the module (e.g., after imports):

const PAGE_SIZE = 15;

Also applies to: 228-228


218-221: Optionally surface error details to users.

Consider displaying the underlying error message (e.g., searchError?.message) in ErrorState to aid troubleshooting. If you adopt this, also destructure searchError from useSearchChats() above.

Would you like me to wire this up and ensure the ErrorState component’s API is respected?

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b062b96 and 3e246db.

📒 Files selected for processing (2)
  • src/components/app-sidebar/index.tsx (9 hunks)
  • src/hooks/chats/use-documents.ts (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/hooks/chats/use-documents.ts
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/components/app-sidebar/index.tsx (6)
src/hooks/chats/use-chats.ts (1)
  • useSearchChats (125-160)
src/store/chatStore.ts (1)
  • pinnedChatsAccordionOpenAtom (56-59)
src/components/ui/sidebar.tsx (4)
  • SidebarContent (703-703)
  • SidebarGroup (705-705)
  • SidebarGroupLabel (708-708)
  • SidebarGroupContent (707-707)
src/components/ui/accordion.tsx (2)
  • AccordionTrigger (234-234)
  • AccordionContent (235-235)
src/components/ui/loading-spinner.tsx (1)
  • LoadingSpinner (11-35)
src/components/ui/error-state.tsx (1)
  • ErrorState (122-122)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (2)
src/components/app-sidebar/index.tsx (2)

79-86: Good call: unobserving the target prevents duplicate auto-loads.

Unobserving the load-more element before invoking loadMore(15) avoids duplicate fetches caused by repeated intersections. Dependency handling and cleanup look correct.

Also applies to: 91-91


23-24: LGTM: Consistent loading and error primitives integrated.

Using shared LoadingSpinner and ErrorState aligns with the PR’s goal of consistent UX across components.

- Removed unnecessary icons from the Pinned and Previous Chats sections in the AppSidebar component for a cleaner UI.
- Adjusted the SidebarGroupLabel to streamline the display of section titles.
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: 1

Caution

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

⚠️ Outside diff range comments (2)
src/components/ui/accordion.tsx (1)

129-134: Add type='button' and wire up ARIA to make the accordion accessible and form-safe.

  • Without type="button", the trigger can submit a surrounding form.
  • Add aria-expanded/aria-controls on the trigger, and id/aria-labelledby on the content for screen readers.

Apply this diff:

@@
-  const isOpen = expandedItems.has(accordionItem.value);
+  const isOpen = expandedItems.has(accordionItem.value);
+  const triggerId = `accordion-trigger-${accordionItem.value}`;
+  const contentId = `accordion-content-${accordionItem.value}`;
@@
-      <motion.button
+      <motion.button
         data-slot="accordion-trigger"
         className={cn(
           "focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50",
           className
         )}
         data-state={isOpen ? "open" : "closed"}
         onClick={handleClick}
+        id={triggerId}
+        aria-controls={contentId}
+        aria-expanded={isOpen}
+        type="button"
         {...props}
       >
@@
-  const isOpen = expandedItems.has(accordionItem.value);
+  const isOpen = expandedItems.has(accordionItem.value);
+  const triggerId = `accordion-trigger-${accordionItem.value}`;
+  const contentId = `accordion-content-${accordionItem.value}`;
@@
-        <motion.div
+        <motion.div
           data-slot="accordion-content"
           className="overflow-hidden text-sm"
           initial="collapsed"
           animate="open"
           exit="collapsed"
+          id={contentId}
+          role="region"
+          aria-labelledby={triggerId}
           variants={{
             open: { opacity: 1, height: "auto" },
             collapsed: { opacity: 0, height: 0 },
           }}

Also applies to: 139-147, 180-186, 185-199

src/components/chat/messages/index.tsx (1)

113-113: Include missing error-related flags in the useMemo dependency array

The mainContent memo relies on isError, error, isStreamError, and streamError to decide what to render. These four flags are not listed in the current dependency array, so the memo won’t update when they change—leading to stale or “frozen” error UI. A quick script confirms all four are missing.

• File: src/components/chat/messages/index.tsx (around line 113)
• Hook: useMemo(… , [/* dependencies */]) for mainContent

Apply this diff to ensure the memo updates correctly:

-  }, [isLoading, isEmpty, streamStatus, chatId, userLoadable]);
+  }, [
+    isLoading,
+    isEmpty,
+    isError,
+    error,
+    isStreamError,
+    streamError,
+    streamStatus,
+    chatId,
+    userLoadable,
+  ]);
♻️ Duplicate comments (2)
src/routes/__root.tsx (1)

79-87: Good fix: moved layout state updates out of render and into an effect.

This resolves the earlier “state mutation during render” issue for settings routes and prevents render loops. Nice job.

src/components/topnav.tsx (1)

18-23: Fix conditional hook usage caused by early return.

Returning before subsequent hooks (e.g., useEffect below) violates React’s rules of hooks. Navigate via an effect instead and keep all hooks unconditional. This also addresses the same concern noted previously.

Apply these diffs:

@@
-import {
-  Navigate,
-  useLocation,
-  useNavigate,
-  useParams,
-} from "@tanstack/react-router";
+import { useLocation, useNavigate, useParams } from "@tanstack/react-router";
@@
-  if (isErrorUser) {
-    return <Navigate to="/auth" />;
-  }
+  useEffect(() => {
+    if (isErrorUser) {
+      navigate({ to: "/auth" });
+    }
+  }, [isErrorUser, navigate]);
#!/bin/bash
# Audit for other early <Navigate /> returns that may conditionally skip hooks.
# Shows surrounding context to verify ordering.
rg -nP -C3 --type=tsx '\breturn\s*<Navigate\b'

Also applies to: 49-51

🧹 Nitpick comments (22)
src/components/ui/accordion.tsx (6)

108-114: Broaden Trigger props to accept native button attributes (disabled, aria-*, id).

You already spread ...props onto a button but the type doesn’t allow native attributes, which can break consumers (e.g., disabled). Extend from the intrinsic button props.

Apply this diff:

-interface AccordionTriggerProps {
-  className?: string;
-  children?: React.ReactNode;
-  onClick?: React.MouseEventHandler<HTMLButtonElement>;
-  showIcon?: boolean;
-}
+interface AccordionTriggerProps
+  extends React.ButtonHTMLAttributes<HTMLButtonElement> {
+  showIcon?: boolean;
+}

29-37: Let Accordion pass through div attributes safely.

AccordionProps doesn’t extend HTML div attributes but you spread ...props to a div. Extend from the intrinsic div props to avoid type holes and enable id/aria/class usage.

Apply this diff:

-interface AccordionProps {
+interface AccordionProps extends React.HTMLAttributes<HTMLDivElement> {
   type?: "single" | "multiple";
   collapsible?: boolean;
   value?: string | string[];
   defaultValue?: string | string[];
   onValueChange?: (value: string | string[]) => void;
-  className?: string;
-  children?: React.ReactNode;
+  className?: string; // optional: redundant, but keep explicit if you prefer
+  children?: React.ReactNode; // ditto
 }

102-107: Same pass-through typing for AccordionItem.

AccordionItemEnhanced forwards ...props to a div. Let AccordionItemProps extend HTML div attributes for correctness and flexibility.

Apply this diff:

-interface AccordionItemProps {
+interface AccordionItemProps extends React.HTMLAttributes<HTMLDivElement> {
   value: string;
   className?: string;
   children?: React.ReactNode;
 }

163-167: Same pass-through typing for AccordionContent.

AccordionContent spreads ...props to the animated div; extend from HTML div attributes to type that properly.

Apply this diff:

-interface AccordionContentProps {
+interface AccordionContentProps extends React.HTMLAttributes<HTMLDivElement> {
   className?: string;
   children?: React.ReactNode;
 }

150-151: Chevron rotation direction—confirm intended UX.

Currently the chevron points down when open and up when closed (isOpen ? 0 : 180). Many designs do the opposite. If you want “rotate up when open,” flip the values.

Apply this diff if that’s the intended behavior:

-            animate={{ rotate: isOpen ? 0 : 180 }}
+            animate={{ rotate: isOpen ? 180 : 0 }}

153-153: Trim trailing whitespace in className.

Minor cleanup; there’s a trailing space at the end of the className value.

Apply this diff:

-            className="items-center flex justify-center "
+            className="items-center flex justify-center"
src/routes/__root.tsx (3)

110-116: Use the next open value from onOpenChange instead of toggling via captured state.

Relying on the captured sidebarOpen can desync state if multiple updates fire quickly. Prefer the callback’s next open value.

Apply this diff:

-          <SidebarProvider
+          <SidebarProvider
             className="font-sans h-svh relative before:content-[''] before:fixed before:inset-0 before:bg-[url('/images/noise.png')] before:opacity-50 before:pointer-events-none before:z-[-1]"
             open={sidebarOpen}
-            onOpenChange={() => {
-              setSidebarOpen(!sidebarOpen);
-            }}
+            onOpenChange={(open) => {
+              setSidebarOpen(open);
+            }}
           >

117-123: Centralize settings-route layout decisions (optional).

TopNav hides itself via CSS on settings while Root always renders it. Consider conditionally rendering TopNav here (like AppSidebar) to avoid mounting work on settings pages and to keep route-specific layout logic in one place.

Potential adjustment:

-                {!isSettingsRoute && <AppSidebar />}
-                <TopNav />
+                {!isSettingsRoute && <AppSidebar />}
+                {!isSettingsRoute && <TopNav />}

74-78: Nit: hoist static publicRoutes outside the component.

This avoids re-allocating the array every render and makes intent clearer. Also consider broadening the check if you introduce nested auth routes (e.g., startsWith("/auth")).

Would you like me to send a follow-up patch that hoists the constant and, if needed, adapts the check for nested auth paths?

src/components/topnav.tsx (4)

53-57: Add setUser to effect deps.

The setter is stable, but including it clarifies intent and silences strict-lint configurations.

-  }, [user]);
+  }, [user, setUser]);

41-47: Auth error handling nuance (optional).

Redirecting on any query error may bounce users to /auth on transient network failures. If possible, gate on an “unauthorized”/auth-specific error code, otherwise surface a non-blocking ErrorState.

Want me to update this to inspect error types from convex/react-query (e.g., unauthorized vs network) and only redirect on auth failures?


113-125: Accessibility: add an aria-label to the Settings button.

Screen reader users won’t get a name from the icon alone.

-          <Button
+          <Button
             variant="ghost"
             size="icon"
             className="cursor-pointer"
             onClick={() => {
               navigate({ to: "/settings/profile" });
             }}
+            aria-label="Open settings"
           >
             <Settings2Icon className="h-6 w-6" />
           </Button>

85-88: Optional: avoid duplicate settings-route visibility logic.

Root decides sidebar visibility for settings routes; TopNav also computes and hides itself. Consider relying on the root to conditionally render TopNav (or lifting a shared helper) to keep this concern in one place.

src/components/chat/input/document-list.tsx (5)

45-49: Improve a11y: title on Badge, aria-label on Remove, and keyboard-visible state.

Add accessible labels and ensure the remove button becomes visible on keyboard focus, not only on hover.

@@
-      <Badge
+      <Badge
         variant="default"
         className="group/badge flex  gap-1.5 py-1.5 cursor-pointer bg-accent/65 hover:bg-accent/100 hover:text-accent-foreground text-accent-foreground/90  transition duration-300 items-center justify-center"
+        title={doc.name}
         onClick={handlePreview}
       >
@@
-          <Button
+          <Button
             variant="ghost"
             size="icon"
-            className="absolute inset-0 h-4 w-4 opacity-0 group-hover/badge:opacity-100 hover:bg-muted-foreground/20 cursor-pointer transition duration-300"
+            aria-label={`Remove document ${doc.name}`}
+            title={`Remove document ${doc.name}`}
+            className="absolute inset-0 h-4 w-4 opacity-0 group-hover/badge:opacity-100 group-focus-within/badge:opacity-100 focus-visible:opacity-100 hover:bg-muted-foreground/20 cursor-pointer transition duration-300 focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none"
             onClick={handleRemove}
           >
-            <XIcon className="w-3 h-3" />
+            <XIcon className="w-3 h-3" aria-hidden="true" />
           </Button>

Also applies to: 54-61, 60-60


115-118: Minor: Use LoadingSpinner’s label prop instead of a separate text node.

Keeps semantics consistent and reduces DOM.

-          <LoadingSpinner sizeClassName="h-4 w-4" />
-          Loading attached documents...
+          <LoadingSpinner sizeClassName="h-4 w-4" label="Loading attached documents..." />

123-134: Simplify error condition and optionally surface the message.

isError already reflects presence of error. Consider relying on it and optionally pass a compact description.

-  if (isDocumentsError || documentsError) {
+  if (isDocumentsError) {
     return (
       <div className="flex items-center justify-start p-2">
         <ErrorState
           title="Error loading documents"
-          showDescription={false}
+          description={documentsError instanceof Error ? documentsError.message : undefined}
+          showDescription={Boolean(documentsError)}
           className="p-2"
           density="compact"
         />
       </div>
     );
   }

12-13: Optional: centralize fetching via a useDocuments hook for consistency.

For parity with the rest of the codebase and to avoid repeating query wiring, consider replacing the inline useQuery block with a dedicated hook (e.g., from @/hooks/chats/use-documents) that returns { documents, isLoadingDocuments, isDocumentsError, documentsError }.

-import { useRemoveDocument } from "@/hooks/chats/use-documents";
+import { useRemoveDocument, useDocuments } from "@/hooks/chats/use-documents";
@@
-  } = useQuery({
-    ...convexQuery(api.documents.queries.getMultiple, { documentIds }),
-    enabled: documentIds.length > 0,
-  });
+  } = useDocuments(documentIds);

Note: If you adopt this, retain the post-error empty-docs guard added in the previous comment.

Also applies to: 79-86


14-14: Tiny micro-optimization: memoize modalities lookup.

Not critical, but avoids re-running find on each render.

-import React, { useCallback } from "react";
+import React, { useCallback, useMemo } from "react";
@@
-  const selectedModel = models.find((m) => m.model_name === model);
-  const modalities = selectedModel?.modalities;
+  const modalities = useMemo(
+    () => models.find((m) => m.model_name === model)?.modalities,
+    [model]
+  );

Also applies to: 107-109

src/components/chat/messages/index.tsx (4)

15-16: Destructured hook return looks good; consider clearer naming.

The expanded shape matches the updated hook. Consider renaming error to messagesError locally to avoid confusion with streamError in the same scope.

-  const { isLoading, isEmpty, isError, error, isStreamError, streamError } =
+  const { isLoading, isEmpty, isError, error: messagesError, isStreamError, streamError } =
     useMessages({ chatId });

And update usages accordingly (e.g., error={messagesError}).


43-46: Use LoadingSpinner’s label prop to avoid duplicate, misaligned text.

Slight simplification and better a11y by keeping the label tied to the spinner component.

-        <div className="flex items-center justify-center h-full">
-          <LoadingSpinner />
-          <div className="text-muted-foreground">Loading messages...</div>
-        </div>
+        <div className="flex items-center justify-center h-full">
+          <LoadingSpinner label="Loading messages..." />
+        </div>

50-62: Guard general error branch on isError only to prevent stale-object flicker.

React Query ensures error is meaningful only when isError is true. Checking error alone can render the error state during refetches or intermediate states if a stale error object lingers.

-    if (isError || error) {
+    if (isError) {
       return (
         <div className="flex items-center justify-center w-full h-full">
           <ErrorState
             className="max-w-4xl"
             title="Failed to load messages"
-            error={error}
+            error={error}
             description="Please try again later."
             density="comfy"
           />
         </div>
       );
     }

64-77: Similarly, gate streaming error UI on isStreamError only.

streamError should be treated as payload; the boolean flag should control the branch to avoid false positives if a stale value exists.

-    if (isStreamError || streamError) {
+    if (isStreamError) {
       return (
         <div className="flex items-center justify-center w-full h-full">
           <ErrorState
             className="max-w-4xl"
             density="comfy"
             description="Unable to load messages, this might be due to either a network issue or a server error."
             title="Error loading messages"
             error={streamError}
             showIcon={false}
           />
         </div>
       );
     }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 3e246db and c136900.

📒 Files selected for processing (8)
  • src/components/app-sidebar/index.tsx (8 hunks)
  • src/components/chat/input/document-list.tsx (3 hunks)
  • src/components/chat/messages/index.tsx (3 hunks)
  • src/components/chat/panels/projects/details.tsx (3 hunks)
  • src/components/chat/panels/projects/list.tsx (3 hunks)
  • src/components/topnav.tsx (4 hunks)
  • src/components/ui/accordion.tsx (1 hunks)
  • src/routes/__root.tsx (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/components/chat/panels/projects/details.tsx
  • src/components/chat/panels/projects/list.tsx
  • src/components/app-sidebar/index.tsx
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-01T07:34:38.459Z
Learnt from: CR
PR: 0bs-chat/zerobs#0
File: .cursor/rules/convex_rules.mdc:0-0
Timestamp: 2025-08-01T07:34:38.459Z
Learning: Applies to convex/**/*.{ts,js} : Use the api object from convex/_generated/api.ts to call public functions and the internal object to call internal functions.

Applied to files:

  • src/components/chat/input/document-list.tsx
🧬 Code graph analysis (4)
src/routes/__root.tsx (3)
src/store/chatStore.ts (1)
  • resizePanelOpenAtom (36-36)
src/components/ui/loading-spinner.tsx (1)
  • LoadingSpinner (11-35)
src/components/app-sidebar/index.tsx (1)
  • AppSidebar (35-245)
src/components/chat/messages/index.tsx (3)
src/store/chatStore.ts (1)
  • userLoadableAtom (11-11)
src/hooks/chats/use-messages.ts (1)
  • useMessages (20-66)
src/components/ui/loading-spinner.tsx (1)
  • LoadingSpinner (11-35)
src/components/topnav.tsx (2)
src/components/ui/loading-spinner.tsx (1)
  • LoadingSpinner (11-35)
src/components/theme-provider.tsx (1)
  • ModeToggle (69-86)
src/components/chat/input/document-list.tsx (3)
src/store/chatStore.ts (1)
  • documentDialogOpenAtom (43-45)
src/components/ui/loading-spinner.tsx (1)
  • LoadingSpinner (11-35)
src/components/ui/error-state.tsx (1)
  • ErrorState (122-122)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (8)
src/components/ui/accordion.tsx (1)

115-122: Prop reordering is safe; default remains explicit.

Moving showIcon to the front with showIcon = true keeps behavior clear and non-breaking for named props. No issues spotted.

src/routes/__root.tsx (2)

9-9: Spinner migration looks good.

Adopting the shared LoadingSpinner here aligns the route’s loading UI with the rest of the app.


92-92: Minor UI: spinner sizing reads well against full-screen container.

The sizeClassName choice keeps the loading state visually balanced. No action needed.

src/components/topnav.tsx (1)

107-111: Nice: lightweight loading affordance in the control cluster.

The compact LoadingSpinner integrates cleanly without shifting layout. No changes needed.

src/components/chat/input/document-list.tsx (2)

7-8: LGTM: central loading/error UI adoption.

Good use of the shared LoadingSpinner and ErrorState components for consistent UX.


85-86: Good: follows Convex rule to use generated api object.

Query is properly issued via api.documents.queries.getMultiple. Matches the team guideline in retrieved learnings.

src/components/chat/messages/index.tsx (2)

2-2: Nice addition: centralizing error UI via ErrorState.

Import looks correct and aligns with the PR goal of consistent error handling across components.


11-11: Loading spinner integration is consistent with shared UI.

Good reuse of the shared LoadingSpinner component.

Comment on lines +79 to 86
const {
data: documents,
isLoading: isLoadingDocuments,
isError: isDocumentsError,
error: documentsError,
} = useQuery({
...convexQuery(api.documents.queries.getMultiple, { documentIds }),
});
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

Fix early null-return that makes Loading/Error UI unreachable; also gate the query when no attachments.

if (!documents?.length) return null; at Line 105 short-circuits before checking isLoadingDocuments/isDocumentsError, so the spinner/error never render. There’s also a duplicate guard at Lines 110-111. Move the empty-docs guard to after the loading/error blocks and add enabled to the query so we don’t fetch (or show a spinner) when documentIds is empty.

Apply:

@@
-  } = useQuery({
-    ...convexQuery(api.documents.queries.getMultiple, { documentIds }),
-  });
+  } = useQuery({
+    ...convexQuery(api.documents.queries.getMultiple, { documentIds }),
+    // Avoid fetching when there are no attachments
+    enabled: documentIds.length > 0,
+  });
@@
-  if (!documents?.length) return null;
@@
-  if (!documents?.length) return null;
@@
   if (isLoadingDocuments) {
@@
   if (isDocumentsError || documentsError) {
@@
   }
+
+  // No attached documents to render
+  if (!documents?.length) return null;

Also applies to: 105-106, 110-111, 112-135

🤖 Prompt for AI Agents
In src/components/chat/input/document-list.tsx around lines 79 to 86 and
affecting 105-106, 110-111, 112-135: the useQuery currently always runs and the
component returns early when documents is empty which prevents loading/error UI
from ever showing; also there are duplicate guards. Update the useQuery call to
include an enabled flag that only runs when documentIds exists and has length >
0 (e.g., enabled: Boolean(documentIds?.length)), remove the duplicate empty-docs
guards, and move the `if (!documents?.length) return null` check to after
explicit checks for isLoadingDocuments and isDocumentsError so the spinner and
error UI render while the query is pending or failed.

- Removed unused GearIcon import from TopNav component.
- Simplified imports in AppSidebar by removing unnecessary icons and loading spinner.
- Adjusted layout properties for SidebarGroup and SidebarGroupLabel for better UI consistency.
- Removed outdated UX improvement note from temp.md.
- Updated mcps submodule to commit 2a33ac72eaa99ca4b87a2e85991523696de5dd1a.
- Made adjustments to the TopNav component by removing unused settings button and mode toggle for a cleaner interface.
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: 0

♻️ Duplicate comments (1)
src/components/topnav.tsx (1)

47-49: Early return with breaks hook order — move redirect into an effect

This conditional return causes hooks below (useEffect/useParams) to be skipped on error renders, violating the Rules of Hooks (Biome errors at Lines 51, 58, 64). Redirect via useEffect instead. This also mirrors the earlier review feedback.

Apply:

-  if (isErrorUser) {
-    return <Navigate to="/auth" />;
-  }
+  // Redirect to auth on user fetch error without breaking hooks order
+  useEffect(() => {
+    if (isErrorUser) {
+      navigate({ to: "/auth" });
+    }
+  }, [isErrorUser, navigate]);
🧹 Nitpick comments (6)
src/components/topnav.tsx (6)

18-23: Remove unused Navigate import after redirect refactor

Once the early return is removed, Navigate is no longer needed.

-import {
-  Navigate,
-  useLocation,
-  useNavigate,
-  useParams,
-} from "@tanstack/react-router";
+import { useLocation, useNavigate, useParams } from "@tanstack/react-router";

39-45: Harden auth query behavior to avoid thrash and unnecessary refetch

Auth failures can loop retries and trigger repeated redirects. Consider turning off retries and giving the user doc a short cache window.

-  } = useQuery({
-    ...convexQuery(api.auth.getUser, {}),
-  });
+  } = useQuery({
+    ...convexQuery(api.auth.getUser, {}),
+    retry: false,
+    staleTime: 60_000,
+  });

63-81: Guard global Cmd/Ctrl+I against text inputs/contentEditable

Prevents accidental panel toggles while typing in inputs or rich editors (Cmd/Ctrl+I is a common “italic” shortcut).

   const handleKeyDown = (event: KeyboardEvent) => {
-    if (
+    const target = event.target as HTMLElement | null;
+    if (target && (target.isContentEditable || ["INPUT", "TEXTAREA"].includes(target.tagName))) {
+      return;
+    }
+    if (
       !event.repeat &&
       event.key === "i" &&
       (event.metaKey || event.ctrlKey)
     ) {

120-129: Use functional updater to avoid stale state on atom toggle

Minor, but the functional form is safer and consistent with your keybinding handler.

-            onClick={() => {
-              setResizePanelOpen(!resizePanelOpen);
-              setSelectedArtifact(undefined);
-            }}
+            onClick={() => {
+              setResizePanelOpen(prev => !prev);
+              setSelectedArtifact(undefined);
+            }}

119-136: UX: Consider disabling (not hiding) the panel toggle when an artifact/MCP is selected

Hiding controls shifts layout and removes affordance; disabling with a tooltip preserves layout and sets user expectation.

Example (conceptual):

- {isOnChatRoute && (
+ {isOnChatRoute && (
     <Button
       variant="ghost"
       size="icon"
-      className={`${selectedArtifact || selectedVibzMcp ? "hidden" : ""}`}
+      className=""
+      disabled={!!(selectedArtifact || selectedVibzMcp)}
       aria-label="Toggle right panel"

83-139: Optional: Avoid nav flicker while user is loading

If you notice a brief topbar flash before redirect, you can hide the bar during initial auth resolution without breaking hook order (return occurs after all hooks).

 export function TopNav() {
   ...
-  return (
+  if (isLoadingUser) return null;
+  return (
     <div
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d003490 and 9792752.

📒 Files selected for processing (5)
  • src/components/chat/input/toolbar/index.tsx (3 hunks)
  • src/components/chat/messages/user-message.tsx (2 hunks)
  • src/components/topnav.tsx (1 hunks)
  • src/components/ui/accordion.tsx (1 hunks)
  • temp.md (0 hunks)
💤 Files with no reviewable changes (1)
  • temp.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/components/chat/messages/user-message.tsx
  • src/components/chat/input/toolbar/index.tsx
  • src/components/ui/accordion.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
src/components/topnav.tsx (5)
src/store/chatStore.ts (5)
  • resizePanelOpenAtom (36-36)
  • selectedArtifactAtom (53-53)
  • sidebarOpenAtom (34-34)
  • selectedVibzMcpAtom (54-54)
  • userAtom (10-10)
convex/_generated/api.js (2)
  • api (21-21)
  • api (21-21)
src/components/ui/sidebar.tsx (1)
  • SidebarTrigger (724-724)
src/components/ui/button.tsx (1)
  • Button (59-59)
src/components/theme-provider.tsx (1)
  • ModeToggle (69-86)
🪛 Biome (2.1.2)
src/components/topnav.tsx

[error] 51-51: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

Hooks should not be called after an early return.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)


[error] 58-58: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

Hooks should not be called after an early return.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)


[error] 64-64: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

Hooks should not be called after an early return.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)

🔇 Additional comments (1)
src/components/topnav.tsx (1)

51-55: Scripts are running… [waiting for results]

- Added stats.html to .gitignore to prevent tracking of generated files.
- Reformatted code in DocumentDialog for improved readability and consistency.
- Simplified user error handling in TopNav by moving error check to a more logical position.
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: 6

🧹 Nitpick comments (11)
src/components/document-dialog.tsx (7)

24-34: Confirm conditional query behavior; consider adding enabled to avoid accidental fetches

You rely on convexQuery(..., "skip"), which is fine if the wrapper handles the sentinel. To be safe with TanStack Query semantics and avoid any accidental fetches when the dialog is closed, add an explicit enabled flag keyed to documentDialogOpen.

   } = useQuery({
     ...convexQuery(
       api.documents.queries.get,
       documentDialogOpen ? { documentId: documentDialogOpen } : "skip",
     ),
+    enabled: !!documentDialogOpen,
   });

If convexQuery already guarantees no-op when passed "skip", this remains a no-op safety net. Please verify current wrapper behavior.


97-105: Broaden download support (image/pdf/audio/video) and guard with try/catch

Users often want to download images/PDFs/media too. Also add basic error handling around the mutation.

 const handleDownload = async () => {
-  if (!document || tag !== "file") return;
-  const url = await generateDownloadUrl({
-    documentId: document._id!,
-  });
-  if (url) {
-    window.open(url, "_blank");
-  }
+  if (!document) return;
+  // Skip external-only resources
+  if (tag === "url" || tag === "site" || tag === "youtube") return;
+  try {
+    const url = await generateDownloadUrl({
+      documentId: document._id!,
+    });
+    if (url) {
+      const w = window.open(url, "_blank", "noopener,noreferrer");
+      if (w) w.opener = null;
+    }
+  } catch (e) {
+    console.error("Failed to generate download url", e);
+  }
 };
 
 ...
 
-          {tag === "file" ? (
+          {(tag === "file" || tag === "pdf" || tag === "image" || tag === "audio" || tag === "video") ? (
             <Button onClick={handleDownload}>Download</Button>
           ) : tag === "url" || tag === "site" || tag === "youtube" ? (
             <Button onClick={handleOpen}>
               Open {tag === "youtube" ? "in YouTube" : "in Browser"}
             </Button>
           ) : null}
```<!-- review_comment_end -->



Also applies to: 261-267

---

`172-176`: **Show “0 Bytes” correctly when size is zero**

The current truthy check hides size 0. Use a null/undefined check instead.


```diff
-          {document?.size && (
+          {document?.size != null && (
             <div className="text-sm text-muted-foreground">
               Size: {formatBytes(document.size)}
             </div>
           )}
```<!-- review_comment_end -->

---

`202-212`: **Add title for PDF object for a11y**

Mirror the file fallback’s title and improve accessibility.


```diff
           <object
+            title="PDF preview"
             data={previewUrl}
             className="absolute inset-0 w-full h-full"
             type="application/pdf"
           >
```<!-- review_comment_end -->

---

`224-237`: **Revisit iframe sandbox policy for external sites**

Sandboxing with allow-same-origin + allow-scripts reduces isolation. If the content is untrusted and you don’t need DOM access, consider dropping allow-same-origin for stronger origin isolation. If you want links to work, add allow-popups (and optionally allow-top-navigation-by-user-activation). You may also want to add referrerPolicy="no-referrer".



Example:

```tsx
<iframe
  title="Website preview"
  src={previewUrl}
  className="absolute inset-0 w-full h-full"
  sandbox="allow-scripts allow-popups allow-top-navigation-by-user-activation"
  referrerPolicy="no-referrer"
/>

Please verify UX expectations (do links inside the frame need to open?) and adjust accordingly.


40-47: Minor naming consistency: use lowerCamelCase for iconClassName

Renaming improves consistency with typical React prop naming.

-  const {
-    icon: Icon,
-    className: IconClassName,
-    tag,
-  } = document
+  const {
+    icon: Icon,
+    className: iconClassName,
+    tag,
+  } = document
     ? getDocTagInfo(document)
     : { icon: () => null, className: "", tag: "" };
-            <Icon className={`${IconClassName} h-8 w-8`} />
+            <Icon className={`${iconClassName} h-8 w-8`} />
```<!-- review_comment_end -->



Also applies to: 160-161

---

`189-257`: **Optional: show a lightweight spinner while generating preview URLs**

Between document load and previewUrl resolution (signed URL generation), the preview area is blank. Surface a small inline spinner for better perceived performance.


```diff
-  const { mutateAsync: generateDownloadUrl } = useMutation({
+  const { mutateAsync: generateDownloadUrl, isPending: isGeneratingPreview } = useMutation({
     mutationFn: useConvexMutation(api.documents.mutations.generateDownloadUrl),
   });
-          {previewUrl && (
+          {previewUrl ? (
             <div className="flex-grow relative min-h-0 bg-muted rounded-md border overflow-hidden">
               {(() => {
                 if (tag === "image") {
                   return (
                     <img
                       src={previewUrl}
                       alt={documentName}
                       className="absolute inset-0 w-full h-full object-contain"
                     />
                   );
                 } else if (tag === "pdf") {
                   return (
                     <object
                       data={previewUrl}
                       className="absolute inset-0 w-full h-full"
                       type="application/pdf"
                     >
                       <div className="absolute inset-0 flex items-center justify-center text-muted-foreground">
                         PDF preview not supported in your browser. Please
                         download the file to view it.
                       </div>
                     </object>
                   );
                 } else if (tag === "youtube") {
                   return (
                     <iframe
                       title="YouTube video player"
                       src={previewUrl}
                       className="absolute inset-0 w-full h-full"
                       allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
                       allowFullScreen
                     />
                   );
                 } else if (tag === "url" || tag === "site") {
                   return (
                     <iframe
                       title="Website preview"
                       src={previewUrl}
                       className="absolute inset-0 w-full h-full"
                       sandbox="allow-same-origin allow-scripts"
                       style={{
                         transform: "scale(0.95)",
                         transformOrigin: "top left",
                         width: "105.3%", // 100/0.95 to compensate for scale
                         height: "105.3%",
                       }}
                     />
                   );
                 } else if (tag === "file") {
                   // fallback for unknown file types
                   return (
                     <object
                       title="File preview"
                       data={previewUrl}
                       className="absolute inset-0 w-full h-full"
                       type="application/octet-stream"
                     >
                       <div className="absolute inset-0 flex items-center justify-center text-muted-foreground">
                         File preview not supported. Please download the file to
                         view it.
                       </div>
                     </object>
                   );
                 }
                 return null;
               })()}
             </div>
-          )}
+          ) : !previewError ? (
+            <div className="flex-grow flex items-center justify-center bg-muted rounded-md border">
+              <LoadingSpinner className="h-5 w-5" label="Preparing preview..." />
+            </div>
+          ) : null}
```<!-- review_comment_end -->
<!-- file_end -->


Also applies to: 36-38

</blockquote></details>
<details>
<summary>src/components/topnav.tsx (4)</summary><blockquote>

`31-36`: **Also wire a setter for `selectedVibzMcp` (to clear it when opening the right panel)**

Hotkey and button clear `selectedArtifact` before opening the panel, but not `selectedVibzMcp`. Add its setter for consistency.



```diff
-  const selectedVibzMcp = useAtomValue(selectedVibzMcpAtom);
+  const selectedVibzMcp = useAtomValue(selectedVibzMcpAtom);
+  const setSelectedVibzMcp = useSetAtom(selectedVibzMcpAtom);

52-71: Hotkey: unify selection clearing and dependency list

When toggling open via Ctrl/Cmd+I, clear both selectedArtifact and selectedVibzMcp so the panel opens in a clean state. Also include the new setter in the effect deps.

   useEffect(() => {
     if (!isOnChatRoute) return;
     const handleKeyDown = (event: KeyboardEvent) => {
       if (
         !event.repeat &&
         event.key === "i" &&
         (event.metaKey || event.ctrlKey)
       ) {
         event.preventDefault();
         setResizePanelOpen((open) => {
-          if (!open) setSelectedArtifact(undefined);
+          if (!open) {
+            setSelectedArtifact(undefined);
+            setSelectedVibzMcp(undefined);
+          }
           return !open;
         });
       }
     };
     window.addEventListener("keydown", handleKeyDown);
     return () => window.removeEventListener("keydown", handleKeyDown);
-  }, [isOnChatRoute, setResizePanelOpen, setSelectedArtifact]);
+  }, [isOnChatRoute, setResizePanelOpen, setSelectedArtifact, setSelectedVibzMcp]);

112-129: Use functional state update and clear both selections when toggling via button

Functional updates prevent stale-closure issues. Clear both selections to mirror the hotkey behavior.

   {isOnChatRoute && (
     <Button
       variant="ghost"
       size="icon"
       className={`${selectedArtifact || selectedVibzMcp ? "hidden" : ""}`}
       aria-label="Toggle right panel"
       onClick={() => {
-        setResizePanelOpen(!resizePanelOpen);
-        setSelectedArtifact(undefined);
+        setResizePanelOpen((prev) => !prev);
+        setSelectedArtifact(undefined);
+        setSelectedVibzMcp(undefined);
       }}
     >

Accessibility nitpick (optional):

  • Add aria-pressed={resizePanelOpen} or aria-expanded={resizePanelOpen} and aria-controls if the right panel has an id.

77-83: Tame complex className strings with a utility for readability

Long template strings are hard to diff and reason about. Consider using a cn/clsx helper or a variant factory (e.g., cva) to compose conditional classes.

Example (illustrative):

import { cn } from "@/lib/utils";

<div
  className={cn(
    "fixed right-0 z-50 flex w-full items-center justify-between bg-transparent py-2 px-1.5 pointer-events-none text-foreground/70",
    isSettingsRoute && "hidden"
  )}
/>

Also applies to: 97-99

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 9792752 and 4716dd0.

📒 Files selected for processing (3)
  • .gitignore (2 hunks)
  • src/components/document-dialog.tsx (1 hunks)
  • src/components/topnav.tsx (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • .gitignore
🧰 Additional context used
🧬 Code graph analysis (2)
src/components/document-dialog.tsx (6)
src/store/chatStore.ts (1)
  • documentDialogOpenAtom (43-45)
convex/documents/mutations.ts (1)
  • generateDownloadUrl (22-39)
src/lib/helper.tsx (1)
  • getDocTagInfo (136-174)
src/components/ui/loading-spinner.tsx (1)
  • LoadingSpinner (11-35)
src/components/ui/error-state.tsx (1)
  • ErrorState (122-122)
src/lib/utils.ts (1)
  • formatBytes (8-16)
src/components/topnav.tsx (3)
src/store/chatStore.ts (5)
  • resizePanelOpenAtom (36-36)
  • selectedArtifactAtom (53-53)
  • sidebarOpenAtom (34-34)
  • selectedVibzMcpAtom (54-54)
  • userAtom (10-10)
convex/_generated/api.js (2)
  • api (21-21)
  • api (21-21)
src/components/theme-provider.tsx (1)
  • ModeToggle (69-86)
🔇 Additional comments (2)
src/components/topnav.tsx (2)

72-74: Past concern resolved: hooks are no longer conditional

The <Navigate /> early return comes after all hooks. Hook order remains stable across renders. Good fix relative to the earlier review.


46-51: Clear stale user on auth error in TopNav

Apply the following update in src/components/topnav.tsx to ensure the global store doesn’t retain a signed-in user when the auth query fails:

   useEffect(() => {
-    if (user) {
-      setUser(user);
-    }
+    if (user) {
+      setUser(user);
+    } else if (isErrorUser) {
+      // Clear stale user on auth failure
+      setUser(undefined as any);
+    }
   }, [user, isErrorUser, setUser]);

• Verify that userAtom in src/store/chatStore.ts permits undefined (e.g. atom<Doc<"users"> | undefined>(undefined)); if it doesn’t, update its type signature accordingly so clearing it won’t cause type errors.
• This change prevents the UI from displaying a stale “signed-in” state when authentication fails.

Comment on lines +42 to +44
const { data: user, isError: isErrorUser } = useQuery({
...convexQuery(api.auth.getUser, {}),
});
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Avoid logging users out on any fetch error — redirect only on auth errors

isErrorUser is true for all failures (network blips, 5xx, timeouts). Redirecting unconditionally to /auth will kick users out spuriously. Capture error from the query and gate the redirect on an auth-specific condition. Also consider basic query hygiene (retry, staleTime).

Apply this diff to capture the error and add query options:

-  const { data: user, isError: isErrorUser } = useQuery({
-    ...convexQuery(api.auth.getUser, {}),
-  });
+  const {
+    data: user,
+    error: userError,
+    isError: isErrorUser,
+  } = useQuery({
+    ...convexQuery(api.auth.getUser, {}),
+    // Avoid looping on auth errors; tolerate brief transient failures.
+    retry: (failureCount, err) => !isUnauthorized(err) && failureCount < 2,
+    staleTime: 5 * 60 * 1000,
+  });

Add this helper in the module (outside the component) to detect auth errors (adjust to your error shape as needed):

function isUnauthorized(err: unknown) {
  const e = err as any;
  return e?.response?.status === 401 || e?.status === 401 || e?.code === "UNAUTHENTICATED";
}

And update the redirect check below (see lines 72–74 comment).

Comment on lines +72 to +74
if (isErrorUser) {
return <Navigate to="/auth" />;
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Scope the redirect to unauthorized errors only

Keep the component-level <Navigate />, but only when the error indicates an auth failure.

Apply this diff:

-  if (isErrorUser) {
-    return <Navigate to="/auth" />;
-  }
+  if (isErrorUser && isUnauthorized(userError)) {
+    return <Navigate to="/auth" />;
+  }
📝 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
if (isErrorUser) {
return <Navigate to="/auth" />;
}
if (isErrorUser && isUnauthorized(userError)) {
return <Navigate to="/auth" />;
}
🤖 Prompt for AI Agents
In src/components/topnav.tsx around lines 72-74, the component currently
redirects to /auth for any error; change this so the <Navigate /> is only
returned when the error explicitly indicates an authorization failure (e.g.,
error.status === 401 or an isUnauthorized flag from your auth/error hook).
Update the conditional from isErrorUser to check for unauthorized (for example:
isErrorUser && (userError?.status === 401 || userError?.code === 'UNAUTHORIZED'
|| isUnauthorized)), leaving other errors to render their normal error UI or
messages.

- Added new dependencies for @dnd-kit components to support drag-and-drop functionality.
- Updated versions for @tanstack/react-query and @tanstack/query-core for improved performance and features.
- Removed unused imports and error handling in DocumentDialog for cleaner code.
- Added preview error state management in DocumentDialog to enhance user feedback.
- Updated TopNav to include retry logic for user query and improved keydown event handling.
- Simplified settings button rendering in TopNav for better accessibility.
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: 1

♻️ Duplicate comments (2)
src/components/topnav.tsx (1)

45-49: Prevent stale user after logout/401; clear the store and optionally redirect

If the request 401s, the userAtom will retain the previous (stale) user because we only set on truthy user. Clear on falsy user and consider redirecting to /auth on explicit unauthorized errors via an effect. If a higher-level route guard already handles the redirect, still clear the store here.

 useEffect(() => {
   if (user) {
     setUser(user);
   }
-}, [user, setUser]);
+  else {
+    // Clear stale user (e.g., after logout or 401).
+    setUser(undefined);
+  }
+}, [user, setUser]);
+
+// Redirect only on explicit unauthorized errors.
+useEffect(() => {
+  if (isErrorUser && isUnauthorized(userError)) {
+    setUser(undefined);
+    navigate({ to: "/auth", replace: true });
+  }
+}, [isErrorUser, userError, navigate, setUser]);
src/components/document-dialog.tsx (1)

44-89: Make preview URL loading race-safe and include video/audio in signed-URL path

Prevent stale updates when switching documents quickly and add support for video/audio. Also fix the comment to reflect the broader set of stored objects that require a signed URL.

 useEffect(() => {
-		setPreviewUrl(null);
-		setPreviewError(null);
-		const loadPreviewUrl = async () => {
+		setPreviewUrl(null);
+		setPreviewError(null);
+		let cancelled = false;
+		const loadPreviewUrl = async () => {
 			if (!document) return;
 			try {
 				switch (tag) {
 					case "image":
 					case "pdf":
+					case "video":
+					case "audio":
 					case "file": {
-						// Only files need download URL
+						// Stored objects need a signed download URL
 						const url = await generateDownloadUrl({
 							documentId: document._id!,
 						});
-						setPreviewUrl(url);
+						if (!cancelled) setPreviewUrl(url);
 						break;
 					}
 					case "url":
 					case "site": {
-						setPreviewUrl(document.key as string);
+						if (!cancelled) setPreviewUrl(document.key as string);
 						break;
 					}
 					case "youtube": {
-						setPreviewUrl(`https://www.youtube.com/embed/${document.key}`);
+						if (!cancelled)
+							setPreviewUrl(`https://www.youtube.com/embed/${document.key}`);
 						break;
 					}
 					default:
 						if (["file", "text", "github"].includes(document.type)) {
 							const url = await generateDownloadUrl({
 								documentId: document._id!,
 							});
-							setPreviewUrl(url);
+							if (!cancelled) setPreviewUrl(url);
 							break;
 						} else {
-							setPreviewUrl(document.key as string);
+							if (!cancelled) setPreviewUrl(document.key as string);
 						}
 						break;
 				}
 			} catch (error) {
-				setPreviewError(
-					`Failed to load preview: ${error instanceof Error ? error.message : "Unknown error"}`,
-				);
+				if (!cancelled) {
+					setPreviewError(
+						`Failed to load preview: ${error instanceof Error ? error.message : "Unknown error"}`,
+					);
+				}
 			}
 		};
 		loadPreviewUrl();
-	}, [document, tag, generateDownloadUrl]);
+		return () => {
+			cancelled = true;
+		};
+	}, [document, tag, generateDownloadUrl]);
🧹 Nitpick comments (9)
src/components/topnav.tsx (4)

34-43: Harden unauthorized detection and expose error/isError from the query

The current isUnauthorized check is brittle (string search in message) and the query doesn’t expose error/isError for downstream handling. Make the guard robust across common error shapes and surface error/isError so you can react to 401s deterministically.

-	function isUnauthorized(err: unknown) {
-		return err instanceof Error && err.message.includes("401");
-	}
+	function isUnauthorized(err: unknown): boolean {
+		if (!err) return false;
+		const e: any = err;
+		const status = e?.status ?? e?.response?.status ?? e?.statusCode;
+		const code = e?.code ?? e?.data?.code ?? e?.response?.data?.code;
+		const message: string =
+			typeof e?.message === "string" ? e.message : "";
+		return (
+			status === 401 ||
+			code === "UNAUTHENTICATED" ||
+			code === "UNAUTHORIZED" ||
+			/(^|[^0-9])401([^0-9]|$)/.test(message) ||
+			/unauth/i.test(message)
+		);
+	}
 
-	const { data: user } = useQuery({
+	const {
+		data: user,
+		error: userError,
+		isError: isErrorUser,
+	} = useQuery({
 		...convexQuery(api.auth.getUser, {}),
 		// Avoid looping on auth errors; tolerate brief transient failures.
 		retry: (failureCount, err) => !isUnauthorized(err) && failureCount < 2,
 		staleTime: 5 * 60 * 1000,
 	});

57-75: Don’t hijack typing: ignore hotkey when focus is in inputs/contentEditable

The global Ctrl/Cmd+I listener currently fires even when the user is typing in an input/textarea or a contentEditable. Add a guard to avoid disrupting text entry.

   useEffect(() => {
     if (!isOnChatRoute) return;
     const handleKeyDown = (event: KeyboardEvent) => {
-      if (
+      if (
         !event.repeat &&
         event.key === "i" &&
         (event.metaKey || event.ctrlKey)
       ) {
+        // Ignore when user is typing in inputs or contentEditable fields.
+        const target = event.target as HTMLElement | null;
+        if (target?.closest("input, textarea, [contenteditable='true']")) {
+          return;
+        }
         event.preventDefault();
         setResizePanelOpen((open) => {
           if (!open) setSelectedArtifact(undefined);
           return !open;
         });
       }
     };

116-126: Use functional state update and clear artifact only on open; add aria-pressed

Align the click toggle with the keyboard handler (clear only when opening) and avoid potential stale reads by using the functional updater. aria-pressed improves a11y for toggle buttons.

-				{isOnChatRoute && (
-					<Button
+				{isOnChatRoute && (
+					<Button
 						variant="ghost"
 						size="icon"
-						className={`${selectedArtifact || selectedVibzMcp ? "hidden" : ""}`}
+						className={`${selectedArtifact || selectedVibzMcp ? "hidden" : ""}`}
 						aria-label="Toggle right panel"
-						onClick={() => {
-							setResizePanelOpen(!resizePanelOpen);
-							setSelectedArtifact(undefined);
-						}}
+						aria-pressed={resizePanelOpen}
+						onClick={() => {
+							setResizePanelOpen((prev) => {
+								const next = !prev;
+								if (next && !prev) setSelectedArtifact(undefined);
+								return next;
+							});
+						}}
 					>

77-83: Add navigation landmark semantics for accessibility

Expose the top bar as a navigation landmark for better screen reader navigation.

-		<div
+		<div role="navigation" aria-label="Top navigation"
 			className={`fixed right-0 py-2 flex items-center w-full bg-transparent justify-between pointer-events-none text-foreground/70 z-50 px-1.5 ${isSettingsRoute ? "hidden" : ""}`}
 		>
src/components/document-dialog.tsx (5)

96-105: Handle download failures to surface errors to users

Wrap generateDownloadUrl and window.open in try/catch and display a friendly error via previewError.

 const handleDownload = async () => {
 	if (!document || tag !== "file") return;
-	const url = await generateDownloadUrl({
-		documentId: document._id!,
-	});
-	if (url) {
-		const w = window.open(url, "_blank", "noopener,noreferrer");
-		if (w) w.opener = null;
-	}
+	try {
+		const url = await generateDownloadUrl({ documentId: document._id! });
+		if (url) {
+			const w = window.open(url, "_blank", "noopener,noreferrer");
+			if (w) w.opener = null;
+		}
+	} catch (e) {
+		setPreviewError(
+			`Failed to download: ${e instanceof Error ? e.message : "Unknown error"}`,
+		);
+	}
 };

23-29: Expose loading/error from useQuery so the dialog can render proper states

You’re already standardizing loading/error across the app. Surface these here to avoid silent failures or infinite spinners when the document fetch fails.

-	const { data: document } = useQuery({
+	const {
+		data: document,
+		error: documentError,
+		isError: isDocumentError,
+		isLoading: isDocumentLoading,
+	} = useQuery({
 		...convexQuery(
 			api.documents.queries.get,
 			documentDialogOpen ? { documentId: documentDialogOpen } : "skip",
 		),
 		enabled: !!documentDialogOpen,
 	});

153-227: Render standardized ErrorState and a LoadingSpinner; add video/audio previews; add iframe referrer policy

Use the new primitives for consistency, preview video/audio when available, and reduce referrer leakage from iframes.

-					{previewError ? (
-						<div className="flex-grow flex items-center justify-center bg-muted rounded-md border">
-							<div className="text-center text-muted-foreground">
-								{previewError}
-							</div>
-						</div>
-					) : previewUrl ? (
+					{isDocumentError ? (
+						<div className="flex-grow flex items-center justify-center bg-muted rounded-md border">
+							<ErrorState
+								density="comfy"
+								title="Unable to load document"
+								error={documentError}
+							/>
+						</div>
+					) : previewError ? (
+						<div className="flex-grow flex items-center justify-center bg-muted rounded-md border">
+							<ErrorState
+								density="comfy"
+								title="Unable to preview document"
+								error={previewError}
+							/>
+						</div>
+					) : previewUrl ? (
 						<div className="flex-grow relative min-h-0 bg-muted rounded-md border overflow-hidden">
 							{(() => {
 								if (tag === "image") {
 									return (
 										<img
 											src={previewUrl}
 											alt={documentName}
 											className="absolute inset-0 w-full h-full object-contain"
 										/>
 									);
 								} else if (tag === "pdf") {
 									return (
 										<object
 											data={previewUrl}
 											className="absolute inset-0 w-full h-full"
 											type="application/pdf"
 										>
 											<div className="absolute inset-0 flex items-center justify-center text-muted-foreground">
 												PDF preview not supported in your browser. Please
 												download the file to view it.
 											</div>
 										</object>
 									);
+								} else if (tag === "video") {
+									return (
+										<video
+											src={previewUrl}
+											className="absolute inset-0 w-full h-full"
+											controls
+										/>
+									);
+								} else if (tag === "audio") {
+									return (
+										<audio
+											src={previewUrl}
+											className="absolute inset-x-0 bottom-4 w-[95%] mx-auto"
+											controls
+										/>
+									);
 								} else if (tag === "youtube") {
 									return (
 										<iframe
 											title={`YouTube video: ${documentName}`}
 											src={previewUrl}
 											className="absolute inset-0 w-full h-full"
 											allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
+											referrerPolicy="no-referrer"
 											allowFullScreen
 										/>
 									);
 								} else if (tag === "url" || tag === "site") {
 									return (
 										<iframe
 											title={`Website preview: ${documentName}`}
 											src={previewUrl}
 											className="absolute inset-0 w-full h-full"
 											sandbox="allow-same-origin allow-scripts"
+											referrerPolicy="no-referrer"
 											style={{
 												transform: "scale(0.95)",
 												transformOrigin: "top left",
 												width: "105.3%", // 100/0.95 to compensate for scale
 												height: "105.3%",
 											}}
 										/>
 									);
 								} else if (tag === "file") {
 									// fallback for unknown file types
 									return (
 										<object
 											data={previewUrl}
 											className="absolute inset-0 w-full h-full"
 											type="application/octet-stream"
 										>
 											<div className="absolute inset-0 flex items-center justify-center text-muted-foreground">
 												File preview not supported. Please download the file to
 												view it.
 											</div>
 										</object>
 									);
 								}
 								return null;
 							})()}
 						</div>
-					) : null}
+					) : (
+						<div
+							className="flex-grow flex items-center justify-center bg-muted rounded-md border"
+							aria-busy
+						>
+							<LoadingSpinner />
+						</div>
+					)}

Note: You’ll need to import the shared primitives if not already:

 import { Button } from "@/components/ui/button";
+import { ErrorState } from "@/components/ui/error-state";
+import { LoadingSpinner } from "@/components/ui/loading-spinner";

146-150: Show size when it’s 0 bytes as well

Guard on defined-ness rather than truthiness so 0 B is rendered.

-						{document?.size && (
+						{typeof document?.size === "number" && (
 							<div className="text-sm text-muted-foreground">
 								Size: {formatBytes(document.size)}
 							</div>
 						)}

131-137: Minor: Add file type to the heading for clarity (optional)

Showing the tag or a human label next to the name improves context when the preview fails.

-					<DialogTitle className="flex items-center gap-2">
+					<DialogTitle className="flex items-center gap-2">
 						<Icon className={`${IconClassName} h-8 w-8`} />
-						<span>{documentName}</span>
+						<span>{documentName}</span>
+						{tag && <span className="text-muted-foreground text-sm">({tag})</span>}
 					</DialogTitle>
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between bb9cdd8 and 5ffe8a7.

📒 Files selected for processing (2)
  • src/components/document-dialog.tsx (2 hunks)
  • src/components/topnav.tsx (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/components/document-dialog.tsx (4)
src/store/chatStore.ts (1)
  • documentDialogOpenAtom (44-46)
convex/documents/mutations.ts (1)
  • generateDownloadUrl (22-39)
src/lib/helper.tsx (1)
  • getDocTagInfo (136-174)
src/lib/utils.ts (1)
  • formatBytes (8-16)
src/components/topnav.tsx (3)
src/store/chatStore.ts (5)
  • resizePanelOpenAtom (37-37)
  • selectedArtifactAtom (54-54)
  • sidebarOpenAtom (35-35)
  • selectedVibzMcpAtom (55-55)
  • userAtom (11-11)
convex/_generated/api.js (2)
  • api (21-21)
  • api (21-21)
src/components/theme-provider.tsx (1)
  • ModeToggle (69-86)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (2)
src/components/topnav.tsx (1)

51-55: Route detection: verify chat route assumption

Using useParams({ strict: false }) and !!params.chatId is fine if chatId is reliably present only on chat routes. If params shape changes or chat routes get nested differently, consider a route match check for resilience (e.g., useMatch({ to: "/chat/$chatId", fuzzy: false })).

src/components/document-dialog.tsx (1)

107-124: LGTM on reverse tabnabbing hardening

Nice use of "noopener,noreferrer" and clearing opener in both branches.

- Added URL validation in DocumentDialog to ensure only safe links are processed.
- Improved error handling for invalid URLs in DocumentDialog's preview functionality.
- Updated TopNav to navigate to the authentication page on user query errors.
- Refactored code for better readability and consistency across components.
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: 6

♻️ Duplicate comments (3)
src/components/topnav.tsx (1)

76-78: Redirect only on auth errors; avoid logging users out on transient failures

Currently any fetch error triggers a redirect to /auth. This will bounce users on network blips, 5xx, timeouts, etc. Capture the query error and gate the redirect on an auth-specific check.

Apply this diff to scope the redirect and plumb the error through:

-const { data: user, isError: isErrorUser } = useQuery({
+const {
+  data: user,
+  error: userError,
+  isError: isErrorUser,
+} = useQuery({
   ...convexQuery(api.auth.getUser, {}),
   // Avoid looping on auth errors; tolerate brief transient failures.
   retry: (failureCount, err) => !isUnauthorized(err) && failureCount < 2,
   staleTime: 5 * 60 * 1000,
 });
 
-if (isErrorUser) {
-  return <Navigate to="/auth" />;
-}
+if (isErrorUser && isUnauthorized(userError)) {
+  return <Navigate to="/auth" />;
+}

If you prefer to avoid early returns entirely, navigate in an effect instead and render normal UI while the redirect is in flight. I can provide that variant if helpful.

src/components/document-dialog.tsx (2)

94-95: Validate URLs in the default case

The default case sets document.key directly as the preview URL without validation. This could be a security risk if the key contains malicious URLs.

 } else {
-  setPreviewUrl(document.key as string);
+  const externalUrl = document.key as string;
+  if (isValidExternalUrl(externalUrl)) {
+    setPreviewUrl(externalUrl);
+  } else {
+    setPreviewError("Invalid or unsafe URL");
+  }
 }

54-105: Add cleanup to prevent state updates after unmount

The useEffect for loading preview URLs should include a cleanup mechanism to prevent setting state after the component unmounts or when the document changes rapidly.

 useEffect(() => {
   setPreviewUrl(null);
   setPreviewError(null);
+  let cancelled = false;
   const loadPreviewUrl = async () => {
     if (!document) return;
     try {
       switch (tag) {
         case "image":
         case "pdf":
         case "file": {
           // Only files need download URL

           const url = await generateDownloadUrl({
             documentId: document._id,
           });
-          setPreviewUrl(url);
+          if (!cancelled) setPreviewUrl(url);
           break;
         }
         case "url":
         case "site": {
           const externalUrl = document.key as string;
           if (isValidExternalUrl(externalUrl)) {
-            setPreviewUrl(externalUrl);
+            if (!cancelled) setPreviewUrl(externalUrl);
           } else {
-            setPreviewError("Invalid or unsafe URL");
+            if (!cancelled) setPreviewError("Invalid or unsafe URL");
           }
           break;
         }
         case "youtube": {
-          setPreviewUrl(`https://www.youtube.com/embed/${document.key}`);
+          if (!cancelled) setPreviewUrl(`https://www.youtube.com/embed/${document.key}`);
           break;
         }
         default:
           if (["file", "text", "github"].includes(document.type)) {
             const url = await generateDownloadUrl({
               documentId: document._id,
             });
-            setPreviewUrl(url);
+            if (!cancelled) setPreviewUrl(url);
             break;
           } else {
-            setPreviewUrl(document.key as string);
+            if (!cancelled) setPreviewUrl(document.key as string);
           }
           break;
       }
     } catch (error) {
-      setPreviewError(
-        `Failed to load preview: ${error instanceof Error ? error.message : "Unknown error"}`,
-      );
+      if (!cancelled) {
+        setPreviewError(
+          `Failed to load preview: ${error instanceof Error ? error.message : "Unknown error"}`,
+        );
+      }
     }
   };
   loadPreviewUrl();
+  return () => {
+    cancelled = true;
+  };
 }, [document, tag, generateDownloadUrl]);
🧹 Nitpick comments (20)
src/components/ui/tooltip-button.tsx (4)

18-23: Type safety: Use React’s MouseEventHandler for onClick.

onClick is currently typed as a no-arg function, which loses event typing and blocks access to event params. Use React.MouseEventHandler for correctness.

-  onClick: () => void;
+  onClick: React.MouseEventHandler<HTMLButtonElement>;

31-31: A11y: Ensure an accessible name even when ariaLabel isn’t provided.

For an icon-only button, the accessible name must be present. If ariaLabel is omitted, fallback to tooltip for aria-label to avoid a silent control.

-          aria-label={ariaLabel}
+          aria-label={ariaLabel ?? tooltip}

27-33: Prevent accidental form submissions and pointer-cursor on disabled.

  • Add type="button" to avoid unintended form submissions when this button is placed inside a form.
  • Don’t force cursor-pointer when disabled; either remove the class or switch to cursor-not-allowed conditionally (addressed in the main fix).
-          variant="ghost"
-          size="icon"
+          variant="ghost"
+          size="icon"
+          type="button"
-          className="cursor-pointer"

24-25: Lift TooltipProvider to a global provider to eliminate redundant mounts

We’ve found multiple local mounts of TooltipProvider without an app-level wrapper, which leads to unnecessary nesting and run-time overhead. It’s safe to assume a single, top-level TooltipProvider exists and remove the per-component wrappers (or, if you prefer local defaults, document that nesting is supported).

Locations to update:

  • src/components/ui/tooltip-button.tsx (lines 24–25): <TooltipProvider> around each button
  • src/components/ui/tooltip.tsx (lines 23–25): provider wrapped around the primitive root
  • src/components/ui/sidebar.tsx (line 131): provider around sidebar tooltips
  • src/components/chat/messages/ai-message/index.tsx (lines 64–65): provider per AI message
  • src/components/chat/messages/utils-bar/copy-button.tsx (lines 27–28): provider per copy button

Suggested refactor:

  1. In your root provider (e.g. src/components/theme-provider.tsx or App.tsx), wrap the app in a single <TooltipProvider>:
    export default function ThemeProvider({ children }) {
      return (
    +   <TooltipProvider delayDuration={0}>
          {/* existing theming/context providers */}
          {children}
    +   </TooltipProvider>
      );
    }
  2. Remove the local <TooltipProvider> wrappers from the files listed above.
  3. (Optional) In components that still include a provider, add a comment:
    // Nesting TooltipProvider is supported but not required if a global provider exists
src/components/chat/messages/utils-bar/index.ts (4)

24-31: Track mutation with a stable key and surface status/error for the new loading/error UX.

Adding a mutationKey improves React Query devtools visibility and deduping. Also, consider exposing isPending/error so callers can hook into LoadingSpinner/ErrorState.

Apply this focused diff:

- const { mutateAsync: updateChatMutation } = useMutation({
-   mutationFn: useConvexMutation(api.chats.mutations.update),
- });
+ const {
+   mutateAsync: updateChatMutation,
+   isPending: isUpdatingChatModel,
+   error: updateChatError,
+ } = useMutation({
+   mutationKey: ["chats.update"],
+   mutationFn: useConvexMutation(api.chats.mutations.update),
+ });

And return these in the hook result (see lines 70-75 suggestion).


53-68: Drop unnecessary optional chaining and add error handling around model updates and regeneration.

  • useNavigateBranch returns a function; optional chaining isn’t needed.
  • Wrap updateChatMutation/regenerate so callers can render ErrorState on failure.

Apply this diff:

- const handleRegenerate = async (
+ const handleRegenerate = async (
   input: MessageWithBranchInfo,
   model?: string,
 ) => {
-   navigateBranch?.(input.depth, "next", input.totalBranches);
-   // If the model is provided, update the chat with the new model or
-   if (model) {
-     await updateChatMutation({
-       chatId: input.message.chatId,
-       updates: { model },
-     });
-   }
-   await regenerate({
-     messageId: input.message._id,
-   });
+   try {
+     navigateBranch(input.depth, "next", input.totalBranches);
+     if (model) {
+       await updateChatMutation({
+         chatId: input.message.chatId,
+         updates: { model },
+       });
+     }
+     await regenerate({ messageId: input.message._id });
+   } catch (err) {
+     // Bubble up so the caller can render ErrorState
+     throw err;
+   }
 };

70-75: Expose mutation status/error so callers can plug into LoadingSpinner/ErrorState.

Given the PR’s UX goals, returning isUpdatingChatModel and updateChatError will let consumers show spinners and error UIs during regenerate-with-model-change flows.

Apply this diff (pairs with the change suggested on lines 24-31):

 return {
   handleBranch,
   handleRegenerate,
   navigate,
   navigateBranch,
+  isUpdatingChatModel,
+  updateChatError,
 };

33-51: Optional: Enhance handleBranch with error propagation and confirm non-null chatId

I’ve verified that MessageWithBranchInfo comes from convex/chatMessages/helpers.ts and that its message property is a MessageNode (an Omit<Doc<"chatMessages">,…>) which always includes a non-nullable chatId: Id<"chats"> (no chatId? in the repo) so you don’t need any runtime guard for undefined there. To align with the PR’s ErrorState pattern and ensure any failures bubble up properly, you can optionally wrap your branchChat/chat calls in a try/catch (and await the chat call).

• Location: src/components/chat/messages/utils-bar/index.ts (around lines 33–51)

- const handleBranch = async (
+ const handleBranch = async (
   input: MessageWithBranchInfo,
   model?: string,
   editedContent?: { text?: string; documents?: Id<"documents">[] },
 ) => {
-   const result = await branchChat({
-     chatId: input.message.chatId,
-     branchFrom: input.message._id,
-     ...(model && { model }),
-     ...(editedContent && { editedContent }),
-   });
-   if (result?.newChatId) {
-     // Model is already persisted by branchChat; start chat without forwarding model
-     chat({
-       chatId: result.newChatId,
-     });
-     navigateToChat(navigate, result.newChatId);
-   }
+   try {
+     const result = await branchChat({
+       chatId: input.message.chatId,
+       branchFrom: input.message._id,
+       ...(model && { model }),
+       ...(editedContent && { editedContent }),
+     });
+     if (result?.newChatId) {
+       // Model is already persisted by branchChat; start chat without forwarding model
+       await chat({ chatId: result.newChatId });
+       navigateToChat(navigate, result.newChatId);
+     }
+   } catch (err) {
+     // Bubble up so the caller can render ErrorState
+     throw err;
+   }
 };

This change is purely optional but will make sure any branch/chat failures surface correctly in your UI.

src/components/chat/messages/utils-bar/user-utils-bar.tsx (6)

114-130: Await mutation before closing edit UI to avoid race conditions.

onDone?.() fires immediately; navigation and chat() run in a .then, so the UI may close before the update/branch is applied. Make handleSubmit async, await the mutation (and optional chat), then call onDone.

-    const handleSubmit = (applySame: boolean) => {
+    const handleSubmit = async (applySame: boolean) => {
       if (!editedText) return;
 
       if (applySame === false) {
         navigateBranch?.(input.depth, "next", input.totalBranches);
       }
-      updateMessage({
+      await updateMessage({
         id: input.message._id as Id<"chatMessages">,
         updates: { text: editedText, documents: editedDocuments || [] },
         applySame: applySame,
-      }).then(() => {
-        if (applySame === false) {
-          chat({ chatId: input.message.chatId });
-        }
-      });
-      onDone?.();
+      });
+      if (applySame === false) {
+        await chat({ chatId: input.message.chatId });
+      }
+      onDone?.();
     };

58-68: Reset file input after upload to allow re-selecting the same file; consider accept filter.

Without resetting, picking the same file again won't trigger onChange. Also, consider an accept attribute (if you want to constrain types).

   const handleFileInputChange = useCallback(
     async (e: React.ChangeEvent<HTMLInputElement>) => {
       if (e.target.files && e.target.files.length > 0) {
         const uploadedIds = await handleFileUpload(e.target.files);
         if (uploadedIds && onDocumentsChange) {
           onDocumentsChange([...(editedDocuments || []), ...uploadedIds]);
         }
       }
+      // Allow selecting the same file again
+      e.target.value = "";
     },
     [handleFileUpload, editedDocuments, onDocumentsChange],
   );
   <input
     type="file"
     ref={fileInputRef}
     className="hidden"
     multiple
+    /* e.g., accept common doc types; adjust as needed */
+    accept=".pdf,.txt,.md,.doc,.docx,.csv,.json,image/*"
     onChange={handleFileInputChange}
   />

Also applies to: 140-146


84-90: Polish: indicate copy-drop intent during dragover.

Setting dropEffect improves UX and communicates intent to the OS.

   (e: React.DragEvent<HTMLDivElement>) => {
     e.preventDefault();
+    if (e.dataTransfer) e.dataTransfer.dropEffect = "copy";
     if (!isDragActive) setIsDragActive(true);
   },

147-151: Add aria-labels to icon-only buttons for accessibility.

Tooltip text isn't announced by screen readers; add ariaLabel.

 <TooltipButton
   onClick={() => setEditing?.(null)}
   icon={<X className="h-4 w-4 text-foreground/70 cursor-pointer " />}
   tooltip="Cancel"
+  ariaLabel="Cancel"
 />
 <TooltipButton
   onClick={() => fileInputRef.current?.click()}
   icon={<Paperclip className="h-4 w-4 text-foreground/70" />}
   tooltip="Attach files"
+  ariaLabel="Attach files"
 />

Also applies to: 196-201


101-112: Optional: memoize and centralize text extraction.

The IIFE runs every render and duplicates content-parsing logic seen elsewhere. Consider a small utility (and useMemo) to extract text safely and reuse across components.

Example:

const copyText = useMemo(() => {
  const content = input?.message.message.content;
  if (!content) return "";
  if (Array.isArray(content)) {
    const entry = (content as { type: string; text?: string }[]).find(e => e.type === "text");
    return entry?.text ?? "";
  }
  return typeof content === "string" ? content : "";
}, [input?.message.message.content]);

24-27: Consider a discriminated union for MessageContent.

If only known kinds are expected, narrow type to a union (e.g., "text" | "image" | "tool"). This improves type safety in copyText and other consumers.

interface MessageContentText {
  type: "text";
  text: string;
}
type MessageContent = MessageContentText /* | MessageContentImage | ... */;
src/components/topnav.tsx (3)

125-128: Prefer functional updater for consistency and to avoid stale closures

This click handler uses the current value from the render, which is fine, but the keyboard shortcut uses a functional updater. Align the two and avoid potential stale toggles after rapid state changes.

 onClick={() => {
-  setResizePanelOpen(!resizePanelOpen);
+  setResizePanelOpen((prev) => !prev);
   setSelectedArtifact(undefined);
 }}

120-135: Improve accessibility: indicate toggle state with aria-pressed

Expose the expanded/collapsed state of the right panel to assistive tech.

 <Button
   variant="ghost"
   size="icon"
   className={`${selectedArtifact || selectedVibzMcp ? "hidden" : ""}`}
   aria-label="Toggle right panel"
+  aria-pressed={resizePanelOpen}
   onClick={() => {
     setResizePanelOpen((prev) => !prev);
     setSelectedArtifact(undefined);
   }}
>

49-53: Optional: clear user in store on confirmed auth failure

If you gate the redirect to only unauthorized errors, consider also clearing the user atom so downstream consumers don’t briefly read stale user data after an auth failure.

Example (add alongside your other effects):

useEffect(() => {
  // Requires userError from the query destructure
  if (isErrorUser && isUnauthorized(userError)) {
    setUser(undefined as any); // adjust typing if userAtom excludes undefined
  }
}, [isErrorUser, userError, setUser]);

If userAtom’s type excludes undefined, I can help adjust its typing to include undefined safely.

src/components/document-dialog.tsx (3)

254-260: Expand download button visibility

Currently, the Download button only appears for tag === "file". Consider showing it for other downloadable types like PDFs and images.

-  {tag === "file" ? (
+  {["file", "pdf", "image", "video", "audio", "text", "github"].includes(tag) ? (
     <Button onClick={handleDownload}>Download</Button>
   ) : tag === "url" || tag === "site" || tag === "youtube" ? (
     <Button onClick={handleOpen}>
       Open {tag === "youtube" ? "in YouTube" : "in Browser"}
     </Button>
   ) : null}

176-249: Consider adding video and audio preview support

The preview rendering doesn't handle video or audio tags that might be returned from getDocTagInfo.

Add cases for video and audio preview:

 } else if (tag === "youtube") {
   return (
     <iframe
       title={`YouTube video: ${documentName}`}
       src={previewUrl}
       className="absolute inset-0 w-full h-full"
       allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
       allowFullScreen
     />
   );
+} else if (tag === "video") {
+  return (
+    <video
+      src={previewUrl}
+      controls
+      className="absolute inset-0 w-full h-full object-contain"
+    >
+      <div className="absolute inset-0 flex items-center justify-center text-muted-foreground">
+        Video preview not supported in your browser.
+      </div>
+    </video>
+  );
+} else if (tag === "audio") {
+  return (
+    <div className="absolute inset-0 flex items-center justify-center">
+      <audio src={previewUrl} controls />
+    </div>
+  );
 } else if (tag === "url" || tag === "site") {

168-174: Consider using the ErrorState component for consistency

The PR introduces an ErrorState component for consistent error handling across the app, but this component renders error messages directly without using it.

+import { ErrorState } from "@/components/ui/error-state";

 {previewError ? (
   <div className="flex-grow flex items-center justify-center bg-muted rounded-md border">
-    <div className="text-center text-muted-foreground">
-      {previewError}
-    </div>
+    <ErrorState
+      density="comfy"
+      title="Unable to preview document"
+      error={new Error(previewError)}
+    />
   </div>
 ) : previewUrl ? (
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 5ffe8a7 and 66ed6e1.

📒 Files selected for processing (5)
  • src/components/chat/messages/utils-bar/index.ts (1 hunks)
  • src/components/chat/messages/utils-bar/user-utils-bar.tsx (2 hunks)
  • src/components/document-dialog.tsx (2 hunks)
  • src/components/topnav.tsx (2 hunks)
  • src/components/ui/tooltip-button.tsx (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/convex_rules.mdc)

**/*.ts: You can use the helper TypeScript type Id imported from './_generated/dataModel' to get the type of the id for a given table.
If you need to define a Record, provide the correct key and value types (e.g., Record<Id<'users'>, string>).
Be strict with types, particularly around ids of documents. Use Id<'table'> rather than string for document ids.
Always use as const for string literals in discriminated union types.
When using the Array type, always define arrays as const array: Array = [...];
When using the Record type, always define records as const record: Record<KeyType, ValueType> = {...};

Files:

  • src/components/chat/messages/utils-bar/index.ts
🧬 Code graph analysis (5)
src/components/chat/messages/utils-bar/index.ts (4)
convex/_generated/dataModel.d.ts (1)
  • Id (48-49)
convex/langchain/index.ts (3)
  • regenerate (302-322)
  • branchChat (324-442)
  • chat (61-300)
src/hooks/chats/use-messages.ts (2)
  • useNavigateBranch (76-102)
  • MessageWithBranchInfo (17-17)
convex/chatMessages/helpers.ts (1)
  • MessageWithBranchInfo (18-23)
src/components/document-dialog.tsx (4)
src/store/chatStore.ts (1)
  • documentDialogOpenAtom (44-46)
convex/documents/mutations.ts (1)
  • generateDownloadUrl (22-39)
src/lib/helper.tsx (1)
  • getDocTagInfo (136-174)
src/lib/utils.ts (1)
  • formatBytes (8-16)
src/components/ui/tooltip-button.tsx (2)
src/components/ui/tooltip.tsx (4)
  • TooltipProvider (59-59)
  • Tooltip (59-59)
  • TooltipTrigger (59-59)
  • TooltipContent (59-59)
src/components/ui/button.tsx (1)
  • Button (59-59)
src/components/topnav.tsx (1)
src/store/chatStore.ts (5)
  • resizePanelOpenAtom (37-37)
  • selectedArtifactAtom (54-54)
  • sidebarOpenAtom (35-35)
  • selectedVibzMcpAtom (55-55)
  • userAtom (11-11)
src/components/chat/messages/utils-bar/user-utils-bar.tsx (6)
src/components/chat/messages/utils-bar/index.ts (1)
  • useMessageActions (23-76)
src/hooks/chats/use-documents.ts (1)
  • useUploadDocuments (59-146)
src/components/ui/tooltip-button.tsx (1)
  • TooltipButton (11-41)
src/components/chat/messages/utils-bar/action-dropdown.tsx (1)
  • ActionDropdown (12-102)
src/components/chat/messages/utils-bar/branch-navigation.tsx (1)
  • BranchNavigation (19-51)
src/components/chat/messages/utils-bar/copy-button.tsx (1)
  • CopyButton (11-49)
🔇 Additional comments (6)
src/components/ui/tooltip-button.tsx (1)

8-8: Alias configuration validated
Your tsconfig.json defines

  • "baseUrl": "."
  • "paths": { "@/*": ["./src/*"] }

and we confirmed imports like from "@/components/ui/button" resolve throughout the codebase (including src/components/ui/tooltip-button.tsx) without errors. No further changes needed.

src/components/chat/messages/utils-bar/index.ts (1)

14-20: Nice extraction of a typed navigation helper.

Centralizing route construction with strict Id<"chats"> typing reduces repetition and avoids accidental stringly-typed params. LGTM.

src/components/chat/messages/utils-bar/user-utils-bar.tsx (2)

6-13: Verify lucide icon import: use Paperclip, not PaperclipIcon.

lucide-react typically exports Paperclip. If PaperclipIcon isn't available in your version, this will fail at build-time. Replace the import and usage.

 import {
   Check,
   CheckCheck,
   GitBranch,
   Pencil,
   RefreshCcw,
   X,
-  PaperclipIcon,
+  Paperclip,
 } from "lucide-react";
-  icon={<PaperclipIcon className="h-4 w-4 text-foreground/70" />}
+  icon={<Paperclip className="h-4 w-4 text-foreground/70" />}

Also applies to: 196-201


205-208: Guard against potentially undefined navigateBranch instead of using non-null assertion.

useMessageActions() returns navigateBranch from a hook; if it can be undefined in some contexts, the non-null assertion here can crash. Guard before rendering BranchNavigation.

-  <div className={`flex flex-row items-center gap-1 self-start opacity-70`}>
-    <BranchNavigation item={input} navigateBranch={navigateBranch!} />
+  <div className={`flex flex-row items-center gap-1 self-start opacity-70`}>
+    {navigateBranch && (
+      <BranchNavigation item={input} navigateBranch={navigateBranch} />
+    )}
src/components/topnav.tsx (1)

55-74: Keyboard toggle effect looks solid

Unregisters reliably on dependency change, guards by route, and uses a functional updater to avoid stale state. Good job.

src/components/document-dialog.tsx (1)

18-25: Good security measure for URL validation

The isValidExternalUrl helper properly restricts external URLs to http/https schemes only, preventing XSS attacks through javascript: or data: URLs. This is a critical security control.

- Updated DocumentDialog to remove unnecessary line and improve code clarity.
- Refined TopNav's error handling for unauthorized access with additional checks.
- Improved drag event handling in UserUtilsBar for better user interaction.
- Enhanced TooltipButton to provide a disabled state with appropriate styling.
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: 0

♻️ Duplicate comments (4)
src/components/topnav.tsx (2)

38-47: Broaden unauthorized detection to cover statusCode variants

Some stacks expose 401 as statusCode (or on response). Cheap to include.

   return (
     e?.response?.status === 401 ||
     e?.status === 401 ||
+    e?.statusCode === 401 ||
+    e?.response?.statusCode === 401 ||
     e?.code === "UNAUTHENTICATED" ||
     e?.code === "UNAUTHORIZED" ||
     (e instanceof Error && /\b401\b/.test(e.message))
   );

83-85: Redirect only on 401; wire query error into the condition

Unconditionally redirecting on any fetch error will log users out for transient/network issues. Gate the redirect on an auth error and plumb the query’s error.

-  const { data: user, isError: isErrorUser } = useQuery({
+  const {
+    data: user,
+    error: userError,
+    isError: isErrorUser,
+  } = useQuery({
     ...convexQuery(api.auth.getUser, {}),
     // Avoid looping on auth errors; tolerate brief transient failures.
     retry: (failureCount, err) => !isUnauthorized(err) && failureCount < 2,
     staleTime: 5 * 60 * 1000,
   });
-  if (isErrorUser) {
-    return <Navigate to="/auth" />;
-  }
+  if (isErrorUser && isUnauthorized(userError)) {
+    return <Navigate to="/auth" />;
+  }
src/components/document-dialog.tsx (2)

111-120: Allow downloads for all file-based types, not just "file"

Images, PDFs, video/audio, etc. should be downloadable via the same signed URL.

 const handleDownload = async () => {
-  if (!document || tag !== "file") return;
+  if (!document) return;
+  const downloadable =
+    ["file", "pdf", "image", "video", "audio", "text", "github"].includes(tag) ||
+    ["file", "pdf", "image", "video", "audio", "text", "github"].includes(document.type);
+  if (!downloadable) return;
   const url = await generateDownloadUrl({
     documentId: document._id,
   });
   if (url) {
     const w = window.open(url, "_blank", "noopener,noreferrer");
     if (w) w.opener = null;
   }
 };

54-104: Make preview loading race-safe and support video/audio signed URLs

Avoid setting state after unmount or rapid doc switches, and add video/audio handling consistent with images/PDFs.

 useEffect(() => {
   setPreviewUrl(null);
   setPreviewError(null);
-  const loadPreviewUrl = async () => {
+  let cancelled = false;
+  const loadPreviewUrl = async () => {
     if (!document) return;
     try {
       switch (tag) {
         case "image":
         case "pdf":
+        case "video":
+        case "audio":
         case "file": {
-          // Only files need download URL
+          // File-based types use a signed download URL
           const url = await generateDownloadUrl({
             documentId: document._id,
           });
-          setPreviewUrl(url);
+          if (!cancelled) setPreviewUrl(url);
           break;
         }
         case "url":
         case "site": {
           const externalUrl = document.key as string;
           if (isValidExternalUrl(externalUrl)) {
-            setPreviewUrl(externalUrl);
+            if (!cancelled) setPreviewUrl(externalUrl);
           } else {
-            setPreviewError("Invalid or unsafe URL");
+            if (!cancelled) setPreviewError("Invalid or unsafe URL");
           }
           break;
         }
         case "youtube": {
-          setPreviewUrl(`https://www.youtube.com/embed/${document.key}`);
+          const id = encodeURIComponent(String(document.key));
+          if (!cancelled) setPreviewUrl(`https://www.youtube.com/embed/${id}`);
           break;
         }
         default:
           if (["file", "text", "github"].includes(document.type)) {
             const url = await generateDownloadUrl({
               documentId: document._id,
             });
-            setPreviewUrl(url);
+            if (!cancelled) setPreviewUrl(url);
             break;
           } else {
-            setPreviewUrl(document.key as string);
+            const maybeUrl = String(document.key ?? "");
+            if (isValidExternalUrl(maybeUrl)) {
+              if (!cancelled) setPreviewUrl(maybeUrl);
+            } else {
+              if (!cancelled) setPreviewError("Unsupported preview source");
+            }
           }
           break;
       }
     } catch (error) {
-      setPreviewError(
+      if (!cancelled) setPreviewError(
         `Failed to load preview: ${error instanceof Error ? error.message : "Unknown error"}`,
       );
     }
   };
   loadPreviewUrl();
-}, [document, tag, generateDownloadUrl]);
+  return () => { cancelled = true; };
+}, [document, tag, generateDownloadUrl]);
🧹 Nitpick comments (6)
src/components/topnav.tsx (2)

132-135: Use functional updater to avoid stale toggles and coalesce side-effect

Align this onClick handler with the keyboard handler above; prevents stale reads and guarantees setSelectedArtifact(undefined) only when opening.

-            onClick={() => {
-              setResizePanelOpen(!resizePanelOpen);
-              setSelectedArtifact(undefined);
-            }}
+            onClick={() => {
+              setResizePanelOpen((open) => {
+                if (!open) setSelectedArtifact(undefined);
+                return !open;
+              });
+            }}

56-60: Optional: clear stale user on unauthorized

Prevents UI from briefly rendering with a stale user when an auth error occurs.

 useEffect(() => {
   if (user) {
     setUser(user);
   }
 }, [user, setUser]);
+
+useEffect(() => {
+  // If we start gating redirect on isUnauthorized(userError)
+  // (see comment above), also clear user here.
+  if (isErrorUser /* && isUnauthorized(userError) */) {
+    setUser(undefined);
+  }
+}, [isErrorUser, setUser]);
src/components/document-dialog.tsx (4)

81-84: Sanitize YouTube key with encodeURIComponent

Prevents malformed IDs from breaking the URL.

-  case "youtube": {
-    setPreviewUrl(`https://www.youtube.com/embed/${document.key}`);
+  case "youtube": {
+    const id = encodeURIComponent(String(document.key));
+    setPreviewUrl(`https://www.youtube.com/embed/${id}`);
     break;
   }
-  const w = window.open(
-    `https://youtube.com/watch?v=${document.key}`,
+  const w = window.open(
+    `https://youtube.com/watch?v=${encodeURIComponent(String(document.key))}`,
     "_blank",
     "noopener,noreferrer",
   );

Also applies to: 131-135


63-69: Align comment with behavior

The comment says “Only files need download URL” but the code fetches URLs for image/pdf/file; tweak for accuracy.

-            // Only files need download URL
+            // File-based types use a signed download URL

1-6: Render a shared ErrorState for preview failures (and import it)

Keeps error UI consistent with the PR’s new primitives.

 import {
   Dialog,
   DialogContent,
   DialogHeader,
   DialogTitle,
 } from "@/components/ui/dialog";
+import { ErrorState } from "@/components/ui/error-state";
-          {previewError ? (
-            <div className="flex-grow flex items-center justify-center bg-muted rounded-md border">
-              <div className="text-center text-muted-foreground">
-                {previewError}
-              </div>
-            </div>
-          ) : previewUrl ? (
+          {previewError ? (
+            <div className="flex-grow flex items-center justify-center bg-muted rounded-md border">
+              <ErrorState density="comfy" title="Unable to preview document" error={previewError} />
+            </div>
+          ) : previewUrl ? (

Also applies to: 167-173


173-249: Show a LoadingSpinner while resolving the preview URL

Improves perceived responsiveness and aligns with the PR’s loading UX.

-          ) : null}
+          ) : (
+            <div className="flex-grow flex items-center justify-center bg-muted rounded-md border">
+              <div className="text-muted-foreground">
+                {/* Replace with your shared spinner */}
+                Loading preview…
+              </div>
+            </div>
+          )}

If you have a shared spinner component:

+import { LoadingSpinner } from "@/components/ui/loading-spinner";
-              <div className="text-muted-foreground">
-                {/* Replace with your shared spinner */}
-                Loading preview…
-              </div>
+              <LoadingSpinner />
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 66ed6e1 and b5adc1c.

📒 Files selected for processing (4)
  • src/components/chat/messages/utils-bar/user-utils-bar.tsx (2 hunks)
  • src/components/document-dialog.tsx (2 hunks)
  • src/components/topnav.tsx (2 hunks)
  • src/components/ui/tooltip-button.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/components/ui/tooltip-button.tsx
  • src/components/chat/messages/utils-bar/user-utils-bar.tsx
🧰 Additional context used
🧬 Code graph analysis (2)
src/components/document-dialog.tsx (6)
src/store/chatStore.ts (1)
  • documentDialogOpenAtom (44-46)
convex/documents/mutations.ts (1)
  • generateDownloadUrl (22-39)
src/lib/helper.tsx (1)
  • getDocTagInfo (136-174)
src/components/ui/dialog.tsx (4)
  • Dialog (131-131)
  • DialogContent (133-133)
  • DialogHeader (136-136)
  • DialogTitle (139-139)
src/lib/utils.ts (1)
  • formatBytes (8-16)
src/components/ui/button.tsx (1)
  • Button (59-59)
src/components/topnav.tsx (2)
src/store/chatStore.ts (5)
  • resizePanelOpenAtom (37-37)
  • selectedArtifactAtom (54-54)
  • sidebarOpenAtom (35-35)
  • selectedVibzMcpAtom (55-55)
  • userAtom (11-11)
convex/_generated/api.js (2)
  • api (21-21)
  • api (21-21)

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant