fix(mira): 4 bug — popup 在 edit / 思考 indicator 條件 / RAG / edit @mira
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 回覆」。
This commit is contained in:
@@ -164,6 +164,37 @@ function hasMiraMention(text: string): boolean {
|
|||||||
return /(^|[^\w])@mira\b/i.test(text);
|
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<string> {
|
||||||
|
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 }) {
|
async function triggerWikiSynthesis(opts: { apiKey: string; rawBlockId: string }) {
|
||||||
// 觸發 arcrun wiki_synthesis workflow(arcrun-native public trigger endpoint)
|
// 觸發 arcrun wiki_synthesis workflow(arcrun-native public trigger endpoint)
|
||||||
// 不等結果(workflow 60-90s 含 2 次 claude_api pause/resume)
|
// 不等結果(workflow 60-90s 含 2 次 claude_api pause/resume)
|
||||||
@@ -200,7 +231,12 @@ async function triggerAiReply(opts: {
|
|||||||
? `\nleo 用 \`@mira ${topic}\` 呼叫了你,所以這則對話的主題鎖定在「${topic}」。\n`
|
? `\nleo 用 \`@mira ${topic}\` 呼叫了你,所以這則對話的主題鎖定在「${topic}」。\n`
|
||||||
: '\nleo 用 \`@mira\` 呼叫了你,請針對訊息回覆。\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 =
|
const prompt =
|
||||||
|
(knowledgeContext ? `## 你目前的 wiki 知識庫(跟本次訊息相關的 entity 摘要)\n\n${knowledgeContext}\n\n---\n\n` : '') +
|
||||||
`用戶 leo 在 mira 河道發了這則訊息:\n\n` +
|
`用戶 leo 在 mira 河道發了這則訊息:\n\n` +
|
||||||
`「${opts.postContent}」\n\n` +
|
`「${opts.postContent}」\n\n` +
|
||||||
topicHint +
|
topicHint +
|
||||||
@@ -209,7 +245,8 @@ async function triggerAiReply(opts: {
|
|||||||
`- 繁體中文(台灣用語)\n` +
|
`- 繁體中文(台灣用語)\n` +
|
||||||
`- 簡短 1-3 段,務實,不客套\n` +
|
`- 簡短 1-3 段,務實,不客套\n` +
|
||||||
`- 可以發問、補充、提建議、或反問釐清\n` +
|
`- 可以發問、補充、提建議、或反問釐清\n` +
|
||||||
`- 不替老闆下決策,但可以給判斷依據`;
|
`- 不替老闆下決策,但可以給判斷依據\n` +
|
||||||
|
(knowledgeContext ? `- **如果 leo 提到的 entity 你已有 wiki 知識,請主動引用 / 連結(用「你之前寫過...」或「在《X》wiki 中你提到...」)**\n` : '');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const aiRes = await fetch(CLAUDE_API, {
|
const aiRes = await fetch(CLAUDE_API, {
|
||||||
@@ -306,6 +343,7 @@ function PostComposer({
|
|||||||
|
|
||||||
// P0 #5b:只有 @mira 時才觸發 Mira AI 回覆(撤回每篇 auto-reply)
|
// P0 #5b:只有 @mira 時才觸發 Mira AI 回覆(撤回每篇 auto-reply)
|
||||||
// 對應 design.md §3.6.5「河道是 process 場 + Mira 是 KB 同步介面」
|
// 對應 design.md §3.6.5「河道是 process 場 + Mira 是 KB 同步介面」
|
||||||
|
// 「Mira 思考中」indicator 也只在 @mira 時 show(避免每篇都顯示讓 leo 誤會)
|
||||||
if (hasMiraMention(trimmed)) {
|
if (hasMiraMention(trimmed)) {
|
||||||
void triggerAiReply({
|
void triggerAiReply({
|
||||||
apiKey: me.api_key,
|
apiKey: me.api_key,
|
||||||
@@ -313,11 +351,11 @@ function PostComposer({
|
|||||||
parentBlockId: postBlockId,
|
parentBlockId: postBlockId,
|
||||||
pageName,
|
pageName,
|
||||||
});
|
});
|
||||||
|
onAiTriggered(pageName);
|
||||||
}
|
}
|
||||||
// 7B.3h:fire-and-forget 觸發 wiki_synthesis(browser → cypher.arcrun.dev,arcrun-native)
|
// 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 });
|
void triggerWikiSynthesis({ apiKey: me.api_key, rawBlockId: postBlockId });
|
||||||
onAiTriggered(pageName);
|
|
||||||
|
|
||||||
// 給 D1 GROUP BY 查詢看到新資料的時間
|
// 給 D1 GROUP BY 查詢看到新資料的時間
|
||||||
await new Promise(r => setTimeout(r, 1500));
|
await new Promise(r => setTimeout(r, 1500));
|
||||||
@@ -985,6 +1023,16 @@ function PostEditor({
|
|||||||
}
|
}
|
||||||
|
|
||||||
onSaved(trimmed);
|
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) {
|
} catch (e) {
|
||||||
setErr(e instanceof Error ? e.message : String(e));
|
setErr(e instanceof Error ? e.message : String(e));
|
||||||
} finally {
|
} finally {
|
||||||
@@ -1001,7 +1049,7 @@ function PostEditor({
|
|||||||
saving={saving}
|
saving={saving}
|
||||||
err={err}
|
err={err}
|
||||||
submitLabel="儲存"
|
submitLabel="儲存"
|
||||||
placeholder="編輯內容⋯"
|
placeholder="編輯內容⋯(@mira 呼叫 Mira 回覆)"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1077,6 +1125,12 @@ function BlockLine({
|
|||||||
const data = (await res.json()) as { success?: boolean; error?: string };
|
const data = (await res.json()) as { success?: boolean; error?: string };
|
||||||
if (!res.ok || !data.success) { setErr(data.error || `儲存失敗:${res.status}`); return; }
|
if (!res.ok || !data.success) { setErr(data.error || `儲存失敗:${res.status}`); return; }
|
||||||
onUpdated(block.id, draft);
|
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');
|
setMode('view');
|
||||||
} catch (e) { setErr(e instanceof Error ? e.message : String(e)); }
|
} catch (e) { setErr(e instanceof Error ? e.message : String(e)); }
|
||||||
finally { setSaving(false); }
|
finally { setSaving(false); }
|
||||||
@@ -1198,6 +1252,12 @@ function ReplyLine({
|
|||||||
const data = (await res.json()) as { success?: boolean; error?: string };
|
const data = (await res.json()) as { success?: boolean; error?: string };
|
||||||
if (!res.ok || !data.success) { setErr(data.error || `儲存失敗:${res.status}`); return; }
|
if (!res.ok || !data.success) { setErr(data.error || `儲存失敗:${res.status}`); return; }
|
||||||
onUpdated(reply.id, draft);
|
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');
|
setMode('view');
|
||||||
} catch (e) { setErr(e instanceof Error ? e.message : String(e)); }
|
} catch (e) { setErr(e instanceof Error ? e.message : String(e)); }
|
||||||
finally { setSaving(false); }
|
finally { setSaving(false); }
|
||||||
@@ -1391,6 +1451,8 @@ function EditingArea({
|
|||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
}) {
|
}) {
|
||||||
const taRef = useRef<HTMLTextAreaElement>(null);
|
const taRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
const popupTaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
const [expanded, setExpanded] = useState(false);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MarkdownToolbar textareaRef={taRef} value={draft} setValue={setDraft} />
|
<MarkdownToolbar textareaRef={taRef} value={draft} setValue={setDraft} />
|
||||||
@@ -1410,6 +1472,13 @@ function EditingArea({
|
|||||||
className="mira-edit-textarea"
|
className="mira-edit-textarea"
|
||||||
/>
|
/>
|
||||||
<div className="mira-edit-actions">
|
<div className="mira-edit-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => { setExpanded(true); setTimeout(() => popupTaRef.current?.focus(), 50); }}
|
||||||
|
className="mira-btn-ghost mira-composer-expand"
|
||||||
|
title="放大編輯"
|
||||||
|
aria-label="放大編輯"
|
||||||
|
>⇱</button>
|
||||||
{err && <span className="mira-msg-error">{err}</span>}
|
{err && <span className="mira-msg-error">{err}</span>}
|
||||||
<span style={{ flex: 1 }} />
|
<span style={{ flex: 1 }} />
|
||||||
<span className="mira-kbd">⌘+Enter · Esc</span>
|
<span className="mira-kbd">⌘+Enter · Esc</span>
|
||||||
@@ -1418,6 +1487,49 @@ function EditingArea({
|
|||||||
{saving ? '處理中⋯' : submitLabel}
|
{saving ? '處理中⋯' : submitLabel}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* P0 #5c: editing 也支援 popup 放大 */}
|
||||||
|
{expanded && (
|
||||||
|
<div
|
||||||
|
className="mira-composer-popup-backdrop"
|
||||||
|
onClick={(e) => { if (e.target === e.currentTarget) setExpanded(false); }}
|
||||||
|
>
|
||||||
|
<div className="mira-composer-popup">
|
||||||
|
<header className="mira-composer-popup-header">
|
||||||
|
<span className="mira-composer-popup-title">{submitLabel}</span>
|
||||||
|
<span style={{ flex: 1 }} />
|
||||||
|
<button type="button" onClick={() => setExpanded(false)} className="mira-btn-ghost" title="收起" aria-label="關閉">✕</button>
|
||||||
|
</header>
|
||||||
|
<textarea
|
||||||
|
ref={popupTaRef}
|
||||||
|
value={draft}
|
||||||
|
onChange={e => setDraft(e.target.value)}
|
||||||
|
onKeyDown={e => {
|
||||||
|
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') { e.preventDefault(); onSubmit(); setExpanded(false); return; }
|
||||||
|
if (e.key === 'Escape') { e.preventDefault(); setExpanded(false); return; }
|
||||||
|
handleTabIndent(e, draft, setDraft);
|
||||||
|
}}
|
||||||
|
placeholder={placeholder ?? '寫點什麼…(Esc 收起、⌘+Enter 送出)'}
|
||||||
|
disabled={saving}
|
||||||
|
className="mira-composer-popup-textarea"
|
||||||
|
/>
|
||||||
|
<div className="mira-composer-popup-actions">
|
||||||
|
<MarkdownToolbar textareaRef={popupTaRef} value={draft} setValue={setDraft} />
|
||||||
|
{err && <span className="mira-msg-error">{err}</span>}
|
||||||
|
<span style={{ flex: 1 }} />
|
||||||
|
<span className="mira-kbd">⌘+Enter</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={saving}
|
||||||
|
onClick={() => { onSubmit(); setExpanded(false); }}
|
||||||
|
className="mira-btn-primary"
|
||||||
|
>
|
||||||
|
{saving ? '處理中⋯' : submitLabel}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user