@bytexbyte/nxtlinq-ai-agent-ui-react-development 0.1.1 → 0.1.3
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/context/AgentAssistantContext.d.ts.map +1 -1
- package/dist/context/AgentAssistantContext.js +21 -1
- package/dist/legacy/chatbot/ui/ChatBotHeader.d.ts +15 -0
- package/dist/legacy/chatbot/ui/ChatBotHeader.d.ts.map +1 -0
- package/dist/legacy/chatbot/ui/ChatBotHeader.js +62 -0
- package/dist/legacy/chatbot/ui/ChatBotUI.d.ts.map +1 -1
- package/dist/legacy/chatbot/ui/ChatBotUI.js +3 -71
- package/dist/legacy/chatbot/ui/chatBotHeaderParts.d.ts +15 -0
- package/dist/legacy/chatbot/ui/chatBotHeaderParts.d.ts.map +1 -0
- package/dist/legacy/chatbot/ui/chatBotHeaderParts.js +50 -0
- package/dist/voice/useVoiceTranscriptMessages.d.ts +7 -5
- package/dist/voice/useVoiceTranscriptMessages.d.ts.map +1 -1
- package/dist/voice/useVoiceTranscriptMessages.js +79 -76
- package/dist/voice/voiceUserBubble.d.ts +10 -0
- package/dist/voice/voiceUserBubble.d.ts.map +1 -0
- package/dist/voice/voiceUserBubble.js +52 -0
- package/package.json +3 -3
- package/src/context/AgentAssistantContext.tsx +20 -2
- package/src/legacy/chatbot/ui/ChatBotHeader.tsx +143 -0
- package/src/legacy/chatbot/ui/ChatBotUI.tsx +13 -144
- package/src/legacy/chatbot/ui/chatBotHeaderParts.tsx +115 -0
- package/src/voice/useVoiceTranscriptMessages.ts +87 -72
- package/src/voice/voiceUserBubble.ts +71 -0
|
@@ -5,11 +5,13 @@ import type {
|
|
|
5
5
|
} from '@bytexbyte/nxtlinq-ai-agent-core-development';
|
|
6
6
|
import { mergeStreamingTranscript } from '@bytexbyte/nxtlinq-ai-agent-core-development';
|
|
7
7
|
import { useCallback, useRef } from 'react';
|
|
8
|
-
import
|
|
8
|
+
import { ensureUserBubbleForVoiceTurn } from './voiceUserBubble';
|
|
9
9
|
|
|
10
|
-
type
|
|
10
|
+
type InteractionMode = 'text' | 'voice';
|
|
11
|
+
|
|
12
|
+
export type VoiceTranscriptAgentApi = {
|
|
11
13
|
getMessages: () => Message[];
|
|
12
|
-
|
|
14
|
+
updateMessages: (updater: (prev: Message[]) => Message[]) => void;
|
|
13
15
|
syncVoiceTurnHistory: (options?: { last?: number }) => Promise<void>;
|
|
14
16
|
};
|
|
15
17
|
|
|
@@ -26,6 +28,7 @@ export function useVoiceTranscriptMessages(
|
|
|
26
28
|
api: VoiceTranscriptAgentApi,
|
|
27
29
|
interactionMode: InteractionMode,
|
|
28
30
|
voiceSessionId: string | null,
|
|
31
|
+
getPendingUserText?: () => string,
|
|
29
32
|
) {
|
|
30
33
|
const streamIdRef = useRef<string | null>(null);
|
|
31
34
|
const sessionIdRef = useRef(voiceSessionId);
|
|
@@ -38,42 +41,48 @@ export function useVoiceTranscriptMessages(
|
|
|
38
41
|
|
|
39
42
|
const upsertStreaming = useCallback(
|
|
40
43
|
(text: string) => {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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) =>
|
|
56
59
|
i === idx
|
|
57
60
|
? { ...m, partialContent, isStreaming: true, metadata: { ...m.metadata, ...meta } }
|
|
58
61
|
: m,
|
|
59
|
-
)
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const withUser = ensureUserBubbleForVoiceTurn(
|
|
66
|
+
prev,
|
|
67
|
+
getPendingUserText?.() ?? '',
|
|
68
|
+
undefined,
|
|
69
|
+
meta,
|
|
60
70
|
);
|
|
61
|
-
return
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
]);
|
|
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
|
+
});
|
|
75
84
|
},
|
|
76
|
-
[api],
|
|
85
|
+
[api, getPendingUserText],
|
|
77
86
|
);
|
|
78
87
|
|
|
79
88
|
const finalizeAssistant = useCallback(
|
|
@@ -82,11 +91,10 @@ export function useVoiceTranscriptMessages(
|
|
|
82
91
|
streamIdRef.current = null;
|
|
83
92
|
if (!trimmed) return;
|
|
84
93
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
messages.map((m, i) =>
|
|
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) =>
|
|
90
98
|
i === streamIdx
|
|
91
99
|
? {
|
|
92
100
|
...m,
|
|
@@ -97,22 +105,21 @@ export function useVoiceTranscriptMessages(
|
|
|
97
105
|
metadata: { ...m.metadata, ...voiceMeta(sessionIdRef.current) },
|
|
98
106
|
}
|
|
99
107
|
: m,
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
]);
|
|
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
|
+
});
|
|
116
123
|
},
|
|
117
124
|
[api],
|
|
118
125
|
);
|
|
@@ -122,31 +129,31 @@ export function useVoiceTranscriptMessages(
|
|
|
122
129
|
if (!isVoiceUiActive()) return;
|
|
123
130
|
const text = event.text?.trim() ?? '';
|
|
124
131
|
if (event.role === 'assistant') {
|
|
125
|
-
// Keep one streaming bubble for the whole turn; finalize only in handleDone.
|
|
126
132
|
if (text) upsertStreaming(text);
|
|
127
133
|
return;
|
|
128
134
|
}
|
|
129
135
|
if (event.role === 'user' && !event.interim && text) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
+
});
|
|
143
150
|
}
|
|
144
151
|
},
|
|
145
|
-
[api,
|
|
152
|
+
[api, isVoiceUiActive, upsertStreaming],
|
|
146
153
|
);
|
|
147
154
|
|
|
148
155
|
const handleDone = useCallback(
|
|
149
|
-
(event: VoiceDoneEvent) => {
|
|
156
|
+
(event: VoiceDoneEvent, options?: { pendingUserText?: string }) => {
|
|
150
157
|
if (!isVoiceUiActive()) return;
|
|
151
158
|
if (event.guardrailsBlocked || event.billingBlocked || event.error) {
|
|
152
159
|
streamIdRef.current = null;
|
|
@@ -154,6 +161,14 @@ export function useVoiceTranscriptMessages(
|
|
|
154
161
|
}
|
|
155
162
|
const reply = event.replyText?.trim() ?? '';
|
|
156
163
|
if (reply) {
|
|
164
|
+
api.updateMessages((prev) =>
|
|
165
|
+
ensureUserBubbleForVoiceTurn(
|
|
166
|
+
prev,
|
|
167
|
+
options?.pendingUserText ?? getPendingUserText?.() ?? '',
|
|
168
|
+
event.userMessageId,
|
|
169
|
+
voiceMeta(sessionIdRef.current),
|
|
170
|
+
),
|
|
171
|
+
);
|
|
157
172
|
finalizeAssistant(reply, event.assistantMessageId ?? undefined);
|
|
158
173
|
} else {
|
|
159
174
|
streamIdRef.current = null;
|
|
@@ -162,7 +177,7 @@ export function useVoiceTranscriptMessages(
|
|
|
162
177
|
console.warn('[nxtlinq] syncVoiceTurnHistory after voice turn failed', err);
|
|
163
178
|
});
|
|
164
179
|
},
|
|
165
|
-
[api, finalizeAssistant, isVoiceUiActive],
|
|
180
|
+
[api, finalizeAssistant, getPendingUserText, isVoiceUiActive],
|
|
166
181
|
);
|
|
167
182
|
|
|
168
183
|
const clearVoiceStream = useCallback(() => {
|
|
@@ -0,0 +1,71 @@
|
|
|
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
|
+
}
|