diff --git a/.cursor/rules/tasks.mdc b/.cursor/rules/tasks.mdc new file mode 100644 index 000000000..34db87c93 --- /dev/null +++ b/.cursor/rules/tasks.mdc @@ -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 \ No newline at end of file diff --git a/src/backend/base/langflow/alembic/versions/e56d87f8994a_add_optins_column_to_user.py b/src/backend/base/langflow/alembic/versions/e56d87f8994a_add_optins_column_to_user.py new file mode 100644 index 000000000..599c8138d --- /dev/null +++ b/src/backend/base/langflow/alembic/versions/e56d87f8994a_add_optins_column_to_user.py @@ -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 ### diff --git a/src/backend/base/langflow/services/database/models/user/model.py b/src/backend/base/langflow/services/database/models/user/model.py index 2f420d882..7bd4606d2 100644 --- a/src/backend/base/langflow/services/database/models/user/model.py +++ b/src/backend/base/langflow/services/database/models/user/model.py @@ -1,7 +1,9 @@ from datetime import datetime, timezone -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from uuid import UUID, uuid4 +from pydantic import BaseModel +from sqlalchemy import JSON, Column from sqlmodel import Field, Relationship, SQLModel from langflow.schema.serialize import UUIDstr @@ -13,6 +15,13 @@ if TYPE_CHECKING: 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] id: UUIDstr = Field(default_factory=uuid4, primary_key=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", 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): username: str = Field() password: str = Field() + optins: dict[str, Any] | None = Field( + default={"github_starred": False, "dialog_dismissed": False, "discord_clicked": False} + ) class UserRead(SQLModel): @@ -54,6 +69,7 @@ class UserRead(SQLModel): create_at: datetime = Field() updated_at: datetime = Field() last_login_at: datetime | None = Field(nullable=True) + optins: dict[str, Any] | None = Field(default=None) class UserUpdate(SQLModel): @@ -63,3 +79,4 @@ class UserUpdate(SQLModel): is_active: bool | None = None is_superuser: bool | None = None last_login_at: datetime | None = None + optins: dict[str, Any] | None = None diff --git a/src/frontend/src/CustomNodes/GenericNode/components/RenderInputParameters/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/RenderInputParameters/index.tsx index e361718d3..c0ab05610 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/RenderInputParameters/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/components/RenderInputParameters/index.tsx @@ -74,8 +74,6 @@ const RenderInputParameters = ({ return keyMap; }, [templateFields, data.id, data.node?.template]); - console.log({ data }); - const renderInputParameter = templateFields.map( (templateField: string, idx) => { const template = data.node?.template[templateField]; diff --git a/src/frontend/src/alerts/alertDropDown/index.tsx b/src/frontend/src/alerts/alertDropDown/index.tsx index 87e2fac56..d9746046f 100644 --- a/src/frontend/src/alerts/alertDropDown/index.tsx +++ b/src/frontend/src/alerts/alertDropDown/index.tsx @@ -47,7 +47,7 @@ const AlertDropdown = forwardRef(
Notifications diff --git a/src/frontend/src/assets/github-bg.png b/src/frontend/src/assets/github-bg.png new file mode 100644 index 000000000..3b130fd72 Binary files /dev/null and b/src/frontend/src/assets/github-bg.png differ diff --git a/src/frontend/src/assets/logo_dark.png b/src/frontend/src/assets/logo_dark.png new file mode 100644 index 000000000..f470c4f25 Binary files /dev/null and b/src/frontend/src/assets/logo_dark.png differ diff --git a/src/frontend/src/assets/logo_light.png b/src/frontend/src/assets/logo_light.png new file mode 100644 index 000000000..d457a6116 Binary files /dev/null and b/src/frontend/src/assets/logo_light.png differ diff --git a/src/frontend/src/components/core/appHeaderComponent/components/AccountMenu/index.tsx b/src/frontend/src/components/core/appHeaderComponent/components/AccountMenu/index.tsx index c57dda22c..6fbbf4155 100644 --- a/src/frontend/src/components/core/appHeaderComponent/components/AccountMenu/index.tsx +++ b/src/frontend/src/components/core/appHeaderComponent/components/AccountMenu/index.tsx @@ -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 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 useAuthStore from "@/stores/authStore"; 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 GithubStarComponent from "../GithubStarButton"; import { HeaderMenu, HeaderMenuItemButton, HeaderMenuItemLink, HeaderMenuItems, - HeaderMenuItemsSection, HeaderMenuToggle, } from "../HeaderMenu"; import { ProfileIcon } from "../ProfileIcon"; import ThemeButtons from "../ThemeButtons"; export const AccountMenu = () => { - const [isFeedbackOpen, setIsFeedbackOpen] = useState(false); - const [isCustomFeatureFlagsOpen, setIsCustomFeatureFlagsOpen] = - useState(false); const { customParam: id } = useParams(); const version = useDarkStore((state) => state.version); + const latestVersion = useDarkStore((state) => state.latestVersion); const navigate = useCustomNavigate(); const { mutate: mutationLogout } = useLogout(); @@ -40,6 +38,8 @@ export const AccountMenu = () => { mutationLogout(); }; + const isLatestVersion = version === latestVersion; + return ( <> @@ -48,34 +48,37 @@ export const AccountMenu = () => { className="h-7 w-7 rounded-lg focus-visible:outline-0" data-testid="user-profile-settings" > - {ENABLE_DATASTAX_LANGFLOW ? : } +
- - {ENABLE_DATASTAX_LANGFLOW && ( - - - - )} - -
-
- - Version {version} - + +
+
+
+
+ + Version + +
+ {version}{" "} + {isLatestVersion ? "(latest)" : "(update available)"} +
+
- {!ENABLE_DATASTAX_LANGFLOW && }
- {ENABLE_DATASTAX_LANGFLOW ? ( - - Account Settings - - ) : ( + +
{ navigate("/settings"); }} @@ -87,100 +90,84 @@ export const AccountMenu = () => { Settings - )} - {!ENABLE_DATASTAX_LANGFLOW && ( - <> - {isAdmin && !autoLogin && ( - navigate("/admin")}> + + {isAdmin && !autoLogin && ( +
+ { + navigate("/admin"); + }} + > Admin Page - )} - - )} - {ENABLE_DATASTAX_LANGFLOW ? ( - <> - setIsFeedbackOpen(true)}> - - Feedback - - - setIsCustomFeatureFlagsOpen(true)} - /> - - ) : ( - +
+ )} + Docs - )} - - - {ENABLE_DATASTAX_LANGFLOW ? ( - -
-
Star the repo
- -
-
- ) : ( - - - Share Feedback on Github +
+ +
+ + + + GitHub - )} - - - Follow Langflow on X - - - - - Join the Langflow Discord - - - - {ENABLE_DATASTAX_LANGFLOW ? ( - - - Logout + + + + Discord + - - ) : ( - !autoLogin && ( - + + + + X + + +
+ +
+ Theme +
+ +
+
+ + {!autoLogin && ( +
Logout - - ) - )} +
+ )} +
- - ); }; diff --git a/src/frontend/src/components/core/appHeaderComponent/components/GithubStarButton/index.tsx b/src/frontend/src/components/core/appHeaderComponent/components/GithubStarButton/index.tsx deleted file mode 100644 index 08d1f2ed4..000000000 --- a/src/frontend/src/components/core/appHeaderComponent/components/GithubStarButton/index.tsx +++ /dev/null @@ -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 ( - -
- -
Star
-
- {stars?.toLocaleString() ?? 0} -
-
-
- ); -}; - -export default GithubStarComponent; diff --git a/src/frontend/src/components/core/appHeaderComponent/components/HeaderMenu/index.tsx b/src/frontend/src/components/core/appHeaderComponent/components/HeaderMenu/index.tsx index 414e687bf..cbff24a0e 100644 --- a/src/frontend/src/components/core/appHeaderComponent/components/HeaderMenu/index.tsx +++ b/src/frontend/src/components/core/appHeaderComponent/components/HeaderMenu/index.tsx @@ -22,11 +22,7 @@ export const HeaderMenuToggle = ({ children }) => ( >
{children} - +
); @@ -72,10 +68,14 @@ export const HeaderMenuItemButton = ({ icon = "", onClick, children }) => ( export const HeaderMenuItems = ({ position = "left", children, -}: React.PropsWithChildren<{ position?: "left" | "right" }>) => { + classNameSize = "w-[20rem]", +}: React.PropsWithChildren<{ + position?: "left" | "right"; + classNameSize?: string; +}>) => { const positionClass = position === "left" ? "left-0" : "right-0"; return ( - + {children} ); diff --git a/src/frontend/src/components/core/appHeaderComponent/components/ThemeButtons/index.tsx b/src/frontend/src/components/core/appHeaderComponent/components/ThemeButtons/index.tsx index 92c8c223d..e2b5aa3ff 100644 --- a/src/frontend/src/components/core/appHeaderComponent/components/ThemeButtons/index.tsx +++ b/src/frontend/src/components/core/appHeaderComponent/components/ThemeButtons/index.tsx @@ -60,7 +60,7 @@ export const ThemeButtons = () => { data-testid="menu_light_button" id="menu_light_button" > - + {/* Dark Theme Button */} @@ -68,14 +68,14 @@ export const ThemeButtons = () => { unstyled className={`relative z-10 mx-1 inline-flex items-center rounded-full px-1 ${ selectedTheme === "dark" - ? "text-background dark:hover:bg-purple-400" - : "text-foreground hover:bg-purple-400 hover:text-background" + ? "bg-indigo-foreground text-primary hover:bg-indigo-foreground" + : "text-foreground hover:bg-indigo-foreground hover:text-background" }`} onClick={() => handleThemeChange("dark")} data-testid="menu_dark_button" id="menu_dark_button" > - + {/* System Theme Button */} @@ -90,7 +90,11 @@ export const ThemeButtons = () => { data-testid="menu_system_button" id="menu_system_button" > - +
); diff --git a/src/frontend/src/components/core/appHeaderComponent/components/langflow-counts.tsx b/src/frontend/src/components/core/appHeaderComponent/components/langflow-counts.tsx new file mode 100644 index 000000000..94d2c6706 --- /dev/null +++ b/src/frontend/src/components/core/appHeaderComponent/components/langflow-counts.tsx @@ -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 ( +
+ +
+ + {formatNumber(stars)} +
+
+ + +
+ + + {formatNumber(discordCount)} + +
+
+
+ ); +}; + +export default LangflowCounts; diff --git a/src/frontend/src/components/core/appHeaderComponent/index.tsx b/src/frontend/src/components/core/appHeaderComponent/index.tsx index 34c8a69f5..da95a823f 100644 --- a/src/frontend/src/components/core/appHeaderComponent/index.tsx +++ b/src/frontend/src/components/core/appHeaderComponent/index.tsx @@ -12,10 +12,12 @@ import { useCustomNavigate } from "@/customization/hooks/use-custom-navigate"; import useTheme from "@/customization/hooks/use-custom-theme"; import { useResetDismissUpdateAll } from "@/hooks/use-reset-dismiss-update-all"; import useAlertStore from "@/stores/alertStore"; +import useFlowsManagerStore from "@/stores/flowsManagerStore"; +import { useFolderStore } from "@/stores/foldersStore"; import { useEffect, useRef, useState } from "react"; import { AccountMenu } from "./components/AccountMenu"; import FlowMenu from "./components/FlowMenu"; -import GithubStarComponent from "./components/GithubStarButton"; +import LangflowCounts from "./components/langflow-counts"; export default function AppHeader(): JSX.Element { const notificationCenter = useAlertStore((state) => state.notificationCenter); @@ -46,9 +48,17 @@ export default function AppHeader(): JSX.Element { 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 (
{/* Left Section */} @@ -83,7 +93,7 @@ export default function AppHeader(): JSX.Element { {/* Right Section */}
{!ENABLE_DATASTAX_LANGFLOW && ( @@ -95,7 +105,7 @@ export default function AppHeader(): JSX.Element { window.open("https://github.com/langflow-ai/langflow", "_blank") } > - + )} @@ -111,8 +121,7 @@ export default function AppHeader(): JSX.Element { setActiveState(null)}> - {!ENABLE_DATASTAX_LANGFLOW && ( - <> - - - - - - )} + {ENABLE_DATASTAX_LANGFLOW && ( <> diff --git a/src/frontend/src/components/core/cardsWrapComponent/index.tsx b/src/frontend/src/components/core/cardsWrapComponent/index.tsx index e5853a788..18d149259 100644 --- a/src/frontend/src/components/core/cardsWrapComponent/index.tsx +++ b/src/frontend/src/components/core/cardsWrapComponent/index.tsx @@ -74,7 +74,7 @@ export default function CardsWrapComponent({ className={cn( "h-full w-full", 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" : "", )} > diff --git a/src/frontend/src/components/core/folderSidebarComponent/components/sideBarFolderButtons/components/get-started-progress.tsx b/src/frontend/src/components/core/folderSidebarComponent/components/sideBarFolderButtons/components/get-started-progress.tsx new file mode 100644 index 000000000..3de854e16 --- /dev/null +++ b/src/frontend/src/components/core/folderSidebarComponent/components/sideBarFolderButtons/components/get-started-progress.tsx @@ -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 ( +
+
+ Get started + +
+ +
+
+
+
+ + {percentageGetStarted}% + +
+ +
+ + + + + +
+ + {}} + handleDeleteFolder={() => {}} + /> +
+ ); +}; diff --git a/src/frontend/src/components/core/folderSidebarComponent/components/sideBarFolderButtons/components/header-buttons.tsx b/src/frontend/src/components/core/folderSidebarComponent/components/sideBarFolderButtons/components/header-buttons.tsx index 56c9b23bd..0421303df 100644 --- a/src/frontend/src/components/core/folderSidebarComponent/components/sideBarFolderButtons/components/header-buttons.tsx +++ b/src/frontend/src/components/core/folderSidebarComponent/components/sideBarFolderButtons/components/header-buttons.tsx @@ -1,5 +1,9 @@ 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 useAuthStore from "@/stores/authStore"; +import { Separator } from "@radix-ui/react-separator"; +import { useState } from "react"; import { AddFolderButton } from "./add-folder-button"; import { UploadFolderButton } from "./upload-folder-button"; @@ -13,23 +17,54 @@ export const HeaderButtons = ({ isUpdatingFolder: boolean; isPending: boolean; addNewFolder: () => void; -}) => ( -
- - - +}) => { + const userData = useAuthStore((state) => state.userData); + const userDismissedDialog = userData?.optins?.dialog_dismissed; + const isGithubStarred = userData?.optins?.github_starred; + const isDiscordJoined = userData?.optins?.discord_clicked; -
Folders
-
- - -
-
-); + const [isDismissedDialog, setIsDismissedDialog] = + useState(userDismissedDialog); + + const handleDismissDialog = () => { + setIsDismissedDialog(true); + }; + + return ( + <> + {!isDismissedDialog && ( + <> + + +
+
+
+ + )} + +
+ + + + +
Folders
+
+ + +
+
+ + ); +}; diff --git a/src/frontend/src/components/ui/background-gradient.tsx b/src/frontend/src/components/ui/background-gradient.tsx new file mode 100644 index 000000000..e324362f0 --- /dev/null +++ b/src/frontend/src/components/ui/background-gradient.tsx @@ -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 ( +
+ + + +
{children}
+
+ ); +}; diff --git a/src/frontend/src/components/ui/dot-background.tsx b/src/frontend/src/components/ui/dot-background.tsx new file mode 100644 index 000000000..08873fbc8 --- /dev/null +++ b/src/frontend/src/components/ui/dot-background.tsx @@ -0,0 +1,36 @@ +import { cn } from "@/utils/utils"; + +export function DotBackgroundDemo({ + children, + className, + containerClassName, +}: { + children: React.ReactNode; + className?: string; + containerClassName?: string; +}) { + return ( +
+
+
+ {children} +
+ ); +} diff --git a/src/frontend/src/constants/constants.ts b/src/frontend/src/constants/constants.ts index 7ec483bec..bea80f179 100644 --- a/src/frontend/src/constants/constants.ts +++ b/src/frontend/src/constants/constants.ts @@ -1005,6 +1005,7 @@ export const GRADIENT_CLASS_DISABLED = "linear-gradient(to right, hsl(var(--muted) / 0.3), hsl(var(--muted)))"; export const RECEIVING_INPUT_VALUE = "Receiving input"; +export const SELECT_AN_OPTION = "Select an option..."; export const ICON_STROKE_WIDTH = 1.5; @@ -1068,5 +1069,9 @@ export const OPENAI_VOICES = [ export const DEFAULT_POLLING_INTERVAL = 5000; export const DEFAULT_TIMEOUT = 30000; 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"; diff --git a/src/frontend/src/controllers/API/index.ts b/src/frontend/src/controllers/API/index.ts index 28289cce8..495ba4e2f 100644 --- a/src/frontend/src/controllers/API/index.ts +++ b/src/frontend/src/controllers/API/index.ts @@ -10,6 +10,8 @@ import { FlowStyleType, FlowType } from "../../types/flow"; import { StoreComponentResponse } from "../../types/store"; 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) { 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) { try { const res = await api.post(`${BASE_URL_API}api_key/`, { name }); diff --git a/src/frontend/src/controllers/API/queries/auth/use-get-user.ts b/src/frontend/src/controllers/API/queries/auth/use-get-user.ts index 1f5c73ab9..cf7bb1866 100644 --- a/src/frontend/src/controllers/API/queries/auth/use-get-user.ts +++ b/src/frontend/src/controllers/API/queries/auth/use-get-user.ts @@ -1,16 +1,18 @@ +import useAuthStore from "@/stores/authStore"; import { UseMutationResult } from "@tanstack/react-query"; import { useMutationFunctionType, Users } from "../../../../types/api"; import { api } from "../../api"; import { getURL } from "../../helpers/constants"; import { UseRequestProcessor } from "../../services/request-processor"; - export const useGetUserData: useMutationFunctionType = ( options?, ) => { + const setUserData = useAuthStore((state) => state.setUserData); const { mutate } = UseRequestProcessor(); const getUserData = async () => { const response = await api.get(`${getURL("USERS")}/whoami`); + setUserData(response["data"]); return response["data"]; }; diff --git a/src/frontend/src/controllers/API/queries/version/use-get-version.ts b/src/frontend/src/controllers/API/queries/version/use-get-version.ts index 0282f99f0..134c74e08 100644 --- a/src/frontend/src/controllers/API/queries/version/use-get-version.ts +++ b/src/frontend/src/controllers/API/queries/version/use-get-version.ts @@ -7,6 +7,7 @@ import { UseRequestProcessor } from "../../services/request-processor"; interface versionQueryResponse { version: string; package: string; + main_version: string; } export const useGetVersionQuery: useQueryFunctionType< @@ -22,7 +23,9 @@ export const useGetVersionQuery: useQueryFunctionType< const responseFn = async () => { const { data } = await getVersionFn(); const refreshVersion = useDarkStore.getState().refreshVersion; + const refreshLatestVersion = useDarkStore.getState().refreshLatestVersion; refreshVersion(data.version); + refreshLatestVersion(data.main_version); return data; }; diff --git a/src/frontend/src/icons/Twitter X/TwitterX.jsx b/src/frontend/src/icons/Twitter X/TwitterX.jsx new file mode 100644 index 000000000..ec46eb97b --- /dev/null +++ b/src/frontend/src/icons/Twitter X/TwitterX.jsx @@ -0,0 +1,46 @@ +const TwitterXSVG = (props) => { + return props.isdark === "true" ? ( + + + + + + + + ) : ( + + + + ); +}; + +export default TwitterXSVG; diff --git a/src/frontend/src/icons/Twitter X/iconX.svg b/src/frontend/src/icons/Twitter X/iconX.svg new file mode 100644 index 000000000..0ede3f359 --- /dev/null +++ b/src/frontend/src/icons/Twitter X/iconX.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/frontend/src/icons/Twitter X/icons8-x.svg b/src/frontend/src/icons/Twitter X/icons8-x.svg new file mode 100644 index 000000000..69cf5fec6 --- /dev/null +++ b/src/frontend/src/icons/Twitter X/icons8-x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/frontend/src/icons/Twitter X/index.tsx b/src/frontend/src/icons/Twitter X/index.tsx new file mode 100644 index 000000000..57c9b7e43 --- /dev/null +++ b/src/frontend/src/icons/Twitter X/index.tsx @@ -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 ; +}); diff --git a/src/frontend/src/icons/lucideIcons.ts b/src/frontend/src/icons/lucideIcons.ts index 7b03a979e..25231459e 100644 --- a/src/frontend/src/icons/lucideIcons.ts +++ b/src/frontend/src/icons/lucideIcons.ts @@ -231,6 +231,7 @@ import { ZoomIn, ZoomOut, } from "lucide-react"; +import { TwitterXIcon } from "./Twitter X"; // Create a map of eagerly loaded Lucide icons export const lucideIcons = { @@ -464,4 +465,5 @@ export const lucideIcons = { Zap, ZoomIn, ZoomOut, + TwitterXIcon, }; diff --git a/src/frontend/src/modals/deleteConfirmationModal/index.tsx b/src/frontend/src/modals/deleteConfirmationModal/index.tsx index a0eb1200f..0ac8e1bf3 100644 --- a/src/frontend/src/modals/deleteConfirmationModal/index.tsx +++ b/src/frontend/src/modals/deleteConfirmationModal/index.tsx @@ -61,6 +61,7 @@ export default function DeleteConfirmationModal({ onClick={(e) => e.stopPropagation()} className="mr-1" variant="outline" + data-testid="btn_cancel_delete_confirmation_modal" > Cancel @@ -72,6 +73,7 @@ export default function DeleteConfirmationModal({ onClick={(e) => { onConfirm(e); }} + data-testid="btn_delete_delete_confirmation_modal" > Delete diff --git a/src/frontend/src/pages/AppInitPage/index.tsx b/src/frontend/src/pages/AppInitPage/index.tsx index 708caaea3..ea2303b5e 100644 --- a/src/frontend/src/pages/AppInitPage/index.tsx +++ b/src/frontend/src/pages/AppInitPage/index.tsx @@ -15,6 +15,9 @@ import { LoadingPage } from "../LoadingPage"; export function AppInitPage() { const refreshStars = useDarkStore((state) => state.refreshStars); + const refreshDiscordCount = useDarkStore( + (state) => state.refreshDiscordCount, + ); const isLoading = useFlowsManagerStore((state) => state.isLoading); const { isFetched: isLoaded } = useCustomPrimaryLoading(); @@ -31,6 +34,7 @@ export function AppInitPage() { useEffect(() => { if (isFetched) { refreshStars(); + refreshDiscordCount(); } if (isConfigFetched) { diff --git a/src/frontend/src/pages/MainPage/components/dropdown/index.tsx b/src/frontend/src/pages/MainPage/components/dropdown/index.tsx index 907128bee..1b1144c43 100644 --- a/src/frontend/src/pages/MainPage/components/dropdown/index.tsx +++ b/src/frontend/src/pages/MainPage/components/dropdown/index.tsx @@ -93,6 +93,7 @@ const DropdownComponent = ({ setOpenDelete(true); }} className="cursor-pointer text-destructive" + data-testid="btn_delete_dropdown_menu" > +  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 ( + + +
+
+
+
+ Langflow Logo Light +
+
+ Langflow Logo Dark +
+ + {EMPTY_PAGE_TITLE} + + + + {folders?.length > 1 + ? EMPTY_PAGE_FOLDER_DESCRIPTION + : EMPTY_PAGE_DESCRIPTION} + +
+ +
+ + + + + +
+
+
+

+ {EMPTY_PAGE_DRAG_AND_DROP_TEXT} +

+
+
+ ); +}; + +export default EmptyPageCommunity; diff --git a/src/frontend/src/pages/MainPage/pages/enchanced-beam-effect.tsx b/src/frontend/src/pages/MainPage/pages/enchanced-beam-effect.tsx new file mode 100644 index 000000000..c52a62510 --- /dev/null +++ b/src/frontend/src/pages/MainPage/pages/enchanced-beam-effect.tsx @@ -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 ( +
+ {children} + + {/* Primary beam - larger, slower rotation */} + +
+ ); +}; + +export default EnhancedBeamEffect; diff --git a/src/frontend/src/pages/MainPage/pages/index.tsx b/src/frontend/src/pages/MainPage/pages/main-page.tsx similarity index 95% rename from src/frontend/src/pages/MainPage/pages/index.tsx rename to src/frontend/src/pages/MainPage/pages/main-page.tsx index 0c3470476..a4e85987b 100644 --- a/src/frontend/src/pages/MainPage/pages/index.tsx +++ b/src/frontend/src/pages/MainPage/pages/main-page.tsx @@ -10,6 +10,7 @@ import { useQueryClient } from "@tanstack/react-query"; import { useEffect, useState } from "react"; import { Outlet } from "react-router-dom"; import ModalsComponent from "../components/modalsComponent"; +import EmptyPageCommunity from "./empty-page"; import EmptyPage from "./emptyPage"; export default function CollectionPage(): JSX.Element { @@ -80,7 +81,9 @@ export default function CollectionPage(): JSX.Element { {flows?.length !== examples?.length || folders?.length > 1 ? ( ) : ( - + // + + )}
) : ( diff --git a/src/frontend/src/routes.tsx b/src/frontend/src/routes.tsx index 0659310eb..b87a63d0d 100644 --- a/src/frontend/src/routes.tsx +++ b/src/frontend/src/routes.tsx @@ -23,9 +23,9 @@ import { AppWrapperPage } from "./pages/AppWrapperPage"; import { DashboardWrapperPage } from "./pages/DashboardWrapperPage"; import FlowPage from "./pages/FlowPage"; import LoginPage from "./pages/LoginPage"; -import CollectionPage from "./pages/MainPage/pages"; import FilesPage from "./pages/MainPage/pages/filesPage"; import HomePage from "./pages/MainPage/pages/homePage"; +import CollectionPage from "./pages/MainPage/pages/main-page"; import SettingsPage from "./pages/SettingsPage"; import ApiKeysPage from "./pages/SettingsPage/pages/ApiKeysPage"; import GeneralPage from "./pages/SettingsPage/pages/GeneralPage"; diff --git a/src/frontend/src/stores/darkStore.ts b/src/frontend/src/stores/darkStore.ts index 6e3c28afa..444a76af2 100644 --- a/src/frontend/src/stores/darkStore.ts +++ b/src/frontend/src/stores/darkStore.ts @@ -1,5 +1,5 @@ import { create } from "zustand"; -import { getRepoStars } from "../controllers/API"; +import { getDiscordCount, getRepoStars } from "../controllers/API"; import { DarkStoreType } from "../types/zustand/dark"; const startedStars = Number(window.localStorage.getItem("githubStars")) ?? 0; @@ -8,6 +8,10 @@ export const useDarkStore = create((set, get) => ({ dark: JSON.parse(window.localStorage.getItem("isDark")!) ?? false, stars: startedStars, version: "", + latestVersion: "", + refreshLatestVersion: (v: string) => { + set(() => ({ latestVersion: v })); + }, setDark: (dark) => { set(() => ({ dark: dark })); window.localStorage.setItem("isDark", dark.toString()); @@ -40,4 +44,10 @@ export const useDarkStore = create((set, get) => ({ }); } }, + discordCount: 0, + refreshDiscordCount: () => { + getDiscordCount().then((res) => { + set(() => ({ discordCount: res })); + }); + }, })); diff --git a/src/frontend/src/style/index.css b/src/frontend/src/style/index.css index 2f2d0fef2..d522ff4b8 100644 --- a/src/frontend/src/style/index.css +++ b/src/frontend/src/style/index.css @@ -69,6 +69,7 @@ --round-btn-shadow: #00000063; --ice: #31a3cc; --selected: #2196f3; + --discord-color: #5765f2; --error-background: #fef2f2; --error-foreground: #991b1b; @@ -194,6 +195,7 @@ --accent-amber: 48, 96%, 89%; --accent-amber-foreground: 26 90.5% 37.1%; --red-foreground: 0 90.6% 70.8%; + --indigo-foreground: 243.7 54.5% 41.4%; } .dark { @@ -344,6 +346,8 @@ --hover: #1a202e; --disabled-run: #6366f1; --selected: #0369a1; + --discord-color: #5765f2; + --filter-foreground: #eef2ff; --filter-background: #4e46e599; @@ -450,5 +454,6 @@ --accent-amber: 22, 78%, 26%; --accent-amber-foreground: 45.9 96.7% 64.5%; --red-foreground: 0 72.2% 50.6%; + --indigo-foreground: 234.5 89.5% 73.9%; } } diff --git a/src/frontend/src/types/api/index.ts b/src/frontend/src/types/api/index.ts index 651717c3e..ebbe5115e 100644 --- a/src/frontend/src/types/api/index.ts +++ b/src/frontend/src/types/api/index.ts @@ -157,6 +157,11 @@ export type changeUser = { is_superuser?: boolean; password?: string; profile_image?: string; + optins?: { + github_starred?: boolean; + discord_clicked?: boolean; + dialog_dismissed?: boolean; + }; }; export type resetPasswordType = { @@ -172,6 +177,11 @@ export type Users = { profile_image: string; create_at: Date; updated_at: Date; + optins?: { + github_starred?: boolean; + discord_clicked?: boolean; + dialog_dismissed?: boolean; + }; }; export type Component = { diff --git a/src/frontend/src/types/zustand/dark/index.ts b/src/frontend/src/types/zustand/dark/index.ts index f71b98bde..ee696af83 100644 --- a/src/frontend/src/types/zustand/dark/index.ts +++ b/src/frontend/src/types/zustand/dark/index.ts @@ -2,7 +2,11 @@ export type DarkStoreType = { dark: boolean; stars: number; version: string; + latestVersion: string; setDark: (dark: boolean) => void; refreshVersion: (v: string) => void; + refreshLatestVersion: (v: string) => void; refreshStars: () => void; + discordCount: number; + refreshDiscordCount: () => void; }; diff --git a/src/frontend/src/utils/styleUtils.ts b/src/frontend/src/utils/styleUtils.ts index 9535ac028..cb7a2d8bf 100644 --- a/src/frontend/src/utils/styleUtils.ts +++ b/src/frontend/src/utils/styleUtils.ts @@ -421,6 +421,7 @@ export const nodeIconToDisplayIconMap: Record = { ScrapeGraphMarkdownifyApi: "ScrapeGraph", Unlink: "UnlinkIcon", note: "StickyNote", + TwitterXIcon: "TwitterXIcon", }; export const getLucideIconName = (name: string): string => { diff --git a/src/frontend/src/utils/utils.ts b/src/frontend/src/utils/utils.ts index 0152ed73d..df9ec1f03 100644 --- a/src/frontend/src/utils/utils.ts +++ b/src/frontend/src/utils/utils.ts @@ -872,3 +872,15 @@ export const convertUTCToLocalTimezone = (timestamp: string) => { const localTimezone = moment.tz.guess(); 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(); +}; diff --git a/src/frontend/tailwind.config.mjs b/src/frontend/tailwind.config.mjs index ee310be15..424eec9c5 100644 --- a/src/frontend/tailwind.config.mjs +++ b/src/frontend/tailwind.config.mjs @@ -309,6 +309,8 @@ const config = { "slider-input-border": "var(--slider-input-border)", "zinc-foreground": "hsl(var(--zinc-foreground))", "red-foreground": "hsl(var(--red-foreground))", + "indigo-foreground": "hsl(var(--indigo-foreground))", + "discord-color": "var(--discord-color)", }, borderRadius: { lg: `var(--radius)`, diff --git a/src/frontend/tests/core/features/user-progress-track.spec.ts b/src/frontend/tests/core/features/user-progress-track.spec.ts new file mode 100644 index 000000000..6bcd1c11c --- /dev/null +++ b/src/frontend/tests/core/features/user-progress-track.spec.ts @@ -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(); + }, +); diff --git a/src/frontend/tests/utils/add-new-user-and-loggin.ts b/src/frontend/tests/utils/add-new-user-and-loggin.ts new file mode 100644 index 000000000..21fdcbaed --- /dev/null +++ b/src/frontend/tests/utils/add-new-user-and-loggin.ts @@ -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"); + }); +}; diff --git a/tasks/20240610_165500_background_gradient_border_styling.md b/tasks/20240610_165500_background_gradient_border_styling.md new file mode 100644 index 000000000..bbdc5cb28 --- /dev/null +++ b/tasks/20240610_165500_background_gradient_border_styling.md @@ -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