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

218 lines
8.8 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, 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>
);
}