@lemoncat7/dsh-knowledge 0.6.0 → 0.7.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/README.md +7 -4
- package/lib/tools.d.ts +1 -1
- package/lib/tools.d.ts.map +1 -1
- package/lib/tools.js +148 -1
- package/lib/tools.js.map +1 -1
- package/package.json +1 -1
- package/web/app.js +209 -145
- package/web/styles.css +210 -127
package/web/app.js
CHANGED
|
@@ -11,6 +11,16 @@ const TYPE_DOCUMENTS = {
|
|
|
11
11
|
preference: 'preferences.md', fact: 'facts.md', decision: 'decisions.md', procedure: 'procedures.md', lesson: 'lessons.md',
|
|
12
12
|
}
|
|
13
13
|
const DOCUMENT_LAYOUT_KEY = 'dsh-knowledge.document-layout'
|
|
14
|
+
const DOCUMENT_COLUMN_LAYOUTS = Object.freeze({
|
|
15
|
+
library: {
|
|
16
|
+
widthKey: 'libraryWidth', hiddenKey: 'libraryHidden', cssVariable: '--library-width',
|
|
17
|
+
minimum: 170, maximum: 360, controls: 'knowledge-library-column',
|
|
18
|
+
},
|
|
19
|
+
documentList: {
|
|
20
|
+
widthKey: 'documentListWidth', hiddenKey: 'documentListHidden', cssVariable: '--document-list-width',
|
|
21
|
+
minimum: 210, maximum: 480, controls: 'document-list-column',
|
|
22
|
+
},
|
|
23
|
+
})
|
|
14
24
|
const pageParams = new URLSearchParams(location.search)
|
|
15
25
|
const mountContext = {
|
|
16
26
|
sessionId: pageParams.get('sessionId')?.trim() || '',
|
|
@@ -27,6 +37,8 @@ const state = {
|
|
|
27
37
|
stats: null,
|
|
28
38
|
overview: null,
|
|
29
39
|
knowledgeBases: [],
|
|
40
|
+
knowledgeBaseView: 'libraries',
|
|
41
|
+
knowledgeBaseQuery: '',
|
|
30
42
|
mounts: [],
|
|
31
43
|
resolvedMounts: [],
|
|
32
44
|
mountContext,
|
|
@@ -53,10 +65,13 @@ const state = {
|
|
|
53
65
|
candidateStatus: 'pending',
|
|
54
66
|
tokens: [],
|
|
55
67
|
service: { publicApiEnabled: false, publicApiPrefix: '/knowledge-api/v1', remote: false },
|
|
68
|
+
scrollPositions: new Map(),
|
|
56
69
|
loading: false,
|
|
57
70
|
error: '',
|
|
58
71
|
}
|
|
59
72
|
|
|
73
|
+
let scrollRestoreFrame = 0
|
|
74
|
+
|
|
60
75
|
function readDocumentLayout() {
|
|
61
76
|
const fallback = {
|
|
62
77
|
sidebarHidden: false, libraryHidden: false, documentListHidden: false,
|
|
@@ -93,6 +108,37 @@ function clampNumber(value, minimum, maximum, fallback) {
|
|
|
93
108
|
return Number.isFinite(number) ? Math.min(maximum, Math.max(minimum, Math.round(number))) : fallback
|
|
94
109
|
}
|
|
95
110
|
|
|
111
|
+
function captureScrollPosition() {
|
|
112
|
+
const shell = app.querySelector('.app-shell[data-view]')
|
|
113
|
+
if (!shell || shell.dataset.loading === 'true') return
|
|
114
|
+
const regions = {}
|
|
115
|
+
shell.querySelectorAll('[data-scroll-key]').forEach(node => {
|
|
116
|
+
const key = node.getAttribute('data-scroll-key')
|
|
117
|
+
if (key) regions[key] = { left: node.scrollLeft, top: node.scrollTop }
|
|
118
|
+
})
|
|
119
|
+
state.scrollPositions.set(shell.dataset.view, {
|
|
120
|
+
window: { left: window.scrollX, top: window.scrollY },
|
|
121
|
+
regions,
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function restoreScrollPosition(view) {
|
|
126
|
+
if (state.loading) return
|
|
127
|
+
const saved = state.scrollPositions.get(view)
|
|
128
|
+
if (!saved) return
|
|
129
|
+
if (scrollRestoreFrame) window.cancelAnimationFrame(scrollRestoreFrame)
|
|
130
|
+
scrollRestoreFrame = window.requestAnimationFrame(() => {
|
|
131
|
+
window.scrollTo(saved.window.left, saved.window.top)
|
|
132
|
+
app.querySelectorAll('[data-scroll-key]').forEach(node => {
|
|
133
|
+
const position = saved.regions[node.getAttribute('data-scroll-key')]
|
|
134
|
+
if (!position) return
|
|
135
|
+
node.scrollLeft = position.left
|
|
136
|
+
node.scrollTop = position.top
|
|
137
|
+
})
|
|
138
|
+
scrollRestoreFrame = 0
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
|
|
96
142
|
function element(tag, attributes = {}, ...children) {
|
|
97
143
|
const node = document.createElement(tag)
|
|
98
144
|
for (const [key, value] of Object.entries(attributes)) {
|
|
@@ -122,6 +168,16 @@ function paneToggleButton(pane, visible, onClick, label) {
|
|
|
122
168
|
}, element('span', { class: `pane-icon pane-icon-${pane}`, 'aria-hidden': 'true' }))
|
|
123
169
|
}
|
|
124
170
|
|
|
171
|
+
function interfaceIcon(name, className = 'interface-icon') {
|
|
172
|
+
const paths = {
|
|
173
|
+
search: 'M10.8 4.5a6.3 6.3 0 1 0 0 12.6 6.3 6.3 0 0 0 0-12.6Zm4.6 11 4.1 4',
|
|
174
|
+
}
|
|
175
|
+
return element('svg', {
|
|
176
|
+
class: className, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor',
|
|
177
|
+
'stroke-width': '1.8', 'stroke-linecap': 'round', 'stroke-linejoin': 'round', 'aria-hidden': 'true',
|
|
178
|
+
}, element('path', { d: paths[name] }))
|
|
179
|
+
}
|
|
180
|
+
|
|
125
181
|
function badge(label, variant = '') {
|
|
126
182
|
return element('span', { class: `badge ${variant}`.trim() }, label)
|
|
127
183
|
}
|
|
@@ -346,6 +402,7 @@ async function loadTokens() {
|
|
|
346
402
|
}
|
|
347
403
|
|
|
348
404
|
function renderShell() {
|
|
405
|
+
captureScrollPosition()
|
|
349
406
|
const titles = {
|
|
350
407
|
overview: ['概览', '知识库运行状态与最近活动'],
|
|
351
408
|
bases: ['知识库', '创建知识库,并限定项目与会话的召回和写入范围'],
|
|
@@ -356,6 +413,7 @@ function renderShell() {
|
|
|
356
413
|
const [title, subtitle] = titles[state.view]
|
|
357
414
|
const shell = element('div', {
|
|
358
415
|
class: 'app-shell', 'data-menu-open': String(state.menuOpen),
|
|
416
|
+
'data-view': state.view, 'data-loading': String(state.loading),
|
|
359
417
|
'data-sidebar-hidden': String(state.documentView.sidebarHidden),
|
|
360
418
|
style: `--sidebar-width: ${state.documentView.sidebarWidth}px`,
|
|
361
419
|
},
|
|
@@ -376,6 +434,7 @@ function renderShell() {
|
|
|
376
434
|
if (event.target === shell) { state.menuOpen = false; renderShell() }
|
|
377
435
|
})
|
|
378
436
|
app.replaceChildren(shell)
|
|
437
|
+
restoreScrollPosition(state.view)
|
|
379
438
|
}
|
|
380
439
|
|
|
381
440
|
function renderAppSidebarResizer() {
|
|
@@ -445,19 +504,21 @@ function setSidebarHidden(hidden) {
|
|
|
445
504
|
|
|
446
505
|
function renderSidebar() {
|
|
447
506
|
const pending = state.stats?.candidates.pending
|
|
448
|
-
const
|
|
449
|
-
['
|
|
450
|
-
|
|
507
|
+
const navGroups = [
|
|
508
|
+
['工作区', [['overview', '概览'], ['bases', '知识库'], ['entries', '文档'], ['candidates', '审核']]],
|
|
509
|
+
['服务', [['tokens', '访问管理']].filter(([id]) => id !== 'tokens' || !state.service.remote)],
|
|
510
|
+
].filter(([, items]) => items.length)
|
|
451
511
|
return element('aside', { class: 'sidebar', 'aria-label': '知识库导航' },
|
|
452
512
|
element('div', { class: 'brand' },
|
|
453
|
-
element('div', { class: 'brand-
|
|
454
|
-
element('div', { class: 'brand-copy' }, element('strong', {}, 'DSH Knowledge'), element('span', {}, '管理控制台')),
|
|
513
|
+
element('div', { class: 'brand-copy' }, element('span', {}, 'DSH Knowledge'), element('strong', {}, '知识库')),
|
|
455
514
|
),
|
|
456
|
-
element('nav', { class: 'nav' },
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
515
|
+
element('nav', { class: 'nav' }, navGroups.map(([group, items]) => element('div', { class: 'nav-group' },
|
|
516
|
+
element('div', { class: 'nav-group-label' }, group),
|
|
517
|
+
items.map(([id, label]) => element('button', {
|
|
518
|
+
type: 'button', class: 'nav-button', 'aria-current': state.view === id ? 'page' : undefined,
|
|
519
|
+
onClick: () => navigate(id),
|
|
520
|
+
}, element('span', { class: 'nav-label' }, label),
|
|
521
|
+
id === 'candidates' && pending ? element('span', { class: 'nav-count', 'aria-label': `${pending} 条待审核` }, pending) : null))))),
|
|
461
522
|
element('div', { class: 'sidebar-footer' },
|
|
462
523
|
element('div', { class: 'connection' }, element('span', { class: 'status-dot', 'aria-hidden': 'true' }), state.service.remote ? '中央知识库已连接' : '本地知识库已连接'),
|
|
463
524
|
AUTH_MODE === 'bearer' ? actionButton('退出当前会话', signOut, 'ghost small') : null,
|
|
@@ -476,7 +537,16 @@ function renderCurrentView() {
|
|
|
476
537
|
}
|
|
477
538
|
|
|
478
539
|
function loadingView() {
|
|
479
|
-
return element('div', { class: 'loading', role: 'status'
|
|
540
|
+
return element('div', { class: 'loading-skeleton', role: 'status', 'aria-label': '正在加载' },
|
|
541
|
+
element('span', { class: 'visually-hidden' }, '正在加载'),
|
|
542
|
+
element('div', { class: 'skeleton-line skeleton-title', 'aria-hidden': 'true' }),
|
|
543
|
+
element('div', { class: 'skeleton-line skeleton-copy', 'aria-hidden': 'true' }),
|
|
544
|
+
element('div', { class: 'skeleton-grid', 'aria-hidden': 'true' },
|
|
545
|
+
element('div', { class: 'skeleton-block' }),
|
|
546
|
+
element('div', { class: 'skeleton-block' }),
|
|
547
|
+
element('div', { class: 'skeleton-block' }),
|
|
548
|
+
),
|
|
549
|
+
)
|
|
480
550
|
}
|
|
481
551
|
|
|
482
552
|
function errorView(message, retry) {
|
|
@@ -516,24 +586,57 @@ function renderKnowledgeBases() {
|
|
|
516
586
|
const activeBases = state.knowledgeBases.filter(base => base.status === 'active')
|
|
517
587
|
const archivedBases = state.knowledgeBases.filter(base => base.status === 'archived')
|
|
518
588
|
const contextAvailable = Boolean(state.mountContext.projectId || state.mountContext.sessionId)
|
|
589
|
+
const query = state.knowledgeBaseQuery.trim().toLocaleLowerCase()
|
|
590
|
+
const matchesQuery = base => !query || [base.name, base.description, base.defaultTags.join(' '), base.writebackProvider, base.writebackModel]
|
|
591
|
+
.some(value => String(value || '').toLocaleLowerCase().includes(query))
|
|
592
|
+
const visibleActiveBases = activeBases.filter(matchesQuery)
|
|
593
|
+
const visibleArchivedBases = archivedBases.filter(matchesQuery)
|
|
594
|
+
const switcher = element('div', { class: 'workspace-switcher' },
|
|
595
|
+
element('div', { class: 'tabs workspace-tabs', role: 'tablist', 'aria-label': '知识库管理范围' }, [
|
|
596
|
+
['libraries', '知识库', activeBases.length],
|
|
597
|
+
['mounts', '项目与会话挂载', state.mounts.filter(mount => mount.enabled).length],
|
|
598
|
+
].map(([id, label, count]) => element('button', {
|
|
599
|
+
type: 'button', role: 'tab', class: 'tab', 'aria-selected': String(state.knowledgeBaseView === id),
|
|
600
|
+
onClick: () => { state.knowledgeBaseView = id; renderShell() },
|
|
601
|
+
}, element('span', {}, label), element('span', { class: 'tab-count' }, count)))),
|
|
602
|
+
element('p', {}, state.knowledgeBaseView === 'libraries'
|
|
603
|
+
? '管理知识库本身、标签和回写规则'
|
|
604
|
+
: '决定当前项目或会话可以召回、审核或直接写入哪些知识库'),
|
|
605
|
+
)
|
|
519
606
|
return element('div', { class: 'bases-page' },
|
|
520
|
-
|
|
607
|
+
switcher,
|
|
608
|
+
state.knowledgeBaseView === 'libraries' ? element('section', { class: 'library-management', 'aria-labelledby': 'bases-heading' },
|
|
521
609
|
element('div', { class: 'section-heading' },
|
|
522
|
-
element('div', {}, element('h2', { id: 'bases-heading' },
|
|
610
|
+
element('div', {}, element('h2', { id: 'bases-heading' }, '我的知识库'), element('p', {}, '名称和描述帮助 AI 判断知识应该写到哪里。')),
|
|
523
611
|
actionButton('+ 创建知识库', () => openKnowledgeBaseEditor(), 'primary'),
|
|
524
612
|
),
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
613
|
+
element('div', { class: 'knowledge-base-toolbar' },
|
|
614
|
+
element('div', { class: 'search-box base-search' }, interfaceIcon('search', 'search-symbol'), element('input', {
|
|
615
|
+
class: 'input', type: 'search', value: state.knowledgeBaseQuery,
|
|
616
|
+
placeholder: '搜索名称、描述、标签或模型', 'aria-label': '搜索知识库',
|
|
617
|
+
onInput: event => {
|
|
618
|
+
state.knowledgeBaseQuery = event.target.value
|
|
619
|
+
renderShell()
|
|
620
|
+
document.querySelector('.base-search input')?.focus()
|
|
621
|
+
},
|
|
622
|
+
})),
|
|
623
|
+
element('div', { class: 'base-result-summary', 'aria-live': 'polite' }, query
|
|
624
|
+
? `找到 ${visibleActiveBases.length + visibleArchivedBases.length} 个知识库`
|
|
625
|
+
: `${activeBases.length} 个可用 · ${archivedBases.length} 个已归档`),
|
|
626
|
+
),
|
|
627
|
+
visibleActiveBases.length
|
|
628
|
+
? element('div', { class: 'base-grid' }, visibleActiveBases.map(renderKnowledgeBaseCard))
|
|
629
|
+
: query && visibleArchivedBases.length === 0
|
|
630
|
+
? emptyState('没有匹配的知识库', '尝试搜索名称、描述、标签或回写模型。')
|
|
631
|
+
: !query ? emptyState('还没有可用知识库', '使用右上角按钮创建第一个知识库。') : null,
|
|
632
|
+
visibleArchivedBases.length ? element('details', { class: 'archived-bases', open: Boolean(query) },
|
|
633
|
+
element('summary', {}, element('span', {}, '已归档知识库'), element('span', { class: 'summary-count' }, visibleArchivedBases.length)),
|
|
634
|
+
element('div', { class: 'base-grid' }, visibleArchivedBases.map(renderKnowledgeBaseCard)),
|
|
531
635
|
) : null,
|
|
532
|
-
),
|
|
533
|
-
element('section', { class: 'mount-section', 'aria-labelledby': 'mounts-heading' },
|
|
636
|
+
) : element('section', { class: 'mount-section', 'aria-labelledby': 'mounts-heading' },
|
|
534
637
|
element('div', { class: 'section-heading' }, element('div', {},
|
|
535
|
-
element('h2', { id: 'mounts-heading' }, '
|
|
536
|
-
element('p', {}, '
|
|
638
|
+
element('h2', { id: 'mounts-heading' }, '挂载范围'),
|
|
639
|
+
element('p', {}, '会话默认继承项目;只有需要差异时才创建会话覆盖。'),
|
|
537
640
|
)),
|
|
538
641
|
contextAvailable
|
|
539
642
|
? element('div', { class: 'mount-context' },
|
|
@@ -554,21 +657,24 @@ function contextPill(label, value) {
|
|
|
554
657
|
|
|
555
658
|
function renderKnowledgeBaseCard(base) {
|
|
556
659
|
const archived = base.status === 'archived'
|
|
660
|
+
const visibleTags = base.defaultTags.slice(0, 4)
|
|
661
|
+
const hiddenTagCount = Math.max(0, base.defaultTags.length - visibleTags.length)
|
|
557
662
|
return element('article', { class: `base-card${archived ? ' is-archived' : ''}` },
|
|
558
663
|
element('div', { class: 'base-card-header' },
|
|
559
|
-
element('div', {
|
|
664
|
+
element('div', { class: 'base-card-identity' },
|
|
665
|
+
element('span', { class: 'base-symbol', 'aria-hidden': 'true' }, base.name.trim().slice(0, 1).toLocaleUpperCase() || 'K'),
|
|
666
|
+
element('div', {}, element('h3', {}, base.name), element('small', { title: base.id }, base.id === 'default' ? '系统默认库' : `ID · ${base.id}`)),
|
|
667
|
+
),
|
|
560
668
|
badge(archived ? '已归档' : '可用', archived ? '' : 'success'),
|
|
561
669
|
),
|
|
562
|
-
element('
|
|
563
|
-
base.
|
|
564
|
-
?
|
|
565
|
-
: element('span', { class: '
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
:
|
|
569
|
-
|
|
570
|
-
element('strong', {}, '回写模型'),
|
|
571
|
-
badge(base.writebackProvider && base.writebackModel ? `${base.writebackProvider} / ${base.writebackModel}` : '跟随当前会话'),
|
|
670
|
+
element('p', { class: 'base-description' }, base.description || '通用知识库,尚未设置匹配描述。'),
|
|
671
|
+
element('div', { class: 'base-card-tags' }, visibleTags.length
|
|
672
|
+
? visibleTags.map(tag => element('span', { class: 'tag' }, `#${tag}`))
|
|
673
|
+
: element('span', { class: 'tag is-empty' }, '无默认标签'),
|
|
674
|
+
hiddenTagCount ? element('span', { class: 'tag' }, `+${hiddenTagCount}`) : null),
|
|
675
|
+
element('div', { class: 'base-card-meta' },
|
|
676
|
+
element('span', {}, element('strong', {}, '回写模型'), base.writebackProvider && base.writebackModel ? `${base.writebackProvider} / ${base.writebackModel}` : '跟随当前会话'),
|
|
677
|
+
base.extractionInstructions ? element('span', {}, element('strong', {}, '提取规则'), '已设置') : null,
|
|
572
678
|
),
|
|
573
679
|
element('div', { class: 'base-card-actions' },
|
|
574
680
|
actionButton('查看知识', () => {
|
|
@@ -657,7 +763,7 @@ function renderMountManager(activeBases) {
|
|
|
657
763
|
renderShell()
|
|
658
764
|
},
|
|
659
765
|
}, label))),
|
|
660
|
-
element('div', { class: 'search-box mount-search' },
|
|
766
|
+
element('div', { class: 'search-box mount-search' }, interfaceIcon('search', 'search-symbol'), searchInput),
|
|
661
767
|
filter,
|
|
662
768
|
),
|
|
663
769
|
element('div', { class: 'mount-target-line' },
|
|
@@ -666,7 +772,7 @@ function renderMountManager(activeBases) {
|
|
|
666
772
|
element('span', { class: 'field-hint' }, `显示 ${visibleRows.length} / ${activeBases.length}`),
|
|
667
773
|
),
|
|
668
774
|
visibleRows.length
|
|
669
|
-
? element('div', { class: 'mount-table', role: 'list', 'aria-label': '知识库挂载列表' }, visibleRows.map(({ base, view }) => renderMountListRow(base, view, manager.targetKind, targetId)))
|
|
775
|
+
? element('div', { class: 'mount-table', role: 'list', 'aria-label': '知识库挂载列表', 'data-scroll-key': 'mount-table' }, visibleRows.map(({ base, view }) => renderMountListRow(base, view, manager.targetKind, targetId)))
|
|
670
776
|
: emptyState('没有匹配的知识库', '调整搜索词或筛选条件。'),
|
|
671
777
|
element('div', { class: 'mount-bulk-bar', 'aria-live': 'polite' },
|
|
672
778
|
element('strong', {}, `已选择 ${selectedCount} 个`),
|
|
@@ -760,19 +866,16 @@ function renderEntries() {
|
|
|
760
866
|
|
|
761
867
|
return element('section', { class: 'document-page', 'aria-labelledby': 'documents-heading' },
|
|
762
868
|
element('div', { class: 'document-page-toolbar' },
|
|
763
|
-
|
|
869
|
+
renderDocumentModeTabs(),
|
|
870
|
+
element('div', { class: 'search-box document-global-search' }, interfaceIcon('search', 'search-symbol'), search),
|
|
764
871
|
element('select', {
|
|
765
872
|
class: 'select compact-library-picker', 'aria-label': '选择知识库', value: view.knowledgeBaseId,
|
|
766
873
|
onChange: (event) => { view.knowledgeBaseId = event.target.value; view.documentId = ''; selectDefaultDocument(); renderShell() },
|
|
767
874
|
}, activeBases.map(base => element('option', { value: base.id, selected: base.id === view.knowledgeBaseId }, base.name))),
|
|
768
|
-
element('div', { class: '
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
paneToggleButton('library', !view.libraryHidden, () => setDocumentColumnHidden('library', !view.libraryHidden), '知识库二级栏'),
|
|
772
|
-
paneToggleButton('document', !view.documentListHidden, () => setDocumentColumnHidden('documentList', !view.documentListHidden), '文档列表栏'),
|
|
875
|
+
element('div', { class: 'document-toolbar-actions' },
|
|
876
|
+
renderDocumentColumnControls(),
|
|
877
|
+
actionButton('+ 新建知识', () => openEntryEditor(), 'primary'),
|
|
773
878
|
),
|
|
774
|
-
actionButton('调整栏宽', openLayoutEditor, 'ghost small', { 'aria-label': '调整主导航、知识库和文档栏宽度' }),
|
|
775
|
-
actionButton('+ 新建知识', () => openEntryEditor(), 'primary'),
|
|
776
879
|
),
|
|
777
880
|
element('div', {
|
|
778
881
|
class: 'document-browser',
|
|
@@ -780,11 +883,11 @@ function renderEntries() {
|
|
|
780
883
|
'data-document-list-hidden': String(view.documentListHidden),
|
|
781
884
|
style: `--library-width:${view.libraryWidth}px;--document-list-width:${view.documentListWidth}px`,
|
|
782
885
|
},
|
|
783
|
-
element('aside', { class: 'knowledge-library-column', 'aria-label': '知识库列表' },
|
|
886
|
+
element('aside', { id: 'knowledge-library-column', class: 'knowledge-library-column', 'aria-label': '知识库列表' },
|
|
784
887
|
element('header', { class: 'column-header' },
|
|
785
888
|
element('div', {}, element('h2', { id: 'documents-heading' }, '知识库'), element('span', {}, `${activeBases.length} 个可用`)),
|
|
786
889
|
),
|
|
787
|
-
activeBases.length ? element('div', { class: 'library-list', role: 'listbox', tabindex: '0', onKeyDown: event => moveDocumentSelection(event, 'base') },
|
|
890
|
+
activeBases.length ? element('div', { class: 'library-list', role: 'listbox', tabindex: '0', 'data-scroll-key': 'library-list', onKeyDown: event => moveDocumentSelection(event, 'base') },
|
|
788
891
|
activeBases.map(base => {
|
|
789
892
|
const documentCount = state.documents.filter(document => document.knowledgeBaseId === base.id).length
|
|
790
893
|
const selected = base.id === view.knowledgeBaseId
|
|
@@ -798,11 +901,11 @@ function renderEntries() {
|
|
|
798
901
|
element('footer', { class: 'column-footer' }, actionButton('+ 新建知识库', () => openKnowledgeBaseEditor(), 'ghost small')),
|
|
799
902
|
),
|
|
800
903
|
renderColumnResizer('library', '调整知识库栏宽度'),
|
|
801
|
-
element('aside', { class: 'document-list-column', 'aria-label': '文档列表' },
|
|
904
|
+
element('aside', { id: 'document-list-column', class: 'document-list-column', 'aria-label': '文档列表' },
|
|
802
905
|
element('header', { class: 'column-header' },
|
|
803
906
|
element('div', {}, element('h2', {}, selectedBase?.name || '文档'), element('span', {}, query ? `找到 ${visibleDocuments.length} 篇` : `${allBaseDocuments.length} 篇文档`)),
|
|
804
907
|
),
|
|
805
|
-
visibleDocuments.length ? element('div', { class: 'document-list', role: 'listbox', tabindex: '0', onKeyDown: event => moveDocumentSelection(event, 'document') },
|
|
908
|
+
visibleDocuments.length ? element('div', { class: 'document-list', role: 'listbox', tabindex: '0', 'data-scroll-key': 'document-list', onKeyDown: event => moveDocumentSelection(event, 'document') },
|
|
806
909
|
visibleDocuments.map(document => element('button', {
|
|
807
910
|
type: 'button', class: 'document-row', role: 'option', 'aria-selected': String(document.id === view.documentId),
|
|
808
911
|
onClick: () => { view.documentId = document.id; renderShell() },
|
|
@@ -824,104 +927,56 @@ function setDocumentColumnHidden(column, hidden) {
|
|
|
824
927
|
renderShell()
|
|
825
928
|
}
|
|
826
929
|
|
|
827
|
-
function
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
return element('div', {
|
|
833
|
-
class: 'column-resizer', 'data-column': column, role: 'separator', tabindex: '0',
|
|
834
|
-
title: `${label};可拖动或使用左右方向键`,
|
|
835
|
-
'aria-label': label, 'aria-orientation': 'vertical',
|
|
836
|
-
'aria-valuemin': minimum, 'aria-valuemax': maximum, 'aria-valuenow': value,
|
|
837
|
-
onPointerDown: event => startColumnResize(event, column, minimum, maximum),
|
|
838
|
-
onKeyDown: event => resizeColumnWithKeyboard(event, column, minimum, maximum),
|
|
839
|
-
}, element('span', { 'aria-hidden': 'true' }, '⋮'))
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
function openLayoutEditor() {
|
|
843
|
-
const fields = [
|
|
844
|
-
layoutRangeField('主导航栏', state.documentView.sidebarWidth, 190, 340),
|
|
845
|
-
layoutRangeField('知识库二级栏', state.documentView.libraryWidth, 170, 360),
|
|
846
|
-
layoutRangeField('文档列表栏', state.documentView.documentListWidth, 210, 480),
|
|
847
|
-
]
|
|
848
|
-
const visibility = [
|
|
849
|
-
layoutVisibilityOption('显示主导航栏', !state.documentView.sidebarHidden),
|
|
850
|
-
layoutVisibilityOption('显示知识库二级栏', !state.documentView.libraryHidden),
|
|
851
|
-
layoutVisibilityOption('显示文档列表栏', !state.documentView.documentListHidden),
|
|
852
|
-
]
|
|
853
|
-
const reset = actionButton('恢复默认布局', () => {
|
|
854
|
-
const defaults = [236, 220, 280]
|
|
855
|
-
fields.forEach((field, index) => field.setValue(defaults[index]))
|
|
856
|
-
visibility.forEach(option => { option.input.checked = true })
|
|
857
|
-
}, 'ghost small')
|
|
858
|
-
const body = element('div', { class: 'layout-editor' },
|
|
859
|
-
element('div', { class: 'layout-visibility', 'aria-label': '边栏显示设置' }, visibility.map(option => option.wrapper)),
|
|
860
|
-
fields.map(field => field.wrapper),
|
|
861
|
-
element('div', { class: 'layout-editor-note' },
|
|
862
|
-
element('span', {}, '也可以直接拖动栏与栏之间的三点分隔线。窗口过窄时会自动切换为紧凑布局。'),
|
|
863
|
-
reset,
|
|
864
|
-
),
|
|
930
|
+
function renderDocumentColumnControls() {
|
|
931
|
+
return element('div', { class: 'document-column-controls', role: 'group', 'aria-label': '显示文档栏位' },
|
|
932
|
+
element('span', { class: 'document-column-controls-label', 'aria-hidden': 'true' }, '显示'),
|
|
933
|
+
documentColumnControl('library', '知识库栏'),
|
|
934
|
+
documentColumnControl('documentList', '文档栏'),
|
|
865
935
|
)
|
|
866
|
-
openModal({
|
|
867
|
-
title: '调整边栏宽度',
|
|
868
|
-
description: '分别设置管理导航、知识库二级栏和文档列表栏。',
|
|
869
|
-
body,
|
|
870
|
-
primaryLabel: '应用布局',
|
|
871
|
-
onPrimary: async () => {
|
|
872
|
-
state.documentView.sidebarHidden = !visibility[0].input.checked
|
|
873
|
-
state.documentView.libraryHidden = !visibility[1].input.checked
|
|
874
|
-
state.documentView.documentListHidden = !visibility[2].input.checked
|
|
875
|
-
state.documentView.sidebarWidth = fields[0].value()
|
|
876
|
-
state.documentView.libraryWidth = fields[1].value()
|
|
877
|
-
state.documentView.documentListWidth = fields[2].value()
|
|
878
|
-
saveDocumentLayout()
|
|
879
|
-
renderShell()
|
|
880
|
-
showToast('边栏宽度已保存。')
|
|
881
|
-
return true
|
|
882
|
-
},
|
|
883
|
-
})
|
|
884
936
|
}
|
|
885
937
|
|
|
886
|
-
function
|
|
887
|
-
const
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
938
|
+
function documentColumnControl(column, label) {
|
|
939
|
+
const layout = DOCUMENT_COLUMN_LAYOUTS[column]
|
|
940
|
+
const visible = !state.documentView[layout.hiddenKey]
|
|
941
|
+
const action = `${visible ? '收起' : '展开'}${label}`
|
|
942
|
+
return element('button', {
|
|
943
|
+
type: 'button', class: 'document-column-control', 'data-column': column,
|
|
944
|
+
'aria-label': action, 'aria-pressed': String(visible), title: action,
|
|
945
|
+
onClick: () => setDocumentColumnHidden(column, visible),
|
|
946
|
+
}, label)
|
|
892
947
|
}
|
|
893
948
|
|
|
894
|
-
function
|
|
895
|
-
const
|
|
896
|
-
const
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
),
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
}
|
|
949
|
+
function renderColumnResizer(column, label) {
|
|
950
|
+
const layout = DOCUMENT_COLUMN_LAYOUTS[column]
|
|
951
|
+
const hidden = state.documentView[layout.hiddenKey]
|
|
952
|
+
if (hidden) return null
|
|
953
|
+
const value = state.documentView[layout.widthKey]
|
|
954
|
+
return element('div', {
|
|
955
|
+
class: 'column-resizer', 'data-column': column, role: 'separator', tabindex: '0',
|
|
956
|
+
title: `${label};可拖动或使用左右方向键`,
|
|
957
|
+
'aria-label': label, 'aria-controls': layout.controls, 'aria-orientation': 'vertical',
|
|
958
|
+
'aria-valuemin': layout.minimum, 'aria-valuemax': layout.maximum, 'aria-valuenow': value,
|
|
959
|
+
'aria-valuetext': `${value} 像素`,
|
|
960
|
+
onPointerDown: event => startColumnResize(event, column),
|
|
961
|
+
onKeyDown: event => resizeColumnWithKeyboard(event, column),
|
|
962
|
+
}, element('span', { 'aria-hidden': 'true' }, '⋮'))
|
|
909
963
|
}
|
|
910
964
|
|
|
911
|
-
function startColumnResize(event, column
|
|
965
|
+
function startColumnResize(event, column) {
|
|
912
966
|
if (event.button !== 0) return
|
|
913
967
|
event.preventDefault()
|
|
968
|
+
const layout = DOCUMENT_COLUMN_LAYOUTS[column]
|
|
914
969
|
const handle = event.currentTarget
|
|
915
970
|
const browser = handle.closest('.document-browser')
|
|
916
971
|
if (!browser) return
|
|
917
972
|
const startX = event.clientX
|
|
918
|
-
const startWidth =
|
|
973
|
+
const startWidth = state.documentView[layout.widthKey]
|
|
919
974
|
handle.setPointerCapture?.(event.pointerId)
|
|
920
975
|
handle.classList.add('is-dragging')
|
|
921
976
|
document.body.classList.add('is-resizing-columns')
|
|
922
977
|
const move = moveEvent => {
|
|
923
|
-
const width = clampNumber(startWidth + moveEvent.clientX - startX, minimum, maximum, startWidth)
|
|
924
|
-
|
|
978
|
+
const width = clampNumber(startWidth + moveEvent.clientX - startX, layout.minimum, layout.maximum, startWidth)
|
|
979
|
+
setDocumentColumnWidth(column, width, browser, handle)
|
|
925
980
|
}
|
|
926
981
|
const finish = () => {
|
|
927
982
|
handle.classList.remove('is-dragging')
|
|
@@ -936,27 +991,32 @@ function startColumnResize(event, column, minimum, maximum) {
|
|
|
936
991
|
handle.addEventListener('pointercancel', finish)
|
|
937
992
|
}
|
|
938
993
|
|
|
939
|
-
function resizeColumnWithKeyboard(event, column
|
|
994
|
+
function resizeColumnWithKeyboard(event, column) {
|
|
940
995
|
if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return
|
|
941
996
|
event.preventDefault()
|
|
942
|
-
const
|
|
943
|
-
const
|
|
944
|
-
: event.key === 'End' ? maximum
|
|
945
|
-
: clampNumber(current + (event.key === 'ArrowRight' ? 16 : -16), minimum, maximum, current)
|
|
997
|
+
const layout = DOCUMENT_COLUMN_LAYOUTS[column]
|
|
998
|
+
const current = state.documentView[layout.widthKey]
|
|
946
999
|
const browser = event.currentTarget.closest('.document-browser')
|
|
947
|
-
|
|
1000
|
+
const width = event.key === 'Home' ? layout.minimum
|
|
1001
|
+
: event.key === 'End' ? layout.maximum
|
|
1002
|
+
: clampNumber(current + (event.key === 'ArrowRight' ? 16 : -16), layout.minimum, layout.maximum, current)
|
|
1003
|
+
setDocumentColumnWidth(column, width, browser, event.currentTarget)
|
|
948
1004
|
saveDocumentLayout()
|
|
949
1005
|
}
|
|
950
1006
|
|
|
951
|
-
function
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
} else {
|
|
956
|
-
state.documentView.documentListWidth = width
|
|
957
|
-
browser?.style.setProperty('--document-list-width', `${width}px`)
|
|
958
|
-
}
|
|
1007
|
+
function setDocumentColumnWidth(column, width, browser, handle) {
|
|
1008
|
+
const layout = DOCUMENT_COLUMN_LAYOUTS[column]
|
|
1009
|
+
state.documentView[layout.widthKey] = width
|
|
1010
|
+
browser?.style.setProperty(layout.cssVariable, `${width}px`)
|
|
959
1011
|
handle?.setAttribute('aria-valuenow', String(width))
|
|
1012
|
+
handle?.setAttribute('aria-valuetext', `${width} 像素`)
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
function renderDocumentModeTabs() {
|
|
1016
|
+
return element('div', { class: 'tabs', role: 'tablist', 'aria-label': '知识视图' },
|
|
1017
|
+
documentViewTab('文档', 'documents'),
|
|
1018
|
+
documentViewTab('条目管理', 'entries'),
|
|
1019
|
+
)
|
|
960
1020
|
}
|
|
961
1021
|
|
|
962
1022
|
function documentViewTab(label, mode) {
|
|
@@ -1002,7 +1062,7 @@ function renderDocumentReader(document, base) {
|
|
|
1002
1062
|
.map(item => element('option', { value: item.id, selected: item.id === document.id }, item.relPath))),
|
|
1003
1063
|
element('div', { class: 'reader-actions' }, badge(`${document.entryCount} 条知识`), actionButton('添加内容', () => openEntryEditor(), 'small')),
|
|
1004
1064
|
),
|
|
1005
|
-
element('article', { class: 'markdown-document' }, renderMarkdown(document.content)),
|
|
1065
|
+
element('article', { class: 'markdown-document', 'data-scroll-key': 'document-reader' }, renderMarkdown(document.content)),
|
|
1006
1066
|
element('footer', { class: 'reader-footer' },
|
|
1007
1067
|
element('span', {}, `自动整理 · ${formatDate(document.updatedAt)}`),
|
|
1008
1068
|
element('span', {}, `SHA-256 · ${document.contentHash.slice(0, 8)}`),
|
|
@@ -1077,7 +1137,11 @@ function renderLegacyEntries() {
|
|
|
1077
1137
|
projectInput,
|
|
1078
1138
|
actionButton('搜索', () => {}, 'primary', { type: 'submit' }),
|
|
1079
1139
|
)
|
|
1080
|
-
return element('section', { 'aria-labelledby': 'entries-heading' },
|
|
1140
|
+
return element('section', { class: 'document-page legacy-entries-page', 'aria-labelledby': 'entries-heading' },
|
|
1141
|
+
element('div', { class: 'document-page-toolbar legacy-entries-toolbar' },
|
|
1142
|
+
renderDocumentModeTabs(),
|
|
1143
|
+
element('div', { class: 'document-toolbar-actions' }, actionButton('+ 新建知识', () => openEntryEditor(), 'primary')),
|
|
1144
|
+
),
|
|
1081
1145
|
element('div', { class: 'section-heading' }, element('div', {}, element('h2', { id: 'entries-heading' }, `${STATUS_LABELS[state.entryFilters.status]} · ${state.entries.length}`), element('p', {}, '搜索范围会包含全局知识和指定项目知识'))),
|
|
1082
1146
|
toolbar,
|
|
1083
1147
|
state.entries.length
|