feat(canvas): add CAD vector semantic context and non-destructive vector crop
Extend CAD conversion to emit bounded semantic metadata (units, layers, blocks, entity types, labels, crop-indexed bounds) alongside the derived SVG, tag imported drawings as cad-vector nodes, and carry semanticContext through canvas nodes, snapshot normalization, and reference images. Agent Chat forwards this metadata as hidden cad-context reference parts. Add non-destructive vector cropping: cropping a vector/CAD node keeps the source SVG and spawns a transparent WebP sibling with crop-scoped semantic context, wired through the vectorized image toolbar and the workspace crop tool. Include node --test coverage for CAD semantics and vector crop. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -38,7 +38,8 @@ import { WorkspaceTitle } from "@/ui/components/WorkspaceTitle";
|
||||
import { SharePopover } from "@/ui/components/SharePopover";
|
||||
import { autoImageAdjustments, defaultImageAdjustments, parseImageAdjustments, serializeImageAdjustments } from "@/ui/lib/imageAdjustments";
|
||||
import { friendlyProjectFailureMessage } from "@/ui/lib/friendlyAgentErrors";
|
||||
import { cadImportMessageKey, isCadFile, isCanvasImportFile, prepareCanvasImportFile } from "@/ui/lib/cadImport";
|
||||
import { cadImportMessageKey, isCadFile, isCanvasImportFile, prepareCanvasImportAsset, type PreparedCanvasImportAsset } from "@/ui/lib/cadImport";
|
||||
import { cadSemanticContextFromSvgText } from "@/ui/lib/cadSemantics";
|
||||
import { isSvgSource, isVectorImageNode } from "@/ui/lib/vectorImage";
|
||||
import { canvasImageUrl } from "@/ui/lib/imageDelivery";
|
||||
import { createModelAgentContentPreparer } from "@/ui/lib/modelImageReferences";
|
||||
@@ -48,7 +49,9 @@ import { CanvasColorPopover } from "./canvas/CanvasColorPopover";
|
||||
import { buildAlignmentGuides, clamp, minGroupNodeScale, nodeBounds, nodeIntersectsRect, normalizeScreenRect, pointInsideNode, scaleGroupNode, screenBoundsForNodes, screenRectToWorldRect, type AlignmentGuide, type NodeAlignment, type NodeDistribution, type NodeTransform, type ResizeHandle, type SelectionBox } from "@/ui/pages/CanvasWorkspace/canvas/canvasGeometry";
|
||||
import { alignNodes, attachNodesToContainingFrames, autoLayoutAroundNode as autoLayoutAroundNodeOp, autoLayoutNodes, canResizeCanvasNode, canUseNodeContextMenu, cloneCanvasNode, distributeNodes, groupNodes, isCanvasPenStrokeNode, isFrameChildNode, isFrameContainerNode, isRealCanvasImageNode, moveNodesByDelta, nodeIdsWithFrameChildren, orderedCanvasNodesForRender, patchFrameStyleNode, removeNodesById, reorderLayer as reorderLayerOp, reorderNode as reorderNodeOp, resizeFrameWithChildren, resizeVisualNodeByHandle, toggleSetValues, ungroupNodes, updateNodeContent as updateNodeContentOp, updateNodeTransform as updateNodeTransformOp, visibleCanvasNodes as getVisibleCanvasNodes, type LayerReorderRequest } from "@/ui/pages/CanvasWorkspace/canvas/canvasNodeOps";
|
||||
import { CanvasSelectionFrame } from "@/ui/pages/CanvasWorkspace/canvas/CanvasSelectionFrame";
|
||||
import { createCroppedSvgRasterSource, createVectorCropSiblingNode } from "./canvas/cadCrop";
|
||||
import { installCanvasWheelListener } from "./canvas/canvasWheel";
|
||||
import { visibleCropSelectionForBounds } from "./canvas/cropViewport";
|
||||
import { beginFrameDrawing, FrameDraftOverlay, type FrameDraft, type FrameWorldRect } from "./canvas/FrameToolOverlay";
|
||||
import { FrameStyleToolbar } from "./canvas/FrameStyleToolbar";
|
||||
import { defaultFrameStrokeColor, defaultFrameStrokeStyle, defaultFrameStrokeWidth, normalizeHexColor } from "./canvas/frameStyle";
|
||||
@@ -578,6 +581,7 @@ export function CanvasWorkspace({ projectId, shareAccess = ownerWorkspaceAccess
|
||||
const moveRecognitionRequestRef = useRef(0);
|
||||
const quickEditFocusRef = useRef<{ viewport: CanvasViewport; dirty: boolean } | null>(null);
|
||||
const webSearchHydratedRef = useRef(false);
|
||||
const pendingWorkspaceCropRef = useRef<{ nodeId: string; selection: NormalizedRect } | null>(null);
|
||||
const { project, setProject, projectRef, viewport, setViewport, viewportRef, nodes, setNodes, nodesRef, applyProjectDocument } = useCanvasDocument();
|
||||
const { selectedId, selectedIds, setSelectedIds, selectedNodes, selectedNode, selectOnlyNode, selectNodes, toggleNodeSelection } = useCanvasSelection(nodes);
|
||||
const [messages, setMessages] = useState<AgentMessage[]>([]);
|
||||
@@ -1037,6 +1041,16 @@ export function CanvasWorkspace({ projectId, shareAccess = ownerWorkspaceAccess
|
||||
const selectedControlNode = selectedNodeControlsEnabled ? selectedNode : null;
|
||||
const selectedImageNode = selectedControlNode && isRealCanvasImageNode(selectedControlNode) ? selectedControlNode : null;
|
||||
const selectedImageNodeBusy = selectedImageNode ? isImageActionBusy(selectedImageNode) : false;
|
||||
const workspaceCropTarget = useMemo(() => {
|
||||
if (selectedImageNode) return selectedImageNode;
|
||||
const candidates = nodes.filter((node) => {
|
||||
if (!isRealCanvasImageNode(node) || hiddenNodeIds.has(node.id) || lockedNodeIds.has(node.id)) return false;
|
||||
const bounds = canvasNodeScreenBounds(node, viewport);
|
||||
return bounds.left < canvasStageSize.width && bounds.top < canvasStageSize.height && bounds.left + bounds.width > 0 && bounds.top + bounds.height > 0;
|
||||
});
|
||||
return candidates.length === 1 ? candidates[0] : null;
|
||||
}, [canvasStageSize.height, canvasStageSize.width, hiddenNodeIds, lockedNodeIds, nodes, selectedImageNode, viewport]);
|
||||
const workspaceCropTargetBusy = workspaceCropTarget ? isImageActionBusy(workspaceCropTarget) : false;
|
||||
const selectedImageGeneratorNode = selectedControlNode?.type === "image" && selectedControlNode.layerRole === "image-generator" ? selectedControlNode : null;
|
||||
const selectedImageGeneratorBusy = selectedImageGeneratorNode?.status === "generating";
|
||||
const selectedPenStrokeNode = selectedControlNode && isCanvasPenStrokeNode(selectedControlNode) ? selectedControlNode : null;
|
||||
@@ -1132,7 +1146,7 @@ export function CanvasWorkspace({ projectId, shareAccess = ownerWorkspaceAccess
|
||||
pending: true,
|
||||
replacePending: true
|
||||
});
|
||||
}, [insertCanvasImageReference, selectedImageNode?.content, selectedImageNode?.id]);
|
||||
}, [insertCanvasImageReference, selectedImageNode?.content, selectedImageNode?.id, selectedImageNode?.semanticContext]);
|
||||
|
||||
const multiSelectionBounds = useMemo<NodeScreenBounds | null>(() => {
|
||||
if (selectedNodes.length < 2) return null;
|
||||
@@ -1180,6 +1194,14 @@ export function CanvasWorkspace({ projectId, shareAccess = ownerWorkspaceAccess
|
||||
setCropPrompt("");
|
||||
}, [selectedId, selectedIds.size]);
|
||||
|
||||
useEffect(() => {
|
||||
const pending = pendingWorkspaceCropRef.current;
|
||||
if (!pending || selectedId !== pending.nodeId || selectedIds.size !== 1) return;
|
||||
pendingWorkspaceCropRef.current = null;
|
||||
setCropSelection(pending.selection);
|
||||
setImageActionMode("crop");
|
||||
}, [selectedId, selectedIds.size]);
|
||||
|
||||
useEffect(() => {
|
||||
if (imageActionMode !== "move-object" || !selectedImageNode || !moveSelection || moveSelection.srcBox.width < 0.02 || moveSelection.srcBox.height < 0.02) {
|
||||
moveRecognitionRequestRef.current += 1;
|
||||
@@ -2737,7 +2759,7 @@ export function CanvasWorkspace({ projectId, shareAccess = ownerWorkspaceAccess
|
||||
source === "image-generator"
|
||||
? imageGeneratorContents ?? []
|
||||
: activeRef.current?.toAgentContents() ?? [{ type: "text" as const, text: activePrompt, textSource: "input" }];
|
||||
const hasUserText = baseContents.some((content) => content.type === "text" && content.text.replace(/\u00a0/g, " ").trim());
|
||||
const hasUserText = baseContents.some((content) => content.type === "text" && !isHiddenTextSource(content.textSource) && content.text.replace(/\u00a0/g, " ").trim());
|
||||
const contents =
|
||||
activeMode === "image"
|
||||
? withImageGeneratorTarget(baseContents, selectedGeneratorNode?.id)
|
||||
@@ -3334,6 +3356,24 @@ export function CanvasWorkspace({ projectId, shareAccess = ownerWorkspaceAccess
|
||||
});
|
||||
};
|
||||
|
||||
const openCropForNode = (targetNode: CanvasNode) => {
|
||||
if (isImageActionBusy(targetNode)) return;
|
||||
chooseCanvasTool("select");
|
||||
const selection = visibleCropSelectionForBounds(canvasNodeScreenBounds(targetNode, viewportRef.current), canvasStageSize);
|
||||
if (selectedId !== targetNode.id || selectedIds.size !== 1) {
|
||||
pendingWorkspaceCropRef.current = { nodeId: targetNode.id, selection };
|
||||
selectOnlyNode(targetNode.id);
|
||||
return;
|
||||
}
|
||||
setCropSelection(selection);
|
||||
setImageActionMode("crop");
|
||||
};
|
||||
|
||||
const openWorkspaceCrop = () => {
|
||||
if (!workspaceCropTarget || workspaceCropTargetBusy) return;
|
||||
openCropForNode(workspaceCropTarget);
|
||||
};
|
||||
|
||||
const applyCropImage = async () => {
|
||||
const targetNode = selectedImageNode;
|
||||
const currentProject = projectRef.current ?? project;
|
||||
@@ -3345,19 +3385,32 @@ export function CanvasWorkspace({ projectId, shareAccess = ownerWorkspaceAccess
|
||||
try {
|
||||
const cropped = await cropImageNodeFile(targetNode, selection);
|
||||
const publicUrl = await designGateway.uploadImage(cropped.file);
|
||||
const nextNodes = nodesRef.current.map((node) =>
|
||||
node.id === targetNode.id
|
||||
? {
|
||||
...node,
|
||||
x: targetNode.x + targetNode.width * selection.x,
|
||||
y: targetNode.y + targetNode.height * selection.y,
|
||||
width: cropped.displayWidth,
|
||||
height: cropped.displayHeight,
|
||||
content: publicUrl,
|
||||
status: "success"
|
||||
}
|
||||
: node
|
||||
);
|
||||
const preserveSource = isVectorImageNode(targetNode);
|
||||
const croppedNode: CanvasNode | null = preserveSource
|
||||
? createVectorCropSiblingNode(targetNode, {
|
||||
id: cryptoFallbackId(),
|
||||
title: t("cropResultTitle", { title: targetNode.title || t("imageNode") }),
|
||||
publicUrl,
|
||||
cropWidth: cropped.displayWidth,
|
||||
cropHeight: cropped.displayHeight,
|
||||
semanticContext: cropped.semanticContext
|
||||
})
|
||||
: null;
|
||||
const nextNodes = croppedNode
|
||||
? [...nodesRef.current, croppedNode]
|
||||
: nodesRef.current.map((node) =>
|
||||
node.id === targetNode.id
|
||||
? {
|
||||
...node,
|
||||
x: targetNode.x + targetNode.width * selection.x,
|
||||
y: targetNode.y + targetNode.height * selection.y,
|
||||
width: cropped.displayWidth,
|
||||
height: cropped.displayHeight,
|
||||
content: publicUrl,
|
||||
status: "success"
|
||||
}
|
||||
: node
|
||||
);
|
||||
nodesRef.current = nextNodes;
|
||||
setNodes(nextNodes);
|
||||
setImageActionMode(null);
|
||||
@@ -3366,6 +3419,10 @@ export function CanvasWorkspace({ projectId, shareAccess = ownerWorkspaceAccess
|
||||
setDirty(true);
|
||||
const { project: savedProject } = await designGateway.saveCanvas(currentProject.id, viewportRef.current, nextNodes, [], currentProject);
|
||||
applyProject(savedProject);
|
||||
if (croppedNode) {
|
||||
selectOnlyNode(croppedNode.id);
|
||||
showCanvasToast(t("cropCreated"));
|
||||
}
|
||||
setDirty(false);
|
||||
} catch (err) {
|
||||
setError(toUserMessageText(err, "generateError"));
|
||||
@@ -3437,13 +3494,13 @@ export function CanvasWorkspace({ projectId, shareAccess = ownerWorkspaceAccess
|
||||
setError("");
|
||||
const preparedUploads: Array<{ file: File; previewUrl: string; node: CanvasNode }> = [];
|
||||
try {
|
||||
const imageFiles: File[] = [];
|
||||
const imageAssets: PreparedCanvasImportAsset[] = [];
|
||||
let cadImportCount = 0;
|
||||
let cadImportError: unknown = null;
|
||||
if (importFiles.some(isCadFile)) showCanvasToast(t("cadImportConverting"));
|
||||
for (const file of importFiles) {
|
||||
try {
|
||||
imageFiles.push(await prepareCanvasImportFile(file));
|
||||
imageAssets.push(await prepareCanvasImportAsset(file));
|
||||
if (isCadFile(file)) cadImportCount += 1;
|
||||
} catch (error) {
|
||||
cadImportError ??= error;
|
||||
@@ -3454,9 +3511,10 @@ export function CanvasWorkspace({ projectId, shareAccess = ownerWorkspaceAccess
|
||||
setError(message);
|
||||
showCanvasToast(message, "error");
|
||||
}
|
||||
if (imageFiles.length === 0) return;
|
||||
if (imageAssets.length === 0) return;
|
||||
|
||||
for (const [index, file] of imageFiles.entries()) {
|
||||
for (const [index, asset] of imageAssets.entries()) {
|
||||
const { file, semanticContext } = asset;
|
||||
const previewUrl = URL.createObjectURL(file);
|
||||
const dimensions = await readImageDimensions(file).catch(() => ({ width: 512, height: 512 }));
|
||||
const size = originalCanvasImageSize(dimensions.width, dimensions.height);
|
||||
@@ -3474,7 +3532,8 @@ export function CanvasWorkspace({ projectId, shareAccess = ownerWorkspaceAccess
|
||||
height: size.height,
|
||||
content: previewUrl,
|
||||
tone,
|
||||
layerRole: isSvgSource(file.name) || file.type.toLowerCase().includes("svg") ? "vector-image" : undefined,
|
||||
layerRole: semanticContext ? "cad-vector" : isSvgSource(file.name) || file.type.toLowerCase().includes("svg") ? "vector-image" : undefined,
|
||||
semanticContext,
|
||||
status: "uploading"
|
||||
}
|
||||
});
|
||||
@@ -3805,7 +3864,33 @@ export function CanvasWorkspace({ projectId, shareAccess = ownerWorkspaceAccess
|
||||
{selectedImageNode && selectedNodeBounds && (
|
||||
<>
|
||||
{isVectorImageNode(selectedImageNode) ? (
|
||||
<VectorizedImageToolbar bounds={selectedNodeBounds} leftInset={floatingToolbarLeftInset} onDownload={() => exportImageNode(selectedImageNode, "svg")} />
|
||||
<>
|
||||
{imageActionMode !== "crop" && (
|
||||
<VectorizedImageToolbar
|
||||
bounds={selectedNodeBounds}
|
||||
leftInset={floatingToolbarLeftInset}
|
||||
onCrop={() => openCropForNode(selectedImageNode)}
|
||||
onDownload={() => exportImageNode(selectedImageNode, "svg")}
|
||||
/>
|
||||
)}
|
||||
{imageActionMode === "crop" && (
|
||||
<CropOverlay
|
||||
bounds={selectedNodeBounds}
|
||||
sourceSize={{ width: selectedImageNode.width, height: selectedImageNode.height }}
|
||||
title={selectedImageNode.title}
|
||||
selection={cropSelection}
|
||||
prompt={cropPrompt}
|
||||
disabled={selectedImageNodeBusy}
|
||||
onSelectionChange={setCropSelection}
|
||||
onPromptChange={setCropPrompt}
|
||||
onCancel={() => {
|
||||
setImageActionMode(null);
|
||||
setCropSelection(null);
|
||||
}}
|
||||
onApply={applyCropImage}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{imageActionMode !== "quick-edit" && imageActionMode !== "visual-annotation" && imageActionMode !== "eraser" && imageActionMode !== "edit-text" && imageActionMode !== "expand" && imageActionMode !== "crop" && imageActionMode !== "flip-rotate" && selectedImageNode.status !== "text-extracting" && mockupPendingTargetId !== selectedImageNode.id && (
|
||||
@@ -3843,10 +3928,7 @@ export function CanvasWorkspace({ projectId, shareAccess = ownerWorkspaceAccess
|
||||
onMockup={prepareMockupPlacement}
|
||||
onExpand={openExpandPanel}
|
||||
onAdjust={openAdjustPanel}
|
||||
onCrop={() => {
|
||||
setCropSelection(defaultCropSelection);
|
||||
setImageActionMode("crop");
|
||||
}}
|
||||
onCrop={() => openCropForNode(selectedImageNode)}
|
||||
onVectorize={() => void runImageNodeAction("vectorize", { prompt: t("vectorPrompt") })}
|
||||
onFlipRotate={() => setImageActionMode("flip-rotate")}
|
||||
onDownload={() => exportImageNode(selectedImageNode, "png")}
|
||||
@@ -4119,12 +4201,15 @@ export function CanvasWorkspace({ projectId, shareAccess = ownerWorkspaceAccess
|
||||
shapeToolMenuOpen={shapeToolMenuOpen}
|
||||
selectedShapeKind={selectedShapeKind}
|
||||
showGrid={showGrid}
|
||||
cropEnabled={Boolean(workspaceCropTarget && !workspaceCropTargetBusy)}
|
||||
cropActive={imageActionMode === "crop"}
|
||||
disabled={!canEdit}
|
||||
offsetBy={generatedFilesPanelOpen ? "files" : layersPanelOpen ? "layers" : null}
|
||||
onCanvasToolMenuOpenChange={setCanvasToolMenuOpen}
|
||||
onShapeToolMenuOpenChange={setShapeToolMenuOpen}
|
||||
onChooseTool={chooseCanvasTool}
|
||||
onChooseShapeKind={setSelectedShapeKind}
|
||||
onCrop={openWorkspaceCrop}
|
||||
onToggleGrid={() => setShowGrid((value) => !value)}
|
||||
onUploadAsset={() => canvasAssetInputRef.current?.click()}
|
||||
/>
|
||||
@@ -4794,6 +4879,15 @@ function mockupPlacementWorldRect(target: CanvasNode, placement: MockupPlacement
|
||||
};
|
||||
}
|
||||
|
||||
function canvasNodeScreenBounds(node: CanvasNode, viewport: CanvasViewport): NodeScreenBounds {
|
||||
return {
|
||||
left: viewport.x + node.x * viewport.k,
|
||||
top: viewport.y + node.y * viewport.k,
|
||||
width: node.width * viewport.k,
|
||||
height: node.height * viewport.k
|
||||
};
|
||||
}
|
||||
|
||||
function mockupPlacementScreenBounds(target: CanvasNode, placement: MockupPlacement, viewport: CanvasViewport): NodeScreenBounds {
|
||||
const rect = mockupPlacementWorldRect(target, placement);
|
||||
return {
|
||||
@@ -5381,22 +5475,28 @@ function buildTextEditPrompt(items: EditableTextLayer[], fallback: string) {
|
||||
async function cropImageNodeFile(node: CanvasNode, selection: NormalizedRect) {
|
||||
const rect = clampCropSelection(selection);
|
||||
const blob = await imageBlobForCanvas(node.content);
|
||||
const imageUrl = URL.createObjectURL(blob);
|
||||
const vectorSource = isVectorImageNode(node);
|
||||
const svgText = vectorSource ? await blob.text() : "";
|
||||
const semanticContext = node.layerRole === "cad-vector" ? cadSemanticContextFromSvgText(svgText, rect) : node.semanticContext || "";
|
||||
const vectorCrop = vectorSource ? createCroppedSvgRasterSource(svgText, rect) : null;
|
||||
const imageUrl = URL.createObjectURL(vectorCrop?.source ?? blob);
|
||||
try {
|
||||
const image = await loadCanvasImage(imageUrl);
|
||||
const naturalWidth = image.naturalWidth || image.width;
|
||||
const naturalHeight = image.naturalHeight || image.height;
|
||||
const sourceX = Math.round(rect.x * naturalWidth);
|
||||
const sourceY = Math.round(rect.y * naturalHeight);
|
||||
const sourceWidth = Math.max(1, Math.round(rect.width * naturalWidth));
|
||||
const sourceHeight = Math.max(1, Math.round(rect.height * naturalHeight));
|
||||
const sourceX = vectorCrop ? 0 : Math.round(rect.x * naturalWidth);
|
||||
const sourceY = vectorCrop ? 0 : Math.round(rect.y * naturalHeight);
|
||||
const sourceWidth = vectorCrop ? naturalWidth : Math.max(1, Math.round(rect.width * naturalWidth));
|
||||
const sourceHeight = vectorCrop ? naturalHeight : Math.max(1, Math.round(rect.height * naturalHeight));
|
||||
const outputSize = vectorCrop?.outputSize ?? { width: sourceWidth, height: sourceHeight };
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = sourceWidth;
|
||||
canvas.height = sourceHeight;
|
||||
canvas.width = outputSize.width;
|
||||
canvas.height = outputSize.height;
|
||||
const context = canvas.getContext("2d");
|
||||
if (!context) throw new Error("Failed to create crop canvas");
|
||||
context.clearRect(0, 0, sourceWidth, sourceHeight);
|
||||
context.drawImage(image, sourceX, sourceY, sourceWidth, sourceHeight, 0, 0, sourceWidth, sourceHeight);
|
||||
context.clearRect(0, 0, outputSize.width, outputSize.height);
|
||||
if (vectorCrop) context.drawImage(image, 0, 0, outputSize.width, outputSize.height);
|
||||
else context.drawImage(image, sourceX, sourceY, sourceWidth, sourceHeight, 0, 0, outputSize.width, outputSize.height);
|
||||
const output = await canvasToImageBlob(canvas, "image/webp", 0.94);
|
||||
if (!output) throw new Error("Failed to encode cropped image");
|
||||
return {
|
||||
@@ -5405,7 +5505,8 @@ async function cropImageNodeFile(node: CanvasNode, selection: NormalizedRect) {
|
||||
lastModified: Date.now()
|
||||
}),
|
||||
displayWidth: Math.max(1, node.width * rect.width),
|
||||
displayHeight: Math.max(1, node.height * rect.height)
|
||||
displayHeight: Math.max(1, node.height * rect.height),
|
||||
semanticContext
|
||||
};
|
||||
} finally {
|
||||
URL.revokeObjectURL(imageUrl);
|
||||
@@ -5882,10 +5983,12 @@ function createOptimisticAgentMessages(threadId: string, contents: AgentContent[
|
||||
}
|
||||
|
||||
function agentContentsToDisplayText(contents: AgentContent[]) {
|
||||
let imageIndex = 0;
|
||||
return contents
|
||||
.map((content, index) => {
|
||||
if (content.type === "text") return content.text;
|
||||
return `参考图 ${index + 1}(${agentContentImageName(content)}):${content.imageUrl}`;
|
||||
.map((content) => {
|
||||
if (content.type === "text") return isHiddenTextSource(content.textSource) ? "" : content.text;
|
||||
imageIndex += 1;
|
||||
return `参考图 ${imageIndex}(${agentContentImageName(content)}):${content.imageUrl}`;
|
||||
})
|
||||
.filter((item) => item.trim())
|
||||
.join("\n\n");
|
||||
@@ -5896,11 +5999,15 @@ function agentContentImageName(content: Extract<AgentContent, { type: "image" }>
|
||||
}
|
||||
|
||||
function imageGeneratorSubmittedPrompt(contents: AgentContent[]) {
|
||||
let imageIndex = 0;
|
||||
return cleanImageGeneratorPrompt(
|
||||
contents
|
||||
.map((content, index) => {
|
||||
if (content.type === "text" && !isSettingsTextSource(content.textSource)) return content.text;
|
||||
if (content.type === "image") return `参考图 ${index + 1}(${agentContentImageName(content)}):${content.imageUrl}`;
|
||||
.map((content) => {
|
||||
if (content.type === "text" && !isHiddenTextSource(content.textSource)) return content.text;
|
||||
if (content.type === "image") {
|
||||
imageIndex += 1;
|
||||
return `参考图 ${imageIndex}(${agentContentImageName(content)}):${content.imageUrl}`;
|
||||
}
|
||||
return "";
|
||||
})
|
||||
.filter((item) => item.trim())
|
||||
@@ -5918,8 +6025,9 @@ function cleanImageGeneratorPrompt(value: string) {
|
||||
.trim();
|
||||
}
|
||||
|
||||
function isSettingsTextSource(value?: string) {
|
||||
return value?.trim().toLowerCase() === "settings";
|
||||
function isHiddenTextSource(value?: string) {
|
||||
const source = value?.trim().toLowerCase();
|
||||
return source === "settings" || source === "cad-context";
|
||||
}
|
||||
|
||||
function isBlankTextContent(content: string) {
|
||||
|
||||
Reference in New Issue
Block a user