feat(api): add Lovart-style project and agent thread endpoints
Add canva/canda-compatible routes for lightweight project open/save and agent thread history: - POST /canva/project/queryProject, saveProject - POST /canva/agent/queryAgentLastThread, agentThreadList - GET /canda/chat-history, /canda/thread/status Canvas save now accepts a compressed SHAKKERDATA:// snapshot payload, preserves connections, and returns a SaveProject response; blank successful image nodes are backfilled from generated artifacts. Frontend designGateway adopts the new save/query flow. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -757,7 +757,7 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) {
|
||||
}
|
||||
}, [projectRef]);
|
||||
|
||||
const { saveCanvasSnapshot, updateProjectTitle } = useCanvasPersistence({
|
||||
const { saveCanvasSnapshot, scheduleCanvasSave, updateProjectTitle } = useCanvasPersistence({
|
||||
project,
|
||||
projectRef,
|
||||
viewport,
|
||||
@@ -859,7 +859,14 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) {
|
||||
if (history && !agentStreamingProjectIdRef.current) {
|
||||
const currentProject = projectRef.current;
|
||||
if (currentProject) {
|
||||
setMessages(visibleAgentMessagesForThread({ ...currentProject, messages: history.messages }, imageGeneratorThreadIdsRef.current, selectedAgentThreadIdRef.current));
|
||||
const projectWithHistory = {
|
||||
...currentProject,
|
||||
lastThreadId: history.threadId || currentProject.lastThreadId,
|
||||
messages: history.messages
|
||||
};
|
||||
projectRef.current = projectWithHistory;
|
||||
setProject(projectWithHistory);
|
||||
setMessages(visibleAgentMessagesForThread(projectWithHistory, imageGeneratorThreadIdsRef.current, selectedAgentThreadIdRef.current));
|
||||
} else {
|
||||
setMessages(visibleAgentMessages(history.messages, imageGeneratorThreadIdsRef.current));
|
||||
}
|
||||
@@ -1200,7 +1207,7 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) {
|
||||
window.removeEventListener("pointerup", handleUp);
|
||||
window.removeEventListener("pointercancel", handleUp);
|
||||
setCanvasPanning(false);
|
||||
void saveCanvasSnapshot({ quiet: true });
|
||||
scheduleCanvasSave();
|
||||
};
|
||||
|
||||
window.addEventListener("pointermove", handleMove);
|
||||
@@ -1627,7 +1634,7 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) {
|
||||
void refreshMockupPrintTextures(Array.from(dragIds));
|
||||
return;
|
||||
}
|
||||
void saveCanvasSnapshot({ quiet: true });
|
||||
scheduleCanvasSave();
|
||||
};
|
||||
|
||||
window.addEventListener("pointermove", handleMove);
|
||||
@@ -1686,7 +1693,7 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) {
|
||||
void refreshMockupPrintTextures([node.id]);
|
||||
return;
|
||||
}
|
||||
void saveCanvasSnapshot({ quiet: true });
|
||||
scheduleCanvasSave();
|
||||
};
|
||||
|
||||
window.addEventListener("pointermove", handleMove);
|
||||
@@ -1724,7 +1731,7 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) {
|
||||
window.removeEventListener("pointermove", handleMove);
|
||||
window.removeEventListener("pointerup", handleUp);
|
||||
setAlignmentGuides([]);
|
||||
void saveCanvasSnapshot({ quiet: true });
|
||||
scheduleCanvasSave();
|
||||
};
|
||||
|
||||
window.addEventListener("pointermove", handleMove);
|
||||
@@ -1751,7 +1758,7 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) {
|
||||
window.removeEventListener("pointermove", handleMove);
|
||||
window.removeEventListener("pointerup", handleUp);
|
||||
setAlignmentGuides([]);
|
||||
void saveCanvasSnapshot({ quiet: true });
|
||||
scheduleCanvasSave();
|
||||
};
|
||||
|
||||
window.addEventListener("pointermove", handleMove);
|
||||
@@ -1804,7 +1811,7 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) {
|
||||
window.removeEventListener("pointermove", handleMove);
|
||||
window.removeEventListener("pointerup", handleUp);
|
||||
setAlignmentGuides([]);
|
||||
void saveCanvasSnapshot({ quiet: true });
|
||||
scheduleCanvasSave();
|
||||
};
|
||||
|
||||
window.addEventListener("pointermove", handleMove);
|
||||
@@ -2517,6 +2524,22 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) {
|
||||
const currentProject = projectRef.current;
|
||||
if (currentProject) {
|
||||
setMessages(visibleAgentMessagesForThread(currentProject, imageGeneratorThreadIdsRef.current, threadId));
|
||||
void designGateway
|
||||
.getProjectHistory(currentProject.id, threadId)
|
||||
.then((history) => {
|
||||
if (selectedAgentThreadIdRef.current !== threadId) return;
|
||||
const nextProject = {
|
||||
...currentProject,
|
||||
lastThreadId: history.threadId || currentProject.lastThreadId,
|
||||
messages: history.messages
|
||||
};
|
||||
projectRef.current = nextProject;
|
||||
setProject(nextProject);
|
||||
setMessages(visibleAgentMessagesForThread(nextProject, imageGeneratorThreadIdsRef.current, threadId));
|
||||
})
|
||||
.catch((err) => {
|
||||
console.warn("Failed to load agent thread history", err);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2836,7 +2859,7 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) {
|
||||
setMockupSurface(null);
|
||||
setMockupModel(null);
|
||||
setDirty(true);
|
||||
void saveCanvasSnapshot({ quiet: true });
|
||||
scheduleCanvasSave();
|
||||
};
|
||||
|
||||
const refreshMockupPrintTextures = async (nodeIds: string[]) => {
|
||||
@@ -2860,13 +2883,13 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) {
|
||||
}
|
||||
}
|
||||
if (!changed) {
|
||||
void saveCanvasSnapshot({ quiet: true });
|
||||
scheduleCanvasSave();
|
||||
return;
|
||||
}
|
||||
nodesRef.current = nextNodes;
|
||||
setNodes(nextNodes);
|
||||
setDirty(true);
|
||||
void saveCanvasSnapshot({ quiet: true });
|
||||
scheduleCanvasSave();
|
||||
};
|
||||
|
||||
const restoreQuickEditViewport = useCallback(() => {
|
||||
@@ -3131,11 +3154,7 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) {
|
||||
setCropSelection(null);
|
||||
setCropPrompt("");
|
||||
setDirty(true);
|
||||
const { project: savedProject } = await designGateway.saveCanvas(currentProject.id, viewportRef.current, nextNodes, [], {
|
||||
version: currentProject.version,
|
||||
snapshotId: currentProject.snapshotId,
|
||||
canvas: currentProject.canvas
|
||||
});
|
||||
const { project: savedProject } = await designGateway.saveCanvas(currentProject.id, viewportRef.current, nextNodes, [], currentProject);
|
||||
applyProject(savedProject);
|
||||
setDirty(false);
|
||||
} catch (err) {
|
||||
@@ -3211,7 +3230,7 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) {
|
||||
for (const [index, file] of imageFiles.entries()) {
|
||||
const previewUrl = URL.createObjectURL(file);
|
||||
const dimensions = await readImageDimensions(file).catch(() => ({ width: 512, height: 512 }));
|
||||
const size = fitCanvasImageSize(dimensions.width, dimensions.height);
|
||||
const size = originalCanvasImageSize(dimensions.width, dimensions.height);
|
||||
const tone = imageMaySupportTransparency(file) ? "transparent-image" : "natural";
|
||||
preparedUploads.push({
|
||||
file,
|
||||
@@ -3269,11 +3288,7 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) {
|
||||
setError(t("generateError"));
|
||||
}
|
||||
|
||||
const { project: savedProject } = await designGateway.saveCanvas(currentProject.id, viewportRef.current, finalNodes, [], {
|
||||
version: currentProject.version,
|
||||
snapshotId: currentProject.snapshotId,
|
||||
canvas: ""
|
||||
});
|
||||
const { project: savedProject } = await designGateway.saveCanvas(currentProject.id, viewportRef.current, finalNodes, [], currentProject);
|
||||
applyProject(savedProject);
|
||||
setDirty(false);
|
||||
if (failedIds.size === 0 && finalUploadedNodeIds.size > 0) showCanvasToast(t("imageUploadSuccess"));
|
||||
@@ -3754,7 +3769,7 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) {
|
||||
setViewport(nextViewport);
|
||||
setDirty(true);
|
||||
}}
|
||||
onViewportCommit={() => void saveCanvasSnapshot({ quiet: true })}
|
||||
onViewportCommit={scheduleCanvasSave}
|
||||
/>
|
||||
)}
|
||||
<div className="canvas-statusbar">
|
||||
@@ -5305,19 +5320,13 @@ function readImageDimensions(file: File) {
|
||||
});
|
||||
}
|
||||
|
||||
function fitCanvasImageSize(width: number, height: number) {
|
||||
function originalCanvasImageSize(width: number, height: number) {
|
||||
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
|
||||
return { width: 420, height: 420 };
|
||||
}
|
||||
const maxSide = 520;
|
||||
const minSide = 120;
|
||||
let scale = Math.min(1, maxSide / Math.max(width, height));
|
||||
if (Math.max(width, height) * scale < minSide) {
|
||||
scale = minSide / Math.max(width, height);
|
||||
return { width: 512, height: 512 };
|
||||
}
|
||||
return {
|
||||
width: Math.round(width * scale),
|
||||
height: Math.round(height * scale)
|
||||
width: Math.round(width),
|
||||
height: Math.round(height)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user