218 lines
8.8 KiB
TypeScript
218 lines
8.8 KiB
TypeScript
import { ArrowUp, Loader2, Sparkles, Upload } from "lucide-react";
|
||
import type { ChangeEvent } from "react";
|
||
import { HomeAnimationEmbed } from "@/components/home/home-animation-embed";
|
||
import { ImageParamsBar, type ImageGenerationParams } from "@/components/image-params-bar";
|
||
import { Button } from "@/components/ui/button";
|
||
import { Textarea } from "@/components/ui/textarea";
|
||
import type { ModelOption, UploadFileItem } from "./types";
|
||
import type { PROMPT_TAGS } from "./utils";
|
||
|
||
type PromptTag = (typeof PROMPT_TAGS)[number];
|
||
|
||
type LovartHeroComposerProps = {
|
||
composerPlaceholder: string;
|
||
displayLogo: string;
|
||
files: UploadFileItem[];
|
||
handleAddFiles: (files: FileList | null) => void;
|
||
handleImageParamsChange: (nextParams: ImageGenerationParams) => void;
|
||
heroPromoActionText?: string | null;
|
||
heroPromoText?: string | null;
|
||
heroSubtitleText?: string | null;
|
||
heroTitleText?: string | null;
|
||
homeAnimationData?: {
|
||
enabled?: boolean;
|
||
htmlContent?: string;
|
||
fileName?: string;
|
||
};
|
||
imageParams: ImageGenerationParams;
|
||
isMembershipEnabled: boolean;
|
||
isSubmitting: boolean;
|
||
onSubmit: () => void;
|
||
openUpgradeDialog: () => void;
|
||
prompt: string;
|
||
removeFile: (id: string) => void;
|
||
setPrompt: (value: string) => void;
|
||
shouldInvertLogo: boolean;
|
||
siteName: string;
|
||
triggerComposerModel: ModelOption;
|
||
visiblePromptTags: PromptTag[];
|
||
};
|
||
|
||
export function LovartHeroComposer({
|
||
composerPlaceholder,
|
||
displayLogo,
|
||
files,
|
||
handleAddFiles,
|
||
handleImageParamsChange,
|
||
heroPromoActionText,
|
||
heroPromoText,
|
||
heroSubtitleText,
|
||
heroTitleText,
|
||
homeAnimationData,
|
||
imageParams,
|
||
isMembershipEnabled,
|
||
isSubmitting,
|
||
onSubmit,
|
||
openUpgradeDialog,
|
||
prompt,
|
||
removeFile,
|
||
setPrompt,
|
||
shouldInvertLogo,
|
||
siteName,
|
||
triggerComposerModel,
|
||
visiblePromptTags,
|
||
}: 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 ${shouldInvertLogo ? "brightness-0 invert" : ""}`}
|
||
/>
|
||
) : (
|
||
<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-6 w-full max-w-[780px] rounded-[22px] border border-black/10 bg-white px-4 pb-3 pt-4 shadow-[0_10px_24px_rgba(0,0,0,0.05)] 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-[76px] resize-none border-0 bg-transparent px-1 text-[12px] shadow-none focus-visible:ring-0 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 items-center justify-between border-t border-black/8 pt-3 dark:border-white/8">
|
||
<div className="flex min-w-0 flex-1 items-center gap-1.5">
|
||
<label className="inline-flex h-9 w-9 cursor-pointer items-center justify-center rounded-full border border-black/10 text-black/60 hover:bg-black/5 dark:border-white/12 dark:text-white/60 dark:hover:bg-white/8">
|
||
<Upload className="h-4 w-4" />
|
||
<input
|
||
type="file"
|
||
accept="image/*"
|
||
multiple
|
||
className="hidden"
|
||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||
handleAddFiles(event.target.files);
|
||
event.target.value = "";
|
||
}}
|
||
/>
|
||
</label>
|
||
|
||
<div className="inline-flex h-8 min-w-[130px] items-center gap-1.5 rounded-full border border-blue-100 bg-blue-50 px-2 text-xs text-blue-600 dark:border-sky-500/20 dark:bg-sky-500/12 dark:text-sky-200">
|
||
<span className="h-5 w-5 overflow-hidden rounded-full border border-blue-200 bg-white dark:border-sky-500/30 dark:bg-[#0d1117]">
|
||
{triggerComposerModel?.avatarUrl ? (
|
||
<img
|
||
src={triggerComposerModel.avatarUrl}
|
||
alt={triggerComposerModel.displayName || "模型头像"}
|
||
className="h-full w-full object-cover"
|
||
/>
|
||
) : (
|
||
<span className="flex h-full w-full items-center justify-center text-[10px] font-medium text-blue-500">
|
||
{(triggerComposerModel?.displayName || "A").slice(0, 1)}
|
||
</span>
|
||
)}
|
||
</span>
|
||
<span className="max-w-[95px] truncate">
|
||
{triggerComposerModel?.displayName || "Agent模式"}
|
||
</span>
|
||
</div>
|
||
|
||
<div className="min-w-0 flex-1 overflow-hidden">
|
||
<ImageParamsBar
|
||
modelName={triggerComposerModel?.displayName}
|
||
initialParams={imageParams}
|
||
onParamsChange={handleImageParamsChange}
|
||
inline
|
||
providerType={triggerComposerModel?.providerType}
|
||
imageGenConfig={triggerComposerModel?.imageGenConfig || undefined}
|
||
hideImageCount
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex items-center gap-1.5 pl-2">
|
||
<Button
|
||
onClick={onSubmit}
|
||
disabled={isSubmitting || (!prompt.trim() && files.length === 0)}
|
||
className="h-8 w-8 rounded-full bg-[#9ca3af] p-0 text-white hover:bg-black dark:bg-white/16 dark:hover:bg-white/26"
|
||
>
|
||
{isSubmitting ? (
|
||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||
) : (
|
||
<ArrowUp className="h-3.5 w-3.5" />
|
||
)}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="mt-4 flex flex-wrap items-center justify-center gap-1.5">
|
||
{visiblePromptTags.map((tag) => (
|
||
<button
|
||
key={tag.label}
|
||
type="button"
|
||
className={`inline-flex items-center gap-1.5 rounded-full border px-3 py-1 text-[12px] ${
|
||
tag.highlighted
|
||
? "border-[#ffb15f] bg-[#fff4e7] text-[#ff8c1f] dark:border-[#8b6330] dark:bg-[#38270f] dark:text-[#ffbe6b]"
|
||
: "border-black/15 bg-white text-black/70 dark:border-white/10 dark:bg-[#141416] dark:text-white/70"
|
||
}`}
|
||
>
|
||
<tag.icon className="h-3 w-3" />
|
||
{tag.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
{homeAnimationData?.enabled && homeAnimationData?.htmlContent ? (
|
||
<div className="mt-6 w-full">
|
||
<HomeAnimationEmbed htmlContent={homeAnimationData.htmlContent} />
|
||
</div>
|
||
) : null}
|
||
</section>
|
||
);
|
||
}
|