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",
"message": "Claude 会话活跃 · 最近命令claude · 分支 master · 1 项未提交变更 · 最近提交auto-save 2026-05-19 00:06 (+1, ~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;
html, body {
background: #FAFAFA;
color: #09090B;
font-feature-settings: "cv02", "cv03", "cv04", "cv11";
background: #0A0A0F;
color: #FFFFFF;
font-feature-settings: "cv02", "cv03", "cv04", "cv11", "ss01";
-webkit-font-smoothing: antialiased;
}
* { box-sizing: border-box; }
body {
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 {
@apply inline-flex items-center justify-center gap-1.5 px-4 py-2 rounded-xl text-sm font-medium transition-all;
}
.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 {
@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 {
@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 {
@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 {
@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 {
@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 {
@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 {
@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 {
@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 {
@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 {
@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 {
@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 {
@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 {
@apply opacity-30 grayscale;
}
.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;
}
.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 {
@apply bg-zinc-900 text-white;
background: linear-gradient(135deg, #8B5CF6, #3B82F6);
color: #fff;
}
.tile-badge-rejected {
@apply bg-red-500 text-white;
@apply bg-rose-500 text-white;
}
/* ===== Fields ===== */
input, textarea {
font-family: inherit;
}
.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 {
@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;
}
/* ===== 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 (
<div className="flex h-screen bg-[#FAFAFA]">
<div className="flex h-screen text-white">
<Sidebar
open={sidebarOpen}
onToggle={() => setSidebarOpen(v => !v)}
@@ -186,20 +186,22 @@ export default function Home() {
onNew={() => setCurrent(null)}
/>
<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">
<div>
<h1 className="text-[26px] font-semibold tracking-tight text-zinc-900 leading-tight">
AI
<span className="section-eyebrow">AI Toy Workflow</span>
<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>
<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>
</div>
<div className="flex items-center gap-2 pt-1">
<span className={provider === 'gpt' ? 'chip chip-live' : 'chip chip-mock'}>
<span className={`w-1.5 h-1.5 rounded-full ${provider === 'gpt' ? 'bg-emerald-500' : 'bg-amber-500'}`} />
{provider === 'gpt' ? 'GPT · 最高规格' : provider === 'mock' ? 'Mock · 占位图' : provider}
<div className="flex items-center gap-2 pt-1 shrink-0">
<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'}`} />
{provider === 'gpt' ? 'GPT · 最高规格' : provider === 'mock' ? 'Mock · 占位图' : provider === '?' ? '待连接' : provider}
</span>
</div>
</header>
@@ -207,15 +209,16 @@ export default function Home() {
<div className="space-y-8">
<PromptPanel onGenerate={handleGenerate} loading={loading} />
{current && (
<section className="space-y-4">
<section className="space-y-5">
<div className="flex items-end justify-between">
<div>
<h2 className="text-sm font-medium text-zinc-900"></h2>
<p className="text-xs text-zinc-500 mt-0.5">
<span className="section-eyebrow">Step · 02 · Quick Screen</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')}
</p>
</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>
<ResultGrid images={current.images} onAction={handleAction} />
<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';
const PACK_DESCRIPTIONS: Record<PackKind, string> = {
patent: '六面视图、45 立体图和局部放大,用于外观专利素材整理。',
patent: '六面视图、45° 立体图和局部放大,用于外观专利素材整理。',
production: '尺寸、材料、颜色、拆件和包装结构,用于工厂报价与打样沟通。',
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) {
return `/api/export/${sessionId}_${kind}_${version}_manifest.json`;
}
@@ -40,26 +61,35 @@ export default function PackPanel({
if (!primaryImage) {
return (
<section className="card p-5">
<h2 className="text-sm font-semibold text-zinc-900"></h2>
<p className="mt-1 text-xs text-zinc-500">
</p>
<section className="card p-6">
<div className="flex items-start gap-4">
<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>
<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>
);
}
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>
<h2 className="text-sm font-semibold text-zinc-900"></h2>
<p className="mt-1 text-xs text-zinc-500">
<span className="section-eyebrow">Step · 03 · Lock Character</span>
<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>
</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" />
<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>
@@ -67,64 +97,88 @@ export default function PackPanel({
<button
onClick={() => onLockCharacter(primaryImage)}
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
onClick={() => onGenerateAll(primaryImage)}
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>
</div>
{session.characterSpec && (
<div className="rounded-2xl bg-zinc-50 border border-zinc-200/70 p-4">
<div className="flex items-center justify-between gap-3">
<div className="card-2 p-4">
<div className="flex items-center justify-between gap-3 mb-3">
<div>
<div className="text-xs font-semibold text-zinc-900">{session.characterSpec.name}</div>
<div className="text-xs text-zinc-500 mt-1 line-clamp-2">{session.characterSpec.oneLiner}</div>
<div className="text-sm font-semibold text-white">{session.characterSpec.name}</div>
<div className="text-[11px] text-white/55 mt-1 line-clamp-2 max-w-[440px]">{session.characterSpec.oneLiner}</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 className="mt-3 grid grid-cols-2 gap-2 text-[11px] text-zinc-600">
<div>{session.characterSpec.speciesShape}</div>
<div>{session.characterSpec.bodyRatio}</div>
<div>{session.characterSpec.colorPalette.join('、')}</div>
<div>{session.characterSpec.materials.join('、')}</div>
<div className="divider-line mb-3" />
<div className="grid grid-cols-2 gap-x-4 gap-y-1.5 text-[11px]">
<div className="flex gap-2"><span className="text-white/40 w-12"></span><span className="text-white/85">{session.characterSpec.speciesShape}</span></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 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 className="grid grid-cols-3 gap-3">
<div className="grid grid-cols-3 gap-4">
{PACK_ORDER.map(kind => {
const pack = packs.find(item => item.kind === kind && item.sourceImageId === primaryImage.id);
const isLoading = loadingKind === kind;
const accent = PACK_ACCENT[kind];
return (
<div key={kind} className="rounded-2xl border border-zinc-200 p-4 bg-white space-y-3">
<div>
<div className="text-sm font-semibold text-zinc-900">{PACK_LABELS[kind]}</div>
<p className="text-[11px] text-zinc-500 mt-1 leading-relaxed">{PACK_DESCRIPTIONS[kind]}</p>
<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 className={`absolute top-0 left-4 right-4 h-px bg-gradient-to-r ${accent.bar} opacity-60`} />
<div className="flex items-center justify-between">
<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>
<p className="text-[11px] text-white/45 leading-relaxed">{PACK_DESCRIPTIONS[kind]}</p>
<button
onClick={() => onGenerate(primaryImage, kind)}
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>
{pack && (
<div className="space-y-2">
<div className="text-[11px] text-zinc-500">
<span className="font-semibold text-zinc-900">{pack.assets.length}</span> · {pack.version}
<div className="pt-1 space-y-1.5">
<div className="text-[10px] text-white/55">
<span className="font-semibold text-white">{pack.assets.length}</span>
</div>
<a
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>
</div>
)}
@@ -134,48 +188,69 @@ export default function PackPanel({
</div>
{packs.length > 0 && (
<div className="space-y-4">
{packs.map(pack => (
<div key={pack.id} className="space-y-2">
<div className="flex items-center justify-between">
<h3 className="text-xs font-semibold text-zinc-900">{PACK_LABELS[pack.kind]} · {pack.assets.length} </h3>
<code className="text-[10px] text-zinc-400">{pack.id}</code>
</div>
<div className="grid grid-cols-5 gap-2">
{pack.assets.map(asset => (
<div key={asset.id} className="rounded-xl overflow-hidden border border-zinc-200 bg-zinc-50">
<div className="aspect-square bg-zinc-100">
<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 className="space-y-5">
{packs.map(pack => {
const accent = PACK_ACCENT[pack.kind];
return (
<div key={pack.id} className="card-2 p-4 space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className={`w-1.5 h-1.5 rounded-full ${accent.dot}`} />
<h3 className="text-xs font-semibold text-white">
{PACK_LABELS[pack.kind]} <span className="text-white/40 font-normal">· {pack.assets.length} </span>
</h3>
</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 className="rounded-2xl border border-zinc-200 p-4 bg-white space-y-3">
<div>
<div className="text-sm font-semibold text-zinc-900">Seedance </div>
<p className="text-[11px] text-zinc-500 mt-1">
Seedance/
</p>
<div className="card-2 p-4 space-y-3 ring-1 ring-fuchsia-400/20">
<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="flex items-center justify-between">
<div>
<div className="flex items-center gap-2">
<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 className="grid grid-cols-4 gap-2">
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
{VIDEO_TEMPLATES.map(template => (
<button
key={template.id}
onClick={() => onGenerateVideo(primaryImage, template.promptTemplate)}
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}
>
{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>
))}
</div>

View File

@@ -38,9 +38,14 @@ export default function PromptPanel({ onGenerate, loading }: PromptPanelProps) {
}
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>
<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
</label>
<textarea
@@ -51,29 +56,29 @@ export default function PromptPanel({ onGenerate, loading }: PromptPanelProps) {
placeholder="描述要生成的玩具意向…"
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>
</p>
</div>
<div>
<label className="block text-xs font-medium text-zinc-500 uppercase tracking-wider mb-2.5">
<span className="text-zinc-400 normal-case tracking-normal">· 4 </span>
<label className="block text-[11px] font-medium text-white/55 uppercase tracking-[0.14em] mb-2.5">
<span className="text-white/35 normal-case tracking-normal">· 4 </span>
</label>
<div className="flex flex-wrap gap-2.5">
{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" />
<button
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>
</div>
))}
{refs.length < 4 && (
<button
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>
)}
<input
@@ -88,7 +93,7 @@ export default function PromptPanel({ onGenerate, loading }: PromptPanelProps) {
</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>
<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>
<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>
<div className="seg">
@@ -143,6 +148,6 @@ export default function PromptPanel({ onGenerate, loading }: PromptPanelProps) {
)}
</button>
</div>
</div>
</section>
);
}

View File

@@ -32,20 +32,21 @@ export default function ResultGrid({ images, onAction }: ResultGridProps) {
return (
<div className="space-y-4">
<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>
<span></span>
<span className="text-white/40"></span>
<kbd className="kbd">{Math.min(9, images.length)}</kbd>
<span></span>
<span className="text-zinc-300 mx-1">/</span>
<span className="text-white/20 mx-1">/</span>
<kbd className="kbd"></kbd>
<span>+</span>
<span className="text-white/40">+</span>
<kbd className="kbd">1</kbd>
<span></span>
</div>
<div className="text-xs text-zinc-600">
<span className="font-semibold text-zinc-900">{selectedCount}</span>
<span className="text-zinc-400"> / {images.length}</span>
<div className="text-xs text-white/55">
<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>
@@ -65,16 +66,16 @@ export default function ResultGrid({ images, onAction }: ResultGridProps) {
<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
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' ? '✓ 已选' : '选中'}
</button>
<button
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>
</div>
</div>

View File

@@ -19,10 +19,10 @@ export default function Sidebar({
}) {
if (!open) {
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
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="展开侧栏"
>
<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 (
<aside className="w-72 shrink-0 border-r border-zinc-200 bg-white flex flex-col">
<div className="p-4 flex items-center gap-2">
<button
onClick={onNew}
className="btn btn-primary flex-1 text-sm"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<path d="M12 5v14M5 12h14" />
<aside className="w-72 shrink-0 border-r border-white/[0.06] bg-black/30 backdrop-blur-xl flex flex-col">
<div className="px-4 pt-5 pb-3 flex items-center gap-3">
<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">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" 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>
</button>
</div>
<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
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="收起侧栏"
>
<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" />
</svg>
</button>
</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 className="flex-1 overflow-y-auto px-3 pb-4 space-y-1">
{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>
)}
@@ -75,22 +87,22 @@ export default function Sidebar({
onClick={() => onPick(s.id)}
className={`w-full text-left px-3 py-2.5 rounded-xl text-xs transition-all ${
active
? 'bg-zinc-900 text-white shadow-sm'
: 'hover:bg-zinc-100 text-zinc-700'
? '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-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}
</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>
{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">
<span>{s.images.length}</span>
<span className="flex items-center gap-1.5">
<span>{s.images.length} </span>
{selectedCount > 0 && (
<span className={active ? 'text-white' : 'text-zinc-900 font-semibold'}>
· {selectedCount}
<span className={active ? 'text-violet-200 font-semibold' : 'text-violet-300 font-semibold'}>
{selectedCount}
</span>
)}
</span>
@@ -99,6 +111,10 @@ export default function Sidebar({
);
})}
</div>
<div className="border-t border-white/[0.06] px-5 py-3 text-[10px] text-white/30">
4560 · / data
</div>
</aside>
);
}

View File

@@ -8,6 +8,19 @@ const config: Config = {
ink: '#0a0a0a',
paper: '#fafafa',
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)',
},
},
},