@gram-ai/elements 1.21.2 → 1.22.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/dist/components/Chat/stories/StyleIsolation.stories.d.ts +6 -0
- package/dist/components/Chat/stories/Theme.stories.d.ts +8 -0
- package/dist/components/Chat/stories/ToolMentions.stories.d.ts +19 -0
- package/dist/components/ChatHistory.d.ts +5 -0
- package/dist/components/ShadowRoot.d.ts +8 -0
- package/dist/components/assistant-ui/mentioned-tools-badges.d.ts +10 -0
- package/dist/components/assistant-ui/tool-mention-autocomplete.d.ts +12 -0
- package/dist/elements.cjs +1 -1
- package/dist/elements.css +1 -1
- package/dist/elements.js +1 -1
- package/dist/embedded.d.ts +0 -0
- package/dist/hooks/useGramThreadListAdapter.d.ts +7 -0
- package/dist/hooks/useMCPTools.d.ts +2 -1
- package/dist/hooks/useToolMentions.d.ts +17 -0
- package/dist/index-DSGCuihM.cjs +145 -0
- package/dist/index-DSGCuihM.cjs.map +1 -0
- package/dist/index-DiaPGyWF.js +38113 -0
- package/dist/index-DiaPGyWF.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/lib/tool-mentions.d.ts +20 -0
- package/dist/{profiler-DPH9zydw.cjs → profiler-Cr1UEa8B.cjs} +2 -2
- package/dist/{profiler-DPH9zydw.cjs.map → profiler-Cr1UEa8B.cjs.map} +1 -1
- package/dist/{profiler-D_HNXmyv.js → profiler-DL_h66-O.js} +2 -2
- package/dist/{profiler-D_HNXmyv.js.map → profiler-DL_h66-O.js.map} +1 -1
- package/dist/{startRecording-DOMzQAAr.js → startRecording-BW2zkd0-.js} +2 -2
- package/dist/{startRecording-DOMzQAAr.js.map → startRecording-BW2zkd0-.js.map} +1 -1
- package/dist/{startRecording-B97e4Mlu.cjs → startRecording-D8-ckYMS.cjs} +2 -2
- package/dist/{startRecording-B97e4Mlu.cjs.map → startRecording-D8-ckYMS.cjs.map} +1 -1
- package/dist/types/index.d.ts +20 -0
- package/package.json +13 -4
- package/src/components/Chat/index.tsx +4 -13
- package/src/components/Chat/stories/StyleIsolation.stories.tsx +41 -0
- package/src/components/Chat/stories/Theme.stories.tsx +74 -0
- package/src/components/Chat/stories/ToolApproval.stories.tsx +0 -1
- package/src/components/Chat/stories/ToolMentions.stories.tsx +62 -0
- package/src/components/ChatHistory.tsx +16 -0
- package/src/components/ShadowRoot.tsx +90 -0
- package/src/components/assistant-ui/markdown-text.tsx +0 -2
- package/src/components/assistant-ui/mentioned-tools-badges.tsx +105 -0
- package/src/components/assistant-ui/thread-list.tsx +1 -3
- package/src/components/assistant-ui/thread.tsx +106 -1
- package/src/components/assistant-ui/tool-mention-autocomplete.tsx +253 -0
- package/src/components/ui/tool-ui.tsx +1 -1
- package/src/contexts/ElementsProvider.tsx +67 -10
- package/src/embedded.ts +3 -0
- package/src/global.css +45 -0
- package/src/hooks/useGramThreadListAdapter.tsx +90 -9
- package/src/hooks/useMCPTools.ts +4 -1
- package/src/hooks/useToolMentions.ts +93 -0
- package/src/index.ts +2 -1
- package/src/lib/tool-mentions.ts +106 -0
- package/src/types/index.ts +23 -0
- package/src/vite-env.d.ts +5 -0
- package/dist/index-BVvrv2G3.cjs +0 -169
- package/dist/index-BVvrv2G3.cjs.map +0 -1
- package/dist/index-OU3wjArm.js +0 -54670
- package/dist/index-OU3wjArm.js.map +0 -1
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { FC } from 'react'
|
|
2
|
+
import { Wrench, X } from 'lucide-react'
|
|
3
|
+
import { AnimatePresence } from 'motion/react'
|
|
4
|
+
import * as m from 'motion/react-m'
|
|
5
|
+
|
|
6
|
+
import { cn } from '@/lib/utils'
|
|
7
|
+
import { useDensity } from '@/hooks/useDensity'
|
|
8
|
+
import { useRadius } from '@/hooks/useRadius'
|
|
9
|
+
import { EASE_OUT_QUINT } from '@/lib/easing'
|
|
10
|
+
import { MentionableTool } from '@/lib/tool-mentions'
|
|
11
|
+
|
|
12
|
+
export interface MentionedToolsBadgesProps {
|
|
13
|
+
mentionedToolIds: string[]
|
|
14
|
+
tools: MentionableTool[]
|
|
15
|
+
onRemove?: (toolId: string) => void
|
|
16
|
+
className?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const MentionedToolsBadges: FC<MentionedToolsBadgesProps> = ({
|
|
20
|
+
mentionedToolIds,
|
|
21
|
+
tools,
|
|
22
|
+
onRemove,
|
|
23
|
+
className,
|
|
24
|
+
}) => {
|
|
25
|
+
const d = useDensity()
|
|
26
|
+
const mentionedTools = tools.filter((tool) =>
|
|
27
|
+
mentionedToolIds.includes(tool.id)
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
if (mentionedTools.length === 0) {
|
|
31
|
+
return null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div
|
|
36
|
+
className={cn(
|
|
37
|
+
'aui-mentioned-tools-badges flex flex-wrap items-center gap-1',
|
|
38
|
+
d('px-sm'),
|
|
39
|
+
d('py-xs'),
|
|
40
|
+
className
|
|
41
|
+
)}
|
|
42
|
+
>
|
|
43
|
+
<span className="text-muted-foreground flex-shrink-0 text-xs">
|
|
44
|
+
Tools:
|
|
45
|
+
</span>
|
|
46
|
+
<AnimatePresence mode="popLayout">
|
|
47
|
+
{mentionedTools.map((tool) => (
|
|
48
|
+
<m.div
|
|
49
|
+
key={tool.id}
|
|
50
|
+
layout
|
|
51
|
+
initial={{ opacity: 0, scale: 0.8 }}
|
|
52
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
53
|
+
exit={{ opacity: 0, scale: 0.8 }}
|
|
54
|
+
transition={{ duration: 0.15, ease: EASE_OUT_QUINT }}
|
|
55
|
+
>
|
|
56
|
+
<ToolBadge
|
|
57
|
+
tool={tool}
|
|
58
|
+
onRemove={onRemove ? () => onRemove(tool.id) : undefined}
|
|
59
|
+
/>
|
|
60
|
+
</m.div>
|
|
61
|
+
))}
|
|
62
|
+
</AnimatePresence>
|
|
63
|
+
</div>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface ToolBadgeProps {
|
|
68
|
+
tool: MentionableTool
|
|
69
|
+
onRemove?: () => void
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const ToolBadge: FC<ToolBadgeProps> = ({ tool, onRemove }) => {
|
|
73
|
+
const d = useDensity()
|
|
74
|
+
const r = useRadius()
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<div
|
|
78
|
+
className={cn(
|
|
79
|
+
'aui-tool-badge bg-primary/10 text-primary inline-flex items-center gap-1',
|
|
80
|
+
r('md'),
|
|
81
|
+
d('px-sm'),
|
|
82
|
+
d('py-xs')
|
|
83
|
+
)}
|
|
84
|
+
>
|
|
85
|
+
<Wrench className="size-3 flex-shrink-0" />
|
|
86
|
+
<span className="text-xs font-medium">{tool.name}</span>
|
|
87
|
+
{onRemove && (
|
|
88
|
+
<button
|
|
89
|
+
type="button"
|
|
90
|
+
onClick={(e) => {
|
|
91
|
+
e.preventDefault()
|
|
92
|
+
e.stopPropagation()
|
|
93
|
+
onRemove()
|
|
94
|
+
}}
|
|
95
|
+
className="hover:opacity-70 focus:outline-none"
|
|
96
|
+
aria-label={`Remove ${tool.name}`}
|
|
97
|
+
>
|
|
98
|
+
<X className="size-3 flex-shrink-0" />
|
|
99
|
+
</button>
|
|
100
|
+
)}
|
|
101
|
+
</div>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export default MentionedToolsBadges
|
|
@@ -11,7 +11,6 @@ import { Skeleton } from '@/components/ui/skeleton'
|
|
|
11
11
|
import { useRadius } from '@/hooks/useRadius'
|
|
12
12
|
import { cn } from '@/lib/utils'
|
|
13
13
|
import { useDensity } from '@/hooks/useDensity'
|
|
14
|
-
import { ROOT_SELECTOR } from '@/constants/tailwind'
|
|
15
14
|
|
|
16
15
|
interface ThreadListProps {
|
|
17
16
|
className?: string
|
|
@@ -24,8 +23,7 @@ export const ThreadList: FC<ThreadListProps> = ({ className }) => {
|
|
|
24
23
|
className={cn(
|
|
25
24
|
'aui-root aui-thread-list-root bg-background flex flex-col items-stretch',
|
|
26
25
|
d('gap-sm'),
|
|
27
|
-
className
|
|
28
|
-
ROOT_SELECTOR
|
|
26
|
+
className
|
|
29
27
|
)}
|
|
30
28
|
>
|
|
31
29
|
<div
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
import { LazyMotion, MotionConfig, domAnimation } from 'motion/react'
|
|
24
24
|
import * as m from 'motion/react-m'
|
|
25
25
|
import { useEffect, useRef, useState, type FC } from 'react'
|
|
26
|
+
import { AnimatePresence } from 'motion/react'
|
|
26
27
|
|
|
27
28
|
import {
|
|
28
29
|
ComposerAddAttachment,
|
|
@@ -32,8 +33,11 @@ import {
|
|
|
32
33
|
import { MarkdownText } from '@/components/assistant-ui/markdown-text'
|
|
33
34
|
import { Reasoning, ReasoningGroup } from '@/components/assistant-ui/reasoning'
|
|
34
35
|
import { ToolFallback } from '@/components/assistant-ui/tool-fallback'
|
|
36
|
+
import { ToolMentionAutocomplete } from '@/components/assistant-ui/tool-mention-autocomplete'
|
|
37
|
+
import { MentionedToolsBadges } from '@/components/assistant-ui/mentioned-tools-badges'
|
|
35
38
|
import { TooltipIconButton } from '@/components/assistant-ui/tooltip-icon-button'
|
|
36
39
|
import { Button } from '@/components/ui/button'
|
|
40
|
+
import { useToolMentions } from '@/hooks/useToolMentions'
|
|
37
41
|
|
|
38
42
|
import { useDensity } from '@/hooks/useDensity'
|
|
39
43
|
import { useElements } from '@/hooks/useElements'
|
|
@@ -262,8 +266,97 @@ const ThreadSuggestions: FC = () => {
|
|
|
262
266
|
)
|
|
263
267
|
}
|
|
264
268
|
|
|
269
|
+
/**
|
|
270
|
+
* Component that handles tool mentions (@tool) in the composer.
|
|
271
|
+
* Shows autocomplete dropdown and badges for mentioned tools.
|
|
272
|
+
*/
|
|
273
|
+
const ComposerToolMentions: FC<{
|
|
274
|
+
tools: Record<string, unknown> | undefined
|
|
275
|
+
}> = ({ tools }) => {
|
|
276
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
277
|
+
|
|
278
|
+
const {
|
|
279
|
+
mentionableTools,
|
|
280
|
+
mentionedToolIds,
|
|
281
|
+
value,
|
|
282
|
+
cursorPosition,
|
|
283
|
+
textareaRef,
|
|
284
|
+
updateCursorPosition,
|
|
285
|
+
handleAutocompleteChange,
|
|
286
|
+
removeMention,
|
|
287
|
+
isActive,
|
|
288
|
+
} = useToolMentions({ tools })
|
|
289
|
+
|
|
290
|
+
// Find and attach to the textarea within the composer
|
|
291
|
+
useEffect(() => {
|
|
292
|
+
if (!isActive) return
|
|
293
|
+
|
|
294
|
+
const findTextarea = () => {
|
|
295
|
+
const textarea = document.querySelector(
|
|
296
|
+
'.aui-composer-input'
|
|
297
|
+
) as HTMLTextAreaElement | null
|
|
298
|
+
if (textarea && textareaRef.current !== textarea) {
|
|
299
|
+
textareaRef.current = textarea
|
|
300
|
+
|
|
301
|
+
const handleSelectionChange = () => updateCursorPosition()
|
|
302
|
+
textarea.addEventListener('click', handleSelectionChange)
|
|
303
|
+
textarea.addEventListener('keyup', handleSelectionChange)
|
|
304
|
+
textarea.addEventListener('input', handleSelectionChange)
|
|
305
|
+
|
|
306
|
+
return () => {
|
|
307
|
+
textarea.removeEventListener('click', handleSelectionChange)
|
|
308
|
+
textarea.removeEventListener('keyup', handleSelectionChange)
|
|
309
|
+
textarea.removeEventListener('input', handleSelectionChange)
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const cleanup = findTextarea()
|
|
315
|
+
|
|
316
|
+
const observer = new MutationObserver(() => {
|
|
317
|
+
findTextarea()
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
observer.observe(document.body, {
|
|
321
|
+
childList: true,
|
|
322
|
+
subtree: true,
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
return () => {
|
|
326
|
+
cleanup?.()
|
|
327
|
+
observer.disconnect()
|
|
328
|
+
}
|
|
329
|
+
}, [isActive, textareaRef, updateCursorPosition])
|
|
330
|
+
|
|
331
|
+
if (!isActive) {
|
|
332
|
+
return null
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return (
|
|
336
|
+
<div ref={containerRef} className="aui-composer-tool-mentions relative">
|
|
337
|
+
{/* Badges showing mentioned tools */}
|
|
338
|
+
<MentionedToolsBadges
|
|
339
|
+
mentionedToolIds={mentionedToolIds}
|
|
340
|
+
tools={mentionableTools}
|
|
341
|
+
onRemove={removeMention}
|
|
342
|
+
/>
|
|
343
|
+
|
|
344
|
+
{/* Autocomplete dropdown */}
|
|
345
|
+
<AnimatePresence>
|
|
346
|
+
<ToolMentionAutocomplete
|
|
347
|
+
tools={mentionableTools}
|
|
348
|
+
value={value}
|
|
349
|
+
cursorPosition={cursorPosition}
|
|
350
|
+
onValueChange={handleAutocompleteChange}
|
|
351
|
+
textareaRef={textareaRef}
|
|
352
|
+
/>
|
|
353
|
+
</AnimatePresence>
|
|
354
|
+
</div>
|
|
355
|
+
)
|
|
356
|
+
}
|
|
357
|
+
|
|
265
358
|
const Composer: FC = () => {
|
|
266
|
-
const { config } = useElements()
|
|
359
|
+
const { config, mcpTools } = useElements()
|
|
267
360
|
const r = useRadius()
|
|
268
361
|
const d = useDensity()
|
|
269
362
|
const composerConfig = config.composer ?? {
|
|
@@ -272,6 +365,15 @@ const Composer: FC = () => {
|
|
|
272
365
|
}
|
|
273
366
|
const components = config.components ?? {}
|
|
274
367
|
|
|
368
|
+
// Determine if tool mentions are enabled (default: true)
|
|
369
|
+
const toolMentionsEnabled =
|
|
370
|
+
composerConfig.toolMentions === undefined ||
|
|
371
|
+
composerConfig.toolMentions === true ||
|
|
372
|
+
(typeof composerConfig.toolMentions === 'object' &&
|
|
373
|
+
composerConfig.toolMentions.enabled !== false)
|
|
374
|
+
|
|
375
|
+
const composerRootRef = useRef<HTMLFormElement>(null)
|
|
376
|
+
|
|
275
377
|
if (components.Composer) {
|
|
276
378
|
return <components.Composer />
|
|
277
379
|
}
|
|
@@ -287,6 +389,7 @@ const Composer: FC = () => {
|
|
|
287
389
|
>
|
|
288
390
|
<ThreadScrollToBottom />
|
|
289
391
|
<ComposerPrimitive.Root
|
|
392
|
+
ref={composerRootRef}
|
|
290
393
|
className={cn(
|
|
291
394
|
'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 w-full flex-col border px-1 pt-2 shadow-xs transition-[color,box-shadow] outline-none has-[textarea:focus-visible]:ring-1',
|
|
292
395
|
r('xl')
|
|
@@ -294,6 +397,8 @@ const Composer: FC = () => {
|
|
|
294
397
|
>
|
|
295
398
|
{composerConfig.attachments && <ComposerAttachments />}
|
|
296
399
|
|
|
400
|
+
{toolMentionsEnabled && <ComposerToolMentions tools={mcpTools} />}
|
|
401
|
+
|
|
297
402
|
<ComposerPrimitive.Input
|
|
298
403
|
placeholder={composerConfig.placeholder}
|
|
299
404
|
className={cn(
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { FC, useCallback, useEffect, useRef, useState } from 'react'
|
|
2
|
+
import { Wrench } from 'lucide-react'
|
|
3
|
+
import * as m from 'motion/react-m'
|
|
4
|
+
|
|
5
|
+
import { cn } from '@/lib/utils'
|
|
6
|
+
import { useDensity } from '@/hooks/useDensity'
|
|
7
|
+
import { useRadius } from '@/hooks/useRadius'
|
|
8
|
+
import { EASE_OUT_QUINT } from '@/lib/easing'
|
|
9
|
+
import {
|
|
10
|
+
MentionableTool,
|
|
11
|
+
detectMentionContext,
|
|
12
|
+
filterToolsByQuery,
|
|
13
|
+
insertToolMention,
|
|
14
|
+
} from '@/lib/tool-mentions'
|
|
15
|
+
|
|
16
|
+
export interface ToolMentionAutocompleteProps {
|
|
17
|
+
tools: MentionableTool[]
|
|
18
|
+
value: string
|
|
19
|
+
cursorPosition: number
|
|
20
|
+
onValueChange: (value: string, cursorPosition: number) => void
|
|
21
|
+
textareaRef: React.RefObject<HTMLTextAreaElement | null>
|
|
22
|
+
className?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const ToolMentionAutocomplete: FC<ToolMentionAutocompleteProps> = ({
|
|
26
|
+
tools,
|
|
27
|
+
value,
|
|
28
|
+
cursorPosition,
|
|
29
|
+
onValueChange,
|
|
30
|
+
textareaRef,
|
|
31
|
+
className,
|
|
32
|
+
}) => {
|
|
33
|
+
const [selectedIndex, setSelectedIndex] = useState(0)
|
|
34
|
+
const [isVisible, setIsVisible] = useState(false)
|
|
35
|
+
const [filteredTools, setFilteredTools] = useState<MentionableTool[]>([])
|
|
36
|
+
const [mentionContext, setMentionContext] = useState<{
|
|
37
|
+
atPosition: number
|
|
38
|
+
query: string
|
|
39
|
+
} | null>(null)
|
|
40
|
+
|
|
41
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
42
|
+
const d = useDensity()
|
|
43
|
+
const r = useRadius()
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
const context = detectMentionContext(value, cursorPosition)
|
|
47
|
+
|
|
48
|
+
if (context.isInMention && tools.length > 0) {
|
|
49
|
+
const filtered = filterToolsByQuery(tools, context.query)
|
|
50
|
+
setFilteredTools(filtered)
|
|
51
|
+
setIsVisible(filtered.length > 0)
|
|
52
|
+
setMentionContext({
|
|
53
|
+
atPosition: context.atPosition,
|
|
54
|
+
query: context.query,
|
|
55
|
+
})
|
|
56
|
+
setSelectedIndex(0)
|
|
57
|
+
} else {
|
|
58
|
+
setIsVisible(false)
|
|
59
|
+
setMentionContext(null)
|
|
60
|
+
}
|
|
61
|
+
}, [value, cursorPosition, tools])
|
|
62
|
+
|
|
63
|
+
const selectTool = useCallback(
|
|
64
|
+
(tool: MentionableTool) => {
|
|
65
|
+
if (!mentionContext) return
|
|
66
|
+
|
|
67
|
+
const result = insertToolMention(
|
|
68
|
+
value,
|
|
69
|
+
tool.name,
|
|
70
|
+
mentionContext.atPosition,
|
|
71
|
+
cursorPosition
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
onValueChange(result.text, result.cursorPosition)
|
|
75
|
+
setIsVisible(false)
|
|
76
|
+
},
|
|
77
|
+
[mentionContext, value, cursorPosition, onValueChange, textareaRef]
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
if (!isVisible) return
|
|
82
|
+
|
|
83
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
84
|
+
switch (e.key) {
|
|
85
|
+
case 'ArrowDown':
|
|
86
|
+
e.preventDefault()
|
|
87
|
+
e.stopPropagation()
|
|
88
|
+
setSelectedIndex((prev) => (prev + 1) % filteredTools.length)
|
|
89
|
+
break
|
|
90
|
+
case 'ArrowUp':
|
|
91
|
+
e.preventDefault()
|
|
92
|
+
e.stopPropagation()
|
|
93
|
+
setSelectedIndex(
|
|
94
|
+
(prev) => (prev - 1 + filteredTools.length) % filteredTools.length
|
|
95
|
+
)
|
|
96
|
+
break
|
|
97
|
+
case 'Enter':
|
|
98
|
+
case 'Tab':
|
|
99
|
+
e.preventDefault()
|
|
100
|
+
e.stopPropagation()
|
|
101
|
+
if (filteredTools[selectedIndex]) {
|
|
102
|
+
selectTool(filteredTools[selectedIndex])
|
|
103
|
+
}
|
|
104
|
+
break
|
|
105
|
+
case 'Escape':
|
|
106
|
+
e.preventDefault()
|
|
107
|
+
e.stopPropagation()
|
|
108
|
+
setIsVisible(false)
|
|
109
|
+
break
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const textarea = textareaRef.current
|
|
114
|
+
if (textarea) {
|
|
115
|
+
textarea.addEventListener('keydown', handleKeyDown, { capture: true })
|
|
116
|
+
return () =>
|
|
117
|
+
textarea.removeEventListener('keydown', handleKeyDown, {
|
|
118
|
+
capture: true,
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
}, [isVisible, filteredTools, selectedIndex, selectTool, textareaRef])
|
|
122
|
+
|
|
123
|
+
// Scroll selected item into view
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
if (!isVisible) return
|
|
126
|
+
const container = containerRef.current
|
|
127
|
+
if (!container) return
|
|
128
|
+
|
|
129
|
+
const selectedItem = container.querySelector(
|
|
130
|
+
`[data-index="${selectedIndex}"]`
|
|
131
|
+
) as HTMLElement
|
|
132
|
+
if (selectedItem) {
|
|
133
|
+
selectedItem.scrollIntoView({ block: 'nearest' })
|
|
134
|
+
}
|
|
135
|
+
}, [selectedIndex, isVisible])
|
|
136
|
+
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
if (!isVisible) return
|
|
139
|
+
|
|
140
|
+
const handleClickOutside = (e: MouseEvent) => {
|
|
141
|
+
if (
|
|
142
|
+
containerRef.current &&
|
|
143
|
+
!containerRef.current.contains(e.target as Node) &&
|
|
144
|
+
textareaRef.current &&
|
|
145
|
+
!textareaRef.current.contains(e.target as Node)
|
|
146
|
+
) {
|
|
147
|
+
setIsVisible(false)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
document.addEventListener('mousedown', handleClickOutside)
|
|
152
|
+
return () => document.removeEventListener('mousedown', handleClickOutside)
|
|
153
|
+
}, [isVisible, textareaRef])
|
|
154
|
+
|
|
155
|
+
// When autocomplete is visible, modify composer styles and position autocomplete to match composer width
|
|
156
|
+
useEffect(() => {
|
|
157
|
+
const textarea = textareaRef.current
|
|
158
|
+
if (!textarea) return
|
|
159
|
+
|
|
160
|
+
const composer = textarea.closest('.aui-composer-root') as HTMLElement
|
|
161
|
+
if (!composer) return
|
|
162
|
+
|
|
163
|
+
const updateStyles = () => {
|
|
164
|
+
const autocomplete = containerRef.current
|
|
165
|
+
if (!autocomplete) return
|
|
166
|
+
|
|
167
|
+
if (isVisible) {
|
|
168
|
+
// Modify composer to connect with autocomplete
|
|
169
|
+
composer.style.borderTopColor = 'var(--ring)'
|
|
170
|
+
composer.style.borderTopLeftRadius = '0'
|
|
171
|
+
composer.style.borderTopRightRadius = '0'
|
|
172
|
+
|
|
173
|
+
// Position autocomplete to match composer width
|
|
174
|
+
const composerRect = composer.getBoundingClientRect()
|
|
175
|
+
const autocompleteParent = autocomplete.offsetParent as HTMLElement
|
|
176
|
+
if (autocompleteParent) {
|
|
177
|
+
const parentRect = autocompleteParent.getBoundingClientRect()
|
|
178
|
+
autocomplete.style.left = `${composerRect.left - parentRect.left}px`
|
|
179
|
+
autocomplete.style.right = 'auto'
|
|
180
|
+
autocomplete.style.width = `${composerRect.width}px`
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (isVisible) {
|
|
186
|
+
// Use requestAnimationFrame to ensure DOM is updated
|
|
187
|
+
requestAnimationFrame(updateStyles)
|
|
188
|
+
} else {
|
|
189
|
+
composer.style.borderTopColor = ''
|
|
190
|
+
composer.style.borderTopLeftRadius = ''
|
|
191
|
+
composer.style.borderTopRightRadius = ''
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return () => {
|
|
195
|
+
composer.style.borderTopColor = ''
|
|
196
|
+
composer.style.borderTopLeftRadius = ''
|
|
197
|
+
composer.style.borderTopRightRadius = ''
|
|
198
|
+
}
|
|
199
|
+
}, [isVisible, textareaRef])
|
|
200
|
+
|
|
201
|
+
if (!isVisible || filteredTools.length === 0) {
|
|
202
|
+
return null
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return (
|
|
206
|
+
<m.div
|
|
207
|
+
ref={containerRef}
|
|
208
|
+
initial={{ opacity: 0, y: 8 }}
|
|
209
|
+
animate={{ opacity: 1, y: 0 }}
|
|
210
|
+
exit={{ opacity: 0, y: 8 }}
|
|
211
|
+
transition={{ duration: 0.15, ease: EASE_OUT_QUINT }}
|
|
212
|
+
className={cn(
|
|
213
|
+
'aui-tool-mention-autocomplete border-ring bg-background absolute bottom-full z-50 max-h-[220px] overflow-clip overflow-y-auto overscroll-contain rounded-br-none! rounded-bl-none! border border-b-0 shadow-xs',
|
|
214
|
+
r('xl'),
|
|
215
|
+
className
|
|
216
|
+
)}
|
|
217
|
+
>
|
|
218
|
+
<div className="flex flex-col gap-1">
|
|
219
|
+
{filteredTools.map((tool, index) => (
|
|
220
|
+
<button
|
|
221
|
+
key={tool.id}
|
|
222
|
+
type="button"
|
|
223
|
+
data-index={index}
|
|
224
|
+
className={cn(
|
|
225
|
+
'aui-tool-mention-item flex w-full items-center gap-2 text-left transition-colors',
|
|
226
|
+
d('p-sm'),
|
|
227
|
+
'hover:bg-accent hover:text-accent-foreground',
|
|
228
|
+
index === selectedIndex && 'bg-accent text-accent-foreground'
|
|
229
|
+
)}
|
|
230
|
+
onClick={(e) => {
|
|
231
|
+
e.preventDefault()
|
|
232
|
+
e.stopPropagation()
|
|
233
|
+
selectTool(tool)
|
|
234
|
+
}}
|
|
235
|
+
onMouseEnter={() => setSelectedIndex(index)}
|
|
236
|
+
>
|
|
237
|
+
<Wrench className="mt-0.5 size-4 flex-shrink-0 opacity-50" />
|
|
238
|
+
<div className="min-w-0 flex-1">
|
|
239
|
+
<div className="text-sm font-medium">{tool.name}</div>
|
|
240
|
+
{tool.description && (
|
|
241
|
+
<div className="text-muted-foreground line-clamp-2 text-xs">
|
|
242
|
+
{tool.description}
|
|
243
|
+
</div>
|
|
244
|
+
)}
|
|
245
|
+
</div>
|
|
246
|
+
</button>
|
|
247
|
+
))}
|
|
248
|
+
</div>
|
|
249
|
+
</m.div>
|
|
250
|
+
)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export default ToolMentionAutocomplete
|
|
@@ -322,7 +322,7 @@ function ToolUISection({
|
|
|
322
322
|
<div data-slot="tool-ui-section" className="border-border border-t">
|
|
323
323
|
<button
|
|
324
324
|
onClick={() => setIsExpanded(!isExpanded)}
|
|
325
|
-
className="hover:bg-accent/50 flex w-full cursor-pointer items-center justify-between px-
|
|
325
|
+
className="hover:bg-accent/50 flex w-full cursor-pointer items-center justify-between px-5 py-2.5 text-left transition-colors"
|
|
326
326
|
>
|
|
327
327
|
<span className="text-muted-foreground text-sm">{title}</span>
|
|
328
328
|
<div className="flex items-center gap-1">
|
|
@@ -46,7 +46,11 @@ import {
|
|
|
46
46
|
import { useAuth } from '../hooks/useAuth'
|
|
47
47
|
import { ElementsContext } from './contexts'
|
|
48
48
|
import { ToolApprovalProvider } from './ToolApprovalContext'
|
|
49
|
-
import {
|
|
49
|
+
import {
|
|
50
|
+
isLocalThreadId,
|
|
51
|
+
useGramThreadListAdapter,
|
|
52
|
+
} from '@/hooks/useGramThreadListAdapter'
|
|
53
|
+
import { ROOT_SELECTOR } from '@/constants/tailwind'
|
|
50
54
|
|
|
51
55
|
export interface ElementsProviderProps {
|
|
52
56
|
children: ReactNode
|
|
@@ -139,6 +143,7 @@ const ElementsProviderWithApproval = ({
|
|
|
139
143
|
auth,
|
|
140
144
|
mcp: config.mcp,
|
|
141
145
|
environment: config.environment ?? {},
|
|
146
|
+
gramEnvironment: config.gramEnvironment,
|
|
142
147
|
})
|
|
143
148
|
|
|
144
149
|
// Store approval helpers in ref so they can be used in async contexts
|
|
@@ -187,6 +192,9 @@ const ElementsProviderWithApproval = ({
|
|
|
187
192
|
// When history is enabled, the thread adapter manages chat IDs instead
|
|
188
193
|
const chatIdRef = useRef<string | null>(null)
|
|
189
194
|
|
|
195
|
+
// Map to share local thread IDs to UUIDs between adapter and transport (for history mode)
|
|
196
|
+
const localIdToUuidMapRef = useRef(new Map<string, string>())
|
|
197
|
+
|
|
190
198
|
// Create chat transport configuration
|
|
191
199
|
const transport = useMemo<ChatTransport<UIMessage>>(
|
|
192
200
|
() => ({
|
|
@@ -197,9 +205,47 @@ const ElementsProviderWithApproval = ({
|
|
|
197
205
|
throw new Error('Session is loading')
|
|
198
206
|
}
|
|
199
207
|
|
|
200
|
-
//
|
|
201
|
-
|
|
202
|
-
|
|
208
|
+
// Get chat ID - try runtime's thread remoteId first (history mode),
|
|
209
|
+
// fall back to generated ID (non-history mode)
|
|
210
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
211
|
+
const runtimeAny = runtimeRef.current as any
|
|
212
|
+
|
|
213
|
+
// Try multiple paths to get thread info
|
|
214
|
+
const threadListItemState = runtimeAny?.threadListItem?.getState?.()
|
|
215
|
+
const threadsState = runtimeAny?.threads?.getState?.()
|
|
216
|
+
|
|
217
|
+
// Get the thread ID - try different sources
|
|
218
|
+
const threadRemoteId = threadListItemState?.remoteId as
|
|
219
|
+
| string
|
|
220
|
+
| undefined
|
|
221
|
+
const localThreadId = (threadListItemState?.id ??
|
|
222
|
+
threadsState?.mainThreadId ??
|
|
223
|
+
threadsState?.threadIds?.[0]) as string | undefined
|
|
224
|
+
|
|
225
|
+
let chatId = threadRemoteId
|
|
226
|
+
|
|
227
|
+
if (isLocalThreadId(chatId) || (!chatId && localThreadId)) {
|
|
228
|
+
const lookupKey = chatId ?? localThreadId
|
|
229
|
+
if (lookupKey) {
|
|
230
|
+
// For local thread IDs, check if we already have a UUID mapping
|
|
231
|
+
const existingUuid = localIdToUuidMapRef.current.get(lookupKey)
|
|
232
|
+
if (existingUuid) {
|
|
233
|
+
chatId = existingUuid
|
|
234
|
+
} else {
|
|
235
|
+
// Generate a new UUID and store the mapping
|
|
236
|
+
const newUuid = crypto.randomUUID()
|
|
237
|
+
localIdToUuidMapRef.current.set(lookupKey, newUuid)
|
|
238
|
+
chatId = newUuid
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (!chatId) {
|
|
244
|
+
// Non-history mode fallback - use stable chatIdRef
|
|
245
|
+
if (!chatIdRef.current) {
|
|
246
|
+
chatIdRef.current = crypto.randomUUID()
|
|
247
|
+
}
|
|
248
|
+
chatId = chatIdRef.current
|
|
203
249
|
}
|
|
204
250
|
|
|
205
251
|
const context = runtimeRef.current?.thread.getModelContext()
|
|
@@ -207,10 +253,13 @@ const ElementsProviderWithApproval = ({
|
|
|
207
253
|
getEnabledTools(context?.tools ?? {})
|
|
208
254
|
)
|
|
209
255
|
|
|
210
|
-
// Include Gram-Chat-ID header for chat persistence
|
|
256
|
+
// Include Gram-Chat-ID header for chat persistence and Gram-Environment for environment selection
|
|
211
257
|
const headersWithChatId = {
|
|
212
258
|
...auth.headers,
|
|
213
|
-
'Gram-Chat-ID':
|
|
259
|
+
'Gram-Chat-ID': chatId,
|
|
260
|
+
...(config.gramEnvironment && {
|
|
261
|
+
'Gram-Environment': config.gramEnvironment,
|
|
262
|
+
}),
|
|
214
263
|
}
|
|
215
264
|
|
|
216
265
|
// Create OpenRouter model (only needed when not using custom model)
|
|
@@ -300,8 +349,9 @@ const ElementsProviderWithApproval = ({
|
|
|
300
349
|
isOpen: isOpen ?? false,
|
|
301
350
|
setIsOpen,
|
|
302
351
|
plugins,
|
|
352
|
+
mcpTools,
|
|
303
353
|
}),
|
|
304
|
-
[config, model, isExpanded, isOpen, plugins]
|
|
354
|
+
[config, model, isExpanded, isOpen, plugins, mcpTools]
|
|
305
355
|
)
|
|
306
356
|
|
|
307
357
|
const frontendTools = config.tools?.frontendTools ?? {}
|
|
@@ -317,6 +367,7 @@ const ElementsProviderWithApproval = ({
|
|
|
317
367
|
contextValue={contextValue}
|
|
318
368
|
runtimeRef={runtimeRef}
|
|
319
369
|
frontendTools={frontendTools}
|
|
370
|
+
localIdToUuidMap={localIdToUuidMapRef.current}
|
|
320
371
|
>
|
|
321
372
|
{children}
|
|
322
373
|
</ElementsProviderWithHistory>
|
|
@@ -344,6 +395,7 @@ interface ElementsProviderWithHistoryProps {
|
|
|
344
395
|
contextValue: React.ContextType<typeof ElementsContext>
|
|
345
396
|
runtimeRef: React.RefObject<ReturnType<typeof useChatRuntime> | null>
|
|
346
397
|
frontendTools: Record<string, AssistantTool>
|
|
398
|
+
localIdToUuidMap: Map<string, string>
|
|
347
399
|
}
|
|
348
400
|
|
|
349
401
|
const ElementsProviderWithHistory = ({
|
|
@@ -354,8 +406,13 @@ const ElementsProviderWithHistory = ({
|
|
|
354
406
|
contextValue,
|
|
355
407
|
runtimeRef,
|
|
356
408
|
frontendTools,
|
|
409
|
+
localIdToUuidMap,
|
|
357
410
|
}: ElementsProviderWithHistoryProps) => {
|
|
358
|
-
const threadListAdapter = useGramThreadListAdapter({
|
|
411
|
+
const threadListAdapter = useGramThreadListAdapter({
|
|
412
|
+
apiUrl,
|
|
413
|
+
headers,
|
|
414
|
+
localIdToUuidMap,
|
|
415
|
+
})
|
|
359
416
|
|
|
360
417
|
// Hook factory for creating the base chat runtime
|
|
361
418
|
const useChatRuntimeHook = useCallback(() => {
|
|
@@ -381,7 +438,7 @@ const ElementsProviderWithHistory = ({
|
|
|
381
438
|
<AssistantRuntimeProvider runtime={runtime}>
|
|
382
439
|
<HistoryProvider>
|
|
383
440
|
<ElementsContext.Provider value={contextValue}>
|
|
384
|
-
{children}
|
|
441
|
+
<div className={`${ROOT_SELECTOR} h-full`}>{children}</div>
|
|
385
442
|
<FrontendTools tools={frontendTools} />
|
|
386
443
|
</ElementsContext.Provider>
|
|
387
444
|
</HistoryProvider>
|
|
@@ -415,7 +472,7 @@ const ElementsProviderWithoutHistory = ({
|
|
|
415
472
|
return (
|
|
416
473
|
<AssistantRuntimeProvider runtime={runtime}>
|
|
417
474
|
<ElementsContext.Provider value={contextValue}>
|
|
418
|
-
{children}
|
|
475
|
+
<div className={`${ROOT_SELECTOR} h-full`}>{children}</div>
|
|
419
476
|
<FrontendTools tools={frontendTools} />
|
|
420
477
|
</ElementsContext.Provider>
|
|
421
478
|
</AssistantRuntimeProvider>
|
package/src/embedded.ts
ADDED