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 <otavio2204@gmail.com> * 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 <cristhian.lousa@gmail.com> * Update src/frontend/tests/core/features/publish-flow.spec.ts Co-authored-by: Cristhian Zanforlin Lousa <cristhian.lousa@gmail.com> * Update src/frontend/tests/core/features/publish-flow.spec.ts Co-authored-by: Cristhian Zanforlin Lousa <cristhian.lousa@gmail.com> * Update src/frontend/tests/core/features/publish-flow.spec.ts Co-authored-by: Cristhian Zanforlin Lousa <cristhian.lousa@gmail.com> * Update src/frontend/tests/core/features/publish-flow.spec.ts Co-authored-by: Cristhian Zanforlin Lousa <cristhian.lousa@gmail.com> * Update src/frontend/tests/core/features/publish-flow.spec.ts Co-authored-by: Cristhian Zanforlin Lousa <cristhian.lousa@gmail.com> * Update src/frontend/tests/core/features/publish-flow.spec.ts Co-authored-by: Cristhian Zanforlin Lousa <cristhian.lousa@gmail.com> * Update src/frontend/src/modals/apiModal/utils/get-python-api-code.tsx Co-authored-by: Cristhian Zanforlin Lousa <cristhian.lousa@gmail.com> * Update src/frontend/tests/core/features/publish-flow.spec.ts Co-authored-by: Cristhian Zanforlin Lousa <cristhian.lousa@gmail.com> * Update src/frontend/tests/core/features/publish-flow.spec.ts Co-authored-by: Cristhian Zanforlin Lousa <cristhian.lousa@gmail.com> * Update src/frontend/src/modals/apiModal/utils/get-curl-code.tsx Co-authored-by: Cristhian Zanforlin Lousa <cristhian.lousa@gmail.com> * Update src/frontend/src/modals/apiModal/utils/get-curl-code.tsx Co-authored-by: Cristhian Zanforlin Lousa <cristhian.lousa@gmail.com> * Update src/frontend/src/modals/apiModal/utils/get-curl-code.tsx Co-authored-by: Cristhian Zanforlin Lousa <cristhian.lousa@gmail.com> * Update src/frontend/src/modals/apiModal/utils/get-python-api-code.tsx Co-authored-by: Cristhian Zanforlin Lousa <cristhian.lousa@gmail.com> * Update src/frontend/src/modals/apiModal/utils/get-python-api-code.tsx Co-authored-by: Cristhian Zanforlin Lousa <cristhian.lousa@gmail.com> * 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 <italojohnnydosanjos@gmail.com> Co-authored-by: Gabriel Luiz Freitas Almeida <gabriel@langflow.org> Co-authored-by: Cristhian Zanforlin Lousa <cristhian.lousa@gmail.com> Co-authored-by: Lucas Oliveira <62335616+lucaseduoli@users.noreply.github.com> Co-authored-by: Lucas Oliveira <lucas.edu.oli@hotmail.com>
This commit is contained in:
parent
63a2e3349f
commit
7aca264fec
86 changed files with 2252 additions and 1465 deletions
|
|
@ -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')
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
*,
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -274,6 +274,7 @@ def create_app():
|
|||
FastAPIInstrumentor.instrument_app(app)
|
||||
|
||||
add_pagination(app)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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'."""
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
136
src/backend/base/langflow/services/task/temp_flow_cleanup.py
Normal file
136
src/backend/base/langflow/services/task/temp_flow_cleanup.py
Normal file
|
|
@ -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()
|
||||
0
src/backend/tests/unit/services/tasks/__init__.py
Normal file
0
src/backend/tests/unit/services/tasks/__init__.py
Normal file
109
src/backend/tests/unit/services/tasks/test_temp_flow_cleanup.py
Normal file
109
src/backend/tests/unit/services/tasks/test_temp_flow_cleanup.py
Normal file
|
|
@ -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)
|
||||
1228
src/frontend/package-lock.json
generated
1228
src/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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 (
|
||||
<Suspense fallback={<LoadingPage />}>
|
||||
<RouterProvider router={router} />
|
||||
|
|
|
|||
9
src/frontend/src/assets/LangflowLogoColor.svg
Normal file
9
src/frontend/src/assets/LangflowLogoColor.svg
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="langflow-icon-color-black-transparent">
|
||||
<g id="Group 3">
|
||||
<path id="Vector" d="M13.121 9.31941H14.9266C15.2808 9.31941 15.5677 9.60632 15.5677 9.96049V11.0257C15.5677 11.3798 15.2808 11.6667 14.9266 11.6667H13.3518C13.1816 11.6667 13.0185 11.7345 12.8982 11.8547L10.1958 14.5567C10.0756 14.677 9.91251 14.7447 9.74233 14.7447H8.44325C8.09577 14.7447 7.81109 14.4676 7.80218 14.1201L7.77412 13.0326C7.76476 12.6722 8.05433 12.3746 8.41519 12.3746H9.53339C9.70357 12.3746 9.86662 12.3069 9.98691 12.1866L12.6666 9.50697C12.7869 9.38669 12.9499 9.31897 13.1201 9.31897L13.121 9.31941Z" fill="#7528FC"/>
|
||||
<path id="Vector_2" d="M7.75986 3.25531H9.56546C9.91963 3.25531 10.2065 3.54221 10.2065 3.89638V4.96157C10.2065 5.31574 9.91963 5.60264 9.56546 5.60264H7.99063C7.82045 5.60264 7.65739 5.67036 7.53711 5.79064L4.83472 8.49303C4.71443 8.61332 4.55138 8.68104 4.3812 8.68104H3.08212C2.73464 8.68104 2.44996 8.40394 2.44105 8.05645L2.41299 6.96898C2.40363 6.60858 2.6932 6.31143 3.05406 6.31143H4.17226C4.34244 6.31143 4.50549 6.24371 4.62578 6.12343L7.30545 3.44376C7.42573 3.32347 7.58879 3.25576 7.75897 3.25576L7.75986 3.25531Z" fill="#FF3276"/>
|
||||
<path id="Vector_3" d="M13.121 4.62744H14.9266C15.2808 4.62744 15.5677 4.91434 15.5677 5.26851V6.3337C15.5677 6.68787 15.2808 6.97477 14.9266 6.97477H13.3518C13.1816 6.97477 13.0185 7.04249 12.8982 7.16277L10.1958 9.86517C10.0756 9.98545 9.91251 10.0532 9.74233 10.0532H8.16393C7.99865 10.0532 7.8396 10.1169 7.72021 10.2314L4.68637 13.1387C4.56697 13.2532 4.40793 13.3169 4.24265 13.3169H3.13692C2.78275 13.3169 2.49585 13.0295 2.49585 12.6758V11.5812C2.49585 11.2271 2.78275 10.9402 3.13692 10.9402H4.23463C4.40481 10.9402 4.56786 10.8724 4.68815 10.7522L7.56918 7.87112C7.68947 7.75083 7.85252 7.68312 8.0227 7.68312H9.53339C9.70357 7.68312 9.86662 7.6154 9.98691 7.49511L12.6666 4.81544C12.7869 4.69516 12.9499 4.62744 13.1201 4.62744H13.121Z" fill="#F480FF"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2 KiB |
|
|
@ -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 ? (
|
||||
<div
|
||||
className="flex w-full items-center justify-center gap-2"
|
||||
|
|
@ -261,6 +268,12 @@ export const MenuBar = ({}: {}): JSX.Element => {
|
|||
>
|
||||
/
|
||||
</div>
|
||||
<div className={cn(`flex rounded p-1`, swatchColors[swatchIndex])}>
|
||||
<IconComponent
|
||||
name={currentFlow?.icon ?? "Workflow"}
|
||||
className="h-3.5 w-3.5"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="shrink-0 overflow-hidden text-sm sm:whitespace-normal"
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ interface ContentBlockDisplayProps {
|
|||
isLoading?: boolean;
|
||||
state?: string;
|
||||
chatId: string;
|
||||
playgroundPage?: boolean;
|
||||
}
|
||||
|
||||
export function ContentBlockDisplay({
|
||||
|
|
@ -25,6 +26,7 @@ export function ContentBlockDisplay({
|
|||
isLoading,
|
||||
state,
|
||||
chatId,
|
||||
playgroundPage,
|
||||
}: ContentBlockDisplayProps) {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
|
|
@ -41,7 +43,7 @@ export function ContentBlockDisplay({
|
|||
const lastContent =
|
||||
contentBlocks[0]?.contents[contentBlocks[0]?.contents.length - 1];
|
||||
const headerIcon =
|
||||
state === "partial" ? lastContent?.header?.icon || "Bot" : "Bot";
|
||||
state === "partial" ? lastContent?.header?.icon || "Bot" : "Check";
|
||||
|
||||
const headerTitle =
|
||||
state === "partial" ? (lastContent?.header?.title ?? "Steps") : "Finished";
|
||||
|
|
@ -76,11 +78,14 @@ export function ContentBlockDisplay({
|
|||
className="flex cursor-pointer items-center justify-between p-4"
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 align-baseline">
|
||||
{headerIcon && (
|
||||
<ForwardedIconComponent
|
||||
name={headerIcon}
|
||||
className="h-4 w-4"
|
||||
className={cn(
|
||||
"h-4 w-4",
|
||||
state !== "partial" && "text-status-green",
|
||||
)}
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -102,7 +107,9 @@ export function ContentBlockDisplay({
|
|||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<DurationDisplay duration={totalDuration} chatId={chatId} />
|
||||
{!playgroundPage && (
|
||||
<DurationDisplay duration={totalDuration} chatId={chatId} />
|
||||
)}
|
||||
<motion.div
|
||||
animate={{ rotate: isExpanded ? 180 : 0 }}
|
||||
transition={{ duration: 0.2, ease: "easeInOut" }}
|
||||
|
|
@ -193,6 +200,7 @@ export function ContentBlockDisplay({
|
|||
)}
|
||||
</AnimatePresence>
|
||||
<ContentDisplay
|
||||
playgroundPage={playgroundPage}
|
||||
content={content}
|
||||
chatId={`${chatId}-${index}`}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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({
|
|||
</div>
|
||||
</>
|
||||
);
|
||||
const renderDuration = content.duration !== undefined && (
|
||||
const renderDuration = content.duration !== undefined && !playgroundPage && (
|
||||
<div className="absolute right-2 top-4">
|
||||
<DurationDisplay duration={content.duration} chatId={chatId} />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="default"
|
||||
className="!h-8 !w-[95px] font-medium"
|
||||
data-testid="publish-button"
|
||||
>
|
||||
Publish
|
||||
<IconComponent
|
||||
name="ChevronDown"
|
||||
className="icon-size font-medium"
|
||||
/>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
forceMount
|
||||
sideOffset={10}
|
||||
alignOffset={-10}
|
||||
align="end"
|
||||
className="min-w-[300px] max-w-[400px]"
|
||||
>
|
||||
<DropdownMenuItem
|
||||
className="deploy-dropdown-item group"
|
||||
onClick={() => setOpenApiModal(true)}
|
||||
>
|
||||
<div
|
||||
className="group-hover:bg-accent"
|
||||
data-testid="api-access-item"
|
||||
>
|
||||
<IconComponent
|
||||
name="Code2"
|
||||
className={`${groupStyle} icon-size mr-2`}
|
||||
/>
|
||||
<span>API access</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
{ENABLE_WIDGET && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => setOpenEmbedModal(true)}
|
||||
className="deploy-dropdown-item group"
|
||||
>
|
||||
<div className="group-hover:bg-accent">
|
||||
<IconComponent
|
||||
name="Columns2"
|
||||
className={`${groupStyle} icon-size mr-2`}
|
||||
/>
|
||||
<span>Embed into site</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<ShadTooltipComponent
|
||||
styleClasses="truncate"
|
||||
side="left"
|
||||
content={
|
||||
hasIO
|
||||
? isPublished
|
||||
? encodeURI(`${domain}/playground/${flowId}`)
|
||||
: "Active to share a public version of this Playground"
|
||||
: "Add a Chat Input or Chat Output to access your flow"
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
!hasIO ? "cursor-not-allowed" : "" + "flex items-center"
|
||||
}
|
||||
>
|
||||
<DropdownMenuItem
|
||||
data-testid="shareable-playground"
|
||||
disabled={!hasIO || !isPublished}
|
||||
className="deploy-dropdown-item group flex-1"
|
||||
onClick={() => {
|
||||
if (hasIO) {
|
||||
if (isPublished) {
|
||||
window.open(`${domain}/playground/${flowId}`, "_blank");
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="group-hover:bg-accent">
|
||||
<IconComponent
|
||||
name="Globe"
|
||||
className={`${groupStyle} icon-size mr-2`}
|
||||
/>
|
||||
<span>Shareable Playground</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<div className={`z-50 mr-2 text-foreground`}>
|
||||
<Switch
|
||||
data-testid="publish-switch"
|
||||
className="scale-[85%]"
|
||||
checked={isPublished}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handlePublishedSwitch(isPublished);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ShadTooltipComponent>
|
||||
{/* <DropdownMenuItem className="deploy-dropdown-item group">
|
||||
<div className="group-hover:bg-accent">
|
||||
<IconComponent
|
||||
name="FileCode2"
|
||||
className={`${groupStyle} icon-size mr-2`}
|
||||
/>
|
||||
<span>Langflow SDK</span>
|
||||
<IconComponent
|
||||
name="ExternalLink"
|
||||
className={`icon-size ml-auto mr-3 ${externalUrlStyle} text-foreground`}
|
||||
/>
|
||||
</div>
|
||||
</DropdownMenuItem> */}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<ApiModal open={openApiModal} setOpen={setOpenApiModal}>
|
||||
<></>
|
||||
</ApiModal>
|
||||
<EmbedModal
|
||||
open={openEmbedModal}
|
||||
setOpen={setOpenEmbedModal}
|
||||
flowId={flowId ?? ""}
|
||||
flowName={flowName ?? ""}
|
||||
isAuth={isAuth}
|
||||
tweaksBuildedObject={{}}
|
||||
activeTweaks={false}
|
||||
></EmbedModal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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<boolean>(false);
|
||||
const hasIO = useFlowStore((state) => state.hasIO);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="flex h-full w-full gap-1.5 rounded-sm transition-all">
|
||||
<PlaygroundButton
|
||||
hasIO={hasIO}
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
canvasOpen
|
||||
/>
|
||||
</div>
|
||||
<PublishDropdown />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 = () => (
|
||||
<ForwardedIconComponent name="Play" className="h-4 w-4 transition-all" />
|
||||
<ForwardedIconComponent
|
||||
name="Play"
|
||||
className="h-4 w-4 transition-all"
|
||||
strokeWidth={ENABLE_PUBLISH ? 2 : 1.5}
|
||||
/>
|
||||
);
|
||||
|
||||
const ButtonLabel = () => <span className="hidden md:block">Playground</span>;
|
||||
const ButtonLabel = () => (
|
||||
<span className="hidden md:block">{PLAYGROUND_BUTTON_NAME}</span>
|
||||
);
|
||||
|
||||
const ActiveButton = () => (
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import PlaygroundButton from "@/components/core/flowToolbarComponent/components/
|
|||
import {
|
||||
ENABLE_API,
|
||||
ENABLE_LANGFLOW_STORE,
|
||||
ENABLE_PUBLISH,
|
||||
} from "@/customization/feature-flags";
|
||||
import { track } from "@/customization/utils/analytics";
|
||||
import { Panel } from "@xyflow/react";
|
||||
|
|
@ -13,8 +14,9 @@ import ShareModal from "../../../modals/shareModal";
|
|||
import useFlowStore from "../../../stores/flowStore";
|
||||
import { useShortcutsStore } from "../../../stores/shortcuts";
|
||||
import { useStoreStore } from "../../../stores/storeStore";
|
||||
import { classNames, isThereModal } from "../../../utils/utils";
|
||||
import { classNames, cn, isThereModal } from "../../../utils/utils";
|
||||
import ForwardedIconComponent from "../../common/genericIconComponent";
|
||||
import FlowToolbarOptions from "./components/flow-toolbar-options";
|
||||
|
||||
export default function FlowToolbar(): JSX.Element {
|
||||
const preventDefault = true;
|
||||
|
|
@ -119,62 +121,67 @@ export default function FlowToolbar(): JSX.Element {
|
|||
<>
|
||||
<Panel className="!m-2" position="top-right">
|
||||
<div
|
||||
className={
|
||||
"hover:shadow-round-btn-shadow flex items-center justify-center gap-7 rounded-md border bg-background p-1.5 shadow transition-all"
|
||||
}
|
||||
className={cn(
|
||||
"hover:shadow-round-btn-shadow flex items-center justify-center gap-7 rounded-md border bg-background px-1.5 shadow transition-all",
|
||||
ENABLE_PUBLISH ? "h-11" : "",
|
||||
)}
|
||||
>
|
||||
<div className="flex gap-1.5">
|
||||
<div className="flex h-full w-full gap-1.5 rounded-sm transition-all">
|
||||
<PlaygroundButton
|
||||
hasIO={hasIO}
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
canvasOpen
|
||||
/>
|
||||
</div>
|
||||
{ENABLE_API && (
|
||||
<>
|
||||
<div
|
||||
className="flex cursor-pointer items-center gap-2"
|
||||
data-testid="api_button_modal"
|
||||
id="api_button_modal"
|
||||
>
|
||||
{currentFlow && currentFlow.data && (
|
||||
<ApiModal
|
||||
flow={currentFlow}
|
||||
open={openCodeModal}
|
||||
setOpen={setOpenCodeModal}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
"relative inline-flex h-8 w-full items-center justify-center gap-1.5 rounded px-3 py-1.5 text-sm font-semibold text-foreground transition-all duration-150 ease-in-out hover:bg-accent",
|
||||
)}
|
||||
>
|
||||
<ForwardedIconComponent
|
||||
name="Code2"
|
||||
className={"h-4 w-4"}
|
||||
/>
|
||||
<span className="hidden md:block">API</span>
|
||||
</div>
|
||||
</ApiModal>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{ENABLE_LANGFLOW_STORE && (
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className={`side-bar-button ${
|
||||
!hasApiKey || !validApiKey || !hasStore
|
||||
? "cursor-not-allowed"
|
||||
: "cursor-pointer"
|
||||
}`}
|
||||
>
|
||||
{ModalMemo}
|
||||
</div>
|
||||
{ENABLE_PUBLISH ? (
|
||||
<FlowToolbarOptions />
|
||||
) : (
|
||||
<div className="flex gap-1.5">
|
||||
<div className="flex h-full w-full gap-1.5 rounded-sm transition-all">
|
||||
<PlaygroundButton
|
||||
hasIO={hasIO}
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
canvasOpen
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{ENABLE_API && (
|
||||
<>
|
||||
<div
|
||||
className="flex cursor-pointer items-center gap-2"
|
||||
data-testid="api_button_modal"
|
||||
id="api_button_modal"
|
||||
>
|
||||
{currentFlow && currentFlow.data && (
|
||||
<ApiModal
|
||||
flow={currentFlow}
|
||||
open={openCodeModal}
|
||||
setOpen={setOpenCodeModal}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
"relative inline-flex h-8 w-full items-center justify-center gap-1.5 rounded px-3 py-1.5 text-sm font-semibold text-foreground transition-all duration-150 ease-in-out hover:bg-accent",
|
||||
)}
|
||||
>
|
||||
<ForwardedIconComponent
|
||||
name="Code2"
|
||||
className={"h-4 w-4"}
|
||||
/>
|
||||
<span className="hidden md:block">API</span>
|
||||
</div>
|
||||
</ApiModal>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{ENABLE_LANGFLOW_STORE && (
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className={`side-bar-button ${
|
||||
!hasApiKey || !validApiKey || !hasStore
|
||||
? "cursor-not-allowed"
|
||||
: "cursor-pointer"
|
||||
}`}
|
||||
>
|
||||
{ModalMemo}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Panel>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -55,45 +55,56 @@ const DialogContent = React.forwardRef<
|
|||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
|
||||
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 (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed z-50 flex w-full max-w-lg flex-col gap-4 rounded-xl border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{!hasDialogTitle && (
|
||||
<VisuallyHidden>
|
||||
<DialogTitle>Dialog</DialogTitle>
|
||||
</VisuallyHidden>
|
||||
)}
|
||||
{children}
|
||||
<ShadTooltip
|
||||
styleClasses="z-50"
|
||||
content="Close"
|
||||
side="bottom"
|
||||
avoidCollisions
|
||||
return (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed z-50 flex w-full max-w-lg flex-col gap-4 rounded-xl border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<DialogPrimitive.Close className="absolute right-2 top-2 flex h-8 w-8 items-center justify-center rounded-sm ring-offset-background transition-opacity hover:bg-secondary-hover hover:text-accent-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
<Cross2Icon className="h-[18px] w-[18px]" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</ShadTooltip>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
);
|
||||
});
|
||||
{!hasDialogTitle && (
|
||||
<VisuallyHidden>
|
||||
<DialogTitle>Dialog</DialogTitle>
|
||||
</VisuallyHidden>
|
||||
)}
|
||||
{children}
|
||||
<ShadTooltip
|
||||
styleClasses="z-50"
|
||||
content="Close"
|
||||
side="bottom"
|
||||
avoidCollisions
|
||||
>
|
||||
<DialogPrimitive.Close
|
||||
className={cn(
|
||||
"absolute right-2 top-2 flex h-8 w-8 items-center justify-center rounded-sm ring-offset-background transition-opacity hover:bg-secondary-hover hover:text-accent-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground",
|
||||
closeButtonClassName,
|
||||
)}
|
||||
>
|
||||
<Cross2Icon className="h-[18px] w-[18px]" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</ShadTooltip>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
);
|
||||
},
|
||||
);
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||
|
||||
const DialogHeader = ({
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 = {}) {
|
||||
|
|
|
|||
|
|
@ -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<undefined, IGetFlow> = (
|
|||
|
||||
const getFlowFn = async (payload: IGetFlow): Promise<FlowType> => {
|
||||
const response = await api.get<FlowType>(
|
||||
`${getURL("FLOWS")}/${payload.id}`,
|
||||
`${getURL(payload.public ? "PUBLIC_FLOW" : "FLOWS")}/${payload.id}`,
|
||||
);
|
||||
|
||||
const flowsArrayToProcess = [response.data];
|
||||
|
|
|
|||
|
|
@ -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<any> => {
|
||||
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<any> => {
|
||||
const response = await api.patch(`${getURL("FLOWS")}/${id}`, payload);
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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<any>(`${getURL("MESSAGES")}`, config);
|
||||
if (!isPlaygroundPage) {
|
||||
return await api.get<any>(`${getURL("MESSAGES")}`, config);
|
||||
} else {
|
||||
return {
|
||||
data: JSON.parse(window.sessionStorage.getItem(id ?? "") || "[]"),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const responseFn = async () => {
|
||||
|
|
|
|||
|
|
@ -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<Message, any, UpdateMessageParams> = mutate(
|
||||
|
|
|
|||
|
|
@ -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<Message[], any, UpdateSessionParams> =
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
3
src/frontend/src/customization/utils/urls.ts
Normal file
3
src/frontend/src/customization/utils/urls.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export const LangflowButtonRedirectTarget = () => {
|
||||
return "https://langflow.org";
|
||||
};
|
||||
33
src/frontend/src/icons/BW python/Python.jsx
Normal file
33
src/frontend/src/icons/BW python/Python.jsx
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
export const BWSvgPython = (props) => (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<g id="method-card / logo" clipPath="url(#clip0_2342_182)">
|
||||
<path
|
||||
id="path1948"
|
||||
d="M7.73326 0.000129445C7.08782 0.00312847 6.47144 0.0581749 5.92909 0.154143C4.3314 0.436402 4.04133 1.0272 4.04133 2.11672V3.55565H7.81686V4.03529H4.04133H2.6244C1.52712 4.03529 0.566317 4.69482 0.265789 5.94946C-0.0808665 7.38757 -0.0962426 8.28498 0.265789 9.7866C0.534167 10.9044 1.17509 11.7008 2.27237 11.7008H3.57048V9.97582C3.57048 8.72964 4.64871 7.63041 5.92909 7.63041H9.70023C10.75 7.63041 11.588 6.76608 11.588 5.71184V2.11672C11.588 1.09353 10.7248 0.32491 9.70023 0.154143C9.05165 0.0461786 8.37869 -0.00286962 7.73326 0.000129445ZM5.69147 1.15743C6.08146 1.15743 6.39994 1.48111 6.39994 1.8791C6.39994 2.27567 6.08146 2.59636 5.69147 2.59636C5.30009 2.59636 4.98301 2.27567 4.98301 1.8791C4.98301 1.48111 5.30009 1.15743 5.69147 1.15743Z"
|
||||
fill={props.isdark === "true" ? "white" : "black"}
|
||||
/>
|
||||
<path
|
||||
id="path1950"
|
||||
d="M12.0589 4.03528V5.71183C12.0589 7.01163 10.9569 8.10564 9.70029 8.10564H5.92915C4.89617 8.10564 4.04138 8.98973 4.04138 10.0242V13.6193C4.04138 14.6425 4.93112 15.2444 5.92915 15.5379C7.12427 15.8893 8.27033 15.9528 9.70029 15.5379C10.6508 15.2627 11.5881 14.7089 11.5881 13.6193V12.1804H7.81692V11.7008H11.5881H13.4758C14.5731 11.7008 14.982 10.9354 15.3636 9.78659C15.7578 8.60392 15.741 7.4666 15.3636 5.94945C15.0924 4.8571 14.5745 4.03528 13.4758 4.03528H12.0589ZM9.93791 13.1397C10.3293 13.1397 10.6464 13.4604 10.6464 13.857C10.6464 14.2549 10.3293 14.5786 9.93791 14.5786C9.54792 14.5786 9.22944 14.2549 9.22944 13.857C9.22945 13.4604 9.54792 13.1397 9.93791 13.1397Z"
|
||||
fill={props.isdark === "true" ? "white" : "black"}
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2342_182">
|
||||
<rect
|
||||
width="16"
|
||||
height="16"
|
||||
fill={props.isdark === "true" ? "black" : "white"}
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
export default BWSvgPython;
|
||||
11
src/frontend/src/icons/BW python/index.tsx
Normal file
11
src/frontend/src/icons/BW python/index.tsx
Normal file
|
|
@ -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 <BWSvgPython ref={ref} {...props} isdark={isdark} />;
|
||||
});
|
||||
11
src/frontend/src/icons/BW python/logo.svg
Normal file
11
src/frontend/src/icons/BW python/logo.svg
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="method-card / logo" clip-path="url(#clip0_2342_182)">
|
||||
<path id="path1948" d="M7.73326 0.000129445C7.08782 0.00312847 6.47144 0.0581749 5.92909 0.154143C4.3314 0.436402 4.04133 1.0272 4.04133 2.11672V3.55565H7.81686V4.03529H4.04133H2.6244C1.52712 4.03529 0.566317 4.69482 0.265789 5.94946C-0.0808665 7.38757 -0.0962426 8.28498 0.265789 9.7866C0.534167 10.9044 1.17509 11.7008 2.27237 11.7008H3.57048V9.97582C3.57048 8.72964 4.64871 7.63041 5.92909 7.63041H9.70023C10.75 7.63041 11.588 6.76608 11.588 5.71184V2.11672C11.588 1.09353 10.7248 0.32491 9.70023 0.154143C9.05165 0.0461786 8.37869 -0.00286962 7.73326 0.000129445ZM5.69147 1.15743C6.08146 1.15743 6.39994 1.48111 6.39994 1.8791C6.39994 2.27567 6.08146 2.59636 5.69147 2.59636C5.30009 2.59636 4.98301 2.27567 4.98301 1.8791C4.98301 1.48111 5.30009 1.15743 5.69147 1.15743Z" fill="black"/>
|
||||
<path id="path1950" d="M12.0589 4.03528V5.71183C12.0589 7.01163 10.9569 8.10564 9.70029 8.10564H5.92915C4.89617 8.10564 4.04138 8.98973 4.04138 10.0242V13.6193C4.04138 14.6425 4.93112 15.2444 5.92915 15.5379C7.12427 15.8893 8.27033 15.9528 9.70029 15.5379C10.6508 15.2627 11.5881 14.7089 11.5881 13.6193V12.1804H7.81692V11.7008H11.5881H13.4758C14.5731 11.7008 14.982 10.9354 15.3636 9.78659C15.7578 8.60392 15.741 7.4666 15.3636 5.94945C15.0924 4.8571 14.5745 4.03528 13.4758 4.03528H12.0589ZM9.93791 13.1397C10.3293 13.1397 10.6464 13.4604 10.6464 13.857C10.6464 14.2549 10.3293 14.5786 9.93791 14.5786C9.54792 14.5786 9.22944 14.2549 9.22944 13.857C9.22945 13.4604 9.54792 13.1397 9.93791 13.1397Z" fill="black"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2342_182">
|
||||
<rect width="16" height="16" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
11
src/frontend/src/icons/JSicon/Frame.svg
Normal file
11
src/frontend/src/icons/JSicon/Frame.svg
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Frame" clip-path="url(#clip0_2046_939)">
|
||||
<path id="Vector" d="M16 0H0V16H16V0Z" fill="black"/>
|
||||
<path id="Vector_2" d="M10.7479 12.5001C11.0702 13.0263 11.4895 13.4131 12.2311 13.4131C12.854 13.4131 13.252 13.1017 13.252 12.6715C13.252 12.1559 12.8431 11.9733 12.1574 11.6734L11.7815 11.5121C10.6966 11.0499 9.97582 10.4709 9.97582 9.24674C9.97582 8.11912 10.835 7.26071 12.1777 7.26071C13.1337 7.26071 13.8209 7.59341 14.3161 8.46452L13.1453 9.21627C12.8876 8.75405 12.6095 8.57195 12.1777 8.57195C11.7373 8.57195 11.4582 8.85131 11.4582 9.21627C11.4582 9.66731 11.7376 9.84992 12.3827 10.1293L12.7585 10.2903C14.036 10.8381 14.7573 11.3966 14.7573 12.6522C14.7573 14.0059 13.6939 14.7474 12.2658 14.7474C10.8695 14.7474 9.96743 14.082 9.52604 13.2099L10.7479 12.5001ZM5.43664 12.6304C5.67283 13.0494 5.88769 13.4037 6.40426 13.4037C6.89823 13.4037 7.20985 13.2104 7.20985 12.4589V7.34655H8.71334V12.4793C8.71334 14.0361 7.80058 14.7446 6.46826 14.7446C5.26445 14.7446 4.56731 14.1217 4.21277 13.3713L5.43664 12.6304Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2046_939">
|
||||
<rect width="16" height="16" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
26
src/frontend/src/icons/JSicon/JSIcon.jsx
Normal file
26
src/frontend/src/icons/JSicon/JSIcon.jsx
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
const SvgJSIcon = (props) => (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
filter={props.isdark === "true" ? "invert(100%)" : "invert(0%)"}
|
||||
>
|
||||
<g id="Frame" clipPath="url(#clip0_2046_939)">
|
||||
<path id="Vector" d="M16 0H0V16H16V0Z" fill="black" />
|
||||
<path
|
||||
id="Vector_2"
|
||||
d="M10.7479 12.5001C11.0702 13.0263 11.4895 13.4131 12.2311 13.4131C12.854 13.4131 13.252 13.1017 13.252 12.6715C13.252 12.1559 12.8431 11.9733 12.1574 11.6734L11.7815 11.5121C10.6966 11.0499 9.97582 10.4709 9.97582 9.24674C9.97582 8.11912 10.835 7.26071 12.1777 7.26071C13.1337 7.26071 13.8209 7.59341 14.3161 8.46452L13.1453 9.21627C12.8876 8.75405 12.6095 8.57195 12.1777 8.57195C11.7373 8.57195 11.4582 8.85131 11.4582 9.21627C11.4582 9.66731 11.7376 9.84992 12.3827 10.1293L12.7585 10.2903C14.036 10.8381 14.7573 11.3966 14.7573 12.6522C14.7573 14.0059 13.6939 14.7474 12.2658 14.7474C10.8695 14.7474 9.96743 14.082 9.52604 13.2099L10.7479 12.5001ZM5.43664 12.6304C5.67283 13.0494 5.88769 13.4037 6.40426 13.4037C6.89823 13.4037 7.20985 13.2104 7.20985 12.4589V7.34655H8.71334V12.4793C8.71334 14.0361 7.80058 14.7446 6.46826 14.7446C5.26445 14.7446 4.56731 14.1217 4.21277 13.3713L5.43664 12.6304Z"
|
||||
fill="white"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2046_939">
|
||||
<rect width="16" height="16" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
export default SvgJSIcon;
|
||||
10
src/frontend/src/icons/JSicon/index.tsx
Normal file
10
src/frontend/src/icons/JSicon/index.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { useDarkStore } from "@/stores/darkStore";
|
||||
import React, { forwardRef } from "react";
|
||||
import SvgJSIcon from "./JSIcon";
|
||||
|
||||
export const JSIcon = forwardRef<SVGSVGElement, React.PropsWithChildren<{}>>(
|
||||
(props, ref) => {
|
||||
const isdark = useDarkStore((state) => state.dark.toString());
|
||||
return <SvgJSIcon ref={ref} {...props} isdark={isdark} />;
|
||||
},
|
||||
);
|
||||
99
src/frontend/src/modals/EmbedModal/embed-modal.tsx
Normal file
99
src/frontend/src/modals/EmbedModal/embed-modal.tsx
Normal file
|
|
@ -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<boolean>(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 (
|
||||
<BaseModal open={open} setOpen={setOpen} size="retangular">
|
||||
<BaseModal.Header>
|
||||
<div className="flex items-center gap-2 text-[16px] font-semibold">
|
||||
<IconComponent name="Columns2" className="icon-size" />
|
||||
Embed into site
|
||||
</div>
|
||||
</BaseModal.Header>
|
||||
<BaseModal.Content className="">
|
||||
<div className="relative flex h-full w-full">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={copyToClipboard}
|
||||
data-testid="btn-copy-code"
|
||||
className="!hover:bg-foreground group absolute right-2 top-2"
|
||||
>
|
||||
{isCopied ? (
|
||||
<IconComponent
|
||||
name="Check"
|
||||
className="h-5 w-5 text-muted-foreground"
|
||||
/>
|
||||
) : (
|
||||
<IconComponent
|
||||
name="Copy"
|
||||
className="!h-5 !w-5 text-muted-foreground"
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
<SyntaxHighlighter
|
||||
showLineNumbers={true}
|
||||
wrapLongLines={true}
|
||||
language="html"
|
||||
style={isDark ? oneDark : oneLight}
|
||||
className="!mt-0 h-full w-full overflow-scroll !rounded-b-md border border-border text-left !custom-scroll"
|
||||
>
|
||||
{embedCode}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
</BaseModal.Content>
|
||||
</BaseModal>
|
||||
);
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -21,23 +21,23 @@ export const ChatViewWrapper = ({
|
|||
sendMessage,
|
||||
canvasOpen,
|
||||
setOpen,
|
||||
playgroundTitle,
|
||||
playgroundPage,
|
||||
}: ChatViewWrapperProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-full w-full flex-col justify-between p-4",
|
||||
"flex h-full w-full flex-col justify-between px-4 pb-4 pt-2",
|
||||
selectedViewField ? "hidden" : "",
|
||||
)}
|
||||
>
|
||||
<div className="mb-4 h-[5%] text-[16px] font-semibold">
|
||||
{visibleSession && sessions.length > 0 && sidebarOpen && (
|
||||
<div className="hidden lg:block">
|
||||
{visibleSession === currentFlowId
|
||||
? "Default Session"
|
||||
: `${visibleSession}`}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"mb-4 flex h-[5%] items-center text-[16px] font-semibold",
|
||||
playgroundPage ? "justify-between" : "lg:justify-start",
|
||||
)}
|
||||
<div className={cn(sidebarOpen ? "lg:hidden" : "")}>
|
||||
>
|
||||
<div className={cn(sidebarOpen ? "lg:hidden" : "left-4")}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
|
@ -50,14 +50,26 @@ export const ChatViewWrapper = ({
|
|||
className="h-[18px] w-[18px] text-ring"
|
||||
/>
|
||||
</Button>
|
||||
<div className="font-semibold">Playground</div>
|
||||
</div>
|
||||
</div>
|
||||
{visibleSession && sessions.length > 0 && (
|
||||
<div
|
||||
className={cn(
|
||||
"truncate text-center font-semibold",
|
||||
playgroundPage ? "" : "mr-12 flex-grow lg:mr-0",
|
||||
sidebarOpen ? "blur-sm lg:blur-0" : "",
|
||||
)}
|
||||
>
|
||||
{visibleSession === currentFlowId
|
||||
? "Default Session"
|
||||
: `${visibleSession}`}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
sidebarOpen ? "pointer-events-none opacity-0" : "",
|
||||
"absolute flex h-8 items-center justify-center rounded-sm ring-offset-background transition-opacity focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
isPlayground ? "right-2 top-4" : "right-12 top-2",
|
||||
"flex items-center justify-center rounded-sm ring-offset-background transition-opacity focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
playgroundPage ? "right-2 top-4" : "absolute right-12 top-2 h-8",
|
||||
)}
|
||||
>
|
||||
<ShadTooltip side="bottom" styleClasses="z-50" content="New Chat">
|
||||
|
|
@ -76,7 +88,7 @@ export const ChatViewWrapper = ({
|
|||
/>
|
||||
</Button>
|
||||
</ShadTooltip>
|
||||
{!isPlayground && <Separator orientation="vertical" />}
|
||||
{!playgroundPage && <Separator orientation="vertical" />}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
|
@ -99,6 +111,7 @@ export const ChatViewWrapper = ({
|
|||
setOpen(false);
|
||||
}
|
||||
}
|
||||
playgroundPage={playgroundPage}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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<HTMLDivElement | null>(null);
|
||||
const [chatHistory, setChatHistory] = useState<ChatMessageType[] | undefined>(
|
||||
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({
|
|||
</div>
|
||||
<div className="m-auto w-full max-w-[768px] md:w-5/6">
|
||||
<ChatInput
|
||||
playgroundPage={!!playgroundPage}
|
||||
noInput={!inputTypes.includes("ChatInput")}
|
||||
sendMessage={({ repeat, files }) => {
|
||||
sendMessage({ repeat, files });
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ export default function ChatInput({
|
|||
files,
|
||||
setFiles,
|
||||
isDragging,
|
||||
playgroundPage,
|
||||
}: ChatInputType): JSX.Element {
|
||||
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
|
@ -49,6 +50,10 @@ export default function ChatInput({
|
|||
const handleFileChange = async (
|
||||
event: React.ChangeEvent<HTMLInputElement> | ClipboardEvent,
|
||||
) => {
|
||||
if (playgroundPage) {
|
||||
return;
|
||||
}
|
||||
|
||||
let file: File | null = null;
|
||||
|
||||
if ("clipboardData" in event) {
|
||||
|
|
@ -238,15 +243,17 @@ export default function ChatInput({
|
|||
))}
|
||||
</div>
|
||||
<div className="flex w-full items-end justify-between">
|
||||
<div className={isBuilding ? "cursor-not-allowed" : ""}>
|
||||
<UploadFileButton
|
||||
isBuilding={isBuilding}
|
||||
fileInputRef={fileInputRef}
|
||||
handleFileChange={handleFileChange}
|
||||
handleButtonClick={handleButtonClick}
|
||||
/>
|
||||
</div>
|
||||
<div className="">
|
||||
{!playgroundPage && (
|
||||
<div className={isBuilding ? "cursor-not-allowed" : ""}>
|
||||
<UploadFileButton
|
||||
isBuilding={isBuilding}
|
||||
fileInputRef={fileInputRef}
|
||||
handleFileChange={handleFileChange}
|
||||
handleButtonClick={handleButtonClick}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className={playgroundPage ? "ml-auto" : ""}>
|
||||
<ButtonSendWrapper
|
||||
send={send}
|
||||
noInput={noInput}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,10 @@ import { ProfileIcon } from "@/components/core/appHeaderComponent/components/Pro
|
|||
import { ContentBlockDisplay } from "@/components/core/chatComponents/ContentBlockDisplay";
|
||||
import { useUpdateMessage } from "@/controllers/API/queries/messages";
|
||||
import { CustomProfileIcon } from "@/customization/components/custom-profile-icon";
|
||||
import { ENABLE_DATASTAX_LANGFLOW } from "@/customization/feature-flags";
|
||||
import {
|
||||
ENABLE_DATASTAX_LANGFLOW,
|
||||
ENABLE_PUBLISH,
|
||||
} from "@/customization/feature-flags";
|
||||
import useFlowsManagerStore from "@/stores/flowsManagerStore";
|
||||
import useFlowStore from "@/stores/flowStore";
|
||||
import { useUtilityStore } from "@/stores/utilityStore";
|
||||
|
|
@ -31,6 +34,7 @@ export default function ChatMessage({
|
|||
lastMessage,
|
||||
updateChat,
|
||||
closeChat,
|
||||
playgroundPage,
|
||||
}: chatMessagePropsType): JSX.Element {
|
||||
const convert = new Convert({ newline: true });
|
||||
const [hidden, setHidden] = useState(true);
|
||||
|
|
@ -277,8 +281,10 @@ export default function ChatMessage({
|
|||
) : (
|
||||
<ForwardedIconComponent name={chat.properties.icon} />
|
||||
)
|
||||
) : !ENABLE_DATASTAX_LANGFLOW ? (
|
||||
) : !ENABLE_DATASTAX_LANGFLOW && !playgroundPage ? (
|
||||
<ProfileIcon />
|
||||
) : playgroundPage ? (
|
||||
<ForwardedIconComponent name="User" />
|
||||
) : (
|
||||
<CustomProfileIcon />
|
||||
)}
|
||||
|
|
@ -301,7 +307,7 @@ export default function ChatMessage({
|
|||
}
|
||||
>
|
||||
{chat.sender_name}
|
||||
{chat.properties?.source && (
|
||||
{chat.properties?.source && !playgroundPage && (
|
||||
<div className="text-[13px] font-normal text-muted-foreground">
|
||||
{chat.properties?.source.source}
|
||||
</div>
|
||||
|
|
@ -310,6 +316,7 @@ export default function ChatMessage({
|
|||
</div>
|
||||
{chat.content_blocks && chat.content_blocks.length > 0 && (
|
||||
<ContentBlockDisplay
|
||||
playgroundPage={playgroundPage}
|
||||
contentBlocks={chat.content_blocks}
|
||||
isLoading={
|
||||
chatMessage === "" &&
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export const SidebarOpenView = ({
|
|||
handleDeleteSession,
|
||||
visibleSession,
|
||||
selectedViewField,
|
||||
playgroundPage,
|
||||
}: SidebarOpenViewProps) => {
|
||||
return (
|
||||
<>
|
||||
|
|
@ -51,6 +52,7 @@ export const SidebarOpenView = ({
|
|||
selectedView={selectedViewField}
|
||||
key={index}
|
||||
session={session}
|
||||
playgroundPage={playgroundPage}
|
||||
deleteSession={(session) => {
|
||||
handleDeleteSession(session);
|
||||
if (selectedViewField?.id === session) {
|
||||
|
|
|
|||
|
|
@ -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<string | undefined>(
|
||||
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 (
|
||||
<BaseModal
|
||||
open={open}
|
||||
|
|
@ -261,8 +298,31 @@ export default function IOModal({
|
|||
: "w-0",
|
||||
)}
|
||||
>
|
||||
<div className="flex h-full flex-col overflow-y-auto border-r border-border bg-muted p-4 text-center custom-scroll dark:bg-canvas">
|
||||
<div className="flex items-center gap-2 pb-8">
|
||||
<div
|
||||
className={cn(
|
||||
"relative flex h-full flex-col overflow-y-auto border-r border-border bg-muted p-4 text-center custom-scroll dark:bg-canvas",
|
||||
playgroundPage ? "pt-[15px]" : "pt-3.5",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2 pb-8 align-middle">
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className={cn(
|
||||
`flex rounded p-1`,
|
||||
swatchColors[swatchIndex],
|
||||
)}
|
||||
>
|
||||
<IconComponent
|
||||
name={currentFlow?.icon ?? "Workflow"}
|
||||
className="h-3.5 w-3.5"
|
||||
/>
|
||||
</div>
|
||||
{sidebarOpen && (
|
||||
<div className="truncate font-semibold">
|
||||
{PlaygroundTitle}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<ShadTooltip
|
||||
styleClasses="z-50"
|
||||
side="right"
|
||||
|
|
@ -279,9 +339,6 @@ export default function IOModal({
|
|||
/>
|
||||
</Button>
|
||||
</ShadTooltip>
|
||||
{sidebarOpen && (
|
||||
<div className="font-semibold">Playground</div>
|
||||
)}
|
||||
</div>
|
||||
{sidebarOpen && (
|
||||
<SidebarOpenView
|
||||
|
|
@ -291,10 +348,44 @@ export default function IOModal({
|
|||
handleDeleteSession={handleDeleteSession}
|
||||
visibleSession={visibleSession}
|
||||
selectedViewField={selectedViewField}
|
||||
playgroundPage={!!playgroundPage}
|
||||
/>
|
||||
)}
|
||||
{sidebarOpen && showPublishOptions && (
|
||||
<div className="absolute bottom-2 left-0 flex w-full flex-col gap-8 border-t border-border px-2 py-4 transition-all">
|
||||
<div className="flex items-center justify-between px-2">
|
||||
<div className="text-sm">Theme</div>
|
||||
<ThemeButtons />
|
||||
</div>
|
||||
<Button
|
||||
onClick={LangflowButtonClick}
|
||||
variant="primary"
|
||||
className="w-full !rounded-xl shadow-lg"
|
||||
>
|
||||
<LangflowLogoColor />
|
||||
<div className="text-sm">Built with Langflow</div>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{!sidebarOpen && showPublishOptions && (
|
||||
<div className="absolute bottom-6 left-4 hidden transition-all md:block">
|
||||
<ShadTooltip
|
||||
styleClasses="z-50"
|
||||
side="right"
|
||||
content="Built with Langflow"
|
||||
>
|
||||
<Button
|
||||
variant="primary"
|
||||
className="h-12 w-12 !rounded-xl !p-4 shadow-lg"
|
||||
onClick={LangflowButtonClick}
|
||||
>
|
||||
<LangflowLogoColor className="h-[18px] w-[18px] scale-150" />
|
||||
</Button>
|
||||
</ShadTooltip>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex h-full min-w-96 flex-grow bg-background">
|
||||
{selectedViewField && (
|
||||
<SelectedViewField
|
||||
|
|
@ -309,6 +400,7 @@ export default function IOModal({
|
|||
/>
|
||||
)}
|
||||
<ChatViewWrapper
|
||||
playgroundPage={playgroundPage}
|
||||
selectedViewField={selectedViewField}
|
||||
visibleSession={visibleSession}
|
||||
sessions={sessions}
|
||||
|
|
@ -324,6 +416,7 @@ export default function IOModal({
|
|||
sendMessage={sendMessage}
|
||||
canvasOpen={canvasOpen}
|
||||
setOpen={setOpen}
|
||||
playgroundTitle={PlaygroundTitle}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -16,4 +16,6 @@ export type ChatViewWrapperProps = {
|
|||
sendMessage: (options: { repeat: number; files?: string[] }) => Promise<void>;
|
||||
canvasOpen: boolean | undefined;
|
||||
setOpen: (open: boolean) => void;
|
||||
playgroundTitle: string;
|
||||
playgroundPage?: boolean;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,4 +7,5 @@ export type SidebarOpenViewProps = {
|
|||
handleDeleteSession: (session: string) => void;
|
||||
visibleSession: string | undefined;
|
||||
selectedViewField: { type: string; id: string } | undefined;
|
||||
playgroundPage: boolean;
|
||||
};
|
||||
|
|
|
|||
164
src/frontend/src/modals/apiModal/codeTabs/code-tabs.tsx
Normal file
164
src/frontend/src/modals/apiModal/codeTabs/code-tabs.tsx
Normal file
|
|
@ -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<Boolean>(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<number>(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 (
|
||||
<Tabs
|
||||
value={activeTab.toString()}
|
||||
className={"api-modal-tabs inset-0 m-0"}
|
||||
onValueChange={(value) => {
|
||||
setActiveTab(parseInt(value));
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
{tabsList.length > 0 && tabsList[0].title !== "" ? (
|
||||
<TabsList className="flex w-fit items-center rounded bg-muted p-1">
|
||||
{tabsList.map((tab, index) => (
|
||||
<TabsTrigger
|
||||
key={index}
|
||||
value={index.toString()}
|
||||
className="flex items-center gap-2.5 rounded-md !border-0 px-4 py-2 !text-[14px] data-[state=active]:bg-background"
|
||||
>
|
||||
<IconComponent name={tab.icon} className="h-4 w-4" />
|
||||
{tab.title}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
) : (
|
||||
<div></div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{tabsList.map((tab, idx) => (
|
||||
<TabsContent
|
||||
value={idx.toString()}
|
||||
className="api-modal-tabs-content mt-4 overflow-hidden"
|
||||
key={idx}
|
||||
>
|
||||
<div className="relative flex h-full w-full">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={copyToClipboard}
|
||||
data-testid="btn-copy-code"
|
||||
className="!hover:bg-foreground group absolute right-2 top-2"
|
||||
>
|
||||
{isCopied ? (
|
||||
<IconComponent
|
||||
name="Check"
|
||||
className="h-5 w-5 text-muted-foreground"
|
||||
/>
|
||||
) : (
|
||||
<IconComponent
|
||||
name="Copy"
|
||||
className="!h-5 !w-5 text-muted-foreground"
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
<SyntaxHighlighter
|
||||
showLineNumbers={true}
|
||||
wrapLongLines={true}
|
||||
language={tab.language}
|
||||
style={dark ? oneDark : oneLight}
|
||||
className="!mt-0 h-full w-full overflow-scroll !rounded-b-md border border-border text-left !custom-scroll"
|
||||
>
|
||||
{tab.code}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
141
src/frontend/src/modals/apiModal/new-api-modal.tsx
Normal file
141
src/frontend/src/modals/apiModal/new-api-modal.tsx
Normal file
|
|
@ -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 (
|
||||
<>
|
||||
<BaseModal
|
||||
closeButtonClassName="!top-3"
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
size="medium"
|
||||
className="pt-4"
|
||||
>
|
||||
<BaseModal.Trigger asChild>{children}</BaseModal.Trigger>
|
||||
<BaseModal.Header
|
||||
description={
|
||||
autoLogin ? undefined : (
|
||||
<>
|
||||
<span className="pr-2">
|
||||
API access requires an API key. You can{" "}
|
||||
<a
|
||||
href="/settings/api-keys"
|
||||
className="text-accent-pink-foreground"
|
||||
>
|
||||
{" "}
|
||||
create an API key
|
||||
</a>{" "}
|
||||
in settings.
|
||||
</span>
|
||||
</>
|
||||
)
|
||||
}
|
||||
>
|
||||
<IconComponent
|
||||
name="Code2"
|
||||
className="h-6 w-6 text-gray-800 dark:text-white"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="pl-2">API access</span>
|
||||
{nodes.length > 0 && (
|
||||
<div className="border-r-1 absolute right-12 flex items-center text-[13px] font-medium leading-[16px]">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 px-3"
|
||||
onClick={() => setOpenTweaks(true)}
|
||||
>
|
||||
<IconComponent
|
||||
name="SlidersHorizontal"
|
||||
className="h-3.5 w-3.5"
|
||||
/>
|
||||
<span>Tweaks ({Object.keys(tweaks)?.length}) </span>
|
||||
</Button>
|
||||
<Separator orientation="vertical" className="ml-2 h-8" />
|
||||
</div>
|
||||
)}
|
||||
</BaseModal.Header>
|
||||
<BaseModal.Content overflowHidden>
|
||||
{open && (
|
||||
<>
|
||||
<CustomAPIGenerator isOpen={open} />
|
||||
<APITabsComponent />
|
||||
</>
|
||||
)}
|
||||
</BaseModal.Content>
|
||||
</BaseModal>
|
||||
|
||||
<BaseModal
|
||||
open={openTweaks}
|
||||
setOpen={setOpenTweaks}
|
||||
size="medium-small-tall"
|
||||
>
|
||||
<BaseModal.Header
|
||||
description={
|
||||
autoLogin ? undefined : (
|
||||
<>
|
||||
<span className="pr-2">
|
||||
API access requires an API key. You can{" "}
|
||||
<a
|
||||
href="/settings/api-keys"
|
||||
className="text-accent-pink-foreground"
|
||||
>
|
||||
{" "}
|
||||
create an API key
|
||||
</a>{" "}
|
||||
in settings.
|
||||
</span>
|
||||
</>
|
||||
)
|
||||
}
|
||||
>
|
||||
<IconComponent
|
||||
name="SlidersHorizontal"
|
||||
className="h-6 w-6 text-gray-800 dark:text-white"
|
||||
/>
|
||||
<span className="pl-2">Tweaks</span>
|
||||
</BaseModal.Header>
|
||||
<BaseModal.Content overflowHidden>
|
||||
<div className="h-full w-full overflow-y-auto overflow-x-hidden rounded-lg bg-muted custom-scroll">
|
||||
<TweaksComponent open={openTweaks} />
|
||||
</div>
|
||||
</BaseModal.Content>
|
||||
</BaseModal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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)}'`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,9 +9,18 @@ export default function getWidgetCode({
|
|||
flowId,
|
||||
flowName,
|
||||
isAuth,
|
||||
copy = false,
|
||||
}: GetCodeType): string {
|
||||
return `<script src="https://cdn.jsdelivr.net/gh/logspace-ai/langflow-embedded-chat@v1.0.7/dist/build/static/js/bundle.min.js"></script>
|
||||
const source = copy
|
||||
? `<script
|
||||
src="https://cdn.jsdelivr.net/gh/logspace-ai/langflow-embedded-chat@v1.0.7/dist/build/static/js/bundle.min.js">
|
||||
</script>`
|
||||
: `<script
|
||||
src="https://cdn.jsdelivr.net/gh/logspace-ai/langflow-embedded-chat@v1.0.7/dist/
|
||||
build/static/js/bundle.min.js">
|
||||
</script>`;
|
||||
|
||||
return `${source}
|
||||
<langflow-chat
|
||||
window_title="${flowName}"
|
||||
flow_id="${flowId}"
|
||||
|
|
@ -20,7 +29,6 @@ export default function getWidgetCode({
|
|||
? `
|
||||
api_key="..."`
|
||||
: ""
|
||||
}
|
||||
|
||||
></langflow-chat>`;
|
||||
}>
|
||||
</langflow-chat>`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]";
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ const Trigger: React.FC<TriggerProps> = ({
|
|||
|
||||
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<{
|
|||
<DialogTitle className="line-clamp-1 flex items-center pb-0.5 text-base">
|
||||
{children}
|
||||
</DialogTitle>
|
||||
<DialogDescription
|
||||
className={`line-clamp-${clampDescription ?? 2} text-sm`}
|
||||
>
|
||||
{description}
|
||||
</DialogDescription>
|
||||
{description && (
|
||||
<DialogDescription
|
||||
className={`line-clamp-${clampDescription ?? 2} text-sm`}
|
||||
>
|
||||
{description}
|
||||
</DialogDescription>
|
||||
)}
|
||||
</DialogHeader>
|
||||
);
|
||||
};
|
||||
|
|
@ -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 ? (
|
||||
<Form.Root
|
||||
|
|
|
|||
|
|
@ -40,14 +40,6 @@ export function AppInitPage() {
|
|||
}
|
||||
}, [isFetched]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!dark) {
|
||||
document.getElementById("body")!.classList.remove("dark");
|
||||
} else {
|
||||
document.getElementById("body")!.classList.add("dark");
|
||||
}
|
||||
}, [dark]);
|
||||
|
||||
return (
|
||||
//need parent component with width and height
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="flex h-full w-full flex-col items-center justify-center align-middle">
|
||||
{currentSavedFlow && (
|
||||
<IOModal open={true} setOpen={() => {}} isPlayground>
|
||||
<IOModal open={true} setOpen={() => {}} isPlayground playgroundPage>
|
||||
<></>
|
||||
</IOModal>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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([
|
||||
<Route path="/playground/:id/">
|
||||
<Route
|
||||
path=""
|
||||
element={
|
||||
<ContextWrapper key={1}>
|
||||
<PlaygroundPage />
|
||||
</ContextWrapper>
|
||||
}
|
||||
/>
|
||||
</Route>,
|
||||
<Route
|
||||
path={ENABLE_CUSTOM_PARAM ? "/:customParam?" : "/"}
|
||||
element={
|
||||
<ContextWrapper>
|
||||
<ContextWrapper key={2}>
|
||||
<Outlet />
|
||||
</ContextWrapper>
|
||||
}
|
||||
|
|
@ -151,9 +161,6 @@ const router = createBrowserRouter(
|
|||
</Route>
|
||||
<Route path="view" element={<ViewPage />} />
|
||||
</Route>
|
||||
{/* <Route path="playground/:id/">
|
||||
<Route path="" element={<PlaygroundPage />} />
|
||||
</Route> */}
|
||||
</Route>
|
||||
</Route>
|
||||
<Route
|
||||
|
|
|
|||
|
|
@ -58,6 +58,10 @@ import { useTypesStore } from "./typesStore";
|
|||
|
||||
// this is our useStore hook that we can use in our components to get parts of the store and call actions
|
||||
const useFlowStore = create<FlowStoreType>((set, get) => ({
|
||||
playgroundPage: false,
|
||||
setPlaygroundPage: (playgroundPage) => {
|
||||
set({ playgroundPage });
|
||||
},
|
||||
positionDictionary: {},
|
||||
setPositionDictionary: (positionDictionary) => {
|
||||
set({ positionDictionary });
|
||||
|
|
@ -604,6 +608,7 @@ const useFlowStore = create<FlowStoreType>((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<FlowStoreType>((set, get) => ({
|
|||
nodes: get().nodes || undefined,
|
||||
edges: get().edges || undefined,
|
||||
logBuilds: get().onFlowPage,
|
||||
playgroundPage,
|
||||
stream,
|
||||
});
|
||||
get().setIsBuilding(false);
|
||||
|
|
|
|||
|
|
@ -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<TweaksStoreType>((set, get) => ({
|
||||
activeTweaks: false,
|
||||
tweaks: {},
|
||||
setActiveTweaks: (activeTweaks: boolean) => {
|
||||
set({ activeTweaks }), get().refreshTabs();
|
||||
},
|
||||
|
|
@ -21,6 +22,7 @@ export const useTweaksStore = create<TweaksStoreType>((set, get) => ({
|
|||
nodes: newChange,
|
||||
});
|
||||
get().refreshTabs();
|
||||
get().updateTweaks();
|
||||
},
|
||||
setNode: (id, change) => {
|
||||
let newChange =
|
||||
|
|
@ -59,6 +61,13 @@ export const useTweaksStore = create<TweaksStoreType>((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<TweaksStoreType>((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,
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import { UtilityStoreType } from "@/types/zustand/utility";
|
|||
import { create } from "zustand";
|
||||
|
||||
export const useUtilityStore = create<UtilityStoreType>((set, get) => ({
|
||||
clientId: "",
|
||||
setClientId: (clientId: string) => set({ clientId }),
|
||||
dismissAll: false,
|
||||
setDismissAll: (dismissAll: boolean) => set({ dismissAll }),
|
||||
chatValueStore: "",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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<NodeDataType, "genericNode">;
|
||||
|
|
|
|||
|
|
@ -69,3 +69,13 @@ export type errorsVarType = {
|
|||
title: string;
|
||||
list?: Array<string>;
|
||||
};
|
||||
|
||||
export type APITabType = {
|
||||
title: string;
|
||||
language: string;
|
||||
icon: string;
|
||||
code: string;
|
||||
copyCode: string;
|
||||
};
|
||||
|
||||
export type tabsArrayType = Array<APITabType>;
|
||||
|
|
|
|||
|
|
@ -14,4 +14,5 @@ export type GetCodeType = {
|
|||
tweaksBuildedObject?: {};
|
||||
endpointName?: string | null;
|
||||
activeTweaks?: boolean;
|
||||
copy?: boolean;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -139,6 +139,8 @@ export type FlowStoreType = {
|
|||
getFilterEdge: any[];
|
||||
onConnect: (connection: Connection) => void;
|
||||
unselectAll: () => void;
|
||||
playgroundPage: boolean;
|
||||
setPlaygroundPage: (playgroundPage: boolean) => void;
|
||||
buildFlow: ({
|
||||
startNodeId,
|
||||
stopNodeId,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -21,4 +21,6 @@ export type UtilityStoreType = {
|
|||
setChatValueStore: (value: string) => void;
|
||||
dismissAll: boolean;
|
||||
setDismissAll: (dismissAll: boolean) => void;
|
||||
setClientId: (clientId: string) => void;
|
||||
clientId: string;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
73
src/frontend/tests/core/features/publish-flow.spec.ts
Normal file
73
src/frontend/tests/core/features/publish-flow.spec.ts
Normal file
|
|
@ -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();
|
||||
},
|
||||
);
|
||||
|
|
@ -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(() =>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue