@cognizant-ai-lab/ui-common 1.6.0 → 1.8.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 (72) hide show
  1. package/dist/Theme/Palettes.d.ts +0 -9
  2. package/dist/Theme/Palettes.js +0 -18
  3. package/dist/components/AgentChat/ChatCommon/ChatCommon.d.ts +11 -7
  4. package/dist/components/AgentChat/ChatCommon/ChatCommon.js +119 -90
  5. package/dist/components/AgentChat/ChatCommon/Const.d.ts +1 -0
  6. package/dist/components/AgentChat/ChatCommon/Const.js +2 -0
  7. package/dist/components/AgentChat/ChatCommon/ControlButtons.d.ts +1 -1
  8. package/dist/components/AgentChat/ChatCommon/ControlButtons.js +7 -4
  9. package/dist/components/AgentChat/ChatCommon/FormattedMarkdown.js +3 -0
  10. package/dist/components/AgentChat/ChatCommon/SendButton.js +7 -6
  11. package/dist/components/AgentChat/ChatCommon/SyntaxHighlighterThemes.js +1 -1
  12. package/dist/components/AgentChat/ChatCommon/Thinking.d.ts +1 -0
  13. package/dist/components/AgentChat/ChatCommon/Thinking.js +10 -7
  14. package/dist/components/AgentChat/Common/LlmChatButton.d.ts +0 -2
  15. package/dist/components/AgentChat/Common/LlmChatButton.js +6 -9
  16. package/dist/components/AgentChat/VoiceChat/MicrophoneButton.js +12 -10
  17. package/dist/components/ChatBot/ChatBot.d.ts +0 -4
  18. package/dist/components/ChatBot/ChatBot.js +2 -2
  19. package/dist/components/Common/Navbar.js +24 -22
  20. package/dist/components/Common/StatusLight.d.ts +9 -0
  21. package/dist/components/Common/StatusLight.js +21 -0
  22. package/dist/components/ErrorPage/ErrorBoundary.js +1 -1
  23. package/dist/components/ErrorPage/ErrorPage.d.ts +3 -3
  24. package/dist/components/ErrorPage/ErrorPage.js +4 -3
  25. package/dist/components/Logo/Common.d.ts +4 -0
  26. package/dist/components/Logo/Common.js +8 -0
  27. package/dist/components/Logo/CustomerLogo.d.ts +23 -0
  28. package/dist/components/Logo/CustomerLogo.js +41 -0
  29. package/dist/components/MultiAgentAccelerator/AgentConversations.js +0 -1
  30. package/dist/components/MultiAgentAccelerator/AgentFlow.js +85 -53
  31. package/dist/components/MultiAgentAccelerator/AgentNode.js +23 -27
  32. package/dist/components/MultiAgentAccelerator/GraphLayouts.d.ts +17 -10
  33. package/dist/components/MultiAgentAccelerator/GraphLayouts.js +4 -6
  34. package/dist/components/MultiAgentAccelerator/MultiAgentAccelerator.d.ts +3 -9
  35. package/dist/components/MultiAgentAccelerator/MultiAgentAccelerator.js +102 -47
  36. package/dist/components/MultiAgentAccelerator/Sidebar/AgentNetworkTreeItem.d.ts +0 -5
  37. package/dist/components/MultiAgentAccelerator/Sidebar/AgentNetworkTreeItem.js +30 -25
  38. package/dist/components/MultiAgentAccelerator/Sidebar/Sidebar.d.ts +3 -4
  39. package/dist/components/MultiAgentAccelerator/Sidebar/Sidebar.js +84 -168
  40. package/dist/components/MultiAgentAccelerator/Sidebar/TreeBuilder.d.ts +30 -13
  41. package/dist/components/MultiAgentAccelerator/Sidebar/TreeBuilder.js +157 -100
  42. package/dist/components/MultiAgentAccelerator/Tour/MainTourSteps.js +10 -8
  43. package/dist/components/MultiAgentAccelerator/const.d.ts +2 -0
  44. package/dist/components/MultiAgentAccelerator/const.js +4 -0
  45. package/dist/components/Settings/ApiKeyInput.d.ts +16 -0
  46. package/dist/components/Settings/ApiKeyInput.js +69 -0
  47. package/dist/components/Settings/InfoTip.d.ts +8 -0
  48. package/dist/components/Settings/InfoTip.js +14 -0
  49. package/dist/components/Settings/SettingsDialog.js +379 -178
  50. package/dist/components/Settings/SettingsRow.d.ts +18 -0
  51. package/dist/components/Settings/SettingsRow.js +35 -0
  52. package/dist/controller/agent/Agent.d.ts +3 -20
  53. package/dist/controller/agent/Agent.js +9 -43
  54. package/dist/controller/agent/IconSuggestions.d.ts +35 -0
  55. package/dist/controller/agent/IconSuggestions.js +85 -0
  56. package/dist/controller/llm/Providers.d.ts +2 -0
  57. package/dist/controller/llm/Providers.js +41 -0
  58. package/dist/index.d.ts +0 -2
  59. package/dist/index.js +0 -2
  60. package/dist/state/IconSuggestions.d.ts +27 -0
  61. package/dist/state/IconSuggestions.js +50 -0
  62. package/dist/state/Settings.d.ts +19 -3
  63. package/dist/state/Settings.js +24 -1
  64. package/dist/tsconfig.build.tsbuildinfo +1 -1
  65. package/dist/utils/title.js +1 -1
  66. package/package.json +5 -1
  67. package/dist/components/Common/CustomerLogo.d.ts +0 -17
  68. package/dist/components/Common/CustomerLogo.js +0 -49
  69. package/dist/components/Common/MUIAccordion.d.ts +0 -17
  70. package/dist/components/Common/MUIAccordion.js +0 -66
  71. package/dist/utils/useLocalStorage.d.ts +0 -1
  72. package/dist/utils/useLocalStorage.js +0 -55
@@ -7,12 +7,3 @@ export declare const PALETTES: {
7
7
  coldToHot: string[];
8
8
  visionImpaired: string[];
9
9
  };
10
- export type PaletteKey = keyof typeof PALETTES | "brand";
11
- /**
12
- * Custom hook to get the current color palette based on user settings.
13
- * If the user has selected custom branding, it will return the palette for that.
14
- * Otherwise, it will return one of the predefined palettes from the PALETTES object based on the user's selection.
15
- *
16
- * @returns An array of color hex codes representing the current color palette.
17
- */
18
- export declare const usePalette: () => string[];
@@ -1,5 +1,4 @@
1
1
  // Palettes for progressive coloring of nodes based on depth or heatmap value
2
- import { useSettingsStore } from "../state/Settings.js";
3
2
  export const PALETTES = {
4
3
  blue: [
5
4
  "#f7fbff",
@@ -75,20 +74,3 @@ export const PALETTES = {
75
74
  "#b6dbff",
76
75
  ],
77
76
  };
78
- /**
79
- * Custom hook to get the current color palette based on user settings.
80
- * If the user has selected custom branding, it will return the palette for that.
81
- * Otherwise, it will return one of the predefined palettes from the PALETTES object based on the user's selection.
82
- *
83
- * @returns An array of color hex codes representing the current color palette.
84
- */
85
- export const usePalette = () => {
86
- const brandPalette = useSettingsStore((state) => state.settings.branding.rangePalette);
87
- const paletteKey = useSettingsStore((state) => state.settings.appearance.rangePalette);
88
- if (paletteKey === "brand" && brandPalette) {
89
- return brandPalette;
90
- }
91
- else {
92
- return PALETTES[paletteKey];
93
- }
94
- };
@@ -1,4 +1,5 @@
1
1
  import { Dispatch, Ref, SetStateAction } from "react";
2
+ import { LLMProvider } from "../../../state/Settings.js";
2
3
  import { CombinedAgentType } from "../Common/Types.js";
3
4
  export interface ChatCommonProps {
4
5
  /**
@@ -9,10 +10,6 @@ export interface ChatCommonProps {
9
10
  * The current username of the logged-in user. Used for fetching things from APIs mainly
10
11
  */
11
12
  readonly currentUser: string;
12
- /**
13
- * Path to image for user avatar
14
- */
15
- readonly userImage: string;
16
13
  /**
17
14
  * Function to set the state of the component to indicate whether we are awaiting a response from the LLM
18
15
  */
@@ -22,9 +19,13 @@ export interface ChatCommonProps {
22
19
  */
23
20
  readonly isAwaitingLlm: boolean;
24
21
  /**
25
- * The agent to send the request to.
22
+ * The network to send the request to.
23
+ */
24
+ readonly selectedNetwork: string | null;
25
+ /**
26
+ * Setter for changing the selected network.
26
27
  */
27
- readonly targetAgent: string;
28
+ readonly setSelectedNetwork?: (network: string | null) => void;
28
29
  /**
29
30
  * Special endpoint for legacy agents since they do not have a single unified endpoint like Neuro-san agents.
30
31
  */
@@ -94,12 +95,15 @@ export interface ChatCommonProps {
94
95
  * Sample queries for the current network that the user can "click to send"
95
96
  */
96
97
  readonly sampleQueries?: string[];
98
+ /**
99
+ * Array of LLM providers for which API keys are required but missing. Only applies to BYOK networks.
100
+ */
101
+ readonly missingApiKeys?: LLMProvider[];
97
102
  }
98
103
  export type ChatCommonHandle = {
99
104
  handleStop: () => void;
100
105
  handleClearChat: () => void;
101
106
  };
102
- export declare const MAX_TURNS = 50;
103
107
  /**
104
108
  * Common chat component for agent chat. This component is used by all agent chat components to provide a consistent
105
109
  * 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 } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  /*
3
3
  Copyright 2025 Cognizant Technology Solutions Corp, www.cognizant.com.
4
4
 
@@ -18,6 +18,7 @@ limitations under the License.
18
18
  * See main function description.
19
19
  */
20
20
  import { AIMessage, HumanMessage } from "@langchain/core/messages";
21
+ import AddBoxRounded from "@mui/icons-material/AddBoxRounded";
21
22
  import ClearIcon from "@mui/icons-material/Clear";
22
23
  import CloseIcon from "@mui/icons-material/Close";
23
24
  import TuneIcon from "@mui/icons-material/Tune";
@@ -32,12 +33,12 @@ import ListItemText from "@mui/material/ListItemText";
32
33
  import Menu from "@mui/material/Menu";
33
34
  import MenuItem from "@mui/material/MenuItem";
34
35
  import { useTheme } from "@mui/material/styles";
35
- import Tooltip from "@mui/material/Tooltip";
36
36
  import Typography from "@mui/material/Typography";
37
37
  import { isEmpty } from "lodash-es";
38
38
  import { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, } from "react";
39
39
  import { v4 as uuid } from "uuid";
40
40
  import { ChatHistory } from "./ChatHistory.js";
41
+ import { MAX_TURNS } from "./Const.js";
41
42
  import { ControlButtons } from "./ControlButtons.js";
42
43
  import { Conversation } from "./Conversation.js";
43
44
  import { MessageRole } from "./ConversationTurn.js";
@@ -48,11 +49,16 @@ import { sendChatQuery } from "../../../controller/agent/Agent.js";
48
49
  import { sendLlmRequest, StreamingUnit } from "../../../controller/llm/LlmChat.js";
49
50
  import { ChatMessageType } from "../../../generated/neuro-san/NeuroSanClient.js";
50
51
  import { useAgentChatHistoryStore } from "../../../state/ChatHistory.js";
52
+ import { useSettingsStore } from "../../../state/Settings.js";
51
53
  import { hasOnlyWhitespace } from "../../../utils/text.js";
54
+ import { getZIndex } from "../../../utils/zIndexLayers.js";
55
+ import { AGENT_NETWORK_DESIGNER_ID } from "../../MultiAgentAccelerator/const.js";
52
56
  import { givesFinalAnswer, isLegacyAgentType } from "../Common/Types.js";
53
57
  import { chatMessageFromChunk, checkError, cleanUpAgentName, removeTrailingUuid } from "../Common/Utils.js";
54
58
  import { MicrophoneButton } from "../VoiceChat/MicrophoneButton.js";
55
59
  import { cleanupAndStopSpeechRecognition, setupSpeechRecognition } from "../VoiceChat/VoiceChat.js";
60
+ //#endregion
61
+ //#region Constants
56
62
  // Define fancy EMPTY constant to avoid linter error about using object literals as default props
57
63
  const EMPTY = {};
58
64
  // How many times to retry the entire agent interaction process. Some networks have a well-defined success condition.
@@ -64,15 +70,14 @@ const MAX_AGENT_RETRIES = 3;
64
70
  * @returns The final answer from the agent, if it exists or undefined if it doesn't
65
71
  */
66
72
  const extractFinalAnswer = (response) => /Final Answer: (?<finalAnswerText>.*)/su.exec(response)?.groups?.["finalAnswerText"];
67
- // Maximum number of turns to save
68
- export const MAX_TURNS = 50;
73
+ //#endregion
69
74
  /**
70
75
  * Common chat component for agent chat. This component is used by all agent chat components to provide a consistent
71
76
  * experience for users when chatting with agents. It handles user input as well as displaying and nicely formatting
72
77
  * agent responses. Customization for inputs and outputs is provided via event handlers-like props.
73
78
  */
74
79
  export const ChatCommon = ({ ref, ...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;
80
+ const { agentPlaceholders = EMPTY, backgroundColor, currentUser, customAgentGreetings = EMPTY, extraParams, extraSlyData, id, isAwaitingLlm, legacyAgentEndpoint, missingApiKeys = [], networkDescription, neuroSanURL, onChunkReceived, onClose, onSend, onStreamingComplete, onStreamingStarted, sampleQueries, selectedNetwork, setIsAwaitingLlm, setPreviousResponse, setSelectedNetwork, title, } = props;
76
81
  // MUI theme
77
82
  const theme = useTheme();
78
83
  // User LLM chat input
@@ -99,8 +104,10 @@ export const ChatCommon = ({ ref, ...props }) => {
99
104
  const [optionsMenuOpen, setOptionsMenuOpen] = useState(false);
100
105
  // Persistent agent chat history store, which is where we store both kinds of chat histories
101
106
  // (see store implementation for details)
102
- const storedChatHistory = useAgentChatHistoryStore((state) => state?.history?.[targetAgent]);
107
+ const storedChatHistory = useAgentChatHistoryStore((state) => selectedNetwork ? state?.history?.[selectedNetwork] : undefined);
103
108
  const agentChatHistory = useMemo(() => storedChatHistory ?? { chatHistory: [], chatContext: null, slyData: {} }, [storedChatHistory]);
109
+ // Display option for agent/network names
110
+ const useNativeNames = useSettingsStore((state) => state.settings.appearance.useNativeNames);
104
111
  // Access store for context items
105
112
  const updateChatContext = useAgentChatHistoryStore((state) => state.updateChatContext);
106
113
  const updateChatHistory = useAgentChatHistoryStore((state) => state.updateChatHistory);
@@ -135,7 +142,7 @@ export const ChatCommon = ({ ref, ...props }) => {
135
142
  };
136
143
  // Keeps track of whether the agent completed its task
137
144
  const succeeded = useRef(false);
138
- const agentDisplayName = useMemo(() => cleanUpAgentName(removeTrailingUuid(targetAgent)), [targetAgent]);
145
+ const networkDisplayName = useMemo(() => (useNativeNames ? selectedNetwork : cleanUpAgentName(removeTrailingUuid(selectedNetwork))), [selectedNetwork, useNativeNames]);
139
146
  useEffect(() => {
140
147
  // Set up speech recognition
141
148
  const handlers = setupSpeechRecognition(setChatInput, setVoiceInputState, speechRecognitionRef);
@@ -170,7 +177,11 @@ export const ChatCommon = ({ ref, ...props }) => {
170
177
  const legacyTurnIdRef = useRef(null);
171
178
  const handleLegacyAgentChunk = useCallback((chunk) => {
172
179
  currentResponse.current += chunk;
173
- if (!legacyTurnIdRef.current) {
180
+ if (legacyTurnIdRef.current) {
181
+ // We already have a turn for this response, so just update the text of that turn.
182
+ setTurns((prev) => prev.map((t) => (t.id === legacyTurnIdRef.current ? { ...t, text: currentResponse.current } : t)));
183
+ }
184
+ else {
174
185
  // We don't yet have a turn for this response, so create one. On subsequent chunks, we'll just
175
186
  // update the text of this turn.
176
187
  legacyTurnIdRef.current = uuid();
@@ -181,10 +192,6 @@ export const ChatCommon = ({ ref, ...props }) => {
181
192
  text: currentResponse.current,
182
193
  });
183
194
  }
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
195
  }, [addTurn]);
189
196
  const handleNeuroSanAgentChunk = useCallback((chunk) => {
190
197
  // For Neuro-san agents, we expect a ChatMessage structure in the chunk.
@@ -196,13 +203,13 @@ export const ChatCommon = ({ ref, ...props }) => {
196
203
  }
197
204
  // Shallow merge existing slyData with incoming chatMessage.sly_data
198
205
  if (chatMessage.sly_data) {
199
- updateSlyData(targetAgent, chatMessage.sly_data);
206
+ updateSlyData(selectedNetwork, chatMessage.sly_data);
200
207
  }
201
208
  // It's a ChatMessage. Does it have chat context? Only AGENT_FRAMEWORK messages can have chat context.
202
209
  if (chatMessage.type === ChatMessageType.AGENT_FRAMEWORK && chatMessage.chat_context) {
203
210
  // Save the chat context, potentially overwriting any previous ones we received during this session.
204
211
  // We only care about the last one received.
205
- updateChatContext(targetAgent, chatMessage.chat_context);
212
+ updateChatContext(selectedNetwork, chatMessage.chat_context);
206
213
  }
207
214
  // Check if there is an error block in the "structure" field of the chat message.
208
215
  const errorMessage = checkError(chatMessage.structure);
@@ -215,14 +222,12 @@ export const ChatCommon = ({ ref, ...props }) => {
215
222
  });
216
223
  succeeded.current = false;
217
224
  }
218
- else if (chatMessage?.text?.trim().length > 0 || chatMessage.structure) {
225
+ else if (chatMessage?.text?.trim().length > 0 || !isEmpty(chatMessage.structure)) {
219
226
  // Not an error, so output it if it has text or a structure.
220
227
  // This is the normal happy path for an incoming message.
221
228
  // The backend sometimes sends messages with no text content, and we don't want to display those to the
222
229
  // user. Agent name is the last tool in the origin array. If it's not there, use a default name.
223
- const agentName = chatMessage.origin?.length > 0
224
- ? cleanUpAgentName(chatMessage.origin[chatMessage.origin.length - 1].tool)
225
- : "Agent";
230
+ const agentName = chatMessage.origin?.at(-1)?.tool ?? "agent";
226
231
  addTurn({
227
232
  agentName,
228
233
  id: uuid(),
@@ -236,7 +241,7 @@ export const ChatCommon = ({ ref, ...props }) => {
236
241
  currentResponse.current += chatMessage.text;
237
242
  }
238
243
  }
239
- }, [addTurn, targetAgent, updateChatContext, updateSlyData]);
244
+ }, [addTurn, selectedNetwork, updateChatContext, updateSlyData]);
240
245
  /**
241
246
  * Handle a chunk of response from the server. Called each time the server streams a chunk.
242
247
  */
@@ -244,14 +249,14 @@ export const ChatCommon = ({ ref, ...props }) => {
244
249
  // Give container a chance to process the chunk first
245
250
  const onChunkReceivedResult = onChunkReceived?.(chunk) ?? true;
246
251
  succeeded.current = succeeded.current || onChunkReceivedResult;
247
- if (isLegacyAgentType(targetAgent)) {
252
+ if (isLegacyAgentType(selectedNetwork)) {
248
253
  // For legacy agents, we either get plain text or Markdown. Just output it as-is.
249
254
  handleLegacyAgentChunk(chunk);
250
255
  }
251
256
  else {
252
257
  handleNeuroSanAgentChunk(chunk);
253
258
  }
254
- }, [onChunkReceived, targetAgent, handleNeuroSanAgentChunk, handleLegacyAgentChunk]);
259
+ }, [onChunkReceived, selectedNetwork, handleNeuroSanAgentChunk, handleLegacyAgentChunk]);
255
260
  /**
256
261
  * Reset the state of the component. This is called after a request is completed, regardless of success or failure.
257
262
  */
@@ -259,10 +264,10 @@ export const ChatCommon = ({ ref, ...props }) => {
259
264
  // Reset state, whatever happened during request
260
265
  setIsAwaitingLlm(false);
261
266
  setChatInput("");
262
- setPreviousResponse?.(targetAgent, currentResponse.current);
267
+ setPreviousResponse?.(selectedNetwork, currentResponse.current);
263
268
  currentResponse.current = "";
264
269
  legacyTurnIdRef.current = null;
265
- }, [setIsAwaitingLlm, setPreviousResponse, targetAgent]);
270
+ }, [setIsAwaitingLlm, setPreviousResponse, selectedNetwork]);
266
271
  /*
267
272
  * The main logic for sending a query to the server, with retries on errors.
268
273
  */
@@ -275,7 +280,7 @@ export const ChatCommon = ({ ref, ...props }) => {
275
280
  // Increment the attempt number and set the state to indicate we're awaiting a response
276
281
  attemptNumber += 1;
277
282
  // Check which agent type we are dealing with
278
- if (isLegacyAgentType(targetAgent)) {
283
+ if (isLegacyAgentType(selectedNetwork)) {
279
284
  // It's a legacy agent (these go directly to the LLM and are different from
280
285
  // the Neuro-san agents).
281
286
  // Send the chat query to the server. This will block until the stream ends from the server
@@ -284,8 +289,8 @@ export const ChatCommon = ({ ref, ...props }) => {
284
289
  else {
285
290
  // It's a Neuro-san agent.
286
291
  // Some coded tools (data generator...) expect the username provided in slyData.
287
- const slyDataWithUserName = { ...agentChatHistory?.slyData, ...extraSlyData, login: currentUser };
288
- await sendChatQuery(neuroSanURL, controller?.current.signal, query, targetAgent, handleChunk, agentChatHistory.chatContext, slyDataWithUserName, currentUser, StreamingUnit.Line);
292
+ const slyDataWithUsername = { ...agentChatHistory?.slyData, ...extraSlyData, login: currentUser };
293
+ await sendChatQuery(neuroSanURL, controller?.current.signal, query, selectedNetwork, handleChunk, agentChatHistory.chatContext, slyDataWithUsername, currentUser, StreamingUnit.Line);
289
294
  }
290
295
  }
291
296
  catch (error) {
@@ -317,7 +322,7 @@ export const ChatCommon = ({ ref, ...props }) => {
317
322
  handleChunk,
318
323
  legacyAgentEndpoint,
319
324
  neuroSanURL,
320
- targetAgent,
325
+ selectedNetwork,
321
326
  ]);
322
327
  const getFinalAnswerErrorTurn = () => ({
323
328
  id: uuid(),
@@ -329,7 +334,7 @@ export const ChatCommon = ({ ref, ...props }) => {
329
334
  // Prefer the most recent matching turn
330
335
  const idx = currentTurns.reduceRight((foundIndex, turn, i) => foundIndex !== -1 || extractFinalAnswer(turn.text) === undefined ? foundIndex : i, -1);
331
336
  if (idx === -1) {
332
- if (givesFinalAnswer(targetAgent)) {
337
+ if (givesFinalAnswer(selectedNetwork)) {
333
338
  // This agent is supposed to give final answers, but didn't this time. An error.
334
339
  setTurns((prev) => [...prev, getFinalAnswerErrorTurn()]);
335
340
  return;
@@ -342,13 +347,13 @@ export const ChatCommon = ({ ref, ...props }) => {
342
347
  // Just set the last turn as the final answer
343
348
  setTurns((prev) => prev.map((turn) => (turn.id === lastTurn.id ? { ...turn, role: MessageRole.FinalAnswer } : turn)));
344
349
  // Save it to chat history
345
- updateChatHistory(targetAgent, [new AIMessage({ content: lastTurn.text, id: uuid() })]);
350
+ updateChatHistory(selectedNetwork, [new AIMessage({ content: lastTurn.text, id: uuid() })]);
346
351
  return;
347
352
  }
348
353
  }
349
354
  const sourceTurn = currentTurns[idx];
350
355
  // Save item to chat history (same as original behavior)
351
- updateChatHistory(targetAgent, [new AIMessage({ content: sourceTurn.text, id: uuid() })]);
356
+ updateChatHistory(selectedNetwork, [new AIMessage({ content: sourceTurn.text, id: uuid() })]);
352
357
  // Extract the final answer from the turn.
353
358
  const finalAnswer = extractFinalAnswer(sourceTurn.text)?.trim();
354
359
  // Update the turn to be a final answer turn, and add a new final answer turn with just the final answer text.
@@ -365,7 +370,7 @@ export const ChatCommon = ({ ref, ...props }) => {
365
370
  });
366
371
  return updated;
367
372
  });
368
- }, [targetAgent, updateChatHistory]);
373
+ }, [selectedNetwork, updateChatHistory]);
369
374
  /**
370
375
  * Extract the final answer from the turns for a Neuro-san agent. For Neuro-san agents, we expect the final answer
371
376
  * to be the most recent turn messageType === ChatMessageType.AGENT_FRAMEWORK.
@@ -384,23 +389,16 @@ export const ChatCommon = ({ ref, ...props }) => {
384
389
  }
385
390
  // Extract final answer from that turn
386
391
  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]);
392
+ // Update relevant turn to be the final answer
393
+ setTurns((prev) => prev.map((turn, i) => (i === idx ? { ...turn, role: MessageRole.FinalAnswer } : turn)));
394
+ // Save final answer to chat history
395
+ const finalAnswerContent = finalAnswerTurn.text || JSON.stringify(finalAnswerTurn.structure, null, 2);
396
+ updateChatHistory(selectedNetwork, [new AIMessage({ content: finalAnswerContent, id: uuid() })]);
397
+ }, [selectedNetwork, updateChatHistory]);
400
398
  const handleSend = useCallback(async (query) => {
401
399
  // Record user query in chat history. Discard anything beyond MAX_CHAT_HISTORY_ITEMS
402
400
  const userQueryMessage = new HumanMessage({ content: query, id: uuid() });
403
- updateChatHistory(targetAgent, [userQueryMessage]);
401
+ updateChatHistory(selectedNetwork, [userQueryMessage]);
404
402
  // Allow parent to intercept and modify the query before sending if needed
405
403
  const queryToSend = onSend?.(query) ?? query;
406
404
  // Save query for "regenerate" use. Again we save the real user input, not the modified query. It will again
@@ -428,7 +426,7 @@ export const ChatCommon = ({ ref, ...props }) => {
428
426
  if (!wasAborted) {
429
427
  if (succeeded.current) {
430
428
  // Success: infer final answer depending on agent type
431
- if (isLegacyAgentType(targetAgent)) {
429
+ if (isLegacyAgentType(selectedNetwork)) {
432
430
  handleFinalAnswerLegacyAgent();
433
431
  }
434
432
  else {
@@ -460,7 +458,7 @@ export const ChatCommon = ({ ref, ...props }) => {
460
458
  onStreamingStarted,
461
459
  resetState,
462
460
  setIsAwaitingLlm,
463
- targetAgent,
461
+ selectedNetwork,
464
462
  updateChatHistory,
465
463
  ]);
466
464
  const handleStop = useCallback(() => {
@@ -485,29 +483,34 @@ export const ChatCommon = ({ ref, ...props }) => {
485
483
  const shouldEnableRegenerateButton = previousUserQuery && !isAwaitingLlm;
486
484
  // Enable Clear Chat button if not awaiting response and there is chat output to clear
487
485
  const enableClearChatButton = !isAwaitingLlm && (turns.length > 0 || agentChatHistory?.chatHistory?.length > 0);
488
- const getPlaceholder = () => !targetAgent ? null : agentPlaceholders[targetAgent] || `Chat with ${agentDisplayName}`;
486
+ const getPlaceholder = () => selectedNetwork ? agentPlaceholders[selectedNetwork] || `Chat with ${networkDisplayName}` : null;
489
487
  const handleClearChat = useCallback(() => {
490
488
  setTurns([]);
491
- resetHistory(targetAgent);
489
+ resetHistory(selectedNetwork);
492
490
  setPreviousUserQuery("");
493
491
  currentResponse.current = "";
494
- }, [resetHistory, targetAgent]);
492
+ }, [resetHistory, selectedNetwork]);
495
493
  // Expose the handleStop and handleClearChat methods to parent components via ref for external control
496
494
  useImperativeHandle(ref, () => ({
497
495
  handleStop,
498
496
  handleClearChat,
499
497
  }), [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: {
498
+ const getErrorOverlay = (errorText) => (_jsx(Box, { id: "chat-disabled-overlay", sx: {
499
+ position: "absolute",
500
+ top: 0,
501
+ left: 0,
502
+ right: 0,
503
+ bottom: 0,
504
+ zIndex: getZIndex(2, theme),
505
+ cursor: "not-allowed",
506
+ // Capture all pointer events to prevent interaction with the chat when no agent is selected
507
+ pointerEvents: "all",
508
+ }, children: _jsx(Typography, { sx: {
501
509
  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
- } }) }));
510
+ top: "50%",
511
+ left: "50%",
512
+ transform: "translate(-50%, -50%)",
513
+ }, children: errorText }) }));
511
514
  const getTitle = () => (_jsxs(Box, { id: `llm-chat-title-container-${id}`, sx: {
512
515
  alignItems: "center",
513
516
  borderTopLeftRadius: "var(--bs-border-radius)",
@@ -519,15 +522,18 @@ export const ChatCommon = ({ ref, ...props }) => {
519
522
  paddingTop: "0.25rem",
520
523
  paddingBottom: "0.25rem",
521
524
  }, 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}` }) }))] }));
522
- const getOptionsMenuButton = () => (_jsx(Box, { sx: {
525
+ const getOptionsMenuButton = () => (_jsx(Box, { id: `${id}-options-menu-button-container`, sx: {
526
+ backgroundColor: theme.palette.background.paper,
527
+ borderRadius: "var(--bs-border-radius)",
523
528
  position: "absolute",
524
- top: "0.25rem",
525
- right: "0.0rem",
529
+ right: theme.spacing(2.5),
530
+ top: theme.spacing(1),
531
+ zIndex: getZIndex(2, theme),
526
532
  }, children: _jsx(IconButton, { onClick: (e) => {
527
533
  setOptionsMenuAnchorEl(e.currentTarget);
528
534
  setOptionsMenuOpen(true);
529
- }, children: _jsx(TuneIcon, { sx: { fontSize: "1.2rem" } }) }) }));
530
- const agentGreeting = customAgentGreetings[targetAgent] ?? "Hi, how can I help?";
535
+ }, children: _jsx(TuneIcon, { fontSize: "small" }) }) }));
536
+ const agentGreeting = customAgentGreetings[selectedNetwork] ?? "Hi, how can I help?";
531
537
  const handleOptionsMenuClose = () => {
532
538
  setOptionsMenuAnchorEl(null);
533
539
  setOptionsMenuOpen(false);
@@ -549,7 +555,22 @@ export const ChatCommon = ({ ref, ...props }) => {
549
555
  },
550
556
  },
551
557
  }, 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" })] })] }));
552
- const getResponseBox = () => (_jsxs(Box, { id: "llm-response-div", sx: {
558
+ const getNetworkIntro = () => (_jsxs(Box, { sx: { marginBottom: "0.5rem", marginTop: "1rem" }, children: [_jsxs(Typography, { component: "span", sx: { fontWeight: 700 }, variant: "inherit", children: [networkDisplayName, networkDescription && ":"] }), networkDescription && (_jsxs(Typography, { component: "span", sx: { ml: 0.5 }, variant: "inherit", children: [" ", networkDescription] }))] }));
559
+ const getChatHistory = () => (_jsx(_Fragment, { children: agentChatHistory?.chatHistory?.length > 0 && (_jsx(Box, { sx: { my: "0.5rem" }, children: _jsx(ChatHistory, { id: id, messages: agentChatHistory.chatHistory }) })) }));
560
+ const getAgentGreeting = () => _jsx(Box, { sx: { marginBottom: "0.5rem", marginTop: "1rem" }, children: agentGreeting });
561
+ const getControlButtons = () => (_jsx(Box, { sx: {
562
+ bottom: theme.spacing(3),
563
+ right: theme.spacing(3),
564
+ position: "absolute",
565
+ zIndex: getZIndex(2, theme),
566
+ }, children: _jsx(ControlButtons, { enableClearChatButton: enableClearChatButton, handleClearChat: handleClearChat, handleSend: handleSend, handleStop: handleStop, isAwaitingLlm: isAwaitingLlm, previousUserQuery: previousUserQuery, shouldEnableRegenerateButton: shouldEnableRegenerateButton }) }));
567
+ const getWorkingSpinner = () => (_jsx(_Fragment, { children: 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: {
568
+ color: "var(--bs-primary)",
569
+ }, size: "1rem" })] })) }));
570
+ const getThinking = () => (_jsx(_Fragment, { children: !isAwaitingLlm && turns.length > 0 && (
571
+ // Only show thinking once streaming is complete
572
+ _jsx(Thinking, { id: id, turns: turns, useNativeNames: useNativeNames })) }));
573
+ const getResponseBox = () => (_jsx(Box, { id: "llm-response-div", sx: {
553
574
  ...divStyle,
554
575
  border: "var(--bs-border-width) var(--bs-border-style)",
555
576
  borderRadius: "var(--bs-border-radius)",
@@ -558,27 +579,23 @@ export const ChatCommon = ({ ref, ...props }) => {
558
579
  marginLeft: "10px",
559
580
  position: "relative",
560
581
  overflowY: "auto",
561
- }, children: [_jsxs(Box, { id: "llm-responses", ref: chatOutputRef, sx: {
562
- backgroundColor,
563
- borderRadius: "0.5rem",
564
- fontSize: "16px",
565
- overflowY: "auto",
566
- paddingBottom: "60px",
567
- paddingLeft: "15px",
568
- paddingRight: "15px",
569
- paddingTop: "7.5px",
570
- scrollbarGutter: "stable",
571
- width: "100%",
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: {
575
- color: "var(--bs-primary)",
576
- }, size: "1rem" })] }))] }), _jsx(ControlButtons, { enableClearChatButton: enableClearChatButton, handleClearChat: handleClearChat, handleSend: handleSend, handleStop: handleStop, isAwaitingLlm: isAwaitingLlm, previousUserQuery: previousUserQuery, shouldEnableRegenerateButton: shouldEnableRegenerateButton })] }));
582
+ }, children: _jsxs(Box, { id: "llm-responses", ref: chatOutputRef, sx: {
583
+ backgroundColor,
584
+ borderRadius: "0.5rem",
585
+ fontSize: "16px",
586
+ overflowY: "auto",
587
+ paddingBottom: "60px",
588
+ paddingLeft: "15px",
589
+ paddingRight: "15px",
590
+ paddingTop: "7.5px",
591
+ scrollbarGutter: "stable",
592
+ width: "100%",
593
+ }, tabIndex: -1, children: [getOptionsMenu(), getOptionsMenuButton(), getChatHistory(), getNetworkIntro(), getAgentGreeting(), _jsx(SampleQueries, { disabled: isAwaitingLlm, handleSend: handleSend, sampleQueries: sampleQueries }), _jsx(Conversation, { id: id, includeAgentMessages: !givesFinalAnswer(selectedNetwork), shouldWrapOutput: shouldWrapOutput, turns: turns }), getThinking(), getWorkingSpinner(), getControlButtons()] }) }));
577
594
  const getUserInputBox = () => (_jsxs(Box, { id: "user-input-div", sx: {
578
595
  ...divStyle,
596
+ alignItems: "center",
579
597
  display: "flex",
580
598
  margin: "10px",
581
- alignItems: "flex-end",
582
599
  position: "relative",
583
600
  }, children: [_jsx(Input, { autoComplete: "off", id: "user-input", multiline: true, placeholder: getPlaceholder(), ref: chatInputRef, sx: {
584
601
  border: "var(--bs-border-style) var(--bs-border-width) var(--bs-gray-light)",
@@ -607,21 +624,33 @@ export const ChatCommon = ({ ref, ...props }) => {
607
624
  }, sx: {
608
625
  color: "var(--bs-primary)",
609
626
  opacity: userInputEmpty ? "25%" : "100%",
610
- }, disabled: userInputEmpty, tabIndex: -1, edge: "end", children: _jsx(ClearIcon, { id: "clear-input-icon" }) })] }) }), _jsx(MicrophoneButton, { isMicOn: isMicOn, onMicToggle: setIsMicOn, speechRecognitionRef: speechRecognitionRef, voiceInputState: voiceInputState, setVoiceInputState: setVoiceInputState }), _jsx(SendButton, { enableSendButton: shouldEnableSendButton, id: "submit-query-button", onClickCallback: () => handleSend(chatInput) })] }));
627
+ }, disabled: userInputEmpty, tabIndex: -1, edge: "end", children: _jsx(ClearIcon, { id: "clear-input-icon" }) })] }) }), _jsxs(Box, { sx: { display: "flex", gap: "0.25rem", position: "relative", alignItems: "center" }, children: [_jsx(MicrophoneButton, { isMicOn: isMicOn, onMicToggle: setIsMicOn, setVoiceInputState: setVoiceInputState, speechRecognitionRef: speechRecognitionRef, voiceInputState: voiceInputState }), _jsx(SendButton, { enableSendButton: shouldEnableSendButton, id: "submit-query-button", onClickCallback: () => handleSend(chatInput) })] })] }));
611
628
  const getChatBox = () => (_jsxs(Box, { id: `llm-chat-${id}`, sx: {
612
629
  display: "flex",
613
630
  flexDirection: "column",
614
631
  flexGrow: 1,
615
632
  height: "100%",
616
- opacity: targetAgent ? 1 : 0.4,
617
- pointerEvents: targetAgent ? "auto" : "none",
633
+ opacity: selectedNetwork ? 1 : 0.4,
634
+ pointerEvents: selectedNetwork ? "auto" : "none",
618
635
  position: "relative",
619
636
  }, children: [title && getTitle(), getResponseBox(), getUserInputBox()] }));
637
+ const getSelectNetworkOverlayBody = () => (_jsxs(Typography, { component: "span", children: ["Please select a Network from the list to start the chat, or click", _jsx(IconButton, { onClick: () => setSelectedNetwork?.(AGENT_NETWORK_DESIGNER_ID), "aria-label": "select-network-designer", sx: {
638
+ px: 0.5,
639
+ verticalAlign: "middle",
640
+ mb: "4px",
641
+ }, children: _jsx(AddBoxRounded, { sx: {
642
+ color: "var(--bs-secondary)",
643
+ fontSize: "1rem",
644
+ } }) }), "to design your own network!"] }));
645
+ const allApiKeysPresent = missingApiKeys?.length === 0;
646
+ const getMissingApiKeysOverlayBody = () => (_jsx(Typography, { component: "span", children: `API key(s) required for: ${missingApiKeys.join(", ")}. Please add the required key(s) in
647
+ "Settings" to use this Network.` }));
620
648
  return (_jsx(Box, { id: `llm-chat-${id}`, sx: {
621
- display: "flex",
622
- flexDirection: "column",
623
- flexGrow: 1,
624
649
  height: "100%",
625
650
  position: "relative",
626
- }, children: targetAgent ? getChatBox() : getNoAgentOverlay() }));
651
+ }, children: selectedNetwork
652
+ ? allApiKeysPresent
653
+ ? getChatBox()
654
+ : getErrorOverlay(getMissingApiKeysOverlayBody())
655
+ : getErrorOverlay(getSelectNetworkOverlayBody()) }));
627
656
  };
@@ -0,0 +1 @@
1
+ export declare const MAX_TURNS = 50;
@@ -0,0 +1,2 @@
1
+ // Maximum number of turns to save
2
+ export const MAX_TURNS = 50;
@@ -9,7 +9,7 @@ interface ControlButtonsProps {
9
9
  shouldEnableRegenerateButton: boolean;
10
10
  }
11
11
  /**
12
- * Generate the Control Buttons for a chat window.
12
+ * Generate the Control Buttons for a chat window (resend, clear chat, stop)
13
13
  * @returns A fragment containing the Control Buttons.
14
14
  */
15
15
  export declare const ControlButtons: FC<ControlButtonsProps>;
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  /*
3
3
  Copyright 2025 Cognizant Technology Solutions Corp, www.cognizant.com.
4
4
 
@@ -17,10 +17,13 @@ limitations under the License.
17
17
  import DeleteOutlined from "@mui/icons-material/DeleteOutlined";
18
18
  import Loop from "@mui/icons-material/Loop";
19
19
  import StopCircle from "@mui/icons-material/StopCircle";
20
+ import Box from "@mui/material/Box";
20
21
  import { SmallLlmChatButton } from "../Common/LlmChatButton.js";
21
- // #endregion: Types
22
+ //#endregion: Types
22
23
  /**
23
- * Generate the Control Buttons for a chat window.
24
+ * Generate the Control Buttons for a chat window (resend, clear chat, stop)
24
25
  * @returns A fragment containing the Control Buttons.
25
26
  */
26
- export const ControlButtons = ({ handleClearChat, enableClearChatButton, isAwaitingLlm, handleSend, handleStop, previousUserQuery, shouldEnableRegenerateButton, }) => (_jsxs(_Fragment, { children: [!isAwaitingLlm && (_jsx(SmallLlmChatButton, { "aria-label": "Clear Chat", disabled: !enableClearChatButton, id: "clear-chat-button", onClick: handleClearChat, posBottom: 8, posRight: 65, children: _jsx(DeleteOutlined, { fontSize: "small", id: "stop-button-icon", sx: { color: "var(--bs-white)" } }) })), isAwaitingLlm && (_jsx(SmallLlmChatButton, { "aria-label": "Stop", disabled: !isAwaitingLlm, id: "stop-output-button", onClick: () => handleStop(), posBottom: 8, posRight: 23, children: _jsx(StopCircle, { fontSize: "small", id: "stop-button-icon", sx: { color: "var(--bs-white)" } }) })), !isAwaitingLlm && (_jsx(SmallLlmChatButton, { "aria-label": "Regenerate", disabled: !shouldEnableRegenerateButton, id: "regenerate-output-button", onClick: () => handleSend(previousUserQuery), posBottom: 8, posRight: 23, children: _jsx(Loop, { fontSize: "small", id: "generate-icon", sx: { color: "var(--bs-white)" } }) }))] }));
27
+ export const ControlButtons = ({ handleClearChat, enableClearChatButton, isAwaitingLlm, handleSend, handleStop, previousUserQuery, shouldEnableRegenerateButton, }) => (_jsx(_Fragment, { children: isAwaitingLlm ? (
28
+ // Stop Button
29
+ _jsx(SmallLlmChatButton, { "aria-label": "Stop", disabled: !isAwaitingLlm, id: "stop-output-button", onClick: () => handleStop(), children: _jsx(StopCircle, { fontSize: "small", id: "stop-button-icon" }) })) : (_jsxs(Box, { sx: { display: "flex", gap: 1 }, children: [_jsx(SmallLlmChatButton, { "aria-label": "Clear Chat", disabled: !enableClearChatButton, id: "clear-chat-button", onClick: handleClearChat, children: _jsx(DeleteOutlined, { fontSize: "small", id: "stop-button-icon" }) }), _jsx(SmallLlmChatButton, { "aria-label": "Regenerate", disabled: !shouldEnableRegenerateButton, id: "regenerate-output-button", onClick: () => handleSend(previousUserQuery), children: _jsx(Loop, { fontSize: "small", id: "generate-icon" }) })] })) }));
@@ -53,6 +53,9 @@ export const FormattedMarkdown = ({ id, nodesList, style, wrapLongLines = false,
53
53
  // If not, do our best to synthesize a key
54
54
  return `${hashString(node?.toString() || "")}-${index}`;
55
55
  };
56
+ if (!nodesList || nodesList.length === 0) {
57
+ return null;
58
+ }
56
59
  // Walk through the nodes list. If we encounter a string node, we'll aggregate it with other string nodes.
57
60
  const formattedOutput = [];
58
61
  let currentTextNodes = [];