@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,43 @@
|
|
|
1
|
+
import { ChatMessage } from '../types/chat';
|
|
2
|
+
/**
|
|
3
|
+
* Conversation Manager
|
|
4
|
+
* Handles conversation history and session management
|
|
5
|
+
*/
|
|
6
|
+
export declare class ConversationManager {
|
|
7
|
+
private sessionId;
|
|
8
|
+
private messages;
|
|
9
|
+
private maxMessages;
|
|
10
|
+
constructor(sessionId?: string);
|
|
11
|
+
/**
|
|
12
|
+
* Initialize manager
|
|
13
|
+
*/
|
|
14
|
+
initialize(): Promise<void>;
|
|
15
|
+
/**
|
|
16
|
+
* Add message to conversation
|
|
17
|
+
*/
|
|
18
|
+
addMessage(message: ChatMessage): void;
|
|
19
|
+
/**
|
|
20
|
+
* Get conversation history
|
|
21
|
+
*/
|
|
22
|
+
getHistory(limit?: number): ChatMessage[];
|
|
23
|
+
/**
|
|
24
|
+
* Get session ID
|
|
25
|
+
*/
|
|
26
|
+
getSessionId(): string;
|
|
27
|
+
/**
|
|
28
|
+
* Clear conversation
|
|
29
|
+
*/
|
|
30
|
+
clear(): Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* Load from storage
|
|
33
|
+
*/
|
|
34
|
+
private loadFromStorage;
|
|
35
|
+
/**
|
|
36
|
+
* Save to storage
|
|
37
|
+
*/
|
|
38
|
+
private saveToStorage;
|
|
39
|
+
/**
|
|
40
|
+
* Generate session ID
|
|
41
|
+
*/
|
|
42
|
+
private generateSessionId;
|
|
43
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ConversationManager = void 0;
|
|
4
|
+
const storage_1 = require("./storage");
|
|
5
|
+
const constants_1 = require("../constants");
|
|
6
|
+
/**
|
|
7
|
+
* Conversation Manager
|
|
8
|
+
* Handles conversation history and session management
|
|
9
|
+
*/
|
|
10
|
+
class ConversationManager {
|
|
11
|
+
sessionId;
|
|
12
|
+
messages = [];
|
|
13
|
+
maxMessages = 50;
|
|
14
|
+
constructor(sessionId) {
|
|
15
|
+
this.sessionId = sessionId || this.generateSessionId();
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Initialize manager
|
|
19
|
+
*/
|
|
20
|
+
async initialize() {
|
|
21
|
+
await this.loadFromStorage();
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Add message to conversation
|
|
25
|
+
*/
|
|
26
|
+
addMessage(message) {
|
|
27
|
+
this.messages.push(message);
|
|
28
|
+
// Keep only last N messages
|
|
29
|
+
if (this.messages.length > this.maxMessages) {
|
|
30
|
+
this.messages = this.messages.slice(-this.maxMessages);
|
|
31
|
+
}
|
|
32
|
+
this.saveToStorage();
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Get conversation history
|
|
36
|
+
*/
|
|
37
|
+
getHistory(limit) {
|
|
38
|
+
if (limit) {
|
|
39
|
+
return this.messages.slice(-limit);
|
|
40
|
+
}
|
|
41
|
+
return [...this.messages];
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Get session ID
|
|
45
|
+
*/
|
|
46
|
+
getSessionId() {
|
|
47
|
+
return this.sessionId;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Clear conversation
|
|
51
|
+
*/
|
|
52
|
+
async clear() {
|
|
53
|
+
this.messages = [];
|
|
54
|
+
this.sessionId = this.generateSessionId();
|
|
55
|
+
await storage_1.storage.removeItem(constants_1.STORAGE_KEYS.CONVERSATION_HISTORY);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Load from storage
|
|
59
|
+
*/
|
|
60
|
+
async loadFromStorage() {
|
|
61
|
+
try {
|
|
62
|
+
const stored = await storage_1.storage.getItem(constants_1.STORAGE_KEYS.CONVERSATION_HISTORY);
|
|
63
|
+
if (stored) {
|
|
64
|
+
const data = JSON.parse(stored);
|
|
65
|
+
this.messages = data.messages || [];
|
|
66
|
+
this.sessionId = data.sessionId || this.sessionId;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// Silent error handling
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Save to storage
|
|
75
|
+
*/
|
|
76
|
+
async saveToStorage() {
|
|
77
|
+
try {
|
|
78
|
+
const data = {
|
|
79
|
+
sessionId: this.sessionId,
|
|
80
|
+
messages: this.messages,
|
|
81
|
+
updatedAt: new Date().toISOString(),
|
|
82
|
+
};
|
|
83
|
+
await storage_1.storage.setItem(constants_1.STORAGE_KEYS.CONVERSATION_HISTORY, JSON.stringify(data));
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// Silent error handling
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Generate session ID
|
|
91
|
+
*/
|
|
92
|
+
generateSessionId() {
|
|
93
|
+
return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
exports.ConversationManager = ConversationManager;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { NavigationContainerRef } from '@react-navigation/native';
|
|
2
|
+
/**
|
|
3
|
+
* Navigation Handler
|
|
4
|
+
* Handles navigation based on AI suggestions
|
|
5
|
+
*/
|
|
6
|
+
export declare class NavigationHandler {
|
|
7
|
+
private navigationRef;
|
|
8
|
+
constructor(navigationRef?: NavigationContainerRef<any>);
|
|
9
|
+
/**
|
|
10
|
+
* Set navigation reference
|
|
11
|
+
*/
|
|
12
|
+
setNavigationRef(ref: NavigationContainerRef<any>): void;
|
|
13
|
+
/**
|
|
14
|
+
* Navigate to screen
|
|
15
|
+
*/
|
|
16
|
+
navigate(screenName: string, params?: any): void;
|
|
17
|
+
/**
|
|
18
|
+
* Go back
|
|
19
|
+
*/
|
|
20
|
+
goBack(): void;
|
|
21
|
+
/**
|
|
22
|
+
* Get current route name
|
|
23
|
+
*/
|
|
24
|
+
getCurrentRoute(): string | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* Reset navigation
|
|
27
|
+
*/
|
|
28
|
+
reset(routeName: string, params?: any): void;
|
|
29
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NavigationHandler = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Navigation Handler
|
|
6
|
+
* Handles navigation based on AI suggestions
|
|
7
|
+
*/
|
|
8
|
+
class NavigationHandler {
|
|
9
|
+
navigationRef;
|
|
10
|
+
constructor(navigationRef) {
|
|
11
|
+
this.navigationRef = navigationRef || null;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Set navigation reference
|
|
15
|
+
*/
|
|
16
|
+
setNavigationRef(ref) {
|
|
17
|
+
this.navigationRef = ref;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Navigate to screen
|
|
21
|
+
*/
|
|
22
|
+
navigate(screenName, params) {
|
|
23
|
+
if (!this.navigationRef) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (!this.navigationRef.isReady()) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
// @ts-ignore - navigate accepts screen name
|
|
31
|
+
this.navigationRef.navigate(screenName, params);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// Navigation error silently ignored
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Go back
|
|
39
|
+
*/
|
|
40
|
+
goBack() {
|
|
41
|
+
if (!this.navigationRef) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (this.navigationRef.canGoBack()) {
|
|
45
|
+
this.navigationRef.goBack();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Get current route name
|
|
50
|
+
*/
|
|
51
|
+
getCurrentRoute() {
|
|
52
|
+
if (!this.navigationRef || !this.navigationRef.isReady()) {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
return this.navigationRef.getCurrentRoute()?.name;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Reset navigation
|
|
59
|
+
*/
|
|
60
|
+
reset(routeName, params) {
|
|
61
|
+
if (!this.navigationRef || !this.navigationRef.isReady()) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
this.navigationRef.reset({
|
|
65
|
+
index: 0,
|
|
66
|
+
routes: [{ name: routeName, params }],
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
exports.NavigationHandler = NavigationHandler;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export type RealtimeEvent = {
|
|
2
|
+
type: 'session.ready';
|
|
3
|
+
} | {
|
|
4
|
+
type: 'session.closed';
|
|
5
|
+
reason: string;
|
|
6
|
+
} | {
|
|
7
|
+
type: 'response.audio.delta';
|
|
8
|
+
delta: string;
|
|
9
|
+
} | {
|
|
10
|
+
type: 'response.text.delta';
|
|
11
|
+
delta: string;
|
|
12
|
+
} | {
|
|
13
|
+
type: 'response.done';
|
|
14
|
+
} | {
|
|
15
|
+
type: 'conversation.item.input_audio_transcription.completed';
|
|
16
|
+
transcript: string;
|
|
17
|
+
} | {
|
|
18
|
+
type: 'error';
|
|
19
|
+
message: string;
|
|
20
|
+
} | {
|
|
21
|
+
type: 'tool_status';
|
|
22
|
+
toolCallId: string;
|
|
23
|
+
toolKey: string;
|
|
24
|
+
loadingMessage: string;
|
|
25
|
+
} | {
|
|
26
|
+
type: 'tool_suggested';
|
|
27
|
+
toolCallId: string;
|
|
28
|
+
tools: any[];
|
|
29
|
+
conversationId: string;
|
|
30
|
+
messageId?: string;
|
|
31
|
+
} | {
|
|
32
|
+
type: 'tool_done';
|
|
33
|
+
toolCallId: string;
|
|
34
|
+
};
|
|
35
|
+
export type RealtimeEventHandler = (event: RealtimeEvent) => void;
|
|
36
|
+
export declare class RealtimeService {
|
|
37
|
+
private apiUrl;
|
|
38
|
+
private apiKey;
|
|
39
|
+
private ws;
|
|
40
|
+
private audioUnsubscribe;
|
|
41
|
+
private eventHandler;
|
|
42
|
+
private audioChunksSent;
|
|
43
|
+
private sessionTokenGetter;
|
|
44
|
+
private appVersion;
|
|
45
|
+
constructor(apiUrl: string, apiKey: string);
|
|
46
|
+
setSessionTokenGetter(fn: () => Promise<string | null>): void;
|
|
47
|
+
connect(onEvent: RealtimeEventHandler, options?: {
|
|
48
|
+
userContext?: Record<string, any>;
|
|
49
|
+
contextDescription?: string;
|
|
50
|
+
endUserId?: string;
|
|
51
|
+
endUserData?: Record<string, unknown>;
|
|
52
|
+
}): Promise<void>;
|
|
53
|
+
private audioDeltasReceived;
|
|
54
|
+
private handleEvent;
|
|
55
|
+
private attachAudioListener;
|
|
56
|
+
private detachAudioListener;
|
|
57
|
+
private startAudioPipeline;
|
|
58
|
+
/**
|
|
59
|
+
* Fully stop the native audio engine (capture + playback). Use when leaving
|
|
60
|
+
* voice mode entirely (e.g. scrolling back to chat page). NOT for tool-flow
|
|
61
|
+
* pauses — that breaks the AI's turn-2 audio playback because iOS
|
|
62
|
+
* AVAudioEngine unifies capture + playback in one engine instance.
|
|
63
|
+
*/
|
|
64
|
+
pauseMic(): Promise<void>;
|
|
65
|
+
resumeMic(): Promise<void>;
|
|
66
|
+
/**
|
|
67
|
+
* Stop forwarding mic audio to the server WHILE keeping the native audio
|
|
68
|
+
* engine running so the AI's turn-2 audio playback continues. Use during
|
|
69
|
+
* tool flows where the user shouldn't interrupt but the AI may still speak.
|
|
70
|
+
*/
|
|
71
|
+
muteMic(): void;
|
|
72
|
+
unmuteMic(): void;
|
|
73
|
+
sendToolResult(payload: {
|
|
74
|
+
toolCallId: string;
|
|
75
|
+
toolName: string;
|
|
76
|
+
output: any;
|
|
77
|
+
ok: boolean;
|
|
78
|
+
error?: string;
|
|
79
|
+
}): void;
|
|
80
|
+
disconnect(): Promise<void>;
|
|
81
|
+
private cleanup;
|
|
82
|
+
get isConnected(): boolean;
|
|
83
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RealtimeService = void 0;
|
|
4
|
+
const QafkaAudio_1 = require("../native/QafkaAudio");
|
|
5
|
+
const QafkaAttestation_1 = require("../native/QafkaAttestation");
|
|
6
|
+
class RealtimeService {
|
|
7
|
+
apiUrl;
|
|
8
|
+
apiKey;
|
|
9
|
+
ws = null;
|
|
10
|
+
audioUnsubscribe = null;
|
|
11
|
+
eventHandler = null;
|
|
12
|
+
audioChunksSent = 0;
|
|
13
|
+
sessionTokenGetter = null;
|
|
14
|
+
appVersion = null;
|
|
15
|
+
constructor(apiUrl, apiKey) {
|
|
16
|
+
this.apiUrl = apiUrl;
|
|
17
|
+
this.apiKey = apiKey;
|
|
18
|
+
const deviceInfo = (0, QafkaAttestation_1.getDeviceInfo)();
|
|
19
|
+
this.appVersion = deviceInfo.appVersion || null;
|
|
20
|
+
}
|
|
21
|
+
setSessionTokenGetter(fn) {
|
|
22
|
+
this.sessionTokenGetter = fn;
|
|
23
|
+
}
|
|
24
|
+
async connect(onEvent, options) {
|
|
25
|
+
this.eventHandler = onEvent;
|
|
26
|
+
// Force wss:// in production builds. If the host app supplies an http://
|
|
27
|
+
// apiUrl in a non-dev build, that's a misconfiguration we'd rather hard-
|
|
28
|
+
// fail (with a TLS handshake failure) than silently downgrade to a
|
|
29
|
+
// plaintext WebSocket vulnerable to MITM.
|
|
30
|
+
const isDev = typeof __DEV__ !== 'undefined' && __DEV__;
|
|
31
|
+
const wsUrl = this.apiUrl.replace(/^https?:/, (m) => (m === 'https:' ? 'wss:' : isDev ? 'ws:' : 'wss:'));
|
|
32
|
+
const params = new URLSearchParams();
|
|
33
|
+
if (options?.userContext && Object.keys(options.userContext).length > 0) {
|
|
34
|
+
params.set('context', JSON.stringify(options.userContext));
|
|
35
|
+
}
|
|
36
|
+
if (options?.contextDescription &&
|
|
37
|
+
options.contextDescription.trim().length > 0) {
|
|
38
|
+
params.set('contextDescription', options.contextDescription);
|
|
39
|
+
}
|
|
40
|
+
if (options?.endUserId && options.endUserId.trim().length > 0) {
|
|
41
|
+
params.set('endUserId', options.endUserId);
|
|
42
|
+
}
|
|
43
|
+
if (options?.endUserData &&
|
|
44
|
+
typeof options.endUserData === 'object' &&
|
|
45
|
+
Object.keys(options.endUserData).length > 0) {
|
|
46
|
+
params.set('endUserData', JSON.stringify(options.endUserData));
|
|
47
|
+
}
|
|
48
|
+
const qs = params.toString();
|
|
49
|
+
const fullUrl = `${wsUrl}/v1/realtime${qs ? `?${qs}` : ''}`;
|
|
50
|
+
// Start the audio engine FIRST so the player is ready to schedule buffers
|
|
51
|
+
// the instant the backend starts streaming audio. The realtime service can
|
|
52
|
+
// emit audio deltas before native startCapture finishes if we start it after
|
|
53
|
+
// the WebSocket — the first ~20 chunks would drop with "engine not running".
|
|
54
|
+
// Doing it in this order closes that race.
|
|
55
|
+
await this.startAudioPipeline();
|
|
56
|
+
// Resolve the device-attestation session token BEFORE opening the WS so
|
|
57
|
+
// that it is ready to ship in the first auth frame. The getter is async
|
|
58
|
+
// (it may need to refresh an expired token via native attestation).
|
|
59
|
+
const sessionToken = this.sessionTokenGetter
|
|
60
|
+
? await this.sessionTokenGetter()
|
|
61
|
+
: null;
|
|
62
|
+
return new Promise((resolve, reject) => {
|
|
63
|
+
this.ws = new WebSocket(fullUrl);
|
|
64
|
+
this.ws.onopen = () => {
|
|
65
|
+
// Send auth as the first frame instead of leaking the API key in the
|
|
66
|
+
// URL (avoids exposure via access logs / proxy histories). Session
|
|
67
|
+
// token, when available, lets the server tie this socket to a
|
|
68
|
+
// device-attested session.
|
|
69
|
+
const authPayload = {
|
|
70
|
+
type: 'auth',
|
|
71
|
+
apiKey: this.apiKey,
|
|
72
|
+
};
|
|
73
|
+
if (sessionToken)
|
|
74
|
+
authPayload.sessionToken = sessionToken;
|
|
75
|
+
if (this.appVersion)
|
|
76
|
+
authPayload.appVersion = this.appVersion;
|
|
77
|
+
try {
|
|
78
|
+
this.ws?.send(JSON.stringify(authPayload));
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// ignore — onerror/onclose will surface failures
|
|
82
|
+
}
|
|
83
|
+
resolve();
|
|
84
|
+
};
|
|
85
|
+
this.ws.onmessage = (event) => {
|
|
86
|
+
try {
|
|
87
|
+
const data = JSON.parse(event.data);
|
|
88
|
+
this.handleEvent(data);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// Ignore malformed messages
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
this.ws.onerror = () => {
|
|
95
|
+
reject(new Error('WebSocket connection failed'));
|
|
96
|
+
};
|
|
97
|
+
this.ws.onclose = (event) => {
|
|
98
|
+
this.cleanup();
|
|
99
|
+
this.eventHandler?.({
|
|
100
|
+
type: 'session.closed',
|
|
101
|
+
reason: event.reason || 'CONNECTION_CLOSED',
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
audioDeltasReceived = 0;
|
|
107
|
+
handleEvent(event) {
|
|
108
|
+
if (event.type === 'response.audio.delta' && 'delta' in event) {
|
|
109
|
+
this.audioDeltasReceived++;
|
|
110
|
+
QafkaAudio_1.QafkaAudio.playAudioChunk(event.delta).catch(() => {
|
|
111
|
+
// playback errors are surfaced to the consumer via eventHandler
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
this.eventHandler?.(event);
|
|
115
|
+
}
|
|
116
|
+
attachAudioListener() {
|
|
117
|
+
if (this.audioUnsubscribe)
|
|
118
|
+
return; // already attached
|
|
119
|
+
this.audioUnsubscribe = QafkaAudio_1.QafkaAudio.onAudioData(({ audio }) => {
|
|
120
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
121
|
+
this.ws.send(JSON.stringify({ type: 'input_audio_buffer.append', audio }));
|
|
122
|
+
this.audioChunksSent++;
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
detachAudioListener() {
|
|
127
|
+
this.audioUnsubscribe?.();
|
|
128
|
+
this.audioUnsubscribe = null;
|
|
129
|
+
}
|
|
130
|
+
async startAudioPipeline() {
|
|
131
|
+
try {
|
|
132
|
+
// Attach listener BEFORE startCapture so the native tap
|
|
133
|
+
// doesn't drop early buffers (hasListeners/listenerCount gate).
|
|
134
|
+
this.attachAudioListener();
|
|
135
|
+
await QafkaAudio_1.QafkaAudio.startCapture();
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
this.eventHandler?.({
|
|
139
|
+
type: 'error',
|
|
140
|
+
message: `Mic capture failed: ${error}`,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Fully stop the native audio engine (capture + playback). Use when leaving
|
|
146
|
+
* voice mode entirely (e.g. scrolling back to chat page). NOT for tool-flow
|
|
147
|
+
* pauses — that breaks the AI's turn-2 audio playback because iOS
|
|
148
|
+
* AVAudioEngine unifies capture + playback in one engine instance.
|
|
149
|
+
*/
|
|
150
|
+
async pauseMic() {
|
|
151
|
+
await QafkaAudio_1.QafkaAudio.stopCapture();
|
|
152
|
+
this.detachAudioListener();
|
|
153
|
+
}
|
|
154
|
+
async resumeMic() {
|
|
155
|
+
await this.startAudioPipeline();
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Stop forwarding mic audio to the server WHILE keeping the native audio
|
|
159
|
+
* engine running so the AI's turn-2 audio playback continues. Use during
|
|
160
|
+
* tool flows where the user shouldn't interrupt but the AI may still speak.
|
|
161
|
+
*/
|
|
162
|
+
muteMic() {
|
|
163
|
+
this.detachAudioListener();
|
|
164
|
+
}
|
|
165
|
+
unmuteMic() {
|
|
166
|
+
this.attachAudioListener();
|
|
167
|
+
}
|
|
168
|
+
sendToolResult(payload) {
|
|
169
|
+
if (this.ws?.readyState !== WebSocket.OPEN) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
this.ws.send(JSON.stringify({
|
|
173
|
+
type: 'tool_result',
|
|
174
|
+
toolCallId: payload.toolCallId,
|
|
175
|
+
toolName: payload.toolName,
|
|
176
|
+
output: payload.output,
|
|
177
|
+
ok: payload.ok,
|
|
178
|
+
error: payload.error,
|
|
179
|
+
}));
|
|
180
|
+
}
|
|
181
|
+
async disconnect() {
|
|
182
|
+
await this.cleanup();
|
|
183
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
184
|
+
this.ws.close();
|
|
185
|
+
}
|
|
186
|
+
this.ws = null;
|
|
187
|
+
this.audioChunksSent = 0;
|
|
188
|
+
}
|
|
189
|
+
async cleanup() {
|
|
190
|
+
this.audioUnsubscribe?.();
|
|
191
|
+
this.audioUnsubscribe = null;
|
|
192
|
+
try {
|
|
193
|
+
await QafkaAudio_1.QafkaAudio.stopCapture();
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
// Ignore cleanup errors
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
get isConnected() {
|
|
200
|
+
return this.ws?.readyState === WebSocket.OPEN;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
exports.RealtimeService = RealtimeService;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type QafkaStorage } from './storageCore';
|
|
2
|
+
/**
|
|
3
|
+
* Singleton storage wrapper used throughout the SDK.
|
|
4
|
+
*
|
|
5
|
+
* - Backed by Keychain (iOS) / EncryptedSharedPreferences (Android) when the
|
|
6
|
+
* native QafkaStorage module is registered.
|
|
7
|
+
* - Falls back to in-memory Map (with a single console.warn) if the native
|
|
8
|
+
* module is missing — which should only happen if autolinking failed or the
|
|
9
|
+
* host app was not rebuilt after upgrading the SDK.
|
|
10
|
+
*/
|
|
11
|
+
export declare const storage: QafkaStorage;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.storage = void 0;
|
|
4
|
+
const QafkaStorage_1 = require("../native/QafkaStorage");
|
|
5
|
+
const storageCore_1 = require("./storageCore");
|
|
6
|
+
/**
|
|
7
|
+
* Singleton storage wrapper used throughout the SDK.
|
|
8
|
+
*
|
|
9
|
+
* - Backed by Keychain (iOS) / EncryptedSharedPreferences (Android) when the
|
|
10
|
+
* native QafkaStorage module is registered.
|
|
11
|
+
* - Falls back to in-memory Map (with a single console.warn) if the native
|
|
12
|
+
* module is missing — which should only happen if autolinking failed or the
|
|
13
|
+
* host app was not rebuilt after upgrading the SDK.
|
|
14
|
+
*/
|
|
15
|
+
exports.storage = (0, storageCore_1.createStorage)((0, QafkaStorage_1.getNativeStorage)());
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native storage contract. Mirrors AsyncStorage's surface for the methods
|
|
3
|
+
* this SDK actually uses (getItem / setItem / removeItem / multiRemove).
|
|
4
|
+
*/
|
|
5
|
+
export interface QafkaStorageNative {
|
|
6
|
+
getItem(key: string): Promise<string | null>;
|
|
7
|
+
setItem(key: string, value: string): Promise<void>;
|
|
8
|
+
removeItem(key: string): Promise<void>;
|
|
9
|
+
multiRemove(keys: string[]): Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
export interface QafkaStorage {
|
|
12
|
+
getItem(key: string): Promise<string | null>;
|
|
13
|
+
setItem(key: string, value: string): Promise<void>;
|
|
14
|
+
removeItem(key: string): Promise<void>;
|
|
15
|
+
multiRemove(keys: string[]): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
export declare function createStorage(native: QafkaStorageNative | null): QafkaStorage;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createStorage = createStorage;
|
|
4
|
+
const FALLBACK_WARNING = '[Qafka] Native QafkaStorage module not registered — falling back to ' +
|
|
5
|
+
'in-memory storage. Persistence will be lost when the app closes. ' +
|
|
6
|
+
'This usually means autolinking failed or the app was not rebuilt after ' +
|
|
7
|
+
'upgrading the SDK.';
|
|
8
|
+
function createStorage(native) {
|
|
9
|
+
if (native) {
|
|
10
|
+
return {
|
|
11
|
+
getItem: (key) => native.getItem(key),
|
|
12
|
+
setItem: (key, value) => native.setItem(key, value),
|
|
13
|
+
removeItem: (key) => native.removeItem(key),
|
|
14
|
+
multiRemove: (keys) => native.multiRemove(keys),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
const memory = new Map();
|
|
18
|
+
let warned = false;
|
|
19
|
+
const warnOnce = () => {
|
|
20
|
+
if (!warned) {
|
|
21
|
+
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
22
|
+
console.warn(FALLBACK_WARNING);
|
|
23
|
+
}
|
|
24
|
+
warned = true;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
return {
|
|
28
|
+
async getItem(key) {
|
|
29
|
+
warnOnce();
|
|
30
|
+
return memory.has(key) ? memory.get(key) : null;
|
|
31
|
+
},
|
|
32
|
+
async setItem(key, value) {
|
|
33
|
+
warnOnce();
|
|
34
|
+
memory.set(key, value);
|
|
35
|
+
},
|
|
36
|
+
async removeItem(key) {
|
|
37
|
+
warnOnce();
|
|
38
|
+
memory.delete(key);
|
|
39
|
+
},
|
|
40
|
+
async multiRemove(keys) {
|
|
41
|
+
warnOnce();
|
|
42
|
+
for (const k of keys)
|
|
43
|
+
memory.delete(k);
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|