22 * LearningLibraryPanel — 设置页「学习库」段(v0.19 REQ-258/REQ-260/REQ-262)。
33 *
44 * @ai -context: 检索与发现层的治理面:①引擎状态如实展示(FTS5 恒可用;
5- * embedding v0.19.3 前恒"未接入"——诚实不装);②「学习库问答
6- * 生成」开关 + 预算档位(双闸门之二,默认关——命中列表不受
7- * 其约束);③索引统计/全量重建(脏源/失败角标可见 + 重建兜底;
8- * 进度经 Channel 事件流——与聊天流同构,不做全局监听)。
5+ * embedding v0.19.5 起按引擎槽与回填态展示——诚实不装);②「学习
6+ * 库问答生成」开关 + 预算档位(双闸门之二,默认关——命中列表
7+ * 不受其约束);③索引统计/全量重建(脏源/失败角标可见 + 重建兜
8+ * 底;进度经 Channel 事件流——与聊天流同构);④v0.19.5(REQ-259/
9+ * REQ-262):本地语义引擎治理——状态/下载(hf-mirror 双源)/
10+ * 加载(换槽)/重建回填引导。
911 */
1012import { useCallback , useEffect , useState } from "react" ;
1113import { Channel , invoke } from "@tauri-apps/api/core" ;
1214import type { AiSettingsView } from "../types/ai" ;
1315import type { KbIndexStats , KbReindexEvent } from "../types/kb" ;
1416
17+ /** kb_embedding_status/load 载荷(Rust EmbeddingStatusView camelCase 契约) */
18+ interface EmbeddingStatusView {
19+ kind : string ;
20+ ready : boolean ;
21+ dim : number | null ;
22+ modelDir : string ;
23+ detail : string ;
24+ }
25+
1526const TIERS = [
1627 { id : "light" , label : "轻量(~4K)" } ,
1728 { id : "standard" , label : "标准(~10K,推荐)" } ,
@@ -27,6 +38,17 @@ export default function LearningLibraryPanel({ active = true }: { active?: boole
2738 const [ msg , setMsg ] = useState < { kind : "ok" | "err" ; text : string } | null > ( null ) ;
2839 const [ busy , setBusy ] = useState ( false ) ;
2940 const [ progress , setProgress ] = useState < { done : number ; total : number } | null > ( null ) ;
41+ // REQ-259(v0.19.5):本地语义引擎状态(noop=未配置;onnx=就绪)
42+ const [ emb , setEmb ] = useState < EmbeddingStatusView | null > ( null ) ;
43+ const [ engBusy , setEngBusy ] = useState ( false ) ;
44+
45+ const loadEmb = useCallback ( async ( ) => {
46+ try {
47+ setEmb ( await invoke < EmbeddingStatusView > ( "kb_embedding_status" ) ) ;
48+ } catch ( e ) {
49+ setMsg ( { kind : "err" , text : `语义引擎状态读取失败: ${ e } ` } ) ;
50+ }
51+ } , [ ] ) ;
3052
3153 const loadStats = useCallback ( async ( ) => {
3254 try {
@@ -53,7 +75,8 @@ export default function LearningLibraryPanel({ active = true }: { active?: boole
5375 }
5476 } ) ( ) ;
5577 void loadStats ( ) ;
56- } , [ loadStats ] ) ;
78+ void loadEmb ( ) ;
79+ } , [ loadStats , loadEmb ] ) ;
5780
5881 // 轻量轮询(审查 L2 修复):设置页随应用常驻挂载(display:none 不卸载)——
5982 // 轮询只在页面可见时运行(对齐 SessionsPage active 门控先例;进入设置页即
@@ -142,22 +165,88 @@ export default function LearningLibraryPanel({ active = true }: { active?: boole
142165 stats != null &&
143166 ( stats . dirtySources > 0 || ( stats . errorCount > 0 || stats . indexVersion !== stats . currentIndexVersion ) ) ;
144167
168+ /** REQ-259:加载本地语义引擎(换槽;成功后引导重建回填向量) */
169+ const loadEngine = async ( ) => {
170+ if ( engBusy ) return ;
171+ setEngBusy ( true ) ;
172+ setMsg ( null ) ;
173+ try {
174+ const v = await invoke < EmbeddingStatusView > ( "kb_embedding_load" ) ;
175+ setEmb ( v ) ;
176+ setMsg ( { kind : "ok" , text : v . detail } ) ;
177+ } catch ( e ) {
178+ setMsg ( { kind : "err" , text : `语义引擎加载失败: ${ e } ` } ) ;
179+ } finally {
180+ setEngBusy ( false ) ;
181+ }
182+ } ;
183+
184+ /** REQ-262:按需下载 bge 模型(model_quantized.onnx + vocab.txt) */
185+ const downloadEngine = async ( ) => {
186+ if ( engBusy ) return ;
187+ setEngBusy ( true ) ;
188+ setMsg ( null ) ;
189+ try {
190+ const text = await invoke < string > ( "kb_embedding_download" ) ;
191+ setMsg ( { kind : "ok" , text } ) ;
192+ } catch ( e ) {
193+ setMsg ( { kind : "err" , text : `模型下载失败: ${ e } ` } ) ;
194+ } finally {
195+ setEngBusy ( false ) ;
196+ }
197+ } ;
198+
145199 return (
146200 < div data-testid = "learning-library-panel" style = { { padding : 8 } } >
147- { /* 引擎状态(如实——FTS5 恒可用;embedding 未接入不伪装 ) */ }
201+ { /* 引擎状态(如实——FTS5 恒可用;语义引擎按槽位/回填态展示 ) */ }
148202 < div style = { { display : "flex" , gap : 8 , flexWrap : "wrap" , marginBottom : 6 , fontSize : 12 } } >
149203 < span style = { { color : "#047857" , background : "#ecfdf5" , border : "1px solid #a7f3d0" , borderRadius : 10 , padding : "1px 8px" } } >
150204 ✓ 本地词法引擎(FTS5)就绪
151205 </ span >
152206 { stats ?. embeddingReady ? (
153- < span style = { { color : "#047857" , background : "#ecfdf5" , borderRadius : 10 , padding : "1px 8px" } } > ✓ 语义检索已接入</ span >
207+ < span style = { { color : "#047857" , background : "#ecfdf5" , borderRadius : 10 , padding : "1px 8px" } } > ✓ 语义检索已接入( { stats . engine } ) </ span >
154208 ) : (
155- < span style = { { color : "#6b7280" , background : "#f3f4f6" , borderRadius : 10 , padding : "1px 8px" } } title = "语义检索接入属 v0.19.4(embedding 选型 spike 后)——当前按词法精度工作,能力如实 " >
156- ○ 语义检索未接入 (当前词法精度)
209+ < span style = { { color : "#6b7280" , background : "#f3f4f6" , borderRadius : 10 , padding : "1px 8px" } } title = "下载模型并加载后全量重建即回填向量 " >
210+ ○ 语义检索未回填 (当前词法精度)
157211 </ span >
158212 ) }
159213 </ div >
160214
215+ { /* v0.19.5(REQ-259/262):本地语义引擎治理——状态 / 下载 / 加载 / 重建引导 */ }
216+ < div style = { { display : "flex" , alignItems : "center" , gap : 8 , flexWrap : "wrap" , marginBottom : 8 , fontSize : 11.5 } } >
217+ < span style = { { fontWeight : 600 , fontSize : 12 , color : "#374151" } } >
218+ 🧬 本地语义引擎(bge-small-zh)
219+ </ span >
220+ { emb ?. ready ? (
221+ < span data-testid = "kb-emb-ready" style = { { color : "#047857" } } >
222+ ✓ 就绪(dim={ emb . dim } )
223+ </ span >
224+ ) : (
225+ < span data-testid = "kb-emb-noop" style = { { color : "#9ca3af" } } title = { emb ?. modelDir ?? "模型目录未探测" } >
226+ ○ { emb ?. detail ?? "状态读取中…" }
227+ </ span >
228+ ) }
229+ < span style = { { flex : 1 } } />
230+ < button
231+ data-testid = "kb-emb-download"
232+ onClick = { ( ) => void downloadEngine ( ) }
233+ disabled = { engBusy || emb ?. ready }
234+ title = "按需下载 model_quantized.onnx 与 vocab.txt(约 25MB,hf-mirror)"
235+ style = { { fontSize : 12 , cursor : emb ?. ready || engBusy ? "default" : "pointer" , padding : "3px 10px" , borderRadius : 6 , border : "1px solid #d1d5db" , background : "#fff" , color : emb ?. ready ? "#9ca3af" : "#374151" } }
236+ >
237+ { engBusy ? "处理中…" : emb ?. ready ? "已下载" : "⬇ 下载模型" }
238+ </ button >
239+ < button
240+ data-testid = "kb-emb-load"
241+ onClick = { ( ) => void loadEngine ( ) }
242+ disabled = { engBusy || emb ?. ready }
243+ title = "从模型目录加载引擎并换槽(加载成功后点「🔄 全量重建」回填向量)"
244+ style = { { fontSize : 12 , cursor : emb ?. ready || engBusy ? "default" : "pointer" , padding : "3px 10px" , borderRadius : 6 , border : "1px solid #0d9488" , background : emb ?. ready ? "#f0fdfa" : "#fff" , color : emb ?. ready ? "#9ca3af" : "#0f766e" } }
245+ >
246+ { emb ?. ready ? "已加载" : "▶ 加载引擎" }
247+ </ button >
248+ </ div >
249+
161250 { /* 生成开关 + 预算档位 */ }
162251 < div style = { { display : "flex" , alignItems : "center" , gap : 8 , marginBottom : 4 , flexWrap : "wrap" } } >
163252 < span style = { { fontWeight : 600 , fontSize : 12 , color : "#374151" } } > 📚 学习库问答生成</ span >
0 commit comments