407 lines
13 KiB
TypeScript
407 lines
13 KiB
TypeScript
import { cloneDeep } from "lodash";
|
|
import React, {
|
|
ReactNode,
|
|
useContext,
|
|
useEffect,
|
|
useRef,
|
|
useState,
|
|
} from "react";
|
|
import { Handle, Position, useUpdateNodeInternals } from "reactflow";
|
|
import ShadTooltip from "../../../../components/ShadTooltipComponent";
|
|
import CodeAreaComponent from "../../../../components/codeAreaComponent";
|
|
import DictComponent from "../../../../components/dictComponent";
|
|
import Dropdown from "../../../../components/dropdownComponent";
|
|
import FloatComponent from "../../../../components/floatComponent";
|
|
import IconComponent from "../../../../components/genericIconComponent";
|
|
import InputComponent from "../../../../components/inputComponent";
|
|
import InputFileComponent from "../../../../components/inputFileComponent";
|
|
import InputListComponent from "../../../../components/inputListComponent";
|
|
import IntComponent from "../../../../components/intComponent";
|
|
import KeypairListComponent from "../../../../components/keypairListComponent";
|
|
import PromptAreaComponent from "../../../../components/promptComponent";
|
|
import TextAreaComponent from "../../../../components/textAreaComponent";
|
|
import ToggleShadComponent from "../../../../components/toggleShadComponent";
|
|
import { TOOLTIP_EMPTY } from "../../../../constants/constants";
|
|
import { TabsContext } from "../../../../contexts/tabsContext";
|
|
import { typesContext } from "../../../../contexts/typesContext";
|
|
import { ParameterComponentType } from "../../../../types/components";
|
|
import { TabsState } from "../../../../types/tabs";
|
|
import {
|
|
convertArrayToObj,
|
|
convertObjToArray,
|
|
hasDuplicateKeys,
|
|
isValidConnection,
|
|
} from "../../../../utils/reactflowUtils";
|
|
import {
|
|
nodeColors,
|
|
nodeIconsLucide,
|
|
nodeNames,
|
|
} from "../../../../utils/styleUtils";
|
|
import { classNames, groupByFamily } from "../../../../utils/utils";
|
|
|
|
export default function ParameterComponent({
|
|
left,
|
|
id,
|
|
data,
|
|
setData,
|
|
tooltipTitle,
|
|
title,
|
|
color,
|
|
type,
|
|
name = "",
|
|
required = false,
|
|
optionalHandle = null,
|
|
info = "",
|
|
}: ParameterComponentType): JSX.Element {
|
|
const ref = useRef<HTMLDivElement>(null);
|
|
const refHtml = useRef<HTMLDivElement & ReactNode>(null);
|
|
const infoHtml = useRef<HTMLDivElement & ReactNode>(null);
|
|
const updateNodeInternals = useUpdateNodeInternals();
|
|
const [position, setPosition] = useState(0);
|
|
const { setTabsState, tabId, save, flows } = useContext(TabsContext);
|
|
|
|
const flow = flows.find((flow) => flow.id === tabId)?.data?.nodes ?? null;
|
|
|
|
// Update component position
|
|
useEffect(() => {
|
|
if (ref.current && ref.current.offsetTop && ref.current.clientHeight) {
|
|
setPosition(ref.current.offsetTop + ref.current.clientHeight / 2);
|
|
updateNodeInternals(data.id);
|
|
}
|
|
}, [data.id, ref, ref.current, ref.current?.offsetTop, updateNodeInternals]);
|
|
|
|
useEffect(() => {
|
|
updateNodeInternals(data.id);
|
|
}, [data.id, position, updateNodeInternals]);
|
|
|
|
const { reactFlowInstance } = useContext(typesContext);
|
|
let disabled =
|
|
reactFlowInstance?.getEdges().some((edge) => edge.targetHandle === id) ??
|
|
false;
|
|
|
|
const { data: myData } = useContext(typesContext);
|
|
|
|
const handleOnNewValue = (newValue: string | string[] | boolean): void => {
|
|
let newData = cloneDeep(data);
|
|
newData.node!.template[name].value = newValue;
|
|
setData(newData);
|
|
// Set state to pending
|
|
//@ts-ignore
|
|
setTabsState((prev: TabsState) => {
|
|
return {
|
|
...prev,
|
|
[tabId]: {
|
|
...prev[tabId],
|
|
isPending: true,
|
|
formKeysData: prev[tabId].formKeysData,
|
|
},
|
|
};
|
|
});
|
|
renderTooltips();
|
|
};
|
|
|
|
const [obj, setObj] = useState({
|
|
arr: ["test", 123456, false, null],
|
|
boolean: false,
|
|
longString:
|
|
"long string long string long string long string long string long string",
|
|
number: 123456,
|
|
try: {
|
|
k1: 123,
|
|
k2: "123",
|
|
k3: false,
|
|
},
|
|
string: "string",
|
|
});
|
|
|
|
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
|
|
const [dict, setDict] = useState({} as {});
|
|
const [dictArr, setDictArr] = useState([] as string[]);
|
|
|
|
useEffect(() => {
|
|
setDictArr(convertObjToArray(dict));
|
|
}, [dict]);
|
|
|
|
useEffect(() => {
|
|
if (name === "openai_api_base") console.log(info);
|
|
// @ts-ignore
|
|
infoHtml.current = (
|
|
<div className="h-full w-full break-words">
|
|
{info.split("\n").map((line, index) => (
|
|
<p key={index} className="block">
|
|
{line}
|
|
</p>
|
|
))}
|
|
</div>
|
|
);
|
|
}, [info]);
|
|
|
|
function renderTooltips() {
|
|
let groupedObj = groupByFamily(myData, tooltipTitle!, left, flow!);
|
|
|
|
if (groupedObj && groupedObj.length > 0) {
|
|
//@ts-ignore
|
|
//@ts-ignore
|
|
refHtml.current = groupedObj.map((item, index) => {
|
|
const Icon: any =
|
|
nodeIconsLucide[item.family] ?? nodeIconsLucide["unknown"];
|
|
|
|
return (
|
|
<span
|
|
key={index}
|
|
className={classNames(
|
|
index > 0 ? "mt-2 flex items-center" : "flex items-center"
|
|
)}
|
|
>
|
|
<div
|
|
className="h-5 w-5"
|
|
style={{
|
|
color: nodeColors[item.family],
|
|
}}
|
|
>
|
|
<Icon
|
|
className="h-5 w-5"
|
|
strokeWidth={1.5}
|
|
style={{
|
|
color: nodeColors[item.family] ?? nodeColors.unknown,
|
|
}}
|
|
/>
|
|
</div>
|
|
<span className="ps-2 text-xs text-foreground">
|
|
{nodeNames[item.family] ?? "Other"}
|
|
<span className="text-xs">
|
|
{" "}
|
|
{item.type === "" ? "" : " - "}
|
|
{item.type.split(", ").length > 2
|
|
? item.type.split(", ").map((el, index) => (
|
|
<React.Fragment key={el + index}>
|
|
<span>
|
|
{index === item.type.split(", ").length - 1
|
|
? el
|
|
: (el += `, `)}
|
|
</span>
|
|
</React.Fragment>
|
|
))
|
|
: item.type}
|
|
</span>
|
|
</span>
|
|
</span>
|
|
);
|
|
});
|
|
} else {
|
|
//@ts-ignore
|
|
refHtml.current = <span>{TOOLTIP_EMPTY}</span>;
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
renderTooltips();
|
|
}, [tooltipTitle, flow]);
|
|
|
|
return (
|
|
<div
|
|
ref={ref}
|
|
className="mt-1 flex w-full flex-wrap items-center justify-between bg-muted px-5 py-2"
|
|
>
|
|
<>
|
|
<div
|
|
className={
|
|
"w-full truncate text-sm" +
|
|
(left ? "" : " text-end") +
|
|
(info !== "" ? " flex items-center" : "")
|
|
}
|
|
>
|
|
{title}
|
|
<span className="text-status-red">{required ? " *" : ""}</span>
|
|
<div className="">
|
|
{info !== "" && (
|
|
<ShadTooltip content={infoHtml.current}>
|
|
{/* put div to avoid bug that does not display tooltip */}
|
|
<div>
|
|
<IconComponent
|
|
name="Info"
|
|
className="relative bottom-0.5 ml-2 h-3 w-4"
|
|
/>
|
|
</div>
|
|
</ShadTooltip>
|
|
)}
|
|
</div>
|
|
</div>
|
|
{left &&
|
|
(type === "str" ||
|
|
type === "bool" ||
|
|
type === "float" ||
|
|
type === "code" ||
|
|
type === "prompt" ||
|
|
type === "file" ||
|
|
type === "int") &&
|
|
!optionalHandle ? (
|
|
<></>
|
|
) : left === true && type === "nested_dict" ? (
|
|
<div className="mt-2 w-full">
|
|
<DictComponent
|
|
disabled={disabled}
|
|
editNode={false}
|
|
value={obj}
|
|
onChange={(newValue) => {
|
|
setObj(newValue);
|
|
}}
|
|
/>
|
|
</div>
|
|
) : left === true && type === "dict" ? (
|
|
<div className="mt-2 w-full">
|
|
<KeypairListComponent
|
|
disabled={disabled}
|
|
editNode={false}
|
|
value={dictArr}
|
|
duplicateKey={errorDuplicateKey}
|
|
onChange={(newValue: string[]) => {
|
|
setErrorDuplicateKey(hasDuplicateKeys(newValue));
|
|
if (hasDuplicateKeys(newValue)) {
|
|
setDictArr(newValue);
|
|
} else {
|
|
setDict(convertArrayToObj(newValue));
|
|
}
|
|
}}
|
|
/>
|
|
</div>
|
|
) : (
|
|
<ShadTooltip
|
|
styleClasses={"tooltip-fixed-width custom-scroll nowheel"}
|
|
delayDuration={0}
|
|
content={refHtml.current}
|
|
side={left ? "left" : "right"}
|
|
>
|
|
<Handle
|
|
type={left ? "target" : "source"}
|
|
position={left ? Position.Left : Position.Right}
|
|
id={id}
|
|
isValidConnection={(connection) =>
|
|
isValidConnection(connection, reactFlowInstance!)
|
|
}
|
|
className={classNames(
|
|
left ? "-ml-0.5 " : "-mr-0.5 ",
|
|
"h-3 w-3 rounded-full border-2 bg-background"
|
|
)}
|
|
style={{
|
|
borderColor: color,
|
|
top: position,
|
|
}}
|
|
></Handle>
|
|
</ShadTooltip>
|
|
)}
|
|
|
|
{left === true &&
|
|
type === "str" &&
|
|
!data.node?.template[name].options ? (
|
|
<div className="mt-2 w-full">
|
|
{data.node?.template[name].list ? (
|
|
<InputListComponent
|
|
disabled={disabled}
|
|
value={
|
|
!data.node.template[name].value ||
|
|
data.node.template[name].value === ""
|
|
? [""]
|
|
: data.node.template[name].value
|
|
}
|
|
onChange={handleOnNewValue}
|
|
/>
|
|
) : data.node?.template[name].multiline ? (
|
|
<TextAreaComponent
|
|
disabled={disabled}
|
|
value={data.node.template[name].value ?? ""}
|
|
onChange={handleOnNewValue}
|
|
/>
|
|
) : (
|
|
<InputComponent
|
|
disabled={disabled}
|
|
password={data.node?.template[name].password ?? false}
|
|
value={data.node?.template[name].value ?? ""}
|
|
onChange={handleOnNewValue}
|
|
/>
|
|
)}
|
|
</div>
|
|
) : left === true && type === "bool" ? (
|
|
<div className="mt-2 w-full">
|
|
<ToggleShadComponent
|
|
disabled={disabled}
|
|
enabled={data.node?.template[name].value ?? false}
|
|
setEnabled={(isEnabled) => {
|
|
handleOnNewValue(isEnabled);
|
|
}}
|
|
size="large"
|
|
/>
|
|
</div>
|
|
) : left === true && type === "float" ? (
|
|
<div className="mt-2 w-full">
|
|
<FloatComponent
|
|
disabled={disabled}
|
|
value={data.node?.template[name].value ?? ""}
|
|
onChange={handleOnNewValue}
|
|
/>
|
|
</div>
|
|
) : left === true &&
|
|
type === "str" &&
|
|
data.node?.template[name].options ? (
|
|
<div className="mt-2 w-full">
|
|
<Dropdown
|
|
options={data.node.template[name].options}
|
|
onSelect={handleOnNewValue}
|
|
value={data.node.template[name].value ?? "Choose an option"}
|
|
></Dropdown>
|
|
</div>
|
|
) : left === true && type === "code" ? (
|
|
<div className="mt-2 w-full">
|
|
<CodeAreaComponent
|
|
dynamic={data.node?.template[name].dynamic ?? false}
|
|
setNodeClass={(nodeClass) => {
|
|
data.node = nodeClass;
|
|
}}
|
|
nodeClass={data.node}
|
|
disabled={disabled}
|
|
value={data.node?.template[name].value ?? ""}
|
|
onChange={handleOnNewValue}
|
|
/>
|
|
</div>
|
|
) : left === true && type === "file" ? (
|
|
<div className="mt-2 w-full">
|
|
<InputFileComponent
|
|
disabled={disabled}
|
|
value={data.node?.template[name].value ?? ""}
|
|
onChange={handleOnNewValue}
|
|
fileTypes={data.node?.template[name].fileTypes}
|
|
suffixes={data.node?.template[name].suffixes}
|
|
onFileChange={(filePath: string) => {
|
|
data.node!.template[name].file_path = filePath;
|
|
save();
|
|
}}
|
|
></InputFileComponent>
|
|
</div>
|
|
) : left === true && type === "int" ? (
|
|
<div className="mt-2 w-full">
|
|
<IntComponent
|
|
disabled={disabled}
|
|
value={data.node?.template[name].value ?? ""}
|
|
onChange={handleOnNewValue}
|
|
/>
|
|
</div>
|
|
) : left === true && type === "prompt" ? (
|
|
<div className="mt-2 w-full">
|
|
<PromptAreaComponent
|
|
field_name={name}
|
|
setNodeClass={(nodeClass) => {
|
|
data.node = nodeClass;
|
|
}}
|
|
nodeClass={data.node}
|
|
disabled={disabled}
|
|
value={data.node?.template[name].value ?? ""}
|
|
onChange={handleOnNewValue}
|
|
/>
|
|
</div>
|
|
) : (
|
|
<></>
|
|
)}
|
|
</>
|
|
</div>
|
|
);
|
|
}
|