feat: changed tweaks into input schema and enabled persistence (#8235)

* Fixed layground and share font weight

* Fixed namings and deleted unused api files

* Deleted unused code

* Changed tweaks to input schema

* Changed TableAdvancedToggleCellRender to change Tweaks instead of Nodes if in tweaks page

* Removed Hide Visibility to show tweaks' Enable Input

* Set advanced of all nodes as true in tweaks

* Changed column defs to change width and pass isTweaks to advanced

* Changed tweaks store to identify changes in the Advanced field instead of the Values field

* Changed to not change quantity of hooks

* Changed table node cell render to not change the value of the tweaks

* Changed to disable inputs

* Added disabled behavior to table node cell render

* Added cursor default to cell classes

* Updated tweaks test

* Added persistence in session

* Removed initial setup from api modal to enable persistence

* Added persistence to storage

* Changed test to test persistence of tweaks

* Re-added initial setup to api modal

* Fixed size of tweaks modal
This commit is contained in:
Lucas Oliveira 2025-05-27 17:38:19 -03:00 • committed by GitHub
commit d88abdd658
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 319 additions and 957 deletions

View file

@ -4,7 +4,7 @@ import Markdown from "react-markdown";
import rehypeMathjax from "rehype-mathjax";
import remarkGfm from "remark-gfm";
import ForwardedIconComponent from "../../common/genericIconComponent";
import SimplifiedCodeTabComponent from "../codeTabsComponent/ChatCodeTabComponent";
import SimplifiedCodeTabComponent from "../codeTabsComponent";
import DurationDisplay from "./DurationDisplay";
export default function ContentDisplay({

View file

@ -1,62 +0,0 @@
import { useState } from "react";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { tomorrow } from "react-syntax-highlighter/dist/cjs/styles/prism";
import IconComponent from "../../common/genericIconComponent";
import { Button } from "../../ui/button";
type SimplifiedCodeTabProps = {
code: string;
language: string;
};
export default function SimplifiedCodeTabComponent({
code,
language,
}: SimplifiedCodeTabProps) {
const [isCopied, setIsCopied] = useState<boolean>(false);
const copyToClipboard = () => {
if (!navigator.clipboard || !navigator.clipboard.writeText) {
return;
}
navigator.clipboard.writeText(code).then(() => {
setIsCopied(true);
setTimeout(() => {
setIsCopied(false);
}, 2000);
});
};
return (
<div
className="mt-2 flex w-full flex-col overflow-hidden rounded-md text-left dark"
data-testid="chat-code-tab"
>
<div className="flex w-full items-center justify-between rounded-t-md border border-b-0 border-border bg-muted px-4 py-2">
<span className="dar text-sm font-semibold text-white">{language}</span>
<Button
variant="ghost"
size="icon"
className="text-muted-foreground hover:bg-card"
data-testid="copy-code-button"
onClick={copyToClipboard}
>
{isCopied ? (
<IconComponent name="Check" className="h-4 w-4" />
) : (
<IconComponent name="Copy" className="h-4 w-4" />
)}
</Button>
</div>
<SyntaxHighlighter
language={language.toLowerCase()}
style={tomorrow}
className="!mt-0 h-full w-full overflow-scroll !rounded-b-md !rounded-t-none border border-border text-left !custom-scroll"
>
{code}
</SyntaxHighlighter>
</div>
);
}

View file

@ -37,7 +37,6 @@ export function TweakComponent({
<EditNodeComponent
open={open}
autoHeight
hideVisibility
nodeClass={nodeClass}
isTweaks
nodeId={node.data.id}

View file

@ -1,36 +1,26 @@
import ShadTooltip from "@/components/common/shadTooltipComponent";
import { useTweaksStore } from "@/stores/tweaksStore";
import { useState } from "react";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { oneDark } from "react-syntax-highlighter/dist/cjs/styles/prism";
import { useDarkStore } from "../../../stores/darkStore";
import { codeTabsPropsType } from "../../../types/components";
import { tomorrow } from "react-syntax-highlighter/dist/cjs/styles/prism";
import IconComponent from "../../common/genericIconComponent";
import { Button } from "../../ui/button";
import { Label } from "../../ui/label";
import { Switch } from "../../ui/switch";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../ui/tabs";
import { TweaksComponent } from "./components/tweaksComponent";
export default function CodeTabsComponent({
open,
tabs,
activeTab,
setActiveTab,
isMessage,
setActiveTweaks,
activeTweaks,
}: codeTabsPropsType) {
const [isCopied, setIsCopied] = useState<Boolean>(false);
const dark = useDarkStore((state) => state.dark);
const nodes = useTweaksStore((state) => state.nodes);
type SimplifiedCodeTabProps = {
code: string;
language: string;
};
export default function SimplifiedCodeTabComponent({
code,
language,
}: SimplifiedCodeTabProps) {
const [isCopied, setIsCopied] = useState<boolean>(false);
const copyToClipboard = () => {
if (!navigator.clipboard || !navigator.clipboard.writeText) {
return;
}
navigator.clipboard.writeText(tabs[activeTab].code).then(() => {
navigator.clipboard.writeText(code).then(() => {
setIsCopied(true);
setTimeout(() => {
@ -40,121 +30,33 @@ export default function CodeTabsComponent({
};
return (
<Tabs
value={activeTab}
className={
"api-modal-tabs inset-0 m-0 " +
(isMessage ? "dark" : "") +
(dark && isMessage ? "bg-background" : "")
}
onValueChange={(value) => {
setActiveTab(value);
}}
<div
className="mt-2 flex w-full flex-col overflow-hidden rounded-md text-left dark"
data-testid="chat-code-tab"
>
<div className="api-modal-tablist-div">
{tabs.length > 0 && tabs[0].name !== "" ? (
<TabsList className="mt-[-1px] border-b">
{tabs.map((tab, index) => (
<TabsTrigger
className={
isMessage ? "data-[state=active]:bg-primary-foreground" : ""
}
key={index}
value={index.toString()}
>
{tab.name}
</TabsTrigger>
))}
</TabsList>
) : (
<div></div>
)}
</div>
{tabs.map((tab, idx) => (
<TabsContent
value={idx.toString()}
className="api-modal-tabs-content overflow-hidden dark"
key={idx} // Remember to add a unique key prop
<div className="flex w-full items-center justify-between rounded-t-md border border-b-0 border-border bg-muted px-4 py-2">
<span className="dar text-sm font-semibold text-white">{language}</span>
<Button
variant="ghost"
size="icon"
className="text-muted-foreground hover:bg-card"
data-testid="copy-code-button"
onClick={copyToClipboard}
>
{tab.name.toLowerCase() !== "tweaks" ? (
<div className="mt-2 flex h-full w-full flex-col">
{tab.description && (
<div
className="mb-2 w-full text-left text-sm"
dangerouslySetInnerHTML={{ __html: tab.description }}
></div>
)}
<div className="flex w-full items-center justify-end gap-4 rounded-t-md border border-border bg-muted px-4 py-2">
{nodes.length > 0 &&
tabs.find((tab) => tab.name.toLowerCase() === "tweaks") &&
tabs[activeTab].hasTweaks && (
<div className="flex items-center gap-2">
<ShadTooltip content="Makes temporary adjustments managed in 'Tweaks'">
<div className="flex items-center gap-2">
<Label
className={"text-xs font-medium text-white"}
htmlFor="tweaks-switch"
>
Enable Tweaks
</Label>
<IconComponent
name="info"
className="h-3.5 w-3.5 text-placeholder-foreground"
/>
</div>
</ShadTooltip>
<Switch
style={{
transform: `scaleX(${0.85}) scaleY(${0.85})`,
}}
id="tweaks-switch"
onCheckedChange={setActiveTweaks}
checked={activeTweaks}
autoFocus={false}
/>
<span className="text-lg text-accent">|</span>
</div>
)}
{tabs[activeTab].name.toLowerCase !== "tweaks" && (
<>
<Button
variant="ghost"
size="icon"
onClick={copyToClipboard}
data-testid="btn-copy-code"
>
{isCopied ? (
<IconComponent
name="Check"
className="h-4 w-4 text-muted-foreground"
/>
) : (
<IconComponent
name="Copy"
className="h-4 w-4 text-muted-foreground"
/>
)}
</Button>
</>
)}
</div>
<SyntaxHighlighter
language={tab.language}
style={oneDark}
className="!my-0 h-full overflow-auto rounded-sm !rounded-t-none border border-t-0 border-border text-left custom-scroll"
>
{tab.code}
</SyntaxHighlighter>
</div>
) : tab.name.toLowerCase() === "tweaks" ? (
<>
<TweaksComponent open={open ?? false} />
</>
) : null}
</TabsContent>
))}
</Tabs>
{isCopied ? (
<IconComponent name="Check" className="h-4 w-4" />
) : (
<IconComponent name="Copy" className="h-4 w-4" />
)}
</Button>
</div>
<SyntaxHighlighter
language={language.toLowerCase()}
style={tomorrow}
className="!mt-0 h-full w-full overflow-scroll !rounded-b-md !rounded-t-none border border-border text-left !custom-scroll"
>
{code}
</SyntaxHighlighter>
</div>
);
}

View file

@ -12,7 +12,7 @@ import { usePatchUpdateFlow } from "@/controllers/API/queries/flows/use-patch-up
import { CustomLink } from "@/customization/components/custom-link";
import { ENABLE_PUBLISH, ENABLE_WIDGET } from "@/customization/feature-flags";
import { customMcpOpen } from "@/customization/utils/custom-mcp-open";
import ApiModal from "@/modals/apiModal/new-api-modal";
import ApiModal from "@/modals/apiModal";
import EmbedModal from "@/modals/EmbedModal/embed-modal";
import useAlertStore from "@/stores/alertStore";
import useAuthStore from "@/stores/authStore";
@ -87,7 +87,7 @@ export default function PublishDropdown() {
<Button
variant="ghost"
size="md"
className="!px-2.5 font-medium"
className="!px-2.5 font-normal"
data-testid="publish-button"
>
Share

View file

@ -1,18 +1,23 @@
import useHandleOnNewValue from "@/CustomNodes/hooks/use-handle-new-value";
import ShadTooltip from "@/components/common/shadTooltipComponent";
import useFlowStore from "@/stores/flowStore";
import { useTweaksStore } from "@/stores/tweaksStore";
import { APIClassType } from "@/types/api";
import { isTargetHandleConnected } from "@/utils/reactflowUtils";
import { CustomCellRendererProps } from "ag-grid-react";
import ToggleShadComponent from "../../../toggleShadComponent";
export default function TableAdvancedToggleCellRender({
value: { nodeId, parameterId },
value: { nodeId, parameterId, isTweaks },
}: CustomCellRendererProps) {
const edges = useFlowStore((state) => state.edges);
const node = useFlowStore((state) => state.getNode(nodeId));
const node = isTweaks
? useTweaksStore((state) => state.getNode(nodeId))
: useFlowStore((state) => state.getNode(nodeId));
const parameter = node?.data?.node?.template?.[parameterId];
const setNode = useTweaksStore((state) => state.setNode);
const disabled = isTargetHandleConnected(
edges,
parameterId,
@ -24,6 +29,7 @@ export default function TableAdvancedToggleCellRender({
node: node?.data.node as APIClassType,
nodeId,
name: parameterId,
setNode: isTweaks ? setNode : undefined,
});
return (
@ -31,22 +37,24 @@ export default function TableAdvancedToggleCellRender({
<ShadTooltip
content={
disabled
? "Cannot change visibility of connected handles"
: "Change visibility of the field"
? isTweaks
? "Cannot enable input of connected handles"
: "Cannot change visibility of connected handles"
: isTweaks
? "Toggle input of the field in the API"
: "Change visibility of the field"
}
styleClasses="z-50"
>
<div>
<div className="flex h-full items-center">
<ToggleShadComponent
disabled={disabled}
value={!parameter.advanced}
handleOnNewValue={handleOnNewValue}
editNode={true}
showToogle
id={"show" + parameterId}
/>
</div>
<div className="flex h-full w-full items-center justify-center">
<ToggleShadComponent
disabled={disabled}
value={!parameter.advanced}
handleOnNewValue={handleOnNewValue}
editNode={true}
showToogle
id={"show" + parameterId}
/>
</div>
</ShadTooltip>
)

View file

@ -4,9 +4,9 @@ import { ParameterRenderComponent } from "@/components/core/parameterRenderCompo
import { NodeInfoType } from "@/components/core/parameterRenderComponent/types";
import useAuthStore from "@/stores/authStore";
import useFlowStore from "@/stores/flowStore";
import { useTweaksStore } from "@/stores/tweaksStore";
import { APIClassType } from "@/types/api";
import { isTargetHandleConnected } from "@/utils/reactflowUtils";
import { cn } from "@/utils/utils";
import { CustomCellRendererProps } from "ag-grid-react";
import { useMemo } from "react";
@ -14,15 +14,11 @@ export default function TableNodeCellRender({
value: { nodeId, parameterId, isTweaks },
}: CustomCellRendererProps) {
const edges = useFlowStore((state) => state.edges);
const node = isTweaks
? useTweaksStore((state) => state.getNode(nodeId))
: useFlowStore((state) => state.getNode(nodeId));
const node = useFlowStore((state) => state.getNode(nodeId));
const parameter = node?.data?.node?.template?.[parameterId];
const currentFlow = useFlowStore((state) => state.currentFlow);
const isAuth = useAuthStore((state) => state.isAuthenticated);
const setNode = useTweaksStore((state) => state.setNode);
const disabled = isTargetHandleConnected(
edges,
parameterId,
@ -34,12 +30,12 @@ export default function TableNodeCellRender({
node: node?.data.node as APIClassType,
nodeId,
name: parameterId,
setNode: isTweaks ? setNode : undefined,
setNode: isTweaks ? () => {} : undefined,
});
const { handleNodeClass } = useHandleNodeClass(
nodeId,
isTweaks ? setNode : undefined,
isTweaks ? () => {} : undefined,
);
const nodeInformationMetadata: NodeInfoType = useMemo(() => {
@ -54,7 +50,12 @@ export default function TableNodeCellRender({
return (
parameter && (
<div className="group mx-auto flex h-full max-h-48 w-[300px] items-center justify-center overflow-auto px-1 py-2.5 custom-scroll">
<div
className={cn(
"group mx-auto flex h-full max-h-48 w-[300px] items-center justify-center overflow-auto px-1 py-2.5 custom-scroll",
isTweaks && "pointer-events-none opacity-70",
)}
>
<ParameterRenderComponent
nodeId={nodeId}
handleOnNewValue={handleOnNewValue}

View file

@ -1,19 +0,0 @@
import {
getCurlRunCode,
getCurlWebhookCode,
} from "@/modals/apiModal/utils/get-curl-code";
import getJsApiCode from "@/modals/apiModal/utils/get-js-api-code";
import getPythonApiCode from "@/modals/apiModal/utils/get-python-api-code";
import getPythonCode from "@/modals/apiModal/utils/get-python-code";
import getWidgetCode from "@/modals/apiModal/utils/get-widget-code";
export function useCustomAPICode() {
return {
getCurlRunCode,
getCurlWebhookCode,
getJsApiCode,
getPythonApiCode,
getPythonCode,
getWidgetCode,
};
}

View file

@ -4,7 +4,7 @@ import { cn } from "@/utils/utils";
import { AnimatePresence, motion } from "framer-motion";
import Markdown from "react-markdown";
import remarkGfm from "remark-gfm";
import CodeTabsComponent from "../../../../../../components/core/codeTabsComponent/ChatCodeTabComponent";
import CodeTabsComponent from "../../../../../../components/core/codeTabsComponent";
import LogoIcon from "./chat-logo-icon";
export const ErrorView = ({

View file

@ -4,7 +4,7 @@ import Markdown from "react-markdown";
import rehypeMathjax from "rehype-mathjax";
import rehypeRaw from "rehype-raw";
import remarkGfm from "remark-gfm";
import CodeTabsComponent from "../../../../../../components/core/codeTabsComponent/ChatCodeTabComponent";
import CodeTabsComponent from "../../../../../../components/core/codeTabsComponent";
type MarkdownFieldProps = {
chat: any;

View file

@ -1,73 +1,226 @@
import { TweaksComponent } from "@/components/core/codeTabsComponent/components/tweaksComponent";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator";
import { CustomAPIGenerator } from "@/customization/components/custom-api-generator";
import { useCustomAPICode } from "@/customization/hooks/use-custom-api-code";
import useSaveFlow from "@/hooks/flows/use-save-flow";
import useAuthStore from "@/stores/authStore";
import useFlowsManagerStore from "@/stores/flowsManagerStore";
import useFlowStore from "@/stores/flowStore";
import { isEndpointNameValid } from "@/utils/utils";
import "ace-builds/src-noconflict/ext-language_tools";
import "ace-builds/src-noconflict/mode-python";
import "ace-builds/src-noconflict/theme-github";
import "ace-builds/src-noconflict/theme-twilight";
import { ReactNode, useEffect, useState } from "react";
import { cloneDeep } from "lodash";
import { ChangeEvent, ReactNode, useEffect, useState } from "react";
import { useShallow } from "zustand/react/shallow";
import IconComponent from "../../components/common/genericIconComponent";
import CodeTabsComponent from "../../components/core/codeTabsComponent";
import { EXPORT_CODE_DIALOG } from "../../constants/constants";
import { useTweaksStore } from "../../stores/tweaksStore";
import { FlowType } from "../../types/flow/index";
import BaseModal from "../baseModal";
import APITabsComponent from "./codeTabs/code-tabs";
const MAX_LENGTH = 20;
const MIN_LENGTH = 1;
export default function ApiModal({
flow,
children,
open: myOpen,
setOpen: mySetOpen,
}: {
flow: FlowType;
children: ReactNode;
open?: boolean;
setOpen?: (a: boolean | ((o?: boolean) => boolean)) => void;
}) {
const autoLogin = useAuthStore((state) => state.autoLogin);
const nodes = useFlowStore((state) => state.nodes);
const [openTweaks, setOpenTweaks] = useState(false);
const tweaks = useTweaksStore((state) => state.tweaks);
const [open, setOpen] =
mySetOpen !== undefined && myOpen !== undefined
? [myOpen, mySetOpen]
: useState(false);
const [activeTab, setActiveTab] = useState("0");
const activeTweaks = useTweaksStore((state) => state.activeTweaks);
const setActiveTweaks = useTweaksStore((state) => state.setActiveTweaks);
const tabs = useTweaksStore((state) => state.tabs);
const initialSetup = useTweaksStore((state) => state.initialSetup);
const getCodes = useCustomAPICode();
const flowEndpointName = useFlowStore(
useShallow((state) => state.currentFlow?.endpoint_name),
);
const currentFlowId = useFlowStore(
useShallow((state) => state.currentFlow?.id),
);
const [endpointName, setEndpointName] = useState(flowEndpointName ?? "");
const [validEndpointName, setValidEndpointName] = useState(true);
const handleEndpointNameChange = (event: ChangeEvent<HTMLInputElement>) => {
const { value } = event.target;
// Validate the endpoint name
// use this regex r'^[a-zA-Z0-9_-]+$'
const isValid = isEndpointNameValid(event.target.value, MAX_LENGTH);
setValidEndpointName(isValid);
// Only update if valid and meets minimum length (if set)
if (isValid && value.length >= MIN_LENGTH) {
setEndpointName!(value);
} else if (value.length === 0) {
// Always allow empty endpoint name (it's optional)
setEndpointName!("");
}
};
useEffect(() => {
if (open) initialSetup(autoLogin ?? false, flow, getCodes);
setActiveTab("0");
if (open && currentFlowId) initialSetup(nodes, currentFlowId);
}, [open]);
const autoSaving = useFlowsManagerStore((state) => state.autoSaving);
const saveFlow = useSaveFlow();
const setCurrentFlow = useFlowStore((state) => state.setCurrentFlow);
function handleSave(): void {
const newFlow = cloneDeep(useFlowStore.getState().currentFlow);
if (!newFlow) return;
newFlow.endpoint_name =
endpointName && endpointName.length > 0 ? endpointName : null;
if (autoSaving) {
saveFlow(newFlow);
} else {
setCurrentFlow(newFlow);
}
}
useEffect(() => {
if (!openTweaks && endpointName !== flowEndpointName) handleSave();
else if (openTweaks) {
setEndpointName(flowEndpointName ?? "");
}
}, [openTweaks]);
return (
<BaseModal open={open} setOpen={setOpen} size="x-large">
<BaseModal.Trigger asChild>{children}</BaseModal.Trigger>
<BaseModal.Header description={EXPORT_CODE_DIALOG}>
<span className="pr-2">API</span>
<IconComponent
name="Code2"
className="h-6 w-6 pl-1 text-gray-800 dark:text-white"
aria-hidden="true"
/>
</BaseModal.Header>
<BaseModal.Content overflowHidden>
{open && (
<>
<CustomAPIGenerator isOpen={open} />
<CodeTabsComponent
open={open}
tabs={tabs!}
activeTab={activeTab}
setActiveTab={setActiveTab}
activeTweaks={activeTweaks}
setActiveTweaks={setActiveTweaks}
/>
</>
)}
</BaseModal.Content>
</BaseModal>
<>
<BaseModal
closeButtonClassName="!top-3"
open={open}
setOpen={setOpen}
size="medium"
className="pt-4"
>
<BaseModal.Trigger asChild>{children}</BaseModal.Trigger>
<BaseModal.Header
description={
autoLogin ? undefined : (
<>
<span className="pr-2">
API access requires an API key. You can{" "}
<a
href="/settings/api-keys"
className="text-accent-pink-foreground"
>
{" "}
create an API key
</a>{" "}
in settings.
</span>
</>
)
}
>
<IconComponent
name="Code2"
className="h-6 w-6 text-gray-800 dark:text-white"
aria-hidden="true"
/>
<span className="pl-2">API access</span>
{nodes.length > 0 && (
<div className="border-r-1 absolute right-12 flex items-center text-mmd font-medium leading-[16px]">
<Button
variant="ghost"
size="icon"
className="h-8 px-3"
onClick={() => setOpenTweaks(true)}
data-testid="tweaks-button"
>
<IconComponent
name="SlidersHorizontal"
className="h-3.5 w-3.5"
/>
<span>Input Schema ({Object.keys(tweaks)?.length}) </span>
</Button>
<Separator orientation="vertical" className="ml-2 h-8" />
</div>
)}
</BaseModal.Header>
<BaseModal.Content overflowHidden>
{open && (
<>
<CustomAPIGenerator isOpen={open} />
<APITabsComponent />
</>
)}
</BaseModal.Content>
</BaseModal>
<BaseModal
open={openTweaks}
setOpen={setOpenTweaks}
size="medium-small-tall"
>
<BaseModal.Header
description={
autoLogin ? undefined : (
<>
<span className="pr-2">
API access requires an API key. You can{" "}
<a
href="/settings/api-keys"
className="text-accent-pink-foreground"
>
{" "}
create an API key
</a>{" "}
in settings.
</span>
</>
)
}
>
<IconComponent name="SlidersHorizontal" className="text-f h-6 w-6" />
<span className="pl-2">Input Schema</span>
</BaseModal.Header>
<BaseModal.Content overflowHidden className="flex flex-col gap-4">
{true && (
<Label>
<div className="edit-flow-arrangement mt-2">
<span className="shrink-0 text-mmd font-medium">
Endpoint Name
</span>
{!validEndpointName && (
<span className="edit-flow-span">
Use only letters, numbers, hyphens, and underscores (
{MAX_LENGTH} characters max).
</span>
)}
</div>
<Input
className="nopan nodelete nodrag noflow mt-2 font-normal"
onChange={handleEndpointNameChange}
type="text"
name="endpoint_name"
value={endpointName ?? ""}
placeholder="An alternative name to run the endpoint"
maxLength={MAX_LENGTH}
minLength={MIN_LENGTH}
id="endpoint_name"
/>
</Label>
)}
<div className="h-full w-full overflow-y-auto overflow-x-hidden rounded-lg bg-muted custom-scroll">
<TweaksComponent open={openTweaks} />
</div>
</BaseModal.Content>
</BaseModal>
</>
);
}

View file

@ -1,225 +0,0 @@
import { TweaksComponent } from "@/components/core/codeTabsComponent/components/tweaksComponent";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator";
import { CustomAPIGenerator } from "@/customization/components/custom-api-generator";
import useSaveFlow from "@/hooks/flows/use-save-flow";
import useAuthStore from "@/stores/authStore";
import useFlowsManagerStore from "@/stores/flowsManagerStore";
import useFlowStore from "@/stores/flowStore";
import { isEndpointNameValid } from "@/utils/utils";
import "ace-builds/src-noconflict/ext-language_tools";
import "ace-builds/src-noconflict/mode-python";
import "ace-builds/src-noconflict/theme-github";
import "ace-builds/src-noconflict/theme-twilight";
import { cloneDeep } from "lodash";
import { ChangeEvent, ReactNode, useEffect, useState } from "react";
import { useShallow } from "zustand/react/shallow";
import IconComponent from "../../components/common/genericIconComponent";
import { useTweaksStore } from "../../stores/tweaksStore";
import BaseModal from "../baseModal";
import APITabsComponent from "./codeTabs/code-tabs";
const MAX_LENGTH = 20;
const MIN_LENGTH = 1;
export default function ApiModal({
children,
open: myOpen,
setOpen: mySetOpen,
}: {
children: ReactNode;
open?: boolean;
setOpen?: (a: boolean | ((o?: boolean) => boolean)) => void;
}) {
const autoLogin = useAuthStore((state) => state.autoLogin);
const nodes = useFlowStore((state) => state.nodes);
const [openTweaks, setOpenTweaks] = useState(false);
const tweaks = useTweaksStore((state) => state.tweaks);
const [open, setOpen] =
mySetOpen !== undefined && myOpen !== undefined
? [myOpen, mySetOpen]
: useState(false);
const newInitialSetup = useTweaksStore((state) => state.newInitialSetup);
const flowEndpointName = useFlowStore(
useShallow((state) => state.currentFlow?.endpoint_name),
);
const [endpointName, setEndpointName] = useState(flowEndpointName ?? "");
const [validEndpointName, setValidEndpointName] = useState(true);
const handleEndpointNameChange = (event: ChangeEvent<HTMLInputElement>) => {
const { value } = event.target;
// Validate the endpoint name
// use this regex r'^[a-zA-Z0-9_-]+$'
const isValid = isEndpointNameValid(event.target.value, MAX_LENGTH);
setValidEndpointName(isValid);
// Only update if valid and meets minimum length (if set)
if (isValid && value.length >= MIN_LENGTH) {
setEndpointName!(value);
} else if (value.length === 0) {
// Always allow empty endpoint name (it's optional)
setEndpointName!("");
}
};
useEffect(() => {
if (open) newInitialSetup(nodes);
}, [open]);
const autoSaving = useFlowsManagerStore((state) => state.autoSaving);
const saveFlow = useSaveFlow();
const setCurrentFlow = useFlowStore((state) => state.setCurrentFlow);
function handleSave(): void {
const newFlow = cloneDeep(useFlowStore.getState().currentFlow);
if (!newFlow) return;
newFlow.endpoint_name =
endpointName && endpointName.length > 0 ? endpointName : null;
if (autoSaving) {
saveFlow(newFlow);
} else {
setCurrentFlow(newFlow);
}
}
useEffect(() => {
if (!openTweaks && endpointName !== flowEndpointName) handleSave();
else if (openTweaks) {
setEndpointName(flowEndpointName ?? "");
}
}, [openTweaks]);
return (
<>
<BaseModal
closeButtonClassName="!top-3"
open={open}
setOpen={setOpen}
size="medium"
className="pt-4"
>
<BaseModal.Trigger asChild>{children}</BaseModal.Trigger>
<BaseModal.Header
description={
autoLogin ? undefined : (
<>
<span className="pr-2">
API access requires an API key. You can{" "}
<a
href="/settings/api-keys"
className="text-accent-pink-foreground"
>
{" "}
create an API key
</a>{" "}
in settings.
</span>
</>
)
}
>
<IconComponent
name="Code2"
className="h-6 w-6 text-gray-800 dark:text-white"
aria-hidden="true"
/>
<span className="pl-2">API access</span>
{nodes.length > 0 && (
<div className="border-r-1 absolute right-12 flex items-center text-mmd font-medium leading-[16px]">
<Button
variant="ghost"
size="icon"
className="h-8 px-3"
onClick={() => setOpenTweaks(true)}
data-testid="tweaks-button"
>
<IconComponent
name="SlidersHorizontal"
className="h-3.5 w-3.5"
/>
<span>Tweaks ({Object.keys(tweaks)?.length}) </span>
</Button>
<Separator orientation="vertical" className="ml-2 h-8" />
</div>
)}
</BaseModal.Header>
<BaseModal.Content overflowHidden>
{open && (
<>
<CustomAPIGenerator isOpen={open} />
<APITabsComponent />
</>
)}
</BaseModal.Content>
</BaseModal>
<BaseModal
open={openTweaks}
setOpen={setOpenTweaks}
size="medium-small-tall"
>
<BaseModal.Header
description={
autoLogin ? undefined : (
<>
<span className="pr-2">
API access requires an API key. You can{" "}
<a
href="/settings/api-keys"
className="text-accent-pink-foreground"
>
{" "}
create an API key
</a>{" "}
in settings.
</span>
</>
)
}
>
<IconComponent
name="SlidersHorizontal"
className="h-6 w-6 text-gray-800 dark:text-white"
/>
<span className="pl-2">Tweaks</span>
</BaseModal.Header>
<BaseModal.Content overflowHidden className="flex flex-col gap-4">
{true && (
<Label>
<div className="edit-flow-arrangement mt-2">
<span className="shrink-0 text-mmd font-medium">
Endpoint Name
</span>
{!validEndpointName && (
<span className="edit-flow-span">
Use only letters, numbers, hyphens, and underscores (
{MAX_LENGTH} characters max).
</span>
)}
</div>
<Input
className="nopan nodelete nodrag noflow mt-2 font-normal"
onChange={handleEndpointNameChange}
type="text"
name="endpoint_name"
value={endpointName ?? ""}
placeholder="An alternative name to run the endpoint"
maxLength={MAX_LENGTH}
minLength={MIN_LENGTH}
id="endpoint_name"
/>
</Label>
)}
<div className="h-full w-full overflow-y-auto overflow-x-hidden rounded-lg bg-muted custom-scroll">
<TweaksComponent open={openTweaks} />
</div>
</BaseModal.Content>
</BaseModal>
</>
);
}

View file

@ -1,44 +1,7 @@
import { ENABLE_DATASTAX_LANGFLOW } from "@/customization/feature-flags";
import { customGetHostProtocol } from "@/customization/utils/custom-get-host-protocol";
import useFlowStore from "@/stores/flowStore";
import { GetCodeType } from "@/types/tweaks";
/**
* Function to get the curl code for the API
* @param {string} flowId - The id of the flow
* @param {boolean} isAuth - If the API is authenticated
* @returns {string} - The curl code
*/
export function getCurlRunCode({
flowId,
isAuth,
tweaksBuildedObject,
endpointName,
activeTweaks,
}: GetCodeType): string {
let tweaksString = "{}";
const inputs = useFlowStore.getState().inputs;
const outputs = useFlowStore.getState().outputs;
const hasChatInput = inputs.some((input) => input.type === "ChatInput");
const hasChatOutput = outputs.some((output) => output.type === "ChatOutput");
if (tweaksBuildedObject)
tweaksString = JSON.stringify(tweaksBuildedObject, null, 2);
// show the endpoint name in the curl command if it exists
const { protocol, host } = customGetHostProtocol();
return `curl -X POST \\
"${protocol}//${host}/api/v1/run/${endpointName || flowId}?stream=false" \\
-H 'Content-Type: application/json'\\${
!isAuth ? `\n -H 'x-api-key: <your api key>'\\` : ""
}
-d '{${!activeTweaks ? `"input_value": "message",` : ""}
"output_type": ${hasChatOutput ? '"chat"' : '"text"'},
"input_type": ${hasChatInput ? '"chat"' : '"text"'},
"tweaks": ${tweaksString}}'
`;
}
/**
* Generates a cURL command for making a POST request to a webhook endpoint.
*

View file

@ -1,52 +1,5 @@
import { customGetHostProtocol } from "@/customization/utils/custom-get-host-protocol";
import useFlowStore from "@/stores/flowStore";
import { GetCodeType } from "@/types/tweaks";
/**
* Function to generate JavaScript code for interfacing with an API using the LangflowClient class.
* @param {string} flowId - The id of the flow.
* @param {boolean} isAuth - Whether the API requires authentication.
* @param {any[]} tweaksBuildedObject - Customizations applied to the flow.
* @param {string} [endpointName] - Optional endpoint name.
* @returns {string} - The JavaScript code as a string.
*/
export default function getJsApiCode({
flowId,
isAuth,
tweaksBuildedObject,
endpointName,
activeTweaks,
}: GetCodeType): string {
let tweaksString = "{}";
if (tweaksBuildedObject)
tweaksString = JSON.stringify(tweaksBuildedObject, null, 8);
const inputs = useFlowStore.getState().inputs;
const outputs = useFlowStore.getState().outputs;
const hasChatInput = inputs.some((input) => input.type === "ChatInput");
const hasChatOutput = outputs.some((output) => output.type === "ChatOutput");
const { protocol, host } = customGetHostProtocol();
return `${activeTweaks ? "" : 'let inputValue = ""; // Insert input value here\n\n'}fetch(
"${protocol}//${host}/api/v1/run/${endpointName || flowId}?stream=false",
{
method: "POST",
headers: {
"Authorization": "Bearer <TOKEN>",
"Content-Type": "application/json",${isAuth ? '\n\t\t\t"x-api-key": <your api key>' : ""}
},
body: JSON.stringify({${activeTweaks ? "" : "\n\t\t\tinput_value: inputValue, "}
output_type: ${hasChatOutput ? '"chat"' : '"text"'},
input_type: ${hasChatInput ? '"chat"' : '"text"'},
tweaks: ${tweaksString}
}),
},
)
.then(res => res.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
`;
}
/**
* Generates JavaScript code for making API calls to a Langflow endpoint.
*

View file

@ -2,7 +2,14 @@ import { AllNodeType } from "@/types/flow";
import { cloneDeep } from "lodash";
import { LANGFLOW_SUPPORTED_TYPES } from "../../../constants/constants";
export const getNodesWithDefaultValue = (nodes: AllNodeType[]) => {
export const getNodesWithDefaultValue = (
nodes: AllNodeType[],
oldTweaks: {
[key: string]: {
[key: string]: any;
};
},
) => {
const filteredNodes: AllNodeType[] = [];
nodes.forEach((node) => {
@ -20,6 +27,8 @@ export const getNodesWithDefaultValue = (nodes: AllNodeType[]) => {
if (newNode?.data?.node?.template) {
newNode.data.node.template = templateKeys.reduce((acc, key) => {
acc[key] = cloneDeep(node?.data?.node?.template[key]);
acc[key].advanced =
node.id in oldTweaks && key in oldTweaks[node.id] ? false : true;
return acc;
}, {});
}

View file

@ -1,120 +1,4 @@
import { customGetHostProtocol } from "@/customization/utils/custom-get-host-protocol";
import { GetCodeType } from "@/types/tweaks";
/**
* Function to get the python code for the API
* @param {string} flowId - The id of the flow
* @param {boolean} isAuth - If the API is authenticated
* @param {any[]} tweaksBuildedObject - The tweaks
* @param {string} [endpointName] - The optional endpoint name
* @returns {string} - The python code
*/
export default function getPythonApiCode({
flowId,
tweaksBuildedObject,
endpointName,
activeTweaks,
}: GetCodeType): string {
let tweaksString = "{}";
if (tweaksBuildedObject)
tweaksString = JSON.stringify(tweaksBuildedObject, null, 2)
.replace(/true/g, "True")
.replace(/false/g, "False")
.replace(/null|undefined/g, "None");
return `import argparse
import json
from argparse import RawTextHelpFormatter
import requests
from typing import Optional
import warnings
try:
from langflow.load import upload_file
except ImportError:
warnings.warn("Langflow provides a function to help you upload files to the flow. Please install langflow to use it.")
upload_file = None
BASE_API_URL = "${window.location.protocol}//${window.location.host}"
FLOW_ID = "${flowId}"
ENDPOINT = "${endpointName || ""}" ${
endpointName
? `# The endpoint name of the flow`
: `# You can set a specific endpoint name in the flow settings`
}
# You can tweak the flow by adding a tweaks dictionary
# e.g {"OpenAI-XXXXX": {"model_name": "gpt-4"}}
TWEAKS = ${tweaksString}
def run_flow(message: str,
endpoint: str,
output_type: str = "chat",
input_type: str = "chat",
tweaks: Optional[dict] = None,
api_key: Optional[str] = None) -> dict:
"""
Run a flow with a given message and optional tweaks.
:param message: The message to send to the flow
:param endpoint: The ID or the endpoint name of the flow
:param tweaks: Optional tweaks to customize the flow
:return: The JSON response from the flow
"""
api_url = f"{BASE_API_URL}/api/v1/run/{endpoint}"
payload = {
${!activeTweaks ? `"input_value": message,` : ""}
"output_type": output_type,
"input_type": input_type,
}
headers = None
if tweaks:
payload["tweaks"] = tweaks
if api_key:
headers = {"x-api-key": api_key}
response = requests.post(api_url, json=payload, headers=headers)
return response.json()
def main():
parser = argparse.ArgumentParser(description="""Run a flow with a given message and optional tweaks.\nRun it like: python <your file>.py "your message here" --endpoint "your_endpoint" --tweaks '{"key": "value"}'""",
formatter_class=RawTextHelpFormatter)
parser.add_argument("message", type=str, help="The message to send to the flow")
parser.add_argument("--endpoint", type=str, default=ENDPOINT or FLOW_ID, help="The ID or the endpoint name of the flow")
parser.add_argument("--tweaks", type=str, help="JSON string representing the tweaks to customize the flow", default=json.dumps(TWEAKS))
parser.add_argument("--api_key", type=str, help="API key for authentication", default=None)
parser.add_argument("--output_type", type=str, default="chat", help="The output type")
parser.add_argument("--input_type", type=str, default="chat", help="The input type")
parser.add_argument("--upload_file", type=str, help="Path to the file to upload", default=None)
parser.add_argument("--components", type=str, help="Components to upload the file to", default=None)
args = parser.parse_args()
try:
tweaks = json.loads(args.tweaks)
except json.JSONDecodeError:
raise ValueError("Invalid tweaks JSON string")
if args.upload_file:
if not upload_file:
raise ImportError("Langflow is not installed. Please install it to use the upload_file function.")
elif not args.components:
raise ValueError("You need to provide the components to upload the file to.")
tweaks = upload_file(file_path=args.upload_file, host=BASE_API_URL, flow_id=args.endpoint, components=[args.components], tweaks=tweaks)
response = run_flow(
message=args.message,
endpoint=args.endpoint,
output_type=args.output_type,
input_type=args.input_type,
tweaks=tweaks,
api_key=args.api_key
)
print(json.dumps(response, indent=2))
if __name__ == "__main__":
main()
`;
}
export function getNewPythonApiCode({
flowId,

View file

@ -1,28 +0,0 @@
import { GetCodeType } from "@/types/tweaks";
/**
* Function to get the python code for the API
* @param {string} flow - The current flow
* @param {any[]} tweaksBuildedObject - The tweaks
* @returns {string} - The python code
*/
export default function getPythonCode({
flowName,
tweaksBuildedObject,
activeTweaks,
}: GetCodeType): string {
let tweaksString = "{}";
if (tweaksBuildedObject)
tweaksString = JSON.stringify(tweaksBuildedObject, null, 2)
.replace(/true/g, "True")
.replace(/false/g, "False")
.replace(/null|undefined/g, "None");
return `from langflow.load import run_flow_from_json
TWEAKS = ${tweaksString}
result = run_flow_from_json(flow="${flowName}.json",
${!activeTweaks ? `input_value="message",\n ` : ""}session_id="", # provide a session id if you want to use session state
fallback_to_env_vars=True, # False by default
tweaks=TWEAKS)`;
}

View file

@ -1,80 +0,0 @@
import { tabsArrayType } from "@/types/components";
export function createTabsArray(
codes: { [key: string]: string },
includeTweaks = false,
) {
const tabs: tabsArrayType[] = [];
if (codes.runCurlCode) {
tabs.push({
name: "cURL",
mode: "bash",
image: "https://curl.se/logo/curl-symbol-transparent.png",
language: "sh",
code: codes.runCurlCode,
hasTweaks: includeTweaks,
});
}
if (codes.webhookCurlCode) {
tabs.push({
name: "Webhook cURL",
mode: "bash",
image: "https://curl.se/logo/curl-symbol-transparent.png",
language: "sh",
code: codes.webhookCurlCode,
});
}
if (codes.pythonApiCode) {
tabs.push({
name: "Python API",
mode: "python",
image:
"https://images.squarespace-cdn.com/content/v1/5df3d8c5d2be5962e4f87890/1628015119369-OY4TV3XJJ53ECO0W2OLQ/Python+API+Training+Logo.png?format=1000w",
language: "py",
code: codes.pythonApiCode,
hasTweaks: includeTweaks,
});
}
if (codes.jsApiCode) {
tabs.push({
name: "JS API",
mode: "javascript",
image: "https://cdn-icons-png.flaticon.com/512/136/136530.png",
language: "js",
code: codes.jsApiCode,
hasTweaks: includeTweaks,
});
}
if (codes.pythonCode) {
tabs.push({
name: "Python Code",
mode: "python",
image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png",
language: "py",
code: codes.pythonCode,
hasTweaks: includeTweaks,
});
}
if (codes.widgetCode) {
tabs.push({
name: "Chat Widget HTML",
description:
"Insert this code anywhere in your &lt;body&gt; tag. To use with react and other libs, check our <a class='link-color' href='https://langflow.org/guidelines/widget'>documentation</a>.",
mode: "html",
image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png",
language: "html",
code: codes.widgetCode,
});
}
if (includeTweaks) {
tabs.push({
name: "Tweaks",
mode: "python",
image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png",
language: "py",
code: codes.tweaksCode,
});
}
return tabs;
}

View file

@ -31,7 +31,7 @@ export const switchCaseModalSize = (size: string) => {
height = "h-fit";
break;
case "medium-small-tall":
minWidth = "min-w-[50vw]";
minWidth = "w-[900px] max-w-[98vw]";
height = "h-[70vh]";
break;
case "small-h-full":

View file

@ -26,7 +26,7 @@ const useColumnDefs = (
autoHeight: true,
flex: 1,
resizable: false,
cellClass: "no-border",
cellClass: "no-border cursor-default",
},
{
headerName: "Description",
@ -36,7 +36,7 @@ const useColumnDefs = (
autoHeight: true,
flex: 2,
resizable: false,
cellClass: "no-border",
cellClass: "no-border cursor-default",
},
{
headerName: "Value",
@ -61,24 +61,26 @@ const useColumnDefs = (
autoHeight: true,
flex: 1,
resizable: false,
cellClass: "no-border",
cellClass: "no-border cursor-default",
},
];
if (!hideVisibility) {
colDefs.push({
headerName: "Show",
headerName: isTweaks ? "Enable Input" : "Show",
field: "advanced",
cellRenderer: TableAdvancedToggleCellRender,
valueGetter: (params: ValueGetterParams) => {
return {
nodeId,
parameterId: params.data.key,
isTweaks,
};
},
editable: false,
maxWidth: 80,
maxWidth: !isTweaks ? 80 : 120,
minWidth: !isTweaks ? 80 : 120,
resizable: false,
cellClass: "no-border",
cellClass: "no-border cursor-default",
});
}
return colDefs;

View file

@ -53,6 +53,7 @@ import useAlertStore from "./alertStore";
import { useDarkStore } from "./darkStore";
import useFlowsManagerStore from "./flowsManagerStore";
import { useGlobalVariablesStore } from "./globalVariablesStore/globalVariables";
import { useTweaksStore } from "./tweaksStore";
import { useTypesStore } from "./typesStore";
// this is our useStore hook that we can use in our components to get parts of the store and call actions
@ -226,6 +227,9 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
) as string[],
});
unselectAllNodesEdges(nodes, edges);
if (flow?.id) {
useTweaksStore.getState().initialSetup(nodes, flow?.id);
}
set({
nodes,
edges: newEdges,

View file

@ -1,19 +1,13 @@
import { getChangesType } from "@/modals/apiModal/utils/get-changes-types";
import { getNodesWithDefaultValue } from "@/modals/apiModal/utils/get-nodes-with-default-value";
import { createTabsArray } from "@/modals/apiModal/utils/tabs-array";
import { AllNodeType, FlowType, NodeDataType } from "@/types/flow";
import { GetCodesType } from "@/types/tweaks";
import { customStringify } from "@/utils/reactflowUtils";
import { AllNodeType, NodeDataType } from "@/types/flow";
import { getLocalStorage, setLocalStorage } from "@/utils/local-storage-util";
import { create } from "zustand";
import { TweaksStoreType } from "../types/zustand/tweaks";
import useFlowStore from "./flowStore";
export const useTweaksStore = create<TweaksStoreType>((set, get) => ({
activeTweaks: false,
tweaks: {},
setActiveTweaks: (activeTweaks: boolean) => {
set({ activeTweaks }), get().refreshTabs();
},
nodes: [],
setNodes: (change) => {
let newChange = typeof change === "function" ? change(get().nodes) : change;
@ -21,7 +15,6 @@ export const useTweaksStore = create<TweaksStoreType>((set, get) => ({
set({
nodes: newChange,
});
get().refreshTabs();
get().updateTweaks();
},
setNode: (id, change) => {
@ -44,112 +37,28 @@ export const useTweaksStore = create<TweaksStoreType>((set, get) => ({
getNode: (id: string) => {
return get().nodes.find((node) => node.id === id);
},
autoLogin: false,
flow: null,
getCodes: {},
initialSetup: (
autoLogin: boolean,
flow: FlowType,
getCodes: GetCodesType,
) => {
currentFlowId: "",
initialSetup: (nodes: AllNodeType[], flowId: string) => {
useFlowStore.getState().unselectAll();
set({
nodes: getNodesWithDefaultValue(flow?.data?.nodes ?? []),
autoLogin,
flow,
getCodes,
currentFlowId: flowId,
});
get().refreshTabs();
},
newInitialSetup: (nodes: AllNodeType[]) => {
useFlowStore.getState().unselectAll();
const tweaks = JSON.parse(getLocalStorage(`lf_tweaks_${flowId}`) || "{}");
set({
nodes: getNodesWithDefaultValue(nodes),
nodes: getNodesWithDefaultValue(nodes, tweaks),
});
get().updateTweaks();
},
tabs: [],
refreshTabs: () => {
const autoLogin = get().autoLogin;
const flow = get().flow;
const tweak = {};
const nodes = get().nodes;
const originalNodes = flow?.data?.nodes;
if (!flow) return;
nodes.forEach((node) => {
const originalNodeTemplate = originalNodes?.find((n) => n.id === node.id)
?.data?.node?.template;
const nodeTemplate = node.data?.node?.template;
if (originalNodeTemplate && nodeTemplate && node.type === "genericNode") {
const currentTweak = {};
Object.keys(nodeTemplate).forEach((name) => {
if (
customStringify(nodeTemplate[name]) !==
customStringify(originalNodeTemplate[name]) ||
get().activeTweaks
) {
currentTweak[name] = getChangesType(
nodeTemplate[name].value,
nodeTemplate[name],
);
}
});
tweak[node.id] = currentTweak;
}
});
const codesObj = {};
const getCodes = get().getCodes;
const props = {
flowId: flow?.id,
flowName: flow?.name,
isAuth: autoLogin,
tweaksBuildedObject: tweak,
endpointName: flow?.endpoint_name,
activeTweaks: get().activeTweaks,
};
if (getCodes) {
if (getCodes.getCurlRunCode) {
codesObj["runCurlCode"] = getCodes.getCurlRunCode(props);
}
if (getCodes.getCurlWebhookCode && !!flow.webhook) {
codesObj["webhookCurlCode"] = getCodes.getCurlWebhookCode(props);
}
if (getCodes.getJsApiCode) {
codesObj["jsApiCode"] = getCodes.getJsApiCode(props);
}
if (getCodes.getPythonApiCode) {
codesObj["pythonApiCode"] = getCodes.getPythonApiCode(props);
}
if (getCodes.getPythonCode) {
codesObj["pythonCode"] = getCodes.getPythonCode(props);
}
if (getCodes.getWidgetCode) {
codesObj["widgetCode"] = getCodes.getWidgetCode(props);
}
}
set({
tabs: createTabsArray(codesObj, nodes.length > 0),
});
},
updateTweaks: () => {
const nodes = get().nodes;
const originalNodes = useFlowStore.getState().nodes;
const tweak = {};
const flowId = get().currentFlowId;
nodes.forEach((node) => {
const originalNodeTemplate = originalNodes?.find((n) => n.id === node.id)
?.data?.node?.template;
const nodeTemplate = node.data?.node?.template;
if (originalNodeTemplate && nodeTemplate && node.type === "genericNode") {
if (nodeTemplate && node.type === "genericNode") {
const currentTweak = {};
Object.keys(nodeTemplate).forEach((name) => {
if (
customStringify(nodeTemplate[name]) !==
customStringify(originalNodeTemplate[name])
) {
if (!nodeTemplate[name].advanced) {
currentTweak[name] = getChangesType(
nodeTemplate[name].value,
nodeTemplate[name],
@ -161,6 +70,7 @@ export const useTweaksStore = create<TweaksStoreType>((set, get) => ({
}
}
});
setLocalStorage(`lf_tweaks_${flowId}`, JSON.stringify(tweak));
set({
tweaks: tweak,
});

View file

@ -1240,7 +1240,7 @@
}
.playground-btn-flow-toolbar {
@apply relative inline-flex h-8 w-full items-center justify-center gap-1.5 rounded px-3 py-1.5 text-sm font-semibold transition-all duration-500 ease-in-out;
@apply relative inline-flex h-8 w-full items-center justify-center gap-1.5 rounded px-3 py-1.5 text-sm font-normal transition-all duration-500 ease-in-out;
}
.input-slider-text {

View file

@ -1,11 +1,8 @@
import { AllNodeType, FlowType, TweaksType } from "@/types/flow";
import { GetCodesType } from "@/types/tweaks";
import { tabsArrayType } from "../../components";
import { AllNodeType } from "@/types/flow";
export type TweaksStoreType = {
activeTweaks: boolean;
setActiveTweaks: (activeTweaks: boolean) => void;
nodes: AllNodeType[];
currentFlowId: string;
setNodes: (
update: AllNodeType[] | ((oldState: AllNodeType[]) => AllNodeType[]),
skipSave?: boolean,
@ -14,18 +11,8 @@ export type TweaksStoreType = {
id: string,
update: AllNodeType | ((oldState: AllNodeType) => AllNodeType),
) => void;
getCodes: GetCodesType;
getNode: (id: string) => AllNodeType | undefined;
tabs: tabsArrayType[];
initialSetup: (
autoLogin: boolean,
flow: FlowType,
getCodes: GetCodesType,
) => void;
newInitialSetup: (nodes: AllNodeType[]) => void;
refreshTabs: () => void;
autoLogin: boolean;
flow: FlowType | null;
initialSetup: (nodes: AllNodeType[], flowId: string) => void;
updateTweaks: () => void;
tweaks: {
[key: string]: {

View file

@ -27,17 +27,11 @@ test(
.first()
.click();
await page.waitForSelector(
'[data-testid="popover-anchor-input-openai_api_base-edit"]',
{
timeout: 1000,
},
);
await page.waitForSelector('[data-testid="showstream"]', {
timeout: 1000,
});
await page
.getByTestId("popover-anchor-input-openai_api_base-edit")
.first()
.fill("teste");
await page.getByTestId("showstream").first().click();
await page.getByText("Close").last().click();
@ -50,6 +44,13 @@ test(
const newValue = clipboardContent2;
expect(oldValue).not.toBe(newValue);
expect(clipboardContent2.length).toBeGreaterThan(clipboardContent.length);
await awaitBootstrapTest(page, { skipModal: true });
await page.getByText("Basic Prompting").first().click();
await page.getByTestId("publish-button").click();
await page.getByTestId("api-access-item").click();
expect(
await page.getByText("Input Schema (1)", { exact: true }).isVisible(),
);
},
);
@ -131,5 +132,5 @@ test("check if tweaks are updating when someothing on the flow changes", async (
await page.getByText("collection_name_test_123123123!@#$&*(&%$@").isVisible();
await page.getByText("persist_directory_123123123!@#$&*(&%$@").isVisible();
expect(await page.getByText("Tweaks (2)", { exact: true }).isVisible());
expect(await page.getByText("Input Schema (2)", { exact: true }).isVisible());
});