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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/dist/Theme/Theme.js +3 -3
  2. package/dist/components/AgentChat/ChatCommon/ChatCommon.d.ts +11 -1
  3. package/dist/components/AgentChat/ChatCommon/ChatCommon.js +284 -285
  4. package/dist/components/AgentChat/ChatCommon/ChatHistory.d.ts +1 -7
  5. package/dist/components/AgentChat/ChatCommon/ChatHistory.js +33 -22
  6. package/dist/components/AgentChat/ChatCommon/ControlButtons.js +2 -2
  7. package/dist/components/AgentChat/ChatCommon/Conversation.d.ts +13 -0
  8. package/dist/components/AgentChat/ChatCommon/Conversation.js +80 -0
  9. package/dist/components/AgentChat/ChatCommon/ConversationTurn.d.ts +23 -0
  10. package/dist/components/AgentChat/ChatCommon/ConversationTurn.js +11 -0
  11. package/dist/components/AgentChat/ChatCommon/FormattedMarkdown.js +5 -3
  12. package/dist/components/AgentChat/ChatCommon/SampleQueries.d.ts +3 -0
  13. package/dist/components/AgentChat/ChatCommon/SampleQueries.js +6 -3
  14. package/dist/components/AgentChat/ChatCommon/Thinking.d.ts +12 -0
  15. package/dist/components/AgentChat/ChatCommon/Thinking.js +51 -0
  16. package/dist/components/AgentChat/Common/LlmChatButton.d.ts +2 -2
  17. package/dist/components/AgentChat/Common/Types.d.ts +6 -5
  18. package/dist/components/AgentChat/Common/Types.js +5 -0
  19. package/dist/components/AgentChat/Common/Utils.d.ts +1 -1
  20. package/dist/components/AgentChat/Common/Utils.js +14 -9
  21. package/dist/components/Common/AccordionLite.d.ts +14 -0
  22. package/dist/components/Common/AccordionLite.js +25 -0
  23. package/dist/components/Common/ConfirmationModal.d.ts +1 -0
  24. package/dist/components/Common/ConfirmationModal.js +1 -1
  25. package/dist/components/Common/CustomerLogo.js +1 -1
  26. package/dist/components/Common/MUIAlert.d.ts +1 -0
  27. package/dist/components/Common/MUIAlert.js +3 -4
  28. package/dist/components/Common/Navbar.d.ts +2 -1
  29. package/dist/components/Common/Navbar.js +8 -4
  30. package/dist/components/Common/notification.d.ts +1 -1
  31. package/dist/components/Common/notification.js +17 -12
  32. package/dist/components/MultiAgentAccelerator/AgentFlow.d.ts +8 -0
  33. package/dist/components/MultiAgentAccelerator/AgentFlow.js +282 -82
  34. package/dist/components/MultiAgentAccelerator/AgentNode.d.ts +3 -1
  35. package/dist/components/MultiAgentAccelerator/AgentNode.js +64 -28
  36. package/dist/components/MultiAgentAccelerator/AgentNodePopup.d.ts +1 -4
  37. package/dist/components/MultiAgentAccelerator/AgentNodePopup.js +4 -5
  38. package/dist/components/MultiAgentAccelerator/GraphLayouts.js +19 -9
  39. package/dist/components/MultiAgentAccelerator/MultiAgentAccelerator.d.ts +2 -2
  40. package/dist/components/MultiAgentAccelerator/MultiAgentAccelerator.js +268 -60
  41. package/dist/components/MultiAgentAccelerator/Sidebar/AgentNetworkTreeItem.d.ts +1 -0
  42. package/dist/components/MultiAgentAccelerator/Sidebar/AgentNetworkTreeItem.js +28 -12
  43. package/dist/components/MultiAgentAccelerator/Sidebar/Sidebar.d.ts +1 -0
  44. package/dist/components/MultiAgentAccelerator/Sidebar/Sidebar.js +21 -5
  45. package/dist/components/MultiAgentAccelerator/Sidebar/TreeBuilder.d.ts +4 -3
  46. package/dist/components/MultiAgentAccelerator/Sidebar/TreeBuilder.js +8 -2
  47. package/dist/components/MultiAgentAccelerator/TemporaryNetworks.d.ts +19 -2
  48. package/dist/components/MultiAgentAccelerator/TemporaryNetworks.js +40 -5
  49. package/dist/components/MultiAgentAccelerator/ThoughtBubbleOverlay.js +27 -14
  50. package/dist/components/MultiAgentAccelerator/Tour/MainTourSteps.d.ts +7 -0
  51. package/dist/components/MultiAgentAccelerator/Tour/MainTourSteps.js +88 -0
  52. package/dist/components/MultiAgentAccelerator/const.d.ts +7 -10
  53. package/dist/components/MultiAgentAccelerator/const.js +9 -10
  54. package/dist/const.d.ts +5 -1
  55. package/dist/const.js +5 -2
  56. package/dist/controller/agent/Agent.d.ts +10 -0
  57. package/dist/controller/agent/Agent.js +17 -1
  58. package/dist/controller/llm/LlmChat.js +2 -2
  59. package/dist/index.d.ts +0 -1
  60. package/dist/index.js +0 -1
  61. package/dist/state/TemporaryNetworks.d.ts +5 -15
  62. package/dist/state/TemporaryNetworks.js +15 -34
  63. package/dist/state/Tour.d.ts +29 -0
  64. package/dist/state/Tour.js +22 -0
  65. package/dist/state/UserInfo.d.ts +2 -1
  66. package/dist/tsconfig.build.tsbuildinfo +1 -1
  67. package/dist/utils/Authentication.js +12 -3
  68. package/dist/utils/File.d.ts +7 -0
  69. package/dist/utils/File.js +14 -3
  70. package/dist/utils/text.js +2 -2
  71. package/dist/utils/title.js +1 -1
  72. package/dist/utils/zIndexLayers.js +3 -0
  73. package/package.json +15 -11
  74. package/dist/components/AgentChat/ChatCommon/AgentConnectivity.d.ts +0 -14
  75. package/dist/components/AgentChat/ChatCommon/AgentConnectivity.js +0 -23
  76. package/dist/components/AgentChat/ChatCommon/Greetings.d.ts +0 -1
  77. package/dist/components/AgentChat/ChatCommon/Greetings.js +0 -38
  78. package/dist/components/AgentChat/ChatCommon/UserQueryDisplay.d.ts +0 -7
  79. package/dist/components/AgentChat/ChatCommon/UserQueryDisplay.js +0 -32
  80. package/dist/components/Common/LlmChatOptionsButton.d.ts +0 -6
  81. package/dist/components/Common/LlmChatOptionsButton.js +0 -31
@@ -15,11 +15,19 @@ See the License for the specific language governing permissions and
15
15
  limitations under the License.
16
16
  */
17
17
  import AdjustRoundedIcon from "@mui/icons-material/AdjustRounded";
18
- import ChatBubbleOutlineIcon from "@mui/icons-material/ChatBubbleOutline";
18
+ import ChatBubbleOutlinedIcon from "@mui/icons-material/ChatBubbleOutlined";
19
+ import CloseIcon from "@mui/icons-material/Close";
20
+ import EditIcon from "@mui/icons-material/Edit";
19
21
  import HubOutlinedIcon from "@mui/icons-material/HubOutlined";
20
22
  import ScatterPlotOutlinedIcon from "@mui/icons-material/ScatterPlotOutlined";
23
+ import Backdrop from "@mui/material/Backdrop";
21
24
  import Box from "@mui/material/Box";
22
- import { useTheme } from "@mui/material/styles";
25
+ import Button from "@mui/material/Button";
26
+ import CircularProgress from "@mui/material/CircularProgress";
27
+ import IconButton from "@mui/material/IconButton";
28
+ import Paper from "@mui/material/Paper";
29
+ import { alpha, useTheme } from "@mui/material/styles";
30
+ import TextField from "@mui/material/TextField";
23
31
  import ToggleButton from "@mui/material/ToggleButton";
24
32
  import ToggleButtonGroup from "@mui/material/ToggleButtonGroup";
25
33
  import Tooltip from "@mui/material/Tooltip";
@@ -28,10 +36,10 @@ import { applyNodeChanges, Background, ConnectionMode, ControlButton, Controls,
28
36
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
29
37
  import { AgentNode, NODE_HEIGHT, NODE_WIDTH } from "./AgentNode.js";
30
38
  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";
39
+ import { AGENT_NETWORK_DEFINITION_KEY, AGENT_NETWORK_DESIGNER_ID, AGENT_NETWORK_NAME_KEY, BASE_RADIUS, DEFAULT_FRONTMAN_X_POS, DEFAULT_FRONTMAN_Y_POS, LEVEL_SPACING, } from "./const.js";
32
40
  import { addThoughtBubbleEdge, layoutLinear, layoutRadial } from "./GraphLayouts.js";
33
41
  import { PlasmaEdge } from "./PlasmaEdge.js";
34
- import { convertReservationsToNetworks, extractNetworkHocon, extractReservations } from "./TemporaryNetworks.js";
42
+ import { convertReservationsToNetworks, extractNetworkHocon, extractReservations, isEditableAgent, mergeNetworks, } from "./TemporaryNetworks.js";
35
43
  import { ThoughtBubbleEdge } from "./ThoughtBubbleEdge.js";
36
44
  import { ThoughtBubbleOverlay } from "./ThoughtBubbleOverlay.js";
37
45
  import { sendChatQuery } from "../../controller/agent/Agent.js";
@@ -41,35 +49,25 @@ import { useTempNetworksStore } from "../../state/TemporaryNetworks.js";
41
49
  import { usePalette } from "../../Theme/Palettes.js";
42
50
  import { getZIndex } from "../../utils/zIndexLayers.js";
43
51
  import { chatMessageFromChunk } from "../AgentChat/Common/Utils.js";
52
+ import { MUIAlert } from "../Common/MUIAlert.js";
44
53
  import { NotificationType, sendNotification } from "../Common/notification.js";
45
54
  // #endregion: Types
46
55
  // #region: Constants
47
56
  // Timeout for thought bubbles is set to 10 seconds
48
57
  const THOUGHT_BUBBLE_TIMEOUT_MS = 10_000;
58
+ // How long the dock'sstatus banner stays visible before auto-dismissing. Error banners persist until dismissed.
59
+ // Exported for tests.
60
+ export const DOCK_BANNER_AUTO_DISMISS_MS = 5_000;
49
61
  // #endregion: Constants
62
+ const DOCK_PROMPT_PLACEHOLDER = "Describe a change to the network";
50
63
  // #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
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.
65
+ * Streams the Agent Network Designer endpoint with a natural-language prompt and the current
66
+ * network definition, collecting any returned reservations.
67
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) => {
68
+ const streamNetworkDesignerPrompt = async (neuroSanURL, signal, userPrompt, currentDefinition, agentNetworkName, currentUser) => {
69
+ let newNetworks = [];
70
+ await sendChatQuery(neuroSanURL, signal, userPrompt, AGENT_NETWORK_DESIGNER_ID, (chunk) => {
73
71
  const chatMessage = chatMessageFromChunk(chunk);
74
72
  if (!chatMessage)
75
73
  return;
@@ -77,26 +75,19 @@ const streamNetworkDesignerUpdate = async (neuroSanURL, signal, agentName, updat
77
75
  if (reservations.length === 0)
78
76
  return;
79
77
  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
78
  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
79
  const networkName = agentNetworkName ?? agentNetworkNameFromMessage;
87
- const converted = convertReservationsToNetworks(reservations, networkHocon, updated, networkName);
88
- mergeNetworks(newNetworks, converted);
80
+ const definitionFromMessage = chatMessage.sly_data?.[AGENT_NETWORK_DEFINITION_KEY];
81
+ const converted = convertReservationsToNetworks(reservations, networkHocon, definitionFromMessage ?? currentDefinition, networkName);
82
+ newNetworks = mergeNetworks(newNetworks, converted);
89
83
  }, null, {
90
- [AGENT_NETWORK_DEFINITION_KEY]: updated,
91
- // Use the backend's canonical name, not the local UUID-based key.
84
+ [AGENT_NETWORK_DEFINITION_KEY]: currentDefinition,
92
85
  ...(agentNetworkName ? { [AGENT_NETWORK_NAME_KEY]: agentNetworkName } : {}),
93
- // skip_designer prevents the backend from using a reasoning model for edits
94
- skip_designer: true,
95
86
  }, currentUser, StreamingUnit.Line);
96
87
  return newNetworks;
97
88
  };
98
89
  // #endregion: Helpers
99
- export const AgentFlow = ({ agentCounts, agentIconSuggestions, agentsInNetwork, currentConversations, currentUser, id, isAgentNetworkDesignerMode, isAwaitingLlm, isStreaming, isSelectedNetworkTemporary: isTemporaryNetwork, networkId, neuroSanURL, onNetworkReplaced, thoughtBubbleEdges, setThoughtBubbleEdges, }) => {
90
+ export const AgentFlow = ({ agentCounts, agentIconSuggestions, agentsInNetwork, currentConversations, currentUser, id, isAgentNetworkDesignerMode, isAwaitingLlm, isEditMode, isStreaming, isSelectedNetworkTemporary: isTemporaryNetwork, networkDisplayName, networkId, neuroSanURL, onEnterEditMode, onNetworkReplaced, onExitEditMode, onSaveAgent, thoughtBubbleEdges, setThoughtBubbleEdges, }) => {
100
91
  const theme = useTheme();
101
92
  const { fitView } = useReactFlow();
102
93
  const handleResize = useCallback(() => {
@@ -146,7 +137,7 @@ export const AgentFlow = ({ agentCounts, agentIconSuggestions, agentsInNetwork,
146
137
  let edgesMap = null;
147
138
  for (const conv of currentConversations) {
148
139
  const convText = conv.text?.trim();
149
- const agentList = Array.from(conv.agents);
140
+ const agentList = [...conv.agents];
150
141
  if (convText &&
151
142
  agentList.length >= 2 &&
152
143
  !processedText.has(convText) &&
@@ -197,7 +188,8 @@ export const AgentFlow = ({ agentCounts, agentIconSuggestions, agentsInNetwork,
197
188
  return () => clearInterval(cleanupInterval);
198
189
  }, [setThoughtBubbleEdges]); // mount/unmount only
199
190
  // Shadow color for icon
200
- const shadowColor = theme.palette.mode === "dark" ? theme.palette.common.white : theme.palette.common.black;
191
+ const isDarkMode = theme.palette.mode === "dark";
192
+ const foregroundColor = isDarkMode ? theme.palette.common.white : theme.palette.common.black;
201
193
  const isHeatmap = coloringOption === "heatmap";
202
194
  const palette = usePalette();
203
195
  // Merge agents from active thought bubbles with agentsInNetwork for layout
@@ -213,7 +205,7 @@ export const AgentFlow = ({ agentCounts, agentIconSuggestions, agentsInNetwork,
213
205
  const mergedAgentsInNetwork = useMemo(() => {
214
206
  // Add any missing agents from bubbles as minimal ConnectivityInfo
215
207
  const existingIds = new Set(agentsInNetwork.map((a) => a.origin));
216
- const missing = Array.from(bubbleAgentIds).filter((bubbleAgentId) => !existingIds.has(bubbleAgentId));
208
+ const missing = [...bubbleAgentIds].filter((bubbleAgentId) => !existingIds.has(bubbleAgentId));
217
209
  const minimalAgents = missing.map((missingId) => ({
218
210
  origin: missingId,
219
211
  tools: [],
@@ -249,6 +241,49 @@ export const AgentFlow = ({ agentCounts, agentIconSuggestions, agentsInNetwork,
249
241
  const [isSavingAgent, setIsSavingAgent] = useState(false);
250
242
  // AbortController for the in-flight save request — stored in a ref so handlePopupClose can cancel it.
251
243
  const saveAbortControllerRef = useRef(null);
244
+ // Dock (edit-mode prompt bar) state
245
+ const [dockPrompt, setDockPrompt] = useState("");
246
+ const [isDockStreaming, setIsDockStreaming] = useState(false);
247
+ const dockAbortControllerRef = useRef(null);
248
+ // Stop-confirm overlay state: null = not shown, "confirming" = abort dialog open.
249
+ const [stopState, setStopState] = useState(null);
250
+ // Inline status banner shown above the dock header after an apply succeeds, is cancelled, or fails.
251
+ const [dockBanner, setDockBanner] = useState(null);
252
+ const bannerTimeoutRef = useRef(null);
253
+ // Clear the banner auto-dismiss timer on unmount.
254
+ useEffect(() => {
255
+ return () => {
256
+ clearTimeout(bannerTimeoutRef.current);
257
+ };
258
+ }, []);
259
+ const handleDismissBanner = useCallback(() => {
260
+ clearTimeout(bannerTimeoutRef.current);
261
+ setDockBanner(null);
262
+ }, []);
263
+ // Show a dock banner. Success/cancel banners auto-dismiss; error banners persist until dismissed.
264
+ const showDockBanner = useCallback((banner) => {
265
+ clearTimeout(bannerTimeoutRef.current);
266
+ setDockBanner(banner);
267
+ if (banner.severity !== "error") {
268
+ bannerTimeoutRef.current = setTimeout(() => setDockBanner(null), DOCK_BANNER_AUTO_DISMISS_MS);
269
+ }
270
+ }, []);
271
+ const handleStopClick = useCallback(() => {
272
+ setStopState("confirming");
273
+ }, []);
274
+ const handleKeepApplying = useCallback(() => {
275
+ setStopState(null);
276
+ }, []);
277
+ const handleStopAndDiscard = useCallback(() => {
278
+ dockAbortControllerRef.current?.abort();
279
+ dockAbortControllerRef.current = null;
280
+ setStopState(null);
281
+ showDockBanner({
282
+ severity: "info",
283
+ title: "Applying cancelled.",
284
+ detail: "Nothing was changed. Your prompt is restored below.",
285
+ });
286
+ }, [showDockBanner]);
252
287
  const handleNodeClick = useCallback((_event, node) => {
253
288
  // Popup is only available for temporary networks.
254
289
  if (!isTemporaryNetwork)
@@ -276,11 +311,18 @@ export const AgentFlow = ({ agentCounts, agentIconSuggestions, agentsInNetwork,
276
311
  setIsPopupOpen(false);
277
312
  setIsSavingAgent(false);
278
313
  }, []);
279
- /** Applies the networks returned by the designer: upserts them and triggers navigation if needed. */
280
- const applyNetworkSaveResult = useCallback((agentName, newNetworksFromSave, currentAgentNetworkName) => {
314
+ /**
315
+ * Applies the networks returned by the designer: upserts them and triggers navigation if needed.
316
+ * Returns true when a matching reservation was applied, false (and surfaces an error banner) otherwise.
317
+ */
318
+ const applyNetworkSaveResult = useCallback((newNetworksFromSave, currentAgentNetworkName) => {
281
319
  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;
320
+ showDockBanner({
321
+ severity: "error",
322
+ title: "Failed to apply network change.",
323
+ detail: "The network designer did not return a reservation. Please try again.",
324
+ });
325
+ return false;
284
326
  }
285
327
  const replacement = newNetworksFromSave.find((n) => n.agentNetworkName === currentAgentNetworkName);
286
328
  if (replacement) {
@@ -289,12 +331,70 @@ export const AgentFlow = ({ agentCounts, agentIconSuggestions, agentsInNetwork,
289
331
  useAgentChatHistoryStore.getState().copyHistory(networkId, replacement.agentInfo.agent_name);
290
332
  onNetworkReplaced(networkId, replacement.agentInfo.agent_name);
291
333
  }
334
+ return true;
335
+ }
336
+ // Reservations came back but none matched the current network — surface this in the dock banner.
337
+ showDockBanner({
338
+ severity: "error",
339
+ title: "Failed to apply network change.",
340
+ detail: "A reservation was returned but did not match the current network. Please try again.",
341
+ });
342
+ return false;
343
+ }, [networkId, onNetworkReplaced, showDockBanner]);
344
+ const handleDockApply = useCallback(async () => {
345
+ if (!dockPrompt.trim() || !neuroSanURL || !currentUser)
346
+ return;
347
+ const currentTempNetwork = networkId
348
+ ? tempNetworks.find((n) => n.agentInfo.agent_name === networkId)
349
+ : undefined;
350
+ const currentDefinition = currentTempNetwork?.agentNetworkDefinition ?? [];
351
+ setIsDockStreaming(true);
352
+ const controller = new AbortController();
353
+ dockAbortControllerRef.current = controller;
354
+ let hasTimedOut = false;
355
+ const timeoutId = setTimeout(() => {
356
+ hasTimedOut = true;
357
+ controller.abort();
358
+ }, 120_000); // 2 min timeout
359
+ try {
360
+ const newNetworks = await streamNetworkDesignerPrompt(neuroSanURL, controller.signal, dockPrompt, currentDefinition, currentTempNetwork?.agentNetworkName, currentUser);
361
+ const applied = applyNetworkSaveResult(newNetworks, currentTempNetwork?.agentNetworkName);
362
+ if (applied) {
363
+ setDockPrompt("");
364
+ showDockBanner({
365
+ severity: "success",
366
+ title: "Changes applied.",
367
+ detail: "Your network has been updated.",
368
+ });
369
+ }
292
370
  }
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.");
371
+ catch (e) {
372
+ const isAbort = e instanceof DOMException && e.name === "AbortError";
373
+ if (!isAbort) {
374
+ showDockBanner({ severity: "error", title: "Failed to apply network change.", detail: String(e) });
375
+ }
376
+ else if (hasTimedOut) {
377
+ showDockBanner({
378
+ severity: "error",
379
+ title: "Failed to apply network change.",
380
+ detail: "The request timed out. Please try again.",
381
+ });
382
+ }
296
383
  }
297
- }, [networkId, onNetworkReplaced]);
384
+ finally {
385
+ clearTimeout(timeoutId);
386
+ dockAbortControllerRef.current = null;
387
+ setIsDockStreaming(false);
388
+ }
389
+ }, [applyNetworkSaveResult, currentUser, dockPrompt, networkId, neuroSanURL, showDockBanner, tempNetworks]);
390
+ const handleExitEditMode = useCallback(() => {
391
+ if (isDockStreaming) {
392
+ dockAbortControllerRef.current?.abort();
393
+ dockAbortControllerRef.current = null;
394
+ setIsDockStreaming(false);
395
+ }
396
+ onExitEditMode?.();
397
+ }, [isDockStreaming, onExitEditMode]);
298
398
  const handlePopupSave = useCallback(async (agentName, instructionsText, descriptionText) => {
299
399
  if (!selectedAgent)
300
400
  return;
@@ -310,33 +410,22 @@ export const AgentFlow = ({ agentCounts, agentIconSuggestions, agentsInNetwork,
310
410
  if (networkId) {
311
411
  updateTempNetworkDefinition(networkId, updated);
312
412
  }
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) {
413
+ if (!onSaveAgent) {
317
414
  setIsPopupOpen(false);
318
415
  return;
319
416
  }
320
417
  setIsSavingAgent(true);
321
418
  const saveController = new AbortController();
322
419
  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);
420
+ const saveTimeoutId = setTimeout(() => saveController.abort(new DOMException("Save timed out", "TimeoutError")), 60_000 // 1 min timeout
421
+ );
325
422
  try {
326
- const newNetworksFromSave = await streamNetworkDesignerUpdate(neuroSanURL, saveController.signal, agentName, updated, currentTempNetwork?.agentNetworkName, currentUser);
327
- applyNetworkSaveResult(agentName, newNetworksFromSave, currentTempNetwork?.agentNetworkName);
423
+ await onSaveAgent(agentName, updated, currentTempNetwork?.agentNetworkName, saveController.signal);
328
424
  }
329
425
  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.
426
+ console.error(`Error saving network ${agentName}. See onSaveAgent implementation for details.`, e);
427
+ sendNotification(NotificationType.error, `Failed to save agent "${agentName}".`, String(e), undefined, null // show indefinitely until the user dismisses
428
+ );
340
429
  }
341
430
  finally {
342
431
  clearTimeout(saveTimeoutId);
@@ -344,15 +433,7 @@ export const AgentFlow = ({ agentCounts, agentIconSuggestions, agentsInNetwork,
344
433
  setIsSavingAgent(false);
345
434
  setIsPopupOpen(false);
346
435
  }
347
- }, [
348
- selectedAgent,
349
- tempNetworks,
350
- updateTempNetworkDefinition,
351
- neuroSanURL,
352
- currentUser,
353
- networkId,
354
- applyNetworkSaveResult,
355
- ]);
436
+ }, [selectedAgent, tempNetworks, updateTempNetworkDefinition, networkId, onSaveAgent]);
356
437
  const edges = layoutResult.edges;
357
438
  // Make sure to extract only thought bubble edges for the overlay.
358
439
  const thoughtBubbleEdgesForOverlay = useMemo(() => edges.filter((e) => e.type === "thoughtBubbleEdge"), [edges]);
@@ -399,11 +480,11 @@ export const AgentFlow = ({ agentCounts, agentIconSuggestions, agentsInNetwork,
399
480
  const length = isHeatmap ? palette.length : Math.min(maxDepth, palette.length);
400
481
  return (_jsxs(Box, { id: `${id}-legend`, sx: {
401
482
  position: "absolute",
402
- top: "5px",
483
+ top: "1.5rem",
403
484
  right: "10px",
404
485
  padding: "5px",
405
486
  borderRadius: "5px",
406
- boxShadow: `0 0 5px color-mix(in srgb, ${shadowColor} 30%, transparent)`,
487
+ boxShadow: `0 0 5px color-mix(in srgb, ${foregroundColor} 30%, transparent)`,
407
488
  display: "flex",
408
489
  alignItems: "center",
409
490
  zIndex: getZIndex(2, theme),
@@ -444,7 +525,7 @@ export const AgentFlow = ({ agentCounts, agentIconSuggestions, agentsInNetwork,
444
525
  if (!isActive) {
445
526
  return undefined;
446
527
  }
447
- return theme.palette.mode === "dark" ? theme.palette.grey[800] : theme.palette.grey[200];
528
+ return isDarkMode ? theme.palette.grey[800] : theme.palette.grey[200];
448
529
  };
449
530
  // Only show radial guides if radial layout is selected, radial guides are enabled, and it's not just Frontman
450
531
  const shouldShowRadialGuides = enableRadialGuides && layout === "radial" && maxDepth > 1;
@@ -464,25 +545,144 @@ export const AgentFlow = ({ agentCounts, agentIconSuggestions, agentsInNetwork,
464
545
  backgroundColor: getControlButtonBackgroundColor(enableRadialGuides),
465
546
  }, disabled: layout !== "radial", children: _jsx(AdjustRoundedIcon, { id: "radial-guides-icon" }) }) }) }), _jsx(Tooltip, { id: "thought-bubble-tooltip", title: `Toggle thought bubbles ${showThoughtBubbles ? "off" : "on"}`, placement: "right", children: _jsx("span", { id: "thought-bubble-span", children: _jsx(ControlButton, { id: "thought-bubble-button", onClick: () => setShowThoughtBubbles(!showThoughtBubbles), style: {
466
547
  backgroundColor: getControlButtonBackgroundColor(showThoughtBubbles),
467
- }, children: _jsx(ChatBubbleOutlineIcon, { id: "thought-bubble-icon" }) }) }) })] }));
548
+ }, children: _jsx(ChatBubbleOutlinedIcon, { id: "thought-bubble-icon" }) }) }) })] }));
549
+ };
550
+ const titleBackgroundColor = alpha(theme.palette.background.paper, 0.75);
551
+ const getTitle = () => {
552
+ return (networkDisplayName && (_jsxs(Box, { id: `${id}-network-title-bar`, sx: {
553
+ alignItems: "center",
554
+ display: "flex",
555
+ gap: 1,
556
+ left: "50%",
557
+ pointerEvents: "none",
558
+ position: "absolute",
559
+ top: 0,
560
+ transform: "translateX(-50%)",
561
+ zIndex: getZIndex(2, theme),
562
+ }, children: [_jsx(Tooltip, { title: networkDisplayName, placement: "top", children: _jsx(Box, { sx: { pointerEvents: "auto" }, children: _jsx(Typography, { id: `${id}-network-title`, variant: "subtitle1", sx: {
563
+ backdropFilter: "blur(6px)",
564
+ backgroundColor: titleBackgroundColor,
565
+ border: `1px solid ${alpha(theme.palette.divider, 0.6)}`,
566
+ borderRadius: 2,
567
+ boxShadow: theme.palette.mode === "dark"
568
+ ? `0 6px 20px ${alpha(theme.palette.common.black, 0.35)}`
569
+ : `0 6px 16px ${alpha(theme.palette.common.black, 0.12)}`,
570
+ color: theme.palette.getContrastText(alpha(titleBackgroundColor, 0.65)),
571
+ fontWeight: 600,
572
+ letterSpacing: "0.01em",
573
+ lineHeight: 1.35,
574
+ maxWidth: 400,
575
+ overflow: "hidden",
576
+ px: 2,
577
+ py: 0.45,
578
+ textOverflow: "ellipsis",
579
+ whiteSpace: "nowrap",
580
+ }, children: networkDisplayName }) }) }), isTemporaryNetwork && !isEditMode && !isAwaitingLlm && onEnterEditMode && (_jsx(Button, { id: `${id}-enter-edit-mode-btn`, variant: "contained", size: "small", onClick: onEnterEditMode, startIcon: _jsx(EditIcon, {}), sx: {
581
+ pointerEvents: "auto",
582
+ "&:hover": { backgroundColor: theme.palette.primary.main },
583
+ }, children: "Edit" }))] })));
468
584
  };
469
585
  return (_jsxs(Box, { id: `${id}-outer-box`, sx: {
586
+ display: "flex",
587
+ flexDirection: "column",
588
+ position: "relative",
470
589
  height: "100%",
471
590
  width: "100%",
472
591
  backgroundColor: theme.palette.background.default,
473
592
  "& .react-flow__node": {
474
- border: `1px solid ${theme.palette.divider}`,
593
+ border: "1px solid divider",
475
594
  },
476
595
  "& .react-flow__panel": {
477
596
  backgroundColor: theme.palette.background.paper,
478
- border: `1px solid ${theme.palette.divider}`,
597
+ border: "1px solid divider",
479
598
  color: theme.palette.text.primary,
480
599
  },
481
600
  "& .react-flow__controls-button": {
482
601
  backgroundColor: theme.palette.background.paper,
483
- borderBottom: `1px solid ${theme.palette.divider}`,
602
+ borderBottom: "1px solid divider",
484
603
  color: theme.palette.text.primary,
485
604
  fill: theme.palette.text.primary,
486
605
  },
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 }))] }));
606
+ }, children: [_jsxs(Box, { id: `${id}-react-flow-wrapper`, sx: { position: "relative", flex: 1, minHeight: 0 }, children: [networkDisplayName ? _jsx(Box, { sx: { marginBottom: "1rem" }, children: getTitle() }) : null, _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 && !isEditMode ? getLegend() : null, _jsx(Background, { id: `${id}-background` }), !isAgentNetworkDesignerMode && !isEditMode && getControls(), shouldShowRadialGuides ? getRadialGuides() : null] })) }), _jsx(ThoughtBubbleOverlay, { nodes: nodes, edges: thoughtBubbleEdgesForOverlay, showThoughtBubbles: showThoughtBubbles, isStreaming: isStreaming, onBubbleHoverChange: handleBubbleHoverChange })] }), isEditMode && isTemporaryNetwork && !isAwaitingLlm && (_jsxs(Box, { sx: {
607
+ borderTop: `2px solid ${theme.palette.primary.main}`,
608
+ backgroundColor: theme.palette.background.paper,
609
+ flexShrink: 0,
610
+ }, children: [dockBanner && (_jsx(MUIAlert, { closeable: true, id: `${id}-dock-banner`, onClose: handleDismissBanner, severity: dockBanner.severity, sx: {
611
+ borderRadius: 0,
612
+ // Override MUIAlert's default 1rem bottom margin so the banner sits flush
613
+ // against the dock header below it.
614
+ marginBottom: 0,
615
+ py: 0,
616
+ // Match the dock header's right padding so the banner's close X lines up
617
+ // vertically with the header's close X below it.
618
+ paddingRight: 0.5,
619
+ alignItems: "center",
620
+ // Frost the banner like the dock header so the graph doesn't show through the
621
+ // app's translucent paper background; keep a tinted, mostly-opaque severity wash.
622
+ backdropFilter: "blur(8px)",
623
+ backgroundColor: alpha(theme.palette[dockBanner.severity].main, isDarkMode ? 0.28 : 0.16),
624
+ "& .MuiAlert-action": {
625
+ alignItems: "center",
626
+ marginRight: 0,
627
+ paddingTop: 0,
628
+ },
629
+ }, children: _jsxs(Typography, { variant: "caption", component: "span", children: [_jsx("strong", { children: dockBanner.title }), ` ${dockBanner.detail}`] }) })), _jsxs(Box, { sx: {
630
+ backdropFilter: "blur(6px)",
631
+ borderBottom: "1px solid divider",
632
+ display: "flex",
633
+ alignItems: "center",
634
+ justifyContent: "space-between",
635
+ paddingLeft: 1.25,
636
+ paddingRight: 0.25,
637
+ }, children: [_jsx(Typography, { variant: "overline", sx: { fontWeight: "bold", letterSpacing: 1, lineHeight: 1.8 }, children: "Network Editor" }), _jsx(IconButton, { size: "small", "aria-label": "close edit mode", onClick: handleExitEditMode, children: _jsx(CloseIcon, { fontSize: "small" }) })] }), _jsxs(Box, { sx: {
638
+ display: "flex",
639
+ gap: 1,
640
+ px: 1,
641
+ py: 0.5,
642
+ alignItems: "center",
643
+ }, children: [_jsx(TextField, { fullWidth: true, placeholder: DOCK_PROMPT_PLACEHOLDER, variant: "outlined", size: "small", value: dockPrompt, onChange: (e) => setDockPrompt(e.target.value), onKeyDown: (e) => {
644
+ if (e.key === "Enter" && !e.shiftKey) {
645
+ e.preventDefault();
646
+ void handleDockApply();
647
+ }
648
+ }, disabled: isDockStreaming, slotProps: { htmlInput: { style: { fontSize: "0.75rem" } } } }), _jsx(Button, { variant: "contained", onClick: () => void handleDockApply(), disabled: isDockStreaming || !dockPrompt.trim(), sx: {
649
+ fontSize: 16,
650
+ marginBottom: "1px",
651
+ marginRight: 0,
652
+ minWidth: 110,
653
+ paddingTop: 0.3,
654
+ paddingBottom: 0.3,
655
+ whiteSpace: "nowrap",
656
+ }, startIcon: isDockStreaming ? (_jsx(CircularProgress, { size: 16, color: "inherit" })) : undefined, children: isDockStreaming ? "Applying..." : "Apply" })] })] })), selectedAgent && !isAwaitingLlm && (_jsx(AgentNodePopup, { agentName: selectedAgent.agentName, initialInstructions: selectedAgent.initialInstructions, initialDescription: selectedAgent.initialDescription, isOpen: isPopupOpen, isSaving: isSavingAgent, onClose: handlePopupClose, onSave: handlePopupSave })), _jsx(Backdrop, { open: isDockStreaming, sx: { zIndex: (t) => t.zIndex.modal + 1 }, children: stopState === "confirming" ? (_jsxs(Paper, { elevation: 6, sx: {
657
+ display: "flex",
658
+ flexDirection: "column",
659
+ gap: 2,
660
+ px: 4,
661
+ py: 3,
662
+ borderRadius: 2,
663
+ maxWidth: 420,
664
+ }, children: [_jsx(Typography, { variant: "body1", sx: { fontWeight: "bold" }, children: "Abort changes?" }), _jsx(Typography, { variant: "body2", color: "text.secondary", children: "The in-progress update will be cancelled and discarded. Your network will not be modified." }), _jsxs(Box, { sx: {
665
+ display: "flex",
666
+ gap: 1.5,
667
+ justifyContent: "flex-end",
668
+ }, children: [_jsx(Button, { variant: "outlined", onClick: handleKeepApplying, children: "Keep applying" }), _jsx(Button, { variant: "contained", color: "error", startIcon: _jsx("span", { style: { fontSize: "0.7rem" }, children: "\u25A0" }), onClick: handleStopAndDiscard, children: "Stop & discard" })] })] })) : (_jsxs(Paper, { elevation: 6, sx: {
669
+ display: "flex",
670
+ alignItems: "center",
671
+ gap: 2,
672
+ px: 4,
673
+ py: 2.5,
674
+ borderRadius: 2,
675
+ maxWidth: 480,
676
+ }, children: [_jsx(CircularProgress, { size: 24 }), _jsxs(Box, { sx: { flex: 1 }, children: [_jsx(Typography, { variant: "body1", sx: { fontWeight: "bold" }, children: "Applying changes to network" }), dockPrompt && (_jsx(Typography, { variant: "body2", color: "text.secondary", sx: { mt: 0.25 }, children: dockPrompt }))] }), _jsx(Button, { variant: "outlined", size: "small", startIcon: _jsx("span", { style: { fontSize: "0.65rem" }, children: "\u25A0" }), onClick: handleStopClick, sx: {
677
+ whiteSpace: "nowrap",
678
+ flexShrink: 0,
679
+ color: theme.palette.common.white,
680
+ borderColor: theme.palette.common.white,
681
+ fontWeight: "bold",
682
+ "&:hover": {
683
+ borderColor: theme.palette.error.main,
684
+ color: theme.palette.error.main,
685
+ backgroundColor: alpha(theme.palette.error.main, 0.08),
686
+ },
687
+ }, children: "Stop" })] })) })] }));
488
688
  };
@@ -9,11 +9,13 @@ export interface AgentNodeProps extends Record<string, unknown> {
9
9
  readonly displayAs?: string;
10
10
  readonly getConversations: () => AgentConversation[] | null;
11
11
  readonly isAwaitingLlm?: boolean;
12
+ readonly isEditMode?: boolean;
12
13
  readonly agentIconSuggestion?: string;
13
- readonly isTemporaryNetwork?: boolean;
14
+ readonly isEditable?: boolean;
14
15
  }
15
16
  export declare const NODE_HEIGHT = 100;
16
17
  export declare const NODE_WIDTH = 100;
18
+ export declare const FRONTMAN_SIZE_MULTIPLIER = 1.25;
17
19
  /**
18
20
  * A node representing an agent in the network for use in react-flow.
19
21
  * @param props See AgentNodeProps