mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
refactor(frontend): new conversation component (#10937)
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
This commit is contained in:
parent
77ee9e25d9
commit
69498bebb4
@ -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
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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" />;
|
||||
|
||||
77
frontend/src/ui/card-title.tsx
Normal file
77
frontend/src/ui/card-title.tsx
Normal 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
46
frontend/src/ui/card.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user