@cognizant-ai-lab/ui-common 1.4.1 → 1.5.0

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 (70) hide show
  1. package/dist/components/AgentChat/ChatCommon/AgentConnectivity.d.ts +14 -0
  2. package/dist/components/AgentChat/ChatCommon/AgentConnectivity.js +23 -0
  3. package/dist/components/AgentChat/{ChatCommon.d.ts → ChatCommon/ChatCommon.d.ts} +8 -4
  4. package/dist/components/AgentChat/{ChatCommon.js → ChatCommon/ChatCommon.js} +318 -307
  5. package/dist/components/AgentChat/ChatCommon/ChatHistory.d.ts +17 -0
  6. package/dist/components/AgentChat/ChatCommon/ChatHistory.js +27 -0
  7. package/dist/components/AgentChat/{ControlButtons.d.ts → ChatCommon/ControlButtons.d.ts} +1 -1
  8. package/dist/components/AgentChat/ChatCommon/ControlButtons.js +26 -0
  9. package/dist/components/AgentChat/{FormattedMarkdown.js → ChatCommon/FormattedMarkdown.js} +1 -1
  10. package/dist/components/AgentChat/ChatCommon/SampleQueries.d.ts +16 -0
  11. package/dist/components/AgentChat/ChatCommon/SampleQueries.js +29 -0
  12. package/dist/components/AgentChat/{SendButton.js → ChatCommon/SendButton.js} +1 -1
  13. package/dist/components/AgentChat/ChatCommon/UserQueryDisplay.d.ts +7 -0
  14. package/dist/components/AgentChat/{UserQueryDisplay.js → ChatCommon/UserQueryDisplay.js} +4 -3
  15. package/dist/components/AgentChat/{LlmChatButton.d.ts → Common/LlmChatButton.d.ts} +2 -2
  16. package/dist/components/AgentChat/{Utils.d.ts → Common/Utils.d.ts} +1 -1
  17. package/dist/components/AgentChat/{Utils.js → Common/Utils.js} +2 -1
  18. package/dist/components/AgentChat/VoiceChat/MicrophoneButton.js +1 -1
  19. package/dist/components/ChatBot/ChatBot.js +2 -2
  20. package/dist/components/Common/CustomerLogo.js +1 -1
  21. package/dist/components/Common/LlmChatOptionsButton.d.ts +1 -1
  22. package/dist/components/Common/MUIDialog.d.ts +1 -0
  23. package/dist/components/Common/MUIDialog.js +2 -2
  24. package/dist/components/MultiAgentAccelerator/AgentCounts.d.ts +2 -2
  25. package/dist/components/MultiAgentAccelerator/AgentFlow.d.ts +13 -1
  26. package/dist/components/MultiAgentAccelerator/AgentFlow.js +193 -20
  27. package/dist/components/MultiAgentAccelerator/AgentNetworkDesigner.d.ts +10 -0
  28. package/dist/components/MultiAgentAccelerator/AgentNetworkDesigner.js +20 -0
  29. package/dist/components/MultiAgentAccelerator/AgentNode.d.ts +1 -0
  30. package/dist/components/MultiAgentAccelerator/AgentNode.js +9 -4
  31. package/dist/components/MultiAgentAccelerator/AgentNodePopup.d.ts +33 -0
  32. package/dist/components/MultiAgentAccelerator/AgentNodePopup.js +81 -0
  33. package/dist/components/MultiAgentAccelerator/GraphLayouts.d.ts +4 -4
  34. package/dist/components/MultiAgentAccelerator/GraphLayouts.js +12 -8
  35. package/dist/components/MultiAgentAccelerator/MultiAgentAccelerator.d.ts +1 -0
  36. package/dist/components/MultiAgentAccelerator/MultiAgentAccelerator.js +103 -65
  37. package/dist/components/MultiAgentAccelerator/Sidebar/AgentNetworkTreeItem.d.ts +1 -0
  38. package/dist/components/MultiAgentAccelerator/Sidebar/AgentNetworkTreeItem.js +24 -5
  39. package/dist/components/MultiAgentAccelerator/Sidebar/Sidebar.d.ts +1 -0
  40. package/dist/components/MultiAgentAccelerator/Sidebar/Sidebar.js +34 -23
  41. package/dist/components/MultiAgentAccelerator/Sidebar/TreeBuilder.js +1 -1
  42. package/dist/components/MultiAgentAccelerator/TemporaryNetworks.d.ts +21 -0
  43. package/dist/components/MultiAgentAccelerator/TemporaryNetworks.js +42 -2
  44. package/dist/components/MultiAgentAccelerator/ThoughtBubbleOverlay.js +8 -7
  45. package/dist/components/MultiAgentAccelerator/const.d.ts +26 -0
  46. package/dist/components/MultiAgentAccelerator/const.js +23 -0
  47. package/dist/controller/llm/LlmChat.js +1 -1
  48. package/dist/index.d.ts +8 -7
  49. package/dist/index.js +8 -7
  50. package/dist/state/ChatHistory.d.ts +50 -0
  51. package/dist/state/ChatHistory.js +98 -0
  52. package/dist/state/IndexedDBStorage.d.ts +14 -0
  53. package/dist/state/IndexedDBStorage.js +65 -0
  54. package/dist/state/TemporaryNetworks.d.ts +24 -0
  55. package/dist/state/TemporaryNetworks.js +43 -0
  56. package/dist/tsconfig.build.tsbuildinfo +1 -1
  57. package/dist/utils/File.d.ts +29 -0
  58. package/dist/utils/File.js +61 -0
  59. package/package.json +16 -11
  60. package/dist/components/AgentChat/ControlButtons.js +0 -26
  61. package/dist/components/AgentChat/UserQueryDisplay.d.ts +0 -5
  62. /package/dist/components/AgentChat/{FormattedMarkdown.d.ts → ChatCommon/FormattedMarkdown.d.ts} +0 -0
  63. /package/dist/components/AgentChat/{Greetings.d.ts → ChatCommon/Greetings.d.ts} +0 -0
  64. /package/dist/components/AgentChat/{Greetings.js → ChatCommon/Greetings.js} +0 -0
  65. /package/dist/components/AgentChat/{SendButton.d.ts → ChatCommon/SendButton.d.ts} +0 -0
  66. /package/dist/components/AgentChat/{SyntaxHighlighterThemes.d.ts → ChatCommon/SyntaxHighlighterThemes.d.ts} +0 -0
  67. /package/dist/components/AgentChat/{SyntaxHighlighterThemes.js → ChatCommon/SyntaxHighlighterThemes.js} +0 -0
  68. /package/dist/components/AgentChat/{LlmChatButton.js → Common/LlmChatButton.js} +0 -0
  69. /package/dist/components/AgentChat/{Types.d.ts → Common/Types.d.ts} +0 -0
  70. /package/dist/components/AgentChat/{Types.js → Common/Types.js} +0 -0
@@ -24,7 +24,6 @@ import CloseIcon from "@mui/icons-material/Close";
24
24
  import VerticalAlignBottomIcon from "@mui/icons-material/VerticalAlignBottom";
25
25
  import WrapTextIcon from "@mui/icons-material/WrapText";
26
26
  import Box from "@mui/material/Box";
27
- import Chip from "@mui/material/Chip";
28
27
  import CircularProgress from "@mui/material/CircularProgress";
29
28
  import IconButton from "@mui/material/IconButton";
30
29
  import Input from "@mui/material/Input";
@@ -33,27 +32,34 @@ import { alpha, useTheme } from "@mui/material/styles";
33
32
  import Tooltip from "@mui/material/Tooltip";
34
33
  import Typography from "@mui/material/Typography";
35
34
  import { jsonrepair } from "jsonrepair";
36
- import { isValidElement, useEffect, useImperativeHandle, useRef, useState, } from "react";
35
+ import { isValidElement, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, } from "react";
37
36
  import ReactMarkdown from "react-markdown";
38
37
  import SyntaxHighlighter from "react-syntax-highlighter";
38
+ import { v4 as uuid } from "uuid";
39
+ import { AgentConnectivity } from "./AgentConnectivity.js";
40
+ import { ChatHistory } from "./ChatHistory.js";
39
41
  import { ControlButtons } from "./ControlButtons.js";
40
42
  import { FormattedMarkdown } from "./FormattedMarkdown.js";
41
43
  import { AGENT_GREETINGS } from "./Greetings.js";
44
+ import { SampleQueries } from "./SampleQueries.js";
42
45
  import { SendButton } from "./SendButton.js";
43
46
  import { HLJS_THEMES } from "./SyntaxHighlighterThemes.js";
44
- import { isLegacyAgentType } from "./Types.js";
45
47
  import { UserQueryDisplay } from "./UserQueryDisplay.js";
46
- import { chatMessageFromChunk, checkError, cleanUpAgentName, removeTrailingUuid } from "./Utils.js";
47
- import { MicrophoneButton } from "./VoiceChat/MicrophoneButton.js";
48
- import { cleanupAndStopSpeechRecognition, setupSpeechRecognition } from "./VoiceChat/VoiceChat.js";
49
- import { getAgentFunction, getConnectivity, sendChatQuery } from "../../controller/agent/Agent.js";
50
- import { sendLlmRequest, StreamingUnit } from "../../controller/llm/LlmChat.js";
51
- import { ChatMessageType, } from "../../generated/neuro-san/NeuroSanClient.js";
52
- import { hashString, hasOnlyWhitespace } from "../../utils/text.js";
53
- 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";
48
+ import { getAgentFunction, getConnectivity, sendChatQuery } from "../../../controller/agent/Agent.js";
49
+ import { sendLlmRequest, StreamingUnit } from "../../../controller/llm/LlmChat.js";
50
+ import { ChatMessageType, } from "../../../generated/neuro-san/NeuroSanClient.js";
51
+ import { useAgentChatHistoryStore } from "../../../state/ChatHistory.js";
52
+ import { hashString, hasOnlyWhitespace } from "../../../utils/text.js";
53
+ 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
+ import { isLegacyAgentType } from "../Common/Types.js";
58
+ import { chatMessageFromChunk, checkError, cleanUpAgentName, removeTrailingUuid } from "../Common/Utils.js";
59
+ import { MicrophoneButton } from "../VoiceChat/MicrophoneButton.js";
60
+ import { cleanupAndStopSpeechRecognition, setupSpeechRecognition } from "../VoiceChat/VoiceChat.js";
61
+ // Key for the chat history, which gets special treatment; always visible even if "show thinking" is off.
62
+ const CHAT_HISTORY_KEY = "chat-history-accordion";
57
63
  // Define fancy EMPTY constant to avoid linter error about using object literals as default props
58
64
  const EMPTY = {};
59
65
  // Avatar to use for agents in chat
@@ -61,29 +67,28 @@ const AGENT_IMAGE = "/agent.svg";
61
67
  // How many times to retry the entire agent interaction process. Some networks have a well-defined success condition.
62
68
  // For others, it's just "whenever the stream is done".
63
69
  const MAX_AGENT_RETRIES = 3;
64
- // Maximum number of sample queries to show
65
- const MAX_SAMPLE_QUERIES = 5;
66
- // Maximum length of query to show in sample query chips
67
- const QUERY_TRUNCATE_LENGTH = 80;
70
+ /**
71
+ * Extract the final answer from the response from a legacy agent
72
+ * @param response The response from the legacy agent
73
+ * @returns The final answer from the agent, if it exists or undefined if it doesn't
74
+ */
75
+ const extractFinalAnswer = (response) => /Final Answer: (?<finalAnswerText>.*)/su.exec(response)?.groups?.["finalAnswerText"];
76
+ // Maximum number of items to keep in the chat output window
77
+ const MAX_CHAT_OUTPUT_ITEMS = 50;
68
78
  /**
69
79
  * Common chat component for agent chat. This component is used by all agent chat components to provide a consistent
70
80
  * experience for users when chatting with agents. It handles user input as well as displaying and nicely formatting
71
81
  * agent responses. Customization for inputs and outputs is provided via event handlers-like props.
72
82
  */
73
83
  export const ChatCommon = ({ ref, ...props }) => {
74
- const slyData = useRef({});
75
- const { id, currentUser, userImage, setIsAwaitingLlm, isAwaitingLlm, onChunkReceived, onStreamingStarted, onStreamingComplete, onSend, setPreviousResponse, targetAgent, legacyAgentEndpoint, agentPlaceholders = EMPTY, clearChatOnNewAgent = false, extraParams, backgroundColor, title, onClose, neuroSanURL, } = props;
76
- // Expose the handleStop method to parent components via ref for external control (e.g., to cancel chat requests)
77
- useImperativeHandle(ref, () => ({
78
- handleStop,
79
- }));
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;
80
85
  // MUI theme
81
86
  const theme = useTheme();
82
87
  const shadowColor = theme.palette.mode === "dark" ? theme.palette.common.white : theme.palette.common.black;
83
88
  // User LLM chat input
84
89
  const [chatInput, setChatInput] = useState("");
85
90
  // Previous user query (for "regenerate" feature)
86
- const [previousUserQuery, setPreviousUserQuery] = useState("");
91
+ const previousUserQuery = useRef("");
87
92
  // Chat output window contents
88
93
  const [chatOutput, setChatOutput] = useState([]);
89
94
  // To accumulate current response, which will be different from the contents of the output window if there is a
@@ -95,7 +100,7 @@ export const ChatCommon = ({ ref, ...props }) => {
95
100
  const chatInputRef = useRef(null);
96
101
  // Controller for cancelling fetch request
97
102
  const controller = useRef(null);
98
- // For tracking if we're autoscrolling. A button allows the user to enable or disable autoscrolling.
103
+ // For tracking if we're auto-scrolling. A button allows the user to enable or disable auto-scrolling.
99
104
  const [autoScrollEnabled, setAutoScrollEnabled] = useState(true);
100
105
  // ref for same
101
106
  const autoScrollEnabledRef = useRef(autoScrollEnabled);
@@ -105,18 +110,19 @@ export const ChatCommon = ({ ref, ...props }) => {
105
110
  const lastAIMessage = useRef("");
106
111
  // Ref for the final answer key, so we can highlight the accordion
107
112
  const finalAnswerKey = useRef("");
108
- // Use useRef here since we don't want changes in the chat history to trigger a re-render
109
- const chatHistory = useRef([]);
110
- /* Use useRef here since we don't want changes in the chat context to trigger a re-render
111
- Note on ChatContext vs ChatHistory:
112
- "Legacy" (not Neuro-san) agents use ChatHistory, which is a collection of messages of various types, Human, AI,
113
- System etc. It mimics the langchain field of the same name.
114
- Neuro-san agents deal in ChatContext, which is a more complex collection of chat histories, since more agents
115
- are involved.
116
- Both fields fulfill the same purpose: to maintain conversation state across multiple messages.
117
- */
118
- const chatContext = useRef(null);
113
+ // Persistent agent chat history store, which is where we store both kinds of chat histories
114
+ // (see store implementation for details)
115
+ const storedChatHistory = useAgentChatHistoryStore((state) => state?.history?.[targetAgent]);
116
+ const agentChatHistory = useMemo(() => storedChatHistory ?? { chatHistory: [], chatContext: null, slyData: {} }, [storedChatHistory]);
117
+ const [agentSampleQueries, setAgentSampleQueries] = useState([]);
118
+ // Access store for context items
119
+ const updateChatContext = useAgentChatHistoryStore((state) => state.updateChatContext);
120
+ const updateChatHistory = useAgentChatHistoryStore((state) => state.updateChatHistory);
121
+ const updateSlyData = useAgentChatHistoryStore((state) => state.updateSlyData);
122
+ const resetHistory = useAgentChatHistoryStore((state) => state.resetHistory);
123
+ // Ref to the item we think is the Final Answer from the agent
119
124
  const finalAnswerRef = useRef(null);
125
+ // Track state of "show thinking" toggle
120
126
  const [showThinking, setShowThinking] = useState(false);
121
127
  // Microphone state for voice input
122
128
  const [isMicOn, setIsMicOn] = useState(false);
@@ -147,6 +153,7 @@ export const ChatCommon = ({ ref, ...props }) => {
147
153
  const succeeded = useRef(false);
148
154
  const darkMode = theme.palette.mode === "dark";
149
155
  const { atelierDuneDark, a11yLight } = HLJS_THEMES;
156
+ const agentDisplayName = useMemo(() => cleanUpAgentName(removeTrailingUuid(targetAgent)), [targetAgent]);
150
157
  useEffect(() => {
151
158
  // Set up speech recognition
152
159
  const handlers = setupSpeechRecognition(setChatInput, setVoiceInputState, speechRecognitionRef);
@@ -165,21 +172,13 @@ export const ChatCommon = ({ ref, ...props }) => {
165
172
  useEffect(() => {
166
173
  // Scroll the final answer into view
167
174
  if (finalAnswerRef.current && !isAwaitingLlm) {
168
- const offset = 50;
169
- chatOutputRef.current.scrollTop = finalAnswerRef.current.offsetTop - offset;
175
+ chatOutputRef.current.scrollTop = finalAnswerRef.current.offsetTop - 50;
170
176
  return;
171
177
  }
172
178
  if (autoScrollEnabledRef.current && chatOutputRef?.current) {
173
179
  chatOutputRef.current.scrollTop = chatOutputRef.current.scrollHeight;
174
180
  }
175
- }, [chatOutput]);
176
- useEffect(() => {
177
- // Clear chat output on change of neuro-san URL
178
- // TODO: We want to revise this in the future to not need a useEffect
179
- setChatOutput([]);
180
- currentResponse.current = "";
181
- setShowThinking(false);
182
- }, [neuroSanURL]);
181
+ }, [chatOutput, isAwaitingLlm]);
183
182
  /**
184
183
  * Process a log line from the agent and format it nicely using the syntax highlighter and Accordion components.
185
184
  * By the time we get to here, it's assumed things like errors and termination conditions have already been handled.
@@ -192,7 +191,7 @@ export const ChatCommon = ({ ref, ...props }) => {
192
191
  * @param summary Used as the "title" for the accordion block. Something like an agent name or "Final Answer"
193
192
  * @returns A React component representing the log line (agent message)
194
193
  */
195
- const processLogLine = (logLine, summary, messageType, isFinalAnswer) => {
194
+ const processLogLine = useCallback((logLine, summary, messageType, isFinalAnswer) => {
196
195
  // extract the parts of the line
197
196
  let repairedJson;
198
197
  try {
@@ -229,146 +228,20 @@ export const ChatCommon = ({ ref, ...props }) => {
229
228
  0 9px 28px 8px ${alpha(shadowColor, 0.05)}`
230
229
  : "none",
231
230
  } }, hashedSummary));
232
- };
233
- const agentDisplayName = cleanUpAgentName(removeTrailingUuid(targetAgent));
234
- const introduceAgent = () => {
235
- /**
236
- * Introduce the agent to the user with a friendly greeting
237
- */
238
- updateOutput(_jsx(UserQueryDisplay, { userQuery: agentDisplayName, title: targetAgent, userImage: AGENT_IMAGE }));
239
- // Random greeting
240
- const greeting = AGENT_GREETINGS[Math.floor(Math.random() * AGENT_GREETINGS.length)];
241
- updateOutput(greeting);
242
- };
243
- /**
244
- * Render the connectivity info as a list of origins and their tools
245
- * @param connectivityInfo The connectivity info to render
246
- * @returns A ReactNode representing the connectivity info with agents and their tools
247
- */
248
- const renderConnectivityInfo = (connectivityInfo) => (_jsx(_Fragment, { children: connectivityInfo
249
- // Don't show connection to self
250
- .filter((info) => info.origin.toLowerCase() !== targetAgent.toLowerCase())
251
- // Sort by origin name
252
- .sort((a, b) => a.origin.localeCompare(b.origin))
253
- // Render each origin and its tools
254
- .map((info) => (_jsxs("li", { id: info.origin, children: [_jsx("b", { id: info.origin, children: info.origin }), _jsx("ul", { id: `${info.origin}-tools`, style: { marginLeft: "8px" }, children: info?.tools?.map((tool) => (_jsx("li", { id: tool, children: tool }, tool))) })] }, info.origin))) }));
255
- /**
256
- * Render sample queries as clickable chips. Agents may or may not have sample queries defined.
257
- * @param sampleQueries The sample queries to render (from "connectivity" API)
258
- * @returns A ReactNode representing the sample queries as clickable chips. If a user clicks a chip, it will
259
- * send the query to the agent.
260
- */
261
- const renderSampleQueries = (sampleQueries) => {
262
- return sampleQueries?.length > 0 ? (_jsx(Box, { id: "sample-queries-box", sx: { marginTop: "2rem", marginBottom: "1rem" }, children: sampleQueries.slice(0, MAX_SAMPLE_QUERIES).map((query) => (_jsx(Tooltip, { id: `tooltip-${query}`, title: `Click to send query: "${query}"`, children: _jsx(Chip, { id: `sample-query-${query}`, label: query.length > QUERY_TRUNCATE_LENGTH
263
- ? `${query.slice(0, QUERY_TRUNCATE_LENGTH)}...`
264
- : query, onClick: async () => {
265
- await handleSend(query);
266
- }, sx: {
267
- color: "var(--bs-white)",
268
- marginRight: "1rem",
269
- marginBottom: "1rem",
270
- backgroundColor: "var(--bs-accent1-medium)",
271
- "&:hover": {
272
- backgroundColor: "var(--bs-accent1-dark)",
273
- },
274
- } }, query) }, `tooltip-${query}`))) })) : null;
275
- };
276
- useEffect(() => {
277
- const newAgent = async () => {
278
- if (clearChatOnNewAgent) {
279
- // New agent, so clear chat context if desired
280
- chatContext.current = null;
281
- currentResponse.current = "";
282
- slyData.current = null;
283
- setChatOutput([]);
284
- }
285
- // Introduce the agent to the user
286
- introduceAgent();
287
- // if not neuro san agent return since we won't get connectivity info
288
- if (isLegacyAgentType(targetAgent)) {
289
- return;
290
- }
291
- let agentFunction;
292
- // It is a Neuro-san agent, so get the function and connectivity info
293
- try {
294
- agentFunction = await getAgentFunction(neuroSanURL, targetAgent, currentUser);
295
- }
296
- catch {
297
- // For now, just return. May be a legacy agent without a functional description in Neuro-san.
298
- return;
299
- }
300
- try {
301
- const connectivity = await getConnectivity(neuroSanURL, targetAgent, currentUser);
302
- updateOutput(_jsx(MUIAccordion, { id: `${id}-agent-details`, sx: {
303
- marginTop: "1rem",
304
- marginBottom: "1rem",
305
- }, items: [
306
- {
307
- title: "Network Details",
308
- content: [
309
- `My description is: "${agentFunction?.function?.description}"`,
310
- _jsx("h6", { id: "connectivity-header", style: { marginTop: "1rem" }, children: "I can connect you to the following agents" }, "item-1"),
311
- _jsx("ul", { id: "connectivity-list", "aria-labelledby": "connectivity-header", style: { marginTop: "1rem" }, children: renderConnectivityInfo(connectivity?.connectivity_info.concat()) }, "item-2"),
312
- ],
313
- },
314
- ] }));
315
- updateOutput(renderSampleQueries(connectivity?.metadata?.["sample_queries"] ?? []));
316
- }
317
- catch (e) {
318
- sendNotification(NotificationType.error, `Failed to get connectivity info for ${cleanUpAgentName(targetAgent)}. Error: ${e}`);
319
- }
320
- };
321
- if (targetAgent) {
322
- void newAgent();
323
- }
324
- }, [targetAgent]);
231
+ }, [a11yLight, atelierDuneDark, darkMode, shadowColor, shouldWrapOutput]);
325
232
  /**
326
- * Handles adding content to the output window.
233
+ * Handles adding content to the output window. We only store the last MAX_CHAT_OUTPUT_ITEMS items to keep
234
+ * memory usage down.
327
235
  * @param node A ReactNode to add to the output window -- text, spinner, etc. but could also be simple string
328
- * @returns Nothing, but updates the output window with the new content. Updates currentResponse as a side effect.
236
+ * @returns Nothing, but updates the output window with the new content.
329
237
  */
330
- const updateOutput = (node) => {
331
- currentResponse.current += node;
332
- setChatOutput((currentOutput) => [...currentOutput, node]);
333
- };
334
- /**
335
- * Reset the state of the component. This is called after a request is completed, regardless of success or failure.
336
- */
337
- const resetState = () => {
338
- // Reset state, whatever happened during request
339
- setIsAwaitingLlm(false);
340
- setChatInput("");
341
- lastAIMessage.current = "";
342
- finalAnswerRef.current = null;
343
- // Get agent name, either from the enum (Neuro-san) or from the targetAgent string directly (legacy)
344
- setPreviousResponse?.(targetAgent, currentResponse.current);
345
- currentResponse.current = "";
346
- };
347
- const handleStop = () => {
348
- try {
349
- controller?.current?.abort();
350
- controller.current = null;
351
- updateOutput(_jsx(MUIAlert, { id: "opp-finder-error-occurred-alert", severity: "warning", children: "Request cancelled." }));
352
- }
353
- finally {
354
- resetState();
355
- }
356
- };
357
- // Regex to check if user has typed anything besides whitespace
358
- const userInputEmpty = !chatInput || chatInput.length === 0 || hasOnlyWhitespace(chatInput);
359
- // Enable Send button when there is user input and not awaiting a response
360
- const shouldEnableSendButton = !userInputEmpty && !isAwaitingLlm;
361
- // Enable regenerate button when there is a previous query to resent, and we're not awaiting a response
362
- const shouldEnableRegenerateButton = previousUserQuery && !isAwaitingLlm;
363
- // Enable Clear Chat button if not awaiting response and there is chat output to clear
364
- const enableClearChatButton = !isAwaitingLlm && chatOutput.length > 0;
365
- /**
366
- * Extract the final answer from the response from a legacy agent
367
- * @param response The response from the legacy agent
368
- * @returns The final answer from the agent, if it exists or null if it doesn't
369
- */
370
- const extractFinalAnswer = (response) => /Final Answer: (?<finalAnswerText>.*)/su.exec(response)?.groups?.["finalAnswerText"];
371
- const handleChunk = (chunk) => {
238
+ const updateOutput = useCallback((node) => {
239
+ setChatOutput((current) => {
240
+ const next = [...current, node];
241
+ return next.length > MAX_CHAT_OUTPUT_ITEMS ? next.slice(-MAX_CHAT_OUTPUT_ITEMS) : next;
242
+ });
243
+ }, []);
244
+ const handleChunk = useCallback((chunk) => {
372
245
  // Give container a chance to process the chunk first
373
246
  const onChunkReceivedResult = onChunkReceived?.(chunk) ?? true;
374
247
  succeeded.current = succeeded.current || onChunkReceivedResult;
@@ -376,6 +249,7 @@ export const ChatCommon = ({ ref, ...props }) => {
376
249
  if (isLegacyAgentType(targetAgent)) {
377
250
  // Display output as-is
378
251
  updateOutput(chunk);
252
+ currentResponse.current += chunk;
379
253
  // Check for Final Answer from legacy agent
380
254
  const finalAnswerMatch = extractFinalAnswer(currentResponse.current);
381
255
  if (finalAnswerMatch) {
@@ -390,17 +264,15 @@ export const ChatCommon = ({ ref, ...props }) => {
390
264
  // But don't want to spam output by logging errors for every bad message.
391
265
  return;
392
266
  }
267
+ // Shallow merge existing slyData with incoming chatMessage.sly_data
268
+ if (chatMessage.sly_data) {
269
+ updateSlyData(targetAgent, chatMessage.sly_data);
270
+ }
393
271
  // It's a ChatMessage. Does it have chat context? Only AGENT_FRAMEWORK messages can have chat context.
394
272
  if (chatMessage.type === ChatMessageType.AGENT_FRAMEWORK && chatMessage.chat_context) {
395
273
  // Save the chat context, potentially overwriting any previous ones we received during this session.
396
274
  // We only care about the last one received.
397
- chatContext.current = chatMessage.chat_context;
398
- // Nothing more to do with this message. It's just a message to give us the chat context, so return
399
- return;
400
- }
401
- // Merge slyData.current with incoming chatMessage.sly_data
402
- if (chatMessage.sly_data) {
403
- slyData.current = { ...slyData.current, ...chatMessage.sly_data };
275
+ updateChatContext(targetAgent, chatMessage.chat_context);
404
276
  }
405
277
  // Check if there is an error block in the "structure" field of the chat message.
406
278
  if (chatMessage.structure) {
@@ -419,9 +291,36 @@ export const ChatCommon = ({ ref, ...props }) => {
419
291
  ? cleanUpAgentName(chatMessage.origin[chatMessage.origin.length - 1].tool)
420
292
  : "Agent message";
421
293
  updateOutput(processLogLine(chatMessage.text, agentName, chatMessage.type));
294
+ currentResponse.current += chatMessage.text;
422
295
  }
423
- };
424
- const doQueryLoop = async (query) => {
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]);
307
+ /**
308
+ * Reset the state of the component. This is called after a request is completed, regardless of success or failure.
309
+ */
310
+ const resetState = useCallback(() => {
311
+ // Reset state, whatever happened during request
312
+ setIsAwaitingLlm(false);
313
+ setChatInput("");
314
+ lastAIMessage.current = "";
315
+ finalAnswerRef.current = null;
316
+ // Get agent name, either from the enum (Neuro-san) or from the targetAgent string directly (legacy)
317
+ setPreviousResponse?.(targetAgent, currentResponse.current);
318
+ currentResponse.current = "";
319
+ }, [setIsAwaitingLlm, setPreviousResponse, targetAgent]);
320
+ /*
321
+ * The main logic for sending a query to the server, with retries on errors.
322
+ */
323
+ const doRetryLoop = useCallback(async (query) => {
425
324
  succeeded.current = false;
426
325
  let attemptNumber = 0;
427
326
  let wasAborted = false;
@@ -431,15 +330,16 @@ export const ChatCommon = ({ ref, ...props }) => {
431
330
  attemptNumber += 1;
432
331
  // Check which agent type we are dealing with
433
332
  if (isLegacyAgentType(targetAgent)) {
434
- // It's a legacy agent (these go directly to the LLM and are different from the Neuro-san agents).
333
+ // It's a legacy agent (these go directly to the LLM and are different from
334
+ // the Neuro-san agents).
435
335
  // Send the chat query to the server. This will block until the stream ends from the server
436
- await sendLlmRequest(handleChunk, controller?.current.signal, legacyAgentEndpoint, extraParams, query, chatHistory.current, null, StreamingUnit.Chunk);
336
+ await sendLlmRequest(handleChunk, controller?.current.signal, legacyAgentEndpoint, extraParams, query, agentChatHistory.chatHistory, null, StreamingUnit.Chunk);
437
337
  }
438
338
  else {
439
339
  // It's a Neuro-san agent.
440
340
  // Some coded tools (data generator...) expect the username provided in slyData.
441
- const slyDataWithUserName = { ...slyData.current, login: currentUser };
442
- await sendChatQuery(neuroSanURL, controller?.current.signal, query, targetAgent, handleChunk, chatContext.current, slyDataWithUserName, currentUser, StreamingUnit.Line);
341
+ const slyDataWithUserName = { ...agentChatHistory?.slyData, ...extraSlyData, login: currentUser };
342
+ await sendChatQuery(neuroSanURL, controller?.current.signal, query, targetAgent, handleChunk, agentChatHistory.chatContext, slyDataWithUserName, currentUser, StreamingUnit.Line);
443
343
  }
444
344
  }
445
345
  catch (error) {
@@ -457,24 +357,35 @@ export const ChatCommon = ({ ref, ...props }) => {
457
357
  }
458
358
  }
459
359
  } while (attemptNumber < MAX_AGENT_RETRIES && !succeeded.current);
460
- return { wasAborted };
461
- };
462
- const handleSend = async (query) => {
463
- // Record user query in chat history
464
- chatHistory.current = [...chatHistory.current, new HumanMessage(previousUserQuery)];
360
+ return wasAborted;
361
+ }, [
362
+ agentChatHistory,
363
+ currentUser,
364
+ extraParams,
365
+ extraSlyData,
366
+ handleChunk,
367
+ legacyAgentEndpoint,
368
+ neuroSanURL,
369
+ targetAgent,
370
+ updateOutput,
371
+ ]);
372
+ const handleSend = useCallback(async (query) => {
373
+ // Record user query in chat history. Discard anything beyond MAX_CHAT_HISTORY_ITEMS
374
+ const userQueryMessage = new HumanMessage({ content: query, id: uuid() });
375
+ updateChatHistory(targetAgent, [userQueryMessage]);
465
376
  // Allow parent to intercept and modify the query before sending if needed
466
377
  const queryToSend = onSend?.(query) ?? query;
467
378
  // Save query for "regenerate" use. Again we save the real user input, not the modified query. It will again
468
379
  // get intercepted and re-modified (if applicable) on "regenerate".
469
- setPreviousUserQuery(query);
380
+ previousUserQuery.current = query;
470
381
  setIsAwaitingLlm(true);
471
382
  // Always start output by echoing user query.
472
383
  // Note: we display the original user query, not the modified one. The modified one could be a monstrosity
473
- // that we generated behind their back. Ultimately, we shouldn't need to generate a fake query on behalf of the
474
- // user, but currently we do for orchestration.
384
+ // that we generated behind their back. Ultimately, we shouldn't need to generate a fake query on behalf
385
+ // of the user, but currently we do for orchestration.
475
386
  updateOutput(_jsx(UserQueryDisplay, { userQuery: query, title: currentUser, userImage: userImage }));
476
387
  // Add ID block for agent
477
- updateOutput(_jsx(UserQueryDisplay, { userQuery: cleanUpAgentName(removeTrailingUuid(targetAgent)), title: targetAgent, userImage: AGENT_IMAGE }));
388
+ updateOutput(_jsx(UserQueryDisplay, { userQuery: agentDisplayName, title: targetAgent, userImage: AGENT_IMAGE }));
478
389
  // Allow clients to do something when streaming starts
479
390
  onStreamingStarted?.();
480
391
  // Set up the abort controller
@@ -483,13 +394,14 @@ export const ChatCommon = ({ ref, ...props }) => {
483
394
  if (showThinking) {
484
395
  updateOutput(_jsx(MUIAccordion, { id: "initiating-orchestration-accordion", items: [
485
396
  {
486
- title: `Contacting ${cleanUpAgentName(targetAgent)}...`,
397
+ title: `Contacting ${agentDisplayName}...`,
487
398
  content: `Query: ${queryToSend}`,
488
399
  },
489
400
  ], sx: { marginBottom: "1rem" } }));
490
401
  }
491
402
  try {
492
- const { wasAborted } = await doQueryLoop(queryToSend);
403
+ // Invoke the logic to send the request and retry as necessary
404
+ const wasAborted = await doRetryLoop(queryToSend);
493
405
  if (!wasAborted && !succeeded.current) {
494
406
  updateOutput(_jsx(MUIAlert, { id: "opp-finder-max-retries-exceeded-alert", severity: "error", children: `Gave up after ${MAX_AGENT_RETRIES} attempts.` }));
495
407
  }
@@ -500,118 +412,217 @@ export const ChatCommon = ({ ref, ...props }) => {
500
412
  updateOutput(" \n\n");
501
413
  }
502
414
  updateOutput(_jsx("div", { id: "final-answer-div", ref: finalAnswerRef, style: { marginBottom: "1rem" }, children: processLogLine(lastAIMessage.current, "Final Answer", ChatMessageType.AI, true) }));
415
+ // Record bot answer in history.
416
+ if (currentResponse?.current?.length > 0) {
417
+ updateChatHistory(targetAgent, [new AIMessage({ content: lastAIMessage.current, id: uuid() })]);
418
+ }
419
+ }
420
+ else if (isLegacyAgentType(targetAgent) && currentResponse.current.length > 0) {
421
+ // It's a legacy agent that didn't provide a "Final Answer", so just record the whole response
422
+ // as the bot answer in that case.
423
+ updateChatHistory(targetAgent, [new AIMessage({ content: currentResponse.current, id: uuid() })]);
503
424
  }
504
425
  // Add a blank line after response
505
426
  updateOutput("\n");
506
- // Record bot answer in history.
507
- if (currentResponse?.current?.length > 0) {
508
- chatHistory.current = [...chatHistory.current, new AIMessage(currentResponse.current)];
509
- }
510
427
  }
511
428
  finally {
512
429
  resetState();
513
430
  // Allow parent components to do something when streaming is complete
514
431
  onStreamingComplete?.();
515
432
  }
516
- };
433
+ }, [
434
+ agentDisplayName,
435
+ currentUser,
436
+ doRetryLoop,
437
+ onSend,
438
+ onStreamingComplete,
439
+ onStreamingStarted,
440
+ processLogLine,
441
+ resetState,
442
+ setIsAwaitingLlm,
443
+ showThinking,
444
+ targetAgent,
445
+ updateChatHistory,
446
+ updateOutput,
447
+ userImage,
448
+ ]);
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
+ const handleStop = useCallback(() => {
481
+ try {
482
+ controller?.current?.abort();
483
+ controller.current = null;
484
+ updateOutput(_jsx(MUIAlert, { id: "opp-finder-error-occurred-alert", severity: "warning", children: "Request cancelled." }));
485
+ }
486
+ finally {
487
+ resetState();
488
+ }
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]);
494
+ // Regex to check if user has typed anything besides whitespace
495
+ const userInputEmpty = !chatInput || chatInput.length === 0 || hasOnlyWhitespace(chatInput);
496
+ // Enable Send button when there is user input and not awaiting a response
497
+ const shouldEnableSendButton = !userInputEmpty && !isAwaitingLlm;
498
+ // Enable regenerate button when there is a previous query to resent, and we're not awaiting a response
499
+ const shouldEnableRegenerateButton = previousUserQuery && !isAwaitingLlm;
500
+ // Enable Clear Chat button if not awaiting response and there is chat output to clear
501
+ const enableClearChatButton = !isAwaitingLlm && chatOutput.length > 0;
517
502
  const getPlaceholder = () => !targetAgent ? null : agentPlaceholders[targetAgent] || `Chat with ${agentDisplayName}`;
518
- return (_jsxs(Box, { id: `llm-chat-${id}`, sx: {
503
+ const handleClearChat = useCallback(() => {
504
+ setChatOutput([]);
505
+ resetHistory(targetAgent);
506
+ previousUserQuery.current = "";
507
+ currentResponse.current = "";
508
+ 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
+ } }));
541
+ const getTitle = () => (_jsxs(Box, { id: `llm-chat-title-container-${id}`, sx: {
542
+ alignItems: "center",
543
+ borderTopLeftRadius: "var(--bs-border-radius)",
544
+ borderTopRightRadius: "var(--bs-border-radius)",
545
+ display: "flex",
546
+ justifyContent: "space-between",
547
+ paddingLeft: "1rem",
548
+ paddingRight: "0.5rem",
549
+ paddingTop: "0.25rem",
550
+ paddingBottom: "0.25rem",
551
+ }, 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
+ 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" } }) }) })] }));
553
+ const getResponseBox = () => (_jsxs(Box, { id: "llm-response-div", sx: {
554
+ ...divStyle,
555
+ border: "var(--bs-border-width) var(--bs-border-style)",
556
+ borderRadius: "var(--bs-border-radius)",
519
557
  display: "flex",
520
- flexDirection: "column",
521
558
  flexGrow: 1,
522
559
  height: "100%",
560
+ margin: "10px",
523
561
  position: "relative",
524
- }, children: [!targetAgent && (_jsx(Box, { id: "chat-disabled-overlay", sx: {
525
- position: "absolute",
526
- top: 0,
527
- left: 0,
528
- right: 0,
529
- bottom: 0,
530
- zIndex: theme.zIndex.modal - 1,
531
- cursor: "not-allowed",
532
- // Capture all pointer events to prevent interaction with the chat when no agent is selected
533
- pointerEvents: "all",
534
- } })), _jsxs(Box, { id: "chat-content", sx: {
562
+ overflowY: "auto",
563
+ }, children: [getOptionsButtons(), _jsxs(Box, { id: "llm-responses", ref: chatOutputRef, sx: {
564
+ backgroundColor: backgroundColor || undefined,
565
+ borderWidth: "1px",
566
+ borderRadius: "0.5rem",
567
+ fontSize: "smaller",
568
+ resize: "none",
569
+ overflowY: "auto", // Enable vertical scrollbar
570
+ paddingBottom: "60px",
571
+ paddingTop: "7.5px",
572
+ paddingLeft: "15px",
573
+ paddingRight: "15px",
574
+ 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: {
576
+ color: "var(--bs-primary)",
577
+ }, size: "1rem" })] }))] }), _jsx(ControlButtons, { handleClearChat: handleClearChat, enableClearChatButton: enableClearChatButton, isAwaitingLlm: isAwaitingLlm, handleSend: handleSend, handleStop: handleStop, previousUserQuery: previousUserQuery.current, shouldEnableRegenerateButton: shouldEnableRegenerateButton })] }));
578
+ const getUserInputBox = () => (_jsxs(Box, { id: "user-input-div", sx: {
579
+ ...divStyle,
580
+ display: "flex",
581
+ margin: "10px",
582
+ alignItems: "flex-end",
583
+ position: "relative",
584
+ }, children: [_jsx(Input, { autoComplete: "off", id: "user-input", multiline: true, placeholder: getPlaceholder(), ref: chatInputRef, sx: {
585
+ border: "var(--bs-border-style) var(--bs-border-width) var(--bs-gray-light)",
586
+ borderRadius: "var(--bs-border-radius)",
535
587
  display: "flex",
536
- flexDirection: "column",
537
588
  flexGrow: 1,
538
- height: "100%",
539
- opacity: targetAgent ? 1 : 0.4,
540
- pointerEvents: targetAgent ? "auto" : "none",
541
- }, children: [title && (_jsxs(Box, { id: `llm-chat-title-container-${id}`, sx: {
542
- alignItems: "center",
543
- borderTopLeftRadius: "var(--bs-border-radius)",
544
- borderTopRightRadius: "var(--bs-border-radius)",
545
- display: "flex",
546
- justifyContent: "space-between",
547
- paddingLeft: "1rem",
548
- paddingRight: "0.5rem",
549
- paddingTop: "0.25rem",
550
- paddingBottom: "0.25rem",
551
- }, 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}` }) }))] })), _jsxs(Box, { id: "llm-response-div", sx: {
552
- ...divStyle,
553
- border: "var(--bs-border-width) var(--bs-border-style)",
554
- borderRadius: "var(--bs-border-radius)",
555
- display: "flex",
556
- flexGrow: 1,
557
- height: "100%",
558
- margin: "10px",
559
- position: "relative",
560
- overflowY: "auto",
561
- }, 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" } }) }) }), _jsxs(Box, { id: "llm-responses", ref: chatOutputRef, sx: {
562
- backgroundColor: backgroundColor || undefined,
563
- borderWidth: "1px",
564
- borderRadius: "0.5rem",
565
- fontSize: "smaller",
566
- resize: "none",
567
- overflowY: "auto", // Enable vertical scrollbar
568
- paddingBottom: "60px",
569
- paddingTop: "7.5px",
570
- paddingLeft: "15px",
571
- paddingRight: "15px",
572
- width: "100%",
573
- }, tabIndex: -1, children: [_jsx(FormattedMarkdown, { id: `${id}-formatted-markdown`, nodesList: chatOutput.map((item) => {
574
- if (isValidElement(item) && item.type === MUIAccordion) {
575
- const shouldShow = showThinking || item.key === finalAnswerKey.current;
576
- return (_jsx(Box, { sx: { display: shouldShow ? "block" : "none" }, children: item }, item.key));
577
- }
578
- return item;
579
- }), style: darkMode ? atelierDuneDark : a11yLight, wrapLongLines: shouldWrapOutput }), 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: {
580
- color: "var(--bs-primary)",
581
- }, size: "1rem" })] }))] }), _jsx(ControlButtons, { clearChatOnClickCallback: () => {
582
- setChatOutput([]);
583
- chatHistory.current = [];
584
- chatContext.current = null;
585
- setPreviousUserQuery("");
586
- currentResponse.current = "";
587
- lastAIMessage.current = "";
588
- introduceAgent();
589
- }, enableClearChatButton: enableClearChatButton, isAwaitingLlm: isAwaitingLlm, handleSend: handleSend, handleStop: handleStop, previousUserQuery: previousUserQuery, shouldEnableRegenerateButton: shouldEnableRegenerateButton })] }), _jsxs(Box, { id: "user-input-div", style: { ...divStyle, display: "flex", margin: "10px", alignItems: "flex-end", position: "relative" }, children: [_jsx(Input, { autoComplete: "off", id: "user-input", multiline: true, placeholder: getPlaceholder(), ref: chatInputRef, sx: {
590
- border: "var(--bs-border-style) var(--bs-border-width) var(--bs-gray-light)",
591
- borderRadius: "var(--bs-border-radius)",
592
- display: "flex",
593
- flexGrow: 1,
594
- fontSize: "smaller",
595
- marginRight: "0.75rem",
596
- paddingBottom: "0.5rem",
597
- paddingTop: "0.5rem",
598
- paddingLeft: "1rem",
599
- paddingRight: "1rem",
600
- transition: "margin-right 0.2s",
601
- }, onChange: (event) => {
602
- setChatInput(event.target.value);
603
- }, onKeyDown: async (event) => {
604
- if (event.key === "Enter" && !event.shiftKey) {
605
- event.preventDefault();
606
- await handleSend(chatInput);
607
- }
608
- }, value: chatInput, endAdornment: _jsxs(InputAdornment, { id: "input-adornments", position: "end", disableTypography: true, children: [voiceInputState.isProcessingSpeech && (_jsx(CircularProgress, { size: 16, sx: {
609
- color: "var(--bs-primary)",
610
- marginRight: "0.5rem",
611
- } })), _jsx(IconButton, { id: "clear-input-button", onClick: () => {
612
- setChatInput("");
613
- }, sx: {
614
- color: "var(--bs-primary)",
615
- opacity: userInputEmpty ? "25%" : "100%",
616
- }, disabled: userInputEmpty, tabIndex: -1, edge: "end", children: _jsx(ClearIcon, { id: "clear-input-icon" }) })] }) }), _jsx(MicrophoneButton, { isMicOn: isMicOn, onMicToggle: setIsMicOn, speechRecognitionRef: speechRecognitionRef, voiceInputState: voiceInputState, setVoiceInputState: setVoiceInputState }), _jsx(SendButton, { enableSendButton: shouldEnableSendButton, id: "submit-query-button", onClickCallback: () => handleSend(chatInput) })] })] })] }));
589
+ fontSize: "smaller",
590
+ marginRight: "0.75rem",
591
+ paddingBottom: "0.5rem",
592
+ paddingTop: "0.5rem",
593
+ paddingLeft: "1rem",
594
+ paddingRight: "1rem",
595
+ transition: "margin-right 0.2s",
596
+ }, onChange: (event) => {
597
+ setChatInput(event.target.value);
598
+ }, onKeyDown: async (event) => {
599
+ if (event.key === "Enter" && !event.shiftKey) {
600
+ event.preventDefault();
601
+ await handleSend(chatInput);
602
+ }
603
+ }, value: chatInput, endAdornment: _jsxs(InputAdornment, { id: "input-adornments", position: "end", disableTypography: true, children: [voiceInputState.isProcessingSpeech && (_jsx(CircularProgress, { size: 16, sx: {
604
+ color: "var(--bs-primary)",
605
+ marginRight: "0.5rem",
606
+ } })), _jsx(IconButton, { id: "clear-input-button", onClick: () => {
607
+ setChatInput("");
608
+ }, sx: {
609
+ color: "var(--bs-primary)",
610
+ opacity: userInputEmpty ? "25%" : "100%",
611
+ }, disabled: userInputEmpty, tabIndex: -1, edge: "end", children: _jsx(ClearIcon, { id: "clear-input-icon" }) })] }) }), _jsx(MicrophoneButton, { isMicOn: isMicOn, onMicToggle: setIsMicOn, speechRecognitionRef: speechRecognitionRef, voiceInputState: voiceInputState, setVoiceInputState: setVoiceInputState }), _jsx(SendButton, { enableSendButton: shouldEnableSendButton, id: "submit-query-button", onClickCallback: () => handleSend(chatInput) })] }));
612
+ const getChatBox = () => (_jsxs(Box, { id: `llm-chat-${id}`, sx: {
613
+ display: "flex",
614
+ flexDirection: "column",
615
+ flexGrow: 1,
616
+ height: "100%",
617
+ opacity: targetAgent ? 1 : 0.4,
618
+ pointerEvents: targetAgent ? "auto" : "none",
619
+ position: "relative",
620
+ }, children: [title && getTitle(), getResponseBox(), getUserInputBox()] }));
621
+ return (_jsx(Box, { id: `llm-chat-${id}`, sx: {
622
+ display: "flex",
623
+ flexDirection: "column",
624
+ flexGrow: 1,
625
+ height: "100%",
626
+ position: "relative",
627
+ }, children: targetAgent ? getChatBox() : getNoAgentOverlay() }));
617
628
  };