Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
121 changes: 121 additions & 0 deletions site/src/pages/AgentsPage/AgentChatPage.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import type { FC } from "react";
import { useRef } from "react";
import { hashKey } from "react-query";
import { Outlet, useNavigate } from "react-router";
import { expect, spyOn, userEvent, waitFor, within } from "storybook/test";
import {
Expand All @@ -22,6 +23,7 @@ import {
import { workspaceByIdKey } from "#/api/queries/workspaces";
import type * as TypesGen from "#/api/typesGenerated";
import {
MockChat,
MockChatMessage,
MockChatQueuedMessage,
} from "#/testHelpers/chatEntities";
Expand All @@ -32,6 +34,7 @@ import {
MockOrganizationMember2,
MockUserOwner,
MockWorkspace,
mockApiError,
} from "#/testHelpers/entities";
import {
withAuthProvider,
Expand Down Expand Up @@ -3059,6 +3062,124 @@ export const SendResponseAfterChatSwitch: Story = {
},
};

const mockErrorChat: TypesGen.Chat = {
...MockChat,
id: CHAT_ID,
...baseChatFields,
title: "Failing chat",
};

const mockServerError = {
...mockApiError({ message: "Internal server error." }),
status: 500,
};

const withoutQuery = (
queries: ReturnType<typeof buildQueries>,
queryKey: readonly unknown[],
) => queries.filter(({ key }) => hashKey(key) !== hashKey(queryKey));

export const DetailQueryError: Story = {
parameters: {
queries: withoutQuery(
buildQueries(mockErrorChat, {
messages: [],
queued_messages: [],
has_more: false,
}),
chatKey(CHAT_ID),
),
},
beforeEach: () => {
spyOn(API.experimental, "getChat").mockRejectedValue(mockServerError);
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(await canvas.findByText("Failed to load chat")).toBeVisible();
expect(canvas.queryByText("Chat not found")).not.toBeInTheDocument();
expect(
canvas.getByRole("button", { name: "Try again" }),
).toBeInTheDocument();
},
};

export const InitialMessagesError: Story = {
parameters: {
queries: withoutQuery(
buildQueries(mockErrorChat, {
messages: [],
queued_messages: [],
has_more: false,
}),
chatMessagesKey(CHAT_ID),
),
},
beforeEach: () => {
spyOn(API.experimental, "getChatMessages").mockRejectedValue(
mockServerError,
);
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(await canvas.findByText("Failed to load chat")).toBeVisible();
expect(canvas.queryByText("Chat not found")).not.toBeInTheDocument();
},
};

export const ErrorRetryRecovers: Story = {
parameters: {
queries: withoutQuery(
buildQueries(mockErrorChat, {
messages: [],
queued_messages: [],
has_more: false,
}),
chatKey(CHAT_ID),
),
},
beforeEach: ({ parameters }) => {
const getChatSpy = spyOn(API.experimental, "getChat")
.mockRejectedValueOnce(mockServerError)
.mockResolvedValue(mockErrorChat);
parameters.getChatCallsForChat = () =>
getChatSpy.mock.calls.filter(([chatId]) => chatId === CHAT_ID).length;
},
play: async ({ canvasElement, parameters }) => {
const canvas = within(canvasElement);
expect(await canvas.findByText("Failed to load chat")).toBeVisible();
await userEvent.click(canvas.getByRole("button", { name: "Try again" }));
await waitFor(() => {
expect(canvas.queryByText("Failed to load chat")).not.toBeInTheDocument();
});
expect(canvas.queryByText("Chat not found")).not.toBeInTheDocument();
expect(parameters.getChatCallsForChat()).toBeGreaterThanOrEqual(2);
},
};

export const ChatNotFound: Story = {
parameters: {
queries: withoutQuery(
buildQueries(mockErrorChat, {
messages: [],
queued_messages: [],
has_more: false,
}),
chatKey(CHAT_ID),
),
},
beforeEach: () => {
spyOn(API.experimental, "getChat").mockRejectedValue({
...mockApiError({ message: "Chat not found." }),
status: 404,
});
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(await canvas.findByText("Chat not found")).toBeVisible();
expect(canvas.queryByText("Failed to load chat")).not.toBeInTheDocument();
},
};

export const SendRejectedByHookDispatchFailure: Story = {
parameters: {
queries: buildQueries(
Expand Down
34 changes: 33 additions & 1 deletion site/src/pages/AgentsPage/AgentChatPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
type CreateChatMessageRequestWithClearablePlanMode,
watchWorkspace,
} from "#/api/api";
import { getErrorMessage, isApiError } from "#/api/errors";
import { getErrorMessage, getErrorStatus, isApiError } from "#/api/errors";
import { checkAuthorization } from "#/api/queries/authCheck";
import { buildOptimisticEditedMessage } from "#/api/queries/chatMessageEdits";
import {
Expand Down Expand Up @@ -68,6 +68,7 @@ import { isMobileViewport } from "#/utils/mobile";
import { pageTitle } from "#/utils/page";
import { rewriteLocalhostURL } from "#/utils/portForward";
import { createReconnectingWebSocket } from "#/utils/reconnectingWebSocket";
import { AgentChatPageErrorView } from "./AgentChatPageErrorView";
import {
AgentChatPageLoadingView,
AgentChatPageNotFoundView,
Expand Down Expand Up @@ -1881,6 +1882,37 @@ const AgentChatPage: FC = () => {
);
}

if (chatQuery.isLoadingError || chatMessagesQuery.isLoadingError) {
if (getErrorStatus(chatQuery.error) === 404) {
return (
<AgentChatPageNotFoundView
titleElement={titleElement}
isSidebarCollapsed={isSidebarCollapsed}
onToggleSidebarCollapsed={onToggleSidebarCollapsed}
/>
);
}

return (
<AgentChatPageErrorView
titleElement={titleElement}
isSidebarCollapsed={isSidebarCollapsed}
onToggleSidebarCollapsed={onToggleSidebarCollapsed}
error={
chatQuery.isLoadingError ? chatQuery.error : chatMessagesQuery.error
}
onRetry={() => {
if (chatQuery.isLoadingError) {
void chatQuery.refetch();
}
if (chatMessagesQuery.isLoadingError) {
void chatMessagesQuery.refetch();
}
}}
/>
);
}

if (!chatQuery.data || !chatMessagesQuery.data?.pages?.length || !agentId) {
return (
<AgentChatPageNotFoundView
Expand Down
60 changes: 60 additions & 0 deletions site/src/pages/AgentsPage/AgentChatPageErrorView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { RotateCcwIcon } from "lucide-react";
import type { FC, ReactNode } from "react";
import { getErrorDetail, getErrorMessage } from "#/api/errors";
import { Button } from "#/components/Button/Button";
import { ChatTopBar } from "./components/ChatTopBar";

interface AgentChatPageErrorViewProps {
titleElement: ReactNode;
isSidebarCollapsed: boolean;
onToggleSidebarCollapsed: () => void;
error: unknown;
onRetry: () => void;
}

export const AgentChatPageErrorView: FC<AgentChatPageErrorViewProps> = ({
titleElement,
isSidebarCollapsed,
onToggleSidebarCollapsed,
error,
onRetry,
}) => {
const detail = getErrorDetail(error);

return (
<div className="flex h-full min-h-0 min-w-0 flex-1 flex-col">
{titleElement}
<ChatTopBar
panel={{
showSidebarPanel: false,
onToggleSidebar: () => {},
}}
onArchiveAgent={() => {}}
onUnarchiveAgent={() => {}}
onArchiveAndDeleteWorkspace={() => {}}
hasWorkspace={false}
isSidebarCollapsed={isSidebarCollapsed}
onToggleSidebarCollapsed={onToggleSidebarCollapsed}
/>
<div className="flex flex-1 items-center justify-center px-6 text-center">
<div className="flex flex-col items-center">
<h3 className="m-0 font-medium text-base text-content-primary">
Failed to load chat
</h3>
<p className="m-0 mt-1 max-w-md text-sm text-content-secondary">
{getErrorMessage(error, "The chat could not be loaded.")}
</p>
{detail && (
<p className="m-0 mt-1 max-w-md text-sm text-content-secondary">
{detail}
</p>
)}
<Button size="sm" onClick={onRetry} className="mt-4">
<RotateCcwIcon />
Try again
</Button>
</div>
</div>
</div>
);
};
Loading