@qafka/react-native 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/CONTRIBUTING.md +92 -0
  3. package/LICENSE +22 -0
  4. package/README.md +109 -0
  5. package/SECURITY.md +67 -0
  6. package/android/build.gradle +35 -0
  7. package/android/src/main/AndroidManifest.xml +2 -0
  8. package/android/src/main/java/com/qafka/attestation/QafkaAttestationModule.kt +92 -0
  9. package/android/src/main/java/com/qafka/attestation/QafkaAttestationPackage.kt +22 -0
  10. package/android/src/main/java/com/qafka/audio/QafkaAudioModule.kt +290 -0
  11. package/android/src/main/java/com/qafka/clipboard/QafkaClipboardModule.kt +28 -0
  12. package/android/src/main/java/com/qafka/storage/QafkaStorageModule.kt +80 -0
  13. package/app.plugin.js +1 -0
  14. package/dist/QafkaSDK.d.ts +174 -0
  15. package/dist/QafkaSDK.js +461 -0
  16. package/dist/cards/bindings/resolveFieldName.d.ts +25 -0
  17. package/dist/cards/bindings/resolveFieldName.js +82 -0
  18. package/dist/cards/cta/CardContext.d.ts +16 -0
  19. package/dist/cards/cta/CardContext.js +58 -0
  20. package/dist/cards/cta/dispatcher.d.ts +7 -0
  21. package/dist/cards/cta/dispatcher.js +90 -0
  22. package/dist/cards/cta/types.d.ts +66 -0
  23. package/dist/cards/cta/types.js +2 -0
  24. package/dist/cards/index.d.ts +20 -0
  25. package/dist/cards/index.js +34 -0
  26. package/dist/cards/primitives/QButton.d.ts +10 -0
  27. package/dist/cards/primitives/QButton.js +115 -0
  28. package/dist/cards/primitives/QDivider.d.ts +7 -0
  29. package/dist/cards/primitives/QDivider.js +17 -0
  30. package/dist/cards/primitives/QIcon.d.ts +13 -0
  31. package/dist/cards/primitives/QIcon.js +26 -0
  32. package/dist/cards/primitives/QImage.d.ts +9 -0
  33. package/dist/cards/primitives/QImage.js +22 -0
  34. package/dist/cards/primitives/QText.d.ts +9 -0
  35. package/dist/cards/primitives/QText.js +30 -0
  36. package/dist/cards/primitives/QView.d.ts +8 -0
  37. package/dist/cards/primitives/QView.js +19 -0
  38. package/dist/cards/renderer/CardRenderer.d.ts +19 -0
  39. package/dist/cards/renderer/CardRenderer.js +64 -0
  40. package/dist/cards/renderer/renderNode.d.ts +13 -0
  41. package/dist/cards/renderer/renderNode.js +42 -0
  42. package/dist/cards/types.d.ts +110 -0
  43. package/dist/cards/types.js +6 -0
  44. package/dist/components/ActionResultBadge.d.ts +12 -0
  45. package/dist/components/ActionResultBadge.js +58 -0
  46. package/dist/components/ChatPage.d.ts +44 -0
  47. package/dist/components/ChatPage.js +84 -0
  48. package/dist/components/DataChip.d.ts +8 -0
  49. package/dist/components/DataChip.js +80 -0
  50. package/dist/components/DataChipList.d.ts +13 -0
  51. package/dist/components/DataChipList.js +21 -0
  52. package/dist/components/FloatingButton.d.ts +11 -0
  53. package/dist/components/FloatingButton.js +162 -0
  54. package/dist/components/InputArea.d.ts +57 -0
  55. package/dist/components/InputArea.js +142 -0
  56. package/dist/components/MarkdownText.d.ts +15 -0
  57. package/dist/components/MarkdownText.js +283 -0
  58. package/dist/components/MessageBubble.d.ts +134 -0
  59. package/dist/components/MessageBubble.js +384 -0
  60. package/dist/components/NavigationSuggestion.d.ts +11 -0
  61. package/dist/components/NavigationSuggestion.js +109 -0
  62. package/dist/components/Qafka.d.ts +39 -0
  63. package/dist/components/Qafka.handlers.d.ts +21 -0
  64. package/dist/components/Qafka.handlers.js +54 -0
  65. package/dist/components/Qafka.js +493 -0
  66. package/dist/components/Qafka.styles.d.ts +19 -0
  67. package/dist/components/Qafka.styles.js +101 -0
  68. package/dist/components/Qafka.types.d.ts +744 -0
  69. package/dist/components/Qafka.types.js +2 -0
  70. package/dist/components/Qafka.utils.d.ts +7 -0
  71. package/dist/components/Qafka.utils.js +34 -0
  72. package/dist/components/QafkaProvider.d.ts +12 -0
  73. package/dist/components/QafkaProvider.js +87 -0
  74. package/dist/components/QuickReplies.d.ts +14 -0
  75. package/dist/components/QuickReplies.js +48 -0
  76. package/dist/components/StepProgressIndicator.d.ts +12 -0
  77. package/dist/components/StepProgressIndicator.js +48 -0
  78. package/dist/components/SuggestionButton.d.ts +42 -0
  79. package/dist/components/SuggestionButton.js +67 -0
  80. package/dist/components/ToolStatusPill.d.ts +20 -0
  81. package/dist/components/ToolStatusPill.js +43 -0
  82. package/dist/components/TypingIndicator.d.ts +28 -0
  83. package/dist/components/TypingIndicator.js +109 -0
  84. package/dist/components/VoicePage.d.ts +48 -0
  85. package/dist/components/VoicePage.js +683 -0
  86. package/dist/components/defaults/DefaultCard.d.ts +14 -0
  87. package/dist/components/defaults/DefaultCard.js +156 -0
  88. package/dist/components/defaults/DefaultDetail.d.ts +14 -0
  89. package/dist/components/defaults/DefaultDetail.js +138 -0
  90. package/dist/components/defaults/DefaultList.d.ts +12 -0
  91. package/dist/components/defaults/DefaultList.js +98 -0
  92. package/dist/components/defaults/DefaultTable.d.ts +14 -0
  93. package/dist/components/defaults/DefaultTable.js +204 -0
  94. package/dist/components/defaults/index.d.ts +14 -0
  95. package/dist/components/defaults/index.js +25 -0
  96. package/dist/components/index.d.ts +22 -0
  97. package/dist/components/index.js +36 -0
  98. package/dist/constants.d.ts +10 -0
  99. package/dist/constants.js +13 -0
  100. package/dist/hooks/useChatMessages.d.ts +72 -0
  101. package/dist/hooks/useChatMessages.js +505 -0
  102. package/dist/hooks/useContextManager.d.ts +12 -0
  103. package/dist/hooks/useContextManager.js +46 -0
  104. package/dist/hooks/useProjectTheme.d.ts +19 -0
  105. package/dist/hooks/useProjectTheme.js +163 -0
  106. package/dist/hooks/useSDK.d.ts +31 -0
  107. package/dist/hooks/useSDK.js +103 -0
  108. package/dist/hooks/useVoiceChat.d.ts +110 -0
  109. package/dist/hooks/useVoiceChat.js +436 -0
  110. package/dist/index.d.ts +13 -0
  111. package/dist/index.js +59 -0
  112. package/dist/native/QafkaAttestation.d.ts +23 -0
  113. package/dist/native/QafkaAttestation.js +70 -0
  114. package/dist/native/QafkaAudio.d.ts +14 -0
  115. package/dist/native/QafkaAudio.js +31 -0
  116. package/dist/native/QafkaClipboard.d.ts +11 -0
  117. package/dist/native/QafkaClipboard.js +14 -0
  118. package/dist/native/QafkaStorage.d.ts +15 -0
  119. package/dist/native/QafkaStorage.js +12 -0
  120. package/dist/resolve-project-config.d.ts +35 -0
  121. package/dist/resolve-project-config.js +41 -0
  122. package/dist/runtime-config-loader.d.ts +37 -0
  123. package/dist/runtime-config-loader.js +53 -0
  124. package/dist/services/AttestationManager.d.ts +38 -0
  125. package/dist/services/AttestationManager.js +296 -0
  126. package/dist/services/BackendService.d.ts +156 -0
  127. package/dist/services/BackendService.js +755 -0
  128. package/dist/services/ConversationManager.d.ts +43 -0
  129. package/dist/services/ConversationManager.js +96 -0
  130. package/dist/services/NavigationHandler.d.ts +29 -0
  131. package/dist/services/NavigationHandler.js +70 -0
  132. package/dist/services/RealtimeService.d.ts +83 -0
  133. package/dist/services/RealtimeService.js +203 -0
  134. package/dist/services/storage.d.ts +11 -0
  135. package/dist/services/storage.js +15 -0
  136. package/dist/services/storageCore.d.ts +17 -0
  137. package/dist/services/storageCore.js +46 -0
  138. package/dist/themes/dark.d.ts +5 -0
  139. package/dist/themes/dark.js +129 -0
  140. package/dist/themes/index.d.ts +12 -0
  141. package/dist/themes/index.js +33 -0
  142. package/dist/themes/light.d.ts +5 -0
  143. package/dist/themes/light.js +129 -0
  144. package/dist/themes/types.d.ts +155 -0
  145. package/dist/themes/types.js +5 -0
  146. package/dist/types/chat.d.ts +126 -0
  147. package/dist/types/chat.js +5 -0
  148. package/dist/types/components.d.ts +56 -0
  149. package/dist/types/components.js +16 -0
  150. package/dist/types/external-navigation.d.ts +19 -0
  151. package/dist/types/external-navigation.js +8 -0
  152. package/dist/types/index.d.ts +9 -0
  153. package/dist/types/index.js +25 -0
  154. package/dist/types/navigation.d.ts +86 -0
  155. package/dist/types/navigation.js +5 -0
  156. package/dist/types/sdk.d.ts +36 -0
  157. package/dist/types/sdk.js +5 -0
  158. package/dist/utils/deepMerge.d.ts +46 -0
  159. package/dist/utils/deepMerge.js +70 -0
  160. package/dist/utils/fontUtils.d.ts +8 -0
  161. package/dist/utils/fontUtils.js +16 -0
  162. package/dist/validate-end-user.d.ts +18 -0
  163. package/dist/validate-end-user.js +74 -0
  164. package/expo-plugin/withQafkaAttestation.js +57 -0
  165. package/ios/QafkaAttestation.m +25 -0
  166. package/ios/QafkaAttestation.swift +128 -0
  167. package/ios/QafkaAudio.m +23 -0
  168. package/ios/QafkaAudio.swift +519 -0
  169. package/ios/QafkaClipboard.m +10 -0
  170. package/ios/QafkaClipboard.swift +21 -0
  171. package/ios/QafkaReactImports.h +2 -0
  172. package/ios/QafkaStorage.m +26 -0
  173. package/ios/QafkaStorage.swift +118 -0
  174. package/package.json +82 -0
  175. package/qafka.config.d.ts +9 -0
  176. package/qafka.config.js +9 -0
  177. package/react-native-qafka.podspec +28 -0
  178. package/react-native.config.js +14 -0
@@ -0,0 +1,505 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useChatMessages = void 0;
4
+ const react_1 = require("react");
5
+ const QafkaSDK_1 = require("../QafkaSDK");
6
+ const Qafka_utils_1 = require("../components/Qafka.utils");
7
+ /**
8
+ * Custom hook for managing chat messages and sending
9
+ */
10
+ const useChatMessages = ({ enableStreaming = true, context = {}, contextDescription, onMessageSent, onResponseReceived, onError, onNavigationSuggest, onToolSuggested, onActionResult, onStepCompleted, onFileUploadRequest, onExtractionResult, navigationLabelFormat, }) => {
11
+ const [messages, setMessages] = (0, react_1.useState)([]);
12
+ const [isTyping, setIsTyping] = (0, react_1.useState)(false);
13
+ const [isSending, setIsSending] = (0, react_1.useState)(false);
14
+ const [streamingMessage, setStreamingMessage] = (0, react_1.useState)('');
15
+ // active tool status pill
16
+ const [toolStatus, setToolStatus] = (0, react_1.useState)(null);
17
+ const flatListRef = (0, react_1.useRef)(null);
18
+ // Keep pending tool responses until AI message arrives
19
+ const pendingToolResponsesRef = (0, react_1.useRef)([]);
20
+ // accumulate `final_chunk` content across a single turn.
21
+ // `response.text` from the `done` event only contains what the backend
22
+ // streamed as `content` (the turn 1 bridge). `final_chunk` events carry the
23
+ // turn 2 / missing-params / error-path reply separately. We buffer them here
24
+ // and merge into the final assistant message in onComplete so the user sees
25
+ // ONE bubble with bridge + final, not two out-of-order bubbles.
26
+ const finalChunkBufferRef = (0, react_1.useRef)('');
27
+ // capture backend messageId and conversationId for the managed
28
+ // follow-up round-trip. messageId comes from the `done` event.
29
+ // conversationId currently only surfaces on the `file_uploaded` SSE event;
30
+ // if unavailable, the follow-up POST is skipped gracefully.
31
+ const lastMessageIdRef = (0, react_1.useRef)(null);
32
+ const lastConversationIdRef = (0, react_1.useRef)(null);
33
+ // guard against double-fire of postToolResult per toolKey
34
+ const inFlightPostToolResultRef = (0, react_1.useRef)(new Set());
35
+ const scrollToBottom = (animated = true) => {
36
+ setTimeout(() => {
37
+ flatListRef.current?.scrollToEnd({ animated });
38
+ }, 100);
39
+ };
40
+ const handleSend = async (message) => {
41
+ if (isSending)
42
+ return;
43
+ setIsSending(true);
44
+ setIsTyping(true);
45
+ setStreamingMessage('');
46
+ finalChunkBufferRef.current = '';
47
+ // Add user message
48
+ const userMessage = {
49
+ id: Date.now().toString(),
50
+ role: 'user',
51
+ text: message,
52
+ content: message,
53
+ timestamp: new Date(),
54
+ };
55
+ setMessages((prev) => [...prev, userMessage]);
56
+ onMessageSent?.(message);
57
+ scrollToBottom();
58
+ const messageToSendAI = contextDescription
59
+ ? `${contextDescription}\n\n${message}`
60
+ : message;
61
+ try {
62
+ const sdk = QafkaSDK_1.QafkaSDK.getInstance();
63
+ if (enableStreaming) {
64
+ let firstChunkReceived = false;
65
+ await sdk.sendMessageStream(messageToSendAI,
66
+ // onChunk
67
+ (chunk) => {
68
+ // Track first chunk silently
69
+ if (!firstChunkReceived) {
70
+ firstChunkReceived = true;
71
+ }
72
+ // 🔧 Append each incoming chunk to the accumulated message
73
+ setStreamingMessage((prevMessage) => {
74
+ return prevMessage + chunk;
75
+ });
76
+ scrollToBottom();
77
+ },
78
+ // onComplete
79
+ (response) => {
80
+ setIsTyping(false);
81
+ setStreamingMessage('');
82
+ // capture messageId for the managed follow-up round-trip
83
+ if (response.id) {
84
+ lastMessageIdRef.current = response.id;
85
+ }
86
+ const actionButtons = (0, Qafka_utils_1.buildActionButtons)(response, navigationLabelFormat);
87
+ const bridgeMessage = (response.text || '').trim();
88
+ // merge turn 2 / missing-params reply from
89
+ // finalChunkBufferRef into the same assistant bubble so the user
90
+ // sees bridge + final reply as ONE message (not two).
91
+ const finalChunkBuffered = finalChunkBufferRef.current;
92
+ finalChunkBufferRef.current = '';
93
+ const finalMessage = finalChunkBuffered
94
+ ? bridgeMessage
95
+ ? `${bridgeMessage}\n\n${finalChunkBuffered}`
96
+ : finalChunkBuffered
97
+ : bridgeMessage;
98
+ const aiMessage = {
99
+ id: response.id,
100
+ role: 'assistant',
101
+ text: finalMessage,
102
+ content: finalMessage,
103
+ timestamp: new Date(response.timestamp),
104
+ actions: actionButtons.length > 0 ? actionButtons : undefined,
105
+ externalSuggestions: response.externalSuggestions && response.externalSuggestions.length > 0
106
+ ? response.externalSuggestions
107
+ : undefined,
108
+ metadata: {},
109
+ };
110
+ setMessages((prev) => [...prev, aiMessage]);
111
+ onResponseReceived?.(response);
112
+ // Process pending tool responses now that AI message has arrived
113
+ if (pendingToolResponsesRef.current.length > 0) {
114
+ pendingToolResponsesRef.current.forEach(({ toolKey, data, tool }) => {
115
+ const position = tool?.response?.position || 'after';
116
+ // if the tool has an embedded card template,
117
+ // build the envelope client-side. This works for ALL execution
118
+ // modes (client-side, server, server-managed); the latter also
119
+ // has a redundant SSE `card` event but the registry-embedded
120
+ // template wins because it arrives first.
121
+ const cardEnvelope = tool?.cardTemplate?.isActive
122
+ ? {
123
+ templateId: tool.cardTemplate.id,
124
+ templateSlug: tool.cardTemplate.slug,
125
+ definition: tool.cardTemplate.definition,
126
+ data,
127
+ }
128
+ : undefined;
129
+ if (position === 'before') {
130
+ // Add tool message BEFORE the AI message we just added
131
+ setMessages((prev) => {
132
+ const toolMessage = {
133
+ id: Date.now().toString() + '_tool',
134
+ role: 'assistant',
135
+ text: '',
136
+ content: '',
137
+ timestamp: new Date(),
138
+ toolResponse: { toolKey, data, tool },
139
+ ...(cardEnvelope && { card: cardEnvelope }),
140
+ };
141
+ // Insert before last message (which is the AI message)
142
+ return [
143
+ ...prev.slice(0, -1),
144
+ toolMessage,
145
+ prev[prev.length - 1],
146
+ ];
147
+ });
148
+ }
149
+ else {
150
+ // Merge tool response with the AI message we just added
151
+ setMessages((prev) => {
152
+ const lastMessage = prev[prev.length - 1];
153
+ const updatedMessage = {
154
+ ...lastMessage,
155
+ toolResponse: { toolKey, data, tool },
156
+ ...(cardEnvelope && { card: cardEnvelope }),
157
+ };
158
+ return [...prev.slice(0, -1), updatedMessage];
159
+ });
160
+ }
161
+ const sdk = QafkaSDK_1.QafkaSDK.getInstance();
162
+ sdk.addToolResponse(toolKey, data, tool);
163
+ });
164
+ // Clear pending queue
165
+ pendingToolResponsesRef.current = [];
166
+ // Scroll after tool responses are processed
167
+ scrollToBottom();
168
+ }
169
+ // Handle navigation suggestion
170
+ if (response.navigationSuggestion && onNavigationSuggest) {
171
+ onNavigationSuggest(response.navigationSuggestion);
172
+ }
173
+ setIsSending(false);
174
+ },
175
+ // onError
176
+ (err) => {
177
+ handleError(err);
178
+ }, context, contextDescription,
179
+ // onToolSuggested
180
+ async (tools, conversationId, messageId) => {
181
+ if (conversationId) {
182
+ lastConversationIdRef.current = conversationId;
183
+ }
184
+ if (messageId) {
185
+ lastMessageIdRef.current = messageId;
186
+ }
187
+ if (onToolSuggested) {
188
+ // Tracks tools the consumer's `onToolSuggested` handler explicitly
189
+ // called `addResponse` for. The auto-fire loop below uses this — NOT
190
+ // `pendingToolResponsesRef` — to decide whether to inject an empty
191
+ // payload. Why: when the consumer's handler is async and the assistant
192
+ // message lands before its `await` resolves, `addToolResponseToWidget`
193
+ // attaches the real data AND drains it from the pending queue
194
+ // (see filter at the start of `setMessages` below). The auto-fire then
195
+ // observes `!alreadyPending` and overwrites the consumer's payload
196
+ // with `{}`. Tracking handled-by-consumer separately keeps the two
197
+ // signals from collapsing into one.
198
+ const consumerHandledKeys = new Set();
199
+ const addToolResponseToWidget = (data, tool, mode = 'both') => {
200
+ const toolKey = tool?.key ?? tool?.name ?? '';
201
+ const updatesUI = mode === 'ui' || mode === 'both';
202
+ const commits = mode === 'data' || mode === 'both';
203
+ consumerHandledKeys.add(toolKey);
204
+ // 🔧 Always add to pending queue first (outside setMessages to avoid batching issues)
205
+ if (updatesUI) {
206
+ pendingToolResponsesRef.current.push({
207
+ toolKey,
208
+ data,
209
+ tool,
210
+ });
211
+ // Merge tool response into the last assistant message. For
212
+ // multi-step managed flows, each step appends content
213
+ // to the SAME assistant message via final_chunk streaming —
214
+ // and emits a fresh tool suggestion. Without replacing the
215
+ // toolResponse here, the UI stays frozen on step 1's data.
216
+ // Always overwrite with the latest data; voice handles the
217
+ // equivalent via renderedTools upsert by toolKey.
218
+ setMessages((prev) => {
219
+ const lastMsg = prev[prev.length - 1];
220
+ if (lastMsg && lastMsg.role === 'assistant') {
221
+ pendingToolResponsesRef.current =
222
+ pendingToolResponsesRef.current.filter((p) => p.toolKey !== toolKey);
223
+ // attach the card envelope when the tool's
224
+ // registry entry includes a card template.
225
+ const cardEnvelope = tool?.cardTemplate?.isActive
226
+ ? {
227
+ templateId: tool.cardTemplate.id,
228
+ templateSlug: tool.cardTemplate.slug,
229
+ definition: tool.cardTemplate.definition,
230
+ data,
231
+ }
232
+ : undefined;
233
+ const updatedMsg = {
234
+ ...lastMsg,
235
+ toolResponse: { toolKey, data, tool },
236
+ ...(cardEnvelope && { card: cardEnvelope }),
237
+ };
238
+ return [...prev.slice(0, -1), updatedMsg];
239
+ }
240
+ return prev; // No assistant message yet; pending queue handles it in onComplete
241
+ });
242
+ // Register in SDK
243
+ const sdk = QafkaSDK_1.QafkaSDK.getInstance();
244
+ sdk.addToolResponse(toolKey, data, tool);
245
+ }
246
+ // Custom-with-AI round-trip
247
+ // In managed follow-up mode the SDK should POST the
248
+ // tool result back to the backend, which runs a second LLM turn
249
+ // and streams a `final_chunk` reply.
250
+ if (commits && tool?.executionMode === 'custom-with-ai') {
251
+ const conversationId = lastConversationIdRef.current;
252
+ const messageId = lastMessageIdRef.current;
253
+ if (conversationId && messageId) {
254
+ // Guard: prevent double-fire of postToolResult for the
255
+ // SAME (messageId, toolKey) pair. Multi-step managed
256
+ // tools fire the same toolKey across multiple messages —
257
+ // those are legitimately separate calls and must not be
258
+ // blocked. Keying by messageId+toolKey only blocks the
259
+ // genuine double-fire (developer calling addResponse twice
260
+ // for one tool suggestion).
261
+ const inFlightKey = `${messageId}:${toolKey}`;
262
+ if (inFlightPostToolResultRef.current.has(inFlightKey)) {
263
+ if (__DEV__) {
264
+ console.warn('[Qafka] postToolResult already in-flight for', inFlightKey);
265
+ }
266
+ }
267
+ else {
268
+ inFlightPostToolResultRef.current.add(inFlightKey);
269
+ setToolStatus({
270
+ toolKey,
271
+ message: tool.loadingMessage || 'Loading…',
272
+ stage: 'processing',
273
+ });
274
+ sdk.postToolResult({
275
+ conversationId,
276
+ messageId,
277
+ toolKey,
278
+ data,
279
+ // Forward the Qafka <context> prop so turn-2 has
280
+ // the same runtime context as turn-1.
281
+ sdkContext: context,
282
+ }, (content) => {
283
+ setToolStatus(null);
284
+ setMessages((prev) => {
285
+ const lastMsg = prev[prev.length - 1];
286
+ if (lastMsg && lastMsg.role === 'assistant') {
287
+ const nextText = (lastMsg.text || '') + content;
288
+ return [
289
+ ...prev.slice(0, -1),
290
+ {
291
+ ...lastMsg,
292
+ text: nextText,
293
+ content: nextText,
294
+ },
295
+ ];
296
+ }
297
+ return prev;
298
+ });
299
+ }, () => {
300
+ setToolStatus(null);
301
+ inFlightPostToolResultRef.current.delete(inFlightKey);
302
+ }, (err) => {
303
+ setToolStatus(null);
304
+ inFlightPostToolResultRef.current.delete(inFlightKey);
305
+ if (__DEV__) {
306
+ console.warn('[Qafka] postToolResult error:', err.message);
307
+ }
308
+ },
309
+ // Card Template: attach card envelope to last assistant message
310
+ (cardEnvelope) => {
311
+ setMessages((prev) => {
312
+ const lastMsg = prev[prev.length - 1];
313
+ if (lastMsg && lastMsg.role === 'assistant') {
314
+ return [
315
+ ...prev.slice(0, -1),
316
+ { ...lastMsg, card: cardEnvelope },
317
+ ];
318
+ }
319
+ return prev;
320
+ });
321
+ }).catch(() => {
322
+ setToolStatus(null);
323
+ inFlightPostToolResultRef.current.delete(inFlightKey);
324
+ });
325
+ }
326
+ }
327
+ else if (__DEV__) {
328
+ console.warn('[Qafka] Skipping tool round-trip: missing conversationId or messageId');
329
+ }
330
+ }
331
+ };
332
+ // Important: wait for tool processing before continuing
333
+ await onToolSuggested(tools, addToolResponseToWidget);
334
+ // Auto-trigger UI for uiConfig-only tools (no endpoint/action/fileInput).
335
+ // Skips the managed follow-up mode: those tools' contract is that the consumer
336
+ // handler computes the data (often async, e.g. fetching cities)
337
+ // and then calls addResponse with it. Auto-firing with `{}` here
338
+ // races against the consumer's async handler — its empty payload
339
+ // wins the in-flight postToolResult guard, and the real data
340
+ // arrives too late, leaving turn-2 LLM narrating an empty result.
341
+ //
342
+ // Gate on `consumerHandledKeys`, not `pendingToolResponsesRef`: when the
343
+ // consumer's async handler resolves AFTER the assistant message has
344
+ // landed, `addToolResponseToWidget` attaches the real data and removes
345
+ // the entry from pending — so a pending-only check would (wrongly) see
346
+ // the slot as empty and overwrite the consumer's payload with `{}`.
347
+ for (const tool of tools) {
348
+ if (tool?.executionMode === 'custom-with-ai')
349
+ continue;
350
+ const m = tool.modules || {};
351
+ if (tool.uiConfig && !m.hasEndpoint && !m.hasActions && !m.hasFileInput) {
352
+ if (!consumerHandledKeys.has(tool.key)) {
353
+ addToolResponseToWidget({}, tool);
354
+ }
355
+ }
356
+ }
357
+ }
358
+ }, undefined, // isInitialMessage
359
+ // onActionResult — attach to message + forward to user callback
360
+ (results) => {
361
+ setMessages((prev) => {
362
+ const lastMsg = prev[prev.length - 1];
363
+ if (lastMsg && lastMsg.role === 'assistant') {
364
+ return [
365
+ ...prev.slice(0, -1),
366
+ { ...lastMsg, actionResults: [...(lastMsg.actionResults || []), ...results] },
367
+ ];
368
+ }
369
+ return prev;
370
+ });
371
+ onActionResult?.(results);
372
+ },
373
+ // onStepCompleted — attach to message + forward to user callback
374
+ (result) => {
375
+ setMessages((prev) => {
376
+ const lastMsg = prev[prev.length - 1];
377
+ if (lastMsg && lastMsg.role === 'assistant') {
378
+ return [
379
+ ...prev.slice(0, -1),
380
+ {
381
+ ...lastMsg,
382
+ completedSteps: [
383
+ ...(lastMsg.completedSteps || []),
384
+ { tool: result.tool, step: result.step, data: result.data },
385
+ ],
386
+ },
387
+ ];
388
+ }
389
+ return prev;
390
+ });
391
+ onStepCompleted?.(result);
392
+ },
393
+ // onFileUploadRequest — also capture conversationId if present
394
+ (request) => {
395
+ if (request && typeof request.conversationId === 'string') {
396
+ lastConversationIdRef.current = request.conversationId;
397
+ }
398
+ onFileUploadRequest?.(request);
399
+ },
400
+ // onExtractionResult
401
+ onExtractionResult,
402
+ // onToolStatus: show pill
403
+ (status) => {
404
+ setToolStatus(status);
405
+ },
406
+ // onToolResultPayload: merge into last AI message
407
+ (payload) => {
408
+ setToolStatus(null);
409
+ setMessages((prev) => {
410
+ const lastMsg = prev[prev.length - 1];
411
+ if (lastMsg && lastMsg.role === 'assistant') {
412
+ return [
413
+ ...prev.slice(0, -1),
414
+ {
415
+ ...lastMsg,
416
+ toolResponse: {
417
+ toolKey: payload.toolKey,
418
+ data: payload.data,
419
+ tool: {
420
+ key: payload.toolKey,
421
+ uiConfig: payload.uiConfig,
422
+ },
423
+ },
424
+ },
425
+ ];
426
+ }
427
+ return prev;
428
+ });
429
+ },
430
+ // onFinalChunk: accumulate into finalChunkBufferRef +
431
+ // mirror into streamingMessage so the user sees it live. onComplete
432
+ // merges buffer with response.text so one assistant bubble gets the
433
+ // full text (bridge + turn 2 reply) instead of two separate ones.
434
+ (content) => {
435
+ setToolStatus(null);
436
+ finalChunkBufferRef.current +=
437
+ (finalChunkBufferRef.current ? '\n\n' : '') + content;
438
+ setStreamingMessage((prev) => {
439
+ const sep = prev && !prev.endsWith('\n') ? '\n\n' : '';
440
+ return prev + sep + content;
441
+ });
442
+ });
443
+ }
444
+ else {
445
+ // Non-streaming
446
+ const response = await sdk.sendMessage(message, context, contextDescription);
447
+ const aiMessage = {
448
+ id: response.id,
449
+ role: 'assistant',
450
+ text: response.text || '',
451
+ content: response.text || '',
452
+ timestamp: new Date(response.timestamp),
453
+ externalSuggestions: response.externalSuggestions && response.externalSuggestions.length > 0
454
+ ? response.externalSuggestions
455
+ : undefined,
456
+ metadata: {},
457
+ };
458
+ setMessages((prev) => [...prev, aiMessage]);
459
+ onResponseReceived?.(response);
460
+ if (response.navigationSuggestion && onNavigationSuggest) {
461
+ onNavigationSuggest(response.navigationSuggestion);
462
+ }
463
+ scrollToBottom();
464
+ setIsTyping(false);
465
+ setIsSending(false);
466
+ }
467
+ }
468
+ catch (err) {
469
+ handleError(err);
470
+ }
471
+ };
472
+ const handleError = (err) => {
473
+ setIsTyping(false);
474
+ setStreamingMessage('');
475
+ setIsSending(false);
476
+ setToolStatus(null);
477
+ finalChunkBufferRef.current = '';
478
+ const errorMessage = err.message || 'Unknown error';
479
+ onError?.(err instanceof Error ? err : new Error(errorMessage));
480
+ // Default fallback bubble. Apps that want a localized or custom error
481
+ // surface should handle the `onError` callback above and suppress this
482
+ // bubble in their own UI (e.g. by clearing messages on error).
483
+ const errorMsg = {
484
+ id: Date.now().toString(),
485
+ role: 'assistant',
486
+ text: '⚠️ Something went wrong.',
487
+ content: '⚠️ Something went wrong.',
488
+ timestamp: new Date(),
489
+ };
490
+ setMessages((prev) => [...prev, errorMsg]);
491
+ };
492
+ return {
493
+ messages,
494
+ isTyping,
495
+ isSending,
496
+ streamingMessage,
497
+ toolStatus,
498
+ flatListRef,
499
+ handleSend,
500
+ setMessages,
501
+ setIsTyping,
502
+ setStreamingMessage,
503
+ };
504
+ };
505
+ exports.useChatMessages = useChatMessages;
@@ -0,0 +1,12 @@
1
+ export interface UseContextManagerOptions {
2
+ initialContext?: Record<string, any>;
3
+ debounceMs?: number;
4
+ }
5
+ export interface UseContextManagerResult {
6
+ currentContext: Record<string, any>;
7
+ updateContext: (newContext: Record<string, any>) => void;
8
+ }
9
+ /**
10
+ * Custom hook for managing context with debouncing
11
+ */
12
+ export declare const useContextManager: ({ initialContext, debounceMs, }?: UseContextManagerOptions) => UseContextManagerResult;
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useContextManager = void 0;
4
+ const react_1 = require("react");
5
+ /**
6
+ * Custom hook for managing context with debouncing
7
+ */
8
+ const useContextManager = ({ initialContext = {}, debounceMs = 500, } = {}) => {
9
+ const [currentContext, setCurrentContext] = (0, react_1.useState)(initialContext);
10
+ const debounceTimer = (0, react_1.useRef)(null);
11
+ const updateContext = (newContext) => {
12
+ // Clear existing timer
13
+ if (debounceTimer.current) {
14
+ clearTimeout(debounceTimer.current);
15
+ }
16
+ // Set new timer
17
+ debounceTimer.current = setTimeout(() => {
18
+ setCurrentContext(newContext);
19
+ }, debounceMs);
20
+ };
21
+ // Cleanup on unmount
22
+ (0, react_1.useEffect)(() => {
23
+ return () => {
24
+ if (debounceTimer.current) {
25
+ clearTimeout(debounceTimer.current);
26
+ }
27
+ };
28
+ }, []);
29
+ // Handle external context updates
30
+ (0, react_1.useEffect)(() => {
31
+ if (initialContext) {
32
+ // Use JSON.stringify for deep comparison to avoid infinite loops
33
+ // when a new object reference is passed with the same content
34
+ const prevContextStr = JSON.stringify(currentContext);
35
+ const newContextStr = JSON.stringify(initialContext);
36
+ if (prevContextStr !== newContextStr) {
37
+ updateContext(initialContext);
38
+ }
39
+ }
40
+ }, [initialContext, currentContext]);
41
+ return {
42
+ currentContext,
43
+ updateContext,
44
+ };
45
+ };
46
+ exports.useContextManager = useContextManager;
@@ -0,0 +1,19 @@
1
+ import { Theme } from '../themes';
2
+ import { ThemeOverride } from '../themes/types';
3
+ interface UseProjectThemeOptions {
4
+ sdkReady: boolean;
5
+ themeName: 'light' | 'dark' | Theme;
6
+ customTheme?: Theme;
7
+ themeOverride?: ThemeOverride;
8
+ subProjectId?: string;
9
+ }
10
+ interface UseProjectThemeResult {
11
+ theme: Theme;
12
+ isLoadingTheme: boolean;
13
+ greeting: string | null;
14
+ initialMessage: string | null;
15
+ isInitialMessageEnabled: boolean;
16
+ voiceEnabled: boolean;
17
+ }
18
+ export declare function useProjectTheme({ sdkReady, themeName, customTheme, themeOverride, subProjectId, }: UseProjectThemeOptions): UseProjectThemeResult;
19
+ export {};