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,87 @@
.canvas-toast {
position: fixed;
left: 50%;
top: 48px;
z-index: 120;
width: min(1040px, calc(100vw - 140px));
min-height: 126px;
display: flex;
align-items: center;
gap: 28px;
padding: 0 44px;
border: 1px solid rgba(224, 229, 238, 0.96);
border-radius: 24px;
color: #1f2329;
background: rgba(255, 255, 255, 0.98);
box-shadow: 0 38px 90px rgba(17, 24, 39, 0.16);
pointer-events: none;
transform: translateX(-50%);
animation: canvas-toast-enter 180ms cubic-bezier(0.2, 0, 0.2, 1);
}
.canvas-toast-icon {
width: 44px;
height: 44px;
display: grid;
place-items: center;
flex: 0 0 auto;
border-radius: 999px;
color: #ffffff;
}
.canvas-toast.success .canvas-toast-icon {
background: #45d27c;
}
.canvas-toast.error .canvas-toast-icon {
background: #e5484d;
}
.canvas-toast strong {
min-width: 0;
overflow: hidden;
font-size: 32px;
font-weight: 850;
line-height: 1.18;
text-overflow: ellipsis;
white-space: normal;
word-break: break-word;
}
@keyframes canvas-toast-enter {
from {
opacity: 0;
transform: translate(-50%, -8px);
}
to {
opacity: 1;
transform: translate(-50%, 0);
}
}
@media (max-width: 760px) {
.canvas-toast {
top: 18px;
width: calc(100vw - 32px);
min-height: 76px;
gap: 14px;
padding: 16px 18px;
border-radius: 18px;
}
.canvas-toast-icon {
width: 32px;
height: 32px;
}
.canvas-toast-icon svg {
width: 20px;
height: 20px;
}
.canvas-toast strong {
font-size: 18px;
line-height: 1.28;
}
}
@@ -0,0 +1,93 @@
"use client";
import { Check, X } from "lucide-react";
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
import "./index.css";
type ToastTone = "success" | "error";
type GlobalToastState = {
message: string;
tone: ToastTone;
};
type GlobalToastContextValue = {
showToast: (message: string, tone?: ToastTone) => void;
showSuccess: (message: string) => void;
showError: (message: string) => void;
hideToast: () => void;
};
const GlobalToastContext = createContext<GlobalToastContextValue | null>(null);
export function GlobalToastProvider({ children }: { children: ReactNode }) {
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const [toast, setToast] = useState<GlobalToastState | null>(null);
const hideToast = useCallback(() => {
if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = null;
}
setToast(null);
}, []);
const showToast = useCallback((message: string, tone: ToastTone = "success") => {
if (timerRef.current) clearTimeout(timerRef.current);
setToast({ message, tone });
timerRef.current = setTimeout(() => {
setToast(null);
timerRef.current = null;
}, tone === "error" ? 3800 : 2600);
}, []);
useEffect(
() => () => {
if (timerRef.current) clearTimeout(timerRef.current);
},
[]
);
const value = useMemo(
() => ({
showToast,
showSuccess: (message: string) => showToast(message, "success"),
showError: (message: string) => showToast(message, "error"),
hideToast
}),
[hideToast, showToast]
);
return (
<GlobalToastContext.Provider value={value}>
{children}
<CanvasToast message={toast?.message ?? ""} tone={toast?.tone ?? "success"} visible={Boolean(toast)} />
</GlobalToastContext.Provider>
);
}
export function useGlobalToast() {
const context = useContext(GlobalToastContext);
if (!context) {
throw new Error("useGlobalToast must be used inside GlobalToastProvider");
}
return context;
}
export function CanvasToast({ message, visible, tone = "success" }: { message: string; visible: boolean; tone?: ToastTone }) {
if (!visible) return null;
const Icon = tone === "error" ? X : Check;
return (
<div
className={`canvas-toast ${tone === "error" ? "error" : "success"}`}
role={tone === "error" ? "alert" : "status"}
aria-live={tone === "error" ? "assertive" : "polite"}
>
<span className="canvas-toast-icon" aria-hidden="true">
<Icon size={26} />
</span>
<strong>{message}</strong>
</div>
);
}