Initial commit: img-infinite-canvas AI design workbench MVP
Moteva-style AI design workbench replica. Home prompt creates a project; the project page provides an infinite canvas with node drag, zoom/pan, a design chat panel, history replay, image asset upload, and project save/regenerate. - Backend: go-zero, DDD layering, sqlc/pgx, optional PostgreSQL; memory or Redis cache; asynq job queue; MinIO/S3/R2/OSS object storage; sky-valley/pi agent runtime adapter. - Frontend: Next.js App Router + Vite artifact build, TypeScript, i18n, shadcn/ui components, auth (OTP/Turnstile/Google/WeChat). - Deploy: Docker Compose and k3s manifests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
import type { CanvasNode } from "@/domain/design";
|
||||
import type { UploadedReferenceImage } from "@/ui/components/PromptComposer";
|
||||
|
||||
const canvasNodeReferencePrefix = "canvas-node:";
|
||||
|
||||
export function canvasNodeToReferenceImage(node: CanvasNode): UploadedReferenceImage | null {
|
||||
const content = node.content.trim();
|
||||
if (node.type !== "image" || node.layerRole === "pen-stroke" || node.layerRole === "image-generator" || node.status === "generating" || node.status === "error" || !isImageReferenceUrl(content)) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
id: `${canvasNodeReferencePrefix}${node.id}`,
|
||||
name: node.title.trim() || "Image",
|
||||
publicUrl: content,
|
||||
previewUrl: content
|
||||
};
|
||||
}
|
||||
|
||||
export function isCanvasNodeReferenceImage(reference: UploadedReferenceImage) {
|
||||
return reference.id.startsWith(canvasNodeReferencePrefix);
|
||||
}
|
||||
|
||||
export function isAssetReferencedByCanvas(publicUrl: string, nodes: CanvasNode[]) {
|
||||
const targetKeys = assetReferenceKeys(publicUrl);
|
||||
if (targetKeys.size === 0) return false;
|
||||
return nodes.some((node) => {
|
||||
const contentKeys = assetReferenceKeys(node.content);
|
||||
return Array.from(contentKeys).some((key) => targetKeys.has(key));
|
||||
});
|
||||
}
|
||||
|
||||
function isImageReferenceUrl(value: string) {
|
||||
return /^(https?:\/\/|data:image\/|blob:|\/)/.test(value);
|
||||
}
|
||||
|
||||
function assetReferenceKeys(value: string) {
|
||||
const trimmed = value.trim();
|
||||
const keys = new Set<string>();
|
||||
if (!trimmed || trimmed.startsWith("data:image/") || trimmed.startsWith("blob:")) return keys;
|
||||
keys.add(trimmed);
|
||||
const queryIndex = trimmed.indexOf("?");
|
||||
if (queryIndex > 0) keys.add(trimmed.slice(0, queryIndex));
|
||||
try {
|
||||
const url = new URL(trimmed, typeof window === "undefined" ? "http://localhost" : window.location.origin);
|
||||
keys.add(url.pathname);
|
||||
keys.add(`${url.pathname}${url.search}`);
|
||||
} catch {
|
||||
// Keep exact/path keys above for non-URL storage identifiers.
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
Reference in New Issue
Block a user