Skip to content

Commit 8a1b257

Browse files
committed
feat(goal): v0.18.2 收尾——设置目标 AI 段/对话页 /goal 注入/体系页服务目标反查(REQ-252~254)
- GoalAiSection(设置页 AI 服务组):独立开关默认关 + 预算档位 4K/10K/30K (ai_set_goal_plan read-modify-write 最小面;关闭提示规则草案零影响) - ChatPage /goal <目标名>:L4.5 目标摘要现算注入问答(库即记忆; 未命中目标不发送防误注入——与 /refine 精确形态同纪律) - KnowledgeDetailPanel「🎯 服务于目标」反查块(links targetType='goal'—— 体系页可见目标联动;knowledge_links 白名单+1 与显示标签同步) - tsc 零错误;vitest 509 全绿
1 parent 6db80a7 commit 8a1b257

5 files changed

Lines changed: 130 additions & 4 deletions

File tree

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/**
2+
* GoalAiSection — 设置页「目标 AI」段(v0.18.2 REQ-254)。
3+
*
4+
* @ai-context: 目标规划=内容上传类调用——独立开关默认关(content_gate 之外
5+
* 第二闸门)+ 预算档位(4K/10K/30K,默认标准);关闭时 AI 规划
6+
* 不可用但**规则草案完全可用**(本地优先降级链)。
7+
* @ai-context: read-modify-write 最小面(ai_set_goal_plan——不覆盖其他设置)。
8+
*/
9+
import { useEffect, useState } from "react";
10+
import { invoke } from "@tauri-apps/api/core";
11+
import type { AiSettingsView } from "../types/ai";
12+
13+
const TIERS = [
14+
{ id: "light", label: "轻量(~4K,快而省)" },
15+
{ id: "standard", label: "标准(~10K,推荐)" },
16+
{ id: "deep", label: "深度(~30K,计划更细)" },
17+
];
18+
19+
export default function GoalAiSection() {
20+
const [enabled, setEnabled] = useState(false);
21+
const [tier, setTier] = useState("standard");
22+
const [msg, setMsg] = useState("");
23+
const [busy, setBusy] = useState(false);
24+
25+
useEffect(() => {
26+
invoke<AiSettingsView>("ai_get_settings")
27+
.then((s) => { setEnabled(s.goalPlanEnabled); setTier(s.goalPlanTier || "standard"); })
28+
.catch((e) => setMsg(`目标 AI 设置读取失败: ${e}`));
29+
}, []);
30+
31+
const save = async (nextEnabled: boolean, nextTier: string) => {
32+
setBusy(true);
33+
setMsg("");
34+
try {
35+
await invoke("ai_set_goal_plan", { enabled: nextEnabled, tier: nextTier || null });
36+
setEnabled(nextEnabled);
37+
setTier(nextTier);
38+
setMsg(nextEnabled ? "已开启——访谈/详情页出现「✨ 用 AI 规划」,草案确认后落库" : "已关闭——按规则草案规划(本地优先,零影响)");
39+
} catch (e) {
40+
setMsg(`保存失败: ${e}`);
41+
} finally {
42+
setBusy(false);
43+
}
44+
};
45+
46+
return (
47+
<div style={{ marginTop: 8, padding: 8, background: "#fff", borderRadius: 6, border: "1px solid #e5e7eb" }}>
48+
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 4 }}>
49+
<span style={{ fontWeight: 600, fontSize: 12, color: "#374151" }}>🎯 目标 AI(规划师)</span>
50+
<label style={{ fontSize: 12, display: "flex", alignItems: "center", gap: 4, cursor: "pointer" }}>
51+
<input
52+
data-testid="goal-ai-toggle"
53+
type="checkbox"
54+
checked={enabled}
55+
disabled={busy}
56+
onChange={(e) => void save(e.target.checked, tier)}
57+
/>
58+
开启(默认关)
59+
</label>
60+
<span style={{ marginLeft: "auto", fontSize: 11, color: "#9ca3af" }}>
61+
{tier === "standard" ? "标准档 ~10K" : TIERS.find((t) => t.id === tier)?.label}
62+
</span>
63+
</div>
64+
<div style={{ display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" }}>
65+
<select
66+
data-testid="goal-ai-tier"
67+
value={tier}
68+
disabled={busy || !enabled}
69+
onChange={(e) => void save(enabled, e.target.value)}
70+
style={{ fontSize: 11, padding: "2px 6px", border: "1px solid #e5e7eb", borderRadius: 4 }}
71+
>
72+
{TIERS.map((t) => <option key={t.id} value={t.id}>{t.label}</option>)}
73+
</select>
74+
<span style={{ fontSize: 11, color: "#9ca3af" }}>预算硬顶·成本/轨迹可查·失败自动回退规则草案</span>
75+
</div>
76+
{msg && <p style={{ fontSize: 11, color: "#6b7280", margin: "4px 0 0" }}>{msg}</p>}
77+
</div>
78+
);
79+
}

‎app/src/components/KnowledgeDetailPanel.tsx‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,14 @@ export default function KnowledgeDetailPanel({ width = 320, system, nodes, conce
5555
const [mValidWhen, setMValidWhen] = useState("");
5656
const [mInvalidWhen, setMInvalidWhen] = useState("");
5757
const [mStatus, setMStatus] = useState<KnowledgeModelStatus>("active");
58+
// v0.18.2:目标↔体系反查(体系页「服务于目标 X」——knowledge_links target_type='goal')
59+
const [goalNames, setGoalNames] = useState<Map<number, string>>(new Map());
60+
61+
useEffect(() => {
62+
invoke<{ goal: { id: number; name: string } }[]>("list_goals")
63+
.then((gs) => setGoalNames(new Map(gs.map((g) => [g.goal.id, g.goal.name]))))
64+
.catch(() => { /* 目标未接入时反查降级为空——次要块 */ });
65+
}, []);
5866

5967
// 选中实体变化 → 同步编辑器草稿
6068
useEffect(() => {
@@ -140,6 +148,17 @@ export default function KnowledgeDetailPanel({ width = 320, system, nodes, conce
140148
</div>
141149

142150
<div style={{ flex: 1, overflowY: "auto", padding: 12 }}>
151+
{/* v0.18.2:体系「服务于目标 X」反查块(任何选中态均可见——体系上下文) */}
152+
{links.filter((l) => l.targetType === "goal").length > 0 && (
153+
<div data-testid="goal-link-block" style={{ fontSize: 12, background: "#f0fdfa", border: "1px solid #99f6e4", borderRadius: 8, padding: "8px 10px", marginBottom: 10 }}>
154+
<div style={{ fontWeight: 600, color: "#0f766e", marginBottom: 4 }}>🎯 服务于目标</div>
155+
{links.filter((l) => l.targetType === "goal").map((l) => (
156+
<div key={l.id} style={{ color: "#374151" }}>
157+
· {goalNames.get(l.targetId) ?? `目标 #${l.targetId}`}
158+
</div>
159+
))}
160+
</div>
161+
)}
143162
{!selection && (
144163
<div style={{ fontSize: 12, color: "#9ca3af", textAlign: "center", paddingTop: 40 }}>
145164
从中间选择一个实体查看详情

‎app/src/pages/ChatPage.tsx‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,26 @@ export default function ChatPage(props: Props) {
222222
setDraft("");
223223
return;
224224
}
225+
// v0.18.2(REQ-253):`/goal <目标名>`——L4.5 目标摘要现算注入(库即记忆,
226+
// 与普通提问分流;找不到目标不发送,防误注入)
227+
const goalCmd = content.match(/^\/goal (.+)$/);
228+
let effective = content;
229+
if (goalCmd) {
230+
const name = goalCmd[1].trim();
231+
try {
232+
const goals = await invoke<{ goal: { id: number; name: string } }[]>("list_goals");
233+
const g = goals.find((x) => x.goal.name === name) ?? goals.find((x) => x.goal.name.includes(name));
234+
if (!g) {
235+
setGateError(`找不到目标「${name}」——请在🎯目标页确认名称`);
236+
return;
237+
}
238+
const ctx = await invoke<string>("goal_chat_context", { goalId: g.goal.id });
239+
effective = `(以下为目标上下文·现算注入)\n${ctx}\n\n——请基于以上回答:我学到哪了?下一步重点是什么?`;
240+
} catch (e) {
241+
setGateError(String(e));
242+
return;
243+
}
244+
}
225245
// 首次发送前一次性云端提示(审查补充:设计承诺→实现)
226246
if (!localStorage.getItem(CLOUD_NOTICE_KEY)) {
227247
const ok = await confirm(CLOUD_NOTICE_TEXT, { title: "熵减 · AI 对话", kind: "warning" });
@@ -233,7 +253,7 @@ export default function ChatPage(props: Props) {
233253
setDraft("");
234254
setGateError(null);
235255
try {
236-
const ok = await launch(activeChatId, "chat_send", { content, resendMessageId: resendId });
256+
const ok = await launch(activeChatId, "chat_send", { content: effective, resendMessageId: resendId });
237257
if (!ok) setGateError("该会话已有进行中的对话——请等待完成或先停止");
238258
} catch (e) {
239259
setGateError(String(e));

‎app/src/pages/SettingsPage.tsx‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import { VocabManager } from "../components/VocabManager";
1818
import ModelManagementPanel from "../components/ModelManagementPanel";
1919
import ModelDiskPanel from "../components/ModelDiskPanel";
2020
import AiServicePanel from "../components/AiServicePanel";
21+
// v0.18.2(REQ-254):目标 AI 规划师开关与预算档位(默认关)
22+
import GoalAiSection from "../components/GoalAiSection";
2123
import AiProviderSettings from "../components/AiProviderSettings";
2224
import AiTaskPanel from "../components/AiTaskPanel";
2325
import FeatureFlagSetting from "../components/FeatureFlagSetting";
@@ -72,6 +74,10 @@ export default function SettingsPage() {
7274
<div style={{ marginTop: 8 }}>
7375
<AiServicePanel />
7476
</div>
77+
{/* v0.18.2(REQ-254):目标 AI 规划师独立开关+预算档位(默认关) */}
78+
<div style={{ marginTop: 8 }}>
79+
<GoalAiSection />
80+
</div>
7581
<div style={{ marginTop: 8 }}>
7682
<AiTaskPanel />
7783
</div>

‎app/src/types/knowledge.ts‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ export type KnowledgeConceptStatus = "core" | "watching" | "archived";
3030
/** 模型状态(command 层白名单校验) */
3131
export type KnowledgeModelStatus = "active" | "watching" | "archived";
3232

33-
/** 引用目标类型:note_group / note / flashcard / fragment(command 层白名单校验) */
34-
export type KnowledgeLinkTargetType = "note_group" | "note" | "flashcard" | "fragment";
33+
/** 引用目标类型:note_group / note / flashcard / fragment / goal(v0.18.2 目标↔体系联动) */
34+
export type KnowledgeLinkTargetType = "note_group" | "note" | "flashcard" | "fragment" | "goal";
3535

3636
/** 知识体系(含计数——list_knowledge_systems 返回;create/update 可能缺省计数) */
3737
export interface KnowledgeSystem {
@@ -333,12 +333,14 @@ export const conceptStatusLabel: Record<KnowledgeConceptStatus, string> = {
333333
archived: "已归档",
334334
};
335335

336-
/** 引用目标类型中文标签(「挂引用」下拉用) */
336+
/** 引用目标类型中文标签(「挂引用」下拉用;goal 供目标↔体系联动反查展示——
337+
* 挂引用下拉不暴露 goal(目标侧经确认流落库,避免双入口歧义) */
337338
export const linkTargetTypeLabel: Record<KnowledgeLinkTargetType, string> = {
338339
note_group: "笔记组",
339340
note: "笔记",
340341
flashcard: "闪卡",
341342
fragment: "碎片",
343+
goal: "目标",
342344
};
343345

344346
/** 详情面板选中实体的分发类型(node/concept/model;id=null 表示新建) */

0 commit comments

Comments
 (0)