// ———— Search Page ————
function MarkdownLite({ text }) {
const lines = text.split("\n");
const out = [];
let listBuf = [];
const flush = () => { if (listBuf.length) { out.push(
); listBuf = []; } };
const inline = (s, k) => {
const parts = [];
let rest = s;
let idx = 0;
const patterns = [
[/\*\*(.+?)\*\*/, (m) => {m[1]}],
[/`([^`]+)`/, (m) => {m[1]}],
];
while (rest) {
let earliest = -1, which = -1, match;
patterns.forEach(([re], i) => {
const m = rest.match(re);
if (m && (earliest === -1 || m.index < earliest)) { earliest = m.index; which = i; match = m; }
});
if (which === -1) { parts.push(rest); break; }
if (earliest > 0) parts.push(rest.slice(0, earliest));
parts.push(patterns[which][1](match));
rest = rest.slice(earliest + match[0].length);
}
return parts;
};
lines.forEach((ln, i) => {
if (ln.startsWith("- ")) {
listBuf.push({inline(ln.slice(2), i)});
} else if (ln.trim() === "") {
flush();
} else {
flush();
out.push({inline(ln, i)}
);
}
});
flush();
return {out}
;
}
function MechBadge({ name }) {
const cls = MECH_COLORS[name] || "neutral";
return {name};
}
function SearchPage({ openDetail }) {
const [q, setQ] = useState("");
const [loading, setLoading] = useState(false);
const [results, setResults] = useState([]);
const [insight, setInsight] = useState("");
const [error, setError] = useState("");
const [searched, setSearched] = useState(false);
const doSearch = async (e) => {
if (e) e.preventDefault();
if (!q.trim()) return;
setLoading(true);
setError("");
try {
const data = await window.API.search(q.trim(), 20);
setInsight(data.insight || "");
setResults(data.videos || []);
setSearched(true);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
const vectorCount = results.filter(r => r.score_source === "vector").length;
const aiCount = results.filter(r => r.score_source === "hermes_ref").length;
const bothCount = results.filter(r => r.score_source === "both").length;
const _openDetail = (r) => {
const platMap = { youtube: PLATFORMS[0], tiktok: PLATFORMS[1], bilibili: PLATFORMS[2] };
const meta = r.metadata || {};
const hue = (r.id || "").split("").reduce((a, c) => a + c.charCodeAt(0), 200) % 360;
const mechs = (() => { try { return JSON.parse(meta.viral_mechanisms || "[]"); } catch { return []; } })();
const labels = (() => { try { return JSON.parse(meta.structure_labels || "[]"); } catch { return []; } })();
openDetail({
id: r.id,
title: meta.title || r.id,
platform: platMap[meta.platform] || { id: "?", label: "?", cls: "neutral" },
type: meta.video_type || "",
structure: meta.story_structure || "",
hook_type: meta.hook_type || "",
score: meta.score_total || 0,
reproducibility: meta.reproducibility || "",
publishedAt: meta.published_at || "",
duration: meta.duration_sec || 0,
url: meta.url || "",
thumbHue: hue,
mechanisms: mechs,
emotionPath: (meta.emotion_curve || "").split(/[→\-]/).map(s => s.trim()).filter(Boolean),
transferable: mechs,
structure_labels_arr: labels,
radar: { "综合评分": meta.score_total || 0 },
metricsHistory: [],
});
};
return (
<>
语义搜索
结合 ChromaDB 向量检索与 Hermes AI 洞察,返回最贴近意图的爆款样本。
在知识库中,描述你想找的视频
自然语言 / 机制关键词 / 结构标签都可以 · 支持中英文混写
{error && (
{error}
)}
{(searched || loading) && (
<>
{/* AI Insight */}
H
AI 知识库洞察
Hermes LLM Wiki
{loading ? (
) : insight ? (
) : (
Hermes 未配置或无洞察内容。向量搜索结果如下。
)}
{/* Results */}
{!loading && (
<>
{results.length} 条视频结果 · 按综合评分排序
{vectorCount > 0 && 向量匹配 {vectorCount}}
{aiCount > 0 && AI 引用 {aiCount}}
{bothCount > 0 && 双重命中 {bothCount}}
{results.length === 0 ? (
知识库中暂无相关视频,请先采集一些视频
) : (
{results.map((r, idx) => {
const meta = r.metadata || {};
const mechs = (() => { try { return JSON.parse(meta.viral_mechanisms || "[]"); } catch { return []; } })();
const platMap = { youtube: PLATFORMS[0], tiktok: PLATFORMS[1], bilibili: PLATFORMS[2] };
const plat = platMap[meta.platform] || { id: "?", label: "?", cls: "neutral" };
const hue = (r.id || "").split("").reduce((a, c) => a + c.charCodeAt(0), 200) % 360;
const scoreDisplay = Math.round((r.score || 0) * 100);
return (
_openDetail(r)}>
{formatDuration(meta.duration_sec || 0)}
{meta.title || r.id}
{meta.story_structure || ""} · {formatDate(meta.published_at || "")}
{mechs.slice(0, 4).map(m => )}
{meta.score_total || "—"}
{meta.score_total && /100}
{r.score_source === "vector" && 向量匹配}
{r.score_source === "hermes_ref" && AI 引用}
{r.score_source === "both" && 双重命中}
得分 {(r.score || 0).toFixed(3)}
);
})}
)}
>
)}
>
)}
{!searched && !loading && (
输入查询词后按搜索
)}
>
);
}
window.SearchPage = SearchPage;