From 69498bebb4e798b50ed62faeac96f103d7305014 Mon Sep 17 00:00:00 2001 From: Hiep Le <69354317+hieptl@users.noreply.github.com> Date: Fri, 12 Sep 2025 22:15:26 +0700 Subject: [PATCH] refactor(frontend): new conversation component (#10937) Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com> --- .../features/home/new-conversation.test.tsx | 2 +- .../create-conversation-button.tsx | 45 +++++++++++ .../new-conversation/new-conversation.tsx | 23 ++++++ frontend/src/routes/home.tsx | 2 +- frontend/src/ui/card-title.tsx | 77 +++++++++++++++++++ frontend/src/ui/card.tsx | 46 +++++++++++ frontend/src/ui/typography.tsx | 16 +++- 7 files changed, 208 insertions(+), 3 deletions(-) create mode 100644 frontend/src/components/features/home/new-conversation/create-conversation-button.tsx create mode 100644 frontend/src/components/features/home/new-conversation/new-conversation.tsx create mode 100644 frontend/src/ui/card-title.tsx create mode 100644 frontend/src/ui/card.tsx diff --git a/frontend/__tests__/components/features/home/new-conversation.test.tsx b/frontend/__tests__/components/features/home/new-conversation.test.tsx index a6771fa266..96dd8387b2 100644 --- a/frontend/__tests__/components/features/home/new-conversation.test.tsx +++ b/frontend/__tests__/components/features/home/new-conversation.test.tsx @@ -5,7 +5,7 @@ import { createRoutesStub } from "react-router"; import { setupStore } from "test-utils"; import { describe, expect, it, vi } from "vitest"; import userEvent from "@testing-library/user-event"; -import { NewConversation } from "#/components/features/home/new-conversation"; +import { NewConversation } from "#/components/features/home/new-conversation/new-conversation"; import OpenHands from "#/api/open-hands"; // Mock the translation function diff --git a/frontend/src/components/features/home/new-conversation/create-conversation-button.tsx b/frontend/src/components/features/home/new-conversation/create-conversation-button.tsx new file mode 100644 index 0000000000..96f08ad9f3 --- /dev/null +++ b/frontend/src/components/features/home/new-conversation/create-conversation-button.tsx @@ -0,0 +1,45 @@ +import { useNavigate } from "react-router"; +import { useTranslation } from "react-i18next"; +import { BrandButton } from "../../settings/brand-button"; +import { useCreateConversation } from "#/hooks/mutation/use-create-conversation"; +import { useIsCreatingConversation } from "#/hooks/use-is-creating-conversation"; + +export function CreateConversationButton() { + const { t } = useTranslation(); + const navigate = useNavigate(); + + const { + mutate: createConversation, + isPending, + isSuccess, + } = useCreateConversation(); + const isCreatingConversationElsewhere = useIsCreatingConversation(); + + // We check for isSuccess because the app might require time to render + // into the new conversation screen after the conversation is created. + const isCreatingConversation = + isPending || isSuccess || isCreatingConversationElsewhere; + + const handleCreateConversation = () => { + createConversation( + {}, + { + onSuccess: (data) => navigate(`/conversations/${data.conversation_id}`), + }, + ); + }; + + return ( + + {!isCreatingConversation && t("COMMON$NEW_CONVERSATION")} + {isCreatingConversation && t("HOME$LOADING")} + + ); +} diff --git a/frontend/src/components/features/home/new-conversation/new-conversation.tsx b/frontend/src/components/features/home/new-conversation/new-conversation.tsx new file mode 100644 index 0000000000..0182ee0c10 --- /dev/null +++ b/frontend/src/components/features/home/new-conversation/new-conversation.tsx @@ -0,0 +1,23 @@ +import { useTranslation } from "react-i18next"; +import { I18nKey } from "#/i18n/declaration"; +import PlusIcon from "#/icons/u-plus.svg?react"; +import { CardTitle } from "#/ui/card-title"; +import { Typography } from "#/ui/typography"; +import { CreateConversationButton } from "./create-conversation-button"; +import { Card } from "#/ui/card"; + +export function NewConversation() { + const { t } = useTranslation(); + + return ( + + }> + {t(I18nKey.COMMON$START_FROM_SCRATCH)} + + + {t(I18nKey.HOME$NEW_PROJECT_DESCRIPTION)} + + + + ); +} diff --git a/frontend/src/routes/home.tsx b/frontend/src/routes/home.tsx index 3f106b5d9e..5c3679414c 100644 --- a/frontend/src/routes/home.tsx +++ b/frontend/src/routes/home.tsx @@ -4,7 +4,7 @@ import { HomeHeader } from "#/components/features/home/home-header/home-header"; import { RepoConnector } from "#/components/features/home/repo-connector"; import { TaskSuggestions } from "#/components/features/home/tasks/task-suggestions"; import { GitRepository } from "#/types/git"; -import { NewConversation } from "#/components/features/home/new-conversation"; +import { NewConversation } from "#/components/features/home/new-conversation/new-conversation"; import { RecentConversations } from "#/components/features/home/recent-conversations/recent-conversations"; ; diff --git a/frontend/src/ui/card-title.tsx b/frontend/src/ui/card-title.tsx new file mode 100644 index 0000000000..7c46fa9ef6 --- /dev/null +++ b/frontend/src/ui/card-title.tsx @@ -0,0 +1,77 @@ +import { ReactNode } from "react"; +import { cva, type VariantProps } from "class-variance-authority"; +import { cn } from "#/utils/utils"; + +const cardTitleVariants = cva("flex items-center", { + variants: { + gap: { + default: "gap-[10px]", + }, + textSize: { + default: "text-base", + }, + fontWeight: { + default: "font-bold", + }, + textColor: { + default: "text-white", + }, + lineHeight: { + default: "leading-5", + }, + }, + defaultVariants: { + gap: "default", + textSize: "default", + fontWeight: "default", + textColor: "default", + lineHeight: "default", + }, +}); + +interface CardTitleProps extends VariantProps { + icon?: ReactNode; + children: ReactNode; + className?: string; +} + +export function CardTitle({ + icon, + children, + className = "", + gap, + textSize, + fontWeight, + textColor, + lineHeight, +}: CardTitleProps) { + return ( +
+ {icon} + + {children} + +
+ ); +} diff --git a/frontend/src/ui/card.tsx b/frontend/src/ui/card.tsx new file mode 100644 index 0000000000..519cbd5730 --- /dev/null +++ b/frontend/src/ui/card.tsx @@ -0,0 +1,46 @@ +import { ReactNode } from "react"; +import { cva, type VariantProps } from "class-variance-authority"; +import { cn } from "#/utils/utils"; + +const cardVariants = cva( + "w-full flex flex-col rounded-[12px] p-[20px] border border-[#727987] bg-[#26282D] relative", + { + variants: { + gap: { + default: "gap-[10px]", + large: "gap-6", + }, + minHeight: { + default: "min-h-[286px] md:min-h-auto", + small: "min-h-[263.5px]", + }, + }, + defaultVariants: { + gap: "default", + minHeight: "default", + }, + }, +); + +interface CardProps extends VariantProps { + children: ReactNode; + className?: string; + testId?: string; +} + +export function Card({ + children, + className = "", + testId, + gap, + minHeight, +}: CardProps) { + return ( +
+ {children} +
+ ); +} diff --git a/frontend/src/ui/typography.tsx b/frontend/src/ui/typography.tsx index b0db531106..54687139b2 100644 --- a/frontend/src/ui/typography.tsx +++ b/frontend/src/ui/typography.tsx @@ -5,6 +5,7 @@ const typographyVariants = cva("", { variants: { variant: { h1: "text-[32px] text-white font-bold leading-5", + span: "text-sm font-normal text-white leading-5.5", }, }, defaultVariants: { @@ -49,5 +50,18 @@ export function H1({ ); } -// Attach H1 to Typography for the expected API +export function Text({ + className, + testId, + children, +}: Omit) { + return ( + + {children} + + ); +} + +// Attach components to Typography for the expected API Typography.H1 = H1; +Typography.Text = Text;