@cognizant-ai-lab/ui-common 1.5.0 → 1.6.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 (81) hide show
  1. package/dist/Theme/Theme.js +3 -3
  2. package/dist/components/AgentChat/ChatCommon/ChatCommon.d.ts +11 -1
  3. package/dist/components/AgentChat/ChatCommon/ChatCommon.js +284 -285
  4. package/dist/components/AgentChat/ChatCommon/ChatHistory.d.ts +1 -7
  5. package/dist/components/AgentChat/ChatCommon/ChatHistory.js +33 -22
  6. package/dist/components/AgentChat/ChatCommon/ControlButtons.js +2 -2
  7. package/dist/components/AgentChat/ChatCommon/Conversation.d.ts +13 -0
  8. package/dist/components/AgentChat/ChatCommon/Conversation.js +80 -0
  9. package/dist/components/AgentChat/ChatCommon/ConversationTurn.d.ts +23 -0
  10. package/dist/components/AgentChat/ChatCommon/ConversationTurn.js +11 -0
  11. package/dist/components/AgentChat/ChatCommon/FormattedMarkdown.js +5 -3
  12. package/dist/components/AgentChat/ChatCommon/SampleQueries.d.ts +3 -0
  13. package/dist/components/AgentChat/ChatCommon/SampleQueries.js +6 -3
  14. package/dist/components/AgentChat/ChatCommon/Thinking.d.ts +12 -0
  15. package/dist/components/AgentChat/ChatCommon/Thinking.js +51 -0
  16. package/dist/components/AgentChat/Common/LlmChatButton.d.ts +2 -2
  17. package/dist/components/AgentChat/Common/Types.d.ts +6 -5
  18. package/dist/components/AgentChat/Common/Types.js +5 -0
  19. package/dist/components/AgentChat/Common/Utils.d.ts +1 -1
  20. package/dist/components/AgentChat/Common/Utils.js +14 -9
  21. package/dist/components/Common/AccordionLite.d.ts +14 -0
  22. package/dist/components/Common/AccordionLite.js +25 -0
  23. package/dist/components/Common/ConfirmationModal.d.ts +1 -0
  24. package/dist/components/Common/ConfirmationModal.js +1 -1
  25. package/dist/components/Common/CustomerLogo.js +1 -1
  26. package/dist/components/Common/MUIAlert.d.ts +1 -0
  27. package/dist/components/Common/MUIAlert.js +3 -4
  28. package/dist/components/Common/Navbar.d.ts +2 -1
  29. package/dist/components/Common/Navbar.js +8 -4
  30. package/dist/components/Common/notification.d.ts +1 -1
  31. package/dist/components/Common/notification.js +17 -12
  32. package/dist/components/MultiAgentAccelerator/AgentFlow.d.ts +8 -0
  33. package/dist/components/MultiAgentAccelerator/AgentFlow.js +282 -82
  34. package/dist/components/MultiAgentAccelerator/AgentNode.d.ts +3 -1
  35. package/dist/components/MultiAgentAccelerator/AgentNode.js +64 -28
  36. package/dist/components/MultiAgentAccelerator/AgentNodePopup.d.ts +1 -4
  37. package/dist/components/MultiAgentAccelerator/AgentNodePopup.js +4 -5
  38. package/dist/components/MultiAgentAccelerator/GraphLayouts.js +19 -9
  39. package/dist/components/MultiAgentAccelerator/MultiAgentAccelerator.d.ts +2 -2
  40. package/dist/components/MultiAgentAccelerator/MultiAgentAccelerator.js +268 -60
  41. package/dist/components/MultiAgentAccelerator/Sidebar/AgentNetworkTreeItem.d.ts +1 -0
  42. package/dist/components/MultiAgentAccelerator/Sidebar/AgentNetworkTreeItem.js +28 -12
  43. package/dist/components/MultiAgentAccelerator/Sidebar/Sidebar.d.ts +1 -0
  44. package/dist/components/MultiAgentAccelerator/Sidebar/Sidebar.js +21 -5
  45. package/dist/components/MultiAgentAccelerator/Sidebar/TreeBuilder.d.ts +4 -3
  46. package/dist/components/MultiAgentAccelerator/Sidebar/TreeBuilder.js +8 -2
  47. package/dist/components/MultiAgentAccelerator/TemporaryNetworks.d.ts +19 -2
  48. package/dist/components/MultiAgentAccelerator/TemporaryNetworks.js +40 -5
  49. package/dist/components/MultiAgentAccelerator/ThoughtBubbleOverlay.js +27 -14
  50. package/dist/components/MultiAgentAccelerator/Tour/MainTourSteps.d.ts +7 -0
  51. package/dist/components/MultiAgentAccelerator/Tour/MainTourSteps.js +88 -0
  52. package/dist/components/MultiAgentAccelerator/const.d.ts +7 -10
  53. package/dist/components/MultiAgentAccelerator/const.js +9 -10
  54. package/dist/const.d.ts +5 -1
  55. package/dist/const.js +5 -2
  56. package/dist/controller/agent/Agent.d.ts +10 -0
  57. package/dist/controller/agent/Agent.js +17 -1
  58. package/dist/controller/llm/LlmChat.js +2 -2
  59. package/dist/index.d.ts +0 -1
  60. package/dist/index.js +0 -1
  61. package/dist/state/TemporaryNetworks.d.ts +5 -15
  62. package/dist/state/TemporaryNetworks.js +15 -34
  63. package/dist/state/Tour.d.ts +29 -0
  64. package/dist/state/Tour.js +22 -0
  65. package/dist/state/UserInfo.d.ts +2 -1
  66. package/dist/tsconfig.build.tsbuildinfo +1 -1
  67. package/dist/utils/Authentication.js +12 -3
  68. package/dist/utils/File.d.ts +7 -0
  69. package/dist/utils/File.js +14 -3
  70. package/dist/utils/text.js +2 -2
  71. package/dist/utils/title.js +1 -1
  72. package/dist/utils/zIndexLayers.js +3 -0
  73. package/package.json +15 -11
  74. package/dist/components/AgentChat/ChatCommon/AgentConnectivity.d.ts +0 -14
  75. package/dist/components/AgentChat/ChatCommon/AgentConnectivity.js +0 -23
  76. package/dist/components/AgentChat/ChatCommon/Greetings.d.ts +0 -1
  77. package/dist/components/AgentChat/ChatCommon/Greetings.js +0 -38
  78. package/dist/components/AgentChat/ChatCommon/UserQueryDisplay.d.ts +0 -7
  79. package/dist/components/AgentChat/ChatCommon/UserQueryDisplay.js +0 -32
  80. package/dist/components/Common/LlmChatOptionsButton.d.ts +0 -6
  81. package/dist/components/Common/LlmChatOptionsButton.js +0 -31
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /*
3
3
  Copyright 2025 Cognizant Technology Solutions Corp, www.cognizant.com.
4
4
 
@@ -18,52 +18,43 @@ limitations under the License.
18
18
  * See main function description.
19
19
  */
20
20
  import { AIMessage, HumanMessage } from "@langchain/core/messages";
21
- import AccountTreeIcon from "@mui/icons-material/AccountTree";
22
21
  import ClearIcon from "@mui/icons-material/Clear";
23
22
  import CloseIcon from "@mui/icons-material/Close";
24
- import VerticalAlignBottomIcon from "@mui/icons-material/VerticalAlignBottom";
25
- import WrapTextIcon from "@mui/icons-material/WrapText";
23
+ import TuneIcon from "@mui/icons-material/Tune";
26
24
  import Box from "@mui/material/Box";
25
+ import Checkbox from "@mui/material/Checkbox";
27
26
  import CircularProgress from "@mui/material/CircularProgress";
28
27
  import IconButton from "@mui/material/IconButton";
29
28
  import Input from "@mui/material/Input";
30
29
  import InputAdornment from "@mui/material/InputAdornment";
31
- import { alpha, useTheme } from "@mui/material/styles";
30
+ import ListItemIcon from "@mui/material/ListItemIcon";
31
+ import ListItemText from "@mui/material/ListItemText";
32
+ import Menu from "@mui/material/Menu";
33
+ import MenuItem from "@mui/material/MenuItem";
34
+ import { useTheme } from "@mui/material/styles";
32
35
  import Tooltip from "@mui/material/Tooltip";
33
36
  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";
37
+ import { isEmpty } from "lodash-es";
38
+ import { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, } from "react";
38
39
  import { v4 as uuid } from "uuid";
39
- import { AgentConnectivity } from "./AgentConnectivity.js";
40
40
  import { ChatHistory } from "./ChatHistory.js";
41
41
  import { ControlButtons } from "./ControlButtons.js";
42
- import { FormattedMarkdown } from "./FormattedMarkdown.js";
43
- import { AGENT_GREETINGS } from "./Greetings.js";
42
+ import { Conversation } from "./Conversation.js";
43
+ import { MessageRole } from "./ConversationTurn.js";
44
44
  import { SampleQueries } from "./SampleQueries.js";
45
45
  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";
46
+ import { Thinking } from "./Thinking.js";
47
+ import { sendChatQuery } from "../../../controller/agent/Agent.js";
49
48
  import { sendLlmRequest, StreamingUnit } from "../../../controller/llm/LlmChat.js";
50
- import { ChatMessageType, } from "../../../generated/neuro-san/NeuroSanClient.js";
49
+ import { ChatMessageType } from "../../../generated/neuro-san/NeuroSanClient.js";
51
50
  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";
51
+ import { hasOnlyWhitespace } from "../../../utils/text.js";
52
+ import { givesFinalAnswer, isLegacyAgentType } from "../Common/Types.js";
58
53
  import { chatMessageFromChunk, checkError, cleanUpAgentName, removeTrailingUuid } from "../Common/Utils.js";
59
54
  import { MicrophoneButton } from "../VoiceChat/MicrophoneButton.js";
60
55
  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";
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;
@@ -73,24 +64,23 @@ const MAX_AGENT_RETRIES = 3;
73
64
  * @returns The final answer from the agent, if it exists or undefined if it doesn't
74
65
  */
75
66
  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;
67
+ // Maximum number of turns to save
68
+ export const MAX_TURNS = 50;
78
69
  /**
79
70
  * Common chat component for agent chat. This component is used by all agent chat components to provide a consistent
80
71
  * experience for users when chatting with agents. It handles user input as well as displaying and nicely formatting
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, networkDescription, neuroSanURL, onChunkReceived, onClose, onSend, onStreamingComplete, onStreamingStarted, sampleQueries, setIsAwaitingLlm, setPreviousResponse, targetAgent, title, } = 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,28 +92,22 @@ 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
- // Keeps a copy of the last AI message so we can highlight it as "final answer"
110
- const lastAIMessage = useRef("");
111
- // Ref for the final answer key, so we can highlight the accordion
112
- const finalAnswerKey = useRef("");
97
+ // Options menu control
98
+ const [optionsMenuAnchorEl, setOptionsMenuAnchorEl] = useState(null);
99
+ const [optionsMenuOpen, setOptionsMenuOpen] = useState(false);
113
100
  // Persistent agent chat history store, which is where we store both kinds of chat histories
114
101
  // (see store implementation for details)
115
102
  const storedChatHistory = useAgentChatHistoryStore((state) => state?.history?.[targetAgent]);
116
103
  const agentChatHistory = useMemo(() => storedChatHistory ?? { chatHistory: [], chatContext: null, slyData: {} }, [storedChatHistory]);
117
- const [agentSampleQueries, setAgentSampleQueries] = useState([]);
118
104
  // Access store for context items
119
105
  const updateChatContext = useAgentChatHistoryStore((state) => state.updateChatContext);
120
106
  const updateChatHistory = useAgentChatHistoryStore((state) => state.updateChatHistory);
121
107
  const updateSlyData = useAgentChatHistoryStore((state) => state.updateSlyData);
122
108
  const resetHistory = useAgentChatHistoryStore((state) => state.resetHistory);
123
- // Ref to the item we think is the Final Answer from the agent
124
- const finalAnswerRef = useRef(null);
125
- // Track state of "show thinking" toggle
126
- const [showThinking, setShowThinking] = useState(false);
109
+ // Ref copy of current turns, so we can safely use it in callbacks without worrying about stale closures
110
+ const turnsRef = useRef([]);
127
111
  // Microphone state for voice input
128
112
  const [isMicOn, setIsMicOn] = useState(false);
129
113
  // Ref for speech recognition
@@ -151,8 +135,6 @@ export const ChatCommon = ({ ref, ...props }) => {
151
135
  };
152
136
  // Keeps track of whether the agent completed its task
153
137
  const succeeded = useRef(false);
154
- const darkMode = theme.palette.mode === "dark";
155
- const { atelierDuneDark, a11yLight } = HLJS_THEMES;
156
138
  const agentDisplayName = useMemo(() => cleanUpAgentName(removeTrailingUuid(targetAgent)), [targetAgent]);
157
139
  useEffect(() => {
158
140
  // Set up speech recognition
@@ -160,103 +142,51 @@ export const ChatCommon = ({ ref, ...props }) => {
160
142
  // Clean up function
161
143
  return () => cleanupAndStopSpeechRecognition(speechRecognitionRef, handlers);
162
144
  }, []);
163
- // Sync ref with state variable for use within timer etc.
164
- useEffect(() => {
165
- autoScrollEnabledRef.current = autoScrollEnabled;
166
- }, [autoScrollEnabled]);
167
145
  useEffect(() => {
168
146
  // Delay for a second before focusing on the input area; gets around ChatBot stealing focus.
169
147
  setTimeout(() => chatInputRef?.current?.focus(), 1000);
170
148
  }, []);
171
149
  // Auto scroll chat output window when new content is added
172
150
  useEffect(() => {
173
- // Scroll the final answer into view
174
- if (finalAnswerRef.current && !isAwaitingLlm) {
175
- chatOutputRef.current.scrollTop = finalAnswerRef.current.offsetTop - 50;
151
+ const container = chatOutputRef.current;
152
+ if (!container)
176
153
  return;
154
+ // Live-streaming auto-scroll
155
+ if (autoScrollEnabled) {
156
+ container.scrollTop = container.scrollHeight;
177
157
  }
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;
213
- }
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];
241
- return next.length > MAX_CHAT_OUTPUT_ITEMS ? next.slice(-MAX_CHAT_OUTPUT_ITEMS) : next;
158
+ }, [autoScrollEnabled, isAwaitingLlm, turns]);
159
+ // Keep a ref copy of the turns array
160
+ useEffect(() => {
161
+ turnsRef.current = turns;
162
+ }, [turns]);
163
+ const addTurn = useCallback((turn) => {
164
+ setTurns((current) => {
165
+ const next = [...current, turn];
166
+ return next.length > MAX_TURNS ? next.slice(-MAX_TURNS) : next;
242
167
  });
243
168
  }, []);
244
- const handleChunk = useCallback((chunk) => {
245
- // Give container a chance to process the chunk first
246
- const onChunkReceivedResult = onChunkReceived?.(chunk) ?? true;
247
- succeeded.current = succeeded.current || onChunkReceivedResult;
248
- // For legacy agents, we either get plain text or Markdown. Just output it as-is.
249
- if (isLegacyAgentType(targetAgent)) {
250
- // Display output as-is
251
- updateOutput(chunk);
252
- currentResponse.current += chunk;
253
- // Check for Final Answer from legacy agent
254
- const finalAnswerMatch = extractFinalAnswer(currentResponse.current);
255
- if (finalAnswerMatch) {
256
- lastAIMessage.current = finalAnswerMatch;
257
- }
258
- return;
169
+ // We use this to update the same "turn" as chunks come in from legacy agents
170
+ const legacyTurnIdRef = useRef(null);
171
+ const handleLegacyAgentChunk = useCallback((chunk) => {
172
+ currentResponse.current += chunk;
173
+ if (!legacyTurnIdRef.current) {
174
+ // We don't yet have a turn for this response, so create one. On subsequent chunks, we'll just
175
+ // update the text of this turn.
176
+ legacyTurnIdRef.current = uuid();
177
+ addTurn({
178
+ id: legacyTurnIdRef.current,
179
+ messageType: ChatMessageType.AGENT,
180
+ role: MessageRole.Agent,
181
+ text: currentResponse.current,
182
+ });
259
183
  }
184
+ else {
185
+ // We already have a turn for this response, so just update the text of that turn.
186
+ setTurns((prev) => prev.map((t) => (t.id === legacyTurnIdRef.current ? { ...t, text: currentResponse.current } : t)));
187
+ }
188
+ }, [addTurn]);
189
+ const handleNeuroSanAgentChunk = useCallback((chunk) => {
260
190
  // For Neuro-san agents, we expect a ChatMessage structure in the chunk.
261
191
  const chatMessage = chatMessageFromChunk(chunk);
262
192
  if (!chatMessage) {
@@ -275,35 +205,53 @@ export const ChatCommon = ({ ref, ...props }) => {
275
205
  updateChatContext(targetAgent, chatMessage.chat_context);
276
206
  }
277
207
  // Check if there is an error block in the "structure" field of the chat message.
278
- if (chatMessage.structure) {
279
- // If there is an error block, we should display it as an alert.
280
- const errorMessage = checkError(chatMessage.structure);
281
- if (errorMessage) {
282
- updateOutput(_jsx(MUIAlert, { id: "retry-message-alert", severity: "warning", children: errorMessage }));
283
- succeeded.current = false;
284
- }
208
+ const errorMessage = checkError(chatMessage.structure);
209
+ if (errorMessage) {
210
+ // If there is an error block, display it.
211
+ addTurn({
212
+ id: uuid(),
213
+ role: MessageRole.Warning,
214
+ text: errorMessage,
215
+ });
216
+ succeeded.current = false;
285
217
  }
286
- else if (chatMessage?.text?.trim() !== "") {
287
- // Not an error, so output it if it has text. The backend sometimes sends messages with no text content,
288
- // and we don't want to display those to the user.
289
- // Agent name is the last tool in the origin array. If it's not there, use a default name.
218
+ else if (chatMessage?.text?.trim().length > 0 || chatMessage.structure) {
219
+ // Not an error, so output it if it has text or a structure.
220
+ // This is the normal happy path for an incoming message.
221
+ // The backend sometimes sends messages with no text content, and we don't want to display those to the
222
+ // user. Agent name is the last tool in the origin array. If it's not there, use a default name.
290
223
  const agentName = chatMessage.origin?.length > 0
291
224
  ? cleanUpAgentName(chatMessage.origin[chatMessage.origin.length - 1].tool)
292
- : "Agent message";
293
- updateOutput(processLogLine(chatMessage.text, agentName, chatMessage.type));
294
- currentResponse.current += chatMessage.text;
225
+ : "Agent";
226
+ addTurn({
227
+ agentName,
228
+ id: uuid(),
229
+ messageType: chatMessage.type,
230
+ role: MessageRole.Agent,
231
+ structure: chatMessage.structure,
232
+ text: chatMessage.text,
233
+ });
234
+ if (chatMessage?.text?.trim().length > 0) {
235
+ // Append to current response if present
236
+ currentResponse.current += chatMessage.text;
237
+ }
238
+ }
239
+ }, [addTurn, targetAgent, updateChatContext, updateSlyData]);
240
+ /**
241
+ * Handle a chunk of response from the server. Called each time the server streams a chunk.
242
+ */
243
+ const handleChunk = useCallback((chunk) => {
244
+ // Give container a chance to process the chunk first
245
+ const onChunkReceivedResult = onChunkReceived?.(chunk) ?? true;
246
+ succeeded.current = succeeded.current || onChunkReceivedResult;
247
+ if (isLegacyAgentType(targetAgent)) {
248
+ // For legacy agents, we either get plain text or Markdown. Just output it as-is.
249
+ handleLegacyAgentChunk(chunk);
250
+ }
251
+ else {
252
+ handleNeuroSanAgentChunk(chunk);
295
253
  }
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]);
254
+ }, [onChunkReceived, targetAgent, handleNeuroSanAgentChunk, handleLegacyAgentChunk]);
307
255
  /**
308
256
  * Reset the state of the component. This is called after a request is completed, regardless of success or failure.
309
257
  */
@@ -311,11 +259,9 @@ export const ChatCommon = ({ ref, ...props }) => {
311
259
  // Reset state, whatever happened during request
312
260
  setIsAwaitingLlm(false);
313
261
  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
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,17 @@ 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
+ });
357
307
  }
358
308
  }
359
309
  } while (attemptNumber < MAX_AGENT_RETRIES && !succeeded.current);
360
310
  return wasAborted;
361
311
  }, [
312
+ addTurn,
362
313
  agentChatHistory,
363
314
  currentUser,
364
315
  extraParams,
@@ -367,8 +318,85 @@ export const ChatCommon = ({ ref, ...props }) => {
367
318
  legacyAgentEndpoint,
368
319
  neuroSanURL,
369
320
  targetAgent,
370
- updateOutput,
371
321
  ]);
322
+ const getFinalAnswerErrorTurn = () => ({
323
+ id: uuid(),
324
+ role: MessageRole.Error,
325
+ text: "The agent did not provide a final answer in the expected format. This is an internal error.",
326
+ });
327
+ const handleFinalAnswerLegacyAgent = useCallback(() => {
328
+ const currentTurns = turnsRef.current;
329
+ // Prefer the most recent matching turn
330
+ const idx = currentTurns.reduceRight((foundIndex, turn, i) => foundIndex !== -1 || extractFinalAnswer(turn.text) === undefined ? foundIndex : i, -1);
331
+ if (idx === -1) {
332
+ if (givesFinalAnswer(targetAgent)) {
333
+ // This agent is supposed to give final answers, but didn't this time. An error.
334
+ setTurns((prev) => [...prev, getFinalAnswerErrorTurn()]);
335
+ return;
336
+ }
337
+ else {
338
+ // Use the last received turn as the final answer
339
+ const lastTurn = currentTurns.slice(-1)[0];
340
+ if (!lastTurn)
341
+ return;
342
+ // Just set the last turn as the final answer
343
+ setTurns((prev) => prev.map((turn) => (turn.id === lastTurn.id ? { ...turn, role: MessageRole.FinalAnswer } : turn)));
344
+ // Save it to chat history
345
+ updateChatHistory(targetAgent, [new AIMessage({ content: lastTurn.text, id: uuid() })]);
346
+ return;
347
+ }
348
+ }
349
+ const sourceTurn = currentTurns[idx];
350
+ // Save item to chat history (same as original behavior)
351
+ updateChatHistory(targetAgent, [new AIMessage({ content: sourceTurn.text, id: uuid() })]);
352
+ // Extract the final answer from the turn.
353
+ const finalAnswer = extractFinalAnswer(sourceTurn.text)?.trim();
354
+ // Update the turn to be a final answer turn, and add a new final answer turn with just the final answer text.
355
+ setTurns((prev) => {
356
+ const sourceTurnIndex = prev.findIndex(({ id: itemId }) => itemId === sourceTurn.id);
357
+ const updated = sourceTurnIndex === -1
358
+ ? [...prev]
359
+ : prev.map((turn, index) => (index === sourceTurnIndex ? { ...turn, text: sourceTurn.text } : turn));
360
+ // Add explicit final answer as a new terminal turn
361
+ updated.push({
362
+ id: uuid(),
363
+ role: MessageRole.FinalAnswer,
364
+ text: finalAnswer,
365
+ });
366
+ return updated;
367
+ });
368
+ }, [targetAgent, updateChatHistory]);
369
+ /**
370
+ * Extract the final answer from the turns for a Neuro-san agent. For Neuro-san agents, we expect the final answer
371
+ * to be the most recent turn messageType === ChatMessageType.AGENT_FRAMEWORK.
372
+ */
373
+ const handleFinalAnswerNeuroSanAgent = useCallback(() => {
374
+ // Get current turns snapshot
375
+ const currentTurns = turnsRef.current;
376
+ // Find the most recent turn that is from the agent framework, which should be the one that contains the
377
+ // final answer.
378
+ const idx = currentTurns.reduceRight((found, turn, i) => (found !== -1 || turn.messageType !== ChatMessageType.AGENT_FRAMEWORK ? found : i), -1);
379
+ // Check for final answer
380
+ if (idx === -1) {
381
+ // No final answer found in the turns. Should never happen for a Neuro-san agent.
382
+ setTurns((prev) => [...prev, getFinalAnswerErrorTurn()]);
383
+ return;
384
+ }
385
+ // Extract final answer from that turn
386
+ const finalAnswerTurn = currentTurns[idx];
387
+ const hasFinalAnswer = finalAnswerTurn.text?.trim().length > 0 || !isEmpty(finalAnswerTurn.structure);
388
+ if (hasFinalAnswer) {
389
+ // Update relevant turn to be the final answer
390
+ setTurns((prev) => prev.map((turn, i) => (i === idx ? { ...turn, role: MessageRole.FinalAnswer } : turn)));
391
+ // Save final answer to chat history
392
+ const finalAnswerContent = finalAnswerTurn.text || JSON.stringify(finalAnswerTurn.structure, null, 2);
393
+ updateChatHistory(targetAgent, [new AIMessage({ content: finalAnswerContent, id: uuid() })]);
394
+ }
395
+ else {
396
+ // No final answer found, display error
397
+ setTurns((prev) => [...prev, getFinalAnswerErrorTurn()]);
398
+ }
399
+ }, [targetAgent, updateChatHistory]);
372
400
  const handleSend = useCallback(async (query) => {
373
401
  // Record user query in chat history. Discard anything beyond MAX_CHAT_HISTORY_ITEMS
374
402
  const userQueryMessage = new HumanMessage({ content: query, id: uuid() });
@@ -377,53 +405,45 @@ export const ChatCommon = ({ ref, ...props }) => {
377
405
  const queryToSend = onSend?.(query) ?? query;
378
406
  // Save query for "regenerate" use. Again we save the real user input, not the modified query. It will again
379
407
  // get intercepted and re-modified (if applicable) on "regenerate".
380
- previousUserQuery.current = query;
408
+ setPreviousUserQuery(query);
381
409
  setIsAwaitingLlm(true);
382
410
  // Always start output by echoing user query.
383
411
  // Note: we display the original user query, not the modified one. The modified one could be a monstrosity
384
412
  // that we generated behind their back. Ultimately, we shouldn't need to generate a fake query on behalf
385
413
  // of the user, but currently we do for orchestration.
386
- updateOutput(_jsx(UserQueryDisplay, { userQuery: query, title: currentUser, userImage: userImage }));
387
- // Add ID block for agent
388
- updateOutput(_jsx(UserQueryDisplay, { userQuery: agentDisplayName, title: targetAgent, userImage: AGENT_IMAGE }));
414
+ addTurn({
415
+ id: uuid(),
416
+ role: MessageRole.User,
417
+ text: query,
418
+ });
389
419
  // Allow clients to do something when streaming starts
390
420
  onStreamingStarted?.();
391
421
  // Set up the abort controller
392
422
  controller.current = new AbortController();
393
423
  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
- }
402
424
  try {
403
425
  // Invoke the logic to send the request and retry as necessary
404
426
  const wasAborted = await doRetryLoop(queryToSend);
405
- 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.` }));
407
- }
408
- // Display prominent "Final Answer" message if we have one
409
- 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");
427
+ // Abort condition is handled elsewhere
428
+ if (!wasAborted) {
429
+ if (succeeded.current) {
430
+ // Success: infer final answer depending on agent type
431
+ if (isLegacyAgentType(targetAgent)) {
432
+ handleFinalAnswerLegacyAgent();
433
+ }
434
+ else {
435
+ handleFinalAnswerNeuroSanAgent();
436
+ }
413
437
  }
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() })]);
438
+ else {
439
+ // Exhausted retries without success. Display error to user.
440
+ addTurn({
441
+ id: uuid(),
442
+ role: MessageRole.Error,
443
+ text: `Gave up after ${MAX_AGENT_RETRIES} attempts.`,
444
+ });
418
445
  }
419
446
  }
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() })]);
424
- }
425
- // Add a blank line after response
426
- updateOutput("\n");
427
447
  }
428
448
  finally {
429
449
  resetState();
@@ -431,66 +451,32 @@ export const ChatCommon = ({ ref, ...props }) => {
431
451
  onStreamingComplete?.();
432
452
  }
433
453
  }, [
434
- agentDisplayName,
435
- currentUser,
454
+ addTurn,
436
455
  doRetryLoop,
456
+ handleFinalAnswerLegacyAgent,
457
+ handleFinalAnswerNeuroSanAgent,
437
458
  onSend,
438
459
  onStreamingComplete,
439
460
  onStreamingStarted,
440
- processLogLine,
441
461
  resetState,
442
462
  setIsAwaitingLlm,
443
- showThinking,
444
463
  targetAgent,
445
464
  updateChatHistory,
446
- updateOutput,
447
- userImage,
448
465
  ]);
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
466
  const handleStop = useCallback(() => {
481
467
  try {
482
468
  controller?.current?.abort();
483
469
  controller.current = null;
484
- updateOutput(_jsx(MUIAlert, { id: "opp-finder-error-occurred-alert", severity: "warning", children: "Request cancelled." }));
470
+ addTurn({
471
+ id: uuid(),
472
+ role: MessageRole.Warning,
473
+ text: "Request cancelled.",
474
+ });
485
475
  }
486
476
  finally {
487
477
  resetState();
488
478
  }
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]);
479
+ }, [addTurn, resetState]);
494
480
  // Regex to check if user has typed anything besides whitespace
495
481
  const userInputEmpty = !chatInput || chatInput.length === 0 || hasOnlyWhitespace(chatInput);
496
482
  // Enable Send button when there is user input and not awaiting a response
@@ -498,46 +484,30 @@ export const ChatCommon = ({ ref, ...props }) => {
498
484
  // Enable regenerate button when there is a previous query to resent, and we're not awaiting a response
499
485
  const shouldEnableRegenerateButton = previousUserQuery && !isAwaitingLlm;
500
486
  // Enable Clear Chat button if not awaiting response and there is chat output to clear
501
- const enableClearChatButton = !isAwaitingLlm && chatOutput.length > 0;
487
+ const enableClearChatButton = !isAwaitingLlm && (turns.length > 0 || agentChatHistory?.chatHistory?.length > 0);
502
488
  const getPlaceholder = () => !targetAgent ? null : agentPlaceholders[targetAgent] || `Chat with ${agentDisplayName}`;
503
489
  const handleClearChat = useCallback(() => {
504
- setChatOutput([]);
490
+ setTurns([]);
505
491
  resetHistory(targetAgent);
506
- previousUserQuery.current = "";
492
+ setPreviousUserQuery("");
507
493
  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
- } }));
494
+ }, [resetHistory, targetAgent]);
495
+ // Expose the handleStop and handleClearChat methods to parent components via ref for external control
496
+ useImperativeHandle(ref, () => ({
497
+ handleStop,
498
+ handleClearChat,
499
+ }), [handleStop, handleClearChat]);
500
+ 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: {
501
+ position: "absolute",
502
+ top: 0,
503
+ left: 0,
504
+ right: 0,
505
+ bottom: 0,
506
+ zIndex: theme.zIndex.modal - 1,
507
+ cursor: "not-allowed",
508
+ // Capture all pointer events to prevent interaction with the chat when no agent is selected
509
+ pointerEvents: "all",
510
+ } }) }));
541
511
  const getTitle = () => (_jsxs(Box, { id: `llm-chat-title-container-${id}`, sx: {
542
512
  alignItems: "center",
543
513
  borderTopLeftRadius: "var(--bs-border-radius)",
@@ -549,32 +519,61 @@ export const ChatCommon = ({ ref, ...props }) => {
549
519
  paddingTop: "0.25rem",
550
520
  paddingBottom: "0.25rem",
551
521
  }, 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" } }) }) })] }));
522
+ const getOptionsMenuButton = () => (_jsx(Box, { sx: {
523
+ position: "absolute",
524
+ top: "0.25rem",
525
+ right: "0.0rem",
526
+ }, children: _jsx(IconButton, { onClick: (e) => {
527
+ setOptionsMenuAnchorEl(e.currentTarget);
528
+ setOptionsMenuOpen(true);
529
+ }, children: _jsx(TuneIcon, { sx: { fontSize: "1.2rem" } }) }) }));
530
+ const agentGreeting = customAgentGreetings[targetAgent] ?? "Hi, how can I help?";
531
+ const handleOptionsMenuClose = () => {
532
+ setOptionsMenuAnchorEl(null);
533
+ setOptionsMenuOpen(false);
534
+ };
535
+ const handleToggleAutoScroll = () => {
536
+ setAutoScrollEnabled((prev) => !prev);
537
+ };
538
+ const handleToggleWrapOutput = () => {
539
+ setShouldWrapOutput((prev) => !prev);
540
+ };
541
+ const getOptionsMenu = () => (_jsxs(Menu, { id: `${id}-options-menu`, anchorEl: optionsMenuAnchorEl, open: optionsMenuOpen, onClose: handleOptionsMenuClose, slotProps: {
542
+ list: {
543
+ dense: true,
544
+ sx: {
545
+ py: 0,
546
+ "& .MuiMenuItem-root": { minHeight: 30, py: 0.5, px: 1 },
547
+ "& .MuiCheckbox-root": { p: 0.5 },
548
+ "& .MuiListItemText-primary": { fontSize: "smaller" },
549
+ },
550
+ },
551
+ }, children: [_jsxs(MenuItem, { onClick: handleToggleAutoScroll, children: [_jsx(ListItemIcon, { children: _jsx(Checkbox, { checked: autoScrollEnabled, tabIndex: -1, sx: { pointerEvents: "none" } }) }), _jsx(ListItemText, { primary: "Auto-scroll output" })] }), _jsxs(MenuItem, { onClick: handleToggleWrapOutput, children: [_jsx(ListItemIcon, { children: _jsx(Checkbox, { checked: shouldWrapOutput, tabIndex: -1, sx: { pointerEvents: "none" } }) }), _jsx(ListItemText, { primary: "Wrap output" })] })] }));
553
552
  const getResponseBox = () => (_jsxs(Box, { id: "llm-response-div", sx: {
554
553
  ...divStyle,
555
554
  border: "var(--bs-border-width) var(--bs-border-style)",
556
555
  borderRadius: "var(--bs-border-radius)",
557
556
  display: "flex",
558
557
  flexGrow: 1,
559
- height: "100%",
560
- margin: "10px",
558
+ marginLeft: "10px",
561
559
  position: "relative",
562
560
  overflowY: "auto",
563
- }, children: [getOptionsButtons(), _jsxs(Box, { id: "llm-responses", ref: chatOutputRef, sx: {
564
- backgroundColor: backgroundColor || undefined,
565
- borderWidth: "1px",
561
+ }, children: [_jsxs(Box, { id: "llm-responses", ref: chatOutputRef, sx: {
562
+ backgroundColor,
566
563
  borderRadius: "0.5rem",
567
- fontSize: "smaller",
568
- resize: "none",
569
- overflowY: "auto", // Enable vertical scrollbar
564
+ fontSize: "16px",
565
+ overflowY: "auto",
570
566
  paddingBottom: "60px",
571
- paddingTop: "7.5px",
572
567
  paddingLeft: "15px",
573
568
  paddingRight: "15px",
569
+ paddingTop: "7.5px",
570
+ scrollbarGutter: "stable",
574
571
  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: {
572
+ }, tabIndex: -1, children: [getOptionsMenu(), getOptionsMenuButton(), agentChatHistory?.chatHistory?.length > 0 && (_jsx(ChatHistory, { id: id, messages: agentChatHistory.chatHistory })), _jsxs(Box, { sx: { marginBottom: "0.5rem", marginTop: "1rem", color: "var(--bs-gray)" }, children: [_jsxs(Typography, { component: "span", sx: { fontWeight: 700 }, variant: "inherit", children: [targetAgent, networkDescription && ":"] }), networkDescription && (_jsxs(Typography, { component: "span", sx: { ml: 0.5 }, variant: "inherit", children: [" ", networkDescription] }))] }), _jsx(Box, { sx: { marginBottom: "0.5rem", marginTop: "1rem" }, children: agentGreeting }), _jsx(SampleQueries, { disabled: isAwaitingLlm, handleSend: handleSend, sampleQueries: sampleQueries }), _jsx(Conversation, { id: id, includeAgentMessages: !givesFinalAnswer(targetAgent), shouldWrapOutput: shouldWrapOutput, turns: turns }), !isAwaitingLlm && turns.length > 0 && (
573
+ // Only show thinking once streaming is complete
574
+ _jsx(Thinking, { id: id, turns: turns })), 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
575
  color: "var(--bs-primary)",
577
- }, size: "1rem" })] }))] }), _jsx(ControlButtons, { handleClearChat: handleClearChat, enableClearChatButton: enableClearChatButton, isAwaitingLlm: isAwaitingLlm, handleSend: handleSend, handleStop: handleStop, previousUserQuery: previousUserQuery.current, shouldEnableRegenerateButton: shouldEnableRegenerateButton })] }));
576
+ }, size: "1rem" })] }))] }), _jsx(ControlButtons, { enableClearChatButton: enableClearChatButton, handleClearChat: handleClearChat, handleSend: handleSend, handleStop: handleStop, isAwaitingLlm: isAwaitingLlm, previousUserQuery: previousUserQuery, shouldEnableRegenerateButton: shouldEnableRegenerateButton })] }));
578
577
  const getUserInputBox = () => (_jsxs(Box, { id: "user-input-div", sx: {
579
578
  ...divStyle,
580
579
  display: "flex",
@@ -586,7 +585,7 @@ export const ChatCommon = ({ ref, ...props }) => {
586
585
  borderRadius: "var(--bs-border-radius)",
587
586
  display: "flex",
588
587
  flexGrow: 1,
589
- fontSize: "smaller",
588
+ fontSize: "17px",
590
589
  marginRight: "0.75rem",
591
590
  paddingBottom: "0.5rem",
592
591
  paddingTop: "0.5rem",