feat(canvas): 支持拖拽连线替换提示词参考图并全量同步提示词引用

This commit is contained in:
Danborad
2026-09-11 19:16:08 +08:00
committed by ddCat
parent 7ebbd9e003
commit f4306676f0
7 changed files with 400 additions and 26 deletions

View File

@@ -1,7 +1,7 @@
import { Button, Image as AntImage, InputNumber, Modal, Popover } from "antd";
import { Tooltip } from "@/components/ui/base/tooltip";
import { useEffect, useMemo, useRef, useState, type CSSProperties, type KeyboardEvent as ReactKeyboardEvent, type PointerEvent as ReactPointerEvent, type ReactNode } from "react";
import { ArrowUp, AtSign, Boxes, ChevronDown, FileText, GripVertical, ImageIcon, ImagePlus, Link2, LoaderCircle, Maximize2, Music2, Pencil, SlidersHorizontal, UserRound, Video, WandSparkles, X } from "lucide-react";
import { ArrowLeftRight, ArrowUp, AtSign, Boxes, Camera, ChevronDown, FileText, GripVertical, ImageIcon, ImagePlus, Link2, LoaderCircle, Maximize2, Music2, Pencil, SlidersHorizontal, UserRound, Video, WandSparkles, X } from "lucide-react";
import { ModelPicker } from "@/components/model-picker";
import { defaultConfig, modelOptionName, resolveModelChannel, useEffectiveConfig, type AiConfig } from "@/stores/use-config-store";
@@ -43,6 +43,8 @@ type CanvasNodePromptPanelProps = {
mentionReferences?: CanvasResourceReference[];
onRemoveReference?: (nodeId: string, reference: CanvasResourceReference) => void;
onReorderReferences?: (nodeId: string, orderedNodeIds: string[]) => void;
onReplaceReference?: (nodeId: string, oldReference: CanvasResourceReference, sourceNodeId: string) => void;
onReplaceReferenceFiles?: (nodeId: string, oldReference: CanvasResourceReference, files: File[]) => void;
onClose?: () => void;
onNodeMouseDown?: (event: ReactPointerEvent, nodeId: string) => void;
onImageSettingsOpenChange?: (open: boolean) => void;
@@ -61,7 +63,7 @@ const PROMPT_EDITOR_VERTICAL_PADDING = 12;
const PROMPT_EDITOR_EXPANDED_VERTICAL_PADDING = 20;
const PROMPT_EDITOR_MAX_LINES = 8;
export function CanvasNodePromptPanel({ projectId, node, isRunning, onPromptChange, onConfigChange, onGenerate, mentionReferences = [], onRemoveReference, onReorderReferences, onClose, onNodeMouseDown, onImageSettingsOpenChange, workspaceMode = "professional" }: CanvasNodePromptPanelProps) {
export function CanvasNodePromptPanel({ projectId, node, isRunning, onPromptChange, onConfigChange, onGenerate, mentionReferences = [], onRemoveReference, onReorderReferences, onReplaceReference, onReplaceReferenceFiles, onClose, onNodeMouseDown, onImageSettingsOpenChange, workspaceMode = "professional" }: CanvasNodePromptPanelProps) {
const globalConfig = useEffectiveConfig();
const themeName = useThemeStore((state) => state.theme);
const theme = canvasThemes[themeName];
@@ -103,6 +105,7 @@ export function CanvasNodePromptPanel({ projectId, node, isRunning, onPromptChan
size: node.metadata?.size || globalConfig.size,
quality: node.metadata?.quality || globalConfig.quality,
count: String(node.metadata?.count || globalConfig.count),
transparentBackground: node.metadata?.transparentBackground || globalConfig.transparentBackground,
videoSeconds: node.metadata?.seconds || globalConfig.videoSeconds,
vquality: node.metadata?.vquality || globalConfig.vquality,
videoGenerateAudio: node.metadata?.generateAudio || globalConfig.videoGenerateAudio,
@@ -438,11 +441,14 @@ export function CanvasNodePromptPanel({ projectId, node, isRunning, onPromptChan
<>
<div className="canvas-node-composer-editor" style={{ height }}>
<ConnectedReferenceShelf
targetNodeId={node.id}
references={resolvedMentionReferences}
theme={theme}
onInsert={insertPromptReference}
onRemove={(reference) => onRemoveReference?.(node.id, reference)}
onReorder={onReorderReferences ? (orderedNodeIds) => onReorderReferences(node.id, orderedNodeIds) : undefined}
onReplaceReference={onReplaceReference ? (oldReference, sourceNodeId) => onReplaceReference(node.id, oldReference, sourceNodeId) : undefined}
onReplaceReferenceFiles={onReplaceReferenceFiles ? (oldReference, files) => onReplaceReferenceFiles(node.id, oldReference, files) : undefined}
/>
<CanvasResourceMentionTextarea
value={prompt}
@@ -450,6 +456,7 @@ export function CanvasNodePromptPanel({ projectId, node, isRunning, onPromptChan
includeAssetLibrary
onChange={updatePrompt}
autoLinkEnabled={autoLinkEnabled}
onReferenceFilesDrop={onReplaceReferenceFiles ? (reference, files) => onReplaceReferenceFiles(node.id, reference, files) : undefined}
onContentSizeChange={expanded ? setExpandedPromptContentHeight : setPromptContentHeight}
containerClassName="min-h-0 flex-1"
className={expanded
@@ -627,10 +634,29 @@ function referenceShelfHeading(references: CanvasResourceReference[]) {
return `${label} · ${references.length}`;
}
function ConnectedReferenceShelf({ references, theme, onInsert, onRemove, onReorder }: { references: CanvasResourceReference[]; theme: CanvasTheme; onInsert: (reference: CanvasResourceReference) => void; onRemove?: (reference: CanvasResourceReference) => void; onReorder?: (orderedNodeIds: string[]) => void }) {
function ConnectedReferenceShelf({
targetNodeId,
references,
theme,
onInsert,
onRemove,
onReorder,
onReplaceReference,
onReplaceReferenceFiles,
}: {
targetNodeId?: string;
references: CanvasResourceReference[];
theme: CanvasTheme;
onInsert: (reference: CanvasResourceReference) => void;
onRemove?: (reference: CanvasResourceReference) => void;
onReorder?: (orderedNodeIds: string[]) => void;
onReplaceReference?: (oldReference: CanvasResourceReference, sourceNodeId: string) => void;
onReplaceReferenceFiles?: (oldReference: CanvasResourceReference, files: File[]) => void;
}) {
const activeReferences = references.filter((item) => item.active && item.kind !== "skill");
const [imagePreview, setImagePreview] = useState<CanvasResourceReference | null>(null);
const [draggedReferenceId, setDraggedReferenceId] = useState<string | null>(null);
const [dropTargetReferenceId, setDropTargetReferenceId] = useState<string | null>(null);
if (!activeReferences.length) return null;
const moveReference = (sourceId: string, targetId: string) => {
@@ -657,21 +683,72 @@ function ConnectedReferenceShelf({ references, theme, onInsert, onRemove, onReor
<div className="canvas-node-composer-references-track thin-scrollbar">
{activeReferences.map((reference, index) => {
const canPreview = Boolean(reference.previewUrl) && (reference.kind === "image" || reference.kind === "character" || reference.kind === "video");
const isDropTarget = dropTargetReferenceId === reference.id;
return (
<span
key={reference.id}
className="canvas-node-reference-chip"
className="canvas-node-reference-chip relative"
data-reference-chip="true"
data-reference-id={reference.id}
data-reference-node-id={reference.nodeId}
data-reference-label={reference.label}
data-reference-title={reference.title || reference.label}
data-target-node-id={targetNodeId}
data-dragging={draggedReferenceId === reference.nodeId || undefined}
data-drop-target={isDropTarget ? "true" : undefined}
style={{
boxShadow: isDropTarget ? "0 0 0 2px #3b82f6, 0 0 16px rgba(59, 130, 246, 0.45)" : undefined,
}}
onDragOver={(event) => {
if (!onReorder || !draggedReferenceId) return;
event.preventDefault();
event.dataTransfer.dropEffect = "move";
if (draggedReferenceId) {
if (!onReorder) return;
event.preventDefault();
event.dataTransfer.dropEffect = "move";
return;
}
const hasImageNode = event.dataTransfer.types.includes("application/x-canvas-image-node-id");
const hasFiles = event.dataTransfer.types.includes("Files");
if (hasImageNode || hasFiles) {
event.preventDefault();
event.stopPropagation();
event.dataTransfer.dropEffect = "copy";
if (dropTargetReferenceId !== reference.id) {
setDropTargetReferenceId(reference.id);
}
}
}}
onDragLeave={(event) => {
if (!event.currentTarget.contains(event.relatedTarget as Node)) {
if (dropTargetReferenceId === reference.id) {
setDropTargetReferenceId(null);
}
}
}}
onDrop={(event) => {
event.preventDefault();
const sourceId = draggedReferenceId || event.dataTransfer.getData("text/plain");
setDraggedReferenceId(null);
moveReference(sourceId, reference.nodeId);
if (dropTargetReferenceId === reference.id) {
setDropTargetReferenceId(null);
}
if (draggedReferenceId) {
event.preventDefault();
const sourceId = draggedReferenceId || event.dataTransfer.getData("text/plain");
setDraggedReferenceId(null);
moveReference(sourceId, reference.nodeId);
return;
}
const sourceNodeId = event.dataTransfer.getData("application/x-canvas-image-node-id");
if (sourceNodeId && sourceNodeId !== reference.nodeId && onReplaceReference) {
event.preventDefault();
event.stopPropagation();
onReplaceReference(reference, sourceNodeId);
return;
}
const files = Array.from(event.dataTransfer.files).filter((f) => f.type.startsWith("image/"));
if (files.length && onReplaceReferenceFiles) {
event.preventDefault();
event.stopPropagation();
onReplaceReferenceFiles(reference, files);
return;
}
}}
>
{onReorder ? (
@@ -875,6 +952,7 @@ export function buildNodeConfig(globalConfig: AiConfig, node: CanvasNodeData, mo
size: node.metadata?.size,
quality: node.metadata?.quality,
transparentBackground: node.metadata?.transparentBackground,
videoWatermark: node.metadata?.watermark,
count: String(node.metadata?.count || globalConfig.canvasImageCount || globalConfig.count || defaultConfig.count),
}
: {
@@ -898,18 +976,18 @@ export function buildNodeConfig(globalConfig: AiConfig, node: CanvasNodeData, mo
return {
...globalConfig,
model,
quality: defaults.quality || globalConfig.quality || defaultConfig.quality,
quality: defaults.quality ?? globalConfig.quality ?? defaultConfig.quality,
size: defaults.size ?? globalConfig.size ?? defaultConfig.size,
transparentBackground: defaults.transparentBackground || "false",
transparentBackground: defaults.transparentBackground ?? "false",
videoSeconds: defaults.videoSeconds || normalizeVideoDuration(globalConfig.videoSeconds || defaultConfig.videoSeconds),
vquality: defaults.vquality ?? normalizeVideoResolution(globalConfig.vquality || defaultConfig.vquality),
videoGenerateAudio: defaults.videoGenerateAudio || globalConfig.videoGenerateAudio || defaultConfig.videoGenerateAudio,
videoWatermark: defaults.videoWatermark || globalConfig.videoWatermark || defaultConfig.videoWatermark,
videoGenerateAudio: defaults.videoGenerateAudio ?? globalConfig.videoGenerateAudio ?? defaultConfig.videoGenerateAudio,
videoWatermark: defaults.videoWatermark ?? globalConfig.videoWatermark ?? defaultConfig.videoWatermark,
audioVoice: node.metadata?.audioVoice || globalConfig.audioVoice || defaultConfig.audioVoice,
audioFormat: node.metadata?.audioFormat || globalConfig.audioFormat || defaultConfig.audioFormat,
audioSpeed: node.metadata?.audioSpeed || globalConfig.audioSpeed || defaultConfig.audioSpeed,
audioInstructions: node.metadata?.audioInstructions || globalConfig.audioInstructions || defaultConfig.audioInstructions,
count: defaults.count || String(node.metadata?.count || (mode === "image" ? globalConfig.canvasImageCount || globalConfig.count : globalConfig.count) || defaultConfig.count),
count: defaults.count ?? String(node.metadata?.count || (mode === "image" ? globalConfig.canvasImageCount || globalConfig.count : globalConfig.count) || defaultConfig.count),
};
}