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.
- package/CHANGELOG.md +29 -0
- package/README.md +107 -12
- package/dist/client.js +1420 -303
- package/dist/core/extract.d.ts +1 -0
- package/dist/core/extract.js +62 -5
- package/dist/core/fetch.js +82 -11
- package/dist/core/grouping.d.ts +6 -0
- package/dist/core/grouping.js +71 -0
- package/dist/core/incremental.d.ts +14 -0
- package/dist/core/incremental.js +52 -0
- package/dist/core/jobs.d.ts +1 -0
- package/dist/core/jobs.js +4 -0
- package/dist/core/knowledge.d.ts +37 -0
- package/dist/core/knowledge.js +617 -0
- package/dist/core/library.d.ts +20 -2
- package/dist/core/library.js +271 -12
- package/dist/core/page-access.d.ts +6 -0
- package/dist/core/page-access.js +38 -0
- package/dist/core/purge.d.ts +20 -0
- package/dist/core/purge.js +106 -0
- package/dist/directory-settings.d.ts +18 -0
- package/dist/directory-settings.js +92 -0
- package/dist/http.d.ts +3 -1
- package/dist/http.js +121 -4
- package/dist/index.js +17 -5
- package/dist/knowledge-model.d.ts +18 -0
- package/dist/knowledge-model.js +116 -0
- package/dist/preview-settings.d.ts +6 -0
- package/dist/preview-settings.js +42 -0
- package/dist/public/app.css +126 -2
- package/dist/public/app.js +11 -7
- package/dist/standalone.js +8 -3
- package/dist/tools.d.ts +2 -1
- package/dist/tools.js +10 -1
- package/dist/types.d.ts +143 -1
- package/dist/ui/App.js +243 -47
- package/dist/ui/Dialog.d.ts +7 -0
- package/dist/ui/Dialog.js +32 -0
- package/dist/ui/Grouping.d.ts +12 -0
- package/dist/ui/Grouping.js +93 -0
- package/dist/ui/Knowledge.d.ts +26 -0
- package/dist/ui/Knowledge.js +106 -0
- package/dist/ui/api.d.ts +7 -0
- package/dist/ui/api.js +16 -0
- package/dist/ui/icons.js +2 -0
- 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
|
-
|
|
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
|
-
|
|
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
|
-
} }),
|
|
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
|
|
142
|
-
const
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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,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 {};
|