@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
@@ -62,3 +62,11 @@ export declare const isTemporaryNetwork: (agentName: string | null, networks: Te
62
62
  * Returns a new array; does not mutate either argument.
63
63
  */
64
64
  export declare const mergeNetworks: (target: TemporaryNetwork[], incoming: TemporaryNetwork[]) => TemporaryNetwork[];
65
+ /** Logs and notifies about a save error. Suppresses AbortError (user-canceled). */
66
+ export declare const notifySaveError: (agentName: string, e: unknown) => void;
67
+ /**
68
+ * Streams a network definition through the network designer to obtain its reservation.
69
+ * Returns the single updated network as a one-element list (empty if the designer returned
70
+ * no reservation).
71
+ */
72
+ export declare const streamNetworkDesignerUpsert: (neuroSanURL: string, signal: AbortSignal, frontman: string, networkDef: AgentNetworkDefinitionEntry[], agentNetworkName: string | undefined, username: string) => Promise<TemporaryNetwork[]>;
@@ -1,6 +1,11 @@
1
+ import { keyBy, mergeWith } from "lodash-es";
1
2
  import { AGENT_NETWORK_DEFINITION_KEY, AGENT_NETWORK_HOCON, AGENT_RESERVATIONS_KEY, DisplayAs, TEMPORARY_NETWORK_FOLDER, } from "./const.js";
3
+ import { sendNetworkDesignerRequest } from "../../controller/agent/Agent.js";
2
4
  import { ChatMessageType } from "../../generated/neuro-san/NeuroSanClient.js";
3
- import { removeTrailingUuid } from "../AgentChat/Common/Utils.js";
5
+ import { removeTrailingUuid } from "../../utils/AgentName.js";
6
+ import { chatMessageFromChunk } from "../AgentChat/Common/Utils.js";
7
+ import { NotificationType, sendNotification } from "../Common/notification.js";
8
+ //#endregion: Types
4
9
  /**
5
10
  * Extracts agent reservations from a chat message, if they exist.
6
11
  * @param message The chat message to extract reservations from. We expect reservations to be present in messages of
@@ -44,7 +49,11 @@ export const extractNetworkHocon = (message) => {
44
49
  * @returns List of TemporaryNetwork objects ready for the store.
45
50
  */
46
51
  export const convertReservationsToNetworks = (agentReservations, networkHocon, agentNetworkDefinition, agentNetworkName) => {
47
- return agentReservations.map((reservation) => ({
52
+ return (agentReservations
53
+ // reservation_id is typed as required but is echoed from un-vetted backend data; skip entries
54
+ // missing it rather than build a "temporary/undefined" network.
55
+ .filter((reservation) => Boolean(reservation?.reservation_id))
56
+ .map((reservation) => ({
48
57
  reservation,
49
58
  agentInfo: {
50
59
  agent_name: `${TEMPORARY_NETWORK_FOLDER}/${reservation.reservation_id}`,
@@ -54,7 +63,7 @@ export const convertReservationsToNetworks = (agentReservations, networkHocon, a
54
63
  agentNetworkName: agentNetworkName ?? removeTrailingUuid(reservation.reservation_id),
55
64
  networkHocon,
56
65
  agentNetworkDefinition,
57
- }));
66
+ })));
58
67
  };
59
68
  export const isEditableAgent = (displayAs) => displayAs === DisplayAs.LLM_AGENT;
60
69
  /**
@@ -82,14 +91,53 @@ export const isTemporaryNetwork = (agentName, networks) => agentName !== null &&
82
91
  * Merges incoming networks into target, keeping the entry with the highest expiration time.
83
92
  * Returns a new array; does not mutate either argument.
84
93
  */
85
- export const mergeNetworks = (target, incoming) => incoming.reduce((result, n) => {
86
- const existingIdx = result.findIndex((e) => e.agentNetworkName === n.agentNetworkName);
87
- // No existing entry with this name - append.
88
- if (existingIdx < 0)
89
- return [...result, n];
90
- // Existing entry found - keep whichever reservation expires later.
91
- if (n.reservation.expiration_time_in_seconds > result[existingIdx].reservation.expiration_time_in_seconds) {
92
- return result.map((e, i) => (i === existingIdx ? n : e));
94
+ export const mergeNetworks = (target, incoming) => Object.values(mergeWith(keyBy(target, "agentNetworkName"), keyBy(incoming, "agentNetworkName"),
95
+ // Both collections share a name: keep whichever reservation expires later, existing wins ties.
96
+ (existing, candidate) => existing &&
97
+ existing.reservation.expiration_time_in_seconds >= candidate.reservation.expiration_time_in_seconds
98
+ ? existing
99
+ : candidate));
100
+ /**
101
+ * Extracts TemporaryNetworks from a single streamed chunk, merging into `accumulated`.
102
+ * Returns `accumulated` unchanged if the chunk yields no reservations or on parse error.
103
+ */
104
+ const collectNetworkFromChunk = (chunk, updated, accumulated) => {
105
+ try {
106
+ const chatMessage = chatMessageFromChunk(chunk);
107
+ if (!chatMessage)
108
+ return accumulated;
109
+ // Always use the user's edited definition as the authoritative value.
110
+ const converted = extractTemporaryNetworksFromMessage(chatMessage, updated);
111
+ if (converted.length === 0)
112
+ return accumulated;
113
+ return mergeNetworks(accumulated, converted);
93
114
  }
94
- return result;
95
- }, [...target]);
115
+ catch (e) {
116
+ console.warn("Failed to process chunk from network designer:", e);
117
+ return accumulated;
118
+ }
119
+ };
120
+ /** Logs and notifies about a save error. Suppresses AbortError (user-canceled). */
121
+ export const notifySaveError = (agentName, e) => {
122
+ if (e instanceof DOMException && e.name === "AbortError")
123
+ return;
124
+ console.error("Failed to submit agent network update:", e);
125
+ const detail = e instanceof DOMException && e.name === "TimeoutError"
126
+ ? "The request timed out waiting for the server. Please try again."
127
+ : String(e);
128
+ sendNotification(NotificationType.error, `Failed to update network "${agentName}".`, detail);
129
+ };
130
+ /**
131
+ * Streams a network definition through the network designer to obtain its reservation.
132
+ * Returns the single updated network as a one-element list (empty if the designer returned
133
+ * no reservation).
134
+ */
135
+ export const streamNetworkDesignerUpsert = async (neuroSanURL, signal, frontman, networkDef, agentNetworkName, username) => {
136
+ let newNetworks = [];
137
+ await sendNetworkDesignerRequest(neuroSanURL, signal, frontman, networkDef, agentNetworkName, username, (chunk) => {
138
+ // There is only one network. The designer echoes it across streamed chunks. Merge by name so the
139
+ // repeated echoes collapse into that single entry rather than appending duplicates.
140
+ newNetworks = collectNetworkFromChunk(chunk, networkDef, newNetworks);
141
+ });
142
+ return newNetworks;
143
+ };
@@ -1,6 +1,6 @@
1
1
  import { Edge, EdgeProps } from "@xyflow/react";
2
2
  import { FC } from "react";
3
- import { ChatMessageType } from "../../generated/neuro-san/NeuroSanClient.js";
3
+ import { ChatMessageType } from "../../../generated/neuro-san/NeuroSanClient.js";
4
4
  interface ThoughtBubbleEdgeData extends Record<string, unknown> {
5
5
  text?: string;
6
6
  showAlways?: boolean;
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { styled } from "@mui/material/styles";
3
3
  import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from "react";
4
- import { ChatMessageType } from "../../generated/neuro-san/NeuroSanClient.js";
4
+ import { ChatMessageType } from "../../../generated/neuro-san/NeuroSanClient.js";
5
5
  // #endregion: Types
6
6
  // #region: Constants
7
7
  const BUBBLE_DISTANCE_FROM_RIGHT_EDGE = 20; // Fixed distance from right edge
@@ -1,3 +1,4 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
1
2
  /**
2
3
  * This file defines the steps for the main tour of the Multi-Agent Accelerator application.
3
4
  *
@@ -18,8 +19,7 @@ export const MAIN_TOUR_STEPS = [
18
19
  placement: "bottom",
19
20
  },
20
21
  {
21
- content: "Agent Network Designer: Click here to create and edit your own network, with the help of a " +
22
- "powerful AI assistant.",
22
+ content: (_jsxs(_Fragment, { children: ["Agent Network Designer: Click here to create and edit your own network, with the help of a powerful AI assistant.", _jsx("br", {}), _jsx("br", {}), "Networks you create can be downloaded from the sidebar as a HOCON file and run in neuro-san or neuro-san-studio. See the User Guide (under Help) for more information."] })),
23
23
  target: () => document.querySelector("#add-network-icon"),
24
24
  placement: "bottom",
25
25
  },
@@ -67,6 +67,11 @@ export const MAIN_TOUR_STEPS = [
67
67
  target: () => document.querySelector("#agent-network-ui-options-menu-button-container"),
68
68
  placement: "bottom",
69
69
  },
70
+ {
71
+ content: "This button allows you to download the chat with this agent, including history, as a plain text file.",
72
+ target: () => document.querySelector("#save-chat-button"),
73
+ placement: "bottom",
74
+ },
70
75
  {
71
76
  content: "Access Settings from this icon. Here you can change the look and feel of the application, including " +
72
77
  "auto branding for a particular customer, enter API keys for providers like OpenAI and Anthropic, and " +
@@ -1,16 +1,10 @@
1
1
  import { ConnectivityInfo } from "../../generated/neuro-san/NeuroSanClient.js";
2
- export declare const DEFAULT_FRONTMAN_X_POS = 150;
3
- export declare const DEFAULT_FRONTMAN_Y_POS = 450;
4
- export declare const BASE_RADIUS = 100;
5
- export declare const LEVEL_SPACING = 150;
6
- export declare const TEMPORARY_NETWORK_FOLDER = "temporary";
7
- export declare const AGENT_RESERVATIONS_KEY = "agent_reservations";
8
- export declare const AGENT_NETWORK_HOCON = "agent_network_hocon_text";
9
- export declare const AGENT_PROGRESS_CONNECTIVITY_KEY = "connectivity_info";
10
- export declare const AGENT_NETWORK_DESIGNER_ID = "agent_network_designer";
11
2
  export declare const AGENT_NETWORK_DEFINITION_KEY = "agent_network_definition";
3
+ export declare const AGENT_NETWORK_DESIGNER_ID = "agent_network_designer";
4
+ export declare const AGENT_NETWORK_HOCON = "agent_network_hocon_text";
12
5
  export declare const AGENT_NETWORK_NAME_KEY = "agent_network_name";
13
- export declare const TRIGGER_APP_TOUR_EVENT_NAME = "trigger-app-tour";
6
+ export declare const AGENT_PROGRESS_CONNECTIVITY_KEY = "connectivity_info";
7
+ export declare const AGENT_RESERVATIONS_KEY = "agent_reservations";
14
8
  /**
15
9
  * A single agent entry within an agent network definition, as received in sly_data from the backend.
16
10
  * Extends ConnectivityInfo with editable instructions and description fields for the Agent Network Designer.
@@ -19,6 +13,9 @@ export type AgentNetworkDefinitionEntry = ConnectivityInfo & {
19
13
  readonly instructions?: string;
20
14
  readonly description?: string;
21
15
  };
16
+ export declare const BASE_RADIUS = 100;
17
+ export declare const DEFAULT_FRONTMAN_X_POS = 150;
18
+ export declare const DEFAULT_FRONTMAN_Y_POS = 450;
22
19
  /** Possible values for the `display_as` field in ConnectivityInfo / AgentNetworkDefinitionEntry. */
23
20
  export declare enum DisplayAs {
24
21
  LLM_AGENT = "llm_agent",
@@ -26,5 +23,9 @@ export declare enum DisplayAs {
26
23
  LANGCHAIN_TOOL = "langchain_tool",
27
24
  EXTERNAL_AGENT = "external_agent"
28
25
  }
26
+ export declare const EXPIRED_NETWORKS_CHECK_INTERVAL_MS: number;
29
27
  export declare const GRACE_PERIOD_MS: number;
28
+ export declare const LEVEL_SPACING = 150;
30
29
  export declare const SHOW_TOUR_DELAY_MS = 5000;
30
+ export declare const TEMPORARY_NETWORK_FOLDER = "temporary";
31
+ export declare const TRIGGER_APP_TOUR_EVENT_NAME = "trigger-app-tour";
@@ -13,30 +13,22 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */
16
- export const DEFAULT_FRONTMAN_X_POS = 150;
17
- export const DEFAULT_FRONTMAN_Y_POS = 450;
18
- // Minimum distance from center
19
- export const BASE_RADIUS = 100;
20
- // Distance between depth levels
21
- export const LEVEL_SPACING = 150;
22
- // Temporary folder name for networks created from agent reservations. These networks are not "in a folder" when
23
- // they come from the backend, but we need to put them somewhere in the UI, and this makes it clear that they're
24
- // temporary.
25
- export const TEMPORARY_NETWORK_FOLDER = "temporary";
26
- // We expect the agent reservations to be stored in sly_data under this key
27
- export const AGENT_RESERVATIONS_KEY = "agent_reservations";
28
- // We expect the agent network HOCON to be stored in sly_data under this key, if it is provided by the backend
29
- export const AGENT_NETWORK_HOCON = "agent_network_hocon_text";
30
- // The key in the message structure where connectivity info is stored for agent progress messages
31
- export const AGENT_PROGRESS_CONNECTIVITY_KEY = "connectivity_info";
32
- // Agent name for the special "Agent Network Designer" network
33
- export const AGENT_NETWORK_DESIGNER_ID = "agent_network_designer";
34
16
  // The key in sly_data where the agent network definition is stored
35
17
  export const AGENT_NETWORK_DEFINITION_KEY = "agent_network_definition";
18
+ // Agent name for the special "Agent Network Designer" network
19
+ export const AGENT_NETWORK_DESIGNER_ID = "agent_network_designer";
20
+ // We expect the agent network HOCON to be stored in sly_data under this key, if it is provided by the backend
21
+ export const AGENT_NETWORK_HOCON = "agent_network_hocon_text";
36
22
  // The key in sly_data where the agent network name is stored
37
23
  export const AGENT_NETWORK_NAME_KEY = "agent_network_name";
38
- // The event name that needs to be fired for the app tour to start
39
- export const TRIGGER_APP_TOUR_EVENT_NAME = "trigger-app-tour";
24
+ // The key in the message structure where connectivity info is stored for agent progress messages
25
+ export const AGENT_PROGRESS_CONNECTIVITY_KEY = "connectivity_info";
26
+ // We expect the agent reservations to be stored in sly_data under this key
27
+ export const AGENT_RESERVATIONS_KEY = "agent_reservations";
28
+ // Minimum distance from center
29
+ export const BASE_RADIUS = 100;
30
+ export const DEFAULT_FRONTMAN_X_POS = 150;
31
+ export const DEFAULT_FRONTMAN_Y_POS = 450;
40
32
  /** Possible values for the `display_as` field in ConnectivityInfo / AgentNetworkDefinitionEntry. */
41
33
  export var DisplayAs;
42
34
  (function (DisplayAs) {
@@ -45,7 +37,17 @@ export var DisplayAs;
45
37
  DisplayAs["LANGCHAIN_TOOL"] = "langchain_tool";
46
38
  DisplayAs["EXTERNAL_AGENT"] = "external_agent";
47
39
  })(DisplayAs || (DisplayAs = {}));
40
+ // Check for expired networks every this many milliseconds
41
+ export const EXPIRED_NETWORKS_CHECK_INTERVAL_MS = 10 * 1000;
48
42
  // Display expired temporary networks for this amount of time after they expire so users can see what happened
49
43
  export const GRACE_PERIOD_MS = 5 * 60 * 1000; // 5 minutes
44
+ // Distance between depth levels
45
+ export const LEVEL_SPACING = 150;
50
46
  // We show the tour modal after this amount of time so as not to "pounce" on the user when they first open the app
51
47
  export const SHOW_TOUR_DELAY_MS = 5000;
48
+ // Temporary folder name for networks created from agent reservations. These networks are not "in a folder" when
49
+ // they come from the backend, but we need to put them somewhere in the UI, and this makes it clear that they're
50
+ // temporary.
51
+ export const TEMPORARY_NETWORK_FOLDER = "temporary";
52
+ // The event name that needs to be fired for the app tour to start
53
+ export const TRIGGER_APP_TOUR_EVENT_NAME = "trigger-app-tour";
@@ -19,12 +19,11 @@ import Typography from "@mui/material/Typography";
19
19
  import { useEffect, useMemo, useState, } from "react";
20
20
  import { ApiKeyInput } from "./ApiKeyInput.js";
21
21
  import { useCheckmarkFade } from "./FadingCheckmark.js";
22
- import InfoTip from "./InfoTip.js";
23
22
  import { SettingsRow } from "./SettingsRow.js";
24
23
  import { getBrandingSuggestions, testConnection } from "../../controller/agent/Agent.js";
25
24
  import { isAnthropicKeyValid, isOpenAIKeyValid } from "../../controller/llm/Providers.js";
26
25
  import { useEnvironmentStore } from "../../state/Environment.js";
27
- import { DEFAULT_SETTINGS, useSettingsStore } from "../../state/Settings.js";
26
+ import { API_KEYS_TTL_MS, DEFAULT_SETTINGS, getApiKey, useSettingsStore, } from "../../state/Settings.js";
28
27
  import { PALETTES } from "../../Theme/Palettes.js";
29
28
  import { ConfirmationModal } from "../Common/ConfirmationModal.js";
30
29
  import { MUIDialog } from "../Common/MUIDialog.js";
@@ -40,7 +39,7 @@ const Section = styled(Box)(({ theme }) => ({
40
39
  display: "flex",
41
40
  flexDirection: "column",
42
41
  gap: theme.spacing(2),
43
- marginBottom: theme.spacing(4),
42
+ marginBottom: theme.spacing(2),
44
43
  }));
45
44
  const SubsectionTitle = styled(Typography)(({ theme }) => ({
46
45
  borderBottom: `1px solid ${theme.palette.divider}`,
@@ -235,10 +234,20 @@ export const SettingsDialog = ({ id, isOpen, logoServiceToken, onClose }) => {
235
234
  brandingCheckmark.trigger();
236
235
  logoCheckmark.trigger();
237
236
  };
238
- const persistKey = (vendor, key, checkmark) => {
237
+ /**
238
+ * Used for both saving and removing keys.
239
+ * @param vendor Vendor for this key: see the list in LLMProvider type
240
+ * @param key Value of the key
241
+ * @param checkmark Checkmark to trigger on save
242
+ * @param now Timestamp for "now"
243
+ */
244
+ const persistKey = (vendor, key, checkmark, now) => {
239
245
  updateSettings({
240
246
  apiKeys: {
241
- [vendor]: key,
247
+ [vendor]: {
248
+ value: key,
249
+ expiresAt: key ? now + API_KEYS_TTL_MS : 0,
250
+ },
242
251
  },
243
252
  });
244
253
  checkmark.trigger();
@@ -324,9 +333,17 @@ export const SettingsDialog = ({ id, isOpen, logoServiceToken, onClose }) => {
324
333
  }, sx: { flex: 1 }, value: neuroSanUrlInput }), _jsx(StatusLight, { id: `${id}-status-light`, statusValue: statusLightValue }), _jsx(Button, { disabled: neuroSanUrlInput.trim().length === 0, onClick: handleTestConnection, size: "small", variant: "contained", children: "Test" }), _jsx(Button, { disabled: neuroSanUrlSaveDisabled, onClick: handleSaveNeuroSanUrl, size: "small", variant: "contained", children: "Save" }), _jsx(Button, { disabled: neuroSanUrlResetDisabled, onClick: handleResetNeuroSanUrl, size: "small", variant: "contained", children: "Default" })] }, `${id}-neuro-san-server-url`) }));
325
334
  };
326
335
  const getServicesSection = () => (_jsxs(Section, { children: [_jsx(SettingsSectionTitle, { children: "External Services" }), getNeuroSanSubsection()] }));
327
- const getApiKeysSection = () => (_jsxs(Section, { children: [_jsxs(Box, { sx: { display: "flex", alignItems: "center", gap: theme.spacing(2) }, children: [_jsx(SettingsSectionTitle, { children: "API Keys" }), _jsx(InfoTip, { title: "API keys are used to access external services. Some networks may require an API key for " +
328
- "you to use them. Your keys will be saved in your browser local storage and sent to " +
329
- "services that require them. Do not use this option if you are on a shared or public computer." })] }), _jsx(SubsectionTitle, { variant: "subtitle1", children: "Providers" }), _jsx(SubSectionBody, { children: _jsx(Box, { sx: { display: "flex", flexDirection: "column", alignItems: "center", gap: 1.5 }, children: apiKeyConfigs.map(({ checkmark, vendor, idSuffix, logo, onTest, placeholder }) => (_jsx(SettingsRow, { checkmark: checkmark, label: "", tooltip: `API key for ${vendor}.`, children: _jsx(ApiKeyInput, { forgetKey: () => persistKey(vendor, "", checkmark), id: `${id}-${idSuffix}`, logo: logo, onSave: (key) => persistKey(vendor, key, checkmark), onTest: onTest, persistedValue: apiKeys[vendor], placeholder: placeholder, vendor: vendor }) }, `${id}-${idSuffix}`))) }) })] }));
336
+ const getApiKeysSection = () => (_jsxs(Section, { children: [_jsxs(Box, { sx: { display: "flex", alignItems: "center", gap: theme.spacing(1) }, children: [_jsx(SettingsSectionTitle, { children: "API Keys" }), _jsx(Tooltip, { title: "API keys are stored locally in your browser's memory for the duration of your session only\n and are only sent to the Neuro SAN service for use with the associated LLM provider when you use\n this application to interact with networks.\n Your keys are never sent to any other servers or services and are not stored on our servers.\n When you close this tab or your browser, your keys will be permanently cleared from memory and\n you will need to enter them again to use services that require them.", children: _jsxs(Typography, { sx: {
337
+ alignSelf: "flex-start",
338
+ color: "var(--bs-secondary)",
339
+ cursor: "help",
340
+ fontSize: "0.7rem",
341
+ lineHeight: 1,
342
+ mt: 0.5,
343
+ }, variant: "caption", children: [_jsx(Box, { component: "span", sx: { mr: 0.5 }, children: "\u24D8" }), _jsx(Box, { component: "span", sx: {
344
+ borderBottom: "1px dashed",
345
+ pb: 0.25,
346
+ }, children: "How are my keys stored and used?" })] }) })] }), _jsx(SettingsSubsection, { title: "LLM Providers", children: _jsx(Box, { sx: { display: "flex", flexDirection: "column", alignItems: "center", gap: 1.5 }, children: apiKeyConfigs.map(({ checkmark, vendor, idSuffix, logo, onTest, placeholder }) => (_jsx(SettingsRow, { checkmark: checkmark, label: "", tooltip: `API key for ${vendor}.`, children: _jsx(ApiKeyInput, { forgetKey: () => persistKey(vendor, "", checkmark, 0), id: `${id}-${idSuffix}`, logo: logo, onSave: (key) => persistKey(vendor, key, checkmark, Date.now()), onTest: onTest, persistedValue: getApiKey(apiKeys, vendor), placeholder: placeholder, vendor: vendor }) }, `${id}-${idSuffix}`))) }) })] }));
330
347
  const getBehaviorSection = () => (_jsxs(Section, { children: [_jsx(SettingsSectionTitle, { children: "Behavior" }), _jsx(SettingsSubsection, { title: "Zen mode", children: _jsx(SettingsRow, { label: 'Enable "Zen" mode:', checkmark: enableZenModeCheckmark, tooltip: "Hides most of the UI during agent network animations providing a more immersive " +
331
348
  "experience.", children: _jsx(Checkbox, { checked: enableZenMode, "data-testid": "zen-mode-checkbox", onChange: (_, checked) => {
332
349
  updateSettings({ behavior: { enableZenMode: checked } });
@@ -345,7 +362,6 @@ export const SettingsDialog = ({ id, isOpen, logoServiceToken, onClose }) => {
345
362
  nativeNamesCheckmark.trigger();
346
363
  }
347
364
  }, size: "small", value: useNativeNames ? "native" : "beautified", children: [_jsx(ToggleButton, { value: "native", children: "Native" }), _jsx(ToggleButton, { value: "beautified", children: "Beautified" })] }), _jsx(FormLabel, { children: "Preview: " }), _jsx(FormLabel, { sx: {
348
- marginBottom: 0,
349
365
  border: "1px solid",
350
366
  borderRadius: 1,
351
367
  backgroundColor: "background.paper",
@@ -456,7 +472,7 @@ export const SettingsDialog = ({ id, isOpen, logoServiceToken, onClose }) => {
456
472
  },
457
473
  }), [paletteMode]);
458
474
  return (
459
- // Always use default theme for settings dialog so user can always see to reset. It's possible that with
475
+ // Always use the default theme for settings dialog so user can always see to reset. It's possible that with
460
476
  // certain custom themes the dialog would be unreadable.
461
477
  _jsxs(ThemeProvider, { theme: settingsTheme, children: [resetToDefaultSettingsOpen ? getConfirmationModal() : null, _jsxs(MUIDialog, { id: id, title: _jsx(Box, { sx: { fontSize: "1.5rem" }, children: "Settings" }), isOpen: isOpen, onClose: onClose, paperProps: {
462
478
  minWidth: "50%",
@@ -2,11 +2,19 @@ import { AgentNetworkDefinitionEntry } from "../../components/MultiAgentAccelera
2
2
  import { AgentInfo, ChatContext, ChatResponse, ConnectivityResponse, FunctionResponse } from "../../generated/neuro-san/NeuroSanClient.js";
3
3
  import { StreamingUnit } from "../llm/LlmChat.js";
4
4
  import { BrandingSuggestions } from "../Types/Branding.js";
5
+ export interface HealthCheckResponse {
6
+ readonly service?: string;
7
+ readonly status?: string;
8
+ readonly versions?: {
9
+ readonly "neuro-san-studio"?: string;
10
+ readonly "neuro-san"?: string;
11
+ };
12
+ }
5
13
  export interface TestConnectionResult {
14
+ readonly healthCheckResponse?: HealthCheckResponse;
6
15
  readonly httpStatus?: number;
7
- readonly status?: string;
16
+ readonly statusText?: string;
8
17
  readonly success: boolean;
9
- readonly version?: string;
10
18
  }
11
19
  /**
12
20
  * Test connection for a neuro-san server.
@@ -64,8 +72,8 @@ export declare const getConnectivity: (url: string, network: string, userId: str
64
72
  */
65
73
  export declare const getAgentFunction: (url: string, agent: string, userId: string) => Promise<FunctionResponse>;
66
74
  /**
67
- * Streams the Agent Network Designer endpoint with an updated agent definition.
68
- * Calls `onChunk` for each line received; callers are responsible for parsing
69
- * reservations / networks out of each chunk.
75
+ * Streams the Agent Network Designer endpoint with an agent definition, either updating an
76
+ * existing network or adding a fresh one. Calls `onChunk` for each line received; callers are
77
+ * responsible for parsing reservations / networks out of each chunk.
70
78
  */
71
- export declare const sendNetworkDesignerUpdate: (url: string, signal: AbortSignal, agentName: string, updated: AgentNetworkDefinitionEntry[], agentNetworkName: string | undefined, currentUser: string, onChunk: (chunk: string) => void) => Promise<void>;
79
+ export declare const sendNetworkDesignerRequest: (url: string, signal: AbortSignal, agentName: string, updated: AgentNetworkDefinitionEntry[], agentNetworkName: string | undefined, currentUser: string, onChunk: (chunk: string) => void) => Promise<void>;
@@ -50,23 +50,27 @@ export const testConnection = async (url) => {
50
50
  const response = await fetch(url, { signal: controller.signal });
51
51
  if (!response.ok) {
52
52
  return {
53
- success: false,
54
53
  httpStatus: response.status,
55
- status: response.statusText,
54
+ statusText: response.statusText,
55
+ success: false,
56
56
  };
57
57
  }
58
58
  const jsonResponse = await response.json();
59
- // eslint-disable-next-line no-shadow
60
- const status = jsonResponse?.status;
61
- // Different versions of the server return different status values, so we need to check for both.
62
- const success = status === "healthy" || status === "ok";
63
- // For now, just capture the Neuro-san version since that's all the server returns. More can be added later.
64
- const version = jsonResponse?.versions?.["neuro-san"];
65
- return { httpStatus: response.status, success, status, version };
59
+ const statusText = jsonResponse?.status;
60
+ const success = statusText === "ok";
61
+ return {
62
+ healthCheckResponse: jsonResponse,
63
+ httpStatus: response.status,
64
+ statusText,
65
+ success,
66
+ };
66
67
  }
67
68
  catch (error) {
68
69
  const errorMessage = error instanceof Error ? error.message : String(error);
69
- return { success: false, status: errorMessage };
70
+ return {
71
+ statusText: errorMessage,
72
+ success: false,
73
+ };
70
74
  }
71
75
  finally {
72
76
  clearTimeout(timeout);
@@ -180,11 +184,11 @@ export const getAgentFunction = async (url, agent, userId) => {
180
184
  return response.json();
181
185
  };
182
186
  /**
183
- * Streams the Agent Network Designer endpoint with an updated agent definition.
184
- * Calls `onChunk` for each line received; callers are responsible for parsing
185
- * reservations / networks out of each chunk.
187
+ * Streams the Agent Network Designer endpoint with an agent definition, either updating an
188
+ * existing network or adding a fresh one. Calls `onChunk` for each line received; callers are
189
+ * responsible for parsing reservations / networks out of each chunk.
186
190
  */
187
- export const sendNetworkDesignerUpdate = async (url, signal, agentName, updated, agentNetworkName, currentUser, onChunk) => {
191
+ export const sendNetworkDesignerRequest = async (url, signal, agentName, updated, agentNetworkName, currentUser, onChunk) => {
188
192
  await sendChatQuery(url, signal,
189
193
  // Shouldn't have to pass a user message, but API behaves differently without it
190
194
  `Update instructions for agent "${agentName}"`, AGENT_NETWORK_DESIGNER_ID, onChunk, null, {
package/dist/index.d.ts CHANGED
@@ -23,7 +23,7 @@ export * from "./components/Common/notification.js";
23
23
  export * from "./components/ErrorPage/ErrorBoundary.js";
24
24
  export * from "./components/MultiAgentAccelerator/AgentConversations.js";
25
25
  export * from "./components/MultiAgentAccelerator/AgentCounts.js";
26
- export * from "./components/MultiAgentAccelerator/AgentFlow.js";
26
+ export * from "./components/MultiAgentAccelerator/AgentFlow/AgentFlow.js";
27
27
  export * from "./components/MultiAgentAccelerator/MultiAgentAccelerator.js";
28
28
  export * from "./components/MultiAgentAccelerator/Sidebar/Sidebar.js";
29
29
  export * from "./components/MultiAgentAccelerator/TemporaryNetworks.js";
package/dist/index.js CHANGED
@@ -38,7 +38,7 @@ export * from "./components/Common/notification.js";
38
38
  export * from "./components/ErrorPage/ErrorBoundary.js";
39
39
  export * from "./components/MultiAgentAccelerator/AgentConversations.js";
40
40
  export * from "./components/MultiAgentAccelerator/AgentCounts.js";
41
- export * from "./components/MultiAgentAccelerator/AgentFlow.js";
41
+ export * from "./components/MultiAgentAccelerator/AgentFlow/AgentFlow.js";
42
42
  export * from "./components/MultiAgentAccelerator/MultiAgentAccelerator.js";
43
43
  export * from "./components/MultiAgentAccelerator/Sidebar/Sidebar.js";
44
44
  export * from "./components/MultiAgentAccelerator/TemporaryNetworks.js";
@@ -11,6 +11,13 @@ type DeepPartial<T> = {
11
11
  };
12
12
  export type LLMProvider = "OpenAI" | "Anthropic";
13
13
  export type LogoSource = "none" | "generic" | "auto";
14
+ export type ByokKeyField = (typeof LLM_PROVIDER_API_KEY_FIELD)[LLMProvider];
15
+ interface ApiKeyEntry {
16
+ readonly value: string;
17
+ readonly expiresAt: number;
18
+ }
19
+ type ApiKeys = Partial<Record<LLMProvider, ApiKeyEntry>>;
20
+ export type PaletteKey = keyof typeof PALETTES | "brand";
14
21
  /**
15
22
  * User preference settings
16
23
  */
@@ -35,7 +42,7 @@ interface Settings {
35
42
  readonly behavior: {
36
43
  readonly enableZenMode: boolean;
37
44
  };
38
- readonly apiKeys: Partial<Record<LLMProvider, string>>;
45
+ readonly apiKeys: ApiKeys;
39
46
  readonly externalServices: {
40
47
  neuroSanUrl: string | null;
41
48
  };
@@ -48,25 +55,95 @@ interface SettingsStore {
48
55
  readonly updateSettings: (updates: DeepPartial<Settings>) => void;
49
56
  readonly resetSettings: () => void;
50
57
  }
58
+ export declare const LLM_PROVIDER_API_KEY_FIELD: {
59
+ readonly OpenAI: "openai_api_key";
60
+ readonly Anthropic: "anthropic_api_key";
61
+ };
51
62
  /**
52
63
  * Default settings, used on first load and on reset
53
64
  */
54
65
  export declare const DEFAULT_SETTINGS: Settings;
66
+ export declare const APP_SETTINGS_STORAGE_KEY = "app-settings";
67
+ export declare const SESSION_API_KEYS_STORAGE_KEY = "app-settings-api-keys";
68
+ export declare const API_KEYS_TTL_MS: number;
69
+ /**
70
+ * Returns the API key for the given provider if it exists and is not expired, otherwise returns undefined.
71
+ * This helper should be used to retrieve API keys from the settings store, rather than accessing the store directly,
72
+ * to ensure that expired keys are not returned.
73
+ * @param apiKeys Set of API keys stored in the settings store
74
+ * @param provider The LLM provider for which to retrieve the API key
75
+ */
76
+ export declare const getApiKey: (apiKeys: ApiKeys, provider: LLMProvider) => string;
55
77
  /**
56
78
  * The hook that lets apps use the store
57
79
  */
58
80
  export declare const useSettingsStore: import("zustand").UseBoundStore<Omit<import("zustand").StoreApi<SettingsStore>, "persist"> & {
59
81
  persist: {
60
- setOptions: (options: Partial<import("zustand/middleware").PersistOptions<SettingsStore, SettingsStore>>) => void;
82
+ setOptions: (options: Partial<import("zustand/middleware").PersistOptions<SettingsStore, {
83
+ settings: {
84
+ appearance: {
85
+ readonly agentIconColor: string;
86
+ readonly agentNodeColor: string;
87
+ readonly autoAgentIconColor: boolean;
88
+ readonly plasmaColor: string;
89
+ readonly rangePalette: PaletteKey;
90
+ readonly useNativeNames: boolean;
91
+ };
92
+ branding: {
93
+ readonly customer: string | null;
94
+ readonly primary: string | null;
95
+ readonly secondary: string | null;
96
+ readonly background: string | null;
97
+ readonly rangePalette: string[] | null;
98
+ readonly iconSuggestion: string | null;
99
+ readonly logoSource: LogoSource;
100
+ };
101
+ behavior: {
102
+ readonly enableZenMode: boolean;
103
+ };
104
+ externalServices: {
105
+ neuroSanUrl: string | null;
106
+ };
107
+ };
108
+ updateSettings: (updates: DeepPartial<Settings>) => void;
109
+ resetSettings: () => void;
110
+ }>>) => void;
61
111
  clearStorage: () => void;
62
112
  rehydrate: () => Promise<void> | void;
63
113
  hasHydrated: () => boolean;
64
114
  onHydrate: (fn: (state: SettingsStore) => void) => () => void;
65
115
  onFinishHydration: (fn: (state: SettingsStore) => void) => () => void;
66
- getOptions: () => Partial<import("zustand/middleware").PersistOptions<SettingsStore, SettingsStore>>;
116
+ getOptions: () => Partial<import("zustand/middleware").PersistOptions<SettingsStore, {
117
+ settings: {
118
+ appearance: {
119
+ readonly agentIconColor: string;
120
+ readonly agentNodeColor: string;
121
+ readonly autoAgentIconColor: boolean;
122
+ readonly plasmaColor: string;
123
+ readonly rangePalette: PaletteKey;
124
+ readonly useNativeNames: boolean;
125
+ };
126
+ branding: {
127
+ readonly customer: string | null;
128
+ readonly primary: string | null;
129
+ readonly secondary: string | null;
130
+ readonly background: string | null;
131
+ readonly rangePalette: string[] | null;
132
+ readonly iconSuggestion: string | null;
133
+ readonly logoSource: LogoSource;
134
+ };
135
+ behavior: {
136
+ readonly enableZenMode: boolean;
137
+ };
138
+ externalServices: {
139
+ neuroSanUrl: string | null;
140
+ };
141
+ };
142
+ updateSettings: (updates: DeepPartial<Settings>) => void;
143
+ resetSettings: () => void;
144
+ }>>;
67
145
  };
68
146
  }>;
69
- export type PaletteKey = keyof typeof PALETTES | "brand";
70
147
  /**
71
148
  * Custom hook to get the current color palette based on user settings.
72
149
  * If the user has selected custom branding, it will return the palette for that.