Merge branch 'bug/component_share' into dev

This commit is contained in:
Lucas Oliveira 2023-12-10 22:35:20 -03:00
commit 54064737e8
16 changed files with 216 additions and 170 deletions

View file

@ -1,11 +1,12 @@
import warnings
from pathlib import Path
from typing import TYPE_CHECKING, List
from fastapi import HTTPException
from platformdirs import user_cache_dir
from langflow.services.store.schema import StoreComponentCreate
from langflow.services.store.utils import get_lf_version_from_pypi
import warnings
from platformdirs import user_cache_dir
if TYPE_CHECKING:
from langflow.services.database.models.flow.model import Flow
@ -62,7 +63,7 @@ def build_input_keys_response(langchain_object, artifacts):
return input_keys_response
def update_frontend_node_with_template_values(frontend_node, raw_template_data):
def update_frontend_node_with_template_values(frontend_node, raw_frontend_node):
"""
Updates the given frontend node with values from the raw template data.
@ -70,19 +71,28 @@ def update_frontend_node_with_template_values(frontend_node, raw_template_data):
:param raw_template_data: A dict representing raw template data.
:return: Updated frontend node.
"""
if not is_valid_data(frontend_node, raw_template_data):
if not is_valid_data(frontend_node, raw_frontend_node):
return frontend_node
update_template_values(frontend_node["template"], raw_template_data.template)
# Check if the display_name is different than "CustomComponent"
# if so, update the display_name in the frontend_node
if raw_frontend_node["display_name"] != "CustomComponent":
frontend_node["display_name"] = raw_frontend_node["display_name"]
update_template_values(frontend_node["template"], raw_frontend_node["template"])
return frontend_node
def is_valid_data(frontend_node, raw_template_data):
def raw_frontend_data_is_valid(raw_frontend_data):
"""Check if the raw frontend data is valid for processing."""
return "template" in raw_frontend_data and "display_name" in raw_frontend_data
def is_valid_data(frontend_node, raw_frontend_data):
"""Check if the data is valid for processing."""
return (
frontend_node and "template" in frontend_node and raw_template_data and hasattr(raw_template_data, "template")
)
return frontend_node and "template" in frontend_node and raw_frontend_data_is_valid(raw_frontend_data)
def update_template_values(frontend_template, raw_template):

View file

@ -3,6 +3,9 @@ from typing import Annotated, Optional, Union
import sqlalchemy as sa
from fastapi import APIRouter, Body, Depends, HTTPException, UploadFile, status
from loguru import logger
from sqlmodel import select
from langflow.api.utils import update_frontend_node_with_template_values
from langflow.api.v1.schemas import (
CustomComponentCode,
@ -20,8 +23,6 @@ from langflow.services.cache.utils import save_uploaded_file
from langflow.services.database.models.flow import Flow
from langflow.services.database.models.user.model import User
from langflow.services.deps import get_session, get_session_service, get_settings_service, get_task_service
from loguru import logger
from sqlmodel import select
try:
from langflow.worker import process_graph_cached_task
@ -31,9 +32,10 @@ except ImportError:
raise NotImplementedError("Celery is not installed")
from langflow.services.task.service import TaskService
from sqlmodel import Session
from langflow.services.task.service import TaskService
# build router
router = APIRouter(tags=["Base"])
@ -218,7 +220,7 @@ async def custom_component(
built_frontend_node = build_custom_component_template(component, user_id=user.id)
built_frontend_node = update_frontend_node_with_template_values(built_frontend_node, raw_code)
built_frontend_node = update_frontend_node_with_template_values(built_frontend_node, raw_code.frontend_node)
return built_frontend_node

View file

@ -3,11 +3,12 @@ from pathlib import Path
from typing import Any, Dict, List, Optional, Union
from uuid import UUID
from pydantic import BaseModel, Field, field_validator
from langflow.services.database.models.api_key.model import ApiKeyRead
from langflow.services.database.models.base import orjson_dumps
from langflow.services.database.models.flow import FlowCreate, FlowRead
from langflow.services.database.models.user import UserRead
from pydantic import BaseModel, Field, field_validator
class BuildStatus(Enum):
@ -157,7 +158,7 @@ class StreamData(BaseModel):
class CustomComponentCode(BaseModel):
code: str
field: Optional[str] = None
template: Optional[dict] = None
frontend_node: Optional[dict] = None
class CustomComponentResponseError(BaseModel):

View file

@ -3,8 +3,6 @@ from langflow.field_typing import Data
class Component(CustomComponent):
display_name: str = "Custom Component"
description: str = "Create any custom component you want!"
documentation: str = "http://docs.langflow.org/components/custom"
def build_config(self):

View file

@ -19,8 +19,8 @@ from langflow.utils import validate
class CustomComponent(Component):
display_name: Optional[str] = "Custom Component"
description: Optional[str] = "Custom Component"
display_name: Optional[str] = None
description: Optional[str] = None
code: Optional[str] = None
field_config: dict = {}
code_class_base_inheritance: ClassVar[str] = "CustomComponent"

View file

@ -3,7 +3,6 @@ from typing import Optional
from langflow.template.field.base import TemplateField
from langflow.template.frontend_node.base import FrontendNode
from langflow.template.template.base import Template
from pydantic import field_serializer
DEFAULT_CUSTOM_COMPONENT_CODE = """from langflow import CustomComponent
from typing import Optional, List, Dict, Union
@ -47,7 +46,7 @@ class Component(CustomComponent):
class CustomComponentFrontendNode(FrontendNode):
name: str = "CustomComponent"
display_name: str = "Custom Component"
display_name: Optional[str] = "CustomComponent"
beta: bool = True
template: Template = Template(
type_name="CustomComponent",
@ -67,9 +66,3 @@ class CustomComponentFrontendNode(FrontendNode):
)
description: Optional[str] = None
base_classes: list[str] = []
@field_serializer("display_name")
def process_display_name(self, display_name: str) -> str:
"""Sets the display name of the frontend node."""
return display_name

View file

@ -156,11 +156,16 @@ export default function ParameterComponent({
};
const handleNodeClass = (newNodeClass: APIClassType, code?: string): void => {
if (!data.node) return;
if (data.node!.template[name].value !== newNodeClass.template[name].value) {
takeSnapshot();
}
data.node = newNodeClass;
data.node.template[name].value = code;
data.node! = {
...newNodeClass,
description: newNodeClass.description ?? data.node!.description,
display_name: newNodeClass.display_name ?? data.node!.display_name,
};
data.node!.template[name].value = code;
updateNodeInternals(data.id);
// Set state to pending
//@ts-ignore
@ -174,19 +179,22 @@ export default function ParameterComponent({
}
renderTooltips();
let flow = flows.find((flow) => flow.id === tabId);
if (reactFlowInstance && flow && flow.data) {
cleanEdges({
flow: {
edges: flow.data!.edges,
nodes: flow.data!.nodes,
},
updateEdge: (edge) => {
reactFlowInstance.setEdges(edge);
updateNodeInternals(data.id);
},
});
updateFlow(flow);
}
setTimeout(() => {
//timeout necessary because ReactFlow updates are not async
if (reactFlowInstance && flow && flow.data) {
cleanEdges({
flow: {
edges: flow.data!.edges,
nodes: flow.data!.nodes,
},
updateEdge: (edge) => {
reactFlowInstance.setEdges(edge);
updateNodeInternals(data.id);
},
});
updateFlow(flow);
}
}, 50);
};
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);

View file

@ -16,7 +16,7 @@ import { validationStatusType } from "../../types/components";
import { NodeDataType } from "../../types/flow";
import { handleKeyDown, scapedJSONStringfy } from "../../utils/reactflowUtils";
import { nodeColors, nodeIconsLucide } from "../../utils/styleUtils";
import { classNames, getFieldTitle } from "../../utils/utils";
import { classNames, cn, getFieldTitle } from "../../utils/utils";
import ParameterComponent from "./components/parameterComponent";
export default function GenericNode({
@ -107,6 +107,8 @@ export default function GenericNode({
const showNode = data.showNode ?? true;
const nameEditable = data.node?.flow || data.type === "CustomComponent";
return (
<>
<NodeToolbar>
@ -164,7 +166,7 @@ export default function GenericNode({
/>
{showNode && (
<div className="generic-node-tooltip-div">
{data.node?.flow && inputName ? (
{nameEditable && inputName ? (
<div>
<InputComponent
onBlur={() => {
@ -172,6 +174,7 @@ export default function GenericNode({
if (nodeName.trim() !== "") {
setNodeName(nodeName);
data.node!.display_name = nodeName;
updateNodeInternals(data.id);
} else {
setNodeName(data.node!.display_name);
}
@ -194,7 +197,7 @@ export default function GenericNode({
<div className="generic-node-tooltip-div pr-2 text-primary">
{data.node?.display_name}
</div>
{data.node?.flow && (
{nameEditable && (
<IconComponent
name="Pencil"
className="h-4 w-4 text-ring"
@ -361,57 +364,62 @@ export default function GenericNode({
<div
className={
showNode
? "generic-node-desc overflow-hidden " +
(data.node?.description !== "" ? "py-5" : "pb-5")
? "overflow-hidden " +
(data.node?.description === "" && !nameEditable
? "pb-5"
: "py-5")
: ""
}
>
{data.node?.description !== "" &&
showNode &&
data.node?.flow &&
inputDescription ? (
<Textarea
autoFocus
onBlur={() => {
setInputDescription(false);
if (nodeDescription.trim() !== "") {
<div className="generic-node-desc">
{showNode && nameEditable && inputDescription ? (
<Textarea
autoFocus
onBlur={() => {
setInputDescription(false);
setNodeDescription(nodeDescription);
data.node!.description = nodeDescription;
} else {
setNodeDescription(data.node!.description);
}
}}
value={nodeDescription}
onChange={(e) => setNodeDescription(e.target.value)}
onKeyDown={(e) => {
handleKeyDown(e, nodeDescription, "");
if (
e.key === "Enter" &&
e.shiftKey === false &&
e.ctrlKey === false &&
e.altKey === false
) {
setInputDescription(false);
if (nodeDescription.trim() !== "") {
updateNodeInternals(data.id);
}}
value={nodeDescription}
onChange={(e) => setNodeDescription(e.target.value)}
onKeyDown={(e) => {
handleKeyDown(e, nodeDescription, "");
if (
e.key === "Enter" &&
e.shiftKey === false &&
e.ctrlKey === false &&
e.altKey === false
) {
setInputDescription(false);
setNodeDescription(nodeDescription);
data.node!.description = nodeDescription;
} else {
setNodeDescription(data.node!.description);
updateNodeInternals(data.id);
}
}
}}
/>
) : (
<div
className="generic-node-desc-text truncate-multiline word-break-break-word"
onDoubleClick={() => {
setInputDescription(true);
takeSnapshot();
}}
>
{data.node?.description}
</div>
)}
}}
/>
) : (
<div
className={cn(
"generic-node-desc-text truncate-multiline word-break-break-word",
(data.node?.description === "" ||
!data.node?.description) &&
nameEditable
? "font-light italic"
: ""
)}
onDoubleClick={() => {
setInputDescription(true);
takeSnapshot();
}}
>
{(data.node?.description === "" || !data.node?.description) &&
nameEditable
? "Double Click to Edit Description"
: data.node?.description}
</div>
)}
</div>
<>
{Object.keys(data.node!.template)
.filter((templateField) => templateField.charAt(0) !== "_")

View file

@ -3,6 +3,7 @@ import { Input } from "../../components/ui/input";
import { Label } from "../../components/ui/label";
import { Textarea } from "../../components/ui/textarea";
import { InputProps } from "../../types/components";
import { cn } from "../../utils/utils";
export const EditFlowSettings: React.FC<InputProps> = ({
name,
@ -21,47 +22,65 @@ export const EditFlowSettings: React.FC<InputProps> = ({
} else {
setIsMaxLength(false);
}
setName(value);
setName!(value);
};
const handleDescriptionChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
setDescription(event.target.value);
setDescription!(event.target.value);
};
return (
<>
<Label>
<div className="edit-flow-arrangement">
<span className="font-medium">Name</span>{" "}
<span className="font-medium">Name{setName ? "" : ":"}</span>{" "}
{isMaxLength && (
<span className="edit-flow-span">Character limit reached</span>
)}
</div>
<Input
className="nopan nodelete nodrag noundo nocopy mt-2 font-normal"
onChange={handleNameChange}
type="text"
name="name"
value={name ?? ""}
placeholder="Flow name"
id="name"
maxLength={maxLength}
/>
{setName ? (
<Input
className="nopan nodelete nodrag noundo nocopy mt-2 font-normal"
onChange={handleNameChange}
type="text"
name="name"
value={name ?? ""}
placeholder="Flow name"
id="name"
maxLength={maxLength}
/>
) : (
<span className="font-normal text-muted-foreground word-break-break-word">
{name}
</span>
)}
</Label>
<Label>
<div className="edit-flow-arrangement mt-3">
<span className="font-medium ">Description (optional)</span>
<span className="font-medium ">
Description{setDescription ? " (optional)" : ":"}
</span>
</div>
<Textarea
name="description"
id="description"
onChange={handleDescriptionChange}
value={description!}
placeholder="Flow description"
className="mt-2 max-h-[100px] font-normal"
rows={3}
/>
{setDescription ? (
<Textarea
name="description"
id="description"
onChange={handleDescriptionChange}
value={description!}
placeholder="Flow description"
className="mt-2 max-h-[100px] font-normal"
rows={3}
/>
) : (
<span
className={cn(
"font-normal text-muted-foreground word-break-break-word",
description === "" ? "font-light italic" : ""
)}
>
{description === "" ? "No description" : description}
</span>
)}
</Label>
</>
);

View file

@ -357,10 +357,10 @@ export async function postCustomComponent(
code: string,
apiClass: APIClassType
): Promise<AxiosResponse<APIClassType>> {
let template = apiClass.template;
// let template = apiClass.template;
return await api.post(`${BASE_URL_API}custom_component`, {
code,
template,
frontend_node: apiClass,
});
}

View file

@ -44,11 +44,9 @@ export default function ShareModal({
const { setSuccessData, setErrorData } = useContext(alertContext);
const { reactFlowInstance } = useContext(typesContext);
const [checked, setChecked] = useState(false);
const [name, setName] = useState(component?.name ?? "");
const [description, setDescription] = useState(component?.description ?? "");
const [internalOpen, internalSetOpen] = useState(children ? false : true);
const [openConfirmationModal, setOpenConfirmationModal] = useState(false);
const nameComponent = is_component ? "Component" : "Flow";
const nameComponent = is_component ? "component" : "flow";
const [tags, setTags] = useState<{ id: string; name: string }[]>([]);
const [loadingTags, setLoadingTags] = useState<boolean>(false);
@ -61,6 +59,9 @@ export default function ShareModal({
const [loadingNames, setLoadingNames] = useState(false);
const name = component?.name ?? "";
const description = component?.description ?? "";
useEffect(() => {
if (open || internalOpen) {
if (hasApiKey && hasStore) {
@ -94,11 +95,6 @@ export default function ShareModal({
});
}
useEffect(() => {
setName(component?.name ?? "");
setDescription(component?.description ?? "");
}, [component, open, internalOpen]);
const handleShareComponent = async (update = false) => {
//remove file names from flows before sharing
removeFileNameFromComponents(component);
@ -120,11 +116,9 @@ export default function ShareModal({
is_component: is_component,
});
await saveFlow(flow!, true);
function successShare() {
if (is_component) {
addFlow(true, flow);
if (!is_component) {
saveFlow(flow!, true);
}
setSuccessData({
title: `${nameComponent} shared successfully`,
@ -223,13 +217,9 @@ export default function ShareModal({
/>
</BaseModal.Header>
<BaseModal.Content>
<EditFlowSettings
name={name}
invalidNameList={unavaliableNames.map((element) => element.name)}
description={description}
setName={setName}
setDescription={setDescription}
/>
<div className="w-full rounded-lg border border-border p-4">
<EditFlowSettings name={name} description={description} />
</div>
<div className="mt-3 flex h-8 w-full">
<TagsSelector
tags={tags}
@ -297,8 +287,8 @@ export default function ShareModal({
</>
) : (
<>
{is_component && !loadingNames ? "Save and " : ""}Share{" "}
{!is_component && !loadingNames ? "Flow" : ""}
Share{" "}
{!loadingNames && (!is_component ? "Flow" : "Component")}
</>
)}
</Button>

View file

@ -282,6 +282,17 @@ export default function Page({
let newX = _.cloneDeep(node);
return newX;
});
//@ts-ignore
setTabsState((prev: FlowsState) => {
return {
...prev,
[tabId]: {
...prev[tabId],
isPending: true,
},
};
});
saveCurrentFlowTimeout();
},
[setEdges, setNodes, takeSnapshot, addEdge]
);

View file

@ -77,7 +77,15 @@ export default function NodeToolbarComponent({
useEffect(() => {
setFlowComponent(createFlowComponent(cloneDeep(data), version));
}, [data, data.node, showModalAdvanced]);
}, [
data,
data.node,
data.node?.display_name,
data.node?.description,
data.node?.template,
showModalAdvanced,
showconfirmShare,
]);
const handleSelectChange = (event) => {
switch (event) {

View file

@ -311,10 +311,10 @@
@apply hover:text-accent-foreground hover:transition-all;
}
.generic-node-desc {
@apply h-full w-full text-foreground;
@apply h-full px-5 mb-4 w-full text-foreground;
}
.generic-node-desc-text {
@apply w-full px-5 mb-4 text-sm text-muted-foreground;
@apply w-full text-sm text-muted-foreground;
}
.alert-icon {

View file

@ -240,8 +240,8 @@ export type InputProps = {
name: string | null;
description: string | null;
maxLength?: number;
setName: (name: string) => void;
setDescription: (description: string) => void;
setName?: (name: string) => void;
setDescription?: (description: string) => void;
invalidNameList?: string[];
};

View file

@ -41,37 +41,35 @@ export function cleanEdges({
const targetNode = nodes.find((node) => node.id === edge.target);
if (!sourceNode || !targetNode) {
newEdges = newEdges.filter((edg) => edg.id !== edge.id);
return;
}
// check if the source and target handle still exists
if (sourceNode && targetNode) {
const sourceHandle = edge.sourceHandle; //right
const targetHandle = edge.targetHandle; //left
if (targetHandle) {
const targetHandleObject: targetHandleType =
scapeJSONParse(targetHandle);
const field = targetHandleObject.fieldName;
const id: targetHandleType = {
type: targetNode.data.node!.template[field]?.type,
fieldName: field,
id: targetNode.data.id,
inputTypes: targetNode.data.node!.template[field]?.input_types,
};
if (targetNode.data.node!.template[field]?.proxy) {
id.proxy = targetNode.data.node!.template[field]?.proxy;
}
if (scapedJSONStringfy(id) !== targetHandle) {
newEdges = newEdges.filter((e) => e.id !== edge.id);
}
const sourceHandle = edge.sourceHandle; //right
const targetHandle = edge.targetHandle; //left
if (targetHandle) {
const targetHandleObject: targetHandleType = scapeJSONParse(targetHandle);
const field = targetHandleObject.fieldName;
const id: targetHandleType = {
type: targetNode.data.node!.template[field]?.type,
fieldName: field,
id: targetNode.data.id,
inputTypes: targetNode.data.node!.template[field]?.input_types,
};
if (targetNode.data.node!.template[field]?.proxy) {
id.proxy = targetNode.data.node!.template[field]?.proxy;
}
if (sourceHandle) {
const id: sourceHandleType = {
id: sourceNode.data.id,
baseClasses: sourceNode.data.node!.base_classes,
dataType: sourceNode.data.type,
};
if (scapedJSONStringfy(id) !== sourceHandle) {
newEdges = newEdges.filter((e) => e.id !== edge.id);
}
if (scapedJSONStringfy(id) !== targetHandle) {
newEdges = newEdges.filter((e) => e.id !== edge.id);
}
}
if (sourceHandle) {
const id: sourceHandleType = {
id: sourceNode.data.id,
baseClasses: sourceNode.data.node!.base_classes,
dataType: sourceNode.data.type,
};
if (scapedJSONStringfy(id) !== sourceHandle) {
newEdges = newEdges.filter((e) => e.id !== edge.id);
}
}
});
@ -761,7 +759,7 @@ export function generateNodeFromFlow(
display_name: "Group",
documentation: "",
base_classes: outputNode!.data.node!.base_classes,
description: "double click to edit description",
description: "",
template: generateNodeTemplate(data),
flow: data,
},