@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.
- package/dist/components/Chat/stories/Sidecar.stories.d.ts +6 -0
- package/dist/elements.cjs +22 -21
- package/dist/elements.cjs.map +1 -0
- package/dist/elements.js +601 -591
- package/dist/elements.js.map +1 -0
- package/dist/index-Bj7jPiuy.cjs +1 -0
- package/dist/index-Bj7jPiuy.cjs.map +1 -0
- package/dist/index-CJRypLIa.js +1 -0
- package/dist/index-CJRypLIa.js.map +1 -0
- package/dist/plugins.cjs +1 -0
- package/dist/plugins.cjs.map +1 -0
- package/dist/plugins.js +1 -0
- package/dist/plugins.js.map +1 -0
- package/dist/server.cjs +1 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.js +1 -0
- package/dist/server.js.map +1 -0
- package/package.json +3 -2
- package/src/components/Chat/index.tsx +21 -0
- package/src/components/Chat/stories/ColorScheme.stories.tsx +52 -0
- package/src/components/Chat/stories/Composer.stories.tsx +42 -0
- package/src/components/Chat/stories/Customization.stories.tsx +88 -0
- package/src/components/Chat/stories/Density.stories.tsx +52 -0
- package/src/components/Chat/stories/FrontendTools.stories.tsx +145 -0
- package/src/components/Chat/stories/Modal.stories.tsx +84 -0
- package/src/components/Chat/stories/Model.stories.tsx +32 -0
- package/src/components/Chat/stories/Plugins.stories.tsx +50 -0
- package/src/components/Chat/stories/Radius.stories.tsx +52 -0
- package/src/components/Chat/stories/Sidecar.stories.tsx +27 -0
- package/src/components/Chat/stories/ToolApproval.stories.tsx +110 -0
- package/src/components/Chat/stories/Tools.stories.tsx +175 -0
- package/src/components/Chat/stories/Variants.stories.tsx +46 -0
- package/src/components/Chat/stories/Welcome.stories.tsx +42 -0
- package/src/components/FrontendTools/index.tsx +9 -0
- package/src/components/assistant-ui/assistant-modal.tsx +255 -0
- package/src/components/assistant-ui/assistant-sidecar.tsx +88 -0
- package/src/components/assistant-ui/attachment.tsx +233 -0
- package/src/components/assistant-ui/markdown-text.tsx +240 -0
- package/src/components/assistant-ui/reasoning.tsx +261 -0
- package/src/components/assistant-ui/thread-list.tsx +97 -0
- package/src/components/assistant-ui/thread.tsx +632 -0
- package/src/components/assistant-ui/tool-fallback.tsx +111 -0
- package/src/components/assistant-ui/tool-group.tsx +59 -0
- package/src/components/assistant-ui/tooltip-icon-button.tsx +57 -0
- package/src/components/ui/avatar.tsx +51 -0
- package/src/components/ui/button.tsx +27 -0
- package/src/components/ui/buttonVariants.ts +33 -0
- package/src/components/ui/collapsible.tsx +31 -0
- package/src/components/ui/dialog.tsx +141 -0
- package/src/components/ui/popover.tsx +46 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/tool-ui.stories.tsx +146 -0
- package/src/components/ui/tool-ui.tsx +676 -0
- package/src/components/ui/tooltip.tsx +61 -0
- package/src/contexts/ElementsProvider.tsx +256 -0
- package/src/contexts/ToolApprovalContext.tsx +120 -0
- package/src/contexts/contexts.ts +10 -0
- package/src/global.css +136 -0
- package/src/hooks/useAuth.ts +71 -0
- package/src/hooks/useDensity.ts +110 -0
- package/src/hooks/useElements.ts +14 -0
- package/src/hooks/useExpanded.ts +20 -0
- package/src/hooks/useMCPTools.ts +73 -0
- package/src/hooks/usePluginComponents.ts +34 -0
- package/src/hooks/useRadius.ts +42 -0
- package/src/hooks/useSession.ts +38 -0
- package/src/hooks/useThemeProps.ts +24 -0
- package/src/hooks/useToolApproval.ts +16 -0
- package/src/index.ts +45 -0
- package/src/lib/api.test.ts +90 -0
- package/src/lib/api.ts +8 -0
- package/src/lib/auth.ts +10 -0
- package/src/lib/easing.ts +1 -0
- package/src/lib/humanize.ts +14 -0
- package/src/lib/models.ts +22 -0
- package/src/lib/tools.ts +210 -0
- package/src/lib/utils.ts +16 -0
- package/src/plugins/README.md +49 -0
- package/src/plugins/chart/component.tsx +102 -0
- package/src/plugins/chart/index.ts +27 -0
- package/src/plugins/index.ts +7 -0
- package/src/server.ts +89 -0
- package/src/types/index.ts +726 -0
- package/src/types/plugins.ts +65 -0
- package/src/vite-env.d.ts +12 -0
|
@@ -0,0 +1,676 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { useState, useEffect } from 'react'
|
|
3
|
+
import { cva } from 'class-variance-authority'
|
|
4
|
+
import {
|
|
5
|
+
CheckIcon,
|
|
6
|
+
ChevronDownIcon,
|
|
7
|
+
ChevronRightIcon,
|
|
8
|
+
CopyIcon,
|
|
9
|
+
LoaderIcon,
|
|
10
|
+
XIcon,
|
|
11
|
+
} from 'lucide-react'
|
|
12
|
+
import { cn } from '@/lib/utils'
|
|
13
|
+
import { codeToHtml, BundledLanguage } from 'shiki'
|
|
14
|
+
import { Button } from './button'
|
|
15
|
+
import { Popover, PopoverContent, PopoverTrigger } from './popover'
|
|
16
|
+
|
|
17
|
+
/* -----------------------------------------------------------------------------
|
|
18
|
+
* Status indicator styles
|
|
19
|
+
* -------------------------------------------------------------------------- */
|
|
20
|
+
|
|
21
|
+
const statusVariants = cva(
|
|
22
|
+
'flex size-5 items-center justify-center rounded-full',
|
|
23
|
+
{
|
|
24
|
+
variants: {
|
|
25
|
+
status: {
|
|
26
|
+
pending: 'border border-dashed border-muted-foreground/50',
|
|
27
|
+
running: 'text-primary',
|
|
28
|
+
complete: 'text-green-600 dark:text-green-500',
|
|
29
|
+
error: 'text-destructive',
|
|
30
|
+
approval: 'text-amber-500',
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
defaultVariants: {
|
|
34
|
+
status: 'pending',
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
/* -----------------------------------------------------------------------------
|
|
40
|
+
* Types
|
|
41
|
+
* -------------------------------------------------------------------------- */
|
|
42
|
+
|
|
43
|
+
type ToolStatus = 'pending' | 'running' | 'complete' | 'error' | 'approval'
|
|
44
|
+
|
|
45
|
+
type ContentItem =
|
|
46
|
+
| { type: 'text'; text: string; _meta?: { 'getgram.ai/mime-type'?: string } }
|
|
47
|
+
| { type: 'image'; data: string; _meta?: { 'getgram.ai/mime-type'?: string } }
|
|
48
|
+
|
|
49
|
+
interface ToolUIProps {
|
|
50
|
+
/** Display name of the tool */
|
|
51
|
+
name: string
|
|
52
|
+
/** Optional icon to display (defaults to first letter of name) */
|
|
53
|
+
icon?: React.ReactNode
|
|
54
|
+
/** Provider/source name (e.g., "Notion", "GitHub") */
|
|
55
|
+
provider?: string
|
|
56
|
+
/** Current status of the tool execution */
|
|
57
|
+
status?: ToolStatus
|
|
58
|
+
/** Request/input data - can be string or object */
|
|
59
|
+
request?: string | Record<string, unknown>
|
|
60
|
+
/** Result/output data - can be string, object, or structured content array */
|
|
61
|
+
result?: string | Record<string, unknown> | { content: ContentItem[] }
|
|
62
|
+
/** Whether the tool card starts expanded */
|
|
63
|
+
defaultExpanded?: boolean
|
|
64
|
+
/** Additional class names */
|
|
65
|
+
className?: string
|
|
66
|
+
/** Approval callbacks */
|
|
67
|
+
onApproveOnce?: () => void
|
|
68
|
+
onApproveForSession?: () => void
|
|
69
|
+
onDeny?: () => void
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface ToolUISectionProps {
|
|
73
|
+
/** Section title */
|
|
74
|
+
title: string
|
|
75
|
+
/** Content to display - string or object (will be JSON stringified) */
|
|
76
|
+
content: string | Record<string, unknown> | { content: ContentItem[] }
|
|
77
|
+
/** Whether section starts expanded */
|
|
78
|
+
defaultExpanded?: boolean
|
|
79
|
+
/** Enable syntax highlighting */
|
|
80
|
+
highlightSyntax?: boolean
|
|
81
|
+
/** Language hint for syntax highlighting */
|
|
82
|
+
language?: BundledLanguage
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* -----------------------------------------------------------------------------
|
|
86
|
+
* Helper Functions
|
|
87
|
+
* -------------------------------------------------------------------------- */
|
|
88
|
+
|
|
89
|
+
function getLanguageFromMimeType(
|
|
90
|
+
mimeType: string
|
|
91
|
+
): BundledLanguage | undefined {
|
|
92
|
+
switch (mimeType) {
|
|
93
|
+
case 'text/markdown':
|
|
94
|
+
return 'markdown'
|
|
95
|
+
case 'text/html':
|
|
96
|
+
return 'html'
|
|
97
|
+
case 'text/css':
|
|
98
|
+
return 'css'
|
|
99
|
+
case 'application/json':
|
|
100
|
+
return 'json'
|
|
101
|
+
case 'text/javascript':
|
|
102
|
+
return 'javascript'
|
|
103
|
+
case 'text/typescript':
|
|
104
|
+
return 'typescript'
|
|
105
|
+
case 'text/python':
|
|
106
|
+
return 'python'
|
|
107
|
+
default:
|
|
108
|
+
return undefined
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function formatTextForLanguage(
|
|
113
|
+
text: string,
|
|
114
|
+
language: BundledLanguage | undefined
|
|
115
|
+
): string {
|
|
116
|
+
if (language === 'json') {
|
|
117
|
+
try {
|
|
118
|
+
return JSON.stringify(JSON.parse(text), null, 2)
|
|
119
|
+
} catch {
|
|
120
|
+
return text
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return text
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function isStructuredContent(
|
|
127
|
+
content: unknown
|
|
128
|
+
): content is { content: ContentItem[] } {
|
|
129
|
+
return (
|
|
130
|
+
typeof content === 'object' &&
|
|
131
|
+
content !== null &&
|
|
132
|
+
'content' in content &&
|
|
133
|
+
Array.isArray((content as { content: unknown }).content)
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* -----------------------------------------------------------------------------
|
|
138
|
+
* Helper Components
|
|
139
|
+
* -------------------------------------------------------------------------- */
|
|
140
|
+
|
|
141
|
+
function StatusIndicator({ status }: { status: ToolStatus }) {
|
|
142
|
+
return (
|
|
143
|
+
<div className={cn(statusVariants({ status }))}>
|
|
144
|
+
{status === 'pending' && null}
|
|
145
|
+
{status === 'running' && <LoaderIcon className="size-4 animate-spin" />}
|
|
146
|
+
{status === 'complete' && <CheckIcon className="size-4" />}
|
|
147
|
+
{status === 'error' && <XIcon className="size-4" />}
|
|
148
|
+
{status === 'approval' && (
|
|
149
|
+
<LoaderIcon className="text-muted-foreground size-4 animate-spin" />
|
|
150
|
+
)}
|
|
151
|
+
</div>
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function CopyButton({ content }: { content: string }) {
|
|
156
|
+
const [copied, setCopied] = useState(false)
|
|
157
|
+
|
|
158
|
+
const handleCopy = async (e: React.MouseEvent) => {
|
|
159
|
+
e.stopPropagation()
|
|
160
|
+
await navigator.clipboard.writeText(content)
|
|
161
|
+
setCopied(true)
|
|
162
|
+
setTimeout(() => setCopied(false), 2000)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<button
|
|
167
|
+
onClick={handleCopy}
|
|
168
|
+
className="text-muted-foreground hover:bg-accent hover:text-foreground rounded p-1 transition-colors"
|
|
169
|
+
aria-label="Copy to clipboard"
|
|
170
|
+
>
|
|
171
|
+
{copied ? (
|
|
172
|
+
<CheckIcon className="size-4" />
|
|
173
|
+
) : (
|
|
174
|
+
<CopyIcon className="size-4" />
|
|
175
|
+
)}
|
|
176
|
+
</button>
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/* -----------------------------------------------------------------------------
|
|
181
|
+
* SyntaxHighlightedCode - Code block with shiki syntax highlighting
|
|
182
|
+
* -------------------------------------------------------------------------- */
|
|
183
|
+
|
|
184
|
+
function SyntaxHighlightedCode({
|
|
185
|
+
text,
|
|
186
|
+
language,
|
|
187
|
+
className,
|
|
188
|
+
}: {
|
|
189
|
+
text: string
|
|
190
|
+
language?: BundledLanguage
|
|
191
|
+
className?: string
|
|
192
|
+
}) {
|
|
193
|
+
const [highlightedCode, setHighlightedCode] = useState<string | null>(null)
|
|
194
|
+
|
|
195
|
+
useEffect(() => {
|
|
196
|
+
if (!language) return
|
|
197
|
+
codeToHtml(text, {
|
|
198
|
+
lang: language,
|
|
199
|
+
theme: 'github-dark-default',
|
|
200
|
+
rootStyle: 'background-color: transparent;',
|
|
201
|
+
transformers: [
|
|
202
|
+
{
|
|
203
|
+
pre(node) {
|
|
204
|
+
node.properties.class =
|
|
205
|
+
'w-full py-3 px-4 max-h-[300px] overflow-y-auto whitespace-pre-wrap text-left text-sm'
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
}).then(setHighlightedCode)
|
|
210
|
+
}, [text, language])
|
|
211
|
+
|
|
212
|
+
if (!highlightedCode) {
|
|
213
|
+
return (
|
|
214
|
+
<pre
|
|
215
|
+
className={cn(
|
|
216
|
+
'w-full bg-slate-800/90 px-4 py-3 text-sm whitespace-pre-wrap text-slate-100',
|
|
217
|
+
className
|
|
218
|
+
)}
|
|
219
|
+
>
|
|
220
|
+
{text}
|
|
221
|
+
</pre>
|
|
222
|
+
)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return (
|
|
226
|
+
<div
|
|
227
|
+
className={cn('w-full bg-slate-800/90', className)}
|
|
228
|
+
dangerouslySetInnerHTML={{ __html: highlightedCode }}
|
|
229
|
+
/>
|
|
230
|
+
)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/* -----------------------------------------------------------------------------
|
|
234
|
+
* ImageContent - Display base64 encoded images with checkerboard background
|
|
235
|
+
* -------------------------------------------------------------------------- */
|
|
236
|
+
|
|
237
|
+
function ImageContent({ data }: { data: string }) {
|
|
238
|
+
const image = `data:image/png;base64,${data}`
|
|
239
|
+
return (
|
|
240
|
+
<div
|
|
241
|
+
className="flex items-center justify-center rounded-lg p-5"
|
|
242
|
+
style={{
|
|
243
|
+
backgroundImage: `linear-gradient(45deg, #ccc 25%, transparent 25%),
|
|
244
|
+
linear-gradient(135deg, #ccc 25%, transparent 25%),
|
|
245
|
+
linear-gradient(45deg, transparent 75%, #ccc 75%),
|
|
246
|
+
linear-gradient(135deg, transparent 75%, #ccc 75%)`,
|
|
247
|
+
backgroundSize: '25px 25px',
|
|
248
|
+
backgroundPosition: '0 0, 12.5px 0, 12.5px -12.5px, 0px 12.5px',
|
|
249
|
+
}}
|
|
250
|
+
>
|
|
251
|
+
<img src={image} className="max-h-[300px] max-w-full object-contain" />
|
|
252
|
+
</div>
|
|
253
|
+
)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/* -----------------------------------------------------------------------------
|
|
257
|
+
* StructuredResultContent - Renders structured content array
|
|
258
|
+
* -------------------------------------------------------------------------- */
|
|
259
|
+
|
|
260
|
+
function StructuredResultContent({
|
|
261
|
+
content,
|
|
262
|
+
}: {
|
|
263
|
+
content: { content: ContentItem[] }
|
|
264
|
+
}) {
|
|
265
|
+
return (
|
|
266
|
+
<div className="w-full">
|
|
267
|
+
{content.content.map((item, index) => {
|
|
268
|
+
switch (item.type) {
|
|
269
|
+
case 'text': {
|
|
270
|
+
const language = getLanguageFromMimeType(
|
|
271
|
+
item._meta?.['getgram.ai/mime-type'] ?? 'text/plain'
|
|
272
|
+
)
|
|
273
|
+
const formattedText = formatTextForLanguage(item.text, language)
|
|
274
|
+
return (
|
|
275
|
+
<SyntaxHighlightedCode
|
|
276
|
+
key={index}
|
|
277
|
+
text={formattedText}
|
|
278
|
+
language={language}
|
|
279
|
+
/>
|
|
280
|
+
)
|
|
281
|
+
}
|
|
282
|
+
case 'image': {
|
|
283
|
+
return <ImageContent key={index} data={item.data} />
|
|
284
|
+
}
|
|
285
|
+
default:
|
|
286
|
+
return (
|
|
287
|
+
<pre
|
|
288
|
+
key={index}
|
|
289
|
+
className="px-4 py-3 text-sm whitespace-pre-wrap"
|
|
290
|
+
>
|
|
291
|
+
{JSON.stringify(item, null, 2)}
|
|
292
|
+
</pre>
|
|
293
|
+
)
|
|
294
|
+
}
|
|
295
|
+
})}
|
|
296
|
+
</div>
|
|
297
|
+
)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/* -----------------------------------------------------------------------------
|
|
301
|
+
* ToolUISection - Expandable section for Request/Result
|
|
302
|
+
* -------------------------------------------------------------------------- */
|
|
303
|
+
|
|
304
|
+
function ToolUISection({
|
|
305
|
+
title,
|
|
306
|
+
content,
|
|
307
|
+
defaultExpanded = false,
|
|
308
|
+
highlightSyntax = true,
|
|
309
|
+
language = 'json',
|
|
310
|
+
}: ToolUISectionProps) {
|
|
311
|
+
const [isExpanded, setIsExpanded] = useState(defaultExpanded)
|
|
312
|
+
|
|
313
|
+
// For structured content, we don't stringify it
|
|
314
|
+
const isStructured = isStructuredContent(content)
|
|
315
|
+
const contentString = isStructured
|
|
316
|
+
? JSON.stringify(content, null, 2)
|
|
317
|
+
: typeof content === 'string'
|
|
318
|
+
? content
|
|
319
|
+
: JSON.stringify(content, null, 2)
|
|
320
|
+
|
|
321
|
+
return (
|
|
322
|
+
<div data-slot="tool-ui-section" className="border-border border-t">
|
|
323
|
+
<button
|
|
324
|
+
onClick={() => setIsExpanded(!isExpanded)}
|
|
325
|
+
className="hover:bg-accent/50 flex w-full cursor-pointer items-center justify-between px-4 py-2.5 text-left transition-colors"
|
|
326
|
+
>
|
|
327
|
+
<span className="text-muted-foreground text-sm">{title}</span>
|
|
328
|
+
<div className="flex items-center gap-1">
|
|
329
|
+
<CopyButton content={contentString} />
|
|
330
|
+
<ChevronRightIcon
|
|
331
|
+
className={cn(
|
|
332
|
+
'text-muted-foreground size-4 transition-transform duration-200',
|
|
333
|
+
isExpanded && 'rotate-90'
|
|
334
|
+
)}
|
|
335
|
+
/>
|
|
336
|
+
</div>
|
|
337
|
+
</button>
|
|
338
|
+
{isExpanded && (
|
|
339
|
+
<div className="border-border border-t">
|
|
340
|
+
{isStructured ? (
|
|
341
|
+
<StructuredResultContent content={content} />
|
|
342
|
+
) : highlightSyntax ? (
|
|
343
|
+
<SyntaxHighlightedCode text={contentString} language={language} />
|
|
344
|
+
) : (
|
|
345
|
+
<pre className="text-foreground overflow-x-auto px-4 py-3 text-sm whitespace-pre-wrap">
|
|
346
|
+
{contentString}
|
|
347
|
+
</pre>
|
|
348
|
+
)}
|
|
349
|
+
</div>
|
|
350
|
+
)}
|
|
351
|
+
</div>
|
|
352
|
+
)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
type ApprovalMode = 'one-time' | 'for-session'
|
|
356
|
+
|
|
357
|
+
/* -----------------------------------------------------------------------------
|
|
358
|
+
* ToolUI - Main component
|
|
359
|
+
* -------------------------------------------------------------------------- */
|
|
360
|
+
|
|
361
|
+
function ToolUI({
|
|
362
|
+
name,
|
|
363
|
+
icon,
|
|
364
|
+
provider,
|
|
365
|
+
status = 'complete',
|
|
366
|
+
request,
|
|
367
|
+
result,
|
|
368
|
+
defaultExpanded = false,
|
|
369
|
+
className,
|
|
370
|
+
onApproveOnce,
|
|
371
|
+
onApproveForSession,
|
|
372
|
+
onDeny,
|
|
373
|
+
}: ToolUIProps) {
|
|
374
|
+
const isApprovalPending =
|
|
375
|
+
status === 'approval' && onApproveOnce !== undefined && onDeny !== undefined
|
|
376
|
+
// Auto-expand when approval is pending, collapse when approved
|
|
377
|
+
const [isExpanded, setIsExpanded] = useState(defaultExpanded)
|
|
378
|
+
const hasContent = request !== undefined || result !== undefined
|
|
379
|
+
|
|
380
|
+
// Track approval mode: 'one-time' or 'for-session'
|
|
381
|
+
const [approvalMode, setApprovalMode] = useState<ApprovalMode>('one-time')
|
|
382
|
+
const [isDropdownOpen, setIsDropdownOpen] = useState(false)
|
|
383
|
+
|
|
384
|
+
// Collapse when transitioning from approval to non-approval (i.e., when approved/denied)
|
|
385
|
+
useEffect(() => {
|
|
386
|
+
if (!isApprovalPending && isExpanded && !defaultExpanded) {
|
|
387
|
+
setIsExpanded(false)
|
|
388
|
+
}
|
|
389
|
+
}, [isApprovalPending])
|
|
390
|
+
|
|
391
|
+
// Handle approve based on selected mode
|
|
392
|
+
const handleApprove = () => {
|
|
393
|
+
if (approvalMode === 'for-session' && onApproveForSession) {
|
|
394
|
+
onApproveForSession()
|
|
395
|
+
} else if (onApproveOnce) {
|
|
396
|
+
onApproveOnce()
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return (
|
|
401
|
+
<div
|
|
402
|
+
data-slot="tool-ui"
|
|
403
|
+
className={cn(
|
|
404
|
+
'border-border bg-card overflow-hidden rounded-lg border',
|
|
405
|
+
className
|
|
406
|
+
)}
|
|
407
|
+
>
|
|
408
|
+
{/* Header with provider */}
|
|
409
|
+
{provider && (
|
|
410
|
+
<div
|
|
411
|
+
data-slot="tool-ui-provider"
|
|
412
|
+
className={cn(
|
|
413
|
+
'border-border flex items-center gap-2 border-b px-4 py-2.5'
|
|
414
|
+
)}
|
|
415
|
+
>
|
|
416
|
+
{icon ? (
|
|
417
|
+
<span className="flex size-5 items-center justify-center">
|
|
418
|
+
{icon}
|
|
419
|
+
</span>
|
|
420
|
+
) : (
|
|
421
|
+
<span className="bg-muted flex size-5 items-center justify-center rounded text-xs font-medium">
|
|
422
|
+
{provider.charAt(0).toUpperCase()}
|
|
423
|
+
</span>
|
|
424
|
+
)}
|
|
425
|
+
<span className="text-sm font-medium">{provider}</span>
|
|
426
|
+
</div>
|
|
427
|
+
)}
|
|
428
|
+
|
|
429
|
+
{/* Tool row */}
|
|
430
|
+
<button
|
|
431
|
+
onClick={() => hasContent && setIsExpanded(!isExpanded)}
|
|
432
|
+
disabled={!hasContent}
|
|
433
|
+
className={cn(
|
|
434
|
+
'flex w-full items-center gap-2 px-4 py-3 text-left',
|
|
435
|
+
hasContent && 'hover:bg-accent/50 cursor-pointer transition-colors'
|
|
436
|
+
)}
|
|
437
|
+
>
|
|
438
|
+
<StatusIndicator status={status} />
|
|
439
|
+
<span
|
|
440
|
+
className={cn(
|
|
441
|
+
'flex-1 text-sm',
|
|
442
|
+
!provider && isApprovalPending && 'shimmer'
|
|
443
|
+
)}
|
|
444
|
+
>
|
|
445
|
+
{name}
|
|
446
|
+
</span>
|
|
447
|
+
{hasContent && (
|
|
448
|
+
<ChevronDownIcon
|
|
449
|
+
className={cn(
|
|
450
|
+
'text-muted-foreground size-4 transition-transform duration-200',
|
|
451
|
+
isExpanded && 'rotate-180'
|
|
452
|
+
)}
|
|
453
|
+
/>
|
|
454
|
+
)}
|
|
455
|
+
</button>
|
|
456
|
+
|
|
457
|
+
{/* Expandable content */}
|
|
458
|
+
{isExpanded && hasContent && (
|
|
459
|
+
<div data-slot="tool-ui-content">
|
|
460
|
+
{/* When not approval pending, use collapsible section */}
|
|
461
|
+
{request !== undefined && (
|
|
462
|
+
<ToolUISection
|
|
463
|
+
title="Arguments"
|
|
464
|
+
content={request}
|
|
465
|
+
highlightSyntax
|
|
466
|
+
language="json"
|
|
467
|
+
/>
|
|
468
|
+
)}
|
|
469
|
+
{/* Hide output when approval is pending */}
|
|
470
|
+
{result !== undefined && (
|
|
471
|
+
<ToolUISection
|
|
472
|
+
title="Output"
|
|
473
|
+
content={result}
|
|
474
|
+
highlightSyntax
|
|
475
|
+
language="json"
|
|
476
|
+
/>
|
|
477
|
+
)}
|
|
478
|
+
</div>
|
|
479
|
+
)}
|
|
480
|
+
|
|
481
|
+
{/* Approval actions */}
|
|
482
|
+
{isApprovalPending && (
|
|
483
|
+
<div
|
|
484
|
+
data-slot="tool-ui-approval-actions"
|
|
485
|
+
className="border-border flex items-center justify-end gap-2 border-t px-4 py-3"
|
|
486
|
+
>
|
|
487
|
+
<div>
|
|
488
|
+
<span className="text-muted-foreground text-sm">
|
|
489
|
+
This tool requires approval
|
|
490
|
+
</span>
|
|
491
|
+
</div>
|
|
492
|
+
<div className="ml-auto flex items-center gap-2">
|
|
493
|
+
<Button
|
|
494
|
+
variant="outline"
|
|
495
|
+
size="sm"
|
|
496
|
+
onClick={onDeny}
|
|
497
|
+
className="text-destructive hover:bg-destructive/10"
|
|
498
|
+
>
|
|
499
|
+
<XIcon className="mr-1 size-3" />
|
|
500
|
+
Deny
|
|
501
|
+
</Button>
|
|
502
|
+
{/* Split button: main approve + dropdown for options */}
|
|
503
|
+
<div className="flex items-center">
|
|
504
|
+
<Button
|
|
505
|
+
variant="default"
|
|
506
|
+
size="sm"
|
|
507
|
+
onClick={handleApprove}
|
|
508
|
+
className="flex cursor-pointer justify-between gap-1 rounded-r-none bg-emerald-600 hover:bg-emerald-700"
|
|
509
|
+
>
|
|
510
|
+
<CheckIcon className="mr-1 size-3" />
|
|
511
|
+
|
|
512
|
+
{/* The min-width is needed to prevent the button from shifting when the text changes */}
|
|
513
|
+
<span className="min-w-[110px]">
|
|
514
|
+
{approvalMode === 'one-time'
|
|
515
|
+
? 'Approve this time'
|
|
516
|
+
: 'Approve always'}
|
|
517
|
+
</span>
|
|
518
|
+
</Button>
|
|
519
|
+
<Popover open={isDropdownOpen} onOpenChange={setIsDropdownOpen}>
|
|
520
|
+
<PopoverTrigger asChild>
|
|
521
|
+
<Button
|
|
522
|
+
variant="default"
|
|
523
|
+
size="sm"
|
|
524
|
+
className="cursor-pointer rounded-l-none border-l border-emerald-700 bg-emerald-600 px-2 hover:bg-emerald-700"
|
|
525
|
+
>
|
|
526
|
+
<ChevronDownIcon className="size-3" />
|
|
527
|
+
</Button>
|
|
528
|
+
</PopoverTrigger>
|
|
529
|
+
<PopoverContent align="end" className="w-64 p-1" sideOffset={4}>
|
|
530
|
+
<button
|
|
531
|
+
onClick={() => {
|
|
532
|
+
setApprovalMode('one-time')
|
|
533
|
+
setIsDropdownOpen(false)
|
|
534
|
+
}}
|
|
535
|
+
className="hover:bg-accent relative flex w-full items-start gap-2 rounded-sm px-2 py-2 text-left"
|
|
536
|
+
>
|
|
537
|
+
<CheckIcon
|
|
538
|
+
className={cn(
|
|
539
|
+
'relative top-1 mt-0.5 size-3 shrink-0',
|
|
540
|
+
approvalMode !== 'one-time' && 'invisible'
|
|
541
|
+
)}
|
|
542
|
+
/>
|
|
543
|
+
<div className="flex flex-col gap-0.5">
|
|
544
|
+
<span className="text-sm">Approve only once</span>
|
|
545
|
+
<span className="text-muted-foreground text-xs">
|
|
546
|
+
You'll be asked again next time
|
|
547
|
+
</span>
|
|
548
|
+
</div>
|
|
549
|
+
</button>
|
|
550
|
+
{onApproveForSession && (
|
|
551
|
+
<button
|
|
552
|
+
onClick={() => {
|
|
553
|
+
setApprovalMode('for-session')
|
|
554
|
+
setIsDropdownOpen(false)
|
|
555
|
+
}}
|
|
556
|
+
className="hover:bg-accent relative flex w-full items-start gap-2 rounded-sm px-2 py-2 text-left"
|
|
557
|
+
>
|
|
558
|
+
<CheckIcon
|
|
559
|
+
className={cn(
|
|
560
|
+
'relative top-1 mt-0.5 size-3 shrink-0',
|
|
561
|
+
approvalMode !== 'for-session' && 'invisible'
|
|
562
|
+
)}
|
|
563
|
+
/>
|
|
564
|
+
<div className="flex flex-col gap-0.5">
|
|
565
|
+
<span className="text-sm">Approve always</span>
|
|
566
|
+
<span className="text-muted-foreground text-xs">
|
|
567
|
+
Trust this tool for the session
|
|
568
|
+
</span>
|
|
569
|
+
</div>
|
|
570
|
+
</button>
|
|
571
|
+
)}
|
|
572
|
+
</PopoverContent>
|
|
573
|
+
</Popover>
|
|
574
|
+
</div>
|
|
575
|
+
</div>
|
|
576
|
+
</div>
|
|
577
|
+
)}
|
|
578
|
+
</div>
|
|
579
|
+
)
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/* -----------------------------------------------------------------------------
|
|
583
|
+
* ToolUIGroup - Container for multiple tool calls
|
|
584
|
+
* -------------------------------------------------------------------------- */
|
|
585
|
+
|
|
586
|
+
interface ToolUIGroupProps {
|
|
587
|
+
/** Title for the group header */
|
|
588
|
+
title: string
|
|
589
|
+
/** Optional icon */
|
|
590
|
+
icon?: React.ReactNode
|
|
591
|
+
/** Overall status of the group */
|
|
592
|
+
status?: 'running' | 'complete'
|
|
593
|
+
/** Whether the group starts expanded */
|
|
594
|
+
defaultExpanded?: boolean
|
|
595
|
+
/** Child tool UI components */
|
|
596
|
+
children: React.ReactNode
|
|
597
|
+
/** Additional class names */
|
|
598
|
+
className?: string
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function ToolUIGroup({
|
|
602
|
+
title,
|
|
603
|
+
icon,
|
|
604
|
+
status = 'complete',
|
|
605
|
+
defaultExpanded = false,
|
|
606
|
+
children,
|
|
607
|
+
className,
|
|
608
|
+
}: ToolUIGroupProps) {
|
|
609
|
+
const [isExpanded, setIsExpanded] = useState(defaultExpanded)
|
|
610
|
+
|
|
611
|
+
return (
|
|
612
|
+
<div
|
|
613
|
+
data-slot="tool-ui-group"
|
|
614
|
+
className={cn(
|
|
615
|
+
'border-border bg-card overflow-hidden rounded-lg border',
|
|
616
|
+
className
|
|
617
|
+
)}
|
|
618
|
+
>
|
|
619
|
+
{/* Group header */}
|
|
620
|
+
<button
|
|
621
|
+
onClick={() => setIsExpanded(!isExpanded)}
|
|
622
|
+
className="hover:bg-accent/50 flex w-full items-center gap-2 px-4 py-3 text-left transition-colors"
|
|
623
|
+
>
|
|
624
|
+
{icon || (
|
|
625
|
+
<StatusIndicator
|
|
626
|
+
status={status === 'running' ? 'running' : 'complete'}
|
|
627
|
+
/>
|
|
628
|
+
)}
|
|
629
|
+
<span
|
|
630
|
+
className={cn(
|
|
631
|
+
'flex-1 text-sm font-medium',
|
|
632
|
+
status === 'running' && 'shimmer'
|
|
633
|
+
)}
|
|
634
|
+
>
|
|
635
|
+
{title}
|
|
636
|
+
</span>
|
|
637
|
+
<ChevronDownIcon
|
|
638
|
+
className={cn(
|
|
639
|
+
'text-muted-foreground size-4 transition-transform duration-200',
|
|
640
|
+
isExpanded && 'rotate-180'
|
|
641
|
+
)}
|
|
642
|
+
/>
|
|
643
|
+
</button>
|
|
644
|
+
|
|
645
|
+
{/* Expandable children */}
|
|
646
|
+
{isExpanded && (
|
|
647
|
+
<div
|
|
648
|
+
data-slot="tool-ui-group-content"
|
|
649
|
+
className="border-border border-t"
|
|
650
|
+
>
|
|
651
|
+
{children}
|
|
652
|
+
</div>
|
|
653
|
+
)}
|
|
654
|
+
</div>
|
|
655
|
+
)
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/* -----------------------------------------------------------------------------
|
|
659
|
+
* Exports
|
|
660
|
+
* -------------------------------------------------------------------------- */
|
|
661
|
+
|
|
662
|
+
export {
|
|
663
|
+
ToolUI,
|
|
664
|
+
ToolUISection,
|
|
665
|
+
ToolUIGroup,
|
|
666
|
+
SyntaxHighlightedCode,
|
|
667
|
+
StatusIndicator,
|
|
668
|
+
CopyButton,
|
|
669
|
+
}
|
|
670
|
+
export type {
|
|
671
|
+
ToolUIProps,
|
|
672
|
+
ToolUISectionProps,
|
|
673
|
+
ToolUIGroupProps,
|
|
674
|
+
ToolStatus,
|
|
675
|
+
ContentItem,
|
|
676
|
+
}
|