@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 +8 -0
- package/dist/components/Qafka.js +9 -2
- package/dist/components/Qafka.types.d.ts +19 -0
- package/dist/components/QafkaProvider.d.ts +1 -1
- package/dist/components/VoicePage.d.ts +2 -1
- package/dist/components/VoicePage.js +28 -1
- package/dist/components/index.d.ts +1 -1
- package/docs/qafka-tool-authoring-guide.md +4 -0
- package/package.json +1 -1
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
|
package/dist/components/Qafka.js
CHANGED
|
@@ -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