mirror of
https://github.com/vercel/ai-elements.git
synced 2026-09-14 19:48:26 +08:00
Release cleanup (#174)
* Bump deps * Update package.json * Update shadcn/ui * Run Ultracite * Run Ultracite * Fix unit tests * Update prompt-input.tsx * Build fixes * Modify docs styles * Fix terminal titles * Update code-block.mdx * Add tool-approval docs and examples * Rename ToolApproval to Confirmation * Fix confirmation * Misc fixes * Update test.yml * Downgrade jsdom * Update confirmation.test.tsx
This commit is contained in:
@@ -32,8 +32,5 @@ jobs:
|
||||
- name: Install Dependencies
|
||||
run: pnpm i
|
||||
|
||||
- name: Run Check
|
||||
run: pnpm check
|
||||
|
||||
- name: Run Tests
|
||||
run: pnpm test
|
||||
|
||||
@@ -14,7 +14,7 @@ const Layout = ({ children }: LayoutProps<"/">) => (
|
||||
sidebar={{
|
||||
collapsible: false,
|
||||
tabs: [],
|
||||
className: "bg-background! transition-none!",
|
||||
className: "bg-background! transition-none! border-none!",
|
||||
}}
|
||||
tree={source.pageTree}
|
||||
>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@import 'tailwindcss';
|
||||
@import 'fumadocs-ui/css/shadcn.css';
|
||||
@import 'fumadocs-ui/css/preset.css';
|
||||
@import "tailwindcss";
|
||||
@import "fumadocs-ui/css/shadcn.css";
|
||||
@import "fumadocs-ui/css/preset.css";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@source "../**/*.{ts,tsx,mdx}";
|
||||
@@ -55,7 +55,7 @@
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(57.61% .2508 258.23);
|
||||
--primary: oklch(57.61% 0.2508 258.23);
|
||||
--primary-foreground: oklch(1 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
@@ -91,7 +91,7 @@
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(57.61% .2508 258.23);
|
||||
--primary: oklch(57.61% 0.2508 258.23);
|
||||
--primary-foreground: oklch(1 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
@@ -161,7 +161,7 @@
|
||||
@apply fill-none;
|
||||
}
|
||||
|
||||
.prose h1,
|
||||
.prose h1,
|
||||
.prose h2,
|
||||
.prose h3,
|
||||
.prose h4,
|
||||
@@ -174,4 +174,4 @@
|
||||
--fd-nav-height: 57px;
|
||||
--fd-page-width: 100%;
|
||||
--fd-layout-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ const LogoGithub = () => (
|
||||
);
|
||||
|
||||
export const ClientNavbar = ({ pages }: { pages: NavPageItem[] }) => (
|
||||
<div className="fixed top-0 right-0 left-0 z-40 flex items-center justify-between border-b bg-background py-2.5">
|
||||
<div className="fixed top-0 right-0 left-0 z-40 flex items-center justify-between bg-background/80 backdrop-blur-sm py-2.5">
|
||||
<div className="flex select-none flex-row items-center">
|
||||
<div className="flex shrink-0 flex-row items-center gap-2">
|
||||
<HomeLinks />
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
title: Code Block
|
||||
description: Provides syntax highlighting, line numbers, and copy to clipboard functionality for code blocks.
|
||||
path: elements/components/code-block
|
||||
icon: Braces
|
||||
---
|
||||
|
||||
The `CodeBlock` component provides syntax highlighting, line numbers, and copy to clipboard functionality for code blocks.
|
||||
|
||||
@@ -0,0 +1,352 @@
|
||||
---
|
||||
title: Confirmation
|
||||
description: An alert-based component for managing tool execution approval workflows with request, accept, and reject states.
|
||||
path: elements/components/confirmation
|
||||
---
|
||||
|
||||
The `Confirmation` component provides a flexible system for displaying tool approval requests and their outcomes. Perfect for showing users when AI tools require approval before execution, and displaying the approval status afterward.
|
||||
|
||||
<Preview path="confirmation" />
|
||||
|
||||
## Installation
|
||||
|
||||
<ElementsInstaller path="confirmation" />
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
import {
|
||||
Confirmation,
|
||||
ConfirmationContent,
|
||||
ConfirmationRequest,
|
||||
ConfirmationAccepted,
|
||||
ConfirmationRejected,
|
||||
ConfirmationActions,
|
||||
ConfirmationAction,
|
||||
} from '@/components/ai-elements/confirmation';
|
||||
```
|
||||
|
||||
```tsx
|
||||
<Confirmation approval={{ id: 'tool-1' }} state="approval-requested">
|
||||
<ConfirmationContent>
|
||||
<ConfirmationRequest>
|
||||
This tool wants to access your file system. Do you approve?
|
||||
</ConfirmationRequest>
|
||||
<ConfirmationAccepted>
|
||||
<CheckIcon className="size-4" />
|
||||
<span>Approved</span>
|
||||
</ConfirmationAccepted>
|
||||
<ConfirmationRejected>
|
||||
<XIcon className="size-4" />
|
||||
<span>Rejected</span>
|
||||
</ConfirmationRejected>
|
||||
</ConfirmationContent>
|
||||
<ConfirmationActions>
|
||||
<ConfirmationAction variant="outline" onClick={handleReject}>
|
||||
Reject
|
||||
</ConfirmationAction>
|
||||
<ConfirmationAction variant="default" onClick={handleApprove}>
|
||||
Approve
|
||||
</ConfirmationAction>
|
||||
</ConfirmationActions>
|
||||
</Confirmation>
|
||||
```
|
||||
|
||||
## Usage with AI SDK
|
||||
|
||||
Build a chat UI with tool approval workflow where dangerous tools require user confirmation before execution.
|
||||
|
||||
Add the following component to your frontend:
|
||||
|
||||
```tsx title="app/page.tsx"
|
||||
'use client';
|
||||
|
||||
import { useChat } from '@ai-sdk/react';
|
||||
import { DefaultChatTransport, type ToolUIPart } from 'ai';
|
||||
import { useState } from 'react';
|
||||
import { CheckIcon, XIcon } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Confirmation,
|
||||
ConfirmationContent,
|
||||
ConfirmationRequest,
|
||||
ConfirmationAccepted,
|
||||
ConfirmationRejected,
|
||||
ConfirmationActions,
|
||||
ConfirmationAction,
|
||||
} from '@/components/ai-elements/confirmation';
|
||||
import { Response } from '@/components/ai-elements/response';
|
||||
|
||||
type DeleteFileInput = {
|
||||
filePath: string;
|
||||
confirm: boolean;
|
||||
};
|
||||
|
||||
type DeleteFileToolUIPart = ToolUIPart<{
|
||||
delete_file: {
|
||||
input: DeleteFileInput;
|
||||
output: { success: boolean; message: string };
|
||||
};
|
||||
}>;
|
||||
|
||||
const Example = () => {
|
||||
const { messages, sendMessage, status, respondToConfirmationRequest } = useChat({
|
||||
transport: new DefaultChatTransport({
|
||||
api: '/api/chat',
|
||||
}),
|
||||
});
|
||||
|
||||
const handleDeleteFile = () => {
|
||||
sendMessage({ text: 'Delete the file at /tmp/example.txt' });
|
||||
};
|
||||
|
||||
const latestMessage = messages[messages.length - 1];
|
||||
const deleteTool = latestMessage?.parts?.find(
|
||||
(part) => part.type === 'tool-delete_file'
|
||||
) as DeleteFileToolUIPart | undefined;
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto p-6 relative size-full rounded-lg border h-[600px]">
|
||||
<div className="flex flex-col h-full space-y-4">
|
||||
<Button onClick={handleDeleteFile} disabled={status !== 'ready'}>
|
||||
Delete Example File
|
||||
</Button>
|
||||
|
||||
{deleteTool?.approval && (
|
||||
<Confirmation approval={deleteTool.approval} state={deleteTool.state}>
|
||||
<ConfirmationContent>
|
||||
<ConfirmationRequest>
|
||||
This tool wants to delete: <code>{deleteTool.input?.filePath}</code>
|
||||
<br />
|
||||
Do you approve this action?
|
||||
</ConfirmationRequest>
|
||||
<ConfirmationAccepted>
|
||||
<CheckIcon className="size-4" />
|
||||
<span>You approved this tool execution</span>
|
||||
</ConfirmationAccepted>
|
||||
<ConfirmationRejected>
|
||||
<XIcon className="size-4" />
|
||||
<span>You rejected this tool execution</span>
|
||||
</ConfirmationRejected>
|
||||
</ConfirmationContent>
|
||||
<ConfirmationActions>
|
||||
<ConfirmationAction
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
respondToConfirmationRequest({
|
||||
approvalId: deleteTool.approval!.id,
|
||||
approved: false,
|
||||
})
|
||||
}
|
||||
>
|
||||
Reject
|
||||
</ConfirmationAction>
|
||||
<ConfirmationAction
|
||||
variant="default"
|
||||
onClick={() =>
|
||||
respondToConfirmationRequest({
|
||||
approvalId: deleteTool.approval!.id,
|
||||
approved: true,
|
||||
})
|
||||
}
|
||||
>
|
||||
Approve
|
||||
</ConfirmationAction>
|
||||
</ConfirmationActions>
|
||||
</Confirmation>
|
||||
)}
|
||||
|
||||
{deleteTool?.output && (
|
||||
<Response>
|
||||
{deleteTool.output.success
|
||||
? deleteTool.output.message
|
||||
: `Error: ${deleteTool.output.message}`}
|
||||
</Response>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Example;
|
||||
```
|
||||
|
||||
Add the following route to your backend:
|
||||
|
||||
```ts title="app/api/chat/route.tsx"
|
||||
import { streamText, UIMessage, convertToModelMessages } from 'ai';
|
||||
import { z } from 'zod';
|
||||
|
||||
// Allow streaming responses up to 30 seconds
|
||||
export const maxDuration = 30;
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const { messages }: { messages: UIMessage[] } = await req.json();
|
||||
|
||||
const result = streamText({
|
||||
model: 'openai/gpt-4o',
|
||||
messages: convertToModelMessages(messages),
|
||||
tools: {
|
||||
delete_file: {
|
||||
description: 'Delete a file from the file system',
|
||||
parameters: z.object({
|
||||
filePath: z.string().describe('The path to the file to delete'),
|
||||
confirm: z
|
||||
.boolean()
|
||||
.default(false)
|
||||
.describe('Confirmation that the user wants to delete the file'),
|
||||
}),
|
||||
requireApproval: true, // Enable approval workflow
|
||||
execute: async ({ filePath, confirm }) => {
|
||||
if (!confirm) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Deletion not confirmed',
|
||||
};
|
||||
}
|
||||
|
||||
// Simulate file deletion
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Successfully deleted ${filePath}`,
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return result.toUIMessageStreamResponse();
|
||||
}
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- Context-based state management for approval workflow
|
||||
- Conditional rendering based on approval state
|
||||
- Support for approval-requested, approval-responded, output-denied, and output-available states
|
||||
- Built on shadcn/ui Alert and Button components
|
||||
- TypeScript support with comprehensive type definitions
|
||||
- Customizable styling with Tailwind CSS
|
||||
- Keyboard navigation and accessibility support
|
||||
- Theme-aware with automatic dark mode support
|
||||
|
||||
## Examples
|
||||
|
||||
### Approval Request State
|
||||
|
||||
Shows the approval request with action buttons when state is `approval-requested`.
|
||||
|
||||
<Preview path="confirmation-request" />
|
||||
|
||||
### Approved State
|
||||
|
||||
Shows the accepted status when user approves and state is `approval-responded` or `output-available`.
|
||||
|
||||
<Preview path="confirmation-accepted" />
|
||||
|
||||
### Rejected State
|
||||
|
||||
Shows the rejected status when user rejects and state is `output-denied`.
|
||||
|
||||
<Preview path="confirmation-rejected" />
|
||||
|
||||
## Props
|
||||
|
||||
### `<Confirmation />`
|
||||
|
||||
<TypeTable
|
||||
type={{
|
||||
approval: {
|
||||
description: 'The approval object containing the approval ID and status. If not provided or undefined, the component will not render.',
|
||||
type: 'ToolUIPart["approval"]',
|
||||
},
|
||||
state: {
|
||||
description: 'The current state of the tool (input-streaming, input-available, approval-requested, approval-responded, output-denied, or output-available). Will not render for input-streaming or input-available states.',
|
||||
type: 'ToolUIPart["state"]',
|
||||
},
|
||||
className: {
|
||||
description: 'Additional CSS classes to apply to the Alert component.',
|
||||
type: 'string',
|
||||
},
|
||||
'...props': {
|
||||
description: 'Any other props are spread to the Alert component.',
|
||||
type: 'React.ComponentProps<typeof Alert>',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
### `<ConfirmationContent />`
|
||||
|
||||
<TypeTable
|
||||
type={{
|
||||
className: {
|
||||
description: 'Additional CSS classes to apply to the AlertDescription component.',
|
||||
type: 'string',
|
||||
},
|
||||
'...props': {
|
||||
description: 'Any other props are spread to the AlertDescription component.',
|
||||
type: 'React.ComponentProps<typeof AlertDescription>',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
### `<ConfirmationRequest />`
|
||||
|
||||
<TypeTable
|
||||
type={{
|
||||
children: {
|
||||
description: 'The content to display when approval is requested. Only renders when state is "approval-requested".',
|
||||
type: 'React.ReactNode',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
### `<ConfirmationAccepted />`
|
||||
|
||||
<TypeTable
|
||||
type={{
|
||||
children: {
|
||||
description: 'The content to display when approval is accepted. Only renders when approval.approved is true and state is "approval-responded", "output-denied", or "output-available".',
|
||||
type: 'React.ReactNode',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
### `<ConfirmationRejected />`
|
||||
|
||||
<TypeTable
|
||||
type={{
|
||||
children: {
|
||||
description: 'The content to display when approval is rejected. Only renders when approval.approved is false and state is "approval-responded", "output-denied", or "output-available".',
|
||||
type: 'React.ReactNode',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
### `<ConfirmationActions />`
|
||||
|
||||
<TypeTable
|
||||
type={{
|
||||
className: {
|
||||
description: 'Additional CSS classes to apply to the actions container.',
|
||||
type: 'string',
|
||||
},
|
||||
'...props': {
|
||||
description: 'Any other props are spread to the div element. Only renders when state is "approval-requested".',
|
||||
type: 'React.ComponentProps<"div">',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
### `<ConfirmationAction />`
|
||||
|
||||
<TypeTable
|
||||
type={{
|
||||
'...props': {
|
||||
description: 'Any other props are spread to the Button component. Styled with h-8 px-3 text-sm classes by default.',
|
||||
type: 'React.ComponentProps<typeof Button>',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
@@ -15,7 +15,7 @@ Double-check that:
|
||||
- Your components.json file (if using shadcn-style config) is set up correctly.
|
||||
- You’re using the latest version of the AI Elements CLI:
|
||||
|
||||
```bash
|
||||
```bash title="Terminal"
|
||||
npx ai-elements@latest
|
||||
```
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"fumadocs-core": "16.0.4",
|
||||
"fumadocs-mdx": "13.0.2",
|
||||
"fumadocs-ui": "16.0.4",
|
||||
"lucide-react": "^0.546.0",
|
||||
"lucide-react": "^0.548.0",
|
||||
"next": "16.0.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
@@ -27,10 +27,10 @@
|
||||
"remark": "^15.0.1",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"remark-mdx": "^3.1.1",
|
||||
"shiki": "3.13.0"
|
||||
"shiki": "3.14.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.1.15",
|
||||
"@tailwindcss/postcss": "^4.1.16",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/node": "24.9.1",
|
||||
"@types/react": "^19.2.2",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export default {
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
defineDocs,
|
||||
frontmatterSchema,
|
||||
metaSchema,
|
||||
} from 'fumadocs-mdx/config';
|
||||
} from "fumadocs-mdx/config";
|
||||
|
||||
// You can customise Zod schemas for frontmatter and `meta.json` here
|
||||
// see https://fumadocs.dev/docs/mdx/collections#define-docs
|
||||
|
||||
+5
-17
@@ -2,11 +2,7 @@
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"target": "ESNext",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
@@ -20,15 +16,9 @@
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"paths": {
|
||||
"@/.source": [
|
||||
"./.source/index.ts"
|
||||
],
|
||||
"@/*": [
|
||||
"./*"
|
||||
],
|
||||
"@repo/*": [
|
||||
"../../packages/*"
|
||||
]
|
||||
"@/.source": ["./.source/index.ts"],
|
||||
"@/*": ["./*"],
|
||||
"@repo/*": ["../../packages/*"]
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
@@ -44,7 +34,5 @@
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"next": "16.0.0",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"shadcn": "^3.4.2",
|
||||
"shadcn": "^3.5.0",
|
||||
"ts-morph": "^27.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
@@ -23,9 +19,7 @@
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
@@ -35,7 +29,5 @@
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
+5
-2
@@ -1,13 +1,16 @@
|
||||
{
|
||||
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
|
||||
"extends": ["ultracite"],
|
||||
"extends": ["ultracite/core", "ultracite/react", "ultracite/next"],
|
||||
"files": {
|
||||
"includes": ["**/*", "!packages/shadcn-ui/**/*"]
|
||||
"includes": ["**/*", "!packages/shadcn-ui"]
|
||||
},
|
||||
"linter": {
|
||||
"rules": {
|
||||
"performance": {
|
||||
"noImgElement": "off"
|
||||
},
|
||||
"suspicious": {
|
||||
"noConsole": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -8,16 +8,16 @@
|
||||
"test:coverage": "turbo run test:coverage",
|
||||
"check": "npx ultracite@latest check",
|
||||
"fix": "npx ultracite@latest fix",
|
||||
"bump-deps": "npx npm-check-updates --deep -u -x recharts,react-syntax-highlighter,@types/react-syntax-highlighter && pnpm install",
|
||||
"bump-deps": "npx npm-check-updates --deep -u -x recharts && pnpm install",
|
||||
"bump-ui": "npx shadcn@latest add --all --overwrite -c packages/shadcn-ui && npx shadcn@latest migrate radix -c packages/shadcn-ui",
|
||||
"changeset": "changeset"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.2.7",
|
||||
"@biomejs/biome": "2.3.1",
|
||||
"@changesets/cli": "^2.29.7",
|
||||
"turbo": "^2.5.8",
|
||||
"typescript": "5.9.3",
|
||||
"ultracite": "5.6.4"
|
||||
"ultracite": "6.0.4"
|
||||
},
|
||||
"packageManager": "pnpm@10.19.0",
|
||||
"engines": {
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { userEvent } from "@testing-library/user-event";
|
||||
import { CheckIcon, XIcon } from "lucide-react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
Confirmation,
|
||||
ConfirmationAccepted,
|
||||
ConfirmationAction,
|
||||
ConfirmationActions,
|
||||
ConfirmationRejected,
|
||||
ConfirmationRequest,
|
||||
} from "../src/confirmation";
|
||||
|
||||
describe("Confirmation", () => {
|
||||
it("renders children when approval is present", () => {
|
||||
render(
|
||||
<Confirmation approval={{ id: "test-id" }} state="approval-requested">
|
||||
<div>Approval Content</div>
|
||||
</Confirmation>
|
||||
);
|
||||
expect(screen.getByText("Approval Content")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not render when approval is not present", () => {
|
||||
const { container } = render(
|
||||
<Confirmation state="input-streaming">
|
||||
<div>Approval Content</div>
|
||||
</Confirmation>
|
||||
);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it("does not render in input-streaming state", () => {
|
||||
const { container } = render(
|
||||
<Confirmation approval={{ id: "test-id" }} state="input-streaming">
|
||||
<div>Approval Content</div>
|
||||
</Confirmation>
|
||||
);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it("does not render in input-available state", () => {
|
||||
const { container } = render(
|
||||
<Confirmation approval={{ id: "test-id" }} state="input-available">
|
||||
<div>Approval Content</div>
|
||||
</Confirmation>
|
||||
);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it("applies custom className", () => {
|
||||
const { container } = render(
|
||||
<Confirmation
|
||||
approval={{ id: "test-id" }}
|
||||
className="custom-class"
|
||||
state="approval-requested"
|
||||
>
|
||||
<div>Content</div>
|
||||
</Confirmation>
|
||||
);
|
||||
expect(container.firstChild).toHaveClass("custom-class");
|
||||
});
|
||||
});
|
||||
|
||||
describe("ConfirmationRequest, ConfirmationAccepted, ConfirmationRejected", () => {
|
||||
it("renders ConfirmationRequest when state is approval-requested", () => {
|
||||
render(
|
||||
<Confirmation approval={{ id: "test-id" }} state="approval-requested">
|
||||
<ConfirmationRequest>Custom approval message</ConfirmationRequest>
|
||||
<ConfirmationAccepted>Accepted</ConfirmationAccepted>
|
||||
<ConfirmationRejected>Rejected</ConfirmationRejected>
|
||||
</Confirmation>
|
||||
);
|
||||
expect(screen.getByText("Custom approval message")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Accepted")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Rejected")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders ConfirmationAccepted when approved and state is approval-responded", () => {
|
||||
render(
|
||||
<Confirmation
|
||||
approval={{ id: "test-id", approved: true }}
|
||||
state="approval-responded"
|
||||
>
|
||||
<ConfirmationRequest>Custom approval message</ConfirmationRequest>
|
||||
<ConfirmationAccepted>
|
||||
<CheckIcon />
|
||||
<span>Accepted</span>
|
||||
</ConfirmationAccepted>
|
||||
<ConfirmationRejected>
|
||||
<XIcon />
|
||||
<span>Rejected</span>
|
||||
</ConfirmationRejected>
|
||||
</Confirmation>
|
||||
);
|
||||
expect(screen.getByText("Accepted")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText("Custom approval message")
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Rejected")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders ConfirmationRejected when not approved and state is output-denied", () => {
|
||||
render(
|
||||
<Confirmation
|
||||
approval={{ id: "test-id", approved: false }}
|
||||
state="output-denied"
|
||||
>
|
||||
<ConfirmationRequest>Custom approval message</ConfirmationRequest>
|
||||
<ConfirmationAccepted>
|
||||
<CheckIcon />
|
||||
<span>Accepted</span>
|
||||
</ConfirmationAccepted>
|
||||
<ConfirmationRejected>
|
||||
<XIcon />
|
||||
<span>Rejected</span>
|
||||
</ConfirmationRejected>
|
||||
</Confirmation>
|
||||
);
|
||||
expect(screen.getByText("Rejected")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText("Custom approval message")
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Accepted")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("ConfirmationActions", () => {
|
||||
it("renders custom children buttons", () => {
|
||||
render(
|
||||
<Confirmation approval={{ id: "test-id" }} state="approval-requested">
|
||||
<ConfirmationActions>
|
||||
<ConfirmationAction variant="outline">Reject</ConfirmationAction>
|
||||
<ConfirmationAction variant="default">Accept</ConfirmationAction>
|
||||
</ConfirmationActions>
|
||||
</Confirmation>
|
||||
);
|
||||
expect(screen.getByText("Accept")).toBeInTheDocument();
|
||||
expect(screen.getByText("Reject")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("hides when state is not approval-requested", () => {
|
||||
render(
|
||||
<Confirmation approval={{ id: "test-id" }} state="approval-responded">
|
||||
<ConfirmationActions>
|
||||
<ConfirmationAction variant="outline">Reject</ConfirmationAction>
|
||||
<ConfirmationAction variant="default">Accept</ConfirmationAction>
|
||||
</ConfirmationActions>
|
||||
</Confirmation>
|
||||
);
|
||||
expect(screen.queryByText("Accept")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Reject")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows when state is approval-requested", () => {
|
||||
render(
|
||||
<Confirmation approval={{ id: "test-id" }} state="approval-requested">
|
||||
<ConfirmationActions>
|
||||
<ConfirmationAction variant="outline">Reject</ConfirmationAction>
|
||||
<ConfirmationAction variant="default">Accept</ConfirmationAction>
|
||||
</ConfirmationActions>
|
||||
</Confirmation>
|
||||
);
|
||||
expect(screen.getByText("Accept")).toBeInTheDocument();
|
||||
expect(screen.getByText("Reject")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("calls onClick when accept button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const handleAccept = vi.fn();
|
||||
render(
|
||||
<Confirmation approval={{ id: "test-id" }} state="approval-requested">
|
||||
<ConfirmationActions>
|
||||
<ConfirmationAction variant="outline">Reject</ConfirmationAction>
|
||||
<ConfirmationAction onClick={handleAccept} variant="default">
|
||||
Accept
|
||||
</ConfirmationAction>
|
||||
</ConfirmationActions>
|
||||
</Confirmation>
|
||||
);
|
||||
|
||||
await user.click(screen.getByText("Accept"));
|
||||
expect(handleAccept).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("calls onClick when reject button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const handleReject = vi.fn();
|
||||
render(
|
||||
<Confirmation approval={{ id: "test-id" }} state="approval-requested">
|
||||
<ConfirmationActions>
|
||||
<ConfirmationAction onClick={handleReject} variant="outline">
|
||||
Reject
|
||||
</ConfirmationAction>
|
||||
<ConfirmationAction variant="default">Accept</ConfirmationAction>
|
||||
</ConfirmationActions>
|
||||
</Confirmation>
|
||||
);
|
||||
|
||||
await user.click(screen.getByText("Reject"));
|
||||
expect(handleReject).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("disables buttons when disabled prop is true", () => {
|
||||
render(
|
||||
<Confirmation approval={{ id: "test-id" }} state="approval-requested">
|
||||
<ConfirmationActions>
|
||||
<ConfirmationAction disabled variant="outline">
|
||||
Reject
|
||||
</ConfirmationAction>
|
||||
<ConfirmationAction disabled variant="default">
|
||||
Accept
|
||||
</ConfirmationAction>
|
||||
</ConfirmationActions>
|
||||
</Confirmation>
|
||||
);
|
||||
expect(screen.getByText("Accept")).toBeDisabled();
|
||||
expect(screen.getByText("Reject")).toBeDisabled();
|
||||
});
|
||||
|
||||
it("applies custom className", () => {
|
||||
render(
|
||||
<Confirmation approval={{ id: "test-id" }} state="approval-requested">
|
||||
<ConfirmationActions className="custom-class">
|
||||
<ConfirmationAction variant="outline">Reject</ConfirmationAction>
|
||||
<ConfirmationAction variant="default">Accept</ConfirmationAction>
|
||||
</ConfirmationActions>
|
||||
</Confirmation>
|
||||
);
|
||||
const actionsContainer = screen.getByText("Accept").parentElement;
|
||||
expect(actionsContainer).toHaveClass("custom-class");
|
||||
});
|
||||
});
|
||||
|
||||
describe("ConfirmationAccepted", () => {
|
||||
it("renders accepted status with icon", () => {
|
||||
render(
|
||||
<Confirmation
|
||||
approval={{ id: "test-id", approved: true }}
|
||||
state="approval-responded"
|
||||
>
|
||||
<ConfirmationRequest>Request</ConfirmationRequest>
|
||||
<ConfirmationAccepted>
|
||||
<CheckIcon className="size-4" />
|
||||
<span>Accepted</span>
|
||||
</ConfirmationAccepted>
|
||||
<ConfirmationRejected>Rejected</ConfirmationRejected>
|
||||
</Confirmation>
|
||||
);
|
||||
expect(screen.getByText("Accepted")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("ConfirmationRejected", () => {
|
||||
it("renders rejected status with icon", () => {
|
||||
render(
|
||||
<Confirmation
|
||||
approval={{ id: "test-id", approved: false }}
|
||||
state="output-denied"
|
||||
>
|
||||
<ConfirmationRequest>Request</ConfirmationRequest>
|
||||
<ConfirmationAccepted>Accepted</ConfirmationAccepted>
|
||||
<ConfirmationRejected>
|
||||
<XIcon className="size-4" />
|
||||
<span>Rejected</span>
|
||||
</ConfirmationRejected>
|
||||
</Confirmation>
|
||||
);
|
||||
expect(screen.getByText("Rejected")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { render, screen, act } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
@@ -1012,7 +1012,9 @@ describe("Paste functionality", () => {
|
||||
],
|
||||
};
|
||||
|
||||
textarea.dispatchEvent(pasteEvent);
|
||||
await act(async () => {
|
||||
textarea.dispatchEvent(pasteEvent);
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(screen.getByTestId("count")).toHaveTextContent("1");
|
||||
@@ -1067,9 +1069,9 @@ describe("PromptInputSpeechButton", () => {
|
||||
};
|
||||
|
||||
// @ts-expect-error - Mocking browser API
|
||||
global.window.SpeechRecognition = vi.fn(function () {
|
||||
global.window.SpeechRecognition = function () {
|
||||
return mockRecognition;
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -1099,7 +1101,7 @@ describe("PromptInputSpeechButton", () => {
|
||||
|
||||
it("initializes speech recognition when available", () => {
|
||||
render(<PromptInputSpeechButton />);
|
||||
expect(global.window.SpeechRecognition).toHaveBeenCalled();
|
||||
// Verify speech recognition was properly initialized by checking properties
|
||||
expect(mockRecognition.continuous).toBe(true);
|
||||
expect(mockRecognition.interimResults).toBe(true);
|
||||
expect(mockRecognition.lang).toBe("en-US");
|
||||
|
||||
@@ -1,297 +0,0 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { userEvent } from "@testing-library/user-event";
|
||||
import { CheckIcon, XIcon } from "lucide-react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
ToolApproval,
|
||||
ToolApprovalAccepted,
|
||||
ToolApprovalAction,
|
||||
ToolApprovalActions,
|
||||
ToolApprovalContent,
|
||||
ToolApprovalRejected,
|
||||
ToolApprovalRequest,
|
||||
} from "../src/tool-approval";
|
||||
|
||||
describe("ToolApproval", () => {
|
||||
it("renders children when approval is present", () => {
|
||||
render(
|
||||
<ToolApproval approval={{ id: "test-id" }} state="approval-requested">
|
||||
<div>Approval Content</div>
|
||||
</ToolApproval>
|
||||
);
|
||||
expect(screen.getByText("Approval Content")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not render when approval is not present", () => {
|
||||
const { container } = render(
|
||||
<ToolApproval state="input-streaming">
|
||||
<div>Approval Content</div>
|
||||
</ToolApproval>
|
||||
);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it("does not render in input-streaming state", () => {
|
||||
const { container } = render(
|
||||
<ToolApproval approval={{ id: "test-id" }} state="input-streaming">
|
||||
<div>Approval Content</div>
|
||||
</ToolApproval>
|
||||
);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it("does not render in input-available state", () => {
|
||||
const { container } = render(
|
||||
<ToolApproval approval={{ id: "test-id" }} state="input-available">
|
||||
<div>Approval Content</div>
|
||||
</ToolApproval>
|
||||
);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it("applies custom className", () => {
|
||||
const { container } = render(
|
||||
<ToolApproval
|
||||
approval={{ id: "test-id" }}
|
||||
className="custom-class"
|
||||
state="approval-requested"
|
||||
>
|
||||
<div>Content</div>
|
||||
</ToolApproval>
|
||||
);
|
||||
expect(container.firstChild).toHaveClass("custom-class");
|
||||
});
|
||||
});
|
||||
|
||||
describe("ToolApprovalContent", () => {
|
||||
it("renders ToolApprovalRequest when state is approval-requested", () => {
|
||||
render(
|
||||
<ToolApproval approval={{ id: "test-id" }} state="approval-requested">
|
||||
<ToolApprovalContent>
|
||||
<ToolApprovalRequest>Custom approval message</ToolApprovalRequest>
|
||||
<ToolApprovalAccepted>Accepted</ToolApprovalAccepted>
|
||||
<ToolApprovalRejected>Rejected</ToolApprovalRejected>
|
||||
</ToolApprovalContent>
|
||||
</ToolApproval>
|
||||
);
|
||||
expect(screen.getByText("Custom approval message")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Accepted")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Rejected")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders ToolApprovalAccepted when approved and state is approval-responded", () => {
|
||||
render(
|
||||
<ToolApproval
|
||||
approval={{ id: "test-id", approved: true }}
|
||||
state="approval-responded"
|
||||
>
|
||||
<ToolApprovalContent>
|
||||
<ToolApprovalRequest>Custom approval message</ToolApprovalRequest>
|
||||
<ToolApprovalAccepted>
|
||||
<CheckIcon />
|
||||
<span>Accepted</span>
|
||||
</ToolApprovalAccepted>
|
||||
<ToolApprovalRejected>
|
||||
<XIcon />
|
||||
<span>Rejected</span>
|
||||
</ToolApprovalRejected>
|
||||
</ToolApprovalContent>
|
||||
</ToolApproval>
|
||||
);
|
||||
expect(screen.getByText("Accepted")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText("Custom approval message")
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Rejected")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders ToolApprovalRejected when not approved and state is output-denied", () => {
|
||||
render(
|
||||
<ToolApproval
|
||||
approval={{ id: "test-id", approved: false }}
|
||||
state="output-denied"
|
||||
>
|
||||
<ToolApprovalContent>
|
||||
<ToolApprovalRequest>Custom approval message</ToolApprovalRequest>
|
||||
<ToolApprovalAccepted>
|
||||
<CheckIcon />
|
||||
<span>Accepted</span>
|
||||
</ToolApprovalAccepted>
|
||||
<ToolApprovalRejected>
|
||||
<XIcon />
|
||||
<span>Rejected</span>
|
||||
</ToolApprovalRejected>
|
||||
</ToolApprovalContent>
|
||||
</ToolApproval>
|
||||
);
|
||||
expect(screen.getByText("Rejected")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText("Custom approval message")
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Accepted")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("applies custom className", () => {
|
||||
const { container } = render(
|
||||
<ToolApproval approval={{ id: "test-id" }} state="approval-requested">
|
||||
<ToolApprovalContent className="custom-class">
|
||||
<ToolApprovalRequest>Custom approval message</ToolApprovalRequest>
|
||||
<ToolApprovalAccepted>Accepted</ToolApprovalAccepted>
|
||||
<ToolApprovalRejected>Rejected</ToolApprovalRejected>
|
||||
</ToolApprovalContent>
|
||||
</ToolApproval>
|
||||
);
|
||||
const content = container.querySelector(".custom-class");
|
||||
expect(content).toBeInTheDocument();
|
||||
expect(content).toHaveTextContent("Custom approval message");
|
||||
});
|
||||
});
|
||||
|
||||
describe("ToolApprovalActions", () => {
|
||||
it("renders custom children buttons", () => {
|
||||
render(
|
||||
<ToolApproval approval={{ id: "test-id" }} state="approval-requested">
|
||||
<ToolApprovalActions>
|
||||
<ToolApprovalAction variant="outline">Reject</ToolApprovalAction>
|
||||
<ToolApprovalAction variant="default">Accept</ToolApprovalAction>
|
||||
</ToolApprovalActions>
|
||||
</ToolApproval>
|
||||
);
|
||||
expect(screen.getByText("Accept")).toBeInTheDocument();
|
||||
expect(screen.getByText("Reject")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("hides when state is not approval-requested", () => {
|
||||
render(
|
||||
<ToolApproval approval={{ id: "test-id" }} state="approval-responded">
|
||||
<ToolApprovalActions>
|
||||
<ToolApprovalAction variant="outline">Reject</ToolApprovalAction>
|
||||
<ToolApprovalAction variant="default">Accept</ToolApprovalAction>
|
||||
</ToolApprovalActions>
|
||||
</ToolApproval>
|
||||
);
|
||||
expect(screen.queryByText("Accept")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Reject")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows when state is approval-requested", () => {
|
||||
render(
|
||||
<ToolApproval approval={{ id: "test-id" }} state="approval-requested">
|
||||
<ToolApprovalActions>
|
||||
<ToolApprovalAction variant="outline">Reject</ToolApprovalAction>
|
||||
<ToolApprovalAction variant="default">Accept</ToolApprovalAction>
|
||||
</ToolApprovalActions>
|
||||
</ToolApproval>
|
||||
);
|
||||
expect(screen.getByText("Accept")).toBeInTheDocument();
|
||||
expect(screen.getByText("Reject")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("calls onClick when accept button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const handleAccept = vi.fn();
|
||||
render(
|
||||
<ToolApproval approval={{ id: "test-id" }} state="approval-requested">
|
||||
<ToolApprovalActions>
|
||||
<ToolApprovalAction variant="outline">Reject</ToolApprovalAction>
|
||||
<ToolApprovalAction onClick={handleAccept} variant="default">
|
||||
Accept
|
||||
</ToolApprovalAction>
|
||||
</ToolApprovalActions>
|
||||
</ToolApproval>
|
||||
);
|
||||
|
||||
await user.click(screen.getByText("Accept"));
|
||||
expect(handleAccept).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("calls onClick when reject button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const handleReject = vi.fn();
|
||||
render(
|
||||
<ToolApproval approval={{ id: "test-id" }} state="approval-requested">
|
||||
<ToolApprovalActions>
|
||||
<ToolApprovalAction onClick={handleReject} variant="outline">
|
||||
Reject
|
||||
</ToolApprovalAction>
|
||||
<ToolApprovalAction variant="default">Accept</ToolApprovalAction>
|
||||
</ToolApprovalActions>
|
||||
</ToolApproval>
|
||||
);
|
||||
|
||||
await user.click(screen.getByText("Reject"));
|
||||
expect(handleReject).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("disables buttons when disabled prop is true", () => {
|
||||
render(
|
||||
<ToolApproval approval={{ id: "test-id" }} state="approval-requested">
|
||||
<ToolApprovalActions>
|
||||
<ToolApprovalAction disabled variant="outline">
|
||||
Reject
|
||||
</ToolApprovalAction>
|
||||
<ToolApprovalAction disabled variant="default">
|
||||
Accept
|
||||
</ToolApprovalAction>
|
||||
</ToolApprovalActions>
|
||||
</ToolApproval>
|
||||
);
|
||||
expect(screen.getByText("Accept")).toBeDisabled();
|
||||
expect(screen.getByText("Reject")).toBeDisabled();
|
||||
});
|
||||
|
||||
it("applies custom className", () => {
|
||||
render(
|
||||
<ToolApproval approval={{ id: "test-id" }} state="approval-requested">
|
||||
<ToolApprovalActions className="custom-class">
|
||||
<ToolApprovalAction variant="outline">Reject</ToolApprovalAction>
|
||||
<ToolApprovalAction variant="default">Accept</ToolApprovalAction>
|
||||
</ToolApprovalActions>
|
||||
</ToolApproval>
|
||||
);
|
||||
const actionsContainer = screen.getByText("Accept").parentElement;
|
||||
expect(actionsContainer).toHaveClass("custom-class");
|
||||
});
|
||||
});
|
||||
|
||||
describe("ToolApprovalAccepted", () => {
|
||||
it("renders accepted status with icon", () => {
|
||||
render(
|
||||
<ToolApproval
|
||||
approval={{ id: "test-id", approved: true }}
|
||||
state="approval-responded"
|
||||
>
|
||||
<ToolApprovalContent>
|
||||
<ToolApprovalRequest>Request</ToolApprovalRequest>
|
||||
<ToolApprovalAccepted>
|
||||
<CheckIcon className="size-4" />
|
||||
<span>Accepted</span>
|
||||
</ToolApprovalAccepted>
|
||||
<ToolApprovalRejected>Rejected</ToolApprovalRejected>
|
||||
</ToolApprovalContent>
|
||||
</ToolApproval>
|
||||
);
|
||||
expect(screen.getByText("Accepted")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("ToolApprovalRejected", () => {
|
||||
it("renders rejected status with icon", () => {
|
||||
render(
|
||||
<ToolApproval
|
||||
approval={{ id: "test-id", approved: false }}
|
||||
state="output-denied"
|
||||
>
|
||||
<ToolApprovalContent>
|
||||
<ToolApprovalRequest>Request</ToolApprovalRequest>
|
||||
<ToolApprovalAccepted>Accepted</ToolApprovalAccepted>
|
||||
<ToolApprovalRejected>
|
||||
<XIcon className="size-4" />
|
||||
<span>Rejected</span>
|
||||
</ToolApprovalRejected>
|
||||
</ToolApprovalContent>
|
||||
</ToolApproval>
|
||||
);
|
||||
expect(screen.getByText("Rejected")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -17,11 +17,11 @@
|
||||
"@xyflow/react": "^12.9.0",
|
||||
"ai": "5.1.0-beta.22",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"lucide-react": "^0.546.0",
|
||||
"lucide-react": "^0.548.0",
|
||||
"motion": "^12.23.24",
|
||||
"nanoid": "^5.1.6",
|
||||
"react": "19.2.0",
|
||||
"shiki": "3.13.0",
|
||||
"shiki": "3.14.0",
|
||||
"streamdown": "^1.4.0",
|
||||
"tokenlens": "^1.3.1",
|
||||
"use-stick-to-bottom": "^1.1.1"
|
||||
@@ -33,10 +33,10 @@
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/hast": "^3.0.4",
|
||||
"@types/react": "19.2.2",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
"@vitest/coverage-v8": "^4.0.1",
|
||||
"jsdom": "^27.0.1",
|
||||
"@vitejs/plugin-react": "^5.1.0",
|
||||
"@vitest/coverage-v8": "^4.0.4",
|
||||
"jsdom": "^26.0.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "^4.0.1"
|
||||
"vitest": "^4.0.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
"use client";
|
||||
|
||||
import { Alert, AlertDescription } from "@repo/shadcn-ui/components/ui/alert";
|
||||
import { Button } from "@repo/shadcn-ui/components/ui/button";
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
import type { ToolUIPart } from "ai";
|
||||
import {
|
||||
type ComponentProps,
|
||||
createContext,
|
||||
type ReactNode,
|
||||
useContext,
|
||||
} from "react";
|
||||
|
||||
type ConfirmationContextValue = {
|
||||
approval: ToolUIPart["approval"];
|
||||
state: ToolUIPart["state"];
|
||||
};
|
||||
|
||||
const ConfirmationContext = createContext<ConfirmationContextValue | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const useConfirmation = () => {
|
||||
const context = useContext(ConfirmationContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error("Confirmation components must be used within Confirmation");
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
export type ConfirmationProps = ComponentProps<typeof Alert> & {
|
||||
approval?: ToolUIPart["approval"];
|
||||
state: ToolUIPart["state"];
|
||||
};
|
||||
|
||||
export const Confirmation = ({
|
||||
className,
|
||||
approval,
|
||||
state,
|
||||
...props
|
||||
}: ConfirmationProps) => {
|
||||
if (
|
||||
!approval ||
|
||||
state === "input-streaming" ||
|
||||
state === "input-available"
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ConfirmationContext.Provider value={{ approval, state }}>
|
||||
<Alert className={cn("flex flex-col gap-2", className)} {...props} />
|
||||
</ConfirmationContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export type ConfirmationTitleProps = ComponentProps<typeof AlertDescription>;
|
||||
|
||||
export const ConfirmationTitle = ({
|
||||
className,
|
||||
...props
|
||||
}: ConfirmationTitleProps) => (
|
||||
<AlertDescription className={className} {...props} />
|
||||
);
|
||||
|
||||
export type ConfirmationRequestProps = {
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export const ConfirmationRequest = ({
|
||||
children,
|
||||
}: ConfirmationRequestProps) => {
|
||||
const { state } = useConfirmation();
|
||||
|
||||
// Only show when approval is requested
|
||||
if (state !== "approval-requested") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return children;
|
||||
};
|
||||
|
||||
export type ConfirmationAcceptedProps = {
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export const ConfirmationAccepted = ({
|
||||
children,
|
||||
}: ConfirmationAcceptedProps) => {
|
||||
const { approval, state } = useConfirmation();
|
||||
|
||||
// Only show when approved and in response states
|
||||
if (
|
||||
!approval?.approved ||
|
||||
(state !== "approval-responded" &&
|
||||
state !== "output-denied" &&
|
||||
state !== "output-available")
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return children;
|
||||
};
|
||||
|
||||
export type ConfirmationRejectedProps = {
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export const ConfirmationRejected = ({
|
||||
children,
|
||||
}: ConfirmationRejectedProps) => {
|
||||
const { approval, state } = useConfirmation();
|
||||
|
||||
// Only show when rejected and in response states
|
||||
if (
|
||||
approval?.approved !== false ||
|
||||
(state !== "approval-responded" &&
|
||||
state !== "output-denied" &&
|
||||
state !== "output-available")
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return children;
|
||||
};
|
||||
|
||||
export type ConfirmationActionsProps = ComponentProps<"div">;
|
||||
|
||||
export const ConfirmationActions = ({
|
||||
className,
|
||||
...props
|
||||
}: ConfirmationActionsProps) => {
|
||||
const { state } = useConfirmation();
|
||||
|
||||
// Only show when approval is requested
|
||||
if (state !== "approval-requested") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-end gap-2 self-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export type ConfirmationActionProps = ComponentProps<typeof Button>;
|
||||
|
||||
export const ConfirmationAction = (props: ConfirmationActionProps) => (
|
||||
<Button className="h-8 px-3 text-sm" type="button" {...props} />
|
||||
);
|
||||
@@ -1,168 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Alert, AlertDescription } from "@repo/shadcn-ui/components/ui/alert";
|
||||
import { Button } from "@repo/shadcn-ui/components/ui/button";
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
import type { ToolUIPart } from "ai";
|
||||
import {
|
||||
type ComponentProps,
|
||||
createContext,
|
||||
memo,
|
||||
type ReactNode,
|
||||
useContext,
|
||||
} from "react";
|
||||
|
||||
type ToolApprovalContextValue = {
|
||||
approval: ToolUIPart["approval"];
|
||||
state: ToolUIPart["state"];
|
||||
};
|
||||
|
||||
const ToolApprovalContext = createContext<ToolApprovalContextValue | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const useToolApproval = () => {
|
||||
const context = useContext(ToolApprovalContext);
|
||||
if (!context) {
|
||||
throw new Error("ToolApproval components must be used within ToolApproval");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export type ToolApprovalProps = ComponentProps<typeof Alert> & {
|
||||
approval?: ToolUIPart["approval"];
|
||||
state: ToolUIPart["state"];
|
||||
};
|
||||
|
||||
export const ToolApproval = memo(
|
||||
({ className, approval, state, children, ...props }: ToolApprovalProps) => {
|
||||
if (
|
||||
!approval ||
|
||||
state === "input-streaming" ||
|
||||
state === "input-available"
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ToolApprovalContext.Provider value={{ approval, state }}>
|
||||
<Alert className={cn("flex flex-col gap-2", className)} {...props}>
|
||||
{children}
|
||||
</Alert>
|
||||
</ToolApprovalContext.Provider>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export type ToolApprovalContentProps = ComponentProps<typeof AlertDescription>;
|
||||
|
||||
export const ToolApprovalContent = memo(
|
||||
({ className, children, ...props }: ToolApprovalContentProps) => (
|
||||
<AlertDescription
|
||||
className={cn("flex items-center gap-2", className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</AlertDescription>
|
||||
)
|
||||
);
|
||||
|
||||
export type ToolApprovalRequestProps = {
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export const ToolApprovalRequest = memo(
|
||||
({ children }: ToolApprovalRequestProps) => {
|
||||
const { state } = useToolApproval();
|
||||
|
||||
// Only show when approval is requested
|
||||
if (state !== "approval-requested") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
);
|
||||
|
||||
export type ToolApprovalAcceptedProps = {
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export const ToolApprovalAccepted = memo(
|
||||
({ children }: ToolApprovalAcceptedProps) => {
|
||||
const { approval, state } = useToolApproval();
|
||||
|
||||
// Only show when approved and in response states
|
||||
if (
|
||||
!approval?.approved ||
|
||||
(state !== "approval-responded" &&
|
||||
state !== "output-denied" &&
|
||||
state !== "output-available")
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
);
|
||||
|
||||
export type ToolApprovalRejectedProps = {
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export const ToolApprovalRejected = memo(
|
||||
({ children }: ToolApprovalRejectedProps) => {
|
||||
const { approval, state } = useToolApproval();
|
||||
|
||||
// Only show when rejected and in response states
|
||||
if (
|
||||
approval?.approved !== false ||
|
||||
(state !== "approval-responded" &&
|
||||
state !== "output-denied" &&
|
||||
state !== "output-available")
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
);
|
||||
|
||||
export type ToolApprovalActionsProps = ComponentProps<"div">;
|
||||
|
||||
export const ToolApprovalActions = memo(
|
||||
({ className, children, ...props }: ToolApprovalActionsProps) => {
|
||||
const { state } = useToolApproval();
|
||||
|
||||
// Only show when approval is requested
|
||||
if (state !== "approval-requested") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-end gap-2 self-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export type ToolApprovalActionProps = ComponentProps<typeof Button>;
|
||||
|
||||
export const ToolApprovalAction = memo((props: ToolApprovalActionProps) => (
|
||||
<Button className="h-8 px-3 text-sm" type="button" {...props} />
|
||||
));
|
||||
|
||||
ToolApproval.displayName = "ToolApproval";
|
||||
ToolApprovalContent.displayName = "ToolApprovalContent";
|
||||
ToolApprovalRequest.displayName = "ToolApprovalRequest";
|
||||
ToolApprovalAccepted.displayName = "ToolApprovalAccepted";
|
||||
ToolApprovalRejected.displayName = "ToolApprovalRejected";
|
||||
ToolApprovalActions.displayName = "ToolApprovalActions";
|
||||
ToolApprovalAction.displayName = "ToolApprovalAction";
|
||||
@@ -11,7 +11,7 @@
|
||||
"@repo/elements": "workspace:*",
|
||||
"@xyflow/react": "^12.9.0",
|
||||
"ai": "5.1.0-beta.22",
|
||||
"lucide-react": "^0.546.0",
|
||||
"lucide-react": "^0.548.0",
|
||||
"nanoid": "^5.1.6",
|
||||
"react": "19.2.0",
|
||||
"sonner": "^2.0.7"
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
PromptInputAttachments,
|
||||
PromptInputBody,
|
||||
PromptInputButton,
|
||||
PromptInputFooter,
|
||||
PromptInputHeader,
|
||||
type PromptInputMessage,
|
||||
PromptInputModelSelect,
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Confirmation,
|
||||
ConfirmationAccepted,
|
||||
ConfirmationRejected,
|
||||
ConfirmationRequest,
|
||||
ConfirmationTitle,
|
||||
} from "@repo/elements/confirmation";
|
||||
import { CheckIcon, XIcon } from "lucide-react";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
const Example = () => (
|
||||
<div className="w-full max-w-2xl">
|
||||
<Confirmation
|
||||
approval={{ id: nanoid(), approved: true }}
|
||||
state="approval-responded"
|
||||
>
|
||||
<ConfirmationTitle>
|
||||
<ConfirmationRequest>
|
||||
This tool wants to delete the file{" "}
|
||||
<code className="rounded bg-muted px-1.5 py-0.5 text-sm">
|
||||
/tmp/example.txt
|
||||
</code>
|
||||
. Do you approve this action?
|
||||
</ConfirmationRequest>
|
||||
<ConfirmationAccepted>
|
||||
<CheckIcon className="size-4 text-green-600 dark:text-green-400" />
|
||||
<span>You approved this tool execution</span>
|
||||
</ConfirmationAccepted>
|
||||
<ConfirmationRejected>
|
||||
<XIcon className="size-4 text-destructive" />
|
||||
<span>You rejected this tool execution</span>
|
||||
</ConfirmationRejected>
|
||||
</ConfirmationTitle>
|
||||
</Confirmation>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Example;
|
||||
@@ -0,0 +1,40 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Confirmation,
|
||||
ConfirmationAccepted,
|
||||
ConfirmationRejected,
|
||||
ConfirmationRequest,
|
||||
ConfirmationTitle,
|
||||
} from "@repo/elements/confirmation";
|
||||
import { CheckIcon, XIcon } from "lucide-react";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
const Example = () => (
|
||||
<div className="w-full max-w-2xl">
|
||||
<Confirmation
|
||||
approval={{ id: nanoid(), approved: false }}
|
||||
state="output-denied"
|
||||
>
|
||||
<ConfirmationTitle>
|
||||
<ConfirmationRequest>
|
||||
This tool wants to delete the file{" "}
|
||||
<code className="rounded bg-muted px-1.5 py-0.5 text-sm">
|
||||
/tmp/example.txt
|
||||
</code>
|
||||
. Do you approve this action?
|
||||
</ConfirmationRequest>
|
||||
<ConfirmationAccepted>
|
||||
<CheckIcon className="size-4 text-green-600 dark:text-green-400" />
|
||||
<span>You approved this tool execution</span>
|
||||
</ConfirmationAccepted>
|
||||
<ConfirmationRejected>
|
||||
<XIcon className="size-4 text-destructive" />
|
||||
<span>You rejected this tool execution</span>
|
||||
</ConfirmationRejected>
|
||||
</ConfirmationTitle>
|
||||
</Confirmation>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Example;
|
||||
@@ -0,0 +1,56 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Confirmation,
|
||||
ConfirmationAccepted,
|
||||
ConfirmationAction,
|
||||
ConfirmationActions,
|
||||
ConfirmationRejected,
|
||||
ConfirmationRequest,
|
||||
ConfirmationTitle,
|
||||
} from "@repo/elements/confirmation";
|
||||
import { CheckIcon, XIcon } from "lucide-react";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
const Example = () => (
|
||||
<div className="w-full max-w-2xl">
|
||||
<Confirmation approval={{ id: nanoid() }} state="approval-requested">
|
||||
<ConfirmationTitle>
|
||||
<ConfirmationRequest>
|
||||
This tool wants to execute a query on the production database:
|
||||
<code className="mt-2 block rounded bg-muted p-2 text-sm">
|
||||
SELECT * FROM users WHERE role = 'admin'
|
||||
</code>
|
||||
</ConfirmationRequest>
|
||||
<ConfirmationAccepted>
|
||||
<CheckIcon className="size-4 text-green-600 dark:text-green-400" />
|
||||
<span>You approved this tool execution</span>
|
||||
</ConfirmationAccepted>
|
||||
<ConfirmationRejected>
|
||||
<XIcon className="size-4 text-destructive" />
|
||||
<span>You rejected this tool execution</span>
|
||||
</ConfirmationRejected>
|
||||
</ConfirmationTitle>
|
||||
<ConfirmationActions>
|
||||
<ConfirmationAction
|
||||
onClick={() => {
|
||||
// In production, call respondToConfirmationRequest with approved: false
|
||||
}}
|
||||
variant="outline"
|
||||
>
|
||||
Reject
|
||||
</ConfirmationAction>
|
||||
<ConfirmationAction
|
||||
onClick={() => {
|
||||
// In production, call respondToConfirmationRequest with approved: true
|
||||
}}
|
||||
variant="default"
|
||||
>
|
||||
Approve
|
||||
</ConfirmationAction>
|
||||
</ConfirmationActions>
|
||||
</Confirmation>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Example;
|
||||
@@ -0,0 +1,57 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Confirmation,
|
||||
ConfirmationAccepted,
|
||||
ConfirmationAction,
|
||||
ConfirmationActions,
|
||||
ConfirmationRejected,
|
||||
ConfirmationRequest,
|
||||
ConfirmationTitle,
|
||||
} from "@repo/elements/confirmation";
|
||||
import { CheckIcon, XIcon } from "lucide-react";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
const Example = () => (
|
||||
<div className="w-full max-w-2xl">
|
||||
<Confirmation approval={{ id: nanoid() }} state="approval-requested">
|
||||
<ConfirmationTitle>
|
||||
<ConfirmationRequest>
|
||||
This tool wants to delete the file{" "}
|
||||
<code className="inline rounded bg-muted px-1.5 py-0.5 text-sm">
|
||||
/tmp/example.txt
|
||||
</code>
|
||||
. Do you approve this action?
|
||||
</ConfirmationRequest>
|
||||
<ConfirmationAccepted>
|
||||
<CheckIcon className="size-4 text-green-600 dark:text-green-400" />
|
||||
<span>You approved this tool execution</span>
|
||||
</ConfirmationAccepted>
|
||||
<ConfirmationRejected>
|
||||
<XIcon className="size-4 text-destructive" />
|
||||
<span>You rejected this tool execution</span>
|
||||
</ConfirmationRejected>
|
||||
</ConfirmationTitle>
|
||||
<ConfirmationActions>
|
||||
<ConfirmationAction
|
||||
onClick={() => {
|
||||
// In production, call respondToConfirmationRequest with approved: false
|
||||
}}
|
||||
variant="outline"
|
||||
>
|
||||
Reject
|
||||
</ConfirmationAction>
|
||||
<ConfirmationAction
|
||||
onClick={() => {
|
||||
// In production, call respondToConfirmationRequest with approved: true
|
||||
}}
|
||||
variant="default"
|
||||
>
|
||||
Approve
|
||||
</ConfirmationAction>
|
||||
</ConfirmationActions>
|
||||
</Confirmation>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Example;
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
PromptInputSpeechButton,
|
||||
PromptInputSubmit,
|
||||
PromptInputTextarea,
|
||||
PromptInputFooter,
|
||||
PromptInputTools,
|
||||
usePromptInputController,
|
||||
} from "@repo/elements/prompt-input";
|
||||
|
||||
@@ -8,14 +8,14 @@ import {
|
||||
ToolOutput,
|
||||
} from "@repo/elements/tool";
|
||||
import {
|
||||
ToolApproval,
|
||||
ToolApprovalAccepted,
|
||||
ToolApprovalAction,
|
||||
ToolApprovalActions,
|
||||
ToolApprovalContent,
|
||||
ToolApprovalRejected,
|
||||
ToolApprovalRequest,
|
||||
} from "@repo/elements/tool-approval";
|
||||
Confirmation,
|
||||
ConfirmationAccepted,
|
||||
ConfirmationAction,
|
||||
ConfirmationActions,
|
||||
ConfirmationRejected,
|
||||
ConfirmationRequest,
|
||||
ConfirmationTitle,
|
||||
} from "@repo/elements/confirmation";
|
||||
import type { ToolUIPart } from "ai";
|
||||
import { CheckIcon, XIcon } from "lucide-react";
|
||||
import { nanoid } from "nanoid";
|
||||
@@ -62,39 +62,39 @@ const Example = () => (
|
||||
/>
|
||||
<ToolContent>
|
||||
<ToolInput input={toolCall.input} />
|
||||
<ToolApproval approval={{ id: nanoid() }} state="approval-requested">
|
||||
<ToolApprovalContent>
|
||||
<ToolApprovalRequest>
|
||||
<Confirmation approval={{ id: nanoid() }} state="approval-requested">
|
||||
<ConfirmationTitle>
|
||||
<ConfirmationRequest>
|
||||
This tool will execute a query on the production database.
|
||||
</ToolApprovalRequest>
|
||||
<ToolApprovalAccepted>
|
||||
</ConfirmationRequest>
|
||||
<ConfirmationAccepted>
|
||||
<CheckIcon className="size-4 text-green-600 dark:text-green-400" />
|
||||
<span>Accepted</span>
|
||||
</ToolApprovalAccepted>
|
||||
<ToolApprovalRejected>
|
||||
</ConfirmationAccepted>
|
||||
<ConfirmationRejected>
|
||||
<XIcon className="size-4 text-destructive" />
|
||||
<span>Rejected</span>
|
||||
</ToolApprovalRejected>
|
||||
</ToolApprovalContent>
|
||||
<ToolApprovalActions>
|
||||
<ToolApprovalAction
|
||||
</ConfirmationRejected>
|
||||
</ConfirmationTitle>
|
||||
<ConfirmationActions>
|
||||
<ConfirmationAction
|
||||
onClick={() => {
|
||||
// In production, call addToolApprovalResponse
|
||||
// In production, call addConfirmationResponse
|
||||
}}
|
||||
variant="outline"
|
||||
>
|
||||
Reject
|
||||
</ToolApprovalAction>
|
||||
<ToolApprovalAction
|
||||
</ConfirmationAction>
|
||||
<ConfirmationAction
|
||||
onClick={() => {
|
||||
// In production, call addToolApprovalResponse
|
||||
// In production, call addConfirmationResponse
|
||||
}}
|
||||
variant="default"
|
||||
>
|
||||
Accept
|
||||
</ToolApprovalAction>
|
||||
</ToolApprovalActions>
|
||||
</ToolApproval>
|
||||
</ConfirmationAction>
|
||||
</ConfirmationActions>
|
||||
</Confirmation>
|
||||
</ToolContent>
|
||||
</Tool>
|
||||
|
||||
@@ -107,24 +107,24 @@ const Example = () => (
|
||||
/>
|
||||
<ToolContent>
|
||||
<ToolInput input={toolCall.input} />
|
||||
<ToolApproval
|
||||
<Confirmation
|
||||
approval={{ id: nanoid(), approved: true }}
|
||||
state="approval-responded"
|
||||
>
|
||||
<ToolApprovalContent>
|
||||
<ToolApprovalRequest>
|
||||
<ConfirmationTitle>
|
||||
<ConfirmationRequest>
|
||||
This tool will execute a query on the production database.
|
||||
</ToolApprovalRequest>
|
||||
<ToolApprovalAccepted>
|
||||
</ConfirmationRequest>
|
||||
<ConfirmationAccepted>
|
||||
<CheckIcon className="size-4 text-green-600 dark:text-green-400" />
|
||||
<span>Accepted</span>
|
||||
</ToolApprovalAccepted>
|
||||
<ToolApprovalRejected>
|
||||
</ConfirmationAccepted>
|
||||
<ConfirmationRejected>
|
||||
<XIcon className="size-4 text-destructive" />
|
||||
<span>Rejected</span>
|
||||
</ToolApprovalRejected>
|
||||
</ToolApprovalContent>
|
||||
</ToolApproval>
|
||||
</ConfirmationRejected>
|
||||
</ConfirmationTitle>
|
||||
</Confirmation>
|
||||
</ToolContent>
|
||||
</Tool>
|
||||
|
||||
@@ -145,24 +145,24 @@ const Example = () => (
|
||||
<ToolHeader state={toolCall.state} type={toolCall.type} />
|
||||
<ToolContent>
|
||||
<ToolInput input={toolCall.input} />
|
||||
<ToolApproval
|
||||
<Confirmation
|
||||
approval={{ id: nanoid(), approved: true }}
|
||||
state="output-available"
|
||||
>
|
||||
<ToolApprovalContent>
|
||||
<ToolApprovalRequest>
|
||||
<ConfirmationTitle>
|
||||
<ConfirmationRequest>
|
||||
This tool will execute a query on the production database.
|
||||
</ToolApprovalRequest>
|
||||
<ToolApprovalAccepted>
|
||||
</ConfirmationRequest>
|
||||
<ConfirmationAccepted>
|
||||
<CheckIcon className="size-4 text-green-600 dark:text-green-400" />
|
||||
<span>Accepted</span>
|
||||
</ToolApprovalAccepted>
|
||||
<ToolApprovalRejected>
|
||||
</ConfirmationAccepted>
|
||||
<ConfirmationRejected>
|
||||
<XIcon className="size-4 text-destructive" />
|
||||
<span>Rejected</span>
|
||||
</ToolApprovalRejected>
|
||||
</ToolApprovalContent>
|
||||
</ToolApproval>
|
||||
</ConfirmationRejected>
|
||||
</ConfirmationTitle>
|
||||
</Confirmation>
|
||||
{toolCall.state === "output-available" && (
|
||||
<ToolOutput errorText={toolCall.errorText} output={toolCall.output} />
|
||||
)}
|
||||
@@ -194,7 +194,7 @@ const Example = () => (
|
||||
/>
|
||||
<ToolContent>
|
||||
<ToolInput input={toolCall.input} />
|
||||
<ToolApproval
|
||||
<Confirmation
|
||||
approval={{
|
||||
id: nanoid(),
|
||||
approved: false,
|
||||
@@ -202,25 +202,20 @@ const Example = () => (
|
||||
}}
|
||||
state="output-denied"
|
||||
>
|
||||
<ToolApprovalContent>
|
||||
<ToolApprovalRequest>
|
||||
<ConfirmationTitle>
|
||||
<ConfirmationRequest>
|
||||
This tool will execute a query on the production database.
|
||||
</ToolApprovalRequest>
|
||||
<ToolApprovalAccepted>
|
||||
</ConfirmationRequest>
|
||||
<ConfirmationAccepted>
|
||||
<CheckIcon className="size-4 text-green-600 dark:text-green-400" />
|
||||
<span>Accepted</span>
|
||||
</ToolApprovalAccepted>
|
||||
<ToolApprovalRejected>
|
||||
</ConfirmationAccepted>
|
||||
<ConfirmationRejected>
|
||||
<XIcon className="size-4 text-destructive" />
|
||||
<span>
|
||||
Rejected
|
||||
<span className="text-muted-foreground">
|
||||
: Query could impact production performance
|
||||
</span>
|
||||
</span>
|
||||
</ToolApprovalRejected>
|
||||
</ToolApprovalContent>
|
||||
</ToolApproval>
|
||||
<span>Rejected: Query could impact production performance</span>
|
||||
</ConfirmationRejected>
|
||||
</ConfirmationTitle>
|
||||
</Confirmation>
|
||||
</ToolContent>
|
||||
</Tool>
|
||||
</div>
|
||||
|
||||
@@ -36,21 +36,6 @@ const exampleLogs = [
|
||||
},
|
||||
];
|
||||
|
||||
const code = [
|
||||
{
|
||||
language: "jsx",
|
||||
filename: "MyComponent.jsx",
|
||||
code: `function MyComponent(props) {
|
||||
return (
|
||||
<div>
|
||||
<h1>Hello, {props.name}!</h1>
|
||||
<p>This is an example React component.</p>
|
||||
</div>
|
||||
);
|
||||
}`,
|
||||
},
|
||||
];
|
||||
|
||||
const Example = () => {
|
||||
const [fullscreen, setFullscreen] = useState(false);
|
||||
|
||||
|
||||
@@ -100,7 +100,10 @@ function Calendar({
|
||||
defaultClassNames.week_number
|
||||
),
|
||||
day: cn(
|
||||
"relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none",
|
||||
"relative w-full h-full p-0 text-center [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none",
|
||||
props.showWeekNumber
|
||||
? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-md"
|
||||
: "[&:first-child[data-selected=true]_button]:rounded-l-md",
|
||||
defaultClassNames.day
|
||||
),
|
||||
range_start: cn(
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"date-fns": "^4.1.0",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"input-otp": "^1.4.2",
|
||||
"lucide-react": "^0.546.0",
|
||||
"lucide-react": "^0.548.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"radix-ui": "latest",
|
||||
"react": "19.2.0",
|
||||
|
||||
Generated
+183
-541
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user