fix: preserve canvas context for slot controls
This commit is contained in:
parent
d9bb014742
commit
f23fae7676
@ -98,6 +98,7 @@ import { shouldBypassCanvasImagePointsPrecheckForAgentControl } from "@/lib/canv
|
||||
import { applyCanvasOperationPlan } from "@/lib/canvas/agent-mutations";
|
||||
import { isProductionEnvironment } from "@/lib/constants";
|
||||
import type {
|
||||
CanvasAgentContext,
|
||||
CanvasOperationPlan,
|
||||
CanvasViewOperationPlan,
|
||||
} from "@/types/canvas/agent-protocol";
|
||||
@ -881,23 +882,28 @@ export async function POST(request: Request) {
|
||||
return "";
|
||||
}
|
||||
})();
|
||||
const isCanvasPageRequest = (() => {
|
||||
const canvasPageRequest = (() => {
|
||||
const referer = request.headers.get("referer");
|
||||
if (!referer) return false;
|
||||
if (!referer) return { isCanvasPage: false, projectId: "" };
|
||||
try {
|
||||
const pathname = new URL(referer).pathname;
|
||||
return pathname === "/canvas" || pathname.startsWith("/canvas/");
|
||||
const match = pathname.match(/^\/canvas\/([^/?#]+)/);
|
||||
return {
|
||||
isCanvasPage: pathname === "/canvas" || pathname.startsWith("/canvas/"),
|
||||
projectId: match?.[1] || "",
|
||||
};
|
||||
} catch {
|
||||
return false;
|
||||
return { isCanvasPage: false, projectId: "" };
|
||||
}
|
||||
})();
|
||||
const isCanvasPageRequest = canvasPageRequest.isCanvasPage;
|
||||
const requestTextContent = message.parts
|
||||
.filter((part): part is { type: "text"; text: string } => part.type === "text")
|
||||
.map((part) => part.text)
|
||||
.join(" ")
|
||||
.trim();
|
||||
const autoContextText = typeof autoContext === "string" ? autoContext.trim() : "";
|
||||
const requestCanvasAgentContext = normalizeCanvasAgentContext(
|
||||
const initialRequestCanvasAgentContext = normalizeCanvasAgentContext(
|
||||
contextPayload || undefined
|
||||
);
|
||||
const canvasAgentFeatures = getCanvasAgentFeatureFlags();
|
||||
@ -910,7 +916,7 @@ export async function POST(request: Request) {
|
||||
composerMode !== "agent" &&
|
||||
(contextPayload?.source === "canvas" ||
|
||||
contextPayload?.source === "realtime-canvas" ||
|
||||
Boolean(requestCanvasAgentContext) ||
|
||||
Boolean(initialRequestCanvasAgentContext) ||
|
||||
isCanvasPageRequest) &&
|
||||
shouldBypassCanvasImagePointsPrecheckForAgentControl({
|
||||
text: requestTextContent,
|
||||
@ -920,6 +926,35 @@ export async function POST(request: Request) {
|
||||
hasWorkflowContext: Boolean(agentModePayload),
|
||||
agentEnabled: true,
|
||||
});
|
||||
const inferredCanvasProjectId =
|
||||
contextPayload?.projectId ||
|
||||
initialRequestCanvasAgentContext?.projectId ||
|
||||
canvasPageRequest.projectId ||
|
||||
(isCanvasPageRequest ? id : "");
|
||||
const requestCanvasAgentContext: CanvasAgentContext | null =
|
||||
initialRequestCanvasAgentContext ||
|
||||
(hasCanvasControlIntentFastPath && inferredCanvasProjectId
|
||||
? {
|
||||
version: 1,
|
||||
source: "canvas",
|
||||
projectId: inferredCanvasProjectId,
|
||||
viewport: { x: 0, y: 0, zoom: 1 },
|
||||
selectedNodeIds: [],
|
||||
selectedNodes: [],
|
||||
candidateNodes: [],
|
||||
slots: [],
|
||||
annotations: [],
|
||||
focusPolicy: "auto",
|
||||
}
|
||||
: null);
|
||||
const canvasAgentProjectId =
|
||||
contextPayload?.projectId ||
|
||||
requestCanvasAgentContext?.projectId ||
|
||||
inferredCanvasProjectId ||
|
||||
"";
|
||||
const isCanvasAgentCanvasRequest =
|
||||
contextPayload?.source === "canvas" ||
|
||||
(hasCanvasControlIntentFastPath && Boolean(canvasAgentProjectId));
|
||||
if (requestCanvasAgentContext) {
|
||||
chatApiLogger.info("[CanvasAgentContext] Normalized request context:", {
|
||||
source: requestCanvasAgentContext.source,
|
||||
@ -3401,7 +3436,7 @@ export async function POST(request: Request) {
|
||||
const resolveCanvasLinkOperationPlan = (): CanvasOperationPlan | null => {
|
||||
if (!canApplyCanvasAgentMutation) return null;
|
||||
if (composerMode !== "agent" && !hasCanvasControlIntentFastPath) return null;
|
||||
if (contextPayload?.source !== "canvas" || !contextPayload.projectId) return null;
|
||||
if (!isCanvasAgentCanvasRequest || !canvasAgentProjectId) return null;
|
||||
if (!requestCanvasAgentContext) return null;
|
||||
|
||||
const text = requestTextContent.trim().toLowerCase();
|
||||
@ -3428,7 +3463,7 @@ export async function POST(request: Request) {
|
||||
version: 1,
|
||||
operationId: `${message.id}:canvas-link`,
|
||||
operation: "link_nodes",
|
||||
projectId: contextPayload.projectId,
|
||||
projectId: canvasAgentProjectId,
|
||||
baseCanvasRevision: requestCanvasAgentContext.canvasRevision,
|
||||
baseCanvasUpdatedAt: requestCanvasAgentContext.canvasUpdatedAt,
|
||||
sourceNodeIds: nodeIds,
|
||||
@ -3471,7 +3506,7 @@ export async function POST(request: Request) {
|
||||
const resolveCanvasTextOperationPlan = (): CanvasOperationPlan | null => {
|
||||
if (!canApplyCanvasAgentMutation) return null;
|
||||
if (composerMode !== "agent" && !hasCanvasControlIntentFastPath) return null;
|
||||
if (contextPayload?.source !== "canvas" || !contextPayload.projectId) return null;
|
||||
if (!isCanvasAgentCanvasRequest || !canvasAgentProjectId) return null;
|
||||
if (!requestCanvasAgentContext) return null;
|
||||
|
||||
const text = requestTextContent.trim();
|
||||
@ -3498,7 +3533,7 @@ export async function POST(request: Request) {
|
||||
version: 1,
|
||||
operationId: `${message.id}:canvas-update-text`,
|
||||
operation: "update_text",
|
||||
projectId: contextPayload.projectId,
|
||||
projectId: canvasAgentProjectId,
|
||||
baseCanvasRevision: requestCanvasAgentContext.canvasRevision,
|
||||
baseCanvasUpdatedAt: requestCanvasAgentContext.canvasUpdatedAt,
|
||||
targetNodeIds: selectedTextNodeIds,
|
||||
@ -3516,12 +3551,12 @@ export async function POST(request: Request) {
|
||||
(): CanvasAgentExplicitOperationPlan | null => {
|
||||
if (!canApplyCanvasAgentMutation) return null;
|
||||
if (composerMode !== "agent" && !hasCanvasControlIntentFastPath) return null;
|
||||
if (contextPayload?.source !== "canvas" || !contextPayload.projectId) return null;
|
||||
if (!isCanvasAgentCanvasRequest || !canvasAgentProjectId) return null;
|
||||
if (!requestCanvasAgentContext) return null;
|
||||
|
||||
return buildCanvasAgentExplicitOperationPlan({
|
||||
context: requestCanvasAgentContext,
|
||||
projectId: contextPayload.projectId,
|
||||
projectId: canvasAgentProjectId,
|
||||
messageId: message.id,
|
||||
chatId: id,
|
||||
userText: requestTextContent,
|
||||
@ -3534,7 +3569,7 @@ export async function POST(request: Request) {
|
||||
const resolveCanvasModelPlannerOutcome =
|
||||
async (): Promise<CanvasAgentModelPlannerOutcome | null> => {
|
||||
if (composerMode !== "agent" && !hasCanvasControlIntentFastPath) return null;
|
||||
if (contextPayload?.source !== "canvas" || !contextPayload.projectId) return null;
|
||||
if (!isCanvasAgentCanvasRequest || !canvasAgentProjectId) return null;
|
||||
if (!requestCanvasAgentContext) return null;
|
||||
if (
|
||||
!canvasAgentFeatures.viewOperationEnabled &&
|
||||
@ -3561,7 +3596,7 @@ export async function POST(request: Request) {
|
||||
model: plannerModel || myProvider.languageModel("chat-model"),
|
||||
userText: requestTextContent,
|
||||
context: requestCanvasAgentContext,
|
||||
projectId: contextPayload.projectId,
|
||||
projectId: canvasAgentProjectId,
|
||||
messageId: message.id,
|
||||
chatId: id,
|
||||
});
|
||||
|
||||
@ -5890,6 +5890,16 @@ export function CanvasChatPanel({
|
||||
|
||||
const effectiveComposerMode = activeComposerModeRef.current || activeComposerMode;
|
||||
const shouldIsolateDirectModeContext = effectiveComposerMode !== "agent";
|
||||
const shouldPreserveCanvasControlContext =
|
||||
shouldBypassCanvasImagePointsPrecheckForAgentControl({
|
||||
text: input,
|
||||
activeComposerMode: effectiveComposerMode,
|
||||
attachmentCount: attachments.length,
|
||||
hasSkillTag: Boolean(selectedSkillTag),
|
||||
hasWorkflowContext,
|
||||
isDetailGenerationMode,
|
||||
agentEnabled: effectiveAgentEnabled,
|
||||
});
|
||||
const selectedImageNodesForContext = Array.from(
|
||||
new Map(
|
||||
[
|
||||
@ -5901,7 +5911,11 @@ export function CanvasChatPanel({
|
||||
).values()
|
||||
);
|
||||
|
||||
if (shouldIsolateDirectModeContext && selectedImageNodesForContext.length === 0) {
|
||||
if (
|
||||
shouldIsolateDirectModeContext &&
|
||||
selectedImageNodesForContext.length === 0 &&
|
||||
!shouldPreserveCanvasControlContext
|
||||
) {
|
||||
generationAnchorNodesRef.current = [];
|
||||
return undefined;
|
||||
}
|
||||
@ -6172,10 +6186,16 @@ export function CanvasChatPanel({
|
||||
};
|
||||
}, [
|
||||
activeComposerMode,
|
||||
attachments.length,
|
||||
canvasId,
|
||||
canvasRevision,
|
||||
canvasUpdatedAt,
|
||||
effectiveAgentEnabled,
|
||||
hasWorkflowContext,
|
||||
input,
|
||||
isDetailGenerationMode,
|
||||
nodes,
|
||||
selectedSkillTag,
|
||||
selectedImageNodes,
|
||||
selectedNodeIds,
|
||||
viewport,
|
||||
|
||||
@ -235,6 +235,7 @@ test.describe("canvas chat pure helpers", () => {
|
||||
expect(hasCanvasAgentNonGenerativeControlIntent("把画布放大一点")).toBe(true);
|
||||
expect(hasCanvasAgentNonGenerativeControlIntent("缩小看全局")).toBe(true);
|
||||
expect(hasCanvasAgentNonGenerativeControlIntent("新建一个图片槽位")).toBe(true);
|
||||
expect(hasCanvasAgentNonGenerativeControlIntent("创建一个图片槽位")).toBe(true);
|
||||
expect(hasCanvasAgentNonGenerativeControlIntent("删除选中的节点")).toBe(true);
|
||||
expect(hasCanvasAgentNonGenerativeControlIntent("把选中图片背景换白")).toBe(false);
|
||||
expect(hasCanvasAgentNonGenerativeControlIntent("生成一张产品主图")).toBe(false);
|
||||
@ -1126,6 +1127,31 @@ test.describe("canvas chat pure helpers", () => {
|
||||
},
|
||||
});
|
||||
expect(outcome?.reply).toContain("视频槽位");
|
||||
|
||||
const imageOutcome = buildCanvasAgentExplicitOperationPlan({
|
||||
context,
|
||||
projectId: plannerContext.projectId || "",
|
||||
messageId: "message-2",
|
||||
chatId: "chat-1",
|
||||
userText: "创建一个图片槽位",
|
||||
});
|
||||
|
||||
expect(imageOutcome?.operationPlan).toMatchObject({
|
||||
operation: "create_agent_slot",
|
||||
targetPosition: { x: 340, y: 240 },
|
||||
slot: {
|
||||
slotType: "image",
|
||||
label: "图片槽位",
|
||||
aspectRatio: "1:1",
|
||||
fillPolicy: "replace-empty",
|
||||
},
|
||||
metadata: {
|
||||
messageId: "message-2",
|
||||
chatId: "chat-1",
|
||||
planner: "deterministic",
|
||||
},
|
||||
});
|
||||
expect(imageOutcome?.reply).toContain("图片槽位");
|
||||
});
|
||||
|
||||
test("builds deterministic node rename plans without treating titles as image edits", () => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user