@qafka/react-native 2.8.0 → 2.8.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/CHANGELOG.md CHANGED
@@ -7,6 +7,14 @@ and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.ht
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.8.1] — 2026-10-06
11
+
12
+ ### Added
13
+
14
+ - Close button on the voice page, in its top left corner. It only leaves voice: the voice session is ended and the widget returns to the chat page, so users no longer have to swipe back. The chat stays open and `onClose` is not called — closing the assistant is still done from the chat page.
15
+ - New optional `onVoiceClose` prop on `<Qafka />`, called when that button is pressed.
16
+ - New optional `voiceComponents.VoiceCloseButton` slot (`VoiceCloseButtonProps`) to replace the default button.
17
+
10
18
  ## [2.8.0] — 2026-10-06
11
19
 
12
20
  ### Added
@@ -87,7 +87,7 @@ const { width: SCREEN_WIDTH } = react_native_1.Dimensions.get('window');
87
87
  * />
88
88
  * ```
89
89
  */
90
- 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, onUnavailable, 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
+ 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, onUnavailable, onNavigationSuggest, onNavigationAction, onExternalSuggestion, onCardDeepLink, onCardSuggestMessage, onCardExternalNavigation, onCardShare, onCardCopy, onCardToolTrigger, onCardCTAClick, onToolSuggested, onToolDataRequested, onActionResult, onStepCompleted, onFileUploadRequest, onExtractionResult, onClose, onVoiceClose, onBack, CloseComponent, BackComponent, navigationLabelFormat, NavigationButtonComponent, voiceComponents, voiceTranscript = 'centered', toolRenderMode, }, ref) {
91
91
  // === ALL HOOKS FIRST (Rules of Hooks compliance) ===
92
92
  const insets = (0, react_native_safe_area_context_1.useSafeAreaInsets)();
93
93
  const { sdkReady, unavailable, error: sdkError, resolvedApiKey, resolvedApiUrl } = (0, useSDK_1.useSDK)({
@@ -514,7 +514,14 @@ exports.Qafka = (0, react_1.forwardRef)(function Qafka({ style, apiUrl: apiUrlPr
514
514
 
515
515
  {/* Page 1: Voice — VoicePage paints Background edge-to-edge and
516
516
  handles its own internal safe-area padding for orb/transcript. */}
517
- <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} canResume={voiceCanResume} onResume={voiceResume} resumeLabel={voiceResumeLabel} navigationSuggestion={voiceNavigationSuggestion} onNavigationAccept={voiceAcceptNavigation} onNavigationDismiss={voiceDismissNavigation} NavigationButtonComponent={NavigationButtonComponent}/>
517
+ <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} canResume={voiceCanResume} onResume={voiceResume} resumeLabel={voiceResumeLabel} navigationSuggestion={voiceNavigationSuggestion} onNavigationAccept={voiceAcceptNavigation} onNavigationDismiss={voiceDismissNavigation} NavigationButtonComponent={NavigationButtonComponent}
518
+ // The voice page's close button only leaves voice: end the session
519
+ // and slide back to the chat page, which stays open.
520
+ onClose={() => {
521
+ void voiceDisconnect();
522
+ scrollViewRef.current?.scrollTo({ x: 0, animated: true });
523
+ onVoiceClose?.();
524
+ }}/>
518
525
  </react_native_1.ScrollView>
519
526
  </react_native_1.View>) : (<ChatPage_1.ChatPage {...chatPageProps}/>);
520
527
  return (<react_native_1.View style={[
@@ -64,6 +64,14 @@ export interface VoiceMuteButtonProps {
64
64
  theme: Theme;
65
65
  state: VoiceChatState;
66
66
  }
67
+ /**
68
+ * Props passed to custom voice close button component
69
+ */
70
+ export interface VoiceCloseButtonProps {
71
+ onClose: () => void;
72
+ theme: Theme;
73
+ state: VoiceChatState;
74
+ }
67
75
  /**
68
76
  * Props passed to custom voice background component
69
77
  */
@@ -125,6 +133,11 @@ export interface VoiceComponents {
125
133
  * row in every voice layout. Defaults to a circular icon button.
126
134
  */
127
135
  VoiceMuteButton?: React.ComponentType<VoiceMuteButtonProps>;
136
+ /**
137
+ * Slot for the button that leaves the voice page and returns to the chat
138
+ * page, rendered in the top left corner. Defaults to a circular icon button.
139
+ */
140
+ VoiceCloseButton?: React.ComponentType<VoiceCloseButtonProps>;
128
141
  /**
129
142
  * Slot for the multi-chip list rendered when the AI calls the built-in
130
143
  * `qafka_display` tool with mode="chip".
@@ -689,6 +702,12 @@ export interface QafkaProps {
689
702
  */
690
703
  onClose?: () => void;
691
704
  /**
705
+ * Optional. Called when the close button on the voice page is pressed. That
706
+ * button only leaves voice: the voice session is ended and the widget
707
+ * returns to the chat page, which stays open — `onClose` is not called.
708
+ */
709
+ onVoiceClose?: () => void;
710
+ /**
692
711
  * Callback when back button is pressed
693
712
  * If provided, a back button will be shown in the top left corner
694
713
  */
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import { QafkaProps } from './Qafka.types';
3
- type QafkaProviderProps = Pick<QafkaProps, 'style' | 'projectId' | 'apiUrl' | 'theme' | 'themeOverride' | 'customTheme' | 'enableStreaming' | 'isAuthenticated' | 'endUserId' | 'endUserData' | 'context' | 'contextDescription' | 'components' | 'showTimestamps' | 'placeholder' | 'maxMessageLength' | 'greetingMessage' | 'title' | 'showHeader' | 'onReady' | 'onMessageSent' | 'onResponseReceived' | 'onError' | 'onNavigationSuggest' | 'onNavigationAction' | 'onToolSuggested' | 'onActionResult' | 'onStepCompleted' | 'onClose' | 'onBack' | 'CloseComponent' | 'BackComponent' | 'navigationLabelFormat' | 'NavigationButtonComponent'> & {
3
+ type QafkaProviderProps = Pick<QafkaProps, 'style' | 'projectId' | 'apiUrl' | 'theme' | 'themeOverride' | 'customTheme' | 'enableStreaming' | 'isAuthenticated' | 'endUserId' | 'endUserData' | 'context' | 'contextDescription' | 'components' | 'showTimestamps' | 'placeholder' | 'maxMessageLength' | 'greetingMessage' | 'title' | 'showHeader' | 'onReady' | 'onMessageSent' | 'onResponseReceived' | 'onError' | 'onNavigationSuggest' | 'onNavigationAction' | 'onToolSuggested' | 'onActionResult' | 'onStepCompleted' | 'onClose' | 'onVoiceClose' | 'onBack' | 'CloseComponent' | 'BackComponent' | 'navigationLabelFormat' | 'NavigationButtonComponent'> & {
4
4
  mode?: 'floating' | 'fullscreen' | 'inline';
5
5
  position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
6
6
  };
@@ -51,6 +51,7 @@ interface VoicePageProps {
51
51
  onNavigationAccept?: () => void;
52
52
  onNavigationDismiss?: () => void;
53
53
  NavigationButtonComponent?: ComponentType<NavigationButtonProps>;
54
+ onClose?: () => void;
54
55
  }
55
- export declare function VoicePage({ state, transcript, userTranscript, transcriptHistory, transcriptMode, amplitude, theme, voiceComponents, toolStatus, renderedTools, registeredComponents, transcriptOverrideForTurn, DataChipListComponent, isMuted, onToggleMute, canResume, onResume, resumeLabel, navigationSuggestion, onNavigationAccept, onNavigationDismiss, NavigationButtonComponent, }: VoicePageProps): React.JSX.Element;
56
+ export declare function VoicePage({ state, transcript, userTranscript, transcriptHistory, transcriptMode, amplitude, theme, voiceComponents, toolStatus, renderedTools, registeredComponents, transcriptOverrideForTurn, DataChipListComponent, isMuted, onToggleMute, canResume, onResume, resumeLabel, navigationSuggestion, onNavigationAccept, onNavigationDismiss, NavigationButtonComponent, onClose, }: VoicePageProps): React.JSX.Element;
56
57
  export {};
@@ -306,6 +306,19 @@ function DefaultVoiceMuteButton({ isMuted, onToggle, theme, }) {
306
306
  {isMuted ? <react_native_1.View style={[muteStyles.slash, { backgroundColor: fg }]}/> : null}
307
307
  </react_native_1.Pressable>);
308
308
  }
309
+ function DefaultVoiceCloseButton({ onClose, theme }) {
310
+ return (<react_native_1.Pressable onPress={onClose} hitSlop={8} accessibilityRole="button" accessibilityLabel="End voice chat" style={({ pressed }) => [
311
+ muteStyles.button,
312
+ {
313
+ backgroundColor: theme?.colors?.surface ?? 'rgba(0,0,0,0.06)',
314
+ opacity: pressed ? 0.75 : 1,
315
+ },
316
+ ]}>
317
+ <react_native_1.Text style={{ fontSize: 24, lineHeight: 26, color: theme?.colors?.text ?? '#111' }}>
318
+ {'×'}
319
+ </react_native_1.Text>
320
+ </react_native_1.Pressable>);
321
+ }
309
322
  /**
310
323
  * Minimal microphone glyph built with stock Views — capsule body, stand
311
324
  * post, and base bar. No external icon font, no SVG, no Skia.
@@ -408,7 +421,7 @@ function ChatTranscript({ history, liveAiText, state, primaryTextColor, secondar
408
421
  </react_native_1.View>);
409
422
  }
410
423
  // --- Main component ---
411
- function VoicePage({ state, transcript, userTranscript, transcriptHistory, transcriptMode = 'centered', amplitude, theme, voiceComponents, toolStatus, renderedTools, registeredComponents, transcriptOverrideForTurn = false, DataChipListComponent, isMuted = false, onToggleMute, canResume = false, onResume, resumeLabel, navigationSuggestion, onNavigationAccept, onNavigationDismiss, NavigationButtonComponent, }) {
424
+ function VoicePage({ state, transcript, userTranscript, transcriptHistory, transcriptMode = 'centered', amplitude, theme, voiceComponents, toolStatus, renderedTools, registeredComponents, transcriptOverrideForTurn = false, DataChipListComponent, isMuted = false, onToggleMute, canResume = false, onResume, resumeLabel, navigationSuggestion, onNavigationAccept, onNavigationDismiss, NavigationButtonComponent, onClose, }) {
412
425
  const Indicator = voiceComponents?.VoiceIndicator || DefaultVoiceIndicator;
413
426
  const Background = voiceComponents?.VoiceBackground || DefaultVoiceBackground;
414
427
  const Transcript = voiceComponents?.VoiceTranscript || DefaultVoiceTranscript;
@@ -424,6 +437,11 @@ function VoicePage({ state, transcript, userTranscript, transcriptHistory, trans
424
437
  const resumeHint = resumable && resumeLabel ? (<react_native_1.Text style={{ marginTop: 12, fontSize: 14, color: STATE_COLORS.idle }}>
425
438
  {resumeLabel}
426
439
  </react_native_1.Text>) : null;
440
+ // Leaves voice and returns to the chat page, so that does not require
441
+ // swiping back. Top left: the mic button owns the top right corner.
442
+ const CloseButton = voiceComponents?.VoiceCloseButton || DefaultVoiceCloseButton;
443
+ const showClose = typeof onClose === 'function';
444
+ const closeButton = showClose ? (<CloseButton onClose={onClose} theme={theme} state={state}/>) : null;
427
445
  const hasToolUI = !transcriptOverrideForTurn && (renderedTools?.length ?? 0) > 0;
428
446
  // When tool UI is rendered, the compact status bar takes over regardless of
429
447
  // chosen mode — preserves the single-screen / hands-free experience.
@@ -463,6 +481,7 @@ function VoicePage({ state, transcript, userTranscript, transcriptHistory, trans
463
481
  borderBottomColor: 'rgba(0,0,0,0.08)',
464
482
  backgroundColor: theme?.colors?.background ?? '#fff',
465
483
  }}>
484
+ {closeButton}
466
485
  <react_native_1.View style={{
467
486
  width: 44,
468
487
  height: 44,
@@ -605,6 +624,14 @@ function VoicePage({ state, transcript, userTranscript, transcriptHistory, trans
605
624
  </react_native_1.ScrollView>) : null}
606
625
  </react_native_1.View>
607
626
 
627
+ {showClose && !hasToolUI ? (<react_native_1.View style={{
628
+ position: 'absolute',
629
+ top: insets.top + 12,
630
+ left: 16,
631
+ }}>
632
+ {closeButton}
633
+ </react_native_1.View>) : null}
634
+
608
635
  {showMute && !hasToolUI ? (<react_native_1.View style={{
609
636
  position: 'absolute',
610
637
  // Absolute children aren't pushed by the parent's safe-area paddingTop,
@@ -3,7 +3,7 @@
3
3
  */
4
4
  export { QafkaProvider } from './QafkaProvider';
5
5
  export { Qafka } from './Qafka';
6
- export type { VoiceComponents, VoiceIndicatorProps, VoiceBackgroundProps, VoiceTranscriptProps, VoiceMuteButtonProps, QafkaHandle } from './Qafka.types';
6
+ export type { VoiceComponents, VoiceIndicatorProps, VoiceBackgroundProps, VoiceTranscriptProps, VoiceMuteButtonProps, VoiceCloseButtonProps, QafkaHandle } from './Qafka.types';
7
7
  export type { ToolResponseMode } from './Qafka.types';
8
8
  export type { QafkaProps, QafkaProjectIds, ProjectIdOf } from './Qafka.types';
9
9
  export { MessageBubble } from './MessageBubble';
@@ -299,6 +299,10 @@ You may optionally restyle the chips via `voiceComponents.dataChipList` / `voice
299
299
  <Qafka onUnavailable={() => setChatAvailable(false)} />
300
300
  {chatAvailable && <LauncherButton onPress={openChat} />}
301
301
  ```
302
+ - **`onVoiceClose`** — optional; fires when the close button on the voice page is pressed.
303
+ That button only leaves voice: the voice session ends and the widget returns to the chat
304
+ page, which stays open. `onClose` is not called — it still belongs to the chat page's own
305
+ close button. Restyle the button via `voiceComponents.VoiceCloseButton`.
302
306
 
303
307
  ---
304
308
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qafka/react-native",
3
- "version": "2.8.0",
3
+ "version": "2.8.1",
4
4
  "description": "Drop-in AI assistant for React Native: chat, voice, and tool execution with screen-aware navigation.",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",