@bytexbyte/nxtlinq-ai-agent-ui-react-development 0.1.1

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 (142) hide show
  1. package/dist/NxtlinqAgentChat.d.ts +26 -0
  2. package/dist/NxtlinqAgentChat.d.ts.map +1 -0
  3. package/dist/NxtlinqAgentChat.js +28 -0
  4. package/dist/components/AgentAssistantShell.d.ts +5 -0
  5. package/dist/components/AgentAssistantShell.d.ts.map +1 -0
  6. package/dist/components/AgentAssistantShell.js +52 -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 +60 -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 +37 -0
  13. package/dist/components/AgentRemoteAudio.d.ts +4 -0
  14. package/dist/components/AgentRemoteAudio.d.ts.map +1 -0
  15. package/dist/components/AgentRemoteAudio.js +34 -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 +91 -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 +23 -0
  22. package/dist/context/AgentAssistantContext.d.ts +32 -0
  23. package/dist/context/AgentAssistantContext.d.ts.map +1 -0
  24. package/dist/context/AgentAssistantContext.js +159 -0
  25. package/dist/index.d.ts +16 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +12 -0
  28. package/dist/legacy/assets/images/adiSideItalicDataUri.d.ts +2 -0
  29. package/dist/legacy/assets/images/adiSideItalicDataUri.d.ts.map +1 -0
  30. package/dist/legacy/assets/images/adiSideItalicDataUri.js +1 -0
  31. package/dist/legacy/chatbot/ChatBot.d.ts +5 -0
  32. package/dist/legacy/chatbot/ChatBot.d.ts.map +1 -0
  33. package/dist/legacy/chatbot/ChatBot.js +35 -0
  34. package/dist/legacy/chatbot/context/ChatBotContext.d.ts +5 -0
  35. package/dist/legacy/chatbot/context/ChatBotContext.d.ts.map +1 -0
  36. package/dist/legacy/chatbot/context/ChatBotContext.js +2908 -0
  37. package/dist/legacy/chatbot/types/ChatBotTypes.d.ts +166 -0
  38. package/dist/legacy/chatbot/types/ChatBotTypes.d.ts.map +1 -0
  39. package/dist/legacy/chatbot/types/ChatBotTypes.js +1 -0
  40. package/dist/legacy/chatbot/ui/BerifyMeModal.d.ts +17 -0
  41. package/dist/legacy/chatbot/ui/BerifyMeModal.d.ts.map +1 -0
  42. package/dist/legacy/chatbot/ui/BerifyMeModal.js +110 -0
  43. package/dist/legacy/chatbot/ui/ChatBotUI.d.ts +3 -0
  44. package/dist/legacy/chatbot/ui/ChatBotUI.d.ts.map +1 -0
  45. package/dist/legacy/chatbot/ui/ChatBotUI.js +625 -0
  46. package/dist/legacy/chatbot/ui/MessageInput.d.ts +3 -0
  47. package/dist/legacy/chatbot/ui/MessageInput.d.ts.map +1 -0
  48. package/dist/legacy/chatbot/ui/MessageInput.js +321 -0
  49. package/dist/legacy/chatbot/ui/MessageList.d.ts +4 -0
  50. package/dist/legacy/chatbot/ui/MessageList.d.ts.map +1 -0
  51. package/dist/legacy/chatbot/ui/MessageList.js +455 -0
  52. package/dist/legacy/chatbot/ui/ModelSelector.d.ts +4 -0
  53. package/dist/legacy/chatbot/ui/ModelSelector.d.ts.map +1 -0
  54. package/dist/legacy/chatbot/ui/ModelSelector.js +122 -0
  55. package/dist/legacy/chatbot/ui/NotificationModal.d.ts +15 -0
  56. package/dist/legacy/chatbot/ui/NotificationModal.d.ts.map +1 -0
  57. package/dist/legacy/chatbot/ui/NotificationModal.js +53 -0
  58. package/dist/legacy/chatbot/ui/PermissionForm.d.ts +8 -0
  59. package/dist/legacy/chatbot/ui/PermissionForm.d.ts.map +1 -0
  60. package/dist/legacy/chatbot/ui/PermissionForm.js +465 -0
  61. package/dist/legacy/chatbot/ui/PresetMessages.d.ts +4 -0
  62. package/dist/legacy/chatbot/ui/PresetMessages.d.ts.map +1 -0
  63. package/dist/legacy/chatbot/ui/PresetMessages.js +33 -0
  64. package/dist/legacy/chatbot/ui/VoiceModePanel.d.ts +3 -0
  65. package/dist/legacy/chatbot/ui/VoiceModePanel.d.ts.map +1 -0
  66. package/dist/legacy/chatbot/ui/VoiceModePanel.js +95 -0
  67. package/dist/legacy/chatbot/ui/styles/isolatedStyles.d.ts +73 -0
  68. package/dist/legacy/chatbot/ui/styles/isolatedStyles.d.ts.map +1 -0
  69. package/dist/legacy/chatbot/ui/styles/isolatedStyles.js +985 -0
  70. package/dist/legacy/index.d.ts +14 -0
  71. package/dist/legacy/index.d.ts.map +1 -0
  72. package/dist/legacy/index.js +12 -0
  73. package/dist/theme/defaultTheme.d.ts +3 -0
  74. package/dist/theme/defaultTheme.d.ts.map +1 -0
  75. package/dist/theme/defaultTheme.js +20 -0
  76. package/dist/types.d.ts +62 -0
  77. package/dist/types.d.ts.map +1 -0
  78. package/dist/types.js +1 -0
  79. package/dist/voice/useVoiceConnectOrchestration.d.ts +21 -0
  80. package/dist/voice/useVoiceConnectOrchestration.d.ts.map +1 -0
  81. package/dist/voice/useVoiceConnectOrchestration.js +86 -0
  82. package/dist/voice/useVoiceMicState.d.ts +15 -0
  83. package/dist/voice/useVoiceMicState.d.ts.map +1 -0
  84. package/dist/voice/useVoiceMicState.js +94 -0
  85. package/dist/voice/useVoiceSilenceCommit.d.ts +10 -0
  86. package/dist/voice/useVoiceSilenceCommit.d.ts.map +1 -0
  87. package/dist/voice/useVoiceSilenceCommit.js +67 -0
  88. package/dist/voice/useVoiceTranscriptMessages.d.ts +16 -0
  89. package/dist/voice/useVoiceTranscriptMessages.d.ts.map +1 -0
  90. package/dist/voice/useVoiceTranscriptMessages.js +129 -0
  91. package/dist/voice/useWsRealtimeAudio.d.ts +18 -0
  92. package/dist/voice/useWsRealtimeAudio.d.ts.map +1 -0
  93. package/dist/voice/useWsRealtimeAudio.js +102 -0
  94. package/dist/voice/voiceMicConstants.d.ts +4 -0
  95. package/dist/voice/voiceMicConstants.d.ts.map +1 -0
  96. package/dist/voice/voiceMicConstants.js +10 -0
  97. package/dist/voice/ws/BrowserWsPcmPlayer.d.ts +23 -0
  98. package/dist/voice/ws/BrowserWsPcmPlayer.d.ts.map +1 -0
  99. package/dist/voice/ws/BrowserWsPcmPlayer.js +137 -0
  100. package/dist/voice/ws/BrowserWsPcmRecorder.d.ts +17 -0
  101. package/dist/voice/ws/BrowserWsPcmRecorder.d.ts.map +1 -0
  102. package/dist/voice/ws/BrowserWsPcmRecorder.js +71 -0
  103. package/dist/voice/ws/float32ToPcm16.d.ts +2 -0
  104. package/dist/voice/ws/float32ToPcm16.d.ts.map +1 -0
  105. package/dist/voice/ws/float32ToPcm16.js +8 -0
  106. package/dist/voice/ws/voiceSilenceConstants.d.ts +5 -0
  107. package/dist/voice/ws/voiceSilenceConstants.d.ts.map +1 -0
  108. package/dist/voice/ws/voiceSilenceConstants.js +4 -0
  109. package/dist/voice/ws/wsRealtimeConstants.d.ts +2 -0
  110. package/dist/voice/ws/wsRealtimeConstants.d.ts.map +1 -0
  111. package/dist/voice/ws/wsRealtimeConstants.js +1 -0
  112. package/package.json +60 -0
  113. package/src/NxtlinqAgentChat.tsx +79 -0
  114. package/src/components/AgentAssistantShell.tsx +104 -0
  115. package/src/components/AgentComposer.tsx +134 -0
  116. package/src/components/AgentMessageList.tsx +78 -0
  117. package/src/components/AgentRemoteAudio.tsx +34 -0
  118. package/src/components/AgentVoiceBar.tsx +173 -0
  119. package/src/components/PresetMessageChips.tsx +41 -0
  120. package/src/context/AgentAssistantContext.tsx +276 -0
  121. package/src/index.ts +78 -0
  122. package/src/legacy/assets/images/adiSideItalicDataUri.ts +1 -0
  123. package/src/legacy/chatbot/ChatBot.tsx +61 -0
  124. package/src/legacy/chatbot/context/ChatBotContext.tsx +3227 -0
  125. package/src/legacy/chatbot/types/ChatBotTypes.ts +195 -0
  126. package/src/legacy/chatbot/ui/BerifyMeModal.tsx +145 -0
  127. package/src/legacy/chatbot/ui/ChatBotUI.tsx +949 -0
  128. package/src/legacy/chatbot/ui/MessageInput.tsx +517 -0
  129. package/src/legacy/chatbot/ui/MessageList.tsx +764 -0
  130. package/src/legacy/chatbot/ui/ModelSelector.tsx +190 -0
  131. package/src/legacy/chatbot/ui/NotificationModal.tsx +110 -0
  132. package/src/legacy/chatbot/ui/PermissionForm.tsx +632 -0
  133. package/src/legacy/chatbot/ui/PresetMessages.tsx +50 -0
  134. package/src/legacy/chatbot/ui/VoiceModePanel.tsx +168 -0
  135. package/src/legacy/chatbot/ui/styles/isolatedStyles.ts +1058 -0
  136. package/src/legacy/index.ts +26 -0
  137. package/src/theme/defaultTheme.ts +22 -0
  138. package/src/types.ts +65 -0
  139. package/src/voice/useVoiceConnectOrchestration.ts +117 -0
  140. package/src/voice/useVoiceMicState.ts +117 -0
  141. package/src/voice/useVoiceTranscriptMessages.ts +173 -0
  142. package/src/voice/voiceMicConstants.ts +13 -0
@@ -0,0 +1,26 @@
1
+ export { ChatBot } from './chatbot/ChatBot';
2
+ export { ChatBotProvider, useChatBot } from './chatbot/context/ChatBotContext';
3
+
4
+ export { ChatBotUI } from './chatbot/ui/ChatBotUI';
5
+ export { MessageInput } from './chatbot/ui/MessageInput';
6
+ export { MessageList } from './chatbot/ui/MessageList';
7
+ export { ModelSelector } from './chatbot/ui/ModelSelector';
8
+ export { NotificationModal } from './chatbot/ui/NotificationModal';
9
+ export { PermissionForm } from './chatbot/ui/PermissionForm';
10
+ export { PresetMessages } from './chatbot/ui/PresetMessages';
11
+ export { BerifyMeModal } from './chatbot/ui/BerifyMeModal';
12
+
13
+ export type {
14
+ AIModel,
15
+ AITMetadata,
16
+ ChatBotContextType,
17
+ ChatBotProps,
18
+ NovaError,
19
+ NovaResponse,
20
+ PresetMessage,
21
+ ToolCall,
22
+ ToolUse,
23
+ } from './chatbot/types/ChatBotTypes';
24
+
25
+ /** Alias for consumers migrating to the new package naming. */
26
+ export { ChatBot as NxtlinqChatBot } from './chatbot/ChatBot';
@@ -0,0 +1,22 @@
1
+ import type { AgentAssistantTheme } from '../types';
2
+
3
+ export const defaultAgentAssistantTheme: AgentAssistantTheme = {
4
+ colors: {
5
+ background: '#f8fafc',
6
+ surface: '#ffffff',
7
+ border: '#e2e8f0',
8
+ primary: '#2563eb',
9
+ primaryText: '#ffffff',
10
+ userBubble: '#2563eb',
11
+ userText: '#ffffff',
12
+ assistantBubble: '#f1f5f9',
13
+ assistantText: '#0f172a',
14
+ mutedText: '#64748b',
15
+ error: '#dc2626',
16
+ voiceActive: '#22c55e',
17
+ voiceSpeaking: '#ec4899',
18
+ },
19
+ spacing: { xs: 4, sm: 8, md: 16, lg: 24 },
20
+ radius: { bubble: 12, panel: 8, button: 8 },
21
+ typography: { titleSize: 17, bodySize: 15, captionSize: 13 },
22
+ };
package/src/types.ts ADDED
@@ -0,0 +1,65 @@
1
+ import type { Message, ToolUse as CoreToolUse } from '@bytexbyte/nxtlinq-ai-agent-core-development';
2
+ import type { NxtlinqAgentProviderProps } from '@bytexbyte/nxtlinq-ai-agent-web-development';
3
+ import type { CSSProperties } from 'react';
4
+
5
+ export type PresetMessage = {
6
+ text: string;
7
+ autoSend?: boolean;
8
+ };
9
+
10
+ export type AgentAssistantTheme = {
11
+ colors: {
12
+ background: string;
13
+ surface: string;
14
+ border: string;
15
+ primary: string;
16
+ primaryText: string;
17
+ userBubble: string;
18
+ userText: string;
19
+ assistantBubble: string;
20
+ assistantText: string;
21
+ mutedText: string;
22
+ error: string;
23
+ voiceActive: string;
24
+ voiceSpeaking: string;
25
+ };
26
+ spacing: {
27
+ xs: number;
28
+ sm: number;
29
+ md: number;
30
+ lg: number;
31
+ };
32
+ radius: {
33
+ bubble: number;
34
+ panel: number;
35
+ button: number;
36
+ };
37
+ typography: {
38
+ titleSize: number;
39
+ bodySize: number;
40
+ captionSize: number;
41
+ };
42
+ };
43
+
44
+ export type NxtlinqAgentChatProps = Omit<NxtlinqAgentProviderProps, 'children'> & {
45
+ children?: NxtlinqAgentProviderProps['children'];
46
+ title?: string;
47
+ placeholder?: string;
48
+ presetMessages?: PresetMessage[];
49
+ loadHistoryOnMount?: boolean;
50
+ historyLast?: number;
51
+ enableVoice?: boolean;
52
+ enableFileUpload?: boolean;
53
+ startInVoiceMode?: boolean;
54
+ startWithMicMuted?: boolean;
55
+ holdMicDuringAssistant?: boolean;
56
+ theme?: Partial<AgentAssistantTheme>;
57
+ style?: CSSProperties;
58
+ headerStyle?: CSSProperties;
59
+ onMessage?: NxtlinqAgentProviderProps['onMessage'];
60
+ onError?: NxtlinqAgentProviderProps['onError'];
61
+ onToolUse?: NxtlinqAgentProviderProps['onToolUse'];
62
+ };
63
+
64
+ export type ToolUse = CoreToolUse;
65
+ export type { Message };
@@ -0,0 +1,117 @@
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
+ }
@@ -0,0 +1,117 @@
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
+ }
@@ -0,0 +1,173 @@
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 type { InteractionMode } from '../context/AgentAssistantContext';
9
+
10
+ type VoiceTranscriptAgentApi = {
11
+ getMessages: () => Message[];
12
+ setMessages: (messages: Message[]) => void;
13
+ syncVoiceTurnHistory: (options?: { last?: number }) => Promise<void>;
14
+ };
15
+
16
+ const STREAM_PREFIX = 'voice-stream-';
17
+
18
+ function voiceMeta(sessionId: string | null) {
19
+ return {
20
+ voiceRealtime: true as const,
21
+ voiceSessionId: sessionId ?? undefined,
22
+ };
23
+ }
24
+
25
+ export function useVoiceTranscriptMessages(
26
+ api: VoiceTranscriptAgentApi,
27
+ interactionMode: InteractionMode,
28
+ voiceSessionId: string | null,
29
+ ) {
30
+ const streamIdRef = useRef<string | null>(null);
31
+ const sessionIdRef = useRef(voiceSessionId);
32
+ sessionIdRef.current = voiceSessionId;
33
+
34
+ const isVoiceUiActive = useCallback(
35
+ () => interactionMode === 'voice' && sessionIdRef.current != null,
36
+ [interactionMode],
37
+ );
38
+
39
+ const upsertStreaming = useCallback(
40
+ (text: string) => {
41
+ const messages = api.getMessages();
42
+ let streamId = streamIdRef.current;
43
+ if (!streamId) {
44
+ streamId = `${STREAM_PREFIX}${Date.now()}`;
45
+ streamIdRef.current = streamId;
46
+ }
47
+ const idx = messages.findIndex((m) => m.id === streamId);
48
+ const partialContent =
49
+ idx >= 0
50
+ ? mergeStreamingTranscript(messages[idx]?.partialContent ?? '', text)
51
+ : text;
52
+ const meta = voiceMeta(sessionIdRef.current);
53
+ if (idx >= 0) {
54
+ api.setMessages(
55
+ messages.map((m, i) =>
56
+ i === idx
57
+ ? { ...m, partialContent, isStreaming: true, metadata: { ...m.metadata, ...meta } }
58
+ : m,
59
+ ),
60
+ );
61
+ return;
62
+ }
63
+ api.setMessages([
64
+ ...messages,
65
+ {
66
+ id: streamId,
67
+ role: 'assistant',
68
+ content: '',
69
+ partialContent,
70
+ isStreaming: true,
71
+ timestamp: new Date().toISOString(),
72
+ metadata: meta,
73
+ },
74
+ ]);
75
+ },
76
+ [api],
77
+ );
78
+
79
+ const finalizeAssistant = useCallback(
80
+ (text: string, messageId?: string | null) => {
81
+ const trimmed = text.trim();
82
+ streamIdRef.current = null;
83
+ if (!trimmed) return;
84
+
85
+ const messages = api.getMessages();
86
+ const streamIdx = messages.findIndex((m) => m.isStreaming && m.role === 'assistant');
87
+ if (streamIdx >= 0) {
88
+ api.setMessages(
89
+ messages.map((m, i) =>
90
+ i === streamIdx
91
+ ? {
92
+ ...m,
93
+ id: messageId ?? m.id,
94
+ content: trimmed,
95
+ partialContent: undefined,
96
+ isStreaming: false,
97
+ metadata: { ...m.metadata, ...voiceMeta(sessionIdRef.current) },
98
+ }
99
+ : m,
100
+ ),
101
+ );
102
+ return;
103
+ }
104
+ const last = messages[messages.length - 1];
105
+ if (last?.role === 'assistant' && last.content === trimmed) return;
106
+ api.setMessages([
107
+ ...messages,
108
+ {
109
+ id: messageId ?? `voice-asst-${Date.now()}`,
110
+ role: 'assistant',
111
+ content: trimmed,
112
+ timestamp: new Date().toISOString(),
113
+ metadata: voiceMeta(sessionIdRef.current),
114
+ },
115
+ ]);
116
+ },
117
+ [api],
118
+ );
119
+
120
+ const handleTranscript = useCallback(
121
+ (event: VoiceTranscriptEvent) => {
122
+ if (!isVoiceUiActive()) return;
123
+ const text = event.text?.trim() ?? '';
124
+ if (event.role === 'assistant') {
125
+ // Keep one streaming bubble for the whole turn; finalize only in handleDone.
126
+ if (text) upsertStreaming(text);
127
+ return;
128
+ }
129
+ if (event.role === 'user' && !event.interim && text) {
130
+ const messages = api.getMessages();
131
+ const last = messages[messages.length - 1];
132
+ if (last?.role === 'user' && last.content === text) return;
133
+ api.setMessages([
134
+ ...messages,
135
+ {
136
+ id: `voice-user-${Date.now()}`,
137
+ role: 'user',
138
+ content: text,
139
+ timestamp: new Date().toISOString(),
140
+ metadata: voiceMeta(sessionIdRef.current),
141
+ },
142
+ ]);
143
+ }
144
+ },
145
+ [api, finalizeAssistant, isVoiceUiActive, upsertStreaming],
146
+ );
147
+
148
+ const handleDone = useCallback(
149
+ (event: VoiceDoneEvent) => {
150
+ if (!isVoiceUiActive()) return;
151
+ if (event.guardrailsBlocked || event.billingBlocked || event.error) {
152
+ streamIdRef.current = null;
153
+ return;
154
+ }
155
+ const reply = event.replyText?.trim() ?? '';
156
+ if (reply) {
157
+ finalizeAssistant(reply, event.assistantMessageId ?? undefined);
158
+ } else {
159
+ streamIdRef.current = null;
160
+ }
161
+ void api.syncVoiceTurnHistory({ last: 20 }).catch((err) => {
162
+ console.warn('[nxtlinq] syncVoiceTurnHistory after voice turn failed', err);
163
+ });
164
+ },
165
+ [api, finalizeAssistant, isVoiceUiActive],
166
+ );
167
+
168
+ const clearVoiceStream = useCallback(() => {
169
+ streamIdRef.current = null;
170
+ }, []);
171
+
172
+ return { handleTranscript, handleDone, clearVoiceStream };
173
+ }
@@ -0,0 +1,13 @@
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
+ ]);