Guardrail notice

A calm note that part of a reply was left out or a request was declined, with the reason one click away.

Loading preview…

Install

  1. 1

    Run this command in your Tailwind CSS project. It adds components/ai/guardrail-notice.tsx and installs its dependencies.

    $ npx shadcn@latest add https://gradientdeck.com/ai-components/r/guardrail-notice.json

Usage & API

Example

guardrail-notice.tsx
import { GuardrailNotice } from "@/components/ai/guardrail-notice"

<GuardrailNotice
  reason="…"
/>

Props GuardrailNotice

PropType & descriptionRequired
kind"withheld" | "declined"Optional
titlestringOptional
reasonstringRequired
suggestionsstring[]Optional
defaultOpenbooleanOptional

Dependencies

  • React
  • Tailwind CSS
  • motion
  • lucide-react
Implementation notes

The inset is full width, lightly filled, with the heading at 13 pixels so it reads as a note under the answer rather than a second title. "Why?" is a short button. Opening animates the panel height from zero. The content is the reason, then the optional list. Each suggestion is plain text with a small dot, not a link and not a button. The demo's two lines are distinct: one names the order id ORD-58213, the other asks for the last four digits of the card. The reason is not truncated, and there is no "learn more" link and no thumbs control. defaultOpen is the only way to start expanded. The demo leaves it false, so the card-number explanation stays behind the click, which matches the point of not repeating payment details in the open transcript. Pass kind "declined" when the heading should refuse the whole request. Pass title when neither stock sentence matches the policy you actually applied. The shield does not change between the two kinds. Nothing in the file inspects the reason string to pick a kind for you.

Choosing a layout

Use the withheld kind when a partial answer is still worth showing, which is the shape of the demo. The person can read the summary and open the reason only if they want to know what was removed. Use the declined kind when there is no partial answer and the heading itself should refuse the request. In both cases you write the reason. The component does not classify text, and it does not call a model. It will not invent suggestions either. Pass none, and the "What you can do" block is omitted, so a bare refusal does not pretend there is a workaround.

Do not use it for a failed request you intend to send again. Model unavailable has the countdown, the retry button, and the switch to a fallback model, and its copy says the message was saved and nothing was charged. A guardrail notice has none of those controls, and it does not say anything about billing. Do not use it for an empty allowance either. The reached quota card and Credit top-up already cover running out of messages and buying more. This notice belongs in the transcript, under or instead of the part you removed. Closing the reason is the same Why button that opened it. It toggles. There is no separate dismiss icon.

Does kind change anything besides the heading?

No. The shield, the Why toggle, the reason paragraph, and the suggestions list render the same way for "withheld" and "declined". title wins over both stock sentences, even if you also pass a kind. The demo never sets kind, and the answer paragraph above the notice is not inside GuardrailNotice. If that paragraph should disappear on a hard refusal, you omit it yourself and pass kind "declined" or a custom title. The component will not infer a refusal from words in the reason.

Can a suggestion be a button or a link?

Not through the props. suggestions is an array of strings, drawn as list items. The order id in the demo is text, not a control that opens the billing dashboard. There is no click handler on a suggestion and no callback when the panel opens, other than the local toggle. The Why button is the only button. defaultOpen lets you skip that click when the reason should be visible immediately, for example a declined request with no safe partial answer above the notice. An empty string in the array would still render a row, so pass only the lines you want seen.