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:
anovazzi1 2025-03-17 11:03:59 -03:00 • committed by GitHub
commit 7aca264fec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
86 changed files with 2252 additions and 1465 deletions

File diff suppressed because it is too large Load diff

View file

@ -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} />

View 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

View file

@ -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"

View file

@ -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}`}
/>

View file

@ -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>

View file

@ -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>
</>
);
}

View file

@ -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>
);
}

View file

@ -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

View file

@ -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>
</>

View file

@ -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 = ({

View file

@ -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",

View file

@ -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();

View file

@ -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 = {}) {

View file

@ -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];

View file

@ -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;
};

View file

@ -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 () => {

View file

@ -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(

View file

@ -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> =

View file

@ -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;

View file

@ -0,0 +1,3 @@
export const LangflowButtonRedirectTarget = () => {
return "https://langflow.org";
};

View 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;

View 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} />;
});

View 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

View 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

View 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;

View 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} />;
},
);

View 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>
);
}

View file

@ -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();

View file

@ -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>

View file

@ -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 });

View file

@ -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}

View file

@ -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 === "" &&

View file

@ -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) {

View file

@ -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>

View file

@ -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;
};

View file

@ -7,4 +7,5 @@ export type SidebarOpenViewProps = {
handleDeleteSession: (session: string) => void;
visibleSession: string | undefined;
selectedViewField: { type: string; id: string } | undefined;
playgroundPage: boolean;
};

View 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>
);
}

View 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>
</>
);
}

View file

@ -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)}'`;
}

View file

@ -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));
`;
}

View file

@ -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}")
`;
}

View file

@ -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>`;
}

View file

@ -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]";

View file

@ -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

View file

@ -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
<>

View file

@ -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>
)}

View file

@ -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

View file

@ -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);

View file

@ -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,
});
},
}));

View file

@ -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: "",

View file

@ -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;

View file

@ -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;
}

View file

@ -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 = {

View file

@ -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">;

View file

@ -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>;

View file

@ -14,4 +14,5 @@ export type GetCodeType = {
tweaksBuildedObject?: {};
endpointName?: string | null;
activeTweaks?: boolean;
copy?: boolean;
};

View file

@ -139,6 +139,8 @@ export type FlowStoreType = {
getFilterEdge: any[];
onConnect: (connection: Connection) => void;
unselectAll: () => void;
playgroundPage: boolean;
setPlaygroundPage: (playgroundPage: boolean) => void;
buildFlow: ({
startNodeId,
stopNodeId,

View file

@ -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;
};
};
};

View file

@ -21,4 +21,6 @@ export type UtilityStoreType = {
setChatValueStore: (value: string) => void;
dismissAll: boolean;
setDismissAll: (dismissAll: boolean) => void;
setClientId: (clientId: string) => void;
clientId: string;
};

View file

@ -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) {

View file

@ -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);
}

View file

@ -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,

View file

@ -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;
}

View file

@ -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": {

View file

@ -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();

View 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();
},
);

View file

@ -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(() =>

View file

@ -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);

View file

@ -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(),