@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,166 @@
1
+ import {
2
+ AnalyserNode,
3
+ AudioBuffer,
4
+ AudioBufferSourceNode,
5
+ AudioContext,
6
+ GainNode,
7
+ } from 'react-native-audio-api';
8
+ import { WS_REALTIME_SAMPLE_RATE } from './wsRealtimeConstants';
9
+
10
+ const LEVEL_SCALE = 3.2;
11
+ const LOOKAHEAD_SECONDS = 0.02;
12
+ const CROSSFADE_SECONDS = 0.04;
13
+
14
+ function applyFade(buffer: Float32Array, fadeSamples: number): void {
15
+ const fade = Math.min(fadeSamples, Math.floor(buffer.length / 2));
16
+ if (fade <= 0) return;
17
+ for (let i = 0; i < fade; i += 1) {
18
+ buffer[i]! *= i / fade;
19
+ buffer[buffer.length - 1 - i]! *= (fade - i) / fade;
20
+ }
21
+ }
22
+
23
+ export class WsPcmPlayer {
24
+ private readonly sampleRate = WS_REALTIME_SAMPLE_RATE;
25
+ private readonly fadeSamples = Math.max(1, Math.floor(this.sampleRate * 0.01));
26
+ private audioContext: AudioContext | null = null;
27
+ private gainNode: GainNode | null = null;
28
+ private analyserNode: AnalyserNode | null = null;
29
+ private analyserBuffer: Float32Array | null = null;
30
+ private readonly activeSources = new Set<AudioBufferSourceNode>();
31
+ private queue: AudioBuffer[] = [];
32
+ private isPlaying = true;
33
+ private playHead = 0;
34
+ private lastChunkRms = 0;
35
+ private lastChunkAt = 0;
36
+
37
+ async ensureRunning(): Promise<void> {
38
+ const ctx = this.ensureContext();
39
+ if (ctx.state === 'suspended') {
40
+ await ctx.resume();
41
+ }
42
+ this.isPlaying = true;
43
+ }
44
+
45
+ prewarm(): void {
46
+ this.ensureContext();
47
+ }
48
+
49
+ addAudio(pcm16: Int16Array | ArrayBuffer): void {
50
+ const ctx = this.ensureContext();
51
+ const int16 = pcm16 instanceof ArrayBuffer ? new Int16Array(pcm16) : pcm16;
52
+ if (int16.length === 0) return;
53
+
54
+ const floats = new Float32Array(int16.length);
55
+ let sumSq = 0;
56
+ for (let i = 0; i < int16.length; i += 1) {
57
+ const sample = int16[i]! / 32768;
58
+ floats[i] = Math.max(-1, Math.min(1, sample));
59
+ sumSq += sample * sample;
60
+ }
61
+ applyFade(floats, this.fadeSamples);
62
+ this.lastChunkRms = Math.min(1, Math.sqrt(sumSq / int16.length) * LEVEL_SCALE);
63
+ this.lastChunkAt = Date.now();
64
+
65
+ const buffer = ctx.createBuffer(1, floats.length, this.sampleRate);
66
+ buffer.getChannelData(0).set(floats);
67
+ this.queue.push(buffer);
68
+ this.isPlaying = true;
69
+ this.pipelineQueue();
70
+ }
71
+
72
+ getAudioLevel(): number {
73
+ const analyser = this.analyserNode;
74
+ const buf = this.analyserBuffer;
75
+ if (analyser && buf && this.activeSources.size > 0) {
76
+ analyser.getFloatTimeDomainData(buf);
77
+ let sumSq = 0;
78
+ for (let i = 0; i < buf.length; i += 1) {
79
+ const v = buf[i]!;
80
+ sumSq += v * v;
81
+ }
82
+ return Math.min(1, Math.sqrt(sumSq / buf.length) * LEVEL_SCALE);
83
+ }
84
+ if (Date.now() - this.lastChunkAt < 500) {
85
+ return this.lastChunkRms;
86
+ }
87
+ return 0;
88
+ }
89
+
90
+ clearQueue(): void {
91
+ for (const source of this.activeSources) {
92
+ try {
93
+ source.stop();
94
+ } catch {
95
+ /* already stopped */
96
+ }
97
+ }
98
+ this.activeSources.clear();
99
+ this.queue = [];
100
+ this.playHead = this.audioContext?.currentTime ?? 0;
101
+ this.lastChunkRms = 0;
102
+ this.lastChunkAt = 0;
103
+ }
104
+
105
+ cleanup(): void {
106
+ this.clearQueue();
107
+ this.isPlaying = false;
108
+ this.analyserNode = null;
109
+ this.analyserBuffer = null;
110
+ this.gainNode = null;
111
+ this.audioContext = null;
112
+ }
113
+
114
+ private ensureContext(): AudioContext {
115
+ if (!this.audioContext) {
116
+ this.audioContext = new AudioContext({ sampleRate: this.sampleRate });
117
+ this.gainNode = this.audioContext.createGain();
118
+ this.analyserNode = this.audioContext.createAnalyser();
119
+ this.analyserNode.fftSize = 512;
120
+ this.analyserBuffer = new Float32Array(this.analyserNode.fftSize);
121
+ this.gainNode.connect(this.analyserNode);
122
+ this.analyserNode.connect(this.audioContext.destination);
123
+ this.playHead = this.audioContext.currentTime;
124
+ }
125
+ return this.audioContext;
126
+ }
127
+
128
+ /** Berify WavStreamPlayer: schedule chunks with lookahead + crossfade, no gaps. */
129
+ private pipelineQueue(): void {
130
+ const ctx = this.audioContext;
131
+ const gain = this.gainNode;
132
+ if (!ctx || !gain || !this.isPlaying) return;
133
+
134
+ while (this.queue.length > 0) {
135
+ const audioBuffer = this.queue.shift();
136
+ if (!audioBuffer) return;
137
+
138
+ const source = ctx.createBufferSource();
139
+ source.buffer = audioBuffer;
140
+ const perSourceGain = ctx.createGain();
141
+ source.connect(perSourceGain);
142
+ perSourceGain.connect(gain);
143
+
144
+ const now = ctx.currentTime;
145
+ const startAt = Math.max(
146
+ (this.playHead || now) - CROSSFADE_SECONDS * 0.75,
147
+ now + LOOKAHEAD_SECONDS,
148
+ );
149
+ const duration = audioBuffer.duration;
150
+ const endAt = startAt + duration;
151
+ const fade = Math.min(CROSSFADE_SECONDS, duration / 2);
152
+
153
+ perSourceGain.gain.setValueAtTime(0, startAt);
154
+ perSourceGain.gain.linearRampToValueAtTime(1, startAt + fade);
155
+ perSourceGain.gain.setValueAtTime(1, endAt - fade);
156
+ perSourceGain.gain.linearRampToValueAtTime(0, endAt);
157
+
158
+ this.playHead = endAt;
159
+ this.activeSources.add(source);
160
+ source.start(startAt);
161
+ source.onEnded = () => {
162
+ this.activeSources.delete(source);
163
+ };
164
+ }
165
+ }
166
+ }
@@ -0,0 +1,152 @@
1
+ import type { VoiceSession } from '@bytexbyte/nxtlinq-ai-agent-core-development';
2
+ import {
3
+ AudioManager,
4
+ AudioRecorder,
5
+ type AudioBuffer,
6
+ } from 'react-native-audio-api';
7
+ import { float32ToPcm16 } from './float32ToPcm16';
8
+ import { WS_REALTIME_SAMPLE_RATE } from './wsRealtimeConstants';
9
+
10
+ /**
11
+ * Berify {@link WavRecorder}-aligned lifecycle: reuse one native AudioRecorder,
12
+ * configure playAndRecord on start, reset to playback on stop.
13
+ */
14
+ export class WsPcmRecorder {
15
+ private recorder: AudioRecorder | null = null;
16
+ private recording = false;
17
+ private audioReadyAttached = false;
18
+ private session: VoiceSession | null = null;
19
+ private onRms?: (rms: number) => void;
20
+
21
+ async initialize(options?: { force?: boolean }): Promise<void> {
22
+ if (options?.force && this.recorder) {
23
+ this.disposeRecorderInternal();
24
+ }
25
+ if (this.recorder) return;
26
+ await AudioManager.requestRecordingPermissions();
27
+ this.recorder = new AudioRecorder({
28
+ sampleRate: WS_REALTIME_SAMPLE_RATE,
29
+ bufferLengthInSamples: WS_REALTIME_SAMPLE_RATE,
30
+ });
31
+ }
32
+
33
+ bindSession(session: VoiceSession | null): void {
34
+ this.session = session;
35
+ }
36
+
37
+ setOnRms(handler: (rms: number) => void): void {
38
+ this.onRms = handler;
39
+ }
40
+
41
+ private disposeRecorderInternal(): void {
42
+ if (!this.recorder) return;
43
+ try {
44
+ this.recorder.stop();
45
+ } catch {
46
+ /* noop */
47
+ }
48
+ this.recorder = null;
49
+ this.audioReadyAttached = false;
50
+ this.recording = false;
51
+ this.resetSession();
52
+ }
53
+
54
+ async start(): Promise<void> {
55
+ await this.initialize();
56
+ const recorder = this.recorder;
57
+ if (!recorder) return;
58
+
59
+ if (this.recording) {
60
+ this.recording = false;
61
+ try {
62
+ recorder.stop();
63
+ } catch {
64
+ /* noop */
65
+ }
66
+ }
67
+
68
+ this.configureSession();
69
+ if (!this.audioReadyAttached) {
70
+ recorder.onAudioReady(({ buffer }) => {
71
+ this.handleAudioReady(buffer);
72
+ });
73
+ this.audioReadyAttached = true;
74
+ }
75
+
76
+ try {
77
+ recorder.start();
78
+ this.recording = true;
79
+ } catch (err) {
80
+ this.recording = false;
81
+ console.error('[WsPcmRecorder] start failed', err);
82
+ throw err;
83
+ }
84
+ }
85
+
86
+ stop(): void {
87
+ if (!this.recorder || !this.recording) return;
88
+ try {
89
+ this.recorder.stop();
90
+ } catch (err) {
91
+ console.warn('[WsPcmRecorder] stop', err);
92
+ }
93
+ this.recording = false;
94
+ this.resetSession();
95
+ }
96
+
97
+ cleanup(): void {
98
+ if (this.recorder) {
99
+ try {
100
+ if (this.recording) {
101
+ this.stop();
102
+ } else {
103
+ this.recorder.stop();
104
+ }
105
+ } catch {
106
+ /* noop */
107
+ }
108
+ }
109
+ this.recorder = null;
110
+ this.audioReadyAttached = false;
111
+ this.recording = false;
112
+ this.session = null;
113
+ this.onRms = undefined;
114
+ }
115
+
116
+ get isRecording(): boolean {
117
+ return this.recording;
118
+ }
119
+
120
+ private configureSession(): void {
121
+ AudioManager.setAudioSessionOptions({
122
+ iosCategory: 'playAndRecord',
123
+ iosMode: 'voiceChat',
124
+ iosOptions: ['defaultToSpeaker', 'allowBluetoothA2DP'],
125
+ iosAllowHaptics: true,
126
+ });
127
+ }
128
+
129
+ private resetSession(): void {
130
+ try {
131
+ AudioManager.setAudioSessionOptions({
132
+ iosCategory: 'playback',
133
+ iosMode: 'default',
134
+ });
135
+ } catch {
136
+ /* noop */
137
+ }
138
+ }
139
+
140
+ private handleAudioReady(buffer: AudioBuffer): void {
141
+ const channel = buffer.getChannelData(0);
142
+ let sum = 0;
143
+ for (let i = 0; i < channel.length; i += 1) {
144
+ const sample = channel[i]!;
145
+ sum += sample * sample;
146
+ }
147
+ const rms = channel.length > 0 ? Math.sqrt(sum / channel.length) : 0;
148
+ this.onRms?.(rms);
149
+ if (!this.recording || !this.session?.appendInputAudio) return;
150
+ this.session.appendInputAudio(float32ToPcm16(channel));
151
+ }
152
+ }
@@ -0,0 +1 @@
1
+ export const WS_REALTIME_SAMPLE_RATE = 24000;
@@ -1,105 +0,0 @@
1
- import React, { useEffect, useState } from 'react';
2
- import { StyleSheet } from 'react-native';
3
- import { useAgentAssistant } from '../context/AgentAssistantContext';
4
- import {
5
- applyRemoteAudioPlaybackGain,
6
- DEFAULT_REMOTE_AUDIO_GAIN,
7
- } from '@bytexbyte/nxtlinq-ai-agent-core-development';
8
-
9
- type RTCViewComponent = React.ComponentType<{
10
- streamURL: string;
11
- objectFit?: string;
12
- style?: object;
13
- zOrder?: number;
14
- }>;
15
-
16
- let RTCView: RTCViewComponent | null = null;
17
- try {
18
- // eslint-disable-next-line @typescript-eslint/no-require-imports
19
- RTCView = require('react-native-webrtc').RTCView as RTCViewComponent;
20
- } catch {
21
- RTCView = null;
22
- }
23
-
24
- type StreamWithUrl = {
25
- toURL?: () => string;
26
- getAudioTracks?: () => Array<{ enabled: boolean; kind?: string }>;
27
- };
28
-
29
- export type AgentRemoteAudioProps = {
30
- /**
31
- * Playback gain for assistant WebRTC audio (`MediaStreamTrack._setVolume`, 0–10).
32
- * Text TTS uses react-native-video at volume 1 and sounds louder by default.
33
- */
34
- remoteAudioGain?: number;
35
- };
36
-
37
- /**
38
- * Plays remote WebRTC assistant audio. On iOS/Android, {@link RTCView} is required
39
- * (enabling tracks alone is not enough).
40
- */
41
- export function AgentRemoteAudio({
42
- remoteAudioGain = DEFAULT_REMOTE_AUDIO_GAIN,
43
- }: AgentRemoteAudioProps): React.ReactElement | null {
44
- const { getRemoteAudioStream, interactionMode, voiceSessionId, voiceStatus } =
45
- useAgentAssistant();
46
- const [streamUrl, setStreamUrl] = useState<string | null>(null);
47
-
48
- useEffect(() => {
49
- if (interactionMode !== 'voice' || !voiceSessionId) {
50
- setStreamUrl(null);
51
- return;
52
- }
53
-
54
- const sync = () => {
55
- const stream = getRemoteAudioStream() as StreamWithUrl | null;
56
- applyRemoteAudioPlaybackGain(
57
- stream as Parameters<typeof applyRemoteAudioPlaybackGain>[0],
58
- remoteAudioGain,
59
- );
60
- const tracks = stream?.getAudioTracks?.() ?? [];
61
- for (const track of tracks) {
62
- if (track.kind === 'audio' || track.kind == null) {
63
- track.enabled = true;
64
- }
65
- }
66
- const url = stream?.toURL?.() ?? null;
67
- setStreamUrl((prev) => (prev === url ? prev : url));
68
- };
69
-
70
- sync();
71
- const id = setInterval(sync, 150);
72
- return () => clearInterval(id);
73
- }, [
74
- getRemoteAudioStream,
75
- interactionMode,
76
- voiceSessionId,
77
- voiceStatus,
78
- remoteAudioGain,
79
- ]);
80
-
81
- if (!RTCView || !streamUrl || interactionMode !== 'voice') {
82
- return null;
83
- }
84
-
85
- return (
86
- <RTCView
87
- key={streamUrl}
88
- streamURL={streamUrl}
89
- objectFit="cover"
90
- zOrder={-1}
91
- style={styles.hidden}
92
- />
93
- );
94
- }
95
-
96
- const styles = StyleSheet.create({
97
- hidden: {
98
- position: 'absolute',
99
- width: 1,
100
- height: 1,
101
- opacity: 0.01,
102
- left: -1000,
103
- top: -1000,
104
- },
105
- });