@paymanai/payman-ask-sdk 4.0.20 → 4.0.21

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.
@@ -0,0 +1,3826 @@
1
+ 'use strict';
2
+
3
+ var lucideReactNative = require('lucide-react-native');
4
+ var react = require('react');
5
+ var reactNative = require('react-native');
6
+ var Markdown = require('react-native-markdown-display');
7
+ var jsxRuntime = require('react/jsx-runtime');
8
+ var clsx = require('clsx');
9
+ var tailwindMerge = require('tailwind-merge');
10
+ var Sentry = require('@sentry/react');
11
+
12
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
13
+
14
+ function _interopNamespace(e) {
15
+ if (e && e.__esModule) return e;
16
+ var n = Object.create(null);
17
+ if (e) {
18
+ Object.keys(e).forEach(function (k) {
19
+ if (k !== 'default') {
20
+ var d = Object.getOwnPropertyDescriptor(e, k);
21
+ Object.defineProperty(n, k, d.get ? d : {
22
+ enumerable: true,
23
+ get: function () { return e[k]; }
24
+ });
25
+ }
26
+ });
27
+ }
28
+ n.default = e;
29
+ return Object.freeze(n);
30
+ }
31
+
32
+ var Markdown__default = /*#__PURE__*/_interopDefault(Markdown);
33
+ var Sentry__namespace = /*#__PURE__*/_interopNamespace(Sentry);
34
+
35
+ // src/components/PaymanChat/index.native.tsx
36
+ var __defProp = Object.defineProperty;
37
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
38
+ var __publicField = (obj, key, value) => __defNormalProp(obj, key + "", value);
39
+ function generateId() {
40
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
41
+ const r = Math.random() * 16 | 0;
42
+ const v = c === "x" ? r : r & 3 | 8;
43
+ return v.toString(16);
44
+ });
45
+ }
46
+ var memoryStore = /* @__PURE__ */ new Map();
47
+ var chatStore = {
48
+ get(key) {
49
+ return memoryStore.get(key) ?? [];
50
+ },
51
+ set(key, messages) {
52
+ memoryStore.set(key, messages);
53
+ },
54
+ delete(key) {
55
+ memoryStore.delete(key);
56
+ }
57
+ };
58
+ var streams = /* @__PURE__ */ new Map();
59
+ var activeStreamStore = {
60
+ has(key) {
61
+ return streams.has(key);
62
+ },
63
+ get(key) {
64
+ const entry = streams.get(key);
65
+ if (!entry) return null;
66
+ return { messages: entry.messages, isWaiting: entry.isWaiting };
67
+ },
68
+ // Called before startStream — registers the controller and initial messages
69
+ start(key, abortController, initialMessages) {
70
+ const existing = streams.get(key);
71
+ streams.set(key, {
72
+ messages: initialMessages,
73
+ isWaiting: true,
74
+ abortController,
75
+ listeners: existing?.listeners ?? /* @__PURE__ */ new Set()
76
+ });
77
+ },
78
+ // Called by the stream on every event — applies the same updater pattern React uses
79
+ applyMessages(key, updater) {
80
+ const entry = streams.get(key);
81
+ if (!entry) return;
82
+ const next = typeof updater === "function" ? updater(entry.messages) : updater;
83
+ entry.messages = next;
84
+ entry.listeners.forEach((l) => l(next, entry.isWaiting));
85
+ },
86
+ setWaiting(key, waiting) {
87
+ const entry = streams.get(key);
88
+ if (!entry) return;
89
+ entry.isWaiting = waiting;
90
+ entry.listeners.forEach((l) => l(entry.messages, waiting));
91
+ },
92
+ // Called when stream completes — persists to chatStore and cleans up
93
+ complete(key) {
94
+ const entry = streams.get(key);
95
+ if (!entry) return;
96
+ entry.isWaiting = false;
97
+ entry.listeners.forEach((l) => l(entry.messages, false));
98
+ const toSave = entry.messages.filter((m) => !m.isStreaming);
99
+ if (toSave.length > 0) chatStore.set(key, toSave);
100
+ streams.delete(key);
101
+ },
102
+ // Subscribe — returns unsubscribe fn. Component calls this on mount, cleanup on unmount.
103
+ subscribe(key, listener) {
104
+ const entry = streams.get(key);
105
+ if (!entry) return () => {
106
+ };
107
+ entry.listeners.add(listener);
108
+ return () => {
109
+ streams.get(key)?.listeners.delete(listener);
110
+ };
111
+ },
112
+ // Rename an entry — used when the server assigns a new session ID mid-stream
113
+ rename(oldKey, newKey) {
114
+ const entry = streams.get(oldKey);
115
+ if (!entry) return;
116
+ streams.set(newKey, entry);
117
+ streams.delete(oldKey);
118
+ },
119
+ // Explicit user cancel — aborts the controller and removes the entry
120
+ abort(key) {
121
+ const entry = streams.get(key);
122
+ if (!entry) return;
123
+ entry.abortController.abort();
124
+ streams.delete(key);
125
+ }
126
+ };
127
+ function yieldAfterProgressEvent(event) {
128
+ const t = event.eventType;
129
+ if (t === "RUN_IN_PROGRESS" || t === "INTENT_PROGRESS" || t === "THINKING_DELTA") {
130
+ return new Promise((resolve) => setTimeout(resolve, 0));
131
+ }
132
+ return Promise.resolve();
133
+ }
134
+ function parseJSONBuffer(buffer) {
135
+ const events = [];
136
+ let braceCount = 0;
137
+ let startIndex = 0;
138
+ let inString = false;
139
+ let escapeNext = false;
140
+ let lastParsedIndex = -1;
141
+ for (let i = 0; i < buffer.length; i++) {
142
+ const char = buffer[i];
143
+ if (escapeNext) {
144
+ escapeNext = false;
145
+ continue;
146
+ }
147
+ if (char === "\\") {
148
+ escapeNext = true;
149
+ continue;
150
+ }
151
+ if (char === '"' && !escapeNext) {
152
+ inString = !inString;
153
+ continue;
154
+ }
155
+ if (inString) {
156
+ continue;
157
+ }
158
+ if (char === "{") {
159
+ if (braceCount === 0) {
160
+ startIndex = i;
161
+ }
162
+ braceCount++;
163
+ } else if (char === "}") {
164
+ braceCount--;
165
+ if (braceCount === 0) {
166
+ const jsonStr = buffer.substring(startIndex, i + 1);
167
+ try {
168
+ const parsed = JSON.parse(jsonStr);
169
+ const event = parsed;
170
+ events.push(event);
171
+ lastParsedIndex = i;
172
+ } catch (err) {
173
+ console.error("Failed to parse JSON event:", jsonStr, err);
174
+ }
175
+ }
176
+ }
177
+ }
178
+ const remaining = lastParsedIndex >= 0 ? buffer.substring(lastParsedIndex + 1) : buffer;
179
+ return { events, remaining };
180
+ }
181
+ async function streamWorkflowEvents(url, body, headers, options = {}) {
182
+ const { signal, onEvent, onError, onComplete } = options;
183
+ try {
184
+ const response = await fetch(url, {
185
+ method: "POST",
186
+ headers: {
187
+ "Content-Type": "application/json",
188
+ ...headers
189
+ },
190
+ body: JSON.stringify(body),
191
+ signal
192
+ });
193
+ if (!response.ok) {
194
+ const errorText = await response.text();
195
+ throw new Error(`HTTP ${response.status}: ${errorText}`);
196
+ }
197
+ const reader = response.body?.getReader();
198
+ if (!reader) {
199
+ throw new Error("No response body");
200
+ }
201
+ const decoder = new TextDecoder();
202
+ let buffer = "";
203
+ while (true) {
204
+ const { done, value } = await reader.read();
205
+ if (done) {
206
+ if (buffer.trim()) {
207
+ const { events: events2 } = parseJSONBuffer(buffer);
208
+ for (const event of events2) {
209
+ onEvent?.(event);
210
+ await yieldAfterProgressEvent(event);
211
+ }
212
+ }
213
+ break;
214
+ }
215
+ buffer += decoder.decode(value, { stream: true });
216
+ const { events, remaining } = parseJSONBuffer(buffer);
217
+ for (const event of events) {
218
+ onEvent?.(event);
219
+ await yieldAfterProgressEvent(event);
220
+ }
221
+ buffer = remaining;
222
+ }
223
+ onComplete?.();
224
+ } catch (error) {
225
+ if (error instanceof Error && error.name === "AbortError") {
226
+ return;
227
+ }
228
+ onError?.(error);
229
+ }
230
+ }
231
+ var STAGE_LABELS = {
232
+ sanitizer: "Checking your request",
233
+ analyzer: "Understanding what you're asking",
234
+ prefetcher: "Gathering context",
235
+ planner: "Planning how to handle this",
236
+ execution: "Working on it",
237
+ formatter: "Writing the response"
238
+ };
239
+ function stageLabel(stage) {
240
+ const label = STAGE_LABELS[stage];
241
+ if (label) return label;
242
+ const pretty = stage.replace(/[_-]/g, " ");
243
+ return `Running ${pretty}`;
244
+ }
245
+ function isBlandStatus(message) {
246
+ if (!message) return true;
247
+ const normalized = message.trim().toLowerCase().replace(/[…\.]+$/, "").trim();
248
+ return BLAND_STATUS_LABELS.has(normalized);
249
+ }
250
+ var BLAND_STATUS_LABELS = /* @__PURE__ */ new Set([
251
+ "executing",
252
+ "working on it",
253
+ "thinking",
254
+ "processing",
255
+ "reviewing your request",
256
+ "composing response",
257
+ "checking your request",
258
+ "polishing the response"
259
+ ]);
260
+ function getEventMessage(event) {
261
+ if (event.message?.trim()) {
262
+ return event.message.trim();
263
+ }
264
+ if (event.errorMessage?.trim()) {
265
+ return event.errorMessage.trim();
266
+ }
267
+ const eventType = event.eventType;
268
+ switch (eventType) {
269
+ case "RUN_STARTED":
270
+ return "Starting agent run...";
271
+ case "TOOL_CALL_STARTED": {
272
+ const description = typeof event.description === "string" ? event.description.trim() : "";
273
+ if (description) return description;
274
+ const toolName = typeof event.toolName === "string" ? event.toolName : "";
275
+ return toolName ? `Calling ${toolName}...` : "Calling tool...";
276
+ }
277
+ case "TOOL_CALL_COMPLETED": {
278
+ const description = typeof event.description === "string" ? event.description.trim() : "";
279
+ if (description) return description;
280
+ const toolName = typeof event.toolName === "string" ? event.toolName : "";
281
+ return toolName ? `${toolName} completed` : "Tool call completed";
282
+ }
283
+ case "RUN_IN_PROGRESS":
284
+ return event.partialText || "Thinking...";
285
+ case "RUN_COMPLETED":
286
+ return "Agent run completed";
287
+ case "RUN_FAILED":
288
+ return event.errorMessage || "Agent run failed";
289
+ case "KEEP_ALIVE":
290
+ return event.description || "";
291
+ case "THINKING_DELTA":
292
+ return event.text || "";
293
+ default:
294
+ return eventType;
295
+ }
296
+ }
297
+ function extractResponseContent(response) {
298
+ if (typeof response === "string") {
299
+ return response;
300
+ }
301
+ if (typeof response === "object" && response !== null) {
302
+ const resp = response;
303
+ if ("text" in resp && typeof resp.text === "string") {
304
+ return resp.text;
305
+ }
306
+ if ("content" in resp && typeof resp.content === "string") {
307
+ return resp.content;
308
+ }
309
+ if ("message" in resp && typeof resp.message === "string") {
310
+ return resp.message;
311
+ }
312
+ if ("answer" in resp && typeof resp.answer === "string") {
313
+ return resp.answer;
314
+ }
315
+ return JSON.stringify(response);
316
+ }
317
+ return "";
318
+ }
319
+ function normalizeEvent(event) {
320
+ const type = event.eventType;
321
+ switch (type) {
322
+ case "RUN_STARTED":
323
+ return { ...event, eventType: "WORKFLOW_STARTED" };
324
+ case "TOOL_CALL_STARTED": {
325
+ const toolName = typeof event.toolName === "string" ? event.toolName : void 0;
326
+ const description = typeof event.description === "string" ? event.description.trim() : "";
327
+ return {
328
+ ...event,
329
+ eventType: "INTENT_STARTED",
330
+ workerName: toolName ?? event.workerName,
331
+ message: description || event.message
332
+ };
333
+ }
334
+ case "TOOL_CALL_COMPLETED": {
335
+ const toolName = typeof event.toolName === "string" ? event.toolName : void 0;
336
+ const description = typeof event.description === "string" ? event.description.trim() : "";
337
+ return {
338
+ ...event,
339
+ eventType: "INTENT_COMPLETED",
340
+ workerName: toolName ?? event.workerName,
341
+ message: description || event.message
342
+ };
343
+ }
344
+ case "RUN_IN_PROGRESS":
345
+ return {
346
+ ...event,
347
+ eventType: "INTENT_PROGRESS",
348
+ message: event.partialText ?? event.message
349
+ };
350
+ case "RUN_COMPLETED":
351
+ return {
352
+ ...event,
353
+ eventType: "WORKFLOW_COMPLETED",
354
+ // state machine reads event.response for the final content
355
+ response: event.response ?? event.message
356
+ };
357
+ case "RUN_FAILED":
358
+ return {
359
+ ...event,
360
+ eventType: "WORKFLOW_ERROR",
361
+ errorMessage: event.errorMessage ?? event.message
362
+ };
363
+ default:
364
+ return event;
365
+ }
366
+ }
367
+ function isVerificationSchema(schema) {
368
+ const props = schema?.properties;
369
+ if (!props) return false;
370
+ const keys = Object.keys(props);
371
+ return keys.length === 1 && keys[0] === "verificationCode";
372
+ }
373
+ function classifyUserActionKind(action, schema) {
374
+ switch ((action || "").toLowerCase()) {
375
+ case "userverificationrequest":
376
+ return "verification";
377
+ case "usernotificationrequest":
378
+ return "notification";
379
+ case "userformrequest":
380
+ return isVerificationSchema(schema) ? "verification" : "form";
381
+ default:
382
+ return isVerificationSchema(schema) ? "verification" : "form";
383
+ }
384
+ }
385
+ function getEventText(event, field) {
386
+ const value = event[field];
387
+ return typeof value === "string" ? value.trim() : "";
388
+ }
389
+ function updateExecutionStageMessage(state, msg) {
390
+ if (!msg) return;
391
+ for (let i = state.steps.length - 1; i >= 0; i--) {
392
+ const s2 = state.steps[i];
393
+ if (s2.eventType === "STAGE_STARTED" && (s2.stage === "executor" || s2.stage === "execution") && s2.status === "in_progress") {
394
+ s2.message = msg;
395
+ return;
396
+ }
397
+ }
398
+ }
399
+ function completeLastInProgressStep(steps) {
400
+ for (let i = steps.length - 1; i >= 0; i--) {
401
+ if (steps[i].status === "in_progress") {
402
+ steps[i].status = "completed";
403
+ return;
404
+ }
405
+ }
406
+ }
407
+ function createInitialV2State() {
408
+ return {
409
+ finalResponse: "",
410
+ lastEventType: "",
411
+ sessionId: void 0,
412
+ executionId: void 0,
413
+ hasError: false,
414
+ errorMessage: "",
415
+ userActions: [],
416
+ notifications: [],
417
+ lastUserAction: void 0,
418
+ lastNotification: void 0,
419
+ finalData: void 0,
420
+ steps: [],
421
+ stepCounter: 0,
422
+ currentExecutingStepId: void 0
423
+ };
424
+ }
425
+ function upsertUserAction(state, req) {
426
+ const active = { ...req, status: "pending" };
427
+ const matchIdx = state.userActions.findIndex(
428
+ (p) => req.toolCallId ? p.toolCallId === req.toolCallId : p.userActionId === req.userActionId
429
+ );
430
+ if (matchIdx >= 0) {
431
+ state.userActions[matchIdx] = active;
432
+ } else {
433
+ state.userActions.push(active);
434
+ }
435
+ }
436
+ function processStreamEventV2(rawEvent, state) {
437
+ const event = normalizeEvent(rawEvent);
438
+ const eventType = event.eventType;
439
+ state.lastUserAction = void 0;
440
+ state.lastNotification = void 0;
441
+ if (typeof eventType === "string" && eventType.toUpperCase() === "KEEP_ALIVE") {
442
+ if (event.executionId) state.executionId = event.executionId;
443
+ if (event.sessionId) state.sessionId = event.sessionId;
444
+ const description = typeof event.description === "string" ? event.description : "";
445
+ if (description) {
446
+ for (let i = state.steps.length - 1; i >= 0; i--) {
447
+ if (state.steps[i].status === "in_progress") {
448
+ state.steps[i].message = description;
449
+ break;
450
+ }
451
+ }
452
+ }
453
+ return state;
454
+ }
455
+ if (typeof eventType === "string" && eventType.toUpperCase() === "THINKING_DELTA") {
456
+ if (event.executionId) state.executionId = event.executionId;
457
+ if (event.sessionId) state.sessionId = event.sessionId;
458
+ state.lastEventType = "THINKING_DELTA";
459
+ return state;
460
+ }
461
+ if (event.executionId) state.executionId = event.executionId;
462
+ if (event.sessionId) state.sessionId = event.sessionId;
463
+ const message = getEventMessage(event);
464
+ switch (eventType) {
465
+ case "WORKFLOW_STARTED":
466
+ state.lastEventType = eventType;
467
+ break;
468
+ case "INTENT_PROGRESS": {
469
+ const rawMessage = typeof event.message === "string" ? event.message : "";
470
+ const rawPartial = typeof event.partialText === "string" ? event.partialText : "";
471
+ const delta = rawMessage || rawPartial;
472
+ if (delta.length > 0) {
473
+ state.finalResponse += delta;
474
+ }
475
+ state.lastEventType = eventType;
476
+ break;
477
+ }
478
+ case "INTENT_STARTED": {
479
+ const stepId = `step-${state.stepCounter++}`;
480
+ state.steps.push({
481
+ id: stepId,
482
+ eventType,
483
+ message,
484
+ status: "in_progress",
485
+ timestamp: Date.now(),
486
+ elapsedMs: event.elapsedMs
487
+ });
488
+ state.currentExecutingStepId = stepId;
489
+ updateExecutionStageMessage(state, message);
490
+ state.lastEventType = eventType;
491
+ break;
492
+ }
493
+ case "INTENT_COMPLETED": {
494
+ const intentStep = state.steps.find((s2) => s2.eventType === "INTENT_STARTED" && s2.status === "in_progress");
495
+ if (intentStep) {
496
+ intentStep.status = "completed";
497
+ if (event.elapsedMs) intentStep.elapsedMs = event.elapsedMs;
498
+ if (intentStep.id === state.currentExecutingStepId) state.currentExecutingStepId = void 0;
499
+ }
500
+ state.lastEventType = eventType;
501
+ break;
502
+ }
503
+ case "WORKFLOW_COMPLETED": {
504
+ const totalTime = Number(event.totalTimeMs);
505
+ if (Number.isFinite(totalTime) && totalTime > 0) {
506
+ state.totalElapsedMs = totalTime;
507
+ }
508
+ const content = extractResponseContent(event.response);
509
+ if (content) {
510
+ state.finalResponse = content;
511
+ if (event.trace && typeof event.trace === "object") {
512
+ state.finalData = event.trace;
513
+ }
514
+ state.hasError = false;
515
+ state.errorMessage = "";
516
+ } else {
517
+ state.hasError = true;
518
+ state.errorMessage = "WORKFLOW_FAILED";
519
+ }
520
+ state.steps.forEach((step) => {
521
+ if (step.status === "in_progress") {
522
+ step.status = "completed";
523
+ }
524
+ });
525
+ state.lastEventType = eventType;
526
+ break;
527
+ }
528
+ case "USER_ACTION_REQUIRED": {
529
+ const rawAction = typeof event.action === "string" ? event.action : void 0;
530
+ const schema = event.requestedSchema;
531
+ const kind = classifyUserActionKind(rawAction, schema);
532
+ const promptMessage = typeof event.message === "string" && event.message.trim() || message || "";
533
+ const userActionId = typeof event.userActionId === "string" ? event.userActionId : "";
534
+ if (kind === "notification") {
535
+ const notification = {
536
+ id: userActionId || `note-${state.stepCounter++}`,
537
+ message: promptMessage
538
+ };
539
+ state.notifications.push(notification);
540
+ state.lastNotification = notification;
541
+ state.lastEventType = eventType;
542
+ break;
543
+ }
544
+ if (!userActionId) {
545
+ state.lastEventType = eventType;
546
+ break;
547
+ }
548
+ completeLastInProgressStep(state.steps);
549
+ const verificationType = event.verificationType === "ALPHANUMERIC_CODE" || event.verificationType === "NUMERIC_CODE" ? event.verificationType : void 0;
550
+ const request = {
551
+ userActionId,
552
+ kind,
553
+ rawAction,
554
+ subAction: typeof event.subAction === "string" ? event.subAction : void 0,
555
+ verificationType,
556
+ expirySeconds: typeof event.expirySeconds === "number" ? event.expirySeconds : void 0,
557
+ message: promptMessage || void 0,
558
+ requestedSchema: schema,
559
+ metadata: event.metadata,
560
+ toolCallId: typeof event.toolCallId === "string" ? event.toolCallId : void 0,
561
+ executionId: state.executionId,
562
+ sessionId: state.sessionId
563
+ };
564
+ upsertUserAction(state, request);
565
+ state.lastUserAction = request;
566
+ const stepId = `step-${state.stepCounter++}`;
567
+ state.steps.push({
568
+ id: stepId,
569
+ eventType,
570
+ message: promptMessage || (kind === "verification" ? "Waiting for verification..." : "Waiting for your input..."),
571
+ status: "in_progress",
572
+ timestamp: Date.now(),
573
+ elapsedMs: event.elapsedMs
574
+ });
575
+ state.currentExecutingStepId = stepId;
576
+ state.lastEventType = eventType;
577
+ break;
578
+ }
579
+ case "USER_NOTIFICATION": {
580
+ const noteMessage = typeof event.message === "string" && event.message.trim() || message || "";
581
+ const userActionId = typeof event.userActionId === "string" ? event.userActionId : "";
582
+ if (noteMessage || userActionId) {
583
+ const notification = {
584
+ id: userActionId || `note-${state.stepCounter++}`,
585
+ message: noteMessage
586
+ };
587
+ state.notifications.push(notification);
588
+ state.lastNotification = notification;
589
+ }
590
+ state.lastEventType = eventType;
591
+ break;
592
+ }
593
+ case "WORKFLOW_ERROR":
594
+ state.hasError = true;
595
+ state.errorMessage = event.errorMessage || event.message || "Workflow error";
596
+ state.lastEventType = eventType;
597
+ break;
598
+ // ---- K2 pipeline stage lifecycle events ----
599
+ //
600
+ // The k2-server playground streaming API emits
601
+ // STAGE_STARTED / STAGE_COMPLETED / STAGE_FAILED /
602
+ // STAGE_SKIPPED around each pipeline stage so the UI can
603
+ // show real progress (sanitizer → analyzer → planner → …)
604
+ // instead of a static placeholder. Each STAGE_STARTED
605
+ // pushes an in-progress step with a user-facing label; the
606
+ // matching STAGE_COMPLETED/FAILED closes it WITHOUT
607
+ // rewriting the label — a terse "completed" flash between
608
+ // stages was more noise than signal. STAGE_SKIPPED removes
609
+ // the just-opened step entirely (skipping is expected on
610
+ // DIRECT_RESPONSE short-circuits).
611
+ //
612
+ // Finding the matching step uses `stepId` directly rather
613
+ // than comparing labels — the processor remembers which id
614
+ // it minted for the latest open STAGE_STARTED and the
615
+ // closing events look it up from state.currentExecutingStepId.
616
+ case "STAGE_STARTED": {
617
+ const stage = getEventText(event, "stage");
618
+ if (!stage) {
619
+ state.lastEventType = eventType;
620
+ break;
621
+ }
622
+ const serverMessage = getEventText(event, "message");
623
+ let initialMessage = serverMessage || stageLabel(stage);
624
+ if (stage === "executor" || stage === "execution") {
625
+ for (let i = state.steps.length - 1; i >= 0; i--) {
626
+ const s2 = state.steps[i];
627
+ if (s2.eventType === "STAGE_STARTED" && s2.stage === "analyzer" && s2.status === "completed" && s2.message) {
628
+ initialMessage = s2.message;
629
+ break;
630
+ }
631
+ }
632
+ }
633
+ const stepId = `stage-${stage}-${state.stepCounter++}`;
634
+ state.steps.push({
635
+ id: stepId,
636
+ eventType,
637
+ message: initialMessage,
638
+ status: "in_progress",
639
+ timestamp: Date.now(),
640
+ stage
641
+ });
642
+ state.currentExecutingStepId = stepId;
643
+ state.lastEventType = eventType;
644
+ break;
645
+ }
646
+ case "STAGE_COMPLETED": {
647
+ const stage = getEventText(event, "stage");
648
+ if (!stage) {
649
+ state.lastEventType = eventType;
650
+ break;
651
+ }
652
+ const serverMessage = getEventText(event, "message");
653
+ for (let i = state.steps.length - 1; i >= 0; i--) {
654
+ const s2 = state.steps[i];
655
+ if (s2.eventType === "STAGE_STARTED" && s2.stage === stage && s2.status === "in_progress") {
656
+ if (serverMessage) s2.message = serverMessage;
657
+ s2.status = "completed";
658
+ const durationMs = Number(event.durationMs);
659
+ if (Number.isFinite(durationMs)) s2.elapsedMs = durationMs;
660
+ if (s2.id === state.currentExecutingStepId) {
661
+ state.currentExecutingStepId = void 0;
662
+ }
663
+ break;
664
+ }
665
+ }
666
+ state.lastEventType = eventType;
667
+ break;
668
+ }
669
+ case "STAGE_FAILED": {
670
+ const stage = getEventText(event, "stage");
671
+ if (!stage) {
672
+ state.lastEventType = eventType;
673
+ break;
674
+ }
675
+ const serverMessage = getEventText(event, "message");
676
+ for (let i = state.steps.length - 1; i >= 0; i--) {
677
+ const s2 = state.steps[i];
678
+ if (s2.eventType === "STAGE_STARTED" && s2.stage === stage && s2.status === "in_progress") {
679
+ if (serverMessage) s2.message = serverMessage;
680
+ s2.status = "error";
681
+ const durationMs = Number(event.durationMs);
682
+ if (Number.isFinite(durationMs)) s2.elapsedMs = durationMs;
683
+ if (s2.id === state.currentExecutingStepId) {
684
+ state.currentExecutingStepId = void 0;
685
+ }
686
+ break;
687
+ }
688
+ }
689
+ state.lastEventType = eventType;
690
+ break;
691
+ }
692
+ case "STAGE_SKIPPED": {
693
+ const stage = getEventText(event, "stage");
694
+ if (!stage) {
695
+ state.lastEventType = eventType;
696
+ break;
697
+ }
698
+ for (let i = state.steps.length - 1; i >= 0; i--) {
699
+ const s2 = state.steps[i];
700
+ if (s2.eventType === "STAGE_STARTED" && s2.stage === stage && s2.status === "in_progress") {
701
+ if (s2.id === state.currentExecutingStepId) {
702
+ state.currentExecutingStepId = void 0;
703
+ }
704
+ s2.status = "skipped";
705
+ break;
706
+ }
707
+ }
708
+ state.lastEventType = eventType;
709
+ break;
710
+ }
711
+ default:
712
+ state.lastEventType = eventType;
713
+ break;
714
+ }
715
+ return state;
716
+ }
717
+ var DEFAULT_STREAM_ENDPOINT = "/api/playground/ask/stream";
718
+ function buildRequestBody(config, userMessage, sessionId, options) {
719
+ const sessionOwner = config.sessionParams;
720
+ const sessionOwnerId = sessionOwner?.id?.trim();
721
+ if (!sessionOwnerId) {
722
+ throw new Error("ChatConfig.sessionParams.id is required to send ask requests.");
723
+ }
724
+ const sessionAttributes = sessionOwner?.attributes && Object.keys(sessionOwner.attributes).length > 0 ? sessionOwner.attributes : void 0;
725
+ return {
726
+ agentId: config.agentId,
727
+ userInput: userMessage,
728
+ sessionId,
729
+ sessionOwnerId,
730
+ sessionOwnerLabel: sessionOwner?.name || void 0,
731
+ sessionAttributes,
732
+ analysisMode: options?.analysisMode,
733
+ locale: resolveLocale(config.locale),
734
+ timezone: resolveTimezone(config.timezone),
735
+ ...options?.attachments?.length ? { attachments: options.attachments } : {}
736
+ };
737
+ }
738
+ function resolveLocale(configured) {
739
+ if (configured && configured.trim().length > 0) return configured.trim();
740
+ if (typeof navigator !== "undefined") {
741
+ const navLocale = navigator.language;
742
+ if (navLocale && navLocale.length > 0) return navLocale;
743
+ }
744
+ return "en-US";
745
+ }
746
+ function resolveTimezone(configured) {
747
+ if (configured && configured.trim().length > 0) return configured.trim();
748
+ try {
749
+ const zone = Intl.DateTimeFormat().resolvedOptions().timeZone;
750
+ if (zone && zone.length > 0) return zone;
751
+ } catch {
752
+ }
753
+ return "UTC";
754
+ }
755
+ function buildStreamingUrl(config) {
756
+ const endpoint = config.api.streamEndpoint || DEFAULT_STREAM_ENDPOINT;
757
+ const stage = config.stage || "DEV";
758
+ const stageParamName = config.stageQueryParam ?? "stage";
759
+ const queryParams = new URLSearchParams({ [stageParamName]: stage });
760
+ return `${config.api.baseUrl}${endpoint}?${queryParams.toString()}`;
761
+ }
762
+ function buildUserActionUrl(config, userActionId, action) {
763
+ const endpoint = config.api.streamEndpoint || DEFAULT_STREAM_ENDPOINT;
764
+ const [endpointPath] = endpoint.split("?");
765
+ const normalizedEndpointPath = endpointPath.replace(/\/+$/, "");
766
+ const basePath = normalizedEndpointPath.endsWith("/stream") ? normalizedEndpointPath.slice(0, -"/stream".length) : normalizedEndpointPath;
767
+ const encodedUserActionId = encodeURIComponent(userActionId);
768
+ return `${config.api.baseUrl}${basePath}/user-action/${encodedUserActionId}/${action}`;
769
+ }
770
+ function buildResolveImagesUrl(config) {
771
+ if (config.api.resolveImagesEndpoint) {
772
+ return `${config.api.baseUrl}${config.api.resolveImagesEndpoint}`;
773
+ }
774
+ const streamEndpoint = config.api.streamEndpoint || DEFAULT_STREAM_ENDPOINT;
775
+ const [endpointPath] = streamEndpoint.split("?");
776
+ const normalizedEndpointPath = endpointPath.replace(/\/+$/, "");
777
+ const basePath = normalizedEndpointPath.endsWith("/stream") ? normalizedEndpointPath.slice(0, -"/stream".length) : normalizedEndpointPath;
778
+ return `${config.api.baseUrl}${basePath}/resolve-image-urls`;
779
+ }
780
+ var NGROK_SKIP_BROWSER_WARNING = "ngrok-skip-browser-warning";
781
+ function buildRequestHeaders(config) {
782
+ const headers = {
783
+ ...config.api.headers
784
+ };
785
+ if (config.api.authToken) {
786
+ headers.Authorization = `Bearer ${config.api.authToken}`;
787
+ }
788
+ if (!headers[NGROK_SKIP_BROWSER_WARNING]) {
789
+ headers[NGROK_SKIP_BROWSER_WARNING] = "true";
790
+ }
791
+ return headers;
792
+ }
793
+ var RAG_IMAGE_REGEX = /\/api\/rag\/chunks\/[^"'\s]+\/image/;
794
+ function hasRagImages(content) {
795
+ return RAG_IMAGE_REGEX.test(content);
796
+ }
797
+ async function waitForNextPaint(signal) {
798
+ if (signal?.aborted) return;
799
+ await new Promise((resolve) => {
800
+ let isSettled = false;
801
+ const finish = () => {
802
+ if (isSettled) return;
803
+ isSettled = true;
804
+ signal?.removeEventListener("abort", finish);
805
+ resolve();
806
+ };
807
+ signal?.addEventListener("abort", finish, { once: true });
808
+ if (typeof requestAnimationFrame === "function") {
809
+ requestAnimationFrame(() => {
810
+ setTimeout(finish, 0);
811
+ });
812
+ return;
813
+ }
814
+ setTimeout(finish, 0);
815
+ });
816
+ }
817
+ async function resolveRagImageUrls(config, content, signal) {
818
+ const url = buildResolveImagesUrl(config);
819
+ const baseHeaders = buildRequestHeaders(config);
820
+ const headers = { "Content-Type": "application/json", ...baseHeaders };
821
+ const response = await fetch(url, {
822
+ method: "POST",
823
+ headers,
824
+ body: JSON.stringify({ input: content }),
825
+ signal
826
+ });
827
+ if (!response.ok) {
828
+ const errorText = await response.text();
829
+ throw new Error(`HTTP ${response.status}: ${errorText}`);
830
+ }
831
+ const text = await response.text();
832
+ try {
833
+ const parsed = JSON.parse(text);
834
+ if (typeof parsed === "string") return parsed;
835
+ if (typeof parsed.output === "string") return parsed.output;
836
+ if (typeof parsed.result === "string") return parsed.result;
837
+ } catch {
838
+ }
839
+ return text;
840
+ }
841
+ var FRIENDLY_ERROR_MESSAGE = "Oops, something went wrong. Please try again.";
842
+ function useStreamManagerV2(config, callbacks, setMessages, setIsWaitingForResponse) {
843
+ const abortControllerRef = react.useRef(null);
844
+ const configRef = react.useRef(config);
845
+ configRef.current = config;
846
+ const callbacksRef = react.useRef(callbacks);
847
+ callbacksRef.current = callbacks;
848
+ const startStream = react.useCallback(
849
+ async (userMessage, streamingId, sessionId, externalAbortController, options) => {
850
+ abortControllerRef.current?.abort();
851
+ const abortController = externalAbortController ?? new AbortController();
852
+ abortControllerRef.current = abortController;
853
+ const state = createInitialV2State();
854
+ const updateMessage = (update) => {
855
+ if (abortController.signal.aborted) return;
856
+ setMessages(
857
+ (prev) => prev.map(
858
+ (msg) => msg.id === streamingId ? { ...msg, ...update } : msg
859
+ )
860
+ );
861
+ };
862
+ try {
863
+ const currentConfig = configRef.current;
864
+ const requestBody = buildRequestBody(
865
+ currentConfig,
866
+ userMessage,
867
+ sessionId,
868
+ options
869
+ );
870
+ const url = buildStreamingUrl(currentConfig);
871
+ const headers = buildRequestHeaders(currentConfig);
872
+ await streamWorkflowEvents(url, requestBody, headers, {
873
+ signal: abortController.signal,
874
+ onEvent: (event) => {
875
+ if (abortController.signal.aborted) return;
876
+ processStreamEventV2(event, state);
877
+ if (state.lastUserAction) {
878
+ callbacksRef.current.onUserActionRequired?.(state.lastUserAction);
879
+ }
880
+ if (state.lastNotification) {
881
+ callbacksRef.current.onUserNotification?.(state.lastNotification);
882
+ }
883
+ const activeStep = state.steps.find((s2) => s2.id === state.currentExecutingStepId);
884
+ const lastInProgressStep = [...state.steps].reverse().find((s2) => s2.status === "in_progress");
885
+ const useful = (m) => m && !isBlandStatus(m) ? m : void 0;
886
+ const latestUsefulStep = [...state.steps].reverse().find(
887
+ (s2) => s2.message && !isBlandStatus(s2.message)
888
+ );
889
+ const currentMessage = useful(activeStep?.message) ?? useful(lastInProgressStep?.message) ?? latestUsefulStep?.message ?? useful(getEventMessage(event)) ?? // Last-resort: every candidate is bland (very first event,
890
+ // nothing useful seen yet). Render the bland label so the
891
+ // bubble isn't blank.
892
+ activeStep?.message ?? lastInProgressStep?.message ?? getEventMessage(event);
893
+ if (currentMessage) {
894
+ callbacksRef.current.onStatusMessage?.(currentMessage);
895
+ }
896
+ callbacksRef.current.onStepsUpdate?.([...state.steps]);
897
+ if (state.hasError) {
898
+ updateMessage({
899
+ streamingContent: FRIENDLY_ERROR_MESSAGE,
900
+ content: FRIENDLY_ERROR_MESSAGE,
901
+ streamProgress: "error",
902
+ isError: true,
903
+ errorDetails: state.errorMessage,
904
+ steps: [...state.steps],
905
+ currentExecutingStepId: void 0,
906
+ executionId: state.executionId,
907
+ sessionId: state.sessionId
908
+ });
909
+ } else {
910
+ updateMessage({
911
+ streamingContent: state.finalResponse,
912
+ content: "",
913
+ currentMessage,
914
+ streamProgress: "processing",
915
+ isError: false,
916
+ steps: [...state.steps],
917
+ currentExecutingStepId: state.currentExecutingStepId,
918
+ executionId: state.executionId,
919
+ sessionId: state.sessionId,
920
+ isCancelled: false
921
+ });
922
+ }
923
+ },
924
+ onError: (error) => {
925
+ setIsWaitingForResponse(false);
926
+ callbacksRef.current.onStatusMessage?.(null);
927
+ if (error.name !== "AbortError") {
928
+ callbacksRef.current.onError?.(error);
929
+ }
930
+ const isAborted = error.name === "AbortError";
931
+ setMessages(
932
+ (prev) => prev.map(
933
+ (msg) => msg.id === streamingId ? {
934
+ ...msg,
935
+ isStreaming: false,
936
+ streamProgress: isAborted ? "processing" : "error",
937
+ isError: !isAborted,
938
+ isCancelled: isAborted,
939
+ errorDetails: isAborted ? void 0 : error.message,
940
+ content: isAborted ? state.finalResponse || "" : state.finalResponse || FRIENDLY_ERROR_MESSAGE,
941
+ currentMessage: isAborted ? "Thinking..." : void 0,
942
+ steps: [...state.steps].map((step) => {
943
+ if (step.status === "in_progress" && isAborted) {
944
+ return { ...step, status: "pending" };
945
+ }
946
+ return step;
947
+ }),
948
+ currentExecutingStepId: void 0
949
+ } : msg
950
+ )
951
+ );
952
+ },
953
+ onComplete: () => {
954
+ setIsWaitingForResponse(false);
955
+ callbacksRef.current.onStatusMessage?.(null);
956
+ callbacksRef.current.onStepsUpdate?.([]);
957
+ if (state.sessionId && state.sessionId !== sessionId) {
958
+ callbacksRef.current.onSessionIdChange?.(state.sessionId);
959
+ }
960
+ const needsImageResolve = !state.hasError && !abortController.signal.aborted && hasRagImages(state.finalResponse);
961
+ const finalMessage = {
962
+ id: streamingId,
963
+ sessionId: state.sessionId || sessionId,
964
+ role: "assistant",
965
+ content: state.hasError ? FRIENDLY_ERROR_MESSAGE : state.finalResponse || "",
966
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
967
+ isStreaming: false,
968
+ streamProgress: state.hasError ? "error" : "completed",
969
+ isError: state.hasError,
970
+ errorDetails: state.hasError ? state.errorMessage : void 0,
971
+ executionId: state.executionId,
972
+ // Defensive: tracingData must be an object (or undefined)
973
+ // so the JSON viewer doesn't char-iterate a string. The
974
+ // event processor already drops bare strings; this is
975
+ // the last stop before the message leaves the SDK.
976
+ tracingData: state.finalData != null && typeof state.finalData === "object" ? state.finalData : void 0,
977
+ steps: state.hasError ? [] : [...state.steps],
978
+ isCancelled: false,
979
+ currentExecutingStepId: void 0,
980
+ isResolvingImages: needsImageResolve,
981
+ totalElapsedMs: state.hasError ? void 0 : state.totalElapsedMs
982
+ };
983
+ setMessages(
984
+ (prev) => prev.map(
985
+ (msg) => msg.id === streamingId ? finalMessage : msg
986
+ )
987
+ );
988
+ callbacksRef.current.onStreamComplete?.(finalMessage);
989
+ }
990
+ });
991
+ const shouldResolveImages = !abortController.signal.aborted && !state.hasError && hasRagImages(state.finalResponse);
992
+ if (shouldResolveImages) {
993
+ await waitForNextPaint(abortController.signal);
994
+ }
995
+ if (shouldResolveImages && !abortController.signal.aborted) {
996
+ try {
997
+ const resolvedContent = await resolveRagImageUrls(
998
+ currentConfig,
999
+ state.finalResponse,
1000
+ abortController.signal
1001
+ );
1002
+ setMessages(
1003
+ (prev) => prev.map(
1004
+ (msg) => msg.id === streamingId ? { ...msg, content: resolvedContent, isResolvingImages: false } : msg
1005
+ )
1006
+ );
1007
+ } catch {
1008
+ setMessages(
1009
+ (prev) => prev.map(
1010
+ (msg) => msg.id === streamingId ? { ...msg, isResolvingImages: false } : msg
1011
+ )
1012
+ );
1013
+ }
1014
+ }
1015
+ return state.sessionId;
1016
+ } catch (error) {
1017
+ setIsWaitingForResponse(false);
1018
+ if (error.name !== "AbortError") {
1019
+ callbacksRef.current.onError?.(error);
1020
+ }
1021
+ const isAborted = error.name === "AbortError";
1022
+ setMessages(
1023
+ (prev) => prev.map(
1024
+ (msg) => msg.id === streamingId ? {
1025
+ ...msg,
1026
+ isStreaming: false,
1027
+ streamProgress: isAborted ? "processing" : "error",
1028
+ isError: !isAborted,
1029
+ isCancelled: isAborted,
1030
+ errorDetails: isAborted ? void 0 : error.message,
1031
+ content: isAborted ? state.finalResponse || "" : state.finalResponse || FRIENDLY_ERROR_MESSAGE,
1032
+ steps: [...state.steps].map((step) => {
1033
+ if (step.status === "in_progress" && isAborted) {
1034
+ return { ...step, status: "pending" };
1035
+ }
1036
+ return step;
1037
+ }),
1038
+ currentExecutingStepId: void 0
1039
+ } : msg
1040
+ )
1041
+ );
1042
+ return state.sessionId;
1043
+ }
1044
+ },
1045
+ [setMessages, setIsWaitingForResponse]
1046
+ );
1047
+ const cancelStream = react.useCallback(() => {
1048
+ abortControllerRef.current?.abort();
1049
+ }, []);
1050
+ return {
1051
+ startStream,
1052
+ cancelStream,
1053
+ abortControllerRef
1054
+ };
1055
+ }
1056
+ function createCancelledMessageUpdate(steps, currentMessage) {
1057
+ const updatedSteps = steps.map((step) => {
1058
+ if (step.status === "in_progress") {
1059
+ return { ...step, status: "pending" };
1060
+ }
1061
+ return step;
1062
+ });
1063
+ return {
1064
+ isStreaming: false,
1065
+ isCancelled: true,
1066
+ steps: updatedSteps,
1067
+ currentExecutingStepId: void 0,
1068
+ currentMessage: currentMessage || "Thinking..."
1069
+ };
1070
+ }
1071
+ var DEFAULT_SIGNED_URL_ENDPOINT = "/api/files/signed-url";
1072
+ function fileExtension(filename) {
1073
+ const ext = filename.split(".").pop()?.toLowerCase().replace(/^\./, "");
1074
+ return ext && ext.length > 0 ? ext : "bin";
1075
+ }
1076
+ function resolveMimeType(file) {
1077
+ if (file.type && file.type.trim().length > 0) return file.type;
1078
+ const ext = fileExtension(file.name);
1079
+ const byExt = {
1080
+ pdf: "application/pdf",
1081
+ png: "image/png",
1082
+ jpg: "image/jpeg",
1083
+ jpeg: "image/jpeg",
1084
+ gif: "image/gif",
1085
+ webp: "image/webp"
1086
+ };
1087
+ return byExt[ext] ?? "application/octet-stream";
1088
+ }
1089
+ function buildSignedUrlEndpoint(config, ext) {
1090
+ const endpoint = config.api.signedUrlEndpoint || DEFAULT_SIGNED_URL_ENDPOINT;
1091
+ const queryParams = new URLSearchParams({ extn: ext.replace(/^\./, "") });
1092
+ return `${config.api.baseUrl}${endpoint}?${queryParams.toString()}`;
1093
+ }
1094
+ async function requestSignedUrl(config, ext, signal) {
1095
+ const url = buildSignedUrlEndpoint(config, ext);
1096
+ const headers = buildRequestHeaders(config);
1097
+ const response = await fetch(url, {
1098
+ method: "GET",
1099
+ headers,
1100
+ signal
1101
+ });
1102
+ if (!response.ok) {
1103
+ const errorText = await response.text().catch(() => "");
1104
+ throw new Error(
1105
+ errorText ? `Failed to get upload URL (${response.status}): ${errorText}` : `Failed to get upload URL (${response.status})`
1106
+ );
1107
+ }
1108
+ const data = await response.json();
1109
+ if (!data.key || !data.url) {
1110
+ throw new Error("Signed URL response missing key or url");
1111
+ }
1112
+ return { key: data.key, url: data.url };
1113
+ }
1114
+ async function uploadToSignedUrl(signedUrl, file, mimeType, signal) {
1115
+ const response = await fetch(signedUrl, {
1116
+ method: "PUT",
1117
+ headers: {
1118
+ "Content-Type": mimeType,
1119
+ "x-ms-blob-type": "BlockBlob"
1120
+ },
1121
+ body: file,
1122
+ signal
1123
+ });
1124
+ if (!response.ok) {
1125
+ const errorText = await response.text().catch(() => "");
1126
+ throw new Error(
1127
+ errorText ? `Failed to upload file (${response.status}): ${errorText}` : `Failed to upload file (${response.status})`
1128
+ );
1129
+ }
1130
+ }
1131
+ async function uploadAttachment(config, file, signal) {
1132
+ const ext = fileExtension(file.name);
1133
+ const mimeType = resolveMimeType(file);
1134
+ const { key, url } = await requestSignedUrl(config, ext, signal);
1135
+ await uploadToSignedUrl(url, file, mimeType, signal);
1136
+ return {
1137
+ tempKey: key,
1138
+ filename: file.name,
1139
+ mimeType
1140
+ };
1141
+ }
1142
+ async function uploadAttachments(config, files, signal) {
1143
+ const uploads = files.map((file) => uploadAttachment(config, file, signal));
1144
+ return Promise.all(uploads);
1145
+ }
1146
+ var UserActionStaleError = class extends Error {
1147
+ constructor(userActionId, message = "User action is no longer actionable") {
1148
+ super(message);
1149
+ __publicField(this, "userActionId");
1150
+ this.name = "UserActionStaleError";
1151
+ this.userActionId = userActionId;
1152
+ }
1153
+ };
1154
+ async function sendUserActionRequest(config, userActionId, action, data) {
1155
+ const url = buildUserActionUrl(config, userActionId, action);
1156
+ const baseHeaders = buildRequestHeaders(config);
1157
+ const hasBody = data !== void 0;
1158
+ const headers = hasBody ? { "Content-Type": "application/json", ...baseHeaders } : baseHeaders;
1159
+ const response = await fetch(url, {
1160
+ method: "POST",
1161
+ headers,
1162
+ body: hasBody ? JSON.stringify(data) : void 0
1163
+ });
1164
+ if (response.status === 404) {
1165
+ throw new UserActionStaleError(userActionId);
1166
+ }
1167
+ if (!response.ok) {
1168
+ const errorText = await response.text();
1169
+ throw new Error(`HTTP ${response.status}: ${errorText}`);
1170
+ }
1171
+ return await response.json();
1172
+ }
1173
+ async function submitUserAction(config, userActionId, content) {
1174
+ return sendUserActionRequest(config, userActionId, "submit", content ?? {});
1175
+ }
1176
+ async function cancelUserAction(config, userActionId) {
1177
+ return sendUserActionRequest(config, userActionId, "cancel");
1178
+ }
1179
+ async function resendUserAction(config, userActionId) {
1180
+ return sendUserActionRequest(config, userActionId, "resend");
1181
+ }
1182
+ var EMPTY_USER_ACTION_STATE = { prompts: [], notifications: [] };
1183
+ function upsertPrompt(prompts, req) {
1184
+ const active = { ...req, status: "pending" };
1185
+ const idx = prompts.findIndex(
1186
+ (p) => req.toolCallId ? p.toolCallId === req.toolCallId : p.userActionId === req.userActionId
1187
+ );
1188
+ if (idx >= 0) {
1189
+ const next = prompts.slice();
1190
+ next[idx] = active;
1191
+ return next;
1192
+ }
1193
+ return [...prompts, active];
1194
+ }
1195
+ function getStoredOrInitialMessages(config) {
1196
+ if (!config.userId) return config.initialMessages ?? [];
1197
+ const activeStream = activeStreamStore.get(config.userId);
1198
+ if (activeStream) return activeStream.messages;
1199
+ const stored = chatStore.get(config.userId);
1200
+ if (stored.length > 0) return stored;
1201
+ if (config.initialMessages?.length) {
1202
+ chatStore.set(config.userId, config.initialMessages);
1203
+ return config.initialMessages;
1204
+ }
1205
+ return [];
1206
+ }
1207
+ function getSessionIdFromMessages(messages) {
1208
+ return messages.find((message) => message.sessionId)?.sessionId;
1209
+ }
1210
+ var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set(["jpg", "jpeg", "png", "gif", "webp", "avif"]);
1211
+ function attachmentKindFromFile(file) {
1212
+ const ext = file.name.split(".").pop()?.toLowerCase() ?? "";
1213
+ if (IMAGE_EXTENSIONS.has(ext) || file.type.startsWith("image/")) return "image";
1214
+ return "file";
1215
+ }
1216
+ function buildMessageAttachments(files) {
1217
+ return files.map((file, index) => {
1218
+ const kind = attachmentKindFromFile(file);
1219
+ return {
1220
+ id: `att-${Date.now()}-${index}`,
1221
+ filename: file.name,
1222
+ mimeType: file.type || "application/octet-stream",
1223
+ // Blob URL for all local files so PDFs/docs stay previewable after send.
1224
+ previewUrl: URL.createObjectURL(file),
1225
+ kind
1226
+ };
1227
+ });
1228
+ }
1229
+ function useChatV2(config, callbacks = {}) {
1230
+ const [messages, setMessages] = react.useState(() => getStoredOrInitialMessages(config));
1231
+ const [isWaitingForResponse, setIsWaitingForResponse] = react.useState(() => {
1232
+ if (!config.userId) return false;
1233
+ return activeStreamStore.get(config.userId)?.isWaiting ?? false;
1234
+ });
1235
+ const [isUploadingAttachments, setIsUploadingAttachments] = react.useState(false);
1236
+ const sessionIdRef = react.useRef(
1237
+ getSessionIdFromMessages(getStoredOrInitialMessages(config)) ?? config.initialSessionId ?? void 0
1238
+ );
1239
+ const prevUserIdRef = react.useRef(config.userId);
1240
+ const streamUserIdRef = react.useRef(void 0);
1241
+ const subscriptionPrevUserIdRef = react.useRef(config.userId);
1242
+ const callbacksRef = react.useRef(callbacks);
1243
+ callbacksRef.current = callbacks;
1244
+ const configRef = react.useRef(config);
1245
+ configRef.current = config;
1246
+ const messagesRef = react.useRef(messages);
1247
+ messagesRef.current = messages;
1248
+ const storeAwareSetMessages = react.useCallback(
1249
+ (updater) => {
1250
+ const streamUserId = streamUserIdRef.current;
1251
+ const currentUserId = configRef.current.userId;
1252
+ const storeKey = streamUserId ?? currentUserId;
1253
+ if (storeKey && activeStreamStore.has(storeKey)) {
1254
+ activeStreamStore.applyMessages(storeKey, updater);
1255
+ }
1256
+ if (!streamUserId || streamUserId === currentUserId) {
1257
+ setMessages(updater);
1258
+ }
1259
+ },
1260
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1261
+ []
1262
+ );
1263
+ const storeAwareSetIsWaiting = react.useCallback(
1264
+ (waiting) => {
1265
+ const streamUserId = streamUserIdRef.current;
1266
+ const currentUserId = configRef.current.userId;
1267
+ const storeKey = streamUserId ?? currentUserId;
1268
+ if (storeKey && activeStreamStore.has(storeKey)) {
1269
+ activeStreamStore.setWaiting(storeKey, waiting);
1270
+ }
1271
+ if (!streamUserId || streamUserId === currentUserId) {
1272
+ setIsWaitingForResponse(waiting);
1273
+ }
1274
+ },
1275
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1276
+ []
1277
+ );
1278
+ const [userActionState, setUserActionState] = react.useState(EMPTY_USER_ACTION_STATE);
1279
+ const wrappedCallbacks = react.useMemo(() => ({
1280
+ ...callbacksRef.current,
1281
+ onMessageSent: (message) => callbacksRef.current.onMessageSent?.(message),
1282
+ onStreamStart: () => callbacksRef.current.onStreamStart?.(),
1283
+ onStreamComplete: (message) => callbacksRef.current.onStreamComplete?.(message),
1284
+ onError: (error) => callbacksRef.current.onError?.(error),
1285
+ onExecutionTraceClick: (data) => callbacksRef.current.onExecutionTraceClick?.(data),
1286
+ onSessionIdChange: (sessionId) => callbacksRef.current.onSessionIdChange?.(sessionId),
1287
+ onUserActionRequired: (request) => {
1288
+ setUserActionState((prev) => ({
1289
+ ...prev,
1290
+ prompts: upsertPrompt(prev.prompts, request)
1291
+ }));
1292
+ callbacksRef.current.onUserActionRequired?.(request);
1293
+ },
1294
+ onUserNotification: (notification) => {
1295
+ setUserActionState(
1296
+ (prev) => prev.notifications.some((n) => n.id === notification.id) ? prev : { ...prev, notifications: [...prev.notifications, notification] }
1297
+ );
1298
+ callbacksRef.current.onUserNotification?.(notification);
1299
+ }
1300
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1301
+ }), []);
1302
+ const { startStream, cancelStream: cancelStreamManager, abortControllerRef } = useStreamManagerV2(
1303
+ config,
1304
+ wrappedCallbacks,
1305
+ storeAwareSetMessages,
1306
+ storeAwareSetIsWaiting
1307
+ );
1308
+ const sendMessage = react.useCallback(
1309
+ async (userMessage, options) => {
1310
+ const trimmedMessage = userMessage.trim();
1311
+ const files = options?.files ?? [];
1312
+ const hasPreuploadedAttachments = (options?.attachments?.length ?? 0) > 0;
1313
+ if (!trimmedMessage && files.length === 0 && !hasPreuploadedAttachments) return;
1314
+ let streamAttachments = options?.attachments;
1315
+ if (!streamAttachments && files.length > 0) {
1316
+ setIsUploadingAttachments(true);
1317
+ try {
1318
+ streamAttachments = await uploadAttachments(configRef.current, files);
1319
+ } catch (error) {
1320
+ if (error.name !== "AbortError") {
1321
+ callbacksRef.current.onError?.(error);
1322
+ }
1323
+ throw error;
1324
+ } finally {
1325
+ setIsUploadingAttachments(false);
1326
+ }
1327
+ }
1328
+ if (!sessionIdRef.current && configRef.current.autoGenerateSessionId !== false) {
1329
+ sessionIdRef.current = generateId();
1330
+ callbacksRef.current.onSessionIdChange?.(sessionIdRef.current);
1331
+ }
1332
+ const messageAttachments = files.length > 0 ? buildMessageAttachments(files) : streamAttachments?.length ? streamAttachments.map((attachment, index) => ({
1333
+ id: `att-${Date.now()}-${index}`,
1334
+ filename: attachment.filename,
1335
+ mimeType: attachment.mimeType,
1336
+ kind: attachment.mimeType.startsWith("image/") ? "image" : "file"
1337
+ })) : void 0;
1338
+ const userMessageId = `user-${Date.now()}`;
1339
+ const userMsg = {
1340
+ id: userMessageId,
1341
+ sessionId: sessionIdRef.current,
1342
+ role: "user",
1343
+ content: trimmedMessage,
1344
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1345
+ attachments: messageAttachments
1346
+ };
1347
+ setMessages((prev) => [...prev, userMsg]);
1348
+ callbacksRef.current.onMessageSent?.(trimmedMessage);
1349
+ setIsWaitingForResponse(true);
1350
+ callbacksRef.current.onStreamStart?.();
1351
+ const streamingId = `assistant-${Date.now()}`;
1352
+ const streamingMsg = {
1353
+ id: streamingId,
1354
+ sessionId: sessionIdRef.current,
1355
+ role: "assistant",
1356
+ content: "",
1357
+ streamingContent: "",
1358
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1359
+ isStreaming: true,
1360
+ streamProgress: "started",
1361
+ steps: [],
1362
+ currentExecutingStepId: void 0,
1363
+ isCancelled: false,
1364
+ currentMessage: void 0
1365
+ };
1366
+ setMessages((prev) => [...prev, streamingMsg]);
1367
+ const abortController = new AbortController();
1368
+ const { userId } = configRef.current;
1369
+ if (userId) {
1370
+ streamUserIdRef.current = userId;
1371
+ const initialMessages = [...messagesRef.current, userMsg, streamingMsg];
1372
+ activeStreamStore.start(userId, abortController, initialMessages);
1373
+ }
1374
+ const newSessionId = await startStream(
1375
+ trimmedMessage,
1376
+ streamingId,
1377
+ sessionIdRef.current,
1378
+ abortController,
1379
+ {
1380
+ analysisMode: options?.analysisMode,
1381
+ attachments: streamAttachments
1382
+ }
1383
+ );
1384
+ const finalStreamUserId = streamUserIdRef.current ?? userId;
1385
+ if (finalStreamUserId) {
1386
+ activeStreamStore.complete(finalStreamUserId);
1387
+ }
1388
+ streamUserIdRef.current = void 0;
1389
+ if (!abortController.signal.aborted && newSessionId && newSessionId !== sessionIdRef.current) {
1390
+ sessionIdRef.current = newSessionId;
1391
+ }
1392
+ },
1393
+ [startStream]
1394
+ );
1395
+ const clearMessages = react.useCallback(() => {
1396
+ if (configRef.current.userId) {
1397
+ chatStore.delete(configRef.current.userId);
1398
+ }
1399
+ setMessages([]);
1400
+ }, []);
1401
+ const prependMessages = react.useCallback((msgs) => {
1402
+ setMessages((prev) => [...msgs, ...prev]);
1403
+ }, []);
1404
+ const cancelStream = react.useCallback(() => {
1405
+ const streamUserId = streamUserIdRef.current ?? configRef.current.userId;
1406
+ if (streamUserId) {
1407
+ activeStreamStore.abort(streamUserId);
1408
+ }
1409
+ streamUserIdRef.current = void 0;
1410
+ cancelStreamManager();
1411
+ setIsWaitingForResponse(false);
1412
+ setUserActionState((prev) => ({ ...prev, prompts: [] }));
1413
+ setMessages(
1414
+ (prev) => prev.map((msg) => {
1415
+ if (msg.isStreaming) {
1416
+ return {
1417
+ ...msg,
1418
+ ...createCancelledMessageUpdate(
1419
+ msg.steps || [],
1420
+ msg.currentMessage
1421
+ )
1422
+ };
1423
+ }
1424
+ return msg;
1425
+ })
1426
+ );
1427
+ }, [cancelStreamManager]);
1428
+ const resetSession = react.useCallback(() => {
1429
+ const streamUserId = streamUserIdRef.current ?? configRef.current.userId;
1430
+ if (streamUserId) {
1431
+ activeStreamStore.abort(streamUserId);
1432
+ }
1433
+ if (configRef.current.userId) {
1434
+ chatStore.delete(configRef.current.userId);
1435
+ }
1436
+ streamUserIdRef.current = void 0;
1437
+ setMessages([]);
1438
+ sessionIdRef.current = void 0;
1439
+ abortControllerRef.current?.abort();
1440
+ setIsWaitingForResponse(false);
1441
+ setUserActionState(EMPTY_USER_ACTION_STATE);
1442
+ }, []);
1443
+ const getSessionId = react.useCallback(() => {
1444
+ return sessionIdRef.current;
1445
+ }, []);
1446
+ const getMessages = react.useCallback(() => {
1447
+ return messages;
1448
+ }, [messages]);
1449
+ const setPromptStatus = react.useCallback(
1450
+ (userActionId, status) => {
1451
+ setUserActionState((prev) => ({
1452
+ ...prev,
1453
+ prompts: prev.prompts.map(
1454
+ (p) => p.userActionId === userActionId ? { ...p, status } : p
1455
+ )
1456
+ }));
1457
+ },
1458
+ []
1459
+ );
1460
+ const removePrompt = react.useCallback((userActionId) => {
1461
+ setUserActionState((prev) => ({
1462
+ ...prev,
1463
+ prompts: prev.prompts.filter((p) => p.userActionId !== userActionId)
1464
+ }));
1465
+ }, []);
1466
+ const submitUserAction2 = react.useCallback(
1467
+ async (userActionId, content) => {
1468
+ setPromptStatus(userActionId, "submitting");
1469
+ try {
1470
+ await submitUserAction(configRef.current, userActionId, content);
1471
+ removePrompt(userActionId);
1472
+ } catch (error) {
1473
+ if (error instanceof UserActionStaleError) {
1474
+ setPromptStatus(userActionId, "stale");
1475
+ return;
1476
+ }
1477
+ setPromptStatus(userActionId, "pending");
1478
+ callbacksRef.current.onError?.(error);
1479
+ throw error;
1480
+ }
1481
+ },
1482
+ [removePrompt, setPromptStatus]
1483
+ );
1484
+ const cancelUserAction2 = react.useCallback(
1485
+ async (userActionId) => {
1486
+ setPromptStatus(userActionId, "submitting");
1487
+ try {
1488
+ await cancelUserAction(configRef.current, userActionId);
1489
+ removePrompt(userActionId);
1490
+ } catch (error) {
1491
+ if (error instanceof UserActionStaleError) {
1492
+ removePrompt(userActionId);
1493
+ return;
1494
+ }
1495
+ setPromptStatus(userActionId, "pending");
1496
+ callbacksRef.current.onError?.(error);
1497
+ throw error;
1498
+ }
1499
+ },
1500
+ [removePrompt, setPromptStatus]
1501
+ );
1502
+ const resendUserAction2 = react.useCallback(
1503
+ async (userActionId) => {
1504
+ setPromptStatus(userActionId, "submitting");
1505
+ try {
1506
+ await resendUserAction(configRef.current, userActionId);
1507
+ setPromptStatus(userActionId, "pending");
1508
+ } catch (error) {
1509
+ if (error instanceof UserActionStaleError) {
1510
+ setPromptStatus(userActionId, "stale");
1511
+ return;
1512
+ }
1513
+ setPromptStatus(userActionId, "pending");
1514
+ callbacksRef.current.onError?.(error);
1515
+ throw error;
1516
+ }
1517
+ },
1518
+ [setPromptStatus]
1519
+ );
1520
+ const dismissNotification = react.useCallback((id) => {
1521
+ setUserActionState((prev) => ({
1522
+ ...prev,
1523
+ notifications: prev.notifications.filter((n) => n.id !== id)
1524
+ }));
1525
+ }, []);
1526
+ react.useEffect(() => {
1527
+ const prevSubscriptionUserId = subscriptionPrevUserIdRef.current;
1528
+ subscriptionPrevUserIdRef.current = config.userId;
1529
+ const { userId } = config;
1530
+ if (!userId) return;
1531
+ if (prevSubscriptionUserId && prevSubscriptionUserId !== userId && streamUserIdRef.current === prevSubscriptionUserId && !activeStreamStore.has(prevSubscriptionUserId) && activeStreamStore.has(userId)) {
1532
+ streamUserIdRef.current = userId;
1533
+ }
1534
+ const unsubscribe = activeStreamStore.subscribe(userId, (msgs, isWaiting) => {
1535
+ setMessages(msgs);
1536
+ setIsWaitingForResponse(isWaiting);
1537
+ });
1538
+ const active = activeStreamStore.get(userId);
1539
+ if (active) {
1540
+ setMessages(active.messages);
1541
+ setIsWaitingForResponse(active.isWaiting);
1542
+ }
1543
+ return unsubscribe;
1544
+ }, [config.userId]);
1545
+ react.useEffect(() => {
1546
+ if (!config.userId) return;
1547
+ if (prevUserIdRef.current !== config.userId) return;
1548
+ const toSave = messages.filter((m) => !m.isStreaming);
1549
+ if (toSave.length > 0) {
1550
+ chatStore.set(config.userId, toSave);
1551
+ }
1552
+ }, [messages, config.userId]);
1553
+ react.useEffect(() => {
1554
+ if (!config.userId || activeStreamStore.has(config.userId)) return;
1555
+ if (!config.initialMessages?.length || messagesRef.current.length > 0) return;
1556
+ chatStore.set(config.userId, config.initialMessages);
1557
+ setMessages(config.initialMessages);
1558
+ sessionIdRef.current = getSessionIdFromMessages(config.initialMessages) ?? config.initialSessionId;
1559
+ }, [config.initialMessages, config.initialSessionId, config.userId]);
1560
+ const hydratedSessionIdRef = react.useRef(void 0);
1561
+ react.useEffect(() => {
1562
+ if (config.userId) return;
1563
+ if (!config.initialMessages?.length) return;
1564
+ const sessionId = config.initialSessionId;
1565
+ if (hydratedSessionIdRef.current === sessionId && messagesRef.current.length > 0) {
1566
+ return;
1567
+ }
1568
+ hydratedSessionIdRef.current = sessionId;
1569
+ setMessages(config.initialMessages);
1570
+ sessionIdRef.current = getSessionIdFromMessages(config.initialMessages) ?? sessionId;
1571
+ }, [config.initialMessages, config.initialSessionId, config.userId]);
1572
+ react.useEffect(() => {
1573
+ const prevUserId = prevUserIdRef.current;
1574
+ prevUserIdRef.current = config.userId;
1575
+ if (prevUserId === config.userId) return;
1576
+ if (prevUserId && !config.userId) {
1577
+ chatStore.delete(prevUserId);
1578
+ setMessages([]);
1579
+ sessionIdRef.current = void 0;
1580
+ setIsWaitingForResponse(false);
1581
+ setUserActionState(EMPTY_USER_ACTION_STATE);
1582
+ } else if (config.userId) {
1583
+ const active = activeStreamStore.get(config.userId);
1584
+ if (active) {
1585
+ setMessages(active.messages);
1586
+ setIsWaitingForResponse(active.isWaiting);
1587
+ sessionIdRef.current = getSessionIdFromMessages(active.messages) ?? config.initialSessionId;
1588
+ return;
1589
+ }
1590
+ const nextMessages = getStoredOrInitialMessages(config);
1591
+ setMessages(nextMessages);
1592
+ sessionIdRef.current = getSessionIdFromMessages(nextMessages) ?? config.initialSessionId;
1593
+ setIsWaitingForResponse(false);
1594
+ }
1595
+ }, [config]);
1596
+ return {
1597
+ messages,
1598
+ sendMessage,
1599
+ clearMessages,
1600
+ prependMessages,
1601
+ cancelStream,
1602
+ resetSession,
1603
+ getSessionId,
1604
+ getMessages,
1605
+ isWaitingForResponse,
1606
+ isUploadingAttachments,
1607
+ sessionId: sessionIdRef.current,
1608
+ userActionState,
1609
+ submitUserAction: submitUserAction2,
1610
+ cancelUserAction: cancelUserAction2,
1611
+ resendUserAction: resendUserAction2,
1612
+ dismissNotification
1613
+ };
1614
+ }
1615
+ function getSpeechRecognition() {
1616
+ if (typeof window === "undefined") return null;
1617
+ return window.SpeechRecognition || window.webkitSpeechRecognition || null;
1618
+ }
1619
+ function useVoice(config = {}, callbacks = {}) {
1620
+ const [voiceState, setVoiceState] = react.useState("idle");
1621
+ const [transcribedText, setTranscribedText] = react.useState("");
1622
+ const [isAvailable, setIsAvailable] = react.useState(false);
1623
+ const [isRecording, setIsRecording] = react.useState(false);
1624
+ const recognitionRef = react.useRef(null);
1625
+ const autoStopTimerRef = react.useRef(null);
1626
+ const {
1627
+ lang = "en-US",
1628
+ interimResults = true,
1629
+ continuous = true,
1630
+ maxAlternatives = 1,
1631
+ autoStopAfterSilence
1632
+ } = config;
1633
+ const { onStart, onEnd, onResult, onError, onStateChange } = callbacks;
1634
+ react.useEffect(() => {
1635
+ const SpeechRecognitionAPI = getSpeechRecognition();
1636
+ setIsAvailable(SpeechRecognitionAPI !== null);
1637
+ }, []);
1638
+ react.useEffect(() => {
1639
+ onStateChange?.(voiceState);
1640
+ }, [voiceState, onStateChange]);
1641
+ const requestPermissions = react.useCallback(async () => {
1642
+ try {
1643
+ const result = await navigator.mediaDevices.getUserMedia({
1644
+ audio: true
1645
+ });
1646
+ result.getTracks().forEach((track) => track.stop());
1647
+ return {
1648
+ granted: true,
1649
+ status: "granted"
1650
+ };
1651
+ } catch (error) {
1652
+ return {
1653
+ granted: false,
1654
+ status: "denied"
1655
+ };
1656
+ }
1657
+ }, []);
1658
+ const getPermissions = react.useCallback(async () => {
1659
+ if (typeof navigator === "undefined" || !navigator.permissions) {
1660
+ return {
1661
+ granted: false,
1662
+ status: "undetermined"
1663
+ };
1664
+ }
1665
+ try {
1666
+ const result = await navigator.permissions.query({
1667
+ name: "microphone"
1668
+ });
1669
+ return {
1670
+ granted: result.state === "granted",
1671
+ status: result.state === "granted" ? "granted" : result.state === "denied" ? "denied" : "undetermined"
1672
+ };
1673
+ } catch {
1674
+ return {
1675
+ granted: false,
1676
+ status: "undetermined"
1677
+ };
1678
+ }
1679
+ }, []);
1680
+ const clearAutoStopTimer = react.useCallback(() => {
1681
+ if (autoStopTimerRef.current) {
1682
+ clearTimeout(autoStopTimerRef.current);
1683
+ autoStopTimerRef.current = null;
1684
+ }
1685
+ }, []);
1686
+ const resetAutoStopTimer = react.useCallback(() => {
1687
+ clearAutoStopTimer();
1688
+ if (autoStopAfterSilence && autoStopAfterSilence > 0) {
1689
+ autoStopTimerRef.current = setTimeout(() => {
1690
+ if (recognitionRef.current && isRecording) {
1691
+ recognitionRef.current.stop();
1692
+ }
1693
+ }, autoStopAfterSilence);
1694
+ }
1695
+ }, [autoStopAfterSilence, clearAutoStopTimer, isRecording]);
1696
+ const stopRecording = react.useCallback(() => {
1697
+ if (recognitionRef.current) {
1698
+ try {
1699
+ recognitionRef.current.stop();
1700
+ } catch (error) {
1701
+ console.warn("Error stopping speech recognition:", error);
1702
+ }
1703
+ }
1704
+ clearAutoStopTimer();
1705
+ setIsRecording(false);
1706
+ setVoiceState("idle");
1707
+ }, [clearAutoStopTimer]);
1708
+ const startRecording = react.useCallback(async () => {
1709
+ const SpeechRecognitionAPI = getSpeechRecognition();
1710
+ if (!SpeechRecognitionAPI) {
1711
+ onError?.("Speech recognition not supported in this browser");
1712
+ return;
1713
+ }
1714
+ try {
1715
+ try {
1716
+ await navigator.mediaDevices.getUserMedia({ audio: true });
1717
+ } catch (permError) {
1718
+ onError?.("Microphone access denied. Please allow microphone access in your browser settings.");
1719
+ return;
1720
+ }
1721
+ const recognition = new SpeechRecognitionAPI();
1722
+ recognition.continuous = continuous;
1723
+ recognition.interimResults = interimResults;
1724
+ recognition.lang = lang;
1725
+ recognition.maxAlternatives = maxAlternatives;
1726
+ recognition.onstart = () => {
1727
+ setVoiceState("listening");
1728
+ setIsRecording(true);
1729
+ onStart?.();
1730
+ resetAutoStopTimer();
1731
+ };
1732
+ recognition.onend = () => {
1733
+ setVoiceState("idle");
1734
+ setIsRecording(false);
1735
+ clearAutoStopTimer();
1736
+ onEnd?.();
1737
+ };
1738
+ recognition.onresult = (event) => {
1739
+ const results = event.results;
1740
+ let transcript = "";
1741
+ for (let i = 0; i < results.length; i++) {
1742
+ const result = results[i];
1743
+ if (result && result[0]) {
1744
+ const text = result[0].transcript;
1745
+ if (transcript && !transcript.endsWith(" ") && !text.startsWith(" ")) {
1746
+ transcript += " " + text;
1747
+ } else {
1748
+ transcript += text;
1749
+ }
1750
+ }
1751
+ }
1752
+ transcript = transcript.trim();
1753
+ if (transcript) {
1754
+ setTranscribedText(transcript);
1755
+ onResult?.(transcript);
1756
+ resetAutoStopTimer();
1757
+ }
1758
+ };
1759
+ recognition.onerror = (event) => {
1760
+ setVoiceState("error");
1761
+ setIsRecording(false);
1762
+ clearAutoStopTimer();
1763
+ let errorMessage = event.error;
1764
+ if (event.error === "not-allowed") {
1765
+ errorMessage = "Microphone access denied. Please allow microphone access in your browser settings.";
1766
+ } else if (event.error === "no-speech") {
1767
+ errorMessage = "No speech detected. Please try again.";
1768
+ } else if (event.error === "audio-capture") {
1769
+ errorMessage = "No microphone found or microphone is in use.";
1770
+ } else if (event.error === "network") {
1771
+ errorMessage = "Network error occurred. Please check your connection.";
1772
+ }
1773
+ onError?.(errorMessage);
1774
+ };
1775
+ recognitionRef.current = recognition;
1776
+ setTranscribedText("");
1777
+ recognition.start();
1778
+ } catch (error) {
1779
+ setVoiceState("error");
1780
+ setIsRecording(false);
1781
+ onError?.(
1782
+ error instanceof Error ? error.message : "Failed to start recording"
1783
+ );
1784
+ }
1785
+ }, [
1786
+ lang,
1787
+ interimResults,
1788
+ continuous,
1789
+ maxAlternatives,
1790
+ onStart,
1791
+ onEnd,
1792
+ onResult,
1793
+ onError,
1794
+ getPermissions,
1795
+ resetAutoStopTimer,
1796
+ clearAutoStopTimer
1797
+ ]);
1798
+ const clearTranscript = react.useCallback(() => {
1799
+ setTranscribedText("");
1800
+ }, []);
1801
+ const reset = react.useCallback(() => {
1802
+ stopRecording();
1803
+ setTranscribedText("");
1804
+ setVoiceState("idle");
1805
+ }, [stopRecording]);
1806
+ react.useEffect(() => {
1807
+ return () => {
1808
+ if (recognitionRef.current) {
1809
+ try {
1810
+ recognitionRef.current.stop();
1811
+ } catch {
1812
+ }
1813
+ }
1814
+ clearAutoStopTimer();
1815
+ };
1816
+ }, [clearAutoStopTimer]);
1817
+ return {
1818
+ voiceState,
1819
+ transcribedText,
1820
+ isAvailable,
1821
+ isRecording,
1822
+ startRecording,
1823
+ stopRecording,
1824
+ requestPermissions,
1825
+ getPermissions,
1826
+ clearTranscript,
1827
+ reset
1828
+ };
1829
+ }
1830
+ function classifyField(field) {
1831
+ if (!field) return "text";
1832
+ if (Array.isArray(field.oneOf) && field.oneOf.length > 0) return "select";
1833
+ switch (field.type) {
1834
+ case "boolean":
1835
+ return "boolean";
1836
+ case "integer":
1837
+ return "integer";
1838
+ case "number":
1839
+ return "decimal";
1840
+ case "string":
1841
+ return "text";
1842
+ default:
1843
+ return "text";
1844
+ }
1845
+ }
1846
+ function isNestedOrUnsupported(field) {
1847
+ if (!field) return false;
1848
+ if (field.type === "object" || field.type === "array") return true;
1849
+ if ("properties" in field && field.properties != null) return true;
1850
+ if ("items" in field && field.items != null) return true;
1851
+ return false;
1852
+ }
1853
+ function getOptions(field) {
1854
+ if (!field || !Array.isArray(field.oneOf)) return [];
1855
+ return field.oneOf.filter(
1856
+ (o) => !!o && typeof o === "object" && typeof o.const === "string"
1857
+ );
1858
+ }
1859
+ function isRequired(schema, key) {
1860
+ return Array.isArray(schema?.required) && schema.required.includes(key);
1861
+ }
1862
+ function coerceValue(field, raw) {
1863
+ const widget = classifyField(field);
1864
+ if (widget === "boolean") {
1865
+ if (typeof raw === "boolean") return raw;
1866
+ if (raw === "true") return true;
1867
+ if (raw === "false") return false;
1868
+ return Boolean(raw);
1869
+ }
1870
+ if (widget === "integer" || widget === "decimal") {
1871
+ if (raw === "" || raw == null) return void 0;
1872
+ const num = typeof raw === "number" ? raw : Number(String(raw).trim());
1873
+ if (Number.isNaN(num)) return void 0;
1874
+ return widget === "integer" ? Math.trunc(num) : num;
1875
+ }
1876
+ if (raw == null) return void 0;
1877
+ const str = String(raw);
1878
+ return str === "" ? void 0 : str;
1879
+ }
1880
+ function defaultValueFor(field) {
1881
+ if (!field || field.default === void 0) {
1882
+ return classifyField(field) === "boolean" ? false : "";
1883
+ }
1884
+ return field.default;
1885
+ }
1886
+ function validateField(field, value, required) {
1887
+ const widget = classifyField(field);
1888
+ const label = field?.title || "This field";
1889
+ const isEmpty = value === void 0 || value === null || typeof value === "string" && value.trim() === "";
1890
+ if (isEmpty) {
1891
+ if (required && widget !== "boolean") return `${label} is required.`;
1892
+ return null;
1893
+ }
1894
+ if (widget === "integer" || widget === "decimal") {
1895
+ const num = typeof value === "number" ? value : Number(value);
1896
+ if (Number.isNaN(num)) return `${label} must be a number.`;
1897
+ if (widget === "integer" && !Number.isInteger(num)) {
1898
+ return `${label} must be a whole number.`;
1899
+ }
1900
+ if (typeof field?.minimum === "number" && num < field.minimum) {
1901
+ return `${label} must be at least ${field.minimum}.`;
1902
+ }
1903
+ if (typeof field?.maximum === "number" && num > field.maximum) {
1904
+ return `${label} must be at most ${field.maximum}.`;
1905
+ }
1906
+ return null;
1907
+ }
1908
+ if (widget === "select") {
1909
+ const allowed = getOptions(field).map((o) => o.const);
1910
+ if (allowed.length > 0 && !allowed.includes(String(value))) {
1911
+ return `${label} has an invalid selection.`;
1912
+ }
1913
+ return null;
1914
+ }
1915
+ const str = String(value);
1916
+ if (typeof field?.minLength === "number" && str.length < field.minLength) {
1917
+ return `${label} must be at least ${field.minLength} characters.`;
1918
+ }
1919
+ if (typeof field?.maxLength === "number" && str.length > field.maxLength) {
1920
+ return `${label} must be at most ${field.maxLength} characters.`;
1921
+ }
1922
+ return null;
1923
+ }
1924
+ function renderableFields(schema) {
1925
+ const props = schema?.properties;
1926
+ if (!props) return [];
1927
+ return Object.entries(props).filter(([, field]) => !isNestedOrUnsupported(field));
1928
+ }
1929
+ function validateForm(schema, values) {
1930
+ const errors = {};
1931
+ for (const [key, field] of renderableFields(schema)) {
1932
+ const coerced = coerceValue(field, values[key]);
1933
+ const err = validateField(field, coerced, isRequired(schema, key));
1934
+ if (err) errors[key] = err;
1935
+ }
1936
+ return errors;
1937
+ }
1938
+ function buildContent(schema, values) {
1939
+ const content = {};
1940
+ for (const [key, field] of renderableFields(schema)) {
1941
+ const coerced = coerceValue(field, values[key]);
1942
+ if (coerced !== void 0) content[key] = coerced;
1943
+ }
1944
+ return content;
1945
+ }
1946
+ var PaymanChatContext = react.createContext(void 0);
1947
+ function usePaymanChat() {
1948
+ const context = react.useContext(PaymanChatContext);
1949
+ if (!context) {
1950
+ throw new Error("usePaymanChat must be used within a PaymanChat component");
1951
+ }
1952
+ return context;
1953
+ }
1954
+ if (reactNative.Platform.OS === "android" && reactNative.UIManager.setLayoutAnimationEnabledExperimental) {
1955
+ reactNative.UIManager.setLayoutAnimationEnabledExperimental(true);
1956
+ }
1957
+ var DEFAULT_ACCENT = "#00858d";
1958
+ var VOICE_DOT_COUNT = 24;
1959
+ var VOICE_DOT_MIN_H = 5;
1960
+ var VOICE_DOT_MAX_H = 14;
1961
+ var SPEECH_ACTIVITY_MS = 1400;
1962
+ var VOICE_BAR_BG = "#f0f9f9";
1963
+ var AI_DISCLAIMER = "AI can make mistakes. Please double-check responses.";
1964
+ var VoiceWaveformBar = react.memo(function VoiceWaveformBar2({
1965
+ index,
1966
+ isActive,
1967
+ accent
1968
+ }) {
1969
+ const height = react.useRef(new reactNative.Animated.Value(VOICE_DOT_MIN_H)).current;
1970
+ react.useEffect(() => {
1971
+ if (isActive) {
1972
+ const anim = reactNative.Animated.loop(
1973
+ reactNative.Animated.sequence([
1974
+ reactNative.Animated.timing(height, {
1975
+ toValue: VOICE_DOT_MAX_H,
1976
+ duration: 180 + index % 3 * 40,
1977
+ delay: index * 25,
1978
+ easing: reactNative.Easing.inOut(reactNative.Easing.ease),
1979
+ useNativeDriver: false
1980
+ }),
1981
+ reactNative.Animated.timing(height, {
1982
+ toValue: VOICE_DOT_MIN_H,
1983
+ duration: 180 + index % 3 * 40,
1984
+ easing: reactNative.Easing.inOut(reactNative.Easing.ease),
1985
+ useNativeDriver: false
1986
+ })
1987
+ ])
1988
+ );
1989
+ anim.start();
1990
+ return () => anim.stop();
1991
+ }
1992
+ reactNative.Animated.timing(height, { toValue: VOICE_DOT_MIN_H, duration: 200, useNativeDriver: false }).start();
1993
+ }, [isActive, index, height]);
1994
+ return /* @__PURE__ */ jsxRuntime.jsx(
1995
+ reactNative.Animated.View,
1996
+ {
1997
+ style: [s.voiceDot, { height, backgroundColor: accent + "99" }]
1998
+ }
1999
+ );
2000
+ });
2001
+ function formatDuration(sec) {
2002
+ const m = Math.floor(sec / 60);
2003
+ const s2 = sec % 60;
2004
+ return `${m}:${s2.toString().padStart(2, "0")}`;
2005
+ }
2006
+ function InputBar({
2007
+ value,
2008
+ onChange,
2009
+ onSend,
2010
+ onCancel,
2011
+ disabled,
2012
+ placeholder,
2013
+ isStreaming,
2014
+ accent,
2015
+ enableVoice,
2016
+ voiceAvailable,
2017
+ isRecording,
2018
+ recordingSeconds,
2019
+ transcribedText,
2020
+ onVoicePress,
2021
+ onConfirmRecording,
2022
+ onCancelRecording,
2023
+ onFocus,
2024
+ insetBottom
2025
+ }) {
2026
+ const canSend = !disabled && !isStreaming && value.trim().length > 0;
2027
+ const voiceDisabled = !voiceAvailable || disabled || isStreaming;
2028
+ const showVoiceBar = enableVoice && isRecording;
2029
+ const showVoiceBtn = enableVoice && !isRecording;
2030
+ const [keyboardOpen, setKeyboardOpen] = react.useState(false);
2031
+ react.useEffect(() => {
2032
+ const show = reactNative.Keyboard.addListener("keyboardWillShow", () => setKeyboardOpen(true));
2033
+ const hide = reactNative.Keyboard.addListener("keyboardWillHide", () => setKeyboardOpen(false));
2034
+ return () => {
2035
+ show.remove();
2036
+ hide.remove();
2037
+ };
2038
+ }, []);
2039
+ const [isSpeaking, setIsSpeaking] = react.useState(false);
2040
+ const prevTranscript = react.useRef("");
2041
+ const speechTimeout = react.useRef(null);
2042
+ react.useEffect(() => {
2043
+ if (!showVoiceBar) {
2044
+ setIsSpeaking(false);
2045
+ prevTranscript.current = "";
2046
+ if (speechTimeout.current) clearTimeout(speechTimeout.current);
2047
+ return;
2048
+ }
2049
+ const changed = transcribedText !== prevTranscript.current;
2050
+ prevTranscript.current = transcribedText;
2051
+ if (changed) {
2052
+ setIsSpeaking(true);
2053
+ if (speechTimeout.current) clearTimeout(speechTimeout.current);
2054
+ speechTimeout.current = setTimeout(() => setIsSpeaking(false), SPEECH_ACTIVITY_MS);
2055
+ }
2056
+ return () => {
2057
+ if (speechTimeout.current) clearTimeout(speechTimeout.current);
2058
+ };
2059
+ }, [showVoiceBar, transcribedText]);
2060
+ const animOpacity = react.useRef(new reactNative.Animated.Value(1)).current;
2061
+ const animScale = react.useRef(new reactNative.Animated.Value(1)).current;
2062
+ react.useEffect(() => {
2063
+ reactNative.LayoutAnimation.configureNext(reactNative.LayoutAnimation.create(200, "easeInEaseOut", "opacity"));
2064
+ if (showVoiceBar) {
2065
+ animOpacity.setValue(0.92);
2066
+ animScale.setValue(0.98);
2067
+ reactNative.Animated.parallel([
2068
+ reactNative.Animated.timing(animOpacity, { toValue: 1, duration: 200, useNativeDriver: true }),
2069
+ reactNative.Animated.spring(animScale, { toValue: 1, useNativeDriver: true, speed: 12, bounciness: 4 })
2070
+ ]).start();
2071
+ } else {
2072
+ animOpacity.setValue(1);
2073
+ animScale.setValue(1);
2074
+ }
2075
+ }, [showVoiceBar, animOpacity, animScale]);
2076
+ const accentLight = accent + "33";
2077
+ const barBorderColor = accent + "59";
2078
+ const bottomPad = keyboardOpen ? 8 : Math.max(insetBottom, 10);
2079
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [s.outerBar, showVoiceBar && s.outerBarVoice, { paddingBottom: bottomPad }], children: [
2080
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: s.column, children: [
2081
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [s.inputWrap, showVoiceBar && s.inputWrapRelative], children: [
2082
+ /* @__PURE__ */ jsxRuntime.jsx(
2083
+ reactNative.TextInput,
2084
+ {
2085
+ value,
2086
+ onChangeText: onChange,
2087
+ onFocus,
2088
+ editable: !disabled,
2089
+ placeholder,
2090
+ placeholderTextColor: "#9CA3AF",
2091
+ multiline: true,
2092
+ style: [s.input, disabled && s.inputDisabled, showVoiceBar && s.inputHidden],
2093
+ returnKeyType: "default",
2094
+ blurOnSubmit: false,
2095
+ onSubmitEditing: onSend
2096
+ }
2097
+ ),
2098
+ showVoiceBar && /* @__PURE__ */ jsxRuntime.jsx(
2099
+ reactNative.Animated.View,
2100
+ {
2101
+ pointerEvents: "box-none",
2102
+ style: [s.voiceBarContainer, { opacity: animOpacity, transform: [{ scale: animScale }] }],
2103
+ children: /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [s.voiceBar, { backgroundColor: VOICE_BAR_BG, borderColor: barBorderColor }], children: [
2104
+ /* @__PURE__ */ jsxRuntime.jsx(
2105
+ reactNative.Pressable,
2106
+ {
2107
+ onPress: onCancelRecording,
2108
+ style: ({ pressed }) => [s.voiceBarCircle, { backgroundColor: accentLight }, pressed && s.pressed],
2109
+ accessibilityLabel: "Cancel recording",
2110
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReactNative.X, { size: 20, color: accent, strokeWidth: 2.5 })
2111
+ }
2112
+ ),
2113
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: s.voiceBarCenter, children: [
2114
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: s.voiceDots, children: Array.from({ length: VOICE_DOT_COUNT }).map((_, i) => /* @__PURE__ */ jsxRuntime.jsx(VoiceWaveformBar, { index: i, isActive: isSpeaking, accent }, i)) }),
2115
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [s.voiceTimer, { color: accent }], children: formatDuration(recordingSeconds) })
2116
+ ] }),
2117
+ /* @__PURE__ */ jsxRuntime.jsx(
2118
+ reactNative.Pressable,
2119
+ {
2120
+ onPress: onConfirmRecording,
2121
+ style: ({ pressed }) => [s.voiceBarCircle, s.voiceConfirm, { borderColor: accent }, pressed && s.pressed],
2122
+ accessibilityLabel: "Send recording",
2123
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReactNative.Check, { size: 22, color: accent, strokeWidth: 2.5 })
2124
+ }
2125
+ )
2126
+ ] })
2127
+ }
2128
+ )
2129
+ ] }),
2130
+ !showVoiceBar && /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: s.actionsRow, children: /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: s.rightActions, children: [
2131
+ showVoiceBtn && /* @__PURE__ */ jsxRuntime.jsx(
2132
+ reactNative.Pressable,
2133
+ {
2134
+ onPress: onVoicePress,
2135
+ disabled: voiceDisabled,
2136
+ style: [s.iconBtn, voiceDisabled && s.btnDisabled],
2137
+ accessibilityLabel: "Voice input",
2138
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: s.iconBtnInner, children: /* @__PURE__ */ jsxRuntime.jsx(lucideReactNative.Mic, { size: 16, color: "#6B7280", strokeWidth: 2 }) })
2139
+ }
2140
+ ),
2141
+ /* @__PURE__ */ jsxRuntime.jsx(
2142
+ reactNative.Pressable,
2143
+ {
2144
+ onPress: onSend,
2145
+ disabled: !canSend,
2146
+ style: [s.sendBtn, !canSend && s.btnDisabled],
2147
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [s.sendBtnInner, { backgroundColor: accent }], children: /* @__PURE__ */ jsxRuntime.jsx(lucideReactNative.Send, { size: 14, color: "#FFFFFF" }) })
2148
+ }
2149
+ )
2150
+ ] }) })
2151
+ ] }),
2152
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: s.disclaimerWrap, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: s.disclaimerText, children: AI_DISCLAIMER }) })
2153
+ ] });
2154
+ }
2155
+ var RESPONSE_SPEED = {
2156
+ normal: [2, 4],
2157
+ fast: 1,
2158
+ punctuation: [8, 12],
2159
+ newline: [4, 6],
2160
+ idle: 15
2161
+ };
2162
+ function charDelay(char, speed, multiplier) {
2163
+ const raw = (() => {
2164
+ if (char === "*") return speed.fast;
2165
+ if (char === "\n") return speed.newline[0] + Math.random() * speed.newline[1];
2166
+ if (".!?,;:".includes(char)) return speed.punctuation[0] + Math.random() * speed.punctuation[1];
2167
+ return speed.normal[0] + Math.random() * speed.normal[1];
2168
+ })();
2169
+ return raw / multiplier;
2170
+ }
2171
+ var TYPE_CHUNK = 2;
2172
+ function blockJumpLength(remaining, isStreaming) {
2173
+ if (remaining.startsWith("```")) {
2174
+ const closeIdx = remaining.indexOf("\n```", 3);
2175
+ if (closeIdx === -1) return 0;
2176
+ const afterFence = remaining.indexOf("\n", closeIdx + 1);
2177
+ return afterFence === -1 ? remaining.length : afterFence + 1;
2178
+ }
2179
+ const lines = remaining.split("\n");
2180
+ if (!/^\s*\|/.test(lines[0])) return 0;
2181
+ let consumed = 0;
2182
+ let tableLines = 0;
2183
+ for (let i = 0; i < lines.length; i++) {
2184
+ if (/^\s*\|/.test(lines[i])) {
2185
+ tableLines++;
2186
+ consumed += lines[i].length + 1;
2187
+ } else {
2188
+ break;
2189
+ }
2190
+ }
2191
+ if (tableLines < 2) return 0;
2192
+ if (consumed >= remaining.length && isStreaming) return 0;
2193
+ return Math.min(consumed, remaining.length);
2194
+ }
2195
+ function useTypingEffect(targetText, enabled, isStreaming, speedMultiplier = 6) {
2196
+ const instant = speedMultiplier === 0;
2197
+ const multiplier = instant ? 1 : Math.max(speedMultiplier, 0.1);
2198
+ const [displayedText, setDisplayedText] = react.useState("");
2199
+ const displayedRef = react.useRef("");
2200
+ const targetRef = react.useRef(targetText);
2201
+ const enabledRef = react.useRef(enabled);
2202
+ const streamingRef = react.useRef(isStreaming);
2203
+ const timerRef = react.useRef(null);
2204
+ const runningRef = react.useRef(false);
2205
+ targetRef.current = targetText;
2206
+ enabledRef.current = enabled;
2207
+ streamingRef.current = isStreaming;
2208
+ react.useEffect(() => {
2209
+ if (!enabled || instant) {
2210
+ if (timerRef.current) {
2211
+ clearTimeout(timerRef.current);
2212
+ timerRef.current = null;
2213
+ }
2214
+ runningRef.current = false;
2215
+ displayedRef.current = targetText;
2216
+ setDisplayedText(targetText);
2217
+ return;
2218
+ }
2219
+ if (displayedRef.current && !targetRef.current.startsWith(displayedRef.current)) {
2220
+ displayedRef.current = targetRef.current;
2221
+ setDisplayedText(targetRef.current);
2222
+ }
2223
+ if (runningRef.current) return;
2224
+ runningRef.current = true;
2225
+ const tick = () => {
2226
+ if (!enabledRef.current) {
2227
+ runningRef.current = false;
2228
+ return;
2229
+ }
2230
+ if (displayedRef.current && !targetRef.current.startsWith(displayedRef.current)) {
2231
+ displayedRef.current = targetRef.current;
2232
+ setDisplayedText(targetRef.current);
2233
+ timerRef.current = setTimeout(tick, RESPONSE_SPEED.idle / multiplier);
2234
+ return;
2235
+ }
2236
+ if (displayedRef.current.length < targetRef.current.length) {
2237
+ const remaining = targetRef.current.slice(displayedRef.current.length);
2238
+ const atLineStart = displayedRef.current.length === 0 || targetRef.current[displayedRef.current.length - 1] === "\n";
2239
+ if (atLineStart) {
2240
+ const jump = blockJumpLength(remaining, streamingRef.current);
2241
+ if (jump > 0) {
2242
+ displayedRef.current += remaining.slice(0, jump);
2243
+ setDisplayedText(displayedRef.current);
2244
+ timerRef.current = setTimeout(tick, 0);
2245
+ return;
2246
+ }
2247
+ if (streamingRef.current && /^(```|\s*\|)/.test(remaining)) {
2248
+ timerRef.current = setTimeout(tick, RESPONSE_SPEED.idle / multiplier);
2249
+ return;
2250
+ }
2251
+ }
2252
+ let chunk = remaining.slice(0, TYPE_CHUNK);
2253
+ const nl = chunk.indexOf("\n");
2254
+ if (nl !== -1) chunk = chunk.slice(0, nl + 1);
2255
+ displayedRef.current += chunk;
2256
+ setDisplayedText(displayedRef.current);
2257
+ const lastChar = chunk[chunk.length - 1];
2258
+ timerRef.current = setTimeout(tick, charDelay(lastChar, RESPONSE_SPEED, multiplier));
2259
+ } else {
2260
+ timerRef.current = setTimeout(tick, RESPONSE_SPEED.idle / multiplier);
2261
+ }
2262
+ };
2263
+ tick();
2264
+ return () => {
2265
+ if (timerRef.current) {
2266
+ clearTimeout(timerRef.current);
2267
+ timerRef.current = null;
2268
+ }
2269
+ runningRef.current = false;
2270
+ };
2271
+ }, [enabled, instant]);
2272
+ const isTyping = enabled && !instant && displayedRef.current.length < targetRef.current.length;
2273
+ return { displayedText: enabled || instant ? displayedText : targetText, isTyping };
2274
+ }
2275
+ var PENDING_LABEL = "Thinking";
2276
+ var SHIMMER_DURATION = 2800;
2277
+ var SHIMMER_BAND = 0.55;
2278
+ var SHIMMER_LIGHT = { dim: "#b4b4b4", bright: "#1f2937" };
2279
+ var SHIMMER_DARK = { dim: "#5b6b6e", bright: "#eaf2f3" };
2280
+ function ShimmerChar({
2281
+ ch,
2282
+ pos,
2283
+ progress,
2284
+ dim,
2285
+ bright
2286
+ }) {
2287
+ const center = (pos + 0.4) / 1.8;
2288
+ const half = SHIMMER_BAND / 1.8;
2289
+ const color = progress.interpolate({
2290
+ inputRange: [center - half, center, center + half],
2291
+ outputRange: [dim, bright, dim],
2292
+ extrapolate: "clamp"
2293
+ });
2294
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.Animated.Text, { style: [ts.cursorLabel, { color }], children: ch });
2295
+ }
2296
+ function ShimmerText({ text, isDark }) {
2297
+ const progress = react.useRef(new reactNative.Animated.Value(0)).current;
2298
+ const { dim, bright } = isDark ? SHIMMER_DARK : SHIMMER_LIGHT;
2299
+ react.useEffect(() => {
2300
+ const loop = reactNative.Animated.loop(
2301
+ reactNative.Animated.timing(progress, {
2302
+ toValue: 1,
2303
+ duration: SHIMMER_DURATION,
2304
+ easing: reactNative.Easing.inOut(reactNative.Easing.ease),
2305
+ // color interpolation isn't supported by the native driver
2306
+ useNativeDriver: false
2307
+ })
2308
+ );
2309
+ loop.start();
2310
+ return () => loop.stop();
2311
+ }, [progress]);
2312
+ const chars = Array.from(text);
2313
+ const n = Math.max(chars.length - 1, 1);
2314
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [ts.cursorLabel, { color: dim }], children: chars.map((ch, i) => /* @__PURE__ */ jsxRuntime.jsx(ShimmerChar, { ch, pos: i / n, progress, dim, bright }, i)) });
2315
+ }
2316
+ function FlipLabel({ label, isDark }) {
2317
+ const opacity = react.useRef(new reactNative.Animated.Value(0)).current;
2318
+ const translateY = react.useRef(new reactNative.Animated.Value(6)).current;
2319
+ react.useEffect(() => {
2320
+ opacity.setValue(0);
2321
+ translateY.setValue(6);
2322
+ reactNative.Animated.parallel([
2323
+ reactNative.Animated.timing(opacity, { toValue: 1, duration: 300, easing: reactNative.Easing.out(reactNative.Easing.ease), useNativeDriver: true }),
2324
+ reactNative.Animated.timing(translateY, { toValue: 0, duration: 320, easing: reactNative.Easing.out(reactNative.Easing.ease), useNativeDriver: true })
2325
+ ]).start();
2326
+ }, [label, opacity, translateY]);
2327
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.Animated.View, { style: { opacity, transform: [{ translateY }] }, children: /* @__PURE__ */ jsxRuntime.jsx(ShimmerText, { text: label, isDark }) });
2328
+ }
2329
+ function StickyLabel({ label, isDark }) {
2330
+ const opacity = react.useRef(new reactNative.Animated.Value(0)).current;
2331
+ const translateY = react.useRef(new reactNative.Animated.Value(4)).current;
2332
+ react.useEffect(() => {
2333
+ reactNative.Animated.parallel([
2334
+ reactNative.Animated.timing(opacity, { toValue: 1, duration: 400, easing: reactNative.Easing.out(reactNative.Easing.ease), useNativeDriver: true }),
2335
+ reactNative.Animated.timing(translateY, { toValue: 0, duration: 400, easing: reactNative.Easing.out(reactNative.Easing.ease), useNativeDriver: true })
2336
+ ]).start();
2337
+ }, [opacity, translateY]);
2338
+ return /* @__PURE__ */ jsxRuntime.jsx(
2339
+ reactNative.Animated.Text,
2340
+ {
2341
+ style: [
2342
+ ts.stickyLabel,
2343
+ { color: isDark ? "rgba(255,255,255,0.62)" : "#4b5563", opacity, transform: [{ translateY }] }
2344
+ ],
2345
+ children: label
2346
+ }
2347
+ );
2348
+ }
2349
+ function ThinkingBlock({
2350
+ stickyLabel,
2351
+ currentStepLabel,
2352
+ isDark
2353
+ }) {
2354
+ const label = currentStepLabel || PENDING_LABEL;
2355
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: ts.container, children: [
2356
+ stickyLabel ? /* @__PURE__ */ jsxRuntime.jsx(StickyLabel, { label: stickyLabel, isDark }, stickyLabel) : null,
2357
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: ts.cursorRow, children: /* @__PURE__ */ jsxRuntime.jsx(FlipLabel, { label, isDark }, label) })
2358
+ ] });
2359
+ }
2360
+ function deriveStickyLabel(steps) {
2361
+ if (!steps || steps.length === 0) return void 0;
2362
+ for (let i = steps.length - 1; i >= 0; i--) {
2363
+ const s2 = steps[i];
2364
+ if (s2.eventType === "STAGE_STARTED" && s2.stage === "analyzer" && s2.status === "completed" && s2.message) {
2365
+ return s2.message;
2366
+ }
2367
+ }
2368
+ return void 0;
2369
+ }
2370
+ function deriveCurrentStepLabel(steps, stickyLabel) {
2371
+ if (!steps || steps.length === 0) return void 0;
2372
+ for (let i = steps.length - 1; i >= 0; i--) {
2373
+ const s2 = steps[i];
2374
+ if (s2.status === "in_progress" && s2.message) {
2375
+ if (s2.stage === "analyzer" && s2.message === stickyLabel) continue;
2376
+ if (s2.message === stickyLabel) continue;
2377
+ return s2.message;
2378
+ }
2379
+ }
2380
+ for (let i = steps.length - 1; i >= 0; i--) {
2381
+ const s2 = steps[i];
2382
+ if (s2.message && s2.message !== stickyLabel) return s2.message;
2383
+ }
2384
+ return void 0;
2385
+ }
2386
+ var ts = reactNative.StyleSheet.create({
2387
+ container: { paddingVertical: 2 },
2388
+ stickyLabel: {
2389
+ fontSize: 14,
2390
+ lineHeight: 21,
2391
+ color: "#4b5563",
2392
+ marginBottom: 6
2393
+ },
2394
+ cursorRow: { overflow: "hidden" },
2395
+ cursorLabel: {
2396
+ fontSize: 14,
2397
+ lineHeight: 21,
2398
+ fontWeight: "500"
2399
+ }
2400
+ });
2401
+ function UserBubble({ message, accent }) {
2402
+ const text = message.content ?? "";
2403
+ if (!text) return null;
2404
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [sd.bubbleRow, sd.rowUser], children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [sd.bubble, sd.bubbleUser, { backgroundColor: accent }], children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [sd.bubbleText, sd.textUser], children: text }) }) });
2405
+ }
2406
+ function stripIncompleteImageToken(text) {
2407
+ const lastBang = text.lastIndexOf("![");
2408
+ if (lastBang === -1) return text;
2409
+ const after = text.slice(lastBang);
2410
+ if (/^!\[[^\]]*\]\([^)]*\)/.test(after)) return text;
2411
+ return text.slice(0, lastBang);
2412
+ }
2413
+ function stripDuplicatePromptText(text, promptText) {
2414
+ if (!promptText) return text;
2415
+ const pm = promptText.replace(/\\n/g, "\n").trim();
2416
+ if (!pm) return text;
2417
+ const idx = text.indexOf(pm);
2418
+ if (idx !== -1) {
2419
+ return (text.slice(0, idx) + text.slice(idx + pm.length)).trim();
2420
+ }
2421
+ for (let cut = Math.min(text.length, pm.length); cut >= 24; cut--) {
2422
+ if (text.endsWith(pm.slice(0, cut))) {
2423
+ return text.slice(0, text.length - cut).trim();
2424
+ }
2425
+ }
2426
+ return text;
2427
+ }
2428
+ function AssistantBubble({
2429
+ message,
2430
+ shouldType,
2431
+ isDark,
2432
+ accent,
2433
+ suppressText
2434
+ }) {
2435
+ const isCurrentlyStreaming = !!message.isStreaming && !message.isCancelled;
2436
+ const mdStyles = isDark ? MD_STYLES_DARK : MD_STYLES_LIGHT;
2437
+ const mdRules = react.useMemo(() => makeMdRules(isDark, accent), [isDark, accent]);
2438
+ const bubbleBg = isDark ? "rgba(255,255,255,0.07)" : "#f3f4f6";
2439
+ const errorBg = isDark ? "rgba(220,38,38,0.14)" : "#fef2f2";
2440
+ const rawResponseContent = (() => {
2441
+ const raw = message.isStreaming ? message.streamingContent || message.content : message.content;
2442
+ if (!raw) return "";
2443
+ const normalized = raw.replace(/\\n/g, "\n");
2444
+ const cleaned = message.isStreaming ? stripIncompleteImageToken(normalized) : normalized;
2445
+ return stripDuplicatePromptText(cleaned, suppressText);
2446
+ })();
2447
+ const isThinkingStreaming = isCurrentlyStreaming && !rawResponseContent && !message.isError;
2448
+ const hasEverStreamed = react.useRef(!!message.isStreaming);
2449
+ if (message.isStreaming) hasEverStreamed.current = true;
2450
+ const typingEnabled = shouldType && hasEverStreamed.current && Boolean(rawResponseContent) && !message.isError;
2451
+ const { displayedText, isTyping } = useTypingEffect(
2452
+ rawResponseContent,
2453
+ typingEnabled,
2454
+ isCurrentlyStreaming,
2455
+ 3
2456
+ );
2457
+ const stickyLabel = deriveStickyLabel(message.steps);
2458
+ const currentStepLabel = deriveCurrentStepLabel(message.steps, stickyLabel);
2459
+ if (message.isError && !rawResponseContent && !displayedText) {
2460
+ const detail = message.errorDetails || "Something went wrong. Please try again.";
2461
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [sd.bubbleRow, sd.rowAssistant], children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [sd.bubble, sd.bubbleAssistant, { backgroundColor: errorBg }], children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: sd.errorText, children: detail }) }) });
2462
+ }
2463
+ if (isThinkingStreaming) {
2464
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [sd.bubbleRow, sd.rowAssistant], children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [sd.bubble, sd.bubbleAssistant, { backgroundColor: bubbleBg }], children: /* @__PURE__ */ jsxRuntime.jsx(ThinkingBlock, { stickyLabel, currentStepLabel, isDark }) }) });
2465
+ }
2466
+ const showCursor = (isCurrentlyStreaming || isTyping) && !message.isCancelled;
2467
+ const markdownContent = showCursor ? displayedText + "\u258B" : displayedText;
2468
+ if (!markdownContent) return null;
2469
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [sd.bubbleRow, sd.rowAssistant], children: /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [sd.bubble, sd.bubbleAssistant, sd.bubbleAssistantWide, { backgroundColor: bubbleBg }], children: [
2470
+ /* @__PURE__ */ jsxRuntime.jsx(Markdown__default.default, { style: mdStyles, rules: mdRules, children: markdownContent }),
2471
+ message.isCancelled && /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: sd.cancelledText, children: "Response stopped" }),
2472
+ message.isError && displayedText && /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: sd.partialErrorText, children: message.errorDetails || "The response was interrupted." })
2473
+ ] }) });
2474
+ }
2475
+ function MessageBubble({ message, accent, shouldType, isDark, suppressText }) {
2476
+ if (message.role === "user") return /* @__PURE__ */ jsxRuntime.jsx(UserBubble, { message, accent });
2477
+ return /* @__PURE__ */ jsxRuntime.jsx(AssistantBubble, { message, shouldType, isDark, accent, suppressText });
2478
+ }
2479
+ var RESEND_COOLDOWN_S = 30;
2480
+ var DEFAULT_CODE_LEN = 6;
2481
+ function uaPalette(isDark) {
2482
+ return isDark ? {
2483
+ sheetBg: "#13201f",
2484
+ text: "rgba(255,255,255,0.95)",
2485
+ muted: "rgba(255,255,255,0.55)",
2486
+ border: "rgba(255,255,255,0.10)",
2487
+ fieldBg: "rgba(255,255,255,0.06)",
2488
+ fieldBorder: "rgba(255,255,255,0.18)",
2489
+ overlay: "rgba(0,0,0,0.62)",
2490
+ grabber: "rgba(255,255,255,0.22)"
2491
+ } : {
2492
+ sheetBg: "#ffffff",
2493
+ text: "#111827",
2494
+ muted: "#6b7280",
2495
+ border: "#eceef1",
2496
+ fieldBg: "#f9fafb",
2497
+ fieldBorder: "#d1d5db",
2498
+ overlay: "rgba(15,23,42,0.45)",
2499
+ grabber: "rgba(15,23,42,0.18)"
2500
+ };
2501
+ }
2502
+ function promptInitialSeconds(prompt) {
2503
+ return prompt && typeof prompt.expirySeconds === "number" && prompt.expirySeconds > 0 ? Math.floor(prompt.expirySeconds) : void 0;
2504
+ }
2505
+ function promptTimerKey(prompt) {
2506
+ return prompt ? `${prompt.userActionId}|${prompt.subAction ?? ""}` : "";
2507
+ }
2508
+ function useExpiredFlag(prompt) {
2509
+ const initial = promptInitialSeconds(prompt);
2510
+ const key = promptTimerKey(prompt);
2511
+ const [expired, setExpired] = react.useState(false);
2512
+ react.useEffect(() => {
2513
+ setExpired(false);
2514
+ if (initial === void 0) return;
2515
+ const t = setTimeout(() => setExpired(true), initial * 1e3);
2516
+ return () => clearTimeout(t);
2517
+ }, [key, initial]);
2518
+ return expired;
2519
+ }
2520
+ function useSecondsLeft(expirySeconds, restartKey) {
2521
+ const initial = typeof expirySeconds === "number" && expirySeconds > 0 ? Math.floor(expirySeconds) : void 0;
2522
+ const [left, setLeft] = react.useState(initial);
2523
+ react.useEffect(() => {
2524
+ setLeft(initial);
2525
+ if (initial === void 0) return;
2526
+ const id = setInterval(() => {
2527
+ setLeft((s2) => s2 === void 0 ? s2 : s2 <= 1 ? 0 : s2 - 1);
2528
+ }, 1e3);
2529
+ return () => clearInterval(id);
2530
+ }, [restartKey, initial]);
2531
+ return left;
2532
+ }
2533
+ function SheetTimerPill({
2534
+ prompt,
2535
+ accent,
2536
+ isDark
2537
+ }) {
2538
+ const left = useSecondsLeft(prompt.expirySeconds, promptTimerKey(prompt));
2539
+ if (left === void 0 || prompt.status === "stale") return null;
2540
+ const expired = left <= 0;
2541
+ return /* @__PURE__ */ jsxRuntime.jsx(
2542
+ reactNative.View,
2543
+ {
2544
+ style: [
2545
+ sht.timer,
2546
+ {
2547
+ borderColor: expired ? "#ef4444" : accent + "40",
2548
+ backgroundColor: expired ? "rgba(239,68,68,0.10)" : accent + (isDark ? "1F" : "12")
2549
+ }
2550
+ ],
2551
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: { color: expired ? "#ef4444" : accent, fontSize: 12.5, fontWeight: "700", letterSpacing: 0.2 }, children: expired ? "Expired" : `${left}s` })
2552
+ }
2553
+ );
2554
+ }
2555
+ function CardTimerPill({
2556
+ prompt,
2557
+ accent,
2558
+ isDark
2559
+ }) {
2560
+ const left = useSecondsLeft(prompt.expirySeconds, promptTimerKey(prompt));
2561
+ if (left === void 0 || left <= 0) return null;
2562
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [rc.timer, { backgroundColor: accent + (isDark ? "26" : "14") }], children: /* @__PURE__ */ jsxRuntime.jsxs(reactNative.Text, { style: { color: accent, fontSize: 12, fontWeight: "700" }, children: [
2563
+ left,
2564
+ "s"
2565
+ ] }) });
2566
+ }
2567
+ function CodeInput({
2568
+ value,
2569
+ onChange,
2570
+ length,
2571
+ disabled,
2572
+ error,
2573
+ accent,
2574
+ pal
2575
+ }) {
2576
+ const ref = react.useRef(null);
2577
+ react.useEffect(() => {
2578
+ const t = setTimeout(() => ref.current?.focus(), 380);
2579
+ return () => clearTimeout(t);
2580
+ }, []);
2581
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.Pressable, { onPress: () => ref.current?.focus(), style: ub.codeRow, children: [
2582
+ Array.from({ length }).map((_, i) => {
2583
+ const ch = value[i] ?? "";
2584
+ const isCursor = i === value.length && !disabled;
2585
+ const bc = error ? "#ef4444" : isCursor ? accent : pal.fieldBorder;
2586
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [ub.codeBox, { borderColor: bc, backgroundColor: pal.fieldBg }], children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [ub.codeChar, { color: pal.text }], children: ch }) }, i);
2587
+ }),
2588
+ /* @__PURE__ */ jsxRuntime.jsx(
2589
+ reactNative.TextInput,
2590
+ {
2591
+ ref,
2592
+ value,
2593
+ onChangeText: (t) => onChange(t.replace(/[^0-9]/g, "").slice(0, length)),
2594
+ keyboardType: "number-pad",
2595
+ maxLength: length,
2596
+ editable: !disabled,
2597
+ caretHidden: true,
2598
+ style: ub.codeHidden
2599
+ }
2600
+ )
2601
+ ] });
2602
+ }
2603
+ function PrimaryButton({
2604
+ label,
2605
+ onPress,
2606
+ disabled,
2607
+ accent
2608
+ }) {
2609
+ const bg = accent || DEFAULT_ACCENT;
2610
+ return /* @__PURE__ */ jsxRuntime.jsx(
2611
+ reactNative.Pressable,
2612
+ {
2613
+ onPress,
2614
+ disabled,
2615
+ android_ripple: { color: "rgba(255,255,255,0.22)" },
2616
+ style: [ub.primaryBtn, { backgroundColor: bg, borderColor: bg, opacity: disabled ? 0.5 : 1 }],
2617
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: ub.primaryBtnText, children: label })
2618
+ }
2619
+ );
2620
+ }
2621
+ function VerificationBody({
2622
+ prompt,
2623
+ expired,
2624
+ pal,
2625
+ accent,
2626
+ onSubmit,
2627
+ onCancel,
2628
+ onResend
2629
+ }) {
2630
+ const isNumeric = prompt.verificationType !== "ALPHANUMERIC_CODE";
2631
+ const field = prompt.requestedSchema?.properties?.verificationCode;
2632
+ const codeLen = (typeof field?.maxLength === "number" ? field.maxLength : void 0) ?? (typeof field?.minLength === "number" ? field.minLength : void 0) ?? DEFAULT_CODE_LEN;
2633
+ const [code, setCode] = react.useState("");
2634
+ const [errored, setErrored] = react.useState(false);
2635
+ const [resendSec, setResendSec] = react.useState(0);
2636
+ const lastSubmitted = react.useRef(null);
2637
+ const resendTimer = react.useRef(null);
2638
+ const busy = prompt.status === "submitting";
2639
+ const stale = prompt.status === "stale";
2640
+ const locked = busy || stale || expired;
2641
+ react.useEffect(() => {
2642
+ if (prompt.subAction === "SubmissionInvalid") {
2643
+ setErrored(true);
2644
+ setCode("");
2645
+ lastSubmitted.current = null;
2646
+ }
2647
+ }, [prompt.subAction, prompt.userActionId]);
2648
+ const doSubmit = react.useCallback(
2649
+ (value) => {
2650
+ if (locked || !value) return;
2651
+ if (lastSubmitted.current === value) return;
2652
+ lastSubmitted.current = value;
2653
+ void onSubmit(prompt.userActionId, { verificationCode: value }).catch(() => {
2654
+ lastSubmitted.current = null;
2655
+ setErrored(true);
2656
+ });
2657
+ },
2658
+ [locked, onSubmit, prompt.userActionId]
2659
+ );
2660
+ react.useEffect(() => {
2661
+ if (!isNumeric || locked) return;
2662
+ if (code.length === codeLen && /^\d+$/.test(code)) doSubmit(code);
2663
+ }, [code, codeLen, doSubmit, isNumeric, locked]);
2664
+ react.useEffect(() => () => {
2665
+ if (resendTimer.current) clearInterval(resendTimer.current);
2666
+ }, []);
2667
+ const handleResend = react.useCallback(async () => {
2668
+ if (locked || resendSec > 0) return;
2669
+ setErrored(false);
2670
+ setCode("");
2671
+ lastSubmitted.current = null;
2672
+ try {
2673
+ await onResend(prompt.userActionId);
2674
+ setResendSec(RESEND_COOLDOWN_S);
2675
+ if (resendTimer.current) clearInterval(resendTimer.current);
2676
+ resendTimer.current = setInterval(() => {
2677
+ setResendSec((s2) => {
2678
+ if (s2 <= 1) {
2679
+ if (resendTimer.current) clearInterval(resendTimer.current);
2680
+ return 0;
2681
+ }
2682
+ return s2 - 1;
2683
+ });
2684
+ }, 1e3);
2685
+ } catch {
2686
+ }
2687
+ }, [locked, onResend, prompt.userActionId, resendSec]);
2688
+ const description = prompt.message?.trim() || (isNumeric ? `Enter the ${codeLen}-digit code to continue` : "Enter the verification code to continue");
2689
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: ub.bodyWrap, children: [
2690
+ /* @__PURE__ */ jsxRuntime.jsxs(
2691
+ reactNative.ScrollView,
2692
+ {
2693
+ style: ub.fieldsScroll,
2694
+ contentContainerStyle: ub.fieldsContent,
2695
+ keyboardShouldPersistTaps: "handled",
2696
+ showsVerticalScrollIndicator: false,
2697
+ children: [
2698
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [ub.desc, { color: pal.muted }], children: description }),
2699
+ isNumeric ? /* @__PURE__ */ jsxRuntime.jsx(
2700
+ CodeInput,
2701
+ {
2702
+ value: code,
2703
+ onChange: (v) => {
2704
+ setErrored(false);
2705
+ setCode(v);
2706
+ },
2707
+ length: codeLen,
2708
+ disabled: locked,
2709
+ error: errored,
2710
+ accent,
2711
+ pal
2712
+ }
2713
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
2714
+ reactNative.TextInput,
2715
+ {
2716
+ value: code,
2717
+ onChangeText: (t) => {
2718
+ setErrored(false);
2719
+ setCode(t);
2720
+ },
2721
+ editable: !locked,
2722
+ placeholder: "Verification code",
2723
+ placeholderTextColor: pal.muted,
2724
+ autoComplete: "one-time-code",
2725
+ style: [
2726
+ ub.input,
2727
+ { color: pal.text, backgroundColor: pal.fieldBg, borderColor: errored ? "#ef4444" : pal.fieldBorder }
2728
+ ]
2729
+ }
2730
+ )
2731
+ ]
2732
+ }
2733
+ ),
2734
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: ub.actions, children: [
2735
+ !isNumeric && /* @__PURE__ */ jsxRuntime.jsx(
2736
+ PrimaryButton,
2737
+ {
2738
+ label: busy ? "Verifying\u2026" : "Verify",
2739
+ onPress: () => doSubmit(code.trim()),
2740
+ disabled: locked || !code.trim(),
2741
+ accent
2742
+ }
2743
+ ),
2744
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: ub.linkRow, children: [
2745
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { disabled: locked || resendSec > 0, onPress: () => void handleResend(), children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [ub.link, { color: accent, opacity: locked || resendSec > 0 ? 0.4 : 1 }], children: resendSec > 0 ? `Resend (${resendSec}s)` : "Resend" }) }),
2746
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { disabled: busy, onPress: () => void onCancel(prompt.userActionId), children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [ub.link, ub.linkDanger, { opacity: busy ? 0.4 : 1 }], children: "Cancel" }) })
2747
+ ] })
2748
+ ] })
2749
+ ] });
2750
+ }
2751
+ function FormBody({
2752
+ prompt,
2753
+ pal,
2754
+ accent,
2755
+ isDark,
2756
+ onSubmit,
2757
+ onCancel
2758
+ }) {
2759
+ const schema = prompt.requestedSchema;
2760
+ const fields = react.useMemo(() => renderableFields(schema), [schema]);
2761
+ const [values, setValues] = react.useState(() => {
2762
+ const init = {};
2763
+ for (const [key, field] of fields) init[key] = defaultValueFor(field);
2764
+ return init;
2765
+ });
2766
+ const [errors, setErrors] = react.useState({});
2767
+ const busy = prompt.status === "submitting";
2768
+ const stale = prompt.status === "stale";
2769
+ const locked = busy || stale;
2770
+ const isConfirm = prompt.subAction === "UserConfirmation";
2771
+ const setValue = (key, v) => {
2772
+ setValues((prev) => ({ ...prev, [key]: v }));
2773
+ setErrors((prev) => {
2774
+ if (!prev[key]) return prev;
2775
+ const next = { ...prev };
2776
+ delete next[key];
2777
+ return next;
2778
+ });
2779
+ };
2780
+ const handleSubmit = () => {
2781
+ if (locked) return;
2782
+ const validation = validateForm(schema, values);
2783
+ if (Object.keys(validation).length > 0) {
2784
+ setErrors(validation);
2785
+ return;
2786
+ }
2787
+ void onSubmit(prompt.userActionId, buildContent(schema, values)).catch(() => {
2788
+ });
2789
+ };
2790
+ const mdStyles = isDark ? MD_STYLES_DARK : MD_STYLES_LIGHT;
2791
+ const mdRules = react.useMemo(() => makeMdRules(isDark, accent), [isDark, accent]);
2792
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: ub.bodyWrap, children: [
2793
+ /* @__PURE__ */ jsxRuntime.jsxs(
2794
+ reactNative.ScrollView,
2795
+ {
2796
+ style: ub.fieldsScroll,
2797
+ contentContainerStyle: ub.fieldsContent,
2798
+ keyboardShouldPersistTaps: "handled",
2799
+ showsVerticalScrollIndicator: true,
2800
+ children: [
2801
+ prompt.message?.trim() ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: ub.messageBlock, children: /* @__PURE__ */ jsxRuntime.jsx(Markdown__default.default, { style: mdStyles, rules: mdRules, children: prompt.message.replace(/\\n/g, "\n") }) }) : null,
2802
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: ub.form, children: fields.map(([key, field]) => {
2803
+ const widget = classifyField(field);
2804
+ const label = field.title || key;
2805
+ const required = isRequired(schema, key);
2806
+ const err = errors[key];
2807
+ if (widget === "boolean") {
2808
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: ub.switchRow, children: [
2809
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.Text, { style: [ub.label, { color: pal.text, flex: 1 }], children: [
2810
+ label,
2811
+ required ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: { color: "#ef4444" }, children: " *" }) : null
2812
+ ] }),
2813
+ /* @__PURE__ */ jsxRuntime.jsx(
2814
+ reactNative.Switch,
2815
+ {
2816
+ value: Boolean(values[key]),
2817
+ disabled: locked,
2818
+ onValueChange: (v) => setValue(key, v),
2819
+ trackColor: { true: accent }
2820
+ }
2821
+ )
2822
+ ] }, key);
2823
+ }
2824
+ return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: ub.field, children: [
2825
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.Text, { style: [ub.label, { color: pal.text }], children: [
2826
+ label,
2827
+ required ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: { color: "#ef4444" }, children: " *" }) : null
2828
+ ] }),
2829
+ field.description ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [ub.hint, { color: pal.muted }], children: field.description }) : null,
2830
+ widget === "select" ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: ub.optionList, children: getOptions(field).map((opt) => {
2831
+ const selected = String(values[key] ?? "") === opt.const;
2832
+ return /* @__PURE__ */ jsxRuntime.jsxs(
2833
+ reactNative.Pressable,
2834
+ {
2835
+ disabled: locked,
2836
+ onPress: () => setValue(key, opt.const),
2837
+ android_ripple: { color: accent + "22" },
2838
+ style: [
2839
+ ub.optionRow,
2840
+ {
2841
+ borderColor: selected ? accent : pal.fieldBorder,
2842
+ backgroundColor: selected ? accent + (isDark ? "26" : "14") : pal.fieldBg
2843
+ }
2844
+ ],
2845
+ children: [
2846
+ /* @__PURE__ */ jsxRuntime.jsx(
2847
+ reactNative.View,
2848
+ {
2849
+ style: [
2850
+ ub.radio,
2851
+ { borderColor: selected ? accent : pal.fieldBorder }
2852
+ ],
2853
+ children: selected ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [ub.radioDot, { backgroundColor: accent }] }) : null
2854
+ }
2855
+ ),
2856
+ /* @__PURE__ */ jsxRuntime.jsx(
2857
+ reactNative.Text,
2858
+ {
2859
+ style: [
2860
+ ub.optionLabel,
2861
+ { color: pal.text, fontWeight: selected ? "600" : "500" }
2862
+ ],
2863
+ children: opt.title || opt.const
2864
+ }
2865
+ )
2866
+ ]
2867
+ },
2868
+ opt.const
2869
+ );
2870
+ }) }) : /* @__PURE__ */ jsxRuntime.jsx(
2871
+ reactNative.TextInput,
2872
+ {
2873
+ value: String(values[key] ?? ""),
2874
+ editable: !locked,
2875
+ onChangeText: (t) => setValue(key, t),
2876
+ placeholder: field.description ? void 0 : label,
2877
+ placeholderTextColor: pal.muted,
2878
+ keyboardType: widget === "integer" ? "number-pad" : widget === "decimal" ? "decimal-pad" : "default",
2879
+ style: [
2880
+ ub.input,
2881
+ { color: pal.text, backgroundColor: pal.fieldBg, borderColor: err ? "#ef4444" : pal.fieldBorder }
2882
+ ]
2883
+ }
2884
+ ),
2885
+ err ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: ub.error, children: err }) : null
2886
+ ] }, key);
2887
+ }) })
2888
+ ]
2889
+ }
2890
+ ),
2891
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: ub.actions, children: [
2892
+ /* @__PURE__ */ jsxRuntime.jsx(
2893
+ PrimaryButton,
2894
+ {
2895
+ label: busy ? "Submitting\u2026" : isConfirm ? "Confirm" : "Next",
2896
+ onPress: handleSubmit,
2897
+ disabled: locked,
2898
+ accent
2899
+ }
2900
+ ),
2901
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: ub.linkRow, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { disabled: busy, onPress: () => void onCancel(prompt.userActionId), children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [ub.link, ub.linkDanger, { opacity: busy ? 0.4 : 1 }], children: "Cancel" }) }) })
2902
+ ] })
2903
+ ] });
2904
+ }
2905
+ function UserActionSheet({
2906
+ open,
2907
+ active,
2908
+ expired,
2909
+ isDark,
2910
+ accent,
2911
+ insetBottom,
2912
+ onSubmit,
2913
+ onCancel,
2914
+ onResend,
2915
+ onMinimize
2916
+ }) {
2917
+ const pal = uaPalette(isDark);
2918
+ const screenH = reactNative.Dimensions.get("window").height;
2919
+ const [shown, setShown] = react.useState(active);
2920
+ react.useEffect(() => {
2921
+ if (active) setShown(active);
2922
+ }, [active]);
2923
+ const translateY = react.useRef(new reactNative.Animated.Value(screenH)).current;
2924
+ const backdrop = react.useRef(new reactNative.Animated.Value(0)).current;
2925
+ const minimizeRef = react.useRef(onMinimize);
2926
+ react.useEffect(() => {
2927
+ minimizeRef.current = onMinimize;
2928
+ });
2929
+ const [mounted, setMounted] = react.useState(open);
2930
+ const [kbHeight, setKbHeight] = react.useState(0);
2931
+ react.useEffect(() => {
2932
+ const showEvt = reactNative.Platform.OS === "ios" ? "keyboardWillShow" : "keyboardDidShow";
2933
+ const hideEvt = reactNative.Platform.OS === "ios" ? "keyboardWillHide" : "keyboardDidHide";
2934
+ const s1 = reactNative.Keyboard.addListener(showEvt, (e) => setKbHeight(e?.endCoordinates?.height ?? 0));
2935
+ const s2 = reactNative.Keyboard.addListener(hideEvt, () => setKbHeight(0));
2936
+ return () => {
2937
+ s1.remove();
2938
+ s2.remove();
2939
+ };
2940
+ }, []);
2941
+ react.useEffect(() => {
2942
+ if (open) {
2943
+ setMounted(true);
2944
+ translateY.setValue(screenH);
2945
+ backdrop.setValue(0);
2946
+ const id = requestAnimationFrame(() => {
2947
+ reactNative.Animated.parallel([
2948
+ reactNative.Animated.spring(translateY, { toValue: 0, useNativeDriver: true, damping: 30, stiffness: 360, mass: 0.85 }),
2949
+ reactNative.Animated.timing(backdrop, { toValue: 1, duration: 190, useNativeDriver: true })
2950
+ ]).start();
2951
+ });
2952
+ return () => cancelAnimationFrame(id);
2953
+ }
2954
+ reactNative.Animated.parallel([
2955
+ reactNative.Animated.timing(translateY, { toValue: screenH, duration: 230, easing: reactNative.Easing.in(reactNative.Easing.cubic), useNativeDriver: true }),
2956
+ reactNative.Animated.timing(backdrop, { toValue: 0, duration: 180, useNativeDriver: true })
2957
+ ]).start(({ finished }) => {
2958
+ if (finished) setMounted(false);
2959
+ });
2960
+ return void 0;
2961
+ }, [open, translateY, backdrop, screenH]);
2962
+ const pan = react.useRef(
2963
+ reactNative.PanResponder.create({
2964
+ onMoveShouldSetPanResponder: (_evt, g) => g.dy > 6 && Math.abs(g.dy) > Math.abs(g.dx) * 1.4,
2965
+ onPanResponderMove: (_evt, g) => {
2966
+ if (g.dy > 0) translateY.setValue(g.dy);
2967
+ },
2968
+ onPanResponderRelease: (_evt, g) => {
2969
+ if (g.dy > 110 || g.vy > 0.8) {
2970
+ minimizeRef.current();
2971
+ } else {
2972
+ reactNative.Animated.spring(translateY, { toValue: 0, useNativeDriver: true, damping: 30, stiffness: 360 }).start();
2973
+ }
2974
+ }
2975
+ })
2976
+ ).current;
2977
+ const a = active ?? shown;
2978
+ if (!mounted || !a) return null;
2979
+ const isVerification = a.kind === "verification";
2980
+ const title = isVerification ? "Verification required" : "Action required";
2981
+ const subtitle = isVerification ? "Confirm the one-time code to continue" : "Review the details and confirm to continue";
2982
+ return /* @__PURE__ */ jsxRuntime.jsx(
2983
+ reactNative.Modal,
2984
+ {
2985
+ visible: mounted,
2986
+ transparent: true,
2987
+ animationType: "none",
2988
+ statusBarTranslucent: true,
2989
+ onRequestClose: () => minimizeRef.current(),
2990
+ children: /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: reactNative.StyleSheet.absoluteFill, pointerEvents: open ? "auto" : "none", children: [
2991
+ /* @__PURE__ */ jsxRuntime.jsx(
2992
+ reactNative.Animated.View,
2993
+ {
2994
+ pointerEvents: "none",
2995
+ style: [sht.backdrop, { backgroundColor: pal.overlay, opacity: backdrop }]
2996
+ }
2997
+ ),
2998
+ open ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { style: reactNative.StyleSheet.absoluteFill, onPress: () => minimizeRef.current(), accessibilityLabel: "Dismiss" }) : null,
2999
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [sht.kav, { paddingBottom: kbHeight }], pointerEvents: "box-none", children: /* @__PURE__ */ jsxRuntime.jsxs(
3000
+ reactNative.Animated.View,
3001
+ {
3002
+ style: [
3003
+ sht.sheet,
3004
+ {
3005
+ backgroundColor: pal.sheetBg,
3006
+ borderColor: pal.border,
3007
+ // Cap to the space above the keyboard (and a top margin) so the
3008
+ // header stays visible and the footer sits above the keyboard;
3009
+ // the field area scrolls within whatever's left.
3010
+ maxHeight: Math.min(screenH * 0.9, screenH - kbHeight - 72),
3011
+ paddingBottom: kbHeight > 0 ? 16 : Math.max(insetBottom, 16) + 6,
3012
+ transform: [{ translateY }]
3013
+ }
3014
+ ],
3015
+ children: [
3016
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { ...pan.panHandlers, style: sht.handleZone, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [sht.grabber, { backgroundColor: pal.grabber }] }) }),
3017
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: sht.header, children: [
3018
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [sht.iconCircle, { backgroundColor: accent + (isDark ? "26" : "14") }], children: isVerification ? /* @__PURE__ */ jsxRuntime.jsx(lucideReactNative.ShieldCheck, { size: 19, color: accent, strokeWidth: 2.1 }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReactNative.Pencil, { size: 17, color: accent, strokeWidth: 2.1 }) }),
3019
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: sht.headerText, children: [
3020
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [sht.title, { color: pal.text }], numberOfLines: 1, children: title }),
3021
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [sht.subtitle, { color: pal.muted }], numberOfLines: 1, children: subtitle })
3022
+ ] }),
3023
+ /* @__PURE__ */ jsxRuntime.jsx(SheetTimerPill, { prompt: a, accent, isDark }),
3024
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { onPress: () => minimizeRef.current(), hitSlop: 10, style: [sht.closeBtn, { backgroundColor: pal.fieldBg }], children: /* @__PURE__ */ jsxRuntime.jsx(lucideReactNative.X, { size: 15, color: pal.muted, strokeWidth: 2.3 }) })
3025
+ ] }),
3026
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [sht.headerRule, { backgroundColor: pal.border }] }),
3027
+ a.status === "stale" ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: sht.simpleBody, children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [ub.desc, { color: pal.muted }], children: "This request is no longer available." }) }) : expired ? /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: sht.simpleBody, children: [
3028
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [ub.desc, { color: pal.muted }], children: isVerification ? "This verification request expired." : "This request expired." }),
3029
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: ub.actions, children: /* @__PURE__ */ jsxRuntime.jsx(PrimaryButton, { label: "Close", onPress: () => void onCancel(a.userActionId), accent }) })
3030
+ ] }) : isVerification ? /* @__PURE__ */ jsxRuntime.jsx(
3031
+ VerificationBody,
3032
+ {
3033
+ prompt: a,
3034
+ expired,
3035
+ pal,
3036
+ accent,
3037
+ onSubmit,
3038
+ onCancel,
3039
+ onResend
3040
+ },
3041
+ a.userActionId
3042
+ ) : /* @__PURE__ */ jsxRuntime.jsx(FormBody, { prompt: a, pal, accent, isDark, onSubmit, onCancel }, a.userActionId)
3043
+ ]
3044
+ }
3045
+ ) })
3046
+ ] })
3047
+ }
3048
+ );
3049
+ }
3050
+ function UserActionReopenCard({
3051
+ active,
3052
+ expired,
3053
+ isDark,
3054
+ accent,
3055
+ onReopen
3056
+ }) {
3057
+ if (expired || active.status === "stale") return null;
3058
+ const pal = uaPalette(isDark);
3059
+ const isVerification = active.kind === "verification";
3060
+ const title = isVerification ? "Verification required" : "Action required";
3061
+ return /* @__PURE__ */ jsxRuntime.jsxs(
3062
+ reactNative.Pressable,
3063
+ {
3064
+ onPress: onReopen,
3065
+ accessibilityRole: "button",
3066
+ accessibilityLabel: `${title}. Tap to respond.`,
3067
+ android_ripple: { color: accent + "22" },
3068
+ style: [
3069
+ rc.card,
3070
+ {
3071
+ backgroundColor: pal.sheetBg,
3072
+ borderColor: accent + (isDark ? "59" : "40")
3073
+ }
3074
+ ],
3075
+ children: [
3076
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [rc.iconCircle, { backgroundColor: accent + (isDark ? "26" : "14") }], children: isVerification ? /* @__PURE__ */ jsxRuntime.jsx(lucideReactNative.ShieldCheck, { size: 18, color: accent, strokeWidth: 2 }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReactNative.Pencil, { size: 16, color: accent, strokeWidth: 2 }) }),
3077
+ /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: rc.textWrap, children: [
3078
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [rc.title, { color: pal.text }], numberOfLines: 1, children: title }),
3079
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [rc.subtitle, { color: pal.muted }], numberOfLines: 1, children: "Tap to respond and continue" })
3080
+ ] }),
3081
+ /* @__PURE__ */ jsxRuntime.jsx(CardTimerPill, { prompt: active, accent, isDark }),
3082
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReactNative.ChevronRight, { size: 18, color: pal.muted, strokeWidth: 2 })
3083
+ ]
3084
+ }
3085
+ );
3086
+ }
3087
+ function NotificationStack({
3088
+ notifications,
3089
+ isDark,
3090
+ onDismiss
3091
+ }) {
3092
+ if (notifications.length === 0) return null;
3093
+ const pal = uaPalette(isDark);
3094
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: ub.noteStack, children: notifications.map((n) => /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [ub.note, { backgroundColor: pal.fieldBg, borderColor: pal.border }], children: [
3095
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReactNative.Info, { size: 14, color: pal.muted, strokeWidth: 1.9 }),
3096
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [ub.noteText, { color: pal.text }], children: n.message }),
3097
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Pressable, { onPress: () => onDismiss(n.id), hitSlop: 8, children: /* @__PURE__ */ jsxRuntime.jsx(lucideReactNative.X, { size: 14, color: pal.muted, strokeWidth: 2 }) })
3098
+ ] }, n.id)) });
3099
+ }
3100
+ var ub = reactNative.StyleSheet.create({
3101
+ // Body wrapper can shrink within the sheet's (bounded) maxHeight so the inner
3102
+ // field scroll takes over for tall forms while the footer stays pinned.
3103
+ bodyWrap: { flexShrink: 1 },
3104
+ // Field scroll region. flexShrink:1 + no flexGrow → hugs content for short
3105
+ // forms (no scroll), shrinks and scrolls only when the form exceeds the sheet.
3106
+ fieldsScroll: { flexShrink: 1 },
3107
+ fieldsContent: { paddingBottom: 6 },
3108
+ messageBlock: { marginBottom: 14, marginTop: 0 },
3109
+ desc: { fontSize: 14, lineHeight: 20, marginBottom: 14 },
3110
+ form: { gap: 14 },
3111
+ field: { gap: 6 },
3112
+ label: { fontSize: 13.5, fontWeight: "600", letterSpacing: -0.1 },
3113
+ hint: { fontSize: 12, lineHeight: 16 },
3114
+ input: {
3115
+ borderWidth: 1,
3116
+ borderRadius: 12,
3117
+ paddingHorizontal: 14,
3118
+ paddingVertical: 12,
3119
+ fontSize: 16
3120
+ },
3121
+ error: { color: "#ef4444", fontSize: 12, marginTop: 2 },
3122
+ switchRow: { flexDirection: "row", alignItems: "center", gap: 12 },
3123
+ // Selectable option "boxes" for oneOf / enum single-select fields.
3124
+ optionList: { gap: 9 },
3125
+ optionRow: {
3126
+ flexDirection: "row",
3127
+ alignItems: "center",
3128
+ gap: 12,
3129
+ borderWidth: 1.5,
3130
+ borderRadius: 13,
3131
+ paddingHorizontal: 14,
3132
+ paddingVertical: 13
3133
+ },
3134
+ radio: {
3135
+ width: 19,
3136
+ height: 19,
3137
+ borderRadius: 10,
3138
+ borderWidth: 2,
3139
+ alignItems: "center",
3140
+ justifyContent: "center"
3141
+ },
3142
+ radioDot: { width: 10, height: 10, borderRadius: 5 },
3143
+ optionLabel: { flex: 1, fontSize: 15, letterSpacing: -0.1 },
3144
+ // Pinned footer (outside the fields scroll) — always visible.
3145
+ actions: { paddingTop: 14, gap: 12 },
3146
+ primaryBtn: { minHeight: 50, borderRadius: 14, borderWidth: 1, paddingVertical: 14, alignItems: "center", justifyContent: "center" },
3147
+ primaryBtnText: { color: "#fff", fontSize: 15.5, fontWeight: "600", letterSpacing: -0.1 },
3148
+ linkRow: { flexDirection: "row", justifyContent: "center", gap: 24, paddingTop: 2 },
3149
+ link: { fontSize: 14, fontWeight: "500" },
3150
+ linkDanger: { color: "#dc2626" },
3151
+ codeRow: { flexDirection: "row", justifyContent: "center", gap: 9, position: "relative" },
3152
+ codeBox: {
3153
+ width: 46,
3154
+ height: 56,
3155
+ borderWidth: 1.5,
3156
+ borderRadius: 12,
3157
+ alignItems: "center",
3158
+ justifyContent: "center"
3159
+ },
3160
+ codeChar: { fontSize: 24, fontWeight: "600" },
3161
+ codeHidden: { ...reactNative.StyleSheet.absoluteFillObject, opacity: 0 },
3162
+ noteStack: { paddingHorizontal: 16, paddingBottom: 6, gap: 6 },
3163
+ note: {
3164
+ flexDirection: "row",
3165
+ alignItems: "center",
3166
+ gap: 8,
3167
+ borderWidth: 1,
3168
+ borderRadius: 12,
3169
+ paddingHorizontal: 12,
3170
+ paddingVertical: 10
3171
+ },
3172
+ noteText: { flex: 1, fontSize: 13.5, lineHeight: 19 }
3173
+ });
3174
+ var sht = reactNative.StyleSheet.create({
3175
+ // High stacking so the sheet sits above the chat input bar (which carries its
3176
+ // own zIndex/elevation) and everything else in the chat.
3177
+ overlay: { zIndex: 100, ...reactNative.Platform.select({ android: { elevation: 100 }, default: {} }) },
3178
+ backdrop: { ...reactNative.StyleSheet.absoluteFillObject },
3179
+ kav: { flex: 1, justifyContent: "flex-end" },
3180
+ sheet: {
3181
+ borderTopLeftRadius: 26,
3182
+ borderTopRightRadius: 26,
3183
+ borderWidth: reactNative.StyleSheet.hairlineWidth,
3184
+ borderBottomWidth: 0,
3185
+ paddingHorizontal: 20,
3186
+ paddingTop: 4,
3187
+ maxWidth: 640,
3188
+ width: "100%",
3189
+ alignSelf: "center",
3190
+ ...reactNative.Platform.select({
3191
+ ios: {
3192
+ shadowColor: "#000",
3193
+ shadowOffset: { width: 0, height: -8 },
3194
+ shadowOpacity: 0.22,
3195
+ shadowRadius: 28
3196
+ },
3197
+ android: { elevation: 24 }
3198
+ })
3199
+ },
3200
+ handleZone: { alignItems: "center", paddingTop: 9, paddingBottom: 8 },
3201
+ grabber: { width: 38, height: 5, borderRadius: 3 },
3202
+ header: { flexDirection: "row", alignItems: "center", gap: 11, marginTop: 0, marginBottom: 14 },
3203
+ iconCircle: { width: 36, height: 36, borderRadius: 12, alignItems: "center", justifyContent: "center" },
3204
+ headerText: { flex: 1, gap: 1 },
3205
+ title: { fontSize: 17, fontWeight: "700", letterSpacing: -0.3 },
3206
+ subtitle: { fontSize: 12.5, letterSpacing: -0.1 },
3207
+ // Hairline rule under the header so the title cluster reads as its own zone.
3208
+ headerRule: { height: reactNative.StyleSheet.hairlineWidth, marginBottom: 16, marginHorizontal: -20 },
3209
+ timer: { borderWidth: 1, borderRadius: 999, paddingHorizontal: 10, paddingVertical: 4.5, minWidth: 44, alignItems: "center" },
3210
+ closeBtn: { width: 29, height: 29, borderRadius: 14.5, alignItems: "center", justifyContent: "center" },
3211
+ // Short, non-scrolling bodies (stale / expired notices).
3212
+ simpleBody: { paddingBottom: 8 }
3213
+ });
3214
+ var rc = reactNative.StyleSheet.create({
3215
+ card: {
3216
+ flexDirection: "row",
3217
+ alignItems: "center",
3218
+ gap: 12,
3219
+ borderWidth: 1.5,
3220
+ borderRadius: 18,
3221
+ paddingHorizontal: 14,
3222
+ paddingVertical: 13,
3223
+ marginVertical: 6
3224
+ },
3225
+ iconCircle: { width: 34, height: 34, borderRadius: 17, alignItems: "center", justifyContent: "center" },
3226
+ textWrap: { flex: 1, gap: 1 },
3227
+ title: { fontSize: 14.5, fontWeight: "700", letterSpacing: -0.2 },
3228
+ subtitle: { fontSize: 12.5, letterSpacing: -0.1 },
3229
+ timer: { borderRadius: 999, paddingHorizontal: 9, paddingVertical: 4, minWidth: 40, alignItems: "center" }
3230
+ });
3231
+ var PaymanChat = react.forwardRef(
3232
+ function PaymanChat2({ config, callbacks, children, onLoadMoreMessages, isLoadingMoreMessages = false, hasMoreMessages = false }, ref) {
3233
+ const accent = config.accent ?? DEFAULT_ACCENT;
3234
+ const systemScheme = reactNative.useColorScheme();
3235
+ const isDark = (config.colorScheme ?? systemScheme) === "dark";
3236
+ const ui = isDark ? {
3237
+ listBg: "#0d1719",
3238
+ emptyText: "rgba(255,255,255,0.55)"
3239
+ } : {
3240
+ listBg: "#ffffff",
3241
+ emptyText: "#6b7280"
3242
+ };
3243
+ const callbacksRef = react.useRef(callbacks);
3244
+ react.useEffect(() => {
3245
+ callbacksRef.current = callbacks;
3246
+ });
3247
+ const stableCallbacks = react.useMemo(() => ({
3248
+ onSessionIdChange: (id) => callbacksRef.current?.onSessionIdChange?.(id),
3249
+ onStreamStart: () => callbacksRef.current?.onStreamStart?.(),
3250
+ onStreamEnd: () => callbacksRef.current?.onStreamEnd?.(),
3251
+ onError: (e) => {
3252
+ console.error("[PaymanChat] stream error:", e.message, e);
3253
+ callbacksRef.current?.onError?.(e);
3254
+ },
3255
+ onStatusMessage: (m) => callbacksRef.current?.onStatusMessage?.(m),
3256
+ onStepsUpdate: (steps) => callbacksRef.current?.onStepsUpdate?.(steps),
3257
+ onUserActionRequired: (r) => {
3258
+ console.log("[PaymanChat] USER_ACTION_REQUIRED:", JSON.stringify({ kind: r?.kind, rawAction: r?.rawAction, userActionId: r?.userActionId, hasSchema: !!r?.requestedSchema }));
3259
+ callbacksRef.current?.onUserActionRequired?.(r);
3260
+ },
3261
+ onUserNotification: (n) => {
3262
+ console.log("[PaymanChat] USER_NOTIFICATION:", JSON.stringify({ id: n?.id, message: n?.message }));
3263
+ callbacksRef.current?.onUserNotification?.(n);
3264
+ }
3265
+ }), []);
3266
+ const {
3267
+ messages,
3268
+ sendMessage,
3269
+ isWaitingForResponse,
3270
+ resetSession,
3271
+ clearMessages,
3272
+ prependMessages,
3273
+ cancelStream,
3274
+ getSessionId,
3275
+ getMessages,
3276
+ userActionState,
3277
+ submitUserAction: submitUserAction2,
3278
+ cancelUserAction: cancelUserAction2,
3279
+ resendUserAction: resendUserAction2,
3280
+ dismissNotification
3281
+ } = useChatV2(config, stableCallbacks);
3282
+ react.useEffect(() => {
3283
+ console.log(
3284
+ "[PaymanChat] userActionState ->",
3285
+ "prompts:",
3286
+ userActionState.prompts.length,
3287
+ userActionState.prompts.map((p) => `${p.kind}:${p.status}`).join(","),
3288
+ "| notifications:",
3289
+ userActionState.notifications.length
3290
+ );
3291
+ }, [userActionState]);
3292
+ const activePrompt = react.useMemo(
3293
+ () => userActionState.prompts.find((p) => p.kind !== "notification"),
3294
+ [userActionState.prompts]
3295
+ );
3296
+ const expired = useExpiredFlag(activePrompt);
3297
+ const [dismissedActionId, setDismissedActionId] = react.useState(null);
3298
+ react.useEffect(() => {
3299
+ if (activePrompt) setDismissedActionId(null);
3300
+ }, [activePrompt?.userActionId, activePrompt?.subAction]);
3301
+ const sheetOpen = !!activePrompt && dismissedActionId !== activePrompt.userActionId;
3302
+ const showReopenCard = !!activePrompt && !sheetOpen && !expired && activePrompt.status !== "stale";
3303
+ const minimizeActivePrompt = react.useCallback(() => {
3304
+ setDismissedActionId((prev) => activePrompt?.userActionId ?? prev);
3305
+ }, [activePrompt?.userActionId]);
3306
+ const {
3307
+ transcribedText,
3308
+ isAvailable: voiceAvailable,
3309
+ isRecording,
3310
+ startRecording,
3311
+ stopRecording,
3312
+ clearTranscript
3313
+ } = useVoice(
3314
+ { lang: "en-US", interimResults: true, continuous: true },
3315
+ { onError: (e) => console.error("Voice error:", e) }
3316
+ );
3317
+ const [inputValue, setInputValue] = react.useState("");
3318
+ const [recordingSeconds, setRecordingSeconds] = react.useState(0);
3319
+ const recordingStartRef = react.useRef(null);
3320
+ const recordingTimerRef = react.useRef(null);
3321
+ react.useImperativeHandle(ref, () => ({ resetSession, clearMessages, cancelStream, getSessionId, getMessages }));
3322
+ const contextValue = react.useMemo(() => ({
3323
+ resetSession,
3324
+ clearMessages,
3325
+ prependMessages,
3326
+ cancelStream,
3327
+ getSessionId,
3328
+ getMessages,
3329
+ isWaitingForResponse
3330
+ }), [resetSession, clearMessages, prependMessages, cancelStream, getSessionId, getMessages, isWaitingForResponse]);
3331
+ react.useEffect(() => {
3332
+ if (isRecording) {
3333
+ recordingStartRef.current = Date.now();
3334
+ setRecordingSeconds(0);
3335
+ recordingTimerRef.current = setInterval(() => {
3336
+ if (recordingStartRef.current != null)
3337
+ setRecordingSeconds(Math.floor((Date.now() - recordingStartRef.current) / 1e3));
3338
+ }, 1e3);
3339
+ } else {
3340
+ recordingStartRef.current = null;
3341
+ if (recordingTimerRef.current) {
3342
+ clearInterval(recordingTimerRef.current);
3343
+ recordingTimerRef.current = null;
3344
+ }
3345
+ setRecordingSeconds(0);
3346
+ }
3347
+ return () => {
3348
+ if (recordingTimerRef.current) clearInterval(recordingTimerRef.current);
3349
+ };
3350
+ }, [isRecording]);
3351
+ const prevInput = react.useRef(inputValue);
3352
+ react.useEffect(() => {
3353
+ const wasEmpty = prevInput.current.trim() === "";
3354
+ const isEmpty2 = inputValue.trim() === "";
3355
+ prevInput.current = inputValue;
3356
+ if (!wasEmpty && isEmpty2) {
3357
+ clearTranscript();
3358
+ if (isRecording) stopRecording();
3359
+ }
3360
+ }, [inputValue, clearTranscript, isRecording, stopRecording]);
3361
+ const handleSend = react.useCallback(() => {
3362
+ if (isRecording) stopRecording();
3363
+ const text = inputValue.trim();
3364
+ if (!text || isWaitingForResponse) return;
3365
+ console.log("[PaymanChat] sending. baseUrl:", config.api?.baseUrl, "agentId:", config.agentId, "stage:", config.stage, "sessionOwnerId:", config.sessionParams?.id);
3366
+ setInputValue("");
3367
+ void sendMessage(text);
3368
+ }, [inputValue, isWaitingForResponse, sendMessage, isRecording, stopRecording, config]);
3369
+ const handleVoicePress = react.useCallback(async () => {
3370
+ if (!voiceAvailable) return;
3371
+ clearTranscript();
3372
+ await startRecording();
3373
+ }, [voiceAvailable, startRecording, clearTranscript]);
3374
+ const handleConfirmRecording = react.useCallback(() => {
3375
+ stopRecording();
3376
+ if (transcribedText.trim()) setInputValue(transcribedText);
3377
+ }, [stopRecording, transcribedText]);
3378
+ const handleCancelRecording = react.useCallback(() => {
3379
+ stopRecording();
3380
+ clearTranscript();
3381
+ }, [stopRecording, clearTranscript]);
3382
+ const isInputDisabled = !config.sessionParams?.id?.trim();
3383
+ const enableVoice = config.enableVoice !== false;
3384
+ const isEmpty = messages.length === 0;
3385
+ const prevMessageCountRef = react.useRef(messages.length);
3386
+ const typingMessageIds = react.useRef(/* @__PURE__ */ new Set());
3387
+ react.useEffect(() => {
3388
+ if (messages.length > prevMessageCountRef.current) {
3389
+ const newMsgs = messages.slice(prevMessageCountRef.current);
3390
+ newMsgs.forEach((m) => {
3391
+ if (m.role === "assistant") typingMessageIds.current.add(m.id);
3392
+ });
3393
+ }
3394
+ prevMessageCountRef.current = messages.length;
3395
+ }, [messages]);
3396
+ const scrollRef = react.useRef(null);
3397
+ const scrollToBottom = react.useCallback((animated = true) => {
3398
+ scrollRef.current?.scrollToEnd({ animated });
3399
+ }, []);
3400
+ react.useEffect(() => {
3401
+ const t = setTimeout(() => scrollToBottom(true), 60);
3402
+ return () => clearTimeout(t);
3403
+ }, [messages, scrollToBottom]);
3404
+ return /* @__PURE__ */ jsxRuntime.jsx(PaymanChatContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsxRuntime.jsxs(
3405
+ reactNative.KeyboardAvoidingView,
3406
+ {
3407
+ style: s.container,
3408
+ behavior: reactNative.Platform.OS === "ios" ? "padding" : "height",
3409
+ keyboardVerticalOffset: 0,
3410
+ children: [
3411
+ children,
3412
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [s.messageListWrap, { backgroundColor: ui.listBg }], children: isEmpty && !isWaitingForResponse ? /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: s.emptyState, children: [
3413
+ config.emptyStateComponent,
3414
+ config.emptyStateComponent != null && /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: { height: 16 } }),
3415
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [s.emptyText, { color: ui.emptyText }], children: config.emptyStateText ?? "How can I help you today?" })
3416
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(
3417
+ reactNative.ScrollView,
3418
+ {
3419
+ ref: scrollRef,
3420
+ style: s.scrollList,
3421
+ contentContainerStyle: s.listContent,
3422
+ showsVerticalScrollIndicator: false,
3423
+ keyboardShouldPersistTaps: "handled",
3424
+ onContentSizeChange: () => scrollToBottom(false),
3425
+ children: [
3426
+ messages.map((msg) => /* @__PURE__ */ jsxRuntime.jsx(
3427
+ MessageBubble,
3428
+ {
3429
+ message: msg,
3430
+ accent,
3431
+ shouldType: typingMessageIds.current.has(msg.id),
3432
+ isDark,
3433
+ suppressText: activePrompt?.message && msg.role === "assistant" && msg.id === messages[messages.length - 1]?.id ? activePrompt.message : void 0
3434
+ },
3435
+ msg.id
3436
+ )),
3437
+ showReopenCard && activePrompt ? /* @__PURE__ */ jsxRuntime.jsx(
3438
+ UserActionReopenCard,
3439
+ {
3440
+ active: activePrompt,
3441
+ expired,
3442
+ isDark,
3443
+ accent,
3444
+ onReopen: () => setDismissedActionId(null)
3445
+ }
3446
+ ) : null,
3447
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: { height: 4 } })
3448
+ ]
3449
+ }
3450
+ ) }),
3451
+ /* @__PURE__ */ jsxRuntime.jsx(
3452
+ NotificationStack,
3453
+ {
3454
+ notifications: userActionState.notifications,
3455
+ isDark,
3456
+ onDismiss: dismissNotification
3457
+ }
3458
+ ),
3459
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: s.inputBarWrap, children: /* @__PURE__ */ jsxRuntime.jsx(
3460
+ InputBar,
3461
+ {
3462
+ value: inputValue,
3463
+ onChange: setInputValue,
3464
+ onSend: handleSend,
3465
+ onCancel: cancelStream,
3466
+ disabled: isInputDisabled,
3467
+ placeholder: config.placeholder ?? "Type your message...",
3468
+ isStreaming: isWaitingForResponse,
3469
+ accent,
3470
+ enableVoice,
3471
+ voiceAvailable: enableVoice && voiceAvailable,
3472
+ isRecording: enableVoice && isRecording,
3473
+ recordingSeconds,
3474
+ transcribedText,
3475
+ onVoicePress: handleVoicePress,
3476
+ onConfirmRecording: handleConfirmRecording,
3477
+ onCancelRecording: handleCancelRecording,
3478
+ onFocus: () => {
3479
+ },
3480
+ insetBottom: config.contentInsetBottom ?? (reactNative.Platform.OS === "ios" ? 20 : 8)
3481
+ }
3482
+ ) }),
3483
+ /* @__PURE__ */ jsxRuntime.jsx(
3484
+ UserActionSheet,
3485
+ {
3486
+ open: sheetOpen,
3487
+ active: activePrompt,
3488
+ expired,
3489
+ isDark,
3490
+ accent,
3491
+ insetBottom: config.contentInsetBottom ?? (reactNative.Platform.OS === "ios" ? 20 : 8),
3492
+ onSubmit: submitUserAction2,
3493
+ onCancel: cancelUserAction2,
3494
+ onResend: resendUserAction2,
3495
+ onMinimize: minimizeActivePrompt
3496
+ }
3497
+ )
3498
+ ]
3499
+ }
3500
+ ) });
3501
+ }
3502
+ );
3503
+ var s = reactNative.StyleSheet.create({
3504
+ container: { flex: 1, backgroundColor: "#f1f5f9" },
3505
+ messageListWrap: { flex: 1, minHeight: 0, backgroundColor: "#ffffff" },
3506
+ inputBarWrap: {
3507
+ width: "100%",
3508
+ zIndex: 2,
3509
+ backgroundColor: "#ffffff",
3510
+ ...reactNative.Platform.select({ android: { elevation: 8 }, default: {} })
3511
+ },
3512
+ // ── Input bar ──
3513
+ outerBar: {
3514
+ width: "100%",
3515
+ backgroundColor: "#f1f5f9",
3516
+ borderTopWidth: 1,
3517
+ borderTopColor: "rgba(0,0,0,0.08)",
3518
+ paddingTop: 6,
3519
+ paddingHorizontal: 12,
3520
+ borderTopLeftRadius: 20,
3521
+ borderTopRightRadius: 20
3522
+ },
3523
+ outerBarVoice: {},
3524
+ column: { maxWidth: 672, width: "100%", alignSelf: "center", gap: 2 },
3525
+ inputWrap: { width: "100%", justifyContent: "center", paddingHorizontal: 4 },
3526
+ inputWrapRelative: { position: "relative" },
3527
+ input: {
3528
+ minHeight: 36,
3529
+ maxHeight: 110,
3530
+ paddingVertical: 8,
3531
+ paddingHorizontal: 16,
3532
+ fontSize: 16,
3533
+ color: "#1F2937",
3534
+ lineHeight: 22
3535
+ },
3536
+ inputDisabled: { opacity: 0.5 },
3537
+ inputHidden: { position: "absolute", left: 0, right: 0, top: 0, opacity: 0, zIndex: 0 },
3538
+ actionsRow: { flexDirection: "row", justifyContent: "flex-end", alignItems: "center", marginBottom: 2 },
3539
+ rightActions: { flexDirection: "row", alignItems: "center", gap: 6 },
3540
+ iconBtn: { width: 28, height: 28, justifyContent: "center", alignItems: "center", borderRadius: 14, overflow: "hidden" },
3541
+ iconBtnInner: { width: 28, height: 28, justifyContent: "center", alignItems: "center", backgroundColor: "rgba(0,0,0,0.1)", borderRadius: 14 },
3542
+ sendBtn: { width: 28, height: 28, borderRadius: 14, overflow: "hidden", justifyContent: "center", alignItems: "center" },
3543
+ sendBtnInner: { width: 28, height: 28, borderRadius: 14, justifyContent: "center", alignItems: "center" },
3544
+ btnDisabled: { opacity: 0.35 },
3545
+ pressed: { opacity: 0.7 },
3546
+ disclaimerWrap: { maxWidth: 672, width: "100%", alignSelf: "center", paddingHorizontal: 12, marginTop: 2 },
3547
+ disclaimerText: { fontSize: 10, lineHeight: 13, color: "#9CA3AF", textAlign: "center" },
3548
+ // ── Voice bar ──
3549
+ voiceBarContainer: { position: "absolute", left: 0, right: 0, top: 0, bottom: 0, justifyContent: "center", zIndex: 1 },
3550
+ voiceBar: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", borderWidth: 1, borderRadius: 28, minHeight: 56, paddingHorizontal: 12, paddingVertical: 10, gap: 12 },
3551
+ voiceBarCircle: { width: 40, height: 40, borderRadius: 20, alignItems: "center", justifyContent: "center" },
3552
+ voiceConfirm: { backgroundColor: "#FFFFFF", borderWidth: 1 },
3553
+ voiceBarCenter: { flex: 1, flexDirection: "row", alignItems: "center", justifyContent: "space-between", gap: 10, marginLeft: 8 },
3554
+ voiceDots: { flex: 1, flexDirection: "row", alignItems: "center", justifyContent: "space-between", gap: 4 },
3555
+ voiceDot: { width: 4, borderRadius: 2 },
3556
+ voiceTimer: { fontSize: 15, fontWeight: "600", minWidth: 36, textAlign: "right" },
3557
+ // ── Messages ──
3558
+ scrollList: { flex: 1 },
3559
+ listContent: { paddingHorizontal: 16, paddingTop: 8, paddingBottom: 4, flexGrow: 1 },
3560
+ emptyState: { flex: 1, alignItems: "center", justifyContent: "center", paddingHorizontal: 32, paddingBottom: 60 },
3561
+ emptyText: { fontSize: 16, color: "#6b7280", textAlign: "center" }
3562
+ });
3563
+ var sd = reactNative.StyleSheet.create({
3564
+ bubbleRow: { flexDirection: "row", marginVertical: 3 },
3565
+ rowUser: { justifyContent: "flex-end" },
3566
+ rowAssistant: { justifyContent: "flex-start" },
3567
+ bubble: { maxWidth: "82%", paddingHorizontal: 14, paddingVertical: 10, borderRadius: 18 },
3568
+ bubbleUser: { borderBottomRightRadius: 4 },
3569
+ bubbleAssistant: { backgroundColor: "#f3f4f6", borderBottomLeftRadius: 4 },
3570
+ // Assistant bubbles stretch wider so tables and code blocks have room
3571
+ bubbleAssistantWide: { maxWidth: "97%" },
3572
+ bubbleError: { backgroundColor: "#fef2f2" },
3573
+ bubbleText: { fontSize: 15, lineHeight: 22 },
3574
+ textUser: { color: "#ffffff" },
3575
+ textAssistant: { color: "#111827" },
3576
+ errorText: { fontSize: 14, lineHeight: 20, color: "#dc2626" },
3577
+ partialErrorText: { fontSize: 12, color: "#dc2626", marginTop: 4 },
3578
+ cancelledText: { fontSize: 12, color: "#9ca3af", marginTop: 4, fontStyle: "italic" },
3579
+ thinkingRow: { flexDirection: "row", alignItems: "flex-start", gap: 6, marginBottom: 3 },
3580
+ thinkingIcon: { fontSize: 12, lineHeight: 20, width: 14 },
3581
+ thinkingStepText: { flex: 1, fontSize: 14, lineHeight: 20, color: "#374151", fontStyle: "italic" },
3582
+ thinkingStepDone: { color: "#9ca3af" }
3583
+ });
3584
+ function mdPalette(isDark) {
3585
+ return isDark ? {
3586
+ text: "rgba(255,255,255,0.92)",
3587
+ heading: "#ffffff",
3588
+ muted: "rgba(255,255,255,0.55)",
3589
+ border: "rgba(255,255,255,0.16)",
3590
+ borderSoft: "rgba(255,255,255,0.10)",
3591
+ headerBg: "rgba(255,255,255,0.07)",
3592
+ codeInlineBg: "rgba(255,255,255,0.12)",
3593
+ codeInlineText: "#e8eaed",
3594
+ codeBlockBg: "#0b1416",
3595
+ codeBlockText: "#f3f4f6",
3596
+ blockquoteBar: "rgba(255,255,255,0.22)",
3597
+ hr: "rgba(255,255,255,0.14)",
3598
+ link: "#34c3cc"
3599
+ } : {
3600
+ text: "#1f2937",
3601
+ heading: "#111827",
3602
+ muted: "#4b5563",
3603
+ border: "#d1d5db",
3604
+ borderSoft: "#e5e7eb",
3605
+ headerBg: "#f3f4f6",
3606
+ codeInlineBg: "#e5e7eb",
3607
+ codeInlineText: "#1f2937",
3608
+ codeBlockBg: "#1f2937",
3609
+ codeBlockText: "#f9fafb",
3610
+ blockquoteBar: "#d1d5db",
3611
+ hr: "#e5e7eb",
3612
+ link: "#00858d"
3613
+ };
3614
+ }
3615
+ var TABLE_CHAR_W = 7.6;
3616
+ var TABLE_CELL_PAD = 26;
3617
+ var TABLE_MIN_W = 44;
3618
+ var TABLE_MAX_W = 220;
3619
+ function nodeText(node) {
3620
+ if (!node) return "";
3621
+ if (Array.isArray(node.children) && node.children.length) {
3622
+ return node.children.map(nodeText).join("");
3623
+ }
3624
+ return typeof node.content === "string" ? node.content : "";
3625
+ }
3626
+ function tableMatrix(tableNode) {
3627
+ const rows = [];
3628
+ const walk = (n) => {
3629
+ if (!n) return;
3630
+ if (n.type === "tr") {
3631
+ rows.push((n.children || []).map(nodeText));
3632
+ return;
3633
+ }
3634
+ (n.children || []).forEach(walk);
3635
+ };
3636
+ walk(tableNode);
3637
+ return rows;
3638
+ }
3639
+ function makeMdRules(isDark, accent) {
3640
+ const c = mdPalette(isDark);
3641
+ const headerBg = isDark ? accent + "2E" : accent + "14";
3642
+ const headerText = isDark ? "#8fe3e9" : accent;
3643
+ return {
3644
+ table: (node) => {
3645
+ const rows = tableMatrix(node);
3646
+ if (rows.length === 0) return null;
3647
+ const ncols = Math.max(...rows.map((r) => r.length));
3648
+ const colW = [];
3649
+ for (let ci = 0; ci < ncols; ci++) {
3650
+ let maxLen = 0;
3651
+ for (const r of rows) maxLen = Math.max(maxLen, (r[ci] || "").length);
3652
+ colW[ci] = Math.min(
3653
+ TABLE_MAX_W,
3654
+ Math.max(TABLE_MIN_W, Math.round(maxLen * TABLE_CHAR_W) + TABLE_CELL_PAD)
3655
+ );
3656
+ }
3657
+ return /* @__PURE__ */ jsxRuntime.jsx(
3658
+ reactNative.ScrollView,
3659
+ {
3660
+ horizontal: true,
3661
+ showsHorizontalScrollIndicator: true,
3662
+ style: { marginVertical: 8 },
3663
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: { borderWidth: 1, borderColor: c.border, borderRadius: 8, overflow: "hidden" }, children: rows.map((cells, ri) => {
3664
+ const isHeader = ri === 0;
3665
+ return /* @__PURE__ */ jsxRuntime.jsx(
3666
+ reactNative.View,
3667
+ {
3668
+ style: {
3669
+ flexDirection: "row",
3670
+ backgroundColor: isHeader ? headerBg : "transparent",
3671
+ borderTopWidth: ri === 0 ? 0 : 1,
3672
+ borderTopColor: c.borderSoft
3673
+ },
3674
+ children: Array.from({ length: ncols }).map((_, ci) => /* @__PURE__ */ jsxRuntime.jsx(
3675
+ reactNative.View,
3676
+ {
3677
+ style: {
3678
+ width: colW[ci],
3679
+ paddingHorizontal: 12,
3680
+ paddingVertical: 9,
3681
+ borderRightWidth: ci === ncols - 1 ? 0 : 1,
3682
+ borderRightColor: c.borderSoft,
3683
+ justifyContent: "center"
3684
+ },
3685
+ children: /* @__PURE__ */ jsxRuntime.jsx(
3686
+ reactNative.Text,
3687
+ {
3688
+ style: {
3689
+ fontSize: 13,
3690
+ lineHeight: 18,
3691
+ color: isHeader ? headerText : c.text,
3692
+ fontWeight: isHeader ? "700" : "400"
3693
+ },
3694
+ children: cells[ci] ?? ""
3695
+ }
3696
+ )
3697
+ },
3698
+ ci
3699
+ ))
3700
+ },
3701
+ ri
3702
+ );
3703
+ }) })
3704
+ },
3705
+ node.key
3706
+ );
3707
+ }
3708
+ };
3709
+ }
3710
+ function makeMdStyles(isDark) {
3711
+ const c = mdPalette(isDark);
3712
+ const mono = reactNative.Platform.OS === "ios" ? "Menlo" : "monospace";
3713
+ return {
3714
+ // Body
3715
+ body: { color: c.text, fontSize: 14, lineHeight: 21 },
3716
+ paragraph: { marginTop: 0, marginBottom: 4 },
3717
+ // Emphasis
3718
+ strong: { fontWeight: "700", color: c.text },
3719
+ em: { fontStyle: "italic", color: c.text },
3720
+ // Links
3721
+ link: { color: c.link, textDecorationLine: "underline" },
3722
+ // Inline code
3723
+ code_inline: {
3724
+ backgroundColor: c.codeInlineBg,
3725
+ color: c.codeInlineText,
3726
+ fontFamily: mono,
3727
+ fontSize: 12,
3728
+ paddingHorizontal: 4,
3729
+ borderRadius: 4
3730
+ },
3731
+ code_inline_container: { borderRadius: 4 },
3732
+ // Code blocks
3733
+ fence: { backgroundColor: c.codeBlockBg, borderRadius: 8, padding: 12, marginVertical: 6 },
3734
+ code_block: { backgroundColor: c.codeBlockBg, borderRadius: 8, padding: 12, marginVertical: 6 },
3735
+ fence_code: { color: c.codeBlockText, fontFamily: mono, fontSize: 12 },
3736
+ // Lists
3737
+ bullet_list: { marginVertical: 4 },
3738
+ ordered_list: { marginVertical: 4 },
3739
+ list_item: { marginVertical: 1 },
3740
+ bullet_list_icon: { color: c.muted, marginTop: 6 },
3741
+ ordered_list_icon: { color: c.muted },
3742
+ list_item_text: { fontSize: 14, color: c.text },
3743
+ // Headings
3744
+ heading1: { fontSize: 18, fontWeight: "700", color: c.heading, marginBottom: 6, marginTop: 8 },
3745
+ heading2: { fontSize: 16, fontWeight: "700", color: c.heading, marginBottom: 4, marginTop: 6 },
3746
+ heading3: { fontSize: 15, fontWeight: "600", color: c.heading, marginBottom: 4, marginTop: 4 },
3747
+ // Blockquote / HR
3748
+ blockquote: { borderLeftWidth: 3, borderLeftColor: c.blockquoteBar, paddingLeft: 10, marginVertical: 4 },
3749
+ hr: { backgroundColor: c.hr, height: 1, marginVertical: 8 }
3750
+ // Tables are rendered by a fully custom `table` rule (see makeMdRules) —
3751
+ // no per-cell styles needed here.
3752
+ };
3753
+ }
3754
+ var MD_STYLES_LIGHT = makeMdStyles(false);
3755
+ var MD_STYLES_DARK = makeMdStyles(true);
3756
+ function cn(...inputs) {
3757
+ return tailwindMerge.twMerge(clsx.clsx(inputs));
3758
+ }
3759
+
3760
+ // src/utils/formatDate.ts
3761
+ function formatDate(timestamp) {
3762
+ const date = typeof timestamp === "string" ? new Date(timestamp) : timestamp;
3763
+ const now = /* @__PURE__ */ new Date();
3764
+ const diffMs = now.getTime() - date.getTime();
3765
+ const diffMins = Math.floor(diffMs / 6e4);
3766
+ const diffHours = Math.floor(diffMs / 36e5);
3767
+ const diffDays = Math.floor(diffMs / 864e5);
3768
+ if (diffMins < 1) {
3769
+ return "Just now";
3770
+ }
3771
+ if (diffMins < 60) {
3772
+ return `${diffMins}m ago`;
3773
+ }
3774
+ if (diffHours < 24) {
3775
+ return `${diffHours}h ago`;
3776
+ }
3777
+ if (diffDays < 7) {
3778
+ return `${diffDays}d ago`;
3779
+ }
3780
+ return date.toLocaleDateString("en-US", {
3781
+ month: "short",
3782
+ day: "numeric",
3783
+ year: date.getFullYear() !== now.getFullYear() ? "numeric" : void 0
3784
+ });
3785
+ }
3786
+ function captureSentryError(error, context) {
3787
+ if (!Sentry__namespace.getClient()) return;
3788
+ const tags = {};
3789
+ if (context.executionId) tags.executionId = context.executionId;
3790
+ if (context.sessionId) tags.sessionId = context.sessionId;
3791
+ if (context.route) tags.route = context.route;
3792
+ if (context.cfRay) tags.cfRay = context.cfRay;
3793
+ if (context.customerId) tags.customerId = context.customerId;
3794
+ if (context.customerEmail) tags.customerEmail = context.customerEmail;
3795
+ const contexts = {
3796
+ chat_session: {
3797
+ sessionId: context.sessionId ?? null,
3798
+ sessionOwnerId: context.sessionOwnerId ?? null,
3799
+ executionId: context.executionId ?? null,
3800
+ agentId: context.agentId ?? null,
3801
+ cfRay: context.cfRay ?? null,
3802
+ customerId: context.customerId ?? null,
3803
+ customerEmail: context.customerEmail ?? null
3804
+ }
3805
+ };
3806
+ if (typeof error === "string") {
3807
+ Sentry__namespace.captureMessage(error, { level: "error", tags, contexts });
3808
+ } else {
3809
+ Sentry__namespace.captureException(error, { tags, contexts });
3810
+ }
3811
+ }
3812
+
3813
+ exports.PaymanChat = PaymanChat;
3814
+ exports.PaymanChatContext = PaymanChatContext;
3815
+ exports.UserActionStaleError = UserActionStaleError;
3816
+ exports.cancelUserAction = cancelUserAction;
3817
+ exports.captureSentryError = captureSentryError;
3818
+ exports.cn = cn;
3819
+ exports.formatDate = formatDate;
3820
+ exports.resendUserAction = resendUserAction;
3821
+ exports.submitUserAction = submitUserAction;
3822
+ exports.useChatV2 = useChatV2;
3823
+ exports.usePaymanChat = usePaymanChat;
3824
+ exports.useVoice = useVoice;
3825
+ //# sourceMappingURL=index.native.js.map
3826
+ //# sourceMappingURL=index.native.js.map