@cognizant-ai-lab/ui-common 1.8.0 → 1.9.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.
- package/README.md +3 -3
- package/dist/components/AgentChat/ChatCommon/ChatCommon.d.ts +9 -3
- package/dist/components/AgentChat/ChatCommon/ChatCommon.js +89 -23
- package/dist/components/AgentChat/ChatCommon/ChatHistory.d.ts +7 -0
- package/dist/components/AgentChat/ChatCommon/ChatHistory.js +1 -1
- package/dist/components/AgentChat/ChatCommon/ControlButtons.d.ts +4 -2
- package/dist/components/AgentChat/ChatCommon/ControlButtons.js +8 -2
- package/dist/components/AgentChat/ChatCommon/ConversationTurn.d.ts +5 -5
- package/dist/components/AgentChat/ChatCommon/ConversationTurn.js +1 -0
- package/dist/components/AgentChat/Common/Utils.d.ts +4 -0
- package/dist/components/AgentChat/Common/Utils.js +5 -1
- package/dist/components/Common/Breadcrumbs.js +1 -1
- package/dist/components/Common/notification.d.ts +4 -4
- package/dist/components/MultiAgentAccelerator/{AgentFlow.d.ts → AgentFlow/AgentFlow.d.ts} +15 -5
- package/dist/components/MultiAgentAccelerator/{AgentFlow.js → AgentFlow/AgentFlow.js} +44 -35
- package/dist/components/MultiAgentAccelerator/{AgentNode.d.ts → AgentFlow/AgentNode.d.ts} +1 -1
- package/dist/components/MultiAgentAccelerator/{AgentNode.js → AgentFlow/AgentNode.js} +4 -4
- package/dist/components/MultiAgentAccelerator/{GraphLayouts.d.ts → AgentFlow/GraphLayouts.d.ts} +4 -4
- package/dist/components/MultiAgentAccelerator/{GraphLayouts.js → AgentFlow/GraphLayouts.js} +6 -35
- package/dist/components/MultiAgentAccelerator/AgentFlow/GraphStructure.d.ts +21 -0
- package/dist/components/MultiAgentAccelerator/AgentFlow/GraphStructure.js +27 -0
- package/dist/components/MultiAgentAccelerator/AgentFlow/PlasmaEdge.d.ts +7 -0
- package/dist/components/MultiAgentAccelerator/{PlasmaEdge.js → AgentFlow/PlasmaEdge.js} +13 -8
- package/dist/components/MultiAgentAccelerator/MultiAgentAccelerator.js +116 -78
- package/dist/components/MultiAgentAccelerator/Schema/SlyData.d.ts +17 -0
- package/dist/components/MultiAgentAccelerator/Schema/SlyData.js +23 -0
- package/dist/components/MultiAgentAccelerator/Sidebar/AgentNetworkTreeItem.js +9 -4
- package/dist/components/MultiAgentAccelerator/Sidebar/ImportNetworkModal.d.ts +67 -0
- package/dist/components/MultiAgentAccelerator/Sidebar/ImportNetworkModal.js +585 -0
- package/dist/components/MultiAgentAccelerator/Sidebar/Sidebar.d.ts +2 -0
- package/dist/components/MultiAgentAccelerator/Sidebar/Sidebar.js +19 -26
- package/dist/components/MultiAgentAccelerator/Sidebar/TreeBuilder.d.ts +2 -1
- package/dist/components/MultiAgentAccelerator/Sidebar/TreeBuilder.js +24 -4
- package/dist/components/MultiAgentAccelerator/TemporaryNetworks.d.ts +22 -0
- package/dist/components/MultiAgentAccelerator/TemporaryNetworks.js +74 -3
- package/dist/components/MultiAgentAccelerator/{ThoughtBubbleEdge.d.ts → ThoughtBubbles/ThoughtBubbleEdge.d.ts} +1 -1
- package/dist/components/MultiAgentAccelerator/{ThoughtBubbleOverlay.js → ThoughtBubbles/ThoughtBubbleOverlay.js} +1 -1
- package/dist/components/MultiAgentAccelerator/Tour/MainTourSteps.js +5 -0
- package/dist/components/MultiAgentAccelerator/const.d.ts +1 -0
- package/dist/components/MultiAgentAccelerator/const.js +2 -0
- package/dist/components/Settings/SettingsDialog.js +26 -10
- package/dist/controller/agent/Agent.d.ts +14 -6
- package/dist/controller/agent/Agent.js +18 -14
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/state/Settings.d.ts +81 -4
- package/dist/state/Settings.js +87 -7
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/dist/utils/BrowserNavigation.js +2 -0
- package/dist/utils/File.d.ts +4 -1
- package/dist/utils/File.js +4 -9
- package/dist/utils/text.js +3 -7
- package/package.json +9 -10
- package/dist/components/MultiAgentAccelerator/PlasmaEdge.d.ts +0 -3
- /package/dist/components/MultiAgentAccelerator/{ThoughtBubbleEdge.js → ThoughtBubbles/ThoughtBubbleEdge.js} +0 -0
- /package/dist/components/MultiAgentAccelerator/{ThoughtBubbleOverlay.d.ts → ThoughtBubbles/ThoughtBubbleOverlay.d.ts} +0 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { FC } from "react";
|
|
2
|
+
import { AgentNetworkDefinitionEntry } from "../const.js";
|
|
3
|
+
export declare const IMPORT_MODAL_MAX_FILE_SIZE_BYTES: number;
|
|
4
|
+
export type ImportFileValidation = "valid" | "unsupported_type" | "too_large";
|
|
5
|
+
export interface ImportNetworkModalProps {
|
|
6
|
+
readonly existingNetworkNames?: readonly string[];
|
|
7
|
+
readonly isOpen: boolean;
|
|
8
|
+
readonly onClose: () => void;
|
|
9
|
+
readonly onImport?: (name: string, content: string) => void;
|
|
10
|
+
}
|
|
11
|
+
export interface NetworkSummary {
|
|
12
|
+
readonly agents: number;
|
|
13
|
+
readonly codedTools: number;
|
|
14
|
+
readonly externalAgents: number;
|
|
15
|
+
readonly frontman: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Parse and validate a network definition file. Imports are JSON only.
|
|
19
|
+
*
|
|
20
|
+
* Returns the parsed value on success, or an error message on failure. The value is typed `unknown`,
|
|
21
|
+
* so callers narrow it (see `jsonToNetworkDefinition`) or stringify it as needed.
|
|
22
|
+
*/
|
|
23
|
+
export declare const parseNetworkFileContent: (text: string) => {
|
|
24
|
+
success: true;
|
|
25
|
+
data: unknown;
|
|
26
|
+
} | {
|
|
27
|
+
success: false;
|
|
28
|
+
error: string;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Converts a parsed network definition into an array of AgentNetworkDefinitionEntry objects
|
|
32
|
+
* suitable for sendNetworkDesignerRequest.
|
|
33
|
+
*
|
|
34
|
+
* Expects the top-level array shape — each entry carrying `origin`, `tools`, `display_as`, etc.
|
|
35
|
+
* Anything that isn't an array yields an empty result, and entries without a string `origin`
|
|
36
|
+
* are dropped. The `instructions` and `description` fields are trimmed when present.
|
|
37
|
+
*/
|
|
38
|
+
export declare const jsonToNetworkDefinition: (parsed: unknown) => AgentNetworkDefinitionEntry[];
|
|
39
|
+
export declare const summarizeNetworkDefinition: (networkDef: AgentNetworkDefinitionEntry[]) => NetworkSummary;
|
|
40
|
+
export declare const formatFileSize: (bytes: number) => string;
|
|
41
|
+
/**
|
|
42
|
+
* Validate a selected file against the advertised constraints before reading it.
|
|
43
|
+
*
|
|
44
|
+
* The `<input accept>` attribute only filters the file-picker dialog and is bypassed
|
|
45
|
+
* entirely by drag/drop, so we re-check the extension here. We also enforce the
|
|
46
|
+
* advertised size limit — purely a client-side guard so a pathological file can't be
|
|
47
|
+
* fed into the synchronous parsing/regex pass and freeze the UI; the server imposes
|
|
48
|
+
* no such limit.
|
|
49
|
+
*
|
|
50
|
+
* Returns a status the caller can branch on; rendering a message is left to the caller
|
|
51
|
+
* (see `importFileValidationMessage`).
|
|
52
|
+
*/
|
|
53
|
+
export declare const validateImportFile: (file: File) => ImportFileValidation;
|
|
54
|
+
export declare const importFileValidationMessage: (validation: ImportFileValidation, file: File) => string | null;
|
|
55
|
+
/** Convert a filename stem to a display-friendly network name.
|
|
56
|
+
*
|
|
57
|
+
* Strips a trailing UUID that neuro-san appends to exported filenames (e.g.
|
|
58
|
+
* `my_network_683b0dfb_4816_464d_9c83_7e59ce6497d3.json` → `My Network`).
|
|
59
|
+
*/
|
|
60
|
+
export declare const filenameToNetworkName: (filename: string) => string;
|
|
61
|
+
/** Pick the first non-colliding name by appending an incrementing index (" 2", " 3", …).
|
|
62
|
+
*
|
|
63
|
+
* Starts at 2 and skips any index already in use, so importing "My Network" alongside an existing
|
|
64
|
+
* "My Network" yields "My Network (2)" — or "My Network (3)" if "My Network (2)" is also taken.
|
|
65
|
+
*/
|
|
66
|
+
export declare const nextAvailableNetworkName: (baseName: string, existingNames: readonly string[]) => string;
|
|
67
|
+
export declare const ImportNetworkModal: FC<ImportNetworkModalProps>;
|
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/*
|
|
3
|
+
Copyright 2026 Cognizant Technology Solutions Corp, www.cognizant.com.
|
|
4
|
+
|
|
5
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
you may not use this file except in compliance with the License.
|
|
7
|
+
You may obtain a copy of the License at
|
|
8
|
+
|
|
9
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
|
|
11
|
+
Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
See the License for the specific language governing permissions and
|
|
15
|
+
limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import CheckCircleOutlineIcon from "@mui/icons-material/CheckCircleOutlined";
|
|
18
|
+
import CloudUploadOutlinedIcon from "@mui/icons-material/CloudUploadOutlined";
|
|
19
|
+
import ErrorOutlineIcon from "@mui/icons-material/ErrorOutlined";
|
|
20
|
+
import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
|
|
21
|
+
import InsertDriveFileOutlinedIcon from "@mui/icons-material/InsertDriveFileOutlined";
|
|
22
|
+
import WarningAmberIcon from "@mui/icons-material/WarningAmber";
|
|
23
|
+
import Box from "@mui/material/Box";
|
|
24
|
+
import Button from "@mui/material/Button";
|
|
25
|
+
import CircularProgress from "@mui/material/CircularProgress";
|
|
26
|
+
import Step from "@mui/material/Step";
|
|
27
|
+
import StepLabel from "@mui/material/StepLabel";
|
|
28
|
+
import Stepper from "@mui/material/Stepper";
|
|
29
|
+
import { alpha, styled } from "@mui/material/styles";
|
|
30
|
+
import TextField from "@mui/material/TextField";
|
|
31
|
+
import ToggleButton from "@mui/material/ToggleButton";
|
|
32
|
+
import ToggleButtonGroup from "@mui/material/ToggleButtonGroup";
|
|
33
|
+
import Tooltip from "@mui/material/Tooltip";
|
|
34
|
+
import Typography from "@mui/material/Typography";
|
|
35
|
+
import startCase from "lodash-es/startCase";
|
|
36
|
+
import { useEffect, useRef, useState } from "react";
|
|
37
|
+
import { splitFilename } from "../../../utils/File.js";
|
|
38
|
+
import { MUIDialog } from "../../Common/MUIDialog.js";
|
|
39
|
+
import { getFrontman } from "../AgentFlow/GraphStructure.js";
|
|
40
|
+
import { DisplayAs } from "../const.js";
|
|
41
|
+
//#region: Constants
|
|
42
|
+
export const IMPORT_MODAL_MAX_FILE_SIZE_BYTES = 5 * 1024 * 1024; // 5 MB
|
|
43
|
+
const IMPORT_MODAL_ACCEPTED_EXTENSIONS = [".json"];
|
|
44
|
+
const ACCEPTED_MIME_TYPES = IMPORT_MODAL_ACCEPTED_EXTENSIONS.join(", ");
|
|
45
|
+
const STEPS = ["Select file", "Review", "Confirm"];
|
|
46
|
+
/** Trailing UUID on a filename stem. Separator is `[_-]` because filename sanitization
|
|
47
|
+
* (`toSafeFilename`, neuro-san exports) flattens the UUID's hyphens to underscores.
|
|
48
|
+
*/
|
|
49
|
+
const FILENAME_TRAILING_UUID_PATTERN = /[_-][0-9a-fA-F]{8}[_-][0-9a-fA-F]{4}[_-][0-9a-fA-F]{4}[_-][0-9a-fA-F]{4}[_-][0-9a-fA-F]{12}$/u;
|
|
50
|
+
//#endregion: Interfaces and Types
|
|
51
|
+
//#region: Helpers
|
|
52
|
+
/**
|
|
53
|
+
* Parse and validate a network definition file. Imports are JSON only.
|
|
54
|
+
*
|
|
55
|
+
* Returns the parsed value on success, or an error message on failure. The value is typed `unknown`,
|
|
56
|
+
* so callers narrow it (see `jsonToNetworkDefinition`) or stringify it as needed.
|
|
57
|
+
*/
|
|
58
|
+
export const parseNetworkFileContent = (text) => {
|
|
59
|
+
if (text.trim() === "") {
|
|
60
|
+
return { success: false, error: "The file is empty." };
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
return { success: true, data: JSON.parse(text) };
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
return { success: false, error: err instanceof Error ? err.message : String(err) };
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Converts a parsed network definition into an array of AgentNetworkDefinitionEntry objects
|
|
71
|
+
* suitable for sendNetworkDesignerRequest.
|
|
72
|
+
*
|
|
73
|
+
* Expects the top-level array shape — each entry carrying `origin`, `tools`, `display_as`, etc.
|
|
74
|
+
* Anything that isn't an array yields an empty result, and entries without a string `origin`
|
|
75
|
+
* are dropped. The `instructions` and `description` fields are trimmed when present.
|
|
76
|
+
*/
|
|
77
|
+
export const jsonToNetworkDefinition = (parsed) => {
|
|
78
|
+
if (!Array.isArray(parsed))
|
|
79
|
+
return [];
|
|
80
|
+
return parsed
|
|
81
|
+
.filter((entry) => typeof entry?.["origin"] === "string")
|
|
82
|
+
.map((entry) => ({
|
|
83
|
+
...entry,
|
|
84
|
+
...(entry.instructions !== undefined && { instructions: entry.instructions.trim() }),
|
|
85
|
+
...(entry.description !== undefined && { description: entry.description.trim() }),
|
|
86
|
+
}));
|
|
87
|
+
};
|
|
88
|
+
// Summarises a parsed network definition (counts by `display_as`, plus the frontman).
|
|
89
|
+
export const summarizeNetworkDefinition = (networkDef) => {
|
|
90
|
+
const { agents, codedTools, externalAgents } = networkDef.reduce((acc, entry) => {
|
|
91
|
+
switch (entry.display_as) {
|
|
92
|
+
case DisplayAs.LLM_AGENT:
|
|
93
|
+
acc.agents += 1;
|
|
94
|
+
break;
|
|
95
|
+
case DisplayAs.CODED_TOOL:
|
|
96
|
+
acc.codedTools += 1;
|
|
97
|
+
break;
|
|
98
|
+
case DisplayAs.EXTERNAL_AGENT:
|
|
99
|
+
acc.externalAgents += 1;
|
|
100
|
+
break;
|
|
101
|
+
default:
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
return acc;
|
|
105
|
+
}, { agents: 0, codedTools: 0, externalAgents: 0 });
|
|
106
|
+
return {
|
|
107
|
+
agents,
|
|
108
|
+
codedTools,
|
|
109
|
+
externalAgents,
|
|
110
|
+
frontman: getFrontman(networkDef)?.origin ?? networkDef[0]?.origin ?? "—",
|
|
111
|
+
};
|
|
112
|
+
};
|
|
113
|
+
// Format byte count to a human-readable string (e.g. "4.2 KB").
|
|
114
|
+
export const formatFileSize = (bytes) => {
|
|
115
|
+
if (bytes < 1024)
|
|
116
|
+
return `${bytes} B`;
|
|
117
|
+
if (bytes < 1024 * 1024)
|
|
118
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
119
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
120
|
+
};
|
|
121
|
+
/**
|
|
122
|
+
* Validate a selected file against the advertised constraints before reading it.
|
|
123
|
+
*
|
|
124
|
+
* The `<input accept>` attribute only filters the file-picker dialog and is bypassed
|
|
125
|
+
* entirely by drag/drop, so we re-check the extension here. We also enforce the
|
|
126
|
+
* advertised size limit — purely a client-side guard so a pathological file can't be
|
|
127
|
+
* fed into the synchronous parsing/regex pass and freeze the UI; the server imposes
|
|
128
|
+
* no such limit.
|
|
129
|
+
*
|
|
130
|
+
* Returns a status the caller can branch on; rendering a message is left to the caller
|
|
131
|
+
* (see `importFileValidationMessage`).
|
|
132
|
+
*/
|
|
133
|
+
export const validateImportFile = (file) => {
|
|
134
|
+
const { ext } = splitFilename(file.name);
|
|
135
|
+
const normalizedExt = `.${ext.toLowerCase()}`;
|
|
136
|
+
if (!IMPORT_MODAL_ACCEPTED_EXTENSIONS.includes(normalizedExt)) {
|
|
137
|
+
return "unsupported_type";
|
|
138
|
+
}
|
|
139
|
+
if (file.size > IMPORT_MODAL_MAX_FILE_SIZE_BYTES) {
|
|
140
|
+
return "too_large";
|
|
141
|
+
}
|
|
142
|
+
return "valid";
|
|
143
|
+
};
|
|
144
|
+
// Human-readable explanation for a non-VALID validation status, or null if the file is acceptable.
|
|
145
|
+
export const importFileValidationMessage = (validation, file) => {
|
|
146
|
+
switch (validation) {
|
|
147
|
+
case "unsupported_type": {
|
|
148
|
+
const { ext } = splitFilename(file.name);
|
|
149
|
+
const accepted = IMPORT_MODAL_ACCEPTED_EXTENSIONS.join(" and ");
|
|
150
|
+
return `Unsupported file type${ext ? ` ".${ext}"` : ""}. Accepts ${accepted}.`;
|
|
151
|
+
}
|
|
152
|
+
case "too_large": {
|
|
153
|
+
const max = formatFileSize(IMPORT_MODAL_MAX_FILE_SIZE_BYTES);
|
|
154
|
+
return `File is too large (${formatFileSize(file.size)}). Maximum size is ${max}.`;
|
|
155
|
+
}
|
|
156
|
+
case "valid":
|
|
157
|
+
default:
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
/** Convert a filename stem to a display-friendly network name.
|
|
162
|
+
*
|
|
163
|
+
* Strips a trailing UUID that neuro-san appends to exported filenames (e.g.
|
|
164
|
+
* `my_network_683b0dfb_4816_464d_9c83_7e59ce6497d3.json` → `My Network`).
|
|
165
|
+
*/
|
|
166
|
+
export const filenameToNetworkName = (filename) => {
|
|
167
|
+
const { name: stem } = splitFilename(filename);
|
|
168
|
+
return startCase(stem.replace(FILENAME_TRAILING_UUID_PATTERN, ""));
|
|
169
|
+
};
|
|
170
|
+
// Normalize a network name for conflict comparison: underscores, hyphens, parentheses and whitespace
|
|
171
|
+
// all collapse to single spaces, so the display form "My Network (2)" and its API form "My_Network_2"
|
|
172
|
+
// compare equal.
|
|
173
|
+
const normalizeForComparison = (rawName) => {
|
|
174
|
+
const spaced = rawName.replaceAll(/[\s_()-]+/gu, " ").toLowerCase();
|
|
175
|
+
return spaced.trim();
|
|
176
|
+
};
|
|
177
|
+
/** Pick the first non-colliding name by appending an incrementing index (" 2", " 3", …).
|
|
178
|
+
*
|
|
179
|
+
* Starts at 2 and skips any index already in use, so importing "My Network" alongside an existing
|
|
180
|
+
* "My Network" yields "My Network (2)" — or "My Network (3)" if "My Network (2)" is also taken.
|
|
181
|
+
*/
|
|
182
|
+
export const nextAvailableNetworkName = (baseName, existingNames) => {
|
|
183
|
+
const taken = new Set(existingNames.map((existing) => normalizeForComparison(existing)));
|
|
184
|
+
let index = 2;
|
|
185
|
+
while (taken.has(normalizeForComparison(`${baseName} (${index})`))) {
|
|
186
|
+
index += 1;
|
|
187
|
+
}
|
|
188
|
+
return `${baseName} (${index})`;
|
|
189
|
+
};
|
|
190
|
+
//#endregion: Helpers
|
|
191
|
+
//#region: Styled Components
|
|
192
|
+
const DropZone = styled(Box, {
|
|
193
|
+
shouldForwardProp: (prop) => prop !== "isDragOver",
|
|
194
|
+
})(({ theme, isDragOver }) => ({
|
|
195
|
+
alignItems: "center",
|
|
196
|
+
borderRadius: theme.shape.borderRadius,
|
|
197
|
+
borderStyle: "dashed",
|
|
198
|
+
borderWidth: "2px",
|
|
199
|
+
borderColor: isDragOver ? theme.palette.primary.main : theme.palette.divider,
|
|
200
|
+
cursor: "pointer",
|
|
201
|
+
display: "flex",
|
|
202
|
+
flexDirection: "column",
|
|
203
|
+
gap: theme.spacing(1),
|
|
204
|
+
justifyContent: "center",
|
|
205
|
+
marginTop: theme.spacing(3),
|
|
206
|
+
minHeight: "220px",
|
|
207
|
+
padding: theme.spacing(4),
|
|
208
|
+
transition: "border-color 0.2s ease",
|
|
209
|
+
}));
|
|
210
|
+
// Hidden file input, triggered programmatically via the DropZone. MUI's idiomatic
|
|
211
|
+
// visually-hidden pattern (https://mui.com/material-ui/react-button/#file-upload).
|
|
212
|
+
const VisuallyHiddenInput = styled("input")({
|
|
213
|
+
bottom: 0,
|
|
214
|
+
clip: "rect(0 0 0 0)",
|
|
215
|
+
clipPath: "inset(50%)",
|
|
216
|
+
height: 1,
|
|
217
|
+
left: 0,
|
|
218
|
+
overflow: "hidden",
|
|
219
|
+
position: "absolute",
|
|
220
|
+
whiteSpace: "nowrap",
|
|
221
|
+
width: 1,
|
|
222
|
+
});
|
|
223
|
+
//#endregion: Styled Components
|
|
224
|
+
const EMPTY_NETWORK_NAMES = [];
|
|
225
|
+
export const ImportNetworkModal = ({ existingNetworkNames = EMPTY_NETWORK_NAMES, isOpen, onClose, onImport, }) => {
|
|
226
|
+
const [activeStep, setActiveStep] = useState(0);
|
|
227
|
+
// When the imported name conflicts, how the user wants to resolve it.
|
|
228
|
+
const [conflictResolution, setConflictResolution] = useState("keep-both");
|
|
229
|
+
const [file, setFile] = useState(null);
|
|
230
|
+
const fileInputRef = useRef(null);
|
|
231
|
+
const [isDragOver, setIsDragOver] = useState(false);
|
|
232
|
+
const [networkName, setNetworkName] = useState("");
|
|
233
|
+
const [parseState, setParseState] = useState(null);
|
|
234
|
+
const [parseError, setParseError] = useState(null);
|
|
235
|
+
const [parsedData, setParsedData] = useState(null);
|
|
236
|
+
// Reset all state whenever the modal is opened
|
|
237
|
+
useEffect(() => {
|
|
238
|
+
if (isOpen) {
|
|
239
|
+
setActiveStep(0);
|
|
240
|
+
setConflictResolution("keep-both");
|
|
241
|
+
setIsDragOver(false);
|
|
242
|
+
setFile(null);
|
|
243
|
+
setNetworkName("");
|
|
244
|
+
setParseState(null);
|
|
245
|
+
setParseError(null);
|
|
246
|
+
setParsedData(null);
|
|
247
|
+
}
|
|
248
|
+
}, [isOpen]);
|
|
249
|
+
// Read and parse the selected file. Driven by an effect so the read is owned by the component
|
|
250
|
+
// lifecycle: the cleanup aborts an in-flight FileReader (discarding its load/error listeners) if
|
|
251
|
+
// the modal closes or a new file is selected before this read finishes, preventing a stale read
|
|
252
|
+
// from updating state out from under a newer one.
|
|
253
|
+
useEffect(() => {
|
|
254
|
+
if (!file)
|
|
255
|
+
return undefined;
|
|
256
|
+
// Validate extension + size before reading. The <input accept> filter only
|
|
257
|
+
// hints the picker and is bypassed by drag/drop, so unsupported or oversized
|
|
258
|
+
// files would otherwise be fed straight into the parser.
|
|
259
|
+
const validation = validateImportFile(file);
|
|
260
|
+
if (validation !== "valid") {
|
|
261
|
+
setParseState("error");
|
|
262
|
+
setParseError(importFileValidationMessage(validation, file));
|
|
263
|
+
return undefined;
|
|
264
|
+
}
|
|
265
|
+
setParseState("loading");
|
|
266
|
+
setParseError(null);
|
|
267
|
+
const reader = new FileReader();
|
|
268
|
+
reader.addEventListener("load", (event) => {
|
|
269
|
+
const text = event.target?.result;
|
|
270
|
+
const result = parseNetworkFileContent(text);
|
|
271
|
+
if ("error" in result) {
|
|
272
|
+
setParseState("error");
|
|
273
|
+
setParseError(`Parse error: ${result.error}`);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
setParsedData(result.data);
|
|
277
|
+
setParseState("success");
|
|
278
|
+
setNetworkName(filenameToNetworkName(file.name));
|
|
279
|
+
setConflictResolution("keep-both");
|
|
280
|
+
});
|
|
281
|
+
reader.addEventListener("error", () => {
|
|
282
|
+
setParseState("error");
|
|
283
|
+
setParseError("Failed to read the file.");
|
|
284
|
+
});
|
|
285
|
+
// We use FileReader (rather than the blob's text() promise) so the read can be
|
|
286
|
+
// aborted on cleanup and report failures via the "error" event above.
|
|
287
|
+
// eslint-disable-next-line unicorn/prefer-blob-reading-methods
|
|
288
|
+
reader.readAsText(file);
|
|
289
|
+
return () => reader.abort();
|
|
290
|
+
}, [file]);
|
|
291
|
+
// True if `candidate` collides with any existing network name (normalized for comparison).
|
|
292
|
+
const nameConflictsWith = (candidate) => existingNetworkNames.some((existing) => normalizeForComparison(existing) === normalizeForComparison(candidate));
|
|
293
|
+
// The name pulled from the filename — fixed for the selected file. Whether it collides with an
|
|
294
|
+
// existing network is what drives the conflict-resolution UI (it stays visible while the user
|
|
295
|
+
// types a replacement, so we can't key it off the editable name).
|
|
296
|
+
const importedName = file ? filenameToNetworkName(file.name) : "";
|
|
297
|
+
const importedNameHasConflict = nameConflictsWith(importedName);
|
|
298
|
+
const trimmedName = networkName.trim();
|
|
299
|
+
const newNameHasConflict = nameConflictsWith(trimmedName);
|
|
300
|
+
// Advance to the review step and kick off reading/parsing the selected file.
|
|
301
|
+
const processFile = (selectedFile) => {
|
|
302
|
+
setActiveStep(1);
|
|
303
|
+
setParsedData(null);
|
|
304
|
+
setConflictResolution("keep-both");
|
|
305
|
+
// Setting the file kicks off the read effect below, which owns reading/parsing and
|
|
306
|
+
// cleans up its own FileReader.
|
|
307
|
+
setFile(selectedFile);
|
|
308
|
+
};
|
|
309
|
+
const handleDragOver = (event) => {
|
|
310
|
+
event.preventDefault();
|
|
311
|
+
setIsDragOver(true);
|
|
312
|
+
};
|
|
313
|
+
const handleDragLeave = (event) => {
|
|
314
|
+
event.preventDefault();
|
|
315
|
+
setIsDragOver(false);
|
|
316
|
+
};
|
|
317
|
+
const handleDrop = (event) => {
|
|
318
|
+
event.preventDefault();
|
|
319
|
+
setIsDragOver(false);
|
|
320
|
+
const dropped = event.dataTransfer.files[0];
|
|
321
|
+
if (dropped)
|
|
322
|
+
processFile(dropped);
|
|
323
|
+
};
|
|
324
|
+
const handleBrowseClick = () => {
|
|
325
|
+
fileInputRef.current?.click();
|
|
326
|
+
};
|
|
327
|
+
const handleFileChange = (event) => {
|
|
328
|
+
const selected = event.target.files?.[0];
|
|
329
|
+
if (selected)
|
|
330
|
+
processFile(selected);
|
|
331
|
+
// Reset input so the same file can be re-selected if needed
|
|
332
|
+
event.target.value = "";
|
|
333
|
+
};
|
|
334
|
+
const handleBack = () => setActiveStep((prev) => prev - 1);
|
|
335
|
+
const handleContinue = () => {
|
|
336
|
+
setConflictResolution("keep-both");
|
|
337
|
+
// On a collision, "Keep both" defaults to the next free indexed name (e.g. "My Network (2)")
|
|
338
|
+
// so the import is valid out of the box — the user can still edit it.
|
|
339
|
+
if (importedNameHasConflict) {
|
|
340
|
+
setNetworkName(nextAvailableNetworkName(importedName, existingNetworkNames));
|
|
341
|
+
}
|
|
342
|
+
setActiveStep(2);
|
|
343
|
+
};
|
|
344
|
+
const handleImport = () => {
|
|
345
|
+
if (parseState !== "success")
|
|
346
|
+
return;
|
|
347
|
+
// "Replace existing" overwrites the colliding network, so always send the original
|
|
348
|
+
// imported name; "Keep both" sends whatever unique name the user typed.
|
|
349
|
+
const nameToImport = importedNameHasConflict && conflictResolution === "replace" ? importedName : networkName;
|
|
350
|
+
// The API echoes back agent_network_name as-is, and the UI splits on underscores to
|
|
351
|
+
// produce display names — so send underscores instead of spaces. Parentheses around an
|
|
352
|
+
// auto-appended index ("My Network (2)") are dropped so the API name reads "My_Network_2".
|
|
353
|
+
const apiName = nameToImport.trim().replaceAll(" ", "_").replaceAll(/[()]/gu, "");
|
|
354
|
+
onImport?.(apiName, JSON.stringify(parsedData));
|
|
355
|
+
onClose();
|
|
356
|
+
};
|
|
357
|
+
// Switching modes resets the editable name: "Keep both" pre-fills the next free indexed name
|
|
358
|
+
// (e.g. "My Network (2)") so the user doesn't have to invent one, and "Replace existing" targets
|
|
359
|
+
// the original colliding name.
|
|
360
|
+
const handleConflictResolutionChange = (resolution) => {
|
|
361
|
+
setConflictResolution(resolution);
|
|
362
|
+
setNetworkName(resolution === "keep-both" ? nextAvailableNetworkName(importedName, existingNetworkNames) : importedName);
|
|
363
|
+
};
|
|
364
|
+
const { name: fileStem, ext: fileNameExt } = file ? splitFilename(file.name) : { name: "", ext: "" };
|
|
365
|
+
const fileExt = fileNameExt.toUpperCase();
|
|
366
|
+
// Summary shown on the review step. parsedData is the already-validated parsed value, so this never throws.
|
|
367
|
+
const networkSummary = parseState === "success" ? summarizeNetworkDefinition(jsonToNetworkDefinition(parsedData)) : null;
|
|
368
|
+
// The confirm-step primary action adapts to how the conflict is being resolved.
|
|
369
|
+
const isReplacing = importedNameHasConflict && conflictResolution === "replace";
|
|
370
|
+
const importButtonLabel = isReplacing
|
|
371
|
+
? "Replace network"
|
|
372
|
+
: importedNameHasConflict
|
|
373
|
+
? "Import as new"
|
|
374
|
+
: "Import network";
|
|
375
|
+
// Replacing overwrites the existing network, so the typed name is irrelevant; otherwise a name
|
|
376
|
+
// is required and must not collide with an existing network.
|
|
377
|
+
const importDisabled = !isReplacing && (!trimmedName || newNameHasConflict);
|
|
378
|
+
// Footer actions, which vary by step (Cancel / Back+Continue / Back+Import).
|
|
379
|
+
const renderFooter = () => {
|
|
380
|
+
switch (activeStep) {
|
|
381
|
+
case 0:
|
|
382
|
+
return (_jsx(Button, { id: "import-network-modal-cancel-btn", onClick: onClose, variant: "outlined", children: "Cancel" }));
|
|
383
|
+
case 1:
|
|
384
|
+
return (_jsxs(_Fragment, { children: [_jsx(Button, { id: "import-network-modal-back-btn", onClick: handleBack, variant: "outlined", children: "Back" }), parseState === "success" && (_jsx(Button, { id: "import-network-modal-continue-btn", onClick: handleContinue, sx: { "&:hover": { backgroundColor: "var(--bs-primary)" } }, variant: "contained", children: "Continue \u2192" }))] }));
|
|
385
|
+
case 2:
|
|
386
|
+
return (_jsxs(_Fragment, { children: [_jsx(Button, { id: "import-network-modal-back-btn", onClick: handleBack, variant: "outlined", children: "Back" }), _jsx(Button, { color: isReplacing ? "error" : "primary", disabled: importDisabled, id: "import-network-modal-import-btn", onClick: handleImport, variant: "contained", children: importButtonLabel })] }));
|
|
387
|
+
default:
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
// The three-step progress header.
|
|
392
|
+
const renderStepper = () => (_jsx(Stepper, { activeStep: activeStep, id: "import-network-modal-stepper", sx: { marginTop: 1 }, children: STEPS.map((label) => (_jsx(Step, { children: _jsx(StepLabel, { children: label }) }, label))) }));
|
|
393
|
+
// Step 1: the drag-and-drop / browse file picker, plus the restriction callout beneath it.
|
|
394
|
+
const renderSelectFileStep = () => (_jsxs(_Fragment, { children: [_jsxs(DropZone, { isDragOver: isDragOver, onDragLeave: handleDragLeave, onDragOver: handleDragOver, onDrop: handleDrop, onClick: handleBrowseClick, onKeyDown: (event) => {
|
|
395
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
396
|
+
event.preventDefault();
|
|
397
|
+
handleBrowseClick();
|
|
398
|
+
}
|
|
399
|
+
}, role: "button", tabIndex: 0, "aria-label": "Drop zone for network definition file", children: [_jsx(Box, { sx: {
|
|
400
|
+
alignItems: "center",
|
|
401
|
+
backgroundColor: (theme) => alpha(theme.palette.primary.main, 0.16),
|
|
402
|
+
borderRadius: 2,
|
|
403
|
+
color: "primary.main",
|
|
404
|
+
display: "flex",
|
|
405
|
+
height: 64,
|
|
406
|
+
justifyContent: "center",
|
|
407
|
+
marginBottom: 1,
|
|
408
|
+
width: 64,
|
|
409
|
+
}, children: _jsx(CloudUploadOutlinedIcon, { id: "import-network-modal-upload-icon", sx: { fontSize: "2rem" } }) }), _jsx(Typography, { id: "import-network-modal-drop-text", variant: "subtitle1", sx: { color: "primary.main", fontWeight: "bold" }, children: "Drag & drop a network definition" }), _jsxs(Typography, { variant: "body2", children: ["or ", _jsx(Box, { component: "button", id: "import-network-modal-browse-link", onClick: (event) => {
|
|
410
|
+
event.stopPropagation();
|
|
411
|
+
handleBrowseClick();
|
|
412
|
+
}, sx: {
|
|
413
|
+
background: "none",
|
|
414
|
+
border: "none",
|
|
415
|
+
color: "var(--bs-secondary)",
|
|
416
|
+
cursor: "pointer",
|
|
417
|
+
font: "inherit",
|
|
418
|
+
padding: 0,
|
|
419
|
+
textDecoration: "underline",
|
|
420
|
+
"&:hover": { color: "var(--bs-primary)", textDecoration: "none" },
|
|
421
|
+
}, children: "browse your files" })] }), _jsxs(Box, { id: "import-network-modal-file-types", sx: {
|
|
422
|
+
alignItems: "center",
|
|
423
|
+
border: "1px solid",
|
|
424
|
+
borderColor: "divider",
|
|
425
|
+
borderRadius: 999,
|
|
426
|
+
color: "text.secondary",
|
|
427
|
+
display: "inline-flex",
|
|
428
|
+
gap: 0.75,
|
|
429
|
+
marginTop: 1,
|
|
430
|
+
padding: "4px 12px",
|
|
431
|
+
}, children: [_jsx(InsertDriveFileOutlinedIcon, { sx: { fontSize: "1rem" } }), _jsx(Typography, { variant: "caption", children: "Accepts .json up to 5 MB" })] }), _jsx(VisuallyHiddenInput, { accept: ACCEPTED_MIME_TYPES, "aria-hidden": "true", "data-testid": "import-network-file-input", onChange: handleFileChange, onClick: (event) => event.stopPropagation(), ref: fileInputRef, tabIndex: -1, type: "file" })] }), _jsxs(Box, { id: "import-network-modal-file-restriction", sx: {
|
|
432
|
+
alignItems: "flex-start",
|
|
433
|
+
backgroundColor: (theme) => alpha(theme.palette.text.primary, 0.04),
|
|
434
|
+
border: "1px solid",
|
|
435
|
+
borderColor: "divider",
|
|
436
|
+
borderRadius: 2,
|
|
437
|
+
display: "flex",
|
|
438
|
+
gap: 1.5,
|
|
439
|
+
marginTop: 2,
|
|
440
|
+
padding: "14px 16px",
|
|
441
|
+
}, children: [_jsx(InfoOutlinedIcon, { sx: { color: "text.secondary", fontSize: "1.25rem", flexShrink: 0, marginTop: 0.25 } }), _jsxs(Typography, { variant: "caption", sx: { color: "text.secondary" }, children: ["Must be a JSON file previously exported from an ", _jsx("strong", { children: "Agent Network Designer" }), "\u2013created network. General Neuro SAN HOCON files are not currently supported."] })] })] }));
|
|
442
|
+
// Step 2: parsing/validation outcome — loading spinner, success summary, or error banner.
|
|
443
|
+
const renderReviewParsing = () => (_jsxs(_Fragment, { children: [_jsx(CircularProgress, { id: "import-network-modal-spinner", size: 48 }), _jsx(Typography, { id: "import-network-modal-parsing-text", variant: "subtitle1", sx: { fontWeight: "bold" }, children: `Parsing & validating ${fileExt}…` }), _jsx(Typography, { id: "import-network-modal-parsing-filename", variant: "body2", sx: { color: "text.secondary", fontFamily: "monospace" }, children: file?.name })] }));
|
|
444
|
+
const renderReviewSuccessBanner = () => (_jsxs(Box, { id: "import-network-modal-success-banner", sx: {
|
|
445
|
+
alignItems: "center",
|
|
446
|
+
backgroundColor: (theme) => alpha(theme.palette.success.main, 0.12),
|
|
447
|
+
border: (theme) => `1px solid ${alpha(theme.palette.success.main, 0.4)}`,
|
|
448
|
+
borderRadius: 2,
|
|
449
|
+
color: "var(--bs-green)",
|
|
450
|
+
display: "flex",
|
|
451
|
+
gap: 1.5,
|
|
452
|
+
padding: "14px 16px",
|
|
453
|
+
}, children: [_jsx(CheckCircleOutlineIcon, {}), _jsxs(Typography, { variant: "body2", children: [`Valid ${fileExt} — `, _jsx(Box, { component: "span", sx: { fontWeight: "bold" }, children: "parsed successfully" })] })] }));
|
|
454
|
+
const renderReviewFileInfo = () => (_jsxs(Box, { sx: {
|
|
455
|
+
alignItems: "center",
|
|
456
|
+
backgroundColor: (theme) => alpha(theme.palette.text.primary, 0.04),
|
|
457
|
+
border: "1px solid",
|
|
458
|
+
borderColor: "divider",
|
|
459
|
+
borderRadius: 2,
|
|
460
|
+
display: "flex",
|
|
461
|
+
gap: 1.5,
|
|
462
|
+
marginTop: 3,
|
|
463
|
+
padding: "12px 16px",
|
|
464
|
+
}, children: [_jsx(Box, { sx: {
|
|
465
|
+
alignItems: "center",
|
|
466
|
+
backgroundColor: (theme) => alpha(theme.palette.text.primary, 0.06),
|
|
467
|
+
borderRadius: 1.5,
|
|
468
|
+
color: "text.secondary",
|
|
469
|
+
display: "flex",
|
|
470
|
+
flexShrink: 0,
|
|
471
|
+
height: 44,
|
|
472
|
+
justifyContent: "center",
|
|
473
|
+
width: 44,
|
|
474
|
+
}, children: _jsx(InsertDriveFileOutlinedIcon, { id: "import-network-modal-file-icon", sx: { fontSize: "1.5rem" } }) }), _jsx(Tooltip, { title: file?.name ?? "", children: _jsxs(Box, { id: "import-network-modal-review-filename", sx: {
|
|
475
|
+
display: "flex",
|
|
476
|
+
flex: 1,
|
|
477
|
+
fontFamily: "monospace",
|
|
478
|
+
fontSize: 15,
|
|
479
|
+
minWidth: 0,
|
|
480
|
+
}, children: [_jsx(Box, { component: "span", sx: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: fileStem }), fileNameExt && (_jsx(Box, { component: "span", sx: { flexShrink: 0 }, children: `.${fileNameExt}` }))] }) }), _jsx(Typography, { id: "import-network-modal-review-filesize", variant: "body2", sx: {
|
|
481
|
+
color: "text.secondary",
|
|
482
|
+
flexShrink: 0,
|
|
483
|
+
fontSize: 15,
|
|
484
|
+
whiteSpace: "nowrap",
|
|
485
|
+
}, children: formatFileSize(file?.size ?? 0) })] }));
|
|
486
|
+
const renderReviewSummary = () => networkSummary && (_jsx(Box, { id: "import-network-modal-summary", sx: {
|
|
487
|
+
border: "1px solid",
|
|
488
|
+
borderColor: "divider",
|
|
489
|
+
borderRadius: 2,
|
|
490
|
+
display: "grid",
|
|
491
|
+
gridTemplateColumns: "1fr 1fr",
|
|
492
|
+
marginTop: 3,
|
|
493
|
+
overflow: "hidden",
|
|
494
|
+
}, children: [
|
|
495
|
+
{ label: "Agents", value: networkSummary.agents },
|
|
496
|
+
{ label: "Coded tools", value: networkSummary.codedTools },
|
|
497
|
+
{ label: "External agents", value: networkSummary.externalAgents },
|
|
498
|
+
{ label: "Front man", value: networkSummary.frontman, isFrontman: true },
|
|
499
|
+
].map((stat, index) => (_jsxs(Box, { sx: {
|
|
500
|
+
borderColor: "divider",
|
|
501
|
+
borderLeft: index % 2 === 1 ? "1px solid" : undefined,
|
|
502
|
+
borderTop: index >= 2 ? "1px solid" : undefined,
|
|
503
|
+
padding: "14px 16px",
|
|
504
|
+
}, children: [_jsx(Typography, { variant: "caption", sx: {
|
|
505
|
+
color: "text.secondary",
|
|
506
|
+
letterSpacing: "0.08em",
|
|
507
|
+
textTransform: "uppercase",
|
|
508
|
+
}, children: stat.label }), _jsx(Typography, { sx: {
|
|
509
|
+
fontFamily: "isFrontman" in stat ? "monospace" : undefined,
|
|
510
|
+
fontSize: "isFrontman" in stat ? 15 : 18,
|
|
511
|
+
lineHeight: 1.4,
|
|
512
|
+
marginTop: 0.5,
|
|
513
|
+
wordBreak: "break-word",
|
|
514
|
+
}, children: stat.value })] }, stat.label))) }));
|
|
515
|
+
const renderReviewSuccess = () => (_jsxs(Box, { sx: { width: "100%" }, children: [renderReviewSuccessBanner(), renderReviewFileInfo(), renderReviewSummary()] }));
|
|
516
|
+
const renderReviewError = () => (_jsxs(Box, { id: "import-network-modal-error-banner", sx: {
|
|
517
|
+
alignItems: "center",
|
|
518
|
+
backgroundColor: "error.dark",
|
|
519
|
+
borderRadius: 1,
|
|
520
|
+
color: "error.contrastText",
|
|
521
|
+
display: "flex",
|
|
522
|
+
gap: 1,
|
|
523
|
+
padding: "10px 14px",
|
|
524
|
+
width: "100%",
|
|
525
|
+
}, children: [_jsx(ErrorOutlineIcon, { fontSize: "small" }), _jsx(Typography, { variant: "body2", children: parseError })] }));
|
|
526
|
+
const renderReviewStep = () => (_jsxs(Box, { id: "import-network-modal-review", sx: {
|
|
527
|
+
alignItems: "center",
|
|
528
|
+
display: "flex",
|
|
529
|
+
flexDirection: "column",
|
|
530
|
+
gap: 2,
|
|
531
|
+
marginTop: 4,
|
|
532
|
+
minHeight: "220px",
|
|
533
|
+
}, children: [parseState === "loading" && renderReviewParsing(), parseState === "success" && renderReviewSuccess(), parseState === "error" && renderReviewError()] }));
|
|
534
|
+
// Step 3: name the network and resolve any collision with an existing one.
|
|
535
|
+
const renderConflictKeepBothField = () => (_jsxs(Box, { children: [_jsx(Typography, { id: "import-network-modal-name-label", variant: "caption", sx: {
|
|
536
|
+
color: "text.secondary",
|
|
537
|
+
fontWeight: "bold",
|
|
538
|
+
letterSpacing: "0.08em",
|
|
539
|
+
textTransform: "uppercase",
|
|
540
|
+
}, children: "New network name" }), _jsx(TextField, { id: "import-network-modal-name-input", fullWidth: true, onChange: (event) => setNetworkName(event.target.value), size: "small", sx: { marginTop: 0.5 }, value: networkName }), newNameHasConflict ? (_jsxs(Box, { id: "import-network-modal-name-taken", sx: { alignItems: "center", display: "flex", gap: 0.5, marginTop: 0.75 }, children: [_jsx(WarningAmberIcon, { fontSize: "small", sx: { color: "warning.main", flexShrink: 0 } }), _jsx(Typography, { variant: "body2", sx: { color: "warning.main", fontWeight: "bold" }, children: "That name is taken. Choose a new name to keep both networks." })] })) : (trimmedName !== "" && (_jsxs(Box, { id: "import-network-modal-name-available", sx: { alignItems: "center", display: "flex", gap: 0.5, marginTop: 0.75 }, children: [_jsx(CheckCircleOutlineIcon, { fontSize: "small", sx: { color: "success.main", flexShrink: 0 } }), _jsx(Typography, { variant: "body2", sx: { color: "success.main", fontWeight: "bold" }, children: "Name is available." })] })))] }));
|
|
541
|
+
const renderConflictReplaceWarning = () => (_jsxs(Box, { id: "import-network-modal-replace-warning", sx: {
|
|
542
|
+
alignItems: "center",
|
|
543
|
+
backgroundColor: (theme) => alpha(theme.palette.error.main, 0.12),
|
|
544
|
+
borderRadius: 1,
|
|
545
|
+
borderStyle: "solid",
|
|
546
|
+
borderWidth: "1px",
|
|
547
|
+
borderColor: "error.main",
|
|
548
|
+
display: "flex",
|
|
549
|
+
gap: 1,
|
|
550
|
+
padding: "10px 14px",
|
|
551
|
+
}, children: [_jsx(WarningAmberIcon, { fontSize: "small", sx: { color: "error.main", flexShrink: 0 } }), _jsxs(Typography, { variant: "body2", children: [_jsxs("strong", { children: ["\"", importedName, "\""] }), " will be ", _jsx("strong", { children: "permanently overwritten" }), ". This can't be undone."] })] }));
|
|
552
|
+
const renderConflictResolution = () => (_jsxs(_Fragment, { children: [_jsxs(Box, { id: "import-network-modal-conflict-prompt", children: [_jsx(Typography, { variant: "caption", sx: {
|
|
553
|
+
color: "text.secondary",
|
|
554
|
+
fontWeight: "bold",
|
|
555
|
+
letterSpacing: "0.08em",
|
|
556
|
+
textTransform: "uppercase",
|
|
557
|
+
}, children: "Name conflict" }), _jsxs(Typography, { variant: "body2", sx: { marginTop: 0.5 }, children: ["A network named ", _jsxs("strong", { children: ["\"", importedName, "\""] }), " already exists. How would you like to handle it?"] })] }), _jsxs(ToggleButtonGroup, { id: "import-network-modal-conflict-toggle", exclusive: true, fullWidth: true, onChange: (_, value) => {
|
|
558
|
+
if (value !== null)
|
|
559
|
+
handleConflictResolutionChange(value);
|
|
560
|
+
}, value: conflictResolution, sx: {
|
|
561
|
+
"& .MuiToggleButton-root": {
|
|
562
|
+
textTransform: "none",
|
|
563
|
+
},
|
|
564
|
+
"& #import-network-modal-keep-both-btn.Mui-selected": {
|
|
565
|
+
backgroundColor: "primary.main",
|
|
566
|
+
color: "primary.contrastText",
|
|
567
|
+
"&:hover": { backgroundColor: "primary.dark" },
|
|
568
|
+
},
|
|
569
|
+
"& #import-network-modal-replace-existing-btn.Mui-selected": {
|
|
570
|
+
backgroundColor: "error.main",
|
|
571
|
+
color: "error.contrastText",
|
|
572
|
+
"&:hover": { backgroundColor: "error.dark" },
|
|
573
|
+
},
|
|
574
|
+
}, children: [_jsx(ToggleButton, { id: "import-network-modal-keep-both-btn", value: "keep-both", children: "Keep both" }), _jsx(ToggleButton, { id: "import-network-modal-replace-existing-btn", value: "replace", children: "Replace existing" })] }), conflictResolution === "keep-both" ? renderConflictKeepBothField() : renderConflictReplaceWarning()] }));
|
|
575
|
+
const renderNameField = () => (_jsxs(Box, { children: [_jsx(Typography, { id: "import-network-modal-name-label", variant: "caption", sx: {
|
|
576
|
+
color: "text.secondary",
|
|
577
|
+
fontWeight: "bold",
|
|
578
|
+
letterSpacing: "0.08em",
|
|
579
|
+
textTransform: "uppercase",
|
|
580
|
+
}, children: "Network name" }), _jsx(TextField, { id: "import-network-modal-name-input", error: newNameHasConflict, fullWidth: true, helperText: newNameHasConflict
|
|
581
|
+
? "That name is taken. Pick another to continue."
|
|
582
|
+
: "Pulled from the filename — edit if you like.", onChange: (event) => setNetworkName(event.target.value), size: "small", sx: { marginTop: 0.5 }, value: networkName })] }));
|
|
583
|
+
const renderConfirmStep = () => (_jsx(Box, { id: "import-network-modal-confirm", sx: { display: "flex", flexDirection: "column", gap: 2, marginTop: 3 }, children: importedNameHasConflict ? renderConflictResolution() : renderNameField() }));
|
|
584
|
+
return (_jsxs(MUIDialog, { id: "import-network-modal", isOpen: isOpen, onClose: onClose, title: "Import network definition", paperProps: { minWidth: "560px" }, footer: renderFooter(), children: [renderStepper(), activeStep === 0 && renderSelectFileStep(), activeStep === 1 && renderReviewStep(), activeStep === 2 && renderConfirmStep()] }));
|
|
585
|
+
};
|