Model unavailable
Shown when the chosen model is busy, with an automatic retry countdown and a switch to another model.
Loading preview…
Install
- 1
Run this command in your Tailwind CSS project. It adds
components/ai/model-unavailable.tsxand installs its dependencies.$ npx shadcn@latest add https://gradientdeck.com/ai-components/r/model-unavailable.json
Usage & API
Example
import { ModelUnavailable } from "@/components/ai/model-unavailable"
<ModelUnavailable
model="…"
fallback="…"
status={status}
retryIn={1}
retryTotal={1}
onRetry={() => {}}
onSwitch={() => {}}
/>Props ModelUnavailable
| Prop | Type & description | Required |
|---|---|---|
model | string | Required |
fallback | string | Required |
status | Status | Required |
retryIn | numberSeconds until the next automatic retry. | Required |
retryTotal | number | Required |
onRetry | () => void | Required |
onSwitch | () => void | Required |
onReset | () => void | Optional |
Dependencies
- React
- Tailwind CSS
- motion
- lucide-react
Implementation notes
Busy and done swap in the same card. There is no third layout. ready and switched share the green check tile. Only the heading and the optional reset label differ. The countdown ring's sand stroke sits on a faint track. Reduced motion sets that stroke's transition to zero duration. The enter and exit motion of the two blocks still runs. Retry is the button that disables, and only while status is retrying. Switch never disables. The demo starts on waiting at 12 seconds. While waiting, it subtracts one each second and calls retry at zero. retry moves to retrying, increments an attempt count, and waits 1.6 seconds. If that count is still under 2, it puts retryIn back to 12 and returns to waiting, so the first try fails on purpose and the ring starts full again. The second attempt sets ready, which shows the check, the "is back" heading, and Replay. Clicking Retry during the first wait calls that same function, so the click counts as the attempt that fails and the ring starts over at 12 instead of running out the clock. The automatic fire at zero counts too. Switching clears the demo's timeout and sets switched immediately, even if a retry was in flight, and the side button then reads Undo. Both Replay and Undo call onReset. The component does not remember the previous model. Whatever undo should restore is up to the handler you pass. Without onReset, the done state has no button at all, only the check and the two sentences.
Choosing a layout
Use it in the thread, directly under the message that did not go out, which is how the demo is arranged. The person can see that their text is still on screen, that a retry is coming, and that another model is one click away. Keep status on waiting while the countdown runs. Move it to retrying when the attempt actually starts. The button and an automatic timer should call the same onRetry if you want them to share one attempt counter, which is what the demo does. There is no network call in the file. You decide what a retry means, including whether the first one is allowed to fail.
Do not use it for a plan cap. "Nothing was charged" is the wrong sentence if the person is out of messages, and the actions are wrong too. Quota banner's reached card offers an upgrade path and a reset time, and Credit top-up takes the payment when the recovery is bought credits. Do not use it for a safety refusal. Guardrail notice has a reason and optional next steps, and it has no countdown ring. Pick this notice when the failure is capacity on a named model, you have a fallback string, and you can honestly say the message is saved. If you cannot retry on a timer, you still pass retryIn and retryTotal, because the waiting layout always draws them. Hiding the countdown means choosing a different status, not omitting the props.
What does the countdown ring actually measure?
Remaining time in the current wait, not a health score for the model. The stroke offset is the circumference times one minus retryIn over retryTotal. The demo starts both at 12, so the ring begins full and drains toward empty. The component never reads the clock. If retryIn is larger than retryTotal, the offset goes the wrong way and the arc will not match the "in Ns" label, so keep the count inside that range. At zero the label would read "in 0s" until you change status. During retrying the ring unmounts, which is why a slow attempt shows a spinner instead of a frozen arc. The spinner is the only indication inside the button while onRetry's work is still running.
Does "Sending your message now" submit anything?
No. That sentence is static copy on the ready and switched branches. ModelUnavailable has no message prop and no submit handler. The meeting-notes bubble in the demo is a sibling rendered by the demo function. onRetry and onSwitch are the actions, plus optional onReset, which the demo uses to return to waiting rather than to resend. If the real request should leave when status becomes ready, start it in the code that sets that status. The polite live region will announce the heading change, including "is back" or "Switched to" the fallback name, because the whole card is aria-live polite. The busy sentence about nothing being charged is not repeated once the check mark shows.