Skip to content

Commit 93253e8

Browse files
committed
feat(feed): 设置页开关与组侧栏碎片捕获区
1 parent 9805c02 commit 93253e8

3 files changed

Lines changed: 143 additions & 2 deletions

File tree

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/**
2+
* FeatureFlagSetting — 功能预览开关面板(v0.11.1:feed 碎片捕获)。
3+
*
4+
* @ai-context: v4 §11.3 交付层纪律——feed 能力功能开关默认关;本面板是
5+
* 用户唯一的开关入口(后端 capture_fragment 二次校验,不信前端隐藏)。
6+
* 开关状态经 get_feature_flags/set_feature_flag 读写(JSON 持久化)。
7+
*/
8+
import { useCallback, useEffect, useState } from "react";
9+
import { invoke } from "@tauri-apps/api/core";
10+
11+
interface FeatureFlags {
12+
feedCapture: boolean;
13+
}
14+
15+
export default function FeatureFlagSetting() {
16+
const [flags, setFlags] = useState<FeatureFlags | null>(null);
17+
const [status, setStatus] = useState("");
18+
19+
const load = useCallback(async () => {
20+
try {
21+
const f = await invoke<FeatureFlags>("get_feature_flags");
22+
setFlags(f);
23+
} catch (e) {
24+
setStatus(`开关读取失败: ${e}`);
25+
}
26+
}, []);
27+
28+
useEffect(() => { void load(); }, [load]);
29+
30+
const toggleFeed = async () => {
31+
if (!flags) return;
32+
const next = !flags.feedCapture;
33+
try {
34+
await invoke<boolean>("set_feature_flag", { name: "feed_capture", value: next });
35+
setFlags({ ...flags, feedCapture: next });
36+
setStatus("");
37+
} catch (e) {
38+
setStatus(`开关切换失败: ${e}`);
39+
}
40+
};
41+
42+
return (
43+
<div>
44+
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
45+
<label style={{ display: "flex", alignItems: "center", gap: 6, fontSize: 13, cursor: "pointer" }}>
46+
<input
47+
type="checkbox"
48+
checked={flags?.feedCapture ?? false}
49+
disabled={!flags}
50+
onChange={() => void toggleFeed()}
51+
/>
52+
碎片快速捕获(feed 进料口)
53+
</label>
54+
<span style={{ fontSize: 11, color: "#9ca3af" }}>
55+
{flags?.feedCapture ? "已启用" : "默认关闭"}
56+
</span>
57+
</div>
58+
<p style={{ fontSize: 12, color: "#9ca3af", margin: "6px 0 0" }}>
59+
启用后笔记页组侧栏出现碎片捕获框:几句话 + 可选截图,按领域自动归入主题组。
60+
碎片不是笔记——独立原料层,供组级复习与结算消费(v4 契约)。
61+
</p>
62+
{status && <p style={{ fontSize: 12, color: "#dc2626", margin: "6px 0 0" }}>{status}</p>}
63+
</div>
64+
);
65+
}

‎app/src/components/NoteGroupPanel.tsx‎

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ export default function NoteGroupPanel({ groupFilter, onGroupFilterChange, selec
5252
const [groups, setGroups] = useState<NoteGroup[]>([]);
5353
const [expandedId, setExpandedId] = useState<number | null>(null);
5454
const [status, setStatus] = useState("");
55+
// v0.11.1:feed 捕获开关(默认关——功能预览纪律)与捕获输入态
56+
const [feedCaptureOn, setFeedCaptureOn] = useState(false);
57+
const [fragText, setFragText] = useState("");
5558
// 改判表单态(展开区消费)
5659
const [overrideKind, setOverrideKind] = useState("standalone");
5760
const [overrideDomain, setOverrideDomain] = useState("");
@@ -65,7 +68,46 @@ export default function NoteGroupPanel({ groupFilter, onGroupFilterChange, selec
6568
}
6669
}, []);
6770

68-
useEffect(() => { void load(); }, [load]);
71+
useEffect(() => {
72+
void load();
73+
// 开关状态只在挂载时读一次(设置页改动后重新进笔记页即刷新)
74+
invoke<{ feedCapture: boolean }>("get_feature_flags")
75+
.then((f) => setFeedCaptureOn(f.feedCapture))
76+
.catch(() => setFeedCaptureOn(false));
77+
}, [load]);
78+
79+
// ── 碎片捕获(feed 进料口;后端二次校验开关)──
80+
const runCapture = async (source: "manual" | "clipboard") => {
81+
try {
82+
let text = fragText.trim();
83+
let imageBytes: number[] | null = null;
84+
if (source === "clipboard") {
85+
// 剪贴板优先:文本+图片一次捕获(REQ-132 预留兑现)
86+
const clipText = await navigator.clipboard.readText().catch(() => "");
87+
if (clipText.trim()) text = clipText.trim();
88+
const items = await navigator.clipboard.read().catch(() => []);
89+
for (const item of items) {
90+
const imgType = item.types.find((t) => t.startsWith("image/"));
91+
if (imgType) {
92+
const blob = await item.getType(imgType);
93+
imageBytes = Array.from(new Uint8Array(await blob.arrayBuffer()));
94+
break;
95+
}
96+
}
97+
}
98+
if (!text && !imageBytes) {
99+
setStatus("请输入碎片内容或复制剪贴板后再捕获");
100+
return;
101+
}
102+
await invoke("capture_fragment", { text, imageBytes, source });
103+
setFragText("");
104+
setStatus("");
105+
await load(); // 可能新建 feed 主题组——刷新组列表
106+
onChanged();
107+
} catch (e) {
108+
setStatus(`碎片捕获失败: ${e}`);
109+
}
110+
};
69111

70112
// ── 改判(REQ-198 修改即记忆)──
71113
const runOverride = async (g: NoteGroup) => {
@@ -103,6 +145,30 @@ export default function NoteGroupPanel({ groupFilter, onGroupFilterChange, selec
103145
<button onClick={() => void load()} style={{ marginLeft: "auto", fontSize: 13, cursor: "pointer" }} title="刷新组列表">⟳</button>
104146
</div>
105147
<div style={{ flex: 1, overflowY: "auto", padding: 6 }}>
148+
{/* v0.11.1:碎片快速捕获区(开关默认关;开启后置顶——feed 进料口) */}
149+
{feedCaptureOn && (
150+
<div style={{ margin: "2px 0 8px", padding: 8, background: "#faf5ff", borderRadius: 6, border: "1px solid #e9d5ff" }}>
151+
<div style={{ fontSize: 11, fontWeight: 600, color: "#7c3aed", marginBottom: 4 }}>⚡ 碎片捕获</div>
152+
<textarea
153+
value={fragText}
154+
onChange={(e) => setFragText(e.target.value)}
155+
placeholder="几句话记下刚学到的…"
156+
rows={2}
157+
style={{ width: "100%", fontSize: 12, padding: 4, border: "1px solid #e5e7eb", borderRadius: 4, resize: "vertical", boxSizing: "border-box" }}
158+
/>
159+
<div style={{ display: "flex", gap: 4, marginTop: 4 }}>
160+
<button onClick={() => void runCapture("manual")}
161+
style={{ fontSize: 11, cursor: "pointer", padding: "2px 8px", borderRadius: 4, border: "1px solid #d1d5db", background: "#fff" }}>
162+
捕获
163+
</button>
164+
<button onClick={() => void runCapture("clipboard")}
165+
style={{ fontSize: 11, cursor: "pointer", padding: "2px 8px", borderRadius: 4, border: "1px solid #d1d5db", background: "#fff" }}
166+
title="文本+图片从剪贴板一次捕获">
167+
📋 剪贴板捕获
168+
</button>
169+
</div>
170+
</div>
171+
)}
106172
{/* 全部(取消组过滤) */}
107173
<div
108174
onClick={() => onGroupFilterChange(null)}
@@ -149,6 +215,9 @@ export default function NoteGroupPanel({ groupFilter, onGroupFilterChange, selec
149215
</div>
150216
<div style={{ display: "flex", alignItems: "center", gap: 4, marginTop: 3 }}>
151217
<span style={{ fontSize: 10, color: badge.color, background: "#f9fafb", borderRadius: 8, padding: "0 5px" }}>{badge.label}</span>
218+
{g.terrain === "feed" && (
219+
<span style={{ fontSize: 10, color: "#7c3aed", background: "#faf5ff", borderRadius: 8, padding: "0 5px" }}>feed</span>
220+
)}
152221
{reason.needsConfirm && !g.routeOverridden && (
153222
<span style={{ fontSize: 10, color: "#b45309", background: "#fffbeb", borderRadius: 8, padding: "0 5px" }}>⚠ 待确认</span>
154223
)}

‎app/src/pages/SettingsPage.tsx‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import ModelManagementPanel from "../components/ModelManagementPanel";
1919
import ModelDiskPanel from "../components/ModelDiskPanel";
2020
import AiServicePanel from "../components/AiServicePanel";
2121
import AiTaskPanel from "../components/AiTaskPanel";
22+
import FeatureFlagSetting from "../components/FeatureFlagSetting";
2223

2324
const panel: React.CSSProperties = { border: "1px solid #e5e7eb", borderRadius: 8, padding: 12 };
2425
const groupTitle: React.CSSProperties = {
@@ -78,11 +79,17 @@ export default function SettingsPage() {
7879
<BackupPanel />
7980
</div>
8081

81-
{/* ── 词表(热词/替换词闭环) ── */}
82+
{/* ── 词表(热词/替换词闭环)── */}
8283
<GroupTitle>词表</GroupTitle>
8384
<div style={{ ...panel, marginBottom: 4 }}>
8485
<VocabManager />
8586
</div>
87+
88+
{/* ── 功能预览(默认关的新能力开关,v4 §11.3)── */}
89+
<GroupTitle>功能预览</GroupTitle>
90+
<div style={{ ...panel, marginBottom: 4 }}>
91+
<FeatureFlagSetting />
92+
</div>
8693
</div>
8794
</div>
8895
);

0 commit comments

Comments
 (0)