feat: support canvas comparison view intent
This commit is contained in:
parent
ad6b09d1b2
commit
2945528f44
@ -74,6 +74,10 @@ import {
|
||||
type CanvasAgentModelPlannerOutcome,
|
||||
} from "@/lib/canvas/agent-model-planner";
|
||||
import { buildCanvasAgentResultViewOperation } from "@/lib/canvas/agent-result-view";
|
||||
import {
|
||||
buildCanvasAgentComparisonViewPlan,
|
||||
hasCanvasAgentComparisonViewIntent,
|
||||
} from "@/lib/canvas/agent-view-intent";
|
||||
import {
|
||||
getCanvasAgentOperationErrorMessage,
|
||||
normalizeCanvasAgentOperationErrorCode,
|
||||
@ -3237,7 +3241,9 @@ export async function POST(request: Request) {
|
||||
if (!text) return null;
|
||||
|
||||
const lowerText = text.toLowerCase();
|
||||
const hasComparisonViewIntent = hasCanvasAgentComparisonViewIntent(text);
|
||||
const hasViewKeyword =
|
||||
hasComparisonViewIntent ||
|
||||
/视角|视野|画布|聚焦|定位|居中|看全局|看整体|看全部|缩小|放大|拉近|拉远|zoom/.test(
|
||||
lowerText
|
||||
);
|
||||
@ -3268,6 +3274,15 @@ export async function POST(request: Request) {
|
||||
explicitTargetNodeIds.length > 0 || Boolean(anchorNode?.nodeId);
|
||||
const viewOperationId = `${message.id}:canvas-view:${Date.now()}`;
|
||||
|
||||
if (hasComparisonViewIntent) {
|
||||
const comparisonViewPlan = buildCanvasAgentComparisonViewPlan({
|
||||
context: requestCanvasAgentContext,
|
||||
projectId: contextPayload.projectId,
|
||||
operationId: viewOperationId,
|
||||
});
|
||||
if (comparisonViewPlan) return comparisonViewPlan;
|
||||
}
|
||||
|
||||
if (/看全局|看整体|看全部|全局|整体/.test(lowerText)) {
|
||||
return {
|
||||
version: 1,
|
||||
|
||||
@ -5,6 +5,7 @@ import type {
|
||||
CanvasOperationPlan,
|
||||
CanvasViewOperationPlan,
|
||||
} from "@/types/canvas/agent-protocol";
|
||||
import { hasCanvasAgentComparisonViewIntent } from "@/lib/canvas/agent-view-intent";
|
||||
|
||||
const plannerDecisionSchema = z.object({
|
||||
action: z.enum([
|
||||
@ -401,6 +402,7 @@ export function shouldConsiderCanvasAgentModelPlanner(params: {
|
||||
if (!text) return false;
|
||||
|
||||
const hasViewIntent =
|
||||
hasCanvasAgentComparisonViewIntent(text) ||
|
||||
/视角|视野|画布|聚焦|定位|居中|看全局|看整体|看全部|缩小|放大|拉近|拉远|zoom/.test(
|
||||
text
|
||||
);
|
||||
|
||||
119
lib/canvas/agent-view-intent.ts
Normal file
119
lib/canvas/agent-view-intent.ts
Normal file
@ -0,0 +1,119 @@
|
||||
import type {
|
||||
CanvasAgentContext,
|
||||
CanvasAgentNodeRef,
|
||||
CanvasViewOperationPlan,
|
||||
} from "@/types/canvas/agent-protocol";
|
||||
|
||||
function normalizeNodeIds(values: Array<string | null | undefined>, maxItems = 20) {
|
||||
return Array.from(
|
||||
new Set(values.map((value) => String(value || "").trim()).filter(Boolean))
|
||||
).slice(0, maxItems);
|
||||
}
|
||||
|
||||
function getNodeSourceIds(node?: CanvasAgentNodeRef | null) {
|
||||
const metadata = node?.metadata || {};
|
||||
const sourceIds = Array.isArray(metadata.sourceNodeIds)
|
||||
? metadata.sourceNodeIds
|
||||
.map((value) => String(value || "").trim())
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
return normalizeNodeIds([
|
||||
typeof metadata.revisionOf === "string" ? metadata.revisionOf : undefined,
|
||||
typeof metadata.sourceNodeId === "string" ? metadata.sourceNodeId : undefined,
|
||||
...sourceIds,
|
||||
]);
|
||||
}
|
||||
|
||||
function isAgentResultNode(node?: CanvasAgentNodeRef | null) {
|
||||
const metadata = node?.metadata || {};
|
||||
return (
|
||||
metadata.generatedByAgent === true ||
|
||||
typeof metadata.canvasAgentOperationId === "string" ||
|
||||
typeof metadata.canvasAgentMediaItemId === "string" ||
|
||||
getNodeSourceIds(node).length > 0
|
||||
);
|
||||
}
|
||||
|
||||
export function hasCanvasAgentComparisonViewIntent(userText: string) {
|
||||
return /对比|比较|一起看|同时看|原图.*新图|新图.*原图/.test(
|
||||
userText.toLowerCase()
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveCanvasAgentComparisonNodeIds(params: {
|
||||
context: CanvasAgentContext;
|
||||
maxItems?: number;
|
||||
}) {
|
||||
const maxItems = Math.max(2, Math.min(20, params.maxItems || 10));
|
||||
const allNodes = [...params.context.selectedNodes, ...params.context.candidateNodes];
|
||||
const nodeMap = new Map(allNodes.map((node) => [node.nodeId, node]));
|
||||
const selectedNodeIds = normalizeNodeIds(
|
||||
[
|
||||
...params.context.selectedNodeIds,
|
||||
...params.context.selectedNodes
|
||||
.filter((node) => node.isSelected)
|
||||
.map((node) => node.nodeId),
|
||||
],
|
||||
maxItems
|
||||
).filter((nodeId) => nodeMap.has(nodeId));
|
||||
|
||||
if (selectedNodeIds.length >= 2) return selectedNodeIds.slice(0, maxItems);
|
||||
|
||||
const selectedNode = selectedNodeIds[0] ? nodeMap.get(selectedNodeIds[0]) : null;
|
||||
const selectedSourceIds = getNodeSourceIds(selectedNode).filter((nodeId) =>
|
||||
nodeMap.has(nodeId)
|
||||
);
|
||||
if (selectedNode?.nodeId && selectedSourceIds.length > 0) {
|
||||
return normalizeNodeIds([...selectedSourceIds, selectedNode.nodeId], maxItems);
|
||||
}
|
||||
|
||||
if (selectedNode?.nodeId) {
|
||||
const relatedResult = allNodes.find(
|
||||
(node) =>
|
||||
node.nodeId !== selectedNode.nodeId &&
|
||||
isAgentResultNode(node) &&
|
||||
getNodeSourceIds(node).includes(selectedNode.nodeId)
|
||||
);
|
||||
if (relatedResult) {
|
||||
return normalizeNodeIds([selectedNode.nodeId, relatedResult.nodeId], maxItems);
|
||||
}
|
||||
}
|
||||
|
||||
const latestResultWithSource = allNodes.find((node) => {
|
||||
if (!isAgentResultNode(node)) return false;
|
||||
return getNodeSourceIds(node).some((nodeId) => nodeMap.has(nodeId));
|
||||
});
|
||||
if (latestResultWithSource) {
|
||||
return normalizeNodeIds(
|
||||
[...getNodeSourceIds(latestResultWithSource), latestResultWithSource.nodeId],
|
||||
maxItems
|
||||
).filter((nodeId) => nodeMap.has(nodeId));
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
export function buildCanvasAgentComparisonViewPlan(params: {
|
||||
context: CanvasAgentContext;
|
||||
projectId: string;
|
||||
operationId: string;
|
||||
reason?: string;
|
||||
}): CanvasViewOperationPlan | null {
|
||||
const nodeIds = resolveCanvasAgentComparisonNodeIds({
|
||||
context: params.context,
|
||||
maxItems: 10,
|
||||
});
|
||||
if (nodeIds.length < 2) return null;
|
||||
|
||||
return {
|
||||
version: 1,
|
||||
viewOperationId: `${params.operationId}:view`,
|
||||
operation: "focus_nodes",
|
||||
projectId: params.projectId,
|
||||
nodeIds,
|
||||
anchorNodeId: nodeIds[0],
|
||||
padding: 140,
|
||||
persist: false,
|
||||
reason: params.reason || "focus_canvas_comparison_targets",
|
||||
};
|
||||
}
|
||||
@ -49,6 +49,11 @@ import {
|
||||
} from "@/lib/canvas/agent-model-planner";
|
||||
import { resolveCanvasAgentMediaPlacements } from "@/lib/canvas/agent-placement";
|
||||
import { buildCanvasAgentResultViewOperation } from "@/lib/canvas/agent-result-view";
|
||||
import {
|
||||
buildCanvasAgentComparisonViewPlan,
|
||||
hasCanvasAgentComparisonViewIntent,
|
||||
resolveCanvasAgentComparisonNodeIds,
|
||||
} from "@/lib/canvas/agent-view-intent";
|
||||
import {
|
||||
computeCanvasAgentPanViewport,
|
||||
resolveCanvasViewOperation,
|
||||
@ -174,6 +179,12 @@ test.describe("canvas chat pure helpers", () => {
|
||||
context: plannerContext,
|
||||
})
|
||||
).toBe(true);
|
||||
expect(
|
||||
shouldConsiderCanvasAgentModelPlanner({
|
||||
userText: "对比原图和新图",
|
||||
context: plannerContext,
|
||||
})
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("normalizes model-planned empty slot creation", () => {
|
||||
@ -1259,6 +1270,89 @@ test.describe("canvas chat pure helpers", () => {
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
test("resolves comparison view targets from selection and revision metadata", () => {
|
||||
const comparisonContext: CanvasAgentContext = {
|
||||
...plannerContext,
|
||||
selectedNodeIds: ["agent-result-1"],
|
||||
selectedNodes: [
|
||||
{
|
||||
nodeId: "agent-result-1",
|
||||
type: "image",
|
||||
title: "白底版本",
|
||||
x: 460,
|
||||
y: 20,
|
||||
width: 300,
|
||||
height: 240,
|
||||
isSelected: true,
|
||||
metadata: {
|
||||
generatedByAgent: true,
|
||||
revisionOf: "image-1",
|
||||
sourceNodeIds: ["image-1"],
|
||||
},
|
||||
},
|
||||
],
|
||||
candidateNodes: [
|
||||
{
|
||||
nodeId: "image-1",
|
||||
type: "image",
|
||||
title: "原图",
|
||||
x: 10,
|
||||
y: 20,
|
||||
width: 300,
|
||||
height: 240,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(hasCanvasAgentComparisonViewIntent("对比原图和新图")).toBe(true);
|
||||
expect(
|
||||
resolveCanvasAgentComparisonNodeIds({
|
||||
context: comparisonContext,
|
||||
})
|
||||
).toEqual(["image-1", "agent-result-1"]);
|
||||
|
||||
expect(
|
||||
buildCanvasAgentComparisonViewPlan({
|
||||
context: comparisonContext,
|
||||
projectId: plannerContext.projectId || "",
|
||||
operationId: "operation-compare",
|
||||
})
|
||||
).toMatchObject({
|
||||
operation: "focus_nodes",
|
||||
nodeIds: ["image-1", "agent-result-1"],
|
||||
anchorNodeId: "image-1",
|
||||
persist: false,
|
||||
reason: "focus_canvas_comparison_targets",
|
||||
});
|
||||
|
||||
expect(
|
||||
resolveCanvasAgentComparisonNodeIds({
|
||||
context: {
|
||||
...comparisonContext,
|
||||
selectedNodeIds: ["image-1", "agent-result-1"],
|
||||
selectedNodes: [
|
||||
{
|
||||
nodeId: "image-1",
|
||||
type: "image",
|
||||
x: 10,
|
||||
y: 20,
|
||||
isSelected: true,
|
||||
},
|
||||
{
|
||||
nodeId: "agent-result-1",
|
||||
type: "image",
|
||||
x: 460,
|
||||
y: 20,
|
||||
isSelected: true,
|
||||
metadata: { generatedByAgent: true, revisionOf: "image-1" },
|
||||
},
|
||||
],
|
||||
candidateNodes: [],
|
||||
},
|
||||
})
|
||||
).toEqual(["image-1", "agent-result-1"]);
|
||||
});
|
||||
|
||||
test("resolves latest-result and pan canvas view operations", () => {
|
||||
expect(
|
||||
resolveCanvasViewOperationTargetNodeIds({
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user