@devicai/ui 0.58.0 → 0.60.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.
Files changed (153) hide show
  1. package/README.md +30 -0
  2. package/dist/cjs/api/assistantInfo.js.map +1 -1
  3. package/dist/cjs/api/client.js +20 -2
  4. package/dist/cjs/api/client.js.map +1 -1
  5. package/dist/cjs/api/types.js.map +1 -1
  6. package/dist/cjs/components/AICommandBar/AICommandBar.js +2 -1
  7. package/dist/cjs/components/AICommandBar/AICommandBar.js.map +1 -1
  8. package/dist/cjs/components/AICommandBar/useAICommandBar.js +5 -1
  9. package/dist/cjs/components/AICommandBar/useAICommandBar.js.map +1 -1
  10. package/dist/cjs/components/AIElementWrapper/AIElementWrapper.js +2 -1
  11. package/dist/cjs/components/AIElementWrapper/AIElementWrapper.js.map +1 -1
  12. package/dist/cjs/components/AIElementWrapper/activeWrapperRegistry.js.map +1 -1
  13. package/dist/cjs/components/AIElementWrapper/useAIElementWrapper.js +5 -1
  14. package/dist/cjs/components/AIElementWrapper/useAIElementWrapper.js.map +1 -1
  15. package/dist/cjs/components/AIGenerationButton/AIGenerationButton.js +2 -1
  16. package/dist/cjs/components/AIGenerationButton/AIGenerationButton.js.map +1 -1
  17. package/dist/cjs/components/AIGenerationButton/useAIGenerationButton.js +5 -1
  18. package/dist/cjs/components/AIGenerationButton/useAIGenerationButton.js.map +1 -1
  19. package/dist/cjs/components/ChatDrawer/ChatDrawer.js +2 -1
  20. package/dist/cjs/components/ChatDrawer/ChatDrawer.js.map +1 -1
  21. package/dist/cjs/components/ChatDrawer/ChatInput.js.map +1 -1
  22. package/dist/cjs/components/ChatDrawer/ChatMessages.js.map +1 -1
  23. package/dist/cjs/components/ChatDrawer/CompactionWidget.js.map +1 -1
  24. package/dist/cjs/components/ChatDrawer/ConversationSelector.js.map +1 -1
  25. package/dist/cjs/components/ChatDrawer/ErrorBoundary.js.map +1 -1
  26. package/dist/cjs/components/ChatDrawer/GuardrailNotice.js.map +1 -1
  27. package/dist/cjs/components/ChatDrawer/HandoffSubagentWidget.js.map +1 -1
  28. package/dist/cjs/components/ChatDrawer/LimitBanner.js.map +1 -1
  29. package/dist/cjs/components/ChatDrawer/QueueNotice.js.map +1 -1
  30. package/dist/cjs/components/ChatDrawer/RecalledMemoriesWidget.js.map +1 -1
  31. package/dist/cjs/components/ChatDrawer/ReferenceChip.js.map +1 -1
  32. package/dist/cjs/components/ChatDrawer/ToolTimeline.js.map +1 -1
  33. package/dist/cjs/components/ChatDrawer/UsageBar.js.map +1 -1
  34. package/dist/cjs/components/ChatDrawer/pastedText.js.map +1 -1
  35. package/dist/cjs/components/CoreMemoryModal/CoreMemoryModal.js.map +1 -1
  36. package/dist/cjs/components/Feedback/FeedbackModal.js.map +1 -1
  37. package/dist/cjs/components/Feedback/MessageActions.js.map +1 -1
  38. package/dist/cjs/components/IntegrationsModal/ConnectFieldsForm.js.map +1 -1
  39. package/dist/cjs/components/IntegrationsModal/IntegrationLogo.js.map +1 -1
  40. package/dist/cjs/components/IntegrationsModal/IntegrationsHint.js.map +1 -1
  41. package/dist/cjs/components/IntegrationsModal/IntegrationsLauncher.js.map +1 -1
  42. package/dist/cjs/components/IntegrationsModal/IntegrationsModal.js.map +1 -1
  43. package/dist/cjs/components/IntegrationsModal/IntegrationsPanel.js.map +1 -1
  44. package/dist/cjs/components/IntegrationsModal/IntegrationsToggle.js.map +1 -1
  45. package/dist/cjs/components/IntegrationsModal/McpConnectForm.js.map +1 -1
  46. package/dist/cjs/components/IntegrationsModal/McpServersSection.js.map +1 -1
  47. package/dist/cjs/components/IntegrationsModal/integrationChoice.js.map +1 -1
  48. package/dist/cjs/components/IntegrationsModal/useIntegrations.js.map +1 -1
  49. package/dist/cjs/components/IntegrationsModal/useTenantMcp.js.map +1 -1
  50. package/dist/cjs/components/ThreadStateTag/ThreadStateTag.js.map +1 -1
  51. package/dist/cjs/components/theme.js.map +1 -1
  52. package/dist/cjs/hooks/useDevicChat.js +10 -2
  53. package/dist/cjs/hooks/useDevicChat.js.map +1 -1
  54. package/dist/cjs/hooks/useModelInterface.js.map +1 -1
  55. package/dist/cjs/hooks/usePolling.js +117 -7
  56. package/dist/cjs/hooks/usePolling.js.map +1 -1
  57. package/dist/cjs/hooks/useSpeechRecording.js.map +1 -1
  58. package/dist/cjs/i18n/translate.js.map +1 -1
  59. package/dist/cjs/i18n/useTranslations.js.map +1 -1
  60. package/dist/cjs/provider/DevicContext.js.map +1 -1
  61. package/dist/cjs/provider/DevicProvider.js +3 -1
  62. package/dist/cjs/provider/DevicProvider.js.map +1 -1
  63. package/dist/cjs/provider/sharedSession.js.map +1 -1
  64. package/dist/cjs/utils/avatar.js.map +1 -1
  65. package/dist/cjs/utils/consumeChatStream.js +74 -0
  66. package/dist/cjs/utils/consumeChatStream.js.map +1 -0
  67. package/dist/cjs/utils/index.js.map +1 -1
  68. package/dist/cjs/utils/logger.js.map +1 -1
  69. package/dist/cjs/utils/toolGroups.js.map +1 -1
  70. package/dist/esm/api/assistantInfo.js.map +1 -1
  71. package/dist/esm/api/client.d.ts +5 -1
  72. package/dist/esm/api/client.js +20 -2
  73. package/dist/esm/api/client.js.map +1 -1
  74. package/dist/esm/api/types.d.ts +2 -0
  75. package/dist/esm/api/types.js.map +1 -1
  76. package/dist/esm/components/AICommandBar/AICommandBar.js +2 -1
  77. package/dist/esm/components/AICommandBar/AICommandBar.js.map +1 -1
  78. package/dist/esm/components/AICommandBar/AICommandBar.types.d.ts +6 -0
  79. package/dist/esm/components/AICommandBar/useAICommandBar.d.ts +2 -0
  80. package/dist/esm/components/AICommandBar/useAICommandBar.js +6 -2
  81. package/dist/esm/components/AICommandBar/useAICommandBar.js.map +1 -1
  82. package/dist/esm/components/AIElementWrapper/AIElementWrapper.js +2 -1
  83. package/dist/esm/components/AIElementWrapper/AIElementWrapper.js.map +1 -1
  84. package/dist/esm/components/AIElementWrapper/AIElementWrapper.types.d.ts +6 -0
  85. package/dist/esm/components/AIElementWrapper/activeWrapperRegistry.js.map +1 -1
  86. package/dist/esm/components/AIElementWrapper/useAIElementWrapper.d.ts +2 -0
  87. package/dist/esm/components/AIElementWrapper/useAIElementWrapper.js +6 -2
  88. package/dist/esm/components/AIElementWrapper/useAIElementWrapper.js.map +1 -1
  89. package/dist/esm/components/AIGenerationButton/AIGenerationButton.js +2 -1
  90. package/dist/esm/components/AIGenerationButton/AIGenerationButton.js.map +1 -1
  91. package/dist/esm/components/AIGenerationButton/AIGenerationButton.types.d.ts +6 -0
  92. package/dist/esm/components/AIGenerationButton/useAIGenerationButton.d.ts +2 -0
  93. package/dist/esm/components/AIGenerationButton/useAIGenerationButton.js +6 -2
  94. package/dist/esm/components/AIGenerationButton/useAIGenerationButton.js.map +1 -1
  95. package/dist/esm/components/ChatDrawer/ChatDrawer.js +2 -1
  96. package/dist/esm/components/ChatDrawer/ChatDrawer.js.map +1 -1
  97. package/dist/esm/components/ChatDrawer/ChatDrawer.types.d.ts +8 -0
  98. package/dist/esm/components/ChatDrawer/ChatInput.js.map +1 -1
  99. package/dist/esm/components/ChatDrawer/ChatMessages.js.map +1 -1
  100. package/dist/esm/components/ChatDrawer/CompactionWidget.js.map +1 -1
  101. package/dist/esm/components/ChatDrawer/ConversationSelector.js.map +1 -1
  102. package/dist/esm/components/ChatDrawer/ErrorBoundary.js.map +1 -1
  103. package/dist/esm/components/ChatDrawer/GuardrailNotice.js.map +1 -1
  104. package/dist/esm/components/ChatDrawer/HandoffSubagentWidget.js.map +1 -1
  105. package/dist/esm/components/ChatDrawer/LimitBanner.js.map +1 -1
  106. package/dist/esm/components/ChatDrawer/QueueNotice.js.map +1 -1
  107. package/dist/esm/components/ChatDrawer/RecalledMemoriesWidget.js.map +1 -1
  108. package/dist/esm/components/ChatDrawer/ReferenceChip.js.map +1 -1
  109. package/dist/esm/components/ChatDrawer/ToolTimeline.js.map +1 -1
  110. package/dist/esm/components/ChatDrawer/UsageBar.js.map +1 -1
  111. package/dist/esm/components/ChatDrawer/pastedText.js.map +1 -1
  112. package/dist/esm/components/CoreMemoryModal/CoreMemoryModal.js.map +1 -1
  113. package/dist/esm/components/Feedback/FeedbackModal.js.map +1 -1
  114. package/dist/esm/components/Feedback/MessageActions.js.map +1 -1
  115. package/dist/esm/components/IntegrationsModal/ConnectFieldsForm.js.map +1 -1
  116. package/dist/esm/components/IntegrationsModal/IntegrationLogo.js.map +1 -1
  117. package/dist/esm/components/IntegrationsModal/IntegrationsHint.js.map +1 -1
  118. package/dist/esm/components/IntegrationsModal/IntegrationsLauncher.js.map +1 -1
  119. package/dist/esm/components/IntegrationsModal/IntegrationsModal.js.map +1 -1
  120. package/dist/esm/components/IntegrationsModal/IntegrationsPanel.js.map +1 -1
  121. package/dist/esm/components/IntegrationsModal/IntegrationsToggle.js.map +1 -1
  122. package/dist/esm/components/IntegrationsModal/McpConnectForm.js.map +1 -1
  123. package/dist/esm/components/IntegrationsModal/McpServersSection.js.map +1 -1
  124. package/dist/esm/components/IntegrationsModal/integrationChoice.js.map +1 -1
  125. package/dist/esm/components/IntegrationsModal/useIntegrations.js.map +1 -1
  126. package/dist/esm/components/IntegrationsModal/useTenantMcp.js.map +1 -1
  127. package/dist/esm/components/ThreadStateTag/ThreadStateTag.js.map +1 -1
  128. package/dist/esm/components/theme.js.map +1 -1
  129. package/dist/esm/hooks/index.d.ts +1 -1
  130. package/dist/esm/hooks/useDevicChat.d.ts +6 -0
  131. package/dist/esm/hooks/useDevicChat.js +12 -4
  132. package/dist/esm/hooks/useDevicChat.js.map +1 -1
  133. package/dist/esm/hooks/useModelInterface.js.map +1 -1
  134. package/dist/esm/hooks/usePolling.d.ts +35 -0
  135. package/dist/esm/hooks/usePolling.js +114 -8
  136. package/dist/esm/hooks/usePolling.js.map +1 -1
  137. package/dist/esm/hooks/useSpeechRecording.js.map +1 -1
  138. package/dist/esm/i18n/translate.js.map +1 -1
  139. package/dist/esm/i18n/useTranslations.js.map +1 -1
  140. package/dist/esm/provider/DevicContext.js.map +1 -1
  141. package/dist/esm/provider/DevicProvider.d.ts +1 -1
  142. package/dist/esm/provider/DevicProvider.js +3 -1
  143. package/dist/esm/provider/DevicProvider.js.map +1 -1
  144. package/dist/esm/provider/sharedSession.js.map +1 -1
  145. package/dist/esm/provider/types.d.ts +18 -0
  146. package/dist/esm/utils/avatar.js.map +1 -1
  147. package/dist/esm/utils/consumeChatStream.d.ts +10 -0
  148. package/dist/esm/utils/consumeChatStream.js +72 -0
  149. package/dist/esm/utils/consumeChatStream.js.map +1 -0
  150. package/dist/esm/utils/index.js.map +1 -1
  151. package/dist/esm/utils/logger.js.map +1 -1
  152. package/dist/esm/utils/toolGroups.js.map +1 -1
  153. package/package.json +3 -1
@@ -11,6 +11,14 @@ const DEFAULT_POLLING_INTERVAL_MS = 1000;
11
11
  * request loop against the API rather than a faster chat.
12
12
  */
13
13
  const MIN_POLLING_INTERVAL_MS = 250;
14
+ /**
15
+ * A stream that has carried nothing (no snapshot, no keep-alive) for this long
16
+ * is presumed dead and reopened. The API sends a keep-alive every 15 s, so a
17
+ * healthy connection never gets near it.
18
+ */
19
+ const DEFAULT_STREAM_SILENCE_MS = 30000;
20
+ /** Pause between a stream ending (the server's lifetime) and the next one. */
21
+ const STREAM_RECONNECT_DELAY_MS = 250;
14
22
  /**
15
23
  * Picks the first usable cadence out of the candidates, in priority order
16
24
  * (component prop, then provider, then the caller's own default), ignoring
@@ -25,6 +33,24 @@ function resolvePollingInterval(...candidates) {
25
33
  }
26
34
  return DEFAULT_POLLING_INTERVAL_MS;
27
35
  }
36
+ /**
37
+ * Whether a conversation in progress is followed over a server-sent event
38
+ * stream instead of polled. Off until the integrator opts in; the flag will
39
+ * default to on once the streaming endpoint has been exercised in the field.
40
+ */
41
+ const DEFAULT_STREAMING = false;
42
+ /**
43
+ * Picks the first explicit choice out of the candidates, in priority order
44
+ * (component prop, then provider), ignoring anything that is not a boolean
45
+ * and falling back to `DEFAULT_STREAMING`.
46
+ */
47
+ function resolveStreaming(...candidates) {
48
+ for (const value of candidates) {
49
+ if (typeof value === 'boolean')
50
+ return value;
51
+ }
52
+ return DEFAULT_STREAMING;
53
+ }
28
54
  /**
29
55
  * Hook for polling real-time chat history
30
56
  *
@@ -42,6 +68,18 @@ function usePolling(chatUid, fetchFn, options = {}) {
42
68
  const [error, setError] = useState(null);
43
69
  const intervalRef = useRef(null);
44
70
  const isMountedRef = useRef(true);
71
+ const chatUidRef = useRef(chatUid);
72
+ chatUidRef.current = chatUid;
73
+ const streamFnRef = useRef(options.streamFn);
74
+ streamFnRef.current = options.streamFn;
75
+ const lastStreamAt = useRef(0);
76
+ // While a stream connection is open the timer makes no request at all: the
77
+ // stream is the source, the poll only the recovery.
78
+ const streamAliveRef = useRef(false);
79
+ const lastStreamActivity = useRef(0);
80
+ const streamAttemptRef = useRef(null);
81
+ const streamSilenceRef = useRef(options.streamSilenceMs ?? DEFAULT_STREAM_SILENCE_MS);
82
+ streamSilenceRef.current = options.streamSilenceMs ?? DEFAULT_STREAM_SILENCE_MS;
45
83
  // Refs for callbacks and options to avoid stale closures and unnecessary re-renders
46
84
  const onStopRef = useRef(onStop);
47
85
  const holdOpenRef = useRef(holdOpen);
@@ -70,13 +108,28 @@ function usePolling(chatUid, fetchFn, options = {}) {
70
108
  armedIntervalRef.current = null;
71
109
  isPollingRef.current = false;
72
110
  }, []);
73
- const fetchData = useCallback(async () => {
111
+ const fetchData = useCallback(async (snapshot, force = false) => {
74
112
  logRef.current.log('[usePolling] fetchData called, isMounted:', isMountedRef.current);
75
113
  if (!isMountedRef.current)
76
114
  return;
77
115
  try {
78
116
  logRef.current.log('[usePolling] Fetching...');
79
- const result = await fetchFnRef.current();
117
+ const started = Date.now();
118
+ const startedChat = chatUidRef.current;
119
+ if (!snapshot && !force && streamAliveRef.current) {
120
+ // A stream that has gone quiet for too long is presumed dead: drop it
121
+ // so the loop below reopens it. Until then, nothing is asked.
122
+ if (started - lastStreamActivity.current > streamSilenceRef.current) {
123
+ logRef.current.log('[usePolling] Stream silent for too long, reconnecting');
124
+ streamAttemptRef.current?.abort();
125
+ }
126
+ return;
127
+ }
128
+ const result = snapshot || await fetchFnRef.current();
129
+ if (startedChat !== chatUidRef.current)
130
+ return;
131
+ if (!snapshot && lastStreamAt.current > started)
132
+ return;
80
133
  logRef.current.log('[usePolling] Fetch result:', { status: result.status, messageCount: result.chatHistory?.length });
81
134
  if (!isMountedRef.current)
82
135
  return;
@@ -106,14 +159,65 @@ function usePolling(chatUid, fetchFn, options = {}) {
106
159
  setIsPolling(false);
107
160
  }
108
161
  }, [clearPolling]);
162
+ useEffect(() => {
163
+ if (!enabled || !chatUid || !isPolling || !streamFnRef.current)
164
+ return;
165
+ const controller = new AbortController();
166
+ lastStreamAt.current = 0;
167
+ const leave = () => {
168
+ streamAliveRef.current = false;
169
+ streamAttemptRef.current = null;
170
+ lastStreamAt.current = 0;
171
+ };
172
+ const connect = async () => {
173
+ while (!controller.signal.aborted && isPollingRef.current) {
174
+ // One controller per attempt, so the silence watchdog can drop a dead
175
+ // connection without ending the loop.
176
+ const attempt = new AbortController();
177
+ const abortAttempt = () => attempt.abort();
178
+ controller.signal.addEventListener('abort', abortAttempt);
179
+ streamAttemptRef.current = attempt;
180
+ streamAliveRef.current = true;
181
+ lastStreamActivity.current = Date.now();
182
+ let failed = false;
183
+ try {
184
+ await streamFnRef.current(async (snapshot) => {
185
+ if (attempt.signal.aborted || !isPollingRef.current)
186
+ return;
187
+ lastStreamAt.current = Date.now();
188
+ lastStreamActivity.current = lastStreamAt.current;
189
+ await fetchData(snapshot);
190
+ }, attempt.signal, () => { lastStreamActivity.current = Date.now(); });
191
+ }
192
+ catch {
193
+ // Our own abort is a reconnect; anything else means the stream is
194
+ // not available here, and the poll is the recovery path.
195
+ failed = !attempt.signal.aborted;
196
+ }
197
+ finally {
198
+ controller.signal.removeEventListener('abort', abortAttempt);
199
+ }
200
+ leave();
201
+ if (failed || controller.signal.aborted)
202
+ break;
203
+ // The server closes a stream after its lifetime; reopen right away.
204
+ await new Promise(resolve => setTimeout(resolve, STREAM_RECONNECT_DELAY_MS));
205
+ }
206
+ leave();
207
+ };
208
+ void connect();
209
+ return () => { controller.abort(); leave(); };
210
+ }, [enabled, chatUid, isPolling, fetchData]);
109
211
  const start = useCallback(() => {
110
212
  if (intervalRef.current)
111
213
  return;
112
214
  isPollingRef.current = true;
113
215
  setIsPolling(true);
114
216
  setError(null);
115
- // Immediate first fetch
116
- fetchData();
217
+ // Immediate first fetch — unless a stream is about to open, whose first
218
+ // frame is that same state.
219
+ if (!streamFnRef.current)
220
+ fetchData();
117
221
  // Set up interval
118
222
  armedIntervalRef.current = intervalValueRef.current;
119
223
  intervalRef.current = setInterval(fetchData, intervalValueRef.current);
@@ -123,7 +227,7 @@ function usePolling(chatUid, fetchFn, options = {}) {
123
227
  setIsPolling(false);
124
228
  }, [clearPolling]);
125
229
  const refetch = useCallback(async () => {
126
- await fetchData();
230
+ await fetchData(undefined, true);
127
231
  }, [fetchData]);
128
232
  // Auto-start polling when enabled and chatUid is set
129
233
  useEffect(() => {
@@ -143,8 +247,10 @@ function usePolling(chatUid, fetchFn, options = {}) {
143
247
  isPollingRef.current = true;
144
248
  setIsPolling(true);
145
249
  setError(null);
146
- // Immediate first fetch
147
- fetchData();
250
+ // Immediate first fetch — unless a stream is about to open, whose first
251
+ // frame is that same state.
252
+ if (!streamFnRef.current)
253
+ fetchData();
148
254
  // Set up interval
149
255
  armedIntervalRef.current = intervalValueRef.current;
150
256
  intervalRef.current = setInterval(fetchData, intervalValueRef.current);
@@ -190,5 +296,5 @@ function usePolling(chatUid, fetchFn, options = {}) {
190
296
  };
191
297
  }
192
298
 
193
- export { DEFAULT_POLLING_INTERVAL_MS, MIN_POLLING_INTERVAL_MS, resolvePollingInterval, usePolling };
299
+ export { DEFAULT_POLLING_INTERVAL_MS, DEFAULT_STREAMING, DEFAULT_STREAM_SILENCE_MS, MIN_POLLING_INTERVAL_MS, STREAM_RECONNECT_DELAY_MS, resolvePollingInterval, resolveStreaming, usePolling };
194
300
  //# sourceMappingURL=usePolling.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"usePolling.js","sources":["../../../src/hooks/usePolling.ts"],"sourcesContent":["import { useState, useEffect, useRef, useCallback, useMemo } from 'react';\nimport type { RealtimeChatHistory, RealtimeStatus } from '../api/types';\nimport { createLogger } from '../utils/logger';\n\n/**\n * Cadence used when nobody configures one.\n */\nexport const DEFAULT_POLLING_INTERVAL_MS = 1000;\n\n/**\n * Floor applied to any configured cadence. A conversation in flight is polled\n * from every mounted component, so a value below this turns the widget into a\n * request loop against the API rather than a faster chat.\n */\nexport const MIN_POLLING_INTERVAL_MS = 250;\n\n/**\n * Picks the first usable cadence out of the candidates, in priority order\n * (component prop, then provider, then the caller's own default), ignoring\n * anything that is not a positive finite number and clamping the winner to\n * `MIN_POLLING_INTERVAL_MS`.\n */\nexport function resolvePollingInterval(\n ...candidates: Array<number | undefined | null>\n): number {\n for (const value of candidates) {\n if (typeof value === 'number' && Number.isFinite(value) && value > 0) {\n return Math.max(value, MIN_POLLING_INTERVAL_MS);\n }\n }\n return DEFAULT_POLLING_INTERVAL_MS;\n}\n\nexport interface UsePollingOptions {\n /**\n * Polling interval in milliseconds\n * @default 1000\n */\n interval?: number;\n\n /**\n * Whether polling is enabled\n * @default true\n */\n enabled?: boolean;\n\n /**\n * Statuses that should stop polling\n * @default ['completed', 'error']\n */\n stopStatuses?: RealtimeStatus[];\n\n /**\n * Consulted when the status says the conversation is done. Returning true\n * keeps the poll running and skips `onStop`.\n *\n * A run can settle while the conversation still owes an answer to something\n * queued: between one run liquidating and the follow-up run marking itself as\n * processing, the realtime state reads as finished. Stopping in that window\n * leaves the answer invisible until a reload. Whoever passes this is\n * responsible for bounding it — the hook will hold open for as long as it is\n * told to.\n */\n holdOpen?: (data: RealtimeChatHistory) => boolean;\n\n /**\n * Callback when polling stops\n */\n onStop?: (data: RealtimeChatHistory | null) => void;\n\n /**\n * Callback on each poll update\n */\n onUpdate?: (data: RealtimeChatHistory) => void;\n\n /**\n * Callback on poll error\n */\n onError?: (error: Error) => void;\n\n /**\n * Enable debug logging\n * @default false\n */\n debug?: boolean;\n}\n\nexport interface UsePollingResult {\n /**\n * Current polling data\n */\n data: RealtimeChatHistory | null;\n\n /**\n * Whether polling is currently active\n */\n isPolling: boolean;\n\n /**\n * Last error that occurred\n */\n error: Error | null;\n\n /**\n * Start polling\n */\n start: () => void;\n\n /**\n * Stop polling\n */\n stop: () => void;\n\n /**\n * Manually trigger a fetch\n */\n refetch: () => Promise<void>;\n}\n\n/**\n * Hook for polling real-time chat history\n *\n * @param chatUid - The chat UID to poll for\n * @param fetchFn - Function that fetches the realtime history\n * @param options - Polling options\n */\nexport function usePolling(\n chatUid: string | null,\n fetchFn: () => Promise<RealtimeChatHistory>,\n options: UsePollingOptions = {}\n): UsePollingResult {\n const {\n interval = DEFAULT_POLLING_INTERVAL_MS,\n enabled = true,\n stopStatuses = ['completed', 'error'],\n holdOpen,\n onStop,\n onUpdate,\n onError,\n debug = false,\n } = options;\n\n const log = useMemo(() => createLogger(debug), [debug]);\n const logRef = useRef(log);\n logRef.current = log;\n\n const [data, setData] = useState<RealtimeChatHistory | null>(null);\n const [isPolling, setIsPolling] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);\n const isMountedRef = useRef(true);\n\n // Refs for callbacks and options to avoid stale closures and unnecessary re-renders\n const onStopRef = useRef(onStop);\n const holdOpenRef = useRef(holdOpen);\n const onUpdateRef = useRef(onUpdate);\n const onErrorRef = useRef(onError);\n const fetchFnRef = useRef(fetchFn);\n const stopStatusesRef = useRef(stopStatuses);\n const intervalValueRef = useRef(interval);\n const isPollingRef = useRef(false);\n // Cadence the running timer was armed with, so a change can be detected.\n const armedIntervalRef = useRef<number | null>(null);\n\n useEffect(() => {\n onStopRef.current = onStop;\n holdOpenRef.current = holdOpen;\n onUpdateRef.current = onUpdate;\n onErrorRef.current = onError;\n fetchFnRef.current = fetchFn;\n stopStatusesRef.current = stopStatuses;\n intervalValueRef.current = interval;\n });\n\n const clearPolling = useCallback(() => {\n if (intervalRef.current) {\n clearInterval(intervalRef.current);\n intervalRef.current = null;\n }\n armedIntervalRef.current = null;\n isPollingRef.current = false;\n }, []);\n\n const fetchData = useCallback(async () => {\n logRef.current.log('[usePolling] fetchData called, isMounted:', isMountedRef.current);\n if (!isMountedRef.current) return;\n\n try {\n logRef.current.log('[usePolling] Fetching...');\n const result = await fetchFnRef.current();\n logRef.current.log('[usePolling] Fetch result:', { status: result.status, messageCount: result.chatHistory?.length });\n\n if (!isMountedRef.current) return;\n\n setData(result);\n setError(null);\n onUpdateRef.current?.(result);\n\n // Check if we should stop polling\n const shouldStop =\n stopStatusesRef.current.includes(result.status) &&\n !holdOpenRef.current?.(result);\n logRef.current.log('[usePolling] Should stop?', shouldStop, 'stopStatuses:', stopStatusesRef.current, 'current status:', result.status);\n if (shouldStop) {\n logRef.current.log('[usePolling] Stopping polling due to status:', result.status);\n clearPolling();\n setIsPolling(false);\n onStopRef.current?.(result);\n }\n } catch (err) {\n logRef.current.error('[usePolling] Fetch error:', err);\n if (!isMountedRef.current) return;\n\n const error = err instanceof Error ? err : new Error(String(err));\n setError(error);\n onErrorRef.current?.(error);\n\n // Stop polling on error\n clearPolling();\n setIsPolling(false);\n }\n }, [clearPolling]);\n\n const start = useCallback(() => {\n if (intervalRef.current) return;\n\n isPollingRef.current = true;\n setIsPolling(true);\n setError(null);\n\n // Immediate first fetch\n fetchData();\n\n // Set up interval\n armedIntervalRef.current = intervalValueRef.current;\n intervalRef.current = setInterval(fetchData, intervalValueRef.current);\n }, [fetchData]);\n\n const stop = useCallback(() => {\n clearPolling();\n setIsPolling(false);\n }, [clearPolling]);\n\n const refetch = useCallback(async () => {\n await fetchData();\n }, [fetchData]);\n\n // Auto-start polling when enabled and chatUid is set\n useEffect(() => {\n logRef.current.log('[usePolling] Auto-start effect triggered:', { enabled, chatUid, isPollingRef: isPollingRef.current, intervalRef: !!intervalRef.current });\n\n if (!enabled || !chatUid) {\n logRef.current.log('[usePolling] Not enabled or no chatUid, stopping if active');\n // Stop polling if disabled or no chatUid\n if (isPollingRef.current) {\n clearPolling();\n setIsPolling(false);\n }\n return;\n }\n\n // Start polling if not already polling\n if (!isPollingRef.current) {\n logRef.current.log('[usePolling] Starting polling, interval:', intervalValueRef.current);\n isPollingRef.current = true;\n setIsPolling(true);\n setError(null);\n\n // Immediate first fetch\n fetchData();\n\n // Set up interval\n armedIntervalRef.current = intervalValueRef.current;\n intervalRef.current = setInterval(fetchData, intervalValueRef.current);\n logRef.current.log('[usePolling] Interval set:', intervalRef.current);\n } else {\n logRef.current.log('[usePolling] Already polling, skipping start');\n }\n\n // Only cleanup on unmount, not on every dependency change\n }, [enabled, chatUid, fetchData, clearPolling]);\n\n // Re-arm the timer when the cadence changes while a conversation is already\n // in flight. Without this a new `interval` would only take effect on the next\n // conversation, which is not what changing a provider setting looks like.\n useEffect(() => {\n if (!isPollingRef.current || armedIntervalRef.current === null) return;\n if (armedIntervalRef.current === interval) return;\n\n logRef.current.log('[usePolling] Interval changed mid-flight:', {\n from: armedIntervalRef.current,\n to: interval,\n });\n if (intervalRef.current) clearInterval(intervalRef.current);\n armedIntervalRef.current = interval;\n intervalRef.current = setInterval(fetchData, interval);\n }, [interval, fetchData]);\n\n // Cleanup on unmount\n useEffect(() => {\n isMountedRef.current = true;\n return () => {\n isMountedRef.current = false;\n clearPolling();\n };\n }, [clearPolling]);\n\n return {\n data,\n isPolling,\n error,\n start,\n stop,\n refetch,\n };\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"usePolling.js","sources":["../../../src/hooks/usePolling.ts"],"sourcesContent":["import { useState, useEffect, useRef, useCallback, useMemo } from 'react';\nimport type { RealtimeChatHistory, RealtimeStatus } from '../api/types';\nimport { createLogger } from '../utils/logger';\n\n/**\n * Cadence used when nobody configures one.\n */\nexport const DEFAULT_POLLING_INTERVAL_MS = 1000;\n\n/**\n * Floor applied to any configured cadence. A conversation in flight is polled\n * from every mounted component, so a value below this turns the widget into a\n * request loop against the API rather than a faster chat.\n */\nexport const MIN_POLLING_INTERVAL_MS = 250;\n\n/**\n * A stream that has carried nothing (no snapshot, no keep-alive) for this long\n * is presumed dead and reopened. The API sends a keep-alive every 15 s, so a\n * healthy connection never gets near it.\n */\nexport const DEFAULT_STREAM_SILENCE_MS = 30_000;\n\n/** Pause between a stream ending (the server's lifetime) and the next one. */\nexport const STREAM_RECONNECT_DELAY_MS = 250;\n\n/**\n * Picks the first usable cadence out of the candidates, in priority order\n * (component prop, then provider, then the caller's own default), ignoring\n * anything that is not a positive finite number and clamping the winner to\n * `MIN_POLLING_INTERVAL_MS`.\n */\nexport function resolvePollingInterval(\n ...candidates: Array<number | undefined | null>\n): number {\n for (const value of candidates) {\n if (typeof value === 'number' && Number.isFinite(value) && value > 0) {\n return Math.max(value, MIN_POLLING_INTERVAL_MS);\n }\n }\n return DEFAULT_POLLING_INTERVAL_MS;\n}\n\n/**\n * Whether a conversation in progress is followed over a server-sent event\n * stream instead of polled. Off until the integrator opts in; the flag will\n * default to on once the streaming endpoint has been exercised in the field.\n */\nexport const DEFAULT_STREAMING = false;\n\n/**\n * Picks the first explicit choice out of the candidates, in priority order\n * (component prop, then provider), ignoring anything that is not a boolean\n * and falling back to `DEFAULT_STREAMING`.\n */\nexport function resolveStreaming(\n ...candidates: Array<boolean | undefined | null>\n): boolean {\n for (const value of candidates) {\n if (typeof value === 'boolean') return value;\n }\n return DEFAULT_STREAMING;\n}\n\nexport interface UsePollingOptions {\n /**\n * Opens a server-sent event stream for the conversation and feeds every\n * snapshot it carries. While the connection is open the timer below makes\n * no request at all; when the stream ends it is reopened, and only when it\n * fails does the timer take over. `onActivity` reports every chunk received\n * (keep-alives included) so a silent connection can be told from a dead one.\n * Absent, the hook only polls.\n */\n streamFn?: (\n onSnapshot: (data: RealtimeChatHistory) => Promise<void>,\n signal: AbortSignal,\n onActivity?: () => void,\n ) => Promise<void>;\n\n /**\n * How long (ms) an open stream may stay silent before it is presumed dead\n * and reopened.\n * @default 30000\n */\n streamSilenceMs?: number;\n /**\n * Polling interval in milliseconds\n * @default 1000\n */\n interval?: number;\n\n /**\n * Whether polling is enabled\n * @default true\n */\n enabled?: boolean;\n\n /**\n * Statuses that should stop polling\n * @default ['completed', 'error']\n */\n stopStatuses?: RealtimeStatus[];\n\n /**\n * Consulted when the status says the conversation is done. Returning true\n * keeps the poll running and skips `onStop`.\n *\n * A run can settle while the conversation still owes an answer to something\n * queued: between one run liquidating and the follow-up run marking itself as\n * processing, the realtime state reads as finished. Stopping in that window\n * leaves the answer invisible until a reload. Whoever passes this is\n * responsible for bounding it — the hook will hold open for as long as it is\n * told to.\n */\n holdOpen?: (data: RealtimeChatHistory) => boolean;\n\n /**\n * Callback when polling stops\n */\n onStop?: (data: RealtimeChatHistory | null) => void;\n\n /**\n * Callback on each poll update\n */\n onUpdate?: (data: RealtimeChatHistory) => void;\n\n /**\n * Callback on poll error\n */\n onError?: (error: Error) => void;\n\n /**\n * Enable debug logging\n * @default false\n */\n debug?: boolean;\n}\n\nexport interface UsePollingResult {\n /**\n * Current polling data\n */\n data: RealtimeChatHistory | null;\n\n /**\n * Whether polling is currently active\n */\n isPolling: boolean;\n\n /**\n * Last error that occurred\n */\n error: Error | null;\n\n /**\n * Start polling\n */\n start: () => void;\n\n /**\n * Stop polling\n */\n stop: () => void;\n\n /**\n * Manually trigger a fetch\n */\n refetch: () => Promise<void>;\n}\n\n/**\n * Hook for polling real-time chat history\n *\n * @param chatUid - The chat UID to poll for\n * @param fetchFn - Function that fetches the realtime history\n * @param options - Polling options\n */\nexport function usePolling(\n chatUid: string | null,\n fetchFn: () => Promise<RealtimeChatHistory>,\n options: UsePollingOptions = {}\n): UsePollingResult {\n const {\n interval = DEFAULT_POLLING_INTERVAL_MS,\n enabled = true,\n stopStatuses = ['completed', 'error'],\n holdOpen,\n onStop,\n onUpdate,\n onError,\n debug = false,\n } = options;\n\n const log = useMemo(() => createLogger(debug), [debug]);\n const logRef = useRef(log);\n logRef.current = log;\n\n const [data, setData] = useState<RealtimeChatHistory | null>(null);\n const [isPolling, setIsPolling] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);\n const isMountedRef = useRef(true);\n const chatUidRef = useRef(chatUid);\n chatUidRef.current = chatUid;\n const streamFnRef = useRef(options.streamFn);\n streamFnRef.current = options.streamFn;\n const lastStreamAt = useRef(0);\n // While a stream connection is open the timer makes no request at all: the\n // stream is the source, the poll only the recovery.\n const streamAliveRef = useRef(false);\n const lastStreamActivity = useRef(0);\n const streamAttemptRef = useRef<AbortController | null>(null);\n const streamSilenceRef = useRef(options.streamSilenceMs ?? DEFAULT_STREAM_SILENCE_MS);\n streamSilenceRef.current = options.streamSilenceMs ?? DEFAULT_STREAM_SILENCE_MS;\n\n // Refs for callbacks and options to avoid stale closures and unnecessary re-renders\n const onStopRef = useRef(onStop);\n const holdOpenRef = useRef(holdOpen);\n const onUpdateRef = useRef(onUpdate);\n const onErrorRef = useRef(onError);\n const fetchFnRef = useRef(fetchFn);\n const stopStatusesRef = useRef(stopStatuses);\n const intervalValueRef = useRef(interval);\n const isPollingRef = useRef(false);\n // Cadence the running timer was armed with, so a change can be detected.\n const armedIntervalRef = useRef<number | null>(null);\n\n useEffect(() => {\n onStopRef.current = onStop;\n holdOpenRef.current = holdOpen;\n onUpdateRef.current = onUpdate;\n onErrorRef.current = onError;\n fetchFnRef.current = fetchFn;\n stopStatusesRef.current = stopStatuses;\n intervalValueRef.current = interval;\n });\n\n const clearPolling = useCallback(() => {\n if (intervalRef.current) {\n clearInterval(intervalRef.current);\n intervalRef.current = null;\n }\n armedIntervalRef.current = null;\n isPollingRef.current = false;\n }, []);\n\n const fetchData = useCallback(async (snapshot?: RealtimeChatHistory, force = false) => {\n logRef.current.log('[usePolling] fetchData called, isMounted:', isMountedRef.current);\n if (!isMountedRef.current) return;\n\n try {\n logRef.current.log('[usePolling] Fetching...');\n const started = Date.now();\n const startedChat = chatUidRef.current;\n if (!snapshot && !force && streamAliveRef.current) {\n // A stream that has gone quiet for too long is presumed dead: drop it\n // so the loop below reopens it. Until then, nothing is asked.\n if (started - lastStreamActivity.current > streamSilenceRef.current) {\n logRef.current.log('[usePolling] Stream silent for too long, reconnecting');\n streamAttemptRef.current?.abort();\n }\n return;\n }\n const result = snapshot || await fetchFnRef.current();\n if (startedChat !== chatUidRef.current) return;\n if (!snapshot && lastStreamAt.current > started) return;\n logRef.current.log('[usePolling] Fetch result:', { status: result.status, messageCount: result.chatHistory?.length });\n\n if (!isMountedRef.current) return;\n\n setData(result);\n setError(null);\n onUpdateRef.current?.(result);\n\n // Check if we should stop polling\n const shouldStop =\n stopStatusesRef.current.includes(result.status) &&\n !holdOpenRef.current?.(result);\n logRef.current.log('[usePolling] Should stop?', shouldStop, 'stopStatuses:', stopStatusesRef.current, 'current status:', result.status);\n if (shouldStop) {\n logRef.current.log('[usePolling] Stopping polling due to status:', result.status);\n clearPolling();\n setIsPolling(false);\n onStopRef.current?.(result);\n }\n } catch (err) {\n logRef.current.error('[usePolling] Fetch error:', err);\n if (!isMountedRef.current) return;\n\n const error = err instanceof Error ? err : new Error(String(err));\n setError(error);\n onErrorRef.current?.(error);\n\n // Stop polling on error\n clearPolling();\n setIsPolling(false);\n }\n }, [clearPolling]);\n\n useEffect(() => {\n if (!enabled || !chatUid || !isPolling || !streamFnRef.current) return;\n const controller = new AbortController();\n lastStreamAt.current = 0;\n const leave = () => {\n streamAliveRef.current = false;\n streamAttemptRef.current = null;\n lastStreamAt.current = 0;\n };\n const connect = async () => {\n while (!controller.signal.aborted && isPollingRef.current) {\n // One controller per attempt, so the silence watchdog can drop a dead\n // connection without ending the loop.\n const attempt = new AbortController();\n const abortAttempt = () => attempt.abort();\n controller.signal.addEventListener('abort', abortAttempt);\n streamAttemptRef.current = attempt;\n streamAliveRef.current = true;\n lastStreamActivity.current = Date.now();\n let failed = false;\n try {\n await streamFnRef.current!(\n async snapshot => {\n if (attempt.signal.aborted || !isPollingRef.current) return;\n lastStreamAt.current = Date.now();\n lastStreamActivity.current = lastStreamAt.current;\n await fetchData(snapshot);\n },\n attempt.signal,\n () => { lastStreamActivity.current = Date.now(); },\n );\n } catch {\n // Our own abort is a reconnect; anything else means the stream is\n // not available here, and the poll is the recovery path.\n failed = !attempt.signal.aborted;\n } finally {\n controller.signal.removeEventListener('abort', abortAttempt);\n }\n leave();\n if (failed || controller.signal.aborted) break;\n // The server closes a stream after its lifetime; reopen right away.\n await new Promise(resolve => setTimeout(resolve, STREAM_RECONNECT_DELAY_MS));\n }\n leave();\n };\n void connect();\n return () => { controller.abort(); leave(); };\n }, [enabled, chatUid, isPolling, fetchData]);\n\n const start = useCallback(() => {\n if (intervalRef.current) return;\n\n isPollingRef.current = true;\n setIsPolling(true);\n setError(null);\n\n // Immediate first fetch — unless a stream is about to open, whose first\n // frame is that same state.\n if (!streamFnRef.current) fetchData();\n\n // Set up interval\n armedIntervalRef.current = intervalValueRef.current;\n intervalRef.current = setInterval(fetchData, intervalValueRef.current);\n }, [fetchData]);\n\n const stop = useCallback(() => {\n clearPolling();\n setIsPolling(false);\n }, [clearPolling]);\n\n const refetch = useCallback(async () => {\n await fetchData(undefined, true);\n }, [fetchData]);\n\n // Auto-start polling when enabled and chatUid is set\n useEffect(() => {\n logRef.current.log('[usePolling] Auto-start effect triggered:', { enabled, chatUid, isPollingRef: isPollingRef.current, intervalRef: !!intervalRef.current });\n\n if (!enabled || !chatUid) {\n logRef.current.log('[usePolling] Not enabled or no chatUid, stopping if active');\n // Stop polling if disabled or no chatUid\n if (isPollingRef.current) {\n clearPolling();\n setIsPolling(false);\n }\n return;\n }\n\n // Start polling if not already polling\n if (!isPollingRef.current) {\n logRef.current.log('[usePolling] Starting polling, interval:', intervalValueRef.current);\n isPollingRef.current = true;\n setIsPolling(true);\n setError(null);\n\n // Immediate first fetch — unless a stream is about to open, whose first\n // frame is that same state.\n if (!streamFnRef.current) fetchData();\n\n // Set up interval\n armedIntervalRef.current = intervalValueRef.current;\n intervalRef.current = setInterval(fetchData, intervalValueRef.current);\n logRef.current.log('[usePolling] Interval set:', intervalRef.current);\n } else {\n logRef.current.log('[usePolling] Already polling, skipping start');\n }\n\n // Only cleanup on unmount, not on every dependency change\n }, [enabled, chatUid, fetchData, clearPolling]);\n\n // Re-arm the timer when the cadence changes while a conversation is already\n // in flight. Without this a new `interval` would only take effect on the next\n // conversation, which is not what changing a provider setting looks like.\n useEffect(() => {\n if (!isPollingRef.current || armedIntervalRef.current === null) return;\n if (armedIntervalRef.current === interval) return;\n\n logRef.current.log('[usePolling] Interval changed mid-flight:', {\n from: armedIntervalRef.current,\n to: interval,\n });\n if (intervalRef.current) clearInterval(intervalRef.current);\n armedIntervalRef.current = interval;\n intervalRef.current = setInterval(fetchData, interval);\n }, [interval, fetchData]);\n\n // Cleanup on unmount\n useEffect(() => {\n isMountedRef.current = true;\n return () => {\n isMountedRef.current = false;\n clearPolling();\n };\n }, [clearPolling]);\n\n return {\n data,\n isPolling,\n error,\n start,\n stop,\n refetch,\n };\n}\n"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"useSpeechRecording.js","sources":["../../../src/hooks/useSpeechRecording.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\nimport { useTranslations } from '../i18n';\n\n/**\n * Recording lifecycle state.\n * - `idle`: not recording\n * - `recording`: actively capturing audio\n * - `paused`: capture paused, can be resumed\n */\nexport type SpeechRecordingStatus = 'idle' | 'recording' | 'paused';\n\nexport interface UseSpeechRecordingOptions {\n /** Number of equalizer bars to expose in `levels`. @default 5 */\n bars?: number;\n /** Preferred MediaRecorder mime type. Falls back to a supported one. */\n mimeType?: string;\n /**\n * Auto-stop the recording after a short silence, but only once speech has\n * actually been detected. When it triggers, the countdown runs and then\n * `onAutoStop` is fired. Disable to require a manual confirm. @default true\n */\n autoStop?: boolean;\n /**\n * Continuous silence (ms) that must elapse — after speech was detected —\n * before the auto-stop countdown begins. @default 1000\n */\n autoStopSilenceMs?: number;\n /** Duration (ms) of the auto-stop countdown shown before firing. @default 1000 */\n autoStopCountdownMs?: number;\n /**\n * Silence is **adaptive**: the threshold is this fraction of the loudest\n * speech observed in the recording (e.g. 0.1 = 10% of peak voice). This makes\n * it robust to ambient noise — a quiet room and a loud one calibrate\n * differently. @default 0.1\n */\n autoStopSilenceRatio?: number;\n /**\n * Absolute floor (0..1) for the adaptive silence threshold, so it never drops\n * so low that background hiss reads as \"sound\". @default 0.02\n */\n autoStopSilenceLevel?: number;\n /**\n * Absolute floor (0..1) a peak must clear to first count as speech (arms the\n * detector / calibrates the reference loudness). @default 0.12\n */\n autoStopSpeechLevel?: number;\n /**\n * Fired when the silence countdown completes. The consumer decides what\n * \"stop\" means (e.g. confirm + transcribe). Read fresh on every frame, so it\n * does not need to be memoized.\n */\n onAutoStop?: () => void;\n}\n\nexport interface UseSpeechRecordingResult {\n status: SpeechRecordingStatus;\n isRecording: boolean;\n isPaused: boolean;\n /** Whether the browser supports audio recording. */\n isSupported: boolean;\n /** Normalized amplitude per bar (0..1), updated in real time for the equalizer. */\n levels: number[];\n /** Elapsed recording time in milliseconds (excludes paused time). */\n durationMs: number;\n /** Last error message, if any (e.g. permission denied). */\n error: string | null;\n /** True while the silence-triggered auto-stop countdown is running. */\n isAutoStopping: boolean;\n /** Auto-stop countdown progress, 1 (full) → 0 (empty), for an inverted ring. */\n autoStopProgress: number;\n /** True once a real voice peak was detected in the current recording. */\n speechDetected: boolean;\n /** Request mic access and start recording. */\n start: () => Promise<void>;\n /** Pause an active recording. */\n pause: () => void;\n /** Resume a paused recording. */\n resume: () => void;\n /** Stop and return the recorded audio as a Blob (null if nothing captured). */\n stop: () => Promise<Blob | null>;\n /** Stop and discard the recording without producing a Blob. */\n cancel: () => void;\n}\n\nconst PREFERRED_MIME_TYPES = [\n 'audio/webm;codecs=opus',\n 'audio/webm',\n 'audio/ogg;codecs=opus',\n 'audio/mp4',\n];\n\nfunction pickMimeType(preferred?: string): string | undefined {\n if (typeof MediaRecorder === 'undefined') return undefined;\n const candidates = preferred\n ? [preferred, ...PREFERRED_MIME_TYPES]\n : PREFERRED_MIME_TYPES;\n for (const type of candidates) {\n if (MediaRecorder.isTypeSupported(type)) return type;\n }\n return undefined;\n}\n\n/**\n * Encapsulates microphone capture via MediaRecorder plus a Web Audio analyser\n * that exposes live amplitude levels for an equalizer-style animation.\n *\n * The hook owns all teardown: stopping tracks, closing the AudioContext and\n * cancelling the animation frame on stop/cancel/unmount.\n */\nexport function useSpeechRecording(\n options: UseSpeechRecordingOptions = {},\n): UseSpeechRecordingResult {\n const {\n bars = 5,\n mimeType,\n autoStop = true,\n autoStopSilenceMs = 1000,\n autoStopCountdownMs = 1000,\n autoStopSilenceRatio = 0.1,\n autoStopSilenceLevel = 0.02,\n autoStopSpeechLevel = 0.12,\n onAutoStop,\n } = options;\n\n const t = useTranslations();\n\n const isSupported =\n typeof navigator !== 'undefined' &&\n !!navigator.mediaDevices?.getUserMedia &&\n typeof MediaRecorder !== 'undefined';\n\n const [status, setStatus] = useState<SpeechRecordingStatus>('idle');\n const [levels, setLevels] = useState<number[]>(() => new Array(bars).fill(0));\n const [durationMs, setDurationMs] = useState(0);\n const [error, setError] = useState<string | null>(null);\n const [isAutoStopping, setIsAutoStopping] = useState(false);\n const [autoStopProgress, setAutoStopProgress] = useState(1);\n const [speechDetected, setSpeechDetected] = useState(false);\n\n const mediaRecorderRef = useRef<MediaRecorder | null>(null);\n const streamRef = useRef<MediaStream | null>(null);\n const chunksRef = useRef<Blob[]>([]);\n const audioContextRef = useRef<AudioContext | null>(null);\n const analyserRef = useRef<AnalyserNode | null>(null);\n const rafRef = useRef<number | null>(null);\n const startedAtRef = useRef<number>(0);\n const accumulatedRef = useRef<number>(0);\n const stopResolveRef = useRef<((blob: Blob | null) => void) | null>(null);\n const discardRef = useRef<boolean>(false);\n\n // --- Auto-stop (silence detection) state, all kept in refs so the rAF loop\n // reads it without forcing `tick` to re-subscribe. ---\n const hasSpeechRef = useRef(false); // user has spoken at least once\n const loudestRef = useRef(0); // loudest peak seen (reference for adaptive silence)\n const silenceStartRef = useRef<number | null>(null); // when current silence began\n const autoStopStartRef = useRef<number | null>(null); // when the countdown started\n const autoStopFiredRef = useRef(false); // guard against re-firing before stop\n // Latest-config ref so the loop always sees fresh thresholds and callback.\n const autoStopCfgRef = useRef({\n autoStop,\n autoStopSilenceMs,\n autoStopCountdownMs,\n autoStopSilenceRatio,\n autoStopSilenceLevel,\n autoStopSpeechLevel,\n onAutoStop,\n });\n autoStopCfgRef.current = {\n autoStop,\n autoStopSilenceMs,\n autoStopCountdownMs,\n autoStopSilenceRatio,\n autoStopSilenceLevel,\n autoStopSpeechLevel,\n onAutoStop,\n };\n\n // Reset all auto-stop tracking. `keepSpeech` preserves the \"user has spoken\"\n // flag across a pause/resume so it doesn't re-arm from scratch.\n const resetAutoStop = useCallback((keepSpeech = false) => {\n if (!keepSpeech) {\n hasSpeechRef.current = false;\n loudestRef.current = 0;\n setSpeechDetected(false);\n }\n silenceStartRef.current = null;\n autoStopStartRef.current = null;\n autoStopFiredRef.current = false;\n setIsAutoStopping(false);\n setAutoStopProgress(1);\n }, []);\n\n // Evaluate the current peak amplitude against the silence/speech thresholds\n // and drive the auto-stop countdown. Called once per animation frame.\n const evaluateAutoStop = useCallback((peak: number) => {\n const cfg = autoStopCfgRef.current;\n if (!cfg.autoStop || autoStopFiredRef.current) return;\n const now = Date.now();\n\n // 1) Arm only once a real voice peak (absolute floor) has been heard. This\n // also seeds the reference loudness so ambient noise alone can't arm it.\n if (!hasSpeechRef.current) {\n if (peak >= cfg.autoStopSpeechLevel) {\n hasSpeechRef.current = true;\n loudestRef.current = peak;\n setSpeechDetected(true);\n }\n return;\n }\n\n // 2) Track the loudest voice so the silence threshold scales with how loud\n // the user actually speaks (robust to ambient noise).\n if (peak > loudestRef.current) loudestRef.current = peak;\n\n // 3) Adaptive thresholds: silence = ratio of the loudest voice (with an\n // absolute floor); activity sits above it for hysteresis, so background\n // noise between the two doesn't keep cancelling the countdown.\n const silenceThreshold = Math.max(\n cfg.autoStopSilenceLevel,\n cfg.autoStopSilenceRatio * loudestRef.current,\n );\n const activityThreshold = silenceThreshold * 1.8;\n\n if (peak >= activityThreshold) {\n // Talking again: reset silence and cancel any pending countdown.\n silenceStartRef.current = null;\n if (autoStopStartRef.current !== null) {\n autoStopStartRef.current = null;\n setIsAutoStopping(false);\n setAutoStopProgress(1);\n }\n return;\n }\n\n if (peak <= silenceThreshold) {\n if (silenceStartRef.current === null) silenceStartRef.current = now;\n\n if (autoStopStartRef.current === null) {\n // Waiting out the silence window before the countdown begins.\n if (now - silenceStartRef.current >= cfg.autoStopSilenceMs) {\n autoStopStartRef.current = now;\n setIsAutoStopping(true);\n setAutoStopProgress(1);\n }\n } else {\n // Countdown running: drain the ring 1 → 0, then fire.\n const elapsed = now - autoStopStartRef.current;\n const progress = Math.max(0, 1 - elapsed / cfg.autoStopCountdownMs);\n setAutoStopProgress(progress);\n if (elapsed >= cfg.autoStopCountdownMs) {\n autoStopFiredRef.current = true;\n autoStopStartRef.current = null;\n silenceStartRef.current = null;\n setIsAutoStopping(false);\n setAutoStopProgress(1);\n cfg.onAutoStop?.();\n }\n }\n }\n // Hysteresis deadzone (silence < peak < activity): keep timers running.\n }, []);\n\n const cleanupAudioGraph = useCallback(() => {\n if (rafRef.current !== null) {\n cancelAnimationFrame(rafRef.current);\n rafRef.current = null;\n }\n if (audioContextRef.current) {\n audioContextRef.current.close().catch(() => undefined);\n audioContextRef.current = null;\n }\n analyserRef.current = null;\n if (streamRef.current) {\n streamRef.current.getTracks().forEach((t) => t.stop());\n streamRef.current = null;\n }\n }, []);\n\n // Drives both the equalizer levels and the duration counter.\n const tick = useCallback(() => {\n const analyser = analyserRef.current;\n if (analyser) {\n const data = new Uint8Array(analyser.frequencyBinCount);\n analyser.getByteFrequencyData(data);\n const bucketSize = Math.floor(data.length / bars) || 1;\n const next: number[] = [];\n let peak = 0;\n for (let i = 0; i < bars; i++) {\n let sum = 0;\n for (let j = 0; j < bucketSize; j++) {\n sum += data[i * bucketSize + j] ?? 0;\n }\n // Normalize 0..255 -> 0..1 with a small floor so bars stay visible.\n const level = Math.min(1, sum / bucketSize / 255);\n next.push(level);\n if (level > peak) peak = level;\n }\n setLevels(next);\n evaluateAutoStop(peak);\n }\n setDurationMs(accumulatedRef.current + (Date.now() - startedAtRef.current));\n rafRef.current = requestAnimationFrame(tick);\n }, [bars, evaluateAutoStop]);\n\n const start = useCallback(async () => {\n if (!isSupported) {\n setError(t('Audio recording is not supported in this browser'));\n return;\n }\n if (mediaRecorderRef.current) return;\n\n setError(null);\n try {\n const stream = await navigator.mediaDevices.getUserMedia({ audio: true });\n streamRef.current = stream;\n\n const selectedType = pickMimeType(mimeType);\n const recorder = selectedType\n ? new MediaRecorder(stream, { mimeType: selectedType })\n : new MediaRecorder(stream);\n mediaRecorderRef.current = recorder;\n chunksRef.current = [];\n discardRef.current = false;\n resetAutoStop();\n\n recorder.ondataavailable = (e: BlobEvent) => {\n if (e.data && e.data.size > 0) chunksRef.current.push(e.data);\n };\n recorder.onstop = () => {\n const type = recorder.mimeType || selectedType || 'audio/webm';\n const blob = discardRef.current\n ? null\n : new Blob(chunksRef.current, { type });\n chunksRef.current = [];\n cleanupAudioGraph();\n mediaRecorderRef.current = null;\n setStatus('idle');\n setLevels(new Array(bars).fill(0));\n setDurationMs(0);\n accumulatedRef.current = 0;\n resetAutoStop();\n const resolve = stopResolveRef.current;\n stopResolveRef.current = null;\n resolve?.(blob);\n };\n\n // Web Audio analyser for the equalizer animation.\n const AudioCtx =\n window.AudioContext ||\n (window as unknown as { webkitAudioContext: typeof AudioContext })\n .webkitAudioContext;\n const audioContext = new AudioCtx();\n const source = audioContext.createMediaStreamSource(stream);\n const analyser = audioContext.createAnalyser();\n analyser.fftSize = 64;\n source.connect(analyser);\n audioContextRef.current = audioContext;\n analyserRef.current = analyser;\n\n accumulatedRef.current = 0;\n startedAtRef.current = Date.now();\n recorder.start(100);\n setStatus('recording');\n rafRef.current = requestAnimationFrame(tick);\n } catch (e) {\n cleanupAudioGraph();\n mediaRecorderRef.current = null;\n const message =\n (e as Error)?.name === 'NotAllowedError'\n ? t('Microphone permission denied')\n : t('Could not start recording: {error}', {\n error: (e as Error)?.message || t('unknown error'),\n });\n setError(message);\n setStatus('idle');\n }\n }, [isSupported, mimeType, bars, tick, cleanupAudioGraph, resetAutoStop, t]);\n\n const pause = useCallback(() => {\n const recorder = mediaRecorderRef.current;\n if (recorder && recorder.state === 'recording') {\n recorder.pause();\n accumulatedRef.current += Date.now() - startedAtRef.current;\n if (rafRef.current !== null) {\n cancelAnimationFrame(rafRef.current);\n rafRef.current = null;\n }\n setLevels(new Array(bars).fill(0));\n // Drop any in-flight silence/countdown but remember that speech happened.\n resetAutoStop(true);\n setStatus('paused');\n }\n }, [bars, resetAutoStop]);\n\n const resume = useCallback(() => {\n const recorder = mediaRecorderRef.current;\n if (recorder && recorder.state === 'paused') {\n recorder.resume();\n startedAtRef.current = Date.now();\n // Fresh silence timing on resume so it doesn't fire immediately.\n resetAutoStop(true);\n rafRef.current = requestAnimationFrame(tick);\n setStatus('recording');\n }\n }, [tick, resetAutoStop]);\n\n const stop = useCallback((): Promise<Blob | null> => {\n const recorder = mediaRecorderRef.current;\n if (!recorder) return Promise.resolve(null);\n discardRef.current = false;\n return new Promise<Blob | null>((resolve) => {\n stopResolveRef.current = resolve;\n recorder.stop();\n });\n }, []);\n\n const cancel = useCallback(() => {\n const recorder = mediaRecorderRef.current;\n if (!recorder) {\n cleanupAudioGraph();\n setStatus('idle');\n setLevels(new Array(bars).fill(0));\n setDurationMs(0);\n resetAutoStop();\n return;\n }\n discardRef.current = true;\n recorder.stop();\n }, [bars, cleanupAudioGraph, resetAutoStop]);\n\n // Teardown on unmount.\n useEffect(() => {\n return () => {\n const recorder = mediaRecorderRef.current;\n if (recorder && recorder.state !== 'inactive') {\n discardRef.current = true;\n try {\n recorder.stop();\n } catch {\n // ignore\n }\n }\n cleanupAudioGraph();\n };\n }, [cleanupAudioGraph]);\n\n return {\n status,\n isRecording: status === 'recording',\n isPaused: status === 'paused',\n isSupported,\n levels,\n durationMs,\n error,\n isAutoStopping,\n autoStopProgress,\n speechDetected,\n start,\n pause,\n resume,\n stop,\n cancel,\n };\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"useSpeechRecording.js","sources":["../../../src/hooks/useSpeechRecording.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\nimport { useTranslations } from '../i18n';\n\n/**\n * Recording lifecycle state.\n * - `idle`: not recording\n * - `recording`: actively capturing audio\n * - `paused`: capture paused, can be resumed\n */\nexport type SpeechRecordingStatus = 'idle' | 'recording' | 'paused';\n\nexport interface UseSpeechRecordingOptions {\n /** Number of equalizer bars to expose in `levels`. @default 5 */\n bars?: number;\n /** Preferred MediaRecorder mime type. Falls back to a supported one. */\n mimeType?: string;\n /**\n * Auto-stop the recording after a short silence, but only once speech has\n * actually been detected. When it triggers, the countdown runs and then\n * `onAutoStop` is fired. Disable to require a manual confirm. @default true\n */\n autoStop?: boolean;\n /**\n * Continuous silence (ms) that must elapse — after speech was detected —\n * before the auto-stop countdown begins. @default 1000\n */\n autoStopSilenceMs?: number;\n /** Duration (ms) of the auto-stop countdown shown before firing. @default 1000 */\n autoStopCountdownMs?: number;\n /**\n * Silence is **adaptive**: the threshold is this fraction of the loudest\n * speech observed in the recording (e.g. 0.1 = 10% of peak voice). This makes\n * it robust to ambient noise — a quiet room and a loud one calibrate\n * differently. @default 0.1\n */\n autoStopSilenceRatio?: number;\n /**\n * Absolute floor (0..1) for the adaptive silence threshold, so it never drops\n * so low that background hiss reads as \"sound\". @default 0.02\n */\n autoStopSilenceLevel?: number;\n /**\n * Absolute floor (0..1) a peak must clear to first count as speech (arms the\n * detector / calibrates the reference loudness). @default 0.12\n */\n autoStopSpeechLevel?: number;\n /**\n * Fired when the silence countdown completes. The consumer decides what\n * \"stop\" means (e.g. confirm + transcribe). Read fresh on every frame, so it\n * does not need to be memoized.\n */\n onAutoStop?: () => void;\n}\n\nexport interface UseSpeechRecordingResult {\n status: SpeechRecordingStatus;\n isRecording: boolean;\n isPaused: boolean;\n /** Whether the browser supports audio recording. */\n isSupported: boolean;\n /** Normalized amplitude per bar (0..1), updated in real time for the equalizer. */\n levels: number[];\n /** Elapsed recording time in milliseconds (excludes paused time). */\n durationMs: number;\n /** Last error message, if any (e.g. permission denied). */\n error: string | null;\n /** True while the silence-triggered auto-stop countdown is running. */\n isAutoStopping: boolean;\n /** Auto-stop countdown progress, 1 (full) → 0 (empty), for an inverted ring. */\n autoStopProgress: number;\n /** True once a real voice peak was detected in the current recording. */\n speechDetected: boolean;\n /** Request mic access and start recording. */\n start: () => Promise<void>;\n /** Pause an active recording. */\n pause: () => void;\n /** Resume a paused recording. */\n resume: () => void;\n /** Stop and return the recorded audio as a Blob (null if nothing captured). */\n stop: () => Promise<Blob | null>;\n /** Stop and discard the recording without producing a Blob. */\n cancel: () => void;\n}\n\nconst PREFERRED_MIME_TYPES = [\n 'audio/webm;codecs=opus',\n 'audio/webm',\n 'audio/ogg;codecs=opus',\n 'audio/mp4',\n];\n\nfunction pickMimeType(preferred?: string): string | undefined {\n if (typeof MediaRecorder === 'undefined') return undefined;\n const candidates = preferred\n ? [preferred, ...PREFERRED_MIME_TYPES]\n : PREFERRED_MIME_TYPES;\n for (const type of candidates) {\n if (MediaRecorder.isTypeSupported(type)) return type;\n }\n return undefined;\n}\n\n/**\n * Encapsulates microphone capture via MediaRecorder plus a Web Audio analyser\n * that exposes live amplitude levels for an equalizer-style animation.\n *\n * The hook owns all teardown: stopping tracks, closing the AudioContext and\n * cancelling the animation frame on stop/cancel/unmount.\n */\nexport function useSpeechRecording(\n options: UseSpeechRecordingOptions = {},\n): UseSpeechRecordingResult {\n const {\n bars = 5,\n mimeType,\n autoStop = true,\n autoStopSilenceMs = 1000,\n autoStopCountdownMs = 1000,\n autoStopSilenceRatio = 0.1,\n autoStopSilenceLevel = 0.02,\n autoStopSpeechLevel = 0.12,\n onAutoStop,\n } = options;\n\n const t = useTranslations();\n\n const isSupported =\n typeof navigator !== 'undefined' &&\n !!navigator.mediaDevices?.getUserMedia &&\n typeof MediaRecorder !== 'undefined';\n\n const [status, setStatus] = useState<SpeechRecordingStatus>('idle');\n const [levels, setLevels] = useState<number[]>(() => new Array(bars).fill(0));\n const [durationMs, setDurationMs] = useState(0);\n const [error, setError] = useState<string | null>(null);\n const [isAutoStopping, setIsAutoStopping] = useState(false);\n const [autoStopProgress, setAutoStopProgress] = useState(1);\n const [speechDetected, setSpeechDetected] = useState(false);\n\n const mediaRecorderRef = useRef<MediaRecorder | null>(null);\n const streamRef = useRef<MediaStream | null>(null);\n const chunksRef = useRef<Blob[]>([]);\n const audioContextRef = useRef<AudioContext | null>(null);\n const analyserRef = useRef<AnalyserNode | null>(null);\n const rafRef = useRef<number | null>(null);\n const startedAtRef = useRef<number>(0);\n const accumulatedRef = useRef<number>(0);\n const stopResolveRef = useRef<((blob: Blob | null) => void) | null>(null);\n const discardRef = useRef<boolean>(false);\n\n // --- Auto-stop (silence detection) state, all kept in refs so the rAF loop\n // reads it without forcing `tick` to re-subscribe. ---\n const hasSpeechRef = useRef(false); // user has spoken at least once\n const loudestRef = useRef(0); // loudest peak seen (reference for adaptive silence)\n const silenceStartRef = useRef<number | null>(null); // when current silence began\n const autoStopStartRef = useRef<number | null>(null); // when the countdown started\n const autoStopFiredRef = useRef(false); // guard against re-firing before stop\n // Latest-config ref so the loop always sees fresh thresholds and callback.\n const autoStopCfgRef = useRef({\n autoStop,\n autoStopSilenceMs,\n autoStopCountdownMs,\n autoStopSilenceRatio,\n autoStopSilenceLevel,\n autoStopSpeechLevel,\n onAutoStop,\n });\n autoStopCfgRef.current = {\n autoStop,\n autoStopSilenceMs,\n autoStopCountdownMs,\n autoStopSilenceRatio,\n autoStopSilenceLevel,\n autoStopSpeechLevel,\n onAutoStop,\n };\n\n // Reset all auto-stop tracking. `keepSpeech` preserves the \"user has spoken\"\n // flag across a pause/resume so it doesn't re-arm from scratch.\n const resetAutoStop = useCallback((keepSpeech = false) => {\n if (!keepSpeech) {\n hasSpeechRef.current = false;\n loudestRef.current = 0;\n setSpeechDetected(false);\n }\n silenceStartRef.current = null;\n autoStopStartRef.current = null;\n autoStopFiredRef.current = false;\n setIsAutoStopping(false);\n setAutoStopProgress(1);\n }, []);\n\n // Evaluate the current peak amplitude against the silence/speech thresholds\n // and drive the auto-stop countdown. Called once per animation frame.\n const evaluateAutoStop = useCallback((peak: number) => {\n const cfg = autoStopCfgRef.current;\n if (!cfg.autoStop || autoStopFiredRef.current) return;\n const now = Date.now();\n\n // 1) Arm only once a real voice peak (absolute floor) has been heard. This\n // also seeds the reference loudness so ambient noise alone can't arm it.\n if (!hasSpeechRef.current) {\n if (peak >= cfg.autoStopSpeechLevel) {\n hasSpeechRef.current = true;\n loudestRef.current = peak;\n setSpeechDetected(true);\n }\n return;\n }\n\n // 2) Track the loudest voice so the silence threshold scales with how loud\n // the user actually speaks (robust to ambient noise).\n if (peak > loudestRef.current) loudestRef.current = peak;\n\n // 3) Adaptive thresholds: silence = ratio of the loudest voice (with an\n // absolute floor); activity sits above it for hysteresis, so background\n // noise between the two doesn't keep cancelling the countdown.\n const silenceThreshold = Math.max(\n cfg.autoStopSilenceLevel,\n cfg.autoStopSilenceRatio * loudestRef.current,\n );\n const activityThreshold = silenceThreshold * 1.8;\n\n if (peak >= activityThreshold) {\n // Talking again: reset silence and cancel any pending countdown.\n silenceStartRef.current = null;\n if (autoStopStartRef.current !== null) {\n autoStopStartRef.current = null;\n setIsAutoStopping(false);\n setAutoStopProgress(1);\n }\n return;\n }\n\n if (peak <= silenceThreshold) {\n if (silenceStartRef.current === null) silenceStartRef.current = now;\n\n if (autoStopStartRef.current === null) {\n // Waiting out the silence window before the countdown begins.\n if (now - silenceStartRef.current >= cfg.autoStopSilenceMs) {\n autoStopStartRef.current = now;\n setIsAutoStopping(true);\n setAutoStopProgress(1);\n }\n } else {\n // Countdown running: drain the ring 1 → 0, then fire.\n const elapsed = now - autoStopStartRef.current;\n const progress = Math.max(0, 1 - elapsed / cfg.autoStopCountdownMs);\n setAutoStopProgress(progress);\n if (elapsed >= cfg.autoStopCountdownMs) {\n autoStopFiredRef.current = true;\n autoStopStartRef.current = null;\n silenceStartRef.current = null;\n setIsAutoStopping(false);\n setAutoStopProgress(1);\n cfg.onAutoStop?.();\n }\n }\n }\n // Hysteresis deadzone (silence < peak < activity): keep timers running.\n }, []);\n\n const cleanupAudioGraph = useCallback(() => {\n if (rafRef.current !== null) {\n cancelAnimationFrame(rafRef.current);\n rafRef.current = null;\n }\n if (audioContextRef.current) {\n audioContextRef.current.close().catch(() => undefined);\n audioContextRef.current = null;\n }\n analyserRef.current = null;\n if (streamRef.current) {\n streamRef.current.getTracks().forEach((t) => t.stop());\n streamRef.current = null;\n }\n }, []);\n\n // Drives both the equalizer levels and the duration counter.\n const tick = useCallback(() => {\n const analyser = analyserRef.current;\n if (analyser) {\n const data = new Uint8Array(analyser.frequencyBinCount);\n analyser.getByteFrequencyData(data);\n const bucketSize = Math.floor(data.length / bars) || 1;\n const next: number[] = [];\n let peak = 0;\n for (let i = 0; i < bars; i++) {\n let sum = 0;\n for (let j = 0; j < bucketSize; j++) {\n sum += data[i * bucketSize + j] ?? 0;\n }\n // Normalize 0..255 -> 0..1 with a small floor so bars stay visible.\n const level = Math.min(1, sum / bucketSize / 255);\n next.push(level);\n if (level > peak) peak = level;\n }\n setLevels(next);\n evaluateAutoStop(peak);\n }\n setDurationMs(accumulatedRef.current + (Date.now() - startedAtRef.current));\n rafRef.current = requestAnimationFrame(tick);\n }, [bars, evaluateAutoStop]);\n\n const start = useCallback(async () => {\n if (!isSupported) {\n setError(t('Audio recording is not supported in this browser'));\n return;\n }\n if (mediaRecorderRef.current) return;\n\n setError(null);\n try {\n const stream = await navigator.mediaDevices.getUserMedia({ audio: true });\n streamRef.current = stream;\n\n const selectedType = pickMimeType(mimeType);\n const recorder = selectedType\n ? new MediaRecorder(stream, { mimeType: selectedType })\n : new MediaRecorder(stream);\n mediaRecorderRef.current = recorder;\n chunksRef.current = [];\n discardRef.current = false;\n resetAutoStop();\n\n recorder.ondataavailable = (e: BlobEvent) => {\n if (e.data && e.data.size > 0) chunksRef.current.push(e.data);\n };\n recorder.onstop = () => {\n const type = recorder.mimeType || selectedType || 'audio/webm';\n const blob = discardRef.current\n ? null\n : new Blob(chunksRef.current, { type });\n chunksRef.current = [];\n cleanupAudioGraph();\n mediaRecorderRef.current = null;\n setStatus('idle');\n setLevels(new Array(bars).fill(0));\n setDurationMs(0);\n accumulatedRef.current = 0;\n resetAutoStop();\n const resolve = stopResolveRef.current;\n stopResolveRef.current = null;\n resolve?.(blob);\n };\n\n // Web Audio analyser for the equalizer animation.\n const AudioCtx =\n window.AudioContext ||\n (window as unknown as { webkitAudioContext: typeof AudioContext })\n .webkitAudioContext;\n const audioContext = new AudioCtx();\n const source = audioContext.createMediaStreamSource(stream);\n const analyser = audioContext.createAnalyser();\n analyser.fftSize = 64;\n source.connect(analyser);\n audioContextRef.current = audioContext;\n analyserRef.current = analyser;\n\n accumulatedRef.current = 0;\n startedAtRef.current = Date.now();\n recorder.start(100);\n setStatus('recording');\n rafRef.current = requestAnimationFrame(tick);\n } catch (e) {\n cleanupAudioGraph();\n mediaRecorderRef.current = null;\n const message =\n (e as Error)?.name === 'NotAllowedError'\n ? t('Microphone permission denied')\n : t('Could not start recording: {error}', {\n error: (e as Error)?.message || t('unknown error'),\n });\n setError(message);\n setStatus('idle');\n }\n }, [isSupported, mimeType, bars, tick, cleanupAudioGraph, resetAutoStop, t]);\n\n const pause = useCallback(() => {\n const recorder = mediaRecorderRef.current;\n if (recorder && recorder.state === 'recording') {\n recorder.pause();\n accumulatedRef.current += Date.now() - startedAtRef.current;\n if (rafRef.current !== null) {\n cancelAnimationFrame(rafRef.current);\n rafRef.current = null;\n }\n setLevels(new Array(bars).fill(0));\n // Drop any in-flight silence/countdown but remember that speech happened.\n resetAutoStop(true);\n setStatus('paused');\n }\n }, [bars, resetAutoStop]);\n\n const resume = useCallback(() => {\n const recorder = mediaRecorderRef.current;\n if (recorder && recorder.state === 'paused') {\n recorder.resume();\n startedAtRef.current = Date.now();\n // Fresh silence timing on resume so it doesn't fire immediately.\n resetAutoStop(true);\n rafRef.current = requestAnimationFrame(tick);\n setStatus('recording');\n }\n }, [tick, resetAutoStop]);\n\n const stop = useCallback((): Promise<Blob | null> => {\n const recorder = mediaRecorderRef.current;\n if (!recorder) return Promise.resolve(null);\n discardRef.current = false;\n return new Promise<Blob | null>((resolve) => {\n stopResolveRef.current = resolve;\n recorder.stop();\n });\n }, []);\n\n const cancel = useCallback(() => {\n const recorder = mediaRecorderRef.current;\n if (!recorder) {\n cleanupAudioGraph();\n setStatus('idle');\n setLevels(new Array(bars).fill(0));\n setDurationMs(0);\n resetAutoStop();\n return;\n }\n discardRef.current = true;\n recorder.stop();\n }, [bars, cleanupAudioGraph, resetAutoStop]);\n\n // Teardown on unmount.\n useEffect(() => {\n return () => {\n const recorder = mediaRecorderRef.current;\n if (recorder && recorder.state !== 'inactive') {\n discardRef.current = true;\n try {\n recorder.stop();\n } catch {\n // ignore\n }\n }\n cleanupAudioGraph();\n };\n }, [cleanupAudioGraph]);\n\n return {\n status,\n isRecording: status === 'recording',\n isPaused: status === 'paused',\n isSupported,\n levels,\n durationMs,\n error,\n isAutoStopping,\n autoStopProgress,\n speechDetected,\n start,\n pause,\n resume,\n stop,\n cancel,\n };\n}\n"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"translate.js","sources":["../../../src/i18n/translate.ts"],"sourcesContent":["import type { DevicTranslations, Translator } from './types';\n\n/**\n * Replace `{name}` placeholders. A placeholder with no matching value is\n * dropped rather than left in the text — half-substituted braces on screen\n * read as a bug to the end user.\n */\nexport function fillTemplate(\n template: string,\n vars?: Record<string, string | number>\n): string {\n if (!vars) return template;\n return template.replace(/\\{(\\w+)\\}/g, (_, name: string) => {\n const value = vars[name];\n return value === undefined || value === null ? '' : String(value);\n });\n}\n\n/**\n * Look one English text up in the dictionary, then fill its placeholders.\n * An absent entry falls back to the English text itself, so a partial\n * dictionary is perfectly usable.\n */\nexport function translate(\n dictionary: DevicTranslations | undefined,\n text: string,\n vars?: Record<string, string | number>\n): string {\n return fillTemplate(dictionary?.[text] ?? text, vars);\n}\n\n/**\n * Build a translator bound to one dictionary. Stable for as long as the\n * dictionary is, so it is safe in a dependency list.\n */\nexport function createTranslator(dictionary?: DevicTranslations): Translator {\n return (text, vars) => translate(dictionary, text, vars);\n}\n\n/**\n * Merge dictionaries, later ones winning. `undefined` layers are skipped so\n * callers can pass optional ones straight in.\n */\nexport function mergeTranslations(\n ...layers: (DevicTranslations | undefined)[]\n): DevicTranslations | undefined {\n const present = layers.filter(Boolean) as DevicTranslations[];\n if (present.length === 0) return undefined;\n if (present.length === 1) return present[0];\n return Object.assign({}, ...present) as DevicTranslations;\n}\n"],"names":[],"mappings":"AAEA;;;;AAIG;AACG,SAAU,YAAY,CAC1B,QAAgB,EAChB,IAAsC,EAAA;AAEtC,IAAA,IAAI,CAAC,IAAI;AAAE,QAAA,OAAO,QAAQ;IAC1B,OAAO,QAAQ,CAAC,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC,EAAE,IAAY,KAAI;AACxD,QAAA,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC;AACxB,QAAA,OAAO,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,GAAG,EAAE,GAAG,MAAM,CAAC,KAAK,CAAC;AACnE,IAAA,CAAC,CAAC;AACJ;AAEA;;;;AAIG;SACa,SAAS,CACvB,UAAyC,EACzC,IAAY,EACZ,IAAsC,EAAA;AAEtC,IAAA,OAAO,YAAY,CAAC,UAAU,GAAG,IAAI,CAAC,IAAI,IAAI,EAAE,IAAI,CAAC;AACvD;AAEA;;;AAGG;AACG,SAAU,gBAAgB,CAAC,UAA8B,EAAA;AAC7D,IAAA,OAAO,CAAC,IAAI,EAAE,IAAI,KAAK,SAAS,CAAC,UAAU,EAAE,IAAI,EAAE,IAAI,CAAC;AAC1D;AAEA;;;AAGG;AACG,SAAU,iBAAiB,CAC/B,GAAG,MAAyC,EAAA;IAE5C,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,OAAO,CAAwB;AAC7D,IAAA,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;AAAE,QAAA,OAAO,SAAS;AAC1C,IAAA,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;AAAE,QAAA,OAAO,OAAO,CAAC,CAAC,CAAC;IAC3C,OAAO,MAAM,CAAC,MAAM,CAAC,EAAE,EAAE,GAAG,OAAO,CAAsB;AAC3D;;"}
1
+ {"version":3,"file":"translate.js","sources":["../../../src/i18n/translate.ts"],"sourcesContent":["import type { DevicTranslations, Translator } from './types';\n\n/**\n * Replace `{name}` placeholders. A placeholder with no matching value is\n * dropped rather than left in the text — half-substituted braces on screen\n * read as a bug to the end user.\n */\nexport function fillTemplate(\n template: string,\n vars?: Record<string, string | number>\n): string {\n if (!vars) return template;\n return template.replace(/\\{(\\w+)\\}/g, (_, name: string) => {\n const value = vars[name];\n return value === undefined || value === null ? '' : String(value);\n });\n}\n\n/**\n * Look one English text up in the dictionary, then fill its placeholders.\n * An absent entry falls back to the English text itself, so a partial\n * dictionary is perfectly usable.\n */\nexport function translate(\n dictionary: DevicTranslations | undefined,\n text: string,\n vars?: Record<string, string | number>\n): string {\n return fillTemplate(dictionary?.[text] ?? text, vars);\n}\n\n/**\n * Build a translator bound to one dictionary. Stable for as long as the\n * dictionary is, so it is safe in a dependency list.\n */\nexport function createTranslator(dictionary?: DevicTranslations): Translator {\n return (text, vars) => translate(dictionary, text, vars);\n}\n\n/**\n * Merge dictionaries, later ones winning. `undefined` layers are skipped so\n * callers can pass optional ones straight in.\n */\nexport function mergeTranslations(\n ...layers: (DevicTranslations | undefined)[]\n): DevicTranslations | undefined {\n const present = layers.filter(Boolean) as DevicTranslations[];\n if (present.length === 0) return undefined;\n if (present.length === 1) return present[0];\n return Object.assign({}, ...present) as DevicTranslations;\n}\n"],"names":[],"mappings":"AAEA;;;;AAIG;AACG,SAAU,YAAY,CAC1B,QAAgB,EAChB,IAAsC,EAAA;AAEtC,IAAA,IAAI,CAAC,IAAI;AAAE,QAAA,OAAO,QAAQ;IAC1B,OAAO,QAAQ,CAAC,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC,EAAE,IAAY,KAAI;AACxD,QAAA,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC;AACxB,QAAA,OAAO,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,GAAG,EAAE,GAAG,MAAM,CAAC,KAAK,CAAC;AACnE,IAAA,CAAC,CAAC;AACJ;AAEA;;;;AAIG;SACa,SAAS,CACvB,UAAyC,EACzC,IAAY,EACZ,IAAsC,EAAA;AAEtC,IAAA,OAAO,YAAY,CAAC,UAAU,GAAG,IAAI,CAAC,IAAI,IAAI,EAAE,IAAI,CAAC;AACvD;AAEA;;;AAGG;AACG,SAAU,gBAAgB,CAAC,UAA8B,EAAA;AAC7D,IAAA,OAAO,CAAC,IAAI,EAAE,IAAI,KAAK,SAAS,CAAC,UAAU,EAAE,IAAI,EAAE,IAAI,CAAC;AAC1D;AAEA;;;AAGG;AACG,SAAU,iBAAiB,CAC/B,GAAG,MAAyC,EAAA;IAE5C,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,OAAO,CAAwB;AAC7D,IAAA,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;AAAE,QAAA,OAAO,SAAS;AAC1C,IAAA,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;AAAE,QAAA,OAAO,OAAO,CAAC,CAAC,CAAC;IAC3C,OAAO,MAAM,CAAC,MAAM,CAAC,EAAE,EAAE,GAAG,OAAO,CAAsB;AAC3D;;;;"}
@@ -1 +1 @@
1
- {"version":3,"file":"useTranslations.js","sources":["../../../src/i18n/useTranslations.tsx"],"sourcesContent":["import React, { createContext, useContext, useMemo } from 'react';\nimport { useOptionalDevicContext } from '../provider/DevicContext';\nimport { createTranslator, mergeTranslations } from './translate';\nimport type { DevicTranslations, Translator } from './types';\n\n/**\n * Dictionary contributed by an enclosing component, on top of the provider's.\n *\n * It exists so `<ChatDrawer options={{ translations }}>` reaches the input,\n * the message list and the dialogs it opens without every one of them taking\n * a prop for it. Nesting merges rather than replaces.\n */\nconst TranslationsScope = createContext<DevicTranslations | undefined>(undefined);\n\nexport interface DevicTranslationsProviderProps {\n /** Entries added on top of the enclosing dictionaries. */\n translations?: DevicTranslations;\n children: React.ReactNode;\n}\n\n/**\n * Scopes a dictionary to a subtree. Rarely needed directly — the components\n * that take a `translations` option already wrap their own subtree with it —\n * but useful to translate one region of a page differently.\n */\nexport function DevicTranslationsProvider({\n translations,\n children,\n}: DevicTranslationsProviderProps): JSX.Element {\n const outer = useContext(TranslationsScope);\n const value = useMemo(\n () => mergeTranslations(outer, translations),\n [outer, translations]\n );\n return (\n <TranslationsScope.Provider value={value}>\n {children}\n </TranslationsScope.Provider>\n );\n}\n\n/**\n * The dictionary in force here: the provider's, plus any scoped by an\n * enclosing component, plus the `local` argument. Later layers win.\n */\nexport function useTranslationsDictionary(\n local?: DevicTranslations\n): DevicTranslations | undefined {\n const context = useOptionalDevicContext();\n const scoped = useContext(TranslationsScope);\n const global = context?.translations;\n return useMemo(\n () => mergeTranslations(global, scoped, local),\n [global, scoped, local]\n );\n}\n\n/**\n * Returns `t`, the translator for every text this library renders itself.\n *\n * Works with or without a `DevicProvider`: with no dictionary anywhere it\n * returns the English text unchanged, which is what a component mounted\n * standalone gets.\n *\n * @example\n * ```tsx\n * const t = useTranslations();\n * <button aria-label={t('New chat')}>…</button>\n * ```\n */\nexport function useTranslations(local?: DevicTranslations): Translator {\n const dictionary = useTranslationsDictionary(local);\n return useMemo(() => createTranslator(dictionary), [dictionary]);\n}\n"],"names":["_jsx"],"mappings":";;;;;AAKA;;;;;;AAMG;AACH,MAAM,iBAAiB,GAAG,aAAa,CAAgC,SAAS,CAAC;AAQjF;;;;AAIG;SACa,yBAAyB,CAAC,EACxC,YAAY,EACZ,QAAQ,GACuB,EAAA;AAC/B,IAAA,MAAM,KAAK,GAAG,UAAU,CAAC,iBAAiB,CAAC;IAC3C,MAAM,KAAK,GAAG,OAAO,CACnB,MAAM,iBAAiB,CAAC,KAAK,EAAE,YAAY,CAAC,EAC5C,CAAC,KAAK,EAAE,YAAY,CAAC,CACtB;AACD,IAAA,QACEA,GAAA,CAAC,iBAAiB,CAAC,QAAQ,EAAA,EAAC,KAAK,EAAE,KAAK,EAAA,QAAA,EACrC,QAAQ,EAAA,CACkB;AAEjC;AAEA;;;AAGG;AACG,SAAU,yBAAyB,CACvC,KAAyB,EAAA;AAEzB,IAAA,MAAM,OAAO,GAAG,uBAAuB,EAAE;AACzC,IAAA,MAAM,MAAM,GAAG,UAAU,CAAC,iBAAiB,CAAC;AAC5C,IAAA,MAAM,MAAM,GAAG,OAAO,EAAE,YAAY;IACpC,OAAO,OAAO,CACZ,MAAM,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,EAC9C,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,CACxB;AACH;AAEA;;;;;;;;;;;;AAYG;AACG,SAAU,eAAe,CAAC,KAAyB,EAAA;AACvD,IAAA,MAAM,UAAU,GAAG,yBAAyB,CAAC,KAAK,CAAC;AACnD,IAAA,OAAO,OAAO,CAAC,MAAM,gBAAgB,CAAC,UAAU,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC;AAClE;;"}
1
+ {"version":3,"file":"useTranslations.js","sources":["../../../src/i18n/useTranslations.tsx"],"sourcesContent":["import React, { createContext, useContext, useMemo } from 'react';\nimport { useOptionalDevicContext } from '../provider/DevicContext';\nimport { createTranslator, mergeTranslations } from './translate';\nimport type { DevicTranslations, Translator } from './types';\n\n/**\n * Dictionary contributed by an enclosing component, on top of the provider's.\n *\n * It exists so `<ChatDrawer options={{ translations }}>` reaches the input,\n * the message list and the dialogs it opens without every one of them taking\n * a prop for it. Nesting merges rather than replaces.\n */\nconst TranslationsScope = createContext<DevicTranslations | undefined>(undefined);\n\nexport interface DevicTranslationsProviderProps {\n /** Entries added on top of the enclosing dictionaries. */\n translations?: DevicTranslations;\n children: React.ReactNode;\n}\n\n/**\n * Scopes a dictionary to a subtree. Rarely needed directly — the components\n * that take a `translations` option already wrap their own subtree with it —\n * but useful to translate one region of a page differently.\n */\nexport function DevicTranslationsProvider({\n translations,\n children,\n}: DevicTranslationsProviderProps): JSX.Element {\n const outer = useContext(TranslationsScope);\n const value = useMemo(\n () => mergeTranslations(outer, translations),\n [outer, translations]\n );\n return (\n <TranslationsScope.Provider value={value}>\n {children}\n </TranslationsScope.Provider>\n );\n}\n\n/**\n * The dictionary in force here: the provider's, plus any scoped by an\n * enclosing component, plus the `local` argument. Later layers win.\n */\nexport function useTranslationsDictionary(\n local?: DevicTranslations\n): DevicTranslations | undefined {\n const context = useOptionalDevicContext();\n const scoped = useContext(TranslationsScope);\n const global = context?.translations;\n return useMemo(\n () => mergeTranslations(global, scoped, local),\n [global, scoped, local]\n );\n}\n\n/**\n * Returns `t`, the translator for every text this library renders itself.\n *\n * Works with or without a `DevicProvider`: with no dictionary anywhere it\n * returns the English text unchanged, which is what a component mounted\n * standalone gets.\n *\n * @example\n * ```tsx\n * const t = useTranslations();\n * <button aria-label={t('New chat')}>…</button>\n * ```\n */\nexport function useTranslations(local?: DevicTranslations): Translator {\n const dictionary = useTranslationsDictionary(local);\n return useMemo(() => createTranslator(dictionary), [dictionary]);\n}\n"],"names":["_jsx"],"mappings":";;;;;AAKA;;;;;;AAMG;AACH,MAAM,iBAAiB,GAAG,aAAa,CAAgC,SAAS,CAAC;AAQjF;;;;AAIG;SACa,yBAAyB,CAAC,EACxC,YAAY,EACZ,QAAQ,GACuB,EAAA;AAC/B,IAAA,MAAM,KAAK,GAAG,UAAU,CAAC,iBAAiB,CAAC;IAC3C,MAAM,KAAK,GAAG,OAAO,CACnB,MAAM,iBAAiB,CAAC,KAAK,EAAE,YAAY,CAAC,EAC5C,CAAC,KAAK,EAAE,YAAY,CAAC,CACtB;AACD,IAAA,QACEA,GAAA,CAAC,iBAAiB,CAAC,QAAQ,EAAA,EAAC,KAAK,EAAE,KAAK,EAAA,QAAA,EACrC,QAAQ,EAAA,CACkB;AAEjC;AAEA;;;AAGG;AACG,SAAU,yBAAyB,CACvC,KAAyB,EAAA;AAEzB,IAAA,MAAM,OAAO,GAAG,uBAAuB,EAAE;AACzC,IAAA,MAAM,MAAM,GAAG,UAAU,CAAC,iBAAiB,CAAC;AAC5C,IAAA,MAAM,MAAM,GAAG,OAAO,EAAE,YAAY;IACpC,OAAO,OAAO,CACZ,MAAM,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,EAC9C,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,CACxB;AACH;AAEA;;;;;;;;;;;;AAYG;AACG,SAAU,eAAe,CAAC,KAAyB,EAAA;AACvD,IAAA,MAAM,UAAU,GAAG,yBAAyB,CAAC,KAAK,CAAC;AACnD,IAAA,OAAO,OAAO,CAAC,MAAM,gBAAgB,CAAC,UAAU,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC;AAClE;;;;"}
@@ -1 +1 @@
1
- {"version":3,"file":"DevicContext.js","sources":["../../../src/provider/DevicContext.ts"],"sourcesContent":["import { createContext, useContext } from 'react';\nimport type { DevicContextValue } from './types';\n\n/**\n * Context for Devic configuration\n */\nexport const DevicContext = createContext<DevicContextValue | null>(null);\n\n/**\n * Hook to access the Devic context\n * @throws Error if used outside of DevicProvider\n */\nexport function useDevicContext(): DevicContextValue {\n const context = useContext(DevicContext);\n if (!context) {\n throw new Error(\n 'useDevicContext must be used within a DevicProvider. ' +\n 'Make sure to wrap your component tree with <DevicProvider>.'\n );\n }\n return context;\n}\n\n/**\n * Hook to optionally access the Devic context\n * Returns null if not within a provider (for components that can work standalone)\n */\nexport function useOptionalDevicContext(): DevicContextValue | null {\n return useContext(DevicContext);\n}\n"],"names":[],"mappings":";;AAGA;;AAEG;MACU,YAAY,GAAG,aAAa,CAA2B,IAAI;AAExE;;;AAGG;SACa,eAAe,GAAA;AAC7B,IAAA,MAAM,OAAO,GAAG,UAAU,CAAC,YAAY,CAAC;IACxC,IAAI,CAAC,OAAO,EAAE;QACZ,MAAM,IAAI,KAAK,CACb,uDAAuD;AACvD,YAAA,6DAA6D,CAC9D;IACH;AACA,IAAA,OAAO,OAAO;AAChB;AAEA;;;AAGG;SACa,uBAAuB,GAAA;AACrC,IAAA,OAAO,UAAU,CAAC,YAAY,CAAC;AACjC;;"}
1
+ {"version":3,"file":"DevicContext.js","sources":["../../../src/provider/DevicContext.ts"],"sourcesContent":["import { createContext, useContext } from 'react';\nimport type { DevicContextValue } from './types';\n\n/**\n * Context for Devic configuration\n */\nexport const DevicContext = createContext<DevicContextValue | null>(null);\n\n/**\n * Hook to access the Devic context\n * @throws Error if used outside of DevicProvider\n */\nexport function useDevicContext(): DevicContextValue {\n const context = useContext(DevicContext);\n if (!context) {\n throw new Error(\n 'useDevicContext must be used within a DevicProvider. ' +\n 'Make sure to wrap your component tree with <DevicProvider>.'\n );\n }\n return context;\n}\n\n/**\n * Hook to optionally access the Devic context\n * Returns null if not within a provider (for components that can work standalone)\n */\nexport function useOptionalDevicContext(): DevicContextValue | null {\n return useContext(DevicContext);\n}\n"],"names":[],"mappings":";;AAGA;;AAEG;MACU,YAAY,GAAG,aAAa,CAA2B,IAAI;AAExE;;;AAGG;SACa,eAAe,GAAA;AAC7B,IAAA,MAAM,OAAO,GAAG,UAAU,CAAC,YAAY,CAAC;IACxC,IAAI,CAAC,OAAO,EAAE;QACZ,MAAM,IAAI,KAAK,CACb,uDAAuD;AACvD,YAAA,6DAA6D,CAC9D;IACH;AACA,IAAA,OAAO,OAAO;AAChB;AAEA;;;AAGG;SACa,uBAAuB,GAAA;AACrC,IAAA,OAAO,UAAU,CAAC,YAAY,CAAC;AACjC;;;;"}
@@ -14,4 +14,4 @@ import type { DevicProviderProps } from './types';
14
14
  * </DevicProvider>
15
15
  * ```
16
16
  */
17
- export declare function DevicProvider({ apiKey, getTenantSession, onSessionExpired, baseUrl, tenantId, tenantMetadata, subtenantId, subtenantMetadata, tags, pollingInterval, translations, debug, children, }: DevicProviderProps): JSX.Element;
17
+ export declare function DevicProvider({ apiKey, getTenantSession, onSessionExpired, baseUrl, tenantId, tenantMetadata, subtenantId, subtenantMetadata, tags, pollingInterval, streaming, translations, debug, children, }: DevicProviderProps): JSX.Element;
@@ -21,7 +21,7 @@ const DEFAULT_BASE_URL = 'https://api.devic.ai';
21
21
  * </DevicProvider>
22
22
  * ```
23
23
  */
24
- function DevicProvider({ apiKey, getTenantSession, onSessionExpired, baseUrl = DEFAULT_BASE_URL, tenantId, tenantMetadata, subtenantId, subtenantMetadata, tags, pollingInterval, translations, debug, children, }) {
24
+ function DevicProvider({ apiKey, getTenantSession, onSessionExpired, baseUrl = DEFAULT_BASE_URL, tenantId, tenantMetadata, subtenantId, subtenantMetadata, tags, pollingInterval, streaming, translations, debug, children, }) {
25
25
  const [references, setReferences] = useState([]);
26
26
  const drawerRef = useRef(null);
27
27
  const addReference = useCallback((ref) => {
@@ -81,6 +81,7 @@ function DevicProvider({ apiKey, getTenantSession, onSessionExpired, baseUrl = D
81
81
  subtenantMetadata,
82
82
  tags,
83
83
  pollingInterval,
84
+ streaming,
84
85
  translations,
85
86
  isConfigured: !!apiKey || usesSessions,
86
87
  debug,
@@ -103,6 +104,7 @@ function DevicProvider({ apiKey, getTenantSession, onSessionExpired, baseUrl = D
103
104
  subtenantMetadata,
104
105
  tags,
105
106
  pollingInterval,
107
+ streaming,
106
108
  translations,
107
109
  debug,
108
110
  references,
@@ -1 +1 @@
1
- {"version":3,"file":"DevicProvider.js","sources":["../../../src/provider/DevicProvider.tsx"],"sourcesContent":["import React, { useMemo, useState, useRef, useCallback } from 'react';\nimport { DevicContext } from './DevicContext';\nimport { DevicApiClient } from '../api/client';\nimport { createSharedSession } from './sharedSession';\nimport { generateId } from '../utils';\nimport type {\n DevicProviderProps,\n DevicContextValue,\n AIReference,\n DrawerRegistration,\n} from './types';\n\nconst DEFAULT_BASE_URL = 'https://api.devic.ai';\n\n/**\n * Provider component for Devic UI configuration\n *\n * @example\n * ```tsx\n * <DevicProvider\n * apiKey=\"devic-xxx\"\n * baseUrl=\"https://api.devic.ai\"\n * tenantId=\"tenant-123\"\n * tenantMetadata={{ userId: '456' }}\n * >\n * <App />\n * </DevicProvider>\n * ```\n */\nexport function DevicProvider({\n apiKey,\n getTenantSession,\n onSessionExpired,\n baseUrl = DEFAULT_BASE_URL,\n tenantId,\n tenantMetadata,\n subtenantId,\n subtenantMetadata,\n tags,\n pollingInterval,\n translations,\n debug,\n children,\n}: DevicProviderProps): JSX.Element {\n const [references, setReferences] = useState<AIReference[]>([]);\n const drawerRef = useRef<DrawerRegistration | null>(null);\n\n const addReference = useCallback((ref: Omit<AIReference, 'id'>): string => {\n const id = generateId();\n setReferences((prev) => [...prev, { ...ref, id }]);\n return id;\n }, []);\n\n const removeReference = useCallback((id: string) => {\n setReferences((prev) => prev.filter((r) => r.id !== id));\n }, []);\n\n const clearReferences = useCallback(() => {\n setReferences([]);\n }, []);\n\n const registerDrawer = useCallback((handle: DrawerRegistration) => {\n drawerRef.current = handle;\n return () => {\n if (drawerRef.current === handle) {\n drawerRef.current = null;\n }\n };\n }, []);\n\n const openDrawer = useCallback(() => {\n drawerRef.current?.open();\n }, []);\n\n // Both callbacks are read through a ref and handed on as stable wrappers.\n // An inline `getTenantSession={() => …}` is a new function on every render,\n // and passing it straight through would rebuild the client — throwing away\n // the session it holds, so the page fetches a token per render — and rebuild\n // the context, so every component below it would do the same.\n const sessionSourceRef = useRef(getTenantSession);\n sessionSourceRef.current = getTenantSession;\n const expiredRef = useRef(onSessionExpired);\n expiredRef.current = onSessionExpired;\n const usesSessions = !!getTenantSession;\n\n // One session for the whole tree: every component below builds its own API\n // client, and each dedupes only its own renewals — so without this, N clients\n // ask for N tokens. See `createSharedSession`.\n const tenantSession = useMemo(\n () =>\n usesSessions\n ? createSharedSession(() => sessionSourceRef.current!())\n : undefined,\n [usesSessions]\n );\n const sessionExpired = useMemo(() => () => expiredRef.current?.(), []);\n\n const client = useMemo(\n () =>\n new DevicApiClient({\n apiKey,\n baseUrl,\n getTenantSession: tenantSession,\n onSessionExpired: sessionExpired,\n }),\n [apiKey, baseUrl, tenantSession, sessionExpired]\n );\n\n const contextValue = useMemo<DevicContextValue>(\n () => ({\n client,\n apiKey,\n getTenantSession: tenantSession,\n onSessionExpired: sessionExpired,\n baseUrl,\n tenantId,\n tenantMetadata,\n subtenantId,\n subtenantMetadata,\n tags,\n pollingInterval,\n translations,\n isConfigured: !!apiKey || usesSessions,\n debug,\n references,\n addReference,\n removeReference,\n clearReferences,\n registerDrawer,\n openDrawer,\n }),\n [\n client,\n apiKey,\n tenantSession,\n sessionExpired,\n usesSessions,\n baseUrl,\n tenantId,\n tenantMetadata,\n subtenantId,\n subtenantMetadata,\n tags,\n pollingInterval,\n translations,\n debug,\n references,\n addReference,\n removeReference,\n clearReferences,\n registerDrawer,\n openDrawer,\n ]\n );\n\n return (\n <DevicContext.Provider value={contextValue}>\n {children}\n </DevicContext.Provider>\n );\n}\n"],"names":["_jsx"],"mappings":"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"}
1
+ {"version":3,"file":"DevicProvider.js","sources":["../../../src/provider/DevicProvider.tsx"],"sourcesContent":["import React, { useMemo, useState, useRef, useCallback } from 'react';\nimport { DevicContext } from './DevicContext';\nimport { DevicApiClient } from '../api/client';\nimport { createSharedSession } from './sharedSession';\nimport { generateId } from '../utils';\nimport type {\n DevicProviderProps,\n DevicContextValue,\n AIReference,\n DrawerRegistration,\n} from './types';\n\nconst DEFAULT_BASE_URL = 'https://api.devic.ai';\n\n/**\n * Provider component for Devic UI configuration\n *\n * @example\n * ```tsx\n * <DevicProvider\n * apiKey=\"devic-xxx\"\n * baseUrl=\"https://api.devic.ai\"\n * tenantId=\"tenant-123\"\n * tenantMetadata={{ userId: '456' }}\n * >\n * <App />\n * </DevicProvider>\n * ```\n */\nexport function DevicProvider({\n apiKey,\n getTenantSession,\n onSessionExpired,\n baseUrl = DEFAULT_BASE_URL,\n tenantId,\n tenantMetadata,\n subtenantId,\n subtenantMetadata,\n tags,\n pollingInterval,\n streaming,\n translations,\n debug,\n children,\n}: DevicProviderProps): JSX.Element {\n const [references, setReferences] = useState<AIReference[]>([]);\n const drawerRef = useRef<DrawerRegistration | null>(null);\n\n const addReference = useCallback((ref: Omit<AIReference, 'id'>): string => {\n const id = generateId();\n setReferences((prev) => [...prev, { ...ref, id }]);\n return id;\n }, []);\n\n const removeReference = useCallback((id: string) => {\n setReferences((prev) => prev.filter((r) => r.id !== id));\n }, []);\n\n const clearReferences = useCallback(() => {\n setReferences([]);\n }, []);\n\n const registerDrawer = useCallback((handle: DrawerRegistration) => {\n drawerRef.current = handle;\n return () => {\n if (drawerRef.current === handle) {\n drawerRef.current = null;\n }\n };\n }, []);\n\n const openDrawer = useCallback(() => {\n drawerRef.current?.open();\n }, []);\n\n // Both callbacks are read through a ref and handed on as stable wrappers.\n // An inline `getTenantSession={() => …}` is a new function on every render,\n // and passing it straight through would rebuild the client — throwing away\n // the session it holds, so the page fetches a token per render — and rebuild\n // the context, so every component below it would do the same.\n const sessionSourceRef = useRef(getTenantSession);\n sessionSourceRef.current = getTenantSession;\n const expiredRef = useRef(onSessionExpired);\n expiredRef.current = onSessionExpired;\n const usesSessions = !!getTenantSession;\n\n // One session for the whole tree: every component below builds its own API\n // client, and each dedupes only its own renewals — so without this, N clients\n // ask for N tokens. See `createSharedSession`.\n const tenantSession = useMemo(\n () =>\n usesSessions\n ? createSharedSession(() => sessionSourceRef.current!())\n : undefined,\n [usesSessions]\n );\n const sessionExpired = useMemo(() => () => expiredRef.current?.(), []);\n\n const client = useMemo(\n () =>\n new DevicApiClient({\n apiKey,\n baseUrl,\n getTenantSession: tenantSession,\n onSessionExpired: sessionExpired,\n }),\n [apiKey, baseUrl, tenantSession, sessionExpired]\n );\n\n const contextValue = useMemo<DevicContextValue>(\n () => ({\n client,\n apiKey,\n getTenantSession: tenantSession,\n onSessionExpired: sessionExpired,\n baseUrl,\n tenantId,\n tenantMetadata,\n subtenantId,\n subtenantMetadata,\n tags,\n pollingInterval,\n streaming,\n translations,\n isConfigured: !!apiKey || usesSessions,\n debug,\n references,\n addReference,\n removeReference,\n clearReferences,\n registerDrawer,\n openDrawer,\n }),\n [\n client,\n apiKey,\n tenantSession,\n sessionExpired,\n usesSessions,\n baseUrl,\n tenantId,\n tenantMetadata,\n subtenantId,\n subtenantMetadata,\n tags,\n pollingInterval,\n streaming,\n translations,\n debug,\n references,\n addReference,\n removeReference,\n clearReferences,\n registerDrawer,\n openDrawer,\n ]\n );\n\n return (\n <DevicContext.Provider value={contextValue}>\n {children}\n </DevicContext.Provider>\n );\n}\n"],"names":["_jsx"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"sharedSession.js","sources":["../../../src/provider/sharedSession.ts"],"sourcesContent":["import type { TenantSessionToken } from '../api/client';\n\n/**\n * Renew this long before expiry. A little wider than the client's own margin,\n * so the sharing decides when to renew and the clients find a live token rather\n * than each reaching for one.\n */\nconst RENEWAL_MARGIN_MS = 90_000;\n\n/** The `exp` inside a JWT, in epoch ms. Unknown when it cannot be read. */\nfunction expiryOf(token: string): number | undefined {\n try {\n const payload = token.split('.')[1];\n if (!payload) return undefined;\n const json = atob(payload.replace(/-/g, '+').replace(/_/g, '/'));\n const exp = JSON.parse(json)?.exp;\n return typeof exp === 'number' ? exp * 1000 : undefined;\n } catch {\n return undefined;\n }\n}\n\n/** Whatever the integrator returned, as a token with an expiry we can compare. */\nexport function normaliseSession(\n result: string | TenantSessionToken,\n): TenantSessionToken {\n if (typeof result === 'string') {\n return { token: result, expiresAt: expiryOf(result) };\n }\n const declared =\n result.expiresAt ??\n (result.expiresIn ? Date.now() + result.expiresIn * 1000 : undefined);\n return { ...result, expiresAt: declared ?? expiryOf(result.token) };\n}\n\n/**\n * One session for a whole tree of components.\n *\n * Every component in this library builds its own API client, and each of those\n * dedupes only its own renewals — so N clients ask for N tokens. Opening the\n * drawer fired three requests at the integrator's backend before anyone had\n * said a word. The sharing belongs where there is exactly one of something,\n * which is the provider, and not in each client, where there is not.\n *\n * `force` is what a client passes after the API refused the token it just used.\n * The cached answer is then known to be dead, so it is skipped: without that, a\n * shared cache would hand the refused token straight back and turn a\n * recoverable 401 into a widget that never speaks again.\n */\nexport function createSharedSession(\n source: () => Promise<string | TenantSessionToken>,\n): (force?: boolean) => Promise<TenantSessionToken> {\n let value: TenantSessionToken | undefined;\n let inFlight: Promise<TenantSessionToken> | undefined;\n\n return (force = false) => {\n const stillGood =\n value &&\n (value.expiresAt === undefined ||\n value.expiresAt - Date.now() > RENEWAL_MARGIN_MS);\n if (stillGood && !force) return Promise.resolve(value!);\n\n if (!inFlight) {\n inFlight = Promise.resolve(source())\n .then((result) => {\n value = normaliseSession(result);\n return value;\n })\n .finally(() => {\n inFlight = undefined;\n });\n }\n return inFlight;\n };\n}\n"],"names":[],"mappings":"AAEA;;;;AAIG;AACH,MAAM,iBAAiB,GAAG,KAAM;AAEhC;AACA,SAAS,QAAQ,CAAC,KAAa,EAAA;AAC7B,IAAA,IAAI;QACF,MAAM,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;AACnC,QAAA,IAAI,CAAC,OAAO;AAAE,YAAA,OAAO,SAAS;QAC9B,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;QAChE,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,GAAG;AACjC,QAAA,OAAO,OAAO,GAAG,KAAK,QAAQ,GAAG,GAAG,GAAG,IAAI,GAAG,SAAS;IACzD;AAAE,IAAA,MAAM;AACN,QAAA,OAAO,SAAS;IAClB;AACF;AAEA;AACM,SAAU,gBAAgB,CAC9B,MAAmC,EAAA;AAEnC,IAAA,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE;AAC9B,QAAA,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,EAAE;IACvD;AACA,IAAA,MAAM,QAAQ,GACZ,MAAM,CAAC,SAAS;SACf,MAAM,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC,SAAS,GAAG,IAAI,GAAG,SAAS,CAAC;AACvE,IAAA,OAAO,EAAE,GAAG,MAAM,EAAE,SAAS,EAAE,QAAQ,IAAI,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE;AACrE;AAEA;;;;;;;;;;;;;AAaG;AACG,SAAU,mBAAmB,CACjC,MAAkD,EAAA;AAElD,IAAA,IAAI,KAAqC;AACzC,IAAA,IAAI,QAAiD;AAErD,IAAA,OAAO,CAAC,KAAK,GAAG,KAAK,KAAI;QACvB,MAAM,SAAS,GACb,KAAK;AACL,aAAC,KAAK,CAAC,SAAS,KAAK,SAAS;gBAC5B,KAAK,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,iBAAiB,CAAC;QACrD,IAAI,SAAS,IAAI,CAAC,KAAK;AAAE,YAAA,OAAO,OAAO,CAAC,OAAO,CAAC,KAAM,CAAC;QAEvD,IAAI,CAAC,QAAQ,EAAE;AACb,YAAA,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE;AAChC,iBAAA,IAAI,CAAC,CAAC,MAAM,KAAI;AACf,gBAAA,KAAK,GAAG,gBAAgB,CAAC,MAAM,CAAC;AAChC,gBAAA,OAAO,KAAK;AACd,YAAA,CAAC;iBACA,OAAO,CAAC,MAAK;gBACZ,QAAQ,GAAG,SAAS;AACtB,YAAA,CAAC,CAAC;QACN;AACA,QAAA,OAAO,QAAQ;AACjB,IAAA,CAAC;AACH;;"}
1
+ {"version":3,"file":"sharedSession.js","sources":["../../../src/provider/sharedSession.ts"],"sourcesContent":["import type { TenantSessionToken } from '../api/client';\n\n/**\n * Renew this long before expiry. A little wider than the client's own margin,\n * so the sharing decides when to renew and the clients find a live token rather\n * than each reaching for one.\n */\nconst RENEWAL_MARGIN_MS = 90_000;\n\n/** The `exp` inside a JWT, in epoch ms. Unknown when it cannot be read. */\nfunction expiryOf(token: string): number | undefined {\n try {\n const payload = token.split('.')[1];\n if (!payload) return undefined;\n const json = atob(payload.replace(/-/g, '+').replace(/_/g, '/'));\n const exp = JSON.parse(json)?.exp;\n return typeof exp === 'number' ? exp * 1000 : undefined;\n } catch {\n return undefined;\n }\n}\n\n/** Whatever the integrator returned, as a token with an expiry we can compare. */\nexport function normaliseSession(\n result: string | TenantSessionToken,\n): TenantSessionToken {\n if (typeof result === 'string') {\n return { token: result, expiresAt: expiryOf(result) };\n }\n const declared =\n result.expiresAt ??\n (result.expiresIn ? Date.now() + result.expiresIn * 1000 : undefined);\n return { ...result, expiresAt: declared ?? expiryOf(result.token) };\n}\n\n/**\n * One session for a whole tree of components.\n *\n * Every component in this library builds its own API client, and each of those\n * dedupes only its own renewals — so N clients ask for N tokens. Opening the\n * drawer fired three requests at the integrator's backend before anyone had\n * said a word. The sharing belongs where there is exactly one of something,\n * which is the provider, and not in each client, where there is not.\n *\n * `force` is what a client passes after the API refused the token it just used.\n * The cached answer is then known to be dead, so it is skipped: without that, a\n * shared cache would hand the refused token straight back and turn a\n * recoverable 401 into a widget that never speaks again.\n */\nexport function createSharedSession(\n source: () => Promise<string | TenantSessionToken>,\n): (force?: boolean) => Promise<TenantSessionToken> {\n let value: TenantSessionToken | undefined;\n let inFlight: Promise<TenantSessionToken> | undefined;\n\n return (force = false) => {\n const stillGood =\n value &&\n (value.expiresAt === undefined ||\n value.expiresAt - Date.now() > RENEWAL_MARGIN_MS);\n if (stillGood && !force) return Promise.resolve(value!);\n\n if (!inFlight) {\n inFlight = Promise.resolve(source())\n .then((result) => {\n value = normaliseSession(result);\n return value;\n })\n .finally(() => {\n inFlight = undefined;\n });\n }\n return inFlight;\n };\n}\n"],"names":[],"mappings":"AAEA;;;;AAIG;AACH,MAAM,iBAAiB,GAAG,KAAM;AAEhC;AACA,SAAS,QAAQ,CAAC,KAAa,EAAA;AAC7B,IAAA,IAAI;QACF,MAAM,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;AACnC,QAAA,IAAI,CAAC,OAAO;AAAE,YAAA,OAAO,SAAS;QAC9B,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;QAChE,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,GAAG;AACjC,QAAA,OAAO,OAAO,GAAG,KAAK,QAAQ,GAAG,GAAG,GAAG,IAAI,GAAG,SAAS;IACzD;AAAE,IAAA,MAAM;AACN,QAAA,OAAO,SAAS;IAClB;AACF;AAEA;AACM,SAAU,gBAAgB,CAC9B,MAAmC,EAAA;AAEnC,IAAA,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE;AAC9B,QAAA,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,EAAE;IACvD;AACA,IAAA,MAAM,QAAQ,GACZ,MAAM,CAAC,SAAS;SACf,MAAM,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC,SAAS,GAAG,IAAI,GAAG,SAAS,CAAC;AACvE,IAAA,OAAO,EAAE,GAAG,MAAM,EAAE,SAAS,EAAE,QAAQ,IAAI,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE;AACrE;AAEA;;;;;;;;;;;;;AAaG;AACG,SAAU,mBAAmB,CACjC,MAAkD,EAAA;AAElD,IAAA,IAAI,KAAqC;AACzC,IAAA,IAAI,QAAiD;AAErD,IAAA,OAAO,CAAC,KAAK,GAAG,KAAK,KAAI;QACvB,MAAM,SAAS,GACb,KAAK;AACL,aAAC,KAAK,CAAC,SAAS,KAAK,SAAS;gBAC5B,KAAK,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,iBAAiB,CAAC;QACrD,IAAI,SAAS,IAAI,CAAC,KAAK;AAAE,YAAA,OAAO,OAAO,CAAC,OAAO,CAAC,KAAM,CAAC;QAEvD,IAAI,CAAC,QAAQ,EAAE;AACb,YAAA,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE;AAChC,iBAAA,IAAI,CAAC,CAAC,MAAM,KAAI;AACf,gBAAA,KAAK,GAAG,gBAAgB,CAAC,MAAM,CAAC;AAChC,gBAAA,OAAO,KAAK;AACd,YAAA,CAAC;iBACA,OAAO,CAAC,MAAK;gBACZ,QAAQ,GAAG,SAAS;AACtB,YAAA,CAAC,CAAC;QACN;AACA,QAAA,OAAO,QAAQ;AACjB,IAAA,CAAC;AACH;;;;"}
@@ -139,6 +139,18 @@ export interface DevicProviderConfig {
139
139
  * @default 1000
140
140
  */
141
141
  pollingInterval?: number;
142
+ /**
143
+ * Follow a conversation in progress over a server-sent event stream
144
+ * instead of polling it, for every component under this provider. The
145
+ * assistant's reply then arrives as it is produced, and the request per
146
+ * `pollingInterval` becomes a fallback that only runs while the stream is
147
+ * down. Needs an API that serves `GET .../chats/:chatUid/stream`; against
148
+ * an older one the widgets notice and keep polling. Overridable per
149
+ * component via its own `streaming`.
150
+ *
151
+ * @default false
152
+ */
153
+ streaming?: boolean;
142
154
  /**
143
155
  * Translations for the texts the library renders itself — the drawer
144
156
  * header, the input placeholder, the buttons and tooltips of every widget.
@@ -247,6 +259,12 @@ export interface DevicContextValue {
247
259
  * integrator did not configure one, so each component keeps its own default.
248
260
  */
249
261
  pollingInterval?: number;
262
+ /**
263
+ * Whether conversations are followed over a server-sent event stream.
264
+ * Undefined when the integrator did not choose, so each component keeps its
265
+ * own default.
266
+ */
267
+ streaming?: boolean;
250
268
  /**
251
269
  * Translations for the texts the library renders itself, keyed by the
252
270
  * English text. Undefined when the integrator configured none.
@@ -1 +1 @@
1
- {"version":3,"file":"avatar.js","sources":["../../../src/utils/avatar.ts"],"sourcesContent":["import { blobatarUri } from 'blobatar/uri';\nimport type { BlobatarOptions } from 'blobatar';\n\n/**\n * The generated avatar an agent or an assistant falls back to when it has no\n * uploaded image.\n *\n * Derived from the entity's id, and the platform derives it the same way from\n * the same value — so an assistant embedded here wears the face its owner sees\n * in the console. `AvatarStyle` is only what has been pinned on top of that:\n * an absent field keeps following the id.\n */\nexport type AvatarShape =\n | 'round'\n | 'organic'\n | 'boxy'\n | 'nub'\n | 'cloud'\n | 'sun';\n\nexport type AvatarTone =\n | 'pastel'\n | 'neutral'\n | 'mid'\n | 'deep'\n | 'bright'\n | 'ink';\n\nexport interface AvatarStyle {\n shape?: AvatarShape;\n /** Hue in degrees, 0-359. */\n hue?: number;\n tone?: AvatarTone;\n}\n\n/**\n * Where each name sits in the renderer's 0-1 trait space — the middle of each\n * band, so a pinned value lands nowhere near a threshold. Kept identical to the\n * platform's mapping: the same style must resolve to the same avatar in both.\n */\nconst SHAPE_POSITION: Record<AvatarShape, number> = {\n round: 0.14,\n organic: 0.43,\n boxy: 0.65,\n nub: 0.78,\n cloud: 0.885,\n sun: 0.965,\n};\n\nconst TONE_POSITION: Record<AvatarTone, number> = {\n pastel: 0.1,\n neutral: 0.28,\n mid: 0.49,\n deep: 0.71,\n bright: 0.865,\n ink: 0.965,\n};\n\nfunction blobatarOptions(style?: AvatarStyle): BlobatarOptions {\n const opts: BlobatarOptions = {};\n if (style?.shape) opts.traits = { shape: SHAPE_POSITION[style.shape] };\n if (typeof style?.hue === 'number') opts.hue = style.hue;\n if (style?.tone) opts.tone = TONE_POSITION[style.tone];\n return opts;\n}\n\n/**\n * Memoised: a drawer header and every hand-off card in the transcript can be\n * asking for the same few avatars, and each one is a few hundred bytes of path\n * data to solve. Bounded, since a long conversation can hand off to many\n * subagents.\n */\nconst cache = new Map<string, string>();\nconst CACHE_LIMIT = 100;\n\nexport function avatarUri(seed: string, style?: AvatarStyle): string {\n const key = `${seed}|${style?.shape ?? ''}|${style?.hue ?? ''}|${style?.tone ?? ''}`;\n const hit = cache.get(key);\n if (hit) return hit;\n\n const uri = blobatarUri(seed, blobatarOptions(style));\n if (cache.size >= CACHE_LIMIT) {\n const oldest = cache.keys().next().value;\n if (oldest !== undefined) cache.delete(oldest);\n }\n cache.set(key, uri);\n return uri;\n}\n"],"names":[],"mappings":";;AAmCA;;;;AAIG;AACH,MAAM,cAAc,GAAgC;AAClD,IAAA,KAAK,EAAE,IAAI;AACX,IAAA,OAAO,EAAE,IAAI;AACb,IAAA,IAAI,EAAE,IAAI;AACV,IAAA,GAAG,EAAE,IAAI;AACT,IAAA,KAAK,EAAE,KAAK;AACZ,IAAA,GAAG,EAAE,KAAK;CACX;AAED,MAAM,aAAa,GAA+B;AAChD,IAAA,MAAM,EAAE,GAAG;AACX,IAAA,OAAO,EAAE,IAAI;AACb,IAAA,GAAG,EAAE,IAAI;AACT,IAAA,IAAI,EAAE,IAAI;AACV,IAAA,MAAM,EAAE,KAAK;AACb,IAAA,GAAG,EAAE,KAAK;CACX;AAED,SAAS,eAAe,CAAC,KAAmB,EAAA;IAC1C,MAAM,IAAI,GAAoB,EAAE;IAChC,IAAI,KAAK,EAAE,KAAK;AAAE,QAAA,IAAI,CAAC,MAAM,GAAG,EAAE,KAAK,EAAE,cAAc,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE;AACtE,IAAA,IAAI,OAAO,KAAK,EAAE,GAAG,KAAK,QAAQ;AAAE,QAAA,IAAI,CAAC,GAAG,GAAG,KAAK,CAAC,GAAG;IACxD,IAAI,KAAK,EAAE,IAAI;QAAE,IAAI,CAAC,IAAI,GAAG,aAAa,CAAC,KAAK,CAAC,IAAI,CAAC;AACtD,IAAA,OAAO,IAAI;AACb;AAEA;;;;;AAKG;AACH,MAAM,KAAK,GAAG,IAAI,GAAG,EAAkB;AACvC,MAAM,WAAW,GAAG,GAAG;AAEjB,SAAU,SAAS,CAAC,IAAY,EAAE,KAAmB,EAAA;IACzD,MAAM,GAAG,GAAG,CAAA,EAAG,IAAI,IAAI,KAAK,EAAE,KAAK,IAAI,EAAE,CAAA,CAAA,EAAI,KAAK,EAAE,GAAG,IAAI,EAAE,CAAA,CAAA,EAAI,KAAK,EAAE,IAAI,IAAI,EAAE,CAAA,CAAE;IACpF,MAAM,GAAG,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC;AAC1B,IAAA,IAAI,GAAG;AAAE,QAAA,OAAO,GAAG;IAEnB,MAAM,GAAG,GAAG,WAAW,CAAC,IAAI,EAAE,eAAe,CAAC,KAAK,CAAC,CAAC;AACrD,IAAA,IAAI,KAAK,CAAC,IAAI,IAAI,WAAW,EAAE;QAC7B,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK;QACxC,IAAI,MAAM,KAAK,SAAS;AAAE,YAAA,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC;IAChD;AACA,IAAA,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC;AACnB,IAAA,OAAO,GAAG;AACZ;;"}
1
+ {"version":3,"file":"avatar.js","sources":["../../../src/utils/avatar.ts"],"sourcesContent":["import { blobatarUri } from 'blobatar/uri';\nimport type { BlobatarOptions } from 'blobatar';\n\n/**\n * The generated avatar an agent or an assistant falls back to when it has no\n * uploaded image.\n *\n * Derived from the entity's id, and the platform derives it the same way from\n * the same value — so an assistant embedded here wears the face its owner sees\n * in the console. `AvatarStyle` is only what has been pinned on top of that:\n * an absent field keeps following the id.\n */\nexport type AvatarShape =\n | 'round'\n | 'organic'\n | 'boxy'\n | 'nub'\n | 'cloud'\n | 'sun';\n\nexport type AvatarTone =\n | 'pastel'\n | 'neutral'\n | 'mid'\n | 'deep'\n | 'bright'\n | 'ink';\n\nexport interface AvatarStyle {\n shape?: AvatarShape;\n /** Hue in degrees, 0-359. */\n hue?: number;\n tone?: AvatarTone;\n}\n\n/**\n * Where each name sits in the renderer's 0-1 trait space — the middle of each\n * band, so a pinned value lands nowhere near a threshold. Kept identical to the\n * platform's mapping: the same style must resolve to the same avatar in both.\n */\nconst SHAPE_POSITION: Record<AvatarShape, number> = {\n round: 0.14,\n organic: 0.43,\n boxy: 0.65,\n nub: 0.78,\n cloud: 0.885,\n sun: 0.965,\n};\n\nconst TONE_POSITION: Record<AvatarTone, number> = {\n pastel: 0.1,\n neutral: 0.28,\n mid: 0.49,\n deep: 0.71,\n bright: 0.865,\n ink: 0.965,\n};\n\nfunction blobatarOptions(style?: AvatarStyle): BlobatarOptions {\n const opts: BlobatarOptions = {};\n if (style?.shape) opts.traits = { shape: SHAPE_POSITION[style.shape] };\n if (typeof style?.hue === 'number') opts.hue = style.hue;\n if (style?.tone) opts.tone = TONE_POSITION[style.tone];\n return opts;\n}\n\n/**\n * Memoised: a drawer header and every hand-off card in the transcript can be\n * asking for the same few avatars, and each one is a few hundred bytes of path\n * data to solve. Bounded, since a long conversation can hand off to many\n * subagents.\n */\nconst cache = new Map<string, string>();\nconst CACHE_LIMIT = 100;\n\nexport function avatarUri(seed: string, style?: AvatarStyle): string {\n const key = `${seed}|${style?.shape ?? ''}|${style?.hue ?? ''}|${style?.tone ?? ''}`;\n const hit = cache.get(key);\n if (hit) return hit;\n\n const uri = blobatarUri(seed, blobatarOptions(style));\n if (cache.size >= CACHE_LIMIT) {\n const oldest = cache.keys().next().value;\n if (oldest !== undefined) cache.delete(oldest);\n }\n cache.set(key, uri);\n return uri;\n}\n"],"names":[],"mappings":";;AAmCA;;;;AAIG;AACH,MAAM,cAAc,GAAgC;AAClD,IAAA,KAAK,EAAE,IAAI;AACX,IAAA,OAAO,EAAE,IAAI;AACb,IAAA,IAAI,EAAE,IAAI;AACV,IAAA,GAAG,EAAE,IAAI;AACT,IAAA,KAAK,EAAE,KAAK;AACZ,IAAA,GAAG,EAAE,KAAK;CACX;AAED,MAAM,aAAa,GAA+B;AAChD,IAAA,MAAM,EAAE,GAAG;AACX,IAAA,OAAO,EAAE,IAAI;AACb,IAAA,GAAG,EAAE,IAAI;AACT,IAAA,IAAI,EAAE,IAAI;AACV,IAAA,MAAM,EAAE,KAAK;AACb,IAAA,GAAG,EAAE,KAAK;CACX;AAED,SAAS,eAAe,CAAC,KAAmB,EAAA;IAC1C,MAAM,IAAI,GAAoB,EAAE;IAChC,IAAI,KAAK,EAAE,KAAK;AAAE,QAAA,IAAI,CAAC,MAAM,GAAG,EAAE,KAAK,EAAE,cAAc,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE;AACtE,IAAA,IAAI,OAAO,KAAK,EAAE,GAAG,KAAK,QAAQ;AAAE,QAAA,IAAI,CAAC,GAAG,GAAG,KAAK,CAAC,GAAG;IACxD,IAAI,KAAK,EAAE,IAAI;QAAE,IAAI,CAAC,IAAI,GAAG,aAAa,CAAC,KAAK,CAAC,IAAI,CAAC;AACtD,IAAA,OAAO,IAAI;AACb;AAEA;;;;;AAKG;AACH,MAAM,KAAK,GAAG,IAAI,GAAG,EAAkB;AACvC,MAAM,WAAW,GAAG,GAAG;AAEjB,SAAU,SAAS,CAAC,IAAY,EAAE,KAAmB,EAAA;IACzD,MAAM,GAAG,GAAG,CAAA,EAAG,IAAI,IAAI,KAAK,EAAE,KAAK,IAAI,EAAE,CAAA,CAAA,EAAI,KAAK,EAAE,GAAG,IAAI,EAAE,CAAA,CAAA,EAAI,KAAK,EAAE,IAAI,IAAI,EAAE,CAAA,CAAE;IACpF,MAAM,GAAG,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC;AAC1B,IAAA,IAAI,GAAG;AAAE,QAAA,OAAO,GAAG;IAEnB,MAAM,GAAG,GAAG,WAAW,CAAC,IAAI,EAAE,eAAe,CAAC,KAAK,CAAC,CAAC;AACrD,IAAA,IAAI,KAAK,CAAC,IAAI,IAAI,WAAW,EAAE;QAC7B,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK;QACxC,IAAI,MAAM,KAAK,SAAS;AAAE,YAAA,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC;IAChD;AACA,IAAA,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC;AACnB,IAAA,OAAO,GAAG;AACZ;;;;"}