feat: 完善画布管理与快捷键体验
修改历史: - 为画布编辑器补齐节点复制粘贴、全选、追加选择、缩放、删除、生成等快捷键,并新增快捷键说明弹窗 - 支持粘贴剪贴板文本/图片为画布节点、拖入图片上传,以及多选节点复制时保留内部连接 - 在 /canvas 画布管理页新增编辑和删除操作,可编辑标题与说明,并支持删除当前画布 - 为画布项目新增 description 字段,补充数据库兼容迁移、列表展示、API 保存以及导入导出保留说明 - 优化画布节点、提示词引用、配置节点尺寸、生成任务状态与相关服务逻辑
This commit is contained in:
@@ -2,59 +2,126 @@ import type {
|
||||
CanvasConnection,
|
||||
CanvasImageNodeMetadata,
|
||||
CanvasNode,
|
||||
CanvasPromptNodeMetadata,
|
||||
} from "@/lib/canvas/types";
|
||||
|
||||
export type PromptMentionSource = {
|
||||
alias: string;
|
||||
token: string;
|
||||
nodeId: string;
|
||||
kind: "image" | "prompt";
|
||||
label: string;
|
||||
description: string;
|
||||
imageUrl: string;
|
||||
imageUrl?: string;
|
||||
prompt?: string;
|
||||
index: number;
|
||||
};
|
||||
|
||||
const mentionUrlPrefix = "canvas-image://";
|
||||
const imageMentionUrlPrefix = "canvas-image://";
|
||||
const promptMentionUrlPrefix = "canvas-prompt://";
|
||||
|
||||
export function getPromptMentionSources(
|
||||
nodes: CanvasNode[],
|
||||
connections: CanvasConnection[],
|
||||
promptNodeId: string,
|
||||
) {
|
||||
const imageById = new Map(
|
||||
): PromptMentionSource[] {
|
||||
const nodeById = new Map(nodes.map((node) => [node.id, node] as const));
|
||||
const sources: PromptMentionSource[] = [];
|
||||
let imageIndex = 0;
|
||||
let promptIndex = 0;
|
||||
|
||||
for (const connection of connections) {
|
||||
if (connection.toNodeId !== promptNodeId) continue;
|
||||
|
||||
const node = nodeById.get(connection.fromNodeId);
|
||||
if (!node) continue;
|
||||
|
||||
if (node.type === "prompt") {
|
||||
const metadata = node.metadata as CanvasPromptNodeMetadata;
|
||||
const prompt = metadata.prompt?.trim() || "";
|
||||
promptIndex += 1;
|
||||
sources.push({
|
||||
alias: `@文本${promptIndex}`,
|
||||
token: createPromptMentionToken(`文本${promptIndex}`, node.id, "prompt"),
|
||||
nodeId: node.id,
|
||||
kind: "prompt",
|
||||
label: `文本${promptIndex}`,
|
||||
index: promptIndex,
|
||||
description: prompt || node.title.trim() || `已连接文本 ${promptIndex}`,
|
||||
prompt,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (node.type === "image") {
|
||||
const metadata = node.metadata as CanvasImageNodeMetadata;
|
||||
if (!metadata.imageUrl) continue;
|
||||
imageIndex += 1;
|
||||
sources.push({
|
||||
alias: `@图${imageIndex}`,
|
||||
token: createPromptMentionToken(`图${imageIndex}`, node.id, "image"),
|
||||
nodeId: node.id,
|
||||
kind: "image",
|
||||
label: `图${imageIndex}`,
|
||||
index: imageIndex,
|
||||
description:
|
||||
metadata.prompt?.trim() || node.title.trim() || `已连接素材 ${imageIndex}`,
|
||||
imageUrl: metadata.imageUrl,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return sources;
|
||||
}
|
||||
|
||||
export function getConnectedPromptSources(
|
||||
nodes: CanvasNode[],
|
||||
connections: CanvasConnection[],
|
||||
targetNodeId: string,
|
||||
): PromptMentionSource[] {
|
||||
const promptById = new Map(
|
||||
nodes
|
||||
.filter((node) => node.type === "image")
|
||||
.filter((node) => node.type === "prompt")
|
||||
.map((node) => [node.id, node] as const),
|
||||
);
|
||||
|
||||
return connections
|
||||
.filter((connection) => connection.toNodeId === promptNodeId)
|
||||
.map((connection) => imageById.get(connection.fromNodeId))
|
||||
.filter((connection) => connection.toNodeId === targetNodeId)
|
||||
.map((connection) => promptById.get(connection.fromNodeId))
|
||||
.filter((node): node is CanvasNode => Boolean(node))
|
||||
.map((node, index) => {
|
||||
const metadata = node.metadata as CanvasImageNodeMetadata;
|
||||
const metadata = node.metadata as CanvasPromptNodeMetadata;
|
||||
const prompt = metadata.prompt?.trim() || "";
|
||||
return {
|
||||
alias: `@图${index + 1}`,
|
||||
token: createPromptMentionToken(`图${index + 1}`, node.id),
|
||||
alias: `@文本${index + 1}`,
|
||||
token: createPromptMentionToken(`文本${index + 1}`, node.id, "prompt"),
|
||||
nodeId: node.id,
|
||||
label: `图${index + 1}`,
|
||||
kind: "prompt",
|
||||
label: `文本${index + 1}`,
|
||||
index: index + 1,
|
||||
description:
|
||||
metadata.prompt?.trim() || node.title.trim() || `已连接素材 ${index + 1}`,
|
||||
imageUrl: metadata.imageUrl,
|
||||
description: prompt || node.title.trim() || `已连接文本 ${index + 1}`,
|
||||
prompt,
|
||||
} satisfies PromptMentionSource;
|
||||
})
|
||||
.filter((item) => Boolean(item.imageUrl));
|
||||
});
|
||||
}
|
||||
|
||||
export function createPromptMentionToken(label: string, nodeId: string) {
|
||||
return `[${label}](${mentionUrlPrefix}${nodeId})`;
|
||||
export function createPromptMentionToken(
|
||||
label: string,
|
||||
nodeId: string,
|
||||
kind: PromptMentionSource["kind"] = "image",
|
||||
) {
|
||||
const prefix = kind === "prompt" ? promptMentionUrlPrefix : imageMentionUrlPrefix;
|
||||
return `[${label}](${prefix}${nodeId})`;
|
||||
}
|
||||
|
||||
export function parsePromptMentionUrl(value: string) {
|
||||
return value.startsWith(mentionUrlPrefix)
|
||||
? value.slice(mentionUrlPrefix.length)
|
||||
: null;
|
||||
if (value.startsWith(imageMentionUrlPrefix)) {
|
||||
return value.slice(imageMentionUrlPrefix.length);
|
||||
}
|
||||
if (value.startsWith(promptMentionUrlPrefix)) {
|
||||
return value.slice(promptMentionUrlPrefix.length);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function resolvePromptMentions(
|
||||
@@ -65,7 +132,10 @@ export function resolvePromptMentions(
|
||||
let resolvedPrompt = prompt;
|
||||
|
||||
for (const mention of mentions) {
|
||||
const replacement = `${mention.label}(第 ${mention.index} 张输入参考图)`;
|
||||
const replacement =
|
||||
mention.kind === "image"
|
||||
? `${mention.label}(第 ${mention.index} 张输入参考图)`
|
||||
: `${mention.label}(第 ${mention.index} 段输入文本)`;
|
||||
const usedMarkdownToken = resolvedPrompt.includes(mention.token);
|
||||
const usedAlias = resolvedPrompt.includes(mention.alias);
|
||||
if (!usedMarkdownToken && !usedAlias) continue;
|
||||
@@ -75,13 +145,25 @@ export function resolvePromptMentions(
|
||||
}
|
||||
|
||||
if (mentions.length > 0) {
|
||||
const referenceGuide = mentions
|
||||
const imageGuide = mentions
|
||||
.filter((mention) => mention.kind === "image")
|
||||
.map(
|
||||
(mention) =>
|
||||
`${mention.label} = 第 ${mention.index} 张输入参考图:${mention.description}`,
|
||||
)
|
||||
.join("\n");
|
||||
resolvedPrompt = `${resolvedPrompt.trim()}\n\n已连接参考图编号:\n${referenceGuide}`;
|
||||
);
|
||||
const promptGuide = mentions
|
||||
.filter((mention) => mention.kind === "prompt")
|
||||
.map(
|
||||
(mention) =>
|
||||
`${mention.label} = 第 ${mention.index} 段输入文本:${mention.description}`,
|
||||
);
|
||||
const guide = [
|
||||
imageGuide.length ? `已连接参考图编号:\n${imageGuide.join("\n")}` : "",
|
||||
promptGuide.length ? `已连接文本编号:\n${promptGuide.join("\n")}` : "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n\n");
|
||||
resolvedPrompt = `${resolvedPrompt.trim()}\n\n${guide}`;
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user