Merge remote-tracking branch 'origin/cz/mergeAll' into fix/ui_adjustments
This commit is contained in:
commit
5b3a6551ae
434 changed files with 16538 additions and 10995 deletions
98
src/frontend/package-lock.json
generated
98
src/frontend/package-lock.json
generated
|
|
@ -26,6 +26,7 @@
|
|||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@radix-ui/react-switch": "^1.0.3",
|
||||
"@radix-ui/react-tabs": "^1.0.4",
|
||||
"@radix-ui/react-toggle": "^1.0.3",
|
||||
"@radix-ui/react-tooltip": "^1.0.6",
|
||||
"@tabler/icons-react": "^2.32.0",
|
||||
"@tailwindcss/forms": "^0.5.6",
|
||||
|
|
@ -40,9 +41,9 @@
|
|||
"class-variance-authority": "^0.6.1",
|
||||
"clsx": "^1.2.1",
|
||||
"cmdk": "^1.0.0",
|
||||
"debounce-promise": "^3.1.2",
|
||||
"dompurify": "^3.0.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"emoji-regex": "^10.3.0",
|
||||
"esbuild": "^0.17.19",
|
||||
"file-saver": "^2.0.5",
|
||||
"framer-motion": "^11.0.6",
|
||||
|
|
@ -51,6 +52,7 @@
|
|||
"million": "^3.0.6",
|
||||
"moment": "^2.29.4",
|
||||
"openseadragon": "^4.1.1",
|
||||
"p-debounce": "^4.0.0",
|
||||
"playwright": "^1.42.0",
|
||||
"react": "^18.2.21",
|
||||
"react-ace": "^10.1.0",
|
||||
|
|
@ -58,6 +60,7 @@
|
|||
"react-dom": "^18.2.21",
|
||||
"react-error-boundary": "^4.0.11",
|
||||
"react-hook-form": "^7.51.4",
|
||||
"react-hotkeys-hook": "^4.5.0",
|
||||
"react-icons": "^5.0.1",
|
||||
"react-laag": "^2.0.5",
|
||||
"react-markdown": "^8.0.7",
|
||||
|
|
@ -1921,12 +1924,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.44.0",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.44.0.tgz",
|
||||
"integrity": "sha512-rNX5lbNidamSUorBhB4XZ9SQTjAqfe5M+p37Z8ic0jPFBMo5iCtQz1kRWkEMg+rYOKSlVycpQmpqjSFq7LXOfg==",
|
||||
"version": "1.44.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.44.1.tgz",
|
||||
"integrity": "sha512-1hZ4TNvD5z9VuhNJ/walIjvMVvYkZKf71axoF/uiAqpntQJXpG64dlXhoDXE3OczPuTuvjf/M5KWFg5VAVUS3Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"playwright": "1.44.0"
|
||||
"playwright": "1.44.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
|
@ -2762,6 +2765,31 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-toggle": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.0.3.tgz",
|
||||
"integrity": "sha512-Pkqg3+Bc98ftZGsl60CLANXQBBQ4W3mTFS9EJvNxKMZ7magklKV69/id1mlAlOFDDfHvlCms0fx8fA4CMKDJHg==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/primitive": "1.0.1",
|
||||
"@radix-ui/react-primitive": "1.0.3",
|
||||
"@radix-ui/react-use-controllable-state": "1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.0.7.tgz",
|
||||
|
|
@ -5671,11 +5699,6 @@
|
|||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/debounce-promise": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/debounce-promise/-/debounce-promise-3.1.2.tgz",
|
||||
"integrity": "sha512-rZHcgBkbYavBeD9ej6sP56XfG53d51CD4dnaw989YX/nZ/ZJfgRx/9ePKmTNiUiyQvh4mtrMoS3OAWW+yoYtpg=="
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
|
|
@ -5966,14 +5989,14 @@
|
|||
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.4.778",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.778.tgz",
|
||||
"integrity": "sha512-C6q/xcUJf/2yODRxAVCfIk4j3y3LMsD0ehiE2RQNV2cxc8XU62gR6vvYh3+etSUzlgTfil+qDHI1vubpdf0TOA=="
|
||||
"version": "1.4.780",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.780.tgz",
|
||||
"integrity": "sha512-NPtACGFe7vunRYzvYqVRhQvsDrTevxpgDKxG/Vcbe0BTNOY+5+/2mOXSw2ls7ToNbE5Bf/+uQbjTxcmwMozpCw=="
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
|
||||
"version": "10.3.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz",
|
||||
"integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw=="
|
||||
},
|
||||
"node_modules/end-of-stream": {
|
||||
"version": "1.4.4",
|
||||
|
|
@ -7596,6 +7619,7 @@
|
|||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
|
||||
"deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"once": "^1.3.0",
|
||||
|
|
@ -10023,6 +10047,15 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/p-debounce": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-debounce/-/p-debounce-4.0.0.tgz",
|
||||
"integrity": "sha512-4Ispi9I9qYGO4lueiLDhe4q4iK5ERK8reLsuzH6BPaXn53EGaua8H66PXIFGrW897hwjXp+pVLrm/DLxN0RF0A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/p-finally": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
|
||||
|
|
@ -10272,11 +10305,11 @@
|
|||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.44.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.0.tgz",
|
||||
"integrity": "sha512-F9b3GUCLQ3Nffrfb6dunPOkE5Mh68tR7zN32L4jCk4FjQamgesGay7/dAAe1WaMEGV04DkdJfcJzjoCKygUaRQ==",
|
||||
"version": "1.44.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.1.tgz",
|
||||
"integrity": "sha512-qr/0UJ5CFAtloI3avF95Y0L1xQo6r3LQArLIg/z/PoGJ6xa+EwzrwO5lpNr/09STxdHuUoP2mvuELJS+hLdtgg==",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.44.0"
|
||||
"playwright-core": "1.44.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
|
@ -10289,9 +10322,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.44.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.0.tgz",
|
||||
"integrity": "sha512-ZTbkNpFfYcGWohvTTl+xewITm7EOuqIqex0c7dNZ+aXsbrLj0qI8XlGKfPpipjm0Wny/4Lt4CJsWJk1stVS5qQ==",
|
||||
"version": "1.44.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.1.tgz",
|
||||
"integrity": "sha512-wh0JWtYTrhv1+OSsLPgFzGzt67Y7BE/ZS3jEqgGBlp2ppp1ZDj8c+9IARNW4dwf1poq5MgHreEM2KV/GuR4cFA==",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
|
|
@ -10990,6 +11023,15 @@
|
|||
"react": "^16.8.0 || ^17 || ^18"
|
||||
}
|
||||
},
|
||||
"node_modules/react-hotkeys-hook": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-4.5.0.tgz",
|
||||
"integrity": "sha512-Samb85GSgAWFQNvVt3PS90LPPGSf9mkH/r4au81ZP1yOIFayLC3QAvqTgGtJ8YEDMXtPmaVBs6NgipHO6h4Mug==",
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.1",
|
||||
"react-dom": ">=16.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-icons": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.2.1.tgz",
|
||||
|
|
@ -12178,6 +12220,16 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width-cjs/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
|
||||
},
|
||||
"node_modules/string-width/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
"@headlessui/react": "^1.7.17",
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
"@million/lint": "^0.0.73",
|
||||
"react-hotkeys-hook": "^4.5.0",
|
||||
"@radix-ui/react-accordion": "^1.1.2",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-dialog": "^1.0.4",
|
||||
|
|
@ -21,6 +22,7 @@
|
|||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@radix-ui/react-switch": "^1.0.3",
|
||||
"@radix-ui/react-tabs": "^1.0.4",
|
||||
"@radix-ui/react-toggle": "^1.0.3",
|
||||
"@radix-ui/react-tooltip": "^1.0.6",
|
||||
"@tabler/icons-react": "^2.32.0",
|
||||
"@tailwindcss/forms": "^0.5.6",
|
||||
|
|
@ -35,9 +37,9 @@
|
|||
"class-variance-authority": "^0.6.1",
|
||||
"clsx": "^1.2.1",
|
||||
"cmdk": "^1.0.0",
|
||||
"debounce-promise": "^3.1.2",
|
||||
"dompurify": "^3.0.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"emoji-regex": "^10.3.0",
|
||||
"esbuild": "^0.17.19",
|
||||
"file-saver": "^2.0.5",
|
||||
"framer-motion": "^11.0.6",
|
||||
|
|
@ -46,6 +48,7 @@
|
|||
"million": "^3.0.6",
|
||||
"moment": "^2.29.4",
|
||||
"openseadragon": "^4.1.1",
|
||||
"p-debounce": "^4.0.0",
|
||||
"playwright": "^1.42.0",
|
||||
"react": "^18.2.21",
|
||||
"react-ace": "^10.1.0",
|
||||
|
|
|
|||
|
|
@ -45,6 +45,9 @@ export default defineConfig({
|
|||
name: "chromium",
|
||||
use: {
|
||||
...devices["Desktop Chrome"],
|
||||
launchOptions: {
|
||||
// headless: false,
|
||||
},
|
||||
contextOptions: {
|
||||
// chromium-specific permissions
|
||||
permissions: ["clipboard-read", "clipboard-write"],
|
||||
|
|
@ -57,6 +60,7 @@ export default defineConfig({
|
|||
// use: {
|
||||
// ...devices["Desktop Firefox"],
|
||||
// launchOptions: {
|
||||
// headless: false,
|
||||
// firefoxUserPrefs: {
|
||||
// "dom.events.asyncClipboard.readText": true,
|
||||
// "dom.events.testing.asyncClipboard": true,
|
||||
|
|
|
|||
|
|
@ -164,3 +164,22 @@ body {
|
|||
.ag-body-vertical-scroll-viewport::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #bbb;
|
||||
}
|
||||
|
||||
/* This CSS is to not apply the border for the column having 'no-border' class */
|
||||
.no-border.ag-cell:focus {
|
||||
border: none !important;
|
||||
outline: none;
|
||||
}
|
||||
.no-border.ag-cell {
|
||||
border: none !important;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* selected */
|
||||
.react-flow__edge.selected .react-flow__edge-path {
|
||||
stroke: var(--selected) !important;
|
||||
}
|
||||
|
||||
.react-flow__edge .react-flow__edge-path {
|
||||
stroke: var(--connection) !important;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import axios from "axios";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { ErrorBoundary } from "react-error-boundary";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
import { Textarea } from "../../../../../../../components/ui/textarea";
|
||||
|
||||
export default function ErrorOutput({ value }: { value: string }) {
|
||||
return (
|
||||
<Textarea
|
||||
className={`h-full w-full text-destructive custom-scroll`}
|
||||
placeholder={"Empty"}
|
||||
value={value}
|
||||
readOnly
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
export const convertToTableRows = (obj: Object) => {
|
||||
const tokensArray = [Object.values(obj)[0]];
|
||||
return tokensArray;
|
||||
};
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
import ForwardedIconComponent from "../../../../../../components/genericIconComponent";
|
||||
import RecordsOutputComponent from "../../../../../../components/recordsOutputComponent";
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
AlertTitle,
|
||||
} from "../../../../../../components/ui/alert";
|
||||
import { Case } from "../../../../../../shared/components/caseComponent";
|
||||
import TextOutputView from "../../../../../../shared/components/textOutputView";
|
||||
import useFlowStore from "../../../../../../stores/flowStore";
|
||||
import ErrorOutput from "./components";
|
||||
|
||||
export default function SwitchOutputView(nodeId): JSX.Element {
|
||||
const nodeIdentity = nodeId.nodeId;
|
||||
|
||||
const nodes = useFlowStore((state) => state.nodes);
|
||||
const flowPool = useFlowStore((state) => state.flowPool);
|
||||
const node = nodes.find((node) => node?.id === nodeIdentity);
|
||||
|
||||
const flowPoolNode = (flowPool[nodeIdentity] ?? [])[
|
||||
(flowPool[nodeIdentity]?.length ?? 1) - 1
|
||||
];
|
||||
|
||||
const results = flowPoolNode?.data?.logs[0] ?? "";
|
||||
const resultType = results?.type;
|
||||
let resultMessage = results?.message;
|
||||
const RECORD_TYPES = ["record", "object", "array", "message"];
|
||||
if (resultMessage.raw) {
|
||||
resultMessage = resultMessage.raw;
|
||||
}
|
||||
console.log("resultType", results);
|
||||
return (
|
||||
<>
|
||||
<Case condition={!resultType || resultType === "unknown"}>
|
||||
<div>NO OUTPUT</div>
|
||||
</Case>
|
||||
<Case condition={resultType === "ValueError"}>
|
||||
<ErrorOutput value={resultMessage}></ErrorOutput>
|
||||
</Case>
|
||||
|
||||
<Case condition={node && resultType === "text"}>
|
||||
<TextOutputView left={false} value={resultMessage} />
|
||||
</Case>
|
||||
|
||||
<Case condition={RECORD_TYPES.includes(resultType)}>
|
||||
<RecordsOutputComponent
|
||||
rows={
|
||||
Array.isArray(resultMessage)
|
||||
? (resultMessage as Array<any>).every((item) => item.data)
|
||||
? (resultMessage as Array<any>).map((item) => item.data)
|
||||
: resultMessage
|
||||
: [resultMessage]
|
||||
}
|
||||
pagination={true}
|
||||
columnMode="union"
|
||||
/>
|
||||
</Case>
|
||||
|
||||
<Case condition={resultType === "stream"}>
|
||||
<div className="flex h-full w-full items-center justify-center align-middle">
|
||||
<Alert variant={"default"} className="w-fit">
|
||||
<ForwardedIconComponent
|
||||
name="AlertCircle"
|
||||
className="h-5 w-5 text-primary"
|
||||
/>
|
||||
<AlertTitle>{"Streaming is not supported"}</AlertTitle>
|
||||
<AlertDescription>
|
||||
{
|
||||
"Use the playground to interact with components that stream data"
|
||||
}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
</Case>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import { Button } from "../../../../components/ui/button";
|
||||
import BaseModal from "../../../../modals/baseModal";
|
||||
import SwitchOutputView from "./components/switchOutputView";
|
||||
|
||||
export default function OutputModal({ open, setOpen, nodeId }): JSX.Element {
|
||||
return (
|
||||
<BaseModal open={open} setOpen={setOpen} size="medium-tall">
|
||||
<BaseModal.Header description="Inspect the output of the component below.">
|
||||
<div className="flex items-center">
|
||||
<span className="pr-2">Component Output</span>
|
||||
</div>
|
||||
</BaseModal.Header>
|
||||
<BaseModal.Content>
|
||||
<SwitchOutputView nodeId={nodeId} />
|
||||
</BaseModal.Content>
|
||||
<BaseModal.Footer>
|
||||
<div className="flex w-full justify-end pt-2">
|
||||
<Button className="flex gap-2 px-3" onClick={() => setOpen(false)}>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</BaseModal.Footer>
|
||||
</BaseModal>
|
||||
);
|
||||
}
|
||||
|
|
@ -22,7 +22,6 @@ import {
|
|||
TOOLTIP_EMPTY,
|
||||
} from "../../../../constants/constants";
|
||||
import { Case } from "../../../../shared/components/caseComponent";
|
||||
import useAlertStore from "../../../../stores/alertStore";
|
||||
import useFlowStore from "../../../../stores/flowStore";
|
||||
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
|
||||
import { useTypesStore } from "../../../../stores/typesStore";
|
||||
|
|
@ -45,6 +44,7 @@ import useFetchDataOnMount from "../../../hooks/use-fetch-data-on-mount";
|
|||
import useHandleOnNewValue from "../../../hooks/use-handle-new-value";
|
||||
import useHandleNodeClass from "../../../hooks/use-handle-node-class";
|
||||
import useHandleRefreshButtonPress from "../../../hooks/use-handle-refresh-buttons";
|
||||
import OutputModal from "../outputModal";
|
||||
import TooltipRenderComponent from "../tooltipRenderComponent";
|
||||
import { TEXT_FIELD_TYPES } from "./constants";
|
||||
|
||||
|
|
@ -67,7 +67,6 @@ export default function ParameterComponent({
|
|||
const ref = useRef<HTMLDivElement>(null);
|
||||
const refHtml = useRef<HTMLDivElement & ReactNode>(null);
|
||||
const infoHtml = useRef<HTMLDivElement & ReactNode>(null);
|
||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||
const currentFlow = useFlowsManagerStore((state) => state.currentFlow);
|
||||
const nodes = useFlowStore((state) => state.nodes);
|
||||
const edges = useFlowStore((state) => state.edges);
|
||||
|
|
@ -80,6 +79,16 @@ export default function ParameterComponent({
|
|||
const flow = currentFlow?.data?.nodes ?? null;
|
||||
const groupedEdge = useRef(null);
|
||||
const setFilterEdge = useFlowStore((state) => state.setFilterEdge);
|
||||
const [openOutputModal, setOpenOutputModal] = useState(false);
|
||||
const flowPool = useFlowStore((state) => state.flowPool);
|
||||
|
||||
const displayOutputPreview = !!flowPool[data.id];
|
||||
|
||||
const unknownOutput = !!(
|
||||
flowPool[data.id] &&
|
||||
flowPool[data.id][flowPool[data.id].length - 1]?.data?.logs[0]?.type ===
|
||||
"unknown"
|
||||
);
|
||||
|
||||
const { handleOnNewValue: handleOnNewValueHook } = useHandleOnNewValue(
|
||||
data,
|
||||
|
|
@ -251,9 +260,40 @@ export default function ParameterComponent({
|
|||
</span>
|
||||
</ShadTooltip>
|
||||
) : (
|
||||
<span className={!left && data.node?.frozen ? " text-ice" : ""}>
|
||||
{title}
|
||||
</span>
|
||||
<div className="flex gap-2">
|
||||
<span className={!left && data.node?.frozen ? " text-ice" : ""}>
|
||||
{title}
|
||||
</span>
|
||||
{!left && (
|
||||
<ShadTooltip
|
||||
content={
|
||||
displayOutputPreview
|
||||
? unknownOutput
|
||||
? "Output can't be displayed"
|
||||
: "Inspect Output"
|
||||
: "Please build the component first"
|
||||
}
|
||||
>
|
||||
<Button
|
||||
variant="none"
|
||||
size="none"
|
||||
disabled={!displayOutputPreview || unknownOutput}
|
||||
onClick={() => setOpenOutputModal(true)}
|
||||
data-testid={`output-inspection-${title.toLowerCase()}`}
|
||||
>
|
||||
<IconComponent
|
||||
className={classNames(
|
||||
"h-5 w-5 rounded-md",
|
||||
displayOutputPreview && !unknownOutput
|
||||
? " hover:bg-secondary-foreground/5 hover:text-medium-indigo"
|
||||
: " cursor-not-allowed text-muted-foreground",
|
||||
)}
|
||||
name={"ScanEye"}
|
||||
/>
|
||||
</Button>
|
||||
</ShadTooltip>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<span className={(required ? "ml-2 " : "") + "text-status-red"}>
|
||||
{required ? "*" : ""}
|
||||
|
|
@ -392,7 +432,7 @@ export default function ParameterComponent({
|
|||
});
|
||||
}}
|
||||
name={name}
|
||||
data={data.node?.template[name]}
|
||||
data={data.node?.template[name]!}
|
||||
/>
|
||||
</div>
|
||||
{data.node?.template[name]?.refresh_button && (
|
||||
|
|
@ -448,8 +488,8 @@ export default function ParameterComponent({
|
|||
data.node?.template[name]?.real_time_refresh)
|
||||
}
|
||||
>
|
||||
<div className="mt-2 flex w-full items-center">
|
||||
<div className="w-5/6 flex-grow">
|
||||
<div className="mt-2 flex w-full items-center gap-2">
|
||||
<div className="flex-1">
|
||||
<Dropdown
|
||||
disabled={disabled}
|
||||
isLoading={isLoading}
|
||||
|
|
@ -467,7 +507,6 @@ export default function ParameterComponent({
|
|||
name={name}
|
||||
data={data}
|
||||
button_text={data.node?.template[name]?.refresh_button_text}
|
||||
className="extra-side-bar-buttons ml-2 mt-1"
|
||||
handleUpdateValues={handleRefreshButtonPress}
|
||||
id={"refresh-button-" + name}
|
||||
/>
|
||||
|
|
@ -580,6 +619,13 @@ export default function ParameterComponent({
|
|||
/>
|
||||
</div>
|
||||
</Case>
|
||||
{openOutputModal && (
|
||||
<OutputModal
|
||||
open={openOutputModal}
|
||||
nodeId={data.id}
|
||||
setOpen={setOpenOutputModal}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -1,46 +1,52 @@
|
|||
import { cloneDeep } from "lodash";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import emojiRegex from "emoji-regex";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { NodeToolbar, useUpdateNodeInternals } from "reactflow";
|
||||
import IconComponent from "../../components/genericIconComponent";
|
||||
import InputComponent from "../../components/inputComponent";
|
||||
import ShadTooltip from "../../components/shadTooltipComponent";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import Checkmark from "../../components/ui/checkmark";
|
||||
import Loading from "../../components/ui/loading";
|
||||
import { Textarea } from "../../components/ui/textarea";
|
||||
import Xmark from "../../components/ui/xmark";
|
||||
import {
|
||||
RUN_TIMESTAMP_PREFIX,
|
||||
STATUS_BUILD,
|
||||
STATUS_BUILDING,
|
||||
TOOLTIP_OUTDATED_NODE,
|
||||
} from "../../constants/constants";
|
||||
import { BuildStatus } from "../../constants/enums";
|
||||
import { countHandlesFn } from "../helpers/count-handles";
|
||||
import { getSpecificClassFromBuildStatus } from "../helpers/get-class-from-build-status";
|
||||
import NodeToolbarComponent from "../../pages/FlowPage/components/nodeToolbarComponent";
|
||||
import useAlertStore from "../../stores/alertStore";
|
||||
import { useDarkStore } from "../../stores/darkStore";
|
||||
import useFlowStore from "../../stores/flowStore";
|
||||
import useFlowsManagerStore from "../../stores/flowsManagerStore";
|
||||
import { useTypesStore } from "../../stores/typesStore";
|
||||
import { APIClassType } from "../../types/api";
|
||||
import { validationStatusType } from "../../types/components";
|
||||
import { VertexBuildTypeAPI } from "../../types/api";
|
||||
import { NodeDataType } from "../../types/flow";
|
||||
import { handleKeyDown, scapedJSONStringfy } from "../../utils/reactflowUtils";
|
||||
import { nodeColors, nodeIconsLucide } from "../../utils/styleUtils";
|
||||
import { classNames, cn } from "../../utils/utils";
|
||||
import useCheckCodeValidity from "../hooks/use-check-code-validity";
|
||||
import useIconNodeRender from "../hooks/use-icon-render";
|
||||
import useIconStatus from "../hooks/use-icons-status";
|
||||
import useUpdateNodeCode from "../hooks/use-update-node-code";
|
||||
import useUpdateValidationStatus from "../hooks/use-update-validation-status";
|
||||
import useValidationStatusString from "../hooks/use-validation-status-string";
|
||||
import getFieldTitle from "../utils/get-field-title";
|
||||
import sortFields from "../utils/sort-fields";
|
||||
import ParameterComponent from "./components/parameterComponent";
|
||||
import { postCustomComponent } from "../../controllers/API";
|
||||
import { useShortcutsStore } from "../../stores/shortcuts";
|
||||
|
||||
export default function GenericNode({
|
||||
data,
|
||||
xPos,
|
||||
yPos,
|
||||
|
||||
selected,
|
||||
}: {
|
||||
data: NodeDataType;
|
||||
selected: boolean;
|
||||
xPos: number;
|
||||
yPos: number;
|
||||
xPos?: number;
|
||||
yPos?: number;
|
||||
}): JSX.Element {
|
||||
const types = useTypesStore((state) => state.types);
|
||||
const templates = useTypesStore((state) => state.templates);
|
||||
|
|
@ -50,7 +56,15 @@ export default function GenericNode({
|
|||
const setNode = useFlowStore((state) => state.setNode);
|
||||
const updateNodeInternals = useUpdateNodeInternals();
|
||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||
const name = nodeIconsLucide[data.type] ? data.type : types[data.type];
|
||||
const isDark = useDarkStore((state) => state.dark);
|
||||
const buildStatus = useFlowStore(
|
||||
(state) => state.flowBuildStatus[data.id]?.status,
|
||||
);
|
||||
const lastRunTime = useFlowStore(
|
||||
(state) => state.flowBuildStatus[data.id]?.timestamp,
|
||||
);
|
||||
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
|
||||
|
||||
const [inputName, setInputName] = useState(false);
|
||||
const [nodeName, setNodeName] = useState(data.node!.display_name);
|
||||
const [inputDescription, setInputDescription] = useState(false);
|
||||
|
|
@ -58,185 +72,25 @@ export default function GenericNode({
|
|||
data.node?.description!,
|
||||
);
|
||||
const [isOutdated, setIsOutdated] = useState(false);
|
||||
const buildStatus = useFlowStore(
|
||||
(state) => state.flowBuildStatus[data.id]?.status,
|
||||
);
|
||||
const lastRunTime = useFlowStore(
|
||||
(state) => state.flowBuildStatus[data.id]?.timestamp,
|
||||
);
|
||||
const [validationStatus, setValidationStatus] =
|
||||
useState<validationStatusType | null>(null);
|
||||
useState<VertexBuildTypeAPI | null>(null);
|
||||
const [handles, setHandles] = useState<number>(0);
|
||||
|
||||
const [validationString, setValidationString] = useState<string>("");
|
||||
|
||||
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
|
||||
|
||||
useEffect(() => {
|
||||
// This one should run only once
|
||||
// first check if data.type in NATIVE_CATEGORIES
|
||||
// if not return
|
||||
if (!data.node?.template?.code?.value) return;
|
||||
const thisNodeTemplate = templates[data.type]?.template;
|
||||
// if the template does not have a code key
|
||||
// return
|
||||
if (!thisNodeTemplate?.code) return;
|
||||
const currentCode = thisNodeTemplate.code?.value;
|
||||
const thisNodesCode = data.node!.template?.code?.value;
|
||||
const componentsToIgnore = ["Custom Component"];
|
||||
if (
|
||||
currentCode !== thisNodesCode &&
|
||||
!componentsToIgnore.includes(data.node!.display_name)
|
||||
) {
|
||||
setIsOutdated(true);
|
||||
} else {
|
||||
setIsOutdated(false);
|
||||
}
|
||||
// template.code can be undefined
|
||||
}, [data.node?.template?.code?.value]);
|
||||
|
||||
const updateNodeCode = useCallback(
|
||||
(newNodeClass: APIClassType, code: string, name: string) => {
|
||||
setNode(data.id, (oldNode) => {
|
||||
let newNode = cloneDeep(oldNode);
|
||||
|
||||
newNode.data = {
|
||||
...newNode.data,
|
||||
node: newNodeClass,
|
||||
description: newNodeClass.description ?? data.node!.description,
|
||||
display_name: newNodeClass.display_name ?? data.node!.display_name,
|
||||
};
|
||||
|
||||
newNode.data.node.template[name].value = code;
|
||||
setIsOutdated(false);
|
||||
|
||||
return newNode;
|
||||
});
|
||||
|
||||
updateNodeInternals(data.id);
|
||||
},
|
||||
[data.id, data.node, setNode, setIsOutdated],
|
||||
);
|
||||
|
||||
if (!data.node!.template) {
|
||||
setErrorData({
|
||||
title: `Error in component ${data.node!.display_name}`,
|
||||
list: [
|
||||
`The component ${data.node!.display_name} has no template.`,
|
||||
`Please contact the developer of the component to fix this issue.`,
|
||||
],
|
||||
});
|
||||
takeSnapshot();
|
||||
deleteNode(data.id);
|
||||
}
|
||||
|
||||
function countHandles(): void {
|
||||
let count = Object.keys(data.node!.template)
|
||||
.filter((templateField) => templateField.charAt(0) !== "_")
|
||||
.map((templateCamp) => {
|
||||
const { template } = data.node!;
|
||||
if (template[templateCamp].input_types) return true;
|
||||
if (!template[templateCamp].show) return false;
|
||||
switch (template[templateCamp].type) {
|
||||
case "str":
|
||||
case "bool":
|
||||
case "float":
|
||||
case "code":
|
||||
case "prompt":
|
||||
case "file":
|
||||
case "int":
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
})
|
||||
.reduce((total, value) => total + (value ? 1 : 0), 0);
|
||||
|
||||
setHandles(count);
|
||||
}
|
||||
useEffect(() => {
|
||||
countHandles();
|
||||
}, [data, data.node]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selected) {
|
||||
setInputName(false);
|
||||
setInputDescription(false);
|
||||
}
|
||||
}, [selected]);
|
||||
|
||||
const iconStatus = useIconStatus(buildStatus, validationStatus);
|
||||
const [showNode, setShowNode] = useState(data.showNode ?? true);
|
||||
// State for outline color
|
||||
const isBuilding = useFlowStore((state) => state.isBuilding);
|
||||
|
||||
// should be empty string if no duration
|
||||
// else should be `Duration: ${duration}`
|
||||
const getDurationString = (duration: number | undefined): string => {
|
||||
if (duration === undefined) {
|
||||
return "";
|
||||
} else {
|
||||
return `${duration}`;
|
||||
}
|
||||
};
|
||||
const durationString = getDurationString(validationStatus?.data.duration);
|
||||
const updateNodeCode = useUpdateNodeCode(
|
||||
data?.id,
|
||||
data.node!,
|
||||
setNode,
|
||||
setIsOutdated,
|
||||
updateNodeInternals,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setNodeDescription(data.node!.description);
|
||||
}, [data.node!.description]);
|
||||
|
||||
useEffect(() => {
|
||||
setNodeName(data.node!.display_name);
|
||||
}, [data.node!.display_name]);
|
||||
|
||||
useEffect(() => {
|
||||
const relevantData =
|
||||
flowPool[data.id] && flowPool[data.id]?.length > 0
|
||||
? flowPool[data.id][flowPool[data.id].length - 1]
|
||||
: null;
|
||||
if (relevantData) {
|
||||
// Extract validation information from relevantData and update the validationStatus state
|
||||
setValidationStatus(relevantData);
|
||||
} else {
|
||||
setValidationStatus(null);
|
||||
}
|
||||
}, [flowPool[data.id], data.id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (validationStatus?.params) {
|
||||
// if it is not a string turn it into a string
|
||||
let newValidationString = validationStatus.params;
|
||||
if (typeof newValidationString !== "string") {
|
||||
newValidationString = JSON.stringify(validationStatus.params);
|
||||
}
|
||||
|
||||
setValidationString(newValidationString);
|
||||
}
|
||||
}, [validationStatus, validationStatus?.params]);
|
||||
|
||||
const [showNode, setShowNode] = useState(data.showNode ?? true);
|
||||
|
||||
useEffect(() => {
|
||||
setShowNode(data.showNode ?? true);
|
||||
}, [data.showNode]);
|
||||
|
||||
const nameEditable = true;
|
||||
|
||||
const emojiRegex = /\p{Emoji}/u;
|
||||
const isEmoji = emojiRegex.test(data?.node?.icon!);
|
||||
|
||||
const iconNodeRender = useCallback(() => {
|
||||
const iconElement = data?.node?.icon;
|
||||
const iconColor = nodeColors[types[data.type]];
|
||||
const iconName =
|
||||
iconElement || (data.node?.flow ? "group_components" : name);
|
||||
const iconClassName = `generic-node-icon ${
|
||||
!showNode ? " absolute inset-x-6 h-12 w-12 " : ""
|
||||
}`;
|
||||
if (iconElement && isEmoji) {
|
||||
return nodeIconFragment(iconElement);
|
||||
} else {
|
||||
return checkNodeIconFragment(iconColor, iconName, iconClassName);
|
||||
}
|
||||
}, [data, isEmoji, name, showNode]);
|
||||
const name = nodeIconsLucide[data.type] ? data.type : types[data.type];
|
||||
|
||||
const nodeIconFragment = (icon) => {
|
||||
return <span className="text-lg">{icon}</span>;
|
||||
|
|
@ -252,79 +106,24 @@ export default function GenericNode({
|
|||
);
|
||||
};
|
||||
|
||||
const isDark = useDarkStore((state) => state.dark);
|
||||
const renderIconStatus = (
|
||||
buildStatus: BuildStatus | undefined,
|
||||
validationStatus: validationStatusType | null,
|
||||
) => {
|
||||
if (buildStatus === BuildStatus.BUILDING) {
|
||||
return <Loading className="text-medium-indigo" />;
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<IconComponent
|
||||
name="Play"
|
||||
className="absolute ml-0.5 h-5 fill-current stroke-2 text-medium-indigo opacity-0 transition-all group-hover:opacity-100"
|
||||
/>
|
||||
{validationStatus && validationStatus.valid ? (
|
||||
<Checkmark
|
||||
className="absolute ml-0.5 h-5 stroke-2 text-status-green opacity-100 transition-all group-hover:opacity-0"
|
||||
isVisible={true}
|
||||
/>
|
||||
) : validationStatus &&
|
||||
!validationStatus.valid &&
|
||||
buildStatus === BuildStatus.INACTIVE ? (
|
||||
<IconComponent
|
||||
name="Play"
|
||||
className="absolute ml-0.5 h-5 fill-current stroke-2 text-status-green opacity-30 transition-all group-hover:opacity-0"
|
||||
/>
|
||||
) : buildStatus === BuildStatus.ERROR ||
|
||||
(validationStatus && !validationStatus.valid) ? (
|
||||
<Xmark
|
||||
isVisible={true}
|
||||
className="absolute ml-0.5 h-5 fill-current stroke-2 text-status-red opacity-100 transition-all group-hover:opacity-0"
|
||||
/>
|
||||
) : (
|
||||
<IconComponent
|
||||
name="Play"
|
||||
className="absolute ml-0.5 h-5 fill-current stroke-2 text-muted-foreground opacity-100 transition-all group-hover:opacity-0"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
const getSpecificClassFromBuildStatus = (
|
||||
buildStatus: BuildStatus | undefined,
|
||||
validationStatus: validationStatusType | null,
|
||||
) => {
|
||||
let isInvalid = validationStatus && !validationStatus.valid;
|
||||
|
||||
if (buildStatus === BuildStatus.INACTIVE) {
|
||||
// INACTIVE should have its own class
|
||||
return "inactive-status";
|
||||
}
|
||||
if (
|
||||
(buildStatus === BuildStatus.BUILT && isInvalid) ||
|
||||
buildStatus === BuildStatus.ERROR
|
||||
) {
|
||||
return isDark ? "built-invalid-status-dark" : "built-invalid-status";
|
||||
} else if (buildStatus === BuildStatus.BUILDING) {
|
||||
return "building-status";
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
const renderIconStatus = () => {
|
||||
return (
|
||||
<div className="generic-node-status-position flex items-center justify-center">
|
||||
{iconStatus}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getNodeBorderClassName = (
|
||||
selected: boolean,
|
||||
showNode: boolean,
|
||||
buildStatus: BuildStatus | undefined,
|
||||
validationStatus: validationStatusType | null,
|
||||
validationStatus: VertexBuildTypeAPI | null,
|
||||
) => {
|
||||
const specificClassFromBuildStatus = getSpecificClassFromBuildStatus(
|
||||
buildStatus,
|
||||
validationStatus,
|
||||
isDark,
|
||||
);
|
||||
|
||||
const baseBorderClass = getBaseBorderClass(selected);
|
||||
|
|
@ -332,12 +131,14 @@ export default function GenericNode({
|
|||
const names = classNames(
|
||||
baseBorderClass,
|
||||
nodeSizeClass,
|
||||
"generic-node-div",
|
||||
"generic-node-div group/node",
|
||||
specificClassFromBuildStatus,
|
||||
);
|
||||
return names;
|
||||
};
|
||||
|
||||
// const [openWDoubleCLick, setOpenWDoubleCLick] = useState(false);
|
||||
|
||||
const getBaseBorderClass = (selected) => {
|
||||
let className = selected ? "border border-ring" : "border";
|
||||
let frozenClass = selected ? "border-ring-frozen" : "border-frozen";
|
||||
|
|
@ -347,10 +148,99 @@ export default function GenericNode({
|
|||
const getNodeSizeClass = (showNode) =>
|
||||
showNode ? "w-96 rounded-lg" : "w-26 h-26 rounded-full";
|
||||
|
||||
const nameEditable = true;
|
||||
const isEmoji = emojiRegex().test(data?.node?.icon!);
|
||||
|
||||
if (!data.node!.template) {
|
||||
setErrorData({
|
||||
title: `Error in component ${data.node!.display_name}`,
|
||||
list: [
|
||||
`The component ${data.node!.display_name} has no template.`,
|
||||
`Please contact the developer of the component to fix this issue.`,
|
||||
],
|
||||
});
|
||||
takeSnapshot();
|
||||
deleteNode(data.id);
|
||||
}
|
||||
|
||||
useCheckCodeValidity(data, templates, setIsOutdated, types);
|
||||
useValidationStatusString(validationStatus, setValidationString);
|
||||
useUpdateValidationStatus(data?.id, flowPool, setValidationStatus);
|
||||
|
||||
const iconNodeRender = useIconNodeRender(
|
||||
data,
|
||||
types,
|
||||
nodeColors,
|
||||
name,
|
||||
showNode,
|
||||
isEmoji,
|
||||
nodeIconFragment,
|
||||
checkNodeIconFragment,
|
||||
);
|
||||
|
||||
function countHandles(): void {
|
||||
const count = countHandlesFn(data);
|
||||
setHandles(count);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
countHandles();
|
||||
}, [data, data.node]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selected) {
|
||||
setInputName(false);
|
||||
setInputDescription(false);
|
||||
}
|
||||
}, [selected]);
|
||||
|
||||
useEffect(() => {
|
||||
setNodeDescription(data.node!.description);
|
||||
}, [data.node!.description]);
|
||||
|
||||
useEffect(() => {
|
||||
setNodeName(data.node!.display_name);
|
||||
}, [data.node!.display_name]);
|
||||
|
||||
useEffect(() => {
|
||||
setShowNode(data.showNode ?? true);
|
||||
}, [data.showNode]);
|
||||
|
||||
const [loadingUpdate, setLoadingUpdate] = useState(false);
|
||||
|
||||
const handleUpdateCode = () => {
|
||||
setLoadingUpdate(true);
|
||||
takeSnapshot();
|
||||
// to update we must get the code from the templates in useTypesStore
|
||||
const thisNodeTemplate = templates[data.type]?.template;
|
||||
// if the template does not have a code key
|
||||
// return
|
||||
if (!thisNodeTemplate?.code) return;
|
||||
|
||||
const currentCode = thisNodeTemplate.code.value;
|
||||
if (data.node) {
|
||||
postCustomComponent(currentCode, data.node)
|
||||
.then((apiReturn) => {
|
||||
const { data } = apiReturn;
|
||||
if (data && updateNodeCode) {
|
||||
updateNodeCode(data, currentCode, "code");
|
||||
setLoadingUpdate(false);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const shortcuts = useShortcutsStore((state) => state.shortcuts);
|
||||
|
||||
const memoizedNodeToolbarComponent = useMemo(() => {
|
||||
return (
|
||||
<NodeToolbar>
|
||||
<NodeToolbarComponent
|
||||
// openWDoubleClick={openWDoubleCLick}
|
||||
// setOpenWDoubleClick={setOpenWDoubleCLick}
|
||||
data={data}
|
||||
deleteNode={(id) => {
|
||||
takeSnapshot();
|
||||
|
|
@ -367,8 +257,6 @@ export default function GenericNode({
|
|||
showNode={showNode}
|
||||
openAdvancedModal={false}
|
||||
onCloseAdvancedModal={() => {}}
|
||||
updateNodeCode={updateNodeCode}
|
||||
isOutdated={isOutdated}
|
||||
selected={selected}
|
||||
/>
|
||||
</NodeToolbar>
|
||||
|
|
@ -384,11 +272,18 @@ export default function GenericNode({
|
|||
updateNodeCode,
|
||||
isOutdated,
|
||||
selected,
|
||||
shortcuts,
|
||||
// openWDoubleCLick,
|
||||
// setOpenWDoubleCLick,
|
||||
]);
|
||||
return (
|
||||
<>
|
||||
{memoizedNodeToolbarComponent}
|
||||
<div
|
||||
// onDoubleClick={(event) => {
|
||||
// if (!isWrappedWithClass(event, "nodoubleclick"))
|
||||
// setOpenWDoubleCLick(true);
|
||||
// }}
|
||||
className={getNodeBorderClassName(
|
||||
selected,
|
||||
showNode,
|
||||
|
|
@ -462,7 +357,7 @@ export default function GenericNode({
|
|||
event.preventDefault();
|
||||
}}
|
||||
data-testid={"title-" + data.node?.display_name}
|
||||
className="generic-node-tooltip-div cursor-text text-primary"
|
||||
className="nodoubleclick generic-node-tooltip-div cursor-text text-primary"
|
||||
>
|
||||
{data.node?.display_name}
|
||||
</div>
|
||||
|
|
@ -582,67 +477,73 @@ export default function GenericNode({
|
|||
)}
|
||||
</div>
|
||||
{showNode && (
|
||||
<ShadTooltip
|
||||
content={
|
||||
buildStatus === BuildStatus.BUILDING ? (
|
||||
<span> {STATUS_BUILDING} </span>
|
||||
) : !validationStatus ? (
|
||||
<span className="flex">{STATUS_BUILD}</span>
|
||||
) : (
|
||||
<div className="max-h-100 p-2">
|
||||
<div>
|
||||
{lastRunTime && (
|
||||
<>
|
||||
<div className="flex flex-shrink-0 items-center gap-1">
|
||||
{isOutdated && (
|
||||
<ShadTooltip content={TOOLTIP_OUTDATED_NODE}>
|
||||
<Button
|
||||
onClick={handleUpdateCode}
|
||||
variant="secondary"
|
||||
className={"h-9 px-1.5"}
|
||||
loading={loadingUpdate}
|
||||
>
|
||||
<IconComponent
|
||||
name="AlertTriangle"
|
||||
className="h-5 w-5 text-status-yellow"
|
||||
/>
|
||||
</Button>
|
||||
</ShadTooltip>
|
||||
)}
|
||||
<ShadTooltip
|
||||
content={
|
||||
buildStatus === BuildStatus.BUILDING ? (
|
||||
<span> {STATUS_BUILDING} </span>
|
||||
) : !validationStatus ? (
|
||||
<span className="flex">{STATUS_BUILD}</span>
|
||||
) : (
|
||||
<div className="max-h-100 p-2">
|
||||
<div>
|
||||
{lastRunTime && (
|
||||
<div className="justify-left flex font-normal text-muted-foreground">
|
||||
<div>{RUN_TIMESTAMP_PREFIX}</div>
|
||||
<div className="ml-1 text-status-blue">
|
||||
{lastRunTime}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="justify-left flex font-normal text-muted-foreground">
|
||||
<div>{RUN_TIMESTAMP_PREFIX}</div>
|
||||
<div>Duration:</div>
|
||||
<div className="ml-1 text-status-blue">
|
||||
{lastRunTime}
|
||||
{validationStatus?.data.duration}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="justify-left flex font-normal text-muted-foreground">
|
||||
<div>Duration:</div>
|
||||
<div className="mb-3 ml-1 text-status-blue">
|
||||
{validationStatus?.data.duration}
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<span className="mb-2 mt-2 flex justify-center font-semibold text-muted-foreground">
|
||||
Output
|
||||
</span>
|
||||
<div className="max-h-96 overflow-auto font-normal custom-scroll">
|
||||
{validationString.split("\n").map((line, index) => (
|
||||
<div className="font-normal" key={index}>
|
||||
{line}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
side="bottom"
|
||||
>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (buildStatus === BuildStatus.BUILDING || isBuilding)
|
||||
return;
|
||||
setValidationStatus(null);
|
||||
buildFlow({ stopNodeId: data.id });
|
||||
}}
|
||||
variant="secondary"
|
||||
className={"group h-9 px-1.5"}
|
||||
>
|
||||
<div
|
||||
data-testid={
|
||||
`button_run_` + data?.node?.display_name.toLowerCase()
|
||||
)
|
||||
}
|
||||
side="bottom"
|
||||
>
|
||||
<div className="generic-node-status-position flex items-center justify-center">
|
||||
{renderIconStatus(buildStatus, validationStatus)}
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
</ShadTooltip>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (buildStatus === BuildStatus.BUILDING || isBuilding)
|
||||
return;
|
||||
setValidationStatus(null);
|
||||
buildFlow({ stopNodeId: data.id });
|
||||
}}
|
||||
variant="secondary"
|
||||
className={"group h-9 px-1.5"}
|
||||
>
|
||||
<div
|
||||
data-testid={
|
||||
`button_run_` + data?.node?.display_name.toLowerCase()
|
||||
}
|
||||
>
|
||||
{renderIconStatus()}
|
||||
</div>
|
||||
</Button>
|
||||
</ShadTooltip>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -704,7 +605,7 @@ export default function GenericNode({
|
|||
) : (
|
||||
<div
|
||||
className={cn(
|
||||
"generic-node-desc-text cursor-text truncate-multiline word-break-break-word",
|
||||
"nodoubleclick generic-node-desc-text cursor-text truncate-multiline word-break-break-word",
|
||||
(data.node?.description === "" ||
|
||||
!data.node?.description) &&
|
||||
nameEditable
|
||||
26
src/frontend/src/CustomNodes/helpers/count-handles.ts
Normal file
26
src/frontend/src/CustomNodes/helpers/count-handles.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { NodeDataType } from "../../types/flow";
|
||||
|
||||
export function countHandlesFn(data: NodeDataType): number {
|
||||
let count = Object.keys(data.node!.template)
|
||||
.filter((templateField) => templateField.charAt(0) !== "_")
|
||||
.map((templateCamp) => {
|
||||
const { template } = data.node!;
|
||||
if (template[templateCamp].input_types) return true;
|
||||
if (!template[templateCamp].show) return false;
|
||||
switch (template[templateCamp].type) {
|
||||
case "str":
|
||||
case "bool":
|
||||
case "float":
|
||||
case "code":
|
||||
case "prompt":
|
||||
case "file":
|
||||
case "int":
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
})
|
||||
.reduce((total, value) => total + (value ? 1 : 0), 0);
|
||||
|
||||
return count;
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import { BuildStatus } from "../../constants/enums";
|
||||
import { VertexBuildTypeAPI } from "../../types/api";
|
||||
|
||||
export const getSpecificClassFromBuildStatus = (
|
||||
buildStatus: BuildStatus | undefined,
|
||||
validationStatus: VertexBuildTypeAPI | null,
|
||||
isDark: boolean,
|
||||
) => {
|
||||
let isInvalid = validationStatus && !validationStatus.valid;
|
||||
|
||||
if (buildStatus === BuildStatus.INACTIVE) {
|
||||
// INACTIVE should have its own class
|
||||
return "inactive-status";
|
||||
}
|
||||
if (
|
||||
(buildStatus === BuildStatus.BUILT && isInvalid) ||
|
||||
buildStatus === BuildStatus.ERROR
|
||||
) {
|
||||
return isDark ? "built-invalid-status-dark" : "built-invalid-status";
|
||||
} else if (buildStatus === BuildStatus.BUILDING) {
|
||||
return "building-status";
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
import { useEffect } from "react";
|
||||
import { NATIVE_CATEGORIES } from "../../constants/constants";
|
||||
import { NodeDataType } from "../../types/flow";
|
||||
|
||||
const useCheckCodeValidity = (
|
||||
data: NodeDataType,
|
||||
templates: { [key: string]: any },
|
||||
setIsOutdated: (value: boolean) => void,
|
||||
types,
|
||||
) => {
|
||||
useEffect(() => {
|
||||
// This one should run only once
|
||||
// first check if data.type in NATIVE_CATEGORIES
|
||||
// if not return
|
||||
if (
|
||||
!NATIVE_CATEGORIES.includes(types[data.type]) ||
|
||||
!data.node?.template?.code?.value
|
||||
)
|
||||
return;
|
||||
const thisNodeTemplate = templates[data.type].template;
|
||||
// if the template does not have a code key
|
||||
// return
|
||||
if (!thisNodeTemplate.code) return;
|
||||
const currentCode = thisNodeTemplate.code?.value;
|
||||
const thisNodesCode = data.node!.template?.code?.value;
|
||||
const componentsToIgnore = ["CustomComponent", "Prompt"];
|
||||
if (
|
||||
currentCode !== thisNodesCode &&
|
||||
!componentsToIgnore.includes(data.type) &&
|
||||
!(data.node?.edited ?? false)
|
||||
) {
|
||||
setIsOutdated(true);
|
||||
} else {
|
||||
setIsOutdated(false);
|
||||
}
|
||||
// template.code can be undefined
|
||||
}, [data.node?.template?.code?.value, templates, setIsOutdated]);
|
||||
};
|
||||
|
||||
export default useCheckCodeValidity;
|
||||
45
src/frontend/src/CustomNodes/hooks/use-icon-render.tsx
Normal file
45
src/frontend/src/CustomNodes/hooks/use-icon-render.tsx
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { useCallback } from "react";
|
||||
import { NodeDataType } from "../../types/flow";
|
||||
|
||||
const useIconNodeRender = (
|
||||
data: NodeDataType,
|
||||
types: { [key: string]: string },
|
||||
nodeColors: { [key: string]: string },
|
||||
name: string,
|
||||
showNode: boolean,
|
||||
isEmoji: boolean,
|
||||
nodeIconFragment: (iconElement: string) => JSX.Element,
|
||||
checkNodeIconFragment: (
|
||||
iconColor: string,
|
||||
iconName: string,
|
||||
iconClassName: string,
|
||||
) => JSX.Element,
|
||||
) => {
|
||||
const iconNodeRender = useCallback(() => {
|
||||
const iconElement = data?.node?.icon;
|
||||
const iconColor = nodeColors[types[data.type]];
|
||||
const iconName =
|
||||
iconElement || (data.node?.flow ? "group_components" : name);
|
||||
const iconClassName = `generic-node-icon ${
|
||||
!showNode ? " absolute inset-x-6 h-12 w-12 " : ""
|
||||
}`;
|
||||
if (iconElement && isEmoji) {
|
||||
return nodeIconFragment(iconElement);
|
||||
} else {
|
||||
return checkNodeIconFragment(iconColor, iconName, iconClassName);
|
||||
}
|
||||
}, [
|
||||
data,
|
||||
types,
|
||||
nodeColors,
|
||||
name,
|
||||
showNode,
|
||||
isEmoji,
|
||||
nodeIconFragment,
|
||||
checkNodeIconFragment,
|
||||
]);
|
||||
|
||||
return iconNodeRender;
|
||||
};
|
||||
|
||||
export default useIconNodeRender;
|
||||
54
src/frontend/src/CustomNodes/hooks/use-icons-status.tsx
Normal file
54
src/frontend/src/CustomNodes/hooks/use-icons-status.tsx
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import IconComponent from "../../components/genericIconComponent";
|
||||
import Checkmark from "../../components/ui/checkmark";
|
||||
import Loading from "../../components/ui/loading";
|
||||
import Xmark from "../../components/ui/xmark";
|
||||
import { BuildStatus } from "../../constants/enums";
|
||||
import { VertexBuildTypeAPI } from "../../types/api";
|
||||
|
||||
const useIconStatus = (
|
||||
buildStatus: BuildStatus | undefined,
|
||||
validationStatus: VertexBuildTypeAPI | null,
|
||||
) => {
|
||||
const renderIconStatus = () => {
|
||||
if (buildStatus === BuildStatus.BUILDING) {
|
||||
return <Loading className="text-medium-indigo" />;
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<IconComponent
|
||||
name="Play"
|
||||
className="absolute ml-0.5 h-5 fill-current stroke-2 text-medium-indigo opacity-0 transition-all group-hover:opacity-100"
|
||||
/>
|
||||
{validationStatus && validationStatus.valid ? (
|
||||
<Checkmark
|
||||
className="absolute ml-0.5 h-5 stroke-2 text-status-green opacity-100 transition-all group-hover:opacity-0"
|
||||
isVisible={true}
|
||||
/>
|
||||
) : validationStatus &&
|
||||
!validationStatus.valid &&
|
||||
buildStatus === BuildStatus.INACTIVE ? (
|
||||
<IconComponent
|
||||
name="Play"
|
||||
className="absolute ml-0.5 h-5 fill-current stroke-2 text-status-green opacity-30 transition-all group-hover:opacity-0"
|
||||
/>
|
||||
) : buildStatus === BuildStatus.ERROR ||
|
||||
(validationStatus && !validationStatus.valid) ? (
|
||||
<Xmark
|
||||
isVisible={true}
|
||||
className="absolute ml-0.5 h-5 fill-current stroke-2 text-status-red opacity-100 transition-all group-hover:opacity-0"
|
||||
/>
|
||||
) : (
|
||||
<IconComponent
|
||||
name="Play"
|
||||
className="absolute ml-0.5 h-5 fill-current stroke-2 text-muted-foreground opacity-100 transition-all group-hover:opacity-0"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return renderIconStatus();
|
||||
};
|
||||
|
||||
export default useIconStatus;
|
||||
39
src/frontend/src/CustomNodes/hooks/use-update-node-code.tsx
Normal file
39
src/frontend/src/CustomNodes/hooks/use-update-node-code.tsx
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { cloneDeep } from "lodash"; // or any other deep cloning library you prefer
|
||||
import { useCallback } from "react";
|
||||
import { APIClassType } from "../../types/api";
|
||||
|
||||
const useUpdateNodeCode = (
|
||||
dataId: string,
|
||||
dataNode: APIClassType, // Define YourNodeType according to your data structure
|
||||
setNode: (id: string, callback: (oldNode) => any) => void,
|
||||
setIsOutdated: (value: boolean) => void,
|
||||
updateNodeInternals: (id: string) => void,
|
||||
) => {
|
||||
const updateNodeCode = useCallback(
|
||||
(newNodeClass: APIClassType, code: string, name: string) => {
|
||||
setNode(dataId, (oldNode) => {
|
||||
let newNode = cloneDeep(oldNode);
|
||||
|
||||
newNode.data = {
|
||||
...newNode.data,
|
||||
node: newNodeClass,
|
||||
description: newNodeClass.description ?? dataNode.description,
|
||||
display_name: newNodeClass.display_name ?? dataNode.display_name,
|
||||
edited: false,
|
||||
};
|
||||
|
||||
newNode.data.node.template[name].value = code;
|
||||
setIsOutdated(false);
|
||||
|
||||
return newNode;
|
||||
});
|
||||
|
||||
updateNodeInternals(dataId);
|
||||
},
|
||||
[dataId, dataNode, setNode, setIsOutdated, updateNodeInternals],
|
||||
);
|
||||
|
||||
return updateNodeCode;
|
||||
};
|
||||
|
||||
export default useUpdateNodeCode;
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import { useEffect } from "react";
|
||||
|
||||
const useUpdateValidationStatus = (dataId, flowPool, setValidationStatus) => {
|
||||
useEffect(() => {
|
||||
const relevantData =
|
||||
flowPool[dataId] && flowPool[dataId]?.length > 0
|
||||
? flowPool[dataId][flowPool[dataId].length - 1]
|
||||
: null;
|
||||
if (relevantData) {
|
||||
// Extract validation information from relevantData and update the validationStatus state
|
||||
setValidationStatus(relevantData);
|
||||
} else {
|
||||
setValidationStatus(null);
|
||||
}
|
||||
}, [flowPool[dataId], dataId, setValidationStatus]);
|
||||
};
|
||||
|
||||
export default useUpdateValidationStatus;
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import { useEffect } from "react";
|
||||
|
||||
const useValidationStatusString = (validationStatus, setValidationString) => {
|
||||
useEffect(() => {
|
||||
if (validationStatus?.data.logs) {
|
||||
// if it is not a string turn it into a string
|
||||
let newValidationString = "";
|
||||
if (Array.isArray(validationStatus.data.logs)) {
|
||||
newValidationString = validationStatus.data.logs
|
||||
.map((log) => (log?.message ? log.message : JSON.stringify(log)))
|
||||
.join("\n");
|
||||
}
|
||||
if (typeof newValidationString !== "string") {
|
||||
newValidationString = JSON.stringify(validationStatus.data.logs);
|
||||
}
|
||||
|
||||
setValidationString(newValidationString);
|
||||
}
|
||||
}, [validationStatus, validationStatus?.data.logs, setValidationString]);
|
||||
};
|
||||
|
||||
export default useValidationStatusString;
|
||||
|
|
@ -51,13 +51,15 @@ export default function ErrorAlert({
|
|||
/>
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<h3 className="error-build-foreground">{title}</h3>
|
||||
<h3 className="error-build-foreground line-clamp-2">{title}</h3>
|
||||
{list?.length !== 0 &&
|
||||
list?.some((item) => item !== null && item !== undefined) ? (
|
||||
<div className="error-build-message-div">
|
||||
<ul className="error-build-message-list">
|
||||
{list.map((item, index) => (
|
||||
<li key={index}>{item}</li>
|
||||
<li key={index} className="line-clamp-5">
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ export default function NoticeAlert({
|
|||
/>
|
||||
</div>
|
||||
<div className="ml-3 flex-1 md:flex md:justify-between">
|
||||
<p className="text-sm text-info-foreground word-break-break-word">
|
||||
<p className="line-clamp-2 text-sm text-info-foreground word-break-break-word">
|
||||
{title}
|
||||
</p>
|
||||
<p className="mt-3 text-sm md:ml-6 md:mt-0">
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ export default function SuccessAlert({
|
|||
/>
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<p className="success-alert-message">{title}</p>
|
||||
<p className="success-alert-message line-clamp-2">{title}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
BIN
src/frontend/src/assets/profile-circle.png
Normal file
BIN
src/frontend/src/assets/profile-circle.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
|
|
@ -7,7 +7,6 @@ import {
|
|||
} from "../../components/ui/accordion";
|
||||
import { AccordionComponentType } from "../../types/components";
|
||||
import { cn } from "../../utils/utils";
|
||||
import ShadTooltip from "../shadTooltipComponent";
|
||||
|
||||
export default function AccordionComponent({
|
||||
trigger,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
import TableAutoCellRender from "../tableComponent/components/tableAutoCellRender";
|
||||
|
||||
export default function ArrayReader({ array }: { array: any[] }): JSX.Element {
|
||||
//TODO check array type
|
||||
return (
|
||||
<div>
|
||||
<ul>
|
||||
{array.map((item, index) => (
|
||||
<li key={index}>{item}</li>
|
||||
<li key={index}>{<TableAutoCellRender value={item} />}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -27,8 +27,8 @@ import {
|
|||
import { Checkbox } from "../ui/checkbox";
|
||||
import { FormControl, FormField } from "../ui/form";
|
||||
import Loading from "../ui/loading";
|
||||
import { convertTestName } from "./utils/convert-test-name";
|
||||
import DragCardComponent from "./components/dragCardComponent";
|
||||
import { convertTestName } from "./utils/convert-test-name";
|
||||
|
||||
export default function CollectionCardComponent({
|
||||
data,
|
||||
|
|
@ -217,7 +217,7 @@ export default function CollectionCardComponent({
|
|||
data-testid={`card-${convertTestName(data.name)}`}
|
||||
//TODO check color schema
|
||||
className={cn(
|
||||
"group relative flex min-h-[11rem] flex-col justify-between overflow-hidden transition-all hover:bg-muted/50 hover:shadow-md hover:dark:bg-[#ffffff10]",
|
||||
"group relative flex h-[11rem] flex-col justify-between overflow-hidden hover:bg-muted/50 hover:shadow-md hover:dark:bg-[#5f5f5f0e]",
|
||||
disabled ? "pointer-events-none opacity-50" : "",
|
||||
onClick ? "cursor-pointer" : "",
|
||||
isSelectedCard ? "border border-selected" : "",
|
||||
|
|
|
|||
|
|
@ -1,40 +1,53 @@
|
|||
import { Transition } from "@headlessui/react";
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import IOModal from "../../modals/IOModal";
|
||||
import ApiModal from "../../modals/apiModal/views";
|
||||
import ShareModal from "../../modals/shareModal";
|
||||
import useFlowStore from "../../stores/flowStore";
|
||||
import useFlowsManagerStore from "../../stores/flowsManagerStore";
|
||||
import { useShortcutsStore } from "../../stores/shortcuts";
|
||||
import { useStoreStore } from "../../stores/storeStore";
|
||||
import { classNames } from "../../utils/utils";
|
||||
import { classNames, isThereModal } from "../../utils/utils";
|
||||
import ForwardedIconComponent from "../genericIconComponent";
|
||||
import { Separator } from "../ui/separator";
|
||||
|
||||
export default function FlowToolbar(): JSX.Element {
|
||||
const [open, setOpen] = useState(false);
|
||||
const preventDefault = true;
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
const [openCodeModal, setOpenCodeModal] = useState<boolean>(false);
|
||||
const [openShareModal, setOpenShareModal] = useState<boolean>(false);
|
||||
function handleAPIWShortcut(e: KeyboardEvent) {
|
||||
if (isThereModal() && !openCodeModal) return;
|
||||
setOpenCodeModal((oldOpen) => !oldOpen);
|
||||
}
|
||||
|
||||
function handleChatWShortcut(e: KeyboardEvent) {
|
||||
if (isThereModal() && !open) return;
|
||||
if (useFlowStore.getState().hasIO) {
|
||||
setOpen((oldState) => !oldState);
|
||||
}
|
||||
}
|
||||
|
||||
function handleShareWShortcut(e: KeyboardEvent) {
|
||||
if (isThereModal() && !openShareModal) return;
|
||||
setOpenShareModal((oldState) => !oldState);
|
||||
}
|
||||
|
||||
const openPlayground = useShortcutsStore((state) => state.open);
|
||||
const api = useShortcutsStore((state) => state.api);
|
||||
const flow = useShortcutsStore((state) => state.flow);
|
||||
|
||||
useHotkeys(openPlayground, handleChatWShortcut, { preventDefault });
|
||||
useHotkeys(api, handleAPIWShortcut, { preventDefault });
|
||||
useHotkeys(flow, handleShareWShortcut, { preventDefault });
|
||||
|
||||
const hasIO = useFlowStore((state) => state.hasIO);
|
||||
const hasStore = useStoreStore((state) => state.hasStore);
|
||||
const validApiKey = useStoreStore((state) => state.validApiKey);
|
||||
const hasApiKey = useStoreStore((state) => state.hasApiKey);
|
||||
const currentFlow = useFlowsManagerStore((state) => state.currentFlow);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (
|
||||
(event.key === "K" || event.key === "k") &&
|
||||
(event.metaKey || event.ctrlKey) &&
|
||||
useFlowStore.getState().hasIO
|
||||
) {
|
||||
event.preventDefault();
|
||||
setOpen((oldState) => !oldState);
|
||||
}
|
||||
};
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
return () => {
|
||||
document.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const prevNodesRef = useRef<any[] | undefined>();
|
||||
|
||||
const ModalMemo = useMemo(
|
||||
|
|
@ -43,6 +56,8 @@ export default function FlowToolbar(): JSX.Element {
|
|||
is_component={false}
|
||||
component={currentFlow!}
|
||||
disabled={!hasApiKey || !validApiKey || !hasStore}
|
||||
open={openShareModal}
|
||||
setOpen={setOpenShareModal}
|
||||
>
|
||||
<button
|
||||
disabled={!hasApiKey || !validApiKey || !hasStore}
|
||||
|
|
@ -66,7 +81,14 @@ export default function FlowToolbar(): JSX.Element {
|
|||
</button>
|
||||
</ShareModal>
|
||||
),
|
||||
[hasApiKey, validApiKey, currentFlow, hasStore],
|
||||
[
|
||||
hasApiKey,
|
||||
validApiKey,
|
||||
currentFlow,
|
||||
hasStore,
|
||||
openShareModal,
|
||||
setOpenShareModal,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
@ -115,7 +137,11 @@ export default function FlowToolbar(): JSX.Element {
|
|||
</div>
|
||||
<div className="flex cursor-pointer items-center gap-2">
|
||||
{currentFlow && currentFlow.data && (
|
||||
<ApiModal flow={currentFlow}>
|
||||
<ApiModal
|
||||
flow={currentFlow}
|
||||
open={openCodeModal}
|
||||
setOpen={setOpenCodeModal}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
"relative inline-flex w-full items-center justify-center gap-1 px-5 py-3 text-sm font-semibold text-foreground transition-all duration-150 ease-in-out hover:bg-hover",
|
||||
|
|
|
|||
|
|
@ -115,6 +115,7 @@ function CsvOutputComponent({
|
|||
style={{ height: "100%", width: "100%" }}
|
||||
>
|
||||
<TableComponent
|
||||
key={"csv-output"}
|
||||
rowData={rowData}
|
||||
columnDefs={colDefs}
|
||||
defaultColDef={defaultColDef}
|
||||
|
|
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
import { gradients } from "../../utils/styleUtils";
|
||||
|
||||
export default function GradientChooserComponent({ value, onChange }) {
|
||||
return (
|
||||
<div className="flex flex-wrap items-center justify-start gap-2">
|
||||
{gradients.map((gradient, idx) => (
|
||||
<div
|
||||
onClick={() => {
|
||||
onChange(gradient);
|
||||
}}
|
||||
className={
|
||||
"duration-400 h-12 w-12 cursor-pointer rounded-full transition-all " +
|
||||
gradient +
|
||||
(value === gradient ? " shadow-lg ring-2 ring-primary" : "")
|
||||
}
|
||||
key={idx}
|
||||
></div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -9,7 +9,7 @@ import {
|
|||
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { UPLOAD_ERROR_ALERT } from "../../../../constants/alerts_constants";
|
||||
import { SAVED_HOVER } from "../../../../constants/constants";
|
||||
import { IS_MAC, SAVED_HOVER } from "../../../../constants/constants";
|
||||
import ExportModal from "../../../../modals/exportModal";
|
||||
import FlowLogsModal from "../../../../modals/flowLogsModal";
|
||||
import FlowSettingsModal from "../../../../modals/flowSettingsModal";
|
||||
|
|
@ -20,8 +20,10 @@ import { cn } from "../../../../utils/utils";
|
|||
import IconComponent from "../../../genericIconComponent";
|
||||
import ShadTooltip from "../../../shadTooltipComponent";
|
||||
import { Button } from "../../../ui/button";
|
||||
import { useShortcutsStore } from "../../../../stores/shortcuts";
|
||||
|
||||
export const MenuBar = ({}: {}): JSX.Element => {
|
||||
const shortcuts = useShortcutsStore((state) => state.shortcuts);
|
||||
const addFlow = useFlowsManagerStore((state) => state.addFlow);
|
||||
const currentFlow = useFlowsManagerStore((state) => state.currentFlow);
|
||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||
|
|
@ -113,7 +115,7 @@ export const MenuBar = ({}: {}): JSX.Element => {
|
|||
title: UPLOAD_ERROR_ALERT,
|
||||
list: [error],
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
}}
|
||||
>
|
||||
|
|
@ -137,17 +139,19 @@ export const MenuBar = ({}: {}): JSX.Element => {
|
|||
>
|
||||
<IconComponent name="Undo" className="header-menu-options " />
|
||||
Undo
|
||||
{navigator.userAgent.toUpperCase().includes("MAC") ? (
|
||||
{IS_MAC ? (
|
||||
<IconComponent
|
||||
name="Command"
|
||||
className="absolute right-[1.15rem] top-[0.65em] h-3.5 w-3.5 stroke-2"
|
||||
/>
|
||||
) : (
|
||||
<span className="absolute right-[1.15rem] top-[0.40em] stroke-2">
|
||||
Ctrl +{" "}
|
||||
{
|
||||
shortcuts.find((s) => s.name.toLowerCase() === "undo")
|
||||
?.shortcut
|
||||
}
|
||||
</span>
|
||||
)}
|
||||
<span className="absolute right-2 top-[0.4em]">Z</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
|
|
@ -157,17 +161,19 @@ export const MenuBar = ({}: {}): JSX.Element => {
|
|||
>
|
||||
<IconComponent name="Redo" className="header-menu-options " />
|
||||
Redo
|
||||
{navigator.userAgent.toUpperCase().includes("MAC") ? (
|
||||
{IS_MAC ? (
|
||||
<IconComponent
|
||||
name="Command"
|
||||
className="absolute right-[1.15rem] top-[0.65em] h-3.5 w-3.5 stroke-2"
|
||||
/>
|
||||
) : (
|
||||
<span className="absolute right-[1.15rem] top-[0.40em] stroke-2">
|
||||
Ctrl +{" "}
|
||||
{
|
||||
shortcuts.find((s) => s.name.toLowerCase() === "redo")
|
||||
?.shortcut
|
||||
}
|
||||
</span>
|
||||
)}
|
||||
<span className="absolute right-2 top-[0.4em]">Y</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
|
@ -195,7 +201,7 @@ export const MenuBar = ({}: {}): JSX.Element => {
|
|||
name={isBuilding || saveLoading ? "Loader2" : "CheckCircle2"}
|
||||
className={cn(
|
||||
"h-4 w-4",
|
||||
isBuilding || saveLoading ? "animate-spin" : "animate-wiggle",
|
||||
isBuilding || saveLoading ? "animate-spin" : "animate-wiggle"
|
||||
)}
|
||||
/>
|
||||
{printByBuildStatus()}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
import { useContext } from "react";
|
||||
import profileCircle from "../../assets/profile-circle.png";
|
||||
import { FaDiscord, FaGithub } from "react-icons/fa";
|
||||
import { RiTwitterXFill } from "react-icons/ri";
|
||||
import { Link, useLocation, useNavigate, useParams } from "react-router-dom";
|
||||
import AlertDropdown from "../../alerts/alertDropDown";
|
||||
import {
|
||||
BACKEND_URL,
|
||||
BASE_URL_API,
|
||||
LOCATIONS_TO_RETURN,
|
||||
USER_PROJECTS_HEADER,
|
||||
} from "../../constants/constants";
|
||||
|
|
@ -81,14 +84,16 @@ export default function Header(): JSX.Element {
|
|||
<span className="ml-4 text-2xl">⛓️</span>
|
||||
</Link>
|
||||
{showArrowReturnIcon && (
|
||||
<button
|
||||
<Button
|
||||
variant="none"
|
||||
size="none"
|
||||
onClick={() => {
|
||||
checkForChanges();
|
||||
redirectToLastLocation();
|
||||
}}
|
||||
>
|
||||
<IconComponent name="ChevronLeft" className="w-4" />
|
||||
</button>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<MenuBar />
|
||||
|
|
@ -186,20 +191,45 @@ export default function Header(): JSX.Element {
|
|||
<Separator orientation="vertical" />
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
<Button
|
||||
variant="none"
|
||||
size="none"
|
||||
data-testid="user-profile-settings"
|
||||
className={
|
||||
"h-7 w-7 rounded-full focus-visible:outline-0 " +
|
||||
(userData?.profile_image ??
|
||||
(userData?.id
|
||||
? gradients[
|
||||
parseInt(userData?.id ?? "", 30) % gradients.length
|
||||
]
|
||||
: "bg-gray-500"))
|
||||
}
|
||||
/>
|
||||
>
|
||||
<img
|
||||
src={
|
||||
`${BACKEND_URL.slice(
|
||||
0,
|
||||
BACKEND_URL.length - 1,
|
||||
)}${BASE_URL_API}files/profile_pictures/${userData?.profile_image ?? "Space/046-rocket.png"}` ??
|
||||
profileCircle
|
||||
}
|
||||
className="h-7 w-7 focus-visible:outline-0 "
|
||||
/>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
{!autoLogin && (
|
||||
<>
|
||||
<DropdownMenuLabel>
|
||||
<div className="flex items-center gap-3">
|
||||
<img
|
||||
src={
|
||||
`${BACKEND_URL.slice(
|
||||
0,
|
||||
BACKEND_URL.length - 1,
|
||||
)}${BASE_URL_API}files/profile_pictures/${userData?.profile_image}` ??
|
||||
profileCircle
|
||||
}
|
||||
className="h-5 w-5 focus-visible:outline-0 "
|
||||
/>
|
||||
|
||||
{userData?.username ?? "User"}
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
<DropdownMenuLabel>General</DropdownMenuLabel>
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
CONSOLE_ERROR_MSG,
|
||||
CONSOLE_SUCCESS_MSG,
|
||||
INVALID_FILE_ALERT,
|
||||
} from "../../constants/alerts_constants";
|
||||
import { uploadFile } from "../../controllers/API";
|
||||
|
|
|
|||
|
|
@ -1,19 +1,22 @@
|
|||
import { ColDef, ColGroupDef } from "ag-grid-community";
|
||||
import "ag-grid-community/styles/ag-grid.css"; // Mandatory CSS required by the grid
|
||||
import "ag-grid-community/styles/ag-theme-balham.css"; // Optional Theme applied to the grid
|
||||
import { FlowPoolObjectType } from "../../types/chat";
|
||||
import { extractColumnsFromRows } from "../../utils/utils";
|
||||
import TableComponent from "../tableComponent";
|
||||
|
||||
function RecordsOutputComponent({
|
||||
flowPool,
|
||||
pagination,
|
||||
rows,
|
||||
columnMode = "union",
|
||||
}: {
|
||||
flowPool: FlowPoolObjectType;
|
||||
pagination: boolean;
|
||||
rows: any;
|
||||
columnMode?: "intersection" | "union";
|
||||
}) {
|
||||
const rows = flowPool?.data?.artifacts?.records ?? [];
|
||||
const columns = extractColumnsFromRows(rows, "union");
|
||||
console.log("rows", rows);
|
||||
const columns = extractColumnsFromRows(rows, columnMode);
|
||||
console.log("columns", columns);
|
||||
|
||||
const columnDefs = columns.map((col, idx) => ({
|
||||
...col,
|
||||
resizable: idx !== columns.length - 1,
|
||||
|
|
@ -22,6 +25,7 @@ function RecordsOutputComponent({
|
|||
|
||||
return (
|
||||
<TableComponent
|
||||
key={"recordsOutputComponent"}
|
||||
overlayNoRowsTemplate="No data available"
|
||||
suppressRowClickSelection={true}
|
||||
pagination={pagination}
|
||||
|
|
|
|||
46
src/frontend/src/components/renderIconComponent/index.tsx
Normal file
46
src/frontend/src/components/renderIconComponent/index.tsx
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import ForwardedIconComponent from "../genericIconComponent";
|
||||
|
||||
export default function RenderIcons({
|
||||
isMac,
|
||||
hasShift,
|
||||
filteredShortcut,
|
||||
shortcutWPlus,
|
||||
}: {
|
||||
isMac: boolean;
|
||||
hasShift: boolean;
|
||||
filteredShortcut: string[];
|
||||
shortcutWPlus: string[];
|
||||
}): JSX.Element {
|
||||
return hasShift ? (
|
||||
<>
|
||||
{isMac ? (
|
||||
<ForwardedIconComponent name="Command" className="h-4 w-4" />
|
||||
) : (
|
||||
filteredShortcut[0]
|
||||
)}
|
||||
<ForwardedIconComponent name="ArrowBigUp" className="ml-1 h-5 w-5" />
|
||||
{filteredShortcut.map((key, idx) => {
|
||||
if (idx > 0) {
|
||||
return <span className="ml-1"> {key.toUpperCase()} </span>;
|
||||
}
|
||||
})}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{shortcutWPlus[0].toLowerCase() === "space" ? (
|
||||
"Space"
|
||||
) : shortcutWPlus[0].length <= 1 ? (
|
||||
shortcutWPlus[0]
|
||||
) : isMac ? (
|
||||
<ForwardedIconComponent name="Command" className="h-4 w-4" />
|
||||
) : (
|
||||
shortcutWPlus[0]
|
||||
)}
|
||||
{shortcutWPlus.map((key, idx) => {
|
||||
if (idx > 0) {
|
||||
return <span className="ml-0.5"> {key.toUpperCase()} </span>;
|
||||
}
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
import { Link } from "react-router-dom";
|
||||
import { cn } from "../../../../utils/utils";
|
||||
import { buttonVariants } from "../../../ui/button";
|
||||
import ForwardedIconComponent from "../../../genericIconComponent";
|
||||
|
||||
type SideBarButtonsComponentProps = {
|
||||
items: {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { useLocation } from "react-router-dom";
|
|||
import { FolderType } from "../../../../pages/MainPage/entities";
|
||||
import { addFolder, updateFolder } from "../../../../pages/MainPage/services";
|
||||
import { handleDownloadFolderFn } from "../../../../pages/MainPage/utils/handle-download-folder";
|
||||
import useAlertStore from "../../../../stores/alertStore";
|
||||
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
|
||||
import { useFolderStore } from "../../../../stores/foldersStore";
|
||||
import { handleKeyDown } from "../../../../utils/reactflowUtils";
|
||||
|
|
@ -15,16 +16,13 @@ import { Input } from "../../../ui/input";
|
|||
import useFileDrop from "../../hooks/use-on-file-drop";
|
||||
|
||||
type SideBarFoldersButtonsComponentProps = {
|
||||
folders: FolderType[];
|
||||
pathname: string;
|
||||
handleChangeFolder?: (id: string) => void;
|
||||
handleEditFolder?: (item: FolderType) => void;
|
||||
handleDeleteFolder?: (item: FolderType) => void;
|
||||
};
|
||||
const SideBarFoldersButtonsComponent = ({
|
||||
pathname,
|
||||
handleChangeFolder,
|
||||
handleEditFolder,
|
||||
handleDeleteFolder,
|
||||
}: SideBarFoldersButtonsComponentProps) => {
|
||||
const refInput = useRef<HTMLInputElement>(null);
|
||||
|
|
@ -51,6 +49,8 @@ const SideBarFoldersButtonsComponent = ({
|
|||
const location = useLocation();
|
||||
const folderId = location?.state?.folderId ?? myCollectionId;
|
||||
const getFolderById = useFolderStore((state) => state.getFolderById);
|
||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||
const setSuccessData = useAlertStore((state) => state.setSuccessData);
|
||||
|
||||
const handleFolderChange = (folderId: string) => {
|
||||
getFolderById(folderId);
|
||||
|
|
@ -62,7 +62,20 @@ const SideBarFoldersButtonsComponent = ({
|
|||
);
|
||||
|
||||
const handleUploadFlowsToFolder = () => {
|
||||
uploadFolder(folderId);
|
||||
uploadFolder(folderId)
|
||||
.then(() => {
|
||||
getFolderById(folderId);
|
||||
setSuccessData({
|
||||
title: "Uploaded successfully",
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
setErrorData({
|
||||
title: `Error on upload`,
|
||||
list: [err["response"]["data"]],
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleDownloadFolder = (id: string) => {
|
||||
|
|
|
|||
|
|
@ -5,9 +5,6 @@ import { cn } from "../../utils/utils";
|
|||
import HorizontalScrollFadeComponent from "../horizontalScrollFadeComponent";
|
||||
import SideBarButtonsComponent from "./components/sideBarButtons";
|
||||
import SideBarFoldersButtonsComponent from "./components/sideBarFolderButtons";
|
||||
import { addFolder } from "../../pages/MainPage/services";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import useFlowStore from "../../stores/flowStore";
|
||||
|
||||
type SidebarNavProps = {
|
||||
items: {
|
||||
|
|
@ -15,7 +12,6 @@ type SidebarNavProps = {
|
|||
title: string;
|
||||
icon: React.ReactNode;
|
||||
}[];
|
||||
handleOpenNewFolderModal?: () => void;
|
||||
handleChangeFolder?: (id: string) => void;
|
||||
handleEditFolder?: (item: FolderType) => void;
|
||||
handleDeleteFolder?: (item: FolderType) => void;
|
||||
|
|
@ -48,10 +44,8 @@ export default function SidebarNav({
|
|||
folders?.length > 0 &&
|
||||
isFolderPath && (
|
||||
<SideBarFoldersButtonsComponent
|
||||
folders={folders}
|
||||
pathname={pathname}
|
||||
handleChangeFolder={handleChangeFolder}
|
||||
handleEditFolder={handleEditFolder}
|
||||
handleDeleteFolder={handleDeleteFolder}
|
||||
/>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3,9 +3,5 @@ export default function StringReader({
|
|||
}: {
|
||||
string: string;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<span className="text-wrap py-2.5 leading-5 truncate-multiline">
|
||||
{string}
|
||||
</span>
|
||||
);
|
||||
return <span className="truncate">{string}</span>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
import { cn } from "../../../../utils/utils";
|
||||
|
||||
export default function ResetColumns({
|
||||
resetGrid,
|
||||
}: {
|
||||
resetGrid: () => void;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<div className={cn("absolute bottom-4 left-6")}>
|
||||
<span
|
||||
className="cursor-pointer underline"
|
||||
onClick={() => {
|
||||
resetGrid();
|
||||
}}
|
||||
>
|
||||
Reset Columns
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
import { cn } from "../../../../utils/utils";
|
||||
import ShadTooltip from "../../../shadTooltipComponent";
|
||||
import { Button } from "../../../ui/button";
|
||||
import IconComponent from "../../../genericIconComponent";
|
||||
|
||||
export default function TableOptions({
|
||||
resetGrid,
|
||||
duplicateRow,
|
||||
deleteRow,
|
||||
hasSelection,
|
||||
stateChange,
|
||||
}: {
|
||||
resetGrid: () => void;
|
||||
duplicateRow?: () => void;
|
||||
deleteRow?: () => void;
|
||||
hasSelection: boolean;
|
||||
stateChange: boolean;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<div className={cn("absolute bottom-3 left-6")}>
|
||||
<div className="flex items-center gap-2">
|
||||
<div>
|
||||
<ShadTooltip content="Reset Columns">
|
||||
<Button
|
||||
variant="none"
|
||||
size="none"
|
||||
onClick={() => {
|
||||
resetGrid();
|
||||
}}
|
||||
disabled={!stateChange}
|
||||
>
|
||||
<IconComponent
|
||||
name="RotateCcw"
|
||||
className={cn("h-5 w-5 text-primary transition-all")}
|
||||
/>
|
||||
</Button>
|
||||
</ShadTooltip>
|
||||
</div>
|
||||
{duplicateRow && (
|
||||
<div>
|
||||
<ShadTooltip
|
||||
content={
|
||||
!hasSelection ? (
|
||||
<span>Select items to duplicate</span>
|
||||
) : (
|
||||
<span>Duplicate selected items</span>
|
||||
)
|
||||
}
|
||||
>
|
||||
<Button
|
||||
variant="none"
|
||||
size="none"
|
||||
onClick={duplicateRow}
|
||||
disabled={!hasSelection}
|
||||
>
|
||||
<IconComponent
|
||||
name="Copy"
|
||||
className={cn("h-5 w-5 text-primary transition-all")}
|
||||
/>
|
||||
</Button>
|
||||
</ShadTooltip>
|
||||
</div>
|
||||
)}
|
||||
{deleteRow && (
|
||||
<div>
|
||||
<ShadTooltip
|
||||
content={
|
||||
!hasSelection ? (
|
||||
<span>Select items to delete</span>
|
||||
) : (
|
||||
<span>Delete selected items</span>
|
||||
)
|
||||
}
|
||||
>
|
||||
<Button
|
||||
variant="none"
|
||||
size="none"
|
||||
onClick={deleteRow}
|
||||
disabled={!hasSelection}
|
||||
>
|
||||
<IconComponent
|
||||
name="Trash2"
|
||||
className={cn(
|
||||
"h-5 w-5 text-primary transition-all",
|
||||
!hasSelection ? "" : "hover:text-destructive",
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
</ShadTooltip>
|
||||
</div>
|
||||
)}{" "}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,30 +1,26 @@
|
|||
import { CustomCellRendererProps } from "ag-grid-react";
|
||||
import { cn, isTimeStampString } from "../../utils/utils";
|
||||
import ArrayReader from "../arrayReaderComponent";
|
||||
import DateReader from "../dateReaderComponent";
|
||||
import NumberReader from "../numberReader";
|
||||
import ObjectRender from "../objectRender";
|
||||
import StringReader from "../stringReaderComponent";
|
||||
import { Badge } from "../ui/badge";
|
||||
import { cn, isTimeStampString } from "../../../../utils/utils";
|
||||
import ArrayReader from "../../../arrayReaderComponent";
|
||||
import DateReader from "../../../dateReaderComponent";
|
||||
import NumberReader from "../../../numberReader";
|
||||
import ObjectRender from "../../../objectRender";
|
||||
import StringReader from "../../../stringReaderComponent";
|
||||
import { Badge } from "../../../ui/badge";
|
||||
|
||||
export default function TableAutoCellRender({
|
||||
value,
|
||||
}: CustomCellRendererProps) {
|
||||
}: CustomCellRendererProps | { value: any }) {
|
||||
function getCellType() {
|
||||
switch (typeof value) {
|
||||
case "object":
|
||||
if (value === null) {
|
||||
return String(value);
|
||||
} else if (Array.isArray(value)) {
|
||||
return <ArrayReader array={value} />;
|
||||
} else if (value.definitions) {
|
||||
// use a custom render defined by the sender
|
||||
return <ObjectRender object={value} />;
|
||||
} else {
|
||||
return <ObjectRender object={value} />;
|
||||
}
|
||||
break;
|
||||
case "string":
|
||||
console.log(isTimeStampString(value), value);
|
||||
if (isTimeStampString(value)) {
|
||||
return <DateReader date={value} />;
|
||||
}
|
||||
|
|
@ -52,7 +48,7 @@ export default function TableAutoCellRender({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="group flex h-full w-full items-center align-middle">
|
||||
<div className="group flex h-full w-full items-center truncate align-middle">
|
||||
{getCellType()}
|
||||
</div>
|
||||
);
|
||||
|
|
@ -1,33 +1,26 @@
|
|||
import { CustomCellEditorProps, CustomCellRendererProps } from "ag-grid-react";
|
||||
import { classNames, cn, isTimeStampString } from "../../utils/utils";
|
||||
import ArrayReader from "../arrayReaderComponent";
|
||||
import DateReader from "../dateReaderComponent";
|
||||
import NumberReader from "../numberReader";
|
||||
import ObjectRender from "../objectRender";
|
||||
import StringReader from "../stringReaderComponent";
|
||||
import { Badge } from "../ui/badge";
|
||||
import { CustomCellRendererProps } from "ag-grid-react";
|
||||
import { cloneDeep } from "lodash";
|
||||
import { type } from "os";
|
||||
import { useState } from "react";
|
||||
import useFlowStore from "../../../../stores/flowStore";
|
||||
import {
|
||||
convertObjToArray,
|
||||
convertValuesToNumbers,
|
||||
hasDuplicateKeys,
|
||||
scapedJSONStringfy,
|
||||
} from "../../utils/reactflowUtils";
|
||||
import CodeAreaComponent from "../codeAreaComponent";
|
||||
import DictComponent from "../dictComponent";
|
||||
import Dropdown from "../dropdownComponent";
|
||||
import FloatComponent from "../floatComponent";
|
||||
import InputFileComponent from "../inputFileComponent";
|
||||
import InputGlobalComponent from "../inputGlobalComponent";
|
||||
import InputListComponent from "../inputListComponent";
|
||||
import IntComponent from "../intComponent";
|
||||
import KeypairListComponent from "../keypairListComponent";
|
||||
import PromptAreaComponent from "../promptComponent";
|
||||
import TextAreaComponent from "../textAreaComponent";
|
||||
import ToggleShadComponent from "../toggleShadComponent";
|
||||
import { useState } from "react";
|
||||
import useFlowStore from "../../stores/flowStore";
|
||||
} from "../../../../utils/reactflowUtils";
|
||||
import { classNames } from "../../../../utils/utils";
|
||||
import CodeAreaComponent from "../../../codeAreaComponent";
|
||||
import DictComponent from "../../../dictComponent";
|
||||
import Dropdown from "../../../dropdownComponent";
|
||||
import FloatComponent from "../../../floatComponent";
|
||||
import InputFileComponent from "../../../inputFileComponent";
|
||||
import InputGlobalComponent from "../../../inputGlobalComponent";
|
||||
import InputListComponent from "../../../inputListComponent";
|
||||
import IntComponent from "../../../intComponent";
|
||||
import KeypairListComponent from "../../../keypairListComponent";
|
||||
import PromptAreaComponent from "../../../promptComponent";
|
||||
import TextAreaComponent from "../../../textAreaComponent";
|
||||
import ToggleShadComponent from "../../../toggleShadComponent";
|
||||
|
||||
export default function TableNodeCellRender({
|
||||
node: { data },
|
||||
|
|
@ -266,7 +259,7 @@ export default function TableNodeCellRender({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="group flex h-full w-[300px] items-center justify-center">
|
||||
<div className="group flex h-full w-[300px] items-center justify-center py-2.5">
|
||||
{getCellType()}
|
||||
</div>
|
||||
);
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import { CustomCellRendererProps } from "ag-grid-react";
|
||||
import { useState } from "react";
|
||||
import ToggleShadComponent from "../../../toggleShadComponent";
|
||||
|
||||
export default function TableToggleCellRender({
|
||||
value: { name, enabled, setEnabled },
|
||||
}: CustomCellRendererProps) {
|
||||
const [value, setValue] = useState(enabled);
|
||||
|
||||
return (
|
||||
<div className="flex h-full items-center">
|
||||
<ToggleShadComponent
|
||||
id={"show" + name}
|
||||
enabled={value}
|
||||
setEnabled={(e) => {
|
||||
setValue(e);
|
||||
setEnabled(e);
|
||||
}}
|
||||
size="small"
|
||||
editNode={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,22 +1,28 @@
|
|||
import "ag-grid-community/styles/ag-grid.css"; // Mandatory CSS required by the grid
|
||||
import "ag-grid-community/styles/ag-theme-quartz.css"; // Optional Theme applied to the grid
|
||||
import { AgGridReact, AgGridReactProps } from "ag-grid-react";
|
||||
import { ElementRef, forwardRef, useCallback } from "react";
|
||||
import { ElementRef, forwardRef, useRef, useState } from "react";
|
||||
import {
|
||||
DEFAULT_TABLE_ALERT_MSG,
|
||||
DEFAULT_TABLE_ALERT_TITLE,
|
||||
} from "../../constants/constants";
|
||||
import { useDarkStore } from "../../stores/darkStore";
|
||||
import "../../style/ag-theme-shadcn.css"; // Custom CSS applied to the grid
|
||||
import { cn } from "../../utils/utils";
|
||||
import { cn, toTitleCase } from "../../utils/utils";
|
||||
import ForwardedIconComponent from "../genericIconComponent";
|
||||
import { Alert, AlertDescription, AlertTitle } from "../ui/alert";
|
||||
import TableOptions from "./components/TableOptions";
|
||||
import { useParams } from "react-router-dom";
|
||||
import resetGrid from "./utils/reset-grid-columns";
|
||||
|
||||
interface TableComponentProps extends AgGridReactProps {
|
||||
columnDefs: NonNullable<AgGridReactProps["columnDefs"]>;
|
||||
rowData: NonNullable<AgGridReactProps["rowData"]>;
|
||||
alertTitle?: string;
|
||||
alertDescription?: string;
|
||||
editable?: boolean | string[];
|
||||
onDelete?: () => void;
|
||||
onDuplicate?: () => void;
|
||||
}
|
||||
|
||||
const TableComponent = forwardRef<
|
||||
|
|
@ -31,7 +37,73 @@ const TableComponent = forwardRef<
|
|||
},
|
||||
ref,
|
||||
) => {
|
||||
let colDef = props.columnDefs.map((col, index) => {
|
||||
let newCol = {
|
||||
...col,
|
||||
headerName: toTitleCase(col.headerName),
|
||||
};
|
||||
if (index === props.columnDefs.length - 1) {
|
||||
newCol = {
|
||||
...newCol,
|
||||
resizable: false,
|
||||
};
|
||||
}
|
||||
if (props.onSelectionChanged && index === 0) {
|
||||
newCol = {
|
||||
...newCol,
|
||||
checkboxSelection: true,
|
||||
headerCheckboxSelection: true,
|
||||
headerCheckboxSelectionFilteredOnly: true,
|
||||
};
|
||||
}
|
||||
if (
|
||||
(typeof props.editable === "boolean" && props.editable) ||
|
||||
(Array.isArray(props.editable) &&
|
||||
props.editable.includes(newCol.headerName ?? ""))
|
||||
) {
|
||||
newCol = {
|
||||
...newCol,
|
||||
editable: true,
|
||||
};
|
||||
}
|
||||
return newCol;
|
||||
});
|
||||
const gridRef = useRef(null);
|
||||
// @ts-ignore
|
||||
const realRef: React.MutableRefObject<AgGridReact> = ref?.current
|
||||
? ref
|
||||
: gridRef;
|
||||
const dark = useDarkStore((state) => state.dark);
|
||||
const initialColumnDefs = useRef(colDef);
|
||||
const [columnStateChange, setColumnStateChange] = useState(false);
|
||||
|
||||
const makeLastColumnNonResizable = (columnDefs) => {
|
||||
columnDefs.forEach((colDef, index) => {
|
||||
colDef.resizable = index !== columnDefs.length - 1;
|
||||
});
|
||||
return columnDefs;
|
||||
};
|
||||
|
||||
const onGridReady = (params) => {
|
||||
// @ts-ignore
|
||||
realRef.current = params;
|
||||
const updatedColumnDefs = makeLastColumnNonResizable([...colDef]);
|
||||
params.api.setGridOption("columnDefs", updatedColumnDefs);
|
||||
initialColumnDefs.current = params.api.getColumnDefs();
|
||||
if (props.onGridReady) props.onGridReady(params);
|
||||
setTimeout(() => {
|
||||
setColumnStateChange(false);
|
||||
}, 50);
|
||||
};
|
||||
|
||||
const onColumnMoved = (params) => {
|
||||
const updatedColumnDefs = makeLastColumnNonResizable(
|
||||
params.columnApi.getAllGridColumns().map((col) => col.getColDef()),
|
||||
);
|
||||
params.api.setGridOption("columnDefs", updatedColumnDefs);
|
||||
if (props.onColumnMoved) props.onColumnMoved(params);
|
||||
};
|
||||
|
||||
if (props.rowData.length === 0) {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center rounded-md border">
|
||||
|
|
@ -46,21 +118,46 @@ const TableComponent = forwardRef<
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
dark ? "ag-theme-quartz-dark" : "ag-theme-quartz",
|
||||
"ag-theme-shadcn flex h-full flex-col",
|
||||
"relative",
|
||||
)} // applying the grid theme
|
||||
>
|
||||
<AgGridReact
|
||||
{...props}
|
||||
className={cn(props.className, "custom-scroll")}
|
||||
animateRows={false}
|
||||
className={cn(props.className, "cusm-scroll")}
|
||||
defaultColDef={{
|
||||
minWidth: 100,
|
||||
autoHeight: true,
|
||||
}}
|
||||
columnDefs={colDef}
|
||||
ref={realRef}
|
||||
pagination={true}
|
||||
onGridReady={onGridReady}
|
||||
onColumnMoved={onColumnMoved}
|
||||
onStateUpdated={(e) => {
|
||||
console.log(e);
|
||||
if (e.sources.some((source) => source.includes("column"))) {
|
||||
setColumnStateChange(true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<TableOptions
|
||||
stateChange={columnStateChange}
|
||||
hasSelection={realRef.current?.api?.getSelectedRows().length > 0}
|
||||
duplicateRow={props.onDuplicate ? props.onDuplicate : undefined}
|
||||
deleteRow={props.onDelete ? props.onDelete : undefined}
|
||||
resetGrid={() => {
|
||||
console.log("teste");
|
||||
resetGrid(realRef, initialColumnDefs);
|
||||
setTimeout(() => {
|
||||
setColumnStateChange(false);
|
||||
}, 100);
|
||||
}}
|
||||
ref={ref}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
export default function resetGrid(ref, initialColumnDefs) {
|
||||
if (ref?.current && ref?.current.api) {
|
||||
ref.current.api.resetColumnState();
|
||||
if (initialColumnDefs.current) {
|
||||
const resetColumns = ref.current.api.applyColumnState({
|
||||
state: initialColumnDefs.current,
|
||||
applyOrder: true,
|
||||
});
|
||||
return resetColumns;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ import { cn } from "../../utils/utils";
|
|||
import ForwardedIconComponent from "../genericIconComponent";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background",
|
||||
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
|
|
@ -59,6 +59,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|||
variant,
|
||||
size,
|
||||
loading,
|
||||
type,
|
||||
disabled,
|
||||
asChild = false,
|
||||
children,
|
||||
|
|
@ -76,6 +77,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
disabled={loading || disabled}
|
||||
{...(asChild ? {} : { type: type || "button" })}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -31,11 +31,7 @@ function RefreshButton({
|
|||
|
||||
// icon class name should take into account the disabled state and the loading state
|
||||
const disabledIconTextClass = disabled ? "text-muted-foreground" : "";
|
||||
const iconClassName = cn(
|
||||
"h-4 w-4",
|
||||
isLoading ? "animate-spin" : "animate-wiggle",
|
||||
disabledIconTextClass
|
||||
);
|
||||
const iconClassName = cn("h-4 w-4 animate-wiggle", disabledIconTextClass);
|
||||
|
||||
return (
|
||||
<Button
|
||||
|
|
@ -44,10 +40,11 @@ function RefreshButton({
|
|||
className={classNames}
|
||||
onClick={handleClick}
|
||||
id={id}
|
||||
loading={isLoading}
|
||||
>
|
||||
{button_text && <span className="mr-1">{button_text}</span>}
|
||||
<IconComponent
|
||||
name={isLoading ? "Loader2" : "RefreshCcw"}
|
||||
name={"RefreshCcw"}
|
||||
className={iconClassName}
|
||||
id={id + "-icon"}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -33,10 +33,10 @@ const SelectContent = React.forwardRef<
|
|||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
"relative z-50 min-w-[14rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
|
|
@ -45,7 +45,7 @@ const SelectContent = React.forwardRef<
|
|||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
|
|
@ -75,7 +75,7 @@ const SelectItem = React.forwardRef<
|
|||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5 pl-3 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
|
|
|
|||
45
src/frontend/src/components/ui/toggle.tsx
Normal file
45
src/frontend/src/components/ui/toggle.tsx
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
"use client";
|
||||
|
||||
import * as TogglePrimitive from "@radix-ui/react-toggle";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "../../utils/utils";
|
||||
|
||||
const toggleVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-transparent",
|
||||
outline:
|
||||
"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-3",
|
||||
sm: "h-9 px-2.5",
|
||||
lg: "h-11 px-5",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const Toggle = React.forwardRef<
|
||||
React.ElementRef<typeof TogglePrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> &
|
||||
VariantProps<typeof toggleVariants>
|
||||
>(({ className, variant, size, ...props }, ref) => (
|
||||
<TogglePrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(toggleVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
Toggle.displayName = TogglePrimitive.Root.displayName;
|
||||
|
||||
export { Toggle, toggleVariants };
|
||||
|
|
@ -31,6 +31,8 @@ export const INVALID_SELECTION_ERROR_ALERT = "Invalid selection";
|
|||
export const EDIT_PASSWORD_ERROR_ALERT = "Error changing password";
|
||||
export const EDIT_PASSWORD_ALERT_LIST = "Passwords do not match";
|
||||
export const SAVE_ERROR_ALERT = "Error saving changes";
|
||||
export const PROFILE_PICTURES_GET_ERROR_ALERT =
|
||||
"Error retrieving profile pictures";
|
||||
export const SIGNUP_ERROR_ALERT = "Error signing up";
|
||||
export const APIKEY_ERROR_ALERT = "API Key Error";
|
||||
export const NOAPI_ERROR_ALERT =
|
||||
|
|
|
|||
|
|
@ -589,7 +589,7 @@ export const CONTROL_INPUT_STATE = {
|
|||
export const CONTROL_PATCH_USER_STATE = {
|
||||
password: "",
|
||||
cnfPassword: "",
|
||||
gradient: "",
|
||||
profilePicture: "",
|
||||
apikey: "",
|
||||
};
|
||||
|
||||
|
|
@ -661,6 +661,9 @@ export const OUTPUT_TYPES = new Set([
|
|||
export const CHAT_FIRST_INITIAL_TEXT =
|
||||
"Start a conversation and click the agent's thoughts";
|
||||
|
||||
export const TOOLTIP_OUTDATED_NODE =
|
||||
"Your component is outdated. Click to update (data may be lost)";
|
||||
|
||||
export const CHAT_SECOND_INITIAL_TEXT = "to inspect the chaining process.";
|
||||
|
||||
export const ZERO_NOTIFICATIONS = "No new notifications";
|
||||
|
|
@ -668,7 +671,7 @@ export const ZERO_NOTIFICATIONS = "No new notifications";
|
|||
export const SUCCESS_BUILD = "Built sucessfully ✨";
|
||||
|
||||
export const ALERT_SAVE_WITH_API =
|
||||
"Caution: Uncheck this box only removes API keys from fields specifically designated for API keys.";
|
||||
"Caution: Unchecking this box only removes API keys from fields specifically designated for API keys.";
|
||||
|
||||
export const SAVE_WITH_API_CHECKBOX = "Save with my API keys";
|
||||
export const EDIT_TEXT_MODAL_TITLE = "Edit Text";
|
||||
|
|
@ -731,8 +734,98 @@ export const AUTHORIZED_DUPLICATE_REQUESTS = [
|
|||
|
||||
export const SAVE_DEBOUNCE_TIME = 300;
|
||||
|
||||
export const IS_MAC = navigator.userAgent.toUpperCase().includes("MAC");
|
||||
|
||||
export const defaultShortcuts = [
|
||||
{
|
||||
name: "Advanced Settings",
|
||||
shortcut: `${IS_MAC ? "Cmd" : "Ctrl"} + Shift + A`,
|
||||
},
|
||||
{
|
||||
name: "Minimize",
|
||||
shortcut: `${IS_MAC ? "Cmd" : "Ctrl"} + Q`,
|
||||
},
|
||||
{
|
||||
name: "Code",
|
||||
shortcut: `Space`,
|
||||
},
|
||||
{
|
||||
name: "Copy",
|
||||
shortcut: `${IS_MAC ? "Cmd" : "Ctrl"} + C`,
|
||||
},
|
||||
{
|
||||
name: "Duplicate",
|
||||
shortcut: `${IS_MAC ? "Cmd" : "Ctrl"} + D`,
|
||||
},
|
||||
{
|
||||
name: "Share",
|
||||
shortcut: `${IS_MAC ? "Cmd" : "Ctrl"} + Shift + S`,
|
||||
},
|
||||
{
|
||||
name: "Docs",
|
||||
shortcut: `${IS_MAC ? "Cmd" : "Ctrl"} + Shift + D`,
|
||||
},
|
||||
{
|
||||
name: "Save",
|
||||
shortcut: `${IS_MAC ? "Cmd" : "Ctrl"} + S`,
|
||||
},
|
||||
{
|
||||
name: "Delete",
|
||||
shortcut: "Backspace",
|
||||
},
|
||||
{
|
||||
name: "Open playground",
|
||||
shortcut: `${IS_MAC ? "Cmd" : "Ctrl"} + K`,
|
||||
},
|
||||
{
|
||||
name: "Undo",
|
||||
shortcut: `${IS_MAC ? "Cmd" : "Ctrl"} + Z`,
|
||||
},
|
||||
{
|
||||
name: "Redo",
|
||||
shortcut: `${IS_MAC ? "Cmd" : "Ctrl"} + Y`,
|
||||
},
|
||||
{
|
||||
name: "Group",
|
||||
shortcut: `${IS_MAC ? "Cmd" : "Ctrl"} + G`,
|
||||
},
|
||||
{
|
||||
name: "Cut",
|
||||
shortcut: `${IS_MAC ? "Cmd" : "Ctrl"} + X`,
|
||||
},
|
||||
{
|
||||
name: "Paste",
|
||||
shortcut: `${IS_MAC ? "Cmd" : "Ctrl"} + V`,
|
||||
},
|
||||
{
|
||||
name: "API",
|
||||
shortcut: `${IS_MAC ? "Cmd" : "Ctrl"} + R`,
|
||||
},
|
||||
{
|
||||
name: "Download",
|
||||
shortcut: `${IS_MAC ? "Cmd" : "Ctrl"} + J`,
|
||||
},
|
||||
{
|
||||
name: "Update",
|
||||
shortcut: `${IS_MAC ? "Cmd" : "Ctrl"} + U`,
|
||||
},
|
||||
{
|
||||
name: "Freeze",
|
||||
shortcut: `${IS_MAC ? "Cmd" : "Ctrl"} + F`,
|
||||
},
|
||||
{
|
||||
name: "Flow Share",
|
||||
shortcut: `${IS_MAC ? "Cmd" : "Ctrl"} + B`,
|
||||
},
|
||||
];
|
||||
|
||||
export const DEFAULT_TABLE_ALERT_MSG = `Oops! It seems there's no data to display right now. Please check back later.`;
|
||||
|
||||
export const DEFAULT_TABLE_ALERT_TITLE = "No Data Available";
|
||||
|
||||
export const LOCATIONS_TO_RETURN = ["/flow/", "/settings/"];
|
||||
|
||||
export const MAX_BATCH_SIZE = 50;
|
||||
|
||||
export const MODAL_CLASSES =
|
||||
"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";
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import axios, { AxiosError, AxiosInstance } from "axios";
|
||||
import axios, { Axios, AxiosError, AxiosInstance } from "axios";
|
||||
import { useContext, useEffect } from "react";
|
||||
import { Cookies } from "react-cookie";
|
||||
import { renewAccessToken } from ".";
|
||||
|
|
@ -83,8 +83,10 @@ function ApiInterceptor() {
|
|||
(config) => {
|
||||
const checkRequest = checkDuplicateRequestAndStoreRequest(config);
|
||||
|
||||
const controller = new AbortController();
|
||||
|
||||
if (!checkRequest) {
|
||||
return Promise.reject("Duplicate request.");
|
||||
controller.abort("Duplicate Request");
|
||||
}
|
||||
|
||||
const accessToken = cookies.get("access_token_lf");
|
||||
|
|
@ -92,7 +94,10 @@ function ApiInterceptor() {
|
|||
config.headers["Authorization"] = `Bearer ${accessToken}`;
|
||||
}
|
||||
|
||||
return config;
|
||||
return {
|
||||
...config,
|
||||
signal: controller.signal,
|
||||
};
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
import { ColDef, ColGroupDef } from "ag-grid-community";
|
||||
import { AxiosRequestConfig, AxiosResponse } from "axios";
|
||||
import { Edge, Node, ReactFlowJsonObject } from "reactflow";
|
||||
import { BASE_URL_API } from "../../constants/constants";
|
||||
import { BASE_URL_API, MAX_BATCH_SIZE } from "../../constants/constants";
|
||||
import { api } from "../../controllers/API/api";
|
||||
import {
|
||||
APIObjectType,
|
||||
APITemplateType,
|
||||
Component,
|
||||
LoginType,
|
||||
ProfilePicturesTypeAPI,
|
||||
Users,
|
||||
VertexBuildTypeAPI,
|
||||
VerticesOrderTypeAPI,
|
||||
|
|
@ -17,6 +18,7 @@ import {
|
|||
} from "../../types/api/index";
|
||||
import { UserInputType } from "../../types/components";
|
||||
import { FlowStyleType, FlowType } from "../../types/flow";
|
||||
import { Message } from "../../types/messages";
|
||||
import { StoreComponentResponse } from "../../types/store";
|
||||
import { FlowPoolType } from "../../types/zustand/flow";
|
||||
import { extractColumnsFromRows } from "../../utils/utils";
|
||||
|
|
@ -61,7 +63,7 @@ export async function sendAll(data: sendAllProps) {
|
|||
}
|
||||
|
||||
export async function postValidateCode(
|
||||
code: string,
|
||||
code: string
|
||||
): Promise<AxiosResponse<errorsTypeAPI>> {
|
||||
return await api.post(`${BASE_URL_API}validate/code`, { code });
|
||||
}
|
||||
|
|
@ -76,7 +78,7 @@ export async function postValidateCode(
|
|||
export async function postValidatePrompt(
|
||||
name: string,
|
||||
template: string,
|
||||
frontend_node: APIClassType,
|
||||
frontend_node: APIClassType
|
||||
): Promise<AxiosResponse<PromptTypeAPI>> {
|
||||
return api.post(`${BASE_URL_API}validate/prompt`, {
|
||||
name,
|
||||
|
|
@ -149,7 +151,7 @@ export async function saveFlowToDatabase(newFlow: {
|
|||
* @throws Will throw an error if the update fails.
|
||||
*/
|
||||
export async function updateFlowInDatabase(
|
||||
updatedFlow: FlowType,
|
||||
updatedFlow: FlowType
|
||||
): Promise<FlowType> {
|
||||
try {
|
||||
const response = await api.patch(`${BASE_URL_API}flows/${updatedFlow.id}`, {
|
||||
|
|
@ -327,7 +329,7 @@ export async function getHealth() {
|
|||
*
|
||||
*/
|
||||
export async function getBuildStatus(
|
||||
flowId: string,
|
||||
flowId: string
|
||||
): Promise<AxiosResponse<BuildStatusTypeAPI>> {
|
||||
return await api.get(`${BASE_URL_API}build/${flowId}/status`);
|
||||
}
|
||||
|
|
@ -340,7 +342,7 @@ export async function getBuildStatus(
|
|||
*
|
||||
*/
|
||||
export async function postBuildInit(
|
||||
flow: FlowType,
|
||||
flow: FlowType
|
||||
): Promise<AxiosResponse<InitTypeAPI>> {
|
||||
return await api.post(`${BASE_URL_API}build/init/${flow.id}`, flow);
|
||||
}
|
||||
|
|
@ -356,16 +358,29 @@ export async function postBuildInit(
|
|||
*/
|
||||
export async function uploadFile(
|
||||
file: File,
|
||||
id: string,
|
||||
id: string
|
||||
): Promise<AxiosResponse<UploadFileTypeAPI>> {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
return await api.post(`${BASE_URL_API}files/upload/${id}`, formData);
|
||||
}
|
||||
|
||||
export async function getProfilePictures(): Promise<ProfilePicturesTypeAPI | null> {
|
||||
try {
|
||||
const res = await api.get(`${BASE_URL_API}files/profile_pictures/list`);
|
||||
|
||||
if (res.status === 200) {
|
||||
return res.data;
|
||||
}
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function postCustomComponent(
|
||||
code: string,
|
||||
apiClass: APIClassType,
|
||||
apiClass: APIClassType
|
||||
): Promise<AxiosResponse<APIClassType>> {
|
||||
// let template = apiClass.template;
|
||||
return await api.post(`${BASE_URL_API}custom_component`, {
|
||||
|
|
@ -378,7 +393,7 @@ export async function postCustomComponentUpdate(
|
|||
code: string,
|
||||
template: APITemplateType,
|
||||
field: string,
|
||||
field_value: any,
|
||||
field_value: any
|
||||
): Promise<AxiosResponse<APIClassType>> {
|
||||
return await api.post(`${BASE_URL_API}custom_component/update`, {
|
||||
code,
|
||||
|
|
@ -400,7 +415,7 @@ export async function onLogin(user: LoginType) {
|
|||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (response.status === 200) {
|
||||
|
|
@ -462,11 +477,11 @@ export async function addUser(user: UserInputType): Promise<Array<Users>> {
|
|||
|
||||
export async function getUsersPage(
|
||||
skip: number,
|
||||
limit: number,
|
||||
limit: number
|
||||
): Promise<Array<Users>> {
|
||||
try {
|
||||
const res = await api.get(
|
||||
`${BASE_URL_API}users/?skip=${skip}&limit=${limit}`,
|
||||
`${BASE_URL_API}users/?skip=${skip}&limit=${limit}`
|
||||
);
|
||||
if (res.status === 200) {
|
||||
return res.data;
|
||||
|
|
@ -503,7 +518,7 @@ export async function resetPassword(user_id: string, user: resetPasswordType) {
|
|||
try {
|
||||
const res = await api.patch(
|
||||
`${BASE_URL_API}users/${user_id}/reset-password`,
|
||||
user,
|
||||
user
|
||||
);
|
||||
if (res.status === 200) {
|
||||
return res.data;
|
||||
|
|
@ -577,7 +592,7 @@ export async function saveFlowStore(
|
|||
last_tested_version?: string;
|
||||
},
|
||||
tags: string[],
|
||||
publicFlow = false,
|
||||
publicFlow = false
|
||||
): Promise<FlowType> {
|
||||
try {
|
||||
const response = await api.post(`${BASE_URL_API}store/components/`, {
|
||||
|
|
@ -706,7 +721,7 @@ export async function postStoreComponents(component: Component) {
|
|||
export async function getComponent(component_id: string) {
|
||||
try {
|
||||
const res = await api.get(
|
||||
`${BASE_URL_API}store/components/${component_id}`,
|
||||
`${BASE_URL_API}store/components/${component_id}`
|
||||
);
|
||||
if (res.status === 200) {
|
||||
return res.data;
|
||||
|
|
@ -721,7 +736,7 @@ export async function searchComponent(
|
|||
page?: number | null,
|
||||
limit?: number | null,
|
||||
status?: string | null,
|
||||
tags?: string[],
|
||||
tags?: string[]
|
||||
): Promise<StoreComponentResponse | undefined> {
|
||||
try {
|
||||
let url = `${BASE_URL_API}store/components/`;
|
||||
|
|
@ -833,7 +848,7 @@ export async function updateFlowStore(
|
|||
},
|
||||
tags: string[],
|
||||
publicFlow = false,
|
||||
id: string,
|
||||
id: string
|
||||
): Promise<FlowType> {
|
||||
try {
|
||||
const response = await api.patch(`${BASE_URL_API}store/components/${id}`, {
|
||||
|
|
@ -917,7 +932,7 @@ export async function deleteGlobalVariable(id: string) {
|
|||
export async function updateGlobalVariable(
|
||||
name: string,
|
||||
value: string,
|
||||
id: string,
|
||||
id: string
|
||||
) {
|
||||
try {
|
||||
const response = api.patch(`${BASE_URL_API}variables/${id}`, {
|
||||
|
|
@ -936,7 +951,7 @@ export async function getVerticesOrder(
|
|||
startNodeId?: string | null,
|
||||
stopNodeId?: string | null,
|
||||
nodes?: Node[],
|
||||
Edges?: Edge[],
|
||||
Edges?: Edge[]
|
||||
): Promise<AxiosResponse<VerticesOrderTypeAPI>> {
|
||||
// nodeId is optional and is a query parameter
|
||||
// if nodeId is not provided, the API will return all vertices
|
||||
|
|
@ -956,7 +971,7 @@ export async function getVerticesOrder(
|
|||
return await api.post(
|
||||
`${BASE_URL_API}build/${flowId}/vertices`,
|
||||
data,
|
||||
config,
|
||||
config
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -964,11 +979,19 @@ export async function postBuildVertex(
|
|||
flowId: string,
|
||||
vertexId: string,
|
||||
input_value: string,
|
||||
files?: string[]
|
||||
): Promise<AxiosResponse<VertexBuildTypeAPI>> {
|
||||
// input_value is optional and is a query parameter
|
||||
let data = {};
|
||||
if (typeof input_value !== "undefined") {
|
||||
data["inputs"] = { input_value: input_value };
|
||||
}
|
||||
if (data && files) {
|
||||
data["files"] = files;
|
||||
}
|
||||
return await api.post(
|
||||
`${BASE_URL_API}build/${flowId}/vertices/${vertexId}`,
|
||||
input_value ? { inputs: { input_value: input_value } } : undefined,
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -992,25 +1015,54 @@ export async function getFlowPool({
|
|||
}
|
||||
|
||||
export async function deleteFlowPool(
|
||||
flowId: string,
|
||||
flowId: string
|
||||
): Promise<AxiosResponse<any>> {
|
||||
const config = {};
|
||||
config["params"] = { flow_id: flowId };
|
||||
return await api.delete(`${BASE_URL_API}monitor/builds`, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes multiple flow components by their IDs.
|
||||
* @param flowIds - An array of flow IDs to be deleted.
|
||||
* @param token - The authorization token for the API request.
|
||||
* @returns A promise that resolves to an array of AxiosResponse objects representing the delete responses.
|
||||
*/
|
||||
export async function multipleDeleteFlowsComponents(
|
||||
flowIds: string[],
|
||||
): Promise<AxiosResponse<any>> {
|
||||
return await api.post(`${BASE_URL_API}flows/multiple_delete/`, {
|
||||
flow_ids: flowIds,
|
||||
});
|
||||
flowIds: string[]
|
||||
): Promise<AxiosResponse<any>[]> {
|
||||
const batches: string[][] = [];
|
||||
|
||||
// Split the flowIds into batches
|
||||
for (let i = 0; i < flowIds.length; i += MAX_BATCH_SIZE) {
|
||||
batches.push(flowIds.slice(i, i + MAX_BATCH_SIZE));
|
||||
}
|
||||
|
||||
// Function to delete a batch of flow IDs
|
||||
const deleteBatch = async (batch: string[]): Promise<AxiosResponse<any>> => {
|
||||
try {
|
||||
return await api.delete(`${BASE_URL_API}flows/`, {
|
||||
data: batch,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error deleting flows:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Execute all delete requests
|
||||
const responses: Promise<AxiosResponse<any>>[] = batches.map((batch) =>
|
||||
deleteBatch(batch)
|
||||
);
|
||||
|
||||
// Return the responses after all requests are completed
|
||||
return Promise.all(responses);
|
||||
}
|
||||
|
||||
export async function getTransactionTable(
|
||||
id: string,
|
||||
mode: "intersection" | "union",
|
||||
params = {},
|
||||
params = {}
|
||||
): Promise<{ rows: Array<object>; columns: Array<ColDef | ColGroupDef> }> {
|
||||
const config = {};
|
||||
config["params"] = { flow_id: id };
|
||||
|
|
@ -1023,16 +1075,38 @@ export async function getTransactionTable(
|
|||
}
|
||||
|
||||
export async function getMessagesTable(
|
||||
id: string,
|
||||
mode: "intersection" | "union",
|
||||
params = {},
|
||||
): Promise<{ rows: Array<object>; columns: Array<ColDef | ColGroupDef> }> {
|
||||
id?: string,
|
||||
excludedFields?: string[],
|
||||
params = {}
|
||||
): Promise<{ rows: Array<Message>; columns: Array<ColDef | ColGroupDef> }> {
|
||||
const config = {};
|
||||
config["params"] = { flow_id: id };
|
||||
if (id) {
|
||||
config["params"] = { flow_id: id };
|
||||
}
|
||||
if (params) {
|
||||
config["params"] = { ...config["params"], ...params };
|
||||
}
|
||||
const rows = await api.get(`${BASE_URL_API}monitor/messages`, config);
|
||||
const columns = extractColumnsFromRows(rows.data, mode);
|
||||
const columns = extractColumnsFromRows(rows.data, mode, excludedFields);
|
||||
const sessions = new Set<string>();
|
||||
rows.data.forEach((row) => {
|
||||
sessions.add(row.session_id);
|
||||
});
|
||||
return { rows: rows.data, columns };
|
||||
}
|
||||
|
||||
export async function deleteMessagesFn(ids: number[]) {
|
||||
try {
|
||||
return await api.delete(`${BASE_URL_API}monitor/messages`, {
|
||||
data: ids,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error deleting flows:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateMessageApi(data: Message) {
|
||||
return await api.post(`${BASE_URL_API}monitor/messages/${data.index}`, data);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@ const SvgBotMessageSquare = (props) => (
|
|||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="lucide lucide-bot-message-square"
|
||||
{...props}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -4,5 +4,5 @@ import SvgGroqLogo from "./GroqLogo";
|
|||
export const GroqIcon = forwardRef<SVGSVGElement, React.PropsWithChildren<{}>>(
|
||||
(props, ref) => {
|
||||
return <SvgGroqLogo ref={ref} {...props} />;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
|
|||
25
src/frontend/src/icons/Streamlit/SvgStreamlit.jsx
Normal file
25
src/frontend/src/icons/Streamlit/SvgStreamlit.jsx
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
export default function SvgStreamlit(props) {
|
||||
return (
|
||||
<svg
|
||||
width="301"
|
||||
height="165"
|
||||
viewBox="0 0 301 165"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M150.731 101.547L98.1387 73.7471L6.84674 25.4969C6.7634 25.4136 6.59674 25.4136 6.51341 25.4136C3.18007 23.8303 -0.236608 27.1636 1.0134 30.497L47.5302 149.139L47.5385 149.164C47.5885 149.281 47.6302 149.397 47.6802 149.514C49.5885 153.939 53.7552 156.672 58.2886 157.747C58.6719 157.831 58.9461 157.906 59.4064 157.998C59.8645 158.1 60.5052 158.239 61.0552 158.281C61.1469 158.289 61.2302 158.289 61.3219 158.297H61.3886C61.4552 158.306 61.5219 158.306 61.5886 158.314H61.6802C61.7386 158.322 61.8052 158.322 61.8636 158.322H61.9719C62.0386 158.331 62.1052 158.331 62.1719 158.331V158.331C121.084 164.754 180.519 164.754 239.431 158.331V158.331C240.139 158.331 240.831 158.297 241.497 158.231C241.714 158.206 241.922 158.181 242.131 158.156C242.156 158.147 242.189 158.147 242.214 158.139C242.356 158.122 242.497 158.097 242.639 158.072C242.847 158.047 243.056 158.006 243.264 157.964C243.681 157.872 243.87 157.806 244.436 157.611C245.001 157.417 245.94 157.077 246.527 156.794C247.115 156.511 247.522 156.239 248.014 155.931C248.622 155.547 249.201 155.155 249.788 154.715C250.041 154.521 250.214 154.397 250.397 154.222L250.297 154.164L150.731 101.547Z"
|
||||
fill="#FF4B4B"
|
||||
/>
|
||||
<path
|
||||
d="M294.766 25.4981H294.683L203.357 73.7483L254.124 149.357L300.524 30.4981V30.3315C301.691 26.8314 298.108 23.6648 294.766 25.4981"
|
||||
fill="#7D353B"
|
||||
/>
|
||||
<path
|
||||
d="M155.598 2.55572C153.264 -0.852624 148.181 -0.852624 145.931 2.55572L98.1389 73.7477L150.731 101.548L250.398 154.222C251.024 153.609 251.526 153.012 252.056 152.381C252.806 151.456 253.506 150.465 254.123 149.356L203.356 73.7477L155.598 2.55572Z"
|
||||
fill="#BD4043"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
8
src/frontend/src/icons/Streamlit/index.tsx
Normal file
8
src/frontend/src/icons/Streamlit/index.tsx
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import React, { forwardRef } from "react";
|
||||
import SvgStreamlit from "./SvgStreamlit";
|
||||
|
||||
export const Streamlit = forwardRef<SVGSVGElement, React.PropsWithChildren<{}>>(
|
||||
(props, ref) => {
|
||||
return <SvgStreamlit className="icon" ref={ref} {...props} />;
|
||||
},
|
||||
);
|
||||
|
|
@ -12,13 +12,11 @@ import { StrictMode } from "react";
|
|||
import "./style/classes.css";
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById("root") as HTMLElement
|
||||
document.getElementById("root") as HTMLElement,
|
||||
);
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<ContextWrapper>
|
||||
<App />
|
||||
</ContextWrapper>
|
||||
</StrictMode>
|
||||
<ContextWrapper>
|
||||
<App />
|
||||
</ContextWrapper>,
|
||||
);
|
||||
reportWebVitals();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "../../../../../../components/ui/select";
|
||||
|
||||
export default function CsvSelect({ node, handleChangeSelect }): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<div className="flex justify-between">
|
||||
Expand the ouptut to see the CSV
|
||||
</div>
|
||||
<div className="flex items-center justify-between pt-5">
|
||||
<span>CSV separator </span>
|
||||
<Select
|
||||
value={node.data.node.template.separator.value}
|
||||
onValueChange={(e) => handleChangeSelect(e)}
|
||||
>
|
||||
<SelectTrigger className="w-[70px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{node?.data?.node?.template?.separator?.options.map(
|
||||
(separator) => (
|
||||
<SelectItem key={separator} value={separator}>
|
||||
{separator}
|
||||
</SelectItem>
|
||||
),
|
||||
)}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -5,17 +5,10 @@ import CsvOutputComponent from "../../../../components/csvOutputComponent";
|
|||
import InputListComponent from "../../../../components/inputListComponent";
|
||||
import PdfViewer from "../../../../components/pdfViewer";
|
||||
import RecordsOutputComponent from "../../../../components/recordsOutputComponent";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "../../../../components/ui/select";
|
||||
import { Textarea } from "../../../../components/ui/textarea";
|
||||
import { PDFViewConstant } from "../../../../constants/constants";
|
||||
import { InputOutput } from "../../../../constants/enums";
|
||||
import TextOutputView from "../../../../shared/components/textOutputView";
|
||||
import useFlowStore from "../../../../stores/flowStore";
|
||||
import { IOFieldViewProps } from "../../../../types/components";
|
||||
import {
|
||||
|
|
@ -24,6 +17,7 @@ import {
|
|||
} from "../../../../utils/reactflowUtils";
|
||||
import IOFileInput from "./components/FileInput";
|
||||
import IoJsonInput from "./components/JSONInput";
|
||||
import CsvSelect from "./components/csvSelect";
|
||||
import IOKeyPairInput from "./components/keyPairInput";
|
||||
|
||||
export default function IOFieldView({
|
||||
|
|
@ -51,6 +45,12 @@ export default function IOFieldView({
|
|||
|
||||
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
|
||||
|
||||
const textOutputValue =
|
||||
(flowPool[node!.id] ?? [])[(flowPool[node!.id]?.length ?? 1) - 1]?.data
|
||||
.results.result ?? "";
|
||||
|
||||
console.log(flowPoolNode?.data?.artifacts?.records);
|
||||
|
||||
function handleOutputType() {
|
||||
if (!node) return <>"No node found!"</>;
|
||||
switch (type) {
|
||||
|
|
@ -163,21 +163,7 @@ export default function IOFieldView({
|
|||
case InputOutput.OUTPUT:
|
||||
switch (fieldType) {
|
||||
case "TextOutput":
|
||||
return (
|
||||
<Textarea
|
||||
className={`w-full custom-scroll ${
|
||||
left ? " min-h-32" : " h-full"
|
||||
}`}
|
||||
placeholder={"Empty"}
|
||||
// update to real value on flowPool
|
||||
value={
|
||||
(flowPool[node.id] ?? [])[
|
||||
(flowPool[node.id]?.length ?? 1) - 1
|
||||
]?.params ?? ""
|
||||
}
|
||||
readOnly
|
||||
/>
|
||||
);
|
||||
return <TextOutputView left={left} value={textOutputValue} />;
|
||||
case "PDFOutput":
|
||||
return left ? (
|
||||
<div>{PDFViewConstant}</div>
|
||||
|
|
@ -187,31 +173,10 @@ export default function IOFieldView({
|
|||
case "CSVOutput":
|
||||
return left ? (
|
||||
<>
|
||||
<div className="flex justify-between">
|
||||
Expand the ouptut to see the CSV
|
||||
</div>
|
||||
<div className="flex items-center justify-between pt-5">
|
||||
<span>CSV separator </span>
|
||||
<Select
|
||||
value={node.data.node.template.separator.value}
|
||||
onValueChange={(e) => handleChangeSelect(e)}
|
||||
>
|
||||
<SelectTrigger className="w-[70px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{node?.data?.node?.template?.separator?.options.map(
|
||||
(separator) => (
|
||||
<SelectItem key={separator} value={separator}>
|
||||
{separator}
|
||||
</SelectItem>
|
||||
),
|
||||
)}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<CsvSelect
|
||||
node={node}
|
||||
handleChangeSelect={handleChangeSelect}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
|
@ -286,8 +251,9 @@ export default function IOFieldView({
|
|||
return (
|
||||
<div className={left ? "h-56" : "h-full"}>
|
||||
<RecordsOutputComponent
|
||||
flowPool={flowPoolNode}
|
||||
pagination={!left}
|
||||
rows={flowPoolNode?.data?.artifacts?.records ?? []}
|
||||
columnMode="union"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -303,7 +269,7 @@ export default function IOFieldView({
|
|||
value={
|
||||
(flowPool[node.id] ?? [])[
|
||||
(flowPool[node.id]?.length ?? 1) - 1
|
||||
]?.params ?? ""
|
||||
]?.data.results.result ?? ""
|
||||
}
|
||||
readOnly
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
import { deleteMessagesFn } from "../../../../../controllers/API";
|
||||
import { useMessagesStore } from "../../../../../stores/messagesStore";
|
||||
|
||||
const useRemoveSession = (setSuccessData, setErrorData) => {
|
||||
const deleteSession = useMessagesStore((state) => state.deleteSession);
|
||||
const messages = useMessagesStore((state) => state.messages);
|
||||
|
||||
const handleRemoveSession = async (session_id: string) => {
|
||||
try {
|
||||
await deleteMessagesFn(
|
||||
messages
|
||||
.filter((msg) => msg.session_id === session_id)
|
||||
.map((msg) => msg.index),
|
||||
);
|
||||
deleteSession(session_id);
|
||||
setSuccessData({
|
||||
title: "Session deleted successfully.",
|
||||
});
|
||||
} catch (error) {
|
||||
setErrorData({
|
||||
title: "Error deleting Session.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return { handleRemoveSession };
|
||||
};
|
||||
|
||||
export default useRemoveSession;
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
import {
|
||||
CellEditRequestEvent,
|
||||
ColDef,
|
||||
ColGroupDef,
|
||||
SelectionChangedEvent,
|
||||
} from "ag-grid-community";
|
||||
import { useState } from "react";
|
||||
import TableComponent from "../../../../components/tableComponent";
|
||||
import { Card, CardContent } from "../../../../components/ui/card";
|
||||
import useAlertStore from "../../../../stores/alertStore";
|
||||
import { useMessagesStore } from "../../../../stores/messagesStore";
|
||||
import useUpdateMessage from "../../../../pages/SettingsPage/pages/messagesPage/hooks/use-updateMessage";
|
||||
import useRemoveMessages from "../../../../pages/SettingsPage/pages/messagesPage/hooks/use-remove-messages";
|
||||
import HeaderMessagesComponent from "../../../../pages/SettingsPage/pages/messagesPage/components/headerMessages";
|
||||
import { Button } from "../../../../components/ui/button";
|
||||
import ForwardedIconComponent from "../../../../components/genericIconComponent";
|
||||
import { cn } from "../../../../utils/utils";
|
||||
|
||||
export default function SessionView({ rows }: { rows: Array<any> }) {
|
||||
const columns = useMessagesStore((state) => state.columns);
|
||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||
const setSuccessData = useAlertStore((state) => state.setSuccessData);
|
||||
|
||||
const [selectedRows, setSelectedRows] = useState<number[]>([]);
|
||||
|
||||
const { handleRemoveMessages } = useRemoveMessages(
|
||||
setSelectedRows,
|
||||
setSuccessData,
|
||||
setErrorData,
|
||||
selectedRows,
|
||||
);
|
||||
|
||||
const { handleUpdate } = useUpdateMessage(setSuccessData, setErrorData);
|
||||
|
||||
function handleUpdateMessage(event: CellEditRequestEvent<any, string>) {
|
||||
const newValue = event.newValue;
|
||||
const field = event.column.getColId();
|
||||
const row = event.data;
|
||||
const data = {
|
||||
...row,
|
||||
[field]: newValue,
|
||||
};
|
||||
handleUpdate(data);
|
||||
}
|
||||
|
||||
return (
|
||||
<TableComponent
|
||||
key={"sessionView"}
|
||||
onDelete={handleRemoveMessages}
|
||||
readOnlyEdit
|
||||
onCellEditRequest={(event) => {
|
||||
handleUpdateMessage(event);
|
||||
}}
|
||||
editable={["Sender Name", "Message"]}
|
||||
overlayNoRowsTemplate="No data available"
|
||||
onSelectionChanged={(event: SelectionChangedEvent) => {
|
||||
setSelectedRows(event.api.getSelectedRows().map((row) => row.index));
|
||||
}}
|
||||
rowSelection="multiple"
|
||||
suppressRowClickSelection={true}
|
||||
pagination={true}
|
||||
columnDefs={columns}
|
||||
rowData={rows}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
import IconComponent from "../../../../../../../components/genericIconComponent";
|
||||
import { Case } from "../../../../../../../shared/components/caseComponent";
|
||||
import { FilePreviewType } from "../../../../../../../types/components";
|
||||
import { classNames } from "../../../../../../../utils/utils";
|
||||
|
||||
type ButtonSendWrapperProps = {
|
||||
send: () => void;
|
||||
lockChat: boolean;
|
||||
noInput: boolean;
|
||||
saveLoading: boolean;
|
||||
chatValue: string;
|
||||
files: FilePreviewType[];
|
||||
};
|
||||
|
||||
const ButtonSendWrapper = ({
|
||||
send,
|
||||
lockChat,
|
||||
noInput,
|
||||
saveLoading,
|
||||
chatValue,
|
||||
files,
|
||||
}: ButtonSendWrapperProps) => {
|
||||
return (
|
||||
<button
|
||||
className={classNames(
|
||||
"form-modal-send-button",
|
||||
noInput
|
||||
? "bg-high-indigo text-background"
|
||||
: chatValue === ""
|
||||
? "text-primary"
|
||||
: "bg-chat-send text-background",
|
||||
)}
|
||||
disabled={lockChat || saveLoading}
|
||||
onClick={(): void => send()}
|
||||
>
|
||||
<Case
|
||||
condition={
|
||||
lockChat || saveLoading || files.some((file) => file.loading)
|
||||
}
|
||||
>
|
||||
<IconComponent
|
||||
name="Lock"
|
||||
className="form-modal-lock-icon"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Case>
|
||||
|
||||
<Case condition={noInput}>
|
||||
<IconComponent
|
||||
name="Zap"
|
||||
className="form-modal-play-icon"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Case>
|
||||
|
||||
<Case
|
||||
condition={
|
||||
!(lockChat || saveLoading || files.some((file) => file.loading)) &&
|
||||
!noInput
|
||||
}
|
||||
>
|
||||
<IconComponent
|
||||
name="LucideSend"
|
||||
className="form-modal-send-icon "
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Case>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default ButtonSendWrapper;
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
import { Textarea } from "../../../../../../../components/ui/textarea";
|
||||
import { classNames } from "../../../../../../../utils/utils";
|
||||
|
||||
const TextAreaWrapper = ({
|
||||
checkSendingOk,
|
||||
send,
|
||||
lockChat,
|
||||
noInput,
|
||||
saveLoading,
|
||||
chatValue,
|
||||
setChatValue,
|
||||
CHAT_INPUT_PLACEHOLDER,
|
||||
CHAT_INPUT_PLACEHOLDER_SEND,
|
||||
inputRef,
|
||||
setInputFocus,
|
||||
files,
|
||||
isDragging,
|
||||
}) => {
|
||||
const getPlaceholderText = (
|
||||
isDragging: boolean,
|
||||
noInput: boolean,
|
||||
): string => {
|
||||
if (isDragging) {
|
||||
return "Drop here";
|
||||
} else if (noInput) {
|
||||
return CHAT_INPUT_PLACEHOLDER;
|
||||
} else {
|
||||
return CHAT_INPUT_PLACEHOLDER_SEND;
|
||||
}
|
||||
};
|
||||
|
||||
const lockClass =
|
||||
lockChat || saveLoading
|
||||
? "form-modal-lock-true bg-input"
|
||||
: noInput
|
||||
? "form-modal-no-input bg-input"
|
||||
: "form-modal-lock-false bg-background";
|
||||
|
||||
const fileClass =
|
||||
files.length > 0
|
||||
? "rounded-b-lg ring-0 focus:ring-0 focus:border-2 rounded-t-none border-t-0 border-border focus:border-t-0 focus:border-ring"
|
||||
: "rounded-md border-t border-border focus:ring-0 focus:border-2 focus:border-ring";
|
||||
|
||||
const additionalClassNames = "form-modal-lockchat pl-14";
|
||||
|
||||
return (
|
||||
<Textarea
|
||||
onFocus={(e) => {
|
||||
setInputFocus(true);
|
||||
e.target.style.borderTopWidth = "0";
|
||||
}}
|
||||
onBlur={() => setInputFocus(false)}
|
||||
onKeyDown={(event) => {
|
||||
if (checkSendingOk(event)) {
|
||||
send();
|
||||
}
|
||||
}}
|
||||
rows={1}
|
||||
ref={inputRef}
|
||||
disabled={lockChat || noInput || saveLoading}
|
||||
style={{
|
||||
resize: "none",
|
||||
bottom: `${inputRef?.current?.scrollHeight}px`,
|
||||
maxHeight: "150px",
|
||||
overflow: `${
|
||||
inputRef.current && inputRef.current.scrollHeight > 150
|
||||
? "auto"
|
||||
: "hidden"
|
||||
}`,
|
||||
}}
|
||||
value={lockChat ? "Thinking..." : saveLoading ? "Saving..." : chatValue}
|
||||
onChange={(event): void => {
|
||||
setChatValue(event.target.value);
|
||||
}}
|
||||
className={classNames(lockClass, fileClass, additionalClassNames)}
|
||||
placeholder={getPlaceholderText(isDragging, noInput)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default TextAreaWrapper;
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import ForwardedIconComponent from "../../../../../../../components/genericIconComponent";
|
||||
import { Button } from "../../../../../../../components/ui/button";
|
||||
|
||||
const UploadFileButton = ({
|
||||
fileInputRef,
|
||||
handleFileChange,
|
||||
handleButtonClick,
|
||||
lockChat,
|
||||
}) => {
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
disabled={lockChat}
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
style={{ display: "none" }}
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
<Button
|
||||
disabled={lockChat}
|
||||
className={`font-bold text-white transition-all ${lockChat ? "cursor-not-allowed" : "hover:text-muted-foreground"}`}
|
||||
onClick={handleButtonClick}
|
||||
variant="none"
|
||||
size="none"
|
||||
>
|
||||
<ForwardedIconComponent name="PaperclipIcon" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UploadFileButton;
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
export const getClassNamesFilePreview = (inputFocus) => {
|
||||
return `flex w-full items-center gap-4 rounded-t-lg bg-background px-14 py-5 overflow-auto custom-scroll ${
|
||||
inputFocus
|
||||
? "border border-b-0 border-ring border-2"
|
||||
: "border border-b-0 border-border"
|
||||
}`;
|
||||
};
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import { useEffect } from "react";
|
||||
|
||||
const useAutoResizeTextArea = (value, inputRef) => {
|
||||
useEffect(() => {
|
||||
if (inputRef.current && inputRef.current.scrollHeight! !== 0) {
|
||||
inputRef.current.style!.height = "inherit"; // Reset the height
|
||||
inputRef.current.style!.height = `${inputRef.current.scrollHeight!}px`; // Set it to the scrollHeight
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
return inputRef;
|
||||
};
|
||||
|
||||
export default useAutoResizeTextArea;
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
import ShortUniqueId from "short-unique-id";
|
||||
import useFileUpload from "./use-file-upload";
|
||||
import useAlertStore from "../../../../../../stores/alertStore";
|
||||
|
||||
const fsErrorText =
|
||||
"Please ensure your file has one of the following extensions:";
|
||||
const snErrorTxt = "png, jpg, jpeg, gif, bmp, webp";
|
||||
|
||||
const useDragAndDrop = (
|
||||
setIsDragging,
|
||||
setFiles,
|
||||
currentFlowId,
|
||||
setErrorData,
|
||||
) => {
|
||||
const dragOver = (e) => {
|
||||
e.preventDefault();
|
||||
if (e.dataTransfer.types.some((type) => type === "Files")) {
|
||||
setIsDragging(true);
|
||||
}
|
||||
};
|
||||
|
||||
const dragEnter = (e) => {
|
||||
if (e.dataTransfer.types.some((type) => type === "Files")) {
|
||||
setIsDragging(true);
|
||||
}
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
const dragLeave = (e) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(false);
|
||||
};
|
||||
|
||||
const onDrop = (e) => {
|
||||
e.preventDefault();
|
||||
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
|
||||
handleFiles(e.dataTransfer.files, setFiles, currentFlowId, setErrorData);
|
||||
e.dataTransfer.clearData();
|
||||
}
|
||||
setIsDragging(false);
|
||||
};
|
||||
return {
|
||||
dragOver,
|
||||
dragEnter,
|
||||
dragLeave,
|
||||
onDrop,
|
||||
};
|
||||
};
|
||||
|
||||
const handleFiles = (files, setFiles, currentFlowId, setErrorData) => {
|
||||
if (files) {
|
||||
const allowedExtensions = ["png", "jpg", "jpeg", "gif", "bmp", "webp"];
|
||||
const file = files?.[0];
|
||||
const fileExtension = file.name.split(".").pop()?.toLowerCase();
|
||||
if (!fileExtension || !allowedExtensions.includes(fileExtension)) {
|
||||
console.log("Error uploading file");
|
||||
setErrorData({
|
||||
title: "Error uploading file",
|
||||
list: [fsErrorText, snErrorTxt],
|
||||
});
|
||||
return;
|
||||
}
|
||||
const uid = new ShortUniqueId({ length: 3 });
|
||||
const id = uid();
|
||||
const type = files[0].type.split("/")[0];
|
||||
const blob = files[0];
|
||||
|
||||
setFiles((prevFiles) => [
|
||||
...prevFiles,
|
||||
{ file: blob, loading: true, error: false, id, type },
|
||||
]);
|
||||
|
||||
useFileUpload(blob, currentFlowId, setFiles, id);
|
||||
}
|
||||
};
|
||||
export default useDragAndDrop;
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import { uploadFile } from "../../../../../../controllers/API";
|
||||
|
||||
const useFileUpload = (blob, currentFlowId, setFiles, id) => {
|
||||
uploadFile(blob, currentFlowId)
|
||||
.then((res) => {
|
||||
setFiles((prev) => {
|
||||
const newFiles = [...prev];
|
||||
const updatedIndex = newFiles.findIndex((file) => file.id === id);
|
||||
newFiles[updatedIndex].loading = false;
|
||||
newFiles[updatedIndex].path = res.data.file_path;
|
||||
return newFiles;
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
setFiles((prev) => {
|
||||
const newFiles = [...prev];
|
||||
const updatedIndex = newFiles.findIndex((file) => file.id === id);
|
||||
newFiles[updatedIndex].loading = false;
|
||||
newFiles[updatedIndex].error = true;
|
||||
return newFiles;
|
||||
});
|
||||
});
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default useFileUpload;
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import { useEffect } from "react";
|
||||
|
||||
const useFocusOnUnlock = (lockChat, inputRef) => {
|
||||
useEffect(() => {
|
||||
if (!lockChat && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [lockChat, inputRef]);
|
||||
|
||||
return inputRef;
|
||||
};
|
||||
|
||||
export default useFocusOnUnlock;
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
import ShortUniqueId from "short-unique-id";
|
||||
import useFileUpload from "./use-file-upload";
|
||||
import useAlertStore from "../../../../../../stores/alertStore";
|
||||
|
||||
const fsErrorText =
|
||||
"Please ensure your file has one of the following extensions:";
|
||||
const snErrorTxt = "png, jpg, jpeg, gif, bmp, webp";
|
||||
|
||||
export const useHandleFileChange = (setFiles, currentFlowId) => {
|
||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||
const handleFileChange = async (
|
||||
event: React.ChangeEvent<HTMLInputElement>,
|
||||
) => {
|
||||
const fileInput = event.target;
|
||||
const file = fileInput.files?.[0];
|
||||
if (file) {
|
||||
const allowedExtensions = ["png", "jpg", "jpeg", "gif", "bmp", "webp"];
|
||||
const fileExtension = file.name.split(".").pop()?.toLowerCase();
|
||||
|
||||
if (!fileExtension || !allowedExtensions.includes(fileExtension)) {
|
||||
setErrorData({
|
||||
title: "Error uploading file",
|
||||
list: [fsErrorText, snErrorTxt],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const uid = new ShortUniqueId({ length: 10 }); // Increase the length to ensure uniqueness
|
||||
const id = uid();
|
||||
const type = file.type.split("/")[0];
|
||||
const blob = file;
|
||||
|
||||
setFiles((prevFiles) => [
|
||||
...prevFiles,
|
||||
{ file: blob, loading: true, error: false, id, type },
|
||||
]);
|
||||
|
||||
useFileUpload(blob, currentFlowId, setFiles, id);
|
||||
}
|
||||
|
||||
// Clear the file input value to ensure the change event is triggered even for the same file
|
||||
fileInput.value = "";
|
||||
};
|
||||
|
||||
return {
|
||||
handleFileChange,
|
||||
};
|
||||
};
|
||||
|
||||
export default useHandleFileChange;
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
import { useEffect } from "react";
|
||||
import ShortUniqueId from "short-unique-id";
|
||||
import useFileUpload from "./use-file-upload";
|
||||
import useAlertStore from "../../../../../../stores/alertStore";
|
||||
|
||||
const fsErrorText =
|
||||
"Please ensure your file has one of the following extensions:";
|
||||
const snErrorTxt = "png, jpg, jpeg, gif, bmp, webp";
|
||||
|
||||
const useUpload = (uploadFile, currentFlowId, setFiles, lockChat) => {
|
||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||
useEffect(() => {
|
||||
const handlePaste = (event: ClipboardEvent): void => {
|
||||
if (lockChat) {
|
||||
return;
|
||||
}
|
||||
const items = event.clipboardData?.items;
|
||||
if (items) {
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const type = items[0].type.split("/")[0];
|
||||
const uid = new ShortUniqueId({ length: 3 });
|
||||
const blob = items[i].getAsFile();
|
||||
if (blob) {
|
||||
const allowedExtensions = [
|
||||
"png",
|
||||
"jpg",
|
||||
"jpeg",
|
||||
"gif",
|
||||
"bmp",
|
||||
"webp",
|
||||
];
|
||||
const fileExtension = blob.name.split(".").pop()?.toLowerCase();
|
||||
|
||||
if (!fileExtension || !allowedExtensions.includes(fileExtension)) {
|
||||
setErrorData({
|
||||
title: "Error uploading file",
|
||||
list: [fsErrorText, snErrorTxt],
|
||||
});
|
||||
return;
|
||||
}
|
||||
const id = uid();
|
||||
setFiles((prevFiles) => [
|
||||
...prevFiles,
|
||||
{ file: blob, loading: true, error: false, id, type },
|
||||
]);
|
||||
useFileUpload(blob, currentFlowId, setFiles, id);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("paste", handlePaste);
|
||||
return () => {
|
||||
document.removeEventListener("paste", handlePaste);
|
||||
};
|
||||
}, [uploadFile, currentFlowId, lockChat]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default useUpload;
|
||||
|
|
@ -1,14 +1,23 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import IconComponent from "../../../../../components/genericIconComponent";
|
||||
import { Textarea } from "../../../../../components/ui/textarea";
|
||||
import { useRef, useState } from "react";
|
||||
import {
|
||||
CHAT_INPUT_PLACEHOLDER,
|
||||
CHAT_INPUT_PLACEHOLDER_SEND,
|
||||
} from "../../../../../constants/constants";
|
||||
import { uploadFile } from "../../../../../controllers/API";
|
||||
import useFlowsManagerStore from "../../../../../stores/flowsManagerStore";
|
||||
import { chatInputType } from "../../../../../types/components";
|
||||
import { classNames } from "../../../../../utils/utils";
|
||||
|
||||
import {
|
||||
ChatInputType,
|
||||
FilePreviewType,
|
||||
} from "../../../../../types/components";
|
||||
import FilePreview from "../filePreviewChat";
|
||||
import ButtonSendWrapper from "./components/buttonSendWrapper";
|
||||
import TextAreaWrapper from "./components/textAreaWrapper";
|
||||
import UploadFileButton from "./components/uploadFileButton";
|
||||
import { getClassNamesFilePreview } from "./helpers/get-class-file-preview";
|
||||
import useAutoResizeTextArea from "./hooks/use-auto-resize-text-area";
|
||||
import useFocusOnUnlock from "./hooks/use-focus-unlock";
|
||||
import useHandleFileChange from "./hooks/use-handle-file-change";
|
||||
import useUpload from "./hooks/use-upload";
|
||||
export default function ChatInput({
|
||||
lockChat,
|
||||
chatValue,
|
||||
|
|
@ -16,125 +25,103 @@ export default function ChatInput({
|
|||
setChatValue,
|
||||
inputRef,
|
||||
noInput,
|
||||
}: chatInputType): JSX.Element {
|
||||
files,
|
||||
setFiles,
|
||||
isDragging,
|
||||
}: ChatInputType): JSX.Element {
|
||||
const [repeat, setRepeat] = useState(1);
|
||||
const saveLoading = useFlowsManagerStore((state) => state.saveLoading);
|
||||
useEffect(() => {
|
||||
if (!lockChat && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [lockChat, inputRef]);
|
||||
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
|
||||
const [inputFocus, setInputFocus] = useState<boolean>(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (inputRef.current && inputRef.current.scrollHeight !== 0) {
|
||||
inputRef.current.style.height = "inherit"; // Reset the height
|
||||
inputRef.current.style.height = `${inputRef.current.scrollHeight}px`; // Set it to the scrollHeight
|
||||
}
|
||||
}, [chatValue]);
|
||||
useFocusOnUnlock(lockChat, inputRef);
|
||||
useAutoResizeTextArea(chatValue, inputRef);
|
||||
useUpload(uploadFile, currentFlowId, setFiles, lockChat || saveLoading);
|
||||
const { handleFileChange } = useHandleFileChange(setFiles, currentFlowId);
|
||||
|
||||
const send = () => {
|
||||
sendMessage({
|
||||
repeat,
|
||||
files: files.map((file) => file.path ?? "").filter((file) => file !== ""),
|
||||
});
|
||||
setFiles([]);
|
||||
};
|
||||
|
||||
const checkSendingOk = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
return (
|
||||
event.key === "Enter" &&
|
||||
!lockChat &&
|
||||
!saveLoading &&
|
||||
!event.shiftKey &&
|
||||
!event.nativeEvent.isComposing
|
||||
);
|
||||
};
|
||||
|
||||
const classNameFilePreview = getClassNamesFilePreview(inputFocus);
|
||||
|
||||
const handleButtonClick = () => {
|
||||
fileInputRef.current!.click();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex w-full gap-2">
|
||||
<div className="flex w-full flex-col-reverse">
|
||||
<div className="relative w-full">
|
||||
<Textarea
|
||||
onKeyDown={(event) => {
|
||||
if (
|
||||
event.key === "Enter" &&
|
||||
!lockChat &&
|
||||
!saveLoading &&
|
||||
!event.shiftKey &&
|
||||
!event.nativeEvent.isComposing
|
||||
) {
|
||||
sendMessage(repeat);
|
||||
}
|
||||
}}
|
||||
rows={1}
|
||||
ref={inputRef}
|
||||
disabled={lockChat || noInput || saveLoading}
|
||||
style={{
|
||||
resize: "none",
|
||||
bottom: `${inputRef?.current?.scrollHeight}px`,
|
||||
maxHeight: "150px",
|
||||
overflow: `${
|
||||
inputRef.current && inputRef.current.scrollHeight > 150
|
||||
? "auto"
|
||||
: "hidden"
|
||||
}`,
|
||||
}}
|
||||
value={
|
||||
lockChat ? "Thinking..." : saveLoading ? "Saving..." : chatValue
|
||||
}
|
||||
onChange={(event): void => {
|
||||
setChatValue(event.target.value);
|
||||
}}
|
||||
className={classNames(
|
||||
lockChat || saveLoading
|
||||
? " form-modal-lock-true bg-input"
|
||||
: noInput
|
||||
? "form-modal-no-input bg-input"
|
||||
: " form-modal-lock-false bg-background",
|
||||
|
||||
"form-modal-lockchat"
|
||||
)}
|
||||
placeholder={
|
||||
noInput ? CHAT_INPUT_PLACEHOLDER : CHAT_INPUT_PLACEHOLDER_SEND
|
||||
}
|
||||
<TextAreaWrapper
|
||||
checkSendingOk={checkSendingOk}
|
||||
send={send}
|
||||
lockChat={lockChat}
|
||||
noInput={noInput}
|
||||
saveLoading={saveLoading}
|
||||
chatValue={chatValue}
|
||||
setChatValue={setChatValue}
|
||||
CHAT_INPUT_PLACEHOLDER={CHAT_INPUT_PLACEHOLDER}
|
||||
CHAT_INPUT_PLACEHOLDER_SEND={CHAT_INPUT_PLACEHOLDER_SEND}
|
||||
inputRef={inputRef}
|
||||
setInputFocus={setInputFocus}
|
||||
files={files}
|
||||
isDragging={isDragging}
|
||||
/>
|
||||
<div className="form-modal-send-icon-position">
|
||||
<button
|
||||
className={classNames(
|
||||
"form-modal-send-button",
|
||||
noInput
|
||||
? "bg-high-indigo text-background"
|
||||
: chatValue === ""
|
||||
? "text-primary"
|
||||
: "bg-chat-send text-background"
|
||||
)}
|
||||
disabled={lockChat || saveLoading}
|
||||
onClick={(): void => sendMessage(repeat)}
|
||||
>
|
||||
{lockChat || saveLoading ? (
|
||||
<IconComponent
|
||||
name="Lock"
|
||||
className="form-modal-lock-icon"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : noInput ? (
|
||||
<IconComponent
|
||||
name="Zap"
|
||||
className="form-modal-play-icon"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : (
|
||||
<IconComponent
|
||||
name="LucideSend"
|
||||
className="form-modal-send-icon "
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
<ButtonSendWrapper
|
||||
send={send}
|
||||
lockChat={lockChat}
|
||||
noInput={noInput}
|
||||
saveLoading={saveLoading}
|
||||
chatValue={chatValue}
|
||||
files={files}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`absolute bottom-2 left-4 ${lockChat || saveLoading ? "cursor-not-allowed" : ""}`}
|
||||
>
|
||||
<UploadFileButton
|
||||
lockChat={lockChat || saveLoading}
|
||||
fileInputRef={fileInputRef}
|
||||
handleFileChange={handleFileChange}
|
||||
handleButtonClick={handleButtonClick}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/*
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="primary" className="h-13 px-4">
|
||||
<IconComponent name="Repeat" className="" aria-hidden="true" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-fit">
|
||||
<div className="flex flex-col items-center justify-center gap-2">
|
||||
<span className="text-sm">Repetitions: </span>
|
||||
<Input
|
||||
onChange={(e) => {
|
||||
handleChange(parseInt(e.target.value));
|
||||
{files.length > 0 && (
|
||||
<div className={classNameFilePreview}>
|
||||
{files.map((file) => (
|
||||
<FilePreview
|
||||
error={file.error}
|
||||
file={file.file}
|
||||
loading={file.loading}
|
||||
key={file.id}
|
||||
onDelete={() => {
|
||||
setFiles((prev: FilePreviewType[]) =>
|
||||
prev.filter((f) => f.id !== file.id),
|
||||
);
|
||||
// TODO: delete file on backend
|
||||
}}
|
||||
className="w-16"
|
||||
type="number"
|
||||
min={0}
|
||||
/>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover> */}
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
import { useState } from "react";
|
||||
import ForwardedIconComponent from "../../../../../../../components/genericIconComponent";
|
||||
import formatFileName from "../../../filePreviewChat/utils/format-file-name";
|
||||
import FileCard from "../../../fileComponent";
|
||||
|
||||
export default function FileCardWrapper({
|
||||
index,
|
||||
name,
|
||||
type,
|
||||
path,
|
||||
}: {
|
||||
index: number;
|
||||
name: string;
|
||||
type: string;
|
||||
path: string;
|
||||
}) {
|
||||
const [show, setShow] = useState<boolean>(true);
|
||||
return (
|
||||
<div key={index} className="flex flex-col gap-2">
|
||||
<span
|
||||
onClick={() => setShow(!show)}
|
||||
className="flex cursor-pointer gap-2 text-sm text-muted-foreground"
|
||||
>
|
||||
{formatFileName(name, 50)}
|
||||
<ForwardedIconComponent name={show ? "ChevronDown" : "ChevronRight"} />
|
||||
</span>
|
||||
<FileCard
|
||||
showFile={show}
|
||||
fileName={name}
|
||||
fileType={type}
|
||||
content={path}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -7,13 +7,17 @@ import remarkMath from "remark-math";
|
|||
import MaleTechnology from "../../../../../assets/male-technologist.png";
|
||||
import Robot from "../../../../../assets/robot.png";
|
||||
import CodeTabsComponent from "../../../../../components/codeTabsComponent";
|
||||
import IconComponent from "../../../../../components/genericIconComponent";
|
||||
import IconComponent, {
|
||||
ForwardedIconComponent,
|
||||
} from "../../../../../components/genericIconComponent";
|
||||
import SanitizedHTMLWrapper from "../../../../../components/sanitizedHTMLWrapper";
|
||||
import useAlertStore from "../../../../../stores/alertStore";
|
||||
import useFlowStore from "../../../../../stores/flowStore";
|
||||
import { chatMessagePropsType } from "../../../../../types/components";
|
||||
import { classNames, cn } from "../../../../../utils/utils";
|
||||
import FileCard from "../fileComponent";
|
||||
import formatFileName from "../filePreviewChat/utils/format-file-name";
|
||||
import FileCardWrapper from "./components/fileCardWrapper";
|
||||
|
||||
export default function ChatMessage({
|
||||
chat,
|
||||
|
|
@ -22,6 +26,7 @@ export default function ChatMessage({
|
|||
updateChat,
|
||||
setLockChat,
|
||||
}: chatMessagePropsType): JSX.Element {
|
||||
const [showFile, setShowFile] = useState<boolean>(true);
|
||||
const convert = new Convert({ newline: true });
|
||||
const [hidden, setHidden] = useState(true);
|
||||
const template = chat.template;
|
||||
|
|
@ -251,21 +256,6 @@ dark:prose-invert"
|
|||
[chat.message, chatMessage],
|
||||
)}
|
||||
</div>
|
||||
{chat.files && (
|
||||
<div className="my-2 w-full">
|
||||
{chat.files.map((file, index) => {
|
||||
return (
|
||||
<div key={index} className="my-2 w-full">
|
||||
<FileCard
|
||||
fileName={"Generated File"}
|
||||
fileType={file.data_type}
|
||||
content={file.data}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -323,14 +313,30 @@ dark:prose-invert"
|
|||
</span>
|
||||
</>
|
||||
) : (
|
||||
<span
|
||||
className="prose text-primary word-break-break-word dark:prose-invert"
|
||||
data-testid={
|
||||
"chat-message-" + chat.sender_name + "-" + chatMessage
|
||||
}
|
||||
>
|
||||
{chatMessage}
|
||||
</span>
|
||||
<div className="flex flex-col">
|
||||
<span
|
||||
className="prose text-primary word-break-break-word dark:prose-invert"
|
||||
data-testid={
|
||||
"chat-message-" + chat.sender_name + "-" + chatMessage
|
||||
}
|
||||
>
|
||||
{chatMessage}
|
||||
</span>
|
||||
{chat.files && (
|
||||
<div className="my-2 flex flex-col gap-5">
|
||||
{chat.files.map((file, index) => {
|
||||
return (
|
||||
<FileCardWrapper
|
||||
index={index}
|
||||
name={file.name}
|
||||
type={file.type}
|
||||
path={file.path}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
import ForwardedIconComponent from "../../../../../../../components/genericIconComponent";
|
||||
import { Button } from "../../../../../../../components/ui/button";
|
||||
|
||||
export default function DownloadButton({
|
||||
isHovered,
|
||||
handleDownload,
|
||||
}: {
|
||||
isHovered: boolean;
|
||||
handleDownload: () => void;
|
||||
}): JSX.Element | undefined {
|
||||
if (isHovered) {
|
||||
return (
|
||||
<div
|
||||
className={`absolute right-1 top-1 rounded-md bg-muted text-sm font-bold text-foreground `}
|
||||
>
|
||||
<Button
|
||||
variant={"none"}
|
||||
className="bg-transparent px-2 py-1 text-ring"
|
||||
onClick={handleDownload}
|
||||
>
|
||||
<ForwardedIconComponent
|
||||
name="DownloadCloud"
|
||||
className="h-5 w-5 bg-transparent text-current hover:scale-110"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
|
@ -1,26 +1,23 @@
|
|||
import * as base64js from "base64-js";
|
||||
import { useState } from "react";
|
||||
import IconComponent from "../../../../../components/genericIconComponent";
|
||||
import { ForwardedIconComponent } from "../../../../../components/genericIconComponent";
|
||||
import { BACKEND_URL, BASE_URL_API } from "../../../../../constants/constants";
|
||||
import useFlowsManagerStore from "../../../../../stores/flowsManagerStore";
|
||||
import { fileCardPropsType } from "../../../../../types/components";
|
||||
import formatFileName from "../filePreviewChat/utils/format-file-name";
|
||||
import DownloadButton from "./components/downloadButton/downloadButton";
|
||||
import getClasses from "./utils/get-classes";
|
||||
import handleDownload from "./utils/handle-download";
|
||||
|
||||
const imgTypes = new Set(["png", "jpg", "jpeg", "gif", "webp", "image"]);
|
||||
|
||||
export default function FileCard({
|
||||
fileName,
|
||||
content,
|
||||
fileType,
|
||||
}: fileCardPropsType): JSX.Element {
|
||||
const handleDownload = (): void => {
|
||||
const byteArray = new Uint8Array(base64js.toByteArray(content));
|
||||
const blob = new Blob([byteArray], { type: "application/octet-stream" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = fileName + ".png";
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
showFile = true,
|
||||
}: fileCardPropsType): JSX.Element | undefined {
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
|
||||
function handleMouseEnter(): void {
|
||||
setIsHovered(true);
|
||||
}
|
||||
|
|
@ -28,58 +25,57 @@ export default function FileCard({
|
|||
setIsHovered(false);
|
||||
}
|
||||
|
||||
if (fileType === "image") {
|
||||
const fileWrapperClasses = getClasses(isHovered);
|
||||
|
||||
const imgSrc = `${BACKEND_URL.slice(
|
||||
0,
|
||||
BACKEND_URL.length - 1,
|
||||
)}${BASE_URL_API}files/images/${content}`;
|
||||
|
||||
if (showFile) {
|
||||
if (imgTypes.has(fileType)) {
|
||||
return (
|
||||
<div
|
||||
className="inline-block w-full rounded-lg transition-all"
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
style={{ display: "inline-block" }}
|
||||
>
|
||||
<div className="relative w-[50%] rounded-lg border border-border">
|
||||
<img
|
||||
src={imgSrc}
|
||||
alt="generated image"
|
||||
className="m-0 h-auto w-auto rounded-lg border border-border p-0 transition-all"
|
||||
/>
|
||||
<DownloadButton
|
||||
isHovered={isHovered}
|
||||
handleDownload={() => handleDownload({ fileName, content })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative h-1/4 w-1/4"
|
||||
className={fileWrapperClasses}
|
||||
onClick={() => handleDownload({ fileName, content })}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<img
|
||||
src={`data:image/png;base64,${content}`}
|
||||
alt="generated image"
|
||||
className="h-full w-full rounded-lg"
|
||||
/>
|
||||
{isHovered && (
|
||||
<div className={`file-card-modal-image-div `}>
|
||||
<button
|
||||
className="file-card-modal-image-button "
|
||||
onClick={handleDownload}
|
||||
>
|
||||
<IconComponent
|
||||
name="DownloadCloud"
|
||||
className="h-5 w-5 text-current hover:scale-110"
|
||||
/>
|
||||
</button>
|
||||
<div className="ml-3 flex h-full w-full items-center gap-2 text-sm">
|
||||
<ForwardedIconComponent name="File" className="h-8 w-8" />
|
||||
<div className="flex flex-col">
|
||||
<span className="font-bold">{formatFileName(fileName, 20)}</span>
|
||||
<span>File</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<DownloadButton
|
||||
isHovered={isHovered}
|
||||
handleDownload={() => handleDownload({ fileName, content })}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button onClick={handleDownload} className="file-card-modal-button">
|
||||
<div className="file-card-modal-div">
|
||||
ooooooooooooooo{" "}
|
||||
{fileType === "image" ? (
|
||||
<img
|
||||
src={`data:image/png;base64,${content}`}
|
||||
alt=""
|
||||
className="h-8 w-8"
|
||||
/>
|
||||
) : (
|
||||
<IconComponent name="File" className="h-8 w-8" />
|
||||
)}
|
||||
<div className="file-card-modal-footer">
|
||||
{" "}
|
||||
<div className="file-card-modal-name">{fileName}</div>
|
||||
<div className="file-card-modal-type">{fileType}</div>
|
||||
</div>
|
||||
<IconComponent
|
||||
name="DownloadCloud"
|
||||
className="ml-auto h-6 w-6 text-current"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
export default function getClasses(isHovered: boolean): string {
|
||||
return `relative ${false ? "h-20 w-20" : "h-20 w-80"} cursor-pointer rounded-lg border border-ring bg-muted shadow transition duration-300 hover:drop-shadow-lg ${
|
||||
isHovered ? "shadow-md" : ""
|
||||
}`;
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
import {
|
||||
BACKEND_URL,
|
||||
BASE_URL_API,
|
||||
} from "../../../../../../constants/constants";
|
||||
|
||||
let isDownloading = false;
|
||||
|
||||
export default async function handleDownload({
|
||||
fileName,
|
||||
content,
|
||||
}: {
|
||||
fileName: string;
|
||||
content: string;
|
||||
}): Promise<void> {
|
||||
if (isDownloading) return;
|
||||
|
||||
try {
|
||||
isDownloading = true;
|
||||
|
||||
const response = await fetch(
|
||||
`${BACKEND_URL.slice(0, BACKEND_URL.length - 1)}${BASE_URL_API}files/download/${content}`,
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error("Network response was not ok");
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.setAttribute("download", fileName); // Set the filename
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
URL.revokeObjectURL(url); // Clean up the URL object
|
||||
} catch (error) {
|
||||
console.error("Failed to download file:", error);
|
||||
} finally {
|
||||
isDownloading = false;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
import { useState } from "react";
|
||||
import IconComponent, {
|
||||
ForwardedIconComponent,
|
||||
} from "../../../../../components/genericIconComponent";
|
||||
import { Skeleton } from "../../../../../components/ui/skeleton";
|
||||
import formatFileName from "./utils/format-file-name";
|
||||
|
||||
const supImgFiles = ["png", "jpg", "jpeg", "gif", "bmp", "webp", "image"];
|
||||
|
||||
export default function FilePreview({
|
||||
error,
|
||||
file,
|
||||
loading,
|
||||
onDelete,
|
||||
}: {
|
||||
loading: boolean;
|
||||
file: File;
|
||||
error: boolean;
|
||||
onDelete: () => void;
|
||||
}) {
|
||||
const fileType = file.type.toLowerCase();
|
||||
const isImage = supImgFiles.some((type) => fileType.includes(type));
|
||||
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="relative inline-block">
|
||||
{loading ? (
|
||||
isImage ? (
|
||||
<div className="flex h-20 w-20 items-center justify-center rounded-md border border-ring bg-background ">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className={`h-10 w-10 animate-spin fill-black text-muted`}
|
||||
viewBox="0 0 100 101"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
||||
fill="currentFill"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={`relative ${
|
||||
isImage ? "h-20 w-20" : "h-20 w-80"
|
||||
} cursor-wait rounded-lg border border-ring bg-background transition duration-300 ${
|
||||
isHovered ? "shadow-md" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="ml-3 flex h-full w-full items-center gap-2 text-sm">
|
||||
<Skeleton className="h-10 w-10 rounded-lg" />
|
||||
<div className="flex flex-col gap-1">
|
||||
<Skeleton className="h-3 w-48" />
|
||||
<Skeleton className="h-3 w-10" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
) : error ? (
|
||||
<div>Error...</div>
|
||||
) : (
|
||||
<div
|
||||
className={`relative mt-2 ${
|
||||
isImage ? "h-20 w-20" : "h-20 w-80"
|
||||
} cursor-pointer rounded-lg border border-ring bg-background transition duration-300 ${
|
||||
isHovered ? "shadow-md" : ""
|
||||
}`}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
{isImage ? (
|
||||
<img
|
||||
src={URL.createObjectURL(file)}
|
||||
alt="file"
|
||||
className="block h-full w-full rounded-md border border-border"
|
||||
/>
|
||||
) : (
|
||||
<div className="ml-3 flex h-full w-full items-center gap-2 text-sm">
|
||||
<ForwardedIconComponent name="File" className="h-8 w-8" />
|
||||
<div className="flex flex-col">
|
||||
<span className="font-bold">{formatFileName(file.name)}</span>
|
||||
<span>File</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{isHovered && (
|
||||
<div
|
||||
className={`absolute ${
|
||||
isImage ? "bottom-16 left-16" : "bottom-16 left-[19em]"
|
||||
} flex h-5 w-5 items-center justify-center`}
|
||||
>
|
||||
<div
|
||||
className="flex h-7 w-7 cursor-pointer items-center justify-center rounded-full bg-gray-200 p-2 transition-all"
|
||||
onClick={onDelete}
|
||||
>
|
||||
<IconComponent name="X" className="stroke-slate-950 stroke-2" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
export default function formatFileName(
|
||||
name: string,
|
||||
numberToTruncate: number = 25,
|
||||
): string {
|
||||
if (name[numberToTruncate] === undefined) {
|
||||
return name;
|
||||
}
|
||||
const fileExtension = name.split(".").pop(); // Get the file extension
|
||||
const baseName = name.slice(0, name.lastIndexOf(".")); // Get the base name without the extension
|
||||
if (baseName.length > 6) {
|
||||
return `${baseName.slice(0, numberToTruncate)}...${fileExtension}`;
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { useEffect, useRef, useState } from "react";
|
||||
import IconComponent from "../../../../components/genericIconComponent";
|
||||
import { Button } from "../../../../components/ui/button";
|
||||
import {
|
||||
CHAT_FIRST_INITIAL_TEXT,
|
||||
CHAT_SECOND_INITIAL_TEXT,
|
||||
|
|
@ -8,15 +9,12 @@ import { deleteFlowPool } from "../../../../controllers/API";
|
|||
import useAlertStore from "../../../../stores/alertStore";
|
||||
import useFlowStore from "../../../../stores/flowStore";
|
||||
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
|
||||
import { sendAllProps } from "../../../../types/api";
|
||||
import {
|
||||
ChatMessageType,
|
||||
ChatOutputType,
|
||||
FlowPoolObjectType,
|
||||
} from "../../../../types/chat";
|
||||
import { chatViewProps } from "../../../../types/components";
|
||||
import { VertexBuildTypeAPI, sendAllProps } from "../../../../types/api";
|
||||
import { ChatMessageType, ChatOutputType } from "../../../../types/chat";
|
||||
import { FilePreviewType, chatViewProps } from "../../../../types/components";
|
||||
import { classNames } from "../../../../utils/utils";
|
||||
import ChatInput from "./chatInput";
|
||||
import useDragAndDrop from "./chatInput/hooks/use-drag-and-drop";
|
||||
import ChatMessage from "./chatMessage";
|
||||
|
||||
export default function ChatView({
|
||||
|
|
@ -27,7 +25,7 @@ export default function ChatView({
|
|||
setLockChat,
|
||||
}: chatViewProps): JSX.Element {
|
||||
const { flowPool, outputs, inputs, CleanFlowPool } = useFlowStore();
|
||||
const { setNoticeData } = useAlertStore();
|
||||
const { setErrorData } = useAlertStore();
|
||||
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
|
||||
const messagesRef = useRef<HTMLDivElement | null>(null);
|
||||
const [chatHistory, setChatHistory] = useState<ChatMessageType[]>([]);
|
||||
|
|
@ -46,7 +44,7 @@ export default function ChatView({
|
|||
|
||||
//build chat history
|
||||
useEffect(() => {
|
||||
const chatOutputResponses: FlowPoolObjectType[] = [];
|
||||
const chatOutputResponses: VertexBuildTypeAPI[] = [];
|
||||
outputIds.forEach((outputId) => {
|
||||
if (outputId.includes("ChatOutput")) {
|
||||
if (flowPool[outputId] && flowPool[outputId].length > 0) {
|
||||
|
|
@ -64,11 +62,11 @@ export default function ChatView({
|
|||
const chatMessages: ChatMessageType[] = chatOutputResponses
|
||||
.sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp))
|
||||
//
|
||||
.filter((output) => output.data.artifacts?.message !== null)
|
||||
.filter((output) => output.data.message)
|
||||
.map((output, index) => {
|
||||
try {
|
||||
const { sender, message, sender_name, stream_url } = output.data
|
||||
.artifacts as ChatOutputType;
|
||||
const { sender, message, sender_name, stream_url, files } = output
|
||||
.data.message as ChatOutputType;
|
||||
|
||||
const is_ai = sender === "Machine" || sender === null;
|
||||
return {
|
||||
|
|
@ -77,6 +75,7 @@ export default function ChatView({
|
|||
sender_name,
|
||||
componentId: output.id,
|
||||
stream_url: stream_url,
|
||||
files,
|
||||
};
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
|
@ -118,10 +117,21 @@ export default function ChatView({
|
|||
if (lockChat) setLockChat(false);
|
||||
}
|
||||
|
||||
function handleSelectChange(event: string): void {
|
||||
switch (event) {
|
||||
case "builds":
|
||||
clearChat();
|
||||
break;
|
||||
case "buildsNSession":
|
||||
console.log("delete build and session");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function updateChat(
|
||||
chat: ChatMessageType,
|
||||
message: string,
|
||||
stream_url?: string
|
||||
stream_url?: string,
|
||||
) {
|
||||
// if (message === "") return;
|
||||
chat.message = message;
|
||||
|
|
@ -144,23 +154,77 @@ export default function ChatView({
|
|||
// return newChatHistory;
|
||||
// });
|
||||
}
|
||||
const [files, setFiles] = useState<FilePreviewType[]>([]);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
|
||||
const { dragOver, dragEnter, dragLeave, onDrop } = useDragAndDrop(
|
||||
setIsDragging,
|
||||
setFiles,
|
||||
currentFlowId,
|
||||
setErrorData,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="eraser-column-arrangement">
|
||||
<div
|
||||
className="eraser-column-arrangement"
|
||||
onDragOver={dragOver}
|
||||
onDragEnter={dragEnter}
|
||||
onDragLeave={dragLeave}
|
||||
onDrop={onDrop}
|
||||
>
|
||||
<div className="eraser-size">
|
||||
<div className="eraser-position">
|
||||
<button disabled={lockChat} onClick={() => clearChat()}>
|
||||
<Button
|
||||
className="flex gap-1"
|
||||
size="none"
|
||||
variant="none"
|
||||
disabled={lockChat}
|
||||
onClick={() => handleSelectChange("builds")}
|
||||
>
|
||||
<IconComponent
|
||||
name="Eraser"
|
||||
className={classNames(
|
||||
"h-5 w-5",
|
||||
lockChat
|
||||
? "animate-pulse text-primary"
|
||||
: "text-primary hover:text-gray-600"
|
||||
)}
|
||||
className={classNames("h-5 w-5 text-primary")}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
</Button>
|
||||
{/* <Select
|
||||
onValueChange={handleSelectChange}
|
||||
value=""
|
||||
disabled={lockChat}
|
||||
>
|
||||
<SelectTrigger className="">
|
||||
<button className="flex gap-1">
|
||||
<IconComponent
|
||||
name="Eraser"
|
||||
className={classNames(
|
||||
"h-5 w-5 transition-all duration-100",
|
||||
lockChat ? "animate-pulse text-primary" : "text-primary",
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
</SelectTrigger>
|
||||
<SelectContent className="right-[9.5em]">
|
||||
<SelectItem value="builds" className="cursor-pointer">
|
||||
<div className="flex">
|
||||
<IconComponent
|
||||
name={"Trash2"}
|
||||
className={`relative top-0.5 mr-2 h-4 w-4`}
|
||||
/>
|
||||
<span className="">Clear Builds</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="buildsNSession" className="cursor-pointer">
|
||||
<div className="flex">
|
||||
<IconComponent
|
||||
name={"Trash2"}
|
||||
className={`relative top-0.5 mr-2 h-4 w-4`}
|
||||
/>
|
||||
<span className="">Clear Builds & Session</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select> */}
|
||||
</div>
|
||||
<div ref={messagesRef} className="chat-message-div">
|
||||
{chatHistory?.length > 0 ? (
|
||||
|
|
@ -202,11 +266,16 @@ export default function ChatView({
|
|||
chatValue={chatValue}
|
||||
noInput={!inputTypes.includes("ChatInput")}
|
||||
lockChat={lockChat}
|
||||
sendMessage={(count) => sendMessage(count)}
|
||||
sendMessage={({ repeat, files }) =>
|
||||
sendMessage({ repeat, files })
|
||||
}
|
||||
setChatValue={(value) => {
|
||||
setChatValue(value);
|
||||
}}
|
||||
inputRef={ref}
|
||||
files={files}
|
||||
setFiles={setFiles}
|
||||
isDragging={isDragging}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,26 +3,28 @@ import AccordionComponent from "../../components/accordionComponent";
|
|||
import IconComponent from "../../components/genericIconComponent";
|
||||
import ShadTooltip from "../../components/shadTooltipComponent";
|
||||
import { Badge } from "../../components/ui/badge";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "../../components/ui/tabs";
|
||||
import {
|
||||
CHAT_FORM_DIALOG_SUBTITLE,
|
||||
OUTPUTS_MODAL_TITLE,
|
||||
TEXT_INPUT_MODAL_TITLE,
|
||||
} from "../../constants/constants";
|
||||
import { CHAT_FORM_DIALOG_SUBTITLE } from "../../constants/constants";
|
||||
import { InputOutput } from "../../constants/enums";
|
||||
import { getMessagesTable } from "../../controllers/API";
|
||||
import useAlertStore from "../../stores/alertStore";
|
||||
import useFlowStore from "../../stores/flowStore";
|
||||
import useFlowsManagerStore from "../../stores/flowsManagerStore";
|
||||
import { useMessagesStore } from "../../stores/messagesStore";
|
||||
import { IOModalPropsType } from "../../types/components";
|
||||
import { NodeType } from "../../types/flow";
|
||||
import { NodeDataType, NodeType } from "../../types/flow";
|
||||
import { updateVerticesOrder } from "../../utils/buildUtils";
|
||||
import { cn } from "../../utils/utils";
|
||||
import BaseModal from "../baseModal";
|
||||
import IOFieldView from "./components/IOFieldView";
|
||||
import SessionView from "./components/SessionView";
|
||||
import useRemoveSession from "./components/SessionView/hooks";
|
||||
import ChatView from "./components/chatView";
|
||||
|
||||
export default function IOModal({
|
||||
|
|
@ -32,6 +34,7 @@ export default function IOModal({
|
|||
disable,
|
||||
}: IOModalPropsType): JSX.Element {
|
||||
const allNodes = useFlowStore((state) => state.nodes);
|
||||
const setMessages = useMessagesStore((state) => state.setMessages);
|
||||
const inputs = useFlowStore((state) => state.inputs).filter(
|
||||
(input) => input.type !== "ChatInput",
|
||||
);
|
||||
|
|
@ -53,6 +56,8 @@ export default function IOModal({
|
|||
const [selectedTab, setSelectedTab] = useState(
|
||||
inputs.length > 0 ? 1 : outputs.length > 0 ? 2 : 0,
|
||||
);
|
||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||
const setSuccessData = useAlertStore((state) => state.setSuccessData);
|
||||
|
||||
function startView() {
|
||||
if (!chatInput && !chatOutput) {
|
||||
|
|
@ -77,51 +82,94 @@ export default function IOModal({
|
|||
const isBuilding = useFlowStore((state) => state.isBuilding);
|
||||
const currentFlow = useFlowsManagerStore((state) => state.currentFlow);
|
||||
const setNode = useFlowStore((state) => state.setNode);
|
||||
|
||||
const [sessions, setSessions] = useState<string[]>([]);
|
||||
const messages = useMessagesStore((state) => state.messages);
|
||||
const setColumns = useMessagesStore((state) => state.setColumns);
|
||||
async function updateVertices() {
|
||||
return updateVerticesOrder(currentFlow!.id, null);
|
||||
}
|
||||
|
||||
async function sendMessage(count = 1): Promise<void> {
|
||||
async function sendMessage({
|
||||
repeat = 1,
|
||||
files,
|
||||
}: {
|
||||
repeat: number;
|
||||
files?: string[];
|
||||
}): Promise<void> {
|
||||
if (isBuilding) return;
|
||||
setIsBuilding(true);
|
||||
setLockChat(true);
|
||||
setChatValue("");
|
||||
for (let i = 0; i < count; i++) {
|
||||
for (let i = 0; i < repeat; i++) {
|
||||
await buildFlow({
|
||||
input_value: chatValue,
|
||||
startNodeId: chatInput?.id,
|
||||
files: files,
|
||||
silent: true,
|
||||
}).catch((err) => {
|
||||
console.error(err);
|
||||
setLockChat(false);
|
||||
});
|
||||
}
|
||||
const { rows, columns } = await getMessagesTable("union", currentFlow!.id);
|
||||
setMessages(rows);
|
||||
setColumns(columns);
|
||||
setLockChat(false);
|
||||
if (chatInput) {
|
||||
setNode(chatInput.id, (node: NodeType) => {
|
||||
const newNode = { ...node };
|
||||
|
||||
newNode.data.node!.template["input_value"].value = chatValue;
|
||||
return newNode;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const { handleRemoveSession } = useRemoveSession(
|
||||
setSuccessData,
|
||||
setErrorData,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedTab(inputs.length > 0 ? 1 : outputs.length > 0 ? 2 : 0);
|
||||
}, [allNodes.length]);
|
||||
|
||||
const flow_sessions = allNodes.map((node) => {
|
||||
if ((node.data as NodeDataType).node?.template["session_id"]) {
|
||||
return {
|
||||
id: node.id,
|
||||
session_id: (node.data as NodeDataType).node?.template["session_id"]
|
||||
.value,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedViewField(startView());
|
||||
if (haveChat) {
|
||||
getMessagesTable("union", currentFlow!.id).then(({ rows, columns }) => {
|
||||
setMessages(rows);
|
||||
setColumns(columns);
|
||||
});
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
const sessions = new Set<string>();
|
||||
messages.forEach((row) => {
|
||||
sessions.add(row.session_id);
|
||||
});
|
||||
setSessions(Array.from(sessions));
|
||||
sessions;
|
||||
}, [messages]);
|
||||
|
||||
return (
|
||||
<BaseModal
|
||||
size={selectedTab === 0 ? "sm-thin" : "md-thin"}
|
||||
size={"md-thin"}
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
disable={disable}
|
||||
onSubmit={() => sendMessage(1)}
|
||||
onSubmit={() => sendMessage({ repeat: 1 })}
|
||||
>
|
||||
<BaseModal.Trigger>{children}</BaseModal.Trigger>
|
||||
{/* TODO ADAPT TO ALL TYPES OF INPUTS AND OUTPUTS */}
|
||||
|
|
@ -138,176 +186,233 @@ export default function IOModal({
|
|||
<BaseModal.Content>
|
||||
<div className="flex h-full flex-col ">
|
||||
<div className="flex-max-width h-full">
|
||||
{selectedTab !== 0 && (
|
||||
<div
|
||||
className={cn(
|
||||
"mr-6 flex h-full w-2/6 flex-shrink-0 flex-col justify-start transition-all duration-300",
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
"mr-6 flex h-full w-2/6 flex-shrink-0 flex-col justify-start transition-all duration-300",
|
||||
)}
|
||||
>
|
||||
<Tabs
|
||||
value={selectedTab.toString()}
|
||||
className={
|
||||
"flex h-full flex-col overflow-y-auto rounded-md border bg-muted text-center custom-scroll"
|
||||
}
|
||||
onValueChange={(value) => {
|
||||
setSelectedTab(Number(value));
|
||||
}}
|
||||
>
|
||||
<Tabs
|
||||
value={selectedTab.toString()}
|
||||
className={
|
||||
"flex h-full flex-col overflow-y-auto rounded-md border bg-muted text-center custom-scroll"
|
||||
}
|
||||
onValueChange={(value) => {
|
||||
setSelectedTab(Number(value));
|
||||
}}
|
||||
>
|
||||
<div className="api-modal-tablist-div">
|
||||
<TabsList>
|
||||
{inputs.length > 0 && (
|
||||
<TabsTrigger value={"1"}>Inputs</TabsTrigger>
|
||||
)}
|
||||
{outputs.length > 0 && (
|
||||
<TabsTrigger value={"2"}>Outputs</TabsTrigger>
|
||||
)}
|
||||
</TabsList>
|
||||
</div>
|
||||
<div className="api-modal-tablist-div">
|
||||
<TabsList>
|
||||
{inputs.length > 0 && (
|
||||
<TabsTrigger value={"1"}>Inputs</TabsTrigger>
|
||||
)}
|
||||
{outputs.length > 0 && (
|
||||
<TabsTrigger value={"2"}>Outputs</TabsTrigger>
|
||||
)}
|
||||
{haveChat && (
|
||||
<TabsTrigger value={"0"}>Memories</TabsTrigger>
|
||||
)}
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
<TabsContent
|
||||
value={"1"}
|
||||
className="api-modal-tabs-content mt-4"
|
||||
>
|
||||
<div className="mx-2 mb-2 flex items-center gap-2 text-sm font-bold">
|
||||
<IconComponent className="h-4 w-4" name={"Type"} />
|
||||
{TEXT_INPUT_MODAL_TITLE}
|
||||
</div>
|
||||
{nodes
|
||||
.filter((node) =>
|
||||
inputs.some((input) => input.id === node.id),
|
||||
)
|
||||
.map((node, index) => {
|
||||
const input = inputs.find(
|
||||
(input) => input.id === node.id,
|
||||
)!;
|
||||
return (
|
||||
<div
|
||||
className="file-component-accordion-div"
|
||||
key={index}
|
||||
>
|
||||
<AccordionComponent
|
||||
trigger={
|
||||
<div className="file-component-badge-div">
|
||||
<ShadTooltip
|
||||
content={input.id}
|
||||
styleClasses="z-50"
|
||||
>
|
||||
<div>
|
||||
<Badge variant="gray" size="md">
|
||||
{node.data.node.display_name}
|
||||
</Badge>
|
||||
</div>
|
||||
</ShadTooltip>
|
||||
<div
|
||||
className="-mb-1 pr-4"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
setSelectedViewField(input);
|
||||
}}
|
||||
>
|
||||
<IconComponent
|
||||
className="h-4 w-4"
|
||||
name="ExternalLink"
|
||||
></IconComponent>
|
||||
<TabsContent value={"1"} className="api-modal-tabs-content">
|
||||
{nodes
|
||||
.filter((node) =>
|
||||
inputs.some((input) => input.id === node.id),
|
||||
)
|
||||
.map((node, index) => {
|
||||
const input = inputs.find(
|
||||
(input) => input.id === node.id,
|
||||
)!;
|
||||
return (
|
||||
<div
|
||||
className="file-component-accordion-div"
|
||||
key={index}
|
||||
>
|
||||
<AccordionComponent
|
||||
disabled={
|
||||
node.data.node!.template["input_value"]?.value ===
|
||||
""
|
||||
}
|
||||
trigger={
|
||||
<div className="file-component-badge-div">
|
||||
<ShadTooltip
|
||||
content={input.id}
|
||||
styleClasses="z-50"
|
||||
>
|
||||
<div>
|
||||
<Badge variant="gray" size="md">
|
||||
{node.data.node.display_name}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
key={index}
|
||||
keyValue={input.id}
|
||||
>
|
||||
<div className="file-component-tab-column">
|
||||
<div className="">
|
||||
{input && (
|
||||
<IOFieldView
|
||||
type={InputOutput.INPUT}
|
||||
left={true}
|
||||
fieldType={input.type}
|
||||
fieldId={input.id}
|
||||
/>
|
||||
)}
|
||||
</ShadTooltip>
|
||||
<div
|
||||
className="-mb-1 pr-4"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
setSelectedViewField(input);
|
||||
}}
|
||||
>
|
||||
<IconComponent
|
||||
className="h-4 w-4"
|
||||
name="ExternalLink"
|
||||
></IconComponent>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionComponent>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</TabsContent>
|
||||
<TabsContent
|
||||
value={"2"}
|
||||
className="api-modal-tabs-content mt-4"
|
||||
>
|
||||
<div className="mx-2 mb-2 flex items-center gap-2 text-sm font-bold">
|
||||
<IconComponent className="h-4 w-4" name={"Type"} />
|
||||
{OUTPUTS_MODAL_TITLE}
|
||||
</div>
|
||||
{nodes
|
||||
.filter((node) =>
|
||||
outputs.some((output) => output.id === node.id),
|
||||
)
|
||||
.map((node, index) => {
|
||||
const output = outputs.find(
|
||||
(output) => output.id === node.id,
|
||||
)!;
|
||||
return (
|
||||
<div
|
||||
className="file-component-accordion-div"
|
||||
}
|
||||
key={index}
|
||||
keyValue={input.id}
|
||||
>
|
||||
<AccordionComponent
|
||||
disabled={
|
||||
node.data.node!.template["input_value"]
|
||||
?.value === ""
|
||||
}
|
||||
trigger={
|
||||
<div className="file-component-badge-div">
|
||||
<ShadTooltip
|
||||
content={output.id}
|
||||
styleClasses="z-50"
|
||||
>
|
||||
<div>
|
||||
<Badge variant="gray" size="md">
|
||||
{node.data.node.display_name}
|
||||
</Badge>
|
||||
</div>
|
||||
</ShadTooltip>
|
||||
<div
|
||||
className="-mb-1 pr-4"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
setSelectedViewField(output);
|
||||
}}
|
||||
>
|
||||
<IconComponent
|
||||
className="h-4 w-4"
|
||||
name="ExternalLink"
|
||||
></IconComponent>
|
||||
<div className="file-component-tab-column">
|
||||
<div className="">
|
||||
{input && (
|
||||
<IOFieldView
|
||||
type={InputOutput.INPUT}
|
||||
left={true}
|
||||
fieldType={input.type}
|
||||
fieldId={input.id}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</AccordionComponent>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</TabsContent>
|
||||
<TabsContent value={"2"} className="api-modal-tabs-content">
|
||||
{nodes
|
||||
.filter((node) =>
|
||||
outputs.some((output) => output.id === node.id),
|
||||
)
|
||||
.map((node, index) => {
|
||||
const output = outputs.find(
|
||||
(output) => output.id === node.id,
|
||||
)!;
|
||||
return (
|
||||
<div
|
||||
className="file-component-accordion-div"
|
||||
key={index}
|
||||
>
|
||||
<AccordionComponent
|
||||
trigger={
|
||||
<div className="file-component-badge-div">
|
||||
<ShadTooltip
|
||||
content={output.id}
|
||||
styleClasses="z-50"
|
||||
>
|
||||
<div>
|
||||
<Badge variant="gray" size="md">
|
||||
{node.data.node.display_name}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
key={index}
|
||||
keyValue={output.id}
|
||||
>
|
||||
<div className="file-component-tab-column">
|
||||
<div className="">
|
||||
{output && (
|
||||
<IOFieldView
|
||||
type={InputOutput.OUTPUT}
|
||||
left={true}
|
||||
fieldType={output.type}
|
||||
fieldId={output.id}
|
||||
/>
|
||||
)}
|
||||
</ShadTooltip>
|
||||
<div
|
||||
className="-mb-1 pr-4"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
setSelectedViewField(output);
|
||||
}}
|
||||
>
|
||||
<IconComponent
|
||||
className="h-4 w-4"
|
||||
name="ExternalLink"
|
||||
></IconComponent>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionComponent>
|
||||
}
|
||||
key={index}
|
||||
keyValue={output.id}
|
||||
>
|
||||
<div className="file-component-tab-column">
|
||||
<div className="">
|
||||
{output && (
|
||||
<IOFieldView
|
||||
type={InputOutput.OUTPUT}
|
||||
left={true}
|
||||
fieldType={output.type}
|
||||
fieldId={output.id}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</AccordionComponent>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</TabsContent>
|
||||
<TabsContent value={"0"} className="api-modal-tabs-content">
|
||||
{sessions.map((session, index) => {
|
||||
return (
|
||||
<div
|
||||
className="file-component-accordion-div cursor-pointer"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
setSelectedViewField({
|
||||
id: session,
|
||||
type: "Session",
|
||||
});
|
||||
}}
|
||||
>
|
||||
<div className="flex w-full items-center justify-between border-b px-2 py-1 align-middle">
|
||||
<Badge variant="gray" size="md">
|
||||
{session}
|
||||
</Badge>
|
||||
<div className="flex items-center justify-center gap-2 align-middle">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleRemoveSession(session);
|
||||
if (selectedViewField?.id === session)
|
||||
setSelectedViewField(undefined);
|
||||
}}
|
||||
>
|
||||
<ShadTooltip
|
||||
styleClasses="z-50"
|
||||
content={"delete"}
|
||||
>
|
||||
<div>
|
||||
<IconComponent
|
||||
name="Trash2"
|
||||
className="h-4 w-4"
|
||||
></IconComponent>
|
||||
</div>
|
||||
</ShadTooltip>
|
||||
</Button>
|
||||
<div>
|
||||
<ShadTooltip
|
||||
styleClasses="z-50"
|
||||
content={
|
||||
flow_sessions.some(
|
||||
(f_session) =>
|
||||
f_session?.session_id === session,
|
||||
)
|
||||
? "Active Session"
|
||||
: "Inactive Session"
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"h-2 w-2 rounded-full",
|
||||
flow_sessions.some(
|
||||
(f_session) =>
|
||||
f_session?.session_id === session,
|
||||
)
|
||||
? "bg-status-green"
|
||||
: "bg-slate-500",
|
||||
)}
|
||||
></div>
|
||||
</ShadTooltip>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
<div className="flex h-full min-w-96 flex-grow">
|
||||
{selectedViewField && (
|
||||
<div
|
||||
|
|
@ -333,14 +438,17 @@ export default function IOModal({
|
|||
<div className="h-full w-full">
|
||||
{inputs.some(
|
||||
(input) => input.id === selectedViewField.id,
|
||||
) ? (
|
||||
) && (
|
||||
<IOFieldView
|
||||
type={InputOutput.INPUT}
|
||||
left={false}
|
||||
fieldType={selectedViewField.type!}
|
||||
fieldId={selectedViewField.id!}
|
||||
/>
|
||||
) : (
|
||||
)}
|
||||
{outputs.some(
|
||||
(output) => output.id === selectedViewField.id,
|
||||
) && (
|
||||
<IOFieldView
|
||||
type={InputOutput.OUTPUT}
|
||||
left={false}
|
||||
|
|
@ -348,6 +456,16 @@ export default function IOModal({
|
|||
fieldId={selectedViewField.id!}
|
||||
/>
|
||||
)}
|
||||
{sessions.some(
|
||||
(session) => session === selectedViewField.id,
|
||||
) && (
|
||||
<SessionView
|
||||
rows={messages.filter(
|
||||
(message) =>
|
||||
message.session_id === selectedViewField.id,
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
252
src/frontend/src/modals/apiModal/index.tsx
Normal file
252
src/frontend/src/modals/apiModal/index.tsx
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
import "ace-builds/src-noconflict/ext-language_tools";
|
||||
import "ace-builds/src-noconflict/mode-python";
|
||||
import "ace-builds/src-noconflict/theme-github";
|
||||
import "ace-builds/src-noconflict/theme-twilight";
|
||||
import {
|
||||
ReactNode,
|
||||
forwardRef,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
// import "ace-builds/webpack-resolver";
|
||||
import CodeTabsComponent from "../../components/codeTabsComponent";
|
||||
import IconComponent from "../../components/genericIconComponent";
|
||||
import {
|
||||
EXPORT_CODE_DIALOG,
|
||||
LANGFLOW_SUPPORTED_TYPES,
|
||||
} from "../../constants/constants";
|
||||
import { AuthContext } from "../../contexts/authContext";
|
||||
import useFlowStore from "../../stores/flowStore";
|
||||
import { TemplateVariableType } from "../../types/api";
|
||||
import { tweakType, uniqueTweakType } from "../../types/components";
|
||||
import { FlowType, NodeType } from "../../types/flow/index";
|
||||
import { buildTweaks, convertArrayToObj } from "../../utils/reactflowUtils";
|
||||
import {
|
||||
getCurlCode,
|
||||
getPythonApiCode,
|
||||
getPythonCode,
|
||||
getWidgetCode,
|
||||
tabsArray,
|
||||
} from "../../utils/utils";
|
||||
import BaseModal from "../baseModal";
|
||||
|
||||
const ApiModal = forwardRef(
|
||||
(
|
||||
{
|
||||
flow,
|
||||
children,
|
||||
open: myOpen,
|
||||
setOpen: mySetOpen,
|
||||
}: {
|
||||
flow: FlowType;
|
||||
children: ReactNode;
|
||||
open: any;
|
||||
setOpen: any;
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const { autoLogin } = useContext(AuthContext);
|
||||
const [open, setOpen] =
|
||||
mySetOpen !== undefined && myOpen !== undefined
|
||||
? [myOpen, mySetOpen]
|
||||
: useState(false);
|
||||
const [activeTab, setActiveTab] = useState("0");
|
||||
const tweak = useRef<tweakType>([]);
|
||||
const tweaksList = useRef<string[]>([]);
|
||||
const [getTweak, setTweak] = useState<tweakType>([]);
|
||||
const flowState = useFlowStore((state) => state.flowState);
|
||||
const pythonApiCode = getPythonApiCode(flow, autoLogin, tweak.current);
|
||||
const curl_code = getCurlCode(flow, autoLogin, tweak.current);
|
||||
const pythonCode = getPythonCode(flow, tweak.current);
|
||||
const widgetCode = getWidgetCode(flow, autoLogin, flowState);
|
||||
const tweaksCode = buildTweaks(flow);
|
||||
const codesArray = [
|
||||
curl_code,
|
||||
pythonApiCode,
|
||||
pythonCode,
|
||||
widgetCode,
|
||||
pythonCode,
|
||||
];
|
||||
const [tabs, setTabs] = useState(tabsArray(codesArray, 0));
|
||||
|
||||
function startState() {
|
||||
tweak.current = [];
|
||||
setTweak([]);
|
||||
tweaksList.current = [];
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (flow["data"]!["nodes"].length == 0) {
|
||||
startState();
|
||||
} else {
|
||||
tweak.current = [];
|
||||
const t = buildTweaks(flow);
|
||||
tweak.current.push(t);
|
||||
}
|
||||
|
||||
filterNodes();
|
||||
|
||||
if (Object.keys(tweaksCode).length > 0) {
|
||||
setActiveTab("0");
|
||||
setTabs(tabsArray(codesArray, 1));
|
||||
} else {
|
||||
setTabs(tabsArray(codesArray, 1));
|
||||
}
|
||||
}, [flow["data"]!["nodes"], open]);
|
||||
|
||||
function filterNodes() {
|
||||
let arrNodesWithValues: string[] = [];
|
||||
|
||||
flow["data"]!["nodes"].forEach((node) => {
|
||||
if (!node["data"]["node"]["template"]) {
|
||||
return;
|
||||
}
|
||||
Object.keys(node["data"]["node"]["template"])
|
||||
.filter(
|
||||
(templateField) =>
|
||||
templateField.charAt(0) !== "_" &&
|
||||
node.data.node.template[templateField].show &&
|
||||
LANGFLOW_SUPPORTED_TYPES.has(
|
||||
node.data.node.template[templateField].type
|
||||
)
|
||||
)
|
||||
.map((n, i) => {
|
||||
arrNodesWithValues.push(node["id"]);
|
||||
});
|
||||
});
|
||||
|
||||
tweaksList.current = arrNodesWithValues.filter((value, index, self) => {
|
||||
return self.indexOf(value) === index;
|
||||
});
|
||||
}
|
||||
function buildTweakObject(
|
||||
tw: string,
|
||||
changes: string | string[] | boolean | number | Object[] | Object,
|
||||
template: TemplateVariableType
|
||||
) {
|
||||
if (typeof changes === "string" && template.type === "float") {
|
||||
changes = parseFloat(changes);
|
||||
}
|
||||
if (typeof changes === "string" && template.type === "int") {
|
||||
changes = parseInt(changes);
|
||||
}
|
||||
if (template.list === true && Array.isArray(changes)) {
|
||||
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)
|
||||
);
|
||||
|
||||
if (existingTweak) {
|
||||
existingTweak[tw][template["name"]!] = changes as string;
|
||||
|
||||
if (existingTweak[tw][template["name"]!] == template.value) {
|
||||
tweak.current.forEach((element) => {
|
||||
if (element[tw] && Object.keys(element[tw])?.length === 0) {
|
||||
tweak.current = tweak.current.filter((obj) => {
|
||||
const prop = obj[Object.keys(obj)[0]].prop;
|
||||
return prop !== undefined && prop !== null && prop !== "";
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const newTweak = {
|
||||
[tw]: {
|
||||
[template["name"]!]: changes,
|
||||
},
|
||||
} as uniqueTweakType;
|
||||
tweak.current.push(newTweak);
|
||||
}
|
||||
|
||||
const pythonApiCode = getPythonApiCode(flow, autoLogin, tweak.current);
|
||||
const curl_code = getCurlCode(flow, autoLogin, tweak.current);
|
||||
const pythonCode = getPythonCode(flow, tweak.current);
|
||||
const widgetCode = getWidgetCode(flow, autoLogin, flowState);
|
||||
|
||||
tabs![0].code = curl_code;
|
||||
tabs![1].code = pythonApiCode;
|
||||
tabs![2].code = pythonCode;
|
||||
tabs![3].code = widgetCode;
|
||||
|
||||
setTweak(tweak.current);
|
||||
}
|
||||
|
||||
function buildContent(value: string) {
|
||||
const htmlContent = (
|
||||
<div className="w-[200px]">
|
||||
<span>{value != null && value != "" ? value : "None"}</span>
|
||||
</div>
|
||||
);
|
||||
return htmlContent;
|
||||
}
|
||||
|
||||
function getValue(
|
||||
value: string,
|
||||
node: NodeType,
|
||||
template: TemplateVariableType
|
||||
) {
|
||||
let returnValue = value ?? "";
|
||||
|
||||
if (getTweak.length > 0) {
|
||||
for (const obj of getTweak) {
|
||||
Object.keys(obj).forEach((key) => {
|
||||
const value = obj[key];
|
||||
if (key == node["id"]) {
|
||||
Object.keys(value).forEach((key) => {
|
||||
if (key == template["name"]) {
|
||||
returnValue = value[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
return value ?? "";
|
||||
}
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseModal open={open} setOpen={setOpen}>
|
||||
<BaseModal.Trigger asChild>{children}</BaseModal.Trigger>
|
||||
<BaseModal.Header description={EXPORT_CODE_DIALOG}>
|
||||
<span className="pr-2">API</span>
|
||||
<IconComponent
|
||||
name="Code2"
|
||||
className="h-6 w-6 pl-1 text-gray-800 dark:text-white"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</BaseModal.Header>
|
||||
<BaseModal.Content>
|
||||
<CodeTabsComponent
|
||||
flow={flow}
|
||||
tabs={tabs!}
|
||||
activeTab={activeTab}
|
||||
setActiveTab={setActiveTab}
|
||||
tweaks={{
|
||||
tweak,
|
||||
tweaksList,
|
||||
buildContent,
|
||||
buildTweakObject,
|
||||
getValue,
|
||||
}}
|
||||
/>
|
||||
</BaseModal.Content>
|
||||
</BaseModal>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default ApiModal;
|
||||
|
|
@ -9,7 +9,7 @@ export default function getPythonApiCode(
|
|||
flowId: string,
|
||||
isAuth: boolean,
|
||||
tweaksBuildedObject,
|
||||
endpointName?: string
|
||||
endpointName?: string,
|
||||
): string {
|
||||
const tweaksObject = tweaksBuildedObject[0];
|
||||
return `import argparse
|
||||
|
|
|
|||
|
|
@ -31,11 +31,15 @@ const ApiModal = forwardRef(
|
|||
{
|
||||
flow,
|
||||
children,
|
||||
open: myOpen,
|
||||
setOpen: mySetOpen,
|
||||
}: {
|
||||
flow: FlowType;
|
||||
children: ReactNode;
|
||||
open?: boolean;
|
||||
setOpen?: (a: boolean | ((o?: boolean) => boolean)) => void;
|
||||
},
|
||||
ref
|
||||
ref,
|
||||
) => {
|
||||
const tweak = useTweaksStore((state) => state.tweak);
|
||||
const addTweaks = useTweaksStore((state) => state.setTweak);
|
||||
|
|
@ -44,28 +48,30 @@ const ApiModal = forwardRef(
|
|||
|
||||
const [activeTweaks, setActiveTweaks] = useState(false);
|
||||
const { autoLogin } = useContext(AuthContext);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [open, setOpen] =
|
||||
mySetOpen !== undefined && myOpen !== undefined
|
||||
? [myOpen, mySetOpen]
|
||||
: useState(false);
|
||||
const [activeTab, setActiveTab] = useState("0");
|
||||
const pythonApiCode = getPythonApiCode(
|
||||
flow?.id,
|
||||
autoLogin,
|
||||
tweak,
|
||||
flow?.endpoint_name
|
||||
flow?.endpoint_name,
|
||||
);
|
||||
const curl_run_code = getCurlRunCode(
|
||||
flow?.id,
|
||||
autoLogin,
|
||||
tweak,
|
||||
flow?.endpoint_name
|
||||
flow?.endpoint_name,
|
||||
);
|
||||
const curl_webhook_code = getCurlWebhookCode(
|
||||
flow?.id,
|
||||
autoLogin,
|
||||
flow?.endpoint_name
|
||||
flow?.endpoint_name,
|
||||
);
|
||||
const pythonCode = getPythonCode(flow?.name, tweak);
|
||||
const widgetCode = getWidgetCode(flow?.id, flow?.name, autoLogin);
|
||||
console.log("flow", flow);
|
||||
const includeWebhook = flow.webhook;
|
||||
const tweaksCode = buildTweaks(flow);
|
||||
const codesArray = [
|
||||
|
|
@ -77,7 +83,7 @@ const ApiModal = forwardRef(
|
|||
pythonCode,
|
||||
];
|
||||
const [tabs, setTabs] = useState(
|
||||
createTabsArray(codesArray, includeWebhook)
|
||||
createTabsArray(codesArray, includeWebhook),
|
||||
);
|
||||
|
||||
const canShowTweaks =
|
||||
|
|
@ -126,7 +132,7 @@ const ApiModal = forwardRef(
|
|||
buildTweakObject(
|
||||
nodeId,
|
||||
element.data.node.template[templateField].value,
|
||||
element.data.node.template[templateField]
|
||||
element.data.node.template[templateField],
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
@ -143,7 +149,7 @@ const ApiModal = forwardRef(
|
|||
async function buildTweakObject(
|
||||
tw: string,
|
||||
changes: string | string[] | boolean | number | Object[] | Object,
|
||||
template: TemplateVariableType
|
||||
template: TemplateVariableType,
|
||||
) {
|
||||
changes = getChangesType(changes, template);
|
||||
|
||||
|
|
@ -185,7 +191,7 @@ const ApiModal = forwardRef(
|
|||
flow?.id,
|
||||
autoLogin,
|
||||
cloneTweak,
|
||||
flow?.endpoint_name
|
||||
flow?.endpoint_name,
|
||||
);
|
||||
const pythonCode = getPythonCode(flow?.name, cloneTweak);
|
||||
const widgetCode = getWidgetCode(flow?.id, flow?.name, autoLogin);
|
||||
|
|
@ -229,7 +235,7 @@ const ApiModal = forwardRef(
|
|||
</BaseModal.Content>
|
||||
</BaseModal>
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export default ApiModal;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ export const switchCaseModalSize = (size: string) => {
|
|||
switch (size) {
|
||||
case "x-small":
|
||||
minWidth = "min-w-[20vw]";
|
||||
height = "h-full";
|
||||
height = "";
|
||||
break;
|
||||
case "smaller":
|
||||
minWidth = "min-w-[40vw]";
|
||||
|
|
@ -12,7 +12,7 @@ export const switchCaseModalSize = (size: string) => {
|
|||
break;
|
||||
case "smaller-h-full":
|
||||
minWidth = "min-w-[40vw]";
|
||||
height = "h-full";
|
||||
height = "";
|
||||
break;
|
||||
case "small":
|
||||
minWidth = "min-w-[40vw]";
|
||||
|
|
@ -20,16 +20,19 @@ export const switchCaseModalSize = (size: string) => {
|
|||
break;
|
||||
case "small-h-full":
|
||||
minWidth = "min-w-[40vw]";
|
||||
height = "h-full";
|
||||
height = "";
|
||||
break;
|
||||
case "medium":
|
||||
minWidth = "min-w-[60vw]";
|
||||
height = "h-[60vh]";
|
||||
break;
|
||||
case "medium-tall":
|
||||
minWidth = "min-w-[60vw]";
|
||||
height = "h-[90vh]";
|
||||
break;
|
||||
case "medium-h-full":
|
||||
minWidth = "min-w-[60vw]";
|
||||
height = "h-full";
|
||||
|
||||
height = "";
|
||||
break;
|
||||
case "large":
|
||||
minWidth = "min-w-[85vw]";
|
||||
|
|
@ -41,26 +44,26 @@ export const switchCaseModalSize = (size: string) => {
|
|||
break;
|
||||
case "large-thin":
|
||||
minWidth = "min-w-[65vw]";
|
||||
height = "h-[80vh]";
|
||||
height = "h-[90vh]";
|
||||
break;
|
||||
|
||||
case "md-thin":
|
||||
minWidth = "min-w-[85vw]";
|
||||
height = "h-[70vh]";
|
||||
height = "h-[90vh]";
|
||||
break;
|
||||
|
||||
case "sm-thin":
|
||||
minWidth = "min-w-[65vw]";
|
||||
height = "h-[70vh]";
|
||||
height = "h-[90vh]";
|
||||
break;
|
||||
|
||||
case "large-h-full":
|
||||
minWidth = "min-w-[80vw]";
|
||||
height = "h-full";
|
||||
height = "";
|
||||
break;
|
||||
default:
|
||||
minWidth = "min-w-[80vw]";
|
||||
height = "h-[80vh]";
|
||||
height = "h-[90vh]";
|
||||
break;
|
||||
}
|
||||
return { minWidth, height };
|
||||
|
|
|
|||
|
|
@ -16,11 +16,11 @@ import {
|
|||
} from "../../components/ui/dialog-with-no-close";
|
||||
|
||||
import { DialogClose } from "@radix-ui/react-dialog";
|
||||
import * as Form from "@radix-ui/react-form";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { modalHeaderType } from "../../types/components";
|
||||
import { cn } from "../../utils/utils";
|
||||
import { switchCaseModalSize } from "./helpers/switch-case-size";
|
||||
import * as Form from "@radix-ui/react-form";
|
||||
|
||||
type ContentProps = { children: ReactNode };
|
||||
type HeaderProps = { children: ReactNode; description: string };
|
||||
|
|
@ -112,6 +112,7 @@ interface BaseModalProps {
|
|||
| "smaller"
|
||||
| "small"
|
||||
| "medium"
|
||||
| "medium-tall"
|
||||
| "large"
|
||||
| "three-cards"
|
||||
| "large-thin"
|
||||
|
|
@ -120,7 +121,8 @@ interface BaseModalProps {
|
|||
| "medium-h-full"
|
||||
| "md-thin"
|
||||
| "sm-thin"
|
||||
| "smaller-h-full";
|
||||
| "smaller-h-full"
|
||||
| "medium-log";
|
||||
|
||||
disable?: boolean;
|
||||
onChangeOpenModal?: (open?: boolean) => void;
|
||||
|
|
@ -163,25 +165,31 @@ function BaseModal({
|
|||
{type === "modal" ? (
|
||||
<Modal open={open} onOpenChange={setOpen}>
|
||||
{triggerChild}
|
||||
<ModalContent className={cn(minWidth, "duration-300")}>
|
||||
<div className="truncate-doubleline word-break-break-word">
|
||||
<ModalContent
|
||||
className={cn(minWidth, height, "flex flex-col duration-300")}
|
||||
>
|
||||
<div className="flex-shrink-0 truncate-doubleline word-break-break-word">
|
||||
{headerChild}
|
||||
</div>
|
||||
<div
|
||||
className={`flex flex-col ${height} w-full transition-all duration-300`}
|
||||
className={`flex w-full flex-1 flex-col transition-all duration-300`}
|
||||
>
|
||||
{ContentChild}
|
||||
</div>
|
||||
{ContentFooter && (
|
||||
<div className="flex flex-row-reverse">{ContentFooter}</div>
|
||||
<div className="flex flex-shrink-0 flex-row-reverse">
|
||||
{ContentFooter}
|
||||
</div>
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
) : (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
{triggerChild}
|
||||
<DialogContent className={cn(minWidth, "duration-300")}>
|
||||
<div className="truncate-doubleline word-break-break-word">
|
||||
<DialogContent
|
||||
className={cn(minWidth, height, "flex flex-col duration-300")}
|
||||
>
|
||||
<div className="flex-shrink-0 truncate-doubleline word-break-break-word">
|
||||
{headerChild}
|
||||
</div>
|
||||
{onSubmit ? (
|
||||
|
|
@ -190,26 +198,30 @@ function BaseModal({
|
|||
event.preventDefault();
|
||||
onSubmit();
|
||||
}}
|
||||
className="flex flex-col gap-6"
|
||||
className="flex min-h-0 flex-1 flex-col gap-6"
|
||||
>
|
||||
<div
|
||||
className={`flex flex-col ${height} w-full transition-all duration-300`}
|
||||
className={`flex w-full flex-1 flex-col overflow-hidden transition-all duration-300`}
|
||||
>
|
||||
{ContentChild}
|
||||
</div>
|
||||
{ContentFooter && (
|
||||
<div className="flex flex-row-reverse">{ContentFooter}</div>
|
||||
<div className="flex flex-shrink-0 flex-row-reverse">
|
||||
{ContentFooter}
|
||||
</div>
|
||||
)}
|
||||
</Form.Root>
|
||||
) : (
|
||||
<>
|
||||
<div
|
||||
className={`flex flex-col ${height} w-full transition-all duration-300`}
|
||||
className={`flex min-h-0 w-full flex-1 flex-col transition-all duration-300`}
|
||||
>
|
||||
{ContentChild}
|
||||
</div>
|
||||
{ContentFooter && (
|
||||
<div className="flex flex-row-reverse">{ContentFooter}</div>
|
||||
<div className="flex flex-shrink-0 flex-row-reverse">
|
||||
{ContentFooter}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,87 @@
|
|||
import { ColDef, ValueGetterParams } from "ag-grid-community";
|
||||
import { useMemo } from "react";
|
||||
import TableNodeCellRender from "../../../components/tableComponent/components/tableNodeCellRender";
|
||||
import TableToggleCellRender from "../../../components/tableComponent/components/tableToggleCellRender";
|
||||
import TableTooltipRender from "../../../components/tableComponent/components/tableTooltipRender";
|
||||
|
||||
const useColumnDefs = (
|
||||
myData: any,
|
||||
handleOnNewValue: (newValue: any, name: string) => void,
|
||||
changeAdvanced: (n: string) => void,
|
||||
open: boolean,
|
||||
) => {
|
||||
const columnDefs: ColDef[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
headerName: "Name",
|
||||
field: "display_name",
|
||||
valueGetter: (params) => {
|
||||
const templateParam = params.data;
|
||||
return (
|
||||
(templateParam.display_name
|
||||
? templateParam.display_name
|
||||
: templateParam.name) ?? params.data.key
|
||||
);
|
||||
},
|
||||
wrapText: true,
|
||||
autoHeight: true,
|
||||
flex: 1,
|
||||
resizable: false,
|
||||
cellClass: "no-border",
|
||||
},
|
||||
{
|
||||
headerName: "Description",
|
||||
field: "info",
|
||||
tooltipField: "info",
|
||||
wrapText: true,
|
||||
autoHeight: true,
|
||||
flex: 2,
|
||||
resizable: false,
|
||||
cellClass: "no-border",
|
||||
},
|
||||
{
|
||||
headerName: "Value",
|
||||
field: "value",
|
||||
cellRenderer: TableNodeCellRender,
|
||||
valueGetter: (params: ValueGetterParams) => {
|
||||
return {
|
||||
value: params.data.value,
|
||||
nodeClass: myData.node,
|
||||
handleOnNewValue: handleOnNewValue,
|
||||
handleOnChangeDb: (value, key) => {
|
||||
myData.node!.template[key].load_from_db = value;
|
||||
},
|
||||
};
|
||||
},
|
||||
minWidth: 330,
|
||||
autoHeight: true,
|
||||
flex: 1,
|
||||
resizable: false,
|
||||
cellClass: "no-border",
|
||||
},
|
||||
{
|
||||
headerName: "Show",
|
||||
field: "advanced",
|
||||
cellRenderer: TableToggleCellRender,
|
||||
valueGetter: (params: ValueGetterParams) => {
|
||||
return {
|
||||
name: params.data.name,
|
||||
enabled: !params.data.advanced,
|
||||
setEnabled: () => {
|
||||
changeAdvanced(params.data.key);
|
||||
},
|
||||
};
|
||||
},
|
||||
editable: false,
|
||||
maxWidth: 80,
|
||||
resizable: false,
|
||||
cellClass: "no-border",
|
||||
},
|
||||
],
|
||||
[open, myData],
|
||||
);
|
||||
|
||||
return columnDefs;
|
||||
};
|
||||
|
||||
export default useColumnDefs;
|
||||
37
src/frontend/src/modals/editNodeModal/hooks/use-row-data.tsx
Normal file
37
src/frontend/src/modals/editNodeModal/hooks/use-row-data.tsx
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { useMemo } from "react";
|
||||
import { LANGFLOW_SUPPORTED_TYPES } from "../../../constants/constants";
|
||||
import { TemplateVariableType } from "../../../types/api";
|
||||
|
||||
const useRowData = (myData, open) => {
|
||||
const rowData = useMemo(() => {
|
||||
return Object.keys(myData.node!.template)
|
||||
.filter((key: string) => {
|
||||
const templateParam = myData.node!.template[
|
||||
key
|
||||
] as TemplateVariableType;
|
||||
return (
|
||||
key.charAt(0) !== "_" &&
|
||||
templateParam.show &&
|
||||
LANGFLOW_SUPPORTED_TYPES.has(templateParam.type) &&
|
||||
!(
|
||||
(key === "code" && templateParam.type === "code") ||
|
||||
(key.includes("code") && templateParam.proxy)
|
||||
)
|
||||
);
|
||||
})
|
||||
.map((key: string) => {
|
||||
const templateParam = myData.node!.template[
|
||||
key
|
||||
] as TemplateVariableType;
|
||||
return {
|
||||
...templateParam,
|
||||
key: key,
|
||||
id: key,
|
||||
};
|
||||
});
|
||||
}, [open, myData]);
|
||||
|
||||
return rowData;
|
||||
};
|
||||
|
||||
export default useRowData;
|
||||
|
|
@ -1,21 +1,13 @@
|
|||
import { cloneDeep } from "lodash";
|
||||
import { ColDef, GridApi } from "ag-grid-community";
|
||||
import { forwardRef, useEffect, useRef, useState } from "react";
|
||||
import IconComponent from "../../components/genericIconComponent";
|
||||
import TableComponent from "../../components/tableComponent";
|
||||
import { Badge } from "../../components/ui/badge";
|
||||
import {
|
||||
LANGFLOW_SUPPORTED_TYPES,
|
||||
limitScrollFieldsModal,
|
||||
} from "../../constants/constants";
|
||||
import useFlowStore from "../../stores/flowStore";
|
||||
import { NodeDataType } from "../../types/flow";
|
||||
import { classNames } from "../../utils/utils";
|
||||
import BaseModal from "../baseModal";
|
||||
import TableComponent from "../../components/tableComponent";
|
||||
import TableAutoCellRender from "../../components/tableAutoCellRender";
|
||||
import { TemplateVariableType } from "../../types/api";
|
||||
import TableNodeCellRender from "../../components/tableNodeCellRender";
|
||||
import { ColDef, ValueGetterParams } from "ag-grid-community";
|
||||
import TableTooltipRender from "../../components/tableTooltipRender";
|
||||
import useColumnDefs from "./hooks/use-column-defs";
|
||||
import useRowData from "./hooks/use-row-data";
|
||||
|
||||
const EditNodeModal = forwardRef(
|
||||
(
|
||||
|
|
@ -23,135 +15,60 @@ const EditNodeModal = forwardRef(
|
|||
nodeLength,
|
||||
open,
|
||||
setOpen,
|
||||
// setOpenWDoubleClick,
|
||||
data,
|
||||
}: {
|
||||
nodeLength: number;
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
// setOpenWDoubleClick: (open: boolean) => void;
|
||||
data: NodeDataType;
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const nodes = useFlowStore((state) => state.nodes);
|
||||
|
||||
const dataFromStore = nodes.find((node) => node.id === node.id)?.data;
|
||||
|
||||
const myData = useRef(dataFromStore ?? data);
|
||||
const myData = useRef(data);
|
||||
|
||||
const setNode = useFlowStore((state) => state.setNode);
|
||||
|
||||
function changeAdvanced(n) {
|
||||
myData.current.node!.template[n].advanced =
|
||||
!myData.current.node!.template[n].advanced;
|
||||
!myData.current.node!.template[n]?.advanced;
|
||||
}
|
||||
|
||||
const handleOnNewValue = (newValue: any, name) => {
|
||||
myData.current.node!.template[name].value = newValue;
|
||||
};
|
||||
|
||||
const rowData = useRowData(data, open);
|
||||
|
||||
const columnDefs: ColDef[] = useColumnDefs(
|
||||
data,
|
||||
handleOnNewValue,
|
||||
changeAdvanced,
|
||||
open,
|
||||
);
|
||||
|
||||
const [gridApi, setGridApi] = useState<GridApi | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
myData.current = data; // reset data to what it is on node when opening modal
|
||||
if (gridApi && open) {
|
||||
myData.current = data;
|
||||
gridApi.refreshCells();
|
||||
}
|
||||
}, [open]);
|
||||
}, [gridApi, open]);
|
||||
|
||||
const rowData = Object.keys(myData.current.node!.template)
|
||||
.filter((key: string) => {
|
||||
const templateParam = myData.current.node!.template[
|
||||
key
|
||||
] as TemplateVariableType;
|
||||
return (
|
||||
key.charAt(0) !== "_" &&
|
||||
templateParam.show &&
|
||||
LANGFLOW_SUPPORTED_TYPES.has(templateParam.type) &&
|
||||
!(
|
||||
(key === "code" && templateParam.type === "code") ||
|
||||
(key.includes("code") && templateParam.proxy)
|
||||
)
|
||||
);
|
||||
})
|
||||
.map((key: string) => {
|
||||
const templateParam = myData.current.node!.template[
|
||||
key
|
||||
] as TemplateVariableType;
|
||||
return {
|
||||
...templateParam,
|
||||
key: key,
|
||||
};
|
||||
});
|
||||
|
||||
const columnDefs: ColDef[] = [
|
||||
{
|
||||
headerName: "Name",
|
||||
field: "display_name",
|
||||
valueGetter: (params) => {
|
||||
const templateParam = params.data;
|
||||
return (
|
||||
(templateParam.display_name
|
||||
? templateParam.display_name
|
||||
: templateParam.name) ?? params.data.key
|
||||
);
|
||||
},
|
||||
cellRenderer: TableAutoCellRender,
|
||||
flex: 1,
|
||||
resizable: false,
|
||||
},
|
||||
{
|
||||
headerName: "Description",
|
||||
field: "info",
|
||||
tooltipField: "info",
|
||||
tooltipComponent: TableTooltipRender,
|
||||
cellRenderer: TableAutoCellRender,
|
||||
autoHeight: true,
|
||||
flex: 2,
|
||||
resizable: false,
|
||||
},
|
||||
{
|
||||
headerName: "Value",
|
||||
field: "value",
|
||||
cellRenderer: TableNodeCellRender,
|
||||
valueGetter: (params: ValueGetterParams) => {
|
||||
return {
|
||||
value: params.data.value,
|
||||
nodeClass: myData.current.node,
|
||||
handleOnNewValue: handleOnNewValue,
|
||||
handleOnChangeDb: (value, key) => {
|
||||
myData.current.node!.template[key].load_from_db = value;
|
||||
},
|
||||
};
|
||||
},
|
||||
minWidth: 330,
|
||||
flex: 1,
|
||||
resizable: false,
|
||||
},
|
||||
{
|
||||
headerName: "Show",
|
||||
field: "advanced",
|
||||
cellRenderer: "agCheckboxCellRenderer",
|
||||
cellEditor: "agCheckboxCellEditor",
|
||||
valueGetter: (params) => {
|
||||
return !params.data.advanced;
|
||||
},
|
||||
valueSetter: (params) => {
|
||||
changeAdvanced(params.data.key);
|
||||
return true;
|
||||
},
|
||||
editable: true,
|
||||
flex: 1,
|
||||
maxWidth: 70,
|
||||
resizable: false,
|
||||
},
|
||||
];
|
||||
// useEffect(() => {
|
||||
// return () => {
|
||||
// setOpenWDoubleClick(false);
|
||||
// };
|
||||
// }, []);
|
||||
|
||||
return (
|
||||
<BaseModal
|
||||
key={data.id}
|
||||
size="large-h-full"
|
||||
size="medium-tall"
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
onChangeOpenModal={(open) => {
|
||||
myData.current = data;
|
||||
}}
|
||||
onSubmit={() => {
|
||||
setNode(data.id, (old) => ({
|
||||
...old,
|
||||
|
|
@ -166,32 +83,33 @@ const EditNodeModal = forwardRef(
|
|||
<BaseModal.Trigger>
|
||||
<></>
|
||||
</BaseModal.Trigger>
|
||||
<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 description={data.node?.description!}>
|
||||
<span className="pr-2">{data.type}</span>
|
||||
<Badge variant="secondary">ID: {data.id}</Badge>
|
||||
</BaseModal.Header>
|
||||
<BaseModal.Content>
|
||||
<div className="flex pb-2">
|
||||
<IconComponent
|
||||
name="Variable"
|
||||
className="edit-node-modal-variable "
|
||||
/>
|
||||
<span className="edit-node-modal-span">Parameters</span>
|
||||
</div>
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="flex pb-2">
|
||||
<IconComponent
|
||||
name="Variable"
|
||||
className="edit-node-modal-variable "
|
||||
/>
|
||||
<span className="edit-node-modal-span">Parameters</span>
|
||||
</div>
|
||||
|
||||
<div className="w-full">
|
||||
{nodeLength > 0 && (
|
||||
<div className="edit-node-modal-table">
|
||||
<div className="h-96">
|
||||
<TableComponent
|
||||
tooltipShowMode="whenTruncated"
|
||||
tooltipShowDelay={0.5}
|
||||
columnDefs={columnDefs}
|
||||
rowData={rowData}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="h-full">
|
||||
{nodeLength > 0 && (
|
||||
<TableComponent
|
||||
key={"editNode"}
|
||||
onGridReady={(params) => {
|
||||
setGridApi(params.api);
|
||||
}}
|
||||
tooltipShowDelay={0.5}
|
||||
columnDefs={columnDefs}
|
||||
rowData={rowData}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</BaseModal.Content>
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue