Skip to content

Commit c513442

Browse files
committed
feat(kb): REQ-302 笔记段到模型卡草稿接线
1 parent 1ac11ba commit c513442

4 files changed

Lines changed: 151 additions & 0 deletions

File tree

‎app/src-tauri/src/commands_knowledge_cards.rs‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,3 +132,39 @@ pub(crate) fn list_group_cards_inner(
132132
#[cfg(test)]
133133
#[path = "commands_knowledge_cards_tests.rs"]
134134
mod tests;
135+
136+
/// 笔记段 → 模型卡草稿接线(v0.20.3 / REQ-302,核心处理 γ M 行)。
137+
///
138+
/// @ai-context: 防双轨核对(REQ-302 前置):model 卡唯一生成路径 =
139+
/// create_model_card_inner(本文件,幂等同组同 front)——本命令
140+
/// 复用同一 inner,只是把「来源笔记段摘录」作为初始定义草稿载入
141+
/// (应用案例留空——用户到卡编辑完善);与既有「model 卡纳入
142+
/// 体系」(promote_card_to_concept 升格链)不重复。
143+
/// @ai-context: 组=唯一容器——笔记需已归组(无组笔记引导先归组,防孤儿卡)。
144+
#[tauri::command]
145+
pub fn model_card_from_note(
146+
state: State<'_, AppState>,
147+
note_id: i64,
148+
name: String,
149+
excerpt: Option<String>,
150+
) -> Result<Flashcard, String> {
151+
let note = state
152+
.db
153+
.get_note(note_id)
154+
.map_err(|e| e.to_string())?
155+
.ok_or_else(|| "笔记不存在".to_string())?;
156+
let group_id = note
157+
.group_id
158+
.ok_or_else(|| "笔记未归组——请先把笔记归入组(组是唯一容器),或直接用组卡列表入口创建".to_string())?;
159+
// 摘录仅作文本载入(200 字护栏);不做内容推断
160+
let excerpt = match excerpt {
161+
Some(t) => {
162+
let t = t.trim().to_string();
163+
let cut: String = t.chars().take(200).collect();
164+
if t.chars().count() > 200 { format!("{}…", cut) } else { cut }
165+
}
166+
None => String::new(),
167+
};
168+
let essence = if excerpt.is_empty() { None } else { Some(excerpt) };
169+
create_model_card_inner(&state.db, group_id, name, essence, None, None)
170+
}

‎app/src-tauri/src/lib.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -966,6 +966,8 @@ pub fn run() {
966966
commands_after::question_list,
967967
commands_after::question_answer,
968968
commands_after::question_set_status,
969+
// v0.20.3(REQ-302):笔记段 → 模型卡草稿(复用既有 inner 防双轨)
970+
commands_knowledge_cards::model_card_from_note,
969971
// v0.20.2(REQ-269):ASR 混淆画像闭环——候选/确认/忽略/规则管理
970972
commands_asr_confusion::asr_confusion_get,
971973
commands_asr_confusion::asr_confusion_confirm,
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/**
2+
* ModelCardFromNoteDialog — 笔记段 → 模型卡草稿(v0.20.3 / REQ-302)。
3+
*
4+
* @ai-context: 接线唯一生成链(commands_knowledge_cards::create_model_card_inner,
5+
* 防双轨):组内 kind=model 卡,front=归一化概念名,定义行=可选
6+
* 笔记摘录草稿(应用案例留空——复习面/卡编辑完善);笔记未归组
7+
* → 后端引导先归组(组=唯一容器)。
8+
*/
9+
import { useState } from "react";
10+
import { invoke } from "@tauri-apps/api/core";
11+
12+
interface Props {
13+
noteId: number;
14+
noteTitle: string;
15+
onClose: () => void;
16+
onCreated?: () => void;
17+
}
18+
19+
const overlayStyle: React.CSSProperties = {
20+
position: "fixed",
21+
inset: 0,
22+
background: "rgba(0,0,0,.45)",
23+
zIndex: 1000,
24+
display: "flex",
25+
alignItems: "center",
26+
justifyContent: "center",
27+
};
28+
const cardStyle: React.CSSProperties = {
29+
background: "#fff",
30+
borderRadius: 12,
31+
width: 460,
32+
padding: 16,
33+
fontSize: 13,
34+
};
35+
const btn: React.CSSProperties = { padding: "5px 12px", cursor: "pointer", fontSize: 12, borderRadius: 6, border: "1px solid #e5e7eb", background: "#fff", color: "#374151" };
36+
37+
export default function ModelCardFromNoteDialog({ noteId, noteTitle, onClose, onCreated }: Props) {
38+
const [name, setName] = useState("");
39+
const [excerpt, setExcerpt] = useState("");
40+
const [err, setErr] = useState("");
41+
const [msg, setMsg] = useState("");
42+
const [busy, setBusy] = useState(false);
43+
44+
const create = async () => {
45+
setBusy(true);
46+
setErr("");
47+
try {
48+
await invoke("model_card_from_note", { noteId, name, excerpt: excerpt.trim() || null });
49+
setMsg("✓ 已建模型卡草稿(组内 model 卡;复习面/卡编辑可继续完善定义与应用案例)");
50+
onCreated?.();
51+
setTimeout(onClose, 900);
52+
} catch (e) {
53+
setErr(String(e));
54+
} finally {
55+
setBusy(false);
56+
}
57+
};
58+
59+
return (
60+
<div style={overlayStyle} onClick={onClose}>
61+
<div style={cardStyle} onClick={(e) => e.stopPropagation()}>
62+
<div style={{ display: "flex", alignItems: "center", marginBottom: 10 }}>
63+
<h3 style={{ margin: 0, fontSize: 15 }}>🧠 提炼模型卡草稿</h3>
64+
<span style={{ fontSize: 11, color: "#6b7280", marginLeft: 8 }}>来源:{noteTitle.slice(0, 24)}</span>
65+
<button style={{ ...btn, marginLeft: "auto" }} onClick={onClose}>取消</button>
66+
</div>
67+
{msg && <div style={{ fontSize: 12, color: "#047857", marginBottom: 8 }}>{msg}</div>}
68+
{err && <div style={{ fontSize: 12, color: "#dc2626", marginBottom: 8 }}>{err}</div>}
69+
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
70+
<div>
71+
<div style={{ fontSize: 12, color: "#374151", marginBottom: 2 }}>概念名(front)</div>
72+
<input value={name} onChange={(e) => setName(e.target.value)} autoFocus placeholder="如:安全边际" style={{ width: "100%", boxSizing: "border-box", fontSize: 13, border: "1px solid #e5e7eb", borderRadius: 4, padding: "4px 6px" }} />
73+
</div>
74+
<div>
75+
<div style={{ fontSize: 12, color: "#374151", marginBottom: 2 }}>
76+
定义草稿(可空——应用案例留空,后续完善)
77+
</div>
78+
<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" }} />
79+
</div>
80+
<div style={{ display: "flex", justifyContent: "flex-end", gap: 6 }}>
81+
<button style={btn} disabled={busy} onClick={() => void create()}>创建草稿</button>
82+
</div>
83+
</div>
84+
</div>
85+
</div>
86+
);
87+
}

‎app/src/pages/NotesPage.tsx‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ import NoteReadingView from "../components/NoteReadingView";
3232
import ImagePreviewOverlay from "../components/ImagePreviewOverlay";
3333
import VersionPanel from "../components/VersionPanel";
3434
import NoteAiDialog from "../components/NoteAiDialog";
35+
// v0.20.3(REQ-302):笔记段 → 模型卡草稿对话框
36+
import ModelCardFromNoteDialog from "../components/ModelCardFromNoteDialog";
3537
import NoteLinkToSystem from "../components/NoteLinkToSystem";
3638
// v0.16.1:阅读头「移动到组」显式手动分组入口(拖拽/ⓘ 之外的可发现路径)
3739
import NoteMoveToGroupMenu from "../components/NoteMoveToGroupMenu";
@@ -71,6 +73,8 @@ export default function NotesPage({ focusNoteId, focusNoteSearch, focusGroupId,
7173
const [review, setReview] = useState<{ groupId: number | null; name: string } | undefined>(undefined);
7274
// v0.20.3(REQ-293):行动中心(✅ 全量入口——组侧栏底部按钮打开)
7375
const [actionOpen, setActionOpen] = useState(false);
76+
// v0.20.3(REQ-302):笔记段 → 模型卡草稿对话框
77+
const [modelDialogOpen, setModelDialogOpen] = useState(false);
7478
const [selected, setSelected] = useState<Note | null>(null);
7579
const [status, setStatus] = useState("");
7680
// v0.19.1:阅读态命中词搜索请求(来自引用跳转;key 递增可重触发)
@@ -555,6 +559,18 @@ export default function NotesPage({ focusNoteId, focusNoteSearch, focusGroupId,
555559
>
556560
🤖 AI
557561
</button>
562+
{/* v0.20.3(REQ-302):笔记段 → 模型卡草稿(组内 model 卡唯一生成链) */}
563+
<button
564+
data-testid="note-model-card-entry"
565+
onClick={() => setModelDialogOpen(true)}
566+
title="把本条笔记提炼为模型卡草稿(需已归组)"
567+
style={{
568+
padding: "3px 8px", cursor: "pointer", fontSize: 11, borderRadius: 6,
569+
border: "1px solid #d1fae5", background: "#ecfdf5", color: "#047857",
570+
}}
571+
>
572+
🧠 模型卡
573+
</button>
558574
</>
559575
}
560576
onEdit={() => setEditing(true)}
@@ -581,6 +597,16 @@ export default function NotesPage({ focusNoteId, focusNoteSearch, focusGroupId,
581597
onUpdated={() => void handleNoteChanged()}
582598
/>
583599
)}
600+
{/* v0.20.3(REQ-302):模型卡草稿(组内 model 卡唯一生成链,防双轨) */}
601+
{modelDialogOpen && selected && (
602+
<ModelCardFromNoteDialog
603+
key={`mc-${selected.id}`}
604+
noteId={selected.id}
605+
noteTitle={selected.title}
606+
onClose={() => setModelDialogOpen(false)}
607+
onCreated={() => { void handleNoteChanged(); }}
608+
/>
609+
)}
584610
{/* v0.10.1:图片放大预览(ESC/点击遮罩关闭——与编辑退出 ESC 互斥) */}
585611
{previewImg && (
586612
<ImagePreviewOverlay src={previewImg.src} title={previewImg.title} onClose={() => setPreviewImg(null)} />

0 commit comments

Comments
 (0)