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:
parent
bc6e918f49
commit
4ee25359a5
34 changed files with 2071 additions and 1066 deletions
2026
poetry.lock
generated
2026
poetry.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
7
src/frontend/package-lock.json
generated
7
src/frontend/package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
112
src/frontend/src/CustomNodes/NoteNode/index.tsx
Normal file
112
src/frontend/src/CustomNodes/NoteNode/index.tsx
Normal 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;
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)",
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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%) */
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)",
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
185
src/frontend/tests/scheduled-end-to-end/sticky-notes.spec.ts
Normal file
185
src/frontend/tests/scheduled-end-to-end/sticky-notes.spec.ts
Normal 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);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue