import { getBestAnchorNode, getSelectedMediaNodes, getSelectedSlots, } from "@/lib/canvas/agent-context"; import type { CanvasAgentContext, CanvasMediaItem, CanvasOperationKind, CanvasOperationPlan, } from "@/types/canvas/agent-protocol"; export type CanvasAgentDirectWritebackMode = "generate" | "edit" | "video"; export type CanvasAgentDirectWritebackResultType = "image" | "video"; export interface CanvasAgentDirectWritebackTargetRef { nodeId?: string | null; } export interface BuildCanvasAgentDirectWritebackPlanParams { projectId: string; chatId: string; assistantMessageId: string; operationLabel: string; resultType: CanvasAgentDirectWritebackResultType; resultUrls: string[]; prompt: string; mode: CanvasAgentDirectWritebackMode; context: CanvasAgentContext; requestTextContent?: string | null; targetRefs?: CanvasAgentDirectWritebackTargetRef[]; sourceImageUrls?: string[]; assetIdsByUrl?: Record; model?: string | null; targetSlotId?: string | null; } export interface CanvasAgentDirectWritebackPlanResult { plan: CanvasOperationPlan; mediaItems: CanvasMediaItem[]; mediaItemIds: string[]; operationId: string; operation: CanvasOperationKind; targetSlotId: string; shouldFillSlot: boolean; selectedMediaNodes: ReturnType; } export function buildCanvasAgentDirectWritebackPlan( params: BuildCanvasAgentDirectWritebackPlanParams ): CanvasAgentDirectWritebackPlanResult | null { const projectId = String(params.projectId || "").trim(); if (!projectId) return null; const resultUrls = params.resultUrls .map((url) => String(url || "").trim()) .filter((url) => /^https?:\/\//i.test(url)); if (resultUrls.length === 0) return null; const preferredTargetSlotId = String(params.targetSlotId || "").trim(); const selectedSlot = getSelectedSlots(params.context)[0] || null; const targetSlotId = preferredTargetSlotId || selectedSlot?.nodeId || ""; const shouldFillSlot = Boolean(targetSlotId); const selectedMediaNodes = getSelectedMediaNodes(params.context); const anchorNode = selectedSlot || selectedMediaNodes[0] || getBestAnchorNode(params.context); const targetRefs = params.targetRefs || []; const targetNodeIds = Array.from( new Set( targetRefs .map((ref) => ref.nodeId) .filter((value): value is string => Boolean(value)) ) ); const selectedMediaNodeIds = selectedMediaNodes .map((node) => node.nodeId) .filter((value): value is string => Boolean(value)); const perResultSourceNodeIds = !shouldFillSlot && resultUrls.length > 1 ? [targetNodeIds, selectedMediaNodeIds].find( (nodeIds) => nodeIds.length === resultUrls.length ) || [] : []; const sourceNodeIds = Array.from( new Set( [ ...targetNodeIds, ...selectedMediaNodeIds, anchorNode?.nodeId, ].filter((value): value is string => Boolean(value)) ) ); const operation: CanvasOperationKind = selectedSlot ? "fill_slot" : shouldFillSlot ? "fill_slot" : params.mode === "edit" ? "append_revision" : "insert_beside"; const mediaItems: CanvasMediaItem[] = resultUrls.map((url, index) => { const mediaItemId = `${params.assistantMessageId}:${params.operationLabel}:${index + 1}`; return { mediaItemId, type: params.resultType, url, assetId: params.assetIdsByUrl?.[url], title: params.resultType === "video" ? "Agent 视频结果" : "Agent 图片结果", prompt: params.prompt, sourceNodeIds: perResultSourceNodeIds[index] ? [perResultSourceNodeIds[index]] : sourceNodeIds, metadata: { messageId: params.assistantMessageId, chatId: params.chatId, model: params.model || undefined, sourceImageUrls: params.sourceImageUrls || [], }, }; }); const operationId = `${params.assistantMessageId}:${params.operationLabel}`; const plan: CanvasOperationPlan = { version: 1, operationId, operation, projectId, baseCanvasRevision: params.context.canvasRevision, baseCanvasUpdatedAt: params.context.canvasUpdatedAt, sourceNodeIds, targetSlotId: targetSlotId || undefined, anchorNodeId: shouldFillSlot ? undefined : anchorNode?.nodeId, placement: "right", placementStrategy: perResultSourceNodeIds.length === mediaItems.length ? "beside_each_source" : mediaItems.length > 1 ? "grid" : "single", preserveOriginal: true, mediaItems, metadata: { messageId: params.assistantMessageId, chatId: params.chatId, mode: params.mode, prompt: params.prompt || params.requestTextContent || "", }, }; return { plan, mediaItems, mediaItemIds: mediaItems.map((item) => item.mediaItemId), operationId, operation, targetSlotId, shouldFillSlot, selectedMediaNodes, }; }