113 lines
3.5 KiB
TypeScript
113 lines
3.5 KiB
TypeScript
"use client";
|
|
|
|
import { BloubAvatar } from "@/components/bloub-avatar";
|
|
import type { StateId } from "@/lib/bloub-official/states";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
type GenerationPhase =
|
|
| "idle"
|
|
| "queued"
|
|
| "processing"
|
|
| "finalizing"
|
|
| "failed"
|
|
| "completed";
|
|
|
|
interface GenerationDotPlaceholderProps {
|
|
className?: string;
|
|
compact?: boolean;
|
|
phase?: GenerationPhase;
|
|
progress?: number | null;
|
|
showProgress?: boolean;
|
|
title?: string;
|
|
viewportScale?: number;
|
|
}
|
|
|
|
const PHASE_TITLES: Record<GenerationPhase, string> = {
|
|
idle: "正在创建图片",
|
|
queued: "等待开始生成",
|
|
processing: "等待模型返回图片",
|
|
finalizing: "正在保存生成结果",
|
|
failed: "生成失败",
|
|
completed: "生成完成",
|
|
};
|
|
|
|
function clampProgress(value: number) {
|
|
if (!Number.isFinite(value)) return 0;
|
|
return Math.max(0, Math.min(100, Math.round(value)));
|
|
}
|
|
|
|
function getBloubState(phase: GenerationPhase): StateId {
|
|
if (phase === "failed") return "alert";
|
|
if (phase === "completed") return "wink";
|
|
if (phase === "queued") return "thinking";
|
|
return "idle";
|
|
}
|
|
|
|
export function GenerationDotPlaceholder({
|
|
className,
|
|
compact = false,
|
|
phase = "processing",
|
|
progress,
|
|
showProgress = true,
|
|
title,
|
|
}: GenerationDotPlaceholderProps) {
|
|
const resolvedProgress = typeof progress === "number" && Number.isFinite(progress)
|
|
? clampProgress(progress) : null;
|
|
const resolvedTitle = title || PHASE_TITLES[phase];
|
|
const playSequence = phase === "processing" || phase === "finalizing";
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
"relative isolate overflow-hidden rounded-[24px] bg-[#f4f4f4] text-[#565b63] dark:bg-[#08090b] dark:text-zinc-200",
|
|
compact ? "min-h-[190px] p-5" : "min-h-[360px] p-8",
|
|
className
|
|
)}
|
|
>
|
|
<div className="flex h-full min-h-[inherit] flex-col">
|
|
<div
|
|
aria-live="polite"
|
|
className={cn(
|
|
"font-semibold",
|
|
compact
|
|
? "text-[12px] leading-[1.35]"
|
|
: "text-[18px] leading-[1.35]"
|
|
)}
|
|
>
|
|
{resolvedTitle}
|
|
</div>
|
|
|
|
<div className="flex min-h-0 flex-1 items-center justify-center py-3">
|
|
<BloubAvatar
|
|
label={resolvedTitle}
|
|
playSequence={playSequence}
|
|
size={compact ? 88 : 120}
|
|
state={getBloubState(phase)}
|
|
surface="page"
|
|
/>
|
|
</div>
|
|
|
|
{showProgress && phase !== "completed" ? (
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between text-[12px] font-medium text-[#7d838b] dark:text-zinc-400">
|
|
<span>{phase === "failed" ? "已停止" : resolvedProgress === null ? "等待结果,返回后自动展示" : "生成进度"}</span>
|
|
{resolvedProgress !== null ? <span>{resolvedProgress}%</span> : null}
|
|
</div>
|
|
{resolvedProgress !== null ? <div role="progressbar" aria-valuenow={resolvedProgress} aria-valuemin={0} aria-valuemax={100} className="h-1.5 overflow-hidden rounded-full bg-white/80 shadow-[inset_0_0_0_1px_rgba(0,0,0,0.035)] dark:bg-white/10 dark:shadow-none">
|
|
<div
|
|
className={cn(
|
|
"h-full rounded-full transition-[width] duration-700 ease-out",
|
|
phase === "failed"
|
|
? "bg-[#c7ccd3] dark:bg-zinc-600"
|
|
: "bg-[#0a0a0c] dark:bg-[#3ecf8e]"
|
|
)}
|
|
style={{ width: `${resolvedProgress}%` }}
|
|
/>
|
|
</div> : null}
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|