From 27448868ddbca97d7f88e806e4bbc1fa7af4e982 Mon Sep 17 00:00:00 2001 From: ajnart Date: Thu, 30 Jul 2026 10:01:36 +0200 Subject: [PATCH] fix: surface assistant stream errors --- .../src/app/api/assistant/chat/route.ts | 13 ++++++--- .../assistant/assistant-panel.module.css | 5 ++++ .../components/assistant/assistant-panel.tsx | 27 ++++++++++++++++++- .../assistant/assistant-provider.tsx | 12 +++++++++ packages/translation/src/lang/en.json | 6 ++++- 5 files changed, 58 insertions(+), 5 deletions(-) diff --git a/apps/nextjs/src/app/api/assistant/chat/route.ts b/apps/nextjs/src/app/api/assistant/chat/route.ts index c49447a41..b41f4cf85 100644 --- a/apps/nextjs/src/app/api/assistant/chat/route.ts +++ b/apps/nextjs/src/app/api/assistant/chat/route.ts @@ -27,7 +27,8 @@ const getToolApprovalSecret = () => Buffer.from( hkdfSync("sha256", Buffer.from(env.SECRET_ENCRYPTION_KEY, "hex"), "", "assistant-tool-approval", 32), ).toString("base64url"); -const safeStreamError = "The model endpoint stopped the response. Check its URL, model, and credentials."; +const safeStreamError = + "The model endpoint stopped the response. Try again, or ask an administrator to verify its URL, model, and credentials."; const requestSchema = z.object({ id: z.string().min(1).max(64), @@ -267,9 +268,15 @@ export async function POST(request: Request) { }); return result.toUIMessageStreamResponse({ onError: () => safeStreamError }); - } catch { + } catch (error) { + logger.error("Assistant response could not start", { + error: error instanceof Error ? error.message : String(error), + }); return Response.json( - { error: "The configured model endpoint could not start this response. Check its URL, model, and credentials." }, + { + error: + "The configured model endpoint could not start this response. Try again, or ask an administrator to verify its URL, model, and credentials.", + }, { status: 502 }, ); } diff --git a/apps/nextjs/src/components/assistant/assistant-panel.module.css b/apps/nextjs/src/components/assistant/assistant-panel.module.css index ea88c6365..4cb0d06a7 100644 --- a/apps/nextjs/src/components/assistant/assistant-panel.module.css +++ b/apps/nextjs/src/components/assistant/assistant-panel.module.css @@ -85,7 +85,12 @@ border-radius: var(--mantine-radius-md); color: var(--mantine-color-red-text); background: var(--mantine-color-red-light); +} + +.messageErrorText { + color: var(--mantine-color-text); font-size: var(--mantine-font-size-sm); + line-height: 1.45; } .tool { diff --git a/apps/nextjs/src/components/assistant/assistant-panel.tsx b/apps/nextjs/src/components/assistant/assistant-panel.tsx index c5a2c4640..046b67af4 100644 --- a/apps/nextjs/src/components/assistant/assistant-panel.tsx +++ b/apps/nextjs/src/components/assistant/assistant-panel.tsx @@ -5,6 +5,7 @@ import type { ToolCallMessagePartProps } from "@assistant-ui/react"; import { ActionBarPrimitive, ComposerPrimitive, + ErrorPrimitive, MessagePartPrimitive, MessagePrimitive, ThreadListItemPrimitive, @@ -16,6 +17,7 @@ import { ActionIcon, Box, Button, Drawer, Group, ScrollArea, Stack, Text, ThemeI import { useMediaQuery } from "@mantine/hooks"; import { IconActivityHeartbeat, + IconAlertTriangle, IconApps, IconArchive, IconArrowUp, @@ -151,7 +153,30 @@ const RuntimeError = () => { const t = useScopedI18n("common.assistant"); return ( - {t("responseError")} + + + + + + + + {t("responseError.title")} + + + + + + + + ); }; diff --git a/apps/nextjs/src/components/assistant/assistant-provider.tsx b/apps/nextjs/src/components/assistant/assistant-provider.tsx index 012b40610..a342c042e 100644 --- a/apps/nextjs/src/components/assistant/assistant-provider.tsx +++ b/apps/nextjs/src/components/assistant/assistant-provider.tsx @@ -21,6 +21,7 @@ import { lastAssistantMessageIsCompleteWithApprovalResponses } from "ai"; import { clientApi, fetchApi } from "@homarr/api/client"; import { useSession } from "@homarr/auth/client"; import { hotkeys } from "@homarr/definitions"; +import { showErrorNotification } from "@homarr/notifications"; import { useScopedI18n } from "@homarr/translation/client"; import { openMediaRequestSearch, @@ -177,12 +178,23 @@ const createHistoryAdapter = (threadId: string | undefined): ThreadHistoryAdapte }); const AssistantThreadRuntime = () => { + const t = useScopedI18n("common.assistant"); const threadId = useAuiState((state) => state.threadListItem.remoteId); const transport = useMemo(() => new AssistantChatTransport({ api: "/api/assistant/chat" }), []); const history = useMemo(() => createHistoryAdapter(threadId), [threadId]); + const onError = useCallback( + () => + showErrorNotification({ + title: t("responseError.title"), + message: t("responseError.description"), + autoClose: 10_000, + }), + [t], + ); return useChatRuntime({ transport, + onError, sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses, adapters: { history }, }); diff --git a/packages/translation/src/lang/en.json b/packages/translation/src/lang/en.json index 2248155e7..79dbb8f0d 100644 --- a/packages/translation/src/lang/en.json +++ b/packages/translation/src/lang/en.json @@ -2024,7 +2024,11 @@ "send": "Send message", "copy": "Copy response", "regenerate": "Regenerate response", - "responseError": "The response stopped unexpectedly. Try again, or ask an administrator to verify the model endpoint.", + "responseError": { + "title": "The assistant could not finish", + "description": "Try again. If the problem continues, ask an administrator to verify the configured model endpoint.", + "retry": "Try again" + }, "scrollToLatest": "Scroll to latest", "approvalRequired": "Approval required", "complete": "Complete",