ai-chatbot/components/home/lovart/lovart-hero-composer.tsx

244 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import dynamic from "next/dynamic";
import { ArrowUp, Box, Lightbulb, Loader2, Plus, Sparkles, X } from "lucide-react";
import type { ChangeEvent } from "react";
import { Button } from "@/components/ui/button";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { cn } from "@/lib/utils";
import type { ModelOption, UploadFileItem } from "./types";
const HomeAnimationEmbed = dynamic(
() =>
import("@/components/home/home-animation-embed").then(
(module) => module.HomeAnimationEmbed
),
{
loading: () => (
<div className="h-[340px] w-full rounded-[16px] border border-black/10 bg-black/[0.03] md:h-[400px] dark:border-white/10 dark:bg-white/[0.04]" />
),
ssr: false,
}
);
type LovartHeroComposerProps = {
composerPlaceholder: string;
displayLogo: string;
files: UploadFileItem[];
handleAddFiles: (files: FileList | null) => void;
heroPromoActionText?: string | null;
heroPromoText?: string | null;
heroSubtitleText?: string | null;
heroTitleText?: string | null;
homeAnimationData?: {
enabled?: boolean;
htmlContent?: string;
fileName?: string;
};
isAgentModeActive: boolean;
isMembershipEnabled: boolean;
isSubmitting: boolean;
onSubmit: () => void;
openUpgradeDialog: () => void;
prompt: string;
removeFile: (id: string) => void;
setPrompt: (value: string) => void;
siteName: string;
modelOptions: ModelOption[];
onModelChange: (modelId: string) => void;
onStartAgentMode: () => void;
selectedModelId: string;
};
export function LovartHeroComposer({
composerPlaceholder,
displayLogo,
files,
handleAddFiles,
heroPromoActionText,
heroPromoText,
heroSubtitleText,
heroTitleText,
homeAnimationData,
isAgentModeActive,
isMembershipEnabled,
isSubmitting,
onSubmit,
openUpgradeDialog,
prompt,
removeFile,
setPrompt,
siteName,
modelOptions,
onModelChange,
onStartAgentMode,
selectedModelId,
}: LovartHeroComposerProps) {
return (
<section className="mt-6 text-center">
{isMembershipEnabled ? (
<button
type="button"
onClick={openUpgradeDialog}
className="inline-flex w-full max-w-[620px] items-center gap-1.5 rounded-[12px] bg-[#fde8bf] px-3 py-1.5 text-[11px] leading-5 text-black/80 transition-colors duration-150 hover:bg-[#f9dfac] sm:w-auto sm:rounded-full sm:text-[12px] dark:bg-[#3f3013] dark:text-[#f6dfaa] dark:hover:bg-[#493819]"
>
<span className="rounded-full bg-[#ff9d3d] px-2 py-0.5 text-[10px] font-semibold text-white">NEW</span>
<span className="line-clamp-2 min-w-0 flex-1 text-left sm:line-clamp-1">
{heroPromoText || "现在升级,立享 Kling 3.0 与 Nano Banana Pro,最高 5 折优惠"}
</span>
<span className="shrink-0 text-[#ff7a00]">{heroPromoActionText || "立即升级"}</span>
</button>
) : (
<div className="inline-flex w-full max-w-[620px] items-center gap-1.5 rounded-[12px] bg-[#fde8bf] px-3 py-1.5 text-[11px] leading-5 text-black/80 sm:w-auto sm:rounded-full sm:text-[12px] dark:bg-[#3f3013] dark:text-[#f6dfaa]">
<span className="rounded-full bg-[#ff9d3d] px-2 py-0.5 text-[10px] font-semibold text-white">NEW</span>
<span className="line-clamp-2 min-w-0 flex-1 text-left sm:line-clamp-1">
{heroPromoText || "现在升级,立享 Kling 3.0 与 Nano Banana Pro,最高 5 折优惠"}
</span>
</div>
)}
<div className="mt-7 flex items-center justify-center gap-1.5">
{displayLogo ? (
<img
src={displayLogo}
alt={siteName}
className="h-8 w-8 rounded-full object-cover"
/>
) : (
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-black text-white dark:bg-white dark:text-black">
<Sparkles className="h-3.5 w-3.5" />
</div>
)}
<h1 className="text-[28px] font-semibold leading-tight sm:text-[30px] dark:text-white">
{heroTitleText || `${siteName || "Lovart"} 让设计更简单`}
</h1>
</div>
<p className="mt-1.5 text-[12px] text-black/38 dark:text-white/48">
{heroSubtitleText || "懂你的设计代理,帮你搞定一切"}
</p>
<div className="mx-auto mt-7 w-full max-w-[760px] rounded-[16px] border border-[#e1e4e8] bg-white px-[18px] pb-[10px] pt-[14px] shadow-[0_8px_22px_rgba(0,0,0,0.055)] transition-shadow duration-150 focus-within:shadow-[0_10px_26px_rgba(0,0,0,0.075)] dark:border-white/10 dark:bg-[#141416] dark:shadow-[0_14px_38px_rgba(0,0,0,0.32)]">
<Textarea
value={prompt}
onChange={(event) => setPrompt(event.target.value)}
placeholder={composerPlaceholder}
className="min-h-[60px] resize-none border-0 bg-transparent px-0 !text-[16px] leading-6 text-[#20242a] shadow-none placeholder:text-[#aeb3bb] focus-visible:ring-0 md:!text-[14px] dark:text-white dark:placeholder:text-white/35"
/>
{files.length > 0 && (
<div className="mb-3 flex flex-wrap gap-2 px-1">
{files.map((item) => (
<div key={item.id} className="group relative h-11 w-11 overflow-hidden rounded-md border border-black/10 dark:border-white/10">
<img src={item.previewUrl} alt={item.file.name} className="h-full w-full object-cover" />
<button
type="button"
className="absolute right-0 top-0 hidden h-5 w-5 items-center justify-center rounded-bl bg-black/60 text-white group-hover:flex"
onClick={() => removeFile(item.id)}
aria-label={`移除${item.file.name}`}
>
<X className="h-3 w-3" />
</button>
</div>
))}
</div>
)}
<div className="flex min-h-[44px] items-center justify-between pt-2">
<div className="flex min-w-0 flex-1 items-center gap-4">
<label className="inline-flex h-8 w-8 cursor-pointer items-center justify-center rounded-full text-[#20242a] transition-colors duration-150 hover:bg-black/5 dark:text-white/72 dark:hover:bg-white/8">
<Plus className="h-4 w-4" strokeWidth={1.9} />
<input
type="file"
accept="image/*"
multiple
className="hidden"
onChange={(event: ChangeEvent<HTMLInputElement>) => {
handleAddFiles(event.target.files);
event.target.value = "";
}}
/>
</label>
</div>
<div className="flex h-9 items-center gap-3 pl-2">
<button
type="button"
className={cn(
"grid h-8 w-8 shrink-0 place-items-center rounded-full p-0 leading-none transition-colors duration-150 focus:outline-none focus:ring-0",
isAgentModeActive
? "bg-[#eaf1ff] text-[#2563eb] shadow-[inset_0_0_0_1px_rgba(37,99,235,0.16)] dark:bg-[#102042] dark:text-[#8db4ff] dark:shadow-[inset_0_0_0_1px_rgba(141,180,255,0.18)]"
: "text-[#20242a] hover:bg-black/5 dark:text-white/72 dark:hover:bg-white/8"
)}
aria-label="开启 Agent 模式"
onClick={onStartAgentMode}
title="开启 Agent 模式"
>
<Lightbulb className="block h-4 w-4" strokeWidth={1.9} />
</button>
<Select value={selectedModelId} onValueChange={onModelChange}>
<SelectTrigger
aria-label="选择模型"
className="!flex !h-8 !w-8 shrink-0 !items-center !justify-center rounded-full border-0 !bg-transparent !p-0 text-[#20242a] leading-none shadow-none transition-colors duration-150 hover:bg-black/5 focus:outline-none focus:ring-0 focus:ring-offset-0 focus-visible:ring-0 focus-visible:ring-offset-0 dark:text-white/72 dark:hover:bg-white/8 [&>svg]:hidden"
title="选择模型"
>
<div className="flex h-8 w-8 items-center justify-center">
<Box className="block h-4 w-4" strokeWidth={1.9} />
</div>
</SelectTrigger>
<SelectContent
align="end"
sideOffset={10}
className="w-[200px] rounded-[12px] border-black/10 bg-white p-1 text-[#20242a] shadow-[0_18px_44px_rgba(15,23,42,0.14)] dark:border-white/10 dark:bg-[#141416] dark:text-white"
>
{modelOptions.map((model) => (
<SelectItem
key={model.id}
value={model.id}
className="h-8 rounded-[7px] text-[12px] [&>span:first-child]:hidden [&>span:last-child]:block [&>span:last-child]:w-full"
>
<span className="flex min-w-0 items-center gap-2">
<span className="flex h-5 w-5 shrink-0 items-center justify-center overflow-hidden rounded-full border border-black/10 bg-white text-[10px] font-medium text-[#6b7280] dark:border-white/15 dark:bg-white/8 dark:text-white/70">
{model.avatarUrl ? (
<img
alt=""
className="h-full w-full object-cover"
src={model.avatarUrl}
/>
) : (
(model.displayName || "A").slice(0, 1)
)}
</span>
<span className="min-w-0 flex-1 truncate">{model.displayName}</span>
</span>
</SelectItem>
))}
</SelectContent>
</Select>
<Button
onClick={onSubmit}
disabled={isSubmitting || (!prompt.trim() && files.length === 0)}
className="h-9 w-9 rounded-full bg-[#eeeeef] p-0 text-[#c2c7cf] transition-colors duration-150 hover:bg-[#e5e6e8] hover:text-[#7f8794] dark:bg-white/10 dark:text-white/40 dark:hover:bg-white/16"
>
{isSubmitting ? (
<Loader2 className="h-4.5 w-4.5 animate-spin" />
) : (
<ArrowUp className="h-4 w-4" strokeWidth={1.8} />
)}
</Button>
</div>
</div>
</div>
{homeAnimationData?.enabled && homeAnimationData?.htmlContent ? (
<div className="mt-6 w-full">
<HomeAnimationEmbed htmlContent={homeAnimationData.htmlContent} />
</div>
) : null}
</section>
);
}