refactor(frontend): new conversation component (#10937)

Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
This commit is contained in:
Hiep Le 2025-09-12 22:15:26 +07:00 committed by GitHub
parent 77ee9e25d9
commit 69498bebb4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 208 additions and 3 deletions

View File

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

View File

@ -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 (
<BrandButton
testId="launch-new-conversation-button"
variant="primary"
type="button"
onClick={handleCreateConversation}
isDisabled={isCreatingConversation}
className="w-auto absolute bottom-5 left-5 right-5 font-semibold"
>
{!isCreatingConversation && t("COMMON$NEW_CONVERSATION")}
{isCreatingConversation && t("HOME$LOADING")}
</BrandButton>
);
}

View File

@ -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 (
<Card>
<CardTitle icon={<PlusIcon width={17} height={14} />}>
{t(I18nKey.COMMON$START_FROM_SCRATCH)}
</CardTitle>
<Typography.Text>
{t(I18nKey.HOME$NEW_PROJECT_DESCRIPTION)}
</Typography.Text>
<CreateConversationButton />
</Card>
);
}

View File

@ -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";
<PrefetchPageLinks page="/conversations/:conversationId" />;

View File

@ -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<typeof cardTitleVariants> {
icon?: ReactNode;
children: ReactNode;
className?: string;
}
export function CardTitle({
icon,
children,
className = "",
gap,
textSize,
fontWeight,
textColor,
lineHeight,
}: CardTitleProps) {
return (
<div
className={cn(
cardTitleVariants({
gap,
textSize,
fontWeight,
textColor,
lineHeight,
}),
className,
)}
>
{icon}
<span
className={cn(
cardTitleVariants({
lineHeight,
textSize,
fontWeight,
textColor,
}),
"flex items-center",
)}
>
{children}
</span>
</div>
);
}

46
frontend/src/ui/card.tsx Normal file
View File

@ -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<typeof cardVariants> {
children: ReactNode;
className?: string;
testId?: string;
}
export function Card({
children,
className = "",
testId,
gap,
minHeight,
}: CardProps) {
return (
<div
data-testid={testId}
className={cn(cardVariants({ gap, minHeight }), className)}
>
{children}
</div>
);
}

View File

@ -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<TypographyProps, "variant">) {
return (
<Typography variant="span" className={className} testId={testId}>
{children}
</Typography>
);
}
// Attach components to Typography for the expected API
Typography.H1 = H1;
Typography.Text = Text;