auto-save 2026-05-19 00:17 (~8)

This commit is contained in:
2026-05-19 00:18:42 +08:00
parent c3a46375aa
commit 361bbefa71
8 changed files with 342 additions and 148 deletions

View File

@@ -182,6 +182,25 @@
"type": "session-heartbeat", "type": "session-heartbeat",
"message": "Claude 会话活跃 · 最近命令claude · 分支 master · 1 项未提交变更 · 最近提交auto-save 2026-05-19 00:06 (+1, ~1)", "message": "Claude 会话活跃 · 最近命令claude · 分支 master · 1 项未提交变更 · 最近提交auto-save 2026-05-19 00:06 (+1, ~1)",
"files_changed": 1 "files_changed": 1
},
{
"ts": "2026-05-19T00:11:58+08:00",
"type": "commit",
"message": "auto-save 2026-05-19 00:11 (+3, ~1)",
"hash": "c3a4637",
"files_changed": 4
},
{
"ts": "2026-05-18T16:13:50Z",
"type": "session-heartbeat",
"message": "Codex 会话活跃 · 最近命令codex · 分支 master · 1 项未提交变更 · 最近提交auto-save 2026-05-19 00:11 (+3, ~1)",
"files_changed": 1
},
{
"ts": "2026-05-18T16:16:50Z",
"type": "session-heartbeat",
"message": "Claude 会话活跃 · 最近命令claude · 分支 master · 7 项未提交变更 · 最近提交auto-save 2026-05-19 00:11 (+3, ~1)",
"files_changed": 7
} }
] ]
} }

View File

@@ -3,88 +3,150 @@
@tailwind utilities; @tailwind utilities;
html, body { html, body {
background: #FAFAFA; background: #0A0A0F;
color: #09090B; color: #FFFFFF;
font-feature-settings: "cv02", "cv03", "cv04", "cv11"; font-feature-settings: "cv02", "cv03", "cv04", "cv11", "ss01";
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
} }
* { box-sizing: border-box; } * { box-sizing: border-box; }
body { body {
font-family: ui-sans-serif, system-ui, -apple-system, "SF Pro Text", "PingFang SC", "Noto Sans SC", "Helvetica Neue", sans-serif; font-family: ui-sans-serif, system-ui, -apple-system, "SF Pro Text", "PingFang SC", "Noto Sans SC", "Helvetica Neue", sans-serif;
background-color: #0A0A0F;
background-image:
radial-gradient(circle at 18% -10%, rgba(139, 92, 246, 0.22), transparent 55%),
radial-gradient(circle at 88% 8%, rgba(59, 130, 246, 0.18), transparent 50%),
radial-gradient(circle at 50% 110%, rgba(217, 70, 239, 0.12), transparent 60%);
background-attachment: fixed;
min-height: 100vh;
} }
::selection { background: rgba(139, 92, 246, 0.35); color: #fff; }
/* ===== Buttons ===== */
.btn { .btn {
@apply inline-flex items-center justify-center gap-1.5 px-4 py-2 rounded-xl text-sm font-medium transition-all; @apply inline-flex items-center justify-center gap-1.5 px-4 py-2 rounded-xl text-sm font-medium transition-all;
} }
.btn-primary { .btn-primary {
@apply bg-zinc-900 text-white hover:bg-zinc-800 active:scale-[0.98] shadow-sm; @apply text-white bg-gradient-to-r from-violet-500 via-indigo-500 to-blue-500 hover:brightness-110 active:scale-[0.98];
box-shadow: 0 6px 24px -8px rgba(99, 102, 241, 0.6), inset 0 1px 0 rgba(255,255,255,0.18);
} }
.btn-ghost { .btn-ghost {
@apply bg-zinc-100 text-zinc-700 hover:bg-zinc-200 active:scale-[0.98]; @apply text-white/80 bg-white/[0.05] hover:bg-white/[0.09] border border-white/[0.08] active:scale-[0.98];
} }
.btn-outline { .btn-outline {
@apply bg-white text-zinc-700 border border-zinc-200 hover:border-zinc-300 hover:bg-zinc-50 active:scale-[0.98]; @apply text-white/80 bg-white/[0.03] hover:bg-white/[0.07] border border-white/[0.1] hover:border-white/[0.18] active:scale-[0.98];
}
.btn-glass {
@apply text-white bg-white/[0.06] hover:bg-white/[0.10] border border-white/[0.12] backdrop-blur-xl active:scale-[0.98];
} }
/* ===== Cards (glass) ===== */
.card { .card {
@apply bg-white border border-zinc-200/70 rounded-2xl shadow-[0_1px_2px_rgba(0,0,0,0.04),0_4px_12px_rgba(0,0,0,0.04)]; @apply relative rounded-3xl bg-white/[0.035] border border-white/[0.08] backdrop-blur-xl;
box-shadow:
0 1px 0 0 rgba(255,255,255,0.06) inset,
0 18px 60px -24px rgba(0,0,0,0.6);
}
.card-2 {
@apply rounded-2xl bg-white/[0.03] border border-white/[0.07] backdrop-blur-xl;
} }
.card-hover { .card-hover {
@apply transition-shadow hover:shadow-[0_2px_4px_rgba(0,0,0,0.04),0_8px_24px_rgba(0,0,0,0.06)]; @apply transition-all hover:border-white/[0.14];
} }
/* ===== Chips ===== */
.chip { .chip {
@apply inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium; @apply inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-[11px] font-medium border;
} }
.chip-mock { .chip-mock {
@apply bg-amber-50 text-amber-700 border border-amber-200; @apply bg-amber-500/10 text-amber-300 border-amber-400/30;
} }
.chip-live { .chip-live {
@apply bg-emerald-50 text-emerald-700 border border-emerald-200; @apply bg-emerald-500/10 text-emerald-300 border-emerald-400/30;
}
.chip-neutral {
@apply bg-white/[0.05] text-white/70 border-white/[0.12];
}
.chip-violet {
@apply bg-violet-500/15 text-violet-200 border-violet-400/30;
} }
/* ===== Segmented ===== */
.seg { .seg {
@apply inline-flex p-1 bg-zinc-100 rounded-xl gap-1; @apply inline-flex p-1 bg-white/[0.04] border border-white/[0.07] rounded-xl gap-1;
} }
.seg-item { .seg-item {
@apply px-3 py-1.5 rounded-lg text-xs font-medium text-zinc-600 transition-all cursor-pointer; @apply px-3 py-1.5 rounded-lg text-xs font-medium text-white/55 transition-all cursor-pointer;
} }
.seg-item-active { .seg-item-active {
@apply bg-white text-zinc-900 shadow-sm; @apply text-white;
background: linear-gradient(135deg, rgba(139,92,246,0.35), rgba(59,130,246,0.25));
box-shadow: inset 0 1px 0 rgba(255,255,255,0.18), 0 4px 16px -8px rgba(99,102,241,0.6);
} }
/* ===== Tiles ===== */
.tile { .tile {
@apply relative aspect-square overflow-hidden rounded-2xl bg-zinc-100 border-2 border-transparent ring-1 ring-zinc-200/70 transition-all cursor-pointer; @apply relative aspect-square overflow-hidden rounded-2xl bg-white/[0.04] ring-1 ring-white/[0.08] transition-all cursor-pointer;
} }
.tile-selected { .tile-selected {
@apply border-zinc-900 ring-0 shadow-[0_4px_16px_rgba(0,0,0,0.12)]; position: relative;
background: linear-gradient(#0A0A0F, #0A0A0F) padding-box, linear-gradient(135deg, #8B5CF6, #3B82F6) border-box;
border: 2px solid transparent;
box-shadow: 0 0 0 1px rgba(139,92,246,0.25), 0 8px 32px -8px rgba(139,92,246,0.55);
} }
.tile-rejected { .tile-rejected {
@apply opacity-30 grayscale; @apply opacity-30 grayscale;
} }
.tile-keynum { .tile-keynum {
@apply absolute top-2 left-2 w-7 h-7 rounded-lg bg-white/95 backdrop-blur text-[11px] font-semibold text-zinc-700 flex items-center justify-center shadow-sm; @apply absolute top-2 left-2 w-7 h-7 rounded-lg bg-black/60 backdrop-blur text-[11px] font-semibold text-white/85 flex items-center justify-center ring-1 ring-white/10;
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace; font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
} }
.tile-badge { .tile-badge {
@apply absolute top-2 right-2 w-7 h-7 rounded-full flex items-center justify-center text-xs font-bold shadow-sm; @apply absolute top-2 right-2 w-7 h-7 rounded-full flex items-center justify-center text-xs font-bold;
box-shadow: 0 6px 18px -4px rgba(0,0,0,0.6);
} }
.tile-badge-selected { .tile-badge-selected {
@apply bg-zinc-900 text-white; background: linear-gradient(135deg, #8B5CF6, #3B82F6);
color: #fff;
} }
.tile-badge-rejected { .tile-badge-rejected {
@apply bg-red-500 text-white; @apply bg-rose-500 text-white;
} }
/* ===== Fields ===== */
input, textarea { input, textarea {
font-family: inherit; font-family: inherit;
} }
.field { .field {
@apply w-full bg-white border border-zinc-200 rounded-xl px-3.5 py-3 text-sm text-zinc-900 placeholder:text-zinc-400 transition-colors focus:border-zinc-900 focus:ring-2 focus:ring-zinc-900/5 outline-none resize-none; @apply w-full rounded-xl px-3.5 py-3 text-sm text-white placeholder:text-white/30 outline-none resize-none transition-colors;
background: rgba(255,255,255,0.04);
border: 1px solid rgba(255,255,255,0.08);
}
.field:focus {
border-color: rgba(139, 92, 246, 0.55);
box-shadow: 0 0 0 4px rgba(139, 92, 246, 0.12);
} }
/* ===== KBD ===== */
.kbd { .kbd {
@apply inline-flex items-center justify-center min-w-[20px] h-5 px-1.5 rounded bg-white border border-zinc-200 text-[10px] font-medium text-zinc-600 shadow-sm; @apply inline-flex items-center justify-center min-w-[20px] h-5 px-1.5 rounded text-[10px] font-medium text-white/70 ring-1 ring-white/10 bg-white/[0.06];
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace; font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
} }
/* ===== Section header ===== */
.section-eyebrow {
@apply inline-block text-[10px] font-semibold uppercase tracking-[0.18em] text-violet-300/80;
}
/* ===== Divider ===== */
.divider-line {
@apply h-px w-full;
background: linear-gradient(to right, transparent, rgba(255,255,255,0.10), transparent);
}
/* ===== Scrollbar (subtle) ===== */
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.06); border-radius: 8px; }
::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.12); }

View File

@@ -176,7 +176,7 @@ export default function Home() {
} }
return ( return (
<div className="flex h-screen bg-[#FAFAFA]"> <div className="flex h-screen text-white">
<Sidebar <Sidebar
open={sidebarOpen} open={sidebarOpen}
onToggle={() => setSidebarOpen(v => !v)} onToggle={() => setSidebarOpen(v => !v)}
@@ -186,20 +186,22 @@ export default function Home() {
onNew={() => setCurrent(null)} onNew={() => setCurrent(null)}
/> />
<main className="flex-1 overflow-y-auto"> <main className="flex-1 overflow-y-auto">
<div className="mx-auto max-w-[1180px] px-10 py-10"> <div className="mx-auto max-w-[1200px] px-10 py-10">
<header className="flex items-start justify-between mb-10"> <header className="flex items-start justify-between mb-10">
<div> <div>
<h1 className="text-[26px] font-semibold tracking-tight text-zinc-900 leading-tight"> <span className="section-eyebrow">AI Toy Workflow</span>
AI <h1 className="mt-2 text-[30px] font-semibold tracking-tight leading-tight">
<span className="bg-gradient-to-r from-violet-300 via-fuchsia-300 to-blue-300 bg-clip-text text-transparent"> </span>
</h1> </h1>
<p className="text-sm text-zinc-500 mt-1.5"> <p className="text-sm text-white/50 mt-2 max-w-[560px] leading-relaxed">
· · · · Seedance
</p> </p>
</div> </div>
<div className="flex items-center gap-2 pt-1"> <div className="flex items-center gap-2 pt-1 shrink-0">
<span className={provider === 'gpt' ? 'chip chip-live' : 'chip chip-mock'}> <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-500' : 'bg-amber-500'}`} /> <span className={`w-1.5 h-1.5 rounded-full ${provider === 'gpt' ? 'bg-emerald-400' : provider === '?' ? 'bg-white/40' : 'bg-amber-400'}`} />
{provider === 'gpt' ? 'GPT · 最高规格' : provider === 'mock' ? 'Mock · 占位图' : provider} {provider === 'gpt' ? 'GPT · 最高规格' : provider === 'mock' ? 'Mock · 占位图' : provider === '?' ? '待连接' : provider}
</span> </span>
</div> </div>
</header> </header>
@@ -207,15 +209,16 @@ export default function Home() {
<div className="space-y-8"> <div className="space-y-8">
<PromptPanel onGenerate={handleGenerate} loading={loading} /> <PromptPanel onGenerate={handleGenerate} loading={loading} />
{current && ( {current && (
<section className="space-y-4"> <section className="space-y-5">
<div className="flex items-end justify-between"> <div className="flex items-end justify-between">
<div> <div>
<h2 className="text-sm font-medium text-zinc-900"></h2> <span className="section-eyebrow">Step · 02 · Quick Screen</span>
<p className="text-xs text-zinc-500 mt-0.5"> <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')} {new Date(current.createdAt).toLocaleString('zh-CN')}
</p> </p>
</div> </div>
<code className="text-[11px] text-zinc-400 font-mono">{current.id}</code> <code className="text-[11px] text-white/30 font-mono">{current.id}</code>
</div> </div>
<ResultGrid images={current.images} onAction={handleAction} /> <ResultGrid images={current.images} onAction={handleAction} />
<PackPanel <PackPanel

View File

@@ -4,11 +4,32 @@ import type { GenImage, GenSession, PackKind } from '@/lib/types';
import { PACK_LABELS, PACK_ORDER, VIDEO_TEMPLATES } from '@/lib/templates'; import { PACK_LABELS, PACK_ORDER, VIDEO_TEMPLATES } from '@/lib/templates';
const PACK_DESCRIPTIONS: Record<PackKind, string> = { const PACK_DESCRIPTIONS: Record<PackKind, string> = {
patent: '六面视图、45 立体图和局部放大,用于外观专利素材整理。', patent: '六面视图、45° 立体图和局部放大,用于外观专利素材整理。',
production: '尺寸、材料、颜色、拆件和包装结构,用于工厂报价与打样沟通。', production: '尺寸、材料、颜色、拆件和包装结构,用于工厂报价与打样沟通。',
marketing: '白底商品图、场景图、细节图和社媒图,用于新品宣发。', marketing: '白底商品图、场景图、细节图和社媒图,用于新品宣发。',
}; };
const PACK_ACCENT: Record<PackKind, { ring: string; chip: string; dot: string; bar: string }> = {
patent: {
ring: 'ring-violet-400/30',
chip: 'bg-violet-500/15 text-violet-200 border-violet-400/30',
dot: 'bg-violet-400',
bar: 'from-violet-400 to-indigo-400',
},
production: {
ring: 'ring-amber-400/30',
chip: 'bg-amber-500/15 text-amber-200 border-amber-400/30',
dot: 'bg-amber-400',
bar: 'from-amber-400 to-orange-400',
},
marketing: {
ring: 'ring-emerald-400/30',
chip: 'bg-emerald-500/15 text-emerald-200 border-emerald-400/30',
dot: 'bg-emerald-400',
bar: 'from-emerald-400 to-teal-400',
},
};
function manifestUrl(sessionId: string, kind: PackKind, version: string) { function manifestUrl(sessionId: string, kind: PackKind, version: string) {
return `/api/export/${sessionId}_${kind}_${version}_manifest.json`; return `/api/export/${sessionId}_${kind}_${version}_manifest.json`;
} }
@@ -40,26 +61,35 @@ export default function PackPanel({
if (!primaryImage) { if (!primaryImage) {
return ( return (
<section className="card p-5"> <section className="card p-6">
<h2 className="text-sm font-semibold text-zinc-900"></h2> <div className="flex items-start gap-4">
<p className="mt-1 text-xs text-zinc-500"> <div className="w-9 h-9 rounded-xl bg-white/[0.05] border border-white/[0.08] flex items-center justify-center text-white/40 text-sm"></div>
<div>
</p> <span className="section-eyebrow">Step · 03 · Lock Character</span>
<h2 className="mt-2 text-sm font-semibold text-white"></h2>
<p className="mt-1 text-[11px] text-white/45 leading-relaxed">
Seedance
</p>
</div>
</div>
</section> </section>
); );
} }
return ( return (
<section className="card p-5 space-y-5"> <section className="card p-6 space-y-6">
<div className="flex items-start justify-between gap-4"> <div className="flex items-start justify-between gap-4">
<div> <div>
<h2 className="text-sm font-semibold text-zinc-900"></h2> <span className="section-eyebrow">Step · 03 · Lock Character</span>
<p className="mt-1 text-xs text-zinc-500"> <h2 className="mt-2 text-lg font-semibold text-white"> & </h2>
<p className="mt-1 text-[11px] text-white/45 leading-relaxed max-w-[440px]">
CharacterSpec
</p> </p>
</div> </div>
<div className="w-16 h-16 rounded-xl overflow-hidden ring-1 ring-zinc-200 bg-zinc-100 shrink-0"> <div className="relative w-20 h-20 rounded-2xl overflow-hidden ring-1 ring-white/15 shrink-0 shadow-glow-violet">
<img src={primaryImage.url} alt="selected source" className="w-full h-full object-cover" /> <img src={primaryImage.url} alt="selected source" className="w-full h-full object-cover" />
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent" />
<div className="absolute bottom-1 left-1 right-1 text-[9px] font-semibold text-white/90 uppercase tracking-wider truncate"></div>
</div> </div>
</div> </div>
@@ -67,64 +97,88 @@ export default function PackPanel({
<button <button
onClick={() => onLockCharacter(primaryImage)} onClick={() => onLockCharacter(primaryImage)}
disabled={characterLoading || !!loadingKind || allLoading} disabled={characterLoading || !!loadingKind || allLoading}
className="btn btn-outline text-xs" className="btn btn-glass text-xs disabled:opacity-40 disabled:cursor-not-allowed"
> >
{characterLoading ? '锁定中' : session.characterSpec ? '刷新角色设定' : '锁定角色设定'} {characterLoading ? (
<>
<svg width="12" height="12" viewBox="0 0 24 24" className="animate-spin" fill="none" stroke="currentColor" strokeWidth="2.5">
<path d="M12 2a10 10 0 0 1 10 10" strokeLinecap="round" />
</svg>
</>
) : session.characterSpec ? '刷新角色设定' : '锁定角色设定'}
</button> </button>
<button <button
onClick={() => onGenerateAll(primaryImage)} onClick={() => onGenerateAll(primaryImage)}
disabled={allLoading || !!loadingKind || characterLoading} disabled={allLoading || !!loadingKind || characterLoading}
className="btn btn-primary text-xs" className="btn btn-primary text-xs disabled:opacity-40 disabled:cursor-not-allowed"
> >
{allLoading ? '全量生成中' : '一键生成完整三包'} {allLoading ? '全量生成中' : '一键三包'}
</button> </button>
</div> </div>
{session.characterSpec && ( {session.characterSpec && (
<div className="rounded-2xl bg-zinc-50 border border-zinc-200/70 p-4"> <div className="card-2 p-4">
<div className="flex items-center justify-between gap-3"> <div className="flex items-center justify-between gap-3 mb-3">
<div> <div>
<div className="text-xs font-semibold text-zinc-900">{session.characterSpec.name}</div> <div className="text-sm font-semibold text-white">{session.characterSpec.name}</div>
<div className="text-xs text-zinc-500 mt-1 line-clamp-2">{session.characterSpec.oneLiner}</div> <div className="text-[11px] text-white/55 mt-1 line-clamp-2 max-w-[440px]">{session.characterSpec.oneLiner}</div>
</div> </div>
<span className="chip bg-white text-zinc-600 border border-zinc-200">CharacterSpec</span> <span className="chip chip-violet shrink-0">CharacterSpec · v1</span>
</div> </div>
<div className="mt-3 grid grid-cols-2 gap-2 text-[11px] text-zinc-600"> <div className="divider-line mb-3" />
<div>{session.characterSpec.speciesShape}</div> <div className="grid grid-cols-2 gap-x-4 gap-y-1.5 text-[11px]">
<div>{session.characterSpec.bodyRatio}</div> <div className="flex gap-2"><span className="text-white/40 w-12"></span><span className="text-white/85">{session.characterSpec.speciesShape}</span></div>
<div>{session.characterSpec.colorPalette.join('、')}</div> <div className="flex gap-2"><span className="text-white/40 w-12"></span><span className="text-white/85">{session.characterSpec.bodyRatio}</span></div>
<div>{session.characterSpec.materials.join('、')}</div> <div className="flex gap-2"><span className="text-white/40 w-12"></span><span className="text-white/85">{session.characterSpec.colorPalette.join('、')}</span></div>
<div className="flex gap-2"><span className="text-white/40 w-12"></span><span className="text-white/85">{session.characterSpec.materials.join('、')}</span></div>
</div> </div>
</div> </div>
)} )}
<div className="grid grid-cols-3 gap-3"> <div className="grid grid-cols-3 gap-4">
{PACK_ORDER.map(kind => { {PACK_ORDER.map(kind => {
const pack = packs.find(item => item.kind === kind && item.sourceImageId === primaryImage.id); const pack = packs.find(item => item.kind === kind && item.sourceImageId === primaryImage.id);
const isLoading = loadingKind === kind; const isLoading = loadingKind === kind;
const accent = PACK_ACCENT[kind];
return ( return (
<div key={kind} className="rounded-2xl border border-zinc-200 p-4 bg-white space-y-3"> <div key={kind} className={`relative rounded-2xl bg-white/[0.03] border border-white/[0.08] backdrop-blur-xl p-4 space-y-3 ring-1 ${accent.ring}`}>
<div> <div className={`absolute top-0 left-4 right-4 h-px bg-gradient-to-r ${accent.bar} opacity-60`} />
<div className="text-sm font-semibold text-zinc-900">{PACK_LABELS[kind]}</div> <div className="flex items-center justify-between">
<p className="text-[11px] text-zinc-500 mt-1 leading-relaxed">{PACK_DESCRIPTIONS[kind]}</p> <div className="flex items-center gap-2">
<div className={`w-1.5 h-1.5 rounded-full ${accent.dot}`} />
<div className="text-sm font-semibold text-white">{PACK_LABELS[kind]}</div>
</div>
{pack && <span className="text-[10px] text-white/50 font-mono">{pack.version}</span>}
</div> </div>
<p className="text-[11px] text-white/45 leading-relaxed">{PACK_DESCRIPTIONS[kind]}</p>
<button <button
onClick={() => onGenerate(primaryImage, kind)} onClick={() => onGenerate(primaryImage, kind)}
disabled={!!loadingKind} disabled={!!loadingKind}
className={pack ? 'btn btn-outline w-full text-xs' : 'btn btn-primary w-full text-xs'} className={`${pack ? 'btn btn-outline' : 'btn btn-primary'} w-full text-xs disabled:opacity-40 disabled:cursor-not-allowed`}
> >
{isLoading ? '生成中' : pack ? '重新生成' : `生成${PACK_LABELS[kind]}`} {isLoading ? (
<>
<svg width="12" height="12" viewBox="0 0 24 24" className="animate-spin" fill="none" stroke="currentColor" strokeWidth="2.5">
<path d="M12 2a10 10 0 0 1 10 10" strokeLinecap="round" />
</svg>
</>
) : pack ? '重新生成' : `生成 ${PACK_LABELS[kind]}`}
</button> </button>
{pack && ( {pack && (
<div className="space-y-2"> <div className="pt-1 space-y-1.5">
<div className="text-[11px] text-zinc-500"> <div className="text-[10px] text-white/55">
<span className="font-semibold text-zinc-900">{pack.assets.length}</span> · {pack.version} <span className="font-semibold text-white">{pack.assets.length}</span>
</div> </div>
<a <a
href={manifestUrl(session.id, kind, pack.version)} href={manifestUrl(session.id, kind, pack.version)}
className="text-[11px] font-medium text-zinc-900 underline underline-offset-2" className="inline-flex items-center gap-1 text-[10px] text-violet-300 hover:text-violet-200 transition-colors"
> >
manifest JSON <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<path d="M12 4v12m0 0l-4-4m4 4l4-4M4 20h16" strokeLinecap="round" strokeLinejoin="round" />
</svg>
manifest
</a> </a>
</div> </div>
)} )}
@@ -134,48 +188,69 @@ export default function PackPanel({
</div> </div>
{packs.length > 0 && ( {packs.length > 0 && (
<div className="space-y-4"> <div className="space-y-5">
{packs.map(pack => ( {packs.map(pack => {
<div key={pack.id} className="space-y-2"> const accent = PACK_ACCENT[pack.kind];
<div className="flex items-center justify-between"> return (
<h3 className="text-xs font-semibold text-zinc-900">{PACK_LABELS[pack.kind]} · {pack.assets.length} </h3> <div key={pack.id} className="card-2 p-4 space-y-3">
<code className="text-[10px] text-zinc-400">{pack.id}</code> <div className="flex items-center justify-between">
</div> <div className="flex items-center gap-2">
<div className="grid grid-cols-5 gap-2"> <div className={`w-1.5 h-1.5 rounded-full ${accent.dot}`} />
{pack.assets.map(asset => ( <h3 className="text-xs font-semibold text-white">
<div key={asset.id} className="rounded-xl overflow-hidden border border-zinc-200 bg-zinc-50"> {PACK_LABELS[pack.kind]} <span className="text-white/40 font-normal">· {pack.assets.length} </span>
<div className="aspect-square bg-zinc-100"> </h3>
<img src={asset.url} alt={asset.title} className="w-full h-full object-cover" />
</div>
<div className="p-2">
<div className="text-[11px] font-medium text-zinc-800 truncate">{asset.title}</div>
<div className="text-[10px] text-zinc-400 truncate">{asset.view}</div>
</div>
</div> </div>
))} <code className="text-[10px] text-white/30 font-mono">{pack.id}</code>
</div>
<div className="grid grid-cols-5 gap-2">
{pack.assets.map(asset => (
<div key={asset.id} className="group relative rounded-xl overflow-hidden ring-1 ring-white/[0.08] bg-white/[0.02] hover:ring-white/20 transition-all">
<div className="aspect-square bg-white/[0.03] overflow-hidden">
<img src={asset.url} alt={asset.title} className="w-full h-full object-cover" />
</div>
<div className="p-2">
<div className="text-[11px] font-medium text-white/90 truncate">{asset.title}</div>
<div className="text-[10px] text-white/40 truncate">{asset.view}</div>
</div>
{asset.required && (
<span className="absolute top-1.5 right-1.5 px-1.5 py-0.5 rounded-md text-[9px] font-semibold text-violet-200 bg-violet-500/25 border border-violet-400/30 backdrop-blur">
</span>
)}
</div>
))}
</div>
</div> </div>
</div> );
))} })}
</div> </div>
)} )}
<div className="rounded-2xl border border-zinc-200 p-4 bg-white space-y-3"> <div className="card-2 p-4 space-y-3 ring-1 ring-fuchsia-400/20">
<div> <div className="absolute top-0 left-4 right-4 h-px bg-gradient-to-r from-fuchsia-400 to-violet-400 opacity-60" />
<div className="text-sm font-semibold text-zinc-900">Seedance </div> <div className="flex items-center justify-between">
<p className="text-[11px] text-zinc-500 mt-1"> <div>
Seedance/ <div className="flex items-center gap-2">
</p> <div className="w-1.5 h-1.5 rounded-full bg-fuchsia-400" />
<div className="text-sm font-semibold text-white">Seedance </div>
</div>
<p className="text-[11px] text-white/45 mt-1">
Seedance · /
</p>
</div>
<span className="chip chip-violet"></span>
</div> </div>
<div className="grid grid-cols-4 gap-2"> <div className="grid grid-cols-2 md:grid-cols-4 gap-2">
{VIDEO_TEMPLATES.map(template => ( {VIDEO_TEMPLATES.map(template => (
<button <button
key={template.id} key={template.id}
onClick={() => onGenerateVideo(primaryImage, template.promptTemplate)} onClick={() => onGenerateVideo(primaryImage, template.promptTemplate)}
disabled={videoLoading} disabled={videoLoading}
className="btn btn-outline text-[11px] px-2 py-2" className="btn btn-outline text-[11px] px-3 py-2.5 disabled:opacity-40 disabled:cursor-not-allowed flex-col h-auto items-start gap-0.5"
title={template.description} title={template.description}
> >
{videoLoading ? '提交中' : template.title} <span className="text-white/90 font-medium text-left w-full">{template.title}</span>
<span className="text-[9px] text-white/40 truncate w-full text-left normal-case font-normal">{template.description}</span>
</button> </button>
))} ))}
</div> </div>

View File

@@ -38,9 +38,14 @@ export default function PromptPanel({ onGenerate, loading }: PromptPanelProps) {
} }
return ( return (
<div className="card p-7 space-y-6"> <section className="card p-7 space-y-6">
<div className="flex items-center gap-2">
<span className="section-eyebrow">Step · 01 · Ideation</span>
<span className="text-[10px] text-white/30">· + + </span>
</div>
<div> <div>
<label className="block text-xs font-medium text-zinc-500 uppercase tracking-wider mb-2.5"> <label className="block text-[11px] font-medium text-white/55 uppercase tracking-[0.14em] mb-2.5">
Prompt Prompt
</label> </label>
<textarea <textarea
@@ -51,29 +56,29 @@ export default function PromptPanel({ onGenerate, loading }: PromptPanelProps) {
placeholder="描述要生成的玩具意向…" placeholder="描述要生成的玩具意向…"
className="field text-[15px] leading-relaxed" className="field text-[15px] leading-relaxed"
/> />
<p className="mt-2 text-[11px] text-zinc-400 flex items-center gap-1.5"> <p className="mt-2 text-[11px] text-white/35 flex items-center gap-1.5">
<kbd className="kbd"></kbd><kbd className="kbd"></kbd> <kbd className="kbd"></kbd><kbd className="kbd"></kbd>
</p> </p>
</div> </div>
<div> <div>
<label className="block text-xs font-medium text-zinc-500 uppercase tracking-wider mb-2.5"> <label className="block text-[11px] font-medium text-white/55 uppercase tracking-[0.14em] mb-2.5">
<span className="text-zinc-400 normal-case tracking-normal">· 4 </span> <span className="text-white/35 normal-case tracking-normal">· 4 </span>
</label> </label>
<div className="flex flex-wrap gap-2.5"> <div className="flex flex-wrap gap-2.5">
{refs.map((r, i) => ( {refs.map((r, i) => (
<div key={i} className="relative w-20 h-20 rounded-xl overflow-hidden ring-1 ring-zinc-200 group"> <div key={i} className="relative w-20 h-20 rounded-xl overflow-hidden ring-1 ring-white/[0.1] group">
<img src={r} alt="ref" className="w-full h-full object-cover" /> <img src={r} alt="ref" className="w-full h-full object-cover" />
<button <button
onClick={() => setRefs(prev => prev.filter((_, j) => j !== i))} onClick={() => setRefs(prev => prev.filter((_, j) => j !== i))}
className="absolute top-1 right-1 w-5 h-5 rounded-full bg-zinc-900 text-white text-[10px] opacity-0 group-hover:opacity-100 transition-opacity shadow-md" className="absolute top-1 right-1 w-5 h-5 rounded-full bg-black/80 text-white text-[10px] opacity-0 group-hover:opacity-100 transition-opacity shadow-md"
></button> ></button>
</div> </div>
))} ))}
{refs.length < 4 && ( {refs.length < 4 && (
<button <button
onClick={() => fileInput.current?.click()} onClick={() => fileInput.current?.click()}
className="w-20 h-20 rounded-xl border-2 border-dashed border-zinc-200 hover:border-zinc-400 hover:bg-zinc-50 text-zinc-400 hover:text-zinc-600 text-2xl transition-colors flex items-center justify-center" className="w-20 h-20 rounded-xl border-2 border-dashed border-white/15 hover:border-violet-400/50 hover:bg-white/[0.03] text-white/30 hover:text-violet-300 text-2xl transition-all flex items-center justify-center"
>+</button> >+</button>
)} )}
<input <input
@@ -88,7 +93,7 @@ export default function PromptPanel({ onGenerate, loading }: PromptPanelProps) {
</div> </div>
<div> <div>
<label className="block text-xs font-medium text-zinc-500 uppercase tracking-wider mb-2.5"> <label className="block text-[11px] font-medium text-white/55 uppercase tracking-[0.14em] mb-2.5">
</label> </label>
<div className="flex flex-wrap gap-1.5"> <div className="flex flex-wrap gap-1.5">
@@ -108,7 +113,7 @@ export default function PromptPanel({ onGenerate, loading }: PromptPanelProps) {
<div className="flex items-end justify-between gap-4 pt-2"> <div className="flex items-end justify-between gap-4 pt-2">
<div> <div>
<label className="block text-xs font-medium text-zinc-500 uppercase tracking-wider mb-2.5"> <label className="block text-[11px] font-medium text-white/55 uppercase tracking-[0.14em] mb-2.5">
</label> </label>
<div className="seg"> <div className="seg">
@@ -143,6 +148,6 @@ export default function PromptPanel({ onGenerate, loading }: PromptPanelProps) {
)} )}
</button> </button>
</div> </div>
</div> </section>
); );
} }

View File

@@ -32,20 +32,21 @@ export default function ResultGrid({ images, onAction }: ResultGridProps) {
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2 text-[11px] text-zinc-500"> <div className="flex items-center gap-2 text-[11px] text-white/55">
<kbd className="kbd">1</kbd> <kbd className="kbd">1</kbd>
<span></span> <span className="text-white/40"></span>
<kbd className="kbd">{Math.min(9, images.length)}</kbd> <kbd className="kbd">{Math.min(9, images.length)}</kbd>
<span></span> <span></span>
<span className="text-zinc-300 mx-1">/</span> <span className="text-white/20 mx-1">/</span>
<kbd className="kbd"></kbd> <kbd className="kbd"></kbd>
<span>+</span> <span className="text-white/40">+</span>
<kbd className="kbd">1</kbd> <kbd className="kbd">1</kbd>
<span></span> <span></span>
</div> </div>
<div className="text-xs text-zinc-600"> <div className="text-xs text-white/55">
<span className="font-semibold text-zinc-900">{selectedCount}</span>
<span className="text-zinc-400"> / {images.length}</span> <span className="ml-1 font-semibold bg-gradient-to-r from-violet-300 to-blue-300 bg-clip-text text-transparent">{selectedCount}</span>
<span className="text-white/30"> / {images.length}</span>
</div> </div>
</div> </div>
@@ -65,16 +66,16 @@ export default function ResultGrid({ images, onAction }: ResultGridProps) {
<div className="tile-badge tile-badge-rejected"></div> <div className="tile-badge tile-badge-rejected"></div>
)} )}
<div className="absolute inset-x-0 bottom-0 p-2 flex gap-2 opacity-0 group-hover:opacity-100 transition-opacity bg-gradient-to-t from-black/70 via-black/30 to-transparent"> <div className="absolute inset-x-0 bottom-0 p-2 flex gap-2 opacity-0 group-hover:opacity-100 transition-opacity bg-gradient-to-t from-black/80 via-black/40 to-transparent">
<button <button
onClick={() => onAction(img.id, img.status === 'selected' ? 'reset' : 'select')} onClick={() => onAction(img.id, img.status === 'selected' ? 'reset' : 'select')}
className="flex-1 px-3 py-1.5 rounded-lg bg-white text-zinc-900 text-xs font-medium hover:bg-zinc-100 transition-colors shadow-sm" className="flex-1 px-3 py-1.5 rounded-lg bg-gradient-to-r from-violet-500 to-blue-500 text-white text-xs font-semibold hover:brightness-110 transition shadow-glow-violet"
> >
{img.status === 'selected' ? '✓ 已选' : '选中'} {img.status === 'selected' ? '✓ 已选' : '选中'}
</button> </button>
<button <button
onClick={() => onAction(img.id, img.status === 'rejected' ? 'reset' : 'reject')} onClick={() => onAction(img.id, img.status === 'rejected' ? 'reset' : 'reject')}
className="px-3 py-1.5 rounded-lg bg-white/90 text-zinc-700 text-xs font-medium hover:bg-white hover:text-red-600 transition-colors shadow-sm" className="px-3 py-1.5 rounded-lg bg-white/[0.10] text-white/85 text-xs font-medium hover:bg-rose-500/60 hover:text-white transition-colors backdrop-blur"
></button> ></button>
</div> </div>
</div> </div>

View File

@@ -19,10 +19,10 @@ export default function Sidebar({
}) { }) {
if (!open) { if (!open) {
return ( return (
<aside className="w-12 shrink-0 border-r border-zinc-200 bg-white flex flex-col items-center py-4"> <aside className="w-12 shrink-0 border-r border-white/[0.06] bg-black/30 backdrop-blur-xl flex flex-col items-center py-4">
<button <button
onClick={onToggle} onClick={onToggle}
className="w-8 h-8 rounded-lg hover:bg-zinc-100 flex items-center justify-center text-zinc-500" className="w-8 h-8 rounded-lg hover:bg-white/[0.08] flex items-center justify-center text-white/60 transition-colors"
title="展开侧栏" title="展开侧栏"
> >
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
@@ -34,35 +34,47 @@ export default function Sidebar({
} }
return ( return (
<aside className="w-72 shrink-0 border-r border-zinc-200 bg-white flex flex-col"> <aside className="w-72 shrink-0 border-r border-white/[0.06] bg-black/30 backdrop-blur-xl flex flex-col">
<div className="p-4 flex items-center gap-2"> <div className="px-4 pt-5 pb-3 flex items-center gap-3">
<button <div className="w-8 h-8 rounded-xl bg-gradient-to-br from-violet-500 to-blue-500 flex items-center justify-center shadow-glow-violet">
onClick={onNew} <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2.5">
className="btn btn-primary flex-1 text-sm" <path d="M12 2l2.5 6.5L21 11l-6.5 2.5L12 20l-2.5-6.5L3 11l6.5-2.5z" strokeLinejoin="round" />
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<path d="M12 5v14M5 12h14" />
</svg> </svg>
</div>
</button> <div className="flex-1 min-w-0">
<div className="text-[13px] font-semibold tracking-tight">Toy Patent</div>
<div className="text-[10px] text-white/40">AI </div>
</div>
<button <button
onClick={onToggle} onClick={onToggle}
className="w-9 h-9 rounded-xl hover:bg-zinc-100 flex items-center justify-center text-zinc-500 transition-colors" className="w-8 h-8 rounded-lg hover:bg-white/[0.08] flex items-center justify-center text-white/60 transition-colors"
title="收起侧栏" title="收起侧栏"
> >
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M15 18l-6-6 6-6" /> <path d="M15 18l-6-6 6-6" />
</svg> </svg>
</button> </button>
</div> </div>
<div className="px-5 pt-1 pb-2 text-[11px] font-medium text-zinc-400 uppercase tracking-wider"> <div className="px-4 pb-4">
<button
onClick={onNew}
className="btn btn-primary w-full 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>
<div className="px-5 pt-1 pb-2 text-[10px] font-semibold text-white/35 uppercase tracking-[0.18em]">
</div> </div>
<div className="flex-1 overflow-y-auto px-3 pb-4 space-y-1"> <div className="flex-1 overflow-y-auto px-3 pb-4 space-y-1">
{sessions.length === 0 && ( {sessions.length === 0 && (
<div className="px-3 py-8 text-center text-xs text-zinc-400"> <div className="px-3 py-12 text-center text-xs text-white/30">
</div> </div>
)} )}
@@ -75,22 +87,22 @@ export default function Sidebar({
onClick={() => onPick(s.id)} onClick={() => onPick(s.id)}
className={`w-full text-left px-3 py-2.5 rounded-xl text-xs transition-all ${ className={`w-full text-left px-3 py-2.5 rounded-xl text-xs transition-all ${
active active
? 'bg-zinc-900 text-white shadow-sm' ? 'bg-gradient-to-r from-violet-500/30 via-indigo-500/20 to-blue-500/20 ring-1 ring-violet-400/40 text-white shadow-glow-violet'
: 'hover:bg-zinc-100 text-zinc-700' : 'hover:bg-white/[0.05] text-white/75 ring-1 ring-transparent hover:ring-white/[0.08]'
}`} }`}
> >
<div className={`line-clamp-2 font-medium ${active ? 'text-white' : 'text-zinc-800'}`}> <div className={`line-clamp-2 font-medium leading-relaxed ${active ? 'text-white' : 'text-white/85'}`}>
{s.prompt} {s.prompt}
</div> </div>
<div className={`mt-1.5 flex justify-between items-center text-[11px] ${active ? 'text-white/60' : 'text-zinc-500'}`}> <div className={`mt-1.5 flex justify-between items-center text-[10px] ${active ? 'text-white/65' : 'text-white/40'}`}>
<span> <span>
{new Date(s.createdAt).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })} {new Date(s.createdAt).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })}
</span> </span>
<span className="flex items-center gap-1"> <span className="flex items-center gap-1.5">
<span>{s.images.length}</span> <span>{s.images.length} </span>
{selectedCount > 0 && ( {selectedCount > 0 && (
<span className={active ? 'text-white' : 'text-zinc-900 font-semibold'}> <span className={active ? 'text-violet-200 font-semibold' : 'text-violet-300 font-semibold'}>
· {selectedCount} {selectedCount}
</span> </span>
)} )}
</span> </span>
@@ -99,6 +111,10 @@ export default function Sidebar({
); );
})} })}
</div> </div>
<div className="border-t border-white/[0.06] px-5 py-3 text-[10px] text-white/30">
4560 · / data
</div>
</aside> </aside>
); );
} }

View File

@@ -8,6 +8,19 @@ const config: Config = {
ink: '#0a0a0a', ink: '#0a0a0a',
paper: '#fafafa', paper: '#fafafa',
accent: '#ff6b35', accent: '#ff6b35',
noir: '#0A0A0F',
'noir-2': '#11111A',
'noir-3': '#171723',
},
backgroundImage: {
'accent-violet': 'linear-gradient(135deg, #8B5CF6 0%, #6366F1 50%, #3B82F6 100%)',
'accent-fuchsia': 'linear-gradient(135deg, #D946EF 0%, #8B5CF6 100%)',
'noir-radial': 'radial-gradient(circle at 20% 0%, rgba(139,92,246,0.18), transparent 60%), radial-gradient(circle at 90% 90%, rgba(59,130,246,0.12), transparent 50%)',
},
boxShadow: {
'glow-violet': '0 0 40px -8px rgba(139, 92, 246, 0.45)',
'glow-blue': '0 0 40px -8px rgba(59, 130, 246, 0.45)',
'card-noir': '0 1px 0 0 rgba(255,255,255,0.06) inset, 0 18px 60px -20px rgba(0,0,0,0.6)',
}, },
}, },
}, },