@pipecat-ai/client-react 1.1.0 → 1.2.1

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.
@@ -1,6 +1,6 @@
1
1
  import {jsx as $h9lXz$jsx, Fragment as $h9lXz$Fragment} from "react/jsx-runtime";
2
2
  import {RTVIEvent as $h9lXz$RTVIEvent, setAboutClient as $h9lXz$setAboutClient} from "@pipecat-ai/client-js";
3
- import $h9lXz$react, {useRef as $h9lXz$useRef, useEffect as $h9lXz$useEffect, useCallback as $h9lXz$useCallback, useContext as $h9lXz$useContext, createContext as $h9lXz$createContext, useState as $h9lXz$useState, forwardRef as $h9lXz$forwardRef} from "react";
3
+ import $h9lXz$react, {useRef as $h9lXz$useRef, useEffect as $h9lXz$useEffect, useCallback as $h9lXz$useCallback, useContext as $h9lXz$useContext, createContext as $h9lXz$createContext, useState as $h9lXz$useState, forwardRef as $h9lXz$forwardRef, useId as $h9lXz$useId, useMemo as $h9lXz$useMemo} from "react";
4
4
  import {atom as $h9lXz$atom, useAtomValue as $h9lXz$useAtomValue, createStore as $h9lXz$createStore} from "jotai";
5
5
  import {atomFamily as $h9lXz$atomFamily, useAtomCallback as $h9lXz$useAtomCallback} from "jotai/utils";
6
6
  import {Provider as $h9lXz$Provider} from "jotai/react";
@@ -31,13 +31,784 @@ import {Provider as $h9lXz$Provider} from "jotai/react";
31
31
 
32
32
 
33
33
  var $ad20387e24e513d4$exports = {};
34
- $ad20387e24e513d4$exports = JSON.parse("{\"name\":\"@pipecat-ai/client-react\",\"version\":\"1.1.0\",\"license\":\"BSD-2-Clause\",\"main\":\"dist/index.js\",\"module\":\"dist/index.module.js\",\"types\":\"dist/index.d.ts\",\"source\":\"src/index.ts\",\"repository\":{\"type\":\"git\",\"url\":\"git+https://github.com/pipecat-ai/pipecat-client-web.git\"},\"files\":[\"dist\",\"package.json\",\"README.md\"],\"scripts\":{\"build\":\"parcel build --no-cache\",\"dev\":\"parcel watch\",\"lint\":\"eslint . --report-unused-disable-directives --max-warnings 0 --ignore-pattern 'dist/'\"},\"devDependencies\":{\"@pipecat-ai/client-js\":\"*\",\"@types/react\":\"^18.3.3\",\"@types/react-dom\":\"^18.3.0\",\"@typescript-eslint/eslint-plugin\":\"^8.32.0\",\"eslint\":\"^9.11.1\",\"eslint-config-prettier\":\"^9.1.0\",\"eslint-plugin-react-hooks\":\"^5.2.0\",\"eslint-plugin-simple-import-sort\":\"^12.1.1\",\"parcel\":\"^2.12.0\",\"react\":\"^18.3.1\",\"react-dom\":\"^18.3.1\",\"typescript\":\"^5.2.2\"},\"peerDependencies\":{\"@pipecat-ai/client-js\":\"*\",\"react\":\">=18\",\"react-dom\":\">=18\"},\"dependencies\":{\"jotai\":\"^2.9.0\"}}");
34
+ $ad20387e24e513d4$exports = JSON.parse("{\"name\":\"@pipecat-ai/client-react\",\"version\":\"1.2.1\",\"license\":\"BSD-2-Clause\",\"main\":\"dist/index.js\",\"module\":\"dist/index.module.js\",\"types\":\"dist/index.d.ts\",\"source\":\"src/index.ts\",\"repository\":{\"type\":\"git\",\"url\":\"git+https://github.com/pipecat-ai/pipecat-client-web.git\"},\"exports\":{\".\":{\"types\":\"./dist/index.d.ts\",\"import\":\"./dist/index.module.js\",\"require\":\"./dist/index.js\"}},\"files\":[\"dist\",\"package.json\",\"README.md\"],\"scripts\":{\"build\":\"parcel build --no-cache\",\"dev\":\"parcel watch\",\"lint\":\"eslint . --report-unused-disable-directives --max-warnings 0 --ignore-pattern 'dist/'\",\"test\":\"jest\"},\"jest\":{\"preset\":\"ts-jest\",\"testEnvironment\":\"jsdom\",\"moduleNameMapper\":{\"^@/(.*)$\":\"<rootDir>/src/$1\"},\"transform\":{\"^.+\\\\.tsx?$\":[\"ts-jest\",{\"tsconfig\":{\"jsx\":\"react-jsx\",\"module\":\"commonjs\",\"moduleResolution\":\"node\",\"esModuleInterop\":true,\"allowImportingTsExtensions\":false,\"noUnusedLocals\":false,\"noUnusedParameters\":false}}]}},\"devDependencies\":{\"@jest/globals\":\"^30.2.0\",\"@pipecat-ai/client-js\":\"*\",\"@types/jest\":\"^30.0.0\",\"@types/react\":\"^18.3.3\",\"@types/react-dom\":\"^18.3.0\",\"@typescript-eslint/eslint-plugin\":\"^8.32.0\",\"eslint\":\"^9.11.1\",\"eslint-config-prettier\":\"^9.1.0\",\"eslint-plugin-react-hooks\":\"^5.2.0\",\"eslint-plugin-simple-import-sort\":\"^12.1.1\",\"jest\":\"^30.2.0\",\"jest-environment-jsdom\":\"^30.2.0\",\"parcel\":\"^2.12.0\",\"react\":\"^18.3.1\",\"react-dom\":\"^18.3.1\",\"ts-jest\":\"^29.4.6\",\"typescript\":\"^5.2.2\"},\"peerDependencies\":{\"@pipecat-ai/client-js\":\"*\",\"react\":\">=18\",\"react-dom\":\">=18\"},\"dependencies\":{\"jotai\":\"^2.9.0\"}}");
35
35
 
36
36
 
37
37
 
38
38
 
39
39
 
40
40
 
41
+ /**
42
+ * Copyright (c) 2024, Daily.
43
+ *
44
+ * SPDX-License-Identifier: BSD-2-Clause
45
+ */ /**
46
+ * Copyright (c) 2024, Daily.
47
+ *
48
+ * SPDX-License-Identifier: BSD-2-Clause
49
+ */ const $5989c1f88db60f73$export$7d7fdcf7579a70b2 = (text)=>{
50
+ return text.toLowerCase().replace(/[^\p{L}\p{N}\s]/gu, "");
51
+ };
52
+ const $5989c1f88db60f73$var$skipWhitespace = (text, start)=>{
53
+ let i = start;
54
+ while(i < text.length && /\s/.test(text[i]))i++;
55
+ return i;
56
+ };
57
+ /**
58
+ * Finds where `spoken` appears in `unspoken`, starting from `startPosition`.
59
+ * - Best-effort sequential word matching (normalized, punctuation-stripped)
60
+ * - Returns the original start position on mismatch (no advancement)
61
+ */ const $5989c1f88db60f73$var$findSpokenPositionInUnspoken = (spoken, unspoken, startPosition)=>{
62
+ if (!spoken || !unspoken || startPosition >= unspoken.length) return startPosition;
63
+ // If spoken text includes a leading separator space, skip leading whitespace in unspoken.
64
+ let actualStart = startPosition;
65
+ let spokenForMatching = spoken;
66
+ if (spoken.startsWith(" ") && startPosition < unspoken.length) {
67
+ actualStart = $5989c1f88db60f73$var$skipWhitespace(unspoken, startPosition);
68
+ spokenForMatching = spoken.trimStart();
69
+ } else if (startPosition === 0 && startPosition < unspoken.length) // If we're at the start, also skip leading whitespace (e.g. newlines)
70
+ actualStart = $5989c1f88db60f73$var$skipWhitespace(unspoken, 0);
71
+ const remainder = unspoken.slice(actualStart);
72
+ // Sentence-level: if spoken exactly matches the remainder (normalized), consume the whole part
73
+ // so we never leave a word unspoken due to word-matching edge cases.
74
+ if ($5989c1f88db60f73$export$7d7fdcf7579a70b2(spokenForMatching).trim() === $5989c1f88db60f73$export$7d7fdcf7579a70b2(remainder).trim()) return unspoken.length;
75
+ const spokenWords = $5989c1f88db60f73$export$7d7fdcf7579a70b2(spokenForMatching).split(/\s+/).filter(Boolean);
76
+ if (spokenWords.length === 0) return actualStart;
77
+ const unspokenWords = $5989c1f88db60f73$export$7d7fdcf7579a70b2(unspoken.slice(actualStart)).split(/\s+/).filter(Boolean);
78
+ // Sequential match, allowing prefix match for contractions (e.g. "I" vs "I'm")
79
+ // and limited skipping of mismatched unspoken words (e.g. punctuation artifacts).
80
+ let matchedWords = 0;
81
+ let consecutiveSkips = 0;
82
+ const MAX_CONSECUTIVE_SKIPS = 2;
83
+ for(let i = 0; i < unspokenWords.length && matchedWords < spokenWords.length; i++){
84
+ const target = spokenWords[matchedWords];
85
+ const candidate = unspokenWords[i];
86
+ if (candidate === target || candidate.startsWith(target)) {
87
+ matchedWords++;
88
+ consecutiveSkips = 0;
89
+ continue;
90
+ }
91
+ consecutiveSkips++;
92
+ if (consecutiveSkips > MAX_CONSECUTIVE_SKIPS) return actualStart;
93
+ // Skip this unspoken word and try matching the next one
94
+ }
95
+ if (matchedWords < spokenWords.length) return actualStart;
96
+ // Convert word matches back into a character position in the original unspoken string.
97
+ const isWordChar = (char)=>/[\p{L}\p{N}]/u.test(char);
98
+ let wordCount = 0;
99
+ let i = actualStart;
100
+ let inWord = false;
101
+ while(i < unspoken.length){
102
+ const charIsWord = isWordChar(unspoken[i]);
103
+ if (charIsWord && !inWord) {
104
+ inWord = true;
105
+ wordCount++;
106
+ if (wordCount === matchedWords) {
107
+ // Consume the rest of this word
108
+ i++;
109
+ while(i < unspoken.length && isWordChar(unspoken[i]))i++;
110
+ // Include any punctuation after the word until the next space, then include the space
111
+ while(i < unspoken.length){
112
+ if (unspoken[i] === " ") {
113
+ i++;
114
+ break;
115
+ }
116
+ i++;
117
+ }
118
+ return i;
119
+ }
120
+ } else if (!charIsWord && inWord) inWord = false;
121
+ i++;
122
+ }
123
+ return unspoken.length;
124
+ };
125
+ function $5989c1f88db60f73$export$802e9b947136afbf(cursor, parts) {
126
+ if (parts.length === 0) return false;
127
+ for(let i = 0; i < parts.length; i++){
128
+ if (typeof parts[i]?.text !== "string") continue;
129
+ if (!cursor.partFinalFlags[i]) return true;
130
+ }
131
+ return false;
132
+ }
133
+ function $5989c1f88db60f73$export$9b39fd8438081d6d(cursor, parts, spokenText) {
134
+ if (parts.length === 0) return false;
135
+ // Find the next part that should be spoken (skip parts already marked final/skipped)
136
+ let partToMatch = cursor.currentPartIndex;
137
+ while(partToMatch < parts.length && cursor.partFinalFlags[partToMatch])partToMatch++;
138
+ if (partToMatch >= parts.length) return false;
139
+ if (partToMatch > cursor.currentPartIndex) {
140
+ cursor.currentPartIndex = partToMatch;
141
+ cursor.currentCharIndex = 0;
142
+ }
143
+ const currentPart = parts[cursor.currentPartIndex];
144
+ if (typeof currentPart.text !== "string") return false;
145
+ const partText = currentPart.text;
146
+ const startChar = cursor.currentCharIndex;
147
+ // If the spoken text is pure punctuation (e.g. an em dash "—"), normalization
148
+ // strips it to empty and word-matching would fail. Treat it as successfully
149
+ // consumed so the cursor stays put and subsequent words continue matching.
150
+ if ($5989c1f88db60f73$export$7d7fdcf7579a70b2(spokenText).trim().length === 0) return true;
151
+ const newPosition = $5989c1f88db60f73$var$findSpokenPositionInUnspoken(spokenText, partText, startChar);
152
+ const whitespaceEnd = $5989c1f88db60f73$var$skipWhitespace(partText, startChar);
153
+ if (newPosition > whitespaceEnd) {
154
+ cursor.currentCharIndex = newPosition;
155
+ if (newPosition >= partText.length) {
156
+ cursor.partFinalFlags[cursor.currentPartIndex] = true;
157
+ if (cursor.currentPartIndex < parts.length - 1) {
158
+ cursor.currentPartIndex++;
159
+ cursor.currentCharIndex = 0;
160
+ }
161
+ }
162
+ return true;
163
+ }
164
+ // Intra-part scan-ahead recovery: if matching failed at the current position,
165
+ // scan forward word-by-word within the same part. This prevents the cursor
166
+ // from getting permanently stuck mid-part when a single word mismatch occurs
167
+ // (e.g. TTS variation, punctuation boundary like `apexes."Sometimes`).
168
+ if (startChar > 0) {
169
+ const MAX_SCAN_WORDS = 8;
170
+ let scanPos = startChar;
171
+ for(let scan = 0; scan < MAX_SCAN_WORDS; scan++){
172
+ // Advance past current word
173
+ while(scanPos < partText.length && !/\s/.test(partText[scanPos]))scanPos++;
174
+ // Advance past whitespace to next word
175
+ while(scanPos < partText.length && /\s/.test(partText[scanPos]))scanPos++;
176
+ if (scanPos >= partText.length) break;
177
+ const retryPos = $5989c1f88db60f73$var$findSpokenPositionInUnspoken(spokenText, partText, scanPos);
178
+ const scanWsEnd = $5989c1f88db60f73$var$skipWhitespace(partText, scanPos);
179
+ if (retryPos > scanWsEnd) {
180
+ cursor.currentCharIndex = retryPos;
181
+ if (retryPos >= partText.length) {
182
+ cursor.partFinalFlags[cursor.currentPartIndex] = true;
183
+ if (cursor.currentPartIndex < parts.length - 1) {
184
+ cursor.currentPartIndex++;
185
+ cursor.currentCharIndex = 0;
186
+ }
187
+ }
188
+ return true;
189
+ }
190
+ }
191
+ }
192
+ // Mismatch recovery: try to find the spoken text in a later part.
193
+ for(let nextPartIdx = cursor.currentPartIndex + 1; nextPartIdx < parts.length; nextPartIdx++){
194
+ const nextPart = parts[nextPartIdx];
195
+ if (typeof nextPart.text !== "string") continue;
196
+ const match = $5989c1f88db60f73$var$findSpokenPositionInUnspoken(spokenText, nextPart.text, 0);
197
+ const nextWhitespaceEnd = $5989c1f88db60f73$var$skipWhitespace(nextPart.text, 0);
198
+ if (match > nextWhitespaceEnd) {
199
+ // Mark skipped parts as final and jump to the matched part
200
+ for(let i = cursor.currentPartIndex; i < nextPartIdx; i++)cursor.partFinalFlags[i] = true;
201
+ cursor.currentPartIndex = nextPartIdx;
202
+ cursor.currentCharIndex = match;
203
+ return true;
204
+ }
205
+ }
206
+ // If we're stuck at the start, mark the current part as skipped to avoid deadlock.
207
+ if (startChar === 0 && cursor.currentPartIndex < parts.length - 1) {
208
+ cursor.partFinalFlags[cursor.currentPartIndex] = true;
209
+ cursor.currentPartIndex++;
210
+ cursor.currentCharIndex = 0;
211
+ return true;
212
+ }
213
+ return false;
214
+ }
215
+
216
+
217
+ /**
218
+ * Copyright (c) 2024, Daily.
219
+ *
220
+ * SPDX-License-Identifier: BSD-2-Clause
221
+ */
222
+ const $7b7579093c9adf9b$export$92a4076839a978d0 = (0, $h9lXz$atom)([]);
223
+ const $7b7579093c9adf9b$export$bd01e11bc95333a1 = (0, $h9lXz$atom)(new Map());
224
+ const $7b7579093c9adf9b$export$b0cd27059c88d0ab = (0, $h9lXz$atom)(new Map());
225
+ const $7b7579093c9adf9b$export$57b1a1df06a7e728 = (0, $h9lXz$atom)(null);
226
+
227
+
228
+ /**
229
+ * Copyright (c) 2024, Daily.
230
+ *
231
+ * SPDX-License-Identifier: BSD-2-Clause
232
+ */ // ES2020-compatible polyfills for findLast / findLastIndex
233
+ function $603acc766b2aa439$export$8855a8be7bd3e9f8(arr, predicate) {
234
+ for(let i = arr.length - 1; i >= 0; i--){
235
+ if (predicate(arr[i], i, arr)) return i;
236
+ }
237
+ return -1;
238
+ }
239
+ function $603acc766b2aa439$export$296de88ccac4bedb(arr, predicate) {
240
+ const idx = $603acc766b2aa439$export$8855a8be7bd3e9f8(arr, predicate);
241
+ return idx === -1 ? undefined : arr[idx];
242
+ }
243
+
244
+
245
+ /** Max time gap (ms) between consecutive same-role messages for merging. */ const $38ede2d92c123540$var$MERGE_WINDOW_MS = 30000;
246
+ /**
247
+ * Unicode characters used by `FilterIncompleteTurns` on the server to mark
248
+ * turn completion status. The LLM emits one of these as the very first
249
+ * character of every response:
250
+ *
251
+ * ✓ (U+2713) — complete turn
252
+ * ○ (U+25CB) — incomplete short
253
+ * ◐ (U+25D0) — incomplete long
254
+ *
255
+ * They must be stripped before the text reaches conversation state.
256
+ */ const $38ede2d92c123540$var$TURN_COMPLETION_MARKERS = new Set([
257
+ "\u2713",
258
+ "\u25CB",
259
+ "\u25D0"
260
+ ]);
261
+ function $38ede2d92c123540$export$146b18879fa8cfd7(text) {
262
+ if (text.length === 0) return text;
263
+ if ($38ede2d92c123540$var$TURN_COMPLETION_MARKERS.has(text[0])) // Remove marker and optional single trailing space
264
+ return text[1] === " " ? text.slice(2) : text.slice(1);
265
+ return text;
266
+ }
267
+ const $38ede2d92c123540$export$cbefc5865f8cbd89 = (a, b)=>{
268
+ return a.createdAt < b.createdAt ? -1 : a.createdAt > b.createdAt ? 1 : 0;
269
+ };
270
+ const $38ede2d92c123540$export$f4610a88b89838b = (message)=>{
271
+ if (message.role === "function_call") return false;
272
+ const parts = message.parts || [];
273
+ if (parts.length === 0) return true;
274
+ return parts.every((p)=>{
275
+ if (typeof p.text === "string") return p.text.trim().length === 0;
276
+ // Check BotOutputText objects
277
+ if (typeof p.text === "object" && p.text !== null && "spoken" in p.text && "unspoken" in p.text) {
278
+ const botText = p.text;
279
+ return botText.spoken.trim().length === 0 && botText.unspoken.trim().length === 0;
280
+ }
281
+ // For ReactNode, consider it non-empty
282
+ return false;
283
+ });
284
+ };
285
+ const $38ede2d92c123540$export$3d3951a55577516 = (messages)=>{
286
+ return messages.filter((message, index, array)=>{
287
+ if (!$38ede2d92c123540$export$f4610a88b89838b(message)) return true;
288
+ // For empty messages, keep only if no following non-empty message with same role
289
+ const nextMessageWithSameRole = array.slice(index + 1).find((m)=>m.role === message.role && !$38ede2d92c123540$export$f4610a88b89838b(m));
290
+ return !nextMessageWithSameRole;
291
+ });
292
+ };
293
+ const $38ede2d92c123540$export$23a96e868837e158 = (messages)=>{
294
+ const mergedMessages = [];
295
+ for(let i = 0; i < messages.length; i++){
296
+ const currentMessage = messages[i];
297
+ const lastMerged = mergedMessages[mergedMessages.length - 1];
298
+ const timeDiff = lastMerged ? Math.abs(new Date(currentMessage.createdAt).getTime() - new Date(lastMerged.createdAt).getTime()) : Infinity;
299
+ const shouldMerge = lastMerged && lastMerged.role === currentMessage.role && currentMessage.role !== "system" && currentMessage.role !== "function_call" && timeDiff < $38ede2d92c123540$var$MERGE_WINDOW_MS;
300
+ if (shouldMerge) mergedMessages[mergedMessages.length - 1] = {
301
+ ...lastMerged,
302
+ parts: [
303
+ ...lastMerged.parts || [],
304
+ ...currentMessage.parts || []
305
+ ],
306
+ updatedAt: currentMessage.updatedAt || currentMessage.createdAt,
307
+ final: currentMessage.final !== false
308
+ };
309
+ else mergedMessages.push(currentMessage);
310
+ }
311
+ return mergedMessages;
312
+ };
313
+ const $38ede2d92c123540$var$statusPriority = {
314
+ started: 0,
315
+ in_progress: 1,
316
+ completed: 2
317
+ };
318
+ const $38ede2d92c123540$export$dfa183f99573a822 = (messages)=>{
319
+ const bestByToolCallId = new Map();
320
+ const toRemove = new Set();
321
+ for(let i = 0; i < messages.length; i++){
322
+ const msg = messages[i];
323
+ const tcid = msg.functionCall?.tool_call_id;
324
+ if (msg.role !== "function_call" || !tcid) continue;
325
+ const existingIdx = bestByToolCallId.get(tcid);
326
+ if (existingIdx !== undefined) {
327
+ const existingPriority = $38ede2d92c123540$var$statusPriority[messages[existingIdx].functionCall.status] ?? 0;
328
+ const currentPriority = $38ede2d92c123540$var$statusPriority[msg.functionCall.status] ?? 0;
329
+ if (currentPriority >= existingPriority) {
330
+ toRemove.add(existingIdx);
331
+ bestByToolCallId.set(tcid, i);
332
+ } else toRemove.add(i);
333
+ } else bestByToolCallId.set(tcid, i);
334
+ }
335
+ if (toRemove.size === 0) return messages;
336
+ return messages.filter((_, i)=>!toRemove.has(i));
337
+ };
338
+ const $38ede2d92c123540$var$normalizeMessagesForUI = (messages)=>{
339
+ return $38ede2d92c123540$export$23a96e868837e158($38ede2d92c123540$export$dfa183f99573a822($38ede2d92c123540$export$3d3951a55577516([
340
+ ...messages
341
+ ].sort($38ede2d92c123540$export$cbefc5865f8cbd89))));
342
+ };
343
+ // ---------------------------------------------------------------------------
344
+ // Internal helpers
345
+ // ---------------------------------------------------------------------------
346
+ const $38ede2d92c123540$var$callCallbacks = (callbacksMap, type, message)=>{
347
+ callbacksMap.forEach((callbacks)=>{
348
+ try {
349
+ callbacks[type]?.(message);
350
+ } catch (error) {
351
+ console.error(`Error in ${type} callback:`, error);
352
+ }
353
+ });
354
+ };
355
+ function $38ede2d92c123540$export$f2820c5e040afe17(get, set, id, callbacks) {
356
+ const map = new Map(get((0, $7b7579093c9adf9b$export$b0cd27059c88d0ab)));
357
+ map.set(id, callbacks);
358
+ set((0, $7b7579093c9adf9b$export$b0cd27059c88d0ab), map);
359
+ }
360
+ function $38ede2d92c123540$export$46ae1ba121fdc956(get, set, id) {
361
+ const map = new Map(get((0, $7b7579093c9adf9b$export$b0cd27059c88d0ab)));
362
+ map.delete(id);
363
+ set((0, $7b7579093c9adf9b$export$b0cd27059c88d0ab), map);
364
+ }
365
+ function $38ede2d92c123540$export$f2434643f2abff11(_get, set) {
366
+ set((0, $7b7579093c9adf9b$export$92a4076839a978d0), []);
367
+ set((0, $7b7579093c9adf9b$export$bd01e11bc95333a1), new Map());
368
+ }
369
+ function $38ede2d92c123540$export$16fdb433d434f08(get, set, messageData) {
370
+ const now = new Date();
371
+ const message = {
372
+ ...messageData,
373
+ createdAt: now.toISOString(),
374
+ updatedAt: now.toISOString()
375
+ };
376
+ const current = get((0, $7b7579093c9adf9b$export$92a4076839a978d0));
377
+ const updatedMessages = [
378
+ ...current,
379
+ message
380
+ ];
381
+ const processedMessages = $38ede2d92c123540$var$normalizeMessagesForUI(updatedMessages);
382
+ $38ede2d92c123540$var$callCallbacks(get((0, $7b7579093c9adf9b$export$b0cd27059c88d0ab)), "onMessageCreated", message);
383
+ set((0, $7b7579093c9adf9b$export$92a4076839a978d0), processedMessages);
384
+ }
385
+ function $38ede2d92c123540$export$a05f96a2aa40873e(get, set, role, updates) {
386
+ const messages = [
387
+ ...get((0, $7b7579093c9adf9b$export$92a4076839a978d0))
388
+ ];
389
+ const lastMessageIndex = (0, $603acc766b2aa439$export$8855a8be7bd3e9f8)(messages, (msg)=>msg.role === role);
390
+ if (lastMessageIndex === -1) return;
391
+ const existing = messages[lastMessageIndex];
392
+ const updatedMessage = {
393
+ ...existing,
394
+ ...updates,
395
+ updatedAt: new Date().toISOString()
396
+ };
397
+ messages[lastMessageIndex] = updatedMessage;
398
+ const processedMessages = $38ede2d92c123540$var$normalizeMessagesForUI(messages);
399
+ $38ede2d92c123540$var$callCallbacks(get((0, $7b7579093c9adf9b$export$b0cd27059c88d0ab)), "onMessageUpdated", updatedMessage);
400
+ set((0, $7b7579093c9adf9b$export$92a4076839a978d0), processedMessages);
401
+ }
402
+ function $38ede2d92c123540$export$78bd074993ab081f(get, set, role) {
403
+ const messages = [
404
+ ...get((0, $7b7579093c9adf9b$export$92a4076839a978d0))
405
+ ];
406
+ const lastMessageIndex = (0, $603acc766b2aa439$export$8855a8be7bd3e9f8)(messages, (msg)=>msg.role === role);
407
+ if (lastMessageIndex === -1) return;
408
+ const lastMessage = messages[lastMessageIndex];
409
+ // Check if message is empty
410
+ if ($38ede2d92c123540$export$f4610a88b89838b(lastMessage)) // Remove empty message only if it has no text in streams either
411
+ messages.splice(lastMessageIndex, 1);
412
+ else {
413
+ // Finalize message and its last part
414
+ const parts = [
415
+ ...lastMessage.parts || []
416
+ ];
417
+ if (parts.length > 0) parts[parts.length - 1] = {
418
+ ...parts[parts.length - 1],
419
+ final: true
420
+ };
421
+ messages[lastMessageIndex] = {
422
+ ...lastMessage,
423
+ parts: parts,
424
+ final: true,
425
+ updatedAt: new Date().toISOString()
426
+ };
427
+ $38ede2d92c123540$var$callCallbacks(get((0, $7b7579093c9adf9b$export$b0cd27059c88d0ab)), "onMessageUpdated", messages[lastMessageIndex]);
428
+ }
429
+ const processedMessages = $38ede2d92c123540$var$normalizeMessagesForUI(messages);
430
+ set((0, $7b7579093c9adf9b$export$92a4076839a978d0), processedMessages);
431
+ }
432
+ function $38ede2d92c123540$export$cd74f809c89ea10c(get, set, role) {
433
+ const messages = [
434
+ ...get((0, $7b7579093c9adf9b$export$92a4076839a978d0))
435
+ ];
436
+ const lastMessageIndex = (0, $603acc766b2aa439$export$8855a8be7bd3e9f8)(messages, (msg)=>msg.role === role);
437
+ if (lastMessageIndex === -1) return;
438
+ const lastMessage = messages[lastMessageIndex];
439
+ if ($38ede2d92c123540$export$f4610a88b89838b(lastMessage)) {
440
+ messages.splice(lastMessageIndex, 1);
441
+ const processedMessages = $38ede2d92c123540$var$normalizeMessagesForUI(messages);
442
+ set((0, $7b7579093c9adf9b$export$92a4076839a978d0), processedMessages);
443
+ }
444
+ }
445
+ function $38ede2d92c123540$export$c3a007efc2f315a0(get, set, messageData) {
446
+ const now = new Date();
447
+ const current = get((0, $7b7579093c9adf9b$export$92a4076839a978d0));
448
+ // If the most recent message is an active (non-final) assistant message,
449
+ // backdate the injected message so it sorts before the assistant's createdAt.
450
+ // This prevents the injected message from splitting the bot's response across
451
+ // two bubbles and breaking the karaoke cursor.
452
+ const lastMessage = current[current.length - 1];
453
+ const lastAssistant = lastMessage?.role === "assistant" ? lastMessage : undefined;
454
+ let timestamp;
455
+ if (lastAssistant && lastAssistant.final === false && messageData.role === "system") {
456
+ const assistantTime = new Date(lastAssistant.createdAt);
457
+ timestamp = new Date(assistantTime.getTime() - 1).toISOString();
458
+ } else timestamp = now.toISOString();
459
+ const message = {
460
+ role: messageData.role,
461
+ final: true,
462
+ parts: [
463
+ ...messageData.parts
464
+ ],
465
+ createdAt: timestamp,
466
+ updatedAt: now.toISOString()
467
+ };
468
+ const updatedMessages = [
469
+ ...current,
470
+ message
471
+ ];
472
+ const processedMessages = $38ede2d92c123540$var$normalizeMessagesForUI(updatedMessages);
473
+ $38ede2d92c123540$var$callCallbacks(get((0, $7b7579093c9adf9b$export$b0cd27059c88d0ab)), "onMessageCreated", message);
474
+ set((0, $7b7579093c9adf9b$export$92a4076839a978d0), processedMessages);
475
+ }
476
+ function $38ede2d92c123540$export$57de01211e3df548(get, set, text, final) {
477
+ const now = new Date();
478
+ const messages = [
479
+ ...get((0, $7b7579093c9adf9b$export$92a4076839a978d0))
480
+ ];
481
+ // Find last user message
482
+ const lastUserIndex = (0, $603acc766b2aa439$export$8855a8be7bd3e9f8)(messages, (m)=>m.role === "user");
483
+ if (lastUserIndex !== -1 && !messages[lastUserIndex].final) {
484
+ // Update existing user message
485
+ const target = {
486
+ ...messages[lastUserIndex]
487
+ };
488
+ const parts = Array.isArray(target.parts) ? [
489
+ ...target.parts
490
+ ] : [];
491
+ const lastPart = parts[parts.length - 1];
492
+ if (!lastPart || lastPart.final) // Start a new part
493
+ parts.push({
494
+ text: text,
495
+ final: final,
496
+ createdAt: now.toISOString()
497
+ });
498
+ else // Update in-progress part
499
+ parts[parts.length - 1] = {
500
+ ...lastPart,
501
+ text: text,
502
+ final: final
503
+ };
504
+ const updatedMessage = {
505
+ ...target,
506
+ parts: parts,
507
+ updatedAt: now.toISOString()
508
+ };
509
+ messages[lastUserIndex] = updatedMessage;
510
+ const processedMessages = $38ede2d92c123540$var$normalizeMessagesForUI(messages);
511
+ $38ede2d92c123540$var$callCallbacks(get((0, $7b7579093c9adf9b$export$b0cd27059c88d0ab)), "onMessageUpdated", updatedMessage);
512
+ set((0, $7b7579093c9adf9b$export$92a4076839a978d0), processedMessages);
513
+ return;
514
+ }
515
+ // Create a new user message initialized with this transcript
516
+ const newMessage = {
517
+ role: "user",
518
+ final: false,
519
+ parts: [
520
+ {
521
+ text: text,
522
+ final: final,
523
+ createdAt: now.toISOString()
524
+ }
525
+ ],
526
+ createdAt: now.toISOString(),
527
+ updatedAt: now.toISOString()
528
+ };
529
+ const updatedMessages = [
530
+ ...messages,
531
+ newMessage
532
+ ];
533
+ const processedMessages = $38ede2d92c123540$var$normalizeMessagesForUI(updatedMessages);
534
+ $38ede2d92c123540$var$callCallbacks(get((0, $7b7579093c9adf9b$export$b0cd27059c88d0ab)), "onMessageCreated", newMessage);
535
+ set((0, $7b7579093c9adf9b$export$92a4076839a978d0), processedMessages);
536
+ }
537
+ function $38ede2d92c123540$export$d78810e35f742a16(get, set, text, final, spoken, aggregatedBy) {
538
+ // Strip turn-completion markers emitted by FilterIncompleteTurns before
539
+ // the text enters conversation state.
540
+ text = $38ede2d92c123540$export$146b18879fa8cfd7(text);
541
+ if (text.length === 0) return;
542
+ const now = new Date();
543
+ const messages = [
544
+ ...get((0, $7b7579093c9adf9b$export$92a4076839a978d0))
545
+ ];
546
+ const botOutputMessageState = new Map(get((0, $7b7579093c9adf9b$export$bd01e11bc95333a1)));
547
+ const lastAssistantIndex = (0, $603acc766b2aa439$export$8855a8be7bd3e9f8)(messages, (msg)=>msg.role === "assistant");
548
+ let messageId;
549
+ let isNewMessage = false;
550
+ if (lastAssistantIndex === -1) {
551
+ // Create new assistant message
552
+ isNewMessage = true;
553
+ messageId = now.toISOString();
554
+ const newMessage = {
555
+ role: "assistant",
556
+ final: final,
557
+ parts: [],
558
+ createdAt: messageId,
559
+ updatedAt: messageId
560
+ };
561
+ messages.push(newMessage);
562
+ // Initialize message state
563
+ botOutputMessageState.set(messageId, {
564
+ currentPartIndex: 0,
565
+ currentCharIndex: 0,
566
+ partFinalFlags: []
567
+ });
568
+ } else {
569
+ // Update existing assistant message
570
+ const lastMessage = messages[lastAssistantIndex];
571
+ messageId = lastMessage.createdAt;
572
+ messages[lastAssistantIndex] = {
573
+ ...lastMessage,
574
+ final: final ? true : lastMessage.final,
575
+ updatedAt: now.toISOString()
576
+ };
577
+ // Ensure message state exists
578
+ if (!botOutputMessageState.has(messageId)) botOutputMessageState.set(messageId, {
579
+ currentPartIndex: 0,
580
+ currentCharIndex: 0,
581
+ partFinalFlags: []
582
+ });
583
+ }
584
+ const messageState = botOutputMessageState.get(messageId);
585
+ const message = messages[lastAssistantIndex === -1 ? messages.length - 1 : lastAssistantIndex];
586
+ const parts = [
587
+ ...message.parts || []
588
+ ];
589
+ if (!spoken) {
590
+ // UNSPOKEN EVENT: Create/update message parts immediately
591
+ // Only append when both current and last part are word-level; sentence-level
592
+ // and other types each get their own part so spoken events can match 1:1.
593
+ const isDefaultType = aggregatedBy === "sentence" || aggregatedBy === "word" || !aggregatedBy;
594
+ const lastPart = parts[parts.length - 1];
595
+ const shouldAppend = lastPart && aggregatedBy === "word" && lastPart.aggregatedBy === "word" && typeof lastPart.text === "string";
596
+ if (shouldAppend) {
597
+ // Append to last part (word-level only)
598
+ const lastPartText = lastPart.text;
599
+ const separator = lastPartText && !lastPartText.endsWith(" ") && !text.startsWith(" ") ? " " : "";
600
+ parts[parts.length - 1] = {
601
+ ...lastPart,
602
+ text: lastPartText + separator + text
603
+ };
604
+ } else {
605
+ // Create new part (sentence-level, custom types, or first word chunk)
606
+ // Default to inline; custom types get displayMode from metadata in the hook
607
+ const defaultDisplayMode = isDefaultType ? "inline" : undefined;
608
+ const newPart = {
609
+ text: text,
610
+ final: false,
611
+ createdAt: now.toISOString(),
612
+ aggregatedBy: aggregatedBy,
613
+ displayMode: defaultDisplayMode
614
+ };
615
+ parts.push(newPart);
616
+ // Extend partFinalFlags array
617
+ messageState.partFinalFlags.push(false);
618
+ }
619
+ // Update message with new parts
620
+ messages[lastAssistantIndex === -1 ? messages.length - 1 : lastAssistantIndex] = {
621
+ ...message,
622
+ parts: parts
623
+ };
624
+ } else {
625
+ // SPOKEN EVENT: advance cursor into existing text, or add as new part if
626
+ // there is none (bots that only send spoken: true, never unspoken).
627
+ const advanced = parts.length > 0 && (0, $5989c1f88db60f73$export$9b39fd8438081d6d)(messageState, parts, text);
628
+ if (!advanced) {
629
+ // No unspoken content to advance: add this text as a part already fully spoken
630
+ const isDefaultType = aggregatedBy === "sentence" || aggregatedBy === "word" || !aggregatedBy;
631
+ const defaultDisplayMode = isDefaultType ? "inline" : undefined;
632
+ const newPart = {
633
+ text: text,
634
+ final: false,
635
+ createdAt: now.toISOString(),
636
+ aggregatedBy: aggregatedBy,
637
+ displayMode: defaultDisplayMode
638
+ };
639
+ parts.push(newPart);
640
+ messageState.partFinalFlags.push(true);
641
+ messageState.currentPartIndex = parts.length - 1;
642
+ messageState.currentCharIndex = text.length;
643
+ messages[lastAssistantIndex === -1 ? messages.length - 1 : lastAssistantIndex] = {
644
+ ...message,
645
+ parts: parts
646
+ };
647
+ }
648
+ }
649
+ const processedMessages = $38ede2d92c123540$var$normalizeMessagesForUI(messages);
650
+ const cbs = get((0, $7b7579093c9adf9b$export$b0cd27059c88d0ab));
651
+ const updatedMsg = messages[lastAssistantIndex === -1 ? messages.length - 1 : lastAssistantIndex];
652
+ if (isNewMessage) $38ede2d92c123540$var$callCallbacks(cbs, "onMessageCreated", updatedMsg);
653
+ else $38ede2d92c123540$var$callCallbacks(cbs, "onMessageUpdated", updatedMsg);
654
+ set((0, $7b7579093c9adf9b$export$92a4076839a978d0), processedMessages);
655
+ set((0, $7b7579093c9adf9b$export$bd01e11bc95333a1), botOutputMessageState);
656
+ }
657
+ function $38ede2d92c123540$export$e450805545c7dd86(get, set, data) {
658
+ const messages = get((0, $7b7579093c9adf9b$export$92a4076839a978d0));
659
+ // If a tool_call_id is provided, check for an existing entry to avoid duplicates
660
+ if (data.tool_call_id) {
661
+ const existingIndex = (0, $603acc766b2aa439$export$8855a8be7bd3e9f8)(messages, (msg)=>msg.role === "function_call" && msg.functionCall?.tool_call_id === data.tool_call_id);
662
+ if (existingIndex !== -1) return;
663
+ }
664
+ const now = new Date();
665
+ // If the most recent message is an active (non-final) assistant message,
666
+ // backdate the function call so it sorts before the assistant's createdAt.
667
+ // This prevents the function call from splitting the bot's response across
668
+ // two bubbles and breaking the karaoke cursor.
669
+ const lastMessage = messages[messages.length - 1];
670
+ let timestamp;
671
+ if (lastMessage?.role === "assistant" && lastMessage.final === false) {
672
+ const assistantTime = new Date(lastMessage.createdAt);
673
+ timestamp = new Date(assistantTime.getTime() - 1).toISOString();
674
+ } else timestamp = now.toISOString();
675
+ const message = {
676
+ role: "function_call",
677
+ final: false,
678
+ parts: [],
679
+ createdAt: timestamp,
680
+ updatedAt: now.toISOString(),
681
+ functionCall: {
682
+ function_name: data.function_name,
683
+ tool_call_id: data.tool_call_id,
684
+ args: data.args,
685
+ status: "started"
686
+ }
687
+ };
688
+ const updatedMessages = [
689
+ ...messages,
690
+ message
691
+ ];
692
+ const processedMessages = $38ede2d92c123540$var$normalizeMessagesForUI(updatedMessages);
693
+ $38ede2d92c123540$var$callCallbacks(get((0, $7b7579093c9adf9b$export$b0cd27059c88d0ab)), "onMessageCreated", message);
694
+ set((0, $7b7579093c9adf9b$export$92a4076839a978d0), processedMessages);
695
+ }
696
+ function $38ede2d92c123540$export$1908264de3a8afe4(get, set, tool_call_id, updates) {
697
+ const messages = [
698
+ ...get((0, $7b7579093c9adf9b$export$92a4076839a978d0))
699
+ ];
700
+ const index = (0, $603acc766b2aa439$export$8855a8be7bd3e9f8)(messages, (msg)=>msg.role === "function_call" && msg.functionCall?.tool_call_id === tool_call_id);
701
+ if (index === -1) return false;
702
+ const existing = messages[index];
703
+ const updated = {
704
+ ...existing,
705
+ updatedAt: new Date().toISOString(),
706
+ final: updates.status === "completed" ? true : existing.final,
707
+ functionCall: {
708
+ ...existing.functionCall,
709
+ ...updates
710
+ }
711
+ };
712
+ messages[index] = updated;
713
+ const processedMessages = $38ede2d92c123540$var$normalizeMessagesForUI(messages);
714
+ $38ede2d92c123540$var$callCallbacks(get((0, $7b7579093c9adf9b$export$b0cd27059c88d0ab)), "onMessageUpdated", updated);
715
+ set((0, $7b7579093c9adf9b$export$92a4076839a978d0), processedMessages);
716
+ return true;
717
+ }
718
+ function $38ede2d92c123540$export$54b3765e641b1813(get, set, updates) {
719
+ const messages = [
720
+ ...get((0, $7b7579093c9adf9b$export$92a4076839a978d0))
721
+ ];
722
+ const index = (0, $603acc766b2aa439$export$8855a8be7bd3e9f8)(messages, (msg)=>msg.role === "function_call" && msg.functionCall?.status === "started" && !msg.functionCall?.tool_call_id);
723
+ if (index === -1) return false;
724
+ const existing = messages[index];
725
+ const updated = {
726
+ ...existing,
727
+ updatedAt: new Date().toISOString(),
728
+ functionCall: {
729
+ ...existing.functionCall,
730
+ ...updates
731
+ }
732
+ };
733
+ messages[index] = updated;
734
+ const processedMessages = $38ede2d92c123540$var$normalizeMessagesForUI(messages);
735
+ $38ede2d92c123540$var$callCallbacks(get((0, $7b7579093c9adf9b$export$b0cd27059c88d0ab)), "onMessageUpdated", updated);
736
+ set((0, $7b7579093c9adf9b$export$92a4076839a978d0), processedMessages);
737
+ return true;
738
+ }
739
+ function $38ede2d92c123540$export$809efdca1a10761a(get, set, data) {
740
+ const messages = get((0, $7b7579093c9adf9b$export$92a4076839a978d0));
741
+ const lastFc = (0, $603acc766b2aa439$export$296de88ccac4bedb)(messages, (m)=>m.role === "function_call");
742
+ // Check if InProgress already created an entry (events arrived out of order).
743
+ if (lastFc?.functionCall && lastFc.functionCall.status !== "started" && Date.now() - new Date(lastFc.updatedAt ?? lastFc.createdAt).getTime() < 2000) {
744
+ if (data.function_name && !lastFc.functionCall.function_name && lastFc.functionCall.tool_call_id) $38ede2d92c123540$export$1908264de3a8afe4(get, set, lastFc.functionCall.tool_call_id, {
745
+ function_name: data.function_name
746
+ });
747
+ return;
748
+ }
749
+ $38ede2d92c123540$export$e450805545c7dd86(get, set, {
750
+ function_name: data.function_name
751
+ });
752
+ }
753
+ function $38ede2d92c123540$export$568cde5f4aa4dcba(get, set, data) {
754
+ // Tier 1: Try to update the last "started" entry (from LLMFunctionCallStarted)
755
+ const updated = $38ede2d92c123540$export$54b3765e641b1813(get, set, {
756
+ function_name: data.function_name,
757
+ tool_call_id: data.tool_call_id,
758
+ args: data.args,
759
+ status: "in_progress"
760
+ });
761
+ if (!updated) {
762
+ // Tier 2: Try updating an existing entry by tool_call_id
763
+ const found = $38ede2d92c123540$export$1908264de3a8afe4(get, set, data.tool_call_id, {
764
+ function_name: data.function_name,
765
+ args: data.args,
766
+ status: "in_progress"
767
+ });
768
+ if (!found) {
769
+ // Tier 3: No existing entry at all; create a new one as in_progress
770
+ $38ede2d92c123540$export$e450805545c7dd86(get, set, {
771
+ function_name: data.function_name,
772
+ tool_call_id: data.tool_call_id,
773
+ args: data.args
774
+ });
775
+ $38ede2d92c123540$export$1908264de3a8afe4(get, set, data.tool_call_id, {
776
+ status: "in_progress"
777
+ });
778
+ }
779
+ }
780
+ }
781
+ function $38ede2d92c123540$export$a3cea3f7508e820b(get, set, data) {
782
+ // Tier 1: Try updating by tool_call_id
783
+ const found = $38ede2d92c123540$export$1908264de3a8afe4(get, set, data.tool_call_id, {
784
+ function_name: data.function_name,
785
+ status: "completed",
786
+ result: data.result,
787
+ cancelled: data.cancelled
788
+ });
789
+ if (!found) {
790
+ // Tier 2: No match by tool_call_id (e.g. InProgress was skipped).
791
+ const matched = $38ede2d92c123540$export$54b3765e641b1813(get, set, {
792
+ function_name: data.function_name,
793
+ tool_call_id: data.tool_call_id
794
+ });
795
+ if (matched) $38ede2d92c123540$export$1908264de3a8afe4(get, set, data.tool_call_id, {
796
+ status: "completed",
797
+ result: data.result,
798
+ cancelled: data.cancelled
799
+ });
800
+ }
801
+ }
802
+
803
+
804
+
805
+ /**
806
+ * Copyright (c) 2024, Daily.
807
+ *
808
+ * SPDX-License-Identifier: BSD-2-Clause
809
+ */
810
+
811
+
41
812
  /**
42
813
  * Copyright (c) 2024, Daily.
43
814
  *
@@ -66,6 +837,239 @@ const $824ea64b5f757259$export$33a6ac53b8f02625 = (event, handler)=>{
66
837
  };
67
838
 
68
839
 
840
+
841
+
842
+
843
+
844
+ /**
845
+ * Checks if a version meets a minimum version requirement.
846
+ * Inlined to avoid adding a `semver` dependency.
847
+ */ function $d1596d1cb01c59d1$var$isMinVersion(currentVersion, minVersion) {
848
+ // Strip pre-release suffix (e.g. "1.1.0-beta.1" -> "1.1.0")
849
+ const parts = currentVersion.split("-")[0].split(".").map(Number);
850
+ for(let i = 0; i < 3; i++){
851
+ if ((parts[i] || 0) > minVersion[i]) return true;
852
+ if ((parts[i] || 0) < minVersion[i]) return false;
853
+ }
854
+ return true; // equal
855
+ }
856
+ /** Delay (ms) before finalizing the assistant message after bot stops speaking. */ const $d1596d1cb01c59d1$var$BOT_STOPPED_FINALIZE_DELAY_MS = 2500;
857
+ function $d1596d1cb01c59d1$export$52680e26621bab39() {
858
+ const userStoppedTimeout = (0, $h9lXz$useRef)(undefined);
859
+ const botStoppedSpeakingTimeoutRef = (0, $h9lXz$useRef)(undefined);
860
+ const assistantStreamResetRef = (0, $h9lXz$useRef)(0);
861
+ const botOutputLastChunkRef = (0, $h9lXz$useRef)({
862
+ spoken: "",
863
+ unspoken: ""
864
+ });
865
+ // Clean up pending timeouts on unmount
866
+ (0, $h9lXz$useEffect)(()=>{
867
+ return ()=>{
868
+ clearTimeout(userStoppedTimeout.current);
869
+ clearTimeout(botStoppedSpeakingTimeoutRef.current);
870
+ };
871
+ }, []);
872
+ // -- helpers ---------------------------------------------------------------
873
+ const finalizeLastAssistantMessageIfPending = (0, $h9lXz$useAtomCallback)((0, $h9lXz$useCallback)((get, set)=>{
874
+ clearTimeout(botStoppedSpeakingTimeoutRef.current);
875
+ botStoppedSpeakingTimeoutRef.current = undefined;
876
+ const messages = get((0, $7b7579093c9adf9b$export$92a4076839a978d0));
877
+ const lastAssistant = (0, $603acc766b2aa439$export$296de88ccac4bedb)(messages, (m)=>m.role === "assistant");
878
+ if (lastAssistant && !lastAssistant.final) (0, $38ede2d92c123540$export$78bd074993ab081f)(get, set, "assistant");
879
+ }, []));
880
+ const ensureAssistantMessage = (0, $h9lXz$useAtomCallback)((0, $h9lXz$useCallback)((get, set)=>{
881
+ const messages = get((0, $7b7579093c9adf9b$export$92a4076839a978d0));
882
+ const lastAssistantIndex = (0, $603acc766b2aa439$export$8855a8be7bd3e9f8)(messages, (msg)=>msg.role === "assistant");
883
+ const lastAssistant = lastAssistantIndex !== -1 ? messages[lastAssistantIndex] : undefined;
884
+ if (!lastAssistant || lastAssistant.final) {
885
+ // If the message was finalized but still has unspoken content, it was
886
+ // finalized prematurely (e.g. BotStoppedSpeaking timer fired during a
887
+ // TTS pause mid-response). Un-finalize it instead of creating a new
888
+ // message bubble — but only when no user message followed.
889
+ if (lastAssistant?.final && lastAssistantIndex === messages.length - 1) {
890
+ const messageId = lastAssistant.createdAt;
891
+ const botOutputState = get((0, $7b7579093c9adf9b$export$bd01e11bc95333a1));
892
+ const cursor = botOutputState.get(messageId);
893
+ if (cursor && (0, $5989c1f88db60f73$export$802e9b947136afbf)(cursor, lastAssistant.parts || [])) {
894
+ (0, $38ede2d92c123540$export$a05f96a2aa40873e)(get, set, "assistant", {
895
+ final: false
896
+ });
897
+ return false;
898
+ }
899
+ }
900
+ (0, $38ede2d92c123540$export$16fdb433d434f08)(get, set, {
901
+ role: "assistant",
902
+ final: false,
903
+ parts: []
904
+ });
905
+ assistantStreamResetRef.current += 1;
906
+ return true;
907
+ }
908
+ return false;
909
+ }, []));
910
+ // -- event handlers --------------------------------------------------------
911
+ (0, $824ea64b5f757259$export$33a6ac53b8f02625)((0, $h9lXz$RTVIEvent).Connected, (0, $h9lXz$useAtomCallback)((0, $h9lXz$useCallback)((get, set)=>{
912
+ (0, $38ede2d92c123540$export$f2434643f2abff11)(get, set);
913
+ set((0, $7b7579093c9adf9b$export$57b1a1df06a7e728), null);
914
+ clearTimeout(botStoppedSpeakingTimeoutRef.current);
915
+ botStoppedSpeakingTimeoutRef.current = undefined;
916
+ botOutputLastChunkRef.current = {
917
+ spoken: "",
918
+ unspoken: ""
919
+ };
920
+ }, [])));
921
+ (0, $824ea64b5f757259$export$33a6ac53b8f02625)((0, $h9lXz$RTVIEvent).BotReady, (0, $h9lXz$useAtomCallback)((0, $h9lXz$useCallback)((_get, set, botData)=>{
922
+ const rtviVersion = botData.version;
923
+ const supportsBotOutput = $d1596d1cb01c59d1$var$isMinVersion(rtviVersion, [
924
+ 1,
925
+ 1,
926
+ 0
927
+ ]);
928
+ set((0, $7b7579093c9adf9b$export$57b1a1df06a7e728), supportsBotOutput);
929
+ }, [])));
930
+ (0, $824ea64b5f757259$export$33a6ac53b8f02625)((0, $h9lXz$RTVIEvent).BotOutput, (0, $h9lXz$useAtomCallback)((0, $h9lXz$useCallback)((get, set, data)=>{
931
+ // A BotOutput event means the response is still active; cancel any
932
+ // pending finalize timer from BotStoppedSpeaking.
933
+ clearTimeout(botStoppedSpeakingTimeoutRef.current);
934
+ botStoppedSpeakingTimeoutRef.current = undefined;
935
+ ensureAssistantMessage();
936
+ // Handle spacing for BotOutput chunks
937
+ let textToAdd = data.text;
938
+ const lastChunk = data.spoken ? botOutputLastChunkRef.current.spoken : botOutputLastChunkRef.current.unspoken;
939
+ // Add space separator if needed between BotOutput chunks
940
+ if (lastChunk) textToAdd = " " + textToAdd;
941
+ // Update the appropriate last chunk tracker
942
+ if (data.spoken) botOutputLastChunkRef.current.spoken = textToAdd;
943
+ else botOutputLastChunkRef.current.unspoken = textToAdd;
944
+ // Update both spoken and unspoken text streams
945
+ const isFinal = data.aggregated_by === "sentence";
946
+ (0, $38ede2d92c123540$export$d78810e35f742a16)(get, set, textToAdd, isFinal, data.spoken, data.aggregated_by);
947
+ }, [
948
+ ensureAssistantMessage
949
+ ])));
950
+ (0, $824ea64b5f757259$export$33a6ac53b8f02625)((0, $h9lXz$RTVIEvent).BotStoppedSpeaking, (0, $h9lXz$useAtomCallback)((0, $h9lXz$useCallback)((get, set)=>{
951
+ // Don't finalize immediately; start a timer. Bot may start speaking again (pause).
952
+ clearTimeout(botStoppedSpeakingTimeoutRef.current);
953
+ const messages = get((0, $7b7579093c9adf9b$export$92a4076839a978d0));
954
+ const lastAssistant = (0, $603acc766b2aa439$export$296de88ccac4bedb)(messages, (m)=>m.role === "assistant");
955
+ if (!lastAssistant || lastAssistant.final) return;
956
+ botStoppedSpeakingTimeoutRef.current = setTimeout(()=>{
957
+ botStoppedSpeakingTimeoutRef.current = undefined;
958
+ // Snap the speech-progress cursor to the end of all parts.
959
+ // The bot finished speaking normally (not interrupted), so all
960
+ // text should render as "spoken". Without this, text from the
961
+ // last sentence can remain grey if the spoken BotOutput event
962
+ // didn't match the unspoken text exactly.
963
+ const msgs = get((0, $7b7579093c9adf9b$export$92a4076839a978d0));
964
+ const cursorMap = new Map(get((0, $7b7579093c9adf9b$export$bd01e11bc95333a1)));
965
+ const last = (0, $603acc766b2aa439$export$296de88ccac4bedb)(msgs, (m)=>m.role === "assistant");
966
+ if (last) {
967
+ const cursor = cursorMap.get(last.createdAt);
968
+ if (cursor && last.parts && last.parts.length > 0) {
969
+ const lastPartIdx = last.parts.length - 1;
970
+ const lastPartText = last.parts[lastPartIdx]?.text;
971
+ cursor.currentPartIndex = lastPartIdx;
972
+ cursor.currentCharIndex = typeof lastPartText === "string" ? lastPartText.length : 0;
973
+ for(let i = 0; i <= lastPartIdx; i++)cursor.partFinalFlags[i] = true;
974
+ set((0, $7b7579093c9adf9b$export$bd01e11bc95333a1), cursorMap);
975
+ }
976
+ }
977
+ (0, $38ede2d92c123540$export$78bd074993ab081f)(get, set, "assistant");
978
+ }, $d1596d1cb01c59d1$var$BOT_STOPPED_FINALIZE_DELAY_MS);
979
+ }, [])));
980
+ (0, $824ea64b5f757259$export$33a6ac53b8f02625)((0, $h9lXz$RTVIEvent).BotStartedSpeaking, (0, $h9lXz$useCallback)(()=>{
981
+ // Bot is speaking again; reset the finalize timer (bot was just pausing).
982
+ clearTimeout(botStoppedSpeakingTimeoutRef.current);
983
+ botStoppedSpeakingTimeoutRef.current = undefined;
984
+ }, []));
985
+ (0, $824ea64b5f757259$export$33a6ac53b8f02625)((0, $h9lXz$RTVIEvent).UserStartedSpeaking, (0, $h9lXz$useAtomCallback)((0, $h9lXz$useCallback)((get, set)=>{
986
+ // User started a new turn; bot's turn is done. Fast-forward: finalize immediately.
987
+ finalizeLastAssistantMessageIfPending();
988
+ clearTimeout(userStoppedTimeout.current);
989
+ // Only finalize the previous user message if the bot has responded since
990
+ // the user last spoke. This prevents finalizing during VAD gaps (brief
991
+ // breathing pauses within the same user turn where UserStoppedSpeaking/
992
+ // UserStartedSpeaking fire without an actual turn change).
993
+ const messages = get((0, $7b7579093c9adf9b$export$92a4076839a978d0));
994
+ const lastUserIdx = (0, $603acc766b2aa439$export$8855a8be7bd3e9f8)(messages, (m)=>m.role === "user");
995
+ if (lastUserIdx !== -1 && !messages[lastUserIdx].final) {
996
+ const hasBotActivityAfterUser = messages.slice(lastUserIdx + 1).some((m)=>m.role === "assistant");
997
+ if (hasBotActivityAfterUser) (0, $38ede2d92c123540$export$78bd074993ab081f)(get, set, "user");
998
+ }
999
+ }, [
1000
+ finalizeLastAssistantMessageIfPending
1001
+ ])));
1002
+ (0, $824ea64b5f757259$export$33a6ac53b8f02625)((0, $h9lXz$RTVIEvent).UserTranscript, (0, $h9lXz$useAtomCallback)((0, $h9lXz$useCallback)((get, set, data)=>{
1003
+ const text = data.text ?? "";
1004
+ const final = Boolean(data.final);
1005
+ (0, $38ede2d92c123540$export$57de01211e3df548)(get, set, text, final);
1006
+ // If we got any transcript, cancel pending cleanup
1007
+ clearTimeout(userStoppedTimeout.current);
1008
+ }, [])));
1009
+ (0, $824ea64b5f757259$export$33a6ac53b8f02625)((0, $h9lXz$RTVIEvent).UserStoppedSpeaking, (0, $h9lXz$useAtomCallback)((0, $h9lXz$useCallback)((get, set)=>{
1010
+ clearTimeout(userStoppedTimeout.current);
1011
+ // If no transcript ends up arriving, ensure any accidental empty placeholder is removed.
1012
+ userStoppedTimeout.current = setTimeout(()=>{
1013
+ // Re-read state at timeout time
1014
+ const messages = get((0, $7b7579093c9adf9b$export$92a4076839a978d0));
1015
+ const lastUser = (0, $603acc766b2aa439$export$296de88ccac4bedb)(messages, (m)=>m.role === "user");
1016
+ const hasParts = Array.isArray(lastUser?.parts) && lastUser.parts.length > 0;
1017
+ if (!lastUser || !hasParts) (0, $38ede2d92c123540$export$cd74f809c89ea10c)(get, set, "user");
1018
+ else if (!lastUser.final) (0, $38ede2d92c123540$export$78bd074993ab081f)(get, set, "user");
1019
+ }, 3000);
1020
+ }, [])));
1021
+ // LLM Function Call lifecycle events
1022
+ (0, $824ea64b5f757259$export$33a6ac53b8f02625)((0, $h9lXz$RTVIEvent).LLMFunctionCallStarted, (0, $h9lXz$useAtomCallback)((0, $h9lXz$useCallback)((get, set, data)=>{
1023
+ (0, $38ede2d92c123540$export$809efdca1a10761a)(get, set, {
1024
+ function_name: data.function_name
1025
+ });
1026
+ }, [])));
1027
+ (0, $824ea64b5f757259$export$33a6ac53b8f02625)((0, $h9lXz$RTVIEvent).LLMFunctionCallInProgress, (0, $h9lXz$useAtomCallback)((0, $h9lXz$useCallback)((get, set, data)=>{
1028
+ (0, $38ede2d92c123540$export$568cde5f4aa4dcba)(get, set, {
1029
+ function_name: data.function_name,
1030
+ tool_call_id: data.tool_call_id,
1031
+ args: data.arguments
1032
+ });
1033
+ }, [])));
1034
+ (0, $824ea64b5f757259$export$33a6ac53b8f02625)((0, $h9lXz$RTVIEvent).LLMFunctionCallStopped, (0, $h9lXz$useAtomCallback)((0, $h9lXz$useCallback)((get, set, data)=>{
1035
+ (0, $38ede2d92c123540$export$a3cea3f7508e820b)(get, set, {
1036
+ function_name: data.function_name,
1037
+ tool_call_id: data.tool_call_id,
1038
+ result: data.result,
1039
+ cancelled: data.cancelled
1040
+ });
1041
+ }, [])));
1042
+ }
1043
+
1044
+
1045
+ const $b5a460b32af71766$export$4d68d6035e4164b1 = /*#__PURE__*/ (0, $h9lXz$createContext)(null);
1046
+ const $b5a460b32af71766$export$e372a2172e7d18e8 = ({ children: children })=>{
1047
+ (0, $d1596d1cb01c59d1$export$52680e26621bab39)();
1048
+ const injectMessage = (0, $h9lXz$useAtomCallback)((0, $h9lXz$useCallback)((get, set, message)=>{
1049
+ (0, $38ede2d92c123540$export$c3a007efc2f315a0)(get, set, message);
1050
+ }, []));
1051
+ const botOutputSupported = (0, $h9lXz$useAtomValue)((0, $7b7579093c9adf9b$export$57b1a1df06a7e728));
1052
+ return (0, $h9lXz$jsx)($b5a460b32af71766$export$4d68d6035e4164b1.Provider, {
1053
+ value: {
1054
+ injectMessage: injectMessage,
1055
+ botOutputSupported: botOutputSupported
1056
+ },
1057
+ children: children
1058
+ });
1059
+ };
1060
+ $b5a460b32af71766$export$e372a2172e7d18e8.displayName = "PipecatConversationProvider";
1061
+ const $b5a460b32af71766$export$8eec679bf8249822 = ()=>{
1062
+ const context = (0, $h9lXz$useContext)($b5a460b32af71766$export$4d68d6035e4164b1);
1063
+ if (!context) throw new Error("useConversationContext must be used within a PipecatClientProvider");
1064
+ return context;
1065
+ };
1066
+
1067
+
1068
+
1069
+
1070
+
1071
+
1072
+
69
1073
  const $a90aa7250c094218$export$d6bdcccacef16204 = /*#__PURE__*/ (0, $h9lXz$createContext)({
70
1074
  enableCam: ()=>{
71
1075
  throw new Error("PipecatClientCamStateContext: enableCam() called outside of provider");
@@ -200,7 +1204,9 @@ const $d2e362c5a07ee3c5$export$bb43666ced7a20d0 = ({ children: children, client:
200
1204
  off: off
201
1205
  },
202
1206
  children: (0, $h9lXz$jsx)((0, $a90aa7250c094218$export$4777554fda61c378), {
203
- children: children
1207
+ children: (0, $h9lXz$jsx)((0, $b5a460b32af71766$export$e372a2172e7d18e8), {
1208
+ children: children
1209
+ })
204
1210
  })
205
1211
  })
206
1212
  })
@@ -846,5 +1852,126 @@ $993a744193844a95$export$59bf27bd43679db6.displayName = "VoiceVisualizer";
846
1852
 
847
1853
 
848
1854
 
849
- export {$f209aa7ddb77dcb2$export$b52250cb73ff4de1 as PipecatClientAudio, $7cb2ce2c4cbfb401$export$dc9a029eeca8213f as PipecatClientCamToggle, $2984fdfc31bad375$export$bc8133b69ff660a2 as PipecatClientMicToggle, $d2e362c5a07ee3c5$export$bb43666ced7a20d0 as PipecatClientProvider, $d6dc7e80e6e976a0$export$93764714dfab3a46 as PipecatClientScreenShareToggle, $6a65deb8615a2ad7$export$85974db6d0cc43b3 as PipecatClientVideo, $034a56e7ee1b7bed$export$777fa8498be78705 as usePipecatClient, $e76ee2f021b54325$export$3ea2601427f0430f as usePipecatClientCamControl, $f934f1f8b10aaf19$export$642bc4d2d2a376f1 as usePipecatClientMediaDevices, $4b4b9099cdb5b776$export$9813dcd2d0c26814 as usePipecatClientMediaTrack, $5905c001b0dc8d25$export$388e706586309ef0 as usePipecatClientMicControl, $d39c0bc3744ea030$export$be63b19bd7f7d4f5 as usePipecatClientScreenShareControl, $33f3729bbe9f09df$export$30aee278309a867b as usePipecatClientTransportState, $824ea64b5f757259$export$33a6ac53b8f02625 as useRTVIClientEvent, $993a744193844a95$export$59bf27bd43679db6 as VoiceVisualizer};
1855
+ /**
1856
+ * Copyright (c) 2024, Daily.
1857
+ *
1858
+ * SPDX-License-Identifier: BSD-2-Clause
1859
+ */
1860
+
1861
+
1862
+
1863
+
1864
+
1865
+ const $894023a10d0bd040$export$acb832b064cb6c09 = ({ onMessageCreated: onMessageCreated, onMessageUpdated: onMessageUpdated, onMessageAdded: onMessageAdded, aggregationMetadata: aggregationMetadata } = {})=>{
1866
+ const { injectMessage: injectMessage } = (0, $b5a460b32af71766$export$8eec679bf8249822)();
1867
+ // Generate a unique ID for this hook instance
1868
+ const callbackId = (0, $h9lXz$useId)();
1869
+ // Resolve deprecated onMessageAdded → onMessageCreated
1870
+ const resolvedCreated = onMessageCreated ?? onMessageAdded;
1871
+ // Register and unregister the callbacks
1872
+ const doRegister = (0, $h9lXz$useAtomCallback)((0, $h9lXz$useCallback)((get, set)=>{
1873
+ (0, $38ede2d92c123540$export$f2820c5e040afe17)(get, set, callbackId, {
1874
+ onMessageCreated: resolvedCreated,
1875
+ onMessageUpdated: onMessageUpdated
1876
+ });
1877
+ }, [
1878
+ callbackId,
1879
+ resolvedCreated,
1880
+ onMessageUpdated
1881
+ ]));
1882
+ const doUnregister = (0, $h9lXz$useAtomCallback)((0, $h9lXz$useCallback)((get, set)=>{
1883
+ (0, $38ede2d92c123540$export$46ae1ba121fdc956)(get, set, callbackId);
1884
+ }, [
1885
+ callbackId
1886
+ ]));
1887
+ (0, $h9lXz$useEffect)(()=>{
1888
+ doRegister();
1889
+ return ()=>{
1890
+ doUnregister();
1891
+ };
1892
+ }, [
1893
+ doRegister,
1894
+ doUnregister
1895
+ ]);
1896
+ // Get the raw state from atoms
1897
+ const messages = (0, $h9lXz$useAtomValue)((0, $7b7579093c9adf9b$export$92a4076839a978d0));
1898
+ const botOutputMessageState = (0, $h9lXz$useAtomValue)((0, $7b7579093c9adf9b$export$bd01e11bc95333a1));
1899
+ // Memoize the filtered messages to prevent infinite loops
1900
+ const filteredMessages = (0, $h9lXz$useMemo)(()=>{
1901
+ const getMetadata = (part)=>{
1902
+ return part.aggregatedBy ? aggregationMetadata?.[part.aggregatedBy] : undefined;
1903
+ };
1904
+ // Process messages: convert string parts to BotOutputText based on position state
1905
+ const processedMessages = messages.map((message)=>{
1906
+ if (message.role === "assistant") {
1907
+ const messageId = message.createdAt;
1908
+ const messageState = botOutputMessageState.get(messageId);
1909
+ if (!messageState) // No state yet, return message as-is
1910
+ return message;
1911
+ const parts = message.parts || [];
1912
+ // Find the actual current part index (skip parts that aren't meant to be spoken)
1913
+ let actualCurrentPartIndex = messageState.currentPartIndex;
1914
+ while(actualCurrentPartIndex < parts.length){
1915
+ const part = parts[actualCurrentPartIndex];
1916
+ if (typeof part?.text !== "string") break;
1917
+ const isSpoken = getMetadata(part)?.isSpoken !== false;
1918
+ if (isSpoken) break;
1919
+ actualCurrentPartIndex++;
1920
+ }
1921
+ if (parts.length > 0 && actualCurrentPartIndex >= parts.length) actualCurrentPartIndex = parts.length - 1;
1922
+ // Convert parts to BotOutputText format based on position state
1923
+ const processedParts = parts.map((part, partIndex)=>{
1924
+ // If part text is not a string, it's already processed (e.g., ReactNode)
1925
+ if (typeof part.text !== "string") return part;
1926
+ const metadata = getMetadata(part);
1927
+ const displayMode = part.displayMode ?? metadata?.displayMode ?? "inline";
1928
+ const isSpoken = metadata?.isSpoken !== false;
1929
+ const partText = displayMode === "block" && !isSpoken ? part.text.trim() : part.text;
1930
+ if (!isSpoken) return {
1931
+ ...part,
1932
+ displayMode: displayMode,
1933
+ text: {
1934
+ spoken: "",
1935
+ unspoken: partText
1936
+ }
1937
+ };
1938
+ // Use cursor split for the part at actualCurrentPartIndex for every message,
1939
+ // so previous (e.g. interrupted) messages keep partially spoken state.
1940
+ const isPartAtCursor = partIndex === actualCurrentPartIndex;
1941
+ const currentCharIndex = messageState.currentCharIndex;
1942
+ const spokenText = isPartAtCursor ? partText.slice(0, currentCharIndex) : partIndex < actualCurrentPartIndex ? partText : "";
1943
+ const unspokenText = isPartAtCursor ? partText.slice(currentCharIndex) : partIndex < actualCurrentPartIndex ? "" : partText;
1944
+ return {
1945
+ ...part,
1946
+ displayMode: displayMode,
1947
+ text: {
1948
+ spoken: spokenText,
1949
+ unspoken: unspokenText
1950
+ }
1951
+ };
1952
+ });
1953
+ return {
1954
+ ...message,
1955
+ parts: processedParts
1956
+ };
1957
+ }
1958
+ return message;
1959
+ });
1960
+ // Messages are already normalized (sorted, filtered, deduped, merged) on write.
1961
+ return processedMessages;
1962
+ }, [
1963
+ messages,
1964
+ botOutputMessageState,
1965
+ aggregationMetadata
1966
+ ]);
1967
+ return {
1968
+ messages: filteredMessages,
1969
+ injectMessage: injectMessage
1970
+ };
1971
+ };
1972
+
1973
+
1974
+
1975
+
1976
+ export {$f209aa7ddb77dcb2$export$b52250cb73ff4de1 as PipecatClientAudio, $7cb2ce2c4cbfb401$export$dc9a029eeca8213f as PipecatClientCamToggle, $2984fdfc31bad375$export$bc8133b69ff660a2 as PipecatClientMicToggle, $d2e362c5a07ee3c5$export$bb43666ced7a20d0 as PipecatClientProvider, $d6dc7e80e6e976a0$export$93764714dfab3a46 as PipecatClientScreenShareToggle, $6a65deb8615a2ad7$export$85974db6d0cc43b3 as PipecatClientVideo, $034a56e7ee1b7bed$export$777fa8498be78705 as usePipecatClient, $e76ee2f021b54325$export$3ea2601427f0430f as usePipecatClientCamControl, $f934f1f8b10aaf19$export$642bc4d2d2a376f1 as usePipecatClientMediaDevices, $4b4b9099cdb5b776$export$9813dcd2d0c26814 as usePipecatClientMediaTrack, $5905c001b0dc8d25$export$388e706586309ef0 as usePipecatClientMicControl, $d39c0bc3744ea030$export$be63b19bd7f7d4f5 as usePipecatClientScreenShareControl, $33f3729bbe9f09df$export$30aee278309a867b as usePipecatClientTransportState, $824ea64b5f757259$export$33a6ac53b8f02625 as useRTVIClientEvent, $993a744193844a95$export$59bf27bd43679db6 as VoiceVisualizer, $b5a460b32af71766$export$8eec679bf8249822 as useConversationContext, $894023a10d0bd040$export$acb832b064cb6c09 as usePipecatConversation, $38ede2d92c123540$export$dfa183f99573a822 as deduplicateFunctionCalls, $38ede2d92c123540$export$3d3951a55577516 as filterEmptyMessages, $38ede2d92c123540$export$f4610a88b89838b as isMessageEmpty, $38ede2d92c123540$export$23a96e868837e158 as mergeMessages, $38ede2d92c123540$export$cbefc5865f8cbd89 as sortByCreatedAt};
850
1977
  //# sourceMappingURL=index.module.js.map