@gram-ai/elements 1.25.1 → 1.25.2

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 (44) hide show
  1. package/dist/components/Chat/stories/MessageFeedback.stories.d.ts +1 -1
  2. package/dist/contexts/ChatIdContext.d.ts +11 -0
  3. package/dist/contexts/contexts.d.ts +1 -0
  4. package/dist/elements.cjs +1 -1
  5. package/dist/elements.css +1 -1
  6. package/dist/elements.js +7 -6
  7. package/dist/{index-iUSSoKFz.cjs → index-B8nSCdu4.cjs} +11 -11
  8. package/dist/index-B8nSCdu4.cjs.map +1 -0
  9. package/dist/{index-C3UbmFRR.cjs → index-CAtaLV1E.cjs} +63 -54
  10. package/dist/index-CAtaLV1E.cjs.map +1 -0
  11. package/dist/{index-CtyV0c-T.js → index-CJrwma08.js} +3737 -3730
  12. package/dist/index-CJrwma08.js.map +1 -0
  13. package/dist/{index-DxJwZ5Kc.js → index-DLWQ91ow.js} +8439 -8365
  14. package/dist/index-DLWQ91ow.js.map +1 -0
  15. package/dist/index.d.ts +1 -0
  16. package/dist/plugins.cjs +1 -1
  17. package/dist/plugins.js +1 -1
  18. package/dist/{profiler-CijCgLrw.js → profiler-BaG0scxd.js} +2 -2
  19. package/dist/{profiler-CijCgLrw.js.map → profiler-BaG0scxd.js.map} +1 -1
  20. package/dist/{profiler-DAT0DL1W.cjs → profiler-CuqENACf.cjs} +2 -2
  21. package/dist/{profiler-DAT0DL1W.cjs.map → profiler-CuqENACf.cjs.map} +1 -1
  22. package/dist/{startRecording-gmhENmf0.js → startRecording-86bHmd-l.js} +2 -2
  23. package/dist/{startRecording-gmhENmf0.js.map → startRecording-86bHmd-l.js.map} +1 -1
  24. package/dist/{startRecording-DotsE8QT.cjs → startRecording-BiLmoqZa.cjs} +2 -2
  25. package/dist/{startRecording-DotsE8QT.cjs.map → startRecording-BiLmoqZa.cjs.map} +1 -1
  26. package/dist/types/index.d.ts +4 -4
  27. package/package.json +1 -1
  28. package/src/components/Chat/stories/MessageFeedback.stories.tsx +6 -6
  29. package/src/components/Chat/stories/ToolApproval.stories.tsx +10 -10
  30. package/src/components/Chat/stories/Tools.stories.tsx +122 -104
  31. package/src/components/Chat/stories/Variants.stories.tsx +1 -1
  32. package/src/components/ShadowRoot.tsx +5 -1
  33. package/src/components/assistant-ui/message-feedback.tsx +6 -7
  34. package/src/components/assistant-ui/thread.tsx +76 -11
  35. package/src/contexts/ChatIdContext.tsx +21 -0
  36. package/src/contexts/ElementsProvider.tsx +77 -37
  37. package/src/contexts/contexts.ts +2 -0
  38. package/src/hooks/useAuth.ts +1 -2
  39. package/src/index.ts +1 -0
  40. package/src/types/index.ts +4 -4
  41. package/dist/index-C3UbmFRR.cjs.map +0 -1
  42. package/dist/index-CtyV0c-T.js.map +0 -1
  43. package/dist/index-DxJwZ5Kc.js.map +0 -1
  44. package/dist/index-iUSSoKFz.cjs.map +0 -1
@@ -1,8 +1,9 @@
1
1
  import { ToolCallMessagePartProps } from '@assistant-ui/react'
2
2
  import type { Meta, StoryFn } from '@storybook/react-vite'
3
- import React from 'react'
3
+ import React, { useState, useCallback } from 'react'
4
4
  import z from 'zod'
5
5
  import { Chat } from '..'
6
+ import { useToolExecution } from '../../../contexts/ToolExecutionContext'
6
7
  import { defineFrontendTool } from '../../../lib/tools'
7
8
 
8
9
  const meta: Meta<typeof Chat> = {
@@ -17,128 +18,145 @@ export default meta
17
18
 
18
19
  type Story = StoryFn<typeof Chat>
19
20
 
20
- const CardPinRevealComponent = ({
21
- result,
22
- argsText,
23
- }: ToolCallMessagePartProps) => {
24
- const [isFlipped, setIsFlipped] = React.useState(false)
21
+ const ProductCardComponent = ({ result }: ToolCallMessagePartProps) => {
22
+ const { executeTool, isToolAvailable } = useToolExecution()
23
+ const [isLoading, setIsLoading] = useState(false)
24
+ const [addedToCart, setAddedToCart] = useState(false)
25
+
26
+ // Parse the result to get product details
27
+ let product = {
28
+ id: '',
29
+ name: 'Loading...',
30
+ description: '',
31
+ price: 0,
32
+ category: '',
33
+ rating: 0,
34
+ reviewCount: 0,
35
+ imageUrl: '',
36
+ inStock: true,
37
+ }
25
38
 
26
- // Parse the result to get the pin
27
- let pin = '****'
28
39
  try {
29
40
  if (result) {
30
41
  const parsed = typeof result === 'string' ? JSON.parse(result) : result
31
42
  if (parsed?.content?.[0]?.text) {
32
43
  const content = JSON.parse(parsed.content[0].text)
33
- pin = content.pin || '****'
34
- } else if (parsed?.pin) {
35
- pin = parsed.pin
44
+ product = { ...product, ...content }
45
+ } else if (parsed?.name) {
46
+ product = { ...product, ...parsed }
36
47
  }
37
48
  }
38
49
  } catch {
39
50
  // Fallback to default
40
51
  }
41
52
 
42
- const args = JSON.parse(argsText || '{}')
43
- const cardNumber = args?.queryParameters?.cardNumber || '4532 •••• •••• 1234'
44
- const cardHolder = 'JOHN DOE'
45
- const expiry = '12/25'
46
- const cvv = '123'
53
+ const canAddToCart = isToolAvailable('ecommerce_api_add_to_cart')
47
54
 
48
- if (!cardNumber) {
49
- return null
50
- }
55
+ const handleAddToCart = useCallback(async () => {
56
+ if (!product.id || !canAddToCart) return
51
57
 
52
- return (
53
- <div className="my-4 perspective-[1000px]">
54
- <div
55
- className={`relative h-48 w-80 cursor-pointer transition-transform duration-700 [transform-style:preserve-3d] ${
56
- isFlipped ? 'transform-[rotateY(180deg)]' : ''
57
- }`}
58
- onClick={() => setIsFlipped(!isFlipped)}
59
- >
60
- {/* Front of card */}
61
- <div className="absolute inset-0 backface-hidden">
62
- <div className="relative h-full w-full overflow-hidden rounded-xl bg-gradient-to-br from-indigo-600 via-purple-600 to-pink-500 p-6 text-white shadow-2xl">
63
- {/* Card pattern overlay */}
64
- <div className="absolute inset-0 opacity-10">
65
- <div className="absolute -top-10 -right-10 h-40 w-40 rounded-full bg-white"></div>
66
- <div className="absolute -bottom-10 -left-10 h-32 w-32 rounded-full bg-white"></div>
67
- </div>
58
+ setIsLoading(true)
59
+ try {
60
+ // HTTP tools from OpenAPI expect body content wrapped in a 'body' field
61
+ const toolResult = await executeTool('ecommerce_api_add_to_cart', {
62
+ body: {
63
+ productId: product.id,
64
+ quantity: 1,
65
+ },
66
+ })
68
67
 
69
- {/* Card content */}
70
- <div className="relative z-10 flex h-full flex-col justify-between">
71
- <div className="flex items-center justify-between">
72
- <div className="text-2xl font-bold">VISA</div>
73
- <div className="h-8 w-12 rounded bg-white/20"></div>
74
- </div>
68
+ if (toolResult.success) {
69
+ setAddedToCart(true)
70
+ } else {
71
+ console.error('[ProductCard] Tool failed:', toolResult.error)
72
+ }
73
+ } catch (err) {
74
+ console.error('[ProductCard] Exception:', err)
75
+ } finally {
76
+ setIsLoading(false)
77
+ }
78
+ }, [product.id, canAddToCart, executeTool])
75
79
 
76
- <div className="space-y-2">
77
- <div className="font-mono text-2xl tracking-wider">
78
- {cardNumber}
79
- </div>
80
- <div className="flex items-center justify-between text-sm">
81
- <div>
82
- <div className="text-xs opacity-70">CARDHOLDER</div>
83
- <div className="font-semibold">{cardHolder}</div>
84
- </div>
85
- <div>
86
- <div className="text-xs opacity-70">EXPIRES</div>
87
- <div className="font-semibold">{expiry}</div>
88
- </div>
89
- </div>
90
- </div>
80
+ return (
81
+ <div className="my-4 w-80">
82
+ <div className="overflow-hidden rounded-xl bg-white shadow-lg dark:bg-slate-800">
83
+ {/* Product Image */}
84
+ <div className="relative h-48 bg-gradient-to-br from-indigo-100 to-purple-100 dark:from-indigo-900 dark:to-purple-900">
85
+ {product.imageUrl ? (
86
+ <img
87
+ src={product.imageUrl}
88
+ alt={product.name}
89
+ className="h-full w-full object-cover"
90
+ />
91
+ ) : (
92
+ <div className="flex h-full items-center justify-center">
93
+ <span className="text-6xl">📦</span>
91
94
  </div>
92
-
93
- {/* Click hint */}
94
- <div className="absolute right-2 bottom-2 text-xs opacity-50">
95
- Click to flip
95
+ )}
96
+ {!product.inStock && (
97
+ <div className="absolute top-2 right-2 rounded-full bg-red-500 px-2 py-1 text-xs font-semibold text-white">
98
+ Out of Stock
96
99
  </div>
97
- </div>
100
+ )}
98
101
  </div>
99
102
 
100
- {/* Back of card */}
101
- <div className="absolute inset-0 transform-[rotateY(180deg)] backface-hidden">
102
- <div className="relative h-full w-full overflow-hidden rounded-xl bg-gradient-to-br from-slate-800 via-slate-700 to-slate-900 p-6 text-white shadow-2xl">
103
- {/* Magnetic strip */}
104
- <div className="absolute top-8 right-0 left-0 h-12 bg-black"></div>
105
-
106
- {/* Card content */}
107
- <div className="relative z-10 flex h-full flex-col justify-between">
108
- <div className="mt-16 space-y-4">
109
- <div className="flex items-center gap-2">
110
- <div className="h-8 flex-1 rounded bg-white/10 px-3 py-2 text-right font-mono text-sm">
111
- {cvv}
112
- </div>
113
- <div className="text-xs opacity-70">CVV</div>
114
- </div>
115
-
116
- {/* PIN Display */}
117
- <div className="mt-6 space-y-2">
118
- <div className="text-xs opacity-70">PIN</div>
119
- <div className="flex items-center gap-3">
120
- <div className="flex h-16 w-16 items-center justify-center rounded-lg bg-gradient-to-br from-yellow-400 to-orange-500 shadow-lg">
121
- <span className="text-2xl font-bold text-white">
122
- {pin}
123
- </span>
124
- </div>
125
- <div className="text-xs opacity-60">
126
- Keep this PIN secure
127
- </div>
128
- </div>
129
- </div>
130
- </div>
103
+ {/* Product Details */}
104
+ <div className="p-4">
105
+ <div className="mb-1 text-xs font-medium tracking-wide text-indigo-500 uppercase dark:text-indigo-400">
106
+ {product.category}
107
+ </div>
108
+ <h3 className="mb-2 text-lg font-bold text-slate-900 dark:text-white">
109
+ {product.name}
110
+ </h3>
111
+ <p className="mb-3 line-clamp-2 text-sm text-slate-600 dark:text-slate-300">
112
+ {product.description}
113
+ </p>
131
114
 
132
- <div className="flex items-center justify-between text-xs opacity-50">
133
- <div>VISA</div>
134
- <div>{cardNumber}</div>
135
- </div>
115
+ {/* Rating */}
116
+ <div className="mb-3 flex items-center gap-1">
117
+ <div className="flex">
118
+ {[1, 2, 3, 4, 5].map((star) => (
119
+ <span
120
+ key={star}
121
+ className={
122
+ star <= Math.round(product.rating)
123
+ ? 'text-yellow-400'
124
+ : 'text-slate-300'
125
+ }
126
+ >
127
+
128
+ </span>
129
+ ))}
136
130
  </div>
131
+ <span className="text-sm text-slate-500">
132
+ ({product.reviewCount} reviews)
133
+ </span>
134
+ </div>
137
135
 
138
- {/* Click hint */}
139
- <div className="absolute bottom-2 left-2 text-xs opacity-50">
140
- Click to flip back
141
- </div>
136
+ {/* Price and Add to Cart */}
137
+ <div className="flex items-center justify-between">
138
+ <span className="text-2xl font-bold text-slate-900 dark:text-white">
139
+ ${product.price?.toFixed(2)}
140
+ </span>
141
+ <button
142
+ onClick={handleAddToCart}
143
+ disabled={
144
+ isLoading || addedToCart || !canAddToCart || !product.inStock
145
+ }
146
+ className={`rounded-lg px-4 py-2 text-sm font-semibold text-white transition-colors ${
147
+ addedToCart
148
+ ? 'bg-green-500'
149
+ : isLoading
150
+ ? 'bg-indigo-400'
151
+ : 'bg-indigo-600 hover:bg-indigo-700'
152
+ } disabled:cursor-not-allowed disabled:opacity-50`}
153
+ >
154
+ {addedToCart
155
+ ? '✓ Added'
156
+ : isLoading
157
+ ? 'Adding...'
158
+ : 'Add to Cart'}
159
+ </button>
142
160
  </div>
143
161
  </div>
144
162
  </div>
@@ -154,15 +172,15 @@ CustomToolComponent.parameters = {
154
172
  welcome: {
155
173
  suggestions: [
156
174
  {
157
- title: 'Get card details',
158
- label: 'for your card',
159
- prompt: 'Get card details for your card number 4532 •••• •••• 1234',
175
+ title: 'Get product details',
176
+ label: 'View a product',
177
+ prompt: 'List products and then show me details for the first one',
160
178
  },
161
179
  ],
162
180
  },
163
181
  tools: {
164
182
  components: {
165
- kitchen_sink_get_get_card_details: CardPinRevealComponent,
183
+ ecommerce_api_get_product: ProductCardComponent,
166
184
  },
167
185
  },
168
186
  },
@@ -61,7 +61,7 @@ StandaloneWithHistory.parameters = {
61
61
  history: { enabled: true, showThreadList: true },
62
62
  model: { showModelPicker: true },
63
63
  tools: {
64
- toolsRequiringApproval: ['kitchen_sink_get_salutation'],
64
+ toolsRequiringApproval: ['ecommerce_api_create_order'],
65
65
  },
66
66
  },
67
67
  },
@@ -69,7 +69,11 @@ export const ShadowRoot = ({
69
69
  }, [shadowRoot, elementsStyles])
70
70
 
71
71
  return (
72
- <div ref={hostRef} className={hostClassName} style={hostStyle}>
72
+ <div
73
+ ref={hostRef}
74
+ className={hostClassName}
75
+ style={{ isolation: 'isolate', ...hostStyle }}
76
+ >
73
77
  {shadowRoot
74
78
  ? createPortal(
75
79
  <div
@@ -1,16 +1,15 @@
1
- import { X, Heart } from 'lucide-react'
2
- import * as m from 'motion/react-m'
3
- import { useState, type FC } from 'react'
4
- import { AnimatePresence } from 'motion/react'
5
-
6
- import { cn } from '@/lib/utils'
7
- import { EASE_OUT_QUINT } from '@/lib/easing'
8
1
  import {
9
2
  Tooltip,
10
3
  TooltipContent,
11
4
  TooltipProvider,
12
5
  TooltipTrigger,
13
6
  } from '@/components/ui/tooltip'
7
+ import { EASE_OUT_QUINT } from '@/lib/easing'
8
+ import { cn } from '@/lib/utils'
9
+ import { Heart, X } from 'lucide-react'
10
+ import { AnimatePresence } from 'motion/react'
11
+ import * as m from 'motion/react-m'
12
+ import { useState, type FC } from 'react'
14
13
 
15
14
  export type FeedbackType = 'dislike' | 'like'
16
15
 
@@ -23,10 +23,16 @@ import {
23
23
  useAssistantState,
24
24
  } from '@assistant-ui/react'
25
25
 
26
- import { LazyMotion, MotionConfig, domAnimation } from 'motion/react'
26
+ import {
27
+ AnimatePresence,
28
+ LazyMotion,
29
+ MotionConfig,
30
+ domAnimation,
31
+ } from 'motion/react'
27
32
  import * as m from 'motion/react-m'
28
33
  import {
29
34
  createContext,
35
+ useCallback,
30
36
  useContext,
31
37
  useEffect,
32
38
  useMemo,
@@ -34,7 +40,6 @@ import {
34
40
  useState,
35
41
  type FC,
36
42
  } from 'react'
37
- import { AnimatePresence } from 'motion/react'
38
43
 
39
44
  import {
40
45
  ComposerAddAttachment,
@@ -43,25 +48,27 @@ import {
43
48
  } from '@/components/assistant-ui/attachment'
44
49
  import { FollowOnSuggestions } from '@/components/assistant-ui/follow-on-suggestions'
45
50
  import { MarkdownText } from '@/components/assistant-ui/markdown-text'
51
+ import { MentionedToolsBadges } from '@/components/assistant-ui/mentioned-tools-badges'
46
52
  import { MessageFeedback } from '@/components/assistant-ui/message-feedback'
47
53
  import { Reasoning, ReasoningGroup } from '@/components/assistant-ui/reasoning'
48
54
  import { ToolFallback } from '@/components/assistant-ui/tool-fallback'
49
55
  import { ToolMentionAutocomplete } from '@/components/assistant-ui/tool-mention-autocomplete'
50
- import { MentionedToolsBadges } from '@/components/assistant-ui/mentioned-tools-badges'
51
56
  import { TooltipIconButton } from '@/components/assistant-ui/tooltip-icon-button'
52
57
  import { Button } from '@/components/ui/button'
53
- import { useToolMentions } from '@/hooks/useToolMentions'
54
-
58
+ import { useChatId } from '@/contexts/ChatIdContext'
59
+ import { useReplayContext } from '@/contexts/ReplayContext'
60
+ import { useAuth } from '@/hooks/useAuth'
55
61
  import { useDensity } from '@/hooks/useDensity'
56
62
  import { useElements } from '@/hooks/useElements'
63
+ import { isLocalThreadId } from '@/hooks/useGramThreadListAdapter'
57
64
  import { useRadius } from '@/hooks/useRadius'
65
+ import { useRecordCassette } from '@/hooks/useRecordCassette'
58
66
  import { useThemeProps } from '@/hooks/useThemeProps'
67
+ import { useToolMentions } from '@/hooks/useToolMentions'
68
+ import { getApiUrl } from '@/lib/api'
59
69
  import { EASE_OUT_QUINT } from '@/lib/easing'
60
70
  import { MODELS } from '@/lib/models'
61
71
  import { cn } from '@/lib/utils'
62
- import { useRecordCassette } from '@/hooks/useRecordCassette'
63
- import { useReplayContext } from '@/contexts/ReplayContext'
64
- import { ConnectionStatusIndicatorSafe } from './connection-status-indicator'
65
72
  import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover'
66
73
  import {
67
74
  Tooltip,
@@ -69,8 +76,11 @@ import {
69
76
  TooltipProvider,
70
77
  TooltipTrigger,
71
78
  } from '../ui/tooltip'
79
+ import { ConnectionStatusIndicatorSafe } from './connection-status-indicator'
72
80
  import { ToolGroup } from './tool-group'
73
81
 
82
+ type Feedback = 'success' | 'failure'
83
+
74
84
  // Context for chat resolution state
75
85
  const ChatResolutionContext = createContext<{
76
86
  isResolved: boolean
@@ -78,12 +88,14 @@ const ChatResolutionContext = createContext<{
78
88
  setResolved: () => void
79
89
  setUnresolved: () => void
80
90
  resetFeedbackHidden: () => void
91
+ submitFeedback: (feedback: Feedback) => Promise<void>
81
92
  }>({
82
93
  isResolved: false,
83
94
  feedbackHidden: false,
84
95
  setResolved: () => {},
85
96
  setUnresolved: () => {},
86
97
  resetFeedbackHidden: () => {},
98
+ submitFeedback: async () => {},
87
99
  })
88
100
 
89
101
  const useChatResolution = () => useContext(ChatResolutionContext)
@@ -113,9 +125,16 @@ export const Thread: FC<ThreadProps> = ({ className }) => {
113
125
  const { config } = useElements()
114
126
  const components = config.components ?? {}
115
127
  const showStaticSessionWarning = config.api && 'sessionToken' in config.api
116
- const showFeedback = config.thread?.experimental_showFeedback ?? false
128
+ const showFeedback = config.thread?.showFeedback ?? false
117
129
  const [isResolved, setIsResolved] = useState(false)
118
130
  const [feedbackHidden, setFeedbackHidden] = useState(false)
131
+ const chatId = useChatId()
132
+
133
+ const apiUrl = getApiUrl(config)
134
+ const auth = useAuth({
135
+ auth: config.api,
136
+ projectSlug: config.projectSlug,
137
+ })
119
138
 
120
139
  const setResolved = () => setIsResolved(true)
121
140
  const setUnresolved = () => {
@@ -124,6 +143,38 @@ export const Thread: FC<ThreadProps> = ({ className }) => {
124
143
  }
125
144
  const resetFeedbackHidden = () => setFeedbackHidden(false)
126
145
 
146
+ // Submit feedback to the API
147
+ const submitFeedback = useCallback(
148
+ async (feedback: Feedback) => {
149
+ if (!chatId) return
150
+ if (isLocalThreadId(chatId)) {
151
+ console.error("Local thread ID, can't submit feedback")
152
+ return
153
+ }
154
+
155
+ try {
156
+ const response = await fetch(`${apiUrl}/rpc/chat.submitFeedback`, {
157
+ method: 'POST',
158
+ headers: {
159
+ 'Content-Type': 'application/json',
160
+ ...auth.headers,
161
+ },
162
+ body: JSON.stringify({
163
+ id: chatId,
164
+ feedback,
165
+ }),
166
+ })
167
+
168
+ if (!response.ok) {
169
+ console.error('Failed to submit feedback:', response.statusText)
170
+ }
171
+ } catch (error) {
172
+ console.error('Failed to submit feedback:', error)
173
+ }
174
+ },
175
+ [chatId, apiUrl, auth.headers]
176
+ )
177
+
127
178
  return (
128
179
  <ChatResolutionContext.Provider
129
180
  value={{
@@ -132,6 +183,7 @@ export const Thread: FC<ThreadProps> = ({ className }) => {
132
183
  setResolved,
133
184
  setUnresolved,
134
185
  resetFeedbackHidden,
186
+ submitFeedback,
135
187
  }}
136
188
  >
137
189
  <LazyMotion features={domAnimation}>
@@ -448,7 +500,16 @@ const FeedbackHiddenResetter: FC = () => {
448
500
  }
449
501
 
450
502
  const ComposerFeedback: FC = () => {
451
- const { isResolved, feedbackHidden, setResolved } = useChatResolution()
503
+ const { isResolved, feedbackHidden, setResolved, submitFeedback } =
504
+ useChatResolution()
505
+
506
+ const handleFeedback = useCallback(
507
+ async (type: 'like' | 'dislike') => {
508
+ const feedback = type === 'like' ? 'success' : 'failure'
509
+ await submitFeedback(feedback)
510
+ },
511
+ [submitFeedback]
512
+ )
452
513
 
453
514
  return (
454
515
  <ThreadPrimitive.If empty={false}>
@@ -466,7 +527,11 @@ const ComposerFeedback: FC = () => {
466
527
  transition={{ duration: 0.2, ease: EASE_OUT_QUINT }}
467
528
  className="mb-3"
468
529
  >
469
- <MessageFeedback className="mx-auto" onResolved={setResolved} />
530
+ <MessageFeedback
531
+ className="mx-auto"
532
+ onResolved={setResolved}
533
+ onFeedback={handleFeedback}
534
+ />
470
535
  </m.div>
471
536
  )}
472
537
  </AnimatePresence>
@@ -0,0 +1,21 @@
1
+ import { createContext, useContext } from 'react'
2
+
3
+ export interface ChatIdContextValue {
4
+ chatId: string | null
5
+ }
6
+
7
+ export const ChatIdContext = createContext<ChatIdContextValue | null>(null)
8
+
9
+ /**
10
+ * Hook to access the current chat ID from the Elements context.
11
+ * Works in both history-enabled and history-disabled modes.
12
+ *
13
+ * @returns The current chat ID, or null if not yet initialized
14
+ */
15
+ export const useChatId = () => {
16
+ const context = useContext(ChatIdContext)
17
+ if (!context) {
18
+ throw new Error('useChatId must be used within ElementsProvider')
19
+ }
20
+ return context.chatId
21
+ }