feature: add flow notes (#3460)

* feat: Add NoteDraggableComponent to extraSidebarComponent

This commit adds the NoteDraggableComponent to the extraSidebarComponent in the FlowPage. The NoteDraggableComponent allows users to drag and drop sticky note icons onto the page. When a note is dragged, its data is set as "note" in the dataTransfer object. The note has a default text value of null and a noteColor of "yellow". This component enhances the user experience by providing a convenient way to add notes to the page.

* feat: Add NoteNode component for displaying and editing notes

This commit adds the NoteNode component, which is responsible for displaying and editing notes in the FlowPage. The NoteNode component includes functionality for resizing, selecting, and editing the note text. It enhances the user experience by providing a convenient way to add and manage notes on the page.

* feat: Add NoteNode and NoteDraggableComponent for managing notes in FlowPage

This commit adds the NoteNode component, responsible for displaying and editing notes in the FlowPage. It also introduces the NoteDraggableComponent, allowing users to drag and drop sticky note icons onto the page. These components enhance the user experience by providing a convenient way to add and manage notes on the page.

* feat: Add DRAG_EVENTS_CUSTOM_TYPESS constant for custom drag event types

This commit adds the DRAG_EVENTS_CUSTOM_TYPESS constant to the constants file. It defines custom drag event types for the generic node and note node components. This constant enhances the code by providing a centralized place to manage and reference the custom drag event types.

* feat: Add support functions for custom drag event types

* feat: Add support for custom drag event types in PageComponent

This commit adds support for custom drag event types in the PageComponent of the FlowPage. It imports the necessary functions from the utils file and uses them to check if the dragged data has supported node types. This enhancement improves the drag and drop functionality by allowing only supported node types to be dropped on the page.

* feat: Add NoteDataType for managing note data in FlowPage

* refactor: create new types for noteNode

* feat: Update NoteNode component to use new NoteDataType

The NoteNode component in the NoteNode/index.tsx file has been updated to use the new NoteDataType for managing note data in the FlowPage. This change ensures consistency and improves the codebase.

* node with title and description

* feat: Add "note" alias for StickyNote in nodeIconsLucide

This commit adds the "note" alias for the StickyNote icon in the nodeIconsLucide object in the styleUtils.ts file. This alias allows for more intuitive usage of the StickyNote icon by providing an alternative name. It improves code readability and maintainability.

* refactor: Update NodeDescription component to use emptyPlaceholder prop

The NodeDescription component in the GenericNode/components/NodeDescription/index.tsx file has been updated to use the emptyPlaceholder prop. This change allows for more flexibility in customizing the placeholder text when the description is empty. It improves code reusability and enhances the user experience.

* refactor: Remove unused Textarea import in NoteNode component

* add initial resize to note component

* [autofix.ci] apply automated fixes

* refactor: add code validation functionality on tanstack mutation (#3469)

* Added Validate endpoint

* Added API Code Validate type

* Added post validate code hook

* Used mutation instead of API call to validate code

* Removed validate code api call

* refactor: Update NodeName component to use full width in GenericNode

The NodeName component in the GenericNode module has been updated to use the full width of the parent container. This change ensures that the input field or tooltip for the node name occupies the entire available space, improving the visual consistency and user experience.

* refactor: Update NodeDescription component to use full height in GenericNode

The NodeDescription component in the GenericNode module has been updated to use the full height of the parent container. This change ensures that the description text area occupies the entire available space, improving the visual consistency and user experience.

* refactor: Update NodeDescription component to use full height in GenericNode

* refactor: Update NodeDescription component to use full height in GenericNode

* increase size control on note node

* refactor: Update NoteNode component to use constants for min and max dimensions

The NoteNode component in the CustomNodes module has been updated to use the constants for the minimum and maximum dimensions. This change improves code readability and maintainability by centralizing the values in the constants file. The component now uses the NOTE_NODE_MIN_WIDTH, NOTE_NODE_MIN_HEIGHT, NOTE_NODE_MAX_HEIGHT, and NOTE_NODE_MAX_WIDTH constants for setting the dimensions of the NodeResizer and the inline styles. This ensures consistency across the application and makes it easier to adjust the dimensions in the future.

* fix overflow issue

* refactor: update NoteDraggableComponent

The NoteDraggableComponent in the extraSidebarComponent module has been updated to remove unused code and improve functionality. The code for adding a note has been removed as it is no longer needed. Additionally, the component has been updated to use a new design and layout for better user experience. This refactor improves the overall code cleanliness and removes unnecessary clutter.

* refactor: Update NoteNode component to use constants for min and max dimensions

* update component to accept multiple colors

* update note colors

* update min width

* refactor: Update NodeDescription component to accept additional styling options

The NodeDescription component in the GenericNode module has been updated to accept additional styling options. The component now includes the inputClassName, mdClassName, and style props, allowing for more customization of the input and markdown elements. This refactor improves the flexibility and extensibility of the component, making it easier to adapt to different design requirements.

* fix bug on description size

* [autofix.ci] apply automated fixes

* feat: skip note nodes when building vertices"

add check to skip nodes of type NoteNode when building vertices in the graph
this prevents unnecessary processing of note nodes which are not part of the actual graph logic

* fix: update serialization and improve error handling (#3516)

* feat(utils): add support for V1BaseModel in serialize_field

Add support for V1BaseModel instances in the serialize_field function by
checking for a "to_json" method. If the method is not present, return the
attribute values as a dictionary.

* refactor: Update field serializer function and error handling in build_flow function

* fix: no module named 'psycopg2' (#3526)

* fix: add dependecy to Dockerfile

* fix: revert quick fix

* fix: update poetry.lock

* feat: add compression support to frontend and backend (#3484)

* Added compression lib to frontend

* Added compression handling to backend

* Added compression to body requests on frontend

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>

* refactor: add code validation functionality on tanstack mutation (#3469)

* Added Validate endpoint

* Added API Code Validate type

* Added post validate code hook

* Used mutation instead of API call to validate code

* Removed validate code api call

* refactor: Update NodeName component to use full width in GenericNode

The NodeName component in the GenericNode module has been updated to use the full width of the parent container. This change ensures that the input field or tooltip for the node name occupies the entire available space, improving the visual consistency and user experience.

* fix imports

* refactor: remove unused import in styleUtils.ts

* refactor: update checkOldNodesOutput to include only generic nodes

The checkOldNodesOutput function in reactflowUtils.ts has been updated to include only generic nodes when checking for nodes without outputs. This change ensures that only nodes of type "genericNode" are considered, improving the accuracy of the check and preventing false positives.

* refactor: improve checkOldNodesOutput to include only generic nodes

* [autofix.ci] apply automated fixes

* 📝 (frontend): Add data-testid attribute to elements for testing purposes in NodeDescription, NoteToolbarComponent, NoteNode, and Textarea components
 (frontend): Create end-to-end test for interacting with sticky notes including creating, editing, duplicating, and deleting notes

*  (stop-building.spec.ts): Add a 1-second delay after clicking the stop building button to improve user experience
 (sticky-notes.spec.ts): Add a new end-to-end test for interacting with sticky notes on the main page to ensure functionality and user interaction with sticky notes.

* refactor: update NodeName and GenericNode components to improve UI and code readability

* [autofix.ci] apply automated fixes

* chore: Update NodeDescription component to use dark mode placeholder color

* chore: Update sidebar note component icon to use StickyNote instead of SquarePen

* refactor(main.py): remove unused imports and middleware related to GZip to simplify code and improve readability
feat(main.py): add middleware for configuring logger to improve logging functionality and centralize logging configuration

* add center postion in the flow

* chore: Update NoteNode icon to use SquarePen instead of StickyNote

* [autofix.ci] apply automated fixes

* chore: Update API base URL

* [autofix.ci] apply automated fixes

* chore:  add feature flag for MVPs

* code format

*  (NoteNode/index.tsx): Wrap IconComponent in a div with data-testid "note_icon" for better accessibility and testing
 (PageComponent/index.tsx): Add data-testid "add_note" to ControlButton for easier testing and identification
🔧 (sticky-notes.spec.ts): Update test selectors to use new data-testid "note_icon" and "add_note" for improved test reliability

* chore: Update types-markdown to version 3.7.0.20240822

* feat: Add lazy loading for images in sticky-notes.spec.ts

* [autofix.ci] apply automated fixes

* update poetry lock

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes

* Refactor feature flag import for ENABLE_MVPS across components and tests

* Add skip marker to unimplemented test in test_graph_state_model.py

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Lucas Oliveira <62335616+lucaseduoli@users.noreply.github.com>
Co-authored-by: Gabriel Luiz Freitas Almeida <gabriel@langflow.org>
Co-authored-by: Ítalo Johnny <italojohnnydosanjos@gmail.com>
Co-authored-by: cristhianzl <cristhian.lousa@gmail.com>
This commit is contained in:
anovazzi1 2024-09-02 08:53:47 -03:00 committed by GitHub
commit 4ee25359a5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 2071 additions and 1066 deletions

2026
poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -329,7 +329,8 @@ async def build_flow(
build_task = asyncio.create_task(await asyncio.to_thread(_build_vertex, vertex_id, graph))
try:
await build_task
except asyncio.CancelledError:
except asyncio.CancelledError as exc:
logger.exception(exc)
build_task.cancel()
return

View file

@ -31,7 +31,7 @@ from langflow.graph.graph.utils import (
)
from langflow.graph.schema import InterfaceComponentTypes, RunOutputs
from langflow.graph.vertex.base import Vertex, VertexStates
from langflow.graph.vertex.schema import NodeData
from langflow.graph.vertex.schema import NodeData, NodeTypeEnum
from langflow.graph.vertex.types import ComponentVertex, InterfaceVertex, StateVertex
from langflow.logging.logger import LogConfig, configure
from langflow.schema import Data
@ -1628,6 +1628,8 @@ class Graph:
"""Builds the vertices of the graph."""
vertices: list["Vertex"] = []
for frontend_data in self._vertices:
if frontend_data.get("type") == NodeTypeEnum.NoteNode:
continue
try:
vertex_instance = self.get_vertex(frontend_data["id"])
except ValueError:

View file

@ -1,6 +1,13 @@
from enum import Enum
from typing_extensions import NotRequired, TypedDict
class NodeTypeEnum(str, Enum):
NoteNode = "noteNode"
GenericNode = "genericNode"
class Position(TypedDict):
x: float
y: float
@ -16,3 +23,4 @@ class NodeData(TypedDict):
positionAbsolute: NotRequired[Position]
selected: NotRequired[bool]
parent_node_id: NotRequired[str]
type: NotRequired[NodeTypeEnum]

View file

@ -29,10 +29,10 @@ from langflow.initial_setup.setup import (
)
from langflow.interface.types import get_and_cache_all_types_dict
from langflow.interface.utils import setup_llm_caching
from langflow.logging.logger import configure
from langflow.services.deps import get_cache_service, get_settings_service, get_telemetry_service
from langflow.services.plugins.langfuse_plugin import LangfuseInstance
from langflow.services.utils import initialize_services, teardown_services
from langflow.logging.logger import configure
# Ignore Pydantic deprecation warnings from Langchain
warnings.filterwarnings("ignore", category=PydanticDeprecatedSince20)
@ -133,8 +133,6 @@ def create_app():
allow_headers=["*"],
)
app.add_middleware(JavaScriptMIMETypeMiddleware)
# ! Deactivating this until we find a better solution
# app.add_middleware(RequestCancelledMiddleware)
@app.middleware("http")
async def flatten_query_string_lists(request: Request, call_next):

View file

@ -108,6 +108,7 @@ def test_graph_state_model_serialization():
assert serialized_state_model["chat_input"]["message"]["text"] == "Test Sender Name"
@pytest.mark.skip(reason="Not implemented yet")
def test_graph_state_model_json_schema():
chat_input = ChatInput(_id="chat_input")
chat_input.set(input_value="Test Sender Name")

View file

@ -57,6 +57,7 @@
"moment": "^2.30.1",
"openseadragon": "^4.1.1",
"p-debounce": "^4.0.0",
"pako": "^2.1.0",
"playwright": "^1.44.1",
"react": "^18.3.1",
"react-ace": "^11.0.1",
@ -13071,6 +13072,12 @@
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
"license": "(MIT AND Zlib)"
},
"node_modules/pako": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
"license": "(MIT AND Zlib)"
},
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",

View file

@ -52,6 +52,7 @@
"moment": "^2.30.1",
"openseadragon": "^4.1.1",
"p-debounce": "^4.0.0",
"pako": "^2.1.0",
"playwright": "^1.44.1",
"react": "^18.3.1",
"react-ace": "^11.0.1",

View file

@ -161,3 +161,11 @@ body {
width: 100%;
height: 100%;
}
.react-flow__resize-control.handle {
width: 0.75rem!important;
height: 0.75rem !important;
background-color: white !important;
border-color: var(--border) !important;
z-index: 1000 !important;
border-radius: 20% !important;
}

View file

@ -10,10 +10,20 @@ export default function NodeDescription({
description,
selected,
nodeId,
emptyPlaceholder = "Double Click to Edit Description",
charLimit,
inputClassName,
mdClassName,
style,
}: {
description?: string;
selected: boolean;
nodeId: string;
emptyPlaceholder?: string;
charLimit?: number;
inputClassName?: string;
mdClassName?: string;
style?: React.CSSProperties;
}) {
const [inputDescription, setInputDescription] = useState(false);
const [nodeDescription, setNodeDescription] = useState(description);
@ -31,35 +41,20 @@ export default function NodeDescription({
}, [description]);
return (
<div className="generic-node-desc">
<div
className={cn(
"generic-node-desc",
!inputDescription ? "overflow-auto" : "",
)}
>
{inputDescription ? (
<Textarea
className="nowheel min-h-40"
autoFocus
onBlur={() => {
setInputDescription(false);
setNodeDescription(nodeDescription);
setNode(nodeId, (old) => ({
...old,
data: {
...old.data,
node: {
...old.data.node,
description: nodeDescription,
},
},
}));
}}
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
) {
<>
<Textarea
maxLength={charLimit}
className={cn("nowheel h-full", inputClassName)}
autoFocus
style={style}
onBlur={() => {
setInputDescription(false);
setNodeDescription(nodeDescription);
setNode(nodeId, (old) => ({
@ -72,13 +67,50 @@ export default function NodeDescription({
},
},
}));
}
}}
/>
}}
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);
setNode(nodeId, (old) => ({
...old,
data: {
...old.data,
node: {
...old.data.node,
description: nodeDescription,
},
},
}));
}
}}
/>
{charLimit && (
<div
className={cn(
"text-left text-xs",
(nodeDescription?.length ?? 0) >= charLimit
? "text-error"
: "text-primary",
)}
data-testid="note_char_limit"
>
{nodeDescription?.length ?? 0}/{charLimit}
</div>
)}
</>
) : (
<div
className={cn(
"nodoubleclick generic-node-desc-text cursor-text word-break-break-word",
"nodoubleclick generic-node-desc-text h-full cursor-text word-break-break-word dark:text-note-placeholder",
description === "" || !description ? "font-light italic" : "",
)}
onDoubleClick={(e) => {
@ -87,9 +119,14 @@ export default function NodeDescription({
}}
>
{description === "" || !description ? (
"Double Click to Edit Description"
emptyPlaceholder
) : (
<Markdown className="markdown prose flex flex-col text-primary word-break-break-word dark:prose-invert">
<Markdown
className={cn(
"markdown prose flex h-full w-full flex-col text-primary word-break-break-word dark:prose-invert",
mdClassName,
)}
>
{String(description)}
</Markdown>
)}

View file

@ -17,7 +17,6 @@ export default function NodeName({
const [nodeName, setNodeName] = useState(display_name);
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
const setNode = useFlowStore((state) => state.setNode);
useEffect(() => {
if (!selected) {
setInputName(false);
@ -29,7 +28,7 @@ export default function NodeName({
}, [display_name]);
return inputName ? (
<div>
<div className="w-full">
<InputComponent
onBlur={() => {
setInputName(false);
@ -58,7 +57,7 @@ export default function NodeName({
/>
</div>
) : (
<div className="group flex items-center gap-1">
<div className="group flex w-full items-center gap-1">
<ShadTooltip content={display_name}>
<div
onDoubleClick={(event) => {
@ -68,7 +67,7 @@ export default function NodeName({
event.preventDefault();
}}
data-testid={"title-" + display_name}
className="nodoubleclick generic-node-tooltip-div cursor-text text-primary"
className="nodoubleclick w-full cursor-text truncate text-primary"
>
{display_name}
</div>

View file

@ -287,8 +287,8 @@ export default function GenericNode({
>
<div
className={
"generic-node-title-arrangement rounded-full" +
(!showNode && " justify-center")
"generic-node-title-arrangement " +
(!showNode ? " justify-center" : "")
}
data-testid="generic-node-title-arrangement"
>

View file

@ -0,0 +1,218 @@
import ShadTooltip from "@/components/shadTooltipComponent";
import { Button } from "@/components/ui/button";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
} from "@/components/ui/select-custom";
import { COLOR_OPTIONS } from "@/constants/constants";
import ToolbarSelectItem from "@/pages/FlowPage/components/nodeToolbarComponent/toolbarSelectItem";
import useAlertStore from "@/stores/alertStore";
import useFlowStore from "@/stores/flowStore";
import useFlowsManagerStore from "@/stores/flowsManagerStore";
import { useShortcutsStore } from "@/stores/shortcuts";
import { NodeDataType, noteDataType } from "@/types/flow";
import { classNames, cn, openInNewTab } from "@/utils/utils";
import { cloneDeep, set, take } from "lodash";
import { useState } from "react";
import IconComponent from "../../../components/genericIconComponent";
export default function NoteToolbarComponent({
data,
bgColor,
}: {
data: noteDataType;
bgColor: string;
}) {
const setSuccessData = useAlertStore((state) => state.setSuccessData);
const setNoticeData = useAlertStore((state) => state.setNoticeData);
const nodes = useFlowStore((state) => state.nodes);
const setLastCopiedSelection = useFlowStore(
(state) => state.setLastCopiedSelection,
);
const paste = useFlowStore((state) => state.paste);
const shortcuts = useShortcutsStore((state) => state.shortcuts);
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
const deleteNode = useFlowStore((state) => state.deleteNode);
const setNode = useFlowStore((state) => state.setNode);
function openDocs() {
if (data.node?.documentation) {
return openInNewTab(data.node?.documentation);
}
setNoticeData({
title: `${data.id} docs is not available at the moment.`,
});
}
const handleSelectChange = (event) => {
switch (event) {
case "documentation":
openDocs();
break;
case "delete":
takeSnapshot();
deleteNode(data.id);
break;
case "copy":
const node = nodes.filter((node) => node.id === data.id);
setLastCopiedSelection({ nodes: cloneDeep(node), edges: [] });
break;
case "duplicate":
paste(
{
nodes: [nodes.find((node) => node.id === data.id)!],
edges: [],
},
{
x: 50,
y: 10,
paneX: nodes.find((node) => node.id === data.id)?.position.x,
paneY: nodes.find((node) => node.id === data.id)?.position.y,
},
);
break;
}
};
// the deafult value is allways the first one if none is provided
return (
<>
<div className="w-26 noflow nowheel nopan nodelete nodrag h-10">
<span className="isolate inline-flex rounded-md shadow-sm">
<Popover>
<ShadTooltip content="Color pick">
<PopoverTrigger>
<div>
<div
data-testid="color_picker"
className="relative inline-flex items-center rounded-l-md bg-background px-2 py-2 text-foreground shadow-md ring-1 ring-inset ring-ring transition-all duration-500 ease-in-out hover:bg-muted focus:z-10"
>
<div
style={{
backgroundColor: COLOR_OPTIONS[bgColor],
}}
className="h-4 w-4 rounded-full"
></div>
</div>
</div>
</PopoverTrigger>
</ShadTooltip>
<PopoverContent side="top" className="w-fit px-2 py-2">
<div className="flew-row flex gap-3">
{Object.entries(COLOR_OPTIONS).map(([color, code]) => {
return (
<Button
data-testid={`color_picker_button_${color}`}
unstyled
key={color}
onClick={() => {
setNode(data.id, (old) => ({
...old,
data: {
...old.data,
node: {
...old.data.node,
template: {
...old.data.node?.template,
backgroundColor: color,
},
},
},
}));
}}
>
<div
className={cn(
"h-4 w-4 rounded-full hover:border hover:border-ring",
bgColor === color ? "border-2 border-blue-500" : "",
)}
style={{
backgroundColor: code,
}}
></div>
</Button>
);
})}
</div>
</PopoverContent>
</Popover>
<Select onValueChange={handleSelectChange} value="">
<ShadTooltip content="All" side="top">
<SelectTrigger>
<div>
<div
data-testid="more-options-modal"
className={classNames(
"relative -ml-px inline-flex h-8 w-[2rem] items-center rounded-r-md bg-background text-foreground shadow-md ring-1 ring-inset ring-ring transition-all duration-500 ease-in-out hover:bg-muted focus:z-10",
)}
>
<IconComponent
name="MoreHorizontal"
className="relative left-2 h-4 w-4"
/>
</div>
</div>
</SelectTrigger>
</ShadTooltip>
<SelectContent>
<SelectItem value={"duplicate"}>
<ToolbarSelectItem
shortcut={
shortcuts.find((obj) => obj.name === "Duplicate")?.shortcut!
}
value={"Duplicate"}
icon={"Copy"}
dataTestId="copy-button-modal"
/>
</SelectItem>
<SelectItem value={"copy"}>
<ToolbarSelectItem
shortcut={
shortcuts.find((obj) => obj.name === "Copy")?.shortcut!
}
value={"Copy"}
icon={"Clipboard"}
dataTestId="copy-button-modal"
/>
</SelectItem>
<SelectItem
value={"documentation"}
disabled={data.node?.documentation === ""}
>
<ToolbarSelectItem
shortcut={
shortcuts.find((obj) => obj.name === "Docs")?.shortcut!
}
value={"Docs"}
icon={"FileText"}
dataTestId="docs-button-modal"
/>
</SelectItem>
<SelectItem value={"delete"} className="focus:bg-red-400/[.20]">
<div className="font-red flex text-status-red">
<IconComponent
name="Trash2"
className="relative top-0.5 mr-2 h-4 w-4"
/>{" "}
<span className="">Delete</span>{" "}
<span className="absolute right-2 top-2 flex items-center justify-center rounded-sm px-1 py-[0.2]">
<IconComponent
name="Delete"
className="h-4 w-4 stroke-2 text-red-400"
></IconComponent>
</span>
</div>
</SelectItem>
</SelectContent>
</Select>
</span>
</div>
</>
);
}

View file

@ -0,0 +1,112 @@
import {
COLOR_OPTIONS,
NOTE_NODE_MAX_HEIGHT,
NOTE_NODE_MAX_WIDTH,
NOTE_NODE_MIN_HEIGHT,
NOTE_NODE_MIN_WIDTH,
} from "@/constants/constants";
import { noteDataType } from "@/types/flow";
import { cn } from "@/utils/utils";
import { useEffect, useMemo, useRef, useState } from "react";
import { NodeResizer, NodeToolbar } from "reactflow";
import IconComponent from "../../components/genericIconComponent";
import NodeDescription from "../GenericNode/components/NodeDescription";
import NodeName from "../GenericNode/components/NodeName";
import NoteToolbarComponent from "./NoteToolbarComponent";
function NoteNode({
data,
selected,
}: {
data: noteDataType;
selected: boolean;
}) {
const bgColor =
data.node?.template.backgroundColor ?? Object.keys(COLOR_OPTIONS)[0];
const nodeDiv = useRef<HTMLDivElement>(null);
const [size, setSize] = useState({ width: 0, height: 0 });
//tricky to start the description with the right size
useEffect(() => {
if (nodeDiv.current) {
setSize({
width: nodeDiv.current.offsetWidth - 43,
height: nodeDiv.current.offsetHeight - 80,
});
}
}, []);
const MemoNoteToolbarComponent = useMemo(
() => (
<NodeToolbar>
<NoteToolbarComponent data={data} bgColor={bgColor} />
</NodeToolbar>
),
[data, bgColor],
);
return (
<>
{MemoNoteToolbarComponent}
<NodeResizer
minWidth={NOTE_NODE_MIN_WIDTH}
minHeight={NOTE_NODE_MIN_HEIGHT}
maxHeight={NOTE_NODE_MAX_HEIGHT}
maxWidth={NOTE_NODE_MAX_WIDTH}
onResize={(_, params) => {
const { width, height } = params;
setSize({ width: width - 43, height: height - 80 });
}}
isVisible={selected}
lineClassName="border-[3px] border-border"
/>
<div
data-testid="note_node"
style={{
maxHeight: NOTE_NODE_MAX_HEIGHT,
maxWidth: NOTE_NODE_MAX_WIDTH,
minWidth: NOTE_NODE_MIN_WIDTH,
minHeight: NOTE_NODE_MIN_HEIGHT,
backgroundColor: COLOR_OPTIONS[bgColor],
}}
ref={nodeDiv}
className={cn(
"flex h-full w-full flex-col gap-3 rounded-md border border-b p-5 transition-all",
selected ? "" : "shadow-sm",
)}
>
<div className="flex h-fit w-full items-center align-middle">
<div className="flex w-full gap-2">
<div data-testid="note_icon">
<IconComponent name="SquarePen" className="min-w-fit" />
</div>
<div className="w-11/12">
<NodeName
nodeId={data.id}
selected={selected}
display_name={data.node?.display_name || "Note"}
/>
</div>
</div>
</div>
<div
style={{
width: size.width,
height: size.height,
}}
className="nowheel overflow-auto"
>
<NodeDescription
inputClassName="border-0 ring-transparent resize-none rounded-none shadow-none h-full w-full"
style={{ backgroundColor: COLOR_OPTIONS[bgColor] }}
charLimit={2500}
nodeId={data.id}
selected={selected}
description={data.node?.description}
emptyPlaceholder="Double-click to start typing or enter Markdown..."
/>
</div>
</div>
</>
);
}
export default NoteNode;

View file

@ -12,6 +12,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
return (
<div className="h-full w-full">
<textarea
data-testid="textarea"
className={cn(
"nopan nodelete nodrag noflow textarea-primary nowheel",
className,

View file

@ -898,3 +898,19 @@ export const SHORTCUT_KEYS = ["cmd", "ctrl", "alt", "shift"];
export const SERVER_HEALTH_INTERVAL = 10000;
export const REFETCH_SERVER_HEALTH_INTERVAL = 20000;
export const DRAG_EVENTS_CUSTOM_TYPESS = {
genericnode: "genericNode",
notenode: "noteNode",
};
export const NOTE_NODE_MIN_WIDTH = 324;
export const NOTE_NODE_MIN_HEIGHT = 324;
export const NOTE_NODE_MAX_HEIGHT = 800;
export const NOTE_NODE_MAX_WIDTH = 600;
export const COLOR_OPTIONS = {
indigo: "var(--note-indigo)",
emerald: "var(--note-emerald)",
amber: "var(--note-amber)",
red: "var(--note-red)",
};

View file

@ -2,6 +2,7 @@ import { LANGFLOW_ACCESS_TOKEN } from "@/constants/constants";
import { useCustomApiHeaders } from "@/customization/hooks/use-custom-api-headers";
import useAuthStore from "@/stores/authStore";
import axios, { AxiosError, AxiosInstance, AxiosRequestConfig } from "axios";
import pako from "pako";
import { useContext, useEffect } from "react";
import { Cookies } from "react-cookie";
import { BuildStatus } from "../../constants/enums";

View file

@ -6,6 +6,7 @@ import {
APIObjectType,
Component,
CustomComponentRequest,
PromptTypeAPI,
Users,
VertexBuildTypeAPI,
VerticesOrderTypeAPI,
@ -55,6 +56,176 @@ export async function sendAll(data: sendAllProps) {
return await api.post(`${BASE_URL_API}predict`, data);
}
/**
* Checks the prompt for the code block by sending it to an API endpoint.
* @param {string} name - The name of the field to check.
* @param {string} template - The template string of the prompt to check.
* @param {APIClassType} frontend_node - The frontend node to check.
* @returns {Promise<AxiosResponse<PromptTypeAPI>>} A promise that resolves to an AxiosResponse containing the validation results.
*/
export async function postValidatePrompt(
name: string,
template: string,
frontend_node: APIClassType,
): Promise<AxiosResponse<PromptTypeAPI>> {
return api.post(`${BASE_URL_API}validate/prompt`, {
name,
template,
frontend_node,
});
}
/**
* Fetches a list of JSON files from a GitHub repository and returns their contents as an array of FlowType objects.
*
* @returns {Promise<FlowType[]>} A promise that resolves to an array of FlowType objects.
*/
export async function getExamples(): Promise<FlowType[]> {
const url =
"https://api.github.com/repos/langflow-ai/langflow_examples/contents/examples?ref=main";
const response = await api.get(url);
const jsonFiles = response?.data.filter((file: any) => {
return file.name.endsWith(".json");
});
const contentsPromises = jsonFiles.map(async (file: any) => {
const contentResponse = await api.get(file.download_url);
return contentResponse.data;
});
return await Promise.all(contentsPromises);
}
/**
* Saves a new flow to the database.
*
* @param {FlowType} newFlow - The flow data to save.
* @returns {Promise<any>} The saved flow data.
* @throws Will throw an error if saving fails.
*/
export async function saveFlowToDatabase(newFlow: {
name: string;
id: string;
data: ReactFlowJsonObject | null;
description: string;
style?: FlowStyleType;
is_component?: boolean;
folder_id?: string;
endpoint_name?: string;
}): Promise<FlowType> {
try {
const response = await api.post(`${BASE_URL_API}flows/`, {
name: newFlow.name,
data: newFlow.data,
description: newFlow.description,
is_component: newFlow.is_component,
folder_id: newFlow.folder_id === "" ? null : newFlow.folder_id,
endpoint_name: newFlow.endpoint_name,
});
if (response?.status !== 201) {
throw new Error(`HTTP error! status: ${response?.status}`);
}
return response?.data;
} catch (error) {
console.error(error);
throw error;
}
}
/**
* Reads all flows from the database.
*
* @returns {Promise<any>} The flows data.
* @throws Will throw an error if reading fails.
*/
export async function readFlowsFromDatabase() {
try {
const response = await api.get(`${BASE_URL_API}flows/`);
if (response && response?.status !== 200) {
throw new Error(`HTTP error! status: ${response?.status}`);
}
return response?.data;
} catch (error) {
console.error(error);
throw error;
}
}
export async function uploadFlowsToDatabase(flows: FormData) {
try {
const response = await api.post(`${BASE_URL_API}flows/upload/`, flows);
if (response?.status !== 201) {
throw new Error(`HTTP error! status: ${response?.status}`);
}
return response?.data;
} catch (error) {
console.error(error);
throw error;
}
}
/**
* Deletes a flow from the database.
*
* @param {string} flowId - The ID of the flow to delete.
* @returns {Promise<any>} The deleted flow data.
* @throws Will throw an error if deletion fails.
*/
export async function deleteFlowFromDatabase(flowId: string) {
try {
const response = await api.delete(`${BASE_URL_API}flows/${flowId}`);
if (response.status !== 200) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response?.data;
} catch (error) {
console.error(error);
throw error;
}
}
/**
* Fetches a flow from the database by ID.
*
* @param {number} flowId - The ID of the flow to fetch.
* @returns {Promise<any>} The flow data.
* @throws Will throw an error if fetching fails.
*/
export async function getFlowFromDatabase(flowId: number) {
try {
const response = await api.get(`${BASE_URL_API}flows/${flowId}`);
if (response && response?.status !== 200) {
throw new Error(`HTTP error! status: ${response?.status}`);
}
return response?.data;
} catch (error) {
console.error(error);
throw error;
}
}
/**
* Fetches flow styles from the database.
*
* @returns {Promise<any>} The flow styles data.
* @throws Will throw an error if fetching fails.
*/
export async function getFlowStylesFromDatabase() {
try {
const response = await api.get(`${BASE_URL_API}flow_styles/`);
if (response.status !== 200) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response?.data;
} catch (error) {
console.error(error);
throw error;
}
}
/**
* Fetches the version of the API.
*

View file

@ -23,6 +23,7 @@ import {
CODE_PROMPT_DIALOG_SUBTITLE,
EDIT_CODE_TITLE,
} from "../../constants/constants";
import { postCustomComponent } from "../../controllers/API";
import useAlertStore from "../../stores/alertStore";
import { useDarkStore } from "../../stores/darkStore";
import { CodeErrorDataTypeAPI } from "../../types/api";
@ -52,11 +53,11 @@ export default function CodeAreaModal({
const setErrorData = useAlertStore((state) => state.setErrorData);
const [openConfirmation, setOpenConfirmation] = useState(false);
const codeRef = useRef<ReactAce | null>(null);
const { mutate, isPending } = usePostValidateCode();
const [error, setError] = useState<{
detail: CodeErrorDataTypeAPI;
} | null>(null);
const { mutate: validateCode } = usePostValidateCode();
const { mutate: validateComponentCode } = usePostValidateComponentCode();
useEffect(() => {
@ -68,7 +69,7 @@ export default function CodeAreaModal({
}, []);
function processNonDynamicField() {
validateCode(
mutate(
{ code },
{
onSuccess: (apiReturn) => {
@ -256,7 +257,9 @@ export default function CodeAreaModal({
</div>
</div>
<ConfirmationModal
onClose={setOpenConfirmation}
onClose={() => {
setOpenConfirmation(false);
}}
onEscapeKeyDown={(e) => {
e.stopPropagation();
setOpenConfirmation(false);

View file

@ -1,8 +1,14 @@
import NoteNode from "@/CustomNodes/NoteNode";
import IconComponent from "@/components/genericIconComponent";
import LoadingComponent from "@/components/loadingComponent";
import ShadTooltip from "@/components/shadTooltipComponent";
import { useGetBuildsQuery } from "@/controllers/API/queries/_builds";
import { track } from "@/customization/utils/analytics";
import useAutoSaveFlow from "@/hooks/flows/use-autosave-flow";
import useUploadFlow from "@/hooks/flows/use-upload-flow";
import { getNodeRenderType, isSupportedNodeTypes } from "@/utils/utils";
import { ENABLE_MVPS } from "@/customization/feature-flags";
import _, { cloneDeep } from "lodash";
import {
KeyboardEvent,
@ -16,6 +22,7 @@ import { useHotkeys } from "react-hotkeys-hook";
import ReactFlow, {
Background,
Connection,
ControlButton,
Controls,
Edge,
NodeDragHandler,
@ -55,6 +62,7 @@ import isWrappedWithClass from "./utils/is-wrapped-with-class";
const nodeTypes = {
genericNode: GenericNode,
noteNode: NoteNode,
};
export default function Page({ view }: { view?: boolean }): JSX.Element {
@ -317,7 +325,7 @@ export default function Page({ view }: { view?: boolean }): JSX.Element {
const onDragOver = useCallback((event: React.DragEvent) => {
event.preventDefault();
if (event.dataTransfer.types.some((types) => types === "nodedata")) {
if (event.dataTransfer.types.some((types) => isSupportedNodeTypes(types))) {
event.dataTransfer.dropEffect = "move";
} else {
event.dataTransfer.dropEffect = "copy";
@ -327,12 +335,16 @@ export default function Page({ view }: { view?: boolean }): JSX.Element {
const onDrop = useCallback(
(event: React.DragEvent) => {
event.preventDefault();
if (event.dataTransfer.types.some((types) => types === "nodedata")) {
if (event.dataTransfer.types.some((type) => isSupportedNodeTypes(type))) {
takeSnapshot();
const datakey = event.dataTransfer.types.find((type) =>
isSupportedNodeTypes(type),
);
// Extract the data from the drag event and parse it as a JSON object
const data: { type: string; node?: APIClassType } = JSON.parse(
event.dataTransfer.getData("nodedata"),
event.dataTransfer.getData(datakey!),
);
track(`Component Added: ${data.node?.display_name}`);
@ -341,7 +353,7 @@ export default function Page({ view }: { view?: boolean }): JSX.Element {
const newNode: NodeType = {
id: newId,
type: "genericNode",
type: getNodeRenderType(datakey!),
position: { x: 0, y: 0 },
data: {
...data,
@ -470,7 +482,62 @@ export default function Page({ view }: { view?: boolean }): JSX.Element {
>
<Background className="" />
{!view && (
<Controls className="fill-foreground stroke-foreground text-primary [&>button]:border-b-border [&>button]:bg-muted hover:[&>button]:bg-border"></Controls>
<Controls className="fill-foreground stroke-foreground text-primary [&>button]:border-b-border [&>button]:bg-muted hover:[&>button]:bg-border">
{ENABLE_MVPS && (
<ControlButton
data-testid="add_note"
onClick={() => {
const wrapper = reactFlowWrapper.current!;
const viewport = reactFlowInstance?.getViewport();
const x = wrapper.getBoundingClientRect().width / 2;
const y = wrapper.getBoundingClientRect().height / 2;
const nodePosition =
reactFlowInstance?.screenToFlowPosition({ x, y })!;
const data = {
node: {
description: "",
display_name: "",
documentation: "",
template: {},
},
type: "note",
};
const newId = getNodeId(data.type);
const newNode: NodeType = {
id: newId,
type: "noteNode",
position: { x: 0, y: 0 },
data: {
...data,
id: newId,
},
};
paste(
{ nodes: [newNode], edges: [] },
{
x: nodePosition.x,
y: nodePosition?.y,
paneX: wrapper.getBoundingClientRect().x,
paneY: wrapper.getBoundingClientRect().y,
},
);
}}
className="postion absolute -top-10 rounded-sm"
>
<ShadTooltip content="Add note">
<div>
<IconComponent
name="SquarePen"
aria-hidden="true"
className="scale-125"
/>
</div>
</ShadTooltip>
</ControlButton>
)}
</Controls>
)}
<SelectionMenu
lastSelection={lastSelection}

View file

@ -16,6 +16,7 @@ import { APIClassType, APIObjectType } from "../../../../types/api";
import { nodeIconsLucide } from "../../../../utils/styleUtils";
import ParentDisclosureComponent from "../ParentDisclosureComponent";
import { SidebarCategoryComponent } from "./SidebarCategoryComponent";
import { sortKeys } from "./utils";
export default function ExtraSidebar(): JSX.Element {
@ -41,7 +42,7 @@ export default function ExtraSidebar(): JSX.Element {
crt.classList.add("cursor-grabbing");
document.body.appendChild(crt);
event.dataTransfer.setDragImage(crt, 0, 0);
event.dataTransfer.setData("nodedata", JSON.stringify(data));
event.dataTransfer.setData("genericNode", JSON.stringify(data));
}
// Handle showing components after use search input
@ -218,12 +219,14 @@ export default function ExtraSidebar(): JSX.Element {
</div>
</div>
<Separator />
<div className="side-bar-components-div-arrangement">
<div className="parent-disclosure-arrangement">
<div className="flex items-center gap-4 align-middle">
<span className="parent-disclosure-title">Components</span>
</div>
</div>
<Separator />
{Object.keys(dataFilter)
.sort(sortKeys)
.filter((x) => PRIORITY_SIDEBAR_ORDER.includes(x))

View file

@ -0,0 +1,34 @@
import { APIClassType } from "@/types/api";
import IconComponent from "../../../../../components/genericIconComponent";
export default function NoteDraggableComponent() {
function onDragStart(event: React.DragEvent<any>): void {
const noteNode: APIClassType = {
description: "",
display_name: "",
documentation: "",
template: {},
};
event.dataTransfer.setData(
"noteNode",
JSON.stringify({ node: noteNode, type: "note" }),
);
}
return (
<div
draggable
className={"cursor-grab rounded-l-md bg-background p-2"}
onDragStart={onDragStart}
>
<div
data-testid={"note_component"}
id={"note component"}
className="flex w-full items-center justify-between rounded-md border border-dashed border-ring bg-white px-3 py-1 text-sm"
>
<IconComponent name="StickyNote" className="pr-2" />
<span className="side-bar-components-text">Add Note</span>
<IconComponent name="Menu" className="side-bar-components-icon" />
</div>
</div>
);
}

View file

@ -18,20 +18,20 @@ export default function useShortcuts({
ungroup,
minimizeFunction,
}: {
showOverrideModal: boolean;
showModalAdvanced: boolean;
openModal: boolean;
showconfirmShare: boolean;
FreezeAllVertices: () => void;
Freeze: () => void;
downloadFunction: () => void;
displayDocs: () => void;
saveComponent: () => void;
showAdvance: () => void;
handleCodeModal: () => void;
shareComponent: () => void;
ungroup: () => void;
minimizeFunction: () => void;
showOverrideModal?: boolean;
showModalAdvanced?: boolean;
openModal?: boolean;
showconfirmShare?: boolean;
FreezeAllVertices?: () => void;
Freeze?: () => void;
downloadFunction?: () => void;
displayDocs?: () => void;
saveComponent?: () => void;
showAdvance?: () => void;
handleCodeModal?: () => void;
shareComponent?: () => void;
ungroup?: () => void;
minimizeFunction?: () => void;
}) {
const advanced = useShortcutsStore((state) => state.advanced);
const minimize = useShortcutsStore((state) => state.minimize);
@ -45,59 +45,71 @@ export default function useShortcuts({
const freezeAll = useShortcutsStore((state) => state.FreezePath);
function handleFreezeAll(e: KeyboardEvent) {
if (isWrappedWithClass(e, "noflow")) return;
if (isWrappedWithClass(e, "noflow") || !FreezeAllVertices) return;
e.preventDefault();
FreezeAllVertices();
}
function handleFreeze(e: KeyboardEvent) {
if (isWrappedWithClass(e, "noflow")) return;
if (isWrappedWithClass(e, "noflow") || !Freeze) return;
e.preventDefault();
Freeze();
}
function handleDownloadWShortcut(e: KeyboardEvent) {
if (!downloadFunction) return;
e.preventDefault();
downloadFunction();
}
function handleDocsWShortcut(e: KeyboardEvent) {
if (!displayDocs) return;
e.preventDefault();
displayDocs();
}
function handleSaveWShortcut(e: KeyboardEvent) {
if (isWrappedWithClass(e, "noflow") && !showOverrideModal) return;
if (
(isWrappedWithClass(e, "noflow") && !showOverrideModal) ||
!saveComponent
)
return;
e.preventDefault();
saveComponent();
}
function handleAdvancedWShortcut(e: KeyboardEvent) {
//check if there is another modal open
if (isWrappedWithClass(e, "noflow") && !showModalAdvanced) return;
if ((isWrappedWithClass(e, "noflow") && !showModalAdvanced) || !showAdvance)
return;
e.preventDefault();
showAdvance();
}
function handleCodeWShortcut(e: KeyboardEvent) {
if (isWrappedWithClass(e, "noflow") && !openModal) return;
if ((isWrappedWithClass(e, "noflow") && !openModal) || !handleCodeModal)
return;
e.preventDefault();
handleCodeModal();
}
function handleShareWShortcut(e: KeyboardEvent) {
if (isWrappedWithClass(e, "noflow") && !showconfirmShare) return;
if (
(isWrappedWithClass(e, "noflow") && !showconfirmShare) ||
!shareComponent
)
return;
e.preventDefault();
shareComponent();
}
function handleGroupWShortcut(e: KeyboardEvent) {
if (isWrappedWithClass(e, "noflow")) return;
if (isWrappedWithClass(e, "noflow") || !ungroup) return;
e.preventDefault();
ungroup();
}
function handleMinimizeWShortcut(e: KeyboardEvent) {
if (isWrappedWithClass(e, "noflow")) return;
if (isWrappedWithClass(e, "noflow") || !minimizeFunction) return;
e.preventDefault();
minimizeFunction();
}

View file

@ -68,7 +68,6 @@ export default function NodeToolbarComponent({
const hasApiKey = useStoreStore((state) => state.hasApiKey);
const validApiKey = useStoreStore((state) => state.validApiKey);
const shortcuts = useShortcutsStore((state) => state.shortcuts);
const unselectAll = useFlowStore((state) => state.unselectAll);
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
const [openModal, setOpenModal] = useState(false);
const isGroup = data.node?.flow ? true : false;
@ -254,9 +253,6 @@ export default function NodeToolbarComponent({
break;
case "disabled":
break;
case "unselect":
unselectAll();
break;
case "ungroup":
handleungroup();
break;
@ -313,7 +309,6 @@ export default function NodeToolbarComponent({
};
const hasCode = Object.keys(data.node!.template).includes("code");
const [deleteIsFocus, setDeleteIsFocus] = useState(false);
return (
<>
@ -576,12 +571,7 @@ export default function NodeToolbarComponent({
dataTestId="download-button-modal"
/>
</SelectItem>
<SelectItem
value={"delete"}
className="focus:bg-red-400/[.20]"
onFocus={() => setDeleteIsFocus(true)}
onBlur={() => setDeleteIsFocus(false)}
>
<SelectItem value={"delete"} className="focus:bg-red-400/[.20]">
<div className="font-red flex text-status-red">
<IconComponent
name="Trash2"
@ -589,9 +579,7 @@ export default function NodeToolbarComponent({
/>{" "}
<span className="">Delete</span>{" "}
<span
className={`absolute right-2 top-2 flex items-center justify-center rounded-sm px-1 py-[0.2] ${
deleteIsFocus ? " " : "bg-muted"
}`}
className={`absolute right-2 top-2 flex items-center justify-center rounded-sm px-1 py-[0.2]`}
>
<IconComponent
name="Delete"

View file

@ -342,7 +342,7 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
// Create a new node object
const newNode: NodeType = {
id: newId,
type: "genericNode",
type: node.type,
position: {
x: insidePosition.x + node.position!.x - minimumX,
y: insidePosition.y + node.position!.y - minimumY,

View file

@ -339,7 +339,7 @@
@apply h-8 w-8 rounded;
}
.generic-node-tooltip-div {
@apply ml-2 flex max-w-[220px] gap-1 truncate;
@apply ml-2 w-full truncate flex;
}
.generic-node-validation-div {
@apply max-h-96 overflow-auto;
@ -394,7 +394,7 @@
@apply hover:text-accent-foreground hover:transition-all;
}
.generic-node-desc {
@apply mb-4 h-full w-full px-5 text-foreground;
@apply pb-4 h-full w-full px-5 text-foreground;
}
.generic-node-desc-text {
@apply w-full text-sm text-muted-foreground;
@ -1182,4 +1182,7 @@
.store-beta-icon {
@apply relative bottom-3 left-1 ml-2 rounded-full bg-beta-background px-2 py-1 text-center text-xs font-semibold text-beta-foreground;
}
.color-option-container {
@apply w-fit
}
}

View file

@ -69,9 +69,19 @@
--status-blue: #2563eb;
--status-gray: #6b7280;
--connection: #555;
--note-indigo: #e0e7ff;
--note-emerald: #d1fae5;
--note-amber: #fef3c7;
--note-red: #fee2e2;
}
.dark {
--note-indigo: #312e81;
--note-emerald: #064e3b;
--note-amber: #78350f;
--note-red: #7f1d1d;
--note-placeholder: 216 12% 84%; /* hsl(216 12% 84%) */
--node-selected: 234 89% 74%;
--background: 224 28% 7.5%; /* hsl(224 10% 7.5%) */
--foreground: 213 31% 80%; /* hsl(213 31% 91%) */

View file

@ -30,6 +30,21 @@ export type NodeType = {
selected?: boolean;
};
export interface noteClassType
extends Pick<APIClassType, "description" | "display_name" | "documentation"> {
template: {
backgroundColor: string;
[key: string]: any;
};
}
export interface noteDataType
extends Pick<NodeDataType, "showNode" | "type" | "id"> {
showNode?: boolean;
type: string;
node?: noteClassType;
id: string;
}
export type NodeDataType = {
showNode?: boolean;
type: string;

View file

@ -789,7 +789,10 @@ export function checkEdgeWithoutEscapedHandleIds(edges: Edge[]): boolean {
}
export function checkOldNodesOutput(nodes: NodeType[]): boolean {
return nodes.some((node) => !node.data.node?.outputs);
return nodes.some(
(node) =>
node.data.node?.outputs === undefined && node.type === "genericNode",
);
}
export function customStringify(obj: any): string {

View file

@ -26,6 +26,7 @@ import {
ChevronRight,
ChevronRightSquare,
ChevronUp,
ChevronsDownUp,
ChevronsLeft,
ChevronsRight,
ChevronsUpDown,
@ -137,6 +138,7 @@ import {
Sparkles,
Square,
SquarePen,
StickyNote,
Store,
SunIcon,
Table,
@ -585,6 +587,8 @@ export const nodeIconsLucide: iconsType = {
Command,
ArrowBigUp,
Dot,
StickyNote,
note: StickyNote,
RotateCcw,
Wrench,
FolderPlusIcon,
@ -596,6 +600,7 @@ export const nodeIconsLucide: iconsType = {
MistralAI: MistralIcon,
Upstash: UpstashSvgIcon,
PGVector: CpuIcon,
ChevronsDownUp,
Confluence: ConfluenceIcon,
FreezeAll: freezeAllIcon,
Table: Table,

View file

@ -4,6 +4,7 @@ import clsx, { ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
import TableAutoCellRender from "../components/tableComponent/components/tableAutoCellRender";
import {
DRAG_EVENTS_CUSTOM_TYPESS,
MESSAGES_TABLE_ORDER,
MODAL_CLASSES,
SHORTCUT_KEYS,
@ -621,3 +622,11 @@ export function addPlusSignes(array: string[]): string[] {
return "+" + key;
});
}
export function isSupportedNodeTypes(type: string) {
return Object.keys(DRAG_EVENTS_CUSTOM_TYPESS).some((key) => key === type);
}
export function getNodeRenderType(MIMEtype: string) {
return DRAG_EVENTS_CUSTOM_TYPESS[MIMEtype];
}

View file

@ -67,6 +67,7 @@ const config = {
"dark-blue": "var(--dark-blue)",
"dark-gray": "var(--dark-gray)",
"dark-red": "var(--dark-red)",
"note-placeholder": "var(--note-placeholder)",
error: {
DEFAULT: "var(--error)",
background: "var(--error-background)",

View file

@ -222,6 +222,8 @@ test("user must be able to stop a building", async ({ page }) => {
await page.getByTestId("stop_building_button").click();
await page.waitForTimeout(1000);
expect(await page.getByTestId("loading_icon").isHidden()).toBeTruthy();
expect(
await page.getByTestId("stop_building_button").isEnabled(),

View file

@ -0,0 +1,185 @@
import { ENABLE_MVPS } from "@/../../src/customization/feature-flags";
import { expect, test } from "@playwright/test";
import uaParser from "ua-parser-js";
test("user should be able to interact with sticky notes", async ({ page }) => {
// prevent test from failing if feature flag is disabled
if (!ENABLE_MVPS) {
return;
}
await page.goto("/");
await page.waitForSelector('[data-testid="mainpage_title"]', {
timeout: 30000,
});
await page.waitForSelector('[id="new-project-btn"]', {
timeout: 30000,
});
let modalCount = 0;
try {
const modalTitleElement = await page?.getByTestId("modal-title");
if (modalTitleElement) {
modalCount = await modalTitleElement.count();
}
} catch (error) {
modalCount = 0;
}
const getUA = await page.evaluate(() => navigator.userAgent);
const userAgentInfo = uaParser(getUA);
let control = "Control";
if (userAgentInfo.os.name.includes("Mac")) {
control = "Meta";
}
const noteText = `
Artificial Intelligence (AI) has rapidly evolved from a speculative concept in science fiction to a transformative force reshaping industries and everyday life. The term AI encompasses a broad range of technologies, from simple algorithms designed to perform specific tasks to complex systems capable of learning and adapting independently. As AI continues to advance, its applications are becoming increasingly diverse, impacting everything from healthcare to finance, entertainment, and beyond.
At its core, AI is about creating systems that can perform tasks that would typically require human intelligence. This includes abilities such as visual perception, speech recognition, decision-making, and even language translation. The development of AI can be traced back to the mid-20th century, when pioneers like Alan Turing began exploring the idea of machines that could think. Turing's famous "Turing Test" proposed a benchmark for AI, where a machine would be considered intelligent if it could engage in a conversation with a human without being detected as a machine.
The early days of AI research were marked by optimism, with researchers believing that human-like intelligence in machines was just around the corner. However, progress was slower than expected, leading to periods known as "AI winters," where interest and funding in the field waned. Despite these setbacks, AI research persisted, and by the 21st century, significant breakthroughs began to emerge.
One of the key drivers of modern AI is the availability of vast amounts of data. The internet and the proliferation of digital devices have generated unprecedented quantities of data, which AI systems can analyze to identify patterns and make predictions. This data-driven approach is at the heart of machine learning, a subset of AI that focuses on teaching machines to learn from experience rather than relying on explicitly programmed instructions.
Machine learning has enabled remarkable advancements in AI, particularly in areas like image and speech recognition. For example, AI systems can now accurately identify objects in images, transcribe spoken words into text, and even understand natural language. These capabilities have led to the development of virtual assistants like Siri, Alexa, and Google Assistant, which can perform a wide range of tasks, from setting reminders to controlling smart home devices.
Another important development in AI is the rise of deep learning, a type of machine learning that uses artificial neural networks to model complex patterns in data. Deep learning has been instrumental in achieving breakthroughs in areas such as computer vision and natural language processing. For instance, deep learning algorithms power the facial recognition systems used in security applications and the language models behind advanced chatbots and translation services.
AI's impact is not limited to consumer applications; it is also transforming industries on a larger scale. In healthcare, AI is being used to analyze medical images, predict patient outcomes, and even discover new drugs. In finance, AI-driven algorithms are used for trading, fraud detection, and personalized financial advice. The automotive industry is leveraging AI to develop self-driving cars, which have the potential to reduce accidents and revolutionize transportation.
Despite its many benefits, AI also raises important ethical and societal questions. As AI systems become more capable, there are concerns about job displacement, privacy, and the potential for bias in decision-making. AI algorithms are only as good as the data they are trained on, and if that data is biased, the AI's decisions may be biased as well. This has led to calls for greater transparency and accountability in AI development, as well as discussions about the need for regulations to ensure that AI is used responsibly.
The future of AI is both exciting and uncertain. As the technology continues to advance, it will undoubtedly bring about profound changes in society. The challenge will be to harness AI's potential for good while addressing the ethical and societal issues that arise. Whether it's through smarter healthcare, more efficient transportation, or enhanced creativity, AI has the potential to reshape the world in ways we are only beginning to imagine. The journey of AI is far from over, and its impact will be felt for generations to come.
`;
const randomTitle = Math.random().toString(36).substring(7);
while (modalCount === 0) {
await page.getByText("New Project", { exact: true }).click();
await page.waitForTimeout(3000);
modalCount = await page.getByTestId("modal-title")?.count();
}
await page.waitForSelector('[data-testid="blank-flow"]', {
timeout: 30000,
});
await page.getByTestId("blank-flow").click();
await page.waitForSelector('[data-testid="extended-disclosure"]', {
timeout: 30000,
});
await page.getByTestId("add_note").click();
await page.waitForTimeout(1000);
const targetElement = await page.locator('//*[@id="react-flow-id"]');
await page.mouse.up();
await page.mouse.down();
await page.waitForSelector('[title="fit view"]', {
timeout: 100000,
});
await page.getByTitle("fit view").click();
await page.getByTitle("zoom out").click();
await page.getByTitle("zoom out").click();
await page.getByTestId("note_node").click();
await page.getByTestId("title-Note").dblclick();
await page.waitForTimeout(1000);
await page.getByTestId("popover-anchor-input-title-Note").fill(randomTitle);
await page.getByTestId("note_icon").first().dblclick();
await page.locator(".generic-node-desc").last().dblclick();
await page.getByTestId("textarea").fill(noteText);
expect(await page.getByText("2500/2500")).toBeVisible();
await page.getByTestId("note_icon").first().dblclick();
const textMarkdown = await page.locator(".markdown").innerText();
const textLength = textMarkdown.length;
const noteTextLength = noteText.length;
expect(textLength).toBeLessThan(noteTextLength);
await page.getByTestId("note_node").click();
let element = await page.getByTestId("note_node");
let hasStyles = await element?.evaluate((el) => {
const style = window.getComputedStyle(el);
return style.backgroundColor === "rgb(224, 231, 255)";
});
expect(hasStyles).toBe(true);
await page.getByTestId("note_node").click();
await page.getByTestId("color_picker").click();
await page.getByTestId("color_picker_button_red").click();
await page.waitForTimeout(1000);
await page.getByTestId("note_node").click();
element = await page.getByTestId("note_node");
hasStyles = await element?.evaluate((el) => {
const style = window.getComputedStyle(el);
return style.backgroundColor === "rgb(254, 226, 226)";
});
expect(hasStyles).toBe(true);
await page.getByTestId("note_node").click();
await page.getByTestId("more-options-modal").click();
await page.getByText("Duplicate").click();
let titleNumber = await page.getByText(randomTitle).count();
expect(titleNumber).toBe(2);
await page.getByTestId("note_node").last().click();
await page.getByTestId("more-options-modal").click();
await page.getByText("Copy").click();
await page.waitForTimeout(1000);
await page.getByTitle("fit view").click();
await page.getByTitle("zoom out").click();
await page.getByTitle("zoom out").click();
targetElement.focus();
targetElement.click();
await page.waitForTimeout(1000);
targetElement.click();
await page.waitForTimeout(1000);
targetElement.click();
await page.keyboard.press(`${control}+v`);
await page.waitForTimeout(1000);
titleNumber = await page.getByText(randomTitle).count();
expect(titleNumber).toBe(3);
await page.getByTestId("note_node").last().click();
await page.getByTestId("more-options-modal").click();
await page.getByText("Delete").last().click();
await page.waitForTimeout(1000);
await page.getByTestId("note_node").last().click();
await page.getByTestId("more-options-modal").click();
await page.getByText("Delete").last().click();
await page.waitForTimeout(1000);
titleNumber = await page.getByText(randomTitle).count();
expect(titleNumber).toBe(1);
});