// ———— Library Page ———— function PlatformBadge({ platform }) { if (!platform) return null; return {platform.label || "?"}; } function MechBadge({ name }) { const cls = MECH_COLORS[name] || "neutral"; return {name}; } // Map API video to display format function _mapVideo(v) { const platMap = { youtube: PLATFORMS[0], tiktok: PLATFORMS[1], bilibili: PLATFORMS[2] }; const hue = (v.id || "").split("").reduce((a, c) => a + c.charCodeAt(0), 200) % 360; return { ...v, type: v.video_type, structure: v.story_structure, mechanisms: Array.isArray(v.viral_mechanisms) ? v.viral_mechanisms : [], score: v.score_total || 0, publishedAt: v.published_at || "", duration: v.duration_sec || 0, channel: "", // not in ChromaDB views: 0, // not in ChromaDB thumbHue: hue, platform: platMap[v.platform] || { id: v.platform, label: v.platform?.slice(0,2).toUpperCase() || "?", cls: "neutral", name: v.platform }, emotionPath: (v.emotion_curve || "").split(/[→\-]/).map(s => s.trim()).filter(Boolean), transferable: Array.isArray(v.viral_mechanisms) ? v.viral_mechanisms : [], structure_labels_arr: Array.isArray(v.structure_labels) ? v.structure_labels : [], radar: { "综合评分": v.score_total || 0, }, metricsHistory: [], }; } function VideoCard({ video, onClick }) { return (
{video.id}
{formatDuration(video.duration)}
{video.title}
{video.type} · {formatDate(video.publishedAt)}
{video.mechanisms.slice(0, 3).map(m => )} {video.mechanisms.length > 3 && +{video.mechanisms.length - 3}}
{video.score} {video.reproducibility}
); } function RadarChart({ data, weights }) { const keys = Object.keys(data); const N = keys.length; const cx = 120, cy = 110, R = 80; const pt = (i, v) => { const a = -Math.PI / 2 + (i / N) * Math.PI * 2; const r = (v / 100) * R; return [cx + r * Math.cos(a), cy + r * Math.sin(a)]; }; const axisPt = (i, r=R) => { const a = -Math.PI / 2 + (i / N) * Math.PI * 2; return [cx + r * Math.cos(a), cy + r * Math.sin(a)]; }; const points = keys.map((k, i) => pt(i, data[k]).join(",")).join(" "); return (
{[0.25, 0.5, 0.75, 1].map(r => ( axisPt(i, R * r).join(",")).join(" ")} fill="none" stroke="oklch(0.88 0.008 255)" strokeWidth="1" /> ))} {keys.map((k, i) => { const [x, y] = axisPt(i); return ; })} {keys.map((k, i) => { const [x, y] = pt(i, data[k]); return ; })} {keys.map((k, i) => { const [x, y] = axisPt(i, R + 18); return {k}; })}
); } function LineChart({ data }) { if (!data || data.length < 2) return
暂无播放量历史数据
; const W = 500, H = 160, P = 28; const max = Math.max(...data.map(d => d.v)); const min = Math.min(...data.map(d => d.v)); const x = (i) => P + (i / (data.length - 1)) * (W - P * 2); const y = (v) => H - P - ((v - min) / Math.max(1, max - min)) * (H - P * 2); const pts = data.map((d, i) => `${x(i)},${y(d.v)}`).join(" "); const area = `M ${x(0)},${H - P} L ${data.map((d, i) => `${x(i)},${y(d.v)}`).join(" L ")} L ${x(data.length-1)},${H-P} Z`; return (
{[0.25, 0.5, 0.75].map(f => ( ))} {data.map((d, i) => i % 4 === 0 && ( ))} {formatViews(max)} {formatViews(min)}
); } function Drawer({ video, onClose }) { if (!video) return null; return ( <>
e.stopPropagation()}>

视频详情 · {video.id}

{formatDuration(video.duration)}

{video.title}

发布时间
{formatDate(video.publishedAt)}
视频类型
{video.type}
剧情结构
{video.structure}
钩子类型
{video.hook_type || "—"}
综合评分
{video.score} / 100
可复刻
{video.reproducibility}
{video.url && ( )} {video.emotionPath && video.emotionPath.length > 0 && (

情绪路径

{video.emotionPath.map((e, i) => (
0{i+1} {e}
))}
)} {video.structure_labels_arr && video.structure_labels_arr.length > 0 && (

结构标签

{video.structure_labels_arr.map(l => {l})}
)} {video.transferable && video.transferable.length > 0 && (

可迁移机制

{video.transferable.map((t, i) => (
· {t}
))}
)}

评分

); } function MultiPick({ label, options, value, onChange, single }) { const [open, setOpen] = useState(false); const text = value.length === 0 ? "全部" : single ? value[0] : `已选`; return (
{label}
setOpen(o=>!o)}> {text} {value.length > 0 && !single && {value.length}}
{open && ( <>
setOpen(false)} />
{options.map(opt => { const active = value.includes(opt); return (
{ if (single) { onChange(active ? [] : [opt]); setOpen(false); } else onChange(active ? value.filter(x=>x!==opt) : [...value, opt]); }} style={{ padding: "6px 10px", fontSize: 12.5, borderRadius: 4, cursor: "pointer", background: active ? "var(--accent-weak)" : "transparent", color: active ? "var(--accent-ink)" : "var(--ink-1)", display: "flex", alignItems: "center", gap: 6 }}> {!single && {active && }} {opt}
); })}
)}
); } function LibraryPage({ openDetail }) { const [allVideos, setAllVideos] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); const [types, setTypes] = useState([]); const [mechs, setMechs] = useState([]); const [structure, setStructure] = useState([]); const [scoreMin, setScoreMin] = useState(0); const [plats, setPlats] = useState([]); const [page, setPage] = useState(1); const loadVideos = async () => { setLoading(true); setError(""); try { const data = await window.API.listVideos({ page_size: 200 }); setAllVideos(data.map(_mapVideo)); } catch (e) { setError(e.message); } finally { setLoading(false); } }; useEffect(() => { loadVideos(); }, []); const filtered = useMemo(() => { return allVideos.filter(v => (types.length === 0 || types.includes(v.type)) && (mechs.length === 0 || v.mechanisms.some(m => mechs.includes(m))) && (structure.length === 0 || structure.includes(v.structure)) && (v.score >= scoreMin) && (plats.length === 0 || plats.some(p => v.platform.id === p || v.platform === p)) ); }, [allVideos, types, mechs, structure, scoreMin, plats]); const pageSize = 18; const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize)); const shown = filtered.slice((page - 1) * pageSize, page * pageSize); const reset = () => { setTypes([]); setMechs([]); setStructure([]); setScoreMin(0); setPlats([]); setPage(1); }; return ( <>

知识库

{allVideos.length} 条 · 筛选 {filtered.length} 条 · 每页 18 条
{error && (
{error}
)}
{setTypes(v); setPage(1);}} /> {setMechs(v); setPage(1);}} /> {setStructure(v); setPage(1);}} single />
评分 ≥ {scoreMin}
{setScoreMin(+e.target.value); setPage(1);}} />
0100
平台
{PLATFORMS.map(p => ( ))}
{(types.length || mechs.length || structure.length || scoreMin > 0 || plats.length) ? (
) : null}
{loading ? (
加载中...
) : shown.length === 0 ? (
{allVideos.length === 0 ? "知识库为空,请先在采集页提交视频链接" : "当前筛选条件下无结果"}
) : ( <>
{shown.map(v => ( openDetail(v)} /> ))}
{totalPages > 1 && (
{page} / {totalPages}
)} )} ); } window.LibraryPage = LibraryPage; window.Drawer = Drawer;