@bytexbyte/nxtlinq-ai-agent-ui-react-development 0.1.2 → 0.1.4
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/ChatBot.d.ts +5 -0
- package/dist/ChatBot.d.ts.map +1 -0
- package/dist/ChatBot.js +35 -0
- package/dist/assets/images/adiSideItalicDataUri.d.ts +2 -0
- package/dist/assets/images/adiSideItalicDataUri.d.ts.map +1 -0
- package/dist/assets/images/adiSideItalicDataUri.js +1 -0
- package/dist/context/AgentAssistantContext.d.ts.map +1 -1
- package/dist/context/AgentAssistantContext.js +18 -0
- package/dist/context/ChatBotContext.d.ts +5 -0
- package/dist/context/ChatBotContext.d.ts.map +1 -0
- package/dist/context/ChatBotContext.js +2908 -0
- package/dist/index.d.ts +5 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -11
- package/dist/legacy/chatbot/context/ChatBotContext.d.ts.map +1 -1
- package/dist/legacy/chatbot/context/ChatBotContext.js +14 -0
- package/dist/types/ChatBotTypes.d.ts +166 -0
- package/dist/types/ChatBotTypes.d.ts.map +1 -0
- package/dist/types/ChatBotTypes.js +1 -0
- package/dist/ui/BerifyMeModal.d.ts +17 -0
- package/dist/ui/BerifyMeModal.d.ts.map +1 -0
- package/dist/ui/BerifyMeModal.js +110 -0
- package/dist/ui/ChatBotHeader.d.ts +15 -0
- package/dist/ui/ChatBotHeader.d.ts.map +1 -0
- package/dist/ui/ChatBotHeader.js +62 -0
- package/dist/ui/ChatBotUI.d.ts +3 -0
- package/dist/ui/ChatBotUI.d.ts.map +1 -0
- package/dist/ui/ChatBotUI.js +557 -0
- package/dist/ui/MessageInput.d.ts +3 -0
- package/dist/ui/MessageInput.d.ts.map +1 -0
- package/dist/ui/MessageInput.js +321 -0
- package/dist/ui/MessageList.d.ts +4 -0
- package/dist/ui/MessageList.d.ts.map +1 -0
- package/dist/ui/MessageList.js +455 -0
- package/dist/ui/ModelSelector.d.ts +4 -0
- package/dist/ui/ModelSelector.d.ts.map +1 -0
- package/dist/ui/ModelSelector.js +122 -0
- package/dist/ui/NotificationModal.d.ts +15 -0
- package/dist/ui/NotificationModal.d.ts.map +1 -0
- package/dist/ui/NotificationModal.js +53 -0
- package/dist/ui/PermissionForm.d.ts +8 -0
- package/dist/ui/PermissionForm.d.ts.map +1 -0
- package/dist/ui/PermissionForm.js +465 -0
- package/dist/ui/PresetMessages.d.ts +4 -0
- package/dist/ui/PresetMessages.d.ts.map +1 -0
- package/dist/ui/PresetMessages.js +33 -0
- package/dist/ui/VoiceModePanel.d.ts +3 -0
- package/dist/ui/VoiceModePanel.d.ts.map +1 -0
- package/dist/ui/VoiceModePanel.js +95 -0
- package/dist/ui/chatBotHeaderParts.d.ts +15 -0
- package/dist/ui/chatBotHeaderParts.d.ts.map +1 -0
- package/dist/ui/chatBotHeaderParts.js +50 -0
- package/dist/ui/index.d.ts +9 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +8 -0
- package/dist/ui/styles/isolatedStyles.d.ts +73 -0
- package/dist/ui/styles/isolatedStyles.d.ts.map +1 -0
- package/dist/ui/styles/isolatedStyles.js +985 -0
- package/package.json +3 -3
- package/src/{legacy/chatbot/context → context}/ChatBotContext.tsx +0 -1
- package/src/index.ts +17 -40
- package/src/{legacy/chatbot/ui → ui}/ModelSelector.tsx +1 -1
- package/src/{legacy/chatbot/ui → ui}/VoiceModePanel.tsx +1 -1
- package/src/ui/index.ts +8 -0
- package/src/NxtlinqAgentChat.tsx +0 -79
- package/src/components/AgentAssistantShell.tsx +0 -104
- package/src/components/AgentComposer.tsx +0 -134
- package/src/components/AgentMessageList.tsx +0 -78
- package/src/components/AgentRemoteAudio.tsx +0 -34
- package/src/components/AgentVoiceBar.tsx +0 -173
- package/src/components/PresetMessageChips.tsx +0 -41
- package/src/context/AgentAssistantContext.tsx +0 -278
- package/src/legacy/index.ts +0 -26
- package/src/theme/defaultTheme.ts +0 -22
- package/src/types.ts +0 -65
- package/src/voice/useVoiceConnectOrchestration.ts +0 -117
- package/src/voice/useVoiceMicState.ts +0 -117
- package/src/voice/useVoiceTranscriptMessages.ts +0 -188
- package/src/voice/voiceMicConstants.ts +0 -13
- package/src/voice/voiceUserBubble.ts +0 -71
- /package/src/{legacy/chatbot/ChatBot.tsx → ChatBot.tsx} +0 -0
- /package/src/{legacy/assets → assets}/images/adiSideItalicDataUri.ts +0 -0
- /package/src/{legacy/chatbot/types → types}/ChatBotTypes.ts +0 -0
- /package/src/{legacy/chatbot/ui → ui}/BerifyMeModal.tsx +0 -0
- /package/src/{legacy/chatbot/ui → ui}/ChatBotHeader.tsx +0 -0
- /package/src/{legacy/chatbot/ui → ui}/ChatBotUI.tsx +0 -0
- /package/src/{legacy/chatbot/ui → ui}/MessageInput.tsx +0 -0
- /package/src/{legacy/chatbot/ui → ui}/MessageList.tsx +0 -0
- /package/src/{legacy/chatbot/ui → ui}/NotificationModal.tsx +0 -0
- /package/src/{legacy/chatbot/ui → ui}/PermissionForm.tsx +0 -0
- /package/src/{legacy/chatbot/ui → ui}/PresetMessages.tsx +0 -0
- /package/src/{legacy/chatbot/ui → ui}/chatBotHeaderParts.tsx +0 -0
- /package/src/{legacy/chatbot/ui → ui}/styles/isolatedStyles.ts +0 -0
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import type { VoiceDoneEvent, VoiceTranscriptEvent } from '@bytexbyte/nxtlinq-ai-agent-core-development';
|
|
2
|
-
import type {
|
|
3
|
-
UseNxtlinqAgentResult,
|
|
4
|
-
UseNxtlinqVoiceResult,
|
|
5
|
-
} from '@bytexbyte/nxtlinq-ai-agent-web-development';
|
|
6
|
-
import { useCallback, useRef } from 'react';
|
|
7
|
-
import type { InteractionMode } from '../context/AgentAssistantContext';
|
|
8
|
-
|
|
9
|
-
type VoiceCallbacks = {
|
|
10
|
-
handleTranscript: (event: VoiceTranscriptEvent) => void;
|
|
11
|
-
handleDone: (event: VoiceDoneEvent) => void;
|
|
12
|
-
clearVoiceStream: () => void;
|
|
13
|
-
prepareForVoiceConnect: () => void;
|
|
14
|
-
resetMicState: () => void;
|
|
15
|
-
clearAssistantMicHold: () => void;
|
|
16
|
-
setInteractionMode: (mode: InteractionMode) => void;
|
|
17
|
-
setIsVoiceConnecting: (connecting: boolean) => void;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
export function useVoiceConnectOrchestration(
|
|
21
|
-
agent: UseNxtlinqAgentResult,
|
|
22
|
-
voice: UseNxtlinqVoiceResult,
|
|
23
|
-
interactionMode: InteractionMode,
|
|
24
|
-
micStartsMuted: boolean,
|
|
25
|
-
callbacks: VoiceCallbacks,
|
|
26
|
-
) {
|
|
27
|
-
const voiceConnectChainRef = useRef<Promise<unknown>>(Promise.resolve());
|
|
28
|
-
|
|
29
|
-
const wrappedStartVoice = useCallback(
|
|
30
|
-
(options?: Parameters<typeof voice.startVoice>[0]) => {
|
|
31
|
-
const connect = async () => {
|
|
32
|
-
if (voice.voiceSessionId != null) {
|
|
33
|
-
await voice.stopVoice('restart_voice');
|
|
34
|
-
}
|
|
35
|
-
callbacks.prepareForVoiceConnect();
|
|
36
|
-
callbacks.setInteractionMode('voice');
|
|
37
|
-
callbacks.setIsVoiceConnecting(true);
|
|
38
|
-
try {
|
|
39
|
-
const session = await voice.startVoice({
|
|
40
|
-
startWithMicMuted: micStartsMuted,
|
|
41
|
-
keepMicCaptureActive: true,
|
|
42
|
-
...options,
|
|
43
|
-
onOpen: () => {
|
|
44
|
-
if (!micStartsMuted) {
|
|
45
|
-
voice.muteMic(false);
|
|
46
|
-
}
|
|
47
|
-
options?.onOpen?.();
|
|
48
|
-
},
|
|
49
|
-
onClose: (reason) => {
|
|
50
|
-
callbacks.clearVoiceStream();
|
|
51
|
-
callbacks.resetMicState();
|
|
52
|
-
const userInitiated =
|
|
53
|
-
reason === 'switch_to_text' ||
|
|
54
|
-
reason === 'client_stop' ||
|
|
55
|
-
reason === 'mode_text_cleanup';
|
|
56
|
-
if (userInitiated) {
|
|
57
|
-
callbacks.setInteractionMode('text');
|
|
58
|
-
} else {
|
|
59
|
-
console.warn('[nxtlinq] voice session closed:', reason);
|
|
60
|
-
}
|
|
61
|
-
options?.onClose?.(reason);
|
|
62
|
-
},
|
|
63
|
-
onError: (err) => {
|
|
64
|
-
callbacks.clearVoiceStream();
|
|
65
|
-
callbacks.resetMicState();
|
|
66
|
-
console.warn('[nxtlinq] voice session error:', err.message);
|
|
67
|
-
options?.onError?.(err);
|
|
68
|
-
},
|
|
69
|
-
onTranscript: (event) => {
|
|
70
|
-
callbacks.handleTranscript(event);
|
|
71
|
-
options?.onTranscript?.(event);
|
|
72
|
-
},
|
|
73
|
-
onDone: (event) => {
|
|
74
|
-
callbacks.handleDone(event);
|
|
75
|
-
options?.onDone?.(event);
|
|
76
|
-
},
|
|
77
|
-
});
|
|
78
|
-
return session;
|
|
79
|
-
} catch (err) {
|
|
80
|
-
callbacks.setInteractionMode('text');
|
|
81
|
-
callbacks.resetMicState();
|
|
82
|
-
throw err;
|
|
83
|
-
} finally {
|
|
84
|
-
callbacks.setIsVoiceConnecting(false);
|
|
85
|
-
}
|
|
86
|
-
};
|
|
87
|
-
const next = voiceConnectChainRef.current.then(connect, connect);
|
|
88
|
-
voiceConnectChainRef.current = next.catch(() => undefined);
|
|
89
|
-
return next;
|
|
90
|
-
},
|
|
91
|
-
[voice, micStartsMuted, callbacks],
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
const wrappedStopVoice = useCallback(async () => {
|
|
95
|
-
await voice.stopVoice('client_stop');
|
|
96
|
-
callbacks.resetMicState();
|
|
97
|
-
callbacks.setInteractionMode('text');
|
|
98
|
-
}, [voice, callbacks]);
|
|
99
|
-
|
|
100
|
-
const wrappedInterrupt = useCallback(() => {
|
|
101
|
-
voice.interrupt();
|
|
102
|
-
callbacks.clearAssistantMicHold();
|
|
103
|
-
}, [voice, callbacks]);
|
|
104
|
-
|
|
105
|
-
const isVoiceChannelReady =
|
|
106
|
-
interactionMode === 'voice' &&
|
|
107
|
-
voice.voiceSessionId != null &&
|
|
108
|
-
(Boolean(agent.agent.getVoiceSession()?.isAppChannelOpen()) ||
|
|
109
|
-
voice.voiceStatus === 'listening');
|
|
110
|
-
|
|
111
|
-
return {
|
|
112
|
-
wrappedStartVoice,
|
|
113
|
-
wrappedStopVoice,
|
|
114
|
-
wrappedInterrupt,
|
|
115
|
-
isVoiceChannelReady,
|
|
116
|
-
};
|
|
117
|
-
}
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import type { VoiceStatus } from '@bytexbyte/nxtlinq-ai-agent-core-development';
|
|
2
|
-
import type { UseNxtlinqVoiceResult } from '@bytexbyte/nxtlinq-ai-agent-web-development';
|
|
3
|
-
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
4
|
-
import { ASSISTANT_MIC_HOLD_STATUSES } from './voiceMicConstants';
|
|
5
|
-
|
|
6
|
-
export type UseVoiceMicStateOptions = {
|
|
7
|
-
startWithMicMuted?: boolean;
|
|
8
|
-
holdMicDuringAssistant?: boolean;
|
|
9
|
-
onBargeIn?: () => void;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
export function useVoiceMicState(
|
|
13
|
-
voice: UseNxtlinqVoiceResult,
|
|
14
|
-
isVoiceConnecting: boolean,
|
|
15
|
-
options?: UseVoiceMicStateOptions,
|
|
16
|
-
) {
|
|
17
|
-
const connectMuted = options?.startWithMicMuted !== false;
|
|
18
|
-
const holdDuringAssistant = options?.holdMicDuringAssistant !== false;
|
|
19
|
-
const userMicMutedRef = useRef(connectMuted);
|
|
20
|
-
const assistantMicHoldRef = useRef(false);
|
|
21
|
-
const userMicOptInRef = useRef(!connectMuted);
|
|
22
|
-
const [isMicMuted, setIsMicMuted] = useState(connectMuted);
|
|
23
|
-
|
|
24
|
-
const applyMicState = useCallback(() => {
|
|
25
|
-
const shouldMute = userMicMutedRef.current || assistantMicHoldRef.current;
|
|
26
|
-
voice.muteMic(shouldMute);
|
|
27
|
-
setIsMicMuted(shouldMute);
|
|
28
|
-
}, [voice]);
|
|
29
|
-
|
|
30
|
-
const resetMicState = useCallback(() => {
|
|
31
|
-
userMicMutedRef.current = false;
|
|
32
|
-
assistantMicHoldRef.current = false;
|
|
33
|
-
userMicOptInRef.current = false;
|
|
34
|
-
setIsMicMuted(false);
|
|
35
|
-
}, []);
|
|
36
|
-
|
|
37
|
-
const prepareForVoiceConnect = useCallback(() => {
|
|
38
|
-
userMicMutedRef.current = connectMuted;
|
|
39
|
-
userMicOptInRef.current = !connectMuted;
|
|
40
|
-
assistantMicHoldRef.current = false;
|
|
41
|
-
setIsMicMuted(connectMuted);
|
|
42
|
-
voice.muteMic(connectMuted);
|
|
43
|
-
}, [voice, connectMuted]);
|
|
44
|
-
|
|
45
|
-
useEffect(() => {
|
|
46
|
-
if (!isVoiceConnecting) return;
|
|
47
|
-
userMicMutedRef.current = connectMuted;
|
|
48
|
-
voice.muteMic(connectMuted);
|
|
49
|
-
setIsMicMuted(connectMuted);
|
|
50
|
-
}, [isVoiceConnecting, voice, connectMuted]);
|
|
51
|
-
|
|
52
|
-
const prevVoiceStatusRef = useRef(voice.voiceStatus);
|
|
53
|
-
|
|
54
|
-
useEffect(() => {
|
|
55
|
-
const status = voice.voiceStatus;
|
|
56
|
-
const prev = prevVoiceStatusRef.current;
|
|
57
|
-
prevVoiceStatusRef.current = status;
|
|
58
|
-
|
|
59
|
-
if (holdDuringAssistant && ASSISTANT_MIC_HOLD_STATUSES.has(status)) {
|
|
60
|
-
if (!userMicMutedRef.current && userMicOptInRef.current) {
|
|
61
|
-
assistantMicHoldRef.current = false;
|
|
62
|
-
applyMicState();
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
assistantMicHoldRef.current = true;
|
|
66
|
-
applyMicState();
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
if (status === 'listening' || status === 'idle') {
|
|
70
|
-
assistantMicHoldRef.current = false;
|
|
71
|
-
if (
|
|
72
|
-
connectMuted
|
|
73
|
-
&& status === 'listening'
|
|
74
|
-
&& prev === 'speaking'
|
|
75
|
-
&& !userMicOptInRef.current
|
|
76
|
-
) {
|
|
77
|
-
userMicMutedRef.current = true;
|
|
78
|
-
}
|
|
79
|
-
applyMicState();
|
|
80
|
-
}
|
|
81
|
-
}, [voice.voiceStatus, applyMicState, holdDuringAssistant, connectMuted]);
|
|
82
|
-
|
|
83
|
-
const toggleVoiceMicMute = useCallback(() => {
|
|
84
|
-
if (!voice.isVoiceActive && !isVoiceConnecting) return;
|
|
85
|
-
if (assistantMicHoldRef.current && userMicMutedRef.current) {
|
|
86
|
-
assistantMicHoldRef.current = false;
|
|
87
|
-
userMicMutedRef.current = false;
|
|
88
|
-
userMicOptInRef.current = true;
|
|
89
|
-
options?.onBargeIn?.();
|
|
90
|
-
applyMicState();
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
if (assistantMicHoldRef.current) {
|
|
94
|
-
userMicMutedRef.current = true;
|
|
95
|
-
applyMicState();
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
const nextMuted = !userMicMutedRef.current;
|
|
99
|
-
userMicMutedRef.current = nextMuted;
|
|
100
|
-
userMicOptInRef.current = !nextMuted;
|
|
101
|
-
applyMicState();
|
|
102
|
-
}, [voice.isVoiceActive, isVoiceConnecting, applyMicState, options]);
|
|
103
|
-
|
|
104
|
-
const clearAssistantMicHold = useCallback(() => {
|
|
105
|
-
assistantMicHoldRef.current = false;
|
|
106
|
-
applyMicState();
|
|
107
|
-
}, [applyMicState]);
|
|
108
|
-
|
|
109
|
-
return {
|
|
110
|
-
isMicMuted,
|
|
111
|
-
isMicHeldForAssistant: ASSISTANT_MIC_HOLD_STATUSES.has(voice.voiceStatus),
|
|
112
|
-
toggleVoiceMicMute,
|
|
113
|
-
prepareForVoiceConnect,
|
|
114
|
-
resetMicState,
|
|
115
|
-
clearAssistantMicHold,
|
|
116
|
-
};
|
|
117
|
-
}
|
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
Message,
|
|
3
|
-
VoiceDoneEvent,
|
|
4
|
-
VoiceTranscriptEvent,
|
|
5
|
-
} from '@bytexbyte/nxtlinq-ai-agent-core-development';
|
|
6
|
-
import { mergeStreamingTranscript } from '@bytexbyte/nxtlinq-ai-agent-core-development';
|
|
7
|
-
import { useCallback, useRef } from 'react';
|
|
8
|
-
import { ensureUserBubbleForVoiceTurn } from './voiceUserBubble';
|
|
9
|
-
|
|
10
|
-
type InteractionMode = 'text' | 'voice';
|
|
11
|
-
|
|
12
|
-
export type VoiceTranscriptAgentApi = {
|
|
13
|
-
getMessages: () => Message[];
|
|
14
|
-
updateMessages: (updater: (prev: Message[]) => Message[]) => void;
|
|
15
|
-
syncVoiceTurnHistory: (options?: { last?: number }) => Promise<void>;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
const STREAM_PREFIX = 'voice-stream-';
|
|
19
|
-
|
|
20
|
-
function voiceMeta(sessionId: string | null) {
|
|
21
|
-
return {
|
|
22
|
-
voiceRealtime: true as const,
|
|
23
|
-
voiceSessionId: sessionId ?? undefined,
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function useVoiceTranscriptMessages(
|
|
28
|
-
api: VoiceTranscriptAgentApi,
|
|
29
|
-
interactionMode: InteractionMode,
|
|
30
|
-
voiceSessionId: string | null,
|
|
31
|
-
getPendingUserText?: () => string,
|
|
32
|
-
) {
|
|
33
|
-
const streamIdRef = useRef<string | null>(null);
|
|
34
|
-
const sessionIdRef = useRef(voiceSessionId);
|
|
35
|
-
sessionIdRef.current = voiceSessionId;
|
|
36
|
-
|
|
37
|
-
const isVoiceUiActive = useCallback(
|
|
38
|
-
() => interactionMode === 'voice' && sessionIdRef.current != null,
|
|
39
|
-
[interactionMode],
|
|
40
|
-
);
|
|
41
|
-
|
|
42
|
-
const upsertStreaming = useCallback(
|
|
43
|
-
(text: string) => {
|
|
44
|
-
api.updateMessages((prev) => {
|
|
45
|
-
let streamId = streamIdRef.current;
|
|
46
|
-
if (!streamId) {
|
|
47
|
-
streamId = `${STREAM_PREFIX}${Date.now()}`;
|
|
48
|
-
streamIdRef.current = streamId;
|
|
49
|
-
}
|
|
50
|
-
const idx = prev.findIndex((m) => m.id === streamId);
|
|
51
|
-
const partialContent =
|
|
52
|
-
idx >= 0
|
|
53
|
-
? mergeStreamingTranscript(prev[idx]?.partialContent ?? '', text)
|
|
54
|
-
: text;
|
|
55
|
-
const meta = voiceMeta(sessionIdRef.current);
|
|
56
|
-
|
|
57
|
-
if (idx >= 0) {
|
|
58
|
-
return prev.map((m, i) =>
|
|
59
|
-
i === idx
|
|
60
|
-
? { ...m, partialContent, isStreaming: true, metadata: { ...m.metadata, ...meta } }
|
|
61
|
-
: m,
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const withUser = ensureUserBubbleForVoiceTurn(
|
|
66
|
-
prev,
|
|
67
|
-
getPendingUserText?.() ?? '',
|
|
68
|
-
undefined,
|
|
69
|
-
meta,
|
|
70
|
-
);
|
|
71
|
-
return [
|
|
72
|
-
...withUser,
|
|
73
|
-
{
|
|
74
|
-
id: streamId,
|
|
75
|
-
role: 'assistant' as const,
|
|
76
|
-
content: '',
|
|
77
|
-
partialContent,
|
|
78
|
-
isStreaming: true,
|
|
79
|
-
timestamp: new Date().toISOString(),
|
|
80
|
-
metadata: meta,
|
|
81
|
-
},
|
|
82
|
-
];
|
|
83
|
-
});
|
|
84
|
-
},
|
|
85
|
-
[api, getPendingUserText],
|
|
86
|
-
);
|
|
87
|
-
|
|
88
|
-
const finalizeAssistant = useCallback(
|
|
89
|
-
(text: string, messageId?: string | null) => {
|
|
90
|
-
const trimmed = text.trim();
|
|
91
|
-
streamIdRef.current = null;
|
|
92
|
-
if (!trimmed) return;
|
|
93
|
-
|
|
94
|
-
api.updateMessages((prev) => {
|
|
95
|
-
const streamIdx = prev.findIndex((m) => m.isStreaming && m.role === 'assistant');
|
|
96
|
-
if (streamIdx >= 0) {
|
|
97
|
-
return prev.map((m, i) =>
|
|
98
|
-
i === streamIdx
|
|
99
|
-
? {
|
|
100
|
-
...m,
|
|
101
|
-
id: messageId ?? m.id,
|
|
102
|
-
content: trimmed,
|
|
103
|
-
partialContent: undefined,
|
|
104
|
-
isStreaming: false,
|
|
105
|
-
metadata: { ...m.metadata, ...voiceMeta(sessionIdRef.current) },
|
|
106
|
-
}
|
|
107
|
-
: m,
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
const last = prev[prev.length - 1];
|
|
111
|
-
if (last?.role === 'assistant' && last.content === trimmed) return prev;
|
|
112
|
-
return [
|
|
113
|
-
...prev,
|
|
114
|
-
{
|
|
115
|
-
id: messageId ?? `voice-asst-${Date.now()}`,
|
|
116
|
-
role: 'assistant' as const,
|
|
117
|
-
content: trimmed,
|
|
118
|
-
timestamp: new Date().toISOString(),
|
|
119
|
-
metadata: voiceMeta(sessionIdRef.current),
|
|
120
|
-
},
|
|
121
|
-
];
|
|
122
|
-
});
|
|
123
|
-
},
|
|
124
|
-
[api],
|
|
125
|
-
);
|
|
126
|
-
|
|
127
|
-
const handleTranscript = useCallback(
|
|
128
|
-
(event: VoiceTranscriptEvent) => {
|
|
129
|
-
if (!isVoiceUiActive()) return;
|
|
130
|
-
const text = event.text?.trim() ?? '';
|
|
131
|
-
if (event.role === 'assistant') {
|
|
132
|
-
if (text) upsertStreaming(text);
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
if (event.role === 'user' && !event.interim && text) {
|
|
136
|
-
api.updateMessages((prev) => {
|
|
137
|
-
const last = prev[prev.length - 1];
|
|
138
|
-
if (last?.role === 'user' && last.content === text) return prev;
|
|
139
|
-
return [
|
|
140
|
-
...prev,
|
|
141
|
-
{
|
|
142
|
-
id: `voice-user-${Date.now()}`,
|
|
143
|
-
role: 'user' as const,
|
|
144
|
-
content: text,
|
|
145
|
-
timestamp: new Date().toISOString(),
|
|
146
|
-
metadata: voiceMeta(sessionIdRef.current),
|
|
147
|
-
},
|
|
148
|
-
];
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
},
|
|
152
|
-
[api, isVoiceUiActive, upsertStreaming],
|
|
153
|
-
);
|
|
154
|
-
|
|
155
|
-
const handleDone = useCallback(
|
|
156
|
-
(event: VoiceDoneEvent, options?: { pendingUserText?: string }) => {
|
|
157
|
-
if (!isVoiceUiActive()) return;
|
|
158
|
-
if (event.guardrailsBlocked || event.billingBlocked || event.error) {
|
|
159
|
-
streamIdRef.current = null;
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
const reply = event.replyText?.trim() ?? '';
|
|
163
|
-
if (reply) {
|
|
164
|
-
api.updateMessages((prev) =>
|
|
165
|
-
ensureUserBubbleForVoiceTurn(
|
|
166
|
-
prev,
|
|
167
|
-
options?.pendingUserText ?? getPendingUserText?.() ?? '',
|
|
168
|
-
event.userMessageId,
|
|
169
|
-
voiceMeta(sessionIdRef.current),
|
|
170
|
-
),
|
|
171
|
-
);
|
|
172
|
-
finalizeAssistant(reply, event.assistantMessageId ?? undefined);
|
|
173
|
-
} else {
|
|
174
|
-
streamIdRef.current = null;
|
|
175
|
-
}
|
|
176
|
-
void api.syncVoiceTurnHistory({ last: 20 }).catch((err) => {
|
|
177
|
-
console.warn('[nxtlinq] syncVoiceTurnHistory after voice turn failed', err);
|
|
178
|
-
});
|
|
179
|
-
},
|
|
180
|
-
[api, finalizeAssistant, getPendingUserText, isVoiceUiActive],
|
|
181
|
-
);
|
|
182
|
-
|
|
183
|
-
const clearVoiceStream = useCallback(() => {
|
|
184
|
-
streamIdRef.current = null;
|
|
185
|
-
}, []);
|
|
186
|
-
|
|
187
|
-
return { handleTranscript, handleDone, clearVoiceStream };
|
|
188
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import type { VoiceStatus } from '@bytexbyte/nxtlinq-ai-agent-core-development';
|
|
2
|
-
|
|
3
|
-
export const ASSISTANT_MIC_HOLD_STATUSES: ReadonlySet<VoiceStatus> = new Set([
|
|
4
|
-
'transcribing',
|
|
5
|
-
'thinking',
|
|
6
|
-
'generating',
|
|
7
|
-
'speaking',
|
|
8
|
-
]);
|
|
9
|
-
|
|
10
|
-
export const SPEAKER_ACTIVE_STATUSES: ReadonlySet<VoiceStatus> = new Set([
|
|
11
|
-
'generating',
|
|
12
|
-
'speaking',
|
|
13
|
-
]);
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import type { Message } from '@bytexbyte/nxtlinq-ai-agent-core-development';
|
|
2
|
-
|
|
3
|
-
export const VOICE_USER_INPUT_PLACEHOLDER = '(Voice input)';
|
|
4
|
-
|
|
5
|
-
type VoiceMeta = {
|
|
6
|
-
voiceRealtime: true;
|
|
7
|
-
voiceSessionId?: string;
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
/** Ensure a user bubble exists before the in-flight assistant reply for this turn. */
|
|
11
|
-
export function ensureUserBubbleForVoiceTurn(
|
|
12
|
-
messages: Message[],
|
|
13
|
-
userText: string,
|
|
14
|
-
userMessageId: string | null | undefined,
|
|
15
|
-
metaForPlaceholder: VoiceMeta | undefined,
|
|
16
|
-
): Message[] {
|
|
17
|
-
const streamIdx = messages.findIndex((m) => m.isStreaming && m.role === 'assistant');
|
|
18
|
-
const insertAt = streamIdx >= 0 ? streamIdx : messages.length;
|
|
19
|
-
const before = messages.slice(0, insertAt);
|
|
20
|
-
|
|
21
|
-
const lastAsstIdx = (() => {
|
|
22
|
-
for (let i = before.length - 1; i >= 0; i -= 1) {
|
|
23
|
-
const m = before[i];
|
|
24
|
-
if (m.role === 'assistant' && !m.isStreaming && Boolean(m.content?.trim())) return i;
|
|
25
|
-
}
|
|
26
|
-
return -1;
|
|
27
|
-
})();
|
|
28
|
-
const lastUserIdx = (() => {
|
|
29
|
-
for (let i = before.length - 1; i >= 0; i -= 1) {
|
|
30
|
-
if (before[i].role === 'user') return i;
|
|
31
|
-
}
|
|
32
|
-
return -1;
|
|
33
|
-
})();
|
|
34
|
-
const hasUserForTurn = lastUserIdx >= 0 && lastUserIdx > lastAsstIdx;
|
|
35
|
-
|
|
36
|
-
const trimmed = userText.trim();
|
|
37
|
-
const displayText = trimmed || VOICE_USER_INPUT_PLACEHOLDER;
|
|
38
|
-
|
|
39
|
-
if (hasUserForTurn) {
|
|
40
|
-
const existing = before[lastUserIdx];
|
|
41
|
-
const shouldUpgrade =
|
|
42
|
-
trimmed &&
|
|
43
|
-
existing.content !== trimmed &&
|
|
44
|
-
(existing.content === VOICE_USER_INPUT_PLACEHOLDER || !existing.content.trim());
|
|
45
|
-
if (!shouldUpgrade) return messages;
|
|
46
|
-
|
|
47
|
-
const upgraded = before.map((m, i) =>
|
|
48
|
-
i === lastUserIdx
|
|
49
|
-
? {
|
|
50
|
-
...m,
|
|
51
|
-
content: trimmed,
|
|
52
|
-
id: userMessageId ?? m.id,
|
|
53
|
-
metadata: userMessageId ? undefined : m.metadata,
|
|
54
|
-
}
|
|
55
|
-
: m,
|
|
56
|
-
);
|
|
57
|
-
return [...upgraded, ...messages.slice(insertAt)];
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const userMsg: Message = {
|
|
61
|
-
id: userMessageId ?? `voice-user-${Date.now()}`,
|
|
62
|
-
role: 'user',
|
|
63
|
-
content: displayText,
|
|
64
|
-
timestamp: new Date().toISOString(),
|
|
65
|
-
metadata: userMessageId ? undefined : metaForPlaceholder,
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
const next = [...messages];
|
|
69
|
-
next.splice(insertAt, 0, userMsg);
|
|
70
|
-
return next;
|
|
71
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|