Compare commits
2 Commits
7b63ade1b2
...
27c8ce3819
| Author | SHA1 | Date | |
|---|---|---|---|
| 27c8ce3819 | |||
| 7fcda19ba2 |
@@ -1388,6 +1388,107 @@
|
||||
"message": "fix: enlarge result thumbnails",
|
||||
"hash": "a13bd05",
|
||||
"files_changed": 1
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-19T21:05:38+08:00",
|
||||
"type": "commit",
|
||||
"message": "chore: record enlarged thumbnail workflow",
|
||||
"hash": "7b63ade",
|
||||
"files_changed": 1
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-19T13:10:07Z",
|
||||
"type": "session-heartbeat",
|
||||
"message": "Codex 会话活跃 · 最近命令:codex · 分支 master · 1 项未提交变更 · 最近提交:chore: record enlarged thumbnail workflow",
|
||||
"files_changed": 1
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-19T13:20:07Z",
|
||||
"type": "session-heartbeat",
|
||||
"message": "Codex 会话活跃 · 最近命令:codex · 分支 master · 1 项未提交变更 · 最近提交:chore: record enlarged thumbnail workflow",
|
||||
"files_changed": 1
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-19T13:30:08Z",
|
||||
"type": "session-heartbeat",
|
||||
"message": "Codex 会话活跃 · 最近命令:codex · 分支 master · 1 项未提交变更 · 最近提交:chore: record enlarged thumbnail workflow",
|
||||
"files_changed": 1
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-19T13:40:08Z",
|
||||
"type": "session-heartbeat",
|
||||
"message": "Codex 会话活跃 · 最近命令:codex · 分支 master · 1 项未提交变更 · 最近提交:chore: record enlarged thumbnail workflow",
|
||||
"files_changed": 1
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-19T13:50:08Z",
|
||||
"type": "session-heartbeat",
|
||||
"message": "Codex 会话活跃 · 最近命令:codex · 分支 master · 1 项未提交变更 · 最近提交:chore: record enlarged thumbnail workflow",
|
||||
"files_changed": 1
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-19T14:00:08Z",
|
||||
"type": "session-heartbeat",
|
||||
"message": "Codex 会话活跃 · 最近命令:codex · 分支 master · 1 项未提交变更 · 最近提交:chore: record enlarged thumbnail workflow",
|
||||
"files_changed": 1
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-19T15:40:29Z",
|
||||
"type": "session-heartbeat",
|
||||
"message": "Codex 会话活跃 · 最近命令:codex · 分支 master · 1 项未提交变更 · 最近提交:chore: record enlarged thumbnail workflow",
|
||||
"files_changed": 1
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-19T15:50:29Z",
|
||||
"type": "session-heartbeat",
|
||||
"message": "Codex 会话活跃 · 最近命令:codex · 分支 master · 1 项未提交变更 · 最近提交:chore: record enlarged thumbnail workflow",
|
||||
"files_changed": 1
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-19T16:00:29Z",
|
||||
"type": "session-heartbeat",
|
||||
"message": "Codex 会话活跃 · 最近命令:codex · 分支 master · 1 项未提交变更 · 最近提交:chore: record enlarged thumbnail workflow",
|
||||
"files_changed": 1
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-19T16:02:50Z",
|
||||
"type": "session-end",
|
||||
"message": "Codex 会话结束 · 持续 0 秒 · 最近命令:codex · 分支 master · 1 项未提交变更 · 最近提交:chore: record enlarged thumbnail workflow",
|
||||
"files_changed": 1
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-19T16:02:50Z",
|
||||
"type": "session-end",
|
||||
"message": "Codex 会话结束 · 持续 0 秒 · 最近命令:codex · 分支 master · 1 项未提交变更 · 最近提交:chore: record enlarged thumbnail workflow",
|
||||
"files_changed": 1
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-20T00:24:31+08:00",
|
||||
"type": "commit",
|
||||
"message": "auto-save 2026-05-20 00:24 (~5)",
|
||||
"hash": "e72ba50",
|
||||
"files_changed": 5
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-20T00:29:58+08:00",
|
||||
"type": "commit",
|
||||
"message": "auto-save 2026-05-20 00:29 (~4)",
|
||||
"hash": "92df778",
|
||||
"files_changed": 4
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-20T00:51:41+08:00",
|
||||
"type": "commit",
|
||||
"message": "auto-save 2026-05-20 00:51 (~8)",
|
||||
"hash": "3547987",
|
||||
"files_changed": 8
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-20T00:52:31+08:00",
|
||||
"type": "commit",
|
||||
"message": "fix: loosen glass dashboard workspace",
|
||||
"hash": "7fcda19",
|
||||
"files_changed": 8
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
@tailwind utilities;
|
||||
|
||||
html, body {
|
||||
background: #071108;
|
||||
background: #0f0f10;
|
||||
color: #FFFFFF;
|
||||
font-feature-settings: "cv02", "cv03", "cv04", "cv11", "ss01";
|
||||
-webkit-font-smoothing: antialiased;
|
||||
@@ -12,13 +12,17 @@ html, body {
|
||||
|
||||
body {
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, "SF Pro Text", "PingFang SC", "Noto Sans SC", "Helvetica Neue", sans-serif;
|
||||
background-color: #071108;
|
||||
background-image: linear-gradient(180deg, rgba(22, 42, 16, 0.92), rgba(5, 12, 4, 1));
|
||||
background-color: #0f0f10;
|
||||
background-image:
|
||||
radial-gradient(circle at 18% 18%, rgba(110, 99, 200, 0.16), transparent 28%),
|
||||
radial-gradient(circle at 78% 28%, rgba(73, 199, 182, 0.14), transparent 28%),
|
||||
radial-gradient(circle at 48% 76%, rgba(185, 194, 42, 0.14), transparent 34%),
|
||||
linear-gradient(180deg, #171717 0%, #0f0f10 48%, #080809 100%);
|
||||
background-attachment: fixed;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
::selection { background: rgba(230, 245, 120, 0.28); color: #fff; }
|
||||
::selection { background: rgba(185, 194, 42, 0.28); color: #fff; }
|
||||
|
||||
.login-oasis-loop {
|
||||
position: fixed;
|
||||
@@ -124,13 +128,13 @@ body {
|
||||
|
||||
.app-oasis .login-oasis-canvas {
|
||||
z-index: 0;
|
||||
opacity: 0.74;
|
||||
filter: saturate(0.92) contrast(1.04);
|
||||
opacity: 0.18;
|
||||
filter: saturate(0.72) contrast(0.92) grayscale(0.34);
|
||||
}
|
||||
|
||||
.app-oasis .login-oasis-loop {
|
||||
opacity: 1;
|
||||
filter: saturate(1.08) brightness(1.2);
|
||||
opacity: 0.2;
|
||||
filter: saturate(0.72) brightness(0.72);
|
||||
}
|
||||
|
||||
.app-oasis-shade {
|
||||
@@ -139,10 +143,13 @@ body {
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
background:
|
||||
linear-gradient(90deg, rgba(0, 0, 0, 0.42), rgba(9, 22, 7, 0.14) 42%, rgba(0, 0, 0, 0.42)),
|
||||
linear-gradient(180deg, rgba(0, 0, 0, 0.28), rgba(0, 0, 0, 0.02) 36%, rgba(0, 0, 0, 0.54)),
|
||||
linear-gradient(90deg, rgba(255, 255, 255, 0.026) 1px, transparent 1px),
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.022) 1px, transparent 1px);
|
||||
radial-gradient(circle at 21% 24%, rgba(110, 99, 200, 0.18), transparent 30%),
|
||||
radial-gradient(circle at 84% 28%, rgba(73, 199, 182, 0.15), transparent 28%),
|
||||
radial-gradient(circle at 58% 84%, rgba(185, 194, 42, 0.12), transparent 34%),
|
||||
linear-gradient(90deg, rgba(0, 0, 0, 0.52), rgba(20, 20, 20, 0.26) 44%, rgba(0, 0, 0, 0.46)),
|
||||
linear-gradient(180deg, rgba(0, 0, 0, 0.20), rgba(0, 0, 0, 0.02) 36%, rgba(0, 0, 0, 0.58)),
|
||||
linear-gradient(90deg, rgba(255, 255, 255, 0.022) 1px, transparent 1px),
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.018) 1px, transparent 1px);
|
||||
background-size: auto, auto, 64px 64px, 64px 64px;
|
||||
}
|
||||
|
||||
@@ -154,7 +161,7 @@ body {
|
||||
z-index: 2;
|
||||
height: 34vh;
|
||||
pointer-events: none;
|
||||
opacity: 0.46;
|
||||
opacity: 0.08;
|
||||
background:
|
||||
linear-gradient(180deg, transparent, rgba(10, 25, 7, 0.62) 60%, rgba(0, 0, 0, 0.82)),
|
||||
repeating-linear-gradient(103deg, transparent 0 13px, rgba(204, 230, 120, 0.18) 13px 14px, transparent 14px 28px),
|
||||
@@ -168,7 +175,7 @@ body {
|
||||
}
|
||||
.btn-primary {
|
||||
@apply active:scale-[0.98];
|
||||
background: linear-gradient(135deg, rgba(230, 245, 120, 0.96), rgba(214, 179, 106, 0.94));
|
||||
background: linear-gradient(135deg, rgba(217, 223, 42, 0.96), rgba(214, 179, 106, 0.94));
|
||||
color: #081006;
|
||||
box-shadow: 0 6px 24px -8px rgba(99, 102, 241, 0.6), inset 0 1px 0 rgba(255,255,255,0.18);
|
||||
}
|
||||
@@ -177,43 +184,43 @@ body {
|
||||
}
|
||||
.btn-ghost {
|
||||
@apply text-white/80 active:scale-[0.98];
|
||||
border: 1px solid rgba(140, 180, 120, 0.14);
|
||||
background: rgba(10, 18, 10, 0.34);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
background: rgba(255, 255, 255, 0.055);
|
||||
}
|
||||
.btn-outline {
|
||||
@apply text-white/80 active:scale-[0.98];
|
||||
border: 1px solid rgba(140, 180, 120, 0.16);
|
||||
background: rgba(10, 18, 10, 0.28);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
.btn-glass {
|
||||
@apply text-white backdrop-blur-xl active:scale-[0.98];
|
||||
border: 1px solid rgba(140, 180, 120, 0.18);
|
||||
background: rgba(10, 18, 10, 0.42);
|
||||
border: 1px solid rgba(255, 255, 255, 0.13);
|
||||
background: rgba(255, 255, 255, 0.07);
|
||||
}
|
||||
|
||||
/* ===== Cards (glass) ===== */
|
||||
.card {
|
||||
@apply relative rounded-[8px] backdrop-blur-xl;
|
||||
border: 1px solid rgba(140, 180, 120, 0.14);
|
||||
border: 1px solid rgba(255, 255, 255, 0.10);
|
||||
background:
|
||||
linear-gradient(180deg, rgba(17, 28, 13, 0.62), rgba(5, 10, 5, 0.54)),
|
||||
rgba(10, 18, 10, 0.42);
|
||||
linear-gradient(135deg, rgba(255, 255, 255, 0.085), rgba(255, 255, 255, 0.035)),
|
||||
rgba(42, 42, 42, 0.58);
|
||||
box-shadow:
|
||||
0 1px 0 0 rgba(255,255,255,0.08) inset,
|
||||
0 22px 70px -30px rgba(0,0,0,0.82),
|
||||
0 0 42px -34px rgba(230, 245, 120, 0.64);
|
||||
0 0 42px -34px rgba(73, 199, 182, 0.64);
|
||||
}
|
||||
.card-2 {
|
||||
@apply rounded-[8px] backdrop-blur-xl;
|
||||
border: 1px solid rgba(140, 180, 120, 0.12);
|
||||
background: rgba(8, 16, 8, 0.42);
|
||||
border: 1px solid rgba(255, 255, 255, 0.10);
|
||||
background: rgba(255, 255, 255, 0.055);
|
||||
}
|
||||
.card-hover {
|
||||
@apply transition-all;
|
||||
}
|
||||
.card-hover:hover,
|
||||
.card:hover {
|
||||
border-color: rgba(140, 180, 120, 0.28);
|
||||
border-color: rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
|
||||
/* ===== Chips ===== */
|
||||
@@ -230,8 +237,8 @@ body {
|
||||
}
|
||||
.chip-neutral {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
border-color: rgba(140, 180, 120, 0.14);
|
||||
background: rgba(10, 18, 10, 0.34);
|
||||
border-color: rgba(255, 255, 255, 0.11);
|
||||
background: rgba(255, 255, 255, 0.055);
|
||||
}
|
||||
.chip-violet {
|
||||
border-color: rgba(230, 245, 120, 0.28);
|
||||
@@ -242,8 +249,8 @@ body {
|
||||
/* ===== Segmented ===== */
|
||||
.seg {
|
||||
@apply inline-flex p-1 rounded-[8px] gap-1;
|
||||
border: 1px solid rgba(140, 180, 120, 0.13);
|
||||
background: rgba(10, 18, 10, 0.36);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
background: rgba(255, 255, 255, 0.055);
|
||||
backdrop-filter: blur(16px);
|
||||
}
|
||||
.seg-item {
|
||||
@@ -258,10 +265,12 @@ body {
|
||||
/* ===== Tiles ===== */
|
||||
.tile {
|
||||
@apply relative aspect-square overflow-hidden rounded-[8px] transition-all cursor-pointer;
|
||||
background: rgba(10, 18, 10, 0.44);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(255,255,255,0.08), rgba(255,255,255,0.025)),
|
||||
rgba(42, 42, 42, 0.62);
|
||||
box-shadow: 0 1px 0 rgba(255,255,255,0.08) inset, 0 18px 50px -28px rgba(0,0,0,0.8);
|
||||
--tw-ring-color: rgba(140, 180, 120, 0.14);
|
||||
border: 1px solid rgba(140, 180, 120, 0.1);
|
||||
--tw-ring-color: rgba(255, 255, 255, 0.14);
|
||||
border: 1px solid rgba(255, 255, 255, 0.10);
|
||||
}
|
||||
.tile-selected {
|
||||
position: relative;
|
||||
@@ -294,8 +303,8 @@ input, textarea {
|
||||
}
|
||||
.field {
|
||||
@apply w-full rounded-[8px] px-3.5 py-3 text-sm text-white placeholder:text-white/30 outline-none resize-none transition-colors;
|
||||
background: rgba(10, 18, 10, 0.42);
|
||||
border: 1px solid rgba(140, 180, 120, 0.14);
|
||||
background: rgba(18, 18, 18, 0.56);
|
||||
border: 1px solid rgba(255, 255, 255, 0.11);
|
||||
box-shadow: inset 0 1px 0 rgba(255,255,255,0.05);
|
||||
}
|
||||
.field:focus {
|
||||
@@ -306,23 +315,23 @@ input, textarea {
|
||||
/* ===== KBD ===== */
|
||||
.kbd {
|
||||
@apply inline-flex items-center justify-center min-w-[20px] h-5 px-1.5 rounded-[6px] text-[10px] font-medium text-white/70;
|
||||
border: 1px solid rgba(140, 180, 120, 0.16);
|
||||
background: rgba(10, 18, 10, 0.46);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
background: rgba(255, 255, 255, 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];
|
||||
color: rgba(230, 245, 120, 0.82);
|
||||
text-shadow: 0 0 22px rgba(230, 245, 120, 0.16);
|
||||
color: rgba(217, 223, 42, 0.82);
|
||||
text-shadow: 0 0 22px rgba(217, 223, 42, 0.16);
|
||||
}
|
||||
|
||||
.glass-sidebar {
|
||||
border-right: 1px solid rgba(140, 180, 120, 0.14);
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.10);
|
||||
background:
|
||||
linear-gradient(180deg, rgba(8, 16, 7, 0.72), rgba(2, 5, 2, 0.62)),
|
||||
rgba(10, 18, 10, 0.42);
|
||||
linear-gradient(180deg, rgba(46, 46, 46, 0.78), rgba(22, 22, 22, 0.70)),
|
||||
rgba(42, 42, 42, 0.54);
|
||||
box-shadow:
|
||||
inset -1px 0 0 rgba(255,255,255,0.04),
|
||||
16px 0 70px -44px rgba(0,0,0,0.95);
|
||||
@@ -330,7 +339,33 @@ input, textarea {
|
||||
}
|
||||
|
||||
.app-oasis main {
|
||||
background: linear-gradient(90deg, rgba(0,0,0,0.18), rgba(10,18,10,0.08));
|
||||
background: linear-gradient(90deg, rgba(0,0,0,0.16), rgba(42,42,42,0.08));
|
||||
}
|
||||
|
||||
.dashboard-workbench {
|
||||
border: 1px solid rgba(255, 255, 255, 0.10);
|
||||
border-radius: 8px;
|
||||
background:
|
||||
linear-gradient(135deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.025)),
|
||||
rgba(32, 32, 32, 0.48);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.07),
|
||||
0 24px 90px -46px rgba(0, 0, 0, 0.92);
|
||||
backdrop-filter: blur(18px);
|
||||
}
|
||||
|
||||
.result-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr));
|
||||
gap: 16px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
@media (min-width: 1440px) {
|
||||
.result-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Login (cloned from SKG source) ===== */
|
||||
|
||||
@@ -291,8 +291,8 @@ export default function Home() {
|
||||
onPick={id => setCurrent(sessions.find(s => s.id === id) ?? null)}
|
||||
onNew={() => setCurrent(null)}
|
||||
/>
|
||||
<main className={`flex-1 overflow-y-auto transition-[padding] duration-200 ${current && sidebarOpen ? 'pl-[340px]' : ''}`}>
|
||||
<div className="mx-auto max-w-[1240px] px-10 py-8">
|
||||
<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">
|
||||
@@ -353,7 +353,7 @@ export default function Home() {
|
||||
/>
|
||||
)}
|
||||
{current && (
|
||||
<section className="space-y-5">
|
||||
<section className="dashboard-workbench space-y-5 p-5">
|
||||
<div className="flex items-end justify-between gap-4">
|
||||
<div>
|
||||
<span className="section-eyebrow">Step · 02 · Quick Screen</span>
|
||||
|
||||
@@ -21,7 +21,8 @@ function nextPreviewState(event: PointerEvent<HTMLElement>, aspectRatio?: string
|
||||
const gap = 18;
|
||||
const margin = 12;
|
||||
const ratio = parseRatio(aspectRatio);
|
||||
const width = Math.min(620, Math.max(280, window.innerWidth * 0.42));
|
||||
const maxWidth = ratio < 0.8 ? 380 : ratio > 1.35 ? 620 : 500;
|
||||
const width = Math.min(maxWidth, Math.max(260, window.innerWidth * 0.38));
|
||||
const height = Math.min(window.innerHeight * 0.82, width / ratio);
|
||||
let left = event.clientX + gap;
|
||||
let top = event.clientY + gap;
|
||||
@@ -69,7 +70,7 @@ export function HoverImagePreview({
|
||||
/>
|
||||
{preview && (
|
||||
<div
|
||||
className="pointer-events-none fixed z-[90] rounded-[8px] bg-white p-2 shadow-2xl ring-1 ring-white/20"
|
||||
className="pointer-events-none fixed z-[90] rounded-[8px] bg-white p-2 shadow-[0_24px_80px_-24px_rgba(0,0,0,0.86)] ring-1 ring-white/20"
|
||||
style={{ left: preview.left, top: preview.top, width: preview.width }}
|
||||
>
|
||||
<img src={src} alt="" className="max-h-[82vh] w-full object-contain" style={{ aspectRatio: parseRatio(aspectRatio) }} />
|
||||
|
||||
@@ -60,9 +60,12 @@ function AssetRow({ template, asset, accent, onRegenerate }: {
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div className="grid grid-cols-[76px_1fr_auto] gap-3 p-3 rounded-[8px] bg-white/[0.025] ring-1 ring-white/[0.05] hover:ring-white/[0.1] transition-all">
|
||||
<div className="grid grid-cols-[96px_minmax(0,1fr)_minmax(132px,auto)] gap-3 p-3 rounded-[8px] bg-white/[0.035] ring-1 ring-white/[0.06] hover:bg-white/[0.05] hover:ring-white/[0.12] transition-all">
|
||||
{/* thumbnail */}
|
||||
<div className="group/thumb relative w-[72px] h-[72px] rounded-[8px] overflow-visible bg-white/[0.04] ring-1 ring-white/[0.07] flex items-center justify-center">
|
||||
<div
|
||||
className="relative flex w-[92px] max-h-[110px] items-center justify-center overflow-visible rounded-[8px] bg-black/30 ring-1 ring-white/[0.08]"
|
||||
style={{ aspectRatio: aspectCss(asset?.aspectRatio ?? template.aspectRatio) }}
|
||||
>
|
||||
{ready ? (
|
||||
<HoverImagePreview
|
||||
src={asset!.url}
|
||||
@@ -81,7 +84,7 @@ function AssetRow({ template, asset, accent, onRegenerate }: {
|
||||
</div>
|
||||
|
||||
{/* info */}
|
||||
<div className="min-w-0 space-y-1">
|
||||
<div className="min-w-0 space-y-1.5">
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
<span className="text-[13px] font-medium text-white leading-snug">{template.title}</span>
|
||||
{template.required && !ready && (
|
||||
@@ -109,45 +112,43 @@ function AssetRow({ template, asset, accent, onRegenerate }: {
|
||||
anchor: {asset.anchorAssetId ?? asset.anchorImageUrl}
|
||||
</div>
|
||||
)}
|
||||
{ready && onRegenerate && (
|
||||
<div className="mt-2 pt-2 border-t border-amber-400/10 flex items-center justify-between gap-3">
|
||||
<span className="text-[10px] text-white/35">重新生成</span>
|
||||
<button
|
||||
onClick={() => setShowRedo(value => !value)}
|
||||
className="text-[10px] text-white/55 hover:text-white rounded-lg px-2 py-1 ring-1 ring-white/[0.08] bg-white/[0.04]"
|
||||
>
|
||||
{showRedo ? '收起' : '调整后生成'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{showRedo && ready && (
|
||||
<div className="flex items-center gap-2 pt-1">
|
||||
<input
|
||||
value={refinement}
|
||||
onChange={event => setRefinement(event.target.value)}
|
||||
placeholder="重做要求"
|
||||
className="min-w-0 flex-1 rounded-lg bg-black/30 ring-1 ring-white/[0.08] px-2 py-1 text-[11px] text-white/80 outline-none focus:ring-[#e6f578]/40"
|
||||
/>
|
||||
<button
|
||||
onClick={handleRedo}
|
||||
disabled={regenerating}
|
||||
className="rounded-lg bg-white/[0.12] hover:bg-white/[0.18] text-white text-[10px] px-2 py-1 disabled:opacity-40"
|
||||
>
|
||||
{regenerating ? '...' : '确认生成'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* meta */}
|
||||
<div className="flex flex-col items-end justify-between text-right shrink-0">
|
||||
<div className="flex shrink-0 flex-col items-end justify-between gap-3 text-right">
|
||||
<span className="text-[10px] font-mono text-white/40">{ASPECT_PX[template.aspectRatio]}</span>
|
||||
<div className="flex flex-col items-end gap-1">
|
||||
<span className={`text-[10px] ${ready ? 'text-[#dff5a8]' : 'text-white/25'}`}>
|
||||
{ready ? `L${asset!.derivationLevel}` : '待生成'}
|
||||
</span>
|
||||
{ready && onRegenerate && (
|
||||
<button
|
||||
onClick={() => setShowRedo(value => !value)}
|
||||
className="mt-1 rounded-[8px] bg-white/[0.055] px-2.5 py-1.5 text-[10px] text-white/60 ring-1 ring-white/[0.08] transition-colors hover:bg-white/[0.10] hover:text-white"
|
||||
>
|
||||
{showRedo ? '收起重做' : '重做'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showRedo && ready && (
|
||||
<div className="col-start-2 col-span-2 flex items-center gap-2 border-t border-amber-400/10 pt-2">
|
||||
<input
|
||||
value={refinement}
|
||||
onChange={event => setRefinement(event.target.value)}
|
||||
placeholder="重做要求"
|
||||
className="min-w-0 flex-1 rounded-[8px] bg-black/30 ring-1 ring-white/[0.08] px-2.5 py-2 text-[11px] text-white/80 outline-none focus:ring-[#e6f578]/40"
|
||||
/>
|
||||
<button
|
||||
onClick={handleRedo}
|
||||
disabled={regenerating}
|
||||
className="rounded-[8px] bg-[#d6b36a]/18 px-3 py-2 text-[10px] text-[#f2d38c] ring-1 ring-[#d6b36a]/24 transition-colors hover:bg-[#d6b36a]/26 disabled:opacity-40"
|
||||
>
|
||||
{regenerating ? '...' : '确认重做'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -366,7 +367,9 @@ function VideoSection({ videoLoading, primaryImage, onGenerateVideo }: {
|
||||
return (
|
||||
<div key={template.id} className="grid grid-cols-[72px_1fr_auto] gap-3 p-3 rounded-[8px] bg-white/[0.025] ring-1 ring-white/[0.05] hover:ring-white/[0.1] transition-all">
|
||||
<div className="aspect-square rounded-[8px] bg-gradient-to-br from-[#e6f578]/15 to-[#8cb478]/15 ring-1 ring-[#e6f578]/20 flex flex-col items-center justify-center text-[#e6f578] text-[9px] font-mono gap-1">
|
||||
<span>▶</span>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
<path d="M8 5.2v13.6L18.5 12 8 5.2z" />
|
||||
</svg>
|
||||
<span className="text-[8px]">{template.duration}s</span>
|
||||
</div>
|
||||
<div className="min-w-0 space-y-1">
|
||||
|
||||
@@ -17,9 +17,9 @@ function cssRatioToNumber(ratio: string | undefined) {
|
||||
|
||||
function thumbnailWidth(ratio: string | undefined) {
|
||||
const value = cssRatioToNumber(ratio);
|
||||
if (value > 1.35) return 'min(100%, 660px)';
|
||||
if (value < 0.8) return 'min(100%, 276px)';
|
||||
return 'min(100%, 468px)';
|
||||
if (value > 1.35) return 'min(100%, 560px)';
|
||||
if (value < 0.8) return 'min(100%, 260px)';
|
||||
return 'min(100%, 390px)';
|
||||
}
|
||||
|
||||
export default function ResultGrid({ images, onAction }: ResultGridProps) {
|
||||
@@ -42,12 +42,11 @@ export default function ResultGrid({ images, onAction }: ResultGridProps) {
|
||||
return () => window.removeEventListener('keydown', handler);
|
||||
}, [images, onAction]);
|
||||
|
||||
const cols = images.length === 1 ? 'grid-cols-[minmax(220px,520px)]' : images.length <= 4 ? 'grid-cols-2' : images.length <= 9 ? 'grid-cols-3' : 'grid-cols-4';
|
||||
const selectedCount = images.filter(i => i.status === 'selected').length;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-2 text-[11px] text-white/55">
|
||||
<kbd className="kbd">1</kbd>
|
||||
<span className="text-white/40">–</span>
|
||||
@@ -66,11 +65,11 @@ export default function ResultGrid({ images, onAction }: ResultGridProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`grid ${cols} items-start gap-3`}>
|
||||
<div className="result-grid">
|
||||
{images.map((img, i) => (
|
||||
<div
|
||||
key={img.id}
|
||||
className={`tile group ${img.status === 'selected' ? 'tile-selected' : ''} ${img.status === 'rejected' ? 'tile-rejected' : ''}`}
|
||||
className={`tile group justify-self-center ${img.status === 'selected' ? 'tile-selected' : ''} ${img.status === 'rejected' ? 'tile-rejected' : ''}`}
|
||||
style={{ aspectRatio: ratios[img.id] ?? '1 / 1', width: thumbnailWidth(ratios[img.id]), maxWidth: '100%' }}
|
||||
>
|
||||
<HoverImagePreview
|
||||
@@ -102,8 +101,13 @@ export default function ResultGrid({ images, onAction }: ResultGridProps) {
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onAction(img.id, img.status === 'rejected' ? 'reset' : 'reject')}
|
||||
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>
|
||||
className="flex h-8 w-10 items-center justify-center rounded-[8px] bg-white/[0.10] text-white/85 transition-colors hover:bg-rose-500/60 hover:text-white backdrop-blur"
|
||||
title={img.status === 'rejected' ? '取消打叉' : '打叉'}
|
||||
>
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.6">
|
||||
<path d="M6 6l12 12M18 6L6 18" strokeLinecap="round" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import type { GenSession } from '@/lib/types';
|
||||
|
||||
function modeLabel(mode?: GenSession['inputMode']) {
|
||||
@@ -25,9 +24,9 @@ function ActiveSessionDetail({ session }: { session: GenSession }) {
|
||||
const packCount = session.packs?.length ?? 0;
|
||||
|
||||
return (
|
||||
<div className="ml-3 mt-2 rounded-[8px] border border-[#e6f578]/24 bg-[#e6f578]/[0.055] p-3 shadow-[0_18px_52px_-38px_rgba(230,245,120,0.82)]">
|
||||
<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">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="text-[9px] uppercase tracking-[0.16em] text-[#e6f578]/65">下级信息</div>
|
||||
<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">
|
||||
{modeLabel(session.inputMode)}
|
||||
</span>
|
||||
@@ -57,7 +56,7 @@ function ActiveSessionDetail({ session }: { session: GenSession }) {
|
||||
<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 ring-1 ring-white/10">
|
||||
<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>
|
||||
))}
|
||||
@@ -88,28 +87,8 @@ export default function Sidebar({
|
||||
onPick: (id: string) => void;
|
||||
onNew: () => void;
|
||||
}) {
|
||||
const rootRef = useRef<HTMLElement | null>(null);
|
||||
const rowRefs = useRef<Record<string, HTMLDivElement | null>>({});
|
||||
const [detailTop, setDetailTop] = useState(140);
|
||||
|
||||
const activeSession = sessions.find(session => session.id === currentId) ?? null;
|
||||
|
||||
function syncDetailTop() {
|
||||
if (!currentId) return;
|
||||
const row = rowRefs.current[currentId];
|
||||
const root = rootRef.current;
|
||||
if (!row || !root) return;
|
||||
const rowRect = row.getBoundingClientRect();
|
||||
const rootRect = root.getBoundingClientRect();
|
||||
const rawTop = rowRect.top - rootRect.top;
|
||||
const maxTop = Math.max(96, rootRect.height - 360);
|
||||
setDetailTop(Math.min(Math.max(96, rawTop), maxTop));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
syncDetailTop();
|
||||
}, [currentId, sessions.length, open]);
|
||||
|
||||
if (!open) {
|
||||
return (
|
||||
<aside className="glass-sidebar w-12 shrink-0 flex flex-col items-center py-4">
|
||||
@@ -127,103 +106,108 @@ export default function Sidebar({
|
||||
}
|
||||
|
||||
return (
|
||||
<aside ref={rootRef} className="relative flex shrink-0 items-stretch overflow-visible">
|
||||
<div className="glass-sidebar w-72 shrink-0 flex flex-col">
|
||||
<div className="px-4 pt-5 pb-3 flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-[8px] bg-gradient-to-br from-[#e6f578] to-[#d6b36a] flex items-center justify-center 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>
|
||||
<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
|
||||
</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 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>
|
||||
<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="收起侧栏"
|
||||
>
|
||||
<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-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="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>
|
||||
<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="收起侧栏"
|
||||
>
|
||||
<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-[10px] font-semibold text-white/35 uppercase tracking-[0.18em]">
|
||||
最近会话
|
||||
</div>
|
||||
<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="flex-1 overflow-y-auto px-3 pb-4 space-y-1" onScroll={syncDetailTop}>
|
||||
{sessions.length === 0 && (
|
||||
<div className="px-3 py-12 text-center text-xs text-white/30">
|
||||
还没有生成记录
|
||||
{activeSession && (
|
||||
<div className="px-3 pb-4">
|
||||
<ActiveSessionDetail session={activeSession} />
|
||||
</div>
|
||||
)}
|
||||
{sessions.map(s => {
|
||||
const selectedCount = s.images.filter(i => i.status === 'selected').length;
|
||||
const active = currentId === s.id;
|
||||
return (
|
||||
<div
|
||||
key={s.id}
|
||||
ref={node => { rowRefs.current[s.id] = node; }}
|
||||
className="relative"
|
||||
>
|
||||
<button
|
||||
onClick={() => onPick(s.id)}
|
||||
className={`w-full text-left px-3 py-2.5 rounded-[8px] text-xs transition-all ${
|
||||
active
|
||||
? 'bg-gradient-to-r from-[#e6f578]/24 via-[#8cb478]/18 to-[#d6b36a]/16 ring-1 ring-[#e6f578]/35 text-white shadow-glow-violet'
|
||||
: 'hover:bg-[#e6f578]/[0.06] text-white/75 ring-1 ring-transparent hover:ring-[#8cb478]/20'
|
||||
}`}
|
||||
>
|
||||
<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 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.5">
|
||||
<span>{s.images.length} 张</span>
|
||||
{selectedCount > 0 && (
|
||||
<span className={active ? 'text-[#e6f578] font-semibold' : 'text-[#d6b36a] font-semibold'}>
|
||||
✓{selectedCount}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="border-t border-white/[0.06] px-5 py-3 text-[10px] text-white/30">
|
||||
本地端口 4560 · 数据 / data
|
||||
<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">
|
||||
{sessions.length === 0 && (
|
||||
<div className="px-3 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>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="border-t border-white/[0.06] px-5 py-3 text-[10px] text-white/30">
|
||||
本地端口 4560 · 数据 / data
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{activeSession && (
|
||||
<div
|
||||
className="absolute left-full z-30 w-80 pl-3"
|
||||
style={{ top: detailTop }}
|
||||
>
|
||||
<ActiveSessionDetail session={activeSession} />
|
||||
</div>
|
||||
)}
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user