import { AudioLines, Captions, Clapperboard, Download, FolderPlus, Images, Image as ImageIcon, Info, LoaderCircle, Lock, Maximize2, MessageSquare, Minus, Music2, Plus, RefreshCw, Scissors, Settings2, Trash2, Unlock, Upload, UserRound, Video } from "lucide-react"; import { CONTENT_MODERATION_ERROR_CODE, isContentModerationError } from "@/lib/generation-error"; import { registerToolbarTools, type ToolContext, type ToolDefinition } from "@/lib/canvas/tool-registry"; import { CanvasNodeType } from "@/types/canvas"; import { isCanvasImageSourceNode } from "@/lib/canvas/canvas-image-source"; // 节点状态判定辅助函数——从 ToolContext 派生 function isImage(ctx: ToolContext) { return ctx.node?.type === CanvasNodeType.Image; } function isVideo(ctx: ToolContext) { return ctx.node?.type === CanvasNodeType.Video; } function isAudio(ctx: ToolContext) { return ctx.node?.type === CanvasNodeType.Audio; } function isText(ctx: ToolContext) { return ctx.node?.type === CanvasNodeType.Text; } function isConfig(ctx: ToolContext) { return ctx.node?.type === CanvasNodeType.Config; } function hasImage(ctx: ToolContext) { return isImage(ctx) && Boolean(ctx.nodeMetadata?.content); } function hasVideo(ctx: ToolContext) { return isVideo(ctx) && Boolean(ctx.nodeMetadata?.content); } function hasAudio(ctx: ToolContext) { return isAudio(ctx) && Boolean(ctx.nodeMetadata?.content); } function isCharacterReference(ctx: ToolContext) { return isText(ctx) && ctx.nodeMetadata?.workflowKind === "character" && Boolean(ctx.nodeMetadata?.characterAssetId); } function isEditableText(ctx: ToolContext) { return isText(ctx) && !isCharacterReference(ctx); } function canOpenDialog(ctx: ToolContext) { return isEditableText(ctx) || (isImage(ctx) && !isCanvasImageSourceNode(ctx.node)) || isVideo(ctx); } function simpleMode(ctx: ToolContext) { return ctx.workspaceMode === "simple"; } function isImageBatchRoot(ctx: ToolContext) { return isImage(ctx) && Boolean(ctx.nodeMetadata?.isBatchRoot && ctx.nodeMetadata.batchChildIds?.length); } function canRetry(ctx: ToolContext) { if (ctx.nodeMetadata?.fileUpload) return false; const requiresPromptChange = ctx.nodeMetadata?.generationErrorCode === CONTENT_MODERATION_ERROR_CODE || isContentModerationError(ctx.nodeMetadata?.errorDetails); const batchHasFailures = isImageBatchRoot(ctx) && (ctx.nodeMetadata?.batchFailedCount || (ctx.nodeMetadata?.status === "error" ? 1 : 0)) > 0; return (ctx.nodeMetadata?.status === "error" || (batchHasFailures && ctx.nodeMetadata?.status !== "loading")) && !requiresPromptChange; } export const nodeHoverToolbarTools: ToolDefinition[] = [ // 基础工具组 { id: "info", toolbar: "node-hover", category: "node-state", label: (ctx) => isCharacterReference(ctx) ? "查看角色详情" : "查看节点信息", displayLabel: (ctx) => isCharacterReference(ctx) ? "角色详情" : "节点信息", icon: (ctx) => isCharacterReference(ctx) ? : , defaultVisible: true, defaultOrder: 10, nodeToolbar: { group: (ctx) => isCharacterReference(ctx) || (!isImage(ctx) && !isVideo(ctx) && !isAudio(ctx) && !isEditableText(ctx) && !isConfig(ctx)) ? "primary" : "more", order: 10, section: "节点管理", }, run: (ctx) => ctx.handlers.onNodeInfo(ctx.node!), }, { id: "delete", toolbar: "node-hover", category: "node-state", label: "移除节点", displayLabel: "移除节点", icon: , defaultVisible: true, defaultOrder: 20, nodeToolbar: { group: "more", order: 1000, section: "危险操作" }, danger: true, run: (ctx) => ctx.handlers.onNodeDelete(ctx.node!), }, // 节点操作工具组——通过 applicable 谓词实现上下文感知 { id: "retry", toolbar: "node-hover", category: "node-state", label: (ctx) => isImageBatchRoot(ctx) ? "重试批次中的失败图片" : "重新生成", displayLabel: (ctx) => isImageBatchRoot(ctx) ? "重试失败项" : "重试", icon: , defaultVisible: true, defaultOrder: 30, nodeToolbar: { group: "primary", order: 5 }, applicable: canRetry, run: (ctx) => ctx.handlers.onNodeRetry(ctx.node!), }, { id: "extractFrames", toolbar: "node-hover", category: "node-state", label: (ctx) => ctx.extractingVideoFrames ? "正在截取视频画面" : "截取一个或多个视频画面", displayLabel: (ctx) => ctx.extractingVideoFrames ? "提取中" : "提取画面", icon: (ctx) => ctx.extractingVideoFrames ? : , defaultVisible: true, defaultOrder: 40, nodeToolbar: { group: "process", order: 10, section: "提取素材", description: "选取画面,保存为图片节点" }, applicable: (ctx) => hasVideo(ctx) && !simpleMode(ctx), disabled: (ctx) => ctx.extractingVideoFrames, run: (ctx) => ctx.handlers.onNodeExtractVideoFrames(ctx.node!), }, { id: "extractAudio", toolbar: "node-hover", category: "node-state", label: (ctx) => ctx.extractingAudio ? "正在提取音频" : "提取视频声音为 MP3,保留原视频声音", displayLabel: (ctx) => ctx.extractingAudio ? "提取中" : "提取音频", icon: (ctx) => ctx.extractingAudio ? : , defaultVisible: true, defaultOrder: 42, nodeToolbar: { group: "process", order: 20, section: "提取素材", description: "生成音频节点,保留原视频声音" }, applicable: (ctx) => hasVideo(ctx) && !simpleMode(ctx), disabled: (ctx) => ctx.extractingAudio || ctx.trimmingVideo, run: (ctx) => ctx.handlers.onNodeExtractAudioFromVideo(ctx.node!), }, { id: "trimRegenerate", toolbar: "node-hover", category: "node-state", label: (ctx) => ctx.trimmingVideo ? "正在截取片段" : "截取一个或多个视频片段", displayLabel: (ctx) => ctx.trimmingVideo ? "截取中" : "截取片段", icon: (ctx) => ctx.trimmingVideo ? : , defaultVisible: true, defaultOrder: 44, nodeToolbar: { group: "primary", order: 10, description: "截取一段或多段,生成新视频节点" }, applicable: (ctx) => hasVideo(ctx) && !simpleMode(ctx), disabled: (ctx) => ctx.extractingAudio || ctx.trimmingVideo, run: (ctx) => ctx.handlers.onNodeTrimVideoSegments(ctx.node!), }, { id: "saveAsset", toolbar: "node-hover", category: "node-state", label: "加入我的素材", displayLabel: "保存到素材库", icon: , defaultVisible: true, defaultOrder: 50, nodeToolbar: { group: "more", order: 40, section: "素材" }, applicable: (ctx) => hasImage(ctx) || hasVideo(ctx) || isEditableText(ctx), run: (ctx) => ctx.handlers.onNodeSaveAsset(ctx.node!), }, { id: "download", toolbar: "node-hover", category: "node-state", label: (ctx) => hasAudio(ctx) ? "下载音频" : hasVideo(ctx) ? "下载视频" : "下载图片", displayLabel: "下载", icon: , defaultVisible: true, defaultOrder: 60, nodeToolbar: { group: "utility", order: 20 }, applicable: (ctx) => hasImage(ctx) || hasVideo(ctx) || hasAudio(ctx), run: (ctx) => ctx.handlers.onNodeDownload(ctx.node!), }, { id: "edit", toolbar: "node-hover", category: "node-state", label: (ctx) => isEditableText(ctx) ? "调用文本模型生成内容" : "编辑", displayLabel: (ctx) => isEditableText(ctx) ? "文本生成" : "生成设置", icon: , defaultVisible: true, defaultOrder: 70, nodeToolbar: { group: (ctx) => isEditableText(ctx) ? "primary" : "more", order: 20, section: "生成信息" }, applicable: canOpenDialog, run: (ctx) => ctx.handlers.onNodeToggleDialog(ctx.node!), }, { id: "editText", toolbar: "node-hover", category: "node-state", label: "放大编辑文本", displayLabel: "放大编辑", icon: , defaultVisible: true, defaultOrder: 80, nodeToolbar: { group: "primary", order: 30 }, applicable: isEditableText, run: (ctx) => ctx.handlers.onNodeEditText(ctx.node!), }, { id: "generateImage", toolbar: "node-hover", category: "node-state", label: "用文本生图", displayLabel: "生图", icon: , defaultVisible: true, defaultOrder: 90, nodeToolbar: { group: "primary", order: 40 }, applicable: isEditableText, run: (ctx) => ctx.handlers.onNodeGenerateImage(ctx.node!), }, { id: "config", toolbar: "node-hover", category: "node-state", label: "生成配置", displayLabel: "生成配置", icon: , defaultVisible: true, defaultOrder: 100, nodeToolbar: { group: "primary", order: 10 }, applicable: (ctx) => isConfig(ctx) && !simpleMode(ctx), run: (ctx) => ctx.handlers.onNodeToggleDialog(ctx.node!), }, { id: "decreaseFont", toolbar: "node-hover", category: "node-state", label: "减小字号", displayLabel: "缩小", icon: , defaultVisible: true, defaultOrder: 110, nodeToolbar: { group: "process", order: 10 }, applicable: (ctx) => isEditableText(ctx) && !simpleMode(ctx), run: (ctx) => ctx.handlers.onNodeDecreaseFont(ctx.node!), }, { id: "increaseFont", toolbar: "node-hover", category: "node-state", label: "增大字号", displayLabel: "放大", icon: , defaultVisible: true, defaultOrder: 120, nodeToolbar: { group: "process", order: 20 }, applicable: (ctx) => isEditableText(ctx) && !simpleMode(ctx), run: (ctx) => ctx.handlers.onNodeIncreaseFont(ctx.node!), }, { id: "uploadImage", toolbar: "node-hover", category: "node-state", label: "上传图片", displayLabel: "上传图片", icon: , defaultVisible: true, defaultOrder: 130, nodeToolbar: { group: "primary", order: 10 }, applicable: (ctx) => isImage(ctx) && !hasImage(ctx), run: (ctx) => ctx.handlers.onNodeUpload(ctx.node!), }, { id: "uploadVideo", toolbar: "node-hover", category: "node-state", label: (ctx) => hasVideo(ctx) ? "替换视频" : "上传视频", displayLabel: (ctx) => hasVideo(ctx) ? "替换视频" : "上传视频", icon: