160 lines
4.9 KiB
TypeScript
160 lines
4.9 KiB
TypeScript
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<string, string | undefined>;
|
|
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<typeof getSelectedMediaNodes>;
|
|
}
|
|
|
|
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,
|
|
};
|
|
}
|