LehongWu's picture
Upload folder using huggingface_hub
a4b1a9c verified
import { FormEvent, useMemo, useRef, useState } from "react";
import { apiFetch } from "../api";
import { DownloadMediaButton } from "../components/DownloadMediaButton";
import { ElapsedTimer } from "../components/ElapsedTimer";
import { ImageSlot } from "../components/ImageSlot";
import { useGenerationOptions } from "../context/GenerationOptionsContext";
const FLASH25_ID = "gemini-2.5-flash-image";
const FLASH_ID = "gemini-3.1-flash-image-preview";
const PRO_ID = "gemini-3-pro-image-preview";
/** Order: older flash → Nano Banana 2 (default) → long-thinking → Pro */
type ImageGenVariant = "nb25-minimal" | "flash-minimal" | "flash-high" | "pro-high";
export function ImageGen() {
const opts = useGenerationOptions();
const nano25Label =
opts.image.models.find((m) => m.value === FLASH25_ID)?.label ?? "Nano Banana";
const flashLabel =
opts.image.models.find((m) => m.value === FLASH_ID)?.label ?? "Nano Banana 2";
const proLabel =
opts.image.models.find((m) => m.value === PRO_ID)?.label ?? "Nano Banana Pro";
const [prompt, setPrompt] = useState("");
const [variant, setVariant] = useState<ImageGenVariant>("flash-minimal");
const [aspect, setAspect] = useState(opts.image.aspect_ratios[0] ?? "1:1");
const [res, setRes] = useState(opts.image.resolutions[0] ?? "1K");
const [f0, setF0] = useState<File | null>(null);
const [f1, setF1] = useState<File | null>(null);
const [f2, setF2] = useState<File | null>(null);
const [busy, setBusy] = useState(false);
const [err, setErr] = useState<string | null>(null);
const [out, setOut] = useState<string | null>(null);
const formRef = useRef<HTMLFormElement>(null);
const variantOptions = useMemo(
() => [
{ id: "nb25-minimal" as const, label: nano25Label },
{ id: "flash-minimal" as const, label: flashLabel },
{ id: "flash-high" as const, label: `${flashLabel}(长思考)` },
{ id: "pro-high" as const, label: `${proLabel}(长思考)` },
],
[nano25Label, flashLabel, proLabel],
);
async function onSubmit(e: FormEvent) {
e.preventDefault();
setErr(null);
setOut(null);
if (!prompt.trim()) {
setErr("请填写提示词。");
return;
}
let model: string;
let thinking: "minimal" | "high";
switch (variant) {
case "nb25-minimal":
model = FLASH25_ID;
thinking = "minimal";
break;
case "flash-minimal":
model = FLASH_ID;
thinking = "minimal";
break;
case "flash-high":
model = FLASH_ID;
thinking = "high";
break;
case "pro-high":
model = PRO_ID;
thinking = "high";
break;
}
const fd = new FormData();
fd.append("prompt", prompt.trim());
fd.append("model", model);
fd.append("aspect_ratio", aspect);
fd.append("resolution", res);
fd.append("thinking_level", thinking);
if (f0) fd.append("image_0", f0);
if (f1) fd.append("image_1", f1);
if (f2) fd.append("image_2", f2);
setBusy(true);
try {
const r = await apiFetch("/api/generate/image", {
method: "POST",
body: fd,
});
const j = await r.json().catch(() => ({}));
if (!r.ok) {
throw new Error((j as { detail?: string }).detail || r.statusText);
}
const b64 = (j as { image_base64: string; mime_type: string }).image_base64;
const mime = (j as { mime_type: string }).mime_type;
setOut(`data:${mime};base64,${b64}`);
} catch (e: unknown) {
setErr(e instanceof Error ? e.message : "请求失败");
} finally {
setBusy(false);
}
}
return (
<div className="max-w-3xl">
<h1 className="font-display text-2xl font-semibold text-ink mb-2">图片编辑或生成</h1>
<div className="mb-6 space-y-1.5">
<p className="text-mist text-sm leading-relaxed">
填写提示词;参考图最多三张。再选比例与分辨率。
</p>
<p className="text-xs text-mist leading-relaxed">
{nano25Label} 为稳定版;{flashLabel} 与 {proLabel} 为实验性,接口或效果可能变更。
</p>
</div>
<form ref={formRef} onSubmit={onSubmit} className="space-y-5">
<fieldset
disabled={busy}
className="border-0 p-0 m-0 min-w-0 space-y-5 disabled:pointer-events-none disabled:opacity-[0.92]"
>
<div>
<label className="block text-sm font-semibold text-ink mb-1">模型</label>
<select
className="w-full max-w-md rounded-lg border border-slate-300 bg-white px-2 py-2 text-sm"
value={variant}
onChange={(e) => setVariant(e.target.value as ImageGenVariant)}
>
{variantOptions.map((opt) => (
<option key={opt.id} value={opt.id}>
{opt.label}
</option>
))}
</select>
<p className="text-xs text-mist mt-1.5">
{nano25Label} 为上一代快速;{flashLabel} 为默认推荐;{flashLabel}(长思考)与 {proLabel}(长思考)为长思考模式,更慢、更细;{proLabel} 为 Pro 路线。
</p>
</div>
<div>
<label className="block text-sm font-semibold text-ink mb-1">提示词(必填)</label>
<textarea
className="w-full min-h-[120px] rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-clay/50"
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
placeholder="填写画面描述…"
/>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<label className="block text-sm font-semibold text-ink mb-1">宽高比</label>
<select
className="w-full rounded-lg border border-slate-300 bg-white px-2 py-2 text-sm"
value={aspect}
onChange={(e) => setAspect(e.target.value)}
>
{opts.image.aspect_ratios.map((a) => (
<option key={a} value={a}>
{a}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-semibold text-ink mb-1">分辨率</label>
<select
className="w-full rounded-lg border border-slate-300 bg-white px-2 py-2 text-sm"
value={res}
onChange={(e) => setRes(e.target.value)}
>
{opts.image.resolutions.map((x) => (
<option key={x} value={x}>
{x}
</option>
))}
</select>
</div>
</div>
<div>
<p className="text-sm font-semibold text-ink mb-2">参考图(最多三张)</p>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
<ImageSlot label="参考图 1" file={f0} onChange={setF0} />
<ImageSlot label="参考图 2" file={f1} onChange={setF1} />
<ImageSlot label="参考图 3" file={f2} onChange={setF2} />
</div>
</div>
<div className="flex items-center gap-4">
<button
type="submit"
className="rounded-lg bg-ink text-paper px-5 py-2 text-sm font-medium hover:bg-slate-800 disabled:opacity-40"
>
{busy ? "生成中…" : "生成图片"}
</button>
<ElapsedTimer active={busy} />
</div>
</fieldset>
{err && <p className="text-sm text-red-700">{err}</p>}
</form>
{out && (
<div className="mt-10 border-t border-slate-200 pt-8">
<h2 className="font-display text-xl font-semibold text-ink mb-3">生成结果</h2>
<div className="mb-3 flex flex-wrap items-center gap-3">
<DownloadMediaButton dataUrl={out} filenameBase="image-gen" label="下载图片" />
<button
type="button"
onClick={() => formRef.current?.requestSubmit()}
className="rounded-lg border border-slate-300 bg-white px-5 py-2 text-sm font-medium text-ink hover:bg-slate-50"
>
重新生成
</button>
</div>
<img
src={out}
alt="生成结果"
className="max-w-full rounded-xl border border-slate-200 shadow-lg"
/>
</div>
)}
</div>
);
}