Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
stats.html
node_modules
.DS_Store
dist/
Expand All @@ -14,5 +15,4 @@ convex/_generated/
scripts/__pycache__/
*/__pycache__/*
.tanstack
bun.lock
stats.html
bun.lock
16 changes: 14 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 35 additions & 1 deletion src/components/chat/input/document-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { convexQuery } from "@convex-dev/react-query";
import { api } from "../../../../convex/_generated/api";
import type { Id, Doc } from "../../../../convex/_generated/dataModel";
import { XIcon } from "lucide-react";
import { LoadingSpinner } from "@/components/ui/loading-spinner";
import { ErrorState } from "@/components/ui/error-state";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { documentDialogOpenAtom } from "@/store/chatStore";
Expand Down Expand Up @@ -74,10 +76,16 @@ export const DocumentList = ({
documentIds?: Id<"documents">[];
model: string;
}) => {
const { data: documents } = useQuery({
const {
data: documents,
isLoading: isLoadingDocuments,
isError: isDocumentsError,
error: documentsError,
} = useQuery({
...convexQuery(api.documents.queries.getMultiple, { documentIds }),
});
Comment on lines +79 to 86
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.

const setDocumentDialogOpen = useSetAtom(documentDialogOpenAtom);

const removeDocument = useRemoveDocument();

const handlePreview = useCallback(
Expand All @@ -99,6 +107,32 @@ export const DocumentList = ({
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) {
return (
<div className="flex items-center justify-start p-2">
<ErrorState
title="Error loading documents"
showDescription={false}
className="p-2"
density="compact"
/>
</div>
);
}

return (
<ScrollArea className="max-h-24 w-full px-1 pt-1 whitespace-nowrap">
<div id="chatInputDocumentList" className="flex gap-1">
Expand Down
29 changes: 27 additions & 2 deletions src/components/chat/input/toolbar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ import { ModelPopover } from "./model-popover";
import { useRouter } from "@tanstack/react-router";
import { AgentToggles } from "./agent-toggles";
import { useApiKeys } from "@/hooks/use-apikeys";
import { LoadingSpinner } from "@/components/ui/loading-spinner";
import { ErrorState } from "@/components/ui/error-state";
import { toast } from "sonner";

export const ToolBar = () => {
Expand Down Expand Up @@ -74,7 +76,11 @@ export const ToolBar = () => {
useApiKeys();

// Get project details if chat has a project
const { data: project } = useQuery({
const {
data: project,
isLoading: isLoadingProject,
isError: isProjectError,
} = useQuery({
...convexQuery(
api.projects.queries.get,
chat?.projectId ? { projectId: chat.projectId } : "skip",
Expand Down Expand Up @@ -152,7 +158,26 @@ export const ToolBar = () => {
</DropdownMenu>
<AgentToggles />
{/* Render project name with X button on hover */}
{project && (
{isLoadingProject && (
<Button
variant="outline"
className="justify-between px-2 border-none"
disabled
>
<span className="flex items-center gap-1 text-muted-foreground">
<LoadingSpinner sizeClassName="h-3 w-3" />
Loading project...
</span>
</Button>
)}
{isProjectError && (
<ErrorState
density="compact"
title="Failed to load project"
className="h-9"
/>
)}
{project && !isLoadingProject && !isProjectError && (
<Button
variant="outline"
className="group justify-between px-2 border-none"
Expand Down
93 changes: 59 additions & 34 deletions src/components/chat/input/toolbar/projects-dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { useQuery, useMutation } from "@tanstack/react-query";
import { convexQuery, useConvexMutation } from "@convex-dev/react-query";
import { api } from "../../../../../convex/_generated/api";
import { FoldersIcon, PlusIcon } from "lucide-react";
import { LoadingSpinner } from "@/components/ui/loading-spinner";
import { ErrorState } from "@/components/ui/error-state";
import {
createProjectDialogOpenAtom,
newChatAtom,
Expand All @@ -27,7 +29,12 @@ export const ProjectsDropdown = ({
onCloseDropdown,
}: ProjectsDropdownProps) => {
const chatId = useAtomValue(chatIdAtom);
const { data: projects, isFetching: isFetchingProjects } = useQuery({
const {
data: projects,
isLoading: isLoadingProjects,
isError: isProjectsError,
error: projectsError,
} = useQuery({
...convexQuery(api.projects.queries.getAll, {
paginationOpts: { numItems: 3, cursor: null },
}),
Expand All @@ -49,39 +56,57 @@ export const ProjectsDropdown = ({
</div>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent className="ml-2">
{projects?.page.slice(0, 3).map((project) => (
<DropdownMenuItem
key={project._id}
onSelect={() => {
if (chatId === "new") {
setNewChat((prev) => ({ ...prev, projectId: project._id }));
} else {
updateChatMutation({
chatId,
updates: {
projectId: project._id,
},
});
}
onCloseDropdown();
setResizePanelOpen(true);
setSelectedPanelTab("projects");
}}
>
{isFetchingProjects ? <div>Fetching...</div> : project.name}
</DropdownMenuItem>
))}
<DropdownMenuSeparator />
<DropdownMenuItem
onSelect={(e) => {
e.preventDefault();
onCloseDropdown();
setProjectDialogOpen(true);
}}
>
<PlusIcon className="w-4 h-4" />
Add New Project
</DropdownMenuItem>
{isProjectsError ? (
<ErrorState error={projectsError} />
) : (
<>
{isLoadingProjects ? (
<DropdownMenuItem>
<LoadingSpinner
className="h-4 w-4"
label="Loading projects..."
/>
</DropdownMenuItem>
) : (
projects?.page?.slice(0, 3).map((project: any) => (
<DropdownMenuItem
key={project._id}
onSelect={() => {
if (chatId === "new") {
setNewChat((prev) => ({
...prev,
projectId: project._id,
}));
} else {
updateChatMutation({
chatId,
updates: {
projectId: project._id,
},
});
}
onCloseDropdown();
setResizePanelOpen(true);
setSelectedPanelTab("projects");
}}
>
{project.name}
</DropdownMenuItem>
))
)}
<DropdownMenuSeparator />
<DropdownMenuItem
onSelect={(e) => {
e.preventDefault();
onCloseDropdown();
setProjectDialogOpen(true);
}}
>
<PlusIcon className="w-4 h-4" />
Add New Project
</DropdownMenuItem>
</>
)}
</DropdownMenuSubContent>
</DropdownMenuSub>
);
Expand Down
35 changes: 34 additions & 1 deletion src/components/chat/messages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useMessages } from "../../../hooks/chats/use-messages";
import { ErrorState } from "@/components/ui/error-state";
import { useMemo } from "react";
import { ScrollArea } from "@/components/ui/scroll-area";
import { MessagesList } from "./messages";
Expand All @@ -7,10 +8,12 @@ import { TriangleAlertIcon } from "lucide-react";
import type { Id } from "../../../../convex/_generated/dataModel";
import { streamStatusAtom, userLoadableAtom } from "@/store/chatStore";
import { useAtomValue } from "jotai";
import { LoadingSpinner } from "@/components/ui/loading-spinner";

export const ChatMessages = ({ chatId }: { chatId: Id<"chats"> | "new" }) => {
const userLoadable = useAtomValue(userLoadableAtom);
const { isLoading, isEmpty } = useMessages({ chatId });
const { isLoading, isEmpty, isError, error, isStreamError, streamError } =
useMessages({ chatId });

const streamStatus = useAtomValue(streamStatusAtom);

Expand Down Expand Up @@ -38,11 +41,41 @@ export const ChatMessages = ({ chatId }: { chatId: Id<"chats"> | "new" }) => {
if (isLoading) {
return (
<div className="flex items-center justify-center h-full">
<LoadingSpinner />
<div className="text-muted-foreground">Loading messages...</div>
</div>
);
}

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

if (isStreamError || streamError) {
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>
);
}

if (isEmpty) {
return (
<div className="flex items-center justify-center h-full">
Expand Down
31 changes: 30 additions & 1 deletion src/components/chat/messages/user-message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import type {
MessageGroup,
} from "../../../../convex/chatMessages/helpers";
import { Button } from "@/components/ui/button";
import { LoadingSpinner } from "@/components/ui/loading-spinner";
import { ErrorState } from "@/components/ui/error-state";
import { documentDialogOpenAtom } from "@/store/chatStore";
import { useSetAtom } from "jotai";
import { api } from "../../../../convex/_generated/api";
Expand All @@ -32,10 +34,37 @@ const DocumentList = ({
onPreview?: (documentId: Id<"documents">) => void;
showRemove?: boolean;
}) => {
const { data: documents } = useQuery({
const {
data: documents,
isLoading,
isError,
} = useQuery({
...convexQuery(api.documents.queries.getMultiple, { documentIds }),
});

if (isLoading) {
return (
<div className="flex items-center gap-2 px-1 pt-1">
<LoadingSpinner sizeClassName="h-3 w-3" />
<span className="text-xs text-muted-foreground">
Loading documents...
</span>
</div>
);
}

if (isError) {
return (
<div className="px-1 pt-1">
<ErrorState
density="compact"
title="Failed to load documents"
className="h-6"
/>
</div>
);
}

if (!documents?.length) return null;

const selectedModel = models.find((m) => m.model_name === "gpt-4");
Expand Down
Loading