Merge branch 'dev' into saveComponent

This commit is contained in:
anovazzi1 2023-10-11 16:13:11 -03:00
commit f0c64112ea
249 changed files with 13454 additions and 5022 deletions

View file

@ -22,5 +22,5 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*
/test-results/
/playwright-report/
/playwright-report/*/
/playwright/.cache/

View file

@ -1,10 +1,16 @@
FROM node:14-alpine as frontend_build
ARG BACKEND
FROM node:20-alpine as frontend_build
ARG BACKEND_URL
WORKDIR /app
COPY . /app
COPY ./package.json ./package-lock.json ./tsconfig.json ./vite.config.ts ./index.html ./tailwind.config.js ./postcss.config.js ./prettier.config.js /app/
RUN npm install
COPY ./src /app/src
RUN npm run build
FROM nginx
COPY --from=frontend_build /app/build/ /usr/share/nginx/html
COPY /nginx.conf /etc/nginx/conf.d/default.conf
COPY /nginx.conf /etc/nginx/conf.d/default.conf
COPY start-nginx.sh /start-nginx.sh
RUN chmod +x /start-nginx.sh
ENV BACKEND_URL=$BACKEND_URL
CMD ["/start-nginx.sh"]

View file

@ -1,18 +1,20 @@
server {
gzip on;
gzip_comp_level 2;
gzip_min_length 1000;
gzip_types text/xml text/css;
gzip_http_version 1.1;
gzip_vary on;
gzip_disable "MSIE [4-6] \.";
gzip on;
gzip_comp_level 2;
gzip_min_length 1000;
gzip_types text/xml text/css;
gzip_http_version 1.1;
gzip_vary on;
gzip_disable "MSIE [4-6] \.";
listen 80;
listen 80;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html =404;
}
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html =404;
}
include /etc/nginx/extra-conf.d/*.conf;
}

File diff suppressed because it is too large Load diff

View file

@ -54,7 +54,7 @@
"react-tabs": "^6.0.2",
"react-tooltip": "^5.21.1",
"react18-json-view": "^0.2.3",
"reactflow": "^11.8.3",
"reactflow": "^11.9.2",
"rehype-mathjax": "^4.0.3",
"remark-gfm": "^3.0.1",
"remark-math": "^5.1.1",
@ -73,7 +73,7 @@
"start": "vite",
"build": "vite build",
"serve": "vite preview",
"format": "npx prettier --write \"**/*.{js,jsx,ts,tsx,json,md}\"",
"format": "npx prettier --write \"./**/*.{js,jsx,ts,tsx,json,md}\"",
"type-check": "tsc --noEmit --pretty --project tsconfig.json && vite"
},
"eslintConfig": {

View file

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<ul>
<li>
<a href="./e2e/index.html">e2e report</a>
</li>
<li>
<a href="./onlyFront/index.html">frontEnd Only report</a>
</li>
</ul>
</body>
</html>

View file

@ -20,11 +20,13 @@ export default defineConfig({
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html",
reporter: [
["html", { open: "never", outputFolder: "playwright-report/test-results" }],
],
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://127.0.0.1:3000',
// baseURL: "http://127.0.0.1:3000",
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
@ -69,9 +71,16 @@ export default defineConfig({
],
/* Run your local dev server before starting the tests */
// webServer: {
// command: 'npm run start',
// url: 'http://127.0.0.1:3000',
// reuseExistingServer: !process.env.CI,
// },
// webServer: [
// {
// command: "npm run backend",
// reuseExistingServer: !process.env.CI,
// timeout: 120 * 1000,
// },
// {
// command: "npm run start",
// url: "http://127.0.0.1:3000",
// reuseExistingServer: !process.env.CI,
// },
// ],
});

75
src/frontend/run-tests.sh Executable file
View file

@ -0,0 +1,75 @@
#!/bin/bash
# Default value for the --ui flag
ui=false
# Parse command-line arguments
while [[ $# -gt 0 ]]; do
key="$1"
case $key in
--ui)
ui=true
shift
;;
*)
echo "Unknown option: $key"
exit 1
;;
esac
shift
done
# Function to forcibly terminate a process by port
terminate_process_by_port() {
port="$1"
echo "Terminating process on port: $port"
fuser -k -n tcp "$port" # Forcefully terminate processes using the specified port
echo "Process terminated."
}
# Trap signals to ensure cleanup on script termination
trap 'terminate_process_by_port 7860; terminate_process_by_port 3000' EXIT
# Navigate to the project root directory (where the Makefile is located)
cd ../../
# Start the frontend using 'make frontend' in the background
make frontend &
# Give some time for the frontend to start (adjust sleep duration as needed)
sleep 10
# Navigate to the test directory
cd src/frontend
# Run frontend only Playwright tests with or without UI based on the --ui flag
if [ "$ui" = true ]; then
PLAYWRIGHT_HTML_REPORT=playwright-report/onlyFront npx playwright test tests/onlyFront --ui --project=chromium
else
PLAYWRIGHT_HTML_REPORT=playwright-report/onlyFront npx playwright test tests/onlyFront --project=chromium
fi
# Navigate back to the project root directory
cd ../../
# Start the backend using 'make backend' in the background
make backend &
# Give some time for the backend to start (adjust sleep duration as needed)
sleep 10
# Navigate back to the test directory
cd src/frontend
# Run Playwright tests with or without UI based on the --ui flag
if [ "$ui" = true ]; then
PLAYWRIGHT_HTML_REPORT=playwright-report/e2e npx playwright test tests/end-to-end --ui --project=chromium
else
PLAYWRIGHT_HTML_REPORT=playwright-report/e2e npx playwright test tests/end-to-end --project=chromium
fi
npx playwright show-report
# After the tests are finished, you can add cleanup or teardown logic here if needed
# The trap will automatically terminate processes by port on script exit

View file

@ -32,6 +32,7 @@ import {
convertValuesToNumbers,
hasDuplicateKeys,
isValidConnection,
scapedJSONStringfy,
} from "../../../../utils/reactflowUtils";
import {
nodeColors,
@ -53,6 +54,7 @@ export default function ParameterComponent({
required = false,
optionalHandle = null,
info = "",
proxy,
showNode,
}: ParameterComponentType): JSX.Element {
const ref = useRef<HTMLDivElement>(null);
@ -60,7 +62,7 @@ export default function ParameterComponent({
const infoHtml = useRef<HTMLDivElement & ReactNode>(null);
const updateNodeInternals = useUpdateNodeInternals();
const [position, setPosition] = useState(0);
const { setTabsState, tabId, save, flows } = useContext(TabsContext);
const { setTabsState, tabId, flows } = useContext(TabsContext);
const flow = flows.find((flow) => flow.id === tabId)?.data?.nodes ?? null;
@ -80,8 +82,9 @@ export default function ParameterComponent({
const { reactFlowInstance, setFilterEdge } = useContext(typesContext);
let disabled =
reactFlowInstance?.getEdges().some((edge) => edge.targetHandle === id) ??
false;
reactFlowInstance
?.getEdges()
.some((edge) => edge.targetHandle === scapedJSONStringfy(id)) ?? false;
const { data: myData } = useContext(typesContext);
@ -112,7 +115,6 @@ export default function ParameterComponent({
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
useEffect(() => {
if (name === "openai_api_base") console.log(info);
// @ts-ignore
infoHtml.current = (
<div className="h-full w-full break-words">
@ -136,7 +138,7 @@ export default function ParameterComponent({
nodeIconsLucide[item.family] ?? nodeIconsLucide["unknown"];
return (
<>
<div key={index}>
{index === 0 && (
<span>
{left
@ -183,7 +185,7 @@ export default function ParameterComponent({
</span>
</span>
</span>
</>
</div>
);
});
} else {
@ -204,33 +206,46 @@ export default function ParameterComponent({
type === "code" ||
type === "prompt" ||
type === "file" ||
type === "int") &&
type === "int" ||
type === "dict" ||
type === "NestedDict") &&
!optionalHandle ? (
<></>
) : (
<ShadTooltip
styleClasses={"tooltip-fixed-width custom-scroll nowheel"}
delayDuration={0}
content={refHtml.current}
side={left ? "left" : "right"}
>
<Handle
type={left ? "target" : "source"}
position={left ? Position.Left : Position.Right}
id={id}
isValidConnection={(connection) =>
isValidConnection(connection, reactFlowInstance!)
}
className={classNames(
left ? "my-12 -ml-0.5 " : " my-12 -mr-0.5 ",
"h-3 w-3 rounded-full border-2 bg-background"
)}
style={{
borderColor: color,
top: position,
}}
></Handle>
</ShadTooltip>
<Button className="h-7 truncate bg-muted p-0 text-sm font-normal text-black hover:bg-muted">
<div className="flex">
<ShadTooltip
styleClasses={"tooltip-fixed-width custom-scroll nowheel"}
delayDuration={0}
content={refHtml.current}
side={left ? "left" : "right"}
>
<Handle
type={left ? "target" : "source"}
position={left ? Position.Left : Position.Right}
id={
proxy
? scapedJSONStringfy({ ...id, proxy })
: scapedJSONStringfy(id)
}
isValidConnection={(connection) =>
isValidConnection(connection, reactFlowInstance!)
}
className={classNames(
left ? "my-12 -ml-0.5 " : " my-12 -mr-0.5 ",
"h-3 w-3 rounded-full border-2 bg-background"
)}
style={{
borderColor: color,
top: position,
}}
onClick={() => {
setFilterEdge(groupedEdge.current);
}}
></Handle>
</ShadTooltip>
</div>
</Button>
)
) : (
<div
@ -245,7 +260,13 @@ export default function ParameterComponent({
(info !== "" ? " flex items-center" : "")
}
>
{title}
{proxy ? (
<ShadTooltip content={<span>{proxy.id}</span>}>
<span>{title}</span>
</ShadTooltip>
) : (
title
)}
<span className="text-status-red">{required ? " *" : ""}</span>
<div className="">
{info !== "" && (
@ -268,9 +289,9 @@ export default function ParameterComponent({
type === "code" ||
type === "prompt" ||
type === "file" ||
type === "int" ||
type === "dict" ||
type === "NestedDict" ||
type === "int") &&
type === "NestedDict") &&
!optionalHandle ? (
<></>
) : (
@ -285,7 +306,11 @@ export default function ParameterComponent({
<Handle
type={left ? "target" : "source"}
position={left ? Position.Left : Position.Right}
id={id}
id={
proxy
? scapedJSONStringfy({ ...id, proxy })
: scapedJSONStringfy(id)
}
isValidConnection={(connection) =>
isValidConnection(connection, reactFlowInstance!)
}
@ -368,6 +393,11 @@ export default function ParameterComponent({
) : left === true && type === "code" ? (
<div className="mt-2 w-full">
<CodeAreaComponent
readonly={
data.node?.flow && data.node.template[name].dynamic
? true
: false
}
dynamic={data.node?.template[name].dynamic ?? false}
setNodeClass={(nodeClass) => {
data.node = nodeClass;
@ -388,7 +418,6 @@ export default function ParameterComponent({
suffixes={data.node?.template[name].suffixes}
onFileChange={(filePath: string) => {
data.node!.template[name].file_path = filePath;
save();
}}
></InputFileComponent>
</div>
@ -403,6 +432,11 @@ export default function ParameterComponent({
) : left === true && type === "prompt" ? (
<div className="mt-2 w-full">
<PromptAreaComponent
readonly={
data.node?.flow && data.node.template[name].dynamic
? true
: false
}
field_name={name}
setNodeClass={(nodeClass) => {
data.node = nodeClass;
@ -424,9 +458,12 @@ export default function ParameterComponent({
disabled={disabled}
editNode={false}
value={
data.node!.template[name].value ?? {
yourkey: "value",
}
!data.node!.template[name].value ||
data.node!.template[name].value?.toString() === "{}"
? {
yourkey: "value",
}
: data.node!.template[name].value
}
onChange={(newValue) => {
data.node!.template[name].value = newValue;

View file

@ -4,23 +4,33 @@ import { NodeToolbar, useUpdateNodeInternals } from "reactflow";
import ShadTooltip from "../../components/ShadTooltipComponent";
import Tooltip from "../../components/TooltipComponent";
import IconComponent from "../../components/genericIconComponent";
import InputComponent from "../../components/inputComponent";
import { Textarea } from "../../components/ui/textarea";
import { useSSE } from "../../contexts/SSEContext";
import { TabsContext } from "../../contexts/tabsContext";
import { typesContext } from "../../contexts/typesContext";
import NodeToolbarComponent from "../../pages/FlowPage/components/nodeToolbarComponent";
import { validationStatusType } from "../../types/components";
import { NodeDataType } from "../../types/flow";
import { cleanEdges } from "../../utils/reactflowUtils";
import {
cleanEdges,
handleKeyDown,
scapedJSONStringfy,
} from "../../utils/reactflowUtils";
import { nodeColors, nodeIconsLucide } from "../../utils/styleUtils";
import { classNames, toTitleCase } from "../../utils/utils";
import ParameterComponent from "./components/parameterComponent";
export default function GenericNode({
data: olddata,
xPos,
yPos,
selected,
}: {
data: NodeDataType;
selected: boolean;
xPos: number;
yPos: number;
}): JSX.Element {
const [data, setData] = useState(olddata);
const { updateFlow, flows, tabId } = useContext(TabsContext);
@ -28,6 +38,12 @@ export default function GenericNode({
const { types, deleteNode, reactFlowInstance, setFilterEdge, getFilterEdge } =
useContext(typesContext);
const name = nodeIconsLucide[data.type] ? data.type : types[data.type];
const [inputName, setInputName] = useState(true);
const [nodeName, setNodeName] = useState(data.node!.display_name);
const [inputDescription, setInputDescription] = useState(false);
const [nodeDescription, setNodeDescription] = useState(
data.node?.description!
);
const [validationStatus, setValidationStatus] =
useState<validationStatusType | null>(null);
const [showNode, setShowNode] = useState<boolean>(true);
@ -107,11 +123,11 @@ export default function GenericNode({
setValidationStatus(null);
}
}, [sseData, data.id]);
return (
<>
<NodeToolbar>
<NodeToolbarComponent
position={{ x: xPos, y: yPos }}
data={data}
setData={setData}
deleteNode={deleteNode}
@ -152,7 +168,7 @@ export default function GenericNode({
}
>
<IconComponent
name={name}
name={data.node?.flow ? "Ungroup" : name}
className={
"generic-node-icon " +
(!showNode && "absolute inset-x-6 h-12 w-12")
@ -161,11 +177,35 @@ export default function GenericNode({
/>
{showNode && (
<div className="generic-node-tooltip-div">
<ShadTooltip content={data.node?.display_name}>
<div className="generic-node-tooltip-div text-primary">
{data.node?.display_name}
{data.node?.flow && inputName ? (
<div>
<InputComponent
autoFocus
onBlur={() => {
setInputName(false);
if (nodeName.trim() !== "") {
setNodeName(nodeName);
data.node!.display_name = nodeName;
} else {
setNodeName(data.node!.display_name);
}
}}
value={nodeName}
onChange={setNodeName}
password={false}
blurOnEnter={true}
/>
</div>
</ShadTooltip>
) : (
<ShadTooltip content={data.node?.display_name}>
<div
className="generic-node-tooltip-div text-primary"
onDoubleClick={() => setInputName(true)}
>
{data.node?.display_name}
</div>
</ShadTooltip>
)}
</div>
)}
</div>
@ -179,16 +219,14 @@ export default function GenericNode({
data.node!.template[templateField].show &&
!data.node!.template[templateField].advanced && (
<ParameterComponent
key={
(data.node!.template[
templateField
].input_types?.join(";") ??
data.node!.template[templateField].type) +
"|" +
templateField +
"|" +
data.id
}
key={scapedJSONStringfy({
inputTypes:
data.node!.template[templateField].input_types,
type: data.node!.template[templateField].type,
id: data.id,
fieldName: templateField,
proxy: data.node!.template[templateField].proxy,
})}
data={data}
setData={setData}
color={
@ -218,31 +256,31 @@ export default function GenericNode({
data.node?.template[templateField].type
}
required={
data.node?.template[templateField].required
}
id={
(data.node?.template[
templateField
].input_types?.join(";") ??
data.node?.template[templateField].type) +
"|" +
templateField +
"|" +
data.id
data.node!.template[templateField].required
}
id={{
inputTypes:
data.node!.template[templateField].input_types,
type: data.node!.template[templateField].type,
id: data.id,
fieldName: templateField,
}}
left={true}
type={data.node?.template[templateField].type}
optionalHandle={
data.node?.template[templateField].input_types
}
proxy={data.node?.template[templateField].proxy}
showNode={showNode}
/>
)
)}
<ParameterComponent
key={[data.type, data.id, ...data.node!.base_classes].join(
"|"
)}
key={scapedJSONStringfy({
baseClasses: data.node!.base_classes,
id: data.id,
dataType: data.type,
})}
data={data}
setData={setData}
color={nodeColors[types[data.type]] ?? nodeColors.unknown}
@ -253,9 +291,11 @@ export default function GenericNode({
: data.type
}
tooltipTitle={data.node?.base_classes.join("\n")}
id={[data.type, data.id, ...data.node!.base_classes].join(
"|"
)}
id={{
baseClasses: data.node!.base_classes,
id: data.id,
dataType: data.type,
}}
type={data.node?.base_classes.join("|")}
left={false}
showNode={showNode}
@ -331,16 +371,53 @@ export default function GenericNode({
className={
showNode
? "generic-node-desc " +
(data.node?.description !== "" && showNode ? "py-5" : "pb-5")
(data.node?.description !== "" ? "py-5" : "pb-5")
: ""
}
>
{data.node?.description !== "" && showNode && (
<div className="generic-node-desc-text">
{data.node?.description !== "" &&
showNode &&
data.node?.flow &&
inputDescription ? (
<Textarea
autoFocus
onBlur={() => {
setInputDescription(false);
if (nodeDescription.trim() !== "") {
setNodeDescription(nodeDescription);
data.node!.description = nodeDescription;
} else {
setNodeDescription(data.node!.description);
}
}}
value={nodeDescription}
onChange={(e) => setNodeDescription(e.target.value)}
onKeyDown={(e) => {
handleKeyDown(e, nodeDescription, "");
if (
e.key === "Enter" &&
e.shiftKey === false &&
e.ctrlKey === false &&
e.altKey === false
) {
setInputDescription(false);
if (nodeDescription.trim() !== "") {
setNodeDescription(nodeDescription);
data.node!.description = nodeDescription;
} else {
setNodeDescription(data.node!.description);
}
}
}}
/>
) : (
<div
className="generic-node-desc-text"
onDoubleClick={() => setInputDescription(true)}
>
{data.node?.description}
</div>
)}
<>
{Object.keys(data.node!.template)
.filter((templateField) => templateField.charAt(0) !== "_")
@ -349,15 +426,14 @@ export default function GenericNode({
{data.node!.template[templateField].show &&
!data.node!.template[templateField].advanced ? (
<ParameterComponent
key={
(data.node!.template[templateField].input_types?.join(
";"
) ?? data.node!.template[templateField].type) +
"|" +
templateField +
"|" +
data.id
}
key={scapedJSONStringfy({
inputTypes:
data.node!.template[templateField].input_types,
type: data.node!.template[templateField].type,
id: data.id,
fieldName: templateField,
proxy: data.node!.template[templateField].proxy,
})}
data={data}
setData={setData}
color={
@ -385,21 +461,20 @@ export default function GenericNode({
"\n"
) ?? data.node?.template[templateField].type
}
required={data.node?.template[templateField].required}
id={
(data.node?.template[templateField].input_types?.join(
";"
) ?? data.node?.template[templateField].type) +
"|" +
templateField +
"|" +
data.id
}
required={data.node!.template[templateField].required}
id={{
inputTypes:
data.node!.template[templateField].input_types,
type: data.node!.template[templateField].type,
id: data.id,
fieldName: templateField,
}}
left={true}
type={data.node?.template[templateField].type}
optionalHandle={
data.node?.template[templateField].input_types
}
proxy={data.node?.template[templateField].proxy}
showNode={showNode}
/>
) : (
@ -416,7 +491,11 @@ export default function GenericNode({
{" "}
</div>
<ParameterComponent
key={[data.type, data.id, ...data.node!.base_classes].join("|")}
key={scapedJSONStringfy({
baseClasses: data.node!.base_classes,
id: data.id,
dataType: data.type,
})}
data={data}
setData={setData}
color={nodeColors[types[data.type]] ?? nodeColors.unknown}
@ -426,7 +505,11 @@ export default function GenericNode({
: data.type
}
tooltipTitle={data.node?.base_classes.join("\n")}
id={[data.type, data.id, ...data.node!.base_classes].join("|")}
id={{
baseClasses: data.node!.base_classes,
id: data.id,
dataType: data.type,
}}
type={data.node?.base_classes.join("|")}
left={false}
showNode={showNode}

View file

@ -21,6 +21,7 @@ export default function DropdownButton({
<DropdownMenu open={showOptions}>
<DropdownMenuTrigger asChild>
<Button
id="new-project-btn"
variant="primary"
className="relative pr-10"
onClick={(event) => {

View file

@ -67,12 +67,12 @@ export const EditFlowSettings: React.FC<InputProps> = ({
)}
</div>
<Input
className="nopan nodrag noundo nocopy mt-2 font-normal"
className="nopan nodelete nodrag noundo nocopy mt-2 font-normal"
onChange={handleNameChange}
type="text"
name="name"
value={name ?? ""}
placeholder="File name"
placeholder="Flow name"
id="name"
maxLength={maxLength}
/>
@ -86,7 +86,7 @@ export const EditFlowSettings: React.FC<InputProps> = ({
name="description"
id="description"
onChange={handleDescriptionChange}
value={description}
value={description!}
placeholder="Flow description"
className="mt-2 max-h-[100px] font-normal"
rows={3}

View file

@ -37,7 +37,10 @@ export default function BuildTrigger({
if (isBuilding) {
return;
}
const errors = validateNodes(reactFlowInstance!);
const errors = validateNodes(
reactFlowInstance!.getNodes(),
reactFlowInstance!.getEdges()
);
if (errors.length > 0) {
setErrorData({
title: "Oops! Looks like you missed something",
@ -77,61 +80,54 @@ export default function BuildTrigger({
const { flowId } = response.data;
// Step 2: Use the session ID to establish an SSE connection using EventSource
let validationResults: boolean[] = [];
let finished = false;
const apiUrl = `/api/v1/build/stream/${flowId}`;
const eventSource = new EventSource(apiUrl);
return new Promise<boolean>((resolve, reject) => {
const eventSource = new EventSource(apiUrl);
eventSource.onmessage = (event) => {
// If the event is parseable, return
if (!event.data) {
return;
}
const parsedData = JSON.parse(event.data);
// if the event is the end of the stream, close the connection
if (parsedData.end_of_stream) {
// Close the connection and finish
finished = true;
eventSource.onmessage = (event) => {
// If the event is parseable, return
if (!event.data) {
return;
}
const parsedData = JSON.parse(event.data);
// if the event is the end of the stream, close the connection
if (parsedData.end_of_stream) {
eventSource.close();
resolve(validationResults.every((result) => result));
} else if (parsedData.log) {
// If the event is a log, log it
setSuccessData({ title: parsedData.log });
} else if (parsedData.input_keys !== undefined) {
//@ts-ignore
setTabsState((old: TabsState) => {
return {
...old,
[flowId]: {
...old[flowId],
formKeysData: parsedData,
},
};
});
} else {
// Otherwise, process the data
const isValid = processStreamResult(parsedData);
setProgress(parsedData.progress);
validationResults.push(isValid);
}
};
eventSource.onerror = (error: any) => {
console.error("EventSource failed:", error);
if (error.data) {
const parsedData = JSON.parse(error.data);
setErrorData({ title: parsedData.error });
setIsBuilding(false);
}
eventSource.close();
return;
} else if (parsedData.log) {
// If the event is a log, log it
setSuccessData({ title: parsedData.log });
} else if (parsedData.input_keys !== undefined) {
//@ts-ignore
setTabsState((old: TabsState) => {
return {
...old,
[flowId]: {
...old[flowId],
formKeysData: parsedData,
},
};
});
} else {
// Otherwise, process the data
const isValid = processStreamResult(parsedData);
setProgress(parsedData.progress);
validationResults.push(isValid);
}
};
eventSource.onerror = (error: any) => {
console.error("EventSource failed:", error);
eventSource.close();
if (error.data) {
const parsedData = JSON.parse(error.data);
setErrorData({ title: parsedData.error });
setIsBuilding(false);
}
};
// Step 3: Wait for the stream to finish
while (!finished) {
await new Promise((resolve) => setTimeout(resolve, 100));
finished = validationResults.length === flow.data!.nodes.length;
}
// Step 4: Return true if all nodes are valid, false otherwise
return validationResults.every((result) => result);
reject(new Error("Streaming failed"));
};
});
}
function processStreamResult(parsedData: parsedDataType) {

View file

@ -12,6 +12,7 @@ export default function CodeAreaComponent({
nodeClass,
dynamic,
setNodeClass,
readonly = false,
}: CodeAreaComponentType) {
const [myValue, setMyValue] = useState(
typeof value == "string" ? value : JSON.stringify(value)
@ -30,6 +31,7 @@ export default function CodeAreaComponent({
return (
<div className={disabled ? "pointer-events-none w-full " : " w-full"}>
<CodeAreaModal
readonly={readonly}
dynamic={dynamic}
value={myValue}
nodeClass={nodeClass}

View file

@ -3,7 +3,6 @@ import { useContext, useEffect, useState } from "react";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { oneDark } from "react-syntax-highlighter/dist/cjs/styles/prism";
import AccordionComponent from "../../components/AccordionComponent";
import ShadTooltip from "../../components/ShadTooltipComponent";
import CodeAreaComponent from "../../components/codeAreaComponent";
import Dropdown from "../../components/dropdownComponent";
import FloatComponent from "../../components/floatComponent";
@ -31,9 +30,16 @@ import {
import { darkContext } from "../../contexts/darkContext";
import { typesContext } from "../../contexts/typesContext";
import { codeTabsPropsType } from "../../types/components";
import { unselectAllNodes } from "../../utils/reactflowUtils";
import {
convertObjToArray,
convertValuesToNumbers,
hasDuplicateKeys,
unselectAllNodes,
} from "../../utils/reactflowUtils";
import { classNames } from "../../utils/utils";
import DictComponent from "../dictComponent";
import IconComponent from "../genericIconComponent";
import KeypairListComponent from "../keypairListComponent";
export default function CodeTabsComponent({
flow,
@ -48,6 +54,7 @@ export default function CodeTabsComponent({
const [openAccordion, setOpenAccordion] = useState<string[]>([]);
const { dark } = useContext(darkContext);
const { reactFlowInstance } = useContext(typesContext);
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
useEffect(() => {
if (flow && flow["data"]!["nodes"]) {
@ -56,7 +63,7 @@ export default function CodeTabsComponent({
}, [flow]);
useEffect(() => {
if (tweaks) {
if (tweaks && data) {
unselectAllNodes({
data,
updateNodes: (nodes) => {
@ -248,7 +255,11 @@ export default function CodeTabsComponent({
node.data.node.template[templateField]
.type === "file" ||
node.data.node.template[templateField]
.type === "int")
.type === "int" ||
node.data.node.template[templateField]
.type === "dict" ||
node.data.node.template[templateField]
.type === "NestedDict")
)
.map((templateField, indx) => {
return (
@ -310,55 +321,46 @@ export default function CodeTabsComponent({
) : node.data.node.template[
templateField
].multiline ? (
<ShadTooltip
content={tweaks.buildContent!(
node.data.node.template[
templateField
].value
)}
>
<div>
<TextAreaComponent
disabled={false}
editNode={true}
value={
!node.data.node
.template[
<div>
<TextAreaComponent
disabled={false}
editNode={true}
value={
!node.data.node
.template[
templateField
].value ||
node.data.node.template[
templateField
].value === ""
? ""
: node.data.node
.template[
templateField
].value
}
onChange={(target) => {
setData((old) => {
let newInputList =
cloneDeep(old);
newInputList![
i
].data.node.template[
templateField
].value ||
].value = target;
return newInputList;
});
tweaks.buildTweakObject!(
node["data"]["id"],
target,
node.data.node
.template[
templateField
].value === ""
? ""
: node.data.node
.template[
templateField
].value
}
onChange={(target) => {
setData((old) => {
let newInputList =
cloneDeep(old);
newInputList![
i
].data.node.template[
templateField
].value = target;
return newInputList;
});
tweaks.buildTweakObject!(
node["data"]["id"],
target,
node.data.node
.template[
templateField
]
);
}}
/>
</div>
</ShadTooltip>
]
);
}}
/>
</div>
) : (
<InputComponent
editNode={true}
@ -440,52 +442,35 @@ export default function CodeTabsComponent({
) : node.data.node.template[
templateField
].type === "file" ? (
<ShadTooltip
content={tweaks.buildContent!(
!node.data.node.template[
templateField
].value ||
<div className="mx-auto">
<InputFileComponent
editNode={true}
disabled={false}
value={
node.data.node.template[
templateField
].value === ""
? ""
: node.data.node.template[
templateField
].value
)}
>
<div className="mx-auto">
<InputFileComponent
editNode={true}
disabled={false}
value={
node.data.node.template[
templateField
].value ?? ""
}
onChange={(
target: any
) => {}}
fileTypes={
node.data.node.template[
templateField
].fileTypes
}
suffixes={
node.data.node.template[
templateField
].suffixes
}
onFileChange={(
value: any
) => {
node.data.node.template[
templateField
].file_path = value;
}}
></InputFileComponent>
</div>
</ShadTooltip>
].value ?? ""
}
onChange={(target: any) => {}}
fileTypes={
node.data.node.template[
templateField
].fileTypes
}
suffixes={
node.data.node.template[
templateField
].suffixes
}
onFileChange={(
value: any
) => {
node.data.node.template[
templateField
].file_path = value;
}}
></InputFileComponent>
</div>
) : node.data.node.template[
templateField
].type === "float" ? (
@ -617,114 +602,197 @@ export default function CodeTabsComponent({
) : node.data.node.template[
templateField
].type === "prompt" ? (
<ShadTooltip
content={tweaks.buildContent!(
!node.data.node.template[
templateField
].value ||
<div className="mx-auto">
<PromptAreaComponent
readonly={
node.data.node?.flow &&
node.data.node.template[
templateField
].dynamic
? true
: false
}
editNode={true}
disabled={false}
value={
!node.data.node.template[
templateField
].value ||
node.data.node.template[
templateField
].value === ""
? ""
: node.data.node.template[
? ""
: node.data.node.template[
templateField
].value
}
onChange={(target) => {
setData((old) => {
let newInputList =
cloneDeep(old);
newInputList![
i
].data.node.template[
templateField
].value
)}
>
<div className="mx-auto">
<PromptAreaComponent
editNode={true}
disabled={false}
value={
!node.data.node.template[
templateField
].value ||
].value = target;
return newInputList;
});
tweaks.buildTweakObject!(
node["data"]["id"],
target,
node.data.node.template[
templateField
].value === ""
? ""
: node.data.node
.template[
templateField
].value
}
onChange={(target) => {
setData((old) => {
let newInputList =
cloneDeep(old);
newInputList![
i
].data.node.template[
templateField
].value = target;
return newInputList;
});
tweaks.buildTweakObject!(
node["data"]["id"],
target,
node.data.node.template[
templateField
]
);
}}
/>
</div>
</ShadTooltip>
]
);
}}
/>
</div>
) : node.data.node.template[
templateField
].type === "code" ? (
<ShadTooltip
content={tweaks.buildContent!(
tweaks.getValue!(
<div className="mx-auto">
<CodeAreaComponent
disabled={false}
editNode={true}
readonly={
node.data.node?.flow &&
node.data.node.template[
templateField
].value,
node.data,
].dynamic
? true
: false
}
value={
!node.data.node.template[
templateField
].value ||
node.data.node.template[
templateField
]
)
)}
>
<div className="mx-auto">
<CodeAreaComponent
disabled={false}
editNode={true}
value={
!node.data.node.template[
].value === ""
? ""
: node.data.node.template[
templateField
].value
}
onChange={(target) => {
setData((old) => {
let newInputList =
cloneDeep(old);
newInputList![
i
].data.node.template[
templateField
].value ||
].value = target;
return newInputList;
});
tweaks.buildTweakObject!(
node["data"]["id"],
target,
node.data.node.template[
templateField
].value === ""
? ""
: node.data.node
]
);
}}
/>
</div>
) : node.data.node.template[
templateField
].type === "dict" ? (
<div className="mx-auto overflow-auto custom-scroll">
<KeypairListComponent
disabled={false}
editNode={true}
value={
node.data.node!.template[
templateField
].value?.length === 0 ||
!node.data.node!.template[
templateField
].value
? [{ "": "" }]
: convertObjToArray(
node.data.node!
.template[
templateField
].value
}
onChange={(target) => {
setData((old) => {
let newInputList =
cloneDeep(old);
newInputList![
i
].data.node.template[
templateField
].value = target;
return newInputList;
});
tweaks.buildTweakObject!(
node["data"]["id"],
target,
node.data.node.template[
templateField
]
)
}
duplicateKey={
errorDuplicateKey
}
onChange={(target) => {
const valueToNumbers =
convertValuesToNumbers(
target
);
}}
/>
</div>
</ShadTooltip>
node.data.node!.template[
templateField
].value = valueToNumbers;
setErrorDuplicateKey(
hasDuplicateKeys(
valueToNumbers
)
);
setData((old) => {
let newInputList =
cloneDeep(old);
newInputList![
i
].data.node.template[
templateField
].value = target;
return newInputList;
});
tweaks.buildTweakObject!(
node["data"]["id"],
target,
node.data.node.template[
templateField
]
);
}}
/>
</div>
) : node.data.node.template[
templateField
].type === "NestedDict" ? (
<div className="mx-auto">
<DictComponent
disabled={false}
editNode={true}
value={
node.data.node!.template[
templateField
].value.toString() === "{}"
? {
yourkey: "value",
}
: node.data.node!
.template[
templateField
].value
}
onChange={(target) => {
setData((old) => {
let newInputList =
cloneDeep(old);
newInputList![
i
].data.node.template[
templateField
].value = target;
return newInputList;
});
tweaks.buildTweakObject!(
node["data"]["id"],
target,
node.data.node.template[
templateField
]
);
}}
/>
</div>
) : node.data.node.template[
templateField
].type === "Any" ? (

View file

@ -1,4 +1,4 @@
import { useEffect } from "react";
import { useEffect, useRef } from "react";
import { DictComponentType } from "../../types/components";
import DictAreaModal from "../../modals/dictAreaModal";
@ -13,7 +13,7 @@ export default function DictComponent({
}: DictComponentType): JSX.Element {
useEffect(() => {
if (disabled) {
onChange([""]);
onChange({});
}
}, [disabled]);
@ -21,6 +21,8 @@ export default function DictComponent({
if (value) onChange(value);
}, [value]);
const ref = useRef(value);
debugger;
return (
<div
className={classNames(
@ -31,7 +33,7 @@ export default function DictComponent({
{
<div className="flex w-full gap-3">
<DictAreaModal
value={value}
value={ref.current}
onChange={(obj) => {
onChange(obj);
}}

View file

@ -23,6 +23,7 @@ export default function FloatComponent({
return (
<div className="w-full">
<Input
id="float-input"
type="number"
step={step}
min={min}

View file

@ -26,7 +26,7 @@ export const MenuBar = ({ flows, tabId }: menuBarPropsType): JSX.Element => {
function handleAddFlow() {
try {
addFlow(undefined, true).then((id) => {
addFlow(true).then((id) => {
navigate("/flow/" + id);
});
// saveFlowStyleInDataBase();

View file

@ -1,11 +1,13 @@
import * as Form from "@radix-ui/react-form";
import { useEffect, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { InputComponentType } from "../../types/components";
import { handleKeyDown } from "../../utils/reactflowUtils";
import { classNames } from "../../utils/utils";
import { Input } from "../ui/input";
export default function InputComponent({
autoFocus = false,
onBlur,
value,
onChange,
disabled,
@ -15,9 +17,10 @@ export default function InputComponent({
editNode = false,
placeholder = "Type something...",
className,
blurOnEnter = false,
}: InputComponentType): JSX.Element {
const [pwdVisible, setPwdVisible] = useState(false);
const refInput = useRef<HTMLInputElement>(null);
// Clear component state
useEffect(() => {
if (disabled) {
@ -30,6 +33,9 @@ export default function InputComponent({
{isForm ? (
<Form.Control asChild>
<Input
ref={refInput}
onBlur={onBlur}
autoFocus={autoFocus}
type={password && !pwdVisible ? "password" : "text"}
value={value}
disabled={disabled}
@ -49,13 +55,17 @@ export default function InputComponent({
}}
onKeyDown={(e) => {
handleKeyDown(e, value, "");
if (blurOnEnter && e.key === "Enter") refInput.current?.blur();
}}
/>
</Form.Control>
) : (
<Input
ref={refInput}
type="text"
onBlur={onBlur}
value={value}
autoFocus={autoFocus}
disabled={disabled}
required={required}
className={classNames(
@ -73,6 +83,7 @@ export default function InputComponent({
}}
onKeyDown={(e) => {
handleKeyDown(e, value, "");
if (blurOnEnter && e.key === "Enter") refInput.current?.blur();
}}
/>
)}

View file

@ -1,4 +1,4 @@
import { useEffect } from "react";
import { useEffect, useRef } from "react";
import { KeyPairListComponentType } from "../../types/components";
import _ from "lodash";
@ -12,6 +12,8 @@ export default function KeypairListComponent({
disabled,
editNode = false,
duplicateKey,
advanced = false,
dataValue,
}: KeyPairListComponentType): JSX.Element {
useEffect(() => {
if (disabled) {
@ -19,37 +21,41 @@ export default function KeypairListComponent({
}
}, [disabled]);
const ref = useRef(value.length === 0 ? [{ "": "" }] : value);
useEffect(() => {
if (JSON.stringify(value) !== JSON.stringify(ref.current)) {
ref.current = value;
onChange(value);
}
}, [value]);
const handleChangeKey = (event, idx) => {
const newInputList = _.cloneDeep(value);
const oldKey = Object.keys(newInputList[idx])[0];
const updatedObj = { [event.target.value]: newInputList[idx][oldKey] };
newInputList[idx] = updatedObj;
onChange(newInputList);
const oldKey = Object.keys(ref.current[idx])[0];
const updatedObj = { [event.target.value]: ref.current[idx][oldKey] };
ref.current[idx] = updatedObj;
onChange(ref.current);
};
const handleChangeValue = (newValue, idx) => {
const newInputList = _.cloneDeep(value);
const key = Object.keys(newInputList[idx])[0];
newInputList[idx][key] = newValue;
onChange(newInputList);
const key = Object.keys(ref.current[idx])[0];
ref.current[idx][key] = newValue;
onChange(ref.current);
};
useEffect(() => {
if (value) onChange(value);
}, [value]);
return (
<div
className={classNames(
value?.length > 1 && editNode ? "my-1" : "",
"flex flex-col gap-3"
ref.current?.length > 1 && editNode ? "mx-2 my-1" : "",
"flex h-full flex-col gap-3"
)}
>
{value?.map((obj, index) => {
{ref.current?.map((obj, index) => {
return Object.keys(obj).map((key, idx) => {
return (
<div key={idx} className="flex w-full gap-3">
<div key={idx} className="flex w-full gap-2">
<Input
id={"keypair" + index}
type="text"
value={key.trim()}
className={classNames(
@ -67,6 +73,7 @@ export default function KeypairListComponent({
/>
<Input
id={"keypair" + (index + 100).toString()}
type="text"
value={obj[key]}
className={editNode ? "input-edit-node" : ""}
@ -76,13 +83,14 @@ export default function KeypairListComponent({
}
/>
{index === value.length - 1 ? (
{index === ref.current.length - 1 ? (
<button
onClick={() => {
let newInputList = _.cloneDeep(value);
let newInputList = _.cloneDeep(ref.current);
newInputList.push({ "": "" });
onChange(newInputList);
}}
id={"plusbtn" + index.toString()}
>
<IconComponent
name="Plus"
@ -92,10 +100,11 @@ export default function KeypairListComponent({
) : (
<button
onClick={() => {
let newInputList = _.cloneDeep(value);
let newInputList = _.cloneDeep(ref.current);
newInputList.splice(index, 1);
onChange(newInputList);
}}
id={"minusbtn" + index.toString()}
>
<IconComponent
name="X"

View file

@ -14,7 +14,8 @@ export default function PromptAreaComponent({
onChange,
disabled,
editNode = false,
}: PromptAreaComponentType) {
readonly = false,
}: PromptAreaComponentType): JSX.Element {
useEffect(() => {
if (disabled) {
onChange("");
@ -22,7 +23,7 @@ export default function PromptAreaComponent({
}, [disabled]);
useEffect(() => {
if (value !== "" && !editNode) {
if (value !== "" && !editNode && !readonly) {
postValidatePrompt(field_name!, value, nodeClass!).then((apiReturn) => {
if (apiReturn.data) {
setNodeClass!(apiReturn.data.frontend_node);
@ -35,6 +36,7 @@ export default function PromptAreaComponent({
return (
<div className={disabled ? "pointer-events-none w-full " : " w-full"}>
<GenericModal
readonly={readonly}
type={TypeModal.PROMPT}
value={value}
buttonText="Check & Save"

View file

@ -5,11 +5,11 @@ export const SkeletonCardComponent = (): JSX.Element => {
<div className="skeleton-card">
<div className="skeleton-card-wrapper">
<Skeleton className="h-8 w-8 rounded-full" />
<Skeleton className="h-4 w-[120px]" />
<Skeleton className="h-4 w-[40%]" />
</div>
<div className="skeleton-card-text">
<Skeleton className="h-3 w-[250px]" />
<Skeleton className="h-3 w-[200px]" />
<Skeleton className="h-3 w-[90%]" />
<Skeleton className="h-3 w-[80%]" />
</div>
</div>
);

View file

@ -6,6 +6,7 @@ export default function ToggleShadComponent({
setEnabled,
disabled,
size,
id = "",
}: ToggleComponentType): JSX.Element {
let scaleX, scaleY;
switch (size) {
@ -29,6 +30,7 @@ export default function ToggleShadComponent({
return (
<div className={disabled ? "pointer-events-none cursor-not-allowed " : ""}>
<Switch
id={id}
style={{
transform: `scaleX(${scaleX}) scaleY(${scaleY})`,
}}

View file

@ -8,12 +8,11 @@ const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = ({
className,
children,
...props
}: DialogPrimitive.DialogPortalProps) => (
<DialogPrimitive.Portal className={cn(className)} {...props}>
<div className="nopan nodrag noundo nocopy fixed inset-0 z-50 flex items-start justify-center sm:items-center">
<DialogPrimitive.Portal {...props}>
<div className="nopan nodelete nodrag noundo nocopy fixed inset-0 z-50 flex items-start justify-center sm:items-center">
{children}
</div>
</DialogPrimitive.Portal>
@ -27,7 +26,7 @@ const DialogOverlay = React.forwardRef<
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"nopan nodrag noundo nocopy fixed inset-0 bottom-0 left-0 right-0 top-0 z-50 overflow-auto bg-blur-shared backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
"nopan nodelete nodrag noundo nocopy fixed inset-0 bottom-0 left-0 right-0 top-0 z-50 overflow-auto bg-blur-shared backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}

View file

@ -9,7 +9,10 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
return (
<input
type={type}
className={cn("nopan nodrag noundo nocopy primary-input", className)}
className={cn(
"nopan nodelete nodrag noundo nocopy primary-input",
className
)}
ref={ref}
{...props}
/>

View file

@ -57,7 +57,7 @@ export default function RenameLabel(props) {
ref={inputRef}
onInput={resizeInput}
className={cn(
"nopan nodrag noundo nocopy rounded-md bg-transparent px-2 outline-ring hover:outline focus:border-none focus:outline active:outline",
"nopan nodelete nodrag noundo nocopy rounded-md bg-transparent px-2 outline-ring hover:outline focus:border-none focus:outline active:outline",
props.className
)}
onBlur={() => {

View file

@ -8,7 +8,10 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
return (
<textarea
className={cn("nopan nodrag noundo nocopy textarea-primary", className)}
className={cn(
"nopan nodelete nodrag noundo nocopy textarea-primary",
className
)}
ref={ref}
{...props}
/>

View file

@ -19,7 +19,7 @@ const TooltipContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
className={cn(
"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",
"z-50 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",
className
)}
{...props}

View file

@ -529,7 +529,13 @@ export const URL_EXCLUDED_FROM_ERROR_RETRIES = [
"http://localhost:7860/login",
];
export const skipNodeUpdate = ["CustomComponent"];
export const skipNodeUpdate = [
"CustomComponent",
"PromptTemplate",
"ChatMessagePromptTemplate",
"SystemMessagePromptTemplate",
"HumanMessagePromptTemplate",
];
export const CONTROL_INPUT_STATE = {
password: "",

View file

@ -26,10 +26,10 @@ export const AuthContext = createContext<AuthContextType>(initialValue);
export function AuthProvider({ children }): React.ReactElement {
const cookies = new Cookies();
const [accessToken, setAccessToken] = useState<string | null>(
cookies.get("access_token")
cookies.get("access_tkn_lflw")
);
const [refreshToken, setRefreshToken] = useState<string | null>(
cookies.get("refresh_token")
cookies.get("refresh_tkn_lflw")
);
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
const [isAdmin, setIsAdmin] = useState<boolean>(false);
@ -37,7 +37,7 @@ export function AuthProvider({ children }): React.ReactElement {
const [autoLogin, setAutoLogin] = useState<boolean>(false);
const { setLoading } = useContext(alertContext);
useEffect(() => {
const storedAccessToken = cookies.get("access_token");
const storedAccessToken = cookies.get("access_tkn_lflw");
if (storedAccessToken) {
setAccessToken(storedAccessToken);
}
@ -66,7 +66,10 @@ export function AuthProvider({ children }): React.ReactElement {
const isSuperUser = user!.is_superuser;
setIsAdmin(isSuperUser);
})
.catch((error) => {});
.catch((error) => {
console.log("auth context");
setLoading(false);
});
} else {
setLoading(false);
}
@ -74,23 +77,23 @@ export function AuthProvider({ children }): React.ReactElement {
}, [setUserData, setLoading, autoLogin, setIsAdmin]);
function getAuthentication() {
const storedRefreshToken = cookies.get("refresh_token");
const storedAccess = cookies.get("access_token");
const storedRefreshToken = cookies.get("refresh_tkn_lflw");
const storedAccess = cookies.get("access_tkn_lflw");
const auth = storedAccess && storedRefreshToken ? true : false;
return auth;
}
function login(newAccessToken: string, refreshToken: string) {
cookies.set("access_token", newAccessToken, { path: "/" });
cookies.set("refresh_token", refreshToken, { path: "/" });
cookies.set("access_tkn_lflw", newAccessToken, { path: "/" });
cookies.set("refresh_tkn_lflw", refreshToken, { path: "/" });
setAccessToken(newAccessToken);
setRefreshToken(refreshToken);
setIsAuthenticated(true);
}
function logout() {
cookies.remove("access_token", { path: "/" });
cookies.remove("refresh_token", { path: "/" });
cookies.remove("access_tkn_lflw", { path: "/" });
cookies.remove("refresh_tkn_lflw", { path: "/" });
setIsAdmin(false);
setUserData(null);
setAccessToken(null);

View file

@ -21,10 +21,20 @@ import {
} from "../controllers/API";
import { APIClassType, APITemplateType } from "../types/api";
import { tweakType } from "../types/components";
import { FlowType, NodeDataType, NodeType } from "../types/flow";
import {
FlowType,
NodeDataType,
NodeType,
sourceHandleType,
targetHandleType,
} from "../types/flow";
import { TabsContextType, TabsState } from "../types/tabs";
import {
addVersionToDuplicates,
checkOldEdgesHandles,
scapeJSONParse,
scapedJSONStringfy,
updateEdgesHandleIds,
updateIds,
updateTemplate,
} from "../utils/reactflowUtils";
@ -36,13 +46,12 @@ import { typesContext } from "./typesContext";
const uid = new ShortUniqueId({ length: 5 });
const TabsContextInitialValue: TabsContextType = {
save: () => {},
tabId: "",
setTabId: (index: string) => {},
isLoading: true,
flows: [],
removeFlow: (id: string) => {},
addFlow: async (flowData?: any) => "",
addFlow: async (newProject: boolean, flowData?: FlowType) => "",
updateFlow: (newFlow: FlowType) => {},
incrementNodeId: () => uid(),
downloadFlow: (flow: FlowType) => {},
@ -52,7 +61,7 @@ const TabsContextInitialValue: TabsContextType = {
isBuilt: false,
setIsBuilt: (state: boolean) => {},
hardReset: () => {},
saveFlow: async (flow: FlowType) => {},
saveFlow: async (flow: FlowType, silent?: boolean) => {},
lastCopiedSelection: null,
setLastCopiedSelection: (selection: any) => {},
tabsState: {},
@ -101,29 +110,6 @@ export function TabsProvider({ children }: { children: ReactNode }) {
return newNodeId.current;
}
function save() {
// added clone deep to avoid mutating the original object
let Saveflows = _.cloneDeep(flows);
if (Saveflows.length !== 0) {
Saveflows.forEach((flow) => {
if (flow.data && flow.data?.nodes)
flow.data?.nodes.forEach((node) => {
//looking for file fields to prevent saving the content and breaking the flow for exceeding the the data limite for local storage
Object.keys(node.data.node.template).forEach((key) => {
if (node.data.node.template[key].type === "file") {
node.data.node.template[key].content = null;
node.data.node.template[key].value = "";
}
});
});
});
window.localStorage.setItem(
"tabsData",
JSON.stringify({ tabId, flows: Saveflows, id })
);
}
}
function refreshFlows() {
setIsLoading(true);
getTabsDataFromDB().then((DbData) => {
@ -158,14 +144,18 @@ export function TabsProvider({ children }: { children: ReactNode }) {
if (!flow.data) {
return;
}
processFlowEdges(flow);
processFlowNodes(flow);
processDataFromFlow(flow, false);
} catch (e) {}
});
}
function processFlowEdges(flow: FlowType) {
if (!flow.data || !flow.data.edges) return;
if (checkOldEdgesHandles(flow.data.edges)) {
const newEdges = updateEdgesHandleIds(flow.data);
flow.data.edges = newEdges;
}
//update edges colors
flow.data.edges.forEach((edge) => {
edge.className = "";
edge.style = { stroke: "#555" };
@ -183,6 +173,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
function processFlowNodes(flow: FlowType) {
if (!flow.data || !flow.data.nodes) return;
flow.data.nodes.forEach((node: NodeType) => {
if (node.data.node?.flow) return;
if (skipNodeUpdate.includes(node.data.type)) return;
const template = templates[node.data.type];
if (!template) {
@ -192,6 +183,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
if (Object.keys(template["template"]).length > 0) {
updateDisplay_name(node, template);
updateNodeBaseClasses(node, template);
//update baseclasses in edges
updateNodeEdges(flow, node, template);
updateNodeDescription(node, template);
updateNodeTemplate(node, template);
@ -211,11 +203,12 @@ export function TabsProvider({ children }: { children: ReactNode }) {
) {
flow.data!.edges.forEach((edge) => {
if (edge.source === node.id) {
edge.sourceHandle = edge.sourceHandle
?.split("|")
.slice(0, 2)
.concat(template["base_classes"])
.join("|");
let sourceHandleObject: sourceHandleType = scapeJSONParse(
edge.sourceHandle!
);
sourceHandleObject.baseClasses = template["base_classes"];
edge.data.sourceHandle = sourceHandleObject;
edge.sourceHandle = scapedJSONStringfy(sourceHandleObject);
}
});
}
@ -298,16 +291,22 @@ export function TabsProvider({ children }: { children: ReactNode }) {
* The resulting JSON object is passed to the addFlow function.
*/
async function uploadFlow(
newProject?: boolean,
newProject: boolean,
file?: File
): Promise<String | undefined> {
let id;
if (file) {
let text = await file.text();
let fileData = JSON.parse(text);
if (fileData.flows) {
fileData.flows.forEach((flow: FlowType) => {
id = addFlow(flow, newProject);
});
}
// parse the text into a JSON object
let flow: FlowType = JSON.parse(text);
id = await addFlow(flow, newProject);
id = await addFlow(newProject, flow);
} else {
// create a file input
const input = document.createElement("input");
@ -322,7 +321,7 @@ export function TabsProvider({ children }: { children: ReactNode }) {
const currentfile = (e.target as HTMLInputElement).files![0];
let text = await currentfile.text();
let flow: FlowType = JSON.parse(text);
const flowId = await addFlow(flow, newProject);
const flowId = await addFlow(newProject, flow);
resolve(flowId);
}
};
@ -373,7 +372,6 @@ export function TabsProvider({ children }: { children: ReactNode }) {
* Add a new flow to the list of flows.
* @param flow Optional flow to add.
*/
function paste(
selectionInstance: { nodes: Node[]; edges: Edge[] },
position: { x: number; y: number; paneX?: number; paneY?: number }
@ -422,19 +420,27 @@ export function TabsProvider({ children }: { children: ReactNode }) {
});
reactFlowInstance!.setNodes(nodes);
selectionInstance.edges.forEach((edge) => {
selectionInstance.edges.forEach((edge: Edge) => {
let source = idsMap[edge.source];
let target = idsMap[edge.target];
let sourceHandleSplitted = edge.sourceHandle!.split("|");
let sourceHandle =
sourceHandleSplitted[0] +
"|" +
source +
"|" +
sourceHandleSplitted.slice(2).join("|");
let targetHandleSplitted = edge.targetHandle!.split("|");
let targetHandle =
targetHandleSplitted.slice(0, -1).join("|") + "|" + target;
const sourceHandleObject: sourceHandleType = scapeJSONParse(
edge.sourceHandle!
);
let sourceHandle = scapedJSONStringfy({
...sourceHandleObject,
id: source,
});
sourceHandleObject.id = source;
edge.data.sourceHandle = sourceHandleObject;
const targetHandleObject: targetHandleType = scapeJSONParse(
edge.targetHandle!
);
let targetHandle = scapedJSONStringfy({
...targetHandleObject,
id: target,
});
targetHandleObject.id = target;
edge.data.targetHandle = targetHandleObject;
let id =
"reactflow__edge-" +
source +
@ -451,10 +457,10 @@ export function TabsProvider({ children }: { children: ReactNode }) {
id,
style: { stroke: "#555" },
className:
targetHandle.split("|")[0] === "Text"
targetHandleObject.type === "Text"
? "stroke-gray-800 "
: "stroke-gray-900 ",
animated: targetHandle.split("|")[0] === "Text",
animated: targetHandleObject.type === "Text",
selected: false,
},
edges.map((edge) => ({ ...edge, selected: false }))
@ -464,19 +470,16 @@ export function TabsProvider({ children }: { children: ReactNode }) {
}
const addFlow = async (
flow?: FlowType,
newProject?: Boolean
newProject: Boolean,
flow?: FlowType
): Promise<String | undefined> => {
if (newProject) {
let flowData = extractDataFromFlow(flow!);
if (flowData.description == "") {
flowData.description = getRandomDescription();
}
let flowData = flow
? processDataFromFlow(flow)
: { nodes: [], edges: [], viewport: { zoom: 1, x: 0, y: 0 } };
// Create a new flow with a default name if no flow is provided.
const newFlow = createNewFlow(flowData, flow!);
processFlowEdges(newFlow);
processFlowNodes(newFlow);
const flowName = addVersionToDuplicates(newFlow, flows);
@ -504,31 +507,36 @@ export function TabsProvider({ children }: { children: ReactNode }) {
}
};
const extractDataFromFlow = (flow: FlowType) => {
const processDataFromFlow = (flow: FlowType, refreshIds = true) => {
let data = flow?.data ? flow.data : null;
const description = flow?.description ? flow.description : "";
if (data) {
processFlowEdges(flow);
processFlowNodes(flow);
//add animation to text type edges
updateEdges(data.edges);
updateNodes(data.nodes, data.edges);
updateIds(data, getNodeId); // Assuming updateIds is defined elsewhere
// updateNodes(data.nodes, data.edges);
if (refreshIds) updateIds(data, getNodeId); // Assuming updateIds is defined elsewhere
}
return { data, description };
return data;
};
const updateEdges = (edges: Edge[]) => {
edges.forEach((edge) => {
const targetHandleObject: targetHandleType = scapeJSONParse(
edge.targetHandle!
);
edge.className =
(edge.targetHandle!.split("|")[0] === "Text"
(targetHandleObject.type === "Text"
? "stroke-gray-800 "
: "stroke-gray-900 ") + " stroke-connection";
edge.animated = edge.targetHandle!.split("|")[0] === "Text";
edge.animated = targetHandleObject.type === "Text";
});
};
const updateNodes = (nodes: Node[], edges: Edge[]) => {
nodes.forEach((node) => {
if (node.data.node?.flow) return;
if (skipNodeUpdate.includes(node.data.type)) return;
const template = templates[node.data.type];
if (!template) {
@ -538,12 +546,13 @@ export function TabsProvider({ children }: { children: ReactNode }) {
if (Object.keys(template["template"]).length > 0) {
node.data.node.base_classes = template["base_classes"];
edges.forEach((edge) => {
let sourceHandleObject: sourceHandleType = scapeJSONParse(
edge.sourceHandle!
);
if (edge.source === node.id) {
edge.sourceHandle = edge
.sourceHandle!.split("|")
.slice(0, 2)
.concat(template["base_classes"])
.join("|");
let newSourceHandle = sourceHandleObject;
newSourceHandle.baseClasses.concat(template["base_classes"]);
edge.sourceHandle = scapedJSONStringfy(newSourceHandle);
}
});
node.data.node.description = template["description"];
@ -556,12 +565,12 @@ export function TabsProvider({ children }: { children: ReactNode }) {
};
const createNewFlow = (
flowData: { data: ReactFlowJsonObject | null; description: string },
flowData: ReactFlowJsonObject | null,
flow: FlowType
) => ({
description: flowData.description,
description: flow?.description ?? getRandomDescription(),
name: flow?.name ?? getRandomName(),
data: flowData.data,
data: flowData,
id: "",
});
@ -588,13 +597,15 @@ export function TabsProvider({ children }: { children: ReactNode }) {
});
}
async function saveFlow(newFlow: FlowType) {
async function saveFlow(newFlow: FlowType, silent?: boolean) {
try {
// updates flow in db
const updatedFlow = await updateFlowInDatabase(newFlow);
if (updatedFlow) {
// updates flow in state
setSuccessData({ title: "Changes saved successfully" });
if (!silent) {
setSuccessData({ title: "Changes saved successfully" });
}
setFlows((prevState) => {
const newFlows = [...prevState];
const index = newFlows.findIndex((flow) => flow.id === newFlow.id);
@ -638,7 +649,6 @@ export function TabsProvider({ children }: { children: ReactNode }) {
tabId,
setTabId,
flows,
save,
incrementNodeId,
removeFlow,
addFlow,

View file

@ -6,7 +6,7 @@ import {
useEffect,
useState,
} from "react";
import { Node, ReactFlowInstance } from "reactflow";
import { Edge, Node, ReactFlowInstance } from "reactflow";
import { getAll, getHealth } from "../controllers/API";
import { APIKindType } from "../types/api";
import { localStorageUserType } from "../types/entities";
@ -38,6 +38,7 @@ const initialValue: typesContextType = {
getFilterEdge: [],
saveComponent: (component: NodeDataType, key: string) => {},
deleteComponent: (id: string, key: string) => {},
deleteEdge: () => {},
};
export const typesContext = createContext<typesContextType>(initialValue);
@ -115,14 +116,31 @@ export function TypesProvider({ children }: { children: ReactNode }) {
}
}
function deleteNode(idx: string) {
function deleteNode(idx: string | Array<string>) {
reactFlowInstance!.setNodes(
reactFlowInstance!.getNodes().filter((node: Node) => node.id !== idx)
reactFlowInstance!
.getNodes()
.filter((node: Node) =>
typeof idx === "string" ? node.id !== idx : !idx.includes(node.id)
)
);
reactFlowInstance!.setEdges(
reactFlowInstance!
.getEdges()
.filter((edge) => edge.source !== idx && edge.target !== idx)
.filter((edge) =>
typeof idx === "string"
? edge.source !== idx && edge.target !== idx
: !idx.includes(edge.source) && !idx.includes(edge.target)
)
);
}
function deleteEdge(idx: string | Array<string>) {
reactFlowInstance!.setEdges(
reactFlowInstance!
.getEdges()
.filter((edge: Edge) =>
typeof idx === "string" ? edge.id !== idx : !idx.includes(edge.id)
)
);
}
@ -181,6 +199,7 @@ export function TypesProvider({ children }: { children: ReactNode }) {
return (
<typesContext.Provider
value={{
deleteEdge,
deleteComponent,
saveComponent,
types,

View file

@ -23,7 +23,7 @@ function ApiInterceptor() {
(response) => response,
async (error: AxiosError) => {
if (error.response?.status === 401) {
const refreshToken = cookies.get("refresh_token");
const refreshToken = cookies.get("refresh_tkn_lflw");
if (refreshToken && refreshToken !== "auto") {
authenticationErrorCount = authenticationErrorCount + 1;
if (authenticationErrorCount > 3) {
@ -31,17 +31,15 @@ function ApiInterceptor() {
logout();
navigate("/login");
}
const res = await renewAccessToken(refreshToken);
if (res?.data?.access_token && res?.data?.refresh_token) {
login(res?.data?.access_token, res?.data?.refresh_token);
}
try {
const res = await renewAccessToken(refreshToken);
if (res?.data?.access_token && res?.data?.refresh_token) {
login(res?.data?.access_token, res?.data?.refresh_token);
}
if (error?.config?.headers) {
delete error.config.headers["Authorization"];
error.config.headers["Authorization"] = `Bearer ${cookies.get(
"access_token"
"access_tkn_lflw"
)}`;
const response = await axios.request(error.config);
return response;
@ -50,6 +48,10 @@ function ApiInterceptor() {
if (axios.isAxiosError(error) && error.response?.status === 401) {
logout();
navigate("/login");
} else {
console.error(error);
logout();
navigate("/login");
}
}
}

View file

@ -146,8 +146,8 @@ export async function updateFlowInDatabase(
description: updatedFlow.description,
});
if (response.status !== 200) {
throw new Error(`HTTP error! status: ${response.status}`);
if (response?.status !== 200) {
throw new Error(`HTTP error! status: ${response?.status}`);
}
return response.data;
} catch (error) {
@ -399,7 +399,6 @@ export async function renewAccessToken(token: string) {
return await api.post(`${BASE_URL_API}refresh?token=${token}`);
}
} catch (error) {
console.log("Error:", error);
throw error;
}
}

View file

@ -19,7 +19,7 @@ import { TabsContext } from "../../contexts/tabsContext";
import { TemplateVariableType } from "../../types/api";
import { tweakType, uniqueTweakType } from "../../types/components";
import { FlowType, NodeType } from "../../types/flow/index";
import { buildTweaks } from "../../utils/reactflowUtils";
import { buildTweaks, convertArrayToObj } from "../../utils/reactflowUtils";
import {
getCurlCode,
getPythonApiCode,
@ -105,7 +105,9 @@ const ApiModal = forwardRef(
node.data.node.template[templateField].type === "code" ||
node.data.node.template[templateField].type === "prompt" ||
node.data.node.template[templateField].type === "file" ||
node.data.node.template[templateField].type === "int")
node.data.node.template[templateField].type === "int" ||
node.data.node.template[templateField].type === "dict" ||
node.data.node.template[templateField].type === "NestedDict")
)
.map((n, i) => {
arrNodesWithValues.push(node["id"]);
@ -118,7 +120,7 @@ const ApiModal = forwardRef(
}
function buildTweakObject(
tw: string,
changes: string | string[] | boolean | number,
changes: string | string[] | boolean | number | Object[] | Object,
template: TemplateVariableType
) {
if (typeof changes === "string" && template.type === "float") {
@ -131,6 +133,14 @@ const ApiModal = forwardRef(
changes = changes?.filter((x) => x !== "");
}
if (template.type === "dict" && Array.isArray(changes)) {
changes = convertArrayToObj(changes);
}
if (template.type === "NestedDict") {
changes = JSON.stringify(changes);
}
const existingTweak = tweak.current.find((element) =>
element.hasOwnProperty(tw)
);

View file

@ -1,5 +1,13 @@
import { cloneDeep } from "lodash";
import { ReactNode, forwardRef, useContext, useEffect, useState } from "react";
import {
ReactNode,
forwardRef,
useContext,
useEffect,
useRef,
useState,
} from "react";
import ShadTooltip from "../../components/ShadTooltipComponent";
import CodeAreaComponent from "../../components/codeAreaComponent";
import DictComponent from "../../components/dictComponent";
import Dropdown from "../../components/dropdownComponent";
@ -30,6 +38,7 @@ import { NodeDataType } from "../../types/flow";
import { TabsState } from "../../types/tabs";
import {
convertObjToArray,
convertValuesToNumbers,
hasDuplicateKeys,
} from "../../utils/reactflowUtils";
import { classNames } from "../../utils/utils";
@ -55,7 +64,9 @@ const EditNodeModal = forwardRef(
ref
) => {
const [modalOpen, setModalOpen] = useState(open ?? false);
const [myData, setMyData] = useState(data);
const myData = useRef(data);
const { setTabsState, tabId } = useContext(TabsContext);
const { reactFlowInstance } = useContext(typesContext);
let disabled =
@ -64,55 +75,39 @@ const EditNodeModal = forwardRef(
.some((edge) => edge.targetHandle === data.id) ?? false;
function changeAdvanced(n) {
let newData = cloneDeep(data);
newData.node!.template[n].advanced = !newData.node!.template[n].advanced;
setMyData(newData);
myData.current.node!.template[n].advanced =
!myData.current.node!.template[n].advanced;
setAdv(!adv);
}
const handleOnNewValue = (newValue: any, name) => {
let newData = cloneDeep(data);
newData.node!.template[name].value = newValue;
setMyData(newData);
myData.current.node!.template[name].value = newValue;
setDataValue(newValue);
};
useEffect(() => {
setMyData(data); // reset data to what it is on node when opening modal
myData.current = data; // reset data to what it is on node when opening modal
onClose!(modalOpen);
}, [modalOpen]);
const [obj, setObj] = useState({
arr: ["test", 123456, false, null],
boolean: false,
longString:
"long string long string long string long string long string long string",
number: 123456,
try: {
k1: 123,
k2: "123",
k3: false,
},
string: "string",
});
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
const [dictArr, setDictArr] = useState([
{ yourKey: "yourValue" },
] as Object[]);
const [adv, setAdv] = useState<boolean | null>(null);
const [dataValue, setDataValue] = useState(data);
return (
<BaseModal
key={data.id}
size="large-h-full"
open={modalOpen}
setOpen={setModalOpen}
onChangeOpenModal={(open) => {
let newData = cloneDeep(data);
setMyData(newData);
myData.current = data;
}}
>
<BaseModal.Trigger>{children}</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={myData.current.node?.description!}>
<span className="pr-2">{myData.current.type}</span>
<Badge variant="secondary">ID: {myData.current.id}</Badge>
</BaseModal.Header>
<BaseModal.Content>
<div className="flex pb-2">
@ -145,110 +140,92 @@ const EditNodeModal = forwardRef(
</TableRow>
</TableHeader>
<TableBody className="p-0">
{Object.keys(myData.node!.template)
{Object.keys(myData.current.node!.template)
.filter(
(templateParam) =>
templateParam.charAt(0) !== "_" &&
myData.node?.template[templateParam].show &&
(myData.node.template[templateParam].type ===
"str" ||
myData.node.template[templateParam].type ===
"bool" ||
myData.node.template[templateParam].type ===
"float" ||
myData.node.template[templateParam].type ===
"code" ||
myData.node.template[templateParam].type ===
"prompt" ||
myData.node.template[templateParam].type ===
"file" ||
myData.node.template[templateParam].type ===
"int")
myData.current.node?.template[templateParam].show &&
(myData.current.node.template[templateParam]
.type === "str" ||
myData.current.node.template[templateParam]
.type === "bool" ||
myData.current.node.template[templateParam]
.type === "float" ||
myData.current.node.template[templateParam]
.type === "code" ||
myData.current.node.template[templateParam]
.type === "prompt" ||
myData.current.node.template[templateParam]
.type === "file" ||
myData.current.node.template[templateParam]
.type === "int" ||
myData.current.node.template[templateParam]
.type === "dict" ||
myData.current.node.template[templateParam]
.type === "NestedDict")
)
.map((templateParam, index) => (
<TableRow key={index} className="h-10">
<TableCell className="truncate p-0 text-center text-sm text-foreground sm:px-3">
{myData.node?.template[templateParam].name
? myData.node.template[templateParam].name
: myData.node?.template[templateParam]
.display_name}
<ShadTooltip
content={
myData.current.node?.template[templateParam]
.proxy
? myData.current.node?.template[
templateParam
].proxy?.id
: null
}
>
<span>
{myData.current.node?.template[templateParam]
.display_name
? myData.current.node.template[
templateParam
].display_name
: myData.current.node?.template[
templateParam
].name}
</span>
</ShadTooltip>
</TableCell>
<TableCell className="w-[300px] p-0 text-center text-xs text-foreground ">
{myData.node?.template[templateParam].type ===
"str" &&
!myData.node.template[templateParam].options ? (
{myData.current.node?.template[templateParam]
.type === "str" &&
!myData.current.node.template[templateParam]
.options ? (
<div className="mx-auto">
{myData.node.template[templateParam].list ? (
{myData.current.node.template[templateParam]
.list ? (
<InputListComponent
editNode={true}
disabled={disabled}
value={
!myData.node.template[templateParam]
.value ||
myData.node.template[templateParam]
.value === ""
!myData.current.node.template[
templateParam
].value ||
myData.current.node.template[
templateParam
].value === ""
? [""]
: myData.node.template[templateParam]
.value
: myData.current.node.template[
templateParam
].value
}
onChange={(value: string[]) => {
handleOnNewValue(value, templateParam);
}}
/>
) : myData.node?.template[templateParam]
.type === "NestedDict" ? (
<div className="mt-2 w-full">
<DictComponent
disabled={disabled}
editNode={true}
value={obj}
onChange={(newValue) => {
setObj(newValue);
}}
/>
</div>
) : myData.node?.template[templateParam]
.type === "dict" ? (
<div className="mt-2 w-full">
<KeypairListComponent
disabled={disabled}
editNode={false}
value={
myData.node.template[templateParam]
.value?.length === 0 ||
!myData.node.template[templateParam]
.value
? dictArr
: convertObjToArray(
myData.node.template[
templateParam
].value
)
}
duplicateKey={errorDuplicateKey}
onChange={(newValue) => {
setErrorDuplicateKey(
hasDuplicateKeys(newValue)
);
if (hasDuplicateKeys(newValue)) {
setDictArr(newValue);
} else {
setDictArr(newValue);
myData.node!.template[
templateParam
].value = newValue;
}
}}
/>
</div>
) : myData.node.template[templateParam]
.multiline ? (
) : myData.current.node.template[
templateParam
].multiline ? (
<TextAreaComponent
disabled={disabled}
editNode={true}
value={
myData.node.template[templateParam]
.value ?? ""
myData.current.node.template[
templateParam
].value ?? ""
}
onChange={(value: string | string[]) => {
handleOnNewValue(value, templateParam);
@ -259,12 +236,14 @@ const EditNodeModal = forwardRef(
editNode={true}
disabled={disabled}
password={
myData.node.template[templateParam]
.password ?? false
myData.current.node.template[
templateParam
].password ?? false
}
value={
myData.node.template[templateParam]
.value ?? ""
myData.current.node.template[
templateParam
].value ?? ""
}
onChange={(value) => {
handleOnNewValue(value, templateParam);
@ -272,14 +251,88 @@ const EditNodeModal = forwardRef(
/>
)}
</div>
) : myData.node?.template[templateParam].type ===
"bool" ? (
) : myData.current.node?.template[templateParam]
.type === "NestedDict" ? (
<div className=" w-full">
<DictComponent
disabled={disabled}
editNode={true}
value={
myData.current.node!.template[
templateParam
].value.toString() === "{}"
? {
yourkey: "value",
}
: myData.current.node!.template[
templateParam
].value
}
onChange={(newValue) => {
myData.current.node!.template[
templateParam
].value = newValue;
handleOnNewValue(newValue, templateParam);
}}
/>
</div>
) : myData.current.node?.template[templateParam]
.type === "dict" ? (
<div
className={classNames(
"max-h-48 w-full overflow-auto custom-scroll",
myData.current.node!.template[templateParam]
.value?.length > 1
? "my-3"
: ""
)}
>
<KeypairListComponent
dataValue={dataValue}
advanced={adv}
disabled={disabled}
editNode={true}
value={
myData.current.node!.template[
templateParam
].value?.length === 0 ||
!myData.current.node!.template[
templateParam
].value
? [{ "": "" }]
: convertObjToArray(
myData.current.node!.template[
templateParam
].value
)
}
duplicateKey={errorDuplicateKey}
onChange={(newValue) => {
const valueToNumbers =
convertValuesToNumbers(newValue);
myData.current.node!.template[
templateParam
].value = valueToNumbers;
setErrorDuplicateKey(
hasDuplicateKeys(valueToNumbers)
);
handleOnNewValue(
valueToNumbers,
templateParam
);
}}
/>
</div>
) : myData.current.node?.template[templateParam]
.type === "bool" ? (
<div className="ml-auto">
{" "}
<ToggleShadComponent
disabled={disabled}
enabled={
myData.node.template[templateParam].value
myData.current.node.template[
templateParam
].value
}
setEnabled={(isEnabled) => {
handleOnNewValue(
@ -290,76 +343,84 @@ const EditNodeModal = forwardRef(
size="small"
/>
</div>
) : myData.node?.template[templateParam].type ===
"float" ? (
) : myData.current.node?.template[templateParam]
.type === "float" ? (
<div className="mx-auto">
<FloatComponent
disabled={disabled}
editNode={true}
value={
myData.node.template[templateParam]
.value ?? ""
myData.current.node.template[
templateParam
].value ?? ""
}
onChange={(value) => {
handleOnNewValue(value, templateParam);
}}
/>
</div>
) : myData.node?.template[templateParam].type ===
"str" &&
myData.node.template[templateParam].options ? (
) : myData.current.node?.template[templateParam]
.type === "str" &&
myData.current.node.template[templateParam]
.options ? (
<div className="mx-auto">
<Dropdown
numberOfOptions={nodeLength}
editNode={true}
options={
myData.node.template[templateParam]
.options
myData.current.node.template[
templateParam
].options
}
onSelect={(value) =>
handleOnNewValue(value, templateParam)
}
value={
myData.node.template[templateParam]
.value ?? "Choose an option"
myData.current.node.template[
templateParam
].value ?? "Choose an option"
}
></Dropdown>
</div>
) : myData.node?.template[templateParam].type ===
"int" ? (
) : myData.current.node?.template[templateParam]
.type === "int" ? (
<div className="mx-auto">
<IntComponent
disabled={disabled}
editNode={true}
value={
myData.node.template[templateParam]
.value ?? ""
myData.current.node.template[
templateParam
].value ?? ""
}
onChange={(value) => {
handleOnNewValue(value, templateParam);
}}
/>
</div>
) : myData.node?.template[templateParam].type ===
"file" ? (
) : myData.current.node?.template[templateParam]
.type === "file" ? (
<div className="mx-auto">
<InputFileComponent
editNode={true}
disabled={disabled}
value={
myData.node.template[templateParam]
.value ?? ""
myData.current.node.template[
templateParam
].value ?? ""
}
onChange={(value: string | string[]) => {
handleOnNewValue(value, templateParam);
}}
fileTypes={
myData.node.template[templateParam]
.fileTypes
myData.current.node.template[
templateParam
].fileTypes
}
suffixes={
myData.node.template[templateParam]
.suffixes
myData.current.node.template[
templateParam
].suffixes
}
onFileChange={(filePath: string) => {
data.node!.template[
@ -368,30 +429,47 @@ const EditNodeModal = forwardRef(
}}
></InputFileComponent>
</div>
) : myData.node?.template[templateParam].type ===
"prompt" ? (
) : myData.current.node?.template[templateParam]
.type === "prompt" ? (
<div className="mx-auto">
<PromptAreaComponent
readonly={
myData.current.node?.flow &&
myData.current.node.template[
templateParam
].dynamic
? true
: false
}
field_name={templateParam}
editNode={true}
disabled={disabled}
nodeClass={myData.node}
nodeClass={myData.current.node}
setNodeClass={(nodeClass) => {
myData.node = nodeClass;
myData.current.node = nodeClass;
}}
value={
myData.node.template[templateParam]
.value ?? ""
myData.current.node.template[
templateParam
].value ?? ""
}
onChange={(value: string | string[]) => {
handleOnNewValue(value, templateParam);
}}
/>
</div>
) : myData.node?.template[templateParam].type ===
"code" ? (
) : myData.current.node?.template[templateParam]
.type === "code" ? (
<div className="mx-auto">
<CodeAreaComponent
readonly={
myData.current.node?.flow &&
myData.current.node.template[
templateParam
].dynamic
? true
: false
}
dynamic={
data.node!.template[templateParam]
.dynamic ?? false
@ -403,16 +481,17 @@ const EditNodeModal = forwardRef(
disabled={disabled}
editNode={true}
value={
myData.node.template[templateParam]
.value ?? ""
myData.current.node.template[
templateParam
].value ?? ""
}
onChange={(value: string | string[]) => {
handleOnNewValue(value, templateParam);
}}
/>
</div>
) : myData.node?.template[templateParam].type ===
"Any" ? (
) : myData.current.node?.template[templateParam]
.type === "Any" ? (
"-"
) : (
<div className="hidden"></div>
@ -421,13 +500,19 @@ const EditNodeModal = forwardRef(
<TableCell className="p-0 text-right">
<div className="items-center text-center">
<ToggleShadComponent
id={
"show" +
myData.current.node?.template[templateParam]
.name
}
enabled={
!myData.node?.template[templateParam]
.advanced
}
setEnabled={(e) =>
changeAdvanced(templateParam)
!myData.current.node?.template[
templateParam
].advanced
}
setEnabled={(e) => {
changeAdvanced(templateParam);
}}
disabled={disabled}
size="small"
/>
@ -445,9 +530,11 @@ const EditNodeModal = forwardRef(
<BaseModal.Footer>
<Button
id={"saveChangesBtn"}
className="mt-3"
onClick={() => {
setData(cloneDeep(myData)); //saves data with actual state of modal
const newData = cloneDeep(myData.current);
myData.current = newData;
//@ts-ignore
setTabsState((prev: TabsState) => {
return {

View file

@ -70,7 +70,7 @@ interface BaseModalProps {
| "medium-h-full";
disable?: boolean;
onChangeOpenModal?: (open: boolean) => void;
onChangeOpenModal?: (open?: boolean) => void;
}
function BaseModal({
open,

View file

@ -23,6 +23,7 @@ export default function CodeAreaModal({
setNodeClass,
children,
dynamic,
readonly = false,
}: codeAreaModalPropsType): JSX.Element {
const [code, setCode] = useState(value);
const { dark } = useContext(darkContext);
@ -146,6 +147,7 @@ export default function CodeAreaModal({
<div className="flex h-full w-full flex-col transition-all">
<div className="h-full w-full">
<AceEditor
readOnly={readonly}
value={code}
mode="python"
height={height ?? "100%"}
@ -180,7 +182,12 @@ export default function CodeAreaModal({
</div>
</div>
<div className="flex h-fit w-full justify-end">
<Button className="mt-3" onClick={handleClick} type="submit">
<Button
disabled={readonly}
className="mt-3"
onClick={handleClick}
type="submit"
>
Check & Save
</Button>
</div>

View file

@ -4,7 +4,7 @@ import "ace-builds/src-noconflict/mode-python";
import "ace-builds/src-noconflict/theme-github";
import "ace-builds/src-noconflict/theme-twilight";
// import "ace-builds/webpack-resolver";
import { useEffect, useState } from "react";
import { useEffect, useRef, useState } from "react";
import JsonView from "react18-json-view";
import "react18-json-view/src/dark.css";
import "react18-json-view/src/style.css";
@ -19,11 +19,12 @@ export default function DictAreaModal({
value,
}): JSX.Element {
const [open, setOpen] = useState(false);
const [dictObj, setDictObj] = useState(value);
const ref = useRef(value);
useEffect(() => {
if (value) setDictObj(value);
}, [value]);
if (value) ref.current = value;
}, [ref]);
return (
<BaseModal size="medium-h-full" open={open} setOpen={setOpen}>
@ -37,23 +38,26 @@ export default function DictAreaModal({
/>
</BaseModal.Header>
<BaseModal.Content>
<div className="flex h-full w-full flex-col transition-all">
<div className="flex h-full w-full flex-col transition-all ">
<JsonView
theme="vscode"
dark={true}
editable
enableClipboard
onEdit={(edit) => {
setDictObj(edit["src"]);
ref.current = edit["src"];
}}
src={dictObj}
onChange={(edit) => {
ref.current = edit["src"];
}}
src={ref.current}
/>
<div className="flex h-fit w-full justify-end">
<Button
className="mt-3"
type="submit"
onClick={() => {
onChange(dictObj);
onChange(ref.current);
setOpen(false);
}}
>

View file

@ -61,6 +61,7 @@ export default function FormModal({
});
const [chatHistory, setChatHistory] = useState<ChatMessageType[]>([]);
const template = useRef(tabsState[flow.id].formKeysData.template);
const { reactFlowInstance } = useContext(typesContext);
const { accessToken } = useContext(AuthContext);
const { setErrorData } = useContext(alertContext);
@ -125,12 +126,13 @@ export default function FormModal({
function updateLastMessage({
str,
thought,
prompt,
end = false,
files,
}: {
str?: string;
thought?: string;
// end param default is false
prompt?: string;
end?: boolean;
files?: Array<any>;
}) {
@ -150,12 +152,22 @@ export default function FormModal({
if (files) {
newChat[newChat.length - 1].files = files;
}
if (prompt) {
newChat[newChat.length - 2].template = prompt;
}
return newChat;
});
}
function handleOnClose(event: CloseEvent): void {
if (isOpen.current) {
//check if the user has been logged out, if so close the chat when the user is redirected to the login page
if (window.location.href.includes("login")) {
setOpen(false);
ws.current?.close();
return;
}
getBuildStatus(flow.id)
.then((response) => {
if (response.data.built) {
@ -194,17 +206,26 @@ export default function FormModal({
}
function handleWsMessage(data: any) {
console.log(data);
if (Array.isArray(data) && data.length > 0) {
//set chat history
setChatHistory((_) => {
console.log(data);
let newChatHistory: ChatMessageType[] = [];
for (let i = 0; i < data.length; i++) {
if (data[i].type === "prompt" && data[i].prompt) {
if (data[i - 1] && !data[i - 1].is_bot) {
data[i - 1].prompt = data[i].prompt;
template.current = data[i].prompt;
}
}
}
data = data.filter((item: any) => item.type !== "prompt");
data.forEach(
(chatItem: {
intermediate_steps?: string;
is_bot: boolean;
message: string;
template: string;
prompt?: string;
type: string;
chatKey: string;
files?: Array<any>;
@ -215,7 +236,7 @@ export default function FormModal({
? {
isSend: !chatItem.is_bot,
message: chatItem.message,
template: chatItem.template,
template: chatItem.prompt,
thought: chatItem.intermediate_steps,
files: chatItem.files,
chatKey: chatItem.chatKey,
@ -223,7 +244,7 @@ export default function FormModal({
: {
isSend: !chatItem.is_bot,
message: chatItem.message,
template: chatItem.template,
template: chatItem.prompt,
thought: chatItem.intermediate_steps,
chatKey: chatItem.chatKey,
}
@ -240,7 +261,11 @@ export default function FormModal({
}
if (data.type === "end") {
if (data.message) {
updateLastMessage({ str: data.message, end: true });
updateLastMessage({
str: data.message,
end: true,
prompt: template.current,
});
}
if (data.intermediate_steps) {
updateLastMessage({
@ -255,10 +280,12 @@ export default function FormModal({
files: data.files,
});
}
setLockChat(false);
isStream = false;
}
if (data.type == "prompt" && data.prompt) {
template.current = data.prompt;
}
if (data.type === "stream" && isStream) {
updateLastMessage({ str: data.message });
}
@ -283,18 +310,8 @@ export default function FormModal({
handleOnClose(event);
};
newWs.onerror = (ev) => {
if (flow.id === "") {
connectWS();
} else {
setErrorData({
title: "There was an error on web connection, please: ",
list: [
"Refresh the page",
"Use a new flow tab",
"Check if the backend is up",
],
});
}
console.log(ev);
connectWS();
};
ws.current = newWs;
} catch (error) {
@ -316,6 +333,15 @@ export default function FormModal({
// do not add connectWS on dependencies array
}, [open]);
useEffect(() => {
return () => {
if (ws.current) {
console.log("closing ws");
ws.current.close();
}
};
}, []);
useEffect(() => {
if (
ws.current &&
@ -356,24 +382,23 @@ export default function FormModal({
}, [open]);
function sendMessage(): void {
let nodeValidationErrors = validateNodes(reactFlowInstance!);
let nodeValidationErrors = validateNodes(
reactFlowInstance!.getNodes(),
reactFlowInstance!.getEdges()
);
if (nodeValidationErrors.length === 0) {
setLockChat(true);
let inputs = tabsState[id.current].formKeysData.input_keys;
setChatValue("");
const message = inputs;
addChatHistory(
message!,
true,
chatKey!,
tabsState[flow.id].formKeysData.template
);
addChatHistory(message!, true, chatKey!, template.current);
sendAll({
...reactFlowInstance?.toObject()!,
inputs: inputs!,
chatHistory,
name: flow.name,
description: flow.description,
chatKey: chatKey!,
});
//@ts-ignore
setTabsState((old: TabsState) => {
@ -391,6 +416,7 @@ export default function FormModal({
}
function clearChat(): void {
setChatHistory([]);
template.current = tabsState[id.current].formKeysData.template;
ws.current?.send(JSON.stringify({ clear_history: true }));
if (lockChat) setLockChat(false);
}
@ -404,7 +430,6 @@ export default function FormModal({
setChatValue("");
}
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger hidden></DialogTrigger>

View file

@ -17,11 +17,7 @@ import { alertContext } from "../../contexts/alertContext";
import { postValidatePrompt } from "../../controllers/API";
import { genericModalPropsType } from "../../types/components";
import { handleKeyDown } from "../../utils/reactflowUtils";
import {
classNames,
getRandomKeyByssmm,
varHighlightHTML,
} from "../../utils/utils";
import { classNames, varHighlightHTML } from "../../utils/utils";
import BaseModal from "../baseModal";
export default function GenericModal({
@ -34,9 +30,11 @@ export default function GenericModal({
nodeClass,
setNodeClass,
children,
readonly = false,
}: genericModalPropsType): JSX.Element {
const [myButtonText] = useState(buttonText);
const [myModalTitle] = useState(modalTitle);
const [modalOpen, setModalOpen] = useState(false);
const [myModalType] = useState(type);
const [inputValue, setInputValue] = useState(value);
const [isEdit, setIsEdit] = useState(true);
@ -89,7 +87,7 @@ export default function GenericModal({
useEffect(() => {
setInputValue(value);
}, [value]);
}, [value, modalOpen]);
const coloredContent = (inputValue || "")
.replace(/</g, "&lt;")
@ -97,19 +95,6 @@ export default function GenericModal({
.replace(regexHighlight, varHighlightHTML({ name: "$1" }))
.replace(/\n/g, "<br />");
const TextAreaContentView = (): JSX.Element => {
return (
<SanitizedHTMLWrapper
className={getClassByNumberLength()}
content={coloredContent}
onClick={() => {
setIsEdit(true);
}}
suppressWarning={true}
/>
);
};
function getClassByNumberLength(): string {
let sumOfCaracteres: number = 0;
wordsHighlight.forEach((element) => {
@ -125,12 +110,17 @@ export default function GenericModal({
postValidatePrompt(field_name, inputValue, nodeClass!)
.then((apiReturn) => {
// if field_name is an empty string, then we need to set it
// to the first key of the custom_fields object
if (field_name === "") {
console.log(apiReturn.data?.frontend_node?.custom_fields);
field_name = Array.isArray(
apiReturn.data?.frontend_node?.custom_fields?.[""]
)
? apiReturn.data?.frontend_node?.custom_fields?.[""][0] ?? ""
: apiReturn.data?.frontend_node?.custom_fields?.[""] ?? "";
}
if (apiReturn.data) {
setValue(inputValue);
apiReturn.data.frontend_node["template"]["template"]["value"] =
inputValue;
setNodeClass!(apiReturn?.data?.frontend_node);
let inputVariables = apiReturn.data.input_variables ?? [];
if (inputVariables && inputVariables.length === 0) {
setIsEdit(true);
@ -138,6 +128,17 @@ export default function GenericModal({
title: "Your template does not have any variables.",
});
setModalOpen(false);
if (
JSON.stringify(apiReturn.data?.frontend_node) !==
JSON.stringify({})
)
setNodeClass!(apiReturn.data?.frontend_node);
setModalOpen(closeModal);
setValue(inputValue);
if (field_name !== "") {
apiReturn.data.frontend_node["template"][field_name]["value"] =
inputValue;
}
} else {
setIsEdit(false);
setSuccessData({
@ -150,6 +151,10 @@ export default function GenericModal({
setNodeClass!(apiReturn.data?.frontend_node);
setModalOpen(closeModal);
setValue(inputValue);
if (field_name !== "") {
apiReturn.data.frontend_node["template"][field_name]["value"] =
inputValue;
}
}
} else {
setIsEdit(true);
@ -159,16 +164,15 @@ export default function GenericModal({
}
})
.catch((error) => {
console.log(error);
setIsEdit(true);
return setErrorData({
title: "There is something wrong with this prompt, please review it",
list: [error?.response?.data?.detail],
list: [error.toString()],
});
});
}
const [modalOpen, setModalOpen] = useState(false);
return (
<BaseModal
onChangeOpenModal={(open) => {}}
@ -205,7 +209,7 @@ export default function GenericModal({
"flex h-full w-full"
)}
>
{type === TypeModal.PROMPT && isEdit ? (
{type === TypeModal.PROMPT && isEdit && !readonly ? (
<Textarea
ref={divRefPrompt}
className="form-input h-full w-full rounded-lg custom-scroll focus-visible:ring-1"
@ -223,8 +227,15 @@ export default function GenericModal({
handleKeyDown(e, inputValue, "");
}}
/>
) : type === TypeModal.PROMPT && !isEdit ? (
<TextAreaContentView />
) : type === TypeModal.PROMPT && (!isEdit || readonly) ? (
<SanitizedHTMLWrapper
className={getClassByNumberLength()}
content={coloredContent}
onClick={() => {
setIsEdit(true);
}}
suppressWarning={true}
/>
) : type !== TypeModal.PROMPT ? (
<Textarea
//@ts-ignore
@ -238,6 +249,7 @@ export default function GenericModal({
onKeyDown={(e) => {
handleKeyDown(e, value, "");
}}
readOnly={readonly}
/>
) : (
<></>
@ -263,7 +275,7 @@ export default function GenericModal({
{wordsHighlight.map((word, index) => (
<ShadTooltip
key={getRandomKeyByssmm() + index}
key={index}
content={word.replace(/[{}]/g, "")}
asChild={false}
>
@ -294,6 +306,7 @@ export default function GenericModal({
)}
</div>
<Button
disabled={readonly}
onClick={() => {
switch (myModalType) {
case TypeModal.TEXT:

View file

@ -1,101 +0,0 @@
import { buttonBoxPropsType } from "../../../types/components";
import { classNames } from "../../../utils/utils";
export default function ButtonBox({
onClick,
title,
description,
icon,
bgColor,
textColor,
deactivate,
size,
}: buttonBoxPropsType): JSX.Element {
let bigCircle: string;
let smallCircle: string;
let titleFontSize: string;
let descriptionFontSize: string;
let padding: string;
let marginTop: string;
let height: string;
let width: string;
let textHeight: number;
let textWidth: number;
switch (size) {
case "small":
bigCircle = "h-12 w-12";
smallCircle = "h-8 w-8";
titleFontSize = "text-sm";
descriptionFontSize = "text-xs";
padding = "p-2 py-3";
marginTop = "mt-2";
height = "h-36";
width = "w-32";
break;
case "medium":
bigCircle = "h-16 w-16";
smallCircle = "h-12 w-12";
titleFontSize = "text-base";
descriptionFontSize = "text-sm";
padding = "p-4 py-5";
marginTop = "mt-3";
height = "h-44";
width = "w-36";
break;
case "big":
bigCircle = "h-20 w-20";
smallCircle = "h-16 w-16";
titleFontSize = "text-lg";
descriptionFontSize = "text-sm";
padding = "p-8 py-10";
marginTop = "mt-6";
height = "h-56";
width = "w-44";
break;
default:
bigCircle = "h-20 w-20";
smallCircle = "h-16 w-16";
titleFontSize = "text-lg";
descriptionFontSize = "text-sm";
padding = "p-8 py-10";
marginTop = "mt-6";
height = "h-56";
width = "w-44";
break;
}
return (
<button disabled={deactivate} onClick={onClick}>
<div
className={classNames(
"button-box-modal-div",
bgColor,
height,
width,
padding
)}
>
<div
className={`flex items-center justify-center ${bigCircle} mb-1 rounded-full bg-background/30`}
>
<div
className={`flex items-center justify-center ${smallCircle} rounded-full bg-background`}
>
<div className={textColor}>{icon}</div>
</div>
</div>
<div className="mb-auto mt-auto w-full">
<h3
className={classNames(
"w-full font-semibold text-background truncate-multiline word-break-break-word",
titleFontSize,
marginTop
)}
>
{title}
</h3>
</div>
</div>
</button>
);
}

View file

@ -1,190 +0,0 @@
import {
ArrowLeftIcon,
ArrowUpTrayIcon,
ComputerDesktopIcon,
DocumentDuplicateIcon,
} from "@heroicons/react/24/outline";
import { useContext, useRef, useState } from "react";
import LoadingComponent from "../../components/loadingComponent";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "../../components/ui/dialog";
import { IMPORT_DIALOG_SUBTITLE } from "../../constants/constants";
import { alertContext } from "../../contexts/alertContext";
import { TabsContext } from "../../contexts/tabsContext";
import { getExamples } from "../../controllers/API";
import { FlowType } from "../../types/flow";
import { classNames } from "../../utils/utils";
import ButtonBox from "./buttonBox";
export default function ImportModal(): JSX.Element {
const [open, setOpen] = useState(true);
const { setErrorData } = useContext(alertContext);
const ref = useRef();
const [showExamples, setShowExamples] = useState(false);
const [loadingExamples, setLoadingExamples] = useState(false);
const [examples, setExamples] = useState<FlowType[]>([]);
const { uploadFlow, addFlow } = useContext(TabsContext);
function handleExamples(): void {
setLoadingExamples(true);
getExamples()
.then((result) => {
setLoadingExamples(false);
setExamples(result);
})
.catch((error) =>
setErrorData({
title: "there was an error loading examples, please try again",
list: [error.message],
})
);
}
const [modalOpen, setModalOpen] = useState(false);
return (
<Dialog open={modalOpen} onOpenChange={setModalOpen}>
<DialogTrigger></DialogTrigger>
<DialogContent
className={classNames(
showExamples
? "h-[600px] lg:max-w-[650px]"
: "h-[450px] lg:max-w-[650px]"
)}
>
<DialogHeader>
<DialogTitle className="flex items-center">
{showExamples && (
<>
<div className="dialog-header-modal-div">
<button
type="button"
className="dialog-header-modal-button disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"
onClick={() => {
setShowExamples(false);
}}
>
<ArrowLeftIcon
className="ml-1 h-5 w-5 text-foreground"
aria-hidden="true"
/>
</button>
</div>
</>
)}
<span className={classNames(showExamples ? "pl-8 pr-2" : "pr-2")}>
{showExamples ? "Select an example" : "Import"}
</span>
<ArrowUpTrayIcon
className="ml-1 h-5 w-5 text-foreground"
aria-hidden="true"
/>
</DialogTitle>
<DialogDescription>{IMPORT_DIALOG_SUBTITLE}</DialogDescription>
</DialogHeader>
<div
className={classNames(
"dialog-modal-examples-div",
showExamples && !loadingExamples
? "dialog-modal-example-true"
: "dialog-modal-example-false"
)}
>
{!showExamples && (
<div className="dialog-modal-button-box-div">
<ButtonBox
size="big"
bgColor="bg-medium-emerald "
description="Prebuilt Examples"
icon={<DocumentDuplicateIcon className="document-icon" />}
onClick={() => {
setShowExamples(true);
handleExamples();
}}
textColor="text-medium-emerald "
title="Examples"
></ButtonBox>
<ButtonBox
size="big"
bgColor="bg-almost-dark-blue "
description="Import from Local"
icon={<ComputerDesktopIcon className="document-icon" />}
onClick={() => {
uploadFlow();
setModalOpen(false);
}}
textColor="text-almost-dark-blue "
title="Local File"
></ButtonBox>
</div>
)}
{showExamples && loadingExamples && (
<div className="loading-component-div">
<LoadingComponent remSize={30} />
</div>
)}
{showExamples &&
!loadingExamples &&
examples.map((example, index) => {
return (
<div key={example.name} className="m-2">
{" "}
<ButtonBox
size="small"
bgColor="bg-medium-emerald "
description={example.description ?? "Prebuilt Examples"}
icon={
<DocumentDuplicateIcon
strokeWidth={1.5}
className="h-6 w-6 flex-shrink-0"
/>
}
onClick={() => {
addFlow(example, false);
setModalOpen(false);
}}
textColor="text-medium-emerald "
title={example.name}
></ButtonBox>
</div>
);
})}
</div>
<DialogFooter>
<div className="dialog-modal-footer">
<a
href="https://github.com/logspace-ai/langflow_examples"
target="_blank"
className="dialog-modal-footer-link "
rel="noreferrer"
>
<svg
width="24"
viewBox="0 0 98 96"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z"
fill="currentColor"
/>
</svg>
<span className="ml-2 ">Langflow Examples</span>
</a>
</div>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View file

@ -54,7 +54,9 @@ export default function LoginAdminPage() {
.then((user) => {
setUserData(user);
})
.catch((error) => {});
.catch((error) => {
console.log("login admin page", error);
});
}, 1000);
}
}

View file

@ -63,7 +63,7 @@ export default function AdminPage() {
function getUsers() {
setLoadingUsers(true);
getUsersPage(index, size)
getUsersPage(index - 1, size)
.then((users) => {
setTotalRowsCount(users["total_count"]);
userList.current = users["users"];

View file

@ -94,7 +94,7 @@ export default function CommunityPage(): JSX.Element {
size="sm"
className="whitespace-nowrap "
onClick={() => {
addFlow(flow, true).then((id) => {
addFlow(true, flow).then((id) => {
navigate("/flow/" + id);
});
}}

View file

@ -14,6 +14,7 @@ export default function DisclosureComponent({
<div>
<Disclosure.Button className="components-disclosure-arrangement">
<div className="flex gap-4">
{/* BUG ON THIS ICON */}
<Icon strokeWidth={1.5} size={22} className="text-primary" />
<span className="components-disclosure-title">{title}</span>
</div>

View file

@ -33,11 +33,18 @@ import { TabsContext } from "../../../../contexts/tabsContext";
import { typesContext } from "../../../../contexts/typesContext";
import { undoRedoContext } from "../../../../contexts/undoRedoContext";
import { APIClassType } from "../../../../types/api";
import { FlowType, NodeType } from "../../../../types/flow";
import { FlowType, NodeType, targetHandleType } from "../../../../types/flow";
import { TabsState } from "../../../../types/tabs";
import { isValidConnection } from "../../../../utils/reactflowUtils";
import { isWrappedWithClass } from "../../../../utils/utils";
import {
generateFlow,
generateNodeFromFlow,
isValidConnection,
scapeJSONParse,
validateSelection,
} from "../../../../utils/reactflowUtils";
import { getRandomName, isWrappedWithClass } from "../../../../utils/utils";
import ConnectionLineComponent from "../ConnectionLineComponent";
import SelectionMenu from "../SelectionMenuComponent";
import ExtraSidebar from "../extraSidebarComponent";
const nodeTypes = {
@ -70,6 +77,8 @@ export default function Page({
setReactFlowInstance,
templates,
setFilterEdge,
deleteNode,
deleteEdge,
} = useContext(typesContext);
const reactFlowWrapper = useRef<HTMLDivElement>(null);
@ -112,6 +121,16 @@ export default function Page({
event.preventDefault();
}
}
if (!isWrappedWithClass(event, "nodelete")) {
if (
(event.key === "Delete" || event.key === "Backspace") &&
lastSelection
) {
event.preventDefault();
deleteNode(lastSelection.nodes.map((node) => node.id));
deleteEdge(lastSelection.edges.map((edge) => edge.id));
}
}
};
const handleMouseMove = (event) => {
setPosition({ x: event.clientX, y: event.clientY });
@ -160,6 +179,27 @@ export default function Page({
setExtraNavigation({ title: "Components" });
}, [setExtraComponent, setExtraNavigation]);
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds((prevSeconds) => {
let updatedSeconds = prevSeconds + 1;
if (updatedSeconds % 30 === 0) {
saveFlow(flow, true);
updatedSeconds = 0;
}
return updatedSeconds;
});
}, 1000);
return () => {
clearInterval(interval);
};
}, []);
const onEdgesChangeMod = useCallback(
(change: EdgeChange[]) => {
onEdgesChange(change);
@ -205,12 +245,19 @@ export default function Page({
addEdge(
{
...params,
data: {
targetHandle: scapeJSONParse(params.targetHandle!),
sourceHandle: scapeJSONParse(params.sourceHandle!),
},
style: { stroke: "#555" },
className:
(params.targetHandle?.split("|")[0] === "Text"
((scapeJSONParse(params.targetHandle!) as targetHandleType)
.type === "Text"
? "stroke-foreground "
: "stroke-foreground ") + " stroke-connection",
animated: params.targetHandle?.split("|")[0] === "Text",
animated:
(scapeJSONParse(params.targetHandle!) as targetHandleType)
.type === "Text",
},
eds
)
@ -304,7 +351,7 @@ export default function Page({
} else if (event.dataTransfer.types.some((types) => types === "Files")) {
takeSnapshot();
if (event.dataTransfer.files.item(0)!.type === "application/json") {
uploadFlow(true, event.dataTransfer.files.item(0)!);
uploadFlow(false, event.dataTransfer.files.item(0)!);
} else {
setErrorData({
title: "Invalid file type",
@ -361,7 +408,7 @@ export default function Page({
edgeUpdateSuccessful.current = true;
}, []);
const [selectionEnded, setSelectionEnded] = useState(false);
const [selectionEnded, setSelectionEnded] = useState(true);
const onSelectionEnd = useCallback(() => {
setSelectionEnded(true);
@ -401,7 +448,7 @@ export default function Page({
<div className="h-full w-full" ref={reactFlowWrapper}>
{Object.keys(templates).length > 0 &&
Object.keys(types).length > 0 ? (
<div className="h-full w-full">
<div id="react-flow-id" className="h-full w-full">
<ReactFlow
nodes={nodes}
onMove={() => {
@ -429,8 +476,9 @@ export default function Page({
connectionLineComponent={ConnectionLineComponent}
onDragOver={onDragOver}
onDrop={onDrop}
onNodesDelete={onDelete}
onSelectionChange={onSelectionChange}
onNodesDelete={onDelete}
deleteKeyCode={[]}
className="theme-attribution"
minZoom={0.01}
maxZoom={8}
@ -447,6 +495,50 @@ export default function Page({
[&>button]:border-b-border hover:[&>button]:bg-border"
></Controls>
)}
<SelectionMenu
isVisible={selectionMenuVisible}
nodes={lastSelection?.nodes}
onClick={() => {
if (
validateSelection(lastSelection!, edges).length === 0
) {
const { newFlow } = generateFlow(
lastSelection!,
reactFlowInstance!,
getRandomName()
);
const newGroupNode = generateNodeFromFlow(
newFlow,
getNodeId
);
setNodes((oldNodes) => [
...oldNodes.filter(
(oldNodes) =>
!lastSelection?.nodes.some(
(selectionNode) =>
selectionNode.id === oldNodes.id
)
),
newGroupNode,
]);
setEdges((oldEdges) =>
oldEdges.filter(
(oldEdge) =>
!lastSelection!.nodes.some(
(selectionNode) =>
selectionNode.id === oldEdge.target ||
selectionNode.id === oldEdge.source
)
)
);
} else {
setErrorData({
title: "Invalid selection",
list: validateSelection(lastSelection!, edges),
});
}
}}
/>
</ReactFlow>
{!view && (
<Chat flow={flow} reactFlowInstance={reactFlowInstance!} />

View file

@ -0,0 +1,55 @@
import { useEffect, useState } from "react";
import { NodeToolbar } from "reactflow";
import IconComponent from "../../../../components/genericIconComponent";
export default function SelectionMenu({ onClick, nodes, isVisible }) {
const [isOpen, setIsOpen] = useState(false);
const [isTransitioning, setIsTransitioning] = useState(false);
const [lastNodes, setLastNodes] = useState(nodes);
// nodes get saved to not be gone after the toolbar closes
useEffect(() => {
setLastNodes(nodes);
}, [isOpen]);
// transition starts after and ends before the toolbar closes
useEffect(() => {
if (isVisible) {
setIsOpen(true);
setTimeout(() => {
setIsTransitioning(true);
}, 50);
} else {
setIsTransitioning(false);
setTimeout(() => {
setIsOpen(false);
}, 500);
}
}, [isVisible]);
return (
<NodeToolbar
isVisible={isOpen}
offset={5}
nodeId={
lastNodes && lastNodes.length > 0 ? lastNodes.map((n) => n.id) : []
}
>
<div className="h-10 w-28 overflow-hidden">
<div
className={
"h-10 w-24 rounded-md border border-indigo-300 bg-white px-2.5 text-gray-700 shadow-inner transition-all duration-500 ease-in-out dark:bg-gray-800 dark:text-gray-300" +
(isTransitioning ? " translate-y-0" : " translate-y-10")
}
>
<button
className="flex h-full w-full items-center justify-between text-sm hover:text-indigo-500"
onClick={onClick}
>
<IconComponent name="Group" className="w-6" />
Group
</button>
</div>
</div>
</NodeToolbar>
);
}

View file

@ -81,9 +81,11 @@ export default function ExtraSidebar(): JSX.Element {
}, []);
function handleBlur() {
setFilterData(data);
setFilterEdge([]);
setSearch("");
if (!search && search === "") {
setFilterData(data);
setFilterEdge([]);
setSearch("");
}
}
useEffect(() => {
@ -139,7 +141,7 @@ export default function ExtraSidebar(): JSX.Element {
<button
className="extra-side-bar-buttons"
onClick={() => {
uploadFlow();
uploadFlow(false);
}}
>
<IconComponent name="FileUp" className="side-bar-button-size " />
@ -208,7 +210,7 @@ export default function ExtraSidebar(): JSX.Element {
name="search"
id="search"
placeholder="Search"
className="nopan nodrag noundo nocopy input-search"
className="nopan nodelete nodrag noundo nocopy input-search"
onChange={(event) => {
handleSearchInput(event.target.value);
// Set search input state

View file

@ -84,7 +84,7 @@ export default function SidebarDraggableComponent({
);
}}
>
<div className="side-bar-components-div-form">
<div id={display_name} className="side-bar-components-div-form">
<span className="side-bar-components-text">{display_name}</span>
<div>
<SelectTrigger>

View file

@ -16,6 +16,8 @@ import { nodeToolbarPropsType } from "../../../../types/components";
import {
createFlowComponent,
downloadNode,
expandGroupNode,
updateFlowPosition,
} from "../../../../utils/reactflowUtils";
import { classNames } from "../../../../utils/utils";
@ -23,6 +25,7 @@ export default function NodeToolbarComponent({
data,
setData,
deleteNode,
position,
setShowNode,
numberOfHandles,
showNode,
@ -54,6 +57,8 @@ export default function NodeToolbarComponent({
return true;
}
const isMinimal = canMinimize();
const isGroup = data.node?.flow ? true : false;
const { paste } = useContext(TabsContext);
const { saveComponent } = useContext(typesContext);
const reactFlowInstance = useReactFlow();
@ -81,6 +86,12 @@ export default function NodeToolbarComponent({
saveComponent(data, autoLogin ? "auto" : userData?.id!);
}
break;
case "disabled":
break;
case "ungroup":
updateFlowPosition(position, data.node?.flow!);
expandGroupNode(data, reactFlowInstance);
break;
}
};
@ -172,7 +183,7 @@ export default function NodeToolbarComponent({
</SelectTrigger>
</ShadTooltip>
<SelectContent>
<SelectItem value={"advanced"}>
<SelectItem value={nodeLength == 0 ? "disabled" : "advanced"}>
<div className="flex">
<IconComponent
name="Settings2"
@ -219,6 +230,17 @@ export default function NodeToolbarComponent({
</div>
</SelectItem>
)}
{isGroup && (
<SelectItem value="ungroup">
<div className="flex">
<IconComponent
name="Ungroup"
className="relative top-0.5 mr-2 h-4 w-4"
/>{" "}
Ungroup{" "}
</div>
</SelectItem>
)}
</SelectContent>
</Select>
@ -235,29 +257,6 @@ export default function NodeToolbarComponent({
<></>
</EditNodeModal>
)}
{/*
<ShadTooltip content="Edit" side="top">
<div>
<EditNodeModal
data={data}
setData={setData}
nodeLength={nodeLength}
>
<div
className={classNames(
"relative -ml-px inline-flex items-center bg-background px-2 py-2 text-foreground shadow-md ring-1 ring-inset ring-ring transition-all duration-500 ease-in-out hover:bg-muted focus:z-10" +
(!canMinimize() && " rounded-r-md ") +
(nodeLength == 0
? " text-muted-foreground"
: " text-foreground")
)}
>
<IconComponent name="Settings2" className="h-4 w-4 " />
</div>
</EditNodeModal>
</div>
</ShadTooltip> */}
</span>
</div>
</>

View file

@ -41,12 +41,16 @@ export default function HomePage(): JSX.Element {
const dragOver = (e) => {
e.preventDefault();
setIsDragging(true);
if (e.dataTransfer.types.some((types) => types === "Files")) {
setIsDragging(true);
}
};
const dragEnter = (e) => {
if (e.dataTransfer.types.some((types) => types === "Files")) {
setIsDragging(true);
}
e.preventDefault();
setIsDragging(true);
};
const dragLeave = () => {
@ -100,7 +104,7 @@ export default function HomePage(): JSX.Element {
<DropdownButton
firstButtonName="New Project"
onFirstBtnClick={() => {
addFlow(null!, true).then((id) => {
addFlow(true).then((id) => {
navigate("/flow/" + id);
});
}}

View file

@ -58,7 +58,9 @@ export default function LoginPage(): JSX.Element {
setIsAdmin(isSuperUser);
setUserData(user);
})
.catch((error) => {});
.catch((error) => {
console.log("login page", error);
});
}, 500);
}
}

View file

@ -87,7 +87,7 @@
@apply flex flex-col gap-2 p-2;
}
.side-bar-components-div-arrangement {
@apply w-full overflow-auto scrollbar-hide pb-10;
@apply w-full overflow-auto pb-10 scrollbar-hide;
}
.search-icon {
@apply absolute inset-y-0 right-0 flex items-center py-1.5 pr-5;
@ -127,7 +127,7 @@
}
.skeleton-card {
@apply bg-background h-48 p-4 border rounded-lg flex flex-col gap-6;
@apply flex h-48 flex-col gap-6 rounded-lg border bg-background p-4;
}
.skeleton-card-wrapper {
@ -205,7 +205,7 @@
}
.loading-page-panel {
@apply h-full w-full flex justify-center items-center bg-muted;
@apply flex h-full w-full items-center justify-center bg-muted;
}
.main-page-panel {
@ -350,7 +350,7 @@
@apply alert-icon text-status-green;
}
.success-alert-message {
@apply word-break-break-word alert-font-size text-success-foreground;
@apply alert-font-size text-success-foreground word-break-break-word;
}
.card-component-title-display {
@ -360,7 +360,7 @@
@apply flex h-7 w-7 items-center justify-center rounded-full text-2xl;
}
.card-component-title-size {
@apply w-full flex-1 word-break-break-word truncate-doubleline;
@apply w-full flex-1 truncate-doubleline word-break-break-word;
}
.card-component-delete-button {
@apply flex self-start;
@ -505,7 +505,7 @@
}
.header-arrangement {
@apply flex-max-width h-12 items-center justify-between border-border bg-muted;
@apply flex-max-width h-12 items-center justify-between border-border bg-background;
}
.header-start-display {
@apply flex w-[30%] items-center justify-start gap-2;
@ -520,7 +520,7 @@
@apply inline-flex h-9 items-center justify-center rounded-md border border-input px-3 pr-0 shadow-sm;
}
.header-waitlist-link-box {
@apply inline-flex h-9 items-center justify-center rounded-md border border-input px-2 shadow-sm text-sm font-medium text-muted-foreground ring-offset-background disabled:pointer-events-none disabled:opacity-50 whitespace-nowrap;
@apply inline-flex h-9 items-center justify-center whitespace-nowrap rounded-md border border-input px-2 text-sm font-medium text-muted-foreground shadow-sm ring-offset-background disabled:pointer-events-none disabled:opacity-50;
}
.header-waitlist-link-box:hover {
@apply hover:bg-accent hover:text-accent-foreground;
@ -853,7 +853,7 @@
}
.api-modal-tabs {
@apply flex h-full flex-col overflow-hidden rounded-md border bg-muted text-center sm:w-[75vw] lg:w-full;
@apply flex h-full max-w-full flex-col overflow-hidden rounded-md border bg-muted text-center sm:w-[75vw] md:w-[75vw] lg:w-[75vw] xl:w-[76vw] 2xl:w-full;
}
.api-modal-tablist-div {
@apply flex items-center justify-between px-2;
@ -882,11 +882,11 @@
@apply bg-white text-primary;
}
.code-highlight {
@apply block max-h-[64vh] w-full overflow-y-hidden word-break-break-word border-0 px-3 py-2 text-sm outline-0;
@apply block max-h-[64vh] w-full overflow-y-hidden border-0 px-3 py-2 text-sm outline-0 word-break-break-word;
}
.code-nohighlight {
@apply block max-h-[70vh] w-full overflow-y-hidden word-break-break-word border-0 px-3 py-2 text-sm outline-0;
@apply block max-h-[70vh] w-full overflow-y-hidden border-0 px-3 py-2 text-sm outline-0 word-break-break-word;
}
.form-modal-lockchat {
@apply form-input block w-full rounded-md border-border p-4 pr-16 custom-scroll focus:border-ring focus:ring-ring sm:text-sm;
@ -1050,15 +1050,15 @@
@apply rounded-md bg-indigo-100 px-0.5 dark:bg-indigo-900;
}
.field-invalid{
@apply font-medium text-[0.8rem] text-destructive absolute
.field-invalid {
@apply absolute text-[0.8rem] font-medium text-destructive;
}
.label-invalid{
@apply text-destructive
.label-invalid {
@apply text-destructive;
}
.input-invalid{
@apply border-destructive focus:ring-destructive focus:border-destructive
.input-invalid {
@apply border-destructive focus:border-destructive focus:ring-destructive;
}
}

View file

@ -1,12 +1,17 @@
import { Edge, Node, Viewport } from "reactflow";
import { FlowType } from "../flow";
//kind and class are just representative names to represent the actual structure of the object received by the API
export type APIDataType = { [key: string]: APIKindType };
export type APIObjectType = { kind: APIKindType; [key: string]: APIKindType };
export type APIKindType = { class: APIClassType; [key: string]: APIClassType };
export type APITemplateType = {
variable: TemplateVariableType;
[key: string]: TemplateVariableType;
};
export type CustomFieldsType = {
[key: string]: Array<string>;
};
export type APIClassType = {
base_classes: Array<string>;
description: string;
@ -14,11 +19,21 @@ export type APIClassType = {
display_name: string;
input_types?: Array<string>;
output_types?: Array<string>;
custom_fields?: CustomFieldsType;
beta?: boolean;
documentation: string;
error?: string;
official?: boolean;
[key: string]: Array<string> | string | APITemplateType | boolean | undefined;
flow?: FlowType;
[key: string]:
| Array<string>
| string
| APITemplateType
| boolean
| FlowType
| CustomFieldsType
| boolean
| undefined;
};
export type TemplateVariableType = {
@ -27,8 +42,11 @@ export type TemplateVariableType = {
placeholder?: string;
list: boolean;
show: boolean;
readonly: boolean;
multiline?: boolean;
value?: any;
dynamic?: boolean;
proxy?: { id: string; field: string };
input_types?: Array<string>;
[key: string]: any;
};
@ -39,7 +57,7 @@ export type sendAllProps = {
description: string;
viewport: Viewport;
inputs: { text?: string };
chatKey: string;
chatHistory: { message: string | object; isSend: boolean }[];
};
export type errorsTypeAPI = {

View file

@ -8,5 +8,6 @@ export type ChatMessageType = {
isSend: boolean;
thought?: string;
files?: Array<{ data: string; type: string; data_type: string }>;
prompt?: string;
chatKey: string;
};

View file

@ -1,11 +1,13 @@
import { ReactElement, ReactNode } from "react";
import { ReactFlowJsonObject } from "reactflow";
import { ReactFlowJsonObject, XYPosition } from "reactflow";
import { APIClassType, APITemplateType, TemplateVariableType } from "../api";
import { ChatMessageType } from "../chat";
import { FlowStyleType, FlowType, NodeDataType, NodeType } from "../flow/index";
import { typesContextType } from "../typesContext";
import { sourceHandleType, targetHandleType } from "./../flow/index";
export type InputComponentType = {
autoFocus?: boolean;
onBlur?: (event: React.FocusEvent<HTMLInputElement>) => void;
value: string;
disabled?: boolean;
onChange: (value: string) => void;
@ -17,12 +19,14 @@ export type InputComponentType = {
showPass?: boolean;
placeholder?: string;
className?: string;
blurOnEnter?: boolean;
};
export type ToggleComponentType = {
enabled: boolean;
setEnabled: (state: boolean) => void;
disabled: boolean | undefined;
size: "small" | "medium" | "large";
id?: string;
};
export type DropDownComponentType = {
value: string;
@ -36,7 +40,7 @@ export type ParameterComponentType = {
data: NodeDataType;
setData: (value: NodeDataType) => void;
title: string;
id: string;
id: sourceHandleType | targetHandleType;
color: string;
left: boolean;
type: string | undefined;
@ -46,6 +50,7 @@ export type ParameterComponentType = {
dataContext?: typesContextType;
optionalHandle?: Array<String> | null;
info?: string;
proxy?: { field: string; id: string };
showNode?: boolean;
};
export type InputListComponentType = {
@ -61,6 +66,8 @@ export type KeyPairListComponentType = {
disabled: boolean;
editNode?: boolean;
duplicateKey?: boolean;
advanced?: boolean | null;
dataValue?: any;
};
export type DictComponentType = {
@ -78,6 +85,7 @@ export type TextAreaComponentType = {
onChange: (value: string[] | string) => void;
value: string;
editNode?: boolean;
readonly?: boolean;
};
export type PromptAreaComponentType = {
@ -87,6 +95,7 @@ export type PromptAreaComponentType = {
disabled: boolean;
onChange: (value: string[] | string) => void;
value: string;
readonly?: boolean;
editNode?: boolean;
};
@ -98,6 +107,7 @@ export type CodeAreaComponentType = {
nodeClass?: APIClassType;
setNodeClass?: (value: APIClassType) => void;
dynamic?: boolean;
readonly?: boolean;
};
export type FileComponentType = {
@ -424,6 +434,7 @@ export type nodeToolbarPropsType = {
data: NodeDataType;
deleteNode: (idx: string) => void;
setData: (newState: NodeDataType) => void;
position: XYPosition;
setShowNode: (boolean: any) => void;
numberOfHandles: boolean[] | [];
showNode: boolean;
@ -459,6 +470,7 @@ export type codeAreaModalPropsType = {
setNodeClass: (Class: APIClassType) => void | undefined;
children: ReactNode;
dynamic?: boolean;
readonly?: boolean;
};
export type chatMessagePropsType = {
@ -483,6 +495,7 @@ export type genericModalPropsType = {
nodeClass?: APIClassType;
setNodeClass?: (Class: APIClassType) => void;
children: ReactNode;
readonly?: boolean;
};
export type buttonBoxPropsType = {
@ -549,7 +562,7 @@ export type codeTabsPropsType = {
) => string;
buildTweakObject?: (
tw: string,
changes: string | string[] | boolean | number,
changes: string | string[] | boolean | number | Object[] | Object,
template: TemplateVariableType
) => string | void;
};

View file

@ -14,12 +14,14 @@ export type NodeType = {
type?: string;
position: XYPosition;
data: NodeDataType;
selected?: boolean;
};
export type NodeDataType = {
type: string;
node?: APIClassType;
id: string;
output_types?: string[];
};
// FlowStyleType is the type of the style object that is used to style the
// Flow card with an emoji and a color.
@ -36,3 +38,18 @@ export type TweaksType = Array<
};
} & FlowStyleType
>;
// right side
export type sourceHandleType = {
dataType: string;
id: string;
baseClasses: string[];
};
//left side
export type targetHandleType = {
inputTypes?: string[];
type: string;
fieldName: string;
id: string;
proxy?: { field: string; id: string };
};

View file

@ -2,16 +2,15 @@ import { tweakType } from "../components";
import { FlowType } from "../flow";
export type TabsContextType = {
saveFlow: (flow: FlowType) => Promise<void>;
save: () => void;
saveFlow: (flow: FlowType, silent?: boolean) => Promise<void>;
tabId: string;
isLoading: boolean;
setTabId: (index: string) => void;
flows: Array<FlowType>;
removeFlow: (id: string) => void;
addFlow: (
flow?: FlowType,
newProject?: Boolean
newProject: boolean,
flow?: FlowType
) => Promise<String | undefined>;
updateFlow: (newFlow: FlowType) => void;
incrementNodeId: () => string;
@ -24,7 +23,7 @@ export type TabsContextType = {
uploadFlows: () => void;
isBuilt: boolean;
setIsBuilt: (state: boolean) => void;
uploadFlow: (newFlow?: boolean, file?: File) => Promise<String | undefined>;
uploadFlow: (newFlow: boolean, file?: File) => Promise<String | undefined>;
hardReset: () => void;
getNodeId: (nodeType: string) => string;
tabsState: TabsState;

View file

@ -10,7 +10,7 @@ const data: { [char: string]: string } = {};
export type typesContextType = {
reactFlowInstance: ReactFlowInstance | null;
setReactFlowInstance: (newState: ReactFlowInstance) => void;
deleteNode: (idx: string) => void;
deleteNode: (idx: string | Array<string>) => void;
types: typeof types;
setTypes: (newState: {}) => void;
templates: typeof template;
@ -21,6 +21,7 @@ export type typesContextType = {
setFetchError: (newState: boolean) => void;
setFilterEdge: (newState) => void;
getFilterEdge: any[];
deleteEdge: (idx: string | Array<string>) => void;
saveComponent: (component: NodeDataType, key: string) => void;
deleteComponent: (id: string, key: string) => void;
};

View file

@ -1,5 +1,5 @@
import { Edge, Node } from "reactflow";
import { NodeType } from "../flow";
import { FlowType, NodeType } from "../flow";
export type cleanEdgesType = {
flow: {
@ -11,5 +11,17 @@ export type cleanEdgesType = {
export type unselectAllNodesType = {
updateNodes: (nodes: Node[]) => void;
data: Node[] | null;
data: Node[];
};
export type updateEdgesHandleIdsType = {
nodes: NodeType[];
edges: Edge[];
};
export type generateFlowType = { newFlow: FlowType; removedEdges: Edge[] };
export type findLastNodeType = {
nodes: NodeType[];
edges: Edge[];
};

View file

@ -3,17 +3,30 @@ import {
Connection,
Edge,
Node,
OnSelectionChangeParams,
ReactFlowInstance,
ReactFlowJsonObject,
XYPosition,
} from "reactflow";
import ShortUniqueId from "short-unique-id";
import { specialCharsRegex } from "../constants/constants";
import { APITemplateType } from "../types/api";
import { FlowType, NodeDataType, NodeType } from "../types/flow";
import { APITemplateType, TemplateVariableType } from "../types/api";
import {
FlowType,
NodeDataType,
NodeType,
sourceHandleType,
targetHandleType,
} from "../types/flow";
import {
cleanEdgesType,
findLastNodeType,
generateFlowType,
unselectAllNodesType,
updateEdgesHandleIdsType,
} from "../types/utils/reactflowUtils";
import { toNormalCase } from "./utils";
import { toNormalCase, toTitleCase } from "./utils";
const uid = new ShortUniqueId({ length: 5 });
export function cleanEdges({
flow: { edges, nodes },
@ -32,26 +45,30 @@ export function cleanEdges({
const sourceHandle = edge.sourceHandle; //right
const targetHandle = edge.targetHandle; //left
if (targetHandle) {
const field = targetHandle.split("|")[1];
const id =
(targetNode.data.node?.template[field]?.input_types?.join(";") ??
targetNode.data.node?.template[field]?.type) +
"|" +
field +
"|" +
targetNode.data.id;
if (id !== targetHandle) {
const targetHandleObject: targetHandleType =
scapeJSONParse(targetHandle);
const field = targetHandleObject.fieldName;
const id: targetHandleType = {
type: targetNode.data.node!.template[field]?.type,
fieldName: field,
id: targetNode.data.id,
inputTypes: targetNode.data.node!.template[field]?.input_types,
};
if (targetNode.data.node!.template[field]?.proxy) {
id.proxy = targetNode.data.node!.template[field]?.proxy;
}
if (scapedJSONStringfy(id) !== targetHandle) {
newEdges = newEdges.filter((e) => e.id !== edge.id);
}
}
if (sourceHandle) {
const id = [
sourceNode.data.type,
sourceNode.data.id,
...sourceNode.data.node?.base_classes!,
].join("|");
if (id !== sourceHandle) {
newEdges = newEdges.filter((edg) => edg.id !== edge.id);
const id: sourceHandleType = {
id: sourceNode.data.id,
baseClasses: sourceNode.data.node!.base_classes,
dataType: sourceNode.data.type,
};
if (scapedJSONStringfy(id) !== sourceHandle) {
newEdges = newEdges.filter((e) => e.id !== edge.id);
}
}
}
@ -61,7 +78,7 @@ export function cleanEdges({
export function unselectAllNodes({ updateNodes, data }: unselectAllNodesType) {
let newNodes = _.cloneDeep(data);
newNodes!.forEach((node: Node) => {
newNodes.forEach((node: Node) => {
node.selected = false;
});
updateNodes(newNodes!);
@ -71,21 +88,18 @@ export function isValidConnection(
{ source, target, sourceHandle, targetHandle }: Connection,
reactFlowInstance: ReactFlowInstance
) {
const targetHandleObject: targetHandleType = scapeJSONParse(targetHandle!);
const sourceHandleObject: sourceHandleType = scapeJSONParse(sourceHandle!);
if (
targetHandle
?.split("|")[0]
.split(";")
.some((target) => target === sourceHandle?.split("|")[0]) ||
sourceHandle
?.split("|")
.slice(2)
.some((target) =>
targetHandle
?.split("|")[0]
.split(";")
.some((n) => n === target)
) ||
targetHandle?.split("|")[0] === "str"
targetHandleObject.inputTypes?.some(
(n) => n === sourceHandleObject.dataType
) ||
sourceHandleObject.baseClasses.some(
(t) =>
targetHandleObject.inputTypes?.some((n) => n === t) ||
t === targetHandleObject.type
) ||
targetHandleObject.type === "str"
) {
let targetNode = reactFlowInstance?.getNode(target!)?.data?.node;
if (!targetNode) {
@ -97,11 +111,11 @@ export function isValidConnection(
return true;
}
} else if (
(!targetNode.template[targetHandle?.split("|")[1]!].list &&
(!targetNode.template[targetHandleObject.fieldName].list &&
!reactFlowInstance
.getEdges()
.find((e) => e.targetHandle === targetHandle)) ||
targetNode.template[targetHandle?.split("|")[1]!].list
targetNode.template[targetHandleObject.fieldName].list
) {
return true;
}
@ -152,26 +166,36 @@ export function updateIds(
newFlow.nodes.forEach((node: NodeType) => {
// Generate a unique node ID
let newId = getNodeId(node.data.type);
let newId = getNodeId(node.data.node?.flow ? "GroupNode" : node.data.type);
idsMap[node.id] = newId;
node.id = newId;
node.data.id = newId;
// Add the new node to the list of nodes in state
});
newFlow.edges.forEach((edge) => {
newFlow.edges.forEach((edge: Edge) => {
edge.source = idsMap[edge.source];
edge.target = idsMap[edge.target];
let sourceHandleSplitted = edge.sourceHandle!.split("|");
edge.sourceHandle =
sourceHandleSplitted[0] +
"|" +
edge.source +
"|" +
sourceHandleSplitted.slice(2).join("|");
let targetHandleSplitted = edge.targetHandle!.split("|");
edge.targetHandle =
targetHandleSplitted.slice(0, -1).join("|") + "|" + edge.target;
const sourceHandleObject: sourceHandleType = scapeJSONParse(
edge.sourceHandle!
);
edge.sourceHandle = scapedJSONStringfy({
...sourceHandleObject,
id: edge.source,
});
if (edge.data?.sourceHandle?.id) {
edge.data.sourceHandle.id = edge.source;
}
const targetHandleObject: targetHandleType = scapeJSONParse(
edge.targetHandle!
);
edge.targetHandle = scapedJSONStringfy({
...targetHandleObject,
id: edge.target,
});
if (edge.data?.targetHandle?.id) {
edge.data.targetHandle.id = edge.target;
}
edge.id =
"reactflow__edge-" +
edge.source +
@ -189,10 +213,7 @@ export function buildTweaks(flow: FlowType) {
}, {});
}
export function validateNode(
node: NodeType,
reactFlowInstance: ReactFlowInstance
): Array<string> {
export function validateNode(node: NodeType, edges: Edge[]): Array<string> {
if (!node.data?.node?.template || !Object.keys(node.data.node.template)) {
return [
"We've noticed a potential issue with a node in the flow. Please review it and, if necessary, submit a bug report with your exported flow file. Thank you for your help!",
@ -211,13 +232,13 @@ export function validateNode(
(template[t].value === undefined ||
template[t].value === null ||
template[t].value === "") &&
!reactFlowInstance
.getEdges()
.some(
(edge) =>
edge.targetHandle?.split("|")[1] === t &&
edge.targetHandle.split("|")[2] === node.id
)
!edges.some(
(edge) =>
(scapeJSONParse(edge.targetHandle!) as targetHandleType).fieldName ===
t &&
(scapeJSONParse(edge.targetHandle!) as targetHandleType).id ===
node.id
)
) {
errors.push(
`${type} is missing ${
@ -249,15 +270,13 @@ export function validateNode(
}, [] as string[]);
}
export function validateNodes(reactFlowInstance: ReactFlowInstance) {
if (reactFlowInstance.getNodes().length === 0) {
export function validateNodes(nodes: Node[], edges: Edge[]) {
if (nodes.length === 0) {
return [
"No nodes found in the flow. Please add at least one node to the flow.",
];
}
return reactFlowInstance
.getNodes()
.flatMap((n: NodeType) => validateNode(n, reactFlowInstance));
return nodes.flatMap((n: NodeType) => validateNode(n, edges));
}
export function addVersionToDuplicates(flow: FlowType, flows: FlowType[]) {
@ -273,6 +292,49 @@ export function addVersionToDuplicates(flow: FlowType, flows: FlowType[]) {
return newName;
}
export function updateEdgesHandleIds({
edges,
nodes,
}: updateEdgesHandleIdsType): Edge[] {
let newEdges = _.cloneDeep(edges);
newEdges.forEach((edge) => {
const sourceNodeId = edge.source;
const targetNodeId = edge.target;
const sourceNode = nodes.find((node) => node.id === sourceNodeId);
const targetNode = nodes.find((node) => node.id === targetNodeId);
let source = edge.sourceHandle;
let target = edge.targetHandle;
//right
let newSource: sourceHandleType;
//left
let newTarget: targetHandleType;
if (target && targetNode) {
let field = target.split("|")[1];
newTarget = {
type: targetNode.data.node!.template[field].type,
fieldName: field,
id: targetNode.data.id,
inputTypes: targetNode.data.node!.template[field].input_types,
};
}
if (source && sourceNode) {
newSource = {
id: sourceNode.data.id,
baseClasses: sourceNode.data.node!.base_classes,
dataType: sourceNode.data.type,
};
}
edge.sourceHandle = scapedJSONStringfy(newSource!);
edge.targetHandle = scapedJSONStringfy(newTarget!);
const newData = {
sourceHandle: scapeJSONParse(edge.sourceHandle),
targetHandle: scapeJSONParse(edge.targetHandle),
};
edge.data = newData;
});
return newEdges;
}
export function handleKeyDown(
e:
| React.KeyboardEvent<HTMLInputElement>
@ -311,20 +373,39 @@ export function getConnectedNodes(
return nodes.filter((node) => node.id === targetId || node.id === sourceId);
}
export function convertObjToArray(singleObject) {
export function convertObjToArray(singleObject: object | string) {
if (typeof singleObject === "string") {
singleObject = JSON.parse(singleObject);
}
if (Array.isArray(singleObject)) return singleObject;
let arrConverted: any = [];
for (const key in singleObject) {
if (singleObject.hasOwnProperty(key)) {
const newObj = {};
newObj[key] = singleObject[key];
arrConverted.push(newObj);
let arrConverted: any[] = [];
if (typeof singleObject === "object") {
for (const key in singleObject) {
if (Object.prototype.hasOwnProperty.call(singleObject, key)) {
const newObj = {};
newObj[key] = singleObject[key];
arrConverted.push(newObj);
}
}
}
return arrConverted;
}
export function convertArrayToObj(arrayOfObjects) {
if (!Array.isArray(arrayOfObjects)) return arrayOfObjects;
let objConverted = {};
for (const obj of arrayOfObjects) {
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
objConverted[key] = obj[key];
}
}
}
return objConverted;
}
export function hasDuplicateKeys(array) {
const keys = {};
for (const obj of array) {
@ -366,6 +447,596 @@ export function convertValuesToNumbers(arr) {
});
}
export function scapedJSONStringfy(json: object): string {
return customStringify(json).replace(/"/g, "œ");
}
export function scapeJSONParse(json: string): any {
let parsed = json.replace(/œ/g, '"');
return JSON.parse(parsed);
}
// this function receives an array of edges and return true if any of the handles are not a json string
export function checkOldEdgesHandles(edges: Edge[]): boolean {
return edges.some(
(edge) =>
!edge.sourceHandle ||
!edge.targetHandle ||
!edge.sourceHandle.includes("{") ||
!edge.targetHandle.includes("{")
);
}
export function customStringify(obj: any): string {
if (typeof obj === "undefined") {
return "null";
}
if (obj === null || typeof obj !== "object") {
if (obj instanceof Date) {
return `"${obj.toISOString()}"`;
}
return JSON.stringify(obj);
}
if (Array.isArray(obj)) {
const arrayItems = obj.map((item) => customStringify(item)).join(",");
return `[${arrayItems}]`;
}
const keys = Object.keys(obj).sort();
const keyValuePairs = keys.map(
(key) => `"${key}":${customStringify(obj[key])}`
);
return `{${keyValuePairs.join(",")}}`;
}
export function getMiddlePoint(nodes: Node[]) {
let middlePointX = 0;
let middlePointY = 0;
nodes.forEach((node) => {
middlePointX += node.position.x;
middlePointY += node.position.y;
});
const totalNodes = nodes.length;
const averageX = middlePointX / totalNodes;
const averageY = middlePointY / totalNodes;
return { x: averageX, y: averageY };
}
export function generateFlow(
selection: OnSelectionChangeParams,
reactFlowInstance: ReactFlowInstance,
name: string
): generateFlowType {
const newFlowData = reactFlowInstance.toObject();
/* remove edges that are not connected to selected nodes on both ends
in future we can save this edges to when ungrouping reconect to the old nodes
*/
newFlowData.edges = selection.edges.filter(
(edge) =>
selection.nodes.some((node) => node.id === edge.target) &&
selection.nodes.some((node) => node.id === edge.source)
);
newFlowData.nodes = selection.nodes;
const newFlow: FlowType = {
data: newFlowData,
name: name,
description: "",
//generating local id instead of using the id from the server, can change in the future
id: uid(),
};
// filter edges that are not connected to selected nodes on both ends
// using O(n²) aproach because the number of edges is small
// in the future we can use a better aproach using a set
return {
newFlow,
removedEdges: selection.edges.filter(
(edge) => !newFlowData.edges.includes(edge)
),
};
}
export function filterFlow(
selection: OnSelectionChangeParams,
reactFlowInstance: ReactFlowInstance
) {
reactFlowInstance.setNodes((nodes) =>
nodes.filter((node) => !selection.nodes.includes(node))
);
reactFlowInstance.setEdges((edges) =>
edges.filter((edge) => !selection.edges.includes(edge))
);
}
export function findLastNode({ nodes, edges }: findLastNodeType) {
/*
this function receives a flow and return the last node
*/
let lastNode = nodes.find((n) => !edges.some((e) => e.source === n.id));
return lastNode;
}
export function updateFlowPosition(NewPosition: XYPosition, flow: FlowType) {
const middlePoint = getMiddlePoint(flow.data!.nodes);
let deltaPosition = {
x: NewPosition.x - middlePoint.x,
y: NewPosition.y - middlePoint.y,
};
flow.data!.nodes.forEach((node) => {
node.position.x += deltaPosition.x;
node.position.y += deltaPosition.y;
});
}
export function concatFlows(
flow: FlowType,
ReactFlowInstance: ReactFlowInstance
) {
const { nodes, edges } = flow.data!;
ReactFlowInstance.addNodes(nodes);
ReactFlowInstance.addEdges(edges);
}
export function validateSelection(
selection: OnSelectionChangeParams,
edges: Edge[]
): Array<string> {
//add edges to selection if selection mode selected only nodes
if (selection.edges.length === 0) {
selection.edges = edges;
}
// get only edges that are connected to the nodes in the selection
// first creates a set of all the nodes ids
let nodesSet = new Set(selection.nodes.map((n) => n.id));
// then filter the edges that are connected to the nodes in the set
let connectedEdges = selection.edges.filter(
(e) => nodesSet.has(e.source) && nodesSet.has(e.target)
);
// add the edges to the selection
selection.edges = connectedEdges;
let errorsArray: Array<string> = [];
// check if there is more than one node
if (selection.nodes.length < 2) {
errorsArray.push("Please select more than one node");
}
//check if there are two or more nodes with free outputs
if (
selection.nodes.filter(
(n) => !selection.edges.some((e) => e.source === n.id)
).length > 1
) {
errorsArray.push("Please select only one node with free outputs");
}
// check if there is any node that does not have any connection
if (
selection.nodes.some(
(node) =>
!selection.edges.some((edge) => edge.target === node.id) &&
!selection.edges.some((edge) => edge.source === node.id)
)
) {
errorsArray.push("Please select only nodes that are connected");
}
return errorsArray;
}
function updateGroupNodeTemplate(template: APITemplateType) {
/*this function receives a template, iterates for it's items
updating the visibility of all basic types setting it to advanced true*/
Object.keys(template).forEach((key) => {
let type = template[key].type;
let input_types = template[key].input_types;
if (
(type === "str" ||
type === "bool" ||
type === "float" ||
type === "code" ||
type === "prompt" ||
type === "file" ||
type === "int" ||
type === "dict" ||
type === "NestedDict") &&
!template[key].required &&
!input_types
) {
template[key].advanced = true;
}
//prevent code fields from showing on the group node
if (type === "code") {
template[key].show = false;
}
});
return template;
}
export function mergeNodeTemplates({
nodes,
edges,
}: {
nodes: NodeType[];
edges: Edge[];
}): APITemplateType {
/* this function receives a flow and iterate throw each node
and merge the templates with only the visible fields
if there are two keys with the same name in the flow, we will update the display name of each one
to show from which node it came from
*/
let template: APITemplateType = {};
nodes.forEach((node) => {
let nodeTemplate = _.cloneDeep(node.data.node!.template);
Object.keys(nodeTemplate)
.filter((field_name) => field_name.charAt(0) !== "_")
.forEach((key) => {
if (!isHandleConnected(edges, key, nodeTemplate[key], node.id)) {
template[key + "_" + node.id] = nodeTemplate[key];
template[key + "_" + node.id].proxy = { id: node.id, field: key };
if (node.type === "groupNode") {
template[key + "_" + node.id].display_name =
node.data.node!.flow!.name + " - " + nodeTemplate[key].name;
} else {
template[key + "_" + node.id].display_name =
//data id already has the node name on it
nodeTemplate[key].display_name
? nodeTemplate[key].display_name
: nodeTemplate[key].name
? toTitleCase(nodeTemplate[key].name)
: toTitleCase(key);
}
}
});
});
return template;
}
function isHandleConnected(
edges: Edge[],
key: string,
field: TemplateVariableType,
nodeId: string
) {
/*
this function receives a flow and a handleId and check if there is a connection with this handle
*/
scapedJSONStringfy({ type: field.type, fieldName: key, id: nodeId });
if (field.proxy) {
if (
edges.some(
(e) =>
e.targetHandle ===
scapedJSONStringfy({
type: field.type,
fieldName: key,
id: nodeId,
proxy: { id: field.proxy!.id, field: field.proxy!.field },
inputTypes: field.input_types,
} as targetHandleType)
)
) {
return true;
}
} else {
if (
edges.some(
(e) =>
e.targetHandle ===
scapedJSONStringfy({
type: field.type,
fieldName: key,
id: nodeId,
inputTypes: field.input_types,
} as targetHandleType)
)
) {
return true;
}
}
return false;
}
export function generateNodeTemplate(Flow: FlowType) {
/*
this function receives a flow and generate a template for the group node
*/
let template = mergeNodeTemplates({
nodes: Flow.data!.nodes,
edges: Flow.data!.edges,
});
updateGroupNodeTemplate(template);
return template;
}
export function generateNodeFromFlow(
flow: FlowType,
getNodeId: (type: string) => string
): NodeType {
const { nodes } = flow.data!;
const outputNode = _.cloneDeep(findLastNode(flow.data!));
const position = getMiddlePoint(nodes);
let data = _.cloneDeep(flow);
const id = getNodeId(outputNode?.data.type!);
const newGroupNode: NodeType = {
data: {
id,
type: outputNode?.data.type!,
node: {
output_types: outputNode!.data.node!.output_types,
display_name: "Group",
documentation: "",
base_classes: outputNode!.data.node!.base_classes,
description: "double click to edit description",
template: generateNodeTemplate(data),
flow: data,
},
},
id,
position,
type: "genericNode",
};
return newGroupNode;
}
export function connectedInputNodesOnHandle(
nodeId: string,
handleId: string,
{ nodes, edges }: { nodes: NodeType[]; edges: Edge[] }
) {
const connectedNodes: Array<{ name: string; id: string; isGroup: boolean }> =
[];
// return the nodes connected to the input handle of the node
const TargetEdges = edges.filter((e) => e.target === nodeId);
TargetEdges.forEach((edge) => {
if (edge.targetHandle === handleId) {
const sourceNode = nodes.find((n) => n.id === edge.source);
if (sourceNode) {
if (sourceNode.type === "groupNode") {
let lastNode = findLastNode(sourceNode.data.node!.flow!.data!);
while (lastNode && lastNode.type === "groupNode") {
lastNode = findLastNode(lastNode.data.node!.flow!.data!);
}
if (lastNode) {
connectedNodes.push({
name: sourceNode.data.node!.flow!.name,
id: lastNode.id,
isGroup: true,
});
}
} else {
connectedNodes.push({
name: sourceNode.data.type,
id: sourceNode.id,
isGroup: false,
});
}
}
}
});
return connectedNodes;
}
export function ungroupNode(
groupNode: NodeDataType,
BaseFlow: ReactFlowJsonObject
) {
const { template, flow } = groupNode.node!;
const gNodes: NodeType[] = flow!.data!.nodes;
const gEdges = flow!.data!.edges;
//redirect edges to correct proxy node
let updatedEdges: Edge[] = [];
BaseFlow.edges.forEach((edge) => {
let newEdge = _.cloneDeep(edge);
if (newEdge.target === groupNode.id) {
const targetHandle: targetHandleType = newEdge.data.targetHandle;
if (targetHandle.proxy) {
let type = targetHandle.type;
let field = targetHandle.proxy.field;
let proxyId = targetHandle.proxy.id;
let inputTypes = targetHandle.inputTypes;
let node: NodeType = gNodes.find((n) => n.id === proxyId)!;
if (node) {
newEdge.target = proxyId;
let newTargetHandle: targetHandleType = {
fieldName: field,
type,
id: proxyId,
inputTypes: inputTypes,
};
if (node.data.node?.flow) {
newTargetHandle.proxy = {
field: node.data.node.template[field].proxy?.field!,
id: node.data.node.template[field].proxy?.id!,
};
}
newEdge.data.targetHandle = newTargetHandle;
newEdge.targetHandle = scapedJSONStringfy(newTargetHandle);
}
}
}
if (newEdge.source === groupNode.id) {
const lastNode = _.cloneDeep(findLastNode(flow!.data!));
newEdge.source = lastNode!.id;
let newSourceHandle: sourceHandleType = scapeJSONParse(
newEdge.sourceHandle!
);
newSourceHandle.id = lastNode!.id;
newEdge.data.sourceHandle = newSourceHandle;
newEdge.sourceHandle = scapedJSONStringfy(newSourceHandle);
}
if (edge.target === groupNode.id || edge.source === groupNode.id) {
updatedEdges.push(newEdge);
}
});
//update template values
Object.keys(template).forEach((key) => {
let { field, id } = template[key].proxy!;
let nodeIndex = gNodes.findIndex((n) => n.id === id);
if (nodeIndex !== -1) {
let display_name: string;
let show = gNodes[nodeIndex].data.node!.template[field].show;
let advanced = gNodes[nodeIndex].data.node!.template[field].advanced;
if (gNodes[nodeIndex].data.node!.template[field].display_name) {
display_name =
gNodes[nodeIndex].data.node!.template[field].display_name;
} else {
display_name = gNodes[nodeIndex].data.node!.template[field].name;
}
gNodes[nodeIndex].data.node!.template[field] = template[key];
gNodes[nodeIndex].data.node!.template[field].show = show;
gNodes[nodeIndex].data.node!.template[field].advanced = advanced;
gNodes[nodeIndex].data.node!.template[field].display_name = display_name;
}
});
const nodes = [
...BaseFlow.nodes.filter((n) => n.id !== groupNode.id),
...gNodes,
];
const edges = [
...BaseFlow.edges.filter(
(e) => e.target !== groupNode.id && e.source !== groupNode.id
),
...gEdges,
...updatedEdges,
];
BaseFlow.nodes = nodes;
BaseFlow.edges = edges;
}
export function expandGroupNode(
groupNode: NodeDataType,
ReactFlowInstance: ReactFlowInstance
) {
const { template, flow } = _.cloneDeep(groupNode.node!);
const gNodes: NodeType[] = flow?.data?.nodes!;
const gEdges = flow!.data!.edges;
console.log(gEdges);
//redirect edges to correct proxy node
let updatedEdges: Edge[] = [];
ReactFlowInstance.getEdges().forEach((edge) => {
let newEdge = _.cloneDeep(edge);
if (newEdge.target === groupNode.id) {
const targetHandle: targetHandleType = newEdge.data.targetHandle;
if (targetHandle.proxy) {
let type = targetHandle.type;
let field = targetHandle.proxy.field;
let proxyId = targetHandle.proxy.id;
let inputTypes = targetHandle.inputTypes;
let node: NodeType = gNodes.find((n) => n.id === proxyId)!;
if (node) {
newEdge.target = proxyId;
let newTargetHandle: targetHandleType = {
fieldName: field,
type,
id: proxyId,
inputTypes: inputTypes,
};
if (node.data.node?.flow) {
newTargetHandle.proxy = {
field: node.data.node.template[field].proxy?.field!,
id: node.data.node.template[field].proxy?.id!,
};
}
newEdge.data.targetHandle = newTargetHandle;
newEdge.targetHandle = scapedJSONStringfy(newTargetHandle);
}
}
}
if (newEdge.source === groupNode.id) {
const lastNode = _.cloneDeep(findLastNode(flow!.data!));
newEdge.source = lastNode!.id;
let newSourceHandle: sourceHandleType = scapeJSONParse(
newEdge.sourceHandle!
);
newSourceHandle.id = lastNode!.id;
newEdge.data.sourceHandle = newSourceHandle;
newEdge.sourceHandle = scapedJSONStringfy(newSourceHandle);
}
if (edge.target === groupNode.id || edge.source === groupNode.id) {
updatedEdges.push(newEdge);
}
});
//update template values
Object.keys(template).forEach((key) => {
let { field, id } = template[key].proxy!;
let nodeIndex = gNodes.findIndex((n) => n.id === id);
if (nodeIndex !== -1) {
let proxy: { id: string; field: string } | undefined;
let display_name: string;
let show = gNodes[nodeIndex].data.node!.template[field].show;
let advanced = gNodes[nodeIndex].data.node!.template[field].advanced;
if (gNodes[nodeIndex].data.node!.template[field].display_name) {
display_name =
gNodes[nodeIndex].data.node!.template[field].display_name;
} else {
display_name = gNodes[nodeIndex].data.node!.template[field].name;
}
if (gNodes[nodeIndex].data.node!.template[field].proxy) {
proxy = gNodes[nodeIndex].data.node!.template[field].proxy;
}
gNodes[nodeIndex].data.node!.template[field] = template[key];
gNodes[nodeIndex].data.node!.template[field].show = show;
gNodes[nodeIndex].data.node!.template[field].advanced = advanced;
gNodes[nodeIndex].data.node!.template[field].display_name = display_name;
gNodes[nodeIndex].selected = false;
if (proxy) {
gNodes[nodeIndex].data.node!.template[field].proxy = proxy;
} else {
delete gNodes[nodeIndex].data.node!.template[field].proxy;
}
}
});
const nodes = [
...ReactFlowInstance.getNodes().filter((n) => n.id !== groupNode.id),
...gNodes,
];
const edges = [
...ReactFlowInstance.getEdges().filter(
(e) => e.target !== groupNode.id && e.source !== groupNode.id
),
...gEdges,
...updatedEdges,
];
console.log(edges);
ReactFlowInstance.setNodes(nodes);
ReactFlowInstance.setEdges(edges);
}
export function processFlow(FlowObject: ReactFlowJsonObject) {
let clonedFLow = _.cloneDeep(FlowObject);
clonedFLow.nodes.forEach((node: NodeType) => {
if (node.data.node?.flow) {
processFlow(node.data.node!.flow!.data!);
ungroupNode(node.data, clonedFLow);
}
});
return clonedFLow;
}
export function getGroupStatus(
flow: FlowType,
ssData: { [key: string]: { valid: boolean; params: string } }
) {
let status = { valid: true, params: "Built sucessfully ✨" };
const { nodes } = flow.data!;
const ids = nodes.map((n: NodeType) => n.data.id);
ids.forEach((id) => console.log(ssData[id]));
ids.forEach((id) => {
if (!ssData[id]) {
status = ssData[id];
return;
}
if (!ssData[id].valid) {
status = { valid: false, params: ssData[id].params };
}
});
return status;
}
export function createFlowComponent(nodeData: NodeDataType): FlowType {
const flowNode: FlowType = {
data: {

View file

@ -34,6 +34,7 @@ import {
Gift,
GitFork,
GithubIcon,
Group,
Hammer,
HelpCircle,
Home,
@ -72,6 +73,7 @@ import {
TerminalSquare,
Trash2,
Undo,
Ungroup,
Unplug,
Upload,
User,
@ -314,8 +316,10 @@ export const nodeIconsLucide: iconsType = {
UserCog2,
Key,
Unplug,
BookMarked,
Group,
ChevronUp,
Ungroup,
BookMarked,
Minus,
Square,
Minimize2,

View file

@ -327,7 +327,7 @@ TWEAKS = ${
}
def run_flow(inputs: dict, flow_id: str, tweaks: Optional[dict] = None${
!isAuth ? `, apiKey: str=""` : ""
!isAuth ? `, api_key: Optional[str] = None` : ""
}) -> dict:
"""
Run a flow with a given message and optional tweaks.
@ -340,18 +340,19 @@ def run_flow(inputs: dict, flow_id: str, tweaks: Optional[dict] = None${
api_url = f"{BASE_API_URL}/{flow_id}"
payload = {"inputs": inputs}
headers = None
if tweaks:
payload["tweaks"] = tweaks
${!isAuth ? 'headers = {"api-key": apiKey}' : ""}
response = requests.post(api_url, json=payload,headers=headers)
if api_key:
headers = {"x-api-key": api_key}
response = requests.post(api_url, json=payload, headers=headers)
return response.json()
# Setup any tweaks you want to apply to the flow
inputs = ${inputs}
${!isAuth ? `api_key = "<your api key>"` : ""}
print(run_flow(inputs, flow_id=FLOW_ID, tweaks=TWEAKS${
!isAuth ? `, apiKey=api_key` : ""
!isAuth ? `, api_key=api_key` : ""
}))`;
}
@ -374,8 +375,9 @@ export function getCurlCode(
${window.location.protocol}//${
window.location.host
}/api/v1/process/${flowId} \\
-H 'Content-Type: application/json'\\
${!isAuth ? `-H 'api-key: <your api key>'\\` : ""}
-H 'Content-Type: application/json'\\${
!isAuth ? `\n -H 'x-api-key: <your api key>'\\` : ""
}
-d '{"inputs": ${inputs}, "tweaks": ${
tweak && tweak.length > 0
? buildTweakObject(tweak)
@ -443,10 +445,33 @@ chat_input_field: Input key that you want the chat to send the user message with
api_key="..."`
: ""
}
></langflow-chat>`;
}
export function truncateLongId(id: string): string {
let [componentName, newId] = id.split("-");
if (componentName.length > 15) {
componentName = componentName.slice(0, 15);
componentName += "...";
return componentName + "-" + newId;
}
return id;
}
export function extractIdFromLongId(id: string): string {
let [_, newId] = id.split("-");
return newId;
}
export function truncateDisplayName(name: string): string {
if (name.length > 15) {
name = name.slice(0, 15);
name += "...";
}
return name;
}
export function tabsArray(codes: string[], method: number) {
if (!method) return;
if (method === 0) {

View file

@ -0,0 +1,8 @@
#!/bin/sh
# Replace the placeholder with the actual value
sed -i "s|__BACKEND_URL__|$BACKEND_URL|g" /etc/nginx/conf.d/default.conf
# Start nginx
exec nginx -g 'daemon off;'

View file

@ -0,0 +1,20 @@
export const custom = `from langflow import CustomComponent
from langchain.llms.base import BaseLLM
from langchain.chains import LLMChain
from langchain.prompts import PromptTemplate
from langchain.schema import Document
from langchain.field_typing import NestedDict
import requests
class YourComponent(CustomComponent):
display_name: str = "Custom Component"
description: str = "Create any custom component you want!"
def build_config(self):
return { "file": { "file_type": ["json"], } }
def build(self, url: str,file:str,integer:int,nested:NestedDict,flt:float,boolean:bool,lisst:list[str],dictionary:dict, llm: BaseLLM, prompt: PromptTemplate) -> Document:
return "test"`;

View file

@ -0,0 +1,20 @@
import { Page, test } from "@playwright/test";
test.describe("Flow Page tests", () => {
async function goToFlowPage(page: Page) {
await page.goto("http://localhost:3000/");
await page.getByRole("button", { name: "New Project" }).click();
}
test("save", async ({ page }) => {
await goToFlowPage(page);
await page.getByRole("button", { name: "Custom" }).click();
await page
.locator("div")
.filter({ hasText: /^Custom Component$/ })
.nth(4)
.dragTo(page.locator(".react-flow__pane"));
await page.locator("div:nth-child(4) > .extra-side-bar-buttons").click();
await page.locator(".success-alert").click();
});
});

View file

@ -0,0 +1,100 @@
import { expect, test } from "@playwright/test";
test.describe("Group component tests", () => {
test("group test", async ({ page }) => {
await page.goto("http://localhost:3000/");
await page.getByRole("button", { name: "Community Examples" }).click();
await page
.locator(
"div:nth-child(7) > div:nth-child(2) > .card-component-footer-arrangement > .inline-flex"
)
.click();
await page
.locator(
"//html/body/div/div/div[2]/div/main/div/div/div/div[1]/div[1]/div[1]/div/div[2]/div[1]/div/div[1]/div"
)
.click({
modifiers: ["Control"],
});
await page
.locator(
"//html/body/div/div/div[2]/div/main/div/div/div/div[1]/div[1]/div[1]/div/div[2]/div[2]/div/div[1]/div"
)
.click({
modifiers: ["Control"],
});
await page
.locator(
"//html/body/div/div/div[2]/div/main/div/div/div/div[1]/div[1]/div[1]/div/div[2]/div[3]/div/div[1]/div"
)
.click({
modifiers: ["Control"],
});
await page.getByRole("button", { name: "Group" }).click();
expect(
await page
.locator(
"//html/body/div/div/div[2]/div/main/div/div/div/div[1]/div[1]/div[1]/div/div[2]/div/div"
)
.isVisible()
).toBeTruthy();
await page.getByPlaceholder("Type something...").first().click();
await page.getByPlaceholder("Type something...").first().fill("test");
await page.locator(".side-bar-buttons-arrangement").click();
expect(
await page
.locator(
"//html/body/div/div/div[2]/div/main/div/div/div/div[1]/div[1]/div/div/div[2]/div/div/div[1]/div/div[1]/div/div"
)
.textContent()
).toBe("test");
await page
.locator(
"//html/body/div/div/div[2]/div/main/div/div/div/div[1]/div[1]/div[1]/div/div[2]/div/div"
)
.locator('input[type="text"]')
.click();
await page
.locator(
"//html/body/div/div/div[2]/div/main/div/div/div/div[1]/div[1]/div[1]/div/div[2]/div/div"
)
.locator('input[type="text"]')
.fill("fieldValue");
await page.locator(".side-bar-buttons-arrangement").click();
await page
.locator(
"//html/body/div/div/div[2]/div/main/div/div/div/div[1]/div[1]/div[1]/div/div[2]/div/div/div[1]/div"
)
.click();
await page
.locator(
"//html/body/div/div/div[2]/div/main/div/div/div/div[1]/div[1]/div[2]/div/span/button[3]/div/div"
)
.click();
await page.getByLabel("Edit").click();
await page
.getByRole("button", { name: "zero-shot-react-description" })
.click();
await page.getByText("openai-functions").click();
await page.getByRole("button", { name: "Save Changes" }).click();
await page
.locator(
"//html/body/div/div/div[2]/div/main/div/div/div/div[1]/div[1]/div[2]/div/span/button[3]/div/div"
)
.click();
await page.getByLabel("Ungroup").click();
await expect(
page.locator(
"//html/body/div/div/div[2]/div/main/div/div/div/div[1]/div[1]/div/div/div[2]/div[3]/div/div[2]/div[4]/div/div[2]/div/input"
)
).toHaveValue("fieldValue");
expect(
await page
.locator(
"//html/body/div/div/div[2]/div/main/div/div/div/div[1]/div[1]/div/div/div[2]/div[2]/div/div[2]/div[5]/div/div[2]/div/button/span[1]"
)
.textContent()
).toBe("openai-functions");
});
});

View file

@ -0,0 +1,563 @@
import { expect, test } from "@playwright/test";
test("KeypairListComponent", async ({ page }) => {
await page.goto("http://localhost:3000/");
await page.waitForTimeout(2000);
await page.locator('//*[@id="new-project-btn"]').click();
await page.waitForTimeout(2000);
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("csv");
await page.waitForTimeout(2000);
await page
.locator('//*[@id="sideCSVLoader"]')
.dragTo(page.locator('//*[@id="react-flow-id"]'));
await page.mouse.up();
await page.mouse.down();
await page.locator('//*[@id="keypair0"]').click();
await page.locator('//*[@id="keypair0"]').fill("testtesttesttest");
await page.locator('//*[@id="keypair100"]').click();
await page.locator('//*[@id="keypair100"]').fill("testtesttesttesttesttest");
const plusButtonLocatorNode = page.locator('//*[@id="plusbtn0"]');
const elementCountNode = await plusButtonLocatorNode.count();
if (elementCountNode > 0) {
await plusButtonLocatorNode.click();
}
await page.locator('//*[@id="keypair1"]').click();
await page.locator('//*[@id="keypair1"]').fill("testtesttesttest1");
await page.locator('//*[@id="keypair101"]').click();
await page.locator('//*[@id="keypair101"]').fill("testtesttesttesttesttest1");
await page.locator('//*[@id="plusbtn1"]').click();
await page.locator('//*[@id="keypair2"]').click();
await page.locator('//*[@id="keypair2"]').fill("testtesttesttest2");
await page.locator('//*[@id="keypair102"]').click();
await page.locator('//*[@id="keypair102"]').fill("testtesttesttesttesttest2");
await page.locator('//*[@id="minusbtn1"]').click();
const keyPairVerification = page.locator('//*[@id="keypair102"]');
const elementKeyCount = await keyPairVerification.count();
if (elementKeyCount === 0) {
expect(true).toBeTruthy();
} else {
expect(false).toBeTruthy();
}
await page
.locator(
'//*[@id="react-flow-id"]/div[1]/div[1]/div[1]/div/div[2]/div/div/div[1]/div/div[1]/div'
)
.click();
await page.locator('//*[@id="advancedIcon"]').click();
await page.locator('//*[@id="editAdvancedBtn"]').click();
await page.locator('//*[@id="showfile_path"]').click();
expect(
await page.locator('//*[@id="showfile_path"]').isChecked()
).toBeFalsy();
await page.locator('//*[@id="showmetadata"]').click();
expect(await page.locator('//*[@id="showmetadata"]').isChecked()).toBeFalsy();
await page.locator('//*[@id="saveChangesBtn"]').click();
const plusButtonLocator = page.locator('//*[@id="plusbtn0"]');
const elementCount = await plusButtonLocator.count();
if (elementCount === 0) {
expect(true).toBeTruthy();
await page
.locator(
'//*[@id="react-flow-id"]/div[1]/div[1]/div[1]/div/div[2]/div/div/div[1]/div/div[1]/div'
)
.click();
await page.locator('//*[@id="advancedIcon"]').click();
await page.locator('//*[@id="editAdvancedBtn"]').click();
await page.locator('//*[@id="showfile_path"]').click();
expect(
await page.locator('//*[@id="showfile_path"]').isChecked()
).toBeTruthy();
await page.locator('//*[@id="showmetadata"]').click();
expect(
await page.locator('//*[@id="showmetadata"]').isChecked()
).toBeTruthy();
await page.locator('//*[@id="keypair0"]').click();
await page.locator('//*[@id="keypair0"]').fill("testtesttesttest");
await page.locator('//*[@id="keypair100"]').click();
await page
.locator('//*[@id="keypair100"]')
.fill("testtesttesttesttesttest");
const plusButtonLocator = page.locator('//*[@id="plusbtn0"]');
const elementCount = await plusButtonLocator.count();
if (elementCount > 0) {
await plusButtonLocator.click();
}
await page.locator('//*[@id="keypair1"]').click();
await page.locator('//*[@id="keypair1"]').fill("testtesttesttest1");
await page.locator('//*[@id="keypair101"]').click();
await page
.locator('//*[@id="keypair101"]')
.fill("testtesttesttesttesttest1");
await page.locator('//*[@id="plusbtn1"]').click();
await page.locator('//*[@id="keypair2"]').click();
await page.locator('//*[@id="keypair2"]').fill("testtesttesttest2");
await page.locator('//*[@id="keypair102"]').click();
await page
.locator('//*[@id="keypair102"]')
.fill("testtesttesttesttesttest2");
await page.locator('//*[@id="minusbtn1"]').click();
const keyPairVerification = page.locator('//*[@id="keypair102"]');
const elementKeyCount = await keyPairVerification.count();
if (elementKeyCount === 0) {
await page.locator('//*[@id="saveChangesBtn"]').click();
const key1 = await page.locator('//*[@id="keypair0"]').inputValue();
const value1 = await page.locator('//*[@id="keypair100"]').inputValue();
const key2 = await page.locator('//*[@id="keypair1"]').inputValue();
const value2 = await page.locator('//*[@id="keypair101"]').inputValue();
if (
key1 === "testtesttesttest" &&
value1 === "testtesttesttesttesttest" &&
key2 === "testtesttesttest2" &&
value2 === "testtesttesttesttesttest2"
) {
expect(true).toBeTruthy();
} else {
expect(false).toBeTruthy();
}
} else {
expect(false).toBeTruthy();
}
} else {
expect(false).toBeTruthy();
}
});
test("FloatComponent", async ({ page }) => {
await page.goto("http://localhost:3000/");
await page.waitForTimeout(2000);
await page.locator('//*[@id="new-project-btn"]').click();
await page.waitForTimeout(2000);
await page.getByPlaceholder("Search").click();
await page.getByPlaceholder("Search").fill("llamacpp");
await page.waitForTimeout(2000);
await page
.locator('//*[@id="sideLlamaCpp"]')
.dragTo(page.locator('//*[@id="react-flow-id"]'));
await page.mouse.up();
await page.mouse.down();
await page.locator('//*[@id="float-input"]').click();
await page.locator('//*[@id="float-input"]').fill("3");
let value = await page.locator('//*[@id="float-input"]').inputValue();
if (value != "2") {
expect(false).toBeTruthy();
}
await page.locator('//*[@id="float-input"]').click();
await page.locator('//*[@id="float-input"]').fill("-3");
value = await page.locator('//*[@id="float-input"]').inputValue();
if (value != "-2") {
expect(false).toBeTruthy();
}
await page
.locator(
'//*[@id="react-flow-id"]/div[1]/div[1]/div[1]/div/div[2]/div/div/div[1]/div/div[1]/div'
)
.click();
await page.locator('//*[@id="editAdvancedIcon"]').click();
await page.locator('//*[@id="showcache"]').click();
expect(await page.locator('//*[@id="showcache"]').isChecked()).toBeTruthy();
// showecho
await page.locator('//*[@id="showecho"]').click();
expect(await page.locator('//*[@id="showecho"]').isChecked()).toBeTruthy();
// showf16_kv
await page.locator('//*[@id="showf16_kv"]').click();
expect(await page.locator('//*[@id="showf16_kv"]').isChecked()).toBeTruthy();
// showgrammar_path
await page.locator('//*[@id="showgrammar_path"]').click();
expect(
await page.locator('//*[@id="showgrammar_path"]').isChecked()
).toBeTruthy();
// showlast_n_tokens_size
await page.locator('//*[@id="showlast_n_tokens_size"]').click();
expect(
await page.locator('//*[@id="showlast_n_tokens_size"]').isChecked()
).toBeTruthy();
// showlogits_all
await page.locator('//*[@id="showlogits_all"]').click();
expect(
await page.locator('//*[@id="showlogits_all"]').isChecked()
).toBeTruthy();
// showlogprobs
await page.locator('//*[@id="showlogprobs"]').click();
expect(
await page.locator('//*[@id="showlogprobs"]').isChecked()
).toBeTruthy();
// showlora_base
await page.locator('//*[@id="showlora_base"]').click();
expect(
await page.locator('//*[@id="showlora_base"]').isChecked()
).toBeTruthy();
// showlora_path
await page.locator('//*[@id="showlora_path"]').click();
expect(
await page.locator('//*[@id="showlora_path"]').isChecked()
).toBeTruthy();
// showmax_tokens
await page.locator('//*[@id="showmax_tokens"]').click();
expect(
await page.locator('//*[@id="showmax_tokens"]').isChecked()
).toBeTruthy();
// showmetadata
await page.locator('//*[@id="showmetadata"]').click();
expect(
await page.locator('//*[@id="showmetadata"]').isChecked()
).toBeTruthy();
// showmodel_kwargs
await page.locator('//*[@id="showmodel_kwargs"]').click();
expect(
await page.locator('//*[@id="showmodel_kwargs"]').isChecked()
).toBeTruthy();
// showmodel_path
await page.locator('//*[@id="showmodel_path"]').click();
expect(
await page.locator('//*[@id="showmodel_path"]').isChecked()
).toBeFalsy();
// shown_batch
await page.locator('//*[@id="shown_batch"]').click();
expect(await page.locator('//*[@id="shown_batch"]').isChecked()).toBeTruthy();
// shown_ctx
await page.locator('//*[@id="shown_ctx"]').click();
expect(await page.locator('//*[@id="shown_ctx"]').isChecked()).toBeTruthy();
// shown_gpu_layers
await page.locator('//*[@id="shown_gpu_layers"]').click();
expect(
await page.locator('//*[@id="shown_gpu_layers"]').isChecked()
).toBeTruthy();
// shown_parts
await page.locator('//*[@id="shown_parts"]').click();
expect(await page.locator('//*[@id="shown_parts"]').isChecked()).toBeTruthy();
// shown_threads
await page.locator('//*[@id="shown_threads"]').click();
expect(
await page.locator('//*[@id="shown_threads"]').isChecked()
).toBeTruthy();
// showrepeat_penalty
await page.locator('//*[@id="showrepeat_penalty"]').click();
expect(
await page.locator('//*[@id="showrepeat_penalty"]').isChecked()
).toBeTruthy();
// showrope_freq_base
await page.locator('//*[@id="showrope_freq_base"]').click();
expect(
await page.locator('//*[@id="showrope_freq_base"]').isChecked()
).toBeTruthy();
// showrope_freq_scale
await page.locator('//*[@id="showrope_freq_scale"]').click();
expect(
await page.locator('//*[@id="showrope_freq_scale"]').isChecked()
).toBeTruthy();
// showseed
await page.locator('//*[@id="showseed"]').click();
expect(await page.locator('//*[@id="showseed"]').isChecked()).toBeTruthy();
// showstop
await page.locator('//*[@id="showstop"]').click();
expect(await page.locator('//*[@id="showstop"]').isChecked()).toBeTruthy();
// showstreaming
await page.locator('//*[@id="showstreaming"]').click();
expect(
await page.locator('//*[@id="showstreaming"]').isChecked()
).toBeTruthy();
// showsuffix
await page.locator('//*[@id="showsuffix"]').click();
expect(await page.locator('//*[@id="showsuffix"]').isChecked()).toBeTruthy();
// showtags
await page.locator('//*[@id="showtags"]').click();
expect(await page.locator('//*[@id="showtags"]').isChecked()).toBeTruthy();
// showtemperature
await page.locator('//*[@id="showtemperature"]').click();
expect(
await page.locator('//*[@id="showtemperature"]').isChecked()
).toBeFalsy();
// showtop_k
await page.locator('//*[@id="showtop_k"]').click();
expect(await page.locator('//*[@id="showtop_k"]').isChecked()).toBeTruthy();
// showtop_p
await page.locator('//*[@id="showtop_p"]').click();
expect(await page.locator('//*[@id="showtop_p"]').isChecked()).toBeTruthy();
// showuse_mlock
await page.locator('//*[@id="showuse_mlock"]').click();
expect(
await page.locator('//*[@id="showuse_mlock"]').isChecked()
).toBeTruthy();
// showuse_mmap
await page.locator('//*[@id="showuse_mmap"]').click();
expect(
await page.locator('//*[@id="showuse_mmap"]').isChecked()
).toBeTruthy();
// showverbose
await page.locator('//*[@id="showverbose"]').click();
expect(await page.locator('//*[@id="showverbose"]').isChecked()).toBeTruthy();
// showvocab_only
await page.locator('//*[@id="showvocab_only"]').click();
expect(
await page.locator('//*[@id="showvocab_only"]').isChecked()
).toBeTruthy();
await page.locator('//*[@id="showcache"]').click();
expect(await page.locator('//*[@id="showcache"]').isChecked()).toBeFalsy();
// showecho
await page.locator('//*[@id="showecho"]').click();
expect(await page.locator('//*[@id="showecho"]').isChecked()).toBeFalsy();
// showf16_kv
await page.locator('//*[@id="showf16_kv"]').click();
expect(await page.locator('//*[@id="showf16_kv"]').isChecked()).toBeFalsy();
// showgrammar_path
await page.locator('//*[@id="showgrammar_path"]').click();
expect(
await page.locator('//*[@id="showgrammar_path"]').isChecked()
).toBeFalsy();
// showlast_n_tokens_size
await page.locator('//*[@id="showlast_n_tokens_size"]').click();
expect(
await page.locator('//*[@id="showlast_n_tokens_size"]').isChecked()
).toBeFalsy();
// showlogits_all
await page.locator('//*[@id="showlogits_all"]').click();
expect(
await page.locator('//*[@id="showlogits_all"]').isChecked()
).toBeFalsy();
// showlogprobs
await page.locator('//*[@id="showlogprobs"]').click();
expect(await page.locator('//*[@id="showlogprobs"]').isChecked()).toBeFalsy();
// showlora_base
await page.locator('//*[@id="showlora_base"]').click();
expect(
await page.locator('//*[@id="showlora_base"]').isChecked()
).toBeFalsy();
// showlora_path
await page.locator('//*[@id="showlora_path"]').click();
expect(
await page.locator('//*[@id="showlora_path"]').isChecked()
).toBeFalsy();
// showmax_tokens
await page.locator('//*[@id="showmax_tokens"]').click();
expect(
await page.locator('//*[@id="showmax_tokens"]').isChecked()
).toBeFalsy();
// showmetadata
await page.locator('//*[@id="showmetadata"]').click();
expect(await page.locator('//*[@id="showmetadata"]').isChecked()).toBeFalsy();
// showmodel_kwargs
await page.locator('//*[@id="showmodel_kwargs"]').click();
expect(
await page.locator('//*[@id="showmodel_kwargs"]').isChecked()
).toBeFalsy();
// showmodel_path
await page.locator('//*[@id="showmodel_path"]').click();
expect(
await page.locator('//*[@id="showmodel_path"]').isChecked()
).toBeTruthy();
// shown_batch
await page.locator('//*[@id="shown_batch"]').click();
expect(await page.locator('//*[@id="shown_batch"]').isChecked()).toBeFalsy();
// shown_ctx
await page.locator('//*[@id="shown_ctx"]').click();
expect(await page.locator('//*[@id="shown_ctx"]').isChecked()).toBeFalsy();
// shown_gpu_layers
await page.locator('//*[@id="shown_gpu_layers"]').click();
expect(
await page.locator('//*[@id="shown_gpu_layers"]').isChecked()
).toBeFalsy();
// shown_parts
await page.locator('//*[@id="shown_parts"]').click();
expect(await page.locator('//*[@id="shown_parts"]').isChecked()).toBeFalsy();
// shown_threads
await page.locator('//*[@id="shown_threads"]').click();
expect(
await page.locator('//*[@id="shown_threads"]').isChecked()
).toBeFalsy();
// showrepeat_penalty
await page.locator('//*[@id="showrepeat_penalty"]').click();
expect(
await page.locator('//*[@id="showrepeat_penalty"]').isChecked()
).toBeFalsy();
// showrope_freq_base
await page.locator('//*[@id="showrope_freq_base"]').click();
expect(
await page.locator('//*[@id="showrope_freq_base"]').isChecked()
).toBeFalsy();
// showrope_freq_scale
await page.locator('//*[@id="showrope_freq_scale"]').click();
expect(
await page.locator('//*[@id="showrope_freq_scale"]').isChecked()
).toBeFalsy();
// showseed
await page.locator('//*[@id="showseed"]').click();
expect(await page.locator('//*[@id="showseed"]').isChecked()).toBeFalsy();
// showstop
await page.locator('//*[@id="showstop"]').click();
expect(await page.locator('//*[@id="showstop"]').isChecked()).toBeFalsy();
// showstreaming
await page.locator('//*[@id="showstreaming"]').click();
expect(
await page.locator('//*[@id="showstreaming"]').isChecked()
).toBeFalsy();
// showsuffix
await page.locator('//*[@id="showsuffix"]').click();
expect(await page.locator('//*[@id="showsuffix"]').isChecked()).toBeFalsy();
// showtags
await page.locator('//*[@id="showtags"]').click();
expect(await page.locator('//*[@id="showtags"]').isChecked()).toBeFalsy();
// showtop_k
await page.locator('//*[@id="showtop_k"]').click();
expect(await page.locator('//*[@id="showtop_k"]').isChecked()).toBeFalsy();
// showtop_p
await page.locator('//*[@id="showtop_p"]').click();
expect(await page.locator('//*[@id="showtop_p"]').isChecked()).toBeFalsy();
// showuse_mlock
await page.locator('//*[@id="showuse_mlock"]').click();
expect(
await page.locator('//*[@id="showuse_mlock"]').isChecked()
).toBeFalsy();
// showuse_mmap
await page.locator('//*[@id="showuse_mmap"]').click();
expect(await page.locator('//*[@id="showuse_mmap"]').isChecked()).toBeFalsy();
// showverbose
await page.locator('//*[@id="showverbose"]').click();
expect(await page.locator('//*[@id="showverbose"]').isChecked()).toBeFalsy();
// showvocab_only
await page.locator('//*[@id="showvocab_only"]').click();
expect(
await page.locator('//*[@id="showvocab_only"]').isChecked()
).toBeFalsy();
await page.locator('//*[@id="saveChangesBtn"]').click();
const plusButtonLocator = page.locator('//*[@id="float-input"]');
const elementCount = await plusButtonLocator.count();
if (elementCount === 0) {
expect(true).toBeTruthy();
await page
.locator(
'//*[@id="react-flow-id"]/div[1]/div[1]/div[1]/div/div[2]/div/div/div[1]/div/div[1]/div'
)
.click();
await page.locator('//*[@id="editAdvancedIcon"]').click();
// showtemperature
await page.locator('//*[@id="showtemperature"]').click();
expect(
await page.locator('//*[@id="showtemperature"]').isChecked()
).toBeTruthy();
await page.locator('//*[@id="saveChangesBtn"]').click();
await page.locator('//*[@id="float-input"]').click();
await page.locator('//*[@id="float-input"]').fill("3");
let value = await page.locator('//*[@id="float-input"]').inputValue();
if (value != "2") {
expect(false).toBeTruthy();
}
await page.locator('//*[@id="float-input"]').click();
await page.locator('//*[@id="float-input"]').fill("-3");
value = await page.locator('//*[@id="float-input"]').inputValue();
if (value != "-2") {
expect(false).toBeTruthy();
}
}
});

View file

@ -0,0 +1,5 @@
import { test } from "@playwright/test";
test("test", async ({ page }) => {
// Recording...
});