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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/dist/components/AgentChat/ChatCommon/AgentConnectivity.d.ts +14 -0
  2. package/dist/components/AgentChat/ChatCommon/AgentConnectivity.js +23 -0
  3. package/dist/components/AgentChat/{ChatCommon.d.ts → ChatCommon/ChatCommon.d.ts} +8 -4
  4. package/dist/components/AgentChat/{ChatCommon.js → ChatCommon/ChatCommon.js} +318 -307
  5. package/dist/components/AgentChat/ChatCommon/ChatHistory.d.ts +17 -0
  6. package/dist/components/AgentChat/ChatCommon/ChatHistory.js +27 -0
  7. package/dist/components/AgentChat/{ControlButtons.d.ts → ChatCommon/ControlButtons.d.ts} +1 -1
  8. package/dist/components/AgentChat/ChatCommon/ControlButtons.js +26 -0
  9. package/dist/components/AgentChat/{FormattedMarkdown.js → ChatCommon/FormattedMarkdown.js} +1 -1
  10. package/dist/components/AgentChat/ChatCommon/SampleQueries.d.ts +16 -0
  11. package/dist/components/AgentChat/ChatCommon/SampleQueries.js +29 -0
  12. package/dist/components/AgentChat/{SendButton.js → ChatCommon/SendButton.js} +1 -1
  13. package/dist/components/AgentChat/ChatCommon/UserQueryDisplay.d.ts +7 -0
  14. package/dist/components/AgentChat/{UserQueryDisplay.js → ChatCommon/UserQueryDisplay.js} +4 -3
  15. package/dist/components/AgentChat/{LlmChatButton.d.ts → Common/LlmChatButton.d.ts} +2 -2
  16. package/dist/components/AgentChat/{Utils.d.ts → Common/Utils.d.ts} +1 -1
  17. package/dist/components/AgentChat/{Utils.js → Common/Utils.js} +2 -1
  18. package/dist/components/AgentChat/VoiceChat/MicrophoneButton.js +1 -1
  19. package/dist/components/ChatBot/ChatBot.js +2 -2
  20. package/dist/components/Common/CustomerLogo.js +1 -1
  21. package/dist/components/Common/LlmChatOptionsButton.d.ts +1 -1
  22. package/dist/components/Common/MUIDialog.d.ts +1 -0
  23. package/dist/components/Common/MUIDialog.js +2 -2
  24. package/dist/components/MultiAgentAccelerator/AgentCounts.d.ts +2 -2
  25. package/dist/components/MultiAgentAccelerator/AgentFlow.d.ts +13 -1
  26. package/dist/components/MultiAgentAccelerator/AgentFlow.js +193 -20
  27. package/dist/components/MultiAgentAccelerator/AgentNetworkDesigner.d.ts +10 -0
  28. package/dist/components/MultiAgentAccelerator/AgentNetworkDesigner.js +20 -0
  29. package/dist/components/MultiAgentAccelerator/AgentNode.d.ts +1 -0
  30. package/dist/components/MultiAgentAccelerator/AgentNode.js +9 -4
  31. package/dist/components/MultiAgentAccelerator/AgentNodePopup.d.ts +33 -0
  32. package/dist/components/MultiAgentAccelerator/AgentNodePopup.js +81 -0
  33. package/dist/components/MultiAgentAccelerator/GraphLayouts.d.ts +4 -4
  34. package/dist/components/MultiAgentAccelerator/GraphLayouts.js +12 -8
  35. package/dist/components/MultiAgentAccelerator/MultiAgentAccelerator.d.ts +1 -0
  36. package/dist/components/MultiAgentAccelerator/MultiAgentAccelerator.js +101 -44
  37. package/dist/components/MultiAgentAccelerator/Sidebar/AgentNetworkTreeItem.js +4 -4
  38. package/dist/components/MultiAgentAccelerator/Sidebar/Sidebar.d.ts +1 -0
  39. package/dist/components/MultiAgentAccelerator/Sidebar/Sidebar.js +29 -23
  40. package/dist/components/MultiAgentAccelerator/Sidebar/TreeBuilder.js +1 -1
  41. package/dist/components/MultiAgentAccelerator/TemporaryNetworks.d.ts +14 -0
  42. package/dist/components/MultiAgentAccelerator/TemporaryNetworks.js +26 -1
  43. package/dist/components/MultiAgentAccelerator/ThoughtBubbleOverlay.js +8 -7
  44. package/dist/components/MultiAgentAccelerator/const.d.ts +24 -0
  45. package/dist/components/MultiAgentAccelerator/const.js +19 -0
  46. package/dist/controller/llm/LlmChat.js +1 -1
  47. package/dist/index.d.ts +8 -7
  48. package/dist/index.js +8 -7
  49. package/dist/state/ChatHistory.d.ts +50 -0
  50. package/dist/state/ChatHistory.js +98 -0
  51. package/dist/state/IndexedDBStorage.d.ts +14 -0
  52. package/dist/state/IndexedDBStorage.js +65 -0
  53. package/dist/state/TemporaryNetworks.d.ts +23 -0
  54. package/dist/state/TemporaryNetworks.js +43 -0
  55. package/dist/tsconfig.build.tsbuildinfo +1 -1
  56. package/package.json +8 -2
  57. package/dist/components/AgentChat/ControlButtons.js +0 -26
  58. package/dist/components/AgentChat/UserQueryDisplay.d.ts +0 -5
  59. /package/dist/components/AgentChat/{FormattedMarkdown.d.ts → ChatCommon/FormattedMarkdown.d.ts} +0 -0
  60. /package/dist/components/AgentChat/{Greetings.d.ts → ChatCommon/Greetings.d.ts} +0 -0
  61. /package/dist/components/AgentChat/{Greetings.js → ChatCommon/Greetings.js} +0 -0
  62. /package/dist/components/AgentChat/{SendButton.d.ts → ChatCommon/SendButton.d.ts} +0 -0
  63. /package/dist/components/AgentChat/{SyntaxHighlighterThemes.d.ts → ChatCommon/SyntaxHighlighterThemes.d.ts} +0 -0
  64. /package/dist/components/AgentChat/{SyntaxHighlighterThemes.js → ChatCommon/SyntaxHighlighterThemes.js} +0 -0
  65. /package/dist/components/AgentChat/{LlmChatButton.js → Common/LlmChatButton.js} +0 -0
  66. /package/dist/components/AgentChat/{Types.d.ts → Common/Types.d.ts} +0 -0
  67. /package/dist/components/AgentChat/{Types.js → Common/Types.js} +0 -0
@@ -27,24 +27,81 @@ import Typography from "@mui/material/Typography";
27
27
  import { applyNodeChanges, Background, ConnectionMode, ControlButton, Controls, ReactFlow, useReactFlow, useStore, } from "@xyflow/react";
28
28
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
29
29
  import { AgentNode, NODE_HEIGHT, NODE_WIDTH } from "./AgentNode.js";
30
- import { BASE_RADIUS, DEFAULT_FRONTMAN_X_POS, DEFAULT_FRONTMAN_Y_POS, LEVEL_SPACING } from "./const.js";
30
+ import { AgentNodePopup } from "./AgentNodePopup.js";
31
+ import { AGENT_NETWORK_DEFINITION_KEY, AGENT_NETWORK_DESIGNER_ID, AGENT_NETWORK_NAME_KEY, BASE_RADIUS, DEFAULT_FRONTMAN_X_POS, DEFAULT_FRONTMAN_Y_POS, isEditableAgent, LEVEL_SPACING, } from "./const.js";
31
32
  import { addThoughtBubbleEdge, layoutLinear, layoutRadial } from "./GraphLayouts.js";
32
33
  import { PlasmaEdge } from "./PlasmaEdge.js";
34
+ import { convertReservationsToNetworks, extractNetworkHocon, extractReservations } from "./TemporaryNetworks.js";
33
35
  import { ThoughtBubbleEdge } from "./ThoughtBubbleEdge.js";
34
36
  import { ThoughtBubbleOverlay } from "./ThoughtBubbleOverlay.js";
37
+ import { sendChatQuery } from "../../controller/agent/Agent.js";
38
+ import { StreamingUnit } from "../../controller/llm/LlmChat.js";
39
+ import { useAgentChatHistoryStore } from "../../state/ChatHistory.js";
40
+ import { useTempNetworksStore } from "../../state/TemporaryNetworks.js";
35
41
  import { usePalette } from "../../Theme/Palettes.js";
36
42
  import { getZIndex } from "../../utils/zIndexLayers.js";
43
+ import { chatMessageFromChunk } from "../AgentChat/Common/Utils.js";
44
+ import { NotificationType, sendNotification } from "../Common/notification.js";
37
45
  // #endregion: Types
38
46
  // #region: Constants
39
47
  // Timeout for thought bubbles is set to 10 seconds
40
48
  const THOUGHT_BUBBLE_TIMEOUT_MS = 10_000;
41
49
  // #endregion: Constants
42
- export const AgentFlow = ({ agentCounts, agentIconSuggestions, agentsInNetwork, currentConversations, id, isAwaitingLlm, isStreaming, thoughtBubbleEdges, setThoughtBubbleEdges, }) => {
50
+ // #region: Helpers
51
+ /** Merges incoming networks into target, keeping the entry with the highest expiration time. */
52
+ const mergeNetworks = (target, incoming) => {
53
+ for (const n of incoming) {
54
+ const key = n.agentNetworkName ?? n.reservation.reservation_id;
55
+ const existingIdx = target.findIndex((e) => (e.agentNetworkName ?? e.reservation.reservation_id) === key);
56
+ if (existingIdx < 0) {
57
+ target.push(n);
58
+ }
59
+ else if (n.reservation.expiration_time_in_seconds > target[existingIdx].reservation.expiration_time_in_seconds) {
60
+ target[existingIdx] = n;
61
+ }
62
+ }
63
+ };
64
+ /**
65
+ * Streams the Agent Network Designer endpoint with the updated definition and collects
66
+ * the resulting reservations. Returns the deduplicated list of new networks.
67
+ */
68
+ const streamNetworkDesignerUpdate = async (neuroSanURL, signal, agentName, updated, agentNetworkName, currentUser) => {
69
+ const newNetworks = [];
70
+ await sendChatQuery(neuroSanURL, signal,
71
+ // Shouldn't have to pass a user message, but API behaves different without it
72
+ `Update instructions for agent "${agentName}"`, AGENT_NETWORK_DESIGNER_ID, (chunk) => {
73
+ const chatMessage = chatMessageFromChunk(chunk);
74
+ if (!chatMessage)
75
+ return;
76
+ const reservations = extractReservations(chatMessage);
77
+ if (reservations.length === 0)
78
+ return;
79
+ const networkHocon = extractNetworkHocon(chatMessage);
80
+ // Always use the user's edited definition as the authoritative value.
81
+ // The backend may not echo agent_network_definition back, may return
82
+ // an empty array, or may return the pre-edit version.
83
+ const agentNetworkNameFromMessage = chatMessage.sly_data?.[AGENT_NETWORK_NAME_KEY];
84
+ // Prefer the locally-known name so upsert can match the existing entry even
85
+ // when the backend response omits AGENT_NETWORK_NAME_KEY.
86
+ const networkName = agentNetworkName ?? agentNetworkNameFromMessage;
87
+ const converted = convertReservationsToNetworks(reservations, networkHocon, updated, networkName);
88
+ mergeNetworks(newNetworks, converted);
89
+ }, null, {
90
+ [AGENT_NETWORK_DEFINITION_KEY]: updated,
91
+ // Use the backend's canonical name, not the local UUID-based key.
92
+ ...(agentNetworkName ? { [AGENT_NETWORK_NAME_KEY]: agentNetworkName } : {}),
93
+ // skip_designer prevents the backend from using a reasoning model for edits
94
+ skip_designer: true,
95
+ }, currentUser, StreamingUnit.Line);
96
+ return newNetworks;
97
+ };
98
+ // #endregion: Helpers
99
+ export const AgentFlow = ({ agentCounts, agentIconSuggestions, agentsInNetwork, currentConversations, currentUser, id, isAgentNetworkDesignerMode, isAwaitingLlm, isStreaming, isSelectedNetworkTemporary: isTemporaryNetwork, networkId, neuroSanURL, onNetworkReplaced, thoughtBubbleEdges, setThoughtBubbleEdges, }) => {
43
100
  const theme = useTheme();
44
101
  const { fitView } = useReactFlow();
45
102
  const handleResize = useCallback(() => {
46
- fitView(); // Adjusts the view to fit after resizing
47
- }, [fitView, isAwaitingLlm]);
103
+ void fitView(); // Adjusts the view to fit after resizing
104
+ }, [fitView]);
48
105
  useEffect(() => {
49
106
  window.addEventListener("resize", handleResize);
50
107
  return () => window.removeEventListener("resize", handleResize);
@@ -53,6 +110,9 @@ export const AgentFlow = ({ agentCounts, agentIconSuggestions, agentsInNetwork,
53
110
  const [coloringOption, setColoringOption] = useState("depth");
54
111
  const [enableRadialGuides, setEnableRadialGuides] = useState(true);
55
112
  const [showThoughtBubbles, setShowThoughtBubbles] = useState(true);
113
+ // Read temporary networks to find agent_network_definition for the currently selected network.
114
+ const tempNetworks = useTempNetworksStore((state) => state.tempNetworks);
115
+ const updateTempNetworkDefinition = useTempNetworksStore((state) => state.updateTempNetworkDefinition);
56
116
  // Track conversation IDs we've already processed to prevent re-adding after expiry
57
117
  const processedConversationIdsRef = useRef(new Set());
58
118
  // Track which bubble is currently being hovered
@@ -76,7 +136,7 @@ export const AgentFlow = ({ agentCounts, agentIconSuggestions, agentsInNetwork,
76
136
  useEffect(() => {
77
137
  if (!currentConversations || currentConversations.length === 0)
78
138
  return;
79
- setThoughtBubbleEdges((prev) => {
139
+ setThoughtBubbleEdges?.((prev) => {
80
140
  const processedText = new Set();
81
141
  for (const entry of prev.values()) {
82
142
  const text = entry.edge.data?.text?.trim();
@@ -121,7 +181,7 @@ export const AgentFlow = ({ agentCounts, agentIconSuggestions, agentsInNetwork,
121
181
  if (!isStreamingRef.current)
122
182
  return;
123
183
  const now = Date.now();
124
- setThoughtBubbleEdges((prev) => {
184
+ setThoughtBubbleEdges?.((prev) => {
125
185
  let changed = false;
126
186
  const edgesMap = new Map(prev);
127
187
  for (const [convId, entry] of prev) {
@@ -135,7 +195,7 @@ export const AgentFlow = ({ agentCounts, agentIconSuggestions, agentsInNetwork,
135
195
  });
136
196
  }, 1000);
137
197
  return () => clearInterval(cleanupInterval);
138
- }, []); // mount/unmount only
198
+ }, [setThoughtBubbleEdges]); // mount/unmount only
139
199
  // Shadow color for icon
140
200
  const shadowColor = theme.palette.mode === "dark" ? theme.palette.common.white : theme.palette.common.black;
141
201
  const isHeatmap = coloringOption === "heatmap";
@@ -164,16 +224,17 @@ export const AgentFlow = ({ agentCounts, agentIconSuggestions, agentsInNetwork,
164
224
  // Create the flow layout depending on user preference
165
225
  // Memoize layoutResult so it only recalculates when relevant data changes
166
226
  const layoutResult = useMemo(() => layout === "linear"
167
- ? layoutLinear(isHeatmap ? agentCounts : undefined, mergedAgentsInNetwork, currentConversations, isAwaitingLlm, thoughtBubbleEdges, agentIconSuggestions)
168
- : layoutRadial(isHeatmap ? agentCounts : undefined, mergedAgentsInNetwork, currentConversations, isAwaitingLlm, thoughtBubbleEdges, agentIconSuggestions), [
227
+ ? layoutLinear(isHeatmap ? agentCounts : undefined, mergedAgentsInNetwork, currentConversations, isAwaitingLlm, isAgentNetworkDesignerMode, thoughtBubbleEdges, agentIconSuggestions, isTemporaryNetwork)
228
+ : layoutRadial(isHeatmap ? agentCounts : undefined, mergedAgentsInNetwork, currentConversations, isAwaitingLlm, isAgentNetworkDesignerMode, thoughtBubbleEdges, agentIconSuggestions, isTemporaryNetwork), [
169
229
  agentCounts,
170
230
  agentIconSuggestions,
171
- coloringOption,
172
231
  currentConversations,
232
+ isAgentNetworkDesignerMode,
173
233
  isAwaitingLlm,
234
+ isHeatmap,
235
+ isTemporaryNetwork,
174
236
  layout,
175
237
  mergedAgentsInNetwork,
176
- showThoughtBubbles,
177
238
  thoughtBubbleEdges,
178
239
  ]);
179
240
  const [nodes, setNodes] = useState(layoutResult.nodes);
@@ -181,31 +242,143 @@ export const AgentFlow = ({ agentCounts, agentIconSuggestions, agentsInNetwork,
181
242
  useEffect(() => {
182
243
  setNodes(layoutResult.nodes);
183
244
  }, [layoutResult.nodes]);
245
+ // Track which node the user clicked on so we can open the popup
246
+ const [selectedAgent, setSelectedAgent] = useState(null);
247
+ const [isPopupOpen, setIsPopupOpen] = useState(false);
248
+ // True while the agent-edit request is in-flight so we can disable the Save button.
249
+ const [isSavingAgent, setIsSavingAgent] = useState(false);
250
+ // AbortController for the in-flight save request — stored in a ref so handlePopupClose can cancel it.
251
+ const saveAbortControllerRef = useRef(null);
252
+ const handleNodeClick = useCallback((_event, node) => {
253
+ // Popup is only available for temporary networks.
254
+ if (!isTemporaryNetwork)
255
+ return;
256
+ // Only llm_agent nodes support instructions/description editing.
257
+ if (!isEditableAgent(node.data.displayAs))
258
+ return;
259
+ // Find the clicked agent's existing instructions and description from the temp network definition.
260
+ const currentTempNetwork = networkId
261
+ ? tempNetworks.find((n) => n.agentInfo.agent_name === networkId)
262
+ : undefined;
263
+ const found = (currentTempNetwork?.agentNetworkDefinition ?? []).find((e) => e.origin === node.id);
264
+ setSelectedAgent({
265
+ agentId: node.id,
266
+ agentName: node.data.agentName,
267
+ initialInstructions: found?.instructions ?? "",
268
+ initialDescription: found?.description ?? "",
269
+ });
270
+ setIsPopupOpen(true);
271
+ }, [tempNetworks, isTemporaryNetwork, networkId]);
272
+ const handlePopupClose = useCallback(() => {
273
+ // If a save is in-flight, abort it immediately so the stream doesn't hang.
274
+ saveAbortControllerRef.current?.abort();
275
+ saveAbortControllerRef.current = null;
276
+ setIsPopupOpen(false);
277
+ setIsSavingAgent(false);
278
+ }, []);
279
+ /** Applies the networks returned by the designer: upserts them and triggers navigation if needed. */
280
+ const applyNetworkSaveResult = useCallback((agentName, newNetworksFromSave, currentAgentNetworkName) => {
281
+ if (newNetworksFromSave.length === 0) {
282
+ sendNotification(NotificationType.error, `Failed to update agent "${agentName}".`, "The network designer did not return a reservation. Please try again.");
283
+ return;
284
+ }
285
+ const replacement = newNetworksFromSave.find((n) => n.agentNetworkName === currentAgentNetworkName);
286
+ if (replacement) {
287
+ useTempNetworksStore.getState().upsertTempNetworks(newNetworksFromSave);
288
+ if (networkId && onNetworkReplaced) {
289
+ useAgentChatHistoryStore.getState().copyHistory(networkId, replacement.agentInfo.agent_name);
290
+ onNetworkReplaced(networkId, replacement.agentInfo.agent_name);
291
+ }
292
+ }
293
+ else {
294
+ // Reservations came back but none matched the current network — surface this to the user.
295
+ sendNotification(NotificationType.error, `Failed to update agent "${agentName}".`, "A reservation was returned but did not match the current network. Please try again.");
296
+ }
297
+ }, [networkId, onNetworkReplaced]);
298
+ const handlePopupSave = useCallback(async (agentName, instructionsText, descriptionText) => {
299
+ if (!selectedAgent)
300
+ return;
301
+ // Find the temp network entry for the currently selected network.
302
+ const currentTempNetwork = networkId
303
+ ? tempNetworks.find((n) => n.agentInfo.agent_name === networkId)
304
+ : undefined;
305
+ // Produce a new array with the saved agent's fields updated; all other entries pass through unchanged.
306
+ const currentDefinitions = currentTempNetwork?.agentNetworkDefinition ?? [];
307
+ const updated = currentDefinitions.map((entry) => entry.origin === selectedAgent.agentId
308
+ ? { ...entry, instructions: instructionsText, description: descriptionText }
309
+ : entry);
310
+ if (networkId) {
311
+ updateTempNetworkDefinition(networkId, updated);
312
+ }
313
+ // POST the updated definition to the Agent Network Designer and wait for the response.
314
+ // The backend is immutable for temporary networks, so a new reservation will always be created.
315
+ // We need to capture it and replace the old network in the store.
316
+ if (!neuroSanURL || !currentUser || updated.length === 0) {
317
+ setIsPopupOpen(false);
318
+ return;
319
+ }
320
+ setIsSavingAgent(true);
321
+ const saveController = new AbortController();
322
+ saveAbortControllerRef.current = saveController;
323
+ // 60-second hard timeout — belt-and-suspenders in case the server never closes the stream.
324
+ const saveTimeoutId = setTimeout(() => saveController.abort(new DOMException("Save timed out", "TimeoutError")), 60_000);
325
+ try {
326
+ const newNetworksFromSave = await streamNetworkDesignerUpdate(neuroSanURL, saveController.signal, agentName, updated, currentTempNetwork?.agentNetworkName, currentUser);
327
+ applyNetworkSaveResult(agentName, newNetworksFromSave, currentTempNetwork?.agentNetworkName);
328
+ }
329
+ catch (e) {
330
+ const isAbort = e instanceof DOMException && e.name === "AbortError";
331
+ const isTimeout = e instanceof DOMException && e.name === "TimeoutError";
332
+ if (!isAbort) {
333
+ console.error("Failed to submit agent network update:", e);
334
+ const detail = isTimeout
335
+ ? "The request timed out waiting for the server. Please try again."
336
+ : String(e);
337
+ sendNotification(NotificationType.error, `Failed to update agent "${agentName}".`, detail);
338
+ }
339
+ // isAbort: user dismissed the dialog — no toast needed.
340
+ }
341
+ finally {
342
+ clearTimeout(saveTimeoutId);
343
+ saveAbortControllerRef.current = null;
344
+ setIsSavingAgent(false);
345
+ setIsPopupOpen(false);
346
+ }
347
+ }, [
348
+ selectedAgent,
349
+ tempNetworks,
350
+ updateTempNetworkDefinition,
351
+ neuroSanURL,
352
+ currentUser,
353
+ networkId,
354
+ applyNetworkSaveResult,
355
+ ]);
184
356
  const edges = layoutResult.edges;
185
357
  // Make sure to extract only thought bubble edges for the overlay.
186
358
  const thoughtBubbleEdgesForOverlay = useMemo(() => edges.filter((e) => e.type === "thoughtBubbleEdge"), [edges]);
187
359
  useEffect(() => {
188
360
  // Schedule a fitView after the layout is set to ensure the view is adjusted correctly
189
361
  setTimeout(() => {
190
- fitView();
362
+ void fitView();
191
363
  }, 50);
192
- }, [agentsInNetwork, layout]);
364
+ }, [agentsInNetwork, fitView, layout]);
193
365
  const onNodesChange = useCallback((changes) => {
194
- setNodes((ns) => applyNodeChanges(
195
- // For now, we only allow dragging, no updates
196
- changes.filter((c) => c.type === "position"), ns));
197
- }, []);
366
+ setNodes((currentNodes) => applyNodeChanges(
367
+ // For now, we only allow dragging, no updates. In agent network designer mode, doesn't make sense
368
+ // to allow position changes since the user isn't actually manipulating a real network
369
+ changes.filter((c) => c.type === "position" && !isAgentNetworkDesignerMode), currentNodes));
370
+ }, [isAgentNetworkDesignerMode]);
198
371
  const transform = useStore((state) => state.transform);
199
372
  // Why not just a "const"? See: https://reactflow.dev/learn/customization/custom-nodes
200
373
  // "It’s important that the nodeTypes are memoized or defined outside the component. Otherwise, React creates
201
374
  // a new object on every render which leads to performance issues and bugs."
202
375
  const nodeTypes = useMemo(() => ({
203
376
  agentNode: AgentNode,
204
- }), [AgentNode]);
377
+ }), []);
205
378
  const edgeTypes = useMemo(() => ({
206
379
  plasmaEdge: PlasmaEdge,
207
380
  thoughtBubbleEdge: ThoughtBubbleEdge,
208
- }), [PlasmaEdge, ThoughtBubbleEdge]);
381
+ }), []);
209
382
  // Figure out the maximum depth of the network
210
383
  const maxDepth = useMemo(() => {
211
384
  return nodes?.reduce((max, node) => Math.max(node.data.depth, max), 0) + 1;
@@ -311,5 +484,5 @@ export const AgentFlow = ({ agentCounts, agentIconSuggestions, agentsInNetwork,
311
484
  color: theme.palette.text.primary,
312
485
  fill: theme.palette.text.primary,
313
486
  },
314
- }, children: [_jsx(ReactFlow, { id: `${id}-react-flow`, nodes: nodes, edges: edges, onNodesChange: onNodesChange, fitView: true, nodeTypes: nodeTypes, edgeTypes: edgeTypes, connectionMode: ConnectionMode.Loose, children: !isAwaitingLlm && (_jsxs(_Fragment, { children: [agentsInNetwork?.length ? getLegend() : null, _jsx(Background, { id: `${id}-background` }), getControls(), shouldShowRadialGuides ? getRadialGuides() : null] })) }), _jsx(ThoughtBubbleOverlay, { nodes: nodes, edges: thoughtBubbleEdgesForOverlay, showThoughtBubbles: showThoughtBubbles, isStreaming: isStreaming, onBubbleHoverChange: handleBubbleHoverChange })] }));
487
+ }, children: [_jsx(ReactFlow, { id: `${id}-react-flow`, nodes: nodes, edges: edges, onNodesChange: onNodesChange, onNodeClick: handleNodeClick, fitView: true, nodeTypes: nodeTypes, edgeTypes: edgeTypes, connectionMode: ConnectionMode.Loose, children: !isAwaitingLlm && (_jsxs(_Fragment, { children: [agentsInNetwork?.length && !isAgentNetworkDesignerMode ? getLegend() : null, _jsx(Background, { id: `${id}-background` }), !isAgentNetworkDesignerMode && getControls(), shouldShowRadialGuides ? getRadialGuides() : null] })) }), _jsx(ThoughtBubbleOverlay, { nodes: nodes, edges: thoughtBubbleEdgesForOverlay, showThoughtBubbles: showThoughtBubbles, isStreaming: isStreaming, onBubbleHoverChange: handleBubbleHoverChange }), selectedAgent && !isAwaitingLlm && (_jsx(AgentNodePopup, { agentName: selectedAgent.agentName, initialInstructions: selectedAgent.initialInstructions, initialDescription: selectedAgent.initialDescription, isOpen: isPopupOpen, isSaving: isSavingAgent, onClose: handlePopupClose, onSave: handlePopupSave }))] }));
315
488
  };
@@ -0,0 +1,10 @@
1
+ import { ChatMessage, ConnectivityInfo } from "../../generated/neuro-san/NeuroSanClient.js";
2
+ /**
3
+ * Extracts network progress information from a chat message, if present. Only messages of type AGENT_PROGRESS
4
+ * can have this information. When present, it should be under the key defined by AGENT_PROGRESS_CONNECTIVITY_KEY
5
+ * in the message structure.
6
+ * @param message The chat message to extract network progress from
7
+ * @returns An array of ConnectivityInfo objects if the message contains network progress information, or an empty
8
+ * array if not
9
+ */
10
+ export declare const extractAgentNetworkDesignerProgress: (message: ChatMessage) => ConnectivityInfo[];
@@ -0,0 +1,20 @@
1
+ import { AGENT_PROGRESS_CONNECTIVITY_KEY } from "./const.js";
2
+ import { ChatMessageType } from "../../generated/neuro-san/NeuroSanClient.js";
3
+ /**
4
+ * Extracts network progress information from a chat message, if present. Only messages of type AGENT_PROGRESS
5
+ * can have this information. When present, it should be under the key defined by AGENT_PROGRESS_CONNECTIVITY_KEY
6
+ * in the message structure.
7
+ * @param message The chat message to extract network progress from
8
+ * @returns An array of ConnectivityInfo objects if the message contains network progress information, or an empty
9
+ * array if not
10
+ */
11
+ export const extractAgentNetworkDesignerProgress = (message) => {
12
+ const agentProgress = message?.structure?.[AGENT_PROGRESS_CONNECTIVITY_KEY];
13
+ if (message?.type === ChatMessageType.AGENT_PROGRESS && agentProgress) {
14
+ return agentProgress;
15
+ }
16
+ else {
17
+ // Not the type of message that would contain network progress information, or the key is not present
18
+ return [];
19
+ }
20
+ };
@@ -10,6 +10,7 @@ export interface AgentNodeProps extends Record<string, unknown> {
10
10
  readonly getConversations: () => AgentConversation[] | null;
11
11
  readonly isAwaitingLlm?: boolean;
12
12
  readonly agentIconSuggestion?: string;
13
+ readonly isTemporaryNetwork?: boolean;
13
14
  }
14
15
  export declare const NODE_HEIGHT = 100;
15
16
  export declare const NODE_WIDTH = 100;
@@ -25,6 +25,7 @@ import { keyframes, styled, useTheme } from "@mui/material/styles";
25
25
  import Tooltip from "@mui/material/Tooltip";
26
26
  import Typography from "@mui/material/Typography";
27
27
  import { Handle, Position } from "@xyflow/react";
28
+ import { DISPLAY_AS_CODED_TOOL, DISPLAY_AS_EXTERNAL_AGENT, DISPLAY_AS_LANGCHAIN_TOOL, DISPLAY_AS_LLM_AGENT, isEditableAgent, } from "./const.js";
28
29
  import { useSettingsStore } from "../../state/Settings.js";
29
30
  import { usePalette } from "../../Theme/Palettes.js";
30
31
  import { isLightColor } from "../../Theme/Theme.js";
@@ -81,7 +82,7 @@ export const AgentNode = (props) => {
81
82
  const palette = usePalette();
82
83
  // Unpack the node-specific data
83
84
  const data = props.data;
84
- const { agentCounts, agentName, depth, displayAs, getConversations, agentIconSuggestion, isAwaitingLlm } = data;
85
+ const { agentCounts, agentName, depth, displayAs, getConversations, agentIconSuggestion, isAwaitingLlm, isTemporaryNetwork, } = data;
85
86
  // Determine if this is the Frontman node (depth 0)
86
87
  const isFrontman = depth === 0;
87
88
  // Determine max agent count for heatmap scaling
@@ -133,11 +134,13 @@ export const AgentNode = (props) => {
133
134
  }
134
135
  else {
135
136
  switch (displayAs) {
136
- case "external_agent":
137
+ case DISPLAY_AS_EXTERNAL_AGENT:
137
138
  return (_jsx(TravelExploreIcon, { id: id, sx: { fontSize: AGENT_ICON_SIZE } }));
138
- case "coded_tool":
139
+ // This should be a supported type but we're not seeing it?
140
+ case DISPLAY_AS_LANGCHAIN_TOOL:
141
+ case DISPLAY_AS_CODED_TOOL:
139
142
  return (_jsx(HandymanIcon, { id: id, sx: { fontSize: AGENT_ICON_SIZE } }));
140
- case "llm_agent":
143
+ case DISPLAY_AS_LLM_AGENT:
141
144
  default:
142
145
  return (_jsx(AutoAwesomeIcon, { id: id, sx: { fontSize: AGENT_ICON_SIZE } }));
143
146
  }
@@ -150,9 +153,11 @@ export const AgentNode = (props) => {
150
153
  const glowColor = isLightColor(theme.palette.background.default)
151
154
  ? theme.palette.common.black
152
155
  : theme.palette.common.white;
156
+ const isClickableNode = isTemporaryNetwork && isEditableAgent(displayAs);
153
157
  return (_jsxs(_Fragment, { children: [_jsxs(AnimatedNode, { id: agentId, "data-testid": agentId, glowColor: glowColor, isActive: isActiveAgent, sx: {
154
158
  backgroundColor,
155
159
  color,
160
+ cursor: isClickableNode ? "pointer" : "grab",
156
161
  height: NODE_HEIGHT * (isFrontman ? 1.25 : 1.0),
157
162
  width: NODE_WIDTH * (isFrontman ? 1.25 : 1.0),
158
163
  zIndex: getZIndex(1, theme),
@@ -0,0 +1,33 @@
1
+ import { FC } from "react";
2
+ export interface AgentNodePopupProps {
3
+ /** The agent's display name — shown read-only in the dialog header area. */
4
+ readonly agentName: string;
5
+ /** Whether the dialog is open. */
6
+ readonly isOpen: boolean;
7
+ /** Called when the user closes or cancels the dialog without saving. */
8
+ readonly onClose: () => void;
9
+ /**
10
+ * Called when the user saves the edited fields.
11
+ * @param agentName The agent's name (unchanged).
12
+ * @param instructions The updated instructions text.
13
+ * @param description The updated description text.
14
+ */
15
+ readonly onSave: (agentName: string, instructions: string, description: string) => void;
16
+ /** Initial instructions text shown in the editable field. Defaults to an empty string. */
17
+ readonly initialInstructions?: string;
18
+ /** Initial description text shown in the editable field. Defaults to an empty string. */
19
+ readonly initialDescription?: string;
20
+ /**
21
+ * When true the dialog is in a saving state: the Save button is disabled and shows a spinner.
22
+ * Defaults to false.
23
+ */
24
+ readonly isSaving?: boolean;
25
+ }
26
+ /**
27
+ * A popup dialog for viewing and editing an agent node's instructions and description.
28
+ *
29
+ * - Agent name is displayed read-only in the dialog header.
30
+ * - Both instructions and description are editable.
31
+ * - Saving is a no-op until the API endpoint is wired up; `onSave` receives the current values.
32
+ */
33
+ export declare const AgentNodePopup: FC<AgentNodePopupProps>;
@@ -0,0 +1,81 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /*
3
+ Copyright 2025 Cognizant Technology Solutions Corp, www.cognizant.com.
4
+
5
+ Licensed under the Apache License, Version 2.0 (the "License");
6
+ you may not use this file except in compliance with the License.
7
+ You may obtain a copy of the License at
8
+
9
+ http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ Unless required by applicable law or agreed to in writing, software
12
+ distributed under the License is distributed on an "AS IS" BASIS,
13
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ See the License for the specific language governing permissions and
15
+ limitations under the License.
16
+ */
17
+ import Box from "@mui/material/Box";
18
+ import Button from "@mui/material/Button";
19
+ import CircularProgress from "@mui/material/CircularProgress";
20
+ import LinearProgress from "@mui/material/LinearProgress";
21
+ import TextField from "@mui/material/TextField";
22
+ import { useEffect, useState } from "react";
23
+ import { ConfirmationModal } from "../Common/ConfirmationModal.js";
24
+ import { MUIDialog } from "../Common/MUIDialog.js";
25
+ // #endregion: Types
26
+ /**
27
+ * A popup dialog for viewing and editing an agent node's instructions and description.
28
+ *
29
+ * - Agent name is displayed read-only in the dialog header.
30
+ * - Both instructions and description are editable.
31
+ * - Saving is a no-op until the API endpoint is wired up; `onSave` receives the current values.
32
+ */
33
+ export const AgentNodePopup = ({ agentName, isOpen, onClose, onSave, initialInstructions = "", initialDescription = "", isSaving = false, }) => {
34
+ const [instructionsText, setInstructionsText] = useState(initialInstructions);
35
+ const [descriptionText, setDescriptionText] = useState(initialDescription);
36
+ const isDirty = instructionsText !== initialInstructions || descriptionText !== initialDescription;
37
+ const [displayConfirmationModal, setDisplayConfirmationModal] = useState(false);
38
+ // Keep local fields in sync when the dialog opens or if initial values change while open.
39
+ // Guarding on isOpen prevents resetting the text during the close animation, which would cause a visible flash.
40
+ useEffect(() => {
41
+ if (isOpen) {
42
+ setInstructionsText(initialInstructions);
43
+ setDescriptionText(initialDescription);
44
+ }
45
+ }, [initialInstructions, initialDescription, isOpen]);
46
+ const handleSave = () => {
47
+ onSave(agentName, instructionsText, descriptionText);
48
+ };
49
+ const handleClose = () => {
50
+ if (isSaving) {
51
+ return;
52
+ }
53
+ if (isDirty) {
54
+ setDisplayConfirmationModal(true);
55
+ return;
56
+ }
57
+ setInstructionsText(initialInstructions);
58
+ setDescriptionText(initialDescription);
59
+ onClose();
60
+ };
61
+ const footer = (_jsx(Box, { sx: {
62
+ display: "flex",
63
+ alignItems: "flex-start",
64
+ justifyContent: "flex-end",
65
+ width: "100%",
66
+ gap: 1,
67
+ }, children: _jsxs(Box, { sx: { display: "flex", gap: 1 }, children: [_jsx(Button, { id: "agent-node-popup-cancel-btn", onClick: handleClose, variant: "outlined", size: "small", disabled: isSaving, children: "Cancel" }), _jsx(Button, { id: "agent-node-popup-save-btn", onClick: handleSave, variant: "contained", size: "small", disabled: isSaving || !isDirty, startIcon: isSaving ? (_jsx(CircularProgress, { size: 14, color: "inherit" })) : undefined, children: isSaving ? "Applying changes…" : "Save" })] }) }));
68
+ const getConfirmationModal = () => (_jsx(ConfirmationModal, { id: "agent-node-popup-unsaved-changes-modal", cancelBtnLabel: "Discard changes", closeable: false, content: _jsx("p", { children: "You have unsaved edits. Are you sure you want to discard your changes and close the dialog?" }), handleCancel: () => {
69
+ setDisplayConfirmationModal(false);
70
+ setInstructionsText(initialInstructions);
71
+ setDescriptionText(initialDescription);
72
+ onClose();
73
+ }, handleOk: handleSave, maskCloseable: false, okBtnLabel: "Save changes", title: "Unsaved Changes" }));
74
+ return (_jsxs(_Fragment, { children: [displayConfirmationModal && getConfirmationModal(), _jsxs(MUIDialog, { dialogSx: isSaving ? { "& *, & *::before, & *::after": { cursor: "wait !important" } } : undefined, footer: footer, id: "agent-node-popup", isOpen: isOpen, onClose: handleClose, paperProps: { minWidth: "480px", maxWidth: "600px", width: "100%" }, title: agentName, children: [isSaving && (_jsx(LinearProgress, { "aria-label": "Saving agent", sx: { mb: 2, borderRadius: 1 } })), _jsx(TextField, { disabled: isSaving, fullWidth: true, id: "agent-node-popup-description-field", label: "Description", multiline: true, onChange: (e) => setDescriptionText(e.target.value), onKeyDown: (e) => {
75
+ if (e.key !== "Escape")
76
+ e.stopPropagation();
77
+ }, placeholder: "Enter a short description of this agent\u2026", rows: 6, size: "small", sx: { "& .MuiInputBase-input": { fontSize: "0.85rem" } }, value: descriptionText }), _jsx(TextField, { autoFocus: true, disabled: isSaving, fullWidth: true, id: "agent-node-popup-instructions-field", label: "Instructions", multiline: true, onChange: (e) => setInstructionsText(e.target.value), onKeyDown: (e) => {
78
+ if (e.key !== "Escape")
79
+ e.stopPropagation();
80
+ }, placeholder: "Enter instructions for this agent\u2026", rows: 6, size: "small", sx: { "& .MuiInputBase-input": { fontSize: "0.85rem" }, marginTop: 2 }, value: instructionsText })] })] }));
81
+ };
@@ -25,12 +25,12 @@ export declare const getThoughtBubbleEdges: (thoughtBubbleEdges: Map<string, {
25
25
  timestamp: number;
26
26
  }>) => ThoughtBubbleEdgeShape[];
27
27
  export declare const layoutRadial: (agentCounts: Map<string, number>, agentsInNetwork: ConnectivityInfo[], currentConversations: AgentConversation[] | null, // For plasma edges (live) and node highlighting
28
- isAwaitingLlm: boolean, thoughtBubbleEdges: Map<string, {
28
+ isAwaitingLlm: boolean, isAgentNetworkDesignerMode: boolean, thoughtBubbleEdges: Map<string, {
29
29
  edge: ThoughtBubbleEdgeShape;
30
30
  timestamp: number;
31
- }>, agentIconSuggestions?: AgentIconSuggestions) => LayoutResult;
31
+ }>, agentIconSuggestions?: AgentIconSuggestions, isTemporaryNetwork?: boolean) => LayoutResult;
32
32
  export declare const layoutLinear: (agentCounts: Map<string, number>, agentsInNetwork: ConnectivityInfo[], currentConversations: AgentConversation[] | null, // For plasma edges (live) and node highlighting
33
- isAwaitingLlm: boolean, thoughtBubbleEdges: Map<string, {
33
+ isAwaitingLlm: boolean, isAgentNetworkDesignerMode: boolean, thoughtBubbleEdges: Map<string, {
34
34
  edge: ThoughtBubbleEdgeShape;
35
35
  timestamp: number;
36
- }>, agentIconSuggestions?: AgentIconSuggestions) => LayoutResult;
36
+ }>, agentIconSuggestions?: AgentIconSuggestions, isTemporaryNetwork?: boolean) => LayoutResult;
@@ -21,7 +21,7 @@ import { MarkerType } from "@xyflow/react";
21
21
  import cloneDeep from "lodash-es/cloneDeep.js";
22
22
  import { NODE_HEIGHT, NODE_WIDTH } from "./AgentNode.js";
23
23
  import { BASE_RADIUS, DEFAULT_FRONTMAN_X_POS, DEFAULT_FRONTMAN_Y_POS, LEVEL_SPACING } from "./const.js";
24
- import { cleanUpAgentName, KNOWN_MESSAGE_TYPES_FOR_PLASMA } from "../AgentChat/Utils.js";
24
+ import { cleanUpAgentName, KNOWN_MESSAGE_TYPES_FOR_PLASMA } from "../AgentChat/Common/Utils.js";
25
25
  export const MAX_GLOBAL_THOUGHT_BUBBLES = 5;
26
26
  export const addThoughtBubbleEdge = (thoughtBubbleEdges, conversationId, edge) => {
27
27
  // Add with timestamp for age-based cleanup
@@ -100,7 +100,7 @@ const getEdgeProperties = (sourceId, targetId, sourceHandle, targetHandle, isAni
100
100
  };
101
101
  };
102
102
  export const layoutRadial = (agentCounts, agentsInNetwork, currentConversations, // For plasma edges (live) and node highlighting
103
- isAwaitingLlm, thoughtBubbleEdges, agentIconSuggestions = null) => {
103
+ isAwaitingLlm, isAgentNetworkDesignerMode, thoughtBubbleEdges, agentIconSuggestions = null, isTemporaryNetwork = false) => {
104
104
  const nodesInNetwork = [];
105
105
  const edgesInNetwork = [];
106
106
  // Compute depth of each node using breadth-first traversal
@@ -170,8 +170,10 @@ isAwaitingLlm, thoughtBubbleEdges, agentIconSuggestions = null) => {
170
170
  }
171
171
  // Plasma edges based on currentConversations (live, cleared at network end)
172
172
  const isEdgeAnimated = areInSameConversation(currentConversations, nodeId, graphNode.id);
173
- // Add edge from parent to node
174
- if (!isAwaitingLlm || isEdgeAnimated) {
173
+ // Add edge from parent to node.
174
+ // We always show animated edges, all edges when in "idle mode" (not awaiting LLM),
175
+ // and all edges in the designer mode preview.
176
+ if (!isAwaitingLlm || isAgentNetworkDesignerMode || isEdgeAnimated) {
175
177
  edgesInNetwork.push(getEdgeProperties(graphNode.id, nodeId, sourceHandle, targetHandle, isEdgeAnimated));
176
178
  }
177
179
  }
@@ -188,6 +190,7 @@ isAwaitingLlm, thoughtBubbleEdges, agentIconSuggestions = null) => {
188
190
  getConversations: () => currentConversations,
189
191
  isAwaitingLlm,
190
192
  agentIconSuggestion: agentIconSuggestions?.[nodeId],
193
+ isTemporaryNetwork,
191
194
  },
192
195
  position: isFrontman ? { x: DEFAULT_FRONTMAN_X_POS, y: DEFAULT_FRONTMAN_Y_POS } : { x, y },
193
196
  style: {
@@ -207,7 +210,7 @@ isAwaitingLlm, thoughtBubbleEdges, agentIconSuggestions = null) => {
207
210
  return { nodes: nodesInNetwork, edges: edgesInNetwork };
208
211
  };
209
212
  export const layoutLinear = (agentCounts, agentsInNetwork, currentConversations, // For plasma edges (live) and node highlighting
210
- isAwaitingLlm, thoughtBubbleEdges, agentIconSuggestions = null) => {
213
+ isAwaitingLlm, isAgentNetworkDesignerMode, thoughtBubbleEdges, agentIconSuggestions = null, isTemporaryNetwork = false) => {
211
214
  const nodesInNetwork = [];
212
215
  const edgesInNetwork = [];
213
216
  // Do these calculations outside the loop for efficiency
@@ -229,6 +232,7 @@ isAwaitingLlm, thoughtBubbleEdges, agentIconSuggestions = null) => {
229
232
  isAwaitingLlm,
230
233
  depth: undefined, // Depth will be computed later,
231
234
  agentIconSuggestion: agentIconSuggestions?.[nodeId],
235
+ isTemporaryNetwork,
232
236
  },
233
237
  position: isFrontman ? { x: DEFAULT_FRONTMAN_X_POS, y: DEFAULT_FRONTMAN_Y_POS } : { x: 0, y: 0 },
234
238
  style: {
@@ -286,9 +290,9 @@ isAwaitingLlm, thoughtBubbleEdges, agentIconSuggestions = null) => {
286
290
  depth: xPositions.indexOf(nodeWithPosition.x),
287
291
  };
288
292
  });
289
- // If we're in "awaiting LLM" mode, we filter edges to only include those that are between conversation agents.
290
- // Use currentConversations (plasma edges are live and clear at network end)
291
- const filteredEdges = isAwaitingLlm
293
+ // If we're in "awaiting LLM" mode, but not Agent Network Designer mode, we filter edges to only include those
294
+ // that are between conversation agents. Use currentConversations (plasma edges are live and clear at network end)
295
+ const filteredEdges = isAwaitingLlm && !isAgentNetworkDesignerMode
292
296
  ? edgesInNetwork.filter((edge) => areInSameConversation(currentConversations, edge.source, edge.target))
293
297
  : edgesInNetwork;
294
298
  // Add thought bubble edges from cache to avoid duplicates across layout recalculations
@@ -6,6 +6,7 @@ interface MultiAgentAcceleratorProps {
6
6
  };
7
7
  readonly backendNeuroSanApiUrl: string;
8
8
  }
9
+ export declare const GRACE_PERIOD_MS: number;
9
10
  /**
10
11
  * Main Multi-Agent Accelerator component that contains the sidebar, agent flow, and chat components.
11
12
  * @param backendNeuroSanApiUrl Initial URL of the backend Neuro-San API. User can change this in the UI.