fix: redesign project workbench layout

This commit is contained in:
2026-05-20 15:01:21 +08:00
parent 4d047b0939
commit 794d95e7d5
4 changed files with 525 additions and 209 deletions

View File

@@ -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
}
]
}

View File

@@ -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;

View File

@@ -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 (
<div className={`project-stat ${tone === 'accent' ? 'project-stat--accent' : ''}`}>
<div className="text-[10px] text-white/40">{label}</div>
<div className="mt-1 text-[18px] font-semibold tracking-tight">{value}</div>
</div>
);
}
function ReferenceStrip({ session }: { session: GenSession }) {
const refs = imageSourcesForSession(session);
if (!refs.length) return null;
return (
<div className="project-reference-strip">
{refs.slice(0, 6).map((ref, index) => (
<div key={`${ref.url}-${index}`} className="project-reference-tile">
<img src={ref.url} alt={ref.label} className="h-full w-full object-contain" />
<span>{ref.label}</span>
</div>
))}
</div>
);
}
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 (
<section className="project-brief-panel">
<div className="flex items-start justify-between gap-4">
<div className="min-w-0">
<span className="section-eyebrow">Project Core</span>
<h2 className="mt-2 text-[20px] font-semibold leading-tight text-white">{projectTitle(session)}</h2>
<p className="mt-2 line-clamp-3 text-[12px] leading-relaxed text-white/50">
{session.characterSpec?.oneLiner || session.prompt || '当前项目暂无描述'}
</p>
</div>
<span className="rounded-full bg-[#e6f578]/15 px-3 py-1 text-[10px] font-semibold text-[#e6f578] ring-1 ring-[#e6f578]/25">
{modeLabel(session.inputMode)}
</span>
</div>
{primaryImage && (
<div className="project-primary-preview mt-5">
<img src={primaryImage.url} alt="当前主方案" className="h-full w-full object-contain" />
<div className="absolute inset-x-0 bottom-0 flex items-center justify-between gap-3 bg-gradient-to-t from-black/72 to-transparent p-3">
<span className="text-[10px] font-semibold uppercase tracking-[0.16em] text-white/70">Primary</span>
<span className="text-[10px] text-white/50">{primaryImage.status === 'selected' ? '已选中' : '待筛选'}</span>
</div>
</div>
)}
<div className="mt-5 grid grid-cols-3 gap-2">
<ProjectStat label="图库" value={session.images.length} />
<ProjectStat label="选中" value={selectedImages.length} tone="accent" />
<ProjectStat label="产出" value={`${generatedAssets}/${totalSlots}`} />
</div>
{session.characterSpec ? (
<div className="project-spec-card mt-5">
<div className="flex items-center justify-between gap-3">
<span className="text-[11px] font-semibold text-white/80"></span>
<span className="text-[10px] text-[#e6f578]/70"></span>
</div>
<div className="mt-3 space-y-2 text-[11px]">
{[
['形态', session.characterSpec.speciesShape],
['比例', session.characterSpec.bodyRatio],
['配色', session.characterSpec.colorPalette.join('、')],
['材料', session.characterSpec.materials.join('、')],
].map(([label, value]) => (
<div key={label} className="grid grid-cols-[42px_minmax(0,1fr)] gap-2">
<span className="text-white/35">{label}</span>
<span className="truncate text-white/75">{value}</span>
</div>
))}
</div>
</div>
) : (
<div className="project-spec-card mt-5">
<div className="text-[11px] font-semibold text-white/80"></div>
<p className="mt-2 text-[11px] leading-relaxed text-white/45">
</p>
</div>
)}
</section>
);
}
export default function Home() {
const [sessions, setSessions] = useState<GenSession[]>([]);
const [current, setCurrent] = useState<GenSession | null>(null);
@@ -282,7 +401,7 @@ export default function Home() {
<OasisCanvas />
<div className="app-oasis-shade" />
<div className="app-grass-floor" />
<div className="relative z-10 flex h-screen text-white">
<div className="project-shell relative z-10 flex h-screen text-white">
<Sidebar
open={sidebarOpen}
onToggle={() => setSidebarOpen(v => !v)}
@@ -291,26 +410,15 @@ export default function Home() {
onPick={id => setCurrent(sessions.find(s => s.id === id) ?? null)}
onNew={() => setCurrent(null)}
/>
<main className="flex-1 overflow-y-auto">
<div className="mx-auto max-w-[1680px] px-5 py-6 sm:px-8 xl:px-10">
<header className="flex items-center justify-between mb-8 gap-4">
<div className="flex items-center gap-3 min-w-0">
<div className="w-9 h-9 rounded-[8px] bg-gradient-to-br from-[#e6f578] to-[#d6b36a] flex items-center justify-center shrink-0 shadow-glow-violet">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#081006" strokeWidth="2.5">
<path d="M12 2l2.5 6.5L21 11l-6.5 2.5L12 20l-2.5-6.5L3 11l6.5-2.5z" strokeLinejoin="round" />
</svg>
</div>
<main className="project-stage min-w-0 flex-1">
<div className="project-stage-inner">
<header className="project-topbar">
<div className="min-w-0">
<h1 className="text-base font-semibold tracking-tight leading-tight text-[#f8f7ef]">
AI Toy Patent
<span className="ml-2 text-white/48 font-normal text-sm"> / / </span>
</h1>
<p className="text-[11px] text-[#e6f578]/55 mt-0.5"> · · · · Seedance </p>
<span className="section-eyebrow">AI Toy Patent</span>
<h1 className="mt-1 text-[18px] font-semibold tracking-tight text-[#f8f7ef]"></h1>
</div>
</div>
<div className="flex items-center gap-2 shrink-0">
<div className="flex min-w-0 items-center gap-2">
{current && (
<>
<a
href={`/api/audit/${encodeURIComponent(current.id)}`}
target="_blank"
@@ -319,7 +427,6 @@ export default function Home() {
>
</a>
</>
)}
<button
onClick={handleLogout}
@@ -328,14 +435,14 @@ export default function Home() {
退
</button>
<span className={provider === 'gpt' ? 'chip chip-live' : provider === '?' ? 'chip chip-neutral' : 'chip chip-mock'}>
<span className={`w-1.5 h-1.5 rounded-full ${provider === 'gpt' ? 'bg-emerald-400' : provider === '?' ? 'bg-white/40' : 'bg-amber-400'}`} />
<span className={`h-1.5 w-1.5 rounded-full ${provider === 'gpt' ? 'bg-emerald-400' : provider === '?' ? 'bg-white/40' : 'bg-amber-400'}`} />
{provider === 'gpt' ? 'GPT · gpt-image-2' : provider === 'mock' ? 'Mock · 占位图' : provider === '?' ? '待连接' : provider}
</span>
</div>
</header>
<div className="space-y-8">
{!current && (
<section className="project-empty-canvas">
<PromptPanel
session={null}
onGenerate={handleGenerate}
@@ -343,21 +450,25 @@ export default function Home() {
loading={loading}
uploadLoading={uploadLoading}
/>
</section>
)}
{current && (
<section className="dashboard-workbench session-workspace flex flex-col gap-5 p-5">
<div className="flex shrink-0 items-end justify-between gap-4">
<div>
<span className="section-eyebrow">Project Workspace</span>
<h2 className="mt-2 text-lg font-semibold text-white"></h2>
<p className="text-xs text-white/40 mt-1">
{new Date(current.createdAt).toLocaleString('zh-CN')}
<section className="project-board">
<div className="project-board-head">
<div className="min-w-0">
<span className="section-eyebrow">Current Project</span>
<h2 className="mt-2 line-clamp-1 text-[24px] font-semibold leading-tight text-white">{projectTitle(current)}</h2>
<p className="mt-1 text-[11px] text-white/40">
{new Date(current.createdAt).toLocaleString('zh-CN')} · {current.id}
</p>
</div>
<code className="max-w-[220px] truncate text-[11px] text-white/30 font-mono">{current.id}</code>
<ReferenceStrip session={current} />
</div>
<div className="session-split min-h-0 flex-1">
<aside className="session-pack-pane min-h-0 overflow-hidden">
<div className="project-board-grid">
<ProjectBrief session={current} />
<section className="project-production-panel">
<PackPanel
session={current}
loadingKind={loadingKind}
@@ -370,13 +481,12 @@ export default function Home() {
onRegenerateAsset={handleRegenerateAsset}
onGenerateVideo={handleGenerateVideo}
/>
</aside>
</section>
<ProjectGalleryDrawer session={current} onAction={handleAction} />
</div>
</section>
)}
</div>
</div>
</main>
</div>
</div>

View File

@@ -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 (
<div className="flex -space-x-2">
{thumbs.slice(0, 3).map((url, index) => (
<span key={`${url}-${index}`} className="grid h-8 w-8 overflow-hidden rounded-[8px] bg-white ring-1 ring-white/15">
<img src={url} alt="" className="h-full w-full object-contain" />
</span>
))}
{thumbs.length === 0 && (
<span className="grid h-8 w-8 place-items-center rounded-[8px] bg-white/[0.06] text-[10px] text-white/30 ring-1 ring-white/10">
-
</span>
)}
</div>
);
}
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 (
<div className="rounded-[8px] border border-white/10 bg-white/[0.055] p-3 shadow-[0_18px_52px_-38px_rgba(230,245,120,0.68)] backdrop-blur-xl">
<button
onClick={onPick}
className={`project-card-button w-full cursor-pointer rounded-[8px] p-3 text-left transition-all ${
active
? 'bg-[#e6f578]/15 text-white ring-1 ring-[#e6f578]/45 shadow-[0_18px_48px_-34px_rgba(230,245,120,0.85)]'
: 'bg-white/[0.035] text-white/75 ring-1 ring-white/[0.07] hover:bg-white/[0.06] hover:text-white hover:ring-white/20'
}`}
>
<div className="flex items-start gap-3">
<ProjectThumbs session={session} />
<div className="min-w-0 flex-1">
<div className="flex items-center justify-between gap-2">
<div className="text-[9px] uppercase tracking-[0.16em] text-[#b9c22a]/80">Current session</div>
<span className="rounded-full border border-white/10 bg-black/28 px-2 py-0.5 text-[9px] text-white/48">
<span className={`rounded-full px-2 py-0.5 text-[9px] ${active ? 'bg-[#e6f578] text-[#081006]' : 'bg-black/25 text-white/45'}`}>
{modeLabel(session.inputMode)}
</span>
<span className="text-[9px] text-white/30">
{new Date(session.createdAt).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit' })}
</span>
</div>
<div className="mt-2 line-clamp-4 text-[11px] font-medium leading-relaxed text-white/80">
{session.characterSpec?.name || session.prompt || '未填写'}
<div className="mt-2 line-clamp-2 text-[12px] font-semibold leading-snug">
{projectTitle(session)}
</div>
<div className="mt-3 grid grid-cols-3 gap-1.5 text-[9px] text-white/44">
<div className="rounded-[6px] bg-black/22 px-2 py-1.5">
<div></div>
<b className="mt-0.5 block text-[11px] text-white/78">{session.images.length}</b>
</div>
<div className="rounded-[6px] bg-black/22 px-2 py-1.5">
<div></div>
<b className="mt-0.5 block text-[11px] text-[#e6f578]">{selectedCount}</b>
</div>
<div className="rounded-[6px] bg-black/22 px-2 py-1.5">
<div></div>
<b className="mt-0.5 block text-[11px] text-white/78">{packCount}</b>
<div className="mt-2 flex items-center gap-2 text-[10px] text-white/40">
<span>{session.images.length} </span>
<span>{selectedCount} </span>
<span>{packCount} </span>
</div>
</div>
{refs.length > 0 && (
<div className="mt-3">
<div className="mb-1.5 text-[9px] uppercase tracking-[0.14em] text-white/34">Reference</div>
<div className="grid grid-cols-4 gap-1.5">
{refs.slice(0, 4).map((ref, index) => (
<div key={`${ref.url}-${index}`} className="relative aspect-square overflow-hidden rounded-[6px] bg-white/90 ring-1 ring-white/10">
<img src={ref.url} alt={ref.label} className="h-full w-full object-contain" />
</div>
))}
</div>
</div>
)}
<div className="mt-3 flex items-center justify-between gap-2 text-[9px] text-white/36">
<span>{new Date(session.createdAt).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })}</span>
<span className="font-mono">{session.id.slice(0, 10)}</span>
</div>
</div>
</button>
);
}
@@ -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 (
<aside className="glass-sidebar w-12 shrink-0 flex flex-col items-center py-4">
<aside className="project-dock project-dock--closed w-14 shrink-0">
<button
onClick={onToggle}
className="w-8 h-8 rounded-lg hover:bg-white/[0.08] flex items-center justify-center text-white/60 transition-colors"
title="展开栏"
className="mt-4 grid h-10 w-10 cursor-pointer place-items-center rounded-[8px] bg-white/[0.06] text-white/60 transition-colors hover:bg-white/[0.10] hover:text-white"
title="展开项目栏"
aria-label="展开项目栏"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M9 18l6-6-6-6" />
@@ -106,33 +104,21 @@ export default function Sidebar({
}
return (
<aside className="glass-sidebar w-[304px] shrink-0 overflow-hidden">
<div className="flex h-full min-h-0">
<div className="flex w-[58px] shrink-0 flex-col items-center border-r border-white/[0.06] py-5">
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-gradient-to-br from-[#d9df2a] via-[#a7c83b] to-[#49c7b6] text-[11px] font-bold text-[#081006] shadow-[0_12px_34px_-18px_rgba(185,194,42,0.9)]">
Lo
<aside className="project-dock w-[276px] shrink-0 overflow-hidden">
<div className="flex h-full min-h-0 flex-col p-4">
<div className="flex items-center gap-3">
<div className="grid h-10 w-10 place-items-center rounded-[8px] bg-[#e6f578] text-sm font-black text-[#081006]">
AI
</div>
<div className="mt-7 flex flex-col gap-4 text-white/32">
{['M4 6h16M4 12h16M4 18h16', 'M4 5h7v7H4zM13 5h7v7h-7zM4 14h7v5H4zM13 14h7v5h-7z', 'M5 7h14M7 7v12h10V7M9 7V5h6v2', 'M6 6h12v12H6z'].map((path, index) => (
<span key={index} className={`flex h-8 w-8 items-center justify-center rounded-[8px] ${index === 0 ? 'bg-white/[0.08] text-[#d9df2a]' : 'hover:bg-white/[0.06] hover:text-white/70'}`}>
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d={path} strokeLinecap="round" strokeLinejoin="round" />
</svg>
</span>
))}
</div>
</div>
<div className="flex min-w-0 flex-1 flex-col">
<div className="flex items-center gap-3 px-4 pb-3 pt-5">
<div className="min-w-0 flex-1">
<div className="text-[13px] font-semibold tracking-tight">Toy Patent</div>
<div className="text-[10px] text-white/40">AI </div>
<div className="text-[13px] font-semibold tracking-tight text-white">Project Dock</div>
<div className="text-[10px] text-white/40"> = </div>
</div>
<button
onClick={onToggle}
className="flex h-8 w-8 items-center justify-center rounded-[8px] text-white/60 transition-colors hover:bg-white/[0.08]"
title="收起栏"
className="grid h-9 w-9 cursor-pointer place-items-center rounded-[8px] text-white/55 transition-colors hover:bg-white/[0.08] hover:text-white"
title="收起项目栏"
aria-label="收起项目栏"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M15 18l-6-6 6-6" />
@@ -140,72 +126,36 @@ export default function Sidebar({
</button>
</div>
<div className="px-4 pb-4">
<button
onClick={onNew}
className="btn btn-primary w-full text-sm"
>
<button onClick={onNew} className="btn btn-primary mt-5 w-full justify-center text-sm">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<path d="M12 5v14M5 12h14" strokeLinecap="round" />
</svg>
</button>
<div className="mt-6 flex items-center justify-between text-[10px] font-semibold uppercase tracking-[0.18em] text-white/35">
<span>Projects</span>
<span className="tracking-normal">{sessions.length}</span>
</div>
{activeSession && (
<div className="px-3 pb-4">
<ActiveSessionDetail session={activeSession} />
</div>
)}
<div className="px-5 pb-2 pt-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-white/35">
</div>
<div className="flex-1 space-y-1 overflow-y-auto px-3 pb-4">
<div className="project-dock-list mt-3 flex-1 space-y-2 overflow-y-auto pr-1">
{sessions.length === 0 && (
<div className="px-3 py-12 text-center text-xs text-white/30">
<div className="rounded-[8px] border border-dashed border-white/10 px-4 py-12 text-center text-xs text-white/30">
</div>
)}
{sessions.map(s => {
const selectedCount = s.images.filter(i => i.status === 'selected').length;
const active = currentId === s.id;
return (
<div key={s.id} className="relative">
<button
onClick={() => onPick(s.id)}
className={`w-full rounded-[8px] px-3 py-2.5 text-left text-xs transition-all ${
active
? 'bg-gradient-to-r from-[#d9df2a]/22 via-[#6e63c8]/16 to-[#49c7b6]/14 text-white ring-1 ring-[#d9df2a]/35 shadow-[0_18px_48px_-32px_rgba(185,194,42,0.86)]'
: 'text-white/75 ring-1 ring-transparent hover:bg-white/[0.055] hover:text-white/90 hover:ring-white/10'
}`}
>
<div className={`line-clamp-2 font-medium leading-relaxed ${active ? 'text-white' : 'text-white/85'}`}>
{s.characterSpec?.name || s.prompt}
</div>
<div className={`mt-1.5 flex items-center justify-between text-[10px] ${active ? 'text-white/65' : 'text-white/40'}`}>
<span>
{new Date(s.createdAt).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })}
</span>
<span className="flex items-center gap-1.5">
<span>{s.images.length} </span>
{selectedCount > 0 && (
<span className={active ? 'font-semibold text-[#d9df2a]' : 'font-semibold text-[#d6b36a]'}>
{selectedCount}
</span>
)}
</span>
</div>
</button>
</div>
);
})}
{sessions.map(session => (
<ProjectCard
key={session.id}
session={session}
active={currentId === session.id}
onPick={() => onPick(session.id)}
/>
))}
</div>
<div className="border-t border-white/[0.06] px-5 py-3 text-[10px] text-white/30">
4560 · / data
</div>
<div className="mt-4 border-t border-white/[0.06] pt-3 text-[10px] text-white/30">
Docker · 4560
</div>
</div>
</aside>