@gram-ai/elements 1.23.0 → 1.24.0
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/README.md +61 -0
- package/dist/components/Chat/stories/MessageFeedback.stories.d.ts +28 -0
- package/dist/components/Chat/stories/Thread.stories.d.ts +15 -0
- package/dist/components/Replay.d.ts +11 -0
- package/dist/components/Replay.stories.d.ts +34 -0
- package/dist/components/assistant-ui/follow-on-suggestions.d.ts +6 -0
- package/dist/components/assistant-ui/message-feedback.d.ts +9 -0
- package/dist/contexts/ReplayContext.d.ts +6 -0
- package/dist/elements.cjs +1 -1
- package/dist/elements.css +1 -1
- package/dist/elements.js +10 -8
- package/dist/hooks/useFollowOnSuggestions.d.ts +14 -0
- package/dist/hooks/useMCPTools.d.ts +4 -3
- package/dist/hooks/useRecordCassette.d.ts +32 -0
- package/dist/{index-D-QXb5EF.js → index-B_7BIEfu.js} +11591 -10174
- package/dist/index-B_7BIEfu.js.map +1 -0
- package/dist/index-Bps9R2k6.cjs +178 -0
- package/dist/index-Bps9R2k6.cjs.map +1 -0
- package/dist/{index-vM3v0unX.js → index-CtyV0c-T.js} +5477 -5450
- package/dist/index-CtyV0c-T.js.map +1 -0
- package/dist/{index-Co05S1C8.cjs → index-iUSSoKFz.cjs} +12 -12
- package/dist/index-iUSSoKFz.cjs.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/lib/cassette.d.ts +46 -0
- package/dist/plugins.cjs +1 -1
- package/dist/plugins.js +1 -1
- package/dist/{profiler-Dshm-O8k.cjs → profiler-B6MySCOn.cjs} +2 -2
- package/dist/{profiler-Dshm-O8k.cjs.map → profiler-B6MySCOn.cjs.map} +1 -1
- package/dist/{profiler-D8-vgPGn.js → profiler-BWBC_MF5.js} +2 -2
- package/dist/{profiler-D8-vgPGn.js.map → profiler-BWBC_MF5.js.map} +1 -1
- package/dist/{startRecording-DnWeZRhl.js → startRecording-52IGlVhJ.js} +2 -2
- package/dist/{startRecording-DnWeZRhl.js.map → startRecording-52IGlVhJ.js.map} +1 -1
- package/dist/{startRecording-2p7-xVUh.cjs → startRecording-9LKBFNDH.cjs} +2 -2
- package/dist/{startRecording-2p7-xVUh.cjs.map → startRecording-9LKBFNDH.cjs.map} +1 -1
- package/dist/types/index.d.ts +37 -0
- package/package.json +5 -4
- package/src/components/Chat/stories/MessageFeedback.stories.tsx +169 -0
- package/src/components/Chat/stories/Thread.stories.tsx +78 -0
- package/src/components/Replay.stories.tsx +326 -0
- package/src/components/Replay.tsx +241 -0
- package/src/components/assistant-ui/follow-on-suggestions.tsx +122 -0
- package/src/components/assistant-ui/markdown-text.tsx +6 -7
- package/src/components/assistant-ui/message-feedback.tsx +177 -0
- package/src/components/assistant-ui/thread.tsx +319 -80
- package/src/components/assistant-ui/tool-group.tsx +27 -16
- package/src/components/ui/tool-ui.tsx +59 -17
- package/src/contexts/ElementsProvider.tsx +7 -2
- package/src/contexts/ReplayContext.ts +7 -0
- package/src/global.css +14 -8
- package/src/hooks/useAuth.ts +3 -0
- package/src/hooks/useFollowOnSuggestions.ts +237 -0
- package/src/hooks/useMCPTools.ts +32 -12
- package/src/hooks/useRecordCassette.ts +79 -0
- package/src/index.ts +10 -0
- package/src/lib/cassette.ts +260 -0
- package/src/plugins/chart/component.tsx +48 -11
- package/src/plugins/components/PluginLoadingState.tsx +2 -2
- package/src/types/index.ts +40 -0
- package/src/vite-env.d.ts +1 -0
- package/dist/index-CVYyyxfm.cjs +0 -147
- package/dist/index-CVYyyxfm.cjs.map +0 -1
- package/dist/index-Co05S1C8.cjs.map +0 -1
- package/dist/index-D-QXb5EF.js.map +0 -1
- package/dist/index-vM3v0unX.js.map +0 -1
|
@@ -4,7 +4,9 @@ import {
|
|
|
4
4
|
CheckIcon,
|
|
5
5
|
ChevronLeftIcon,
|
|
6
6
|
ChevronRightIcon,
|
|
7
|
+
CircleIcon,
|
|
7
8
|
CopyIcon,
|
|
9
|
+
DownloadIcon,
|
|
8
10
|
PencilIcon,
|
|
9
11
|
Settings2,
|
|
10
12
|
Square,
|
|
@@ -18,11 +20,20 @@ import {
|
|
|
18
20
|
ImageMessagePartProps,
|
|
19
21
|
MessagePrimitive,
|
|
20
22
|
ThreadPrimitive,
|
|
23
|
+
useAssistantState,
|
|
21
24
|
} from '@assistant-ui/react'
|
|
22
25
|
|
|
23
26
|
import { LazyMotion, MotionConfig, domAnimation } from 'motion/react'
|
|
24
27
|
import * as m from 'motion/react-m'
|
|
25
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
createContext,
|
|
30
|
+
useContext,
|
|
31
|
+
useEffect,
|
|
32
|
+
useMemo,
|
|
33
|
+
useRef,
|
|
34
|
+
useState,
|
|
35
|
+
type FC,
|
|
36
|
+
} from 'react'
|
|
26
37
|
import { AnimatePresence } from 'motion/react'
|
|
27
38
|
|
|
28
39
|
import {
|
|
@@ -30,7 +41,9 @@ import {
|
|
|
30
41
|
ComposerAttachments,
|
|
31
42
|
UserMessageAttachments,
|
|
32
43
|
} from '@/components/assistant-ui/attachment'
|
|
44
|
+
import { FollowOnSuggestions } from '@/components/assistant-ui/follow-on-suggestions'
|
|
33
45
|
import { MarkdownText } from '@/components/assistant-ui/markdown-text'
|
|
46
|
+
import { MessageFeedback } from '@/components/assistant-ui/message-feedback'
|
|
34
47
|
import { Reasoning, ReasoningGroup } from '@/components/assistant-ui/reasoning'
|
|
35
48
|
import { ToolFallback } from '@/components/assistant-ui/tool-fallback'
|
|
36
49
|
import { ToolMentionAutocomplete } from '@/components/assistant-ui/tool-mention-autocomplete'
|
|
@@ -46,6 +59,8 @@ import { useThemeProps } from '@/hooks/useThemeProps'
|
|
|
46
59
|
import { EASE_OUT_QUINT } from '@/lib/easing'
|
|
47
60
|
import { MODELS } from '@/lib/models'
|
|
48
61
|
import { cn } from '@/lib/utils'
|
|
62
|
+
import { useRecordCassette } from '@/hooks/useRecordCassette'
|
|
63
|
+
import { useReplayContext } from '@/contexts/ReplayContext'
|
|
49
64
|
import { ConnectionStatusIndicatorSafe } from './connection-status-indicator'
|
|
50
65
|
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover'
|
|
51
66
|
import {
|
|
@@ -56,6 +71,23 @@ import {
|
|
|
56
71
|
} from '../ui/tooltip'
|
|
57
72
|
import { ToolGroup } from './tool-group'
|
|
58
73
|
|
|
74
|
+
// Context for chat resolution state
|
|
75
|
+
const ChatResolutionContext = createContext<{
|
|
76
|
+
isResolved: boolean
|
|
77
|
+
feedbackHidden: boolean
|
|
78
|
+
setResolved: () => void
|
|
79
|
+
setUnresolved: () => void
|
|
80
|
+
resetFeedbackHidden: () => void
|
|
81
|
+
}>({
|
|
82
|
+
isResolved: false,
|
|
83
|
+
feedbackHidden: false,
|
|
84
|
+
setResolved: () => {},
|
|
85
|
+
setUnresolved: () => {},
|
|
86
|
+
resetFeedbackHidden: () => {},
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
const useChatResolution = () => useContext(ChatResolutionContext)
|
|
90
|
+
|
|
59
91
|
const StaticSessionWarning = () => (
|
|
60
92
|
<div className="m-2 rounded-md border border-amber-500 bg-amber-100 px-4 py-3 text-sm text-amber-800 dark:border-amber-600 dark:bg-amber-900/30 dark:text-amber-200">
|
|
61
93
|
<strong>Warning:</strong> You are using a static session token in the
|
|
@@ -81,52 +113,89 @@ export const Thread: FC<ThreadProps> = ({ className }) => {
|
|
|
81
113
|
const { config } = useElements()
|
|
82
114
|
const components = config.components ?? {}
|
|
83
115
|
const showStaticSessionWarning = config.api && 'sessionToken' in config.api
|
|
116
|
+
const showFeedback = config.thread?.experimental_showFeedback ?? false
|
|
117
|
+
const [isResolved, setIsResolved] = useState(false)
|
|
118
|
+
const [feedbackHidden, setFeedbackHidden] = useState(false)
|
|
119
|
+
|
|
120
|
+
const setResolved = () => setIsResolved(true)
|
|
121
|
+
const setUnresolved = () => {
|
|
122
|
+
setIsResolved(false)
|
|
123
|
+
setFeedbackHidden(true)
|
|
124
|
+
}
|
|
125
|
+
const resetFeedbackHidden = () => setFeedbackHidden(false)
|
|
84
126
|
|
|
85
127
|
return (
|
|
86
|
-
<
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
128
|
+
<ChatResolutionContext.Provider
|
|
129
|
+
value={{
|
|
130
|
+
isResolved: showFeedback && isResolved,
|
|
131
|
+
feedbackHidden,
|
|
132
|
+
setResolved,
|
|
133
|
+
setUnresolved,
|
|
134
|
+
resetFeedbackHidden,
|
|
135
|
+
}}
|
|
136
|
+
>
|
|
137
|
+
<LazyMotion features={domAnimation}>
|
|
138
|
+
<MotionConfig reducedMotion="user">
|
|
139
|
+
<ThreadPrimitive.Root
|
|
97
140
|
className={cn(
|
|
98
|
-
'aui-thread-
|
|
99
|
-
|
|
141
|
+
'aui-root aui-thread-root bg-background @container relative flex h-full flex-col',
|
|
142
|
+
themeProps.className,
|
|
143
|
+
className
|
|
100
144
|
)}
|
|
101
145
|
>
|
|
102
|
-
<
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
146
|
+
<ConnectionStatusIndicatorSafe />
|
|
147
|
+
<ThreadPrimitive.Viewport
|
|
148
|
+
className={cn(
|
|
149
|
+
'aui-thread-viewport relative mx-auto flex w-full flex-1 flex-col overflow-x-auto overflow-y-scroll pb-0!',
|
|
150
|
+
d('p-lg')
|
|
151
|
+
)}
|
|
152
|
+
>
|
|
153
|
+
<ThreadPrimitive.If empty>
|
|
154
|
+
{components.ThreadWelcome ? (
|
|
155
|
+
<components.ThreadWelcome />
|
|
156
|
+
) : (
|
|
157
|
+
<ThreadWelcome />
|
|
158
|
+
)}
|
|
159
|
+
</ThreadPrimitive.If>
|
|
160
|
+
|
|
161
|
+
{showStaticSessionWarning && <StaticSessionWarning />}
|
|
162
|
+
|
|
163
|
+
<ThreadPrimitive.Messages
|
|
164
|
+
components={{
|
|
165
|
+
UserMessage: components.UserMessage ?? UserMessage,
|
|
166
|
+
EditComposer: components.EditComposer ?? EditComposer,
|
|
167
|
+
AssistantMessage:
|
|
168
|
+
components.AssistantMessage ?? AssistantMessage,
|
|
169
|
+
}}
|
|
170
|
+
/>
|
|
171
|
+
|
|
172
|
+
<ThreadPrimitive.If empty={false} running={false}>
|
|
173
|
+
<FollowOnSuggestions />
|
|
174
|
+
</ThreadPrimitive.If>
|
|
175
|
+
|
|
176
|
+
<ThreadPrimitive.If empty={false}>
|
|
177
|
+
<div className="aui-thread-viewport-spacer min-h-8 grow" />
|
|
178
|
+
</ThreadPrimitive.If>
|
|
179
|
+
|
|
180
|
+
<Composer showFeedback={showFeedback} />
|
|
181
|
+
</ThreadPrimitive.Viewport>
|
|
182
|
+
|
|
183
|
+
{/* Resolution overlay - subtle readonly effect */}
|
|
184
|
+
<AnimatePresence>
|
|
185
|
+
{showFeedback && isResolved && (
|
|
186
|
+
<m.div
|
|
187
|
+
className="bg-background/40 pointer-events-none absolute inset-0 z-50"
|
|
188
|
+
initial={{ opacity: 0 }}
|
|
189
|
+
animate={{ opacity: 1 }}
|
|
190
|
+
exit={{ opacity: 0 }}
|
|
191
|
+
transition={{ duration: 0.3, ease: EASE_OUT_QUINT }}
|
|
192
|
+
/>
|
|
107
193
|
)}
|
|
108
|
-
</
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
components={{
|
|
114
|
-
UserMessage: components.UserMessage ?? UserMessage,
|
|
115
|
-
EditComposer: components.EditComposer ?? EditComposer,
|
|
116
|
-
AssistantMessage:
|
|
117
|
-
components.AssistantMessage ?? AssistantMessage,
|
|
118
|
-
}}
|
|
119
|
-
/>
|
|
120
|
-
|
|
121
|
-
<ThreadPrimitive.If empty={false}>
|
|
122
|
-
<div className="aui-thread-viewport-spacer min-h-8 grow" />
|
|
123
|
-
</ThreadPrimitive.If>
|
|
124
|
-
|
|
125
|
-
<Composer />
|
|
126
|
-
</ThreadPrimitive.Viewport>
|
|
127
|
-
</ThreadPrimitive.Root>
|
|
128
|
-
</MotionConfig>
|
|
129
|
-
</LazyMotion>
|
|
194
|
+
</AnimatePresence>
|
|
195
|
+
</ThreadPrimitive.Root>
|
|
196
|
+
</MotionConfig>
|
|
197
|
+
</LazyMotion>
|
|
198
|
+
</ChatResolutionContext.Provider>
|
|
130
199
|
)
|
|
131
200
|
}
|
|
132
201
|
|
|
@@ -357,10 +426,57 @@ const ComposerToolMentions: FC<{
|
|
|
357
426
|
)
|
|
358
427
|
}
|
|
359
428
|
|
|
360
|
-
|
|
429
|
+
// Resets feedbackHidden when a new message starts generating
|
|
430
|
+
const FeedbackHiddenResetter: FC = () => {
|
|
431
|
+
const { resetFeedbackHidden } = useChatResolution()
|
|
432
|
+
|
|
433
|
+
useEffect(() => {
|
|
434
|
+
resetFeedbackHidden()
|
|
435
|
+
}, [resetFeedbackHidden])
|
|
436
|
+
|
|
437
|
+
return null
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const ComposerFeedback: FC = () => {
|
|
441
|
+
const { isResolved, feedbackHidden, setResolved } = useChatResolution()
|
|
442
|
+
|
|
443
|
+
return (
|
|
444
|
+
<ThreadPrimitive.If empty={false}>
|
|
445
|
+
{/* Reset feedbackHidden when a new message starts generating */}
|
|
446
|
+
<ThreadPrimitive.If running>
|
|
447
|
+
<FeedbackHiddenResetter />
|
|
448
|
+
</ThreadPrimitive.If>
|
|
449
|
+
<ThreadPrimitive.If running={false}>
|
|
450
|
+
<AnimatePresence>
|
|
451
|
+
{!isResolved && !feedbackHidden && (
|
|
452
|
+
<m.div
|
|
453
|
+
initial={{ opacity: 0, y: 10 }}
|
|
454
|
+
animate={{ opacity: 1, y: 0 }}
|
|
455
|
+
exit={{ opacity: 0, y: 10 }}
|
|
456
|
+
transition={{ duration: 0.2, ease: EASE_OUT_QUINT }}
|
|
457
|
+
className="mb-3"
|
|
458
|
+
>
|
|
459
|
+
<MessageFeedback className="mx-auto" onResolved={setResolved} />
|
|
460
|
+
</m.div>
|
|
461
|
+
)}
|
|
462
|
+
</AnimatePresence>
|
|
463
|
+
</ThreadPrimitive.If>
|
|
464
|
+
</ThreadPrimitive.If>
|
|
465
|
+
)
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
interface ComposerProps {
|
|
469
|
+
showFeedback?: boolean
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const Composer: FC<ComposerProps> = ({ showFeedback = false }) => {
|
|
361
473
|
const { config, mcpTools } = useElements()
|
|
474
|
+
const { isResolved, setUnresolved } = useChatResolution()
|
|
362
475
|
const r = useRadius()
|
|
363
476
|
const d = useDensity()
|
|
477
|
+
const replayCtx = useReplayContext()
|
|
478
|
+
|
|
479
|
+
const isReplay = replayCtx?.isReplay ?? false
|
|
364
480
|
const composerConfig = config.composer ?? {
|
|
365
481
|
placeholder: 'Send a message...',
|
|
366
482
|
attachments: true,
|
|
@@ -383,37 +499,61 @@ const Composer: FC = () => {
|
|
|
383
499
|
return (
|
|
384
500
|
<div
|
|
385
501
|
className={cn(
|
|
386
|
-
'aui-composer-wrapper bg-background sticky bottom-0 flex w-full flex-col overflow-visible',
|
|
502
|
+
'aui-composer-wrapper bg-background sticky bottom-0 z-[60] flex w-full flex-col overflow-visible',
|
|
387
503
|
d('gap-md'),
|
|
388
504
|
d('py-md'),
|
|
389
505
|
r('xl')
|
|
390
506
|
)}
|
|
391
507
|
>
|
|
508
|
+
{showFeedback && <ComposerFeedback />}
|
|
392
509
|
<ThreadScrollToBottom />
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
510
|
+
{showFeedback && isResolved ? (
|
|
511
|
+
<m.div
|
|
512
|
+
className="aui-composer-resolved border-input flex min-h-[118px] flex-col items-center justify-center gap-2 border-t px-1"
|
|
513
|
+
initial={{ opacity: 0 }}
|
|
514
|
+
animate={{ opacity: 1 }}
|
|
515
|
+
transition={{ duration: 0.2, ease: EASE_OUT_QUINT }}
|
|
516
|
+
>
|
|
517
|
+
<span className="text-muted-foreground text-sm">
|
|
518
|
+
This conversation has been resolved
|
|
519
|
+
</span>
|
|
520
|
+
<Button
|
|
521
|
+
variant="outline"
|
|
522
|
+
size="sm"
|
|
523
|
+
className="text-foreground"
|
|
524
|
+
onClick={setUnresolved}
|
|
525
|
+
>
|
|
526
|
+
Reopen conversation
|
|
527
|
+
</Button>
|
|
528
|
+
</m.div>
|
|
529
|
+
) : (
|
|
530
|
+
<ComposerPrimitive.Root
|
|
531
|
+
ref={composerRootRef}
|
|
406
532
|
className={cn(
|
|
407
|
-
'aui-composer-input
|
|
408
|
-
|
|
409
|
-
|
|
533
|
+
'aui-composer-root group/input-group border-input bg-background has-[textarea:focus-visible]:border-ring has-[textarea:focus-visible]:ring-ring/5 dark:bg-background relative flex min-h-[118px] w-full flex-col border px-1 pt-2 shadow-xs transition-[color,box-shadow] outline-none has-[textarea:focus-visible]:ring-1',
|
|
534
|
+
r('xl'),
|
|
535
|
+
isReplay && 'pointer-events-none opacity-50'
|
|
410
536
|
)}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
537
|
+
>
|
|
538
|
+
{composerConfig.attachments && <ComposerAttachments />}
|
|
539
|
+
|
|
540
|
+
{toolMentionsEnabled && <ComposerToolMentions tools={mcpTools} />}
|
|
541
|
+
|
|
542
|
+
<ComposerPrimitive.Input
|
|
543
|
+
placeholder={composerConfig.placeholder}
|
|
544
|
+
className={cn(
|
|
545
|
+
'aui-composer-input text-foreground placeholder:text-muted-foreground mb-1 max-h-32 w-full resize-none bg-transparent px-3.5 pt-1.5 pb-3 outline-none focus-visible:ring-0',
|
|
546
|
+
d('h-input'),
|
|
547
|
+
d('text-base')
|
|
548
|
+
)}
|
|
549
|
+
rows={1}
|
|
550
|
+
autoFocus={!isReplay}
|
|
551
|
+
disabled={isReplay}
|
|
552
|
+
aria-label="Message input"
|
|
553
|
+
/>
|
|
554
|
+
<ComposerAction />
|
|
555
|
+
</ComposerPrimitive.Root>
|
|
556
|
+
)}
|
|
417
557
|
</div>
|
|
418
558
|
)
|
|
419
559
|
}
|
|
@@ -508,13 +648,90 @@ const ComposerModelPicker: FC = () => {
|
|
|
508
648
|
)
|
|
509
649
|
}
|
|
510
650
|
|
|
651
|
+
const CASSETTE_RECORDING_ENABLED =
|
|
652
|
+
import.meta.env.VITE_ELEMENTS_ENABLE_CASSETTE_RECORDING === 'true'
|
|
653
|
+
|
|
654
|
+
const ComposerCassetteRecorder: FC = () => {
|
|
655
|
+
const [popoverOpen, setPopoverOpen] = useState(false)
|
|
656
|
+
const [tooltipOpen, setTooltipOpen] = useState(false)
|
|
657
|
+
const { isRecording, startRecording, stopRecording, download } =
|
|
658
|
+
useRecordCassette()
|
|
659
|
+
|
|
660
|
+
useEffect(() => {
|
|
661
|
+
if (popoverOpen) setTooltipOpen(false)
|
|
662
|
+
}, [popoverOpen])
|
|
663
|
+
|
|
664
|
+
return (
|
|
665
|
+
<TooltipProvider>
|
|
666
|
+
<Tooltip open={tooltipOpen && !popoverOpen} onOpenChange={setTooltipOpen}>
|
|
667
|
+
<Popover open={popoverOpen} onOpenChange={setPopoverOpen}>
|
|
668
|
+
<TooltipTrigger asChild>
|
|
669
|
+
<PopoverTrigger asChild>
|
|
670
|
+
<Button
|
|
671
|
+
variant="ghost"
|
|
672
|
+
size="icon"
|
|
673
|
+
data-state={popoverOpen ? 'open' : 'closed'}
|
|
674
|
+
className={cn(
|
|
675
|
+
'aui-composer-cassette-recorder data-[state=open]:bg-muted-foreground/15 dark:border-muted-foreground/15 dark:hover:bg-muted-foreground/30 flex w-fit items-center gap-2 rounded-full px-2.5 py-1 text-xs font-semibold',
|
|
676
|
+
isRecording && 'text-red-500'
|
|
677
|
+
)}
|
|
678
|
+
aria-label="Cassette Recorder"
|
|
679
|
+
>
|
|
680
|
+
<CircleIcon
|
|
681
|
+
className={cn(
|
|
682
|
+
'size-5 stroke-[1.5px]',
|
|
683
|
+
isRecording && 'animate-pulse fill-red-500 text-red-500'
|
|
684
|
+
)}
|
|
685
|
+
/>
|
|
686
|
+
</Button>
|
|
687
|
+
</PopoverTrigger>
|
|
688
|
+
</TooltipTrigger>
|
|
689
|
+
<PopoverContent side="top" align="start" className="w-64 p-3">
|
|
690
|
+
<div className="flex flex-col gap-3">
|
|
691
|
+
<div className="text-sm font-medium">Cassette Recorder</div>
|
|
692
|
+
{!isRecording ? (
|
|
693
|
+
<Button
|
|
694
|
+
size="sm"
|
|
695
|
+
variant="outline"
|
|
696
|
+
className="w-full justify-start gap-2"
|
|
697
|
+
onClick={startRecording}
|
|
698
|
+
>
|
|
699
|
+
<CircleIcon className="size-3 fill-red-500 text-red-500" />
|
|
700
|
+
Start Recording
|
|
701
|
+
</Button>
|
|
702
|
+
) : (
|
|
703
|
+
<Button
|
|
704
|
+
size="sm"
|
|
705
|
+
variant="outline"
|
|
706
|
+
className="w-full justify-start gap-2"
|
|
707
|
+
onClick={() => {
|
|
708
|
+
stopRecording()
|
|
709
|
+
download()
|
|
710
|
+
setPopoverOpen(false)
|
|
711
|
+
}}
|
|
712
|
+
>
|
|
713
|
+
<DownloadIcon className="size-3" />
|
|
714
|
+
Stop & Download
|
|
715
|
+
</Button>
|
|
716
|
+
)}
|
|
717
|
+
</div>
|
|
718
|
+
</PopoverContent>
|
|
719
|
+
</Popover>
|
|
720
|
+
<TooltipContent side="bottom" align="start">
|
|
721
|
+
{isRecording ? 'Recording…' : 'Cassette Recorder'}
|
|
722
|
+
</TooltipContent>
|
|
723
|
+
</Tooltip>
|
|
724
|
+
</TooltipProvider>
|
|
725
|
+
)
|
|
726
|
+
}
|
|
727
|
+
|
|
511
728
|
const ComposerAction: FC = () => {
|
|
512
729
|
const { config } = useElements()
|
|
513
730
|
const r = useRadius()
|
|
514
731
|
const composerConfig = config.composer ?? { attachments: true }
|
|
515
732
|
return (
|
|
516
733
|
<div className="aui-composer-action-wrapper relative mx-1 mt-2 mb-2 flex items-center justify-between">
|
|
517
|
-
<div className="aui-composer-action-wrapper-inner flex items-center">
|
|
734
|
+
<div className="aui-composer-action-wrapper-inner text-muted-foreground flex items-center">
|
|
518
735
|
{composerConfig.attachments ? (
|
|
519
736
|
<ComposerAddAttachment />
|
|
520
737
|
) : (
|
|
@@ -524,6 +741,8 @@ const ComposerAction: FC = () => {
|
|
|
524
741
|
{config.model?.showModelPicker && !config.languageModel && (
|
|
525
742
|
<ComposerModelPicker />
|
|
526
743
|
)}
|
|
744
|
+
|
|
745
|
+
{CASSETTE_RECORDING_ENABLED && <ComposerCassetteRecorder />}
|
|
527
746
|
</div>
|
|
528
747
|
|
|
529
748
|
<ThreadPrimitive.If running={false}>
|
|
@@ -572,10 +791,41 @@ const MessageError: FC = () => {
|
|
|
572
791
|
)
|
|
573
792
|
}
|
|
574
793
|
|
|
794
|
+
/**
|
|
795
|
+
* Shows the pulsing dot indicator when the message is still running but the
|
|
796
|
+
* last rendered part is a tool call (not text). Without this, there's no
|
|
797
|
+
* visual feedback that the model is still working after a tool call.
|
|
798
|
+
*/
|
|
799
|
+
const ToolCallStreamingIndicator: FC = () => {
|
|
800
|
+
const show = useAssistantState(({ message }) => {
|
|
801
|
+
if (message.status?.type !== 'running') return false
|
|
802
|
+
const lastPart = message.parts[message.parts.length - 1]
|
|
803
|
+
return lastPart?.type === 'tool-call'
|
|
804
|
+
})
|
|
805
|
+
if (!show) return null
|
|
806
|
+
return <div className="aui-md mt-2" data-status="running" />
|
|
807
|
+
}
|
|
808
|
+
|
|
575
809
|
const AssistantMessage: FC = () => {
|
|
576
810
|
const { config } = useElements()
|
|
577
811
|
const toolsConfig = config.tools ?? {}
|
|
578
812
|
const components = config.components ?? {}
|
|
813
|
+
|
|
814
|
+
const partsComponents = useMemo(
|
|
815
|
+
() => ({
|
|
816
|
+
Text: components.Text ?? MarkdownText,
|
|
817
|
+
Image: components.Image ?? Image,
|
|
818
|
+
tools: {
|
|
819
|
+
by_name: toolsConfig.components,
|
|
820
|
+
Fallback: components.ToolFallback ?? ToolFallback,
|
|
821
|
+
},
|
|
822
|
+
Reasoning: components.Reasoning ?? Reasoning,
|
|
823
|
+
ReasoningGroup: components.ReasoningGroup ?? ReasoningGroup,
|
|
824
|
+
ToolGroup: components.ToolGroup ?? ToolGroup,
|
|
825
|
+
}),
|
|
826
|
+
[components, toolsConfig.components]
|
|
827
|
+
)
|
|
828
|
+
|
|
579
829
|
return (
|
|
580
830
|
<MessagePrimitive.Root asChild>
|
|
581
831
|
<div
|
|
@@ -583,23 +833,12 @@ const AssistantMessage: FC = () => {
|
|
|
583
833
|
data-role="assistant"
|
|
584
834
|
>
|
|
585
835
|
<div className="aui-assistant-message-content text-foreground mx-2 leading-7 wrap-break-word">
|
|
586
|
-
<MessagePrimitive.Parts
|
|
587
|
-
|
|
588
|
-
Text: components.Text ?? MarkdownText,
|
|
589
|
-
Image: components.Image ?? Image,
|
|
590
|
-
tools: {
|
|
591
|
-
by_name: toolsConfig.components,
|
|
592
|
-
Fallback: components.ToolFallback ?? ToolFallback,
|
|
593
|
-
},
|
|
594
|
-
Reasoning: components.Reasoning ?? Reasoning,
|
|
595
|
-
ReasoningGroup: components.ReasoningGroup ?? ReasoningGroup,
|
|
596
|
-
ToolGroup: components.ToolGroup ?? ToolGroup,
|
|
597
|
-
}}
|
|
598
|
-
/>
|
|
836
|
+
<MessagePrimitive.Parts components={partsComponents} />
|
|
837
|
+
<ToolCallStreamingIndicator />
|
|
599
838
|
<MessageError />
|
|
600
839
|
</div>
|
|
601
840
|
|
|
602
|
-
<div className="aui-assistant-message-footer mt-2 ml-2 flex">
|
|
841
|
+
<div className="aui-assistant-message-footer mt-2 ml-2 flex items-center gap-3">
|
|
603
842
|
{/* <BranchPicker /> */}
|
|
604
843
|
<AssistantActionBar />
|
|
605
844
|
</div>
|
|
@@ -7,34 +7,45 @@ import { ToolUIGroup } from '@/components/ui/tool-ui'
|
|
|
7
7
|
|
|
8
8
|
export const ToolGroup: FC<
|
|
9
9
|
PropsWithChildren<{ startIndex: number; endIndex: number }>
|
|
10
|
-
> = ({ children }) => {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
> = ({ children, startIndex, endIndex }) => {
|
|
11
|
+
// startIndex/endIndex are inclusive indices into message.parts.
|
|
12
|
+
// assistant-ui only groups consecutive tool-call parts, so every part
|
|
13
|
+
// in the range is a tool-call — the count is simply the range size.
|
|
14
|
+
const toolCount = endIndex - startIndex + 1
|
|
15
|
+
|
|
16
|
+
const firstToolName = useAssistantState(({ message }) => {
|
|
17
|
+
const part = message.parts[startIndex]
|
|
18
|
+
return part?.type === 'tool-call' ? part.toolName : undefined
|
|
19
|
+
})
|
|
20
|
+
const anyMessagePartsAreRunning = useAssistantState(({ message }) => {
|
|
21
|
+
for (let i = startIndex; i <= endIndex; i++) {
|
|
22
|
+
if (message.parts[i]?.status?.type === 'running') return true
|
|
23
|
+
}
|
|
24
|
+
return false
|
|
25
|
+
})
|
|
16
26
|
|
|
17
27
|
const { config } = useElements()
|
|
18
28
|
const defaultExpanded = config.tools?.expandToolGroupsByDefault ?? false
|
|
19
29
|
|
|
20
30
|
const groupTitle = useMemo(() => {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
31
|
+
if (toolCount === 0) return 'No tools called'
|
|
32
|
+
if (toolCount === 1) {
|
|
33
|
+
return firstToolName
|
|
34
|
+
? `Calling ${humanizeToolName(firstToolName)}...`
|
|
35
|
+
: 'Calling tool...'
|
|
36
|
+
}
|
|
26
37
|
return anyMessagePartsAreRunning
|
|
27
|
-
? `Calling ${
|
|
28
|
-
: `Executed ${
|
|
29
|
-
}, [
|
|
38
|
+
? `Calling ${toolCount} tools...`
|
|
39
|
+
: `Executed ${toolCount} tools`
|
|
40
|
+
}, [toolCount, firstToolName, anyMessagePartsAreRunning])
|
|
30
41
|
|
|
31
42
|
// If there's a custom component for the single tool, render children directly
|
|
32
|
-
if (config.tools?.components?.[
|
|
43
|
+
if (firstToolName && config.tools?.components?.[firstToolName]) {
|
|
33
44
|
return children
|
|
34
45
|
}
|
|
35
46
|
|
|
36
47
|
// For single tool calls, render without the group wrapper
|
|
37
|
-
if (
|
|
48
|
+
if (toolCount === 1) {
|
|
38
49
|
return (
|
|
39
50
|
<div className={cn('my-4 w-full max-w-xl')}>
|
|
40
51
|
<div className="border-border bg-card overflow-hidden rounded-lg border">
|
|
@@ -181,6 +181,22 @@ function CopyButton({ content }: { content: string }) {
|
|
|
181
181
|
* SyntaxHighlightedCode - Code block with shiki syntax highlighting
|
|
182
182
|
* -------------------------------------------------------------------------- */
|
|
183
183
|
|
|
184
|
+
/** Max characters to send through shiki — above this we skip highlighting. */
|
|
185
|
+
const SHIKI_CHAR_LIMIT = 8_000
|
|
186
|
+
/** Max lines shown in the collapsed preview. */
|
|
187
|
+
const PREVIEW_LINE_LIMIT = 50
|
|
188
|
+
|
|
189
|
+
function truncateToLines(text: string, maxLines: number) {
|
|
190
|
+
let pos = 0
|
|
191
|
+
for (let i = 0; i < maxLines; i++) {
|
|
192
|
+
const next = text.indexOf('\n', pos)
|
|
193
|
+
if (next === -1) return { text, truncated: false, totalLines: i + 1 }
|
|
194
|
+
pos = next + 1
|
|
195
|
+
}
|
|
196
|
+
const totalLines = text.split('\n').length
|
|
197
|
+
return { text: text.slice(0, pos), truncated: true, totalLines }
|
|
198
|
+
}
|
|
199
|
+
|
|
184
200
|
function SyntaxHighlightedCode({
|
|
185
201
|
text,
|
|
186
202
|
language,
|
|
@@ -191,10 +207,20 @@ function SyntaxHighlightedCode({
|
|
|
191
207
|
className?: string
|
|
192
208
|
}) {
|
|
193
209
|
const [highlightedCode, setHighlightedCode] = useState<string | null>(null)
|
|
210
|
+
const [expanded, setExpanded] = useState(false)
|
|
211
|
+
|
|
212
|
+
const preview = React.useMemo(
|
|
213
|
+
() => truncateToLines(text, PREVIEW_LINE_LIMIT),
|
|
214
|
+
[text]
|
|
215
|
+
)
|
|
216
|
+
const displayText = expanded ? text : preview.text
|
|
217
|
+
const canHighlight = displayText.length <= SHIKI_CHAR_LIMIT
|
|
194
218
|
|
|
195
219
|
useEffect(() => {
|
|
196
|
-
|
|
197
|
-
|
|
220
|
+
setHighlightedCode(null)
|
|
221
|
+
if (!language || !canHighlight) return
|
|
222
|
+
let cancelled = false
|
|
223
|
+
codeToHtml(displayText, {
|
|
198
224
|
lang: language,
|
|
199
225
|
theme: 'github-dark-default',
|
|
200
226
|
rootStyle: 'background-color: transparent;',
|
|
@@ -206,27 +232,43 @@ function SyntaxHighlightedCode({
|
|
|
206
232
|
},
|
|
207
233
|
},
|
|
208
234
|
],
|
|
209
|
-
}).then(
|
|
210
|
-
|
|
235
|
+
}).then((html) => {
|
|
236
|
+
if (!cancelled) setHighlightedCode(html)
|
|
237
|
+
})
|
|
238
|
+
return () => {
|
|
239
|
+
cancelled = true
|
|
240
|
+
}
|
|
241
|
+
}, [displayText, language, canHighlight])
|
|
242
|
+
|
|
243
|
+
const showMoreButton = preview.truncated && !expanded && (
|
|
244
|
+
<button
|
|
245
|
+
type="button"
|
|
246
|
+
onClick={() => setExpanded(true)}
|
|
247
|
+
className="w-full bg-slate-800/90 px-4 py-2 text-left text-xs text-slate-400 transition-colors hover:text-slate-200"
|
|
248
|
+
>
|
|
249
|
+
Show all {preview.totalLines} lines…
|
|
250
|
+
</button>
|
|
251
|
+
)
|
|
211
252
|
|
|
212
|
-
if (!highlightedCode) {
|
|
253
|
+
if (!canHighlight || !highlightedCode) {
|
|
213
254
|
return (
|
|
214
|
-
<
|
|
215
|
-
className=
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
>
|
|
220
|
-
{text}
|
|
221
|
-
</pre>
|
|
255
|
+
<div className={cn('w-full', className)}>
|
|
256
|
+
<pre className="max-h-[300px] w-full overflow-y-auto bg-slate-800/90 px-4 py-3 text-sm whitespace-pre-wrap text-slate-100">
|
|
257
|
+
{displayText}
|
|
258
|
+
</pre>
|
|
259
|
+
{showMoreButton}
|
|
260
|
+
</div>
|
|
222
261
|
)
|
|
223
262
|
}
|
|
224
263
|
|
|
225
264
|
return (
|
|
226
|
-
<div
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
265
|
+
<div className={cn('w-full', className)}>
|
|
266
|
+
<div
|
|
267
|
+
className="w-full bg-slate-800/90"
|
|
268
|
+
dangerouslySetInnerHTML={{ __html: highlightedCode }}
|
|
269
|
+
/>
|
|
270
|
+
{showMoreButton}
|
|
271
|
+
</div>
|
|
230
272
|
)
|
|
231
273
|
}
|
|
232
274
|
|