From 94368ec981bf4f40ca451fe4b01a3e05fbacb9ed Mon Sep 17 00:00:00 2001 From: richblack Date: Sat, 16 May 2026 11:07:34 +0800 Subject: [PATCH] =?UTF-8?q?fix(mira):=204=20bug=20=E2=80=94=20popup=20?= =?UTF-8?q?=E5=9C=A8=20edit=20/=20=E6=80=9D=E8=80=83=20indicator=20?= =?UTF-8?q?=E6=A2=9D=E4=BB=B6=20/=20RAG=20/=20edit=20@mira?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit leo 試用 P0 後反饋的 4 個問題: bug #1: popup 編輯時沒有,更擠 - EditingArea 元件加 ⇱ 放大 button + popup overlay(共用 PostComposer 的 popup pattern) - 影響:所有 edit / reply 模式現在都有放大編輯 bug #2: Mira 思考中很久(即使沒 @mira) - root cause: onAiTriggered(pageName) 在 PostComposer 永遠 fire,會在每篇貼文觸發 thinking indicator UI - 修:移到 if (hasMiraMention) 分支內。沒 @mira → 不顯示 thinking - wiki_synthesis 仍每篇都跑(背景靜默)但不顯示 thinking,避免 leo 誤會 bug #3: Mira reply 不知道有 wiki,無狀態 - 加 fetchRelevantWikiContext: 撈所有 type=index-entry blocks,用 entity 名稱 keyword match against post content,取 top 3 整篇 markdown 餵進 prompt - prompt 加規則「如果 leo 提到的 entity 你已有 wiki 知識,請主動引用」 - 對應 leo「應該以擁有知識庫的專人來回覆」 bug #4: edit 時 @mira 沒反應 - PostEditor.save / BlockEditor.saveEdit / ReplyLine.save 三處都加 hasMiraMention 偵測 - 條件:新內容含 @mira 且舊版沒有 → 觸發(避免重複編輯重複 trigger) placeholder 加 hint「@mira 呼叫 Mira 回覆」。 --- landing/app/mira/feed/page.tsx | 120 +++++++++++++++++++++++++++++++-- 1 file changed, 116 insertions(+), 4 deletions(-) diff --git a/landing/app/mira/feed/page.tsx b/landing/app/mira/feed/page.tsx index c0d4ff5..05eefc1 100644 --- a/landing/app/mira/feed/page.tsx +++ b/landing/app/mira/feed/page.tsx @@ -164,6 +164,37 @@ function hasMiraMention(text: string): boolean { return /(^|[^\w])@mira\b/i.test(text); } +// 撈相關 wiki context 給 mira reply 用(簡單 RAG) +// 策略:拿全部 index-entry blocks → 用 entity 名稱 keyword match against post content +// → 取 top 3 把整篇 markdown 餵進 prompt(每篇通常 < 1KB,3 篇還在 token 預算內) +async function fetchRelevantWikiContext(apiKey: string, postContent: string): Promise { + try { + const res = await fetch(`${KBDB_BASE}/blocks?type=index-entry&limit=200`, { + headers: { Authorization: `Bearer ${apiKey}` }, + }); + if (!res.ok) return ''; + const data = await res.json() as { blocks?: Array<{ id: string; page_name: string | null; content: string }> }; + const all = data.blocks ?? []; + if (all.length === 0) return ''; + const lower = postContent.toLowerCase(); + // 評分:page_name 剝 `index-` prefix = entity 名稱,看 postContent 是否含 + const scored = all + .map(b => { + const entity = (b.page_name ?? '').replace(/^index-/, '').trim(); + const score = entity && lower.includes(entity.toLowerCase()) ? entity.length : 0; + return { entity, content: b.content, score }; + }) + .filter(x => x.score > 0) + .sort((a, b) => b.score - a.score) + .slice(0, 3); + if (scored.length === 0) return ''; + return scored.map(s => `### entity: ${s.entity}\n${s.content}`).join('\n\n'); + } catch (e) { + console.warn('[fetchRelevantWikiContext] failed', e); + return ''; + } +} + async function triggerWikiSynthesis(opts: { apiKey: string; rawBlockId: string }) { // 觸發 arcrun wiki_synthesis workflow(arcrun-native public trigger endpoint) // 不等結果(workflow 60-90s 含 2 次 claude_api pause/resume) @@ -200,7 +231,12 @@ async function triggerAiReply(opts: { ? `\nleo 用 \`@mira ${topic}\` 呼叫了你,所以這則對話的主題鎖定在「${topic}」。\n` : '\nleo 用 \`@mira\` 呼叫了你,請針對訊息回覆。\n'; + // RAG:撈既有 wiki index-entries,挑跟貼文 keyword match 的塞進 system context + // 對應 leo bug #3「Mira 應該以擁有知識庫的專人來回覆,不是無狀態」 + const knowledgeContext = await fetchRelevantWikiContext(opts.apiKey, opts.postContent); + const prompt = + (knowledgeContext ? `## 你目前的 wiki 知識庫(跟本次訊息相關的 entity 摘要)\n\n${knowledgeContext}\n\n---\n\n` : '') + `用戶 leo 在 mira 河道發了這則訊息:\n\n` + `「${opts.postContent}」\n\n` + topicHint + @@ -209,7 +245,8 @@ async function triggerAiReply(opts: { `- 繁體中文(台灣用語)\n` + `- 簡短 1-3 段,務實,不客套\n` + `- 可以發問、補充、提建議、或反問釐清\n` + - `- 不替老闆下決策,但可以給判斷依據`; + `- 不替老闆下決策,但可以給判斷依據\n` + + (knowledgeContext ? `- **如果 leo 提到的 entity 你已有 wiki 知識,請主動引用 / 連結(用「你之前寫過...」或「在《X》wiki 中你提到...」)**\n` : ''); try { const aiRes = await fetch(CLAUDE_API, { @@ -306,6 +343,7 @@ function PostComposer({ // P0 #5b:只有 @mira 時才觸發 Mira AI 回覆(撤回每篇 auto-reply) // 對應 design.md §3.6.5「河道是 process 場 + Mira 是 KB 同步介面」 + // 「Mira 思考中」indicator 也只在 @mira 時 show(避免每篇都顯示讓 leo 誤會) if (hasMiraMention(trimmed)) { void triggerAiReply({ apiKey: me.api_key, @@ -313,11 +351,11 @@ function PostComposer({ parentBlockId: postBlockId, pageName, }); + onAiTriggered(pageName); } // 7B.3h:fire-and-forget 觸發 wiki_synthesis(browser → cypher.arcrun.dev,arcrun-native) - // 跟 @mira 無關,每篇都跑 — 河道書寫永遠進 wiki KB 副本 + // 跟 @mira 無關,每篇都跑 — 河道書寫永遠進 wiki KB 副本(不顯示 thinking indicator,背景靜默跑) void triggerWikiSynthesis({ apiKey: me.api_key, rawBlockId: postBlockId }); - onAiTriggered(pageName); // 給 D1 GROUP BY 查詢看到新資料的時間 await new Promise(r => setTimeout(r, 1500)); @@ -985,6 +1023,16 @@ function PostEditor({ } onSaved(trimmed); + // P0 #5b bug #4: edit 後若加了 @mira(舊版沒)→ 觸發 mira reply + if (hasMiraMention(trimmed) && !hasMiraMention(initial)) { + const firstId = mainBlocks[0]?.id; + const pageName = mainBlocks[0]?.page_name ?? ''; + if (firstId && pageName) { + void triggerAiReply({ + apiKey, postContent: trimmed, parentBlockId: firstId, pageName, + }); + } + } } catch (e) { setErr(e instanceof Error ? e.message : String(e)); } finally { @@ -1001,7 +1049,7 @@ function PostEditor({ saving={saving} err={err} submitLabel="儲存" - placeholder="編輯內容⋯" + placeholder="編輯內容⋯(@mira 呼叫 Mira 回覆)" /> ); } @@ -1077,6 +1125,12 @@ function BlockLine({ const data = (await res.json()) as { success?: boolean; error?: string }; if (!res.ok || !data.success) { setErr(data.error || `儲存失敗:${res.status}`); return; } onUpdated(block.id, draft); + // P0 #5b bug #4:edit 後若加了 @mira(之前沒)→ 觸發 mira reply + if (hasMiraMention(draft) && !hasMiraMention(block.content ?? '')) { + void triggerAiReply({ + apiKey, postContent: draft, parentBlockId: block.id, pageName: docPageName, + }); + } setMode('view'); } catch (e) { setErr(e instanceof Error ? e.message : String(e)); } finally { setSaving(false); } @@ -1198,6 +1252,12 @@ function ReplyLine({ const data = (await res.json()) as { success?: boolean; error?: string }; if (!res.ok || !data.success) { setErr(data.error || `儲存失敗:${res.status}`); return; } onUpdated(reply.id, draft); + // P0 #5b bug #4: edit reply 加 @mira 也觸發 + if (hasMiraMention(draft) && !hasMiraMention(reply.content ?? '')) { + void triggerAiReply({ + apiKey, postContent: draft, parentBlockId: reply.id, pageName: docPageName, + }); + } setMode('view'); } catch (e) { setErr(e instanceof Error ? e.message : String(e)); } finally { setSaving(false); } @@ -1391,6 +1451,8 @@ function EditingArea({ placeholder?: string; }) { const taRef = useRef(null); + const popupTaRef = useRef(null); + const [expanded, setExpanded] = useState(false); return ( <> @@ -1410,6 +1472,13 @@ function EditingArea({ className="mira-edit-textarea" />
+ {err && {err}} ⌘+Enter · Esc @@ -1418,6 +1487,49 @@ function EditingArea({ {saving ? '處理中⋯' : submitLabel}
+ + {/* P0 #5c: editing 也支援 popup 放大 */} + {expanded && ( +
{ if (e.target === e.currentTarget) setExpanded(false); }} + > +
+
+ {submitLabel} + + +
+