'use client'; export const runtime = 'edge'; // Mira 搜尋頁 — 大眾化語義入口(leo 2026-07-05 拍板) // default = 語義搜尋(普通用戶記大概意思;語義 normalize:黃仁勳=皮衣男=Jensen Huang 歸一) // 進階 = 關鍵字(懂的人用) // 後端:self-hosted leo21c KBDB(新 ingest 產 triplet/gloss entry,非舊 index-entry blocks)。 // 過渡期直連 kbdb raw worker(cypher-proxy /kbdb/entries 尚未補,見 mira §1.7)。 // 舊後端 kbdb.finally.click 已移除(mira 鐵律:禁直打舊 SaaS KBDB 當主儲存)。 // graph 遍歷之後再補(本次不做)。 import { Suspense, useCallback, useEffect, useState } from 'react'; import { useRouter, useSearchParams } from 'next/navigation'; import Link from 'next/link'; import '../mira.css'; // 新 KBDB(self-hosted leo21c)。過渡期直連 raw worker(無 auth、單租戶), // 不帶 owner_id(Vectorize owner metadata index 未建,帶了會回 0;總管另修)。 const KBDB_BASE = 'https://arcrun-kbdb.leo21c.workers.dev'; // 登入 gate 仍走官方 cypher(session cookie 綁 arcrun.dev;leo21c 對應 URL 未定 → 保留原樣,見回報)。 const API_BASE = process.env.NEXT_PUBLIC_API_BASE ?? 'https://cypher.arcrun.dev'; type SearchMode = 'semantic' | 'keyword'; // KBDB /entries/search 回傳的 entry(gloss:content = "實體:描述") type SearchEntry = { id: string; content: string; entry_type?: string; owner_id?: string; score?: number; }; type SearchResponse = { entries?: SearchEntry[]; mode?: string; // 實際採用的 mode(semantic 未開會誠實降級 keyword) capability_hint?: string; // 降級時的提示 }; // 繁體異體字正規化(臺→台),讓 query 對得上多用「台」的 KB 內容 function normalizeQuery(q: string): string { return q.replace(/臺/g, '台'); } // gloss content「實體:描述」→ { title, desc }(無冒號則整串當標題) function splitGloss(content: string): { title: string; desc: string } { const m = content.match(/^\s*([^::]+)[::]\s*([\s\S]*)$/); if (m) return { title: m[1].trim(), desc: m[2].trim() }; return { title: content.trim(), desc: '' }; } function SearchInner() { const router = useRouter(); const params = useSearchParams(); const initialQ = params.get('q') ?? ''; const [ready, setReady] = useState(false); // 登入檢查完成 const [input, setInput] = useState(initialQ); const [mode, setMode] = useState('semantic'); const [results, setResults] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [hint, setHint] = useState(null); // 登入 gate(沿用官方 cypher /me;只確認登入,不需 api_key——raw worker 無 auth) useEffect(() => { (async () => { try { const meRes = await fetch(`${API_BASE}/me`, { credentials: 'include' }); if (!meRes.ok) { window.location.href = '/login?redirect=/mira/search'; return; } setReady(true); } catch (e) { setError(e instanceof Error ? e.message : String(e)); } })(); }, []); // 語義/關鍵字搜尋:GET {KBDB_BASE}/entries/search?q=&mode=&limit=12(不帶 owner_id) const runSearch = useCallback(async (rawQuery: string, m: SearchMode) => { const q = normalizeQuery(rawQuery.trim()); if (!q) { setResults(null); setHint(null); return; } setLoading(true); setError(null); setHint(null); try { const url = `${KBDB_BASE}/entries/search?q=${encodeURIComponent(q)}&mode=${m}&limit=12`; const res = await fetch(url); if (!res.ok) { setError(`搜尋失敗:${res.status}`); setResults([]); return; } const data = (await res.json()) as SearchResponse; const entries = (data.entries ?? []) .slice() .sort((a, b) => (b.score ?? 0) - (a.score ?? 0)); // 語義無門檻,照 score 排序顯示前 N setResults(entries); // 誠實降級提示:要了 semantic 卻回 keyword(Vectorize 未開) if (m === 'semantic' && data.mode && data.mode !== 'semantic') { setHint(data.capability_hint ?? '語義索引尚未啟用,暫以關鍵字比對代替。'); } } catch (e) { setError(e instanceof Error ? e.message : String(e)); setResults([]); } finally { setLoading(false); } }, []); // 進站帶 ?q= 且登入完成 → 自動查一次 useEffect(() => { if (ready && initialQ.trim()) runSearch(initialQ, mode); // eslint-disable-next-line react-hooks/exhaustive-deps }, [ready]); const submit = (e: React.FormEvent) => { e.preventDefault(); const q = input.trim(); router.replace(`/mira/search?q=${encodeURIComponent(q)}`); runSearch(q, mode); }; // 切換 default 語義/進階 keyword;已有 query 就重查 const switchMode = (m: SearchMode) => { if (m === mode) return; setMode(m); if (input.trim()) runSearch(input, m); }; return (
← 河道

🔍 搜尋

會打字就能查——{mode === 'semantic' ? '語義搜尋(記大概意思即可)' : '關鍵字精確比對'}

setInput(e.target.value)} placeholder={mode === 'semantic' ? '想找什麼?(例:那個穿皮衣的 AI 老闆、本地模型)' : '關鍵字(精確比對)'} autoFocus />
{/* default 語義/進階 keyword 切換 */}
{error &&
{error}
} {hint &&
ℹ️ {hint}
} {loading &&
搜尋中…
} {!loading && results && (
{mode === 'semantic' ? '🧬 語義結果' : '📇 關鍵字結果'}({results.length})
{results.length === 0 ? (
沒有找到相關結果,換個說法試試。
) : ( results.map(e => ) )}
)}
); } function ResultRow({ entry }: { entry: SearchEntry }) { const { title, desc } = splitGloss(entry.content || ''); const pct = entry.score != null ? Math.round(entry.score * 100) : null; return (
📚 {title} {pct != null && pct > 0 && {pct}%}
{desc &&
{desc}
}
); } export default function MiraSearchPage() { return ( 載入中…}> ); }