|
| 1 | +/** |
| 2 | + * AsrConfusionPanel — ASR 混淆画像闭环面板(v0.20.2 / REQ-269)。 |
| 3 | + * |
| 4 | + * @ai-context: 画像由「离线精修采纳流」自动采集(用户采纳=有标注参考),本面板 |
| 5 | + * 展示达门槛候选(共现才替换、确认制——OCR ocr_confusion 哲学迁移): |
| 6 | + * 确认=进纠错规则(转笔记/预览产物文本应用)+ 正确词反哺流式热词; |
| 7 | + * 忽略=不再推荐(dismissed 可人工编辑 asr_confusion.json 复原)。 |
| 8 | + */ |
| 9 | +import { useCallback, useEffect, useState } from "react"; |
| 10 | +import { invoke } from "@tauri-apps/api/core"; |
| 11 | + |
| 12 | +interface AsrPairView { |
| 13 | + wrong: string; |
| 14 | + right: string; |
| 15 | + count: number; |
| 16 | +} |
| 17 | +interface AsrRuleView { |
| 18 | + from: string; |
| 19 | + to: string; |
| 20 | +} |
| 21 | +interface AsrConfusionView { |
| 22 | + candidates: AsrPairView[]; |
| 23 | + rules: AsrRuleView[]; |
| 24 | + dismissed_count: number; |
| 25 | +} |
| 26 | + |
| 27 | +const rowBtn: React.CSSProperties = { |
| 28 | + padding: "4px 10px", |
| 29 | + cursor: "pointer", |
| 30 | + fontSize: 12, |
| 31 | + borderRadius: 6, |
| 32 | + border: "1px solid #e5e7eb", |
| 33 | + background: "#fff", |
| 34 | + color: "#374151", |
| 35 | +}; |
| 36 | + |
| 37 | +export function AsrConfusionPanel() { |
| 38 | + const [view, setView] = useState<AsrConfusionView | null>(null); |
| 39 | + const [msg, setMsg] = useState(""); |
| 40 | + const [err, setErr] = useState(""); |
| 41 | + |
| 42 | + const reload = useCallback(async () => { |
| 43 | + try { |
| 44 | + setErr(""); |
| 45 | + const v = await invoke<AsrConfusionView>("asr_confusion_get"); |
| 46 | + setView(v); |
| 47 | + } catch (e) { |
| 48 | + setErr(`加载失败: ${e}`); |
| 49 | + } |
| 50 | + }, []); |
| 51 | + |
| 52 | + useEffect(() => { |
| 53 | + void reload(); |
| 54 | + }, [reload]); |
| 55 | + |
| 56 | + const confirm = async (wrong: string, right: string) => { |
| 57 | + try { |
| 58 | + await invoke("asr_confusion_confirm", { wrong, right }); |
| 59 | + setMsg(`✓ 已确认「${wrong}→${right}」:纠错规则生效,正确词已加入 ASR 热词`); |
| 60 | + await reload(); |
| 61 | + } catch (e) { |
| 62 | + setErr(String(e)); |
| 63 | + } |
| 64 | + }; |
| 65 | + |
| 66 | + const dismiss = async (wrong: string, right: string) => { |
| 67 | + try { |
| 68 | + await invoke("asr_confusion_dismiss", { wrong, right }); |
| 69 | + await reload(); |
| 70 | + } catch (e) { |
| 71 | + setErr(String(e)); |
| 72 | + } |
| 73 | + }; |
| 74 | + |
| 75 | + const removeRule = async (from: string) => { |
| 76 | + try { |
| 77 | + await invoke("asr_confusion_remove_rule", { from }); |
| 78 | + setMsg(`已删除规则「${from}」(热词保留,可单独管理)`); |
| 79 | + await reload(); |
| 80 | + } catch (e) { |
| 81 | + setErr(String(e)); |
| 82 | + } |
| 83 | + }; |
| 84 | + |
| 85 | + const candidates = view?.candidates ?? []; |
| 86 | + |
| 87 | + return ( |
| 88 | + <div> |
| 89 | + <div style={{ fontSize: 12, color: "#374151", marginBottom: 8 }}> |
| 90 | + <b>ASR 同音混淆闭环</b>{" "} |
| 91 | + <span style={{ color: "#9ca3af", fontSize: 11 }}> |
| 92 | + 画像来自离线精修采纳流;确认=纠错规则(共现才替换)+ 反哺热词;asr_confusion.json 可校准 |
| 93 | + </span> |
| 94 | + </div> |
| 95 | + {msg && ( |
| 96 | + <div style={{ fontSize: 11, color: "#047857", background: "#ecfdf5", border: "1px solid #6ee7b7", borderRadius: 6, padding: "4px 8px", marginBottom: 6 }}> |
| 97 | + {msg} |
| 98 | + </div> |
| 99 | + )} |
| 100 | + {err && ( |
| 101 | + <div style={{ fontSize: 11, color: "#dc2626", background: "#fef2f2", border: "1px solid #fca5a5", borderRadius: 6, padding: "4px 8px", marginBottom: 6 }}> |
| 102 | + {err} |
| 103 | + </div> |
| 104 | + )} |
| 105 | + <div style={{ fontSize: 12, marginBottom: 4 }}> |
| 106 | + 待确认候选({candidates.length}) |
| 107 | + {view != null && view.dismissed_count > 0 && ( |
| 108 | + <span style={{ color: "#9ca3af", fontSize: 11, marginLeft: 6 }}>已忽略 {view.dismissed_count} 条</span> |
| 109 | + )} |
| 110 | + </div> |
| 111 | + {candidates.length === 0 ? ( |
| 112 | + <div style={{ fontSize: 12, color: "#9ca3af" }}> |
| 113 | + {view === null ? "加载中…" : "暂无新候选——采纳离线精修草稿后自动积累画像(≥2 次出现才提名)"} |
| 114 | + </div> |
| 115 | + ) : ( |
| 116 | + <div style={{ display: "flex", flexDirection: "column", gap: 4 }}> |
| 117 | + {candidates.map((c) => ( |
| 118 | + <div key={`${c.wrong}|${c.right}`} style={{ display: "flex", gap: 6, alignItems: "center", fontSize: 12 }}> |
| 119 | + <span style={{ background: "#fff7ed", border: "1px solid #fdba74", borderRadius: 4, padding: "1px 6px" }}> |
| 120 | + {c.wrong} |
| 121 | + </span> |
| 122 | + <span style={{ color: "#9ca3af" }}>→</span> |
| 123 | + <span style={{ background: "#ecfdf5", border: "1px solid #6ee7b7", borderRadius: 4, padding: "1px 6px" }}> |
| 124 | + {c.right} |
| 125 | + </span> |
| 126 | + <span style={{ color: "#9ca3af", fontSize: 11 }}>×{c.count}</span> |
| 127 | + <span style={{ marginLeft: "auto", display: "flex", gap: 4 }}> |
| 128 | + <button style={{ ...rowBtn, color: "#047857", borderColor: "#6ee7b7" }} onClick={() => void confirm(c.wrong, c.right)}> |
| 129 | + 确认纠错+热词 |
| 130 | + </button> |
| 131 | + <button style={rowBtn} onClick={() => void dismiss(c.wrong, c.right)}> |
| 132 | + 忽略 |
| 133 | + </button> |
| 134 | + </span> |
| 135 | + </div> |
| 136 | + ))} |
| 137 | + </div> |
| 138 | + )} |
| 139 | + |
| 140 | + {(view?.rules.length ?? 0) > 0 && ( |
| 141 | + <> |
| 142 | + <div style={{ fontSize: 12, margin: "10px 0 4px" }}>已确认规则({view?.rules.length})</div> |
| 143 | + <div style={{ display: "flex", flexDirection: "column", gap: 4 }}> |
| 144 | + {view?.rules.map((r) => ( |
| 145 | + <div key={r.from} style={{ display: "flex", gap: 6, alignItems: "center", fontSize: 12 }}> |
| 146 | + <span style={{ color: "#6b7280" }}>{r.from} → {r.to}</span> |
| 147 | + <button style={{ ...rowBtn, marginLeft: "auto", color: "#dc2626" }} onClick={() => void removeRule(r.from)}> |
| 148 | + 删除 |
| 149 | + </button> |
| 150 | + </div> |
| 151 | + ))} |
| 152 | + </div> |
| 153 | + </> |
| 154 | + )} |
| 155 | + </div> |
| 156 | + ); |
| 157 | +} |
| 158 | + |
| 159 | +export default AsrConfusionPanel; |
0 commit comments