@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,436 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useVoiceChat = useVoiceChat;
|
|
4
|
+
const react_1 = require("react");
|
|
5
|
+
const react_native_1 = require("react-native");
|
|
6
|
+
const RealtimeService_1 = require("../services/RealtimeService");
|
|
7
|
+
const QafkaAudio_1 = require("../native/QafkaAudio");
|
|
8
|
+
function useVoiceChat({ apiUrl, apiKey, userContext, contextDescription, onToolSuggested, toolRenderMode = 'upsert', getSessionToken, endUserId, endUserData, }) {
|
|
9
|
+
const [state, setState] = (0, react_1.useState)('idle');
|
|
10
|
+
const [transcript, setTranscript] = (0, react_1.useState)('');
|
|
11
|
+
const [userTranscript, setUserTranscript] = (0, react_1.useState)('');
|
|
12
|
+
const [transcriptHistory, setTranscriptHistory] = (0, react_1.useState)([]);
|
|
13
|
+
const [amplitude, setAmplitude] = (0, react_1.useState)(0);
|
|
14
|
+
const [toolStatus, setToolStatus] = (0, react_1.useState)(null);
|
|
15
|
+
const [renderedTools, setRenderedTools] = (0, react_1.useState)([]);
|
|
16
|
+
const [transcriptOverrideForTurn, setTranscriptOverrideForTurn] = (0, react_1.useState)(false);
|
|
17
|
+
const serviceRef = (0, react_1.useRef)(null);
|
|
18
|
+
// Snapshot of the live AI transcript so `response.done` can push the
|
|
19
|
+
// finished text into history without depending on stale closure state.
|
|
20
|
+
const transcriptRef = (0, react_1.useRef)('');
|
|
21
|
+
const amplitudeUnsubRef = (0, react_1.useRef)(null);
|
|
22
|
+
const onToolSuggestedRef = (0, react_1.useRef)(onToolSuggested);
|
|
23
|
+
(0, react_1.useEffect)(() => {
|
|
24
|
+
onToolSuggestedRef.current = onToolSuggested;
|
|
25
|
+
}, [onToolSuggested]);
|
|
26
|
+
// handleEvent is wrapped in useCallback([]) so we read the live value
|
|
27
|
+
// through a ref to avoid stale closures.
|
|
28
|
+
const toolRenderModeRef = (0, react_1.useRef)(toolRenderMode);
|
|
29
|
+
(0, react_1.useEffect)(() => {
|
|
30
|
+
toolRenderModeRef.current = toolRenderMode;
|
|
31
|
+
}, [toolRenderMode]);
|
|
32
|
+
// Two-layer mute orchestration. The hook itself mutes/unmutes the mic
|
|
33
|
+
// around tool flows ("system" layer); the user can also toggle mute from
|
|
34
|
+
// the UI ("user" layer). The effective mute is `user || system`, and a
|
|
35
|
+
// system unmute MUST NOT override an active user mute — otherwise the
|
|
36
|
+
// user's mute would silently lift the moment a tool finishes.
|
|
37
|
+
const userMutedRef = (0, react_1.useRef)(false);
|
|
38
|
+
const systemMutedRef = (0, react_1.useRef)(false);
|
|
39
|
+
const [isMuted, setIsMuted] = (0, react_1.useState)(false);
|
|
40
|
+
const systemMute = (0, react_1.useCallback)(() => {
|
|
41
|
+
systemMutedRef.current = true;
|
|
42
|
+
serviceRef.current?.muteMic();
|
|
43
|
+
}, []);
|
|
44
|
+
const systemUnmute = (0, react_1.useCallback)(() => {
|
|
45
|
+
systemMutedRef.current = false;
|
|
46
|
+
if (userMutedRef.current)
|
|
47
|
+
return;
|
|
48
|
+
serviceRef.current?.unmuteMic();
|
|
49
|
+
}, []);
|
|
50
|
+
const mute = (0, react_1.useCallback)(() => {
|
|
51
|
+
if (userMutedRef.current)
|
|
52
|
+
return;
|
|
53
|
+
userMutedRef.current = true;
|
|
54
|
+
setIsMuted(true);
|
|
55
|
+
serviceRef.current?.muteMic();
|
|
56
|
+
}, []);
|
|
57
|
+
const unmute = (0, react_1.useCallback)(() => {
|
|
58
|
+
if (!userMutedRef.current)
|
|
59
|
+
return;
|
|
60
|
+
userMutedRef.current = false;
|
|
61
|
+
setIsMuted(false);
|
|
62
|
+
// Only lift the mic if no system mute is active.
|
|
63
|
+
if (!systemMutedRef.current) {
|
|
64
|
+
serviceRef.current?.unmuteMic();
|
|
65
|
+
}
|
|
66
|
+
}, []);
|
|
67
|
+
const toggleMute = (0, react_1.useCallback)(() => {
|
|
68
|
+
if (userMutedRef.current)
|
|
69
|
+
unmute();
|
|
70
|
+
else
|
|
71
|
+
mute();
|
|
72
|
+
}, [mute, unmute]);
|
|
73
|
+
const handleEvent = (0, react_1.useCallback)(async (event) => {
|
|
74
|
+
switch (event.type) {
|
|
75
|
+
case 'session.ready':
|
|
76
|
+
setState('listening');
|
|
77
|
+
return;
|
|
78
|
+
case 'conversation.item.input_audio_transcription.completed':
|
|
79
|
+
setUserTranscript(event.transcript);
|
|
80
|
+
// Append the completed user turn to history for chat-style transcript views.
|
|
81
|
+
if (event.transcript && event.transcript.trim().length > 0) {
|
|
82
|
+
setTranscriptHistory((h) => [
|
|
83
|
+
...h,
|
|
84
|
+
{ id: `u_${Date.now()}_${h.length}`, role: 'user', text: event.transcript },
|
|
85
|
+
]);
|
|
86
|
+
}
|
|
87
|
+
setState('thinking');
|
|
88
|
+
return;
|
|
89
|
+
case 'response.text.delta':
|
|
90
|
+
setState('speaking');
|
|
91
|
+
setTranscript((prev) => {
|
|
92
|
+
const next = prev + event.delta;
|
|
93
|
+
transcriptRef.current = next;
|
|
94
|
+
return next;
|
|
95
|
+
});
|
|
96
|
+
return;
|
|
97
|
+
case 'response.done': {
|
|
98
|
+
// Snapshot the live AI text BEFORE clearing so we can push it to history.
|
|
99
|
+
const finalAi = transcriptRef.current;
|
|
100
|
+
if (finalAi && finalAi.trim().length > 0) {
|
|
101
|
+
setTranscriptHistory((h) => [
|
|
102
|
+
...h,
|
|
103
|
+
{ id: `a_${Date.now()}_${h.length}`, role: 'assistant', text: finalAi },
|
|
104
|
+
]);
|
|
105
|
+
}
|
|
106
|
+
transcriptRef.current = '';
|
|
107
|
+
setTranscript('');
|
|
108
|
+
setState('listening');
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
case 'session.closed':
|
|
112
|
+
setState('idle');
|
|
113
|
+
return;
|
|
114
|
+
case 'error':
|
|
115
|
+
setState('idle');
|
|
116
|
+
return;
|
|
117
|
+
case 'tool_status':
|
|
118
|
+
setToolStatus({
|
|
119
|
+
toolCallId: event.toolCallId,
|
|
120
|
+
toolKey: event.toolKey,
|
|
121
|
+
loadingMessage: event.loadingMessage,
|
|
122
|
+
});
|
|
123
|
+
return;
|
|
124
|
+
case 'tool_suggested': {
|
|
125
|
+
// Built-in qafka_display: handled locally, never delegated to consumer.
|
|
126
|
+
// Backend already pushed a synthetic {result:'ok'} to Gemini for this
|
|
127
|
+
// toolCallId, so we MUST NOT call sendToolResult here — doing so would
|
|
128
|
+
// either be silently dropped (no pending entry) or, worse, race with
|
|
129
|
+
// the backend's response and confuse the upstream session.
|
|
130
|
+
// Also defensively unmute the mic: although our intercept never mutes,
|
|
131
|
+
// unmuteMic is idempotent and protects against any prior mute that
|
|
132
|
+
// wasn't cleared (e.g. a stale tool flow before this turn).
|
|
133
|
+
const firstTool = event.tools?.[0];
|
|
134
|
+
const firstName = firstTool?.key ?? firstTool?.name;
|
|
135
|
+
if (firstName === 'qafka_display') {
|
|
136
|
+
const args = firstTool?.parameters ?? {};
|
|
137
|
+
const mode = args?.mode;
|
|
138
|
+
if (mode === 'chip') {
|
|
139
|
+
const rawItems = Array.isArray(args?.items) ? args.items : [];
|
|
140
|
+
const items = rawItems
|
|
141
|
+
.filter((it) => it &&
|
|
142
|
+
typeof it.label === 'string' &&
|
|
143
|
+
typeof it.value === 'string' &&
|
|
144
|
+
it.value.length > 0)
|
|
145
|
+
.slice(0, 5)
|
|
146
|
+
.map((it) => ({
|
|
147
|
+
label: String(it.label),
|
|
148
|
+
value: String(it.value),
|
|
149
|
+
copyable: it.copyable !== false,
|
|
150
|
+
}));
|
|
151
|
+
if (items.length > 0) {
|
|
152
|
+
const entry = {
|
|
153
|
+
kind: 'display',
|
|
154
|
+
toolCallId: event.toolCallId,
|
|
155
|
+
items,
|
|
156
|
+
};
|
|
157
|
+
setRenderedTools((prev) => [
|
|
158
|
+
...prev.filter((rt) => rt.toolCallId !== event.toolCallId),
|
|
159
|
+
entry,
|
|
160
|
+
]);
|
|
161
|
+
}
|
|
162
|
+
setToolStatus((s) => (s?.toolCallId === event.toolCallId ? null : s));
|
|
163
|
+
systemUnmute();
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (mode === 'transcript') {
|
|
167
|
+
setTranscriptOverrideForTurn(true);
|
|
168
|
+
setToolStatus((s) => (s?.toolCallId === event.toolCallId ? null : s));
|
|
169
|
+
systemUnmute();
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
// Unknown mode — render nothing. Backend already closed Gemini's
|
|
173
|
+
// turn with {result:'ok'}; no SDK action besides clearing UI state.
|
|
174
|
+
setToolStatus((s) => (s?.toolCallId === event.toolCallId ? null : s));
|
|
175
|
+
systemUnmute();
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
// A non-display tool means the AI is about to surface UI (card,
|
|
179
|
+
// list, etc.). Sticky transcript override would suppress that, so
|
|
180
|
+
// any incoming card tool auto-exits transcript mode.
|
|
181
|
+
setTranscriptOverrideForTurn(false);
|
|
182
|
+
const handler = onToolSuggestedRef.current;
|
|
183
|
+
const fallbackToolName = event.tools?.[0]?.key ?? event.tools?.[0]?.name ?? 'unknown';
|
|
184
|
+
if (!handler) {
|
|
185
|
+
// No consumer handler registered. Explicitly skip on the server so
|
|
186
|
+
// the realtime turn doesn't sit in the 30s tool timeout waiting for
|
|
187
|
+
// a tool_result that will never arrive — Gemini falls back to a
|
|
188
|
+
// text answer immediately instead of leaving the user in 30s of
|
|
189
|
+
// "thinking" silence.
|
|
190
|
+
// ok:true + structured note (not ok:false) because the server drops
|
|
191
|
+
// `output` on ok:false and only forwards the error string to Gemini —
|
|
192
|
+
// which leaves the model to invent a recovery (apology, hallucinated
|
|
193
|
+
// answer, retry loop). With ok:true the `note` reaches Gemini as the
|
|
194
|
+
// tool result and steers it toward a graceful, grounded response.
|
|
195
|
+
serviceRef.current?.sendToolResult({
|
|
196
|
+
toolCallId: event.toolCallId,
|
|
197
|
+
toolName: fallbackToolName,
|
|
198
|
+
output: {
|
|
199
|
+
status: 'client_unavailable',
|
|
200
|
+
note: 'This action is not available in the client app right now. Do not invent details. Briefly apologize in the user\'s language, then either answer from the knowledge base if relevant or offer to help with a different request.',
|
|
201
|
+
},
|
|
202
|
+
ok: true,
|
|
203
|
+
});
|
|
204
|
+
setToolStatus((s) => (s?.toolCallId === event.toolCallId ? null : s));
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
let addResponseCalled = false;
|
|
208
|
+
let responded = false;
|
|
209
|
+
// Mute (not pause) so the AI's turn-2 audio playback keeps working —
|
|
210
|
+
// iOS AVAudioEngine unifies capture and playback, so a full pauseMic
|
|
211
|
+
// would also stop playAudioChunk during the AI's response.
|
|
212
|
+
systemMute();
|
|
213
|
+
const addResponse = async (data, tool, mode = 'both') => {
|
|
214
|
+
addResponseCalled = true;
|
|
215
|
+
const toolKey = tool?.key ?? tool?.name ?? '';
|
|
216
|
+
// Only mark "responded" when we actually commit the tool flow.
|
|
217
|
+
// A "ui"-only call leaves the tool open — the consumer must follow
|
|
218
|
+
// up with a final "data" or "both" call to close it.
|
|
219
|
+
const updatesUI = mode === 'ui' || mode === 'both';
|
|
220
|
+
const commits = mode === 'data' || mode === 'both';
|
|
221
|
+
if (commits) {
|
|
222
|
+
responded = true;
|
|
223
|
+
}
|
|
224
|
+
if (updatesUI) {
|
|
225
|
+
// Apply toolRenderMode:
|
|
226
|
+
// "replace" → only the latest tool result is kept on screen
|
|
227
|
+
// "upsert" → same toolKey replaces, different keys coexist
|
|
228
|
+
const entry = {
|
|
229
|
+
toolCallId: event.toolCallId,
|
|
230
|
+
toolKey,
|
|
231
|
+
data,
|
|
232
|
+
tool,
|
|
233
|
+
};
|
|
234
|
+
if (toolRenderModeRef.current === 'replace') {
|
|
235
|
+
setRenderedTools(() => [entry]);
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
setRenderedTools((prev) => {
|
|
239
|
+
const next = prev.filter((rt) => rt.toolKey !== toolKey);
|
|
240
|
+
next.push(entry);
|
|
241
|
+
return next;
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (commits) {
|
|
246
|
+
if (tool?.executionMode === 'custom-with-ai') {
|
|
247
|
+
// Send data to backend → forwarded as toolResponse to the AI →
|
|
248
|
+
// turn 2 audio. Pill stays visible until backend sends tool_done
|
|
249
|
+
// (cleared in tool_done case). Keep mic muted during AI's
|
|
250
|
+
// response — tool_done will unmute when AI is done.
|
|
251
|
+
serviceRef.current?.sendToolResult({
|
|
252
|
+
toolCallId: event.toolCallId,
|
|
253
|
+
toolName: toolKey,
|
|
254
|
+
output: data,
|
|
255
|
+
ok: true,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
// custom (or unspecified): immediate local resolution (UC1 path).
|
|
260
|
+
setToolStatus((s) => s?.toolCallId === event.toolCallId ? null : s);
|
|
261
|
+
systemUnmute();
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return {
|
|
265
|
+
ok: true,
|
|
266
|
+
toolName: toolKey,
|
|
267
|
+
toolOutput: data,
|
|
268
|
+
};
|
|
269
|
+
};
|
|
270
|
+
try {
|
|
271
|
+
await handler(event.tools, addResponse);
|
|
272
|
+
}
|
|
273
|
+
catch (err) {
|
|
274
|
+
if (__DEV__) {
|
|
275
|
+
console.warn('[Qafka] onToolSuggested threw:', err);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (!addResponseCalled) {
|
|
279
|
+
// Handler returned (or threw) without invoking addResponse at all —
|
|
280
|
+
// e.g. it bailed because required context refs weren't wired up.
|
|
281
|
+
// Send an explicit skip so the server doesn't sit in its 30s tool
|
|
282
|
+
// timeout; Gemini will produce a fallback response immediately.
|
|
283
|
+
// ok:true + structured note (not ok:false) because the server drops
|
|
284
|
+
// `output` on ok:false and only forwards the error string to Gemini —
|
|
285
|
+
// which leaves the model to invent a recovery (apology, hallucinated
|
|
286
|
+
// answer, retry loop). With ok:true the `note` reaches Gemini as the
|
|
287
|
+
// tool result and steers it toward a graceful, grounded response.
|
|
288
|
+
serviceRef.current?.sendToolResult({
|
|
289
|
+
toolCallId: event.toolCallId,
|
|
290
|
+
toolName: fallbackToolName,
|
|
291
|
+
output: {
|
|
292
|
+
status: 'client_unavailable',
|
|
293
|
+
note: 'This action is not available in the client app right now. Do not invent details. Briefly apologize in the user\'s language, then either answer from the knowledge base if relevant or offer to help with a different request.',
|
|
294
|
+
},
|
|
295
|
+
ok: true,
|
|
296
|
+
});
|
|
297
|
+
setToolStatus((s) => (s?.toolCallId === event.toolCallId ? null : s));
|
|
298
|
+
systemUnmute();
|
|
299
|
+
}
|
|
300
|
+
else if (!responded) {
|
|
301
|
+
// addResponse was called but only in 'ui' mode — UI rendered, no
|
|
302
|
+
// commit yet. The consumer is expected to commit later via another
|
|
303
|
+
// addResponse('data' | 'both') call. Clear pill and unmute mic so
|
|
304
|
+
// user isn't blocked in the meantime; the server 30s timeout remains
|
|
305
|
+
// the safety net if the consumer never commits.
|
|
306
|
+
setToolStatus((s) => (s?.toolCallId === event.toolCallId ? null : s));
|
|
307
|
+
systemUnmute();
|
|
308
|
+
}
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
case 'tool_done':
|
|
312
|
+
// Backend signaled the tool flow is complete (either the AI consumed the result or
|
|
313
|
+
// synthetic delegated response sent (custom). Either way: clear pill and
|
|
314
|
+
// unmute mic so user can speak again. unmuteMic is idempotent — safe to
|
|
315
|
+
// call even if already unmuted (e.g. UC1 unmuted in addResponse).
|
|
316
|
+
setToolStatus((s) => (s?.toolCallId === event.toolCallId ? null : s));
|
|
317
|
+
systemUnmute();
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
}, []);
|
|
321
|
+
const connect = (0, react_1.useCallback)(async () => {
|
|
322
|
+
if (serviceRef.current?.isConnected)
|
|
323
|
+
return;
|
|
324
|
+
setState('connecting');
|
|
325
|
+
const service = new RealtimeService_1.RealtimeService(apiUrl, apiKey);
|
|
326
|
+
if (getSessionToken)
|
|
327
|
+
service.setSessionTokenGetter(getSessionToken);
|
|
328
|
+
serviceRef.current = service;
|
|
329
|
+
try {
|
|
330
|
+
await service.connect(handleEvent, {
|
|
331
|
+
userContext,
|
|
332
|
+
contextDescription,
|
|
333
|
+
endUserId,
|
|
334
|
+
endUserData,
|
|
335
|
+
});
|
|
336
|
+
// Throttle amplitude to ~20fps. Native emits per audio chunk (can exceed
|
|
337
|
+
// 100Hz during playback); unbounded setState floods the React commit
|
|
338
|
+
// queue and can cascade into "Maximum update depth exceeded" if
|
|
339
|
+
// consumer-tree components animate or re-derive state on amplitude.
|
|
340
|
+
// Use OR semantics so we always skip when too recent OR change is
|
|
341
|
+
// imperceptible — earlier AND logic let amplitude through whenever it
|
|
342
|
+
// changed by >0.02, defeating the throttle during loud audio.
|
|
343
|
+
let lastEmitMs = 0;
|
|
344
|
+
let lastLevel = 0;
|
|
345
|
+
amplitudeUnsubRef.current = QafkaAudio_1.QafkaAudio.onAmplitude(({ level }) => {
|
|
346
|
+
const now = Date.now();
|
|
347
|
+
if (now - lastEmitMs < 50)
|
|
348
|
+
return;
|
|
349
|
+
if (Math.abs(level - lastLevel) < 0.02)
|
|
350
|
+
return;
|
|
351
|
+
lastEmitMs = now;
|
|
352
|
+
lastLevel = level;
|
|
353
|
+
setAmplitude(level);
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
setState('idle');
|
|
358
|
+
serviceRef.current = null;
|
|
359
|
+
}
|
|
360
|
+
}, [apiUrl, apiKey, handleEvent, userContext, contextDescription, getSessionToken, endUserId, endUserData]);
|
|
361
|
+
const disconnect = (0, react_1.useCallback)(async () => {
|
|
362
|
+
amplitudeUnsubRef.current?.();
|
|
363
|
+
amplitudeUnsubRef.current = null;
|
|
364
|
+
await serviceRef.current?.disconnect();
|
|
365
|
+
serviceRef.current = null;
|
|
366
|
+
setState('idle');
|
|
367
|
+
setTranscript('');
|
|
368
|
+
setUserTranscript('');
|
|
369
|
+
setTranscriptHistory([]);
|
|
370
|
+
transcriptRef.current = '';
|
|
371
|
+
setAmplitude(0);
|
|
372
|
+
// Reset both mute layers so the next session starts clean.
|
|
373
|
+
userMutedRef.current = false;
|
|
374
|
+
systemMutedRef.current = false;
|
|
375
|
+
setIsMuted(false);
|
|
376
|
+
}, []);
|
|
377
|
+
const pauseMic = (0, react_1.useCallback)(async () => {
|
|
378
|
+
await serviceRef.current?.pauseMic();
|
|
379
|
+
}, []);
|
|
380
|
+
const resumeMic = (0, react_1.useCallback)(async () => {
|
|
381
|
+
await serviceRef.current?.resumeMic();
|
|
382
|
+
}, []);
|
|
383
|
+
// Disconnect on app background
|
|
384
|
+
(0, react_1.useEffect)(() => {
|
|
385
|
+
const handleAppState = (nextState) => {
|
|
386
|
+
if (nextState !== 'active' && serviceRef.current?.isConnected) {
|
|
387
|
+
disconnect();
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
const sub = react_native_1.AppState.addEventListener('change', handleAppState);
|
|
391
|
+
return () => sub.remove();
|
|
392
|
+
}, [disconnect]);
|
|
393
|
+
// Cleanup on unmount
|
|
394
|
+
(0, react_1.useEffect)(() => {
|
|
395
|
+
return () => {
|
|
396
|
+
amplitudeUnsubRef.current?.();
|
|
397
|
+
serviceRef.current?.disconnect();
|
|
398
|
+
};
|
|
399
|
+
}, []);
|
|
400
|
+
return {
|
|
401
|
+
state,
|
|
402
|
+
transcript,
|
|
403
|
+
userTranscript,
|
|
404
|
+
transcriptHistory,
|
|
405
|
+
amplitude,
|
|
406
|
+
connect,
|
|
407
|
+
disconnect,
|
|
408
|
+
pauseMic,
|
|
409
|
+
resumeMic,
|
|
410
|
+
isConnected: serviceRef.current?.isConnected ?? false,
|
|
411
|
+
toolStatus,
|
|
412
|
+
renderedTools,
|
|
413
|
+
transcriptOverrideForTurn,
|
|
414
|
+
clearRenderedTools: () => setRenderedTools([]),
|
|
415
|
+
setToolStatusManually: (status) => {
|
|
416
|
+
if (status) {
|
|
417
|
+
setToolStatus({
|
|
418
|
+
toolCallId: `manual_${Date.now()}`,
|
|
419
|
+
toolKey: status.toolKey,
|
|
420
|
+
loadingMessage: status.loadingMessage,
|
|
421
|
+
});
|
|
422
|
+
// Mute (don't pause) so AI playback works if the developer's async work
|
|
423
|
+
// overlaps with an in-flight AI response.
|
|
424
|
+
systemMute();
|
|
425
|
+
}
|
|
426
|
+
else {
|
|
427
|
+
setToolStatus(null);
|
|
428
|
+
systemUnmute();
|
|
429
|
+
}
|
|
430
|
+
},
|
|
431
|
+
isMuted,
|
|
432
|
+
mute,
|
|
433
|
+
unmute,
|
|
434
|
+
toggleMute,
|
|
435
|
+
};
|
|
436
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/// <reference path="../qafka.config.d.ts" />
|
|
2
|
+
/**
|
|
3
|
+
* QafkaSDK for React Native
|
|
4
|
+
* AI-powered conversational interface with navigation intelligence
|
|
5
|
+
*/
|
|
6
|
+
export { QafkaSDK, getSDK } from './QafkaSDK';
|
|
7
|
+
export declare const getVersion: () => string;
|
|
8
|
+
export declare const isVersionAtLeast: (minVersion: string) => boolean;
|
|
9
|
+
export * from './components';
|
|
10
|
+
export * from './themes';
|
|
11
|
+
export * from './types';
|
|
12
|
+
export * from './constants';
|
|
13
|
+
export * from './cards';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* QafkaSDK for React Native
|
|
4
|
+
* AI-powered conversational interface with navigation intelligence
|
|
5
|
+
*/
|
|
6
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
7
|
+
if (k2 === undefined) k2 = k;
|
|
8
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
9
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
10
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
11
|
+
}
|
|
12
|
+
Object.defineProperty(o, k2, desc);
|
|
13
|
+
}) : (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
o[k2] = m[k];
|
|
16
|
+
}));
|
|
17
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
18
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
19
|
+
};
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.isVersionAtLeast = exports.getVersion = exports.getSDK = exports.QafkaSDK = void 0;
|
|
22
|
+
// Main SDK export
|
|
23
|
+
var QafkaSDK_1 = require("./QafkaSDK");
|
|
24
|
+
Object.defineProperty(exports, "QafkaSDK", { enumerable: true, get: function () { return QafkaSDK_1.QafkaSDK; } });
|
|
25
|
+
Object.defineProperty(exports, "getSDK", { enumerable: true, get: function () { return QafkaSDK_1.getSDK; } });
|
|
26
|
+
const package_json_1 = require("../package.json");
|
|
27
|
+
const getVersion = () => package_json_1.version;
|
|
28
|
+
exports.getVersion = getVersion;
|
|
29
|
+
const compare = (targetVersion) => {
|
|
30
|
+
// Strip build metadata (+hash) and pre-release tags (-beta) from versions
|
|
31
|
+
// e.g. "0.1.7+abc1234" -> "0.1.7"
|
|
32
|
+
const cleanCurrentVersion = package_json_1.version.split('+')[0].split('-')[0];
|
|
33
|
+
const cleanTargetVersion = targetVersion.split('+')[0].split('-')[0];
|
|
34
|
+
const currentParts = cleanCurrentVersion.split('.').map(Number);
|
|
35
|
+
const targetParts = cleanTargetVersion.split('.').map(Number);
|
|
36
|
+
for (let i = 0; i < Math.max(currentParts.length, targetParts.length); i++) {
|
|
37
|
+
const v1 = currentParts[i] || 0;
|
|
38
|
+
const v2 = targetParts[i] || 0;
|
|
39
|
+
if (v1 > v2)
|
|
40
|
+
return 1;
|
|
41
|
+
if (v1 < v2)
|
|
42
|
+
return -1;
|
|
43
|
+
}
|
|
44
|
+
return 0;
|
|
45
|
+
};
|
|
46
|
+
const isVersionAtLeast = (minVersion) => {
|
|
47
|
+
return compare(minVersion) >= 0;
|
|
48
|
+
};
|
|
49
|
+
exports.isVersionAtLeast = isVersionAtLeast;
|
|
50
|
+
// UI Components
|
|
51
|
+
__exportStar(require("./components"), exports);
|
|
52
|
+
// Theme System
|
|
53
|
+
__exportStar(require("./themes"), exports);
|
|
54
|
+
// Export types
|
|
55
|
+
__exportStar(require("./types"), exports);
|
|
56
|
+
// Export constants
|
|
57
|
+
__exportStar(require("./constants"), exports);
|
|
58
|
+
// Card Template
|
|
59
|
+
__exportStar(require("./cards"), exports);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Platform } from 'react-native';
|
|
2
|
+
export interface QafkaDeviceInfo {
|
|
3
|
+
bundleId: string;
|
|
4
|
+
appVersion: string;
|
|
5
|
+
deviceModel: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Device info exposed as sync constants from the native QafkaAttestation
|
|
9
|
+
* module. Read once at boot — the values are static for the app's lifetime.
|
|
10
|
+
*
|
|
11
|
+
* Returns empty strings (not null) when the native module is missing so
|
|
12
|
+
* downstream code does not need null guards. Caller should treat empty
|
|
13
|
+
* strings as "unknown".
|
|
14
|
+
*/
|
|
15
|
+
export declare function getDeviceInfo(): QafkaDeviceInfo;
|
|
16
|
+
export declare function isAttestationAvailable(): boolean;
|
|
17
|
+
export declare function isSupported(): Promise<boolean>;
|
|
18
|
+
export declare function generateKey(): Promise<string>;
|
|
19
|
+
export declare function attestKey(keyId: string, challengeBase64: string): Promise<string>;
|
|
20
|
+
export declare function generateAssertion(keyId: string, clientDataBase64: string): Promise<string>;
|
|
21
|
+
export declare function generateKeyPair(alias: string, challengeBase64: string): Promise<void>;
|
|
22
|
+
export declare function getAttestationCertChain(alias: string): Promise<string[]>;
|
|
23
|
+
export { Platform };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Platform = void 0;
|
|
4
|
+
exports.getDeviceInfo = getDeviceInfo;
|
|
5
|
+
exports.isAttestationAvailable = isAttestationAvailable;
|
|
6
|
+
exports.isSupported = isSupported;
|
|
7
|
+
exports.generateKey = generateKey;
|
|
8
|
+
exports.attestKey = attestKey;
|
|
9
|
+
exports.generateAssertion = generateAssertion;
|
|
10
|
+
exports.generateKeyPair = generateKeyPair;
|
|
11
|
+
exports.getAttestationCertChain = getAttestationCertChain;
|
|
12
|
+
const react_native_1 = require("react-native");
|
|
13
|
+
Object.defineProperty(exports, "Platform", { enumerable: true, get: function () { return react_native_1.Platform; } });
|
|
14
|
+
const NativeAttestation = react_native_1.NativeModules.QafkaAttestation ?? null;
|
|
15
|
+
/**
|
|
16
|
+
* Device info exposed as sync constants from the native QafkaAttestation
|
|
17
|
+
* module. Read once at boot — the values are static for the app's lifetime.
|
|
18
|
+
*
|
|
19
|
+
* Returns empty strings (not null) when the native module is missing so
|
|
20
|
+
* downstream code does not need null guards. Caller should treat empty
|
|
21
|
+
* strings as "unknown".
|
|
22
|
+
*/
|
|
23
|
+
function getDeviceInfo() {
|
|
24
|
+
const mod = react_native_1.NativeModules.QafkaAttestation;
|
|
25
|
+
return {
|
|
26
|
+
bundleId: mod?.bundleId ?? '',
|
|
27
|
+
appVersion: mod?.appVersion ?? '',
|
|
28
|
+
deviceModel: mod?.deviceModel ?? '',
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function isAttestationAvailable() {
|
|
32
|
+
return NativeAttestation !== null;
|
|
33
|
+
}
|
|
34
|
+
async function isSupported() {
|
|
35
|
+
if (!NativeAttestation)
|
|
36
|
+
return false;
|
|
37
|
+
try {
|
|
38
|
+
return await NativeAttestation.isSupported();
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// iOS methods
|
|
45
|
+
async function generateKey() {
|
|
46
|
+
if (!NativeAttestation?.generateKey)
|
|
47
|
+
throw new Error('iOS only');
|
|
48
|
+
return NativeAttestation.generateKey();
|
|
49
|
+
}
|
|
50
|
+
async function attestKey(keyId, challengeBase64) {
|
|
51
|
+
if (!NativeAttestation?.attestKey)
|
|
52
|
+
throw new Error('iOS only');
|
|
53
|
+
return NativeAttestation.attestKey(keyId, challengeBase64);
|
|
54
|
+
}
|
|
55
|
+
async function generateAssertion(keyId, clientDataBase64) {
|
|
56
|
+
if (!NativeAttestation?.generateAssertion)
|
|
57
|
+
throw new Error('iOS only');
|
|
58
|
+
return NativeAttestation.generateAssertion(keyId, clientDataBase64);
|
|
59
|
+
}
|
|
60
|
+
// Android methods
|
|
61
|
+
async function generateKeyPair(alias, challengeBase64) {
|
|
62
|
+
if (!NativeAttestation?.generateKeyPair)
|
|
63
|
+
throw new Error('Android only');
|
|
64
|
+
return NativeAttestation.generateKeyPair(alias, challengeBase64);
|
|
65
|
+
}
|
|
66
|
+
async function getAttestationCertChain(alias) {
|
|
67
|
+
if (!NativeAttestation?.getAttestationCertChain)
|
|
68
|
+
throw new Error('Android only');
|
|
69
|
+
return NativeAttestation.getAttestationCertChain(alias);
|
|
70
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare const QafkaAudio: {
|
|
2
|
+
startCapture: () => Promise<boolean>;
|
|
3
|
+
stopCapture: () => Promise<boolean>;
|
|
4
|
+
playAudioChunk: (base64Data: string) => Promise<boolean>;
|
|
5
|
+
stopPlayback: () => Promise<boolean>;
|
|
6
|
+
onAudioData: (callback: (data: {
|
|
7
|
+
audio: string;
|
|
8
|
+
}) => void) => () => void | undefined;
|
|
9
|
+
onAmplitude: (callback: (data: {
|
|
10
|
+
level: number;
|
|
11
|
+
source: "mic" | "speaker";
|
|
12
|
+
}) => void) => () => void | undefined;
|
|
13
|
+
isAvailable: () => boolean;
|
|
14
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.QafkaAudio = void 0;
|
|
4
|
+
const react_native_1 = require("react-native");
|
|
5
|
+
const { QafkaAudio: NativeQafkaAudio } = react_native_1.NativeModules;
|
|
6
|
+
const emitter = NativeQafkaAudio ? new react_native_1.NativeEventEmitter(NativeQafkaAudio) : null;
|
|
7
|
+
const MISSING_MODULE_MESSAGE = `QafkaAudio native module is not linked. ` +
|
|
8
|
+
`The JS bundle was updated but the native binary was not rebuilt. ` +
|
|
9
|
+
(react_native_1.Platform.OS === 'ios'
|
|
10
|
+
? `Run \`cd ios && pod install\` then rebuild the app (\`yarn ios\` or from Xcode).`
|
|
11
|
+
: `Rebuild the app (\`yarn android\` or \`cd android && ./gradlew clean && ./gradlew assembleDebug\`).`);
|
|
12
|
+
const requireModule = () => {
|
|
13
|
+
if (!NativeQafkaAudio)
|
|
14
|
+
throw new Error(MISSING_MODULE_MESSAGE);
|
|
15
|
+
return NativeQafkaAudio;
|
|
16
|
+
};
|
|
17
|
+
exports.QafkaAudio = {
|
|
18
|
+
startCapture: () => requireModule().startCapture(),
|
|
19
|
+
stopCapture: () => requireModule().stopCapture(),
|
|
20
|
+
playAudioChunk: (base64Data) => requireModule().playAudioChunk(base64Data),
|
|
21
|
+
stopPlayback: () => requireModule().stopPlayback(),
|
|
22
|
+
onAudioData: (callback) => {
|
|
23
|
+
const subscription = emitter?.addListener('onAudioData', callback);
|
|
24
|
+
return () => subscription?.remove();
|
|
25
|
+
},
|
|
26
|
+
onAmplitude: (callback) => {
|
|
27
|
+
const subscription = emitter?.addListener('onAmplitude', callback);
|
|
28
|
+
return () => subscription?.remove();
|
|
29
|
+
},
|
|
30
|
+
isAvailable: () => NativeQafkaAudio != null,
|
|
31
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native clipboard module — writes a string to the system clipboard.
|
|
3
|
+
* iOS: UIPasteboard.general. Android: ClipboardManager (primary clip).
|
|
4
|
+
*
|
|
5
|
+
* Zero peer dependencies; ships with the Qafka native package.
|
|
6
|
+
*/
|
|
7
|
+
export interface QafkaClipboardNative {
|
|
8
|
+
setString(value: string): Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
export declare function isClipboardAvailable(): boolean;
|
|
11
|
+
export declare function setString(value: string): Promise<void>;
|