tool.tsx 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. 'use client';
  2. import { Badge } from '@/components/ui/badge';
  3. import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
  4. import { cn } from '@/lib/utils';
  5. import type { ToolUIPart } from 'ai';
  6. import {
  7. CheckCircleIcon,
  8. ChevronDownIcon,
  9. CircleIcon,
  10. ClockIcon,
  11. WrenchIcon,
  12. XCircleIcon,
  13. } from 'lucide-react';
  14. import type { ComponentProps, ReactNode } from 'react';
  15. import { isValidElement } from 'react';
  16. import { CodeBlock } from './code-block';
  17. export type ToolProps = ComponentProps<typeof Collapsible>;
  18. export const Tool = ({ className, ...props }: ToolProps) => (
  19. <Collapsible className={cn('not-prose mb-4 w-full rounded-md border', className)} {...props} />
  20. );
  21. export type ToolHeaderProps = {
  22. title?: string;
  23. type: ToolUIPart['type'];
  24. state: ToolUIPart['state'];
  25. className?: string;
  26. };
  27. const getStatusBadge = (status: ToolUIPart['state']) => {
  28. const labels: Record<ToolUIPart['state'], string> = {
  29. 'input-streaming': 'Pending',
  30. 'input-available': 'Running',
  31. 'approval-requested': 'Awaiting Approval',
  32. 'approval-responded': 'Responded',
  33. 'output-available': 'Completed',
  34. 'output-error': 'Error',
  35. 'output-denied': 'Denied',
  36. };
  37. const icons: Record<ToolUIPart['state'], ReactNode> = {
  38. 'input-streaming': <CircleIcon className="size-4" />,
  39. 'input-available': <ClockIcon className="size-4 animate-pulse" />,
  40. 'approval-requested': <ClockIcon className="size-4 text-yellow-600" />,
  41. 'approval-responded': <CheckCircleIcon className="size-4 text-blue-600" />,
  42. 'output-available': <CheckCircleIcon className="size-4 text-green-600" />,
  43. 'output-error': <XCircleIcon className="size-4 text-red-600" />,
  44. 'output-denied': <XCircleIcon className="size-4 text-orange-600" />,
  45. };
  46. return (
  47. <Badge className="gap-1.5 rounded-full text-xs" variant="secondary">
  48. {icons[status]}
  49. {labels[status]}
  50. </Badge>
  51. );
  52. };
  53. export const ToolHeader = ({ className, title, type, state, ...props }: ToolHeaderProps) => (
  54. <CollapsibleTrigger
  55. className={cn('flex w-full items-center justify-between gap-4 p-3', className)}
  56. {...props}
  57. >
  58. <div className="flex items-center gap-2">
  59. <WrenchIcon className="size-4 text-muted-foreground" />
  60. <span className="font-medium text-sm">{title ?? type.split('-').slice(1).join('-')}</span>
  61. {getStatusBadge(state)}
  62. </div>
  63. <ChevronDownIcon className="size-4 text-muted-foreground transition-transform group-data-[state=open]:rotate-180" />
  64. </CollapsibleTrigger>
  65. );
  66. export type ToolContentProps = ComponentProps<typeof CollapsibleContent>;
  67. export const ToolContent = ({ className, ...props }: ToolContentProps) => (
  68. <CollapsibleContent
  69. className={cn(
  70. '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',
  71. className,
  72. )}
  73. {...props}
  74. />
  75. );
  76. export type ToolInputProps = ComponentProps<'div'> & {
  77. input: ToolUIPart['input'];
  78. };
  79. export const ToolInput = ({ className, input, ...props }: ToolInputProps) => (
  80. <div className={cn('space-y-2 overflow-hidden p-4', className)} {...props}>
  81. <h4 className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
  82. Parameters
  83. </h4>
  84. <div className="rounded-md bg-muted/50">
  85. <CodeBlock code={JSON.stringify(input, null, 2)} language="json" />
  86. </div>
  87. </div>
  88. );
  89. export type ToolOutputProps = ComponentProps<'div'> & {
  90. output: ToolUIPart['output'];
  91. errorText: ToolUIPart['errorText'];
  92. };
  93. export const ToolOutput = ({ className, output, errorText, ...props }: ToolOutputProps) => {
  94. if (!(output || errorText)) {
  95. return null;
  96. }
  97. let Output = <div>{output as ReactNode}</div>;
  98. if (typeof output === 'object' && !isValidElement(output)) {
  99. Output = <CodeBlock code={JSON.stringify(output, null, 2)} language="json" />;
  100. } else if (typeof output === 'string') {
  101. Output = <CodeBlock code={output} language="json" />;
  102. }
  103. return (
  104. <div className={cn('space-y-2 p-4', className)} {...props}>
  105. <h4 className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
  106. {errorText ? 'Error' : 'Result'}
  107. </h4>
  108. <div
  109. className={cn(
  110. 'overflow-x-auto rounded-md text-xs [&_table]:w-full',
  111. errorText ? 'bg-destructive/10 text-destructive' : 'bg-muted/50 text-foreground',
  112. )}
  113. >
  114. {errorText && <div>{errorText}</div>}
  115. {Output}
  116. </div>
  117. </div>
  118. );
  119. };