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,84 @@
.workspace-title {
min-width: 0;
display: flex;
align-items: center;
gap: 6px;
}
.moteva-mark {
width: 1.8rem;
height: 1.8rem;
display: grid;
place-items: center;
border-radius: 999px;
color: #fff;
background: #000;
}
.workspace-brand-mark {
width: 0.79rem;
height: 1.1rem;
display: block;
}
.workspace-title-name {
min-width: 0;
max-width: 170px;
height: 26px;
display: flex;
align-items: center;
border-radius: 6px;
color: #1f2329;
background: transparent;
}
.workspace-title-name:hover {
background: #f2f3f5;
}
.workspace-title-name strong {
overflow: hidden;
max-width: 160px;
font-size: 12px;
font-weight: 850;
text-overflow: ellipsis;
white-space: nowrap;
}
.workspace-title-edit {
width: min(220px, 28vw);
height: 26px;
display: flex;
align-items: center;
gap: 4px;
padding: 0;
background: transparent;
}
.workspace-title-edit input {
min-width: 0;
width: 100%;
height: 100%;
border: 0;
outline: 0;
background: transparent;
color: #1f2329;
font: inherit;
font-size: 12px;
font-weight: 850;
line-height: 26px;
}
.title-ghost {
width: 22px;
height: 22px;
display: grid;
place-items: center;
border-radius: 6px;
color: #737780;
background: transparent;
}
.title-ghost:hover {
background: #f2f3f5;
}
@@ -0,0 +1,119 @@
import { ChevronDown, Copy, Loader2 } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { BrandMark } from "@/ui/components/BrandMark";
type WorkspaceTitleProps = {
title: string;
untitledLabel: string;
renameLabel: string;
placeholder: string;
backHomeLabel: string;
copyLabel: string;
moreLabel: string;
onBackHome: () => void;
onRename: (title: string) => Promise<void>;
};
export function WorkspaceTitle({
title,
untitledLabel,
renameLabel,
placeholder,
backHomeLabel,
copyLabel,
moreLabel,
onBackHome,
onRename
}: WorkspaceTitleProps) {
const inputRef = useRef<HTMLInputElement | null>(null);
const committingRef = useRef(false);
const [editing, setEditing] = useState(false);
const [draftTitle, setDraftTitle] = useState(title);
const [renaming, setRenaming] = useState(false);
const trimmedTitle = title.trim();
const displayTitle = trimmedTitle || untitledLabel;
useEffect(() => {
if (!editing) setDraftTitle(title);
}, [editing, title]);
useEffect(() => {
if (!editing) return;
const input = inputRef.current;
if (!input) return;
input.focus();
input.select();
}, [editing]);
const startEditing = () => {
if (renaming) return;
setDraftTitle(trimmedTitle);
setEditing(true);
};
const cancelEditing = () => {
setDraftTitle(title);
setEditing(false);
};
const commitEditing = async () => {
if (committingRef.current) return;
const nextTitle = draftTitle.trim();
if (!nextTitle || nextTitle === trimmedTitle) {
cancelEditing();
return;
}
committingRef.current = true;
setRenaming(true);
try {
await onRename(nextTitle);
setEditing(false);
} catch {
inputRef.current?.focus();
} finally {
committingRef.current = false;
setRenaming(false);
}
};
return (
<div className="workspace-title">
<button className="moteva-mark" type="button" onClick={onBackHome} aria-label={backHomeLabel}>
<BrandMark className="workspace-brand-mark" />
</button>
{editing ? (
<span className="workspace-title-edit">
<input
ref={inputRef}
value={draftTitle}
placeholder={placeholder}
readOnly={renaming}
onChange={(event) => setDraftTitle(event.target.value)}
onBlur={() => void commitEditing()}
onKeyDown={(event) => {
if (event.key === "Enter") {
event.preventDefault();
void commitEditing();
}
if (event.key === "Escape") {
event.preventDefault();
cancelEditing();
}
}}
/>
{renaming && <Loader2 className="spin" size={13} />}
</span>
) : (
<button className="workspace-title-name" type="button" title={displayTitle} aria-label={renameLabel} onClick={startEditing}>
<strong>{displayTitle}</strong>
</button>
)}
<button className="title-ghost" type="button" aria-label={copyLabel}>
<Copy size={14} />
</button>
<button className="title-ghost" type="button" aria-label={moreLabel}>
<ChevronDown size={14} />
</button>
</div>
);
}