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
4 changes: 4 additions & 0 deletions src-frontend/config/i18next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ export default defineConfig({
extract: {
input: "src/**/*.{js,jsx,ts,tsx}",
output: "src/i18n/locales/{{language}}/{{namespace}}.json",
preservePatterns: [
"error:*",
"sidebar:toolsets.status.*",
],
defaultNS: false,
}
});
36 changes: 27 additions & 9 deletions src-frontend/src/api/orval-mutator/custom-fetch.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,29 @@
import { API_BASE } from "..";
import { ApiErrorCode, McpConnectErrorCode, ServiceErrorCode } from "../generated/schemas";

export type ErrorCode =
| ApiErrorCode
| McpConnectErrorCode
| ServiceErrorCode
| "NETWORK_ERROR"
| "UNEXPECTED_ERROR";

export type ErrorResponse = {
error_code: string;
error_code: ErrorCode;
message: string;
};

export class FetchError<E extends ErrorResponse> extends Error {
statusCode: number;
errorCode: string;
errorCode: ErrorCode;
message: string;

constructor(statusCode: number, error: E) {
super(error.message);
this.name = "FetchError";
this.statusCode = statusCode;
this.errorCode = error.error_code;
this.message = error.message;
}
}

Expand All @@ -24,24 +34,32 @@ export async function fetchApi<T>(
init?: RequestInit
): Promise<T> {
const url = new URL(input.toString(), API_BASE);
const res = await fetch(url, init);
let res: Response;
try {
res = await fetch(url, init);
} catch {
throw new FetchError(500, {
error_code: "NETWORK_ERROR",
message: "Network error when fetching",
});
}

if (res.ok) {
if (res.status === 204) {
return undefined as T;
}
return (await res.json()) as T;
}

let errorBody: ErrorResponse;
try {
const errorBody = (await res.json()) as ErrorResponse;
throw new FetchError(res.status, errorBody);
errorBody = (await res.json()) as ErrorResponse;
} catch {
console.warn(
`Failed to parse error response as JSON: ${res.status} ${res.statusText}`
);
console.warn(`Failed to parse error response as JSON: ${res.status} ${res.statusText}`);
throw new FetchError(res.status, {
error_code: "HTTP_ERROR",
error_code: "UNEXPECTED_ERROR",
message: res.statusText,
});
}
throw new FetchError(res.status, errorBody);
}
2 changes: 1 addition & 1 deletion src-frontend/src/api/toolset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ export {
getGetToolsetQueryKey,
getGetToolsetsQueryKey,
getGetToolsetsBriefQueryKey,
useCreateMcpToolset,
useCreateToolset,
useDeleteToolset,
useGetToolsetSuspense,
useGetToolsetsSuspense,
Expand Down
15 changes: 4 additions & 11 deletions src-frontend/src/components/custom/AsyncBoundary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,14 @@ import { FailedToLoad } from "./FailedToLoad";
type AsyncBoundaryProps = {
children: React.ReactNode;
skeleton?: React.ReactNode;
errorRender?: (props: FallbackProps) => React.ReactNode;
onError?: (error: unknown, info: React.ErrorInfo) => void;
} & MustOneOf<{
errorDescription: string | ((error: unknown) => string);
errorRender: (props: FallbackProps) => React.ReactNode;
}>;
};

export function AsyncBoundary({
children,
skeleton,
onError,
errorDescription,
errorRender,
}: AsyncBoundaryProps) {
return (
Expand All @@ -29,12 +26,8 @@ export function AsyncBoundary({
errorRender ??
((props) => (
<FailedToLoad
refetch={props.resetErrorBoundary}
description={
typeof errorDescription === "string"
? errorDescription
: errorDescription(props.error)
}
error={props.error}
retry={props.resetErrorBoundary}
/>
))
}
Expand Down
43 changes: 33 additions & 10 deletions src-frontend/src/components/custom/FailedToLoad.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,52 @@
import { RefreshCcwIcon } from "lucide-react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyTitle,
} from "@/components/ui/empty";
import { FetchError } from "@/api/orval-mutator/custom-fetch";
import { getErrorMessage } from "@/i18n/error-message";
import { COMPONENTS_CUSTOM_NAMESPACE } from "@/i18n/resources";
import { i18n } from "@/i18n";

type FailedToLoadProps = {
refetch: () => void;
className?: string;
retry?: () => void;
title?: string;
} & MustOneOf<{
description: string;
};
error: unknown;
}>;

export function FailedToLoad({
className,
title = i18n.t("load_failed.title", { ns: COMPONENTS_CUSTOM_NAMESPACE }),
description,
error,
retry,
}: FailedToLoadProps) {
const { t } = useTranslation(COMPONENTS_CUSTOM_NAMESPACE);
let errorDescription: string;
if (description) {
errorDescription = description;
} else if (error instanceof FetchError) {
errorDescription = getErrorMessage(error.errorCode);
} else {
errorDescription = getErrorMessage("UNEXPECTED_ERROR");
}

export function FailedToLoad({ refetch, description }: FailedToLoadProps) {
return (
<Empty>
<Empty className={className}>
<EmptyContent>
<EmptyTitle>加载失败</EmptyTitle>
<EmptyDescription>
<div>{description}</div>
</EmptyDescription>
<EmptyTitle>{title}</EmptyTitle>
<EmptyDescription>{errorDescription}</EmptyDescription>
<EmptyContent>
<Button size="sm" variant="outline" onClick={() => refetch()}>
<Button size="sm" variant="outline" onClick={() => retry?.()}>
<RefreshCcwIcon />
重试
{t("load_failed.retry")}
</Button>
</EmptyContent>
</EmptyContent>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,7 @@ export function ModelSelectDialog({ value, onChange: onSelect }: ModelSelectDial
<SelectDialogContent>
<SelectDialogSearch placeholder={t("resource.model.search_placeholder")} />
<SelectDialogList className="max-h-96">
<AsyncBoundary
skeleton={<SelectDialogSkeleton />}
errorDescription={t("resource.model.load_error")}
>
<AsyncBoundary skeleton={<SelectDialogSkeleton />}>
<ModelQueryList />
</AsyncBoundary>
</SelectDialogList>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,7 @@ export function ToolMultiSelectDialog({ value, onChange }: ToolMultiSelectDialog
<SelectDialogSearch placeholder={t("resource.tool.search_placeholder")} />
<SelectDialogList>
<SelectDialogEmpty>{t("resource.tool.empty")}</SelectDialogEmpty>
<AsyncBoundary
skeleton={<SelectDialogSkeleton />}
errorDescription={t("resource.tool.load_error")}
>
<AsyncBoundary skeleton={<SelectDialogSkeleton />}>
<ToolQueryList onFetched={handleFetched} />
</AsyncBoundary>
</SelectDialogList>
Expand Down
2 changes: 1 addition & 1 deletion src-frontend/src/components/custom/form/FormShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export function FormShell<T extends FieldValues>({
onSubmit={methods.handleSubmit(onSubmit)}
className={cn(
"py-4",
"pr-1", // prevent outline of form controls from being cut off
"px-1", // prevent outline of form controls from being cut off
className)}
>
<FieldGroup className="gap-y-2">{children}</FieldGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export function DirectoryField({

async function chooseDirectory() {
try {
// TODO: use unified api
const selected = await open({ directory: true });
if (typeof selected === "string") {
setValue(fieldName, selected, {
Expand Down
50 changes: 23 additions & 27 deletions src-frontend/src/features/SideBar/views/AgentsView/AgentList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,26 +106,6 @@ export function AgentList() {
const { t } = useTranslation("sidebar");
const removeTabs = useTabsStore((state) => state.remove);

const asyncConfirm = useAsyncConfirm<AgentBrief>({
async onConfirm(agent) {
await deleteAgentMutation.mutateAsync({ agentId: agent.id });
await invalidateAgentQueries(agent.id);

removeTabs((tab) => (tab.type === "agent" &&
tab.metadata.mode === "edit" &&
tab.metadata.id === agent.id));

toast.success(t("agents.toast.delete_success_title"), {
description: t("agents.toast.delete_success_description"),
});
},
onError(error: Error) {
toast.error(t("agents.toast.delete_error_title"), {
description: error.message || t("agents.toast.delete_error_description"),
});
},
});

const query = useGetAgentsSuspenseInfinite(undefined, {
query: PAGINATED_QUERY_DEFAULT_OPTIONS,
});
Expand All @@ -137,22 +117,38 @@ export function AgentList() {
toast.success(t("agents.toast.delete_success_title"), {
description: t("agents.toast.delete_success_description"),
});
},
onError(error: Error) {
toast.error(t("agents.toast.delete_error_title"), {
description: error.message || t("agents.toast.delete_error_description"),
});
},
}
},
});

const asyncConfirm = useAsyncConfirm<AgentBrief>({
async onConfirm(agent) {
await deleteAgentMutation.mutateAsync({ agentId: agent.id });
await invalidateAgentQueries(agent.id);

removeTabs((tab) => (tab.type === "agent" &&
tab.metadata.mode === "edit" &&
tab.metadata.id === agent.id));

toast.success(t("agents.toast.delete_success_title"), {
description: t("agents.toast.delete_success_description"),
});
}
});

return (
<>
<ScrollArea className="flex-1">
<InfiniteScroll
query={query}
selectItems={(page) => page.items}
itemRender={(agent) => <AgentItem key={agent.id} agent={agent} onDelete={asyncConfirm.trigger} />}
itemRender={(agent) => (
<AgentItem
key={agent.id}
agent={agent}
onDelete={asyncConfirm.trigger}
/>
)}
/>
</ScrollArea>
<ConfirmDeleteDialog
Expand Down
5 changes: 1 addition & 4 deletions src-frontend/src/features/SideBar/views/AgentsView/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,7 @@ export function AgentsView() {
/>
</SideBarHeader>
<div className="flex-1">
<AsyncBoundary
skeleton={<SideBarListSkeleton />}
errorDescription={t("agents.list.error_load")}
>
<AsyncBoundary skeleton={<SideBarListSkeleton />}>
<AgentList />
</AsyncBoundary>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,9 @@ export function HelperModelSettings() {
<Skeleton className="h-9 w-24" />
</SettingItem>
)}
errorDescription={t("settings.helper_model.flash_model.error_load")}
>
<SettingItem title={t("settings.helper_model.flash_model.title")}>
<HelperModelSettingsSuspense />
<HelperModelSettingsSuspense />
</SettingItem>
</AsyncBoundary>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,12 +125,7 @@ export function ProviderList() {
toast.success(t("settings.providers.toast.delete_success_title"), {
description: t("settings.providers.toast.delete_success_description"),
});
},
onError(error: Error) {
toast.error(t("settings.providers.toast.delete_error_title"), {
description: error.message || t("settings.providers.toast.delete_error_description"),
});
},
}
},
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,7 @@ export function ProviderSettings() {

return (
<div className="flex flex-col">
<AsyncBoundary
skeleton={<ProviderListSkeleton />}
errorDescription={t("settings.providers.list.error_load")}
>
<AsyncBoundary skeleton={<ProviderListSkeleton />}>
<ProviderList />
</AsyncBoundary>

Expand Down
17 changes: 4 additions & 13 deletions src-frontend/src/features/SideBar/views/TasksView/TaskList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ export function TaskList({ workspaceId }: TaskListProps) {
const queryClient = useQueryClient();
const deleteTaskMutation = useDeleteTask();

useEffect(() =>
useEffect(() =>
SseDispatcher.subscribe("TASK_TITLE_UPDATED", ({ task_id, title }: TaskTitleUpdatedEvent) => {
const queryKey = getGetTasksInfiniteQueryKey({ workspace_id: workspaceId });
queryClient.setQueryData<InfiniteData<PageTaskBrief>>(
Expand Down Expand Up @@ -151,21 +151,12 @@ export function TaskList({ workspaceId }: TaskListProps) {
toast.success(t("tasks.toast.delete_success_title"), {
description: t("tasks.toast.delete_success_description"),
});
},
onError(error: Error) {
toast.error(t("tasks.toast.delete_error_title"), {
description: error.message || t("tasks.toast.delete_error_description"),
});
},
}
});

const query = useGetTasksSuspenseInfinite(
{
workspace_id: workspaceId,
},
{
query: PAGINATED_QUERY_DEFAULT_OPTIONS,
}
{ workspace_id: workspaceId },
{ query: PAGINATED_QUERY_DEFAULT_OPTIONS }
);

if (query.data.pages.length === 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export function TasksView() {
}

return (
<AsyncBoundary skeleton={<SideBarListSkeleton />} errorDescription={t("tasks.list.error_load")}>
<AsyncBoundary skeleton={<SideBarListSkeleton />}>
<TaskList workspaceId={currentWorkspace.id} />
</AsyncBoundary>
);
Expand Down
Loading
Loading