import type { CSSProperties, ReactNode } from 'react'; export type RefusalReason = | 'safety' | 'policy' | 'capability' | 'authorization' | 'rate-limit' | 'unknown'; export interface RefusalAction { label: string; onClick: () => void; /** Render this action as the primary CTA. Default: false. */ primary?: boolean; } export interface RefusalCardProps { /** * Why the model declined. Drives the icon + the friendly heading * shown above the explanation. */ reason: RefusalReason; /** Plain-language explanation. Keep it short, kind, specific. */ explanation: string; /** Optional title override; defaults to a reason-specific heading. */ title?: string; /** Up to 3 actions the user can take next. */ actions?: RefusalAction[]; /** Optional appendix slot — links to policy doc, support, etc. */ footer?: ReactNode; className?: string; style?: CSSProperties; } const HEADINGS: Record = { safety: { title: "I can't help with that", icon: '🛡️' }, policy: { title: 'Outside policy', icon: '📕' }, capability: { title: "I'm not sure I can help here", icon: '🤔' }, authorization: { title: 'Permission required', icon: '🔒' }, 'rate-limit': { title: 'Easy there — slow down', icon: '⏳' }, unknown: { title: 'Declined', icon: '⚪' }, }; /** * `` — honest, kind, structured "no" from the model. * * Trust surface (Wave 13.C.3). The pattern every product reinvents * poorly: when the LLM declines, we shouldn't dump the raw refusal * string into the chat. Instead we render a small card with: * - a reason-typed heading + glyph * - one paragraph of plain-language explanation * - up to three actionable next steps * * Calm, not alarming. Token-tinted with the warning palette but never * red — refusals are not errors. */ export function RefusalCard({ reason, explanation, title, actions, footer, className, style, }: RefusalCardProps) { const heading = HEADINGS[reason] ?? HEADINGS.unknown; const acts = (actions ?? []).slice(0, 3); return (

{title ?? heading.title}

{explanation}

{acts.length > 0 && (
{acts.map((a, i) => ( ))}
)} {footer && ( )}
); }