merge dev on feature/ui-table

This commit is contained in:
cristhianzl 2024-05-02 14:57:10 -03:00
commit 5042c0a750
163 changed files with 5263 additions and 2216 deletions

File diff suppressed because it is too large Load diff

View file

@ -37,11 +37,13 @@
"dompurify": "^3.0.5",
"dotenv": "^16.4.5",
"esbuild": "^0.17.19",
"file-saver": "^2.0.5",
"framer-motion": "^11.0.6",
"lodash": "^4.17.21",
"lucide-react": "^0.331.0",
"million": "^3.0.6",
"moment": "^2.29.4",
"openseadragon": "^4.1.1",
"playwright": "^1.42.0",
"react": "^18.2.21",
"react-ace": "^10.1.0",
@ -51,6 +53,7 @@
"react-icons": "^5.0.1",
"react-laag": "^2.0.5",
"react-markdown": "^8.0.7",
"react-pdf": "^7.7.1",
"react-router-dom": "^6.15.0",
"react-syntax-highlighter": "^15.5.0",
"react18-json-view": "^0.2.3",

View file

@ -96,6 +96,18 @@ body {
}
.custom-hover:hover {
background-color: rgba(99, 102, 241, 0.1); /* Medium indigo color with 20% opacity */
background-color: rgba(
99,
102,
241,
0.1
); /* Medium indigo color with 20% opacity */
}
.json-view-playground .json-view {
background-color: #fff !important;
}
.json-view-flow .json-view {
background-color: #bbb !important;
}

View file

@ -65,9 +65,10 @@ export default function App() {
}, [dark]);
useEffect(() => {
const abortController = new AbortController();
const isLoginPage = location.pathname.includes("login");
autoLogin()
autoLogin(abortController.signal)
.then(async (user) => {
if (user && user["access_token"]) {
user["refresh_token"] = "auto";
@ -78,31 +79,44 @@ export default function App() {
await Promise.all([refreshStars(), refreshVersion(), fetchData()]);
}
})
.catch(async () => {
setAutoLogin(false);
if (isAuthenticated && !isLoginPage) {
getUser();
await Promise.all([refreshStars(), refreshVersion(), fetchData()]);
} else {
setLoading(false);
useFlowsManagerStore.setState({ isLoading: false });
.catch(async (error) => {
if (error.name !== "CanceledError") {
setAutoLogin(false);
if (isAuthenticated && !isLoginPage) {
getUser();
await Promise.all([refreshStars(), refreshVersion(), fetchData()]);
} else {
setLoading(false);
useFlowsManagerStore.setState({ isLoading: false });
}
}
});
}, [isAuthenticated]);
/*
Abort the request as it isn't needed anymore, the component being
unmounted. It helps avoid, among other things, the well-known "can't
perform a React state update on an unmounted component" warning.
*/
return () => abortController.abort();
}, []);
const fetchData = async () => {
if (isAuthenticated) {
try {
await getTypes();
refreshFlows();
const res = await getGlobalVariables();
setGlobalVariables(res);
checkHasStore();
fetchApiData();
} catch (error) {
console.error("Failed to fetch data:", error);
return new Promise<void>(async (resolve, reject) => {
if (isAuthenticated) {
try {
await getTypes();
await refreshFlows();
const res = await getGlobalVariables();
setGlobalVariables(res);
checkHasStore();
fetchApiData();
resolve();
} catch (error) {
console.error("Failed to fetch data:", error);
reject();
}
}
}
});
};
useEffect(() => {

View file

@ -618,7 +618,7 @@ export default function ParameterComponent({
<FloatComponent
disabled={disabled}
value={data.node?.template[name].value ?? ""}
rangeSpec={data.node?.template[name].rangeSpec}
rangeSpec={data.node?.template[name]?.rangeSpec}
onChange={handleOnNewValue}
/>
</div>

View file

@ -0,0 +1,141 @@
import { useEffect, useRef, useState } from "react";
import ForwardedIconComponent from "../genericIconComponent";
import useFlowStore from "../../stores/flowStore";
import OpenSeadragon from 'openseadragon';
import { Separator } from "../ui/separator";
import { saveAs } from 'file-saver'
import useAlertStore from "../../stores/alertStore";
import { IMGViewErrorMSG, IMGViewErrorTitle } from "../../constants/constants";
export default function ImageViewer({image }) {
const viewerRef = useRef(null);
const [errorDownloading, setErrordownloading] = useState(false)
const setErrorList = useAlertStore(state => state.setErrorData);
const [initialMsg, setInicialMsg] = useState("Please build your flow");
useEffect(() => {
try {
if (viewerRef.current) {
// Initialize OpenSeadragon viewer
const viewer = OpenSeadragon({
element: viewerRef.current,
prefixUrl: 'https://cdnjs.cloudflare.com/ajax/libs/openseadragon/2.4.2/images/', // Optional: Set the path to OpenSeadragon images
tileSources: {type: 'image', url: image},
defaultZoomLevel: 1,
maxZoomPixelRatio: 4,
showNavigationControl: false,
});
const zoomInButton = document.getElementById('zoom-in-button');
const zoomOutButton = document.getElementById('zoom-out-button');
const homeButton = document.getElementById('home-button');
const fullPageButton = document.getElementById('full-page-button');
zoomInButton!.addEventListener('click', () => viewer.viewport.zoomBy(1.2));
zoomOutButton!.addEventListener('click', () => viewer.viewport.zoomBy(0.8));
homeButton!.addEventListener('click', () => viewer.viewport.goHome());
fullPageButton!.addEventListener('click', () => viewer.setFullScreen(true));
// Optionally, you can set additional viewer options here
// Cleanup function
return () => {
viewer.destroy();
zoomInButton!.removeEventListener('click', () => viewer.viewport.zoomBy(1.2));
zoomOutButton!.removeEventListener('click', () => viewer.viewport.zoomBy(0.8));
homeButton!.removeEventListener('click', () => viewer.viewport.goHome());
fullPageButton!.removeEventListener('click', () => viewer.setFullScreen(true));
};
}
} catch (error) {
console.error('Error initializing OpenSeadragon:', error);
}
}, [image]);
function download() {
const imageUrl = image;
// Fetch the image data
fetch(imageUrl)
.then(response => response.blob())
.then(blob => {
// Save the image using FileSaver.js
saveAs(blob, 'image.jpg');
})
.catch(error => {
setErrorList({title: "There was an error downloading your image"})
console.error('Error downloading image:', error)
});
}
return (
image === "" ? (
<div className="w-full h-full bg-muted rounded-md flex align-center justify-center flex-col gap-5 border border-border">
<div className="flex gap-2 align-center justify-center ">
<ForwardedIconComponent
name="Image"
/>
{IMGViewErrorTitle}
</div>
<div className="flex align-center justify-center">
<div className="langflow-chat-desc flex align-center justify-center">
<div className="langflow-chat-desc-span">
{IMGViewErrorMSG}
</div>
</div>
</div>
</div>
) : (
<>
<div className="w-full flex align-center justify-center my-2 mb-4">
<div className="shadow-round-btn-shadow hover:shadow-round-btn-shadow flex items-center justify-center rounded-sm border bg-muted shadow-md transition-all w-[50%]">
<button id="zoom-in-button" className="relative inline-flex w-full items-center justify-center px-3 py-3 text-sm font-semibold transition-all w-full transition-all duration-500 ease-in-out ease-in-out hover:bg-hover">
<ForwardedIconComponent
name="ZoomIn"
className={"text-secondary-foreground w-5 h-5"}
/>
</button>
<div>
<Separator orientation="vertical" />
</div>
<button id="zoom-out-button" className="relative inline-flex w-full items-center justify-center px-3 py-3 text-sm font-semibold transition-all transition-all duration-500 ease-in-out ease-in-out hover:bg-hover">
<ForwardedIconComponent
name="ZoomOut"
className={"text-secondary-foreground w-5 h-5"}
/>
</button>
<div>
<Separator orientation="vertical" />
</div>
<button id="home-button" className="relative inline-flex w-full items-center justify-center px-3 py-3 text-sm font-semibold transition-all transition-all duration-500 ease-in-out ease-in-out hover:bg-hover">
<ForwardedIconComponent
name="RotateCcw"
className={"text-secondary-foreground w-5 h-5"}
/>
</button>
<div>
<Separator orientation="vertical" />
</div>
<button id="full-page-button" className="relative inline-flex w-full items-center justify-center px-3 py-3 text-sm font-semibold transition-all transition-all duration-500 ease-in-out ease-in-out hover:bg-hover">
<ForwardedIconComponent
name="Maximize2"
className={"text-secondary-foreground w-5 h-5"}
/>
</button>
<div>
<Separator orientation="vertical" />
</div>
<button onClick={download} className="relative inline-flex w-full items-center justify-center px-3 py-3 text-sm font-semibold transition-all transition-all duration-500 ease-in-out ease-in-out hover:bg-hover">
<ForwardedIconComponent
name="ArrowDownToLine"
className={"text-secondary-foreground w-5 h-5"}
/>
</button>
</div>
</div>
<div id="canvas" ref={viewerRef} className={`w-full h-[90%] `} />
</>
)
);
}

View file

@ -1,7 +1,9 @@
import { useEffect, useState } from "react";
import { getComponent, postLikeComponent } from "../../controllers/API";
import DeleteConfirmationModal from "../../modals/DeleteConfirmationModal";
import IOModal from "../../modals/IOModal";
import useAlertStore from "../../stores/alertStore";
import useFlowStore from "../../stores/flowStore";
import useFlowsManagerStore from "../../stores/flowsManagerStore";
import { useStoreStore } from "../../stores/storeStore";
import { storeComponent } from "../../types/store";
@ -18,24 +20,30 @@ import {
CardHeader,
CardTitle,
} from "../ui/card";
import Loading from "../ui/loading";
export default function CollectionCardComponent({
data,
authorized = true,
disabled = false,
button,
onClick,
onDelete,
playground,
}: {
data: storeComponent;
authorized?: boolean;
disabled?: boolean;
onClick?: () => void;
button?: JSX.Element;
playground?: boolean;
onDelete?: () => void;
}) {
const addFlow = useFlowsManagerStore((state) => state.addFlow);
const setSuccessData = useAlertStore((state) => state.setSuccessData);
const setErrorData = useAlertStore((state) => state.setErrorData);
const setValidApiKey = useStoreStore((state) => state.updateValidApiKey);
const cleanFlowPool = useFlowStore((state) => state.CleanFlowPool);
const isStore = false;
const [loading, setLoading] = useState(false);
const [loadingLike, setLoadingLike] = useState(false);
@ -46,9 +54,39 @@ export default function CollectionCardComponent({
const [downloads_count, setDownloads_count] = useState(
data?.downloads_count ?? 0
);
const currentFlow = useFlowsManagerStore((state) => state.currentFlow);
const setCurrentFlow = useFlowsManagerStore((state) => state.setCurrentFlow);
const getFlowById = useFlowsManagerStore((state) => state.getFlowById);
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
const setNodes = useFlowStore((state) => state.setNodes);
const setEdges = useFlowStore((state) => state.setEdges);
const [openPlayground, setOpenPlayground] = useState(false);
const setCurrentFlowId = useFlowsManagerStore(
(state) => state.setCurrentFlowId
);
const [loadingPlayground, setLoadingPlayground] = useState(false);
const name = data.is_component ? "Component" : "Flow";
async function getFlowData() {
const res = await getComponent(data.id);
const newFlow = cloneFLowWithParent(res, res.id, data.is_component, true);
return newFlow;
}
useEffect(() => {
if (currentFlowId && playground) {
if (openPlayground) {
setNodes(currentFlow?.data?.nodes ?? [], true);
setEdges(currentFlow?.data?.edges ?? [], true);
} else {
setNodes([], true);
setEdges([], true);
cleanFlowPool();
}
}
}, [openPlayground]);
useEffect(() => {
if (data) {
setLiked_by_user(data?.liked_by_user ?? false);
@ -128,226 +166,325 @@ export default function CollectionCardComponent({
}
return (
<Card
className={cn(
"group relative flex min-h-[11rem] flex-col justify-between overflow-hidden transition-all hover:shadow-md",
disabled ? "pointer-events-none opacity-50" : ""
)}
>
<div>
<CardHeader>
<div>
<CardTitle className="flex w-full items-center justify-between gap-3 text-xl">
<IconComponent
className={cn(
"flex-shrink-0",
data.is_component
? "mx-0.5 h-6 w-6 text-component-icon"
: "h-7 w-7 flex-shrink-0 text-flow-icon"
)}
name={data.is_component ? "ToyBrick" : "Group"}
/>
<ShadTooltip content={data.name}>
<div className="w-full truncate">{data.name}</div>
</ShadTooltip>
{data?.metadata !== undefined && (
<div className="flex gap-3">
{data.private && (
<ShadTooltip content="Private">
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
<IconComponent name="Lock" className="h-4 w-4" />
</span>
</ShadTooltip>
<>
<Card
className={cn(
"group relative flex min-h-[11rem] flex-col justify-between overflow-hidden transition-all hover:shadow-md",
disabled ? "pointer-events-none opacity-50" : "",
onClick ? "cursor-pointer" : ""
)}
onClick={onClick}
>
<div>
<CardHeader>
<div>
<CardTitle className="flex w-full items-center justify-between gap-3 text-xl">
<IconComponent
className={cn(
"flex-shrink-0",
data.is_component
? "mx-0.5 h-6 w-6 text-component-icon"
: "h-7 w-7 flex-shrink-0 text-flow-icon"
)}
{!data.is_component && (
<ShadTooltip content="Components">
name={data.is_component ? "ToyBrick" : "Group"}
/>
<ShadTooltip content={data.name}>
<div className="w-full truncate">{data.name}</div>
</ShadTooltip>
{data?.metadata !== undefined && (
<div className="flex gap-3">
{data.private && (
<ShadTooltip content="Private">
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
<IconComponent name="Lock" className="h-4 w-4" />
</span>
</ShadTooltip>
)}
{!data.is_component && (
<ShadTooltip content="Components">
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
<IconComponent name="ToyBrick" className="h-4 w-4" />
<span data-testid={`total-${data.name}`}>
{data?.metadata?.total ?? 0}
</span>
</span>
</ShadTooltip>
)}
<ShadTooltip content="Likes">
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
<IconComponent name="ToyBrick" className="h-4 w-4" />
<span data-testid={`total-${data.name}`}>
{data?.metadata?.total ?? 0}
<IconComponent
name="Heart"
className={cn("h-4 w-4 ")}
/>
<span data-testid={`likes-${data.name}`}>
{likes_count ?? 0}
</span>
</span>
</ShadTooltip>
)}
<ShadTooltip content="Likes">
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
<IconComponent name="Heart" className={cn("h-4 w-4 ")} />
<span data-testid={`likes-${data.name}`}>
{likes_count ?? 0}
<ShadTooltip content="Downloads">
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
<IconComponent
name="DownloadCloud"
className="h-4 w-4"
/>
<span data-testid={`downloads-${data.name}`}>
{downloads_count ?? 0}
</span>
</span>
</span>
</ShadTooltip>
<ShadTooltip content="Downloads">
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
<IconComponent name="DownloadCloud" className="h-4 w-4" />
<span data-testid={`downloads-${data.name}`}>
{downloads_count ?? 0}
</span>
</span>
</ShadTooltip>
</div>
)}
</ShadTooltip>
</div>
)}
{onDelete && data?.metadata === undefined && (
<DeleteConfirmationModal
onConfirm={() => {
onDelete();
{onDelete && data?.metadata === undefined && (
<DeleteConfirmationModal
onConfirm={() => {
onDelete();
}}
>
<IconComponent
name="Trash2"
className="h-5 w-5 text-primary opacity-0 transition-all hover:text-destructive group-hover:opacity-100"
/>
</DeleteConfirmationModal>
)}
</CardTitle>
</div>
<div className="flex gap-2">
{data.user_created && data.user_created.username && (
<span className="text-sm text-primary">
by <b>{data.user_created.username}</b>
{data.last_tested_version && (
<>
{" "}
|{" "}
<span className="text-xs">
{" "}
v{data.last_tested_version}
</span>
</>
)}
</span>
)}
<div className="flex w-full flex-1 flex-wrap gap-2">
{data.tags &&
data.tags.length > 0 &&
data.tags.map((tag, index) => (
<Badge
key={index}
variant="outline"
size="xq"
className="text-muted-foreground"
>
{tag.name}
</Badge>
))}
</div>
</div>
<CardDescription className="pb-2 pt-2">
<div className="truncate-doubleline">{data.description}</div>
</CardDescription>
</CardHeader>
</div>
<CardFooter>
<div className="flex w-full items-center justify-between gap-2">
<div className="flex w-full flex-wrap items-end justify-between gap-2">
{playground && data?.metadata !== undefined ? (
<Button
disabled={loadingPlayground}
key={data.id}
tabIndex={-1}
variant="outline"
size="sm"
className="gap-2 whitespace-nowrap"
data-testid={"playground-flow-button-" + data.id}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setLoadingPlayground(true);
if (getFlowById(data.id)) {
setCurrentFlowId(data.id);
setOpenPlayground(true);
setLoadingPlayground(false);
} else {
getFlowData().then((res) => {
setCurrentFlow(res);
setOpenPlayground(true);
setLoadingPlayground(false);
});
}
}}
>
<IconComponent
name="Trash2"
className="h-5 w-5 text-primary opacity-0 transition-all hover:text-destructive group-hover:opacity-100"
/>
</DeleteConfirmationModal>
{!loadingPlayground ? (
<IconComponent
name="BotMessageSquareIcon"
className="h-4 w-4 select-none"
/>
) : (
<Loading className="h-4 w-4 text-medium-indigo" />
)}
Playground
</Button>
) : (
<div></div>
)}
</CardTitle>
</div>
{data.user_created && data.user_created.username && (
<span className="text-sm text-primary">
by <b>{data.user_created.username}</b>
{data.last_tested_version && (
<>
{" "}
|{" "}
<span className="text-xs">
{" "}
v{data.last_tested_version}
</span>
</>
)}
</span>
)}
<CardDescription className="pb-2 pt-2">
<div className="truncate-doubleline">{data.description}</div>
</CardDescription>
</CardHeader>
</div>
<CardFooter>
<div className="flex w-full items-center justify-between gap-2">
<div className="flex w-full flex-wrap items-end justify-between gap-2">
<div className="flex w-full flex-1 flex-wrap gap-2">
{data.tags &&
data.tags.length > 0 &&
data.tags.map((tag, index) => (
<Badge
key={index}
variant="outline"
size="xq"
className="text-muted-foreground"
>
{tag.name}
</Badge>
))}
</div>
{data.liked_by_count != undefined && (
<div className="flex gap-0.5">
{onDelete && data?.metadata !== undefined ? (
<ShadTooltip
content={
authorized ? "Delete" : "Please review your API key."
}
>
<DeleteConfirmationModal
onConfirm={() => {
onDelete();
}}
{data.liked_by_count != undefined && (
<div className="flex gap-0.5">
{onDelete && data?.metadata !== undefined ? (
<ShadTooltip
content={
authorized ? "Delete" : "Please review your API key."
}
>
<DeleteConfirmationModal
onConfirm={() => {
onDelete();
}}
>
<Button
variant="ghost"
size="icon"
className={
"whitespace-nowrap" +
(!authorized ? " cursor-not-allowed" : "")
}
>
<IconComponent
name="Trash2"
className={cn(
"h-5 w-5",
!authorized ? " text-ring" : ""
)}
/>
</Button>
</DeleteConfirmationModal>
</ShadTooltip>
) : (
<ShadTooltip
content={
authorized ? "Like" : "Please review your API key."
}
>
<Button
disabled={loadingLike}
variant="ghost"
size="icon"
className={
"whitespace-nowrap" +
(!authorized ? " cursor-not-allowed" : "")
}
onClick={() => {
if (!authorized) {
return;
}
handleLike();
}}
data-testid={`like-${data.name}`}
>
<IconComponent
name="Trash2"
name="Heart"
className={cn(
"h-5 w-5",
liked_by_user
? "fill-destructive stroke-destructive"
: "",
!authorized ? " text-ring" : ""
)}
/>
</Button>
</DeleteConfirmationModal>
</ShadTooltip>
) : (
</ShadTooltip>
)}
<ShadTooltip
content={
authorized ? "Like" : "Please review your API key."
authorized
? isStore
? "Download"
: "Install Locally"
: "Please review your API key."
}
>
<Button
disabled={loadingLike}
disabled={loading}
variant="ghost"
size="icon"
className={
"whitespace-nowrap" +
(!authorized ? " cursor-not-allowed" : "")
(!authorized ? " cursor-not-allowed" : "") +
(!loading ? " p-0.5" : "")
}
onClick={() => {
if (!authorized) {
if (loading || !authorized) {
return;
}
handleLike();
handleInstall();
}}
data-testid={`like-${data.name}`}
data-testid={`install-${data.name}`}
>
<IconComponent
name="Heart"
name={
loading ? "Loader2" : isStore ? "Download" : "Plus"
}
className={cn(
"h-5 w-5",
liked_by_user
? "fill-destructive stroke-destructive"
: "",
loading ? "h-5 w-5 animate-spin" : "h-5 w-5",
!authorized ? " text-ring" : ""
)}
/>
</Button>
</ShadTooltip>
)}
<ShadTooltip
content={
authorized
? isStore
? "Download"
: "Install Locally"
: "Please review your API key."
}
>
<Button
disabled={loading}
variant="ghost"
size="icon"
className={
"whitespace-nowrap" +
(!authorized ? " cursor-not-allowed" : "") +
(!loading ? " p-0.5" : "")
</div>
)}
{button && button}
{playground && data?.metadata === undefined && (
<Button
disabled={loadingPlayground}
key={data.id}
tabIndex={-1}
variant="outline"
size="sm"
className="gap-2 whitespace-nowrap"
data-testid={"playground-flow-button-" + data.id}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setLoadingPlayground(true);
if (getFlowById(data.id)) {
setCurrentFlowId(data.id);
setOpenPlayground(true);
setLoadingPlayground(false);
} else {
getFlowData().then((res) => {
setCurrentFlow(res);
setOpenPlayground(true);
setLoadingPlayground(false);
});
}
onClick={() => {
if (loading || !authorized) {
return;
}
handleInstall();
}}
data-testid={`install-${data.name}`}
>
}}
>
{!loadingPlayground ? (
<IconComponent
name={loading ? "Loader2" : isStore ? "Download" : "Plus"}
className={cn(
loading ? "h-5 w-5 animate-spin" : "h-5 w-5",
!authorized ? " text-ring" : ""
)}
name="BotMessageSquareIcon"
className="h-4 w-4 select-none"
/>
</Button>
</ShadTooltip>
</div>
)}
{button && button}
) : (
<Loading className="h-4 w-4 text-medium-indigo" />
)}
Playground
</Button>
)}
</div>
</div>
</div>
</CardFooter>
</Card>
</CardFooter>
</Card>
{openPlayground && (
<IOModal
cleanOnClose={true}
open={openPlayground}
setOpen={setOpenPlayground}
>
<></>
</IOModal>
)}
</>
);
}

View file

@ -87,15 +87,15 @@ export default function FlowToolbar(): JSX.Element {
}
>
<div className="flex">
<div className="flex h-full w-full gap-1 rounded-sm text-medium-indigo transition-all">
<div className="flex h-full w-full gap-1 rounded-sm transition-all">
{hasIO ? (
<IOModal 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">
<div className="relative inline-flex w-full items-center justify-center gap-1 px-5 py-3 text-sm font-semibold transition-all duration-500 ease-in-out hover:bg-hover">
<ForwardedIconComponent
name="Zap"
className={"message-button-icon h-5 w-5 transition-all"}
name="BotMessageSquareIcon"
className={" h-5 w-5 transition-all"}
/>
Run
Playground
</div>
</IOModal>
) : (

View file

@ -39,8 +39,8 @@ import { classNames } from "../../utils/utils";
import ShadTooltip from "../ShadTooltipComponent";
import DictComponent from "../dictComponent";
import IconComponent from "../genericIconComponent";
import InputGlobalComponent from "../inputGlobalComponent";
import KeypairListComponent from "../keypairListComponent";
import InputComponent from "../inputComponent";
export default function CodeTabsComponent({
flow,
@ -267,7 +267,7 @@ export default function CodeTabsComponent({
<div className="mx-auto">
{node.data.node.template[
templateField
].list ? (
]?.list ? (
<InputListComponent
componentName={
templateField
@ -351,31 +351,38 @@ export default function CodeTabsComponent({
/>
</div>
) : (
<InputGlobalComponent
<InputComponent
editNode={true}
disabled={false}
password={
node.data.node.template[
templateField
].password ?? false
}
value={
!node.data.node.template[
templateField
].value ||
node.data.node.template[
templateField
].value === ""
? ""
: node.data.node
.template[
templateField
].value
}
onChange={(target) => {
if (node.data) {
setNode(
node.data.id,
(oldNode) => {
let newNode =
cloneDeep(
oldNode
);
newNode.data = {
...newNode.data,
};
newNode.data.node.template[
templateField
].value = target;
return newNode;
}
);
}
setData((old) => {
let newInputList =
cloneDeep(old);
newInputList![
i
].data.node.template[
templateField
].value = target;
return newInputList;
});
tweaks.buildTweakObject!(
node["data"]["id"],
target,
@ -384,25 +391,6 @@ export default function CodeTabsComponent({
]
);
}}
setDb={(value) => {
setNode(
node.data.id,
(oldNode) => {
let newNode =
cloneDeep(oldNode);
newNode.data = {
...newNode.data,
};
newNode.data.node.template[
templateField
].load_from_db =
value;
return newNode;
}
);
}}
name={templateField}
data={node.data}
/>
)}
</div>
@ -745,7 +733,7 @@ export default function CodeTabsComponent({
isList={
node.data.node!.template[
templateField
].list ?? false
]?.list ?? false
}
/>
</div>

View file

@ -0,0 +1,27 @@
export const convertCSVToData = (csvFile, csvSeparator: string) => {
const lines = csvFile.data.trim().split("\n");
const headers = lines[0].trim().split(csvSeparator);
const initialRowData: any = [];
const initialColDefs = headers.map((header) => ({
field: header.trim(),
wrapText: true,
autoHeight: true,
height: "100%",
}));
for (let i = 1; i < lines.length; i++) {
const data = lines[i].trim().split(csvSeparator);
const rowDataEntry: any = {};
for (let j = 0; j < headers.length; j++) {
const value = isNaN(data[j]) ? data[j] : parseFloat(data[j]);
rowDataEntry[headers[j].trim()] = value;
}
initialRowData.push(rowDataEntry);
}
return { rowData: initialRowData, colDefs: initialColDefs };
};

View file

@ -0,0 +1,182 @@
import "ag-grid-community/styles/ag-grid.css"; // Mandatory CSS required by the grid
import "ag-grid-community/styles/ag-theme-balham.css"; // Optional Theme applied to the grid
import { AgGridReact } from "ag-grid-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import {
CSVError,
CSVNoDataError,
CSVViewErrorTitle,
} from "../../constants/constants";
import { useDarkStore } from "../../stores/darkStore";
import { FlowPoolObjectType } from "../../types/chat";
import { NodeType } from "../../types/flow";
import ForwardedIconComponent from "../genericIconComponent";
import Loading from "../ui/loading";
import { convertCSVToData } from "./helpers/convert-data-function";
function CsvOutputComponent({
csvNode,
flowPool,
}: {
csvNode: NodeType;
flowPool: FlowPoolObjectType;
}) {
const csvNodeArtifacts = flowPool?.data?.artifacts?.repr;
const jsonString = csvNodeArtifacts?.replace(/'/g, '"');
let file = null;
try {
file = JSON?.parse(jsonString) || "";
} catch (e) {
console.log("Error parsing JSON");
}
if (!file) {
return (
<div className=" align-center flex h-full w-full flex-col items-center justify-center gap-5">
<div className="align-center flex w-full justify-center gap-2">
<ForwardedIconComponent name="Table" />
{CSVViewErrorTitle}
</div>
<div className="align-center flex w-full justify-center">
<div className="langflow-chat-desc align-center flex justify-center px-6 py-8">
<div className="langflow-chat-desc-span">{CSVError}</div>
</div>
</div>
</div>
);
}
const separator = csvNode?.data?.node?.template?.separator?.value || ",";
const dark = useDarkStore.getState().dark;
const [rowData, setRowData] = useState([]);
const [colDefs, setColDefs] = useState([]);
const [status, setStatus] = useState("loading");
var currentRowHeight: number;
var minRowHeight = 25;
const defaultColDef = useMemo(() => {
return {
width: 200,
editable: true,
filter: true,
};
}, []);
useEffect(() => {
setStatus("loading");
if (file) {
const { rowData: data, colDefs: columns } = convertCSVToData(
file,
separator
);
setRowData(data);
setColDefs(columns);
setTimeout(() => {
setStatus("loaded");
}, 1000);
} else {
setStatus("nodata");
}
}, [separator]);
const getRowHeight = useCallback(() => {
return currentRowHeight;
}, []);
const onGridReady = useCallback((params: any) => {
minRowHeight = params.api.getSizesForCurrentTheme().rowHeight;
currentRowHeight = minRowHeight;
}, []);
const updateRowHeight = (params: { api: any }) => {
const bodyViewport = document.querySelector(".ag-body-viewport");
if (!bodyViewport) {
return;
}
var gridHeight = bodyViewport.clientHeight;
var renderedRowCount = params.api.getDisplayedRowCount();
if (renderedRowCount * minRowHeight >= gridHeight) {
if (currentRowHeight !== minRowHeight) {
currentRowHeight = minRowHeight;
params.api.resetRowHeights();
}
} else {
currentRowHeight = Math.floor(gridHeight / renderedRowCount);
params.api.resetRowHeights();
}
};
const onFirstDataRendered = useCallback(
(params: any) => {
updateRowHeight(params);
},
[updateRowHeight]
);
const onGridSizeChanged = useCallback(
(params: any) => {
updateRowHeight(params);
},
[updateRowHeight]
);
return (
<div className=" h-full rounded-md border bg-muted">
{status === "nodata" && (
<div className=" align-center flex h-full w-full flex-col items-center justify-center gap-5">
<div className="align-center flex w-full justify-center gap-2">
<ForwardedIconComponent name="Table" />
{CSVViewErrorTitle}
</div>
<div className="align-center flex w-full justify-center">
<div className="langflow-chat-desc align-center flex justify-center px-6 py-8">
<div className="langflow-chat-desc-span">{CSVNoDataError}</div>
</div>
</div>
</div>
)}
{status === "error" && (
<div className=" align-center flex h-full w-full flex-col items-center justify-center gap-5">
<div className="align-center flex w-full justify-center gap-2">
<ForwardedIconComponent name="Table" />
{CSVViewErrorTitle}
</div>
<div className="align-center flex w-full justify-center">
<div className="langflow-chat-desc align-center flex justify-center px-6 py-8">
<div className="langflow-chat-desc-span">{CSVError}</div>
</div>
</div>
</div>
)}
{status === "loaded" && (
<div
className={`${dark ? "ag-theme-balham-dark" : "ag-theme-balham"}`}
style={{ height: "100%", width: "100%" }}
>
<AgGridReact
rowData={rowData}
columnDefs={colDefs}
defaultColDef={defaultColDef}
getRowHeight={getRowHeight}
onGridReady={onGridReady}
onFirstDataRendered={onFirstDataRendered}
onGridSizeChanged={onGridSizeChanged}
scrollbarWidth={8}
/>
</div>
)}
{status === "loading" && (
<div className=" flex h-full w-full items-center justify-center align-middle">
<Loading />
</div>
)}
</div>
);
}
export default CsvOutputComponent;

View file

@ -5,6 +5,8 @@ import { nodeIconsLucide } from "../../utils/styleUtils";
import { cn } from "../../utils/utils";
import Loading from "../ui/loading";
import { useEffect, useState } from "react";
const ForwardedIconComponent = memo(
forwardRef(
(
@ -18,9 +20,18 @@ const ForwardedIconComponent = memo(
}: IconComponentProps,
ref
) => {
const [showFallback, setShowFallback] = useState(false);
useEffect(() => {
const timer = setTimeout(() => {
setShowFallback(true);
}, 30);
return () => clearTimeout(timer);
}, []);
let TargetIcon = nodeIconsLucide[name];
if (!TargetIcon) {
// check if name exists in dynamicIconImports
if (!dynamicIconImports[name]) {
TargetIcon = nodeIconsLucide["unknown"];
} else TargetIcon = lazy(dynamicIconImports[name]);
@ -35,11 +46,15 @@ const ForwardedIconComponent = memo(
if (!TargetIcon) {
return null; // Render nothing until the icon is loaded
}
const fallback = (
const fallback = showFallback ? (
<div className={cn(className, "flex items-center justify-center")}>
<Loading />
</div>
) : (
<div className={className}></div>
);
return (
<Suspense fallback={fallback}>
<TargetIcon

View file

@ -12,6 +12,7 @@ export default function InputListComponent({
disabled,
editNode = false,
componentName,
playgroundDisabled,
}: InputListComponentType): JSX.Element {
useEffect(() => {
if (disabled && value.length > 0 && value[0] !== "") {
@ -24,7 +25,7 @@ export default function InputListComponent({
value = [value];
}
if (!value.length) value = [""];
if (!value?.length) value = [""];
return (
<div
@ -37,7 +38,7 @@ export default function InputListComponent({
return (
<div key={idx} className="flex w-full gap-3">
<Input
disabled={disabled}
disabled={disabled || playgroundDisabled}
type="text"
value={singleValue}
className={editNode ? "input-edit-node" : ""}
@ -64,6 +65,7 @@ export default function InputListComponent({
editNode ? "-edit" : ""
}_${componentName}-` + idx
}
disabled={disabled || playgroundDisabled}
>
<IconComponent
name="Plus"
@ -82,10 +84,15 @@ export default function InputListComponent({
newInputList.splice(idx, 1);
onChange(newInputList);
}}
disabled={disabled || playgroundDisabled}
>
<IconComponent
name="X"
className="h-4 w-4 hover:text-status-red"
className={`h-4 w-4 ${
disabled || playgroundDisabled
? ""
: "hover:text-accent-foreground"
}`}
/>
</button>
)}

View file

@ -20,7 +20,13 @@ export default function KeypairListComponent({
}
}, [disabled]);
const ref = useRef(value.length === 0 ? [{ "": "" }] : value);
const checkValueType = (value) => {
return Array.isArray(value) ? value : [value];
};
const ref = useRef<any>([]);
ref.current =
!value || value?.length === 0 ? [{ "": "" }] : checkValueType(value);
useEffect(() => {
if (JSON.stringify(value) !== JSON.stringify(ref.current)) {

View file

@ -0,0 +1,23 @@
import { CHAT_FIRST_INITIAL_TEXT, CHAT_SECOND_INITIAL_TEXT, PDFCheckFlow, PDFLoadErrorTitle } from "../../../constants/constants";
import IconComponent from "../../genericIconComponent";
export default function Error(): JSX.Element {
return (
<div className="flex flex-col items-center justify-center h-full w-full bg-muted">
<div className="chat-alert-box">
<span className="flex gap-2">
<IconComponent name="FileX2" />
<span className="langflow-chat-span">{PDFLoadErrorTitle}</span>
</span>
<br />
<div className="langflow-chat-desc">
<span className="langflow-chat-desc-span">
{PDFCheckFlow}{" "}
</span>
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,155 @@
import { useEffect, useRef, useState } from "react";
import { Document, Page, pdfjs } from "react-pdf";
import "react-pdf/dist/esm/Page/AnnotationLayer.css";
import "react-pdf/dist/esm/Page/TextLayer.css";
import IconComponent from "../genericIconComponent";
import Loading from "../ui/loading";
import Error from "./Error";
import NoDataPdf from "./noData";
pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.js`;
export default function PdfViewer({ pdf }: { pdf: string }): JSX.Element {
const [numPages, setNumPages] = useState(-1);
const [pageNumber, setPageNumber] = useState(1);
const [scale, setScale] = useState(1);
const [width, setWidth] = useState<number | undefined>(undefined);
const [showControl, setShowControl] = useState(false);
const container = useRef<null | HTMLDivElement>(null);
//shortcuts to change page
useEffect(() => {
function handleKeyDown(event: KeyboardEvent) {
if (event.key === "ArrowLeft") {
if (pageNumber > 1) previousPage();
} else if (event.key === "ArrowRight") {
if (pageNumber < numPages) nextPage();
}
}
document.addEventListener("keydown", handleKeyDown);
return () => {
document.removeEventListener("keydown", handleKeyDown);
};
}, [pageNumber]);
function onDocumentLoadSuccess({ numPages }) {
setNumPages(numPages);
setPageNumber(1);
}
function changePage(offset) {
setPageNumber((prevPageNumber) => prevPageNumber + offset);
}
function previousPage() {
changePage(-1);
}
function nextPage() {
changePage(1);
}
//set handle scale in % to real number
function handleScaleChange(e) {
//check if e is a number
if (isNaN(e) || e < 0.1) return;
// round to 2 decimal places
e = Math.round(e * 10) / 10;
setScale(e);
}
function zoomIn() {
handleScaleChange(scale + 0.1);
}
function zoomOut() {
if (scale > 0.1) handleScaleChange(scale - 0.1);
}
function handlePageLoad(page) {
if (!container.current) return;
const containerWidth = container.current.clientWidth;
const pageWidth = page.width;
if (containerWidth > pageWidth) {
setWidth(containerWidth - 10);
}
}
return (
<div
ref={container}
onMouseEnter={(_) => setShowControl(true)}
onMouseLeave={(_) => setShowControl(false)}
className="flex h-full w-full flex-col items-center justify-end overflow-clip rounded-lg border border-border"
>
<div className={"h-full min-h-0 w-full overflow-auto custom-scroll"}>
<Document
loading={
<div className="flex h-full w-full items-center justify-center align-middle">
<Loading />
</div>
}
onLoadSuccess={onDocumentLoadSuccess}
file={pdf}
noData={<NoDataPdf />}
error={<Error />}
className="h-full w-full"
>
<Page
width={width}
onLoadSuccess={handlePageLoad}
scale={scale}
renderTextLayer
pageNumber={pageNumber}
className={"h-full max-h-0 w-full"}
/>
</Document>
</div>
<div
className={
"absolute z-50 pb-5 " + (showControl && numPages > 0 ? "" : " hidden")
}
>
<div className=" flex w-min items-center justify-center gap-0.5 rounded-xl bg-secondary px-2 align-middle">
<button
type="button"
disabled={pageNumber <= 1}
onClick={previousPage}
>
<IconComponent
name={"ChevronLeft"}
className="h-6 w-6"
></IconComponent>
</button>
<p>
{pageNumber || (numPages ? 1 : "--")}/{numPages || "--"}
</p>
<button
type="button"
disabled={pageNumber >= numPages}
onClick={nextPage}
>
<IconComponent
name={"ChevronRight"}
className="h-6 w-6"
></IconComponent>
</button>
<p className="px-2">|</p>
<button type="button" onClick={zoomOut}>
<IconComponent name={"ZoomOut"} className="h-6 w-6"></IconComponent>
</button>
<input
type="number"
step={0.1}
className="w-6 border-b bg-transparent text-center arrow-hide"
onChange={(e) => handleScaleChange(e.target.value)}
value={scale}
/>
<button type="button" onClick={zoomIn}>
<IconComponent name={"ZoomIn"} className="h-6 w-6"></IconComponent>
</button>
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,17 @@
import { PDFErrorTitle, PDFLoadError } from "../../../constants/constants";
export default function NoDataPdf(): JSX.Element {
return (
<div className="flex h-full w-full flex-col items-center justify-center bg-muted">
<div className="chat-alert-box">
<span>
📄 <span className="langflow-chat-span">{PDFErrorTitle}</span>
</span>
<br />
<div className="langflow-chat-desc">
<span className="langflow-chat-desc-span">{PDFLoadError} </span>
</div>
</div>
</div>
);
}

View file

@ -161,6 +161,29 @@ export const IMPORT_DIALOG_SUBTITLE =
*/
export const TOOLTIP_EMPTY = "No compatible components found.";
export const CSVViewErrorTitle = "CSV output";
export const CSVNoDataError = "No data available";
export const PDFViewConstant = "Expand the ouptut to see the PDF";
export const CSVError = "Error loading CSV";
export const PDFLoadErrorTitle = "Error loading PDF";
export const PDFCheckFlow = "Please check your flow and try again";
export const PDFErrorTitle = "PDF Output";
export const PDFLoadError = "Run the flow to see the pdf";
export const IMGViewConstant = "Expand the view to see the image";
export const IMGViewErrorMSG =
"Run the flow or inform a valid url to see your image";
export const IMGViewErrorTitle = "Image output";
/**
* The base text for subtitle of code dialog
* @constant
@ -688,8 +711,23 @@ 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 INPUT_TYPES = new Set([
"ChatInput",
"TextInput",
"KeyPairInput",
"JsonInput",
"StringListInput",
]);
export const OUTPUT_TYPES = new Set([
"ChatOutput",
"TextOutput",
"PDFOutput",
"ImageOutput",
"CSVOutput",
"JsonOutput",
"KeyPairOutput",
"StringListOutput",
]);
export const CHAT_FIRST_INITIAL_TEXT =
"Start a conversation and click the agent's thoughts";

View file

@ -1,5 +1,5 @@
import { AxiosResponse } from "axios";
import { ReactFlowJsonObject } from "reactflow";
import { AxiosRequestConfig, AxiosResponse } from "axios";
import { Edge, ReactFlowJsonObject,Node } from "reactflow";
import { BASE_URL_API } from "../../constants/constants";
import { api } from "../../controllers/API/api";
import {
@ -406,9 +406,11 @@ export async function onLogin(user: LoginType) {
}
}
export async function autoLogin() {
export async function autoLogin(abortSignal) {
try {
const response = await api.get(`${BASE_URL_API}auto_login`);
const response = await api.get(`${BASE_URL_API}auto_login`, {
signal: abortSignal,
});
if (response.status === 200) {
const data = response.data;
@ -926,17 +928,26 @@ export async function updateGlobalVariable(
export async function getVerticesOrder(
flowId: string,
startNodeId?: string | null,
stopNodeId?: string | null
stopNodeId?: string | null,
nodes?:Node[],
Edges?:Edge[]
): Promise<AxiosResponse<VerticesOrderTypeAPI>> {
// nodeId is optional and is a query parameter
// if nodeId is not provided, the API will return all vertices
const config = {};
const config:AxiosRequestConfig<any> = {};
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);
const data = {
data:{}
}
if(nodes && Edges){
data["data"]["nodes"] = nodes
data["data"]["edges"] = Edges
}
return await api.post(`${BASE_URL_API}build/${flowId}/vertices`,data, config);
}
export async function postBuildVertex(

View file

@ -203,7 +203,7 @@ const EditNodeModal = forwardRef(
!myData.node.template[templateParam].options ? (
<div className="mx-auto">
{myData.node.template[templateParam]
.list ? (
?.list ? (
<InputListComponent
componentName={templateParam}
editNode={true}
@ -345,7 +345,7 @@ const EditNodeModal = forwardRef(
}}
isList={
data.node?.template[templateParam]
.list ?? false
?.list ?? false
}
/>
</div>
@ -420,6 +420,10 @@ const EditNodeModal = forwardRef(
.type === "int" ? (
<div className="mx-auto">
<IntComponent
rangeSpec={
data.node?.template[templateParam]
?.rangeSpec
}
id={
"edit-int-input-" +
myData.node.template[templateParam].name

View file

@ -0,0 +1,45 @@
import { useEffect, useRef } from "react";
import JsonView from "react18-json-view";
import { useDarkStore } from "../../../../../../stores/darkStore";
import { DictComponentType } from "../../../../../../types/components";
export default function IoJsonInput({
value = [],
onChange,
left,
output,
}: DictComponentType): JSX.Element {
useEffect(() => {
if (value) onChange(value);
}, [value]);
const isDark = useDarkStore((state) => state.dark);
const ref = useRef<any>(null);
ref.current = value;
const getClassNames = () => {
if (!isDark && !left) return "json-view-playground-white";
if (!isDark && left) return "json-view-playground-white-left";
if (isDark && left) return "json-view-playground-dark-left";
if (isDark && !left) return "json-view-playground-dark";
};
return (
<div className="w-full">
<JsonView
className={getClassNames()}
theme="vscode"
dark={isDark}
editable={!output}
enableClipboard
onEdit={(edit) => {
ref.current = edit["src"];
}}
onChange={(edit) => {
ref.current = edit["src"];
}}
src={ref.current}
/>
</div>
);
}

View file

@ -0,0 +1,107 @@
import _ from "lodash";
import { useRef } from "react";
import IconComponent from "../../../../../../components/genericIconComponent";
import { Input } from "../../../../../../components/ui/input";
import { classNames } from "../../../../../../utils/utils";
export type IOKeyPairInputProps = {
value: any;
onChange: (value: any) => void;
duplicateKey: boolean;
isList: boolean;
isInputField?: boolean;
};
const IOKeyPairInput = ({
value,
onChange,
duplicateKey,
isList = true,
isInputField,
}: IOKeyPairInputProps) => {
const checkValueType = (value) => {
return Array.isArray(value) ? value : [value];
};
const ref = useRef<any>([]);
ref.current =
!value || value?.length === 0 ? [{ "": "" }] : checkValueType(value);
const handleChangeKey = (event, idx) => {
const oldKey = Object.keys(ref.current[idx])[0];
const updatedObj = { [event.target.value]: ref.current[idx][oldKey] };
ref.current[idx] = updatedObj;
onChange(ref.current);
};
const handleChangeValue = (newValue, idx) => {
const key = Object.keys(ref.current[idx])[0];
ref.current[idx][key] = newValue;
onChange(ref.current);
};
return (
<>
<div className={classNames("flex h-full flex-col gap-3")}>
{ref.current?.map((obj, index) => {
return Object.keys(obj).map((key, idx) => {
return (
<div key={idx} className="flex w-full gap-2">
<Input
type="text"
value={key.trim()}
className={classNames(duplicateKey ? "input-invalid" : "")}
placeholder="Type key..."
onChange={(event) => handleChangeKey(event, index)}
disabled={!isInputField}
/>
<Input
type="text"
value={obj[key]}
placeholder="Type a value..."
onChange={(event) =>
handleChangeValue(event.target.value, index)
}
disabled={!isInputField}
/>
{isList && isInputField && index === ref.current.length - 1 ? (
<button
onClick={() => {
let newInputList = _.cloneDeep(ref.current);
newInputList.push({ "": "" });
onChange(newInputList);
}}
>
<IconComponent
name="Plus"
className={"h-4 w-4 hover:text-accent-foreground"}
/>
</button>
) : isList && isInputField ? (
<button
onClick={() => {
let newInputList = _.cloneDeep(ref.current);
newInputList.splice(index, 1);
onChange(newInputList);
}}
>
<IconComponent
name="X"
className="h-4 w-4 hover:text-status-red"
/>
</button>
) : (
""
)}
</div>
);
});
})}
</div>
</>
);
};
export default IOKeyPairInput;

View file

@ -1,9 +1,29 @@
import { cloneDeep } from "lodash";
import { useState } from "react";
import ImageViewer from "../../../../components/ImageViewer";
import CsvOutputComponent from "../../../../components/csvOutputComponent";
import InputListComponent from "../../../../components/inputListComponent";
import PdfViewer from "../../../../components/pdfViewer";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "../../../../components/ui/select";
import { Textarea } from "../../../../components/ui/textarea";
import { PDFViewConstant } from "../../../../constants/constants";
import { InputOutput } from "../../../../constants/enums";
import useFlowStore from "../../../../stores/flowStore";
import { IOFieldViewProps } from "../../../../types/components";
import {
convertValuesToNumbers,
hasDuplicateKeys,
} from "../../../../utils/reactflowUtils";
import IOFileInput from "./components/FileInput";
import IoJsonInput from "./components/JSONInput";
import IOKeyPairInput from "./components/keyPairInput";
export default function IOFieldView({
type,
@ -15,6 +35,20 @@ export default function IOFieldView({
const setNode = useFlowStore((state) => state.setNode);
const flowPool = useFlowStore((state) => state.flowPool);
const node = nodes.find((node) => node.id === fieldId);
const flowPoolNode = (flowPool[node!.id] ?? [])[
(flowPool[node!.id]?.length ?? 1) - 1
];
const handleChangeSelect = (e) => {
if (node) {
let newNode = cloneDeep(node);
if (newNode.data.node.template.separator) {
newNode.data.node.template.separator.value = e;
setNode(newNode.id, newNode);
}
}
};
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
function handleOutputType() {
if (!node) return <>"No node found!"</>;
switch (type) {
@ -53,6 +87,57 @@ export default function IOFieldView({
/>
);
case "KeyPairInput":
return (
<IOKeyPairInput
value={node.data.node!.template["input_value"]?.value}
onChange={(e) => {
if (node) {
let newNode = cloneDeep(node);
newNode.data.node!.template["input_value"].value = e;
setNode(node.id, newNode);
}
const valueToNumbers = convertValuesToNumbers(e);
setErrorDuplicateKey(hasDuplicateKeys(valueToNumbers));
}}
duplicateKey={errorDuplicateKey}
isList={node.data.node!.template["input_value"]?.list ?? false}
isInputField
/>
);
case "JsonInput":
return (
<IoJsonInput
value={node.data.node!.template["input_value"]?.value}
onChange={(e) => {
if (node) {
let newNode = cloneDeep(node);
newNode.data.node!.template["input_value"].value = e;
setNode(node.id, newNode);
}
}}
left={left}
/>
);
case "StringListInput":
return (
<>
<InputListComponent
value={node.data.node!.template["input_value"]?.value}
onChange={(e) => {
if (node) {
let newNode = cloneDeep(node);
newNode.data.node!.template["input_value"].value = e;
setNode(node.id, newNode);
}
}}
disabled={false}
/>
</>
);
default:
return (
<Textarea
@ -91,6 +176,110 @@ export default function IOFieldView({
readOnly
/>
);
case "PDFOutput":
return left ? (
<div>{PDFViewConstant}</div>
) : (
<PdfViewer pdf={flowPoolNode?.params ?? ""} />
);
case "CSVOutput":
return left ? (
<>
<div className="flex justify-between">
Expand the ouptut to see the CSV
</div>
<div className="flex items-center justify-between pt-5">
<span>CSV separator </span>
<Select
value={node.data.node.template.separator.value}
onValueChange={(e) => handleChangeSelect(e)}
>
<SelectTrigger className="w-[70px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{node?.data?.node?.template?.separator?.options.map(
(separator) => (
<SelectItem key={separator} value={separator}>
{separator}
</SelectItem>
)
)}
</SelectGroup>
</SelectContent>
</Select>
</div>
</>
) : (
<>
<CsvOutputComponent csvNode={node} flowPool={flowPoolNode} />
</>
);
case "ImageOutput":
return left ? (
<div>Expand the view to see the image</div>
) : (
<ImageViewer
image={
(flowPool[node.id] ?? [])[
(flowPool[node.id]?.length ?? 1) - 1
]?.params ?? ""
}
/>
);
case "JsonOutput":
return (
<IoJsonInput
value={node.data.node!.template["input_value"]?.value}
onChange={(e) => {
if (node) {
let newNode = cloneDeep(node);
newNode.data.node!.template["input_value"].value = e;
setNode(node.id, newNode);
}
}}
left={left}
output
/>
);
case "KeyPairOutput":
return (
<IOKeyPairInput
value={node.data.node!.template["input_value"]?.value}
onChange={(e) => {
if (node) {
let newNode = cloneDeep(node);
newNode.data.node!.template["input_value"].value = e;
setNode(node.id, newNode);
}
const valueToNumbers = convertValuesToNumbers(e);
setErrorDuplicateKey(hasDuplicateKeys(valueToNumbers));
}}
duplicateKey={errorDuplicateKey}
isList={node.data.node!.template["input_value"]?.list ?? false}
/>
);
case "StringListOutput":
return (
<>
<InputListComponent
value={node.data.node!.template["input_value"]?.value}
onChange={(e) => {
if (node) {
let newNode = cloneDeep(node);
newNode.data.node!.template["input_value"].value = e;
setNode(node.id, newNode);
}
}}
playgroundDisabled
disabled={false}
/>
</>
);
default:
return (

View file

@ -83,12 +83,6 @@ export default function IOModal({
return updateVerticesOrder(currentFlow!.id, null);
}
useEffect(() => {
if (open) {
updateVertices();
}
}, [open, currentFlow]);
async function sendMessage(count = 1): Promise<void> {
if (isBuilding) return;
setIsBuilding(true);
@ -132,9 +126,9 @@ export default function IOModal({
{/* 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">Interaction Panel</span>
<span className="pr-2">Playground</span>
<IconComponent
name="prompts"
name="BotMessageSquareIcon"
className="h-6 w-6 pl-1 text-foreground"
aria-hidden="true"
/>

View file

@ -86,6 +86,7 @@ export default function StoreApiKeyModal({
<Form.Root
onSubmit={(event) => {
event.preventDefault();
handleSaveKey();
}}
>
<div className="grid gap-5">
@ -131,9 +132,6 @@ export default function StoreApiKeyModal({
<Button
data-testid="api-key-save-button-store"
className="mt-8"
onClick={() => {
handleSaveKey();
}}
>
Save
</Button>

View file

@ -11,6 +11,7 @@ import "react18-json-view/src/style.css";
import IconComponent from "../../components/genericIconComponent";
import { Button } from "../../components/ui/button";
import { CODE_DICT_DIALOG_SUBTITLE } from "../../constants/constants";
import { useDarkStore } from "../../stores/darkStore";
import BaseModal from "../baseModal";
export default function DictAreaModal({
@ -19,7 +20,7 @@ export default function DictAreaModal({
value,
}): JSX.Element {
const [open, setOpen] = useState(false);
const isDark = useDarkStore((state) => state.dark);
const ref = useRef(value);
useEffect(() => {
@ -41,7 +42,8 @@ export default function DictAreaModal({
<div className="flex h-full w-full flex-col transition-all ">
<JsonView
theme="vscode"
dark={true}
dark={isDark}
className={!isDark ? "json-view-white" : "json-view-dark"}
editable
enableClipboard
onEdit={(edit) => {

View file

@ -25,6 +25,7 @@ import {
import { getTagsIds } from "../../utils/storeUtils";
import ConfirmationModal from "../ConfirmationModal";
import BaseModal from "../baseModal";
import ExportModal from "../exportModal";
export default function ShareModal({
component,
@ -206,9 +207,8 @@ export default function ShareModal({
{children ? children : <></>}
</BaseModal.Trigger>
<BaseModal.Header
description={`Publish ${
is_component ? "your component" : "workflow"
} to the Langflow Store.`}
description={`Publish ${is_component ? "your component" : "workflow"
} to the Langflow Store.`}
>
<span className="pr-2">Share</span>
<IconComponent
@ -251,18 +251,34 @@ export default function ShareModal({
<BaseModal.Footer>
<div className="flex w-full justify-between gap-2">
<Button
{!is_component && <ExportModal>
<Button
type="button"
variant="outline"
className="gap-2"
onClick={() => {
// (setOpen || internalSetOpen)(false);
}}
>
<IconComponent name="Download" className="h-4 w-4" />
Export
</Button>
</ExportModal>
}
{is_component && <Button
type="button"
variant="outline"
className="gap-2"
onClick={() => {
handleExportComponent();
(setOpen || internalSetOpen)(false);
handleExportComponent();
}}
>
<IconComponent name="Download" className="h-4 w-4" />
Export
</Button>
}
<Button
disabled={loadingNames}
type="button"

View file

@ -6,18 +6,25 @@ import { useDarkStore } from "../../stores/darkStore";
import useFlowsManagerStore from "../../stores/flowsManagerStore";
import Page from "./components/PageComponent";
import ExtraSidebar from "./components/extraSidebarComponent";
import useFlowStore from "../../stores/flowStore";
export default function FlowPage({ view }: { view?: boolean }): JSX.Element {
const setCurrentFlowId = useFlowsManagerStore(
(state) => state.setCurrentFlowId
);
const version = useDarkStore((state) => state.version);
const setOnFlowPage = useFlowStore((state) => state.setOnFlowPage);
const currentFlow = useFlowsManagerStore((state) => state.currentFlow);
const { id } = useParams();
// Set flow tab id
useEffect(() => {
setCurrentFlowId(id!);
setOnFlowPage(true);
return () => {
setOnFlowPage(false);
};
}, [id]);
return (
<>

View file

@ -1,4 +1,4 @@
import { useEffect, useState } from "react";
import { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import PaginatorComponent from "../../../../components/PaginatorComponent";
import CollectionCardComponent from "../../../../components/cardComponent";
@ -14,7 +14,6 @@ import {
import useAlertStore from "../../../../stores/alertStore";
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
import { FlowType } from "../../../../types/flow";
export default function ComponentsComponent({
is_component = true,
}: {
@ -24,43 +23,36 @@ export default function ComponentsComponent({
const uploadFlow = useFlowsManagerStore((state) => state.uploadFlow);
const removeFlow = useFlowsManagerStore((state) => state.removeFlow);
const isLoading = useFlowsManagerStore((state) => state.isLoading);
const setExamples = useFlowsManagerStore((state) => state.setExamples);
const flows = useFlowsManagerStore((state) => state.flows);
const setSuccessData = useAlertStore((state) => state.setSuccessData);
const setErrorData = useAlertStore((state) => state.setErrorData);
const [pageSize, setPageSize] = useState(20);
const [pageIndex, setPageIndex] = useState(1);
const [loadingScreen, setLoadingScreen] = useState(true);
const navigate = useNavigate();
useEffect(() => {
if (isLoading) return;
let all = flows
.filter((f) => (f.is_component ?? false) === is_component)
.sort((a, b) => {
if (a?.updated_at && b?.updated_at) {
return (
new Date(b?.updated_at!).getTime() -
new Date(a?.updated_at!).getTime()
);
} else if (a?.updated_at && !b?.updated_at) {
return 1;
} else if (!a?.updated_at && b?.updated_at) {
return -1;
} else {
return (
new Date(b?.date_created!).getTime() -
new Date(a?.date_created!).getTime()
);
}
});
const start = (pageIndex - 1) * pageSize;
const end = start + pageSize;
setData(all.slice(start, end));
}, [flows, isLoading, pageIndex, pageSize]);
const [data, setData] = useState<FlowType[]>([]);
const all: FlowType[] = flows
.filter((f) => (f.is_component ?? false) === is_component)
.sort((a, b) => {
if (a?.updated_at && b?.updated_at) {
return (
new Date(b?.updated_at!).getTime() -
new Date(a?.updated_at!).getTime()
);
} else if (a?.updated_at && !b?.updated_at) {
return 1;
} else if (!a?.updated_at && b?.updated_at) {
return -1;
} else {
return (
new Date(b?.date_created!).getTime() -
new Date(a?.date_created!).getTime()
);
}
});
const start = (pageIndex - 1) * pageSize;
const end = start + pageSize;
const data: FlowType[] = all.slice(start, end);
const name = is_component ? "Component" : "Flow";
@ -149,8 +141,9 @@ export default function ComponentsComponent({
resetFilter();
}}
key={idx}
data={item}
data={{ is_component: item.is_component ?? false, ...item }}
disabled={isLoading}
data-testid={"edit-flow-button-" + item.id + "-" + idx}
button={
!is_component ? (
<Link to={"/flow/" + item.id}>
@ -174,6 +167,14 @@ export default function ComponentsComponent({
<></>
)
}
onClick={
!is_component
? () => {
navigate("/flow/" + item.id);
}
: undefined
}
playground={!is_component}
/>
))
) : (

View file

@ -0,0 +1,64 @@
import { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import useFlowsManagerStore from "../../stores/flowsManagerStore";
import { getComponent } from "../../controllers/API";
import cloneFLowWithParent from "../../utils/storeUtils";
import LoadingComponent from "../../components/loadingComponent";
import useFlowStore from "../../stores/flowStore";
import IOModal from "../../modals/IOModal";
export default function PlaygroundPage() {
const currentFlow = useFlowsManagerStore((state) => state.currentFlow);
const getFlowById = useFlowsManagerStore((state) => state.getFlowById);
const setCurrentFlowId = useFlowsManagerStore((state) => state.setCurrentFlowId);
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
const setCurrentFlow = useFlowsManagerStore((state) => state.setCurrentFlow);
const setNodes = useFlowStore((state) => state.setNodes);
const setEdges = useFlowStore((state) => state.setEdges);
const cleanFlowPool = useFlowStore((state) => state.CleanFlowPool);
const { id } = useParams();
const [loading, setLoading] = useState(true);
async function getFlowData() {
const res = await getComponent(id!);
const newFlow = cloneFLowWithParent(res, res.id, false, true);
return newFlow;
}
// Set flow tab id
useEffect(() => {
console.log("id", id);
if (getFlowById(id!)) {
setCurrentFlowId(id!);
}
else {
getFlowData().then((flow) => {
setCurrentFlow(flow);
});
}
}, [id]);
useEffect(() => {
if (currentFlow) {
setNodes(currentFlow?.data?.nodes ?? [], true);
setEdges(currentFlow?.data?.edges ?? [], true);
cleanFlowPool();
setLoading(false);
}
return () => {
setNodes([], true);
setEdges([], true);
cleanFlowPool();
};
}, [currentFlow]);
return (
<div className="w-full h-full flex flex-col align-middle items-center justify-center">
{loading ? <div><LoadingComponent remSize={24}></LoadingComponent></div> :
<IOModal open={true}setOpen={()=>{}} isPlayground>
<></>
</IOModal>}
</div>
)
}

View file

@ -9,7 +9,7 @@ import { SkeletonCardComponent } from "../../components/skeletonCardComponent";
import { Button } from "../../components/ui/button";
import { Input } from "../../components/ui/input";
import { Link, useParams } from "react-router-dom";
import { Link, useNavigate, useParams } from "react-router-dom";
import { TagsSelector } from "../../components/tagsSelectorComponent";
import { Badge } from "../../components/ui/badge";
import {
@ -65,6 +65,8 @@ export default function StorePage(): JSX.Element {
const [searchNow, setSearchNow] = useState("");
const [selectFilter, setSelectFilter] = useState("all");
const navigate = useNavigate();
useEffect(() => {
if (!loadingApiKey) {
if (!hasApiKey) {
@ -371,6 +373,10 @@ export default function StorePage(): JSX.Element {
data={item}
authorized={validApiKey}
disabled={loading}
playground={
item.last_tested_version?.includes("1.0.0") &&
!item.is_component
}
/>
</>
);

View file

@ -1,5 +1,4 @@
import { useEffect } from "react";
import { Navigate, Route, Routes, useNavigate } from "react-router-dom";
import { Navigate, Route, Routes } from "react-router-dom";
import { ProtectedAdminRoute } from "./components/authAdminGuard";
import { ProtectedRoute } from "./components/authGuard";
import { ProtectedLoginRoute } from "./components/authLoginGuard";
@ -20,15 +19,9 @@ import ViewPage from "./pages/ViewPage";
import DeleteAccountPage from "./pages/deleteAccountPage";
import LoginPage from "./pages/loginPage";
import SignUp from "./pages/signUpPage";
import PlaygroundPage from "./pages/Playground";
const Router = () => {
const navigate = useNavigate();
useEffect(() => {
// Redirect from root to /flows
if (window.location.pathname === "/") {
navigate("/flows");
}
}, [navigate]);
return (
<Routes>
<Route
@ -39,6 +32,7 @@ const Router = () => {
</ProtectedRoute>
}
>
<Route index element={<Navigate replace to={"flows"} />} />
<Route
path="flows"
element={<ComponentsComponent key="flows" is_component={false} />}
@ -81,7 +75,13 @@ const Router = () => {
</ProtectedRoute>
}
/>
<Route path="/playground/:id/">
element={
<Route path="" element={<ProtectedRoute>
<PlaygroundPage />
</ProtectedRoute>} />
}
</Route>
<Route path="/flow/:id/">
<Route
path=""

View file

@ -44,9 +44,12 @@ import { getInputsAndOutputs } from "../utils/storeUtils";
import useAlertStore from "./alertStore";
import { useDarkStore } from "./darkStore";
import useFlowsManagerStore from "./flowsManagerStore";
import FlowPage from "../pages/FlowPage";
// 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) => ({
onFlowPage: false,
setOnFlowPage:(FlowPage=>set({onFlowPage:FlowPage})),
flowState: undefined,
flowBuildStatus: {},
nodes: [],
@ -149,7 +152,7 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
edges: applyEdgeChanges(changes, get().edges),
});
},
setNodes: (change) => {
setNodes: (change,skipSave=false) => {
let newChange = typeof change === "function" ? change(get().nodes) : change;
let newEdges = cleanEdges(newChange, get().edges);
const { inputs, outputs } = getInputsAndOutputs(newChange);
@ -164,7 +167,7 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
});
const flowsManager = useFlowsManagerStore.getState();
if (!get().isBuilding) {
if (!get().isBuilding && !skipSave && get().onFlowPage) {
flowsManager.autoSaveCurrentFlow(
newChange,
newEdges,
@ -172,7 +175,7 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
);
}
},
setEdges: (change) => {
setEdges: (change,skipSave=false) => {
let newChange = typeof change === "function" ? change(get().edges) : change;
set({
edges: newChange,
@ -180,7 +183,7 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
});
const flowsManager = useFlowsManagerStore.getState();
if (!get().isBuilding) {
if (!get().isBuilding && !skipSave && get().onFlowPage) {
flowsManager.autoSaveCurrentFlow(
get().nodes,
newChange,
@ -478,8 +481,13 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
// const nextVertices will be the zip of vertexBuildData.next_vertices_ids and
// vertexBuildData.top_level_vertices
// the VertexLayerElementType as {id: next_vertices_id, layer: top_level_vertex}
// next_vertices_ids should be next_vertices_ids without the inactivated vertices
const next_vertices_ids = vertexBuildData.next_vertices_ids.filter(
(id) => !vertexBuildData.inactivated_vertices?.includes(id)
);
const nextVertices: VertexLayerElementType[] = zip(
vertexBuildData.next_vertices_ids,
next_vertices_ids,
vertexBuildData.top_level_vertices
).map(([id, reference]) => ({ id: id!, reference }));
@ -489,7 +497,7 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
];
const newIds = [
...get().verticesBuild!.verticesIds,
...vertexBuildData.next_vertices_ids,
...next_vertices_ids,
];
get().updateVerticesBuild({
verticesIds: newIds,
@ -560,6 +568,8 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
useFlowStore.getState().updateBuildStatus(idList, BuildStatus.BUILDING);
},
onValidateNodes: validateSubgraph,
nodes: !get().onFlowPage ? get().nodes : undefined,
edges: !get().onFlowPage ? get().edges : undefined,
});
get().setIsBuilding(false);
get().revertBuiltStatusFromBuilding();
@ -598,7 +608,10 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
set({
verticesBuild: {
...verticesBuild,
// remove the vertices from the list of vertices ids
// that are going to be built
verticesIds: get().verticesBuild!.verticesIds.filter(
// keep the vertices that are not in the list of vertices to remove
(vertex) => !vertices.includes(vertex)
),
},

View file

@ -47,6 +47,16 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
set({ examples });
},
currentFlowId: "",
setCurrentFlow: (flow: FlowType) => {
set((state) => ({
currentFlow: flow,
currentFlowId: flow.id,
}));
},
getFlowById: (id: string) => {
return get().flows.find((flow) => flow.id === id);
},
setCurrentFlowId: (currentFlowId: string) => {
set((state) => ({
currentFlowId,

View file

@ -323,7 +323,7 @@
muted-foreground is too strong, maybe use a lighter shade of it?
*/
@apply border-none ring ring-muted-foreground;
@apply border-none ring grayscale;
}
.built-invalid-status {
@apply border-none ring ring-[#FF9090];

View file

@ -68,3 +68,31 @@ select:-webkit-autofill:focus {
background-color: #bbb;
border-radius: 999px;
}
.json-view-playground-white-left {
background-color: #fff !important;
height: fit-content !important;
}
.json-view-playground-dark {
background-color: #141924 !important;
height: fit-content !important;
}
.json-view-playground-white {
background-color: #f8fafc !important;
height: fit-content !important;
}
.json-view-playground-dark-left {
background-color: #0c101a !important;
height: fit-content !important;
}
.json-view-white {
background-color: #f8fafc !important;
}
.json-view-dark {
background-color: #141924 !important;
}

View file

@ -72,15 +72,7 @@ export type InputListComponentType = {
disabled: boolean;
editNode?: boolean;
componentName?: string;
};
export type InputGlobalComponentType = {
disabled: boolean;
onChange: (value: string) => void;
setDb: (value: boolean) => void;
name: string;
data: NodeDataType;
editNode?: boolean;
playgroundDisabled?: boolean;
};
export type KeyPairListComponentType = {
@ -96,9 +88,11 @@ export type KeyPairListComponentType = {
export type DictComponentType = {
value: any;
onChange: (value) => void;
disabled: boolean;
disabled?: boolean;
editNode?: boolean;
id?: string;
left?: boolean;
output?: boolean;
};
export type TextAreaComponentType = {
@ -595,6 +589,8 @@ export type IOModalPropsType = {
open: boolean;
setOpen: (open: boolean) => void;
disable?: boolean;
isPlayground?: boolean;
cleanOnClose?: boolean;
};
export type buttonBoxPropsType = {

View file

@ -45,6 +45,8 @@ export type FlowPoolType = {
};
export type FlowStoreType = {
onFlowPage: boolean;
setOnFlowPage: (onFlowPage: boolean) => void;
flowPool: FlowPoolType;
inputs: Array<{ type: string; id: string; displayName: string }>;
outputs: Array<{ type: string; id: string; displayName: string }>;
@ -74,8 +76,8 @@ export type FlowStoreType = {
edges: Edge[];
onNodesChange: OnNodesChange;
onEdgesChange: OnEdgesChange;
setNodes: (update: Node[] | ((oldState: Node[]) => Node[])) => void;
setEdges: (update: Edge[] | ((oldState: Edge[]) => Edge[])) => void;
setNodes: (update: Node[] | ((oldState: Node[]) => Node[]),skipSave?:boolean) => void;
setEdges: (update: Edge[] | ((oldState: Edge[]) => Edge[]),skipSave?:boolean) => void;
setNode: (id: string, update: Node | ((oldState: Node) => Node)) => void;
getNode: (id: string) => Node | undefined;
deleteNode: (nodeId: string | Array<string>) => void;

View file

@ -2,6 +2,7 @@ import { Edge, Node, Viewport, XYPosition } from "reactflow";
import { FlowType } from "../../flow";
export type FlowsManagerStoreType = {
getFlowById: (id: string) => FlowType | undefined;
flows: Array<FlowType>;
setFlows: (flows: FlowType[]) => void;
currentFlow: FlowType | undefined;
@ -50,6 +51,7 @@ export type FlowsManagerStoreType = {
takeSnapshot: () => void;
examples: Array<FlowType>;
setExamples: (examples: FlowType[]) => void;
setCurrentFlow: (flow: FlowType) => void;
};
export type UseUndoRedoOptions = {

View file

@ -5,6 +5,7 @@ import useAlertStore from "../stores/alertStore";
import useFlowStore from "../stores/flowStore";
import { VertexBuildTypeAPI } from "../types/api";
import { VertexLayerElementType } from "../types/zustand/flow";
import { Edge, Node } from "reactflow";
type BuildVerticesParams = {
flowId: string; // Assuming FlowType is the type for your flow
@ -21,6 +22,8 @@ type BuildVerticesParams = {
onBuildError?: (title, list, idList: VertexLayerElementType[]) => void;
onBuildStart?: (idList: VertexLayerElementType[]) => void;
onValidateNodes?: (nodes: string[]) => void;
nodes?: Node[];
edges?: Edge[];
};
function getInactiveVertexData(vertexId: string): VertexBuildTypeAPI {
@ -48,7 +51,9 @@ function getInactiveVertexData(vertexId: string): VertexBuildTypeAPI {
export async function updateVerticesOrder(
flowId: string,
startNodeId?: string | null,
stopNodeId?: string | null
stopNodeId?: string | null,
nodes?:Node[],
edges?:Edge[]
): Promise<{
verticesLayers: VertexLayerElementType[][];
verticesIds: string[];
@ -59,7 +64,7 @@ export async function updateVerticesOrder(
const setErrorData = useAlertStore.getState().setErrorData;
let orderResponse;
try {
orderResponse = await getVerticesOrder(flowId, startNodeId, stopNodeId);
orderResponse = await getVerticesOrder(flowId, startNodeId, stopNodeId, nodes, edges);
} catch (error: any) {
setErrorData({
title: "Oops! Looks like you missed something",
@ -101,6 +106,8 @@ export async function buildVertices({
onBuildError,
onBuildStart,
onValidateNodes,
nodes,
edges,
}: BuildVerticesParams) {
let verticesBuild = useFlowStore.getState().verticesBuild;
// if startNodeId and stopNodeId are provided
@ -113,7 +120,9 @@ export async function buildVertices({
let verticesOrderResponse = await updateVerticesOrder(
flowId,
startNodeId,
stopNodeId
stopNodeId,
nodes,
edges
);
if (onValidateNodes) {
try {
@ -166,14 +175,26 @@ export async function buildVertices({
!useFlowStore
.getState()
.verticesBuild?.verticesIds.includes(element.id) &&
!useFlowStore
.getState()
.verticesBuild?.verticesIds.includes(element.reference ?? "") &&
onBuildUpdate
) {
// If it is, skip building and set the state to inactive
onBuildUpdate(
getInactiveVertexData(element.id),
BuildStatus.INACTIVE,
runId
);
if (element.id) {
onBuildUpdate(
getInactiveVertexData(element.id),
BuildStatus.INACTIVE,
runId
);
}
if (element.reference) {
onBuildUpdate(
getInactiveVertexData(element.reference),
BuildStatus.INACTIVE,
runId
);
}
buildResults.push(false);
return;
}

View file

@ -1,4 +1,4 @@
import { cloneDeep } from "lodash";
import { cloneDeep, uniqueId } from "lodash";
import { Node } from "reactflow";
import { FlowType, NodeDataType } from "../types/flow";
import { isInputNode, isOutputNode } from "./reactflowUtils";
@ -6,11 +6,17 @@ import { isInputNode, isOutputNode } from "./reactflowUtils";
export default function cloneFLowWithParent(
flow: FlowType,
parent: string,
is_component: boolean
is_component: boolean,
keepId=false
) {
let childFLow = cloneDeep(flow);
childFLow.parent = parent;
childFLow.id = "";
if(!keepId){
childFLow.id = "";
}
else{
childFLow.id = uniqueId()+"-"+childFLow.id;
}
childFLow.is_component = is_component;
return childFLow;
}

View file

@ -140,6 +140,7 @@ import {
X,
XCircle,
Zap,
PlaySquare
} from "lucide-react";
import { FaApple, FaGithub } from "react-icons/fa";
import { AWSIcon } from "../icons/AWS";
@ -148,7 +149,7 @@ import { AnthropicIcon } from "../icons/Anthropic";
import { AstraDBIcon } from "../icons/AstraDB";
import { AzureIcon } from "../icons/Azure";
import { BingIcon } from "../icons/Bing";
import { BotMessageSquareIcon } from "../icons/BotMessageSquare";
import { BotMessageSquareIcon} from "../icons/BotMessageSquare";
import { ChromaIcon } from "../icons/ChromaIcon";
import { CohereIcon } from "../icons/Cohere";
import { ElasticsearchIcon } from "../icons/ElasticsearchStore";
@ -296,7 +297,8 @@ export const nodeIconsLucide: iconsType = {
ListFlows: Group,
ClearMessageHistory: FileClock,
Python: PythonIcon,
ChatOutput: BotMessageSquareIcon,
ChatOutput: MessagesSquare,
BotMessageSquareIcon,
ChatInput: MessagesSquare,
inputs: Download,
outputs: Upload,

View file

@ -5,7 +5,10 @@
{
"id": "ChatOutput-xPeM1",
"type": "genericNode",
"position": { "x": 231.45405028405742, "y": -109.00715949940081 },
"position": {
"x": 231.45405028405742,
"y": -109.00715949940081
},
"data": {
"type": "ChatOutput",
"node": {
@ -17,7 +20,7 @@
"list": false,
"show": true,
"multiline": true,
"value": "from typing import Optional, Union\n\nfrom langflow.base.io.chat import ChatComponent\nfrom langflow.field_typing import Text\nfrom langflow.schema import Record\n\n\nclass ChatOutput(ChatComponent):\n display_name = \"Chat Output\"\n description = \"Display a chat message in the Interaction Panel.\"\n icon = \"ChatOutput\"\n\n def build(\n self,\n sender: Optional[str] = \"Machine\",\n sender_name: Optional[str] = \"AI\",\n input_value: Optional[str] = None,\n session_id: Optional[str] = None,\n return_record: Optional[bool] = False,\n record_template: Optional[str] = \"{text}\",\n ) -> Union[Text, Record]:\n return super().build_with_record(\n sender=sender,\n sender_name=sender_name,\n input_value=input_value,\n session_id=session_id,\n return_record=return_record,\n record_template=record_template or \"\",\n )\n",
"value": "from typing import Optional, Union\n\nfrom langflow.base.io.chat import ChatComponent\nfrom langflow.field_typing import Text\nfrom langflow.schema import Record\n\n\nclass ChatOutput(ChatComponent):\n display_name = \"Chat Output\"\n description = \"Display a chat message in the Playground.\"\n icon = \"ChatOutput\"\n\n def build(\n self,\n sender: Optional[str] = \"Machine\",\n sender_name: Optional[str] = \"AI\",\n input_value: Optional[str] = None,\n session_id: Optional[str] = None,\n return_record: Optional[bool] = False,\n record_template: Optional[str] = \"{text}\",\n ) -> Union[Text, Record]:\n return super().build_with_record(\n sender=sender,\n sender_name=sender_name,\n input_value=input_value,\n session_id=session_id,\n return_record=return_record,\n record_template=record_template or \"\",\n )\n",
"fileTypes": [],
"file_path": "",
"password": false,
@ -41,7 +44,9 @@
"name": "input_value",
"display_name": "Message",
"advanced": false,
"input_types": ["Text"],
"input_types": [
"Text"
],
"dynamic": false,
"info": "",
"load_from_db": false,
@ -65,7 +70,9 @@
"info": "In case of Message being a Record, this template will be used to convert it to text.",
"load_from_db": false,
"title_case": false,
"input_types": ["Text"]
"input_types": [
"Text"
]
},
"return_record": {
"type": "bool",
@ -97,7 +104,10 @@
"fileTypes": [],
"file_path": "",
"password": false,
"options": ["Machine", "User"],
"options": [
"Machine",
"User"
],
"name": "sender",
"display_name": "Sender Type",
"advanced": true,
@ -105,7 +115,9 @@
"info": "",
"load_from_db": false,
"title_case": false,
"input_types": ["Text"]
"input_types": [
"Text"
]
},
"sender_name": {
"type": "str",
@ -125,7 +137,9 @@
"info": "",
"load_from_db": false,
"title_case": false,
"input_types": ["Text"]
"input_types": [
"Text"
]
},
"session_id": {
"type": "str",
@ -144,13 +158,20 @@
"info": "If provided, the message will be stored in the memory.",
"load_from_db": false,
"title_case": false,
"input_types": ["Text"]
"input_types": [
"Text"
]
},
"_type": "CustomComponent"
},
"description": "Display a chat message in the Interaction Panel.",
"description": "Display a chat message in the Playground.",
"icon": "ChatOutput",
"base_classes": ["object", "Record", "str", "Text"],
"base_classes": [
"object",
"Record",
"str",
"Text"
],
"display_name": "Chat Output",
"documentation": "",
"custom_fields": {
@ -161,7 +182,10 @@
"return_record": null,
"record_template": null
},
"output_types": ["Text", "Record"],
"output_types": [
"Text",
"Record"
],
"field_formatters": {},
"frozen": false,
"field_order": [],
@ -181,7 +205,10 @@
{
"id": "ChatInput-XYvUc",
"type": "genericNode",
"position": { "x": -389.67919096408036, "y": 10.79598792234681 },
"position": {
"x": -389.67919096408036,
"y": 10.79598792234681
},
"data": {
"type": "ChatInput",
"node": {
@ -193,7 +220,7 @@
"list": false,
"show": true,
"multiline": true,
"value": "from typing import Optional, Union\n\nfrom langflow.base.io.chat import ChatComponent\nfrom langflow.field_typing import Text\nfrom langflow.schema import Record\n\n\nclass ChatInput(ChatComponent):\n display_name = \"Chat Input\"\n description = \"Get chat inputs from the Interaction Panel.\"\n icon = \"ChatInput\"\n\n def build_config(self):\n build_config = super().build_config()\n build_config[\"input_value\"] = {\n \"input_types\": [],\n \"display_name\": \"Message\",\n \"multiline\": True,\n }\n\n return build_config\n\n def build(\n self,\n sender: Optional[str] = \"User\",\n sender_name: Optional[str] = \"User\",\n input_value: Optional[str] = None,\n session_id: Optional[str] = None,\n return_record: Optional[bool] = False,\n ) -> Union[Text, Record]:\n return super().build_no_record(\n sender=sender,\n sender_name=sender_name,\n input_value=input_value,\n session_id=session_id,\n return_record=return_record,\n )\n",
"value": "from typing import Optional, Union\n\nfrom langflow.base.io.chat import ChatComponent\nfrom langflow.field_typing import Text\nfrom langflow.schema import Record\n\n\nclass ChatInput(ChatComponent):\n display_name = \"Chat Input\"\n description = \"Get chat inputs from the Playground.\"\n icon = \"ChatInput\"\n\n def build_config(self):\n build_config = super().build_config()\n build_config[\"input_value\"] = {\n \"input_types\": [],\n \"display_name\": \"Message\",\n \"multiline\": True,\n }\n\n return build_config\n\n def build(\n self,\n sender: Optional[str] = \"User\",\n sender_name: Optional[str] = \"User\",\n input_value: Optional[str] = None,\n session_id: Optional[str] = None,\n return_record: Optional[bool] = False,\n ) -> Union[Text, Record]:\n return super().build_no_record(\n sender=sender,\n sender_name=sender_name,\n input_value=input_value,\n session_id=session_id,\n return_record=return_record,\n )\n",
"fileTypes": [],
"file_path": "",
"password": false,
@ -253,7 +280,10 @@
"fileTypes": [],
"file_path": "",
"password": false,
"options": ["Machine", "User"],
"options": [
"Machine",
"User"
],
"name": "sender",
"display_name": "Sender Type",
"advanced": true,
@ -261,7 +291,9 @@
"info": "",
"load_from_db": false,
"title_case": false,
"input_types": ["Text"]
"input_types": [
"Text"
]
},
"sender_name": {
"type": "str",
@ -281,7 +313,9 @@
"info": "",
"load_from_db": false,
"title_case": false,
"input_types": ["Text"]
"input_types": [
"Text"
]
},
"session_id": {
"type": "str",
@ -300,13 +334,20 @@
"info": "If provided, the message will be stored in the memory.",
"load_from_db": false,
"title_case": false,
"input_types": ["Text"]
"input_types": [
"Text"
]
},
"_type": "CustomComponent"
},
"description": "Get chat inputs from the Interaction Panel.",
"description": "Get chat inputs from the Playground.",
"icon": "ChatInput",
"base_classes": ["object", "Record", "str", "Text"],
"base_classes": [
"object",
"Record",
"str",
"Text"
],
"display_name": "Chat Input",
"documentation": "",
"custom_fields": {
@ -316,7 +357,10 @@
"session_id": null,
"return_record": null
},
"output_types": ["Text", "Record"],
"output_types": [
"Text",
"Record"
],
"field_formatters": {},
"frozen": false,
"field_order": [],
@ -339,16 +383,25 @@
"targetHandle": {
"fieldName": "input_value",
"id": "ChatOutput-xPeM1",
"inputTypes": ["Text"],
"inputTypes": [
"Text"
],
"type": "str"
},
"sourceHandle": {
"baseClasses": ["object", "Record", "str", "Text"],
"baseClasses": [
"object",
"Record",
"str",
"Text"
],
"dataType": "ChatInput",
"id": "ChatInput-XYvUc"
}
},
"style": { "stroke": "#555" },
"style": {
"stroke": "#555"
},
"className": "stroke-gray-900 stroke-connection",
"id": "reactflow__edge-ChatInput-XYvUc{œbaseClassesœ:[œobjectœ,œRecordœ,œstrœ,œTextœ],œdataTypeœ:œChatInputœ,œidœ:œChatInput-XYvUcœ}-ChatOutput-xPeM1{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-xPeM1œ,œinputTypesœ:[œTextœ],œtypeœ:œstrœ}"
}
@ -363,4 +416,4 @@
"name": "ChatTest",
"last_tested_version": "1.0.0a14",
"is_component": false
}
}