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

This commit is contained in:
Danborad authored and ddCat committed 2026-09-11 21:43:28 +08:00
1 parent 7ebbd9e003
commit f4306676f0
7 files changed
+400 -26

No files matched your search

@@ -34,6 +34,14 @@ type UseCanvasConnectionControllerOptions = {
setContextMenu: Dispatch<SetStateAction<ContextMenuState | null>>;
setDialogNodeId: Dispatch<SetStateAction<string | null>>;
setDrawingNodeId: Dispatch<SetStateAction<string | null>>;
onReplaceReference?: (targetNodeId: string, oldReference: { id: string; nodeId?: string; label?: string; title?: string }, sourceNodeId: string) => void;
};
export type ConnectionReplaceHover = {
targetNodeId: string;
referenceLabel: string;
clientX: number;
clientY: number;
};
type ConnectionDropTarget = {
@@ -81,6 +89,7 @@ export function useCanvasConnectionController({
setContextMenu,
setDialogNodeId,
setDrawingNodeId,
onReplaceReference,
}: UseCanvasConnectionControllerOptions) {
const { message } = App.useApp();
const tldrawLicenseKey = useUserStore((state) => state.drawingEngine.tldrawLicenseKey);
@@ -89,6 +98,7 @@ export function useCanvasConnectionController({
const [connectionTargetNodeId, setConnectionTargetNodeId] = useState<string | null>(null);
const [connectionApproach, setConnectionApproach] = useState<CanvasConnectionApproach>(null);
const [connectionTargetAnchorRatio, setConnectionTargetAnchorRatio] = useState<number | undefined>();
const [connectionReplaceHover, setConnectionReplaceHover] = useState<ConnectionReplaceHover | null>(null);
const [pendingConnectionCreate, setPendingConnectionCreate] = useState<PendingConnectionCreate | null>(null);
const [batchConnectionPreview, setBatchConnectionPreview] = useState<CanvasBatchConnectionPreview | null>(null);
const [mouseWorld, setMouseWorld] = useState<Position>({ x: 0, y: 0 });
@@ -101,6 +111,28 @@ export function useCanvasConnectionController({
const batchConnectionPointerStartRef = useRef<Position | null>(null);
const pointerMoveFrameRef = useRef<number | null>(null);
const latestPointerMoveRef = useRef<PointerEvent | null>(null);
const hoveredReplaceElRef = useRef<HTMLElement | null>(null);
const updateConnectionReplaceHover = useCallback((element: HTMLElement | null, clientX = 0, clientY = 0) => {
if (hoveredReplaceElRef.current === element) {
if (element) {
setConnectionReplaceHover((prev) => prev ? { ...prev, clientX, clientY } : null);
}
return;
}
if (hoveredReplaceElRef.current) {
hoveredReplaceElRef.current.removeAttribute("data-connection-hover-replace");
}
hoveredReplaceElRef.current = element;
if (element) {
element.setAttribute("data-connection-hover-replace", "true");
const targetNodeId = element.getAttribute("data-target-node-id") || "";
const referenceLabel = element.getAttribute("data-reference-label") || "参考图";
setConnectionReplaceHover({ targetNodeId, referenceLabel, clientX, clientY });
} else {
setConnectionReplaceHover(null);
}
}, []);
useLayoutEffect(() => {
connectingParamsRef.current = connectingParams;
@@ -123,13 +155,14 @@ export function useCanvasConnectionController({
connectingParamsRef.current = next;
setConnectingParams(next);
if (!next) {
updateConnectionReplaceHover(null);
setConnectionApproach(null);
connectingPointerIdRef.current = null;
connectingPointerStartRef.current = null;
setConnectionTargetNodeId(null);
setConnectionTargetAnchorRatio(undefined);
}
}, []);
}, [updateConnectionReplaceHover]);
const closeConnectionCreateMenu = useCallback(() => {
pendingConnectionCreateRef.current = null;
@@ -545,9 +578,69 @@ export function useCanvasConnectionController({
}, [finishBatchConnection, message]);
const finishConnection = useCallback((clientX: number, clientY: number) => {
updateConnectionReplaceHover(null);
if (pendingConnectionCreateRef.current) return;
const currentConnection = connectingParamsRef.current;
if (!currentConnection) return;
// 1. 如果是从输出端(source)拖出的连线,检查是否拖到了参考图卡片或参考区上进行替换
if (currentConnection.handleType === "source" && typeof document !== "undefined") {
const hitElement = document.elementFromPoint(clientX, clientY);
if (hitElement) {
// A) 直接释放在提示词面板的参考图卡片上
const refChip = hitElement.closest<HTMLElement>("[data-reference-chip]");
if (refChip) {
const targetNodeId = refChip.getAttribute("data-target-node-id");
const oldNodeId = refChip.getAttribute("data-reference-node-id");
const oldLabel = refChip.getAttribute("data-reference-label") || "";
const oldRefId = refChip.getAttribute("data-reference-id") || "";
const oldTitle = refChip.getAttribute("data-reference-title") || oldLabel;
if (targetNodeId && oldNodeId && targetNodeId !== currentConnection.nodeId && onReplaceReference) {
if (oldNodeId === currentConnection.nodeId) {
setConnecting(null);
return;
}
onReplaceReference(targetNodeId, { id: oldRefId, nodeId: oldNodeId, label: oldLabel, title: oldTitle }, currentConnection.nodeId);
setConnecting(null);
return;
}
}
// B) 释放在提示词参考架容器内(自动就近匹配最近的参考卡片)
const refShelf = hitElement.closest<HTMLElement>(".canvas-node-composer-references");
if (refShelf) {
const chips = Array.from(refShelf.querySelectorAll<HTMLElement>("[data-reference-chip]"));
let closestChip: HTMLElement | null = null;
let minDistance = Number.POSITIVE_INFINITY;
chips.forEach((chip) => {
const rect = chip.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const dist = Math.abs(clientX - centerX);
if (dist < minDistance) {
minDistance = dist;
closestChip = chip;
}
});
if (closestChip) {
const targetNodeId = (closestChip as HTMLElement).getAttribute("data-target-node-id");
const oldNodeId = (closestChip as HTMLElement).getAttribute("data-reference-node-id");
const oldLabel = (closestChip as HTMLElement).getAttribute("data-reference-label") || "";
const oldRefId = (closestChip as HTMLElement).getAttribute("data-reference-id") || "";
const oldTitle = (closestChip as HTMLElement).getAttribute("data-reference-title") || oldLabel;
if (targetNodeId && oldNodeId && targetNodeId !== currentConnection.nodeId && onReplaceReference) {
if (oldNodeId === currentConnection.nodeId) {
setConnecting(null);
return;
}
onReplaceReference(targetNodeId, { id: oldRefId, nodeId: oldNodeId, label: oldLabel, title: oldTitle }, currentConnection.nodeId);
setConnecting(null);
return;
}
}
}
}
}
const dropTarget = getConnectionDropTarget(clientX, clientY, currentConnection);
if (dropTarget.nodeId) {
connectNodes(currentConnection, dropTarget.nodeId, dropTarget.handleId, dropTarget.anchorRatio);
@@ -561,7 +654,7 @@ export function useCanvasConnectionController({
pendingConnectionCreateRef.current = pending;
setPendingConnectionCreate(pending);
}
}, [connectNodes, getConnectionDropTarget, screenToCanvas, setConnecting]);
}, [connectNodes, getConnectionDropTarget, onReplaceReference, screenToCanvas, setConnecting, updateConnectionReplaceHover]);
const handleConnectStart = useCallback((event: ReactPointerEvent, nodeId: string, handleType: "source" | "target", handleId?: string, anchorRatio?: number) => {
event.preventDefault();
@@ -606,6 +699,30 @@ export function useCanvasConnectionController({
}
const current = connectingParamsRef.current;
if (!current || connectingPointerIdRef.current !== event.pointerId || pendingConnectionCreateRef.current) return;
if (current.handleType === "source" && typeof document !== "undefined") {
const el = document.elementFromPoint(event.clientX, event.clientY);
let chip = el?.closest<HTMLElement>("[data-reference-chip]");
if (!chip) {
const shelf = el?.closest<HTMLElement>(".canvas-node-composer-references");
if (shelf) {
const chips = Array.from(shelf.querySelectorAll<HTMLElement>("[data-reference-chip]"));
let closestChip: HTMLElement | null = null;
let minDistance = Number.POSITIVE_INFINITY;
chips.forEach((c) => {
const rect = c.getBoundingClientRect();
const dist = Math.abs(event.clientX - (rect.left + rect.width / 2));
if (dist < minDistance) {
minDistance = dist;
closestChip = c;
}
});
chip = closestChip;
}
}
updateConnectionReplaceHover(chip || null, event.clientX, event.clientY);
} else {
updateConnectionReplaceHover(null);
}
const dropTarget = getConnectionDropTarget(event.clientX, event.clientY, current);
const point = screenToCanvas(event.clientX, event.clientY);
setConnectionApproach((previous) => latchCanvasConnectionApproach(previous, dropTarget.nodeId, point));
@@ -662,6 +779,7 @@ export function useCanvasConnectionController({
};
const handlePointerCancel = (event: PointerEvent) => {
cancelPendingPointerMove();
updateConnectionReplaceHover(null);
if (batchConnectionPointerIdRef.current === event.pointerId) {
clearBatchConnection();
return;
@@ -670,6 +788,7 @@ export function useCanvasConnectionController({
};
const cancel = () => {
cancelPendingPointerMove();
updateConnectionReplaceHover(null);
if (connectingParamsRef.current) setConnecting(null);
if (batchConnectionPreviewRef.current) clearBatchConnection();
};
@@ -692,6 +811,7 @@ export function useCanvasConnectionController({
connectionTargetNodeId,
connectionApproach,
connectionTargetAnchorRatio,
connectionReplaceHover,
connectingParams,
createConnectedNode,
getConnectionCreateDisabledReason,