@devicai/ui 0.57.0 → 0.59.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 (223) hide show
  1. package/README.md +186 -0
  2. package/dist/cjs/api/assistantInfo.js.map +1 -1
  3. package/dist/cjs/api/client.js +17 -1
  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 +12 -8
  7. package/dist/cjs/components/AICommandBar/AICommandBar.js.map +1 -1
  8. package/dist/cjs/components/AICommandBar/useAICommandBar.js +12 -6
  9. package/dist/cjs/components/AICommandBar/useAICommandBar.js.map +1 -1
  10. package/dist/cjs/components/AIElementWrapper/AIElementWrapper.js +19 -15
  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 +12 -6
  14. package/dist/cjs/components/AIElementWrapper/useAIElementWrapper.js.map +1 -1
  15. package/dist/cjs/components/AIGenerationButton/AIGenerationButton.js +13 -7
  16. package/dist/cjs/components/AIGenerationButton/AIGenerationButton.js.map +1 -1
  17. package/dist/cjs/components/AIGenerationButton/useAIGenerationButton.js +11 -5
  18. package/dist/cjs/components/AIGenerationButton/useAIGenerationButton.js.map +1 -1
  19. package/dist/cjs/components/ChatDrawer/ChatDrawer.js +28 -13
  20. package/dist/cjs/components/ChatDrawer/ChatDrawer.js.map +1 -1
  21. package/dist/cjs/components/ChatDrawer/ChatInput.js +21 -15
  22. package/dist/cjs/components/ChatDrawer/ChatInput.js.map +1 -1
  23. package/dist/cjs/components/ChatDrawer/ChatMessages.js +54 -13
  24. package/dist/cjs/components/ChatDrawer/ChatMessages.js.map +1 -1
  25. package/dist/cjs/components/ChatDrawer/CompactionWidget.js +13 -4
  26. package/dist/cjs/components/ChatDrawer/CompactionWidget.js.map +1 -1
  27. package/dist/cjs/components/ChatDrawer/ConversationSelector.js +6 -4
  28. package/dist/cjs/components/ChatDrawer/ConversationSelector.js.map +1 -1
  29. package/dist/cjs/components/ChatDrawer/ErrorBoundary.js.map +1 -1
  30. package/dist/cjs/components/ChatDrawer/GuardrailNotice.js +40 -0
  31. package/dist/cjs/components/ChatDrawer/GuardrailNotice.js.map +1 -0
  32. package/dist/cjs/components/ChatDrawer/HandoffSubagentWidget.js +3 -1
  33. package/dist/cjs/components/ChatDrawer/HandoffSubagentWidget.js.map +1 -1
  34. package/dist/cjs/components/ChatDrawer/LimitBanner.js +19 -9
  35. package/dist/cjs/components/ChatDrawer/LimitBanner.js.map +1 -1
  36. package/dist/cjs/components/ChatDrawer/QueueNotice.js +19 -7
  37. package/dist/cjs/components/ChatDrawer/QueueNotice.js.map +1 -1
  38. package/dist/cjs/components/ChatDrawer/RecalledMemoriesWidget.js +17 -5
  39. package/dist/cjs/components/ChatDrawer/RecalledMemoriesWidget.js.map +1 -1
  40. package/dist/cjs/components/ChatDrawer/ReferenceChip.js +3 -1
  41. package/dist/cjs/components/ChatDrawer/ReferenceChip.js.map +1 -1
  42. package/dist/cjs/components/ChatDrawer/ToolTimeline.js.map +1 -1
  43. package/dist/cjs/components/ChatDrawer/UsageBar.js +23 -18
  44. package/dist/cjs/components/ChatDrawer/UsageBar.js.map +1 -1
  45. package/dist/cjs/components/ChatDrawer/pastedText.js.map +1 -1
  46. package/dist/cjs/components/CoreMemoryModal/CoreMemoryModal.js +20 -9
  47. package/dist/cjs/components/CoreMemoryModal/CoreMemoryModal.js.map +1 -1
  48. package/dist/cjs/components/Feedback/FeedbackModal.js +5 -1
  49. package/dist/cjs/components/Feedback/FeedbackModal.js.map +1 -1
  50. package/dist/cjs/components/Feedback/MessageActions.js +3 -1
  51. package/dist/cjs/components/Feedback/MessageActions.js.map +1 -1
  52. package/dist/cjs/components/IntegrationsModal/ConnectFieldsForm.js +12 -8
  53. package/dist/cjs/components/IntegrationsModal/ConnectFieldsForm.js.map +1 -1
  54. package/dist/cjs/components/IntegrationsModal/IntegrationLogo.js.map +1 -1
  55. package/dist/cjs/components/IntegrationsModal/IntegrationsHint.js +5 -3
  56. package/dist/cjs/components/IntegrationsModal/IntegrationsHint.js.map +1 -1
  57. package/dist/cjs/components/IntegrationsModal/IntegrationsLauncher.js +12 -3
  58. package/dist/cjs/components/IntegrationsModal/IntegrationsLauncher.js.map +1 -1
  59. package/dist/cjs/components/IntegrationsModal/IntegrationsModal.js.map +1 -1
  60. package/dist/cjs/components/IntegrationsModal/IntegrationsPanel.js +34 -24
  61. package/dist/cjs/components/IntegrationsModal/IntegrationsPanel.js.map +1 -1
  62. package/dist/cjs/components/IntegrationsModal/IntegrationsToggle.js +15 -6
  63. package/dist/cjs/components/IntegrationsModal/IntegrationsToggle.js.map +1 -1
  64. package/dist/cjs/components/IntegrationsModal/McpConnectForm.js +15 -7
  65. package/dist/cjs/components/IntegrationsModal/McpConnectForm.js.map +1 -1
  66. package/dist/cjs/components/IntegrationsModal/McpServersSection.js +32 -19
  67. package/dist/cjs/components/IntegrationsModal/McpServersSection.js.map +1 -1
  68. package/dist/cjs/components/IntegrationsModal/integrationChoice.js.map +1 -1
  69. package/dist/cjs/components/IntegrationsModal/useIntegrations.js +3 -1
  70. package/dist/cjs/components/IntegrationsModal/useIntegrations.js.map +1 -1
  71. package/dist/cjs/components/IntegrationsModal/useTenantMcp.js +3 -1
  72. package/dist/cjs/components/IntegrationsModal/useTenantMcp.js.map +1 -1
  73. package/dist/cjs/components/ThreadStateTag/ThreadStateTag.js +63 -40
  74. package/dist/cjs/components/ThreadStateTag/ThreadStateTag.js.map +1 -1
  75. package/dist/cjs/components/theme.js.map +1 -1
  76. package/dist/cjs/hooks/useDevicChat.js +21 -7
  77. package/dist/cjs/hooks/useDevicChat.js.map +1 -1
  78. package/dist/cjs/hooks/useModelInterface.js.map +1 -1
  79. package/dist/cjs/hooks/usePolling.js +117 -7
  80. package/dist/cjs/hooks/usePolling.js.map +1 -1
  81. package/dist/cjs/hooks/useSpeechRecording.js +8 -4
  82. package/dist/cjs/hooks/useSpeechRecording.js.map +1 -1
  83. package/dist/cjs/i18n/translate.js +48 -0
  84. package/dist/cjs/i18n/translate.js.map +1 -0
  85. package/dist/cjs/i18n/useTranslations.js +57 -0
  86. package/dist/cjs/i18n/useTranslations.js.map +1 -0
  87. package/dist/cjs/index.js +11 -0
  88. package/dist/cjs/index.js.map +1 -1
  89. package/dist/cjs/provider/DevicContext.js.map +1 -1
  90. package/dist/cjs/provider/DevicProvider.js +5 -1
  91. package/dist/cjs/provider/DevicProvider.js.map +1 -1
  92. package/dist/cjs/provider/sharedSession.js.map +1 -1
  93. package/dist/cjs/styles.css +1 -1
  94. package/dist/cjs/utils/avatar.js.map +1 -1
  95. package/dist/cjs/utils/consumeChatStream.js +44 -0
  96. package/dist/cjs/utils/consumeChatStream.js.map +1 -0
  97. package/dist/cjs/utils/index.js.map +1 -1
  98. package/dist/cjs/utils/logger.js.map +1 -1
  99. package/dist/cjs/utils/toolGroups.js.map +1 -1
  100. package/dist/esm/api/assistantInfo.js.map +1 -1
  101. package/dist/esm/api/client.d.ts +5 -1
  102. package/dist/esm/api/client.js +17 -1
  103. package/dist/esm/api/client.js.map +1 -1
  104. package/dist/esm/api/types.d.ts +12 -1
  105. package/dist/esm/api/types.js.map +1 -1
  106. package/dist/esm/components/AICommandBar/AICommandBar.js +13 -9
  107. package/dist/esm/components/AICommandBar/AICommandBar.js.map +1 -1
  108. package/dist/esm/components/AICommandBar/AICommandBar.types.d.ts +16 -0
  109. package/dist/esm/components/AICommandBar/useAICommandBar.d.ts +2 -0
  110. package/dist/esm/components/AICommandBar/useAICommandBar.js +13 -7
  111. package/dist/esm/components/AICommandBar/useAICommandBar.js.map +1 -1
  112. package/dist/esm/components/AIElementWrapper/AIElementWrapper.js +19 -15
  113. package/dist/esm/components/AIElementWrapper/AIElementWrapper.js.map +1 -1
  114. package/dist/esm/components/AIElementWrapper/AIElementWrapper.types.d.ts +16 -0
  115. package/dist/esm/components/AIElementWrapper/activeWrapperRegistry.js.map +1 -1
  116. package/dist/esm/components/AIElementWrapper/useAIElementWrapper.d.ts +2 -0
  117. package/dist/esm/components/AIElementWrapper/useAIElementWrapper.js +13 -7
  118. package/dist/esm/components/AIElementWrapper/useAIElementWrapper.js.map +1 -1
  119. package/dist/esm/components/AIGenerationButton/AIGenerationButton.js +14 -8
  120. package/dist/esm/components/AIGenerationButton/AIGenerationButton.js.map +1 -1
  121. package/dist/esm/components/AIGenerationButton/AIGenerationButton.types.d.ts +16 -0
  122. package/dist/esm/components/AIGenerationButton/useAIGenerationButton.d.ts +2 -0
  123. package/dist/esm/components/AIGenerationButton/useAIGenerationButton.js +12 -6
  124. package/dist/esm/components/AIGenerationButton/useAIGenerationButton.js.map +1 -1
  125. package/dist/esm/components/ChatDrawer/ChatDrawer.js +29 -14
  126. package/dist/esm/components/ChatDrawer/ChatDrawer.js.map +1 -1
  127. package/dist/esm/components/ChatDrawer/ChatDrawer.types.d.ts +41 -0
  128. package/dist/esm/components/ChatDrawer/ChatInput.js +21 -15
  129. package/dist/esm/components/ChatDrawer/ChatInput.js.map +1 -1
  130. package/dist/esm/components/ChatDrawer/ChatMessages.d.ts +1 -1
  131. package/dist/esm/components/ChatDrawer/ChatMessages.js +54 -13
  132. package/dist/esm/components/ChatDrawer/ChatMessages.js.map +1 -1
  133. package/dist/esm/components/ChatDrawer/CompactionWidget.js +13 -4
  134. package/dist/esm/components/ChatDrawer/CompactionWidget.js.map +1 -1
  135. package/dist/esm/components/ChatDrawer/ConversationSelector.js +6 -4
  136. package/dist/esm/components/ChatDrawer/ConversationSelector.js.map +1 -1
  137. package/dist/esm/components/ChatDrawer/ErrorBoundary.js.map +1 -1
  138. package/dist/esm/components/ChatDrawer/GuardrailNotice.d.ts +36 -0
  139. package/dist/esm/components/ChatDrawer/GuardrailNotice.js +38 -0
  140. package/dist/esm/components/ChatDrawer/GuardrailNotice.js.map +1 -0
  141. package/dist/esm/components/ChatDrawer/HandoffSubagentWidget.js +3 -1
  142. package/dist/esm/components/ChatDrawer/HandoffSubagentWidget.js.map +1 -1
  143. package/dist/esm/components/ChatDrawer/LimitBanner.js +19 -9
  144. package/dist/esm/components/ChatDrawer/LimitBanner.js.map +1 -1
  145. package/dist/esm/components/ChatDrawer/QueueNotice.js +19 -7
  146. package/dist/esm/components/ChatDrawer/QueueNotice.js.map +1 -1
  147. package/dist/esm/components/ChatDrawer/RecalledMemoriesWidget.js +17 -5
  148. package/dist/esm/components/ChatDrawer/RecalledMemoriesWidget.js.map +1 -1
  149. package/dist/esm/components/ChatDrawer/ReferenceChip.js +3 -1
  150. package/dist/esm/components/ChatDrawer/ReferenceChip.js.map +1 -1
  151. package/dist/esm/components/ChatDrawer/ToolTimeline.js.map +1 -1
  152. package/dist/esm/components/ChatDrawer/UsageBar.js +23 -18
  153. package/dist/esm/components/ChatDrawer/UsageBar.js.map +1 -1
  154. package/dist/esm/components/ChatDrawer/index.d.ts +2 -0
  155. package/dist/esm/components/ChatDrawer/pastedText.js.map +1 -1
  156. package/dist/esm/components/CoreMemoryModal/CoreMemoryModal.js +20 -9
  157. package/dist/esm/components/CoreMemoryModal/CoreMemoryModal.js.map +1 -1
  158. package/dist/esm/components/Feedback/FeedbackModal.js +5 -1
  159. package/dist/esm/components/Feedback/FeedbackModal.js.map +1 -1
  160. package/dist/esm/components/Feedback/MessageActions.js +3 -1
  161. package/dist/esm/components/Feedback/MessageActions.js.map +1 -1
  162. package/dist/esm/components/IntegrationsModal/ConnectFieldsForm.js +12 -8
  163. package/dist/esm/components/IntegrationsModal/ConnectFieldsForm.js.map +1 -1
  164. package/dist/esm/components/IntegrationsModal/IntegrationLogo.js.map +1 -1
  165. package/dist/esm/components/IntegrationsModal/IntegrationsHint.js +5 -3
  166. package/dist/esm/components/IntegrationsModal/IntegrationsHint.js.map +1 -1
  167. package/dist/esm/components/IntegrationsModal/IntegrationsLauncher.js +12 -3
  168. package/dist/esm/components/IntegrationsModal/IntegrationsLauncher.js.map +1 -1
  169. package/dist/esm/components/IntegrationsModal/IntegrationsModal.js.map +1 -1
  170. package/dist/esm/components/IntegrationsModal/IntegrationsPanel.d.ts +1 -1
  171. package/dist/esm/components/IntegrationsModal/IntegrationsPanel.js +34 -24
  172. package/dist/esm/components/IntegrationsModal/IntegrationsPanel.js.map +1 -1
  173. package/dist/esm/components/IntegrationsModal/IntegrationsToggle.d.ts +1 -1
  174. package/dist/esm/components/IntegrationsModal/IntegrationsToggle.js +15 -6
  175. package/dist/esm/components/IntegrationsModal/IntegrationsToggle.js.map +1 -1
  176. package/dist/esm/components/IntegrationsModal/McpConnectForm.js +15 -7
  177. package/dist/esm/components/IntegrationsModal/McpConnectForm.js.map +1 -1
  178. package/dist/esm/components/IntegrationsModal/McpServersSection.js +32 -19
  179. package/dist/esm/components/IntegrationsModal/McpServersSection.js.map +1 -1
  180. package/dist/esm/components/IntegrationsModal/integrationChoice.js.map +1 -1
  181. package/dist/esm/components/IntegrationsModal/useIntegrations.js +3 -1
  182. package/dist/esm/components/IntegrationsModal/useIntegrations.js.map +1 -1
  183. package/dist/esm/components/IntegrationsModal/useTenantMcp.js +3 -1
  184. package/dist/esm/components/IntegrationsModal/useTenantMcp.js.map +1 -1
  185. package/dist/esm/components/ThreadStateTag/ThreadStateTag.js +63 -40
  186. package/dist/esm/components/ThreadStateTag/ThreadStateTag.js.map +1 -1
  187. package/dist/esm/components/theme.js.map +1 -1
  188. package/dist/esm/hooks/index.d.ts +1 -1
  189. package/dist/esm/hooks/useDevicChat.d.ts +6 -0
  190. package/dist/esm/hooks/useDevicChat.js +23 -9
  191. package/dist/esm/hooks/useDevicChat.js.map +1 -1
  192. package/dist/esm/hooks/useModelInterface.js.map +1 -1
  193. package/dist/esm/hooks/usePolling.d.ts +35 -0
  194. package/dist/esm/hooks/usePolling.js +114 -8
  195. package/dist/esm/hooks/usePolling.js.map +1 -1
  196. package/dist/esm/hooks/useSpeechRecording.js +8 -4
  197. package/dist/esm/hooks/useSpeechRecording.js.map +1 -1
  198. package/dist/esm/i18n/index.d.ts +4 -0
  199. package/dist/esm/i18n/translate.d.ts +23 -0
  200. package/dist/esm/i18n/translate.js +43 -0
  201. package/dist/esm/i18n/translate.js.map +1 -0
  202. package/dist/esm/i18n/types.d.ts +27 -0
  203. package/dist/esm/i18n/useTranslations.d.ts +32 -0
  204. package/dist/esm/i18n/useTranslations.js +53 -0
  205. package/dist/esm/i18n/useTranslations.js.map +1 -0
  206. package/dist/esm/index.d.ts +4 -2
  207. package/dist/esm/index.js +3 -0
  208. package/dist/esm/index.js.map +1 -1
  209. package/dist/esm/provider/DevicContext.js.map +1 -1
  210. package/dist/esm/provider/DevicProvider.d.ts +1 -1
  211. package/dist/esm/provider/DevicProvider.js +5 -1
  212. package/dist/esm/provider/DevicProvider.js.map +1 -1
  213. package/dist/esm/provider/sharedSession.js.map +1 -1
  214. package/dist/esm/provider/types.d.ts +51 -0
  215. package/dist/esm/styles.css +1 -1
  216. package/dist/esm/utils/avatar.js.map +1 -1
  217. package/dist/esm/utils/consumeChatStream.d.ts +6 -0
  218. package/dist/esm/utils/consumeChatStream.js +42 -0
  219. package/dist/esm/utils/consumeChatStream.js.map +1 -0
  220. package/dist/esm/utils/index.js.map +1 -1
  221. package/dist/esm/utils/logger.js.map +1 -1
  222. package/dist/esm/utils/toolGroups.js.map +1 -1
  223. 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,4 +1,5 @@
1
1
  import { useState, useRef, useCallback, useEffect } from 'react';
2
+ import { useTranslations } from '../i18n/useTranslations.js';
2
3
 
3
4
  const PREFERRED_MIME_TYPES = [
4
5
  'audio/webm;codecs=opus',
@@ -27,6 +28,7 @@ function pickMimeType(preferred) {
27
28
  */
28
29
  function useSpeechRecording(options = {}) {
29
30
  const { bars = 5, mimeType, autoStop = true, autoStopSilenceMs = 1000, autoStopCountdownMs = 1000, autoStopSilenceRatio = 0.1, autoStopSilenceLevel = 0.02, autoStopSpeechLevel = 0.12, onAutoStop, } = options;
31
+ const t = useTranslations();
30
32
  const isSupported = typeof navigator !== 'undefined' &&
31
33
  !!navigator.mediaDevices?.getUserMedia &&
32
34
  typeof MediaRecorder !== 'undefined';
@@ -194,7 +196,7 @@ function useSpeechRecording(options = {}) {
194
196
  }, [bars, evaluateAutoStop]);
195
197
  const start = useCallback(async () => {
196
198
  if (!isSupported) {
197
- setError('Audio recording is not supported in this browser');
199
+ setError(t('Audio recording is not supported in this browser'));
198
200
  return;
199
201
  }
200
202
  if (mediaRecorderRef.current)
@@ -253,12 +255,14 @@ function useSpeechRecording(options = {}) {
253
255
  cleanupAudioGraph();
254
256
  mediaRecorderRef.current = null;
255
257
  const message = e?.name === 'NotAllowedError'
256
- ? 'Microphone permission denied'
257
- : `Could not start recording: ${e?.message || 'unknown error'}`;
258
+ ? t('Microphone permission denied')
259
+ : t('Could not start recording: {error}', {
260
+ error: e?.message || t('unknown error'),
261
+ });
258
262
  setError(message);
259
263
  setStatus('idle');
260
264
  }
261
- }, [isSupported, mimeType, bars, tick, cleanupAudioGraph, resetAutoStop]);
265
+ }, [isSupported, mimeType, bars, tick, cleanupAudioGraph, resetAutoStop, t]);
262
266
  const pause = useCallback(() => {
263
267
  const recorder = mediaRecorderRef.current;
264
268
  if (recorder && recorder.state === 'recording') {
@@ -1 +1 @@
1
- {"version":3,"file":"useSpeechRecording.js","sources":["../../../src/hooks/useSpeechRecording.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\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 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('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 ? 'Microphone permission denied'\n : `Could not start recording: ${(e as Error)?.message || 'unknown error'}`;\n setError(message);\n setStatus('idle');\n }\n }, [isSupported, mimeType, bars, tick, cleanupAudioGraph, resetAutoStop]);\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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"}
@@ -0,0 +1,4 @@
1
+ export { DevicTranslationsProvider, useTranslations, useTranslationsDictionary, } from './useTranslations';
2
+ export type { DevicTranslationsProviderProps } from './useTranslations';
3
+ export { createTranslator, translate, mergeTranslations, fillTemplate, } from './translate';
4
+ export type { DevicTranslations, Translator } from './types';
@@ -0,0 +1,23 @@
1
+ import type { DevicTranslations, Translator } from './types';
2
+ /**
3
+ * Replace `{name}` placeholders. A placeholder with no matching value is
4
+ * dropped rather than left in the text — half-substituted braces on screen
5
+ * read as a bug to the end user.
6
+ */
7
+ export declare function fillTemplate(template: string, vars?: Record<string, string | number>): string;
8
+ /**
9
+ * Look one English text up in the dictionary, then fill its placeholders.
10
+ * An absent entry falls back to the English text itself, so a partial
11
+ * dictionary is perfectly usable.
12
+ */
13
+ export declare function translate(dictionary: DevicTranslations | undefined, text: string, vars?: Record<string, string | number>): string;
14
+ /**
15
+ * Build a translator bound to one dictionary. Stable for as long as the
16
+ * dictionary is, so it is safe in a dependency list.
17
+ */
18
+ export declare function createTranslator(dictionary?: DevicTranslations): Translator;
19
+ /**
20
+ * Merge dictionaries, later ones winning. `undefined` layers are skipped so
21
+ * callers can pass optional ones straight in.
22
+ */
23
+ export declare function mergeTranslations(...layers: (DevicTranslations | undefined)[]): DevicTranslations | undefined;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Replace `{name}` placeholders. A placeholder with no matching value is
3
+ * dropped rather than left in the text — half-substituted braces on screen
4
+ * read as a bug to the end user.
5
+ */
6
+ function fillTemplate(template, vars) {
7
+ if (!vars)
8
+ return template;
9
+ return template.replace(/\{(\w+)\}/g, (_, name) => {
10
+ const value = vars[name];
11
+ return value === undefined || value === null ? '' : String(value);
12
+ });
13
+ }
14
+ /**
15
+ * Look one English text up in the dictionary, then fill its placeholders.
16
+ * An absent entry falls back to the English text itself, so a partial
17
+ * dictionary is perfectly usable.
18
+ */
19
+ function translate(dictionary, text, vars) {
20
+ return fillTemplate(dictionary?.[text] ?? text, vars);
21
+ }
22
+ /**
23
+ * Build a translator bound to one dictionary. Stable for as long as the
24
+ * dictionary is, so it is safe in a dependency list.
25
+ */
26
+ function createTranslator(dictionary) {
27
+ return (text, vars) => translate(dictionary, text, vars);
28
+ }
29
+ /**
30
+ * Merge dictionaries, later ones winning. `undefined` layers are skipped so
31
+ * callers can pass optional ones straight in.
32
+ */
33
+ function mergeTranslations(...layers) {
34
+ const present = layers.filter(Boolean);
35
+ if (present.length === 0)
36
+ return undefined;
37
+ if (present.length === 1)
38
+ return present[0];
39
+ return Object.assign({}, ...present);
40
+ }
41
+
42
+ export { createTranslator, fillTemplate, mergeTranslations, translate };
43
+ //# sourceMappingURL=translate.js.map
@@ -0,0 +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;;;;"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Dictionary of translations for the texts this library renders itself.
3
+ *
4
+ * The key is the exact English text the widget shows; the value is what to
5
+ * show instead. Nothing else is needed — no key catalogue to learn, and no
6
+ * i18n runtime bundled here: you keep whatever you already use (i18next,
7
+ * react-intl, a plain object per locale) and hand over the resulting strings.
8
+ *
9
+ * ```ts
10
+ * const translations = {
11
+ * 'New chat': 'Nueva conversación',
12
+ * 'Type a message...': 'Escribe un mensaje...',
13
+ * '{count} left': 'Quedan {count}',
14
+ * };
15
+ * ```
16
+ *
17
+ * A text that carries a `{name}` placeholder keeps it in the translation:
18
+ * placeholders are filled after the lookup, so they can be reordered or
19
+ * dropped freely. A text with no entry is shown in English.
20
+ */
21
+ export type DevicTranslations = Record<string, string>;
22
+ /**
23
+ * Resolves one English text to what should be rendered.
24
+ *
25
+ * `vars` fills the `{name}` placeholders of the result.
26
+ */
27
+ export type Translator = (text: string, vars?: Record<string, string | number>) => string;
@@ -0,0 +1,32 @@
1
+ import React from 'react';
2
+ import type { DevicTranslations, Translator } from './types';
3
+ export interface DevicTranslationsProviderProps {
4
+ /** Entries added on top of the enclosing dictionaries. */
5
+ translations?: DevicTranslations;
6
+ children: React.ReactNode;
7
+ }
8
+ /**
9
+ * Scopes a dictionary to a subtree. Rarely needed directly — the components
10
+ * that take a `translations` option already wrap their own subtree with it —
11
+ * but useful to translate one region of a page differently.
12
+ */
13
+ export declare function DevicTranslationsProvider({ translations, children, }: DevicTranslationsProviderProps): JSX.Element;
14
+ /**
15
+ * The dictionary in force here: the provider's, plus any scoped by an
16
+ * enclosing component, plus the `local` argument. Later layers win.
17
+ */
18
+ export declare function useTranslationsDictionary(local?: DevicTranslations): DevicTranslations | undefined;
19
+ /**
20
+ * Returns `t`, the translator for every text this library renders itself.
21
+ *
22
+ * Works with or without a `DevicProvider`: with no dictionary anywhere it
23
+ * returns the English text unchanged, which is what a component mounted
24
+ * standalone gets.
25
+ *
26
+ * @example
27
+ * ```tsx
28
+ * const t = useTranslations();
29
+ * <button aria-label={t('New chat')}>…</button>
30
+ * ```
31
+ */
32
+ export declare function useTranslations(local?: DevicTranslations): Translator;