@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,46 @@
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/Variants',
7
+ component: Chat,
8
+ parameters: {
9
+ layout: 'fullscreen',
10
+ },
11
+ } satisfies Meta<typeof Chat>
12
+
13
+ export default meta
14
+
15
+ type Story = StoryFn<typeof Chat>
16
+
17
+ export const Default: Story = () => (
18
+ <div className="flex h-full w-full flex-col gap-4 p-10">
19
+ <h1 className="text-2xl font-bold">Modal example</h1>
20
+ <p>Click the button in the bottom right corner to open the chat.</p>
21
+ <Chat />
22
+ </div>
23
+ )
24
+
25
+ export const Standalone: Story = () => <Chat />
26
+ Standalone.parameters = {
27
+ elements: { config: { variant: 'standalone' } },
28
+ }
29
+ Standalone.decorators = [
30
+ (Story) => (
31
+ <div className="m-auto flex h-screen w-full max-w-3xl flex-col">
32
+ <Story />
33
+ </div>
34
+ ),
35
+ ]
36
+
37
+ export const Sidecar: Story = () => (
38
+ <div className="mr-[400px] p-10">
39
+ <h1 className="text-2xl font-bold">Sidecar Variant</h1>
40
+ <p>The sidebar is always visible on the right.</p>
41
+ <Chat />
42
+ </div>
43
+ )
44
+ Sidecar.parameters = {
45
+ elements: { config: { variant: 'sidecar' } },
46
+ }
@@ -0,0 +1,42 @@
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/Welcome',
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 CustomMessage: Story = () => <Chat />
25
+ CustomMessage.parameters = {
26
+ elements: {
27
+ config: {
28
+ variant: 'standalone',
29
+ welcome: {
30
+ title: 'Hello there!',
31
+ subtitle: "How can I serve your organization's needs today?",
32
+ suggestions: [
33
+ {
34
+ title: 'Write a SQL query',
35
+ label: 'to find top customers',
36
+ action: 'Write a SQL query to find top customers',
37
+ },
38
+ ],
39
+ },
40
+ },
41
+ },
42
+ }
@@ -0,0 +1,9 @@
1
+ import { AssistantTool } from '@assistant-ui/react'
2
+
3
+ export function FrontendTools({
4
+ tools: frontendTools,
5
+ }: {
6
+ tools: Record<string, AssistantTool>
7
+ }) {
8
+ return <>{Object.entries(frontendTools).map(([, tool]) => tool({}))}</>
9
+ }
@@ -0,0 +1,255 @@
1
+ 'use client'
2
+
3
+ import { useMemo, useState, type FC } from 'react'
4
+ import {
5
+ Loader,
6
+ Maximize,
7
+ MessageCircleIcon,
8
+ Minimize,
9
+ XIcon,
10
+ } from 'lucide-react'
11
+ import { LazyMotion, domMax, AnimatePresence, MotionConfig } from 'motion/react'
12
+ import * as m from 'motion/react-m'
13
+
14
+ import { Thread } from '@/components/assistant-ui/thread'
15
+ import { useThemeProps } from '@/hooks/useThemeProps'
16
+ import { useRadius } from '@/hooks/useRadius'
17
+ import { useDensity } from '@/hooks/useDensity'
18
+ import { assertNever, cn } from '@/lib/utils'
19
+ import { useElements } from '@/hooks/useElements'
20
+ import { useExpanded } from '@/hooks/useExpanded'
21
+ import { EASE_OUT_QUINT } from '@/lib/easing'
22
+ import { useAssistantState } from '@assistant-ui/react'
23
+
24
+ const LAYOUT_TRANSITION = {
25
+ layout: {
26
+ duration: 0.25,
27
+ ease: EASE_OUT_QUINT,
28
+ },
29
+ } as const
30
+
31
+ type Dimensions = {
32
+ width?: string | number | `${number}%`
33
+ height?: string | number | `${number}%`
34
+ maxHeight?: string | number | `${number}%`
35
+ }
36
+
37
+ export const AssistantModal: FC = () => {
38
+ const { config } = useElements()
39
+ const themeProps = useThemeProps()
40
+ const r = useRadius()
41
+ const d = useDensity()
42
+ const [isOpen, setIsOpen] = useState(config.modal?.defaultOpen ?? false)
43
+ const { expandable, isExpanded, setIsExpanded } = useExpanded()
44
+ const title = config.modal?.title
45
+ const customIcon = config.modal?.icon
46
+
47
+ const position = config.modal?.position ?? 'bottom-right'
48
+ const anchorPositionClass = positionClassname(position)
49
+
50
+ const defaultDimensions: Dimensions = useMemo(
51
+ () =>
52
+ config.modal?.dimensions?.default ?? {
53
+ width: '500px',
54
+ height: '600px',
55
+ maxHeight: '95vh',
56
+ },
57
+ [config.modal?.dimensions?.default]
58
+ )
59
+
60
+ const expandedDimensions: Dimensions = useMemo(
61
+ () =>
62
+ config.modal?.dimensions?.expanded ?? {
63
+ width: '70vw',
64
+ height: '90vh',
65
+ },
66
+ [config.modal?.dimensions?.expanded]
67
+ )
68
+ const thread = useAssistantState(({ thread }) => thread)
69
+ const isGenerating = thread.messages.some(
70
+ (message) => message.status?.type === 'running'
71
+ )
72
+
73
+ const effectiveWidth = isExpanded
74
+ ? expandedDimensions.width
75
+ : defaultDimensions.width
76
+ const effectiveHeight = isExpanded
77
+ ? expandedDimensions.height
78
+ : defaultDimensions.height
79
+ const effectiveMaxHeight = defaultDimensions.maxHeight
80
+
81
+ return (
82
+ <LazyMotion features={domMax}>
83
+ {/* reducedMotion="user" respects prefers-reduced-motion */}
84
+ <MotionConfig reducedMotion="user" transition={LAYOUT_TRANSITION}>
85
+ <div
86
+ className={cn(
87
+ 'aui-root aui-modal-anchor fixed z-10',
88
+ anchorPositionClass,
89
+ themeProps.className,
90
+ r('lg'),
91
+ isOpen && 'shadow-xl'
92
+ )}
93
+ >
94
+ <AnimatePresence mode="wait">
95
+ {!isOpen ? (
96
+ <m.button
97
+ key="button"
98
+ layout
99
+ layoutId="chat-container"
100
+ onClick={() => setIsOpen(true)}
101
+ className={cn(
102
+ 'aui-modal-button bg-primary text-primary-foreground flex size-12 cursor-pointer items-center justify-center border shadow-lg transition-shadow hover:shadow-xl',
103
+ r('full')
104
+ )}
105
+ initial={false}
106
+ aria-label={`Open ${title}`}
107
+ style={{ originX: 1, originY: 1 }}
108
+ >
109
+ <m.div
110
+ initial={{ opacity: 0, scale: 0.8 }}
111
+ animate={{ opacity: 1, scale: 1 }}
112
+ exit={{ opacity: 0, scale: 0.8 }}
113
+ transition={{ duration: 0.2, ease: EASE_OUT_QUINT }}
114
+ className="flex size-full items-center justify-center"
115
+ >
116
+ {customIcon ? (
117
+ customIcon('closed')
118
+ ) : (
119
+ <MessageCircleIcon className="size-6" />
120
+ )}
121
+ </m.div>
122
+ </m.button>
123
+ ) : (
124
+ <m.div
125
+ key="chat"
126
+ layout
127
+ layoutId="chat-container"
128
+ className={cn(
129
+ 'aui-modal-content bg-popover text-popover-foreground flex flex-col overflow-hidden border [&>.aui-thread-root]:bg-inherit',
130
+ r('lg')
131
+ )}
132
+ initial={false}
133
+ style={{
134
+ originX: position.includes('left') ? 0 : 1,
135
+ originY: position.includes('top') ? 0 : 1,
136
+ width: effectiveWidth,
137
+ height: effectiveHeight,
138
+ maxHeight: effectiveMaxHeight,
139
+ }}
140
+ >
141
+ <m.div
142
+ className={cn(
143
+ 'aui-modal-header flex shrink-0 items-center justify-between border-b',
144
+ d('h-header'),
145
+ d('px-lg')
146
+ )}
147
+ initial={{ opacity: 0 }}
148
+ animate={{ opacity: 1 }}
149
+ transition={{
150
+ duration: 0.2,
151
+ delay: 0.1,
152
+ ease: EASE_OUT_QUINT,
153
+ }}
154
+ >
155
+ <div className={cn('flex min-w-0 items-center')}>
156
+ <span
157
+ className={cn(
158
+ 'text-md flex items-center gap-2 truncate font-medium',
159
+ isGenerating && 'shimmer'
160
+ )}
161
+ >
162
+ <span className="truncate">{title}</span>
163
+
164
+ {isGenerating && (
165
+ <Loader
166
+ className="text-muted-foreground size-4.5 animate-spin"
167
+ strokeWidth={1.25}
168
+ />
169
+ )}
170
+ </span>
171
+ </div>
172
+
173
+ <div className="flex flex-row items-center justify-end gap-1">
174
+ {expandable ? (
175
+ <button
176
+ type="button"
177
+ onClick={() => setIsExpanded((v) => !v)}
178
+ className={cn(
179
+ 'text-muted-foreground hover:text-foreground hover:bg-accent flex h-8 cursor-pointer items-center rounded-md px-2 text-xs transition-colors'
180
+ )}
181
+ aria-pressed={isExpanded}
182
+ aria-label={
183
+ isExpanded ? 'Collapse assistant' : 'Expand assistant'
184
+ }
185
+ >
186
+ {isExpanded ? (
187
+ <Minimize
188
+ strokeWidth={2}
189
+ className="size-3.5 rotate-90"
190
+ />
191
+ ) : (
192
+ <Maximize
193
+ strokeWidth={2}
194
+ className="size-3.5 rotate-90"
195
+ />
196
+ )}
197
+ </button>
198
+ ) : null}
199
+ <button
200
+ onClick={() => {
201
+ setIsOpen(false)
202
+ // Optional: reset expansion when closing
203
+ setIsExpanded(false)
204
+ }}
205
+ className="text-muted-foreground hover:text-foreground hover:bg-accent -mr-1 flex size-8 cursor-pointer items-center justify-center rounded-md transition-colors"
206
+ aria-label={`Close ${title}`}
207
+ >
208
+ <XIcon className="size-4.5" />
209
+ </button>
210
+ </div>
211
+ </m.div>
212
+
213
+ {/* Thread content */}
214
+ <m.div
215
+ className="aui-modal-thread w-full flex-1 overflow-hidden"
216
+ initial={{ opacity: 0 }}
217
+ animate={{ opacity: 1 }}
218
+ transition={{
219
+ duration: 0.2,
220
+ delay: 0.05,
221
+ ease: EASE_OUT_QUINT,
222
+ }}
223
+ >
224
+ <Thread />
225
+ </m.div>
226
+ </m.div>
227
+ )}
228
+ </AnimatePresence>
229
+ </div>
230
+ </MotionConfig>
231
+ </LazyMotion>
232
+ )
233
+ }
234
+
235
+ function positionClassname(
236
+ position:
237
+ | 'bottom-right'
238
+ | 'bottom-left'
239
+ | 'top-right'
240
+ | 'top-left'
241
+ | undefined
242
+ ): string {
243
+ switch (position) {
244
+ case 'bottom-left':
245
+ return 'left-4 bottom-4'
246
+ case 'top-right':
247
+ return 'right-4 top-4'
248
+ case 'top-left':
249
+ return 'left-4 top-4'
250
+ case 'bottom-right':
251
+ return 'right-4 bottom-4'
252
+ default:
253
+ assertNever(position)
254
+ }
255
+ }
@@ -0,0 +1,88 @@
1
+ 'use client'
2
+
3
+ import { type FC } from 'react'
4
+ import { Loader, PanelRightClose, PanelRightOpen } from 'lucide-react'
5
+ import { Thread } from '@/components/assistant-ui/thread'
6
+ import { TooltipIconButton } from '@/components/assistant-ui/tooltip-icon-button'
7
+ import { useThemeProps } from '@/hooks/useThemeProps'
8
+ import { useElements } from '@/hooks/useElements'
9
+ import { cn } from '@/lib/utils'
10
+ import { useExpanded } from '@/hooks/useExpanded'
11
+ import { LazyMotion, domMax } from 'motion/react'
12
+ import * as m from 'motion/react-m'
13
+ import { EASE_OUT_QUINT } from '@/lib/easing'
14
+ import { useAssistantState } from '@assistant-ui/react'
15
+
16
+ export const AssistantSidecar: FC = () => {
17
+ const { config } = useElements()
18
+ const themeProps = useThemeProps()
19
+ const sidecarConfig = config.sidecar ?? {}
20
+ const { title, dimensions } = sidecarConfig
21
+ const { isExpanded, setIsExpanded } = useExpanded()
22
+ const thread = useAssistantState(({ thread }) => thread)
23
+ const isGenerating = thread.messages.some(
24
+ (message) => message.status?.type === 'running'
25
+ )
26
+
27
+ return (
28
+ <LazyMotion features={domMax}>
29
+ <m.div
30
+ initial={{
31
+ width: dimensions?.default?.width ?? '400px',
32
+ height: dimensions?.default?.height ?? '100vh',
33
+ }}
34
+ animate={{
35
+ width: isExpanded
36
+ ? (dimensions?.expanded?.width ?? '800px')
37
+ : (dimensions?.default?.width ?? '400px'),
38
+ height: isExpanded
39
+ ? (dimensions?.expanded?.height ?? '100%')
40
+ : (dimensions?.default?.height ?? '100vh'),
41
+ }}
42
+ transition={{ duration: 0.3, ease: EASE_OUT_QUINT }}
43
+ className={cn(
44
+ 'aui-root aui-sidecar bg-popover text-popover-foreground fixed top-0 right-0 border-l',
45
+ themeProps.className
46
+ )}
47
+ >
48
+ {/* Header */}
49
+ <div className="aui-sidecar-header flex h-14 items-center justify-between border-b px-4">
50
+ <span
51
+ className={cn(
52
+ 'text-md flex items-center gap-2 font-medium',
53
+ isGenerating && 'shimmer'
54
+ )}
55
+ >
56
+ {title}
57
+
58
+ {isGenerating && (
59
+ <Loader
60
+ className="text-muted-foreground size-4.5 animate-spin"
61
+ strokeWidth={1.25}
62
+ />
63
+ )}
64
+ </span>
65
+ <div className="aui-sidecar-header-actions flex items-center gap-1">
66
+ <TooltipIconButton
67
+ tooltip={isExpanded ? 'Collapse' : 'Pop out'}
68
+ variant="ghost"
69
+ className="aui-sidecar-popout size-8"
70
+ onClick={() => setIsExpanded((v) => !v)}
71
+ >
72
+ {!isExpanded ? (
73
+ <PanelRightOpen className="size-4.5" />
74
+ ) : (
75
+ <PanelRightClose className="size-4.5" />
76
+ )}
77
+ </TooltipIconButton>
78
+ </div>
79
+ </div>
80
+
81
+ {/* Thread content */}
82
+ <div className="aui-sidecar-content h-[calc(100%-3.5rem)] overflow-hidden">
83
+ <Thread />
84
+ </div>
85
+ </m.div>
86
+ </LazyMotion>
87
+ )
88
+ }
@@ -0,0 +1,233 @@
1
+ 'use client'
2
+
3
+ import { PropsWithChildren, useEffect, useState, type FC } from 'react'
4
+ import { XIcon, PlusIcon, FileText } from 'lucide-react'
5
+ import {
6
+ AttachmentPrimitive,
7
+ ComposerPrimitive,
8
+ MessagePrimitive,
9
+ useAssistantState,
10
+ useAssistantApi,
11
+ } from '@assistant-ui/react'
12
+ import { useShallow } from 'zustand/shallow'
13
+ import {
14
+ Tooltip,
15
+ TooltipContent,
16
+ TooltipTrigger,
17
+ } from '@/components/ui/tooltip'
18
+ import {
19
+ Dialog,
20
+ DialogTitle,
21
+ DialogContent,
22
+ DialogTrigger,
23
+ } from '@/components/ui/dialog'
24
+ import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'
25
+ import { TooltipIconButton } from '@/components/assistant-ui/tooltip-icon-button'
26
+ import { cn } from '@/lib/utils'
27
+
28
+ const useFileSrc = (file: File | undefined) => {
29
+ const [src, setSrc] = useState<string | undefined>(undefined)
30
+
31
+ useEffect(() => {
32
+ if (!file) {
33
+ setSrc(undefined)
34
+ return
35
+ }
36
+
37
+ const objectUrl = URL.createObjectURL(file)
38
+ setSrc(objectUrl)
39
+
40
+ return () => {
41
+ URL.revokeObjectURL(objectUrl)
42
+ }
43
+ }, [file])
44
+
45
+ return src
46
+ }
47
+
48
+ const useAttachmentSrc = () => {
49
+ const { file, src } = useAssistantState(
50
+ useShallow(({ attachment }): { file?: File; src?: string } => {
51
+ if (attachment.type !== 'image') return {}
52
+ if (attachment.file) return { file: attachment.file }
53
+ const src = attachment.content?.filter((c) => c.type === 'image')[0]
54
+ ?.image
55
+ if (!src) return {}
56
+ return { src }
57
+ })
58
+ )
59
+
60
+ return useFileSrc(file) ?? src
61
+ }
62
+
63
+ type AttachmentPreviewProps = {
64
+ src: string
65
+ }
66
+
67
+ const AttachmentPreview: FC<AttachmentPreviewProps> = ({ src }) => {
68
+ const [isLoaded, setIsLoaded] = useState(false)
69
+ return (
70
+ <img
71
+ src={src}
72
+ alt="Image Preview"
73
+ className={
74
+ isLoaded
75
+ ? 'aui-attachment-preview-image-loaded block h-auto max-h-[80vh] w-auto max-w-full object-contain'
76
+ : 'aui-attachment-preview-image-loading hidden'
77
+ }
78
+ onLoad={() => setIsLoaded(true)}
79
+ />
80
+ )
81
+ }
82
+
83
+ const AttachmentPreviewDialog: FC<PropsWithChildren> = ({ children }) => {
84
+ const src = useAttachmentSrc()
85
+
86
+ if (!src) return children
87
+
88
+ return (
89
+ <Dialog>
90
+ <DialogTrigger
91
+ className="aui-attachment-preview-trigger hover:bg-accent/50 cursor-pointer transition-colors"
92
+ asChild
93
+ >
94
+ {children}
95
+ </DialogTrigger>
96
+ <DialogContent className="aui-attachment-preview-dialog-content [&_svg]:text-background [&>button]:bg-foreground/60 [&>button]:hover:[&_svg]:text-destructive p-2 sm:max-w-3xl [&>button]:rounded-full [&>button]:p-1 [&>button]:opacity-100 [&>button]:!ring-0">
97
+ <DialogTitle className="aui-sr-only sr-only">
98
+ Image Attachment Preview
99
+ </DialogTitle>
100
+ <div className="aui-attachment-preview bg-background relative mx-auto flex max-h-[80dvh] w-full items-center justify-center overflow-hidden">
101
+ <AttachmentPreview src={src} />
102
+ </div>
103
+ </DialogContent>
104
+ </Dialog>
105
+ )
106
+ }
107
+
108
+ const AttachmentThumb: FC = () => {
109
+ const isImage = useAssistantState(
110
+ ({ attachment }) => attachment.type === 'image'
111
+ )
112
+ const src = useAttachmentSrc()
113
+
114
+ return (
115
+ <Avatar className="aui-attachment-tile-avatar h-full w-full rounded-none">
116
+ <AvatarImage
117
+ src={src}
118
+ alt="Attachment preview"
119
+ className="aui-attachment-tile-image object-cover"
120
+ />
121
+ <AvatarFallback delayMs={isImage ? 200 : 0}>
122
+ <FileText className="aui-attachment-tile-fallback-icon text-muted-foreground size-8" />
123
+ </AvatarFallback>
124
+ </Avatar>
125
+ )
126
+ }
127
+
128
+ const AttachmentUI: FC = () => {
129
+ const api = useAssistantApi()
130
+ const isComposer = api.attachment.source === 'composer'
131
+
132
+ const isImage = useAssistantState(
133
+ ({ attachment }) => attachment.type === 'image'
134
+ )
135
+ const typeLabel = useAssistantState(({ attachment }) => {
136
+ const type = attachment.type
137
+ switch (type) {
138
+ case 'image':
139
+ return 'Image'
140
+ case 'document':
141
+ return 'Document'
142
+ case 'file':
143
+ return 'File'
144
+ default: {
145
+ const _exhaustiveCheck: never = type
146
+ throw new Error(`Unknown attachment type: ${_exhaustiveCheck}`)
147
+ }
148
+ }
149
+ })
150
+
151
+ return (
152
+ <Tooltip>
153
+ <AttachmentPrimitive.Root
154
+ className={cn(
155
+ 'aui-attachment-root relative',
156
+ isImage &&
157
+ 'aui-attachment-root-composer only:[&>#attachment-tile]:size-24'
158
+ )}
159
+ >
160
+ <AttachmentPreviewDialog>
161
+ <TooltipTrigger asChild>
162
+ <div
163
+ className={cn(
164
+ 'aui-attachment-tile bg-muted size-14 cursor-pointer overflow-hidden rounded-[14px] border transition-opacity hover:opacity-75',
165
+ isComposer &&
166
+ 'aui-attachment-tile-composer border-foreground/20'
167
+ )}
168
+ role="button"
169
+ id="attachment-tile"
170
+ aria-label={`${typeLabel} attachment`}
171
+ >
172
+ <AttachmentThumb />
173
+ </div>
174
+ </TooltipTrigger>
175
+ </AttachmentPreviewDialog>
176
+ {isComposer && <AttachmentRemove />}
177
+ </AttachmentPrimitive.Root>
178
+ <TooltipContent side="top">
179
+ <AttachmentPrimitive.Name />
180
+ </TooltipContent>
181
+ </Tooltip>
182
+ )
183
+ }
184
+
185
+ const AttachmentRemove: FC = () => {
186
+ return (
187
+ <AttachmentPrimitive.Remove asChild>
188
+ <TooltipIconButton
189
+ tooltip="Remove file"
190
+ className="aui-attachment-tile-remove text-muted-foreground hover:[&_svg]:text-destructive absolute top-1.5 right-1.5 size-3.5 rounded-full bg-white opacity-100 shadow-sm hover:!bg-white [&_svg]:text-black"
191
+ side="top"
192
+ >
193
+ <XIcon className="aui-attachment-remove-icon size-3 dark:stroke-[2.5px]" />
194
+ </TooltipIconButton>
195
+ </AttachmentPrimitive.Remove>
196
+ )
197
+ }
198
+
199
+ export const UserMessageAttachments: FC = () => {
200
+ return (
201
+ <div className="aui-user-message-attachments-end col-span-full col-start-1 row-start-1 flex w-full flex-row justify-end gap-2">
202
+ <MessagePrimitive.Attachments components={{ Attachment: AttachmentUI }} />
203
+ </div>
204
+ )
205
+ }
206
+
207
+ export const ComposerAttachments: FC = () => {
208
+ return (
209
+ <div className="aui-composer-attachments mb-2 flex w-full flex-row items-center gap-2 overflow-x-auto px-1.5 pt-0.5 pb-1 empty:hidden">
210
+ <ComposerPrimitive.Attachments
211
+ components={{ Attachment: AttachmentUI }}
212
+ />
213
+ </div>
214
+ )
215
+ }
216
+
217
+ export const ComposerAddAttachment: FC = () => {
218
+ return (
219
+ <ComposerPrimitive.AddAttachment asChild>
220
+ <TooltipIconButton
221
+ tooltip="Add Attachment"
222
+ side="top"
223
+ variant="ghost"
224
+ size="icon"
225
+ align="start"
226
+ className="aui-composer-add-attachment hover:bg-muted-foreground/15 dark:border-muted-foreground/15 dark:hover:bg-muted-foreground/30 size-[34px] rounded-full p-1 text-xs font-semibold"
227
+ aria-label="Add Attachment"
228
+ >
229
+ <PlusIcon className="aui-attachment-add-icon size-5 stroke-[1.5px]" />
230
+ </TooltipIconButton>
231
+ </ComposerPrimitive.AddAttachment>
232
+ )
233
+ }