@bytexbyte/nxtlinq-ai-agent-ui-react-native-development 0.2.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.
Files changed (98) hide show
  1. package/dist/NxtlinqAgentAssistant.d.ts +29 -0
  2. package/dist/NxtlinqAgentAssistant.d.ts.map +1 -0
  3. package/dist/NxtlinqAgentAssistant.js +32 -0
  4. package/dist/components/AgentAssistantShell.d.ts +7 -0
  5. package/dist/components/AgentAssistantShell.d.ts.map +1 -0
  6. package/dist/components/AgentAssistantShell.js +77 -0
  7. package/dist/components/AgentComposer.d.ts +3 -0
  8. package/dist/components/AgentComposer.d.ts.map +1 -0
  9. package/dist/components/AgentComposer.js +56 -0
  10. package/dist/components/AgentMessageList.d.ts +3 -0
  11. package/dist/components/AgentMessageList.d.ts.map +1 -0
  12. package/dist/components/AgentMessageList.js +91 -0
  13. package/dist/components/AgentRemoteAudio.d.ts +14 -0
  14. package/dist/components/AgentRemoteAudio.d.ts.map +1 -0
  15. package/dist/components/AgentRemoteAudio.js +62 -0
  16. package/dist/components/AgentVoiceBar.d.ts +3 -0
  17. package/dist/components/AgentVoiceBar.d.ts.map +1 -0
  18. package/dist/components/AgentVoiceBar.js +133 -0
  19. package/dist/components/PresetMessageChips.d.ts +3 -0
  20. package/dist/components/PresetMessageChips.d.ts.map +1 -0
  21. package/dist/components/PresetMessageChips.js +39 -0
  22. package/dist/components/VoiceGreetTrigger.d.ts +10 -0
  23. package/dist/components/VoiceGreetTrigger.d.ts.map +1 -0
  24. package/dist/components/VoiceGreetTrigger.js +99 -0
  25. package/dist/components/VoiceIcons.d.ts +12 -0
  26. package/dist/components/VoiceIcons.d.ts.map +1 -0
  27. package/dist/components/VoiceIcons.js +17 -0
  28. package/dist/components/VoiceImageInput.d.ts +10 -0
  29. package/dist/components/VoiceImageInput.d.ts.map +1 -0
  30. package/dist/components/VoiceImageInput.js +100 -0
  31. package/dist/components/VoiceWaveform.d.ts +7 -0
  32. package/dist/components/VoiceWaveform.d.ts.map +1 -0
  33. package/dist/components/VoiceWaveform.js +64 -0
  34. package/dist/context/AgentAssistantContext.d.ts +45 -0
  35. package/dist/context/AgentAssistantContext.d.ts.map +1 -0
  36. package/dist/context/AgentAssistantContext.js +244 -0
  37. package/dist/index.d.ts +16 -0
  38. package/dist/index.d.ts.map +1 -0
  39. package/dist/index.js +14 -0
  40. package/dist/theme/defaultTheme.d.ts +3 -0
  41. package/dist/theme/defaultTheme.d.ts.map +1 -0
  42. package/dist/theme/defaultTheme.js +33 -0
  43. package/dist/types.d.ts +103 -0
  44. package/dist/types.d.ts.map +1 -0
  45. package/dist/types.js +1 -0
  46. package/dist/voice/AudioSessionWaker.d.ts +18 -0
  47. package/dist/voice/AudioSessionWaker.d.ts.map +1 -0
  48. package/dist/voice/AudioSessionWaker.js +49 -0
  49. package/dist/voice/TextTtsPlayer.d.ts +21 -0
  50. package/dist/voice/TextTtsPlayer.d.ts.map +1 -0
  51. package/dist/voice/TextTtsPlayer.js +91 -0
  52. package/dist/voice/VoiceAutoGreetBinder.d.ts +6 -0
  53. package/dist/voice/VoiceAutoGreetBinder.d.ts.map +1 -0
  54. package/dist/voice/VoiceAutoGreetBinder.js +25 -0
  55. package/dist/voice/useVoiceAutoGreet.d.ts +24 -0
  56. package/dist/voice/useVoiceAutoGreet.d.ts.map +1 -0
  57. package/dist/voice/useVoiceAutoGreet.js +64 -0
  58. package/dist/voice/useVoiceMicState.d.ts +24 -0
  59. package/dist/voice/useVoiceMicState.d.ts.map +1 -0
  60. package/dist/voice/useVoiceMicState.js +84 -0
  61. package/dist/voice/voiceMicConstants.d.ts +5 -0
  62. package/dist/voice/voiceMicConstants.d.ts.map +1 -0
  63. package/dist/voice/voiceMicConstants.js +11 -0
  64. package/dist/voice/voiceWaveformConstants.d.ts +6 -0
  65. package/dist/voice/voiceWaveformConstants.d.ts.map +1 -0
  66. package/dist/voice/voiceWaveformConstants.js +7 -0
  67. package/dist/voice/webrtcAudioGain.d.ts +6 -0
  68. package/dist/voice/webrtcAudioGain.d.ts.map +1 -0
  69. package/dist/voice/webrtcAudioGain.js +11 -0
  70. package/dist/voice/writeTtsCacheFile.d.ts +9 -0
  71. package/dist/voice/writeTtsCacheFile.d.ts.map +1 -0
  72. package/dist/voice/writeTtsCacheFile.js +37 -0
  73. package/package.json +64 -0
  74. package/src/NxtlinqAgentAssistant.tsx +103 -0
  75. package/src/components/AgentAssistantShell.tsx +167 -0
  76. package/src/components/AgentComposer.tsx +117 -0
  77. package/src/components/AgentMessageList.tsx +187 -0
  78. package/src/components/AgentRemoteAudio.tsx +105 -0
  79. package/src/components/AgentVoiceBar.tsx +232 -0
  80. package/src/components/PresetMessageChips.tsx +64 -0
  81. package/src/components/VoiceGreetTrigger.tsx +158 -0
  82. package/src/components/VoiceIcons.tsx +32 -0
  83. package/src/components/VoiceImageInput.tsx +178 -0
  84. package/src/components/VoiceWaveform.tsx +84 -0
  85. package/src/context/AgentAssistantContext.tsx +369 -0
  86. package/src/index.ts +59 -0
  87. package/src/react-native.d.ts +42 -0
  88. package/src/theme/defaultTheme.ts +35 -0
  89. package/src/types.ts +107 -0
  90. package/src/voice/AudioSessionWaker.tsx +94 -0
  91. package/src/voice/TextTtsPlayer.tsx +151 -0
  92. package/src/voice/VoiceAutoGreetBinder.tsx +38 -0
  93. package/src/voice/useVoiceAutoGreet.ts +95 -0
  94. package/src/voice/useVoiceMicState.ts +116 -0
  95. package/src/voice/voiceMicConstants.ts +14 -0
  96. package/src/voice/voiceWaveformConstants.ts +10 -0
  97. package/src/voice/webrtcAudioGain.ts +21 -0
  98. package/src/voice/writeTtsCacheFile.ts +47 -0
@@ -0,0 +1,178 @@
1
+ import { uriToVoiceImageAttachment } from '@bytexbyte/nxtlinq-ai-agent-react-native-development';
2
+ import React, { useCallback, useState } from 'react';
3
+ import {
4
+ ActivityIndicator,
5
+ Pressable,
6
+ StyleSheet,
7
+ Text,
8
+ TextInput,
9
+ View,
10
+ } from 'react-native';
11
+ import { useAgentAssistant } from '../context/AgentAssistantContext';
12
+
13
+ export type VoiceImageInputProps = {
14
+ /** Demo default image URL (HTTPS) for one-tap send. */
15
+ demoImageUrl?: string;
16
+ };
17
+
18
+ const IMAGE_ONLY_HINT =
19
+ 'Please describe this image or tell me what you would like to know about it.';
20
+
21
+ /**
22
+ * P0 test UI: send image (optional text) during an active voice session (SDK-2).
23
+ */
24
+ export function VoiceImageInput({ demoImageUrl }: VoiceImageInputProps): React.ReactElement | null {
25
+ const {
26
+ theme,
27
+ interactionMode,
28
+ isVoiceChannelReady,
29
+ sendVoiceUserInput,
30
+ } = useAgentAssistant();
31
+ const [imageUrl, setImageUrl] = useState(demoImageUrl ?? '');
32
+ const [busy, setBusy] = useState(false);
33
+ const [error, setError] = useState<string | null>(null);
34
+ const [sentHint, setSentHint] = useState<string | null>(null);
35
+
36
+ const sendImage = useCallback(
37
+ async (url: string) => {
38
+ const trimmed = url.trim();
39
+ if (!trimmed) {
40
+ setError('Enter an image URL');
41
+ return;
42
+ }
43
+ if (!isVoiceChannelReady) {
44
+ setError('Voice channel is not ready yet — wait for Listening');
45
+ return;
46
+ }
47
+ setBusy(true);
48
+ setError(null);
49
+ setSentHint(null);
50
+ try {
51
+ const attachment = await uriToVoiceImageAttachment(trimmed, 'voice-image.jpg');
52
+ sendVoiceUserInput({
53
+ text: IMAGE_ONLY_HINT,
54
+ attachments: [attachment],
55
+ clientMessageId: `user_img_${Date.now()}`,
56
+ });
57
+ setSentHint('Image sent');
58
+ } catch (e) {
59
+ const message = e instanceof Error ? e.message : String(e);
60
+ setError(message);
61
+ console.warn('[nxtlinq] sendVoiceUserInput image failed:', message);
62
+ } finally {
63
+ setBusy(false);
64
+ }
65
+ },
66
+ [isVoiceChannelReady, sendVoiceUserInput],
67
+ );
68
+
69
+ if (interactionMode !== 'voice') return null;
70
+
71
+ return (
72
+ <View
73
+ style={[
74
+ styles.box,
75
+ {
76
+ borderTopColor: theme.colors.border,
77
+ backgroundColor: theme.colors.background,
78
+ padding: theme.spacing.sm,
79
+ },
80
+ ]}
81
+ >
82
+ <Text style={{ color: theme.colors.mutedText, fontSize: theme.typography.captionSize }}>
83
+ Send image during voice (P0 test)
84
+ </Text>
85
+ {!isVoiceChannelReady ? (
86
+ <Text
87
+ style={{
88
+ color: theme.colors.mutedText,
89
+ fontSize: theme.typography.captionSize,
90
+ marginTop: theme.spacing.xs,
91
+ }}
92
+ >
93
+ Waiting for voice channel…
94
+ </Text>
95
+ ) : null}
96
+ <TextInput
97
+ value={imageUrl}
98
+ onChangeText={setImageUrl}
99
+ placeholder="HTTPS image URL or data URI"
100
+ placeholderTextColor={theme.colors.mutedText}
101
+ autoCapitalize="none"
102
+ style={[
103
+ styles.input,
104
+ {
105
+ borderColor: theme.colors.border,
106
+ color: theme.colors.assistantText,
107
+ marginTop: theme.spacing.xs,
108
+ },
109
+ ]}
110
+ />
111
+ {error ? (
112
+ <Text style={{ color: theme.colors.error, fontSize: theme.typography.captionSize }}>
113
+ {error}
114
+ </Text>
115
+ ) : null}
116
+ {sentHint ? (
117
+ <Text style={{ color: theme.colors.primary, fontSize: theme.typography.captionSize }}>
118
+ {sentHint}
119
+ </Text>
120
+ ) : null}
121
+ <View style={[styles.row, { marginTop: theme.spacing.xs }]}>
122
+ <Pressable
123
+ onPress={() => sendImage(imageUrl)}
124
+ disabled={busy || !isVoiceChannelReady}
125
+ style={({ pressed }) => [
126
+ styles.btn,
127
+ {
128
+ backgroundColor: theme.colors.primary,
129
+ opacity: pressed || busy || !isVoiceChannelReady ? 0.6 : 1,
130
+ },
131
+ ]}
132
+ >
133
+ {busy ? (
134
+ <ActivityIndicator color={theme.colors.primaryText} size="small" />
135
+ ) : (
136
+ <Text style={{ color: theme.colors.primaryText, fontWeight: '600' }}>Send image</Text>
137
+ )}
138
+ </Pressable>
139
+ {demoImageUrl ? (
140
+ <Pressable
141
+ onPress={() => sendImage(demoImageUrl)}
142
+ disabled={busy || !isVoiceChannelReady}
143
+ style={({ pressed }) => [
144
+ styles.btn,
145
+ {
146
+ backgroundColor: theme.colors.surface,
147
+ borderWidth: 1,
148
+ borderColor: theme.colors.border,
149
+ opacity: pressed || busy || !isVoiceChannelReady ? 0.6 : 1,
150
+ },
151
+ ]}
152
+ >
153
+ <Text style={{ color: theme.colors.assistantText }}>Product image</Text>
154
+ </Pressable>
155
+ ) : null}
156
+ </View>
157
+ </View>
158
+ );
159
+ }
160
+
161
+ const styles = StyleSheet.create({
162
+ box: { borderTopWidth: StyleSheet.hairlineWidth },
163
+ input: {
164
+ borderWidth: 1,
165
+ borderRadius: 8,
166
+ paddingHorizontal: 10,
167
+ paddingVertical: 8,
168
+ fontSize: 14,
169
+ },
170
+ row: { flexDirection: 'row', gap: 8 },
171
+ btn: {
172
+ paddingHorizontal: 14,
173
+ paddingVertical: 8,
174
+ borderRadius: 8,
175
+ minWidth: 72,
176
+ alignItems: 'center',
177
+ },
178
+ });
@@ -0,0 +1,84 @@
1
+ import React, { useEffect, useRef, useState } from 'react';
2
+ import { StyleSheet, View } from 'react-native';
3
+ import { useAgentAssistant } from '../context/AgentAssistantContext';
4
+ import {
5
+ WAVEFORM_ACTIVE_STATUSES,
6
+ WAVEFORM_AUDIBLE_HOLD_MS,
7
+ WAVEFORM_AUDIBLE_THRESHOLD,
8
+ } from '../voice/voiceWaveformConstants';
9
+
10
+ const BAR_COUNT = 12;
11
+
12
+ /**
13
+ * Polls `getOutputAudioLevel()` while the assistant is speaking (Berify-aligned).
14
+ * JeannieVoiceWaveform uses `active && audible` — not during user mic / idle.
15
+ */
16
+ export function VoiceWaveform(): React.ReactElement | null {
17
+ const { theme, interactionMode, voiceStatus, getOutputAudioLevel } = useAgentAssistant();
18
+ const [levels, setLevels] = useState<number[]>(() => Array(BAR_COUNT).fill(0));
19
+ const lastAudibleAtRef = useRef(0);
20
+
21
+ useEffect(() => {
22
+ if (interactionMode !== 'voice') {
23
+ setLevels(Array(BAR_COUNT).fill(0));
24
+ return;
25
+ }
26
+
27
+ const id = setInterval(() => {
28
+ const now = Date.now();
29
+ const statusActive = WAVEFORM_ACTIVE_STATUSES.has(voiceStatus);
30
+ const rawLevel = getOutputAudioLevel();
31
+ if (rawLevel > WAVEFORM_AUDIBLE_THRESHOLD) {
32
+ lastAudibleAtRef.current = now;
33
+ }
34
+ const audible = now - lastAudibleAtRef.current < WAVEFORM_AUDIBLE_HOLD_MS;
35
+ const effectiveActive = statusActive && audible;
36
+ const scaled = effectiveActive ? rawLevel : 0;
37
+ setLevels((prev) => [...prev.slice(1), scaled]);
38
+ }, 100);
39
+ return () => clearInterval(id);
40
+ }, [interactionMode, voiceStatus, getOutputAudioLevel]);
41
+
42
+ if (interactionMode !== 'voice') return null;
43
+
44
+ return (
45
+ <View
46
+ style={[
47
+ styles.row,
48
+ {
49
+ paddingHorizontal: theme.spacing.md,
50
+ paddingBottom: theme.spacing.sm,
51
+ backgroundColor: theme.colors.surface,
52
+ },
53
+ ]}
54
+ >
55
+ {levels.map((level, i) => (
56
+ <View
57
+ key={`bar-${i}`}
58
+ style={[
59
+ styles.bar,
60
+ {
61
+ height: 8 + level * 28,
62
+ backgroundColor: theme.colors.voiceSpeaking,
63
+ opacity: 0.35 + level * 0.65,
64
+ },
65
+ ]}
66
+ />
67
+ ))}
68
+ </View>
69
+ );
70
+ }
71
+
72
+ const styles = StyleSheet.create({
73
+ row: {
74
+ flexDirection: 'row',
75
+ alignItems: 'flex-end',
76
+ justifyContent: 'center',
77
+ gap: 3,
78
+ minHeight: 40,
79
+ },
80
+ bar: {
81
+ width: 4,
82
+ borderRadius: 2,
83
+ },
84
+ });
@@ -0,0 +1,369 @@
1
+ import type { VoiceStatus } from '@bytexbyte/nxtlinq-ai-agent-core-development';
2
+ import {
3
+ useNxtlinqAgent,
4
+ useNxtlinqVoice,
5
+ type UseNxtlinqAgentResult,
6
+ type UseNxtlinqVoiceResult,
7
+ } from '@bytexbyte/nxtlinq-ai-agent-react-native-development';
8
+ import React, {
9
+ createContext,
10
+ useCallback,
11
+ useContext,
12
+ useEffect,
13
+ useMemo,
14
+ useState,
15
+ type ReactNode,
16
+ } from 'react';
17
+ import { defaultAgentAssistantTheme } from '../theme/defaultTheme';
18
+ import type { AgentAssistantTheme, NxtlinqAgentAssistantProps, PresetMessage } from '../types';
19
+ import { useVoiceMicState } from '../voice/useVoiceMicState';
20
+ import { waitForIOSVoiceCaptureRelease } from '@bytexbyte/nxtlinq-ai-agent-react-native-development';
21
+ import { isTextTtsPlayerSupported, TextTtsPlayer, type TextTtsPlayerHandle } from '../voice/TextTtsPlayer';
22
+
23
+ export type InteractionMode = 'text' | 'voice';
24
+
25
+ export type AgentAssistantContextValue = UseNxtlinqAgentResult &
26
+ UseNxtlinqVoiceResult & {
27
+ theme: AgentAssistantTheme;
28
+ title: string;
29
+ placeholder: string;
30
+ presetMessages: PresetMessage[];
31
+ interactionMode: InteractionMode;
32
+ setInteractionMode: (mode: InteractionMode) => void;
33
+ inputText: string;
34
+ setInputText: (text: string) => void;
35
+ isVoiceAvailable: boolean;
36
+ isVoiceConnecting: boolean;
37
+ isMicMuted: boolean;
38
+ isMicHeldForAssistant: boolean;
39
+ toggleVoiceMicMute: () => void;
40
+ sendText: () => Promise<void>;
41
+ selectPreset: (preset: PresetMessage) => Promise<void>;
42
+ /** True when voice data channel is open and ready for user_input. */
43
+ isVoiceChannelReady: boolean;
44
+ postTextTts: (text: string) => Promise<{ audio: ArrayBuffer; mimeType: string }>;
45
+ buildTextTtsPlaybackUri: (result: { audio: ArrayBuffer; mimeType: string }) => string;
46
+ playMessageTts: (messageId: string, text: string) => Promise<void>;
47
+ playingMessageId: string | null;
48
+ isTextTtsAvailable: boolean;
49
+ };
50
+
51
+ const AgentAssistantContext = createContext<AgentAssistantContextValue | null>(null);
52
+
53
+ export type AgentAssistantProviderProps = {
54
+ children: ReactNode;
55
+ ui: Pick<
56
+ NxtlinqAgentAssistantProps,
57
+ | 'title'
58
+ | 'placeholder'
59
+ | 'presetMessages'
60
+ | 'enableVoice'
61
+ | 'theme'
62
+ | 'startWithMicMuted'
63
+ | 'holdMicDuringAssistant'
64
+ | 'textTtsVolume'
65
+ | 'voiceRemoteAudioGain'
66
+ > & {
67
+ webrtcEnabled: boolean;
68
+ };
69
+ };
70
+
71
+ export function AgentAssistantProvider({
72
+ children,
73
+ ui,
74
+ }: AgentAssistantProviderProps): React.ReactElement {
75
+ const agent = useNxtlinqAgent();
76
+ const voice = useNxtlinqVoice();
77
+
78
+ const theme = useMemo(
79
+ () => ({
80
+ ...defaultAgentAssistantTheme,
81
+ ...ui.theme,
82
+ colors: { ...defaultAgentAssistantTheme.colors, ...ui.theme?.colors },
83
+ spacing: { ...defaultAgentAssistantTheme.spacing, ...ui.theme?.spacing },
84
+ radius: { ...defaultAgentAssistantTheme.radius, ...ui.theme?.radius },
85
+ typography: {
86
+ ...defaultAgentAssistantTheme.typography,
87
+ ...ui.theme?.typography,
88
+ },
89
+ }),
90
+ [ui.theme],
91
+ );
92
+
93
+ const [interactionMode, setInteractionModeState] = useState<InteractionMode>('text');
94
+ const [inputText, setInputText] = useState('');
95
+ const [isVoiceConnecting, setIsVoiceConnecting] = useState(false);
96
+ const [playingMessageId, setPlayingMessageId] = useState<string | null>(null);
97
+ const textTtsPlayerRef = React.useRef<TextTtsPlayerHandle | null>(null);
98
+ const ttsRequestIdRef = React.useRef(0);
99
+ const voiceConnectChainRef = React.useRef<Promise<unknown>>(Promise.resolve());
100
+
101
+ const isVoiceAvailable = Boolean(ui.webrtcEnabled && ui.enableVoice !== false);
102
+ const micStartsMuted = ui.startWithMicMuted ?? true;
103
+
104
+ const {
105
+ isMicMuted,
106
+ isMicHeldForAssistant,
107
+ toggleVoiceMicMute,
108
+ prepareForVoiceConnect,
109
+ resetMicState,
110
+ clearAssistantMicHold,
111
+ } = useVoiceMicState(voice, isVoiceConnecting, {
112
+ startWithMicMuted: micStartsMuted,
113
+ // Decoupled: open-mic demo can start unmuted yet still mute during assistant TTS.
114
+ holdMicDuringAssistant: ui.holdMicDuringAssistant ?? true,
115
+ });
116
+
117
+ const setInteractionMode = useCallback(
118
+ (mode: InteractionMode) => {
119
+ if (mode === 'text' && interactionMode === 'voice') {
120
+ void (async () => {
121
+ await voice.stopVoice('switch_to_text');
122
+ await waitForIOSVoiceCaptureRelease();
123
+ resetMicState();
124
+ })();
125
+ }
126
+ setInteractionModeState(mode);
127
+ },
128
+ [interactionMode, voice, resetMicState],
129
+ );
130
+
131
+ useEffect(() => {
132
+ if (interactionMode === 'text' && voice.voiceSessionId != null) {
133
+ void (async () => {
134
+ await voice.stopVoice('mode_text_cleanup');
135
+ await waitForIOSVoiceCaptureRelease();
136
+ resetMicState();
137
+ })();
138
+ }
139
+ }, [interactionMode, voice.voiceSessionId, voice, resetMicState]);
140
+
141
+ const sendText = useCallback(async () => {
142
+ const text = inputText.trim();
143
+ if (!text || agent.isLoading) return;
144
+ setInputText('');
145
+ await agent.sendMessage(text);
146
+ }, [agent, inputText]);
147
+
148
+ const selectPreset = useCallback(
149
+ async (preset: PresetMessage) => {
150
+ if (preset.autoSend) {
151
+ await agent.sendMessage(preset.text);
152
+ } else {
153
+ setInputText(preset.text);
154
+ }
155
+ },
156
+ [agent],
157
+ );
158
+
159
+ const wrappedStartVoice = useCallback(
160
+ (options?: Parameters<typeof voice.startVoice>[0]) => {
161
+ const connect = async () => {
162
+ if (voice.voiceSessionId != null) {
163
+ await voice.stopVoice('restart_voice');
164
+ await waitForIOSVoiceCaptureRelease();
165
+ }
166
+ prepareForVoiceConnect();
167
+ setInteractionModeState('voice');
168
+ setIsVoiceConnecting(true);
169
+ try {
170
+ const session = await voice.startVoice({
171
+ startWithMicMuted: micStartsMuted,
172
+ ...options,
173
+ onOpen: () => {
174
+ if (!micStartsMuted) {
175
+ voice.muteMic(false);
176
+ }
177
+ options?.onOpen?.();
178
+ },
179
+ onClose: (reason) => {
180
+ resetMicState();
181
+ const userInitiated =
182
+ reason === 'switch_to_text' ||
183
+ reason === 'client_stop' ||
184
+ reason === 'mode_text_cleanup';
185
+ if (userInitiated) {
186
+ setInteractionModeState('text');
187
+ } else {
188
+ console.warn('[nxtlinq] voice session closed:', reason);
189
+ }
190
+ options?.onClose?.(reason);
191
+ },
192
+ onError: (err) => {
193
+ resetMicState();
194
+ console.warn('[nxtlinq] voice session error:', err.message);
195
+ options?.onError?.(err);
196
+ },
197
+ });
198
+ return session;
199
+ } catch (err) {
200
+ setInteractionModeState('text');
201
+ resetMicState();
202
+ throw err;
203
+ } finally {
204
+ setIsVoiceConnecting(false);
205
+ }
206
+ };
207
+ const next = voiceConnectChainRef.current.then(connect, connect);
208
+ voiceConnectChainRef.current = next.catch(() => undefined);
209
+ return next;
210
+ },
211
+ [voice, prepareForVoiceConnect, resetMicState, micStartsMuted, ui.voiceRemoteAudioGain],
212
+ );
213
+
214
+ const wrappedStopVoice = useCallback(async () => {
215
+ await voice.stopVoice('client_stop');
216
+ await waitForIOSVoiceCaptureRelease();
217
+ resetMicState();
218
+ setInteractionModeState('text');
219
+ }, [voice, resetMicState]);
220
+
221
+ const wrappedInterrupt = useCallback(() => {
222
+ voice.interrupt();
223
+ clearAssistantMicHold();
224
+ }, [voice, clearAssistantMicHold]);
225
+
226
+ const isVoiceChannelReady =
227
+ interactionMode === 'voice' &&
228
+ voice.voiceSessionId != null &&
229
+ (Boolean(agent.agent.getVoiceSession()?.isAppChannelOpen()) ||
230
+ voice.voiceStatus === 'listening');
231
+
232
+ const postTextTts = useCallback(
233
+ (text: string) => agent.agent.postTextTts(text),
234
+ [agent.agent],
235
+ );
236
+
237
+ const buildTextTtsPlaybackUri = useCallback(
238
+ (result: { audio: ArrayBuffer; mimeType: string }) =>
239
+ agent.agent.buildTextTtsPlaybackUri(result),
240
+ [agent.agent],
241
+ );
242
+
243
+ const playMessageTts = useCallback(
244
+ async (messageId: string, text: string) => {
245
+ const trimmed = text.trim();
246
+ if (!trimmed) return;
247
+ const player = textTtsPlayerRef.current;
248
+ if (!player?.isAvailable) {
249
+ throw new Error(
250
+ 'Text TTS playback requires react-native-video in your app (see Berify Jeannie).',
251
+ );
252
+ }
253
+ const requestId = ++ttsRequestIdRef.current;
254
+ player.stop();
255
+ setPlayingMessageId(messageId);
256
+ try {
257
+ const result = await postTextTts(trimmed);
258
+ if (ttsRequestIdRef.current !== requestId) return;
259
+ player.play(result);
260
+ } catch (err) {
261
+ if (ttsRequestIdRef.current === requestId) {
262
+ setPlayingMessageId(null);
263
+ }
264
+ throw err;
265
+ }
266
+ },
267
+ [postTextTts],
268
+ );
269
+
270
+ const isTextTtsAvailable = isTextTtsPlayerSupported();
271
+
272
+ const value = useMemo<AgentAssistantContextValue>(
273
+ () => ({
274
+ ...agent,
275
+ ...voice,
276
+ isVoiceActive:
277
+ interactionMode === 'voice' && voice.voiceSessionId != null,
278
+ startVoice: wrappedStartVoice,
279
+ stopVoice: wrappedStopVoice,
280
+ interrupt: wrappedInterrupt,
281
+ isMicMuted,
282
+ isMicHeldForAssistant,
283
+ toggleVoiceMicMute,
284
+ theme,
285
+ title: ui.title ?? 'AI Assistant',
286
+ placeholder: ui.placeholder ?? 'Type a message…',
287
+ presetMessages: ui.presetMessages ?? [],
288
+ interactionMode,
289
+ setInteractionMode,
290
+ inputText,
291
+ setInputText,
292
+ isVoiceAvailable,
293
+ isVoiceConnecting,
294
+ isVoiceChannelReady,
295
+ postTextTts,
296
+ buildTextTtsPlaybackUri,
297
+ playMessageTts,
298
+ playingMessageId,
299
+ isTextTtsAvailable,
300
+ sendText,
301
+ selectPreset,
302
+ }),
303
+ [
304
+ agent,
305
+ voice,
306
+ wrappedStartVoice,
307
+ wrappedStopVoice,
308
+ wrappedInterrupt,
309
+ isMicMuted,
310
+ isMicHeldForAssistant,
311
+ toggleVoiceMicMute,
312
+ theme,
313
+ ui.title,
314
+ ui.placeholder,
315
+ ui.presetMessages,
316
+ interactionMode,
317
+ inputText,
318
+ isVoiceAvailable,
319
+ isVoiceConnecting,
320
+ isVoiceChannelReady,
321
+ postTextTts,
322
+ buildTextTtsPlaybackUri,
323
+ playMessageTts,
324
+ playingMessageId,
325
+ isTextTtsAvailable,
326
+ sendText,
327
+ selectPreset,
328
+ voice.voiceStatus,
329
+ ],
330
+ );
331
+
332
+ return (
333
+ <AgentAssistantContext.Provider value={value}>
334
+ {children}
335
+ <TextTtsPlayer
336
+ playerRef={textTtsPlayerRef}
337
+ volume={ui.textTtsVolume ?? 1}
338
+ onPlayingChange={(playing) => {
339
+ if (!playing) setPlayingMessageId(null);
340
+ }}
341
+ onError={(message) => {
342
+ console.warn('[nxtlinq] TTS playback:', message);
343
+ }}
344
+ />
345
+ </AgentAssistantContext.Provider>
346
+ );
347
+ }
348
+
349
+ export function useAgentAssistant(): AgentAssistantContextValue {
350
+ const ctx = useContext(AgentAssistantContext);
351
+ if (!ctx) {
352
+ throw new Error(
353
+ 'useAgentAssistant must be used within <NxtlinqAgentAssistant> or <AgentAssistantProvider>',
354
+ );
355
+ }
356
+ return ctx;
357
+ }
358
+
359
+ export function voiceStatusLabel(status: VoiceStatus): string {
360
+ const labels: Record<VoiceStatus, string> = {
361
+ idle: 'Ready',
362
+ listening: 'Listening',
363
+ transcribing: 'Transcribing',
364
+ thinking: 'Thinking',
365
+ generating: 'Generating',
366
+ speaking: 'Speaking',
367
+ };
368
+ return labels[status] ?? status;
369
+ }
package/src/index.ts ADDED
@@ -0,0 +1,59 @@
1
+ export { NxtlinqAgentAssistant } from './NxtlinqAgentAssistant';
2
+
3
+ export type {
4
+ NxtlinqAgentAssistantProps,
5
+ AgentAssistantTheme,
6
+ PresetMessage,
7
+ Message,
8
+ ToolUse,
9
+ } from './types';
10
+
11
+ export { defaultAgentAssistantTheme } from './theme/defaultTheme';
12
+
13
+ export {
14
+ AgentAssistantProvider,
15
+ useAgentAssistant,
16
+ voiceStatusLabel,
17
+ type AgentAssistantContextValue,
18
+ type InteractionMode,
19
+ } from './context/AgentAssistantContext';
20
+
21
+ export { AgentMessageList } from './components/AgentMessageList';
22
+ export { AgentComposer } from './components/AgentComposer';
23
+ export { AgentVoiceBar } from './components/AgentVoiceBar';
24
+ export { AgentRemoteAudio } from './components/AgentRemoteAudio';
25
+ export { AudioSessionWaker } from './voice/AudioSessionWaker';
26
+ export { PresetMessageChips } from './components/PresetMessageChips';
27
+ export { AgentAssistantShell } from './components/AgentAssistantShell';
28
+ export { VoiceGreetTrigger } from './components/VoiceGreetTrigger';
29
+
30
+ // Headless SDK re-exports for apps that compose custom layouts
31
+ export {
32
+ NxtlinqAgentProvider,
33
+ useNxtlinqAgent,
34
+ useNxtlinqVoice,
35
+ createNxtlinqAgentRN,
36
+ type NxtlinqAgentProviderProps,
37
+ type RNWebRTCModule,
38
+ } from '@bytexbyte/nxtlinq-ai-agent-react-native-development';
39
+
40
+ export type {
41
+ AgentEnvironment,
42
+ AgentConfig,
43
+ Attachment,
44
+ AgentResponse,
45
+ SendMessageOptions,
46
+ NxtlinqAgentSnapshot,
47
+ StartVoiceSessionOptions,
48
+ VoiceSession,
49
+ VoiceStatus,
50
+ } from '@bytexbyte/nxtlinq-ai-agent-core-development';
51
+
52
+ export {
53
+ NxtlinqAgent,
54
+ setApiHosts,
55
+ VoiceNotSupportedError,
56
+ mapServerHistoryToMessages,
57
+ appendServerHistoryIntoMessages,
58
+ STORAGE_KEYS,
59
+ } from '@bytexbyte/nxtlinq-ai-agent-core-development';