diff --git a/.memory/worklog.json b/.memory/worklog.json
index 75186e2..d1a6c3f 100644
--- a/.memory/worklog.json
+++ b/.memory/worklog.json
@@ -1524,6 +1524,27 @@
"message": "auto-save 2026-05-20 12:49 (~3)",
"hash": "a190800",
"files_changed": 3
+ },
+ {
+ "ts": "2026-05-20T14:07:08+08:00",
+ "type": "commit",
+ "message": "auto-save 2026-05-20 14:07 (-1)",
+ "hash": "09664a2",
+ "files_changed": 1
+ },
+ {
+ "ts": "2026-05-20T14:12:33+08:00",
+ "type": "commit",
+ "message": "auto-save 2026-05-20 14:12 (+1, ~3)",
+ "hash": "6bd8873",
+ "files_changed": 4
+ },
+ {
+ "ts": "2026-05-20T14:45:05+08:00",
+ "type": "commit",
+ "message": "auto-save 2026-05-20 14:45 (~4)",
+ "hash": "527ccfa",
+ "files_changed": 4
}
]
}
diff --git a/src/app/globals.css b/src/app/globals.css
index 54cd7ec..015d50e 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -342,6 +342,207 @@ input, textarea {
background: linear-gradient(90deg, rgba(0,0,0,0.16), rgba(42,42,42,0.08));
}
+.project-shell {
+ background:
+ radial-gradient(circle at 22% 8%, rgba(230, 245, 120, 0.12), transparent 34%),
+ radial-gradient(circle at 86% 18%, rgba(73, 199, 182, 0.09), transparent 30%),
+ linear-gradient(135deg, rgba(255,255,255,0.025), rgba(255,255,255,0));
+}
+
+.project-dock {
+ border-right: 1px solid rgba(255, 255, 255, 0.10);
+ background:
+ linear-gradient(180deg, rgba(36, 36, 36, 0.76), rgba(12, 12, 12, 0.64)),
+ rgba(24, 24, 24, 0.60);
+ box-shadow:
+ inset -1px 0 0 rgba(255,255,255,0.04),
+ 18px 0 70px -48px rgba(0,0,0,0.96);
+ backdrop-filter: blur(20px);
+}
+
+.project-dock--closed {
+ display: flex;
+ justify-content: center;
+}
+
+.project-dock-list,
+.project-brief-panel,
+.project-production-panel {
+ scrollbar-width: thin;
+ scrollbar-color: rgba(255, 255, 255, 0.18) transparent;
+}
+
+.project-dock-list::-webkit-scrollbar,
+.project-brief-panel::-webkit-scrollbar,
+.project-production-panel::-webkit-scrollbar {
+ width: 8px;
+}
+
+.project-dock-list::-webkit-scrollbar-thumb,
+.project-brief-panel::-webkit-scrollbar-thumb,
+.project-production-panel::-webkit-scrollbar-thumb {
+ border-radius: 999px;
+ background: rgba(255, 255, 255, 0.16);
+}
+
+.project-stage {
+ overflow: hidden;
+}
+
+.project-stage-inner {
+ display: flex;
+ height: 100%;
+ min-height: 0;
+ flex-direction: column;
+ gap: 14px;
+ padding: 18px 22px 20px;
+}
+
+.project-topbar {
+ min-height: 58px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 16px;
+ border: 1px solid rgba(255,255,255,0.09);
+ border-radius: 8px;
+ background:
+ linear-gradient(135deg, rgba(255,255,255,0.065), rgba(255,255,255,0.026)),
+ rgba(24,24,24,0.46);
+ box-shadow: inset 0 1px 0 rgba(255,255,255,0.07);
+ backdrop-filter: blur(18px);
+ padding: 10px 14px;
+}
+
+.project-empty-canvas {
+ min-height: 0;
+ overflow: auto;
+}
+
+.project-board {
+ display: flex;
+ min-height: 0;
+ flex: 1;
+ flex-direction: column;
+ gap: 14px;
+}
+
+.project-board-head {
+ min-height: 102px;
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) minmax(300px, 560px);
+ gap: 18px;
+ align-items: stretch;
+ border: 1px solid rgba(255,255,255,0.10);
+ border-radius: 8px;
+ background:
+ linear-gradient(135deg, rgba(255,255,255,0.07), rgba(255,255,255,0.026)),
+ rgba(28,28,28,0.50);
+ box-shadow:
+ inset 0 1px 0 rgba(255,255,255,0.08),
+ 0 24px 90px -54px rgba(0, 0, 0, 0.92);
+ backdrop-filter: blur(18px);
+ padding: 16px;
+}
+
+.project-reference-strip {
+ display: grid;
+ grid-template-columns: repeat(6, minmax(0, 1fr));
+ gap: 8px;
+ min-width: 0;
+}
+
+.project-reference-tile {
+ position: relative;
+ overflow: hidden;
+ border-radius: 8px;
+ background: rgba(255,255,255,0.92);
+ min-height: 70px;
+ aspect-ratio: 1 / 1;
+ box-shadow: inset 0 0 0 1px rgba(255,255,255,0.32);
+}
+
+.project-reference-tile span {
+ position: absolute;
+ left: 6px;
+ right: 6px;
+ bottom: 5px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ border-radius: 6px;
+ background: rgba(0,0,0,0.54);
+ padding: 2px 5px;
+ color: rgba(255,255,255,0.76);
+ font-size: 9px;
+}
+
+.project-board-grid {
+ display: grid;
+ grid-template-columns: minmax(250px, 310px) minmax(0, 1fr) 88px;
+ gap: 14px;
+ min-height: 0;
+ flex: 1;
+ align-items: stretch;
+}
+
+.project-brief-panel {
+ min-height: 0;
+ overflow-y: auto;
+ border: 1px solid rgba(255,255,255,0.10);
+ border-radius: 8px;
+ background:
+ linear-gradient(160deg, rgba(255,255,255,0.07), rgba(255,255,255,0.025)),
+ rgba(24,24,24,0.52);
+ box-shadow: inset 0 1px 0 rgba(255,255,255,0.07);
+ backdrop-filter: blur(18px);
+ padding: 16px;
+}
+
+.project-primary-preview {
+ position: relative;
+ display: grid;
+ place-items: center;
+ overflow: hidden;
+ border-radius: 8px;
+ background: rgba(255,255,255,0.92);
+ aspect-ratio: 4 / 3;
+ min-height: 190px;
+ box-shadow:
+ 0 20px 60px -38px rgba(230,245,120,0.7),
+ inset 0 0 0 1px rgba(255,255,255,0.36);
+}
+
+.project-stat {
+ border-radius: 8px;
+ background: rgba(255,255,255,0.045);
+ box-shadow: inset 0 0 0 1px rgba(255,255,255,0.07);
+ padding: 10px;
+ color: rgba(255,255,255,0.82);
+}
+
+.project-stat--accent {
+ background: rgba(230,245,120,0.13);
+ color: #e6f578;
+ box-shadow: inset 0 0 0 1px rgba(230,245,120,0.22);
+}
+
+.project-spec-card {
+ border-radius: 8px;
+ background: rgba(0,0,0,0.22);
+ box-shadow: inset 0 0 0 1px rgba(255,255,255,0.07);
+ padding: 13px;
+}
+
+.project-production-panel {
+ min-height: 0;
+ overflow: hidden;
+}
+
+.project-production-panel > div {
+ min-height: 0;
+}
+
.dashboard-workbench {
border: 1px solid rgba(255, 255, 255, 0.10);
border-radius: 8px;
@@ -439,9 +640,43 @@ input, textarea {
grid-template-columns: minmax(0, 1fr) 84px;
gap: 22px;
}
+
+ .project-board-grid {
+ grid-template-columns: minmax(280px, 340px) minmax(0, 1fr) 96px;
+ gap: 18px;
+ }
}
@media (max-width: 1180px) {
+ .project-shell {
+ overflow: auto;
+ }
+
+ .project-stage {
+ overflow: visible;
+ }
+
+ .project-stage-inner {
+ height: auto;
+ min-height: 100vh;
+ padding: 14px;
+ }
+
+ .project-board-head,
+ .project-board-grid {
+ display: flex;
+ flex-direction: column;
+ }
+
+ .project-reference-strip {
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ }
+
+ .project-brief-panel,
+ .project-production-panel {
+ overflow: visible;
+ }
+
.session-workspace {
height: auto;
min-height: 0;
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 9273e3c..07816ca 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -6,6 +6,7 @@ import Sidebar from '@/components/Sidebar';
import PackPanel from '@/components/PackPanel';
import ProjectGalleryDrawer from '@/components/ProjectGalleryDrawer';
import { OasisCanvas } from '@/components/login/OasisCanvas';
+import { PACK_ORDER, PACK_TEMPLATES } from '@/lib/templates';
import type {
GenImage,
GenSession,
@@ -22,6 +23,124 @@ import type {
} from '@/lib/types';
import type { VIDEO_TEMPLATES } from '@/lib/templates';
+function modeLabel(mode?: GenSession['inputMode']) {
+ if (mode === 'remix') return '二创项目';
+ if (mode === 'replicate') return '复刻项目';
+ if (mode === 'extend') return '补全项目';
+ return '想法项目';
+}
+
+function projectTitle(session: GenSession) {
+ return session.characterSpec?.name || session.prompt || '未命名项目';
+}
+
+function imageSourcesForSession(session: GenSession) {
+ const uploaded = session.uploadedImages?.map(image => ({
+ url: image.url,
+ label: image.role === 'subject' ? '主体图' : image.role === 'reference' ? '参考图' : image.accessoryName || image.role,
+ })) ?? [];
+ if (uploaded.length) return uploaded;
+ return session.refImages.map((url, index) => ({ url, label: `参考 ${index + 1}` }));
+}
+
+function packSlotTotal() {
+ return PACK_ORDER.reduce((sum, kind) => sum + PACK_TEMPLATES[kind].length, 0);
+}
+
+function ProjectStat({ label, value, tone }: { label: string; value: string | number; tone?: 'accent' | 'soft' }) {
+ return (
+
+ );
+}
+
+function ReferenceStrip({ session }: { session: GenSession }) {
+ const refs = imageSourcesForSession(session);
+ if (!refs.length) return null;
+
+ return (
+
+ {refs.slice(0, 6).map((ref, index) => (
+
+

+
{ref.label}
+
+ ))}
+
+ );
+}
+
+function ProjectBrief({ session }: { session: GenSession }) {
+ const selectedImages = session.images.filter(image => image.status === 'selected');
+ const primaryImage = selectedImages[0] ?? session.images[0] ?? null;
+ const generatedAssets = (session.packs ?? []).reduce((sum, pack) => sum + pack.assets.length, 0);
+ const totalSlots = packSlotTotal();
+
+ return (
+
+
+
+
Project Core
+
{projectTitle(session)}
+
+ {session.characterSpec?.oneLiner || session.prompt || '当前项目暂无描述'}
+
+
+
+ {modeLabel(session.inputMode)}
+
+
+
+ {primaryImage && (
+
+

+
+ Primary
+ {primaryImage.status === 'selected' ? '已选中' : '待筛选'}
+
+
+ )}
+
+
+
+ {session.characterSpec ? (
+
+
+ 角色设定
+ 已锁定
+
+
+ {[
+ ['形态', session.characterSpec.speciesShape],
+ ['比例', session.characterSpec.bodyRatio],
+ ['配色', session.characterSpec.colorPalette.join('、')],
+ ['材料', session.characterSpec.materials.join('、')],
+ ].map(([label, value]) => (
+
+ {label}
+ {value}
+
+ ))}
+
+
+ ) : (
+
+
角色设定
+
+ 从右侧图库选中主方案后,在生产矩阵里锁定角色设定。
+
+
+ )}
+
+ );
+}
+
export default function Home() {
const [sessions, setSessions] = useState([]);
const [current, setCurrent] = useState(null);
@@ -282,35 +401,24 @@ export default function Home() {
-
-
setSidebarOpen(v => !v)}
- sessions={sessions}
- currentId={current?.id ?? null}
- onPick={id => setCurrent(sessions.find(s => s.id === id) ?? null)}
- onNew={() => setCurrent(null)}
- />
-
-
-
-
-
+
+
setSidebarOpen(v => !v)}
+ sessions={sessions}
+ currentId={current?.id ?? null}
+ onPick={id => setCurrent(sessions.find(s => s.id === id) ?? null)}
+ onNew={() => setCurrent(null)}
+ />
+
+
-
- {current && (
- <>
+
+ {current && (
记录
- >
- )}
-
-
-
- {provider === 'gpt' ? 'GPT · gpt-image-2' : provider === 'mock' ? 'Mock · 占位图' : provider === '?' ? '待连接' : provider}
-
-
-
+ )}
+
+
+
+ {provider === 'gpt' ? 'GPT · gpt-image-2' : provider === 'mock' ? 'Mock · 占位图' : provider === '?' ? '待连接' : provider}
+
+
+
-
{!current && (
-
+
)}
+
{current && (
-
-
-
-
Project Workspace
-
项目工作台
-
- {new Date(current.createdAt).toLocaleString('zh-CN')}
+
+
+
+
Current Project
+
{projectTitle(current)}
+
+ {new Date(current.createdAt).toLocaleString('zh-CN')} · {current.id}
-
{current.id}
+
-
-
-
+
);
diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx
index d1facf6..3de7b75 100644
--- a/src/components/Sidebar.tsx
+++ b/src/components/Sidebar.tsx
@@ -9,66 +9,65 @@ function modeLabel(mode?: GenSession['inputMode']) {
return '想法';
}
-function imageSourcesForSession(session: GenSession) {
- const uploaded = session.uploadedImages?.map(image => ({
- url: image.url,
- label: image.role === 'subject' ? '主体图' : image.role === 'reference' ? '参考图' : image.accessoryName || image.role,
- })) ?? [];
- if (uploaded.length) return uploaded;
- return session.refImages.map((url, index) => ({ url, label: `参考 ${index + 1}` }));
+function projectTitle(session: GenSession) {
+ return session.characterSpec?.name || session.prompt || '未命名项目';
}
-function ActiveSessionDetail({ session }: { session: GenSession }) {
- const refs = imageSourcesForSession(session);
+function ProjectThumbs({ session }: { session: GenSession }) {
+ const refs = session.uploadedImages?.map(image => image.url) ?? session.refImages;
+ const thumbs = refs.length ? refs : session.images.map(image => image.url);
+
+ return (
+
+ {thumbs.slice(0, 3).map((url, index) => (
+
+
+
+ ))}
+ {thumbs.length === 0 && (
+
+ -
+
+ )}
+
+ );
+}
+
+function ProjectCard({ session, active, onPick }: { session: GenSession; active: boolean; onPick: () => void }) {
const selectedCount = session.images.filter(image => image.status === 'selected').length;
const packCount = session.packs?.length ?? 0;
return (
-
-
-
Current session
-
- {modeLabel(session.inputMode)}
-
-
-
-
- {session.characterSpec?.name || session.prompt || '未填写'}
-
-
-
-
-
图片
-
{session.images.length}
-
-
-
-
-
- {refs.length > 0 && (
-
-
Reference
-
- {refs.slice(0, 4).map((ref, index) => (
-
-

-
- ))}
+
);
}
@@ -87,15 +86,14 @@ export default function Sidebar({
onPick: (id: string) => void;
onNew: () => void;
}) {
- const activeSession = sessions.find(session => session.id === currentId) ?? null;
-
if (!open) {
return (
-