fix: keep annotation edit target in canvas agent context
This commit is contained in:
parent
c0550988e0
commit
8ee7eb1f0a
@ -132,6 +132,10 @@ import {
|
||||
shouldRetryCanvasViewOperationAfterRefresh,
|
||||
shouldSkipCanvasMediaPayloadForHandledItems,
|
||||
} from "@/components/canvas/chat-panel/canvas-agent-stream";
|
||||
import {
|
||||
mergePendingCanvasAgentTargetNodeIds,
|
||||
resolvePendingCanvasAgentTargetNode,
|
||||
} from "@/components/canvas/chat-panel/canvas-agent-targets";
|
||||
import {
|
||||
type DetailArchive,
|
||||
type DetailBriefForm,
|
||||
@ -1252,6 +1256,10 @@ export function CanvasChatPanel({
|
||||
const handledCanvasMediaItemIdsRef = useRef<Set<string>>(new Set());
|
||||
const pendingCanvasViewOperationRef =
|
||||
useRef<CanvasViewOperationStreamPayload | null>(null);
|
||||
const pendingCanvasAgentAnnotationEditTargetRef = useRef<{
|
||||
nodeId: string;
|
||||
requestedAt: number;
|
||||
} | null>(null);
|
||||
const lastWorkflowAutofillSignatureRef = useRef<string | null>(null);
|
||||
const lastWorkflowAutofillUrlsRef = useRef<string[]>([]);
|
||||
const pendingLaunchRef = useRef<CanvasLaunchPayload | null>(null);
|
||||
@ -3626,6 +3634,13 @@ export function CanvasChatPanel({
|
||||
annotationCount?: number;
|
||||
}>
|
||||
).detail;
|
||||
const nodeId = String(detail?.nodeId || "").trim();
|
||||
if (nodeId) {
|
||||
pendingCanvasAgentAnnotationEditTargetRef.current = {
|
||||
nodeId,
|
||||
requestedAt: Date.now(),
|
||||
};
|
||||
}
|
||||
const title = String(detail?.title || "").trim();
|
||||
const prompt = title
|
||||
? `按「${title}」这张图上的批注修改`
|
||||
@ -4145,6 +4160,7 @@ export function CanvasChatPanel({
|
||||
hiddenPromptRef.current = null;
|
||||
autoContextRef.current = null;
|
||||
contextPayloadRef.current = null;
|
||||
pendingCanvasAgentAnnotationEditTargetRef.current = null;
|
||||
return {
|
||||
body: {
|
||||
id: rawId,
|
||||
@ -5638,7 +5654,25 @@ export function CanvasChatPanel({
|
||||
}, [nodes, selectedImageNodes]);
|
||||
|
||||
const buildContextPayload = useCallback(() => {
|
||||
const selectedNodeIdSet = new Set(selectedNodeIds);
|
||||
const pendingAnnotationTargetNode = resolvePendingCanvasAgentTargetNode({
|
||||
nodes,
|
||||
pendingTarget: pendingCanvasAgentAnnotationEditTargetRef.current,
|
||||
isEligibleNode: (node) =>
|
||||
node.type === "image" &&
|
||||
isValidChatHttpUrl(String((node.data as any)?.src || "").trim()),
|
||||
});
|
||||
if (
|
||||
pendingCanvasAgentAnnotationEditTargetRef.current &&
|
||||
!pendingAnnotationTargetNode
|
||||
) {
|
||||
pendingCanvasAgentAnnotationEditTargetRef.current = null;
|
||||
}
|
||||
const effectiveSelectedNodeIds = mergePendingCanvasAgentTargetNodeIds({
|
||||
selectedNodeIds,
|
||||
pendingTargetNodeId: pendingAnnotationTargetNode?.id,
|
||||
maxItems: 20,
|
||||
});
|
||||
const selectedNodeIdSet = new Set(effectiveSelectedNodeIds);
|
||||
const imageNodes = nodes.filter(
|
||||
(node) =>
|
||||
node.type === "image" &&
|
||||
@ -5647,8 +5681,15 @@ export function CanvasChatPanel({
|
||||
|
||||
const effectiveComposerMode = activeComposerModeRef.current || activeComposerMode;
|
||||
const shouldIsolateDirectModeContext = effectiveComposerMode !== "agent";
|
||||
const selectedImageNodesForContext = selectedImageNodes.filter((node) =>
|
||||
isValidChatHttpUrl(String((node.data as any)?.src || "").trim())
|
||||
const selectedImageNodesForContext = Array.from(
|
||||
new Map(
|
||||
[
|
||||
...selectedImageNodes.filter((node) =>
|
||||
isValidChatHttpUrl(String((node.data as any)?.src || "").trim())
|
||||
),
|
||||
...(pendingAnnotationTargetNode ? [pendingAnnotationTargetNode] : []),
|
||||
].map((node) => [node.id, node])
|
||||
).values()
|
||||
);
|
||||
|
||||
if (shouldIsolateDirectModeContext && selectedImageNodesForContext.length === 0) {
|
||||
|
||||
59
components/canvas/chat-panel/canvas-agent-targets.ts
Normal file
59
components/canvas/chat-panel/canvas-agent-targets.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import type { CanvasNode } from "@/types/canvas";
|
||||
|
||||
export type PendingCanvasAgentTarget = {
|
||||
nodeId?: string | null;
|
||||
requestedAt?: number | null;
|
||||
};
|
||||
|
||||
const DEFAULT_PENDING_TARGET_MAX_AGE_MS = 10 * 60 * 1000;
|
||||
|
||||
function normalizeNodeId(value: unknown) {
|
||||
return typeof value === "string" ? value.trim() : "";
|
||||
}
|
||||
|
||||
export function resolvePendingCanvasAgentTargetNode(params: {
|
||||
nodes: CanvasNode[];
|
||||
pendingTarget?: PendingCanvasAgentTarget | null;
|
||||
now?: number;
|
||||
maxAgeMs?: number;
|
||||
isEligibleNode?: (node: CanvasNode) => boolean;
|
||||
}) {
|
||||
const nodeId = normalizeNodeId(params.pendingTarget?.nodeId);
|
||||
if (!nodeId) return null;
|
||||
|
||||
const requestedAt = Number(params.pendingTarget?.requestedAt || 0);
|
||||
const now = Number(params.now ?? Date.now());
|
||||
const maxAgeMs = Math.max(
|
||||
0,
|
||||
Number(params.maxAgeMs ?? DEFAULT_PENDING_TARGET_MAX_AGE_MS)
|
||||
);
|
||||
if (
|
||||
Number.isFinite(requestedAt) &&
|
||||
requestedAt > 0 &&
|
||||
Number.isFinite(now) &&
|
||||
now - requestedAt > maxAgeMs
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const node = params.nodes.find((item) => item.id === nodeId) || null;
|
||||
if (!node) return null;
|
||||
if (params.isEligibleNode && !params.isEligibleNode(node)) return null;
|
||||
return node;
|
||||
}
|
||||
|
||||
export function mergePendingCanvasAgentTargetNodeIds(params: {
|
||||
selectedNodeIds: string[];
|
||||
pendingTargetNodeId?: string | null;
|
||||
maxItems?: number;
|
||||
}) {
|
||||
const maxItems = Math.max(1, Number(params.maxItems || 20));
|
||||
return Array.from(
|
||||
new Set(
|
||||
[
|
||||
...params.selectedNodeIds,
|
||||
normalizeNodeId(params.pendingTargetNodeId),
|
||||
].filter(Boolean)
|
||||
)
|
||||
).slice(0, maxItems);
|
||||
}
|
||||
@ -12,6 +12,10 @@ import {
|
||||
shouldRetryCanvasViewOperationAfterRefresh,
|
||||
shouldSkipCanvasMediaPayloadForHandledItems,
|
||||
} from "@/components/canvas/chat-panel/canvas-agent-stream";
|
||||
import {
|
||||
mergePendingCanvasAgentTargetNodeIds,
|
||||
resolvePendingCanvasAgentTargetNode,
|
||||
} from "@/components/canvas/chat-panel/canvas-agent-targets";
|
||||
import {
|
||||
writeCanvasMediaChunk,
|
||||
writeCanvasOperationChunk,
|
||||
@ -2237,6 +2241,94 @@ test.describe("canvas chat pure helpers", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test("keeps pending annotation edit targets eligible for the next canvas context", () => {
|
||||
const nodes = [
|
||||
{
|
||||
id: "image-annotated",
|
||||
type: "image",
|
||||
position: { x: 10, y: 20 },
|
||||
size: { width: 300, height: 240 },
|
||||
data: {
|
||||
src: "https://cdn.example.com/annotated.png",
|
||||
metadata: {
|
||||
annotations: [
|
||||
{
|
||||
id: "annotation-1",
|
||||
type: "text-note",
|
||||
text: "背景换浅灰",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "text-1",
|
||||
type: "text",
|
||||
position: { x: 500, y: 20 },
|
||||
size: { width: 220, height: 80 },
|
||||
data: { content: "说明" },
|
||||
},
|
||||
] as any[];
|
||||
|
||||
const pendingNode = resolvePendingCanvasAgentTargetNode({
|
||||
nodes,
|
||||
pendingTarget: {
|
||||
nodeId: "image-annotated",
|
||||
requestedAt: 1000,
|
||||
},
|
||||
now: 1500,
|
||||
isEligibleNode: (node) => node.type === "image",
|
||||
});
|
||||
|
||||
expect(pendingNode?.id).toBe("image-annotated");
|
||||
expect(
|
||||
resolvePendingCanvasAgentTargetNode({
|
||||
nodes,
|
||||
pendingTarget: {
|
||||
nodeId: "image-annotated",
|
||||
requestedAt: 1000,
|
||||
},
|
||||
now: 1000 + 10 * 60 * 1000 + 1,
|
||||
isEligibleNode: (node) => node.type === "image",
|
||||
})
|
||||
).toBeNull();
|
||||
expect(
|
||||
resolvePendingCanvasAgentTargetNode({
|
||||
nodes,
|
||||
pendingTarget: {
|
||||
nodeId: "text-1",
|
||||
requestedAt: 1000,
|
||||
},
|
||||
now: 1500,
|
||||
isEligibleNode: (node) => node.type === "image",
|
||||
})
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
test("merges pending annotation edit targets into selected node ids without duplicates", () => {
|
||||
expect(
|
||||
mergePendingCanvasAgentTargetNodeIds({
|
||||
selectedNodeIds: ["image-1", "image-2"],
|
||||
pendingTargetNodeId: "image-1",
|
||||
})
|
||||
).toEqual(["image-1", "image-2"]);
|
||||
|
||||
expect(
|
||||
mergePendingCanvasAgentTargetNodeIds({
|
||||
selectedNodeIds: ["image-1"],
|
||||
pendingTargetNodeId: "image-annotated",
|
||||
})
|
||||
).toEqual(["image-1", "image-annotated"]);
|
||||
|
||||
expect(
|
||||
mergePendingCanvasAgentTargetNodeIds({
|
||||
selectedNodeIds: Array.from({ length: 20 }, (_, index) => `node-${index + 1}`),
|
||||
pendingTargetNodeId: "node-21",
|
||||
maxItems: 20,
|
||||
})
|
||||
).toHaveLength(20);
|
||||
});
|
||||
|
||||
test("builds annotation-aware edit prompts only for annotation requests", () => {
|
||||
const context: CanvasAgentContext = {
|
||||
...plannerContext,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user