Skip to content

Commit cb28f22

Browse files
committed
fix(web): IPC camelCase 契约对齐(SOP/行动/完成史/文章/校对/练习)
1 parent a3a3b48 commit cb28f22

8 files changed

Lines changed: 156 additions & 100 deletions

‎app/src/components/ActionCenterOverlay.tsx‎

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,38 +14,39 @@ import { invoke } from "@tauri-apps/api/core";
1414
import SopRunOverlay from "./SopRunOverlay";
1515
import { PracticeOverlay, QuestionsOverlay } from "./PracticeQuestionsOverlays";
1616

17+
/** 响应结构(SopTemplate/ActionQueueRow/CompletionEvent 均 serde camelCase——字段须 camel 读取) */
1718
interface SopTemplateView {
1819
id: number;
19-
note_id: number;
20+
noteId: number;
2021
name: string;
21-
start_line: number;
22-
end_line: number;
22+
startLine: number;
23+
endLine: number;
2324
mode: string;
24-
note_title: string;
25+
noteTitle: string;
2526
}
2627

2728
interface ActionRow {
2829
id: number;
29-
note_id: number;
30-
line_no: number;
30+
noteId: number;
31+
lineNo: number;
3132
text: string;
3233
status: string;
3334
unrefined: boolean;
34-
plan_date: number | null;
35+
planDate: number | null;
3536
disposition: string | null;
36-
note_title: string;
37+
noteTitle: string;
3738
}
3839

3940
interface HistoryRow {
4041
id: number;
4142
ts: number;
42-
event_type: string;
43-
source_type: string;
44-
source_id: number | null;
45-
note_id: number | null;
43+
eventType: string;
44+
sourceType: string;
45+
sourceId: number | null;
46+
noteId: number | null;
4647
text: string;
4748
note: string | null;
48-
meta_json: string | null;
49+
metaJson: string | null;
4950
}
5051

5152
interface Props {
@@ -324,7 +325,7 @@ export default function ActionCenterOverlay({ onClose, onChanged }: Props) {
324325
/** 迁出=放手:复制 todo.txt 行到剪贴板(保底三件套之复制面) */
325326
const exportCopy = async (r: ActionRow) => {
326327
try {
327-
const line = `[ ] ${r.text} (via:${r.note_title})`;
328+
const line = `[ ] ${r.text} (via:${r.noteTitle})`;
328329
await navigator.clipboard.writeText(line);
329330
setMsg(`⤴ 已复制 todo.txt 行(外部完成后可到完成史手动回填):${line}`);
330331
} catch (e) {
@@ -336,7 +337,7 @@ export default function ActionCenterOverlay({ onClose, onChanged }: Props) {
336337
<div key={r.id} style={{ border: overdueFlag ? "1px solid #fca5a5" : "1px solid #e5e7eb", background: overdueFlag ? "#fff7f7" : "#fff", borderRadius: 6, padding: "6px 8px", display: "flex", flexDirection: "column", gap: 4 }}>
337338
<div style={{ display: "flex", gap: 6, alignItems: "center" }}>
338339
<span style={{ fontSize: 12.5, color: "#111827", flex: 1 }}>{r.text}</span>
339-
<span style={{ fontSize: 10.5, color: "#9ca3af" }}>@{r.note_title}</span>
340+
<span style={{ fontSize: 10.5, color: "#9ca3af" }}>@{r.noteTitle}</span>
340341
{overdueFlag && <span style={{ fontSize: 10.5, color: "#dc2626", fontWeight: 600 }}>逾期</span>}
341342
</div>
342343
{reasonFor === r.id && (
@@ -438,7 +439,7 @@ export default function ActionCenterOverlay({ onClose, onChanged }: Props) {
438439
<label key={r.id} style={{ display: "flex", gap: 6, alignItems: "center", fontSize: 12, cursor: "pointer", padding: "2px 4px", borderRadius: 4, background: checked.has(r.id) ? "#f5f3ff" : "transparent" }}>
439440
<input type="checkbox" checked={checked.has(r.id)} onChange={() => toggleChecked(r.id)} />
440441
<span style={{ color: "#111827" }}>{r.text}</span>
441-
<span style={{ fontSize: 10.5, color: "#9ca3af", marginLeft: "auto" }}>@{r.note_title}</span>
442+
<span style={{ fontSize: 10.5, color: "#9ca3af", marginLeft: "auto" }}>@{r.noteTitle}</span>
442443
</label>
443444
))}
444445
</div>
@@ -460,7 +461,7 @@ export default function ActionCenterOverlay({ onClose, onChanged }: Props) {
460461
{unrefined.length === 0 ? empty("无待提炼产物行") : unrefined.map((r) => (
461462
<div key={r.id} style={{ border: "1px solid #e5e7eb", borderRadius: 6, padding: "6px 8px", display: "flex", gap: 6, alignItems: "center" }}>
462463
<span style={{ fontSize: 12.5, flex: 1 }}>☑️ {r.text}</span>
463-
<span style={{ fontSize: 10.5, color: "#9ca3af" }}>@{r.note_title}</span>
464+
<span style={{ fontSize: 10.5, color: "#9ca3af" }}>@{r.noteTitle}</span>
464465
<button style={{ ...okBtn, fontSize: 11 }} onClick={() => void refineUnrefined(r.id)}>提炼为任务行</button>
465466
</div>
466467
))}
@@ -471,8 +472,8 @@ export default function ActionCenterOverlay({ onClose, onChanged }: Props) {
471472
{history.length === 0 ? empty("暂无完成记录——完成即证据,周回顾原料在此沉淀") : history.map((h) => (
472473
<div key={h.id} style={{ display: "flex", gap: 8, fontSize: 12, borderBottom: "1px solid #f3f4f6", padding: "4px 2px" }}>
473474
<span style={{ color: "#9ca3af", fontVariantNumeric: "tabular-nums", flexShrink: 0 }}>{fmtDate(h.ts)}</span>
474-
<span style={{ color: h.event_type === "abandoned" ? "#dc2626" : "#0f766e", flexShrink: 0, width: 90 }}>
475-
{TYPE_LABEL[h.event_type] ?? h.event_type}
475+
<span style={{ color: h.eventType === "abandoned" ? "#dc2626" : "#0f766e", flexShrink: 0, width: 90 }}>
476+
{TYPE_LABEL[h.eventType] ?? h.eventType}
476477
</span>
477478
<span style={{ flex: 1, color: "#374151" }}>{h.text}</span>
478479
{h.note && <span style={{ fontSize: 11, color: "#9ca3af" }}>因:{h.note}</span>}
@@ -511,7 +512,7 @@ export default function ActionCenterOverlay({ onClose, onChanged }: Props) {
511512
<div key={t.id} style={{ border: "1px solid #e5e7eb", borderRadius: 8, padding: "6px 10px" }}>
512513
<div style={{ display: "flex", gap: 6, alignItems: "center" }}>
513514
<span style={{ fontSize: 12.5, fontWeight: 600, color: "#111827" }}>{t.name}</span>
514-
<span style={{ fontSize: 11, color: "#9ca3af" }}>@{t.note_title} · 行 {t.start_line}–{t.end_line} · {t.mode === "confirm" ? "总览核对" : "逐步引导"}</span>
515+
<span style={{ fontSize: 11, color: "#9ca3af" }}>@{t.noteTitle} · 行 {t.startLine}–{t.endLine} · {t.mode === "confirm" ? "总览核对" : "逐步引导"}</span>
515516
<span style={{ marginLeft: "auto", display: "flex", gap: 4 }}>
516517
<button style={okBtn} onClick={() => setActiveTemplate(t)}>▶ 执行</button>
517518
<button style={ghostBtn} onClick={() => void fetchSuggestions(t.id)}>💡 修订建议</button>

‎app/src/components/ModelCardFromNoteDialog.tsx‎

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* 笔记摘录草稿(应用案例留空——复习面/卡编辑完善);笔记未归组
77
* → 后端引导先归组(组=唯一容器)。
88
*/
9-
import { useState } from "react";
9+
import { useEffect, useRef, useState } from "react";
1010
import { invoke } from "@tauri-apps/api/core";
1111

1212
interface Props {
@@ -40,15 +40,26 @@ export default function ModelCardFromNoteDialog({ noteId, noteTitle, onClose, on
4040
const [err, setErr] = useState("");
4141
const [msg, setMsg] = useState("");
4242
const [busy, setBusy] = useState(false);
43+
const [created, setCreated] = useState(false);
44+
// 成功后延迟关闭的 timer(unmount 清理——防已关闭后二次回调/泄漏)
45+
const closeTimerRef = useRef<number | null>(null);
46+
47+
useEffect(() => {
48+
return () => {
49+
if (closeTimerRef.current !== null) window.clearTimeout(closeTimerRef.current);
50+
};
51+
}, []);
4352

4453
const create = async () => {
4554
setBusy(true);
4655
setErr("");
4756
try {
4857
await invoke("model_card_from_note", { noteId, name, excerpt: excerpt.trim() || null });
4958
setMsg("✓ 已建模型卡草稿(组内 model 卡;复习面/卡编辑可继续完善定义与应用案例)");
59+
setCreated(true);
5060
onCreated?.();
51-
setTimeout(onClose, 900);
61+
// 成功后禁用按钮(含取消)防二次提交/先关后关;900ms 后自动关闭
62+
closeTimerRef.current = window.setTimeout(onClose, 900);
5263
} catch (e) {
5364
setErr(String(e));
5465
} finally {
@@ -62,7 +73,7 @@ export default function ModelCardFromNoteDialog({ noteId, noteTitle, onClose, on
6273
<div style={{ display: "flex", alignItems: "center", marginBottom: 10 }}>
6374
<h3 style={{ margin: 0, fontSize: 15 }}>🧠 提炼模型卡草稿</h3>
6475
<span style={{ fontSize: 11, color: "#6b7280", marginLeft: 8 }}>来源:{noteTitle.slice(0, 24)}</span>
65-
<button style={{ ...btn, marginLeft: "auto" }} onClick={onClose}>取消</button>
76+
<button style={{ ...btn, marginLeft: "auto" }} onClick={onClose} disabled={created}>取消</button>
6677
</div>
6778
{msg && <div style={{ fontSize: 12, color: "#047857", marginBottom: 8 }}>{msg}</div>}
6879
{err && <div style={{ fontSize: 12, color: "#dc2626", marginBottom: 8 }}>{err}</div>}
@@ -78,7 +89,9 @@ export default function ModelCardFromNoteDialog({ noteId, noteTitle, onClose, on
7889
<textarea value={excerpt} onChange={(e) => setExcerpt(e.target.value)} rows={4} placeholder="粘贴笔记中的模型表述(≤200 字)…" style={{ width: "100%", boxSizing: "border-box", fontSize: 12, border: "1px solid #e5e7eb", borderRadius: 4, padding: "4px 6px", resize: "vertical" }} />
7990
</div>
8091
<div style={{ display: "flex", justifyContent: "flex-end", gap: 6 }}>
81-
<button style={btn} disabled={busy} onClick={() => void create()}>创建草稿</button>
92+
<button style={btn} disabled={busy || created} onClick={() => void create()}>
93+
{created ? "已创建,即将关闭…" : "创建草稿"}
94+
</button>
8295
</div>
8396
</div>
8497
</div>

‎app/src/components/PracticeQuestionsOverlays.tsx‎

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,26 @@
99
import { useCallback, useEffect, useState } from "react";
1010
import { invoke } from "@tauri-apps/api/core";
1111

12+
/** 响应结构(PracticeItem/QuestionItem 均 serde camelCase——字段须 camel 读取) */
1213
interface PracticeView {
1314
id: number;
14-
note_id: number | null;
15-
kb_concept_id: number | null;
15+
noteId: number | null;
16+
kbConceptId: number | null;
1617
text: string;
1718
frequency: string;
1819
goal: string | null;
1920
mastery: number | null;
20-
next_due: number | null;
21+
nextDue: number | null;
2122
status: string;
2223
}
2324
interface QuestionView {
2425
id: number;
25-
note_id: number | null;
26-
kb_concept_id: number | null;
26+
noteId: number | null;
27+
kbConceptId: number | null;
2728
text: string;
2829
context: string | null;
2930
status: string;
30-
answer_ref: string | null;
31+
answerRef: string | null;
3132
}
3233

3334
const overlayStyle: React.CSSProperties = {
@@ -109,7 +110,7 @@ export function PracticeOverlay({ onClose }: { onClose: () => void }) {
109110
}
110111
};
111112

112-
const dueCount = items.filter((i) => i.next_due != null && i.next_due <= Math.floor(Date.now() / 1000)).length;
113+
const dueCount = items.filter((i) => i.nextDue != null && i.nextDue <= Math.floor(Date.now() / 1000)).length;
113114

114115
return shell("🎯 练习条目", `该练了 ${dueCount} · 闪卡之外第二条复利曲线(宽容缺勤只记史)`, onClose, (
115116
<>
@@ -133,7 +134,7 @@ export function PracticeOverlay({ onClose }: { onClose: () => void }) {
133134
<span style={{ fontSize: 10.5, color: "#9ca3af" }}>
134135
{it.frequency === "daily" ? "每日" : "手动"}
135136
{it.mastery != null ? ` · 熟练 ${it.mastery}/5` : ""}
136-
{it.next_due != null ? ` · 下次 ${new Date(it.next_due * 1000).toLocaleDateString()}` : ""}
137+
{it.nextDue != null ? ` · 下次 ${new Date(it.nextDue * 1000).toLocaleDateString()}` : ""}
137138
</span>
138139
<button style={okBtn} onClick={() => void tick(it)}>🎯 打点</button>
139140
</div>
@@ -237,7 +238,7 @@ export function QuestionsOverlay({ onClose }: { onClose: () => void }) {
237238
{answered.map((q) => (
238239
<div key={q.id} style={{ fontSize: 12, color: "#6b7280", display: "flex", gap: 6 }}>
239240
<span>✓ {q.text}</span>
240-
{q.answer_ref && <span style={{ fontSize: 10.5, color: "#9ca3af" }}>→ {q.answer_ref}</span>}
241+
{q.answerRef && <span style={{ fontSize: 10.5, color: "#9ca3af" }}>→ {q.answerRef}</span>}
241242
</div>
242243
))}
243244
</div>

‎app/src/components/ProofreadPanel.tsx‎

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,20 @@ import { useCallback, useEffect, useState } from "react";
1010
import { invoke } from "@tauri-apps/api/core";
1111
import type { RefineDraftView, SecondPassView } from "./SecondPassPanel";
1212

13+
/** 响应结构(ProofreadEstimateView/ProofreadRunView 均 serde camelCase——字段须 camel 读取) */
1314
interface EstimateView {
1415
sentences: number;
1516
chars: number;
16-
cost_yuan: number;
17+
costYuan: number;
1718
model: string;
1819
capped: boolean;
1920
}
2021

2122
interface RunView {
22-
draft_count: number;
23-
suggestions_received: number;
23+
draftCount: number;
24+
suggestionsReceived: number;
2425
chars: number;
25-
cost_yuan: number;
26+
costYuan: number;
2627
model: string;
2728
capped: boolean;
2829
}
@@ -70,17 +71,18 @@ export default function ProofreadPanel({ sessionId, onClose }: Props) {
7071
const [msg, setMsg] = useState("");
7172

7273
const reload = useCallback(async () => {
73-
try {
74-
setErr("");
75-
const [e, l] = await Promise.all([
76-
invoke<EstimateView>("proofread_estimate", { sessionId }),
77-
invoke<SecondPassView>("proofread_list", { sessionId }),
78-
]);
79-
setEst(e);
80-
setList(l);
81-
} catch (e) {
82-
setErr(`加载失败: ${e}`);
83-
}
74+
setErr("");
75+
// allSettled:单侧失败仍刷新成功侧(run 完成消息不因刷新失败被吞)
76+
const [eR, lR] = await Promise.allSettled([
77+
invoke<EstimateView>("proofread_estimate", { sessionId }),
78+
invoke<SecondPassView>("proofread_list", { sessionId }),
79+
]);
80+
if (eR.status === "fulfilled") setEst(eR.value);
81+
if (lR.status === "fulfilled") setList(lR.value);
82+
const reasons = [eR, lR]
83+
.filter((r): r is PromiseRejectedResult => r.status === "rejected")
84+
.map((r) => String(r.reason));
85+
if (reasons.length > 0) setErr(`刷新失败: ${reasons.join(";")}`);
8486
}, [sessionId]);
8587

8688
useEffect(() => {
@@ -95,17 +97,20 @@ export default function ProofreadPanel({ sessionId, onClose }: Props) {
9597
setBusy(true);
9698
setErr("");
9799
setMsg("校对进行中(仅文本上云;长会话分块多次请求)…");
100+
let ok = false;
98101
try {
99102
const v = await invoke<RunView>("proofread_run", { sessionId, authorized: true });
103+
ok = true;
100104
setMsg(
101-
`校对完成:${v.suggestions_received} 条建议 → ${v.draft_count} 条待裁决草稿(预估 ¥${v.cost_yuan.toFixed(4)})${v.capped ? "(超出 240 句部分未校对)" : ""}`,
105+
`校对完成:${v.suggestionsReceived} 条建议 → ${v.draftCount} 条待裁决草稿(预估 ¥${v.costYuan.toFixed(4)})${v.capped ? "(超出 240 句部分未校对)" : ""}`,
102106
);
103-
await reload();
104107
} catch (e) {
105108
setErr(String(e));
106109
setMsg("");
107110
} finally {
108111
setBusy(false);
112+
// 成功路径必刷新草稿列表;reload 内部容错(allSettled),刷新失败不吞完成消息
113+
if (ok) await reload();
109114
}
110115
};
111116

@@ -148,7 +153,7 @@ export default function ProofreadPanel({ sessionId, onClose }: Props) {
148153
<div style={{ fontSize: 12, color: "#374151" }}>
149154
候选句:<b>{est?.sentences ?? "…"}</b> 句(约 {est?.chars ?? "…"} 字符)
150155
{est?.capped ? <span style={{ color: "#b45309" }}>(已超 240 句护栏,超出部分本次不校对)</span> : ""}
151-
,预估 <b>¥{(est?.cost_yuan ?? 0).toFixed(4)}</b>(模型 {est?.model ?? "…"})
156+
,预估 <b>¥{(est?.costYuan ?? 0).toFixed(4)}</b>(模型 {est?.model ?? "…"})
152157
</div>
153158
<label style={{ fontSize: 12, display: "flex", gap: 6, alignItems: "center", cursor: "pointer" }}>
154159
<input type="checkbox" checked={consent} onChange={(e) => setConsent(e.target.checked)} />

‎app/src/components/ProofreadToggle.tsx‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@
77
import { useEffect, useState } from "react";
88
import { invoke } from "@tauri-apps/api/core";
99

10+
/** AiSettingsView 响应结构(serde camelCase——proofreadEnabled 须 camel 读取) */
1011
interface AiSettingsView {
1112
enabled: boolean;
1213
authorized: boolean;
13-
proofread_enabled: boolean;
14+
proofreadEnabled: boolean;
1415
}
1516

1617
const btn: React.CSSProperties = { padding: "5px 10px", cursor: "pointer", fontSize: 12, borderRadius: 6 };
@@ -25,7 +26,7 @@ export function ProofreadToggle() {
2526
useEffect(() => {
2627
void invoke<AiSettingsView>("ai_get_settings")
2728
.then((v) => {
28-
setOn(v.proofread_enabled);
29+
setOn(v.proofreadEnabled);
2930
setGlobalEnabled(v.enabled);
3031
setAuthorized(v.authorized);
3132
})
@@ -35,15 +36,14 @@ export function ProofreadToggle() {
3536
const toggle = async (next: boolean) => {
3637
setErr("");
3738
setMsg("");
38-
if (next && !globalEnabled) {
39-
setMsg("提示:还需开启上方「AI 功能」全局开关(默认关——授权红线)");
40-
}
4139
try {
4240
await invoke("ai_set_proofread", { enabled: next });
4341
setOn(next);
44-
setMsg(next ? "已开启文本校对(建议制:机器只给建议,由你裁决采纳)" : "已关闭文本校对(转写链路零影响)");
42+
// 成功态与错误态分开:门未开属引导提示,附在成功文案后而非当错误
43+
const gateHint = next && !globalEnabled ? "(提示:还需开启「AI 功能」全局开关方可生效——默认关授权红线)" : "";
44+
setMsg(next ? `已开启文本校对(建议制:机器只给建议,由你裁决采纳)${gateHint}` : "已关闭文本校对(转写链路零影响)");
4545
} catch (e) {
46-
setErr(String(e));
46+
setErr(`切换失败: ${e}`);
4747
}
4848
};
4949

0 commit comments

Comments
 (0)