@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.
- package/dist/NxtlinqAgentAssistant.d.ts +29 -0
- package/dist/NxtlinqAgentAssistant.d.ts.map +1 -0
- package/dist/NxtlinqAgentAssistant.js +32 -0
- package/dist/components/AgentAssistantShell.d.ts +7 -0
- package/dist/components/AgentAssistantShell.d.ts.map +1 -0
- package/dist/components/AgentAssistantShell.js +77 -0
- package/dist/components/AgentComposer.d.ts +3 -0
- package/dist/components/AgentComposer.d.ts.map +1 -0
- package/dist/components/AgentComposer.js +56 -0
- package/dist/components/AgentMessageList.d.ts +3 -0
- package/dist/components/AgentMessageList.d.ts.map +1 -0
- package/dist/components/AgentMessageList.js +91 -0
- package/dist/components/AgentRemoteAudio.d.ts +14 -0
- package/dist/components/AgentRemoteAudio.d.ts.map +1 -0
- package/dist/components/AgentRemoteAudio.js +62 -0
- package/dist/components/AgentVoiceBar.d.ts +3 -0
- package/dist/components/AgentVoiceBar.d.ts.map +1 -0
- package/dist/components/AgentVoiceBar.js +133 -0
- package/dist/components/PresetMessageChips.d.ts +3 -0
- package/dist/components/PresetMessageChips.d.ts.map +1 -0
- package/dist/components/PresetMessageChips.js +39 -0
- package/dist/components/VoiceGreetTrigger.d.ts +10 -0
- package/dist/components/VoiceGreetTrigger.d.ts.map +1 -0
- package/dist/components/VoiceGreetTrigger.js +99 -0
- package/dist/components/VoiceIcons.d.ts +12 -0
- package/dist/components/VoiceIcons.d.ts.map +1 -0
- package/dist/components/VoiceIcons.js +17 -0
- package/dist/components/VoiceImageInput.d.ts +10 -0
- package/dist/components/VoiceImageInput.d.ts.map +1 -0
- package/dist/components/VoiceImageInput.js +100 -0
- package/dist/components/VoiceWaveform.d.ts +7 -0
- package/dist/components/VoiceWaveform.d.ts.map +1 -0
- package/dist/components/VoiceWaveform.js +64 -0
- package/dist/context/AgentAssistantContext.d.ts +45 -0
- package/dist/context/AgentAssistantContext.d.ts.map +1 -0
- package/dist/context/AgentAssistantContext.js +244 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/theme/defaultTheme.d.ts +3 -0
- package/dist/theme/defaultTheme.d.ts.map +1 -0
- package/dist/theme/defaultTheme.js +33 -0
- package/dist/types.d.ts +103 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/voice/AudioSessionWaker.d.ts +18 -0
- package/dist/voice/AudioSessionWaker.d.ts.map +1 -0
- package/dist/voice/AudioSessionWaker.js +49 -0
- package/dist/voice/TextTtsPlayer.d.ts +21 -0
- package/dist/voice/TextTtsPlayer.d.ts.map +1 -0
- package/dist/voice/TextTtsPlayer.js +91 -0
- package/dist/voice/VoiceAutoGreetBinder.d.ts +6 -0
- package/dist/voice/VoiceAutoGreetBinder.d.ts.map +1 -0
- package/dist/voice/VoiceAutoGreetBinder.js +25 -0
- package/dist/voice/useVoiceAutoGreet.d.ts +24 -0
- package/dist/voice/useVoiceAutoGreet.d.ts.map +1 -0
- package/dist/voice/useVoiceAutoGreet.js +64 -0
- package/dist/voice/useVoiceMicState.d.ts +24 -0
- package/dist/voice/useVoiceMicState.d.ts.map +1 -0
- package/dist/voice/useVoiceMicState.js +84 -0
- package/dist/voice/voiceMicConstants.d.ts +5 -0
- package/dist/voice/voiceMicConstants.d.ts.map +1 -0
- package/dist/voice/voiceMicConstants.js +11 -0
- package/dist/voice/voiceWaveformConstants.d.ts +6 -0
- package/dist/voice/voiceWaveformConstants.d.ts.map +1 -0
- package/dist/voice/voiceWaveformConstants.js +7 -0
- package/dist/voice/webrtcAudioGain.d.ts +6 -0
- package/dist/voice/webrtcAudioGain.d.ts.map +1 -0
- package/dist/voice/webrtcAudioGain.js +11 -0
- package/dist/voice/writeTtsCacheFile.d.ts +9 -0
- package/dist/voice/writeTtsCacheFile.d.ts.map +1 -0
- package/dist/voice/writeTtsCacheFile.js +37 -0
- package/package.json +64 -0
- package/src/NxtlinqAgentAssistant.tsx +103 -0
- package/src/components/AgentAssistantShell.tsx +167 -0
- package/src/components/AgentComposer.tsx +117 -0
- package/src/components/AgentMessageList.tsx +187 -0
- package/src/components/AgentRemoteAudio.tsx +105 -0
- package/src/components/AgentVoiceBar.tsx +232 -0
- package/src/components/PresetMessageChips.tsx +64 -0
- package/src/components/VoiceGreetTrigger.tsx +158 -0
- package/src/components/VoiceIcons.tsx +32 -0
- package/src/components/VoiceImageInput.tsx +178 -0
- package/src/components/VoiceWaveform.tsx +84 -0
- package/src/context/AgentAssistantContext.tsx +369 -0
- package/src/index.ts +59 -0
- package/src/react-native.d.ts +42 -0
- package/src/theme/defaultTheme.ts +35 -0
- package/src/types.ts +107 -0
- package/src/voice/AudioSessionWaker.tsx +94 -0
- package/src/voice/TextTtsPlayer.tsx +151 -0
- package/src/voice/VoiceAutoGreetBinder.tsx +38 -0
- package/src/voice/useVoiceAutoGreet.ts +95 -0
- package/src/voice/useVoiceMicState.ts +116 -0
- package/src/voice/voiceMicConstants.ts +14 -0
- package/src/voice/voiceWaveformConstants.ts +10 -0
- package/src/voice/webrtcAudioGain.ts +21 -0
- 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
|
+
});
|