@qafka/react-native 2.1.1 → 2.3.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/dist/components/Qafka.js +5 -3
- package/dist/components/Qafka.types.d.ts +11 -0
- package/dist/components/VoicePage.d.ts +7 -2
- package/dist/components/VoicePage.js +7 -2
- package/dist/decide-voice-navigation.d.ts +19 -0
- package/dist/decide-voice-navigation.js +22 -0
- package/dist/hooks/useChatMessages.js +10 -2
- package/dist/hooks/useSDK.d.ts +5 -2
- package/dist/hooks/useSDK.js +7 -8
- package/dist/hooks/useVoiceChat.d.ts +14 -1
- package/dist/hooks/useVoiceChat.js +35 -1
- package/dist/resolve-runtime-config-source.d.ts +9 -0
- package/dist/resolve-runtime-config-source.js +15 -0
- package/dist/services/RealtimeService.d.ts +9 -0
- package/dist/types/navigation.d.ts +4 -0
- package/package.json +1 -1
package/dist/components/Qafka.js
CHANGED
|
@@ -86,11 +86,12 @@ const { width: SCREEN_WIDTH } = react_native_1.Dimensions.get('window');
|
|
|
86
86
|
* />
|
|
87
87
|
* ```
|
|
88
88
|
*/
|
|
89
|
-
exports.Qafka = (0, react_1.forwardRef)(function Qafka({ style, apiUrl: apiUrlProp, subProjectId, projectId, locale, mode = 'fullscreen', theme: themeName = 'light', customTheme, themeOverride, isAuthenticated, endUserId, endUserData, enableStreaming = true, voiceEnabled: voiceEnabledProp = true, context: userContext, contextDescription, components: customComponents, showTimestamps = true, placeholder = 'Type a message...', maxMessageLength = 500, greetingMessage, onReady, onMessageSent, onResponseReceived, onError, onNavigationSuggest, onNavigationAction, onExternalSuggestion, onCardDeepLink, onCardSuggestMessage, onCardExternalNavigation, onCardShare, onCardCopy, onCardToolTrigger, onCardCTAClick, onToolSuggested, onToolDataRequested, onActionResult, onStepCompleted, onFileUploadRequest, onExtractionResult, onClose, onBack, CloseComponent, BackComponent, navigationLabelFormat, NavigationButtonComponent, voiceComponents, voiceTranscript = 'centered', toolRenderMode, }, ref) {
|
|
89
|
+
exports.Qafka = (0, react_1.forwardRef)(function Qafka({ style, apiUrl: apiUrlProp, devConfig, subProjectId, projectId, locale, mode = 'fullscreen', theme: themeName = 'light', customTheme, themeOverride, isAuthenticated, endUserId, endUserData, enableStreaming = true, voiceEnabled: voiceEnabledProp = true, context: userContext, contextDescription, components: customComponents, showTimestamps = true, placeholder = 'Type a message...', maxMessageLength = 500, greetingMessage, onReady, onMessageSent, onResponseReceived, onError, onNavigationSuggest, onNavigationAction, onExternalSuggestion, onCardDeepLink, onCardSuggestMessage, onCardExternalNavigation, onCardShare, onCardCopy, onCardToolTrigger, onCardCTAClick, onToolSuggested, onToolDataRequested, onActionResult, onStepCompleted, onFileUploadRequest, onExtractionResult, onClose, onBack, CloseComponent, BackComponent, navigationLabelFormat, NavigationButtonComponent, voiceComponents, voiceTranscript = 'centered', toolRenderMode, }, ref) {
|
|
90
90
|
// === ALL HOOKS FIRST (Rules of Hooks compliance) ===
|
|
91
91
|
const insets = (0, react_native_safe_area_context_1.useSafeAreaInsets)();
|
|
92
92
|
const { sdkReady, error: sdkError, resolvedApiKey, resolvedApiUrl } = (0, useSDK_1.useSDK)({
|
|
93
93
|
apiUrl: apiUrlProp,
|
|
94
|
+
devConfig,
|
|
94
95
|
subProjectId,
|
|
95
96
|
projectId,
|
|
96
97
|
locale,
|
|
@@ -143,8 +144,9 @@ exports.Qafka = (0, react_1.forwardRef)(function Qafka({ style, apiUrl: apiUrlPr
|
|
|
143
144
|
// grouped with text-mode rows for the same user.
|
|
144
145
|
endUserId: validatedEndUserId,
|
|
145
146
|
endUserData: validatedEndUserData,
|
|
147
|
+
onNavigationSuggest,
|
|
146
148
|
});
|
|
147
|
-
const { toolStatus: voiceToolStatus, renderedTools: voiceRenderedTools, transcriptOverrideForTurn, setToolStatusManually, clearRenderedTools, connect: voiceConnect, disconnect: voiceDisconnect, pauseMic: voicePauseMic, resumeMic: voiceResumeMic, isMuted: voiceIsMuted, mute: voiceMute, unmute: voiceUnmute, toggleMute: voiceToggleMute, } = voiceChat;
|
|
149
|
+
const { toolStatus: voiceToolStatus, renderedTools: voiceRenderedTools, transcriptOverrideForTurn, setToolStatusManually, clearRenderedTools, connect: voiceConnect, disconnect: voiceDisconnect, pauseMic: voicePauseMic, resumeMic: voiceResumeMic, isMuted: voiceIsMuted, mute: voiceMute, unmute: voiceUnmute, toggleMute: voiceToggleMute, navigationSuggestion: voiceNavigationSuggestion, acceptNavigationSuggestion: voiceAcceptNavigation, dismissNavigationSuggestion: voiceDismissNavigation, } = voiceChat;
|
|
148
150
|
const { messages, isTyping, isSending, streamingMessage, toolStatus, flatListRef, handleSend, setMessages, setIsTyping, setStreamingMessage, } = (0, useChatMessages_1.useChatMessages)({
|
|
149
151
|
enableStreaming,
|
|
150
152
|
context: currentContext,
|
|
@@ -465,7 +467,7 @@ exports.Qafka = (0, react_1.forwardRef)(function Qafka({ style, apiUrl: apiUrlPr
|
|
|
465
467
|
|
|
466
468
|
{/* Page 1: Voice — VoicePage paints Background edge-to-edge and
|
|
467
469
|
handles its own internal safe-area padding for orb/transcript. */}
|
|
468
|
-
<VoicePage_1.VoicePage state={voiceChat.state} transcript={voiceChat.transcript} userTranscript={voiceChat.userTranscript} transcriptHistory={voiceChat.transcriptHistory} transcriptMode={voiceTranscript} amplitude={voiceChat.amplitude} theme={theme} voiceComponents={voiceComponents} toolStatus={voiceToolStatus} renderedTools={voiceRenderedTools} registeredComponents={customComponents} transcriptOverrideForTurn={transcriptOverrideForTurn} DataChipListComponent={voiceComponents?.dataChipList} isMuted={voiceIsMuted} onToggleMute={voiceToggleMute}/>
|
|
470
|
+
<VoicePage_1.VoicePage state={voiceChat.state} transcript={voiceChat.transcript} userTranscript={voiceChat.userTranscript} transcriptHistory={voiceChat.transcriptHistory} transcriptMode={voiceTranscript} amplitude={voiceChat.amplitude} theme={theme} voiceComponents={voiceComponents} toolStatus={voiceToolStatus} renderedTools={voiceRenderedTools} registeredComponents={customComponents} transcriptOverrideForTurn={transcriptOverrideForTurn} DataChipListComponent={voiceComponents?.dataChipList} isMuted={voiceIsMuted} onToggleMute={voiceToggleMute} navigationSuggestion={voiceNavigationSuggestion} onNavigationAccept={voiceAcceptNavigation} onNavigationDismiss={voiceDismissNavigation} NavigationButtonComponent={NavigationButtonComponent}/>
|
|
469
471
|
</react_native_1.ScrollView>
|
|
470
472
|
</react_native_1.View>) : (<ChatPage_1.ChatPage {...chatPageProps}/>);
|
|
471
473
|
return (<react_native_1.View style={[
|
|
@@ -4,6 +4,7 @@ import { ComponentRegistry } from '../types/components';
|
|
|
4
4
|
import { ExternalSuggestion } from '../types/external-navigation';
|
|
5
5
|
import { NavigationSuggestion } from '../types/navigation';
|
|
6
6
|
import { VoiceChatState } from './VoicePage';
|
|
7
|
+
import type { RuntimeConfig } from '../runtime-config-loader';
|
|
7
8
|
/**
|
|
8
9
|
* Augmentation hook for consumer-side projectId literal types.
|
|
9
10
|
*
|
|
@@ -256,6 +257,16 @@ export interface QafkaProps {
|
|
|
256
257
|
* Backend API URL (OPTIONAL — advanced, leave unset for production).
|
|
257
258
|
*/
|
|
258
259
|
apiUrl?: string;
|
|
260
|
+
/**
|
|
261
|
+
* Dev-only runtime config the SDK reads for the simulator/emulator
|
|
262
|
+
* `__DEV__` auth path. Pass the CLI-managed file:
|
|
263
|
+
* `devConfig={__DEV__ ? require('../.qafka/qafka-runtime') : undefined}`
|
|
264
|
+
* Omit on real devices / production — attestation handles auth there, and
|
|
265
|
+
* `require` under `__DEV__` is dead-code-eliminated from release bundles so
|
|
266
|
+
* no key reaches the prod build. When omitted, the SDK falls back to the
|
|
267
|
+
* legacy `node_modules` config (backward compatible).
|
|
268
|
+
*/
|
|
269
|
+
devConfig?: RuntimeConfig | null;
|
|
259
270
|
/**
|
|
260
271
|
* Sub-Project Identifier (OPTIONAL)
|
|
261
272
|
*
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import type { ComponentType } from 'react';
|
|
3
|
-
import type { VoiceComponents, VoiceTranscriptMode, VoiceTranscriptTurn } from './Qafka.types';
|
|
3
|
+
import type { VoiceComponents, VoiceTranscriptMode, VoiceTranscriptTurn, NavigationButtonProps } from './Qafka.types';
|
|
4
|
+
import type { NavigationSuggestion } from '../types/navigation';
|
|
4
5
|
export type VoiceChatState = 'idle' | 'connecting' | 'listening' | 'thinking' | 'speaking';
|
|
5
6
|
interface VoiceToolStatusShape {
|
|
6
7
|
toolCallId: string;
|
|
@@ -43,6 +44,10 @@ interface VoicePageProps {
|
|
|
43
44
|
}>;
|
|
44
45
|
isMuted?: boolean;
|
|
45
46
|
onToggleMute?: () => void;
|
|
47
|
+
navigationSuggestion?: NavigationSuggestion | null;
|
|
48
|
+
onNavigationAccept?: () => void;
|
|
49
|
+
onNavigationDismiss?: () => void;
|
|
50
|
+
NavigationButtonComponent?: ComponentType<NavigationButtonProps>;
|
|
46
51
|
}
|
|
47
|
-
export declare function VoicePage({ state, transcript, userTranscript, transcriptHistory, transcriptMode, amplitude, theme, voiceComponents, toolStatus, renderedTools, registeredComponents, transcriptOverrideForTurn, DataChipListComponent, isMuted, onToggleMute, }: VoicePageProps): React.JSX.Element;
|
|
52
|
+
export declare function VoicePage({ state, transcript, userTranscript, transcriptHistory, transcriptMode, amplitude, theme, voiceComponents, toolStatus, renderedTools, registeredComponents, transcriptOverrideForTurn, DataChipListComponent, isMuted, onToggleMute, navigationSuggestion, onNavigationAccept, onNavigationDismiss, NavigationButtonComponent, }: VoicePageProps): React.JSX.Element;
|
|
48
53
|
export {};
|
|
@@ -39,6 +39,7 @@ const react_native_1 = require("react-native");
|
|
|
39
39
|
const react_native_safe_area_context_1 = require("react-native-safe-area-context");
|
|
40
40
|
const ToolStatusPill_1 = require("./ToolStatusPill");
|
|
41
41
|
const DataChipList_1 = require("./DataChipList");
|
|
42
|
+
const NavigationSuggestion_1 = require("./NavigationSuggestion");
|
|
42
43
|
const { width: SCREEN_WIDTH } = react_native_1.Dimensions.get('window');
|
|
43
44
|
const STATE_LABELS = {
|
|
44
45
|
idle: 'Waiting...', connecting: 'Connecting...', listening: 'Listening...',
|
|
@@ -395,7 +396,7 @@ function ChatTranscript({ history, liveAiText, state, primaryTextColor, secondar
|
|
|
395
396
|
</react_native_1.View>);
|
|
396
397
|
}
|
|
397
398
|
// --- Main component ---
|
|
398
|
-
function VoicePage({ state, transcript, userTranscript, transcriptHistory, transcriptMode = 'centered', amplitude, theme, voiceComponents, toolStatus, renderedTools, registeredComponents, transcriptOverrideForTurn = false, DataChipListComponent, isMuted = false, onToggleMute, }) {
|
|
399
|
+
function VoicePage({ state, transcript, userTranscript, transcriptHistory, transcriptMode = 'centered', amplitude, theme, voiceComponents, toolStatus, renderedTools, registeredComponents, transcriptOverrideForTurn = false, DataChipListComponent, isMuted = false, onToggleMute, navigationSuggestion, onNavigationAccept, onNavigationDismiss, NavigationButtonComponent, }) {
|
|
399
400
|
const Indicator = voiceComponents?.VoiceIndicator || DefaultVoiceIndicator;
|
|
400
401
|
const Background = voiceComponents?.VoiceBackground || DefaultVoiceBackground;
|
|
401
402
|
const Transcript = voiceComponents?.VoiceTranscript || DefaultVoiceTranscript;
|
|
@@ -515,6 +516,8 @@ function VoicePage({ state, transcript, userTranscript, transcriptHistory, trans
|
|
|
515
516
|
{showTranscript ? (<Transcript transcript={transcript} userTranscript={userTranscript} state={state} theme={theme} mode="centered"/>) : null}
|
|
516
517
|
</react_native_1.View>)}
|
|
517
518
|
|
|
519
|
+
{!hasToolUI && navigationSuggestion ? (NavigationButtonComponent ? (<NavigationButtonComponent screenName={navigationSuggestion.screenName} suggestion={navigationSuggestion} onPress={onNavigationAccept ?? (() => { })} theme={theme} style="primary" label={navigationSuggestion.message || `Navigate to ${navigationSuggestion.screenName}`}/>) : (<NavigationSuggestion_1.NavigationSuggestionCard screenName={navigationSuggestion.screenName} message={navigationSuggestion.message} onAccept={onNavigationAccept ?? (() => { })} onDismiss={onNavigationDismiss ?? (() => { })} theme={theme}/>)) : null}
|
|
520
|
+
|
|
518
521
|
{hasToolUI ? (<react_native_1.ScrollView style={{ flex: 1, width: '100%', alignSelf: 'stretch' }} contentContainerStyle={{
|
|
519
522
|
paddingHorizontal: 20,
|
|
520
523
|
paddingTop: 16,
|
|
@@ -569,7 +572,9 @@ function VoicePage({ state, transcript, userTranscript, transcriptHistory, trans
|
|
|
569
572
|
|
|
570
573
|
{showMute && !hasToolUI ? (<react_native_1.View style={{
|
|
571
574
|
position: 'absolute',
|
|
572
|
-
|
|
575
|
+
// Absolute children aren't pushed by the parent's safe-area paddingTop,
|
|
576
|
+
// so add the inset here to keep the button clear of the status bar / notch.
|
|
577
|
+
top: insets.top + 12,
|
|
573
578
|
right: 16,
|
|
574
579
|
}}>
|
|
575
580
|
<MuteButton isMuted={isMuted} onToggle={onToggleMute} theme={theme} state={state}/>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { NavigationSuggestion } from './types/navigation';
|
|
2
|
+
export interface VoiceNavigationDecision {
|
|
3
|
+
action: 'callback' | 'suggest';
|
|
4
|
+
suggestion: NavigationSuggestion;
|
|
5
|
+
}
|
|
6
|
+
interface IncomingVoiceNavigation {
|
|
7
|
+
screenName: string;
|
|
8
|
+
route: string;
|
|
9
|
+
deeplink?: string | null;
|
|
10
|
+
message?: string;
|
|
11
|
+
trigger?: 'user' | 'ai';
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Decide how the voice UI should react to a backend navigation event.
|
|
15
|
+
* trigger='user' → app developer navigates immediately (callback).
|
|
16
|
+
* trigger='ai' (or missing) → SDK shows a suggestion button.
|
|
17
|
+
*/
|
|
18
|
+
export declare function decideVoiceNavigation(nav: IncomingVoiceNavigation): VoiceNavigationDecision;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.decideVoiceNavigation = decideVoiceNavigation;
|
|
4
|
+
/**
|
|
5
|
+
* Decide how the voice UI should react to a backend navigation event.
|
|
6
|
+
* trigger='user' → app developer navigates immediately (callback).
|
|
7
|
+
* trigger='ai' (or missing) → SDK shows a suggestion button.
|
|
8
|
+
*/
|
|
9
|
+
function decideVoiceNavigation(nav) {
|
|
10
|
+
const suggestion = {
|
|
11
|
+
screenName: nav.screenName,
|
|
12
|
+
route: nav.route,
|
|
13
|
+
deeplink: nav.deeplink ?? null,
|
|
14
|
+
...(nav.message ? { message: nav.message } : {}),
|
|
15
|
+
trigger: nav.trigger === 'user' ? 'user' : 'ai',
|
|
16
|
+
source: 'voice',
|
|
17
|
+
};
|
|
18
|
+
return {
|
|
19
|
+
action: nav.trigger === 'user' ? 'callback' : 'suggest',
|
|
20
|
+
suggestion,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -168,7 +168,11 @@ const useChatMessages = ({ enableStreaming = true, context = {}, contextDescript
|
|
|
168
168
|
}
|
|
169
169
|
// Handle navigation suggestion
|
|
170
170
|
if (response.navigationSuggestion && onNavigationSuggest) {
|
|
171
|
-
onNavigationSuggest(
|
|
171
|
+
onNavigationSuggest({
|
|
172
|
+
...response.navigationSuggestion,
|
|
173
|
+
trigger: response.navigationSuggestion.trigger ?? 'ai',
|
|
174
|
+
source: 'text',
|
|
175
|
+
});
|
|
172
176
|
}
|
|
173
177
|
setIsSending(false);
|
|
174
178
|
},
|
|
@@ -458,7 +462,11 @@ const useChatMessages = ({ enableStreaming = true, context = {}, contextDescript
|
|
|
458
462
|
setMessages((prev) => [...prev, aiMessage]);
|
|
459
463
|
onResponseReceived?.(response);
|
|
460
464
|
if (response.navigationSuggestion && onNavigationSuggest) {
|
|
461
|
-
onNavigationSuggest(
|
|
465
|
+
onNavigationSuggest({
|
|
466
|
+
...response.navigationSuggestion,
|
|
467
|
+
trigger: response.navigationSuggestion.trigger ?? 'ai',
|
|
468
|
+
source: 'text',
|
|
469
|
+
});
|
|
462
470
|
}
|
|
463
471
|
scrollToBottom();
|
|
464
472
|
setIsTyping(false);
|
package/dist/hooks/useSDK.d.ts
CHANGED
|
@@ -10,12 +10,15 @@ export interface UseSDKOptions {
|
|
|
10
10
|
locale?: string;
|
|
11
11
|
onReady?: () => void;
|
|
12
12
|
onError?: (error: Error) => void;
|
|
13
|
+
devConfig?: unknown;
|
|
13
14
|
}
|
|
14
15
|
export interface UseSDKResult {
|
|
15
16
|
sdkReady: boolean;
|
|
16
17
|
error: string | null;
|
|
17
18
|
/**
|
|
18
|
-
* Dev-only: the resolved developmentKey from
|
|
19
|
+
* Dev-only: the resolved developmentKey from the consumer dev config — the
|
|
20
|
+
* `devConfig` prop (`.qafka/qafka-runtime.js`) or the legacy node_modules
|
|
21
|
+
* fallback. Null in
|
|
19
22
|
* production builds (Metro dead-code-eliminates the dev config branch).
|
|
20
23
|
* Used by `useVoiceChat` whose WebSocket auth still relies on apiKey.
|
|
21
24
|
*/
|
|
@@ -28,4 +31,4 @@ export interface UseSDKResult {
|
|
|
28
31
|
/**
|
|
29
32
|
* Custom hook for managing SDK initialization and lifecycle
|
|
30
33
|
*/
|
|
31
|
-
export declare const useSDK: ({ apiUrl, subProjectId, projectId, locale, onReady, onError, }: UseSDKOptions) => UseSDKResult;
|
|
34
|
+
export declare const useSDK: ({ apiUrl, subProjectId, projectId, locale, onReady, onError, devConfig, }: UseSDKOptions) => UseSDKResult;
|
package/dist/hooks/useSDK.js
CHANGED
|
@@ -5,10 +5,11 @@ const react_1 = require("react");
|
|
|
5
5
|
const QafkaSDK_1 = require("../QafkaSDK");
|
|
6
6
|
const runtime_config_loader_1 = require("../runtime-config-loader");
|
|
7
7
|
const resolve_project_config_1 = require("../resolve-project-config");
|
|
8
|
+
const resolve_runtime_config_source_1 = require("../resolve-runtime-config-source");
|
|
8
9
|
/**
|
|
9
10
|
* Custom hook for managing SDK initialization and lifecycle
|
|
10
11
|
*/
|
|
11
|
-
const useSDK = ({ apiUrl, subProjectId, projectId, locale, onReady, onError, }) => {
|
|
12
|
+
const useSDK = ({ apiUrl, subProjectId, projectId, locale, onReady, onError, devConfig, }) => {
|
|
12
13
|
const [sdkReady, setSdkReady] = (0, react_1.useState)(false);
|
|
13
14
|
const [error, setError] = (0, react_1.useState)(null);
|
|
14
15
|
const [resolvedApiKey, setResolvedApiKey] = (0, react_1.useState)(null);
|
|
@@ -27,18 +28,16 @@ const useSDK = ({ apiUrl, subProjectId, projectId, locale, onReady, onError, })
|
|
|
27
28
|
let apiKey = null;
|
|
28
29
|
let resolvedUrl = apiUrl;
|
|
29
30
|
if (__DEV__) {
|
|
30
|
-
// Dev path:
|
|
31
|
-
// Metro dead-code elimination removes this
|
|
32
|
-
//
|
|
31
|
+
// Dev path: prefer the consumer-supplied .qafka/ config, else the legacy
|
|
32
|
+
// node_modules config. Metro dead-code elimination removes this whole
|
|
33
|
+
// branch (and both requires) from production builds.
|
|
33
34
|
try {
|
|
34
|
-
const cfg = (0, resolve_project_config_1.resolveProjectConfig)({ apiUrl, projectId }, (0, runtime_config_loader_1.loadRuntimeConfig)());
|
|
35
|
+
const cfg = (0, resolve_project_config_1.resolveProjectConfig)({ apiUrl, projectId }, (0, resolve_runtime_config_source_1.resolveRuntimeConfigSource)(devConfig, (0, runtime_config_loader_1.loadRuntimeConfig)()));
|
|
35
36
|
apiKey = cfg.apiKey;
|
|
36
37
|
resolvedUrl = cfg.apiUrl;
|
|
37
38
|
}
|
|
38
39
|
catch (err) {
|
|
39
|
-
|
|
40
|
-
console.warn('[Qafka] Dev config missing or invalid:', err instanceof Error ? err.message : err);
|
|
41
|
-
}
|
|
40
|
+
console.warn('[Qafka] Dev config missing or invalid:', err instanceof Error ? err.message : err);
|
|
42
41
|
}
|
|
43
42
|
}
|
|
44
43
|
// Expose resolved values so `useVoiceChat` (WebSocket auth still
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { NavigationSuggestion } from '../types/navigation';
|
|
1
2
|
export type VoiceChatState = 'idle' | 'connecting' | 'listening' | 'thinking' | 'speaking';
|
|
2
3
|
export interface VoiceToolStatus {
|
|
3
4
|
toolCallId: string;
|
|
@@ -77,6 +78,12 @@ interface UseVoiceChatOptions {
|
|
|
77
78
|
*/
|
|
78
79
|
endUserId?: string;
|
|
79
80
|
endUserData?: Record<string, unknown>;
|
|
81
|
+
/**
|
|
82
|
+
* Callback invoked when the backend suggests navigation.
|
|
83
|
+
* trigger='user' → fires immediately (no button shown).
|
|
84
|
+
* trigger='ai' → fires when the user accepts the suggestion button.
|
|
85
|
+
*/
|
|
86
|
+
onNavigationSuggest?: (suggestion: NavigationSuggestion) => void;
|
|
80
87
|
}
|
|
81
88
|
interface UseVoiceChatResult {
|
|
82
89
|
state: VoiceChatState;
|
|
@@ -106,5 +113,11 @@ interface UseVoiceChatResult {
|
|
|
106
113
|
mute: () => void;
|
|
107
114
|
unmute: () => void;
|
|
108
115
|
toggleMute: () => void;
|
|
116
|
+
/** Pending navigation suggestion waiting for user acceptance (trigger='ai'). */
|
|
117
|
+
navigationSuggestion: NavigationSuggestion | null;
|
|
118
|
+
/** Accept the pending suggestion: fires onNavigationSuggest and clears state. */
|
|
119
|
+
acceptNavigationSuggestion: () => void;
|
|
120
|
+
/** Dismiss the pending suggestion without navigating. */
|
|
121
|
+
dismissNavigationSuggestion: () => void;
|
|
109
122
|
}
|
|
110
|
-
export declare function useVoiceChat({ apiUrl, apiKey, userContext, contextDescription, onToolSuggested, toolRenderMode, getSessionToken, endUserId, endUserData, }: UseVoiceChatOptions): UseVoiceChatResult;
|
|
123
|
+
export declare function useVoiceChat({ apiUrl, apiKey, userContext, contextDescription, onToolSuggested, toolRenderMode, getSessionToken, endUserId, endUserData, onNavigationSuggest, }: UseVoiceChatOptions): UseVoiceChatResult;
|
|
@@ -5,7 +5,8 @@ const react_1 = require("react");
|
|
|
5
5
|
const react_native_1 = require("react-native");
|
|
6
6
|
const RealtimeService_1 = require("../services/RealtimeService");
|
|
7
7
|
const QafkaAudio_1 = require("../native/QafkaAudio");
|
|
8
|
-
|
|
8
|
+
const decide_voice_navigation_1 = require("../decide-voice-navigation");
|
|
9
|
+
function useVoiceChat({ apiUrl, apiKey, userContext, contextDescription, onToolSuggested, toolRenderMode = 'upsert', getSessionToken, endUserId, endUserData, onNavigationSuggest, }) {
|
|
9
10
|
const [state, setState] = (0, react_1.useState)('idle');
|
|
10
11
|
const [transcript, setTranscript] = (0, react_1.useState)('');
|
|
11
12
|
const [userTranscript, setUserTranscript] = (0, react_1.useState)('');
|
|
@@ -14,6 +15,11 @@ function useVoiceChat({ apiUrl, apiKey, userContext, contextDescription, onToolS
|
|
|
14
15
|
const [toolStatus, setToolStatus] = (0, react_1.useState)(null);
|
|
15
16
|
const [renderedTools, setRenderedTools] = (0, react_1.useState)([]);
|
|
16
17
|
const [transcriptOverrideForTurn, setTranscriptOverrideForTurn] = (0, react_1.useState)(false);
|
|
18
|
+
const [navigationSuggestion, setNavigationSuggestion] = (0, react_1.useState)(null);
|
|
19
|
+
const navigationSuggestionRef = (0, react_1.useRef)(null);
|
|
20
|
+
(0, react_1.useEffect)(() => {
|
|
21
|
+
navigationSuggestionRef.current = navigationSuggestion;
|
|
22
|
+
}, [navigationSuggestion]);
|
|
17
23
|
const serviceRef = (0, react_1.useRef)(null);
|
|
18
24
|
// Snapshot of the live AI transcript so `response.done` can push the
|
|
19
25
|
// finished text into history without depending on stale closure state.
|
|
@@ -23,6 +29,10 @@ function useVoiceChat({ apiUrl, apiKey, userContext, contextDescription, onToolS
|
|
|
23
29
|
(0, react_1.useEffect)(() => {
|
|
24
30
|
onToolSuggestedRef.current = onToolSuggested;
|
|
25
31
|
}, [onToolSuggested]);
|
|
32
|
+
const onNavigationSuggestRef = (0, react_1.useRef)(onNavigationSuggest);
|
|
33
|
+
(0, react_1.useEffect)(() => {
|
|
34
|
+
onNavigationSuggestRef.current = onNavigationSuggest;
|
|
35
|
+
}, [onNavigationSuggest]);
|
|
26
36
|
// handleEvent is wrapped in useCallback([]) so we read the live value
|
|
27
37
|
// through a ref to avoid stale closures.
|
|
28
38
|
const toolRenderModeRef = (0, react_1.useRef)(toolRenderMode);
|
|
@@ -110,6 +120,7 @@ function useVoiceChat({ apiUrl, apiKey, userContext, contextDescription, onToolS
|
|
|
110
120
|
}
|
|
111
121
|
case 'session.closed':
|
|
112
122
|
setState('idle');
|
|
123
|
+
setNavigationSuggestion(null);
|
|
113
124
|
return;
|
|
114
125
|
case 'error':
|
|
115
126
|
setState('idle');
|
|
@@ -316,6 +327,16 @@ function useVoiceChat({ apiUrl, apiKey, userContext, contextDescription, onToolS
|
|
|
316
327
|
setToolStatus((s) => (s?.toolCallId === event.toolCallId ? null : s));
|
|
317
328
|
systemUnmute();
|
|
318
329
|
return;
|
|
330
|
+
case 'navigation': {
|
|
331
|
+
const decision = (0, decide_voice_navigation_1.decideVoiceNavigation)(event.navigation);
|
|
332
|
+
if (decision.action === 'callback') {
|
|
333
|
+
onNavigationSuggestRef.current?.(decision.suggestion);
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
setNavigationSuggestion(decision.suggestion);
|
|
337
|
+
}
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
319
340
|
}
|
|
320
341
|
}, []);
|
|
321
342
|
const connect = (0, react_1.useCallback)(async () => {
|
|
@@ -369,6 +390,7 @@ function useVoiceChat({ apiUrl, apiKey, userContext, contextDescription, onToolS
|
|
|
369
390
|
setTranscriptHistory([]);
|
|
370
391
|
transcriptRef.current = '';
|
|
371
392
|
setAmplitude(0);
|
|
393
|
+
setNavigationSuggestion(null);
|
|
372
394
|
// Reset both mute layers so the next session starts clean.
|
|
373
395
|
userMutedRef.current = false;
|
|
374
396
|
systemMutedRef.current = false;
|
|
@@ -380,6 +402,15 @@ function useVoiceChat({ apiUrl, apiKey, userContext, contextDescription, onToolS
|
|
|
380
402
|
const resumeMic = (0, react_1.useCallback)(async () => {
|
|
381
403
|
await serviceRef.current?.resumeMic();
|
|
382
404
|
}, []);
|
|
405
|
+
const acceptNavigationSuggestion = (0, react_1.useCallback)(() => {
|
|
406
|
+
const current = navigationSuggestionRef.current;
|
|
407
|
+
setNavigationSuggestion(null);
|
|
408
|
+
if (current)
|
|
409
|
+
onNavigationSuggestRef.current?.(current);
|
|
410
|
+
}, []);
|
|
411
|
+
const dismissNavigationSuggestion = (0, react_1.useCallback)(() => {
|
|
412
|
+
setNavigationSuggestion(null);
|
|
413
|
+
}, []);
|
|
383
414
|
// Disconnect on app background
|
|
384
415
|
(0, react_1.useEffect)(() => {
|
|
385
416
|
const handleAppState = (nextState) => {
|
|
@@ -432,5 +463,8 @@ function useVoiceChat({ apiUrl, apiKey, userContext, contextDescription, onToolS
|
|
|
432
463
|
mute,
|
|
433
464
|
unmute,
|
|
434
465
|
toggleMute,
|
|
466
|
+
navigationSuggestion,
|
|
467
|
+
acceptNavigationSuggestion,
|
|
468
|
+
dismissNavigationSuggestion,
|
|
435
469
|
};
|
|
436
470
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type RuntimeConfig } from './runtime-config-loader';
|
|
2
|
+
/**
|
|
3
|
+
* Pick the runtime config the SDK should use in dev builds.
|
|
4
|
+
*
|
|
5
|
+
* Precedence: a valid `devConfig` (the consumer's `require('../.qafka/qafka-runtime')`,
|
|
6
|
+
* shape-validated) wins; otherwise the value loaded from the SDK package
|
|
7
|
+
* (`loadRuntimeConfig()`). Returns null when neither yields a usable config.
|
|
8
|
+
*/
|
|
9
|
+
export declare function resolveRuntimeConfigSource(devConfig: unknown, loaded: RuntimeConfig | null): RuntimeConfig | null;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveRuntimeConfigSource = resolveRuntimeConfigSource;
|
|
4
|
+
const runtime_config_loader_1 = require("./runtime-config-loader");
|
|
5
|
+
/**
|
|
6
|
+
* Pick the runtime config the SDK should use in dev builds.
|
|
7
|
+
*
|
|
8
|
+
* Precedence: a valid `devConfig` (the consumer's `require('../.qafka/qafka-runtime')`,
|
|
9
|
+
* shape-validated) wins; otherwise the value loaded from the SDK package
|
|
10
|
+
* (`loadRuntimeConfig()`). Returns null when neither yields a usable config.
|
|
11
|
+
*/
|
|
12
|
+
function resolveRuntimeConfigSource(devConfig, loaded) {
|
|
13
|
+
const provided = devConfig != null ? (0, runtime_config_loader_1.normalizeRuntimeConfig)(devConfig) : null;
|
|
14
|
+
return provided ?? loaded;
|
|
15
|
+
}
|
|
@@ -31,6 +31,15 @@ export type RealtimeEvent = {
|
|
|
31
31
|
} | {
|
|
32
32
|
type: 'tool_done';
|
|
33
33
|
toolCallId: string;
|
|
34
|
+
} | {
|
|
35
|
+
type: 'navigation';
|
|
36
|
+
navigation: {
|
|
37
|
+
screenName: string;
|
|
38
|
+
route: string;
|
|
39
|
+
deeplink?: string | null;
|
|
40
|
+
message?: string;
|
|
41
|
+
trigger?: 'user' | 'ai';
|
|
42
|
+
};
|
|
34
43
|
};
|
|
35
44
|
export type RealtimeEventHandler = (event: RealtimeEvent) => void;
|
|
36
45
|
export declare class RealtimeService {
|
|
@@ -74,6 +74,10 @@ export interface NavigationSuggestion {
|
|
|
74
74
|
confirmed?: boolean;
|
|
75
75
|
message?: string;
|
|
76
76
|
reasoning?: string;
|
|
77
|
+
/** Who triggered this: 'user' = explicit request, 'ai' = proactive suggestion. */
|
|
78
|
+
trigger?: 'user' | 'ai';
|
|
79
|
+
/** Which mode produced it. */
|
|
80
|
+
source?: 'text' | 'voice';
|
|
77
81
|
}
|
|
78
82
|
/**
|
|
79
83
|
* Route matching result
|
package/package.json
CHANGED