244 lines
11 KiB
TypeScript
244 lines
11 KiB
TypeScript
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>
|
||
);
|
||
}
|