Merge branch 'zustand/io/migration' into globalVariables
4
src/frontend/.gitignore
vendored
|
|
@ -24,3 +24,7 @@ yarn-error.log*
|
|||
/test-results/
|
||||
/playwright-report/*/
|
||||
/playwright/.cache/
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
|
|
|
|||
1142
src/frontend/package-lock.json
generated
|
|
@ -39,9 +39,11 @@
|
|||
"clsx": "^1.2.1",
|
||||
"dompurify": "^3.0.5",
|
||||
"esbuild": "^0.17.19",
|
||||
"framer-motion": "^11.0.6",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.233.0",
|
||||
"lucide-react": "^0.331.0",
|
||||
"moment": "^2.29.4",
|
||||
"playwright": "^1.42.0",
|
||||
"react": "^18.2.0",
|
||||
"react-ace": "^10.1.0",
|
||||
"react-cookie": "^4.1.1",
|
||||
|
|
@ -98,7 +100,7 @@
|
|||
},
|
||||
"proxy": "http://127.0.0.1:7860",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.38.0",
|
||||
"@playwright/test": "^1.42.0",
|
||||
"@swc/cli": "^0.1.62",
|
||||
"@swc/core": "^1.3.80",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
|
|
@ -120,7 +122,9 @@
|
|||
"prettier-plugin-tailwindcss": "^0.3.0",
|
||||
"pretty-quick": "^3.1.3",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"tailwindcss-dotted-background": "^1.1.0",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^4.5.1"
|
||||
"ua-parser-js": "^1.0.37",
|
||||
"vite": "^4.5.2"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,9 +10,6 @@
|
|||
<li>
|
||||
<a href="./e2e/index.html">e2e report</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="./onlyFront/index.html">frontEnd Only report</a>
|
||||
</li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -12,13 +12,13 @@ import { defineConfig, devices } from "@playwright/test";
|
|||
export default defineConfig({
|
||||
testDir: "./tests",
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
fullyParallel: false,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
workers: process.env.CI ? 2 : undefined,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: [
|
||||
["html", { open: "never", outputFolder: "playwright-report/test-results" }],
|
||||
|
|
@ -44,10 +44,10 @@ export default defineConfig({
|
|||
use: { ...devices["Desktop Firefox"] },
|
||||
},
|
||||
|
||||
{
|
||||
name: "webkit",
|
||||
use: { ...devices["Desktop Safari"] },
|
||||
},
|
||||
// {
|
||||
// name: "webkit",
|
||||
// use: { ...devices["Desktop Safari"] },
|
||||
// },
|
||||
|
||||
/* Test against mobile viewports. */
|
||||
// {
|
||||
|
|
|
|||
|
|
@ -27,8 +27,16 @@ terminate_process_by_port() {
|
|||
echo "Process terminated."
|
||||
}
|
||||
|
||||
delete_temp() {
|
||||
cd ../../
|
||||
echo "Deleting temp database"
|
||||
rm temp
|
||||
echo "Temp database deleted."
|
||||
}
|
||||
|
||||
|
||||
# Trap signals to ensure cleanup on script termination
|
||||
trap 'terminate_process_by_port 7860; terminate_process_by_port 3000' EXIT
|
||||
trap 'terminate_process_by_port 7860; terminate_process_by_port 3000; delete_temp' EXIT
|
||||
|
||||
# install playwright if there is not installed yet
|
||||
npx playwright install
|
||||
|
|
@ -42,26 +50,16 @@ make frontend &
|
|||
# Give some time for the frontend to start (adjust sleep duration as needed)
|
||||
sleep 10
|
||||
|
||||
# Navigate to the test directory
|
||||
cd src/frontend
|
||||
|
||||
# Run frontend only Playwright tests with or without UI based on the --ui flag
|
||||
if [ "$ui" = true ]; then
|
||||
PLAYWRIGHT_HTML_REPORT=playwright-report/onlyFront npx playwright test tests/onlyFront --ui --project=chromium
|
||||
else
|
||||
PLAYWRIGHT_HTML_REPORT=playwright-report/onlyFront npx playwright test tests/onlyFront --project=chromium
|
||||
fi
|
||||
|
||||
# Navigate back to the project root directory
|
||||
cd ../../
|
||||
#install backend
|
||||
poetry install --extras deploy
|
||||
|
||||
# Start the backend using 'make backend' in the background
|
||||
make backend &
|
||||
LANGFLOW_DATABASE_URL=sqlite:///./temp LANGFLOW_AUTO_LOGIN=True poetry run langflow run --backend-only --port 7860 --host 0.0.0.0 --no-open-browser --env-file .env &
|
||||
|
||||
# Give some time for the backend to start (adjust sleep duration as needed)
|
||||
sleep 25
|
||||
|
||||
# Navigate back to the test directory
|
||||
# Navigate to the test directory
|
||||
cd src/frontend
|
||||
|
||||
# Run Playwright tests with or without UI based on the --ui flag
|
||||
|
|
|
|||
|
|
@ -90,3 +90,12 @@ body {
|
|||
.jv-indent::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #bbb !important;
|
||||
}
|
||||
|
||||
.custom-hover {
|
||||
transition: background-color 0.5s ease;
|
||||
}
|
||||
|
||||
.custom-hover:hover {
|
||||
background-color: rgba(99, 102, 241, 0.1); /* Medium indigo color with 20% opacity */
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import _ from "lodash";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import "reactflow/dist/style.css";
|
||||
import "./App.css";
|
||||
|
|
@ -21,113 +20,33 @@ import useAlertStore from "./stores/alertStore";
|
|||
import { useDarkStore } from "./stores/darkStore";
|
||||
import useFlowsManagerStore from "./stores/flowsManagerStore";
|
||||
import { useGlobalVariablesStore } from "./stores/globalVariables";
|
||||
import { useStoreStore } from "./stores/storeStore";
|
||||
import { useTypesStore } from "./stores/typesStore";
|
||||
|
||||
export default function App() {
|
||||
const errorData = useAlertStore((state) => state.errorData);
|
||||
const errorOpen = useAlertStore((state) => state.errorOpen);
|
||||
const setErrorOpen = useAlertStore((state) => state.setErrorOpen);
|
||||
const noticeData = useAlertStore((state) => state.noticeData);
|
||||
const noticeOpen = useAlertStore((state) => state.noticeOpen);
|
||||
const setNoticeOpen = useAlertStore((state) => state.setNoticeOpen);
|
||||
const successData = useAlertStore((state) => state.successData);
|
||||
const successOpen = useAlertStore((state) => state.successOpen);
|
||||
const setSuccessOpen = useAlertStore((state) => state.setSuccessOpen);
|
||||
const loading = useAlertStore((state) => state.loading);
|
||||
const removeFromTempNotificationList = useAlertStore(
|
||||
(state) => state.removeFromTempNotificationList
|
||||
);
|
||||
const tempNotificationList = useAlertStore(
|
||||
(state) => state.tempNotificationList
|
||||
);
|
||||
const [fetchError, setFetchError] = useState(false);
|
||||
|
||||
// Initialize state variable for the list of alerts
|
||||
const [alertsList, setAlertsList] = useState<
|
||||
Array<{
|
||||
type: string;
|
||||
data: { title: string; list?: Array<string>; link?: string };
|
||||
id: string;
|
||||
}>
|
||||
>([]);
|
||||
|
||||
// Use effect hook to update alertsList when a new alert is added
|
||||
useEffect(() => {
|
||||
// If there is an error alert open with data, add it to the alertsList
|
||||
if (errorOpen && errorData) {
|
||||
if (
|
||||
alertsList.length > 0 &&
|
||||
JSON.stringify(alertsList[alertsList.length - 1].data) ===
|
||||
JSON.stringify(errorData)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setErrorOpen(false);
|
||||
setAlertsList((old) => {
|
||||
let newAlertsList = [
|
||||
...old,
|
||||
{ type: "error", data: _.cloneDeep(errorData), id: _.uniqueId() },
|
||||
];
|
||||
return newAlertsList;
|
||||
});
|
||||
}
|
||||
// If there is a notice alert open with data, add it to the alertsList
|
||||
else if (noticeOpen && noticeData) {
|
||||
if (
|
||||
alertsList.length > 0 &&
|
||||
JSON.stringify(alertsList[alertsList.length - 1].data) ===
|
||||
JSON.stringify(noticeData)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setNoticeOpen(false);
|
||||
setAlertsList((old) => {
|
||||
let newAlertsList = [
|
||||
...old,
|
||||
{ type: "notice", data: _.cloneDeep(noticeData), id: _.uniqueId() },
|
||||
];
|
||||
return newAlertsList;
|
||||
});
|
||||
}
|
||||
// If there is a success alert open with data, add it to the alertsList
|
||||
else if (successOpen && successData) {
|
||||
if (
|
||||
alertsList.length > 0 &&
|
||||
JSON.stringify(alertsList[alertsList.length - 1].data) ===
|
||||
JSON.stringify(successData)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setSuccessOpen(false);
|
||||
setAlertsList((old) => {
|
||||
let newAlertsList = [
|
||||
...old,
|
||||
{ type: "success", data: _.cloneDeep(successData), id: _.uniqueId() },
|
||||
];
|
||||
return newAlertsList;
|
||||
});
|
||||
}
|
||||
}, [
|
||||
_,
|
||||
errorData,
|
||||
errorOpen,
|
||||
noticeData,
|
||||
noticeOpen,
|
||||
setErrorOpen,
|
||||
setNoticeOpen,
|
||||
setSuccessOpen,
|
||||
successData,
|
||||
successOpen,
|
||||
]);
|
||||
const isLoading = useFlowsManagerStore((state) => state.isLoading);
|
||||
|
||||
const removeAlert = (id: string) => {
|
||||
setAlertsList((prevAlertsList) =>
|
||||
prevAlertsList.filter((alert) => alert.id !== id)
|
||||
);
|
||||
removeFromTempNotificationList(id);
|
||||
};
|
||||
|
||||
const { isAuthenticated } = useContext(AuthContext);
|
||||
const refreshFlows = useFlowsManagerStore((state) => state.refreshFlows);
|
||||
const fetchApiData = useStoreStore((state) => state.fetchApiData);
|
||||
const getTypes = useTypesStore((state) => state.getTypes);
|
||||
const refreshVersion = useDarkStore((state) => state.refreshVersion);
|
||||
const refreshStars = useDarkStore((state) => state.refreshStars);
|
||||
const setGlobalVariables = useGlobalVariablesStore(
|
||||
(state) => state.setGlobalVariables
|
||||
);
|
||||
const checkHasStore = useStoreStore((state) => state.checkHasStore);
|
||||
|
||||
useEffect(() => {
|
||||
refreshStars();
|
||||
|
|
@ -142,6 +61,8 @@ export default function App() {
|
|||
getGlobalVariables().then((res) => {
|
||||
setGlobalVariables(res);
|
||||
});
|
||||
checkHasStore();
|
||||
fetchApiData();
|
||||
}
|
||||
}, [isAuthenticated]);
|
||||
|
||||
|
|
@ -155,7 +76,7 @@ export default function App() {
|
|||
.catch(() => {
|
||||
setFetchError(true);
|
||||
});
|
||||
}, 20000);
|
||||
}, 20000); // 20 seconds
|
||||
|
||||
// Clean up the timer on component unmount
|
||||
return () => {
|
||||
|
|
@ -177,7 +98,7 @@ export default function App() {
|
|||
description={FETCH_ERROR_DESCRIPION}
|
||||
message={FETCH_ERROR_MESSAGE}
|
||||
></FetchErrorComponent>
|
||||
) : loading ? (
|
||||
) : isLoading ? (
|
||||
<div className="loading-page-panel">
|
||||
<LoadingComponent remSize={50} />
|
||||
</div>
|
||||
|
|
@ -188,35 +109,46 @@ export default function App() {
|
|||
)}
|
||||
</ErrorBoundary>
|
||||
<div></div>
|
||||
<div className="app-div" style={{ zIndex: 999 }}>
|
||||
{alertsList.map((alert) => (
|
||||
<div key={alert.id}>
|
||||
{alert.type === "error" ? (
|
||||
<ErrorAlert
|
||||
key={alert.id}
|
||||
title={alert.data.title}
|
||||
list={alert.data.list}
|
||||
id={alert.id}
|
||||
removeAlert={removeAlert}
|
||||
/>
|
||||
) : alert.type === "notice" ? (
|
||||
<NoticeAlert
|
||||
key={alert.id}
|
||||
title={alert.data.title}
|
||||
link={alert.data.link}
|
||||
id={alert.id}
|
||||
removeAlert={removeAlert}
|
||||
/>
|
||||
) : (
|
||||
<SuccessAlert
|
||||
key={alert.id}
|
||||
title={alert.data.title}
|
||||
id={alert.id}
|
||||
removeAlert={removeAlert}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<div className="app-div">
|
||||
<div className="flex flex-col-reverse" style={{ zIndex: 999 }}>
|
||||
{tempNotificationList.map((alert) => (
|
||||
<div key={alert.id}>
|
||||
{alert.type === "error" && (
|
||||
<ErrorAlert
|
||||
key={alert.id}
|
||||
title={alert.title}
|
||||
list={alert.list}
|
||||
id={alert.id}
|
||||
removeAlert={removeAlert}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="z-40 flex flex-col-reverse">
|
||||
{tempNotificationList.map((alert) => (
|
||||
<div key={alert.id}>
|
||||
{alert.type === "notice" ? (
|
||||
<NoticeAlert
|
||||
key={alert.id}
|
||||
title={alert.title}
|
||||
link={alert.link}
|
||||
id={alert.id}
|
||||
removeAlert={removeAlert}
|
||||
/>
|
||||
) : (
|
||||
alert.type === "success" && (
|
||||
<SuccessAlert
|
||||
key={alert.id}
|
||||
title={alert.title}
|
||||
id={alert.id}
|
||||
removeAlert={removeAlert}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -16,19 +16,24 @@ import PromptAreaComponent from "../../../../components/promptComponent";
|
|||
import TextAreaComponent from "../../../../components/textAreaComponent";
|
||||
import ToggleShadComponent from "../../../../components/toggleShadComponent";
|
||||
import { Button } from "../../../../components/ui/button";
|
||||
import { RefreshButton } from "../../../../components/ui/refreshButton";
|
||||
import {
|
||||
INPUT_HANDLER_HOVER,
|
||||
LANGFLOW_SUPPORTED_TYPES,
|
||||
OUTPUT_HANDLER_HOVER,
|
||||
TOOLTIP_EMPTY,
|
||||
} from "../../../../constants/constants";
|
||||
import { postCustomComponentUpdate } from "../../../../controllers/API";
|
||||
import useAlertStore from "../../../../stores/alertStore";
|
||||
import useFlowStore from "../../../../stores/flowStore";
|
||||
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
|
||||
import { useGlobalVariablesStore } from "../../../../stores/globalVariables";
|
||||
import { useTypesStore } from "../../../../stores/typesStore";
|
||||
import { APIClassType } from "../../../../types/api";
|
||||
import { APIClassType, ResponseErrorTypeAPI } from "../../../../types/api";
|
||||
import { ParameterComponentType } from "../../../../types/components";
|
||||
import { NodeDataType } from "../../../../types/flow";
|
||||
import {
|
||||
handleUpdateValues,
|
||||
throttledHandleUpdateValues,
|
||||
} from "../../../../utils/parameterUtils";
|
||||
import {
|
||||
convertObjToArray,
|
||||
convertValuesToNumbers,
|
||||
|
|
@ -71,7 +76,7 @@ export default function ParameterComponent({
|
|||
(state) => state.globalVariablesEntries
|
||||
);
|
||||
const setNoticeData = useAlertStore((state) => state.setNoticeData);
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const flow = currentFlow?.data?.nodes ?? null;
|
||||
|
||||
const groupedEdge = useRef(null);
|
||||
|
|
@ -88,32 +93,92 @@ export default function ParameterComponent({
|
|||
|
||||
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
|
||||
|
||||
const handleUpdateValues = async (name: string, data: NodeDataType) => {
|
||||
const code = data.node?.template["code"]?.value;
|
||||
if (!code) {
|
||||
console.error("Code not found in the template");
|
||||
return;
|
||||
}
|
||||
|
||||
const handleRefreshButtonPress = async (name, data) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const res = await postCustomComponentUpdate(code, name);
|
||||
if (res.status === 200 && data.node?.template) {
|
||||
data.node!.template[name] = res.data.template[name];
|
||||
let newTemplate = await handleUpdateValues(name, data);
|
||||
if (newTemplate) {
|
||||
setNode(data.id, (oldNode) => {
|
||||
let newNode = cloneDeep(oldNode);
|
||||
newNode.data = {
|
||||
...newNode.data,
|
||||
};
|
||||
newNode.data.node.template = newTemplate;
|
||||
return newNode;
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
setErrorData(err as { title: string; list?: Array<string> });
|
||||
} catch (error) {
|
||||
let responseError = error as ResponseErrorTypeAPI;
|
||||
setErrorData({
|
||||
title: "Error while updating the Component",
|
||||
list: [responseError.response.data.detail.error ?? "Unknown error"],
|
||||
});
|
||||
}
|
||||
setIsLoading(false);
|
||||
renderTooltips();
|
||||
};
|
||||
|
||||
const handleOnNewValue = (
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
if (
|
||||
(data.node?.template[name]?.real_time_refresh ||
|
||||
data.node?.template[name]?.refresh_button) &&
|
||||
// options can be undefined but not an empty array
|
||||
(data.node?.template[name]?.options?.length ?? 0) === 0
|
||||
) {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
let newTemplate = await handleUpdateValues(name, data);
|
||||
if (newTemplate) {
|
||||
setNode(data.id, (oldNode) => {
|
||||
let newNode = cloneDeep(oldNode);
|
||||
newNode.data = {
|
||||
...newNode.data,
|
||||
};
|
||||
newNode.data.node.template = newTemplate;
|
||||
return newNode;
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
let responseError = error as ResponseErrorTypeAPI;
|
||||
setErrorData({
|
||||
title: "Error while updating the Component",
|
||||
list: [responseError.response.data.detail.error ?? "Unknown error"],
|
||||
});
|
||||
}
|
||||
setIsLoading(false);
|
||||
renderTooltips();
|
||||
}
|
||||
}
|
||||
fetchData();
|
||||
}, []);
|
||||
const handleOnNewValue = async (
|
||||
newValue: string | string[] | boolean | Object[]
|
||||
): void => {
|
||||
): Promise<void> => {
|
||||
if (data.node!.template[name].value !== newValue) {
|
||||
takeSnapshot();
|
||||
}
|
||||
const shouldUpdate =
|
||||
data.node?.template[name].real_time_refresh &&
|
||||
!data.node?.template[name].refresh_button &&
|
||||
data.node!.template[name].value !== newValue;
|
||||
|
||||
data.node!.template[name].value = newValue; // necessary to enable ctrl+z inside the input
|
||||
|
||||
let newTemplate;
|
||||
if (shouldUpdate) {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
newTemplate = await throttledHandleUpdateValues(name, data);
|
||||
} catch (error) {
|
||||
let responseError = error as ResponseErrorTypeAPI;
|
||||
setErrorData({
|
||||
title: "Error while updating the Component",
|
||||
list: [responseError.response.data.detail.error ?? "Unknown error"],
|
||||
});
|
||||
}
|
||||
setIsLoading(false);
|
||||
// this de
|
||||
}
|
||||
setNode(data.id, (oldNode) => {
|
||||
let newNode = cloneDeep(oldNode);
|
||||
|
||||
|
|
@ -121,7 +186,9 @@ export default function ParameterComponent({
|
|||
...newNode.data,
|
||||
};
|
||||
|
||||
newNode.data.node.template[name].value = newValue;
|
||||
if (data.node?.template[name].real_time_refresh && newTemplate) {
|
||||
newNode.data.node.template = newTemplate;
|
||||
} else newNode.data.node.template[name].value = newValue;
|
||||
|
||||
return newNode;
|
||||
});
|
||||
|
|
@ -185,11 +252,7 @@ export default function ParameterComponent({
|
|||
return (
|
||||
<div key={index}>
|
||||
{index === 0 && (
|
||||
<span>
|
||||
{left
|
||||
? "Avaliable input components:"
|
||||
: "Avaliable output components:"}
|
||||
</span>
|
||||
<span>{left ? INPUT_HANDLER_HOVER : OUTPUT_HANDLER_HOVER}</span>
|
||||
)}
|
||||
<span
|
||||
key={index}
|
||||
|
|
@ -219,7 +282,7 @@ export default function ParameterComponent({
|
|||
{item.display_name === "" ? "" : " - "}
|
||||
{item.display_name.split(", ").length > 2
|
||||
? item.display_name.split(", ").map((el, index) => (
|
||||
<React.Fragment key={el + index}>
|
||||
<React.Fragment key={el + name}>
|
||||
<span>
|
||||
{index ===
|
||||
item.display_name.split(", ").length - 1
|
||||
|
|
@ -236,7 +299,7 @@ export default function ParameterComponent({
|
|||
{item.type === "" ? "" : " - "}
|
||||
{item.type.split(", ").length > 2
|
||||
? item.type.split(", ").map((el, index) => (
|
||||
<React.Fragment key={el + index}>
|
||||
<React.Fragment key={el + name}>
|
||||
<span>
|
||||
{index === item.type.split(", ").length - 1
|
||||
? el
|
||||
|
|
@ -257,11 +320,14 @@ export default function ParameterComponent({
|
|||
refHtml.current = <span>{TOOLTIP_EMPTY}</span>;
|
||||
}
|
||||
}
|
||||
// If optionalHandle is an empty list, then it is not an optional handle
|
||||
if (optionalHandle && optionalHandle.length === 0) {
|
||||
optionalHandle = null;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
renderTooltips();
|
||||
}, [tooltipTitle, flow]);
|
||||
|
||||
return !showNode ? (
|
||||
left && LANGFLOW_SUPPORTED_TYPES.has(type ?? "") && !optionalHandle ? (
|
||||
<></>
|
||||
|
|
@ -270,7 +336,7 @@ export default function ParameterComponent({
|
|||
<div className="flex">
|
||||
<ShadTooltip
|
||||
styleClasses={"tooltip-fixed-width custom-scroll nowheel"}
|
||||
delayDuration={0}
|
||||
delayDuration={1000}
|
||||
content={refHtml.current}
|
||||
side={left ? "left" : "right"}
|
||||
>
|
||||
|
|
@ -309,7 +375,13 @@ export default function ParameterComponent({
|
|||
) : (
|
||||
<div
|
||||
ref={ref}
|
||||
className="relative mt-1 flex w-full flex-wrap items-center justify-between bg-muted px-5 py-2"
|
||||
className={
|
||||
"relative mt-1 flex w-full flex-wrap items-center justify-between bg-muted px-5 py-2" +
|
||||
((name === "code" && type === "code") ||
|
||||
(name.includes("code") && proxy)
|
||||
? " hidden "
|
||||
: "")
|
||||
}
|
||||
>
|
||||
<>
|
||||
<div
|
||||
|
|
@ -318,14 +390,25 @@ export default function ParameterComponent({
|
|||
(left ? "" : " text-end")
|
||||
}
|
||||
>
|
||||
{!left && data.node?.frozen && (
|
||||
<div>
|
||||
<IconComponent className="h-5 w-5 text-ice" name={"Snowflake"} />
|
||||
</div>
|
||||
)}
|
||||
{proxy ? (
|
||||
<ShadTooltip content={<span>{proxy.id}</span>}>
|
||||
<span>{title}</span>
|
||||
<span className={!left && data.node?.frozen ? " text-ice" : ""}>
|
||||
{title}
|
||||
</span>
|
||||
</ShadTooltip>
|
||||
) : (
|
||||
title
|
||||
<span className={!left && data.node?.frozen ? " text-ice" : ""}>
|
||||
{title}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-status-red">{required ? " *" : ""}</span>
|
||||
<span className={(info === "" ? "" : "ml-1 ") + " text-status-red"}>
|
||||
{required ? " *" : ""}
|
||||
</span>
|
||||
<div className="">
|
||||
{info !== "" && (
|
||||
<ShadTooltip content={infoHtml.current}>
|
||||
|
|
@ -333,7 +416,7 @@ export default function ParameterComponent({
|
|||
<div>
|
||||
<IconComponent
|
||||
name="Info"
|
||||
className="relative bottom-0.5 ml-2 h-3 w-4"
|
||||
className="relative bottom-px ml-1.5 h-3 w-4"
|
||||
/>
|
||||
</div>
|
||||
</ShadTooltip>
|
||||
|
|
@ -388,31 +471,81 @@ export default function ParameterComponent({
|
|||
!data.node?.template[name].options ? (
|
||||
<div className="mt-2 w-full">
|
||||
{data.node?.template[name].list ? (
|
||||
<InputListComponent
|
||||
disabled={disabled}
|
||||
value={
|
||||
!data.node.template[name].value ||
|
||||
data.node.template[name].value === ""
|
||||
? [""]
|
||||
: data.node.template[name].value
|
||||
<div
|
||||
className={
|
||||
// Commenting this out until we have a better
|
||||
// way to display
|
||||
// (data.node?.template[name].refresh ? "w-5/6 " : "") +
|
||||
"flex-grow"
|
||||
}
|
||||
onChange={handleOnNewValue}
|
||||
/>
|
||||
>
|
||||
<InputListComponent
|
||||
disabled={disabled}
|
||||
value={
|
||||
!data.node.template[name].value ||
|
||||
data.node.template[name].value === ""
|
||||
? [""]
|
||||
: data.node.template[name].value
|
||||
}
|
||||
onChange={handleOnNewValue}
|
||||
/>
|
||||
{/* {data.node?.template[name].refresh_button && (
|
||||
<div className="w-1/6">
|
||||
<RefreshButton
|
||||
isLoading={isLoading}
|
||||
disabled={disabled}
|
||||
name={name}
|
||||
data={data}
|
||||
className="extra-side-bar-buttons ml-2 mt-1"
|
||||
handleUpdateValues={handleRefreshButtonPress}
|
||||
id={"refresh-button-" + name}
|
||||
/>
|
||||
</div>
|
||||
)} */}
|
||||
</div>
|
||||
) : data.node?.template[name].multiline ? (
|
||||
<TextAreaComponent
|
||||
disabled={disabled}
|
||||
value={data.node.template[name].value ?? ""}
|
||||
onChange={handleOnNewValue}
|
||||
id={"textarea-" + data.node.template[name].name}
|
||||
data-testid={"textarea-" + data.node.template[name].name}
|
||||
/>
|
||||
<div className="mt-2 flex w-full flex-col ">
|
||||
<div className="flex-grow">
|
||||
<TextAreaComponent
|
||||
disabled={disabled}
|
||||
value={data.node.template[name].value ?? ""}
|
||||
onChange={handleOnNewValue}
|
||||
id={"textarea-" + data.node.template[name].name}
|
||||
data-testid={"textarea-" + data.node.template[name].name}
|
||||
/>
|
||||
</div>
|
||||
{data.node?.template[name].refresh_button && (
|
||||
<div className="flex-grow">
|
||||
<RefreshButton
|
||||
isLoading={isLoading}
|
||||
disabled={disabled}
|
||||
name={name}
|
||||
data={data}
|
||||
button_text={
|
||||
data.node?.template[name].refresh_button_text ??
|
||||
"Refresh"
|
||||
}
|
||||
className="extra-side-bar-buttons mt-1"
|
||||
handleUpdateValues={handleRefreshButtonPress}
|
||||
id={"refresh-button-" + name}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<InputComponent
|
||||
id={"input-" + index}
|
||||
disabled={disabled}
|
||||
password={data.node?.template[name].password ?? false}
|
||||
value={data.node?.template[name].value ?? ""}
|
||||
options={globalVariablesEntries}
|
||||
<div className="mt-2 flex w-full items-center">
|
||||
<div
|
||||
className={
|
||||
"flex-grow " +
|
||||
(data.node?.template[name].refresh_button ? "w-5/6" : "")
|
||||
}
|
||||
>
|
||||
<InputComponent
|
||||
id={"input-" + name}
|
||||
disabled={disabled}
|
||||
password={data.node?.template[name].password ?? false}
|
||||
value={data.node?.template[name].value ?? ""}
|
||||
options={globalVariablesEntries}
|
||||
onChange={(value) => {
|
||||
handleOnNewValue(value);
|
||||
if (globalVariablesEntries.includes(value)) {
|
||||
|
|
@ -440,17 +573,37 @@ export default function ParameterComponent({
|
|||
}
|
||||
//mark as global variable
|
||||
}}
|
||||
/>
|
||||
/>
|
||||
</div>
|
||||
{data.node?.template[name].refresh_button && (
|
||||
<div className="w-1/6">
|
||||
<RefreshButton
|
||||
isLoading={isLoading}
|
||||
disabled={disabled}
|
||||
name={name}
|
||||
data={data}
|
||||
button_text={
|
||||
data.node?.template[name].refresh_button_text ??
|
||||
"Refresh"
|
||||
}
|
||||
className="extra-side-bar-buttons ml-2 mt-1"
|
||||
handleUpdateValues={handleRefreshButtonPress}
|
||||
id={"refresh-button-" + name}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : left === true && type === "bool" ? (
|
||||
<div className="mt-2 w-full">
|
||||
<ToggleShadComponent
|
||||
id={"toggle-" + index}
|
||||
id={"toggle-" + name}
|
||||
disabled={disabled}
|
||||
enabled={data.node?.template[name].value ?? false}
|
||||
setEnabled={handleOnNewValue}
|
||||
size="large"
|
||||
editNode={false}
|
||||
/>
|
||||
</div>
|
||||
) : left === true && type === "float" ? (
|
||||
|
|
@ -464,26 +617,35 @@ export default function ParameterComponent({
|
|||
</div>
|
||||
) : left === true &&
|
||||
type === "str" &&
|
||||
data.node?.template[name].options ? (
|
||||
(data.node?.template[name].options ||
|
||||
data.node?.template[name]?.real_time_refresh) ? (
|
||||
// TODO: Improve CSS
|
||||
<div className="mt-2 flex w-full items-center">
|
||||
<div className="w-5/6 flex-grow">
|
||||
<Dropdown
|
||||
disabled={disabled}
|
||||
isLoading={isLoading}
|
||||
options={data.node.template[name].options}
|
||||
onSelect={handleOnNewValue}
|
||||
value={data.node.template[name].value ?? "Choose an option"}
|
||||
id={"dropdown-" + index}
|
||||
id={"dropdown-" + name}
|
||||
/>
|
||||
</div>
|
||||
{data.node?.template[name].refresh && (
|
||||
<button
|
||||
className="extra-side-bar-buttons ml-2 mt-1 w-1/6"
|
||||
onClick={() => {
|
||||
handleUpdateValues(name, data);
|
||||
}}
|
||||
>
|
||||
<IconComponent name="RefreshCcw" />
|
||||
</button>
|
||||
{data.node?.template[name].refresh_button && (
|
||||
<div className="w-1/6">
|
||||
<RefreshButton
|
||||
isLoading={isLoading}
|
||||
disabled={disabled}
|
||||
name={name}
|
||||
data={data}
|
||||
button_text={
|
||||
data.node?.template[name].refresh_button_text ?? "Refresh"
|
||||
}
|
||||
className="extra-side-bar-buttons ml-2 mt-1"
|
||||
handleUpdateValues={handleRefreshButtonPress}
|
||||
id={"refresh-button-" + name}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : left === true && type === "code" ? (
|
||||
|
|
@ -500,7 +662,7 @@ export default function ParameterComponent({
|
|||
disabled={disabled}
|
||||
value={data.node?.template[name].value ?? ""}
|
||||
onChange={handleOnNewValue}
|
||||
id={"code-input-" + index}
|
||||
id={"code-input-" + name}
|
||||
/>
|
||||
</div>
|
||||
) : left === true && type === "file" ? (
|
||||
|
|
@ -521,7 +683,7 @@ export default function ParameterComponent({
|
|||
disabled={disabled}
|
||||
value={data.node?.template[name].value ?? ""}
|
||||
onChange={handleOnNewValue}
|
||||
id={"int-input-" + index}
|
||||
id={"int-input-" + name}
|
||||
/>
|
||||
</div>
|
||||
) : left === true && type === "prompt" ? (
|
||||
|
|
@ -534,8 +696,8 @@ export default function ParameterComponent({
|
|||
disabled={disabled}
|
||||
value={data.node?.template[name].value ?? ""}
|
||||
onChange={handleOnNewValue}
|
||||
id={"prompt-input-" + index}
|
||||
data-testid={"prompt-input-" + index}
|
||||
id={"prompt-input-" + name}
|
||||
data-testid={"prompt-input-" + name}
|
||||
/>
|
||||
</div>
|
||||
) : left === true && type === "NestedDict" ? (
|
||||
|
|
@ -570,8 +732,14 @@ export default function ParameterComponent({
|
|||
onChange={(newValue) => {
|
||||
const valueToNumbers = convertValuesToNumbers(newValue);
|
||||
setErrorDuplicateKey(hasDuplicateKeys(valueToNumbers));
|
||||
handleOnNewValue(valueToNumbers);
|
||||
// if data.node?.template[name].list is true, then the value is an array of objects
|
||||
// else we need to get the first object of the array
|
||||
|
||||
if (data.node?.template[name].list) {
|
||||
handleOnNewValue(valueToNumbers);
|
||||
} else handleOnNewValue(valueToNumbers[0]);
|
||||
}}
|
||||
isList={data.node?.template[name].list ?? false}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -1,20 +1,33 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { NodeToolbar } from "reactflow";
|
||||
import { cloneDeep } from "lodash";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { NodeToolbar, useUpdateNodeInternals } from "reactflow";
|
||||
import ShadTooltip from "../../components/ShadTooltipComponent";
|
||||
import Tooltip from "../../components/TooltipComponent";
|
||||
import IconComponent from "../../components/genericIconComponent";
|
||||
import InputComponent from "../../components/inputComponent";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import Checkmark from "../../components/ui/checkmark";
|
||||
import Loading from "../../components/ui/loading";
|
||||
import { Textarea } from "../../components/ui/textarea";
|
||||
import { priorityFields } from "../../constants/constants";
|
||||
import Xmark from "../../components/ui/xmark";
|
||||
import {
|
||||
NATIVE_CATEGORIES,
|
||||
RUN_TIMESTAMP_PREFIX,
|
||||
STATUS_BUILD,
|
||||
STATUS_BUILDING,
|
||||
} from "../../constants/constants";
|
||||
import { BuildStatus } from "../../constants/enums";
|
||||
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 { NodeDataType } from "../../types/flow";
|
||||
import { handleKeyDown, scapedJSONStringfy } from "../../utils/reactflowUtils";
|
||||
import { nodeColors, nodeIconsLucide } from "../../utils/styleUtils";
|
||||
import { classNames, cn, getFieldTitle } from "../../utils/utils";
|
||||
import { classNames, cn, getFieldTitle, sortFields } from "../../utils/utils";
|
||||
import ParameterComponent from "./components/parameterComponent";
|
||||
|
||||
export default function GenericNode({
|
||||
|
|
@ -29,8 +42,13 @@ export default function GenericNode({
|
|||
yPos: number;
|
||||
}): JSX.Element {
|
||||
const types = useTypesStore((state) => state.types);
|
||||
const templates = useTypesStore((state) => state.templates);
|
||||
const deleteNode = useFlowStore((state) => state.deleteNode);
|
||||
const flowPool = useFlowStore((state) => state.flowPool);
|
||||
const buildFlow = useFlowStore((state) => state.buildFlow);
|
||||
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 [inputName, setInputName] = useState(false);
|
||||
const [nodeName, setNodeName] = useState(data.node!.display_name);
|
||||
|
|
@ -38,12 +56,79 @@ export default function GenericNode({
|
|||
const [nodeDescription, setNodeDescription] = useState(
|
||||
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);
|
||||
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 (
|
||||
!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;
|
||||
if (currentCode !== thisNodesCode) {
|
||||
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) !== "_")
|
||||
|
|
@ -68,7 +153,6 @@ export default function GenericNode({
|
|||
|
||||
setHandles(count);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
countHandles();
|
||||
}, [data, data.node]);
|
||||
|
|
@ -81,9 +165,19 @@ export default function GenericNode({
|
|||
}, [selected]);
|
||||
|
||||
// State for outline color
|
||||
const sseData = useFlowStore((state) => state.sseData);
|
||||
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);
|
||||
|
||||
useEffect(() => {
|
||||
setNodeDescription(data.node!.description);
|
||||
}, [data.node!.description]);
|
||||
|
|
@ -92,20 +186,155 @@ export default function GenericNode({
|
|||
setNodeName(data.node!.display_name);
|
||||
}, [data.node!.display_name]);
|
||||
|
||||
// New useEffect to watch for changes in sseData and update validation status
|
||||
useEffect(() => {
|
||||
const relevantData = sseData[data.id];
|
||||
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);
|
||||
}
|
||||
}, [sseData, data.id]);
|
||||
}, [flowPool[data.id], data.id]);
|
||||
|
||||
const showNode = data.showNode ?? true;
|
||||
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);
|
||||
}
|
||||
|
||||
const nameEditable = data.node?.flow || data.type === "CustomComponent";
|
||||
setValidationString(newValidationString);
|
||||
}
|
||||
}, [validationStatus, validationStatus?.params]);
|
||||
|
||||
// const showNode = data.showNode ?? true;
|
||||
|
||||
const [showNode, setShowNode] = useState(data.showNode ?? true);
|
||||
|
||||
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 nodeIconFragment = (icon) => {
|
||||
return <span className="text-lg">{icon}</span>;
|
||||
};
|
||||
|
||||
const checkNodeIconFragment = (iconColor, iconName, iconClassName) => {
|
||||
return (
|
||||
<IconComponent
|
||||
name={iconName}
|
||||
className={iconClassName}
|
||||
iconColor={iconColor}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
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"
|
||||
/>
|
||||
) : 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 && isInvalid) {
|
||||
// INACTIVE should have its own class
|
||||
return "inactive-status";
|
||||
}
|
||||
if (buildStatus === BuildStatus.BUILT && isInvalid) {
|
||||
return isDark ? "built-invalid-status-dark" : "built-invalid-status";
|
||||
} else if (buildStatus === BuildStatus.BUILDING) {
|
||||
return "building-status";
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
const getNodeBorderClassName = (
|
||||
selected: boolean,
|
||||
showNode: boolean,
|
||||
buildStatus: BuildStatus | undefined,
|
||||
validationStatus: validationStatusType | null
|
||||
) => {
|
||||
const specificClassFromBuildStatus = getSpecificClassFromBuildStatus(
|
||||
buildStatus,
|
||||
validationStatus
|
||||
);
|
||||
const baseBorderClass = getBaseBorderClass(selected);
|
||||
const nodeSizeClass = getNodeSizeClass(showNode);
|
||||
return classNames(
|
||||
baseBorderClass,
|
||||
nodeSizeClass,
|
||||
"generic-node-div",
|
||||
specificClassFromBuildStatus
|
||||
);
|
||||
};
|
||||
|
||||
const getBaseBorderClass = (selected) =>
|
||||
selected ? "border border-ring" : "border";
|
||||
|
||||
const getNodeSizeClass = (showNode) =>
|
||||
showNode ? "w-96 rounded-lg" : "w-26 h-26 rounded-full";
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -123,16 +352,22 @@ export default function GenericNode({
|
|||
data: { ...old.data, showNode: show },
|
||||
}));
|
||||
}}
|
||||
setShowState={setShowNode}
|
||||
numberOfHandles={handles}
|
||||
showNode={showNode}
|
||||
openAdvancedModal={false}
|
||||
onCloseAdvancedModal={() => {}}
|
||||
updateNodeCode={updateNodeCode}
|
||||
isOutdated={isOutdated}
|
||||
selected={selected}
|
||||
></NodeToolbarComponent>
|
||||
</NodeToolbar>
|
||||
|
||||
<div
|
||||
className={classNames(
|
||||
selected ? "border border-ring" : "border",
|
||||
showNode ? " w-96 rounded-lg" : " w-26 h-26 rounded-full",
|
||||
"generic-node-div"
|
||||
className={getNodeBorderClassName(
|
||||
selected,
|
||||
showNode,
|
||||
buildStatus,
|
||||
validationStatus
|
||||
)}
|
||||
>
|
||||
{data.node?.beta && showNode && (
|
||||
|
|
@ -153,17 +388,10 @@ export default function GenericNode({
|
|||
<div
|
||||
className={
|
||||
"generic-node-title-arrangement rounded-full" +
|
||||
(!showNode && "justify-center")
|
||||
(!showNode && " justify-center ")
|
||||
}
|
||||
>
|
||||
<IconComponent
|
||||
name={data.node?.flow ? "group_components" : name}
|
||||
className={
|
||||
"generic-node-icon " +
|
||||
(!showNode ? "absolute inset-x-6 h-12 w-12" : "")
|
||||
}
|
||||
iconColor={`${nodeColors[types[data.type]]}`}
|
||||
/>
|
||||
{iconNodeRender()}
|
||||
{showNode && (
|
||||
<div className="generic-node-tooltip-div">
|
||||
{nameEditable && inputName ? (
|
||||
|
|
@ -194,28 +422,39 @@ export default function GenericNode({
|
|||
/>
|
||||
</div>
|
||||
) : (
|
||||
<ShadTooltip content={data.node?.display_name}>
|
||||
<div
|
||||
className="flex"
|
||||
onDoubleClick={() => {
|
||||
setInputName(true);
|
||||
takeSnapshot();
|
||||
}}
|
||||
>
|
||||
<div className="group flex items-start gap-1.5">
|
||||
<ShadTooltip content={data.node?.display_name}>
|
||||
<div
|
||||
onDoubleClick={(event) => {
|
||||
if (nameEditable) {
|
||||
setInputName(true);
|
||||
}
|
||||
takeSnapshot();
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}}
|
||||
data-testid={"title-" + data.node?.display_name}
|
||||
className="generic-node-tooltip-div pr-2 text-primary"
|
||||
className="generic-node-tooltip-div text-primary"
|
||||
>
|
||||
{data.node?.display_name}
|
||||
</div>
|
||||
{nameEditable && (
|
||||
</ShadTooltip>
|
||||
{nameEditable && (
|
||||
<div
|
||||
onClick={(event) => {
|
||||
setInputName(true);
|
||||
takeSnapshot();
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}}
|
||||
>
|
||||
<IconComponent
|
||||
name="Pencil"
|
||||
className="h-4 w-4 text-ring"
|
||||
name="PencilLine"
|
||||
className="hidden h-3 w-3 text-status-blue group-hover:block"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</ShadTooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -241,13 +480,34 @@ export default function GenericNode({
|
|||
})}
|
||||
data={data}
|
||||
color={
|
||||
nodeColors[
|
||||
types[data.node?.template[templateField].type!]
|
||||
] ??
|
||||
nodeColors[
|
||||
data.node?.template[templateField].type!
|
||||
] ??
|
||||
nodeColors.unknown
|
||||
data.node?.template[templateField].input_types &&
|
||||
data.node?.template[templateField].input_types!
|
||||
.length > 0
|
||||
? nodeColors[
|
||||
data.node?.template[templateField]
|
||||
.input_types![
|
||||
data.node?.template[templateField]
|
||||
.input_types!.length - 1
|
||||
]
|
||||
] ??
|
||||
nodeColors[
|
||||
types[
|
||||
data.node?.template[templateField]
|
||||
.input_types![
|
||||
data.node?.template[templateField]
|
||||
.input_types!.length - 1
|
||||
]
|
||||
]
|
||||
]
|
||||
: nodeColors[
|
||||
data.node?.template[templateField].type!
|
||||
] ??
|
||||
nodeColors[
|
||||
types[
|
||||
data.node?.template[templateField].type!
|
||||
]
|
||||
] ??
|
||||
nodeColors.unknown
|
||||
}
|
||||
title={getFieldTitle(
|
||||
data.node?.template!,
|
||||
|
|
@ -292,7 +552,7 @@ export default function GenericNode({
|
|||
title={
|
||||
data.node?.output_types &&
|
||||
data.node.output_types.length > 0
|
||||
? data.node.output_types.join("|")
|
||||
? data.node.output_types.join(" | ")
|
||||
: data.type
|
||||
}
|
||||
tooltipTitle={data.node?.base_classes.join("\n")}
|
||||
|
|
@ -308,65 +568,64 @@ export default function GenericNode({
|
|||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showNode && (
|
||||
<div className="round-button-div">
|
||||
<div>
|
||||
<Tooltip
|
||||
title={
|
||||
isBuilding ? (
|
||||
<span>Building...</span>
|
||||
) : !validationStatus ? (
|
||||
<span className="flex">
|
||||
Build{" "}
|
||||
<IconComponent
|
||||
name="Zap"
|
||||
className="mx-0.5 h-5 fill-build-trigger stroke-build-trigger stroke-1"
|
||||
/>{" "}
|
||||
flow to validate status.
|
||||
</span>
|
||||
) : (
|
||||
<div className="max-h-96 overflow-auto">
|
||||
{typeof validationStatus.params === "string"
|
||||
? `Duration: ${validationStatus.duration}\n${validationStatus.params}`
|
||||
.split("\n")
|
||||
.map((line, index) => (
|
||||
<div key={index}>{line}</div>
|
||||
))
|
||||
: ""}
|
||||
<ShadTooltip
|
||||
content={
|
||||
buildStatus === BuildStatus.BUILDING ? (
|
||||
<span> {STATUS_BUILDING} </span>
|
||||
) : !validationStatus ? (
|
||||
<span className="flex">{STATUS_BUILD}</span>
|
||||
) : (
|
||||
<div className="max-h-100 p-2">
|
||||
<div>
|
||||
{lastRunTime && (
|
||||
<div className="justify-left flex font-normal text-muted-foreground">
|
||||
<div>{RUN_TIMESTAMP_PREFIX}</div>
|
||||
<div className="ml-1 text-status-blue">
|
||||
{lastRunTime}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="justify-left flex font-normal text-muted-foreground">
|
||||
<div>Duration:</div>
|
||||
<div className="mb-3 ml-1 text-status-blue">
|
||||
{validationStatus?.data.duration}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
>
|
||||
<div className="generic-node-status-position">
|
||||
<div
|
||||
className={classNames(
|
||||
validationStatus && validationStatus.valid
|
||||
? "green-status"
|
||||
: "status-build-animation",
|
||||
"status-div"
|
||||
)}
|
||||
></div>
|
||||
<div
|
||||
className={classNames(
|
||||
validationStatus && !validationStatus.valid
|
||||
? "red-status"
|
||||
: "status-build-animation",
|
||||
"status-div"
|
||||
)}
|
||||
></div>
|
||||
<div
|
||||
className={classNames(
|
||||
!validationStatus || isBuilding
|
||||
? "yellow-status"
|
||||
: "status-build-animation",
|
||||
"status-div"
|
||||
)}
|
||||
></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>
|
||||
</Tooltip>
|
||||
</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>
|
||||
<div className="generic-node-status-position flex items-center justify-center">
|
||||
{renderIconStatus(buildStatus, validationStatus)}
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
</ShadTooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -387,6 +646,7 @@ export default function GenericNode({
|
|||
autoFocus
|
||||
onBlur={() => {
|
||||
setInputDescription(false);
|
||||
setInputName(false);
|
||||
setNodeDescription(nodeDescription);
|
||||
setNode(data.id, (old) => ({
|
||||
...old,
|
||||
|
|
@ -434,7 +694,7 @@ export default function GenericNode({
|
|||
? "font-light italic"
|
||||
: ""
|
||||
)}
|
||||
onDoubleClick={() => {
|
||||
onDoubleClick={(e) => {
|
||||
setInputDescription(true);
|
||||
takeSnapshot();
|
||||
}}
|
||||
|
|
@ -449,15 +709,7 @@ export default function GenericNode({
|
|||
<>
|
||||
{Object.keys(data.node!.template)
|
||||
.filter((templateField) => templateField.charAt(0) !== "_")
|
||||
.sort((a, b) => {
|
||||
if (priorityFields.has(a.toLowerCase())) {
|
||||
return -1;
|
||||
} else if (priorityFields.has(b.toLowerCase())) {
|
||||
return 1;
|
||||
} else {
|
||||
return a.localeCompare(b);
|
||||
}
|
||||
})
|
||||
.sort((a, b) => sortFields(a, b, data.node?.field_order ?? []))
|
||||
.map((templateField: string, idx) => (
|
||||
<div key={idx}>
|
||||
{data.node!.template[templateField].show &&
|
||||
|
|
@ -474,13 +726,31 @@ export default function GenericNode({
|
|||
})}
|
||||
data={data}
|
||||
color={
|
||||
nodeColors[
|
||||
data.node?.template[templateField].type!
|
||||
] ??
|
||||
nodeColors[
|
||||
types[data.node?.template[templateField].type!]
|
||||
] ??
|
||||
nodeColors.unknown
|
||||
data.node?.template[templateField].input_types &&
|
||||
data.node?.template[templateField].input_types!
|
||||
.length > 0
|
||||
? nodeColors[
|
||||
data.node?.template[templateField].input_types![
|
||||
data.node?.template[templateField]
|
||||
.input_types!.length - 1
|
||||
]
|
||||
] ??
|
||||
nodeColors[
|
||||
types[
|
||||
data.node?.template[templateField]
|
||||
.input_types![
|
||||
data.node?.template[templateField]
|
||||
.input_types!.length - 1
|
||||
]
|
||||
]
|
||||
]
|
||||
: nodeColors[
|
||||
data.node?.template[templateField].type!
|
||||
] ??
|
||||
nodeColors[
|
||||
types[data.node?.template[templateField].type!]
|
||||
] ??
|
||||
nodeColors.unknown
|
||||
}
|
||||
title={getFieldTitle(
|
||||
data.node?.template!,
|
||||
|
|
@ -539,7 +809,7 @@ export default function GenericNode({
|
|||
}
|
||||
title={
|
||||
data.node?.output_types && data.node.output_types.length > 0
|
||||
? data.node.output_types.join("|")
|
||||
? data.node.output_types.join(" | ")
|
||||
: data.type
|
||||
}
|
||||
tooltipTitle={data.node?.base_classes.join("\n")}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { Cross2Icon } from "@radix-ui/react-icons";
|
||||
import { useState } from "react";
|
||||
import IconComponent from "../../components/genericIconComponent";
|
||||
import {
|
||||
|
|
@ -5,6 +6,7 @@ import {
|
|||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "../../components/ui/popover";
|
||||
import { ZERO_NOTIFICATIONS } from "../../constants/constants";
|
||||
import useAlertStore from "../../stores/alertStore";
|
||||
import { AlertDropdownType } from "../../types/alerts";
|
||||
import SingleAlert from "./components/singleAlertComponent";
|
||||
|
|
@ -45,15 +47,15 @@ export default function AlertDropdown({
|
|||
setTimeout(clearNotificationList, 100);
|
||||
}}
|
||||
>
|
||||
<IconComponent name="Trash2" className="h-[1.1rem] w-[1.1rem]" />
|
||||
<IconComponent name="Trash2" className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
className="text-foreground hover:text-status-red"
|
||||
className="text-foreground opacity-70 hover:opacity-100"
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<IconComponent name="X" className="h-5 w-5" />
|
||||
<Cross2Icon className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -68,7 +70,7 @@ export default function AlertDropdown({
|
|||
))
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center pb-16 text-ring">
|
||||
No new notifications
|
||||
{ZERO_NOTIFICATIONS}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 9.8 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
|
@ -49,8 +49,8 @@ export default function AccordionComponent({
|
|||
>
|
||||
{trigger}
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="AccordionContent">
|
||||
{children}
|
||||
<AccordionContent>
|
||||
<div className="AccordionContent flex flex-col">{children}</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
|
|
|
|||
|
|
@ -1,36 +1,63 @@
|
|||
import { XCircle } from "lucide-react";
|
||||
import { crashComponentPropsType } from "../../types/components";
|
||||
import { Button } from "../ui/button";
|
||||
import { Card, CardContent, CardFooter, CardHeader } from "../ui/card";
|
||||
|
||||
export default function CrashErrorComponent({
|
||||
error,
|
||||
resetErrorBoundary,
|
||||
}: crashComponentPropsType): JSX.Element {
|
||||
return (
|
||||
<div className="fixed left-0 top-0 z-50 flex h-full w-full items-center justify-center bg-foreground bg-opacity-50">
|
||||
<div className="flex h-1/3 min-h-fit max-w-4xl flex-col justify-evenly rounded-lg bg-background p-8 text-start shadow-lg">
|
||||
<h1 className="mb-4 text-3xl text-status-red">
|
||||
Oops! An unknown error has occurred.
|
||||
</h1>
|
||||
<p className="mb-4 text-xl text-foreground">
|
||||
Please click the 'Reset Application' button to restore the
|
||||
application's state. If the error persists, please create an issue on
|
||||
our GitHub page. We apologize for any inconvenience this may have
|
||||
caused.
|
||||
</p>
|
||||
<div className="flex justify-center">
|
||||
<button
|
||||
onClick={resetErrorBoundary}
|
||||
className="mr-4 rounded bg-primary px-4 py-2 font-bold text-background hover:bg-ring"
|
||||
>
|
||||
Reset Application
|
||||
</button>
|
||||
<a
|
||||
href="https://github.com/logspace-ai/langflow/issues/new"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="rounded bg-status-red px-4 py-2 font-bold text-background hover:bg-error-foreground"
|
||||
>
|
||||
Create Issue
|
||||
</a>
|
||||
<div className="z-50 flex h-screen w-screen items-center justify-center bg-foreground bg-opacity-50">
|
||||
<div className="flex h-screen w-screen flex-col bg-background text-start shadow-lg">
|
||||
<div className="m-auto grid w-1/2 justify-center gap-5 text-center">
|
||||
<Card className="p-8">
|
||||
<CardHeader>
|
||||
<div className="m-auto">
|
||||
<XCircle strokeWidth={1.5} className="h-16 w-16" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="mb-4 text-xl text-foreground">
|
||||
Sorry, we found an unexpected error!
|
||||
</p>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="grid">
|
||||
<div>
|
||||
<p>
|
||||
Please report errors with detailed tracebacks on the{" "}
|
||||
<a
|
||||
href="https://github.com/logspace-ai/langflow/issues"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-medium hover:underline "
|
||||
>
|
||||
GitHub Issues
|
||||
</a>{" "}
|
||||
page.
|
||||
<br></br>
|
||||
Thank you!
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
<CardFooter>
|
||||
<div className="m-auto mt-4 flex justify-center">
|
||||
<Button onClick={resetErrorBoundary}>Restart Langflow</Button>
|
||||
|
||||
<a
|
||||
href="https://github.com/logspace-ai/langflow/issues/new"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Button className="ml-3" variant={"outline"}>
|
||||
Report on GitHub
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
69
src/frontend/src/components/IOInputField/index.tsx
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import { cloneDeep } from "lodash";
|
||||
import useFlowStore from "../../stores/flowStore";
|
||||
import { IOInputProps } from "../../types/components";
|
||||
import IOFileInput from "../IOInputs/FileInput";
|
||||
import { Textarea } from "../ui/textarea";
|
||||
|
||||
export default function IOInputField({
|
||||
inputType,
|
||||
inputId,
|
||||
left,
|
||||
}: IOInputProps): JSX.Element | undefined {
|
||||
const nodes = useFlowStore((state) => state.nodes);
|
||||
const setNode = useFlowStore((state) => state.setNode);
|
||||
const node = nodes.find((node) => node.id === inputId);
|
||||
function handleInputType() {
|
||||
if (!node) return <>"No node found!"</>;
|
||||
switch (inputType) {
|
||||
case "TextInput":
|
||||
return (
|
||||
<Textarea
|
||||
className={`w-full ${left ? "" : " h-full"}`}
|
||||
placeholder={"Enter text..."}
|
||||
value={node.data.node!.template["input_value"].value}
|
||||
onChange={(e) => {
|
||||
e.target.value;
|
||||
if (node) {
|
||||
let newNode = cloneDeep(node);
|
||||
newNode.data.node!.template["input_value"].value =
|
||||
e.target.value;
|
||||
setNode(node.id, newNode);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
case "FileLoader":
|
||||
return (
|
||||
<IOFileInput
|
||||
field={node.data.node!.template["file_path"]["value"]}
|
||||
updateValue={(e) => {
|
||||
if (node) {
|
||||
let newNode = cloneDeep(node);
|
||||
newNode.data.node!.template["file_path"].value = e;
|
||||
setNode(node.id, newNode);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
default:
|
||||
return (
|
||||
<Textarea
|
||||
className="w-full custom-scroll"
|
||||
placeholder={"Enter text..."}
|
||||
value={node.data.node!.template["input_value"]}
|
||||
onChange={(e) => {
|
||||
e.target.value;
|
||||
if (node) {
|
||||
let newNode = cloneDeep(node);
|
||||
newNode.data.node!.template["input_value"].value =
|
||||
e.target.value;
|
||||
setNode(node.id, newNode);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
return handleInputType();
|
||||
}
|
||||
151
src/frontend/src/components/IOInputs/FileInput/index.tsx
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
import { Button } from "../../ui/button";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { BASE_URL_API } from "../../../constants/constants";
|
||||
import { uploadFile } from "../../../controllers/API";
|
||||
import useFlowsManagerStore from "../../../stores/flowsManagerStore";
|
||||
import { IOFileInputProps } from "../../../types/components";
|
||||
import IconComponent from "../../genericIconComponent";
|
||||
|
||||
export default function IOFileInput({ field, updateValue }: IOFileInputProps) {
|
||||
//component to handle file upload from chatIO
|
||||
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
|
||||
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [filePath, setFilePath] = useState("");
|
||||
const [image, setImage] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (filePath) {
|
||||
updateValue(filePath, "file");
|
||||
}
|
||||
}, [filePath]);
|
||||
|
||||
useEffect(() => {
|
||||
if (field) {
|
||||
const fileName = field.split("/")[1];
|
||||
const flowFileId = currentFlowId.toString();
|
||||
setImage(`${BASE_URL_API}files/images/${flowFileId}/${fileName}`);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const dragOver = (e) => {
|
||||
e.preventDefault();
|
||||
if (e.dataTransfer.types.some((types) => types === "Files")) {
|
||||
setIsDragging(true);
|
||||
}
|
||||
};
|
||||
|
||||
const dragEnter = (e) => {
|
||||
if (e.dataTransfer.types.some((types) => types === "Files")) {
|
||||
setIsDragging(true);
|
||||
}
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
const dragLeave = (e) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(false);
|
||||
};
|
||||
|
||||
const onDrop = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (e.dataTransfer.files.length > 0) {
|
||||
const file = e.dataTransfer.files[0];
|
||||
|
||||
upload(file);
|
||||
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = (event) => {
|
||||
const imageDataUrl = event.target?.result as string;
|
||||
setImage(imageDataUrl);
|
||||
};
|
||||
fileReader.readAsDataURL(file);
|
||||
}
|
||||
setIsDragging(false);
|
||||
};
|
||||
|
||||
const upload = async (file) => {
|
||||
if (file) {
|
||||
// Check if a file was selected
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = (event) => {
|
||||
const imageDataUrl = event.target?.result as string;
|
||||
setImage(imageDataUrl);
|
||||
|
||||
// Display the image on the screen
|
||||
const imgElement = document.createElement("img");
|
||||
imgElement.src = imageDataUrl;
|
||||
document.body.appendChild(imgElement); // Add the image to the body or replace this with your desired location
|
||||
};
|
||||
fileReader.readAsDataURL(file);
|
||||
|
||||
uploadFile(file, currentFlowId)
|
||||
.then((res) => res.data)
|
||||
.then((data) => {
|
||||
console.log("File uploaded successfully");
|
||||
// Get the file name from the response
|
||||
const { file_path, flowId } = data;
|
||||
setFilePath(file_path);
|
||||
})
|
||||
.catch(() => {
|
||||
console.error("Error occurred while uploading file");
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleButtonClick = (): void => {
|
||||
// Create a file input element
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.style.display = "none"; // Hidden from view
|
||||
input.multiple = false; // Allow only one file selection
|
||||
input.onchange = (event: Event): void => {
|
||||
// Get the selected file
|
||||
const file = (event.target as HTMLInputElement).files?.[0];
|
||||
upload(file);
|
||||
};
|
||||
// Trigger the file selection dialog
|
||||
input.click();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
onDragOver={dragOver}
|
||||
onDragEnter={dragEnter}
|
||||
onDragLeave={dragLeave}
|
||||
onDrop={onDrop}
|
||||
className={
|
||||
"flex h-full w-full items-center justify-between" +
|
||||
(isDragging
|
||||
? "flex h-28 flex-col items-center justify-center gap-4 text-xs font-light"
|
||||
: "")
|
||||
}
|
||||
>
|
||||
{!isDragging && (
|
||||
<Button variant="primary" onClick={handleButtonClick}>
|
||||
Upload or drop your file
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{isDragging ? (
|
||||
<>
|
||||
<IconComponent name="ArrowUpToLine" className="h-5 w-5 stroke-1" />
|
||||
"Drop your file here"
|
||||
</>
|
||||
) : image ? (
|
||||
<img
|
||||
className="order-last h-12 w-12 rounded-full object-cover "
|
||||
src={image ?? ""}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<IconComponent name="SunIcon" className="h-8 w-8 stroke-1" />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
48
src/frontend/src/components/IOOutputView/index.tsx
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { cloneDeep } from "lodash";
|
||||
import useFlowStore from "../../stores/flowStore";
|
||||
import { IOOutputProps } from "../../types/components";
|
||||
import { Textarea } from "../ui/textarea";
|
||||
|
||||
export default function IOOutputView({
|
||||
outputType,
|
||||
outputId,
|
||||
left,
|
||||
}: IOOutputProps): JSX.Element | undefined {
|
||||
const nodes = useFlowStore((state) => state.nodes);
|
||||
const setNode = useFlowStore((state) => state.setNode);
|
||||
const flowPool = useFlowStore((state) => state.flowPool);
|
||||
const node = nodes.find((node) => node.id === outputId);
|
||||
function handleOutputType() {
|
||||
if (!node) return <>"No node found!"</>;
|
||||
switch (outputType) {
|
||||
case "TextOutput":
|
||||
return (
|
||||
<Textarea
|
||||
className={`w-full custom-scroll ${left ? "" : " h-full"}`}
|
||||
placeholder={"Empty"}
|
||||
// update to real value on flowPool
|
||||
value={
|
||||
(flowPool[node.id] ?? [])[(flowPool[node.id]?.length ?? 1) - 1]
|
||||
?.params ?? ""
|
||||
}
|
||||
readOnly
|
||||
/>
|
||||
);
|
||||
|
||||
default:
|
||||
return (
|
||||
<Textarea
|
||||
className={`w-full custom-scroll ${left ? "" : " h-full"}`}
|
||||
placeholder={"Empty"}
|
||||
// update to real value on flowPool
|
||||
value={
|
||||
(flowPool[node.id] ?? [])[(flowPool[node.id]?.length ?? 1) - 1]
|
||||
?.params ?? ""
|
||||
}
|
||||
readOnly
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
return handleOutputType();
|
||||
}
|
||||
372
src/frontend/src/components/IOview/index.tsx
Normal file
|
|
@ -0,0 +1,372 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
CHAT_FORM_DIALOG_SUBTITLE,
|
||||
OUTPUTS_MODAL_TITLE,
|
||||
TEXT_INPUT_MODAL_TITLE,
|
||||
} from "../../constants/constants";
|
||||
import BaseModal from "../../modals/baseModal";
|
||||
import useFlowStore from "../../stores/flowStore";
|
||||
import useFlowsManagerStore from "../../stores/flowsManagerStore";
|
||||
import { NodeType } from "../../types/flow";
|
||||
import { updateVerticesOrder } from "../../utils/buildUtils";
|
||||
import { cn } from "../../utils/utils";
|
||||
import AccordionComponent from "../AccordionComponent";
|
||||
import IOInputField from "../IOInputField";
|
||||
import IOOutputView from "../IOOutputView";
|
||||
import ShadTooltip from "../ShadTooltipComponent";
|
||||
import IconComponent from "../genericIconComponent";
|
||||
import NewChatView from "../newChatView";
|
||||
import { Badge } from "../ui/badge";
|
||||
import { Button } from "../ui/button";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
|
||||
|
||||
export default function IOView({
|
||||
children,
|
||||
open,
|
||||
setOpen,
|
||||
disable,
|
||||
}: {
|
||||
children: JSX.Element;
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
disable?: boolean;
|
||||
}): JSX.Element {
|
||||
const inputs = useFlowStore((state) => state.inputs).filter(
|
||||
(input) => input.type !== "ChatInput"
|
||||
);
|
||||
const chatInput = useFlowStore((state) => state.inputs).find(
|
||||
(input) => input.type === "ChatInput"
|
||||
);
|
||||
const outputs = useFlowStore((state) => state.outputs).filter(
|
||||
(output) => output.type !== "ChatOutput"
|
||||
);
|
||||
const chatOutput = useFlowStore((state) => state.outputs).find(
|
||||
(output) => output.type === "ChatOutput"
|
||||
);
|
||||
const nodes = useFlowStore((state) => state.nodes).filter(
|
||||
(node) =>
|
||||
inputs.some((input) => input.id === node.id) ||
|
||||
outputs.some((output) => output.id === node.id)
|
||||
);
|
||||
const haveChat = chatInput || chatOutput;
|
||||
const [selectedTab, setSelectedTab] = useState(
|
||||
inputs.length > 0 ? 1 : outputs.length > 0 ? 2 : 0
|
||||
);
|
||||
const [selectedViewField, setSelectedViewField] = useState<
|
||||
{ type: string; id: string } | undefined
|
||||
>(undefined);
|
||||
|
||||
const buildFlow = useFlowStore((state) => state.buildFlow);
|
||||
const setIsBuilding = useFlowStore((state) => state.setIsBuilding);
|
||||
const [lockChat, setLockChat] = useState(false);
|
||||
const [chatValue, setChatValue] = useState("");
|
||||
const isBuilding = useFlowStore((state) => state.isBuilding);
|
||||
const currentFlow = useFlowsManagerStore((state) => state.currentFlow);
|
||||
const setNode = useFlowStore((state) => state.setNode);
|
||||
|
||||
async function updateVertices() {
|
||||
return updateVerticesOrder(currentFlow!.id, null);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
updateVertices();
|
||||
}
|
||||
}, [open, currentFlow]);
|
||||
|
||||
async function sendMessage(count = 1): Promise<void> {
|
||||
if (isBuilding) return;
|
||||
setIsBuilding(true);
|
||||
setLockChat(true);
|
||||
setChatValue("");
|
||||
for (let i = 0; i < count; i++) {
|
||||
await buildFlow({
|
||||
input_value: chatValue,
|
||||
startNodeId: chatInput?.id,
|
||||
}).catch((err) => {
|
||||
console.error(err);
|
||||
setLockChat(false);
|
||||
});
|
||||
}
|
||||
setLockChat(false);
|
||||
if (chatInput) {
|
||||
setNode(chatInput.id, (node: NodeType) => {
|
||||
const newNode = { ...node };
|
||||
newNode.data.node!.template["input_value"].value = chatValue;
|
||||
return newNode;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedViewField(undefined);
|
||||
setSelectedTab(inputs.length > 0 ? 1 : outputs.length > 0 ? 2 : 0);
|
||||
}, [inputs.length, outputs.length]);
|
||||
|
||||
return (
|
||||
<BaseModal
|
||||
size={haveChat ? (selectedTab === 0 ? "large-thin" : "large") : "small"}
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
disable={disable}
|
||||
>
|
||||
<BaseModal.Trigger>{children}</BaseModal.Trigger>
|
||||
{/* TODO ADAPT TO ALL TYPES OF INPUTS AND OUTPUTS */}
|
||||
<BaseModal.Header description={CHAT_FORM_DIALOG_SUBTITLE}>
|
||||
<div className="flex items-center">
|
||||
<span className="pr-2">Chat</span>
|
||||
<IconComponent
|
||||
name="prompts"
|
||||
className="h-6 w-6 pl-1 text-foreground"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
</BaseModal.Header>
|
||||
<BaseModal.Content>
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="flex-max-width mt-2 h-full">
|
||||
{selectedTab !== 0 && (
|
||||
<div
|
||||
className={cn(
|
||||
"mr-6 flex h-full w-2/6 flex-shrink-0 flex-col justify-start",
|
||||
haveChat ? "w-2/6" : "w-full"
|
||||
)}
|
||||
>
|
||||
<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>
|
||||
|
||||
<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">
|
||||
<Badge variant="gray" size="md">
|
||||
{node.data.node.display_name}
|
||||
</Badge>
|
||||
{haveChat && (
|
||||
<div
|
||||
className="-mb-1 pr-4"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
setSelectedViewField(input);
|
||||
}}
|
||||
>
|
||||
<IconComponent
|
||||
className="h-4 w-4"
|
||||
name="ExternalLink"
|
||||
></IconComponent>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
key={index}
|
||||
keyValue={input.id}
|
||||
>
|
||||
<div className="file-component-tab-column">
|
||||
<div className="">
|
||||
{input && (
|
||||
<IOInputField
|
||||
left={true}
|
||||
inputType={input.type}
|
||||
inputId={input.id}
|
||||
/>
|
||||
)}
|
||||
</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={"FileType2"} />
|
||||
{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}
|
||||
>
|
||||
<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>
|
||||
</ShadTooltip>
|
||||
{haveChat && (
|
||||
<div
|
||||
className="-mb-1 pr-4"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
setSelectedViewField(output);
|
||||
}}
|
||||
>
|
||||
<IconComponent
|
||||
className="h-4 w-4"
|
||||
name="ExternalLink"
|
||||
></IconComponent>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
key={index}
|
||||
keyValue={output.id}
|
||||
>
|
||||
<div className="file-component-tab-column">
|
||||
<div className="">
|
||||
{output && (
|
||||
<IOOutputView
|
||||
left={true}
|
||||
outputType={output.type}
|
||||
outputId={output.id}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</AccordionComponent>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{haveChat ? (
|
||||
<div className="flex h-full min-w-96 flex-grow">
|
||||
{selectedViewField && (
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-full w-full flex-col items-start gap-4 p-4",
|
||||
!selectedViewField ? "hidden" : ""
|
||||
)}
|
||||
>
|
||||
<div className="font-xl flex items-center justify-center gap-3 font-semibold">
|
||||
<button onClick={() => setSelectedViewField(undefined)}>
|
||||
<IconComponent
|
||||
name={"ArrowLeft"}
|
||||
className="h-6 w-6"
|
||||
></IconComponent>
|
||||
</button>
|
||||
{selectedViewField.type}
|
||||
</div>
|
||||
<div className="h-full w-full">
|
||||
{inputs.some(
|
||||
(input) => input.id === selectedViewField.id
|
||||
) ? (
|
||||
<IOInputField
|
||||
left={false}
|
||||
inputType={selectedViewField.type!}
|
||||
inputId={selectedViewField.id!}
|
||||
/>
|
||||
) : (
|
||||
<IOOutputView
|
||||
left={false}
|
||||
outputType={selectedViewField.type!}
|
||||
outputId={selectedViewField.id!}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-full w-full",
|
||||
selectedViewField ? "hidden" : ""
|
||||
)}
|
||||
>
|
||||
<NewChatView
|
||||
sendMessage={sendMessage}
|
||||
chatValue={chatValue}
|
||||
setChatValue={setChatValue}
|
||||
lockChat={lockChat}
|
||||
setLockChat={setLockChat}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="absolute bottom-8 right-8"></div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</BaseModal.Content>
|
||||
<BaseModal.Footer>
|
||||
{!haveChat && (
|
||||
<div className="flex w-full justify-end pt-6">
|
||||
<Button
|
||||
variant={"outline"}
|
||||
className="flex gap-2 px-3"
|
||||
onClick={() => sendMessage(1)}
|
||||
>
|
||||
<IconComponent
|
||||
name={isBuilding ? "Loader2" : "Play"}
|
||||
className={cn(
|
||||
"h-4 w-4",
|
||||
isBuilding
|
||||
? "animate-spin"
|
||||
: "fill-current text-medium-indigo"
|
||||
)}
|
||||
/>
|
||||
Run Flow
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</BaseModal.Footer>
|
||||
</BaseModal>
|
||||
);
|
||||
}
|
||||
26
src/frontend/src/components/NewFLowCard2/index.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { useNavigate } from "react-router-dom";
|
||||
import useFlowsManagerStore from "../../stores/flowsManagerStore";
|
||||
import { Card, CardContent, CardDescription, CardTitle } from "../ui/card";
|
||||
|
||||
export default function NewFlowCardComponent() {
|
||||
const addFlow = useFlowsManagerStore((state) => state.addFlow);
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<Card
|
||||
onClick={() => {
|
||||
addFlow(true).then((id) => {
|
||||
navigate("/flow/" + id);
|
||||
});
|
||||
}}
|
||||
className="h-64 w-80 cursor-pointer bg-background pt-4"
|
||||
data-testid="blank-flow"
|
||||
>
|
||||
<CardContent className="h-full w-full">
|
||||
<div className="flex h-full w-full flex-col items-center justify-center rounded-md bg-muted align-middle bg-dotted-spacing-6 bg-dotted-muted-foreground bg-dotted-radius-px"></div>
|
||||
</CardContent>
|
||||
<CardDescription className="px-6 pb-4">
|
||||
<CardTitle className="text-lg text-primary">Blank Flow</CardTitle>
|
||||
</CardDescription>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
33
src/frontend/src/components/NewFlowCardComponent/index.tsx
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { useNavigate } from "react-router-dom";
|
||||
import useFlowsManagerStore from "../../stores/flowsManagerStore";
|
||||
import { cn } from "../../utils/utils";
|
||||
import IconComponent from "../genericIconComponent";
|
||||
import { Card, CardContent } from "../ui/card";
|
||||
|
||||
export default function NewFlowCardComponent({}: {}) {
|
||||
const addFlow = useFlowsManagerStore((state) => state.addFlow);
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={cn(
|
||||
"group relative flex h-48 w-2/6 flex-col justify-between overflow-hidden transition-all hover:shadow-md"
|
||||
)}
|
||||
>
|
||||
<CardContent className="flex h-full w-full items-center justify-center align-middle">
|
||||
<button
|
||||
onClick={() => {
|
||||
addFlow(true).then((id) => {
|
||||
navigate("/flow/" + id);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<IconComponent
|
||||
className={cn("h-12 w-12 text-muted-foreground")}
|
||||
name="PlusCircle"
|
||||
/>
|
||||
</button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { ShadToolTipType } from "../../types/components";
|
||||
import { cn } from "../../utils/utils";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
||||
|
||||
export default function ShadTooltip({
|
||||
|
|
@ -14,7 +15,7 @@ export default function ShadTooltip({
|
|||
<TooltipTrigger asChild={asChild}>{children}</TooltipTrigger>
|
||||
|
||||
<TooltipContent
|
||||
className={styleClasses}
|
||||
className={cn(styleClasses, "max-w-96")}
|
||||
side={side}
|
||||
avoidCollisions={false}
|
||||
sticky="always"
|
||||
|
|
|
|||
31
src/frontend/src/components/ViewTriggers/chat/index.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { Transition } from "@headlessui/react";
|
||||
|
||||
import IconComponent from "../../genericIconComponent";
|
||||
|
||||
export default function ChatTrigger({}): JSX.Element {
|
||||
return (
|
||||
<Transition
|
||||
show={true}
|
||||
appear={true}
|
||||
enter="transition ease-out duration-300"
|
||||
enterFrom="translate-y-96"
|
||||
enterTo="translate-y-0"
|
||||
leave="transition ease-in duration-300"
|
||||
leaveFrom="translate-y-0"
|
||||
leaveTo="translate-y-96"
|
||||
>
|
||||
<button
|
||||
className={
|
||||
"shadow-round-btn-shadow hover:shadow-round-btn-shadow message-button cursor-pointer"
|
||||
}
|
||||
>
|
||||
<div className="flex gap-3">
|
||||
<IconComponent
|
||||
name="Zap"
|
||||
className={"message-button-icon h-6 w-6 transition-all"}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</Transition>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,12 +1,11 @@
|
|||
import { Transition } from "@headlessui/react";
|
||||
import { useState } from "react";
|
||||
import Loading from "../../../components/ui/loading";
|
||||
import { postBuildInit } from "../../../controllers/API";
|
||||
import { FlowType } from "../../../types/flow";
|
||||
|
||||
import { MISSED_ERROR_ALERT } from "../../../constants/alerts_constants";
|
||||
import useAlertStore from "../../../stores/alertStore";
|
||||
import useFlowStore from "../../../stores/flowStore";
|
||||
import { parsedDataType } from "../../../types/components";
|
||||
import { validateNodes } from "../../../utils/reactflowUtils";
|
||||
import RadialProgressComponent from "../../RadialProgress";
|
||||
import IconComponent from "../../genericIconComponent";
|
||||
|
|
@ -14,21 +13,16 @@ import IconComponent from "../../genericIconComponent";
|
|||
export default function BuildTrigger({
|
||||
open,
|
||||
flow,
|
||||
setIsBuilt,
|
||||
}: {
|
||||
open: boolean;
|
||||
flow: FlowType;
|
||||
setIsBuilt: any;
|
||||
isBuilt: boolean;
|
||||
}): JSX.Element {
|
||||
const updateSSEData = useFlowStore((state) => state.updateSSEData);
|
||||
const isBuilding = useFlowStore((state) => state.isBuilding);
|
||||
const setIsBuilding = useFlowStore((state) => state.setIsBuilding);
|
||||
const buildFlow = useFlowStore((state) => state.buildFlow);
|
||||
const nodes = useFlowStore((state) => state.nodes);
|
||||
const edges = useFlowStore((state) => state.edges);
|
||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||
const setSuccessData = useAlertStore((state) => state.setSuccessData);
|
||||
const setFlowState = useFlowStore((state) => state.setFlowState);
|
||||
|
||||
const eventClick = isBuilding ? "pointer-events-none" : "";
|
||||
const [progress, setProgress] = useState(0);
|
||||
|
|
@ -41,7 +35,7 @@ export default function BuildTrigger({
|
|||
const errors = validateNodes(nodes, edges);
|
||||
if (errors.length > 0) {
|
||||
setErrorData({
|
||||
title: "Oops! Looks like you missed something",
|
||||
title: MISSED_ERROR_ALERT,
|
||||
list: errors,
|
||||
});
|
||||
return;
|
||||
|
|
@ -50,85 +44,16 @@ export default function BuildTrigger({
|
|||
const startTime = Date.now();
|
||||
setIsBuilding(true);
|
||||
|
||||
const allNodesValid = await streamNodeData(flow);
|
||||
await enforceMinimumLoadingTime(startTime, minimumLoadingTime);
|
||||
setIsBuilt(allNodesValid);
|
||||
if (!allNodesValid) {
|
||||
setErrorData({
|
||||
title: "Oops! Looks like you missed something",
|
||||
list: [
|
||||
"Check components and retry. Hover over component status icon 🔴 to inspect.",
|
||||
],
|
||||
});
|
||||
}
|
||||
if (errors.length === 0 && allNodesValid) {
|
||||
setSuccessData({
|
||||
title: "Flow is ready to run",
|
||||
});
|
||||
}
|
||||
await buildFlow({});
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
} finally {
|
||||
setIsBuilding(false);
|
||||
}
|
||||
}
|
||||
async function streamNodeData(flow: FlowType) {
|
||||
// Step 1: Make a POST request to send the flow data and receive a unique session ID
|
||||
const response = await postBuildInit(flow);
|
||||
const { flowId } = response.data;
|
||||
// Step 2: Use the session ID to establish an SSE connection using EventSource
|
||||
let validationResults: boolean[] = [];
|
||||
const apiUrl = `/api/v1/build/stream/${flowId}`;
|
||||
return new Promise<boolean>((resolve, reject) => {
|
||||
const eventSource = new EventSource(apiUrl);
|
||||
|
||||
eventSource.onmessage = (event) => {
|
||||
// If the event is parseable, return
|
||||
if (!event.data) {
|
||||
return;
|
||||
}
|
||||
const parsedData = JSON.parse(event.data);
|
||||
// if the event is the end of the stream, close the connection
|
||||
if (parsedData.end_of_stream) {
|
||||
eventSource.close();
|
||||
resolve(validationResults.every((result) => result));
|
||||
} else if (parsedData.log) {
|
||||
// If the event is a log, log it
|
||||
setSuccessData({ title: parsedData.log });
|
||||
} else if (parsedData.input_keys !== undefined) {
|
||||
setFlowState(parsedData);
|
||||
} else {
|
||||
// Otherwise, process the data
|
||||
const isValid = processStreamResult(parsedData);
|
||||
setProgress(parsedData.progress);
|
||||
validationResults.push(isValid);
|
||||
}
|
||||
};
|
||||
|
||||
eventSource.onerror = (error: any) => {
|
||||
console.error("EventSource failed:", error);
|
||||
|
||||
if (error.data) {
|
||||
const parsedData = JSON.parse(error.data);
|
||||
setErrorData({ title: parsedData.error });
|
||||
setIsBuilding(false);
|
||||
}
|
||||
eventSource.close();
|
||||
reject(new Error("Streaming failed"));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function processStreamResult(parsedData: parsedDataType) {
|
||||
// Process each chunk of data here
|
||||
// Parse the chunk and update the context
|
||||
try {
|
||||
updateSSEData({ [parsedData.id]: parsedData });
|
||||
} catch (err) {
|
||||
console.log("Error parsing stream data: ", err);
|
||||
}
|
||||
return parsedData.valid;
|
||||
}
|
||||
const hasIO = useFlowStore((state) => state.hasIO);
|
||||
|
||||
async function enforceMinimumLoadingTime(
|
||||
startTime: number,
|
||||
|
|
@ -153,7 +78,9 @@ export default function BuildTrigger({
|
|||
leaveFrom="translate-y-0"
|
||||
leaveTo="translate-y-96"
|
||||
>
|
||||
<div className="fixed bottom-20 right-4">
|
||||
<div
|
||||
className={hasIO ? "fixed bottom-20 right-4" : "fixed bottom-4 right-4"}
|
||||
>
|
||||
<div
|
||||
className={`${eventClick} round-button-form`}
|
||||
onClick={() => {
|
||||
|
|
|
|||
|
|
@ -1,25 +1,32 @@
|
|||
import { useEffect, useRef, useState } from "react";
|
||||
import { useNodes } from "reactflow";
|
||||
import { ChatType } from "../../types/chat";
|
||||
import BuildTrigger from "./buildTrigger";
|
||||
import ChatTrigger from "./chatTrigger";
|
||||
|
||||
import * as _ from "lodash";
|
||||
import FormModal from "../../modals/formModal";
|
||||
import { Transition } from "@headlessui/react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import ApiModal from "../../modals/ApiModal";
|
||||
import ShareModal from "../../modals/shareModal";
|
||||
import useFlowStore from "../../stores/flowStore";
|
||||
import { NodeType } from "../../types/flow";
|
||||
import useFlowsManagerStore from "../../stores/flowsManagerStore";
|
||||
import { useStoreStore } from "../../stores/storeStore";
|
||||
import { ChatType } from "../../types/chat";
|
||||
import { classNames } from "../../utils/utils";
|
||||
import IOView from "../IOview";
|
||||
import ForwardedIconComponent from "../genericIconComponent";
|
||||
import { Separator } from "../ui/separator";
|
||||
|
||||
export default function Chat({ flow }: ChatType): JSX.Element {
|
||||
export default function FlowToolbar({ flow }: ChatType): JSX.Element {
|
||||
const [open, setOpen] = useState(false);
|
||||
const isBuilt = useFlowStore((state) => state.isBuilt);
|
||||
const setIsBuilt = useFlowStore((state) => state.setIsBuilt);
|
||||
const flowState = useFlowStore((state) => state.flowState);
|
||||
const nodes = useFlowStore((state) => state.nodes);
|
||||
const hasIO = useFlowStore((state) => state.hasIO);
|
||||
const hasStore = useStoreStore((state) => state.hasStore);
|
||||
const validApiKey = useStoreStore((state) => state.validApiKey);
|
||||
const currentFlow = useFlowsManagerStore((state) => state.currentFlow);
|
||||
const hasApiKey = useStoreStore((state) => state.hasApiKey);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (
|
||||
(event.key === "K" || event.key === "k") &&
|
||||
(event.metaKey || event.ctrlKey) &&
|
||||
isBuilt
|
||||
useFlowStore.getState().hasIO
|
||||
) {
|
||||
event.preventDefault();
|
||||
setOpen((oldState) => !oldState);
|
||||
|
|
@ -29,40 +36,122 @@ export default function Chat({ flow }: ChatType): JSX.Element {
|
|||
return () => {
|
||||
document.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [isBuilt]);
|
||||
}, []);
|
||||
|
||||
const prevNodesRef = useRef<any[] | undefined>();
|
||||
const nodes: NodeType[] = useNodes();
|
||||
useEffect(() => {
|
||||
const prevNodes = prevNodesRef.current;
|
||||
const currentNodes = nodes.map((node: NodeType) =>
|
||||
_.cloneDeep(node.data.node?.template)
|
||||
);
|
||||
if (JSON.stringify(prevNodes) !== JSON.stringify(currentNodes)) {
|
||||
setIsBuilt(false);
|
||||
}
|
||||
prevNodesRef.current = currentNodes;
|
||||
}, [flowState, flow.id]);
|
||||
|
||||
const ModalMemo = useMemo(
|
||||
() => (
|
||||
<ShareModal
|
||||
is_component={false}
|
||||
component={currentFlow!}
|
||||
disabled={!hasApiKey || !validApiKey || !hasStore}
|
||||
>
|
||||
<button
|
||||
disabled={!hasApiKey || !validApiKey || !hasStore}
|
||||
className={classNames(
|
||||
"relative inline-flex h-full w-full items-center justify-center gap-[4px] bg-muted px-5 py-3 text-sm font-semibold text-foreground transition-all duration-500 ease-in-out hover:bg-background hover:bg-hover ",
|
||||
!hasApiKey || !validApiKey || !hasStore
|
||||
? " button-disable text-muted-foreground "
|
||||
: ""
|
||||
)}
|
||||
>
|
||||
<ForwardedIconComponent
|
||||
name="Share3"
|
||||
className={classNames(
|
||||
"-m-0.5 -ml-1 h-6 w-6",
|
||||
!hasApiKey || !validApiKey || !hasStore
|
||||
? "extra-side-bar-save-disable"
|
||||
: ""
|
||||
)}
|
||||
/>
|
||||
Share
|
||||
</button>
|
||||
</ShareModal>
|
||||
),
|
||||
[hasApiKey, validApiKey, currentFlow, hasStore]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<BuildTrigger
|
||||
open={open}
|
||||
flow={flow}
|
||||
setIsBuilt={setIsBuilt}
|
||||
isBuilt={isBuilt}
|
||||
/>
|
||||
{isBuilt && flowState && !!flowState?.input_keys && (
|
||||
<FormModal key={flow.id} flow={flow} open={open} setOpen={setOpen} />
|
||||
)}
|
||||
<ChatTrigger
|
||||
canOpen={!!flowState?.input_keys}
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
isBuilt={isBuilt}
|
||||
/>
|
||||
</div>
|
||||
<Transition
|
||||
show={true}
|
||||
appear={true}
|
||||
enter="transition ease-out duration-300"
|
||||
enterFrom="translate-y-96"
|
||||
enterTo="translate-y-0"
|
||||
leave="transition ease-in duration-300"
|
||||
leaveFrom="translate-y-0"
|
||||
leaveTo="translate-y-96"
|
||||
>
|
||||
<div
|
||||
className={
|
||||
"shadow-round-btn-shadow hover:shadow-round-btn-shadow message-button-position flex items-center justify-center gap-7 rounded-sm border bg-muted shadow-md transition-all"
|
||||
}
|
||||
>
|
||||
<div className="flex">
|
||||
<div className="flex h-full w-full gap-1 rounded-sm text-medium-indigo transition-all">
|
||||
{hasIO ? (
|
||||
<IOView open={open} setOpen={setOpen} disable={!hasIO}>
|
||||
<div className="relative inline-flex w-full items-center justify-center gap-1 px-5 py-3 text-sm font-semibold text-medium-indigo transition-all transition-all duration-500 ease-in-out ease-in-out hover:bg-hover">
|
||||
<ForwardedIconComponent
|
||||
name="Zap"
|
||||
className={"message-button-icon h-5 w-5 transition-all"}
|
||||
/>
|
||||
Run
|
||||
</div>
|
||||
</IOView>
|
||||
) : (
|
||||
<div
|
||||
className={`relative inline-flex w-full cursor-not-allowed items-center justify-center gap-1 px-5 py-3 text-sm font-semibold text-muted-foreground transition-all duration-500 ease-in-out ease-in-out`}
|
||||
>
|
||||
<ForwardedIconComponent
|
||||
name="Zap"
|
||||
className={
|
||||
"message-button-icon h-5 w-5 fill-muted-foreground stroke-muted-foreground transition-all"
|
||||
}
|
||||
/>
|
||||
Run
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Separator orientation="vertical" />
|
||||
</div>
|
||||
<div className="flex cursor-pointer items-center gap-2">
|
||||
{currentFlow && currentFlow.data && (
|
||||
<ApiModal flow={currentFlow}>
|
||||
<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-500 ease-in-out hover:bg-hover"
|
||||
)}
|
||||
>
|
||||
<ForwardedIconComponent
|
||||
name="Code2"
|
||||
className={" h-5 w-5"}
|
||||
/>
|
||||
API
|
||||
</div>
|
||||
</ApiModal>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Separator orientation="vertical" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className={`side-bar-button ${
|
||||
!hasApiKey || !validApiKey || !hasStore
|
||||
? " cursor-not-allowed"
|
||||
: " cursor-pointer"
|
||||
}`}
|
||||
>
|
||||
{ModalMemo}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ export default function CodeAreaComponent({
|
|||
setNodeClass,
|
||||
id = "",
|
||||
readonly = false,
|
||||
open,
|
||||
setOpen,
|
||||
}: CodeAreaComponentType) {
|
||||
const [myValue, setMyValue] = useState(
|
||||
typeof value == "string" ? value : JSON.stringify(value)
|
||||
|
|
@ -32,6 +34,8 @@ export default function CodeAreaComponent({
|
|||
return (
|
||||
<div className={disabled ? "pointer-events-none w-full " : " w-full"}>
|
||||
<CodeAreaModal
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
readonly={readonly}
|
||||
dynamic={dynamic}
|
||||
value={myValue}
|
||||
|
|
|
|||
|
|
@ -59,8 +59,6 @@ export default function CodeTabsComponent({
|
|||
const globalVariablesEntries = useGlobalVariablesStore(
|
||||
(state) => state.globalVariablesEntries
|
||||
);
|
||||
|
||||
const setNodes = useFlowStore((state) => state.setNodes);
|
||||
const setNoticeData = useAlertStore((state) => state.setNoticeData);
|
||||
|
||||
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
|
||||
|
|
@ -132,7 +130,7 @@ export default function CodeTabsComponent({
|
|||
<Tabs
|
||||
value={activeTab}
|
||||
className={
|
||||
"api-modal-tabs " +
|
||||
"api-modal-tabs inset-0 m-0 " +
|
||||
(isMessage ? "dark " : "") +
|
||||
(dark && isMessage ? "bg-background" : "")
|
||||
}
|
||||
|
|
@ -172,7 +170,7 @@ export default function CodeTabsComponent({
|
|||
) : (
|
||||
<IconComponent name="Clipboard" className="h-4 w-4" />
|
||||
)}
|
||||
{isCopied ? "Copied!" : "Copy code"}
|
||||
{isCopied ? "Copied!" : "Copy Code"}
|
||||
</button>
|
||||
<button
|
||||
className="flex items-center gap-1.5 rounded bg-none p-1 text-xs text-gray-500 dark:text-gray-300"
|
||||
|
|
@ -187,7 +185,7 @@ export default function CodeTabsComponent({
|
|||
{tabs.map((tab, idx) => (
|
||||
<TabsContent
|
||||
value={idx.toString()}
|
||||
className="api-modal-tabs-content"
|
||||
className="api-modal-tabs-content overflow-hidden"
|
||||
key={idx} // Remember to add a unique key prop
|
||||
>
|
||||
{idx < 4 ? (
|
||||
|
|
@ -199,9 +197,9 @@ export default function CodeTabsComponent({
|
|||
></div>
|
||||
)}
|
||||
<SyntaxHighlighter
|
||||
className="mt-0 h-full w-full overflow-auto custom-scroll"
|
||||
language={tab.mode}
|
||||
language={tab.language}
|
||||
style={oneDark}
|
||||
className="mt-0 h-full overflow-auto rounded-sm text-left custom-scroll"
|
||||
>
|
||||
{tab.code}
|
||||
</SyntaxHighlighter>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import { classNames } from "../../utils/utils";
|
|||
import IconComponent from "../genericIconComponent";
|
||||
|
||||
export default function Dropdown({
|
||||
disabled,
|
||||
isLoading,
|
||||
value,
|
||||
options,
|
||||
onSelect,
|
||||
|
|
@ -27,6 +29,7 @@ export default function Dropdown({
|
|||
<>
|
||||
<Listbox
|
||||
value={internalValue}
|
||||
disabled={disabled}
|
||||
onChange={(value) => {
|
||||
setInternalValue(value);
|
||||
onSelect(value);
|
||||
|
|
@ -129,11 +132,17 @@ export default function Dropdown({
|
|||
</>
|
||||
) : (
|
||||
<>
|
||||
<div>
|
||||
<span className="text-sm italic">
|
||||
No parameters are available for display.
|
||||
</span>
|
||||
</div>
|
||||
{(!isLoading && (
|
||||
<div>
|
||||
<span className="text-sm italic">
|
||||
No parameters are available for display.
|
||||
</span>
|
||||
</div>
|
||||
)) || (
|
||||
<div>
|
||||
<span className="text-sm italic">Loading...</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
|
|
|||
100
src/frontend/src/components/exampleComponent/index.tsx
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import { useNavigate } from "react-router-dom";
|
||||
import useFlowsManagerStore from "../../stores/flowsManagerStore";
|
||||
import { FlowType } from "../../types/flow";
|
||||
import { updateIds } from "../../utils/reactflowUtils";
|
||||
import { cn } from "../../utils/utils";
|
||||
import ShadTooltip from "../ShadTooltipComponent";
|
||||
import IconComponent from "../genericIconComponent";
|
||||
import { Button } from "../ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "../ui/card";
|
||||
|
||||
export default function CollectionCardComponent({
|
||||
flow,
|
||||
}: {
|
||||
flow: FlowType;
|
||||
authorized?: boolean;
|
||||
}) {
|
||||
const addFlow = useFlowsManagerStore((state) => state.addFlow);
|
||||
const navigate = useNavigate();
|
||||
const emojiRegex = /\p{Emoji}/u;
|
||||
const isEmoji = (str: string) => emojiRegex.test(str);
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={cn(
|
||||
"group relative flex h-48 w-2/6 flex-col justify-between overflow-hidden transition-all hover:shadow-md"
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
<CardHeader>
|
||||
<div>
|
||||
<CardTitle className="flex w-full items-center justify-between gap-3 text-xl">
|
||||
{flow.icon && isEmoji(flow.icon) && (
|
||||
<div
|
||||
className="flex items-center justify-center rounded-md p-2 align-middle"
|
||||
style={{ backgroundColor: flow.icon_bg_color }}
|
||||
>
|
||||
<div className="h-7 w-7 pl-0.5">{flow.icon}</div>
|
||||
</div>
|
||||
)}
|
||||
{(!flow.icon || !isEmoji(flow.icon)) && (
|
||||
<div
|
||||
className="flex items-center justify-center rounded-md p-2 align-middle"
|
||||
style={{ backgroundColor: flow.icon_bg_color }}
|
||||
>
|
||||
<IconComponent
|
||||
className={cn("h-7 w-7 flex-shrink-0 text-flow-icon")}
|
||||
name={flow.icon || "Group"}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<ShadTooltip content={flow.name}>
|
||||
<div className="w-full truncate">{flow.name}</div>
|
||||
</ShadTooltip>
|
||||
</CardTitle>
|
||||
</div>
|
||||
<CardDescription className="pb-2 pt-2">
|
||||
<ShadTooltip
|
||||
side="bottom"
|
||||
styleClasses="z-50"
|
||||
content={flow.description}
|
||||
>
|
||||
<div className="truncate-doubleline">{flow.description}</div>
|
||||
</ShadTooltip>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
</div>
|
||||
|
||||
<CardFooter>
|
||||
<div className="flex w-full items-center justify-between gap-2">
|
||||
<div className="flex w-full flex-wrap justify-end gap-2">
|
||||
<Button
|
||||
onClick={() => {
|
||||
updateIds(flow.data!);
|
||||
addFlow(true, flow).then((id) => {
|
||||
navigate("/flow/" + id);
|
||||
});
|
||||
}}
|
||||
tabIndex={-1}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="whitespace-nowrap "
|
||||
>
|
||||
<IconComponent
|
||||
name="ExternalLink"
|
||||
className="main-page-nav-button select-none"
|
||||
/>
|
||||
Select Flow
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,36 +1,53 @@
|
|||
import { forwardRef } from "react";
|
||||
import dynamicIconImports from "lucide-react/dynamicIconImports";
|
||||
import { Suspense, forwardRef, lazy, memo } from "react";
|
||||
import { IconComponentProps } from "../../types/components";
|
||||
import { nodeIconsLucide } from "../../utils/styleUtils";
|
||||
|
||||
const ForwardedIconComponent = forwardRef(
|
||||
(
|
||||
{
|
||||
name,
|
||||
className,
|
||||
iconColor,
|
||||
stroke,
|
||||
strokeWidth,
|
||||
id = "",
|
||||
}: IconComponentProps,
|
||||
ref
|
||||
) => {
|
||||
const TargetIcon = nodeIconsLucide[name] ?? nodeIconsLucide["unknown"];
|
||||
const ForwardedIconComponent = memo(
|
||||
forwardRef(
|
||||
(
|
||||
{
|
||||
name,
|
||||
className,
|
||||
iconColor,
|
||||
stroke,
|
||||
strokeWidth,
|
||||
id = "",
|
||||
}: IconComponentProps,
|
||||
ref
|
||||
) => {
|
||||
let TargetIcon = nodeIconsLucide[name];
|
||||
if (!TargetIcon) {
|
||||
// check if name exists in dynamicIconImports
|
||||
if (!dynamicIconImports[name]) {
|
||||
TargetIcon = nodeIconsLucide["unknown"];
|
||||
} else TargetIcon = lazy(dynamicIconImports[name]);
|
||||
}
|
||||
|
||||
const style = {
|
||||
strokeWidth: strokeWidth ?? 1.5,
|
||||
...(stroke && { stroke: stroke }),
|
||||
...(iconColor && { color: iconColor, stroke: stroke }),
|
||||
};
|
||||
const style = {
|
||||
strokeWidth: strokeWidth ?? 1.5,
|
||||
...(stroke && { stroke: stroke }),
|
||||
...(iconColor && { color: iconColor, stroke: stroke }),
|
||||
};
|
||||
|
||||
return (
|
||||
<TargetIcon
|
||||
className={className}
|
||||
style={style}
|
||||
ref={ref}
|
||||
data-testid={id ? `${id}-${name}` : `icon-${name}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (!TargetIcon) {
|
||||
return null; // Render nothing until the icon is loaded
|
||||
}
|
||||
const fallback = (
|
||||
<div style={{ background: "#ddd", width: 24, height: 24 }} />
|
||||
);
|
||||
return (
|
||||
<Suspense fallback={fallback}>
|
||||
<TargetIcon
|
||||
className={className}
|
||||
style={style}
|
||||
ref={ref}
|
||||
data-testid={id ? `${id}-${name}` : `icon-${name}`}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
export default ForwardedIconComponent;
|
||||
|
|
|
|||
|
|
@ -8,37 +8,60 @@ import {
|
|||
} from "../../../ui/dropdown-menu";
|
||||
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Node } from "reactflow";
|
||||
import { UPLOAD_ERROR_ALERT } from "../../../../constants/alerts_constants";
|
||||
import { SAVED_HOVER } from "../../../../constants/constants";
|
||||
import ExportModal from "../../../../modals/exportModal";
|
||||
import FlowSettingsModal from "../../../../modals/flowSettingsModal";
|
||||
import useAlertStore from "../../../../stores/alertStore";
|
||||
import useFlowStore from "../../../../stores/flowStore";
|
||||
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
|
||||
import { cn } from "../../../../utils/utils";
|
||||
import ShadTooltip from "../../../ShadTooltipComponent";
|
||||
import IconComponent from "../../../genericIconComponent";
|
||||
import { Button } from "../../../ui/button";
|
||||
|
||||
export const MenuBar = (): JSX.Element => {
|
||||
export const MenuBar = ({
|
||||
removeFunction,
|
||||
}: {
|
||||
removeFunction: (nodes: Node[]) => void;
|
||||
}): JSX.Element => {
|
||||
const addFlow = useFlowsManagerStore((state) => state.addFlow);
|
||||
const currentFlow = useFlowsManagerStore((state) => state.currentFlow);
|
||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||
const undo = useFlowsManagerStore((state) => state.undo);
|
||||
const redo = useFlowsManagerStore((state) => state.redo);
|
||||
const saveLoading = useFlowsManagerStore((state) => state.saveLoading);
|
||||
const [openSettings, setOpenSettings] = useState(false);
|
||||
|
||||
const nodes = useFlowStore((state) => state.nodes);
|
||||
const uploadFlow = useFlowsManagerStore((state) => state.uploadFlow);
|
||||
const navigate = useNavigate();
|
||||
const isBuilding = useFlowStore((state) => state.isBuilding);
|
||||
|
||||
function handleAddFlow() {
|
||||
try {
|
||||
addFlow(true).then((id) => {
|
||||
navigate("/flow/" + id);
|
||||
});
|
||||
// saveFlowStyleInDataBase();
|
||||
} catch (err) {
|
||||
setErrorData(err as { title: string; list?: Array<string> });
|
||||
}
|
||||
}
|
||||
|
||||
function printByBuildStatus() {
|
||||
if (isBuilding) {
|
||||
return "Building...";
|
||||
} else if (saveLoading) {
|
||||
return "Saving...";
|
||||
}
|
||||
return "Saved";
|
||||
}
|
||||
|
||||
return currentFlow ? (
|
||||
<div className="round-button-div">
|
||||
<button
|
||||
onClick={() => {
|
||||
removeFunction(nodes);
|
||||
navigate(-1);
|
||||
}}
|
||||
>
|
||||
|
|
@ -78,7 +101,31 @@ export const MenuBar = (): JSX.Element => {
|
|||
/>
|
||||
Settings
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
uploadFlow({ newProject: false, isComponent: false }).catch(
|
||||
(error) => {
|
||||
setErrorData({
|
||||
title: UPLOAD_ERROR_ALERT,
|
||||
list: [error],
|
||||
});
|
||||
}
|
||||
);
|
||||
}}
|
||||
>
|
||||
<IconComponent name="FileUp" className="header-menu-options " />
|
||||
Import
|
||||
</DropdownMenuItem>
|
||||
<ExportModal>
|
||||
<div className="header-menubar-item">
|
||||
<IconComponent
|
||||
name="FileDown"
|
||||
className="header-menu-options"
|
||||
/>
|
||||
Export
|
||||
</div>
|
||||
</ExportModal>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
undo();
|
||||
|
|
@ -87,6 +134,17 @@ export const MenuBar = (): JSX.Element => {
|
|||
>
|
||||
<IconComponent name="Undo" className="header-menu-options " />
|
||||
Undo
|
||||
{navigator.userAgent.toUpperCase().includes("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 +{" "}
|
||||
</span>
|
||||
)}
|
||||
<span className="absolute right-2 top-[0.4em]">Z</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
|
|
@ -96,6 +154,17 @@ export const MenuBar = (): JSX.Element => {
|
|||
>
|
||||
<IconComponent name="Redo" className="header-menu-options " />
|
||||
Redo
|
||||
{navigator.userAgent.toUpperCase().includes("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 +{" "}
|
||||
</span>
|
||||
)}
|
||||
<span className="absolute right-2 top-[0.4em]">Y</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
|
@ -104,6 +173,29 @@ export const MenuBar = (): JSX.Element => {
|
|||
setOpen={setOpenSettings}
|
||||
></FlowSettingsModal>
|
||||
</div>
|
||||
<ShadTooltip
|
||||
content={
|
||||
SAVED_HOVER +
|
||||
new Date(currentFlow.updated_at ?? "").toLocaleString("en-US", {
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
second: "numeric",
|
||||
})
|
||||
}
|
||||
side="bottom"
|
||||
styleClasses="cursor-default"
|
||||
>
|
||||
<div className="flex cursor-default items-center gap-1.5 text-sm text-muted-foreground">
|
||||
<IconComponent
|
||||
name={isBuilding || saveLoading ? "Loader2" : "CheckCircle2"}
|
||||
className={cn(
|
||||
"h-4 w-4",
|
||||
isBuilding || saveLoading ? "animate-spin" : "animate-wiggle"
|
||||
)}
|
||||
/>
|
||||
{printByBuildStatus()}
|
||||
</div>
|
||||
</ShadTooltip>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
import { useContext, useEffect } from "react";
|
||||
import { FaDiscord, FaGithub, FaTwitter } from "react-icons/fa";
|
||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||
import { Link, useLocation, useNavigate, useParams } from "react-router-dom";
|
||||
import AlertDropdown from "../../alerts/alertDropDown";
|
||||
import { USER_PROJECTS_HEADER } from "../../constants/constants";
|
||||
import { AuthContext } from "../../contexts/authContext";
|
||||
|
||||
import { Node } from "reactflow";
|
||||
import useAlertStore from "../../stores/alertStore";
|
||||
import { useDarkStore } from "../../stores/darkStore";
|
||||
import useFlowStore from "../../stores/flowStore";
|
||||
import useFlowsManagerStore from "../../stores/flowsManagerStore";
|
||||
import { useStoreStore } from "../../stores/storeStore";
|
||||
import { gradients } from "../../utils/styleUtils";
|
||||
import IconComponent from "../genericIconComponent";
|
||||
|
|
@ -27,8 +30,10 @@ export default function Header(): JSX.Element {
|
|||
const location = useLocation();
|
||||
const { logout, autoLogin, isAdmin, userData } = useContext(AuthContext);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const removeFlow = useFlowsManagerStore((store) => store.removeFlow);
|
||||
const hasStore = useStoreStore((state) => state.hasStore);
|
||||
const { id } = useParams();
|
||||
const nodes = useFlowStore((state) => state.nodes);
|
||||
|
||||
const dark = useDarkStore((state) => state.dark);
|
||||
const setDark = useDarkStore((state) => state.setDark);
|
||||
|
|
@ -43,13 +48,19 @@ export default function Header(): JSX.Element {
|
|||
window.localStorage.setItem("isDark", dark.toString());
|
||||
}, [dark]);
|
||||
|
||||
async function checkForChanges(nodes: Node[]): Promise<void> {
|
||||
if (nodes.length === 0) {
|
||||
await removeFlow(id!);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="header-arrangement">
|
||||
<div className="header-start-display lg:w-[30%]">
|
||||
<Link to="/">
|
||||
<Link to="/" onClick={() => checkForChanges(nodes)}>
|
||||
<span className="ml-4 text-2xl">⛓️</span>
|
||||
</Link>
|
||||
<MenuBar />
|
||||
<MenuBar removeFunction={checkForChanges} />
|
||||
</div>
|
||||
<div className="round-button-div">
|
||||
<Link to="/">
|
||||
|
|
@ -62,6 +73,9 @@ export default function Header(): JSX.Element {
|
|||
: "secondary"
|
||||
}
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
checkForChanges(nodes);
|
||||
}}
|
||||
>
|
||||
<IconComponent name="Home" className="h-4 w-4" />
|
||||
<div className="hidden flex-1 md:block">{USER_PROJECTS_HEADER}</div>
|
||||
|
|
@ -85,6 +99,9 @@ export default function Header(): JSX.Element {
|
|||
className="gap-2"
|
||||
variant={location.pathname === "/store" ? "primary" : "secondary"}
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
checkForChanges(nodes);
|
||||
}}
|
||||
>
|
||||
<IconComponent name="Store" className="h-4 w-4" />
|
||||
<div className="flex-1">Store</div>
|
||||
|
|
@ -105,7 +122,7 @@ export default function Header(): JSX.Element {
|
|||
<div className="header-github-display">{stars ?? 0}</div>
|
||||
</a>
|
||||
<a
|
||||
href="https://twitter.com/logspace_ai"
|
||||
href="https://twitter.com/langflow_ai"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-muted-foreground"
|
||||
|
|
|
|||
|
|
@ -107,7 +107,8 @@ export default function InputComponent({
|
|||
handleKeyDown(e, value, "");
|
||||
if (blurOnEnter && e.key === "Enter") refInput.current?.blur();
|
||||
}}
|
||||
/>
|
||||
data-testid={editNode ? id + "-edit" : id}
|
||||
/>
|
||||
<Listbox
|
||||
onChange={(val) => {
|
||||
onChange(val);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
CONSOLE_ERROR_MSG,
|
||||
CONSOLE_SUCCESS_MSG,
|
||||
INVALID_FILE_ALERT,
|
||||
} from "../../constants/alerts_constants";
|
||||
import { uploadFile } from "../../controllers/API";
|
||||
import useAlertStore from "../../stores/alertStore";
|
||||
import useFlowsManagerStore from "../../stores/flowsManagerStore";
|
||||
|
|
@ -61,7 +66,7 @@ export default function InputFileComponent({
|
|||
uploadFile(file, currentFlowId)
|
||||
.then((res) => res.data)
|
||||
.then((data) => {
|
||||
console.log("File uploaded successfully");
|
||||
console.log(CONSOLE_SUCCESS_MSG);
|
||||
// Get the file name from the response
|
||||
const { file_path } = data;
|
||||
console.log("File name:", file_path);
|
||||
|
|
@ -75,14 +80,13 @@ export default function InputFileComponent({
|
|||
setLoading(false);
|
||||
})
|
||||
.catch(() => {
|
||||
console.error("Error occurred while uploading file");
|
||||
console.error(CONSOLE_ERROR_MSG);
|
||||
setLoading(false);
|
||||
});
|
||||
} else {
|
||||
// Show an error if the file type is not allowed
|
||||
setErrorData({
|
||||
title:
|
||||
"Please select a valid file. Only these file types are allowed:",
|
||||
title: INVALID_FILE_ALERT,
|
||||
list: fileTypes,
|
||||
});
|
||||
setLoading(false);
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ export default function IntComponent({
|
|||
onChange={(event) => {
|
||||
onChange(event.target.value);
|
||||
}}
|
||||
data-testid={id}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -12,10 +12,11 @@ export default function KeypairListComponent({
|
|||
disabled,
|
||||
editNode = false,
|
||||
duplicateKey,
|
||||
isList = true,
|
||||
}: KeyPairListComponentType): JSX.Element {
|
||||
useEffect(() => {
|
||||
if (disabled && value.length > 0 && value[0] !== "") {
|
||||
onChange([""]);
|
||||
onChange([{ "": "" }]);
|
||||
}
|
||||
}, [disabled]);
|
||||
|
||||
|
|
@ -79,6 +80,7 @@ export default function KeypairListComponent({
|
|||
: "keypair" + (index + 100).toString()
|
||||
}
|
||||
type="text"
|
||||
disabled={disabled}
|
||||
value={obj[key]}
|
||||
className={editNode ? "input-edit-node" : ""}
|
||||
placeholder="Type a value..."
|
||||
|
|
@ -87,8 +89,9 @@ export default function KeypairListComponent({
|
|||
}
|
||||
/>
|
||||
|
||||
{index === ref.current.length - 1 ? (
|
||||
{isList && index === ref.current.length - 1 ? (
|
||||
<button
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
let newInputList = _.cloneDeep(ref.current);
|
||||
newInputList.push({ "": "" });
|
||||
|
|
@ -110,7 +113,7 @@ export default function KeypairListComponent({
|
|||
className={"h-4 w-4 hover:text-accent-foreground"}
|
||||
/>
|
||||
</button>
|
||||
) : (
|
||||
) : isList ? (
|
||||
<button
|
||||
onClick={() => {
|
||||
let newInputList = _.cloneDeep(ref.current);
|
||||
|
|
@ -133,6 +136,8 @@ export default function KeypairListComponent({
|
|||
className="h-4 w-4 hover:text-status-red"
|
||||
/>
|
||||
</button>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
148
src/frontend/src/components/newChatView/chatInput/index.tsx
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import IconComponent from "../../../components/genericIconComponent";
|
||||
import { Textarea } from "../../../components/ui/textarea";
|
||||
import {
|
||||
CHAT_INPUT_PLACEHOLDER,
|
||||
CHAT_INPUT_PLACEHOLDER_SEND,
|
||||
} from "../../../constants/constants";
|
||||
import useFlowsManagerStore from "../../../stores/flowsManagerStore";
|
||||
import { chatInputType } from "../../../types/components";
|
||||
import { classNames } from "../../../utils/utils";
|
||||
|
||||
export default function ChatInput({
|
||||
lockChat,
|
||||
chatValue,
|
||||
sendMessage,
|
||||
setChatValue,
|
||||
inputRef,
|
||||
noInput,
|
||||
}: chatInputType): JSX.Element {
|
||||
const [repeat, setRepeat] = useState(1);
|
||||
const saveLoading = useFlowsManagerStore((state) => state.saveLoading);
|
||||
useEffect(() => {
|
||||
if (!lockChat && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [lockChat, inputRef]);
|
||||
|
||||
/* function handleChange(value: number) {
|
||||
console.log(value);
|
||||
if (value > 0) {
|
||||
setRepeat(value);
|
||||
} else {
|
||||
setRepeat(1);
|
||||
}
|
||||
} */
|
||||
|
||||
useEffect(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.style.height = "inherit"; // Reset the height
|
||||
inputRef.current.style.height = `${inputRef.current.scrollHeight}px`; // Set it to the scrollHeight
|
||||
}
|
||||
}, [chatValue]);
|
||||
|
||||
return (
|
||||
<div className="flex w-full gap-2">
|
||||
<div className="relative w-full">
|
||||
<Textarea
|
||||
onKeyDown={(event) => {
|
||||
if (
|
||||
event.key === "Enter" &&
|
||||
!lockChat &&
|
||||
!saveLoading &&
|
||||
!event.shiftKey
|
||||
) {
|
||||
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
|
||||
}
|
||||
/>
|
||||
<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="Play"
|
||||
className="form-modal-play-icon"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : (
|
||||
<IconComponent
|
||||
name="LucideSend"
|
||||
className="form-modal-send-icon "
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
</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));
|
||||
}}
|
||||
className="w-16"
|
||||
type="number"
|
||||
min={0}
|
||||
/>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover> */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
import { IconCheck, IconClipboard, IconDownload } from "@tabler/icons-react";
|
||||
import { useState } from "react";
|
||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||
import { oneDark } from "react-syntax-highlighter/dist/cjs/styles/prism";
|
||||
import { programmingLanguages } from "../../../../constants/constants";
|
||||
import { Props } from "../../../../types/components";
|
||||
|
||||
export function CodeBlock({ language, value }: Props): JSX.Element {
|
||||
const [isCopied, setIsCopied] = useState<Boolean>(false);
|
||||
|
||||
const copyToClipboard = (): void => {
|
||||
if (!navigator.clipboard || !navigator.clipboard.writeText) {
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.clipboard.writeText(value).then(() => {
|
||||
setIsCopied(true);
|
||||
|
||||
setTimeout(() => {
|
||||
setIsCopied(false);
|
||||
}, 2000);
|
||||
});
|
||||
};
|
||||
const downloadAsFile = (): void => {
|
||||
const fileExtension = programmingLanguages[language] || ".file";
|
||||
const suggestedFileName = `${"generated-code"}${fileExtension}`;
|
||||
const fileName = window.prompt("enter file name", suggestedFileName);
|
||||
|
||||
if (!fileName) {
|
||||
// user pressed cancel on prompt
|
||||
return;
|
||||
}
|
||||
|
||||
const blob = new Blob([value], { type: "text/plain" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.download = fileName;
|
||||
link.href = url;
|
||||
link.style.display = "none";
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
return (
|
||||
<div className="codeblock font-sans text-[16px]">
|
||||
<div className="code-block-modal">
|
||||
<span className="code-block-modal-span">{language}</span>
|
||||
|
||||
<div className="flex items-center">
|
||||
<button className="code-block-modal-button" onClick={copyToClipboard}>
|
||||
{isCopied ? <IconCheck size={18} /> : <IconClipboard size={18} />}
|
||||
{isCopied ? "Copied!" : "Copy Code"}
|
||||
</button>
|
||||
<button className="code-block-modal-button" onClick={downloadAsFile}>
|
||||
<IconDownload size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SyntaxHighlighter
|
||||
className="overflow-auto"
|
||||
language={language}
|
||||
style={oneDark}
|
||||
customStyle={{ margin: 0 }}
|
||||
>
|
||||
{value}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
CodeBlock.displayName = "CodeBlock";
|
||||
317
src/frontend/src/components/newChatView/chatMessage/index.tsx
Normal file
|
|
@ -0,0 +1,317 @@
|
|||
import Convert from "ansi-to-html";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import Markdown from "react-markdown";
|
||||
import rehypeMathjax from "rehype-mathjax";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import remarkMath from "remark-math";
|
||||
import MaleTechnology from "../../../assets/male-technologist.png";
|
||||
import Robot from "../../../assets/robot.png";
|
||||
import SanitizedHTMLWrapper from "../../../components/SanitizedHTMLWrapper";
|
||||
import CodeTabsComponent from "../../../components/codeTabsComponent";
|
||||
import IconComponent from "../../../components/genericIconComponent";
|
||||
import useFlowStore from "../../../stores/flowStore";
|
||||
import { chatMessagePropsType } from "../../../types/components";
|
||||
import { classNames } from "../../../utils/utils";
|
||||
import FileCard from "../fileComponent";
|
||||
|
||||
export default function ChatMessage({
|
||||
chat,
|
||||
lockChat,
|
||||
lastMessage,
|
||||
updateChat,
|
||||
setLockChat,
|
||||
}: chatMessagePropsType): JSX.Element {
|
||||
const convert = new Convert({ newline: true });
|
||||
const [hidden, setHidden] = useState(true);
|
||||
const template = chat.template;
|
||||
const [promptOpen, setPromptOpen] = useState(false);
|
||||
const [streamUrl, setStreamUrl] = useState(chat.stream_url);
|
||||
// We need to check if message is not undefined because
|
||||
// we need to run .toString() on it
|
||||
const chatMessageString = chat.message ? chat.message.toString() : "";
|
||||
const [chatMessage, setChatMessage] = useState(chatMessageString);
|
||||
const [isStreaming, setIsStreaming] = useState(false);
|
||||
const eventSource = useRef<EventSource | undefined>(undefined);
|
||||
const updateFlowPool = useFlowStore((state) => state.updateFlowPool);
|
||||
const chatMessageRef = useRef(chatMessage);
|
||||
|
||||
// Sync ref with state
|
||||
useEffect(() => {
|
||||
chatMessageRef.current = chatMessage;
|
||||
}, [chatMessage]);
|
||||
|
||||
// The idea now is that chat.stream_url MAY be a URL if we should stream the output of the chat
|
||||
// probably the message is empty when we have a stream_url
|
||||
// what we need is to update the chat_message with the SSE data
|
||||
const streamChunks = (url: string) => {
|
||||
setIsStreaming(true); // Streaming starts
|
||||
return new Promise<boolean>((resolve, reject) => {
|
||||
eventSource.current = new EventSource(url);
|
||||
eventSource.current.onmessage = (event) => {
|
||||
let parsedData = JSON.parse(event.data);
|
||||
if (parsedData.chunk) {
|
||||
setChatMessage((prev) => prev + parsedData.chunk);
|
||||
}
|
||||
};
|
||||
eventSource.current.onerror = (event) => {
|
||||
setIsStreaming(false);
|
||||
eventSource.current?.close();
|
||||
setStreamUrl(undefined);
|
||||
reject(new Error("Streaming failed"));
|
||||
};
|
||||
eventSource.current.addEventListener("close", (event) => {
|
||||
setStreamUrl(undefined); // Update state to reflect the stream is closed
|
||||
eventSource.current?.close();
|
||||
setIsStreaming(false);
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (streamUrl && !isStreaming) {
|
||||
setLockChat(true);
|
||||
streamChunks(streamUrl)
|
||||
.then(() => {
|
||||
setLockChat(false);
|
||||
if (updateChat) {
|
||||
updateChat(chat, chatMessageRef.current);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
setLockChat(false);
|
||||
});
|
||||
}
|
||||
}, [streamUrl, chatMessage]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
eventSource.current?.close();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const element = document.getElementById("last-chat-message");
|
||||
if (element) {
|
||||
setTimeout(() => {
|
||||
element.scrollIntoView({ behavior: "smooth" });
|
||||
}, 200);
|
||||
}
|
||||
}, [lastMessage]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={classNames(
|
||||
"form-modal-chat-position",
|
||||
chat.isSend ? "" : " "
|
||||
)}
|
||||
>
|
||||
<div className={classNames("form-modal-chatbot-icon")}>
|
||||
{!chat.isSend ? (
|
||||
<div className="form-modal-chat-image">
|
||||
<div className="form-modal-chat-bot-icon ">
|
||||
<img
|
||||
src={Robot}
|
||||
className="form-modal-chat-icon-img"
|
||||
alt="robot_image"
|
||||
/>
|
||||
</div>
|
||||
<span className="truncate text-xs">{chat.sender_name}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="form-modal-chat-image">
|
||||
<div className="form-modal-chat-user-icon ">
|
||||
<img
|
||||
src={MaleTechnology}
|
||||
className="form-modal-chat-icon-img"
|
||||
alt="male_technology"
|
||||
/>
|
||||
</div>
|
||||
<span className="truncate text-xs">{chat.sender_name}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!chat.isSend ? (
|
||||
<div className="form-modal-chat-text-position min-w-96 flex-grow">
|
||||
<div className="form-modal-chat-text">
|
||||
{hidden && chat.thought && chat.thought !== "" && (
|
||||
<div
|
||||
onClick={(): void => setHidden((prev) => !prev)}
|
||||
className="form-modal-chat-icon-div"
|
||||
>
|
||||
<IconComponent
|
||||
name="MessageSquare"
|
||||
className="form-modal-chat-icon"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{chat.thought && chat.thought !== "" && !hidden && (
|
||||
<SanitizedHTMLWrapper
|
||||
className=" form-modal-chat-thought"
|
||||
content={convert.toHtml(chat.thought)}
|
||||
onClick={() => setHidden((prev) => !prev)}
|
||||
/>
|
||||
)}
|
||||
{chat.thought && chat.thought !== "" && !hidden && <br></br>}
|
||||
<div className="flex w-full flex-col">
|
||||
<div className="flex w-full flex-col dark:text-white">
|
||||
<div className="flex w-full flex-col">
|
||||
{useMemo(
|
||||
() =>
|
||||
chatMessage === "" && lockChat ? (
|
||||
<IconComponent
|
||||
name="MoreHorizontal"
|
||||
className="h-8 w-8 animate-pulse"
|
||||
/>
|
||||
) : (
|
||||
<Markdown
|
||||
remarkPlugins={[remarkGfm, remarkMath]}
|
||||
rehypePlugins={[rehypeMathjax]}
|
||||
className="markdown prose flex flex-col text-primary word-break-break-word
|
||||
dark:prose-invert"
|
||||
components={{
|
||||
pre({ node, ...props }) {
|
||||
return <>{props.children}</>;
|
||||
},
|
||||
code: ({
|
||||
node,
|
||||
inline,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}) => {
|
||||
if (children.length) {
|
||||
if (children[0] === "▍") {
|
||||
return (
|
||||
<span className="form-modal-markdown-span">
|
||||
▍
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
children[0] = (children[0] as string).replace(
|
||||
"`▍`",
|
||||
"▍"
|
||||
);
|
||||
}
|
||||
|
||||
const match = /language-(\w+)/.exec(
|
||||
className || ""
|
||||
);
|
||||
|
||||
return !inline ? (
|
||||
<CodeTabsComponent
|
||||
isMessage
|
||||
tabs={[
|
||||
{
|
||||
name: (match && match[1]) || "",
|
||||
mode: (match && match[1]) || "",
|
||||
image:
|
||||
"https://curl.se/logo/curl-symbol-transparent.png",
|
||||
language: (match && match[1]) || "",
|
||||
code: String(children).replace(
|
||||
/\n$/,
|
||||
""
|
||||
),
|
||||
},
|
||||
]}
|
||||
activeTab={"0"}
|
||||
setActiveTab={() => {}}
|
||||
/>
|
||||
) : (
|
||||
<code className={className} {...props}>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
},
|
||||
}}
|
||||
>
|
||||
{chatMessage}
|
||||
</Markdown>
|
||||
),
|
||||
[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>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
{template ? (
|
||||
<>
|
||||
<button
|
||||
className="form-modal-initial-prompt-btn"
|
||||
onClick={() => {
|
||||
setPromptOpen((old) => !old);
|
||||
}}
|
||||
>
|
||||
Display Prompt
|
||||
<IconComponent
|
||||
name="ChevronDown"
|
||||
className={
|
||||
"h-3 w-3 transition-all " +
|
||||
(promptOpen ? "rotate-180" : "")
|
||||
}
|
||||
/>
|
||||
</button>
|
||||
<span className="prose text-primary word-break-break-word dark:prose-invert">
|
||||
{promptOpen
|
||||
? template?.split("\n")?.map((line, index) => {
|
||||
const regex = /{([^}]+)}/g;
|
||||
let match;
|
||||
let parts: Array<JSX.Element | string> = [];
|
||||
let lastIndex = 0;
|
||||
while ((match = regex.exec(line)) !== null) {
|
||||
// Push text up to the match
|
||||
if (match.index !== lastIndex) {
|
||||
parts.push(line.substring(lastIndex, match.index));
|
||||
}
|
||||
// Push div with matched text
|
||||
if (chat.message[match[1]]) {
|
||||
parts.push(
|
||||
<span className="chat-message-highlight">
|
||||
{chat.message[match[1]]}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// Update last index
|
||||
lastIndex = regex.lastIndex;
|
||||
}
|
||||
// Push text after the last match
|
||||
if (lastIndex !== line.length) {
|
||||
parts.push(line.substring(lastIndex));
|
||||
}
|
||||
return <p>{parts}</p>;
|
||||
})
|
||||
: chatMessage}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<span>{chatMessage}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div id={lastMessage ? "last-chat-message" : ""}></div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
import * as base64js from "base64-js";
|
||||
import { useState } from "react";
|
||||
import IconComponent from "../../../components/genericIconComponent";
|
||||
import { fileCardPropsType } from "../../../types/components";
|
||||
|
||||
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);
|
||||
};
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
function handleMouseEnter(): void {
|
||||
setIsHovered(true);
|
||||
}
|
||||
function handleMouseLeave(): void {
|
||||
setIsHovered(false);
|
||||
}
|
||||
|
||||
if (fileType === "image") {
|
||||
return (
|
||||
<div
|
||||
className="relative h-1/4 w-1/4"
|
||||
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>
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
216
src/frontend/src/components/newChatView/index.tsx
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
import { useEffect, useRef, useState } from "react";
|
||||
import IconComponent from "../../components/genericIconComponent";
|
||||
import { NOCHATOUTPUT_NOTICE_ALERT } from "../../constants/alerts_constants";
|
||||
import {
|
||||
CHAT_FIRST_INITIAL_TEXT,
|
||||
CHAT_SECOND_INITIAL_TEXT,
|
||||
} from "../../constants/constants";
|
||||
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 { classNames } from "../../utils/utils";
|
||||
import ChatInput from "./chatInput";
|
||||
import ChatMessage from "./chatMessage";
|
||||
|
||||
export default function NewChatView({
|
||||
sendMessage,
|
||||
chatValue,
|
||||
setChatValue,
|
||||
lockChat,
|
||||
setLockChat,
|
||||
}): JSX.Element {
|
||||
const { flowPool, outputs, inputs, CleanFlowPool } = useFlowStore();
|
||||
const { setNoticeData } = useAlertStore();
|
||||
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
|
||||
const messagesRef = useRef<HTMLDivElement | null>(null);
|
||||
const [chatHistory, setChatHistory] = useState<ChatMessageType[]>([]);
|
||||
|
||||
const inputTypes = inputs.map((obj) => obj.type);
|
||||
const inputIds = inputs.map((obj) => obj.id);
|
||||
const outputIds = outputs.map((obj) => obj.id);
|
||||
const outputTypes = outputs.map((obj) => obj.type);
|
||||
const updateFlowPool = useFlowStore((state) => state.updateFlowPool);
|
||||
|
||||
useEffect(() => {
|
||||
if (!outputTypes.includes("ChatOutput")) {
|
||||
setNoticeData({ title: NOCHATOUTPUT_NOTICE_ALERT });
|
||||
}
|
||||
}, []);
|
||||
|
||||
//build chat history
|
||||
useEffect(() => {
|
||||
const chatOutputResponses: FlowPoolObjectType[] = [];
|
||||
outputIds.forEach((outputId) => {
|
||||
if (outputId.includes("ChatOutput")) {
|
||||
if (flowPool[outputId] && flowPool[outputId].length > 0) {
|
||||
chatOutputResponses.push(...flowPool[outputId]);
|
||||
}
|
||||
}
|
||||
});
|
||||
inputIds.forEach((inputId) => {
|
||||
if (inputId.includes("ChatInput")) {
|
||||
if (flowPool[inputId] && flowPool[inputId].length > 0) {
|
||||
chatOutputResponses.push(...flowPool[inputId]);
|
||||
}
|
||||
}
|
||||
});
|
||||
const chatMessages: ChatMessageType[] = chatOutputResponses
|
||||
.sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp))
|
||||
//
|
||||
.filter((output) => output.data.artifacts?.message !== null)
|
||||
.map((output, index) => {
|
||||
try {
|
||||
const { sender, message, sender_name, stream_url } = output.data
|
||||
.artifacts as ChatOutputType;
|
||||
|
||||
const is_ai = sender === "Machine" || sender === null;
|
||||
return {
|
||||
isSend: !is_ai,
|
||||
message: message,
|
||||
sender_name,
|
||||
componentId: output.id,
|
||||
stream_url: stream_url,
|
||||
};
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return {
|
||||
isSend: false,
|
||||
message: "Error parsing message",
|
||||
sender_name: "Error",
|
||||
componentId: output.id,
|
||||
};
|
||||
}
|
||||
});
|
||||
setChatHistory(chatMessages);
|
||||
}, [flowPool]);
|
||||
useEffect(() => {
|
||||
if (messagesRef.current) {
|
||||
messagesRef.current.scrollTop = messagesRef.current.scrollHeight;
|
||||
}
|
||||
}, []);
|
||||
|
||||
async function sendAll(data: sendAllProps): Promise<void> {}
|
||||
useEffect(() => {
|
||||
if (ref.current) ref.current.scrollIntoView({ behavior: "smooth" });
|
||||
}, []);
|
||||
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
ref.current.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
function clearChat(): void {
|
||||
setChatHistory([]);
|
||||
deleteFlowPool(currentFlowId).then((_) => {
|
||||
CleanFlowPool();
|
||||
});
|
||||
//TODO tell backend to clear chat session
|
||||
if (lockChat) setLockChat(false);
|
||||
}
|
||||
|
||||
function updateChat(
|
||||
chat: ChatMessageType,
|
||||
message: string,
|
||||
stream_url?: string
|
||||
) {
|
||||
if (message === "") return;
|
||||
chat.message = message;
|
||||
// chat is one of the chatHistory
|
||||
updateFlowPool(chat.componentId, {
|
||||
message,
|
||||
sender_name: chat.sender_name ?? "Bot",
|
||||
sender: chat.isSend ? "User" : "Machine",
|
||||
});
|
||||
// setChatHistory((oldChatHistory) => {
|
||||
// const index = oldChatHistory.findIndex((ch) => ch.id === chat.id);
|
||||
// if (index === -1) return oldChatHistory;
|
||||
// let newChatHistory = _.cloneDeep(oldChatHistory);
|
||||
// newChatHistory = [
|
||||
// ...newChatHistory.slice(0, index),
|
||||
// chat,
|
||||
// ...newChatHistory.slice(index + 1),
|
||||
// ];
|
||||
// console.log("newChatHistory:", newChatHistory);
|
||||
// return newChatHistory;
|
||||
// });
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="eraser-column-arrangement">
|
||||
<div className="eraser-size">
|
||||
<div className="eraser-position">
|
||||
<button disabled={lockChat} onClick={() => clearChat()}>
|
||||
<IconComponent
|
||||
name="Eraser"
|
||||
className={classNames(
|
||||
"h-5 w-5",
|
||||
lockChat
|
||||
? "animate-pulse text-primary"
|
||||
: "text-primary hover:text-gray-600"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div ref={messagesRef} className="chat-message-div">
|
||||
{chatHistory?.length > 0 ? (
|
||||
chatHistory.map((chat, index) => (
|
||||
<ChatMessage
|
||||
setLockChat={setLockChat}
|
||||
lockChat={lockChat}
|
||||
chat={chat}
|
||||
lastMessage={chatHistory.length - 1 === index ? true : false}
|
||||
key={`${chat.componentId}-${index}`}
|
||||
updateChat={updateChat}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div className="chat-alert-box">
|
||||
<span>
|
||||
👋 <span className="langflow-chat-span">Langflow Chat</span>
|
||||
</span>
|
||||
<br />
|
||||
<div className="langflow-chat-desc">
|
||||
<span className="langflow-chat-desc-span">
|
||||
{CHAT_FIRST_INITIAL_TEXT}{" "}
|
||||
<span>
|
||||
<IconComponent
|
||||
name="MessageSquare"
|
||||
className="mx-1 inline h-5 w-5 animate-bounce "
|
||||
/>
|
||||
</span>{" "}
|
||||
{CHAT_SECOND_INITIAL_TEXT}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div ref={ref}></div>
|
||||
</div>
|
||||
<div className="langflow-chat-input-div">
|
||||
<div className="langflow-chat-input">
|
||||
<ChatInput
|
||||
chatValue={chatValue}
|
||||
noInput={!inputTypes.includes("ChatInput")}
|
||||
lockChat={lockChat}
|
||||
sendMessage={(count) => sendMessage(count)}
|
||||
setChatValue={(value) => {
|
||||
setChatValue(value);
|
||||
}}
|
||||
inputRef={ref}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { useEffect } from "react";
|
||||
import { EDIT_TEXT_MODAL_TITLE } from "../../constants/constants";
|
||||
import { TypeModal } from "../../constants/enums";
|
||||
import GenericModal from "../../modals/genericModal";
|
||||
import { TextAreaComponentType } from "../../types/components";
|
||||
|
|
@ -20,48 +21,42 @@ export default function TextAreaComponent({
|
|||
}, [disabled]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
"flex w-full items-center " + (disabled ? "pointer-events-none" : "")
|
||||
}
|
||||
>
|
||||
<GenericModal
|
||||
type={TypeModal.TEXT}
|
||||
buttonText="Finishing Editing"
|
||||
modalTitle="Edit Text"
|
||||
value={value}
|
||||
setValue={(value: string) => {
|
||||
onChange(value);
|
||||
}}
|
||||
>
|
||||
<div className="flex w-full items-center" data-testid={"div-" + id}>
|
||||
<Input
|
||||
id={id}
|
||||
data-testid={id}
|
||||
<div className={"flex w-full items-center " + (disabled ? "" : "")}>
|
||||
<div className="flex w-full items-center" data-testid={"div-" + id}>
|
||||
<Input
|
||||
id={id}
|
||||
data-testid={id}
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
className={editNode ? "input-edit-node w-full" : " w-full"}
|
||||
placeholder={"Type something..."}
|
||||
onChange={(event) => {
|
||||
onChange(event.target.value);
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
<GenericModal
|
||||
type={TypeModal.TEXT}
|
||||
buttonText="Finish Editing"
|
||||
modalTitle={EDIT_TEXT_MODAL_TITLE}
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
className={
|
||||
editNode
|
||||
? "input-edit-node pointer-events-none "
|
||||
: " pointer-events-none"
|
||||
}
|
||||
placeholder={"Type something..."}
|
||||
onChange={(event) => {
|
||||
onChange(event.target.value);
|
||||
setValue={(value: string) => {
|
||||
onChange(value);
|
||||
}}
|
||||
/>
|
||||
{!editNode && (
|
||||
<IconComponent
|
||||
id={id}
|
||||
name="ExternalLink"
|
||||
className={
|
||||
"icons-parameters-comp" +
|
||||
(disabled ? " text-ring" : " hover:text-accent-foreground")
|
||||
}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
{!editNode && (
|
||||
<IconComponent
|
||||
id={id}
|
||||
name="ExternalLink"
|
||||
className={
|
||||
"icons-parameters-comp" +
|
||||
(disabled ? " text-ring" : " hover:text-accent-foreground")
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</GenericModal>
|
||||
</div>
|
||||
</GenericModal>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
15
src/frontend/src/components/textInputComponent/index.tsx
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
export default function TextInputComponent({
|
||||
text,
|
||||
emissor,
|
||||
}: {
|
||||
text: string;
|
||||
emissor: string;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<strong> {emissor}</strong>
|
||||
<br></br>
|
||||
<span>{text}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
15
src/frontend/src/components/textOutputComponent/index.tsx
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
export default function TextOutputComponent({
|
||||
text,
|
||||
emissor,
|
||||
}: {
|
||||
text: string;
|
||||
emissor: string;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<strong>{emissor}</strong>
|
||||
<br></br>
|
||||
<div className="w-80 break-all">{text}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ export default function ToggleShadComponent({
|
|||
disabled,
|
||||
size,
|
||||
id = "",
|
||||
editNode = false,
|
||||
}: ToggleComponentType): JSX.Element {
|
||||
let scaleX, scaleY;
|
||||
switch (size) {
|
||||
|
|
@ -31,6 +32,7 @@ export default function ToggleShadComponent({
|
|||
<div className={disabled ? "pointer-events-none cursor-not-allowed " : ""}>
|
||||
<Switch
|
||||
id={id}
|
||||
data-testid={id}
|
||||
style={{
|
||||
transform: `scaleX(${scaleX}) scaleY(${scaleY})`,
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import * as React from "react";
|
|||
import { cn } from "../../utils/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"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",
|
||||
"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: {
|
||||
|
|
@ -12,7 +12,7 @@ const buttonVariants = cva(
|
|||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input hover:bg-accent hover:text-accent-foreground",
|
||||
"border border-input hover:bg-input hover:text-accent-foreground",
|
||||
primary:
|
||||
"border bg-background text-secondary-foreground hover:bg-secondary-foreground/5 dark:hover:bg-background/10 hover:shadow-sm",
|
||||
secondary:
|
||||
|
|
|
|||
31
src/frontend/src/components/ui/checkmark.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { AnimatePresence, motion } from "framer-motion";
|
||||
export default function Checkmark({ initial = true, isVisible, className }) {
|
||||
return (
|
||||
<AnimatePresence initial={initial}>
|
||||
{isVisible && (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className={"CheckIcon " + className}
|
||||
>
|
||||
<motion.path
|
||||
initial={{ pathLength: 0, pathOffset: 1 }}
|
||||
animate={{ pathLength: 1, pathOffset: 0 }}
|
||||
exit={{ pathLength: 0, pathOffset: 1 }}
|
||||
transition={{
|
||||
type: "tween",
|
||||
duration: 0.3,
|
||||
ease: isVisible ? "easeOut" : "easeIn",
|
||||
}}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M20 6 9 17l-5-5"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
|
|
@ -43,7 +43,7 @@ const DialogContent = React.forwardRef<
|
|||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 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-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg md:w-full",
|
||||
"fixed z-50 flex w-full max-w-lg flex-col gap-4 border bg-background p-6 shadow-lg duration-200 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-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] sm:rounded-lg md:w-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
58
src/frontend/src/components/ui/refreshButton.tsx
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import IconComponent from "../../components/genericIconComponent";
|
||||
import { NodeDataType } from "../../types/flow";
|
||||
import { cn } from "../../utils/utils";
|
||||
import { Button } from "./button";
|
||||
|
||||
function RefreshButton({
|
||||
isLoading,
|
||||
disabled,
|
||||
name,
|
||||
button_text,
|
||||
data,
|
||||
handleUpdateValues,
|
||||
className,
|
||||
id,
|
||||
}: {
|
||||
isLoading: boolean;
|
||||
disabled: boolean;
|
||||
name: string;
|
||||
button_text: string;
|
||||
data: NodeDataType;
|
||||
className?: string;
|
||||
handleUpdateValues: (name: string, data: NodeDataType) => void;
|
||||
id: string;
|
||||
}) {
|
||||
const handleClick = async () => {
|
||||
if (disabled) return;
|
||||
handleUpdateValues(name, data);
|
||||
};
|
||||
|
||||
const classNames = cn(className, disabled ? "cursor-not-allowed" : "");
|
||||
|
||||
// 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
|
||||
);
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="primary"
|
||||
disabled={disabled}
|
||||
className={classNames}
|
||||
onClick={handleClick}
|
||||
id={id}
|
||||
>
|
||||
<span className="mr-1">{button_text}</span>
|
||||
<IconComponent
|
||||
name={isLoading ? "Loader2" : "RefreshCcw"}
|
||||
className={iconClassName}
|
||||
id={id + "-icon"}
|
||||
/>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export { RefreshButton };
|
||||
|
|
@ -28,6 +28,9 @@ export default function RenameLabel(props) {
|
|||
}
|
||||
}
|
||||
resizeInput();
|
||||
return () => {
|
||||
if(isRename) document.removeEventListener("keydown", () => {});
|
||||
};
|
||||
}, [isRename]);
|
||||
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ const SelectContent = React.forwardRef<
|
|||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-50 min-w-[8rem] 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-[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",
|
||||
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
|
||||
|
|
|
|||
45
src/frontend/src/components/ui/xmark.tsx
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { AnimatePresence, motion } from "framer-motion";
|
||||
export default function Xmark({ initial = true, isVisible, className }) {
|
||||
return (
|
||||
<AnimatePresence initial={initial}>
|
||||
{isVisible && (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className={"CheckIcon " + className}
|
||||
>
|
||||
<motion.path
|
||||
initial={{ pathLength: 0, pathOffset: 1, strokeLinecap: "butt" }}
|
||||
animate={{ pathLength: 1, pathOffset: 0, strokeLinecap: "round" }}
|
||||
exit={{ pathLength: 0, pathOffset: 1, strokeLinecap: "butt" }}
|
||||
transition={{
|
||||
type: "tween",
|
||||
duration: 0.3,
|
||||
ease: isVisible ? "easeOut" : "easeIn",
|
||||
delay: 0.2,
|
||||
}}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M18 6 6 18"
|
||||
/>
|
||||
<motion.path
|
||||
initial={{ pathLength: 0 }}
|
||||
animate={{ pathLength: 1 }}
|
||||
exit={{ pathLength: 0 }}
|
||||
transition={{
|
||||
type: "tween",
|
||||
duration: 0.3,
|
||||
ease: isVisible ? "easeOut" : "easeIn",
|
||||
}}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="m6 6 12 12"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
107
src/frontend/src/components/undrawCards/index.tsx
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import { useNavigate } from "react-router-dom";
|
||||
/// <reference types="vite-plugin-svgr/client" />
|
||||
//@ts-ignore
|
||||
import { ReactComponent as TransferFiles } from "../../assets/undraw_transfer_files_re_a2a9.svg";
|
||||
//@ts-ignore
|
||||
import { ReactComponent as BasicPrompt } from "../../assets/undraw_design_components_9vy6.svg";
|
||||
//@ts-ignore
|
||||
import { ReactComponent as ChatWithHistory } from "../../assets/undraw_mobile_messages_re_yx8w.svg";
|
||||
//@ts-ignore
|
||||
import { ReactComponent as Assistant } from "../../assets/undraw_team_collaboration_re_ow29.svg";
|
||||
//@ts-ignore
|
||||
import { ReactComponent as APIRequest } from "../../assets/undraw_real_time_analytics_re_yliv.svg";
|
||||
import useFlowsManagerStore from "../../stores/flowsManagerStore";
|
||||
import { FlowType } from "../../types/flow";
|
||||
import { updateIds } from "../../utils/reactflowUtils";
|
||||
import { Card, CardContent, CardDescription, CardTitle } from "../ui/card";
|
||||
|
||||
export default function UndrawCardComponent({ flow }: { flow: FlowType }) {
|
||||
const addFlow = useFlowsManagerStore((state) => state.addFlow);
|
||||
const navigate = useNavigate();
|
||||
|
||||
function selectImage() {
|
||||
switch (flow.name) {
|
||||
case "Data Ingestion":
|
||||
return (
|
||||
<TransferFiles
|
||||
style={{
|
||||
width: "80%",
|
||||
height: "80%",
|
||||
preserveAspectRatio: "xMidYMid meet",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
case "Basic Prompting":
|
||||
return (
|
||||
<BasicPrompt
|
||||
style={{
|
||||
width: "80%",
|
||||
height: "80%",
|
||||
preserveAspectRatio: "xMidYMid meet",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
case "Chat with memory":
|
||||
return (
|
||||
<ChatWithHistory
|
||||
style={{
|
||||
width: "70%",
|
||||
height: "70%",
|
||||
preserveAspectRatio: "xMidYMid meet",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
case "API requests":
|
||||
return (
|
||||
<APIRequest
|
||||
style={{
|
||||
width: "70%",
|
||||
height: "70%",
|
||||
preserveAspectRatio: "xMidYMid meet",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
case "Assistant":
|
||||
return (
|
||||
<Assistant
|
||||
style={{
|
||||
width: "80%",
|
||||
height: "80%",
|
||||
preserveAspectRatio: "xMidYMid meet",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<TransferFiles
|
||||
style={{
|
||||
width: "80%",
|
||||
height: "80%",
|
||||
preserveAspectRatio: "xMidYMid meet",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
onClick={() => {
|
||||
updateIds(flow.data!);
|
||||
addFlow(true, flow).then((id) => {
|
||||
navigate("/flow/" + id);
|
||||
});
|
||||
}}
|
||||
className="h-64 w-80 cursor-pointer bg-background pt-4"
|
||||
>
|
||||
<CardContent className="h-full w-full">
|
||||
<div className="flex h-full w-full flex-col items-center justify-center rounded-md bg-muted p-1 align-middle">
|
||||
{selectImage()}
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardDescription className="px-6 pb-4">
|
||||
<CardTitle className="text-lg text-primary">{flow.name}</CardTitle>
|
||||
</CardDescription>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
60
src/frontend/src/constants/alerts_constants.tsx
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
// ERROR
|
||||
export const MISSED_ERROR_ALERT = "Oops! Looks like you missed something";
|
||||
export const INVALID_FILE_ALERT =
|
||||
"Please select a valid file. Only these file types are allowed:";
|
||||
export const CONSOLE_ERROR_MSG = "Error occurred while uploading file";
|
||||
export const CONSOLE_SUCCESS_MSG = "File uploaded successfully";
|
||||
export const INFO_MISSING_ALERT =
|
||||
"Oops! Looks like you missed some required information:";
|
||||
export const FUNC_ERROR_ALERT = "There is an error in your function";
|
||||
export const IMPORT_ERROR_ALERT = "There is an error in your imports";
|
||||
export const BUG_ALERT = "Something went wrong, please try again";
|
||||
export const CODE_ERROR_ALERT =
|
||||
"There is something wrong with this code, please review it";
|
||||
export const CHAT_ERROR_ALERT =
|
||||
"Please build the flow again before using the chat.";
|
||||
export const MSG_ERROR_ALERT = "There was an error sending the message";
|
||||
export const PROMPT_ERROR_ALERT =
|
||||
"There is something wrong with this prompt, please review it";
|
||||
export const API_ERROR_ALERT =
|
||||
"There was an error saving the API Key, please try again.";
|
||||
export const USER_DEL_ERROR_ALERT = "Error on delete user";
|
||||
export const USER_EDIT_ERROR_ALERT = "Error on edit user";
|
||||
export const USER_ADD_ERROR_ALERT = "Error when adding new user";
|
||||
export const SIGNIN_ERROR_ALERT = "Error signing in";
|
||||
export const DEL_KEY_ERROR_ALERT = "Error on delete key";
|
||||
export const UPLOAD_ERROR_ALERT = "Error uploading file";
|
||||
export const WRONG_FILE_ERROR_ALERT = "Invalid file type";
|
||||
export const UPLOAD_ALERT_LIST = "Please upload a JSON file";
|
||||
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 SIGNUP_ERROR_ALERT = "Error signing up";
|
||||
export const APIKEY_ERROR_ALERT = "API Key Error";
|
||||
export const NOAPI_ERROR_ALERT =
|
||||
"You don't have an API Key. Please add one to use the Langflow Store.";
|
||||
export const INVALID_API_ERROR_ALERT =
|
||||
"Your API Key is not valid. Please add a valid API Key to use the Langflow Store.";
|
||||
export const COMPONENTS_ERROR_ALERT = "Error getting components.";
|
||||
|
||||
// NOTICE
|
||||
export const NOCHATOUTPUT_NOTICE_ALERT =
|
||||
"There is no ChatOutput Component in the flow.";
|
||||
export const API_WARNING_NOTICE_ALERT =
|
||||
"Warning: Critical data, JSON file may include API keys.";
|
||||
export const COPIED_NOTICE_ALERT = "API Key copied!";
|
||||
export const TEMP_NOTICE_ALERT = "Your template does not have any variables.";
|
||||
|
||||
// SUCCESS
|
||||
export const CODE_SUCCESS_ALERT = "Code is ready to run";
|
||||
export const PROMPT_SUCCESS_ALERT = "Prompt is ready";
|
||||
export const API_SUCCESS_ALERT = "Success! Your API Key has been saved.";
|
||||
export const USER_DEL_SUCCESS_ALERT = "Success! User deleted!";
|
||||
export const USER_EDIT_SUCCESS_ALERT = "Success! User edited!";
|
||||
export const USER_ADD_SUCCESS_ALERT = "Success! New user added!";
|
||||
export const DEL_KEY_SUCCESS_ALERT = "Success! Key deleted!";
|
||||
export const FLOW_BUILD_SUCCESS_ALERT = `Flow built successfully`;
|
||||
export const SAVE_SUCCESS_ALERT = "Changes saved successfully!";
|
||||
|
||||
// Generic Node
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
// src/constants/constants.ts
|
||||
|
||||
import { languageMap } from "../types/components";
|
||||
import { FlowType } from "../types/flow";
|
||||
|
||||
/**
|
||||
* invalid characters for flow name
|
||||
|
|
@ -25,10 +26,18 @@ export const INVALID_CHARACTERS = [
|
|||
|
||||
/**
|
||||
* regex to highlight the variables in the text
|
||||
* @constant
|
||||
* @constant regexHighlight
|
||||
* @type {RegExp}
|
||||
* @default
|
||||
* @example
|
||||
* {{variable}} or {variable}
|
||||
* @returns {RegExp}
|
||||
* @description
|
||||
* This regex is used to highlight the variables in the text.
|
||||
* It matches the variables in the text that are between {{}} or {}.
|
||||
*/
|
||||
|
||||
export const regexHighlight = /\{([^}]+)\}/g;
|
||||
export const regexHighlight = /\{\{(.*?)\}\}|\{([^{}]+)\}/g;
|
||||
export const specialCharsRegex = /[!@#$%^&*()\-_=+[\]{}|;:'",.<>/?\\`´]/;
|
||||
|
||||
export const programmingLanguages: languageMap = {
|
||||
|
|
@ -519,6 +528,8 @@ export const ADMIN_HEADER_DESCRIPTION =
|
|||
|
||||
export const BASE_URL_API = "/api/v1/";
|
||||
|
||||
export const BACKEND_URL = "http://localhost:7860/";
|
||||
|
||||
/**
|
||||
* URLs excluded from error retries.
|
||||
* @constant
|
||||
|
|
@ -677,3 +688,137 @@ export const LANGFLOW_SUPPORTED_TYPES = new Set([
|
|||
]);
|
||||
|
||||
export const priorityFields = new Set(["code", "template"]);
|
||||
|
||||
export const INPUT_TYPES = new Set(["ChatInput", "TextInput"]);
|
||||
export const OUTPUT_TYPES = new Set(["ChatOutput", "TextOutput"]);
|
||||
|
||||
export const CHAT_FIRST_INITIAL_TEXT =
|
||||
"Start a conversation and click the agent's thoughts";
|
||||
|
||||
export const CHAT_SECOND_INITIAL_TEXT = "to inspect the chaining process.";
|
||||
|
||||
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.";
|
||||
|
||||
export const SAVE_WITH_API_CHECKBOX = "Save with my API keys";
|
||||
export const EDIT_TEXT_MODAL_TITLE = "Edit Text";
|
||||
export const EDIT_TEXT_PLACEHOLDER = "Type message here.";
|
||||
export const INPUT_HANDLER_HOVER = "Avaliable input components:";
|
||||
export const OUTPUT_HANDLER_HOVER = "Avaliable output components:";
|
||||
export const TEXT_INPUT_MODAL_TITLE = "Text Inputs";
|
||||
export const OUTPUTS_MODAL_TITLE = "Text Outputs";
|
||||
export const LANGFLOW_CHAT_TITLE = "Langflow Chat";
|
||||
export const CHAT_INPUT_PLACEHOLDER =
|
||||
"No chat input variables found. Click to run your flow.";
|
||||
export const CHAT_INPUT_PLACEHOLDER_SEND = "Send a message...";
|
||||
export const EDIT_CODE_TITLE = "Edit Code";
|
||||
export const MY_COLLECTION_DESC =
|
||||
"Manage your personal projects. Download and upload entire collections.";
|
||||
export const STORE_DESC = "Explore community-shared flows and components.";
|
||||
export const STORE_TITLE = "Langflow Store";
|
||||
export const NO_API_KEY = "You don't have an API key. ";
|
||||
export const INSERT_API_KEY = "Insert your Langflow API key.";
|
||||
export const INVALID_API_KEY = "Your API key is not valid. ";
|
||||
export const CREATE_API_KEY = `Don’t have an API key? Sign up at`;
|
||||
export const STATUS_BUILD = "Build to validate status.";
|
||||
export const STATUS_BUILDING = "Building...";
|
||||
export const SAVED_HOVER = "Last saved at ";
|
||||
export const RUN_TIMESTAMP_PREFIX = "Last Run: ";
|
||||
export const STARTER_FOLDER_NAME = "Starter Projects";
|
||||
export const PRIORITY_SIDEBAR_ORDER = [
|
||||
"saved_components",
|
||||
"inputs",
|
||||
"outputs",
|
||||
"prompts",
|
||||
"data",
|
||||
"prompt",
|
||||
"models",
|
||||
"helpers",
|
||||
"experimental",
|
||||
];
|
||||
export const NATIVE_CATEGORIES = [
|
||||
"inputs",
|
||||
"outputs",
|
||||
"prompts",
|
||||
"data",
|
||||
"prompt",
|
||||
"models",
|
||||
"helpers",
|
||||
"experimental",
|
||||
];
|
||||
/*
|
||||
Data ingestion
|
||||
Basic Prompting
|
||||
Chat com memória
|
||||
Working with data (file/website)
|
||||
API requests
|
||||
Vector Store
|
||||
Assistant
|
||||
*/
|
||||
|
||||
export const EXAMPLES_MOCK: FlowType[] = [
|
||||
{
|
||||
name: "Working with data",
|
||||
id: "Working with data Description",
|
||||
data: {
|
||||
nodes: [],
|
||||
edges: [],
|
||||
viewport: { zoom: 1, x: 1, y: 1 },
|
||||
},
|
||||
description: "This flow represents the first process in our application.",
|
||||
folder: STARTER_FOLDER_NAME,
|
||||
user_id: undefined,
|
||||
},
|
||||
{
|
||||
name: "Basic Prompting",
|
||||
id: "Basic Prompting Description",
|
||||
data: {
|
||||
nodes: [],
|
||||
edges: [],
|
||||
viewport: { zoom: 1, x: 1, y: 1 },
|
||||
},
|
||||
description: "This flow represents the first process in our application.",
|
||||
folder: STARTER_FOLDER_NAME,
|
||||
user_id: undefined,
|
||||
},
|
||||
{
|
||||
name: "Chat with memory",
|
||||
id: "Chat with memory Description",
|
||||
data: {
|
||||
nodes: [],
|
||||
edges: [],
|
||||
viewport: { zoom: 1, x: 1, y: 1 },
|
||||
},
|
||||
description: "This flow represents the first process in our application.",
|
||||
folder: STARTER_FOLDER_NAME,
|
||||
user_id: undefined,
|
||||
},
|
||||
{
|
||||
name: "API requests",
|
||||
id: "API requests Description",
|
||||
data: {
|
||||
nodes: [],
|
||||
edges: [],
|
||||
viewport: { zoom: 1, x: 1, y: 1 },
|
||||
},
|
||||
description: "This flow represents the first process in our application.",
|
||||
folder: STARTER_FOLDER_NAME,
|
||||
user_id: undefined,
|
||||
},
|
||||
{
|
||||
name: "Assistant",
|
||||
id: "Assistant Description",
|
||||
data: {
|
||||
nodes: [],
|
||||
edges: [],
|
||||
viewport: { zoom: 1, x: 1, y: 1 },
|
||||
},
|
||||
description: "This flow represents the first process in our application.",
|
||||
folder: STARTER_FOLDER_NAME,
|
||||
user_id: undefined,
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -6,3 +6,10 @@ export enum TypeModal {
|
|||
TEXT = 1,
|
||||
PROMPT = 2,
|
||||
}
|
||||
|
||||
export enum BuildStatus {
|
||||
BUILDING = "BUILDING",
|
||||
TO_BUILD = "TO_BUILD",
|
||||
BUILT = "BUILT",
|
||||
INACTIVE = "INACTIVE",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
requestLogout,
|
||||
} from "../controllers/API";
|
||||
import useAlertStore from "../stores/alertStore";
|
||||
import useFlowsManagerStore from "../stores/flowsManagerStore";
|
||||
import { Users } from "../types/api";
|
||||
import { AuthContextType } from "../types/contexts/auth";
|
||||
|
||||
|
|
@ -79,6 +80,7 @@ export function AuthProvider({ children }): React.ReactElement {
|
|||
getUser();
|
||||
} else {
|
||||
setLoading(false);
|
||||
useFlowsManagerStore.setState({ isLoading: false });
|
||||
}
|
||||
});
|
||||
}, [setUserData, setLoading, autoLogin, setIsAdmin]);
|
||||
|
|
@ -92,7 +94,6 @@ export function AuthProvider({ children }): React.ReactElement {
|
|||
setIsAdmin(isSuperUser);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log("auth context");
|
||||
setLoading(false);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import axios, { AxiosError, AxiosInstance } from "axios";
|
||||
import { useContext, useEffect } from "react";
|
||||
import { Cookies } from "react-cookie";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { renewAccessToken } from ".";
|
||||
import { BuildStatus } from "../../constants/enums";
|
||||
import { AuthContext } from "../../contexts/authContext";
|
||||
import useAlertStore from "../../stores/alertStore";
|
||||
import useFlowStore from "../../stores/flowStore";
|
||||
|
||||
// Create a new Axios instance
|
||||
const api: AxiosInstance = axios.create({
|
||||
|
|
@ -15,54 +16,31 @@ function ApiInterceptor() {
|
|||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||
let { accessToken, login, logout, authenticationErrorCount, autoLogin } =
|
||||
useContext(AuthContext);
|
||||
const navigate = useNavigate();
|
||||
const cookies = new Cookies();
|
||||
|
||||
useEffect(() => {
|
||||
const interceptor = api.interceptors.response.use(
|
||||
(response) => response,
|
||||
async (error: AxiosError) => {
|
||||
if (error.response?.status === 401) {
|
||||
const accessToken = cookies.get("access_token_lf");
|
||||
if (accessToken && !autoLogin) {
|
||||
authenticationErrorCount = authenticationErrorCount + 1;
|
||||
if (authenticationErrorCount > 3) {
|
||||
authenticationErrorCount = 0;
|
||||
logout();
|
||||
if (error.response?.status === 403 || error.response?.status === 401) {
|
||||
if (!autoLogin) {
|
||||
const stillRefresh = checkErrorCount();
|
||||
if (!stillRefresh) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
try {
|
||||
const res = await renewAccessToken();
|
||||
if (res?.data?.access_token && res?.data?.refresh_token) {
|
||||
login(res?.data?.access_token);
|
||||
}
|
||||
if (error?.config?.headers) {
|
||||
delete error.config.headers["Authorization"];
|
||||
error.config.headers["Authorization"] = `Bearer ${cookies.get(
|
||||
"access_token_lf"
|
||||
)}`;
|
||||
const response = await axios.request(error.config);
|
||||
return response;
|
||||
}
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error) && error.response?.status === 401) {
|
||||
logout();
|
||||
} else {
|
||||
console.error(error);
|
||||
logout();
|
||||
}
|
||||
}
|
||||
}
|
||||
const acceptedRequest = await tryToRenewAccessToken(error);
|
||||
|
||||
if (!accessToken && error?.config?.url?.includes("login")) {
|
||||
return Promise.reject(error);
|
||||
} else {
|
||||
logout();
|
||||
const accessToken = cookies.get("access_token_lf");
|
||||
|
||||
if (!accessToken && error?.config?.url?.includes("login")) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
return acceptedRequest;
|
||||
}
|
||||
} else {
|
||||
// if (URL_EXCLUDED_FROM_ERROR_RETRIES.includes(error.config?.url)) {
|
||||
return Promise.reject(error);
|
||||
// }
|
||||
}
|
||||
await clearBuildVerticesState(error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
|
|
@ -115,6 +93,50 @@ function ApiInterceptor() {
|
|||
};
|
||||
}, [accessToken, setErrorData]);
|
||||
|
||||
function checkErrorCount() {
|
||||
authenticationErrorCount = authenticationErrorCount + 1;
|
||||
|
||||
if (authenticationErrorCount > 3) {
|
||||
authenticationErrorCount = 0;
|
||||
logout();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function tryToRenewAccessToken(error: AxiosError) {
|
||||
try {
|
||||
if (window.location.pathname.includes("/login")) return;
|
||||
const res = await renewAccessToken();
|
||||
if (res?.data?.access_token && res?.data?.refresh_token) {
|
||||
login(res?.data?.access_token);
|
||||
}
|
||||
if (error?.config?.headers) {
|
||||
delete error.config.headers["Authorization"];
|
||||
error.config.headers["Authorization"] = `Bearer ${cookies.get(
|
||||
"access_token_lf"
|
||||
)}`;
|
||||
const response = await axios.request(error.config);
|
||||
return response;
|
||||
}
|
||||
} catch (error) {
|
||||
clearBuildVerticesState(error);
|
||||
logout();
|
||||
return Promise.reject("Authentication error");
|
||||
}
|
||||
}
|
||||
|
||||
async function clearBuildVerticesState(error) {
|
||||
if (error?.response?.status === 500) {
|
||||
const vertices = useFlowStore.getState().verticesBuild;
|
||||
useFlowStore
|
||||
.getState()
|
||||
.updateBuildStatus(vertices?.verticesIds ?? [], BuildStatus.BUILT);
|
||||
useFlowStore.getState().setIsBuilding(false);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,9 +4,12 @@ import { BASE_URL_API } from "../../constants/constants";
|
|||
import { api } from "../../controllers/API/api";
|
||||
import {
|
||||
APIObjectType,
|
||||
APITemplateType,
|
||||
Component,
|
||||
LoginType,
|
||||
Users,
|
||||
VertexBuildTypeAPI,
|
||||
VerticesOrderTypeAPI,
|
||||
changeUser,
|
||||
resetPasswordType,
|
||||
sendAllProps,
|
||||
|
|
@ -14,6 +17,7 @@ import {
|
|||
import { UserInputType } from "../../types/components";
|
||||
import { FlowStyleType, FlowType } from "../../types/flow";
|
||||
import { StoreComponentResponse } from "../../types/store";
|
||||
import { FlowPoolType } from "../../types/zustand/flow";
|
||||
import {
|
||||
APIClassType,
|
||||
BuildStatusTypeAPI,
|
||||
|
|
@ -350,7 +354,7 @@ export async function uploadFile(
|
|||
): Promise<AxiosResponse<UploadFileTypeAPI>> {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
return await api.post(`${BASE_URL_API}upload/${id}`, formData);
|
||||
return await api.post(`${BASE_URL_API}files/upload/${id}`, formData);
|
||||
}
|
||||
|
||||
export async function postCustomComponent(
|
||||
|
|
@ -366,11 +370,15 @@ export async function postCustomComponent(
|
|||
|
||||
export async function postCustomComponentUpdate(
|
||||
code: string,
|
||||
field: string
|
||||
template: APITemplateType,
|
||||
field: string,
|
||||
field_value: any
|
||||
): Promise<AxiosResponse<APIClassType>> {
|
||||
return await api.post(`${BASE_URL_API}custom_component/update`, {
|
||||
code,
|
||||
template,
|
||||
field,
|
||||
field_value,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -894,3 +902,58 @@ export async function updateGlobalVariable(
|
|||
value,
|
||||
});
|
||||
}
|
||||
|
||||
export async function getVerticesOrder(
|
||||
flowId: string,
|
||||
startNodeId?: string | null,
|
||||
stopNodeId?: string | null
|
||||
): Promise<AxiosResponse<VerticesOrderTypeAPI>> {
|
||||
// nodeId is optional and is a query parameter
|
||||
// if nodeId is not provided, the API will return all vertices
|
||||
const config = {};
|
||||
if (stopNodeId) {
|
||||
config["params"] = { stop_component_id: stopNodeId };
|
||||
} else if (startNodeId) {
|
||||
config["params"] = { start_component_id: startNodeId };
|
||||
}
|
||||
return await api.get(`${BASE_URL_API}build/${flowId}/vertices`, config);
|
||||
}
|
||||
|
||||
export async function postBuildVertex(
|
||||
flowId: string,
|
||||
vertexId: string,
|
||||
input_value: string
|
||||
): Promise<AxiosResponse<VertexBuildTypeAPI>> {
|
||||
// input_value is optional and is a query parameter
|
||||
return await api.post(
|
||||
`${BASE_URL_API}build/${flowId}/vertices/${vertexId}`,
|
||||
input_value ? { inputs: { input_value: input_value } } : undefined
|
||||
);
|
||||
}
|
||||
|
||||
export async function downloadImage({ flowId, fileName }): Promise<any> {
|
||||
return await api.get(`${BASE_URL_API}files/images/${flowId}/${fileName}`);
|
||||
}
|
||||
|
||||
export async function getFlowPool({
|
||||
flowId,
|
||||
nodeId,
|
||||
}: {
|
||||
flowId: string;
|
||||
nodeId?: string;
|
||||
}): Promise<AxiosResponse<{ vertex_builds: FlowPoolType }>> {
|
||||
const config = {};
|
||||
config["params"] = { flow_id: flowId };
|
||||
if (nodeId) {
|
||||
config["params"] = { nodeId };
|
||||
}
|
||||
return await api.get(`${BASE_URL_API}monitor/builds`, config);
|
||||
}
|
||||
|
||||
export async function deleteFlowPool(
|
||||
flowId: string
|
||||
): Promise<AxiosResponse<any>> {
|
||||
const config = {};
|
||||
config["params"] = { flow_id: flowId };
|
||||
return await api.delete(`${BASE_URL_API}monitor/builds`, config);
|
||||
}
|
||||
|
|
|
|||
61
src/frontend/src/icons/Azure/Azure.jsx
Normal file
8
src/frontend/src/icons/Azure/index.tsx
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import React, { forwardRef } from "react";
|
||||
import SvgAzure from "./Azure";
|
||||
|
||||
export const AzureIcon = forwardRef<SVGSVGElement, React.PropsWithChildren<{}>>(
|
||||
(props, ref) => {
|
||||
return <SvgAzure ref={ref} {...props} />;
|
||||
}
|
||||
);
|
||||
23
src/frontend/src/icons/BotMessageSquare/BotMessageSquare.jsx
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
const SvgBotMessageSquare = (props) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
className="lucide lucide-bot-message-square"
|
||||
{...props}
|
||||
>
|
||||
<path d="M12 6V2H8" />
|
||||
<path d="m8 18-4 4V8a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2Z" />
|
||||
<path d="M2 12h2" />
|
||||
<path d="M9 11v2" />
|
||||
<path d="M15 11v2" />
|
||||
<path d="M20 12h2" />
|
||||
</svg>
|
||||
);
|
||||
export default SvgBotMessageSquare;
|
||||
9
src/frontend/src/icons/BotMessageSquare/index.tsx
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import React, { forwardRef } from "react";
|
||||
import SvgChroma from "./BotMessageSquare";
|
||||
|
||||
export const BotMessageSquareIcon = forwardRef<
|
||||
SVGSVGElement,
|
||||
React.PropsWithChildren<{}>
|
||||
>((props, ref) => {
|
||||
return <SvgChroma ref={ref} {...props} />;
|
||||
});
|
||||
64
src/frontend/src/icons/Ollama/Ollama.jsx
Normal file
1
src/frontend/src/icons/Ollama/Ollama.svg
Normal file
|
After Width: | Height: | Size: 122 KiB |
9
src/frontend/src/icons/Ollama/index.tsx
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import React, { forwardRef } from "react";
|
||||
import SvgOllama from "./Ollama";
|
||||
|
||||
export const OllamaIcon = forwardRef<
|
||||
SVGSVGElement,
|
||||
React.PropsWithChildren<{}>
|
||||
>((props, ref) => {
|
||||
return <SvgOllama ref={ref} {...props} />;
|
||||
});
|
||||
67
src/frontend/src/icons/Postgres/Postgres.jsx
Normal file
1
src/frontend/src/icons/Postgres/Postgres.svg
Normal file
|
After Width: | Height: | Size: 273 KiB |
9
src/frontend/src/icons/Postgres/index.tsx
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import React, { forwardRef } from "react";
|
||||
import SvgPostgres from "./Postgres";
|
||||
|
||||
export const PostgresIcon = forwardRef<
|
||||
SVGSVGElement,
|
||||
React.PropsWithChildren<{}>
|
||||
>((props, ref) => {
|
||||
return <SvgPostgres ref={ref} {...props} />;
|
||||
});
|
||||
158
src/frontend/src/icons/Python/Python.jsx
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
export const SvgPython = (props) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={1000}
|
||||
height={1000}
|
||||
viewBox="0 0 750 750"
|
||||
{...props}
|
||||
>
|
||||
<defs>
|
||||
<clipPath id="a">
|
||||
<path d="M45 42h493v494H45Zm0 0" />
|
||||
</clipPath>
|
||||
<clipPath id="b">
|
||||
<path d="M375.207 42.145c-168.414 0-157.902 73.035-157.902 73.035l.207 75.664h160.703v22.699H153.64S45.906 201.313 45.906 371.262c0 169.906 94.055 163.894 94.055 163.894h56.137v-78.86s-3.028-94.054 92.562-94.054h159.375s89.555 1.453 89.555-86.547V130.188s13.601-88.043-162.383-88.043Zm-88.621 50.87c15.984 0 28.918 12.938 28.918 28.922 0 15.981-12.934 28.918-28.918 28.918-.95 0-1.895-.042-2.836-.136a28.448 28.448 0 0 1-5.563-1.098c-.906-.277-1.8-.594-2.675-.957-.875-.36-1.73-.766-2.567-1.21a29.522 29.522 0 0 1-2.437-1.458 29.114 29.114 0 0 1-6.29-5.703 28.46 28.46 0 0 1-1.69-2.281 27.818 27.818 0 0 1-1.458-2.434 29.596 29.596 0 0 1-1.215-2.566 28.942 28.942 0 0 1-2.055-8.238c-.09-.946-.136-1.891-.132-2.837 0-15.984 12.934-28.921 28.918-28.921Zm0 0" />
|
||||
</clipPath>
|
||||
<clipPath id="d">
|
||||
<path d="M217 209h493v494H217Zm0 0" />
|
||||
</clipPath>
|
||||
<clipPath id="e">
|
||||
<path d="M379.977 702.059c168.414 0 157.902-73.036 157.902-73.036l-.207-75.664h-160.7V530.66h224.551s107.758 12.23 107.758-157.7c0-169.925-94.054-163.894-94.054-163.894H559.09v78.84s3.023 94.051-92.563 94.051H307.152s-89.558-1.45-89.558 86.55v145.509s-13.598 88.043 162.383 88.043Zm88.62-50.875a28.573 28.573 0 0 1-5.644-.547 29.24 29.24 0 0 1-2.754-.688 28.56 28.56 0 0 1-5.242-2.168 29.024 29.024 0 0 1-4.715-3.148 29.42 29.42 0 0 1-2.105-1.906 29.448 29.448 0 0 1-1.907-2.102 29.882 29.882 0 0 1-1.687-2.281 28.516 28.516 0 0 1-2.672-5.004 27.907 27.907 0 0 1-.953-2.676 27.808 27.808 0 0 1-.688-2.754 28.29 28.29 0 0 1-.414-2.808 28.598 28.598 0 0 1-.136-2.836c0-15.961 12.937-28.899 28.918-28.899 15.984 0 28.922 12.914 28.922 28.899 0 16.004-12.938 28.918-28.922 28.918Zm0 0" />
|
||||
</clipPath>
|
||||
<linearGradient
|
||||
id="c"
|
||||
x1={16.152}
|
||||
x2={40.334}
|
||||
y1={15.849}
|
||||
y2={40.112}
|
||||
gradientTransform="matrix(20.73047 0 0 20.73046 -285.782 -289.543)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset={0} stopColor="#387EB8" />
|
||||
<stop offset={0.125} stopColor="#387EB8" />
|
||||
<stop offset={0.141} stopColor="#387EB7" />
|
||||
<stop offset={0.156} stopColor="#387DB7" />
|
||||
<stop offset={0.172} stopColor="#387DB6" />
|
||||
<stop offset={0.188} stopColor="#387CB5" />
|
||||
<stop offset={0.203} stopColor="#387CB4" />
|
||||
<stop offset={0.219} stopColor="#387BB3" />
|
||||
<stop offset={0.234} stopColor="#387BB2" />
|
||||
<stop offset={0.25} stopColor="#387AB1" />
|
||||
<stop offset={0.266} stopColor="#387AB1" />
|
||||
<stop offset={0.281} stopColor="#3879B0" />
|
||||
<stop offset={0.297} stopColor="#3879AF" />
|
||||
<stop offset={0.313} stopColor="#3878AE" />
|
||||
<stop offset={0.328} stopColor="#3778AD" />
|
||||
<stop offset={0.344} stopColor="#3777AC" />
|
||||
<stop offset={0.359} stopColor="#3777AB" />
|
||||
<stop offset={0.375} stopColor="#3776AA" />
|
||||
<stop offset={0.391} stopColor="#3776AA" />
|
||||
<stop offset={0.406} stopColor="#3775A9" />
|
||||
<stop offset={0.422} stopColor="#3775A8" />
|
||||
<stop offset={0.438} stopColor="#3774A7" />
|
||||
<stop offset={0.453} stopColor="#3774A6" />
|
||||
<stop offset={0.469} stopColor="#3773A5" />
|
||||
<stop offset={0.484} stopColor="#3773A4" />
|
||||
<stop offset={0.498} stopColor="#3772A4" />
|
||||
<stop offset={0.5} stopColor="#3772A3" />
|
||||
<stop offset={0.502} stopColor="#3772A3" />
|
||||
<stop offset={0.516} stopColor="#3771A2" />
|
||||
<stop offset={0.531} stopColor="#3771A2" />
|
||||
<stop offset={0.547} stopColor="#3771A1" />
|
||||
<stop offset={0.563} stopColor="#3770A0" />
|
||||
<stop offset={0.578} stopColor="#37709F" />
|
||||
<stop offset={0.594} stopColor="#376F9E" />
|
||||
<stop offset={0.609} stopColor="#376F9E" />
|
||||
<stop offset={0.625} stopColor="#376E9D" />
|
||||
<stop offset={0.641} stopColor="#376E9C" />
|
||||
<stop offset={0.656} stopColor="#366D9B" />
|
||||
<stop offset={0.672} stopColor="#366D9A" />
|
||||
<stop offset={0.688} stopColor="#366C99" />
|
||||
<stop offset={0.703} stopColor="#366C98" />
|
||||
<stop offset={0.719} stopColor="#366B97" />
|
||||
<stop offset={0.734} stopColor="#366B97" />
|
||||
<stop offset={0.75} stopColor="#366A96" />
|
||||
<stop offset={0.766} stopColor="#366A95" />
|
||||
<stop offset={0.781} stopColor="#366994" />
|
||||
<stop offset={0.813} stopColor="#366994" />
|
||||
<stop offset={0.875} stopColor="#366994" />
|
||||
<stop offset={1} stopColor="#366994" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="f"
|
||||
x1={23.548}
|
||||
x2={48.282}
|
||||
y1={23.842}
|
||||
y2={47.538}
|
||||
gradientTransform="matrix(20.73047 0 0 20.73046 -285.782 -289.543)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset={0} stopColor="#FFE052" />
|
||||
<stop offset={0.125} stopColor="#FFE052" />
|
||||
<stop offset={0.188} stopColor="#FFE052" />
|
||||
<stop offset={0.219} stopColor="#FFE052" />
|
||||
<stop offset={0.234} stopColor="#FFDF51" />
|
||||
<stop offset={0.25} stopColor="#FFDF51" />
|
||||
<stop offset={0.266} stopColor="#FFDE50" />
|
||||
<stop offset={0.281} stopColor="#FFDD4F" />
|
||||
<stop offset={0.297} stopColor="#FFDD4E" />
|
||||
<stop offset={0.313} stopColor="#FFDC4E" />
|
||||
<stop offset={0.328} stopColor="#FFDB4D" />
|
||||
<stop offset={0.344} stopColor="#FFDB4C" />
|
||||
<stop offset={0.359} stopColor="#FFDA4B" />
|
||||
<stop offset={0.375} stopColor="#FFD94B" />
|
||||
<stop offset={0.391} stopColor="#FFD94A" />
|
||||
<stop offset={0.406} stopColor="#FFD849" />
|
||||
<stop offset={0.422} stopColor="#FFD748" />
|
||||
<stop offset={0.438} stopColor="#FFD748" />
|
||||
<stop offset={0.453} stopColor="#FFD647" />
|
||||
<stop offset={0.469} stopColor="#FFD546" />
|
||||
<stop offset={0.484} stopColor="#FFD545" />
|
||||
<stop offset={0.486} stopColor="#FFD445" />
|
||||
<stop offset={0.5} stopColor="#FFD444" />
|
||||
<stop offset={0.514} stopColor="#FFD444" />
|
||||
<stop offset={0.516} stopColor="#FFD343" />
|
||||
<stop offset={0.531} stopColor="#FFD343" />
|
||||
<stop offset={0.547} stopColor="#FFD242" />
|
||||
<stop offset={0.563} stopColor="#FFD242" />
|
||||
<stop offset={0.578} stopColor="#FFD141" />
|
||||
<stop offset={0.594} stopColor="#FFD040" />
|
||||
<stop offset={0.609} stopColor="#FFD03F" />
|
||||
<stop offset={0.625} stopColor="#FFCF3F" />
|
||||
<stop offset={0.641} stopColor="#FFCE3E" />
|
||||
<stop offset={0.656} stopColor="#FFCE3D" />
|
||||
<stop offset={0.672} stopColor="#FFCD3C" />
|
||||
<stop offset={0.688} stopColor="#FFCC3C" />
|
||||
<stop offset={0.703} stopColor="#FFCC3B" />
|
||||
<stop offset={0.719} stopColor="#FFCB3A" />
|
||||
<stop offset={0.734} stopColor="#FFCA39" />
|
||||
<stop offset={0.75} stopColor="#FFCA39" />
|
||||
<stop offset={0.766} stopColor="#FFC938" />
|
||||
<stop offset={0.781} stopColor="#FFC837" />
|
||||
<stop offset={0.797} stopColor="#FFC836" />
|
||||
<stop offset={0.813} stopColor="#FFC736" />
|
||||
<stop offset={0.828} stopColor="#FFC635" />
|
||||
<stop offset={0.844} stopColor="#FFC634" />
|
||||
<stop offset={0.859} stopColor="#FFC533" />
|
||||
<stop offset={0.875} stopColor="#FFC433" />
|
||||
<stop offset={0.891} stopColor="#FFC432" />
|
||||
<stop offset={0.906} stopColor="#FFC331" />
|
||||
<stop offset={0.938} stopColor="#FFC331" />
|
||||
<stop offset={1} stopColor="#FFC331" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path fill="#fff" d="M-75-75h900v900H-75z" />
|
||||
<path fill="#fff" d="M-75-75h900v900H-75z" />
|
||||
<g clipPath="url(#a)">
|
||||
<g clipPath="url(#b)">
|
||||
<path fill="url(#c)" d="M45.906 42.145v499.023h505.285V42.145Zm0 0" />
|
||||
</g>
|
||||
</g>
|
||||
<g clipPath="url(#d)">
|
||||
<g clipPath="url(#e)">
|
||||
<path fill="url(#f)" d="M203.996 203.035V702.06h505.285V203.035Zm0 0" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
export default SvgPython;
|
||||
41
src/frontend/src/icons/Python/Python.svg
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={800}
|
||||
height={800}
|
||||
fill="none"
|
||||
viewBox="0 0 64 64"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fill="url(#a)"
|
||||
d="M31.885 16c-8.124 0-7.617 3.523-7.617 3.523l.01 3.65h7.752v1.095H21.197S16 23.678 16 31.876c0 8.196 4.537 7.906 4.537 7.906h2.708v-3.804s-.146-4.537 4.465-4.537h7.688s4.32.07 4.32-4.175v-7.019S40.374 16 31.885 16zm-4.275 2.454a1.394 1.394 0 1 1 0 2.79 1.393 1.393 0 0 1-1.395-1.395c0-.771.624-1.395 1.395-1.395z"
|
||||
/>
|
||||
<path
|
||||
fill="url(#b)"
|
||||
d="M32.115 47.833c8.124 0 7.617-3.523 7.617-3.523l-.01-3.65H31.97v-1.095h10.832S48 40.155 48 31.958c0-8.197-4.537-7.906-4.537-7.906h-2.708v3.803s.146 4.537-4.465 4.537h-7.688s-4.32-.07-4.32 4.175v7.019s-.656 4.247 7.833 4.247zm4.275-2.454a1.393 1.393 0 0 1-1.395-1.395 1.394 1.394 0 1 1 1.395 1.395z"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="a"
|
||||
x1={19.075}
|
||||
x2={34.898}
|
||||
y1={18.782}
|
||||
y2={34.658}
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#387EB8" />
|
||||
<stop offset={1} stopColor="#366994" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="b"
|
||||
x1={28.809}
|
||||
x2={45.803}
|
||||
y1={28.882}
|
||||
y2={45.163}
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#FFE052" />
|
||||
<stop offset={1} stopColor="#FFC331" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
9
src/frontend/src/icons/Python/index.tsx
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import React, { forwardRef } from "react";
|
||||
import SvgPython from "./Python";
|
||||
|
||||
export const PythonIcon = forwardRef<
|
||||
SVGSVGElement,
|
||||
React.PropsWithChildren<{}>
|
||||
>((props, ref) => {
|
||||
return <SvgPython ref={ref} {...props} />;
|
||||
});
|
||||
23
src/frontend/src/icons/QianFanChat/QianFanChat.jsx
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
export const SvgQianFanChat = (props) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={1000}
|
||||
height={1000}
|
||||
viewBox="0 0 750 750"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fill="#00d263"
|
||||
d="M602.984 155.953c.024-.02.059-.043.086-.07L379.008 26.43l-37.793 21.71a86.37 86.37 0 0 0-5.809 3.028L95.051 189.543l106.808 61.68a26.254 26.254 0 0 0 26.223.015l53.691-30.91 69.95-40.273a53.06 53.06 0 0 1 52.933 0l55.02 31.675 67.738 38.997a26.257 26.257 0 0 0 26.184.011l107.093-61.566Zm0 0"
|
||||
/>
|
||||
<path
|
||||
fill="#ff502e"
|
||||
d="M586.313 303.273a26.254 26.254 0 0 0-13.145 22.747V467.78c0 18.887-10.11 36.336-26.5 45.778l-125.336 72.16a26.24 26.24 0 0 0-13.156 22.75v123.054l242.152-139.41c.34-.191.649-.406.98-.605l41.848-24.098V241.754Zm0 0"
|
||||
/>
|
||||
<path
|
||||
fill="#0062ff"
|
||||
d="m331.3 585.71-121.59-70c-16.398-9.44-26.5-26.89-26.5-45.776V328.09a26.253 26.253 0 0 0-13.151-22.746L63.266 243.848l-.047-.203v.175l.004 276.547v48.934l37.5 21.629a84.188 84.188 0 0 0 5.25 3.304l238.476 137.297v-123.07a26.25 26.25 0 0 0-13.148-22.75"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
export default SvgQianFanChat;
|
||||
1
src/frontend/src/icons/QianFanChat/QianFanChat.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1000" zoomAndPan="magnify" viewBox="0 0 750 749.999995" height="1000" preserveAspectRatio="xMidYMid meet" version="1.0"><path fill="#00d263" d="M 602.984375 155.953125 C 603.007812 155.933594 603.042969 155.910156 603.070312 155.882812 L 379.007812 26.429688 L 341.214844 48.140625 C 339.253906 49.078125 337.308594 50.074219 335.40625 51.167969 L 95.050781 189.542969 L 201.859375 251.222656 C 209.964844 255.902344 219.964844 255.914062 228.082031 251.238281 L 281.773438 220.328125 L 351.722656 180.054688 C 368.105469 170.625 388.273438 170.625 404.65625 180.054688 L 459.675781 211.730469 L 527.414062 250.726562 C 535.515625 255.394531 545.484375 255.394531 553.597656 250.738281 L 660.691406 189.171875 Z M 602.984375 155.953125 " fill-opacity="1" fill-rule="nonzero"/><path fill="#ff502e" d="M 586.3125 303.273438 C 578.179688 307.960938 573.167969 316.632812 573.167969 326.019531 L 573.167969 467.78125 C 573.167969 486.667969 563.058594 504.117188 546.667969 513.558594 L 421.332031 585.71875 C 413.191406 590.402344 408.175781 599.074219 408.175781 608.46875 L 408.175781 731.523438 L 650.328125 592.113281 C 650.667969 591.921875 650.976562 591.707031 651.308594 591.507812 L 693.15625 567.410156 L 693.15625 241.753906 Z M 586.3125 303.273438 " fill-opacity="1" fill-rule="nonzero"/><path fill="#0062ff" d="M 331.300781 585.710938 L 209.710938 515.710938 C 193.3125 506.269531 183.210938 488.820312 183.210938 469.933594 L 183.210938 328.089844 C 183.210938 318.703125 178.195312 310.03125 170.058594 305.34375 L 63.265625 243.847656 L 63.21875 243.644531 L 63.21875 243.820312 L 63.222656 520.367188 L 63.222656 569.300781 L 100.722656 590.929688 C 102.425781 592.085938 104.175781 593.199219 105.972656 594.234375 L 344.449219 731.53125 L 344.449219 608.460938 C 344.449219 599.070312 339.4375 590.398438 331.300781 585.710938 " fill-opacity="1" fill-rule="nonzero"/></svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
9
src/frontend/src/icons/QianFanChat/index.tsx
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import React, { forwardRef } from "react";
|
||||
import SvgQianFanChat from "./QianFanChat";
|
||||
|
||||
export const QianFanChatIcon = forwardRef<
|
||||
SVGSVGElement,
|
||||
React.PropsWithChildren<{}>
|
||||
>((props, ref) => {
|
||||
return <SvgQianFanChat ref={ref} {...props} />;
|
||||
});
|
||||
67
src/frontend/src/icons/Redis/Redis.jsx
Normal file
1
src/frontend/src/icons/Redis/Redis.svg
Normal file
|
After Width: | Height: | Size: 304 KiB |
8
src/frontend/src/icons/Redis/index.tsx
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import React, { forwardRef } from "react";
|
||||
import { SvgRedis } from "./Redis";
|
||||
|
||||
export const RedisIcon = forwardRef<SVGSVGElement, React.PropsWithChildren<{}>>(
|
||||
(props, ref) => {
|
||||
return <SvgRedis ref={ref} {...props} />;
|
||||
}
|
||||
);
|
||||
|
Before Width: | Height: | Size: 156 KiB After Width: | Height: | Size: 406 KiB |
|
|
@ -98,6 +98,9 @@ const ApiModal = forwardRef(
|
|||
let arrNodesWithValues: string[] = [];
|
||||
|
||||
flow["data"]!["nodes"].forEach((node) => {
|
||||
if (!node["data"]["node"]["template"]) {
|
||||
return;
|
||||
}
|
||||
Object.keys(node["data"]["node"]["template"])
|
||||
.filter(
|
||||
(templateField) =>
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ function ConfirmationModal({
|
|||
);
|
||||
|
||||
return (
|
||||
<BaseModal size={size} open={modalOpen} setOpen={setModalOpen}>
|
||||
<BaseModal size={size} open={open} setOpen={setModalOpen}>
|
||||
<BaseModal.Trigger>{triggerChild}</BaseModal.Trigger>
|
||||
<BaseModal.Header description={titleHeader ?? null}>
|
||||
<span className="pr-2">{title}</span>
|
||||
|
|
@ -89,6 +89,7 @@ function ConfirmationModal({
|
|||
setModalOpen(false);
|
||||
onConfirm(index, data);
|
||||
}}
|
||||
data-testid="replace-button"
|
||||
>
|
||||
{confirmationText}
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -35,9 +35,8 @@ export default function DeleteConfirmationModal({
|
|||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<span>
|
||||
Are you sure you want to delete this {description ?? "component"}?
|
||||
<br></br>
|
||||
This action cannot be undone.
|
||||
Confirm deletion of {description ?? "component"}?<br></br>
|
||||
Note: This action is irreversible.
|
||||
</span>
|
||||
<DialogFooter>
|
||||
<DialogClose>
|
||||
|
|
|
|||
|
|
@ -58,7 +58,6 @@ const EditNodeModal = forwardRef(
|
|||
) => {
|
||||
const [myData, setMyData] = useState(data);
|
||||
|
||||
const setPending = useFlowStore((state) => state.setPending);
|
||||
const edges = useFlowStore((state) => state.edges);
|
||||
const setNode = useFlowStore((state) => state.setNode);
|
||||
const setNoticeData = useAlertStore((state) => state.setNoticeData);
|
||||
|
|
@ -172,7 +171,19 @@ const EditNodeModal = forwardRef(
|
|||
)
|
||||
) ?? false;
|
||||
return (
|
||||
<TableRow key={index} className="h-10">
|
||||
<TableRow
|
||||
key={index}
|
||||
className={
|
||||
"h-10 " +
|
||||
((templateParam === "code" &&
|
||||
myData.node?.template[templateParam].type ===
|
||||
"code") ||
|
||||
(templateParam.includes("code") &&
|
||||
myData.node?.template[templateParam].proxy)
|
||||
? " hidden "
|
||||
: "")
|
||||
}
|
||||
>
|
||||
<TableCell className="truncate p-0 text-center text-sm text-foreground sm:px-3">
|
||||
<ShadTooltip
|
||||
content={
|
||||
|
|
@ -249,7 +260,11 @@ const EditNodeModal = forwardRef(
|
|||
/>
|
||||
) : (
|
||||
<InputComponent
|
||||
id={"input-" + index}
|
||||
id={
|
||||
"input-" +
|
||||
myData.node.template[templateParam]
|
||||
.name
|
||||
}
|
||||
editNode={true}
|
||||
options={globalVariablesEntries}
|
||||
disabled={disabled}
|
||||
|
|
@ -379,7 +394,10 @@ const EditNodeModal = forwardRef(
|
|||
<div className="ml-auto">
|
||||
{" "}
|
||||
<ToggleShadComponent
|
||||
id={"toggle-edit-" + index}
|
||||
id={
|
||||
"toggle-edit-" +
|
||||
myData.node.template[templateParam].name
|
||||
}
|
||||
disabled={disabled}
|
||||
enabled={
|
||||
myData.node.template[templateParam]
|
||||
|
|
@ -392,6 +410,7 @@ const EditNodeModal = forwardRef(
|
|||
);
|
||||
}}
|
||||
size="small"
|
||||
editNode={true}
|
||||
/>
|
||||
</div>
|
||||
) : myData.node?.template[templateParam]
|
||||
|
|
@ -432,14 +451,20 @@ const EditNodeModal = forwardRef(
|
|||
myData.node.template[templateParam]
|
||||
.value ?? "Choose an option"
|
||||
}
|
||||
id={"dropdown-edit-" + index}
|
||||
id={
|
||||
"dropdown-edit-" +
|
||||
myData.node.template[templateParam].name
|
||||
}
|
||||
></Dropdown>
|
||||
</div>
|
||||
) : myData.node?.template[templateParam]
|
||||
.type === "int" ? (
|
||||
<div className="mx-auto">
|
||||
<IntComponent
|
||||
id={"edit-int-input-" + index}
|
||||
id={
|
||||
"edit-int-input-" +
|
||||
myData.node.template[templateParam].name
|
||||
}
|
||||
disabled={disabled}
|
||||
editNode={true}
|
||||
value={
|
||||
|
|
@ -534,7 +559,10 @@ const EditNodeModal = forwardRef(
|
|||
onChange={(value: string | string[]) => {
|
||||
handleOnNewValue(value, templateParam);
|
||||
}}
|
||||
id={"code-area-edit" + index}
|
||||
id={
|
||||
"code-area-edit" +
|
||||
myData.node.template[templateParam].name
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
) : myData.node?.template[templateParam]
|
||||
|
|
@ -560,6 +588,7 @@ const EditNodeModal = forwardRef(
|
|||
}}
|
||||
disabled={disabled}
|
||||
size="small"
|
||||
editNode={true}
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
|
|
@ -586,7 +615,6 @@ const EditNodeModal = forwardRef(
|
|||
node: myData.node,
|
||||
},
|
||||
}));
|
||||
setPending(true);
|
||||
setOpen(false);
|
||||
}}
|
||||
type="submit"
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { useEffect, useRef, useState } from "react";
|
|||
import IconComponent from "../../components/genericIconComponent";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { Input } from "../../components/ui/input";
|
||||
import { COPIED_NOTICE_ALERT } from "../../constants/alerts_constants";
|
||||
import { createApiKey } from "../../controllers/API";
|
||||
import useAlertStore from "../../stores/alertStore";
|
||||
import { ApiKeyType } from "../../types/components";
|
||||
|
|
@ -47,7 +48,7 @@ export default function SecretKeyModal({
|
|||
inputRef?.current?.focus();
|
||||
inputRef?.current?.select();
|
||||
setSuccessData({
|
||||
title: "API Key copied!",
|
||||
title: COPIED_NOTICE_ALERT,
|
||||
});
|
||||
setTextCopied(false);
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,16 @@ import { useContext, useState } from "react";
|
|||
import IconComponent from "../../components/genericIconComponent";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { Input } from "../../components/ui/input";
|
||||
import {
|
||||
API_ERROR_ALERT,
|
||||
API_SUCCESS_ALERT,
|
||||
} from "../../constants/alerts_constants";
|
||||
import {
|
||||
CREATE_API_KEY,
|
||||
INSERT_API_KEY,
|
||||
INVALID_API_KEY,
|
||||
NO_API_KEY,
|
||||
} from "../../constants/constants";
|
||||
import { AuthContext } from "../../contexts/authContext";
|
||||
import { addApiKeyStore } from "../../controllers/API";
|
||||
import useAlertStore from "../../stores/alertStore";
|
||||
|
|
@ -32,7 +42,7 @@ export default function StoreApiKeyModal({
|
|||
addApiKeyStore(apiKeyValue).then(
|
||||
() => {
|
||||
setSuccessData({
|
||||
title: "Success! Your API Key has been saved.",
|
||||
title: API_SUCCESS_ALERT,
|
||||
});
|
||||
storeApiKey(apiKeyValue);
|
||||
setOpen(false);
|
||||
|
|
@ -42,7 +52,7 @@ export default function StoreApiKeyModal({
|
|||
},
|
||||
(error) => {
|
||||
setErrorData({
|
||||
title: "There was an error saving the API Key, please try again.",
|
||||
title: API_ERROR_ALERT,
|
||||
list: [error["response"]["data"]["detail"]],
|
||||
});
|
||||
setHasApiKey(false);
|
||||
|
|
@ -59,10 +69,10 @@ export default function StoreApiKeyModal({
|
|||
<BaseModal.Header
|
||||
description={
|
||||
(hasApiKey && !validApiKey
|
||||
? "Your API key is not valid. "
|
||||
? INVALID_API_KEY
|
||||
: !hasApiKey
|
||||
? "You don't have an API key. "
|
||||
: "") + "Insert your Langflow API key."
|
||||
? NO_API_KEY
|
||||
: "") + INSERT_API_KEY
|
||||
}
|
||||
>
|
||||
<span className="pr-2">API Key</span>
|
||||
|
|
@ -97,7 +107,7 @@ export default function StoreApiKeyModal({
|
|||
</div>
|
||||
<div className="flex items-end justify-between">
|
||||
<span className="pr-1 text-xs text-muted-foreground">
|
||||
Don’t have an API key? Sign up at{" "}
|
||||
{CREATE_API_KEY}{" "}
|
||||
<a
|
||||
className="text-high-indigo underline"
|
||||
href="https://langflow.store/"
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ type TriggerProps = {
|
|||
};
|
||||
|
||||
const Content: React.FC<ContentProps> = ({ children }) => {
|
||||
return <div className="h-full w-full">{children}</div>;
|
||||
return <div className="flex h-full w-full flex-col">{children}</div>;
|
||||
};
|
||||
const Trigger: React.FC<TriggerProps> = ({ children, asChild, disable }) => {
|
||||
return (
|
||||
|
|
@ -66,6 +66,8 @@ interface BaseModalProps {
|
|||
| "small"
|
||||
| "medium"
|
||||
| "large"
|
||||
| "three-cards"
|
||||
| "large-thin"
|
||||
| "large-h-full"
|
||||
| "small-h-full"
|
||||
| "medium-h-full"
|
||||
|
|
@ -125,7 +127,15 @@ function BaseModal({
|
|||
minWidth = "min-w-[60vw]";
|
||||
break;
|
||||
case "large":
|
||||
minWidth = "min-w-[80vw]";
|
||||
minWidth = "min-w-[85vw]";
|
||||
height = "h-[80vh]";
|
||||
break;
|
||||
case "three-cards":
|
||||
minWidth = "min-w-[1066px]";
|
||||
height = "h-[80vh]";
|
||||
break;
|
||||
case "large-thin":
|
||||
minWidth = "min-w-[65vw]";
|
||||
height = "h-[80vh]";
|
||||
break;
|
||||
case "large-h-full":
|
||||
|
|
|
|||
|
|
@ -9,10 +9,21 @@ import AceEditor from "react-ace";
|
|||
import IconComponent from "../../components/genericIconComponent";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { Input } from "../../components/ui/input";
|
||||
import { CODE_PROMPT_DIALOG_SUBTITLE } from "../../constants/constants";
|
||||
import {
|
||||
BUG_ALERT,
|
||||
CODE_ERROR_ALERT,
|
||||
CODE_SUCCESS_ALERT,
|
||||
FUNC_ERROR_ALERT,
|
||||
IMPORT_ERROR_ALERT,
|
||||
} from "../../constants/alerts_constants";
|
||||
import {
|
||||
CODE_PROMPT_DIALOG_SUBTITLE,
|
||||
EDIT_CODE_TITLE,
|
||||
} from "../../constants/constants";
|
||||
import { postCustomComponent, postValidateCode } from "../../controllers/API";
|
||||
import useAlertStore from "../../stores/alertStore";
|
||||
import { useDarkStore } from "../../stores/darkStore";
|
||||
import { CodeErrorDataTypeAPI } from "../../types/api";
|
||||
import { codeAreaModalPropsType } from "../../types/components";
|
||||
import BaseModal from "../baseModal";
|
||||
|
||||
|
|
@ -24,15 +35,20 @@ export default function CodeAreaModal({
|
|||
children,
|
||||
dynamic,
|
||||
readonly = false,
|
||||
open: myOpen,
|
||||
setOpen: mySetOpen,
|
||||
}: codeAreaModalPropsType): JSX.Element {
|
||||
const [code, setCode] = useState(value);
|
||||
const [open, setOpen] =
|
||||
mySetOpen !== undefined && myOpen !== undefined
|
||||
? [myOpen, mySetOpen]
|
||||
: useState(false);
|
||||
const dark = useDarkStore((state) => state.dark);
|
||||
|
||||
const [height, setHeight] = useState<string | null>(null);
|
||||
const setSuccessData = useAlertStore((state) => state.setSuccessData);
|
||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||
const [error, setError] = useState<{
|
||||
detail: { error: string | undefined; traceback: string | undefined };
|
||||
detail: CodeErrorDataTypeAPI;
|
||||
} | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -51,7 +67,7 @@ export default function CodeAreaModal({
|
|||
let funcErrors = apiReturn.data.function.errors;
|
||||
if (funcErrors.length === 0 && importsErrors.length === 0) {
|
||||
setSuccessData({
|
||||
title: "Code is ready to run",
|
||||
title: CODE_SUCCESS_ALERT,
|
||||
});
|
||||
setOpen(false);
|
||||
setValue(code);
|
||||
|
|
@ -59,26 +75,26 @@ export default function CodeAreaModal({
|
|||
} else {
|
||||
if (funcErrors.length !== 0) {
|
||||
setErrorData({
|
||||
title: "There is an error in your function",
|
||||
title: FUNC_ERROR_ALERT,
|
||||
list: funcErrors,
|
||||
});
|
||||
}
|
||||
if (importsErrors.length !== 0) {
|
||||
setErrorData({
|
||||
title: "There is an error in your imports",
|
||||
title: IMPORT_ERROR_ALERT,
|
||||
list: importsErrors,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setErrorData({
|
||||
title: "Something went wrong, please try again",
|
||||
title: BUG_ALERT,
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((_) => {
|
||||
setErrorData({
|
||||
title: "There is something wrong with this code, please review it",
|
||||
title: CODE_ERROR_ALERT,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -126,8 +142,6 @@ export default function CodeAreaModal({
|
|||
};
|
||||
}, [error, setHeight]);
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setCode(value);
|
||||
}, [value, open]);
|
||||
|
|
@ -136,7 +150,7 @@ export default function CodeAreaModal({
|
|||
<BaseModal open={open} setOpen={setOpen}>
|
||||
<BaseModal.Trigger>{children}</BaseModal.Trigger>
|
||||
<BaseModal.Header description={CODE_PROMPT_DIALOG_SUBTITLE}>
|
||||
<span className="pr-2">Edit Code</span>
|
||||
<span className="pr-2"> {EDIT_CODE_TITLE} </span>
|
||||
<IconComponent
|
||||
name="prompts"
|
||||
className="h-6 w-6 pl-1 text-primary "
|
||||
|
|
|
|||
|
|
@ -3,7 +3,12 @@ import EditFlowSettings from "../../components/EditFlowSettingsComponent";
|
|||
import IconComponent from "../../components/genericIconComponent";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { Checkbox } from "../../components/ui/checkbox";
|
||||
import { EXPORT_DIALOG_SUBTITLE } from "../../constants/constants";
|
||||
import { API_WARNING_NOTICE_ALERT } from "../../constants/alerts_constants";
|
||||
import {
|
||||
ALERT_SAVE_WITH_API,
|
||||
EXPORT_DIALOG_SUBTITLE,
|
||||
SAVE_WITH_API_CHECKBOX,
|
||||
} from "../../constants/constants";
|
||||
import useAlertStore from "../../stores/alertStore";
|
||||
import { useDarkStore } from "../../stores/darkStore";
|
||||
import useFlowsManagerStore from "../../stores/flowsManagerStore";
|
||||
|
|
@ -51,12 +56,11 @@ const ExportModal = forwardRef(
|
|||
}}
|
||||
/>
|
||||
<label htmlFor="terms" className="export-modal-save-api text-sm ">
|
||||
Save with my API keys
|
||||
{SAVE_WITH_API_CHECKBOX}
|
||||
</label>
|
||||
</div>
|
||||
<span className=" text-xs text-destructive ">
|
||||
Caution: Uncheck this box only removes API keys from fields
|
||||
specifically designated for API keys.
|
||||
{ALERT_SAVE_WITH_API}
|
||||
</span>
|
||||
</BaseModal.Content>
|
||||
|
||||
|
|
@ -77,8 +81,7 @@ const ExportModal = forwardRef(
|
|||
description
|
||||
);
|
||||
setNoticeData({
|
||||
title:
|
||||
"Warning: Critical data, JSON file may include API keys.",
|
||||
title: API_WARNING_NOTICE_ALERT,
|
||||
});
|
||||
} else
|
||||
downloadFlow(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
import { useEffect } from "react";
|
||||
import IconComponent from "../../../components/genericIconComponent";
|
||||
import { Textarea } from "../../../components/ui/textarea";
|
||||
import {
|
||||
CHAT_INPUT_PLACEHOLDER,
|
||||
CHAT_INPUT_PLACEHOLDER_SEND,
|
||||
} from "../../../constants/constants";
|
||||
import { chatInputType } from "../../../types/components";
|
||||
import { classNames } from "../../../utils/utils";
|
||||
|
||||
|
|
@ -51,7 +55,7 @@ export default function ChatInput({
|
|||
? "Thinking..."
|
||||
: typeof chatValue === "object" &&
|
||||
Object.keys(chatValue)?.length === 0
|
||||
? "No chat input variables found. Click to run your flow."
|
||||
? CHAT_INPUT_PLACEHOLDER
|
||||
: chatValue
|
||||
}
|
||||
onChange={(event): void => {
|
||||
|
|
@ -67,9 +71,7 @@ export default function ChatInput({
|
|||
"form-modal-lockchat"
|
||||
)}
|
||||
placeholder={
|
||||
noInput
|
||||
? "No chat input variables found. Click to run your flow."
|
||||
: "Send a message..."
|
||||
noInput ? CHAT_INPUT_PLACEHOLDER : CHAT_INPUT_PLACEHOLDER_SEND
|
||||
}
|
||||
/>
|
||||
<div className="form-modal-send-icon-position">
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ export function CodeBlock({ language, value }: Props): JSX.Element {
|
|||
<div className="flex items-center">
|
||||
<button className="code-block-modal-button" onClick={copyToClipboard}>
|
||||
{isCopied ? <IconCheck size={18} /> : <IconClipboard size={18} />}
|
||||
{isCopied ? "Copied!" : "Copy code"}
|
||||
{isCopied ? "Copied!" : "Copy Code"}
|
||||
</button>
|
||||
<button className="code-block-modal-button" onClick={downloadAsFile}>
|
||||
<IconDownload size={18} />
|
||||
|
|
|
|||
|
|
@ -20,7 +20,17 @@ import {
|
|||
DialogTrigger,
|
||||
} from "../../components/ui/dialog";
|
||||
import { Textarea } from "../../components/ui/textarea";
|
||||
import { CHAT_FORM_DIALOG_SUBTITLE } from "../../constants/constants";
|
||||
import {
|
||||
CHAT_ERROR_ALERT,
|
||||
INFO_MISSING_ALERT,
|
||||
MSG_ERROR_ALERT,
|
||||
} from "../../constants/alerts_constants";
|
||||
import {
|
||||
CHAT_FIRST_INITIAL_TEXT,
|
||||
CHAT_FORM_DIALOG_SUBTITLE,
|
||||
CHAT_SECOND_INITIAL_TEXT,
|
||||
LANGFLOW_CHAT_TITLE,
|
||||
} from "../../constants/constants";
|
||||
import { AuthContext } from "../../contexts/authContext";
|
||||
import { getBuildStatus } from "../../controllers/API";
|
||||
import useAlertStore from "../../stores/alertStore";
|
||||
|
|
@ -169,7 +179,7 @@ export default function FormModal({
|
|||
connectWS();
|
||||
} else {
|
||||
setErrorData({
|
||||
title: "Please build the flow again before using the chat.",
|
||||
title: CHAT_ERROR_ALERT,
|
||||
});
|
||||
}
|
||||
})
|
||||
|
|
@ -205,7 +215,6 @@ export default function FormModal({
|
|||
if (Array.isArray(data) && data.length > 0) {
|
||||
//set chat history
|
||||
setChatHistory((_) => {
|
||||
console.log(data);
|
||||
let newChatHistory: ChatMessageType[] = [];
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
if (data[i].type === "prompt" && data[i].prompt) {
|
||||
|
|
@ -357,7 +366,7 @@ export default function FormModal({
|
|||
}
|
||||
} catch (error) {
|
||||
setErrorData({
|
||||
title: "There was an error sending the message",
|
||||
title: MSG_ERROR_ALERT,
|
||||
list: [(error as { message: string }).message],
|
||||
});
|
||||
setChatValue(data.inputs);
|
||||
|
|
@ -402,7 +411,7 @@ export default function FormModal({
|
|||
}
|
||||
} else {
|
||||
setErrorData({
|
||||
title: "Oops! Looks like you missed some required information:",
|
||||
title: INFO_MISSING_ALERT,
|
||||
list: nodeValidationErrors,
|
||||
});
|
||||
}
|
||||
|
|
@ -440,7 +449,7 @@ export default function FormModal({
|
|||
<DialogDescription>{CHAT_FORM_DIALOG_SUBTITLE}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="form-modal-iv-box ">
|
||||
<div className="flex-max-width mt-2 h-[80vh]">
|
||||
<div className="form-modal-iv-size">
|
||||
<div className="file-component-arrangement">
|
||||
<IconComponent
|
||||
|
|
@ -577,6 +586,7 @@ export default function FormModal({
|
|||
chatHistory.length - 1 === index ? true : false
|
||||
}
|
||||
key={index}
|
||||
updateChat={() => {}}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
|
|
@ -584,20 +594,20 @@ export default function FormModal({
|
|||
<span>
|
||||
👋{" "}
|
||||
<span className="langflow-chat-span">
|
||||
Langflow Chat
|
||||
{LANGFLOW_CHAT_TITLE}
|
||||
</span>
|
||||
</span>
|
||||
<br />
|
||||
<div className="langflow-chat-desc">
|
||||
<span className="langflow-chat-desc-span">
|
||||
Start a conversation and click the agent's thoughts{" "}
|
||||
{CHAT_FIRST_INITIAL_TEXT}{" "}
|
||||
<span>
|
||||
<IconComponent
|
||||
name="MessageSquare"
|
||||
className="mx-1 inline h-5 w-5 animate-bounce "
|
||||
/>
|
||||
</span>{" "}
|
||||
to inspect the chaining process.
|
||||
{CHAT_SECOND_INITIAL_TEXT}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,13 @@ import { Badge } from "../../components/ui/badge";
|
|||
import { Button } from "../../components/ui/button";
|
||||
import { Textarea } from "../../components/ui/textarea";
|
||||
import {
|
||||
BUG_ALERT,
|
||||
PROMPT_ERROR_ALERT,
|
||||
PROMPT_SUCCESS_ALERT,
|
||||
TEMP_NOTICE_ALERT,
|
||||
} from "../../constants/alerts_constants";
|
||||
import {
|
||||
EDIT_TEXT_PLACEHOLDER,
|
||||
INVALID_CHARACTERS,
|
||||
MAX_WORDS_HIGHLIGHT,
|
||||
PROMPT_DIALOG_SUBTITLE,
|
||||
|
|
@ -90,13 +97,23 @@ export default function GenericModal({
|
|||
useEffect(() => {
|
||||
setInputValue(value);
|
||||
}, [value, modalOpen]);
|
||||
|
||||
const coloredContent = (inputValue || "")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(regexHighlight, varHighlightHTML({ name: "$1" }))
|
||||
.replace(/\n/g, "<br />");
|
||||
.replace(regexHighlight, (match, p1, p2) => {
|
||||
// Decide which group was matched. If p1 is not undefined, do nothing
|
||||
// we don't want to change the text. If p2 is not undefined, then we
|
||||
// have a variable, so we should highlight it.
|
||||
// ! This will not work with multiline or indented json yet
|
||||
if (p1 !== undefined) {
|
||||
return match;
|
||||
} else if (p2 !== undefined) {
|
||||
return varHighlightHTML({ name: p2 });
|
||||
}
|
||||
|
||||
return match;
|
||||
})
|
||||
.replace(/\n/g, "<br />");
|
||||
function getClassByNumberLength(): string {
|
||||
let sumOfCaracteres: number = 0;
|
||||
wordsHighlight.forEach((element) => {
|
||||
|
|
@ -133,17 +150,17 @@ export default function GenericModal({
|
|||
}
|
||||
if (!inputVariables || inputVariables.length === 0) {
|
||||
setNoticeData({
|
||||
title: "Your template does not have any variables.",
|
||||
title: TEMP_NOTICE_ALERT,
|
||||
});
|
||||
} else {
|
||||
setSuccessData({
|
||||
title: "Prompt is ready",
|
||||
title: PROMPT_SUCCESS_ALERT,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
setIsEdit(true);
|
||||
setErrorData({
|
||||
title: "Something went wrong, please try again",
|
||||
title: BUG_ALERT,
|
||||
});
|
||||
}
|
||||
})
|
||||
|
|
@ -151,8 +168,8 @@ export default function GenericModal({
|
|||
console.log(error);
|
||||
setIsEdit(true);
|
||||
return setErrorData({
|
||||
title: "There is something wrong with this prompt, please review it",
|
||||
list: [error.toString()],
|
||||
title: PROMPT_ERROR_ALERT,
|
||||
list: [error.response.data.detail ?? ""],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -182,7 +199,7 @@ export default function GenericModal({
|
|||
{myModalTitle}
|
||||
</span>
|
||||
<IconComponent
|
||||
name="FileText"
|
||||
name={myModalTitle === "Edit Prompt" ? "TerminalSquare" : "FileText"}
|
||||
className="h-6 w-6 pl-1 text-primary "
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
|
@ -210,7 +227,7 @@ export default function GenericModal({
|
|||
setInputValue(event.target.value);
|
||||
checkVariables(event.target.value);
|
||||
}}
|
||||
placeholder="Type message here."
|
||||
placeholder={EDIT_TEXT_PLACEHOLDER}
|
||||
onKeyDown={(e) => {
|
||||
handleKeyDown(e, inputValue, "");
|
||||
}}
|
||||
|
|
@ -232,7 +249,7 @@ export default function GenericModal({
|
|||
onChange={(event) => {
|
||||
setInputValue(event.target.value);
|
||||
}}
|
||||
placeholder="Type message here."
|
||||
placeholder={EDIT_TEXT_PLACEHOLDER}
|
||||
onKeyDown={(e) => {
|
||||
handleKeyDown(e, value, "");
|
||||
}}
|
||||
|
|
@ -255,7 +272,7 @@ export default function GenericModal({
|
|||
>
|
||||
<div className="flex flex-wrap items-center">
|
||||
<IconComponent
|
||||
name="Variable"
|
||||
name="Braces"
|
||||
className=" -ml-px mr-1 flex h-4 w-4 text-primary"
|
||||
/>
|
||||
<span className="text-md font-semibold text-primary">
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ export default function ShareModal({
|
|||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||
const [internalOpen, internalSetOpen] = useState(children ? false : true);
|
||||
const [openConfirmationModal, setOpenConfirmationModal] = useState(false);
|
||||
const nameComponent = is_component ? "component" : "flow";
|
||||
const nameComponent = is_component ? "component" : "workflow";
|
||||
|
||||
const [tags, setTags] = useState<{ id: string; name: string }[]>([]);
|
||||
const [loadingTags, setLoadingTags] = useState<boolean>(false);
|
||||
|
|
@ -179,7 +179,7 @@ export default function ShareModal({
|
|||
</span>
|
||||
<br></br>
|
||||
<span className=" text-xs text-destructive ">
|
||||
Warning: This action cannot be undone.
|
||||
Note: This action is irreversible.
|
||||
</span>
|
||||
</ConfirmationModal.Content>
|
||||
</ConfirmationModal>
|
||||
|
|
@ -204,7 +204,9 @@ export default function ShareModal({
|
|||
{children ? children : <></>}
|
||||
</BaseModal.Trigger>
|
||||
<BaseModal.Header
|
||||
description={`Share your ${nameComponent} to the Langflow Store.`}
|
||||
description={`Publish ${
|
||||
is_component ? "your component" : "workflow"
|
||||
} to the Langflow Store.`}
|
||||
>
|
||||
<span className="pr-2">Share</span>
|
||||
<IconComponent
|
||||
|
|
@ -235,12 +237,12 @@ export default function ShareModal({
|
|||
}}
|
||||
/>
|
||||
<label htmlFor="public" className="export-modal-save-api text-sm ">
|
||||
Make {nameComponent} public
|
||||
Set {nameComponent} status to public
|
||||
</label>
|
||||
</div>
|
||||
<span className=" text-xs text-destructive ">
|
||||
<b>Warning:</b> API keys in designated fields are removed when
|
||||
sharing.
|
||||
<b>Attention:</b> API keys in specified fields are automatically
|
||||
removed upon sharing.
|
||||
</span>
|
||||
</BaseModal.Content>
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { useContext, useState } from "react";
|
|||
import { useNavigate } from "react-router-dom";
|
||||
import { Button } from "../../../components/ui/button";
|
||||
import { Input } from "../../../components/ui/input";
|
||||
import { SIGNIN_ERROR_ALERT } from "../../../constants/alerts_constants";
|
||||
import { CONTROL_LOGIN_STATE } from "../../../constants/constants";
|
||||
import { AuthContext } from "../../../contexts/authContext";
|
||||
import { onLogin } from "../../../controllers/API";
|
||||
|
|
@ -39,7 +40,7 @@ export default function LoginAdminPage() {
|
|||
})
|
||||
.catch((error) => {
|
||||
setErrorData({
|
||||
title: "Error signing in",
|
||||
title: SIGNIN_ERROR_ALERT,
|
||||
list: [error["response"]["data"]["detail"]],
|
||||
});
|
||||
});
|
||||
|
|
|
|||