"use client"; import { useEffect, useMemo, useState } from "react"; 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 = { idle: "正在创建图片", queued: "等待开始生成", processing: "生成初稿中", finalizing: "正在润饰细节", failed: "生成失败", completed: "生成完成", }; const GENERATION_STATUS_TITLES = [ "正在理解画面关系", "正在读取参考图片", "正在分析主体轮廓", "正在识别关键元素", "正在整理构图线索", "正在确认画面比例", "正在匹配视觉风格", "正在提取材质细节", "正在梳理光影方向", "正在构建初始画面", "正在生成第一版构图", "正在安排主体位置", "正在校准画面重心", "正在补全背景层次", "正在保持主体一致", "正在优化边缘结构", "正在调整透视关系", "正在细化空间层次", "正在增强画面清晰度", "正在统一色彩倾向", "正在检查参考一致性", "正在处理局部细节", "正在补充纹理信息", "正在修正不自然区域", "正在平衡明暗对比", "正在优化产品质感", "正在调整阴影落点", "正在整理背景干扰", "正在增强真实感", "正在重绘细小结构", "正在合成高清结果", "正在检查主体完整性", "正在优化画面边缘", "正在处理反光细节", "正在提升材质表现", "正在微调构图节奏", "正在清理多余噪点", "正在强化视觉焦点", "正在校正颜色偏差", "正在保留参考特征", "正在生成高分辨率版本", "正在压实画面层次", "正在修饰局部瑕疵", "正在检查文字与图形", "正在稳定画面结构", "正在优化细节密度", "正在完善背景过渡", "正在增强主体质感", "正在统一整体光感", "正在检查比例关系", "正在修正边缘锯齿", "正在提升清晰观感", "正在处理最后细节", "正在准备预览结果", "正在同步生成进度", "正在确认输出尺寸", "正在整理结果文件", "正在校验图片可用性", "正在生成最终预览", "正在完成高清渲染", "正在保存生成结果", "正在写入画布节点", "正在同步到对话框", "正在检查结果完整性", "正在收尾生成任务", "正在等待服务商返回", "正在保持连接状态", "正在确认生成结果", "正在拉取图片地址", "正在准备展示图片", "正在完成最后处理", "正在刷新画布内容", "正在整理图片信息", "正在完成本次生成", "正在即将展示结果", ]; const LONG_WAIT_TITLE_THRESHOLD_MS = 5 * 60 * 1000; const LONG_WAIT_STATUS_TITLES = [ "服务繁忙,排队处理中", "高峰期生成较慢,请耐心等候", "仍在保持连接,等待模型返回", "结果返回后会自动展示", ]; const STATUS_TITLE_DURATIONS_MS = GENERATION_STATUS_TITLES.map((_, index) => { const rhythm = [3600, 5200, 4100, 6100, 3300, 4700, 5700, 3900, 6800, 4400]; return rhythm[index % rhythm.length]; }); const TYPEWRITER_START_DELAY_MS = 120; const TYPEWRITER_BASE_STEP_MS = 46; function clampProgress(value: number) { if (!Number.isFinite(value)) return 0; return Math.max(0, Math.min(100, Math.round(value))); } function estimateProgress(phase: GenerationPhase, elapsedMs: number) { if (phase === "completed" || phase === "failed") return 100; if (phase === "finalizing") return 96; if (phase === "queued") { return clampProgress(6 + Math.min(12, elapsedMs / 3500)); } if (phase === "processing") { const eased = 1 - Math.exp(-elapsedMs / 85_000); return clampProgress(18 + eased * 78); } return clampProgress(Math.min(8, elapsedMs / 1000)); } function getStatusTitleState(elapsedMs: number, initialTitle?: string) { const titles = initialTitle ? [ initialTitle, ...GENERATION_STATUS_TITLES.filter((item) => item !== initialTitle), ] : GENERATION_STATUS_TITLES; const durations = initialTitle ? [4200, ...STATUS_TITLE_DURATIONS_MS] : STATUS_TITLE_DURATIONS_MS; let cursor = Math.max(0, elapsedMs); for (let index = 0; index < titles.length; index += 1) { const duration = durations[index] || 4600; if (cursor < duration) { return { elapsedInTitle: cursor, index, title: titles[index] || titles[titles.length - 1] || PHASE_TITLES.processing, }; } cursor -= duration; } return { elapsedInTitle: durations[durations.length - 1] || 4600, index: titles.length - 1, title: titles[titles.length - 1] || PHASE_TITLES.processing, }; } function getLongWaitTitleState(elapsedMs: number, initialTitle?: string) { const titles = initialTitle ? [ initialTitle, ...LONG_WAIT_STATUS_TITLES.filter((item) => item !== initialTitle), ] : LONG_WAIT_STATUS_TITLES; const elapsedAfterThreshold = Math.max( 0, elapsedMs - LONG_WAIT_TITLE_THRESHOLD_MS ); const duration = 7600; const index = Math.floor(elapsedAfterThreshold / duration) % titles.length; return { elapsedInTitle: elapsedAfterThreshold % duration, index, title: titles[index] || titles[0] || PHASE_TITLES.processing, }; } function getTypewriterText(text: string, elapsedInTitle: number, index: number) { const chars = Array.from(text); if (chars.length === 0) return ""; const step = TYPEWRITER_BASE_STEP_MS + (index % 5) * 8; const visibleCount = Math.max( 1, Math.min( chars.length, Math.floor( Math.max(0, elapsedInTitle - TYPEWRITER_START_DELAY_MS) / step ) + 1 ) ); return chars.slice(0, visibleCount).join(""); } 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 [elapsedMs, setElapsedMs] = useState(0); useEffect(() => { const startedAt = Date.now(); const timer = window.setInterval(() => { setElapsedMs(Date.now() - startedAt); }, 90); return () => window.clearInterval(timer); }, [phase]); const resolvedProgress = clampProgress( typeof progress === "number" ? progress : estimateProgress(phase, elapsedMs) ); const isLongWaitTitle = (phase === "processing" || phase === "finalizing") && elapsedMs >= LONG_WAIT_TITLE_THRESHOLD_MS; const statusTitleState = isLongWaitTitle ? getLongWaitTitleState(elapsedMs, title) : getStatusTitleState(elapsedMs, title); const rotatingTitle = getTypewriterText( statusTitleState.title, statusTitleState.elapsedInTitle, statusTitleState.index ); const resolvedTitle = phase === "processing" || phase === "finalizing" ? rotatingTitle : title || PHASE_TITLES[phase]; const playSequence = phase === "processing" || phase === "finalizing"; const progressStyle = useMemo( () => ({ width: `${resolvedProgress}%` }), [resolvedProgress] ); return (
{resolvedTitle}
{showProgress ? (
{phase === "failed" ? "已停止" : "生成进度"} {resolvedProgress}%
) : null}
); }