Merge branch 'zustand/io/migration' into globalVariables

This commit is contained in:
Lucas Oliveira 2024-03-18 23:40:15 +01:00
commit 6c9a87bed7
529 changed files with 26323 additions and 10993 deletions

View file

@ -24,3 +24,7 @@ yarn-error.log*
/test-results/
/playwright-report/*/
/playwright/.cache/
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

@ -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. */
// {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.8 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.9 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 39 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 53 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

View file

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

View file

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

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

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

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

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

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

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

@ -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>
) : (
<></>

View file

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

View file

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

View file

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

View file

@ -45,6 +45,7 @@ export default function IntComponent({
onChange={(event) => {
onChange(event.target.value);
}}
data-testid={id}
/>
</div>
);

View file

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

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

View file

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

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

View file

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

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

View file

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

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

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

View file

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

View file

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

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

View file

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

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

View file

@ -28,6 +28,9 @@ export default function RenameLabel(props) {
}
}
resizeInput();
return () => {
if(isRename) document.removeEventListener("keydown", () => {});
};
}, [isRename]);
const inputRef = useRef<HTMLInputElement | null>(null);

View file

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

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

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

View 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

View file

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

View file

@ -6,3 +6,10 @@ export enum TypeModal {
TEXT = 1,
PROMPT = 2,
}
export enum BuildStatus {
BUILDING = "BUILDING",
TO_BUILD = "TO_BUILD",
BUILT = "BUILT",
INACTIVE = "INACTIVE",
}

View file

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

View file

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

View file

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

File diff suppressed because one or more lines are too long

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

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

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 122 KiB

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 273 KiB

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

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

View 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

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

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

View 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

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 304 KiB

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 156 KiB

After

Width:  |  Height:  |  Size: 406 KiB

Before After
Before After

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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, "&lt;")
.replace(/>/g, "&gt;")
.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">

View file

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

View file

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

Some files were not shown because too many files have changed in this diff Show more