@cognizant-ai-lab/ui-common 1.8.0 → 1.10.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 (59) hide show
  1. package/README.md +3 -3
  2. package/dist/components/AgentChat/ChatCommon/ChatCommon.d.ts +9 -3
  3. package/dist/components/AgentChat/ChatCommon/ChatCommon.js +94 -27
  4. package/dist/components/AgentChat/ChatCommon/ChatHistory.d.ts +7 -0
  5. package/dist/components/AgentChat/ChatCommon/ChatHistory.js +1 -1
  6. package/dist/components/AgentChat/ChatCommon/ControlButtons.d.ts +4 -2
  7. package/dist/components/AgentChat/ChatCommon/ControlButtons.js +8 -2
  8. package/dist/components/AgentChat/ChatCommon/ConversationTurn.d.ts +5 -5
  9. package/dist/components/AgentChat/ChatCommon/ConversationTurn.js +1 -0
  10. package/dist/components/AgentChat/ChatCommon/Thinking.js +2 -2
  11. package/dist/components/AgentChat/Common/Utils.d.ts +0 -7
  12. package/dist/components/AgentChat/Common/Utils.js +0 -12
  13. package/dist/components/Common/Breadcrumbs.js +1 -1
  14. package/dist/components/Common/Footer.js +4 -0
  15. package/dist/components/Common/notification.d.ts +4 -4
  16. package/dist/components/MultiAgentAccelerator/{AgentFlow.d.ts → AgentFlow/AgentFlow.d.ts} +15 -5
  17. package/dist/components/MultiAgentAccelerator/{AgentFlow.js → AgentFlow/AgentFlow.js} +99 -58
  18. package/dist/components/MultiAgentAccelerator/{AgentNode.d.ts → AgentFlow/AgentNode.d.ts} +1 -1
  19. package/dist/components/MultiAgentAccelerator/{AgentNode.js → AgentFlow/AgentNode.js} +11 -9
  20. package/dist/components/MultiAgentAccelerator/{GraphLayouts.d.ts → AgentFlow/GraphLayouts.d.ts} +4 -4
  21. package/dist/components/MultiAgentAccelerator/{GraphLayouts.js → AgentFlow/GraphLayouts.js} +15 -41
  22. package/dist/components/MultiAgentAccelerator/AgentFlow/GraphStructure.d.ts +21 -0
  23. package/dist/components/MultiAgentAccelerator/AgentFlow/GraphStructure.js +27 -0
  24. package/dist/components/MultiAgentAccelerator/AgentFlow/PlasmaEdge.d.ts +7 -0
  25. package/dist/components/MultiAgentAccelerator/{PlasmaEdge.js → AgentFlow/PlasmaEdge.js} +13 -8
  26. package/dist/components/MultiAgentAccelerator/MultiAgentAccelerator.js +74 -83
  27. package/dist/components/MultiAgentAccelerator/Schema/SlyData.d.ts +17 -0
  28. package/dist/components/MultiAgentAccelerator/Schema/SlyData.js +23 -0
  29. package/dist/components/MultiAgentAccelerator/Sidebar/AgentNetworkTreeItem.d.ts +2 -0
  30. package/dist/components/MultiAgentAccelerator/Sidebar/AgentNetworkTreeItem.js +22 -25
  31. package/dist/components/MultiAgentAccelerator/Sidebar/Sidebar.js +16 -22
  32. package/dist/components/MultiAgentAccelerator/Sidebar/TreeBuilder.d.ts +1 -0
  33. package/dist/components/MultiAgentAccelerator/Sidebar/TreeBuilder.js +27 -7
  34. package/dist/components/MultiAgentAccelerator/TemporaryNetworks.d.ts +8 -0
  35. package/dist/components/MultiAgentAccelerator/TemporaryNetworks.js +61 -13
  36. package/dist/components/MultiAgentAccelerator/{ThoughtBubbleEdge.d.ts → ThoughtBubbles/ThoughtBubbleEdge.d.ts} +1 -1
  37. package/dist/components/MultiAgentAccelerator/{ThoughtBubbleOverlay.js → ThoughtBubbles/ThoughtBubbleOverlay.js} +1 -1
  38. package/dist/components/MultiAgentAccelerator/Tour/MainTourSteps.js +7 -2
  39. package/dist/components/MultiAgentAccelerator/const.d.ts +11 -10
  40. package/dist/components/MultiAgentAccelerator/const.js +22 -20
  41. package/dist/components/Settings/SettingsDialog.js +26 -10
  42. package/dist/controller/agent/Agent.d.ts +14 -6
  43. package/dist/controller/agent/Agent.js +18 -14
  44. package/dist/index.d.ts +1 -1
  45. package/dist/index.js +1 -1
  46. package/dist/state/Settings.d.ts +81 -4
  47. package/dist/state/Settings.js +87 -7
  48. package/dist/state/TemporaryNetworks.js +12 -21
  49. package/dist/tsconfig.build.tsbuildinfo +1 -1
  50. package/dist/utils/AgentName.d.ts +17 -0
  51. package/dist/utils/AgentName.js +46 -0
  52. package/dist/utils/BrowserNavigation.js +2 -0
  53. package/dist/utils/File.d.ts +4 -1
  54. package/dist/utils/File.js +4 -9
  55. package/dist/utils/text.js +3 -7
  56. package/package.json +9 -10
  57. package/dist/components/MultiAgentAccelerator/PlasmaEdge.d.ts +0 -3
  58. /package/dist/components/MultiAgentAccelerator/{ThoughtBubbleEdge.js → ThoughtBubbles/ThoughtBubbleEdge.js} +0 -0
  59. /package/dist/components/MultiAgentAccelerator/{ThoughtBubbleOverlay.d.ts → ThoughtBubbles/ThoughtBubbleOverlay.d.ts} +0 -0
@@ -16,7 +16,11 @@ limitations under the License.
16
16
  */
17
17
  import { getBezierPath } from "@xyflow/react";
18
18
  import { useEffect, useRef } from "react";
19
- import { useSettingsStore } from "../../state/Settings.js";
19
+ import { useSettingsStore } from "../../../state/Settings.js";
20
+ //#region Constants
21
+ const DEFAULT_MAX_PARTICLES = 250;
22
+ const DEFAULT_PARTICLES_PER_FRAME = 25;
23
+ //#endregion Interfaces and Types
20
24
  const createFunnelParticleOnPath = (pathEl, canvasOffset, baseProgress, plasmaColor) => {
21
25
  const totalLength = pathEl.getTotalLength();
22
26
  const speed = 0.02 + Math.random() * 0.003;
@@ -64,7 +68,7 @@ const createFunnelParticleOnPath = (pathEl, canvasOffset, baseProgress, plasmaCo
64
68
  const isAlive = () => progress * totalLength < totalLength * 0.98;
65
69
  return { update, draw, isAlive };
66
70
  };
67
- export const PlasmaEdge = ({ sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, }) => {
71
+ export const PlasmaEdge = ({ maxParticles = DEFAULT_MAX_PARTICLES, particlesPerFrame = DEFAULT_PARTICLES_PER_FRAME, sourcePosition, sourceX, sourceY, targetPosition, targetX, targetY, }) => {
68
72
  const canvasRef = useRef(null);
69
73
  const pathRef = useRef(null);
70
74
  const animationRef = useRef(null);
@@ -97,12 +101,10 @@ export const PlasmaEdge = ({ sourceX, sourceY, targetX, targetY, sourcePosition,
97
101
  ctx.setTransform(1, 0, 0, 1, 0, 0);
98
102
  ctx.scale(dpr, dpr);
99
103
  const canvasOffset = { x, y };
100
- const MAX_PARTICLES = 250;
101
- const PARTICLES_PER_FRAME = 25;
102
104
  const animate = () => {
103
105
  ctx.clearRect(0, 0, width, height);
104
- for (let i = 0; i < PARTICLES_PER_FRAME; i += 1) {
105
- if (particles.current.length < MAX_PARTICLES) {
106
+ for (let i = 0; i < particlesPerFrame; i += 1) {
107
+ if (particles.current.length < maxParticles) {
106
108
  const t = Math.random();
107
109
  if (Math.random() < 1 - t) {
108
110
  particles.current.push(createFunnelParticleOnPath(pathEl, canvasOffset, t, plasmaColor));
@@ -118,9 +120,12 @@ export const PlasmaEdge = ({ sourceX, sourceY, targetX, targetY, sourcePosition,
118
120
  };
119
121
  animate();
120
122
  return () => {
121
- if (animationRef.current !== undefined)
123
+ if (animationRef.current !== undefined) {
122
124
  cancelAnimationFrame(animationRef.current);
125
+ animationRef.current = undefined;
126
+ }
127
+ particles.current = [];
123
128
  };
124
- }, [edgePath, width, height, plasmaColor, x, y]);
129
+ }, [edgePath, width, height, plasmaColor, x, y, particlesPerFrame, maxParticles]);
125
130
  return (_jsxs(_Fragment, { children: [_jsx("foreignObject", { id: `foreign-object-${x}-${y}`, width: width, height: height, x: x, y: y, style: { pointerEvents: "none", overflow: "visible" }, children: _jsx("canvas", { id: `canvas-${x}-${y}`, ref: canvasRef }) }), _jsx("path", { id: `path-${edgePath}`, ref: pathRef, d: edgePath, fill: "none", stroke: "none", style: { position: "absolute", visibility: "hidden" } })] }));
126
131
  };
@@ -26,64 +26,33 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
26
26
  import { useJoyride } from "react-joyride";
27
27
  import { extractConversations } from "./AgentConversations.js";
28
28
  import { getUpdatedAgentCounts } from "./AgentCounts.js";
29
- import { AgentFlow } from "./AgentFlow.js";
29
+ import { AgentFlow } from "./AgentFlow/AgentFlow.js";
30
30
  import { extractAgentNetworkDesignerProgress } from "./AgentNetworkDesigner.js";
31
- import { AGENT_NETWORK_DEFINITION_KEY, AGENT_NETWORK_DESIGNER_ID, AGENT_NETWORK_HOCON, AGENT_NETWORK_NAME_KEY, GRACE_PERIOD_MS, SHOW_TOUR_DELAY_MS, TRIGGER_APP_TOUR_EVENT_NAME, } from "./const.js";
31
+ import { AGENT_NETWORK_DEFINITION_KEY, AGENT_NETWORK_DESIGNER_ID, AGENT_NETWORK_HOCON, AGENT_NETWORK_NAME_KEY, EXPIRED_NETWORKS_CHECK_INTERVAL_MS, GRACE_PERIOD_MS, SHOW_TOUR_DELAY_MS, TRIGGER_APP_TOUR_EVENT_NAME, } from "./const.js";
32
32
  import { Sidebar } from "./Sidebar/Sidebar.js";
33
- import { extractTemporaryNetworksFromMessage, isTemporaryNetwork, mergeNetworks } from "./TemporaryNetworks.js";
34
- import { getAgentFunction, getAgentNetworks, getConnectivity, sendNetworkDesignerUpdate, } from "../../controller/agent/Agent.js";
33
+ import { extractTemporaryNetworksFromMessage, isTemporaryNetwork, notifySaveError, streamNetworkDesignerUpsert, } from "./TemporaryNetworks.js";
34
+ import { MAIN_TOUR_STEPS } from "./Tour/MainTourSteps.js";
35
+ import { getAgentFunction, getAgentNetworks, getConnectivity } from "../../controller/agent/Agent.js";
35
36
  import { getAgentIconSuggestions, getNetworkIconSuggestions } from "../../controller/agent/IconSuggestions.js";
36
37
  import { useAgentChatHistoryStore } from "../../state/ChatHistory.js";
37
- import { useSettingsStore } from "../../state/Settings.js";
38
38
  import { useTempNetworksStore } from "../../state/TemporaryNetworks.js";
39
39
  import { TourPromptState, useTourStore } from "../../state/Tour.js";
40
+ import { toDisplayName } from "../../utils/AgentName.js";
40
41
  import { getZIndex } from "../../utils/zIndexLayers.js";
41
42
  import { ChatCommon } from "../AgentChat/ChatCommon/ChatCommon.js";
42
43
  import { LlmChatButton } from "../AgentChat/Common/LlmChatButton.js";
43
44
  import { isLegacyAgentType } from "../AgentChat/Common/Types.js";
44
- import { chatMessageFromChunk, cleanUpAgentName, removeTrailingUuid } from "../AgentChat/Common/Utils.js";
45
+ import { chatMessageFromChunk } from "../AgentChat/Common/Utils.js";
45
46
  import { ConfirmationModal, StyledButton } from "../Common/ConfirmationModal.js";
46
- import { closeNotification, NotificationType, sendNotification } from "../Common/notification.js";
47
- import { MAIN_TOUR_STEPS } from "./Tour/MainTourSteps.js";
47
+ import { MUIAlert } from "../Common/MUIAlert.js";
48
48
  import { MUIDialog } from "../Common/MUIDialog.js";
49
- // Check for expired networks every this many milliseconds
50
- const EXPIRED_NETWORKS_CHECK_INTERVAL_MS = 10 * 1000;
49
+ import { closeNotification, NotificationType, sendNotification } from "../Common/notification.js";
50
+ import { BYOK } from "./Schema/SlyData.js";
51
+ import { getApiKey, LLM_PROVIDER_API_KEY_FIELD, useSettingsStore } from "../../state/Settings.js";
51
52
  // Animation time for the left and right panels to slide in or out when launching the animation
52
53
  const GROW_ANIMATION_TIME_MS = 800;
53
54
  // Optimization to avoid creating a new empty map on every render
54
55
  const EMPTY_THOUGHT_BUBBLE_EDGES = new Map();
55
- // #region: Agent-save helpers
56
- /**
57
- * Extracts TemporaryNetworks from a single streamed chunk, merging into `accumulated`.
58
- * Returns `accumulated` unchanged if the chunk yields no reservations or on parse error.
59
- */
60
- const collectNetworksFromChunk = (chunk, updated, accumulated) => {
61
- try {
62
- const chatMessage = chatMessageFromChunk(chunk);
63
- if (!chatMessage)
64
- return accumulated;
65
- // Always use the user's edited definition as the authoritative value.
66
- const converted = extractTemporaryNetworksFromMessage(chatMessage, updated);
67
- if (converted.length === 0)
68
- return accumulated;
69
- return mergeNetworks(accumulated, converted);
70
- }
71
- catch (e) {
72
- console.warn("Failed to process chunk from network designer:", e);
73
- return accumulated;
74
- }
75
- };
76
- /** Logs and notifies about a save error. Suppresses AbortError (user-canceled). */
77
- const notifySaveError = (agentName, e) => {
78
- if (e instanceof DOMException && e.name === "AbortError")
79
- return;
80
- console.error("Failed to submit agent network update:", e);
81
- const detail = e instanceof DOMException && e.name === "TimeoutError"
82
- ? "The request timed out waiting for the server. Please try again."
83
- : String(e);
84
- sendNotification(NotificationType.error, `Failed to update network "${agentName}".`, detail);
85
- };
86
- // #endregion: Agent-save helpers
87
56
  /**
88
57
  * Main Multi-Agent Accelerator component that contains the sidebar, agent flow, and chat components.
89
58
  * @param backendNeuroSanApiUrl Initial URL of the backend Neuro-San API. User can change this in the UI.
@@ -116,8 +85,9 @@ export const MultiAgentAccelerator = ({ defaultNeuroSanUrl, username, }) => {
116
85
  const [agentIconSuggestions, setAgentIconSuggestions] = useState(null);
117
86
  const [selectedNetwork, setSelectedNetwork] = useState(null);
118
87
  const [networkDescription, setNetworkDescription] = useState("");
119
- const networkDisplayName = useMemo(() => (useNativeNames ? selectedNetwork : cleanUpAgentName(removeTrailingUuid(selectedNetwork))), [selectedNetwork, useNativeNames]);
120
- const [providerKeysRequired, setProviderKeysRequired] = useState(new Set());
88
+ const networkDisplayName = useMemo(() => toDisplayName(selectedNetwork, useNativeNames), [selectedNetwork, useNativeNames]);
89
+ // LLM providers for which BYOK keys are supported by the current network
90
+ const [supportedByokProviders, setSupportedByokProviders] = useState(new Set());
121
91
  const neuroSanURL = useSettingsStore((state) => state.settings.externalServices.neuroSanUrl) ?? defaultNeuroSanUrl;
122
92
  // Tracks how many times each agent has been involved in the conversation
123
93
  const [agentCounts, setAgentCounts] = useState(new Map());
@@ -191,9 +161,8 @@ export const MultiAgentAccelerator = ({ defaultNeuroSanUrl, username, }) => {
191
161
  // progresses. We read it back here to find the matching temp network and override agent_network_definition in the
192
162
  // outgoing request — leaving what's in IndexedDB untouched.
193
163
  const designerSlyData = useAgentChatHistoryStore((state) => state.history[AGENT_NETWORK_DESIGNER_ID]?.slyData);
194
- const designerNetworkName = isNetworkDesignerMode
195
- ? designerSlyData?.[AGENT_NETWORK_NAME_KEY]
196
- : undefined;
164
+ const designerNetworkNameValue = designerSlyData?.[AGENT_NETWORK_NAME_KEY];
165
+ const designerNetworkName = isNetworkDesignerMode && typeof designerNetworkNameValue === "string" ? designerNetworkNameValue : undefined;
197
166
  const designerTempNetwork = designerNetworkName
198
167
  ? temporaryNetworks.find((n) => n.agentNetworkName === designerNetworkName)
199
168
  : undefined;
@@ -201,16 +170,14 @@ export const MultiAgentAccelerator = ({ defaultNeuroSanUrl, username, }) => {
201
170
  * Builds the API keys object to be sent as extraSlyData with each request, based on the LLM providers required
202
171
  * by the currently selected network. Only includes keys for providers that are required.
203
172
  */
204
- const getApiKeys = () => {
205
- const llmConfig = {};
206
- if (providerKeysRequired.has("OpenAI")) {
207
- llmConfig["openai_api_key"] = apiKeys["OpenAI"];
208
- }
209
- if (providerKeysRequired.has("Anthropic")) {
210
- llmConfig["anthropic_api_key"] = apiKeys["Anthropic"];
211
- }
212
- return { llm_config: llmConfig };
213
- };
173
+ const getApiKeys = () => ({
174
+ llm_config: Object.fromEntries([...supportedByokProviders]
175
+ .map((provider) => [
176
+ LLM_PROVIDER_API_KEY_FIELD[provider],
177
+ getApiKey(apiKeys, provider),
178
+ ])
179
+ .filter((entry) => Boolean(entry[1]))),
180
+ });
214
181
  /**
215
182
  * Builds the extraSlyData object to be sent with each request, including information for Agent Network Designer
216
183
  * and (if required) API keys for LLM providers.
@@ -237,7 +204,7 @@ export const MultiAgentAccelerator = ({ defaultNeuroSanUrl, username, }) => {
237
204
  return undefined;
238
205
  };
239
206
  // Whether any API keys are required
240
- const anyApiKeysRequired = providerKeysRequired.size > 0;
207
+ const anyApiKeysRequired = supportedByokProviders.size > 0;
241
208
  // Build base extraSlyData
242
209
  const baseExtraSlyData = buildExtraSlyData();
243
210
  // Add API keys to extraSlyData if needed, merging with baseExtraSlyData if it exists
@@ -252,7 +219,33 @@ export const MultiAgentAccelerator = ({ defaultNeuroSanUrl, username, }) => {
252
219
  chatRef.current?.handleStop();
253
220
  resetState();
254
221
  }, [resetState]);
255
- const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
222
+ /**
223
+ * Maps an API key name to the corresponding LLM provider
224
+ */
225
+ const apiKeyToProvider = useCallback((key) => {
226
+ switch (key) {
227
+ case "openai_api_key":
228
+ return "OpenAI";
229
+ case "anthropic_api_key":
230
+ return "Anthropic";
231
+ default:
232
+ return null;
233
+ }
234
+ }, []);
235
+ /**
236
+ * Given a SlyData schema, returns the set of LLM providers that are supported by the agent network.
237
+ * @note Semantics are "oneOf": a key must be supplied for at least one of the named providers
238
+ */
239
+ const getSupportedLlmProviders = useCallback((schema) => {
240
+ const parsed = BYOK.safeParse(schema);
241
+ if (!parsed.success || !parsed.data.required?.includes("llm_config")) {
242
+ return new Set();
243
+ }
244
+ const providerKeys = Object.keys(parsed.data.properties?.llm_config?.properties ?? {});
245
+ return new Set(providerKeys
246
+ .map((key) => apiKeyToProvider(key))
247
+ .filter((provider) => provider !== null));
248
+ }, [apiKeyToProvider]);
256
249
  useEffect(() => {
257
250
  ;
258
251
  (async () => {
@@ -273,23 +266,14 @@ export const MultiAgentAccelerator = ({ defaultNeuroSanUrl, username, }) => {
273
266
  const fetchAgentDetails = async () => {
274
267
  // It is a Neuro-san agent, so get the function and connectivity info
275
268
  try {
269
+ // Reset before fetching
270
+ setNetworkDescription("");
271
+ setSupportedByokProviders(new Set());
272
+ // Get agent function from server
276
273
  const agentFunction = await getAgentFunction(neuroSanURL, selectedNetwork, username);
277
274
  setNetworkDescription(agentFunction?.function?.description || "");
278
275
  const schema = agentFunction?.function?.sly_data_schema;
279
- const schemaProperties = isRecord(schema) ? schema["properties"] : undefined;
280
- const llmConfig = isRecord(schemaProperties) ? schemaProperties["llm_config"] : undefined;
281
- const llmConfigRequired = isRecord(llmConfig) ? llmConfig["required"] : undefined;
282
- setProviderKeysRequired(new Set((Array.isArray(llmConfigRequired) ? llmConfigRequired : []).flatMap((key) => {
283
- if (key === "openai_api_key") {
284
- return ["OpenAI"];
285
- }
286
- if (key === "anthropic_api_key") {
287
- return ["Anthropic"];
288
- }
289
- console.warn(`Unknown API key requirement "${key}" for network ${selectedNetwork}. Skipping.`);
290
- // Will get dropped by flatMap
291
- return [];
292
- })));
276
+ setSupportedByokProviders(getSupportedLlmProviders(schema));
293
277
  }
294
278
  catch (e) {
295
279
  console.warn(`Unable to get agent details for network ${selectedNetwork}:`, e);
@@ -300,7 +284,7 @@ export const MultiAgentAccelerator = ({ defaultNeuroSanUrl, username, }) => {
300
284
  if (selectedNetwork && !isLegacyAgentType(selectedNetwork)) {
301
285
  void fetchAgentDetails();
302
286
  }
303
- }, [neuroSanURL, selectedNetwork, username]);
287
+ }, [getSupportedLlmProviders, neuroSanURL, selectedNetwork, username]);
304
288
  useEffect(() => {
305
289
  ;
306
290
  (async () => {
@@ -466,10 +450,7 @@ export const MultiAgentAccelerator = ({ defaultNeuroSanUrl, username, }) => {
466
450
  */
467
451
  const onSaveAgent = useCallback(async (agentName, updated, agentNetworkName, signal) => {
468
452
  try {
469
- let newNetworks = [];
470
- await sendNetworkDesignerUpdate(neuroSanURL, signal, agentName, updated, agentNetworkName, username, (chunk) => {
471
- newNetworks = collectNetworksFromChunk(chunk, updated, newNetworks);
472
- });
453
+ const newNetworks = await streamNetworkDesignerUpsert(neuroSanURL, signal, agentName, updated, agentNetworkName, username);
473
454
  if (newNetworks.length === 0) {
474
455
  sendNotification(NotificationType.error, `Failed to update network "${agentName}".`, "The network designer did not return a reservation. Please try again.");
475
456
  return;
@@ -511,7 +492,7 @@ export const MultiAgentAccelerator = ({ defaultNeuroSanUrl, username, }) => {
511
492
  }, [resetState]);
512
493
  const handleDeleteNetwork = (networkId, isExpired) => {
513
494
  if (isExpired) {
514
- // It's expired so just delete it without confirmation
495
+ // It's expired, so just delete it without confirmation
515
496
  const tempNetworksWithoutThisOne = temporaryNetworks.filter((network) => network.agentInfo.agent_name !== networkId);
516
497
  useTempNetworksStore.getState().setTempNetworks(tempNetworksWithoutThisOne);
517
498
  useAgentChatHistoryStore.getState().resetHistory(networkId);
@@ -555,8 +536,11 @@ export const MultiAgentAccelerator = ({ defaultNeuroSanUrl, username, }) => {
555
536
  }
556
537
  }, [tourRequested, selectedNetwork, agentsInNetwork, networks, controls, setTourStatus]);
557
538
  const getMissingApiKeys = () => {
558
- return providerKeysRequired.size > 0 ? [...providerKeysRequired].filter((provider) => !apiKeys?.[provider]) : [];
539
+ // Calculate intersection of what we have (apiKeys) and what is required (providerKeysRequired)
540
+ const intersection = new Set([...supportedByokProviders].filter((provider) => getApiKey(apiKeys, provider)));
541
+ return intersection.size === 0 ? supportedByokProviders : new Set();
559
542
  };
543
+ const hasMissingApiKeys = getMissingApiKeys().size > 0;
560
544
  const getLeftPanel = () => {
561
545
  return (_jsx(Slide, { id: "multi-agent-accelerator-grid-sidebar-slide", in: !enableZenMode || !isAwaitingLlm, direction: "right", timeout: GROW_ANIMATION_TIME_MS, onExited: () => {
562
546
  setIsStreaming(true);
@@ -586,7 +570,7 @@ export const MultiAgentAccelerator = ({ defaultNeuroSanUrl, username, }) => {
586
570
  [AGENT_NETWORK_DESIGNER_ID]: "Describe in plain language the network you would like to build.",
587
571
  }, currentUser: username, customAgentGreetings: {
588
572
  [AGENT_NETWORK_DESIGNER_ID]: "Let's build a network together!",
589
- }, extraSlyData: extraSlyData, id: "agent-network-ui", isAwaitingLlm: isAwaitingLlm, missingApiKeys: getMissingApiKeys(), networkDescription: networkDescription, neuroSanURL: neuroSanURL, onChunkReceived: onChunkReceived, onStreamingComplete: onStreamingComplete, onStreamingStarted: onStreamingStarted, ref: chatRef, sampleQueries: sampleQueries, setIsAwaitingLlm: setIsAwaitingLlm, selectedNetwork: selectedNetwork, setSelectedNetwork: changeSelectedNetwork }, selectedNetwork ?? "no-network") }) }));
573
+ }, extraSlyData: extraSlyData, id: "agent-network-ui", isAwaitingLlm: isAwaitingLlm, hasMissingApiKeys: hasMissingApiKeys, networkDescription: networkDescription, neuroSanURL: neuroSanURL, onChunkReceived: onChunkReceived, onStreamingComplete: onStreamingComplete, onStreamingStarted: onStreamingStarted, ref: chatRef, sampleQueries: sampleQueries, setIsAwaitingLlm: setIsAwaitingLlm, selectedNetwork: selectedNetwork, setSelectedNetwork: changeSelectedNetwork }, selectedNetwork ?? "no-network") }) }));
590
574
  };
591
575
  const getStopButton = () => {
592
576
  return (_jsx(_Fragment, { children: isAwaitingLlm && enableZenMode && (_jsx(Box, { id: "stop-button-container", sx: {
@@ -600,7 +584,7 @@ export const MultiAgentAccelerator = ({ defaultNeuroSanUrl, username, }) => {
600
584
  },
601
585
  }, children: _jsx(StopCircle, { fontSize: "large", id: "stop-button-icon" }) }) }) })) }));
602
586
  };
603
- const getDeleteNetworkConfirmationModal = () => confirmationModalOpen ? (_jsx(ConfirmationModal, { id: "delete-network-confirmation-modal", content: `The network "${cleanUpAgentName(removeTrailingUuid(networkToBeDeleted))}" will be deleted. ` +
587
+ const getDeleteNetworkConfirmationModal = () => confirmationModalOpen ? (_jsx(ConfirmationModal, { id: "delete-network-confirmation-modal", content: `The network "${toDisplayName(networkToBeDeleted)}" will be deleted. ` +
604
588
  "This action cannot be undone. Are you sure you want to proceed?", handleCancel: () => {
605
589
  setConfirmationModalOpen(false);
606
590
  setNetworkToBeDeleted(null);
@@ -656,7 +640,14 @@ export const MultiAgentAccelerator = ({ defaultNeuroSanUrl, username, }) => {
656
640
  opacity: "95%",
657
641
  width: "100%",
658
642
  }, children: [_jsx(Typography, { variant: "h6", sx: { padding: "0.5rem 1rem", fontWeight: "bold", color: "white" }, children: "Network Preview" }), agentsInNetworkDesigner?.length > 0 ? (_jsx(AgentFlow, { id: "and-network-preview", agentsInNetwork: agentsInNetworkDesigner, isAgentNetworkDesignerMode: true, isAwaitingLlm: false, isStreaming: false, thoughtBubbleEdges: EMPTY_THOUGHT_BUBBLE_EDGES }, "and-network-preview")) : (_jsx(Typography, { variant: "body1", sx: { color: "white" }, children: "Awaiting status from Agent Network Designer..." }))] }) }) }));
659
- return (_jsxs(_Fragment, { children: [Tour, getTourModal(), getProgressPopper(), getDeleteNetworkConfirmationModal(), _jsxs(Grid, { id: "multi-agent-accelerator-grid", container: true, columns: 18, sx: {
643
+ const getMissingApiKeysAlert = () => hasMissingApiKeys && (_jsx(MUIAlert, { closeable: false, id: "llm-chat-missing-api-keys-alert", severity: "warning", sx: {
644
+ my: 1.5,
645
+ "& .MuiAlert-message": {
646
+ width: "100%",
647
+ textAlign: "center",
648
+ },
649
+ }, children: _jsxs(Typography, { sx: { fontSize: "larger" }, children: ["API key(s) required for at least one of these providers:", " ", _jsx("strong", { children: [...getMissingApiKeys()].join(", ") }), ". Please add the required key(s) in \"Settings\" to use this Network."] }) }));
650
+ return (_jsxs(_Fragment, { children: [getMissingApiKeysAlert(), Tour, getTourModal(), getProgressPopper(), getDeleteNetworkConfirmationModal(), _jsxs(Grid, { id: "multi-agent-accelerator-grid", container: true, columns: 18, sx: {
660
651
  display: "flex",
661
652
  flex: 1,
662
653
  height: "85%",
@@ -0,0 +1,17 @@
1
+ import * as z from "zod";
2
+ /**
3
+ * Zod schema for sly_data BYOK structure
4
+ */
5
+ export declare const BYOK: z.ZodObject<{
6
+ type: z.ZodOptional<z.ZodString>;
7
+ required: z.ZodOptional<z.ZodTuple<[z.ZodLiteral<"llm_config">], null>>;
8
+ properties: z.ZodOptional<z.ZodObject<{
9
+ llm_config: z.ZodOptional<z.ZodObject<{
10
+ type: z.ZodOptional<z.ZodString>;
11
+ properties: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
12
+ type: z.ZodOptional<z.ZodString>;
13
+ description: z.ZodOptional<z.ZodString>;
14
+ }, z.core.$loose>>>;
15
+ }, z.core.$loose>>;
16
+ }, z.core.$loose>>;
17
+ }, z.core.$loose>;
@@ -0,0 +1,23 @@
1
+ import * as z from "zod";
2
+ /**
3
+ * Zod schema for sly_data BYOK structure
4
+ */
5
+ export const BYOK = z.looseObject({
6
+ type: z.string().optional(),
7
+ required: z.tuple([z.literal("llm_config")]).optional(),
8
+ properties: z
9
+ .looseObject({
10
+ llm_config: z
11
+ .looseObject({
12
+ type: z.string().optional(),
13
+ properties: z
14
+ .record(z.string(), z.looseObject({
15
+ type: z.string().optional(),
16
+ description: z.string().optional(),
17
+ }))
18
+ .optional(),
19
+ })
20
+ .optional(),
21
+ })
22
+ .optional(),
23
+ });
@@ -1,5 +1,7 @@
1
1
  import { TreeItemProps } from "@mui/x-tree-view/TreeItem";
2
2
  import { FC } from "react";
3
+ export declare const EDIT_NETWORK_LABEL = "Edit this network";
4
+ export declare const DOWNLOAD_NETWORK_DEFINITION_LABEL = "Download network definition";
3
5
  export interface AgentNetworkNodeProps extends TreeItemProps {
4
6
  readonly onDeleteNetwork?: (network: string, isExpired: boolean) => void;
5
7
  readonly onEditNetwork?: (network: string) => void;
@@ -9,13 +9,18 @@ import Edit from "@mui/icons-material/Edit";
9
9
  import Box from "@mui/material/Box";
10
10
  import Chip from "@mui/material/Chip";
11
11
  import IconButton from "@mui/material/IconButton";
12
+ import { styled } from "@mui/material/styles";
12
13
  import Tooltip from "@mui/material/Tooltip";
13
14
  import { useTreeItemModel } from "@mui/x-tree-view/hooks";
14
15
  import { TreeItemContent, TreeItemGroupTransition, TreeItemLabel, TreeItemRoot, } from "@mui/x-tree-view/TreeItem";
15
16
  import { TreeItemProvider } from "@mui/x-tree-view/TreeItemProvider";
16
17
  import { useTreeItem } from "@mui/x-tree-view/useTreeItem";
17
18
  import { useRef } from "react";
18
- import { downloadFile, toSafeFilename } from "../../../utils/File.js";
19
+ import { downloadFile } from "../../../utils/File.js";
20
+ //#region Constants
21
+ // Accessible names shared between each action's tooltip title and its aria-label.
22
+ export const EDIT_NETWORK_LABEL = "Edit this network";
23
+ export const DOWNLOAD_NETWORK_DEFINITION_LABEL = "Download network definition";
19
24
  // Palette of colors we can use for tags
20
25
  const TAG_COLORS = [
21
26
  "--bs-accent2-light",
@@ -28,6 +33,17 @@ const TAG_COLORS = [
28
33
  "--bs-secondary",
29
34
  "--bs-yellow",
30
35
  ];
36
+ // Shared styling for the row's action buttons (download, edit, ...). Disabled state keeps the base
37
+ // color and only dims via opacity.
38
+ const ActionIconButton = styled(IconButton)({
39
+ padding: 0,
40
+ color: "var(--bs-secondary)",
41
+ "&:hover": { color: "var(--bs-secondary-dark)" },
42
+ "&.Mui-disabled": {
43
+ opacity: 0.3,
44
+ },
45
+ });
46
+ //#endregion Types and Interfaces
31
47
  // Keep track of which tags have which colors so that the same tag always has the same color
32
48
  const tagsToColors = new Map();
33
49
  /**
@@ -45,8 +61,6 @@ const isTemporaryNetworkExpired = (expirationDate) => {
45
61
  */
46
62
  export const AgentNetworkTreeItem = ({ children, disabled, itemId, label, onDeleteNetwork, onEditNetwork, }) => {
47
63
  const item = useTreeItemModel(itemId);
48
- // We know all labels are strings because we set them that way in the tree view items
49
- const labelString = label;
50
64
  const { getContextProviderProps, getRootProps, getContentProps, getLabelProps, getGroupTransitionProps } = useTreeItem({ itemId, children, label, disabled });
51
65
  const rootRef = useRef(null);
52
66
  const isParent = Array.isArray(children) && children.length > 0;
@@ -105,37 +119,20 @@ export const AgentNetworkTreeItem = ({ children, disabled, itemId, label, onDele
105
119
  .map((tag) => (_jsx(Chip, { label: tag, style: {
106
120
  margin: "0.25rem",
107
121
  backgroundColor: `var(${tagsToColors.get(tag) || TAG_COLORS[0]})`,
108
- } }, tag))), placement: "right", arrow: true, children: _jsx(BookmarkIcon, { sx: { fontSize: "0.75rem", color: "var(--bs-accent1-medium)" } }) })) : null] }), isChild && isTemporaryNetwork && (_jsxs(Box, { sx: { display: "flex", alignItems: "center", gap: "0.25rem", marginLeft: "auto" }, children: [networkHocon && (_jsx(Tooltip, { title: isExpired ? "Expired" : "Download network definition", children: _jsx("span", { children: _jsx(IconButton, { onClick: (e) => {
122
+ } }, tag))), placement: "right", arrow: true, children: _jsx(BookmarkIcon, { sx: { fontSize: "0.75rem", color: "var(--bs-accent1-medium)" } }) })) : null] }), isChild && isTemporaryNetwork && (_jsxs(Box, { sx: { display: "flex", alignItems: "center", gap: "0.25rem", marginLeft: "auto" }, children: [networkHocon && (_jsx(Tooltip, { title: isExpired ? "Expired" : DOWNLOAD_NETWORK_DEFINITION_LABEL, children: _jsx("span", { children: _jsx(ActionIconButton, { onClick: (e) => {
123
+ // The button is disabled while expired, so no need to guard here.
109
124
  e.stopPropagation();
110
- const fileName = `${toSafeFilename(labelString)}.hocon`;
111
- downloadFile(networkHocon, fileName);
112
- }, disabled: isExpired, "aria-label": "Download network definition", size: "small", sx: {
113
- padding: 0,
114
- color: "var(--bs-secondary)",
115
- "&:hover": { color: "var(--bs-secondary-dark)" },
116
- "&.Mui-disabled": {
117
- color: "var(--bs-secondary)",
118
- opacity: 0.3,
119
- },
120
- }, children: _jsx(DownloadIcon, { sx: { fontSize: "0.75rem" } }) }) }) })), _jsx(Tooltip, { title: "Edit this network", children: _jsx("span", { children: _jsx(IconButton, { onClick: (e) => {
125
+ downloadFile(networkHocon, item.downloadFileName, "text/plain");
126
+ }, disabled: isExpired, "aria-label": DOWNLOAD_NETWORK_DEFINITION_LABEL, size: "small", children: _jsx(DownloadIcon, { sx: { fontSize: "0.75rem" } }) }) }) })), _jsx(Tooltip, { title: EDIT_NETWORK_LABEL, children: _jsx("span", { children: _jsx(ActionIconButton, { onClick: (e) => {
121
127
  e.stopPropagation();
122
128
  onEditNetwork?.(itemId);
123
- }, disabled: isExpired, size: "small", sx: {
124
- padding: 0,
125
- color: "var(--bs-secondary)",
126
- "&:hover": { color: "var(--bs-secondary-dark)" },
127
- "&.Mui-disabled": {
128
- color: "var(--bs-secondary)",
129
- opacity: 0.3,
130
- },
131
- }, children: _jsx(Edit, { sx: { fontSize: "0.75rem" } }) }) }) }), _jsx(Tooltip, { title: "Delete network", children: _jsx(Delete, { onClick: (e) => {
129
+ }, disabled: isExpired, "aria-label": EDIT_NETWORK_LABEL, size: "small", children: _jsx(Edit, { sx: { fontSize: "0.75rem" } }) }) }) }), _jsx(Tooltip, { title: "Delete network", children: _jsx(Delete, { onClick: (e) => {
132
130
  e.stopPropagation();
133
131
  onDeleteNetwork?.(itemId, isExpired);
134
132
  }, sx: {
135
133
  color: "var(--bs-secondary)",
136
134
  "&:hover": { color: (theme) => theme.palette.warning.main },
137
135
  "&.Mui-disabled": {
138
- color: "var(--bs-secondary)",
139
136
  opacity: 0.3,
140
137
  },
141
138
  cursor: "pointer",
@@ -120,26 +120,17 @@ export const Sidebar = ({ id, isAwaitingLlm, networkIconSuggestions, networks, n
120
120
  const [selectedItem, setSelectedItem] = useState(null);
121
121
  // Display option for agent/network names
122
122
  const useNativeNames = useSettingsStore((state) => state.settings.appearance.useNativeNames);
123
- const [neuroSanServerStatus, setNeuroSanServerStatus] = useState({ onlineStatus: "unknown", version: null });
123
+ const [neuroSanServerStatus, setNeuroSanServerStatus] = useState(null);
124
124
  useEffect(() => {
125
+ // Track whether the current request is still valid. If the component unmounts or the URL changes,
126
+ // we want to ignore any results from previous requests.
125
127
  let isCurrentRequest = true;
126
128
  const checkStatus = async () => {
127
- try {
128
- const result = await testConnection(neuroSanServerURL);
129
- if (!isCurrentRequest) {
130
- return;
131
- }
132
- setNeuroSanServerStatus(result.success
133
- ? { onlineStatus: "online", version: result.version }
134
- : { error: result.status, httpStatus: result.httpStatus, onlineStatus: "offline", version: null });
135
- }
136
- catch (error) {
137
- if (!isCurrentRequest) {
138
- return;
139
- }
140
- const errorString = error instanceof Error ? error.message : String(error);
141
- setNeuroSanServerStatus({ error: errorString, onlineStatus: "offline", version: null });
129
+ const result = await testConnection(neuroSanServerURL);
130
+ if (!isCurrentRequest) {
131
+ return;
142
132
  }
133
+ setNeuroSanServerStatus(result);
143
134
  };
144
135
  void checkStatus();
145
136
  const intervalId = setInterval(checkStatus, NEURO_SAN_PING_INTERVAL_MS);
@@ -207,25 +198,28 @@ export const Sidebar = ({ id, isAwaitingLlm, networkIconSuggestions, networks, n
207
198
  };
208
199
  }, [newlyAddedTemporaryNetworks]);
209
200
  const toStatusColor = () => {
210
- switch (neuroSanServerStatus.onlineStatus) {
211
- case "online":
201
+ switch (neuroSanServerStatus?.success) {
202
+ case true:
212
203
  return "green";
213
- case "offline":
204
+ case false:
214
205
  return "red";
215
- case "unknown":
206
+ case undefined:
216
207
  default:
217
208
  return "unknown";
218
209
  }
219
210
  };
211
+ const mapStatus = (statusText) => (statusText === "ok" ? "online" : (statusText ?? "unknown"));
220
212
  const getStatusLight = () => (_jsx(ServerStatusTooltip, { slotProps: {
221
213
  popper: {
222
214
  "aria-label": "Neuro-san server status",
223
215
  },
224
- }, title: _jsxs(Box, { sx: { display: "flex", flexDirection: "column", gap: 0.5 }, children: [_jsxs(Typography, { variant: "body2", children: [_jsx("strong", { children: "Status:" }), " ", neuroSanServerStatus.onlineStatus] }), _jsxs(Typography, { variant: "body2", children: [_jsx("strong", { children: "Version:" }), " ", neuroSanServerStatus.version ?? "unknown"] }), _jsxs(Typography, { variant: "body2", children: [_jsx("strong", { children: "URL:" }), " ", neuroSanServerURL || "unknown"] }), neuroSanServerStatus.error && (_jsxs(_Fragment, { children: [_jsxs(Typography, { variant: "body2", children: [_jsx("strong", { children: "Error:" }), " ", neuroSanServerStatus.error] }), neuroSanServerStatus.httpStatus && (_jsxs(Typography, { variant: "body2", children: [_jsx("strong", { children: "HTTP status:" }), " ", `${neuroSanServerStatus.httpStatus} (${httpStatus[neuroSanServerStatus.httpStatus] ?? "Unknown status"})`] }))] }))] }), placement: "right", children: _jsx("span", { children: _jsx(StatusLight, { id: `${id}-agent-network-status-light`, statusValue: toStatusColor() }) }) }));
216
+ }, title: _jsxs(Box, { sx: { display: "flex", flexDirection: "column", gap: 0.5 }, children: [_jsxs(Typography, { variant: "body2", children: [_jsx("strong", { children: "Status:" }), " ", mapStatus(neuroSanServerStatus?.statusText)] }), _jsxs(Typography, { variant: "body2", children: [_jsx("strong", { children: "URL:" }), " ", neuroSanServerURL || "unknown"] }), _jsxs(Typography, { variant: "body2", children: [_jsx("strong", { children: "Service:" }), " ", neuroSanServerStatus?.healthCheckResponse?.service || "unknown"] }), _jsx(Typography, { sx: { fontWeight: "bold" }, variant: "body2", children: "Versions:" }), _jsxs(Box, { sx: { ml: 1 }, children: [_jsxs(Typography, { variant: "body2", children: [_jsx("strong", { children: "Neuro SAN:" }), " ", neuroSanServerStatus?.healthCheckResponse?.versions?.["neuro-san"] ?? "unknown"] }), _jsxs(Typography, { variant: "body2", children: [_jsx("strong", { children: "Neuro SAN Studio:" }), " ", neuroSanServerStatus?.healthCheckResponse?.versions?.["neuro-san-studio"] ?? "unknown"] })] }), !neuroSanServerStatus?.success && neuroSanServerStatus?.httpStatus && (_jsx(_Fragment, { children: _jsxs(Typography, { variant: "body2", children: [_jsx("strong", { children: "HTTP status:" }), " ", `${neuroSanServerStatus.httpStatus} (${httpStatus[neuroSanServerStatus.httpStatus] ?? "Unknown status"})`] }) }))] }), placement: "right", children: _jsx("span", { children: _jsx(StatusLight, { id: `${id}-agent-network-status-light`, statusValue: toStatusColor() }) }) }));
225
217
  const getAddNetworkButton = () => (_jsx(Box, { sx: { display: "flex" }, children: _jsx(Button, { "aria-label": "Add New Network", disabled: isAwaitingLlm, id: "add-network-btn", onClick: () => {
226
218
  setSelectedItem(AGENT_NETWORK_DESIGNER_ID);
227
219
  setSelectedNetwork(AGENT_NETWORK_DESIGNER_ID);
228
- }, sx: { display: "inline-block", minWidth: "40px" }, children: _jsx(Tooltip, { title: "Create your own agent network", placement: "top", children: _jsx(AddBoxRounded, { id: "add-network-icon", sx: { color: "var(--bs-secondary)" } }) }) }) }));
220
+ }, sx: { display: "inline-block", minWidth: "40px" }, children: _jsx(Tooltip, { title: "Create your own agent network", placement: "top", children: _jsx(AddBoxRounded, { id: "add-network-icon", sx: (theme) => ({
221
+ color: isAwaitingLlm ? theme.palette.action.disabled : "var(--bs-secondary)",
222
+ }) }) }) }) }));
229
223
  const getSidebarHeading = () => (_jsxs(SidebarHeading, { id: `${id}-heading`, children: [_jsxs(Box, { sx: { display: "flex", alignItems: "center", gap: (theme) => theme.spacing(1.5) }, children: [getStatusLight(), "Agent Networks"] }), getAddNetworkButton()] }));
230
224
  const getSidebarNetworksTree = () => (_jsx(RichTreeView, { disableSelection: isAwaitingLlm, expandedItems: expandedItems, items: treeViewItems, multiSelect: false, onExpandedItemsChange: (_event, itemIds) => setExpandedItems(itemIds), onSelectedItemsChange: handleSelectedItemsChange, selectedItems: selectedItem, slotProps: {
231
225
  item: { onDeleteNetwork, onEditNetwork: handleEditNetworkWithSelect },
@@ -8,6 +8,7 @@ import { TemporaryNetwork } from "../../../state/TemporaryNetworks.js";
8
8
  export interface AgentNetworkTreeItemModel extends Omit<TreeViewDefaultItemModelProperties, "children"> {
9
9
  readonly children?: readonly AgentNetworkTreeItemModel[];
10
10
  readonly displayName: string;
11
+ readonly downloadFileName?: string;
11
12
  readonly iconSuggestion?: string;
12
13
  readonly isNetwork: boolean;
13
14
  readonly tags?: readonly string[];
@@ -1,5 +1,6 @@
1
- import { cleanUpAgentName, removeTrailingUuid } from "../../AgentChat/Common/Utils.js";
2
- //#endregion
1
+ import { cleanUpAgentName, removeTrailingUuid, toDisplayName } from "../../../utils/AgentName.js";
2
+ import { toSafeFilename } from "../../../utils/File.js";
3
+ //#endregion Types and Interfaces
3
4
  /**
4
5
  * Recursively searches for a tree item with the specified ID within the given list of tree items.
5
6
  * @param items - The list of tree items to search through
@@ -21,13 +22,30 @@ export const findTreeItemById = (items, itemId) => {
21
22
  return undefined;
22
23
  };
23
24
  /**
24
- * Converts a raw agent name into a display name for the tree view, respecting the user's preference for native
25
- * vs cleaned names.
26
- * @param itemName - The raw agent name from the API
25
+ * Computes the display name for a network (leaf) node.
26
+ * @param label - The label to use for the tree item (usually derived from the agent name)
27
27
  * @param useNativeNames - Whether to use native names or cleaned-up names for display
28
+ * @param displayNameOverride - For temporary networks, `label` is the raw reservation_id, which is canonicalized and
29
+ * may have lost the word separators (_/-) that cleanUpAgentName relies on. When provided (the temp network's
30
+ * agentNetworkName), it is used as the basis for the cleaned name instead of the path part.
28
31
  * @returns The display name to show in the tree view
29
32
  */
30
- const toDisplayName = (itemName, useNativeNames) => useNativeNames ? itemName : cleanUpAgentName(removeTrailingUuid(itemName));
33
+ const toLeafDisplayName = (label, useNativeNames, displayNameOverride) => {
34
+ // Native mode shows the raw agent name part, unmodified.
35
+ if (useNativeNames) {
36
+ return label;
37
+ }
38
+ const cleanedName = cleanUpAgentName(displayNameOverride ?? removeTrailingUuid(label));
39
+ return cleanedName;
40
+ };
41
+ /**
42
+ * Computes the filename used when exporting a network's HOCON to disk. The trailing reservation
43
+ * UUID is stripped first so the export carries a clean name (toSafeFilename would otherwise flatten
44
+ * the UUID's hyphens to underscores).
45
+ * @param label - The label to use for the tree item (usually derived from the agent name)
46
+ * @returns The sanitized `.hocon` filename for the exported network HOCON
47
+ */
48
+ const toDownloadFileName = (label) => `${toSafeFilename(removeTrailingUuid(label))}.hocon`;
31
49
  /**
32
50
  * Converts an AgentInfo object into a tree item model representing a network (leaf node).
33
51
  * @param network - The AgentInfo object containing details about the network
@@ -39,7 +57,8 @@ const toDisplayName = (itemName, useNativeNames) => useNativeNames ? itemName :
39
57
  const toNetworkLeaf = (network, label, useNativeNames, metadata = {}) => ({
40
58
  id: network.agent_name,
41
59
  label,
42
- displayName: toDisplayName(label, useNativeNames),
60
+ displayName: toLeafDisplayName(label, useNativeNames, metadata.displayNameOverride),
61
+ downloadFileName: metadata.temporaryNetworkHocon ? toDownloadFileName(label) : undefined,
43
62
  iconSuggestion: metadata.iconSuggestion,
44
63
  isNetwork: true,
45
64
  tags: network.tags,
@@ -158,6 +177,7 @@ export const buildTreeViewItems = (useNativeNames, regularNetworks = [], tempora
158
177
  iconSuggestion: "HourglassTop",
159
178
  temporaryNetworkExpirationTime: new Date(temporaryNetwork.reservation.expiration_time_in_seconds * 1000),
160
179
  temporaryNetworkHocon: temporaryNetwork.networkHocon,
180
+ displayNameOverride: temporaryNetwork.agentNetworkName,
161
181
  });
162
182
  }
163
183
  const treeViewItems = tree.uncategorizedItems.length > 0