// ———— 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 条
{loading ? : "刷新"}
{error && (
{error}
)}
{setTypes(v); setPage(1);}} />
{setMechs(v); setPage(1);}} />
{setStructure(v); setPage(1);}} single />
平台
{PLATFORMS.map(p => (
{ setPlats(plats.includes(p.id) ? plats.filter(x=>x!==p.id) : [...plats, p.id]); setPage(1); }}>
{p.label}
))}
{(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 && (
setPage(p=>p-1)}>← 上一页
{page} / {totalPages}
setPage(p=>p+1)}>下一页 →
)}
>
)}
>
);
}
window.LibraryPage = LibraryPage;
window.Drawer = Drawer;