@qafka/react-native 2.6.0 → 2.7.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 +10 -0
- package/dist/components/Qafka.js +30 -1
- package/dist/decide-voice-close.d.ts +12 -0
- package/dist/decide-voice-close.js +17 -0
- package/dist/hooks/useVoiceChat.d.ts +15 -1
- package/dist/hooks/useVoiceChat.js +38 -2
- package/dist/services/RealtimeService.d.ts +1 -0
- package/dist/services/RealtimeService.js +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,16 @@ and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.ht
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.7.0] — 2026-09-16
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Voice session ended by the server: when the backend ends a voice session because voice time is no longer available for the project, the SDK now closes voice cleanly, returns the user to the text chat and shows a neutral, localized message ("Voice chat is currently unavailable. You can keep chatting by text." / Turkish when `locale` starts with `tr`). Text chat keeps working. This applies both when voice is opened and during an ongoing call.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- Voice sessions closed by the server mid-call are now distinguished from network drops and user-initiated disconnects, so an unexpected close no longer leaves the voice screen in a connecting or listening state. No public API changes.
|
|
19
|
+
|
|
10
20
|
## [2.6.0] — 2026-09-15
|
|
11
21
|
|
|
12
22
|
### Added
|
package/dist/components/Qafka.js
CHANGED
|
@@ -135,6 +135,7 @@ exports.Qafka = (0, react_1.forwardRef)(function Qafka({ style, apiUrl: apiUrlPr
|
|
|
135
135
|
apiKey: resolvedApiKey || '',
|
|
136
136
|
userContext: currentContext,
|
|
137
137
|
contextDescription,
|
|
138
|
+
locale,
|
|
138
139
|
onToolSuggested,
|
|
139
140
|
toolRenderMode,
|
|
140
141
|
// Forward the device-attestation session token so the realtime WS
|
|
@@ -148,7 +149,7 @@ exports.Qafka = (0, react_1.forwardRef)(function Qafka({ style, apiUrl: apiUrlPr
|
|
|
148
149
|
endUserData: validatedEndUserData,
|
|
149
150
|
onNavigationSuggest,
|
|
150
151
|
});
|
|
151
|
-
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;
|
|
152
|
+
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, voiceCloseReason, voiceCloseMessage, } = voiceChat;
|
|
152
153
|
const { messages, isTyping, isSending, streamingMessage, toolStatus, flatListRef, handleSend, setMessages, setIsTyping, setStreamingMessage, } = (0, useChatMessages_1.useChatMessages)({
|
|
153
154
|
enableStreaming,
|
|
154
155
|
context: currentContext,
|
|
@@ -398,6 +399,34 @@ exports.Qafka = (0, react_1.forwardRef)(function Qafka({ style, apiUrl: apiUrlPr
|
|
|
398
399
|
voiceChat.pauseMic();
|
|
399
400
|
}
|
|
400
401
|
}, [voiceChat]);
|
|
402
|
+
// The server can end a voice session for a reason the user should be told
|
|
403
|
+
// about (currently only when it can no longer continue). useVoiceChat
|
|
404
|
+
// already tears the session down; here we mirror the user's own "swipe
|
|
405
|
+
// back to chat" gesture and drop its message into the transcript the same
|
|
406
|
+
// way a failed send does (see useChatMessages' fallback error bubble) —
|
|
407
|
+
// reusing the existing chat-bubble UI instead of adding a new surface.
|
|
408
|
+
const handledVoiceCloseRef = (0, react_1.useRef)(false);
|
|
409
|
+
(0, react_1.useEffect)(() => {
|
|
410
|
+
if (voiceCloseReason === 'minutes_exhausted') {
|
|
411
|
+
if (handledVoiceCloseRef.current)
|
|
412
|
+
return;
|
|
413
|
+
handledVoiceCloseRef.current = true;
|
|
414
|
+
scrollViewRef.current?.scrollTo({ x: 0, animated: true });
|
|
415
|
+
setMessages((prev) => [
|
|
416
|
+
...prev,
|
|
417
|
+
{
|
|
418
|
+
id: `voice_closed_${Date.now()}`,
|
|
419
|
+
role: 'assistant',
|
|
420
|
+
text: voiceCloseMessage ?? '',
|
|
421
|
+
content: voiceCloseMessage ?? '',
|
|
422
|
+
timestamp: new Date(),
|
|
423
|
+
},
|
|
424
|
+
]);
|
|
425
|
+
}
|
|
426
|
+
else {
|
|
427
|
+
handledVoiceCloseRef.current = false;
|
|
428
|
+
}
|
|
429
|
+
}, [voiceCloseReason, voiceCloseMessage, setMessages]);
|
|
401
430
|
// Card CTA host callbacks bundle. Stable reference via useMemo
|
|
402
431
|
// so MessageBubble's CardRuntimeProvider doesn't re-mount on every render.
|
|
403
432
|
// MUST be declared before the conditional returns below — Rules of Hooks.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Classifies why a voice session's WebSocket closed, from the close code
|
|
3
|
+
* and/or reason string the server sends, into an outcome the caller can act
|
|
4
|
+
* on without needing to know the raw close-code values itself. Kept as a
|
|
5
|
+
* pure function so the mapping is unit-testable without loading the
|
|
6
|
+
* native-bound RealtimeService.
|
|
7
|
+
*/
|
|
8
|
+
export type VoiceCloseReason = 'minutes_exhausted' | 'not_allowed' | 'project_unavailable' | 'other';
|
|
9
|
+
export declare function classifyVoiceClose(event: {
|
|
10
|
+
code?: number;
|
|
11
|
+
reason?: string;
|
|
12
|
+
} | null | undefined): VoiceCloseReason;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.classifyVoiceClose = classifyVoiceClose;
|
|
4
|
+
function classifyVoiceClose(event) {
|
|
5
|
+
const code = event?.code;
|
|
6
|
+
const reason = event?.reason;
|
|
7
|
+
if (code === 4011 || reason === 'VOICE_MINUTES_EXHAUSTED') {
|
|
8
|
+
return 'minutes_exhausted';
|
|
9
|
+
}
|
|
10
|
+
if (code === 4003 || reason === 'VOICE_NOT_ALLOWED') {
|
|
11
|
+
return 'not_allowed';
|
|
12
|
+
}
|
|
13
|
+
if (code === 4010 || reason === 'project_unavailable') {
|
|
14
|
+
return 'project_unavailable';
|
|
15
|
+
}
|
|
16
|
+
return 'other';
|
|
17
|
+
}
|
|
@@ -64,6 +64,11 @@ interface UseVoiceChatOptions {
|
|
|
64
64
|
apiKey: string;
|
|
65
65
|
userContext?: Record<string, any>;
|
|
66
66
|
contextDescription?: string;
|
|
67
|
+
/**
|
|
68
|
+
* BCP 47 locale, forwarded to the neutral message shown when the server
|
|
69
|
+
* ends the voice session (e.g. `voiceCloseMessage` after minutes run out).
|
|
70
|
+
*/
|
|
71
|
+
locale?: string;
|
|
67
72
|
onToolSuggested?: VoiceOnToolSuggested;
|
|
68
73
|
toolRenderMode?: VoiceToolRenderMode;
|
|
69
74
|
/**
|
|
@@ -119,5 +124,14 @@ interface UseVoiceChatResult {
|
|
|
119
124
|
acceptNavigationSuggestion: () => void;
|
|
120
125
|
/** Dismiss the pending suggestion without navigating. */
|
|
121
126
|
dismissNavigationSuggestion: () => void;
|
|
127
|
+
/**
|
|
128
|
+
* Set when the server closed the voice session for a reason the user
|
|
129
|
+
* should be told about (currently only `'minutes_exhausted'`). Cleared on
|
|
130
|
+
* the next `connect()`. `null` in every other case, including a normal
|
|
131
|
+
* user-initiated `disconnect()`.
|
|
132
|
+
*/
|
|
133
|
+
voiceCloseReason: 'minutes_exhausted' | null;
|
|
134
|
+
/** Locale-aware copy to show alongside `voiceCloseReason`, or `null`. */
|
|
135
|
+
voiceCloseMessage: string | null;
|
|
122
136
|
}
|
|
123
|
-
export declare function useVoiceChat({ apiUrl, apiKey, userContext, contextDescription, onToolSuggested, toolRenderMode, getSessionToken, endUserId, endUserData, onNavigationSuggest, }: UseVoiceChatOptions): UseVoiceChatResult;
|
|
137
|
+
export declare function useVoiceChat({ apiUrl, apiKey, userContext, contextDescription, locale, onToolSuggested, toolRenderMode, getSessionToken, endUserId, endUserData, onNavigationSuggest, }: UseVoiceChatOptions): UseVoiceChatResult;
|
|
@@ -6,8 +6,26 @@ 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
|
-
|
|
9
|
+
const decide_voice_close_1 = require("../decide-voice-close");
|
|
10
|
+
/**
|
|
11
|
+
* Neutral, locale-aware copy shown when the server ends a voice session
|
|
12
|
+
* because it can no longer be continued. Mirrors `unavailableMessage()` in
|
|
13
|
+
* `services/projectUnavailable.ts` — deliberately generic, no mention of
|
|
14
|
+
* limits or plans.
|
|
15
|
+
*/
|
|
16
|
+
function voiceUnavailableMessage(locale) {
|
|
17
|
+
return locale?.toLowerCase().startsWith('tr')
|
|
18
|
+
? 'Sesli görüşme şu anda kullanılamıyor. Yazarak sohbete devam edebilirsiniz.'
|
|
19
|
+
: 'Voice chat is currently unavailable. You can keep chatting by text.';
|
|
20
|
+
}
|
|
21
|
+
function useVoiceChat({ apiUrl, apiKey, userContext, contextDescription, locale, onToolSuggested, toolRenderMode = 'upsert', getSessionToken, endUserId, endUserData, onNavigationSuggest, }) {
|
|
10
22
|
const [state, setState] = (0, react_1.useState)('idle');
|
|
23
|
+
const [voiceCloseReason, setVoiceCloseReason] = (0, react_1.useState)(null);
|
|
24
|
+
const [voiceCloseMessage, setVoiceCloseMessage] = (0, react_1.useState)(null);
|
|
25
|
+
const localeRef = (0, react_1.useRef)(locale);
|
|
26
|
+
(0, react_1.useEffect)(() => {
|
|
27
|
+
localeRef.current = locale;
|
|
28
|
+
}, [locale]);
|
|
11
29
|
const [transcript, setTranscript] = (0, react_1.useState)('');
|
|
12
30
|
const [userTranscript, setUserTranscript] = (0, react_1.useState)('');
|
|
13
31
|
const [transcriptHistory, setTranscriptHistory] = (0, react_1.useState)([]);
|
|
@@ -147,11 +165,25 @@ function useVoiceChat({ apiUrl, apiKey, userContext, contextDescription, onToolS
|
|
|
147
165
|
setState('listening');
|
|
148
166
|
return;
|
|
149
167
|
}
|
|
150
|
-
case 'session.closed':
|
|
168
|
+
case 'session.closed': {
|
|
169
|
+
const closeReason = (0, decide_voice_close_1.classifyVoiceClose)({ code: event.code, reason: event.reason });
|
|
170
|
+
if (closeReason === 'minutes_exhausted') {
|
|
171
|
+
// Surface the neutral message and fall back to text chat. The server
|
|
172
|
+
// has already closed the socket, so this only tears down local state
|
|
173
|
+
// (native audio engine, refs, mute flags) via the same path a
|
|
174
|
+
// user-initiated disconnect takes.
|
|
175
|
+
setVoiceCloseReason('minutes_exhausted');
|
|
176
|
+
setVoiceCloseMessage(voiceUnavailableMessage(localeRef.current));
|
|
177
|
+
disconnectRef.current?.();
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
setVoiceCloseReason(null);
|
|
181
|
+
setVoiceCloseMessage(null);
|
|
151
182
|
setState('idle');
|
|
152
183
|
setNavigationSuggestion(null);
|
|
153
184
|
pendingNavDisconnectRef.current = false;
|
|
154
185
|
return;
|
|
186
|
+
}
|
|
155
187
|
case 'error':
|
|
156
188
|
setState('idle');
|
|
157
189
|
return;
|
|
@@ -392,6 +424,8 @@ function useVoiceChat({ apiUrl, apiKey, userContext, contextDescription, onToolS
|
|
|
392
424
|
return;
|
|
393
425
|
}
|
|
394
426
|
setState('connecting');
|
|
427
|
+
setVoiceCloseReason(null);
|
|
428
|
+
setVoiceCloseMessage(null);
|
|
395
429
|
const service = new RealtimeService_1.RealtimeService(apiUrl, apiKey);
|
|
396
430
|
if (getSessionToken)
|
|
397
431
|
service.setSessionTokenGetter(getSessionToken);
|
|
@@ -523,5 +557,7 @@ function useVoiceChat({ apiUrl, apiKey, userContext, contextDescription, onToolS
|
|
|
523
557
|
navigationSuggestion,
|
|
524
558
|
acceptNavigationSuggestion,
|
|
525
559
|
dismissNavigationSuggestion,
|
|
560
|
+
voiceCloseReason,
|
|
561
|
+
voiceCloseMessage,
|
|
526
562
|
};
|
|
527
563
|
}
|
package/package.json
CHANGED