@bytexbyte/nxtlinq-ai-agent-ui-react-native-development 0.2.0 → 0.3.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 (104) hide show
  1. package/dist/NxtlinqAgentAssistant.d.ts +4 -4
  2. package/dist/NxtlinqAgentAssistant.d.ts.map +1 -1
  3. package/dist/NxtlinqAgentAssistant.js +5 -6
  4. package/dist/components/AgentAssistantShell.d.ts +1 -3
  5. package/dist/components/AgentAssistantShell.d.ts.map +1 -1
  6. package/dist/components/AgentAssistantShell.js +3 -7
  7. package/dist/components/AgentMessageList.d.ts.map +1 -1
  8. package/dist/components/AgentMessageList.js +7 -9
  9. package/dist/components/AgentVoiceBar.d.ts.map +1 -1
  10. package/dist/components/AgentVoiceBar.js +14 -34
  11. package/dist/components/MessageAttachmentPreview.d.ts +10 -0
  12. package/dist/components/MessageAttachmentPreview.d.ts.map +1 -0
  13. package/dist/components/MessageAttachmentPreview.js +15 -0
  14. package/dist/components/VoiceAddMediaModal.d.ts +12 -0
  15. package/dist/components/VoiceAddMediaModal.d.ts.map +1 -0
  16. package/dist/components/VoiceAddMediaModal.js +31 -0
  17. package/dist/components/VoiceAttachmentButton.d.ts +3 -0
  18. package/dist/components/VoiceAttachmentButton.d.ts.map +1 -0
  19. package/dist/components/VoiceAttachmentButton.js +58 -0
  20. package/dist/components/VoiceIcons.d.ts +1 -0
  21. package/dist/components/VoiceIcons.d.ts.map +1 -1
  22. package/dist/components/VoiceIcons.js +3 -0
  23. package/dist/components/VoiceWaveform.d.ts +2 -2
  24. package/dist/components/VoiceWaveform.d.ts.map +1 -1
  25. package/dist/components/VoiceWaveform.js +16 -5
  26. package/dist/components/useMessageListAutoScroll.d.ts +12 -0
  27. package/dist/components/useMessageListAutoScroll.d.ts.map +1 -0
  28. package/dist/components/useMessageListAutoScroll.js +42 -0
  29. package/dist/context/AgentAssistantContext.d.ts +3 -3
  30. package/dist/context/AgentAssistantContext.d.ts.map +1 -1
  31. package/dist/context/AgentAssistantContext.js +76 -29
  32. package/dist/index.d.ts +3 -2
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +2 -1
  35. package/dist/types.d.ts +3 -8
  36. package/dist/types.d.ts.map +1 -1
  37. package/dist/voice/float32ToPcm16.d.ts +2 -0
  38. package/dist/voice/float32ToPcm16.d.ts.map +1 -0
  39. package/dist/voice/float32ToPcm16.js +8 -0
  40. package/dist/voice/loadImageCropPicker.d.ts +11 -0
  41. package/dist/voice/loadImageCropPicker.d.ts.map +1 -0
  42. package/dist/voice/loadImageCropPicker.js +12 -0
  43. package/dist/voice/sendVoiceImageAttachment.d.ts +15 -0
  44. package/dist/voice/sendVoiceImageAttachment.d.ts.map +1 -0
  45. package/dist/voice/sendVoiceImageAttachment.js +29 -0
  46. package/dist/voice/useVoiceImagePicker.d.ts +11 -0
  47. package/dist/voice/useVoiceImagePicker.d.ts.map +1 -0
  48. package/dist/voice/useVoiceImagePicker.js +38 -0
  49. package/dist/voice/useVoiceMicState.d.ts +4 -0
  50. package/dist/voice/useVoiceMicState.d.ts.map +1 -1
  51. package/dist/voice/useVoiceMicState.js +32 -3
  52. package/dist/voice/useVoiceSilenceCommit.d.ts +10 -0
  53. package/dist/voice/useVoiceSilenceCommit.d.ts.map +1 -0
  54. package/dist/voice/useVoiceSilenceCommit.js +76 -0
  55. package/dist/voice/useVoiceTranscriptMessages.d.ts +16 -0
  56. package/dist/voice/useVoiceTranscriptMessages.d.ts.map +1 -0
  57. package/dist/voice/useVoiceTranscriptMessages.js +133 -0
  58. package/dist/voice/useWsRealtimeAudio.d.ts +17 -0
  59. package/dist/voice/useWsRealtimeAudio.d.ts.map +1 -0
  60. package/dist/voice/useWsRealtimeAudio.js +165 -0
  61. package/dist/voice/voiceImagePickerOptions.d.ts +11 -0
  62. package/dist/voice/voiceImagePickerOptions.d.ts.map +1 -0
  63. package/dist/voice/voiceImagePickerOptions.js +10 -0
  64. package/dist/voice/voiceSilenceConstants.d.ts +8 -0
  65. package/dist/voice/voiceSilenceConstants.d.ts.map +1 -0
  66. package/dist/voice/voiceSilenceConstants.js +7 -0
  67. package/dist/voice/wsPcmPlayer.d.ts +24 -0
  68. package/dist/voice/wsPcmPlayer.d.ts.map +1 -0
  69. package/dist/voice/wsPcmPlayer.js +146 -0
  70. package/dist/voice/wsPcmRecorder.d.ts +26 -0
  71. package/dist/voice/wsPcmRecorder.d.ts.map +1 -0
  72. package/dist/voice/wsPcmRecorder.js +145 -0
  73. package/dist/voice/wsRealtimeConstants.d.ts +2 -0
  74. package/dist/voice/wsRealtimeConstants.d.ts.map +1 -0
  75. package/dist/voice/wsRealtimeConstants.js +1 -0
  76. package/package.json +8 -5
  77. package/src/NxtlinqAgentAssistant.tsx +3 -12
  78. package/src/components/AgentAssistantShell.tsx +2 -18
  79. package/src/components/AgentMessageList.tsx +18 -15
  80. package/src/components/AgentVoiceBar.tsx +35 -70
  81. package/src/components/MessageAttachmentPreview.tsx +43 -0
  82. package/src/components/VoiceAddMediaModal.tsx +69 -0
  83. package/src/components/VoiceAttachmentButton.tsx +100 -0
  84. package/src/components/VoiceIcons.tsx +4 -0
  85. package/src/components/VoiceWaveform.tsx +15 -5
  86. package/src/components/useMessageListAutoScroll.ts +57 -0
  87. package/src/context/AgentAssistantContext.tsx +100 -32
  88. package/src/index.ts +2 -2
  89. package/src/react-native.d.ts +18 -1
  90. package/src/types.ts +3 -8
  91. package/src/voice/float32ToPcm16.ts +8 -0
  92. package/src/voice/loadImageCropPicker.ts +18 -0
  93. package/src/voice/sendVoiceImageAttachment.ts +49 -0
  94. package/src/voice/useVoiceImagePicker.ts +54 -0
  95. package/src/voice/useVoiceMicState.ts +38 -3
  96. package/src/voice/useVoiceSilenceCommit.ts +94 -0
  97. package/src/voice/useVoiceTranscriptMessages.ts +176 -0
  98. package/src/voice/useWsRealtimeAudio.ts +200 -0
  99. package/src/voice/voiceImagePickerOptions.ts +10 -0
  100. package/src/voice/voiceSilenceConstants.ts +10 -0
  101. package/src/voice/wsPcmPlayer.ts +166 -0
  102. package/src/voice/wsPcmRecorder.ts +152 -0
  103. package/src/voice/wsRealtimeConstants.ts +1 -0
  104. package/src/components/AgentRemoteAudio.tsx +0 -105
@@ -0,0 +1,145 @@
1
+ import { AudioManager, AudioRecorder, } from 'react-native-audio-api';
2
+ import { float32ToPcm16 } from './float32ToPcm16';
3
+ import { WS_REALTIME_SAMPLE_RATE } from './wsRealtimeConstants';
4
+ /**
5
+ * Berify {@link WavRecorder}-aligned lifecycle: reuse one native AudioRecorder,
6
+ * configure playAndRecord on start, reset to playback on stop.
7
+ */
8
+ export class WsPcmRecorder {
9
+ constructor() {
10
+ this.recorder = null;
11
+ this.recording = false;
12
+ this.audioReadyAttached = false;
13
+ this.session = null;
14
+ }
15
+ async initialize(options) {
16
+ if (options?.force && this.recorder) {
17
+ this.disposeRecorderInternal();
18
+ }
19
+ if (this.recorder)
20
+ return;
21
+ await AudioManager.requestRecordingPermissions();
22
+ this.recorder = new AudioRecorder({
23
+ sampleRate: WS_REALTIME_SAMPLE_RATE,
24
+ bufferLengthInSamples: WS_REALTIME_SAMPLE_RATE,
25
+ });
26
+ }
27
+ bindSession(session) {
28
+ this.session = session;
29
+ }
30
+ setOnRms(handler) {
31
+ this.onRms = handler;
32
+ }
33
+ disposeRecorderInternal() {
34
+ if (!this.recorder)
35
+ return;
36
+ try {
37
+ this.recorder.stop();
38
+ }
39
+ catch {
40
+ /* noop */
41
+ }
42
+ this.recorder = null;
43
+ this.audioReadyAttached = false;
44
+ this.recording = false;
45
+ this.resetSession();
46
+ }
47
+ async start() {
48
+ await this.initialize();
49
+ const recorder = this.recorder;
50
+ if (!recorder)
51
+ return;
52
+ if (this.recording) {
53
+ this.recording = false;
54
+ try {
55
+ recorder.stop();
56
+ }
57
+ catch {
58
+ /* noop */
59
+ }
60
+ }
61
+ this.configureSession();
62
+ if (!this.audioReadyAttached) {
63
+ recorder.onAudioReady(({ buffer }) => {
64
+ this.handleAudioReady(buffer);
65
+ });
66
+ this.audioReadyAttached = true;
67
+ }
68
+ try {
69
+ recorder.start();
70
+ this.recording = true;
71
+ }
72
+ catch (err) {
73
+ this.recording = false;
74
+ console.error('[WsPcmRecorder] start failed', err);
75
+ throw err;
76
+ }
77
+ }
78
+ stop() {
79
+ if (!this.recorder || !this.recording)
80
+ return;
81
+ try {
82
+ this.recorder.stop();
83
+ }
84
+ catch (err) {
85
+ console.warn('[WsPcmRecorder] stop', err);
86
+ }
87
+ this.recording = false;
88
+ this.resetSession();
89
+ }
90
+ cleanup() {
91
+ if (this.recorder) {
92
+ try {
93
+ if (this.recording) {
94
+ this.stop();
95
+ }
96
+ else {
97
+ this.recorder.stop();
98
+ }
99
+ }
100
+ catch {
101
+ /* noop */
102
+ }
103
+ }
104
+ this.recorder = null;
105
+ this.audioReadyAttached = false;
106
+ this.recording = false;
107
+ this.session = null;
108
+ this.onRms = undefined;
109
+ }
110
+ get isRecording() {
111
+ return this.recording;
112
+ }
113
+ configureSession() {
114
+ AudioManager.setAudioSessionOptions({
115
+ iosCategory: 'playAndRecord',
116
+ iosMode: 'voiceChat',
117
+ iosOptions: ['defaultToSpeaker', 'allowBluetoothA2DP'],
118
+ iosAllowHaptics: true,
119
+ });
120
+ }
121
+ resetSession() {
122
+ try {
123
+ AudioManager.setAudioSessionOptions({
124
+ iosCategory: 'playback',
125
+ iosMode: 'default',
126
+ });
127
+ }
128
+ catch {
129
+ /* noop */
130
+ }
131
+ }
132
+ handleAudioReady(buffer) {
133
+ const channel = buffer.getChannelData(0);
134
+ let sum = 0;
135
+ for (let i = 0; i < channel.length; i += 1) {
136
+ const sample = channel[i];
137
+ sum += sample * sample;
138
+ }
139
+ const rms = channel.length > 0 ? Math.sqrt(sum / channel.length) : 0;
140
+ this.onRms?.(rms);
141
+ if (!this.recording || !this.session?.appendInputAudio)
142
+ return;
143
+ this.session.appendInputAudio(float32ToPcm16(channel));
144
+ }
145
+ }
@@ -0,0 +1,2 @@
1
+ export declare const WS_REALTIME_SAMPLE_RATE = 24000;
2
+ //# sourceMappingURL=wsRealtimeConstants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wsRealtimeConstants.d.ts","sourceRoot":"","sources":["../../src/voice/wsRealtimeConstants.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,uBAAuB,QAAQ,CAAC"}
@@ -0,0 +1 @@
1
+ export const WS_REALTIME_SAMPLE_RATE = 24000;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bytexbyte/nxtlinq-ai-agent-ui-react-native-development",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Official React Native UI for nxtlinq AI Agent — drop-in assistant widget",
5
5
  "main": "dist/index.js",
6
6
  "react-native": "src/index.ts",
@@ -37,20 +37,22 @@
37
37
  "@react-native-async-storage/async-storage": ">=1.17.0",
38
38
  "react": ">=18.0.0",
39
39
  "react-native": ">=0.72.0",
40
+ "react-native-audio-api": ">=0.10.3",
41
+ "react-native-image-crop-picker": ">=0.40.0",
40
42
  "react-native-vector-icons": ">=10.0.0",
41
43
  "react-native-video": ">=6.0.0"
42
44
  },
43
45
  "peerDependenciesMeta": {
44
- "react-native-video": {
46
+ "react-native-image-crop-picker": {
45
47
  "optional": true
46
48
  },
47
- "react-native-webrtc": {
49
+ "react-native-video": {
48
50
  "optional": true
49
51
  }
50
52
  },
51
53
  "dependencies": {
52
- "@bytexbyte/nxtlinq-ai-agent-core-development": "^0.2.0",
53
- "@bytexbyte/nxtlinq-ai-agent-react-native-development": "^0.2.0"
54
+ "@bytexbyte/nxtlinq-ai-agent-core-development": "^0.3.4",
55
+ "@bytexbyte/nxtlinq-ai-agent-react-native-development": "^0.3.4"
54
56
  },
55
57
  "devDependencies": {
56
58
  "@bytexbyte/nxtlinq-ai-agent-core-development": "workspace:^",
@@ -58,6 +60,7 @@
58
60
  "@types/react": "^18.2.64",
59
61
  "@types/react-native-vector-icons": "^6.4.18",
60
62
  "react": "^18.2.0",
63
+ "react-native-audio-api": "^0.10.3",
61
64
  "react-native-vector-icons": "^10.2.0",
62
65
  "typescript": "^5.4.2"
63
66
  }
@@ -9,18 +9,18 @@ import type { NxtlinqAgentAssistantProps } from './types';
9
9
  * @example
10
10
  * ```tsx
11
11
  * import AsyncStorage from '@react-native-async-storage/async-storage';
12
- * import { RTCPeerConnection, mediaDevices } from 'react-native-webrtc';
13
12
  * import { NxtlinqAgentAssistant } from '@bytexbyte/nxtlinq-ai-agent-ui-react-native-development';
14
13
  *
15
14
  * export default function Screen() {
16
15
  * return (
17
16
  * <NxtlinqAgentAssistant
18
17
  * storage={AsyncStorage}
19
- * webrtcModule={{ RTCPeerConnection, mediaDevices }}
20
18
  * serviceId="..."
21
19
  * apiKey="..."
22
20
  * apiSecret="..."
23
- * pseudoId={chatId}
21
+ * environment="staging"
22
+ * pseudoId={userId}
23
+ * conversationId={roomId}
24
24
  * enableVoice
25
25
  * loadHistoryOnMount
26
26
  * />
@@ -43,7 +43,6 @@ export function NxtlinqAgentAssistant({
43
43
  voiceDemoProductImageUrl,
44
44
  voiceAutoGreet,
45
45
  iosSilentAudioSource,
46
- voiceRemoteAudioGain,
47
46
  textTtsVolume,
48
47
  theme,
49
48
  style,
@@ -55,20 +54,14 @@ export function NxtlinqAgentAssistant({
55
54
  storage,
56
55
  fetchImpl,
57
56
  getTimezone,
58
- webrtcModule,
59
- webrtc,
60
57
  resetOnIdentityChange,
61
58
  ...agentConfig
62
59
  }: NxtlinqAgentAssistantProps): React.ReactElement {
63
- const webrtcEnabled = Boolean(webrtcModule || webrtc);
64
-
65
60
  return (
66
61
  <NxtlinqAgentProvider
67
62
  storage={storage}
68
63
  fetchImpl={fetchImpl}
69
64
  getTimezone={getTimezone}
70
- webrtcModule={webrtcModule}
71
- webrtc={webrtc}
72
65
  resetOnIdentityChange={resetOnIdentityChange}
73
66
  onMessage={onMessage}
74
67
  onError={onError}
@@ -90,12 +83,10 @@ export function NxtlinqAgentAssistant({
90
83
  voiceDemoProductImageUrl={voiceDemoProductImageUrl}
91
84
  voiceAutoGreet={voiceAutoGreet}
92
85
  iosSilentAudioSource={iosSilentAudioSource}
93
- voiceRemoteAudioGain={voiceRemoteAudioGain}
94
86
  textTtsVolume={textTtsVolume}
95
87
  theme={theme}
96
88
  style={style}
97
89
  headerStyle={headerStyle}
98
- webrtcEnabled={webrtcEnabled}
99
90
  />
100
91
  {children}
101
92
  </NxtlinqAgentProvider>
@@ -7,11 +7,8 @@ import {
7
7
  import type { NxtlinqAgentAssistantProps } from '../types';
8
8
  import { AgentComposer } from './AgentComposer';
9
9
  import { AgentMessageList } from './AgentMessageList';
10
- import { AgentRemoteAudio } from './AgentRemoteAudio';
11
10
  import { AgentVoiceBar } from './AgentVoiceBar';
12
11
  import { PresetMessageChips } from './PresetMessageChips';
13
- import { VoiceGreetTrigger } from './VoiceGreetTrigger';
14
- import { VoiceImageInput } from './VoiceImageInput';
15
12
  import { VoiceWaveform } from './VoiceWaveform';
16
13
  import { AudioSessionWaker } from '../voice/AudioSessionWaker';
17
14
  import { VoiceAutoGreetBinder } from '../voice/VoiceAutoGreetBinder';
@@ -35,11 +32,8 @@ export type AgentAssistantShellProps = Pick<
35
32
  | 'voiceDemoProductImageUrl'
36
33
  | 'voiceAutoGreet'
37
34
  | 'iosSilentAudioSource'
38
- | 'voiceRemoteAudioGain'
39
35
  | 'textTtsVolume'
40
- > & {
41
- webrtcEnabled: boolean;
42
- };
36
+ >;
43
37
 
44
38
  function AgentAssistantInner({
45
39
  title,
@@ -48,13 +42,11 @@ function AgentAssistantInner({
48
42
  loadHistoryOnMount,
49
43
  historyLast,
50
44
  startInVoiceMode,
51
- startWithMicMuted,
52
45
  showVoiceWaveform,
53
46
  showVoiceImageInput,
54
47
  voiceDemoProductImageUrl,
55
48
  voiceAutoGreet,
56
49
  iosSilentAudioSource,
57
- voiceRemoteAudioGain,
58
50
  }: AgentAssistantShellProps): React.ReactElement {
59
51
  const {
60
52
  theme,
@@ -121,14 +113,7 @@ function AgentAssistantInner({
121
113
  <AgentMessageList />
122
114
  {showVoiceWaveform !== false ? <VoiceWaveform /> : null}
123
115
  <AgentVoiceBar />
124
- {showVoiceImageInput && autoGreetConfig ? (
125
- <VoiceGreetTrigger config={autoGreetConfig} />
126
- ) : null}
127
- {showVoiceImageInput ? (
128
- <VoiceImageInput demoImageUrl={voiceDemoProductImageUrl} />
129
- ) : null}
130
116
  <AgentComposer />
131
- <AgentRemoteAudio remoteAudioGain={voiceRemoteAudioGain} />
132
117
  {autoGreetConfig ? (
133
118
  <VoiceAutoGreetBinder config={autoGreetConfig} historyReady={historyReady} />
134
119
  ) : null}
@@ -148,8 +133,7 @@ export function AgentAssistantShell(props: AgentAssistantShellProps): React.Reac
148
133
  startWithMicMuted: props.startWithMicMuted,
149
134
  holdMicDuringAssistant: props.holdMicDuringAssistant,
150
135
  textTtsVolume: props.textTtsVolume,
151
- voiceRemoteAudioGain: props.voiceRemoteAudioGain,
152
- webrtcEnabled: props.webrtcEnabled,
136
+ showVoiceImageInput: props.showVoiceImageInput,
153
137
  }}
154
138
  >
155
139
  <AgentAssistantInner {...props} />
@@ -1,5 +1,5 @@
1
1
  import type { Message } from '@bytexbyte/nxtlinq-ai-agent-core-development';
2
- import React, { useCallback, useEffect, useRef } from 'react';
2
+ import React, { useCallback, useRef } from 'react';
3
3
  import {
4
4
  ActivityIndicator,
5
5
  FlatList,
@@ -9,7 +9,9 @@ import {
9
9
  View,
10
10
  } from 'react-native';
11
11
  import { useAgentAssistant } from '../context/AgentAssistantContext';
12
+ import { MessageAttachmentPreview } from './MessageAttachmentPreview';
12
13
  import { SpeakerIcon } from './VoiceIcons';
14
+ import { useMessageListAutoScroll } from './useMessageListAutoScroll';
13
15
 
14
16
  function MessageBubble({ message }: { message: Message }): React.ReactElement {
15
17
  const {
@@ -77,14 +79,19 @@ function MessageBubble({ message }: { message: Message }): React.ReactElement {
77
79
  },
78
80
  ]}
79
81
  >
80
- <Text
81
- style={{
82
- color: isUser ? theme.colors.userText : theme.colors.assistantText,
83
- fontSize: theme.typography.bodySize,
84
- }}
85
- >
86
- {displayText || ' '}
87
- </Text>
82
+ {message.attachments?.length ? (
83
+ <MessageAttachmentPreview attachments={message.attachments} theme={theme} />
84
+ ) : null}
85
+ {displayText?.trim() ? (
86
+ <Text
87
+ style={{
88
+ color: isUser ? theme.colors.userText : theme.colors.assistantText,
89
+ fontSize: theme.typography.bodySize,
90
+ }}
91
+ >
92
+ {displayText}
93
+ </Text>
94
+ ) : null}
88
95
  {message.isStreaming && message.streamingStatus ? (
89
96
  <Text
90
97
  style={{
@@ -142,12 +149,7 @@ function MessageBubble({ message }: { message: Message }): React.ReactElement {
142
149
  export function AgentMessageList(): React.ReactElement {
143
150
  const { messages, isLoading, theme } = useAgentAssistant();
144
151
  const listRef = useRef<{ scrollToEnd: (opts: { animated: boolean }) => void } | null>(null);
145
-
146
- useEffect(() => {
147
- if (messages.length > 0) {
148
- listRef.current?.scrollToEnd({ animated: true });
149
- }
150
- }, [messages.length, messages[messages.length - 1]?.content]);
152
+ const { onContentSizeChange } = useMessageListAutoScroll(messages, listRef);
151
153
 
152
154
  return (
153
155
  <View style={[styles.container, { backgroundColor: theme.colors.background }]}>
@@ -156,6 +158,7 @@ export function AgentMessageList(): React.ReactElement {
156
158
  data={messages}
157
159
  keyExtractor={(item: Message) => item.id}
158
160
  renderItem={({ item }: { item: Message }) => <MessageBubble message={item} />}
161
+ onContentSizeChange={onContentSizeChange}
159
162
  contentContainerStyle={{ padding: theme.spacing.md, paddingBottom: theme.spacing.lg }}
160
163
  ListEmptyComponent={
161
164
  <Text style={[styles.empty, { color: theme.colors.mutedText }]}>
@@ -1,4 +1,4 @@
1
- import React, { useCallback, useEffect, useState } from 'react';
1
+ import React, { useCallback } from 'react';
2
2
  import {
3
3
  ActivityIndicator,
4
4
  Pressable,
@@ -10,8 +10,8 @@ import {
10
10
  useAgentAssistant,
11
11
  voiceStatusLabel,
12
12
  } from '../context/AgentAssistantContext';
13
- import { SPEAKER_ACTIVE_STATUSES } from '../voice/voiceMicConstants';
14
- import { MicIcon, MicOffIcon, SpeakerIcon, StopIcon } from './VoiceIcons';
13
+ import { MicIcon, MicOffIcon } from './VoiceIcons';
14
+ import { VoiceAttachmentButton } from './VoiceAttachmentButton';
15
15
 
16
16
  function statusDotColor(
17
17
  status: ReturnType<typeof useAgentAssistant>['voiceStatus'],
@@ -38,29 +38,13 @@ export function AgentVoiceBar(): React.ReactElement | null {
38
38
  isVoiceConnecting,
39
39
  isVoiceChannelReady,
40
40
  stopVoice,
41
- interrupt,
42
41
  isVoiceAvailable,
43
42
  isMicMuted,
44
43
  isMicHeldForAssistant,
45
44
  toggleVoiceMicMute,
45
+ showVoiceImageInput,
46
46
  } = useAgentAssistant();
47
47
 
48
- const [speakerPulseOpacity, setSpeakerPulseOpacity] = useState(1);
49
- const isSpeakerActive = SPEAKER_ACTIVE_STATUSES.has(voiceStatus);
50
-
51
- useEffect(() => {
52
- if (!isSpeakerActive) {
53
- setSpeakerPulseOpacity(1);
54
- return;
55
- }
56
- let high = true;
57
- const id = setInterval(() => {
58
- setSpeakerPulseOpacity(high ? 0.35 : 1);
59
- high = !high;
60
- }, 600);
61
- return () => clearInterval(id);
62
- }, [isSpeakerActive]);
63
-
64
48
  const returnToTextMode = useCallback(() => {
65
49
  void stopVoice();
66
50
  }, [stopVoice]);
@@ -79,17 +63,17 @@ export function AgentVoiceBar(): React.ReactElement | null {
79
63
  ? 'Tap Back to text mode below to cancel'
80
64
  : awaitingChannel
81
65
  ? 'Waiting for voice channel…'
82
- : isMicHeldForAssistant
83
- ? 'Mic paused while assistant responds (use Interrupt to speak)'
84
- : isMicMuted
85
- ? 'Mic is off — tap the mic when ready to speak'
86
- : voiceStatus === 'listening'
87
- ? 'Start speaking'
88
- : voiceStatus === 'speaking'
89
- ? 'Assistant is speaking…'
90
- : voiceStatus === 'thinking'
91
- ? 'Thinking…'
92
- : '';
66
+ : isMicHeldForAssistant
67
+ ? 'Assistant is responding tap mic to interrupt and speak'
68
+ : isMicMuted
69
+ ? 'Mic is off — tap the mic when ready to speak'
70
+ : voiceStatus === 'listening'
71
+ ? 'Start speaking'
72
+ : voiceStatus === 'speaking'
73
+ ? 'Assistant is speaking…'
74
+ : voiceStatus === 'thinking'
75
+ ? 'Thinking…'
76
+ : '';
93
77
 
94
78
  return (
95
79
  <View
@@ -150,42 +134,24 @@ export function AgentVoiceBar(): React.ReactElement | null {
150
134
  ) : null}
151
135
  </View>
152
136
 
153
- <View style={styles.iconRow}>
154
- <View style={{ opacity: isSpeakerActive ? speakerPulseOpacity : 0.35 }}>
155
- <SpeakerIcon size={24} color={theme.colors.voiceSpeaking} />
156
- </View>
157
-
158
- <Pressable
159
- onPress={toggleVoiceMicMute}
160
- disabled={showConnecting || awaitingChannel || isMicHeldForAssistant}
161
- style={({ pressed }) => [
162
- styles.iconButton,
163
- {
164
- opacity:
165
- pressed || showConnecting || awaitingChannel || isMicHeldForAssistant
166
- ? 0.45
167
- : 1,
168
- },
169
- ]}
170
- >
171
- {isMicMuted ? (
172
- <MicOffIcon size={24} color="#ef4444" />
173
- ) : (
174
- <MicIcon size={24} color={theme.colors.assistantText} />
175
- )}
176
- </Pressable>
137
+ {showVoiceImageInput ? <VoiceAttachmentButton /> : null}
177
138
 
178
- <Pressable
179
- onPress={() => interrupt()}
180
- disabled={showConnecting || awaitingChannel}
181
- style={({ pressed }) => [
182
- styles.iconButton,
183
- { opacity: pressed || showConnecting || awaitingChannel ? 0.45 : 1 },
184
- ]}
185
- >
186
- <StopIcon size={24} color={theme.colors.assistantText} />
187
- </Pressable>
188
- </View>
139
+ <Pressable
140
+ onPress={toggleVoiceMicMute}
141
+ disabled={showConnecting || awaitingChannel}
142
+ style={({ pressed }) => [
143
+ styles.micButton,
144
+ {
145
+ opacity: pressed || showConnecting || awaitingChannel ? 0.45 : 1,
146
+ },
147
+ ]}
148
+ >
149
+ {isMicMuted ? (
150
+ <MicOffIcon size={28} color="#ef4444" />
151
+ ) : (
152
+ <MicIcon size={28} color={theme.colors.assistantText} />
153
+ )}
154
+ </Pressable>
189
155
  </View>
190
156
 
191
157
  <Pressable onPress={returnToTextMode} style={styles.textLink}>
@@ -223,10 +189,9 @@ const styles = StyleSheet.create({
223
189
  alignSelf: 'stretch',
224
190
  },
225
191
  dot: { width: 8, height: 8, borderRadius: 4 },
226
- iconRow: { flexDirection: 'row', alignItems: 'center', gap: 2 },
227
- iconButton: {
228
- padding: 8,
229
- borderRadius: 8,
192
+ micButton: {
193
+ padding: 10,
194
+ borderRadius: 12,
230
195
  },
231
196
  textLink: { marginTop: 10, alignSelf: 'center' },
232
197
  });
@@ -0,0 +1,43 @@
1
+ import type { Attachment } from '@bytexbyte/nxtlinq-ai-agent-core-development';
2
+ import React from 'react';
3
+ import { Image, StyleSheet, Text, View } from 'react-native';
4
+ import type { AgentAssistantTheme } from '../types';
5
+
6
+ type MessageAttachmentPreviewProps = {
7
+ attachments: Attachment[];
8
+ theme: AgentAssistantTheme;
9
+ };
10
+
11
+ export function MessageAttachmentPreview({
12
+ attachments,
13
+ theme,
14
+ }: MessageAttachmentPreviewProps): React.ReactElement | null {
15
+ const images = attachments.filter((item) => item.type === 'image');
16
+ if (!images.length) return null;
17
+
18
+ return (
19
+ <View style={styles.wrap}>
20
+ {images.map((item, index) => (
21
+ <Image
22
+ key={`${item.name}-${index}`}
23
+ source={{ uri: item.url }}
24
+ style={[
25
+ styles.image,
26
+ { borderRadius: theme.radius.bubble, backgroundColor: theme.colors.surface },
27
+ ]}
28
+ resizeMode="cover"
29
+ />
30
+ ))}
31
+ {attachments.some((item) => item.type === 'file') ? (
32
+ <Text style={{ color: theme.colors.mutedText, fontSize: theme.typography.captionSize }}>
33
+ {attachments.filter((item) => item.type === 'file').map((item) => item.name).join(', ')}
34
+ </Text>
35
+ ) : null}
36
+ </View>
37
+ );
38
+ }
39
+
40
+ const styles = StyleSheet.create({
41
+ wrap: { marginBottom: 6, gap: 6 },
42
+ image: { width: 160, height: 160 },
43
+ });
@@ -0,0 +1,69 @@
1
+ import React from 'react';
2
+ import { Modal, Pressable, StyleSheet, Text, View } from 'react-native';
3
+ import type { AgentAssistantTheme } from '../types';
4
+
5
+ type VoiceAddMediaModalProps = {
6
+ visible: boolean;
7
+ theme: AgentAssistantTheme;
8
+ onClose: () => void;
9
+ onPickPhotos: () => void;
10
+ onTakePhoto: () => void;
11
+ };
12
+
13
+ export function VoiceAddMediaModal({
14
+ visible,
15
+ theme,
16
+ onClose,
17
+ onPickPhotos,
18
+ onTakePhoto,
19
+ }: VoiceAddMediaModalProps): React.ReactElement | null {
20
+ if (!visible) return null;
21
+
22
+ return (
23
+ <Modal transparent visible={visible} animationType="fade" onRequestClose={onClose}>
24
+ <Pressable style={styles.overlay} onPress={onClose}>
25
+ <Pressable
26
+ style={[
27
+ styles.modal,
28
+ { backgroundColor: theme.colors.assistantBubble, borderRadius: theme.radius.panel },
29
+ ]}
30
+ onPress={() => undefined}
31
+ >
32
+ <Pressable style={styles.option} onPress={onPickPhotos}>
33
+ <Text style={{ color: theme.colors.assistantText, fontSize: theme.typography.bodySize }}>
34
+ Photos
35
+ </Text>
36
+ </Pressable>
37
+ <View style={[styles.separator, { backgroundColor: theme.colors.border }]} />
38
+ <Pressable style={styles.option} onPress={onTakePhoto}>
39
+ <Text style={{ color: theme.colors.assistantText, fontSize: theme.typography.bodySize }}>
40
+ Camera
41
+ </Text>
42
+ </Pressable>
43
+ </Pressable>
44
+ </Pressable>
45
+ </Modal>
46
+ );
47
+ }
48
+
49
+ const styles = StyleSheet.create({
50
+ overlay: {
51
+ flex: 1,
52
+ backgroundColor: 'rgba(0,0,0,0.45)',
53
+ justifyContent: 'flex-end',
54
+ alignItems: 'flex-start',
55
+ paddingBottom: 120,
56
+ paddingLeft: 16,
57
+ },
58
+ modal: {
59
+ width: 160,
60
+ paddingVertical: 4,
61
+ shadowColor: '#000',
62
+ shadowOffset: { width: 0, height: -2 },
63
+ shadowOpacity: 0.2,
64
+ shadowRadius: 8,
65
+ elevation: 8,
66
+ },
67
+ option: { paddingVertical: 12, paddingHorizontal: 16 },
68
+ separator: { height: StyleSheet.hairlineWidth, marginHorizontal: 16 },
69
+ });