@cognizant-ai-lab/ui-common 1.5.0 → 1.5.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.
- package/dist/Theme/Theme.js +3 -3
- package/dist/components/AgentChat/ChatCommon/AgentIntro.d.ts +12 -0
- package/dist/components/AgentChat/ChatCommon/AgentIntro.js +19 -0
- package/dist/components/AgentChat/ChatCommon/AgentMetadata.d.ts +14 -0
- package/dist/components/AgentChat/ChatCommon/AgentMetadata.js +43 -0
- package/dist/components/AgentChat/ChatCommon/ChatCommon.d.ts +2 -1
- package/dist/components/AgentChat/ChatCommon/ChatCommon.js +131 -214
- package/dist/components/AgentChat/ChatCommon/Const.d.ts +1 -0
- package/dist/components/AgentChat/ChatCommon/Const.js +2 -0
- package/dist/components/AgentChat/ChatCommon/ControlButtons.js +2 -2
- package/dist/components/AgentChat/ChatCommon/Conversation.d.ts +15 -0
- package/dist/components/AgentChat/ChatCommon/Conversation.js +102 -0
- package/dist/components/AgentChat/ChatCommon/ConversationTurn.d.ts +19 -0
- package/dist/components/AgentChat/ChatCommon/ConversationTurn.js +10 -0
- package/dist/components/AgentChat/ChatCommon/Greetings.d.ts +1 -1
- package/dist/components/AgentChat/ChatCommon/Greetings.js +1 -1
- package/dist/components/AgentChat/Common/LlmChatButton.d.ts +2 -2
- package/dist/components/AgentChat/Common/Utils.js +1 -2
- package/dist/components/Common/ConfirmationModal.d.ts +1 -0
- package/dist/components/Common/ConfirmationModal.js +1 -1
- package/dist/components/Common/CustomerLogo.js +1 -1
- package/dist/components/Common/LlmChatOptionsButton.d.ts +1 -1
- package/dist/components/Common/Navbar.d.ts +2 -1
- package/dist/components/Common/Navbar.js +8 -4
- package/dist/components/Common/notification.d.ts +1 -1
- package/dist/components/Common/notification.js +17 -12
- package/dist/components/MultiAgentAccelerator/AgentFlow.d.ts +7 -0
- package/dist/components/MultiAgentAccelerator/AgentFlow.js +178 -73
- package/dist/components/MultiAgentAccelerator/AgentNode.d.ts +2 -1
- package/dist/components/MultiAgentAccelerator/AgentNode.js +52 -17
- package/dist/components/MultiAgentAccelerator/AgentNodePopup.d.ts +1 -4
- package/dist/components/MultiAgentAccelerator/AgentNodePopup.js +4 -5
- package/dist/components/MultiAgentAccelerator/GraphLayouts.js +7 -5
- package/dist/components/MultiAgentAccelerator/MultiAgentAccelerator.d.ts +2 -2
- package/dist/components/MultiAgentAccelerator/MultiAgentAccelerator.js +239 -51
- package/dist/components/MultiAgentAccelerator/Sidebar/AgentNetworkTreeItem.d.ts +1 -0
- package/dist/components/MultiAgentAccelerator/Sidebar/AgentNetworkTreeItem.js +28 -12
- package/dist/components/MultiAgentAccelerator/Sidebar/Sidebar.d.ts +1 -0
- package/dist/components/MultiAgentAccelerator/Sidebar/Sidebar.js +21 -5
- package/dist/components/MultiAgentAccelerator/Sidebar/TreeBuilder.d.ts +4 -3
- package/dist/components/MultiAgentAccelerator/Sidebar/TreeBuilder.js +8 -2
- package/dist/components/MultiAgentAccelerator/TemporaryNetworks.d.ts +19 -2
- package/dist/components/MultiAgentAccelerator/TemporaryNetworks.js +40 -5
- package/dist/components/MultiAgentAccelerator/ThoughtBubbleOverlay.js +27 -14
- package/dist/components/MultiAgentAccelerator/Tour/MainTourSteps.d.ts +7 -0
- package/dist/components/MultiAgentAccelerator/Tour/MainTourSteps.js +88 -0
- package/dist/components/MultiAgentAccelerator/const.d.ts +7 -10
- package/dist/components/MultiAgentAccelerator/const.js +9 -10
- package/dist/const.d.ts +5 -1
- package/dist/const.js +5 -2
- package/dist/controller/agent/Agent.d.ts +10 -0
- package/dist/controller/agent/Agent.js +17 -1
- package/dist/controller/llm/LlmChat.js +2 -2
- package/dist/state/TemporaryNetworks.d.ts +5 -15
- package/dist/state/TemporaryNetworks.js +15 -34
- package/dist/state/Tour.d.ts +29 -0
- package/dist/state/Tour.js +22 -0
- package/dist/state/UserInfo.d.ts +2 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/dist/utils/Authentication.js +12 -3
- package/dist/utils/File.d.ts +7 -0
- package/dist/utils/File.js +14 -3
- package/dist/utils/text.js +2 -2
- package/dist/utils/title.js +1 -1
- package/dist/utils/zIndexLayers.js +3 -0
- package/package.json +14 -10
|
@@ -28,32 +28,25 @@ import CircularProgress from "@mui/material/CircularProgress";
|
|
|
28
28
|
import IconButton from "@mui/material/IconButton";
|
|
29
29
|
import Input from "@mui/material/Input";
|
|
30
30
|
import InputAdornment from "@mui/material/InputAdornment";
|
|
31
|
-
import {
|
|
31
|
+
import { useTheme } from "@mui/material/styles";
|
|
32
32
|
import Tooltip from "@mui/material/Tooltip";
|
|
33
33
|
import Typography from "@mui/material/Typography";
|
|
34
|
-
import {
|
|
35
|
-
import { isValidElement, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, } from "react";
|
|
36
|
-
import ReactMarkdown from "react-markdown";
|
|
37
|
-
import SyntaxHighlighter from "react-syntax-highlighter";
|
|
34
|
+
import { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, } from "react";
|
|
38
35
|
import { v4 as uuid } from "uuid";
|
|
39
|
-
import {
|
|
36
|
+
import { AgentIntro } from "./AgentIntro.js";
|
|
37
|
+
import { AgentMetadata } from "./AgentMetadata.js";
|
|
40
38
|
import { ChatHistory } from "./ChatHistory.js";
|
|
39
|
+
import { AGENT_IMAGE } from "./Const.js";
|
|
41
40
|
import { ControlButtons } from "./ControlButtons.js";
|
|
42
|
-
import {
|
|
43
|
-
import {
|
|
44
|
-
import { SampleQueries } from "./SampleQueries.js";
|
|
41
|
+
import { Conversation } from "./Conversation.js";
|
|
42
|
+
import { MessageRole } from "./ConversationTurn.js";
|
|
45
43
|
import { SendButton } from "./SendButton.js";
|
|
46
|
-
import {
|
|
47
|
-
import { UserQueryDisplay } from "./UserQueryDisplay.js";
|
|
48
|
-
import { getAgentFunction, getConnectivity, sendChatQuery } from "../../../controller/agent/Agent.js";
|
|
44
|
+
import { sendChatQuery } from "../../../controller/agent/Agent.js";
|
|
49
45
|
import { sendLlmRequest, StreamingUnit } from "../../../controller/llm/LlmChat.js";
|
|
50
|
-
import { ChatMessageType
|
|
46
|
+
import { ChatMessageType } from "../../../generated/neuro-san/NeuroSanClient.js";
|
|
51
47
|
import { useAgentChatHistoryStore } from "../../../state/ChatHistory.js";
|
|
52
|
-
import {
|
|
48
|
+
import { hasOnlyWhitespace } from "../../../utils/text.js";
|
|
53
49
|
import { LlmChatOptionsButton } from "../../Common/LlmChatOptionsButton.js";
|
|
54
|
-
import { MUIAccordion } from "../../Common/MUIAccordion.js";
|
|
55
|
-
import { MUIAlert } from "../../Common/MUIAlert.js";
|
|
56
|
-
import { NotificationType, sendNotification } from "../../Common/notification.js";
|
|
57
50
|
import { isLegacyAgentType } from "../Common/Types.js";
|
|
58
51
|
import { chatMessageFromChunk, checkError, cleanUpAgentName, removeTrailingUuid } from "../Common/Utils.js";
|
|
59
52
|
import { MicrophoneButton } from "../VoiceChat/MicrophoneButton.js";
|
|
@@ -62,8 +55,6 @@ import { cleanupAndStopSpeechRecognition, setupSpeechRecognition } from "../Voic
|
|
|
62
55
|
const CHAT_HISTORY_KEY = "chat-history-accordion";
|
|
63
56
|
// Define fancy EMPTY constant to avoid linter error about using object literals as default props
|
|
64
57
|
const EMPTY = {};
|
|
65
|
-
// Avatar to use for agents in chat
|
|
66
|
-
const AGENT_IMAGE = "/agent.svg";
|
|
67
58
|
// How many times to retry the entire agent interaction process. Some networks have a well-defined success condition.
|
|
68
59
|
// For others, it's just "whenever the stream is done".
|
|
69
60
|
const MAX_AGENT_RETRIES = 3;
|
|
@@ -81,16 +72,15 @@ const MAX_CHAT_OUTPUT_ITEMS = 50;
|
|
|
81
72
|
* agent responses. Customization for inputs and outputs is provided via event handlers-like props.
|
|
82
73
|
*/
|
|
83
74
|
export const ChatCommon = ({ ref, ...props }) => {
|
|
84
|
-
const {
|
|
75
|
+
const { customAgentGreetings = EMPTY, agentPlaceholders = EMPTY, backgroundColor, currentUser, extraParams, extraSlyData, id, isAwaitingLlm, legacyAgentEndpoint, neuroSanURL, onChunkReceived, onClose, onSend, onStreamingComplete, onStreamingStarted, setIsAwaitingLlm, setPreviousResponse, targetAgent, title, userImage, } = props;
|
|
85
76
|
// MUI theme
|
|
86
77
|
const theme = useTheme();
|
|
87
|
-
const shadowColor = theme.palette.mode === "dark" ? theme.palette.common.white : theme.palette.common.black;
|
|
88
78
|
// User LLM chat input
|
|
89
79
|
const [chatInput, setChatInput] = useState("");
|
|
90
80
|
// Previous user query (for "regenerate" feature)
|
|
91
|
-
const previousUserQuery =
|
|
92
|
-
//
|
|
93
|
-
const [
|
|
81
|
+
const [previousUserQuery, setPreviousUserQuery] = useState("");
|
|
82
|
+
// Turns within the current conversation
|
|
83
|
+
const [turns, setTurns] = useState([]);
|
|
94
84
|
// To accumulate current response, which will be different from the contents of the output window if there is a
|
|
95
85
|
// chat session
|
|
96
86
|
const currentResponse = useRef("");
|
|
@@ -102,19 +92,14 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
102
92
|
const controller = useRef(null);
|
|
103
93
|
// For tracking if we're auto-scrolling. A button allows the user to enable or disable auto-scrolling.
|
|
104
94
|
const [autoScrollEnabled, setAutoScrollEnabled] = useState(true);
|
|
105
|
-
// ref for same
|
|
106
|
-
const autoScrollEnabledRef = useRef(autoScrollEnabled);
|
|
107
95
|
// Whether to wrap output text
|
|
108
96
|
const [shouldWrapOutput, setShouldWrapOutput] = useState(true);
|
|
109
97
|
// Keeps a copy of the last AI message so we can highlight it as "final answer"
|
|
110
98
|
const lastAIMessage = useRef("");
|
|
111
|
-
// Ref for the final answer key, so we can highlight the accordion
|
|
112
|
-
const finalAnswerKey = useRef("");
|
|
113
99
|
// Persistent agent chat history store, which is where we store both kinds of chat histories
|
|
114
100
|
// (see store implementation for details)
|
|
115
101
|
const storedChatHistory = useAgentChatHistoryStore((state) => state?.history?.[targetAgent]);
|
|
116
102
|
const agentChatHistory = useMemo(() => storedChatHistory ?? { chatHistory: [], chatContext: null, slyData: {} }, [storedChatHistory]);
|
|
117
|
-
const [agentSampleQueries, setAgentSampleQueries] = useState([]);
|
|
118
103
|
// Access store for context items
|
|
119
104
|
const updateChatContext = useAgentChatHistoryStore((state) => state.updateChatContext);
|
|
120
105
|
const updateChatHistory = useAgentChatHistoryStore((state) => state.updateChatHistory);
|
|
@@ -151,8 +136,6 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
151
136
|
};
|
|
152
137
|
// Keeps track of whether the agent completed its task
|
|
153
138
|
const succeeded = useRef(false);
|
|
154
|
-
const darkMode = theme.palette.mode === "dark";
|
|
155
|
-
const { atelierDuneDark, a11yLight } = HLJS_THEMES;
|
|
156
139
|
const agentDisplayName = useMemo(() => cleanUpAgentName(removeTrailingUuid(targetAgent)), [targetAgent]);
|
|
157
140
|
useEffect(() => {
|
|
158
141
|
// Set up speech recognition
|
|
@@ -160,96 +143,55 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
160
143
|
// Clean up function
|
|
161
144
|
return () => cleanupAndStopSpeechRecognition(speechRecognitionRef, handlers);
|
|
162
145
|
}, []);
|
|
163
|
-
// Sync ref with state variable for use within timer etc.
|
|
164
|
-
useEffect(() => {
|
|
165
|
-
autoScrollEnabledRef.current = autoScrollEnabled;
|
|
166
|
-
}, [autoScrollEnabled]);
|
|
167
146
|
useEffect(() => {
|
|
168
147
|
// Delay for a second before focusing on the input area; gets around ChatBot stealing focus.
|
|
169
148
|
setTimeout(() => chatInputRef?.current?.focus(), 1000);
|
|
170
149
|
}, []);
|
|
171
150
|
// Auto scroll chat output window when new content is added
|
|
172
151
|
useEffect(() => {
|
|
152
|
+
const container = chatOutputRef.current;
|
|
153
|
+
if (!container)
|
|
154
|
+
return;
|
|
173
155
|
// Scroll the final answer into view
|
|
174
156
|
if (finalAnswerRef.current && !isAwaitingLlm) {
|
|
175
|
-
|
|
157
|
+
container.scrollTop = finalAnswerRef.current.offsetTop - 50;
|
|
176
158
|
return;
|
|
177
159
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
}, [chatOutput, isAwaitingLlm]);
|
|
182
|
-
/**
|
|
183
|
-
* Process a log line from the agent and format it nicely using the syntax highlighter and Accordion components.
|
|
184
|
-
* By the time we get to here, it's assumed things like errors and termination conditions have already been handled.
|
|
185
|
-
*
|
|
186
|
-
* @param logLine The log line to process
|
|
187
|
-
* @param messageType The type of the message (AI, LEGACY_LOGS etc.). Used for displaying certain message types
|
|
188
|
-
* differently
|
|
189
|
-
* @param isFinalAnswer If true, the log line is the final answer from the agent. This will be highlighted in some
|
|
190
|
-
* way to draw the user's attention to it.
|
|
191
|
-
* @param summary Used as the "title" for the accordion block. Something like an agent name or "Final Answer"
|
|
192
|
-
* @returns A React component representing the log line (agent message)
|
|
193
|
-
*/
|
|
194
|
-
const processLogLine = useCallback((logLine, summary, messageType, isFinalAnswer) => {
|
|
195
|
-
// extract the parts of the line
|
|
196
|
-
let repairedJson;
|
|
197
|
-
try {
|
|
198
|
-
// Attempt to parse as JSON
|
|
199
|
-
// First, repair it. Also replace "escaped newlines" with actual newlines for better display.
|
|
200
|
-
repairedJson = jsonrepair(logLine);
|
|
201
|
-
// Now try to parse it. We don't care about the result, only if it throws on parsing.
|
|
202
|
-
JSON.parse(repairedJson);
|
|
203
|
-
repairedJson = repairedJson.replace(/\\n/gu, "\n").replace(/\\"/gu, "'");
|
|
204
|
-
}
|
|
205
|
-
catch {
|
|
206
|
-
// Not valid JSON
|
|
207
|
-
repairedJson = null;
|
|
208
|
-
}
|
|
209
|
-
const hashedSummary = hashString(summary);
|
|
210
|
-
const isAIMessage = messageType === ChatMessageType.AI;
|
|
211
|
-
if (isAIMessage && !isFinalAnswer) {
|
|
212
|
-
lastAIMessage.current = logLine;
|
|
160
|
+
// Live-streaming auto-scroll
|
|
161
|
+
if (autoScrollEnabled) {
|
|
162
|
+
container.scrollTop = container.scrollHeight;
|
|
213
163
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
return (_jsx(MUIAccordion, { id: `${hashedSummary}-panel`, defaultExpandedPanelKey: isFinalAnswer ? 1 : null, items: [
|
|
219
|
-
{
|
|
220
|
-
title: summary,
|
|
221
|
-
content: (_jsx("div", { id: `${summary}-details`, children: repairedJson ? (_jsx(SyntaxHighlighter, { id: "syntax-highlighter", language: "json", style: darkMode ? atelierDuneDark : a11yLight, showLineNumbers: false, wrapLongLines: shouldWrapOutput, children: repairedJson })) : (_jsx(ReactMarkdown, { children: logLine }, hashString(logLine))) })),
|
|
222
|
-
},
|
|
223
|
-
], sx: {
|
|
224
|
-
fontSize: "large",
|
|
225
|
-
marginBottom: "1rem",
|
|
226
|
-
boxShadow: isFinalAnswer
|
|
227
|
-
? `0 6px 16px 0 ${alpha(shadowColor, 0.08)}, 0 3px 6px -4px ${alpha(shadowColor, 0.12)},
|
|
228
|
-
0 9px 28px 8px ${alpha(shadowColor, 0.05)}`
|
|
229
|
-
: "none",
|
|
230
|
-
} }, hashedSummary));
|
|
231
|
-
}, [a11yLight, atelierDuneDark, darkMode, shadowColor, shouldWrapOutput]);
|
|
232
|
-
/**
|
|
233
|
-
* Handles adding content to the output window. We only store the last MAX_CHAT_OUTPUT_ITEMS items to keep
|
|
234
|
-
* memory usage down.
|
|
235
|
-
* @param node A ReactNode to add to the output window -- text, spinner, etc. but could also be simple string
|
|
236
|
-
* @returns Nothing, but updates the output window with the new content.
|
|
237
|
-
*/
|
|
238
|
-
const updateOutput = useCallback((node) => {
|
|
239
|
-
setChatOutput((current) => {
|
|
240
|
-
const next = [...current, node];
|
|
164
|
+
}, [autoScrollEnabled, isAwaitingLlm, turns]);
|
|
165
|
+
const addTurn = useCallback((turn) => {
|
|
166
|
+
setTurns((current) => {
|
|
167
|
+
const next = [...current, turn];
|
|
241
168
|
return next.length > MAX_CHAT_OUTPUT_ITEMS ? next.slice(-MAX_CHAT_OUTPUT_ITEMS) : next;
|
|
242
169
|
});
|
|
243
170
|
}, []);
|
|
171
|
+
// We use this to update the same "turn" as chunks come in from legacy agents
|
|
172
|
+
const legacyTurnIdRef = useRef(null);
|
|
244
173
|
const handleChunk = useCallback((chunk) => {
|
|
245
174
|
// Give container a chance to process the chunk first
|
|
246
175
|
const onChunkReceivedResult = onChunkReceived?.(chunk) ?? true;
|
|
247
176
|
succeeded.current = succeeded.current || onChunkReceivedResult;
|
|
248
177
|
// For legacy agents, we either get plain text or Markdown. Just output it as-is.
|
|
249
178
|
if (isLegacyAgentType(targetAgent)) {
|
|
250
|
-
// Display output as-is
|
|
251
|
-
updateOutput(chunk);
|
|
252
179
|
currentResponse.current += chunk;
|
|
180
|
+
if (!legacyTurnIdRef.current) {
|
|
181
|
+
// We don't yet have a turn for this response, so create one. On subsequent chunks, we'll just
|
|
182
|
+
// update the text of this turn.
|
|
183
|
+
legacyTurnIdRef.current = uuid();
|
|
184
|
+
addTurn({
|
|
185
|
+
id: legacyTurnIdRef.current,
|
|
186
|
+
role: MessageRole.LegacyAgent,
|
|
187
|
+
text: currentResponse.current,
|
|
188
|
+
alwaysShow: true,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
// We already have a turn for this response, so just update the text of that turn.
|
|
193
|
+
setTurns((prev) => prev.map((t) => (t.id === legacyTurnIdRef.current ? { ...t, text: currentResponse.current } : t)));
|
|
194
|
+
}
|
|
253
195
|
// Check for Final Answer from legacy agent
|
|
254
196
|
const finalAnswerMatch = extractFinalAnswer(currentResponse.current);
|
|
255
197
|
if (finalAnswerMatch) {
|
|
@@ -264,6 +206,10 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
264
206
|
// But don't want to spam output by logging errors for every bad message.
|
|
265
207
|
return;
|
|
266
208
|
}
|
|
209
|
+
// Keep track of AI messages. The last one is (by definition) the "final answer" from the agents.
|
|
210
|
+
if (chatMessage.type === ChatMessageType.AI && chatMessage.text) {
|
|
211
|
+
lastAIMessage.current = chatMessage.text;
|
|
212
|
+
}
|
|
267
213
|
// Shallow merge existing slyData with incoming chatMessage.sly_data
|
|
268
214
|
if (chatMessage.sly_data) {
|
|
269
215
|
updateSlyData(targetAgent, chatMessage.sly_data);
|
|
@@ -279,7 +225,12 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
279
225
|
// If there is an error block, we should display it as an alert.
|
|
280
226
|
const errorMessage = checkError(chatMessage.structure);
|
|
281
227
|
if (errorMessage) {
|
|
282
|
-
|
|
228
|
+
addTurn({
|
|
229
|
+
id: uuid(),
|
|
230
|
+
role: MessageRole.Warning,
|
|
231
|
+
text: errorMessage,
|
|
232
|
+
alwaysShow: true,
|
|
233
|
+
});
|
|
283
234
|
succeeded.current = false;
|
|
284
235
|
}
|
|
285
236
|
}
|
|
@@ -290,20 +241,15 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
290
241
|
const agentName = chatMessage.origin?.length > 0
|
|
291
242
|
? cleanUpAgentName(chatMessage.origin[chatMessage.origin.length - 1].tool)
|
|
292
243
|
: "Agent message";
|
|
293
|
-
|
|
244
|
+
addTurn({
|
|
245
|
+
id: uuid(),
|
|
246
|
+
role: MessageRole.Agent,
|
|
247
|
+
agentName,
|
|
248
|
+
text: chatMessage.text,
|
|
249
|
+
});
|
|
294
250
|
currentResponse.current += chatMessage.text;
|
|
295
251
|
}
|
|
296
|
-
}, [onChunkReceived,
|
|
297
|
-
const introduceAgent = useCallback(() => {
|
|
298
|
-
/**
|
|
299
|
-
* Introduce the agent to the user with a friendly greeting
|
|
300
|
-
*/
|
|
301
|
-
updateOutput(_jsx(UserQueryDisplay, { userQuery: agentDisplayName, title: targetAgent, userImage: AGENT_IMAGE }));
|
|
302
|
-
// Random greeting
|
|
303
|
-
const greeting = agentGreetings[targetAgent] ?? AGENT_GREETINGS[Math.floor(Math.random() * AGENT_GREETINGS.length)];
|
|
304
|
-
updateOutput(greeting);
|
|
305
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps -- updateOutput is stable (empty useCallback deps)
|
|
306
|
-
}, [agentDisplayName, targetAgent]);
|
|
252
|
+
}, [onChunkReceived, targetAgent, addTurn, updateSlyData, updateChatContext]);
|
|
307
253
|
/**
|
|
308
254
|
* Reset the state of the component. This is called after a request is completed, regardless of success or failure.
|
|
309
255
|
*/
|
|
@@ -313,9 +259,9 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
313
259
|
setChatInput("");
|
|
314
260
|
lastAIMessage.current = "";
|
|
315
261
|
finalAnswerRef.current = null;
|
|
316
|
-
// Get agent name, either from the enum (Neuro-san) or from the targetAgent string directly (legacy)
|
|
317
262
|
setPreviousResponse?.(targetAgent, currentResponse.current);
|
|
318
263
|
currentResponse.current = "";
|
|
264
|
+
legacyTurnIdRef.current = null;
|
|
319
265
|
}, [setIsAwaitingLlm, setPreviousResponse, targetAgent]);
|
|
320
266
|
/*
|
|
321
267
|
* The main logic for sending a query to the server, with retries on errors.
|
|
@@ -353,12 +299,18 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
353
299
|
if (error instanceof Error) {
|
|
354
300
|
console.error(error, error.stack);
|
|
355
301
|
}
|
|
356
|
-
|
|
302
|
+
addTurn({
|
|
303
|
+
id: uuid(),
|
|
304
|
+
role: MessageRole.Error,
|
|
305
|
+
text: `Error occurred: ${error}`,
|
|
306
|
+
alwaysShow: true,
|
|
307
|
+
});
|
|
357
308
|
}
|
|
358
309
|
}
|
|
359
310
|
} while (attemptNumber < MAX_AGENT_RETRIES && !succeeded.current);
|
|
360
311
|
return wasAborted;
|
|
361
312
|
}, [
|
|
313
|
+
addTurn,
|
|
362
314
|
agentChatHistory,
|
|
363
315
|
currentUser,
|
|
364
316
|
extraParams,
|
|
@@ -367,7 +319,6 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
367
319
|
legacyAgentEndpoint,
|
|
368
320
|
neuroSanURL,
|
|
369
321
|
targetAgent,
|
|
370
|
-
updateOutput,
|
|
371
322
|
]);
|
|
372
323
|
const handleSend = useCallback(async (query) => {
|
|
373
324
|
// Record user query in chat history. Discard anything beyond MAX_CHAT_HISTORY_ITEMS
|
|
@@ -377,41 +328,57 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
377
328
|
const queryToSend = onSend?.(query) ?? query;
|
|
378
329
|
// Save query for "regenerate" use. Again we save the real user input, not the modified query. It will again
|
|
379
330
|
// get intercepted and re-modified (if applicable) on "regenerate".
|
|
380
|
-
|
|
331
|
+
setPreviousUserQuery(query);
|
|
381
332
|
setIsAwaitingLlm(true);
|
|
382
333
|
// Always start output by echoing user query.
|
|
383
334
|
// Note: we display the original user query, not the modified one. The modified one could be a monstrosity
|
|
384
335
|
// that we generated behind their back. Ultimately, we shouldn't need to generate a fake query on behalf
|
|
385
336
|
// of the user, but currently we do for orchestration.
|
|
386
|
-
|
|
337
|
+
addTurn({
|
|
338
|
+
id: uuid(),
|
|
339
|
+
role: MessageRole.User,
|
|
340
|
+
text: query,
|
|
341
|
+
alwaysShow: true,
|
|
342
|
+
});
|
|
387
343
|
// Add ID block for agent
|
|
388
|
-
|
|
344
|
+
addTurn({
|
|
345
|
+
agentDisplayName,
|
|
346
|
+
agentName: targetAgent,
|
|
347
|
+
alwaysShow: true,
|
|
348
|
+
id: uuid(),
|
|
349
|
+
role: MessageRole.AgentHeader,
|
|
350
|
+
text: agentDisplayName,
|
|
351
|
+
});
|
|
389
352
|
// Allow clients to do something when streaming starts
|
|
390
353
|
onStreamingStarted?.();
|
|
391
354
|
// Set up the abort controller
|
|
392
355
|
controller.current = new AbortController();
|
|
393
356
|
setIsAwaitingLlm(true);
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
], sx: { marginBottom: "1rem" } }));
|
|
401
|
-
}
|
|
357
|
+
addTurn({
|
|
358
|
+
agentName: `Contacting ${agentDisplayName}...`,
|
|
359
|
+
id: uuid(),
|
|
360
|
+
role: MessageRole.Agent,
|
|
361
|
+
text: `Query: ${queryToSend}`,
|
|
362
|
+
});
|
|
402
363
|
try {
|
|
403
364
|
// Invoke the logic to send the request and retry as necessary
|
|
404
365
|
const wasAborted = await doRetryLoop(queryToSend);
|
|
405
366
|
if (!wasAborted && !succeeded.current) {
|
|
406
|
-
|
|
367
|
+
addTurn({
|
|
368
|
+
alwaysShow: true,
|
|
369
|
+
id: uuid(),
|
|
370
|
+
role: MessageRole.Error,
|
|
371
|
+
text: `Gave up after ${MAX_AGENT_RETRIES} attempts.`,
|
|
372
|
+
});
|
|
407
373
|
}
|
|
408
374
|
// Display prominent "Final Answer" message if we have one
|
|
409
375
|
if (lastAIMessage.current) {
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
376
|
+
addTurn({
|
|
377
|
+
alwaysShow: true,
|
|
378
|
+
id: uuid(),
|
|
379
|
+
role: MessageRole.FinalAnswer,
|
|
380
|
+
text: lastAIMessage.current,
|
|
381
|
+
});
|
|
415
382
|
// Record bot answer in history.
|
|
416
383
|
if (currentResponse?.current?.length > 0) {
|
|
417
384
|
updateChatHistory(targetAgent, [new AIMessage({ content: lastAIMessage.current, id: uuid() })]);
|
|
@@ -422,8 +389,6 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
422
389
|
// as the bot answer in that case.
|
|
423
390
|
updateChatHistory(targetAgent, [new AIMessage({ content: currentResponse.current, id: uuid() })]);
|
|
424
391
|
}
|
|
425
|
-
// Add a blank line after response
|
|
426
|
-
updateOutput("\n");
|
|
427
392
|
}
|
|
428
393
|
finally {
|
|
429
394
|
resetState();
|
|
@@ -431,66 +396,32 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
431
396
|
onStreamingComplete?.();
|
|
432
397
|
}
|
|
433
398
|
}, [
|
|
399
|
+
addTurn,
|
|
434
400
|
agentDisplayName,
|
|
435
|
-
currentUser,
|
|
436
401
|
doRetryLoop,
|
|
437
402
|
onSend,
|
|
438
403
|
onStreamingComplete,
|
|
439
404
|
onStreamingStarted,
|
|
440
|
-
processLogLine,
|
|
441
405
|
resetState,
|
|
442
406
|
setIsAwaitingLlm,
|
|
443
|
-
showThinking,
|
|
444
407
|
targetAgent,
|
|
445
408
|
updateChatHistory,
|
|
446
|
-
updateOutput,
|
|
447
|
-
userImage,
|
|
448
409
|
]);
|
|
449
|
-
useEffect(() => {
|
|
450
|
-
if (targetAgent) {
|
|
451
|
-
introduceAgent();
|
|
452
|
-
}
|
|
453
|
-
}, [targetAgent, introduceAgent]);
|
|
454
|
-
useEffect(() => {
|
|
455
|
-
const fetchAgentDetails = async () => {
|
|
456
|
-
let agentFunction;
|
|
457
|
-
// It is a Neuro-san agent, so get the function and connectivity info
|
|
458
|
-
try {
|
|
459
|
-
agentFunction = await getAgentFunction(neuroSanURL, targetAgent, currentUser);
|
|
460
|
-
}
|
|
461
|
-
catch {
|
|
462
|
-
// For now, just return. May be a legacy agent without a functional description in Neuro-san.
|
|
463
|
-
return;
|
|
464
|
-
}
|
|
465
|
-
try {
|
|
466
|
-
const connectivity = await getConnectivity(neuroSanURL, targetAgent, currentUser);
|
|
467
|
-
updateOutput(_jsx(AgentConnectivity, { id: id, description: agentFunction?.function?.description, connectivityInfo: connectivity?.connectivity_info, targetAgent: targetAgent }));
|
|
468
|
-
const sampleQueries = (connectivity?.metadata?.["sample_queries"] || []);
|
|
469
|
-
setAgentSampleQueries(sampleQueries);
|
|
470
|
-
}
|
|
471
|
-
catch (e) {
|
|
472
|
-
sendNotification(NotificationType.error, `Failed to get connectivity info for ${agentDisplayName}. Error: ${e}`);
|
|
473
|
-
}
|
|
474
|
-
};
|
|
475
|
-
if (targetAgent && !isLegacyAgentType(targetAgent)) {
|
|
476
|
-
void fetchAgentDetails();
|
|
477
|
-
}
|
|
478
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps -- only want to run this when targetAgent changes
|
|
479
|
-
}, [targetAgent]);
|
|
480
410
|
const handleStop = useCallback(() => {
|
|
481
411
|
try {
|
|
482
412
|
controller?.current?.abort();
|
|
483
413
|
controller.current = null;
|
|
484
|
-
|
|
414
|
+
addTurn({
|
|
415
|
+
alwaysShow: true,
|
|
416
|
+
id: uuid(),
|
|
417
|
+
role: MessageRole.Warning,
|
|
418
|
+
text: "Request cancelled.",
|
|
419
|
+
});
|
|
485
420
|
}
|
|
486
421
|
finally {
|
|
487
422
|
resetState();
|
|
488
423
|
}
|
|
489
|
-
}, [
|
|
490
|
-
// Expose the handleStop method to parent components via ref for external control (e.g., to cancel chat requests)
|
|
491
|
-
useImperativeHandle(ref, () => ({
|
|
492
|
-
handleStop,
|
|
493
|
-
}), [handleStop]);
|
|
424
|
+
}, [addTurn, resetState]);
|
|
494
425
|
// Regex to check if user has typed anything besides whitespace
|
|
495
426
|
const userInputEmpty = !chatInput || chatInput.length === 0 || hasOnlyWhitespace(chatInput);
|
|
496
427
|
// Enable Send button when there is user input and not awaiting a response
|
|
@@ -498,46 +429,31 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
498
429
|
// Enable regenerate button when there is a previous query to resent, and we're not awaiting a response
|
|
499
430
|
const shouldEnableRegenerateButton = previousUserQuery && !isAwaitingLlm;
|
|
500
431
|
// Enable Clear Chat button if not awaiting response and there is chat output to clear
|
|
501
|
-
const enableClearChatButton = !isAwaitingLlm &&
|
|
432
|
+
const enableClearChatButton = !isAwaitingLlm && turns.length > 0;
|
|
502
433
|
const getPlaceholder = () => !targetAgent ? null : agentPlaceholders[targetAgent] || `Chat with ${agentDisplayName}`;
|
|
503
434
|
const handleClearChat = useCallback(() => {
|
|
504
|
-
|
|
435
|
+
setTurns([]);
|
|
505
436
|
resetHistory(targetAgent);
|
|
506
|
-
|
|
437
|
+
setPreviousUserQuery("");
|
|
507
438
|
currentResponse.current = "";
|
|
508
439
|
lastAIMessage.current = "";
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
}
|
|
527
|
-
return item;
|
|
528
|
-
})
|
|
529
|
-
.filter((item) => item !== null), [chatOutput, showThinking]);
|
|
530
|
-
const getNoAgentOverlay = () => (_jsx(Box, { id: "chat-disabled-overlay", sx: {
|
|
531
|
-
position: "absolute",
|
|
532
|
-
top: 0,
|
|
533
|
-
left: 0,
|
|
534
|
-
right: 0,
|
|
535
|
-
bottom: 0,
|
|
536
|
-
zIndex: theme.zIndex.modal - 1,
|
|
537
|
-
cursor: "not-allowed",
|
|
538
|
-
// Capture all pointer events to prevent interaction with the chat when no agent is selected
|
|
539
|
-
pointerEvents: "all",
|
|
540
|
-
} }));
|
|
440
|
+
}, [resetHistory, targetAgent]);
|
|
441
|
+
// Expose the handleStop and handleClearChat methods to parent components via ref for external control
|
|
442
|
+
useImperativeHandle(ref, () => ({
|
|
443
|
+
handleStop,
|
|
444
|
+
handleClearChat,
|
|
445
|
+
}), [handleStop, handleClearChat]);
|
|
446
|
+
const getNoAgentOverlay = () => (_jsx(Tooltip, { title: "Please select a Network from the list to start the chat.", placement: "auto", children: _jsx(Box, { id: "chat-disabled-overlay", sx: {
|
|
447
|
+
position: "absolute",
|
|
448
|
+
top: 0,
|
|
449
|
+
left: 0,
|
|
450
|
+
right: 0,
|
|
451
|
+
bottom: 0,
|
|
452
|
+
zIndex: theme.zIndex.modal - 1,
|
|
453
|
+
cursor: "not-allowed",
|
|
454
|
+
// Capture all pointer events to prevent interaction with the chat when no agent is selected
|
|
455
|
+
pointerEvents: "all",
|
|
456
|
+
} }) }));
|
|
541
457
|
const getTitle = () => (_jsxs(Box, { id: `llm-chat-title-container-${id}`, sx: {
|
|
542
458
|
alignItems: "center",
|
|
543
459
|
borderTopLeftRadius: "var(--bs-border-radius)",
|
|
@@ -550,6 +466,7 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
550
466
|
paddingBottom: "0.25rem",
|
|
551
467
|
}, children: [_jsx(Typography, { id: `llm-chat-title-${id}-text`, sx: { fontSize: "0.9rem" }, children: title }), onClose && (_jsx(IconButton, { "data-testid": `close-button-${id}`, id: `close-button-${id}`, onClick: onClose, children: _jsx(CloseIcon, { id: `close-icon-${id}` }) }))] }));
|
|
552
468
|
const getOptionsButtons = () => (_jsxs(_Fragment, { children: [_jsx(Tooltip, { id: "show-thinking", title: showThinking ? "Displaying agent thinking" : "Hiding agent thinking", children: _jsx("span", { id: "show-thinking-span", children: _jsx(LlmChatOptionsButton, { enabled: showThinking, id: "show-thinking-button", onClick: () => setShowThinking(!showThinking), posRight: 150, disabled: isAwaitingLlm, children: _jsx(AccountTreeIcon, { id: "show-thinking-icon", sx: { color: "var(--bs-white)", fontSize: "0.85rem" } }) }) }) }), _jsx(Tooltip, { id: "enable-autoscroll", title: autoScrollEnabled ? "Autoscroll enabled" : "Autoscroll disabled", children: _jsx(LlmChatOptionsButton, { enabled: autoScrollEnabled, id: "autoscroll-button", onClick: () => setAutoScrollEnabled(!autoScrollEnabled), posRight: 80, children: _jsx(VerticalAlignBottomIcon, { id: "autoscroll-icon", sx: { color: "var(--bs-white)", fontSize: "0.85rem" } }) }) }), _jsx(Tooltip, { id: "wrap-tooltip", title: shouldWrapOutput ? "Text wrapping enabled" : "Text wrapping disabled", children: _jsx(LlmChatOptionsButton, { enabled: shouldWrapOutput, id: "wrap-button", onClick: () => setShouldWrapOutput(!shouldWrapOutput), posRight: 10, children: _jsx(WrapTextIcon, { id: "wrap-icon", sx: { color: "var(--bs-white)", fontSize: "0.85rem" } }) }) })] }));
|
|
469
|
+
const agentIntro = (_jsx(AgentIntro, { agentDisplayName: agentDisplayName, customAgentGreetings: customAgentGreetings, targetAgent: targetAgent }, targetAgent));
|
|
553
470
|
const getResponseBox = () => (_jsxs(Box, { id: "llm-response-div", sx: {
|
|
554
471
|
...divStyle,
|
|
555
472
|
border: "var(--bs-border-width) var(--bs-border-style)",
|
|
@@ -572,9 +489,9 @@ export const ChatCommon = ({ ref, ...props }) => {
|
|
|
572
489
|
paddingLeft: "15px",
|
|
573
490
|
paddingRight: "15px",
|
|
574
491
|
width: "100%",
|
|
575
|
-
}, tabIndex: -1, children: [agentChatHistory?.chatHistory?.length > 0 && (_jsx(ChatHistory, { agentImage: AGENT_IMAGE, agentDisplayName: agentDisplayName, chatHistoryKey: CHAT_HISTORY_KEY, currentUser: currentUser, id: `${id}-chat-history`, messages: agentChatHistory.chatHistory, targetAgent: targetAgent, userImage: userImage })), _jsx(
|
|
492
|
+
}, tabIndex: -1, children: [agentChatHistory?.chatHistory?.length > 0 && (_jsx(ChatHistory, { agentImage: AGENT_IMAGE, agentDisplayName: agentDisplayName, chatHistoryKey: CHAT_HISTORY_KEY, currentUser: currentUser, id: `${id}-chat-history`, messages: agentChatHistory.chatHistory, targetAgent: targetAgent, userImage: userImage })), !isLegacyAgentType(targetAgent) && agentIntro, !isLegacyAgentType(targetAgent) && (_jsx(AgentMetadata, { disableQueries: isAwaitingLlm, handleSend: handleSend, currentUser: currentUser, id: `${id}-agent-metadata-display`, neuroSanURL: neuroSanURL, targetAgent: targetAgent })), _jsx(Conversation, { id: `${id}-conversation-display`, currentUser: currentUser, finalAnswerRef: finalAnswerRef, showThinking: showThinking, shouldWrapOutput: shouldWrapOutput, turns: turns, userImage: userImage }), isLegacyAgentType(targetAgent) && agentIntro, isAwaitingLlm && (_jsxs(Box, { id: "awaitingOutputContainer", sx: { display: "flex", alignItems: "center", fontSize: "smaller" }, children: [_jsx("span", { id: "working-span", style: { marginRight: "1rem" }, children: "Working..." }), _jsx(CircularProgress, { id: "awaitingOutputSpinner", sx: {
|
|
576
493
|
color: "var(--bs-primary)",
|
|
577
|
-
}, size: "1rem" })] }))] }), _jsx(ControlButtons, {
|
|
494
|
+
}, size: "1rem" })] }))] }), _jsx(ControlButtons, { enableClearChatButton: enableClearChatButton, handleClearChat: handleClearChat, handleSend: handleSend, handleStop: handleStop, isAwaitingLlm: isAwaitingLlm, previousUserQuery: previousUserQuery, shouldEnableRegenerateButton: shouldEnableRegenerateButton })] }));
|
|
578
495
|
const getUserInputBox = () => (_jsxs(Box, { id: "user-input-div", sx: {
|
|
579
496
|
...divStyle,
|
|
580
497
|
display: "flex",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const AGENT_IMAGE = "/agent.svg";
|
|
@@ -14,7 +14,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
14
14
|
See the License for the specific language governing permissions and
|
|
15
15
|
limitations under the License.
|
|
16
16
|
*/
|
|
17
|
-
import
|
|
17
|
+
import DeleteOutlined from "@mui/icons-material/DeleteOutlined";
|
|
18
18
|
import Loop from "@mui/icons-material/Loop";
|
|
19
19
|
import StopCircle from "@mui/icons-material/StopCircle";
|
|
20
20
|
import { SmallLlmChatButton } from "../Common/LlmChatButton.js";
|
|
@@ -23,4 +23,4 @@ import { SmallLlmChatButton } from "../Common/LlmChatButton.js";
|
|
|
23
23
|
* Generate the Control Buttons for a chat window.
|
|
24
24
|
* @returns A fragment containing the Control Buttons.
|
|
25
25
|
*/
|
|
26
|
-
export const ControlButtons = ({ handleClearChat, enableClearChatButton, isAwaitingLlm, handleSend, handleStop, previousUserQuery, shouldEnableRegenerateButton, }) => (_jsxs(_Fragment, { children: [!isAwaitingLlm && (_jsx(SmallLlmChatButton, { "aria-label": "Clear Chat", disabled: !enableClearChatButton, id: "clear-chat-button", onClick: handleClearChat, posBottom: 8, posRight: 65, children: _jsx(
|
|
26
|
+
export const ControlButtons = ({ handleClearChat, enableClearChatButton, isAwaitingLlm, handleSend, handleStop, previousUserQuery, shouldEnableRegenerateButton, }) => (_jsxs(_Fragment, { children: [!isAwaitingLlm && (_jsx(SmallLlmChatButton, { "aria-label": "Clear Chat", disabled: !enableClearChatButton, id: "clear-chat-button", onClick: handleClearChat, posBottom: 8, posRight: 65, children: _jsx(DeleteOutlined, { fontSize: "small", id: "stop-button-icon", sx: { color: "var(--bs-white)" } }) })), isAwaitingLlm && (_jsx(SmallLlmChatButton, { "aria-label": "Stop", disabled: !isAwaitingLlm, id: "stop-output-button", onClick: () => handleStop(), posBottom: 8, posRight: 23, children: _jsx(StopCircle, { fontSize: "small", id: "stop-button-icon", sx: { color: "var(--bs-white)" } }) })), !isAwaitingLlm && (_jsx(SmallLlmChatButton, { "aria-label": "Regenerate", disabled: !shouldEnableRegenerateButton, id: "regenerate-output-button", onClick: () => handleSend(previousUserQuery), posBottom: 8, posRight: 23, children: _jsx(Loop, { fontSize: "small", id: "generate-icon", sx: { color: "var(--bs-white)" } }) }))] }));
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { FC, Ref } from "react";
|
|
2
|
+
import { ConversationTurn } from "./ConversationTurn.js";
|
|
3
|
+
interface ConversationProps {
|
|
4
|
+
readonly id: string;
|
|
5
|
+
readonly currentUser: string;
|
|
6
|
+
readonly finalAnswerRef?: Ref<HTMLDivElement>;
|
|
7
|
+
readonly userImage?: string;
|
|
8
|
+
readonly showThinking: boolean;
|
|
9
|
+
readonly shouldWrapOutput: boolean;
|
|
10
|
+
readonly turns: ConversationTurn[];
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
*/
|
|
14
|
+
export declare const Conversation: FC<ConversationProps>;
|
|
15
|
+
export {};
|