feat(toolbar): add reverse prompt feature to image node toolbar for generating prompt text

This commit is contained in:
HouYunFei
2026-06-04 17:25:30 +08:00
parent 51df099936
commit 6d3b89c842
5 changed files with 75 additions and 4 deletions
@@ -89,6 +89,12 @@ const CONNECTION_NODE_HIT_PADDING = 32;
const NODE_STATUS_LOADING = "loading" as const;
const NODE_STATUS_SUCCESS = "success" as const;
const NODE_STATUS_ERROR = "error" as const;
const IMAGE_PROMPT_REVERSE_PRESET = `请根据参考图片反推一段适合用于 AI 生图的提示词。
要求:
1. 只输出提示词正文,不要解释。
2. 覆盖主体、构图、风格、光线、色彩、材质、镜头和氛围。
3. 尽量写成可直接用于生图模型的完整提示词。`;
function createCanvasNode(type: CanvasNodeType, position: Position, metadata?: CanvasNodeMetadata): CanvasNodeData {
const spec = getNodeSpec(type);
@@ -1441,6 +1447,53 @@ function InfiniteCanvasPage() {
[addAsset, message],
);
const createImageReversePromptNodes = useCallback(
(node: CanvasNodeData) => {
if (node.type !== CanvasNodeType.Image || !node.metadata?.content) {
message.warning("图片节点为空,无法反推提示词");
return;
}
const gap = 96;
const textSpec = NODE_DEFAULT_SIZE[CanvasNodeType.Text];
const configSpec = NODE_DEFAULT_SIZE[CanvasNodeType.Config];
const centerY = node.position.y + node.height / 2;
const textNode = {
...createCanvasNode(
CanvasNodeType.Text,
{ x: node.position.x + node.width + gap + textSpec.width / 2, y: centerY },
{ content: IMAGE_PROMPT_REVERSE_PRESET, prompt: IMAGE_PROMPT_REVERSE_PRESET, status: NODE_STATUS_SUCCESS, fontSize: 14 },
),
title: "反推提示词",
};
const configNode = {
...createCanvasNode(
CanvasNodeType.Config,
{ x: textNode.position.x + textNode.width + gap + configSpec.width / 2, y: centerY },
{
generationMode: "text",
model: effectiveConfig.textModel || effectiveConfig.model || defaultConfig.textModel,
count: 1,
composerContent: `参考图片:@[node:${node.id}]\n任务说明:@[node:${textNode.id}]`,
},
),
title: "反推提示词配置",
};
setNodes((prev) => [...prev, textNode, configNode]);
setConnections((prev) => [
...prev,
{ id: nanoid(), fromNodeId: node.id, toNodeId: configNode.id },
{ id: nanoid(), fromNodeId: textNode.id, toNodeId: configNode.id },
]);
setSelectedNodeIds(new Set([configNode.id]));
setSelectedConnectionId(null);
setDialogNodeId(configNode.id);
setContextMenu(null);
},
[effectiveConfig.model, effectiveConfig.textModel, message],
);
const cropImageNode = useCallback(async (node: CanvasNodeData, crop: CanvasImageCropRect) => {
if (!node.metadata?.content) return;
const cropped = await cropDataUrl(node.metadata.content, crop);
@@ -2387,6 +2440,7 @@ function InfiniteCanvasPage() {
onSuperResolve={(node) => setSuperResolveNodeId(node.id)}
onAngle={(node) => setAngleNodeId(node.id)}
onViewImage={(node) => setPreviewNodeId(node.id)}
onReversePrompt={createImageReversePromptNodes}
onRetry={(node) => void handleRetryNode(node)}
onToggleFreeResize={(node) => toggleNodeFreeResize(node.id)}
onDelete={(node) => deleteNodes(new Set([node.id]))}
@@ -1,11 +1,11 @@
"use client";
import type { ReactNode } from "react";
import { Brush, Camera, Copy, Lock, LockOpen, Maximize2, Scissors, Sparkles, Upload, ZoomIn } from "lucide-react";
import { Brush, Camera, Copy, FileText, Lock, LockOpen, Maximize2, Scissors, Sparkles, Upload, ZoomIn } from "lucide-react";
import type { CanvasNodeData } from "../types";
export type ImageNodeActionToolId = "copyPrompt" | "replace" | "resize" | "maskEdit" | "crop" | "upscale" | "superResolve" | "angle" | "view";
export type ImageNodeActionToolId = "copyPrompt" | "reversePrompt" | "replace" | "resize" | "maskEdit" | "crop" | "upscale" | "superResolve" | "angle" | "view";
export type ImageQuickToolId = "info" | "delete" | "saveAsset" | "download" | "edit" | ImageNodeActionToolId;
export type ImageToolHandlers = {
@@ -18,6 +18,7 @@ export type ImageToolHandlers = {
onAngle: (node: CanvasNodeData) => void;
onViewImage: (node: CanvasNodeData) => void;
onCopyPrompt: (node: CanvasNodeData) => void;
onReversePrompt: (node: CanvasNodeData) => void;
};
export type ImageToolDefinition = {
@@ -36,7 +37,7 @@ export type ImageQuickToolsConfig = {
showLabels: boolean;
};
export const IMAGE_QUICK_TOOLS_STORAGE_KEY = "canvas-image-quick-tools-v4";
export const IMAGE_QUICK_TOOLS_STORAGE_KEY = "canvas-image-quick-tools-v5";
const defaultBaseToolIds: ImageQuickToolId[] = ["info", "delete", "saveAsset", "download", "edit"];
@@ -50,6 +51,15 @@ export const imageToolDefinitions: ImageToolDefinition[] = [
icon: () => <Copy className="size-4" />,
run: (node, handlers) => handlers.onCopyPrompt(node),
},
{
id: "reversePrompt",
defaultVisible: true,
panelLabel: "反推提示词",
label: "反推提示词",
title: "创建反推提示词的文本和配置节点",
icon: () => <FileText className="size-4" />,
run: (node, handlers) => handlers.onReversePrompt(node),
},
{
id: "replace",
defaultVisible: true,
@@ -32,6 +32,7 @@ type CanvasNodeHoverToolbarProps = {
onSuperResolve: (node: CanvasNodeData) => void;
onAngle: (node: CanvasNodeData) => void;
onViewImage: (node: CanvasNodeData) => void;
onReversePrompt: (node: CanvasNodeData) => void;
onRetry: (node: CanvasNodeData) => void;
onToggleFreeResize: (node: CanvasNodeData) => void;
onDelete: (node: CanvasNodeData) => void;
@@ -67,6 +68,7 @@ export function CanvasNodeHoverToolbar({
onSuperResolve,
onAngle,
onViewImage,
onReversePrompt,
onRetry,
onToggleFreeResize,
onDelete,
@@ -119,7 +121,7 @@ export function CanvasNodeHoverToolbar({
}
copyText(prompt, "提示词已复制");
};
const imageTools = buildImageToolbarTools(node, { onUpload, onToggleFreeResize, onMaskEdit, onCrop, onUpscale, onSuperResolve, onAngle, onViewImage, onCopyPrompt: copyImagePrompt });
const imageTools = buildImageToolbarTools(node, { onUpload, onToggleFreeResize, onMaskEdit, onCrop, onUpscale, onSuperResolve, onAngle, onViewImage, onCopyPrompt: copyImagePrompt, onReversePrompt });
function openImageToolSettings() {
onKeep(node.id);