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:
@@ -49,6 +49,9 @@ export default function WikiPagePage({
|
|||||||
const [paragraphs, setParagraphs] = useState<Block[]>([]);
|
const [paragraphs, setParagraphs] = useState<Block[]>([]);
|
||||||
const [triplets, setTriplets] = useState<Block[]>([]);
|
const [triplets, setTriplets] = useState<Block[]>([]);
|
||||||
const [entitySet, setEntitySet] = useState<Set<string>>(new Set());
|
const [entitySet, setEntitySet] = useState<Set<string>>(new Set());
|
||||||
|
// Backlinks:所有提到此 entity 的 raw note(V3 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 [collapsed, setCollapsed] = useState<Record<string, boolean>>({});
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
@@ -117,6 +120,42 @@ export default function WikiPagePage({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
setEntitySet(eset);
|
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 blocks,page_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) {
|
} catch (e: any) {
|
||||||
if (!cancelled) setError(e?.message ?? 'load failed');
|
if (!cancelled) setError(e?.message ?? 'load failed');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -207,6 +246,40 @@ export default function WikiPagePage({
|
|||||||
</article>
|
</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
|
<footer
|
||||||
style={{
|
style={{
|
||||||
padding: '20px 0',
|
padding: '20px 0',
|
||||||
|
|||||||
Reference in New Issue
Block a user