ai-chatbot/lib/canvas/agent-direct-writeback.ts

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,
};
}