From 7aca264fec9eb8bfcf17490c17a167217512ac6c Mon Sep 17 00:00:00 2001 From: anovazzi1 Date: Mon, 17 Mar 2025 11:03:59 -0300 Subject: [PATCH] feat: Publish Flow, API code update and UI components (#6140) * refactor: Update flowToolbarComponent with FlowToolbarOptions component and ENABLE_PUBLISH feature flag * [autofix.ci] apply automated fixes * refactor: Update ENABLE_PUBLISH feature flag in feature-flags.ts * add external link for hover state * Refactor routes.tsx to enable PlaygroundPage * Refactor deploy-dropdown.tsx to add ShadTooltipComponent and handle cases where there is no IO * [autofix.ci] apply automated fixes * add colorfull langflow icon * Add playgroundPage prop to IOModalPropsType * Refactor IOModal in PlaygroundPage component * Refactor IOModal component and add publish options * Add LangflowButtonRedirectTarget utility function * Refactor IOModal component and add LangflowButtonRedirectTarget utility function * fix: remove feature flag for playground button name * fix: rename DeployDropdown to PublishDropdown in FlowToolbarOptions * fix: rename DeployDropdown to PublishDropdown and update related functionality * fix: update classNames utility import and refactor class assignment in FlowToolbar * [autofix.ci] apply automated fixes * fix: enhance hover effects and accessibility in PublishDropdown component * [autofix.ci] apply automated fixes * fix: update Playground title in IOModal and ChatViewWrapper components * fix: improve layout and visibility of session information in ChatViewWrapper component * add neutral icon to playground * fix: add playgroundPage prop to ContentBlockDisplay and conditionally render elements in ChatMessage * fix: pass playgroundPage prop to ContentDisplay and conditionally render duration * fix: remove playgroundTitle display from ChatViewWrapper component * fix: adjust padding and alignment in ChatViewWrapper and IOModal components based on playgroundPage prop * fix: update alignment and responsiveness in ChatViewWrapper component based on playgroundPage and sidebarOpen states * fix: update document title based on currentSavedFlow in PlaygroundPage component * [autofix.ci] apply automated fixes * feat: add ENABLE_WIDGET flag to conditionally render embed option in PublishDropdown * feat: add EmbedModal component for copying embed code * feat: integrate EmbedModal in PublishDropdown for embed code sharing * feat: enhance EmbedModal integration in PublishDropdown with dynamic embed code generation * feat: add switch for publishing state in PublishDropdown component and update FlowType * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes * feat: add optional copy property to GetCodeType for enhanced tweak management * feat: make description property optional in modalHeaderType for improved flexibility * feat: update background color in code styles for improved visibility * feat: add support for 'retangular' size in switchCaseModalSize helper * feat: make description property optional in Header component and add 'retangular' size option in BaseModalProps * feat: add optional copy parameter to getWidgetCode for customizable script output * feat: refactor EmbedModal props for improved clarity and consistency * feat: add ApiModal component for custom API code generation * feat: adjust icon size in EmbedModal for better visual consistency * feat: integrate ApiModal into PublishDropdown for enhanced API code generation * feat: update ApiModal header to include API key creation instructions * feat: add APITabsComponent for displaying code snippets in tabs * feat: add APITabType and tabsArrayType for enhanced API tab management * feat: replace CodeTabsComponent with APITabsComponent in ApiModal for improved tab management * feat: add JSIcon component for JavaScript icon representation * feat: add JSIcon to nodeIconsLucide for JavaScript representation * style: format code for consistency in EmbedModal component * feat: change ApiModal size from x-large to medium for better usability * feat: enhance APITabsComponent with light theme support and improve button positioning * feat: adjust APITabsComponent tab styling for improved layout and readability * feat: add BWPython icon and update references in APITabsComponent and styleUtils * fix: correct export name for BWSvgPython icon component * feat: add new column 'public' to flow table * raw: Sample code for creating a new unauthenticated endpoint as a PoCh Note: this code is blatantly copied from another code snippet. * feat: add dark mode support to JSIcon component * feat: add dark mode support to BWPython icon component * feat: add new column 'access_type' to flow * chore: update code * chore: remove unnecessary migration * feat: add getNewCurlCode function for generating cURL commands with dynamic payloads * feat: integrate dynamic code generation for Python, JavaScript, and cURL in APITabsComponent * feat: add getNewJsApiCode function for generating JavaScript API code with dynamic payloads * feat: add getNewPythonApiCode function for generating Python API code with dynamic payloads * fix color on sintax highlight * feat: enhance APITabsComponent with dynamic streaming and authentication state * feat: update APITabsComponent to handle dynamic input and output types based on flow data * [autofix.ci] apply automated fixes * feat: add input and output validation in PlaygroundPage to redirect if none exist * [autofix.ci] apply automated fixes * fix: ensure navigation only occurs when currentSavedFlow data is present * feat: add access_type field to FlowUpdate model * feat: add playgroundPage parameter to buildFlowVertices for conditional URL construction * feat: add playgroundPage state and setter to FlowStoreType * feat: add playgroundPage state and setter to FlowStoreType * feat: add access_type field to FlowType for improved access control * feat: modify useGetMessagesQuery to handle playgroundPage state for conditional message retrieval * feat: update IPatchUpdateFlow interface to make fields optional and add access_type for enhanced flexibility * feat: implement publish toggle functionality in PublishDropdown component for dynamic access control * feat: integrate playgroundPage state management in IOModal for improved session handling * add: new endpoint to public_flow * refactor: remove unused current_user parameter from read_public_flow function * feat: add ContextWrapper to PlaygroundPage route for improved context management * refactor: simplify flow retrieval logic in PlaygroundPage component * feat: add support for public flow retrieval in useGetFlow hook * feat: add PUBLIC_FLOW constant to URLs for public flow retrieval * fix: add whitespace for improved code readability in AuthSettingsGuard component * fix: add whitespace for improved code readability in ProtectedAdminRoute component * [autofix.ci] apply automated fixes * fix: update redirect condition in PlaygroundPage for non-public access types * [autofix.ci] apply automated fixes * persist session name update * fix: enhance message update logic to handle playground state and local storage * fix: remove debugger statement from PlaygroundPage initialization * fix: manage dark mode class in App component and remove redundant logic from AppInitPage * [autofix.ci] apply automated fixes * feat: add access_type field to FlowHeader model for flow access control * fix: refactor flow access handling in PublishDropdown component for improved readability and async operation * feat: enhance FlowMenu component with swatch color display based on flow gradient * feat: add swatch color display in IOModal based on flow gradient * [autofix.ci] apply automated fixes * Update copyCode to use dynamic API code generation for Python, JavaScript, and cURL tabs * Add optional session tracking to JavaScript API code generation * Enhance Python API code generation with detailed comments and error handling * Fix SVG attribute casing and update dark mode state handling in Python icon components * Fix SVG clip-path casing and ensure dark mode state is a string in JS icon components * Fix SVG fill color handling for dark mode in Python icon components * Fix SVG filter handling for dark mode in JS icon components * Add tweaks management and update functionality in tweaks store * Add normal font style to line numbers in CSS * Add debug log for flow retrieval in FlowPage component * Refactor APITabsComponent to remove unused props and integrate tweaks management * Enhance ApiModal to support tweaks management and improve API access UI * Remove flow prop from ApiModal in PublishDropdown component * update package lock * [autofix.ci] apply automated fixes * Update ChatViewWrapper to adjust layout based on visibleSession state * [autofix.ci] apply automated fixes * Update icon fallback in FlowMenu to use "Workflow" * Refactor EmbedModal button styles for consistency and clarity * Add useEffect to reset copied state on active tab change and clean up button styles * Comment out DropdownMenuItem in PublishDropdown for future reference * refactor: remove duplicated code from route * refactor: remove duplicated code from route * [autofix.ci] apply automated fixes * Increase font size for code blocks in classes.css for better readability * Adjust padding and height for deploy dropdown items for improved layout * Update minWidth for medium modal size to include max-width constraint * Refactor ApiModal to conditionally render button and adjust styles for improved layout * Add margin-top to API modal tabs content for improved spacing * Refactor deploy dropdown to include API access and Embed options, and rename 'Standalone app' to 'Shareable Playground' * Enhance API code generation to include environment variable checks for API key in curl, JavaScript, and Python examples * Fix authentication check logic and clean up modal class names * Refactor authentication logic in APITabsComponent to improve clarity and functionality * [autofix.ci] apply automated fixes * Update environment variable references in API code examples to use LANGFLOW_API_KEY * [autofix.ci] apply automated fixes * Update API key references to use LANGFLOW_API_KEY in curl and JS code examples * Remove streaming parameter from API code examples in JavaScript and Python * Adjust button padding and separator margin in API modal for improved layout * Add transparent background to scrollbar corner in Tailwind config * Update publish dropdown to display sharing status based on flow publication state * Add playgroundPage prop to ChatInput and conditionally render file upload button * Refactor ChatViewWrapper layout logic for improved responsiveness * [autofix.ci] apply automated fixes * Add closeButtonClassName prop to BaseModal and DialogContent for customization * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes * feat: use new deterministic flow id for public flow calls (#6314) * Enhance public flow building with deterministic flow ID and name handling * Handle asyncio.CancelledError in message table commit operation * Add UUID v5 generation for flow IDs in chat view and modal components * Add comment explaining CancelledError handling in message table commit * Simplify public flow ID and name generation logic * Refactor UUID v5 import and flow ID generation in IOModal components * Update Alembic migration down_revision identifier --------- Co-authored-by: anovazzi1 * feat: add playgroundPage prop to SessionSelector and SidebarOpenView components * feat: switch from localStorage to sessionStorage for message handling in playground mode * feat: Add public flow settings with cleanup and expiration configurations Add new settings for managing public temporary flows, including: - Configurable cleanup interval (default 1 hour) - Configurable flow expiration time (default 24 hours) - Minimum interval of 10 minutes for both settings * feat: Add public flow expiration settings to ConfigResponse Update ConfigResponse schema to include new configuration parameters for public flow management: - public_flow_cleanup_interval: Interval for cleaning up public flows - public_flow_expiration: Duration for public flow retention * feat: Add temporary public flow cleanup worker Implement a background worker to manage and clean up expired public flows: - Add CleanupWorker class to handle periodic cleanup tasks - Integrate cleanup worker into application lifespan - Implement cleanup logic for removing expired public flow data from database and storage - Add start and stop methods for graceful worker management * feat: implement client ID management using cookies in Playground and update flow ID generation * refactor: Optimize public flow cleanup worker with targeted execution Improve the temporary public flow cleanup process by: - Adding a pre-check to only run cleanup when public flows exist - Passing public flows and session directly to cleanup function - Simplifying the cleanup logic to reduce nested session management * refactor: Enhance database cleanup worker with comprehensive record management Improve the cleanup worker to handle both expired public flows and orphaned records: - Add function to clean up expired public flows with detailed logging - Implement orphaned record cleanup across multiple database tables - Enhance error handling and logging for storage file deletion - Simplify worker run method to execute both cleanup tasks sequentially * feat: Add session cookie validation for public flow generation Enhance public flow building by: - Requiring a session cookie for generating temporary public flows - Incorporating the session cookie into the flow ID generation process - Adding explicit error handling for missing session cookies * fix: Update session cookie retrieval in public flow generation Change cookie key from "session" to "client_id" to align with recent client ID management implementation * fix: Correct flow ID generation by adding an underscore separator between client ID and real flow ID * [autofix.ci] apply automated fixes * fix: add options to the fetch call and add docs Update getNewJsApiCode function to: - Add comprehensive JSDoc documentation - Include fetch options in API call - Simplify code generation logic - Ensure proper payload and options handling * fix: update label for temporary overrides to tweaks in API modal * update package lock * ensure individual instances of contexts * fix: add data-testid attributes for testing in PublishDropdown component * fix: handle authentication errors for public API requests in ApiInterceptor * test: add publish feature test using Playwright * [autofix.ci] apply automated fixes * refactor: optimize temp flow cleanup with improved file and logging management * test: add unit tests for temp flow cleanup service * chore: remove unnecessary console logs and comments for cleaner code * Update src/frontend/tests/core/features/publish-flow.spec.ts Co-authored-by: Cristhian Zanforlin Lousa * Update src/frontend/tests/core/features/publish-flow.spec.ts Co-authored-by: Cristhian Zanforlin Lousa * Update src/frontend/tests/core/features/publish-flow.spec.ts Co-authored-by: Cristhian Zanforlin Lousa * Update src/frontend/tests/core/features/publish-flow.spec.ts Co-authored-by: Cristhian Zanforlin Lousa * Update src/frontend/tests/core/features/publish-flow.spec.ts Co-authored-by: Cristhian Zanforlin Lousa * Update src/frontend/tests/core/features/publish-flow.spec.ts Co-authored-by: Cristhian Zanforlin Lousa * Update src/frontend/tests/core/features/publish-flow.spec.ts Co-authored-by: Cristhian Zanforlin Lousa * Update src/frontend/src/modals/apiModal/utils/get-python-api-code.tsx Co-authored-by: Cristhian Zanforlin Lousa * Update src/frontend/tests/core/features/publish-flow.spec.ts Co-authored-by: Cristhian Zanforlin Lousa * Update src/frontend/tests/core/features/publish-flow.spec.ts Co-authored-by: Cristhian Zanforlin Lousa * Update src/frontend/src/modals/apiModal/utils/get-curl-code.tsx Co-authored-by: Cristhian Zanforlin Lousa * Update src/frontend/src/modals/apiModal/utils/get-curl-code.tsx Co-authored-by: Cristhian Zanforlin Lousa * Update src/frontend/src/modals/apiModal/utils/get-curl-code.tsx Co-authored-by: Cristhian Zanforlin Lousa * Update src/frontend/src/modals/apiModal/utils/get-python-api-code.tsx Co-authored-by: Cristhian Zanforlin Lousa * Update src/frontend/src/modals/apiModal/utils/get-python-api-code.tsx Co-authored-by: Cristhian Zanforlin Lousa * fix: adds edge filtering only to parameters not hidden (#6270) Adds advanced filter to only filter by showing fields on click of edge * Add config parameter to Graph run method with configuration application * fix: remove unnecessary await from flow directory existence check * refactor: Remove expired public flows cleanup method * refactor: Move data_dir initialization to base StorageService * fix: Update temp flow cleanup to use async file operations - Add explicit type hint for storage_service - Use async methods for file and directory existence checks - Improve error handling for file deletion during flow cleanup * refactor: Add explicit type hint for tables in temp flow cleanup - Improve type annotations for tables list in cleanup_orphaned_records - Explicitly define the types of tables to be processed * [autofix.ci] apply automated fixes * refactor: Remove tests for expired public flows from temp flow cleanup * test: Update test_cleanup_orphaned_records_no_orphans to use fixtures * feat: Add utility function for verifying public flow access Implement async function to validate public flow requests with: - Client ID verification - Flow existence and public access check - Deterministic flow ID generation - User retrieval for permission handling * feat: build_public_tmp to use the jobqueue Refactored the build_public_tmp endpoint to: - Add comprehensive docstring explaining endpoint functionality - Improve error handling and logging - Simplify flow verification and user retrieval process - Use new verify_public_flow_and_get_user utility function - Streamline job creation and error management * chore: Add anyio import to local storage service Import anyio library in preparation for potential async storage operations * style: run formatter * changed endpoint * [autofix.ci] apply automated fixes * Add size for tweaks * Add size for tweaks * Change tweaks modal * Fix switch design * [autofix.ci] apply automated fixes * fix: mypy erros * fix: alembic multiple heads error * fix: ruff error * refactor: update test cleanup for orphaned records to use fixtures Changed the test for cleanup of orphaned records to utilize the "client" fixture instead of the "asyncio" marker, enhancing test organization and clarity. * fix: cli test --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: italojohnny Co-authored-by: Gabriel Luiz Freitas Almeida Co-authored-by: Cristhian Zanforlin Lousa Co-authored-by: Lucas Oliveira <62335616+lucaseduoli@users.noreply.github.com> Co-authored-by: Lucas Oliveira --- ...d1f1002d_add_column_access_type_to_flow.py | 33 + src/backend/base/langflow/api/build.py | 12 +- src/backend/base/langflow/api/utils.py | 60 + src/backend/base/langflow/api/v1/chat.py | 120 +- src/backend/base/langflow/api/v1/flows.py | 18 +- src/backend/base/langflow/api/v1/schemas.py | 11 +- src/backend/base/langflow/graph/graph/base.py | 3 + src/backend/base/langflow/main.py | 1 + src/backend/base/langflow/memory.py | 13 +- .../services/database/models/flow/model.py | 23 +- .../base/langflow/services/settings/base.py | 16 +- .../base/langflow/services/storage/local.py | 1 - .../base/langflow/services/storage/service.py | 3 + .../services/task/temp_flow_cleanup.py | 136 ++ .../tests/unit/services/tasks/__init__.py | 0 .../services/tasks/test_temp_flow_cleanup.py | 109 ++ src/frontend/package-lock.json | 1228 ----------------- src/frontend/src/App.tsx | 11 +- src/frontend/src/assets/LangflowLogoColor.svg | 9 + .../components/FlowMenu/index.tsx | 15 +- .../chatComponents/ContentBlockDisplay.tsx | 16 +- .../core/chatComponents/ContentDisplay.tsx | 4 +- .../components/deploy-dropdown.tsx | 208 +++ .../components/flow-toolbar-options.tsx | 23 + .../components/playground-button.tsx | 12 +- .../core/flowToolbarComponent/index.tsx | 117 +- src/frontend/src/components/ui/dialog.tsx | 83 +- src/frontend/src/constants/constants.ts | 1 + src/frontend/src/controllers/API/api.tsx | 5 +- .../src/controllers/API/helpers/constants.ts | 1 + .../API/queries/flows/use-get-flow.ts | 3 +- .../queries/flows/use-patch-update-flow.ts | 25 +- .../API/queries/messages/use-get-messages.ts | 10 +- .../messages/use-put-update-messages.ts | 25 +- .../queries/messages/use-rename-session.ts | 34 +- .../src/customization/feature-flags.ts | 2 + src/frontend/src/customization/utils/urls.ts | 3 + src/frontend/src/icons/BW python/Python.jsx | 33 + src/frontend/src/icons/BW python/index.tsx | 11 + src/frontend/src/icons/BW python/logo.svg | 11 + src/frontend/src/icons/JSicon/Frame.svg | 11 + src/frontend/src/icons/JSicon/JSIcon.jsx | 26 + src/frontend/src/icons/JSicon/index.tsx | 10 + .../src/modals/EmbedModal/embed-modal.tsx | 99 ++ .../components/session-selector.tsx | 11 +- .../IOModal/components/chat-view-wrapper.tsx | 39 +- .../IOModal/components/chatView/chat-view.tsx | 10 +- .../chatView/chatInput/chat-input.tsx | 25 +- .../chatView/chatMessage/chat-message.tsx | 13 +- .../IOModal/components/sidebar-open-view.tsx | 2 + src/frontend/src/modals/IOModal/new-modal.tsx | 109 +- .../modals/IOModal/types/chat-view-wrapper.ts | 2 + .../modals/IOModal/types/sidebar-open-view.ts | 1 + .../modals/apiModal/codeTabs/code-tabs.tsx | 164 +++ .../src/modals/apiModal/new-api-modal.tsx | 141 ++ .../modals/apiModal/utils/get-curl-code.tsx | 53 + .../modals/apiModal/utils/get-js-api-code.tsx | 72 + .../apiModal/utils/get-python-api-code.tsx | 73 + .../modals/apiModal/utils/get-widget-code.tsx | 16 +- .../baseModal/helpers/switch-case-size.ts | 11 +- src/frontend/src/modals/baseModal/index.tsx | 19 +- src/frontend/src/pages/AppInitPage/index.tsx | 8 - src/frontend/src/pages/Playground/index.tsx | 65 +- src/frontend/src/routes.tsx | 17 +- src/frontend/src/stores/flowStore.ts | 6 + src/frontend/src/stores/tweaksStore.ts | 41 +- src/frontend/src/stores/utilityStore.ts | 2 + src/frontend/src/style/applies.css | 8 + src/frontend/src/style/classes.css | 12 +- src/frontend/src/types/components/index.ts | 6 +- src/frontend/src/types/flow/index.ts | 2 + src/frontend/src/types/tabs/index.ts | 10 + src/frontend/src/types/tweaks/index.ts | 1 + src/frontend/src/types/zustand/flow/index.ts | 2 + .../src/types/zustand/tweaks/index.ts | 9 +- .../src/types/zustand/utility/index.ts | 2 + src/frontend/src/utils/buildUtils.ts | 6 +- src/frontend/src/utils/reactflowUtils.ts | 4 + src/frontend/src/utils/styleUtils.ts | 8 + src/frontend/src/utils/utils.ts | 60 + src/frontend/tailwind.config.mjs | 3 + src/frontend/tests/core/features/logs.spec.ts | 4 +- .../tests/core/features/publish-flow.spec.ts | 73 + .../features/curlApiGeneration.spec.ts | 3 +- .../edit-name-description-node.spec.ts | 4 +- .../features/pythonApiGeneration.spec.ts | 5 +- 86 files changed, 2252 insertions(+), 1465 deletions(-) create mode 100644 src/backend/base/langflow/alembic/versions/f3b2d1f1002d_add_column_access_type_to_flow.py create mode 100644 src/backend/base/langflow/services/task/temp_flow_cleanup.py create mode 100644 src/backend/tests/unit/services/tasks/__init__.py create mode 100644 src/backend/tests/unit/services/tasks/test_temp_flow_cleanup.py create mode 100644 src/frontend/src/assets/LangflowLogoColor.svg create mode 100644 src/frontend/src/components/core/flowToolbarComponent/components/deploy-dropdown.tsx create mode 100644 src/frontend/src/components/core/flowToolbarComponent/components/flow-toolbar-options.tsx create mode 100644 src/frontend/src/customization/utils/urls.ts create mode 100644 src/frontend/src/icons/BW python/Python.jsx create mode 100644 src/frontend/src/icons/BW python/index.tsx create mode 100644 src/frontend/src/icons/BW python/logo.svg create mode 100644 src/frontend/src/icons/JSicon/Frame.svg create mode 100644 src/frontend/src/icons/JSicon/JSIcon.jsx create mode 100644 src/frontend/src/icons/JSicon/index.tsx create mode 100644 src/frontend/src/modals/EmbedModal/embed-modal.tsx create mode 100644 src/frontend/src/modals/apiModal/codeTabs/code-tabs.tsx create mode 100644 src/frontend/src/modals/apiModal/new-api-modal.tsx create mode 100644 src/frontend/tests/core/features/publish-flow.spec.ts diff --git a/src/backend/base/langflow/alembic/versions/f3b2d1f1002d_add_column_access_type_to_flow.py b/src/backend/base/langflow/alembic/versions/f3b2d1f1002d_add_column_access_type_to_flow.py new file mode 100644 index 000000000..575a151b7 --- /dev/null +++ b/src/backend/base/langflow/alembic/versions/f3b2d1f1002d_add_column_access_type_to_flow.py @@ -0,0 +1,33 @@ +"""add column 'access_type' to flow + +Revision ID: f3b2d1f1002d +Revises: 93e2705fa8d6 +Create Date: 2025-02-05 14:35:29.658101 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from langflow.utils import migration + + +# revision identifiers, used by Alembic. +revision: str = 'f3b2d1f1002d' +down_revision: Union[str, None] = '93e2705fa8d6' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + conn = op.get_bind() + with op.batch_alter_table('flow', schema=None) as batch_op: + if not migration.column_exists(table_name='flow', column_name='access_type', conn=conn): + batch_op.add_column(sa.Column('access_type', sa.Enum('PRIVATE', 'PUBLIC', name='access_type_enum'), server_default='private', nullable=False)) + + +def downgrade() -> None: + conn = op.get_bind() + with op.batch_alter_table('flow', schema=None) as batch_op: + if migration.column_exists(table_name='flow', column_name='access_type', conn=conn): + batch_op.drop_column('access_type') diff --git a/src/backend/base/langflow/api/build.py b/src/backend/base/langflow/api/build.py index 586d1112f..a68b538c0 100644 --- a/src/backend/base/langflow/api/build.py +++ b/src/backend/base/langflow/api/build.py @@ -50,6 +50,7 @@ async def start_flow_build( log_builds: bool, current_user: CurrentActiveUser, queue_service: JobQueueService, + flow_name: str | None = None, ) -> str: """Start the flow build process by setting up the queue and starting the build task. @@ -70,6 +71,7 @@ async def start_flow_build( start_component_id=start_component_id, log_builds=log_builds, current_user=current_user, + flow_name=flow_name, ) queue_service.start_job(job_id, task_coro) except Exception as e: @@ -154,6 +156,7 @@ async def generate_flow_events( start_component_id: str | None, log_builds: bool, current_user: CurrentActiveUser, + flow_name: str | None = None, ) -> None: """Generate events for flow building process. @@ -175,7 +178,7 @@ async def generate_flow_events( flow_id_str = str(flow_id) # Create a fresh session for database operations async with session_scope() as fresh_session: - graph = await create_graph(fresh_session, flow_id_str) + graph = await create_graph(fresh_session, flow_id_str, flow_name) graph.validate_stream() first_layer = sort_vertices(graph) @@ -214,7 +217,7 @@ async def generate_flow_events( ), ) - async def create_graph(fresh_session, flow_id_str: str) -> Graph: + async def create_graph(fresh_session, flow_id_str: str, flow_name: str | None) -> Graph: if inputs is not None and getattr(inputs, "session", None) is not None: effective_session_id = inputs.session else: @@ -229,8 +232,9 @@ async def generate_flow_events( session_id=effective_session_id, ) - result = await fresh_session.exec(select(Flow.name).where(Flow.id == flow_id)) - flow_name = result.first() + if not flow_name: + result = await fresh_session.exec(select(Flow.name).where(Flow.id == flow_id)) + flow_name = result.first() return await build_graph_from_data( flow_id=flow_id_str, diff --git a/src/backend/base/langflow/api/utils.py b/src/backend/base/langflow/api/utils.py index 48549fd57..6a8258f0a 100644 --- a/src/backend/base/langflow/api/utils.py +++ b/src/backend/base/langflow/api/utils.py @@ -303,3 +303,63 @@ def custom_params( if page is None and size is None: return None return Params(page=page or MIN_PAGE_SIZE, size=size or MAX_PAGE_SIZE) + + +async def verify_public_flow_and_get_user(flow_id: uuid.UUID, client_id: str | None) -> tuple[User, uuid.UUID]: + """Verify a public flow request and generate a deterministic flow ID. + + This utility function: + 1. Checks that a client_id cookie is provided + 2. Verifies the flow exists and is marked as PUBLIC + 3. Creates a deterministic UUID based on client_id and original flow_id + 4. Retrieves the flow owner user for permission purposes + + This function is used to support public flow endpoints that don't require + authentication but still need to operate within the permission model. + + Args: + flow_id: The original flow ID to verify + client_id: The client ID from the request cookie + + Returns: + tuple: (flow owner user, deterministic flow ID for tracking) + + Raises: + HTTPException: + - 400 if no client_id is provided + - 403 if flow doesn't exist or isn't public + - 403 if unable to retrieve the flow owner user + - 403 if user is not found for public flow + """ + if not client_id: + raise HTTPException(status_code=400, detail="No client_id cookie found") + + # Check if the flow is public + async with session_scope() as session: + from sqlmodel import select + + from langflow.services.database.models.flow.model import AccessTypeEnum, Flow + + flow = (await session.exec(select(Flow).where(Flow.id == flow_id))).first() + if not flow or flow.access_type is not AccessTypeEnum.PUBLIC: + raise HTTPException(status_code=403, detail="Flow is not public") + + # Create a new flow ID using the client_id and flow_id + new_id = f"{client_id}_{flow_id}" + new_flow_id = uuid.uuid5(uuid.NAMESPACE_DNS, new_id) + + # Get the user associated with the flow + try: + from langflow.helpers.user import get_user_by_flow_id_or_endpoint_name + + user = await get_user_by_flow_id_or_endpoint_name(str(flow_id)) + + except Exception as exc: + logger.exception(f"Error getting user for public flow {flow_id}") + raise HTTPException(status_code=403, detail="Flow is not accessible") from exc + + if not user: + msg = f"User not found for public flow {flow_id}" + raise HTTPException(status_code=403, detail=msg) + + return user, new_flow_id diff --git a/src/backend/base/langflow/api/v1/chat.py b/src/backend/base/langflow/api/v1/chat.py index 0f2fed846..0af57def0 100644 --- a/src/backend/base/langflow/api/v1/chat.py +++ b/src/backend/base/langflow/api/v1/chat.py @@ -6,7 +6,15 @@ import traceback import uuid from typing import TYPE_CHECKING, Annotated -from fastapi import APIRouter, BackgroundTasks, Body, Depends, HTTPException, status +from fastapi import ( + APIRouter, + BackgroundTasks, + Body, + Depends, + HTTPException, + Request, + status, +) from fastapi.responses import StreamingResponse from loguru import logger @@ -25,6 +33,7 @@ from langflow.api.utils import ( format_exception_message, get_top_level_vertices, parse_exception, + verify_public_flow_and_get_user, ) from langflow.api.v1.schemas import ( CancelFlowResponse, @@ -142,8 +151,29 @@ async def build_flow( log_builds: bool = True, current_user: CurrentActiveUser, queue_service: Annotated[JobQueueService, Depends(get_queue_service)], + flow_name: str | None = None, ): - """Build and process a flow, returning a job ID for event polling.""" + """Build and process a flow, returning a job ID for event polling. + + This endpoint requires authentication through the CurrentActiveUser dependency. + For public flows that don't require authentication, use the /build_public_tmp/{flow_id}/flow endpoint. + + Args: + flow_id: UUID of the flow to build + background_tasks: Background tasks manager + inputs: Optional input values for the flow + data: Optional flow data + files: Optional files to include + stop_component_id: Optional ID of component to stop at + start_component_id: Optional ID of component to start from + log_builds: Whether to log the build process + current_user: The authenticated user + queue_service: Queue service for job management + flow_name: Optional name for the flow + + Returns: + Dict with job_id that can be used to poll for build status + """ # First verify the flow exists async with session_scope() as session: flow = await session.get(Flow, flow_id) @@ -161,6 +191,7 @@ async def build_flow( log_builds=log_builds, current_user=current_user, queue_service=queue_service, + flow_name=flow_name, ) return {"job_id": job_id} @@ -254,7 +285,9 @@ async def build_vertex( # If there's no cache logger.warning(f"No cache found for {flow_id_str}. Building graph starting at {vertex_id}") graph = await build_graph_from_db( - flow_id=flow_id, session=await anext(get_session()), chat_service=chat_service + flow_id=flow_id, + session=await anext(get_session()), + chat_service=chat_service, ) else: graph = cache.get("result") @@ -450,7 +483,11 @@ async def _stream_vertex(flow_id: str, vertex_id: str, chat_service: ChatService yield str(StreamData(event="close", data={"message": "Stream closed"})) -@router.get("/build/{flow_id}/{vertex_id}/stream", response_class=StreamingResponse, deprecated=True) +@router.get( + "/build/{flow_id}/{vertex_id}/stream", + response_class=StreamingResponse, + deprecated=True, +) async def build_vertex_stream( flow_id: uuid.UUID, vertex_id: str, @@ -482,7 +519,80 @@ async def build_vertex_stream( """ try: return StreamingResponse( - _stream_vertex(str(flow_id), vertex_id, get_chat_service()), media_type="text/event-stream" + _stream_vertex(str(flow_id), vertex_id, get_chat_service()), + media_type="text/event-stream", ) except Exception as exc: raise HTTPException(status_code=500, detail="Error building Component") from exc + + +@router.post("/build_public_tmp/{flow_id}/flow") +async def build_public_tmp( + *, + background_tasks: LimitVertexBuildBackgroundTasks, + flow_id: uuid.UUID, + inputs: Annotated[InputValueRequest | None, Body(embed=True)] = None, + data: Annotated[FlowDataRequest | None, Body(embed=True)] = None, + files: list[str] | None = None, + stop_component_id: str | None = None, + start_component_id: str | None = None, + log_builds: bool | None = True, + flow_name: str | None = None, + request: Request, + queue_service: Annotated[JobQueueService, Depends(get_queue_service)], +): + """Build a public flow without requiring authentication. + + This endpoint is specifically for public flows that don't require authentication. + It uses a client_id cookie to create a deterministic flow ID for tracking purposes. + + The endpoint: + 1. Verifies the requested flow is marked as public in the database + 2. Creates a deterministic UUID based on client_id and flow_id + 3. Uses the flow owner's permissions to build the flow + + Requirements: + - The flow must be marked as PUBLIC in the database + - The request must include a client_id cookie + + Args: + flow_id: UUID of the public flow to build + background_tasks: Background tasks manager + inputs: Optional input values for the flow + data: Optional flow data + files: Optional files to include + stop_component_id: Optional ID of component to stop at + start_component_id: Optional ID of component to start from + log_builds: Whether to log the build process + flow_name: Optional name for the flow + request: FastAPI request object (needed for cookie access) + queue_service: Queue service for job management + + Returns: + Dict with job_id that can be used to poll for build status + """ + try: + # Verify this is a public flow and get the associated user + client_id = request.cookies.get("client_id") + owner_user, new_flow_id = await verify_public_flow_and_get_user(flow_id=flow_id, client_id=client_id) + + # Start the flow build using the new flow ID + job_id = await start_flow_build( + flow_id=new_flow_id, + background_tasks=background_tasks, + inputs=inputs, + data=data, + files=files, + stop_component_id=stop_component_id, + start_component_id=start_component_id, + log_builds=log_builds or False, + current_user=owner_user, + queue_service=queue_service, + flow_name=flow_name or f"{client_id}_{flow_id}", + ) + except Exception as exc: + logger.exception("Error building public flow") + if isinstance(exc, HTTPException): + raise + raise HTTPException(status_code=500, detail=str(exc)) from exc + return {"job_id": job_id} diff --git a/src/backend/base/langflow/api/v1/flows.py b/src/backend/base/langflow/api/v1/flows.py index 3995788a6..8519f9be6 100644 --- a/src/backend/base/langflow/api/v1/flows.py +++ b/src/backend/base/langflow/api/v1/flows.py @@ -21,10 +21,11 @@ from sqlmodel.ext.asyncio.session import AsyncSession from langflow.api.utils import CurrentActiveUser, DbSession, cascade_delete_flow, remove_api_keys, validate_is_component from langflow.api.v1.schemas import FlowListCreate +from langflow.helpers.user import get_user_by_flow_id_or_endpoint_name from langflow.initial_setup.constants import STARTER_FOLDER_NAME from langflow.logging import logger from langflow.services.database.models.flow import Flow, FlowCreate, FlowRead, FlowUpdate -from langflow.services.database.models.flow.model import FlowHeader +from langflow.services.database.models.flow.model import AccessTypeEnum, FlowHeader from langflow.services.database.models.flow.utils import get_webhook_component_in_flow from langflow.services.database.models.folder.constants import DEFAULT_FOLDER_NAME from langflow.services.database.models.folder.model import Folder @@ -278,6 +279,21 @@ async def read_flow( raise HTTPException(status_code=404, detail="Flow not found") +@router.get("/public_flow/{flow_id}", response_model=FlowRead, status_code=200) +async def read_public_flow( + *, + session: DbSession, + flow_id: UUID, +): + """Read a public flow.""" + access_type = (await session.exec(select(Flow.access_type).where(Flow.id == flow_id))).first() + if access_type is not AccessTypeEnum.PUBLIC: + raise HTTPException(status_code=403, detail="Flow is not public") + + current_user = await get_user_by_flow_id_or_endpoint_name(str(flow_id)) + return await read_flow(session=session, flow_id=flow_id, current_user=current_user) + + @router.patch("/{flow_id}", response_model=FlowRead, status_code=200) async def update_flow( *, diff --git a/src/backend/base/langflow/api/v1/schemas.py b/src/backend/base/langflow/api/v1/schemas.py index 777390a12..08e0ade10 100644 --- a/src/backend/base/langflow/api/v1/schemas.py +++ b/src/backend/base/langflow/api/v1/schemas.py @@ -4,7 +4,14 @@ from pathlib import Path from typing import Any, Literal from uuid import UUID -from pydantic import BaseModel, ConfigDict, Field, field_serializer, field_validator, model_serializer +from pydantic import ( + BaseModel, + ConfigDict, + Field, + field_serializer, + field_validator, + model_serializer, +) from langflow.graph.schema import RunOutputs from langflow.schema import dotdict @@ -377,6 +384,8 @@ class ConfigResponse(BaseModel): health_check_max_retries: int max_file_size_upload: int webhook_polling_interval: int + public_flow_cleanup_interval: int + public_flow_expiration: int event_delivery: Literal["polling", "streaming"] diff --git a/src/backend/base/langflow/graph/graph/base.py b/src/backend/base/langflow/graph/graph/base.py index df79ac575..32a613a5d 100644 --- a/src/backend/base/langflow/graph/graph/base.py +++ b/src/backend/base/langflow/graph/graph/base.py @@ -341,6 +341,7 @@ class Graph: self, inputs: list[dict] | None = None, max_iterations: int | None = None, + config: StartConfigDict | None = None, event_manager: EventManager | None = None, ): if not self._prepared: @@ -348,6 +349,8 @@ class Graph: raise ValueError(msg) # The idea is for this to return a generator that yields the result of # each step call and raise StopIteration when the graph is done + if config is not None: + self.__apply_config(config) for _input in inputs or []: for key, value in _input.items(): vertex = self.get_vertex(key) diff --git a/src/backend/base/langflow/main.py b/src/backend/base/langflow/main.py index 684564ae0..290284cbe 100644 --- a/src/backend/base/langflow/main.py +++ b/src/backend/base/langflow/main.py @@ -274,6 +274,7 @@ def create_app(): FastAPIInstrumentor.instrument_app(app) add_pagination(app) + return app diff --git a/src/backend/base/langflow/memory.py b/src/backend/base/langflow/memory.py index cdb438fbe..66cfabd87 100644 --- a/src/backend/base/langflow/memory.py +++ b/src/backend/base/langflow/memory.py @@ -1,3 +1,4 @@ +import asyncio import json from collections.abc import Sequence from uuid import UUID @@ -154,9 +155,19 @@ async def aadd_messagetables(messages: list[MessageTable], session: AsyncSession try: for message in messages: session.add(message) - await session.commit() + try: + await session.commit() + # This is a hack. + # We are doing this because build_public_tmp causes the CancelledError to be raised + # while build_flow does not. + except asyncio.CancelledError: + await session.commit() for message in messages: await session.refresh(message) + except asyncio.CancelledError as e: + logger.exception(e) + error_msg = "Operation cancelled" + raise ValueError(error_msg) from e except Exception as e: logger.exception(e) raise diff --git a/src/backend/base/langflow/services/database/models/flow/model.py b/src/backend/base/langflow/services/database/models/flow/model.py index 1dbbd54ec..13ec269df 100644 --- a/src/backend/base/langflow/services/database/models/flow/model.py +++ b/src/backend/base/langflow/services/database/models/flow/model.py @@ -2,6 +2,7 @@ import re from datetime import datetime, timezone +from enum import Enum from typing import TYPE_CHECKING, Optional from uuid import UUID, uuid4 @@ -15,7 +16,8 @@ from pydantic import ( field_serializer, field_validator, ) -from sqlalchemy import Text, UniqueConstraint +from sqlalchemy import Enum as SQLEnum +from sqlalchemy import Text, UniqueConstraint, text from sqlmodel import JSON, Column, Field, Relationship, SQLModel from langflow.schema import Data @@ -30,6 +32,11 @@ if TYPE_CHECKING: HEX_COLOR_LENGTH = 7 +class AccessTypeEnum(str, Enum): + PRIVATE = "private" + PUBLIC = "public" + + class FlowBase(SQLModel): name: str = Field(index=True) description: str | None = Field(default=None, sa_column=Column(Text, index=True, nullable=True)) @@ -43,6 +50,18 @@ class FlowBase(SQLModel): endpoint_name: str | None = Field(default=None, nullable=True, index=True) tags: list[str] | None = None locked: bool | None = Field(default=False, nullable=True) + access_type: AccessTypeEnum = Field( + default=AccessTypeEnum.PRIVATE, + sa_column=Column( + SQLEnum( + AccessTypeEnum, + name="access_type_enum", + values_callable=lambda enum: [member.value for member in enum], + ), + nullable=False, + server_default=text("'private'"), + ), + ) @field_validator("endpoint_name") @classmethod @@ -218,6 +237,7 @@ class FlowHeader(BaseModel): endpoint_name: str | None = Field(None, description="The name of the endpoint associated with this flow") description: str | None = Field(None, description="A description of the flow") data: dict | None = Field(None, description="The data of the component, if is_component is True") + access_type: AccessTypeEnum | None = Field(None, description="The access type of the flow") tags: list[str] | None = Field(None, description="The tags of the flow") @field_validator("data", mode="before") @@ -235,6 +255,7 @@ class FlowUpdate(SQLModel): folder_id: UUID | None = None endpoint_name: str | None = None locked: bool | None = None + access_type: AccessTypeEnum | None = None fs_path: str | None = None @field_validator("endpoint_name") diff --git a/src/backend/base/langflow/services/settings/base.py b/src/backend/base/langflow/services/settings/base.py index c97f37fab..c2e930f42 100644 --- a/src/backend/base/langflow/services/settings/base.py +++ b/src/backend/base/langflow/services/settings/base.py @@ -10,9 +10,14 @@ import orjson import yaml from aiofile import async_open from loguru import logger -from pydantic import field_validator +from pydantic import Field, field_validator from pydantic.fields import FieldInfo -from pydantic_settings import BaseSettings, EnvSettingsSource, PydanticBaseSettingsSource, SettingsConfigDict +from pydantic_settings import ( + BaseSettings, + EnvSettingsSource, + PydanticBaseSettingsSource, + SettingsConfigDict, +) from typing_extensions import override from langflow.services.settings.constants import VARIABLES_TO_GET_FROM_ENVIRONMENT @@ -224,6 +229,13 @@ class Settings(BaseSettings): mcp_server_enable_progress_notifications: bool = False """If set to False, Langflow will not send progress notifications in the MCP server.""" + # Public Flow Settings + public_flow_cleanup_interval: int = Field(default=3600, gt=600) + """The interval in seconds at which public temporary flows will be cleaned up. + Default is 1 hour (3600 seconds). Minimum is 600 seconds (10 minutes).""" + public_flow_expiration: int = Field(default=86400, gt=600) + """The time in seconds after which a public temporary flow will be considered expired and eligible for cleanup. + Default is 24 hours (86400 seconds). Minimum is 600 seconds (10 minutes).""" event_delivery: Literal["polling", "streaming"] = "polling" """How to deliver build events to the frontend. Can be 'polling' or 'streaming'.""" diff --git a/src/backend/base/langflow/services/storage/local.py b/src/backend/base/langflow/services/storage/local.py index fed79b354..428593f81 100644 --- a/src/backend/base/langflow/services/storage/local.py +++ b/src/backend/base/langflow/services/storage/local.py @@ -11,7 +11,6 @@ class LocalStorageService(StorageService): def __init__(self, session_service, settings_service) -> None: """Initialize the local storage service with session and settings services.""" super().__init__(session_service, settings_service) - self.data_dir = anyio.Path(settings_service.settings.config_dir) self.set_ready() def build_full_path(self, flow_id: str, file_name: str) -> str: diff --git a/src/backend/base/langflow/services/storage/service.py b/src/backend/base/langflow/services/storage/service.py index e139b73c4..4cdd2696b 100644 --- a/src/backend/base/langflow/services/storage/service.py +++ b/src/backend/base/langflow/services/storage/service.py @@ -3,6 +3,8 @@ from __future__ import annotations from abc import abstractmethod from typing import TYPE_CHECKING +import anyio + from langflow.services.base import Service if TYPE_CHECKING: @@ -16,6 +18,7 @@ class StorageService(Service): def __init__(self, session_service: SessionService, settings_service: SettingsService): self.settings_service = settings_service self.session_service = session_service + self.data_dir: anyio.Path = anyio.Path(settings_service.settings.config_dir) self.set_ready() def build_full_path(self, flow_id: str, file_name: str) -> str: diff --git a/src/backend/base/langflow/services/task/temp_flow_cleanup.py b/src/backend/base/langflow/services/task/temp_flow_cleanup.py new file mode 100644 index 000000000..3f7317c10 --- /dev/null +++ b/src/backend/base/langflow/services/task/temp_flow_cleanup.py @@ -0,0 +1,136 @@ +from __future__ import annotations + +import asyncio +import contextlib +from typing import TYPE_CHECKING + +from loguru import logger +from sqlmodel import col, delete, select + +from langflow.services.database.models.message.model import MessageTable +from langflow.services.database.models.transactions.model import TransactionTable +from langflow.services.database.models.vertex_builds.model import VertexBuildTable +from langflow.services.deps import get_settings_service, get_storage_service, session_scope + +if TYPE_CHECKING: + from langflow.services.storage.service import StorageService + + +async def cleanup_orphaned_records() -> None: + """Clean up all records that reference non-existent flows.""" + from langflow.services.database.models.flow.model import Flow + + async with session_scope() as session: + # Create a subquery of existing flow IDs + flow_ids_subquery = select(Flow.id) + + # Tables that have flow_id foreign keys + tables: list[type[VertexBuildTable | MessageTable | TransactionTable]] = [ + MessageTable, + VertexBuildTable, + TransactionTable, + ] + + for table in tables: + try: + # Get distinct orphaned flow IDs from the table + orphaned_flow_ids = ( + await session.exec( + select(col(table.flow_id).distinct()).where(col(table.flow_id).not_in(flow_ids_subquery)) + ) + ).all() + + if orphaned_flow_ids: + logger.debug(f"Found {len(orphaned_flow_ids)} orphaned flow IDs in {table.__name__}") + + # Delete all orphaned records in a single query + await session.exec(delete(table).where(col(table.flow_id).in_(orphaned_flow_ids))) + + # Clean up any associated storage files + storage_service: StorageService = get_storage_service() + for flow_id in orphaned_flow_ids: + try: + files = await storage_service.list_files(str(flow_id)) + for file in files: + try: + await storage_service.delete_file(str(flow_id), file) + except Exception as exc: # noqa: BLE001 + logger.error(f"Failed to delete file {file} for flow {flow_id}: {exc!s}") + # Delete the flow directory after all files are deleted + flow_dir = storage_service.data_dir / str(flow_id) + if await flow_dir.exists(): + await flow_dir.rmdir() + except Exception as exc: # noqa: BLE001 + logger.error(f"Failed to list files for flow {flow_id}: {exc!s}") + + await session.commit() + logger.debug(f"Successfully deleted orphaned records from {table.__name__}") + + except Exception as exc: # noqa: BLE001 + logger.error(f"Error cleaning up orphaned records in {table.__name__}: {exc!s}") + await session.rollback() + + +class CleanupWorker: + def __init__(self) -> None: + self._stop_event = asyncio.Event() + self._task: asyncio.Task | None = None + + async def start(self): + """Start the cleanup worker.""" + if self._task is not None: + logger.warning("Cleanup worker is already running") + return + + self._task = asyncio.create_task(self._run()) + logger.debug("Started database cleanup worker") + + async def stop(self): + """Stop the cleanup worker gracefully.""" + if self._task is None: + logger.warning("Cleanup worker is not running") + return + + logger.debug("Stopping database cleanup worker...") + self._stop_event.set() + await self._task + self._task = None + logger.debug("Database cleanup worker stopped") + + async def _run(self): + """Run the cleanup worker until stopped.""" + settings = get_settings_service().settings + while not self._stop_event.is_set(): + try: + # Clean up any orphaned records + await cleanup_orphaned_records() + except Exception as exc: # noqa: BLE001 + logger.error(f"Error in cleanup worker: {exc!s}") + + try: + # Create a task for the timeout + sleep_task = asyncio.create_task(asyncio.sleep(settings.public_flow_cleanup_interval)) + # Create a task for the stop event + stop_task = asyncio.create_task(self._stop_event.wait()) + + # Wait for either the timeout or the stop event + done, pending = await asyncio.wait([sleep_task, stop_task], return_when=asyncio.FIRST_COMPLETED) + + # Cancel any pending tasks + for task in pending: + task.cancel() + with contextlib.suppress(asyncio.CancelledError): + await task + + # If the stop event completed, break the loop + if stop_task in done: + break + + except Exception as exc: # noqa: BLE001 + logger.error(f"Error in cleanup worker sleep: {exc!s}") + # Sleep a minimum amount in case of errors + await asyncio.sleep(60) + + +# Create a global instance of the worker +cleanup_worker = CleanupWorker() diff --git a/src/backend/tests/unit/services/tasks/__init__.py b/src/backend/tests/unit/services/tasks/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/backend/tests/unit/services/tasks/test_temp_flow_cleanup.py b/src/backend/tests/unit/services/tasks/test_temp_flow_cleanup.py new file mode 100644 index 000000000..5b1b0a8c9 --- /dev/null +++ b/src/backend/tests/unit/services/tasks/test_temp_flow_cleanup.py @@ -0,0 +1,109 @@ +from __future__ import annotations + +import datetime +from datetime import timezone +from uuid import uuid4 + +import pytest +from langflow.services.database.models.flow import Flow as FlowTable +from langflow.services.database.models.message.model import MessageTable +from langflow.services.deps import get_settings_service, get_storage_service, session_scope +from langflow.services.task.temp_flow_cleanup import ( + CleanupWorker, + cleanup_orphaned_records, +) + + +@pytest.mark.usefixtures("client") +async def test_cleanup_orphaned_records_no_orphans(): + """Test cleanup when there are no orphaned records.""" + storage_service = get_storage_service() + flow_id = uuid4() + + async with session_scope() as session: + # Create a flow and associated message + flow = FlowTable( + id=flow_id, + name="Test Flow", + data="null", + updated_at=datetime.datetime.now(timezone.utc), + ) + message = MessageTable( + id=uuid4(), + flow_id=flow_id, + sender="test_user", + sender_name="Test User", + timestamp=datetime.datetime.now(timezone.utc), + session_id=str(uuid4()), + ) + session.add(flow) + session.add(message) + await session.commit() + + # Write a file for the flow + await storage_service.save_file(str(flow_id), "test.json", b"test data") + + # Run cleanup + async with session_scope() as session: + await cleanup_orphaned_records() + + # Verify message still exists + async with session_scope() as session: + message = await session.get(MessageTable, message.id) + assert message is not None + + +@pytest.mark.usefixtures("client") +async def test_cleanup_orphaned_records_with_orphans(): + """Test cleanup when there are orphaned records.""" + orphaned_flow_id = uuid4() + + async with session_scope() as session: + # Create orphaned records without an associated flow + message = MessageTable( + id=uuid4(), + flow_id=orphaned_flow_id, + sender="test_user", + sender_name="Test User", + timestamp=datetime.datetime.now(timezone.utc), + session_id=str(uuid4()), + ) + session.add(message) + await session.commit() + + # Run cleanup + async with session_scope() as session: + await cleanup_orphaned_records() + + # Verify orphaned message was deleted + async with session_scope() as session: + message = await session.get(MessageTable, message.id) + assert message is None + + +@pytest.mark.asyncio +async def test_cleanup_worker_start_stop(): + """Test CleanupWorker start and stop functionality.""" + worker = CleanupWorker() + await worker.start() + assert worker._task is not None + assert not worker._stop_event.is_set() + await worker.stop() + assert worker._task is None + assert worker._stop_event.is_set() + + +@pytest.mark.asyncio +async def test_cleanup_worker_run_with_exception(caplog): + """Test CleanupWorker handles exceptions gracefully.""" + settings = get_settings_service().settings + settings.public_flow_cleanup_interval = 601 # Minimum valid interval + worker = CleanupWorker() + + # Start worker and let it run briefly + await worker.start() + await worker.stop() + + # Check logs for expected messages + assert any("Started database cleanup worker" in record.message for record in caplog.records) + assert any("Stopping database cleanup worker" in record.message for record in caplog.records) diff --git a/src/frontend/package-lock.json b/src/frontend/package-lock.json index 44a4d1e1f..a6ae05e95 100644 --- a/src/frontend/package-lock.json +++ b/src/frontend/package-lock.json @@ -862,246 +862,6 @@ "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", "peer": true }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@esbuild/linux-x64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", @@ -1117,96 +877,6 @@ "node": ">=12" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.5.0.tgz", @@ -1768,246 +1438,6 @@ "node": ">=6.9.0" } }, - "node_modules/@million/lint/node_modules/@esbuild/aix-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", - "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", - "cpu": [ - "ppc64" - ], - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@million/lint/node_modules/@esbuild/android-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", - "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@million/lint/node_modules/@esbuild/android-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", - "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@million/lint/node_modules/@esbuild/android-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", - "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@million/lint/node_modules/@esbuild/darwin-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", - "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@million/lint/node_modules/@esbuild/darwin-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", - "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@million/lint/node_modules/@esbuild/freebsd-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", - "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@million/lint/node_modules/@esbuild/freebsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", - "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@million/lint/node_modules/@esbuild/linux-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", - "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@million/lint/node_modules/@esbuild/linux-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", - "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@million/lint/node_modules/@esbuild/linux-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", - "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@million/lint/node_modules/@esbuild/linux-loong64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", - "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", - "cpu": [ - "loong64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@million/lint/node_modules/@esbuild/linux-mips64el": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", - "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", - "cpu": [ - "mips64el" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@million/lint/node_modules/@esbuild/linux-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", - "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", - "cpu": [ - "ppc64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@million/lint/node_modules/@esbuild/linux-riscv64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", - "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", - "cpu": [ - "riscv64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@million/lint/node_modules/@esbuild/linux-s390x": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", - "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", - "cpu": [ - "s390x" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@million/lint/node_modules/@esbuild/linux-x64": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", @@ -2023,96 +1453,6 @@ "node": ">=12" } }, - "node_modules/@million/lint/node_modules/@esbuild/netbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", - "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@million/lint/node_modules/@esbuild/openbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", - "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@million/lint/node_modules/@esbuild/sunos-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", - "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@million/lint/node_modules/@esbuild/win32-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", - "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@million/lint/node_modules/@esbuild/win32-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", - "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@million/lint/node_modules/@esbuild/win32-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", - "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@million/lint/node_modules/esbuild": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", @@ -2206,171 +1546,6 @@ "@napi-rs/nice-win32-x64-msvc": "1.0.1" } }, - "node_modules/@napi-rs/nice-android-arm-eabi": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm-eabi/-/nice-android-arm-eabi-1.0.1.tgz", - "integrity": "sha512-5qpvOu5IGwDo7MEKVqqyAxF90I6aLj4n07OzpARdgDRfz8UbBztTByBp0RC59r3J1Ij8uzYi6jI7r5Lws7nn6w==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/nice-android-arm64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm64/-/nice-android-arm64-1.0.1.tgz", - "integrity": "sha512-GqvXL0P8fZ+mQqG1g0o4AO9hJjQaeYG84FRfZaYjyJtZZZcMjXW5TwkL8Y8UApheJgyE13TQ4YNUssQaTgTyvA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/nice-darwin-arm64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-arm64/-/nice-darwin-arm64-1.0.1.tgz", - "integrity": "sha512-91k3HEqUl2fsrz/sKkuEkscj6EAj3/eZNCLqzD2AA0TtVbkQi8nqxZCZDMkfklULmxLkMxuUdKe7RvG/T6s2AA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/nice-darwin-x64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-x64/-/nice-darwin-x64-1.0.1.tgz", - "integrity": "sha512-jXnMleYSIR/+TAN/p5u+NkCA7yidgswx5ftqzXdD5wgy/hNR92oerTXHc0jrlBisbd7DpzoaGY4cFD7Sm5GlgQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/nice-freebsd-x64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-freebsd-x64/-/nice-freebsd-x64-1.0.1.tgz", - "integrity": "sha512-j+iJ/ezONXRQsVIB/FJfwjeQXX7A2tf3gEXs4WUGFrJjpe/z2KB7sOv6zpkm08PofF36C9S7wTNuzHZ/Iiccfw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/nice-linux-arm-gnueabihf": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm-gnueabihf/-/nice-linux-arm-gnueabihf-1.0.1.tgz", - "integrity": "sha512-G8RgJ8FYXYkkSGQwywAUh84m946UTn6l03/vmEXBYNJxQJcD+I3B3k5jmjFG/OPiU8DfvxutOP8bi+F89MCV7Q==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/nice-linux-arm64-gnu": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-gnu/-/nice-linux-arm64-gnu-1.0.1.tgz", - "integrity": "sha512-IMDak59/W5JSab1oZvmNbrms3mHqcreaCeClUjwlwDr0m3BoR09ZiN8cKFBzuSlXgRdZ4PNqCYNeGQv7YMTjuA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/nice-linux-arm64-musl": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-musl/-/nice-linux-arm64-musl-1.0.1.tgz", - "integrity": "sha512-wG8fa2VKuWM4CfjOjjRX9YLIbysSVV1S3Kgm2Fnc67ap/soHBeYZa6AGMeR5BJAylYRjnoVOzV19Cmkco3QEPw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/nice-linux-ppc64-gnu": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-ppc64-gnu/-/nice-linux-ppc64-gnu-1.0.1.tgz", - "integrity": "sha512-lxQ9WrBf0IlNTCA9oS2jg/iAjQyTI6JHzABV664LLrLA/SIdD+I1i3Mjf7TsnoUbgopBcCuDztVLfJ0q9ubf6Q==", - "cpu": [ - "ppc64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/nice-linux-riscv64-gnu": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-riscv64-gnu/-/nice-linux-riscv64-gnu-1.0.1.tgz", - "integrity": "sha512-3xs69dO8WSWBb13KBVex+yvxmUeEsdWexxibqskzoKaWx9AIqkMbWmE2npkazJoopPKX2ULKd8Fm9veEn0g4Ig==", - "cpu": [ - "riscv64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/nice-linux-s390x-gnu": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-s390x-gnu/-/nice-linux-s390x-gnu-1.0.1.tgz", - "integrity": "sha512-lMFI3i9rlW7hgToyAzTaEybQYGbQHDrpRkg+1gJWEpH0PLAQoZ8jiY0IzakLfNWnVda1eTYYlxxFYzW8Rqczkg==", - "cpu": [ - "s390x" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@napi-rs/nice-linux-x64-gnu": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-gnu/-/nice-linux-x64-gnu-1.0.1.tgz", @@ -2401,51 +1576,6 @@ "node": ">= 10" } }, - "node_modules/@napi-rs/nice-win32-arm64-msvc": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-arm64-msvc/-/nice-win32-arm64-msvc-1.0.1.tgz", - "integrity": "sha512-rEcz9vZymaCB3OqEXoHnp9YViLct8ugF+6uO5McifTedjq4QMQs3DHz35xBEGhH3gJWEsXMUbzazkz5KNM5YUg==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/nice-win32-ia32-msvc": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-ia32-msvc/-/nice-win32-ia32-msvc-1.0.1.tgz", - "integrity": "sha512-t7eBAyPUrWL8su3gDxw9xxxqNwZzAqKo0Szv3IjVQd1GpXXVkb6vBBQUuxfIYaXMzZLwlxRQ7uzM2vdUE9ULGw==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/nice-win32-x64-msvc": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-x64-msvc/-/nice-win32-x64-msvc-1.0.1.tgz", - "integrity": "sha512-JlF+uDcatt3St2ntBG8H02F1mM45i5SF9W+bIKiReVE6wiy3o16oBP/yxt+RZ+N6LbCImJXJ6bXNO2kn9AXicg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -3906,174 +3036,6 @@ } } }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.35.0.tgz", - "integrity": "sha512-uYQ2WfPaqz5QtVgMxfN6NpLD+no0MYHDBywl7itPYd3K5TjjSghNKmX8ic9S8NU8w81NVhJv/XojcHptRly7qQ==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.35.0.tgz", - "integrity": "sha512-FtKddj9XZudurLhdJnBl9fl6BwCJ3ky8riCXjEw3/UIbjmIY58ppWwPEvU3fNu+W7FUsAsB1CdH+7EQE6CXAPA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.35.0.tgz", - "integrity": "sha512-Uk+GjOJR6CY844/q6r5DR/6lkPFOw0hjfOIzVx22THJXMxktXG6CbejseJFznU8vHcEBLpiXKY3/6xc+cBm65Q==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.35.0.tgz", - "integrity": "sha512-3IrHjfAS6Vkp+5bISNQnPogRAW5GAV1n+bNCrDwXmfMHbPl5EhTmWtfmwlJxFRUCBZ+tZ/OxDyU08aF6NI/N5Q==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.35.0.tgz", - "integrity": "sha512-sxjoD/6F9cDLSELuLNnY0fOrM9WA0KrM0vWm57XhrIMf5FGiN8D0l7fn+bpUeBSU7dCgPV2oX4zHAsAXyHFGcQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.35.0.tgz", - "integrity": "sha512-2mpHCeRuD1u/2kruUiHSsnjWtHjqVbzhBkNVQ1aVD63CcexKVcQGwJ2g5VphOd84GvxfSvnnlEyBtQCE5hxVVw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.35.0.tgz", - "integrity": "sha512-mrA0v3QMy6ZSvEuLs0dMxcO2LnaCONs1Z73GUDBHWbY8tFFocM6yl7YyMu7rz4zS81NDSqhrUuolyZXGi8TEqg==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.35.0.tgz", - "integrity": "sha512-DnYhhzcvTAKNexIql8pFajr0PiDGrIsBYPRvCKlA5ixSS3uwo/CWNZxB09jhIapEIg945KOzcYEAGGSmTSpk7A==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.35.0.tgz", - "integrity": "sha512-uagpnH2M2g2b5iLsCTZ35CL1FgyuzzJQ8L9VtlJ+FckBXroTwNOaD0z0/UF+k5K3aNQjbm8LIVpxykUOQt1m/A==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.35.0.tgz", - "integrity": "sha512-XQxVOCd6VJeHQA/7YcqyV0/88N6ysSVzRjJ9I9UA/xXpEsjvAgDTgH3wQYz5bmr7SPtVK2TsP2fQ2N9L4ukoUg==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.35.0.tgz", - "integrity": "sha512-5pMT5PzfgwcXEwOaSrqVsz/LvjDZt+vQ8RT/70yhPU06PTuq8WaHhfT1LW+cdD7mW6i/J5/XIkX/1tCAkh1W6g==", - "cpu": [ - "loong64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.35.0.tgz", - "integrity": "sha512-c+zkcvbhbXF98f4CtEIP1EBA/lCic5xB0lToneZYvMeKu5Kamq3O8gqrxiYYLzlZH6E3Aq+TSW86E4ay8iD8EA==", - "cpu": [ - "ppc64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.35.0.tgz", - "integrity": "sha512-s91fuAHdOwH/Tad2tzTtPX7UZyytHIRR6V4+2IGlV0Cej5rkG0R61SX4l4y9sh0JBibMiploZx3oHKPnQBKe4g==", - "cpu": [ - "riscv64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.35.0.tgz", - "integrity": "sha512-hQRkPQPLYJZYGP+Hj4fR9dDBMIM7zrzJDWFEMPdTnTy95Ljnv0/4w/ixFw3pTBMEuuEuoqtBINYND4M7ujcuQw==", - "cpu": [ - "s390x" - ], - "optional": true, - "os": [ - "linux" - ] - }, "node_modules/@rollup/rollup-linux-x64-gnu": { "version": "4.35.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.35.0.tgz", @@ -4098,42 +3060,6 @@ "linux" ] }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.35.0.tgz", - "integrity": "sha512-OUOlGqPkVJCdJETKOCEf1mw848ZyJ5w50/rZ/3IBQVdLfR5jk/6Sr5m3iO2tdPgwo0x7VcncYuOvMhBWZq8ayg==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.35.0.tgz", - "integrity": "sha512-2/lsgejMrtwQe44glq7AFFHLfJBPafpsTa6JvP2NGef/ifOa4KBoglVf7AKN7EV9o32evBPRqfg96fEHzWo5kw==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.35.0.tgz", - "integrity": "sha512-PIQeY5XDkrOysbQblSW7v3l1MDZzkTEzAfTPkj5VAu3FW8fS4ynyLg2sINp0fp3SjZ8xkRYpLqoKcYqAkhU1dw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/@rrweb/types": { "version": "2.0.0-alpha.16", "resolved": "https://registry.npmjs.org/@rrweb/types/-/types-2.0.0-alpha.16.tgz", @@ -4494,86 +3420,6 @@ } } }, - "node_modules/@swc/core-darwin-arm64": { - "version": "1.11.8", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.11.8.tgz", - "integrity": "sha512-rrSsunyJWpHN+5V1zumndwSSifmIeFQBK9i2RMQQp15PgbgUNxHK5qoET1n20pcUrmZeT6jmJaEWlQchkV//Og==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-darwin-x64": { - "version": "1.11.8", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.11.8.tgz", - "integrity": "sha512-44goLqQuuo0HgWnG8qC+ZFw/qnjCVVeqffhzFr9WAXXotogVaxM8ze6egE58VWrfEc8me8yCcxOYL9RbtjhS/Q==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.11.8", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.11.8.tgz", - "integrity": "sha512-Mzo8umKlhTWwF1v8SLuTM1z2A+P43UVhf4R8RZDhzIRBuB2NkeyE+c0gexIOJBuGSIATryuAF4O4luDu727D1w==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.11.8", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.11.8.tgz", - "integrity": "sha512-EyhO6U+QdoGYC1MeHOR0pyaaSaKYyNuT4FQNZ1eZIbnuueXpuICC7iNmLIOfr3LE5bVWcZ7NKGVPlM2StJEcgA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.11.8", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.11.8.tgz", - "integrity": "sha512-QU6wOkZnS6/QuBN1MHD6G2BgFxB0AclvTVGbqYkRA7MsVkcC29PffESqzTXnypzB252/XkhQjoB2JIt9rPYf6A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, "node_modules/@swc/core-linux-x64-gnu": { "version": "1.11.8", "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.11.8.tgz", @@ -4606,54 +3452,6 @@ "node": ">=10" } }, - "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.11.8", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.11.8.tgz", - "integrity": "sha512-EbjOzQ+B85rumHyeesBYxZ+hq3ZQn+YAAT1ZNE9xW1/8SuLoBmHy/K9YniRGVDq/2NRmp5kI5+5h5TX0asIS9A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.11.8", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.11.8.tgz", - "integrity": "sha512-Z+FF5kgLHfQWIZ1KPdeInToXLzbY0sMAashjd/igKeP1Lz0qKXVAK+rpn6ASJi85Fn8wTftCGCyQUkRVn0bTDg==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.11.8", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.11.8.tgz", - "integrity": "sha512-j6B6N0hChCeAISS6xp/hh6zR5CSCr037BAjCxNLsT8TGe5D+gYZ57heswUWXRH8eMKiRDGiLCYpPB2pkTqxCSw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, "node_modules/@swc/counter": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", @@ -8208,19 +7006,6 @@ "optional": true, "peer": true }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -15237,19 +14022,6 @@ } } }, - "node_modules/vite/node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/w3c-xmlserializer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", diff --git a/src/frontend/src/App.tsx b/src/frontend/src/App.tsx index 9c7a659e5..a30557cdc 100644 --- a/src/frontend/src/App.tsx +++ b/src/frontend/src/App.tsx @@ -1,10 +1,19 @@ import "@xyflow/react/dist/style.css"; -import { Suspense } from "react"; +import { Suspense, useEffect } from "react"; import { RouterProvider } from "react-router-dom"; import { LoadingPage } from "./pages/LoadingPage"; import router from "./routes"; +import { useDarkStore } from "./stores/darkStore"; export default function App() { + const dark = useDarkStore((state) => state.dark); + useEffect(() => { + if (!dark) { + document.getElementById("body")!.classList.remove("dark"); + } else { + document.getElementById("body")!.classList.add("dark"); + } + }, [dark]); return ( }> diff --git a/src/frontend/src/assets/LangflowLogoColor.svg b/src/frontend/src/assets/LangflowLogoColor.svg new file mode 100644 index 000000000..4eaa8203d --- /dev/null +++ b/src/frontend/src/assets/LangflowLogoColor.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/frontend/src/components/core/appHeaderComponent/components/FlowMenu/index.tsx b/src/frontend/src/components/core/appHeaderComponent/components/FlowMenu/index.tsx index 5f3873558..6e0af694f 100644 --- a/src/frontend/src/components/core/appHeaderComponent/components/FlowMenu/index.tsx +++ b/src/frontend/src/components/core/appHeaderComponent/components/FlowMenu/index.tsx @@ -30,7 +30,8 @@ import useAlertStore from "@/stores/alertStore"; import useFlowsManagerStore from "@/stores/flowsManagerStore"; import useFlowStore from "@/stores/flowStore"; import { useShortcutsStore } from "@/stores/shortcuts"; -import { cn } from "@/utils/utils"; +import { swatchColors } from "@/utils/styleUtils"; +import { cn, getNumberFromString } from "@/utils/utils"; import { useQueryClient } from "@tanstack/react-query"; export const MenuBar = ({}: {}): JSX.Element => { @@ -228,6 +229,12 @@ export const MenuBar = ({}: {}): JSX.Element => { } }, [flowName]); + const swatchIndex = + (currentFlow?.gradient && !isNaN(parseInt(currentFlow?.gradient)) + ? parseInt(currentFlow?.gradient) + : getNumberFromString(currentFlow?.gradient ?? currentFlow?.id ?? "")) % + swatchColors.length; + return currentFlow && onFlowPage ? (
{ > /
+
+ +
setIsExpanded(!isExpanded)} > -
+
{headerIcon && ( )} @@ -102,7 +107,9 @@ export function ContentBlockDisplay({
- + {!playgroundPage && ( + + )} diff --git a/src/frontend/src/components/core/chatComponents/ContentDisplay.tsx b/src/frontend/src/components/core/chatComponents/ContentDisplay.tsx index 1100e0255..23d9a5e06 100644 --- a/src/frontend/src/components/core/chatComponents/ContentDisplay.tsx +++ b/src/frontend/src/components/core/chatComponents/ContentDisplay.tsx @@ -10,9 +10,11 @@ import DurationDisplay from "./DurationDisplay"; export default function ContentDisplay({ content, chatId, + playgroundPage, }: { content: ContentType; chatId: string; + playgroundPage?: boolean; }) { // First render the common BaseContent elements if they exist const renderHeader = content.header && ( @@ -39,7 +41,7 @@ export default function ContentDisplay({
); - const renderDuration = content.duration !== undefined && ( + const renderDuration = content.duration !== undefined && !playgroundPage && (
diff --git a/src/frontend/src/components/core/flowToolbarComponent/components/deploy-dropdown.tsx b/src/frontend/src/components/core/flowToolbarComponent/components/deploy-dropdown.tsx new file mode 100644 index 000000000..cb2d7148c --- /dev/null +++ b/src/frontend/src/components/core/flowToolbarComponent/components/deploy-dropdown.tsx @@ -0,0 +1,208 @@ +import IconComponent from "@/components/common/genericIconComponent"; +import ShadTooltipComponent from "@/components/common/shadTooltipComponent"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Switch } from "@/components/ui/switch"; +import { usePatchUpdateFlow } from "@/controllers/API/queries/flows/use-patch-update-flow"; +import { ENABLE_WIDGET } from "@/customization/feature-flags"; +import ApiModal from "@/modals/apiModal/new-api-modal"; +import EmbedModal from "@/modals/EmbedModal/embed-modal"; +import useAlertStore from "@/stores/alertStore"; +import useAuthStore from "@/stores/authStore"; +import useFlowsManagerStore from "@/stores/flowsManagerStore"; +import useFlowStore from "@/stores/flowStore"; +import { useState } from "react"; + +export default function PublishDropdown() { + const domain = window.location.origin; + const [openEmbedModal, setOpenEmbedModal] = useState(false); + const currentFlow = useFlowsManagerStore((state) => state.currentFlow); + const flowId = currentFlow?.id; + const flowName = currentFlow?.name; + const setErrorData = useAlertStore((state) => state.setErrorData); + const { mutateAsync } = usePatchUpdateFlow(); + const flows = useFlowsManagerStore((state) => state.flows); + const setFlows = useFlowsManagerStore((state) => state.setFlows); + const setCurrentFlow = useFlowStore((state) => state.setCurrentFlow); + const isPublished = currentFlow?.access_type === "public"; + const hasIO = useFlowStore((state) => state.hasIO); + const isAuth = useAuthStore((state) => !!state.autoLogin); + const [openApiModal, setOpenApiModal] = useState(false); + + const handlePublishedSwitch = async (checked: boolean) => { + mutateAsync( + { + id: flowId ?? "", + access_type: checked ? "private" : "public", + }, + { + onSuccess: (updatedFlow) => { + if (flows) { + setFlows( + flows.map((flow) => { + if (flow.id === updatedFlow.id) { + return updatedFlow; + } + return flow; + }), + ); + setCurrentFlow(updatedFlow); + } else { + setErrorData({ + title: "Failed to save flow", + list: ["Flows variable undefined"], + }); + } + }, + onError: (e) => { + setErrorData({ + title: "Failed to save flow", + list: [e.message], + }); + }, + }, + ); + }; + + // using js const instead of applies.css because of group tag + const groupStyle = "text-muted-foreground group-hover:text-foreground"; + const externalUrlStyle = + "opacity-0 transition-all duration-300 group-hover:translate-x-3 group-hover:opacity-100 group-focus-visible:translate-x-3 group-focus-visible:opacity-100"; + + return ( + <> + + + + + + setOpenApiModal(true)} + > +
+ + API access +
+
+ {ENABLE_WIDGET && ( + setOpenEmbedModal(true)} + className="deploy-dropdown-item group" + > +
+ + Embed into site +
+
+ )} + +
+ { + if (hasIO) { + if (isPublished) { + window.open(`${domain}/playground/${flowId}`, "_blank"); + } + } + }} + > +
+ + Shareable Playground +
+
+
+ { + e.preventDefault(); + e.stopPropagation(); + handlePublishedSwitch(isPublished); + }} + /> +
+
+
+ {/* +
+ + Langflow SDK + +
+
*/} +
+
+ + <> + + + + ); +} diff --git a/src/frontend/src/components/core/flowToolbarComponent/components/flow-toolbar-options.tsx b/src/frontend/src/components/core/flowToolbarComponent/components/flow-toolbar-options.tsx new file mode 100644 index 000000000..c9e6cf724 --- /dev/null +++ b/src/frontend/src/components/core/flowToolbarComponent/components/flow-toolbar-options.tsx @@ -0,0 +1,23 @@ +import useFlowStore from "@/stores/flowStore"; +import { useState } from "react"; +import PublishDropdown from "./deploy-dropdown"; +import PlaygroundButton from "./playground-button"; + +export default function FlowToolbarOptions() { + const [open, setOpen] = useState(false); + const hasIO = useFlowStore((state) => state.hasIO); + + return ( +
+
+ +
+ +
+ ); +} diff --git a/src/frontend/src/components/core/flowToolbarComponent/components/playground-button.tsx b/src/frontend/src/components/core/flowToolbarComponent/components/playground-button.tsx index 384942fa5..13b63f784 100644 --- a/src/frontend/src/components/core/flowToolbarComponent/components/playground-button.tsx +++ b/src/frontend/src/components/core/flowToolbarComponent/components/playground-button.tsx @@ -1,13 +1,21 @@ import ForwardedIconComponent from "@/components/common/genericIconComponent"; import ShadTooltip from "@/components/common/shadTooltipComponent"; +import { PLAYGROUND_BUTTON_NAME } from "@/constants/constants"; +import { ENABLE_PUBLISH } from "@/customization/feature-flags"; import IOModal from "@/modals/IOModal/new-modal"; const PlaygroundButton = ({ hasIO, open, setOpen, canvasOpen }) => { const PlayIcon = () => ( - + ); - const ButtonLabel = () => Playground; + const ButtonLabel = () => ( + {PLAYGROUND_BUTTON_NAME} + ); const ActiveButton = () => (
-
-
- -
- {ENABLE_API && ( - <> -
- {currentFlow && currentFlow.data && ( - -
- - API -
-
- )} -
- - )} - {ENABLE_LANGFLOW_STORE && ( -
-
- {ModalMemo} -
+ {ENABLE_PUBLISH ? ( + + ) : ( +
+
+
- )} -
+ {ENABLE_API && ( + <> +
+ {currentFlow && currentFlow.data && ( + +
+ + API +
+
+ )} +
+ + )} + {ENABLE_LANGFLOW_STORE && ( +
+
+ {ModalMemo} +
+
+ )} +
+ )}
diff --git a/src/frontend/src/components/ui/dialog.tsx b/src/frontend/src/components/ui/dialog.tsx index a1fa23a4f..8cf0e05bc 100644 --- a/src/frontend/src/components/ui/dialog.tsx +++ b/src/frontend/src/components/ui/dialog.tsx @@ -55,45 +55,56 @@ const DialogContent = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { hideTitle?: boolean; + closeButtonClassName?: string; } ->(({ className, children, hideTitle = false, ...props }, ref) => { - // Check if DialogTitle is included in children - const hasDialogTitle = React.Children.toArray(children).some( - (child) => React.isValidElement(child) && child.type === DialogTitle, - ); +>( + ( + { className, children, hideTitle = false, closeButtonClassName, ...props }, + ref, + ) => { + // Check if DialogTitle is included in children + const hasDialogTitle = React.Children.toArray(children).some( + (child) => React.isValidElement(child) && child.type === DialogTitle, + ); - return ( - - - - {!hasDialogTitle && ( - - Dialog - - )} - {children} - + + - - - Close - - - - - ); -}); + {!hasDialogTitle && ( + + Dialog + + )} + {children} + + + + Close + + + + + ); + }, +); DialogContent.displayName = DialogPrimitive.Content.displayName; const DialogHeader = ({ diff --git a/src/frontend/src/constants/constants.ts b/src/frontend/src/constants/constants.ts index 2d12b941a..18ef1075c 100644 --- a/src/frontend/src/constants/constants.ts +++ b/src/frontend/src/constants/constants.ts @@ -1004,6 +1004,7 @@ export const DEFAULT_PLACEHOLDER = "Type something..."; export const DEFAULT_TOOLSET_PLACEHOLDER = "Used as a tool"; +export const PLAYGROUND_BUTTON_NAME = "Playground"; export const POLLING_MESSAGES = { ENDPOINT_NOT_AVAILABLE: "Endpoint not available", STREAMING_NOT_SUPPORTED: "Streaming not supported", diff --git a/src/frontend/src/controllers/API/api.tsx b/src/frontend/src/controllers/API/api.tsx index 2fc6a886c..faa35f05b 100644 --- a/src/frontend/src/controllers/API/api.tsx +++ b/src/frontend/src/controllers/API/api.tsx @@ -67,7 +67,10 @@ function ApiInterceptor() { if (isAuthenticationError && !IS_AUTO_LOGIN) { if (autoLogin !== undefined && !autoLogin) { - if (error?.config?.url?.includes("github")) { + if ( + error?.config?.url?.includes("github") || + error?.config?.url?.includes("public") + ) { return Promise.reject(error); } const stillRefresh = checkErrorCount(); diff --git a/src/frontend/src/controllers/API/helpers/constants.ts b/src/frontend/src/controllers/API/helpers/constants.ts index 2d8809cc1..54bfca6c4 100644 --- a/src/frontend/src/controllers/API/helpers/constants.ts +++ b/src/frontend/src/controllers/API/helpers/constants.ts @@ -23,6 +23,7 @@ export const URLs = { STARTER_PROJECTS: `starter-projects`, SIDEBAR_CATEGORIES: `sidebar_categories`, ALL: `all`, + PUBLIC_FLOW: `/flows/public_flow`, } as const; export function getURL(key: keyof typeof URLs, params: any = {}) { diff --git a/src/frontend/src/controllers/API/queries/flows/use-get-flow.ts b/src/frontend/src/controllers/API/queries/flows/use-get-flow.ts index e059db1d5..85334fd2e 100644 --- a/src/frontend/src/controllers/API/queries/flows/use-get-flow.ts +++ b/src/frontend/src/controllers/API/queries/flows/use-get-flow.ts @@ -8,6 +8,7 @@ import { UseRequestProcessor } from "../../services/request-processor"; interface IGetFlow { id: string; + public?: boolean; } // add types for error handling and success @@ -19,7 +20,7 @@ export const useGetFlow: useMutationFunctionType = ( const getFlowFn = async (payload: IGetFlow): Promise => { const response = await api.get( - `${getURL("FLOWS")}/${payload.id}`, + `${getURL(payload.public ? "PUBLIC_FLOW" : "FLOWS")}/${payload.id}`, ); const flowsArrayToProcess = [response.data]; diff --git a/src/frontend/src/controllers/API/queries/flows/use-patch-update-flow.ts b/src/frontend/src/controllers/API/queries/flows/use-patch-update-flow.ts index 2a27edab2..c5100678f 100644 --- a/src/frontend/src/controllers/API/queries/flows/use-patch-update-flow.ts +++ b/src/frontend/src/controllers/API/queries/flows/use-patch-update-flow.ts @@ -7,12 +7,13 @@ import { UseRequestProcessor } from "../../services/request-processor"; interface IPatchUpdateFlow { id: string; - name: string; - data: ReactFlowJsonObject; - description: string; - folder_id: string | null | undefined; - endpoint_name: string | null | undefined; + name?: string; + data?: ReactFlowJsonObject; + description?: string; + folder_id?: string | null | undefined; + endpoint_name?: string | null | undefined; locked?: boolean | null | undefined; + access_type?: "public" | "private" | "protected"; } export const usePatchUpdateFlow: useMutationFunctionType< @@ -21,15 +22,11 @@ export const usePatchUpdateFlow: useMutationFunctionType< > = (options?) => { const { mutate, queryClient } = UseRequestProcessor(); - const PatchUpdateFlowFn = async (payload: IPatchUpdateFlow): Promise => { - const response = await api.patch(`${getURL("FLOWS")}/${payload.id}`, { - name: payload.name, - data: payload.data, - description: payload.description, - folder_id: payload.folder_id || null, - endpoint_name: payload.endpoint_name || null, - locked: payload.locked || false, - }); + const PatchUpdateFlowFn = async ({ + id, + ...payload + }: IPatchUpdateFlow): Promise => { + const response = await api.patch(`${getURL("FLOWS")}/${id}`, payload); return response.data; }; diff --git a/src/frontend/src/controllers/API/queries/messages/use-get-messages.ts b/src/frontend/src/controllers/API/queries/messages/use-get-messages.ts index c0be11627..52d5408fd 100644 --- a/src/frontend/src/controllers/API/queries/messages/use-get-messages.ts +++ b/src/frontend/src/controllers/API/queries/messages/use-get-messages.ts @@ -1,3 +1,4 @@ +import useFlowStore from "@/stores/flowStore"; import { useMessagesStore } from "@/stores/messagesStore"; import { keepPreviousData } from "@tanstack/react-query"; import { ColDef, ColGroupDef } from "ag-grid-community"; @@ -26,6 +27,7 @@ export const useGetMessagesQuery: useQueryFunctionType< const { query } = UseRequestProcessor(); const getMessagesFn = async (id?: string, params = {}) => { + const isPlaygroundPage = useFlowStore.getState().playgroundPage; const config = {}; if (id) { config["params"] = { flow_id: id }; @@ -33,7 +35,13 @@ export const useGetMessagesQuery: useQueryFunctionType< if (params) { config["params"] = { ...config["params"], ...params }; } - return await api.get(`${getURL("MESSAGES")}`, config); + if (!isPlaygroundPage) { + return await api.get(`${getURL("MESSAGES")}`, config); + } else { + return { + data: JSON.parse(window.sessionStorage.getItem(id ?? "") || "[]"), + }; + } }; const responseFn = async () => { diff --git a/src/frontend/src/controllers/API/queries/messages/use-put-update-messages.ts b/src/frontend/src/controllers/API/queries/messages/use-put-update-messages.ts index 1ea24cda5..229210d20 100644 --- a/src/frontend/src/controllers/API/queries/messages/use-put-update-messages.ts +++ b/src/frontend/src/controllers/API/queries/messages/use-put-update-messages.ts @@ -1,4 +1,5 @@ import useFlowsManagerStore from "@/stores/flowsManagerStore"; +import useFlowStore from "@/stores/flowStore"; import { useMutationFunctionType } from "@/types/api"; import { Message } from "@/types/messages"; import { UseMutationResult } from "@tanstack/react-query"; @@ -18,15 +19,29 @@ export const useUpdateMessage: useMutationFunctionType< const { mutate, queryClient } = UseRequestProcessor(); const updateMessageApi = async (data: UpdateMessageParams) => { + const isPlayground = useFlowStore.getState().playgroundPage; + const flowId = useFlowsManagerStore.getState().currentFlowId; const message = data.message; if (message.files && typeof message.files === "string") { message.files = JSON.parse(message.files); } - const result = await api.put( - `${getURL("MESSAGES")}/${message.id}`, - message, - ); - return result.data; + if (isPlayground && flowId) { + const messages = JSON.parse(sessionStorage.getItem(flowId) || ""); + const messageIndex = messages.findIndex( + (m: Message) => m.id === message.id, + ); + messages[messageIndex] = message; + sessionStorage.setItem(flowId, JSON.stringify(messages)); + return { + data: message, + }; + } else { + const result = await api.put( + `${getURL("MESSAGES")}/${message.id}`, + message, + ); + return result.data; + } }; const mutation: UseMutationResult = mutate( diff --git a/src/frontend/src/controllers/API/queries/messages/use-rename-session.ts b/src/frontend/src/controllers/API/queries/messages/use-rename-session.ts index 51f088eda..c4cc5e142 100644 --- a/src/frontend/src/controllers/API/queries/messages/use-rename-session.ts +++ b/src/frontend/src/controllers/API/queries/messages/use-rename-session.ts @@ -1,3 +1,4 @@ +import useFlowStore from "@/stores/flowStore"; import { useMutationFunctionType } from "@/types/api"; import { Message } from "@/types/messages"; import { UseMutationResult } from "@tanstack/react-query"; @@ -17,14 +18,31 @@ export const useUpdateSessionName: useMutationFunctionType< const { mutate, queryClient } = UseRequestProcessor(); const updateSessionApi = async (data: UpdateSessionParams) => { - const result = await api.patch( - `${getURL("MESSAGES")}/session/${data.old_session_id}`, - null, - { - params: { new_session_id: data.new_session_id }, - }, - ); - return result.data; + const isPlayground = useFlowStore.getState().playgroundPage; + const flowId = useFlowStore.getState().currentFlow?.id; + // if we are in playground we will edit the local storage instead of the API + if (isPlayground && flowId) { + const messages = JSON.parse(sessionStorage.getItem(flowId) || ""); + const messagesWithNewSessionId = messages.map((message: Message) => { + if (message.session_id === data.old_session_id) { + message.session_id = data.new_session_id; + } + return message; + }); + sessionStorage.setItem(flowId, JSON.stringify(messagesWithNewSessionId)); + return { + data: messagesWithNewSessionId, + }; + } else { + const result = await api.patch( + `${getURL("MESSAGES")}/session/${data.old_session_id}`, + null, + { + params: { new_session_id: data.new_session_id }, + }, + ); + return result.data; + } }; const mutation: UseMutationResult = diff --git a/src/frontend/src/customization/feature-flags.ts b/src/frontend/src/customization/feature-flags.ts index e54902a85..746c12ff6 100644 --- a/src/frontend/src/customization/feature-flags.ts +++ b/src/frontend/src/customization/feature-flags.ts @@ -8,3 +8,5 @@ export const ENABLE_MVPS = false; export const ENABLE_CUSTOM_PARAM = false; export const ENABLE_INTEGRATIONS = false; export const ENABLE_DATASTAX_LANGFLOW = false; +export const ENABLE_PUBLISH = true; +export const ENABLE_WIDGET = true; diff --git a/src/frontend/src/customization/utils/urls.ts b/src/frontend/src/customization/utils/urls.ts new file mode 100644 index 000000000..8546eb7b8 --- /dev/null +++ b/src/frontend/src/customization/utils/urls.ts @@ -0,0 +1,3 @@ +export const LangflowButtonRedirectTarget = () => { + return "https://langflow.org"; +}; diff --git a/src/frontend/src/icons/BW python/Python.jsx b/src/frontend/src/icons/BW python/Python.jsx new file mode 100644 index 000000000..da085dfa0 --- /dev/null +++ b/src/frontend/src/icons/BW python/Python.jsx @@ -0,0 +1,33 @@ +export const BWSvgPython = (props) => ( + + + + + + + + +); +export default BWSvgPython; diff --git a/src/frontend/src/icons/BW python/index.tsx b/src/frontend/src/icons/BW python/index.tsx new file mode 100644 index 000000000..25c2a9c4b --- /dev/null +++ b/src/frontend/src/icons/BW python/index.tsx @@ -0,0 +1,11 @@ +import { useDarkStore } from "@/stores/darkStore"; +import React, { forwardRef } from "react"; +import BWSvgPython from "./Python"; + +export const BWPythonIcon = forwardRef< + SVGSVGElement, + React.PropsWithChildren<{}> +>((props, ref) => { + const isdark = useDarkStore((state) => state.dark.toString()); + return ; +}); diff --git a/src/frontend/src/icons/BW python/logo.svg b/src/frontend/src/icons/BW python/logo.svg new file mode 100644 index 000000000..bc7170f9a --- /dev/null +++ b/src/frontend/src/icons/BW python/logo.svg @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/src/frontend/src/icons/JSicon/Frame.svg b/src/frontend/src/icons/JSicon/Frame.svg new file mode 100644 index 000000000..12d4cef86 --- /dev/null +++ b/src/frontend/src/icons/JSicon/Frame.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/frontend/src/icons/JSicon/JSIcon.jsx b/src/frontend/src/icons/JSicon/JSIcon.jsx new file mode 100644 index 000000000..42459bc09 --- /dev/null +++ b/src/frontend/src/icons/JSicon/JSIcon.jsx @@ -0,0 +1,26 @@ +const SvgJSIcon = (props) => ( + + + + + + + + + + + +); +export default SvgJSIcon; diff --git a/src/frontend/src/icons/JSicon/index.tsx b/src/frontend/src/icons/JSicon/index.tsx new file mode 100644 index 000000000..a66fd867b --- /dev/null +++ b/src/frontend/src/icons/JSicon/index.tsx @@ -0,0 +1,10 @@ +import { useDarkStore } from "@/stores/darkStore"; +import React, { forwardRef } from "react"; +import SvgJSIcon from "./JSIcon"; + +export const JSIcon = forwardRef>( + (props, ref) => { + const isdark = useDarkStore((state) => state.dark.toString()); + return ; + }, +); diff --git a/src/frontend/src/modals/EmbedModal/embed-modal.tsx b/src/frontend/src/modals/EmbedModal/embed-modal.tsx new file mode 100644 index 000000000..66d38d325 --- /dev/null +++ b/src/frontend/src/modals/EmbedModal/embed-modal.tsx @@ -0,0 +1,99 @@ +import { useDarkStore } from "@/stores/darkStore"; +import { useState } from "react"; +import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; +import { + oneDark, + oneLight, +} from "react-syntax-highlighter/dist/cjs/styles/prism"; +import IconComponent from "../../components/common/genericIconComponent"; +import { Button } from "../../components/ui/button"; +import getWidgetCode from "../apiModal/utils/get-widget-code"; +import BaseModal from "../baseModal"; + +interface EmbedModalProps { + open: boolean; + setOpen: (open: boolean) => void; + flowId: string; + flowName: string; + isAuth: boolean; + tweaksBuildedObject: {}; + activeTweaks: boolean; +} + +export default function EmbedModal({ + open, + setOpen, + flowId, + flowName, + isAuth, + tweaksBuildedObject, + activeTweaks, +}: EmbedModalProps) { + const isDark = useDarkStore((state) => state.dark); + const [isCopied, setIsCopied] = useState(false); + const widgetProps = { + flowId: flowId, + flowName: flowName, + isAuth: isAuth, + tweaksBuildedObject: tweaksBuildedObject, + activeTweaks: activeTweaks, + }; + const embedCode = getWidgetCode({ ...widgetProps, copy: false }); + const copyCode = getWidgetCode({ ...widgetProps, copy: true }); + const copyToClipboard = () => { + if (!navigator.clipboard || !navigator.clipboard.writeText) { + return; + } + + navigator.clipboard.writeText(copyCode).then(() => { + setIsCopied(true); + + setTimeout(() => { + setIsCopied(false); + }, 2000); + }); + }; + + return ( + + +
+ + Embed into site +
+
+ +
+ + + {embedCode} + +
+
+
+ ); +} diff --git a/src/frontend/src/modals/IOModal/components/IOFieldView/components/session-selector.tsx b/src/frontend/src/modals/IOModal/components/IOFieldView/components/session-selector.tsx index 40fecf1c5..db8450225 100644 --- a/src/frontend/src/modals/IOModal/components/IOFieldView/components/session-selector.tsx +++ b/src/frontend/src/modals/IOModal/components/IOFieldView/components/session-selector.tsx @@ -8,9 +8,12 @@ import { SelectTrigger, } from "@/components/ui/select-custom"; import { useUpdateSessionName } from "@/controllers/API/queries/messages/use-rename-session"; +import useFlowsManagerStore from "@/stores/flowsManagerStore"; import useFlowStore from "@/stores/flowStore"; +import { useUtilityStore } from "@/stores/utilityStore"; import { cn } from "@/utils/utils"; import React, { useEffect, useRef, useState } from "react"; +import { v5 as uuidv5 } from "uuid"; export default function SessionSelector({ deleteSession, @@ -21,6 +24,7 @@ export default function SessionSelector({ updateVisibleSession, selectedView, setSelectedView, + playgroundPage, }: { deleteSession: (session: string) => void; session: string; @@ -30,8 +34,13 @@ export default function SessionSelector({ updateVisibleSession: (session: string) => void; selectedView?: { type: string; id: string }; setSelectedView: (view: { type: string; id: string } | undefined) => void; + playgroundPage: boolean; }) { - const currentFlowId = useFlowStore((state) => state.currentFlow?.id); + const clientId = useUtilityStore((state) => state.clientId); + let realFlowId = useFlowsManagerStore((state) => state.currentFlowId); + const currentFlowId = playgroundPage + ? uuidv5(`${clientId}_${realFlowId}`, uuidv5.DNS) + : realFlowId; const [isEditing, setIsEditing] = useState(false); const [editedSession, setEditedSession] = useState(session); const { mutate: updateSessionName } = useUpdateSessionName(); diff --git a/src/frontend/src/modals/IOModal/components/chat-view-wrapper.tsx b/src/frontend/src/modals/IOModal/components/chat-view-wrapper.tsx index 6bcf576fe..2cdb76e51 100644 --- a/src/frontend/src/modals/IOModal/components/chat-view-wrapper.tsx +++ b/src/frontend/src/modals/IOModal/components/chat-view-wrapper.tsx @@ -21,23 +21,23 @@ export const ChatViewWrapper = ({ sendMessage, canvasOpen, setOpen, + playgroundTitle, + playgroundPage, }: ChatViewWrapperProps) => { return (
-
- {visibleSession && sessions.length > 0 && sidebarOpen && ( -
- {visibleSession === currentFlowId - ? "Default Session" - : `${visibleSession}`} -
+
+ > +
-
Playground
+ {visibleSession && sessions.length > 0 && ( +
+ {visibleSession === currentFlowId + ? "Default Session" + : `${visibleSession}`} +
+ )}
@@ -76,7 +88,7 @@ export const ChatViewWrapper = ({ /> - {!isPlayground && } + {!playgroundPage && }
)}
diff --git a/src/frontend/src/modals/IOModal/components/chatView/chat-view.tsx b/src/frontend/src/modals/IOModal/components/chatView/chat-view.tsx index 4ee3b3b1f..bde65dc85 100644 --- a/src/frontend/src/modals/IOModal/components/chatView/chat-view.tsx +++ b/src/frontend/src/modals/IOModal/components/chatView/chat-view.tsx @@ -4,6 +4,7 @@ import { track } from "@/customization/utils/analytics"; import { useMessagesStore } from "@/stores/messagesStore"; import { useUtilityStore } from "@/stores/utilityStore"; import { memo, useEffect, useMemo, useRef, useState } from "react"; +import { v5 as uuidv5 } from "uuid"; import useTabVisibility from "../../../../shared/hooks/use-tab-visibility"; import useFlowsManagerStore from "../../../../stores/flowsManagerStore"; import useFlowStore from "../../../../stores/flowStore"; @@ -31,10 +32,15 @@ export default function ChatView({ visibleSession, focusChat, closeChat, + playgroundPage, }: chatViewProps): JSX.Element { const flowPool = useFlowStore((state) => state.flowPool); const inputs = useFlowStore((state) => state.inputs); - const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId); + const clientId = useUtilityStore((state) => state.clientId); + let realFlowId = useFlowsManagerStore((state) => state.currentFlowId); + const currentFlowId = playgroundPage + ? uuidv5(`${clientId}_${realFlowId}`, uuidv5.DNS) + : realFlowId; const messagesRef = useRef(null); const [chatHistory, setChatHistory] = useState( undefined, @@ -171,6 +177,7 @@ export default function ChatView({ key={`${chat.id}-${index}`} updateChat={updateChat} closeChat={closeChat} + playgroundPage={playgroundPage} /> ))} @@ -212,6 +219,7 @@ export default function ChatView({
{ sendMessage({ repeat, files }); diff --git a/src/frontend/src/modals/IOModal/components/chatView/chatInput/chat-input.tsx b/src/frontend/src/modals/IOModal/components/chatView/chatInput/chat-input.tsx index 7de0862bc..c28b0ab3b 100644 --- a/src/frontend/src/modals/IOModal/components/chatView/chatInput/chat-input.tsx +++ b/src/frontend/src/modals/IOModal/components/chatView/chatInput/chat-input.tsx @@ -32,6 +32,7 @@ export default function ChatInput({ files, setFiles, isDragging, + playgroundPage, }: ChatInputType): JSX.Element { const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId); const fileInputRef = useRef(null); @@ -49,6 +50,10 @@ export default function ChatInput({ const handleFileChange = async ( event: React.ChangeEvent | ClipboardEvent, ) => { + if (playgroundPage) { + return; + } + let file: File | null = null; if ("clipboardData" in event) { @@ -238,15 +243,17 @@ export default function ChatInput({ ))}
-
- -
-
+ {!playgroundPage && ( +
+ +
+ )} +
) - ) : !ENABLE_DATASTAX_LANGFLOW ? ( + ) : !ENABLE_DATASTAX_LANGFLOW && !playgroundPage ? ( + ) : playgroundPage ? ( + ) : ( )} @@ -301,7 +307,7 @@ export default function ChatMessage({ } > {chat.sender_name} - {chat.properties?.source && ( + {chat.properties?.source && !playgroundPage && (
{chat.properties?.source.source}
@@ -310,6 +316,7 @@ export default function ChatMessage({
{chat.content_blocks && chat.content_blocks.length > 0 && ( { return ( <> @@ -51,6 +52,7 @@ export const SidebarOpenView = ({ selectedView={selectedViewField} key={index} session={session} + playgroundPage={playgroundPage} deleteSession={(session) => { handleDeleteSession(session); if (selectedViewField?.id === session) { diff --git a/src/frontend/src/modals/IOModal/new-modal.tsx b/src/frontend/src/modals/IOModal/new-modal.tsx index 71dd9478f..4a3abe3ba 100644 --- a/src/frontend/src/modals/IOModal/new-modal.tsx +++ b/src/frontend/src/modals/IOModal/new-modal.tsx @@ -1,11 +1,19 @@ +//import LangflowLogoColor from "@/assets/LangflowLogocolor.svg?react"; +import ThemeButtons from "@/components/core/appHeaderComponent/components/ThemeButtons"; import { EventDeliveryType } from "@/constants/enums"; import { useGetConfig } from "@/controllers/API/queries/config/use-get-config"; import { useDeleteMessages, useGetMessagesQuery, } from "@/controllers/API/queries/messages"; +import { ENABLE_PUBLISH } from "@/customization/feature-flags"; +import { track } from "@/customization/utils/analytics"; +import { LangflowButtonRedirectTarget } from "@/customization/utils/urls"; import { useUtilityStore } from "@/stores/utilityStore"; +import { swatchColors } from "@/utils/styleUtils"; import { useCallback, useEffect, useState } from "react"; +import { v5 as uuidv5 } from "uuid"; +import LangflowLogoColor from "../../assets/LangflowLogoColor.svg?react"; import IconComponent from "../../components/common/genericIconComponent"; import ShadTooltip from "../../components/common/shadTooltipComponent"; import { Button } from "../../components/ui/button"; @@ -14,12 +22,11 @@ import useFlowStore from "../../stores/flowStore"; import useFlowsManagerStore from "../../stores/flowsManagerStore"; import { useMessagesStore } from "../../stores/messagesStore"; import { IOModalPropsType } from "../../types/components"; -import { cn } from "../../utils/utils"; +import { cn, getNumberFromString } from "../../utils/utils"; import BaseModal from "../baseModal"; import { ChatViewWrapper } from "./components/chat-view-wrapper"; import { SelectedViewField } from "./components/selected-view-field"; import { SidebarOpenView } from "./components/sidebar-open-view"; - export default function IOModal({ children, open, @@ -27,6 +34,7 @@ export default function IOModal({ disable, isPlayground, canvasOpen, + playgroundPage, }: IOModalPropsType): JSX.Element { const allNodes = useFlowStore((state) => state.nodes); const setIOModalOpen = useFlowsManagerStore((state) => state.setIOModalOpen); @@ -54,13 +62,23 @@ export default function IOModal({ const setErrorData = useAlertStore((state) => state.setErrorData); const setSuccessData = useAlertStore((state) => state.setSuccessData); const deleteSession = useMessagesStore((state) => state.deleteSession); - const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId); + const clientId = useUtilityStore((state) => state.clientId); + let realFlowId = useFlowsManagerStore((state) => state.currentFlowId); + const currentFlowId = playgroundPage + ? uuidv5(`${clientId}_${realFlowId}`, uuidv5.DNS) + : realFlowId; + const currentFlow = useFlowsManagerStore((state) => state.currentFlow); const [sidebarOpen, setSidebarOpen] = useState(true); + const setPlaygroundPage = useFlowStore((state) => state.setPlaygroundPage); + setPlaygroundPage(!!playgroundPage); const { mutate: deleteSessionFunction } = useDeleteMessages(); const [visibleSession, setvisibleSession] = useState( currentFlowId, ); + const flowName = useFlowStore((state) => state.currentFlow?.name); + const PlaygroundTitle = + playgroundPage && ENABLE_PUBLISH && flowName ? flowName : "Playground"; useEffect(() => { setIOModalOpen(open); @@ -238,6 +256,25 @@ export default function IOModal({ }; }, []); + const showPublishOptions = playgroundPage && ENABLE_PUBLISH; + + const LangflowButtonClick = () => { + track("LangflowButtonClick"); + window.open(LangflowButtonRedirectTarget(), "_blank"); + }; + + useEffect(() => { + if (playgroundPage && messages.length > 0) { + window.sessionStorage.setItem(currentFlowId, JSON.stringify(messages)); + } + }, [playgroundPage, messages]); + + const swatchIndex = + (currentFlow?.gradient && !isNaN(parseInt(currentFlow?.gradient)) + ? parseInt(currentFlow?.gradient) + : getNumberFromString(currentFlow?.gradient ?? currentFlow?.id ?? "")) % + swatchColors.length; + return ( -
-
+
+
+
+
+ +
+ {sidebarOpen && ( +
+ {PlaygroundTitle} +
+ )} +
- {sidebarOpen && ( -
Playground
- )}
{sidebarOpen && ( )} + {sidebarOpen && showPublishOptions && ( +
+
+
Theme
+ +
+ +
+ )}
+ {!sidebarOpen && showPublishOptions && ( +
+ + + +
+ )}
{selectedViewField && ( )}
diff --git a/src/frontend/src/modals/IOModal/types/chat-view-wrapper.ts b/src/frontend/src/modals/IOModal/types/chat-view-wrapper.ts index 31b101a4a..35a629172 100644 --- a/src/frontend/src/modals/IOModal/types/chat-view-wrapper.ts +++ b/src/frontend/src/modals/IOModal/types/chat-view-wrapper.ts @@ -16,4 +16,6 @@ export type ChatViewWrapperProps = { sendMessage: (options: { repeat: number; files?: string[] }) => Promise; canvasOpen: boolean | undefined; setOpen: (open: boolean) => void; + playgroundTitle: string; + playgroundPage?: boolean; }; diff --git a/src/frontend/src/modals/IOModal/types/sidebar-open-view.ts b/src/frontend/src/modals/IOModal/types/sidebar-open-view.ts index 047c6e431..0e299a499 100644 --- a/src/frontend/src/modals/IOModal/types/sidebar-open-view.ts +++ b/src/frontend/src/modals/IOModal/types/sidebar-open-view.ts @@ -7,4 +7,5 @@ export type SidebarOpenViewProps = { handleDeleteSession: (session: string) => void; visibleSession: string | undefined; selectedViewField: { type: string; id: string } | undefined; + playgroundPage: boolean; }; diff --git a/src/frontend/src/modals/apiModal/codeTabs/code-tabs.tsx b/src/frontend/src/modals/apiModal/codeTabs/code-tabs.tsx new file mode 100644 index 000000000..9d3b8c748 --- /dev/null +++ b/src/frontend/src/modals/apiModal/codeTabs/code-tabs.tsx @@ -0,0 +1,164 @@ +import IconComponent from "@/components/common/genericIconComponent"; +import ShadTooltip from "@/components/common/shadTooltipComponent"; +import { Button } from "@/components/ui/button"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import useAuthStore from "@/stores/authStore"; +import useFlowStore from "@/stores/flowStore"; +import { useTweaksStore } from "@/stores/tweaksStore"; +import { AllNodeType } from "@/types/flow"; +import { tabsArrayType } from "@/types/tabs"; +import { hasStreaming } from "@/utils/reactflowUtils"; +import { useEffect, useState } from "react"; +import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; +import { + oneDark, + oneLight, +} from "react-syntax-highlighter/dist/cjs/styles/prism"; +import { useDarkStore } from "../../../stores/darkStore"; +import { getNewCurlCode } from "../utils/get-curl-code"; +import { getNewJsApiCode } from "../utils/get-js-api-code"; +import { getNewPythonApiCode } from "../utils/get-python-api-code"; + +export default function APITabsComponent() { + const [isCopied, setIsCopied] = useState(false); + const dark = useDarkStore((state) => state.dark); + const nodes = useFlowStore((state) => state.nodes); + const flowId = useFlowStore((state) => state.currentFlow?.id); + const autologin = useAuthStore((state) => state.autoLogin); + const inputs = useFlowStore((state) => state.inputs); + const outputs = useFlowStore((state) => state.outputs); + const hasChatInput = inputs.some((input) => input.type === "ChatInput"); + const hasChatOutput = outputs.some((output) => output.type === "ChatOutput"); + let input_value = "hello world!"; + if (hasChatInput) { + const chatInputId = inputs.find((input) => input.type === "ChatInput")?.id; + const inputNode = nodes.find((node) => node.id === chatInputId); + if (inputNode && inputNode?.data.node?.template?.input_value?.value) { + input_value = inputNode?.data.node?.template.input_value?.value; + } + } + const streaming = hasStreaming(nodes); + const tweaks = useTweaksStore((state) => state.tweaks); + const codeOptions = { + streaming: streaming, + flowId: flowId || "", + isAuthenticated: !autologin || false, + input_value: input_value, + input_type: hasChatInput ? "chat" : "text", + output_type: hasChatOutput ? "chat" : "text", + tweaksObject: tweaks, + activeTweaks: Object.values(tweaks).some( + (tweak) => Object.keys(tweak).length > 0, + ), + }; + const tabsList: tabsArrayType = [ + { + title: "Python", + icon: "BWPython", + language: "python", + code: getNewPythonApiCode(codeOptions), + copyCode: getNewPythonApiCode(codeOptions), + }, + { + title: "JavaScript", + icon: "javascript", + language: "javascript", + code: getNewJsApiCode(codeOptions), + copyCode: getNewJsApiCode(codeOptions), + }, + { + title: "cURL", + icon: "TerminalSquare", + language: "shell", + code: getNewCurlCode(codeOptions), + copyCode: getNewCurlCode(codeOptions), + }, + ]; + const [activeTab, setActiveTab] = useState(0); + + const copyToClipboard = () => { + if (!navigator.clipboard || !navigator.clipboard.writeText) { + return; + } + + navigator.clipboard.writeText(tabsList[activeTab].code).then(() => { + setIsCopied(true); + + setTimeout(() => { + setIsCopied(false); + }, 2000); + }); + }; + + useEffect(() => { + setIsCopied(false); + }, [activeTab]); + + return ( + { + setActiveTab(parseInt(value)); + }} + > +
+ {tabsList.length > 0 && tabsList[0].title !== "" ? ( + + {tabsList.map((tab, index) => ( + + + {tab.title} + + ))} + + ) : ( +
+ )} +
+ + {tabsList.map((tab, idx) => ( + +
+ + + {tab.code} + +
+
+ ))} +
+ ); +} diff --git a/src/frontend/src/modals/apiModal/new-api-modal.tsx b/src/frontend/src/modals/apiModal/new-api-modal.tsx new file mode 100644 index 000000000..26d379890 --- /dev/null +++ b/src/frontend/src/modals/apiModal/new-api-modal.tsx @@ -0,0 +1,141 @@ +import { TweaksComponent } from "@/components/core/codeTabsComponent/components/tweaksComponent"; +import { Button } from "@/components/ui/button"; +import { Separator } from "@/components/ui/separator"; +import { CustomAPIGenerator } from "@/customization/components/custom-api-generator"; +import useAuthStore from "@/stores/authStore"; +import useFlowStore from "@/stores/flowStore"; +import "ace-builds/src-noconflict/ext-language_tools"; +import "ace-builds/src-noconflict/mode-python"; +import "ace-builds/src-noconflict/theme-github"; +import "ace-builds/src-noconflict/theme-twilight"; +import { ReactNode, useEffect, useState } from "react"; +import IconComponent from "../../components/common/genericIconComponent"; +import { useTweaksStore } from "../../stores/tweaksStore"; +import BaseModal from "../baseModal"; +import APITabsComponent from "./codeTabs/code-tabs"; + +export default function ApiModal({ + children, + open: myOpen, + setOpen: mySetOpen, +}: { + children: ReactNode; + open?: boolean; + setOpen?: (a: boolean | ((o?: boolean) => boolean)) => void; +}) { + const autoLogin = useAuthStore((state) => state.autoLogin); + const nodes = useFlowStore((state) => state.nodes); + const [openTweaks, setOpenTweaks] = useState(false); + const tweaks = useTweaksStore((state) => state.tweaks); + const [open, setOpen] = + mySetOpen !== undefined && myOpen !== undefined + ? [myOpen, mySetOpen] + : useState(false); + const newInitialSetup = useTweaksStore((state) => state.newInitialSetup); + + useEffect(() => { + if (open) newInitialSetup(nodes); + }, [open]); + + return ( + <> + + {children} + + + API access requires an API key. You can{" "} + + {" "} + create an API key + {" "} + in settings. + + + ) + } + > + + + {open && ( + <> + + + + )} + + + + + + + API access requires an API key. You can{" "} + + {" "} + create an API key + {" "} + in settings. + + + ) + } + > + + Tweaks + + +
+ +
+
+
+ + ); +} diff --git a/src/frontend/src/modals/apiModal/utils/get-curl-code.tsx b/src/frontend/src/modals/apiModal/utils/get-curl-code.tsx index 877620300..27446cd5b 100644 --- a/src/frontend/src/modals/apiModal/utils/get-curl-code.tsx +++ b/src/frontend/src/modals/apiModal/utils/get-curl-code.tsx @@ -68,3 +68,56 @@ export function getCurlWebhookCode({ -d '{"any": "data"}' `.trim(); } + +export function getNewCurlCode({ + flowId, + isAuthenticated, + input_value, + input_type, + output_type, + tweaksObject, + activeTweaks, +}: { + flowId: string; + isAuthenticated: boolean; + input_value: string; + input_type: string; + output_type: string; + tweaksObject: any; + activeTweaks: boolean; +}): string { + const host = window.location.host; + const protocol = window.location.protocol; + const apiUrl = `${protocol}//${host}/api/v1/run/${flowId}`; + + const tweaksString = + tweaksObject && activeTweaks ? JSON.stringify(tweaksObject, null, 2) : "{}"; + + // Construct the payload + const payload = { + input_value: input_value, + output_type: output_type, + input_type: input_type, + ...(activeTweaks && tweaksObject + ? { tweaks: JSON.parse(tweaksString) } + : {}), + }; + + return `${ + isAuthenticated + ? `# Get API key from environment variable +if [ -z "$LANGFLOW_API_KEY" ]; then + echo "Error: LANGFLOW_API_KEY environment variable not found. Please set your API key in the environment variables." +fi +` + : "" + }curl --request POST \\ + --url '${apiUrl}?stream=false' \\ + --header 'Content-Type: application/json' \\${ + isAuthenticated + ? ` + --header "x-api-key: $LANGFLOW_API_KEY" \\` + : "" + } + --data '${JSON.stringify(payload, null, 2)}'`; +} diff --git a/src/frontend/src/modals/apiModal/utils/get-js-api-code.tsx b/src/frontend/src/modals/apiModal/utils/get-js-api-code.tsx index 67246647e..617477fef 100644 --- a/src/frontend/src/modals/apiModal/utils/get-js-api-code.tsx +++ b/src/frontend/src/modals/apiModal/utils/get-js-api-code.tsx @@ -44,3 +44,75 @@ export default function getJsApiCode({ .catch(error => console.error('Error:', error)); `; } +/** + * Generates JavaScript code for making API calls to a Langflow endpoint. + * + * @param {Object} params - The parameters for generating the API code + * @param {string} params.flowId - The ID of the flow to run + * @param {boolean} params.isAuthenticated - Whether authentication is required + * @param {string} params.input_value - The input value to send to the flow + * @param {string} params.input_type - The type of input (e.g. "text", "chat") + * @param {string} params.output_type - The type of output (e.g. "text", "chat") + * @param {Object} params.tweaksObject - Optional tweaks to customize flow behavior + * @param {boolean} params.activeTweaks - Whether tweaks should be included + * @returns {string} Generated JavaScript code as a string + */ +export function getNewJsApiCode({ + flowId, + isAuthenticated, + input_value, + input_type, + output_type, + tweaksObject, + activeTweaks, +}: { + flowId: string; + isAuthenticated: boolean; + input_value: string; + input_type: string; + output_type: string; + tweaksObject: any; + activeTweaks: boolean; +}): string { + const host = window.location.host; + const protocol = window.location.protocol; + const apiUrl = `${protocol}//${host}/api/v1/run/${flowId}`; + + const tweaksString = + tweaksObject && activeTweaks ? JSON.stringify(tweaksObject, null, 2) : "{}"; + + return `${ + isAuthenticated + ? `// Get API key from environment variable +if (!process.env.LANGFLOW_API_KEY) { + throw new Error('LANGFLOW_API_KEY environment variable not found. Please set your API key in the environment variables.'); +} +` + : "" + }const payload = { + "input_value": "${input_value}", + "output_type": "${output_type}", + "input_type": "${input_type}", + // Optional: Use session tracking if needed + "session_id": "user_1"${ + activeTweaks && tweaksObject + ? `, + "tweaks": ${tweaksString}` + : "" + } +}; + +const options = { + method: 'POST', + headers: { + 'Content-Type': 'application/json'${isAuthenticated ? ',\n "x-api-key": process.env.LANGFLOW_API_KEY' : ""} + }, + body: JSON.stringify(payload) +}; + +fetch('${apiUrl}', options) + .then(response => response.json()) + .then(response => console.log(response)) + .catch(err => console.error(err)); + `; +} diff --git a/src/frontend/src/modals/apiModal/utils/get-python-api-code.tsx b/src/frontend/src/modals/apiModal/utils/get-python-api-code.tsx index 0931d1eb2..79bffd92b 100644 --- a/src/frontend/src/modals/apiModal/utils/get-python-api-code.tsx +++ b/src/frontend/src/modals/apiModal/utils/get-python-api-code.tsx @@ -114,3 +114,76 @@ if __name__ == "__main__": main() `; } + +export function getNewPythonApiCode({ + flowId, + isAuthenticated, + input_value, + input_type, + output_type, + tweaksObject, + activeTweaks, +}: { + flowId: string; + isAuthenticated: boolean; + input_value: string; + input_type: string; + output_type: string; + tweaksObject: any; + activeTweaks: boolean; +}): string { + const host = window.location.host; + const protocol = window.location.protocol; + const apiUrl = `${protocol}//${host}/api/v1/run/${flowId}`; + + const tweaksString = + tweaksObject && activeTweaks + ? JSON.stringify(tweaksObject, null, 4) + .replace(/true/g, "True") + .replace(/false/g, "False") + .replace(/null/g, "None") + : "{}"; + + return `import requests +${ + isAuthenticated + ? `import os +# API Configuration +try: + api_key = os.environ["LANGFLOW_API_KEY"] +except KeyError: + raise ValueError("LANGFLOW_API_KEY environment variable not found. Please set your API key in the environment variables.")\n` + : "" +}url = "${apiUrl}" # The complete API endpoint URL for this flow + +# Request payload configuration +payload = { + "input_value": "${input_value}", # The input value to be processed by the flow + "output_type": "${output_type}", # Specifies the expected output format + "input_type": "${input_type}" # Specifies the input format${ + activeTweaks && tweaksObject + ? `, + "tweaks": ${tweaksString} # Custom tweaks to modify flow behavior` + : "" + } +} + +# Request headers +headers = { + "Content-Type": "application/json"${isAuthenticated ? ',\n "x-api-key": api_key # Authentication key from environment variable' : ""} +} + +try: + # Send API request + response = requests.request("POST", url, json=payload, headers=headers) + response.raise_for_status() # Raise exception for bad status codes + + # Print response + print(response.text) + +except requests.exceptions.RequestException as e: + print(f"Error making API request: {e}") +except ValueError as e: + print(f"Error parsing response: {e}") + `; +} diff --git a/src/frontend/src/modals/apiModal/utils/get-widget-code.tsx b/src/frontend/src/modals/apiModal/utils/get-widget-code.tsx index dd3e76bf6..a1ab03ebb 100644 --- a/src/frontend/src/modals/apiModal/utils/get-widget-code.tsx +++ b/src/frontend/src/modals/apiModal/utils/get-widget-code.tsx @@ -9,9 +9,18 @@ export default function getWidgetCode({ flowId, flowName, isAuth, + copy = false, }: GetCodeType): string { - return ` + const source = copy + ? `` + : ``; + return `${source} `; + }> +`; } diff --git a/src/frontend/src/modals/baseModal/helpers/switch-case-size.ts b/src/frontend/src/modals/baseModal/helpers/switch-case-size.ts index 23196ce8f..8882dd9d8 100644 --- a/src/frontend/src/modals/baseModal/helpers/switch-case-size.ts +++ b/src/frontend/src/modals/baseModal/helpers/switch-case-size.ts @@ -18,12 +18,16 @@ export const switchCaseModalSize = (size: string) => { minWidth = "min-w-[40vw]"; height = "h-[40vh]"; break; + case "medium-small-tall": + minWidth = "min-w-[50vw]"; + height = "h-[70vh]"; + break; case "small-h-full": minWidth = "min-w-[40vw]"; height = ""; break; case "medium": - minWidth = "min-w-[60vw]"; + minWidth = "min-w-[60vw] max-w-[720px]"; height = "h-[60vh]"; break; case "medium-tall": @@ -72,6 +76,11 @@ export const switchCaseModalSize = (size: string) => { height = "h-[95vh]"; break; + case "retangular": + minWidth = "!min-w-[900px]"; + height = "min-h-[232px]"; + break; + default: minWidth = "min-w-[80vw]"; height = "h-[90vh]"; diff --git a/src/frontend/src/modals/baseModal/index.tsx b/src/frontend/src/modals/baseModal/index.tsx index 65ba7f986..566e067f9 100644 --- a/src/frontend/src/modals/baseModal/index.tsx +++ b/src/frontend/src/modals/baseModal/index.tsx @@ -73,7 +73,7 @@ const Trigger: React.FC = ({ const Header: React.FC<{ children: ReactNode; - description: string | JSX.Element | null; + description?: string | JSX.Element | null; clampDescription?: number; }> = ({ children, @@ -85,11 +85,13 @@ const Header: React.FC<{ {children} - - {description} - + {description && ( + + {description} + + )} ); }; @@ -166,6 +168,7 @@ interface BaseModalProps { setOpen?: (open: boolean) => void; size?: | "x-small" + | "retangular" | "smaller" | "small" | "medium" @@ -176,6 +179,7 @@ interface BaseModalProps { | "large-h-full" | "templates" | "small-h-full" + | "medium-small-tall" | "medium-h-full" | "md-thin" | "sm-thin" @@ -188,6 +192,7 @@ interface BaseModalProps { type?: "modal" | "dialog" | "full-screen"; onSubmit?: () => void; onEscapeKeyDown?: (e: KeyboardEvent) => void; + closeButtonClassName?: string; } function BaseModal({ className, @@ -199,6 +204,7 @@ function BaseModal({ type = "dialog", onSubmit, onEscapeKeyDown, + closeButtonClassName, }: BaseModalProps) { const headerChild = React.Children.toArray(children).find( (child) => (child as React.ReactElement).type === Header, @@ -253,6 +259,7 @@ function BaseModal({ onOpenAutoFocus={(event) => event.preventDefault()} onEscapeKeyDown={onEscapeKeyDown} className={contentClasses} + closeButtonClassName={closeButtonClassName} > {onSubmit ? ( { - if (!dark) { - document.getElementById("body")!.classList.remove("dark"); - } else { - document.getElementById("body")!.classList.add("dark"); - } - }, [dark]); - return ( //need parent component with width and height <> diff --git a/src/frontend/src/pages/Playground/index.tsx b/src/frontend/src/pages/Playground/index.tsx index 9d3834658..3de0a1e0f 100644 --- a/src/frontend/src/pages/Playground/index.tsx +++ b/src/frontend/src/pages/Playground/index.tsx @@ -2,17 +2,23 @@ import { useGetFlow } from "@/controllers/API/queries/flows/use-get-flow"; import { useCustomNavigate } from "@/customization/hooks/use-custom-navigate"; import { track } from "@/customization/utils/analytics"; import IOModal from "@/modals/IOModal/new-modal"; +import useFlowStore from "@/stores/flowStore"; import { useStoreStore } from "@/stores/storeStore"; +import { useUtilityStore } from "@/stores/utilityStore"; +import { CookieOptions, getCookie, setCookie } from "@/utils/utils"; import { useEffect } from "react"; import { useParams } from "react-router-dom"; +import { v4 as uuid } from "uuid"; import { getComponent } from "../../controllers/API"; import useFlowsManagerStore from "../../stores/flowsManagerStore"; -import cloneFLowWithParent from "../../utils/storeUtils"; - +import cloneFLowWithParent, { + getInputsAndOutputs, +} from "../../utils/storeUtils"; export default function PlaygroundPage() { const setCurrentFlow = useFlowsManagerStore((state) => state.setCurrentFlow); const currentSavedFlow = useFlowsManagerStore((state) => state.currentFlow); - const validApiKey = useStoreStore((state) => state.validApiKey); + const setClientId = useUtilityStore((state) => state.setClientId); + const { id } = useParams(); const { mutateAsync: getFlow } = useGetFlow(); @@ -23,22 +29,11 @@ export default function PlaygroundPage() { async function getFlowData() { try { - const flow = await getFlow({ id: id! }); + const flow = await getFlow({ id: id!, public: true }); return flow; } catch (error: any) { - if (error?.response?.status === 404) { - if (!validApiKey) { - return null; - } - try { - const res = await getComponent(id!); - const newFlow = cloneFLowWithParent(res, res.id, false, true); - return newFlow; - } catch (componentError) { - return null; - } - } - return null; + console.log(error); + navigate("/"); } } @@ -57,16 +52,48 @@ export default function PlaygroundPage() { initializeFlow(); setIsLoading(false); - }, [id, validApiKey]); + }, [id]); useEffect(() => { if (id) track("Playground Page Loaded", { flowId: id }); }, []); + useEffect(() => { + document.title = currentSavedFlow?.name || "Langflow"; + if (currentSavedFlow?.data) { + const { inputs, outputs } = getInputsAndOutputs( + currentSavedFlow?.data?.nodes || [], + ); + if ( + (inputs.length === 0 && outputs.length === 0) || + currentSavedFlow?.access_type !== "public" + ) { + // redirect to the home page + navigate("/"); + } + } + }, [currentSavedFlow]); + + useEffect(() => { + // Get client ID from cookie or create new one + const clientId = getCookie("client_id"); + if (!clientId) { + const newClientId = uuid(); + const cookieOptions: CookieOptions = { + secure: window.location.protocol === "https:", + sameSite: "Strict", + }; + setCookie("client_id", newClientId, cookieOptions); + setClientId(newClientId); + } else { + setClientId(clientId); + } + }, []); + return (
{currentSavedFlow && ( - {}} isPlayground> + {}} isPlayground playgroundPage> <> )} diff --git a/src/frontend/src/routes.tsx b/src/frontend/src/routes.tsx index 6e133b99a..801ceed5e 100644 --- a/src/frontend/src/routes.tsx +++ b/src/frontend/src/routes.tsx @@ -36,15 +36,25 @@ const AdminPage = lazy(() => import("./pages/AdminPage")); const LoginAdminPage = lazy(() => import("./pages/AdminPage/LoginPage")); const DeleteAccountPage = lazy(() => import("./pages/DeleteAccountPage")); -// const PlaygroundPage = lazy(() => import("./pages/Playground")); +const PlaygroundPage = lazy(() => import("./pages/Playground")); const SignUp = lazy(() => import("./pages/SignUpPage")); const router = createBrowserRouter( createRoutesFromElements([ + + + + + } + /> + , + } @@ -151,9 +161,6 @@ const router = createBrowserRouter( } /> - {/* - } /> - */} ((set, get) => ({ + playgroundPage: false, + setPlaygroundPage: (playgroundPage) => { + set({ playgroundPage }); + }, positionDictionary: {}, setPositionDictionary: (positionDictionary) => { set({ positionDictionary }); @@ -604,6 +608,7 @@ const useFlowStore = create((set, get) => ({ session?: string; stream?: boolean; }) => { + const playgroundPage = get().playgroundPage; get().setIsBuilding(true); const currentFlow = useFlowsManagerStore.getState().currentFlow; const setSuccessData = useAlertStore.getState().setSuccessData; @@ -827,6 +832,7 @@ const useFlowStore = create((set, get) => ({ nodes: get().nodes || undefined, edges: get().edges || undefined, logBuilds: get().onFlowPage, + playgroundPage, stream, }); get().setIsBuilding(false); diff --git a/src/frontend/src/stores/tweaksStore.ts b/src/frontend/src/stores/tweaksStore.ts index 290ddffbe..df36f29a6 100644 --- a/src/frontend/src/stores/tweaksStore.ts +++ b/src/frontend/src/stores/tweaksStore.ts @@ -1,7 +1,7 @@ import { getChangesType } from "@/modals/apiModal/utils/get-changes-types"; import { getNodesWithDefaultValue } from "@/modals/apiModal/utils/get-nodes-with-default-value"; import { createTabsArray } from "@/modals/apiModal/utils/tabs-array"; -import { FlowType, NodeDataType } from "@/types/flow"; +import { AllNodeType, FlowType, NodeDataType } from "@/types/flow"; import { GetCodesType } from "@/types/tweaks"; import { customStringify } from "@/utils/reactflowUtils"; import { create } from "zustand"; @@ -10,6 +10,7 @@ import useFlowStore from "./flowStore"; export const useTweaksStore = create((set, get) => ({ activeTweaks: false, + tweaks: {}, setActiveTweaks: (activeTweaks: boolean) => { set({ activeTweaks }), get().refreshTabs(); }, @@ -21,6 +22,7 @@ export const useTweaksStore = create((set, get) => ({ nodes: newChange, }); get().refreshTabs(); + get().updateTweaks(); }, setNode: (id, change) => { let newChange = @@ -59,6 +61,13 @@ export const useTweaksStore = create((set, get) => ({ }); get().refreshTabs(); }, + newInitialSetup: (nodes: AllNodeType[]) => { + useFlowStore.getState().unselectAll(); + set({ + nodes: getNodesWithDefaultValue(nodes), + }); + get().updateTweaks(); + }, tabs: [], refreshTabs: () => { const autoLogin = get().autoLogin; @@ -126,4 +135,34 @@ export const useTweaksStore = create((set, get) => ({ tabs: createTabsArray(codesObj, nodes.length > 0), }); }, + updateTweaks: () => { + const nodes = get().nodes; + const originalNodes = useFlowStore.getState().nodes; + const tweak = {}; + nodes.forEach((node) => { + const originalNodeTemplate = originalNodes?.find((n) => n.id === node.id) + ?.data?.node?.template; + const nodeTemplate = node.data?.node?.template; + if (originalNodeTemplate && nodeTemplate && node.type === "genericNode") { + const currentTweak = {}; + Object.keys(nodeTemplate).forEach((name) => { + if ( + customStringify(nodeTemplate[name]) !== + customStringify(originalNodeTemplate[name]) + ) { + currentTweak[name] = getChangesType( + nodeTemplate[name].value, + nodeTemplate[name], + ); + } + }); + if (Object.keys(currentTweak).length > 0) { + tweak[node.id] = currentTweak; + } + } + }); + set({ + tweaks: tweak, + }); + }, })); diff --git a/src/frontend/src/stores/utilityStore.ts b/src/frontend/src/stores/utilityStore.ts index e71b3d864..d9d800f58 100644 --- a/src/frontend/src/stores/utilityStore.ts +++ b/src/frontend/src/stores/utilityStore.ts @@ -3,6 +3,8 @@ import { UtilityStoreType } from "@/types/zustand/utility"; import { create } from "zustand"; export const useUtilityStore = create((set, get) => ({ + clientId: "", + setClientId: (clientId: string) => set({ clientId }), dismissAll: false, setDismissAll: (dismissAll: boolean) => set({ dismissAll }), chatValueStore: "", diff --git a/src/frontend/src/style/applies.css b/src/frontend/src/style/applies.css index 9aca73461..f202240b6 100644 --- a/src/frontend/src/style/applies.css +++ b/src/frontend/src/style/applies.css @@ -1294,6 +1294,14 @@ align-items: center; } +.deploy-dropdown-item { + @apply px-2.5 py-1 hover:bg-background cursor-pointer h-11 font-normal text-[14px] !important; +} + +.deploy-dropdown-item > :first-child { + @apply flex items-center w-full px-2 h-8 py-1 rounded-md !important; +} + :root { --color-bg1: rgb(255, 255, 255); --color1: 255, 50, 118; diff --git a/src/frontend/src/style/classes.css b/src/frontend/src/style/classes.css index 3334c5223..edbe5b7ab 100644 --- a/src/frontend/src/style/classes.css +++ b/src/frontend/src/style/classes.css @@ -250,13 +250,17 @@ pre code { display: inline-block; width: 100%; /* Background color */ - background-color: hsl(var(--code-background)) !important; + background-color: hsl(var(--muted)) !important; + font-size: 12px !important; +} + +.dark pre code { color: hsl(var(--code-foreground)) !important; } pre { /* Background color */ - background-color: hsl(var(--code-background)) !important; + background-color: hsl(var(--muted)) !important; } .prose li::marker { @@ -286,3 +290,7 @@ input[type="search"]::-webkit-search-cancel-button { .markdown td { min-width: 78px; } + +.linenumber { + font-style: normal !important; +} diff --git a/src/frontend/src/types/components/index.ts b/src/frontend/src/types/components/index.ts index 58e607790..f73d13dcd 100644 --- a/src/frontend/src/types/components/index.ts +++ b/src/frontend/src/types/components/index.ts @@ -535,6 +535,7 @@ export type ChatInputType = { repeat: number; files?: string[]; }) => void; + playgroundPage: boolean; }; export type editNodeToggleType = { @@ -595,7 +596,7 @@ export type iconsType = { export type modalHeaderType = { children: ReactNode; - description: string | JSX.Element | null; + description?: string | JSX.Element | null; clampDescription?: number; }; @@ -622,6 +623,7 @@ export type chatMessagePropsType = { stream_url?: string, ) => void; closeChat?: () => void; + playgroundPage?: boolean; }; export type genericModalPropsType = { @@ -679,6 +681,7 @@ export type IOModalPropsType = { isPlayground?: boolean; cleanOnClose?: boolean; canvasOpen?: boolean; + playgroundPage?: boolean; }; export type buttonBoxPropsType = { @@ -796,6 +799,7 @@ export type chatViewProps = { visibleSession?: string; focusChat?: string; closeChat?: () => void; + playgroundPage?: boolean; }; export type IOFileInputProps = { diff --git a/src/frontend/src/types/flow/index.ts b/src/frontend/src/types/flow/index.ts index 0fe74b8bd..c6aee4b50 100644 --- a/src/frontend/src/types/flow/index.ts +++ b/src/frontend/src/types/flow/index.ts @@ -31,6 +31,8 @@ export type FlowType = { folder_id?: string; webhook?: boolean; locked?: boolean | null; + public?: boolean; + access_type?: "public" | "private" | "protected"; }; export type GenericNodeType = Node; diff --git a/src/frontend/src/types/tabs/index.ts b/src/frontend/src/types/tabs/index.ts index 4b8c5d188..bf06fa973 100644 --- a/src/frontend/src/types/tabs/index.ts +++ b/src/frontend/src/types/tabs/index.ts @@ -69,3 +69,13 @@ export type errorsVarType = { title: string; list?: Array; }; + +export type APITabType = { + title: string; + language: string; + icon: string; + code: string; + copyCode: string; +}; + +export type tabsArrayType = Array; diff --git a/src/frontend/src/types/tweaks/index.ts b/src/frontend/src/types/tweaks/index.ts index d6a672379..c6b584655 100644 --- a/src/frontend/src/types/tweaks/index.ts +++ b/src/frontend/src/types/tweaks/index.ts @@ -14,4 +14,5 @@ export type GetCodeType = { tweaksBuildedObject?: {}; endpointName?: string | null; activeTweaks?: boolean; + copy?: boolean; }; diff --git a/src/frontend/src/types/zustand/flow/index.ts b/src/frontend/src/types/zustand/flow/index.ts index bcfa59374..ef2db91fe 100644 --- a/src/frontend/src/types/zustand/flow/index.ts +++ b/src/frontend/src/types/zustand/flow/index.ts @@ -139,6 +139,8 @@ export type FlowStoreType = { getFilterEdge: any[]; onConnect: (connection: Connection) => void; unselectAll: () => void; + playgroundPage: boolean; + setPlaygroundPage: (playgroundPage: boolean) => void; buildFlow: ({ startNodeId, stopNodeId, diff --git a/src/frontend/src/types/zustand/tweaks/index.ts b/src/frontend/src/types/zustand/tweaks/index.ts index 018b7cbb5..78feb71dd 100644 --- a/src/frontend/src/types/zustand/tweaks/index.ts +++ b/src/frontend/src/types/zustand/tweaks/index.ts @@ -1,4 +1,4 @@ -import { AllNodeType, FlowType } from "@/types/flow"; +import { AllNodeType, FlowType, TweaksType } from "@/types/flow"; import { GetCodesType } from "@/types/tweaks"; import { tabsArrayType } from "../../components"; @@ -22,7 +22,14 @@ export type TweaksStoreType = { flow: FlowType, getCodes: GetCodesType, ) => void; + newInitialSetup: (nodes: AllNodeType[]) => void; refreshTabs: () => void; autoLogin: boolean; flow: FlowType | null; + updateTweaks: () => void; + tweaks: { + [key: string]: { + [key: string]: any; + }; + }; }; diff --git a/src/frontend/src/types/zustand/utility/index.ts b/src/frontend/src/types/zustand/utility/index.ts index dc4b53c8b..788ba193e 100644 --- a/src/frontend/src/types/zustand/utility/index.ts +++ b/src/frontend/src/types/zustand/utility/index.ts @@ -21,4 +21,6 @@ export type UtilityStoreType = { setChatValueStore: (value: string) => void; dismissAll: boolean; setDismissAll: (dismissAll: boolean) => void; + setClientId: (clientId: string) => void; + clientId: string; }; diff --git a/src/frontend/src/utils/buildUtils.ts b/src/frontend/src/utils/buildUtils.ts index 44e5c1460..62aeeacdc 100644 --- a/src/frontend/src/utils/buildUtils.ts +++ b/src/frontend/src/utils/buildUtils.ts @@ -39,6 +39,7 @@ type BuildVerticesParams = { edges?: Edge[]; logBuilds?: boolean; session?: string; + playgroundPage?: boolean; stream?: boolean; }; @@ -226,10 +227,13 @@ export async function buildFlowVertices({ edges, logBuilds, session, + playgroundPage, stream = true, }: BuildVerticesParams) { const inputs = {}; - let buildUrl = `${BASE_URL_API}build/${flowId}/flow`; + + let buildUrl = `${BASE_URL_API}${playgroundPage ? "build_public_tmp" : "build"}/${flowId}/flow`; + const queryParams = new URLSearchParams(); if (startNodeId) { diff --git a/src/frontend/src/utils/reactflowUtils.ts b/src/frontend/src/utils/reactflowUtils.ts index 699a9e3d4..0a05abd99 100644 --- a/src/frontend/src/utils/reactflowUtils.ts +++ b/src/frontend/src/utils/reactflowUtils.ts @@ -1983,3 +1983,7 @@ export function buildPositionDictionary(nodes: AllNodeType[]) { }); return positionDictionary; } + +export function hasStreaming(nodes: AllNodeType[]) { + return nodes.some((node) => node.data.node?.template?.stream?.value); +} diff --git a/src/frontend/src/utils/styleUtils.ts b/src/frontend/src/utils/styleUtils.ts index 2aabf2f90..ae90a9d58 100644 --- a/src/frontend/src/utils/styleUtils.ts +++ b/src/frontend/src/utils/styleUtils.ts @@ -1,7 +1,9 @@ import { AIMLIcon } from "@/icons/AIML"; +import { BWPythonIcon } from "@/icons/BW python"; import { DuckDuckGoIcon } from "@/icons/DuckDuckGo"; import { ExaIcon } from "@/icons/Exa"; import { GleanIcon } from "@/icons/Glean"; +import { JSIcon } from "@/icons/JSicon"; import { LangwatchIcon } from "@/icons/Langwatch"; import { MilvusIcon } from "@/icons/Milvus"; import Perplexity from "@/icons/Perplexity/Perplexity"; @@ -60,6 +62,7 @@ import { Code2, CodeXml, Cog, + Columns2, Combine, Command, Compass, @@ -82,6 +85,7 @@ import { EyeOff, File, FileClock, + FileCode2, FileDown, FileQuestion, FileSearch, @@ -612,6 +616,7 @@ export const nodeIconsLucide: iconsType = { ListFlows: Group, ClearMessageHistory: FileClock, Python: PythonIcon, + BWPython: BWPythonIcon, AzureChatOpenAi: AzureIcon, Ollama: OllamaIcon, ChatOllama: OllamaIcon, @@ -972,6 +977,9 @@ export const nodeIconsLucide: iconsType = { ThumbDownIconCustom, ThumbUpIconCustom, Serper: SerperIcon, + javascript: JSIcon, + FileCode2, + Columns2, ScrapeGraphAI: ScrapeGraph, ScrapeGraph: ScrapeGraph, ScrapeGraphSmartScraperApi: ScrapeGraph, diff --git a/src/frontend/src/utils/utils.ts b/src/frontend/src/utils/utils.ts index 7ad16d018..95b1897ca 100644 --- a/src/frontend/src/utils/utils.ts +++ b/src/frontend/src/utils/utils.ts @@ -795,3 +795,63 @@ export const stringToBool = (str) => (str === "false" ? false : true); export const filterNullOptions = (opts: any[]): any[] => { return opts.filter((opt) => opt !== null && opt !== undefined); }; + +/** + * Gets a cookie value by its name + * @param {string} name - The name of the cookie to retrieve + * @returns {string | undefined} The cookie value if found, undefined otherwise + */ +export function getCookie(name: string): string | undefined { + const cookie = document.cookie + .split("; ") + .find((row) => row.startsWith(`${name}=`)); + + if (cookie) { + return cookie.split("=")[1]; + } + return undefined; +} + +/** + * Interface for cookie options + */ +export interface CookieOptions { + path?: string; + domain?: string; + maxAge?: number; + expires?: Date; + secure?: boolean; + sameSite?: "Strict" | "Lax" | "None"; +} + +/** + * Sets a cookie with the specified name, value, and optional configuration + * @param {string} name - The name of the cookie + * @param {string} value - The value to store in the cookie + * @param {CookieOptions} options - Optional configuration for the cookie + */ +export function setCookie( + name: string, + value: string, + options: CookieOptions = {}, +): void { + const { + path = "/", + domain, + maxAge, + expires, + secure = true, + sameSite = "Strict", + } = options; + + let cookieString = `${name}=${encodeURIComponent(value)}`; + + if (path) cookieString += `; path=${path}`; + if (domain) cookieString += `; domain=${domain}`; + if (maxAge) cookieString += `; max-age=${maxAge}`; + if (expires) cookieString += `; expires=${expires.toUTCString()}`; + if (secure) cookieString += "; secure"; + if (sameSite) cookieString += `; SameSite=${sameSite}`; + + document.cookie = cookieString; +} diff --git a/src/frontend/tailwind.config.mjs b/src/frontend/tailwind.config.mjs index 28087fefa..743574f43 100644 --- a/src/frontend/tailwind.config.mjs +++ b/src/frontend/tailwind.config.mjs @@ -366,6 +366,9 @@ const config = { "&::-webkit-scrollbar-thumb:hover": { backgroundColor: "hsl(var(--placeholder-foreground))", }, + "&::-webkit-scrollbar-corner": { + backgroundColor: "transparent", + }, cursor: "auto", }, ".dark .theme-attribution .react-flow__attribution": { diff --git a/src/frontend/tests/core/features/logs.spec.ts b/src/frontend/tests/core/features/logs.spec.ts index b74272920..fcc78d0db 100644 --- a/src/frontend/tests/core/features/logs.spec.ts +++ b/src/frontend/tests/core/features/logs.spec.ts @@ -39,7 +39,7 @@ test( filledApiKey = await page.getByTestId("remove-icon-badge").count(); } - await page.getByTestId("icon-ChevronDown").click(); + await page.getByTestId("icon-ChevronDown").first().click(); await page.getByText("Logs").click(); await page.getByText("No Data Available", { exact: true }).isVisible(); await page.keyboard.press("Escape"); @@ -68,7 +68,7 @@ test( await page .getByText("Chat Output built successfully", { exact: true }) .isVisible(); - await page.getByTestId("icon-ChevronDown").click(); + await page.getByTestId("icon-ChevronDown").first().click(); await page.getByText("Logs").click(); await page.getByText("timestamp").first().isVisible(); diff --git a/src/frontend/tests/core/features/publish-flow.spec.ts b/src/frontend/tests/core/features/publish-flow.spec.ts new file mode 100644 index 000000000..d43c37f55 --- /dev/null +++ b/src/frontend/tests/core/features/publish-flow.spec.ts @@ -0,0 +1,73 @@ +import { expect, test } from "@playwright/test"; +import { adjustScreenView } from "../../utils/adjust-screen-view"; +import { awaitBootstrapTest } from "../../utils/await-bootstrap-test"; + +test( + "user should be able to publish a flow", + { tag: ["@release", "@workspace", "@api"] }, + async ({ page, context }) => { + await awaitBootstrapTest(page); + + await page.waitForSelector('[data-testid="blank-flow"]', { + timeout: 3000, + }); + const flowId = page.url().split("/").pop(); + expect(flowId).toBeDefined(); + expect(flowId).not.toBeNull(); + expect(flowId!.length).toBeGreaterThan(0); + await page.getByTestId("blank-flow").click(); + await page.waitForSelector('[data-testid="sidebar-search-input"]', { + timeout: 3000, + }); + + await page.getByTestId("sidebar-search-input").click(); + await page.getByTestId("sidebar-search-input").fill("chat input"); + + await page.waitForSelector('[data-testid="inputsChat Input"]', { + timeout: 3000, + }); + await page.getByTestId("inputsChat Input").hover({ timeout: 3000 }); + await page.getByTestId("add-component-button-chat-input").click(); + + await adjustScreenView(page); + await page.getByTestId("publish-button").click(); + await page.waitForSelector('[data-testid="shareable-playground"]', { + timeout: 3000, + }); + await expect( + page.waitForResponse( + (response) => + response.url().includes(flowId!) && response.status() === 200, + ), + ).resolves.toBeTruthy(); + + await page.getByTestId("publish-switch").click(); + await page.getByTestId("shareable-playground").click(); + await expect(page.getByTestId("rf__wrapper")).toBeVisible(); + await page.getByTestId("publish-button").click(); + await page.getByTestId("publish-switch").click(); + await expect(page.getByTestId("rf__wrapper")).toBeVisible(); + await expect(page.getByTestId("publish-switch")).toBeChecked(); + const pagePromise = context.waitForEvent("page"); + await page.getByTestId("shareable-playground").click(); + const newPage = await pagePromise; + await newPage.waitForTimeout(3000); + const newUrl = newPage.url(); + await newPage.getByPlaceholder("Send a message...").fill("Hello"); + await newPage.getByTestId("button-send").click(); + await expect(newPage.getByText("Hello")).toBeVisible(); + await newPage.close(); + await page.bringToFront(); + // check if deactivate the publishworks + await page.getByTestId("publish-button").click(); + await page.getByTestId("publish-switch").click(); + await expect(page.getByTestId("rf__wrapper")).toBeVisible(); + await expect(page.getByTestId("publish-switch")).toBeChecked({ + checked: false, + }); + await page.getByTestId("shareable-playground").click(); + await expect(page.getByTestId("rf__wrapper")).toBeVisible(); + await page.goto(newUrl); + await expect(page.getByTestId("mainpage_title")).toBeVisible(); + }, +); diff --git a/src/frontend/tests/extended/features/curlApiGeneration.spec.ts b/src/frontend/tests/extended/features/curlApiGeneration.spec.ts index f811f2033..52208f8cc 100644 --- a/src/frontend/tests/extended/features/curlApiGeneration.spec.ts +++ b/src/frontend/tests/extended/features/curlApiGeneration.spec.ts @@ -9,7 +9,8 @@ test( await page.getByTestId("side_nav_options_all-templates").click(); await page.getByRole("heading", { name: "Basic Prompting" }).click(); - await page.getByText("API", { exact: true }).click(); + await page.getByTestId("publish-button").click(); + await page.getByTestId("api-access-item").click(); await page.getByRole("tab", { name: "cURL" }).click(); await page.getByTestId("icon-Copy").last().click(); const handle = await page.evaluateHandle(() => diff --git a/src/frontend/tests/extended/features/edit-name-description-node.spec.ts b/src/frontend/tests/extended/features/edit-name-description-node.spec.ts index 7c042f976..6ec8555a7 100644 --- a/src/frontend/tests/extended/features/edit-name-description-node.spec.ts +++ b/src/frontend/tests/extended/features/edit-name-description-node.spec.ts @@ -44,9 +44,9 @@ test( await page.getByTestId("textarea").fill(randomDescription); - await page.getByTestId("api_button_modal").click(); + await page.getByTestId("publish-button").click(); - await page.getByText("Close").last().click(); + await page.keyboard.press("Escape"); expect(await page.getByText(randomName).count()).toBe(1); expect(await page.getByText(randomDescription).count()).toBe(1); diff --git a/src/frontend/tests/extended/features/pythonApiGeneration.spec.ts b/src/frontend/tests/extended/features/pythonApiGeneration.spec.ts index b3efc5bda..91dc4dfbc 100644 --- a/src/frontend/tests/extended/features/pythonApiGeneration.spec.ts +++ b/src/frontend/tests/extended/features/pythonApiGeneration.spec.ts @@ -9,8 +9,9 @@ test( await page.getByTestId("side_nav_options_all-templates").click(); await page.getByRole("heading", { name: "Basic Prompting" }).click(); - await page.getByText("API", { exact: true }).click(); - await page.getByRole("tab", { name: "Python API" }).click(); + await page.getByTestId("publish-button").click(); + await page.getByTestId("api-access-item").click(); + await page.getByRole("tab", { name: "Python" }).click(); await page.getByTestId("icon-Copy").click(); const handle = await page.evaluateHandle(() => navigator.clipboard.readText(),