@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,11 @@
1
+ import { DEFAULT_REMOTE_AUDIO_GAIN, } from '@bytexbyte/nxtlinq-ai-agent-core-development';
2
+ export const DEFAULT_VOICE_REMOTE_AUDIO_GAIN = DEFAULT_REMOTE_AUDIO_GAIN;
3
+ /** @deprecated Use {@link applyRemoteAudioPlaybackGain} */
4
+ export function applyWebRTCAudioTrackGain(tracks, gain) {
5
+ const clamped = Math.max(0, Math.min(10, gain));
6
+ for (const track of tracks) {
7
+ if ((track.kind === 'audio' || track.kind == null) && track._setVolume) {
8
+ track._setVolume(clamped);
9
+ }
10
+ }
11
+ }
@@ -0,0 +1,9 @@
1
+ import { type PostTextTtsResult } from '@bytexbyte/nxtlinq-ai-agent-core-development';
2
+ export declare function isTtsCacheFileSupported(): boolean;
3
+ /**
4
+ * Persist TTS bytes to cache and return a `file://` URI for react-native-video.
5
+ * Data URIs are unreliable for MPEG on iOS.
6
+ */
7
+ export declare function writeTtsCacheFile(result: PostTextTtsResult): Promise<string>;
8
+ export declare function removeTtsCacheFile(uri: string): Promise<void>;
9
+ //# sourceMappingURL=writeTtsCacheFile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"writeTtsCacheFile.d.ts","sourceRoot":"","sources":["../../src/voice/writeTtsCacheFile.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,KAAK,iBAAiB,EAAE,MAAM,8CAA8C,CAAC;AAkB3G,wBAAgB,uBAAuB,IAAI,OAAO,CAEjD;AAED;;;GAGG;AACH,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,CAWlF;AAED,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAOnE"}
@@ -0,0 +1,37 @@
1
+ import { buildTextTtsDataUri } from '@bytexbyte/nxtlinq-ai-agent-core-development';
2
+ let blobUtil = null;
3
+ try {
4
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
5
+ blobUtil = require('react-native-blob-util').default;
6
+ }
7
+ catch {
8
+ blobUtil = null;
9
+ }
10
+ export function isTtsCacheFileSupported() {
11
+ return blobUtil != null;
12
+ }
13
+ /**
14
+ * Persist TTS bytes to cache and return a `file://` URI for react-native-video.
15
+ * Data URIs are unreliable for MPEG on iOS.
16
+ */
17
+ export async function writeTtsCacheFile(result) {
18
+ if (!blobUtil) {
19
+ return buildTextTtsDataUri(result.audio, result.mimeType);
20
+ }
21
+ const dataUri = buildTextTtsDataUri(result.audio, result.mimeType);
22
+ const comma = dataUri.indexOf(',');
23
+ const base64 = comma >= 0 ? dataUri.slice(comma + 1) : '';
24
+ const path = `${blobUtil.fs.dirs.CacheDir}/nxtlinq-tts-${Date.now()}.mp3`;
25
+ await blobUtil.fs.writeFile(path, base64, 'base64');
26
+ return path.startsWith('file://') ? path : `file://${path}`;
27
+ }
28
+ export async function removeTtsCacheFile(uri) {
29
+ if (!blobUtil || !uri.startsWith(blobUtil.fs.dirs.CacheDir))
30
+ return;
31
+ try {
32
+ await blobUtil.fs.unlink(uri.replace(/^file:\/\//, ''));
33
+ }
34
+ catch {
35
+ /* noop */
36
+ }
37
+ }
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@bytexbyte/nxtlinq-ai-agent-ui-react-native-development",
3
+ "version": "0.2.0",
4
+ "description": "Official React Native UI for nxtlinq AI Agent — drop-in assistant widget",
5
+ "main": "dist/index.js",
6
+ "react-native": "src/index.ts",
7
+ "types": "dist/index.d.ts",
8
+ "files": [
9
+ "dist",
10
+ "src"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsc",
14
+ "clean": "rm -rf dist",
15
+ "prepublishOnly": "yarn build"
16
+ },
17
+ "keywords": [
18
+ "nxtlinq",
19
+ "ai-agent",
20
+ "react-native",
21
+ "ui",
22
+ "sdk"
23
+ ],
24
+ "author": "ByteXByte",
25
+ "license": "MIT",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://dev.azure.com/nxtlinqLLC/nxtlinq/_git/nxtlinq-AI-Agent-SDK",
29
+ "directory": "packages/ai-agent-ui-react-native"
30
+ },
31
+ "publishConfig": {
32
+ "access": "public",
33
+ "registry": "https://registry.npmjs.org/"
34
+ },
35
+ "sideEffects": false,
36
+ "peerDependencies": {
37
+ "@react-native-async-storage/async-storage": ">=1.17.0",
38
+ "react": ">=18.0.0",
39
+ "react-native": ">=0.72.0",
40
+ "react-native-vector-icons": ">=10.0.0",
41
+ "react-native-video": ">=6.0.0"
42
+ },
43
+ "peerDependenciesMeta": {
44
+ "react-native-video": {
45
+ "optional": true
46
+ },
47
+ "react-native-webrtc": {
48
+ "optional": true
49
+ }
50
+ },
51
+ "dependencies": {
52
+ "@bytexbyte/nxtlinq-ai-agent-core-development": "^0.2.0",
53
+ "@bytexbyte/nxtlinq-ai-agent-react-native-development": "^0.2.0"
54
+ },
55
+ "devDependencies": {
56
+ "@bytexbyte/nxtlinq-ai-agent-core-development": "workspace:^",
57
+ "@bytexbyte/nxtlinq-ai-agent-react-native-development": "workspace:^",
58
+ "@types/react": "^18.2.64",
59
+ "@types/react-native-vector-icons": "^6.4.18",
60
+ "react": "^18.2.0",
61
+ "react-native-vector-icons": "^10.2.0",
62
+ "typescript": "^5.4.2"
63
+ }
64
+ }
@@ -0,0 +1,103 @@
1
+ import React from 'react';
2
+ import { NxtlinqAgentProvider } from '@bytexbyte/nxtlinq-ai-agent-react-native-development';
3
+ import { AgentAssistantShell } from './components/AgentAssistantShell';
4
+ import type { NxtlinqAgentAssistantProps } from './types';
5
+
6
+ /**
7
+ * Drop-in React Native assistant UI wired to `@bytexbyte/nxtlinq-ai-agent-react-native-development`.
8
+ *
9
+ * @example
10
+ * ```tsx
11
+ * import AsyncStorage from '@react-native-async-storage/async-storage';
12
+ * import { RTCPeerConnection, mediaDevices } from 'react-native-webrtc';
13
+ * import { NxtlinqAgentAssistant } from '@bytexbyte/nxtlinq-ai-agent-ui-react-native-development';
14
+ *
15
+ * export default function Screen() {
16
+ * return (
17
+ * <NxtlinqAgentAssistant
18
+ * storage={AsyncStorage}
19
+ * webrtcModule={{ RTCPeerConnection, mediaDevices }}
20
+ * serviceId="..."
21
+ * apiKey="..."
22
+ * apiSecret="..."
23
+ * pseudoId={chatId}
24
+ * enableVoice
25
+ * loadHistoryOnMount
26
+ * />
27
+ * );
28
+ * }
29
+ * ```
30
+ */
31
+ export function NxtlinqAgentAssistant({
32
+ title,
33
+ placeholder,
34
+ presetMessages,
35
+ loadHistoryOnMount = false,
36
+ historyLast,
37
+ enableVoice = true,
38
+ startInVoiceMode = false,
39
+ startWithMicMuted = true,
40
+ holdMicDuringAssistant = true,
41
+ showVoiceWaveform = true,
42
+ showVoiceImageInput = false,
43
+ voiceDemoProductImageUrl,
44
+ voiceAutoGreet,
45
+ iosSilentAudioSource,
46
+ voiceRemoteAudioGain,
47
+ textTtsVolume,
48
+ theme,
49
+ style,
50
+ headerStyle,
51
+ onMessage,
52
+ onError,
53
+ onToolUse,
54
+ children,
55
+ storage,
56
+ fetchImpl,
57
+ getTimezone,
58
+ webrtcModule,
59
+ webrtc,
60
+ resetOnIdentityChange,
61
+ ...agentConfig
62
+ }: NxtlinqAgentAssistantProps): React.ReactElement {
63
+ const webrtcEnabled = Boolean(webrtcModule || webrtc);
64
+
65
+ return (
66
+ <NxtlinqAgentProvider
67
+ storage={storage}
68
+ fetchImpl={fetchImpl}
69
+ getTimezone={getTimezone}
70
+ webrtcModule={webrtcModule}
71
+ webrtc={webrtc}
72
+ resetOnIdentityChange={resetOnIdentityChange}
73
+ onMessage={onMessage}
74
+ onError={onError}
75
+ onToolUse={onToolUse}
76
+ {...agentConfig}
77
+ >
78
+ <AgentAssistantShell
79
+ title={title}
80
+ placeholder={placeholder}
81
+ presetMessages={presetMessages}
82
+ loadHistoryOnMount={loadHistoryOnMount}
83
+ historyLast={historyLast}
84
+ enableVoice={enableVoice}
85
+ startInVoiceMode={startInVoiceMode}
86
+ startWithMicMuted={startWithMicMuted}
87
+ holdMicDuringAssistant={holdMicDuringAssistant}
88
+ showVoiceWaveform={showVoiceWaveform}
89
+ showVoiceImageInput={showVoiceImageInput}
90
+ voiceDemoProductImageUrl={voiceDemoProductImageUrl}
91
+ voiceAutoGreet={voiceAutoGreet}
92
+ iosSilentAudioSource={iosSilentAudioSource}
93
+ voiceRemoteAudioGain={voiceRemoteAudioGain}
94
+ textTtsVolume={textTtsVolume}
95
+ theme={theme}
96
+ style={style}
97
+ headerStyle={headerStyle}
98
+ webrtcEnabled={webrtcEnabled}
99
+ />
100
+ {children}
101
+ </NxtlinqAgentProvider>
102
+ );
103
+ }
@@ -0,0 +1,167 @@
1
+ import React, { useEffect } from 'react';
2
+ import { StyleSheet, Text, View } from 'react-native';
3
+ import {
4
+ AgentAssistantProvider,
5
+ useAgentAssistant,
6
+ } from '../context/AgentAssistantContext';
7
+ import type { NxtlinqAgentAssistantProps } from '../types';
8
+ import { AgentComposer } from './AgentComposer';
9
+ import { AgentMessageList } from './AgentMessageList';
10
+ import { AgentRemoteAudio } from './AgentRemoteAudio';
11
+ import { AgentVoiceBar } from './AgentVoiceBar';
12
+ import { PresetMessageChips } from './PresetMessageChips';
13
+ import { VoiceGreetTrigger } from './VoiceGreetTrigger';
14
+ import { VoiceImageInput } from './VoiceImageInput';
15
+ import { VoiceWaveform } from './VoiceWaveform';
16
+ import { AudioSessionWaker } from '../voice/AudioSessionWaker';
17
+ import { VoiceAutoGreetBinder } from '../voice/VoiceAutoGreetBinder';
18
+
19
+ export type AgentAssistantShellProps = Pick<
20
+ NxtlinqAgentAssistantProps,
21
+ | 'title'
22
+ | 'placeholder'
23
+ | 'presetMessages'
24
+ | 'enableVoice'
25
+ | 'theme'
26
+ | 'style'
27
+ | 'headerStyle'
28
+ | 'loadHistoryOnMount'
29
+ | 'historyLast'
30
+ | 'startInVoiceMode'
31
+ | 'startWithMicMuted'
32
+ | 'holdMicDuringAssistant'
33
+ | 'showVoiceWaveform'
34
+ | 'showVoiceImageInput'
35
+ | 'voiceDemoProductImageUrl'
36
+ | 'voiceAutoGreet'
37
+ | 'iosSilentAudioSource'
38
+ | 'voiceRemoteAudioGain'
39
+ | 'textTtsVolume'
40
+ > & {
41
+ webrtcEnabled: boolean;
42
+ };
43
+
44
+ function AgentAssistantInner({
45
+ title,
46
+ headerStyle,
47
+ style,
48
+ loadHistoryOnMount,
49
+ historyLast,
50
+ startInVoiceMode,
51
+ startWithMicMuted,
52
+ showVoiceWaveform,
53
+ showVoiceImageInput,
54
+ voiceDemoProductImageUrl,
55
+ voiceAutoGreet,
56
+ iosSilentAudioSource,
57
+ voiceRemoteAudioGain,
58
+ }: AgentAssistantShellProps): React.ReactElement {
59
+ const {
60
+ theme,
61
+ loadHistory,
62
+ startVoice,
63
+ isVoiceAvailable,
64
+ } = useAgentAssistant();
65
+
66
+ const [historyReady, setHistoryReady] = React.useState(!loadHistoryOnMount);
67
+
68
+ useEffect(() => {
69
+ if (loadHistoryOnMount) {
70
+ void loadHistory({ last: historyLast ?? 50 }).finally(() => setHistoryReady(true));
71
+ }
72
+ }, [loadHistory, loadHistoryOnMount, historyLast]);
73
+
74
+ const autoGreetConfig = React.useMemo(() => {
75
+ if (!voiceAutoGreet) return null;
76
+ if (voiceAutoGreet === true) {
77
+ return {
78
+ productImageUrl: voiceDemoProductImageUrl,
79
+ skipUserMessage: true,
80
+ };
81
+ }
82
+ return {
83
+ ...voiceAutoGreet,
84
+ productImageUrl: voiceAutoGreet.productImageUrl ?? voiceDemoProductImageUrl,
85
+ skipUserMessage: voiceAutoGreet.skipUserMessage ?? true,
86
+ };
87
+ }, [voiceAutoGreet, voiceDemoProductImageUrl]);
88
+
89
+ const voiceAutoStartRef = React.useRef(false);
90
+ useEffect(() => {
91
+ if (!startInVoiceMode || !isVoiceAvailable || voiceAutoStartRef.current) {
92
+ return;
93
+ }
94
+ voiceAutoStartRef.current = true;
95
+ void startVoice();
96
+ }, [startInVoiceMode, isVoiceAvailable, startVoice]);
97
+
98
+ return (
99
+ <View style={[styles.root, { backgroundColor: theme.colors.background }, style]}>
100
+ {iosSilentAudioSource ? (
101
+ <AudioSessionWaker silentSource={iosSilentAudioSource} />
102
+ ) : null}
103
+ <View
104
+ style={[
105
+ styles.header,
106
+ { borderBottomColor: theme.colors.border, backgroundColor: theme.colors.surface },
107
+ headerStyle,
108
+ ]}
109
+ >
110
+ <Text
111
+ style={{
112
+ fontSize: theme.typography.titleSize,
113
+ fontWeight: '600',
114
+ color: theme.colors.assistantText,
115
+ }}
116
+ >
117
+ {title ?? 'AI Assistant'}
118
+ </Text>
119
+ </View>
120
+ <PresetMessageChips />
121
+ <AgentMessageList />
122
+ {showVoiceWaveform !== false ? <VoiceWaveform /> : null}
123
+ <AgentVoiceBar />
124
+ {showVoiceImageInput && autoGreetConfig ? (
125
+ <VoiceGreetTrigger config={autoGreetConfig} />
126
+ ) : null}
127
+ {showVoiceImageInput ? (
128
+ <VoiceImageInput demoImageUrl={voiceDemoProductImageUrl} />
129
+ ) : null}
130
+ <AgentComposer />
131
+ <AgentRemoteAudio remoteAudioGain={voiceRemoteAudioGain} />
132
+ {autoGreetConfig ? (
133
+ <VoiceAutoGreetBinder config={autoGreetConfig} historyReady={historyReady} />
134
+ ) : null}
135
+ </View>
136
+ );
137
+ }
138
+
139
+ export function AgentAssistantShell(props: AgentAssistantShellProps): React.ReactElement {
140
+ return (
141
+ <AgentAssistantProvider
142
+ ui={{
143
+ title: props.title,
144
+ placeholder: props.placeholder,
145
+ presetMessages: props.presetMessages,
146
+ enableVoice: props.enableVoice,
147
+ theme: props.theme,
148
+ startWithMicMuted: props.startWithMicMuted,
149
+ holdMicDuringAssistant: props.holdMicDuringAssistant,
150
+ textTtsVolume: props.textTtsVolume,
151
+ voiceRemoteAudioGain: props.voiceRemoteAudioGain,
152
+ webrtcEnabled: props.webrtcEnabled,
153
+ }}
154
+ >
155
+ <AgentAssistantInner {...props} />
156
+ </AgentAssistantProvider>
157
+ );
158
+ }
159
+
160
+ const styles = StyleSheet.create({
161
+ root: { flex: 1, minHeight: 0 },
162
+ header: {
163
+ paddingHorizontal: 16,
164
+ paddingVertical: 12,
165
+ borderBottomWidth: StyleSheet.hairlineWidth,
166
+ },
167
+ });
@@ -0,0 +1,117 @@
1
+ import React from 'react';
2
+ import {
3
+ ActivityIndicator,
4
+ Pressable,
5
+ StyleSheet,
6
+ Text,
7
+ TextInput,
8
+ View,
9
+ } from 'react-native';
10
+ import { useAgentAssistant } from '../context/AgentAssistantContext';
11
+
12
+ export function AgentComposer(): React.ReactElement {
13
+ const {
14
+ theme,
15
+ placeholder,
16
+ inputText,
17
+ setInputText,
18
+ sendText,
19
+ isLoading,
20
+ isVoiceAvailable,
21
+ interactionMode,
22
+ startVoice,
23
+ isVoiceConnecting,
24
+ } = useAgentAssistant();
25
+
26
+ if (interactionMode === 'voice') {
27
+ return <View />;
28
+ }
29
+
30
+ return (
31
+ <View
32
+ style={[
33
+ styles.container,
34
+ {
35
+ borderTopColor: theme.colors.border,
36
+ backgroundColor: theme.colors.surface,
37
+ padding: theme.spacing.md,
38
+ },
39
+ ]}
40
+ >
41
+ <View style={styles.row}>
42
+ <TextInput
43
+ style={[
44
+ styles.input,
45
+ {
46
+ borderColor: theme.colors.border,
47
+ borderRadius: theme.radius.panel,
48
+ fontSize: theme.typography.bodySize,
49
+ color: theme.colors.assistantText,
50
+ },
51
+ ]}
52
+ value={inputText}
53
+ onChangeText={setInputText}
54
+ placeholder={placeholder}
55
+ placeholderTextColor={theme.colors.mutedText}
56
+ multiline
57
+ maxLength={4000}
58
+ editable={!isLoading}
59
+ />
60
+ <Pressable
61
+ onPress={() => void sendText()}
62
+ disabled={isLoading || !inputText.trim()}
63
+ style={({ pressed }: { pressed: boolean }) => [
64
+ styles.sendButton,
65
+ {
66
+ backgroundColor: theme.colors.primary,
67
+ borderRadius: theme.radius.button,
68
+ opacity: pressed || isLoading || !inputText.trim() ? 0.5 : 1,
69
+ },
70
+ ]}
71
+ >
72
+ {isLoading ? (
73
+ <ActivityIndicator color={theme.colors.primaryText} size="small" />
74
+ ) : (
75
+ <Text style={{ color: theme.colors.primaryText, fontWeight: '600' }}>Send</Text>
76
+ )}
77
+ </Pressable>
78
+ </View>
79
+ {isVoiceAvailable ? (
80
+ <Pressable
81
+ onPress={() => {
82
+ startVoice().catch((err: unknown) => {
83
+ const message = err instanceof Error ? err.message : String(err);
84
+ console.warn('[nxtlinq] startVoice failed:', message);
85
+ });
86
+ }}
87
+ disabled={isVoiceConnecting}
88
+ style={styles.voiceLink}
89
+ >
90
+ <Text style={{ color: theme.colors.primary, fontSize: theme.typography.captionSize }}>
91
+ {isVoiceConnecting ? 'Connecting voice…' : 'Switch to voice mode'}
92
+ </Text>
93
+ </Pressable>
94
+ ) : null}
95
+ </View>
96
+ );
97
+ }
98
+
99
+ const styles = StyleSheet.create({
100
+ container: { borderTopWidth: StyleSheet.hairlineWidth },
101
+ row: { flexDirection: 'row', alignItems: 'flex-end', gap: 8 },
102
+ input: {
103
+ flex: 1,
104
+ borderWidth: 1,
105
+ paddingHorizontal: 12,
106
+ paddingVertical: 10,
107
+ maxHeight: 120,
108
+ },
109
+ sendButton: {
110
+ paddingHorizontal: 16,
111
+ paddingVertical: 12,
112
+ justifyContent: 'center',
113
+ alignItems: 'center',
114
+ minWidth: 64,
115
+ },
116
+ voiceLink: { marginTop: 8, alignSelf: 'center' },
117
+ });
@@ -0,0 +1,187 @@
1
+ import type { Message } from '@bytexbyte/nxtlinq-ai-agent-core-development';
2
+ import React, { useCallback, useEffect, useRef } from 'react';
3
+ import {
4
+ ActivityIndicator,
5
+ FlatList,
6
+ Pressable,
7
+ StyleSheet,
8
+ Text,
9
+ View,
10
+ } from 'react-native';
11
+ import { useAgentAssistant } from '../context/AgentAssistantContext';
12
+ import { SpeakerIcon } from './VoiceIcons';
13
+
14
+ function MessageBubble({ message }: { message: Message }): React.ReactElement {
15
+ const {
16
+ theme,
17
+ interactionMode,
18
+ playMessageTts,
19
+ playingMessageId,
20
+ isTextTtsAvailable,
21
+ } = useAgentAssistant();
22
+ const isUser = message.role === 'user';
23
+ const displayText =
24
+ message.partialContent && message.isStreaming
25
+ ? message.partialContent
26
+ : message.content;
27
+ const [ttsBusy, setTtsBusy] = React.useState(false);
28
+ const [ttsError, setTtsError] = React.useState<string | null>(null);
29
+ const requestRef = useRef(0);
30
+
31
+ const playTts = useCallback(async () => {
32
+ const text = (displayText ?? '').trim();
33
+ if (!text || ttsBusy) return;
34
+ if (!isTextTtsAvailable) {
35
+ setTtsError('Install react-native-video for TTS playback');
36
+ return;
37
+ }
38
+ const requestId = ++requestRef.current;
39
+ setTtsBusy(true);
40
+ setTtsError(null);
41
+ try {
42
+ await playMessageTts(message.id, text);
43
+ } catch (e) {
44
+ if (requestRef.current === requestId) {
45
+ setTtsError(e instanceof Error ? e.message : String(e));
46
+ }
47
+ } finally {
48
+ if (requestRef.current === requestId) {
49
+ setTtsBusy(false);
50
+ }
51
+ }
52
+ }, [displayText, ttsBusy, isTextTtsAvailable, playMessageTts, message.id]);
53
+
54
+ const showTts =
55
+ !isUser &&
56
+ Boolean((displayText ?? '').trim()) &&
57
+ !message.isStreaming;
58
+
59
+ const isPlaying = playingMessageId === message.id;
60
+
61
+ return (
62
+ <View
63
+ style={[
64
+ styles.bubbleRow,
65
+ isUser ? styles.bubbleRowUser : styles.bubbleRowAssistant,
66
+ ]}
67
+ >
68
+ <View
69
+ style={[
70
+ styles.bubble,
71
+ {
72
+ backgroundColor: isUser
73
+ ? theme.colors.userBubble
74
+ : theme.colors.assistantBubble,
75
+ borderRadius: theme.radius.bubble,
76
+ maxWidth: '85%',
77
+ },
78
+ ]}
79
+ >
80
+ <Text
81
+ style={{
82
+ color: isUser ? theme.colors.userText : theme.colors.assistantText,
83
+ fontSize: theme.typography.bodySize,
84
+ }}
85
+ >
86
+ {displayText || ' '}
87
+ </Text>
88
+ {message.isStreaming && message.streamingStatus ? (
89
+ <Text
90
+ style={{
91
+ color: theme.colors.mutedText,
92
+ fontSize: theme.typography.captionSize,
93
+ marginTop: theme.spacing.xs,
94
+ }}
95
+ >
96
+ {message.streamingStatus}
97
+ </Text>
98
+ ) : null}
99
+ {message.error ? (
100
+ <Text style={{ color: theme.colors.error, fontSize: theme.typography.captionSize }}>
101
+ {message.error}
102
+ </Text>
103
+ ) : null}
104
+ {showTts ? (
105
+ <View style={styles.ttsRow}>
106
+ <Pressable
107
+ onPress={() => void playTts()}
108
+ disabled={ttsBusy}
109
+ style={({ pressed }) => [
110
+ styles.ttsBtn,
111
+ { opacity: pressed || ttsBusy ? 0.5 : 1 },
112
+ ]}
113
+ >
114
+ {ttsBusy ? (
115
+ <ActivityIndicator size="small" color={theme.colors.primary} />
116
+ ) : (
117
+ <SpeakerIcon
118
+ size={22}
119
+ color={isPlaying ? theme.colors.voiceSpeaking : theme.colors.primary}
120
+ />
121
+ )}
122
+ </Pressable>
123
+ {ttsError ? (
124
+ <Text
125
+ style={{
126
+ color: theme.colors.error,
127
+ fontSize: theme.typography.captionSize - 1,
128
+ flex: 1,
129
+ }}
130
+ numberOfLines={2}
131
+ >
132
+ {ttsError}
133
+ </Text>
134
+ ) : null}
135
+ </View>
136
+ ) : null}
137
+ </View>
138
+ </View>
139
+ );
140
+ }
141
+
142
+ export function AgentMessageList(): React.ReactElement {
143
+ const { messages, isLoading, theme } = useAgentAssistant();
144
+ 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]);
151
+
152
+ return (
153
+ <View style={[styles.container, { backgroundColor: theme.colors.background }]}>
154
+ <FlatList
155
+ ref={listRef}
156
+ data={messages}
157
+ keyExtractor={(item: Message) => item.id}
158
+ renderItem={({ item }: { item: Message }) => <MessageBubble message={item} />}
159
+ contentContainerStyle={{ padding: theme.spacing.md, paddingBottom: theme.spacing.lg }}
160
+ ListEmptyComponent={
161
+ <Text style={[styles.empty, { color: theme.colors.mutedText }]}>
162
+ Send a message to start the conversation.
163
+ </Text>
164
+ }
165
+ ListFooterComponent={
166
+ isLoading ? (
167
+ <ActivityIndicator
168
+ style={{ marginTop: theme.spacing.sm }}
169
+ color={theme.colors.primary}
170
+ />
171
+ ) : null
172
+ }
173
+ />
174
+ </View>
175
+ );
176
+ }
177
+
178
+ const styles = StyleSheet.create({
179
+ container: { flex: 1, minHeight: 0 },
180
+ bubbleRow: { marginBottom: 10 },
181
+ bubbleRowUser: { alignItems: 'flex-end' },
182
+ bubbleRowAssistant: { alignItems: 'flex-start' },
183
+ bubble: { paddingHorizontal: 14, paddingVertical: 10 },
184
+ ttsRow: { flexDirection: 'row', alignItems: 'center', gap: 8, marginTop: 6 },
185
+ ttsBtn: { paddingVertical: 2 },
186
+ empty: { textAlign: 'center', marginTop: 40, fontSize: 15 },
187
+ });