feat(mira): [[entity]] wikilink — 顯式建檔 + autocomplete + render link

對應 tasks.md backlog #12 / design.md §3.6.2。leo 反饋:要像 Logseq 那樣
寫 [[X]] 立刻建檔,下次 [[X 自動補完,render 變連結。

helpers:
- parseWikilinks(text) → 抽 [[X]] entity list
- fetchAllEntityNames(apiKey) → 合 index-entry + wiki-page entity 名稱
- getEntityNamesCached → 30s session cache for autocomplete
- ensureEntitiesExist → 新 entity 立刻建 wiki-page placeholder
  - tags 含 entity-type:book/url/concept(guessEntityType 推測:《》→ book / http → url)
  - source: leo-explicit
- expandWikilinks(text) → render 時把 [[X]] 轉 markdown link to /mira/wiki/wiki-X

UI:
- <WikilinkAutocomplete>:textarea cursor 在 `[[query` 未閉合時,下拉顯示既存
  entity(substring filter)+「⊕ 建立 [[query]]」option,↑↓選 / Enter 確認 /
  Esc 取消。fixed-position below textarea bottom(cursor tracking 留下輪)
- PostComposer compact + popup 兩個 textarea 都掛 autocomplete
- EditingArea(PostEditor / BlockEditor / ReplyLine / PageReplyComposer 共用)
  加 apiKey prop,內部 textarea + popup 都掛 autocomplete

submit hook:
- PostComposer.submit:postBlockId 建好後 ensureEntitiesExist(wikilinks)
- BlockEditor.submitReply / ReplyLine.submitReply 同樣建檔
- 在 wiki_synthesis trigger 前先建,避免 race

render:
- markdown.tsx expandWikilinks 取代 stripLogseqMeta 前處理(兩階段)
- 內部 wiki link(/mira/wiki/...)不開 _target=_blank(不離開頁面)

留下輪:metadata 補完 (作者/出版社) / cursor tracking / PostEditor.save 也建檔
This commit is contained in:
2026-05-16 12:08:28 +08:00
parent d6bff9d551
commit 7da1eb6d65
2 changed files with 308 additions and 22 deletions
+26 -12
View File
@@ -8,23 +8,26 @@ import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
export function MarkdownView({ text }: { text: string }) {
const cleaned = useMemo(() => stripLogseqMeta(text), [text]);
// 兩階段預處理:1. strip Logseq metadata2. [[entity]] 轉成 markdown link
const cleaned = useMemo(() => expandWikilinks(stripLogseqMeta(text)), [text]);
return (
<div className="mira-md">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
a: ({ href, children, ...rest }) => (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
className="wiki-link"
{...rest}
>
{children}
</a>
),
a: ({ href, children, ...rest }) => {
const isWikiLink = typeof href === 'string' && href.startsWith('/mira/wiki/');
return (
<a
href={href}
{...(isWikiLink ? {} : { target: '_blank', rel: 'noopener noreferrer' })}
className="wiki-link"
{...rest}
>
{children}
</a>
);
},
// 圖片不直接 inline 顯示(避免大圖打亂 feed),改成連結
img: ({ src, alt }) => {
const href = typeof src === 'string' ? src : '';
@@ -61,3 +64,14 @@ export function stripLogseqMeta(text: string): string {
})
.join('\n');
}
// 把 [[entity]] 轉成 markdown link 指向 /mira/wiki/wiki-{entity}
// 對應 mira-app design.md §3.6.2 + tasks.md backlog #12
export function expandWikilinks(text: string): string {
return text.replace(/\[\[([^\[\]\n]+?)\]\]/g, (_, entity: string) => {
const e = entity.trim();
if (!e) return '[[]]';
const url = `/mira/wiki/${encodeURIComponent('wiki-' + e)}`;
return `[${e}](${url})`;
});
}