Merge cz/mergeAll into shortcuts_settings
This commit is contained in:
commit
e531e36ad1
256 changed files with 19933 additions and 2731 deletions
13972
src/frontend/package-lock.json
generated
Normal file
13972
src/frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -22,6 +22,7 @@
|
|||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@radix-ui/react-switch": "^1.0.3",
|
||||
"@radix-ui/react-tabs": "^1.0.4",
|
||||
"@radix-ui/react-toggle": "^1.0.3",
|
||||
"@radix-ui/react-tooltip": "^1.0.6",
|
||||
"@tabler/icons-react": "^2.32.0",
|
||||
"@tailwindcss/forms": "^0.5.6",
|
||||
|
|
@ -36,9 +37,9 @@
|
|||
"class-variance-authority": "^0.6.1",
|
||||
"clsx": "^1.2.1",
|
||||
"cmdk": "^1.0.0",
|
||||
"debounce-promise": "^3.1.2",
|
||||
"dompurify": "^3.0.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"emoji-regex": "^10.3.0",
|
||||
"esbuild": "^0.17.19",
|
||||
"file-saver": "^2.0.5",
|
||||
"framer-motion": "^11.0.6",
|
||||
|
|
@ -47,6 +48,7 @@
|
|||
"million": "^3.0.6",
|
||||
"moment": "^2.29.4",
|
||||
"openseadragon": "^4.1.1",
|
||||
"p-debounce": "^4.0.0",
|
||||
"playwright": "^1.42.0",
|
||||
"react": "^18.2.21",
|
||||
"react-ace": "^10.1.0",
|
||||
|
|
|
|||
|
|
@ -45,6 +45,9 @@ export default defineConfig({
|
|||
name: "chromium",
|
||||
use: {
|
||||
...devices["Desktop Chrome"],
|
||||
launchOptions: {
|
||||
// headless: false,
|
||||
},
|
||||
contextOptions: {
|
||||
// chromium-specific permissions
|
||||
permissions: ["clipboard-read", "clipboard-write"],
|
||||
|
|
@ -57,6 +60,7 @@ export default defineConfig({
|
|||
// use: {
|
||||
// ...devices["Desktop Firefox"],
|
||||
// launchOptions: {
|
||||
// headless: false,
|
||||
// firefoxUserPrefs: {
|
||||
// "dom.events.asyncClipboard.readText": true,
|
||||
// "dom.events.testing.asyncClipboard": true,
|
||||
|
|
|
|||
|
|
@ -164,3 +164,13 @@ body {
|
|||
.ag-body-vertical-scroll-viewport::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #bbb;
|
||||
}
|
||||
|
||||
/* This CSS is to not apply the border for the column having 'no-border' class */
|
||||
.no-border.ag-cell:focus {
|
||||
border: none !important;
|
||||
outline: none;
|
||||
}
|
||||
.no-border.ag-cell {
|
||||
border: none !important;
|
||||
outline: none;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import axios from "axios";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { ErrorBoundary } from "react-error-boundary";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
import { Textarea } from "../../../../../../../components/ui/textarea";
|
||||
|
||||
export default function ErrorOutput({ value }: { value: string }) {
|
||||
return (
|
||||
<Textarea
|
||||
className={`h-full w-full text-destructive custom-scroll`}
|
||||
placeholder={"Empty"}
|
||||
value={value}
|
||||
readOnly
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
export const convertToTableRows = (obj: Object) => {
|
||||
const tokensArray = [Object.values(obj)[0]];
|
||||
return tokensArray;
|
||||
};
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
import ForwardedIconComponent from "../../../../../../components/genericIconComponent";
|
||||
import RecordsOutputComponent from "../../../../../../components/recordsOutputComponent";
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
AlertTitle,
|
||||
} from "../../../../../../components/ui/alert";
|
||||
import { Case } from "../../../../../../shared/components/caseComponent";
|
||||
import TextOutputView from "../../../../../../shared/components/textOutputView";
|
||||
import useFlowStore from "../../../../../../stores/flowStore";
|
||||
import ErrorOutput from "./components";
|
||||
|
||||
export default function SwitchOutputView(nodeId): JSX.Element {
|
||||
const nodeIdentity = nodeId.nodeId;
|
||||
|
||||
const nodes = useFlowStore((state) => state.nodes);
|
||||
const flowPool = useFlowStore((state) => state.flowPool);
|
||||
const node = nodes.find((node) => node?.id === nodeIdentity);
|
||||
|
||||
const flowPoolNode = (flowPool[nodeIdentity] ?? [])[
|
||||
(flowPool[nodeIdentity]?.length ?? 1) - 1
|
||||
];
|
||||
|
||||
const results = flowPoolNode?.data?.logs[0] ?? "";
|
||||
const resultType = results?.type;
|
||||
let resultMessage = results?.message;
|
||||
if (resultMessage.raw) {
|
||||
resultMessage = resultMessage.raw;
|
||||
}
|
||||
console.log("resultType", results);
|
||||
return (
|
||||
<>
|
||||
<Case condition={!resultType || resultType === "unknown"}>
|
||||
<div>NO OUTPUT</div>
|
||||
</Case>
|
||||
<Case condition={resultType === "ValueError"}>
|
||||
<ErrorOutput value={resultMessage}></ErrorOutput>
|
||||
</Case>
|
||||
|
||||
<Case condition={node && resultType === "text"}>
|
||||
<TextOutputView left={false} value={resultMessage} />
|
||||
</Case>
|
||||
|
||||
<Case condition={resultType === "record"}>
|
||||
<RecordsOutputComponent
|
||||
rows={[resultMessage] ?? []}
|
||||
pagination={true}
|
||||
columnMode="union"
|
||||
/>
|
||||
</Case>
|
||||
|
||||
<Case condition={resultType === "object"}>
|
||||
<RecordsOutputComponent
|
||||
rows={[resultMessage]}
|
||||
pagination={true}
|
||||
columnMode="union"
|
||||
/>
|
||||
</Case>
|
||||
{Array.isArray(resultMessage) && (
|
||||
<Case condition={resultType === "array"}>
|
||||
<RecordsOutputComponent
|
||||
rows={
|
||||
(resultMessage as Array<any>).every((item) => item.data)
|
||||
? (resultMessage as Array<any>).map((item) => item.data)
|
||||
: resultMessage
|
||||
}
|
||||
pagination={true}
|
||||
columnMode="union"
|
||||
/>
|
||||
</Case>
|
||||
)}
|
||||
<Case condition={resultType === "stream"}>
|
||||
<div className="flex h-full w-full items-center justify-center align-middle">
|
||||
<Alert variant={"default"} className="w-fit">
|
||||
<ForwardedIconComponent
|
||||
name="AlertCircle"
|
||||
className="h-5 w-5 text-primary"
|
||||
/>
|
||||
<AlertTitle>{"Streaming is not supported"}</AlertTitle>
|
||||
<AlertDescription>
|
||||
{
|
||||
"Use the playground to interact with components that stream data"
|
||||
}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
</Case>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import { Button } from "../../../../components/ui/button";
|
||||
import BaseModal from "../../../../modals/baseModal";
|
||||
import SwitchOutputView from "./components/switchOutputView";
|
||||
|
||||
export default function OutputModal({ open, setOpen, nodeId }): JSX.Element {
|
||||
return (
|
||||
<BaseModal open={open} setOpen={setOpen} size="medium">
|
||||
<BaseModal.Header description="Inspect the output of the component below.">
|
||||
<div className="flex items-center">
|
||||
<span className="pr-2">Component Output</span>
|
||||
</div>
|
||||
</BaseModal.Header>
|
||||
<BaseModal.Content>
|
||||
<SwitchOutputView nodeId={nodeId} />
|
||||
</BaseModal.Content>
|
||||
<BaseModal.Footer>
|
||||
<div className="flex w-full justify-end pt-2">
|
||||
<Button className="flex gap-2 px-3" onClick={() => setOpen(false)}>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</BaseModal.Footer>
|
||||
</BaseModal>
|
||||
);
|
||||
}
|
||||
|
|
@ -22,7 +22,6 @@ import {
|
|||
TOOLTIP_EMPTY,
|
||||
} from "../../../../constants/constants";
|
||||
import { Case } from "../../../../shared/components/caseComponent";
|
||||
import useAlertStore from "../../../../stores/alertStore";
|
||||
import useFlowStore from "../../../../stores/flowStore";
|
||||
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
|
||||
import { useTypesStore } from "../../../../stores/typesStore";
|
||||
|
|
@ -45,6 +44,7 @@ import useFetchDataOnMount from "../../../hooks/use-fetch-data-on-mount";
|
|||
import useHandleOnNewValue from "../../../hooks/use-handle-new-value";
|
||||
import useHandleNodeClass from "../../../hooks/use-handle-node-class";
|
||||
import useHandleRefreshButtonPress from "../../../hooks/use-handle-refresh-buttons";
|
||||
import OutputModal from "../outputModal";
|
||||
import TooltipRenderComponent from "../tooltipRenderComponent";
|
||||
import { TEXT_FIELD_TYPES } from "./constants";
|
||||
|
||||
|
|
@ -67,7 +67,6 @@ export default function ParameterComponent({
|
|||
const ref = useRef<HTMLDivElement>(null);
|
||||
const refHtml = useRef<HTMLDivElement & ReactNode>(null);
|
||||
const infoHtml = useRef<HTMLDivElement & ReactNode>(null);
|
||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||
const currentFlow = useFlowsManagerStore((state) => state.currentFlow);
|
||||
const nodes = useFlowStore((state) => state.nodes);
|
||||
const edges = useFlowStore((state) => state.edges);
|
||||
|
|
@ -80,6 +79,16 @@ export default function ParameterComponent({
|
|||
const flow = currentFlow?.data?.nodes ?? null;
|
||||
const groupedEdge = useRef(null);
|
||||
const setFilterEdge = useFlowStore((state) => state.setFilterEdge);
|
||||
const [openOutputModal, setOpenOutputModal] = useState(false);
|
||||
const flowPool = useFlowStore((state) => state.flowPool);
|
||||
|
||||
const displayOutputPreview = !!flowPool[data.id];
|
||||
|
||||
const unknownOutput = !!(
|
||||
flowPool[data.id] &&
|
||||
flowPool[data.id][flowPool[data.id].length - 1]?.data?.logs[0]?.type ===
|
||||
"unknown"
|
||||
);
|
||||
|
||||
const { handleOnNewValue: handleOnNewValueHook } = useHandleOnNewValue(
|
||||
data,
|
||||
|
|
@ -251,9 +260,38 @@ export default function ParameterComponent({
|
|||
</span>
|
||||
</ShadTooltip>
|
||||
) : (
|
||||
<span className={!left && data.node?.frozen ? " text-ice" : ""}>
|
||||
{title}
|
||||
</span>
|
||||
<div className="flex gap-2">
|
||||
<span className={!left && data.node?.frozen ? " text-ice" : ""}>
|
||||
{title}
|
||||
</span>
|
||||
{!left && (
|
||||
<ShadTooltip
|
||||
content={
|
||||
displayOutputPreview
|
||||
? unknownOutput
|
||||
? "Output can't be displayed"
|
||||
: "Inspect Output"
|
||||
: "Please build the component first"
|
||||
}
|
||||
>
|
||||
<button
|
||||
disabled={!displayOutputPreview || unknownOutput}
|
||||
onClick={() => setOpenOutputModal(true)}
|
||||
data-testid={`output-inspection-${title.toLowerCase()}`}
|
||||
>
|
||||
<IconComponent
|
||||
className={classNames(
|
||||
"h-5 w-5 rounded-md",
|
||||
displayOutputPreview && !unknownOutput
|
||||
? " hover:bg-secondary-foreground/5 hover:text-medium-indigo"
|
||||
: " cursor-not-allowed text-muted-foreground",
|
||||
)}
|
||||
name={"ScanEye"}
|
||||
/>
|
||||
</button>
|
||||
</ShadTooltip>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<span className={(required ? "ml-2 " : "") + "text-status-red"}>
|
||||
{required ? "*" : ""}
|
||||
|
|
@ -392,7 +430,7 @@ export default function ParameterComponent({
|
|||
});
|
||||
}}
|
||||
name={name}
|
||||
data={data}
|
||||
data={data.node?.template[name]!}
|
||||
/>
|
||||
</div>
|
||||
{data.node?.template[name]?.refresh_button && (
|
||||
|
|
@ -448,8 +486,8 @@ export default function ParameterComponent({
|
|||
data.node?.template[name]?.real_time_refresh)
|
||||
}
|
||||
>
|
||||
<div className="mt-2 flex w-full items-center">
|
||||
<div className="w-5/6 flex-grow">
|
||||
<div className="mt-2 flex w-full items-center gap-2">
|
||||
<div className="flex-1">
|
||||
<Dropdown
|
||||
disabled={disabled}
|
||||
isLoading={isLoading}
|
||||
|
|
@ -467,7 +505,6 @@ export default function ParameterComponent({
|
|||
name={name}
|
||||
data={data}
|
||||
button_text={data.node?.template[name]?.refresh_button_text}
|
||||
className="extra-side-bar-buttons ml-2 mt-1"
|
||||
handleUpdateValues={handleRefreshButtonPress}
|
||||
id={"refresh-button-" + name}
|
||||
/>
|
||||
|
|
@ -580,6 +617,13 @@ export default function ParameterComponent({
|
|||
/>
|
||||
</div>
|
||||
</Case>
|
||||
{openOutputModal && (
|
||||
<OutputModal
|
||||
open={openOutputModal}
|
||||
nodeId={data.id}
|
||||
setOpen={setOpenOutputModal}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -1,32 +1,36 @@
|
|||
import { cloneDeep } from "lodash";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import emojiRegex from "emoji-regex";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { NodeToolbar, useUpdateNodeInternals } from "reactflow";
|
||||
import IconComponent from "../../components/genericIconComponent";
|
||||
import InputComponent from "../../components/inputComponent";
|
||||
import ShadTooltip from "../../components/shadTooltipComponent";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import Checkmark from "../../components/ui/checkmark";
|
||||
import Loading from "../../components/ui/loading";
|
||||
import { Textarea } from "../../components/ui/textarea";
|
||||
import Xmark from "../../components/ui/xmark";
|
||||
import {
|
||||
RUN_TIMESTAMP_PREFIX,
|
||||
STATUS_BUILD,
|
||||
STATUS_BUILDING,
|
||||
} from "../../constants/constants";
|
||||
import { BuildStatus } from "../../constants/enums";
|
||||
import { countHandlesFn } from "../helpers/count-handles";
|
||||
import { getSpecificClassFromBuildStatus } from "../helpers/get-class-from-build-status";
|
||||
import NodeToolbarComponent from "../../pages/FlowPage/components/nodeToolbarComponent";
|
||||
import useAlertStore from "../../stores/alertStore";
|
||||
import { useDarkStore } from "../../stores/darkStore";
|
||||
import useFlowStore from "../../stores/flowStore";
|
||||
import useFlowsManagerStore from "../../stores/flowsManagerStore";
|
||||
import { useTypesStore } from "../../stores/typesStore";
|
||||
import { APIClassType } from "../../types/api";
|
||||
import { validationStatusType } from "../../types/components";
|
||||
import { VertexBuildTypeAPI } from "../../types/api";
|
||||
import { NodeDataType } from "../../types/flow";
|
||||
import { handleKeyDown, scapedJSONStringfy } from "../../utils/reactflowUtils";
|
||||
import { nodeColors, nodeIconsLucide } from "../../utils/styleUtils";
|
||||
import { classNames, cn } from "../../utils/utils";
|
||||
import useCheckCodeValidity from "../hooks/use-check-code-validity";
|
||||
import useIconNodeRender from "../hooks/use-icon-render";
|
||||
import useIconStatus from "../hooks/use-icons-status";
|
||||
import useUpdateNodeCode from "../hooks/use-update-node-code";
|
||||
import useUpdateValidationStatus from "../hooks/use-update-validation-status";
|
||||
import useValidationStatusString from "../hooks/use-validation-status-string";
|
||||
import getFieldTitle from "../utils/get-field-title";
|
||||
import sortFields from "../utils/sort-fields";
|
||||
import isWrappedWithClass from "../../pages/FlowPage/components/PageComponent/utils/is-wrapped-with-class";
|
||||
|
|
@ -34,14 +38,13 @@ import ParameterComponent from "./components/parameterComponent";
|
|||
|
||||
export default function GenericNode({
|
||||
data,
|
||||
xPos,
|
||||
yPos,
|
||||
|
||||
selected,
|
||||
}: {
|
||||
data: NodeDataType;
|
||||
selected: boolean;
|
||||
xPos: number;
|
||||
yPos: number;
|
||||
xPos?: number;
|
||||
yPos?: number;
|
||||
}): JSX.Element {
|
||||
const types = useTypesStore((state) => state.types);
|
||||
const templates = useTypesStore((state) => state.templates);
|
||||
|
|
@ -51,7 +54,15 @@ export default function GenericNode({
|
|||
const setNode = useFlowStore((state) => state.setNode);
|
||||
const updateNodeInternals = useUpdateNodeInternals();
|
||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||
const name = nodeIconsLucide[data.type] ? data.type : types[data.type];
|
||||
const isDark = useDarkStore((state) => state.dark);
|
||||
const buildStatus = useFlowStore(
|
||||
(state) => state.flowBuildStatus[data.id]?.status,
|
||||
);
|
||||
const lastRunTime = useFlowStore(
|
||||
(state) => state.flowBuildStatus[data.id]?.timestamp,
|
||||
);
|
||||
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
|
||||
|
||||
const [inputName, setInputName] = useState(false);
|
||||
const [nodeName, setNodeName] = useState(data.node!.display_name);
|
||||
const [inputDescription, setInputDescription] = useState(false);
|
||||
|
|
@ -59,185 +70,25 @@ export default function GenericNode({
|
|||
data.node?.description!,
|
||||
);
|
||||
const [isOutdated, setIsOutdated] = useState(false);
|
||||
const buildStatus = useFlowStore(
|
||||
(state) => state.flowBuildStatus[data.id]?.status,
|
||||
);
|
||||
const lastRunTime = useFlowStore(
|
||||
(state) => state.flowBuildStatus[data.id]?.timestamp,
|
||||
);
|
||||
const [validationStatus, setValidationStatus] =
|
||||
useState<validationStatusType | null>(null);
|
||||
useState<VertexBuildTypeAPI | null>(null);
|
||||
const [handles, setHandles] = useState<number>(0);
|
||||
|
||||
const [validationString, setValidationString] = useState<string>("");
|
||||
|
||||
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
|
||||
|
||||
useEffect(() => {
|
||||
// This one should run only once
|
||||
// first check if data.type in NATIVE_CATEGORIES
|
||||
// if not return
|
||||
if (!data.node?.template?.code?.value) return;
|
||||
const thisNodeTemplate = templates[data.type]?.template;
|
||||
// if the template does not have a code key
|
||||
// return
|
||||
if (!thisNodeTemplate?.code) return;
|
||||
const currentCode = thisNodeTemplate.code?.value;
|
||||
const thisNodesCode = data.node!.template?.code?.value;
|
||||
const componentsToIgnore = ["Custom Component"];
|
||||
if (
|
||||
currentCode !== thisNodesCode &&
|
||||
!componentsToIgnore.includes(data.node!.display_name)
|
||||
) {
|
||||
setIsOutdated(true);
|
||||
} else {
|
||||
setIsOutdated(false);
|
||||
}
|
||||
// template.code can be undefined
|
||||
}, [data.node?.template?.code?.value]);
|
||||
|
||||
const updateNodeCode = useCallback(
|
||||
(newNodeClass: APIClassType, code: string, name: string) => {
|
||||
setNode(data.id, (oldNode) => {
|
||||
let newNode = cloneDeep(oldNode);
|
||||
|
||||
newNode.data = {
|
||||
...newNode.data,
|
||||
node: newNodeClass,
|
||||
description: newNodeClass.description ?? data.node!.description,
|
||||
display_name: newNodeClass.display_name ?? data.node!.display_name,
|
||||
};
|
||||
|
||||
newNode.data.node.template[name].value = code;
|
||||
setIsOutdated(false);
|
||||
|
||||
return newNode;
|
||||
});
|
||||
|
||||
updateNodeInternals(data.id);
|
||||
},
|
||||
[data.id, data.node, setNode, setIsOutdated],
|
||||
);
|
||||
|
||||
if (!data.node!.template) {
|
||||
setErrorData({
|
||||
title: `Error in component ${data.node!.display_name}`,
|
||||
list: [
|
||||
`The component ${data.node!.display_name} has no template.`,
|
||||
`Please contact the developer of the component to fix this issue.`,
|
||||
],
|
||||
});
|
||||
takeSnapshot();
|
||||
deleteNode(data.id);
|
||||
}
|
||||
|
||||
function countHandles(): void {
|
||||
let count = Object.keys(data.node!.template)
|
||||
.filter((templateField) => templateField.charAt(0) !== "_")
|
||||
.map((templateCamp) => {
|
||||
const { template } = data.node!;
|
||||
if (template[templateCamp].input_types) return true;
|
||||
if (!template[templateCamp].show) return false;
|
||||
switch (template[templateCamp].type) {
|
||||
case "str":
|
||||
case "bool":
|
||||
case "float":
|
||||
case "code":
|
||||
case "prompt":
|
||||
case "file":
|
||||
case "int":
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
})
|
||||
.reduce((total, value) => total + (value ? 1 : 0), 0);
|
||||
|
||||
setHandles(count);
|
||||
}
|
||||
useEffect(() => {
|
||||
countHandles();
|
||||
}, [data, data.node]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selected) {
|
||||
setInputName(false);
|
||||
setInputDescription(false);
|
||||
}
|
||||
}, [selected]);
|
||||
|
||||
const iconStatus = useIconStatus(buildStatus, validationStatus);
|
||||
const [showNode, setShowNode] = useState(data.showNode ?? true);
|
||||
// State for outline color
|
||||
const isBuilding = useFlowStore((state) => state.isBuilding);
|
||||
|
||||
// should be empty string if no duration
|
||||
// else should be `Duration: ${duration}`
|
||||
const getDurationString = (duration: number | undefined): string => {
|
||||
if (duration === undefined) {
|
||||
return "";
|
||||
} else {
|
||||
return `${duration}`;
|
||||
}
|
||||
};
|
||||
const durationString = getDurationString(validationStatus?.data.duration);
|
||||
const updateNodeCode = useUpdateNodeCode(
|
||||
data?.id,
|
||||
data.node!,
|
||||
setNode,
|
||||
setIsOutdated,
|
||||
updateNodeInternals,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setNodeDescription(data.node!.description);
|
||||
}, [data.node!.description]);
|
||||
|
||||
useEffect(() => {
|
||||
setNodeName(data.node!.display_name);
|
||||
}, [data.node!.display_name]);
|
||||
|
||||
useEffect(() => {
|
||||
const relevantData =
|
||||
flowPool[data.id] && flowPool[data.id]?.length > 0
|
||||
? flowPool[data.id][flowPool[data.id].length - 1]
|
||||
: null;
|
||||
if (relevantData) {
|
||||
// Extract validation information from relevantData and update the validationStatus state
|
||||
setValidationStatus(relevantData);
|
||||
} else {
|
||||
setValidationStatus(null);
|
||||
}
|
||||
}, [flowPool[data.id], data.id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (validationStatus?.params) {
|
||||
// if it is not a string turn it into a string
|
||||
let newValidationString = validationStatus.params;
|
||||
if (typeof newValidationString !== "string") {
|
||||
newValidationString = JSON.stringify(validationStatus.params);
|
||||
}
|
||||
|
||||
setValidationString(newValidationString);
|
||||
}
|
||||
}, [validationStatus, validationStatus?.params]);
|
||||
|
||||
const [showNode, setShowNode] = useState(data.showNode ?? true);
|
||||
|
||||
useEffect(() => {
|
||||
setShowNode(data.showNode ?? true);
|
||||
}, [data.showNode]);
|
||||
|
||||
const nameEditable = true;
|
||||
|
||||
const emojiRegex = /\p{Emoji}/u;
|
||||
const isEmoji = emojiRegex.test(data?.node?.icon!);
|
||||
|
||||
const iconNodeRender = useCallback(() => {
|
||||
const iconElement = data?.node?.icon;
|
||||
const iconColor = nodeColors[types[data.type]];
|
||||
const iconName =
|
||||
iconElement || (data.node?.flow ? "group_components" : name);
|
||||
const iconClassName = `generic-node-icon ${
|
||||
!showNode ? " absolute inset-x-6 h-12 w-12 " : ""
|
||||
}`;
|
||||
if (iconElement && isEmoji) {
|
||||
return nodeIconFragment(iconElement);
|
||||
} else {
|
||||
return checkNodeIconFragment(iconColor, iconName, iconClassName);
|
||||
}
|
||||
}, [data, isEmoji, name, showNode]);
|
||||
const name = nodeIconsLucide[data.type] ? data.type : types[data.type];
|
||||
|
||||
const nodeIconFragment = (icon) => {
|
||||
return <span className="text-lg">{icon}</span>;
|
||||
|
|
@ -253,79 +104,24 @@ export default function GenericNode({
|
|||
);
|
||||
};
|
||||
|
||||
const isDark = useDarkStore((state) => state.dark);
|
||||
const renderIconStatus = (
|
||||
buildStatus: BuildStatus | undefined,
|
||||
validationStatus: validationStatusType | null,
|
||||
) => {
|
||||
if (buildStatus === BuildStatus.BUILDING) {
|
||||
return <Loading className="text-medium-indigo" />;
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<IconComponent
|
||||
name="Play"
|
||||
className="absolute ml-0.5 h-5 fill-current stroke-2 text-medium-indigo opacity-0 transition-all group-hover:opacity-100"
|
||||
/>
|
||||
{validationStatus && validationStatus.valid ? (
|
||||
<Checkmark
|
||||
className="absolute ml-0.5 h-5 stroke-2 text-status-green opacity-100 transition-all group-hover:opacity-0"
|
||||
isVisible={true}
|
||||
/>
|
||||
) : validationStatus &&
|
||||
!validationStatus.valid &&
|
||||
buildStatus === BuildStatus.INACTIVE ? (
|
||||
<IconComponent
|
||||
name="Play"
|
||||
className="absolute ml-0.5 h-5 fill-current stroke-2 text-status-green opacity-30 transition-all group-hover:opacity-0"
|
||||
/>
|
||||
) : buildStatus === BuildStatus.ERROR ||
|
||||
(validationStatus && !validationStatus.valid) ? (
|
||||
<Xmark
|
||||
isVisible={true}
|
||||
className="absolute ml-0.5 h-5 fill-current stroke-2 text-status-red opacity-100 transition-all group-hover:opacity-0"
|
||||
/>
|
||||
) : (
|
||||
<IconComponent
|
||||
name="Play"
|
||||
className="absolute ml-0.5 h-5 fill-current stroke-2 text-muted-foreground opacity-100 transition-all group-hover:opacity-0"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
const getSpecificClassFromBuildStatus = (
|
||||
buildStatus: BuildStatus | undefined,
|
||||
validationStatus: validationStatusType | null,
|
||||
) => {
|
||||
let isInvalid = validationStatus && !validationStatus.valid;
|
||||
|
||||
if (buildStatus === BuildStatus.INACTIVE) {
|
||||
// INACTIVE should have its own class
|
||||
return "inactive-status";
|
||||
}
|
||||
if (
|
||||
(buildStatus === BuildStatus.BUILT && isInvalid) ||
|
||||
buildStatus === BuildStatus.ERROR
|
||||
) {
|
||||
return isDark ? "built-invalid-status-dark" : "built-invalid-status";
|
||||
} else if (buildStatus === BuildStatus.BUILDING) {
|
||||
return "building-status";
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
const renderIconStatus = () => {
|
||||
return (
|
||||
<div className="generic-node-status-position flex items-center justify-center">
|
||||
{iconStatus}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getNodeBorderClassName = (
|
||||
selected: boolean,
|
||||
showNode: boolean,
|
||||
buildStatus: BuildStatus | undefined,
|
||||
validationStatus: validationStatusType | null,
|
||||
validationStatus: VertexBuildTypeAPI | null,
|
||||
) => {
|
||||
const specificClassFromBuildStatus = getSpecificClassFromBuildStatus(
|
||||
buildStatus,
|
||||
validationStatus,
|
||||
isDark,
|
||||
);
|
||||
|
||||
const baseBorderClass = getBaseBorderClass(selected);
|
||||
|
|
@ -333,7 +129,7 @@ export default function GenericNode({
|
|||
const names = classNames(
|
||||
baseBorderClass,
|
||||
nodeSizeClass,
|
||||
"generic-node-div",
|
||||
"generic-node-div group/node",
|
||||
specificClassFromBuildStatus,
|
||||
);
|
||||
return names;
|
||||
|
|
@ -350,6 +146,64 @@ export default function GenericNode({
|
|||
const getNodeSizeClass = (showNode) =>
|
||||
showNode ? "w-96 rounded-lg" : "w-26 h-26 rounded-full";
|
||||
|
||||
const nameEditable = true;
|
||||
const isEmoji = emojiRegex().test(data?.node?.icon!);
|
||||
|
||||
if (!data.node!.template) {
|
||||
setErrorData({
|
||||
title: `Error in component ${data.node!.display_name}`,
|
||||
list: [
|
||||
`The component ${data.node!.display_name} has no template.`,
|
||||
`Please contact the developer of the component to fix this issue.`,
|
||||
],
|
||||
});
|
||||
takeSnapshot();
|
||||
deleteNode(data.id);
|
||||
}
|
||||
|
||||
useCheckCodeValidity(data, templates, setIsOutdated, types);
|
||||
useValidationStatusString(validationStatus, setValidationString);
|
||||
useUpdateValidationStatus(data?.id, flowPool, setValidationStatus);
|
||||
|
||||
const iconNodeRender = useIconNodeRender(
|
||||
data,
|
||||
types,
|
||||
nodeColors,
|
||||
name,
|
||||
showNode,
|
||||
isEmoji,
|
||||
nodeIconFragment,
|
||||
checkNodeIconFragment,
|
||||
);
|
||||
|
||||
function countHandles(): void {
|
||||
const count = countHandlesFn(data);
|
||||
setHandles(count);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
countHandles();
|
||||
}, [data, data.node]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selected) {
|
||||
setInputName(false);
|
||||
setInputDescription(false);
|
||||
}
|
||||
}, [selected]);
|
||||
|
||||
useEffect(() => {
|
||||
setNodeDescription(data.node!.description);
|
||||
}, [data.node!.description]);
|
||||
|
||||
useEffect(() => {
|
||||
setNodeName(data.node!.display_name);
|
||||
}, [data.node!.display_name]);
|
||||
|
||||
useEffect(() => {
|
||||
setShowNode(data.showNode ?? true);
|
||||
}, [data.showNode]);
|
||||
|
||||
const memoizedNodeToolbarComponent = useMemo(() => {
|
||||
return (
|
||||
<NodeToolbar>
|
||||
|
|
@ -593,67 +447,56 @@ export default function GenericNode({
|
|||
)}
|
||||
</div>
|
||||
{showNode && (
|
||||
<ShadTooltip
|
||||
content={
|
||||
buildStatus === BuildStatus.BUILDING ? (
|
||||
<span> {STATUS_BUILDING} </span>
|
||||
) : !validationStatus ? (
|
||||
<span className="flex">{STATUS_BUILD}</span>
|
||||
) : (
|
||||
<div className="max-h-100 p-2">
|
||||
<div>
|
||||
{lastRunTime && (
|
||||
<div className="justify-left flex font-normal text-muted-foreground">
|
||||
<div>{RUN_TIMESTAMP_PREFIX}</div>
|
||||
<div className="ml-1 text-status-blue">
|
||||
{lastRunTime}
|
||||
<>
|
||||
<ShadTooltip
|
||||
content={
|
||||
buildStatus === BuildStatus.BUILDING ? (
|
||||
<span> {STATUS_BUILDING} </span>
|
||||
) : !validationStatus ? (
|
||||
<span className="flex">{STATUS_BUILD}</span>
|
||||
) : (
|
||||
<div className="max-h-100 p-2">
|
||||
<div>
|
||||
{lastRunTime && (
|
||||
<div className="justify-left flex font-normal text-muted-foreground">
|
||||
<div>{RUN_TIMESTAMP_PREFIX}</div>
|
||||
<div className="ml-1 text-status-blue">
|
||||
{lastRunTime}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="justify-left flex font-normal text-muted-foreground">
|
||||
<div>Duration:</div>
|
||||
<div className="ml-1 text-status-blue">
|
||||
{validationStatus?.data.duration}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="justify-left flex font-normal text-muted-foreground">
|
||||
<div>Duration:</div>
|
||||
<div className="mb-3 ml-1 text-status-blue">
|
||||
{validationStatus?.data.duration}
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<span className="mb-2 mt-2 flex justify-center font-semibold text-muted-foreground">
|
||||
Output
|
||||
</span>
|
||||
<div className="max-h-96 overflow-auto font-normal custom-scroll">
|
||||
{validationString.split("\n").map((line, index) => (
|
||||
<div className="font-normal" key={index}>
|
||||
{line}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
side="bottom"
|
||||
>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (buildStatus === BuildStatus.BUILDING || isBuilding)
|
||||
return;
|
||||
setValidationStatus(null);
|
||||
buildFlow({ stopNodeId: data.id });
|
||||
}}
|
||||
variant="secondary"
|
||||
className={"group h-9 px-1.5"}
|
||||
)
|
||||
}
|
||||
side="bottom"
|
||||
>
|
||||
<div
|
||||
data-testid={
|
||||
`button_run_` + data?.node?.display_name.toLowerCase()
|
||||
}
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (buildStatus === BuildStatus.BUILDING || isBuilding)
|
||||
return;
|
||||
setValidationStatus(null);
|
||||
buildFlow({ stopNodeId: data.id });
|
||||
}}
|
||||
variant="secondary"
|
||||
className={"group h-9 px-1.5"}
|
||||
>
|
||||
<div className="generic-node-status-position flex items-center justify-center">
|
||||
{renderIconStatus(buildStatus, validationStatus)}
|
||||
<div
|
||||
data-testid={
|
||||
`button_run_` + data?.node?.display_name.toLowerCase()
|
||||
}
|
||||
>
|
||||
{renderIconStatus()}
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
</ShadTooltip>
|
||||
</Button>
|
||||
</ShadTooltip>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
26
src/frontend/src/CustomNodes/helpers/count-handles.ts
Normal file
26
src/frontend/src/CustomNodes/helpers/count-handles.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { NodeDataType } from "../../types/flow";
|
||||
|
||||
export function countHandlesFn(data: NodeDataType): number {
|
||||
let count = Object.keys(data.node!.template)
|
||||
.filter((templateField) => templateField.charAt(0) !== "_")
|
||||
.map((templateCamp) => {
|
||||
const { template } = data.node!;
|
||||
if (template[templateCamp].input_types) return true;
|
||||
if (!template[templateCamp].show) return false;
|
||||
switch (template[templateCamp].type) {
|
||||
case "str":
|
||||
case "bool":
|
||||
case "float":
|
||||
case "code":
|
||||
case "prompt":
|
||||
case "file":
|
||||
case "int":
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
})
|
||||
.reduce((total, value) => total + (value ? 1 : 0), 0);
|
||||
|
||||
return count;
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import { BuildStatus } from "../../constants/enums";
|
||||
import { VertexBuildTypeAPI } from "../../types/api";
|
||||
|
||||
export const getSpecificClassFromBuildStatus = (
|
||||
buildStatus: BuildStatus | undefined,
|
||||
validationStatus: VertexBuildTypeAPI | null,
|
||||
isDark: boolean,
|
||||
) => {
|
||||
let isInvalid = validationStatus && !validationStatus.valid;
|
||||
|
||||
if (buildStatus === BuildStatus.INACTIVE) {
|
||||
// INACTIVE should have its own class
|
||||
return "inactive-status";
|
||||
}
|
||||
if (
|
||||
(buildStatus === BuildStatus.BUILT && isInvalid) ||
|
||||
buildStatus === BuildStatus.ERROR
|
||||
) {
|
||||
return isDark ? "built-invalid-status-dark" : "built-invalid-status";
|
||||
} else if (buildStatus === BuildStatus.BUILDING) {
|
||||
return "building-status";
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
import { useEffect } from "react";
|
||||
import { NATIVE_CATEGORIES } from "../../constants/constants";
|
||||
import { NodeDataType } from "../../types/flow";
|
||||
|
||||
const useCheckCodeValidity = (
|
||||
data: NodeDataType,
|
||||
templates: { [key: string]: any },
|
||||
setIsOutdated: (value: boolean) => void,
|
||||
types,
|
||||
) => {
|
||||
useEffect(() => {
|
||||
// This one should run only once
|
||||
// first check if data.type in NATIVE_CATEGORIES
|
||||
// if not return
|
||||
if (
|
||||
!NATIVE_CATEGORIES.includes(types[data.type]) ||
|
||||
!data.node?.template?.code?.value
|
||||
)
|
||||
return;
|
||||
const thisNodeTemplate = templates[data.type].template;
|
||||
// if the template does not have a code key
|
||||
// return
|
||||
if (!thisNodeTemplate.code) return;
|
||||
const currentCode = thisNodeTemplate.code?.value;
|
||||
const thisNodesCode = data.node!.template?.code?.value;
|
||||
const componentsToIgnore = ["Custom Component", "Prompt"];
|
||||
if (
|
||||
currentCode !== thisNodesCode &&
|
||||
!componentsToIgnore.includes(data.node!.display_name)
|
||||
) {
|
||||
setIsOutdated(true);
|
||||
} else {
|
||||
setIsOutdated(false);
|
||||
}
|
||||
// template.code can be undefined
|
||||
}, [data.node?.template?.code?.value, templates, setIsOutdated]);
|
||||
};
|
||||
|
||||
export default useCheckCodeValidity;
|
||||
45
src/frontend/src/CustomNodes/hooks/use-icon-render.tsx
Normal file
45
src/frontend/src/CustomNodes/hooks/use-icon-render.tsx
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { useCallback } from "react";
|
||||
import { NodeDataType } from "../../types/flow";
|
||||
|
||||
const useIconNodeRender = (
|
||||
data: NodeDataType,
|
||||
types: { [key: string]: string },
|
||||
nodeColors: { [key: string]: string },
|
||||
name: string,
|
||||
showNode: boolean,
|
||||
isEmoji: boolean,
|
||||
nodeIconFragment: (iconElement: string) => JSX.Element,
|
||||
checkNodeIconFragment: (
|
||||
iconColor: string,
|
||||
iconName: string,
|
||||
iconClassName: string,
|
||||
) => JSX.Element,
|
||||
) => {
|
||||
const iconNodeRender = useCallback(() => {
|
||||
const iconElement = data?.node?.icon;
|
||||
const iconColor = nodeColors[types[data.type]];
|
||||
const iconName =
|
||||
iconElement || (data.node?.flow ? "group_components" : name);
|
||||
const iconClassName = `generic-node-icon ${
|
||||
!showNode ? " absolute inset-x-6 h-12 w-12 " : ""
|
||||
}`;
|
||||
if (iconElement && isEmoji) {
|
||||
return nodeIconFragment(iconElement);
|
||||
} else {
|
||||
return checkNodeIconFragment(iconColor, iconName, iconClassName);
|
||||
}
|
||||
}, [
|
||||
data,
|
||||
types,
|
||||
nodeColors,
|
||||
name,
|
||||
showNode,
|
||||
isEmoji,
|
||||
nodeIconFragment,
|
||||
checkNodeIconFragment,
|
||||
]);
|
||||
|
||||
return iconNodeRender;
|
||||
};
|
||||
|
||||
export default useIconNodeRender;
|
||||
54
src/frontend/src/CustomNodes/hooks/use-icons-status.tsx
Normal file
54
src/frontend/src/CustomNodes/hooks/use-icons-status.tsx
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import IconComponent from "../../components/genericIconComponent";
|
||||
import Checkmark from "../../components/ui/checkmark";
|
||||
import Loading from "../../components/ui/loading";
|
||||
import Xmark from "../../components/ui/xmark";
|
||||
import { BuildStatus } from "../../constants/enums";
|
||||
import { VertexBuildTypeAPI } from "../../types/api";
|
||||
|
||||
const useIconStatus = (
|
||||
buildStatus: BuildStatus | undefined,
|
||||
validationStatus: VertexBuildTypeAPI | null,
|
||||
) => {
|
||||
const renderIconStatus = () => {
|
||||
if (buildStatus === BuildStatus.BUILDING) {
|
||||
return <Loading className="text-medium-indigo" />;
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<IconComponent
|
||||
name="Play"
|
||||
className="absolute ml-0.5 h-5 fill-current stroke-2 text-medium-indigo opacity-0 transition-all group-hover:opacity-100"
|
||||
/>
|
||||
{validationStatus && validationStatus.valid ? (
|
||||
<Checkmark
|
||||
className="absolute ml-0.5 h-5 stroke-2 text-status-green opacity-100 transition-all group-hover:opacity-0"
|
||||
isVisible={true}
|
||||
/>
|
||||
) : validationStatus &&
|
||||
!validationStatus.valid &&
|
||||
buildStatus === BuildStatus.INACTIVE ? (
|
||||
<IconComponent
|
||||
name="Play"
|
||||
className="absolute ml-0.5 h-5 fill-current stroke-2 text-status-green opacity-30 transition-all group-hover:opacity-0"
|
||||
/>
|
||||
) : buildStatus === BuildStatus.ERROR ||
|
||||
(validationStatus && !validationStatus.valid) ? (
|
||||
<Xmark
|
||||
isVisible={true}
|
||||
className="absolute ml-0.5 h-5 fill-current stroke-2 text-status-red opacity-100 transition-all group-hover:opacity-0"
|
||||
/>
|
||||
) : (
|
||||
<IconComponent
|
||||
name="Play"
|
||||
className="absolute ml-0.5 h-5 fill-current stroke-2 text-muted-foreground opacity-100 transition-all group-hover:opacity-0"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return renderIconStatus();
|
||||
};
|
||||
|
||||
export default useIconStatus;
|
||||
38
src/frontend/src/CustomNodes/hooks/use-update-node-code.tsx
Normal file
38
src/frontend/src/CustomNodes/hooks/use-update-node-code.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { cloneDeep } from "lodash"; // or any other deep cloning library you prefer
|
||||
import { useCallback } from "react";
|
||||
import { APIClassType } from "../../types/api";
|
||||
|
||||
const useUpdateNodeCode = (
|
||||
dataId: string,
|
||||
dataNode: APIClassType, // Define YourNodeType according to your data structure
|
||||
setNode: (id: string, callback: (oldNode) => any) => void,
|
||||
setIsOutdated: (value: boolean) => void,
|
||||
updateNodeInternals: (id: string) => void,
|
||||
) => {
|
||||
const updateNodeCode = useCallback(
|
||||
(newNodeClass: APIClassType, code: string, name: string) => {
|
||||
setNode(dataId, (oldNode) => {
|
||||
let newNode = cloneDeep(oldNode);
|
||||
|
||||
newNode.data = {
|
||||
...newNode.data,
|
||||
node: newNodeClass,
|
||||
description: newNodeClass.description ?? dataNode.description,
|
||||
display_name: newNodeClass.display_name ?? dataNode.display_name,
|
||||
};
|
||||
|
||||
newNode.data.node.template[name].value = code;
|
||||
setIsOutdated(false);
|
||||
|
||||
return newNode;
|
||||
});
|
||||
|
||||
updateNodeInternals(dataId);
|
||||
},
|
||||
[dataId, dataNode, setNode, setIsOutdated, updateNodeInternals],
|
||||
);
|
||||
|
||||
return updateNodeCode;
|
||||
};
|
||||
|
||||
export default useUpdateNodeCode;
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import { useEffect } from "react";
|
||||
|
||||
const useUpdateValidationStatus = (dataId, flowPool, setValidationStatus) => {
|
||||
useEffect(() => {
|
||||
const relevantData =
|
||||
flowPool[dataId] && flowPool[dataId]?.length > 0
|
||||
? flowPool[dataId][flowPool[dataId].length - 1]
|
||||
: null;
|
||||
if (relevantData) {
|
||||
// Extract validation information from relevantData and update the validationStatus state
|
||||
setValidationStatus(relevantData);
|
||||
} else {
|
||||
setValidationStatus(null);
|
||||
}
|
||||
}, [flowPool[dataId], dataId, setValidationStatus]);
|
||||
};
|
||||
|
||||
export default useUpdateValidationStatus;
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import { useEffect } from "react";
|
||||
|
||||
const useValidationStatusString = (validationStatus, setValidationString) => {
|
||||
useEffect(() => {
|
||||
if (validationStatus?.data.logs) {
|
||||
// if it is not a string turn it into a string
|
||||
let newValidationString = "";
|
||||
if (Array.isArray(validationStatus.data.logs)) {
|
||||
newValidationString = validationStatus.data.logs
|
||||
.map((log) => (log?.message ? log.message : JSON.stringify(log)))
|
||||
.join("\n");
|
||||
}
|
||||
if (typeof newValidationString !== "string") {
|
||||
newValidationString = JSON.stringify(validationStatus.data.logs);
|
||||
}
|
||||
|
||||
setValidationString(newValidationString);
|
||||
}
|
||||
}, [validationStatus, validationStatus?.data.logs, setValidationString]);
|
||||
};
|
||||
|
||||
export default useValidationStatusString;
|
||||
|
|
@ -51,13 +51,15 @@ export default function ErrorAlert({
|
|||
/>
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<h3 className="error-build-foreground">{title}</h3>
|
||||
<h3 className="error-build-foreground line-clamp-2">{title}</h3>
|
||||
{list?.length !== 0 &&
|
||||
list?.some((item) => item !== null && item !== undefined) ? (
|
||||
<div className="error-build-message-div">
|
||||
<ul className="error-build-message-list">
|
||||
{list.map((item, index) => (
|
||||
<li key={index}>{item}</li>
|
||||
<li key={index} className="line-clamp-5">
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ export default function NoticeAlert({
|
|||
/>
|
||||
</div>
|
||||
<div className="ml-3 flex-1 md:flex md:justify-between">
|
||||
<p className="text-sm text-info-foreground word-break-break-word">
|
||||
<p className="line-clamp-2 text-sm text-info-foreground word-break-break-word">
|
||||
{title}
|
||||
</p>
|
||||
<p className="mt-3 text-sm md:ml-6 md:mt-0">
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ export default function SuccessAlert({
|
|||
/>
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<p className="success-alert-message">{title}</p>
|
||||
<p className="success-alert-message line-clamp-2">{title}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import {
|
|||
} from "../../components/ui/accordion";
|
||||
import { AccordionComponentType } from "../../types/components";
|
||||
import { cn } from "../../utils/utils";
|
||||
import ShadTooltip from "../shadTooltipComponent";
|
||||
|
||||
export default function AccordionComponent({
|
||||
trigger,
|
||||
|
|
|
|||
|
|
@ -27,8 +27,8 @@ import {
|
|||
import { Checkbox } from "../ui/checkbox";
|
||||
import { FormControl, FormField } from "../ui/form";
|
||||
import Loading from "../ui/loading";
|
||||
import { convertTestName } from "./utils/convert-test-name";
|
||||
import DragCardComponent from "./components/dragCardComponent";
|
||||
import { convertTestName } from "./utils/convert-test-name";
|
||||
|
||||
export default function CollectionCardComponent({
|
||||
data,
|
||||
|
|
|
|||
|
|
@ -115,6 +115,7 @@ function CsvOutputComponent({
|
|||
style={{ height: "100%", width: "100%" }}
|
||||
>
|
||||
<TableComponent
|
||||
key={"csv-output"}
|
||||
rowData={rowData}
|
||||
columnDefs={colDefs}
|
||||
defaultColDef={defaultColDef}
|
||||
|
|
|
|||
|
|
@ -33,9 +33,8 @@ export default function Dropdown({
|
|||
|
||||
const refButton = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const PopoverContentDropdown = children
|
||||
? PopoverContent
|
||||
: PopoverContentWithoutPortal;
|
||||
const PopoverContentDropdown =
|
||||
children || editNode ? PopoverContent : PopoverContentWithoutPortal;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -81,14 +81,16 @@ export default function Header(): JSX.Element {
|
|||
<span className="ml-4 text-2xl">⛓️</span>
|
||||
</Link>
|
||||
{showArrowReturnIcon && (
|
||||
<button
|
||||
<Button
|
||||
variant="none"
|
||||
size="none"
|
||||
onClick={() => {
|
||||
checkForChanges();
|
||||
redirectToLastLocation();
|
||||
}}
|
||||
>
|
||||
<IconComponent name="ChevronLeft" className="w-4" />
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<MenuBar />
|
||||
|
|
@ -181,24 +183,14 @@ export default function Header(): JSX.Element {
|
|||
/>
|
||||
</div>
|
||||
</AlertDropdown>
|
||||
{autoLogin && (
|
||||
<button
|
||||
onClick={() => {
|
||||
navigate("/account/api-keys");
|
||||
}}
|
||||
>
|
||||
<IconComponent
|
||||
name="Key"
|
||||
className="side-bar-button-size text-muted-foreground hover:text-accent-foreground"
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<>
|
||||
<Separator orientation="vertical" />
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
<Button
|
||||
variant="none"
|
||||
size="none"
|
||||
data-testid="user-profile-settings"
|
||||
className={
|
||||
"h-7 w-7 rounded-full focus-visible:outline-0 " +
|
||||
|
|
@ -212,6 +204,28 @@ export default function Header(): JSX.Element {
|
|||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
{!autoLogin && (
|
||||
<>
|
||||
<DropdownMenuLabel>
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
className={
|
||||
"h-5 w-5 rounded-full focus-visible:outline-0 " +
|
||||
(userData?.profile_image ??
|
||||
(userData?.id
|
||||
? gradients[
|
||||
parseInt(userData?.id ?? "", 30) %
|
||||
gradients.length
|
||||
]
|
||||
: "bg-gray-500"))
|
||||
}
|
||||
/>
|
||||
{userData?.username ?? "User"}
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
<DropdownMenuLabel>General</DropdownMenuLabel>
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
|
|
|
|||
|
|
@ -10,7 +10,11 @@ import {
|
|||
CommandList,
|
||||
} from "../../../ui/command";
|
||||
import { Input } from "../../../ui/input";
|
||||
import { Popover, PopoverContentWithoutPortal } from "../../../ui/popover";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverContentWithoutPortal,
|
||||
} from "../../../ui/popover";
|
||||
const CustomInputPopover = ({
|
||||
id,
|
||||
refInput,
|
||||
|
|
@ -39,6 +43,9 @@ const CustomInputPopover = ({
|
|||
showOptions,
|
||||
}) => {
|
||||
const setErrorData = useAlertStore.getState().setErrorData;
|
||||
const PopoverContentInput = editNode
|
||||
? PopoverContent
|
||||
: PopoverContentWithoutPortal;
|
||||
|
||||
const handleInputChange = (e) => {
|
||||
if (password) {
|
||||
|
|
@ -107,7 +114,7 @@ const CustomInputPopover = ({
|
|||
data-testid={editNode ? id + "-edit" : id}
|
||||
/>
|
||||
</PopoverAnchor>
|
||||
<PopoverContentWithoutPortal
|
||||
<PopoverContentInput
|
||||
className="nocopy nowheel nopan nodelete nodrag noundo p-0"
|
||||
style={{ minWidth: refInput?.current?.clientWidth ?? "200px" }}
|
||||
side="bottom"
|
||||
|
|
@ -184,7 +191,7 @@ const CustomInputPopover = ({
|
|||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContentWithoutPortal>
|
||||
</PopoverContentInput>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -9,7 +9,11 @@ import {
|
|||
CommandList,
|
||||
} from "../../../ui/command";
|
||||
import { Input } from "../../../ui/input";
|
||||
import { Popover, PopoverContentWithoutPortal } from "../../../ui/popover";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverContentWithoutPortal,
|
||||
} from "../../../ui/popover";
|
||||
const CustomInputPopoverObject = ({
|
||||
id,
|
||||
refInput,
|
||||
|
|
@ -23,6 +27,7 @@ const CustomInputPopoverObject = ({
|
|||
disabled,
|
||||
setShowOptions,
|
||||
required,
|
||||
editNode,
|
||||
className,
|
||||
placeholder,
|
||||
onChange,
|
||||
|
|
@ -34,6 +39,10 @@ const CustomInputPopoverObject = ({
|
|||
handleKeyDown,
|
||||
showOptions,
|
||||
}) => {
|
||||
const PopoverContentInput = editNode
|
||||
? PopoverContent
|
||||
: PopoverContentWithoutPortal;
|
||||
|
||||
const handleInputChange = (e) => {
|
||||
onChange && onChange(e.target.value);
|
||||
};
|
||||
|
|
@ -79,7 +88,7 @@ const CustomInputPopoverObject = ({
|
|||
data-testid={id}
|
||||
/>
|
||||
</PopoverAnchor>
|
||||
<PopoverContentWithoutPortal
|
||||
<PopoverContentInput
|
||||
className="nocopy nowheel nopan nodelete nodrag noundo p-0"
|
||||
style={{ minWidth: refInput?.current?.clientWidth ?? "200px" }}
|
||||
side="bottom"
|
||||
|
|
@ -159,7 +168,7 @@ const CustomInputPopoverObject = ({
|
|||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContentWithoutPortal>
|
||||
</PopoverContentInput>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -108,6 +108,7 @@ export default function InputComponent({
|
|||
setSelectedOptions={setSelectedOptions}
|
||||
options={objectOptions}
|
||||
value={value}
|
||||
editNode={editNode}
|
||||
autoFocus={autoFocus}
|
||||
disabled={disabled}
|
||||
setShowOptions={setShowOptions}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
CONSOLE_ERROR_MSG,
|
||||
CONSOLE_SUCCESS_MSG,
|
||||
INVALID_FILE_ALERT,
|
||||
} from "../../constants/alerts_constants";
|
||||
import { uploadFile } from "../../controllers/API";
|
||||
|
|
|
|||
|
|
@ -32,11 +32,11 @@ export default function InputGlobalComponent({
|
|||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||
|
||||
useEffect(() => {
|
||||
if (data.node?.template[name])
|
||||
if (data)
|
||||
if (
|
||||
globalVariablesEntries &&
|
||||
!globalVariablesEntries.includes(data.node?.template[name].value) &&
|
||||
data.node?.template[name].load_from_db
|
||||
!globalVariablesEntries.includes(data.value) &&
|
||||
data.load_from_db
|
||||
) {
|
||||
setTimeout(() => {
|
||||
onChange("", true);
|
||||
|
|
@ -46,17 +46,11 @@ export default function InputGlobalComponent({
|
|||
}, [globalVariablesEntries]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!data.node?.template[name].value &&
|
||||
data.node?.template[name].display_name
|
||||
) {
|
||||
if (
|
||||
unavaliableFields[data.node?.template[name].display_name!] &&
|
||||
!disabled
|
||||
) {
|
||||
if (!data.value && data.display_name) {
|
||||
if (unavaliableFields[data.display_name!] && !disabled) {
|
||||
setTimeout(() => {
|
||||
setDb(true);
|
||||
onChange(unavaliableFields[data.node?.template[name].display_name!]);
|
||||
onChange(unavaliableFields[data.display_name!]);
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
|
@ -68,10 +62,7 @@ export default function InputGlobalComponent({
|
|||
await deleteGlobalVariable(id)
|
||||
.then(() => {
|
||||
removeGlobalVariable(key);
|
||||
if (
|
||||
data?.node?.template[name].value === key &&
|
||||
data?.node?.template[name].load_from_db
|
||||
) {
|
||||
if (data?.value === key && data?.load_from_db) {
|
||||
onChange("");
|
||||
setDb(false);
|
||||
}
|
||||
|
|
@ -94,8 +85,8 @@ export default function InputGlobalComponent({
|
|||
id={"input-" + name}
|
||||
editNode={editNode}
|
||||
disabled={disabled}
|
||||
password={data.node?.template[name].password ?? false}
|
||||
value={data.node?.template[name].value ?? ""}
|
||||
password={data.password ?? false}
|
||||
value={data.value ?? ""}
|
||||
options={globalVariablesEntries}
|
||||
optionsPlaceholder={"Global Variables"}
|
||||
optionsIcon="Globe"
|
||||
|
|
@ -138,10 +129,10 @@ export default function InputGlobalComponent({
|
|||
</DeleteConfirmationModal>
|
||||
)}
|
||||
selectedOption={
|
||||
data?.node?.template[name].load_from_db &&
|
||||
data?.load_from_db &&
|
||||
globalVariablesEntries &&
|
||||
globalVariablesEntries.includes(data?.node?.template[name].value ?? "")
|
||||
? data?.node?.template[name].value
|
||||
globalVariablesEntries.includes(data?.value ?? "")
|
||||
? data?.value
|
||||
: ""
|
||||
}
|
||||
setSelectedOption={(value) => {
|
||||
|
|
|
|||
|
|
@ -1,19 +1,20 @@
|
|||
import { ColDef, ColGroupDef } from "ag-grid-community";
|
||||
import "ag-grid-community/styles/ag-grid.css"; // Mandatory CSS required by the grid
|
||||
import "ag-grid-community/styles/ag-theme-balham.css"; // Optional Theme applied to the grid
|
||||
import { FlowPoolObjectType } from "../../types/chat";
|
||||
import { extractColumnsFromRows } from "../../utils/utils";
|
||||
import TableComponent from "../tableComponent";
|
||||
|
||||
function RecordsOutputComponent({
|
||||
flowPool,
|
||||
pagination,
|
||||
rows,
|
||||
columnMode = "union",
|
||||
}: {
|
||||
flowPool: FlowPoolObjectType;
|
||||
pagination: boolean;
|
||||
rows: any;
|
||||
columnMode?: "intersection" | "union";
|
||||
}) {
|
||||
const rows = flowPool?.data?.artifacts?.records ?? [];
|
||||
const columns = extractColumnsFromRows(rows, "union");
|
||||
const columns = extractColumnsFromRows(rows, columnMode);
|
||||
|
||||
const columnDefs = columns.map((col, idx) => ({
|
||||
...col,
|
||||
resizable: idx !== columns.length - 1,
|
||||
|
|
@ -22,6 +23,7 @@ function RecordsOutputComponent({
|
|||
|
||||
return (
|
||||
<TableComponent
|
||||
key={"recordsOutputComponent"}
|
||||
overlayNoRowsTemplate="No data available"
|
||||
suppressRowClickSelection={true}
|
||||
pagination={pagination}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export default function ShadTooltip({
|
|||
delayDuration = 500,
|
||||
}: ShadToolTipType): JSX.Element {
|
||||
return (
|
||||
<Tooltip delayDuration={delayDuration}>
|
||||
<Tooltip defaultOpen={!children} delayDuration={delayDuration}>
|
||||
<TooltipTrigger asChild={asChild}>{children}</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className={cn(styleClasses, "max-w-96")}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { Link } from "react-router-dom";
|
||||
import { cn } from "../../../../utils/utils";
|
||||
import { buttonVariants } from "../../../ui/button";
|
||||
import ForwardedIconComponent from "../../../genericIconComponent";
|
||||
|
||||
type SideBarButtonsComponentProps = {
|
||||
items: {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { useLocation } from "react-router-dom";
|
|||
import { FolderType } from "../../../../pages/MainPage/entities";
|
||||
import { addFolder, updateFolder } from "../../../../pages/MainPage/services";
|
||||
import { handleDownloadFolderFn } from "../../../../pages/MainPage/utils/handle-download-folder";
|
||||
import useAlertStore from "../../../../stores/alertStore";
|
||||
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
|
||||
import { useFolderStore } from "../../../../stores/foldersStore";
|
||||
import { handleKeyDown } from "../../../../utils/reactflowUtils";
|
||||
|
|
@ -15,16 +16,13 @@ import { Input } from "../../../ui/input";
|
|||
import useFileDrop from "../../hooks/use-on-file-drop";
|
||||
|
||||
type SideBarFoldersButtonsComponentProps = {
|
||||
folders: FolderType[];
|
||||
pathname: string;
|
||||
handleChangeFolder?: (id: string) => void;
|
||||
handleEditFolder?: (item: FolderType) => void;
|
||||
handleDeleteFolder?: (item: FolderType) => void;
|
||||
};
|
||||
const SideBarFoldersButtonsComponent = ({
|
||||
pathname,
|
||||
handleChangeFolder,
|
||||
handleEditFolder,
|
||||
handleDeleteFolder,
|
||||
}: SideBarFoldersButtonsComponentProps) => {
|
||||
const refInput = useRef<HTMLInputElement>(null);
|
||||
|
|
@ -51,6 +49,8 @@ const SideBarFoldersButtonsComponent = ({
|
|||
const location = useLocation();
|
||||
const folderId = location?.state?.folderId ?? myCollectionId;
|
||||
const getFolderById = useFolderStore((state) => state.getFolderById);
|
||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||
const setSuccessData = useAlertStore((state) => state.setSuccessData);
|
||||
|
||||
const handleFolderChange = (folderId: string) => {
|
||||
getFolderById(folderId);
|
||||
|
|
@ -62,7 +62,20 @@ const SideBarFoldersButtonsComponent = ({
|
|||
);
|
||||
|
||||
const handleUploadFlowsToFolder = () => {
|
||||
uploadFolder(folderId);
|
||||
uploadFolder(folderId)
|
||||
.then(() => {
|
||||
getFolderById(folderId);
|
||||
setSuccessData({
|
||||
title: "Uploaded successfully",
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
setErrorData({
|
||||
title: `Error on upload`,
|
||||
list: [err["response"]["data"]],
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleDownloadFolder = (id: string) => {
|
||||
|
|
|
|||
|
|
@ -5,9 +5,6 @@ import { cn } from "../../utils/utils";
|
|||
import HorizontalScrollFadeComponent from "../horizontalScrollFadeComponent";
|
||||
import SideBarButtonsComponent from "./components/sideBarButtons";
|
||||
import SideBarFoldersButtonsComponent from "./components/sideBarFolderButtons";
|
||||
import { addFolder } from "../../pages/MainPage/services";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import useFlowStore from "../../stores/flowStore";
|
||||
|
||||
type SidebarNavProps = {
|
||||
items: {
|
||||
|
|
@ -15,7 +12,6 @@ type SidebarNavProps = {
|
|||
title: string;
|
||||
icon: React.ReactNode;
|
||||
}[];
|
||||
handleOpenNewFolderModal?: () => void;
|
||||
handleChangeFolder?: (id: string) => void;
|
||||
handleEditFolder?: (item: FolderType) => void;
|
||||
handleDeleteFolder?: (item: FolderType) => void;
|
||||
|
|
@ -48,10 +44,8 @@ export default function SidebarNav({
|
|||
folders?.length > 0 &&
|
||||
isFolderPath && (
|
||||
<SideBarFoldersButtonsComponent
|
||||
folders={folders}
|
||||
pathname={pathname}
|
||||
handleChangeFolder={handleChangeFolder}
|
||||
handleEditFolder={handleEditFolder}
|
||||
handleDeleteFolder={handleDeleteFolder}
|
||||
/>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
import { cn } from "../../../../utils/utils";
|
||||
|
||||
export default function ResetColumns({
|
||||
resetGrid,
|
||||
}: {
|
||||
resetGrid: () => void;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<div className={cn("absolute bottom-4 left-6")}>
|
||||
<span
|
||||
className="cursor-pointer underline"
|
||||
onClick={() => {
|
||||
resetGrid();
|
||||
}}
|
||||
>
|
||||
Reset Columns
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
import { cn } from "../../../../utils/utils";
|
||||
import ShadTooltip from "../../../shadTooltipComponent";
|
||||
import { Button } from "../../../ui/button";
|
||||
import IconComponent from "../../../genericIconComponent";
|
||||
|
||||
export default function TableOptions({
|
||||
resetGrid,
|
||||
duplicateRow,
|
||||
deleteRow,
|
||||
hasSelection,
|
||||
stateChange,
|
||||
}: {
|
||||
resetGrid: () => void;
|
||||
duplicateRow?: () => void;
|
||||
deleteRow?: () => void;
|
||||
hasSelection: boolean;
|
||||
stateChange: boolean;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<div className={cn("absolute bottom-4 left-6")}>
|
||||
<div className="flex items-center gap-2">
|
||||
<div>
|
||||
<ShadTooltip content="Reset Columns">
|
||||
<Button
|
||||
variant="none"
|
||||
size="none"
|
||||
onClick={() => {
|
||||
resetGrid();
|
||||
}}
|
||||
disabled={!stateChange}
|
||||
>
|
||||
<IconComponent
|
||||
name="RotateCcw"
|
||||
className={cn("h-5 w-5 text-primary transition-all")}
|
||||
/>
|
||||
</Button>
|
||||
</ShadTooltip>
|
||||
</div>
|
||||
{duplicateRow && (
|
||||
<div>
|
||||
<ShadTooltip
|
||||
content={
|
||||
!hasSelection ? (
|
||||
<span>Select items to duplicate</span>
|
||||
) : (
|
||||
<span>Duplicate selected items</span>
|
||||
)
|
||||
}
|
||||
>
|
||||
<Button
|
||||
variant="none"
|
||||
size="none"
|
||||
onClick={duplicateRow}
|
||||
disabled={!hasSelection}
|
||||
>
|
||||
<IconComponent
|
||||
name="Copy"
|
||||
className={cn("h-5 w-5 text-primary transition-all")}
|
||||
/>
|
||||
</Button>
|
||||
</ShadTooltip>
|
||||
</div>
|
||||
)}
|
||||
{deleteRow && (
|
||||
<div>
|
||||
<ShadTooltip
|
||||
content={
|
||||
!hasSelection ? (
|
||||
<span>Select items to delete</span>
|
||||
) : (
|
||||
<span>Delete selected items</span>
|
||||
)
|
||||
}
|
||||
>
|
||||
<Button
|
||||
variant="none"
|
||||
size="none"
|
||||
onClick={deleteRow}
|
||||
disabled={!hasSelection}
|
||||
>
|
||||
<IconComponent
|
||||
name="Trash2"
|
||||
className={cn(
|
||||
"h-5 w-5 text-primary transition-all",
|
||||
!hasSelection ? "" : "hover:text-destructive",
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
</ShadTooltip>
|
||||
</div>
|
||||
)}{" "}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
import { CustomCellRendererProps } from "ag-grid-react";
|
||||
import { cn, isTimeStampString } from "../../utils/utils";
|
||||
import ArrayReader from "../arrayReaderComponent";
|
||||
import DateReader from "../dateReaderComponent";
|
||||
import NumberReader from "../numberReader";
|
||||
import ObjectRender from "../objectRender";
|
||||
import StringReader from "../stringReaderComponent";
|
||||
import { Badge } from "../ui/badge";
|
||||
import { cn, isTimeStampString } from "../../../../utils/utils";
|
||||
import ArrayReader from "../../../arrayReaderComponent";
|
||||
import DateReader from "../../../dateReaderComponent";
|
||||
import NumberReader from "../../../numberReader";
|
||||
import ObjectRender from "../../../objectRender";
|
||||
import StringReader from "../../../stringReaderComponent";
|
||||
import { Badge } from "../../../ui/badge";
|
||||
|
||||
export default function TableAutoCellRender({
|
||||
value,
|
||||
|
|
@ -43,7 +43,6 @@ export default function TableAutoCellRender({
|
|||
} else {
|
||||
return <StringReader string={value} />;
|
||||
}
|
||||
break;
|
||||
case "number":
|
||||
return <NumberReader number={value} />;
|
||||
default:
|
||||
|
|
@ -52,7 +51,7 @@ export default function TableAutoCellRender({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="group flex h-full w-full items-center align-middle">
|
||||
<div className="group flex h-full w-full items-center truncate align-middle">
|
||||
{getCellType()}
|
||||
</div>
|
||||
);
|
||||
|
|
@ -0,0 +1,266 @@
|
|||
import { CustomCellRendererProps } from "ag-grid-react";
|
||||
import { cloneDeep } from "lodash";
|
||||
import { useState } from "react";
|
||||
import useFlowStore from "../../../../stores/flowStore";
|
||||
import {
|
||||
convertObjToArray,
|
||||
convertValuesToNumbers,
|
||||
hasDuplicateKeys,
|
||||
scapedJSONStringfy,
|
||||
} from "../../../../utils/reactflowUtils";
|
||||
import { classNames } from "../../../../utils/utils";
|
||||
import CodeAreaComponent from "../../../codeAreaComponent";
|
||||
import DictComponent from "../../../dictComponent";
|
||||
import Dropdown from "../../../dropdownComponent";
|
||||
import FloatComponent from "../../../floatComponent";
|
||||
import InputFileComponent from "../../../inputFileComponent";
|
||||
import InputGlobalComponent from "../../../inputGlobalComponent";
|
||||
import InputListComponent from "../../../inputListComponent";
|
||||
import IntComponent from "../../../intComponent";
|
||||
import KeypairListComponent from "../../../keypairListComponent";
|
||||
import PromptAreaComponent from "../../../promptComponent";
|
||||
import TextAreaComponent from "../../../textAreaComponent";
|
||||
import ToggleShadComponent from "../../../toggleShadComponent";
|
||||
|
||||
export default function TableNodeCellRender({
|
||||
node: { data },
|
||||
value: {
|
||||
value,
|
||||
nodeClass,
|
||||
handleOnNewValue: handleOnNewValueNode,
|
||||
handleOnChangeDb,
|
||||
},
|
||||
}: CustomCellRendererProps) {
|
||||
const handleOnNewValue = (newValue: any, name: string) => {
|
||||
handleOnNewValueNode(newValue, name);
|
||||
setTemplateData((old) => {
|
||||
let newData = cloneDeep(old);
|
||||
newData.value = newValue;
|
||||
return newData;
|
||||
});
|
||||
setTemplateValue(newValue);
|
||||
};
|
||||
|
||||
const [templateValue, setTemplateValue] = useState(value);
|
||||
const [templateData, setTemplateData] = useState(data);
|
||||
|
||||
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
|
||||
const edges = useFlowStore((state) => state.edges);
|
||||
|
||||
const id = {
|
||||
inputTypes: templateData.input_types,
|
||||
type: templateData.type,
|
||||
id: nodeClass.id,
|
||||
fieldName: templateData.key,
|
||||
};
|
||||
const disabled =
|
||||
edges.some(
|
||||
(edge) =>
|
||||
edge.targetHandle ===
|
||||
scapedJSONStringfy(
|
||||
templateData.proxy
|
||||
? {
|
||||
...id,
|
||||
proxy: templateData.proxy,
|
||||
}
|
||||
: id,
|
||||
),
|
||||
) ?? false;
|
||||
function getCellType() {
|
||||
switch (templateData.type) {
|
||||
case "str":
|
||||
if (!templateData.options) {
|
||||
return templateData?.list ? (
|
||||
<InputListComponent
|
||||
componentName={templateData.key ?? undefined}
|
||||
editNode={true}
|
||||
disabled={disabled}
|
||||
value={
|
||||
!templateValue || templateValue === "" ? [""] : templateValue
|
||||
}
|
||||
onChange={(value: string[]) => {
|
||||
handleOnNewValue(value, templateData.key);
|
||||
}}
|
||||
/>
|
||||
) : templateData.multiline ? (
|
||||
<TextAreaComponent
|
||||
id={"textarea-edit-" + templateData.name}
|
||||
data-testid={"textarea-edit-" + templateData.name}
|
||||
disabled={disabled}
|
||||
editNode={true}
|
||||
value={templateValue ?? ""}
|
||||
onChange={(value: string | string[]) => {
|
||||
handleOnNewValue(value, templateData.key);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<InputGlobalComponent
|
||||
disabled={disabled}
|
||||
editNode={true}
|
||||
onChange={(value) => handleOnNewValue(value, templateData.key)}
|
||||
setDb={(value) => {
|
||||
handleOnChangeDb(value, templateData.key);
|
||||
}}
|
||||
name={templateData.key}
|
||||
data={templateData}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Dropdown
|
||||
editNode={true}
|
||||
options={templateData.options}
|
||||
onSelect={(value) => handleOnNewValue(value, templateData.key)}
|
||||
value={templateValue ?? "Choose an option"}
|
||||
id={"dropdown-edit-" + templateData.name}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
case "NestedDict":
|
||||
return (
|
||||
<DictComponent
|
||||
disabled={disabled}
|
||||
editNode={true}
|
||||
value={templateValue.toString() === "{}" ? {} : templateValue}
|
||||
onChange={(newValue) => {
|
||||
handleOnNewValue(newValue, templateData.key);
|
||||
}}
|
||||
id="editnode-div-dict-input"
|
||||
/>
|
||||
);
|
||||
|
||||
case "dict":
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
"max-h-48 w-full overflow-auto custom-scroll",
|
||||
templateValue?.length > 1 ? "my-3" : "",
|
||||
)}
|
||||
>
|
||||
<KeypairListComponent
|
||||
disabled={disabled}
|
||||
editNode={true}
|
||||
value={
|
||||
templateValue?.length === 0 || !templateValue
|
||||
? [{ "": "" }]
|
||||
: convertObjToArray(templateValue, templateData.type)
|
||||
}
|
||||
duplicateKey={errorDuplicateKey}
|
||||
onChange={(newValue) => {
|
||||
const valueToNumbers = convertValuesToNumbers(newValue);
|
||||
setErrorDuplicateKey(hasDuplicateKeys(valueToNumbers));
|
||||
handleOnNewValue(valueToNumbers, templateData.key);
|
||||
}}
|
||||
isList={templateData.list ?? false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
case "bool":
|
||||
return (
|
||||
<ToggleShadComponent
|
||||
id={"toggle-edit-" + templateData.name}
|
||||
disabled={disabled}
|
||||
enabled={templateValue}
|
||||
setEnabled={(isEnabled) => {
|
||||
handleOnNewValue(isEnabled, templateData.key);
|
||||
}}
|
||||
size="small"
|
||||
editNode={true}
|
||||
/>
|
||||
);
|
||||
|
||||
case "float":
|
||||
return (
|
||||
<FloatComponent
|
||||
disabled={disabled}
|
||||
editNode={true}
|
||||
rangeSpec={templateData.rangeSpec}
|
||||
value={templateValue ?? ""}
|
||||
onChange={(value) => {
|
||||
handleOnNewValue(value, templateData.key);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
case "int":
|
||||
return (
|
||||
<IntComponent
|
||||
rangeSpec={templateData.rangeSpec}
|
||||
id={"edit-int-input-" + templateData.name}
|
||||
disabled={disabled}
|
||||
editNode={true}
|
||||
value={templateValue ?? ""}
|
||||
onChange={(value) => {
|
||||
handleOnNewValue(value, templateData.key);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
case "file":
|
||||
return (
|
||||
<InputFileComponent
|
||||
editNode={true}
|
||||
disabled={disabled}
|
||||
value={templateValue ?? ""}
|
||||
onChange={(value: string | string[]) => {
|
||||
handleOnNewValue(value, templateData.key);
|
||||
}}
|
||||
fileTypes={templateData.fileTypes}
|
||||
onFileChange={(filePath: string) => {
|
||||
templateData.file_path = filePath;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
case "prompt":
|
||||
return (
|
||||
<PromptAreaComponent
|
||||
readonly={nodeClass.flow ? true : false}
|
||||
field_name={templateData.key}
|
||||
editNode={true}
|
||||
disabled={disabled}
|
||||
nodeClass={nodeClass}
|
||||
setNodeClass={(value) => {
|
||||
nodeClass = value;
|
||||
}}
|
||||
value={templateValue ?? ""}
|
||||
onChange={(value: string | string[]) => {
|
||||
handleOnNewValue(value, templateData.key);
|
||||
}}
|
||||
id={"prompt-area-edit-" + templateData.name}
|
||||
data-testid={"modal-prompt-input-" + templateData.name}
|
||||
/>
|
||||
);
|
||||
|
||||
case "code":
|
||||
return (
|
||||
<CodeAreaComponent
|
||||
readonly={nodeClass.flow && templateData.dynamic ? true : false}
|
||||
dynamic={templateData.dynamic ?? false}
|
||||
setNodeClass={(value) => {
|
||||
nodeClass = value;
|
||||
}}
|
||||
nodeClass={nodeClass}
|
||||
disabled={disabled}
|
||||
editNode={true}
|
||||
value={templateValue ?? ""}
|
||||
onChange={(value: string | string[]) => {
|
||||
handleOnNewValue(value, templateData.key);
|
||||
}}
|
||||
id={"code-area-edit" + templateData.name}
|
||||
/>
|
||||
);
|
||||
case "Any":
|
||||
return <>-</>;
|
||||
default:
|
||||
return String(templateValue);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="group flex h-full w-[300px] items-center justify-center py-2.5">
|
||||
{getCellType()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import { CustomCellRendererProps } from "ag-grid-react";
|
||||
import { useState } from "react";
|
||||
import ToggleShadComponent from "../../../toggleShadComponent";
|
||||
|
||||
export default function TableToggleCellRender({
|
||||
value: { name, enabled, setEnabled },
|
||||
}: CustomCellRendererProps) {
|
||||
const [value, setValue] = useState(enabled);
|
||||
|
||||
return (
|
||||
<div className="flex h-full items-center">
|
||||
<ToggleShadComponent
|
||||
id={"show" + name}
|
||||
enabled={value}
|
||||
setEnabled={(e) => {
|
||||
setValue(e);
|
||||
setEnabled(e);
|
||||
}}
|
||||
size="small"
|
||||
editNode={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import { CustomTooltipProps } from "ag-grid-react";
|
||||
|
||||
export default function TableTooltipRender({ value }: CustomTooltipProps) {
|
||||
return (
|
||||
<div className="z-45 overflow-y-auto rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-50 data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1">
|
||||
{value}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,22 +1,28 @@
|
|||
import "ag-grid-community/styles/ag-grid.css"; // Mandatory CSS required by the grid
|
||||
import "ag-grid-community/styles/ag-theme-quartz.css"; // Optional Theme applied to the grid
|
||||
import { AgGridReact, AgGridReactProps } from "ag-grid-react";
|
||||
import { ElementRef, forwardRef, useCallback } from "react";
|
||||
import { ElementRef, forwardRef, useRef, useState } from "react";
|
||||
import {
|
||||
DEFAULT_TABLE_ALERT_MSG,
|
||||
DEFAULT_TABLE_ALERT_TITLE,
|
||||
} from "../../constants/constants";
|
||||
import { useDarkStore } from "../../stores/darkStore";
|
||||
import "../../style/ag-theme-shadcn.css"; // Custom CSS applied to the grid
|
||||
import { cn } from "../../utils/utils";
|
||||
import { cn, toTitleCase } from "../../utils/utils";
|
||||
import ForwardedIconComponent from "../genericIconComponent";
|
||||
import { Alert, AlertDescription, AlertTitle } from "../ui/alert";
|
||||
import TableOptions from "./components/TableOptions";
|
||||
import resetGrid from "./utils/reset-grid-columns";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
interface TableComponentProps extends AgGridReactProps {
|
||||
columnDefs: NonNullable<AgGridReactProps["columnDefs"]>;
|
||||
rowData: NonNullable<AgGridReactProps["rowData"]>;
|
||||
alertTitle?: string;
|
||||
alertDescription?: string;
|
||||
editable?: boolean | string[];
|
||||
onDelete?: () => void;
|
||||
onDuplicate?: () => void;
|
||||
}
|
||||
|
||||
const TableComponent = forwardRef<
|
||||
|
|
@ -31,7 +37,73 @@ const TableComponent = forwardRef<
|
|||
},
|
||||
ref,
|
||||
) => {
|
||||
let colDef = props.columnDefs.map((col, index) => {
|
||||
let newCol = {
|
||||
...col,
|
||||
headerName: toTitleCase(col.headerName),
|
||||
};
|
||||
if (index === props.columnDefs.length - 1) {
|
||||
newCol = {
|
||||
...newCol,
|
||||
resizable: false,
|
||||
};
|
||||
}
|
||||
if (props.onSelectionChanged && index === 0) {
|
||||
newCol = {
|
||||
...newCol,
|
||||
checkboxSelection: true,
|
||||
headerCheckboxSelection: true,
|
||||
headerCheckboxSelectionFilteredOnly: true,
|
||||
};
|
||||
}
|
||||
if (
|
||||
(typeof props.editable === "boolean" && props.editable) ||
|
||||
(Array.isArray(props.editable) &&
|
||||
props.editable.includes(newCol.headerName ?? ""))
|
||||
) {
|
||||
newCol = {
|
||||
...newCol,
|
||||
editable: true,
|
||||
};
|
||||
}
|
||||
return newCol;
|
||||
});
|
||||
const gridRef = useRef(null);
|
||||
// @ts-ignore
|
||||
const realRef: React.MutableRefObject<AgGridReact> = ref?.current
|
||||
? ref
|
||||
: gridRef;
|
||||
const dark = useDarkStore((state) => state.dark);
|
||||
const initialColumnDefs = useRef(colDef);
|
||||
const [columnStateChange, setColumnStateChange] = useState(false);
|
||||
|
||||
const makeLastColumnNonResizable = (columnDefs) => {
|
||||
columnDefs.forEach((colDef, index) => {
|
||||
colDef.resizable = index !== columnDefs.length - 1;
|
||||
});
|
||||
return columnDefs;
|
||||
};
|
||||
|
||||
const onGridReady = (params) => {
|
||||
// @ts-ignore
|
||||
realRef.current = params;
|
||||
const updatedColumnDefs = makeLastColumnNonResizable([...colDef]);
|
||||
params.api.setGridOption("columnDefs", updatedColumnDefs);
|
||||
initialColumnDefs.current = params.api.getColumnDefs();
|
||||
if (props.onGridReady) props.onGridReady(params);
|
||||
setTimeout(() => {
|
||||
setColumnStateChange(false);
|
||||
}, 50);
|
||||
};
|
||||
|
||||
const onColumnMoved = (params) => {
|
||||
const updatedColumnDefs = makeLastColumnNonResizable(
|
||||
params.columnApi.getAllGridColumns().map((col) => col.getColDef()),
|
||||
);
|
||||
params.api.setGridOption("columnDefs", updatedColumnDefs);
|
||||
if (props.onColumnMoved) props.onColumnMoved(params);
|
||||
};
|
||||
|
||||
if (props.rowData.length === 0) {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center rounded-md border">
|
||||
|
|
@ -46,12 +118,12 @@ const TableComponent = forwardRef<
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
dark ? "ag-theme-quartz-dark" : "ag-theme-quartz",
|
||||
"ag-theme-shadcn flex h-full flex-col",
|
||||
"relative",
|
||||
)} // applying the grid theme
|
||||
>
|
||||
<AgGridReact
|
||||
|
|
@ -59,8 +131,33 @@ const TableComponent = forwardRef<
|
|||
className={cn(props.className, "custom-scroll")}
|
||||
defaultColDef={{
|
||||
minWidth: 100,
|
||||
autoHeight: true,
|
||||
}}
|
||||
columnDefs={colDef}
|
||||
ref={realRef}
|
||||
pagination={true}
|
||||
onGridReady={onGridReady}
|
||||
onColumnMoved={onColumnMoved}
|
||||
onStateUpdated={(e) => {
|
||||
if (
|
||||
e.sources.includes("columnVisibility") ||
|
||||
e.sources.includes("columnOrder")
|
||||
) {
|
||||
setColumnStateChange(true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<TableOptions
|
||||
stateChange={columnStateChange}
|
||||
hasSelection={realRef.current?.api.getSelectedRows().length > 0}
|
||||
duplicateRow={props.onDuplicate ? props.onDuplicate : undefined}
|
||||
deleteRow={props.onDelete ? props.onDelete : undefined}
|
||||
resetGrid={() => {
|
||||
resetGrid(realRef, initialColumnDefs);
|
||||
setTimeout(() => {
|
||||
setColumnStateChange(false);
|
||||
}, 100);
|
||||
}}
|
||||
ref={ref}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
export default function resetGrid(ref, initialColumnDefs) {
|
||||
if (ref?.current && ref?.current.api) {
|
||||
ref.current.api.resetColumnState();
|
||||
if (initialColumnDefs.current) {
|
||||
const resetColumns = ref.current.api.applyColumnState({
|
||||
state: initialColumnDefs.current,
|
||||
applyOrder: true,
|
||||
});
|
||||
return resetColumns;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -29,20 +29,18 @@ export default function ToggleShadComponent({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className={disabled ? "pointer-events-none cursor-not-allowed " : ""}>
|
||||
<Switch
|
||||
id={id}
|
||||
data-testid={id}
|
||||
style={{
|
||||
transform: `scaleX(${scaleX}) scaleY(${scaleY})`,
|
||||
}}
|
||||
disabled={disabled}
|
||||
className=""
|
||||
checked={enabled}
|
||||
onCheckedChange={(isEnabled: boolean) => {
|
||||
setEnabled(isEnabled);
|
||||
}}
|
||||
></Switch>
|
||||
</div>
|
||||
<Switch
|
||||
id={id}
|
||||
data-testid={id}
|
||||
style={{
|
||||
transform: `scaleX(${scaleX}) scaleY(${scaleY})`,
|
||||
}}
|
||||
disabled={disabled}
|
||||
className=""
|
||||
checked={enabled}
|
||||
onCheckedChange={(isEnabled: boolean) => {
|
||||
setEnabled(isEnabled);
|
||||
}}
|
||||
></Switch>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|||
variant,
|
||||
size,
|
||||
loading,
|
||||
type,
|
||||
disabled,
|
||||
asChild = false,
|
||||
children,
|
||||
|
|
@ -76,6 +77,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
disabled={loading || disabled}
|
||||
{...(asChild ? {} : { type: type || "button" })}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -31,11 +31,7 @@ function RefreshButton({
|
|||
|
||||
// icon class name should take into account the disabled state and the loading state
|
||||
const disabledIconTextClass = disabled ? "text-muted-foreground" : "";
|
||||
const iconClassName = cn(
|
||||
"h-4 w-4",
|
||||
isLoading ? "animate-spin" : "animate-wiggle",
|
||||
disabledIconTextClass
|
||||
);
|
||||
const iconClassName = cn("h-4 w-4 animate-wiggle", disabledIconTextClass);
|
||||
|
||||
return (
|
||||
<Button
|
||||
|
|
@ -44,10 +40,11 @@ function RefreshButton({
|
|||
className={classNames}
|
||||
onClick={handleClick}
|
||||
id={id}
|
||||
loading={isLoading}
|
||||
>
|
||||
{button_text && <span className="mr-1">{button_text}</span>}
|
||||
<IconComponent
|
||||
name={isLoading ? "Loader2" : "RefreshCcw"}
|
||||
name={"RefreshCcw"}
|
||||
className={iconClassName}
|
||||
id={id + "-icon"}
|
||||
/>
|
||||
|
|
|
|||
45
src/frontend/src/components/ui/toggle.tsx
Normal file
45
src/frontend/src/components/ui/toggle.tsx
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
"use client";
|
||||
|
||||
import * as TogglePrimitive from "@radix-ui/react-toggle";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "../../utils/utils";
|
||||
|
||||
const toggleVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-transparent",
|
||||
outline:
|
||||
"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-3",
|
||||
sm: "h-9 px-2.5",
|
||||
lg: "h-11 px-5",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const Toggle = React.forwardRef<
|
||||
React.ElementRef<typeof TogglePrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> &
|
||||
VariantProps<typeof toggleVariants>
|
||||
>(({ className, variant, size, ...props }, ref) => (
|
||||
<TogglePrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(toggleVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
Toggle.displayName = TogglePrimitive.Root.displayName;
|
||||
|
||||
export { Toggle, toggleVariants };
|
||||
|
|
@ -23,6 +23,7 @@ export const USER_EDIT_ERROR_ALERT = "Error on edit user";
|
|||
export const USER_ADD_ERROR_ALERT = "Error when adding new user";
|
||||
export const SIGNIN_ERROR_ALERT = "Error signing in";
|
||||
export const DEL_KEY_ERROR_ALERT = "Error on delete key";
|
||||
export const DEL_KEY_ERROR_ALERT_PLURAL = "Error on delete keys";
|
||||
export const UPLOAD_ERROR_ALERT = "Error uploading file";
|
||||
export const WRONG_FILE_ERROR_ALERT = "Invalid file type";
|
||||
export const UPLOAD_ALERT_LIST = "Please upload a JSON file";
|
||||
|
|
@ -54,6 +55,7 @@ export const USER_DEL_SUCCESS_ALERT = "Success! User deleted!";
|
|||
export const USER_EDIT_SUCCESS_ALERT = "Success! User edited!";
|
||||
export const USER_ADD_SUCCESS_ALERT = "Success! New user added!";
|
||||
export const DEL_KEY_SUCCESS_ALERT = "Success! Key deleted!";
|
||||
export const DEL_KEY_SUCCESS_ALERT_PLURAL = "Success! Keys deleted!";
|
||||
export const FLOW_BUILD_SUCCESS_ALERT = `Flow built successfully`;
|
||||
export const SAVE_SUCCESS_ALERT = "Changes saved successfully!";
|
||||
|
||||
|
|
|
|||
|
|
@ -613,11 +613,8 @@ export const FETCH_ERROR_DESCRIPION =
|
|||
|
||||
export const SIGN_UP_SUCCESS = "Account created! Await admin activation. ";
|
||||
|
||||
export const API_PAGE_PARAGRAPH_1 =
|
||||
"Your secret API keys are listed below. Please note that we do not display your secret API keys again after you generate them.";
|
||||
|
||||
export const API_PAGE_PARAGRAPH_2 =
|
||||
"Do not share your API key with others, or expose it in the browser or other client-side code.";
|
||||
export const API_PAGE_PARAGRAPH =
|
||||
"Your secret API keys are listed below. Do not share your API key with others, or expose it in the browser or other client-side code.";
|
||||
|
||||
export const API_PAGE_USER_KEYS =
|
||||
"This user does not have any keys assigned at the moment.";
|
||||
|
|
@ -671,7 +668,7 @@ export const ZERO_NOTIFICATIONS = "No new notifications";
|
|||
export const SUCCESS_BUILD = "Built sucessfully ✨";
|
||||
|
||||
export const ALERT_SAVE_WITH_API =
|
||||
"Caution: Uncheck this box only removes API keys from fields specifically designated for API keys.";
|
||||
"Caution: Unchecking this box only removes API keys from fields specifically designated for API keys.";
|
||||
|
||||
export const SAVE_WITH_API_CHECKBOX = "Save with my API keys";
|
||||
export const EDIT_TEXT_MODAL_TITLE = "Edit Text";
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import {
|
|||
} from "../../types/api/index";
|
||||
import { UserInputType } from "../../types/components";
|
||||
import { FlowStyleType, FlowType } from "../../types/flow";
|
||||
import { Message } from "../../types/messages";
|
||||
import { StoreComponentResponse } from "../../types/store";
|
||||
import { FlowPoolType } from "../../types/zustand/flow";
|
||||
import { extractColumnsFromRows } from "../../utils/utils";
|
||||
|
|
@ -964,11 +965,16 @@ export async function postBuildVertex(
|
|||
flowId: string,
|
||||
vertexId: string,
|
||||
input_value: string,
|
||||
files?: string[],
|
||||
): Promise<AxiosResponse<VertexBuildTypeAPI>> {
|
||||
// input_value is optional and is a query parameter
|
||||
const data = { inputs: { input_value: input_value ?? "" } };
|
||||
if (data && files) {
|
||||
data["files"] = files;
|
||||
}
|
||||
return await api.post(
|
||||
`${BASE_URL_API}build/${flowId}/vertices/${vertexId}`,
|
||||
input_value ? { inputs: { input_value: input_value } } : undefined,
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1052,16 +1058,38 @@ export async function getTransactionTable(
|
|||
}
|
||||
|
||||
export async function getMessagesTable(
|
||||
id: string,
|
||||
mode: "intersection" | "union",
|
||||
id?: string,
|
||||
excludedFields?: string[],
|
||||
params = {},
|
||||
): Promise<{ rows: Array<object>; columns: Array<ColDef | ColGroupDef> }> {
|
||||
): Promise<{ rows: Array<Message>; columns: Array<ColDef | ColGroupDef> }> {
|
||||
const config = {};
|
||||
config["params"] = { flow_id: id };
|
||||
if (id) {
|
||||
config["params"] = { flow_id: id };
|
||||
}
|
||||
if (params) {
|
||||
config["params"] = { ...config["params"], ...params };
|
||||
}
|
||||
const rows = await api.get(`${BASE_URL_API}monitor/messages`, config);
|
||||
const columns = extractColumnsFromRows(rows.data, mode);
|
||||
const columns = extractColumnsFromRows(rows.data, mode, excludedFields);
|
||||
const sessions = new Set<string>();
|
||||
rows.data.forEach((row) => {
|
||||
sessions.add(row.session_id);
|
||||
});
|
||||
return { rows: rows.data, columns };
|
||||
}
|
||||
|
||||
export async function deleteMessagesFn(ids: number[]) {
|
||||
try {
|
||||
return await api.delete(`${BASE_URL_API}monitor/messages`, {
|
||||
data: ids,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error deleting flows:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateMessageApi(data: Message) {
|
||||
return await api.post(`${BASE_URL_API}monitor/messages/${data.index}`, data);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@ const SvgBotMessageSquare = (props) => (
|
|||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="lucide lucide-bot-message-square"
|
||||
{...props}
|
||||
>
|
||||
|
|
|
|||
25
src/frontend/src/icons/Streamlit/SvgStreamlit.jsx
Normal file
25
src/frontend/src/icons/Streamlit/SvgStreamlit.jsx
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
export default function SvgStreamlit(props) {
|
||||
return (
|
||||
<svg
|
||||
width="301"
|
||||
height="165"
|
||||
viewBox="0 0 301 165"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M150.731 101.547L98.1387 73.7471L6.84674 25.4969C6.7634 25.4136 6.59674 25.4136 6.51341 25.4136C3.18007 23.8303 -0.236608 27.1636 1.0134 30.497L47.5302 149.139L47.5385 149.164C47.5885 149.281 47.6302 149.397 47.6802 149.514C49.5885 153.939 53.7552 156.672 58.2886 157.747C58.6719 157.831 58.9461 157.906 59.4064 157.998C59.8645 158.1 60.5052 158.239 61.0552 158.281C61.1469 158.289 61.2302 158.289 61.3219 158.297H61.3886C61.4552 158.306 61.5219 158.306 61.5886 158.314H61.6802C61.7386 158.322 61.8052 158.322 61.8636 158.322H61.9719C62.0386 158.331 62.1052 158.331 62.1719 158.331V158.331C121.084 164.754 180.519 164.754 239.431 158.331V158.331C240.139 158.331 240.831 158.297 241.497 158.231C241.714 158.206 241.922 158.181 242.131 158.156C242.156 158.147 242.189 158.147 242.214 158.139C242.356 158.122 242.497 158.097 242.639 158.072C242.847 158.047 243.056 158.006 243.264 157.964C243.681 157.872 243.87 157.806 244.436 157.611C245.001 157.417 245.94 157.077 246.527 156.794C247.115 156.511 247.522 156.239 248.014 155.931C248.622 155.547 249.201 155.155 249.788 154.715C250.041 154.521 250.214 154.397 250.397 154.222L250.297 154.164L150.731 101.547Z"
|
||||
fill="#FF4B4B"
|
||||
/>
|
||||
<path
|
||||
d="M294.766 25.4981H294.683L203.357 73.7483L254.124 149.357L300.524 30.4981V30.3315C301.691 26.8314 298.108 23.6648 294.766 25.4981"
|
||||
fill="#7D353B"
|
||||
/>
|
||||
<path
|
||||
d="M155.598 2.55572C153.264 -0.852624 148.181 -0.852624 145.931 2.55572L98.1389 73.7477L150.731 101.548L250.398 154.222C251.024 153.609 251.526 153.012 252.056 152.381C252.806 151.456 253.506 150.465 254.123 149.356L203.356 73.7477L155.598 2.55572Z"
|
||||
fill="#BD4043"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
8
src/frontend/src/icons/Streamlit/index.tsx
Normal file
8
src/frontend/src/icons/Streamlit/index.tsx
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import React, { forwardRef } from "react";
|
||||
import SvgStreamlit from "./SvgStreamlit";
|
||||
|
||||
export const Streamlit = forwardRef<SVGSVGElement, React.PropsWithChildren<{}>>(
|
||||
(props, ref) => {
|
||||
return <SvgStreamlit className="icon" ref={ref} {...props} />;
|
||||
},
|
||||
);
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "../../../../../../components/ui/select";
|
||||
|
||||
export default function CsvSelect({ node, handleChangeSelect }): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<div className="flex justify-between">
|
||||
Expand the ouptut to see the CSV
|
||||
</div>
|
||||
<div className="flex items-center justify-between pt-5">
|
||||
<span>CSV separator </span>
|
||||
<Select
|
||||
value={node.data.node.template.separator.value}
|
||||
onValueChange={(e) => handleChangeSelect(e)}
|
||||
>
|
||||
<SelectTrigger className="w-[70px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{node?.data?.node?.template?.separator?.options.map(
|
||||
(separator) => (
|
||||
<SelectItem key={separator} value={separator}>
|
||||
{separator}
|
||||
</SelectItem>
|
||||
),
|
||||
)}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -5,17 +5,10 @@ import CsvOutputComponent from "../../../../components/csvOutputComponent";
|
|||
import InputListComponent from "../../../../components/inputListComponent";
|
||||
import PdfViewer from "../../../../components/pdfViewer";
|
||||
import RecordsOutputComponent from "../../../../components/recordsOutputComponent";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "../../../../components/ui/select";
|
||||
import { Textarea } from "../../../../components/ui/textarea";
|
||||
import { PDFViewConstant } from "../../../../constants/constants";
|
||||
import { InputOutput } from "../../../../constants/enums";
|
||||
import TextOutputView from "../../../../shared/components/textOutputView";
|
||||
import useFlowStore from "../../../../stores/flowStore";
|
||||
import { IOFieldViewProps } from "../../../../types/components";
|
||||
import {
|
||||
|
|
@ -24,6 +17,7 @@ import {
|
|||
} from "../../../../utils/reactflowUtils";
|
||||
import IOFileInput from "./components/FileInput";
|
||||
import IoJsonInput from "./components/JSONInput";
|
||||
import CsvSelect from "./components/csvSelect";
|
||||
import IOKeyPairInput from "./components/keyPairInput";
|
||||
|
||||
export default function IOFieldView({
|
||||
|
|
@ -51,6 +45,12 @@ export default function IOFieldView({
|
|||
|
||||
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
|
||||
|
||||
const textOutputValue =
|
||||
(flowPool[node!.id] ?? [])[(flowPool[node!.id]?.length ?? 1) - 1]?.data
|
||||
.results.result ?? "";
|
||||
|
||||
console.log(flowPoolNode?.data?.artifacts?.records);
|
||||
|
||||
function handleOutputType() {
|
||||
if (!node) return <>"No node found!"</>;
|
||||
switch (type) {
|
||||
|
|
@ -163,21 +163,7 @@ export default function IOFieldView({
|
|||
case InputOutput.OUTPUT:
|
||||
switch (fieldType) {
|
||||
case "TextOutput":
|
||||
return (
|
||||
<Textarea
|
||||
className={`w-full custom-scroll ${
|
||||
left ? " min-h-32" : " h-full"
|
||||
}`}
|
||||
placeholder={"Empty"}
|
||||
// update to real value on flowPool
|
||||
value={
|
||||
(flowPool[node.id] ?? [])[
|
||||
(flowPool[node.id]?.length ?? 1) - 1
|
||||
]?.params ?? ""
|
||||
}
|
||||
readOnly
|
||||
/>
|
||||
);
|
||||
return <TextOutputView left={left} value={textOutputValue} />;
|
||||
case "PDFOutput":
|
||||
return left ? (
|
||||
<div>{PDFViewConstant}</div>
|
||||
|
|
@ -187,31 +173,10 @@ export default function IOFieldView({
|
|||
case "CSVOutput":
|
||||
return left ? (
|
||||
<>
|
||||
<div className="flex justify-between">
|
||||
Expand the ouptut to see the CSV
|
||||
</div>
|
||||
<div className="flex items-center justify-between pt-5">
|
||||
<span>CSV separator </span>
|
||||
<Select
|
||||
value={node.data.node.template.separator.value}
|
||||
onValueChange={(e) => handleChangeSelect(e)}
|
||||
>
|
||||
<SelectTrigger className="w-[70px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{node?.data?.node?.template?.separator?.options.map(
|
||||
(separator) => (
|
||||
<SelectItem key={separator} value={separator}>
|
||||
{separator}
|
||||
</SelectItem>
|
||||
),
|
||||
)}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<CsvSelect
|
||||
node={node}
|
||||
handleChangeSelect={handleChangeSelect}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
|
@ -286,8 +251,9 @@ export default function IOFieldView({
|
|||
return (
|
||||
<div className={left ? "h-56" : "h-full"}>
|
||||
<RecordsOutputComponent
|
||||
flowPool={flowPoolNode}
|
||||
pagination={!left}
|
||||
rows={flowPoolNode?.data?.artifacts?.records ?? []}
|
||||
columnMode="union"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -303,7 +269,7 @@ export default function IOFieldView({
|
|||
value={
|
||||
(flowPool[node.id] ?? [])[
|
||||
(flowPool[node.id]?.length ?? 1) - 1
|
||||
]?.params ?? ""
|
||||
]?.data.results.result ?? ""
|
||||
}
|
||||
readOnly
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
import { deleteMessagesFn } from "../../../../../controllers/API";
|
||||
import { useMessagesStore } from "../../../../../stores/messagesStore";
|
||||
|
||||
const useRemoveSession = (setSuccessData, setErrorData) => {
|
||||
const deleteSession = useMessagesStore((state) => state.deleteSession);
|
||||
const messages = useMessagesStore((state) => state.messages);
|
||||
|
||||
const handleRemoveSession = async (session_id: string) => {
|
||||
try {
|
||||
await deleteMessagesFn(
|
||||
messages
|
||||
.filter((msg) => msg.session_id === session_id)
|
||||
.map((msg) => msg.index),
|
||||
);
|
||||
deleteSession(session_id);
|
||||
setSuccessData({
|
||||
title: "Session deleted successfully.",
|
||||
});
|
||||
} catch (error) {
|
||||
setErrorData({
|
||||
title: "Error deleting Session.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return { handleRemoveSession };
|
||||
};
|
||||
|
||||
export default useRemoveSession;
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
import {
|
||||
CellEditRequestEvent,
|
||||
ColDef,
|
||||
ColGroupDef,
|
||||
SelectionChangedEvent,
|
||||
} from "ag-grid-community";
|
||||
import { useState } from "react";
|
||||
import TableComponent from "../../../../components/tableComponent";
|
||||
import { Card, CardContent } from "../../../../components/ui/card";
|
||||
import useAlertStore from "../../../../stores/alertStore";
|
||||
import { useMessagesStore } from "../../../../stores/messagesStore";
|
||||
import useUpdateMessage from "../../../../pages/SettingsPage/pages/messagesPage/hooks/use-updateMessage";
|
||||
import useRemoveMessages from "../../../../pages/SettingsPage/pages/messagesPage/hooks/use-remove-messages";
|
||||
import HeaderMessagesComponent from "../../../../pages/SettingsPage/pages/messagesPage/components/headerMessages";
|
||||
import { Button } from "../../../../components/ui/button";
|
||||
import ForwardedIconComponent from "../../../../components/genericIconComponent";
|
||||
import { cn } from "../../../../utils/utils";
|
||||
|
||||
export default function SessionView({ rows }: { rows: Array<any> }) {
|
||||
const columns = useMessagesStore((state) => state.columns);
|
||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||
const setSuccessData = useAlertStore((state) => state.setSuccessData);
|
||||
|
||||
const [selectedRows, setSelectedRows] = useState<number[]>([]);
|
||||
|
||||
const { handleRemoveMessages } = useRemoveMessages(
|
||||
setSelectedRows,
|
||||
setSuccessData,
|
||||
setErrorData,
|
||||
selectedRows,
|
||||
);
|
||||
|
||||
const { handleUpdate } = useUpdateMessage(setSuccessData, setErrorData);
|
||||
|
||||
function handleUpdateMessage(event: CellEditRequestEvent<any, string>) {
|
||||
const newValue = event.newValue;
|
||||
const field = event.column.getColId();
|
||||
const row = event.data;
|
||||
const data = {
|
||||
...row,
|
||||
[field]: newValue,
|
||||
};
|
||||
handleUpdate(data);
|
||||
}
|
||||
|
||||
return (
|
||||
<TableComponent
|
||||
key={"sessionView"}
|
||||
onDelete={handleRemoveMessages}
|
||||
readOnlyEdit
|
||||
onCellEditRequest={(event) => {
|
||||
handleUpdateMessage(event);
|
||||
}}
|
||||
editable={["Sender Name", "Message"]}
|
||||
overlayNoRowsTemplate="No data available"
|
||||
onSelectionChanged={(event: SelectionChangedEvent) => {
|
||||
setSelectedRows(event.api.getSelectedRows().map((row) => row.index));
|
||||
}}
|
||||
rowSelection="multiple"
|
||||
suppressRowClickSelection={true}
|
||||
pagination={true}
|
||||
columnDefs={columns}
|
||||
rowData={rows}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
import IconComponent from "../../../../../../../components/genericIconComponent";
|
||||
import { Case } from "../../../../../../../shared/components/caseComponent";
|
||||
import { classNames } from "../../../../../../../utils/utils";
|
||||
|
||||
const ButtonSendWrapper = ({
|
||||
send,
|
||||
lockChat,
|
||||
noInput,
|
||||
saveLoading,
|
||||
chatValue,
|
||||
}) => {
|
||||
return (
|
||||
<button
|
||||
className={classNames(
|
||||
"form-modal-send-button",
|
||||
noInput
|
||||
? "bg-high-indigo text-background"
|
||||
: chatValue === ""
|
||||
? "text-primary"
|
||||
: "bg-chat-send text-background",
|
||||
)}
|
||||
disabled={lockChat || saveLoading}
|
||||
onClick={(): void => send()}
|
||||
>
|
||||
<Case condition={lockChat || saveLoading}>
|
||||
<IconComponent
|
||||
name="Lock"
|
||||
className="form-modal-lock-icon"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Case>
|
||||
|
||||
<Case condition={noInput}>
|
||||
<IconComponent
|
||||
name="Zap"
|
||||
className="form-modal-play-icon"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Case>
|
||||
|
||||
<Case condition={!(lockChat || saveLoading) && !noInput}>
|
||||
<IconComponent
|
||||
name="LucideSend"
|
||||
className="form-modal-send-icon "
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Case>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default ButtonSendWrapper;
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
import { Textarea } from "../../../../../../../components/ui/textarea";
|
||||
import { classNames } from "../../../../../../../utils/utils";
|
||||
|
||||
const TextAreaWrapper = ({
|
||||
checkSendingOk,
|
||||
send,
|
||||
lockChat,
|
||||
noInput,
|
||||
saveLoading,
|
||||
chatValue,
|
||||
setChatValue,
|
||||
CHAT_INPUT_PLACEHOLDER,
|
||||
CHAT_INPUT_PLACEHOLDER_SEND,
|
||||
inputRef,
|
||||
setInputFocus,
|
||||
files,
|
||||
isDragging,
|
||||
}) => {
|
||||
const getPlaceholderText = (
|
||||
isDragging: boolean,
|
||||
noInput: boolean,
|
||||
): string => {
|
||||
if (isDragging) {
|
||||
return "Drop here";
|
||||
} else if (noInput) {
|
||||
return CHAT_INPUT_PLACEHOLDER;
|
||||
} else {
|
||||
return CHAT_INPUT_PLACEHOLDER_SEND;
|
||||
}
|
||||
};
|
||||
|
||||
const lockClass =
|
||||
lockChat || saveLoading
|
||||
? "form-modal-lock-true bg-input"
|
||||
: noInput
|
||||
? "form-modal-no-input bg-input"
|
||||
: "form-modal-lock-false bg-background";
|
||||
|
||||
const fileClass =
|
||||
files.length > 0
|
||||
? "rounded-b-lg ring-0 focus:ring-0 focus:border-2 rounded-t-none border-t-0 border-border focus:border-t-0 focus:border-ring"
|
||||
: "rounded-md border-t border-border focus:ring-0 focus:border-2 focus:border-ring";
|
||||
|
||||
const additionalClassNames = "form-modal-lockchat pl-14";
|
||||
|
||||
return (
|
||||
<Textarea
|
||||
onFocus={(e) => {
|
||||
setInputFocus(true);
|
||||
e.target.style.borderTopWidth = "0";
|
||||
}}
|
||||
onBlur={() => setInputFocus(false)}
|
||||
onKeyDown={(event) => {
|
||||
if (checkSendingOk(event)) {
|
||||
send();
|
||||
}
|
||||
}}
|
||||
rows={1}
|
||||
ref={inputRef}
|
||||
disabled={lockChat || noInput || saveLoading}
|
||||
style={{
|
||||
resize: "none",
|
||||
bottom: `${inputRef?.current?.scrollHeight}px`,
|
||||
maxHeight: "150px",
|
||||
overflow: `${
|
||||
inputRef.current && inputRef.current.scrollHeight > 150
|
||||
? "auto"
|
||||
: "hidden"
|
||||
}`,
|
||||
}}
|
||||
value={lockChat ? "Thinking..." : saveLoading ? "Saving..." : chatValue}
|
||||
onChange={(event): void => {
|
||||
setChatValue(event.target.value);
|
||||
}}
|
||||
className={classNames(lockClass, fileClass, additionalClassNames)}
|
||||
placeholder={getPlaceholderText(isDragging, noInput)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default TextAreaWrapper;
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import ForwardedIconComponent from "../../../../../../../components/genericIconComponent";
|
||||
import { Button } from "../../../../../../../components/ui/button";
|
||||
|
||||
const UploadFileButton = ({
|
||||
fileInputRef,
|
||||
handleFileChange,
|
||||
handleButtonClick,
|
||||
}) => {
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
style={{ display: "none" }}
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
<Button
|
||||
className="font-bold text-white transition-all hover:text-muted-foreground"
|
||||
onClick={handleButtonClick}
|
||||
variant="none"
|
||||
size="none"
|
||||
>
|
||||
<ForwardedIconComponent name="PaperclipIcon" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UploadFileButton;
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
export const getClassNamesFilePreview = (inputFocus) => {
|
||||
return `flex w-full items-center gap-4 rounded-t-lg bg-background px-14 py-5 overflow-auto custom-scroll ${
|
||||
inputFocus
|
||||
? "border border-b-0 border-ring border-2"
|
||||
: "border border-b-0 border-border"
|
||||
}`;
|
||||
};
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import { useEffect } from "react";
|
||||
|
||||
const useAutoResizeTextArea = (value, inputRef) => {
|
||||
useEffect(() => {
|
||||
if (inputRef.current && inputRef.current.scrollHeight! !== 0) {
|
||||
inputRef.current.style!.height = "inherit"; // Reset the height
|
||||
inputRef.current.style!.height = `${inputRef.current.scrollHeight!}px`; // Set it to the scrollHeight
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
return inputRef;
|
||||
};
|
||||
|
||||
export default useAutoResizeTextArea;
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
import ShortUniqueId from "short-unique-id";
|
||||
import useFileUpload from "./use-file-upload";
|
||||
|
||||
const useDragAndDrop = (setIsDragging, setFiles, currentFlowId) => {
|
||||
const dragOver = (e) => {
|
||||
e.preventDefault();
|
||||
if (e.dataTransfer.types.some((type) => type === "Files")) {
|
||||
setIsDragging(true);
|
||||
}
|
||||
};
|
||||
|
||||
const dragEnter = (e) => {
|
||||
if (e.dataTransfer.types.some((type) => type === "Files")) {
|
||||
setIsDragging(true);
|
||||
}
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
const dragLeave = (e) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(false);
|
||||
};
|
||||
|
||||
const onDrop = (e) => {
|
||||
e.preventDefault();
|
||||
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
|
||||
handleFiles(e.dataTransfer.files, setFiles, currentFlowId);
|
||||
e.dataTransfer.clearData();
|
||||
}
|
||||
setIsDragging(false);
|
||||
};
|
||||
return {
|
||||
dragOver,
|
||||
dragEnter,
|
||||
dragLeave,
|
||||
onDrop,
|
||||
};
|
||||
};
|
||||
|
||||
const handleFiles = (files, setFiles, currentFlowId) => {
|
||||
if (files) {
|
||||
const uid = new ShortUniqueId({ length: 3 });
|
||||
const id = uid();
|
||||
const type = files[0].type.split("/")[0];
|
||||
const blob = files[0];
|
||||
|
||||
setFiles((prevFiles) => [
|
||||
...prevFiles,
|
||||
{ file: blob, loading: true, error: false, id, type },
|
||||
]);
|
||||
|
||||
useFileUpload(blob, currentFlowId, setFiles, id);
|
||||
}
|
||||
};
|
||||
export default useDragAndDrop;
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import { uploadFile } from "../../../../../../controllers/API";
|
||||
|
||||
const useFileUpload = (blob, currentFlowId, setFiles, id) => {
|
||||
uploadFile(blob, currentFlowId)
|
||||
.then((res) => {
|
||||
setFiles((prev) => {
|
||||
const newFiles = [...prev];
|
||||
const updatedIndex = newFiles.findIndex((file) => file.id === id);
|
||||
newFiles[updatedIndex].loading = false;
|
||||
newFiles[updatedIndex].path = res.data.file_path;
|
||||
return newFiles;
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
setFiles((prev) => {
|
||||
const newFiles = [...prev];
|
||||
const updatedIndex = newFiles.findIndex((file) => file.id === id);
|
||||
newFiles[updatedIndex].loading = false;
|
||||
newFiles[updatedIndex].error = true;
|
||||
return newFiles;
|
||||
});
|
||||
});
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default useFileUpload;
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import { useEffect } from "react";
|
||||
|
||||
const useFocusOnUnlock = (lockChat, inputRef) => {
|
||||
useEffect(() => {
|
||||
if (!lockChat && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [lockChat, inputRef]);
|
||||
|
||||
return inputRef;
|
||||
};
|
||||
|
||||
export default useFocusOnUnlock;
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import ShortUniqueId from "short-unique-id";
|
||||
import useFileUpload from "./use-file-upload";
|
||||
|
||||
export const useHandleFileChange = (setFiles, currentFlowId) => {
|
||||
const handleFileChange = async (
|
||||
event: React.ChangeEvent<HTMLInputElement>,
|
||||
) => {
|
||||
const fileInput = event.target;
|
||||
const file = fileInput.files?.[0];
|
||||
if (file) {
|
||||
const uid = new ShortUniqueId({ length: 10 }); // Increase the length to ensure uniqueness
|
||||
const id = uid();
|
||||
const type = file.type.split("/")[0];
|
||||
const blob = file;
|
||||
|
||||
setFiles((prevFiles) => [
|
||||
...prevFiles,
|
||||
{ file: blob, loading: true, error: false, id, type },
|
||||
]);
|
||||
|
||||
useFileUpload(blob, currentFlowId, setFiles, id);
|
||||
}
|
||||
|
||||
// Clear the file input value to ensure the change event is triggered even for the same file
|
||||
fileInput.value = "";
|
||||
};
|
||||
|
||||
return {
|
||||
handleFileChange,
|
||||
};
|
||||
};
|
||||
|
||||
export default useHandleFileChange;
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
import { useEffect } from "react";
|
||||
import ShortUniqueId from "short-unique-id";
|
||||
import useFileUpload from "./use-file-upload";
|
||||
|
||||
const useUpload = (uploadFile, currentFlowId, setFiles) => {
|
||||
useEffect(() => {
|
||||
const handlePaste = (event: ClipboardEvent): void => {
|
||||
const items = event.clipboardData?.items;
|
||||
if (items) {
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const type = items[0].type.split("/")[0];
|
||||
const uid = new ShortUniqueId({ length: 3 });
|
||||
const blob = items[i].getAsFile();
|
||||
if (blob) {
|
||||
const id = uid();
|
||||
setFiles((prevFiles) => [
|
||||
...prevFiles,
|
||||
{ file: blob, loading: true, error: false, id, type },
|
||||
]);
|
||||
useFileUpload(blob, currentFlowId, setFiles, id);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("paste", handlePaste);
|
||||
return () => {
|
||||
document.removeEventListener("paste", handlePaste);
|
||||
};
|
||||
}, [uploadFile, currentFlowId]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default useUpload;
|
||||
|
|
@ -1,14 +1,23 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import IconComponent from "../../../../../components/genericIconComponent";
|
||||
import { Textarea } from "../../../../../components/ui/textarea";
|
||||
import { useRef, useState } from "react";
|
||||
import {
|
||||
CHAT_INPUT_PLACEHOLDER,
|
||||
CHAT_INPUT_PLACEHOLDER_SEND,
|
||||
} from "../../../../../constants/constants";
|
||||
import { uploadFile } from "../../../../../controllers/API";
|
||||
import useFlowsManagerStore from "../../../../../stores/flowsManagerStore";
|
||||
import { chatInputType } from "../../../../../types/components";
|
||||
import { classNames } from "../../../../../utils/utils";
|
||||
|
||||
import {
|
||||
ChatInputType,
|
||||
FilePreviewType,
|
||||
} from "../../../../../types/components";
|
||||
import FilePreview from "../filePreviewChat";
|
||||
import ButtonSendWrapper from "./components/buttonSendWrapper";
|
||||
import TextAreaWrapper from "./components/textAreaWrapper";
|
||||
import UploadFileButton from "./components/uploadFileButton";
|
||||
import { getClassNamesFilePreview } from "./helpers/get-class-file-preview";
|
||||
import useAutoResizeTextArea from "./hooks/use-auto-resize-text-area";
|
||||
import useFocusOnUnlock from "./hooks/use-focus-unlock";
|
||||
import useHandleFileChange from "./hooks/use-handle-file-change";
|
||||
import useUpload from "./hooks/use-upload";
|
||||
export default function ChatInput({
|
||||
lockChat,
|
||||
chatValue,
|
||||
|
|
@ -16,125 +25,99 @@ export default function ChatInput({
|
|||
setChatValue,
|
||||
inputRef,
|
||||
noInput,
|
||||
}: chatInputType): JSX.Element {
|
||||
files,
|
||||
setFiles,
|
||||
isDragging,
|
||||
}: ChatInputType): JSX.Element {
|
||||
const [repeat, setRepeat] = useState(1);
|
||||
const saveLoading = useFlowsManagerStore((state) => state.saveLoading);
|
||||
useEffect(() => {
|
||||
if (!lockChat && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [lockChat, inputRef]);
|
||||
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
|
||||
const [inputFocus, setInputFocus] = useState<boolean>(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (inputRef.current && inputRef.current.scrollHeight !== 0) {
|
||||
inputRef.current.style.height = "inherit"; // Reset the height
|
||||
inputRef.current.style.height = `${inputRef.current.scrollHeight}px`; // Set it to the scrollHeight
|
||||
}
|
||||
}, [chatValue]);
|
||||
useFocusOnUnlock(lockChat, inputRef);
|
||||
useAutoResizeTextArea(chatValue, inputRef);
|
||||
useUpload(uploadFile, currentFlowId, setFiles);
|
||||
const { handleFileChange } = useHandleFileChange(setFiles, currentFlowId);
|
||||
|
||||
const send = () => {
|
||||
sendMessage({
|
||||
repeat,
|
||||
files: files.map((file) => file.path ?? "").filter((file) => file !== ""),
|
||||
});
|
||||
setFiles([]);
|
||||
};
|
||||
|
||||
const checkSendingOk = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
return (
|
||||
event.key === "Enter" &&
|
||||
!lockChat &&
|
||||
!saveLoading &&
|
||||
!event.shiftKey &&
|
||||
!event.nativeEvent.isComposing
|
||||
);
|
||||
};
|
||||
|
||||
const classNameFilePreview = getClassNamesFilePreview(inputFocus);
|
||||
|
||||
const handleButtonClick = () => {
|
||||
fileInputRef.current!.click();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex w-full gap-2">
|
||||
<div className="flex w-full flex-col-reverse">
|
||||
<div className="relative w-full">
|
||||
<Textarea
|
||||
onKeyDown={(event) => {
|
||||
if (
|
||||
event.key === "Enter" &&
|
||||
!lockChat &&
|
||||
!saveLoading &&
|
||||
!event.shiftKey &&
|
||||
!event.nativeEvent.isComposing
|
||||
) {
|
||||
sendMessage(repeat);
|
||||
}
|
||||
}}
|
||||
rows={1}
|
||||
ref={inputRef}
|
||||
disabled={lockChat || noInput || saveLoading}
|
||||
style={{
|
||||
resize: "none",
|
||||
bottom: `${inputRef?.current?.scrollHeight}px`,
|
||||
maxHeight: "150px",
|
||||
overflow: `${
|
||||
inputRef.current && inputRef.current.scrollHeight > 150
|
||||
? "auto"
|
||||
: "hidden"
|
||||
}`,
|
||||
}}
|
||||
value={
|
||||
lockChat ? "Thinking..." : saveLoading ? "Saving..." : chatValue
|
||||
}
|
||||
onChange={(event): void => {
|
||||
setChatValue(event.target.value);
|
||||
}}
|
||||
className={classNames(
|
||||
lockChat || saveLoading
|
||||
? " form-modal-lock-true bg-input"
|
||||
: noInput
|
||||
? "form-modal-no-input bg-input"
|
||||
: " form-modal-lock-false bg-background",
|
||||
|
||||
"form-modal-lockchat"
|
||||
)}
|
||||
placeholder={
|
||||
noInput ? CHAT_INPUT_PLACEHOLDER : CHAT_INPUT_PLACEHOLDER_SEND
|
||||
}
|
||||
<TextAreaWrapper
|
||||
checkSendingOk={checkSendingOk}
|
||||
send={send}
|
||||
lockChat={lockChat}
|
||||
noInput={noInput}
|
||||
saveLoading={saveLoading}
|
||||
chatValue={chatValue}
|
||||
setChatValue={setChatValue}
|
||||
CHAT_INPUT_PLACEHOLDER={CHAT_INPUT_PLACEHOLDER}
|
||||
CHAT_INPUT_PLACEHOLDER_SEND={CHAT_INPUT_PLACEHOLDER_SEND}
|
||||
inputRef={inputRef}
|
||||
setInputFocus={setInputFocus}
|
||||
files={files}
|
||||
isDragging={isDragging}
|
||||
/>
|
||||
<div className="form-modal-send-icon-position">
|
||||
<button
|
||||
className={classNames(
|
||||
"form-modal-send-button",
|
||||
noInput
|
||||
? "bg-high-indigo text-background"
|
||||
: chatValue === ""
|
||||
? "text-primary"
|
||||
: "bg-chat-send text-background"
|
||||
)}
|
||||
disabled={lockChat || saveLoading}
|
||||
onClick={(): void => sendMessage(repeat)}
|
||||
>
|
||||
{lockChat || saveLoading ? (
|
||||
<IconComponent
|
||||
name="Lock"
|
||||
className="form-modal-lock-icon"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : noInput ? (
|
||||
<IconComponent
|
||||
name="Zap"
|
||||
className="form-modal-play-icon"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : (
|
||||
<IconComponent
|
||||
name="LucideSend"
|
||||
className="form-modal-send-icon "
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
<ButtonSendWrapper
|
||||
send={send}
|
||||
lockChat={lockChat}
|
||||
noInput={noInput}
|
||||
saveLoading={saveLoading}
|
||||
chatValue={chatValue}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-2 left-4">
|
||||
<UploadFileButton
|
||||
fileInputRef={fileInputRef}
|
||||
handleFileChange={handleFileChange}
|
||||
handleButtonClick={handleButtonClick}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/*
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="primary" className="h-13 px-4">
|
||||
<IconComponent name="Repeat" className="" aria-hidden="true" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-fit">
|
||||
<div className="flex flex-col items-center justify-center gap-2">
|
||||
<span className="text-sm">Repetitions: </span>
|
||||
<Input
|
||||
onChange={(e) => {
|
||||
handleChange(parseInt(e.target.value));
|
||||
{files.length > 0 && (
|
||||
<div className={classNameFilePreview}>
|
||||
{files.map((file) => (
|
||||
<FilePreview
|
||||
error={file.error}
|
||||
file={file.file}
|
||||
loading={file.loading}
|
||||
key={file.id}
|
||||
onDelete={() => {
|
||||
setFiles((prev: FilePreviewType[]) =>
|
||||
prev.filter((f) => f.id !== file.id),
|
||||
);
|
||||
// TODO: delete file on backend
|
||||
}}
|
||||
className="w-16"
|
||||
type="number"
|
||||
min={0}
|
||||
/>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover> */}
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
import { useState } from "react";
|
||||
import ForwardedIconComponent from "../../../../../../../components/genericIconComponent";
|
||||
import formatFileName from "../../../filePreviewChat/utils/format-file-name";
|
||||
import FileCard from "../../../fileComponent";
|
||||
|
||||
export default function FileCardWrapper({
|
||||
index,
|
||||
name,
|
||||
type,
|
||||
path,
|
||||
}: {
|
||||
index: number;
|
||||
name: string;
|
||||
type: string;
|
||||
path: string;
|
||||
}) {
|
||||
const [show, setShow] = useState<boolean>(true);
|
||||
return (
|
||||
<div key={index} className="flex flex-col gap-2">
|
||||
<span
|
||||
onClick={() => setShow(!show)}
|
||||
className="flex cursor-pointer gap-2 text-sm text-muted-foreground"
|
||||
>
|
||||
{formatFileName(name, 50)}
|
||||
<ForwardedIconComponent name={show ? "ChevronDown" : "ChevronRight"} />
|
||||
</span>
|
||||
<FileCard
|
||||
showFile={show}
|
||||
fileName={name}
|
||||
fileType={type}
|
||||
content={path}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -7,13 +7,17 @@ import remarkMath from "remark-math";
|
|||
import MaleTechnology from "../../../../../assets/male-technologist.png";
|
||||
import Robot from "../../../../../assets/robot.png";
|
||||
import CodeTabsComponent from "../../../../../components/codeTabsComponent";
|
||||
import IconComponent from "../../../../../components/genericIconComponent";
|
||||
import IconComponent, {
|
||||
ForwardedIconComponent,
|
||||
} from "../../../../../components/genericIconComponent";
|
||||
import SanitizedHTMLWrapper from "../../../../../components/sanitizedHTMLWrapper";
|
||||
import useAlertStore from "../../../../../stores/alertStore";
|
||||
import useFlowStore from "../../../../../stores/flowStore";
|
||||
import { chatMessagePropsType } from "../../../../../types/components";
|
||||
import { classNames, cn } from "../../../../../utils/utils";
|
||||
import FileCard from "../fileComponent";
|
||||
import formatFileName from "../filePreviewChat/utils/format-file-name";
|
||||
import FileCardWrapper from "./components/fileCardWrapper";
|
||||
|
||||
export default function ChatMessage({
|
||||
chat,
|
||||
|
|
@ -22,6 +26,7 @@ export default function ChatMessage({
|
|||
updateChat,
|
||||
setLockChat,
|
||||
}: chatMessagePropsType): JSX.Element {
|
||||
const [showFile, setShowFile] = useState<boolean>(true);
|
||||
const convert = new Convert({ newline: true });
|
||||
const [hidden, setHidden] = useState(true);
|
||||
const template = chat.template;
|
||||
|
|
@ -251,21 +256,6 @@ dark:prose-invert"
|
|||
[chat.message, chatMessage],
|
||||
)}
|
||||
</div>
|
||||
{chat.files && (
|
||||
<div className="my-2 w-full">
|
||||
{chat.files.map((file, index) => {
|
||||
return (
|
||||
<div key={index} className="my-2 w-full">
|
||||
<FileCard
|
||||
fileName={"Generated File"}
|
||||
fileType={file.data_type}
|
||||
content={file.data}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -323,14 +313,30 @@ dark:prose-invert"
|
|||
</span>
|
||||
</>
|
||||
) : (
|
||||
<span
|
||||
className="prose text-primary word-break-break-word dark:prose-invert"
|
||||
data-testid={
|
||||
"chat-message-" + chat.sender_name + "-" + chatMessage
|
||||
}
|
||||
>
|
||||
{chatMessage}
|
||||
</span>
|
||||
<div className="flex flex-col">
|
||||
<span
|
||||
className="prose text-primary word-break-break-word dark:prose-invert"
|
||||
data-testid={
|
||||
"chat-message-" + chat.sender_name + "-" + chatMessage
|
||||
}
|
||||
>
|
||||
{chatMessage}
|
||||
</span>
|
||||
{chat.files && (
|
||||
<div className="my-2 flex flex-col gap-5">
|
||||
{chat.files.map((file, index) => {
|
||||
return (
|
||||
<FileCardWrapper
|
||||
index={index}
|
||||
name={file.name}
|
||||
type={file.type}
|
||||
path={file.path}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
import ForwardedIconComponent from "../../../../../../../components/genericIconComponent";
|
||||
|
||||
export default function DownloadButton({
|
||||
isHovered,
|
||||
handleDownload,
|
||||
}: {
|
||||
isHovered: boolean;
|
||||
handleDownload: () => void;
|
||||
}): JSX.Element | undefined {
|
||||
if (isHovered) {
|
||||
return (
|
||||
<div
|
||||
className={`absolute right-1 top-1 rounded-bl-lg bg-muted text-sm font-bold text-foreground `}
|
||||
>
|
||||
<button className="px-2 py-1 text-ring " onClick={handleDownload}>
|
||||
<ForwardedIconComponent
|
||||
name="DownloadCloud"
|
||||
className="h-5 w-5 text-current hover:scale-110"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
|
@ -1,26 +1,23 @@
|
|||
import * as base64js from "base64-js";
|
||||
import { useState } from "react";
|
||||
import IconComponent from "../../../../../components/genericIconComponent";
|
||||
import { ForwardedIconComponent } from "../../../../../components/genericIconComponent";
|
||||
import { BACKEND_URL, BASE_URL_API } from "../../../../../constants/constants";
|
||||
import useFlowsManagerStore from "../../../../../stores/flowsManagerStore";
|
||||
import { fileCardPropsType } from "../../../../../types/components";
|
||||
import formatFileName from "../filePreviewChat/utils/format-file-name";
|
||||
import DownloadButton from "./components/downloadButton/downloadButton";
|
||||
import getClasses from "./utils/get-classes";
|
||||
import handleDownload from "./utils/handle-download";
|
||||
|
||||
const imgTypes = new Set(["png", "jpg"]);
|
||||
|
||||
export default function FileCard({
|
||||
fileName,
|
||||
content,
|
||||
fileType,
|
||||
}: fileCardPropsType): JSX.Element {
|
||||
const handleDownload = (): void => {
|
||||
const byteArray = new Uint8Array(base64js.toByteArray(content));
|
||||
const blob = new Blob([byteArray], { type: "application/octet-stream" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = fileName + ".png";
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
showFile = true,
|
||||
}: fileCardPropsType): JSX.Element | undefined {
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
|
||||
function handleMouseEnter(): void {
|
||||
setIsHovered(true);
|
||||
}
|
||||
|
|
@ -28,58 +25,57 @@ export default function FileCard({
|
|||
setIsHovered(false);
|
||||
}
|
||||
|
||||
if (fileType === "image") {
|
||||
const fileWrapperClasses = getClasses(isHovered);
|
||||
|
||||
const imgSrc = `${BACKEND_URL.slice(
|
||||
0,
|
||||
BACKEND_URL.length - 1,
|
||||
)}${BASE_URL_API}files/images/${content}`;
|
||||
|
||||
if (showFile) {
|
||||
if (imgTypes.has(fileType)) {
|
||||
return (
|
||||
<div
|
||||
className="inline-block w-full rounded-lg transition-all"
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
style={{ display: "inline-block" }}
|
||||
>
|
||||
<div className="relative w-[50%] rounded-lg border border-border">
|
||||
<img
|
||||
src={imgSrc}
|
||||
alt="generated image"
|
||||
className="m-0 h-auto w-auto rounded-lg border border-border p-0 transition-all"
|
||||
/>
|
||||
<DownloadButton
|
||||
isHovered={isHovered}
|
||||
handleDownload={() => handleDownload({ fileName, content })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative h-1/4 w-1/4"
|
||||
className={fileWrapperClasses}
|
||||
onClick={() => handleDownload({ fileName, content })}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<img
|
||||
src={`data:image/png;base64,${content}`}
|
||||
alt="generated image"
|
||||
className="h-full w-full rounded-lg"
|
||||
/>
|
||||
{isHovered && (
|
||||
<div className={`file-card-modal-image-div `}>
|
||||
<button
|
||||
className="file-card-modal-image-button "
|
||||
onClick={handleDownload}
|
||||
>
|
||||
<IconComponent
|
||||
name="DownloadCloud"
|
||||
className="h-5 w-5 text-current hover:scale-110"
|
||||
/>
|
||||
</button>
|
||||
<div className="ml-3 flex h-full w-full items-center gap-2 text-sm">
|
||||
<ForwardedIconComponent name="File" className="h-8 w-8" />
|
||||
<div className="flex flex-col">
|
||||
<span className="font-bold">{formatFileName(fileName, 20)}</span>
|
||||
<span>File</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<DownloadButton
|
||||
isHovered={isHovered}
|
||||
handleDownload={() => handleDownload({ fileName, content })}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button onClick={handleDownload} className="file-card-modal-button">
|
||||
<div className="file-card-modal-div">
|
||||
ooooooooooooooo{" "}
|
||||
{fileType === "image" ? (
|
||||
<img
|
||||
src={`data:image/png;base64,${content}`}
|
||||
alt=""
|
||||
className="h-8 w-8"
|
||||
/>
|
||||
) : (
|
||||
<IconComponent name="File" className="h-8 w-8" />
|
||||
)}
|
||||
<div className="file-card-modal-footer">
|
||||
{" "}
|
||||
<div className="file-card-modal-name">{fileName}</div>
|
||||
<div className="file-card-modal-type">{fileType}</div>
|
||||
</div>
|
||||
<IconComponent
|
||||
name="DownloadCloud"
|
||||
className="ml-auto h-6 w-6 text-current"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
export default function getClasses(isHovered: boolean): string {
|
||||
return `relative ${false ? "h-20 w-20" : "h-20 w-80"} cursor-pointer rounded-lg border border-ring bg-muted shadow transition duration-300 hover:drop-shadow-lg ${
|
||||
isHovered ? "shadow-md" : ""
|
||||
}`;
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
import {
|
||||
BACKEND_URL,
|
||||
BASE_URL_API,
|
||||
} from "../../../../../../constants/constants";
|
||||
|
||||
let isDownloading = false;
|
||||
|
||||
export default async function handleDownload({
|
||||
fileName,
|
||||
content,
|
||||
}: {
|
||||
fileName: string;
|
||||
content: string;
|
||||
}): Promise<void> {
|
||||
if (isDownloading) return;
|
||||
|
||||
try {
|
||||
isDownloading = true;
|
||||
|
||||
const response = await fetch(
|
||||
`${BACKEND_URL.slice(0, BACKEND_URL.length - 1)}${BASE_URL_API}files/download/${content}`,
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.setAttribute("download", fileName); // Set the filename
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
URL.revokeObjectURL(url); // Clean up the URL object
|
||||
} catch (error) {
|
||||
console.error("Failed to download file:", error);
|
||||
} finally {
|
||||
isDownloading = false;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
import { useState } from "react";
|
||||
import IconComponent, {
|
||||
ForwardedIconComponent,
|
||||
} from "../../../../../components/genericIconComponent";
|
||||
import { Skeleton } from "../../../../../components/ui/skeleton";
|
||||
import formatFileName from "./utils/format-file-name";
|
||||
|
||||
export default function FilePreview({
|
||||
error,
|
||||
file,
|
||||
loading,
|
||||
onDelete,
|
||||
}: {
|
||||
loading: boolean;
|
||||
file: File;
|
||||
error: boolean;
|
||||
onDelete: () => void;
|
||||
}) {
|
||||
const isImage = file.type.toLowerCase().includes("image");
|
||||
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="relative inline-block">
|
||||
{loading ? (
|
||||
isImage ? (
|
||||
<div className="flex h-20 w-20 items-center justify-center rounded-md border border-ring bg-background ">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className={`h-10 w-10 animate-spin fill-black text-muted`}
|
||||
viewBox="0 0 100 101"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
||||
fill="currentFill"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={`relative ${
|
||||
isImage ? "h-20 w-20" : "h-20 w-80"
|
||||
} cursor-wait rounded-lg border border-ring bg-background transition duration-300 ${
|
||||
isHovered ? "shadow-md" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="ml-3 flex h-full w-full items-center gap-2 text-sm">
|
||||
<Skeleton className="h-10 w-10 rounded-lg" />
|
||||
<div className="flex flex-col gap-1">
|
||||
<Skeleton className="h-3 w-48" />
|
||||
<Skeleton className="h-3 w-10" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
) : error ? (
|
||||
<div>Error...</div>
|
||||
) : (
|
||||
<div
|
||||
className={`relative mt-2 ${
|
||||
isImage ? "h-20 w-20" : "h-20 w-80"
|
||||
} cursor-pointer rounded-lg border border-ring bg-background transition duration-300 ${
|
||||
isHovered ? "shadow-md" : ""
|
||||
}`}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
{isImage ? (
|
||||
<img
|
||||
src={URL.createObjectURL(file)}
|
||||
alt="file"
|
||||
className="block h-full w-full rounded-md border border-border"
|
||||
/>
|
||||
) : (
|
||||
<div className="ml-3 flex h-full w-full items-center gap-2 text-sm">
|
||||
<ForwardedIconComponent name="File" className="h-8 w-8" />
|
||||
<div className="flex flex-col">
|
||||
<span className="font-bold">{formatFileName(file.name)}</span>
|
||||
<span>File</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{isHovered && (
|
||||
<div
|
||||
className={`absolute ${
|
||||
isImage ? "bottom-16 left-16" : "bottom-16 left-[19em]"
|
||||
} flex h-5 w-5 items-center justify-center`}
|
||||
>
|
||||
<div
|
||||
className="flex h-7 w-7 cursor-pointer items-center justify-center rounded-full bg-gray-200 p-2 transition-all"
|
||||
onClick={onDelete}
|
||||
>
|
||||
<IconComponent name="X" className="stroke-slate-950 stroke-2" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
export default function formatFileName(
|
||||
name: string,
|
||||
numberToTruncate: number = 25,
|
||||
): string {
|
||||
if (name[numberToTruncate] === undefined) {
|
||||
return name;
|
||||
}
|
||||
const fileExtension = name.split(".").pop(); // Get the file extension
|
||||
const baseName = name.slice(0, name.lastIndexOf(".")); // Get the base name without the extension
|
||||
if (baseName.length > 6) {
|
||||
return `${baseName.slice(0, numberToTruncate)}...${fileExtension}`;
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { useEffect, useRef, useState } from "react";
|
||||
import IconComponent from "../../../../components/genericIconComponent";
|
||||
import { Button } from "../../../../components/ui/button";
|
||||
import {
|
||||
CHAT_FIRST_INITIAL_TEXT,
|
||||
CHAT_SECOND_INITIAL_TEXT,
|
||||
|
|
@ -8,15 +9,12 @@ import { deleteFlowPool } from "../../../../controllers/API";
|
|||
import useAlertStore from "../../../../stores/alertStore";
|
||||
import useFlowStore from "../../../../stores/flowStore";
|
||||
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
|
||||
import { sendAllProps } from "../../../../types/api";
|
||||
import {
|
||||
ChatMessageType,
|
||||
ChatOutputType,
|
||||
FlowPoolObjectType,
|
||||
} from "../../../../types/chat";
|
||||
import { chatViewProps } from "../../../../types/components";
|
||||
import { VertexBuildTypeAPI, sendAllProps } from "../../../../types/api";
|
||||
import { ChatMessageType, ChatOutputType } from "../../../../types/chat";
|
||||
import { FilePreviewType, chatViewProps } from "../../../../types/components";
|
||||
import { classNames } from "../../../../utils/utils";
|
||||
import ChatInput from "./chatInput";
|
||||
import useDragAndDrop from "./chatInput/hooks/use-drag-and-drop";
|
||||
import ChatMessage from "./chatMessage";
|
||||
|
||||
export default function ChatView({
|
||||
|
|
@ -46,7 +44,7 @@ export default function ChatView({
|
|||
|
||||
//build chat history
|
||||
useEffect(() => {
|
||||
const chatOutputResponses: FlowPoolObjectType[] = [];
|
||||
const chatOutputResponses: VertexBuildTypeAPI[] = [];
|
||||
outputIds.forEach((outputId) => {
|
||||
if (outputId.includes("ChatOutput")) {
|
||||
if (flowPool[outputId] && flowPool[outputId].length > 0) {
|
||||
|
|
@ -64,11 +62,11 @@ export default function ChatView({
|
|||
const chatMessages: ChatMessageType[] = chatOutputResponses
|
||||
.sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp))
|
||||
//
|
||||
.filter((output) => output.data.artifacts?.message !== null)
|
||||
.filter((output) => output.data.message)
|
||||
.map((output, index) => {
|
||||
try {
|
||||
const { sender, message, sender_name, stream_url } = output.data
|
||||
.artifacts as ChatOutputType;
|
||||
const { sender, message, sender_name, stream_url, files } = output
|
||||
.data.message as ChatOutputType;
|
||||
|
||||
const is_ai = sender === "Machine" || sender === null;
|
||||
return {
|
||||
|
|
@ -77,6 +75,7 @@ export default function ChatView({
|
|||
sender_name,
|
||||
componentId: output.id,
|
||||
stream_url: stream_url,
|
||||
files,
|
||||
};
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
|
@ -118,10 +117,21 @@ export default function ChatView({
|
|||
if (lockChat) setLockChat(false);
|
||||
}
|
||||
|
||||
function handleSelectChange(event: string): void {
|
||||
switch (event) {
|
||||
case "builds":
|
||||
clearChat();
|
||||
break;
|
||||
case "buildsNSession":
|
||||
console.log("delete build and session");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function updateChat(
|
||||
chat: ChatMessageType,
|
||||
message: string,
|
||||
stream_url?: string
|
||||
stream_url?: string,
|
||||
) {
|
||||
// if (message === "") return;
|
||||
chat.message = message;
|
||||
|
|
@ -144,23 +154,76 @@ export default function ChatView({
|
|||
// return newChatHistory;
|
||||
// });
|
||||
}
|
||||
const [files, setFiles] = useState<FilePreviewType[]>([]);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
|
||||
const { dragOver, dragEnter, dragLeave, onDrop } = useDragAndDrop(
|
||||
setIsDragging,
|
||||
setFiles,
|
||||
currentFlowId,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="eraser-column-arrangement">
|
||||
<div
|
||||
className="eraser-column-arrangement"
|
||||
onDragOver={dragOver}
|
||||
onDragEnter={dragEnter}
|
||||
onDragLeave={dragLeave}
|
||||
onDrop={onDrop}
|
||||
>
|
||||
<div className="eraser-size">
|
||||
<div className="eraser-position">
|
||||
<button disabled={lockChat} onClick={() => clearChat()}>
|
||||
<Button
|
||||
className="flex gap-1"
|
||||
size="none"
|
||||
variant="none"
|
||||
disabled={lockChat}
|
||||
onClick={() => handleSelectChange("builds")}
|
||||
>
|
||||
<IconComponent
|
||||
name="Eraser"
|
||||
className={classNames(
|
||||
"h-5 w-5",
|
||||
lockChat
|
||||
? "animate-pulse text-primary"
|
||||
: "text-primary hover:text-gray-600"
|
||||
)}
|
||||
className={classNames("h-5 w-5 text-primary")}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
</Button>
|
||||
{/* <Select
|
||||
onValueChange={handleSelectChange}
|
||||
value=""
|
||||
disabled={lockChat}
|
||||
>
|
||||
<SelectTrigger className="">
|
||||
<button className="flex gap-1">
|
||||
<IconComponent
|
||||
name="Eraser"
|
||||
className={classNames(
|
||||
"h-5 w-5 transition-all duration-100",
|
||||
lockChat ? "animate-pulse text-primary" : "text-primary",
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
</SelectTrigger>
|
||||
<SelectContent className="right-[9.5em]">
|
||||
<SelectItem value="builds" className="cursor-pointer">
|
||||
<div className="flex">
|
||||
<IconComponent
|
||||
name={"Trash2"}
|
||||
className={`relative top-0.5 mr-2 h-4 w-4`}
|
||||
/>
|
||||
<span className="">Clear Builds</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="buildsNSession" className="cursor-pointer">
|
||||
<div className="flex">
|
||||
<IconComponent
|
||||
name={"Trash2"}
|
||||
className={`relative top-0.5 mr-2 h-4 w-4`}
|
||||
/>
|
||||
<span className="">Clear Builds & Session</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select> */}
|
||||
</div>
|
||||
<div ref={messagesRef} className="chat-message-div">
|
||||
{chatHistory?.length > 0 ? (
|
||||
|
|
@ -202,11 +265,16 @@ export default function ChatView({
|
|||
chatValue={chatValue}
|
||||
noInput={!inputTypes.includes("ChatInput")}
|
||||
lockChat={lockChat}
|
||||
sendMessage={(count) => sendMessage(count)}
|
||||
sendMessage={({ repeat, files }) =>
|
||||
sendMessage({ repeat, files })
|
||||
}
|
||||
setChatValue={(value) => {
|
||||
setChatValue(value);
|
||||
}}
|
||||
inputRef={ref}
|
||||
files={files}
|
||||
setFiles={setFiles}
|
||||
isDragging={isDragging}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,26 +3,28 @@ import AccordionComponent from "../../components/accordionComponent";
|
|||
import IconComponent from "../../components/genericIconComponent";
|
||||
import ShadTooltip from "../../components/shadTooltipComponent";
|
||||
import { Badge } from "../../components/ui/badge";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "../../components/ui/tabs";
|
||||
import {
|
||||
CHAT_FORM_DIALOG_SUBTITLE,
|
||||
OUTPUTS_MODAL_TITLE,
|
||||
TEXT_INPUT_MODAL_TITLE,
|
||||
} from "../../constants/constants";
|
||||
import { CHAT_FORM_DIALOG_SUBTITLE } from "../../constants/constants";
|
||||
import { InputOutput } from "../../constants/enums";
|
||||
import { getMessagesTable } from "../../controllers/API";
|
||||
import useAlertStore from "../../stores/alertStore";
|
||||
import useFlowStore from "../../stores/flowStore";
|
||||
import useFlowsManagerStore from "../../stores/flowsManagerStore";
|
||||
import { useMessagesStore } from "../../stores/messagesStore";
|
||||
import { IOModalPropsType } from "../../types/components";
|
||||
import { NodeType } from "../../types/flow";
|
||||
import { NodeDataType, NodeType } from "../../types/flow";
|
||||
import { updateVerticesOrder } from "../../utils/buildUtils";
|
||||
import { cn } from "../../utils/utils";
|
||||
import BaseModal from "../baseModal";
|
||||
import IOFieldView from "./components/IOFieldView";
|
||||
import SessionView from "./components/SessionView";
|
||||
import useRemoveSession from "./components/SessionView/hooks";
|
||||
import ChatView from "./components/chatView";
|
||||
|
||||
export default function IOModal({
|
||||
|
|
@ -32,6 +34,7 @@ export default function IOModal({
|
|||
disable,
|
||||
}: IOModalPropsType): JSX.Element {
|
||||
const allNodes = useFlowStore((state) => state.nodes);
|
||||
const setMessages = useMessagesStore((state) => state.setMessages);
|
||||
const inputs = useFlowStore((state) => state.inputs).filter(
|
||||
(input) => input.type !== "ChatInput",
|
||||
);
|
||||
|
|
@ -53,6 +56,8 @@ export default function IOModal({
|
|||
const [selectedTab, setSelectedTab] = useState(
|
||||
inputs.length > 0 ? 1 : outputs.length > 0 ? 2 : 0,
|
||||
);
|
||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||
const setSuccessData = useAlertStore((state) => state.setSuccessData);
|
||||
|
||||
function startView() {
|
||||
if (!chatInput && !chatOutput) {
|
||||
|
|
@ -77,51 +82,94 @@ export default function IOModal({
|
|||
const isBuilding = useFlowStore((state) => state.isBuilding);
|
||||
const currentFlow = useFlowsManagerStore((state) => state.currentFlow);
|
||||
const setNode = useFlowStore((state) => state.setNode);
|
||||
|
||||
const [sessions, setSessions] = useState<string[]>([]);
|
||||
const messages = useMessagesStore((state) => state.messages);
|
||||
const setColumns = useMessagesStore((state) => state.setColumns);
|
||||
async function updateVertices() {
|
||||
return updateVerticesOrder(currentFlow!.id, null);
|
||||
}
|
||||
|
||||
async function sendMessage(count = 1): Promise<void> {
|
||||
async function sendMessage({
|
||||
repeat = 1,
|
||||
files,
|
||||
}: {
|
||||
repeat: number;
|
||||
files?: string[];
|
||||
}): Promise<void> {
|
||||
if (isBuilding) return;
|
||||
setIsBuilding(true);
|
||||
setLockChat(true);
|
||||
setChatValue("");
|
||||
for (let i = 0; i < count; i++) {
|
||||
for (let i = 0; i < repeat; i++) {
|
||||
await buildFlow({
|
||||
input_value: chatValue,
|
||||
startNodeId: chatInput?.id,
|
||||
files: files,
|
||||
silent: true,
|
||||
}).catch((err) => {
|
||||
console.error(err);
|
||||
setLockChat(false);
|
||||
});
|
||||
}
|
||||
const { rows, columns } = await getMessagesTable("union", currentFlow!.id);
|
||||
setMessages(rows);
|
||||
setColumns(columns);
|
||||
setLockChat(false);
|
||||
if (chatInput) {
|
||||
setNode(chatInput.id, (node: NodeType) => {
|
||||
const newNode = { ...node };
|
||||
|
||||
newNode.data.node!.template["input_value"].value = chatValue;
|
||||
return newNode;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const { handleRemoveSession } = useRemoveSession(
|
||||
setSuccessData,
|
||||
setErrorData,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedTab(inputs.length > 0 ? 1 : outputs.length > 0 ? 2 : 0);
|
||||
}, [allNodes.length]);
|
||||
|
||||
const flow_sessions = allNodes.map((node) => {
|
||||
if ((node.data as NodeDataType).node?.template["session_id"]) {
|
||||
return {
|
||||
id: node.id,
|
||||
session_id: (node.data as NodeDataType).node?.template["session_id"]
|
||||
.value,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedViewField(startView());
|
||||
if (haveChat) {
|
||||
getMessagesTable("union", currentFlow!.id).then(({ rows, columns }) => {
|
||||
setMessages(rows);
|
||||
setColumns(columns);
|
||||
});
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
const sessions = new Set<string>();
|
||||
messages.forEach((row) => {
|
||||
sessions.add(row.session_id);
|
||||
});
|
||||
setSessions(Array.from(sessions));
|
||||
sessions;
|
||||
}, [messages]);
|
||||
|
||||
return (
|
||||
<BaseModal
|
||||
size={selectedTab === 0 ? "sm-thin" : "md-thin"}
|
||||
size={"md-thin"}
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
disable={disable}
|
||||
onSubmit={() => sendMessage(1)}
|
||||
onSubmit={() => sendMessage({ repeat: 1 })}
|
||||
>
|
||||
<BaseModal.Trigger>{children}</BaseModal.Trigger>
|
||||
{/* TODO ADAPT TO ALL TYPES OF INPUTS AND OUTPUTS */}
|
||||
|
|
@ -138,176 +186,231 @@ export default function IOModal({
|
|||
<BaseModal.Content>
|
||||
<div className="flex h-full flex-col ">
|
||||
<div className="flex-max-width h-full">
|
||||
{selectedTab !== 0 && (
|
||||
<div
|
||||
className={cn(
|
||||
"mr-6 flex h-full w-2/6 flex-shrink-0 flex-col justify-start transition-all duration-300",
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
"mr-6 flex h-full w-2/6 flex-shrink-0 flex-col justify-start transition-all duration-300",
|
||||
)}
|
||||
>
|
||||
<Tabs
|
||||
value={selectedTab.toString()}
|
||||
className={
|
||||
"flex h-full flex-col overflow-y-auto rounded-md border bg-muted text-center custom-scroll"
|
||||
}
|
||||
onValueChange={(value) => {
|
||||
setSelectedTab(Number(value));
|
||||
}}
|
||||
>
|
||||
<Tabs
|
||||
value={selectedTab.toString()}
|
||||
className={
|
||||
"flex h-full flex-col overflow-y-auto rounded-md border bg-muted text-center custom-scroll"
|
||||
}
|
||||
onValueChange={(value) => {
|
||||
setSelectedTab(Number(value));
|
||||
}}
|
||||
>
|
||||
<div className="api-modal-tablist-div">
|
||||
<TabsList>
|
||||
{inputs.length > 0 && (
|
||||
<TabsTrigger value={"1"}>Inputs</TabsTrigger>
|
||||
)}
|
||||
{outputs.length > 0 && (
|
||||
<TabsTrigger value={"2"}>Outputs</TabsTrigger>
|
||||
)}
|
||||
</TabsList>
|
||||
</div>
|
||||
<div className="api-modal-tablist-div">
|
||||
<TabsList>
|
||||
{inputs.length > 0 && (
|
||||
<TabsTrigger value={"1"}>Inputs</TabsTrigger>
|
||||
)}
|
||||
{outputs.length > 0 && (
|
||||
<TabsTrigger value={"2"}>Outputs</TabsTrigger>
|
||||
)}
|
||||
{haveChat && <TabsTrigger value={"0"}>History</TabsTrigger>}
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
<TabsContent
|
||||
value={"1"}
|
||||
className="api-modal-tabs-content mt-4"
|
||||
>
|
||||
<div className="mx-2 mb-2 flex items-center gap-2 text-sm font-bold">
|
||||
<IconComponent className="h-4 w-4" name={"Type"} />
|
||||
{TEXT_INPUT_MODAL_TITLE}
|
||||
</div>
|
||||
{nodes
|
||||
.filter((node) =>
|
||||
inputs.some((input) => input.id === node.id),
|
||||
)
|
||||
.map((node, index) => {
|
||||
const input = inputs.find(
|
||||
(input) => input.id === node.id,
|
||||
)!;
|
||||
return (
|
||||
<div
|
||||
className="file-component-accordion-div"
|
||||
key={index}
|
||||
>
|
||||
<AccordionComponent
|
||||
trigger={
|
||||
<div className="file-component-badge-div">
|
||||
<ShadTooltip
|
||||
content={input.id}
|
||||
styleClasses="z-50"
|
||||
>
|
||||
<div>
|
||||
<Badge variant="gray" size="md">
|
||||
{node.data.node.display_name}
|
||||
</Badge>
|
||||
</div>
|
||||
</ShadTooltip>
|
||||
<div
|
||||
className="-mb-1 pr-4"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
setSelectedViewField(input);
|
||||
}}
|
||||
>
|
||||
<IconComponent
|
||||
className="h-4 w-4"
|
||||
name="ExternalLink"
|
||||
></IconComponent>
|
||||
<TabsContent value={"1"} className="api-modal-tabs-content">
|
||||
{nodes
|
||||
.filter((node) =>
|
||||
inputs.some((input) => input.id === node.id),
|
||||
)
|
||||
.map((node, index) => {
|
||||
const input = inputs.find(
|
||||
(input) => input.id === node.id,
|
||||
)!;
|
||||
return (
|
||||
<div
|
||||
className="file-component-accordion-div"
|
||||
key={index}
|
||||
>
|
||||
<AccordionComponent
|
||||
disabled={
|
||||
node.data.node!.template["input_value"]?.value ===
|
||||
""
|
||||
}
|
||||
trigger={
|
||||
<div className="file-component-badge-div">
|
||||
<ShadTooltip
|
||||
content={input.id}
|
||||
styleClasses="z-50"
|
||||
>
|
||||
<div>
|
||||
<Badge variant="gray" size="md">
|
||||
{node.data.node.display_name}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
key={index}
|
||||
keyValue={input.id}
|
||||
>
|
||||
<div className="file-component-tab-column">
|
||||
<div className="">
|
||||
{input && (
|
||||
<IOFieldView
|
||||
type={InputOutput.INPUT}
|
||||
left={true}
|
||||
fieldType={input.type}
|
||||
fieldId={input.id}
|
||||
/>
|
||||
)}
|
||||
</ShadTooltip>
|
||||
<div
|
||||
className="-mb-1 pr-4"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
setSelectedViewField(input);
|
||||
}}
|
||||
>
|
||||
<IconComponent
|
||||
className="h-4 w-4"
|
||||
name="ExternalLink"
|
||||
></IconComponent>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionComponent>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</TabsContent>
|
||||
<TabsContent
|
||||
value={"2"}
|
||||
className="api-modal-tabs-content mt-4"
|
||||
>
|
||||
<div className="mx-2 mb-2 flex items-center gap-2 text-sm font-bold">
|
||||
<IconComponent className="h-4 w-4" name={"Type"} />
|
||||
{OUTPUTS_MODAL_TITLE}
|
||||
</div>
|
||||
{nodes
|
||||
.filter((node) =>
|
||||
outputs.some((output) => output.id === node.id),
|
||||
)
|
||||
.map((node, index) => {
|
||||
const output = outputs.find(
|
||||
(output) => output.id === node.id,
|
||||
)!;
|
||||
return (
|
||||
<div
|
||||
className="file-component-accordion-div"
|
||||
}
|
||||
key={index}
|
||||
keyValue={input.id}
|
||||
>
|
||||
<AccordionComponent
|
||||
disabled={
|
||||
node.data.node!.template["input_value"]
|
||||
?.value === ""
|
||||
}
|
||||
trigger={
|
||||
<div className="file-component-badge-div">
|
||||
<ShadTooltip
|
||||
content={output.id}
|
||||
styleClasses="z-50"
|
||||
>
|
||||
<div>
|
||||
<Badge variant="gray" size="md">
|
||||
{node.data.node.display_name}
|
||||
</Badge>
|
||||
</div>
|
||||
</ShadTooltip>
|
||||
<div
|
||||
className="-mb-1 pr-4"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
setSelectedViewField(output);
|
||||
}}
|
||||
>
|
||||
<IconComponent
|
||||
className="h-4 w-4"
|
||||
name="ExternalLink"
|
||||
></IconComponent>
|
||||
<div className="file-component-tab-column">
|
||||
<div className="">
|
||||
{input && (
|
||||
<IOFieldView
|
||||
type={InputOutput.INPUT}
|
||||
left={true}
|
||||
fieldType={input.type}
|
||||
fieldId={input.id}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</AccordionComponent>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</TabsContent>
|
||||
<TabsContent value={"2"} className="api-modal-tabs-content">
|
||||
{nodes
|
||||
.filter((node) =>
|
||||
outputs.some((output) => output.id === node.id),
|
||||
)
|
||||
.map((node, index) => {
|
||||
const output = outputs.find(
|
||||
(output) => output.id === node.id,
|
||||
)!;
|
||||
return (
|
||||
<div
|
||||
className="file-component-accordion-div"
|
||||
key={index}
|
||||
>
|
||||
<AccordionComponent
|
||||
trigger={
|
||||
<div className="file-component-badge-div">
|
||||
<ShadTooltip
|
||||
content={output.id}
|
||||
styleClasses="z-50"
|
||||
>
|
||||
<div>
|
||||
<Badge variant="gray" size="md">
|
||||
{node.data.node.display_name}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
key={index}
|
||||
keyValue={output.id}
|
||||
>
|
||||
<div className="file-component-tab-column">
|
||||
<div className="">
|
||||
{output && (
|
||||
<IOFieldView
|
||||
type={InputOutput.OUTPUT}
|
||||
left={true}
|
||||
fieldType={output.type}
|
||||
fieldId={output.id}
|
||||
/>
|
||||
)}
|
||||
</ShadTooltip>
|
||||
<div
|
||||
className="-mb-1 pr-4"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
setSelectedViewField(output);
|
||||
}}
|
||||
>
|
||||
<IconComponent
|
||||
className="h-4 w-4"
|
||||
name="ExternalLink"
|
||||
></IconComponent>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionComponent>
|
||||
}
|
||||
key={index}
|
||||
keyValue={output.id}
|
||||
>
|
||||
<div className="file-component-tab-column">
|
||||
<div className="">
|
||||
{output && (
|
||||
<IOFieldView
|
||||
type={InputOutput.OUTPUT}
|
||||
left={true}
|
||||
fieldType={output.type}
|
||||
fieldId={output.id}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</AccordionComponent>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</TabsContent>
|
||||
<TabsContent value={"0"} className="api-modal-tabs-content">
|
||||
{sessions.map((session, index) => {
|
||||
return (
|
||||
<div
|
||||
className="file-component-accordion-div cursor-pointer"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
setSelectedViewField({
|
||||
id: session,
|
||||
type: "Session",
|
||||
});
|
||||
}}
|
||||
>
|
||||
<div className="flex w-full items-center justify-between border-b px-2 py-1 align-middle">
|
||||
<Badge variant="gray" size="md">
|
||||
{session}
|
||||
</Badge>
|
||||
<div className="flex items-center justify-center gap-2 align-middle">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleRemoveSession(session);
|
||||
if (selectedViewField?.id === session)
|
||||
setSelectedViewField(undefined);
|
||||
}}
|
||||
>
|
||||
<ShadTooltip
|
||||
styleClasses="z-50"
|
||||
content={"delete"}
|
||||
>
|
||||
<div>
|
||||
<IconComponent
|
||||
name="Trash2"
|
||||
className="h-4 w-4"
|
||||
></IconComponent>
|
||||
</div>
|
||||
</ShadTooltip>
|
||||
</Button>
|
||||
<div>
|
||||
<ShadTooltip
|
||||
styleClasses="z-50"
|
||||
content={
|
||||
flow_sessions.some(
|
||||
(f_session) =>
|
||||
f_session?.session_id === session,
|
||||
)
|
||||
? "Active Session"
|
||||
: "Inactive Session"
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"h-2 w-2 rounded-full",
|
||||
flow_sessions.some(
|
||||
(f_session) =>
|
||||
f_session?.session_id === session,
|
||||
)
|
||||
? "bg-status-green"
|
||||
: "bg-slate-500",
|
||||
)}
|
||||
></div>
|
||||
</ShadTooltip>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
<div className="flex h-full min-w-96 flex-grow">
|
||||
{selectedViewField && (
|
||||
<div
|
||||
|
|
@ -333,14 +436,17 @@ export default function IOModal({
|
|||
<div className="h-full w-full">
|
||||
{inputs.some(
|
||||
(input) => input.id === selectedViewField.id,
|
||||
) ? (
|
||||
) && (
|
||||
<IOFieldView
|
||||
type={InputOutput.INPUT}
|
||||
left={false}
|
||||
fieldType={selectedViewField.type!}
|
||||
fieldId={selectedViewField.id!}
|
||||
/>
|
||||
) : (
|
||||
)}
|
||||
{outputs.some(
|
||||
(output) => output.id === selectedViewField.id,
|
||||
) && (
|
||||
<IOFieldView
|
||||
type={InputOutput.OUTPUT}
|
||||
left={false}
|
||||
|
|
@ -348,6 +454,16 @@ export default function IOModal({
|
|||
fieldId={selectedViewField.id!}
|
||||
/>
|
||||
)}
|
||||
{sessions.some(
|
||||
(session) => session === selectedViewField.id,
|
||||
) && (
|
||||
<SessionView
|
||||
rows={messages.filter(
|
||||
(message) =>
|
||||
message.session_id === selectedViewField.id,
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -65,7 +65,6 @@ const ApiModal = forwardRef(
|
|||
);
|
||||
const pythonCode = getPythonCode(flow?.name, tweak);
|
||||
const widgetCode = getWidgetCode(flow?.id, flow?.name, autoLogin);
|
||||
console.log("flow", flow);
|
||||
const includeWebhook = flow.webhook;
|
||||
const tweaksCode = buildTweaks(flow);
|
||||
const codesArray = [
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ export const switchCaseModalSize = (size: string) => {
|
|||
switch (size) {
|
||||
case "x-small":
|
||||
minWidth = "min-w-[20vw]";
|
||||
height = "h-full";
|
||||
height = "";
|
||||
break;
|
||||
case "smaller":
|
||||
minWidth = "min-w-[40vw]";
|
||||
|
|
@ -12,7 +12,7 @@ export const switchCaseModalSize = (size: string) => {
|
|||
break;
|
||||
case "smaller-h-full":
|
||||
minWidth = "min-w-[40vw]";
|
||||
height = "h-full";
|
||||
height = "";
|
||||
break;
|
||||
case "small":
|
||||
minWidth = "min-w-[40vw]";
|
||||
|
|
@ -20,16 +20,19 @@ export const switchCaseModalSize = (size: string) => {
|
|||
break;
|
||||
case "small-h-full":
|
||||
minWidth = "min-w-[40vw]";
|
||||
height = "h-full";
|
||||
height = "";
|
||||
break;
|
||||
case "medium":
|
||||
minWidth = "min-w-[60vw]";
|
||||
height = "h-[60vh]";
|
||||
break;
|
||||
case "medium-tall":
|
||||
minWidth = "min-w-[60vw]";
|
||||
height = "h-[90vh]";
|
||||
break;
|
||||
case "medium-h-full":
|
||||
minWidth = "min-w-[60vw]";
|
||||
height = "h-full";
|
||||
|
||||
height = "";
|
||||
break;
|
||||
case "large":
|
||||
minWidth = "min-w-[85vw]";
|
||||
|
|
@ -41,26 +44,26 @@ export const switchCaseModalSize = (size: string) => {
|
|||
break;
|
||||
case "large-thin":
|
||||
minWidth = "min-w-[65vw]";
|
||||
height = "h-[80vh]";
|
||||
height = "h-[90vh]";
|
||||
break;
|
||||
|
||||
case "md-thin":
|
||||
minWidth = "min-w-[85vw]";
|
||||
height = "h-[70vh]";
|
||||
height = "h-[90vh]";
|
||||
break;
|
||||
|
||||
case "sm-thin":
|
||||
minWidth = "min-w-[65vw]";
|
||||
height = "h-[70vh]";
|
||||
height = "h-[90vh]";
|
||||
break;
|
||||
|
||||
case "large-h-full":
|
||||
minWidth = "min-w-[80vw]";
|
||||
height = "h-full";
|
||||
height = "";
|
||||
break;
|
||||
default:
|
||||
minWidth = "min-w-[80vw]";
|
||||
height = "h-[80vh]";
|
||||
height = "h-[90vh]";
|
||||
break;
|
||||
}
|
||||
return { minWidth, height };
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {
|
|||
} from "../../components/ui/dialog-with-no-close";
|
||||
|
||||
import { DialogClose } from "@radix-ui/react-dialog";
|
||||
import * as Form from "@radix-ui/react-form";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { modalHeaderType } from "../../types/components";
|
||||
import { cn } from "../../utils/utils";
|
||||
|
|
@ -52,10 +53,10 @@ const Trigger: React.FC<TriggerProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
const Header: React.FC<{ children: ReactNode; description: string | null }> = ({
|
||||
children,
|
||||
description,
|
||||
}: modalHeaderType): JSX.Element => {
|
||||
const Header: React.FC<{
|
||||
children: ReactNode;
|
||||
description: string | JSX.Element | null;
|
||||
}> = ({ children, description }: modalHeaderType): JSX.Element => {
|
||||
return (
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center">{children}</DialogTitle>
|
||||
|
|
@ -111,6 +112,7 @@ interface BaseModalProps {
|
|||
| "smaller"
|
||||
| "small"
|
||||
| "medium"
|
||||
| "medium-tall"
|
||||
| "large"
|
||||
| "three-cards"
|
||||
| "large-thin"
|
||||
|
|
@ -119,7 +121,8 @@ interface BaseModalProps {
|
|||
| "medium-h-full"
|
||||
| "md-thin"
|
||||
| "sm-thin"
|
||||
| "smaller-h-full";
|
||||
| "smaller-h-full"
|
||||
| "medium-log";
|
||||
|
||||
disable?: boolean;
|
||||
onChangeOpenModal?: (open?: boolean) => void;
|
||||
|
|
@ -162,53 +165,63 @@ function BaseModal({
|
|||
{type === "modal" ? (
|
||||
<Modal open={open} onOpenChange={setOpen}>
|
||||
{triggerChild}
|
||||
<ModalContent className={cn(minWidth, "duration-300")}>
|
||||
<div className="truncate-doubleline word-break-break-word">
|
||||
<ModalContent
|
||||
className={cn(minWidth, height, "flex flex-col duration-300")}
|
||||
>
|
||||
<div className="flex-shrink-0 truncate-doubleline word-break-break-word">
|
||||
{headerChild}
|
||||
</div>
|
||||
<div
|
||||
className={`flex flex-col ${height} w-full transition-all duration-300`}
|
||||
className={`flex w-full flex-1 flex-col transition-all duration-300`}
|
||||
>
|
||||
{ContentChild}
|
||||
</div>
|
||||
{ContentFooter && (
|
||||
<div className="flex flex-row-reverse">{ContentFooter}</div>
|
||||
<div className="flex flex-shrink-0 flex-row-reverse">
|
||||
{ContentFooter}
|
||||
</div>
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
) : (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
{triggerChild}
|
||||
<DialogContent className={cn(minWidth, "duration-300")}>
|
||||
<div className="truncate-doubleline word-break-break-word">
|
||||
<DialogContent
|
||||
className={cn(minWidth, height, "flex flex-col duration-300")}
|
||||
>
|
||||
<div className="flex-shrink-0 truncate-doubleline word-break-break-word">
|
||||
{headerChild}
|
||||
</div>
|
||||
{onSubmit ? (
|
||||
<form
|
||||
<Form.Root
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
onSubmit();
|
||||
}}
|
||||
className="flex flex-col gap-6"
|
||||
className="flex min-h-0 flex-1 flex-col gap-6"
|
||||
>
|
||||
<div
|
||||
className={`flex flex-col ${height} w-full transition-all duration-300`}
|
||||
className={`flex w-full flex-1 flex-col overflow-hidden transition-all duration-300`}
|
||||
>
|
||||
{ContentChild}
|
||||
</div>
|
||||
{ContentFooter && (
|
||||
<div className="flex flex-row-reverse">{ContentFooter}</div>
|
||||
<div className="flex flex-shrink-0 flex-row-reverse">
|
||||
{ContentFooter}
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</Form.Root>
|
||||
) : (
|
||||
<>
|
||||
<div
|
||||
className={`flex flex-col ${height} w-full transition-all duration-300`}
|
||||
className={`flex min-h-0 w-full flex-1 flex-col transition-all duration-300`}
|
||||
>
|
||||
{ContentChild}
|
||||
</div>
|
||||
{ContentFooter && (
|
||||
<div className="flex flex-row-reverse">{ContentFooter}</div>
|
||||
<div className="flex flex-shrink-0 flex-row-reverse">
|
||||
{ContentFooter}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,90 @@
|
|||
import { ColDef, ValueGetterParams } from "ag-grid-community";
|
||||
import { useMemo } from "react";
|
||||
import TableNodeCellRender from "../../../components/tableComponent/components/tableNodeCellRender";
|
||||
import TableToggleCellRender from "../../../components/tableComponent/components/tableToggleCellRender";
|
||||
import TableTooltipRender from "../../../components/tableComponent/components/tableTooltipRender";
|
||||
|
||||
const useColumnDefs = (
|
||||
myData: any,
|
||||
handleOnNewValue: (newValue: any, name: string) => void,
|
||||
changeAdvanced: (n: string) => void,
|
||||
open: boolean,
|
||||
) => {
|
||||
const columnDefs: ColDef[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
headerName: "Name",
|
||||
field: "display_name",
|
||||
valueGetter: (params) => {
|
||||
const templateParam = params.data;
|
||||
return (
|
||||
(templateParam.display_name
|
||||
? templateParam.display_name
|
||||
: templateParam.name) ?? params.data.key
|
||||
);
|
||||
},
|
||||
tooltipField: "display_name",
|
||||
tooltipComponent: TableTooltipRender,
|
||||
wrapText: true,
|
||||
autoHeight: true,
|
||||
flex: 1,
|
||||
resizable: false,
|
||||
cellClass: "no-border",
|
||||
},
|
||||
{
|
||||
headerName: "Description",
|
||||
field: "info",
|
||||
tooltipField: "info",
|
||||
tooltipComponent: TableTooltipRender,
|
||||
wrapText: true,
|
||||
autoHeight: true,
|
||||
flex: 2,
|
||||
resizable: false,
|
||||
cellClass: "no-border",
|
||||
},
|
||||
{
|
||||
headerName: "Value",
|
||||
field: "value",
|
||||
cellRenderer: TableNodeCellRender,
|
||||
valueGetter: (params: ValueGetterParams) => {
|
||||
return {
|
||||
value: params.data.value,
|
||||
nodeClass: myData.node,
|
||||
handleOnNewValue: handleOnNewValue,
|
||||
handleOnChangeDb: (value, key) => {
|
||||
myData.node!.template[key].load_from_db = value;
|
||||
},
|
||||
};
|
||||
},
|
||||
minWidth: 330,
|
||||
autoHeight: true,
|
||||
flex: 1,
|
||||
resizable: false,
|
||||
cellClass: "no-border",
|
||||
},
|
||||
{
|
||||
headerName: "Show",
|
||||
field: "advanced",
|
||||
cellRenderer: TableToggleCellRender,
|
||||
valueGetter: (params: ValueGetterParams) => {
|
||||
return {
|
||||
name: params.data.name,
|
||||
enabled: !params.data.advanced,
|
||||
setEnabled: () => {
|
||||
changeAdvanced(params.data.key);
|
||||
},
|
||||
};
|
||||
},
|
||||
editable: false,
|
||||
maxWidth: 80,
|
||||
resizable: false,
|
||||
cellClass: "no-border",
|
||||
},
|
||||
],
|
||||
[open, myData],
|
||||
);
|
||||
|
||||
return columnDefs;
|
||||
};
|
||||
|
||||
export default useColumnDefs;
|
||||
37
src/frontend/src/modals/editNodeModal/hooks/use-row-data.tsx
Normal file
37
src/frontend/src/modals/editNodeModal/hooks/use-row-data.tsx
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { useMemo } from "react";
|
||||
import { LANGFLOW_SUPPORTED_TYPES } from "../../../constants/constants";
|
||||
import { TemplateVariableType } from "../../../types/api";
|
||||
|
||||
const useRowData = (myData, open) => {
|
||||
const rowData = useMemo(() => {
|
||||
return Object.keys(myData.node!.template)
|
||||
.filter((key: string) => {
|
||||
const templateParam = myData.node!.template[
|
||||
key
|
||||
] as TemplateVariableType;
|
||||
return (
|
||||
key.charAt(0) !== "_" &&
|
||||
templateParam.show &&
|
||||
LANGFLOW_SUPPORTED_TYPES.has(templateParam.type) &&
|
||||
!(
|
||||
(key === "code" && templateParam.type === "code") ||
|
||||
(key.includes("code") && templateParam.proxy)
|
||||
)
|
||||
);
|
||||
})
|
||||
.map((key: string) => {
|
||||
const templateParam = myData.node!.template[
|
||||
key
|
||||
] as TemplateVariableType;
|
||||
return {
|
||||
...templateParam,
|
||||
key: key,
|
||||
id: key,
|
||||
};
|
||||
});
|
||||
}, [open, myData]);
|
||||
|
||||
return rowData;
|
||||
};
|
||||
|
||||
export default useRowData;
|
||||
|
|
@ -1,43 +1,13 @@
|
|||
import { cloneDeep } from "lodash";
|
||||
import { forwardRef, useEffect, useState } from "react";
|
||||
import CodeAreaComponent from "../../components/codeAreaComponent";
|
||||
import DictComponent from "../../components/dictComponent";
|
||||
import Dropdown from "../../components/dropdownComponent";
|
||||
import FloatComponent from "../../components/floatComponent";
|
||||
import { ColDef, GridApi } from "ag-grid-community";
|
||||
import { forwardRef, useEffect, useRef, useState } from "react";
|
||||
import IconComponent from "../../components/genericIconComponent";
|
||||
import InputFileComponent from "../../components/inputFileComponent";
|
||||
import InputGlobalComponent from "../../components/inputGlobalComponent";
|
||||
import InputListComponent from "../../components/inputListComponent";
|
||||
import IntComponent from "../../components/intComponent";
|
||||
import KeypairListComponent from "../../components/keypairListComponent";
|
||||
import PromptAreaComponent from "../../components/promptComponent";
|
||||
import ShadTooltip from "../../components/shadTooltipComponent";
|
||||
import TextAreaComponent from "../../components/textAreaComponent";
|
||||
import ToggleShadComponent from "../../components/toggleShadComponent";
|
||||
import TableComponent from "../../components/tableComponent";
|
||||
import { Badge } from "../../components/ui/badge";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "../../components/ui/table";
|
||||
import {
|
||||
LANGFLOW_SUPPORTED_TYPES,
|
||||
limitScrollFieldsModal,
|
||||
} from "../../constants/constants";
|
||||
import { Case } from "../../shared/components/caseComponent";
|
||||
import useFlowStore from "../../stores/flowStore";
|
||||
import { NodeDataType } from "../../types/flow";
|
||||
import {
|
||||
convertObjToArray,
|
||||
convertValuesToNumbers,
|
||||
hasDuplicateKeys,
|
||||
scapedJSONStringfy,
|
||||
} from "../../utils/reactflowUtils";
|
||||
import { classNames } from "../../utils/utils";
|
||||
import BaseModal from "../baseModal";
|
||||
import useColumnDefs from "./hooks/use-column-defs";
|
||||
import useRowData from "./hooks/use-row-data";
|
||||
|
||||
const EditNodeModal = forwardRef(
|
||||
(
|
||||
|
|
@ -56,37 +26,36 @@ const EditNodeModal = forwardRef(
|
|||
},
|
||||
ref,
|
||||
) => {
|
||||
const nodes = useFlowStore((state) => state.nodes);
|
||||
const myData = useRef(data);
|
||||
|
||||
const dataFromStore = nodes.find((node) => node.id === node.id)?.data;
|
||||
|
||||
const [myData, setMyData] = useState(dataFromStore ?? data);
|
||||
|
||||
const edges = useFlowStore((state) => state.edges);
|
||||
const setNode = useFlowStore((state) => state.setNode);
|
||||
|
||||
function changeAdvanced(n) {
|
||||
setMyData((old) => {
|
||||
let newData = cloneDeep(old);
|
||||
newData.node!.template[n].advanced =
|
||||
!newData.node!.template[n].advanced;
|
||||
return newData;
|
||||
});
|
||||
myData.current.node!.template[n].advanced =
|
||||
!myData.current.node!.template[n]?.advanced;
|
||||
}
|
||||
|
||||
const handleOnNewValue = (newValue: any, name) => {
|
||||
setMyData((old) => {
|
||||
let newData = cloneDeep(old);
|
||||
newData.node!.template[name].value = newValue;
|
||||
return newData;
|
||||
});
|
||||
myData.current.node!.template[name].value = newValue;
|
||||
};
|
||||
|
||||
const rowData = useRowData(data, open);
|
||||
|
||||
const columnDefs: ColDef[] = useColumnDefs(
|
||||
data,
|
||||
handleOnNewValue,
|
||||
changeAdvanced,
|
||||
open,
|
||||
);
|
||||
|
||||
const [gridApi, setGridApi] = useState<GridApi | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setMyData(data); // reset data to what it is on node when opening modal
|
||||
if (gridApi && open) {
|
||||
myData.current = data;
|
||||
gridApi.refreshCells();
|
||||
}
|
||||
}, [open]);
|
||||
}, [gridApi, open]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
|
|
@ -94,27 +63,18 @@ const EditNodeModal = forwardRef(
|
|||
};
|
||||
}, []);
|
||||
|
||||
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
|
||||
|
||||
const type = (templateParam) => {
|
||||
return myData.node?.template[templateParam].type;
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseModal
|
||||
key={data.id}
|
||||
size="large-h-full"
|
||||
size="medium-tall"
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
onChangeOpenModal={(open) => {
|
||||
setMyData(data);
|
||||
}}
|
||||
onSubmit={() => {
|
||||
setNode(data.id, (old) => ({
|
||||
...old,
|
||||
data: {
|
||||
...old.data,
|
||||
node: myData.node,
|
||||
node: myData.current.node,
|
||||
},
|
||||
}));
|
||||
setOpen(false);
|
||||
|
|
@ -123,526 +83,31 @@ const EditNodeModal = forwardRef(
|
|||
<BaseModal.Trigger>
|
||||
<></>
|
||||
</BaseModal.Trigger>
|
||||
<BaseModal.Header description={myData.node?.description!}>
|
||||
<span className="pr-2">{myData.type}</span>
|
||||
<Badge variant="secondary">ID: {myData.id}</Badge>
|
||||
<BaseModal.Header description={data.node?.description!}>
|
||||
<span className="pr-2">{data.type}</span>
|
||||
<Badge variant="secondary">ID: {data.id}</Badge>
|
||||
</BaseModal.Header>
|
||||
<BaseModal.Content>
|
||||
<div className="flex pb-2">
|
||||
<IconComponent
|
||||
name="Variable"
|
||||
className="edit-node-modal-variable "
|
||||
/>
|
||||
<span className="edit-node-modal-span">Parameters</span>
|
||||
</div>
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="flex pb-2">
|
||||
<IconComponent
|
||||
name="Variable"
|
||||
className="edit-node-modal-variable "
|
||||
/>
|
||||
<span className="edit-node-modal-span">Parameters</span>
|
||||
</div>
|
||||
|
||||
<div className="edit-node-modal-arrangement">
|
||||
<div
|
||||
className={classNames(
|
||||
"edit-node-modal-box",
|
||||
nodeLength > limitScrollFieldsModal
|
||||
? "overflow-scroll overflow-x-hidden custom-scroll"
|
||||
: "",
|
||||
)}
|
||||
>
|
||||
<div className="h-full">
|
||||
{nodeLength > 0 && (
|
||||
<div className="edit-node-modal-table">
|
||||
<Table className="table-fixed bg-muted outline-1">
|
||||
<TableHeader className="edit-node-modal-table-header">
|
||||
<TableRow className="">
|
||||
<TableHead className="h-7 text-center">PARAM</TableHead>
|
||||
<TableHead className="h-7 text-center">DESC</TableHead>
|
||||
<TableHead className="h-7 p-0 text-center">
|
||||
VALUE
|
||||
</TableHead>
|
||||
<TableHead className="h-7 text-center">SHOW</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody className="p-0">
|
||||
{Object.keys(myData.node!.template)
|
||||
.filter(
|
||||
(templateParam) =>
|
||||
templateParam.charAt(0) !== "_" &&
|
||||
myData.node?.template[templateParam].show &&
|
||||
LANGFLOW_SUPPORTED_TYPES.has(
|
||||
myData.node!.template[templateParam].type,
|
||||
),
|
||||
)
|
||||
.map((templateParam, index) => {
|
||||
let id = {
|
||||
inputTypes:
|
||||
myData.node!.template[templateParam].input_types,
|
||||
type: myData.node!.template[templateParam].type,
|
||||
id: myData.id,
|
||||
fieldName: templateParam,
|
||||
};
|
||||
let disabled =
|
||||
edges.some(
|
||||
(edge) =>
|
||||
edge.targetHandle ===
|
||||
scapedJSONStringfy(
|
||||
myData.node!.template[templateParam].proxy
|
||||
? {
|
||||
...id,
|
||||
proxy:
|
||||
myData.node?.template[templateParam]
|
||||
.proxy,
|
||||
}
|
||||
: id,
|
||||
),
|
||||
) ?? false;
|
||||
return (
|
||||
<TableRow
|
||||
key={index}
|
||||
className={
|
||||
"h-10 " +
|
||||
((templateParam === "code" &&
|
||||
type(templateParam) === "code") ||
|
||||
(templateParam.includes("code") &&
|
||||
myData.node?.template[templateParam].proxy)
|
||||
? " hidden "
|
||||
: "")
|
||||
}
|
||||
>
|
||||
<TableCell className="truncate p-0 text-center text-sm text-foreground sm:px-3">
|
||||
<ShadTooltip
|
||||
styleClasses="z-50"
|
||||
content={
|
||||
myData.node?.template[templateParam].proxy
|
||||
? myData.node?.template[templateParam]
|
||||
.proxy?.id
|
||||
: myData.node?.template[templateParam]
|
||||
.display_name
|
||||
? myData.node!.template[templateParam]
|
||||
.display_name
|
||||
: myData.node?.template[templateParam]
|
||||
.name
|
||||
}
|
||||
>
|
||||
<span>
|
||||
{myData.node?.template[templateParam]
|
||||
.display_name
|
||||
? myData.node!.template[templateParam]
|
||||
.display_name
|
||||
: myData.node?.template[templateParam]
|
||||
.name}
|
||||
</span>
|
||||
</ShadTooltip>
|
||||
</TableCell>
|
||||
<TableCell className="truncate p-0 text-center text-sm text-foreground sm:px-3">
|
||||
<ShadTooltip
|
||||
styleClasses="z-50"
|
||||
content={
|
||||
data.node?.template[templateParam]?.info ??
|
||||
null
|
||||
}
|
||||
>
|
||||
<span>
|
||||
{data.node?.template[templateParam]?.info ??
|
||||
""}
|
||||
</span>
|
||||
</ShadTooltip>
|
||||
</TableCell>
|
||||
<TableCell className="w-[300px] p-0 text-center text-xs text-foreground ">
|
||||
<Case
|
||||
condition={
|
||||
type(templateParam) === "str" &&
|
||||
!myData.node!.template[templateParam]
|
||||
.options
|
||||
}
|
||||
>
|
||||
<div className="mx-auto">
|
||||
{myData.node!.template[templateParam]
|
||||
?.list ? (
|
||||
<InputListComponent
|
||||
componentName={templateParam}
|
||||
editNode={true}
|
||||
disabled={disabled}
|
||||
value={
|
||||
!myData.node!.template[templateParam]
|
||||
.value ||
|
||||
myData.node!.template[templateParam]
|
||||
.value === ""
|
||||
? [""]
|
||||
: myData.node!.template[
|
||||
templateParam
|
||||
].value
|
||||
}
|
||||
onChange={(value: string[]) => {
|
||||
handleOnNewValue(
|
||||
value,
|
||||
templateParam,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
) : myData.node!.template[templateParam]
|
||||
.multiline ? (
|
||||
<TextAreaComponent
|
||||
id={
|
||||
"textarea-edit-" +
|
||||
myData.node!.template[templateParam]
|
||||
.name
|
||||
}
|
||||
data-testid={
|
||||
"textarea-edit-" +
|
||||
myData.node!.template[templateParam]
|
||||
.name
|
||||
}
|
||||
disabled={disabled}
|
||||
editNode={true}
|
||||
value={
|
||||
myData.node!.template[templateParam]
|
||||
.value ?? ""
|
||||
}
|
||||
onChange={(
|
||||
value: string | string[],
|
||||
) => {
|
||||
handleOnNewValue(
|
||||
value,
|
||||
templateParam,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<InputGlobalComponent
|
||||
disabled={disabled}
|
||||
editNode={true}
|
||||
onChange={(value) =>
|
||||
handleOnNewValue(value, templateParam)
|
||||
}
|
||||
setDb={(value) => {
|
||||
setMyData((oldData) => {
|
||||
let newData = cloneDeep(oldData);
|
||||
newData.node!.template[
|
||||
templateParam
|
||||
].load_from_db = value;
|
||||
return newData;
|
||||
});
|
||||
}}
|
||||
name={templateParam}
|
||||
data={myData}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Case>
|
||||
|
||||
<Case
|
||||
condition={
|
||||
type(templateParam) === "NestedDict"
|
||||
}
|
||||
>
|
||||
<div className=" w-full">
|
||||
<DictComponent
|
||||
disabled={disabled}
|
||||
editNode={true}
|
||||
value={
|
||||
myData.node!.template[
|
||||
templateParam
|
||||
]?.value?.toString() === "{}"
|
||||
? {}
|
||||
: myData.node!.template[templateParam]
|
||||
.value
|
||||
}
|
||||
onChange={(newValue) => {
|
||||
myData.node!.template[
|
||||
templateParam
|
||||
].value = newValue;
|
||||
handleOnNewValue(
|
||||
newValue,
|
||||
templateParam,
|
||||
);
|
||||
}}
|
||||
id="editnode-div-dict-input"
|
||||
/>
|
||||
</div>
|
||||
</Case>
|
||||
|
||||
<Case
|
||||
condition={type(templateParam) === "dict"}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
"max-h-48 w-full overflow-auto custom-scroll",
|
||||
myData.node!.template[templateParam].value
|
||||
?.length > 1
|
||||
? "my-3"
|
||||
: "",
|
||||
)}
|
||||
>
|
||||
<KeypairListComponent
|
||||
disabled={disabled}
|
||||
editNode={true}
|
||||
value={
|
||||
myData.node!.template[templateParam]
|
||||
.value?.length === 0 ||
|
||||
!myData.node!.template[templateParam]
|
||||
.value
|
||||
? [{ "": "" }]
|
||||
: convertObjToArray(
|
||||
myData.node!.template[
|
||||
templateParam
|
||||
].value,
|
||||
type(templateParam)!,
|
||||
)
|
||||
}
|
||||
duplicateKey={errorDuplicateKey}
|
||||
onChange={(newValue) => {
|
||||
const valueToNumbers =
|
||||
convertValuesToNumbers(newValue);
|
||||
myData.node!.template[
|
||||
templateParam
|
||||
].value = valueToNumbers;
|
||||
setErrorDuplicateKey(
|
||||
hasDuplicateKeys(valueToNumbers),
|
||||
);
|
||||
handleOnNewValue(
|
||||
valueToNumbers,
|
||||
templateParam,
|
||||
);
|
||||
}}
|
||||
isList={
|
||||
data.node?.template[templateParam]
|
||||
?.list ?? false
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Case>
|
||||
|
||||
<Case
|
||||
condition={type(templateParam) === "bool"}
|
||||
>
|
||||
<div className="ml-auto">
|
||||
{" "}
|
||||
<ToggleShadComponent
|
||||
id={
|
||||
"toggle-edit-" +
|
||||
myData.node!.template[templateParam]
|
||||
.name
|
||||
}
|
||||
disabled={disabled}
|
||||
enabled={
|
||||
myData.node!.template[templateParam]
|
||||
.value
|
||||
}
|
||||
setEnabled={(isEnabled) => {
|
||||
handleOnNewValue(
|
||||
isEnabled,
|
||||
templateParam,
|
||||
);
|
||||
}}
|
||||
size="small"
|
||||
editNode={true}
|
||||
/>
|
||||
</div>
|
||||
</Case>
|
||||
|
||||
<Case
|
||||
condition={type(templateParam) === "float"}
|
||||
>
|
||||
<div className="mx-auto">
|
||||
<FloatComponent
|
||||
disabled={disabled}
|
||||
editNode={true}
|
||||
rangeSpec={
|
||||
myData.node!.template[templateParam]
|
||||
.rangeSpec
|
||||
}
|
||||
value={
|
||||
myData.node!.template[templateParam]
|
||||
.value ?? ""
|
||||
}
|
||||
onChange={(value) => {
|
||||
handleOnNewValue(value, templateParam);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Case>
|
||||
|
||||
<Case
|
||||
condition={
|
||||
type(templateParam) === "str" &&
|
||||
myData.node!.template[templateParam].options
|
||||
}
|
||||
>
|
||||
<div className="mx-auto">
|
||||
<Dropdown
|
||||
editNode={true}
|
||||
options={
|
||||
myData.node!.template[templateParam]
|
||||
.options
|
||||
}
|
||||
onSelect={(value) =>
|
||||
handleOnNewValue(value, templateParam)
|
||||
}
|
||||
value={
|
||||
myData.node!.template[templateParam]
|
||||
.value ?? "Choose an option"
|
||||
}
|
||||
id={
|
||||
"dropdown-edit-" +
|
||||
myData.node!.template[templateParam]
|
||||
.name
|
||||
}
|
||||
></Dropdown>
|
||||
</div>
|
||||
</Case>
|
||||
|
||||
<Case condition={type(templateParam) === "int"}>
|
||||
<div className="mx-auto">
|
||||
<IntComponent
|
||||
rangeSpec={
|
||||
data.node?.template[templateParam]
|
||||
?.rangeSpec
|
||||
}
|
||||
id={
|
||||
"edit-int-input-" +
|
||||
myData.node!.template[templateParam]
|
||||
.name
|
||||
}
|
||||
disabled={disabled}
|
||||
editNode={true}
|
||||
value={
|
||||
myData.node!.template[templateParam]
|
||||
.value ?? ""
|
||||
}
|
||||
onChange={(value) => {
|
||||
handleOnNewValue(value, templateParam);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Case>
|
||||
|
||||
<Case
|
||||
condition={type(templateParam) === "file"}
|
||||
>
|
||||
<div className="mx-auto">
|
||||
<InputFileComponent
|
||||
editNode={true}
|
||||
disabled={disabled}
|
||||
value={
|
||||
myData.node!.template[templateParam]
|
||||
.value ?? ""
|
||||
}
|
||||
onChange={(value: string | string[]) => {
|
||||
handleOnNewValue(value, templateParam);
|
||||
}}
|
||||
fileTypes={
|
||||
myData.node!.template[templateParam]
|
||||
.fileTypes
|
||||
}
|
||||
onFileChange={(filePath: string) => {
|
||||
data.node!.template[
|
||||
templateParam
|
||||
].file_path = filePath;
|
||||
}}
|
||||
></InputFileComponent>
|
||||
</div>
|
||||
</Case>
|
||||
|
||||
<Case
|
||||
condition={type(templateParam) === "prompt"}
|
||||
>
|
||||
<div className="mx-auto">
|
||||
<PromptAreaComponent
|
||||
readonly={
|
||||
myData.node?.flow ? true : false
|
||||
}
|
||||
field_name={templateParam}
|
||||
editNode={true}
|
||||
disabled={disabled}
|
||||
nodeClass={myData.node}
|
||||
setNodeClass={(nodeClass) => {
|
||||
myData.node = nodeClass;
|
||||
}}
|
||||
value={
|
||||
myData.node!.template[templateParam]
|
||||
.value ?? ""
|
||||
}
|
||||
onChange={(value: string | string[]) => {
|
||||
handleOnNewValue(value, templateParam);
|
||||
}}
|
||||
id={
|
||||
"prompt-area-edit-" +
|
||||
myData.node!.template[templateParam]
|
||||
.name
|
||||
}
|
||||
data-testid={
|
||||
"modal-prompt-input-" +
|
||||
myData.node!.template[templateParam]
|
||||
.name
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Case>
|
||||
|
||||
<Case
|
||||
condition={type(templateParam) === "code"}
|
||||
>
|
||||
<div className="mx-auto">
|
||||
<CodeAreaComponent
|
||||
readonly={
|
||||
myData.node?.flow &&
|
||||
myData.node!.template[templateParam]
|
||||
.dynamic
|
||||
? true
|
||||
: false
|
||||
}
|
||||
dynamic={
|
||||
data.node!.template[templateParam]
|
||||
?.dynamic ?? false
|
||||
}
|
||||
setNodeClass={(nodeClass) => {
|
||||
data.node = nodeClass;
|
||||
}}
|
||||
nodeClass={data.node}
|
||||
disabled={disabled}
|
||||
editNode={true}
|
||||
value={
|
||||
myData.node!.template[templateParam]
|
||||
.value ?? ""
|
||||
}
|
||||
onChange={(value: string | string[]) => {
|
||||
handleOnNewValue(value, templateParam);
|
||||
}}
|
||||
id={
|
||||
"code-area-edit" +
|
||||
myData.node!.template[templateParam]
|
||||
.name
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Case>
|
||||
|
||||
<Case condition={type(templateParam) === "Any"}>
|
||||
<>-</>
|
||||
</Case>
|
||||
</TableCell>
|
||||
<TableCell className="p-0 text-right">
|
||||
<div className="items-center text-center">
|
||||
<ToggleShadComponent
|
||||
id={
|
||||
"show" +
|
||||
myData.node?.template[templateParam].name
|
||||
}
|
||||
enabled={
|
||||
!myData.node?.template[templateParam]
|
||||
.advanced
|
||||
}
|
||||
setEnabled={(e) => {
|
||||
changeAdvanced(templateParam);
|
||||
}}
|
||||
disabled={disabled}
|
||||
size="small"
|
||||
editNode={true}
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<TableComponent
|
||||
key={"editNode"}
|
||||
onGridReady={(params) => {
|
||||
setGridApi(params.api);
|
||||
}}
|
||||
tooltipShowDelay={0.5}
|
||||
columnDefs={columnDefs}
|
||||
rowData={rowData}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ const ExportModal = forwardRef(
|
|||
{SAVE_WITH_API_CHECKBOX}
|
||||
</label>
|
||||
</div>
|
||||
<span className=" text-xs text-destructive ">
|
||||
<span className="mt-1 text-xs text-destructive ">
|
||||
{ALERT_SAVE_WITH_API}
|
||||
</span>
|
||||
</BaseModal.Content>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { ColDef, ColGroupDef } from "ag-grid-community";
|
||||
import { AxiosError } from "axios";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import IconComponent from "../../components/genericIconComponent";
|
||||
import TableComponent from "../../components/tableComponent";
|
||||
|
|
@ -9,7 +8,7 @@ import useAlertStore from "../../stores/alertStore";
|
|||
import useFlowStore from "../../stores/flowStore";
|
||||
import useFlowsManagerStore from "../../stores/flowsManagerStore";
|
||||
import { FlowSettingsPropsType } from "../../types/components";
|
||||
import { FlowType, NodeDataType } from "../../types/flow";
|
||||
import { NodeDataType } from "../../types/flow";
|
||||
import BaseModal from "../baseModal";
|
||||
|
||||
export default function FlowLogsModal({
|
||||
|
|
@ -33,11 +32,13 @@ export default function FlowLogsModal({
|
|||
setRows(rows);
|
||||
});
|
||||
} else if (activeTab === "Messages") {
|
||||
getMessagesTable(currentFlowId, "union").then((data) => {
|
||||
const { columns, rows } = data;
|
||||
setColumns(columns.map((col) => ({ ...col, editable: true })));
|
||||
setRows(rows);
|
||||
});
|
||||
getMessagesTable("union", currentFlowId, ["index", "flow_id"]).then(
|
||||
(data) => {
|
||||
const { columns, rows } = data;
|
||||
setColumns(columns.map((col) => ({ ...col, editable: true })));
|
||||
setRows(rows);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (open && activeTab === "Messages" && !noticed.current) {
|
||||
|
|
@ -85,6 +86,7 @@ export default function FlowLogsModal({
|
|||
</TabsList>
|
||||
</Tabs>
|
||||
<TableComponent
|
||||
key={activeTab}
|
||||
readOnlyEdit
|
||||
className="h-max-full h-full w-full"
|
||||
pagination={rows.length === 0 ? false : true}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import InputComponent from "../../../components/inputComponent";
|
||||
import {
|
||||
FormControl,
|
||||
FormField,
|
||||
|
|
|
|||
|
|
@ -7,19 +7,13 @@ import { COPIED_NOTICE_ALERT } from "../../constants/alerts_constants";
|
|||
import { createApiKey } from "../../controllers/API";
|
||||
import useAlertStore from "../../stores/alertStore";
|
||||
import { ApiKeyType } from "../../types/components";
|
||||
import { nodeIconsLucide } from "../../utils/styleUtils";
|
||||
import BaseModal from "../baseModal";
|
||||
|
||||
export default function SecretKeyModal({
|
||||
title,
|
||||
cancelText,
|
||||
confirmationText,
|
||||
children,
|
||||
icon,
|
||||
data,
|
||||
onCloseModal,
|
||||
}: ApiKeyType) {
|
||||
const Icon: any = nodeIconsLucide[icon];
|
||||
const [open, setOpen] = useState(false);
|
||||
const [apiKeyName, setApiKeyName] = useState(data?.apikeyname ?? "");
|
||||
const [apiKeyValue, setApiKeyValue] = useState("");
|
||||
|
|
@ -66,118 +60,91 @@ export default function SecretKeyModal({
|
|||
.catch((err) => {});
|
||||
}
|
||||
|
||||
function handleSubmitForm() {
|
||||
if (!renderKey) {
|
||||
setRenderKey(true);
|
||||
handleAddNewKey();
|
||||
} else {
|
||||
setOpen(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseModal size="small-h-full" open={open} setOpen={setOpen}>
|
||||
<BaseModal
|
||||
onSubmit={handleSubmitForm}
|
||||
size="small-h-full"
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
>
|
||||
<BaseModal.Trigger>{children}</BaseModal.Trigger>
|
||||
<BaseModal.Header description={""}>
|
||||
<span className="pr-2">{title}</span>
|
||||
<Icon
|
||||
name="icon"
|
||||
className="h-6 w-6 pl-1 text-foreground"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</BaseModal.Header>
|
||||
<BaseModal.Content>
|
||||
{renderKey === true && (
|
||||
<>
|
||||
<span className="text-xs">
|
||||
<BaseModal.Header
|
||||
description={
|
||||
renderKey ? (
|
||||
<>
|
||||
{" "}
|
||||
Please save this secret key somewhere safe and accessible. For
|
||||
security reasons,{" "}
|
||||
<strong>you won't be able to view it again</strong> through your
|
||||
account. If you lose this secret key, you'll need to generate a
|
||||
new one.
|
||||
</span>
|
||||
<div className="flex pt-3">
|
||||
</>
|
||||
) : (
|
||||
<>Create a secret API Key to use Langflow API.</>
|
||||
)
|
||||
}
|
||||
>
|
||||
<span className="pr-2">Create API Key</span>
|
||||
<IconComponent
|
||||
name="Key"
|
||||
className="h-6 w-6 pl-1 text-foreground"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</BaseModal.Header>
|
||||
<BaseModal.Content>
|
||||
{renderKey ? (
|
||||
<>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-full">
|
||||
<Input ref={inputRef} readOnly={true} value={apiKeyValue} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button
|
||||
className="ml-3"
|
||||
onClick={() => {
|
||||
handleCopyClick();
|
||||
}}
|
||||
>
|
||||
{textCopied ? (
|
||||
<IconComponent name="Copy" className="h-4 w-4" />
|
||||
) : (
|
||||
<IconComponent name="Check" className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => {
|
||||
handleCopyClick();
|
||||
}}
|
||||
variant="none"
|
||||
size="none"
|
||||
>
|
||||
{textCopied ? (
|
||||
<IconComponent name="Copy" className="h-4 w-4" />
|
||||
) : (
|
||||
<IconComponent name="Check" className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Form.Root
|
||||
onSubmit={(event) => {
|
||||
setRenderKey(true);
|
||||
handleAddNewKey();
|
||||
event.preventDefault();
|
||||
}}
|
||||
>
|
||||
{renderKey === false && (
|
||||
<div className="grid gap-5">
|
||||
<Form.Field name="username">
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "baseline",
|
||||
justifyContent: "space-between",
|
||||
) : (
|
||||
<Form.Field name="apikey">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<Form.Control asChild>
|
||||
<Input
|
||||
//fake api key
|
||||
id="primary-input"
|
||||
value={apiKeyName}
|
||||
ref={inputRef}
|
||||
onChange={({ target: { value } }) => {
|
||||
setApiKeyName(value);
|
||||
}}
|
||||
>
|
||||
<Form.Label className="data-[invalid]:label-invalid">
|
||||
Name (optional){" "}
|
||||
</Form.Label>
|
||||
</div>
|
||||
<Form.Control asChild>
|
||||
<input
|
||||
onChange={({ target: { value } }) => {
|
||||
setApiKeyName(value);
|
||||
}}
|
||||
value={apiKeyName}
|
||||
className="primary-input"
|
||||
placeholder="My key name"
|
||||
/>
|
||||
</Form.Control>
|
||||
</Form.Field>
|
||||
placeholder="Insert a name for your API Key"
|
||||
/>
|
||||
</Form.Control>
|
||||
</div>
|
||||
)}
|
||||
{renderKey === false && (
|
||||
<div className="float-right">
|
||||
<Button
|
||||
type="button"
|
||||
className="mr-3"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
{cancelText}
|
||||
</Button>
|
||||
|
||||
<Form.Submit asChild>
|
||||
<Button className="mt-8">{confirmationText}</Button>
|
||||
</Form.Submit>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{renderKey === true && (
|
||||
<div className="float-right">
|
||||
<Button
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
setRenderKey(false);
|
||||
}}
|
||||
className="mt-8"
|
||||
>
|
||||
Done
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Form.Root>
|
||||
</Form.Field>
|
||||
)}
|
||||
</BaseModal.Content>
|
||||
<BaseModal.Footer
|
||||
submit={{ label: renderKey ? "Done" : "Create Secret Key" }}
|
||||
/>
|
||||
</BaseModal>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,291 +0,0 @@
|
|||
import { useContext, useEffect, useRef, useState } from "react";
|
||||
import IconComponent from "../../components/genericIconComponent";
|
||||
import ShadTooltip from "../../components/shadTooltipComponent";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "../../components/ui/table";
|
||||
import { AuthContext } from "../../contexts/authContext";
|
||||
import { deleteApiKey, getApiKey } from "../../controllers/API";
|
||||
import ConfirmationModal from "../../modals/confirmationModal";
|
||||
import SecretKeyModal from "../../modals/secretKeyModal";
|
||||
|
||||
import moment from "moment";
|
||||
import Header from "../../components/headerComponent";
|
||||
import {
|
||||
DEL_KEY_ERROR_ALERT,
|
||||
DEL_KEY_SUCCESS_ALERT,
|
||||
} from "../../constants/alerts_constants";
|
||||
import {
|
||||
API_PAGE_PARAGRAPH_1,
|
||||
API_PAGE_PARAGRAPH_2,
|
||||
API_PAGE_USER_KEYS,
|
||||
LAST_USED_SPAN_1,
|
||||
LAST_USED_SPAN_2,
|
||||
} from "../../constants/constants";
|
||||
import useAlertStore from "../../stores/alertStore";
|
||||
import { ApiKey } from "../../types/components";
|
||||
|
||||
export default function ApiKeysPage() {
|
||||
const [loadingKeys, setLoadingKeys] = useState(true);
|
||||
const setSuccessData = useAlertStore((state) => state.setSuccessData);
|
||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||
const { userData } = useContext(AuthContext);
|
||||
const [userId, setUserId] = useState("");
|
||||
const keysList = useRef([]);
|
||||
|
||||
useEffect(() => {
|
||||
getKeys();
|
||||
}, [userData]);
|
||||
|
||||
function getKeys() {
|
||||
setLoadingKeys(true);
|
||||
if (userData) {
|
||||
getApiKey()
|
||||
.then((keys: [ApiKey]) => {
|
||||
keysList.current = keys["api_keys"];
|
||||
setUserId(keys["user_id"]);
|
||||
setLoadingKeys(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
setLoadingKeys(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function resetFilter() {
|
||||
getKeys();
|
||||
}
|
||||
|
||||
function handleDeleteKey(keys) {
|
||||
deleteApiKey(keys)
|
||||
.then((res) => {
|
||||
resetFilter();
|
||||
setSuccessData({
|
||||
title: DEL_KEY_SUCCESS_ALERT,
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
setErrorData({
|
||||
title: DEL_KEY_ERROR_ALERT,
|
||||
list: [error["response"]["data"]["detail"]],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function lastUsedMessage() {
|
||||
return (
|
||||
<div className="text-xs">
|
||||
<span>
|
||||
{LAST_USED_SPAN_1}
|
||||
<br></br> {LAST_USED_SPAN_2}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header></Header>
|
||||
{userData && (
|
||||
<div className="main-page-panel">
|
||||
<div className="m-auto flex h-full flex-row justify-center">
|
||||
<div className="basis-5/6">
|
||||
<div className="m-auto flex h-full flex-col space-y-8 p-8 ">
|
||||
<div className="flex items-center justify-between space-y-2">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold tracking-tight">
|
||||
API keys
|
||||
</h2>
|
||||
<p className="text-muted-foreground">
|
||||
{API_PAGE_PARAGRAPH_1}
|
||||
<br />
|
||||
{API_PAGE_PARAGRAPH_2}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2"></div>
|
||||
</div>
|
||||
|
||||
{keysList.current &&
|
||||
keysList.current.length === 0 &&
|
||||
!loadingKeys && (
|
||||
<>
|
||||
<div className="flex items-center justify-between">
|
||||
<h2>{API_PAGE_USER_KEYS}</h2>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<>
|
||||
{loadingKeys && (
|
||||
<div>
|
||||
<strong>Loading...</strong>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={
|
||||
"max-h-[15rem] overflow-scroll overflow-x-hidden rounded-md border-2 bg-muted custom-scroll" +
|
||||
(loadingKeys ? " border-0" : "")
|
||||
}
|
||||
>
|
||||
{keysList.current &&
|
||||
keysList.current.length > 0 &&
|
||||
!loadingKeys && (
|
||||
<Table className={"table-fixed bg-muted outline-1"}>
|
||||
<TableHeader
|
||||
className={
|
||||
loadingKeys
|
||||
? "hidden"
|
||||
: "table-fixed bg-muted outline-1"
|
||||
}
|
||||
>
|
||||
<TableRow>
|
||||
<TableHead className="h-10">Name</TableHead>
|
||||
<TableHead className="h-10">Key</TableHead>
|
||||
<TableHead className="h-10">Created</TableHead>
|
||||
<TableHead className="flex h-10 items-center">
|
||||
Last Used
|
||||
<ShadTooltip
|
||||
side="top"
|
||||
content={lastUsedMessage()}
|
||||
>
|
||||
<div>
|
||||
<IconComponent
|
||||
name="Info"
|
||||
className="ml-1 h-3 w-3"
|
||||
/>
|
||||
</div>
|
||||
</ShadTooltip>
|
||||
</TableHead>
|
||||
<TableHead className="h-10">Total Uses</TableHead>
|
||||
<TableHead className="h-10 w-[100px] text-right"></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
{!loadingKeys && (
|
||||
<TableBody>
|
||||
{keysList.current.map(
|
||||
(api_keys: ApiKey, index: number) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell className="truncate py-2">
|
||||
<ShadTooltip content={api_keys.name}>
|
||||
<span className="cursor-default">
|
||||
{api_keys.name ? api_keys.name : "-"}
|
||||
</span>
|
||||
</ShadTooltip>
|
||||
</TableCell>
|
||||
<TableCell className="truncate py-2">
|
||||
<span className="cursor-default">
|
||||
{api_keys.api_key}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="truncate py-2 ">
|
||||
<ShadTooltip
|
||||
side="top"
|
||||
content={moment(
|
||||
api_keys.created_at
|
||||
).format("YYYY-MM-DD HH:mm")}
|
||||
>
|
||||
<div>
|
||||
{moment(api_keys.created_at).format(
|
||||
"YYYY-MM-DD HH:mm"
|
||||
)}
|
||||
</div>
|
||||
</ShadTooltip>
|
||||
</TableCell>
|
||||
<TableCell className="truncate py-2">
|
||||
<ShadTooltip
|
||||
side="top"
|
||||
content={
|
||||
moment(api_keys.last_used_at).format(
|
||||
"YYYY-MM-DD HH:mm"
|
||||
) === "Invalid date"
|
||||
? "Never"
|
||||
: moment(
|
||||
api_keys.last_used_at
|
||||
).format("YYYY-MM-DD HH:mm")
|
||||
}
|
||||
>
|
||||
<div>
|
||||
{moment(api_keys.last_used_at).format(
|
||||
"YYYY-MM-DD HH:mm"
|
||||
) === "Invalid date"
|
||||
? "Never"
|
||||
: moment(
|
||||
api_keys.last_used_at
|
||||
).format("YYYY-MM-DD HH:mm")}
|
||||
</div>
|
||||
</ShadTooltip>
|
||||
</TableCell>
|
||||
<TableCell className="truncate py-2">
|
||||
{api_keys.total_uses}
|
||||
</TableCell>
|
||||
<TableCell className="flex w-[100px] py-2 text-right">
|
||||
<div className="flex">
|
||||
<ConfirmationModal
|
||||
title="Delete"
|
||||
titleHeader="Delete User"
|
||||
modalContentTitle="Attention!"
|
||||
cancelText="Cancel"
|
||||
confirmationText="Delete"
|
||||
icon={"UserMinus2"}
|
||||
data={api_keys.id}
|
||||
index={index}
|
||||
onConfirm={(index, keys) => {
|
||||
handleDeleteKey(keys);
|
||||
}}
|
||||
>
|
||||
<ConfirmationModal.Content>
|
||||
<span>
|
||||
Are you sure you want to delete
|
||||
this key? This action cannot be
|
||||
undone.
|
||||
</span>
|
||||
</ConfirmationModal.Content>
|
||||
<ConfirmationModal.Trigger>
|
||||
<IconComponent
|
||||
name="Trash2"
|
||||
className="ml-2 h-4 w-4 cursor-pointer"
|
||||
/>
|
||||
</ConfirmationModal.Trigger>
|
||||
</ConfirmationModal>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
)}
|
||||
</TableBody>
|
||||
)}
|
||||
</Table>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<SecretKeyModal
|
||||
title="Create new secret key"
|
||||
cancelText="Cancel"
|
||||
confirmationText="Create secret key"
|
||||
icon={"Key"}
|
||||
data={userId}
|
||||
onCloseModal={getKeys}
|
||||
>
|
||||
<Button>
|
||||
<IconComponent name="Plus" className="mr-1 h-5 w-5" />
|
||||
Create new secret key
|
||||
</Button>
|
||||
</SecretKeyModal>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -19,13 +19,13 @@ import ReactFlow, {
|
|||
SelectionDragHandler,
|
||||
updateEdge,
|
||||
} from "reactflow";
|
||||
import GenericNode from "../../../../CustomNodes/GenericNode";
|
||||
import {
|
||||
INVALID_SELECTION_ERROR_ALERT,
|
||||
UPLOAD_ALERT_LIST,
|
||||
UPLOAD_ERROR_ALERT,
|
||||
WRONG_FILE_ERROR_ALERT,
|
||||
} from "../../../../constants/alerts_constants";
|
||||
import GenericNode from "../../../../customNodes/genericNode";
|
||||
import useAlertStore from "../../../../stores/alertStore";
|
||||
import useFlowStore from "../../../../stores/flowStore";
|
||||
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
|
||||
|
|
@ -45,8 +45,8 @@ import {
|
|||
} from "../../../../utils/reactflowUtils";
|
||||
import ConnectionLineComponent from "../ConnectionLineComponent";
|
||||
import SelectionMenu from "../SelectionMenuComponent";
|
||||
import isWrappedWithClass from "./utils/is-wrapped-with-class";
|
||||
import getRandomName from "./utils/get-random-name";
|
||||
import isWrappedWithClass from "./utils/is-wrapped-with-class";
|
||||
|
||||
const nodeTypes = {
|
||||
genericNode: GenericNode,
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue