ai-chatbot/lib/canvas/agent-view-operations.ts

123 lines
3.8 KiB
TypeScript

import type { CanvasViewOperationPlan } from "@/types/canvas/agent-protocol";
type CanvasAgentViewNodeLike = {
id: string;
type?: string;
position?: { x?: number; y?: number };
size?: { width?: number; height?: number };
data?: unknown;
createdAt?: string | number | Date;
updatedAt?: string | number | Date;
};
type CanvasAgentViewportLike = {
x: number;
y: number;
zoom: number;
};
type CanvasAgentStageLike = {
width: number;
height: number;
};
const VIEWPORT_ANCHOR_X_RATIO = 0.52;
const VIEWPORT_ANCHOR_Y_RATIO = 0.46;
function toFiniteNumber(value: unknown, fallback = 0) {
const numericValue = Number(value);
return Number.isFinite(numericValue) ? numericValue : fallback;
}
function getNodeTimestamp(value: unknown) {
if (typeof value === "number" && Number.isFinite(value)) return value;
if (value instanceof Date) return value.getTime();
if (typeof value !== "string" || !value.trim()) return 0;
const timestamp = Date.parse(value);
return Number.isFinite(timestamp) ? timestamp : 0;
}
function getCanvasAgentResultScore(node: CanvasAgentViewNodeLike) {
const data =
node.data && typeof node.data === "object" && !Array.isArray(node.data)
? (node.data as Record<string, unknown>)
: {};
const metadata =
data.metadata && typeof data.metadata === "object" && !Array.isArray(data.metadata)
? (data.metadata as Record<string, unknown>)
: {};
const generatedByAgent =
metadata.generatedByAgent === true ||
metadata.latestGeneratedHighlight === true ||
typeof metadata.canvasAgentOperationId === "string";
if (!generatedByAgent) return 0;
return Math.max(
getNodeTimestamp(metadata.recentlyGeneratedAt),
getNodeTimestamp(metadata.agentGeneratedAt),
getNodeTimestamp(metadata.agentReplacedAt),
getNodeTimestamp(node.updatedAt),
getNodeTimestamp(node.createdAt),
1
);
}
export function resolveCanvasViewOperationTargetNodeIds(params: {
plan: Pick<CanvasViewOperationPlan, "operation" | "nodeIds" | "anchorNodeId">;
nodes: CanvasAgentViewNodeLike[];
selectedNodeIds?: string[];
maxLatestResults?: number;
}) {
const explicitNodeIds = Array.isArray(params.plan.nodeIds)
? params.plan.nodeIds.map((id) => String(id || "").trim()).filter(Boolean)
: [];
if (explicitNodeIds.length > 0) return Array.from(new Set(explicitNodeIds));
const anchorNodeId = String(params.plan.anchorNodeId || "").trim();
if (anchorNodeId) return [anchorNodeId];
if (params.plan.operation === "focus_selection") {
return Array.isArray(params.selectedNodeIds)
? params.selectedNodeIds.map((id) => String(id || "").trim()).filter(Boolean)
: [];
}
if (params.plan.operation === "focus_latest_result") {
const maxLatestResults = Math.max(1, Math.min(8, params.maxLatestResults || 4));
return params.nodes
.map((node) => ({
id: node.id,
score: getCanvasAgentResultScore(node),
}))
.filter((item) => item.id && item.score > 0)
.sort((a, b) => b.score - a.score)
.slice(0, maxLatestResults)
.map((item) => item.id);
}
return [];
}
export function computeCanvasAgentPanViewport(params: {
currentViewport: CanvasAgentViewportLike;
center: { x?: number; y?: number };
stage: CanvasAgentStageLike;
zoom?: number;
}) {
const currentZoom = Math.max(0.1, toFiniteNumber(params.currentViewport.zoom, 1));
const zoom = Math.max(
0.1,
Math.min(4, toFiniteNumber(params.zoom, currentZoom))
);
const centerX = toFiniteNumber(params.center.x, 0);
const centerY = toFiniteNumber(params.center.y, 0);
const stageWidth = Math.max(1, toFiniteNumber(params.stage.width, 1200));
const stageHeight = Math.max(1, toFiniteNumber(params.stage.height, 800));
return {
x: stageWidth * VIEWPORT_ANCHOR_X_RATIO - centerX * zoom,
y: stageHeight * VIEWPORT_ANCHOR_Y_RATIO - centerY * zoom,
zoom,
};
}