fix: add canvas agent pan view intent
This commit is contained in:
parent
38435f9401
commit
e73c2da84e
@ -78,6 +78,10 @@ import {
|
||||
buildCanvasAgentComparisonViewPlan,
|
||||
hasCanvasAgentComparisonViewIntent,
|
||||
} from "@/lib/canvas/agent-view-intent";
|
||||
import {
|
||||
buildCanvasAgentPanViewPlan,
|
||||
hasCanvasAgentPanViewIntent,
|
||||
} from "@/lib/canvas/agent-view-pan";
|
||||
import { buildCanvasAgentDirectWritebackPlan } from "@/lib/canvas/agent-direct-writeback";
|
||||
import {
|
||||
getCanvasAgentOperationErrorMessage,
|
||||
@ -3180,8 +3184,10 @@ export async function POST(request: Request) {
|
||||
|
||||
const lowerText = text.toLowerCase();
|
||||
const hasComparisonViewIntent = hasCanvasAgentComparisonViewIntent(text);
|
||||
const hasPanViewIntent = hasCanvasAgentPanViewIntent(text);
|
||||
const hasViewKeyword =
|
||||
hasComparisonViewIntent ||
|
||||
hasPanViewIntent ||
|
||||
/视角|视野|画布|聚焦|定位|居中|看全局|看整体|看全部|缩小|放大|拉近|拉远|zoom/.test(
|
||||
lowerText
|
||||
);
|
||||
@ -3267,6 +3273,16 @@ export async function POST(request: Request) {
|
||||
};
|
||||
}
|
||||
|
||||
if (hasPanViewIntent) {
|
||||
const panViewPlan = buildCanvasAgentPanViewPlan({
|
||||
userText: text,
|
||||
context: requestCanvasAgentContext,
|
||||
projectId: contextPayload.projectId,
|
||||
operationId: viewOperationId,
|
||||
});
|
||||
if (panViewPlan) return panViewPlan;
|
||||
}
|
||||
|
||||
if (/放大|拉近|zoom\s*in/.test(lowerText)) {
|
||||
return {
|
||||
version: 1,
|
||||
@ -3716,14 +3732,16 @@ export async function POST(request: Request) {
|
||||
|
||||
const canvasViewOperationPlan = resolveCanvasViewOperationPlan();
|
||||
if (canvasViewOperationPlan) {
|
||||
const canvasViewReply =
|
||||
canvasViewOperationPlan.operation === "fit_all"
|
||||
? "已缩小到画布全局视图。"
|
||||
: canvasViewOperationPlan.operation === "zoom_in"
|
||||
? "已放大画布视角。"
|
||||
: canvasViewOperationPlan.operation === "zoom_out"
|
||||
? "已缩小画布视角。"
|
||||
: "已聚焦到当前编辑对象。";
|
||||
let canvasViewReply = "已聚焦到当前编辑对象。";
|
||||
if (canvasViewOperationPlan.operation === "fit_all") {
|
||||
canvasViewReply = "已缩小到画布全局视图。";
|
||||
} else if (canvasViewOperationPlan.operation === "zoom_in") {
|
||||
canvasViewReply = "已放大画布视角。";
|
||||
} else if (canvasViewOperationPlan.operation === "zoom_out") {
|
||||
canvasViewReply = "已缩小画布视角。";
|
||||
} else if (canvasViewOperationPlan.operation === "pan_to") {
|
||||
canvasViewReply = "已平移画布视角。";
|
||||
}
|
||||
const canvasViewStream = createUIMessageStream({
|
||||
execute: async ({ writer }) => {
|
||||
const messageId = generateUUID();
|
||||
|
||||
@ -6,6 +6,7 @@ import type {
|
||||
CanvasViewOperationPlan,
|
||||
} from "@/types/canvas/agent-protocol";
|
||||
import { hasCanvasAgentComparisonViewIntent } from "@/lib/canvas/agent-view-intent";
|
||||
import { hasCanvasAgentPanViewIntent } from "@/lib/canvas/agent-view-pan";
|
||||
|
||||
const plannerDecisionSchema = z.object({
|
||||
action: z.enum([
|
||||
@ -409,6 +410,7 @@ export function shouldConsiderCanvasAgentModelPlanner(params: {
|
||||
|
||||
const hasViewIntent =
|
||||
hasCanvasAgentComparisonViewIntent(text) ||
|
||||
hasCanvasAgentPanViewIntent(text) ||
|
||||
/视角|视野|画布|聚焦|定位|居中|看全局|看整体|看全部|缩小|放大|拉近|拉远|zoom/.test(
|
||||
text
|
||||
);
|
||||
|
||||
99
lib/canvas/agent-view-pan.ts
Normal file
99
lib/canvas/agent-view-pan.ts
Normal file
@ -0,0 +1,99 @@
|
||||
import type {
|
||||
CanvasAgentContext,
|
||||
CanvasViewOperationPlan,
|
||||
} from "@/types/canvas/agent-protocol";
|
||||
|
||||
const VIEWPORT_ANCHOR_X_RATIO = 0.52;
|
||||
const VIEWPORT_ANCHOR_Y_RATIO = 0.46;
|
||||
|
||||
function toFiniteNumber(value: unknown, fallback: number) {
|
||||
const numericValue = Number(value);
|
||||
return Number.isFinite(numericValue) ? numericValue : fallback;
|
||||
}
|
||||
|
||||
function getPanDirection(userText: string) {
|
||||
const text = userText.trim().toLowerCase();
|
||||
const hasHorizontalIntent =
|
||||
/左上|左下|右上|右下|向左|往左|左边|左侧|向右|往右|右边|右侧/.test(
|
||||
text
|
||||
);
|
||||
const hasVerticalIntent =
|
||||
/左上|右上|左下|右下|向上|往上|上面|上方|向下|往下|下面|下方/.test(
|
||||
text
|
||||
);
|
||||
if (!hasHorizontalIntent && !hasVerticalIntent) return null;
|
||||
|
||||
const hasPanVerb =
|
||||
/平移|拖动|拖拽|挪动|移动(?:画布|视角|视野)|拖(?:一下|一点|画布)?|看(?:看|一下|一点)?/.test(
|
||||
text
|
||||
) ||
|
||||
/(?:向|往)(?:左|右|上|下).*(?:看|移动|拖|挪)/.test(text) ||
|
||||
/(?:左边|左侧|右边|右侧|上面|上方|下面|下方).*(?:看看|看一下|视角|视野|画布)/.test(
|
||||
text
|
||||
);
|
||||
if (!hasPanVerb) return null;
|
||||
|
||||
return {
|
||||
x: /右上|右下|向右|往右|右边|右侧/.test(text)
|
||||
? 1
|
||||
: /左上|左下|向左|往左|左边|左侧/.test(text)
|
||||
? -1
|
||||
: 0,
|
||||
y: /左下|右下|向下|往下|下面|下方/.test(text)
|
||||
? 1
|
||||
: /左上|右上|向上|往上|上面|上方/.test(text)
|
||||
? -1
|
||||
: 0,
|
||||
};
|
||||
}
|
||||
|
||||
function getPanScale(userText: string) {
|
||||
const text = userText.trim().toLowerCase();
|
||||
if (/大幅|多一点|远一点|一大段|很多|more|far/.test(text)) return 0.52;
|
||||
if (/一点|一下|少许|稍微|轻微|小幅|slightly/.test(text)) return 0.26;
|
||||
return 0.36;
|
||||
}
|
||||
|
||||
export function hasCanvasAgentPanViewIntent(userText: string) {
|
||||
const direction = getPanDirection(userText);
|
||||
return Boolean(direction && (direction.x !== 0 || direction.y !== 0));
|
||||
}
|
||||
|
||||
export function buildCanvasAgentPanViewPlan(params: {
|
||||
userText: string;
|
||||
context: CanvasAgentContext;
|
||||
projectId: string;
|
||||
operationId: string;
|
||||
reason?: string;
|
||||
}): CanvasViewOperationPlan | null {
|
||||
const direction = getPanDirection(params.userText);
|
||||
if (!direction || (direction.x === 0 && direction.y === 0)) return null;
|
||||
|
||||
const viewport = params.context.viewport || { x: 0, y: 0, zoom: 1 };
|
||||
const zoom = Math.max(0.1, toFiniteNumber(viewport.zoom, 1));
|
||||
const stageWidth = Math.max(1, toFiniteNumber(viewport.width, 1200));
|
||||
const stageHeight = Math.max(1, toFiniteNumber(viewport.height, 800));
|
||||
const currentCenterX =
|
||||
(stageWidth * VIEWPORT_ANCHOR_X_RATIO - toFiniteNumber(viewport.x, 0)) /
|
||||
zoom;
|
||||
const currentCenterY =
|
||||
(stageHeight * VIEWPORT_ANCHOR_Y_RATIO - toFiniteNumber(viewport.y, 0)) /
|
||||
zoom;
|
||||
const scale = getPanScale(params.userText);
|
||||
const stepX = (stageWidth * scale) / zoom;
|
||||
const stepY = (stageHeight * scale) / zoom;
|
||||
|
||||
return {
|
||||
version: 1,
|
||||
viewOperationId: `${params.operationId}:view`,
|
||||
operation: "pan_to",
|
||||
projectId: params.projectId,
|
||||
center: {
|
||||
x: currentCenterX + direction.x * stepX,
|
||||
y: currentCenterY + direction.y * stepY,
|
||||
},
|
||||
zoom,
|
||||
persist: false,
|
||||
reason: params.reason || "pan_canvas_view",
|
||||
};
|
||||
}
|
||||
@ -67,6 +67,10 @@ import {
|
||||
hasCanvasAgentComparisonViewIntent,
|
||||
resolveCanvasAgentComparisonNodeIds,
|
||||
} from "@/lib/canvas/agent-view-intent";
|
||||
import {
|
||||
buildCanvasAgentPanViewPlan,
|
||||
hasCanvasAgentPanViewIntent,
|
||||
} from "@/lib/canvas/agent-view-pan";
|
||||
import {
|
||||
computeCanvasAgentPanViewport,
|
||||
resolveCanvasViewOperation,
|
||||
@ -272,6 +276,12 @@ test.describe("canvas chat pure helpers", () => {
|
||||
context: plannerContext,
|
||||
})
|
||||
).toBe(true);
|
||||
expect(
|
||||
shouldConsiderCanvasAgentModelPlanner({
|
||||
userText: "向右移动视角一点",
|
||||
context: plannerContext,
|
||||
})
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("treats selected agent slots as canvas routing context", () => {
|
||||
@ -1777,6 +1787,48 @@ test.describe("canvas chat pure helpers", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("builds relative pan view plans from canvas viewport context", () => {
|
||||
expect(hasCanvasAgentPanViewIntent("右边那张图")).toBe(false);
|
||||
expect(hasCanvasAgentPanViewIntent("向右移动视角一点")).toBe(true);
|
||||
|
||||
const plan = buildCanvasAgentPanViewPlan({
|
||||
userText: "向右移动视角一点",
|
||||
context: {
|
||||
...plannerContext,
|
||||
viewport: { x: -200, y: -100, zoom: 2, width: 1000, height: 800 },
|
||||
},
|
||||
projectId: plannerContext.projectId || "",
|
||||
operationId: "view-pan",
|
||||
});
|
||||
|
||||
expect(plan).toMatchObject({
|
||||
version: 1,
|
||||
viewOperationId: "view-pan:view",
|
||||
operation: "pan_to",
|
||||
projectId: plannerContext.projectId,
|
||||
center: {
|
||||
x: 490,
|
||||
y: 234,
|
||||
},
|
||||
zoom: 2,
|
||||
persist: false,
|
||||
reason: "pan_canvas_view",
|
||||
});
|
||||
|
||||
const diagonalPlan = buildCanvasAgentPanViewPlan({
|
||||
userText: "把画布往左上拖一点",
|
||||
context: {
|
||||
...plannerContext,
|
||||
viewport: { x: -200, y: -100, zoom: 2, width: 1000, height: 800 },
|
||||
},
|
||||
projectId: plannerContext.projectId || "",
|
||||
operationId: "view-pan-diagonal",
|
||||
});
|
||||
|
||||
expect(diagonalPlan?.center?.x).toBeCloseTo(230);
|
||||
expect(diagonalPlan?.center?.y).toBeCloseTo(130);
|
||||
});
|
||||
|
||||
test("resolves canvas view operation plans into transient viewport targets", () => {
|
||||
const zoomIn = resolveCanvasViewOperation({
|
||||
plan: {
|
||||
@ -1853,6 +1905,26 @@ test.describe("canvas chat pure helpers", () => {
|
||||
expect(fitAll?.viewport.zoom).toBeCloseTo(0.1784615385);
|
||||
expect(fitAll?.viewport.x).toBeCloseTo(305.8461538);
|
||||
expect(fitAll?.viewport.y).toBeCloseTo(153.8461538);
|
||||
|
||||
const panTo = resolveCanvasViewOperation({
|
||||
plan: {
|
||||
version: 1,
|
||||
viewOperationId: "view-pan-to",
|
||||
operation: "pan_to",
|
||||
center: { x: 490, y: 234 },
|
||||
zoom: 2,
|
||||
},
|
||||
nodes: [],
|
||||
currentViewport: { x: -200, y: -100, zoom: 2 },
|
||||
stage: { width: 1000, height: 800 },
|
||||
});
|
||||
|
||||
expect(panTo?.targetNodeIds).toEqual([]);
|
||||
expect(panTo?.viewport).toEqual({
|
||||
x: -460,
|
||||
y: -100,
|
||||
zoom: 2,
|
||||
});
|
||||
});
|
||||
|
||||
test("resolves grid media placements to the right of an anchor node", () => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user