226 lines
9.6 KiB
TypeScript
226 lines
9.6 KiB
TypeScript
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>
|
||
);
|
||
}
|