@gram-ai/elements 1.18.5 → 1.18.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/dist/components/Chat/stories/Sidecar.stories.d.ts +6 -0
  2. package/dist/elements.cjs +22 -21
  3. package/dist/elements.cjs.map +1 -0
  4. package/dist/elements.js +601 -591
  5. package/dist/elements.js.map +1 -0
  6. package/dist/index-Bj7jPiuy.cjs +1 -0
  7. package/dist/index-Bj7jPiuy.cjs.map +1 -0
  8. package/dist/index-CJRypLIa.js +1 -0
  9. package/dist/index-CJRypLIa.js.map +1 -0
  10. package/dist/plugins.cjs +1 -0
  11. package/dist/plugins.cjs.map +1 -0
  12. package/dist/plugins.js +1 -0
  13. package/dist/plugins.js.map +1 -0
  14. package/dist/server.cjs +1 -0
  15. package/dist/server.cjs.map +1 -0
  16. package/dist/server.js +1 -0
  17. package/dist/server.js.map +1 -0
  18. package/package.json +3 -2
  19. package/src/components/Chat/index.tsx +21 -0
  20. package/src/components/Chat/stories/ColorScheme.stories.tsx +52 -0
  21. package/src/components/Chat/stories/Composer.stories.tsx +42 -0
  22. package/src/components/Chat/stories/Customization.stories.tsx +88 -0
  23. package/src/components/Chat/stories/Density.stories.tsx +52 -0
  24. package/src/components/Chat/stories/FrontendTools.stories.tsx +145 -0
  25. package/src/components/Chat/stories/Modal.stories.tsx +84 -0
  26. package/src/components/Chat/stories/Model.stories.tsx +32 -0
  27. package/src/components/Chat/stories/Plugins.stories.tsx +50 -0
  28. package/src/components/Chat/stories/Radius.stories.tsx +52 -0
  29. package/src/components/Chat/stories/Sidecar.stories.tsx +27 -0
  30. package/src/components/Chat/stories/ToolApproval.stories.tsx +110 -0
  31. package/src/components/Chat/stories/Tools.stories.tsx +175 -0
  32. package/src/components/Chat/stories/Variants.stories.tsx +46 -0
  33. package/src/components/Chat/stories/Welcome.stories.tsx +42 -0
  34. package/src/components/FrontendTools/index.tsx +9 -0
  35. package/src/components/assistant-ui/assistant-modal.tsx +255 -0
  36. package/src/components/assistant-ui/assistant-sidecar.tsx +88 -0
  37. package/src/components/assistant-ui/attachment.tsx +233 -0
  38. package/src/components/assistant-ui/markdown-text.tsx +240 -0
  39. package/src/components/assistant-ui/reasoning.tsx +261 -0
  40. package/src/components/assistant-ui/thread-list.tsx +97 -0
  41. package/src/components/assistant-ui/thread.tsx +632 -0
  42. package/src/components/assistant-ui/tool-fallback.tsx +111 -0
  43. package/src/components/assistant-ui/tool-group.tsx +59 -0
  44. package/src/components/assistant-ui/tooltip-icon-button.tsx +57 -0
  45. package/src/components/ui/avatar.tsx +51 -0
  46. package/src/components/ui/button.tsx +27 -0
  47. package/src/components/ui/buttonVariants.ts +33 -0
  48. package/src/components/ui/collapsible.tsx +31 -0
  49. package/src/components/ui/dialog.tsx +141 -0
  50. package/src/components/ui/popover.tsx +46 -0
  51. package/src/components/ui/skeleton.tsx +13 -0
  52. package/src/components/ui/tool-ui.stories.tsx +146 -0
  53. package/src/components/ui/tool-ui.tsx +676 -0
  54. package/src/components/ui/tooltip.tsx +61 -0
  55. package/src/contexts/ElementsProvider.tsx +256 -0
  56. package/src/contexts/ToolApprovalContext.tsx +120 -0
  57. package/src/contexts/contexts.ts +10 -0
  58. package/src/global.css +136 -0
  59. package/src/hooks/useAuth.ts +71 -0
  60. package/src/hooks/useDensity.ts +110 -0
  61. package/src/hooks/useElements.ts +14 -0
  62. package/src/hooks/useExpanded.ts +20 -0
  63. package/src/hooks/useMCPTools.ts +73 -0
  64. package/src/hooks/usePluginComponents.ts +34 -0
  65. package/src/hooks/useRadius.ts +42 -0
  66. package/src/hooks/useSession.ts +38 -0
  67. package/src/hooks/useThemeProps.ts +24 -0
  68. package/src/hooks/useToolApproval.ts +16 -0
  69. package/src/index.ts +45 -0
  70. package/src/lib/api.test.ts +90 -0
  71. package/src/lib/api.ts +8 -0
  72. package/src/lib/auth.ts +10 -0
  73. package/src/lib/easing.ts +1 -0
  74. package/src/lib/humanize.ts +14 -0
  75. package/src/lib/models.ts +22 -0
  76. package/src/lib/tools.ts +210 -0
  77. package/src/lib/utils.ts +16 -0
  78. package/src/plugins/README.md +49 -0
  79. package/src/plugins/chart/component.tsx +102 -0
  80. package/src/plugins/chart/index.ts +27 -0
  81. package/src/plugins/index.ts +7 -0
  82. package/src/server.ts +89 -0
  83. package/src/types/index.ts +726 -0
  84. package/src/types/plugins.ts +65 -0
  85. package/src/vite-env.d.ts +12 -0
@@ -0,0 +1,145 @@
1
+ import React from 'react'
2
+ import { Chat } from '..'
3
+ import type { Meta, StoryFn } from '@storybook/react-vite'
4
+ import { ToolCallMessagePartProps } from '@assistant-ui/react'
5
+ import { defineFrontendTool, FrontendTool } from '../../../lib/tools'
6
+ import z from 'zod'
7
+
8
+ const meta: Meta<typeof Chat> = {
9
+ title: 'Chat/Frontend Tools',
10
+ component: Chat,
11
+ parameters: {
12
+ layout: 'fullscreen',
13
+ },
14
+ decorators: [
15
+ (Story) => (
16
+ <div className="m-auto flex h-screen w-full max-w-3xl flex-col">
17
+ <Story />
18
+ </div>
19
+ ),
20
+ ],
21
+ } satisfies Meta<typeof Chat>
22
+
23
+ export default meta
24
+
25
+ type Story = StoryFn<typeof Chat>
26
+
27
+ const FetchTool = defineFrontendTool<{ url: string }, string>(
28
+ {
29
+ description: 'Fetch a URL (supports CORS-enabled URLs like httpbin.org)',
30
+ parameters: z.object({
31
+ url: z.string().describe('URL to fetch (must support CORS)'),
32
+ }),
33
+ execute: async ({ url }) => {
34
+ try {
35
+ const response = await fetch(url as string)
36
+ const text = await response.text()
37
+ return text
38
+ } catch (error) {
39
+ return `Error fetching ${url}: ${error instanceof Error ? error.message : 'Unknown error'}. Note: URL must support CORS for browser requests.`
40
+ }
41
+ },
42
+ },
43
+ 'fetchUrl'
44
+ )
45
+
46
+ const frontendTools: Record<string, FrontendTool<{ url: string }, string>> = {
47
+ fetchUrl: FetchTool,
48
+ }
49
+
50
+ // Render OS X style browser window with the fetched URL html rendered
51
+ const FetchToolComponent = ({ result, args }: ToolCallMessagePartProps) => {
52
+ const url = (args as { url?: string })?.url || 'about:blank'
53
+ const [isLoading, setIsLoading] = React.useState(true)
54
+
55
+ return (
56
+ <div className="my-5 flex w-full flex-col overflow-hidden rounded-lg border shadow-lg">
57
+ {/* macOS Window Controls Bar */}
58
+ <div className="bg-muted flex flex-col border-b">
59
+ <div className="flex items-center gap-2 px-3 py-2">
60
+ {/* Traffic lights */}
61
+ <div className="flex gap-2">
62
+ <div className="h-3 w-3 rounded-full bg-red-500" />
63
+ <div className="h-3 w-3 rounded-full bg-yellow-500" />
64
+ <div className="h-3 w-3 rounded-full bg-green-500" />
65
+ </div>
66
+
67
+ {/* Address bar */}
68
+ <div className="bg-background mx-4 flex flex-1 items-center rounded-md px-3 py-1">
69
+ <svg
70
+ className="text-muted-foreground mr-2 h-4 w-4"
71
+ fill="none"
72
+ viewBox="0 0 24 24"
73
+ stroke="currentColor"
74
+ >
75
+ <path
76
+ strokeLinecap="round"
77
+ strokeLinejoin="round"
78
+ strokeWidth={2}
79
+ d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
80
+ />
81
+ </svg>
82
+ <span className="text-muted-foreground truncate text-sm">
83
+ {url}
84
+ </span>
85
+ </div>
86
+ </div>
87
+
88
+ {/* Loading bar */}
89
+ {isLoading && (
90
+ <div className="h-0.5 w-full overflow-hidden">
91
+ <div
92
+ className="h-full w-1/3 animate-pulse bg-blue-500"
93
+ style={{
94
+ animation: 'slide 1.5s ease-in-out infinite',
95
+ }}
96
+ />
97
+ </div>
98
+ )}
99
+ </div>
100
+
101
+ {/* Content */}
102
+ <div className="bg-background h-96">
103
+ <iframe
104
+ srcDoc={result as string}
105
+ className="h-full w-full"
106
+ onLoad={() => setIsLoading(false)}
107
+ />
108
+ </div>
109
+
110
+ <style>{`
111
+ @keyframes slide {
112
+ 0% { transform: translateX(-100%); }
113
+ 100% { transform: translateX(400%); }
114
+ }
115
+ `}</style>
116
+ </div>
117
+ )
118
+ }
119
+
120
+ export const FetchUrl: Story = () => <Chat />
121
+ FetchUrl.storyName = 'Fetch URL Tool'
122
+ FetchUrl.parameters = {
123
+ elements: {
124
+ config: {
125
+ variant: 'standalone',
126
+ welcome: {
127
+ title: '',
128
+ subtitle: '',
129
+ suggestions: [
130
+ {
131
+ title: 'Fetch a URL',
132
+ label: 'Fetch a URL',
133
+ action: 'Fetch https://httpbin.org/html',
134
+ },
135
+ ],
136
+ },
137
+ tools: {
138
+ frontendTools,
139
+ components: {
140
+ fetchUrl: FetchToolComponent,
141
+ },
142
+ },
143
+ },
144
+ },
145
+ }
@@ -0,0 +1,84 @@
1
+ import React from 'react'
2
+ import { Chat } from '..'
3
+ import type { Meta, StoryFn } from '@storybook/react-vite'
4
+ import { ZapIcon } from 'lucide-react'
5
+
6
+ const meta: Meta<typeof Chat> = {
7
+ title: 'Chat/Modal',
8
+ component: Chat,
9
+ parameters: {
10
+ layout: 'fullscreen',
11
+ },
12
+ } satisfies Meta<typeof Chat>
13
+
14
+ export default meta
15
+
16
+ type Story = StoryFn<typeof Chat>
17
+
18
+ export const CustomIcon: Story = () => <Chat />
19
+ CustomIcon.parameters = {
20
+ elements: {
21
+ config: {
22
+ modal: {
23
+ defaultOpen: false,
24
+ icon: (state: 'open' | 'closed' | undefined) => (
25
+ <ZapIcon
26
+ data-state={state}
27
+ className="aui-modal-button-closed-icon absolute transition-all data-[state=closed]:scale-100 data-[state=closed]:rotate-0 data-[state=open]:scale-0 data-[state=open]:rotate-90"
28
+ />
29
+ ),
30
+ },
31
+ },
32
+ },
33
+ }
34
+
35
+ export const Expandable: Story = () => <Chat />
36
+ Expandable.parameters = {
37
+ elements: {
38
+ config: {
39
+ modal: {
40
+ expandable: true,
41
+ dimensions: {
42
+ default: { width: '500px', height: '600px', maxHeight: '100vh' },
43
+ expanded: { width: '80vw', height: '90vh' },
44
+ },
45
+ },
46
+ },
47
+ },
48
+ }
49
+
50
+ export const PositionTopRight: Story = () => <Chat />
51
+ PositionTopRight.parameters = {
52
+ elements: {
53
+ config: {
54
+ modal: { position: 'top-right' },
55
+ },
56
+ },
57
+ }
58
+
59
+ export const PositionBottomRight: Story = () => <Chat />
60
+ PositionBottomRight.parameters = {
61
+ elements: {
62
+ config: {
63
+ modal: { position: 'bottom-right' },
64
+ },
65
+ },
66
+ }
67
+
68
+ export const PositionBottomLeft: Story = () => <Chat />
69
+ PositionBottomLeft.parameters = {
70
+ elements: {
71
+ config: {
72
+ modal: { position: 'bottom-left' },
73
+ },
74
+ },
75
+ }
76
+
77
+ export const PositionTopLeft: Story = () => <Chat />
78
+ PositionTopLeft.parameters = {
79
+ elements: {
80
+ config: {
81
+ modal: { position: 'top-left' },
82
+ },
83
+ },
84
+ }
@@ -0,0 +1,32 @@
1
+ import React from 'react'
2
+ import { Chat } from '..'
3
+ import type { Meta, StoryFn } from '@storybook/react-vite'
4
+
5
+ const meta: Meta<typeof Chat> = {
6
+ title: 'Chat/Model',
7
+ component: Chat,
8
+ parameters: {
9
+ layout: 'fullscreen',
10
+ },
11
+ decorators: [
12
+ (Story) => (
13
+ <div className="m-auto flex h-screen w-full max-w-3xl flex-col">
14
+ <Story />
15
+ </div>
16
+ ),
17
+ ],
18
+ } satisfies Meta<typeof Chat>
19
+
20
+ export default meta
21
+
22
+ type Story = StoryFn<typeof Chat>
23
+
24
+ export const ModelPicker: Story = () => <Chat />
25
+ ModelPicker.parameters = {
26
+ elements: {
27
+ config: {
28
+ variant: 'standalone',
29
+ model: { showModelPicker: true },
30
+ },
31
+ },
32
+ }
@@ -0,0 +1,50 @@
1
+ import React from 'react'
2
+ import { Chat } from '..'
3
+ import type { Meta, StoryFn } from '@storybook/react-vite'
4
+
5
+ const meta: Meta<typeof Chat> = {
6
+ title: 'Chat/Plugins',
7
+ component: Chat,
8
+ parameters: {
9
+ layout: 'fullscreen',
10
+ },
11
+ decorators: [
12
+ (Story) => (
13
+ <div className="m-auto flex h-screen w-full max-w-3xl flex-col">
14
+ <Story />
15
+ </div>
16
+ ),
17
+ ],
18
+ } satisfies Meta<typeof Chat>
19
+
20
+ export default meta
21
+
22
+ type Story = StoryFn<typeof Chat>
23
+
24
+ const countryData = JSON.stringify({
25
+ countries: [
26
+ { name: 'USA', gdp: 22000 },
27
+ { name: 'Canada', gdp: 16000 },
28
+ { name: 'Mexico', gdp: 10000 },
29
+ ],
30
+ })
31
+
32
+ export const ChartPlugin: Story = () => <Chat />
33
+ ChartPlugin.parameters = {
34
+ elements: {
35
+ config: {
36
+ variant: 'standalone',
37
+ welcome: {
38
+ suggestions: [
39
+ {
40
+ title: 'Create a chart',
41
+ label: 'Visualize your data',
42
+ action: `Create a bar chart for the following country + GDP data:
43
+ ${countryData}
44
+ `,
45
+ },
46
+ ],
47
+ },
48
+ },
49
+ },
50
+ }
@@ -0,0 +1,52 @@
1
+ import React from 'react'
2
+ import { Chat } from '..'
3
+ import type { Meta, StoryFn } from '@storybook/react-vite'
4
+
5
+ const meta: Meta<typeof Chat> = {
6
+ title: 'Chat/Radius',
7
+ component: Chat,
8
+ parameters: {
9
+ layout: 'fullscreen',
10
+ },
11
+ decorators: [
12
+ (Story) => (
13
+ <div className="m-auto flex h-screen w-full max-w-3xl flex-col">
14
+ <Story />
15
+ </div>
16
+ ),
17
+ ],
18
+ } satisfies Meta<typeof Chat>
19
+
20
+ export default meta
21
+
22
+ type Story = StoryFn<typeof Chat>
23
+
24
+ export const Round: Story = () => <Chat />
25
+ Round.parameters = {
26
+ elements: {
27
+ config: {
28
+ variant: 'standalone',
29
+ theme: { radius: 'round' },
30
+ },
31
+ },
32
+ }
33
+
34
+ export const Soft: Story = () => <Chat />
35
+ Soft.parameters = {
36
+ elements: {
37
+ config: {
38
+ variant: 'standalone',
39
+ theme: { radius: 'soft' },
40
+ },
41
+ },
42
+ }
43
+
44
+ export const Sharp: Story = () => <Chat />
45
+ Sharp.parameters = {
46
+ elements: {
47
+ config: {
48
+ variant: 'standalone',
49
+ theme: { radius: 'sharp' },
50
+ },
51
+ },
52
+ }
@@ -0,0 +1,27 @@
1
+ import { Chat } from '..'
2
+ import type { Meta, StoryFn } from '@storybook/react-vite'
3
+
4
+ const meta: Meta<typeof Chat> = {
5
+ title: 'Chat/Sidecar',
6
+ component: Chat,
7
+ } satisfies Meta<typeof Chat>
8
+
9
+ export default meta
10
+
11
+ export const Sidecar: StoryFn<typeof Chat> = () => {
12
+ return <Chat />
13
+ }
14
+
15
+ Sidecar.parameters = {
16
+ elements: { config: { variant: 'sidecar' } },
17
+ }
18
+
19
+ export const SidecarWithTitle: StoryFn<typeof Chat> = () => {
20
+ return <Chat />
21
+ }
22
+
23
+ SidecarWithTitle.parameters = {
24
+ elements: {
25
+ config: { variant: 'sidecar', sidecar: { title: 'Chat with me' } },
26
+ },
27
+ }
@@ -0,0 +1,110 @@
1
+ import React from 'react'
2
+ import { Chat } from '..'
3
+ import type { Meta, StoryFn } from '@storybook/react-vite'
4
+ import { defineFrontendTool } from '../../../lib/tools'
5
+ import z from 'zod'
6
+
7
+ const meta: Meta<typeof Chat> = {
8
+ title: 'Chat/Tool Approval',
9
+ component: Chat,
10
+ parameters: {
11
+ layout: 'fullscreen',
12
+ },
13
+ decorators: [
14
+ (Story) => (
15
+ <div className="m-auto flex h-screen w-full max-w-3xl flex-col">
16
+ <Story />
17
+ </div>
18
+ ),
19
+ ],
20
+ } satisfies Meta<typeof Chat>
21
+
22
+ export default meta
23
+
24
+ type Story = StoryFn<typeof Chat>
25
+
26
+ export const SingleTool: Story = () => <Chat />
27
+ SingleTool.parameters = {
28
+ elements: {
29
+ config: {
30
+ variant: 'standalone',
31
+ welcome: {
32
+ suggestions: [
33
+ {
34
+ title: 'Call a tool requiring approval',
35
+ label: 'Get a salutation',
36
+ action: 'Get a salutation',
37
+ },
38
+ ],
39
+ },
40
+ tools: {
41
+ toolsRequiringApproval: ['kitchen_sink_get_salutation'],
42
+ },
43
+ },
44
+ },
45
+ }
46
+
47
+ export const MultipleGroupedTools: Story = () => <Chat />
48
+ MultipleGroupedTools.storyName = 'Multiple Grouped Tools'
49
+ MultipleGroupedTools.parameters = {
50
+ elements: {
51
+ config: {
52
+ variant: 'standalone',
53
+ welcome: {
54
+ suggestions: [
55
+ {
56
+ title: 'Call both tools requiring approval',
57
+ label: 'Call both tools requiring approval',
58
+ action:
59
+ 'Call both kitchen_sink_get_salutation and kitchen_sink_get_get_card_details',
60
+ },
61
+ ],
62
+ },
63
+ tools: {
64
+ toolsRequiringApproval: [
65
+ 'kitchen_sink_get_salutation',
66
+ 'kitchen_sink_get_get_card_details',
67
+ ],
68
+ },
69
+ },
70
+ },
71
+ }
72
+
73
+ const deleteFile = defineFrontendTool<{ fileId: string }, string>(
74
+ {
75
+ description: 'Delete a file',
76
+ parameters: z.object({
77
+ fileId: z.string().describe('The ID of the file to delete'),
78
+ }),
79
+ execute: async ({ fileId }) => {
80
+ alert(`File ${fileId} deleted`)
81
+ return `File ${fileId} deleted`
82
+ },
83
+ },
84
+ 'deleteFile'
85
+ )
86
+
87
+ export const FrontendTool: Story = () => <Chat />
88
+ FrontendTool.storyName = 'Frontend Tool Requiring Approval'
89
+ FrontendTool.parameters = {
90
+ elements: {
91
+ config: {
92
+ variant: 'standalone',
93
+ welcome: {
94
+ suggestions: [
95
+ {
96
+ title: 'Delete a file',
97
+ label: 'Delete a file',
98
+ action: 'Delete file with ID 123',
99
+ },
100
+ ],
101
+ },
102
+ tools: {
103
+ frontendTools: {
104
+ deleteFile,
105
+ },
106
+ toolsRequiringApproval: ['deleteFile'],
107
+ },
108
+ },
109
+ },
110
+ }
@@ -0,0 +1,175 @@
1
+ import React from 'react'
2
+ import { Chat } from '..'
3
+ import type { Meta, StoryFn } from '@storybook/react-vite'
4
+ import { ToolCallMessagePartProps } from '@assistant-ui/react'
5
+
6
+ const meta: Meta<typeof Chat> = {
7
+ title: 'Chat/Tools',
8
+ component: Chat,
9
+ parameters: {
10
+ layout: 'fullscreen',
11
+ },
12
+ decorators: [
13
+ (Story) => (
14
+ <div className="m-auto flex h-screen w-full max-w-3xl flex-col">
15
+ <Story />
16
+ </div>
17
+ ),
18
+ ],
19
+ } satisfies Meta<typeof Chat>
20
+
21
+ export default meta
22
+
23
+ type Story = StoryFn<typeof Chat>
24
+
25
+ const CardPinRevealComponent = ({
26
+ result,
27
+ argsText,
28
+ }: ToolCallMessagePartProps) => {
29
+ const [isFlipped, setIsFlipped] = React.useState(false)
30
+
31
+ // Parse the result to get the pin
32
+ let pin = '****'
33
+ try {
34
+ if (result) {
35
+ const parsed = typeof result === 'string' ? JSON.parse(result) : result
36
+ if (parsed?.content?.[0]?.text) {
37
+ const content = JSON.parse(parsed.content[0].text)
38
+ pin = content.pin || '****'
39
+ } else if (parsed?.pin) {
40
+ pin = parsed.pin
41
+ }
42
+ }
43
+ } catch {
44
+ // Fallback to default
45
+ }
46
+
47
+ const args = JSON.parse(argsText || '{}')
48
+ const cardNumber = args?.queryParameters?.cardNumber || '4532 •••• •••• 1234'
49
+ const cardHolder = 'JOHN DOE'
50
+ const expiry = '12/25'
51
+ const cvv = '123'
52
+
53
+ if (!cardNumber) {
54
+ return null
55
+ }
56
+
57
+ return (
58
+ <div className="my-4 perspective-[1000px]">
59
+ <div
60
+ className={`relative h-48 w-80 cursor-pointer transition-transform duration-700 [transform-style:preserve-3d] ${
61
+ isFlipped ? 'transform-[rotateY(180deg)]' : ''
62
+ }`}
63
+ onClick={() => setIsFlipped(!isFlipped)}
64
+ >
65
+ {/* Front of card */}
66
+ <div className="absolute inset-0 backface-hidden">
67
+ <div className="relative h-full w-full overflow-hidden rounded-xl bg-gradient-to-br from-indigo-600 via-purple-600 to-pink-500 p-6 text-white shadow-2xl">
68
+ {/* Card pattern overlay */}
69
+ <div className="absolute inset-0 opacity-10">
70
+ <div className="absolute -top-10 -right-10 h-40 w-40 rounded-full bg-white"></div>
71
+ <div className="absolute -bottom-10 -left-10 h-32 w-32 rounded-full bg-white"></div>
72
+ </div>
73
+
74
+ {/* Card content */}
75
+ <div className="relative z-10 flex h-full flex-col justify-between">
76
+ <div className="flex items-center justify-between">
77
+ <div className="text-2xl font-bold">VISA</div>
78
+ <div className="h-8 w-12 rounded bg-white/20"></div>
79
+ </div>
80
+
81
+ <div className="space-y-2">
82
+ <div className="font-mono text-2xl tracking-wider">
83
+ {cardNumber}
84
+ </div>
85
+ <div className="flex items-center justify-between text-sm">
86
+ <div>
87
+ <div className="text-xs opacity-70">CARDHOLDER</div>
88
+ <div className="font-semibold">{cardHolder}</div>
89
+ </div>
90
+ <div>
91
+ <div className="text-xs opacity-70">EXPIRES</div>
92
+ <div className="font-semibold">{expiry}</div>
93
+ </div>
94
+ </div>
95
+ </div>
96
+ </div>
97
+
98
+ {/* Click hint */}
99
+ <div className="absolute right-2 bottom-2 text-xs opacity-50">
100
+ Click to flip
101
+ </div>
102
+ </div>
103
+ </div>
104
+
105
+ {/* Back of card */}
106
+ <div className="absolute inset-0 transform-[rotateY(180deg)] backface-hidden">
107
+ <div className="relative h-full w-full overflow-hidden rounded-xl bg-gradient-to-br from-slate-800 via-slate-700 to-slate-900 p-6 text-white shadow-2xl">
108
+ {/* Magnetic strip */}
109
+ <div className="absolute top-8 right-0 left-0 h-12 bg-black"></div>
110
+
111
+ {/* Card content */}
112
+ <div className="relative z-10 flex h-full flex-col justify-between">
113
+ <div className="mt-16 space-y-4">
114
+ <div className="flex items-center gap-2">
115
+ <div className="h-8 flex-1 rounded bg-white/10 px-3 py-2 text-right font-mono text-sm">
116
+ {cvv}
117
+ </div>
118
+ <div className="text-xs opacity-70">CVV</div>
119
+ </div>
120
+
121
+ {/* PIN Display */}
122
+ <div className="mt-6 space-y-2">
123
+ <div className="text-xs opacity-70">PIN</div>
124
+ <div className="flex items-center gap-3">
125
+ <div className="flex h-16 w-16 items-center justify-center rounded-lg bg-gradient-to-br from-yellow-400 to-orange-500 shadow-lg">
126
+ <span className="text-2xl font-bold text-white">
127
+ {pin}
128
+ </span>
129
+ </div>
130
+ <div className="text-xs opacity-60">
131
+ Keep this PIN secure
132
+ </div>
133
+ </div>
134
+ </div>
135
+ </div>
136
+
137
+ <div className="flex items-center justify-between text-xs opacity-50">
138
+ <div>VISA</div>
139
+ <div>{cardNumber}</div>
140
+ </div>
141
+ </div>
142
+
143
+ {/* Click hint */}
144
+ <div className="absolute bottom-2 left-2 text-xs opacity-50">
145
+ Click to flip back
146
+ </div>
147
+ </div>
148
+ </div>
149
+ </div>
150
+ </div>
151
+ )
152
+ }
153
+
154
+ export const CustomToolComponent: Story = () => <Chat />
155
+ CustomToolComponent.parameters = {
156
+ elements: {
157
+ config: {
158
+ variant: 'standalone',
159
+ welcome: {
160
+ suggestions: [
161
+ {
162
+ title: 'Get card details',
163
+ label: 'for your card',
164
+ action: 'Get card details for your card number 4532 •••• •••• 1234',
165
+ },
166
+ ],
167
+ },
168
+ tools: {
169
+ components: {
170
+ kitchen_sink_get_get_card_details: CardPinRevealComponent,
171
+ },
172
+ },
173
+ },
174
+ },
175
+ }