Message error
What to show in place of a reply that failed, hit a usage limit or was stopped partway.
Loading preview…
Install
- 1
Run this command in your Tailwind CSS project. It adds
components/ai/message-error-error.tsxand installs its dependencies.$ npx shadcn@latest add https://gradientdeck.com/ai-components/r/message-error-error.json
Usage & API
Example
import { MessageError } from "@/components/ai/message-error-error"
<MessageError
onRetry={() => {}}
/>Props MessageError
| Prop | Type & description | Required |
|---|---|---|
title | string | Optional |
detail | string | Optional |
onRetry | () => void | Required |
retrying | boolean | Optional |
Dependencies
- React
- Tailwind CSS
- motion
- lucide-react
Implementation notes
Failed reply is MessageError. The icon is a triangle in a red tile. title and detail are paragraphs, so you can replace the defaults without restyling. The demo's title is Connection lost and the detail matches the default about the message being saved. Retry is disabled while retrying is true, and the label becomes Retrying. The spin is a linear 0.9 second loop. The alert enters with a small rise in the demo's wrapper, and it leaves when the state becomes ok. That enter and exit are the demo's AnimatePresence, not props. Pass a new title for a safety block or a timeout if the default something-went-wrong is too vague. The partial reply above the alert is ordinary text. The component does not truncate it.
Example
import { UsageLimitNotice } from "@/components/ai/message-error-limit"
<UsageLimitNotice
resetsAt={resetsAt}
onUpgrade={() => {}}
/>Props UsageLimitNotice
| Prop | Type & description | Required |
|---|---|---|
resetsAt | DateWhen the limit resets. | Required |
plan | string | Optional |
onUpgrade | () => void | Required |
onSwitchModel | () => void | Optional |
fallbackModel | string | Optional |
Dependencies
- React
- Tailwind CSS
- lucide-react
Implementation notes
Usage limit is UsageLimitNotice. role is status. A one-pixel spectral line runs along the top. The clock icon sits in a sand tile. The countdown updates every second. Over an hour it prints hours, then minutes padded to two digits. Under an hour it prints minutes and seconds. The time of day uses the locale's numeric hour and two-digit minute. Upgrade to Pro calls onUpgrade. Keep going with, plus fallbackModel, calls onSwitchModel. Omit either the callback or the model name and that button is absent. The demo writes Opening plans or Switched to Haiku 5.5 into a polite live region. The component does not do that. It also does not know the plan's real reset. You pass resetsAt. plan only changes the heading, not the upgrade label.
Example
import { StoppedNotice } from "@/components/ai/message-error-stopped"
<StoppedNotice
onContinue={() => {}}
/>Props StoppedNotice
| Prop | Type & description | Required |
|---|---|---|
onContinue | () => void | Required |
continuing | boolean | Optional |
Dependencies
- React
- Tailwind CSS
- motion
- lucide-react
Implementation notes
Stopped is StoppedNotice. The line reads Stopped by you, with a stop icon, a vertical rule, and Continue with a play icon. There is no alert role and no red. continuing disables the button and fades it. The demo starts in the stopped stage under a partial Dolomites itinerary, then on continue appends the remaining days behind a caret until the last sentence, and hides the notice when the stage is done. The caret in that demo is local, the same sand block used by Streaming text's default variant, not an import of CaretStream. onContinue is the only action. There is no skip and no speed. If you need Skip to end, use the caret controls instead of this notice.
Choosing a layout
Use Failed reply when the stream died and a retry might work. Leave the partial text above it, the way the demo leaves the wedding toast unfinished, so the person can see that something was said. Use Usage limit when the API refused because the plan is exhausted. The countdown is the reason to wait, and Keep going with the fallback model is the way to stay in the thread without upgrading. The upgrade button always says Upgrade to Pro, even though the plan name in the heading is the plan prop. Use Stopped when the person hit stop and the rest of the answer can still be requested. Streaming text's caret controls include their own Stopped status, Continue and Skip to end. Use this notice when you do not already show that toolbar.
Text shimmer should unmount when you show any of these. Leaving Thinking above a red alert looks like the request is both running and dead. Message actions can stay if there is partial text worth copying, but Regenerate and Retry will confuse people if both are visible. Pick one. Branch picker is irrelevant until you have more than one finished version. The limit card is a status, not an alert, so it does not steal focus. The failure is an alert and will be announced.
Retry spins and the old error is still on screen.
retrying only changes the button. It does not hide the alert, and it does not clear title or detail. Hide the component when your retry succeeds, the way the demo moves from retrying to ok and completes the truncated sentence. If the retry fails, set retrying back to false so the label returns to Retry. The alert stays announced as an alert for as long as it is mounted, so do not leave a resolved failure in the tree.
The limit card says Upgrade to Pro while the heading names a different plan.
plan is interpolated only into You've hit the plan limit. It defaults to Free. The primary button is fixed copy, Upgrade to Pro, and it always calls onUpgrade. The secondary button is Keep going with plus fallbackModel, and it is omitted unless you pass both fallbackModel and onSwitchModel. The countdown is empty of numbers until mount, on purpose, and then ticks once a second from resetsAt. It will not hit zero and enable the button. Nothing here enables sending. Your composer has to do that when the time passes or the model changes.