@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,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,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
|
+
}
|