@lemoncat7/dsh-knowledge 2.8.0 → 2.9.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.
@@ -0,0 +1,99 @@
1
+ /** Display-only grouping. Knowledge contents, mounts and recall are untouched. */
2
+ const nameCollator = new Intl.Collator('zh-CN', { numeric: true, sensitivity: 'base' })
3
+ export const knowledgeBasePathLabel = base => `${base.group || '未分组'} / ${base.name}`
4
+ export function sortKnowledgeBasesByGroup(bases) {
5
+ return [...bases].sort((a, b) => {
6
+ const left = a.group || '', right = b.group || ''
7
+ if (Boolean(left) !== Boolean(right)) return left ? -1 : 1
8
+ return nameCollator.compare(left, right) || nameCollator.compare(a.name, b.name) || String(a.id).localeCompare(String(b.id))
9
+ })
10
+ }
11
+
12
+ export function groupKnowledgeBases(bases) {
13
+ const groups = new Map()
14
+ for (const base of bases) {
15
+ const name = base.group || ''
16
+ if (!groups.has(name)) groups.set(name, [])
17
+ groups.get(name).push(base)
18
+ }
19
+ return [...groups].sort(([a], [b]) => !a ? 1 : !b ? -1 : a.localeCompare(b, 'zh-CN'))
20
+ }
21
+
22
+ export function createBaseGroups({ element, actionButton, interfaceIcon, openSheet, formField, api, getBases, refresh, showToast, storageKey }) {
23
+ let collapsed = new Set()
24
+ try {
25
+ const saved = JSON.parse(localStorage.getItem(storageKey) || '[]')
26
+ if (Array.isArray(saved)) collapsed = new Set(saved.filter(value => typeof value === 'string'))
27
+ } catch {}
28
+ const remember = () => { try { localStorage.setItem(storageKey, JSON.stringify([...collapsed])) } catch {} }
29
+
30
+ function render(bases, renderCard, searching = false, scope = 'active') {
31
+ return element('div', { class: 'base-groups' }, groupKnowledgeBases(bases).map(([name, members]) => {
32
+ const key = JSON.stringify([scope, name])
33
+ let expanded = searching || !collapsed.has(key)
34
+ const body = element('div', { class: 'base-grid base-group-body', id: `base-group-${scope}-${encodeURIComponent(name)}` })
35
+ const toggle = element('button', {
36
+ type: 'button', class: 'base-group-toggle', 'aria-expanded': String(expanded), 'aria-controls': body.id,
37
+ onClick: () => {
38
+ expanded = !expanded
39
+ if (expanded) collapsed.delete(key); else collapsed.add(key)
40
+ remember(); update()
41
+ },
42
+ }, interfaceIcon('chevron-right', 'base-group-chevron'),
43
+ element('span', { class: 'base-group-name', title: name || '未分组' }, name || '未分组'),
44
+ element('span', { class: 'summary-count' }, members.length))
45
+ const update = () => {
46
+ toggle.setAttribute('aria-expanded', String(expanded))
47
+ body.hidden = !expanded
48
+ // Closed groups do not mount card DOM or animation observers.
49
+ body.replaceChildren(...(expanded ? members.map(renderCard) : []))
50
+ }
51
+ update()
52
+ return element('section', { class: 'base-group', 'aria-label': name || '未分组' },
53
+ element('div', { class: 'base-group-heading' }, toggle,
54
+ name ? actionButton('重命名', () => edit(name), 'ghost small', { 'aria-label': `重命名分组 ${name}` }) : null), body)
55
+ }))
56
+ }
57
+
58
+ function edit(current) {
59
+ const renaming = typeof current === 'string'
60
+ const bases = getBases()
61
+ const name = formField('分组名称', 'input', current || '', { id: 'knowledge-group-name', required: true, maxlength: 64, placeholder: '例如:家里助手、工作、个人' })
62
+ name.wrapper.querySelector('label').htmlFor = name.input.id
63
+ const selection = new Map()
64
+ const error = element('p', { role: 'alert', class: 'base-group-error' })
65
+ const form = element('form', { class: 'base-group-form', onSubmit: event => event.preventDefault() }, name.wrapper)
66
+ if (!renaming) {
67
+ form.append(element('p', {}, '选择要归入这个分组的知识库;原有内容和挂载保持不变。'))
68
+ form.append(element('div', { class: 'base-group-picker' }, bases.map(base => {
69
+ const checkbox = element('input', { type: 'checkbox', value: base.id })
70
+ selection.set(base.id, checkbox)
71
+ return element('label', { class: 'check-option' }, checkbox,
72
+ element('span', {}, element('strong', {}, base.name), element('small', {}, `${base.group || '未分组'}${base.status === 'archived' ? ' · 已归档' : ''}`)))
73
+ })))
74
+ }
75
+ form.append(error)
76
+ openSheet({
77
+ title: renaming ? '重命名分组' : '新建分组',
78
+ description: '每个知识库属于一个分组;没有知识库的分组自动消失。',
79
+ body: form, primaryLabel: renaming ? '保存分组' : '创建分组',
80
+ onPrimary: async () => {
81
+ name.input.setCustomValidity(name.input.value.trim() ? '' : '请输入分组名称')
82
+ if (!form.reportValidity()) return false
83
+ const ids = renaming ? bases.filter(base => base.group === current).map(base => base.id)
84
+ : [...selection].filter(([, input]) => input.checked).map(([id]) => id)
85
+ error.textContent = ''
86
+ if (!ids.length) { error.textContent = '请至少选择一个知识库'; return false }
87
+ await api('knowledge-bases/group', { method: 'POST', body: { ids, group: name.input.value.trim() } })
88
+ collapsed.delete(JSON.stringify(['active', name.input.value.trim()]))
89
+ remember()
90
+ await refresh()
91
+ showToast(renaming ? '分组已更新;同名分组会合并显示。' : '知识库已分组。')
92
+ return true
93
+ },
94
+ })
95
+ name.input.addEventListener('input', () => name.input.setCustomValidity(''))
96
+ name.input.focus()
97
+ }
98
+ return { render, edit }
99
+ }
package/web/styles.css CHANGED
@@ -722,6 +722,32 @@ html,
722
722
  .knowledge-base-toolbar { display: flex; align-items: center; gap: 16px; margin-bottom: 14px; }
723
723
  .base-search { max-width: 560px; }
724
724
  .base-result-summary { margin-left: auto; color: var(--text-tertiary); font-size: 12px; white-space: nowrap; }
725
+ .base-heading-actions { display: flex; flex-wrap: wrap; gap: 8px; }
726
+ .base-groups { display: grid; gap: 18px; min-width: 0; }
727
+ .base-group { min-width: 0; }
728
+ .base-group-heading { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; border-bottom: 1px solid var(--separator); }
729
+ .base-group-toggle { display: flex; flex: 1; align-items: center; gap: 10px; min-width: 0; min-height: 44px; padding: 8px 4px; border: 0; border-radius: var(--radius-small); background: transparent; color: var(--text); cursor: pointer; text-align: left; font: inherit; font-weight: 600; }
730
+ .base-group-toggle:hover { background: var(--material-control-hover); }
731
+ .base-group-toggle:focus-visible { outline: 2px solid var(--text-secondary); outline-offset: 2px; }
732
+ .base-group-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
733
+ .base-group-chevron { flex: none; width: 16px; height: 16px; transition: transform 180ms ease; }
734
+ .base-group-toggle[aria-expanded="true"] .base-group-chevron { transform: rotate(90deg); }
735
+ .base-group-body[hidden] { display: none; }
736
+ .base-group-form { display: grid; gap: 16px; min-width: 0; }
737
+ .base-group-form p { margin: 0; color: var(--text-secondary); }
738
+ .base-group-form .base-group-error { color: var(--danger); }
739
+ .base-group-error:empty { display: none; }
740
+ .mount-list-title .mount-base-path { min-width: 0; white-space: normal; overflow-wrap: anywhere; }
741
+ .base-group-picker { display: grid; gap: 8px; max-height: min(45dvh, 380px); overflow-y: auto; }
742
+ .base-group-picker .check-option { min-width: 0; min-height: 44px; }
743
+ .base-group-picker .check-option span { min-width: 0; overflow-wrap: anywhere; }
744
+ @media (prefers-reduced-motion: reduce) { .base-group-chevron { transition: none; } }
745
+ @media (max-width: 640px) {
746
+ .library-management > .section-heading { align-items: stretch; flex-direction: column; }
747
+ .base-heading-actions { width: 100%; }
748
+ .base-group-heading > .button { min-height: 44px; }
749
+ .knowledge-base-toolbar .base-search { flex: 0 0 auto; }
750
+ }
725
751
  .base-grid {
726
752
  display: grid;
727
753
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 390px), 1fr));
@@ -1785,7 +1811,7 @@ html,
1785
1811
  .library-detail-actions { align-self: center; }
1786
1812
  .library-detail .note-workspace { height: calc(100dvh - 260px); }
1787
1813
  .knowledge-base-toolbar { align-items: stretch; flex-direction: column; gap: 8px; }
1788
- .base-search { max-width: none; }
1814
+ .knowledge-base-toolbar .base-search { max-width: none; flex: 0 0 auto; }
1789
1815
  .base-result-summary { margin-left: 0; }
1790
1816
  .skeleton-grid { grid-template-columns: 1fr; }
1791
1817
  .skeleton-block { height: 96px; }
@@ -47,6 +47,7 @@ export function paneToggleButton(pane, visible, onClick, label) {
47
47
  export function interfaceIcon(name, className = 'interface-icon') {
48
48
  const paths = {
49
49
  close: 'M6 6l12 12M18 6L6 18',
50
+ 'chevron-right': 'M9 5l7 7-7 7',
50
51
  menu: 'M4 6h16M4 12h16M4 18h16',
51
52
  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',
52
53
  more: 'M5 12h.01M12 12h.01M19 12h.01',