fix(ai-chat): improve error banner layout
The AI chat error Alert stranded the warning icon in the top-left while
the detail text hung indented under the heading, wrapping to 3 narrow
lines with empty space below. Switch to a "full-width detail" layout
(icon + bold heading on the first row, detail spanning full width below)
and extract the markup, previously duplicated in ChatThread and
MessageItem, into a single shared ChatErrorAlert component.
- add apps/client/src/features/ai-chat/components/chat-error-alert.tsx
- use it for the live stream error in chat-thread.tsx (mb="xs")
- use it for the persisted history error in message-item.tsx (mt={4})
- heading/icon use the adaptive --mantine-color-red-light-color so the
banner stays correct in dark mode
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,38 @@
|
|||||||
|
import { Alert, Group, Text, type AlertProps } from "@mantine/core";
|
||||||
|
import { IconAlertTriangle } from "@tabler/icons-react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A classified AI chat error banner: a warning icon + bold heading on the first
|
||||||
|
* row, with the detail text spanning the full width below. Rendered for BOTH the
|
||||||
|
* live stream error (ChatThread) and a persisted assistant error (MessageItem),
|
||||||
|
* so this markup lives in one place. The detail is full-width (no hanging indent
|
||||||
|
* under the heading) so it wraps less and leaves no stranded icon / empty gap.
|
||||||
|
* The heading reuses Mantine's adaptive red "light" colour so it stays correct
|
||||||
|
* in dark mode. Layout-only props (mb/mt/...) are forwarded to the Alert root.
|
||||||
|
*/
|
||||||
|
interface ChatErrorAlertProps extends Omit<AlertProps, "title" | "children"> {
|
||||||
|
title: string;
|
||||||
|
detail: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ChatErrorAlert({
|
||||||
|
title,
|
||||||
|
detail,
|
||||||
|
...alertProps
|
||||||
|
}: ChatErrorAlertProps) {
|
||||||
|
// Mantine's own "light" alert colour, adaptive across light/dark schemes.
|
||||||
|
const accent = "var(--mantine-color-red-light-color)";
|
||||||
|
return (
|
||||||
|
<Alert {...alertProps} variant="light" color="red" p="xs">
|
||||||
|
<Group gap={8} wrap="nowrap" align="center" mb={4}>
|
||||||
|
<IconAlertTriangle size={18} style={{ flex: "none", color: accent }} />
|
||||||
|
<Text fw={700} size="sm" lh={1.2} style={{ color: accent }}>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
<Text size="sm" lh={1.4}>
|
||||||
|
{detail}
|
||||||
|
</Text>
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
import { useCallback, useMemo, useRef, useState } from "react";
|
import { useCallback, useMemo, useRef, useState } from "react";
|
||||||
import { generateId } from "ai";
|
import { generateId } from "ai";
|
||||||
import { ActionIcon, Alert, Box, Group, Stack, Text } from "@mantine/core";
|
import { ActionIcon, Box, Group, Stack, Text } from "@mantine/core";
|
||||||
import { IconAlertTriangle, IconClockHour4, IconX } from "@tabler/icons-react";
|
import { IconClockHour4, IconX } from "@tabler/icons-react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useChat, type UIMessage } from "@ai-sdk/react";
|
import { useChat, type UIMessage } from "@ai-sdk/react";
|
||||||
import { DefaultChatTransport } from "ai";
|
import { DefaultChatTransport } from "ai";
|
||||||
import MessageList from "@/features/ai-chat/components/message-list.tsx";
|
import MessageList from "@/features/ai-chat/components/message-list.tsx";
|
||||||
import ChatInput from "@/features/ai-chat/components/chat-input.tsx";
|
import ChatInput from "@/features/ai-chat/components/chat-input.tsx";
|
||||||
import RoleCards from "@/features/ai-chat/components/role-cards.tsx";
|
import RoleCards from "@/features/ai-chat/components/role-cards.tsx";
|
||||||
|
import ChatErrorAlert from "@/features/ai-chat/components/chat-error-alert.tsx";
|
||||||
import {
|
import {
|
||||||
IAiChatMessageRow,
|
IAiChatMessageRow,
|
||||||
IAiRole,
|
IAiRole,
|
||||||
@@ -277,15 +278,11 @@ export default function ChatThread({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{errorView && (
|
{errorView && (
|
||||||
<Alert
|
<ChatErrorAlert
|
||||||
variant="light"
|
|
||||||
color="red"
|
|
||||||
icon={<IconAlertTriangle size={16} />}
|
|
||||||
mb="xs"
|
|
||||||
title={errorView.title}
|
title={errorView.title}
|
||||||
>
|
detail={errorView.detail}
|
||||||
{errorView.detail}
|
mb="xs"
|
||||||
</Alert>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Stack gap={0} className={classes.inputWrapper}>
|
<Stack gap={0} className={classes.inputWrapper}>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Alert, Box, Text } from "@mantine/core";
|
import { Box, Text } from "@mantine/core";
|
||||||
import { IconAlertTriangle } from "@tabler/icons-react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import type { UIMessage } from "@ai-sdk/react";
|
import type { UIMessage } from "@ai-sdk/react";
|
||||||
import ToolCallCard from "@/features/ai-chat/components/tool-call-card.tsx";
|
import ToolCallCard from "@/features/ai-chat/components/tool-call-card.tsx";
|
||||||
|
import ChatErrorAlert from "@/features/ai-chat/components/chat-error-alert.tsx";
|
||||||
import { ToolUiPart, isToolPart } from "@/features/ai-chat/utils/tool-parts.tsx";
|
import { ToolUiPart, isToolPart } from "@/features/ai-chat/utils/tool-parts.tsx";
|
||||||
import { renderChatMarkdown } from "@/features/ai-chat/utils/markdown.ts";
|
import { renderChatMarkdown } from "@/features/ai-chat/utils/markdown.ts";
|
||||||
import { resolveAssistantName } from "@/features/ai-chat/utils/assistant-name.ts";
|
import { resolveAssistantName } from "@/features/ai-chat/utils/assistant-name.ts";
|
||||||
@@ -118,15 +118,11 @@ export default function MessageItem({
|
|||||||
// cause plus a one-line detail.
|
// cause plus a one-line detail.
|
||||||
const errorView = describeChatError(errorText, t);
|
const errorView = describeChatError(errorText, t);
|
||||||
return (
|
return (
|
||||||
<Alert
|
<ChatErrorAlert
|
||||||
variant="light"
|
|
||||||
color="red"
|
|
||||||
icon={<IconAlertTriangle size={16} />}
|
|
||||||
mt={4}
|
|
||||||
title={errorView.title}
|
title={errorView.title}
|
||||||
>
|
detail={errorView.detail}
|
||||||
{errorView.detail}
|
mt={4}
|
||||||
</Alert>
|
/>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
Reference in New Issue
Block a user