Merge branch 'zustand/io/migration' into cz/fixTestsIo

This commit is contained in:
cristhianzl 2024-02-26 18:59:38 -03:00
commit b6f27173e0
142 changed files with 2701 additions and 1587 deletions

View file

@ -44,6 +44,7 @@
"clsx": "^1.2.1",
"dompurify": "^3.0.5",
"esbuild": "^0.17.19",
"framer-motion": "^11.0.6",
"lodash": "^4.17.21",
"lucide-react": "^0.331.0",
"moment": "^2.29.4",
@ -5918,6 +5919,44 @@
"url": "https://github.com/sponsors/rawify"
}
},
"node_modules/framer-motion": {
"version": "11.0.6",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.0.6.tgz",
"integrity": "sha512-BpO3mWF8UwxzO3Ca5AmSkrg14QYTeJa9vKgoLOoBdBdTPj0e81i1dMwnX6EQJXRieUx20uiDBXq8bA6y7N6b8Q==",
"dependencies": {
"tslib": "^2.4.0"
},
"optionalDependencies": {
"@emotion/is-prop-valid": "^0.8.2"
},
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"peerDependenciesMeta": {
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/framer-motion/node_modules/@emotion/is-prop-valid": {
"version": "0.8.8",
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz",
"integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==",
"optional": true,
"dependencies": {
"@emotion/memoize": "0.7.4"
}
},
"node_modules/framer-motion/node_modules/@emotion/memoize": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz",
"integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==",
"optional": true
},
"node_modules/fs-extra": {
"version": "11.2.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz",

View file

@ -39,6 +39,7 @@
"clsx": "^1.2.1",
"dompurify": "^3.0.5",
"esbuild": "^0.17.19",
"framer-motion": "^11.0.6",
"lodash": "^4.17.21",
"lucide-react": "^0.331.0",
"moment": "^2.29.4",

View file

@ -19,6 +19,7 @@ import Router from "./routes";
import useAlertStore from "./stores/alertStore";
import { useDarkStore } from "./stores/darkStore";
import useFlowsManagerStore from "./stores/flowsManagerStore";
import { useStoreStore } from "./stores/storeStore";
import { useTypesStore } from "./stores/typesStore";
export default function App() {
@ -28,7 +29,6 @@ export default function App() {
const tempNotificationList = useAlertStore(
(state) => state.tempNotificationList
);
const loading = useAlertStore((state) => state.loading);
const [fetchError, setFetchError] = useState(false);
const isLoading = useFlowsManagerStore((state) => state.isLoading);
@ -38,9 +38,11 @@ export default function App() {
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 checkHasStore = useStoreStore((state) => state.checkHasStore);
useEffect(() => {
refreshStars();
@ -52,6 +54,8 @@ export default function App() {
getTypes().then(() => {
refreshFlows();
});
checkHasStore();
fetchApiData();
}
}, [isAuthenticated]);

View file

@ -324,7 +324,9 @@ export default function ParameterComponent({
) : (
title
)}
<span className="text-status-red">{required ? " *" : ""}</span>
<span className={(info === "" ? "" : "ml-1 ") + " text-status-red"}>
{required ? " *" : ""}
</span>
<div className="">
{info !== "" && (
<ShadTooltip content={infoHtml.current}>

View file

@ -1,15 +1,17 @@
import { useCallback, useEffect, useState } from "react";
import { NodeToolbar } 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 Xmark from "../../components/ui/xmark";
import { priorityFields } from "../../constants/constants";
import { BuildStatus } from "../../constants/enums";
import NodeToolbarComponent from "../../pages/FlowPage/components/nodeToolbarComponent";
import { useDarkStore } from "../../stores/darkStore";
import useFlowStore from "../../stores/flowStore";
import useFlowsManagerStore from "../../stores/flowsManagerStore";
import { useTypesStore } from "../../stores/typesStore";
@ -43,6 +45,7 @@ export default function GenericNode({
const [nodeDescription, setNodeDescription] = useState(
data.node?.description!
);
const buildStatus = useFlowStore((state) => state.flowBuildStatus[data.id]);
const [validationStatus, setValidationStatus] =
useState<validationStatusType | null>(null);
const [handles, setHandles] = useState<number>(0);
@ -89,14 +92,13 @@ export default function GenericNode({
// should be empty string if no duration
// else should be `Duration: ${duration}`
const getDurationString = (duration: number | null): string => {
if (duration === null) {
const getDurationString = (duration: number | undefined): string => {
if (duration === undefined) {
return "";
} else {
return `Duration: ${duration}`;
}
};
const durationString = getDurationString(validationStatus?.data.duration);
useEffect(() => {
@ -118,10 +120,9 @@ export default function GenericNode({
} else {
setValidationStatus(null);
}
}, [flowPool, data.id]);
}, [flowPool[data.id], data.id]);
const showNode = data.showNode ?? true;
const pinned = data.node?.pinned ?? false;
const nameEditable = data.node?.flow || data.type === "CustomComponent";
@ -136,7 +137,6 @@ export default function GenericNode({
const iconClassName = `generic-node-icon ${
!showNode ? "absolute inset-x-6 h-12 w-12" : ""
}`;
if (iconElement && isEmoji) {
return nodeIconFragment(iconElement);
} else {
@ -158,36 +158,45 @@ export default function GenericNode({
);
};
const getIconPlayOrPauseComponent = (name, className) => (
<IconComponent
name={name}
className={`absolute h-5 stroke-2 ${className} ml-0.5`}
/>
);
const getStatusClassName = (
validationStatus: validationStatusType | null
) => {
if (validationStatus && validationStatus.valid) {
return "green-status";
} else if (validationStatus && !validationStatus.valid) {
return "red-status";
} else if (!validationStatus) {
return "yellow-status";
} else {
return "status-build-animation";
}
};
const renderIconPlayOrPauseComponents = (
const isDark = useDarkStore((state) => state.dark);
const renderIconStatus = (
buildStatus: BuildStatus | undefined,
validationStatus: validationStatusType | null
) => {
if (buildStatus === BuildStatus.BUILDING) {
return <Loading />;
return <Loading className="text-medium-indigo" />;
} else {
const className = getStatusClassName(validationStatus);
return <>{getIconPlayOrPauseComponent("Play", className)}</>;
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"
/>
)}
</>
);
}
};
@ -195,14 +204,18 @@ export default function GenericNode({
buildStatus: BuildStatus | undefined,
validationStatus: validationStatusType | null
) => {
if (
buildStatus === BuildStatus.BUILT &&
validationStatus &&
!validationStatus.valid
) {
return "border-none ring ring-red-300";
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
? "border-none ring ring-[#751C1C]"
: "built-invalid-status";
} else if (buildStatus === BuildStatus.BUILDING) {
return "border-none ring";
return "building-status";
} else {
return "";
}
@ -214,11 +227,17 @@ export default function GenericNode({
buildStatus: BuildStatus | undefined,
validationStatus: validationStatusType | null
) => {
const specificClassFromBuildStatus = getSpecificClassFromBuildStatus(
buildStatus,
validationStatus
);
const baseBorderClass = getBaseBorderClass(selected);
const nodeSizeClass = getNodeSizeClass(showNode);
return classNames(
getBaseBorderClass(selected),
getNodeSizeClass(showNode),
baseBorderClass,
nodeSizeClass,
"generic-node-div",
getSpecificClassFromBuildStatus(buildStatus, validationStatus)
specificClassFromBuildStatus
);
};
@ -250,12 +269,11 @@ export default function GenericNode({
onCloseAdvancedModal={() => {}}
></NodeToolbarComponent>
</NodeToolbar>
<div
className={getNodeBorderClassName(
selected,
showNode,
data?.build_status,
buildStatus,
validationStatus
)}
>
@ -312,28 +330,37 @@ export default function GenericNode({
</div>
) : (
<ShadTooltip content={data.node?.display_name}>
<div
className="flex items-center gap-2"
onDoubleClick={(event) => {
if (nameEditable) {
setInputName(true);
}
takeSnapshot();
event.stopPropagation();
event.preventDefault();
}}
>
<div
data-testid={"title-" + data.node?.display_name}
className="generic-node-tooltip-div text-primary"
>
{data.node?.display_name}
</div>
<div className="group flex items-center gap-2.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 text-primary"
>
{data.node?.display_name}
</div>
</ShadTooltip>
{nameEditable && (
<IconComponent
name="Pencil"
className="h-4 w-4 text-ring"
/>
<div
onClick={(event) => {
setInputName(true);
takeSnapshot();
event.stopPropagation();
event.preventDefault();
}}
>
<IconComponent
name="Pencil"
className="hidden h-4 w-4 animate-pulse text-status-blue group-hover:block"
/>
</div>
)}
</div>
</ShadTooltip>
@ -431,80 +458,40 @@ export default function GenericNode({
</div>
{showNode && (
<Button
variant="outline"
className="h-9 px-1.5"
variant="secondary"
className={"group h-9 px-1.5"}
onClick={() => {
setNode(data.id, (old) => ({
...old,
data: {
...old.data,
node: {
...old.data.node,
pinned: old.data?.node?.pinned ? false : true,
},
},
}));
}}
>
<Tooltip
title={<span>{pinned ? "Pin Output" : "Unpin Output"}</span>}
>
<div className="generic-node-status-position flex items-center">
<IconComponent
name={"Pin"}
className={cn(
"h-5 fill-transparent stroke-chat-trigger stroke-2 transition-all",
pinned ? "animate-wiggle fill-chat-trigger" : ""
)}
/>
</div>
</Tooltip>
</Button>
)}
{showNode && (
<Button
variant="outline"
className={"h-9 px-1.5"}
onClick={() => {
if (data?.build_status === BuildStatus.BUILDING || isBuilding)
if (buildStatus === BuildStatus.BUILDING || isBuilding)
return;
setValidationStatus(null);
buildFlow(data.id);
}}
>
<div>
<Tooltip
title={
data?.build_status === BuildStatus.BUILDING ? (
<ShadTooltip
content={
buildStatus === BuildStatus.BUILDING ? (
<span>Building...</span>
) : !validationStatus ? (
<span className="flex">
Build{" "}
<IconComponent
name="Play"
className=" h-5 stroke-build-trigger stroke-2"
/>{" "}
flow to validate status.
</span>
<span className="flex">Build to validate status.</span>
) : (
<div className="max-h-96 overflow-auto">
{typeof validationStatus.params === "string"
? `${durationString}\n${validationStatus.params}`
? (`${durationString}\n${validationStatus.params}`
.split("\n")
.map((line, index) => (
<div key={index}>{line}</div>
))
)))
: durationString}
</div>
)
}
side="bottom"
>
<div className="generic-node-status-position flex items-center justify-center">
{renderIconPlayOrPauseComponents(
data?.build_status,
validationStatus
)}
{renderIconStatus(buildStatus, validationStatus)}
</div>
</Tooltip>
</ShadTooltip>
</div>
</Button>
)}

View file

@ -0,0 +1,49 @@
// 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 node 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!"

View file

@ -49,7 +49,7 @@ export default function AccordionComponent({
>
{trigger}
</AccordionTrigger>
<AccordionContent className="AccordionContent">
<AccordionContent className="AccordionContent flex flex-col">
{children}
</AccordionContent>
</AccordionItem>

View file

@ -12,12 +12,12 @@ export default function IOInputField({
const setNode = useFlowStore((state) => state.setNode);
const node = nodes.find((node) => node.id === inputId);
function handleInputType() {
if (!node) return "no node found";
if (!node) return <>"No node found!"</>;
switch (inputType) {
case "TextInput":
return (
<Textarea
className="h-full w-full custom-scroll"
className="w-full"
placeholder={"Enter text..."}
value={node.data.node!.template["value"].value}
onChange={(e) => {
@ -47,7 +47,7 @@ export default function IOInputField({
default:
return (
<Textarea
className="h-full w-full custom-scroll"
className="w-full custom-scroll"
placeholder={"Enter text..."}
value={node.data.node!.template["value"]}
onChange={(e) => {
@ -62,10 +62,5 @@ export default function IOInputField({
);
}
}
return (
<div className="font-xl flex h-full w-full flex-col gap-4 p-4 font-semibold">
{inputType}
{handleInputType()}
</div>
);
return handleInputType();
}

View file

@ -12,15 +12,15 @@ export default function IOOutputView({
const flowPool = useFlowStore((state) => state.flowPool);
const node = nodes.find((node) => node.id === outputId);
function handleOutputType() {
if (!node) return "no node found";
if (!node) return <>"No node found!"</>;
switch (outputType) {
case "TextOutput":
return (
<Textarea
className="h-full w-full custom-scroll"
placeholder={"Enter text..."}
className="w-full custom-scroll"
placeholder={"Empty"}
// update to real value on flowPool
value={flowPool[node.id][flowPool[node.id].length - 1].data.results}
value={((flowPool[node.id] ?? [])[(flowPool[node.id]?.length ?? 1) - 1])?.params ?? ""}
readOnly
/>
);
@ -28,7 +28,7 @@ export default function IOOutputView({
default:
return (
<Textarea
className="h-full w-full custom-scroll"
className="w-full custom-scroll"
placeholder={"Enter text..."}
value={node.data.node!.template["value"]}
onChange={(e) => {
@ -43,10 +43,5 @@ export default function IOOutputView({
);
}
}
return (
<div className="font-xl flex h-full w-full flex-col gap-4 p-4 font-semibold">
{outputType}
{handleOutputType()}
</div>
);
return handleOutputType();
}

View file

@ -1,9 +1,9 @@
import { ReactNode, useState } from "react";
import { cloneDeep } from "lodash";
import { useEffect, useState } from "react";
import { CHAT_FORM_DIALOG_SUBTITLE } from "../../constants/constants";
import BaseModal from "../../modals/baseModal";
import useAlertStore from "../../stores/alertStore";
import useFlowStore from "../../stores/flowStore";
import { NodeType } from "../../types/flow";
import { isInputType, isOutputType } from "../../utils/reactflowUtils";
import { cn } from "../../utils/utils";
import AccordionComponent from "../AccordionComponent";
import IOInputField from "../IOInputField";
@ -12,62 +12,70 @@ 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 }): JSX.Element {
const inputs = useFlowStore((state) => state.inputs);
const outputs = useFlowStore((state) => state.outputs);
const inputIds = inputs.map((obj) => obj.id);
const outputIds = outputs.map((obj) => obj.id);
const nodes = useFlowStore((state) => state.nodes);
const setNode = useFlowStore((state) => state.setNode);
const categories = getCategories();
const [selectedCategory, setSelectedCategory] = useState<number>(0);
const [showChat, setShowChat] = useState<boolean>(false);
const [selectedView, setSelectedView] = useState<{
type: string;
id?: string;
}>(handleInitialView());
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);
type CategoriesType = { name: string; icon: string };
const { getNode, setNode, buildFlow, getFlow } = useFlowStore();
const { setErrorData } = useAlertStore();
const setIsBuilding = useFlowStore((state) => state.setIsBuilding);
const [lockChat, setLockChat] = useState(false);
const [chatValue, setChatValue] = useState("");
const isBuilding = useFlowStore((state) => state.isBuilding);
function handleInitialView() {
if (
outputs.map((output) => output.type).includes("ChatOutput") ||
inputs.map((input) => input.type).includes("ChatInput")
) {
return { type: "ChatOutput" };
async function sendMessage(count = 1): Promise<void> {
if (isBuilding) return;
const { nodes, edges } = getFlow();
setIsBuilding(true);
setLockChat(true);
setChatValue("");
const chatInputNode = nodes.find((node) => node.id === chatInput?.id);
if (chatInputNode) {
let newNode = cloneDeep(chatInputNode);
newNode.data.node!.template["message"].value = chatValue;
setNode(chatInput!.id, newNode);
}
return { type: "" };
for (let i = 0; i < count; i++) {
await buildFlow().catch((err) => {
console.error(err);
setLockChat(false);
});
}
setLockChat(false);
}
function getCategories() {
const categories: CategoriesType[] = [];
if (inputs.filter((input) => input.type !== "ChatInput").length > 0)
categories.push({ name: "Inputs", icon: "TextCursorInput" });
if (outputs.filter((output) => output.type !== "ChatOutput").length > 0)
categories.push({ name: "Outputs", icon: "TerminalSquare" });
return categories;
}
function handleSelectChange(): ReactNode {
const { type, id } = selectedView;
if (type === "ChatOutput") return <NewChatView />;
if (isInputType(type))
return <IOInputField inputId={id!} inputType={type} />;
if (isOutputType(type))
return <IOOutputView outputId={id!} outputType={type} />;
else return undefined;
}
function UpdateAccordion() {
return (categories[selectedCategory]?.name ?? "Inputs") === "Inputs"
? inputs
: outputs;
}
useEffect(() => {
setSelectedViewField(undefined);
setSelectedTab(inputs.length > 0 ? 1 : outputs.length > 0 ? 2 : 0);
}, [inputs.length, outputs.length]);
return (
<BaseModal
size={handleSelectChange() ? "large" : "small"}
size={haveChat ? (selectedTab === 0 ? "large-thin" : "large") : "small"}
open={open}
setOpen={setOpen}
>
@ -84,130 +92,229 @@ export default function IOView({ children, open, setOpen }): JSX.Element {
</div>
</BaseModal.Header>
<BaseModal.Content>
<div className="flex-max-width mt-2 h-[80vh]">
<div
className={cn(
"mr-6 flex h-full w-2/6 flex-col justify-start overflow-auto scrollbar-hide",
handleSelectChange() ? "w-2/6" : "w-full"
)}
>
<div className="flex w-full items-center justify-between py-2">
<div className="flex items-start gap-4">
{categories.map((category, index) => {
return (
//hide chat button if chat is alredy on the view
<Button
onClick={() => setSelectedCategory(index)}
variant={
index === selectedCategory ? "primary" : "secondary"
}
key={index}
>
<IconComponent
name={category.icon}
className=" file-component-variable"
/>
<span className="file-component-variables-span text-md">
{category.name}
</span>
</Button>
);
})}
</div>
{(outputs.map((output) => output.type).includes("ChatOutput") ||
inputs.map((output) => output.type).includes("chatInput")) &&
selectedView.type !== "ChatOutput" && (
<Button
onClick={() => setSelectedView({ type: "ChatOutput" })}
variant="outline"
key={"chat"}
className="self-end px-2.5"
>
<IconComponent
name="MessageSquareMore"
className="h-5 w-5"
/>
</Button>
<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 overflow-auto scrollbar-hide",
haveChat ? "w-2/6" : "w-full"
)}
</div>
<div className="mx-2 mb-2 mt-4 flex items-center gap-2 font-semibold">
{categories[selectedCategory]?.name === "Inputs" && (
<>
<IconComponent name={"FormInput"} />
Text Inputs
</>
)}
{categories[selectedCategory]?.name === "Outputs" && (
<>
<IconComponent name={"ChevronRightSquare"} />
Prompt Outputs
</>
)}
</div>
{UpdateAccordion()
.filter(
(input) =>
input.type !== "ChatInput" && input.type !== "ChatOutput"
)
.map((input, index) => {
const node: NodeType = nodes.find(
(node) => node.id === input.id
)!;
return (
<div className="file-component-accordion-div" key={index}>
<AccordionComponent
trigger={
<div className="file-component-badge-div">
<Badge variant="gray" size="md">
{input.id}
</Badge>
<div
className="-mb-1 pr-4"
onClick={(event) => {
event.stopPropagation();
setSelectedView({
type: input.type,
id: input.id,
});
}}
>
<IconComponent
className="h-4 w-4"
name="ExternalLink"
></IconComponent>
</div>
</div>
}
key={index}
keyValue={input.id}
>
<div className="file-component-tab-column">
<div className="">
{node &&
(categories[selectedCategory]?.name === "Inputs" ? (
<IOInputField
inputType={input.type}
inputId={input.id}
/>
) : (
<IOOutputView
outputType={input.type}
outputId={input.id}
/>
))}
</div>
</div>
</AccordionComponent>
>
<Tabs
value={selectedTab.toString()}
className={
"flex h-full flex-col overflow-hidden rounded-md border bg-muted text-center"
}
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 Inputs
</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">
{input.id}
</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
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={"Braces"} />
Prompt Outputs
</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">
<Badge variant="gray" size="md">
{output.id}
</Badge>
{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
outputType={output.type}
outputId={output.id}
/>
)}
</div>
</div>
</AccordionComponent>
</div>
);
})}
</TabsContent>
</Tabs>
</div>
)}
{haveChat ? (
<div className="flex h-full w-full">
{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">
{inputs.some(
(input) => input.id === selectedViewField.id
) ? (
<IOInputField
inputType={selectedViewField.type!}
inputId={selectedViewField.id!}
/>
) : (
<IOOutputView
outputType={selectedViewField.type!}
outputId={selectedViewField.id!}
/>
)}
</div>
</div>
)}
<div
className={cn("flex w-full h-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>
{handleSelectChange() ? (
handleSelectChange()
) : (
<div className="absolute bottom-8 right-8">
<Button className="px-3">
<IconComponent name="Play" className="h-6 w-6" />
{!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>
)}

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

@ -21,7 +21,7 @@ export default function ChatTrigger({}): JSX.Element {
>
<div className="flex gap-3">
<IconComponent
name="Sliders"
name="Zap"
className={"message-button-icon h-6 w-6 transition-all"}
/>
</div>

View file

@ -8,6 +8,7 @@ import useFlowStore from "../../../stores/flowStore";
import { validateNodes } from "../../../utils/reactflowUtils";
import RadialProgressComponent from "../../RadialProgress";
import IconComponent from "../../genericIconComponent";
import { MISSED_ERROR_ALERT } from "../../../alerts_constants";
export default function BuildTrigger({
open,
@ -36,7 +37,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;

View file

@ -14,7 +14,7 @@ import useAlertStore from "../../../../stores/alertStore";
import useFlowStore from "../../../../stores/flowStore";
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
import { cn } from "../../../../utils/utils";
import Tooltip from "../../../TooltipComponent";
import ShadTooltip from "../../../ShadTooltipComponent";
import IconComponent from "../../../genericIconComponent";
import { Button } from "../../../ui/button";
@ -125,8 +125,8 @@ export const MenuBar = ({
setOpen={setOpenSettings}
></FlowSettingsModal>
</div>
<Tooltip
title={
<ShadTooltip
content={
"Last saved at " +
new Date(currentFlow.updated_at ?? "").toLocaleString("en-US", {
hour: "numeric",
@ -134,8 +134,10 @@ export const MenuBar = ({
second: "numeric",
})
}
side="bottom"
styleClasses="cursor-default"
>
<div className="flex items-center gap-1.5 text-sm text-muted-foreground">
<div className="flex cursor-default items-center gap-1.5 text-sm text-muted-foreground">
<IconComponent
name={isBuilding || saveLoading ? "Loader2" : "CheckCircle2"}
className={cn(
@ -145,7 +147,7 @@ export const MenuBar = ({
/>
{printByBuildStatus()}
</div>
</Tooltip>
</ShadTooltip>
</div>
) : (
<></>

View file

@ -4,6 +4,7 @@ import useAlertStore from "../../stores/alertStore";
import useFlowsManagerStore from "../../stores/flowsManagerStore";
import { FileComponentType } from "../../types/components";
import IconComponent from "../genericIconComponent";
import { CONSOLE_ERROR_MSG, CONSOLE_SUCCESS_MSG, INVALID_FILE_ALERT } from "../../alerts_constants";
export default function InputFileComponent({
value,
@ -61,7 +62,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 +76,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

@ -17,12 +17,65 @@ export default function ChatMessage({
chat,
lockChat,
lastMessage,
updateChat,
}: chatMessagePropsType): JSX.Element {
const convert = new Convert({ newline: true });
const [hidden, setHidden] = useState(true);
const template = chat.template;
const [promptOpen, setPromptOpen] = useState(false);
const chat_message = chat.message.toString();
const [streamUrl, setStreamUrl] = useState(chat.stream_url);
const [chatMessage, setChatMessage] = useState(chat.message.toString());
const [isStreaming, setIsStreaming] = useState(false);
// 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) => {
const eventSource = new EventSource(url);
eventSource.onmessage = (event) => {
let parsedData = JSON.parse(event.data);
if (parsedData.chunk) {
setChatMessage((prev) => prev + parsedData.chunk);
}
};
eventSource.onerror = (event) => {
reject(new Error("Streaming failed"));
setIsStreaming(false);
eventSource.close();
};
eventSource.addEventListener("close", (event) => {
setStreamUrl(null); // Update state to reflect the stream is closed
resolve(true);
setIsStreaming(false);
eventSource.close();
});
});
};
useEffect(() => {
if (streamUrl && chat.message === "") {
streamChunks(streamUrl)
.then(() => {
if (updateChat) {
updateChat(chat, chatMessage, streamUrl);
}
})
.catch((error) => {
console.error(error);
});
}
}, [streamUrl]);
useEffect(() => {
// This effect is specifically for calling updateChat after streaming ends
if (!isStreaming && streamUrl) {
if (updateChat) {
updateChat(chat, chatMessage, streamUrl);
}
}
}, [isStreaming]);
useEffect(() => {
const element = document.getElementById("last-chat-message");
@ -103,7 +156,7 @@ export default function ChatMessage({
remarkPlugins={[remarkGfm, remarkMath]}
rehypePlugins={[rehypeMathjax]}
className="markdown prose min-w-full text-primary word-break-break-word
dark:prose-invert"
dark:prose-invert"
components={{
pre({ node, ...props }) {
return <>{props.children}</>;
@ -161,10 +214,10 @@ export default function ChatMessage({
},
}}
>
{chat_message}
{chatMessage}
</Markdown>
),
[chat.message, chat_message]
[chat.message, chatMessage]
)}
</div>
{chat.files && (

View file

@ -1,4 +1,4 @@
import { cloneDeep } from "lodash";
import _ from "lodash";
import { useEffect, useRef, useState } from "react";
import IconComponent from "../../components/genericIconComponent";
import { deleteFlowPool } from "../../controllers/API";
@ -11,31 +11,23 @@ import {
ChatOutputType,
FlowPoolObjectType,
} from "../../types/chat";
import { NodeType } from "../../types/flow";
import { validateNodes } from "../../utils/reactflowUtils";
import { classNames } from "../../utils/utils";
import ChatInput from "./chatInput";
import ChatMessage from "./chatMessage";
import { INFO_MISSING_ALERT, NOCHATOUTPUT_NOTICE_ALERT } from "../../alerts_constants";
export default function newChatView(): JSX.Element {
const [chatValue, setChatValue] = useState("");
const [chatHistory, setChatHistory] = useState<ChatMessageType[]>([]);
const {
flowPool,
outputs,
inputs,
getNode,
setNode,
buildFlow,
getFlow,
CleanFlowPool,
} = useFlowStore();
const { setErrorData, setNoticeData } = useAlertStore();
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 setIsBuilding = useFlowStore((state) => state.setIsBuilding);
const [lockChat, setLockChat] = useState(false);
const messagesRef = useRef<HTMLDivElement | null>(null);
const isBuilding = useFlowStore((state) => state.isBuilding);
const [chatHistory, setChatHistory] = useState<ChatMessageType[]>([]);
const inputTypes = inputs.map((obj) => obj.type);
const inputIds = inputs.map((obj) => obj.id);
@ -44,7 +36,7 @@ export default function newChatView(): JSX.Element {
useEffect(() => {
if (!outputTypes.includes("ChatOutput")) {
setNoticeData({ title: "There is no ChatOutput node in the flow." });
setNoticeData({ title: NOCHATOUTPUT_NOTICE_ALERT });
}
}, []);
@ -67,20 +59,30 @@ export default function newChatView(): JSX.Element {
});
const chatMessages: ChatMessageType[] = chatOutputResponses
.sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp))
.filter((output) => !!output.data.artifacts?.message)
.map((output) => {
//
.filter((output) => output.data.artifacts?.message !== null)
.map((output, index) => {
try {
const { sender, message, sender_name } = output.data
const { sender, message, sender_name, stream_url } = output.data
.artifacts as ChatOutputType;
const componentId = output.id + index;
const is_ai = sender === "Machine" || sender === null;
return { isSend: !is_ai, message: message, sender_name };
return {
isSend: !is_ai,
message: message,
sender_name,
id: componentId,
stream_url: stream_url,
};
} catch (e) {
console.error(e);
return {
isSend: false,
message: "Error parsing message",
sender_name: "Error",
id: output.id + index,
};
}
});
@ -105,40 +107,6 @@ export default function newChatView(): JSX.Element {
}
}, []);
async function sendMessage(count = 1): Promise<void> {
if (isBuilding) return;
const { nodes, edges } = getFlow();
let nodeValidationErrors = validateNodes(nodes, edges);
if (nodeValidationErrors.length === 0) {
setIsBuilding(true);
setLockChat(true);
setChatValue("");
const chatInputId = inputIds.find((inputId) =>
inputId.includes("ChatInput")
);
const chatInput: NodeType = getNode(chatInputId!) as NodeType;
if (chatInput) {
let newNode = cloneDeep(chatInput);
newNode.data.node!.template["message"].value = chatValue;
setNode(chatInputId!, newNode);
}
for (let i = 0; i < count; i++) {
await buildFlow().catch((err) => {
console.error(err);
setLockChat(false);
});
}
setLockChat(false);
//set chat message in the flow and run build
//@ts-ignore
} else {
setErrorData({
title: "Oops! Looks like you missed some required information:",
list: nodeValidationErrors,
});
}
}
function clearChat(): void {
setChatHistory([]);
deleteFlowPool(currentFlowId).then((_) => {
@ -148,6 +116,32 @@ export default function newChatView(): JSX.Element {
if (lockChat) setLockChat(false);
}
function updateChat(
chat: ChatMessageType,
message: string,
stream_url: string | null
) {
if (message === "") return;
console.log(`updateChat: ${message}`);
console.log("chatHistory:", chatHistory);
chat.message = message;
chat.stream_url = stream_url;
// chat is one of the chatHistory
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">
@ -172,7 +166,8 @@ export default function newChatView(): JSX.Element {
lockChat={lockChat}
chat={chat}
lastMessage={chatHistory.length - 1 === index ? true : false}
key={index}
key={`${chat.id}-${index}`}
updateChat={updateChat}
/>
))
) : (

View file

@ -20,48 +20,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"
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,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

@ -0,0 +1,46 @@
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

@ -681,4 +681,4 @@ 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"]);
export const OUTPUT_TYPES = new Set(["ChatOutput", "TextOutput"]);

View file

@ -11,4 +11,5 @@ export enum BuildStatus {
BUILDING = "BUILDING",
TO_BUILD = "TO_BUILD",
BUILT = "BUILT",
INACTIVE = "INACTIVE",
}

View file

@ -8,6 +8,7 @@ import useAlertStore from "../../stores/alertStore";
import { ApiKeyType } from "../../types/components";
import { nodeIconsLucide } from "../../utils/styleUtils";
import BaseModal from "../baseModal";
import { COPIED_NOTICE_ALERT } from "../../alerts_constants";
export default function SecretKeyModal({
title,
@ -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

@ -9,6 +9,7 @@ import useAlertStore from "../../stores/alertStore";
import { useStoreStore } from "../../stores/storeStore";
import { StoreApiKeyType } from "../../types/components";
import BaseModal from "../baseModal";
import { API_ERROR_ALERT, API_SUCCESS_ALERT } from "../../alerts_constants";
export default function StoreApiKeyModal({
children,
@ -32,7 +33,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 +43,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);

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="h-full w-full flex flex-col">{children}</div>;
};
const Trigger: React.FC<TriggerProps> = ({ children, asChild, disable }) => {
return (
@ -66,6 +66,7 @@ interface BaseModalProps {
| "small"
| "medium"
| "large"
| "large-thin"
| "large-h-full"
| "small-h-full"
| "medium-h-full"
@ -128,6 +129,10 @@ function BaseModal({
minWidth = "min-w-[80vw]";
height = "h-[80vh]";
break;
case "large-thin":
minWidth = "min-w-[65vw]";
height = "h-[80vh]";
break;
case "large-h-full":
minWidth = "min-w-[80vw]";
break;

View file

@ -16,6 +16,7 @@ import { useDarkStore } from "../../stores/darkStore";
import useFlowStore from "../../stores/flowStore";
import { codeAreaModalPropsType } from "../../types/components";
import BaseModal from "../baseModal";
import { BUG_ALERT, CODE_ERROR_ALERT, CODE_SUCCESS_ALERT, FUNC_ERROR_ALERT, IMPORT_ERROR_ALERT } from "../../alerts_constants";
export default function CodeAreaModal({
value,
@ -58,7 +59,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);
@ -66,26 +67,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,
});
});
}

View file

@ -9,6 +9,7 @@ import { useDarkStore } from "../../stores/darkStore";
import useFlowsManagerStore from "../../stores/flowsManagerStore";
import { downloadFlow, removeApiKeys } from "../../utils/reactflowUtils";
import BaseModal from "../baseModal";
import { API_WARNING_NOTICE_ALERT } from "../../alerts_constants";
const ExportModal = forwardRef(
(props: { children: ReactNode }, ref): JSX.Element => {
@ -78,7 +79,7 @@ const ExportModal = forwardRef(
);
setNoticeData({
title:
"Warning: Critical data, JSON file may include API keys.",
API_WARNING_NOTICE_ALERT,
});
} else
downloadFlow(

View file

@ -27,6 +27,7 @@ import useAlertStore from "../../stores/alertStore";
import useFlowStore from "../../stores/flowStore";
import { FlowState } from "../../types/tabs";
import { validateNodes } from "../../utils/reactflowUtils";
import { CHAT_ERROR_ALERT, INFO_MISSING_ALERT, MSG_ERROR_ALERT } from "../../alerts_constants";
export default function FormModal({
flow,
@ -169,7 +170,7 @@ export default function FormModal({
connectWS();
} else {
setErrorData({
title: "Please build the flow again before using the chat.",
title: CHAT_ERROR_ALERT,
});
}
})
@ -356,7 +357,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);
@ -401,7 +402,7 @@ export default function FormModal({
}
} else {
setErrorData({
title: "Oops! Looks like you missed some required information:",
title: INFO_MISSING_ALERT,
list: nodeValidationErrors,
});
}
@ -576,6 +577,7 @@ export default function FormModal({
chatHistory.length - 1 === index ? true : false
}
key={index}
updateChat={() => {}}
/>
))
) : (

View file

@ -19,6 +19,7 @@ import { genericModalPropsType } from "../../types/components";
import { handleKeyDown } from "../../utils/reactflowUtils";
import { classNames, varHighlightHTML } from "../../utils/utils";
import BaseModal from "../baseModal";
import { BUG_ALERT, PROMPT_ERROR_ALERT, PROMPT_SUCCESS_ALERT, TEMP_NOTICE_ALERT } from "../../alerts_constants";
export default function GenericModal({
field_name = "",
@ -133,17 +134,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,7 +152,7 @@ export default function GenericModal({
console.log(error);
setIsEdit(true);
return setErrorData({
title: "There is something wrong with this prompt, please review it",
title: PROMPT_ERROR_ALERT,
list: [error.toString()],
});
});
@ -182,7 +183,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"
/>
@ -255,7 +256,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

@ -11,6 +11,7 @@ import {
inputHandlerEventType,
loginInputStateType,
} from "../../../types/components";
import { SIGNIN_ERROR_ALERT } from "../../../alerts_constants";
export default function LoginAdminPage() {
const navigate = useNavigate();
@ -39,7 +40,7 @@ export default function LoginAdminPage() {
})
.catch((error) => {
setErrorData({
title: "Error signing in",
title: SIGNIN_ERROR_ALERT,
list: [error["response"]["data"]["detail"]],
});
});

View file

@ -33,6 +33,7 @@ import useAlertStore from "../../stores/alertStore";
import useFlowsManagerStore from "../../stores/flowsManagerStore";
import { Users } from "../../types/api";
import { UserInputType } from "../../types/components";
import { USER_ADD_ERROR_ALERT, USER_ADD_SUCCESS_ALERT, USER_DEL_ERROR_ALERT, USER_DEL_SUCCESS_ALERT, USER_EDIT_ERROR_ALERT, USER_EDIT_SUCCESS_ALERT } from "../../alerts_constants";
export default function AdminPage() {
const [inputValue, setInputValue] = useState("");
@ -117,12 +118,12 @@ export default function AdminPage() {
.then((res) => {
resetFilter();
setSuccessData({
title: "Success! User deleted!",
title: USER_DEL_SUCCESS_ALERT,
});
})
.catch((error) => {
setErrorData({
title: "Error on delete user",
title: USER_DEL_ERROR_ALERT,
list: [error["response"]["data"]["detail"]],
});
});
@ -133,12 +134,12 @@ export default function AdminPage() {
.then((res) => {
resetFilter();
setSuccessData({
title: "Success! User edited!",
title: USER_EDIT_SUCCESS_ALERT,
});
})
.catch((error) => {
setErrorData({
title: "Error on edit user",
title: USER_EDIT_ERROR_ALERT,
list: [error["response"]["data"]["detail"]],
});
});
@ -152,12 +153,12 @@ export default function AdminPage() {
.then((res) => {
resetFilter();
setSuccessData({
title: "Success! User edited!",
title: USER_EDIT_SUCCESS_ALERT,
});
})
.catch((error) => {
setErrorData({
title: "Error on edit user",
title: USER_EDIT_ERROR_ALERT,
list: [error["response"]["data"]["detail"]],
});
});
@ -170,12 +171,12 @@ export default function AdminPage() {
.then((res) => {
resetFilter();
setSuccessData({
title: "Success! User edited!",
title: USER_EDIT_SUCCESS_ALERT,
});
})
.catch((error) => {
setErrorData({
title: "Error on edit user",
title: USER_EDIT_ERROR_ALERT,
list: [error["response"]["data"]["detail"]],
});
});
@ -190,13 +191,13 @@ export default function AdminPage() {
}).then((res) => {
resetFilter();
setSuccessData({
title: "Success! New user added!",
title: USER_ADD_SUCCESS_ALERT,
});
});
})
.catch((error) => {
setErrorData({
title: "Error when adding new user",
title: USER_ADD_ERROR_ALERT,
list: [error.response.data.detail],
});
});

View file

@ -26,6 +26,7 @@ import {
} from "../../constants/constants";
import useAlertStore from "../../stores/alertStore";
import { ApiKey } from "../../types/components";
import { DEL_KEY_ERROR_ALERT, DEL_KEY_SUCCESS_ALERT } from "../../alerts_constants";
export default function ApiKeysPage() {
const [loadingKeys, setLoadingKeys] = useState(true);
@ -63,12 +64,12 @@ export default function ApiKeysPage() {
.then((res) => {
resetFilter();
setSuccessData({
title: "Success! Key deleted!",
title: DEL_KEY_SUCCESS_ALERT,
});
})
.catch((error) => {
setErrorData({
title: "Error on delete key",
title: DEL_KEY_ERROR_ALERT,
list: [error["response"]["data"]["detail"]],
});
});

View file

@ -24,12 +24,14 @@ import {
generateNodeFromFlow,
getNodeId,
isValidConnection,
reconnectEdges,
validateSelection,
} from "../../../../utils/reactflowUtils";
import { getRandomName, isWrappedWithClass } from "../../../../utils/utils";
import ConnectionLineComponent from "../ConnectionLineComponent";
import SelectionMenu from "../SelectionMenuComponent";
import ExtraSidebar from "../extraSidebarComponent";
import { INVALID_SELECTION_ERROR_ALERT, UPLOAD_ALERT_LIST, UPLOAD_ERROR_ALERT, WRONG_FILE_ERROR_ALERT } from "../../../../alerts_constants";
const nodeTypes = {
genericNode: GenericNode,
@ -256,14 +258,14 @@ export default function Page({
position: position,
}).catch((error) => {
setErrorData({
title: "Error uploading file",
title: UPLOAD_ERROR_ALERT,
list: [error],
});
});
} else {
setErrorData({
title: "Invalid file type",
list: ["Please upload a JSON file"],
title: WRONG_FILE_ERROR_ALERT,
list: [UPLOAD_ALERT_LIST],
});
}
}
@ -394,7 +396,7 @@ export default function Page({
if (
validateSelection(lastSelection!, edges).length === 0
) {
const { newFlow } = generateFlow(
const { newFlow, removedEdges } = generateFlow(
lastSelection!,
nodes,
edges,
@ -404,6 +406,10 @@ export default function Page({
newFlow,
getNodeId
);
const newEdges = reconnectEdges(
newGroupNode,
removedEdges
);
setNodes((oldNodes) => [
...oldNodes.filter(
(oldNodes) =>
@ -414,19 +420,20 @@ export default function Page({
),
newGroupNode,
]);
setEdges((oldEdges) =>
oldEdges.filter(
setEdges((oldEdges) => [
...oldEdges.filter(
(oldEdge) =>
!lastSelection!.nodes.some(
(selectionNode) =>
selectionNode.id === oldEdge.target ||
selectionNode.id === oldEdge.source
)
)
);
),
...newEdges,
]);
} else {
setErrorData({
title: "Invalid selection",
title: INVALID_SELECTION_ERROR_ALERT,
list: validateSelection(lastSelection!, edges),
});
}

View file

@ -25,6 +25,7 @@ import {
} from "../../../../utils/utils";
import DisclosureComponent from "../DisclosureComponent";
import SidebarDraggableComponent from "./sideBarDraggableComponent";
import { UPLOAD_ERROR_ALERT } from "../../../../alerts_constants";
export default function ExtraSidebar(): JSX.Element {
const data = useTypesStore((state) => state.data);
@ -255,7 +256,7 @@ export default function ExtraSidebar(): JSX.Element {
uploadFlow({ newProject: false, isComponent: false }).catch(
(error) => {
setErrorData({
title: "Error uploading file",
title: UPLOAD_ERROR_ALERT,
list: [error],
});
}

View file

@ -25,7 +25,8 @@ import {
expandGroupNode,
updateFlowPosition,
} from "../../../../utils/reactflowUtils";
import { classNames } from "../../../../utils/utils";
import { classNames, cn } from "../../../../utils/utils";
import { useUpdateNodeInternals } from "reactflow";
export default function NodeToolbarComponent({
data,
@ -60,6 +61,7 @@ export default function NodeToolbarComponent({
const isMinimal = numberOfHandles <= 1;
const isGroup = data.node?.flow ? true : false;
const pinned = data.node?.pinned ?? false;
const paste = useFlowStore((state) => state.paste);
const nodes = useFlowStore((state) => state.nodes);
const edges = useFlowStore((state) => state.edges);
@ -85,6 +87,8 @@ export default function NodeToolbarComponent({
onCloseAdvancedModal!(false);
}
}, [showModalAdvanced]);
const updateNodeInternals = useUpdateNodeInternals();
useEffect(() => {
setFlowComponent(createFlowComponent(cloneDeep(data), version));
@ -107,6 +111,9 @@ export default function NodeToolbarComponent({
takeSnapshot();
setShowNode(data.showNode ?? true ? false : true);
break;
case "Share":
if (hasApiKey || hasStore) setShowconfirmShare(true);
break;
case "Download":
downloadNode(flowComponent!);
break;
@ -187,6 +194,7 @@ export default function NodeToolbarComponent({
return newNode;
});
updateNodeInternals(data.id);
};
const [openModal, setOpenModal] = useState(false);
@ -222,7 +230,7 @@ export default function NodeToolbarComponent({
id={"code-input-node-toolbar-" + name}
/>
</div>
<IconComponent name="Code2" className="h-4 w-4" />
<IconComponent name="Code" className="h-4 w-4" />
</button>
</ShadTooltip>
) : (
@ -269,22 +277,35 @@ export default function NodeToolbarComponent({
<IconComponent name="Copy" className="h-4 w-4" />
</button>
</ShadTooltip>
{hasStore && (
<ShadTooltip content="Share" side="top">
<button
className={classNames(
"relative -ml-px inline-flex items-center bg-background px-2 py-2 text-foreground shadow-md ring-1 ring-inset ring-ring transition-all duration-500 ease-in-out hover:bg-muted focus:z-10",
!hasApiKey || !validApiKey ? " text-muted-foreground" : ""
<ShadTooltip content="Pin" side="top">
<button
className={classNames(
"relative -ml-px inline-flex items-center bg-background px-2 py-2 text-foreground shadow-md ring-1 ring-inset ring-ring transition-all duration-500 ease-in-out hover:bg-muted focus:z-10"
)}
onClick={(event) => {
event.preventDefault();
setNode(data.id, (old) => ({
...old,
data: {
...old.data,
node: {
...old.data.node,
pinned: old.data?.node?.pinned ? false : true,
},
},
}));
}}
>
<IconComponent
name="Pin"
className={cn(
"h-4 w-4 transition-all",
pinned ? "animate-wiggle fill-current" : ""
)}
onClick={(event) => {
event.preventDefault();
if (hasApiKey || hasStore) setShowconfirmShare(true);
}}
>
<IconComponent name="Share3" className="-m-1 h-6 w-6" />
</button>
</ShadTooltip>
)}
/>
</button>
</ShadTooltip>
<Select onValueChange={handleSelectChange} value="">
<ShadTooltip content="More" side="top">
@ -340,6 +361,20 @@ export default function NodeToolbarComponent({
</SelectItem>
)
)}
{hasStore && (
<SelectItem
value={"Share"}
disabled={!hasApiKey || !validApiKey}
>
<div className="flex" data-testid="save-button-modal">
<IconComponent
name="Share3"
className="relative top-0.5 -m-1 mr-1 h-6 w-6"
/>{" "}
Share{" "}
</div>{" "}
</SelectItem>
)}
{!hasStore && (
<SelectItem value={"Download"}>
<div className="flex">
@ -360,9 +395,7 @@ export default function NodeToolbarComponent({
name="FileText"
className="relative top-0.5 mr-2 h-4 w-4"
/>{" "}
{data.node?.documentation === ""
? "Coming Soon"
: "Documentation"}
Docs
</div>{" "}
</SelectItem>
{isMinimal && (
@ -388,13 +421,19 @@ export default function NodeToolbarComponent({
</SelectItem>
)}
<SelectItem value={"delete"}>
<div className="font-red flex text-red-500 hover:text-red-500">
<SelectItem value={"delete"} className="focus:bg-red-400/[.20]">
<div className="font-red flex text-status-red">
<IconComponent
name="Trash2"
className="relative top-0.5 mr-2 h-4 w-4 "
/>{" "}
Delete{" "}
<span className="">Delete</span>{" "}
<span>
<IconComponent
name="Delete"
className="absolute right-2 top-2 h-4 w-4 text-red-300"
></IconComponent>
</span>
</div>
</SelectItem>
</SelectContent>

View file

@ -9,6 +9,7 @@ import { Button } from "../../../../components/ui/button";
import useAlertStore from "../../../../stores/alertStore";
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
import { FlowType } from "../../../../types/flow";
import { CONSOLE_ERROR_MSG, UPLOAD_ALERT_LIST, WRONG_FILE_ERROR_ALERT } from "../../../../alerts_constants";
export default function ComponentsComponent({
is_component = true,
@ -76,14 +77,14 @@ export default function ComponentsComponent({
})
.catch((error) => {
setErrorData({
title: "Error uploading file",
title: CONSOLE_ERROR_MSG,
list: [error],
});
});
} else {
setErrorData({
title: "Invalid file type",
list: ["Please upload a JSON file"],
title: WRONG_FILE_ERROR_ALERT,
list: [UPLOAD_ALERT_LIST],
});
}
}

View file

@ -10,6 +10,7 @@ import { USER_PROJECTS_HEADER } from "../../constants/constants";
import useAlertStore from "../../stores/alertStore";
import useFlowsManagerStore from "../../stores/flowsManagerStore";
import { downloadFlows } from "../../utils/reactflowUtils";
import { CONSOLE_ERROR_MSG } from "../../alerts_constants";
export default function HomePage(): JSX.Element {
const addFlow = useFlowsManagerStore((state) => state.addFlow);
const uploadFlow = useFlowsManagerStore((state) => state.uploadFlow);
@ -40,7 +41,7 @@ export default function HomePage(): JSX.Element {
})
.catch((error) => {
setErrorData({
title: "Error uploading file",
title: CONSOLE_ERROR_MSG,
list: [error],
});
});

View file

@ -16,6 +16,7 @@ import {
patchUserInputStateType,
} from "../../types/components";
import { gradients } from "../../utils/styleUtils";
import { EDIT_PASSWORD_ALERT_LIST, EDIT_PASSWORD_ERROR_ALERT, SAVE_ERROR_ALERT, SAVE_SUCCESS_ALERT } from "../../alerts_constants";
export default function ProfileSettingsPage(): JSX.Element {
const setCurrentFlowId = useFlowsManagerStore(
(state) => state.setCurrentFlowId
@ -37,8 +38,8 @@ export default function ProfileSettingsPage(): JSX.Element {
async function handlePatchUser() {
if (password !== cnfPassword) {
setErrorData({
title: "Error changing password",
list: ["Passwords do not match"],
title: EDIT_PASSWORD_ERROR_ALERT,
list: [EDIT_PASSWORD_ALERT_LIST],
});
return;
}
@ -54,10 +55,10 @@ export default function ProfileSettingsPage(): JSX.Element {
}
handleInput({ target: { name: "password", value: "" } });
handleInput({ target: { name: "cnfPassword", value: "" } });
setSuccessData({ title: "Changes saved successfully!" });
setSuccessData({ title: SAVE_SUCCESS_ALERT });
} catch (error) {
setErrorData({
title: "Error saving changes",
title: SAVE_ERROR_ALERT,
list: [(error as any).response.data.detail],
});
}

View file

@ -21,17 +21,14 @@ import {
SelectValue,
} from "../../components/ui/select";
import { AuthContext } from "../../contexts/authContext";
import {
checkHasApiKey,
getStoreComponents,
getStoreTags,
} from "../../controllers/API";
import { getStoreComponents, getStoreTags } from "../../controllers/API";
import StoreApiKeyModal from "../../modals/StoreApiKeyModal";
import useAlertStore from "../../stores/alertStore";
import useFlowsManagerStore from "../../stores/flowsManagerStore";
import { useStoreStore } from "../../stores/storeStore";
import { storeComponent } from "../../types/store";
import { cn } from "../../utils/utils";
import { APIKEY_ERROR_ALERT, COMPONENTS_ERROR_ALERT, INVALID_API_ERROR_ALERT, NOAPI_ERROR_ALERT } from "../../alerts_constants";
export default function StorePage(): JSX.Element {
const hasApiKey = useStoreStore((state) => state.hasApiKey);
@ -39,8 +36,6 @@ export default function StorePage(): JSX.Element {
const loadingApiKey = useStoreStore((state) => state.loadingApiKey);
const setValidApiKey = useStoreStore((state) => state.updateValidApiKey);
const setLoadingApiKey = useStoreStore((state) => state.updateLoadingApiKey);
const setHasApiKey = useStoreStore((state) => state.updateHasApiKey);
const { apiKey } = useContext(AuthContext);
@ -48,6 +43,7 @@ export default function StorePage(): JSX.Element {
const setCurrentFlowId = useFlowsManagerStore(
(state) => state.setCurrentFlowId
);
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
const [loading, setLoading] = useState(true);
const [loadingTags, setLoadingTags] = useState(true);
const { id } = useParams();
@ -63,32 +59,29 @@ export default function StorePage(): JSX.Element {
const [searchNow, setSearchNow] = useState("");
const [selectFilter, setSelectFilter] = useState("all");
useEffect(() => {
handleGetTags();
}, []);
useEffect(() => {
if (!loadingApiKey) {
if (!hasApiKey) {
setErrorData({
title: "API Key Error",
title: APIKEY_ERROR_ALERT,
list: [
"You don't have an API Key. Please add one to use the Langflow Store.",
NOAPI_ERROR_ALERT,
],
});
setLoading(false);
} else if (!validApiKey) {
setErrorData({
title: "API Key Error",
title: APIKEY_ERROR_ALERT,
list: [
"Your API Key is not valid. Please add a valid API Key to use the Langflow Store.",
INVALID_API_ERROR_ALERT,
],
});
}
}
}, [loadingApiKey, validApiKey, hasApiKey]);
}, [loadingApiKey, validApiKey, hasApiKey, currentFlowId]);
useEffect(() => {
handleGetTags();
handleGetComponents();
}, [
tabActive,
@ -119,7 +112,7 @@ export default function StorePage(): JSX.Element {
}
function handleGetComponents() {
if (!hasApiKey || loadingApiKey) return;
if (loadingApiKey) return;
setLoading(true);
getStoreComponents({
component_id: id,
@ -159,7 +152,7 @@ export default function StorePage(): JSX.Element {
setTotalRowsCount(0);
setLoading(false);
setErrorData({
title: "Error getting components.",
title: COMPONENTS_ERROR_ALERT,
list: [err["response"]["data"]["detail"]],
});
}
@ -176,23 +169,6 @@ export default function StorePage(): JSX.Element {
setPageSize(12);
}
const fetchApiData = async () => {
setLoadingApiKey(true);
try {
const res = await checkHasApiKey();
setHasApiKey(res?.has_api_key ?? false);
setValidApiKey(res?.is_valid ?? false);
setLoadingApiKey(false);
} catch (e) {
setLoadingApiKey(false);
console.log(e);
}
};
useEffect(() => {
fetchApiData();
}, [apiKey]);
return (
<PageLayout
betaIcon

View file

@ -13,6 +13,7 @@ import {
inputHandlerEventType,
loginInputStateType,
} from "../../types/components";
import { SIGNIN_ERROR_ALERT } from "../../alerts_constants";
export default function LoginPage(): JSX.Element {
const [inputState, setInputState] =
@ -42,7 +43,7 @@ export default function LoginPage(): JSX.Element {
})
.catch((error) => {
setErrorData({
title: "Error signing in",
title: SIGNIN_ERROR_ALERT,
list: [error["response"]["data"]["detail"]],
});
});

View file

@ -15,6 +15,7 @@ import {
inputHandlerEventType,
signUpInputStateType,
} from "../../types/components";
import { SIGNUP_ERROR_ALERT } from "../../alerts_constants";
export default function SignUp(): JSX.Element {
const [inputState, setInputState] =
@ -60,7 +61,7 @@ export default function SignUp(): JSX.Element {
},
} = error;
setErrorData({
title: "Error signing up",
title: SIGNUP_ERROR_ALERT,
list: [detail],
});
return;

View file

@ -2,9 +2,11 @@ import { create } from "zustand";
import { getRepoStars, getVersion } from "../controllers/API";
import { DarkStoreType } from "../types/zustand/dark";
const startedStars = Number(window.localStorage.getItem("githubStars")) ?? 0;
export const useDarkStore = create<DarkStoreType>((set) => ({
dark: JSON.parse(window.localStorage.getItem("isDark")!) ?? false,
stars: 0,
stars: startedStars,
version: "",
setDark: (dark) => set(() => ({ dark: dark })),
refreshVersion: () => {
@ -13,7 +15,13 @@ export const useDarkStore = create<DarkStoreType>((set) => ({
});
},
refreshStars: () => {
if (window.localStorage.getItem("githubStars") !== null) {
set(() => ({ stars: startedStars }));
return;
}
getRepoStars("logspace-ai", "langflow").then((res) => {
window.localStorage.setItem("githubStars", res.toString());
set(() => ({ stars: res }));
});
},

View file

@ -9,8 +9,10 @@ import {
applyNodeChanges,
} from "reactflow";
import { create } from "zustand";
import { FLOW_BUILD_SUCCESS_ALERT } from "../alerts_constants";
import { BuildStatus } from "../constants/enums";
import { getFlowPool, updateFlowInDatabase } from "../controllers/API";
import { VertexBuildTypeAPI } from "../types/api";
import {
NodeDataType,
NodeType,
@ -25,6 +27,7 @@ import {
getNodeId,
scapeJSONParse,
scapedJSONStringfy,
validateNodes,
} from "../utils/reactflowUtils";
import { getInputsAndOutputs } from "../utils/storeUtils";
import useAlertStore from "./alertStore";
@ -34,6 +37,7 @@ import useFlowsManagerStore from "./flowsManagerStore";
// this is our useStore hook that we can use in our components to get parts of the store and call actions
const useFlowStore = create<FlowStoreType>((set, get) => ({
flowState: undefined,
flowBuildStatus: {},
nodes: [],
edges: [],
isBuilding: false,
@ -151,7 +155,6 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
? change(get().nodes.find((node) => node.id === id)!)
: change;
console.log(newChange);
get().setNodes((oldNodes) =>
oldNodes.map((node) => {
if (node.id === id) {
@ -375,17 +378,29 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
const setSuccessData = useAlertStore.getState().setSuccessData;
const setErrorData = useAlertStore.getState().setErrorData;
const setNoticeData = useAlertStore.getState().setNoticeData;
function handleBuildUpdate(data: any) {
const responseData = data.data[data.id];
// responseData MAY contain inactive_vertices or it
// may be null, so we need to check for it
if (responseData && responseData.inactive_vertices) {
useFlowStore
.getState()
.setInactiveNodes(responseData.inactive_vertices);
function validateSubgraph(nodes: string[]) {
const errors = validateNodes(
get().nodes.filter((node) => nodes.includes(node.id)),
get().edges
);
if (errors.length > 0) {
setErrorData({
title: "Oops! Looks like you missed something",
list: errors,
});
get().setIsBuilding(false);
throw new Error("Invalid nodes");
}
get().addDataToFlowPool(data.data[data.id], data.id);
useFlowStore.getState().updateBuildStatus([data.id], BuildStatus.BUILT);
}
function handleBuildUpdate(
vertexBuildData: VertexBuildTypeAPI,
status: BuildStatus
) {
if (vertexBuildData && vertexBuildData.inactive_vertices) {
get().removeFromVerticesBuild(vertexBuildData.inactive_vertices);
}
get().addDataToFlowPool(vertexBuildData, vertexBuildData.id);
useFlowStore.getState().updateBuildStatus([vertexBuildData.id], status);
}
await updateFlowInDatabase({
data: {
@ -397,15 +412,22 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
name: currentFlow!.name,
description: currentFlow!.description,
});
setNoticeData({ title: "Running components" });
await buildVertices({
flowId: currentFlow!.id,
nodeId,
onGetOrderSuccess: () => {
setNoticeData({ title: "Running components" });
},
onBuildComplete: () => {
if (nodeId) {
setSuccessData({ title: `${nodeId} built successfully` });
setSuccessData({
title: `${
get().nodes.find((node) => node.id === nodeId)?.data.node
?.display_name
} built successfully`,
});
} else {
setSuccessData({ title: `Flow built successfully` });
setSuccessData({ title: FLOW_BUILD_SUCCESS_ALERT });
}
get().setIsBuilding(false);
},
@ -417,7 +439,9 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
onBuildStart: (idList) => {
useFlowStore.getState().updateBuildStatus(idList, BuildStatus.BUILDING);
},
validateNodes: validateSubgraph,
});
get().revertBuiltStatusFromBuilding();
},
getFlow: () => {
return {
@ -426,23 +450,33 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
viewport: get().reactFlowInstance?.getViewport()!,
};
},
updateBuildStatus: (nodeIdList: string[], status: BuildStatus) => {
nodeIdList.forEach((id) => {
const nodeToUpdate = get().nodes.find((node) => node.id === id);
if (nodeToUpdate) {
nodeToUpdate.data.build_status = status;
get().setNodes(get().nodes);
}
});
},
updateVerticesBuild: (vertices: string[]) => {
set({ verticesBuild: vertices });
},
verticesBuild: [],
setInactiveNodes(newState) {
set({ inactiveNodes: newState });
removeFromVerticesBuild: (vertices: string[]) => {
set({
verticesBuild: get().verticesBuild.filter(
(vertex) => !vertices.includes(vertex)
),
});
},
updateBuildStatus: (nodeIdList: string[], status: BuildStatus) => {
const newFlowBuildStatus = { ...get().flowBuildStatus };
nodeIdList.forEach((id) => {
newFlowBuildStatus[id] = status;
});
set({ flowBuildStatus: newFlowBuildStatus });
},
revertBuiltStatusFromBuilding: () => {
const newFlowBuildStatus = { ...get().flowBuildStatus };
Object.keys(newFlowBuildStatus).forEach((id) => {
if (newFlowBuildStatus[id] === BuildStatus.BUILDING) {
newFlowBuildStatus[id] = BuildStatus.BUILT;
}
});
},
inactiveNodes: [],
}));
export default useFlowStore;

View file

@ -1,5 +1,5 @@
import { create } from "zustand";
import { checkHasStore } from "../controllers/API";
import { checkHasApiKey, checkHasStore } from "../controllers/API";
import { StoreStoreType } from "../types/zustand/store";
export const useStoreStore = create<StoreStoreType>((set) => ({
@ -7,13 +7,27 @@ export const useStoreStore = create<StoreStoreType>((set) => ({
validApiKey: false,
hasApiKey: false,
loadingApiKey: true,
updateHasStore: (hasStore) => set(() => ({ hasStore: hasStore })),
checkHasStore: () => {
checkHasStore().then((res) => {
set({ hasStore: res?.enabled ?? false });
});
},
updateValidApiKey: (validApiKey) => set(() => ({ validApiKey: validApiKey })),
updateLoadingApiKey: (loadingApiKey) =>
set(() => ({ loadingApiKey: loadingApiKey })),
updateHasApiKey: (hasApiKey) => set(() => ({ hasApiKey: hasApiKey })),
fetchApiData: async () => {
set({ loadingApiKey: true });
try {
const res = await checkHasApiKey();
set({
validApiKey: res?.is_valid ?? false,
hasApiKey: res?.has_api_key ?? false,
loadingApiKey: false,
});
} catch (e) {
set({ loadingApiKey: false });
console.log(e);
}
},
}));
checkHasStore().then((res) => {
useStoreStore.setState({ hasStore: res?.enabled ?? false });
});

View file

@ -175,7 +175,7 @@
@apply fixed bottom-4 right-4;
}
.message-button-icon {
@apply fill-chat-trigger stroke-chat-trigger stroke-1;
@apply fill-medium-indigo stroke-medium-indigo stroke-1;
}
.disabled-message-button-icon {
@apply fill-chat-trigger-disabled stroke-chat-trigger-disabled stroke-1;
@ -276,7 +276,7 @@
@apply h-10 w-10 rounded p-1;
}
.generic-node-tooltip-div {
@apply ml-2 truncate;
@apply ml-2 max-w-[220px] truncate;
}
.generic-node-validation-div {
@apply max-h-96 overflow-auto;
@ -290,17 +290,34 @@
@apply hidden h-4 w-4 animate-spin rounded-full bg-ring opacity-0;
}
.generic-node-status {
@apply opacity-100 animate-wiggle;
@apply animate-wiggle opacity-100;
}
.green-status {
@apply generic-node-status text-status-green;
}
.gray-status {
@apply generic-node-status text-status-gray;
}
.red-status {
@apply generic-node-status text-status-red;
}
.yellow-status {
@apply generic-node-status text-status-yellow;
}
.inactive-status {
/* what colour for inactive status?
muted-foreground is too strong, maybe use a lighter shade of it?
*/
@apply border-none ring ring-muted-foreground;
}
.built-invalid-status {
@apply border-none ring ring-[#FF9090];
}
.building-status {
@apply border-none ring;
}
.status-build-animation {
@apply opacity-0;
}
@ -828,7 +845,7 @@
@apply flex h-full max-w-full flex-col overflow-hidden rounded-md border bg-muted text-center sm:w-[75vw] md:w-[75vw] lg:w-[75vw] xl:w-[76vw] 2xl:w-full;
}
.api-modal-tablist-div {
@apply flex items-center justify-between px-2;
@apply flex items-center justify-between px-2 py-2;
}
.api-modal-tabs-content {
@apply -mt-1 h-full w-full overflow-hidden px-4 pb-4;
@ -1009,7 +1026,7 @@
}
.beta-badge-wrapper {
@apply absolute right-0 top-0 h-16 w-16 overflow-hidden rounded-tr-lg pointer-events-none;
@apply pointer-events-none absolute right-0 top-0 h-16 w-16 overflow-hidden rounded-tr-lg;
}
.beta-badge-content {
@apply mt-2 w-24 rotate-45 bg-beta-background text-center text-xs font-semibold text-beta-foreground;

View file

@ -60,6 +60,7 @@
--chat-send: #059669;
--status-green: #4ade80;
--status-blue: #2563eb;
--status-gray: #6b7280;
--connection: #555;
}

View file

@ -1,5 +1,4 @@
import { Edge, Node, Viewport } from "reactflow";
import { BuildStatus } from "../../constants/enums";
import { FlowType } from "../flow";
//kind and class are just representative names to represent the actual structure of the object received by the API
export type APIDataType = { [key: string]: APIKindType };
@ -37,7 +36,6 @@ export type APIClassType = {
| CustomFieldsType
| boolean
| undefined;
build_status?: BuildStatus;
};
export type TemplateVariableType = {
@ -139,8 +137,18 @@ export type VerticesOrderTypeAPI = {
export type VertexBuildTypeAPI = {
id: string;
inactive_vertices: Array<string> | null;
valid: boolean;
params: string;
data: VertexDataTypeAPI;
timestamp: string;
};
// data is the object received by the API
// it has results, artifacts, timedelta, duration
export type VertexDataTypeAPI = {
results: { [key: string]: { [key: string]: string } };
artifacts: { [key: string]: string };
timedelta?: number;
duration?: string;
};

View file

@ -9,6 +9,8 @@ export type ChatMessageType = {
files?: Array<{ data: string; type: string; data_type: string }>;
prompt?: string;
chatKey?: string;
id?: string;
stream_url?: string | null;
sender_name?: string;
};
@ -16,6 +18,7 @@ export type ChatOutputType = {
message: string;
sender: string;
sender_name: string;
stream_url?: string;
};
export type chatInputType = {

View file

@ -524,6 +524,11 @@ export type chatMessagePropsType = {
chat: ChatMessageType;
lockChat: boolean;
lastMessage: boolean;
updateChat: (
chat: ChatMessageType,
message: string,
stream_url: string
) => void;
};
export type formModalPropsType = {

View file

@ -29,7 +29,7 @@ export type NodeDataType = {
node?: APIClassType;
id: string;
output_types?: string[];
build_status?: BuildStatus;
buildStatus?: BuildStatus;
};
// FlowStyleType is the type of the style object that is used to style the
// Flow card with an emoji and a color.

View file

@ -86,9 +86,10 @@ export type FlowStoreType = {
unselectAll: () => void;
buildFlow: (nodeId?: string) => Promise<void>;
getFlow: () => { nodes: Node[]; edges: Edge[]; viewport: Viewport };
updateBuildStatus: (nodeId: string[], status: BuildStatus) => void;
updateVerticesBuild: (vertices: string[]) => void;
removeFromVerticesBuild: (vertices: string[]) => void;
verticesBuild: string[];
inactiveNodes: string[];
setInactiveNodes: (newState: string[]) => void;
updateBuildStatus: (nodeId: string[], status: BuildStatus) => void;
revertBuiltStatusFromBuilding: () => void;
flowBuildStatus: { [key: string]: BuildStatus };
};

View file

@ -3,8 +3,9 @@ export type StoreStoreType = {
validApiKey: boolean;
hasApiKey: boolean;
loadingApiKey: boolean;
updateHasStore: (hasStore: boolean) => void;
checkHasStore: () => void;
updateValidApiKey: (validApiKey: boolean) => void;
updateHasApiKey: (hasApiKey: boolean) => void;
updateLoadingApiKey: (loadingApiKey: boolean) => void;
fetchApiData: () => Promise<void>;
};

View file

@ -1,33 +1,73 @@
import { AxiosError } from "axios";
import { BuildStatus } from "../constants/enums";
import { getVerticesOrder, postBuildVertex } from "../controllers/API";
import useAlertStore from "../stores/alertStore";
import useFlowStore from "../stores/flowStore";
import { VertexBuildTypeAPI } from "../types/api";
type BuildVerticesParams = {
flowId: string; // Assuming FlowType is the type for your flow
nodeId?: string | null; // Assuming nodeId is of type string, and it's optional
onProgressUpdate?: (progress: number) => void; // Replace number with the actual type if it's not a number
onBuildUpdate?: (data: any) => void; // Replace any with the actual type of data
onGetOrderSuccess?: () => void;
onBuildUpdate?: (data: VertexBuildTypeAPI, status: BuildStatus) => void; // Replace any with the actual type if it's not any
onBuildComplete?: (allNodesValid: boolean) => void;
onBuildError?: (title, list, idList: string[]) => void;
onBuildStart?: (idList: string[]) => void;
validateNodes?: (nodes: string[]) => void;
};
function getInactiveVertexData(vertexId: string): VertexBuildTypeAPI {
// Build VertexBuildTypeAPI
let inactiveData = {
results: {},
artifacts: { repr: "Inactive" },
};
let inactiveVertexData = {
id: vertexId,
data: inactiveData,
params: "Inactive",
inactive_vertices: null,
valid: false,
timestamp: new Date().toISOString(),
};
return inactiveVertexData;
}
export async function buildVertices({
flowId,
nodeId = null,
onProgressUpdate,
onGetOrderSuccess,
onBuildUpdate,
onBuildComplete,
onBuildError,
onBuildStart,
validateNodes,
}: BuildVerticesParams) {
let orderResponse = await getVerticesOrder(flowId, nodeId);
const setErrorData = useAlertStore.getState().setErrorData;
let orderResponse;
try {
orderResponse = await getVerticesOrder(flowId, nodeId);
} catch (error) {
console.log(error);
setErrorData({
title: "Oops! Looks like you missed something",
list: [error.response?.data?.detail ?? "Unknown Error"],
});
useFlowStore.getState().setIsBuilding(false);
throw new Error("Invalid nodes");
}
if (onGetOrderSuccess) onGetOrderSuccess();
let verticesOrder: Array<Array<string>> = orderResponse.data.ids;
let vertices_layers: Array<Array<string>> = [];
let stop = false;
if (validateNodes) {
try {
validateNodes(verticesOrder.flatMap((id) => id));
} catch (e) {
return;
}
}
if (nodeId) {
for (let i = 0; i < verticesOrder.length; i += 1) {
const innerArray = verticesOrder[i];
@ -57,11 +97,13 @@ export async function buildVertices({
if (onBuildStart) onBuildStart(layer);
for (const id of layer) {
// Check if id is in the list of inactive nodes
if (useFlowStore.getState().inactiveNodes.includes(id)) {
// If it is, skip building
// it should be true because it did
// what it was supposed to do
buildResults.push(true);
if (
!useFlowStore.getState().verticesBuild.includes(id) &&
onBuildUpdate
) {
// If it is, skip building and set the state to inactive
onBuildUpdate(getInactiveVertexData(id), BuildStatus.INACTIVE);
buildResults.push(false);
continue;
}
await buildVertex({
@ -88,7 +130,6 @@ export async function buildVertices({
const allNodesValid = buildResults.every((result) => result);
onBuildComplete(allNodesValid);
useFlowStore.getState().setIsBuilding(false);
useFlowStore.getState().setInactiveNodes([]);
}
}
@ -103,7 +144,7 @@ async function buildVertex({
}: {
flowId: string;
id: string;
onBuildUpdate?: (data: any) => void;
onBuildUpdate?: (data: any, status: BuildStatus) => void;
onBuildError?: (title, list, idList: string[]) => void;
verticesIds: string[];
buildResults: boolean[];
@ -113,7 +154,6 @@ async function buildVertex({
const buildRes = await postBuildVertex(flowId, id);
const buildData: VertexBuildTypeAPI = buildRes.data;
if (onBuildUpdate) {
let data = {};
if (!buildData.valid) {
onBuildError!(
"Error Building Component",
@ -122,8 +162,7 @@ async function buildVertex({
);
stopBuild();
}
data[buildData.id] = buildData;
onBuildUpdate({ data, id: buildData.id });
onBuildUpdate(buildData, BuildStatus.BUILT);
}
buildResults.push(buildData.valid);
} catch (error) {

View file

@ -597,8 +597,7 @@ export function generateFlow(
const newFlowData = { nodes, edges, viewport: { zoom: 1, x: 0, y: 0 } };
const uid = new ShortUniqueId({ length: 5 });
/* remove edges that are not connected to selected nodes on both ends
in future we can save this edges to when ungrouping reconect to the old nodes
*/
*/
newFlowData.edges = selection.edges.filter(
(edge) =>
selection.nodes.some((node) => node.id === edge.target) &&
@ -619,12 +618,48 @@ export function generateFlow(
// in the future we can use a better aproach using a set
return {
newFlow,
removedEdges: selection.edges.filter(
(edge) => !newFlowData.edges.includes(edge)
removedEdges: edges.filter(
(edge) =>
(selection.nodes.some((node) => node.id === edge.target) ||
selection.nodes.some((node) => node.id === edge.source)) &&
newFlowData.edges.every((e) => e.id !== edge.id)
),
};
}
export function reconnectEdges(groupNode: NodeType, excludedEdges: Edge[]) {
let newEdges = cloneDeep(excludedEdges);
if (!groupNode.data.node!.flow) return [];
const { nodes, edges } = groupNode.data.node!.flow!.data!;
const lastNode = findLastNode(groupNode.data.node!.flow!.data!);
newEdges.forEach((edge) => {
if (lastNode && edge.source === lastNode.id) {
edge.source = groupNode.id;
let newSourceHandle: sourceHandleType = scapeJSONParse(
edge.sourceHandle!
);
newSourceHandle.id = groupNode.id;
edge.sourceHandle = scapedJSONStringfy(newSourceHandle);
edge.data.sourceHandle = newSourceHandle;
}
if (nodes.some((node) => node.id === edge.target)) {
const targetNode = nodes.find((node) => node.id === edge.target)!;
console.log("targetNode", targetNode);
const targetHandle: targetHandleType = scapeJSONParse(edge.targetHandle!);
console.log("targetHandle", targetHandle);
const proxy = { id: targetNode.id, field: targetHandle.fieldName };
let newTargetHandle: targetHandleType = cloneDeep(targetHandle);
newTargetHandle.id = groupNode.id;
newTargetHandle.proxy = proxy;
edge.target = groupNode.id;
newTargetHandle.fieldName = targetHandle.fieldName + "_" + targetNode.id;
edge.targetHandle = scapedJSONStringfy(newTargetHandle);
edge.data.targetHandle = newTargetHandle;
}
});
return newEdges;
}
export function filterFlow(
selection: OnSelectionChangeParams,
setNodes: (update: Node[] | ((oldState: Node[]) => Node[])) => void,

View file

@ -1,11 +1,13 @@
import {
AlertCircle,
ArrowLeft,
ArrowUpToLine,
Bell,
BookMarked,
BookmarkPlus,
Bot,
Boxes,
Braces,
Cable,
Check,
CheckCircle2,
@ -18,6 +20,7 @@ import {
ChevronsRight,
ChevronsUpDown,
Circle,
CircleDot,
Clipboard,
Code,
Code2,
@ -25,6 +28,7 @@ import {
Compass,
Copy,
Cpu,
Delete,
Download,
DownloadCloud,
Edit,
@ -101,6 +105,7 @@ import {
TextCursorInput,
ToyBrick,
Trash2,
Type,
Undo,
Ungroup,
Unplug,
@ -194,7 +199,7 @@ export const gradients = [
export const nodeColors: { [char: string]: string } = {
prompts: "#4367BF",
models: "#AA2411",
llms: "#6344BE",
model_specs: "#6344BE",
chains: "#FE7500",
Document: "#7AAE42",
list: "#9AAE42",
@ -223,7 +228,7 @@ export const nodeColors: { [char: string]: string } = {
export const nodeNames: { [char: string]: string } = {
prompts: "Prompts",
models: "Language Models",
llms: "Model Settings",
model_specs: "Model Specs",
chains: "Chains",
agents: "Agents",
tools: "Tools",
@ -295,7 +300,7 @@ export const nodeIconsLucide: iconsType = {
chains: Link,
memories: Cpu,
models: Bot,
llms: Lightbulb,
model_specs: Lightbulb,
prompts: TerminalSquare,
tools: Wrench,
advanced: Laptop2,
@ -336,6 +341,7 @@ export const nodeIconsLucide: iconsType = {
Bell,
ChevronLeft,
ChevronDown,
ArrowLeft,
Shield,
Plus,
Redo,
@ -344,6 +350,7 @@ export const nodeIconsLucide: iconsType = {
FileSearch2,
ChevronRight,
Circle,
CircleDot,
Clipboard,
Code2,
Variable,
@ -413,7 +420,10 @@ export const nodeIconsLucide: iconsType = {
Sliders,
ScreenShare,
Code,
Type,
Braces,
FlaskConical,
AlertCircle,
Bot,
Delete,
};

View file

@ -58,11 +58,15 @@ export function normalCaseToSnakeCase(str: string): string {
.join("_");
}
export function toTitleCase(str: string | undefined): string {
export function toTitleCase(
str: string | undefined,
isNodeField?: boolean
): string {
if (!str) return "";
let result = str
.split("_")
.map((word, index) => {
if (isNodeField) return word;
if (index === 0) {
return checkUpperWords(
word[0].toUpperCase() + word.slice(1).toLowerCase()
@ -75,6 +79,7 @@ export function toTitleCase(str: string | undefined): string {
return result
.split("-")
.map((word, index) => {
if (isNodeField) return word;
if (index === 0) {
return checkUpperWords(
word[0].toUpperCase() + word.slice(1).toLowerCase()
@ -642,6 +647,6 @@ export function getFieldTitle(
return template[templateField].display_name
? template[templateField].display_name!
: template[templateField].name
? toTitleCase(template[templateField].name!)
: toTitleCase(templateField);
? toTitleCase(template[templateField].name!, true)
: toTitleCase(templateField, true);
}

View file

@ -80,6 +80,7 @@ module.exports = {
"status-green": "var(--status-green)",
"status-red": "var(--status-red)",
"status-yellow": "var(--status-yellow)",
"status-gray": "var(--status-gray)",
"success-background": "var(--success-background)",
"success-foreground": "var(--success-foreground)",
"beta-background": "var(--beta-background)",