@cognizant-ai-lab/ui-common 1.5.1 → 1.7.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 (57) hide show
  1. package/dist/components/AgentChat/ChatCommon/ChatCommon.d.ts +16 -6
  2. package/dist/components/AgentChat/ChatCommon/ChatCommon.js +250 -166
  3. package/dist/components/AgentChat/ChatCommon/ChatHistory.d.ts +1 -7
  4. package/dist/components/AgentChat/ChatCommon/ChatHistory.js +33 -22
  5. package/dist/components/AgentChat/ChatCommon/Conversation.d.ts +3 -5
  6. package/dist/components/AgentChat/ChatCommon/Conversation.js +35 -57
  7. package/dist/components/AgentChat/ChatCommon/ConversationTurn.d.ts +9 -5
  8. package/dist/components/AgentChat/ChatCommon/ConversationTurn.js +3 -2
  9. package/dist/components/AgentChat/ChatCommon/FormattedMarkdown.js +5 -3
  10. package/dist/components/AgentChat/ChatCommon/SampleQueries.d.ts +3 -0
  11. package/dist/components/AgentChat/ChatCommon/SampleQueries.js +6 -3
  12. package/dist/components/AgentChat/ChatCommon/Thinking.d.ts +12 -0
  13. package/dist/components/AgentChat/ChatCommon/Thinking.js +51 -0
  14. package/dist/components/AgentChat/Common/LlmChatButton.d.ts +2 -2
  15. package/dist/components/AgentChat/Common/Types.d.ts +6 -5
  16. package/dist/components/AgentChat/Common/Types.js +5 -0
  17. package/dist/components/AgentChat/Common/Utils.d.ts +1 -1
  18. package/dist/components/AgentChat/Common/Utils.js +13 -7
  19. package/dist/components/ChatBot/ChatBot.d.ts +0 -4
  20. package/dist/components/ChatBot/ChatBot.js +2 -2
  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 -1
  24. package/dist/components/Common/CustomerLogo.js +1 -3
  25. package/dist/components/Common/MUIAlert.d.ts +1 -0
  26. package/dist/components/Common/MUIAlert.js +3 -4
  27. package/dist/components/MultiAgentAccelerator/AgentFlow.d.ts +1 -0
  28. package/dist/components/MultiAgentAccelerator/AgentFlow.js +154 -59
  29. package/dist/components/MultiAgentAccelerator/AgentNode.d.ts +1 -0
  30. package/dist/components/MultiAgentAccelerator/AgentNode.js +46 -45
  31. package/dist/components/MultiAgentAccelerator/GraphLayouts.js +12 -4
  32. package/dist/components/MultiAgentAccelerator/MultiAgentAccelerator.js +103 -24
  33. package/dist/components/Settings/ApiKeyInput.d.ts +16 -0
  34. package/dist/components/Settings/ApiKeyInput.js +70 -0
  35. package/dist/components/Settings/SettingsDialog.js +30 -3
  36. package/dist/controller/llm/Providers.d.ts +2 -0
  37. package/dist/controller/llm/Providers.js +41 -0
  38. package/dist/index.d.ts +0 -1
  39. package/dist/index.js +0 -1
  40. package/dist/state/Settings.d.ts +2 -0
  41. package/dist/state/Settings.js +1 -0
  42. package/dist/tsconfig.build.tsbuildinfo +1 -1
  43. package/package.json +2 -2
  44. package/dist/components/AgentChat/ChatCommon/AgentConnectivity.d.ts +0 -14
  45. package/dist/components/AgentChat/ChatCommon/AgentConnectivity.js +0 -23
  46. package/dist/components/AgentChat/ChatCommon/AgentIntro.d.ts +0 -12
  47. package/dist/components/AgentChat/ChatCommon/AgentIntro.js +0 -19
  48. package/dist/components/AgentChat/ChatCommon/AgentMetadata.d.ts +0 -14
  49. package/dist/components/AgentChat/ChatCommon/AgentMetadata.js +0 -43
  50. package/dist/components/AgentChat/ChatCommon/Const.d.ts +0 -1
  51. package/dist/components/AgentChat/ChatCommon/Const.js +0 -2
  52. package/dist/components/AgentChat/ChatCommon/Greetings.d.ts +0 -1
  53. package/dist/components/AgentChat/ChatCommon/Greetings.js +0 -38
  54. package/dist/components/AgentChat/ChatCommon/UserQueryDisplay.d.ts +0 -7
  55. package/dist/components/AgentChat/ChatCommon/UserQueryDisplay.js +0 -32
  56. package/dist/components/Common/LlmChatOptionsButton.d.ts +0 -6
  57. 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,41 +18,40 @@ 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";
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";
31
34
  import { useTheme } from "@mui/material/styles";
32
- import Tooltip from "@mui/material/Tooltip";
33
35
  import Typography from "@mui/material/Typography";
36
+ import { isEmpty } from "lodash-es";
34
37
  import { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, } from "react";
35
38
  import { v4 as uuid } from "uuid";
36
- import { AgentIntro } from "./AgentIntro.js";
37
- import { AgentMetadata } from "./AgentMetadata.js";
38
39
  import { ChatHistory } from "./ChatHistory.js";
39
- import { AGENT_IMAGE } from "./Const.js";
40
40
  import { ControlButtons } from "./ControlButtons.js";
41
41
  import { Conversation } from "./Conversation.js";
42
42
  import { MessageRole } from "./ConversationTurn.js";
43
+ import { SampleQueries } from "./SampleQueries.js";
43
44
  import { SendButton } from "./SendButton.js";
45
+ import { Thinking } from "./Thinking.js";
44
46
  import { sendChatQuery } from "../../../controller/agent/Agent.js";
45
47
  import { sendLlmRequest, StreamingUnit } from "../../../controller/llm/LlmChat.js";
46
48
  import { ChatMessageType } from "../../../generated/neuro-san/NeuroSanClient.js";
47
49
  import { useAgentChatHistoryStore } from "../../../state/ChatHistory.js";
48
50
  import { hasOnlyWhitespace } from "../../../utils/text.js";
49
- import { LlmChatOptionsButton } from "../../Common/LlmChatOptionsButton.js";
50
- import { isLegacyAgentType } from "../Common/Types.js";
51
+ import { givesFinalAnswer, isLegacyAgentType } from "../Common/Types.js";
51
52
  import { chatMessageFromChunk, checkError, cleanUpAgentName, removeTrailingUuid } from "../Common/Utils.js";
52
53
  import { MicrophoneButton } from "../VoiceChat/MicrophoneButton.js";
53
54
  import { cleanupAndStopSpeechRecognition, setupSpeechRecognition } from "../VoiceChat/VoiceChat.js";
54
- // Key for the chat history, which gets special treatment; always visible even if "show thinking" is off.
55
- const CHAT_HISTORY_KEY = "chat-history-accordion";
56
55
  // Define fancy EMPTY constant to avoid linter error about using object literals as default props
57
56
  const EMPTY = {};
58
57
  // How many times to retry the entire agent interaction process. Some networks have a well-defined success condition.
@@ -64,15 +63,15 @@ const MAX_AGENT_RETRIES = 3;
64
63
  * @returns The final answer from the agent, if it exists or undefined if it doesn't
65
64
  */
66
65
  const extractFinalAnswer = (response) => /Final Answer: (?<finalAnswerText>.*)/su.exec(response)?.groups?.["finalAnswerText"];
67
- // Maximum number of items to keep in the chat output window
68
- const MAX_CHAT_OUTPUT_ITEMS = 50;
66
+ // Maximum number of turns to save
67
+ export const MAX_TURNS = 50;
69
68
  /**
70
69
  * Common chat component for agent chat. This component is used by all agent chat components to provide a consistent
71
70
  * experience for users when chatting with agents. It handles user input as well as displaying and nicely formatting
72
71
  * agent responses. Customization for inputs and outputs is provided via event handlers-like props.
73
72
  */
74
73
  export const ChatCommon = ({ ref, ...props }) => {
75
- const { customAgentGreetings = EMPTY, agentPlaceholders = EMPTY, backgroundColor, currentUser, extraParams, extraSlyData, id, isAwaitingLlm, legacyAgentEndpoint, neuroSanURL, onChunkReceived, onClose, onSend, onStreamingComplete, onStreamingStarted, setIsAwaitingLlm, setPreviousResponse, targetAgent, title, userImage, } = props;
74
+ const { customAgentGreetings = EMPTY, agentPlaceholders = EMPTY, backgroundColor, currentUser, extraParams, extraSlyData, id, isAwaitingLlm, legacyAgentEndpoint, missingApiKeys = [], networkDescription, neuroSanURL, onChunkReceived, onClose, onSend, onStreamingComplete, onStreamingStarted, sampleQueries, setIsAwaitingLlm, setPreviousResponse, selectedNetwork, title, } = props;
76
75
  // MUI theme
77
76
  const theme = useTheme();
78
77
  // User LLM chat input
@@ -94,21 +93,20 @@ export const ChatCommon = ({ ref, ...props }) => {
94
93
  const [autoScrollEnabled, setAutoScrollEnabled] = useState(true);
95
94
  // Whether to wrap output text
96
95
  const [shouldWrapOutput, setShouldWrapOutput] = useState(true);
97
- // Keeps a copy of the last AI message so we can highlight it as "final answer"
98
- const lastAIMessage = useRef("");
96
+ // Options menu control
97
+ const [optionsMenuAnchorEl, setOptionsMenuAnchorEl] = useState(null);
98
+ const [optionsMenuOpen, setOptionsMenuOpen] = useState(false);
99
99
  // Persistent agent chat history store, which is where we store both kinds of chat histories
100
100
  // (see store implementation for details)
101
- const storedChatHistory = useAgentChatHistoryStore((state) => state?.history?.[targetAgent]);
101
+ const storedChatHistory = useAgentChatHistoryStore((state) => selectedNetwork ? state?.history?.[selectedNetwork] : undefined);
102
102
  const agentChatHistory = useMemo(() => storedChatHistory ?? { chatHistory: [], chatContext: null, slyData: {} }, [storedChatHistory]);
103
103
  // Access store for context items
104
104
  const updateChatContext = useAgentChatHistoryStore((state) => state.updateChatContext);
105
105
  const updateChatHistory = useAgentChatHistoryStore((state) => state.updateChatHistory);
106
106
  const updateSlyData = useAgentChatHistoryStore((state) => state.updateSlyData);
107
107
  const resetHistory = useAgentChatHistoryStore((state) => state.resetHistory);
108
- // Ref to the item we think is the Final Answer from the agent
109
- const finalAnswerRef = useRef(null);
110
- // Track state of "show thinking" toggle
111
- const [showThinking, setShowThinking] = useState(false);
108
+ // Ref copy of current turns, so we can safely use it in callbacks without worrying about stale closures
109
+ const turnsRef = useRef([]);
112
110
  // Microphone state for voice input
113
111
  const [isMicOn, setIsMicOn] = useState(false);
114
112
  // Ref for speech recognition
@@ -136,7 +134,7 @@ export const ChatCommon = ({ ref, ...props }) => {
136
134
  };
137
135
  // Keeps track of whether the agent completed its task
138
136
  const succeeded = useRef(false);
139
- const agentDisplayName = useMemo(() => cleanUpAgentName(removeTrailingUuid(targetAgent)), [targetAgent]);
137
+ const agentDisplayName = useMemo(() => cleanUpAgentName(removeTrailingUuid(selectedNetwork)), [selectedNetwork]);
140
138
  useEffect(() => {
141
139
  // Set up speech recognition
142
140
  const handlers = setupSpeechRecognition(setChatInput, setVoiceInputState, speechRecognitionRef);
@@ -152,53 +150,42 @@ export const ChatCommon = ({ ref, ...props }) => {
152
150
  const container = chatOutputRef.current;
153
151
  if (!container)
154
152
  return;
155
- // Scroll the final answer into view
156
- if (finalAnswerRef.current && !isAwaitingLlm) {
157
- container.scrollTop = finalAnswerRef.current.offsetTop - 50;
158
- return;
159
- }
160
153
  // Live-streaming auto-scroll
161
154
  if (autoScrollEnabled) {
162
155
  container.scrollTop = container.scrollHeight;
163
156
  }
164
157
  }, [autoScrollEnabled, isAwaitingLlm, turns]);
158
+ // Keep a ref copy of the turns array
159
+ useEffect(() => {
160
+ turnsRef.current = turns;
161
+ }, [turns]);
165
162
  const addTurn = useCallback((turn) => {
166
163
  setTurns((current) => {
167
164
  const next = [...current, turn];
168
- return next.length > MAX_CHAT_OUTPUT_ITEMS ? next.slice(-MAX_CHAT_OUTPUT_ITEMS) : next;
165
+ return next.length > MAX_TURNS ? next.slice(-MAX_TURNS) : next;
169
166
  });
170
167
  }, []);
171
168
  // We use this to update the same "turn" as chunks come in from legacy agents
172
169
  const legacyTurnIdRef = useRef(null);
173
- const handleChunk = useCallback((chunk) => {
174
- // Give container a chance to process the chunk first
175
- const onChunkReceivedResult = onChunkReceived?.(chunk) ?? true;
176
- succeeded.current = succeeded.current || onChunkReceivedResult;
177
- // For legacy agents, we either get plain text or Markdown. Just output it as-is.
178
- if (isLegacyAgentType(targetAgent)) {
179
- currentResponse.current += chunk;
180
- if (!legacyTurnIdRef.current) {
181
- // We don't yet have a turn for this response, so create one. On subsequent chunks, we'll just
182
- // update the text of this turn.
183
- legacyTurnIdRef.current = uuid();
184
- addTurn({
185
- id: legacyTurnIdRef.current,
186
- role: MessageRole.LegacyAgent,
187
- text: currentResponse.current,
188
- alwaysShow: true,
189
- });
190
- }
191
- else {
192
- // We already have a turn for this response, so just update the text of that turn.
193
- setTurns((prev) => prev.map((t) => (t.id === legacyTurnIdRef.current ? { ...t, text: currentResponse.current } : t)));
194
- }
195
- // Check for Final Answer from legacy agent
196
- const finalAnswerMatch = extractFinalAnswer(currentResponse.current);
197
- if (finalAnswerMatch) {
198
- lastAIMessage.current = finalAnswerMatch;
199
- }
200
- return;
170
+ const handleLegacyAgentChunk = useCallback((chunk) => {
171
+ currentResponse.current += chunk;
172
+ if (!legacyTurnIdRef.current) {
173
+ // We don't yet have a turn for this response, so create one. On subsequent chunks, we'll just
174
+ // update the text of this turn.
175
+ legacyTurnIdRef.current = uuid();
176
+ addTurn({
177
+ id: legacyTurnIdRef.current,
178
+ messageType: ChatMessageType.AGENT,
179
+ role: MessageRole.Agent,
180
+ text: currentResponse.current,
181
+ });
201
182
  }
183
+ else {
184
+ // We already have a turn for this response, so just update the text of that turn.
185
+ setTurns((prev) => prev.map((t) => (t.id === legacyTurnIdRef.current ? { ...t, text: currentResponse.current } : t)));
186
+ }
187
+ }, [addTurn]);
188
+ const handleNeuroSanAgentChunk = useCallback((chunk) => {
202
189
  // For Neuro-san agents, we expect a ChatMessage structure in the chunk.
203
190
  const chatMessage = chatMessageFromChunk(chunk);
204
191
  if (!chatMessage) {
@@ -206,50 +193,64 @@ export const ChatCommon = ({ ref, ...props }) => {
206
193
  // But don't want to spam output by logging errors for every bad message.
207
194
  return;
208
195
  }
209
- // Keep track of AI messages. The last one is (by definition) the "final answer" from the agents.
210
- if (chatMessage.type === ChatMessageType.AI && chatMessage.text) {
211
- lastAIMessage.current = chatMessage.text;
212
- }
213
196
  // Shallow merge existing slyData with incoming chatMessage.sly_data
214
197
  if (chatMessage.sly_data) {
215
- updateSlyData(targetAgent, chatMessage.sly_data);
198
+ updateSlyData(selectedNetwork, chatMessage.sly_data);
216
199
  }
217
200
  // It's a ChatMessage. Does it have chat context? Only AGENT_FRAMEWORK messages can have chat context.
218
201
  if (chatMessage.type === ChatMessageType.AGENT_FRAMEWORK && chatMessage.chat_context) {
219
202
  // Save the chat context, potentially overwriting any previous ones we received during this session.
220
203
  // We only care about the last one received.
221
- updateChatContext(targetAgent, chatMessage.chat_context);
204
+ updateChatContext(selectedNetwork, chatMessage.chat_context);
222
205
  }
223
206
  // Check if there is an error block in the "structure" field of the chat message.
224
- if (chatMessage.structure) {
225
- // If there is an error block, we should display it as an alert.
226
- const errorMessage = checkError(chatMessage.structure);
227
- if (errorMessage) {
228
- addTurn({
229
- id: uuid(),
230
- role: MessageRole.Warning,
231
- text: errorMessage,
232
- alwaysShow: true,
233
- });
234
- succeeded.current = false;
235
- }
207
+ const errorMessage = checkError(chatMessage.structure);
208
+ if (errorMessage) {
209
+ // If there is an error block, display it.
210
+ addTurn({
211
+ id: uuid(),
212
+ role: MessageRole.Warning,
213
+ text: errorMessage,
214
+ });
215
+ succeeded.current = false;
236
216
  }
237
- else if (chatMessage?.text?.trim() !== "") {
238
- // Not an error, so output it if it has text. The backend sometimes sends messages with no text content,
239
- // and we don't want to display those to the user.
240
- // Agent name is the last tool in the origin array. If it's not there, use a default name.
217
+ else if (chatMessage?.text?.trim().length > 0 || !isEmpty(chatMessage.structure)) {
218
+ // Not an error, so output it if it has text or a structure.
219
+ // This is the normal happy path for an incoming message.
220
+ // The backend sometimes sends messages with no text content, and we don't want to display those to the
221
+ // user. Agent name is the last tool in the origin array. If it's not there, use a default name.
241
222
  const agentName = chatMessage.origin?.length > 0
242
223
  ? cleanUpAgentName(chatMessage.origin[chatMessage.origin.length - 1].tool)
243
- : "Agent message";
224
+ : "Agent";
244
225
  addTurn({
226
+ agentName,
245
227
  id: uuid(),
228
+ messageType: chatMessage.type,
246
229
  role: MessageRole.Agent,
247
- agentName,
230
+ structure: chatMessage.structure,
248
231
  text: chatMessage.text,
249
232
  });
250
- currentResponse.current += chatMessage.text;
233
+ if (chatMessage?.text?.trim().length > 0) {
234
+ // Append to current response if present
235
+ currentResponse.current += chatMessage.text;
236
+ }
237
+ }
238
+ }, [addTurn, selectedNetwork, updateChatContext, updateSlyData]);
239
+ /**
240
+ * Handle a chunk of response from the server. Called each time the server streams a chunk.
241
+ */
242
+ const handleChunk = useCallback((chunk) => {
243
+ // Give container a chance to process the chunk first
244
+ const onChunkReceivedResult = onChunkReceived?.(chunk) ?? true;
245
+ succeeded.current = succeeded.current || onChunkReceivedResult;
246
+ if (isLegacyAgentType(selectedNetwork)) {
247
+ // For legacy agents, we either get plain text or Markdown. Just output it as-is.
248
+ handleLegacyAgentChunk(chunk);
249
+ }
250
+ else {
251
+ handleNeuroSanAgentChunk(chunk);
251
252
  }
252
- }, [onChunkReceived, targetAgent, addTurn, updateSlyData, updateChatContext]);
253
+ }, [onChunkReceived, selectedNetwork, handleNeuroSanAgentChunk, handleLegacyAgentChunk]);
253
254
  /**
254
255
  * Reset the state of the component. This is called after a request is completed, regardless of success or failure.
255
256
  */
@@ -257,12 +258,10 @@ export const ChatCommon = ({ ref, ...props }) => {
257
258
  // Reset state, whatever happened during request
258
259
  setIsAwaitingLlm(false);
259
260
  setChatInput("");
260
- lastAIMessage.current = "";
261
- finalAnswerRef.current = null;
262
- setPreviousResponse?.(targetAgent, currentResponse.current);
261
+ setPreviousResponse?.(selectedNetwork, currentResponse.current);
263
262
  currentResponse.current = "";
264
263
  legacyTurnIdRef.current = null;
265
- }, [setIsAwaitingLlm, setPreviousResponse, targetAgent]);
264
+ }, [setIsAwaitingLlm, setPreviousResponse, selectedNetwork]);
266
265
  /*
267
266
  * The main logic for sending a query to the server, with retries on errors.
268
267
  */
@@ -275,7 +274,7 @@ export const ChatCommon = ({ ref, ...props }) => {
275
274
  // Increment the attempt number and set the state to indicate we're awaiting a response
276
275
  attemptNumber += 1;
277
276
  // Check which agent type we are dealing with
278
- if (isLegacyAgentType(targetAgent)) {
277
+ if (isLegacyAgentType(selectedNetwork)) {
279
278
  // It's a legacy agent (these go directly to the LLM and are different from
280
279
  // the Neuro-san agents).
281
280
  // Send the chat query to the server. This will block until the stream ends from the server
@@ -285,7 +284,7 @@ export const ChatCommon = ({ ref, ...props }) => {
285
284
  // It's a Neuro-san agent.
286
285
  // Some coded tools (data generator...) expect the username provided in slyData.
287
286
  const slyDataWithUserName = { ...agentChatHistory?.slyData, ...extraSlyData, login: currentUser };
288
- await sendChatQuery(neuroSanURL, controller?.current.signal, query, targetAgent, handleChunk, agentChatHistory.chatContext, slyDataWithUserName, currentUser, StreamingUnit.Line);
287
+ await sendChatQuery(neuroSanURL, controller?.current.signal, query, selectedNetwork, handleChunk, agentChatHistory.chatContext, slyDataWithUserName, currentUser, StreamingUnit.Line);
289
288
  }
290
289
  }
291
290
  catch (error) {
@@ -303,7 +302,6 @@ export const ChatCommon = ({ ref, ...props }) => {
303
302
  id: uuid(),
304
303
  role: MessageRole.Error,
305
304
  text: `Error occurred: ${error}`,
306
- alwaysShow: true,
307
305
  });
308
306
  }
309
307
  }
@@ -318,12 +316,83 @@ export const ChatCommon = ({ ref, ...props }) => {
318
316
  handleChunk,
319
317
  legacyAgentEndpoint,
320
318
  neuroSanURL,
321
- targetAgent,
319
+ selectedNetwork,
322
320
  ]);
321
+ const getFinalAnswerErrorTurn = () => ({
322
+ id: uuid(),
323
+ role: MessageRole.Error,
324
+ text: "The agent did not provide a final answer in the expected format. This is an internal error.",
325
+ });
326
+ const handleFinalAnswerLegacyAgent = useCallback(() => {
327
+ const currentTurns = turnsRef.current;
328
+ // Prefer the most recent matching turn
329
+ const idx = currentTurns.reduceRight((foundIndex, turn, i) => foundIndex !== -1 || extractFinalAnswer(turn.text) === undefined ? foundIndex : i, -1);
330
+ if (idx === -1) {
331
+ if (givesFinalAnswer(selectedNetwork)) {
332
+ // This agent is supposed to give final answers, but didn't this time. An error.
333
+ setTurns((prev) => [...prev, getFinalAnswerErrorTurn()]);
334
+ return;
335
+ }
336
+ else {
337
+ // Use the last received turn as the final answer
338
+ const lastTurn = currentTurns.slice(-1)[0];
339
+ if (!lastTurn)
340
+ return;
341
+ // Just set the last turn as the final answer
342
+ setTurns((prev) => prev.map((turn) => (turn.id === lastTurn.id ? { ...turn, role: MessageRole.FinalAnswer } : turn)));
343
+ // Save it to chat history
344
+ updateChatHistory(selectedNetwork, [new AIMessage({ content: lastTurn.text, id: uuid() })]);
345
+ return;
346
+ }
347
+ }
348
+ const sourceTurn = currentTurns[idx];
349
+ // Save item to chat history (same as original behavior)
350
+ updateChatHistory(selectedNetwork, [new AIMessage({ content: sourceTurn.text, id: uuid() })]);
351
+ // Extract the final answer from the turn.
352
+ const finalAnswer = extractFinalAnswer(sourceTurn.text)?.trim();
353
+ // Update the turn to be a final answer turn, and add a new final answer turn with just the final answer text.
354
+ setTurns((prev) => {
355
+ const sourceTurnIndex = prev.findIndex(({ id: itemId }) => itemId === sourceTurn.id);
356
+ const updated = sourceTurnIndex === -1
357
+ ? [...prev]
358
+ : prev.map((turn, index) => (index === sourceTurnIndex ? { ...turn, text: sourceTurn.text } : turn));
359
+ // Add explicit final answer as a new terminal turn
360
+ updated.push({
361
+ id: uuid(),
362
+ role: MessageRole.FinalAnswer,
363
+ text: finalAnswer,
364
+ });
365
+ return updated;
366
+ });
367
+ }, [selectedNetwork, updateChatHistory]);
368
+ /**
369
+ * Extract the final answer from the turns for a Neuro-san agent. For Neuro-san agents, we expect the final answer
370
+ * to be the most recent turn messageType === ChatMessageType.AGENT_FRAMEWORK.
371
+ */
372
+ const handleFinalAnswerNeuroSanAgent = useCallback(() => {
373
+ // Get current turns snapshot
374
+ const currentTurns = turnsRef.current;
375
+ // Find the most recent turn that is from the agent framework, which should be the one that contains the
376
+ // final answer.
377
+ const idx = currentTurns.reduceRight((found, turn, i) => (found !== -1 || turn.messageType !== ChatMessageType.AGENT_FRAMEWORK ? found : i), -1);
378
+ // Check for final answer
379
+ if (idx === -1) {
380
+ // No final answer found in the turns. Should never happen for a Neuro-san agent.
381
+ setTurns((prev) => [...prev, getFinalAnswerErrorTurn()]);
382
+ return;
383
+ }
384
+ // Extract final answer from that turn
385
+ const finalAnswerTurn = currentTurns[idx];
386
+ // Update relevant turn to be the final answer
387
+ setTurns((prev) => prev.map((turn, i) => (i === idx ? { ...turn, role: MessageRole.FinalAnswer } : turn)));
388
+ // Save final answer to chat history
389
+ const finalAnswerContent = finalAnswerTurn.text || JSON.stringify(finalAnswerTurn.structure, null, 2);
390
+ updateChatHistory(selectedNetwork, [new AIMessage({ content: finalAnswerContent, id: uuid() })]);
391
+ }, [selectedNetwork, updateChatHistory]);
323
392
  const handleSend = useCallback(async (query) => {
324
393
  // Record user query in chat history. Discard anything beyond MAX_CHAT_HISTORY_ITEMS
325
394
  const userQueryMessage = new HumanMessage({ content: query, id: uuid() });
326
- updateChatHistory(targetAgent, [userQueryMessage]);
395
+ updateChatHistory(selectedNetwork, [userQueryMessage]);
327
396
  // Allow parent to intercept and modify the query before sending if needed
328
397
  const queryToSend = onSend?.(query) ?? query;
329
398
  // Save query for "regenerate" use. Again we save the real user input, not the modified query. It will again
@@ -338,56 +407,34 @@ export const ChatCommon = ({ ref, ...props }) => {
338
407
  id: uuid(),
339
408
  role: MessageRole.User,
340
409
  text: query,
341
- alwaysShow: true,
342
- });
343
- // Add ID block for agent
344
- addTurn({
345
- agentDisplayName,
346
- agentName: targetAgent,
347
- alwaysShow: true,
348
- id: uuid(),
349
- role: MessageRole.AgentHeader,
350
- text: agentDisplayName,
351
410
  });
352
411
  // Allow clients to do something when streaming starts
353
412
  onStreamingStarted?.();
354
413
  // Set up the abort controller
355
414
  controller.current = new AbortController();
356
415
  setIsAwaitingLlm(true);
357
- addTurn({
358
- agentName: `Contacting ${agentDisplayName}...`,
359
- id: uuid(),
360
- role: MessageRole.Agent,
361
- text: `Query: ${queryToSend}`,
362
- });
363
416
  try {
364
417
  // Invoke the logic to send the request and retry as necessary
365
418
  const wasAborted = await doRetryLoop(queryToSend);
366
- if (!wasAborted && !succeeded.current) {
367
- addTurn({
368
- alwaysShow: true,
369
- id: uuid(),
370
- role: MessageRole.Error,
371
- text: `Gave up after ${MAX_AGENT_RETRIES} attempts.`,
372
- });
373
- }
374
- // Display prominent "Final Answer" message if we have one
375
- if (lastAIMessage.current) {
376
- addTurn({
377
- alwaysShow: true,
378
- id: uuid(),
379
- role: MessageRole.FinalAnswer,
380
- text: lastAIMessage.current,
381
- });
382
- // Record bot answer in history.
383
- if (currentResponse?.current?.length > 0) {
384
- updateChatHistory(targetAgent, [new AIMessage({ content: lastAIMessage.current, id: uuid() })]);
419
+ // Abort condition is handled elsewhere
420
+ if (!wasAborted) {
421
+ if (succeeded.current) {
422
+ // Success: infer final answer depending on agent type
423
+ if (isLegacyAgentType(selectedNetwork)) {
424
+ handleFinalAnswerLegacyAgent();
425
+ }
426
+ else {
427
+ handleFinalAnswerNeuroSanAgent();
428
+ }
429
+ }
430
+ else {
431
+ // Exhausted retries without success. Display error to user.
432
+ addTurn({
433
+ id: uuid(),
434
+ role: MessageRole.Error,
435
+ text: `Gave up after ${MAX_AGENT_RETRIES} attempts.`,
436
+ });
385
437
  }
386
- }
387
- else if (isLegacyAgentType(targetAgent) && currentResponse.current.length > 0) {
388
- // It's a legacy agent that didn't provide a "Final Answer", so just record the whole response
389
- // as the bot answer in that case.
390
- updateChatHistory(targetAgent, [new AIMessage({ content: currentResponse.current, id: uuid() })]);
391
438
  }
392
439
  }
393
440
  finally {
@@ -397,14 +444,15 @@ export const ChatCommon = ({ ref, ...props }) => {
397
444
  }
398
445
  }, [
399
446
  addTurn,
400
- agentDisplayName,
401
447
  doRetryLoop,
448
+ handleFinalAnswerLegacyAgent,
449
+ handleFinalAnswerNeuroSanAgent,
402
450
  onSend,
403
451
  onStreamingComplete,
404
452
  onStreamingStarted,
405
453
  resetState,
406
454
  setIsAwaitingLlm,
407
- targetAgent,
455
+ selectedNetwork,
408
456
  updateChatHistory,
409
457
  ]);
410
458
  const handleStop = useCallback(() => {
@@ -412,7 +460,6 @@ export const ChatCommon = ({ ref, ...props }) => {
412
460
  controller?.current?.abort();
413
461
  controller.current = null;
414
462
  addTurn({
415
- alwaysShow: true,
416
463
  id: uuid(),
417
464
  role: MessageRole.Warning,
418
465
  text: "Request cancelled.",
@@ -429,31 +476,35 @@ export const ChatCommon = ({ ref, ...props }) => {
429
476
  // Enable regenerate button when there is a previous query to resent, and we're not awaiting a response
430
477
  const shouldEnableRegenerateButton = previousUserQuery && !isAwaitingLlm;
431
478
  // Enable Clear Chat button if not awaiting response and there is chat output to clear
432
- const enableClearChatButton = !isAwaitingLlm && turns.length > 0;
433
- const getPlaceholder = () => !targetAgent ? null : agentPlaceholders[targetAgent] || `Chat with ${agentDisplayName}`;
479
+ const enableClearChatButton = !isAwaitingLlm && (turns.length > 0 || agentChatHistory?.chatHistory?.length > 0);
480
+ const getPlaceholder = () => selectedNetwork ? agentPlaceholders[selectedNetwork] || `Chat with ${agentDisplayName}` : null;
434
481
  const handleClearChat = useCallback(() => {
435
482
  setTurns([]);
436
- resetHistory(targetAgent);
483
+ resetHistory(selectedNetwork);
437
484
  setPreviousUserQuery("");
438
485
  currentResponse.current = "";
439
- lastAIMessage.current = "";
440
- }, [resetHistory, targetAgent]);
486
+ }, [resetHistory, selectedNetwork]);
441
487
  // Expose the handleStop and handleClearChat methods to parent components via ref for external control
442
488
  useImperativeHandle(ref, () => ({
443
489
  handleStop,
444
490
  handleClearChat,
445
491
  }), [handleStop, handleClearChat]);
446
- const getNoAgentOverlay = () => (_jsx(Tooltip, { title: "Please select a Network from the list to start the chat.", placement: "auto", children: _jsx(Box, { id: "chat-disabled-overlay", sx: {
492
+ const getErrorOverlay = (errorText) => (_jsx(Box, { id: "chat-disabled-overlay", sx: {
493
+ position: "absolute",
494
+ top: 0,
495
+ left: 0,
496
+ right: 0,
497
+ bottom: 0,
498
+ zIndex: theme.zIndex.modal - 1,
499
+ cursor: "not-allowed",
500
+ // Capture all pointer events to prevent interaction with the chat when no agent is selected
501
+ pointerEvents: "all",
502
+ }, children: _jsx(Typography, { sx: {
447
503
  position: "absolute",
448
- top: 0,
449
- left: 0,
450
- right: 0,
451
- bottom: 0,
452
- zIndex: theme.zIndex.modal - 1,
453
- cursor: "not-allowed",
454
- // Capture all pointer events to prevent interaction with the chat when no agent is selected
455
- pointerEvents: "all",
456
- } }) }));
504
+ top: "50%",
505
+ left: "50%",
506
+ transform: "translate(-50%, -50%)",
507
+ }, children: errorText }) }));
457
508
  const getTitle = () => (_jsxs(Box, { id: `llm-chat-title-container-${id}`, sx: {
458
509
  alignItems: "center",
459
510
  borderTopLeftRadius: "var(--bs-border-radius)",
@@ -465,31 +516,59 @@ export const ChatCommon = ({ ref, ...props }) => {
465
516
  paddingTop: "0.25rem",
466
517
  paddingBottom: "0.25rem",
467
518
  }, 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}` }) }))] }));
468
- const getOptionsButtons = () => (_jsxs(_Fragment, { children: [_jsx(Tooltip, { id: "show-thinking", title: showThinking ? "Displaying agent thinking" : "Hiding agent thinking", children: _jsx("span", { id: "show-thinking-span", children: _jsx(LlmChatOptionsButton, { enabled: showThinking, id: "show-thinking-button", onClick: () => setShowThinking(!showThinking), posRight: 150, disabled: isAwaitingLlm, children: _jsx(AccountTreeIcon, { id: "show-thinking-icon", sx: { color: "var(--bs-white)", fontSize: "0.85rem" } }) }) }) }), _jsx(Tooltip, { id: "enable-autoscroll", title: autoScrollEnabled ? "Autoscroll enabled" : "Autoscroll disabled", children: _jsx(LlmChatOptionsButton, { enabled: autoScrollEnabled, id: "autoscroll-button", onClick: () => setAutoScrollEnabled(!autoScrollEnabled), posRight: 80, children: _jsx(VerticalAlignBottomIcon, { id: "autoscroll-icon", sx: { color: "var(--bs-white)", fontSize: "0.85rem" } }) }) }), _jsx(Tooltip, { id: "wrap-tooltip", title: shouldWrapOutput ? "Text wrapping enabled" : "Text wrapping disabled", children: _jsx(LlmChatOptionsButton, { enabled: shouldWrapOutput, id: "wrap-button", onClick: () => setShouldWrapOutput(!shouldWrapOutput), posRight: 10, children: _jsx(WrapTextIcon, { id: "wrap-icon", sx: { color: "var(--bs-white)", fontSize: "0.85rem" } }) }) })] }));
469
- const agentIntro = (_jsx(AgentIntro, { agentDisplayName: agentDisplayName, customAgentGreetings: customAgentGreetings, targetAgent: targetAgent }, targetAgent));
519
+ const getOptionsMenuButton = () => (_jsx(Box, { sx: {
520
+ position: "absolute",
521
+ top: "0.25rem",
522
+ right: "0.0rem",
523
+ }, children: _jsx(IconButton, { onClick: (e) => {
524
+ setOptionsMenuAnchorEl(e.currentTarget);
525
+ setOptionsMenuOpen(true);
526
+ }, children: _jsx(TuneIcon, { sx: { fontSize: "1.2rem" } }) }) }));
527
+ const agentGreeting = customAgentGreetings[selectedNetwork] ?? "Hi, how can I help?";
528
+ const handleOptionsMenuClose = () => {
529
+ setOptionsMenuAnchorEl(null);
530
+ setOptionsMenuOpen(false);
531
+ };
532
+ const handleToggleAutoScroll = () => {
533
+ setAutoScrollEnabled((prev) => !prev);
534
+ };
535
+ const handleToggleWrapOutput = () => {
536
+ setShouldWrapOutput((prev) => !prev);
537
+ };
538
+ const getOptionsMenu = () => (_jsxs(Menu, { id: `${id}-options-menu`, anchorEl: optionsMenuAnchorEl, open: optionsMenuOpen, onClose: handleOptionsMenuClose, slotProps: {
539
+ list: {
540
+ dense: true,
541
+ sx: {
542
+ py: 0,
543
+ "& .MuiMenuItem-root": { minHeight: 30, py: 0.5, px: 1 },
544
+ "& .MuiCheckbox-root": { p: 0.5 },
545
+ "& .MuiListItemText-primary": { fontSize: "smaller" },
546
+ },
547
+ },
548
+ }, 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" })] })] }));
470
549
  const getResponseBox = () => (_jsxs(Box, { id: "llm-response-div", sx: {
471
550
  ...divStyle,
472
551
  border: "var(--bs-border-width) var(--bs-border-style)",
473
552
  borderRadius: "var(--bs-border-radius)",
474
553
  display: "flex",
475
554
  flexGrow: 1,
476
- height: "100%",
477
- margin: "10px",
555
+ marginLeft: "10px",
478
556
  position: "relative",
479
557
  overflowY: "auto",
480
- }, children: [getOptionsButtons(), _jsxs(Box, { id: "llm-responses", ref: chatOutputRef, sx: {
481
- backgroundColor: backgroundColor || undefined,
482
- borderWidth: "1px",
558
+ }, children: [_jsxs(Box, { id: "llm-responses", ref: chatOutputRef, sx: {
559
+ backgroundColor,
483
560
  borderRadius: "0.5rem",
484
- fontSize: "smaller",
485
- resize: "none",
486
- overflowY: "auto", // Enable vertical scrollbar
561
+ fontSize: "16px",
562
+ overflowY: "auto",
487
563
  paddingBottom: "60px",
488
- paddingTop: "7.5px",
489
564
  paddingLeft: "15px",
490
565
  paddingRight: "15px",
566
+ paddingTop: "7.5px",
567
+ scrollbarGutter: "stable",
491
568
  width: "100%",
492
- }, tabIndex: -1, children: [agentChatHistory?.chatHistory?.length > 0 && (_jsx(ChatHistory, { agentImage: AGENT_IMAGE, agentDisplayName: agentDisplayName, chatHistoryKey: CHAT_HISTORY_KEY, currentUser: currentUser, id: `${id}-chat-history`, messages: agentChatHistory.chatHistory, targetAgent: targetAgent, userImage: userImage })), !isLegacyAgentType(targetAgent) && agentIntro, !isLegacyAgentType(targetAgent) && (_jsx(AgentMetadata, { disableQueries: isAwaitingLlm, handleSend: handleSend, currentUser: currentUser, id: `${id}-agent-metadata-display`, neuroSanURL: neuroSanURL, targetAgent: targetAgent })), _jsx(Conversation, { id: `${id}-conversation-display`, currentUser: currentUser, finalAnswerRef: finalAnswerRef, showThinking: showThinking, shouldWrapOutput: shouldWrapOutput, turns: turns, userImage: userImage }), isLegacyAgentType(targetAgent) && agentIntro, isAwaitingLlm && (_jsxs(Box, { id: "awaitingOutputContainer", sx: { display: "flex", alignItems: "center", fontSize: "smaller" }, children: [_jsx("span", { id: "working-span", style: { marginRight: "1rem" }, children: "Working..." }), _jsx(CircularProgress, { id: "awaitingOutputSpinner", sx: {
569
+ }, 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: [selectedNetwork, 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(selectedNetwork), shouldWrapOutput: shouldWrapOutput, turns: turns }), !isAwaitingLlm && turns.length > 0 && (
570
+ // Only show thinking once streaming is complete
571
+ _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: {
493
572
  color: "var(--bs-primary)",
494
573
  }, size: "1rem" })] }))] }), _jsx(ControlButtons, { enableClearChatButton: enableClearChatButton, handleClearChat: handleClearChat, handleSend: handleSend, handleStop: handleStop, isAwaitingLlm: isAwaitingLlm, previousUserQuery: previousUserQuery, shouldEnableRegenerateButton: shouldEnableRegenerateButton })] }));
495
574
  const getUserInputBox = () => (_jsxs(Box, { id: "user-input-div", sx: {
@@ -503,7 +582,7 @@ export const ChatCommon = ({ ref, ...props }) => {
503
582
  borderRadius: "var(--bs-border-radius)",
504
583
  display: "flex",
505
584
  flexGrow: 1,
506
- fontSize: "smaller",
585
+ fontSize: "17px",
507
586
  marginRight: "0.75rem",
508
587
  paddingBottom: "0.5rem",
509
588
  paddingTop: "0.5rem",
@@ -531,8 +610,8 @@ export const ChatCommon = ({ ref, ...props }) => {
531
610
  flexDirection: "column",
532
611
  flexGrow: 1,
533
612
  height: "100%",
534
- opacity: targetAgent ? 1 : 0.4,
535
- pointerEvents: targetAgent ? "auto" : "none",
613
+ opacity: selectedNetwork ? 1 : 0.4,
614
+ pointerEvents: selectedNetwork ? "auto" : "none",
536
615
  position: "relative",
537
616
  }, children: [title && getTitle(), getResponseBox(), getUserInputBox()] }));
538
617
  return (_jsx(Box, { id: `llm-chat-${id}`, sx: {
@@ -541,5 +620,10 @@ export const ChatCommon = ({ ref, ...props }) => {
541
620
  flexGrow: 1,
542
621
  height: "100%",
543
622
  position: "relative",
544
- }, children: targetAgent ? getChatBox() : getNoAgentOverlay() }));
623
+ }, children: selectedNetwork
624
+ ? missingApiKeys?.length === 0
625
+ ? getChatBox()
626
+ : getErrorOverlay(`API key(s) required for: ${missingApiKeys.join(", ")}. ` +
627
+ "Please add the required key(s) in Settings to use this Network.")
628
+ : getErrorOverlay("Please select a Network from the list to start the chat.") }));
545
629
  };