@gram-ai/elements 1.18.5 → 1.18.7

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 (86) hide show
  1. package/dist/components/Chat/stories/Sidecar.stories.d.ts +6 -0
  2. package/dist/elements.cjs +29 -28
  3. package/dist/elements.cjs.map +1 -0
  4. package/dist/elements.css +1 -1
  5. package/dist/elements.js +1945 -1918
  6. package/dist/elements.js.map +1 -0
  7. package/dist/index-Bj7jPiuy.cjs +1 -0
  8. package/dist/index-Bj7jPiuy.cjs.map +1 -0
  9. package/dist/index-CJRypLIa.js +1 -0
  10. package/dist/index-CJRypLIa.js.map +1 -0
  11. package/dist/plugins.cjs +1 -0
  12. package/dist/plugins.cjs.map +1 -0
  13. package/dist/plugins.js +1 -0
  14. package/dist/plugins.js.map +1 -0
  15. package/dist/server.cjs +1 -0
  16. package/dist/server.cjs.map +1 -0
  17. package/dist/server.js +1 -0
  18. package/dist/server.js.map +1 -0
  19. package/package.json +3 -2
  20. package/src/components/Chat/index.tsx +21 -0
  21. package/src/components/Chat/stories/ColorScheme.stories.tsx +52 -0
  22. package/src/components/Chat/stories/Composer.stories.tsx +42 -0
  23. package/src/components/Chat/stories/Customization.stories.tsx +88 -0
  24. package/src/components/Chat/stories/Density.stories.tsx +52 -0
  25. package/src/components/Chat/stories/FrontendTools.stories.tsx +145 -0
  26. package/src/components/Chat/stories/Modal.stories.tsx +84 -0
  27. package/src/components/Chat/stories/Model.stories.tsx +32 -0
  28. package/src/components/Chat/stories/Plugins.stories.tsx +50 -0
  29. package/src/components/Chat/stories/Radius.stories.tsx +52 -0
  30. package/src/components/Chat/stories/Sidecar.stories.tsx +27 -0
  31. package/src/components/Chat/stories/ToolApproval.stories.tsx +110 -0
  32. package/src/components/Chat/stories/Tools.stories.tsx +175 -0
  33. package/src/components/Chat/stories/Variants.stories.tsx +46 -0
  34. package/src/components/Chat/stories/Welcome.stories.tsx +42 -0
  35. package/src/components/FrontendTools/index.tsx +9 -0
  36. package/src/components/assistant-ui/assistant-modal.tsx +255 -0
  37. package/src/components/assistant-ui/assistant-sidecar.tsx +88 -0
  38. package/src/components/assistant-ui/attachment.tsx +233 -0
  39. package/src/components/assistant-ui/markdown-text.tsx +240 -0
  40. package/src/components/assistant-ui/reasoning.tsx +261 -0
  41. package/src/components/assistant-ui/thread-list.tsx +97 -0
  42. package/src/components/assistant-ui/thread.tsx +632 -0
  43. package/src/components/assistant-ui/tool-fallback.tsx +111 -0
  44. package/src/components/assistant-ui/tool-group.tsx +59 -0
  45. package/src/components/assistant-ui/tooltip-icon-button.tsx +57 -0
  46. package/src/components/ui/avatar.tsx +51 -0
  47. package/src/components/ui/button.tsx +27 -0
  48. package/src/components/ui/buttonVariants.ts +33 -0
  49. package/src/components/ui/collapsible.tsx +31 -0
  50. package/src/components/ui/dialog.tsx +141 -0
  51. package/src/components/ui/popover.tsx +46 -0
  52. package/src/components/ui/skeleton.tsx +13 -0
  53. package/src/components/ui/tool-ui.stories.tsx +146 -0
  54. package/src/components/ui/tool-ui.tsx +676 -0
  55. package/src/components/ui/tooltip.tsx +61 -0
  56. package/src/contexts/ElementsProvider.tsx +287 -0
  57. package/src/contexts/ToolApprovalContext.tsx +120 -0
  58. package/src/contexts/contexts.ts +10 -0
  59. package/src/global.css +136 -0
  60. package/src/hooks/useAuth.ts +71 -0
  61. package/src/hooks/useDensity.ts +110 -0
  62. package/src/hooks/useElements.ts +14 -0
  63. package/src/hooks/useExpanded.ts +20 -0
  64. package/src/hooks/useMCPTools.ts +73 -0
  65. package/src/hooks/usePluginComponents.ts +34 -0
  66. package/src/hooks/useRadius.ts +42 -0
  67. package/src/hooks/useSession.ts +38 -0
  68. package/src/hooks/useThemeProps.ts +24 -0
  69. package/src/hooks/useToolApproval.ts +16 -0
  70. package/src/index.ts +45 -0
  71. package/src/lib/api.test.ts +90 -0
  72. package/src/lib/api.ts +8 -0
  73. package/src/lib/auth.ts +10 -0
  74. package/src/lib/easing.ts +1 -0
  75. package/src/lib/humanize.ts +14 -0
  76. package/src/lib/models.ts +22 -0
  77. package/src/lib/tools.ts +210 -0
  78. package/src/lib/utils.ts +16 -0
  79. package/src/plugins/README.md +49 -0
  80. package/src/plugins/chart/component.tsx +102 -0
  81. package/src/plugins/chart/index.ts +27 -0
  82. package/src/plugins/index.ts +7 -0
  83. package/src/server.ts +89 -0
  84. package/src/types/index.ts +726 -0
  85. package/src/types/plugins.ts +65 -0
  86. package/src/vite-env.d.ts +12 -0
@@ -0,0 +1,240 @@
1
+ 'use client'
2
+
3
+ import '@assistant-ui/react-markdown/styles/dot.css'
4
+
5
+ import {
6
+ type CodeHeaderProps,
7
+ MarkdownTextPrimitive,
8
+ unstable_memoizeMarkdownComponents as memoizeMarkdownComponents,
9
+ useIsMarkdownCodeBlock,
10
+ } from '@assistant-ui/react-markdown'
11
+ import { CheckIcon, CopyIcon } from 'lucide-react'
12
+ import { type FC, memo, useState } from 'react'
13
+ import remarkGfm from 'remark-gfm'
14
+
15
+ import { TooltipIconButton } from '@/components/assistant-ui/tooltip-icon-button'
16
+ import { cn } from '@/lib/utils'
17
+ import { useElements } from '@/hooks/useElements'
18
+ import { useComponentsByLanguage } from '@/hooks/usePluginComponents'
19
+ import { useAssistantState } from '@assistant-ui/react'
20
+
21
+ const MarkdownTextImpl = () => {
22
+ const { plugins } = useElements()
23
+ const componentsByLanguage = useComponentsByLanguage(plugins)
24
+
25
+ return (
26
+ <MarkdownTextPrimitive
27
+ remarkPlugins={[remarkGfm]}
28
+ className="aui-md"
29
+ components={defaultComponents}
30
+ componentsByLanguage={componentsByLanguage}
31
+ />
32
+ )
33
+ }
34
+
35
+ export const MarkdownText = memo(MarkdownTextImpl)
36
+
37
+ const CodeHeader: FC<CodeHeaderProps> = ({ language, code }) => {
38
+ const message = useAssistantState(({ message }) => message)
39
+ const messageIsComplete = message.status?.type === 'complete'
40
+ const { isCopied, copyToClipboard } = useCopyToClipboard()
41
+ const onCopy = () => {
42
+ if (!code || isCopied) return
43
+ copyToClipboard(code)
44
+ }
45
+
46
+ if (!messageIsComplete) {
47
+ return null
48
+ }
49
+ return (
50
+ <div className="aui-code-header-root bg-muted-foreground/15 text-foreground dark:bg-muted-foreground/20 mt-4 flex items-center justify-between gap-4 rounded-t-lg px-4 py-2 text-sm font-semibold">
51
+ <span className="aui-code-header-language lowercase [&>span]:text-xs">
52
+ {language}
53
+ </span>
54
+ <TooltipIconButton tooltip="Copy" onClick={onCopy}>
55
+ {!isCopied && <CopyIcon />}
56
+ {isCopied && <CheckIcon />}
57
+ </TooltipIconButton>
58
+ </div>
59
+ )
60
+ }
61
+
62
+ const useCopyToClipboard = ({
63
+ copiedDuration = 3000,
64
+ }: {
65
+ copiedDuration?: number
66
+ } = {}) => {
67
+ const [isCopied, setIsCopied] = useState<boolean>(false)
68
+
69
+ const copyToClipboard = (value: string) => {
70
+ if (!value) return
71
+
72
+ navigator.clipboard.writeText(value).then(() => {
73
+ setIsCopied(true)
74
+ setTimeout(() => setIsCopied(false), copiedDuration)
75
+ })
76
+ }
77
+
78
+ return { isCopied, copyToClipboard }
79
+ }
80
+
81
+ const defaultComponents = memoizeMarkdownComponents({
82
+ h1: ({ className, ...props }) => (
83
+ <h1
84
+ className={cn(
85
+ 'aui-md-h1 mb-8 scroll-m-20 text-4xl font-extrabold tracking-tight last:mb-0',
86
+ className
87
+ )}
88
+ {...props}
89
+ />
90
+ ),
91
+ h2: ({ className, ...props }) => (
92
+ <h2
93
+ className={cn(
94
+ 'aui-md-h2 mt-8 mb-4 scroll-m-20 text-3xl font-semibold tracking-tight first:mt-0 last:mb-0',
95
+ className
96
+ )}
97
+ {...props}
98
+ />
99
+ ),
100
+ h3: ({ className, ...props }) => (
101
+ <h3
102
+ className={cn(
103
+ 'aui-md-h3 mt-6 mb-4 scroll-m-20 text-2xl font-semibold tracking-tight first:mt-0 last:mb-0',
104
+ className
105
+ )}
106
+ {...props}
107
+ />
108
+ ),
109
+ h4: ({ className, ...props }) => (
110
+ <h4
111
+ className={cn(
112
+ 'aui-md-h4 mt-6 mb-4 scroll-m-20 text-xl font-semibold tracking-tight first:mt-0 last:mb-0',
113
+ className
114
+ )}
115
+ {...props}
116
+ />
117
+ ),
118
+ h5: ({ className, ...props }) => (
119
+ <h5
120
+ className={cn(
121
+ 'aui-md-h5 my-4 text-lg font-semibold first:mt-0 last:mb-0',
122
+ className
123
+ )}
124
+ {...props}
125
+ />
126
+ ),
127
+ h6: ({ className, ...props }) => (
128
+ <h6
129
+ className={cn(
130
+ 'aui-md-h6 my-4 font-semibold first:mt-0 last:mb-0',
131
+ className
132
+ )}
133
+ {...props}
134
+ />
135
+ ),
136
+ p: ({ className, ...props }) => (
137
+ <p
138
+ className={cn(
139
+ 'aui-md-p mt-5 mb-5 leading-7 first:mt-0 last:mb-0',
140
+ className
141
+ )}
142
+ {...props}
143
+ />
144
+ ),
145
+ a: ({ className, ...props }) => (
146
+ <a
147
+ className={cn(
148
+ 'aui-md-a text-primary font-medium underline underline-offset-4',
149
+ className
150
+ )}
151
+ {...props}
152
+ />
153
+ ),
154
+ blockquote: ({ className, ...props }) => (
155
+ <blockquote
156
+ className={cn('aui-md-blockquote border-l-2 pl-6 italic', className)}
157
+ {...props}
158
+ />
159
+ ),
160
+ ul: ({ className, ...props }) => (
161
+ <ul
162
+ className={cn('aui-md-ul my-5 ml-6 list-disc [&>li]:mt-2', className)}
163
+ {...props}
164
+ />
165
+ ),
166
+ ol: ({ className, ...props }) => (
167
+ <ol
168
+ className={cn('aui-md-ol my-5 ml-6 list-decimal [&>li]:mt-2', className)}
169
+ {...props}
170
+ />
171
+ ),
172
+ hr: ({ className, ...props }) => (
173
+ <hr className={cn('aui-md-hr my-5 border-b', className)} {...props} />
174
+ ),
175
+ table: ({ className, ...props }) => (
176
+ <table
177
+ className={cn(
178
+ 'aui-md-table my-5 w-full border-separate border-spacing-0 overflow-y-auto',
179
+ className
180
+ )}
181
+ {...props}
182
+ />
183
+ ),
184
+ th: ({ className, ...props }) => (
185
+ <th
186
+ className={cn(
187
+ 'aui-md-th bg-muted px-4 py-2 text-left font-bold first:rounded-tl-lg last:rounded-tr-lg [[align=center]]:text-center [[align=right]]:text-right',
188
+ className
189
+ )}
190
+ {...props}
191
+ />
192
+ ),
193
+ td: ({ className, ...props }) => (
194
+ <td
195
+ className={cn(
196
+ 'aui-md-td border-b border-l px-4 py-2 text-left last:border-r [[align=center]]:text-center [[align=right]]:text-right',
197
+ className
198
+ )}
199
+ {...props}
200
+ />
201
+ ),
202
+ tr: ({ className, ...props }) => (
203
+ <tr
204
+ className={cn(
205
+ 'aui-md-tr m-0 border-b p-0 first:border-t [&:last-child>td:first-child]:rounded-bl-lg [&:last-child>td:last-child]:rounded-br-lg',
206
+ className
207
+ )}
208
+ {...props}
209
+ />
210
+ ),
211
+ sup: ({ className, ...props }) => (
212
+ <sup
213
+ className={cn('aui-md-sup [&>a]:text-xs [&>a]:no-underline', className)}
214
+ {...props}
215
+ />
216
+ ),
217
+ pre: ({ className, ...props }) => (
218
+ <pre
219
+ className={cn(
220
+ 'aui-md-pre text-foreground bg-muted overflow-x-auto rounded-t-none! rounded-b-lg border border-t-0 p-4',
221
+ className
222
+ )}
223
+ {...props}
224
+ />
225
+ ),
226
+ code: function Code({ className, ...props }) {
227
+ const isCodeBlock = useIsMarkdownCodeBlock()
228
+ return (
229
+ <code
230
+ className={cn(
231
+ !isCodeBlock &&
232
+ 'aui-md-inline-code bg-muted rounded border font-semibold',
233
+ className
234
+ )}
235
+ {...props}
236
+ />
237
+ )
238
+ },
239
+ CodeHeader,
240
+ })
@@ -0,0 +1,261 @@
1
+ 'use client'
2
+
3
+ import { BrainIcon, ChevronDownIcon } from 'lucide-react'
4
+ import {
5
+ memo,
6
+ useCallback,
7
+ useRef,
8
+ useState,
9
+ type FC,
10
+ type PropsWithChildren,
11
+ } from 'react'
12
+
13
+ import {
14
+ useAssistantState,
15
+ useScrollLock,
16
+ type ReasoningGroupComponent,
17
+ type ReasoningMessagePartComponent,
18
+ } from '@assistant-ui/react'
19
+
20
+ import { MarkdownText } from '@/components/assistant-ui/markdown-text'
21
+ import {
22
+ Collapsible,
23
+ CollapsibleContent,
24
+ CollapsibleTrigger,
25
+ } from '@/components/ui/collapsible'
26
+ import { cn } from '@/lib/utils'
27
+
28
+ const ANIMATION_DURATION = 200
29
+
30
+ /**
31
+ * Root collapsible container that manages open/closed state and scroll lock.
32
+ * Provides animation timing via CSS variable and prevents scroll jumps on collapse.
33
+ */
34
+ const ReasoningRoot: FC<
35
+ PropsWithChildren<{
36
+ className?: string
37
+ }>
38
+ > = ({ className, children }) => {
39
+ const collapsibleRef = useRef<HTMLDivElement>(null)
40
+ const [isOpen, setIsOpen] = useState(false)
41
+ const lockScroll = useScrollLock(collapsibleRef, ANIMATION_DURATION)
42
+
43
+ const handleOpenChange = useCallback(
44
+ (open: boolean) => {
45
+ if (!open) {
46
+ lockScroll()
47
+ }
48
+ setIsOpen(open)
49
+ },
50
+ [lockScroll]
51
+ )
52
+
53
+ return (
54
+ <Collapsible
55
+ ref={collapsibleRef}
56
+ open={isOpen}
57
+ onOpenChange={handleOpenChange}
58
+ className={cn('aui-reasoning-root mb-4 w-full', className)}
59
+ style={
60
+ {
61
+ '--animation-duration': `${ANIMATION_DURATION}ms`,
62
+ } as React.CSSProperties
63
+ }
64
+ >
65
+ {children}
66
+ </Collapsible>
67
+ )
68
+ }
69
+
70
+ ReasoningRoot.displayName = 'ReasoningRoot'
71
+
72
+ /**
73
+ * Gradient overlay that softens the bottom edge during expand/collapse animations.
74
+ * Animation: Fades out with delay when opening and fades back in when closing.
75
+ */
76
+ const GradientFade: FC<{ className?: string }> = ({ className }) => (
77
+ <div
78
+ className={cn(
79
+ 'aui-reasoning-fade pointer-events-none absolute inset-x-0 bottom-0 z-10 h-16',
80
+ 'bg-[linear-gradient(to_top,var(--color-background),transparent)]',
81
+ 'fade-in-0 animate-in',
82
+ 'group-data-[state=open]/collapsible-content:animate-out',
83
+ 'group-data-[state=open]/collapsible-content:fade-out-0',
84
+ 'group-data-[state=open]/collapsible-content:delay-[calc(var(--animation-duration)*0.75)]', // calc for timing the delay
85
+ 'group-data-[state=open]/collapsible-content:fill-mode-forwards',
86
+ 'duration-(--animation-duration)',
87
+ 'group-data-[state=open]/collapsible-content:duration-(--animation-duration)',
88
+ className
89
+ )}
90
+ />
91
+ )
92
+
93
+ /**
94
+ * Trigger button for the Reasoning collapsible.
95
+ * Composed of icons, label, and text shimmer animation when reasoning is being streamed.
96
+ */
97
+ const ReasoningTrigger: FC<{ active: boolean; className?: string }> = ({
98
+ active,
99
+ className,
100
+ }) => (
101
+ <CollapsibleTrigger
102
+ className={cn(
103
+ 'aui-reasoning-trigger group/trigger text-muted-foreground hover:text-foreground -mb-2 flex max-w-[75%] items-center gap-2 py-2 text-sm transition-colors',
104
+ className,
105
+ active && 'shimmer'
106
+ )}
107
+ >
108
+ <BrainIcon className="aui-reasoning-trigger-icon size-4 shrink-0" />
109
+ <span className="aui-reasoning-trigger-label-wrapper relative inline-block leading-none">
110
+ <span>Reasoning</span>
111
+ {active ? (
112
+ <span
113
+ aria-hidden
114
+ className="aui-reasoning-trigger-shimmer shimmer pointer-events-none absolute inset-0 motion-reduce:animate-none"
115
+ >
116
+ Reasoning
117
+ </span>
118
+ ) : null}
119
+ </span>
120
+ <ChevronDownIcon
121
+ className={cn(
122
+ 'aui-reasoning-trigger-chevron mt-0.5 size-4 shrink-0',
123
+ 'transition-transform duration-(--animation-duration) ease-out',
124
+ 'group-data-[state=closed]/trigger:-rotate-90',
125
+ 'group-data-[state=open]/trigger:rotate-0'
126
+ )}
127
+ />
128
+ </CollapsibleTrigger>
129
+ )
130
+
131
+ /**
132
+ * Collapsible content wrapper that handles height expand/collapse animation.
133
+ * Animation: Height animates up (collapse) and down (expand).
134
+ * Also provides group context for child animations via data-state attributes.
135
+ */
136
+ const ReasoningContent: FC<
137
+ PropsWithChildren<{
138
+ className?: string
139
+ 'aria-busy'?: boolean
140
+ }>
141
+ > = ({ className, children, 'aria-busy': ariaBusy }) => (
142
+ <CollapsibleContent
143
+ className={cn(
144
+ 'aui-reasoning-content text-muted-foreground relative overflow-hidden text-sm outline-none',
145
+ 'group/collapsible-content ease-out',
146
+ 'data-[state=closed]:animate-collapsible-up',
147
+ 'data-[state=open]:animate-collapsible-down',
148
+ 'data-[state=closed]:fill-mode-forwards',
149
+ 'data-[state=closed]:pointer-events-none',
150
+ 'data-[state=open]:duration-(--animation-duration)',
151
+ 'data-[state=closed]:duration-(--animation-duration)',
152
+ className
153
+ )}
154
+ aria-busy={ariaBusy}
155
+ >
156
+ {children}
157
+ <GradientFade />
158
+ </CollapsibleContent>
159
+ )
160
+
161
+ ReasoningContent.displayName = 'ReasoningContent'
162
+
163
+ /**
164
+ * Text content wrapper that animates the reasoning text visibility.
165
+ * Animation: Slides in from top + fades in when opening, reverses when closing.
166
+ * Reacts to parent ReasoningContent's data-state via Radix group selectors.
167
+ */
168
+ const ReasoningText: FC<
169
+ PropsWithChildren<{
170
+ className?: string
171
+ }>
172
+ > = ({ className, children }) => (
173
+ <div
174
+ className={cn(
175
+ 'aui-reasoning-text relative z-0 space-y-4 pt-4 pl-6 leading-relaxed',
176
+ 'transform-gpu transition-[transform,opacity]',
177
+ 'group-data-[state=open]/collapsible-content:animate-in',
178
+ 'group-data-[state=closed]/collapsible-content:animate-out',
179
+ 'group-data-[state=open]/collapsible-content:fade-in-0',
180
+ 'group-data-[state=closed]/collapsible-content:fade-out-0',
181
+ 'group-data-[state=open]/collapsible-content:slide-in-from-top-4',
182
+ 'group-data-[state=closed]/collapsible-content:slide-out-to-top-4',
183
+ 'group-data-[state=open]/collapsible-content:duration-(--animation-duration)',
184
+ 'group-data-[state=closed]/collapsible-content:duration-(--animation-duration)',
185
+ '[&_p]:-mb-2',
186
+ className
187
+ )}
188
+ >
189
+ {children}
190
+ </div>
191
+ )
192
+
193
+ ReasoningText.displayName = 'ReasoningText'
194
+
195
+ /**
196
+ * Renders a single reasoning part's text with markdown support.
197
+ * Consecutive reasoning parts are automatically grouped by ReasoningGroup.
198
+ *
199
+ * Pass Reasoning to MessagePrimitive.Parts in thread.tsx
200
+ *
201
+ * @example:
202
+ * ```tsx
203
+ * <MessagePrimitive.Parts
204
+ * components={{
205
+ * Reasoning: Reasoning,
206
+ * ReasoningGroup: ReasoningGroup,
207
+ * }}
208
+ * />
209
+ * ```
210
+ */
211
+ const ReasoningImpl: ReasoningMessagePartComponent = () => <MarkdownText />
212
+
213
+ /**
214
+ * Collapsible wrapper that groups consecutive reasoning parts together.
215
+ * Includes scroll lock to prevent page jumps during collapse animation.
216
+ *
217
+ * Pass ReasoningGroup to MessagePrimitive.Parts in thread.tsx
218
+ *
219
+ * @example:
220
+ * ```tsx
221
+ * <MessagePrimitive.Parts
222
+ * components={{
223
+ * Reasoning: Reasoning,
224
+ * ReasoningGroup: ReasoningGroup,
225
+ * }}
226
+ * />
227
+ * ```
228
+ */
229
+ const ReasoningGroupImpl: ReasoningGroupComponent = ({
230
+ children,
231
+ startIndex,
232
+ endIndex,
233
+ }) => {
234
+ /**
235
+ * Detects if reasoning is currently streaming within this group's range.
236
+ */
237
+ const isReasoningStreaming = useAssistantState(({ message }) => {
238
+ if (message.status?.type !== 'running') return false
239
+ const lastIndex = message.parts.length - 1
240
+ if (lastIndex < 0) return false
241
+ const lastType = message.parts[lastIndex]?.type
242
+ if (lastType !== 'reasoning') return false
243
+ return lastIndex >= startIndex && lastIndex <= endIndex
244
+ })
245
+
246
+ return (
247
+ <ReasoningRoot>
248
+ <ReasoningTrigger active={isReasoningStreaming} />
249
+
250
+ <ReasoningContent aria-busy={isReasoningStreaming}>
251
+ <ReasoningText>{children}</ReasoningText>
252
+ </ReasoningContent>
253
+ </ReasoningRoot>
254
+ )
255
+ }
256
+
257
+ export const Reasoning = memo(ReasoningImpl)
258
+ Reasoning.displayName = 'Reasoning'
259
+
260
+ export const ReasoningGroup = memo(ReasoningGroupImpl)
261
+ ReasoningGroup.displayName = 'ReasoningGroup'
@@ -0,0 +1,97 @@
1
+ import type { FC } from 'react'
2
+ import {
3
+ ThreadListItemPrimitive,
4
+ ThreadListPrimitive,
5
+ useAssistantState,
6
+ } from '@assistant-ui/react'
7
+ import { ArchiveIcon, PlusIcon } from 'lucide-react'
8
+
9
+ import { Button } from '@/components/ui/button'
10
+ import { TooltipIconButton } from '@/components/assistant-ui/tooltip-icon-button'
11
+ import { Skeleton } from '@/components/ui/skeleton'
12
+
13
+ // NOTE: These components are currently not used but they will be
14
+ // when we add support for thread / chat history
15
+ export const ThreadList: FC = () => {
16
+ return (
17
+ <ThreadListPrimitive.Root className="aui-root aui-thread-list-root flex flex-col items-stretch gap-1.5">
18
+ <ThreadListNew />
19
+ <ThreadListItems />
20
+ </ThreadListPrimitive.Root>
21
+ )
22
+ }
23
+
24
+ const ThreadListNew: FC = () => {
25
+ return (
26
+ <ThreadListPrimitive.New asChild>
27
+ <Button
28
+ className="aui-thread-list-new hover:bg-muted data-active:bg-muted flex items-center justify-start gap-1 rounded-lg px-2.5 py-2 text-start"
29
+ variant="ghost"
30
+ >
31
+ <PlusIcon />
32
+ New Thread
33
+ </Button>
34
+ </ThreadListPrimitive.New>
35
+ )
36
+ }
37
+
38
+ const ThreadListItems: FC = () => {
39
+ const isLoading = useAssistantState(({ threads }) => threads.isLoading)
40
+
41
+ if (isLoading) {
42
+ return <ThreadListSkeleton />
43
+ }
44
+
45
+ return <ThreadListPrimitive.Items components={{ ThreadListItem }} />
46
+ }
47
+
48
+ const ThreadListSkeleton: FC = () => {
49
+ return (
50
+ <>
51
+ {Array.from({ length: 5 }, (_, i) => (
52
+ <div
53
+ key={i}
54
+ role="status"
55
+ aria-label="Loading threads"
56
+ aria-live="polite"
57
+ className="aui-thread-list-skeleton-wrapper flex items-center gap-2 rounded-md px-3 py-2"
58
+ >
59
+ <Skeleton className="aui-thread-list-skeleton h-[22px] grow" />
60
+ </div>
61
+ ))}
62
+ </>
63
+ )
64
+ }
65
+
66
+ const ThreadListItem: FC = () => {
67
+ return (
68
+ <ThreadListItemPrimitive.Root className="aui-thread-list-item hover:bg-muted focus-visible:bg-muted focus-visible:ring-ring data-active:bg-muted flex items-center gap-2 rounded-lg transition-all focus-visible:ring-2 focus-visible:outline-none">
69
+ <ThreadListItemPrimitive.Trigger className="aui-thread-list-item-trigger grow px-3 py-2 text-start">
70
+ <ThreadListItemTitle />
71
+ </ThreadListItemPrimitive.Trigger>
72
+ <ThreadListItemArchive />
73
+ </ThreadListItemPrimitive.Root>
74
+ )
75
+ }
76
+
77
+ const ThreadListItemTitle: FC = () => {
78
+ return (
79
+ <span className="aui-thread-list-item-title text-sm">
80
+ <ThreadListItemPrimitive.Title fallback="New Chat" />
81
+ </span>
82
+ )
83
+ }
84
+
85
+ const ThreadListItemArchive: FC = () => {
86
+ return (
87
+ <ThreadListItemPrimitive.Archive asChild>
88
+ <TooltipIconButton
89
+ className="aui-thread-list-item-archive text-foreground hover:text-primary mr-3 ml-auto size-4 p-0"
90
+ variant="ghost"
91
+ tooltip="Archive thread"
92
+ >
93
+ <ArchiveIcon />
94
+ </TooltipIconButton>
95
+ </ThreadListItemPrimitive.Archive>
96
+ )
97
+ }