import { AimOutlined, EditOutlined, MinusCircleOutlined, PlusOutlined } from "@ant-design/icons";
import {
AutoComplete,
Button,
Checkbox,
Divider,
Flex,
Form,
Input,
InputNumber,
Select,
Space,
Switch,
} from "antd";
import TextArea from "antd/es/input/TextArea";
import { DefaultOptionType } from "antd/es/select";
import { FC, useEffect, useMemo } from "react";
import { useTranslation } from "react-i18next";
import Markdown from "react-markdown";
import { useDebounceCallback } from "usehooks-ts";
import { useShallow } from "zustand/react/shallow";
import { EditNode, EditTree, useWorkspace } from "../contexts/workspace-context";
import {
GroupDef,
ImportDef,
isBoolType,
isEnumType,
isExprType,
isFloatType,
isIntType,
isJsonType,
isStringType,
NodeModel,
TreeGraphData,
VarDef,
} from "../misc/b3type";
import {
checkNodeArgValue,
checkOneof,
getNodeArgRawType,
isNodeArgArray,
isNodeArgOptional,
isValidVariableName,
isVariadic,
loadVarDef,
parseExpr,
usingGroups,
usingVars,
} from "../misc/b3util";
import { Hotkey, isMacos } from "../misc/keys";
import { mergeClassNames } from "../misc/util";
interface OptionType extends DefaultOptionType {
value: string;
}
const TreeInspector: FC = () => {
const workspace = useWorkspace(
useShallow((state) => ({
editing: state.editing,
editingTree: state.editingTree!,
allFiles: state.allFiles,
relative: state.relative,
fileTree: state.fileTree,
groupDefs: state.groupDefs,
}))
);
const { t } = useTranslation();
const [form] = Form.useForm();
// group defs
const groupDefs: GroupDef[] = useMemo(() => {
return workspace.groupDefs.map((v) => ({
name: v,
value: workspace.editingTree.data.group.includes(v),
}));
}, [workspace.groupDefs, workspace.editingTree]);
// auto complete for subtree
const subtreeOptions = useMemo(() => {
const options: OptionType[] = [];
workspace.allFiles.forEach((file) => {
const value = workspace.relative(file.path);
const desc = ""; //fileNode.desc ? `(${fileNode.desc})` : "";
options.push({
label: `${value}${desc}`,
value: value,
});
});
options.sort((a, b) => a.value.localeCompare(b.value));
return options;
}, [workspace.allFiles, workspace.fileTree]);
// set form values
useEffect(() => {
const data = workspace.editingTree.data;
loadVarDef(data.import);
form.resetFields();
form.setFieldValue("name", data.name);
form.setFieldValue("desc", data.desc);
form.setFieldValue("export", data.export !== false);
form.setFieldValue("firstid", data.firstid);
form.setFieldValue("group", groupDefs);
form.setFieldValue("declvar", data.declvar);
form.setFieldValue("import", data.import);
form.setFieldValue("subtree", data.subtree);
}, [workspace.editingTree, groupDefs]);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const finish = (values: any) => {
workspace.editing?.dispatch("updateTree", {
data: {
name: values.name,
desc: values.desc,
export: values.export,
firstid: Number(values.firstid),
group: (values.group as GroupDef[])
.map((v) => (v.value ? v.name : undefined))
.filter((v) => v)
.sort(),
declvar: (values.declvar as VarDef[])
.filter((v) => v && v.name)
.sort((a, b) => a.name.localeCompare(b.name)),
import: (values.import as ImportDef[])
.filter((v) => v && v.path)
.sort((a, b) => a.path.localeCompare(b.path))
.map((v) => ({
path: v.path,
vars: v.vars ?? [],
})),
},
} as EditTree);
};
return (
<>
{t("tree.overview")}
)}
>
{workspace.editingTree.data.subtree.length > 0 && (
<>
{t("tree.vars.subtree")}
{(vars) => (
)}
>
)}
<>
{t("tree.vars.imports")}
{(items, { add, remove }, { errors }) => (
{items.map((item) => (
{
remove(item.name);
form.submit();
}}
/>
{(vars) => (
)}
))}
)}
>
>
);
};
const NodeInspector: FC = () => {
const workspace = useWorkspace(
useShallow((state) => ({
editing: state.editing,
editingNode: state.editingNode!,
nodeDefs: state.nodeDefs,
allFiles: state.allFiles,
fileTree: state.fileTree,
relative: state.relative,
onEditingNode: state.onEditingNode,
}))
);
const { t } = useTranslation();
const [form] = Form.useForm();
const validateFieldsLater = useDebounceCallback(
() => form.validateFields({ recursive: true }),
100
);
// set form values
useEffect(() => {
const data = workspace.editingNode.data;
const def = workspace.nodeDefs.get(workspace.editingNode.data.name);
form.resetFields();
form.setFieldValue("id", data.id);
form.setFieldValue("name", data.name);
form.setFieldValue("type", def.type);
form.setFieldValue("desc", data.desc || def.desc);
form.setFieldValue("debug", data.debug);
form.setFieldValue("disabled", data.disabled);
form.setFieldValue("path", data.path);
if (def.children === undefined || def.children === -1) {
form.setFieldValue("children", t("node.children.unlimited"));
} else {
form.setFieldValue("children", def.children);
}
def.args?.forEach((arg) => {
const type = getNodeArgRawType(arg);
const value = data.args?.[arg.name];
const name = `args.${arg.name}`;
if (isNodeArgArray(arg)) {
form.setFieldValue(
name,
(Array.isArray(value) ? value : []).map((item) => {
if (isJsonType(type)) {
return item === null ? "null" : JSON.stringify(item ?? arg.default, null, 2);
} else {
return item;
}
})
);
} else if (isJsonType(type)) {
form.setFieldValue(
name,
value === null ? "null" : JSON.stringify(value ?? arg.default, null, 2)
);
} else {
form.setFieldValue(name, value ?? arg.default);
}
});
def.input?.forEach((_, i) => {
if (isVariadic(def.input!, i)) {
form.setFieldValue(`input.${i}`, data.input?.slice(i) ?? []);
} else {
form.setFieldValue(`input.${i}`, data.input?.[i]);
}
});
def.output?.forEach((_, i) => {
if (isVariadic(def.output!, i)) {
form.setFieldValue(`output.${i}`, data.output?.slice(i) ?? []);
} else {
form.setFieldValue(`output.${i}`, data.output?.[i]);
}
});
validateFieldsLater();
}, [workspace.editingNode]);
// auto complete for node
const nodeOptions = useMemo(() => {
const options: OptionType[] = [];
workspace.nodeDefs.forEach((e) => {
options.push({ label: `${e.name}(${e.desc})`, value: e.name });
});
return options;
}, [workspace.nodeDefs]);
// auto complete for input and output
const inoutVarOptions = useMemo(() => {
const options: OptionType[] = [];
const filter: Record = {};
const collect = (node?: TreeGraphData) => {
if (node) {
node.input?.forEach((v, i) => {
let desc: string;
const inputDef = node.def.input;
if (inputDef && i >= inputDef.length && isVariadic(inputDef, -1)) {
desc = inputDef[inputDef.length - 1];
} else {
desc = inputDef?.[i] ?? "";
}
if (!filter[v]) {
options.push({ label: `${v}(${desc})`, value: v });
filter[v] = true;
}
});
node.output?.forEach((v, i) => {
let desc: string;
const outputDef = node.def.output;
if (outputDef && i >= outputDef.length && isVariadic(outputDef, -1)) {
desc = outputDef[outputDef.length - 1];
} else {
desc = outputDef?.[i] ?? "";
}
if (!filter[v]) {
options.push({ label: `${v}(${desc})`, value: v });
filter[v] = true;
}
});
node.children?.forEach((child) => collect(child));
}
};
if (usingVars) {
Object.values(usingVars ?? {}).forEach((v) => {
if (!filter[v.name]) {
options.push({ label: `${v.name}(${v.desc})`, value: v.name });
filter[v.name] = true;
}
});
} else {
collect(workspace.editing?.root);
}
return options;
}, [workspace.editing, usingVars]);
// auto complete for subtree
const subtreeOptions = useMemo(() => {
const options: OptionType[] = [];
workspace.allFiles.forEach((file) => {
const value = workspace.relative(file.path);
const desc = ""; //fileNode.desc ? `(${fileNode.desc})` : "";
options.push({
label: `${value}${desc}`,
value: value,
});
});
options.sort((a, b) => a.value.localeCompare(b.value));
return options;
}, [workspace.allFiles, workspace.fileTree]);
const editingNode = workspace.editingNode;
const def = workspace.nodeDefs.get(editingNode.data.name);
const disabled = !editingNode.editable;
// update value
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const finish = (values: any) => {
const data = {} as NodeModel;
data.id = editingNode.data.id;
data.name = values.name;
data.debug = values.debug || undefined;
data.disabled = values.disabled || undefined;
data.desc = values.desc && values.desc !== def.desc ? values.desc : undefined;
data.path = values.path || undefined;
if (def.args?.length) {
def.args?.forEach((arg) => {
const value = values[`args.${arg.name}`];
if (value !== null && value !== undefined && value !== "") {
data.args ||= {};
const type = getNodeArgRawType(arg);
if (isNodeArgArray(arg)) {
const arr: unknown[] = [];
if (Array.isArray(value)) {
value.forEach((item) => {
if (isJsonType(type)) {
try {
arr.push(item === "null" ? null : JSON.parse(item));
} catch {
/** ignore */
}
} else if (item !== null && item !== undefined) {
arr.push(item);
}
});
}
data.args[arg.name] = arr;
} else if (isJsonType(type)) {
try {
data.args[arg.name] = value === "null" ? null : JSON.parse(value);
} catch {
/** ignore */
}
} else {
data.args[arg.name] = value;
}
}
});
} else {
data.args = {};
}
if (def.input?.length) {
def.input?.forEach((_, i) => {
data.input ||= [];
if (isVariadic(def.input!, i)) {
const arr = (values[`input.${i}`] ?? []) as string[];
data.input.push(...arr.filter((v) => typeof v === "string"));
} else {
const v = values[`input.${i}`];
data.input.push(v ?? "");
}
});
} else {
data.input = [];
}
if (def.output?.length) {
def.output?.forEach((_, i) => {
data.output ||= [];
if (isVariadic(def.output!, i)) {
const arr = (values[`output.${i}`] ?? []) as string[];
data.output.push(...arr.filter((v) => typeof v === "string"));
} else {
const v = values[`output.${i}`];
data.output.push(v ?? "");
}
});
} else {
data.output = [];
}
workspace.editing?.dispatch("updateNode", {
data: data,
} as EditNode);
};
// change node def
const changeNodeDef = (newname: string) => {
if (editingNode.data.name !== newname) {
workspace.onEditingNode({
data: {
id: editingNode.data.id,
name: workspace.nodeDefs.get(newname)?.name ?? newname,
desc: editingNode.data.desc,
debug: editingNode.data.debug,
disabled: editingNode.data.disabled,
},
editable: editingNode.editable,
});
finish(form.getFieldsValue());
} else {
form.submit();
}
};
const changeSubtree = () => {
if (form.getFieldValue("path") !== editingNode.data.path) {
finish(form.getFieldsValue());
} else {
form.submit();
}
};
return (
<>
{def.desc}
usingGroups[g])) {
return Promise.reject(new Error(t("node.invalidGroup", { group: def.group })));
}
return Promise.resolve();
},
},
]}
>
changeNodeDef(form.getFieldValue("name"))}
onSelect={changeNodeDef}
onInputKeyDown={(e) => e.code === Hotkey.Escape && e.preventDefault()}
filterOption={(inputValue: string, option?: OptionType) => {
const label = option!.label as string;
return label.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1;
}}
/>
e.code === Hotkey.Escape && e.preventDefault()}
filterOption={(inputValue: string, option?: OptionType) => {
const label = option!.label as string;
return label.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1;
}}
/>
{def.doc}
{def.input && def.input.length > 0 && (
<>
{t("node.inputVariable")}
{def.input.map((v, i) => {
const required = v.indexOf("?") === -1;
const desc = v.replace("?", "");
if (isVariadic(def.input!, i)) {
return (
{(fields, { add, remove }, { errors }) => (
{fields.map((field) => (
e.code === Hotkey.Escape && e.preventDefault()
}
filterOption={(inputValue: string, option?: OptionType) => {
const label = option!.label as string;
return (
label.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1
);
}}
/>
{
remove(field.name);
form.submit();
}}
/>
))}
)}
);
} else {
return (
({
validator(_, value) {
if (value && usingVars && !usingVars[value]) {
return Promise.reject(
new Error(t("node.undefinedVariable", { variable: value }))
);
}
if (value && !isValidVariableName(value)) {
return Promise.reject(new Error(t("node.invalidVariableName")));
}
const arg = def.args?.find(
(a) => a.oneof && v.replace("?", "") === a.oneof
);
if (arg) {
const argName = `args.${arg.name}`;
if (!isFieldValidating(argName)) {
setFieldValue(`input.${i}`, value);
validateFields([argName]);
}
if (!checkOneof(getFieldValue(argName) ?? "", value)) {
return Promise.reject(
new Error(
t("node.oneof.error", {
input: v,
arg: arg.name,
desc: arg.desc ?? "",
})
)
);
} else {
return Promise.resolve();
}
}
return Promise.resolve();
},
}),
]}
>
e.code === Hotkey.Escape && e.preventDefault()}
filterOption={(inputValue: string, option?: OptionType) => {
const label = option!.label as string;
return label.toUpperCase().includes(inputValue.toUpperCase());
}}
/>
);
}
})}
>
)}
{def.args && def.args.length > 0 && (
<>
{t("node.args")}
{def.args.map((arg) => {
const required = !isNodeArgOptional(arg);
const type = getNodeArgRawType(arg);
if (isNodeArgArray(arg)) {
return (
{(items, { add, remove }, { errors }) => (
{items.map((item, idx) => (
({
validator(_, value) {
if (usingVars && isExprType(type) && value) {
for (const v of parseExpr(value)) {
if (!usingVars![v]) {
return Promise.reject(
new Error(
t("node.undefinedVariable", { variable: v })
)
);
}
}
}
if (isJsonType(type)) {
try {
if (value !== "null") {
JSON.parse(value);
}
} catch (e) {
return Promise.reject(
new Error(t("node.invalidValue"))
);
}
} else if (
!checkNodeArgValue(editingNode.data, arg, value, true)
) {
return Promise.reject(new Error(t("node.invalidValue")));
}
return Promise.resolve();
},
}),
]}
>
{isStringType(type) && (
)}
{isJsonType(type) && (
)}
{isIntType(type) && (
)}
{isFloatType(type) && (
)}
{isBoolType(type) && (
)}
{isExprType(type) && (
)}
{isEnumType(type) && (
{
remove(item.name);
form.submit();
}}
/>
))}
)}
);
} else {
return (
({
validator(_, value) {
if (usingVars && isExprType(type) && value) {
for (const v of parseExpr(value)) {
if (!usingVars![v]) {
return Promise.reject(
new Error(t("node.undefinedVariable", { variable: v }))
);
}
}
}
if (value && isJsonType(type)) {
try {
if (value !== "null") {
JSON.parse(value);
}
} catch (e) {
return Promise.reject(new Error(t("node.invalidValue")));
}
} else if (value === null && !required) {
value = undefined;
}
if (!checkNodeArgValue(editingNode.data, arg, value)) {
return Promise.reject(new Error(t("node.invalidValue")));
}
if (!arg.oneof) {
return Promise.resolve();
}
const idx = def.input?.findIndex(
(input) => input.replace("?", "") === arg.oneof
);
if (idx === undefined || idx < 0) {
return Promise.reject(
new Error(t("node.oneof.inputNotfound", { input: arg.oneof }))
);
}
const inputName = `input.${idx}`;
if (!isFieldValidating(inputName)) {
setFieldValue(`args.${arg.name}`, value);
validateFields([inputName]);
}
if (!checkOneof(getFieldValue(inputName) ?? "", value)) {
return Promise.reject(
new Error(
t("node.oneof.error", {
input: def.input![idx],
arg: arg.name,
desc: arg.desc ?? "",
})
)
);
} else {
return Promise.resolve();
}
},
}),
]}
>
{isStringType(type) && (
)}
{isJsonType(type) && (
)}
{isIntType(type) && (
)}
{isFloatType(type) && (
)}
{isBoolType(type) && }
{isExprType(type) && }
{isEnumType(type) && (
);
}
})}
>
)}
{def.output && def.output.length > 0 && (
<>
{t("node.outputVariable")}
{def.output.map((v, i) => {
const required = v.indexOf("?") === -1;
const desc = v.replace("?", "");
if (isVariadic(def.output!, i)) {
return (
{(fields, { add, remove }, { errors }) => (
{fields.map((field) => (
e.code === Hotkey.Escape && e.preventDefault()
}
filterOption={(value: string, option?: OptionType) => {
const label = option!.label as string;
return (
label.toUpperCase().indexOf(value.toUpperCase()) !== -1
);
}}
/>
{
remove(field.name);
form.submit();
}}
/>
))}
)}
);
} else {
return (
e.code === Hotkey.Escape && e.preventDefault()}
filterOption={(value: string, option?: OptionType) => {
const label = option!.label as string;
return label.toUpperCase().indexOf(value.toUpperCase()) !== -1;
}}
/>
);
}
})}
>
)}
{disabled && (
}
onClick={() => workspace.editing?.dispatch("editSubtree")}
>
{t("editSubtree")}
)}
>
);
};
const NodeDefInspector: FC = () => {
const workspace = useWorkspace(
useShallow((state) => ({
editingNodeDef: state.editingNodeDef!,
}))
);
const { t } = useTranslation();
const [form] = Form.useForm();
const def = workspace.editingNodeDef.data;
// set form values
useEffect(() => {
form.resetFields();
form.setFieldValue("name", def.name);
form.setFieldValue("type", def.type);
form.setFieldValue("desc", def.desc);
form.setFieldValue("doc", def.doc);
if (def.children === undefined || def.children === -1) {
form.setFieldValue("children", t("node.children.unlimited"));
} else {
form.setFieldValue("children", def.children);
}
def.input?.forEach((v, i) => {
form.setFieldValue(`input.${i}.name`, v.replaceAll("?", ""));
});
def.output?.forEach((v, i) => {
form.setFieldValue(`output.${i}.name`, v.replaceAll("?", ""));
});
def.args?.forEach((v, i) => {
form.setFieldValue(`args.${i}.type`, v.type.replaceAll("?", ""));
});
}, [workspace.editingNodeDef]);
return (
<>
{t("nodeDefinition")}
>
);
};
export const Inspector: FC = () => {
const workspace = {
editingNode: useWorkspace((state) => state.editingNode),
editingTree: useWorkspace((state) => state.editingTree),
editingNodeDef: useWorkspace((state) => state.editingNodeDef),
};
let isEditingNode = false;
let isEditingTree = false;
let isEditingNodeDef = false;
if (workspace.editingNodeDef) {
isEditingNodeDef = true;
} else if (workspace.editingTree) {
isEditingTree = true;
} else if (workspace.editingNode) {
isEditingNode = true;
}
return (
{isEditingNodeDef && }
{isEditingTree && }
{isEditingNode && }
);
};