@qafka/react-native 2.2.0 → 2.3.1
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 +3 -2
- package/dist/components/VoicePage.d.ts +7 -2
- package/dist/components/VoicePage.js +4 -1
- 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/useVoiceChat.d.ts +14 -1
- package/dist/hooks/useVoiceChat.js +61 -1
- 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
|
@@ -144,8 +144,9 @@ exports.Qafka = (0, react_1.forwardRef)(function Qafka({ style, apiUrl: apiUrlPr
|
|
|
144
144
|
// grouped with text-mode rows for the same user.
|
|
145
145
|
endUserId: validatedEndUserId,
|
|
146
146
|
endUserData: validatedEndUserData,
|
|
147
|
+
onNavigationSuggest,
|
|
147
148
|
});
|
|
148
|
-
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;
|
|
149
150
|
const { messages, isTyping, isSending, streamingMessage, toolStatus, flatListRef, handleSend, setMessages, setIsTyping, setStreamingMessage, } = (0, useChatMessages_1.useChatMessages)({
|
|
150
151
|
enableStreaming,
|
|
151
152
|
context: currentContext,
|
|
@@ -466,7 +467,7 @@ exports.Qafka = (0, react_1.forwardRef)(function Qafka({ style, apiUrl: apiUrlPr
|
|
|
466
467
|
|
|
467
468
|
{/* Page 1: Voice — VoicePage paints Background edge-to-edge and
|
|
468
469
|
handles its own internal safe-area padding for orb/transcript. */}
|
|
469
|
-
<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}/>
|
|
470
471
|
</react_native_1.ScrollView>
|
|
471
472
|
</react_native_1.View>) : (<ChatPage_1.ChatPage {...chatPageProps}/>);
|
|
472
473
|
return (<react_native_1.View style={[
|
|
@@ -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,
|
|
@@ -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);
|
|
@@ -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,18 @@ 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]);
|
|
23
|
+
// Set when an explicit (trigger:'user') navigation fires: we navigate
|
|
24
|
+
// immediately but defer closing the voice session until the AI finishes
|
|
25
|
+
// its turn (response.done), so the closing remark isn't cut off.
|
|
26
|
+
const pendingNavDisconnectRef = (0, react_1.useRef)(false);
|
|
27
|
+
// Ref mirror of `disconnect` so handleEvent (useCallback([])) can close the
|
|
28
|
+
// session without going stale.
|
|
29
|
+
const disconnectRef = (0, react_1.useRef)(undefined);
|
|
17
30
|
const serviceRef = (0, react_1.useRef)(null);
|
|
18
31
|
// Snapshot of the live AI transcript so `response.done` can push the
|
|
19
32
|
// finished text into history without depending on stale closure state.
|
|
@@ -23,6 +36,10 @@ function useVoiceChat({ apiUrl, apiKey, userContext, contextDescription, onToolS
|
|
|
23
36
|
(0, react_1.useEffect)(() => {
|
|
24
37
|
onToolSuggestedRef.current = onToolSuggested;
|
|
25
38
|
}, [onToolSuggested]);
|
|
39
|
+
const onNavigationSuggestRef = (0, react_1.useRef)(onNavigationSuggest);
|
|
40
|
+
(0, react_1.useEffect)(() => {
|
|
41
|
+
onNavigationSuggestRef.current = onNavigationSuggest;
|
|
42
|
+
}, [onNavigationSuggest]);
|
|
26
43
|
// handleEvent is wrapped in useCallback([]) so we read the live value
|
|
27
44
|
// through a ref to avoid stale closures.
|
|
28
45
|
const toolRenderModeRef = (0, react_1.useRef)(toolRenderMode);
|
|
@@ -106,10 +123,18 @@ function useVoiceChat({ apiUrl, apiKey, userContext, contextDescription, onToolS
|
|
|
106
123
|
transcriptRef.current = '';
|
|
107
124
|
setTranscript('');
|
|
108
125
|
setState('listening');
|
|
126
|
+
// Explicit navigation deferred its session close to here, so the AI's
|
|
127
|
+
// closing remark could finish first.
|
|
128
|
+
if (pendingNavDisconnectRef.current) {
|
|
129
|
+
pendingNavDisconnectRef.current = false;
|
|
130
|
+
disconnectRef.current?.();
|
|
131
|
+
}
|
|
109
132
|
return;
|
|
110
133
|
}
|
|
111
134
|
case 'session.closed':
|
|
112
135
|
setState('idle');
|
|
136
|
+
setNavigationSuggestion(null);
|
|
137
|
+
pendingNavDisconnectRef.current = false;
|
|
113
138
|
return;
|
|
114
139
|
case 'error':
|
|
115
140
|
setState('idle');
|
|
@@ -316,6 +341,20 @@ function useVoiceChat({ apiUrl, apiKey, userContext, contextDescription, onToolS
|
|
|
316
341
|
setToolStatus((s) => (s?.toolCallId === event.toolCallId ? null : s));
|
|
317
342
|
systemUnmute();
|
|
318
343
|
return;
|
|
344
|
+
case 'navigation': {
|
|
345
|
+
const decision = (0, decide_voice_navigation_1.decideVoiceNavigation)(event.navigation);
|
|
346
|
+
if (decision.action === 'callback') {
|
|
347
|
+
// Explicit request: navigate now, then close the voice session once the
|
|
348
|
+
// AI finishes speaking (handled in response.done) so the closing remark
|
|
349
|
+
// plays and the session doesn't linger in the background.
|
|
350
|
+
onNavigationSuggestRef.current?.(decision.suggestion);
|
|
351
|
+
pendingNavDisconnectRef.current = true;
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
setNavigationSuggestion(decision.suggestion);
|
|
355
|
+
}
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
319
358
|
}
|
|
320
359
|
}, []);
|
|
321
360
|
const connect = (0, react_1.useCallback)(async () => {
|
|
@@ -369,17 +408,35 @@ function useVoiceChat({ apiUrl, apiKey, userContext, contextDescription, onToolS
|
|
|
369
408
|
setTranscriptHistory([]);
|
|
370
409
|
transcriptRef.current = '';
|
|
371
410
|
setAmplitude(0);
|
|
411
|
+
setNavigationSuggestion(null);
|
|
412
|
+
pendingNavDisconnectRef.current = false;
|
|
372
413
|
// Reset both mute layers so the next session starts clean.
|
|
373
414
|
userMutedRef.current = false;
|
|
374
415
|
systemMutedRef.current = false;
|
|
375
416
|
setIsMuted(false);
|
|
376
417
|
}, []);
|
|
418
|
+
(0, react_1.useEffect)(() => {
|
|
419
|
+
disconnectRef.current = disconnect;
|
|
420
|
+
}, [disconnect]);
|
|
377
421
|
const pauseMic = (0, react_1.useCallback)(async () => {
|
|
378
422
|
await serviceRef.current?.pauseMic();
|
|
379
423
|
}, []);
|
|
380
424
|
const resumeMic = (0, react_1.useCallback)(async () => {
|
|
381
425
|
await serviceRef.current?.resumeMic();
|
|
382
426
|
}, []);
|
|
427
|
+
const acceptNavigationSuggestion = (0, react_1.useCallback)(() => {
|
|
428
|
+
const current = navigationSuggestionRef.current;
|
|
429
|
+
setNavigationSuggestion(null);
|
|
430
|
+
if (current)
|
|
431
|
+
onNavigationSuggestRef.current?.(current);
|
|
432
|
+
// Consistent with explicit navigation: leaving for a screen ends the
|
|
433
|
+
// voice session. The AI turn is already finished here (button was shown
|
|
434
|
+
// after response.done), so close immediately.
|
|
435
|
+
disconnect();
|
|
436
|
+
}, [disconnect]);
|
|
437
|
+
const dismissNavigationSuggestion = (0, react_1.useCallback)(() => {
|
|
438
|
+
setNavigationSuggestion(null);
|
|
439
|
+
}, []);
|
|
383
440
|
// Disconnect on app background
|
|
384
441
|
(0, react_1.useEffect)(() => {
|
|
385
442
|
const handleAppState = (nextState) => {
|
|
@@ -432,5 +489,8 @@ function useVoiceChat({ apiUrl, apiKey, userContext, contextDescription, onToolS
|
|
|
432
489
|
mute,
|
|
433
490
|
unmute,
|
|
434
491
|
toggleMute,
|
|
492
|
+
navigationSuggestion,
|
|
493
|
+
acceptNavigationSuggestion,
|
|
494
|
+
dismissNavigationSuggestion,
|
|
435
495
|
};
|
|
436
496
|
}
|
|
@@ -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