Merge cz/mergeAll into shortcuts_settings

This commit is contained in:
igorrCarvalho 2024-06-07 17:18:40 -03:00
commit e531e36ad1
256 changed files with 19933 additions and 2731 deletions

13972
src/frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -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",

View file

@ -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,

View file

@ -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;
}

View file

@ -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";

View file

@ -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
/>
);
}

View file

@ -0,0 +1,4 @@
export const convertToTableRows = (obj: Object) => {
const tokensArray = [Object.values(obj)[0]];
return tokensArray;
};

View file

@ -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>
</>
);
}

View file

@ -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>
);
}

View file

@ -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>
);

View file

@ -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>

View 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;
}

View file

@ -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 "";
}
};

View file

@ -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;

View 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;

View 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;

View 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;

View file

@ -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;

View file

@ -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;

View file

@ -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>

View file

@ -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">

View file

@ -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>

View file

@ -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,

View file

@ -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,

View file

@ -115,6 +115,7 @@ function CsvOutputComponent({
style={{ height: "100%", width: "100%" }}
>
<TableComponent
key={"csv-output"}
rowData={rowData}
columnDefs={colDefs}
defaultColDef={defaultColDef}

View file

@ -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 (
<>

View file

@ -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"

View file

@ -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>
);
};

View file

@ -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>
);
};

View file

@ -108,6 +108,7 @@ export default function InputComponent({
setSelectedOptions={setSelectedOptions}
options={objectOptions}
value={value}
editNode={editNode}
autoFocus={autoFocus}
disabled={disabled}
setShowOptions={setShowOptions}

View file

@ -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";

View file

@ -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) => {

View file

@ -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}

View file

@ -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")}

View file

@ -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: {

View file

@ -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) => {

View file

@ -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}
/>
)

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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>
);

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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>
);

View file

@ -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;
}
}
}

View file

@ -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>
);
}

View file

@ -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}
>

View file

@ -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"}
/>

View 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 };

View file

@ -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!";

View file

@ -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";

View file

@ -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);
}

View file

@ -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}
>

View 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>
);
}

View 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} />;
},
);

View file

@ -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>
</>
);
}

View file

@ -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
/>

View file

@ -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;

View file

@ -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}
/>
);
}

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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"
}`;
};

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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>
)}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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" : ""
}`;
}

View file

@ -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;
}
}

View file

@ -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>
);
}

View file

@ -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;
}

View file

@ -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>

View file

@ -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>
)}

View file

@ -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 = [

View file

@ -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 };

View file

@ -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>
)}
</>
)}

View file

@ -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;

View 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;

View file

@ -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>

View file

@ -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>

View file

@ -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}

View file

@ -1,5 +1,4 @@
import { useEffect, useState } from "react";
import InputComponent from "../../../components/inputComponent";
import {
FormControl,
FormField,

View file

@ -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>
);
}

View file

@ -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>
)}
</>
);
}

View file

@ -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