dsh-knowledge-library 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +107 -12
  3. package/dist/client.js +1420 -303
  4. package/dist/core/extract.d.ts +1 -0
  5. package/dist/core/extract.js +62 -5
  6. package/dist/core/fetch.js +82 -11
  7. package/dist/core/grouping.d.ts +6 -0
  8. package/dist/core/grouping.js +71 -0
  9. package/dist/core/incremental.d.ts +14 -0
  10. package/dist/core/incremental.js +52 -0
  11. package/dist/core/jobs.d.ts +1 -0
  12. package/dist/core/jobs.js +4 -0
  13. package/dist/core/knowledge.d.ts +37 -0
  14. package/dist/core/knowledge.js +617 -0
  15. package/dist/core/library.d.ts +20 -2
  16. package/dist/core/library.js +271 -12
  17. package/dist/core/page-access.d.ts +6 -0
  18. package/dist/core/page-access.js +38 -0
  19. package/dist/core/purge.d.ts +20 -0
  20. package/dist/core/purge.js +106 -0
  21. package/dist/directory-settings.d.ts +18 -0
  22. package/dist/directory-settings.js +92 -0
  23. package/dist/http.d.ts +3 -1
  24. package/dist/http.js +121 -4
  25. package/dist/index.js +17 -5
  26. package/dist/knowledge-model.d.ts +18 -0
  27. package/dist/knowledge-model.js +116 -0
  28. package/dist/preview-settings.d.ts +6 -0
  29. package/dist/preview-settings.js +42 -0
  30. package/dist/public/app.css +126 -2
  31. package/dist/public/app.js +11 -7
  32. package/dist/standalone.js +8 -3
  33. package/dist/tools.d.ts +2 -1
  34. package/dist/tools.js +10 -1
  35. package/dist/types.d.ts +143 -1
  36. package/dist/ui/App.js +243 -47
  37. package/dist/ui/Dialog.d.ts +7 -0
  38. package/dist/ui/Dialog.js +32 -0
  39. package/dist/ui/Grouping.d.ts +12 -0
  40. package/dist/ui/Grouping.js +93 -0
  41. package/dist/ui/Knowledge.d.ts +26 -0
  42. package/dist/ui/Knowledge.js +106 -0
  43. package/dist/ui/api.d.ts +7 -0
  44. package/dist/ui/api.js +16 -0
  45. package/dist/ui/icons.js +2 -0
  46. package/package.json +4 -1
package/dist/ui/App.js CHANGED
@@ -1,21 +1,15 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useCallback, useEffect, useRef, useState } from 'react';
3
3
  import { Icon } from './icons.js';
4
- const API = '/knowledge-library/api';
4
+ import { API, ApiError, api, post } from './api.js';
5
+ import { Dialog } from './Dialog.js';
6
+ import { KnowledgeBadge, KnowledgeJobsDialog, StartKnowledgeDialog } from './Knowledge.js';
5
7
  const kinds = {
6
8
  web: { label: '网页', icon: 'web' }, text: { label: '文本', icon: 'file' }, file: { label: '文件', icon: 'file' },
7
9
  video: { label: '音视频', icon: 'video' }, transcript: { label: '字幕', icon: 'video' }, note: { label: '笔记', icon: 'note' },
8
10
  };
9
11
  const time = (v) => `${String(Math.floor(v / 3600)).padStart(2, '0')}:${String(Math.floor(v / 60) % 60).padStart(2, '0')}:${String(Math.floor(v) % 60).padStart(2, '0')}`;
10
12
  const date = (value) => new Intl.DateTimeFormat('zh-CN', { month: '2-digit', day: '2-digit' }).format(new Date(value));
11
- async function api(path, options = {}) {
12
- const response = await fetch(API + path, { ...options, headers: { 'Content-Type': 'application/json', 'X-Knowledge-Library': '1', ...options.headers } });
13
- const data = await response.json();
14
- if (!response.ok)
15
- throw new Error(data.error || `请求失败:${response.status}`);
16
- return data;
17
- }
18
- const post = (path, value, method = 'POST') => api(path, { method, body: JSON.stringify(value) });
19
13
  function splitTags(value) { return value.split(/[,,]/).map(v => v.trim()).filter(Boolean); }
20
14
  export function App() {
21
15
  const [stats, setStats] = useState();
@@ -27,9 +21,18 @@ export function App() {
27
21
  const [detail, setDetail] = useState();
28
22
  const [anchor, setAnchor] = useState();
29
23
  const [editor, setEditor] = useState();
24
+ const [deletion, setDeletion] = useState();
25
+ const [restoring, setRestoring] = useState();
26
+ const [emptyRoot, setEmptyRoot] = useState();
30
27
  const [jobs, setJobs] = useState([]);
28
+ const [knowledge, setKnowledge] = useState();
29
+ const [batchMode, setBatchMode] = useState(false);
30
+ const [checkedIds, setCheckedIds] = useState([]);
31
+ const [generation, setGeneration] = useState();
32
+ const [knowledgeOpen, setKnowledgeOpen] = useState();
31
33
  const [showJobs, setShowJobs] = useState(false);
32
34
  const [showInfo, setShowInfo] = useState(false);
35
+ const [settingsFromJobs, setSettingsFromJobs] = useState(false);
33
36
  const [showNavigation, setShowNavigation] = useState(false);
34
37
  const [error, setError] = useState('');
35
38
  const [toast, setToast] = useState('');
@@ -41,12 +44,18 @@ export function App() {
41
44
  const detailRef = useRef(null);
42
45
  const refresh = useCallback(() => setRevision(v => v + 1), []);
43
46
  const notify = (message) => setToast(message);
44
- const chooseFilter = (next) => { setFilter(next); setLimit(40); setSelected(undefined); setAnchor(undefined); setDetail(undefined); setShowNavigation(false); };
47
+ const chooseFilter = (next) => { setFilter(next); setLimit(40); setSelected(undefined); setAnchor(undefined); setDetail(undefined); setShowNavigation(false); setBatchMode(false); setCheckedIds([]); };
45
48
  const refreshMeta = useCallback(async () => {
46
- const [s, j] = await Promise.all([api('/stats'), api('/jobs')]);
49
+ const [s, j, k] = await Promise.all([api('/stats'), api('/jobs'), api('/knowledge').catch(e => {
50
+ if (e instanceof ApiError && e.status === 404)
51
+ return { supported: false, available: false, reason: '请重启资料库服务以启用知识整理。', jobs: [], states: {} };
52
+ throw e;
53
+ })]);
47
54
  setStats(s);
48
55
  setJobs(j);
56
+ setKnowledge(k);
49
57
  }, []);
58
+ useEffect(() => { setCheckedIds([]); }, [query]);
50
59
  useEffect(() => { void refreshMeta().catch(e => setError(e.message)); }, [revision, refreshMeta]);
51
60
  useEffect(() => {
52
61
  const controller = new AbortController();
@@ -87,12 +96,14 @@ export function App() {
87
96
  element?.scrollIntoView({ block: 'center' });
88
97
  }, [detail, anchor]);
89
98
  const pending = jobs.filter(j => ['queued', 'running'].includes(j.status)).length;
99
+ const knowledgePending = knowledge?.jobs.filter(j => ['queued', 'running'].includes(j.status)).length || 0;
100
+ const draftCount = knowledge?.jobs.filter(j => ['draft', 'grouping'].includes(j.status)).length || 0;
90
101
  useEffect(() => {
91
- if (!pending)
102
+ if (!pending && !knowledgePending)
92
103
  return;
93
104
  const interval = setInterval(refresh, 2500);
94
105
  return () => clearInterval(interval);
95
- }, [pending, refresh]);
106
+ }, [pending, knowledgePending, refresh]);
96
107
  useEffect(() => {
97
108
  if (!toast)
98
109
  return;
@@ -118,8 +129,50 @@ export function App() {
118
129
  setError('剪贴板不可用,请从资料详情手动复制资料 ID');
119
130
  }
120
131
  }
121
- const currentTitle = filter.category || filter.collection || (filter.tag ? `标签 · ${filter.tag}` : filter.kind === 'note' ? '知识笔记' : '全部资料');
122
- return _jsxs("div", { className: "kl-app", children: [_jsxs("main", { className: "kl-workspace", children: [_jsxs("header", { className: "kl-header", children: [_jsx("h1", { children: currentTitle }), _jsxs("div", { className: "kl-header-actions", children: [_jsxs("button", { className: "kl-secondary", onClick: () => setEditor({ mode: 'note' }), children: [_jsx(Icon, { name: "note", size: 16 }), "\u5199\u7B14\u8BB0"] }), _jsxs("button", { className: "kl-primary", onClick: () => setEditor({ mode: 'capture' }), children: [_jsx(Icon, { name: "plus", size: 16 }), "\u6536\u96C6\u8D44\u6599"] })] })] }), _jsxs("div", { className: "kl-navigation", children: [_jsxs("nav", { "aria-label": "\u8D44\u6599\u5BFC\u822A", children: [_jsx("button", { "aria-pressed": !Object.values(filter).some(Boolean), onClick: () => chooseFilter({}), children: "\u5168\u90E8\u8D44\u6599" }), _jsx("button", { "aria-pressed": filter.category === '收件箱', onClick: () => chooseFilter({ category: '收件箱' }), children: "\u6536\u4EF6\u7BB1" }), _jsx("button", { "aria-pressed": filter.kind === 'note', onClick: () => chooseFilter({ kind: 'note' }), children: "\u77E5\u8BC6\u7B14\u8BB0" })] }), _jsxs("button", { className: "kl-directory-toggle", "aria-expanded": showNavigation, "aria-controls": "kl-directory", onClick: () => setShowNavigation(v => !v), children: [_jsx(Icon, { name: "folder", size: 16 }), "\u5206\u7C7B\u4E0E\u4E13\u9898"] })] }), _jsxs("div", { className: "kl-toolbar", children: [_jsxs("label", { className: "kl-search", children: [_jsx(Icon, { name: "search", size: 19 }), _jsx("input", { ref: searchRef, "aria-label": "\u641C\u7D22\u8D44\u6599", placeholder: "\u641C\u7D22\u6B63\u6587\u3001\u6807\u9898\u3001\u6807\u7B7E\u2026", value: query, onChange: e => { setQuery(e.target.value); setLimit(40); } }), _jsx("kbd", { children: "Ctrl K" })] }), _jsxs("button", { "aria-label": pending ? `采集任务,${pending} 项处理中` : '采集任务', className: `kl-job-button ${pending ? 'has-jobs' : ''}`, onClick: () => setShowJobs(true), children: [_jsx(Icon, { name: "clock", size: 17 }), _jsx("span", { children: pending ? `${pending} 项处理中` : '采集任务' }), jobs.some(j => j.status === 'failed') && _jsx("i", {})] }), _jsx("button", { className: "kl-icon-button", "aria-label": "\u8D44\u6599\u5E93\u8BBE\u7F6E", title: "\u8D44\u6599\u5E93\u8BBE\u7F6E", onClick: () => setShowInfo(true), children: _jsx(Icon, { name: "settings", size: 18 }) })] }), !!error && _jsxs("div", { className: "kl-error", role: "alert", children: [_jsx("span", { children: error }), _jsx("button", { "aria-label": "\u5173\u95ED\u9519\u8BEF", onClick: () => setError(''), children: _jsx(Icon, { name: "close", size: 16 }) })] }), _jsxs("div", { className: "kl-filterbar", children: [_jsxs("span", { children: [_jsx("b", { children: total }), " \u4EFD\u8D44\u6599", query && ` · “${query}”`] }), _jsxs("div", { children: [_jsxs("select", { "aria-label": "\u6309\u7C7B\u578B\u7B5B\u9009", value: filter.kind || '', onChange: e => { setFilter({ ...filter, kind: e.target.value || undefined }); setLimit(40); }, children: [_jsx("option", { value: "", children: "\u6240\u6709\u7C7B\u578B" }), Object.entries(kinds).map(([value, kind]) => _jsx("option", { value: value, children: kind.label }, value))] }), _jsx("span", { className: "kl-sort-label", children: "\u6700\u8FD1\u66F4\u65B0 \u2193" })] })] }), _jsxs("div", { className: `kl-body ${detail || detailLoading ? 'with-detail' : ''}`, children: [showNavigation && _jsxs("aside", { className: "kl-nav", id: "kl-directory", "aria-label": "\u5206\u7C7B\u4E0E\u4E13\u9898", children: [_jsxs("div", { className: "kl-directory-title", children: [_jsx("span", { children: "\u5206\u7C7B\u4E0E\u4E13\u9898" }), _jsx("button", { className: "kl-icon-button", "aria-label": "\u5173\u95ED\u5206\u7C7B\u5BFC\u822A", onClick: () => setShowNavigation(false), children: _jsx(Icon, { name: "close", size: 16 }) })] }), _jsxs("div", { className: "kl-nav-section", children: [_jsx("div", { className: "kl-nav-label", children: "\u5206\u7C7B" }), stats?.categories.filter(c => c.name !== '收件箱').map(c => _jsxs("button", { "aria-pressed": filter.category === c.name, className: "kl-nav-row", onClick: () => chooseFilter({ category: c.name }), children: [_jsx(Icon, { name: "folder", size: 16 }), _jsx("span", { title: c.name, children: c.name }), _jsx("small", { children: c.count })] }, c.name)), !stats?.categories.some(c => c.name !== '收件箱') && _jsx("p", { className: "kl-nav-hint", children: "\u6536\u96C6\u8D44\u6599\u65F6\u6DFB\u52A0\u5206\u7C7B" })] }), _jsxs("div", { className: "kl-nav-section", children: [_jsx("div", { className: "kl-nav-label", children: "\u4E13\u9898" }), stats?.collections.map(c => _jsxs("button", { "aria-pressed": filter.collection === c.name, className: "kl-nav-row", onClick: () => chooseFilter({ collection: c.name }), children: [_jsx("span", { className: "kl-collection-dot" }), _jsx("span", { title: c.name, children: c.name }), _jsx("small", { children: c.count })] }, c.name)), !stats?.collections.length && _jsx("p", { className: "kl-nav-hint", children: "\u6536\u96C6\u8D44\u6599\u65F6\u6DFB\u52A0\u4E13\u9898" })] }), !!stats?.tags.length && _jsxs("div", { className: "kl-nav-section", children: [_jsx("div", { className: "kl-nav-label", children: "\u6807\u7B7E" }), _jsx("div", { className: "kl-tag-cloud", children: stats.tags.slice(0, 15).map(tag => _jsxs("button", { "aria-pressed": filter.tag === tag.name, onClick: () => chooseFilter({ tag: tag.name }), children: ["#", tag.name] }, tag.name)) })] }), _jsxs("div", { className: "kl-nav-bottom", children: [_jsx("span", { className: "kl-live-dot" }), "\u4FDD\u5B58\u5728\u8FD9\u53F0\u7535\u8111"] })] }), _jsxs("section", { className: "kl-list", "aria-label": "\u8D44\u6599\u5217\u8868", "aria-busy": loading, children: [loading && !items.length ? _jsxs("div", { className: "kl-empty", children: [_jsx("span", { className: "kl-spinner" }), "\u6B63\u5728\u8BFB\u53D6\u8D44\u6599\u5E93\u2026"] }) : !items.length ? _jsxs("div", { className: "kl-empty", children: [_jsx(Icon, { name: "inbox", size: 28 }), _jsx("h2", { children: query || Object.values(filter).some(Boolean) ? '没有找到匹配的资料' : '还没有资料,收集的内容会显示在这里' }), query && _jsx("p", { children: "\u8BD5\u8BD5\u5176\u4ED6\u5173\u952E\u8BCD\uFF0C\u6216\u5207\u6362\u5206\u7C7B\u3002" }), _jsx("button", { className: "kl-secondary", onClick: () => query || Object.values(filter).some(Boolean) ? (setQuery(''), chooseFilter({})) : setEditor({ mode: 'capture' }), children: query || Object.values(filter).some(Boolean) ? '查看全部资料' : '收集第一份资料' })] }) : items.map(item => _jsxs("button", { className: `kl-item ${selected === item.id ? 'selected' : ''}`, onClick: () => { setAnchor(undefined); setSelected(item.id); }, children: [_jsxs("div", { className: "kl-item-top", children: [_jsxs("span", { className: `kl-type kl-type-${item.kind}`, children: [_jsx(Icon, { name: kinds[item.kind].icon, size: 15 }), kinds[item.kind].label] }), _jsx("time", { children: date(item.createdAt) })] }), _jsx("h2", { children: item.title }), _jsx("p", { children: item.excerpt || item.extraction }), _jsxs("div", { className: "kl-item-bottom", children: [_jsxs("span", { className: "kl-category", children: [_jsx(Icon, { name: "folder", size: 13 }), item.category] }), item.tags.slice(0, 2).map(tag => _jsx("span", { className: "kl-chip", children: tag }, tag)), item.status === 'stored' && _jsx("span", { className: "kl-stored", children: "\u4EC5\u5F52\u6863" }), _jsx(Icon, { name: "arrow", size: 15 })] })] }, item.id)), items.length < total && _jsx("button", { className: "kl-load-more", onClick: () => setLimit(v => Math.min(100, v + 40)), disabled: limit >= 100, children: limit >= 100 ? '请缩小搜索范围(最多显示 100 条)' : '加载更多' })] }), selected && _jsxs("section", { className: "kl-detail", "aria-label": "\u8D44\u6599\u8BE6\u60C5", ref: detailRef, children: [_jsxs("div", { className: "kl-detail-toolbar", children: [_jsxs("button", { className: "kl-back", onClick: () => { setSelected(undefined); setDetail(undefined); }, children: [_jsx(Icon, { name: "close", size: 18 }), _jsx("span", { children: "\u5173\u95ED\u9605\u8BFB" })] }), detail && _jsxs("div", { children: [_jsx("button", { title: "\u590D\u5236\u8D44\u6599\u5F15\u7528", "aria-label": "\u590D\u5236\u8D44\u6599\u5F15\u7528", onClick: () => void copy(`library://${detail.id}`), children: _jsx(Icon, { name: "copy", size: 17 }) }), _jsx("a", { title: "\u5BFC\u51FA Markdown", "aria-label": "\u5BFC\u51FA Markdown", href: `${API}/materials/${detail.id}/export`, children: _jsx(Icon, { name: "download", size: 17 }) }), _jsx("button", { onClick: () => setEditor({ mode: 'edit', source: detail }), children: "\u7F16\u8F91\u4FE1\u606F" })] })] }), detailLoading ? _jsxs("div", { className: "kl-empty", children: [_jsx("span", { className: "kl-spinner" }), "\u8BFB\u53D6\u4E2D\u2026"] }) : detail && _jsxs("article", { children: [_jsxs("div", { className: "kl-detail-kicker", children: [_jsx("span", { children: kinds[detail.kind].label }), _jsx("span", { children: detail.category })] }), _jsx("h2", { children: detail.title }), _jsxs("div", { className: "kl-source", children: [detail.sourceUrl ? _jsxs("a", { href: detail.sourceUrl, target: "_blank", rel: "noreferrer", children: [new URL(detail.sourceUrl).hostname, _jsx(Icon, { name: "arrow", size: 12 })] }) : _jsx("span", { children: detail.originalName || '本地收集' }), _jsx("span", { children: new Date(detail.createdAt).toLocaleDateString('zh-CN') }), detail.author && _jsx("span", { children: detail.author })] }), !!detail.tags.length && _jsx("div", { className: "kl-detail-tags", children: detail.tags.map(t => _jsxs("span", { className: "kl-chip", children: ["#", t] }, t)) }), detail.sourceNote && _jsx("aside", { className: "kl-source-note", children: detail.sourceNote }), detail.status === 'stored' && _jsxs("div", { className: "kl-notice", children: [_jsx(Icon, { name: "file" }), _jsxs("div", { children: [_jsx("strong", { children: "\u539F\u6587\u4EF6\u5DF2\u5B89\u5168\u5F52\u6863" }), _jsx("p", { children: detail.extraction })] })] }), _jsx("div", { className: "kl-content", children: detail.segments.map(segment => _jsxs("div", { className: `kl-segment ${anchor === segment.id ? 'kl-cited' : ''}`, id: segment.id, "data-segment-id": segment.id, children: [_jsxs("div", { className: "kl-segment-line", children: [_jsx("span", { children: segment.startSeconds !== undefined ? time(segment.startSeconds) : segment.id }), _jsx("button", { title: "\u6458\u5F55\u4E3A\u7B14\u8BB0", "aria-label": `摘录 ${segment.id} 为笔记`, onClick: () => setEditor({ mode: 'note', source: detail, segment }), children: _jsx(Icon, { name: "note", size: 14 }) }), _jsx("button", { title: "\u590D\u5236\u6BB5\u843D\u5F15\u7528", "aria-label": `复制 ${segment.id} 引用`, onClick: () => void copy(`library://${detail.id}/${segment.id}`), children: _jsx(Icon, { name: "copy", size: 14 }) })] }), /^#{1,6}\s/.test(segment.text) ? _jsx("h3", { children: segment.text.replace(/^#{1,6}\s+/, '') }) : _jsx("p", { children: segment.text })] }, segment.id)) }), !!detail.citations.length && _jsxs("div", { className: "kl-references", children: [_jsx("h3", { children: "\u6765\u6E90\u5F15\u7528" }), detail.citations.map((c, i) => _jsxs("button", { "aria-label": `查看引用 ${i + 1}`, onClick: () => { setAnchor(c.segmentId); setSelected(c.materialId); }, children: [_jsx(Icon, { name: "file", size: 15 }), _jsxs("span", { children: [c.materialId.slice(0, 15), "\u2026", c.segmentId ? ` / ${c.segmentId}` : ''] }), _jsx(Icon, { name: "arrow", size: 14 })] }, i))] }), _jsxs("footer", { className: "kl-detail-footer", children: [_jsxs("div", { children: [_jsx("span", { className: "kl-live-dot" }), detail.extraction] }), detail.originalFile && _jsxs("a", { href: `${API}/materials/${detail.id}/original`, children: ["\u4E0B\u8F7D\u539F\u59CB\u6587\u4EF6", _jsx(Icon, { name: "download", size: 14 })] }), _jsx("code", { children: detail.id })] })] })] })] })] }), editor && _jsx(EditorDialog, { editor: editor, categories: stats?.categories.map(c => c.name) || [], onClose: () => setEditor(undefined), onSaved: result => {
132
+ function askDelete(item) {
133
+ if (stats)
134
+ setDeletion({ id: item.id, title: item.title, root: stats.root });
135
+ }
136
+ function afterRemoval(id, message) {
137
+ if (selected === id) {
138
+ setSelected(undefined);
139
+ setDetail(undefined);
140
+ setAnchor(undefined);
141
+ }
142
+ refresh();
143
+ notify(message);
144
+ searchRef.current?.focus();
145
+ }
146
+ async function restoreItem(id) {
147
+ if (!stats || restoring)
148
+ return;
149
+ setRestoring(id);
150
+ try {
151
+ await post(`/materials/${id}/restore`, { expectedRoot: stats.root });
152
+ afterRemoval(id, '资料已恢复到原分类');
153
+ }
154
+ catch (e) {
155
+ setError(e.message);
156
+ }
157
+ finally {
158
+ setRestoring(undefined);
159
+ }
160
+ }
161
+ function openKnowledgeSource(id, segmentId) {
162
+ setKnowledgeOpen(undefined);
163
+ chooseFilter({});
164
+ setQuery('');
165
+ setSelected(id);
166
+ setAnchor(segmentId);
167
+ refresh();
168
+ }
169
+ function generateFor(ids, mode) {
170
+ if (stats)
171
+ setGeneration({ root: stats.root, sourceIds: ids, mode });
172
+ }
173
+ const currentTitle = filter.trashed ? '回收站' : filter.category || filter.collection || (filter.tag ? `标签 · ${filter.tag}` : filter.kind === 'note' ? '知识笔记' : '全部资料');
174
+ return _jsxs("div", { className: "kl-app", children: [_jsxs("main", { className: "kl-workspace", children: [_jsxs("header", { className: "kl-header", children: [_jsx("h1", { children: currentTitle }), _jsxs("div", { className: "kl-header-actions", children: [_jsxs("button", { className: "kl-secondary", onClick: () => setEditor({ mode: 'note' }), children: [_jsx(Icon, { name: "note", size: 16 }), "\u5199\u7B14\u8BB0"] }), _jsxs("button", { className: "kl-primary", onClick: () => setEditor({ mode: 'capture' }), children: [_jsx(Icon, { name: "plus", size: 16 }), "\u6536\u96C6\u8D44\u6599"] })] })] }), _jsxs("div", { className: "kl-navigation", children: [_jsxs("nav", { "aria-label": "\u8D44\u6599\u5BFC\u822A", children: [_jsx("button", { "aria-pressed": !Object.values(filter).some(Boolean), onClick: () => chooseFilter({}), children: "\u5168\u90E8\u8D44\u6599" }), _jsx("button", { "aria-pressed": filter.category === '收件箱', onClick: () => chooseFilter({ category: '收件箱' }), children: "\u6536\u4EF6\u7BB1" }), _jsx("button", { "aria-pressed": filter.kind === 'note', onClick: () => chooseFilter({ kind: 'note' }), children: "\u77E5\u8BC6\u7B14\u8BB0" }), _jsx("button", { "aria-pressed": filter.trashed === '1', onClick: () => chooseFilter({ trashed: '1' }), children: "\u56DE\u6536\u7AD9" })] }), _jsxs("button", { className: "kl-directory-toggle", "aria-expanded": showNavigation, "aria-controls": "kl-directory", onClick: () => setShowNavigation(v => !v), children: [_jsx(Icon, { name: "folder", size: 16 }), "\u5206\u7C7B\u4E0E\u4E13\u9898"] })] }), _jsxs("div", { className: "kl-toolbar", children: [_jsxs("label", { className: "kl-search", children: [_jsx(Icon, { name: "search", size: 19 }), _jsx("input", { ref: searchRef, "aria-label": "\u641C\u7D22\u8D44\u6599", placeholder: "\u641C\u7D22\u6B63\u6587\u3001\u6807\u9898\u3001\u6807\u7B7E\u2026", value: query, onChange: e => { setQuery(e.target.value); setLimit(40); } }), _jsx("kbd", { children: "Ctrl K" })] }), _jsxs("button", { "aria-label": pending ? `采集任务,${pending} 项处理中` : '采集任务', className: `kl-job-button ${pending ? 'has-jobs' : ''}`, onClick: () => setShowJobs(true), children: [_jsx(Icon, { name: "clock", size: 17 }), _jsx("span", { children: pending ? `${pending} 项处理中` : '采集任务' }), jobs.some(j => j.status === 'failed') && _jsx("i", {})] }), _jsxs("button", { "aria-label": "\u77E5\u8BC6\u6574\u7406", title: "\u77E5\u8BC6\u6574\u7406", className: `kl-job-button ${knowledgePending || draftCount ? 'has-jobs' : ''}`, onClick: () => { refresh(); if (stats)
175
+ setKnowledgeOpen({ root: stats.root }); }, children: [_jsx(Icon, { name: "note", size: 17 }), _jsx("span", { children: draftCount ? `${draftCount} 份待确认` : '知识整理' }), !!knowledgePending && _jsx("span", { className: "kl-spinner" })] }), _jsx("button", { className: "kl-icon-button", "aria-label": "\u8D44\u6599\u5E93\u8BBE\u7F6E", title: "\u8D44\u6599\u5E93\u8BBE\u7F6E", onClick: () => { setSettingsFromJobs(false); setShowInfo(true); }, children: _jsx(Icon, { name: "settings", size: 18 }) })] }), !!error && _jsxs("div", { className: "kl-error", role: "alert", children: [_jsx("span", { children: error }), _jsx("button", { "aria-label": "\u5173\u95ED\u9519\u8BEF", onClick: () => setError(''), children: _jsx(Icon, { name: "close", size: 16 }) })] }), _jsxs("div", { className: "kl-filterbar", children: [_jsxs("span", { children: [_jsx("b", { children: total }), " \u4EFD\u8D44\u6599", query && ` · “${query}”`] }), _jsxs("div", { children: [_jsxs("select", { "aria-label": "\u6309\u7C7B\u578B\u7B5B\u9009", value: filter.kind || '', onChange: e => { setFilter({ ...filter, kind: e.target.value || undefined }); setLimit(40); }, children: [_jsx("option", { value: "", children: "\u6240\u6709\u7C7B\u578B" }), Object.entries(kinds).map(([value, kind]) => _jsx("option", { value: value, children: kind.label }, value))] }), _jsx("span", { className: "kl-sort-label", children: "\u6700\u8FD1\u66F4\u65B0 \u2193" })] })] }), filter.trashed && _jsxs("div", { className: "kl-trash-toolbar", children: [_jsx("p", { children: stats?.canEmptyTrash ? (stats.purgePending ? `${stats.purgePending} 份资料的文件清理未完成,可再次清空以重试。` : '清空将永久删除整个回收站,不受当前搜索和类型筛选限制。') : '请重启资料库服务以启用清空回收站。' }), _jsxs("button", { className: "kl-secondary kl-danger", disabled: !stats?.canEmptyTrash || !(stats.trashed || stats.purgePending) || !!restoring, onClick: () => stats && setEmptyRoot(stats.root), children: [_jsx(Icon, { name: "trash", size: 15 }), "\u6E05\u7A7A\u56DE\u6536\u7AD9"] })] }), !filter.trashed && filter.kind !== 'note' && _jsxs("div", { className: "kl-knowledge-toolbar", children: [_jsx("p", { children: batchMode ? `已选择 ${checkedIds.length} / 10 份资料;切换搜索或分类会清除选择。` : knowledge?.smartSupported ? '零散资料先智能分组,再按组形成知识笔记。' : '请重启资料库服务以启用智能分组。' }), _jsxs("div", { children: [batchMode && _jsx("button", { className: "kl-primary", disabled: !checkedIds.length || !knowledge?.smartSupported, onClick: () => generateFor(checkedIds, 'smart'), children: "\u667A\u80FD\u5206\u6790\u5206\u7EC4" }), _jsx("button", { className: "kl-secondary", disabled: !knowledge?.smartSupported, onClick: () => { setBatchMode(!batchMode); setCheckedIds([]); }, children: batchMode ? '取消选择' : '智能整理' })] })] }), _jsxs("div", { className: `kl-body ${detail || detailLoading ? 'with-detail' : ''}`, children: [showNavigation && _jsxs("aside", { className: "kl-nav", id: "kl-directory", "aria-label": "\u5206\u7C7B\u4E0E\u4E13\u9898", children: [_jsxs("div", { className: "kl-directory-title", children: [_jsx("span", { children: "\u5206\u7C7B\u4E0E\u4E13\u9898" }), _jsx("button", { className: "kl-icon-button", "aria-label": "\u5173\u95ED\u5206\u7C7B\u5BFC\u822A", onClick: () => setShowNavigation(false), children: _jsx(Icon, { name: "close", size: 16 }) })] }), _jsxs("div", { className: "kl-nav-section", children: [_jsx("div", { className: "kl-nav-label", children: "\u5206\u7C7B" }), stats?.categories.filter(c => c.name !== '收件箱').map(c => _jsxs("button", { "aria-pressed": filter.category === c.name, className: "kl-nav-row", onClick: () => chooseFilter({ category: c.name }), children: [_jsx(Icon, { name: "folder", size: 16 }), _jsx("span", { title: c.name, children: c.name }), _jsx("small", { children: c.count })] }, c.name)), !stats?.categories.some(c => c.name !== '收件箱') && _jsx("p", { className: "kl-nav-hint", children: "\u6536\u96C6\u8D44\u6599\u65F6\u6DFB\u52A0\u5206\u7C7B" })] }), _jsxs("div", { className: "kl-nav-section", children: [_jsx("div", { className: "kl-nav-label", children: "\u4E13\u9898" }), stats?.collections.map(c => _jsxs("button", { "aria-pressed": filter.collection === c.name, className: "kl-nav-row", onClick: () => chooseFilter({ collection: c.name }), children: [_jsx("span", { className: "kl-collection-dot" }), _jsx("span", { title: c.name, children: c.name }), _jsx("small", { children: c.count })] }, c.name)), !stats?.collections.length && _jsx("p", { className: "kl-nav-hint", children: "\u6536\u96C6\u8D44\u6599\u65F6\u6DFB\u52A0\u4E13\u9898" })] }), !!stats?.tags.length && _jsxs("div", { className: "kl-nav-section", children: [_jsx("div", { className: "kl-nav-label", children: "\u6807\u7B7E" }), _jsx("div", { className: "kl-tag-cloud", children: stats.tags.slice(0, 15).map(tag => _jsxs("button", { "aria-pressed": filter.tag === tag.name, onClick: () => chooseFilter({ tag: tag.name }), children: ["#", tag.name] }, tag.name)) })] }), _jsxs("div", { className: "kl-nav-bottom", children: [_jsx("span", { className: "kl-live-dot" }), "\u4FDD\u5B58\u5728\u8FD9\u53F0\u7535\u8111"] })] }), _jsxs("section", { className: "kl-list", "aria-label": "\u8D44\u6599\u5217\u8868", "aria-busy": loading, children: [loading && !items.length ? _jsxs("div", { className: "kl-empty", children: [_jsx("span", { className: "kl-spinner" }), "\u6B63\u5728\u8BFB\u53D6\u8D44\u6599\u5E93\u2026"] }) : !items.length ? _jsxs("div", { className: "kl-empty", children: [_jsx(Icon, { name: filter.trashed ? 'trash' : 'inbox', size: 28 }), _jsx("h2", { children: filter.trashed ? '回收站中没有匹配的资料' : query || Object.values(filter).some(Boolean) ? '没有找到匹配的资料' : '还没有资料,收集的内容会显示在这里' }), query && _jsx("p", { children: "\u8BD5\u8BD5\u5176\u4ED6\u5173\u952E\u8BCD\uFF0C\u6216\u5207\u6362\u5206\u7C7B\u3002" }), _jsx("button", { className: "kl-secondary", onClick: () => query || Object.values(filter).some(Boolean) ? (setQuery(''), chooseFilter({})) : setEditor({ mode: 'capture' }), children: query || Object.values(filter).some(Boolean) ? '查看全部资料' : '收集第一份资料' })] }) : items.map(item => _jsxs("div", { className: `kl-item-row ${batchMode ? 'kl-selectable' : ''}`, children: [batchMode && _jsx("input", { type: "checkbox", className: "kl-material-checkbox", "aria-label": `选择 ${item.title}`, checked: checkedIds.includes(item.id), disabled: item.kind === 'note' || item.status !== 'ready' || (!!checkedIds.length && checkedIds.length >= 10 && !checkedIds.includes(item.id)), onChange: e => setCheckedIds(ids => e.target.checked ? [...ids, item.id] : ids.filter(id => id !== item.id)) }), _jsxs("button", { className: `kl-item ${selected === item.id ? 'selected' : ''}`, onClick: () => { setAnchor(undefined); setSelected(item.id); }, children: [_jsxs("div", { className: "kl-item-top", children: [_jsxs("span", { className: `kl-type kl-type-${item.kind}`, children: [_jsx(Icon, { name: kinds[item.kind].icon, size: 15 }), kinds[item.kind].label] }), _jsx("time", { children: date(item.createdAt) })] }), _jsx("h2", { children: item.title }), _jsx("p", { children: item.excerpt || item.extraction }), _jsxs("div", { className: "kl-item-bottom", children: [_jsxs("span", { className: "kl-category", children: [_jsx(Icon, { name: "folder", size: 13 }), item.category] }), item.tags.slice(0, 2).map(tag => _jsx("span", { className: "kl-chip", children: tag }, tag)), item.status === 'stored' && _jsx("span", { className: "kl-stored", children: "\u4EC5\u5F52\u6863" }), !item.deletedAt && item.kind !== 'note' && item.status === 'ready' && knowledge?.supported && _jsx(KnowledgeBadge, { state: knowledge.states[item.id] }), _jsx(Icon, { name: "arrow", size: 15 })] })] }), _jsx("button", { className: `kl-icon-button kl-row-action ${item.deletedAt ? '' : 'kl-danger'}`, title: item.deletedAt ? '恢复资料' : '删除资料', "aria-label": `${item.deletedAt ? '恢复' : '删除'} ${item.title}`, disabled: !stats || !!restoring, onClick: () => item.deletedAt ? void restoreItem(item.id) : askDelete(item), children: _jsx(Icon, { name: item.deletedAt ? 'restore' : 'trash', size: 16 }) })] }, item.id)), items.length < total && _jsx("button", { className: "kl-load-more", onClick: () => setLimit(v => Math.min(100, v + 40)), disabled: limit >= 100, children: limit >= 100 ? '请缩小搜索范围(最多显示 100 条)' : '加载更多' })] }), selected && _jsxs("section", { className: "kl-detail", "aria-label": "\u8D44\u6599\u8BE6\u60C5", ref: detailRef, children: [_jsxs("div", { className: "kl-detail-toolbar", children: [_jsxs("button", { className: "kl-back", onClick: () => { setSelected(undefined); setDetail(undefined); }, children: [_jsx(Icon, { name: "close", size: 18 }), _jsx("span", { children: "\u5173\u95ED\u9605\u8BFB" })] }), detail && _jsxs("div", { children: [_jsx("button", { title: "\u590D\u5236\u8D44\u6599\u5F15\u7528", "aria-label": "\u590D\u5236\u8D44\u6599\u5F15\u7528", onClick: () => void copy(`library://${detail.id}`), children: _jsx(Icon, { name: "copy", size: 17 }) }), _jsx("a", { title: "\u5BFC\u51FA Markdown", "aria-label": "\u5BFC\u51FA Markdown", href: `${API}/materials/${detail.id}/export`, children: _jsx(Icon, { name: "download", size: 17 }) }), detail.deletedAt ? _jsxs("button", { disabled: !stats || !!restoring, onClick: () => void restoreItem(detail.id), children: [_jsx(Icon, { name: "restore", size: 16 }), "\u6062\u590D\u8D44\u6599"] }) : _jsxs(_Fragment, { children: [_jsx("button", { onClick: () => setEditor({ mode: 'edit', source: detail }), children: "\u7F16\u8F91\u4FE1\u606F" }), _jsx("button", { className: "kl-danger", title: "\u5220\u9664\u8D44\u6599", "aria-label": "\u5220\u9664\u8D44\u6599", disabled: !stats || !!restoring, onClick: () => askDelete(detail), children: _jsx(Icon, { name: "trash", size: 17 }) })] })] })] }), detailLoading ? _jsxs("div", { className: "kl-empty", children: [_jsx("span", { className: "kl-spinner" }), "\u8BFB\u53D6\u4E2D\u2026"] }) : detail && _jsxs("article", { children: [_jsxs("div", { className: "kl-detail-kicker", children: [_jsx("span", { children: kinds[detail.kind].label }), _jsx("span", { children: detail.category })] }), _jsx("h2", { children: detail.title }), _jsxs("div", { className: "kl-source", children: [detail.sourceUrl ? _jsxs("a", { href: detail.sourceUrl, target: "_blank", rel: "noreferrer", children: [new URL(detail.sourceUrl).hostname, _jsx(Icon, { name: "arrow", size: 12 })] }) : _jsx("span", { children: detail.originalName || '本地收集' }), _jsx("span", { children: new Date(detail.createdAt).toLocaleDateString('zh-CN') }), detail.author && _jsx("span", { children: detail.author })] }), !!detail.tags.length && _jsx("div", { className: "kl-detail-tags", children: detail.tags.map(t => _jsxs("span", { className: "kl-chip", children: ["#", t] }, t)) }), detail.sourceNote && _jsx("aside", { className: "kl-source-note", children: detail.sourceNote }), detail.deletedAt && _jsxs("div", { className: "kl-notice", children: [_jsx(Icon, { name: "trash" }), _jsxs("div", { children: [_jsx("strong", { children: "\u8FD9\u4EFD\u8D44\u6599\u5728\u56DE\u6536\u7AD9" }), _jsx("p", { children: "\u539F\u6587\u4E0E\u6765\u6E90\u5F15\u7528\u4ECD\u53EF\u67E5\u770B\uFF0C\u6062\u590D\u540E\u56DE\u5230\u539F\u5206\u7C7B\u3002" })] })] }), detail.status === 'stored' && _jsxs("div", { className: "kl-notice", children: [_jsx(Icon, { name: "file" }), _jsxs("div", { children: [_jsx("strong", { children: "\u539F\u6587\u4EF6\u5DF2\u5B89\u5168\u5F52\u6863" }), _jsx("p", { children: detail.extraction })] })] }), !detail.deletedAt && detail.kind !== 'note' && detail.status === 'ready' && _jsxs("div", { className: "kl-knowledge-source", children: [_jsxs("div", { children: [_jsx(KnowledgeBadge, { state: knowledge?.states[detail.id] }), _jsx("span", { children: "\u4ECE\u8FD9\u4EFD\u8D44\u6599\u5F62\u6210\u7406\u89E3\u3001\u65B9\u6CD5\u4E0E\u5F85\u9A8C\u8BC1\u7684\u95EE\u9898\u3002" })] }), _jsxs("div", { className: "kl-knowledge-actions", children: [['running', 'draft', 'grouping'].includes(knowledge?.states[detail.id]?.status || '') ? _jsx("button", { className: "kl-secondary", onClick: () => stats && setKnowledgeOpen({ root: stats.root, jobId: knowledge?.states[detail.id]?.jobId }), children: knowledge?.states[detail.id]?.status === 'grouping' ? '查看分组方案' : '查看整理任务' }) : _jsx("button", { className: "kl-secondary", disabled: !knowledge?.supported, onClick: () => generateFor([detail.id]), children: "\u751F\u6210\u77E5\u8BC6\u7B14\u8BB0" }), knowledge?.states[detail.id]?.noteIds.map(id => _jsxs("button", { className: "kl-secondary", onClick: () => openKnowledgeSource(id), children: ["\u67E5\u770B\u77E5\u8BC6\u7B14\u8BB0", _jsx(Icon, { name: "arrow", size: 14 })] }, id))] }), !knowledge?.supported && _jsx("p", { className: "kl-muted", children: "\u8BF7\u91CD\u542F\u8D44\u6599\u5E93\u670D\u52A1\u4EE5\u542F\u7528\u77E5\u8BC6\u6574\u7406\u3002" })] }), _jsx("div", { className: "kl-content", children: detail.segments.map(segment => _jsxs("div", { className: `kl-segment ${anchor === segment.id ? 'kl-cited' : ''}`, id: segment.id, "data-segment-id": segment.id, children: [_jsxs("div", { className: "kl-segment-line", children: [_jsx("span", { children: segment.startSeconds !== undefined ? time(segment.startSeconds) : segment.id }), _jsx("button", { title: "\u6458\u5F55\u4E3A\u7B14\u8BB0", "aria-label": `摘录 ${segment.id} 为笔记`, onClick: () => setEditor({ mode: 'note', source: detail, segment }), children: _jsx(Icon, { name: "note", size: 14 }) }), _jsx("button", { title: "\u590D\u5236\u6BB5\u843D\u5F15\u7528", "aria-label": `复制 ${segment.id} 引用`, onClick: () => void copy(`library://${detail.id}/${segment.id}`), children: _jsx(Icon, { name: "copy", size: 14 }) })] }), /^#{1,6}\s/.test(segment.text) ? _jsx("h3", { children: segment.text.replace(/^#{1,6}\s+/, '') }) : _jsx("p", { children: segment.text })] }, segment.id)) }), !!detail.citations.length && _jsxs("div", { className: "kl-references", children: [_jsx("h3", { children: "\u6765\u6E90\u5F15\u7528" }), detail.citations.map((c, i) => _jsxs("button", { disabled: detail.missingSources?.includes(c.materialId), "aria-label": `查看引用 ${i + 1}`, onClick: () => { setAnchor(c.segmentId); setSelected(c.materialId); }, children: [_jsx(Icon, { name: "file", size: 15 }), _jsx("span", { children: detail.missingSources?.includes(c.materialId) ? '来源已永久删除' : `${c.materialId.slice(0, 15)}…${c.segmentId ? ` / ${c.segmentId}` : ''}` }), _jsx(Icon, { name: "arrow", size: 14 })] }, i))] }), _jsxs("footer", { className: "kl-detail-footer", children: [_jsxs("div", { children: [_jsx("span", { className: "kl-live-dot" }), detail.extraction] }), detail.originalFile && _jsxs("a", { href: `${API}/materials/${detail.id}/original`, children: ["\u4E0B\u8F7D\u539F\u59CB\u6587\u4EF6", _jsx(Icon, { name: "download", size: 14 })] }), _jsx("code", { children: detail.id })] })] })] })] })] }), generation && knowledge && _jsx(StartKnowledgeDialog, { root: generation.root, sourceIds: generation.sourceIds, model: knowledge, initialMode: generation.mode, onClose: () => setGeneration(undefined), onCreated: job => { setGeneration(undefined); setCheckedIds([]); setBatchMode(false); setKnowledgeOpen({ root: generation.root, jobId: job.id }); refresh(); } }), knowledgeOpen && knowledge && _jsx(KnowledgeJobsDialog, { root: knowledgeOpen.root, jobId: knowledgeOpen.jobId, overview: knowledge, onClose: () => setKnowledgeOpen(undefined), onRefresh: refresh, onRegenerate: (ids, mode) => { setKnowledgeOpen(undefined); generateFor(ids, mode); }, onOpenSource: openKnowledgeSource, onSaved: id => { openKnowledgeSource(id); notify('知识笔记已保存,原始资料已保留'); }, onNotify: notify }), editor && _jsx(EditorDialog, { editor: editor, categories: stats?.categories.map(c => c.name) || [], onClose: () => setEditor(undefined), onSaved: result => {
123
176
  setEditor(undefined);
124
177
  refresh();
125
178
  if (result.material) {
@@ -136,42 +189,183 @@ export function App() {
136
189
  setShowJobs(true);
137
190
  notify('网页已加入采集队列');
138
191
  }
139
- } }), showJobs && _jsx(Dialog, { title: "\u91C7\u96C6\u4EFB\u52A1", description: "\u7F51\u9875\u5728\u540E\u53F0\u63D0\u53D6\u6B63\u6587\u3002\u5173\u95ED\u5E94\u7528\u540E\uFF0C\u4E0B\u6B21\u542F\u52A8\u4F1A\u6062\u590D\u672A\u5B8C\u6210\u4EFB\u52A1\u3002", onClose: () => setShowJobs(false), children: _jsx("div", { className: "kl-jobs", children: !jobs.length ? _jsx("p", { className: "kl-muted", children: "\u8FD8\u6CA1\u6709\u91C7\u96C6\u4EFB\u52A1\u3002\u6DFB\u52A0\u4E00\u4E2A\u7F51\u9875\u94FE\u63A5\u5F00\u59CB\u3002" }) : jobs.map(job => _jsxs("div", { className: "kl-job", children: [_jsxs("div", { children: [_jsx("span", { className: `kl-job-status ${job.status}`, children: ({ queued: '排队中', running: '提取中', completed: '已完成', failed: '失败' })[job.status] }), _jsx("time", { children: date(job.createdAt) })] }), _jsx("strong", { children: job.url }), job.error && _jsx("p", { role: job.status === 'failed' ? 'alert' : undefined, children: job.error }), _jsxs("div", { children: [job.materialId && _jsxs("button", { onClick: () => { setSelected(job.materialId); setShowJobs(false); refresh(); }, children: ["\u67E5\u770B\u8D44\u6599 ", _jsx(Icon, { name: "arrow", size: 14 })] }), job.status === 'failed' && _jsx("button", { onClick: () => void post(`/jobs/${job.id}/retry`, {}).then(refresh).catch(e => setError(e.message)), children: "\u91CD\u65B0\u91C7\u96C6" })] })] }, job.id)) }) }), showInfo && _jsx(Dialog, { title: "\u4F60\u7684\u8D44\u6599\uFF0C\u7531\u4F60\u4FDD\u7BA1", description: "\u5185\u5BB9\u4EE5\u666E\u901A\u6587\u4EF6\u4FDD\u5B58\uFF0C\u7D22\u5F15\u53EF\u4EE5\u91CD\u5EFA\u3002", onClose: () => setShowInfo(false), children: _jsxs("div", { className: "kl-info", children: [_jsx("label", { children: "\u8D44\u6599\u5E93\u76EE\u5F55" }), _jsx("code", { children: stats?.root }), _jsx("p", { children: "\u6BCF\u4EFD\u8D44\u6599\u4FDD\u7559\u539F\u59CB\u6587\u4EF6\u3001\u6B63\u6587\u3001\u5143\u6570\u636E\u548C\u5F15\u7528\u6BB5\u843D\u3002\u5206\u7C7B\u5BF9\u5E94\u672C\u5730\u6587\u4EF6\u5939\uFF0C\u63D2\u4EF6\u5378\u8F7D\u4E0D\u4F1A\u5220\u9664\u8FD9\u4E9B\u6587\u4EF6\u3002" }), _jsx("p", { children: "\u672C\u7248\u672C\u53EF\u63D0\u53D6\u6587\u7AE0\u6B63\u6587\u53CA\u5B57\u5E55\uFF1B\u97F3\u89C6\u9891\u3001PDF \u7B49\u683C\u5F0F\u53EF\u4EE5\u5F52\u6863\uFF0C\u81EA\u52A8\u8F6C\u5199\u548C OCR \u5C1A\u672A\u63A5\u5165\u3002" }), stats?.warnings.map(w => _jsx("p", { role: "alert", children: w }, w)), _jsx("button", { className: "kl-secondary", onClick: () => void post('/rebuild', {}).then(() => { refresh(); notify('索引已重建'); }).catch(e => setError(e.message)), children: "\u4ECE\u8D44\u6599\u6587\u4EF6\u91CD\u5EFA\u7D22\u5F15" })] }) }), toast && _jsxs("div", { className: "kl-toast", role: "status", children: [_jsx(Icon, { name: "check", size: 17 }), toast] })] });
192
+ } }), deletion && _jsx(DeleteDialog, { item: deletion, onClose: () => setDeletion(undefined), onDeleted: () => { setDeletion(undefined); afterRemoval(deletion.id, '资料已移入回收站,可随时恢复'); } }), emptyRoot && _jsx(EmptyTrashDialog, { root: emptyRoot, onClose: () => setEmptyRoot(undefined), onEmptied: result => {
193
+ setEmptyRoot(undefined);
194
+ setSelected(undefined);
195
+ setDetail(undefined);
196
+ setAnchor(undefined);
197
+ setError('');
198
+ refresh();
199
+ if (result.errors.length || result.pending)
200
+ setError(`已永久删除 ${result.deleted} 份资料;${result.pending} 份文件待清理。${result.errors.join(';')}`);
201
+ else
202
+ notify(`回收站已清空,永久删除 ${result.deleted} 份资料`);
203
+ searchRef.current?.focus();
204
+ } }), showJobs && _jsx(Dialog, { title: "\u91C7\u96C6\u4EFB\u52A1", description: "\u7F51\u9875\u5728\u540E\u53F0\u63D0\u53D6\u6B63\u6587\u3002\u5173\u95ED\u5E94\u7528\u540E\uFF0C\u4E0B\u6B21\u542F\u52A8\u4F1A\u6062\u590D\u672A\u5B8C\u6210\u4EFB\u52A1\u3002", onClose: () => setShowJobs(false), children: _jsx("div", { className: "kl-jobs", children: !jobs.length ? _jsx("p", { className: "kl-muted", children: "\u8FD8\u6CA1\u6709\u91C7\u96C6\u4EFB\u52A1\u3002\u6DFB\u52A0\u4E00\u4E2A\u7F51\u9875\u94FE\u63A5\u5F00\u59CB\u3002" }) : jobs.map(job => _jsxs("div", { className: "kl-job", children: [_jsxs("div", { children: [_jsx("span", { className: `kl-job-status ${job.status}`, children: job.status === 'failed' && job.errorCode === 'PAGE_REQUIRES_BROWSER' ? '需浏览器验证' : ({ queued: '排队中', running: '提取中', completed: '已完成', failed: '失败' })[job.status] }), _jsx("time", { children: date(job.createdAt) })] }), _jsx("strong", { children: job.url }), job.error && _jsx("p", { role: job.status === 'failed' ? 'alert' : undefined, children: job.error }), _jsxs("div", { className: "kl-job-actions", children: [job.materialId && _jsxs("button", { onClick: () => { setSelected(job.materialId); setShowJobs(false); refresh(); }, children: ["\u67E5\u770B\u8D44\u6599 ", _jsx(Icon, { name: "arrow", size: 14 })] }), job.status === 'failed' && _jsxs(_Fragment, { children: [/^https?:\/\//i.test(job.url) && _jsxs("a", { href: job.url, target: "_blank", rel: "noopener noreferrer", children: ["\u6253\u5F00\u539F\u7F51\u9875", _jsx(Icon, { name: "arrow", size: 14 })] }), _jsx("button", { onClick: () => { setShowJobs(false); setEditor({ mode: 'capture', initialType: 'text', sourceUrl: job.url }); }, children: "\u7C98\u8D34\u6B63\u6587" }), /fake-ip/i.test(job.error || '') && _jsx("button", { onClick: () => { setShowJobs(false); setSettingsFromJobs(true); setShowInfo(true); }, children: "\u4EE3\u7406\u517C\u5BB9\u8BBE\u7F6E" }), _jsx("button", { onClick: () => void post(`/jobs/${job.id}/retry`, {}).then(refresh).catch(e => setError(e.message)), children: "\u91CD\u65B0\u91C7\u96C6" })] })] })] }, job.id)) }) }), showInfo && _jsx(SettingsDialog, { root: stats?.root || '', warnings: stats?.warnings || [], onClose: () => { setShowInfo(false); if (settingsFromJobs)
205
+ setShowJobs(true); setSettingsFromJobs(false); }, onChanged: () => {
206
+ setSelected(undefined);
207
+ setDetail(undefined);
208
+ setAnchor(undefined);
209
+ setItems([]);
210
+ setQuery('');
211
+ chooseFilter({});
212
+ refresh();
213
+ notify('资料库目录已切换');
214
+ }, onRebuilt: () => { refresh(); notify('索引已重建'); }, onProxySaved: enabled => { refresh(); notify(enabled ? '代理兼容已开启,可重新采集失败任务' : '代理兼容已关闭'); if (settingsFromJobs) {
215
+ setShowInfo(false);
216
+ setShowJobs(true);
217
+ setSettingsFromJobs(false);
218
+ } } }), toast && _jsxs("div", { className: "kl-toast", role: "status", children: [_jsx(Icon, { name: "check", size: 17 }), toast] })] });
140
219
  }
141
- function Dialog({ title, description, onClose, children }) {
142
- const ref = useRef(null);
143
- const closeRef = useRef(onClose);
144
- closeRef.current = onClose;
145
- useEffect(() => {
146
- const previous = document.activeElement;
147
- const timer = setTimeout(() => ref.current?.querySelector('input,textarea,button')?.focus(), 0);
148
- const key = (event) => {
149
- if (event.key === 'Escape')
150
- closeRef.current();
151
- if (event.key === 'Tab') {
152
- const elements = Array.from(ref.current?.querySelectorAll('button:not(:disabled),input:not(:disabled),textarea:not(:disabled),select,a[href]') || []);
153
- const first = elements[0], last = elements[elements.length - 1];
154
- if (event.shiftKey && document.activeElement === first) {
155
- event.preventDefault();
156
- last?.focus();
157
- }
158
- if (!event.shiftKey && document.activeElement === last) {
159
- event.preventDefault();
160
- first?.focus();
161
- }
162
- }
163
- };
164
- document.addEventListener('keydown', key);
165
- return () => { clearTimeout(timer); document.removeEventListener('keydown', key); previous?.focus(); };
166
- }, []);
167
- return _jsx("div", { className: "kl-modal-backdrop", onMouseDown: event => { if (event.target === event.currentTarget)
168
- onClose(); }, children: _jsxs("div", { className: "kl-dialog", role: "dialog", "aria-modal": "true", "aria-label": title, ref: ref, children: [_jsxs("header", { children: [_jsxs("div", { children: [_jsx("h2", { children: title }), _jsx("p", { children: description })] }), _jsx("button", { "aria-label": "\u5173\u95ED\u5F39\u7A97", onClick: onClose, children: _jsx(Icon, { name: "close" }) })] }), children] }) });
220
+ function DeleteDialog({ item, onClose, onDeleted }) {
221
+ const [busy, setBusy] = useState(false);
222
+ const [error, setError] = useState('');
223
+ async function remove() {
224
+ setBusy(true);
225
+ setError('');
226
+ try {
227
+ await post(`/materials/${item.id}`, { expectedRoot: item.root }, 'DELETE');
228
+ onDeleted();
229
+ }
230
+ catch (e) {
231
+ setError(e.message);
232
+ }
233
+ finally {
234
+ setBusy(false);
235
+ }
236
+ }
237
+ return _jsx(Dialog, { title: "\u5220\u9664\u8D44\u6599", description: "\u8D44\u6599\u5C06\u79FB\u5165\u56DE\u6536\u7AD9\uFF0C\u539F\u6587\u4EF6\u4FDD\u7559\uFF0C\u53EF\u968F\u65F6\u6062\u590D\u3002", onClose: () => { if (!busy)
238
+ onClose(); }, children: _jsxs("div", { className: "kl-delete-dialog", children: [_jsx("p", { className: "kl-delete-title", children: item.title }), _jsx("p", { className: "kl-muted", children: "\u5DF2\u6709\u7B14\u8BB0\u4ECD\u80FD\u67E5\u770B\u8FD9\u4EFD\u8D44\u6599\u7684\u6765\u6E90\u5F15\u7528\u3002" }), error && _jsx("p", { className: "kl-form-error", role: "alert", children: error }), _jsxs("footer", { children: [_jsx("button", { className: "kl-secondary", disabled: busy, onClick: onClose, children: "\u53D6\u6D88" }), _jsx("button", { className: "kl-secondary kl-danger", disabled: busy, onClick: () => void remove(), children: busy ? '正在删除…' : '移入回收站' })] })] }) });
239
+ }
240
+ function EmptyTrashDialog({ root, onClose, onEmptied }) {
241
+ const [snapshot, setSnapshot] = useState();
242
+ const [busy, setBusy] = useState(false);
243
+ const [error, setError] = useState('');
244
+ useEffect(() => { const abort = new AbortController(); void api('/trash', { signal: abort.signal }).then(setSnapshot).catch(e => { if (!abort.signal.aborted)
245
+ setError(e.message); }); return () => abort.abort(); }, []);
246
+ async function empty() {
247
+ if (!snapshot || busy)
248
+ return;
249
+ setBusy(true);
250
+ setError('');
251
+ try {
252
+ onEmptied(await post('/trash/empty', { expectedRoot: root, confirmed: true, token: snapshot.token }));
253
+ }
254
+ catch (e) {
255
+ setError(e.message);
256
+ setSnapshot(undefined);
257
+ }
258
+ finally {
259
+ setBusy(false);
260
+ }
261
+ }
262
+ return _jsx(Dialog, { title: "\u6E05\u7A7A\u56DE\u6536\u7AD9", description: "\u6C38\u4E45\u5220\u9664\u540E\u65E0\u6CD5\u6062\u590D\uFF0C\u8BF7\u786E\u8BA4\u6574\u4E2A\u56DE\u6536\u7AD9\u5185\u7684\u8D44\u6599\u90FD\u4E0D\u518D\u9700\u8981\u3002", onClose: () => { if (!busy)
263
+ onClose(); }, children: _jsxs("div", { className: "kl-delete-dialog", children: [_jsx("p", { className: "kl-delete-title", children: snapshot ? `将永久删除回收站中的全部 ${snapshot.count} 份资料。` : error ? '请关闭窗口后重新确认。' : '正在核对回收站…' }), !!snapshot?.pending && _jsxs("p", { children: ["\u53E6\u6709 ", snapshot.pending, " \u4EFD\u5DF2\u786E\u8BA4\u5220\u9664\u7684\u6587\u4EF6\u5C06\u7EE7\u7EED\u6E05\u7406\u3002"] }), _jsx("p", { className: "kl-muted", children: "\u6B63\u6587\u3001\u539F\u59CB\u9644\u4EF6\u548C\u7D22\u5F15\u5C06\u88AB\u79FB\u9664\u3002\u76F8\u5173\u7B14\u8BB0\u7684\u6B63\u6587\u4F1A\u4FDD\u7559\uFF0C\u5F15\u7528\u8FD9\u4E9B\u8D44\u6599\u7684\u4F4D\u7F6E\u5C06\u6807\u8BB0\u4E3A\u201C\u6765\u6E90\u5DF2\u6C38\u4E45\u5220\u9664\u201D\u3002" }), error && _jsx("p", { role: "alert", className: "kl-form-error", children: error }), _jsxs("footer", { children: [_jsx("button", { className: "kl-secondary", disabled: busy, onClick: onClose, children: "\u53D6\u6D88" }), _jsx("button", { className: "kl-secondary kl-danger", disabled: busy || !snapshot || !(snapshot.count || snapshot.pending), onClick: () => void empty(), children: busy ? '正在永久删除…' : '永久清空' })] })] }) });
264
+ }
265
+ function SettingsDialog({ root, warnings, onClose, onChanged, onRebuilt, onProxySaved }) {
266
+ const [settings, setSettings] = useState();
267
+ const [proxyMode, setProxyMode] = useState(false);
268
+ const [target, setTarget] = useState(root);
269
+ const [listing, setListing] = useState();
270
+ const [browsePath, setBrowsePath] = useState('');
271
+ const [browsing, setBrowsing] = useState(false);
272
+ const [busy, setBusy] = useState(false);
273
+ const [saving, setSaving] = useState(false);
274
+ const [error, setError] = useState('');
275
+ const request = useRef();
276
+ useEffect(() => { const abort = new AbortController(); void api('/settings', { signal: abort.signal }).then(data => { setSettings(data); setProxyMode(data.allowProxyFakeIp === true); }).catch(e => { if (!abort.signal.aborted)
277
+ setError(e.message); }); return () => { abort.abort(); request.current?.abort(); }; }, []);
278
+ useEffect(() => { setTarget(root); }, [root]);
279
+ async function browse(path) {
280
+ request.current?.abort();
281
+ const abort = new AbortController();
282
+ request.current = abort;
283
+ setBusy(true);
284
+ setError('');
285
+ setListing(undefined);
286
+ try {
287
+ const result = await post('/directory/list', { path }, 'POST', abort.signal);
288
+ setListing(result);
289
+ setBrowsePath(result.path);
290
+ }
291
+ catch (e) {
292
+ if (!abort.signal.aborted)
293
+ setError(e.message);
294
+ }
295
+ finally {
296
+ if (!abort.signal.aborted)
297
+ setBusy(false);
298
+ }
299
+ }
300
+ async function pick() {
301
+ if (settings?.picker === 'browse') {
302
+ setBrowsing(true);
303
+ setListing(undefined);
304
+ setBrowsePath(target);
305
+ await browse(target || undefined);
306
+ return;
307
+ }
308
+ setBusy(true);
309
+ setError('');
310
+ const abort = new AbortController();
311
+ request.current = abort;
312
+ try {
313
+ const result = await post('/directory/pick', {}, 'POST', abort.signal);
314
+ if (result.path)
315
+ setTarget(result.path);
316
+ }
317
+ catch (e) {
318
+ if (!abort.signal.aborted)
319
+ setError(e.message);
320
+ }
321
+ finally {
322
+ if (!abort.signal.aborted)
323
+ setBusy(false);
324
+ }
325
+ }
326
+ async function change() {
327
+ setSaving(true);
328
+ setError('');
329
+ try {
330
+ const result = await post('/settings/root', { root: target.trim(), expectedRoot: root });
331
+ setTarget(result.root);
332
+ onChanged();
333
+ }
334
+ catch (e) {
335
+ setError(e.message);
336
+ }
337
+ finally {
338
+ setSaving(false);
339
+ }
340
+ }
341
+ async function saveProxyMode() {
342
+ if (!settings)
343
+ return;
344
+ setSaving(true);
345
+ setError('');
346
+ try {
347
+ const result = await post('/settings/network', { allowProxyFakeIp: proxyMode, expected: settings.allowProxyFakeIp, expectedRoot: root });
348
+ setSettings({ ...settings, allowProxyFakeIp: result.allowProxyFakeIp });
349
+ setProxyMode(result.allowProxyFakeIp);
350
+ onProxySaved(result.allowProxyFakeIp);
351
+ }
352
+ catch (e) {
353
+ setError(e.message);
354
+ }
355
+ finally {
356
+ setSaving(false);
357
+ }
358
+ }
359
+ const changed = target.trim() && target.trim() !== root;
360
+ return _jsx(Dialog, { title: "\u4F60\u7684\u8D44\u6599\uFF0C\u7531\u4F60\u4FDD\u7BA1", description: "\u5185\u5BB9\u4EE5\u666E\u901A\u6587\u4EF6\u4FDD\u5B58\uFF0C\u7D22\u5F15\u53EF\u4EE5\u91CD\u5EFA\u3002", onClose: () => { if (!saving)
361
+ onClose(); }, children: _jsxs("div", { className: "kl-info", children: [browsing ? _jsxs("div", { className: "kl-folder-browser", "aria-label": "\u9009\u62E9\u8D44\u6599\u5E93\u76EE\u5F55", children: [_jsxs("div", { className: "kl-folder-heading", children: [_jsx("strong", { children: "\u9009\u62E9\u8D44\u6599\u5E93\u76EE\u5F55" }), _jsx("button", { className: "kl-secondary", disabled: busy, onClick: () => void browse(), children: "\u4E3B\u76EE\u5F55" })] }), _jsxs("form", { className: "kl-folder-path", onSubmit: e => { e.preventDefault(); void browse(browsePath); }, children: [_jsx("input", { "aria-label": "\u6D4F\u89C8\u76EE\u5F55\u8DEF\u5F84", value: browsePath, onChange: e => setBrowsePath(e.target.value) }), _jsx("button", { className: "kl-secondary", disabled: busy || !browsePath.trim(), children: "\u524D\u5F80" })] }), _jsx("nav", { className: "kl-folder-crumbs", "aria-label": "\u76EE\u5F55\u5C42\u7EA7", children: listing?.crumbs.map(c => _jsxs("button", { disabled: busy, onClick: () => void browse(c.path), children: [c.name, _jsx("span", { children: "/" })] }, c.path)) }), _jsx("div", { className: "kl-folder-entries", "aria-busy": busy, children: busy ? _jsx("p", { className: "kl-muted", children: "\u6B63\u5728\u8BFB\u53D6\u76EE\u5F55\u2026" }) : listing && !listing.entries.length ? _jsx("p", { className: "kl-muted", children: "\u6B64\u76EE\u5F55\u6CA1\u6709\u5B50\u6587\u4EF6\u5939" }) : listing?.entries.map(entry => _jsxs("button", { onClick: () => void browse(entry.path), children: [_jsx(Icon, { name: "folder", size: 17 }), _jsx("span", { children: entry.name }), _jsx(Icon, { name: "arrow", size: 15 })] }, entry.path)) }), listing?.truncated && _jsx("p", { className: "kl-muted", children: "\u5B50\u76EE\u5F55\u8F83\u591A\uFF0C\u4EC5\u663E\u793A\u90E8\u5206\u3002\u53EF\u586B\u5199\u5B8C\u6574\u8DEF\u5F84\u524D\u5F80\u3002" }), _jsxs("div", { className: "kl-folder-actions", children: [_jsx("button", { className: "kl-secondary", onClick: () => { request.current?.abort(); setBusy(false); setBrowsing(false); setError(''); }, children: "\u53D6\u6D88\u9009\u62E9" }), _jsx("button", { className: "kl-primary", disabled: busy || !listing, onClick: () => { setTarget(listing.path); setBrowsing(false); setError(''); }, children: "\u9009\u62E9\u6B64\u76EE\u5F55" })] })] }) : _jsxs(_Fragment, { children: [_jsx("label", { htmlFor: "kl-library-root", children: "\u8D44\u6599\u5E93\u76EE\u5F55" }), _jsxs("div", { className: "kl-root-field", children: [_jsx("input", { id: "kl-library-root", value: target, title: target, spellCheck: false, readOnly: !settings?.canChange, disabled: saving || busy, onChange: e => setTarget(e.target.value) }), _jsxs("button", { className: "kl-secondary", disabled: !settings?.canChange || !settings.picker || busy || saving, onClick: () => void pick(), children: [_jsx(Icon, { name: "folder", size: 16 }), busy ? '选择中…' : '选择目录'] })] }), settings && !settings.canChange && _jsx("p", { className: "kl-muted", children: "\u8BF7\u5728 Harness \u4E2D\u9009\u62E9\u548C\u5207\u6362\u76EE\u5F55\uFF1B\u72EC\u7ACB\u9884\u89C8\u53EF\u901A\u8FC7\u542F\u52A8\u914D\u7F6E\u6307\u5B9A\u76EE\u5F55\u3002" }), !!changed && _jsxs("div", { className: "kl-root-confirm", children: [_jsx("p", { children: "\u5207\u6362\u540E\u5C06\u8BFB\u53D6\u65B0\u76EE\u5F55\uFF0C\u5E76\u5C06\u540E\u7EED\u8D44\u6599\u4FDD\u5B58\u5230\u90A3\u91CC\u3002\u539F\u76EE\u5F55\u7684\u8D44\u6599\u4F1A\u4FDD\u7559\uFF0C\u4E0D\u4F1A\u81EA\u52A8\u642C\u8FC1\u3002" }), _jsxs("div", { children: [_jsx("button", { className: "kl-secondary", disabled: saving || busy, onClick: () => { setTarget(root); setError(''); }, children: "\u53D6\u6D88\u4FEE\u6539" }), _jsx("button", { className: "kl-primary", disabled: saving || busy, onClick: () => void change(), children: saving ? '正在切换…' : '切换资料库' })] })] }), _jsxs("section", { className: "kl-network-settings", "aria-label": "\u7F51\u9875\u91C7\u96C6\u8BBE\u7F6E", children: [_jsx("h3", { children: "\u7F51\u9875\u91C7\u96C6" }), _jsxs("label", { className: "kl-proxy-option", children: [_jsx("input", { type: "checkbox", "aria-describedby": "kl-proxy-help", checked: proxyMode, disabled: !settings?.canChangeNetwork || saving || busy, onChange: e => { setProxyMode(e.target.checked); setError(''); } }), _jsx("span", { children: "Clash/Mihomo \u4EE3\u7406\u517C\u5BB9\uFF08fake-IP\uFF09" })] }), _jsx("p", { id: "kl-proxy-help", children: "\u672C\u673A\u4F7F\u7528 Clash/Mihomo TUN \u7684 fake-IP DNS \u65F6\u5F00\u542F\uFF0C\u5141\u8BB8\u901A\u8FC7\u4EE3\u7406\u91C7\u96C6\u89E3\u6790\u5230 198.18.* \u6216 198.19.* \u7684\u7F51\u7AD9\u3002\u672C\u673A\u53CA\u5176\u4ED6\u5185\u7F51\u5730\u5740\u4ECD\u4E0D\u5141\u8BB8\u8BBF\u95EE\u3002" }), settings?.canChangeNetwork && !settings.canChange && _jsx("p", { className: "kl-muted", children: "\u6B64\u8BBE\u7F6E\u7528\u4E8E\u5F53\u524D\u8D44\u6599\u5E93\u7684\u72EC\u7ACB\u9884\u89C8\uFF0C\u4FDD\u5B58\u540E\u5BF9\u540E\u7EED\u91C7\u96C6\u751F\u6548\u3002" }), settings && !settings.canChangeNetwork && _jsx("p", { className: "kl-muted", children: "\u5F53\u524D\u73AF\u5883\u65E0\u6CD5\u4FDD\u5B58\u91C7\u96C6\u8BBE\u7F6E\u3002" }), settings?.canChangeNetwork && proxyMode !== settings.allowProxyFakeIp && _jsx("button", { className: "kl-secondary", disabled: saving || busy, onClick: () => void saveProxyMode(), children: saving ? '正在保存…' : '保存采集设置' })] }), _jsx("p", { children: "\u6BCF\u4EFD\u8D44\u6599\u4FDD\u7559\u539F\u59CB\u6587\u4EF6\u3001\u6B63\u6587\u3001\u5143\u6570\u636E\u548C\u5F15\u7528\u6BB5\u843D\u3002\u5206\u7C7B\u5BF9\u5E94\u672C\u5730\u6587\u4EF6\u5939\uFF0C\u63D2\u4EF6\u5378\u8F7D\u4E0D\u4F1A\u5220\u9664\u8FD9\u4E9B\u6587\u4EF6\u3002" }), _jsx("p", { children: "\u672C\u7248\u672C\u53EF\u63D0\u53D6\u6587\u7AE0\u6B63\u6587\u53CA\u5B57\u5E55\uFF1B\u97F3\u89C6\u9891\u3001PDF \u7B49\u683C\u5F0F\u53EF\u4EE5\u5F52\u6863\uFF0C\u81EA\u52A8\u8F6C\u5199\u548C OCR \u5C1A\u672A\u63A5\u5165\u3002" }), warnings.map(w => _jsx("p", { role: "alert", children: w }, w)), _jsx("button", { className: "kl-secondary", disabled: busy || saving, onClick: () => { setSaving(true); setError(''); void post('/rebuild', {}).then(onRebuilt).catch(e => setError(e.message)).finally(() => setSaving(false)); }, children: "\u4ECE\u8D44\u6599\u6587\u4EF6\u91CD\u5EFA\u7D22\u5F15" })] }), error && _jsx("p", { className: "kl-settings-error", role: "alert", children: error })] }) });
169
362
  }
170
363
  function EditorDialog({ editor, categories, onClose, onSaved }) {
171
364
  const editing = editor.mode === 'edit';
172
365
  const note = editor.mode === 'note';
173
366
  const source = editor.source;
174
- const [type, setType] = useState('url');
367
+ const [type, setType] = useState(editor.initialType || 'url');
368
+ const [sourceUrl, setSourceUrl] = useState(editor.sourceUrl || '');
175
369
  const [title, setTitle] = useState(editing ? source?.title || '' : note && source ? `${source.title} · 摘录` : '');
176
370
  const [content, setContent] = useState(editor.segment ? `> ${editor.segment.text}\n\n` : '');
177
371
  const [category, setCategory] = useState(editing ? source?.category || '收件箱' : note ? '知识笔记' : '收件箱');
@@ -188,6 +382,8 @@ function EditorDialog({ editor, categories, onClose, onSaved }) {
188
382
  setBusy(true);
189
383
  try {
190
384
  const data = { title, category, tags: splitTags(tags), collection, sourceNote };
385
+ if (!editing && !note && type === 'text' && sourceUrl.trim())
386
+ data.sourceUrl = sourceUrl.trim();
191
387
  let result;
192
388
  if (editing)
193
389
  result = await post(`/materials/${source.id}`, data, 'PATCH');
@@ -219,5 +415,5 @@ function EditorDialog({ editor, categories, onClose, onSaved }) {
219
415
  }
220
416
  const changeType = (value) => { setType(value); setContent(''); setError(''); };
221
417
  return _jsx(Dialog, { title: editing ? '整理资料' : note ? '写一份知识笔记' : '收集一份资料', description: editing ? '分类会同步到本地文件夹,原文和引用保持不变。' : note ? '写下理解,也留下它的来源。' : '先收集,再慢慢整理。所有内容保存在你的电脑。', onClose: () => { if (!busy)
222
- onClose(); }, children: _jsxs("form", { onSubmit: submit, className: "kl-editor", children: [!editing && !note && _jsx("div", { className: "kl-tabs", role: "tablist", "aria-label": "\u91C7\u96C6\u65B9\u5F0F", children: ['url', 'text', 'file'].map(value => _jsxs("button", { type: "button", role: "tab", "aria-selected": type === value, className: type === value ? 'active' : '', onClick: () => changeType(value), children: [_jsx(Icon, { name: value === 'url' ? 'web' : value === 'file' ? 'upload' : 'file', size: 17 }), value === 'url' ? '网页链接' : value === 'file' ? '本地文件' : '文本摘录'] }, value)) }), !editing && !note && type === 'url' && _jsxs("label", { children: ["\u7F51\u9875\u5730\u5740", _jsx("input", { type: "url", required: true, placeholder: "https://\u2026", value: content, onChange: e => setContent(e.target.value) }), _jsx("small", { children: "\u652F\u6301\u53EF\u76F4\u63A5\u8BBF\u95EE\u7684\u6587\u7AE0\u7F51\u9875\u3002\u767B\u5F55\u9875\u6216\u52A8\u6001\u9875\u9762\u53EF\u590D\u5236\u6B63\u6587\u5BFC\u5165\u3002" })] }), !editing && !note && type === 'file' && _jsxs("label", { className: "kl-upload", children: [_jsx(Icon, { name: "upload", size: 27 }), _jsx("strong", { children: file?.name || '选择或拖入文件' }), _jsx("span", { children: "\u6587\u672C\u3001HTML\u3001SRT / VTT \u5B57\u5E55\u53CA\u5176\u4ED6\u6587\u4EF6 \u00B7 \u6700\u5927 64 MB" }), _jsx("input", { "aria-label": "\u9009\u62E9\u672C\u5730\u6587\u4EF6", type: "file", onChange: e => setFile(e.target.files?.[0]) }), _jsx("small", { children: "\u89C6\u9891\u548C PDF \u53EF\u5F52\u6863\uFF0C\u5F53\u524D\u7248\u672C\u4E0D\u4F1A\u81EA\u52A8\u8F6C\u5199\u6216 OCR\u3002" })] }), _jsxs("label", { children: ["\u6807\u9898", !editing && !note && _jsx("em", { children: "\u53EF\u9009" }), _jsx("input", { required: editing || note, value: title, onChange: e => setTitle(e.target.value), placeholder: type === 'url' && !note ? '自动提取网页标题,也可以自己填写' : '给这份内容起个名字', maxLength: 300 })] }), !editing && (note || type === 'text') && _jsxs("label", { children: [note ? '笔记内容' : '正文', _jsx("textarea", { required: true, rows: 7, value: content, onChange: e => setContent(e.target.value), placeholder: note ? '记录你的理解、结论与尚待验证的问题…' : '粘贴想保留的文字,支持 Markdown…' })] }), !!citations.length && _jsxs("div", { className: "kl-citation-preview", children: [_jsx(Icon, { name: "file", size: 16 }), _jsxs("span", { children: ["\u5F15\u7528\uFF1A", source.title, editor.segment ? ` / ${editor.segment.id}` : ''] })] }), _jsxs("div", { className: "kl-form-grid", children: [_jsxs("label", { children: ["\u5206\u7C7B", _jsx("input", { list: "kl-categories", value: category, onChange: e => setCategory(e.target.value), placeholder: "\u4F8B\u5982 \u6280\u672F/AI" }), _jsx("datalist", { id: "kl-categories", children: categories.map(c => _jsx("option", { value: c }, c)) })] }), _jsxs("label", { children: ["\u4E13\u9898", _jsx("em", { children: "\u53EF\u9009" }), _jsx("input", { value: collection, onChange: e => setCollection(e.target.value), placeholder: "\u56F4\u7ED5\u4E00\u4E2A\u95EE\u9898\u7EC4\u7EC7\u8D44\u6599" })] })] }), _jsxs("label", { children: ["\u6807\u7B7E", _jsx("em", { children: "\u53EF\u9009" }), _jsx("input", { value: tags, onChange: e => setTags(e.target.value), placeholder: "\u7528\u9017\u53F7\u5206\u9694\uFF0C\u4F8B\u5982\uFF1A\u5F85\u8BFB\uFF0CRAG\uFF0C\u5B9E\u8DF5" })] }), _jsxs("label", { children: ["\u6765\u6E90\u5907\u6CE8", _jsx("em", { children: "\u53EF\u9009" }), _jsx("input", { value: sourceNote, onChange: e => setSourceNote(e.target.value), placeholder: "\u4E3A\u4EC0\u4E48\u503C\u5F97\u4FDD\u5B58\uFF0C\u6216\u8005\u5B83\u6765\u81EA\u54EA\u91CC" })] }), error && _jsx("div", { className: "kl-form-error", role: "alert", children: error }), _jsxs("footer", { children: [_jsxs("span", { children: [_jsx("span", { className: "kl-live-dot" }), "\u4EC5\u4FDD\u5B58\u5728\u672C\u5730"] }), _jsx("button", { type: "button", className: "kl-secondary", disabled: busy, onClick: onClose, children: "\u53D6\u6D88" }), _jsxs("button", { className: "kl-primary", type: "submit", disabled: busy, children: [busy ? '保存中…' : editing ? '保存修改' : note ? '保存笔记' : type === 'url' ? '开始采集' : '保存资料', !busy && _jsx(Icon, { name: "arrow", size: 16 })] })] })] }) });
418
+ onClose(); }, children: _jsxs("form", { onSubmit: submit, className: "kl-editor", children: [!editing && !note && _jsx("div", { className: "kl-tabs", role: "tablist", "aria-label": "\u91C7\u96C6\u65B9\u5F0F", children: ['url', 'text', 'file'].map(value => _jsxs("button", { type: "button", role: "tab", "aria-selected": type === value, className: type === value ? 'active' : '', onClick: () => changeType(value), children: [_jsx(Icon, { name: value === 'url' ? 'web' : value === 'file' ? 'upload' : 'file', size: 17 }), value === 'url' ? '网页链接' : value === 'file' ? '本地文件' : '文本摘录'] }, value)) }), !editing && !note && type === 'url' && _jsxs("label", { children: ["\u7F51\u9875\u5730\u5740", _jsx("input", { type: "url", required: true, placeholder: "https://\u2026", value: content, onChange: e => setContent(e.target.value) }), _jsx("small", { children: "\u652F\u6301\u53EF\u76F4\u63A5\u8BBF\u95EE\u7684\u6587\u7AE0\u7F51\u9875\u3002\u767B\u5F55\u9875\u6216\u52A8\u6001\u9875\u9762\u53EF\u590D\u5236\u6B63\u6587\u5BFC\u5165\u3002" })] }), !editing && !note && type === 'file' && _jsxs("label", { className: "kl-upload", children: [_jsx(Icon, { name: "upload", size: 27 }), _jsx("strong", { children: file?.name || '选择或拖入文件' }), _jsx("span", { children: "\u6587\u672C\u3001HTML\u3001SRT / VTT \u5B57\u5E55\u53CA\u5176\u4ED6\u6587\u4EF6 \u00B7 \u6700\u5927 64 MB" }), _jsx("input", { "aria-label": "\u9009\u62E9\u672C\u5730\u6587\u4EF6", type: "file", onChange: e => setFile(e.target.files?.[0]) }), _jsx("small", { children: "\u89C6\u9891\u548C PDF \u53EF\u5F52\u6863\uFF0C\u5F53\u524D\u7248\u672C\u4E0D\u4F1A\u81EA\u52A8\u8F6C\u5199\u6216 OCR\u3002" })] }), _jsxs("label", { children: ["\u6807\u9898", !editing && !note && _jsx("em", { children: "\u53EF\u9009" }), _jsx("input", { required: editing || note, value: title, onChange: e => setTitle(e.target.value), placeholder: type === 'url' && !note ? '自动提取网页标题,也可以自己填写' : '给这份内容起个名字', maxLength: 300 })] }), !editing && (note || type === 'text') && _jsxs("label", { children: [note ? '笔记内容' : '正文', _jsx("textarea", { required: true, rows: 7, value: content, onChange: e => setContent(e.target.value), placeholder: note ? '记录你的理解、结论与尚待验证的问题…' : '粘贴想保留的文字,支持 Markdown…' })] }), !editing && !note && type === 'text' && _jsxs("label", { children: ["\u6765\u6E90\u7F51\u9875", _jsx("em", { children: "\u53EF\u9009" }), _jsx("input", { type: "url", "aria-label": "\u6765\u6E90\u7F51\u9875", placeholder: "https://\u2026", value: sourceUrl, onChange: e => setSourceUrl(e.target.value) }), _jsx("small", { children: "\u4FDD\u7559\u539F\u7F51\u9875\u5730\u5740\uFF0C\u65B9\u4FBF\u56DE\u6EAF\u6765\u6E90\u3002" })] }), !!citations.length && _jsxs("div", { className: "kl-citation-preview", children: [_jsx(Icon, { name: "file", size: 16 }), _jsxs("span", { children: ["\u5F15\u7528\uFF1A", source.title, editor.segment ? ` / ${editor.segment.id}` : ''] })] }), _jsxs("div", { className: "kl-form-grid", children: [_jsxs("label", { children: ["\u5206\u7C7B", _jsx("input", { list: "kl-categories", value: category, onChange: e => setCategory(e.target.value), placeholder: "\u4F8B\u5982 \u6280\u672F/AI" }), _jsx("datalist", { id: "kl-categories", children: categories.map(c => _jsx("option", { value: c }, c)) })] }), _jsxs("label", { children: ["\u4E13\u9898", _jsx("em", { children: "\u53EF\u9009" }), _jsx("input", { value: collection, onChange: e => setCollection(e.target.value), placeholder: "\u56F4\u7ED5\u4E00\u4E2A\u95EE\u9898\u7EC4\u7EC7\u8D44\u6599" })] })] }), _jsxs("label", { children: ["\u6807\u7B7E", _jsx("em", { children: "\u53EF\u9009" }), _jsx("input", { value: tags, onChange: e => setTags(e.target.value), placeholder: "\u7528\u9017\u53F7\u5206\u9694\uFF0C\u4F8B\u5982\uFF1A\u5F85\u8BFB\uFF0CRAG\uFF0C\u5B9E\u8DF5" })] }), _jsxs("label", { children: ["\u6765\u6E90\u5907\u6CE8", _jsx("em", { children: "\u53EF\u9009" }), _jsx("input", { value: sourceNote, onChange: e => setSourceNote(e.target.value), placeholder: "\u4E3A\u4EC0\u4E48\u503C\u5F97\u4FDD\u5B58\uFF0C\u6216\u8005\u5B83\u6765\u81EA\u54EA\u91CC" })] }), error && _jsx("div", { className: "kl-form-error", role: "alert", children: error }), _jsxs("footer", { children: [_jsxs("span", { children: [_jsx("span", { className: "kl-live-dot" }), "\u4EC5\u4FDD\u5B58\u5728\u672C\u5730"] }), _jsx("button", { type: "button", className: "kl-secondary", disabled: busy, onClick: onClose, children: "\u53D6\u6D88" }), _jsxs("button", { className: "kl-primary", type: "submit", disabled: busy, children: [busy ? '保存中…' : editing ? '保存修改' : note ? '保存笔记' : type === 'url' ? '开始采集' : '保存资料', !busy && _jsx(Icon, { name: "arrow", size: 16 })] })] })] }) });
223
419
  }
@@ -0,0 +1,7 @@
1
+ import React from 'react';
2
+ export declare function Dialog({ title, description, onClose, children }: {
3
+ title: string;
4
+ description: string;
5
+ onClose: () => void;
6
+ children: React.ReactNode;
7
+ }): React.JSX.Element;
@@ -0,0 +1,32 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef } from 'react';
3
+ import { Icon } from './icons.js';
4
+ export function Dialog({ title, description, onClose, children }) {
5
+ const ref = useRef(null);
6
+ const closeRef = useRef(onClose);
7
+ closeRef.current = onClose;
8
+ useEffect(() => {
9
+ const previous = document.activeElement;
10
+ const timer = setTimeout(() => ref.current?.querySelector('input,textarea,button')?.focus(), 0);
11
+ const key = (event) => {
12
+ if (event.key === 'Escape')
13
+ closeRef.current();
14
+ if (event.key === 'Tab') {
15
+ const elements = Array.from(ref.current?.querySelectorAll('button:not(:disabled),input:not(:disabled),textarea:not(:disabled),select,a[href]') || []);
16
+ const first = elements[0], last = elements[elements.length - 1];
17
+ if (event.shiftKey && document.activeElement === first) {
18
+ event.preventDefault();
19
+ last?.focus();
20
+ }
21
+ if (!event.shiftKey && document.activeElement === last) {
22
+ event.preventDefault();
23
+ first?.focus();
24
+ }
25
+ }
26
+ };
27
+ document.addEventListener('keydown', key);
28
+ return () => { clearTimeout(timer); document.removeEventListener('keydown', key); previous?.focus(); };
29
+ }, []);
30
+ return _jsx("div", { className: "kl-modal-backdrop", onMouseDown: event => { if (event.target === event.currentTarget)
31
+ onClose(); }, children: _jsxs("div", { className: "kl-dialog", role: "dialog", "aria-modal": "true", "aria-label": title, ref: ref, children: [_jsxs("header", { children: [_jsxs("div", { children: [_jsx("h2", { children: title }), _jsx("p", { children: description })] }), _jsx("button", { "aria-label": "\u5173\u95ED\u5F39\u7A97", onClick: onClose, children: _jsx(Icon, { name: "close" }) })] }), children] }) });
32
+ }
@@ -0,0 +1,12 @@
1
+ import React from 'react';
2
+ import type { KnowledgeJob, KnowledgeOverview } from '../types.js';
3
+ interface Props {
4
+ root: string;
5
+ job: KnowledgeJob;
6
+ model: Pick<KnowledgeOverview, 'available' | 'model' | 'reason'>;
7
+ onRefresh: () => void;
8
+ onNotify: (message: string) => void;
9
+ onOpenSource: (id: string, segmentId?: string) => void;
10
+ }
11
+ export declare function GroupingEditor({ root, job, model, onRefresh, onNotify, onOpenSource }: Props): React.JSX.Element;
12
+ export {};