feat(mira/wiki): backlinks section — 顯示提到此 entity 的 raw notes (#2 leo 反饋)

leo 2026-05-17 反饋:「從這本書的條目應該反向連到那篇筆記去,
如果在 Logseq 會放在下方列表提到這個條目的其他內容」

實作:
- 撈所有 content === entity 的 wiki-page (V3 一次寫入會建多個 wiki-page
  per raw mention)
- 從每個 wiki-page tags_json 取 raw:XXX tag → unique raw_ids
- fetch 對應 raw blocks → render 「📎 提到此 entity 的筆記」section
  每條 link 跳 /mira/feed#page=...
- 顯示前 100 字 preview,全文 hover title
- 樣式:左 border + 暗色背景 (區分於主內容)

對應 wiki_synthesis V3 (commit 63ac4c9 mira) 的 wiki-page tags raw:XXX
標記設計:每篇 raw 提到某 entity 時,create_wiki_page 都會寫一個新的
wiki-page (page_name 同名),tags 含 raw:{raw_id}。反查 wiki 對應 raw
不靠 KBDB graph 反向 index,純走客戶端 wiki-page list filter。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-17 10:40:40 +08:00
parent 4fd2d3ba6c
commit 7dae958dbe
+73
View File
@@ -49,6 +49,9 @@ export default function WikiPagePage({
const [paragraphs, setParagraphs] = useState<Block[]>([]);
const [triplets, setTriplets] = useState<Block[]>([]);
const [entitySet, setEntitySet] = useState<Set<string>>(new Set());
// Backlinks:所有提到此 entity 的 raw noteV3 wiki_synthesis 在 wiki-page tags 寫 raw:XXX
// 對應 leo 2026-05-17 #2 反饋:「從這本書的條目應該反向連到那篇筆記去」
const [backlinkRaws, setBacklinkRaws] = useState<Block[]>([]);
const [collapsed, setCollapsed] = useState<Record<string, boolean>>({});
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
@@ -117,6 +120,42 @@ export default function WikiPagePage({
}
}
setEntitySet(eset);
// Backlinks:找此 entity 的所有 wiki-page (可能多次寫入),提取 raw:XXX tag → fetch raw blocks
if (wikiPage.type === 'wiki-page' && wikiPage.content) {
const sameEntity = allPages.filter((p) => p.content?.trim() === wikiPage.content?.trim());
const rawIds = new Set<string>();
for (const wp of sameEntity) {
try {
const tags = JSON.parse(wp.tags_json || '[]') as string[];
for (const t of tags) {
if (typeof t === 'string' && t.startsWith('raw:')) {
rawIds.add(t.slice(4));
}
}
} catch { /* skip */ }
}
if (rawIds.size > 0) {
// 一次撈 raw blockspage_name 是 unique 一次 query 一個
const rawBlocks: Block[] = [];
await Promise.all(
Array.from(rawIds).map(async (rawId) => {
try {
// KBDB GET /blocks/:id 直接 by id (走 list with block_id filter)
const r = await fetch(`${KBDB_BASE}/blocks/${rawId}`, { headers });
if (r.ok) {
const data = await r.json();
const b = data.blocks?.[0] ?? data;
if (b?.id) rawBlocks.push(b as Block);
}
} catch { /* skip */ }
}),
);
if (!cancelled) {
setBacklinkRaws(rawBlocks.sort((a, b) => b.updated_at - a.updated_at));
}
}
}
} catch (e: any) {
if (!cancelled) setError(e?.message ?? 'load failed');
} finally {
@@ -207,6 +246,40 @@ export default function WikiPagePage({
</article>
)}
{/* Backlinks:提到此 entity 的 raw notes */}
{isWikiPage && backlinkRaws.length > 0 && (
<section
style={{
margin: '24px 0 16px',
padding: '12px 14px',
borderLeft: '3px solid #4a3a2a',
background: 'rgba(80, 60, 40, 0.08)',
}}
>
<h3 style={{ margin: '0 0 8px', fontSize: 14, color: '#aab', fontWeight: 600 }}>
📎 entity ({backlinkRaws.length})
</h3>
<ul style={{ margin: 0, paddingLeft: 18, fontSize: 13, lineHeight: 1.6 }}>
{backlinkRaws.map((raw) => {
const preview = (raw.content || '').replace(/\n/g, ' ').slice(0, 100);
const href = `/mira/feed#page=${encodeURIComponent(raw.page_name || raw.id)}`;
return (
<li key={raw.id} style={{ marginBottom: 4 }}>
<a
href={href}
style={{ color: '#9ab', textDecoration: 'none' }}
title={raw.content || ''}
>
{preview}
{(raw.content || '').length > 100 && '…'}
</a>
</li>
);
})}
</ul>
</section>
)}
<footer
style={{
padding: '20px 0',