diff --git a/.memory/worklog.json b/.memory/worklog.json index e0b63ba..6bc3dc9 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -188,6 +188,13 @@ "message": "auto-save 2026-05-12 18:35 (~3)", "hash": "64db093", "files_changed": 3 + }, + { + "ts": "2026-05-12T18:41:07+08:00", + "type": "commit", + "message": "auto-save 2026-05-12 18:40 (~2)", + "hash": "864781d", + "files_changed": 2 } ] } diff --git a/web/app/globals.css b/web/app/globals.css index 23f5cce..dbba155 100644 --- a/web/app/globals.css +++ b/web/app/globals.css @@ -214,6 +214,73 @@ .glass-node__body { padding: 0.85rem 1rem 1rem; } +/* ============================================================ + Kanban 卡片(Storyflow/参考图风格) + ============================================================ */ +.kanban-card { + position: relative; + border-radius: 14px; + padding: 12px 14px; + border: 1px solid rgba(255, 255, 255, 0.08); + transition: transform 0.2s, box-shadow 0.2s, border-color 0.2s; +} +.kanban-card:hover { + transform: translateY(-1px); + border-color: rgba(255, 255, 255, 0.16); + box-shadow: 0 8px 24px -10px rgba(0, 0, 0, 0.5); +} +/* dark 模式下:低明度彩色 */ +.dark .kanban-violet { background: linear-gradient(160deg, rgba(167, 139, 250, 0.18), rgba(139, 92, 246, 0.08)); } +.dark .kanban-pink { background: linear-gradient(160deg, rgba(244, 114, 182, 0.18), rgba(236, 72, 153, 0.08)); } +.dark .kanban-orange { background: linear-gradient(160deg, rgba(251, 146, 60, 0.18), rgba(249, 115, 22, 0.08)); } +.dark .kanban-blue { background: linear-gradient(160deg, rgba(96, 165, 250, 0.18), rgba(59, 130, 246, 0.08)); } +.dark .kanban-green { background: linear-gradient(160deg, rgba(74, 222, 128, 0.18), rgba(34, 197, 94, 0.08)); } +.dark .kanban-cyan { background: linear-gradient(160deg, rgba(34, 211, 238, 0.18), rgba(6, 182, 212, 0.08)); } +.dark .kanban-rose { background: linear-gradient(160deg, rgba(251, 113, 133, 0.18), rgba(244, 63, 94, 0.08)); } +.dark .kanban-amber { background: linear-gradient(160deg, rgba(252, 211, 77, 0.18), rgba(245, 158, 11, 0.08)); } + +/* light 模式:pastel 暖底 */ +:root:not(.dark) .kanban-violet { background: #ede9fe; border-color: rgba(139, 92, 246, 0.18); } +:root:not(.dark) .kanban-pink { background: #fce7f3; border-color: rgba(236, 72, 153, 0.18); } +:root:not(.dark) .kanban-orange { background: #ffedd5; border-color: rgba(249, 115, 22, 0.18); } +:root:not(.dark) .kanban-blue { background: #dbeafe; border-color: rgba(59, 130, 246, 0.18); } +:root:not(.dark) .kanban-green { background: #dcfce7; border-color: rgba(34, 197, 94, 0.18); } +:root:not(.dark) .kanban-cyan { background: #cffafe; border-color: rgba(6, 182, 212, 0.18); } +:root:not(.dark) .kanban-rose { background: #ffe4e6; border-color: rgba(244, 63, 94, 0.18); } +:root:not(.dark) .kanban-amber { background: #fef3c7; border-color: rgba(245, 158, 11, 0.18); } + +.kanban-tag { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 8px; + border-radius: 999px; + font-size: 10.5px; + font-weight: 500; + background: rgba(255, 255, 255, 0.08); + border: 1px solid rgba(255, 255, 255, 0.1); + color: var(--text-soft); +} +:root:not(.dark) .kanban-tag { + background: rgba(255, 255, 255, 0.7); + border-color: rgba(0, 0, 0, 0.06); + color: var(--text-soft); +} + +.kanban-meta { + display: flex; + align-items: center; + gap: 8px; + margin-top: 10px; + padding-top: 8px; + border-top: 1px solid rgba(255, 255, 255, 0.06); + font-size: 10.5px; + color: var(--text-faint); +} +:root:not(.dark) .kanban-meta { + border-top-color: rgba(0, 0, 0, 0.06); +} + .glass-node__row { display: flex; align-items: center; gap: 0.5rem; font-size: 12px; color: var(--text-soft); diff --git a/web/components/dashboard.tsx b/web/components/dashboard.tsx index 7a7a726..962b7cb 100644 --- a/web/components/dashboard.tsx +++ b/web/components/dashboard.tsx @@ -35,6 +35,41 @@ function MiniCard({ children, className = "", onClick }: { children: ReactNode; ) } +type KanbanTone = "violet" | "pink" | "orange" | "blue" | "green" | "cyan" | "rose" | "amber" + +function KanbanCard({ + tone = "violet", + tags, + title, + className = "", + onClick, + meta, + children, +}: { + tone?: KanbanTone + tags?: string[] + title?: ReactNode + className?: string + onClick?: (e: React.MouseEvent) => void + meta?: ReactNode + children?: ReactNode +}) { + return ( +
+ {tags && tags.length > 0 && ( +
+ {tags.map((t) => ( + #{t} + ))} +
+ )} + {title &&

{title}

} + {children} + {meta &&
{meta}
} +
+ ) +} + interface Props { data: NodeData } @@ -307,11 +342,11 @@ export function Dashboard({ data }: Props) { )} - {/* ---- Keyframe ---- */} + {/* ---- Keyframe — Kanban 卡片 ---- */} {key === "keyframe" && (
{hasVideo && job && ( - + - + )} {!hasFrames ? ( -
等待解析后抽取(默认 5 张)
+ +
候选 30 张 → pHash 去重 + 清晰度排序 → 时序分桶 → 5 张代表分镜
+
) : (
{job!.frames.map((f) => { const isSel = data.selectedFrames.has(f.index) return ( - - -
- 分镜 {f.index + 1} + { e.stopPropagation(); data.onToggleFrame(f.index) }} - className={`text-[10px] px-1.5 py-0.5 rounded inline-flex items-center gap-0.5 ${isSel ? "bg-emerald-500 text-white" : "bg-white/10 text-[var(--text-soft)] border border-white/10"}`} + className={`ml-auto text-[10.5px] px-2 py-0.5 rounded-full inline-flex items-center gap-1 ${ + isSel + ? "bg-emerald-500 text-white" + : "bg-white/10 text-[var(--text-soft)] border border-white/15 hover:bg-white/20" + }`} > - {isSel ? "已选" : "选用"} + {isSel ? "已选用" : "选用此帧"} -
-
+ } + > + + ) })}
@@ -363,26 +406,31 @@ export function Dashboard({ data }: Props) {
)} - {/* ---- ASR / Translate ---- */} + {/* ---- ASR / Translate — Kanban 段落卡 ---- */} {(key === "asr" || key === "translate") && ( !hasTranscript ? ( -
- {colState.asr === "running" ? "Gemini 转录中…" : "等待关键帧抽取"} -
+ +
+ {colState.asr === "running" ? "Gemini 转录中…" : "需要先完成关键帧抽取"} +
+
) : (
{job!.transcript.map((s) => ( - -
- {s.start.toFixed(1)}s → {s.end.toFixed(1)}s + +
+ EN + {s.en}
-
- EN{s.en} +
+ ZH + {s.zh || 翻译中…}
-
- ZH{s.zh || 翻译中…} -
- + ))}
)