@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.
- package/CHANGELOG.md +12 -0
- package/CONTRIBUTING.md +92 -0
- package/LICENSE +22 -0
- package/README.md +109 -0
- package/SECURITY.md +67 -0
- package/android/build.gradle +35 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/qafka/attestation/QafkaAttestationModule.kt +92 -0
- package/android/src/main/java/com/qafka/attestation/QafkaAttestationPackage.kt +22 -0
- package/android/src/main/java/com/qafka/audio/QafkaAudioModule.kt +290 -0
- package/android/src/main/java/com/qafka/clipboard/QafkaClipboardModule.kt +28 -0
- package/android/src/main/java/com/qafka/storage/QafkaStorageModule.kt +80 -0
- package/app.plugin.js +1 -0
- package/dist/QafkaSDK.d.ts +174 -0
- package/dist/QafkaSDK.js +461 -0
- package/dist/cards/bindings/resolveFieldName.d.ts +25 -0
- package/dist/cards/bindings/resolveFieldName.js +82 -0
- package/dist/cards/cta/CardContext.d.ts +16 -0
- package/dist/cards/cta/CardContext.js +58 -0
- package/dist/cards/cta/dispatcher.d.ts +7 -0
- package/dist/cards/cta/dispatcher.js +90 -0
- package/dist/cards/cta/types.d.ts +66 -0
- package/dist/cards/cta/types.js +2 -0
- package/dist/cards/index.d.ts +20 -0
- package/dist/cards/index.js +34 -0
- package/dist/cards/primitives/QButton.d.ts +10 -0
- package/dist/cards/primitives/QButton.js +115 -0
- package/dist/cards/primitives/QDivider.d.ts +7 -0
- package/dist/cards/primitives/QDivider.js +17 -0
- package/dist/cards/primitives/QIcon.d.ts +13 -0
- package/dist/cards/primitives/QIcon.js +26 -0
- package/dist/cards/primitives/QImage.d.ts +9 -0
- package/dist/cards/primitives/QImage.js +22 -0
- package/dist/cards/primitives/QText.d.ts +9 -0
- package/dist/cards/primitives/QText.js +30 -0
- package/dist/cards/primitives/QView.d.ts +8 -0
- package/dist/cards/primitives/QView.js +19 -0
- package/dist/cards/renderer/CardRenderer.d.ts +19 -0
- package/dist/cards/renderer/CardRenderer.js +64 -0
- package/dist/cards/renderer/renderNode.d.ts +13 -0
- package/dist/cards/renderer/renderNode.js +42 -0
- package/dist/cards/types.d.ts +110 -0
- package/dist/cards/types.js +6 -0
- package/dist/components/ActionResultBadge.d.ts +12 -0
- package/dist/components/ActionResultBadge.js +58 -0
- package/dist/components/ChatPage.d.ts +44 -0
- package/dist/components/ChatPage.js +84 -0
- package/dist/components/DataChip.d.ts +8 -0
- package/dist/components/DataChip.js +80 -0
- package/dist/components/DataChipList.d.ts +13 -0
- package/dist/components/DataChipList.js +21 -0
- package/dist/components/FloatingButton.d.ts +11 -0
- package/dist/components/FloatingButton.js +162 -0
- package/dist/components/InputArea.d.ts +57 -0
- package/dist/components/InputArea.js +142 -0
- package/dist/components/MarkdownText.d.ts +15 -0
- package/dist/components/MarkdownText.js +283 -0
- package/dist/components/MessageBubble.d.ts +134 -0
- package/dist/components/MessageBubble.js +384 -0
- package/dist/components/NavigationSuggestion.d.ts +11 -0
- package/dist/components/NavigationSuggestion.js +109 -0
- package/dist/components/Qafka.d.ts +39 -0
- package/dist/components/Qafka.handlers.d.ts +21 -0
- package/dist/components/Qafka.handlers.js +54 -0
- package/dist/components/Qafka.js +493 -0
- package/dist/components/Qafka.styles.d.ts +19 -0
- package/dist/components/Qafka.styles.js +101 -0
- package/dist/components/Qafka.types.d.ts +744 -0
- package/dist/components/Qafka.types.js +2 -0
- package/dist/components/Qafka.utils.d.ts +7 -0
- package/dist/components/Qafka.utils.js +34 -0
- package/dist/components/QafkaProvider.d.ts +12 -0
- package/dist/components/QafkaProvider.js +87 -0
- package/dist/components/QuickReplies.d.ts +14 -0
- package/dist/components/QuickReplies.js +48 -0
- package/dist/components/StepProgressIndicator.d.ts +12 -0
- package/dist/components/StepProgressIndicator.js +48 -0
- package/dist/components/SuggestionButton.d.ts +42 -0
- package/dist/components/SuggestionButton.js +67 -0
- package/dist/components/ToolStatusPill.d.ts +20 -0
- package/dist/components/ToolStatusPill.js +43 -0
- package/dist/components/TypingIndicator.d.ts +28 -0
- package/dist/components/TypingIndicator.js +109 -0
- package/dist/components/VoicePage.d.ts +48 -0
- package/dist/components/VoicePage.js +683 -0
- package/dist/components/defaults/DefaultCard.d.ts +14 -0
- package/dist/components/defaults/DefaultCard.js +156 -0
- package/dist/components/defaults/DefaultDetail.d.ts +14 -0
- package/dist/components/defaults/DefaultDetail.js +138 -0
- package/dist/components/defaults/DefaultList.d.ts +12 -0
- package/dist/components/defaults/DefaultList.js +98 -0
- package/dist/components/defaults/DefaultTable.d.ts +14 -0
- package/dist/components/defaults/DefaultTable.js +204 -0
- package/dist/components/defaults/index.d.ts +14 -0
- package/dist/components/defaults/index.js +25 -0
- package/dist/components/index.d.ts +22 -0
- package/dist/components/index.js +36 -0
- package/dist/constants.d.ts +10 -0
- package/dist/constants.js +13 -0
- package/dist/hooks/useChatMessages.d.ts +72 -0
- package/dist/hooks/useChatMessages.js +505 -0
- package/dist/hooks/useContextManager.d.ts +12 -0
- package/dist/hooks/useContextManager.js +46 -0
- package/dist/hooks/useProjectTheme.d.ts +19 -0
- package/dist/hooks/useProjectTheme.js +163 -0
- package/dist/hooks/useSDK.d.ts +31 -0
- package/dist/hooks/useSDK.js +103 -0
- package/dist/hooks/useVoiceChat.d.ts +110 -0
- package/dist/hooks/useVoiceChat.js +436 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +59 -0
- package/dist/native/QafkaAttestation.d.ts +23 -0
- package/dist/native/QafkaAttestation.js +70 -0
- package/dist/native/QafkaAudio.d.ts +14 -0
- package/dist/native/QafkaAudio.js +31 -0
- package/dist/native/QafkaClipboard.d.ts +11 -0
- package/dist/native/QafkaClipboard.js +14 -0
- package/dist/native/QafkaStorage.d.ts +15 -0
- package/dist/native/QafkaStorage.js +12 -0
- package/dist/resolve-project-config.d.ts +35 -0
- package/dist/resolve-project-config.js +41 -0
- package/dist/runtime-config-loader.d.ts +37 -0
- package/dist/runtime-config-loader.js +53 -0
- package/dist/services/AttestationManager.d.ts +38 -0
- package/dist/services/AttestationManager.js +296 -0
- package/dist/services/BackendService.d.ts +156 -0
- package/dist/services/BackendService.js +755 -0
- package/dist/services/ConversationManager.d.ts +43 -0
- package/dist/services/ConversationManager.js +96 -0
- package/dist/services/NavigationHandler.d.ts +29 -0
- package/dist/services/NavigationHandler.js +70 -0
- package/dist/services/RealtimeService.d.ts +83 -0
- package/dist/services/RealtimeService.js +203 -0
- package/dist/services/storage.d.ts +11 -0
- package/dist/services/storage.js +15 -0
- package/dist/services/storageCore.d.ts +17 -0
- package/dist/services/storageCore.js +46 -0
- package/dist/themes/dark.d.ts +5 -0
- package/dist/themes/dark.js +129 -0
- package/dist/themes/index.d.ts +12 -0
- package/dist/themes/index.js +33 -0
- package/dist/themes/light.d.ts +5 -0
- package/dist/themes/light.js +129 -0
- package/dist/themes/types.d.ts +155 -0
- package/dist/themes/types.js +5 -0
- package/dist/types/chat.d.ts +126 -0
- package/dist/types/chat.js +5 -0
- package/dist/types/components.d.ts +56 -0
- package/dist/types/components.js +16 -0
- package/dist/types/external-navigation.d.ts +19 -0
- package/dist/types/external-navigation.js +8 -0
- package/dist/types/index.d.ts +9 -0
- package/dist/types/index.js +25 -0
- package/dist/types/navigation.d.ts +86 -0
- package/dist/types/navigation.js +5 -0
- package/dist/types/sdk.d.ts +36 -0
- package/dist/types/sdk.js +5 -0
- package/dist/utils/deepMerge.d.ts +46 -0
- package/dist/utils/deepMerge.js +70 -0
- package/dist/utils/fontUtils.d.ts +8 -0
- package/dist/utils/fontUtils.js +16 -0
- package/dist/validate-end-user.d.ts +18 -0
- package/dist/validate-end-user.js +74 -0
- package/expo-plugin/withQafkaAttestation.js +57 -0
- package/ios/QafkaAttestation.m +25 -0
- package/ios/QafkaAttestation.swift +128 -0
- package/ios/QafkaAudio.m +23 -0
- package/ios/QafkaAudio.swift +519 -0
- package/ios/QafkaClipboard.m +10 -0
- package/ios/QafkaClipboard.swift +21 -0
- package/ios/QafkaReactImports.h +2 -0
- package/ios/QafkaStorage.m +26 -0
- package/ios/QafkaStorage.swift +118 -0
- package/package.json +82 -0
- package/qafka.config.d.ts +9 -0
- package/qafka.config.js +9 -0
- package/react-native-qafka.podspec +28 -0
- 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 {};
|