@lemoncat7/dsh-knowledge 1.0.10 → 1.0.13
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/lib/api.d.ts +1 -0
- package/lib/api.d.ts.map +1 -1
- package/lib/api.js +49 -7
- package/lib/api.js.map +1 -1
- package/lib/client.js +28 -5
- package/lib/client.js.map +2 -2
- package/lib/domain.d.ts +8 -1
- package/lib/domain.d.ts.map +1 -1
- package/lib/domain.js +15 -2
- package/lib/domain.js.map +1 -1
- package/lib/extraction.d.ts.map +1 -1
- package/lib/extraction.js +11 -9
- package/lib/extraction.js.map +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +85 -18
- package/lib/index.js.map +1 -1
- package/lib/local-provider.d.ts.map +1 -1
- package/lib/local-provider.js +75 -11
- package/lib/local-provider.js.map +1 -1
- package/lib/runtime.d.ts +15 -0
- package/lib/runtime.d.ts.map +1 -1
- package/lib/runtime.js.map +1 -1
- package/lib/tools.js +1 -0
- package/lib/tools.js.map +1 -1
- package/package.json +1 -1
- package/web/app.js +111 -59
- package/web/styles.css +14 -41
package/web/app.js
CHANGED
|
@@ -61,8 +61,10 @@ const state = {
|
|
|
61
61
|
editor: null,
|
|
62
62
|
},
|
|
63
63
|
candidates: [],
|
|
64
|
+
candidateTargets: new Map(),
|
|
64
65
|
candidateStatus: 'pending',
|
|
65
66
|
settings: { writebackPolicy: 'conservative', updatedAt: '' },
|
|
67
|
+
modelCatalog: null,
|
|
66
68
|
settingsSaving: false,
|
|
67
69
|
tokens: [],
|
|
68
70
|
service: { publicApiEnabled: false, publicApiPrefix: '/knowledge-api/v1', remote: false },
|
|
@@ -93,6 +95,7 @@ function installHostThemeBridge() {
|
|
|
93
95
|
}
|
|
94
96
|
root.dataset.dshHostTheme = 'true'
|
|
95
97
|
root.style.colorScheme = message.colorScheme
|
|
98
|
+
root.style.setProperty('--dialog-surface', message.colorScheme === 'dark' ? '#1c1c1e' : '#ffffff')
|
|
96
99
|
document.querySelector('meta[name="color-scheme"]')?.setAttribute('content', message.colorScheme)
|
|
97
100
|
const background = root.style.getPropertyValue('--bg')
|
|
98
101
|
if (background) document.querySelector('meta[name="theme-color"]')?.setAttribute('content', background)
|
|
@@ -310,7 +313,7 @@ function renderLogin(message = '') {
|
|
|
310
313
|
|
|
311
314
|
function signOut() {
|
|
312
315
|
sessionStorage.removeItem(TOKEN_KEY)
|
|
313
|
-
Object.assign(state, { token: '', stats: null, overview: null, knowledgeBases: [], mounts: [], resolvedMounts: [], entries: [], documents: [], candidates: [], settings: { writebackPolicy: 'conservative', updatedAt: '' }, tokens: [] })
|
|
316
|
+
Object.assign(state, { token: '', stats: null, overview: null, knowledgeBases: [], mounts: [], resolvedMounts: [], entries: [], documents: [], candidates: [], candidateTargets: new Map(), settings: { writebackPolicy: 'conservative', updatedAt: '' }, tokens: [] })
|
|
314
317
|
if (AUTH_MODE === 'same-origin') void boot()
|
|
315
318
|
else renderLogin()
|
|
316
319
|
}
|
|
@@ -543,6 +546,12 @@ async function loadCandidates() {
|
|
|
543
546
|
ensureKnowledgeBases(),
|
|
544
547
|
])
|
|
545
548
|
state.candidates = candidates
|
|
549
|
+
const targetIds = [...new Set(candidates.map(candidate => candidate.targetId).filter(Boolean))]
|
|
550
|
+
const targets = await Promise.all(targetIds.map(async id => {
|
|
551
|
+
try { return [id, await api(`entries/${encodeURIComponent(id)}`)] }
|
|
552
|
+
catch { return [id, null] }
|
|
553
|
+
}))
|
|
554
|
+
state.candidateTargets = new Map(targets)
|
|
546
555
|
if (!state.stats) await refreshStats()
|
|
547
556
|
}
|
|
548
557
|
|
|
@@ -756,11 +765,11 @@ function renderKnowledgeBases() {
|
|
|
756
765
|
type: 'button', role: 'tab', class: 'tab', 'aria-selected': String(state.knowledgeBaseView === id),
|
|
757
766
|
onClick: () => { state.knowledgeBaseView = id; renderShell() },
|
|
758
767
|
}, element('span', {}, label), element('span', { class: 'tab-count' }, count)))),
|
|
759
|
-
|
|
768
|
+
actionButton('默认回写模型', () => openGlobalWritebackModelEditor(), 'ghost small'),
|
|
760
769
|
),
|
|
761
|
-
element('p', {}, state.settings.
|
|
762
|
-
?
|
|
763
|
-
: '
|
|
770
|
+
element('p', {}, state.settings.writebackProvider && state.settings.writebackModel
|
|
771
|
+
? `未单独指定模型的知识库使用 ${state.settings.writebackProvider} / ${state.settings.writebackModel}。`
|
|
772
|
+
: '未单独指定模型的知识库跟随当前会话模型。'),
|
|
764
773
|
)
|
|
765
774
|
return element('div', { class: 'bases-page' },
|
|
766
775
|
switcher,
|
|
@@ -810,41 +819,23 @@ function renderKnowledgeBases() {
|
|
|
810
819
|
)
|
|
811
820
|
}
|
|
812
821
|
|
|
813
|
-
function
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
async function updateWritebackPolicy(writebackPolicy) {
|
|
833
|
-
if (state.settingsSaving || state.settings.writebackPolicy === writebackPolicy) return
|
|
834
|
-
const previous = state.settings
|
|
835
|
-
state.settings = { ...state.settings, writebackPolicy }
|
|
836
|
-
state.settingsSaving = true
|
|
837
|
-
renderShell()
|
|
838
|
-
try {
|
|
839
|
-
state.settings = await api('settings', { method: 'PUT', body: { patch: { writebackPolicy } } })
|
|
840
|
-
showToast(`全局回写策略已切换为${writebackPolicy === 'conservative' ? '严谨' : '主动'}模式`)
|
|
841
|
-
} catch (error) {
|
|
842
|
-
state.settings = previous
|
|
843
|
-
showToast(friendlyError(error), 'error')
|
|
844
|
-
} finally {
|
|
845
|
-
state.settingsSaving = false
|
|
846
|
-
renderShell()
|
|
847
|
-
}
|
|
822
|
+
async function openGlobalWritebackModelEditor() {
|
|
823
|
+
await loadModelCatalog()
|
|
824
|
+
const custom = element('input', { type: 'checkbox', checked: Boolean(state.settings.writebackProvider && state.settings.writebackModel) })
|
|
825
|
+
const route = modelRouteFields(state.settings.writebackProvider || '', state.settings.writebackModel || '')
|
|
826
|
+
const form = element('form', { class: 'form-grid' },
|
|
827
|
+
element('label', { class: 'check-option span-2' }, custom, element('span', {}, element('strong', {}, '使用全局默认模型'), element('small', {}, '关闭后,未单独配置的知识库跟随当前会话模型。'))),
|
|
828
|
+
route.provider.wrapper, route.model.wrapper,
|
|
829
|
+
)
|
|
830
|
+
const sync = () => { route.provider.input.disabled = route.model.input.disabled = !custom.checked; route.provider.input.required = route.model.input.required = custom.checked }
|
|
831
|
+
custom.addEventListener('change', sync); sync()
|
|
832
|
+
openSheet({ title: '默认回写模型', description: '知识库自己的模型优先于这里;这里留空则跟随当前会话。', body: form, primaryLabel: '保存', onPrimary: async () => {
|
|
833
|
+
if (!form.reportValidity()) return false
|
|
834
|
+
state.settings = await api('settings', { method: 'PUT', body: { patch: custom.checked
|
|
835
|
+
? { writebackProvider: route.provider.input.value, writebackModel: route.model.input.value }
|
|
836
|
+
: { writebackProvider: null, writebackModel: null } } })
|
|
837
|
+
showToast('默认回写模型已更新。'); renderShell(); return true
|
|
838
|
+
} })
|
|
848
839
|
}
|
|
849
840
|
|
|
850
841
|
function contextPill(label, value) {
|
|
@@ -869,7 +860,8 @@ function renderKnowledgeBaseCard(base) {
|
|
|
869
860
|
: element('span', { class: 'tag is-empty' }, '无默认标签'),
|
|
870
861
|
hiddenTagCount ? element('span', { class: 'tag' }, `+${hiddenTagCount}`) : null),
|
|
871
862
|
element('div', { class: 'base-card-meta' },
|
|
872
|
-
element('span', {}, element('strong', {}, '
|
|
863
|
+
element('span', {}, element('strong', {}, '回写策略'), base.writebackPolicy === 'proactive' ? '主动' : '严谨'),
|
|
864
|
+
element('span', {}, element('strong', {}, '回写模型'), base.writebackProvider && base.writebackModel ? `${base.writebackProvider} / ${base.writebackModel}` : state.settings.writebackProvider && state.settings.writebackModel ? `默认 · ${state.settings.writebackProvider} / ${state.settings.writebackModel}` : '跟随当前会话'),
|
|
873
865
|
base.extractionInstructions ? element('span', {}, element('strong', {}, '提取规则'), '已设置') : null,
|
|
874
866
|
),
|
|
875
867
|
element('div', { class: 'base-card-actions' },
|
|
@@ -1283,6 +1275,9 @@ function renderCandidates() {
|
|
|
1283
1275
|
|
|
1284
1276
|
function renderCandidateCard(candidate) {
|
|
1285
1277
|
const pending = candidate.status === 'pending'
|
|
1278
|
+
const conflictTarget = candidate.action === 'conflict' && candidate.targetId
|
|
1279
|
+
? state.candidateTargets.get(candidate.targetId)
|
|
1280
|
+
: null
|
|
1286
1281
|
const targetDocument = candidate.action === 'create'
|
|
1287
1282
|
? `新文档“${candidate.draft.title}”`
|
|
1288
1283
|
: `文档“${candidate.draft.title}”`
|
|
@@ -1298,8 +1293,13 @@ function renderCandidateCard(candidate) {
|
|
|
1298
1293
|
),
|
|
1299
1294
|
badge(STATUS_LABELS[candidate.status], candidate.status === 'approved' ? 'success' : candidate.status === 'rejected' ? 'danger' : 'warning'),
|
|
1300
1295
|
),
|
|
1301
|
-
element('div', { class:
|
|
1302
|
-
|
|
1296
|
+
element('div', { class: `candidate-body${candidate.action === 'conflict' ? ' is-conflict' : ''}` },
|
|
1297
|
+
candidate.action === 'conflict'
|
|
1298
|
+
? element('div', { class: 'candidate-comparison' },
|
|
1299
|
+
element('section', {}, element('strong', {}, '当前知识'), element('pre', {}, conflictTarget?.body || '目标文档不可用,请刷新后重试。')),
|
|
1300
|
+
element('section', {}, element('strong', {}, '候选内容'), element('pre', {}, candidate.draft.body)),
|
|
1301
|
+
)
|
|
1302
|
+
: element('p', { class: 'candidate-content' }, candidate.draft.body),
|
|
1303
1303
|
element('div', { class: 'candidate-reason' },
|
|
1304
1304
|
element('strong', {}, '文档变更'),
|
|
1305
1305
|
element('span', { class: 'candidate-target' }, `${candidate.action === 'create' ? '创建' : '更新'}${targetDocument}`),
|
|
@@ -1310,7 +1310,9 @@ function renderCandidateCard(candidate) {
|
|
|
1310
1310
|
pending ? element('div', {},
|
|
1311
1311
|
actionButton('拒绝', () => reviewCandidate(candidate, 'reject'), 'danger small'),
|
|
1312
1312
|
actionButton('编辑并通过', () => openEntryEditor(undefined, candidate), 'small'),
|
|
1313
|
-
|
|
1313
|
+
candidate.action === 'conflict'
|
|
1314
|
+
? actionButton('合并追加', () => reviewCandidate(candidate, 'approve', 'merge'), 'primary small')
|
|
1315
|
+
: actionButton('通过', () => reviewCandidate(candidate, 'approve'), 'primary small'),
|
|
1314
1316
|
) : null,
|
|
1315
1317
|
),
|
|
1316
1318
|
)
|
|
@@ -1398,33 +1400,37 @@ function parseTags(value) {
|
|
|
1398
1400
|
return [...new Set(value.split(/[,,]/).map(tag => tag.trim().toLowerCase()).filter(Boolean))]
|
|
1399
1401
|
}
|
|
1400
1402
|
|
|
1401
|
-
function openKnowledgeBaseEditor(base) {
|
|
1402
|
-
|
|
1403
|
+
async function openKnowledgeBaseEditor(base) {
|
|
1404
|
+
await loadModelCatalog()
|
|
1405
|
+
const source = base || { name: '', description: '', defaultTags: [], extractionInstructions: '', writebackPolicy: 'conservative' }
|
|
1403
1406
|
const form = element('form', { class: 'form-grid' })
|
|
1404
1407
|
const name = formField('名称', 'input', source.name, { required: true, maxlength: 100, placeholder: '例如:项目规范' })
|
|
1405
1408
|
const description = formField('回写匹配描述', 'textarea', source.description, { maxlength: 2000, placeholder: '描述什么样的对话才属于这个库。例如:只记录 dsh-knowledge 项目的架构决策和部署规范' })
|
|
1406
1409
|
const tags = formField('默认标签', 'input', source.defaultTags.join(', '), { placeholder: 'project-rule, backend' })
|
|
1407
1410
|
const instructions = formField('提取要求', 'textarea', source.extractionInstructions, { maxlength: 4000, placeholder: '例如:只收录已确认、可跨会话复用的项目约定' })
|
|
1411
|
+
const policy = selectField('回写策略', [
|
|
1412
|
+
{ value: 'conservative', label: '严谨(高置信度直写)' },
|
|
1413
|
+
{ value: 'proactive', label: '主动(更积极沉淀)' },
|
|
1414
|
+
], source.writebackPolicy || 'conservative')
|
|
1408
1415
|
const customModel = element('input', { type: 'checkbox', checked: Boolean(source.writebackProvider && source.writebackModel) })
|
|
1409
|
-
const
|
|
1410
|
-
const model = formField('模型名称(Model)', 'input', source.writebackModel || '', { maxlength: 200, placeholder: '例如:kimi-k2.7-code' })
|
|
1416
|
+
const route = modelRouteFields(source.writebackProvider || '', source.writebackModel || '')
|
|
1411
1417
|
const modelHint = element('div', { class: 'field-hint span-2' }, '默认跟随当前会话模型。指定后,仅这个知识库使用专用模型回写。')
|
|
1412
1418
|
const updateModelFields = () => {
|
|
1413
|
-
provider.input.disabled = !customModel.checked
|
|
1414
|
-
model.input.disabled = !customModel.checked
|
|
1415
|
-
provider.input.required = customModel.checked
|
|
1416
|
-
model.input.required = customModel.checked
|
|
1419
|
+
route.provider.input.disabled = !customModel.checked
|
|
1420
|
+
route.model.input.disabled = !customModel.checked
|
|
1421
|
+
route.provider.input.required = customModel.checked
|
|
1422
|
+
route.model.input.required = customModel.checked
|
|
1417
1423
|
}
|
|
1418
1424
|
customModel.addEventListener('change', updateModelFields)
|
|
1419
1425
|
updateModelFields()
|
|
1420
1426
|
for (const field of [name, description, tags, instructions]) field.wrapper.classList.add('span-2')
|
|
1421
1427
|
form.append(
|
|
1422
|
-
name.wrapper, description.wrapper, tags.wrapper, instructions.wrapper,
|
|
1428
|
+
name.wrapper, description.wrapper, tags.wrapper, instructions.wrapper, policy.wrapper,
|
|
1423
1429
|
element('label', { class: 'check-option span-2' }, customModel, element('span', {},
|
|
1424
1430
|
element('strong', {}, '为这个知识库指定回写模型'),
|
|
1425
1431
|
element('small', {}, '关闭时自动使用当前会话正在使用的模型。'),
|
|
1426
1432
|
)),
|
|
1427
|
-
provider.wrapper, model.wrapper, modelHint,
|
|
1433
|
+
route.provider.wrapper, route.model.wrapper, modelHint,
|
|
1428
1434
|
)
|
|
1429
1435
|
openSheet({
|
|
1430
1436
|
title: base ? '编辑知识库' : '创建知识库',
|
|
@@ -1438,8 +1444,9 @@ function openKnowledgeBaseEditor(base) {
|
|
|
1438
1444
|
description: description.input.value.trim(),
|
|
1439
1445
|
defaultTags: parseTags(tags.input.value),
|
|
1440
1446
|
extractionInstructions: instructions.input.value.trim(),
|
|
1441
|
-
|
|
1442
|
-
|
|
1447
|
+
writebackPolicy: policy.input.value,
|
|
1448
|
+
writebackProvider: customModel.checked ? route.provider.input.value : '',
|
|
1449
|
+
writebackModel: customModel.checked ? route.model.input.value : '',
|
|
1443
1450
|
}
|
|
1444
1451
|
if (base) await api(`knowledge-bases/${encodeURIComponent(base.id)}`, { method: 'PUT', body: { draft } })
|
|
1445
1452
|
else await api('knowledge-bases', { method: 'POST', body: { draft } })
|
|
@@ -1710,7 +1717,9 @@ function openEntryEditor(entry, candidate) {
|
|
|
1710
1717
|
confidence: Number(confidenceInput.value),
|
|
1711
1718
|
...(source.source ? { source: source.source } : {}),
|
|
1712
1719
|
}
|
|
1713
|
-
if (candidate) await api(`candidates/${encodeURIComponent(candidate.id)}/review`, {
|
|
1720
|
+
if (candidate) await api(`candidates/${encodeURIComponent(candidate.id)}/review`, {
|
|
1721
|
+
method: 'POST', body: { decision: 'approve', draft, ...(candidate.action === 'conflict' ? { resolution: 'merge' } : {}) },
|
|
1722
|
+
})
|
|
1714
1723
|
else if (entry) await api(`entries/${encodeURIComponent(entry.id)}`, { method: 'PUT', body: { draft } })
|
|
1715
1724
|
else await api('entries', { method: 'POST', body: { draft } })
|
|
1716
1725
|
showToast(candidate ? '候选已通过并写入知识库。' : '知识已保存。')
|
|
@@ -1733,14 +1742,57 @@ function selectField(label, options, value) {
|
|
|
1733
1742
|
return { input, wrapper: element('div', { class: 'field' }, element('label', {}, label), input) }
|
|
1734
1743
|
}
|
|
1735
1744
|
|
|
1736
|
-
async function
|
|
1745
|
+
async function loadModelCatalog() {
|
|
1746
|
+
if (state.modelCatalog !== null) return state.modelCatalog
|
|
1747
|
+
try {
|
|
1748
|
+
const response = await fetch('/knowledge-control/v1/models', { headers: { accept: 'application/json' } })
|
|
1749
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
|
1750
|
+
const payload = await response.json()
|
|
1751
|
+
state.modelCatalog = Array.isArray(payload.providers) ? payload.providers : []
|
|
1752
|
+
} catch (error) {
|
|
1753
|
+
state.modelCatalog = []
|
|
1754
|
+
showToast(`无法读取当前 DSH 的模型目录:${error instanceof Error ? error.message : String(error)}`, 'error')
|
|
1755
|
+
}
|
|
1756
|
+
return state.modelCatalog
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
function modelRouteFields(currentProvider, currentModel) {
|
|
1760
|
+
const providers = [...(state.modelCatalog || [])]
|
|
1761
|
+
if (currentProvider && !providers.some(item => item.id === currentProvider)) {
|
|
1762
|
+
providers.push({ id: currentProvider, name: `${currentProvider}(当前配置)`, models: [] })
|
|
1763
|
+
}
|
|
1764
|
+
const provider = selectField('模型提供方', providers.map(item => ({ value: item.id, label: item.name || item.id })), currentProvider || providers[0]?.id || '')
|
|
1765
|
+
const model = selectField('回写模型', [], '')
|
|
1766
|
+
const refreshModels = preferred => {
|
|
1767
|
+
const selected = providers.find(item => item.id === provider.input.value)
|
|
1768
|
+
const models = [...(selected?.models || [])]
|
|
1769
|
+
if (preferred && !models.some(item => item.id === preferred)) models.push({ id: preferred, name: `${preferred}(当前配置)` })
|
|
1770
|
+
model.input.replaceChildren(...models.map(item => element('option', {
|
|
1771
|
+
value: item.id, selected: item.id === preferred,
|
|
1772
|
+
}, item.name && item.name !== item.id ? `${item.name} · ${item.id}` : item.id)))
|
|
1773
|
+
if (preferred && models.some(item => item.id === preferred)) model.input.value = preferred
|
|
1774
|
+
}
|
|
1775
|
+
provider.input.addEventListener('change', () => refreshModels(''))
|
|
1776
|
+
refreshModels(currentModel)
|
|
1777
|
+
return { provider, model }
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
async function reviewCandidate(candidate, decision, resolution) {
|
|
1737
1781
|
const approve = decision === 'approve'
|
|
1738
1782
|
openConfirm({
|
|
1739
1783
|
title: approve ? '通过这条候选?' : '拒绝这条候选?',
|
|
1740
1784
|
message: approve ? '通过后内容会立即写入知识库,并参与后续对话召回。' : '拒绝后会保留审核记录,但不会进入知识库。',
|
|
1741
1785
|
confirmLabel: approve ? '确认通过' : '确认拒绝', danger: !approve,
|
|
1742
1786
|
onConfirm: async () => {
|
|
1743
|
-
await api(`candidates/${encodeURIComponent(candidate.id)}/review`, {
|
|
1787
|
+
const reviewed = await api(`candidates/${encodeURIComponent(candidate.id)}/review`, {
|
|
1788
|
+
method: 'POST', body: { decision, ...(resolution ? { resolution } : {}) },
|
|
1789
|
+
})
|
|
1790
|
+
if (approve && reviewed.status === 'pending' && reviewed.action === 'conflict') {
|
|
1791
|
+
showToast('审核期间知识已变化,候选已转为冲突项,请比较后重新确认。')
|
|
1792
|
+
state.stats = null
|
|
1793
|
+
await navigate('candidates')
|
|
1794
|
+
return
|
|
1795
|
+
}
|
|
1744
1796
|
showToast(approve ? '候选已通过。' : '候选已拒绝。')
|
|
1745
1797
|
state.stats = null
|
|
1746
1798
|
await navigate('candidates')
|
package/web/styles.css
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
--surface-raised: #ffffff;
|
|
8
8
|
--surface-soft: #f0f0f3;
|
|
9
9
|
--surface-hover: #f5f5f8;
|
|
10
|
+
--dialog-surface: #ffffff;
|
|
10
11
|
--text: #1d1d1f;
|
|
11
12
|
--text-secondary: #4c4c50;
|
|
12
13
|
--text-tertiary: #6e6e73;
|
|
@@ -34,6 +35,7 @@
|
|
|
34
35
|
--surface-raised: #242426;
|
|
35
36
|
--surface-soft: #2c2c2e;
|
|
36
37
|
--surface-hover: #303033;
|
|
38
|
+
--dialog-surface: #1c1c1e;
|
|
37
39
|
--text: #f5f5f7;
|
|
38
40
|
--text-secondary: #c7c7cc;
|
|
39
41
|
--text-tertiary: #98989d;
|
|
@@ -106,7 +108,7 @@ button:disabled { cursor: not-allowed; opacity: .56; }
|
|
|
106
108
|
border: 1px solid var(--border);
|
|
107
109
|
border-radius: 20px;
|
|
108
110
|
padding: 32px;
|
|
109
|
-
background: var(--surface
|
|
111
|
+
background: var(--dialog-surface);
|
|
110
112
|
box-shadow: var(--shadow);
|
|
111
113
|
}
|
|
112
114
|
|
|
@@ -127,11 +129,11 @@ button:disabled { cursor: not-allowed; opacity: .56; }
|
|
|
127
129
|
.login-card > p { margin: 0 0 24px; color: var(--text-secondary); }
|
|
128
130
|
|
|
129
131
|
.field { display: grid; gap: 7px; }
|
|
130
|
-
.field + .field { margin-top: 16px; }
|
|
131
132
|
.field label { color: var(--text-secondary); font-size: 13px; font-weight: 600; }
|
|
132
133
|
.field-hint { color: var(--text-tertiary); font-size: 12px; }
|
|
133
134
|
|
|
134
135
|
.input, .select, .textarea {
|
|
136
|
+
box-sizing: border-box;
|
|
135
137
|
width: 100%;
|
|
136
138
|
border: 1px solid var(--border-strong);
|
|
137
139
|
border-radius: 9px;
|
|
@@ -141,7 +143,7 @@ button:disabled { cursor: not-allowed; opacity: .56; }
|
|
|
141
143
|
transition: border-color .15s ease, box-shadow .15s ease;
|
|
142
144
|
}
|
|
143
145
|
|
|
144
|
-
.input, .select { min-height: 40px; }
|
|
146
|
+
.input, .select { height: 40px; min-height: 40px; font: inherit; line-height: 20px; }
|
|
145
147
|
.textarea { min-height: 132px; resize: vertical; }
|
|
146
148
|
.input::placeholder, .textarea::placeholder { color: var(--text-tertiary); }
|
|
147
149
|
.input:focus, .select:focus, .textarea:focus { border-color: var(--accent); }
|
|
@@ -169,6 +171,7 @@ button:disabled { cursor: not-allowed; opacity: .56; }
|
|
|
169
171
|
.button.ghost { border-color: transparent; background: transparent; color: var(--text-secondary); }
|
|
170
172
|
.button.small { min-height: 32px; padding: 5px 10px; font-size: 12px; }
|
|
171
173
|
.login-card .button { width: 100%; margin-top: 20px; }
|
|
174
|
+
.login-card .field + .field { margin-top: 16px; }
|
|
172
175
|
|
|
173
176
|
.app-shell { min-height: 100vh; min-height: 100dvh; display: grid; grid-template-columns: var(--sidebar-width) 6px minmax(0, 1fr); }
|
|
174
177
|
|
|
@@ -392,41 +395,6 @@ button:disabled { cursor: not-allowed; opacity: .56; }
|
|
|
392
395
|
.workspace-switcher p { max-width: 56ch; margin: 0; color: var(--text-tertiary); font-size: 12px; text-align: right; }
|
|
393
396
|
.workspace-tabs { flex: none; }
|
|
394
397
|
.workspace-tabs .tab { display: inline-flex; align-items: center; gap: 7px; }
|
|
395
|
-
.writeback-policy-control {
|
|
396
|
-
min-height: 36px;
|
|
397
|
-
display: inline-flex;
|
|
398
|
-
align-items: center;
|
|
399
|
-
gap: 7px;
|
|
400
|
-
border-left: 1px solid var(--border);
|
|
401
|
-
padding-left: 10px;
|
|
402
|
-
}
|
|
403
|
-
.writeback-policy-label { color: var(--text-tertiary); font-size: 11px; font-weight: 600; white-space: nowrap; }
|
|
404
|
-
.writeback-policy-segments {
|
|
405
|
-
display: inline-flex;
|
|
406
|
-
gap: 2px;
|
|
407
|
-
border: 1px solid var(--border);
|
|
408
|
-
border-radius: 9px;
|
|
409
|
-
padding: 2px;
|
|
410
|
-
background: color-mix(in srgb, var(--surface-soft) 72%, transparent);
|
|
411
|
-
}
|
|
412
|
-
.writeback-policy-option {
|
|
413
|
-
min-width: 48px;
|
|
414
|
-
min-height: 30px;
|
|
415
|
-
border: 0;
|
|
416
|
-
border-radius: 7px;
|
|
417
|
-
padding: 5px 10px;
|
|
418
|
-
background: transparent;
|
|
419
|
-
color: var(--text-tertiary);
|
|
420
|
-
font-size: 11px;
|
|
421
|
-
font-weight: 620;
|
|
422
|
-
transition: background .14s ease, color .14s ease, box-shadow .14s ease;
|
|
423
|
-
}
|
|
424
|
-
.writeback-policy-option:hover:not(:disabled) { background: var(--surface-hover); color: var(--text); }
|
|
425
|
-
.writeback-policy-option[aria-checked="true"] {
|
|
426
|
-
background: var(--surface-raised);
|
|
427
|
-
color: var(--text);
|
|
428
|
-
box-shadow: 0 1px 4px rgb(20 25 35 / 10%);
|
|
429
|
-
}
|
|
430
398
|
.tab-count { min-width: 20px; border-radius: 6px; padding: 1px 6px; background: color-mix(in srgb, var(--text-tertiary) 10%, transparent); color: var(--text-tertiary); font-size: 10px; font-variant-numeric: tabular-nums; }
|
|
431
399
|
.tab[aria-selected="true"] .tab-count { background: var(--accent-soft); color: var(--accent); }
|
|
432
400
|
.library-management, .mount-section { min-width: 0; }
|
|
@@ -678,6 +646,11 @@ button:disabled { cursor: not-allowed; opacity: .56; }
|
|
|
678
646
|
.candidate-header h3 { margin: 8px 0 0; font-size: 15px; }
|
|
679
647
|
.candidate-body { display: grid; grid-template-columns: minmax(0, 1fr) minmax(240px, .55fr); gap: 20px; padding: 0 18px 17px; }
|
|
680
648
|
.candidate-content { margin: 0; color: var(--text-secondary); white-space: pre-wrap; }
|
|
649
|
+
.candidate-body.is-conflict { grid-template-columns: 1fr; }
|
|
650
|
+
.candidate-comparison { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
|
|
651
|
+
.candidate-comparison section { min-width: 0; border: 1px solid var(--border); border-radius: 9px; padding: 12px; background: var(--surface-soft); }
|
|
652
|
+
.candidate-comparison strong { display: block; margin-bottom: 8px; color: var(--text); font-size: 12px; }
|
|
653
|
+
.candidate-comparison pre { overflow: auto; max-height: 280px; margin: 0; color: var(--text-secondary); font: inherit; line-height: 1.65; white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
681
654
|
.candidate-reason { border-left: 2px solid var(--border-strong); padding-left: 13px; color: var(--text-secondary); font-size: 12px; }
|
|
682
655
|
.candidate-reason strong { display: block; margin-bottom: 5px; color: var(--text-tertiary); font-size: 10px; text-transform: uppercase; letter-spacing: .08em; }
|
|
683
656
|
.candidate-target { display: block; margin-bottom: 13px; border-radius: 7px; padding: 7px 9px; background: var(--accent-soft); color: var(--accent); font-weight: 600; }
|
|
@@ -727,7 +700,7 @@ button:disabled { cursor: not-allowed; opacity: .56; }
|
|
|
727
700
|
flex-direction: column;
|
|
728
701
|
border: 1px solid var(--border-strong);
|
|
729
702
|
border-radius: 18px;
|
|
730
|
-
background: var(--surface
|
|
703
|
+
background: var(--dialog-surface);
|
|
731
704
|
box-shadow: 0 28px 80px rgb(0 0 0 / 24%);
|
|
732
705
|
}
|
|
733
706
|
.dialog.narrow { width: min(100%, 460px); }
|
|
@@ -752,7 +725,7 @@ button:disabled { cursor: not-allowed; opacity: .56; }
|
|
|
752
725
|
.dialog-header p { margin: 4px 0 0; color: var(--text-tertiary); font-size: 12px; }
|
|
753
726
|
.dialog-body { overflow: auto; padding: 20px; }
|
|
754
727
|
.dialog-footer { display: flex; justify-content: flex-end; gap: 8px; border-top: 1px solid var(--border); padding: 13px 20px; }
|
|
755
|
-
.form-grid { display: grid; grid-template-columns:
|
|
728
|
+
.form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); align-items: start; gap: 16px; }
|
|
756
729
|
.form-grid .span-2 { grid-column: 1 / -1; }
|
|
757
730
|
.check-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
|
|
758
731
|
.check-option { display: flex; align-items: center; gap: 8px; border: 1px solid var(--border); border-radius: 8px; padding: 9px; }
|
|
@@ -805,6 +778,7 @@ button:disabled { cursor: not-allowed; opacity: .56; }
|
|
|
805
778
|
}
|
|
806
779
|
|
|
807
780
|
@media (max-width: 760px) {
|
|
781
|
+
.candidate-comparison { grid-template-columns: 1fr; }
|
|
808
782
|
:root { --sidebar-width: 0px; }
|
|
809
783
|
.app-shell { display: block; }
|
|
810
784
|
.app-sidebar-resizer { display: none; }
|
|
@@ -890,7 +864,6 @@ button:disabled { cursor: not-allowed; opacity: .56; }
|
|
|
890
864
|
.topbar-actions .button.primary::before { content: "+"; font-size: 18px; }
|
|
891
865
|
.workspace-tabs { width: 100%; }
|
|
892
866
|
.workspace-tabs .tab { min-width: 0; flex: 1; justify-content: center; padding-inline: 8px; }
|
|
893
|
-
.writeback-policy-control { width: 100%; justify-content: space-between; border-left: 0; padding-left: 0; }
|
|
894
867
|
.document-toolbar-actions { width: 100%; }
|
|
895
868
|
.document-toolbar-actions .button.primary { margin-left: auto; }
|
|
896
869
|
.form-grid { grid-template-columns: 1fr; }
|