用 useReducer 管理多参考图生成器的前端状态

单图生成页面可以用几个 useState 很快搭出来:文件、提示词、模型和 loading。当产品开始支持两到五张参考图,并允许用户为图片指定主体、材质或环境角色后,这套状态很容易失控。

最常见的问题不是页面报错,而是状态彼此矛盾:界面显示多图模式,队列里却只有一张图;切换模型后仍保留不支持的分辨率;任务已经提交,用户还能修改本次请求;结果返回后,原始提示词和参考图已经丢失。

这类界面更适合用 useReducer 管理一个明确的状态模型。

先区分"草稿"和"任务状态"

草稿仍可编辑,任务快照不可变:

ts 复制代码
type SourceRole =
  | "subject"
  | "material"
  | "environment"
  | "composition"
  | "lighting";

type SourceItem = {
  localId: string;
  file: File;
  role: SourceRole;
  assetId?: string;
  status: "local" | "uploading" | "ready" | "failed";
};

type Draft = {
  mode: "single" | "fusion";
  sources: SourceItem[];
  change: string;
  preserve: string[];
  model: string;
  aspectRatio: string;
  resolution: string;
};

用户点击生成时,将 Draft 转换成只读 RequestSnapshot。后续即使界面允许准备下一次尝试,也不能改变正在处理的任务记录。

用联合类型代替多个布尔值

isUploadingisGeneratinghasResulthasError 互相独立时,会组合出大量不合法状态。可区分联合类型更容易限制分支:

ts 复制代码
type EditorState =
  | { type: "editing"; draft: Draft }
  | { type: "uploading"; draft: Draft }
  | { type: "ready"; draft: Draft }
  | { type: "submitting"; request: RequestSnapshot }
  | { type: "processing"; request: RequestSnapshot; jobId: string }
  | {
      type: "reviewing";
      request: RequestSnapshot;
      jobId: string;
      resultUrl: string;
    }
  | {
      type: "failed";
      draft: Draft;
      stage: "upload" | "validation" | "submission" | "provider";
      message: string;
    };

reviewing 分支中,组件一定能同时拿到结果和原始请求;在 processing 分支中,一定有可查询的 jobId。TypeScript 也能通过穷尽检查提醒遗漏状态。

reducer 负责维护不变量

事件不应直接修改任意字段,而应维护模式和图片数量之间的不变量:

ts 复制代码
type Action =
  | { type: "SET_MODE"; mode: Draft["mode"] }
  | { type: "ADD_SOURCE"; source: SourceItem }
  | { type: "REMOVE_SOURCE"; localId: string }
  | { type: "SET_ROLE"; localId: string; role: SourceRole }
  | { type: "APPLY_CAPABILITY"; capability: ModeCapability }
  | { type: "SUBMIT"; request: RequestSnapshot }
  | { type: "JOB_ACCEPTED"; jobId: string }
  | { type: "RESULT_READY"; resultUrl: string }
  | {
      type: "FAIL";
      stage: "upload" | "validation" | "submission" | "provider";
      message: string;
    };

例如切回单图模式时,不应该静默保留五张图。产品可以阻止切换并要求用户选择主图,也可以保留第一张并给出明确提示。关键是这个决策只能存在于 reducer 的一个分支里,而不是散落在上传区、模型选择器和提交按钮中。

能力变化时只修正失效字段

模型切换会影响输入数量、宽高比、分辨率、登录要求和积分成本。收到新的能力分支后,可以执行一次归一化:

ts 复制代码
function normalizeDraft(draft: Draft, cap: ModeCapability): Draft {
  return {
    ...draft,
    sources: draft.sources.slice(0, cap.maxSources),
    aspectRatio: cap.aspectRatios.includes(draft.aspectRatio)
      ? draft.aspectRatio
      : cap.aspectRatios[0],
    resolution: cap.resolutions.includes(draft.resolution)
      ? draft.resolution
      : cap.resolutions[0],
  };
}

仍合法的选择继续保留,失效字段才被替换。界面还应展示一条变更说明,例如"当前模型不支持原分辨率,已切换为 HD",避免用户误以为配置被随机清空。

图片角色也是表单状态

多张参考图不能只显示缩略图和删除按钮。每张图需要一个明确角色,发生冲突时还需要优先级。

角色选择应该参与提交前校验。例如融合模式下:

  • 至少有一个 subject
  • 同一强约束角色是否允许重复由产品规则决定;
  • 图片数量符合当前能力;
  • 所有图片状态都是 ready
  • 修改与保留条件不为空。

这些规则用于提前反馈,但服务端仍需最终校验。前端合法不等于服务端一定接受,因为能力和账户状态可能在页面打开后变化。

reviewing 不能省略

生成结果返回后,不要直接把状态改成 completed。在 reviewing 中同时展示:

  • 原参考图及其角色;
  • 修改条件和保留条件;
  • 模型、模式、比例和分辨率;
  • 生成结果;
  • 接受、下载或基于本次请求创建新尝试的操作。

"模型返回成功"只能证明输出存在,不能证明人脸、文字、商品边缘、反射、重复纹理和透视都正确。

失败后恢复草稿

上传失败应保留其他已经完成的资源;能力冲突应刷新选项并标出失效字段;登录中断后应恢复草稿;超时后应继续查询原任务,而不是马上重复提交;结果不满意时应从请求快照派生新草稿。

恢复策略的原则很简单:保留用户输入,刷新外部状态。

实际边界

当前审阅的产品界面中,单图模式接收一张图片,多图融合接收两到五张;支持 JPEG、PNG 和 WebP,单文件公开限制为 24 MB。模型、模式、分辨率、账号要求、积分成本和可用性会变化,高分辨率输出只适用于符合条件的组合。

生成结果也不能保证完美转换、精确身份保持、无瑕疵、唯一性、固定耗时、完全遵循提示词或商业权利无风险。

我参与的 Image to Image Generator 是本文的实现背景。可复用的重点不是某个具体页面,而是把草稿、能力归一化、异步任务和人工复核放进同一套状态模型。

相关推荐
circuitsosk1 小时前
智能体任务拆解与执行:基于ReAct+Plan-and-Execute框架的行业Skill构建实录
前端·javascript·python·react.js·react·llm agent·智能体编排
zzzzzz3104 小时前
36K stars 的“酷炫组件”,到底该怎么用才不显得用力过猛?
前端·react.js·动效
红尘散仙13 小时前
TypeScript WIT Guest:类型约束与 ComponentizeJS 工程化
rust·typescript·webassembly
红尘散仙14 小时前
从 dsh 的插件系统出发:为什么我要探索 WIT 与 Wasm Component Model
rust·typescript·webassembly
红尘散仙14 小时前
从 WIT 到 Wasmtime:构建 Rust Component 工程
rust·typescript·webassembly
记忆张量MemTensor16 小时前
产品更新|MemOS 现已支持 DeepSeek Harness 长期记忆接入
人工智能·typescript·开源·agent
半个落月19 小时前
从 "use client" 到 Route Handler:用 Todos 理解 Next.js 水合与全栈请求
前端·react.js·next.js
记忆张量MemTensor21 小时前
MemOS Skill 上线|一句话即可接入 MemOS Cloud
大数据·数据库·人工智能·typescript·开源
ITmaster07311 天前
Vibe Coding 时代:Vue 消失了还是 React 太强?
前端·vue.js·react.js