feat: add observable UX for community interaction tracking (#7512)

* ✨ (model.py): add UserOptin class to manage user opt-in actions for better organization and extensibility
♻️ (model.py): refactor User model to include user_optin field as a dictionary to store opt-in actions for users

* [autofix.ci] apply automated fixes

* change name optins

* [autofix.ci] apply automated fixes

* ✨ (add_optins_column_to_user.py): Add optins column to the user table to store user preferences
♻️ (model.py): Refactor UserOptin class to BaseModel for better type hinting and add optins field to User model with default values and proper typing

* [autofix.ci] apply automated fixes

* 🐛 (add_optins_column_to_user.py): fix an issue where the optins column was not being added if it already existed in the user table

* ✨ (empty-page.tsx): Add new page EmptyPageCommunity to display community information and actions
✨ (main-page.tsx): Add new page CollectionPage to manage collections and folders
🔧 (routes.tsx): Update import path for CollectionPage to point to the new main-page file

* ✨ (background-gradient.tsx): Add a new component BackgroundGradient to create a visually appealing background gradient effect for UI elements
📝 (empty-page.tsx): Refactor EmptyPageCommunity component to use the newly added BackgroundGradient component for GitHub and Discord sections to enhance visual appeal and consistency

* 📝 (tasks.mdc): Add concise task management protocol for sequential mode to improve task organization and execution
✨ (frontend): Introduce DotBackgroundDemo component for creating a visually appealing dot background effect
♻️ (frontend): Refactor BackgroundGradient component to improve gradient styling and border consistency
🔧 (frontend): Update motion import in background-gradient.tsx to use framer-motion instead of motion/react
🔧 (icons): Add missing newline at the end of Anthropic icon file
🔧 (empty-page.tsx): Adjust styling classes and z-index to improve layout and visual hierarchy in EmptyPageCommunity component

* ✨ (empty-page.tsx): add githubBg image import to use as background image for GitHub link
♻️ (empty-page.tsx): refactor positioning and styling of GitHub link elements for better alignment and readability

* 🔧 refactor(server.ts): change port variable case from lowercase port to uppercase PORT to improve semantics
✨ refactor(server.ts): add support for process.env.PORT environment variable to be able to run app on a configurable port

* 📝 (AccountMenu/index.tsx): Update imports and remove unused code for better organization and performance
🔧 (use-get-version.ts): Add functionality to refresh the latest version in darkStore after fetching version data
♻️ (darkStore.ts): Add refreshLatestVersion function to update the latest version in darkStore
📝 (dark/index.ts): Add latestVersion field and refreshLatestVersion function to DarkStoreType for better state management

* ✨ (AccountMenu/index.tsx): Add constants for Discord, Docs, GitHub, and Twitter URLs for better maintainability and reusability
📝 (constants.ts): Update Twitter URL to a new value for consistency with other URLs
📝 (TwitterX): Add new TwitterX icon and component for use in the application
📝 (styleUtils.ts): Import and use the new TwitterXIcon in the list of node icons
📝 (utils.ts): Update formatNumber function to handle undefined input values for better error handling

* [autofix.ci] apply automated fixes

* 📝 (add_optins_column_to_user.py): Update down_revision to '1b8b740a6fa3' for consistency
🔧 (AccountMenu/index.tsx): Adjust classNameSize prop value to 'w-[272px]' for styling consistency
🔧 (HeaderMenu/index.tsx): Update HeaderMenuItems component to accept classNameSize prop for dynamic styling
🔧 (langflow-counts.tsx): Adjust styling for better visual consistency and spacing
🔧 (index.tsx): Update className for Bell icon to include text-muted-foreground and strokeWidth
🔧 (get-started-progress.tsx): Update styling and spacing for better visual consistency
🔧 (header-buttons.tsx): Add Separator component for visual separation in HeaderButtons component

* 🔧 (AccountMenu/index.tsx): Adjust padding in AccountMenu component for better alignment and spacing. Fix ThemeButtons positioning for improved layout.

* ✨ (appHeaderComponent/index.tsx): Add support for managing flows and folders in the app header component
📝 (get-started-progress.tsx): Update heading tag to improve semantics
📝 (empty-page.tsx): Update text content in empty page to provide clearer instructions and information

* ✨ (background-gradient.tsx): Add support for dynamic border radius in BackgroundGradient component
🔧 (empty-page.tsx): Remove BackgroundGradient import and replace it with EnhancedBeamEffect component
🔧 (empty-page.tsx): Update styles and classes for EnhancedBeamEffect component and adjust layout
✨ (enchanced-beam-effect.tsx): Create EnhancedBeamEffect component to add enhanced beam effect to UI components

* ✨ (frontend): update text content and button labels in empty page component for better user experience
📝 (frontend): add data-testid attributes for testing purposes in various components
🔧 (frontend): add new test file for user progress tracking feature with Playwright tests

* ✨ (AccountMenu/index.tsx): Add Admin Page button for admin users in the account menu component
🔧 (user-progress-track.spec.ts): Add utility function addNewUserAndLogin to facilitate adding and logging in new users for testing purposes

* [autofix.ci] apply automated fixes

* 🐛 (get-started-progress.tsx): fix calculation of percentage to ensure it does not exceed 100%

* ✨ (empty-page.tsx): Add Lucide ExternalLink component for external links and update styling for external link icons
♻️ (empty-page.tsx): Refactor CSS classes for external link icons to improve readability and maintainability
📝 (index.css): Add custom CSS variable for Discord color
📝 (tailwind.config.mjs): Add Discord color to Tailwind CSS custom colors

* [autofix.ci] apply automated fixes

* add logo png

* ✨ (index.tsx): Add z-50 class to improve stacking context in CardsWrapComponent
✨ (empty-page.tsx): Add text-center class to center text elements in EmptyPageCommunity
✨ (empty-page.tsx): Adjust spacing and alignment in EmptyPageCommunity for better layout and readability

* 🐛 (AccountMenu/index.tsx): fix condition to show admin options only when isAdmin is true and autoLogin is false

* 🔧 (alertDropDown/index.tsx): update z-index value in PopoverContent class to z-50 for proper stacking order

* 🔧 (index.tsx): update z-index value to improve the stacking order of the component on the page

* ♻️ (index.tsx): refactor classNames in CardsWrapComponent to improve readability and maintainability

* 🐛 (empty-page.tsx): fix data-testid attribute value to match the updated element name for better consistency and clarity
🐛 (user-progress-track.spec.ts): fix test cases to match the updated data-testid attribute value for the main page title element to ensure accurate testing and assertions

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Cristhian Zanforlin Lousa 2025-04-22 20:26:33 -03:00 • committed by GitHub
commit a8ae17b86d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
45 changed files with 1482 additions and 209 deletions

51
.cursor/rules/tasks.mdc Normal file
View file

@ -0,0 +1,51 @@
---
description:
globs:
alwaysApply: true
---
# Concise Task Management Protocol - Sequential Mode
## File Creation
- Filename: `YYYYMMDD_HHMMSS_task_name.md`
- IMPORTANT: Always include a descriptive task name after the timestamp (e.g., `20250404_135621_api_integration.md`)
- Never create files with timestamp only
- Store in `tasks/` directory
## Task Structure
```
# Task: [Task Name]
**Status**: [Not Started | In Progress | Completed]
## Analysis
- [ ] Requirements
- [ ] Subtask 1
- [ ] Challenges
- [ ] Dependencies
## Plan
- [ ] Step 1: [Description]
- [ ] Subtask 1.1
- [ ] Step 2: [Description]
## Execution
- [ ] Implementation 1
- [ ] Details
- [ ] Implementation 2
- [ ] Files modified: [files]
## Summary
- [ ] Files modified: `filename.ext` (lines X-Y)
- [ ] Dependencies added/changed
- [ ] Edge cases considered
- [ ] Known limitations
- [ ] Future impact points
```
## Execution Rules
- Execute one subtask at a time in sequence
- Update the task file after EACH subtask is completed
- Mark completed subtasks with [x] as they are finished
- Update main task status throughout execution
- Document work incrementally, not all at once
- Never proceed to the next section until all subtasks in the current section are completed and marked

View file

@ -0,0 +1,38 @@
"""add_optins_column_to_user
Revision ID: e56d87f8994a
Revises: 1b8b740a6fa3
Create Date: 2025-04-09 15:57:46.904977
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
import sqlmodel
from sqlalchemy.engine.reflection import Inspector
from langflow.utils import migration
# revision identifiers, used by Alembic.
revision: str = 'e56d87f8994a'
down_revision: Union[str, None] = '1b8b740a6fa3'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
conn = op.get_bind()
# ### commands auto generated by Alembic - please adjust! ###
if not migration.column_exists(table_name='user', column_name='optins', conn=conn):
with op.batch_alter_table('user', schema=None) as batch_op:
batch_op.add_column(sa.Column('optins', sa.JSON(), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
conn = op.get_bind()
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('user', schema=None) as batch_op:
batch_op.drop_column('optins')
# ### end Alembic commands ###

View file

@ -1,7 +1,9 @@
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Any
from uuid import UUID, uuid4 from uuid import UUID, uuid4
from pydantic import BaseModel
from sqlalchemy import JSON, Column
from sqlmodel import Field, Relationship, SQLModel from sqlmodel import Field, Relationship, SQLModel
from langflow.schema.serialize import UUIDstr from langflow.schema.serialize import UUIDstr
@ -13,6 +15,13 @@ if TYPE_CHECKING:
from langflow.services.database.models.variable import Variable from langflow.services.database.models.variable import Variable
class UserOptin(BaseModel):
github_starred: bool = Field(default=False)
dialog_dismissed: bool = Field(default=False)
discord_clicked: bool = Field(default=False)
# Add more opt-in actions as needed
class User(SQLModel, table=True): # type: ignore[call-arg] class User(SQLModel, table=True): # type: ignore[call-arg]
id: UUIDstr = Field(default_factory=uuid4, primary_key=True, unique=True) id: UUIDstr = Field(default_factory=uuid4, primary_key=True, unique=True)
username: str = Field(index=True, unique=True) username: str = Field(index=True, unique=True)
@ -37,11 +46,17 @@ class User(SQLModel, table=True): # type: ignore[call-arg]
back_populates="user", back_populates="user",
sa_relationship_kwargs={"cascade": "delete"}, sa_relationship_kwargs={"cascade": "delete"},
) )
optins: dict[str, Any] | None = Field(
sa_column=Column(JSON, default=lambda: UserOptin().model_dump(), nullable=True)
)
class UserCreate(SQLModel): class UserCreate(SQLModel):
username: str = Field() username: str = Field()
password: str = Field() password: str = Field()
optins: dict[str, Any] | None = Field(
default={"github_starred": False, "dialog_dismissed": False, "discord_clicked": False}
)
class UserRead(SQLModel): class UserRead(SQLModel):
@ -54,6 +69,7 @@ class UserRead(SQLModel):
create_at: datetime = Field() create_at: datetime = Field()
updated_at: datetime = Field() updated_at: datetime = Field()
last_login_at: datetime | None = Field(nullable=True) last_login_at: datetime | None = Field(nullable=True)
optins: dict[str, Any] | None = Field(default=None)
class UserUpdate(SQLModel): class UserUpdate(SQLModel):
@ -63,3 +79,4 @@ class UserUpdate(SQLModel):
is_active: bool | None = None is_active: bool | None = None
is_superuser: bool | None = None is_superuser: bool | None = None
last_login_at: datetime | None = None last_login_at: datetime | None = None
optins: dict[str, Any] | None = None

View file

@ -74,8 +74,6 @@ const RenderInputParameters = ({
return keyMap; return keyMap;
}, [templateFields, data.id, data.node?.template]); }, [templateFields, data.id, data.node?.template]);
console.log({ data });
const renderInputParameter = templateFields.map( const renderInputParameter = templateFields.map(
(templateField: string, idx) => { (templateField: string, idx) => {
const template = data.node?.template[templateField]; const template = data.node?.template[templateField];

View file

@ -47,7 +47,7 @@ const AlertDropdown = forwardRef<HTMLDivElement, AlertDropdownType>(
<PopoverContent <PopoverContent
ref={notificationRef} ref={notificationRef}
data-testid="notification-dropdown-content" data-testid="notification-dropdown-content"
className="noflow nowheel nopan nodelete nodrag z-10 flex h-[500px] w-[500px] flex-col" className="noflow nowheel nopan nodelete nodrag z-50 flex h-[500px] w-[500px] flex-col"
> >
<div className="text-md flex flex-row justify-between pl-3 font-medium text-foreground"> <div className="text-md flex flex-row justify-between pl-3 font-medium text-foreground">
Notifications Notifications

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -1,33 +1,31 @@
import { ForwardedIconComponent } from "@/components/common/genericIconComponent";
import {
DISCORD_URL,
DOCS_URL,
GITHUB_URL,
TWITTER_URL,
} from "@/constants/constants";
import { useLogout } from "@/controllers/API/queries/auth"; import { useLogout } from "@/controllers/API/queries/auth";
import CustomFeatureFlagDialog from "@/customization/components/custom-feature-flag-dialog";
import CustomFeatureFlagMenuItems from "@/customization/components/custom-feature-flag-menu-items";
import { CustomFeedbackDialog } from "@/customization/components/custom-feedback-dialog";
import { CustomHeaderMenuItemsTitle } from "@/customization/components/custom-header-menu-items-title";
import { CustomProfileIcon } from "@/customization/components/custom-profile-icon";
import { ENABLE_DATASTAX_LANGFLOW } from "@/customization/feature-flags";
import { useCustomNavigate } from "@/customization/hooks/use-custom-navigate"; import { useCustomNavigate } from "@/customization/hooks/use-custom-navigate";
import useAuthStore from "@/stores/authStore"; import useAuthStore from "@/stores/authStore";
import { useDarkStore } from "@/stores/darkStore"; import { useDarkStore } from "@/stores/darkStore";
import { useState } from "react"; import { cn } from "@/utils/utils";
import { FaDiscord, FaGithub, FaTwitter } from "react-icons/fa";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import GithubStarComponent from "../GithubStarButton";
import { import {
HeaderMenu, HeaderMenu,
HeaderMenuItemButton, HeaderMenuItemButton,
HeaderMenuItemLink, HeaderMenuItemLink,
HeaderMenuItems, HeaderMenuItems,
HeaderMenuItemsSection,
HeaderMenuToggle, HeaderMenuToggle,
} from "../HeaderMenu"; } from "../HeaderMenu";
import { ProfileIcon } from "../ProfileIcon"; import { ProfileIcon } from "../ProfileIcon";
import ThemeButtons from "../ThemeButtons"; import ThemeButtons from "../ThemeButtons";
export const AccountMenu = () => { export const AccountMenu = () => {
const [isFeedbackOpen, setIsFeedbackOpen] = useState(false);
const [isCustomFeatureFlagsOpen, setIsCustomFeatureFlagsOpen] =
useState(false);
const { customParam: id } = useParams(); const { customParam: id } = useParams();
const version = useDarkStore((state) => state.version); const version = useDarkStore((state) => state.version);
const latestVersion = useDarkStore((state) => state.latestVersion);
const navigate = useCustomNavigate(); const navigate = useCustomNavigate();
const { mutate: mutationLogout } = useLogout(); const { mutate: mutationLogout } = useLogout();
@ -40,6 +38,8 @@ export const AccountMenu = () => {
mutationLogout(); mutationLogout();
}; };
const isLatestVersion = version === latestVersion;
return ( return (
<> <>
<HeaderMenu> <HeaderMenu>
@ -48,34 +48,37 @@ export const AccountMenu = () => {
className="h-7 w-7 rounded-lg focus-visible:outline-0" className="h-7 w-7 rounded-lg focus-visible:outline-0"
data-testid="user-profile-settings" data-testid="user-profile-settings"
> >
{ENABLE_DATASTAX_LANGFLOW ? <CustomProfileIcon /> : <ProfileIcon />} <ProfileIcon />
</div> </div>
</HeaderMenuToggle> </HeaderMenuToggle>
<HeaderMenuItems position="right"> <HeaderMenuItems position="right" classNameSize="w-[272px]">
{ENABLE_DATASTAX_LANGFLOW && ( <div className="divide-y divide-foreground/10">
<HeaderMenuItemsSection> <div>
<CustomHeaderMenuItemsTitle /> <div className="h-[46px] items-center px-4 pt-3">
</HeaderMenuItemsSection> <div className="flex items-center justify-between">
)} <span
<HeaderMenuItemsSection> data-testid="menu_version_button"
<div className="flex h-[46px] w-full items-center justify-between px-3"> id="menu_version_button"
<div className="pl-1 text-xs text-zinc-500"> className="text-sm"
<span >
data-testid="menu_version_button" Version
id="menu_version_button" </span>
> <div
Version {version} className={cn(
</span> "float-right text-xs",
isLatestVersion && "text-accent-emerald-foreground",
!isLatestVersion && "text-accent-amber-foreground",
)}
>
{version}{" "}
{isLatestVersion ? "(latest)" : "(update available)"}
</div>
</div>
</div> </div>
{!ENABLE_DATASTAX_LANGFLOW && <ThemeButtons />}
</div> </div>
{ENABLE_DATASTAX_LANGFLOW ? (
<HeaderMenuItemLink newPage href={`/settings/org/${id}/overview`}> <div>
Account Settings
</HeaderMenuItemLink>
) : (
<HeaderMenuItemButton <HeaderMenuItemButton
icon="arrow-right"
onClick={() => { onClick={() => {
navigate("/settings"); navigate("/settings");
}} }}
@ -87,100 +90,84 @@ export const AccountMenu = () => {
Settings Settings
</span> </span>
</HeaderMenuItemButton> </HeaderMenuItemButton>
)}
{!ENABLE_DATASTAX_LANGFLOW && ( {isAdmin && !autoLogin && (
<> <div>
{isAdmin && !autoLogin && ( <HeaderMenuItemButton
<HeaderMenuItemButton onClick={() => navigate("/admin")}> onClick={() => {
navigate("/admin");
}}
>
<span <span
data-testid="menu_admin_button" data-testid="menu_admin_page_button"
id="menu_admin_button" id="menu_admin_page_button"
> >
Admin Page Admin Page
</span> </span>
</HeaderMenuItemButton> </HeaderMenuItemButton>
)} </div>
</> )}
)} <HeaderMenuItemLink newPage href={DOCS_URL}>
{ENABLE_DATASTAX_LANGFLOW ? (
<>
<HeaderMenuItemButton onClick={() => setIsFeedbackOpen(true)}>
<span
data-testid="menu_feedback_button"
id="menu_feedback_button"
>
Feedback
</span>
</HeaderMenuItemButton>
<CustomFeatureFlagMenuItems
onClick={() => setIsCustomFeatureFlagsOpen(true)}
/>
</>
) : (
<HeaderMenuItemLink newPage href="https://docs.langflow.org">
<span data-testid="menu_docs_button" id="menu_docs_button"> <span data-testid="menu_docs_button" id="menu_docs_button">
Docs Docs
</span> </span>
</HeaderMenuItemLink> </HeaderMenuItemLink>
)} </div>
</HeaderMenuItemsSection>
<HeaderMenuItemsSection> <div>
{ENABLE_DATASTAX_LANGFLOW ? ( <HeaderMenuItemLink newPage href={GITHUB_URL}>
<HeaderMenuItemLink <span
newPage data-testid="menu_github_button"
href="https://github.com/langflow-ai/langflow" id="menu_github_button"
> className="flex items-center gap-2"
<div className="-my-2 mr-2 flex w-full items-center justify-between"> >
<div className="text-sm">Star the repo</div> <FaGithub className="h-4 w-4" />
<GithubStarComponent /> GitHub
</div>
</HeaderMenuItemLink>
) : (
<HeaderMenuItemLink
newPage
href="https://github.com/langflow-ai/langflow/discussions"
>
<span data-testid="menu_github_button" id="menu_github_button">
Share Feedback on Github
</span> </span>
</HeaderMenuItemLink> </HeaderMenuItemLink>
)} <HeaderMenuItemLink newPage href={DISCORD_URL}>
<HeaderMenuItemLink newPage href="https://twitter.com/langflow_ai"> <span
<span data-testid="menu_twitter_button" id="menu_twitter_button"> data-testid="menu_discord_button"
Follow Langflow on X id="menu_discord_button"
</span> className="flex items-center gap-2"
</HeaderMenuItemLink> >
<HeaderMenuItemLink newPage href="https://discord.gg/EqksyE2EX9"> <FaDiscord className="h-4 w-4 text-[#5865F2]" />
<span data-testid="menu_discord_button" id="menu_discord_button"> Discord
Join the Langflow Discord </span>
</span>
</HeaderMenuItemLink>
</HeaderMenuItemsSection>
{ENABLE_DATASTAX_LANGFLOW ? (
<HeaderMenuItemsSection>
<HeaderMenuItemLink href="/session/logout" icon="log-out">
Logout
</HeaderMenuItemLink> </HeaderMenuItemLink>
</HeaderMenuItemsSection> <HeaderMenuItemLink newPage href={TWITTER_URL}>
) : ( <span
!autoLogin && ( data-testid="menu_twitter_button"
<HeaderMenuItemsSection> id="menu_twitter_button"
className="flex items-center gap-2"
>
<ForwardedIconComponent
strokeWidth={2}
name="TwitterXIcon"
className="h-4 w-4"
/>
X
</span>
</HeaderMenuItemLink>
</div>
<div className="flex items-center justify-between px-4 py-1 text-sm">
<span className="">Theme</span>
<div className="relative top-[1px] float-right">
<ThemeButtons />
</div>
</div>
{!autoLogin && (
<div>
<HeaderMenuItemButton onClick={handleLogout} icon="log-out"> <HeaderMenuItemButton onClick={handleLogout} icon="log-out">
Logout Logout
</HeaderMenuItemButton> </HeaderMenuItemButton>
</HeaderMenuItemsSection> </div>
) )}
)} </div>
</HeaderMenuItems> </HeaderMenuItems>
</HeaderMenu> </HeaderMenu>
<CustomFeedbackDialog
isOpen={isFeedbackOpen}
setIsOpen={setIsFeedbackOpen}
/>
<CustomFeatureFlagDialog
isOpen={isCustomFeatureFlagsOpen}
setIsOpen={setIsCustomFeatureFlagsOpen}
/>
</> </>
); );
}; };

View file

@ -1,21 +0,0 @@
import ShadTooltip from "@/components/common/shadTooltipComponent";
import { useDarkStore } from "@/stores/darkStore";
import { FaGithub } from "react-icons/fa";
export const GithubStarComponent = () => {
const stars: number | undefined = useDarkStore((state) => state.stars);
return (
<ShadTooltip content="Go to Github repo" side="bottom" styleClasses="z-10">
<div className="group inline-flex h-8 items-center justify-center gap-1 rounded-md border bg-muted px-2 pr-0 hover:border-input hover:bg-secondary-hover">
<FaGithub className="h-4 w-4" />
<div className="hidden text-xs font-semibold lg:block">Star</div>
<div className="-mr-px ml-1 flex h-8 items-center justify-center rounded-md rounded-l-none border bg-background px-2 text-xs font-semibold text-secondary-foreground group-hover:border-input">
{stars?.toLocaleString() ?? 0}
</div>
</div>
</ShadTooltip>
);
};
export default GithubStarComponent;

View file

@ -22,11 +22,7 @@ export const HeaderMenuToggle = ({ children }) => (
> >
<div className="flex items-center gap-1 rounded-lg px-2 py-1.5 group-hover:bg-muted"> <div className="flex items-center gap-1 rounded-lg px-2 py-1.5 group-hover:bg-muted">
{children} {children}
<ChevronsUpDown <ChevronsUpDown className="h-4 w-4 text-muted-foreground group-hover:text-foreground" />
className="text-muted-foreground group-hover:text-foreground"
size={"15px"}
strokeWidth={"2px"}
/>
</div> </div>
</DropdownMenuTrigger> </DropdownMenuTrigger>
); );
@ -72,10 +68,14 @@ export const HeaderMenuItemButton = ({ icon = "", onClick, children }) => (
export const HeaderMenuItems = ({ export const HeaderMenuItems = ({
position = "left", position = "left",
children, children,
}: React.PropsWithChildren<{ position?: "left" | "right" }>) => { classNameSize = "w-[20rem]",
}: React.PropsWithChildren<{
position?: "left" | "right";
classNameSize?: string;
}>) => {
const positionClass = position === "left" ? "left-0" : "right-0"; const positionClass = position === "left" ? "left-0" : "right-0";
return ( return (
<DropdownMenuContent className={cn("w-[20rem]", positionClass)}> <DropdownMenuContent className={cn(classNameSize, positionClass)}>
{children} {children}
</DropdownMenuContent> </DropdownMenuContent>
); );

View file

@ -60,7 +60,7 @@ export const ThemeButtons = () => {
data-testid="menu_light_button" data-testid="menu_light_button"
id="menu_light_button" id="menu_light_button"
> >
<ForwardedIconComponent name="Sun" className="w-4" /> <ForwardedIconComponent strokeWidth={2} name="Sun" className="w-4" />
</Button> </Button>
{/* Dark Theme Button */} {/* Dark Theme Button */}
@ -68,14 +68,14 @@ export const ThemeButtons = () => {
unstyled unstyled
className={`relative z-10 mx-1 inline-flex items-center rounded-full px-1 ${ className={`relative z-10 mx-1 inline-flex items-center rounded-full px-1 ${
selectedTheme === "dark" selectedTheme === "dark"
? "text-background dark:hover:bg-purple-400" ? "bg-indigo-foreground text-primary hover:bg-indigo-foreground"
: "text-foreground hover:bg-purple-400 hover:text-background" : "text-foreground hover:bg-indigo-foreground hover:text-background"
}`} }`}
onClick={() => handleThemeChange("dark")} onClick={() => handleThemeChange("dark")}
data-testid="menu_dark_button" data-testid="menu_dark_button"
id="menu_dark_button" id="menu_dark_button"
> >
<ForwardedIconComponent name="Moon" className="w-4" /> <ForwardedIconComponent strokeWidth={2} name="Moon" className="w-4" />
</Button> </Button>
{/* System Theme Button */} {/* System Theme Button */}
@ -90,7 +90,11 @@ export const ThemeButtons = () => {
data-testid="menu_system_button" data-testid="menu_system_button"
id="menu_system_button" id="menu_system_button"
> >
<ForwardedIconComponent name="Monitor" className="w-4" /> <ForwardedIconComponent
name="Monitor"
className="w-4"
strokeWidth={2}
/>
</Button> </Button>
</div> </div>
); );

View file

@ -0,0 +1,39 @@
import ShadTooltip from "@/components/common/shadTooltipComponent";
import { useDarkStore } from "@/stores/darkStore";
import { formatNumber } from "@/utils/utils";
import { FaDiscord, FaGithub } from "react-icons/fa";
export const LangflowCounts = () => {
const stars: number | undefined = useDarkStore((state) => state.stars);
const discordCount: number = useDarkStore((state) => state.discordCount);
return (
<div className="flex items-center gap-5">
<ShadTooltip
content="Go to GitHub repo"
side="bottom"
styleClasses="z-10"
>
<div className="flex items-center gap-2 text-muted-foreground hover:text-foreground">
<FaGithub className="h-4 w-4" />
<span className="text-xs font-semibold">{formatNumber(stars)}</span>
</div>
</ShadTooltip>
<ShadTooltip
content="Go to Discord server"
side="bottom"
styleClasses="z-10"
>
<div className="flex items-center gap-2 text-muted-foreground hover:text-foreground">
<FaDiscord className="h-4 w-4" />
<span className="text-xs font-semibold">
{formatNumber(discordCount)}
</span>
</div>
</ShadTooltip>
</div>
);
};
export default LangflowCounts;

View file

@ -12,10 +12,12 @@ import { useCustomNavigate } from "@/customization/hooks/use-custom-navigate";
import useTheme from "@/customization/hooks/use-custom-theme"; import useTheme from "@/customization/hooks/use-custom-theme";
import { useResetDismissUpdateAll } from "@/hooks/use-reset-dismiss-update-all"; import { useResetDismissUpdateAll } from "@/hooks/use-reset-dismiss-update-all";
import useAlertStore from "@/stores/alertStore"; import useAlertStore from "@/stores/alertStore";
import useFlowsManagerStore from "@/stores/flowsManagerStore";
import { useFolderStore } from "@/stores/foldersStore";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { AccountMenu } from "./components/AccountMenu"; import { AccountMenu } from "./components/AccountMenu";
import FlowMenu from "./components/FlowMenu"; import FlowMenu from "./components/FlowMenu";
import GithubStarComponent from "./components/GithubStarButton"; import LangflowCounts from "./components/langflow-counts";
export default function AppHeader(): JSX.Element { export default function AppHeader(): JSX.Element {
const notificationCenter = useAlertStore((state) => state.notificationCenter); const notificationCenter = useAlertStore((state) => state.notificationCenter);
@ -46,9 +48,17 @@ export default function AppHeader(): JSX.Element {
useResetDismissUpdateAll(); useResetDismissUpdateAll();
const flows = useFlowsManagerStore((state) => state.flows);
const examples = useFlowsManagerStore((state) => state.examples);
const folders = useFolderStore((state) => state.folders);
const isEmpty = flows?.length !== examples?.length || folders?.length > 1;
return ( return (
<div <div
className="flex h-[62px] w-full items-center justify-between gap-2 border-b px-5 py-2.5 dark:bg-background" className={`flex h-[44px] w-full items-center justify-between border-b p-6 dark:bg-background ${
!isEmpty ? "hidden" : ""
}`}
data-testid="app-header" data-testid="app-header"
> >
{/* Left Section */} {/* Left Section */}
@ -83,7 +93,7 @@ export default function AppHeader(): JSX.Element {
{/* Right Section */} {/* Right Section */}
<div <div
className={`z-30 flex items-center gap-2`} className={`relative left-3 z-30 flex items-center gap-2`}
data-testid="header_right_section_wrapper" data-testid="header_right_section_wrapper"
> >
{!ENABLE_DATASTAX_LANGFLOW && ( {!ENABLE_DATASTAX_LANGFLOW && (
@ -95,7 +105,7 @@ export default function AppHeader(): JSX.Element {
window.open("https://github.com/langflow-ai/langflow", "_blank") window.open("https://github.com/langflow-ai/langflow", "_blank")
} }
> >
<GithubStarComponent /> <LangflowCounts />
</Button> </Button>
</> </>
)} )}
@ -111,8 +121,7 @@ export default function AppHeader(): JSX.Element {
<AlertDropdown onClose={() => setActiveState(null)}> <AlertDropdown onClose={() => setActiveState(null)}>
<Button <Button
ref={notificationRef} ref={notificationRef}
variant="ghost" unstyled
className={`relative ${activeState === "notifications" ? "bg-accent text-accent-foreground" : ""}`}
onClick={() => onClick={() =>
setActiveState((prev) => setActiveState((prev) =>
prev === "notifications" ? null : "notifications", prev === "notifications" ? null : "notifications",
@ -123,47 +132,24 @@ export default function AppHeader(): JSX.Element {
<span <span
className={ className={
notificationCenter notificationCenter
? `absolute left-[31px] top-[10px] h-1 w-1 rounded-full bg-destructive` ? `absolute right-[5.3rem] top-[10px] h-1 w-1 rounded-full bg-destructive`
: "hidden" : "hidden"
} }
/> />
<ForwardedIconComponent <ForwardedIconComponent
name="Bell" name="Bell"
className="side-bar-button-size h-[18px] w-[18px]" className="side-bar-button-size ml-1 h-4 w-4 text-muted-foreground"
strokeWidth={2}
/> />
<span className="hidden whitespace-nowrap">Notifications</span> <span className="hidden whitespace-nowrap">Notifications</span>
</Button> </Button>
</AlertDropdown> </AlertDropdown>
</ShadTooltip> </ShadTooltip>
</AlertDropdown> </AlertDropdown>
{!ENABLE_DATASTAX_LANGFLOW && ( <Separator
<> orientation="vertical"
<ShadTooltip className="my-auto ml-3 h-7 dark:border-zinc-700"
content="Go to Langflow Store" />
side="bottom"
styleClasses="z-10"
>
<Button
variant="ghost"
className={` ${lastPath === "store" ? "bg-accent text-accent-foreground" : ""} z-50`}
onClick={() => {
navigate("/store");
}}
data-testid="button-store"
>
<ForwardedIconComponent
name="Store"
className="side-bar-button-size h-[18px] w-[18px]"
/>
<span className="hidden whitespace-nowrap">Store</span>
</Button>
</ShadTooltip>
<Separator
orientation="vertical"
className="my-auto h-7 dark:border-zinc-700"
/>
</>
)}
{ENABLE_DATASTAX_LANGFLOW && ( {ENABLE_DATASTAX_LANGFLOW && (
<> <>
<ShadTooltip content="Docs" side="bottom" styleClasses="z-10"> <ShadTooltip content="Docs" side="bottom" styleClasses="z-10">

View file

@ -74,7 +74,7 @@ export default function CardsWrapComponent({
className={cn( className={cn(
"h-full w-full", "h-full w-full",
isDragging isDragging
? "mb-36 flex flex-col items-center justify-center gap-4 text-2xl font-light" ? "z-10 mb-36 flex flex-col items-center justify-center gap-4 text-2xl font-light"
: "", : "",
)} )}
> >

View file

@ -0,0 +1,234 @@
import IconComponent from "@/components/common/genericIconComponent";
import { Button } from "@/components/ui/button";
import { DISCORD_URL, GITHUB_URL } from "@/constants/constants";
import { useGetUserData, useUpdateUser } from "@/controllers/API/queries/auth";
import ModalsComponent from "@/pages/MainPage/components/modalsComponent";
import useFlowsManagerStore from "@/stores/flowsManagerStore";
import { Users } from "@/types/api";
import { cn } from "@/utils/utils";
import { FC, useEffect, useMemo, useState } from "react";
import { FaDiscord, FaGithub } from "react-icons/fa";
export const GetStartedProgress: FC<{
userData: Users;
isGithubStarred: boolean;
isDiscordJoined: boolean;
handleDismissDialog: () => void;
}> = ({ userData, isGithubStarred, isDiscordJoined, handleDismissDialog }) => {
const [isGithubStarredChild, setIsGithubStarredChild] =
useState(isGithubStarred);
const [isDiscordJoinedChild, setIsDiscordJoinedChild] =
useState(isDiscordJoined);
const [newProjectModal, setNewProjectModal] = useState(false);
const flows = useFlowsManagerStore((state) => state.flows);
const { mutate: mutateLoggedUser } = useGetUserData();
const { mutate: updateUser } = useUpdateUser();
useEffect(() => {
if (!userData) {
mutateLoggedUser(null);
}
}, [userData, mutateLoggedUser]);
const hasFlows = flows && flows?.length > 0;
const percentageGetStarted = useMemo(() => {
const totalSteps = 3;
let hasFlowsCount = 0;
const completedSteps = Object.keys(userData?.optins ?? {}).filter(
(key) => userData?.optins?.[key],
)?.length;
if (hasFlows) {
hasFlowsCount = 33;
}
const percentage =
Math.round((completedSteps / totalSteps) * 100) + hasFlowsCount;
if (percentage > 100) {
return 100;
}
return percentage;
}, [userData?.optins, isGithubStarredChild, isDiscordJoinedChild, hasFlows]);
const handleUserTrack = (key: string) => {
const optins = userData?.optins ?? {};
optins[key] = true;
updateUser(
{
user_id: userData?.id!,
user: { optins },
},
{
onSuccess: () => {
mutateLoggedUser({});
if (key === "github_starred") {
setIsGithubStarredChild(true);
window.open(GITHUB_URL, "_blank", "noopener,noreferrer");
} else if (key === "discord_clicked") {
setIsDiscordJoinedChild(true);
window.open(DISCORD_URL, "_blank", "noopener,noreferrer");
} else if (key === "dialog_dismissed") {
handleDismissDialog();
}
},
},
);
};
return (
<div className="h-[180px] w-full">
<div className="mb-2 flex items-center justify-between">
<span className="font-[14px]">Get started</span>
<button
onClick={() => handleUserTrack("dialog_dismissed")}
className="text-muted-foreground hover:text-foreground"
data-testid="close_get_started_dialog"
>
<IconComponent name="X" className="h-4 w-4" />
</button>
</div>
<div className="mb-1 flex items-center justify-between gap-3">
<div className="h-1 w-full rounded-full bg-muted">
<div
className="h-1 w-[33%] rounded-full bg-accent-pink-foreground"
style={{ width: `${percentageGetStarted}%` }}
/>
</div>
<span
className="text-xs text-muted-foreground"
data-testid="get_started_progress_percentage"
>
{percentageGetStarted}%
</span>
</div>
<div className="space-y-1">
<Button
data-testid="github_starred_btn_get_started"
unstyled
className={cn(
"w-full",
isGithubStarredChild && "pointer-events-none",
)}
onClick={(e) => {
if (isGithubStarredChild) {
e.preventDefault();
return;
}
handleUserTrack("github_starred");
}}
>
<div
className={cn(
"flex items-center gap-2 rounded-md p-2 hover:bg-muted",
isGithubStarredChild && "pointer-events-none",
)}
>
{isGithubStarredChild ? (
<span data-testid="github_starred_icon_get_started">
<IconComponent
name="Check"
className="h-4 w-4 text-accent-emerald-foreground"
/>
</span>
) : (
<FaGithub className="h-4 w-4" />
)}
<span
className={cn(
"text-sm",
isGithubStarredChild && "text-muted-foreground line-through",
)}
>
Star repo for updates
</span>
</div>
</Button>
<Button
data-testid="discord_joined_btn_get_started"
unstyled
className={cn(
"w-full",
isDiscordJoinedChild && "pointer-events-none",
)}
onClick={(e) => {
if (isDiscordJoinedChild) {
e.preventDefault();
return;
}
handleUserTrack("discord_clicked");
}}
>
<div
className={cn(
"flex items-center gap-2 rounded-md p-2 hover:bg-muted",
isDiscordJoinedChild && "pointer-events-none",
)}
>
{isDiscordJoinedChild ? (
<span data-testid="discord_joined_icon_get_started">
<IconComponent
name="Check"
className="h-4 w-4 text-accent-emerald-foreground"
/>
</span>
) : (
<FaDiscord className="h-4 w-4 text-[#5865F2]" />
)}
<span
className={cn(
"text-sm",
isDiscordJoinedChild && "text-muted-foreground line-through",
)}
>
Join the community
</span>
</div>
</Button>
<Button
unstyled
className={cn("w-full", hasFlows && "pointer-events-none")}
onClick={() => setNewProjectModal(true)}
>
<div
className={cn(
"flex items-center gap-2 rounded-md p-2 hover:bg-muted",
hasFlows && "pointer-events-none text-muted-foreground",
)}
data-testid="create_flow_btn_get_started"
>
<span data-testid="create_flow_icon_get_started">
<IconComponent
name={hasFlows ? "Check" : "Plus"}
className={cn(
"h-4 w-4 text-primary",
hasFlows && "text-accent-emerald-foreground",
)}
/>
</span>
<span className={cn("text-sm", hasFlows && "line-through")}>
Create a flow
</span>
</div>
</Button>
</div>
<ModalsComponent
openModal={newProjectModal}
setOpenModal={setNewProjectModal}
openDeleteFolderModal={false}
setOpenDeleteFolderModal={() => {}}
handleDeleteFolder={() => {}}
/>
</div>
);
};

View file

@ -1,5 +1,9 @@
import IconComponent from "@/components/common/genericIconComponent"; import IconComponent from "@/components/common/genericIconComponent";
import { GetStartedProgress } from "@/components/core/folderSidebarComponent/components/sideBarFolderButtons/components/get-started-progress";
import { SidebarTrigger } from "@/components/ui/sidebar"; import { SidebarTrigger } from "@/components/ui/sidebar";
import useAuthStore from "@/stores/authStore";
import { Separator } from "@radix-ui/react-separator";
import { useState } from "react";
import { AddFolderButton } from "./add-folder-button"; import { AddFolderButton } from "./add-folder-button";
import { UploadFolderButton } from "./upload-folder-button"; import { UploadFolderButton } from "./upload-folder-button";
@ -13,23 +17,54 @@ export const HeaderButtons = ({
isUpdatingFolder: boolean; isUpdatingFolder: boolean;
isPending: boolean; isPending: boolean;
addNewFolder: () => void; addNewFolder: () => void;
}) => ( }) => {
<div className="flex shrink-0 items-center justify-between gap-2"> const userData = useAuthStore((state) => state.userData);
<SidebarTrigger className="lg:hidden"> const userDismissedDialog = userData?.optins?.dialog_dismissed;
<IconComponent name="PanelLeftClose" className="h-4 w-4" /> const isGithubStarred = userData?.optins?.github_starred;
</SidebarTrigger> const isDiscordJoined = userData?.optins?.discord_clicked;
<div className="flex-1 text-sm font-semibold">Folders</div> const [isDismissedDialog, setIsDismissedDialog] =
<div className="flex items-center gap-1"> useState(userDismissedDialog);
<UploadFolderButton
onClick={handleUploadFlowsToFolder} const handleDismissDialog = () => {
disabled={isUpdatingFolder} setIsDismissedDialog(true);
/> };
<AddFolderButton
onClick={addNewFolder} return (
disabled={isUpdatingFolder} <>
loading={isPending} {!isDismissedDialog && (
/> <>
</div> <GetStartedProgress
</div> userData={userData!}
); isGithubStarred={isGithubStarred ?? false}
isDiscordJoined={isDiscordJoined ?? false}
handleDismissDialog={handleDismissDialog}
/>
<div className="-mx-4 mt-4 w-[280px]">
<hr className="border-t-1 w-full" />
</div>
</>
)}
<div className="flex shrink-0 items-center justify-between gap-2 pt-2">
<SidebarTrigger className="lg:hidden">
<IconComponent name="PanelLeftClose" className="h-4 w-4" />
</SidebarTrigger>
<div className="flex-1 text-sm font-semibold">Folders</div>
<div className="flex items-center gap-1">
<UploadFolderButton
onClick={handleUploadFlowsToFolder}
disabled={isUpdatingFolder}
/>
<AddFolderButton
onClick={addNewFolder}
disabled={isUpdatingFolder}
loading={isPending}
/>
</div>
</div>
</>
);
};

View file

@ -0,0 +1,99 @@
import { cn } from "@/utils/utils";
import { motion } from "framer-motion";
import React from "react";
export const BackgroundGradient = ({
children,
className,
containerClassName,
animate = true,
borderColor,
borderRadius,
}: {
children?: React.ReactNode;
className?: string;
containerClassName?: string;
animate?: boolean;
borderColor?: string;
borderRadius?: string;
}) => {
const variants = {
initial: {
backgroundPosition: "0 50%",
},
animate: {
backgroundPosition: ["0, 50%", "100% 50%", "0 50%"],
},
};
const defaultGradient =
"radial-gradient(circle farthest-side at 0 100%,#00ccb1,transparent),radial-gradient(circle farthest-side at 100% 0,#7b61ff,transparent),radial-gradient(circle farthest-side at 100% 100%,#ffc414,transparent),radial-gradient(circle farthest-side at 0 0,#1ca0fb,#141316)";
return (
<div className={cn("group relative p-[1px]", containerClassName)}>
<motion.div
variants={animate ? variants : undefined}
initial={animate ? "initial" : undefined}
animate={animate ? "animate" : undefined}
transition={
animate
? {
duration: 5,
repeat: Infinity,
repeatType: "reverse",
}
: undefined
}
style={{
backgroundSize: animate ? "400% 400%" : undefined,
background: borderColor || defaultGradient,
position: "absolute",
inset: 0,
borderRadius: "24px",
opacity: 0.2,
filter: "blur(8px)",
transition: "all 0.4s ease-in-out",
willChange: "transform",
}}
className="group-hover:filter-[blur(15px)] group-hover:opacity-70 group-hover:brightness-125"
/>
<motion.div
variants={animate ? variants : undefined}
initial={animate ? "initial" : undefined}
animate={animate ? "animate" : undefined}
transition={
animate
? {
duration: 5,
repeat: Infinity,
repeatType: "reverse",
}
: undefined
}
style={
{
backgroundSize: animate ? "400% 400%" : undefined,
background: borderColor || defaultGradient,
position: "absolute",
inset: 0,
borderRadius: borderRadius || "24px",
willChange: "transform",
transition: "all 0.4s ease-in-out",
"--border-color": borderColor,
"--border-color-transparent": borderColor
? `${borderColor}20`
: undefined,
} as React.CSSProperties
}
className={cn(
"group-hover:brightness-125",
borderColor
? "group-hover:shadow-[0_0_15px_3px_var(--border-color),0_0_25px_5px_var(--border-color-transparent)]"
: "group-hover:shadow-[0_0_15px_3px_rgba(0,204,177,0.3),0_0_25px_5px_rgba(123,97,255,0.2)]",
)}
/>
<div className={cn("relative z-10", className)}>{children}</div>
</div>
);
};

View file

@ -0,0 +1,36 @@
import { cn } from "@/utils/utils";
export function DotBackgroundDemo({
children,
className,
containerClassName,
}: {
children: React.ReactNode;
className?: string;
containerClassName?: string;
}) {
return (
<div
className={cn(
"relative flex h-full w-full items-center justify-center bg-white dark:bg-black",
className,
)}
>
<div
className={cn(
"absolute inset-0",
"[background-size:20px_20px]",
"[background-image:radial-gradient(#d4d4d4_1px,transparent_1px)]",
"dark:[background-image:radial-gradient(#404040_1px,transparent_1px)]",
)}
/>
<div
className={cn(
"absolute inset-0 bg-gradient-to-b from-white/10 via-white/50 to-white dark:from-background/30 dark:via-background/80 dark:to-background",
containerClassName,
)}
/>
{children}
</div>
);
}

View file

@ -1005,6 +1005,7 @@ export const GRADIENT_CLASS_DISABLED =
"linear-gradient(to right, hsl(var(--muted) / 0.3), hsl(var(--muted)))"; "linear-gradient(to right, hsl(var(--muted) / 0.3), hsl(var(--muted)))";
export const RECEIVING_INPUT_VALUE = "Receiving input"; export const RECEIVING_INPUT_VALUE = "Receiving input";
export const SELECT_AN_OPTION = "Select an option...";
export const ICON_STROKE_WIDTH = 1.5; export const ICON_STROKE_WIDTH = 1.5;
@ -1068,5 +1069,9 @@ export const OPENAI_VOICES = [
export const DEFAULT_POLLING_INTERVAL = 5000; export const DEFAULT_POLLING_INTERVAL = 5000;
export const DEFAULT_TIMEOUT = 30000; export const DEFAULT_TIMEOUT = 30000;
export const DEFAULT_FILE_PICKER_TIMEOUT = 60000; export const DEFAULT_FILE_PICKER_TIMEOUT = 60000;
export const DISCORD_URL = "https://discord.com/invite/EqksyE2EX9";
export const GITHUB_URL = "https://github.com/langflow-ai/langflow";
export const TWITTER_URL = "https://x.com/langflow_ai";
export const DOCS_URL = "https://docs.langflow.org";
export const UUID_PARSING_ERROR = "uuid_parsing"; export const UUID_PARSING_ERROR = "uuid_parsing";

View file

@ -10,6 +10,8 @@ import { FlowStyleType, FlowType } from "../../types/flow";
import { StoreComponentResponse } from "../../types/store"; import { StoreComponentResponse } from "../../types/store";
const GITHUB_API_URL = "https://api.github.com"; const GITHUB_API_URL = "https://api.github.com";
const DISCORD_API_URL =
"https://discord.com/api/v9/invites/EqksyE2EX9?with_counts=true";
export async function getRepoStars(owner: string, repo: string) { export async function getRepoStars(owner: string, repo: string) {
try { try {
@ -21,6 +23,15 @@ export async function getRepoStars(owner: string, repo: string) {
} }
} }
export async function getDiscordCount() {
try {
const response = await api.get(DISCORD_API_URL);
return response?.data.approximate_member_count;
} catch (error) {
console.error("Error fetching repository data:", error);
return null;
}
}
export async function createApiKey(name: string) { export async function createApiKey(name: string) {
try { try {
const res = await api.post(`${BASE_URL_API}api_key/`, { name }); const res = await api.post(`${BASE_URL_API}api_key/`, { name });

View file

@ -1,16 +1,18 @@
import useAuthStore from "@/stores/authStore";
import { UseMutationResult } from "@tanstack/react-query"; import { UseMutationResult } from "@tanstack/react-query";
import { useMutationFunctionType, Users } from "../../../../types/api"; import { useMutationFunctionType, Users } from "../../../../types/api";
import { api } from "../../api"; import { api } from "../../api";
import { getURL } from "../../helpers/constants"; import { getURL } from "../../helpers/constants";
import { UseRequestProcessor } from "../../services/request-processor"; import { UseRequestProcessor } from "../../services/request-processor";
export const useGetUserData: useMutationFunctionType<undefined, any> = ( export const useGetUserData: useMutationFunctionType<undefined, any> = (
options?, options?,
) => { ) => {
const setUserData = useAuthStore((state) => state.setUserData);
const { mutate } = UseRequestProcessor(); const { mutate } = UseRequestProcessor();
const getUserData = async () => { const getUserData = async () => {
const response = await api.get<Users>(`${getURL("USERS")}/whoami`); const response = await api.get<Users>(`${getURL("USERS")}/whoami`);
setUserData(response["data"]);
return response["data"]; return response["data"];
}; };

View file

@ -7,6 +7,7 @@ import { UseRequestProcessor } from "../../services/request-processor";
interface versionQueryResponse { interface versionQueryResponse {
version: string; version: string;
package: string; package: string;
main_version: string;
} }
export const useGetVersionQuery: useQueryFunctionType< export const useGetVersionQuery: useQueryFunctionType<
@ -22,7 +23,9 @@ export const useGetVersionQuery: useQueryFunctionType<
const responseFn = async () => { const responseFn = async () => {
const { data } = await getVersionFn(); const { data } = await getVersionFn();
const refreshVersion = useDarkStore.getState().refreshVersion; const refreshVersion = useDarkStore.getState().refreshVersion;
const refreshLatestVersion = useDarkStore.getState().refreshLatestVersion;
refreshVersion(data.version); refreshVersion(data.version);
refreshLatestVersion(data.main_version);
return data; return data;
}; };

View file

@ -0,0 +1,46 @@
const TwitterXSVG = (props) => {
return props.isdark === "true" ? (
<svg
{...props}
xmlns="http://www.w3.org/2000/svg"
viewBox="0,0,256,256"
width="30px"
height="30px"
fillRule="nonzero"
>
<g
fill="#ffffff"
fillRule="nonzero"
stroke="none"
strokeWidth="1"
strokeLinecap="butt"
strokeLinejoin="miter"
strokeMiterlimit="10"
strokeDasharray=""
strokeDashoffset="0"
fontFamily="none"
fontWeight="none"
fontSize="none"
textAnchor="none"
style={{ mixBlendMode: "normal" }}
>
<g transform="scale(8.53333,8.53333)">
<path d="M26.37,26l-8.795,-12.822l0.015,0.012l7.93,-9.19h-2.65l-6.46,7.48l-5.13,-7.48h-6.95l8.211,11.971l-0.001,-0.001l-8.66,10.03h2.65l7.182,-8.322l5.708,8.322zM10.23,6l12.34,18h-2.1l-12.35,-18z"></path>
</g>
</g>
</svg>
) : (
<svg
xmlns="http://www.w3.org/2000/svg"
{...props}
viewBox="0 0 30 30"
width="30px"
height="30px"
fill="#000000"
>
<path d="M26.37,26l-8.795-12.822l0.015,0.012L25.52,4h-2.65l-6.46,7.48L11.28,4H4.33l8.211,11.971L12.54,15.97L3.88,26h2.65 l7.182-8.322L19.42,26H26.37z M10.23,6l12.34,18h-2.1L8.12,6H10.23z" />
</svg>
);
};
export default TwitterXSVG;

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0,0,256,256" width="30px" height="30px" fill-rule="nonzero"><g fill="#ffffff" fill-rule="nonzero" stroke="none" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="10" stroke-dasharray="" stroke-dashoffset="0" font-family="none" font-weight="none" font-size="none" text-anchor="none" style="mix-blend-mode: normal"><g transform="scale(8.53333,8.53333)"><path d="M26.37,26l-8.795,-12.822l0.015,0.012l7.93,-9.19h-2.65l-6.46,7.48l-5.13,-7.48h-6.95l8.211,11.971l-0.001,-0.001l-8.66,10.03h2.65l7.182,-8.322l5.708,8.322zM10.23,6l12.34,18h-2.1l-12.35,-18z"></path></g></g></svg>

After

Width:  |  Height:  |  Size: 693 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30" width="30px" height="30px"><path d="M26.37,26l-8.795-12.822l0.015,0.012L25.52,4h-2.65l-6.46,7.48L11.28,4H4.33l8.211,11.971L12.54,15.97L3.88,26h2.65 l7.182-8.322L19.42,26H26.37z M10.23,6l12.34,18h-2.1L8.12,6H10.23z"/></svg>

After

Width:  |  Height:  |  Size: 283 B

View file

@ -0,0 +1,11 @@
import { useDarkStore } from "@/stores/darkStore";
import React, { forwardRef } from "react";
import TwitterXSVG from "./TwitterX.jsx";
export const TwitterXIcon = forwardRef<
SVGSVGElement,
React.PropsWithChildren<{}>
>((props, ref) => {
const isdark = useDarkStore((state) => state.dark).toString();
return <TwitterXSVG ref={ref} isdark={isdark} {...props} />;
});

View file

@ -231,6 +231,7 @@ import {
ZoomIn, ZoomIn,
ZoomOut, ZoomOut,
} from "lucide-react"; } from "lucide-react";
import { TwitterXIcon } from "./Twitter X";
// Create a map of eagerly loaded Lucide icons // Create a map of eagerly loaded Lucide icons
export const lucideIcons = { export const lucideIcons = {
@ -464,4 +465,5 @@ export const lucideIcons = {
Zap, Zap,
ZoomIn, ZoomIn,
ZoomOut, ZoomOut,
TwitterXIcon,
}; };

View file

@ -61,6 +61,7 @@ export default function DeleteConfirmationModal({
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
className="mr-1" className="mr-1"
variant="outline" variant="outline"
data-testid="btn_cancel_delete_confirmation_modal"
> >
Cancel Cancel
</Button> </Button>
@ -72,6 +73,7 @@ export default function DeleteConfirmationModal({
onClick={(e) => { onClick={(e) => {
onConfirm(e); onConfirm(e);
}} }}
data-testid="btn_delete_delete_confirmation_modal"
> >
Delete Delete
</Button> </Button>

View file

@ -15,6 +15,9 @@ import { LoadingPage } from "../LoadingPage";
export function AppInitPage() { export function AppInitPage() {
const refreshStars = useDarkStore((state) => state.refreshStars); const refreshStars = useDarkStore((state) => state.refreshStars);
const refreshDiscordCount = useDarkStore(
(state) => state.refreshDiscordCount,
);
const isLoading = useFlowsManagerStore((state) => state.isLoading); const isLoading = useFlowsManagerStore((state) => state.isLoading);
const { isFetched: isLoaded } = useCustomPrimaryLoading(); const { isFetched: isLoaded } = useCustomPrimaryLoading();
@ -31,6 +34,7 @@ export function AppInitPage() {
useEffect(() => { useEffect(() => {
if (isFetched) { if (isFetched) {
refreshStars(); refreshStars();
refreshDiscordCount();
} }
if (isConfigFetched) { if (isConfigFetched) {

View file

@ -93,6 +93,7 @@ const DropdownComponent = ({
setOpenDelete(true); setOpenDelete(true);
}} }}
className="cursor-pointer text-destructive" className="cursor-pointer text-destructive"
data-testid="btn_delete_dropdown_menu"
> >
<ForwardedIconComponent <ForwardedIconComponent
name="Trash2" name="Trash2"

View file

@ -0,0 +1,190 @@
import LangflowLogo from "@/assets/LangflowLogo.svg?react";
import logoDarkPng from "@/assets/logo_dark.png";
import logoLightPng from "@/assets/logo_light.png";
import CardsWrapComponent from "@/components/core/cardsWrapComponent";
import { Button } from "@/components/ui/button";
import { DotBackgroundDemo } from "@/components/ui/dot-background";
import { DISCORD_URL, GITHUB_URL } from "@/constants/constants";
import { useGetUserData, useUpdateUser } from "@/controllers/API/queries/auth";
import { EnhancedBeamEffect } from "@/pages/MainPage/pages/enchanced-beam-effect";
import useAuthStore from "@/stores/authStore";
import { useDarkStore } from "@/stores/darkStore";
import { useFolderStore } from "@/stores/foldersStore";
import { formatNumber } from "@/utils/utils";
import { ExternalLink } from "lucide-react";
import { FaDiscord, FaGithub } from "react-icons/fa";
import { HiArrowRight } from "react-icons/hi";
import { useShallow } from "zustand/react/shallow";
import useFileDrop from "../hooks/use-on-file-drop";
const EMPTY_PAGE_TITLE = "Your new favorite way to ship Agents";
const EMPTY_PAGE_DESCRIPTION =
"Design agents that connect to any API, model, or database.";
const EMPTY_PAGE_GITHUB_DESCRIPTION =
"Follow development, star the repo, and shape the future.";
const EMPTY_PAGE_DISCORD_DESCRIPTION =
"Join builders, ask questions, and show off your agents.";
const EMPTY_PAGE_DRAG_AND_DROP_TEXT =
"Already have a flow? Drag and drop to upload.";
const EMPTY_PAGE_FOLDER_DESCRIPTION = "Empty folder";
const EMPTY_PAGE_CREATE_FIRST_FLOW_BUTTON_TEXT = <>+&nbsp; Create first flow</>;
const EXTERNAL_LINK_ICON_CLASS =
"absolute right-6 top-[35px] h-4 w-4 shrink-0 translate-x-0 opacity-0 transition-all duration-300 group-hover:translate-x-1 group-hover:opacity-100";
export const EmptyPageCommunity = ({
setOpenModal,
}: {
setOpenModal: (open: boolean) => void;
}) => {
const handleFileDrop = useFileDrop(undefined);
const folders = useFolderStore((state) => state.folders);
const userData = useAuthStore(useShallow((state) => state.userData));
const stars: number | undefined = useDarkStore((state) => state.stars);
const discordCount: number = useDarkStore((state) => state.discordCount);
const { mutate: updateUser } = useUpdateUser();
const { mutate: mutateLoggedUser } = useGetUserData();
const handleUserTrack = (key: string) => () => {
const optins = userData?.optins ?? {};
optins[key] = true;
updateUser(
{
user_id: userData?.id!,
user: { optins },
},
{
onSuccess: () => {
mutateLoggedUser({});
},
},
);
};
return (
<DotBackgroundDemo>
<CardsWrapComponent
dragMessage={`Drop your flows or components here`}
onFileDrop={handleFileDrop}
>
<div className="m-0 h-full w-full bg-background p-0">
<div className="z-50 flex h-full w-full flex-col items-center justify-center gap-6">
<div className="z-50 flex flex-col items-center gap-3">
<div className="z-50 dark:hidden">
<img
src={logoLightPng}
alt="Langflow Logo Light"
data-testid="empty_page_logo_light"
className=""
/>
</div>
<div className="z-50 hidden dark:block">
<img
src={logoDarkPng}
alt="Langflow Logo Dark"
data-testid="empty_page_logo_dark"
className=""
/>
</div>
<span
data-testid="mainpage_title"
className="z-50 text-center text-2xl font-semibold text-foreground"
>
{EMPTY_PAGE_TITLE}
</span>
<span
data-testid="empty_page_description"
className="z-50 text-center text-[14px] text-secondary-foreground"
>
{folders?.length > 1
? EMPTY_PAGE_FOLDER_DESCRIPTION
: EMPTY_PAGE_DESCRIPTION}
</span>
</div>
<div className="flex w-full max-w-[510px] flex-col gap-12 sm:gap-8">
<Button
unstyled
className="group mx-3 h-[84px] sm:mx-0"
onClick={() => {
handleUserTrack("github_starred")();
window.open(GITHUB_URL, "_blank", "noopener,noreferrer");
}}
data-testid="empty_page_github_button"
>
<div className="relative flex flex-col rounded-lg border-[1px] bg-background p-4 transition-all duration-300 hover:-translate-y-0.5 hover:border-accent-pink-foreground hover:shadow-[0_4px_10px_rgba(198,97,184,0.25)]">
<div className="grid w-full items-center justify-between gap-2">
<div className="flex gap-3">
<FaGithub className="h-6 w-6" />
<div>
<span className="font-semibold">GitHub</span>
<span className="ml-2 font-mono text-muted-foreground">
{formatNumber(stars)}
</span>
</div>
</div>
<div>
<span className="text-sm text-secondary-foreground">
{EMPTY_PAGE_GITHUB_DESCRIPTION}
</span>
</div>
</div>
<ExternalLink className={EXTERNAL_LINK_ICON_CLASS} />
</div>
</Button>
<Button
unstyled
className="group mx-3 h-[84px] sm:mx-0"
onClick={() => {
handleUserTrack("discord_joined")();
window.open(DISCORD_URL, "_blank", "noopener,noreferrer");
}}
data-testid="empty_page_discord_button"
>
<div className="relative flex flex-col rounded-lg border-[1px] bg-background p-4 transition-all duration-300 hover:-translate-y-0.5 hover:border-discord-color hover:shadow-[0_4px_10px_rgba(88,101,242,0.25)]">
<div className="grid w-full items-center justify-between gap-2">
<div className="flex gap-3">
<FaDiscord className="h-6 w-6 text-discord-color" />
<div>
<span className="font-semibold">Discord</span>
<span className="ml-2 font-mono text-muted-foreground">
{formatNumber(discordCount)}
</span>
</div>
</div>
<div>
<span className="text-sm text-secondary-foreground">
{EMPTY_PAGE_DISCORD_DESCRIPTION}
</span>
</div>
</div>
<ExternalLink className={EXTERNAL_LINK_ICON_CLASS} />
</div>
</Button>
<Button
variant="default"
className="z-10 m-auto h-10 w-full max-w-[155px] rounded-lg font-bold transition-all duration-300"
onClick={() => setOpenModal(true)}
id="new-project-btn"
data-testid="new_project_btn_empty_page"
>
<span>{EMPTY_PAGE_CREATE_FIRST_FLOW_BUTTON_TEXT}</span>
</Button>
</div>
</div>
</div>
<p
data-testid="empty_page_drag_and_drop_text"
className="absolute bottom-5 left-0 right-0 mt-4 cursor-default text-center text-sm text-muted-foreground"
>
{EMPTY_PAGE_DRAG_AND_DROP_TEXT}
</p>
</CardsWrapComponent>
</DotBackgroundDemo>
);
};
export default EmptyPageCommunity;

View file

@ -0,0 +1,43 @@
import { cn } from "@/utils/utils";
import { ReactNode } from "react";
import { BorderBeam } from "../../../components/ui/border-beams";
interface EnhancedBeamEffectProps {
children: ReactNode;
className?: string;
primaryColor?: string;
secondaryColor?: string;
size?: number;
}
export const EnhancedBeamEffect = ({
children,
className,
primaryColor = "#C661B8",
secondaryColor = "#61C6B8",
size = 200,
}: EnhancedBeamEffectProps) => {
return (
<div
className={cn(
"relative flex items-center justify-center overflow-hidden rounded-xl",
className,
)}
>
{children}
{/* Primary beam - larger, slower rotation */}
<BorderBeam
duration={12}
size={size}
className="opacity-80"
colorFrom={primaryColor}
colorTo={secondaryColor}
anchor={20}
borderWidth={1.5}
/>
</div>
);
};
export default EnhancedBeamEffect;

View file

@ -10,6 +10,7 @@ import { useQueryClient } from "@tanstack/react-query";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Outlet } from "react-router-dom"; import { Outlet } from "react-router-dom";
import ModalsComponent from "../components/modalsComponent"; import ModalsComponent from "../components/modalsComponent";
import EmptyPageCommunity from "./empty-page";
import EmptyPage from "./emptyPage"; import EmptyPage from "./emptyPage";
export default function CollectionPage(): JSX.Element { export default function CollectionPage(): JSX.Element {
@ -80,7 +81,9 @@ export default function CollectionPage(): JSX.Element {
{flows?.length !== examples?.length || folders?.length > 1 ? ( {flows?.length !== examples?.length || folders?.length > 1 ? (
<Outlet /> <Outlet />
) : ( ) : (
<EmptyPage setOpenModal={setOpenModal} /> // <EmptyPage setOpenModal={setOpenModal} />
<EmptyPageCommunity setOpenModal={setOpenModal} />
)} )}
</div> </div>
) : ( ) : (

View file

@ -23,9 +23,9 @@ import { AppWrapperPage } from "./pages/AppWrapperPage";
import { DashboardWrapperPage } from "./pages/DashboardWrapperPage"; import { DashboardWrapperPage } from "./pages/DashboardWrapperPage";
import FlowPage from "./pages/FlowPage"; import FlowPage from "./pages/FlowPage";
import LoginPage from "./pages/LoginPage"; import LoginPage from "./pages/LoginPage";
import CollectionPage from "./pages/MainPage/pages";
import FilesPage from "./pages/MainPage/pages/filesPage"; import FilesPage from "./pages/MainPage/pages/filesPage";
import HomePage from "./pages/MainPage/pages/homePage"; import HomePage from "./pages/MainPage/pages/homePage";
import CollectionPage from "./pages/MainPage/pages/main-page";
import SettingsPage from "./pages/SettingsPage"; import SettingsPage from "./pages/SettingsPage";
import ApiKeysPage from "./pages/SettingsPage/pages/ApiKeysPage"; import ApiKeysPage from "./pages/SettingsPage/pages/ApiKeysPage";
import GeneralPage from "./pages/SettingsPage/pages/GeneralPage"; import GeneralPage from "./pages/SettingsPage/pages/GeneralPage";

View file

@ -1,5 +1,5 @@
import { create } from "zustand"; import { create } from "zustand";
import { getRepoStars } from "../controllers/API"; import { getDiscordCount, getRepoStars } from "../controllers/API";
import { DarkStoreType } from "../types/zustand/dark"; import { DarkStoreType } from "../types/zustand/dark";
const startedStars = Number(window.localStorage.getItem("githubStars")) ?? 0; const startedStars = Number(window.localStorage.getItem("githubStars")) ?? 0;
@ -8,6 +8,10 @@ export const useDarkStore = create<DarkStoreType>((set, get) => ({
dark: JSON.parse(window.localStorage.getItem("isDark")!) ?? false, dark: JSON.parse(window.localStorage.getItem("isDark")!) ?? false,
stars: startedStars, stars: startedStars,
version: "", version: "",
latestVersion: "",
refreshLatestVersion: (v: string) => {
set(() => ({ latestVersion: v }));
},
setDark: (dark) => { setDark: (dark) => {
set(() => ({ dark: dark })); set(() => ({ dark: dark }));
window.localStorage.setItem("isDark", dark.toString()); window.localStorage.setItem("isDark", dark.toString());
@ -40,4 +44,10 @@ export const useDarkStore = create<DarkStoreType>((set, get) => ({
}); });
} }
}, },
discordCount: 0,
refreshDiscordCount: () => {
getDiscordCount().then((res) => {
set(() => ({ discordCount: res }));
});
},
})); }));

View file

@ -69,6 +69,7 @@
--round-btn-shadow: #00000063; --round-btn-shadow: #00000063;
--ice: #31a3cc; --ice: #31a3cc;
--selected: #2196f3; --selected: #2196f3;
--discord-color: #5765f2;
--error-background: #fef2f2; --error-background: #fef2f2;
--error-foreground: #991b1b; --error-foreground: #991b1b;
@ -194,6 +195,7 @@
--accent-amber: 48, 96%, 89%; --accent-amber: 48, 96%, 89%;
--accent-amber-foreground: 26 90.5% 37.1%; --accent-amber-foreground: 26 90.5% 37.1%;
--red-foreground: 0 90.6% 70.8%; --red-foreground: 0 90.6% 70.8%;
--indigo-foreground: 243.7 54.5% 41.4%;
} }
.dark { .dark {
@ -344,6 +346,8 @@
--hover: #1a202e; --hover: #1a202e;
--disabled-run: #6366f1; --disabled-run: #6366f1;
--selected: #0369a1; --selected: #0369a1;
--discord-color: #5765f2;
--filter-foreground: #eef2ff; --filter-foreground: #eef2ff;
--filter-background: #4e46e599; --filter-background: #4e46e599;
@ -450,5 +454,6 @@
--accent-amber: 22, 78%, 26%; --accent-amber: 22, 78%, 26%;
--accent-amber-foreground: 45.9 96.7% 64.5%; --accent-amber-foreground: 45.9 96.7% 64.5%;
--red-foreground: 0 72.2% 50.6%; --red-foreground: 0 72.2% 50.6%;
--indigo-foreground: 234.5 89.5% 73.9%;
} }
} }

View file

@ -157,6 +157,11 @@ export type changeUser = {
is_superuser?: boolean; is_superuser?: boolean;
password?: string; password?: string;
profile_image?: string; profile_image?: string;
optins?: {
github_starred?: boolean;
discord_clicked?: boolean;
dialog_dismissed?: boolean;
};
}; };
export type resetPasswordType = { export type resetPasswordType = {
@ -172,6 +177,11 @@ export type Users = {
profile_image: string; profile_image: string;
create_at: Date; create_at: Date;
updated_at: Date; updated_at: Date;
optins?: {
github_starred?: boolean;
discord_clicked?: boolean;
dialog_dismissed?: boolean;
};
}; };
export type Component = { export type Component = {

View file

@ -2,7 +2,11 @@ export type DarkStoreType = {
dark: boolean; dark: boolean;
stars: number; stars: number;
version: string; version: string;
latestVersion: string;
setDark: (dark: boolean) => void; setDark: (dark: boolean) => void;
refreshVersion: (v: string) => void; refreshVersion: (v: string) => void;
refreshLatestVersion: (v: string) => void;
refreshStars: () => void; refreshStars: () => void;
discordCount: number;
refreshDiscordCount: () => void;
}; };

View file

@ -421,6 +421,7 @@ export const nodeIconToDisplayIconMap: Record<string, string> = {
ScrapeGraphMarkdownifyApi: "ScrapeGraph", ScrapeGraphMarkdownifyApi: "ScrapeGraph",
Unlink: "UnlinkIcon", Unlink: "UnlinkIcon",
note: "StickyNote", note: "StickyNote",
TwitterXIcon: "TwitterXIcon",
}; };
export const getLucideIconName = (name: string): string => { export const getLucideIconName = (name: string): string => {

View file

@ -872,3 +872,15 @@ export const convertUTCToLocalTimezone = (timestamp: string) => {
const localTimezone = moment.tz.guess(); const localTimezone = moment.tz.guess();
return moment.utc(timestamp).tz(localTimezone).format("MM/DD/YYYY HH:mm:ss"); return moment.utc(timestamp).tz(localTimezone).format("MM/DD/YYYY HH:mm:ss");
}; };
export const formatNumber = (num: number | undefined): string => {
if (num === undefined) return "0";
if (num >= 1000000) {
return (num / 1000000).toFixed(0) + "M";
}
if (num >= 1000) {
return (num / 1000).toFixed(0) + "k";
}
return num?.toString();
};

View file

@ -309,6 +309,8 @@ const config = {
"slider-input-border": "var(--slider-input-border)", "slider-input-border": "var(--slider-input-border)",
"zinc-foreground": "hsl(var(--zinc-foreground))", "zinc-foreground": "hsl(var(--zinc-foreground))",
"red-foreground": "hsl(var(--red-foreground))", "red-foreground": "hsl(var(--red-foreground))",
"indigo-foreground": "hsl(var(--indigo-foreground))",
"discord-color": "var(--discord-color)",
}, },
borderRadius: { borderRadius: {
lg: `var(--radius)`, lg: `var(--radius)`,

View file

@ -0,0 +1,242 @@
import { expect, test } from "@playwright/test";
import { DISCORD_URL, GITHUB_URL } from "../../../src/constants/constants";
import { addNewUserAndLogin } from "../../utils/add-new-user-and-loggin";
import { awaitBootstrapTest } from "../../utils/await-bootstrap-test";
test(
"admin user must be able to track their progress in getting started",
{ tag: ["@release", "@api"] },
async ({ page, context }) => {
await page.goto("/");
// Wait for any loading text to disappear
await page.waitForSelector('text="Loading"', {
state: "hidden",
timeout: 30000,
});
await page.waitForTimeout(2000);
let emptyButton = page.getByTestId("new_project_btn_empty_page");
while ((await emptyButton.count()) === 0) {
await page.getByTestId("home-dropdown-menu").first().click();
await page.getByTestId("btn_delete_dropdown_menu").first().click();
await page
.getByTestId("btn_delete_delete_confirmation_modal")
.first()
.click();
await page.waitForTimeout(1000);
emptyButton = page.getByTestId("new_project_btn_empty_page");
}
await expect(emptyButton).toBeVisible();
await expect(page.getByTestId("mainpage_title")).toBeVisible();
await expect(page.getByTestId("empty_page_description")).toBeVisible();
await expect(page.getByTestId("empty_page_github_button")).toBeVisible();
await expect(page.getByTestId("empty_page_discord_button")).toBeVisible();
await expect(
page.getByTestId("empty_page_drag_and_drop_text"),
).toBeVisible();
await expect(page.getByTestId("app-header")).not.toBeVisible();
await page.getByTestId("empty_page_github_button").click();
const pagePromiseGithub = context.waitForEvent("page");
const newPageGithub = await pagePromiseGithub;
await newPageGithub.waitForTimeout(3000);
const newUrlGithub = newPageGithub.url();
await expect(newUrlGithub).toContain(GITHUB_URL);
await newPageGithub.close();
await expect(page.getByTestId("mainpage_title")).toBeVisible();
await expect(page.getByTestId("empty_page_description")).toBeVisible();
await page.getByTestId("new_project_btn_empty_page").click();
await page.getByTestId("side_nav_options_all-templates").click();
await page.getByRole("heading", { name: "Basic Prompting" }).click();
await page.waitForSelector('[data-testid="icon-ChevronLeft"]', {
timeout: 100000,
});
await page.getByTestId("icon-ChevronLeft").first().click();
await page.waitForSelector('[data-testid="home-dropdown-menu"]', {
timeout: 100000,
});
await expect(page.getByTestId("app-header")).toBeVisible();
await expect(
page.getByTestId("github_starred_icon_get_started"),
).toBeVisible();
await expect(
page.getByTestId("create_flow_icon_get_started"),
).toBeVisible();
await expect(
page.getByTestId("discord_joined_icon_get_started"),
).not.toBeVisible();
await expect(
page.getByTestId("get_started_progress_percentage").first(),
).toHaveText("66%");
await page.getByTestId("discord_joined_btn_get_started").click();
const pagePromiseDiscord = context.waitForEvent("page");
const newPageDiscord = await pagePromiseDiscord;
await newPageDiscord.waitForTimeout(3000);
const newUrlDiscord = newPageDiscord.url();
await expect(newUrlDiscord).toContain(DISCORD_URL);
await newPageDiscord.close();
await expect(page.getByTestId("app-header")).toBeVisible();
await expect(
page.getByTestId("discord_joined_icon_get_started"),
).toBeVisible();
await expect(
page.getByTestId("create_flow_icon_get_started"),
).toBeVisible();
await expect(
page.getByTestId("github_starred_icon_get_started"),
).toBeVisible();
await expect(
page.getByTestId("get_started_progress_percentage").first(),
).toHaveText("100%");
await page.getByTestId("close_get_started_dialog").click();
await expect(
page.getByTestId("discord_joined_icon_get_started"),
).not.toBeVisible();
await expect(
page.getByTestId("create_flow_icon_get_started"),
).not.toBeVisible();
await expect(
page.getByTestId("github_starred_icon_get_started"),
).not.toBeVisible();
},
);
test(
"normal user must be able to track their progress in getting started",
{ tag: ["@release", "@api"] },
async ({ page, context }) => {
await addNewUserAndLogin(page);
// Wait for any loading text to disappear
await page.waitForSelector('text="Loading"', {
state: "hidden",
timeout: 30000,
});
await page.waitForTimeout(2000);
let emptyButton = page.getByTestId("new_project_btn_empty_page");
while ((await emptyButton.count()) === 0) {
await page.getByTestId("home-dropdown-menu").first().click();
await page.getByTestId("btn_delete_dropdown_menu").first().click();
await page
.getByTestId("btn_delete_delete_confirmation_modal")
.first()
.click();
await page.waitForTimeout(1000);
emptyButton = page.getByTestId("new_project_btn_empty_page");
}
await expect(emptyButton).toBeVisible();
await expect(page.getByTestId("mainpage_title")).toBeVisible();
await expect(page.getByTestId("empty_page_description")).toBeVisible();
await expect(page.getByTestId("empty_page_github_button")).toBeVisible();
await expect(page.getByTestId("empty_page_discord_button")).toBeVisible();
await expect(
page.getByTestId("empty_page_drag_and_drop_text"),
).toBeVisible();
await expect(page.getByTestId("app-header")).not.toBeVisible();
await page.getByTestId("empty_page_github_button").click();
const pagePromiseGithub = context.waitForEvent("page");
const newPageGithub = await pagePromiseGithub;
await newPageGithub.waitForTimeout(3000);
const newUrlGithub = newPageGithub.url();
await expect(newUrlGithub).toContain(GITHUB_URL);
await newPageGithub.close();
await expect(page.getByTestId("mainpage_title")).toBeVisible();
await expect(page.getByTestId("empty_page_description")).toBeVisible();
await page.getByTestId("new_project_btn_empty_page").click();
await page.getByTestId("side_nav_options_all-templates").click();
await page.getByRole("heading", { name: "Basic Prompting" }).click();
await page.waitForSelector('[data-testid="icon-ChevronLeft"]', {
timeout: 100000,
});
await page.getByTestId("icon-ChevronLeft").first().click();
await page.waitForSelector('[data-testid="home-dropdown-menu"]', {
timeout: 100000,
});
await expect(page.getByTestId("app-header")).toBeVisible();
await expect(
page.getByTestId("github_starred_icon_get_started"),
).toBeVisible();
await expect(
page.getByTestId("create_flow_icon_get_started"),
).toBeVisible();
await expect(
page.getByTestId("discord_joined_icon_get_started"),
).not.toBeVisible();
await expect(
page.getByTestId("get_started_progress_percentage").first(),
).toHaveText("66%");
await page.getByTestId("discord_joined_btn_get_started").click();
const pagePromiseDiscord = context.waitForEvent("page");
const newPageDiscord = await pagePromiseDiscord;
await newPageDiscord.waitForTimeout(3000);
const newUrlDiscord = newPageDiscord.url();
await expect(newUrlDiscord).toContain(DISCORD_URL);
await newPageDiscord.close();
await expect(page.getByTestId("app-header")).toBeVisible();
await expect(
page.getByTestId("discord_joined_icon_get_started"),
).toBeVisible();
await expect(
page.getByTestId("create_flow_icon_get_started"),
).toBeVisible();
await expect(
page.getByTestId("github_starred_icon_get_started"),
).toBeVisible();
await expect(
page.getByTestId("get_started_progress_percentage").first(),
).toHaveText("100%");
await page.getByTestId("close_get_started_dialog").click();
await expect(
page.getByTestId("discord_joined_icon_get_started"),
).not.toBeVisible();
await expect(
page.getByTestId("create_flow_icon_get_started"),
).not.toBeVisible();
await expect(
page.getByTestId("github_starred_icon_get_started"),
).not.toBeVisible();
},
);

View file

@ -0,0 +1,103 @@
import { expect, Page } from "@playwright/test";
export const addNewUserAndLogin = async (page: Page) => {
await page.route("**/api/v1/auto_login", (route) => {
route.fulfill({
status: 500,
contentType: "application/json",
body: JSON.stringify({
detail: { auto_login: false },
}),
});
});
await page.addInitScript(() => {
window.process = window.process || {};
const newEnv = { ...window.process.env, LANGFLOW_AUTO_LOGIN: "false" };
Object.defineProperty(window.process, "env", {
value: newEnv,
writable: true,
configurable: true,
});
sessionStorage.setItem("testMockAutoLogin", "true");
});
const randomName = Math.random().toString(36).substring(5);
const randomPassword = Math.random().toString(36).substring(5);
await page.goto("/");
await page.waitForSelector("text=sign in to langflow", { timeout: 30000 });
await page.getByPlaceholder("Username").fill("langflow");
await page.getByPlaceholder("Password").fill("langflow");
await page.evaluate(() => {
sessionStorage.removeItem("testMockAutoLogin");
});
await page.getByRole("button", { name: "Sign In" }).click();
await page.waitForSelector('[data-testid="mainpage_title"]', {
timeout: 30000,
});
await page.waitForSelector('[id="new-project-btn"]', {
timeout: 30000,
});
await page.getByTestId("user-profile-settings").click();
await page.getByText("Admin Page", { exact: true }).click();
//CRUD an user
await page.getByText("New User", { exact: true }).click();
await page.getByPlaceholder("Username").last().fill(randomName);
await page.locator('input[name="password"]').fill(randomPassword);
await page.locator('input[name="confirmpassword"]').fill(randomPassword);
await page.waitForSelector("#is_active", {
timeout: 1500,
});
await page.locator("#is_active").click();
await page.getByText("Save", { exact: true }).click();
await page.waitForSelector("text=new user added", { timeout: 30000 });
await expect(page.getByText(randomName, { exact: true })).toBeVisible({
timeout: 2000,
});
await page.waitForSelector("[data-testid='user-profile-settings']", {
timeout: 1500,
});
await page.getByTestId("user-profile-settings").click();
await page.evaluate(() => {
sessionStorage.setItem("testMockAutoLogin", "true");
});
await page.getByText("Logout", { exact: true }).click();
await page.waitForSelector("text=sign in to langflow", { timeout: 30000 });
await page.getByPlaceholder("Username").fill(randomName);
await page.getByPlaceholder("Password").fill(randomPassword);
await page.waitForSelector("text=Sign in", {
timeout: 1500,
});
await page.getByRole("button", { name: "Sign In" }).click();
await page.evaluate(() => {
sessionStorage.removeItem("testMockAutoLogin");
});
};

View file

@ -0,0 +1,56 @@
# Task: Gradient Border Styling Refinement
**Status**: Completed
## Analysis
- [x] Requirements
- [x] Match the gradient border style of the GitHub card shown in the reference image
- [x] Ensure a consistent purple hue that fades from top to bottom
- [x] Maintain proper dark background for content
- [x] Challenges
- [x] Achieving precise gradient fade matching the reference image
- [x] Balancing border visibility while maintaining subtle effect
- [x] Dependencies
- [x] Existing BackgroundGradient component
## Plan
- [x] Step 1: Review the current implementation
- [x] Analyze existing gradient colors and opacity values
- [x] Compare with reference image for differences
- [x] Step 2: Adjust color and gradient parameters
- [x] Modify gradient colors to match the purple hue
- [x] Fine-tune opacity values for natural fade
- [x] Ensure proper background color for inner content
- [x] Step 3: Optimize hover effects
- [x] Refine blur and opacity transitions on hover
- [x] Test with different content types
## Execution
- [x] Modify gradient parameters
- [x] Update gradient direction and opacity stops
- [x] Simplify to single-color fade for consistency
- [x] Adjust container styles
- [x] Set proper padding for border thickness
- [x] Ensure rounded corners match reference
- [x] Test and refine
- [x] Compare implementation with reference image
- [x] Make final adjustments to achieve exact match
## Summary
- [x] Files modified: `src/frontend/src/components/ui/background-gradient.tsx`
- [x] Dependencies added/changed: None
- [x] Edge cases considered: Different screen sizes, container dimensions
- [x] Known limitations: Exact color reproduction may vary slightly based on monitor calibration
- [x] Future impact points: Component can be reused across the application for consistent styling
### Implementation Details
1. Changed the gradient to a simpler purple fade from top to bottom
2. Used opacity values of 0.7 at top and 0.3 at bottom for subtle fade
3. Reduced border thickness to 2px with `p-[2px]`
4. Made the hover effect more subtle with `opacity-60` instead of `opacity-100`
5. Changed blur on hover from `blur-lg` to `blur-md` for a less intense glow