|
|
|
|
| import { Badge } from '@/components/ui/badge'; |
| import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; |
| import { cn } from '@/lib/utils'; |
| import type { ToolUIPart } from 'ai'; |
| import { |
| CheckCircleIcon, |
| ChevronDownIcon, |
| CircleIcon, |
| ClockIcon, |
| WrenchIcon, |
| XCircleIcon, |
| } from 'lucide-react'; |
| import type { ComponentProps, ReactNode } from 'react'; |
| import { isValidElement } from 'react'; |
| import { CodeBlock } from './code-block'; |
|
|
| export type ToolProps = ComponentProps<typeof Collapsible>; |
|
|
| export const Tool = ({ className, ...props }: ToolProps) => ( |
| <Collapsible className={cn('not-prose mb-4 w-full rounded-md border', className)} {...props} /> |
| ); |
|
|
| export type ToolHeaderProps = { |
| title?: string; |
| type: ToolUIPart['type']; |
| state: ToolUIPart['state']; |
| className?: string; |
| }; |
|
|
| const getStatusBadge = (status: ToolUIPart['state']) => { |
| const labels: Record<ToolUIPart['state'], string> = { |
| 'input-streaming': 'Pending', |
| 'input-available': 'Running', |
| 'approval-requested': 'Awaiting Approval', |
| 'approval-responded': 'Responded', |
| 'output-available': 'Completed', |
| 'output-error': 'Error', |
| 'output-denied': 'Denied', |
| }; |
|
|
| const icons: Record<ToolUIPart['state'], ReactNode> = { |
| 'input-streaming': <CircleIcon className="size-4" />, |
| 'input-available': <ClockIcon className="size-4 animate-pulse" />, |
| 'approval-requested': <ClockIcon className="size-4 text-yellow-600" />, |
| 'approval-responded': <CheckCircleIcon className="size-4 text-blue-600" />, |
| 'output-available': <CheckCircleIcon className="size-4 text-green-600" />, |
| 'output-error': <XCircleIcon className="size-4 text-red-600" />, |
| 'output-denied': <XCircleIcon className="size-4 text-orange-600" />, |
| }; |
|
|
| return ( |
| <Badge className="gap-1.5 rounded-full text-xs" variant="secondary"> |
| {icons[status]} |
| {labels[status]} |
| </Badge> |
| ); |
| }; |
|
|
| export const ToolHeader = ({ className, title, type, state, ...props }: ToolHeaderProps) => ( |
| <CollapsibleTrigger |
| className={cn('flex w-full items-center justify-between gap-4 p-3', className)} |
| {...props} |
| > |
| <div className="flex items-center gap-2"> |
| <WrenchIcon className="size-4 text-muted-foreground" /> |
| <span className="font-medium text-sm">{title ?? type.split('-').slice(1).join('-')}</span> |
| {getStatusBadge(state)} |
| </div> |
| <ChevronDownIcon className="size-4 text-muted-foreground transition-transform group-data-[state=open]:rotate-180" /> |
| </CollapsibleTrigger> |
| ); |
|
|
| export type ToolContentProps = ComponentProps<typeof CollapsibleContent>; |
|
|
| export const ToolContent = ({ className, ...props }: ToolContentProps) => ( |
| <CollapsibleContent |
| className={cn( |
| 'data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-popover-foreground outline-none data-[state=closed]:animate-out data-[state=open]:animate-in', |
| className, |
| )} |
| {...props} |
| /> |
| ); |
|
|
| export type ToolInputProps = ComponentProps<'div'> & { |
| input: ToolUIPart['input']; |
| }; |
|
|
| export const ToolInput = ({ className, input, ...props }: ToolInputProps) => ( |
| <div className={cn('space-y-2 overflow-hidden p-4', className)} {...props}> |
| <h4 className="font-medium text-muted-foreground text-xs uppercase tracking-wide"> |
| Parameters |
| </h4> |
| <div className="rounded-md bg-muted/50"> |
| <CodeBlock code={JSON.stringify(input, null, 2)} language="json" /> |
| </div> |
| </div> |
| ); |
|
|
| export type ToolOutputProps = ComponentProps<'div'> & { |
| output: ToolUIPart['output']; |
| errorText: ToolUIPart['errorText']; |
| }; |
|
|
| export const ToolOutput = ({ className, output, errorText, ...props }: ToolOutputProps) => { |
| if (!(output || errorText)) { |
| return null; |
| } |
|
|
| let Output = <div>{output as ReactNode}</div>; |
|
|
| if (typeof output === 'object' && !isValidElement(output)) { |
| Output = <CodeBlock code={JSON.stringify(output, null, 2)} language="json" />; |
| } else if (typeof output === 'string') { |
| Output = <CodeBlock code={output} language="json" />; |
| } |
|
|
| return ( |
| <div className={cn('space-y-2 p-4', className)} {...props}> |
| <h4 className="font-medium text-muted-foreground text-xs uppercase tracking-wide"> |
| {errorText ? 'Error' : 'Result'} |
| </h4> |
| <div |
| className={cn( |
| 'overflow-x-auto rounded-md text-xs [&_table]:w-full', |
| errorText ? 'bg-destructive/10 text-destructive' : 'bg-muted/50 text-foreground', |
| )} |
| > |
| {errorText && <div>{errorText}</div>} |
| {Output} |
| </div> |
| </div> |
| ); |
| }; |
|
|