Skip to content

Commit 05494f4

Browse files
committed
feat(asr): REQ-269 混淆候选确认面板(设置页 ASR 纠错)
1 parent 9612216 commit 05494f4

3 files changed

Lines changed: 168 additions & 1 deletion

File tree

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
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;

‎app/src/components/SecondPassPanel.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ export default function SecondPassPanel({ sessionId, onChanged, onClose }: Props
157157

158158
const decide = async (draftId: number, adopt: boolean) => {
159159
try {
160-
await invoke("second_pass_decide", { draftId, adopt });
160+
await invoke("second_pass_decide", { sessionId, draftId, adopt });
161161
onChanged?.();
162162
await reload();
163163
} catch (e) {

‎app/src/pages/SettingsPage.tsx‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import LearningLibraryPanel from "../components/LearningLibraryPanel";
2525
import AiProviderSettings from "../components/AiProviderSettings";
2626
import AiTaskPanel from "../components/AiTaskPanel";
2727
import FeatureFlagSetting from "../components/FeatureFlagSetting";
28+
// v0.20.2(REQ-269):ASR 混淆画像闭环面板(候选确认制/规则管理)
29+
import AsrConfusionPanel from "../components/AsrConfusionPanel";
2830

2931
const panel: React.CSSProperties = { border: "1px solid #e5e7eb", borderRadius: 8, padding: 12 };
3032
const groupTitle: React.CSSProperties = {
@@ -107,6 +109,12 @@ export default function SettingsPage({ active = true }: Props) {
107109
<div style={{ ...panel, marginBottom: 4 }}>
108110
<VocabManager />
109111
</div>
112+
113+
{/* v0.20.2(REQ-269):ASR 同音混淆闭环(画像候选/确认制/规则管理) */}
114+
<GroupTitle>ASR 纠错</GroupTitle>
115+
<div style={{ ...panel, marginBottom: 4 }}>
116+
<AsrConfusionPanel />
117+
</div>
110118

111119
{/* ── 功能预览(默认关的新能力开关,v4 §11.3)── */}
112120
<GroupTitle>功能预览</GroupTitle>

0 commit comments

Comments
 (0)