ai-chatbot/components/home/lovart/lovart-hero-composer.tsx
2026-07-08 15:23:05 +08:00

226 lines
9.6 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 { ArrowUp, Box, Lightbulb, Loader2, Plus, Sparkles } from "lucide-react";
import type { ChangeEvent } from "react";
import { HomeAnimationEmbed } from "@/components/home/home-animation-embed";
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";
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-8 text-center">
{isMembershipEnabled ? (
<button
type="button"
onClick={openUpgradeDialog}
className="inline-flex items-center gap-2 rounded-full bg-[#fde8bf] px-4 py-2 text-sm text-black/80 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>
{heroPromoText || "现在升级,立享 Kling 3.0 与 Nano Banana Pro,最高 5 折优惠"}
<span className="text-[#ff7a00]">{heroPromoActionText || "立即升级"}</span>
</button>
) : (
<div className="inline-flex items-center gap-2 rounded-full bg-[#fde8bf] px-4 py-2 text-sm text-black/80 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>
{heroPromoText || "现在升级,立享 Kling 3.0 与 Nano Banana Pro,最高 5 折优惠"}
</div>
)}
<div className="mt-8 flex items-center justify-center gap-2">
{displayLogo ? (
<img
src={displayLogo}
alt={siteName}
className="h-10 w-10 rounded-full object-cover"
/>
) : (
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-black text-white dark:bg-white dark:text-black">
<Sparkles className="h-4 w-4" />
</div>
)}
<h1 className="text-[34px] font-semibold tracking-tight dark:text-white">
{heroTitleText || `${siteName || "Lovart"} 让设计更简单`}
</h1>
</div>
<p className="mt-2 text-[13px] text-black/35 dark:text-white/45">
{heroSubtitleText || "懂你的设计代理,帮你搞定一切"}
</p>
<div className="mx-auto mt-[34px] w-full max-w-[900px] rounded-[22px] border border-[#e1e4e8] bg-white px-[24px] pb-[12px] pt-[18px] shadow-[0_10px_26px_rgba(0,0,0,0.06)] dark:border-white/10 dark:bg-[#141416] dark:shadow-[0_18px_50px_rgba(0,0,0,0.35)]">
<Textarea
value={prompt}
onChange={(event) => setPrompt(event.target.value)}
placeholder={composerPlaceholder}
className="min-h-[78px] resize-none border-0 bg-transparent px-0 !text-[16px] leading-[1.4] text-[#20242a] shadow-none placeholder:text-[#b8b8b8] focus-visible:ring-0 md:!text-[16px] 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-12 w-12 overflow-hidden rounded-lg 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-xs text-white group-hover:flex"
onClick={() => removeFile(item.id)}
>
x
</button>
</div>
))}
</div>
)}
<div className="flex min-h-[54px] items-center justify-between pt-[12px]">
<div className="flex min-w-0 flex-1 items-center gap-4">
<label className="inline-flex h-9 w-9 cursor-pointer items-center justify-center rounded-full text-[#20242a] hover:bg-black/5 dark:text-white/72 dark:hover:bg-white/8">
<Plus className="h-5 w-5" 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-11 items-center gap-5 pl-2">
<button
type="button"
className={cn(
"grid h-9 w-9 shrink-0 place-items-center rounded-full p-0 leading-none transition-colors 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-5 w-5" strokeWidth={1.9} />
</button>
<Select value={selectedModelId} onValueChange={onModelChange}>
<SelectTrigger
aria-label="选择模型"
className="!flex !h-9 !w-9 shrink-0 !items-center !justify-center rounded-full border-0 !bg-transparent !p-0 text-[#20242a] leading-none shadow-none 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-9 w-9 items-center justify-center">
<Box className="block h-5 w-5" strokeWidth={1.9} />
</div>
</SelectTrigger>
<SelectContent
align="end"
sideOffset={10}
className="w-[210px] rounded-[18px] border-black/10 bg-white p-1.5 text-[#20242a] shadow-[0_22px_54px_rgba(15,23,42,0.16)] dark:border-white/10 dark:bg-[#141416] dark:text-white"
>
{modelOptions.map((model) => (
<SelectItem
key={model.id}
value={model.id}
className="h-9 rounded-[12px] text-[13px] [&>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-11 w-11 rounded-full bg-[#eeeeef] p-0 text-[#c9cdd4] hover:bg-[#e7e7e8] hover:text-[#8f96a3] 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-5 w-5" strokeWidth={1.8} />
)}
</Button>
</div>
</div>
</div>
{homeAnimationData?.enabled && homeAnimationData?.htmlContent ? (
<div className="mt-6 w-full">
<HomeAnimationEmbed htmlContent={homeAnimationData.htmlContent} />
</div>
) : null}
</section>
);
}