@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.
Files changed (66) hide show
  1. package/dist/Theme/Theme.js +3 -3
  2. package/dist/components/AgentChat/ChatCommon/AgentIntro.d.ts +12 -0
  3. package/dist/components/AgentChat/ChatCommon/AgentIntro.js +19 -0
  4. package/dist/components/AgentChat/ChatCommon/AgentMetadata.d.ts +14 -0
  5. package/dist/components/AgentChat/ChatCommon/AgentMetadata.js +43 -0
  6. package/dist/components/AgentChat/ChatCommon/ChatCommon.d.ts +2 -1
  7. package/dist/components/AgentChat/ChatCommon/ChatCommon.js +131 -214
  8. package/dist/components/AgentChat/ChatCommon/Const.d.ts +1 -0
  9. package/dist/components/AgentChat/ChatCommon/Const.js +2 -0
  10. package/dist/components/AgentChat/ChatCommon/ControlButtons.js +2 -2
  11. package/dist/components/AgentChat/ChatCommon/Conversation.d.ts +15 -0
  12. package/dist/components/AgentChat/ChatCommon/Conversation.js +102 -0
  13. package/dist/components/AgentChat/ChatCommon/ConversationTurn.d.ts +19 -0
  14. package/dist/components/AgentChat/ChatCommon/ConversationTurn.js +10 -0
  15. package/dist/components/AgentChat/ChatCommon/Greetings.d.ts +1 -1
  16. package/dist/components/AgentChat/ChatCommon/Greetings.js +1 -1
  17. package/dist/components/AgentChat/Common/LlmChatButton.d.ts +2 -2
  18. package/dist/components/AgentChat/Common/Utils.js +1 -2
  19. package/dist/components/Common/ConfirmationModal.d.ts +1 -0
  20. package/dist/components/Common/ConfirmationModal.js +1 -1
  21. package/dist/components/Common/CustomerLogo.js +1 -1
  22. package/dist/components/Common/LlmChatOptionsButton.d.ts +1 -1
  23. package/dist/components/Common/Navbar.d.ts +2 -1
  24. package/dist/components/Common/Navbar.js +8 -4
  25. package/dist/components/Common/notification.d.ts +1 -1
  26. package/dist/components/Common/notification.js +17 -12
  27. package/dist/components/MultiAgentAccelerator/AgentFlow.d.ts +7 -0
  28. package/dist/components/MultiAgentAccelerator/AgentFlow.js +178 -73
  29. package/dist/components/MultiAgentAccelerator/AgentNode.d.ts +2 -1
  30. package/dist/components/MultiAgentAccelerator/AgentNode.js +52 -17
  31. package/dist/components/MultiAgentAccelerator/AgentNodePopup.d.ts +1 -4
  32. package/dist/components/MultiAgentAccelerator/AgentNodePopup.js +4 -5
  33. package/dist/components/MultiAgentAccelerator/GraphLayouts.js +7 -5
  34. package/dist/components/MultiAgentAccelerator/MultiAgentAccelerator.d.ts +2 -2
  35. package/dist/components/MultiAgentAccelerator/MultiAgentAccelerator.js +239 -51
  36. package/dist/components/MultiAgentAccelerator/Sidebar/AgentNetworkTreeItem.d.ts +1 -0
  37. package/dist/components/MultiAgentAccelerator/Sidebar/AgentNetworkTreeItem.js +28 -12
  38. package/dist/components/MultiAgentAccelerator/Sidebar/Sidebar.d.ts +1 -0
  39. package/dist/components/MultiAgentAccelerator/Sidebar/Sidebar.js +21 -5
  40. package/dist/components/MultiAgentAccelerator/Sidebar/TreeBuilder.d.ts +4 -3
  41. package/dist/components/MultiAgentAccelerator/Sidebar/TreeBuilder.js +8 -2
  42. package/dist/components/MultiAgentAccelerator/TemporaryNetworks.d.ts +19 -2
  43. package/dist/components/MultiAgentAccelerator/TemporaryNetworks.js +40 -5
  44. package/dist/components/MultiAgentAccelerator/ThoughtBubbleOverlay.js +27 -14
  45. package/dist/components/MultiAgentAccelerator/Tour/MainTourSteps.d.ts +7 -0
  46. package/dist/components/MultiAgentAccelerator/Tour/MainTourSteps.js +88 -0
  47. package/dist/components/MultiAgentAccelerator/const.d.ts +7 -10
  48. package/dist/components/MultiAgentAccelerator/const.js +9 -10
  49. package/dist/const.d.ts +5 -1
  50. package/dist/const.js +5 -2
  51. package/dist/controller/agent/Agent.d.ts +10 -0
  52. package/dist/controller/agent/Agent.js +17 -1
  53. package/dist/controller/llm/LlmChat.js +2 -2
  54. package/dist/state/TemporaryNetworks.d.ts +5 -15
  55. package/dist/state/TemporaryNetworks.js +15 -34
  56. package/dist/state/Tour.d.ts +29 -0
  57. package/dist/state/Tour.js +22 -0
  58. package/dist/state/UserInfo.d.ts +2 -1
  59. package/dist/tsconfig.build.tsbuildinfo +1 -1
  60. package/dist/utils/Authentication.js +12 -3
  61. package/dist/utils/File.d.ts +7 -0
  62. package/dist/utils/File.js +14 -3
  63. package/dist/utils/text.js +2 -2
  64. package/dist/utils/title.js +1 -1
  65. package/dist/utils/zIndexLayers.js +3 -0
  66. 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 { alpha, useTheme } from "@mui/material/styles";
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 { jsonrepair } from "jsonrepair";
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 { AgentConnectivity } from "./AgentConnectivity.js";
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 { FormattedMarkdown } from "./FormattedMarkdown.js";
43
- import { AGENT_GREETINGS } from "./Greetings.js";
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 { HLJS_THEMES } from "./SyntaxHighlighterThemes.js";
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, } from "../../../generated/neuro-san/NeuroSanClient.js";
46
+ import { ChatMessageType } from "../../../generated/neuro-san/NeuroSanClient.js";
51
47
  import { useAgentChatHistoryStore } from "../../../state/ChatHistory.js";
52
- import { hashString, hasOnlyWhitespace } from "../../../utils/text.js";
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 { agentGreetings = EMPTY, agentPlaceholders = EMPTY, backgroundColor, currentUser, extraParams, extraSlyData, id, isAwaitingLlm, legacyAgentEndpoint, neuroSanURL, onChunkReceived, onClose, onSend, onStreamingComplete, onStreamingStarted, setIsAwaitingLlm, setPreviousResponse, targetAgent, title, userImage, } = props;
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 = useRef("");
92
- // Chat output window contents
93
- const [chatOutput, setChatOutput] = useState([]);
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
- chatOutputRef.current.scrollTop = finalAnswerRef.current.offsetTop - 50;
157
+ container.scrollTop = finalAnswerRef.current.offsetTop - 50;
176
158
  return;
177
159
  }
178
- if (autoScrollEnabledRef.current && chatOutputRef?.current) {
179
- chatOutputRef.current.scrollTop = chatOutputRef.current.scrollHeight;
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
- if (isFinalAnswer) {
215
- // Save key of final answer for highlighting
216
- finalAnswerKey.current = hashedSummary;
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
- updateOutput(_jsx(MUIAlert, { id: "retry-message-alert", severity: "warning", children: errorMessage }));
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
- updateOutput(processLogLine(chatMessage.text, agentName, chatMessage.type));
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, processLogLine, updateSlyData, targetAgent, updateChatContext, updateOutput]);
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
- updateOutput(_jsx(MUIAlert, { id: "opp-finder-error-occurred-alert", severity: "error", children: `Error occurred: ${error}` }));
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
- previousUserQuery.current = query;
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
- updateOutput(_jsx(UserQueryDisplay, { userQuery: query, title: currentUser, userImage: userImage }));
337
+ addTurn({
338
+ id: uuid(),
339
+ role: MessageRole.User,
340
+ text: query,
341
+ alwaysShow: true,
342
+ });
387
343
  // Add ID block for agent
388
- updateOutput(_jsx(UserQueryDisplay, { userQuery: agentDisplayName, title: targetAgent, userImage: AGENT_IMAGE }));
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
- if (showThinking) {
395
- updateOutput(_jsx(MUIAccordion, { id: "initiating-orchestration-accordion", items: [
396
- {
397
- title: `Contacting ${agentDisplayName}...`,
398
- content: `Query: ${queryToSend}`,
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
- updateOutput(_jsx(MUIAlert, { id: "opp-finder-max-retries-exceeded-alert", severity: "error", children: `Gave up after ${MAX_AGENT_RETRIES} attempts.` }));
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
- // Legacy agents text is a bit messy and doesn't add a blank line, so we add it here
411
- if (isLegacyAgentType(targetAgent)) {
412
- updateOutput(" \n\n");
413
- }
414
- updateOutput(_jsx("div", { id: "final-answer-div", ref: finalAnswerRef, style: { marginBottom: "1rem" }, children: processLogLine(lastAIMessage.current, "Final Answer", ChatMessageType.AI, true) }));
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
- updateOutput(_jsx(MUIAlert, { id: "opp-finder-error-occurred-alert", severity: "warning", children: "Request cancelled." }));
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
- }, [resetState, updateOutput]);
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 && chatOutput.length > 0;
432
+ const enableClearChatButton = !isAwaitingLlm && turns.length > 0;
502
433
  const getPlaceholder = () => !targetAgent ? null : agentPlaceholders[targetAgent] || `Chat with ${agentDisplayName}`;
503
434
  const handleClearChat = useCallback(() => {
504
- setChatOutput([]);
435
+ setTurns([]);
505
436
  resetHistory(targetAgent);
506
- previousUserQuery.current = "";
437
+ setPreviousUserQuery("");
507
438
  currentResponse.current = "";
508
439
  lastAIMessage.current = "";
509
- introduceAgent();
510
- }, [introduceAgent, resetHistory, targetAgent]);
511
- /**
512
- * Extract the list of React nodes to display in the output window, potentially filtering out "thinking"
513
- * nodes if the user has chosen to hide them. Nodes that aren't to be shown are not even added to the DOM.
514
- * There are a couple of special nodes that are always shown: chat history (collapsible accordion) and whatever
515
- * we detected as the "final answer" (also a collapsible accordion).
516
- *
517
- * We use the MUIAccordion check as a proxy for "lines received from the agents"; everything that isn't
518
- * a MUIAccordion (e.g. alerts, connectivity info, greetings) is not something we would want to hide when
519
- * "show thinking" is off, so we always show those regardless of the "show thinking" setting.
520
- */
521
- const nodesList = useMemo(() => chatOutput
522
- .map((item) => {
523
- if (isValidElement(item) && item.type === MUIAccordion) {
524
- const shouldShow = showThinking || item.key === finalAnswerKey.current || item.key === CHAT_HISTORY_KEY;
525
- return shouldShow ? item : null;
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(FormattedMarkdown, { id: `${id}-formatted-markdown`, nodesList: nodesList, style: darkMode ? atelierDuneDark : a11yLight, wrapLongLines: shouldWrapOutput }), _jsx(SampleQueries, { disabled: isAwaitingLlm, handleSend: handleSend, sampleQueries: agentSampleQueries }), 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: {
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, { handleClearChat: handleClearChat, enableClearChatButton: enableClearChatButton, isAwaitingLlm: isAwaitingLlm, handleSend: handleSend, handleStop: handleStop, previousUserQuery: previousUserQuery.current, shouldEnableRegenerateButton: shouldEnableRegenerateButton })] }));
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";
@@ -0,0 +1,2 @@
1
+ // Avatar to use for agents in chat
2
+ export 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 DeleteOutline from "@mui/icons-material/DeleteOutline";
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(DeleteOutline, { 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)" } }) }))] }));
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 {};