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:
2026-07-07 23:15:37 +08:00
commit 44406b72db
349 changed files with 73265 additions and 0 deletions
@@ -0,0 +1,62 @@
.canvas-zoom-trigger {
min-width: 46px;
height: 24px;
padding: 0 8px;
border-radius: 6px;
color: #687080;
font-size: 13px;
font-weight: 720;
line-height: 24px;
text-align: center;
}
.canvas-zoom-trigger:hover,
.canvas-zoom-trigger[data-state="open"] {
background: #f2f4f7;
}
.canvas-zoom-popover {
width: 264px;
padding: 16px;
border: 1px solid rgba(226, 228, 234, 0.9);
border-radius: 17px;
background: rgba(255, 255, 255, 0.98);
box-shadow: 0 14px 34px rgba(17, 24, 39, 0.13);
}
.canvas-zoom-menu {
display: grid;
gap: 4px;
}
.canvas-zoom-menu button {
width: 100%;
height: 38px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 3px;
border-radius: 8px;
color: #20242c;
background: transparent;
font-size: 17px;
font-weight: 520;
line-height: 1;
}
.canvas-zoom-menu button:hover {
background: #f5f6f8;
}
.canvas-zoom-menu kbd {
color: #afb3ba;
font: inherit;
font-size: 17px;
font-weight: 620;
}
.canvas-zoom-menu i {
height: 1px;
margin: 7px 0 8px;
background: #eceef2;
}
@@ -0,0 +1,69 @@
"use client";
import { useMemo } from "react";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { useI18n } from "@/i18n/i18n";
import "./index.css";
type ZoomAction = {
label: string;
shortcut?: string;
onSelect: () => void;
};
export function CanvasZoomMenu({
zoom,
onZoomIn,
onZoomOut,
onFitAll,
onZoomTo
}: {
zoom: number;
onZoomIn: () => void;
onZoomOut: () => void;
onFitAll: () => void;
onZoomTo: (zoom: number) => void;
}) {
const { t } = useI18n();
const isMac = useMemo(isApplePlatform, []);
const primary = isMac ? "⌘" : "Ctrl";
const actions: ZoomAction[] = [
{ label: t("zoomIn"), shortcut: `${primary} +`, onSelect: onZoomIn },
{ label: t("zoomOut"), shortcut: `${primary} -`, onSelect: onZoomOut },
{ label: t("zoomShowAll"), shortcut: isMac ? "⇧ 1" : "Shift 1", onSelect: onFitAll }
];
const presets = [0.5, 1, 2];
return (
<Popover>
<PopoverTrigger asChild>
<button type="button" className="canvas-zoom-trigger" aria-label={t("zoomMenu")} title={t("zoomMenu")}>
{Math.round(zoom * 100)}%
</button>
</PopoverTrigger>
<PopoverContent className="canvas-zoom-popover" side="top" align="start" sideOffset={12} onPointerDown={(event) => event.stopPropagation()} onWheel={(event) => event.stopPropagation()}>
<div className="canvas-zoom-menu">
{actions.map((action) => (
<button key={action.label} type="button" onClick={action.onSelect}>
<span>{action.label}</span>
{action.shortcut && <kbd>{action.shortcut}</kbd>}
</button>
))}
<i />
{presets.map((preset) => (
<button key={preset} type="button" onClick={() => onZoomTo(preset)}>
<span>{t("zoomTo", { percent: Math.round(preset * 100) })}</span>
</button>
))}
</div>
</PopoverContent>
</Popover>
);
}
function isApplePlatform() {
if (typeof navigator === "undefined") return false;
const nav = navigator as Navigator & { userAgentData?: { platform?: string } };
const platform = nav.userAgentData?.platform || navigator.platform || "";
return /mac|iphone|ipad|ipod/i.test(platform);
}