@cognizant-ai-lab/ui-common 1.5.1 → 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 (47) hide show
  1. package/dist/components/AgentChat/ChatCommon/ChatCommon.d.ts +9 -0
  2. package/dist/components/AgentChat/ChatCommon/ChatCommon.js +220 -138
  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/Common/AccordionLite.d.ts +14 -0
  20. package/dist/components/Common/AccordionLite.js +25 -0
  21. package/dist/components/Common/ConfirmationModal.d.ts +1 -1
  22. package/dist/components/Common/MUIAlert.d.ts +1 -0
  23. package/dist/components/Common/MUIAlert.js +3 -4
  24. package/dist/components/MultiAgentAccelerator/AgentFlow.d.ts +1 -0
  25. package/dist/components/MultiAgentAccelerator/AgentFlow.js +154 -59
  26. package/dist/components/MultiAgentAccelerator/AgentNode.d.ts +1 -0
  27. package/dist/components/MultiAgentAccelerator/AgentNode.js +46 -45
  28. package/dist/components/MultiAgentAccelerator/GraphLayouts.js +12 -4
  29. package/dist/components/MultiAgentAccelerator/MultiAgentAccelerator.js +30 -10
  30. package/dist/index.d.ts +0 -1
  31. package/dist/index.js +0 -1
  32. package/dist/tsconfig.build.tsbuildinfo +1 -1
  33. package/package.json +2 -2
  34. package/dist/components/AgentChat/ChatCommon/AgentConnectivity.d.ts +0 -14
  35. package/dist/components/AgentChat/ChatCommon/AgentConnectivity.js +0 -23
  36. package/dist/components/AgentChat/ChatCommon/AgentIntro.d.ts +0 -12
  37. package/dist/components/AgentChat/ChatCommon/AgentIntro.js +0 -19
  38. package/dist/components/AgentChat/ChatCommon/AgentMetadata.d.ts +0 -14
  39. package/dist/components/AgentChat/ChatCommon/AgentMetadata.js +0 -43
  40. package/dist/components/AgentChat/ChatCommon/Const.d.ts +0 -1
  41. package/dist/components/AgentChat/ChatCommon/Const.js +0 -2
  42. package/dist/components/AgentChat/ChatCommon/Greetings.d.ts +0 -1
  43. package/dist/components/AgentChat/ChatCommon/Greetings.js +0 -38
  44. package/dist/components/AgentChat/ChatCommon/UserQueryDisplay.d.ts +0 -7
  45. package/dist/components/AgentChat/ChatCommon/UserQueryDisplay.js +0 -32
  46. package/dist/components/Common/LlmChatOptionsButton.d.ts +0 -6
  47. package/dist/components/Common/LlmChatOptionsButton.js +0 -31
@@ -86,11 +86,20 @@ export interface ChatCommonProps {
86
86
  * to re-supply data that lives outside the IndexedDB slyData store (e.g. localStorage).
87
87
  */
88
88
  readonly extraSlyData?: Record<string, unknown>;
89
+ /**
90
+ * Optional description of the network to display in the UI.
91
+ */
92
+ readonly networkDescription?: string;
93
+ /**
94
+ * Sample queries for the current network that the user can "click to send"
95
+ */
96
+ readonly sampleQueries?: string[];
89
97
  }
90
98
  export type ChatCommonHandle = {
91
99
  handleStop: () => void;
92
100
  handleClearChat: () => void;
93
101
  };
102
+ export declare const MAX_TURNS = 50;
94
103
  /**
95
104
  * Common chat component for agent chat. This component is used by all agent chat components to provide a consistent
96
105
  * experience for users when chatting with agents. It handles user input as well as displaying and nicely formatting
@@ -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,41 @@ 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
35
  import Tooltip from "@mui/material/Tooltip";
33
36
  import Typography from "@mui/material/Typography";
37
+ import { isEmpty } from "lodash-es";
34
38
  import { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, } from "react";
35
39
  import { v4 as uuid } from "uuid";
36
- import { AgentIntro } from "./AgentIntro.js";
37
- import { AgentMetadata } from "./AgentMetadata.js";
38
40
  import { ChatHistory } from "./ChatHistory.js";
39
- import { AGENT_IMAGE } from "./Const.js";
40
41
  import { ControlButtons } from "./ControlButtons.js";
41
42
  import { Conversation } from "./Conversation.js";
42
43
  import { MessageRole } from "./ConversationTurn.js";
44
+ import { SampleQueries } from "./SampleQueries.js";
43
45
  import { SendButton } from "./SendButton.js";
46
+ import { Thinking } from "./Thinking.js";
44
47
  import { sendChatQuery } from "../../../controller/agent/Agent.js";
45
48
  import { sendLlmRequest, StreamingUnit } from "../../../controller/llm/LlmChat.js";
46
49
  import { ChatMessageType } from "../../../generated/neuro-san/NeuroSanClient.js";
47
50
  import { useAgentChatHistoryStore } from "../../../state/ChatHistory.js";
48
51
  import { hasOnlyWhitespace } from "../../../utils/text.js";
49
- import { LlmChatOptionsButton } from "../../Common/LlmChatOptionsButton.js";
50
- import { isLegacyAgentType } from "../Common/Types.js";
52
+ import { givesFinalAnswer, isLegacyAgentType } from "../Common/Types.js";
51
53
  import { chatMessageFromChunk, checkError, cleanUpAgentName, removeTrailingUuid } from "../Common/Utils.js";
52
54
  import { MicrophoneButton } from "../VoiceChat/MicrophoneButton.js";
53
55
  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
56
  // Define fancy EMPTY constant to avoid linter error about using object literals as default props
57
57
  const EMPTY = {};
58
58
  // How many times to retry the entire agent interaction process. Some networks have a well-defined success condition.
@@ -64,15 +64,15 @@ const MAX_AGENT_RETRIES = 3;
64
64
  * @returns The final answer from the agent, if it exists or undefined if it doesn't
65
65
  */
66
66
  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;
67
+ // Maximum number of turns to save
68
+ export const MAX_TURNS = 50;
69
69
  /**
70
70
  * Common chat component for agent chat. This component is used by all agent chat components to provide a consistent
71
71
  * experience for users when chatting with agents. It handles user input as well as displaying and nicely formatting
72
72
  * agent responses. Customization for inputs and outputs is provided via event handlers-like props.
73
73
  */
74
74
  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;
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;
76
76
  // MUI theme
77
77
  const theme = useTheme();
78
78
  // User LLM chat input
@@ -94,8 +94,9 @@ export const ChatCommon = ({ ref, ...props }) => {
94
94
  const [autoScrollEnabled, setAutoScrollEnabled] = useState(true);
95
95
  // Whether to wrap output text
96
96
  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("");
97
+ // Options menu control
98
+ const [optionsMenuAnchorEl, setOptionsMenuAnchorEl] = useState(null);
99
+ const [optionsMenuOpen, setOptionsMenuOpen] = useState(false);
99
100
  // Persistent agent chat history store, which is where we store both kinds of chat histories
100
101
  // (see store implementation for details)
101
102
  const storedChatHistory = useAgentChatHistoryStore((state) => state?.history?.[targetAgent]);
@@ -105,10 +106,8 @@ export const ChatCommon = ({ ref, ...props }) => {
105
106
  const updateChatHistory = useAgentChatHistoryStore((state) => state.updateChatHistory);
106
107
  const updateSlyData = useAgentChatHistoryStore((state) => state.updateSlyData);
107
108
  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);
109
+ // Ref copy of current turns, so we can safely use it in callbacks without worrying about stale closures
110
+ const turnsRef = useRef([]);
112
111
  // Microphone state for voice input
113
112
  const [isMicOn, setIsMicOn] = useState(false);
114
113
  // Ref for speech recognition
@@ -152,53 +151,42 @@ export const ChatCommon = ({ ref, ...props }) => {
152
151
  const container = chatOutputRef.current;
153
152
  if (!container)
154
153
  return;
155
- // Scroll the final answer into view
156
- if (finalAnswerRef.current && !isAwaitingLlm) {
157
- container.scrollTop = finalAnswerRef.current.offsetTop - 50;
158
- return;
159
- }
160
154
  // Live-streaming auto-scroll
161
155
  if (autoScrollEnabled) {
162
156
  container.scrollTop = container.scrollHeight;
163
157
  }
164
158
  }, [autoScrollEnabled, isAwaitingLlm, turns]);
159
+ // Keep a ref copy of the turns array
160
+ useEffect(() => {
161
+ turnsRef.current = turns;
162
+ }, [turns]);
165
163
  const addTurn = useCallback((turn) => {
166
164
  setTurns((current) => {
167
165
  const next = [...current, turn];
168
- return next.length > MAX_CHAT_OUTPUT_ITEMS ? next.slice(-MAX_CHAT_OUTPUT_ITEMS) : next;
166
+ return next.length > MAX_TURNS ? next.slice(-MAX_TURNS) : next;
169
167
  });
170
168
  }, []);
171
169
  // We use this to update the same "turn" as chunks come in from legacy agents
172
170
  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;
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
+ });
201
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) => {
202
190
  // For Neuro-san agents, we expect a ChatMessage structure in the chunk.
203
191
  const chatMessage = chatMessageFromChunk(chunk);
204
192
  if (!chatMessage) {
@@ -206,10 +194,6 @@ export const ChatCommon = ({ ref, ...props }) => {
206
194
  // But don't want to spam output by logging errors for every bad message.
207
195
  return;
208
196
  }
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
197
  // Shallow merge existing slyData with incoming chatMessage.sly_data
214
198
  if (chatMessage.sly_data) {
215
199
  updateSlyData(targetAgent, chatMessage.sly_data);
@@ -221,35 +205,53 @@ export const ChatCommon = ({ ref, ...props }) => {
221
205
  updateChatContext(targetAgent, chatMessage.chat_context);
222
206
  }
223
207
  // 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
- }
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;
236
217
  }
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.
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.
241
223
  const agentName = chatMessage.origin?.length > 0
242
224
  ? cleanUpAgentName(chatMessage.origin[chatMessage.origin.length - 1].tool)
243
- : "Agent message";
225
+ : "Agent";
244
226
  addTurn({
227
+ agentName,
245
228
  id: uuid(),
229
+ messageType: chatMessage.type,
246
230
  role: MessageRole.Agent,
247
- agentName,
231
+ structure: chatMessage.structure,
248
232
  text: chatMessage.text,
249
233
  });
250
- currentResponse.current += chatMessage.text;
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);
251
253
  }
252
- }, [onChunkReceived, targetAgent, addTurn, updateSlyData, updateChatContext]);
254
+ }, [onChunkReceived, targetAgent, handleNeuroSanAgentChunk, handleLegacyAgentChunk]);
253
255
  /**
254
256
  * Reset the state of the component. This is called after a request is completed, regardless of success or failure.
255
257
  */
@@ -257,8 +259,6 @@ export const ChatCommon = ({ ref, ...props }) => {
257
259
  // Reset state, whatever happened during request
258
260
  setIsAwaitingLlm(false);
259
261
  setChatInput("");
260
- lastAIMessage.current = "";
261
- finalAnswerRef.current = null;
262
262
  setPreviousResponse?.(targetAgent, currentResponse.current);
263
263
  currentResponse.current = "";
264
264
  legacyTurnIdRef.current = null;
@@ -303,7 +303,6 @@ export const ChatCommon = ({ ref, ...props }) => {
303
303
  id: uuid(),
304
304
  role: MessageRole.Error,
305
305
  text: `Error occurred: ${error}`,
306
- alwaysShow: true,
307
306
  });
308
307
  }
309
308
  }
@@ -320,6 +319,84 @@ export const ChatCommon = ({ ref, ...props }) => {
320
319
  neuroSanURL,
321
320
  targetAgent,
322
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]);
323
400
  const handleSend = useCallback(async (query) => {
324
401
  // Record user query in chat history. Discard anything beyond MAX_CHAT_HISTORY_ITEMS
325
402
  const userQueryMessage = new HumanMessage({ content: query, id: uuid() });
@@ -338,56 +415,34 @@ export const ChatCommon = ({ ref, ...props }) => {
338
415
  id: uuid(),
339
416
  role: MessageRole.User,
340
417
  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
418
  });
352
419
  // Allow clients to do something when streaming starts
353
420
  onStreamingStarted?.();
354
421
  // Set up the abort controller
355
422
  controller.current = new AbortController();
356
423
  setIsAwaitingLlm(true);
357
- addTurn({
358
- agentName: `Contacting ${agentDisplayName}...`,
359
- id: uuid(),
360
- role: MessageRole.Agent,
361
- text: `Query: ${queryToSend}`,
362
- });
363
424
  try {
364
425
  // Invoke the logic to send the request and retry as necessary
365
426
  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() })]);
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
+ }
437
+ }
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
+ });
385
445
  }
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
446
  }
392
447
  }
393
448
  finally {
@@ -397,8 +452,9 @@ export const ChatCommon = ({ ref, ...props }) => {
397
452
  }
398
453
  }, [
399
454
  addTurn,
400
- agentDisplayName,
401
455
  doRetryLoop,
456
+ handleFinalAnswerLegacyAgent,
457
+ handleFinalAnswerNeuroSanAgent,
402
458
  onSend,
403
459
  onStreamingComplete,
404
460
  onStreamingStarted,
@@ -412,7 +468,6 @@ export const ChatCommon = ({ ref, ...props }) => {
412
468
  controller?.current?.abort();
413
469
  controller.current = null;
414
470
  addTurn({
415
- alwaysShow: true,
416
471
  id: uuid(),
417
472
  role: MessageRole.Warning,
418
473
  text: "Request cancelled.",
@@ -429,14 +484,13 @@ export const ChatCommon = ({ ref, ...props }) => {
429
484
  // Enable regenerate button when there is a previous query to resent, and we're not awaiting a response
430
485
  const shouldEnableRegenerateButton = previousUserQuery && !isAwaitingLlm;
431
486
  // Enable Clear Chat button if not awaiting response and there is chat output to clear
432
- const enableClearChatButton = !isAwaitingLlm && turns.length > 0;
487
+ const enableClearChatButton = !isAwaitingLlm && (turns.length > 0 || agentChatHistory?.chatHistory?.length > 0);
433
488
  const getPlaceholder = () => !targetAgent ? null : agentPlaceholders[targetAgent] || `Chat with ${agentDisplayName}`;
434
489
  const handleClearChat = useCallback(() => {
435
490
  setTurns([]);
436
491
  resetHistory(targetAgent);
437
492
  setPreviousUserQuery("");
438
493
  currentResponse.current = "";
439
- lastAIMessage.current = "";
440
494
  }, [resetHistory, targetAgent]);
441
495
  // Expose the handleStop and handleClearChat methods to parent components via ref for external control
442
496
  useImperativeHandle(ref, () => ({
@@ -465,31 +519,59 @@ export const ChatCommon = ({ ref, ...props }) => {
465
519
  paddingTop: "0.25rem",
466
520
  paddingBottom: "0.25rem",
467
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}` }) }))] }));
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));
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" })] })] }));
470
552
  const getResponseBox = () => (_jsxs(Box, { id: "llm-response-div", sx: {
471
553
  ...divStyle,
472
554
  border: "var(--bs-border-width) var(--bs-border-style)",
473
555
  borderRadius: "var(--bs-border-radius)",
474
556
  display: "flex",
475
557
  flexGrow: 1,
476
- height: "100%",
477
- margin: "10px",
558
+ marginLeft: "10px",
478
559
  position: "relative",
479
560
  overflowY: "auto",
480
- }, children: [getOptionsButtons(), _jsxs(Box, { id: "llm-responses", ref: chatOutputRef, sx: {
481
- backgroundColor: backgroundColor || undefined,
482
- borderWidth: "1px",
561
+ }, children: [_jsxs(Box, { id: "llm-responses", ref: chatOutputRef, sx: {
562
+ backgroundColor,
483
563
  borderRadius: "0.5rem",
484
- fontSize: "smaller",
485
- resize: "none",
486
- overflowY: "auto", // Enable vertical scrollbar
564
+ fontSize: "16px",
565
+ overflowY: "auto",
487
566
  paddingBottom: "60px",
488
- paddingTop: "7.5px",
489
567
  paddingLeft: "15px",
490
568
  paddingRight: "15px",
569
+ paddingTop: "7.5px",
570
+ scrollbarGutter: "stable",
491
571
  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: {
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: {
493
575
  color: "var(--bs-primary)",
494
576
  }, size: "1rem" })] }))] }), _jsx(ControlButtons, { enableClearChatButton: enableClearChatButton, handleClearChat: handleClearChat, handleSend: handleSend, handleStop: handleStop, isAwaitingLlm: isAwaitingLlm, previousUserQuery: previousUserQuery, shouldEnableRegenerateButton: shouldEnableRegenerateButton })] }));
495
577
  const getUserInputBox = () => (_jsxs(Box, { id: "user-input-div", sx: {
@@ -503,7 +585,7 @@ export const ChatCommon = ({ ref, ...props }) => {
503
585
  borderRadius: "var(--bs-border-radius)",
504
586
  display: "flex",
505
587
  flexGrow: 1,
506
- fontSize: "smaller",
588
+ fontSize: "17px",
507
589
  marginRight: "0.75rem",
508
590
  paddingBottom: "0.5rem",
509
591
  paddingTop: "0.5rem",
@@ -1,14 +1,8 @@
1
1
  import { BaseMessage } from "@langchain/core/messages";
2
2
  import { FC } from "react";
3
3
  interface ChatHistoryProps {
4
- readonly agentDisplayName: string;
5
- readonly agentImage: string;
6
- readonly chatHistoryKey: string;
7
- readonly currentUser: string;
8
- readonly id: string;
9
4
  readonly messages: BaseMessage[];
10
- readonly targetAgent: string;
11
- readonly userImage: string;
5
+ readonly id: string;
12
6
  }
13
7
  /**
14
8
  * Component for displaying chat history from previous interactions with the agent.