dsh-lite-memory 0.16.2 → 0.17.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 +9 -9
- package/TRIGGERS.md +14 -13
- package/anchor.js +259 -24
- package/client.js +1211 -206
- package/docs/CONVENTIONS.md +143 -0
- package/docs/RELEASE-0.17.0.md +8 -0
- package/index.js +337 -95
- package/learning.js +456 -48
- package/package.json +5 -2
- package/state.js +110 -5
- package/store.js +447 -61
- package/test/anchor.test.mjs +240 -0
- package/test/client.test.mjs +176 -6
- package/test/contract.test.mjs +9 -1
- package/test/learning.test.mjs +484 -2
- package/test/palace.test.mjs +102 -0
- package/test/runtime-context.test.mjs +186 -0
- package/test/state.test.mjs +121 -2
- package/test/store.test.mjs +454 -15
- package/view.js +60 -3
package/client.js
CHANGED
|
@@ -23,7 +23,7 @@ window.__ModuleLoader__.load({
|
|
|
23
23
|
const KEEP_OFF = { ...KEEP_STYLE, opacity: 0.45, cursor: 'default' };
|
|
24
24
|
const DROP = { flex: '0 0 auto', border: 'none', background: 'transparent', color: 'inherit', padding: '0 2px', fontSize: '12px', lineHeight: '18px', opacity: 0.55, cursor: 'pointer' };
|
|
25
25
|
const DOCK = { display: 'flex', flexDirection: 'column', gap: '2px', minWidth: 0, fontSize: '12px', lineHeight: '1.6' };
|
|
26
|
-
const TAIL = { ...DOCK, margin: '4px 0 2px'
|
|
26
|
+
const TAIL = { ...DOCK, margin: '4px 0 2px' };
|
|
27
27
|
const CHIP = { alignSelf: 'flex-start', border: 'none', background: 'transparent', color: 'inherit', padding: 0, fontSize: '12px', lineHeight: '1.6', opacity: 0.6, cursor: 'pointer', maxWidth: '100%', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' };
|
|
28
28
|
const ITEM = { alignSelf: 'flex-start', border: 'none', background: 'transparent', color: 'inherit', padding: 0, fontSize: '12px', lineHeight: '1.6', opacity: 0.75, cursor: 'pointer', textAlign: 'left', width: '100%', minWidth: 0, maxWidth: '100%', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' };
|
|
29
29
|
const BODY = { margin: '2px 0 4px', padding: '4px 0 4px 8px', borderLeft: '1px solid currentColor', opacity: 0.7, whiteSpace: 'pre-wrap', wordBreak: 'break-word', maxHeight: '180px', overflowY: 'auto' };
|
|
@@ -98,6 +98,13 @@ window.__ModuleLoader__.load({
|
|
|
98
98
|
origin: meta.origin === 'project_evolution' ? 'project_evolution' : 'user_explicit',
|
|
99
99
|
kind,
|
|
100
100
|
exposure: kind === 'memory-save' || kind === 'memory-forget' ? 'update' : 'reference',
|
|
101
|
+
reason: typeof meta.reason === 'string' ? meta.reason : '',
|
|
102
|
+
reasonCode: typeof meta.reasonCode === 'string' ? meta.reasonCode : '',
|
|
103
|
+
noTriggerReason: typeof meta.noTriggerReason === 'string' ? meta.noTriggerReason : '',
|
|
104
|
+
sourceExcerpt: meta.sourceExcerpt,
|
|
105
|
+
sourceRefs: meta.sourceRefs,
|
|
106
|
+
evidence: meta.evidence,
|
|
107
|
+
processingStatus: processingStatusOf(meta),
|
|
101
108
|
};
|
|
102
109
|
};
|
|
103
110
|
|
|
@@ -122,15 +129,22 @@ window.__ModuleLoader__.load({
|
|
|
122
129
|
const citationsOfTurn = (nodes) => {
|
|
123
130
|
if (!Array.isArray(nodes)) return EMPTY;
|
|
124
131
|
const out = [];
|
|
132
|
+
let noTriggerReason = '';
|
|
125
133
|
for (const node of nodes) {
|
|
126
134
|
const root = node && node.root;
|
|
127
135
|
if (root === null || typeof root !== 'object') continue;
|
|
128
136
|
for (const block of [root, ...(Array.isArray(root.subCalls) ? root.subCalls : [])]) {
|
|
129
137
|
const meta = metaOf(block);
|
|
130
138
|
if (!meta) continue;
|
|
131
|
-
|
|
139
|
+
const citations = citationsOfMeta(meta);
|
|
140
|
+
out.push(...citations);
|
|
141
|
+
if (citations.length === 0 && meta.kind === 'memory-search') {
|
|
142
|
+
const reason = [meta.noTriggerReason, meta.reason].find((value) => typeof value === 'string' && value);
|
|
143
|
+
if (reason) noTriggerReason = reason;
|
|
144
|
+
}
|
|
132
145
|
}
|
|
133
146
|
}
|
|
147
|
+
if (noTriggerReason) Object.defineProperty(out, 'noTriggerReason', { value: noTriggerReason, enumerable: false, configurable: true });
|
|
134
148
|
return out;
|
|
135
149
|
};
|
|
136
150
|
|
|
@@ -148,6 +162,9 @@ window.__ModuleLoader__.load({
|
|
|
148
162
|
out[at] = item;
|
|
149
163
|
}
|
|
150
164
|
}
|
|
165
|
+
if (items && typeof items.noTriggerReason === 'string' && items.noTriggerReason) {
|
|
166
|
+
Object.defineProperty(out, 'noTriggerReason', { value: items.noTriggerReason, enumerable: false, configurable: true });
|
|
167
|
+
}
|
|
151
168
|
return out;
|
|
152
169
|
};
|
|
153
170
|
|
|
@@ -196,7 +213,9 @@ window.__ModuleLoader__.load({
|
|
|
196
213
|
if (value !== lastNodes) {
|
|
197
214
|
lastNodes = value;
|
|
198
215
|
const next = dedupe(citationsOfTurn(value));
|
|
199
|
-
|
|
216
|
+
const nextReason = next.noTriggerReason || '';
|
|
217
|
+
const lastReason = snapshot.noTriggerReason || '';
|
|
218
|
+
if (!sameList(next, snapshot) || nextReason !== lastReason) snapshot = next;
|
|
200
219
|
}
|
|
201
220
|
return snapshot;
|
|
202
221
|
};
|
|
@@ -308,23 +327,267 @@ window.__ModuleLoader__.load({
|
|
|
308
327
|
setState('failed');
|
|
309
328
|
});
|
|
310
329
|
};
|
|
311
|
-
const
|
|
330
|
+
const processing = processingStatusOf(meta);
|
|
331
|
+
const baseLabel = firstLine(text) ?? RUNNING.memory_save;
|
|
332
|
+
const label = note || `${baseLabel}${processing ? ` · ${PROCESSING_LABEL[processing]}` : ''}`;
|
|
312
333
|
const button = changed && state !== 'dropped'
|
|
313
334
|
? h('button', { type: 'button', onClick: drop, disabled: !canDrop, 'aria-label': '撤销此次修改', style: state === 'idle' || state === 'failed' ? DROP : { ...DROP, opacity: 0.25, cursor: 'default' } }, '✕')
|
|
314
335
|
: null;
|
|
315
336
|
return h('div', { 'data-tool': props.toolName, style: ROW }, h('span', { style: DOT }, '·'), h('span', { style: LABEL }, label), button);
|
|
316
337
|
};
|
|
317
338
|
|
|
339
|
+
const scopeTag = (scope) => (scope === 'global' ? '[全局]' : '[项目]');
|
|
340
|
+
|
|
341
|
+
/** 处理状态只认服务端枚举,绝不从中文 reason 反推。 */
|
|
342
|
+
const PROCESSING = ['applied', 'unchanged', 'superseded', 'failed'];
|
|
343
|
+
const PROCESSING_LABEL = {
|
|
344
|
+
applied: '已应用', unchanged: '无变化', superseded: '已被新版本取代', failed: '处理失败',
|
|
345
|
+
};
|
|
346
|
+
const processingStatusOf = (source) => {
|
|
347
|
+
if (!source || typeof source !== 'object') return '';
|
|
348
|
+
for (const field of ['processingStatus', 'processing', 'outcome', 'status']) {
|
|
349
|
+
const value = source[field];
|
|
350
|
+
if (typeof value === 'string' && PROCESSING.includes(value)) return value;
|
|
351
|
+
if (value && typeof value === 'object' && typeof value.status === 'string' && PROCESSING.includes(value.status)) return value.status;
|
|
352
|
+
}
|
|
353
|
+
return '';
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
const recordOfPayload = (payload) =>
|
|
357
|
+
(payload && payload.record && typeof payload.record === 'object' && !Array.isArray(payload.record) ? payload.record : null);
|
|
358
|
+
|
|
359
|
+
const bodyTextOf = (value) => {
|
|
360
|
+
if (typeof value === 'string') return value;
|
|
361
|
+
if (!value || typeof value !== 'object') return '';
|
|
362
|
+
for (const field of ['body', 'text', 'value']) {
|
|
363
|
+
if (typeof value[field] === 'string') return value[field];
|
|
364
|
+
}
|
|
365
|
+
return '';
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
/** 当前正文与历史版本分开,历史不拼进正文。 */
|
|
369
|
+
const currentBodyOf = (payload) => {
|
|
370
|
+
if (!payload || typeof payload !== 'object') return '';
|
|
371
|
+
if (typeof payload.currentBody === 'string') return payload.currentBody;
|
|
372
|
+
const record = recordOfPayload(payload);
|
|
373
|
+
if (record) {
|
|
374
|
+
const recordBody = bodyTextOf(record.current);
|
|
375
|
+
if (recordBody) return recordBody;
|
|
376
|
+
if (typeof record.body === 'string') return record.body;
|
|
377
|
+
}
|
|
378
|
+
if (payload.current !== undefined && typeof payload.current !== 'boolean') {
|
|
379
|
+
const current = bodyTextOf(payload.current);
|
|
380
|
+
if (current) return current;
|
|
381
|
+
}
|
|
382
|
+
return typeof payload.body === 'string' ? payload.body : '';
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
const historyOfPayload = (payload) => {
|
|
386
|
+
const record = recordOfPayload(payload);
|
|
387
|
+
const current = payload && payload.current && typeof payload.current === 'object' ? payload.current : null;
|
|
388
|
+
const lists = [
|
|
389
|
+
payload && payload.versions,
|
|
390
|
+
payload && payload.history,
|
|
391
|
+
record && record.versions,
|
|
392
|
+
record && record.history,
|
|
393
|
+
current && current.versions,
|
|
394
|
+
];
|
|
395
|
+
for (const list of lists) {
|
|
396
|
+
if (Array.isArray(list)) return list.filter((row) => row !== null && row !== undefined);
|
|
397
|
+
}
|
|
398
|
+
return EMPTY;
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
/** 历史行统一成 old → new,缺前后值用相邻版本补。 */
|
|
402
|
+
const historyDiffsOf = (payload) => {
|
|
403
|
+
const rows = historyOfPayload(payload);
|
|
404
|
+
if (rows.length === 0) return EMPTY;
|
|
405
|
+
const currentBody = currentBodyOf(payload);
|
|
406
|
+
const normalized = rows.map((row, index) => {
|
|
407
|
+
const value = row && typeof row === 'object' && !Array.isArray(row) ? row : { body: row };
|
|
408
|
+
const explicitOld = [value.old, value.from, value.before, value.previousBody]
|
|
409
|
+
.find((item) => typeof item === 'string');
|
|
410
|
+
const explicitNew = [value.next, value.to, value.after, value.newBody]
|
|
411
|
+
.find((item) => typeof item === 'string');
|
|
412
|
+
return {
|
|
413
|
+
key: `${Number.isInteger(value.revision) ? value.revision : 'v'}-${index}`,
|
|
414
|
+
at: typeof value.at === 'string' ? value.at : (typeof value.when === 'string' ? value.when : ''),
|
|
415
|
+
revision: Number.isInteger(value.revision) ? value.revision : null,
|
|
416
|
+
status: typeof value.status === 'string' ? value.status : '',
|
|
417
|
+
body: bodyTextOf(value), // ui-copy-ignore
|
|
418
|
+
old: explicitOld || '', // ui-copy-ignore
|
|
419
|
+
next: explicitNew || '', // ui-copy-ignore
|
|
420
|
+
};
|
|
421
|
+
});
|
|
422
|
+
const byRevision = normalized.every((row) => row.revision !== null);
|
|
423
|
+
if (byRevision) normalized.sort((a, b) => a.revision - b.revision);
|
|
424
|
+
else if (normalized.every((row) => Boolean(row.at))) normalized.sort((a, b) => a.at.localeCompare(b.at));
|
|
425
|
+
return normalized.map((row, index) => ({
|
|
426
|
+
...row,
|
|
427
|
+
old: row.old || row.body || '',
|
|
428
|
+
next: row.next || (index + 1 < normalized.length ? normalized[index + 1].body : currentBody),
|
|
429
|
+
}));
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
const payloadFieldOf = (payload, field) => {
|
|
433
|
+
const record = recordOfPayload(payload);
|
|
434
|
+
const current = payload && payload.current && typeof payload.current === 'object' ? payload.current : null;
|
|
435
|
+
for (const owner of [payload, record, current]) {
|
|
436
|
+
if (owner && owner[field] !== undefined) return owner[field];
|
|
437
|
+
}
|
|
438
|
+
return undefined;
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
const sourceExcerptOf = (payload) => bodyTextOf(payloadFieldOf(payload, 'sourceExcerpt'));
|
|
442
|
+
const refsOfPayload = (payload) => {
|
|
443
|
+
const value = payloadFieldOf(payload, 'sourceRefs');
|
|
444
|
+
if (typeof value === 'string' && value) return [value];
|
|
445
|
+
if (Array.isArray(value)) return value;
|
|
446
|
+
if (value && typeof value === 'object') return [value];
|
|
447
|
+
return EMPTY;
|
|
448
|
+
};
|
|
449
|
+
const evidenceOfPayload = (payload) => {
|
|
450
|
+
const value = payloadFieldOf(payload, 'evidence');
|
|
451
|
+
if (typeof value === 'string' && value) return [value];
|
|
452
|
+
if (Array.isArray(value)) return value;
|
|
453
|
+
if (value && typeof value === 'object') return [value];
|
|
454
|
+
return EMPTY;
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
const refLabelOf = (ref, index) => {
|
|
458
|
+
if (typeof ref === 'string') return ref;
|
|
459
|
+
if (!ref || typeof ref !== 'object') return '';
|
|
460
|
+
if (typeof ref.label === 'string' && ref.label) return ref.label;
|
|
461
|
+
if (typeof ref.title === 'string' && ref.title) return ref.title;
|
|
462
|
+
if (typeof ref.text === 'string' && ref.text) return ref.text;
|
|
463
|
+
if (ref.message !== undefined) return `消息 ${ref.message}`;
|
|
464
|
+
if (ref.messageId !== undefined) return `消息 ${ref.messageId}`;
|
|
465
|
+
if (ref.turn !== undefined) return `第 ${ref.turn} 轮`;
|
|
466
|
+
if (ref.turnIndex !== undefined) return `第 ${ref.turnIndex} 轮`;
|
|
467
|
+
return ref.id !== undefined ? String(ref.id) : `引用 ${index + 1}`;
|
|
468
|
+
};
|
|
469
|
+
const refHrefOf = (ref) => (ref && typeof ref === 'object'
|
|
470
|
+
? [ref.href, ref.url, ref.link].find((value) => typeof value === 'string' && value)
|
|
471
|
+
: '');
|
|
472
|
+
|
|
473
|
+
const linkStyle = { color: 'inherit', textDecoration: 'underline', textUnderlineOffset: '2px', wordBreak: 'break-all' };
|
|
474
|
+
|
|
475
|
+
const HistoryVersions = (props) => {
|
|
476
|
+
const rows = Array.isArray(props.rows) ? props.rows : EMPTY;
|
|
477
|
+
const [open, setOpen] = React.useState(false);
|
|
478
|
+
if (rows.length === 0) return null;
|
|
479
|
+
return h('div', { style: { display: 'flex', flexDirection: 'column', gap: '2px' } },
|
|
480
|
+
h('button', {
|
|
481
|
+
type: 'button',
|
|
482
|
+
onClick: () => setOpen((value) => !value),
|
|
483
|
+
'aria-expanded': open,
|
|
484
|
+
'aria-label': `${open ? '收起' : '展开'}历史版本`,
|
|
485
|
+
style: CHIP,
|
|
486
|
+
}, `历史 ${rows.length}${open ? ' · 收起' : ''}`),
|
|
487
|
+
open ? h('div', { style: BODY }, rows.map((row) => h('div', {
|
|
488
|
+
key: row.key, style: { margin: '0 0 4px', minWidth: 0 },
|
|
489
|
+
},
|
|
490
|
+
h('div', { style: { opacity: 0.65, fontSize: '11px' } },
|
|
491
|
+
[row.at, row.revision === null ? '' : `v${row.revision}`, row.status].filter(Boolean).join(' · ')),
|
|
492
|
+
h('div', null,
|
|
493
|
+
h('span', { style: { opacity: 0.6 } }, SHORT(row.old, 120) || '—'),
|
|
494
|
+
' → ',
|
|
495
|
+
h('span', null, SHORT(row.next, 120) || '—'))))) : null,
|
|
496
|
+
);
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
const DetailExtras = (props) => {
|
|
500
|
+
const payload = props.payload;
|
|
501
|
+
const excerpt = sourceExcerptOf(payload);
|
|
502
|
+
const refs = refsOfPayload(payload);
|
|
503
|
+
const evidence = evidenceOfPayload(payload);
|
|
504
|
+
const history = historyDiffsOf(payload);
|
|
505
|
+
if (!excerpt && refs.length === 0 && evidence.length === 0 && history.length === 0) return null;
|
|
506
|
+
const links = (items, label) => (items.length === 0 ? null : h('div', { style: { display: 'flex', gap: '6px', minWidth: 0 } },
|
|
507
|
+
h('span', { style: { opacity: 0.6, flex: '0 0 auto' } }, label),
|
|
508
|
+
h('span', { style: { minWidth: 0, wordBreak: 'break-word' } }, items.map((item, index) => {
|
|
509
|
+
const href = refHrefOf(item);
|
|
510
|
+
const text = refLabelOf(item, index);
|
|
511
|
+
const key = `${index}-${text}`;
|
|
512
|
+
if (!href) return h('span', { key }, index > 0 ? ' · ' : '', text);
|
|
513
|
+
return h('span', { key }, index > 0 ? ' · ' : '', h('a', {
|
|
514
|
+
href, target: '_blank', rel: 'noreferrer', style: linkStyle,
|
|
515
|
+
}, text));
|
|
516
|
+
}))));
|
|
517
|
+
return h('div', { style: { display: 'flex', flexDirection: 'column', gap: '4px' } },
|
|
518
|
+
excerpt ? h('div', { style: BODY }, h('span', { style: { opacity: 0.65 } }, '来源'), ' ', excerpt) : null,
|
|
519
|
+
links(refs, '消息'),
|
|
520
|
+
links(evidence, '依据'),
|
|
521
|
+
h(HistoryVersions, { rows: history }));
|
|
522
|
+
};
|
|
523
|
+
|
|
318
524
|
const versionLine = (shown) => {
|
|
319
525
|
const scope = shown.scopeLabel || (shown.scope === 'global' ? '全局' : '项目');
|
|
320
526
|
const archived = shown.status === 'archived' ? ' · 已归档' : '';
|
|
321
|
-
|
|
527
|
+
const processing = processingStatusOf(shown.processingStatus ? { processingStatus: shown.processingStatus } : shown);
|
|
528
|
+
return `${scope}${archived}${processing ? ` · ${PROCESSING_LABEL[processing]}` : ''}`;
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* 引用三层的色调:中性 / 强调 / 成功。
|
|
533
|
+
* 三档色阶:中性 / 强调 / 成功;正文用主文本色。
|
|
534
|
+
*/
|
|
535
|
+
const REF_TONE = {
|
|
536
|
+
found: 'var(--dsw-alias-label-secondary)',
|
|
537
|
+
provided: 'var(--dsw-alias-brand-primary)',
|
|
538
|
+
update: 'var(--dsw-alias-state-success-primary)',
|
|
539
|
+
};
|
|
540
|
+
const REF_META = { display: 'flex', alignItems: 'center', gap: '6px', margin: '0 0 2px 2px', fontSize: '11px', lineHeight: '18px', color: 'var(--dsw-alias-label-secondary)' };
|
|
541
|
+
const REF_BODY = { margin: '2px 0 3px', padding: '3px 0 3px 9px', display: 'flex', flexDirection: 'column', gap: '3px', fontSize: '12px', lineHeight: '1.65', maxHeight: '180px', overflowY: 'auto' };
|
|
542
|
+
|
|
543
|
+
/** 出处一句话:项目/全局、来源、状态。 */
|
|
544
|
+
const refMetaOf = (item) => {
|
|
545
|
+
const parts = [item.scope === 'global' ? '全局' : '项目', item.origin === 'project_evolution' ? '自进化' : '你记的'];
|
|
546
|
+
if (item.status === 'archived') parts.push('已归档');
|
|
547
|
+
const processing = PROCESSING_LABEL[item.processingStatus] || '';
|
|
548
|
+
if (processing) parts.push(processing);
|
|
549
|
+
return parts.join(' · ');
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
const RefChevron = (props) => h('svg', {
|
|
553
|
+
width: 8, height: 8, viewBox: '0 0 8 8', 'aria-hidden': 'true', focusable: 'false',
|
|
554
|
+
style: { flex: '0 0 auto', opacity: 0.65, transform: props.open ? 'rotate(90deg)' : 'none' },
|
|
555
|
+
}, h('path', { d: 'M2.5 1 L5.5 4 L2.5 7', fill: 'none', stroke: 'currentColor', 'stroke-width': '1.2', 'stroke-linecap': 'round', 'stroke-linejoin': 'round' }));
|
|
556
|
+
|
|
557
|
+
/** 一条引用:标题一行、原因与来源一行,窄栏也不抢宽度。 */
|
|
558
|
+
const RefRow = (props) => {
|
|
559
|
+
const [hover, setHover] = React.useState(false);
|
|
560
|
+
const item = props.item;
|
|
561
|
+
const reason = item.noTriggerReason || item.reason || '';
|
|
562
|
+
return h('button', {
|
|
563
|
+
type: 'button', // ui-copy-ignore
|
|
564
|
+
onMouseEnter: () => setHover(true),
|
|
565
|
+
onMouseLeave: () => setHover(false),
|
|
566
|
+
onClick: props.onShow, // ui-copy-ignore
|
|
567
|
+
'aria-label': `查看记忆 ${item.title || item.id}${reason ? `,${reason}` : ''}`,
|
|
568
|
+
title: reason ? `${item.title || item.id},${reason}` : (item.title || item.id),
|
|
569
|
+
style: { // ui-copy-ignore
|
|
570
|
+
display: 'flex', alignItems: 'flex-start', gap: '7px', width: '100%', minWidth: 0, textAlign: 'left',
|
|
571
|
+
border: 'none', borderRadius: '8px', padding: '3px 7px', cursor: 'pointer', color: 'inherit',
|
|
572
|
+
background: props.active || hover ? 'var(--dsw-alias-bg-layer-2)' : 'transparent',
|
|
573
|
+
fontSize: '12px', lineHeight: '18px',
|
|
574
|
+
},
|
|
575
|
+
},
|
|
576
|
+
h('span', { 'aria-hidden': 'true', style: { flex: '0 0 auto', alignSelf: 'stretch', width: '2px', minHeight: '10px', borderRadius: '999px', background: props.tone, opacity: 0.85 } }),
|
|
577
|
+
h('span', { style: { display: 'flex', flexDirection: 'column', gap: '1px', flex: '1 1 auto', minWidth: 0 } },
|
|
578
|
+
h('span', { style: { minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, item.title || item.id),
|
|
579
|
+
h('span', { style: { display: 'flex', alignItems: 'center', gap: '6px', minWidth: 0, fontSize: '11px', lineHeight: '16px', color: 'var(--dsw-alias-label-secondary)' } },
|
|
580
|
+
reason ? h('span', { style: { minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, reason) : null,
|
|
581
|
+
h('span', { style: { marginLeft: 'auto', flex: '0 0 auto', fontVariantNumeric: 'tabular-nums' } }, refMetaOf(item)),
|
|
582
|
+
),
|
|
583
|
+
));
|
|
322
584
|
};
|
|
323
585
|
|
|
324
586
|
/** 一段引用:点开是名字,再点是当前正文。 */
|
|
325
587
|
const RefSection = (props) => {
|
|
326
588
|
const items = Array.isArray(props.items) ? props.items : EMPTY;
|
|
327
589
|
const sessionId = typeof props.sessionId === 'string' ? props.sessionId : '';
|
|
590
|
+
const tone = REF_TONE[props.tone] || REF_TONE.found;
|
|
328
591
|
const [open, setOpen] = React.useState(false);
|
|
329
592
|
const [shown, setShown] = React.useState(null);
|
|
330
593
|
const [note, setNote] = React.useState('');
|
|
@@ -340,7 +603,10 @@ window.__ModuleLoader__.load({
|
|
|
340
603
|
setShown(null);
|
|
341
604
|
setNote('');
|
|
342
605
|
}, [sessionId, props.turnKey]);
|
|
343
|
-
if (items.length === 0)
|
|
606
|
+
if (items.length === 0) {
|
|
607
|
+
const reason = typeof props.noTriggerReason === 'string' ? props.noTriggerReason : '';
|
|
608
|
+
return reason ? h('div', { style: { ...REF_META, margin: 0 } }, reason) : null;
|
|
609
|
+
}
|
|
344
610
|
const close = () => {
|
|
345
611
|
generation.current += 1;
|
|
346
612
|
setShown(null);
|
|
@@ -377,30 +643,35 @@ window.__ModuleLoader__.load({
|
|
|
377
643
|
scopeLabel: typeof payload.scopeLabel === 'string' ? payload.scopeLabel : '',
|
|
378
644
|
revision: Number.isInteger(payload.revision) ? payload.revision : item.revision,
|
|
379
645
|
status: typeof payload.status === 'string' ? payload.status : 'ok',
|
|
380
|
-
body:
|
|
646
|
+
body: currentBodyOf(payload),
|
|
647
|
+
processingStatus: processingStatusOf(payload) || item.processingStatus || '',
|
|
648
|
+
payload,
|
|
381
649
|
current: payload.revision === item.revision,
|
|
382
650
|
});
|
|
383
651
|
});
|
|
384
652
|
};
|
|
385
|
-
const rows = open ? items.map((item) => h(
|
|
653
|
+
const rows = open ? items.map((item) => h(RefRow, {
|
|
386
654
|
key: `${item.scope}|${item.id}|${item.revision}`, // ui-copy-ignore
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
title: item.title || item.id,
|
|
391
|
-
style: ITEM, // ui-copy-ignore
|
|
392
|
-
}, `${item.title || item.id}${item.origin === 'project_evolution' ? ' · 实验' : ''}`)) : [];
|
|
655
|
+
item, tone, active: Boolean(shown && shown.id === item.id),
|
|
656
|
+
onShow: () => show(item),
|
|
657
|
+
})) : [];
|
|
393
658
|
const detail = shown && shown.ref && items.some((item) => item.id === shown.id && item.revision === shown.ref.revision)
|
|
394
|
-
? h('div', { style:
|
|
395
|
-
shown.loading ? '
|
|
396
|
-
shown.gone ? '这条记忆已删除' : null,
|
|
397
|
-
shown.invalid ? '
|
|
659
|
+
? h('div', { style: { ...REF_BODY, borderLeft: `2px solid ${tone}` } },
|
|
660
|
+
shown.loading ? h('span', { style: REF_META }, '读取中…') : null,
|
|
661
|
+
shown.gone ? h('span', { style: REF_META }, '这条记忆已删除') : null,
|
|
662
|
+
shown.invalid ? h('span', { style: REF_META }, '读不到正文,点名字重试') : null,
|
|
398
663
|
!shown.loading && !shown.gone && !shown.invalid ? h(React.Fragment, null,
|
|
399
|
-
shown.title && shown.title !== shown.ref.title
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
664
|
+
shown.title && shown.title !== shown.ref.title
|
|
665
|
+
? h('div', { style: { fontSize: '12px', lineHeight: '1.55', fontWeight: 600 } }, shown.title)
|
|
666
|
+
: null,
|
|
667
|
+
h('div', { style: REF_META }, versionLine(shown)),
|
|
668
|
+
shown.when ? h('div', { style: REF_META }, shown.when) : null,
|
|
669
|
+
shown.body
|
|
670
|
+
? h('div', { style: { color: 'var(--dsw-alias-label-primary)', whiteSpace: 'pre-wrap', wordBreak: 'break-word' } }, shown.body)
|
|
671
|
+
: null,
|
|
672
|
+
h(DetailExtras, { payload: { ...shown.ref, ...shown.payload } }),
|
|
403
673
|
) : null,
|
|
674
|
+
note ? h('span', { style: REF_META }, note) : null,
|
|
404
675
|
)
|
|
405
676
|
: null;
|
|
406
677
|
return h('div', { style: DOCK, 'data-memory-section': props.label },
|
|
@@ -408,12 +679,20 @@ window.__ModuleLoader__.load({
|
|
|
408
679
|
type: 'button',
|
|
409
680
|
onClick: () => { if (open) close(); else setOpen(true); },
|
|
410
681
|
'aria-expanded': open,
|
|
411
|
-
'aria-label': `${open ? '收起' : '展开'}${props.label}
|
|
412
|
-
style:
|
|
413
|
-
|
|
682
|
+
'aria-label': `${open ? '收起' : '展开'}${props.label},${items.length} 条`,
|
|
683
|
+
style: {
|
|
684
|
+
alignSelf: 'flex-start', display: 'inline-flex', alignItems: 'center', gap: '6px',
|
|
685
|
+
border: 'none', background: 'transparent', padding: '1px 0', cursor: 'pointer',
|
|
686
|
+
color: 'var(--dsw-alias-label-secondary)', fontSize: '11px', lineHeight: '18px',
|
|
687
|
+
},
|
|
688
|
+
},
|
|
689
|
+
h('span', { 'aria-hidden': 'true', style: { width: '2px', height: '10px', borderRadius: '999px', background: tone, opacity: 0.9 } }),
|
|
690
|
+
h('span', null, props.label),
|
|
691
|
+
h('span', { style: { fontVariantNumeric: 'tabular-nums', opacity: 0.8 } }, String(items.length)),
|
|
692
|
+
h(RefChevron, { open }),
|
|
693
|
+
),
|
|
414
694
|
...rows,
|
|
415
695
|
detail,
|
|
416
|
-
note ? h('div', { style: BODY }, note) : null,
|
|
417
696
|
);
|
|
418
697
|
};
|
|
419
698
|
|
|
@@ -422,7 +701,7 @@ window.__ModuleLoader__.load({
|
|
|
422
701
|
/** 大脑:两瓣轮廓加一条中线,状态只靠透明度和面板文字。 */
|
|
423
702
|
const BrainIcon = () => h('svg', {
|
|
424
703
|
width: 18, height: 18, viewBox: '0 0 24 24', fill: 'none',
|
|
425
|
-
stroke: 'currentColor',
|
|
704
|
+
stroke: 'currentColor', 'stroke-width': 1.5, 'stroke-linecap': 'round', 'stroke-linejoin': 'round',
|
|
426
705
|
'aria-hidden': 'true', focusable: 'false',
|
|
427
706
|
},
|
|
428
707
|
h('path', { d: 'M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z' }),
|
|
@@ -431,7 +710,7 @@ window.__ModuleLoader__.load({
|
|
|
431
710
|
|
|
432
711
|
const CloseIcon = () => h('svg', {
|
|
433
712
|
width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none',
|
|
434
|
-
stroke: 'currentColor',
|
|
713
|
+
stroke: 'currentColor', 'stroke-width': 1.6, 'stroke-linecap': 'round',
|
|
435
714
|
'aria-hidden': 'true', focusable: 'false',
|
|
436
715
|
}, h('path', { d: 'M6 6l12 12M18 6L6 18' }));
|
|
437
716
|
|
|
@@ -449,6 +728,151 @@ window.__ModuleLoader__.load({
|
|
|
449
728
|
const text = typeof value === 'string' ? value : '';
|
|
450
729
|
return text.length > size ? `${text.slice(0, size)}…` : text;
|
|
451
730
|
};
|
|
731
|
+
|
|
732
|
+
/** 显示设置只决定画不画,不改学习状态;按项目 key 存在本机。 */
|
|
733
|
+
const DISPLAY_DEFAULTS = Object.freeze({ graph: false, palace: false });
|
|
734
|
+
const displayKeyOf = (projectKey) => `lite-memory:display:${projectKey || 'default'}`;
|
|
735
|
+
const displayStoreOf = () => {
|
|
736
|
+
try {
|
|
737
|
+
const store = globalThis.localStorage;
|
|
738
|
+
return store && typeof store.getItem === 'function' ? store : null;
|
|
739
|
+
} catch {
|
|
740
|
+
return null;
|
|
741
|
+
}
|
|
742
|
+
};
|
|
743
|
+
const readDisplaySettings = (projectKey) => {
|
|
744
|
+
const store = displayStoreOf();
|
|
745
|
+
const next = { ...DISPLAY_DEFAULTS };
|
|
746
|
+
if (!store) return next;
|
|
747
|
+
try {
|
|
748
|
+
const raw = store.getItem(displayKeyOf(projectKey));
|
|
749
|
+
if (!raw) return next;
|
|
750
|
+
const saved = JSON.parse(raw);
|
|
751
|
+
if (saved && typeof saved === 'object') {
|
|
752
|
+
next.graph = saved.graph === true;
|
|
753
|
+
next.palace = saved.palace === true;
|
|
754
|
+
}
|
|
755
|
+
} catch {
|
|
756
|
+
return next;
|
|
757
|
+
}
|
|
758
|
+
return next;
|
|
759
|
+
};
|
|
760
|
+
const writeDisplaySettings = (projectKey, next) => {
|
|
761
|
+
const store = displayStoreOf();
|
|
762
|
+
if (!store) return false;
|
|
763
|
+
try {
|
|
764
|
+
store.setItem(displayKeyOf(projectKey), JSON.stringify({
|
|
765
|
+
graph: next && next.graph === true,
|
|
766
|
+
palace: next && next.palace === true,
|
|
767
|
+
}));
|
|
768
|
+
return true;
|
|
769
|
+
} catch {
|
|
770
|
+
return false;
|
|
771
|
+
}
|
|
772
|
+
};
|
|
773
|
+
/** 功能关闭时视图统一回列表,不保留一个画不出来的页签。 */
|
|
774
|
+
const graphViewOf = (enabled, view) => (enabled === true && view === 'graph' ? 'graph' : 'list');
|
|
775
|
+
const viewModeOf = (display, view) => {
|
|
776
|
+
if (view === 'graph' && display && display.graph === true) return 'graph';
|
|
777
|
+
if (view === 'palace' && display && display.palace === true) return 'palace';
|
|
778
|
+
return 'list';
|
|
779
|
+
};
|
|
780
|
+
const reduceMotionOf = () => {
|
|
781
|
+
try {
|
|
782
|
+
const query = globalThis.matchMedia && globalThis.matchMedia('(prefers-reduced-motion: reduce)');
|
|
783
|
+
return Boolean(query && query.matches === true);
|
|
784
|
+
} catch {
|
|
785
|
+
return false;
|
|
786
|
+
}
|
|
787
|
+
};
|
|
788
|
+
/** 跟随系统偏好切换,打开面板期间改设置也能立即生效。 */
|
|
789
|
+
const useReducedMotion = () => {
|
|
790
|
+
const [reduced, setReduced] = React.useState(reduceMotionOf);
|
|
791
|
+
React.useEffect(() => {
|
|
792
|
+
let query = null;
|
|
793
|
+
try {
|
|
794
|
+
query = globalThis.matchMedia && globalThis.matchMedia('(prefers-reduced-motion: reduce)');
|
|
795
|
+
} catch {
|
|
796
|
+
query = null;
|
|
797
|
+
}
|
|
798
|
+
if (!query || typeof query.addEventListener !== 'function') return undefined;
|
|
799
|
+
const update = () => setReduced(query.matches === true);
|
|
800
|
+
query.addEventListener('change', update);
|
|
801
|
+
return () => query.removeEventListener('change', update);
|
|
802
|
+
}, []);
|
|
803
|
+
return reduced;
|
|
804
|
+
};
|
|
805
|
+
|
|
806
|
+
/** 标签/话题按精确值筛选;只有自由搜索才用包含匹配。 */
|
|
807
|
+
const filterRecords = (records, query, hub) => {
|
|
808
|
+
const rows = Array.isArray(records) ? records : EMPTY;
|
|
809
|
+
const kind = hub && typeof hub.kind === 'string' ? hub.kind : '';
|
|
810
|
+
const value = hub && typeof hub.value === 'string' ? hub.value : '';
|
|
811
|
+
if (kind === 'tag' && value) {
|
|
812
|
+
return rows.filter((record) => Array.isArray(record.tags) && record.tags.includes(value));
|
|
813
|
+
}
|
|
814
|
+
if (kind === 'topic' && value) {
|
|
815
|
+
return rows.filter((record) => record.topicKey === value);
|
|
816
|
+
}
|
|
817
|
+
const needle = typeof query === 'string' ? query.trim().toLowerCase() : '';
|
|
818
|
+
if (!needle) return rows;
|
|
819
|
+
return rows.filter((record) => {
|
|
820
|
+
const tags = Array.isArray(record.tags) ? record.tags.join(' ') : '';
|
|
821
|
+
return `${record.title || ''} ${record.summary || ''} ${tags} ${record.topicKey || ''} ${KIND_LABEL[record.kind] || ''}`.toLowerCase().includes(needle);
|
|
822
|
+
});
|
|
823
|
+
};
|
|
824
|
+
|
|
825
|
+
/** 记忆宫殿:房间固定,槽位固定,换项目只换陈列内容。 */
|
|
826
|
+
const PALACE_PAGE_SIZE = 24;
|
|
827
|
+
const PALACE_PREVIEW = 4;
|
|
828
|
+
const PALACE_ROOMS = Object.freeze([
|
|
829
|
+
Object.freeze({ id: 'habits', name: '习惯与规范', kinds: Object.freeze(['preference', 'convention']) }),
|
|
830
|
+
Object.freeze({ id: 'methods', name: '方法与经验', kinds: Object.freeze(['procedure', 'pitfall']) }),
|
|
831
|
+
Object.freeze({ id: 'entries', name: '资料与入口', kinds: Object.freeze(['pointer', 'credential_hint']) }),
|
|
832
|
+
Object.freeze({ id: 'notes', name: '其他记录', kinds: Object.freeze(['note']) }),
|
|
833
|
+
]);
|
|
834
|
+
const PALACE_ROOM_OF_KIND = Object.freeze({
|
|
835
|
+
preference: 'habits', convention: 'habits',
|
|
836
|
+
procedure: 'methods', pitfall: 'methods',
|
|
837
|
+
pointer: 'entries', credential_hint: 'entries',
|
|
838
|
+
note: 'notes',
|
|
839
|
+
});
|
|
840
|
+
const palaceRoomIdOf = (kind) => PALACE_ROOM_OF_KIND[kind] || 'notes';
|
|
841
|
+
const palaceRoomsOf = (records) => {
|
|
842
|
+
const rows = Array.isArray(records) ? records : EMPTY;
|
|
843
|
+
return PALACE_ROOMS.map((room) => {
|
|
844
|
+
const items = rows.filter((record) => palaceRoomIdOf(record && record.kind) === room.id);
|
|
845
|
+
return { id: room.id, name: room.name, items, total: items.length, pages: Math.max(1, Math.ceil(items.length / PALACE_PAGE_SIZE)) };
|
|
846
|
+
});
|
|
847
|
+
};
|
|
848
|
+
const palaceRoomPageOf = (records, roomId, page) => {
|
|
849
|
+
const room = palaceRoomsOf(records).find((row) => row.id === roomId);
|
|
850
|
+
if (!room) return { items: EMPTY, total: 0, pages: 0, page: 1 };
|
|
851
|
+
const current = Math.min(room.pages, Math.max(1, Number.isFinite(page) ? Math.floor(page) : 1));
|
|
852
|
+
return {
|
|
853
|
+
items: room.items.slice((current - 1) * PALACE_PAGE_SIZE, current * PALACE_PAGE_SIZE),
|
|
854
|
+
total: room.total, pages: room.pages, page: current,
|
|
855
|
+
};
|
|
856
|
+
};
|
|
857
|
+
/** 页码最多铺 7 个,当前页始终在窗口里。 */
|
|
858
|
+
const palacePageWindow = (page, pages) => {
|
|
859
|
+
const total = Math.max(1, Math.floor(Number.isFinite(pages) ? pages : 1));
|
|
860
|
+
const current = Math.min(total, Math.max(1, Math.floor(Number.isFinite(page) ? page : 1)));
|
|
861
|
+
if (total <= 7) return Array.from({ length: total }, (_, index) => index + 1);
|
|
862
|
+
const start = Math.max(1, Math.min(current - 3, total - 6));
|
|
863
|
+
return Array.from({ length: 7 }, (_, index) => start + index);
|
|
864
|
+
};
|
|
865
|
+
const palaceToggleOf = (expanded, roomId) => (expanded === roomId ? '' : roomId);
|
|
866
|
+
const palaceMotionOf = (reduced) => ({ duration: reduced === true ? 0 : 180, easing: 'cubic-bezier(0.2, 0.7, 0.3, 1)' });
|
|
867
|
+
|
|
868
|
+
/** 失败回执分「当前」和「过去」:当前失败要修,过去的只说明发生过。 */
|
|
869
|
+
const failureStateOf = (evolution) => {
|
|
870
|
+
const rows = evolution && evolution.economy && Array.isArray(evolution.economy.recent) ? evolution.economy.recent : EMPTY;
|
|
871
|
+
if (rows.length === 0) return '';
|
|
872
|
+
if (rows[0] && rows[0].status === 'failed') return 'current';
|
|
873
|
+
return rows.slice(1).some((row) => row && row.status === 'failed') ? 'past' : '';
|
|
874
|
+
};
|
|
875
|
+
|
|
452
876
|
/** 面板开关。 */
|
|
453
877
|
const panelUi = { open: false, sessionId: '' };
|
|
454
878
|
const panelListeners = new Set();
|
|
@@ -533,74 +957,124 @@ window.__ModuleLoader__.load({
|
|
|
533
957
|
|
|
534
958
|
/**
|
|
535
959
|
* 图谱只画真实关系:显式关联,以及记录自己的标签 / 主题。
|
|
536
|
-
*
|
|
960
|
+
* 标签 / 主题是组:枢纽画大点,成员围在它周围画小点。
|
|
961
|
+
* 位置由 id 与成员关系决定,同一份数据每次打开都在同一处。
|
|
537
962
|
*/
|
|
538
|
-
/** 位置由 id 决定;同一份数据每次打开都在同一处。 */
|
|
539
963
|
const layoutGraph = (nodes, edges, primaryKind) => {
|
|
964
|
+
const kindOf = new Map(nodes.map((node) => [node.id, node.kind]));
|
|
540
965
|
const degree = new Map();
|
|
966
|
+
const linkedOf = new Map();
|
|
967
|
+
const membersOf = new Map();
|
|
968
|
+
const addLink = (primaryId, hubId) => {
|
|
969
|
+
if (!linkedOf.has(primaryId)) linkedOf.set(primaryId, new Set());
|
|
970
|
+
linkedOf.get(primaryId).add(hubId);
|
|
971
|
+
if (!membersOf.has(hubId)) membersOf.set(hubId, new Set());
|
|
972
|
+
membersOf.get(hubId).add(primaryId);
|
|
973
|
+
};
|
|
541
974
|
for (const edge of edges) {
|
|
542
975
|
degree.set(edge.from, (degree.get(edge.from) || 0) + 1);
|
|
543
976
|
degree.set(edge.to, (degree.get(edge.to) || 0) + 1);
|
|
977
|
+
if (kindOf.get(edge.from) === primaryKind && kindOf.get(edge.to) !== primaryKind) addLink(edge.from, edge.to);
|
|
978
|
+
if (kindOf.get(edge.to) === primaryKind && kindOf.get(edge.from) !== primaryKind) addLink(edge.to, edge.from);
|
|
544
979
|
}
|
|
545
|
-
const placed = new Map();
|
|
546
980
|
const primaries = nodes.filter((node) => node.kind === primaryKind);
|
|
547
|
-
const count = primaries.length;
|
|
548
|
-
const spread = 0.55 + Math.min(0.45, count * 0.06);
|
|
549
|
-
primaries.forEach((node, index) => {
|
|
550
|
-
const seed = hashOf(node.id);
|
|
551
|
-
const ring = count <= 1 ? 0 : count <= 8 ? 128 : count <= 18 ? 168 : 206;
|
|
552
|
-
const wobble = count <= 8 ? 1 : 0.64 + ((seed % 100) / 100) * 0.36;
|
|
553
|
-
const angle = count <= 1 ? 0 : (index / count) * Math.PI * 2 - Math.PI / 2 + ((seed % 17) / 17) * 0.16;
|
|
554
|
-
placed.set(node.id, {
|
|
555
|
-
...node,
|
|
556
|
-
x: 320 + Math.cos(angle) * ring * 1.22 * wobble * spread,
|
|
557
|
-
y: 236 + Math.sin(angle) * ring * 0.8 * wobble * spread,
|
|
558
|
-
radius: 5.5 + Math.min(4, (degree.get(node.id) || 0) * 0.7),
|
|
559
|
-
});
|
|
560
|
-
});
|
|
561
981
|
const hubs = nodes.filter((node) => node.kind !== primaryKind);
|
|
562
|
-
const
|
|
563
|
-
const linkedOf = new Map(hubs.map((node) => [node.id, new Set()]));
|
|
564
|
-
for (const edge of edges) {
|
|
565
|
-
if (linkedOf.has(edge.from) && kindOf.get(edge.to) === primaryKind) linkedOf.get(edge.from).add(edge.to);
|
|
566
|
-
if (linkedOf.has(edge.to) && kindOf.get(edge.from) === primaryKind) linkedOf.get(edge.to).add(edge.from);
|
|
567
|
-
}
|
|
982
|
+
const weightOf = new Map(hubs.map((hub) => [hub.id, (membersOf.get(hub.id) || EMPTY).size]));
|
|
568
983
|
const groups = new Map();
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
984
|
+
const assignedHub = new Map();
|
|
985
|
+
const loose = [];
|
|
986
|
+
for (const node of primaries) {
|
|
987
|
+
const options = [...(linkedOf.get(node.id) || EMPTY)]
|
|
988
|
+
.filter((id) => (weightOf.get(id) || 0) > 1)
|
|
989
|
+
.sort((a, b) => ((weightOf.get(b) || 0) - (weightOf.get(a) || 0)) || a.localeCompare(b));
|
|
990
|
+
const hubId = options[0];
|
|
991
|
+
if (!hubId) { loose.push(node); continue; }
|
|
992
|
+
if (!groups.has(hubId)) groups.set(hubId, []);
|
|
993
|
+
groups.get(hubId).push(node);
|
|
994
|
+
assignedHub.set(node.id, hubId);
|
|
995
|
+
}
|
|
996
|
+
const placed = new Map();
|
|
997
|
+
if (primaryKind === 'memory') {
|
|
998
|
+
for (const [hubId, members] of [...groups]) {
|
|
999
|
+
if (members.length > 1) continue;
|
|
1000
|
+
for (const member of members) {
|
|
1001
|
+
loose.push(member);
|
|
1002
|
+
assignedHub.delete(member.id);
|
|
1003
|
+
}
|
|
1004
|
+
groups.delete(hubId);
|
|
1005
|
+
}
|
|
574
1006
|
}
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
1007
|
+
const clusterList = [...groups.entries()]
|
|
1008
|
+
.sort((a, b) => (b[1].length - a[1].length) || a[0].localeCompare(b[0]));
|
|
1009
|
+
const groupCount = clusterList.length;
|
|
1010
|
+
const ring = groupCount <= 1 ? 0 : groupCount <= 4 ? 150 : groupCount <= 8 ? 182 : 212;
|
|
1011
|
+
const hubRadius = (size) => 8 + Math.min(10, Math.sqrt(size) * 3);
|
|
1012
|
+
const placePrimary = (node, x, y) => {
|
|
1013
|
+
placed.set(node.id, {
|
|
1014
|
+
...node, x, y,
|
|
1015
|
+
radius: 3.2 + Math.min(2.2, (degree.get(node.id) || 0) * 0.35),
|
|
1016
|
+
});
|
|
1017
|
+
};
|
|
1018
|
+
clusterList.forEach(([hubId, members], index) => {
|
|
1019
|
+
const seed = hashOf(hubId);
|
|
1020
|
+
const angle = groupCount <= 1
|
|
1021
|
+
? 0
|
|
1022
|
+
: (index / groupCount) * Math.PI * 2 - Math.PI / 2 + ((seed % 13) / 13) * 0.12;
|
|
1023
|
+
const cx = 320 + Math.cos(angle) * ring * 1.22;
|
|
1024
|
+
const cy = 236 + Math.sin(angle) * ring * 0.8;
|
|
1025
|
+
const hub = hubs.find((node) => node.id === hubId);
|
|
1026
|
+
const radius = hubRadius(members.length);
|
|
1027
|
+
placed.set(hubId, { ...hub, x: cx, y: cy, radius });
|
|
1028
|
+
const orbit = radius + 20 + Math.sqrt(members.length) * 13;
|
|
1029
|
+
const start = ((seed % 360) / 180) * Math.PI;
|
|
1030
|
+
members.forEach((node, memberIndex) => {
|
|
1031
|
+
const step = (memberIndex / members.length) * Math.PI * 2;
|
|
1032
|
+
const wobble = 0.88 + ((hashOf(node.id) % 25) / 100);
|
|
1033
|
+
placePrimary(node, cx + Math.cos(start + step) * orbit * wobble, cy + Math.sin(start + step) * orbit * 0.82 * wobble);
|
|
1034
|
+
});
|
|
1035
|
+
});
|
|
1036
|
+
if (loose.length > 0) {
|
|
1037
|
+
const orbit = groupCount === 0
|
|
1038
|
+
? (loose.length <= 1 ? 0 : loose.length <= 8 ? 128 : loose.length <= 18 ? 168 : 206)
|
|
1039
|
+
: ring + 96;
|
|
1040
|
+
loose.forEach((node, index) => {
|
|
581
1041
|
const seed = hashOf(node.id);
|
|
582
|
-
const
|
|
583
|
-
?
|
|
584
|
-
: ((seed %
|
|
585
|
-
const
|
|
586
|
-
|
|
587
|
-
placed.set(node.id, {
|
|
588
|
-
...node,
|
|
589
|
-
x: cx + Math.cos(angle) * distance,
|
|
590
|
-
y: cy + Math.sin(angle) * distance * 0.82,
|
|
591
|
-
radius: node.kind === 'topic' ? 5 : 4,
|
|
592
|
-
});
|
|
1042
|
+
const angle = loose.length <= 1
|
|
1043
|
+
? 0
|
|
1044
|
+
: (index / loose.length) * Math.PI * 2 - Math.PI / 2 + ((seed % 17) / 17) * 0.14;
|
|
1045
|
+
const wobble = loose.length <= 8 ? 1 : 0.64 + ((seed % 100) / 100) * 0.36;
|
|
1046
|
+
placePrimary(node, 320 + Math.cos(angle) * orbit * 1.22 * wobble, 236 + Math.sin(angle) * orbit * 0.8 * wobble);
|
|
593
1047
|
});
|
|
594
1048
|
}
|
|
595
1049
|
return {
|
|
596
1050
|
nodes: [...placed.values()],
|
|
597
|
-
edges: edges.filter((edge) =>
|
|
598
|
-
|
|
1051
|
+
edges: edges.filter((edge) => {
|
|
1052
|
+
if (!placed.has(edge.from) || !placed.has(edge.to)) return false;
|
|
1053
|
+
const fromPrimary = kindOf.get(edge.from) === primaryKind;
|
|
1054
|
+
const toPrimary = kindOf.get(edge.to) === primaryKind;
|
|
1055
|
+
if (fromPrimary && toPrimary) return true;
|
|
1056
|
+
if (primaryKind !== 'memory') return true;
|
|
1057
|
+
if (fromPrimary) return assignedHub.get(edge.from) === edge.to;
|
|
1058
|
+
if (toPrimary) return assignedHub.get(edge.to) === edge.from;
|
|
1059
|
+
return false;
|
|
1060
|
+
}),
|
|
1061
|
+
people: primaries.length,
|
|
599
1062
|
};
|
|
600
1063
|
};
|
|
601
1064
|
|
|
602
|
-
|
|
603
|
-
|
|
1065
|
+
/** 60 条投影之外也要保留当前选中项,点开的节点不能从图里消失。 */
|
|
1066
|
+
const projectionOf = (all, selectedId) => {
|
|
1067
|
+
const rows = all.slice(0, 60);
|
|
1068
|
+
if (selectedId && !rows.some((row) => row.id === selectedId)) {
|
|
1069
|
+
const selected = all.find((row) => row.id === selectedId);
|
|
1070
|
+
if (selected) rows[rows.length - 1] = selected;
|
|
1071
|
+
}
|
|
1072
|
+
return rows;
|
|
1073
|
+
};
|
|
1074
|
+
|
|
1075
|
+
const graphOf = (records, selectedId) => {
|
|
1076
|
+
const all = Array.isArray(records) ? records : EMPTY;
|
|
1077
|
+
const rows = projectionOf(all, selectedId);
|
|
604
1078
|
const byId = new Map(rows.map((record) => [record.id, record]));
|
|
605
1079
|
const tagCount = new Map();
|
|
606
1080
|
const topicCount = new Map();
|
|
@@ -608,7 +1082,7 @@ window.__ModuleLoader__.load({
|
|
|
608
1082
|
for (const tag of Array.isArray(record.tags) ? record.tags : EMPTY) tagCount.set(tag, (tagCount.get(tag) || 0) + 1);
|
|
609
1083
|
if (record.topicKey) topicCount.set(record.topicKey, (topicCount.get(record.topicKey) || 0) + 1);
|
|
610
1084
|
}
|
|
611
|
-
const tags = [...tagCount.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 8);
|
|
1085
|
+
const tags = [...tagCount.entries()].filter(([, count]) => count > 1).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 8);
|
|
612
1086
|
const topics = [...topicCount.entries()].filter(([, count]) => count > 1).sort((a, b) => b[1] - a[1]).slice(0, 4);
|
|
613
1087
|
const nodes = [];
|
|
614
1088
|
const edges = [];
|
|
@@ -636,12 +1110,17 @@ window.__ModuleLoader__.load({
|
|
|
636
1110
|
if (byId.has(link.id)) addEdge(record.id, link.id, 'link');
|
|
637
1111
|
}
|
|
638
1112
|
}
|
|
639
|
-
return
|
|
1113
|
+
return {
|
|
1114
|
+
...layoutGraph(nodes, edges, 'memory'),
|
|
1115
|
+
shown: rows.length,
|
|
1116
|
+
total: all.length,
|
|
1117
|
+
};
|
|
640
1118
|
};
|
|
641
1119
|
|
|
642
1120
|
/** 经验图谱只连共享条件。 */
|
|
643
|
-
const ruleGraphOf = (rules) => {
|
|
644
|
-
const
|
|
1121
|
+
const ruleGraphOf = (rules, selectedId) => {
|
|
1122
|
+
const all = Array.isArray(rules) ? rules : EMPTY;
|
|
1123
|
+
const rows = projectionOf(all, selectedId);
|
|
645
1124
|
const conditionCount = new Map();
|
|
646
1125
|
for (const rule of rows) {
|
|
647
1126
|
const seen = new Set();
|
|
@@ -678,32 +1157,134 @@ window.__ModuleLoader__.load({
|
|
|
678
1157
|
}
|
|
679
1158
|
}
|
|
680
1159
|
}
|
|
681
|
-
return
|
|
1160
|
+
return {
|
|
1161
|
+
...layoutGraph(nodes, edges, 'rule'),
|
|
1162
|
+
shown: rows.length,
|
|
1163
|
+
total: all.length,
|
|
1164
|
+
};
|
|
682
1165
|
};
|
|
683
1166
|
|
|
684
1167
|
/** 关系图只画真实关系;节点沿用原始 id,位置由 id 决定。 */
|
|
1168
|
+
/** 图谱缩放:28px 命中区,悬停/聚焦才给底色。 */
|
|
1169
|
+
const ZoomButton = (props) => {
|
|
1170
|
+
const [hover, setHover] = React.useState(false);
|
|
1171
|
+
return h('button', {
|
|
1172
|
+
type: 'button', onClick: props.onClick, disabled: props.disabled,
|
|
1173
|
+
'aria-label': props.label, title: props.label,
|
|
1174
|
+
onMouseEnter: () => setHover(true),
|
|
1175
|
+
onMouseLeave: () => setHover(false),
|
|
1176
|
+
onFocus: () => setHover(true),
|
|
1177
|
+
onBlur: () => setHover(false),
|
|
1178
|
+
style: {
|
|
1179
|
+
width: '28px', height: '28px', padding: 0, display: 'inline-flex',
|
|
1180
|
+
alignItems: 'center', justifyContent: 'center', border: 'none',
|
|
1181
|
+
borderRadius: '8px', background: hover && !props.disabled ? 'var(--dsw-alias-bg-layer-2)' : 'transparent',
|
|
1182
|
+
color: 'inherit', opacity: props.disabled ? 0.35 : hover ? 0.95 : 0.72,
|
|
1183
|
+
cursor: props.disabled ? 'default' : 'pointer',
|
|
1184
|
+
},
|
|
1185
|
+
}, props.children);
|
|
1186
|
+
};
|
|
1187
|
+
|
|
685
1188
|
const MemoryGraph = (props) => {
|
|
686
1189
|
const primaryKind = props.primaryKind || 'memory';
|
|
687
|
-
const graph = React.useMemo(() => (props.graphOf || graphOf)(props.records), [props.records, props.graphOf]);
|
|
688
|
-
const [zoom, setZoom] = React.useState(1);
|
|
689
1190
|
const selectedId = props.selectedId;
|
|
690
|
-
const
|
|
691
|
-
const
|
|
1191
|
+
const graph = React.useMemo(() => (props.graphOf || graphOf)(props.records, selectedId), [props.records, props.graphOf, selectedId]);
|
|
1192
|
+
const [zoom, setZoom] = React.useState(1);
|
|
1193
|
+
const [pan, setPan] = React.useState({ x: 0, y: 0 });
|
|
1194
|
+
const [dragging, setDragging] = React.useState(false);
|
|
1195
|
+
const [hoveredId, setHoveredId] = React.useState('');
|
|
1196
|
+
const svgRef = React.useRef(null);
|
|
1197
|
+
const dragRef = React.useRef(null);
|
|
1198
|
+
const labelSet = graph.people <= 40;
|
|
1199
|
+
const hubCount = graph.nodes.length - graph.people;
|
|
1200
|
+
const shownCount = Number.isInteger(graph.shown) ? graph.shown : graph.people;
|
|
1201
|
+
const totalCount = Number.isInteger(props.total) && props.total >= shownCount
|
|
1202
|
+
? props.total
|
|
1203
|
+
: (Number.isInteger(graph.total) ? graph.total : shownCount);
|
|
1204
|
+
const truncated = totalCount > shownCount;
|
|
1205
|
+
const nearIds = React.useMemo(() => {
|
|
1206
|
+
const set = new Set();
|
|
1207
|
+
if (!selectedId) return set;
|
|
1208
|
+
set.add(selectedId);
|
|
1209
|
+
for (const edge of graph.edges) {
|
|
1210
|
+
if (edge.from === selectedId) set.add(edge.to);
|
|
1211
|
+
if (edge.to === selectedId) set.add(edge.from);
|
|
1212
|
+
}
|
|
1213
|
+
return set;
|
|
1214
|
+
}, [graph.edges, selectedId]);
|
|
1215
|
+
const orderedEdges = selectedId
|
|
1216
|
+
? [...graph.edges].sort((a, b) => Number(a.from === selectedId || a.to === selectedId) - Number(b.from === selectedId || b.to === selectedId))
|
|
1217
|
+
: graph.edges;
|
|
1218
|
+
const orderedNodes = selectedId
|
|
1219
|
+
? [...graph.nodes].sort((a, b) => (Number(nearIds.has(a.id)) - Number(nearIds.has(b.id))) || (Number(a.id === selectedId) - Number(b.id === selectedId)))
|
|
1220
|
+
: graph.nodes;
|
|
1221
|
+
const pickHub = props.onPickHub;
|
|
1222
|
+
const MIN_ZOOM = 0.2;
|
|
1223
|
+
const MAX_ZOOM = 2.5;
|
|
1224
|
+
const stepZoom = (delta) => setZoom((value) => {
|
|
1225
|
+
const next = delta > 0 ? value * 1.25 : value / 1.25;
|
|
1226
|
+
return Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, Math.round(next * 100) / 100));
|
|
1227
|
+
});
|
|
1228
|
+
const pointAt = (event, matrix) => {
|
|
1229
|
+
const svg = svgRef.current;
|
|
1230
|
+
if (!svg || !matrix) return null;
|
|
1231
|
+
const point = svg.createSVGPoint();
|
|
1232
|
+
point.x = event.clientX;
|
|
1233
|
+
point.y = event.clientY;
|
|
1234
|
+
return point.matrixTransform(matrix);
|
|
1235
|
+
};
|
|
1236
|
+
const startPan = (event) => {
|
|
1237
|
+
if (event.button !== 0) return;
|
|
1238
|
+
if (event.target && event.target.closest && event.target.closest('[data-node]')) return;
|
|
1239
|
+
const svg = svgRef.current;
|
|
1240
|
+
if (!svg) return;
|
|
1241
|
+
const ctm = svg.getScreenCTM();
|
|
1242
|
+
if (!ctm) return;
|
|
1243
|
+
const inverse = ctm.inverse();
|
|
1244
|
+
const point = pointAt(event, inverse);
|
|
1245
|
+
if (!point) return;
|
|
1246
|
+
dragRef.current = [event.pointerId, inverse, point.x, point.y, pan.x, pan.y];
|
|
1247
|
+
setDragging(true);
|
|
1248
|
+
if (svg.setPointerCapture) svg.setPointerCapture(event.pointerId);
|
|
1249
|
+
event.preventDefault();
|
|
1250
|
+
};
|
|
1251
|
+
const movePan = (event) => {
|
|
1252
|
+
const drag = dragRef.current;
|
|
1253
|
+
if (!drag || drag[0] !== event.pointerId) return;
|
|
1254
|
+
const point = pointAt(event, drag[1]);
|
|
1255
|
+
if (!point) return;
|
|
1256
|
+
setPan({ x: drag[4] - (point.x - drag[2]), y: drag[5] - (point.y - drag[3]) });
|
|
1257
|
+
event.preventDefault();
|
|
1258
|
+
};
|
|
1259
|
+
const endPan = (event) => {
|
|
1260
|
+
const drag = dragRef.current;
|
|
1261
|
+
if (!drag || (event && drag[0] !== event.pointerId)) return;
|
|
1262
|
+
dragRef.current = null;
|
|
1263
|
+
setDragging(false);
|
|
1264
|
+
const svg = svgRef.current;
|
|
1265
|
+
if (event && svg && svg.releasePointerCapture && svg.hasPointerCapture && svg.hasPointerCapture(event.pointerId)) {
|
|
1266
|
+
svg.releasePointerCapture(event.pointerId);
|
|
1267
|
+
}
|
|
1268
|
+
};
|
|
1269
|
+
React.useEffect(() => {
|
|
1270
|
+
setPan({ x: 0, y: 0 });
|
|
1271
|
+
}, [props.records, primaryKind]);
|
|
692
1272
|
const frame = React.useMemo(() => {
|
|
693
1273
|
const list = graph.nodes;
|
|
694
1274
|
if (list.length === 0) return { x: 0, y: 0, width: 640, height: 472 };
|
|
695
1275
|
let minX = Infinity; let maxX = -Infinity; let minY = Infinity; let maxY = -Infinity;
|
|
696
1276
|
for (const node of list) {
|
|
697
|
-
const
|
|
1277
|
+
const wide = node.kind === primaryKind && (graph.people <= 12 || hubCount === 0 || node.id === selectedId);
|
|
1278
|
+
const half = node.kind === primaryKind ? (wide ? 62 : 54) : 38;
|
|
698
1279
|
minX = Math.min(minX, node.x - half);
|
|
699
1280
|
maxX = Math.max(maxX, node.x + half);
|
|
700
1281
|
minY = Math.min(minY, node.y - 16);
|
|
701
|
-
maxY = Math.max(maxY, node.y + (node.kind === primaryKind ? 28 :
|
|
1282
|
+
maxY = Math.max(maxY, node.y + (node.kind === primaryKind ? (wide ? 30 : 28) : 26));
|
|
702
1283
|
}
|
|
703
1284
|
const width = Math.max(380, maxX - minX + 36);
|
|
704
1285
|
const height = Math.max(250, maxY - minY + 36);
|
|
705
1286
|
return { x: (minX + maxX) / 2 - width / 2, y: (minY + maxY) / 2 - height / 2, width, height };
|
|
706
|
-
}, [graph, primaryKind]);
|
|
1287
|
+
}, [graph, primaryKind, selectedId]);
|
|
707
1288
|
if (!props.records || props.records.length === 0) {
|
|
708
1289
|
return h('div', {
|
|
709
1290
|
style: { flex: 1, minHeight: '220px', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--dsw-alias-label-secondary)', fontSize: '12px' },
|
|
@@ -711,80 +1292,184 @@ window.__ModuleLoader__.load({
|
|
|
711
1292
|
}
|
|
712
1293
|
const viewWidth = frame.width / zoom;
|
|
713
1294
|
const viewHeight = frame.height / zoom;
|
|
714
|
-
const zoomButton = (label, delta, icon) => h(
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
'aria-label': label, title: label,
|
|
718
|
-
style: {
|
|
719
|
-
width: '26px', height: '26px', padding: 0, display: 'inline-flex',
|
|
720
|
-
alignItems: 'center', justifyContent: 'center', border: 'none',
|
|
721
|
-
borderRadius: '6px', background: 'transparent', color: 'inherit',
|
|
722
|
-
opacity: delta > 0 ? (zoom >= 2.5 ? 0.28 : 0.68) : (zoom <= 0.75 ? 0.28 : 0.68),
|
|
723
|
-
cursor: 'pointer',
|
|
724
|
-
},
|
|
1295
|
+
const zoomButton = (label, delta, icon) => h(ZoomButton, {
|
|
1296
|
+
key: label, label, disabled: delta > 0 ? zoom >= MAX_ZOOM : zoom <= MIN_ZOOM,
|
|
1297
|
+
onClick: () => stepZoom(delta),
|
|
725
1298
|
}, icon);
|
|
726
1299
|
const magnifier = (sign) => h('svg', {
|
|
727
1300
|
width: '15', height: '15', viewBox: '0 0 16 16', fill: 'none',
|
|
728
|
-
stroke: 'currentColor',
|
|
1301
|
+
stroke: 'currentColor', 'stroke-width': '1.4', 'stroke-linecap': 'round',
|
|
729
1302
|
'aria-hidden': 'true',
|
|
730
1303
|
},
|
|
731
1304
|
h('circle', { cx: '6.8', cy: '6.8', r: '4.5' }),
|
|
732
1305
|
h('line', { x1: '10.1', y1: '10.1', x2: '14', y2: '14' }),
|
|
733
1306
|
h('line', { x1: '4.8', y1: '6.8', x2: '8.8', y2: '6.8' }),
|
|
734
1307
|
sign > 0 ? h('line', { x1: '6.8', y1: '4.8', x2: '6.8', y2: '8.8' }) : null);
|
|
1308
|
+
/** 标签避让:hub 常显、选中/悬停优先,记忆标签按度数贪心排布,重叠就省掉(悬停仍出标题)。 */
|
|
1309
|
+
const labelLayout = React.useMemo(() => {
|
|
1310
|
+
const nodes = graph.nodes;
|
|
1311
|
+
const fsOf = (primary) => (primary ? 10.5 : 10);
|
|
1312
|
+
const widthOf = (text, fs) => {
|
|
1313
|
+
let width = 0;
|
|
1314
|
+
for (const ch of text) width += ch.charCodeAt(0) > 255 ? fs : fs * 0.56;
|
|
1315
|
+
return width;
|
|
1316
|
+
};
|
|
1317
|
+
const rank = (node) => (node.id === selectedId ? 0 : node.id === hoveredId ? 1
|
|
1318
|
+
: nearIds.has(node.id) ? 2 : node.kind === primaryKind ? 4 : 3);
|
|
1319
|
+
const boxes = [];
|
|
1320
|
+
const placed = new Map();
|
|
1321
|
+
const hits = (box) => boxes.some((other) => !(box.x2 < other.x1 || box.x1 > other.x2 || box.y2 < other.y1 || box.y1 > other.y2));
|
|
1322
|
+
const list = nodes.slice().sort((a, b) => rank(a) - rank(b) || (b.degree || 0) - (a.degree || 0));
|
|
1323
|
+
/** 条目多时不全标,用度数最高的十条记忆给方向感。 */
|
|
1324
|
+
const topIds = new Set(list.filter((node) => node.kind === primaryKind).slice(0, 10).map((node) => node.id));
|
|
1325
|
+
for (const node of list) {
|
|
1326
|
+
const isPrimary = node.kind === primaryKind;
|
|
1327
|
+
if (isPrimary && !labelSet && rank(node) > 2 && !topIds.has(node.id)) continue;
|
|
1328
|
+
const chars = rank(node) <= 1 ? 16 : isPrimary ? (labelSet ? 10 : 8) : 13;
|
|
1329
|
+
const text = SHORT(node.label, chars);
|
|
1330
|
+
const fs = fsOf(isPrimary);
|
|
1331
|
+
const width = widthOf(text, fs);
|
|
1332
|
+
const gap = isPrimary ? 12 : 13;
|
|
1333
|
+
let spot = null;
|
|
1334
|
+
for (const below of [true, false]) {
|
|
1335
|
+
const baseline = below ? node.y + node.radius + gap : node.y - node.radius - gap + fs;
|
|
1336
|
+
const box = { x1: node.x - width / 2 - 3, x2: node.x + width / 2 + 3, y1: baseline - fs - 2, y2: baseline + 3 };
|
|
1337
|
+
if (box.y1 < 3 || box.y2 > graph.height - 3) continue;
|
|
1338
|
+
if (hits(box)) continue;
|
|
1339
|
+
spot = { box, baseline, below };
|
|
1340
|
+
break;
|
|
1341
|
+
}
|
|
1342
|
+
if (!spot) continue;
|
|
1343
|
+
boxes.push(spot.box);
|
|
1344
|
+
placed.set(node.id, { text, isPrimary, baseline: spot.baseline, fs, width });
|
|
1345
|
+
}
|
|
1346
|
+
return placed;
|
|
1347
|
+
}, [graph, primaryKind, selectedId, hoveredId, labelSet, nearIds]);
|
|
735
1348
|
return h('div', { style: { position: 'relative', flex: 1, minHeight: 0, display: 'flex' } },
|
|
736
1349
|
h('div', {
|
|
737
1350
|
style: {
|
|
738
|
-
position: 'absolute', top: '
|
|
739
|
-
display: 'flex', alignItems: 'center', gap: '
|
|
1351
|
+
position: 'absolute', top: '8px', right: '8px', zIndex: 2,
|
|
1352
|
+
display: 'flex', alignItems: 'center', gap: '2px', padding: '2px',
|
|
740
1353
|
border: '1px solid var(--dsw-alias-border-l1)', borderRadius: '8px',
|
|
741
1354
|
background: 'var(--dsw-alias-bg-layer-1)',
|
|
742
1355
|
},
|
|
743
1356
|
},
|
|
744
1357
|
zoomButton('缩小图谱', -0.25, magnifier(-1)),
|
|
745
|
-
zoomButton('放大图谱', 0.25, magnifier(1))
|
|
1358
|
+
zoomButton('放大图谱', 0.25, magnifier(1)),
|
|
1359
|
+
h('button', {
|
|
1360
|
+
type: 'button', onClick: () => setZoom(1),
|
|
1361
|
+
'aria-label': '重置缩放', title: '重置缩放',
|
|
1362
|
+
style: {
|
|
1363
|
+
minWidth: '44px', height: '28px', padding: '0 6px', border: 'none', borderRadius: '8px',
|
|
1364
|
+
background: 'transparent', color: 'var(--dsw-alias-label-secondary)',
|
|
1365
|
+
fontSize: '11px', lineHeight: '28px', fontVariantNumeric: 'tabular-nums', cursor: 'pointer',
|
|
1366
|
+
},
|
|
1367
|
+
}, `${Math.round(zoom * 100)}%`)),
|
|
746
1368
|
h('svg', {
|
|
747
|
-
|
|
1369
|
+
ref: svgRef,
|
|
1370
|
+
viewBox: `${frame.x + frame.width / 2 - viewWidth / 2 + pan.x} ${frame.y + frame.height / 2 - viewHeight / 2 + pan.y} ${viewWidth} ${viewHeight}`,
|
|
748
1371
|
preserveAspectRatio: 'xMidYMid meet',
|
|
749
|
-
style: {
|
|
1372
|
+
style: {
|
|
1373
|
+
width: '100%', height: '100%', flex: 1, minHeight: 0, display: 'block',
|
|
1374
|
+
cursor: dragging ? 'grabbing' : 'grab', touchAction: 'none',
|
|
1375
|
+
userSelect: 'none', WebkitUserSelect: 'none',
|
|
1376
|
+
},
|
|
1377
|
+
onPointerDown: startPan,
|
|
1378
|
+
onPointerMove: movePan,
|
|
1379
|
+
onPointerUp: endPan,
|
|
1380
|
+
onPointerCancel: endPan,
|
|
1381
|
+
onLostPointerCapture: endPan,
|
|
1382
|
+
onDragStart: (event) => event.preventDefault(),
|
|
750
1383
|
role: 'img', 'aria-label': props.ariaLabel || '关系图',
|
|
751
1384
|
},
|
|
752
|
-
h('g', null,
|
|
1385
|
+
h('g', null, orderedEdges.map((edge, index) => {
|
|
753
1386
|
const from = graph.nodes.find((node) => node.id === edge.from);
|
|
754
1387
|
const to = graph.nodes.find((node) => node.id === edge.to);
|
|
755
1388
|
if (!from || !to) return null;
|
|
756
1389
|
const linked = selectedId && (edge.from === selectedId || edge.to === selectedId);
|
|
1390
|
+
const dimmed = selectedId && !nearIds.has(edge.from) && !nearIds.has(edge.to);
|
|
757
1391
|
return h('line', {
|
|
758
1392
|
key: `e${index}`, x1: from.x, y1: from.y, x2: to.x, y2: to.y,
|
|
759
1393
|
stroke: linked ? 'var(--dsw-alias-brand-primary)' : 'var(--dsw-alias-label-secondary)',
|
|
760
|
-
|
|
761
|
-
opacity: linked ? 0.
|
|
762
|
-
|
|
1394
|
+
'stroke-width': linked ? 1.4 : 1,
|
|
1395
|
+
opacity: linked ? 0.72 : dimmed ? 0.08 : edge.type === 'link' ? 0.38 : 0.16,
|
|
1396
|
+
'stroke-dasharray': edge.type === 'link' ? undefined : '2 4',
|
|
763
1397
|
});
|
|
764
1398
|
})),
|
|
765
|
-
h('g', null,
|
|
1399
|
+
h('g', null, orderedNodes.map((node) => {
|
|
766
1400
|
const isPrimary = node.kind === primaryKind;
|
|
767
1401
|
const active = node.id === selectedId;
|
|
768
|
-
const
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
const
|
|
1402
|
+
const hovered = node.id === hoveredId;
|
|
1403
|
+
const near = nearIds.has(node.id);
|
|
1404
|
+
const dimmed = Boolean(selectedId) && !near;
|
|
1405
|
+
const pick = isPrimary ? props.onPick : (pickHub || undefined);
|
|
1406
|
+
/** 记忆是主体:实心、可读;主题/标签 hub 是背景:空心、只留轮廓。 */
|
|
1407
|
+
const fill = active ? 'var(--dsw-alias-brand-primary)'
|
|
1408
|
+
: isPrimary ? 'var(--dsw-alias-label-secondary)'
|
|
1409
|
+
: 'none';
|
|
1410
|
+
const stroke = active ? 'none'
|
|
1411
|
+
: hovered || near ? 'var(--dsw-alias-brand-primary)'
|
|
1412
|
+
: node.experimental ? 'var(--dsw-alias-state-warn-primary)'
|
|
1413
|
+
: isPrimary ? 'none' : 'var(--dsw-alias-label-secondary)';
|
|
1414
|
+
const strokeWidth = !active && (hovered || near) ? 1.3 : node.experimental ? 1 : isPrimary ? 0 : 1;
|
|
1415
|
+
const label = labelLayout.get(node.id) || null;
|
|
772
1416
|
return h('g', {
|
|
773
1417
|
key: node.id,
|
|
1418
|
+
'data-node': 'true',
|
|
774
1419
|
transform: `translate(${node.x} ${node.y})`,
|
|
775
|
-
onClick:
|
|
776
|
-
|
|
1420
|
+
onClick: pick ? () => pick(node) : undefined,
|
|
1421
|
+
onKeyDown: pick ? (event) => {
|
|
1422
|
+
if (event.key === 'Enter' || event.key === ' ' || event.key === 'Spacebar') {
|
|
1423
|
+
event.preventDefault();
|
|
1424
|
+
pick(node);
|
|
1425
|
+
}
|
|
1426
|
+
} : undefined,
|
|
1427
|
+
onFocus: () => setHoveredId(node.id),
|
|
1428
|
+
onBlur: () => setHoveredId((value) => (value === node.id ? '' : value)),
|
|
1429
|
+
onMouseEnter: () => setHoveredId(node.id),
|
|
1430
|
+
onMouseLeave: () => setHoveredId((value) => (value === node.id ? '' : value)),
|
|
1431
|
+
tabIndex: pick ? 0 : undefined,
|
|
1432
|
+
role: pick ? 'button' : undefined,
|
|
1433
|
+
focusable: pick ? 'true' : 'false',
|
|
1434
|
+
'aria-label': pick ? (node.title || node.label) : undefined,
|
|
1435
|
+
style: { cursor: pick ? 'pointer' : 'default', outline: 'none' },
|
|
777
1436
|
},
|
|
778
|
-
|
|
779
|
-
h('circle', { r: node.radius, fill,
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
}
|
|
1437
|
+
h('circle', { r: Math.max(isPrimary ? 14 : 18, node.radius + 6), fill: 'transparent' }),
|
|
1438
|
+
active ? h('circle', { r: node.radius + 5, fill: 'none', stroke: 'var(--dsw-alias-brand-primary)', 'stroke-width': 1.4, opacity: 0.45 }) : null,
|
|
1439
|
+
h('circle', {
|
|
1440
|
+
r: node.radius, fill,
|
|
1441
|
+
opacity: active ? 1 : dimmed ? (isPrimary ? 0.32 : 0.14) : isPrimary ? 0.92 : 0.34,
|
|
1442
|
+
stroke, 'stroke-width': strokeWidth,
|
|
1443
|
+
}),
|
|
1444
|
+
label ? h('text', {
|
|
1445
|
+
y: label.baseline, 'text-anchor': 'middle',
|
|
1446
|
+
fill: label.isPrimary ? 'var(--dsw-alias-label-primary)' : 'var(--dsw-alias-label-secondary)',
|
|
1447
|
+
'font-size': label.fs,
|
|
1448
|
+
'font-weight': 500,
|
|
1449
|
+
opacity: active ? 1 : dimmed ? 0.2 : label.isPrimary ? 0.9 : 0.72,
|
|
1450
|
+
stroke: 'var(--dsw-alias-bg-layer-1)', 'stroke-width': 2.6, 'stroke-linejoin': 'round', 'paint-order': 'stroke',
|
|
1451
|
+
}, label.text) : null,
|
|
785
1452
|
h('title', null, node.title || node.label),
|
|
786
1453
|
);
|
|
787
1454
|
}))),
|
|
1455
|
+
h('div', {
|
|
1456
|
+
style: {
|
|
1457
|
+
position: 'absolute', left: '10px', bottom: '8px', zIndex: 1,
|
|
1458
|
+
maxWidth: truncated ? 'calc(100% - 178px)' : 'calc(100% - 60px)', padding: '2px 6px', borderRadius: '6px',
|
|
1459
|
+
background: 'var(--dsw-alias-bg-layer-1)',
|
|
1460
|
+
color: 'var(--dsw-alias-label-secondary)', fontSize: '11px', lineHeight: '1.5',
|
|
1461
|
+
pointerEvents: 'none',
|
|
1462
|
+
},
|
|
1463
|
+
}, props.legend || '实心 记忆 · 空心 标签/主题 · 虚线 归类'),
|
|
1464
|
+
truncated ? h('div', {
|
|
1465
|
+
style: {
|
|
1466
|
+
position: 'absolute', right: '10px', bottom: '8px', zIndex: 1,
|
|
1467
|
+
padding: '2px 6px', borderRadius: '6px',
|
|
1468
|
+
background: 'var(--dsw-alias-bg-layer-1)',
|
|
1469
|
+
color: 'var(--dsw-alias-label-secondary)', fontSize: '11px', lineHeight: '1.5',
|
|
1470
|
+
pointerEvents: 'none', whiteSpace: 'nowrap',
|
|
1471
|
+
},
|
|
1472
|
+
}, `显示 ${shownCount} / 共 ${totalCount} 条`) : null,
|
|
788
1473
|
);
|
|
789
1474
|
};
|
|
790
1475
|
|
|
@@ -801,7 +1486,7 @@ window.__ModuleLoader__.load({
|
|
|
801
1486
|
style: {
|
|
802
1487
|
position: 'absolute', top: '2px', left: props.on ? '18px' : '2px', width: '16px', height: '16px',
|
|
803
1488
|
borderRadius: '999px', background: props.on ? 'var(--dsw-alias-bg-base)' : 'var(--dsw-alias-label-secondary)',
|
|
804
|
-
transition: 'left 120ms ease',
|
|
1489
|
+
transition: reduceMotionOf() ? 'none' : 'left 120ms ease',
|
|
805
1490
|
},
|
|
806
1491
|
}));
|
|
807
1492
|
|
|
@@ -871,22 +1556,41 @@ window.__ModuleLoader__.load({
|
|
|
871
1556
|
const evidenceTextOf = (row) => {
|
|
872
1557
|
if (typeof row === 'string') return row;
|
|
873
1558
|
if (!row || typeof row !== 'object') return '';
|
|
874
|
-
|
|
1559
|
+
for (const key of ['sourceExcerpt', 'excerpt', 'text', 'note']) {
|
|
1560
|
+
if (typeof row[key] === 'string' && row[key].trim()) return row[key];
|
|
1561
|
+
}
|
|
1562
|
+
return '';
|
|
1563
|
+
};
|
|
1564
|
+
/** 样例和证据都带版本/核对信息,不拿「样例」二字占位。 */
|
|
1565
|
+
const evidenceMetaOf = (row) => {
|
|
1566
|
+
if (!row || typeof row !== 'object') return '';
|
|
1567
|
+
const bits = [];
|
|
1568
|
+
if (Number.isInteger(row.artifactVersion)) bits.push(`第 ${row.artifactVersion} 版`);
|
|
1569
|
+
else if (typeof row.artifactVersion === 'string' && row.artifactVersion) bits.push(`第 ${row.artifactVersion} 版`);
|
|
1570
|
+
if (typeof row.dimension === 'string' && row.dimension) bits.push(row.dimension);
|
|
1571
|
+
if (row.verified === true) bits.push('已核对');
|
|
1572
|
+
else if (row.verified === false) bits.push('未核对');
|
|
1573
|
+
if (row.at) bits.push(EVO_DATE(row.at));
|
|
1574
|
+
return bits.join(' · ');
|
|
875
1575
|
};
|
|
876
1576
|
|
|
877
1577
|
const MemoryRow = (props) => {
|
|
878
1578
|
const record = props.record;
|
|
1579
|
+
const [hover, setHover] = React.useState(false);
|
|
879
1580
|
return h('button', {
|
|
880
1581
|
type: 'button', onClick: props.onClick,
|
|
1582
|
+
onMouseEnter: () => setHover(true),
|
|
1583
|
+
onMouseLeave: () => setHover(false),
|
|
881
1584
|
style: {
|
|
882
|
-
display: 'flex', flexDirection: 'column', gap:
|
|
883
|
-
border: 'none', borderBottom: '1px solid var(--dsw-alias-border-l1)',
|
|
884
|
-
|
|
885
|
-
|
|
1585
|
+
display: 'flex', flexDirection: 'column', gap: 0, width: '100%', textAlign: 'left',
|
|
1586
|
+
border: 'none', borderBottom: '1px solid var(--dsw-alias-border-l1)', borderRadius: '6px',
|
|
1587
|
+
borderLeft: `2px solid ${props.active ? 'var(--dsw-alias-brand-primary)' : 'transparent'}`,
|
|
1588
|
+
background: props.active || hover ? 'var(--dsw-alias-bg-layer-2)' : 'transparent',
|
|
1589
|
+
color: 'inherit', padding: '4px 8px', cursor: 'pointer', minWidth: 0,
|
|
886
1590
|
},
|
|
887
1591
|
},
|
|
888
|
-
h('span', { style: { fontSize: '12px', lineHeight: '
|
|
889
|
-
h('span', { style: { ...PANEL_MUTED, display: 'flex', alignItems: 'center', gap: '5px', minWidth: 0 } },
|
|
1592
|
+
h('span', { style: { fontSize: '12px', lineHeight: '16px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, record.title || record.id),
|
|
1593
|
+
h('span', { style: { ...PANEL_MUTED, display: 'flex', alignItems: 'center', gap: '5px', minWidth: 0, lineHeight: '14px' } },
|
|
890
1594
|
h('span', null, KIND_LABEL[record.kind] || record.kind),
|
|
891
1595
|
record.experimental ? h('span', { style: { color: 'var(--dsw-alias-state-warn-primary)' } }, '实验') : null,
|
|
892
1596
|
h('span', { style: { marginLeft: 'auto', flex: '0 0 auto', fontVariantNumeric: 'tabular-nums' } }, EVO_DATE(record.updated)),
|
|
@@ -894,6 +1598,29 @@ window.__ModuleLoader__.load({
|
|
|
894
1598
|
);
|
|
895
1599
|
};
|
|
896
1600
|
|
|
1601
|
+
/** 一条经验:状态用小标签,标题独占一行。 */
|
|
1602
|
+
const EvolutionRow = (props) => {
|
|
1603
|
+
const rule = props.rule;
|
|
1604
|
+
const [hover, setHover] = React.useState(false);
|
|
1605
|
+
return h('button', {
|
|
1606
|
+
type: 'button', onClick: props.onClick,
|
|
1607
|
+
onMouseEnter: () => setHover(true),
|
|
1608
|
+
onMouseLeave: () => setHover(false),
|
|
1609
|
+
style: {
|
|
1610
|
+
display: 'flex', flexDirection: 'column', gap: '1px', width: '100%', textAlign: 'left',
|
|
1611
|
+
border: 'none', borderRadius: '8px', minWidth: 0,
|
|
1612
|
+
borderLeft: `2px solid ${props.active ? 'var(--dsw-alias-brand-primary)' : 'transparent'}`,
|
|
1613
|
+
background: props.active || hover ? 'var(--dsw-alias-bg-layer-2)' : 'transparent',
|
|
1614
|
+
color: 'inherit', padding: '6px 9px', cursor: 'pointer',
|
|
1615
|
+
},
|
|
1616
|
+
},
|
|
1617
|
+
h('span', { style: { fontSize: '12px', lineHeight: '18px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, rule.title),
|
|
1618
|
+
h('span', { style: { ...PANEL_MUTED, display: 'flex', alignItems: 'center', gap: '6px', minWidth: 0, lineHeight: '16px' } },
|
|
1619
|
+
h(StagePill, { state: rule.state }),
|
|
1620
|
+
h('span', { style: { marginLeft: 'auto', flex: '0 0 auto', fontVariantNumeric: 'tabular-nums' } }, EVO_DATE(rule.updated)),
|
|
1621
|
+
));
|
|
1622
|
+
};
|
|
1623
|
+
|
|
897
1624
|
const EvolutionDetail = (props) => {
|
|
898
1625
|
const rule = props.rule;
|
|
899
1626
|
const ruleId = rule ? rule.id : '';
|
|
@@ -916,8 +1643,12 @@ window.__ModuleLoader__.load({
|
|
|
916
1643
|
const versions = Array.isArray(rule.learning?.versions) ? rule.learning.versions : EMPTY;
|
|
917
1644
|
const history = [
|
|
918
1645
|
...checkpoints.map((row) => ({ at: row.at || row.updated || '', text: typeof row.text === 'string' ? row.text : '' })),
|
|
919
|
-
...versions.map((row) => ({
|
|
1646
|
+
...versions.map((row, index) => ({
|
|
1647
|
+
at: row.at || row.updated || '',
|
|
1648
|
+
text: `第 ${versions.length - index} 版${row.body ? ` ${SHORT(row.body, 42)}` : row.digest ? ` ${SHORT(String(row.digest), 16)}` : ''}`,
|
|
1649
|
+
})),
|
|
920
1650
|
].filter((row) => row.text).sort((a, b) => String(b.at).localeCompare(String(a.at))).slice(0, 6);
|
|
1651
|
+
const failureState = failureStateOf(props.evolution);
|
|
921
1652
|
const canManage = props.canManage && !props.busy;
|
|
922
1653
|
const act = (name, extra) => { if (canManage) props.onManage(name, extra || {}); };
|
|
923
1654
|
const actions = props.canManage
|
|
@@ -969,8 +1700,24 @@ window.__ModuleLoader__.load({
|
|
|
969
1700
|
evidence.length === 0 && samples.length === 0
|
|
970
1701
|
? '暂无学习依据'
|
|
971
1702
|
: [
|
|
972
|
-
...evidence.slice(0, 3).map((row, index) =>
|
|
973
|
-
|
|
1703
|
+
...evidence.slice(0, 3).map((row, index) => {
|
|
1704
|
+
const text = evidenceTextOf(row);
|
|
1705
|
+
const meta = evidenceMetaOf(row);
|
|
1706
|
+
return text
|
|
1707
|
+
? h('div', { key: `e${index}`, style: { display: 'flex', flexDirection: 'column', gap: '1px' } },
|
|
1708
|
+
h('span', { style: { color: 'var(--dsw-alias-label-primary)', opacity: 0.8, whiteSpace: 'pre-wrap', wordBreak: 'break-word' } }, text),
|
|
1709
|
+
meta ? h('span', null, meta) : null)
|
|
1710
|
+
: null;
|
|
1711
|
+
}),
|
|
1712
|
+
...samples.slice(0, 2).map((row, index) => {
|
|
1713
|
+
const text = evidenceTextOf(row);
|
|
1714
|
+
const meta = evidenceMetaOf(row);
|
|
1715
|
+
return text || meta
|
|
1716
|
+
? h('div', { key: `s${index}`, style: { display: 'flex', flexDirection: 'column', gap: '1px' } },
|
|
1717
|
+
text ? h('span', { style: { whiteSpace: 'pre-wrap', wordBreak: 'break-word' } }, text) : null,
|
|
1718
|
+
meta ? h('span', null, meta) : null)
|
|
1719
|
+
: null;
|
|
1720
|
+
}),
|
|
974
1721
|
].filter(Boolean))
|
|
975
1722
|
: null;
|
|
976
1723
|
const historyPane = open === 'history'
|
|
@@ -998,6 +1745,10 @@ window.__ModuleLoader__.load({
|
|
|
998
1745
|
h(StagePill, { state: rule.state }),
|
|
999
1746
|
),
|
|
1000
1747
|
h('div', { style: { fontSize: '12px', lineHeight: '1.55', opacity: 0.82 } }, usabilityOf(rule, props.evolution, props.memoryOn)),
|
|
1748
|
+
failureState
|
|
1749
|
+
? h('div', { style: { ...PANEL_MUTED, color: failureState === 'current' ? 'var(--dsw-alias-state-warn-primary)' : undefined } },
|
|
1750
|
+
failureState === 'current' ? '最近一次整理失败,现有经验未更新' : '以前有整理失败,最近一次已完成')
|
|
1751
|
+
: null,
|
|
1001
1752
|
actions,
|
|
1002
1753
|
),
|
|
1003
1754
|
editor,
|
|
@@ -1010,6 +1761,178 @@ window.__ModuleLoader__.load({
|
|
|
1010
1761
|
);
|
|
1011
1762
|
};
|
|
1012
1763
|
|
|
1764
|
+
|
|
1765
|
+
const PalaceSlot = (props) => {
|
|
1766
|
+
const record = props.record;
|
|
1767
|
+
if (!record) {
|
|
1768
|
+
return h('div', {
|
|
1769
|
+
'aria-hidden': 'true',
|
|
1770
|
+
style: { height: '46px', border: '1px dashed var(--dsw-alias-border-l1)', borderRadius: '8px', opacity: 0.28 },
|
|
1771
|
+
});
|
|
1772
|
+
}
|
|
1773
|
+
const title = String(record.title || record.id || '记忆');
|
|
1774
|
+
return h('button', {
|
|
1775
|
+
type: 'button', title, 'aria-label': title,
|
|
1776
|
+
onClick: () => props.onPick(record.id),
|
|
1777
|
+
style: {
|
|
1778
|
+
height: '46px', minWidth: 0, display: 'flex', flexDirection: 'column', justifyContent: 'center',
|
|
1779
|
+
padding: '4px 7px', textAlign: 'left', cursor: 'pointer',
|
|
1780
|
+
border: `1px solid ${props.active ? 'var(--dsw-alias-brand-primary)' : 'var(--dsw-alias-border-l1)'}`,
|
|
1781
|
+
borderRadius: '8px', color: 'inherit',
|
|
1782
|
+
background: props.active ? 'var(--dsw-alias-bg-layer-2)' : 'var(--dsw-alias-bg-base)',
|
|
1783
|
+
},
|
|
1784
|
+
},
|
|
1785
|
+
h('span', {
|
|
1786
|
+
style: {
|
|
1787
|
+
fontSize: '11px', lineHeight: '15px', height: '30px', overflow: 'hidden',
|
|
1788
|
+
display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', wordBreak: 'break-word',
|
|
1789
|
+
},
|
|
1790
|
+
}, title),
|
|
1791
|
+
);
|
|
1792
|
+
};
|
|
1793
|
+
|
|
1794
|
+
const PalaceRoomCard = (props) => {
|
|
1795
|
+
const room = props.room;
|
|
1796
|
+
const empty = room.total === 0;
|
|
1797
|
+
const preview = room.items.slice(0, PALACE_PREVIEW);
|
|
1798
|
+
const slots = Array.from({ length: PALACE_PREVIEW }, (_, index) => preview[index] || null);
|
|
1799
|
+
return h('div', {
|
|
1800
|
+
role: 'listitem',
|
|
1801
|
+
style: { ...PANEL_CARD, padding: 0, position: 'relative', overflow: 'hidden', display: 'flex', width: '100%', flexDirection: 'column' },
|
|
1802
|
+
},
|
|
1803
|
+
h('div', { style: { position: 'relative', display: 'flex', flexDirection: 'column', gap: '8px', padding: '9px 10px 10px', boxSizing: 'border-box' } },
|
|
1804
|
+
h('button', {
|
|
1805
|
+
type: 'button', 'aria-expanded': empty ? undefined : props.expanded, 'aria-controls': empty ? undefined : `lite-palace-room-${room.id}`,
|
|
1806
|
+
disabled: empty,
|
|
1807
|
+
onClick: () => { if (!empty) props.onToggle(room.id); },
|
|
1808
|
+
style: { ...PANEL_LINK, width: '100%', display: 'flex', alignItems: 'center', gap: '7px', textAlign: 'left', opacity: 1 },
|
|
1809
|
+
},
|
|
1810
|
+
h('span', { style: { fontSize: '12px', lineHeight: '18px', fontWeight: 600, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, room.name),
|
|
1811
|
+
h('span', { style: { ...PANEL_MUTED, flex: '0 0 auto', fontVariantNumeric: 'tabular-nums' } }, `${room.total} 条`),
|
|
1812
|
+
empty ? null : h('span', { style: { ...PANEL_MUTED, flex: '0 0 auto', marginLeft: 'auto', opacity: 0.6 } }, props.expanded ? '收起' : '展开'),
|
|
1813
|
+
),
|
|
1814
|
+
empty
|
|
1815
|
+
? h('span', { style: { ...PANEL_MUTED, padding: '2px 0 4px' } }, '还没有记录')
|
|
1816
|
+
: h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(96px, 1fr))', gap: '6px' } },
|
|
1817
|
+
slots.map((record, index) => h(PalaceSlot, {
|
|
1818
|
+
key: record ? record.id : `slot-${room.id}-${index}`,
|
|
1819
|
+
record, active: Boolean(record && record.id === props.selectedId), onPick: props.onPick,
|
|
1820
|
+
}))),
|
|
1821
|
+
),
|
|
1822
|
+
);
|
|
1823
|
+
};
|
|
1824
|
+
|
|
1825
|
+
const MemoryPalace = (props) => {
|
|
1826
|
+
const rows = Array.isArray(props.records) ? props.records : EMPTY;
|
|
1827
|
+
const [expanded, setExpanded] = React.useState('');
|
|
1828
|
+
const [page, setPage] = React.useState(1);
|
|
1829
|
+
const panelRef = React.useRef(null);
|
|
1830
|
+
const reduced = useReducedMotion();
|
|
1831
|
+
const motion = palaceMotionOf(reduced);
|
|
1832
|
+
const rooms = React.useMemo(() => palaceRoomsOf(rows), [rows]);
|
|
1833
|
+
const activeRoom = rooms.find((room) => room.id === expanded) || null;
|
|
1834
|
+
const pageInfo = activeRoom ? palaceRoomPageOf(rows, activeRoom.id, page) : null;
|
|
1835
|
+
|
|
1836
|
+
React.useEffect(() => {
|
|
1837
|
+
if (expanded && !rooms.some((room) => room.id === expanded)) setExpanded('');
|
|
1838
|
+
}, [rooms, expanded]);
|
|
1839
|
+
React.useEffect(() => {
|
|
1840
|
+
const node = panelRef.current;
|
|
1841
|
+
if (!expanded || !node || motion.duration === 0 || typeof node.animate !== 'function') return;
|
|
1842
|
+
node.animate([
|
|
1843
|
+
{ opacity: 0, transform: 'translateY(-6px)' },
|
|
1844
|
+
{ opacity: 1, transform: 'translateY(0)' },
|
|
1845
|
+
], { duration: motion.duration, easing: motion.easing });
|
|
1846
|
+
}, [expanded, page, motion.duration, motion.easing]);
|
|
1847
|
+
|
|
1848
|
+
if (rows.length === 0) {
|
|
1849
|
+
return h('div', { style: { ...PANEL_CARD, padding: '14px', minHeight: 0 } },
|
|
1850
|
+
h('div', { style: PANEL_MUTED }, props.emptyText || '这个项目还没有记忆'));
|
|
1851
|
+
}
|
|
1852
|
+
const toggle = (roomId) => {
|
|
1853
|
+
setExpanded((current) => palaceToggleOf(current, roomId));
|
|
1854
|
+
setPage(1);
|
|
1855
|
+
};
|
|
1856
|
+
const goPage = (target) => {
|
|
1857
|
+
if (!pageInfo) return;
|
|
1858
|
+
setPage(Math.min(pageInfo.pages, Math.max(1, target)));
|
|
1859
|
+
};
|
|
1860
|
+
const pageButton = (label, target, disabled, current) => h('button', {
|
|
1861
|
+
key: `page-${target}-${label}`, type: 'button', disabled,
|
|
1862
|
+
'aria-current': current ? 'page' : undefined, 'aria-label': `第 ${target} 页`,
|
|
1863
|
+
onClick: () => goPage(target),
|
|
1864
|
+
style: {
|
|
1865
|
+
...PANEL_LINK, minWidth: '22px', height: '22px', padding: '0 5px', borderRadius: '6px',
|
|
1866
|
+
border: 'none', opacity: disabled ? 0.28 : current ? 0.85 : 0.5,
|
|
1867
|
+
background: current ? 'var(--dsw-alias-bg-layer-2)' : 'transparent',
|
|
1868
|
+
fontVariantNumeric: 'tabular-nums', cursor: disabled ? 'default' : 'pointer',
|
|
1869
|
+
},
|
|
1870
|
+
}, label);
|
|
1871
|
+
const pageSlots = pageInfo
|
|
1872
|
+
? Array.from({ length: pageInfo.items.length ? Math.ceil(pageInfo.items.length / 6) * 6 : 0 }, (_, index) => pageInfo.items[index] || null)
|
|
1873
|
+
: EMPTY;
|
|
1874
|
+
return h('div', {
|
|
1875
|
+
role: 'group', 'aria-label': '记忆宫殿',
|
|
1876
|
+
onKeyDown: (event) => {
|
|
1877
|
+
if (!pageInfo || pageInfo.pages <= 1) return;
|
|
1878
|
+
if (event.key === 'ArrowRight' && pageInfo.page < pageInfo.pages) {
|
|
1879
|
+
event.preventDefault();
|
|
1880
|
+
goPage(pageInfo.page + 1);
|
|
1881
|
+
}
|
|
1882
|
+
if (event.key === 'ArrowLeft' && pageInfo.page > 1) {
|
|
1883
|
+
event.preventDefault();
|
|
1884
|
+
goPage(pageInfo.page - 1);
|
|
1885
|
+
}
|
|
1886
|
+
if (event.key === 'Home' && pageInfo.page > 1) {
|
|
1887
|
+
event.preventDefault();
|
|
1888
|
+
goPage(1);
|
|
1889
|
+
}
|
|
1890
|
+
if (event.key === 'End' && pageInfo.page < pageInfo.pages) {
|
|
1891
|
+
event.preventDefault();
|
|
1892
|
+
goPage(pageInfo.pages);
|
|
1893
|
+
}
|
|
1894
|
+
},
|
|
1895
|
+
style: { ...PANEL_CARD, padding: '10px', minHeight: 0, flex: '0 0 auto', overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: '10px' },
|
|
1896
|
+
},
|
|
1897
|
+
h('div', {
|
|
1898
|
+
role: 'list',
|
|
1899
|
+
'aria-label': '记忆房间',
|
|
1900
|
+
style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(232px, 1fr))', gap: '9px' },
|
|
1901
|
+
},
|
|
1902
|
+
rooms.map((room) => h(PalaceRoomCard, {
|
|
1903
|
+
key: room.id, room, expanded: room.id === expanded, selectedId: props.selectedId,
|
|
1904
|
+
onPick: props.onPick, onToggle: toggle,
|
|
1905
|
+
}))),
|
|
1906
|
+
h('div', { 'aria-live': 'polite', style: { ...PANEL_MUTED, marginTop: '-2px' } }, `${rooms.length} 个房间 · ${rows.length} 条`),
|
|
1907
|
+
activeRoom && activeRoom.total > 0 && pageInfo
|
|
1908
|
+
? h('section', {
|
|
1909
|
+
id: `lite-palace-room-${activeRoom.id}`, 'aria-label': activeRoom.name, ref: panelRef,
|
|
1910
|
+
style: { borderTop: '1px solid var(--dsw-alias-border-l1)', paddingTop: '9px', display: 'flex', flexDirection: 'column', gap: '8px' },
|
|
1911
|
+
},
|
|
1912
|
+
h('div', { style: { display: 'flex', alignItems: 'center', gap: '8px' } },
|
|
1913
|
+
h('span', { style: { fontSize: '12px', lineHeight: '1.4', fontWeight: 600 } }, activeRoom.name),
|
|
1914
|
+
h('span', { style: PANEL_MUTED, 'aria-live': 'polite', 'aria-atomic': 'true' }, `${pageInfo.total} 条${pageInfo.pages > 1 ? ` · 第 ${pageInfo.page} / ${pageInfo.pages} 页` : ''}`),
|
|
1915
|
+
h('button', {
|
|
1916
|
+
type: 'button', onClick: () => toggle(activeRoom.id),
|
|
1917
|
+
style: { ...PANEL_LINK, marginLeft: 'auto', height: '24px', padding: '0 7px', borderRadius: '6px', border: '1px solid var(--dsw-alias-border-l1)' },
|
|
1918
|
+
}, '收起'),
|
|
1919
|
+
),
|
|
1920
|
+
h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(96px, 1fr))', gap: '6px' } },
|
|
1921
|
+
pageSlots.map((record, index) => h(PalaceSlot, {
|
|
1922
|
+
key: record ? record.id : `page-${pageInfo.page}-slot-${index}`,
|
|
1923
|
+
record, active: Boolean(record && record.id === props.selectedId), onPick: props.onPick,
|
|
1924
|
+
}))),
|
|
1925
|
+
pageInfo.pages > 1
|
|
1926
|
+
? h('div', { role: 'navigation', 'aria-label': '分页', style: { display: 'flex', alignItems: 'center', gap: '4px', flexWrap: 'wrap' } },
|
|
1927
|
+
pageButton('上一页', pageInfo.page - 1, pageInfo.page <= 1, false),
|
|
1928
|
+
palacePageWindow(pageInfo.page, pageInfo.pages).map((number) => pageButton(String(number), number, false, number === pageInfo.page)),
|
|
1929
|
+
pageButton('下一页', pageInfo.page + 1, pageInfo.page >= pageInfo.pages, false))
|
|
1930
|
+
: null,
|
|
1931
|
+
)
|
|
1932
|
+
: null,
|
|
1933
|
+
);
|
|
1934
|
+
};
|
|
1935
|
+
|
|
1013
1936
|
const MemoryPanel = (props) => {
|
|
1014
1937
|
const sessionId = props.sessionId;
|
|
1015
1938
|
const [data, setData] = React.useState(null);
|
|
@@ -1027,9 +1950,14 @@ window.__ModuleLoader__.load({
|
|
|
1027
1950
|
const [detailNote, setDetailNote] = React.useState('');
|
|
1028
1951
|
const [busy, setBusy] = React.useState(false);
|
|
1029
1952
|
const [showReason, setShowReason] = React.useState(false);
|
|
1953
|
+
const [showSettings, setShowSettings] = React.useState(false);
|
|
1954
|
+
const [display, setDisplay] = React.useState({ ...DISPLAY_DEFAULTS });
|
|
1955
|
+
const [hubFilter, setHubFilter] = React.useState(null);
|
|
1030
1956
|
const alive = React.useRef(true);
|
|
1031
1957
|
const generation = React.useRef(0);
|
|
1032
1958
|
const scopeRef = React.useRef('');
|
|
1959
|
+
/** 先算出当前范围键,显示设置的读取/落盘都按它来,不能等后面的派生变量。 */
|
|
1960
|
+
const selectedKey = data && data.selected && data.selected.key ? data.selected.key : '';
|
|
1033
1961
|
React.useEffect(() => {
|
|
1034
1962
|
alive.current = true;
|
|
1035
1963
|
return () => { alive.current = false; };
|
|
@@ -1066,6 +1994,22 @@ window.__ModuleLoader__.load({
|
|
|
1066
1994
|
|
|
1067
1995
|
React.useEffect(() => { load(); }, [sessionId]);
|
|
1068
1996
|
|
|
1997
|
+
/** 显示设置跟着当前项目走;换项目或换范围时重新读。 */
|
|
1998
|
+
React.useEffect(() => {
|
|
1999
|
+
setDisplay(readDisplaySettings(selectedKey));
|
|
2000
|
+
setShowSettings(false);
|
|
2001
|
+
setHubFilter(null);
|
|
2002
|
+
}, [selectedKey]);
|
|
2003
|
+
React.useEffect(() => {
|
|
2004
|
+
setMemoryView((value) => viewModeOf(display, value));
|
|
2005
|
+
setEvolutionView((value) => viewModeOf(display, value));
|
|
2006
|
+
}, [display.graph, display.palace]);
|
|
2007
|
+
const setDisplayFlag = (name, value) => {
|
|
2008
|
+
const next = { ...display, [name]: value === true };
|
|
2009
|
+
setDisplay(next);
|
|
2010
|
+
writeDisplaySettings(selectedKey, next);
|
|
2011
|
+
};
|
|
2012
|
+
|
|
1069
2013
|
React.useEffect(() => {
|
|
1070
2014
|
if (typeof window === 'undefined') return undefined;
|
|
1071
2015
|
const onKey = (event) => { if (event.key === 'Escape') { event.stopPropagation(); props.onClose(); } };
|
|
@@ -1098,7 +2042,6 @@ window.__ModuleLoader__.load({
|
|
|
1098
2042
|
const records = selected && Array.isArray(selected.records) ? selected.records : EMPTY;
|
|
1099
2043
|
const total = selected && selected.stats && Number.isInteger(selected.stats.total) ? selected.stats.total : records.length;
|
|
1100
2044
|
const currentKey = current ? current.key : '';
|
|
1101
|
-
const selectedKey = selected ? selected.key : '';
|
|
1102
2045
|
const control = selected && selected.control
|
|
1103
2046
|
? selected.control
|
|
1104
2047
|
: (current && selected && selected.key === current.key && current.control ? current.control : { enabled: true });
|
|
@@ -1126,6 +2069,11 @@ window.__ModuleLoader__.load({
|
|
|
1126
2069
|
const changedCount = activity && typeof activity === 'object' && Number.isInteger(activity.changed)
|
|
1127
2070
|
? activity.changed
|
|
1128
2071
|
: (Number.isInteger(activity && activity.count) ? activity.count : null);
|
|
2072
|
+
const activityReason = activity && typeof activity === 'object' && typeof activity.reason === 'string'
|
|
2073
|
+
? activity.reason
|
|
2074
|
+
: (latestReceipt && typeof latestReceipt.reason === 'string' ? latestReceipt.reason : '');
|
|
2075
|
+
const staleLearning = /没采用|作废|已更新|换到别的作品/.test(activityReason);
|
|
2076
|
+
const activityReasonText = staleLearning ? '整理过程中反馈或作品变了,这轮结果没采用。' : activityReason;
|
|
1129
2077
|
let activityText = '';
|
|
1130
2078
|
if (!evolution && memoryOn) {
|
|
1131
2079
|
activityText = '读不到自进化状态';
|
|
@@ -1142,7 +2090,7 @@ window.__ModuleLoader__.load({
|
|
|
1142
2090
|
} else if (activityCode === 'quota' || activityCode === 'exhausted') {
|
|
1143
2091
|
activityText = '本轮整理额度已用完';
|
|
1144
2092
|
} else if (activityCode === 'failed' || activityCode === 'error') {
|
|
1145
|
-
activityText = '最近整理失败,可查看原因';
|
|
2093
|
+
activityText = staleLearning ? '这轮整理没采用' : '最近整理失败,可查看原因';
|
|
1146
2094
|
} else if (activityCode === 'waiting' || activityCode === 'idle' || activityCode === 'empty') {
|
|
1147
2095
|
activityText = '等待可学习的反馈';
|
|
1148
2096
|
}
|
|
@@ -1155,18 +2103,7 @@ window.__ModuleLoader__.load({
|
|
|
1155
2103
|
else if (rules.length === 0) activityText = countsUnknown ? '等待可学习的反馈' : '还没有整理出经验';
|
|
1156
2104
|
else activityText = '暂无整理记录';
|
|
1157
2105
|
}
|
|
1158
|
-
const
|
|
1159
|
-
? activity.reason
|
|
1160
|
-
: (latestReceipt && typeof latestReceipt.reason === 'string' ? latestReceipt.reason : '');
|
|
1161
|
-
|
|
1162
|
-
const filtered = React.useMemo(() => {
|
|
1163
|
-
const needle = query.trim().toLowerCase();
|
|
1164
|
-
if (!needle) return records;
|
|
1165
|
-
return records.filter((record) => {
|
|
1166
|
-
const tags = Array.isArray(record.tags) ? record.tags.join(' ') : '';
|
|
1167
|
-
return `${record.title} ${record.summary} ${tags} ${KIND_LABEL[record.kind] || ''}`.toLowerCase().includes(needle);
|
|
1168
|
-
});
|
|
1169
|
-
}, [records, query]);
|
|
2106
|
+
const filtered = React.useMemo(() => filterRecords(records, query, hubFilter), [records, query, hubFilter]);
|
|
1170
2107
|
const filteredRules = React.useMemo(() => {
|
|
1171
2108
|
if (stage === 'all') return rules;
|
|
1172
2109
|
return rules.filter((rule) => rule.state === stage);
|
|
@@ -1179,6 +2116,19 @@ window.__ModuleLoader__.load({
|
|
|
1179
2116
|
setSelectedMemory(node.id);
|
|
1180
2117
|
setSelectedRule('');
|
|
1181
2118
|
};
|
|
2119
|
+
/** 点 hub 用精确值筛选,标签不发散成子串匹配。 */
|
|
2120
|
+
const onPickHub = (node) => {
|
|
2121
|
+
if (!node || typeof node.id !== 'string') return;
|
|
2122
|
+
const cut = node.id.indexOf(':');
|
|
2123
|
+
const kind = cut > 0 ? node.id.slice(0, cut) : '';
|
|
2124
|
+
const value = cut > 0 ? node.id.slice(cut + 1) : '';
|
|
2125
|
+
if ((kind !== 'tag' && kind !== 'topic') || !value) return;
|
|
2126
|
+
setHubFilter({ kind, value, display: kind === 'tag' ? `#${value}` : value });
|
|
2127
|
+
setQuery('');
|
|
2128
|
+
setSelectedMemory('');
|
|
2129
|
+
setSelectedRule('');
|
|
2130
|
+
setMemoryView('list');
|
|
2131
|
+
};
|
|
1182
2132
|
const onPickRuleNode = (node) => {
|
|
1183
2133
|
if (node.kind !== 'rule') return;
|
|
1184
2134
|
setSelectedRule(node.id);
|
|
@@ -1250,31 +2200,47 @@ window.__ModuleLoader__.load({
|
|
|
1250
2200
|
style: { height: '30px', maxWidth: '190px', padding: '0 8px', border: '1px solid var(--dsw-alias-border-l1)', borderRadius: '8px', background: 'var(--dsw-alias-bg-layer-2)', color: 'inherit', fontSize: '12px' },
|
|
1251
2201
|
},
|
|
1252
2202
|
data && Array.isArray(data.scopes) ? data.scopes.map((scope) => h('option', { key: scope.key, value: scope.key }, scope.kind === 'global' ? '全局' : scope.label)) : null) : null,
|
|
2203
|
+
h('button', {
|
|
2204
|
+
type: 'button', 'aria-expanded': showSettings, 'aria-label': '显示设置',
|
|
2205
|
+
onClick: () => setShowSettings((value) => !value),
|
|
2206
|
+
style: {
|
|
2207
|
+
...PANEL_LINK, height: '30px', padding: '0 9px', borderRadius: '8px',
|
|
2208
|
+
border: '1px solid var(--dsw-alias-border-l1)',
|
|
2209
|
+
background: showSettings ? 'var(--dsw-alias-bg-layer-2)' : 'transparent',
|
|
2210
|
+
opacity: showSettings ? 0.92 : 0.68,
|
|
2211
|
+
},
|
|
2212
|
+
}, '显示设置'),
|
|
1253
2213
|
closeButton,
|
|
1254
2214
|
),
|
|
1255
2215
|
);
|
|
1256
2216
|
|
|
1257
|
-
const
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
h(Toggle, { ariaLabel: '启用记忆', on: memoryOn, busy: busy || !canToggle, onClick: canToggle ? toggleControl : undefined }),
|
|
1261
|
-
),
|
|
1262
|
-
h('div', { style: PANEL_MUTED }, '读取项目和全局记忆'), // ui-copy-ignore 用户指定文案
|
|
2217
|
+
const displayToggle = (label, key) => switchItem(
|
|
2218
|
+
label, '', display[key] === true, !selectedKey,
|
|
2219
|
+
selectedKey ? () => setDisplayFlag(key, display[key] !== true) : undefined,
|
|
1263
2220
|
);
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
2221
|
+
|
|
2222
|
+
const switchItem = (label, hint, on, disabled, onClick) => h('div', {
|
|
2223
|
+
style: { display: 'flex', flexDirection: 'column', gap: '1px', minWidth: 0, opacity: disabled && !on ? 0.55 : 1 },
|
|
2224
|
+
},
|
|
2225
|
+
h('span', { style: { display: 'flex', alignItems: 'center', gap: '8px', minWidth: 0 } },
|
|
2226
|
+
h('span', { style: { fontSize: '12px', lineHeight: '20px', fontWeight: 600, whiteSpace: 'nowrap' } }, label),
|
|
2227
|
+
h(Toggle, { ariaLabel: label, on, busy: busy || disabled, onClick: disabled ? undefined : onClick }),
|
|
2228
|
+
),
|
|
2229
|
+
hint ? h('span', { style: { ...PANEL_MUTED, lineHeight: '16px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, hint) : null);
|
|
2230
|
+
const evolutionOn = Boolean(memoryOn && evolution && evolution.enabled === true);
|
|
2231
|
+
const switchBar = h('div', {
|
|
2232
|
+
style: { display: 'flex', flexDirection: 'column', gap: '3px', padding: '7px 18px 8px', borderBottom: '1px solid var(--dsw-alias-border-l1)', flex: '0 0 auto' },
|
|
2233
|
+
},
|
|
2234
|
+
globalView
|
|
2235
|
+
? h('div', { style: { ...PANEL_MUTED, lineHeight: '20px' } }, '全局记忆只读')
|
|
2236
|
+
: h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(190px, 1fr))', gap: '4px 18px' } },
|
|
2237
|
+
switchItem('启用记忆', memoryOn ? '读取项目和全局记忆' : '记忆已关闭,内容只读', memoryOn, busy || !canToggle, toggleControl),
|
|
2238
|
+
switchItem('自进化', evolutionOn ? '实验功能,会增加 token 消耗' : '从反馈里整理经验', evolutionOn, busy || !evolution || !canToggle || !memoryOn, toggleEvolution), // ui-copy-ignore 用户指定文案
|
|
2239
|
+
showSettings ? displayToggle('启用图谱', 'graph') : null,
|
|
2240
|
+
showSettings ? displayToggle('启用记忆宫殿', 'palace') : null,
|
|
1268
2241
|
),
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
);
|
|
1272
|
-
const switchBar = h('div', { style: { display: 'flex', alignItems: 'stretch', gap: '10px', flexWrap: 'wrap', padding: '12px 18px', borderBottom: '1px solid var(--dsw-alias-border-l1)', flex: '0 0 auto' } },
|
|
1273
|
-
globalView ? h('div', { style: { ...PANEL_MUTED, padding: '2px 0' } }, '全局记忆只读') : memorySwitch,
|
|
1274
|
-
globalView ? null : evolutionSwitch,
|
|
1275
|
-
!memoryOn && !globalView ? h('div', { style: { ...PANEL_MUTED, flexBasis: '100%' } }, '记忆已关闭,内容只读') : null,
|
|
1276
|
-
!globalView && !canToggle ? h('div', { style: { ...PANEL_MUTED, flexBasis: '100%' } }, '只读,切换回当前项目后可修改') : null,
|
|
1277
|
-
note ? h('div', { style: { ...PANEL_MUTED, flexBasis: '100%', color: 'var(--dsw-alias-state-error-primary)' } }, note) : null,
|
|
2242
|
+
!globalView && !canToggle ? h('div', { style: { ...PANEL_MUTED, lineHeight: '18px' } }, '只读,切换回当前项目后可修改') : null,
|
|
2243
|
+
note ? h('div', { style: { ...PANEL_MUTED, lineHeight: '18px', color: 'var(--dsw-alias-state-error-primary)' } }, note) : null,
|
|
1278
2244
|
);
|
|
1279
2245
|
|
|
1280
2246
|
const switchTab = (key) => {
|
|
@@ -1298,15 +2264,20 @@ window.__ModuleLoader__.load({
|
|
|
1298
2264
|
globalView ? null : tabButton('evolution', '自进化'),
|
|
1299
2265
|
);
|
|
1300
2266
|
|
|
2267
|
+
const filteredNow = Boolean(query || hubFilter);
|
|
1301
2268
|
const memoryList = loading
|
|
1302
|
-
? h('div', { style: { ...PANEL_MUTED, padding: '
|
|
2269
|
+
? h('div', { style: { ...PANEL_MUTED, padding: '14px 9px' } }, '读取中…')
|
|
1303
2270
|
: filtered.length === 0
|
|
1304
|
-
? h('div', { style: {
|
|
2271
|
+
? h('div', { style: { display: 'flex', alignItems: 'center', gap: '8px', padding: '14px 9px', minWidth: 0 } },
|
|
2272
|
+
h('span', { style: { ...PANEL_MUTED, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, filteredNow ? '没有匹配的记忆' : '还没有记忆'),
|
|
2273
|
+
filteredNow ? h(Action, { onClick: () => { setHubFilter(null); setQuery(''); } }, '清除搜索') : null,
|
|
2274
|
+
)
|
|
1305
2275
|
: filtered.map((record) => h(MemoryRow, {
|
|
1306
2276
|
key: record.id, record, active: record.id === selectedMemory,
|
|
1307
2277
|
onClick: () => { setSelectedMemory(record.id); setSelectedRule(''); },
|
|
1308
2278
|
}));
|
|
1309
2279
|
|
|
2280
|
+
const detailProcessing = detail ? processingStatusOf(detail) : '';
|
|
1310
2281
|
const memoryDetail = selectedMemory
|
|
1311
2282
|
? h('div', { style: { ...PANEL_CARD, minHeight: 0, overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: '6px' } },
|
|
1312
2283
|
detail
|
|
@@ -1315,17 +2286,18 @@ window.__ModuleLoader__.load({
|
|
|
1315
2286
|
h('div', { style: { flex: 1, minWidth: 0, fontSize: '13px', lineHeight: '1.45', fontWeight: 600, wordBreak: 'break-word' } }, detail.title),
|
|
1316
2287
|
h(Action, { onClick: () => setSelectedMemory('') }, '关闭'),
|
|
1317
2288
|
),
|
|
1318
|
-
h('div', { style: PANEL_MUTED }, `${KIND_LABEL[detailRecord && detailRecord.kind] || '记忆'} · ${detail.scopeLabel || ''}`),
|
|
2289
|
+
h('div', { style: PANEL_MUTED }, `${KIND_LABEL[detailRecord && detailRecord.kind] || '记忆'} · ${detail.scopeLabel || ''}${detailProcessing ? ` · ${PROCESSING_LABEL[detailProcessing]}` : ''}`),
|
|
1319
2290
|
Array.isArray(detailRecord && detailRecord.tags) && detailRecord.tags.length
|
|
1320
2291
|
? h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: '4px' } },
|
|
1321
2292
|
detailRecord.tags.slice(0, 6).map((tag) => h('span', { key: tag, style: { ...PANEL_MUTED, padding: '0 6px', border: '1px solid var(--dsw-alias-border-l1)', borderRadius: '999px' } }, `#${tag}`))) : null,
|
|
1322
|
-
h('div', { style: { fontSize: '12px', lineHeight: '1.65', whiteSpace: 'pre-wrap', wordBreak: 'break-word' } }, detail
|
|
2293
|
+
h('div', { style: { fontSize: '12px', lineHeight: '1.65', whiteSpace: 'pre-wrap', wordBreak: 'break-word' } }, currentBodyOf(detail) || '没有正文'),
|
|
2294
|
+
h(DetailExtras, { payload: detail }),
|
|
1323
2295
|
)
|
|
1324
2296
|
: h('div', { style: PANEL_MUTED }, detailNote || '读取中…'),
|
|
1325
2297
|
)
|
|
1326
2298
|
: null;
|
|
1327
2299
|
|
|
1328
|
-
const viewToggleOf = (current, setValue, ariaLabel) => {
|
|
2300
|
+
const viewToggleOf = (current, setValue, ariaLabel, modes) => {
|
|
1329
2301
|
const button = (key, label) => h('button', {
|
|
1330
2302
|
key, type: 'button', onClick: () => setValue(key),
|
|
1331
2303
|
'aria-pressed': current === key,
|
|
@@ -1335,6 +2307,7 @@ window.__ModuleLoader__.load({
|
|
|
1335
2307
|
opacity: current === key ? 0.9 : 0.5,
|
|
1336
2308
|
},
|
|
1337
2309
|
}, label);
|
|
2310
|
+
const options = Array.isArray(modes) ? modes : [['list', '列表'], ['graph', '图谱']];
|
|
1338
2311
|
return h('div', {
|
|
1339
2312
|
role: 'group', 'aria-label': ariaLabel,
|
|
1340
2313
|
style: {
|
|
@@ -1343,23 +2316,41 @@ window.__ModuleLoader__.load({
|
|
|
1343
2316
|
border: '1px solid var(--dsw-alias-border-l1)', borderRadius: '8px',
|
|
1344
2317
|
background: 'var(--dsw-alias-bg-base)',
|
|
1345
2318
|
},
|
|
1346
|
-
},
|
|
2319
|
+
}, options.map(([key, label]) => button(key, label)));
|
|
1347
2320
|
};
|
|
1348
|
-
const
|
|
2321
|
+
const memoryModes = [['list', '列表']];
|
|
2322
|
+
if (display.graph) memoryModes.push(['graph', '图谱']);
|
|
2323
|
+
if (display.palace) memoryModes.push(['palace', '宫殿']);
|
|
2324
|
+
const memoryToggle = memoryModes.length > 1 ? viewToggleOf(memoryView, setMemoryView, '记忆视图', memoryModes) : null;
|
|
2325
|
+
const memoryViewActive = viewModeOf(display, memoryView);
|
|
2326
|
+
const evolutionViewActive = graphViewOf(display.graph, evolutionView);
|
|
1349
2327
|
|
|
1350
2328
|
const memoryListPane = h('div', {
|
|
1351
2329
|
style: {
|
|
1352
2330
|
...PANEL_CARD, padding: '2px 4px', minHeight: 0, overflowY: 'auto',
|
|
2331
|
+
display: 'flex', flexDirection: 'column', gap: 0,
|
|
1353
2332
|
},
|
|
1354
2333
|
}, memoryList);
|
|
1355
2334
|
const memoryGraphPane = h('div', {
|
|
1356
2335
|
style: { ...PANEL_CARD, padding: 0, minHeight: 0, overflow: 'hidden', display: 'flex' },
|
|
1357
2336
|
},
|
|
1358
|
-
h(MemoryGraph, {
|
|
2337
|
+
h(MemoryGraph, {
|
|
2338
|
+
records: filtered, total: hubFilter ? filtered.length : total,
|
|
2339
|
+
selectedId: selectedMemory, onPick: onPickNode,
|
|
2340
|
+
onPickHub,
|
|
2341
|
+
emptyText: query || hubFilter ? '没有匹配的记忆' : '还没有记忆', ariaLabel: '记忆关系图',
|
|
2342
|
+
legend: '实心 记忆 · 空心 标签/主题 · 虚线 归类 · 点标签筛选,点记忆看正文',
|
|
2343
|
+
}));
|
|
2344
|
+
const memoryPalacePane = h(MemoryPalace, {
|
|
2345
|
+
key: selectedKey, records: filtered, selectedId: selectedMemory,
|
|
2346
|
+
onPick: (id) => { setSelectedMemory(id); setSelectedRule(''); },
|
|
2347
|
+
emptyText: query || hubFilter ? '没有匹配的记忆' : '这个项目还没有记忆',
|
|
2348
|
+
});
|
|
1359
2349
|
const memoryTab = h('div', { role: 'tabpanel', 'aria-label': '记忆', style: { flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', gap: '9px' } },
|
|
1360
2350
|
h('div', { style: { display: 'flex', alignItems: 'center', gap: '8px', flex: '0 0 auto' } },
|
|
1361
2351
|
h('input', {
|
|
1362
|
-
value:
|
|
2352
|
+
value: hubFilter ? hubFilter.display : query,
|
|
2353
|
+
onChange: (event) => { setHubFilter(null); setQuery(event.target.value); },
|
|
1363
2354
|
placeholder: '搜索标题或标签', 'aria-label': '搜索记忆',
|
|
1364
2355
|
style: { ...PANEL_INPUT, width: 'auto', flex: '1 1 0', minWidth: 0 },
|
|
1365
2356
|
}),
|
|
@@ -1371,7 +2362,7 @@ window.__ModuleLoader__.load({
|
|
|
1371
2362
|
gridTemplateColumns: selectedMemory ? 'minmax(0, 1fr) minmax(280px, 340px)' : 'minmax(0, 1fr)',
|
|
1372
2363
|
},
|
|
1373
2364
|
},
|
|
1374
|
-
|
|
2365
|
+
memoryViewActive === 'graph' ? memoryGraphPane : memoryViewActive === 'palace' ? memoryPalacePane : memoryListPane,
|
|
1375
2366
|
memoryDetail),
|
|
1376
2367
|
);
|
|
1377
2368
|
|
|
@@ -1397,7 +2388,7 @@ window.__ModuleLoader__.load({
|
|
|
1397
2388
|
fontVariantNumeric: 'tabular-nums',
|
|
1398
2389
|
},
|
|
1399
2390
|
}, `${label} ${countText(key)}`)),
|
|
1400
|
-
h('span', { style: { marginLeft: 'auto' } }, viewToggleOf(evolutionView, setEvolutionView, '自进化视图')),
|
|
2391
|
+
h('span', { style: { marginLeft: 'auto' } }, display.graph ? viewToggleOf(evolutionView, setEvolutionView, '自进化视图') : null),
|
|
1401
2392
|
);
|
|
1402
2393
|
|
|
1403
2394
|
const stageLabel = (stageFilters.find(([key]) => key === stage) || ['all', '全部'])[1];
|
|
@@ -1407,21 +2398,14 @@ window.__ModuleLoader__.load({
|
|
|
1407
2398
|
: rules.length === 0 ? (countsUnknown ? '数量未知' : '还没有整理出经验')
|
|
1408
2399
|
: `没有${stageLabel}的经验`;
|
|
1409
2400
|
const evolutionList = filteredRules.length === 0
|
|
1410
|
-
? h('div', { style: {
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
},
|
|
1419
|
-
},
|
|
1420
|
-
h('span', { style: { fontSize: '12px', lineHeight: '1.45', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', opacity: 0.92 } }, rule.title),
|
|
1421
|
-
h('span', { style: { ...PANEL_MUTED, display: 'flex', alignItems: 'center', gap: '6px', minWidth: 0 } },
|
|
1422
|
-
h(StagePill, { state: rule.state }),
|
|
1423
|
-
h('span', { style: { marginLeft: 'auto', flex: '0 0 auto', fontVariantNumeric: 'tabular-nums' } }, EVO_DATE(rule.updated)),
|
|
1424
|
-
)));
|
|
2401
|
+
? h('div', { style: { display: 'flex', alignItems: 'center', gap: '8px', padding: '14px 9px', minWidth: 0 } },
|
|
2402
|
+
h('span', { style: { ...PANEL_MUTED, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, evolutionEmptyText),
|
|
2403
|
+
stage !== 'all' && (memoryOn && evolution && evolution.enabled === true) ? h(Action, { onClick: () => setStage('all') }, '看全部') : null,
|
|
2404
|
+
)
|
|
2405
|
+
: filteredRules.map((rule) => h(EvolutionRow, {
|
|
2406
|
+
key: rule.id, rule, active: rule.id === selectedRule,
|
|
2407
|
+
onClick: () => { setSelectedRule(rule.id); setSelectedMemory(''); setShowReason(false); },
|
|
2408
|
+
}));
|
|
1425
2409
|
|
|
1426
2410
|
const evolutionReady = selectedKey === currentKey;
|
|
1427
2411
|
const evolutionFailed = !evolutionReady || failed
|
|
@@ -1442,16 +2426,18 @@ window.__ModuleLoader__.load({
|
|
|
1442
2426
|
: null;
|
|
1443
2427
|
const evolutionListPane = h('div', {
|
|
1444
2428
|
style: {
|
|
1445
|
-
...PANEL_CARD, padding: '
|
|
2429
|
+
...PANEL_CARD, padding: '4px', minHeight: 0, overflowY: 'auto',
|
|
2430
|
+
display: 'flex', flexDirection: 'column', gap: '2px',
|
|
1446
2431
|
},
|
|
1447
2432
|
}, evolutionList);
|
|
1448
2433
|
const evolutionGraphPane = h('div', {
|
|
1449
2434
|
style: { ...PANEL_CARD, padding: 0, minHeight: 0, overflow: 'hidden', display: 'flex' },
|
|
1450
2435
|
},
|
|
1451
2436
|
h(MemoryGraph, {
|
|
1452
|
-
records: filteredRules, graphOf: ruleGraphOf, primaryKind: 'rule',
|
|
2437
|
+
records: filteredRules, total: filteredRules.length, graphOf: ruleGraphOf, primaryKind: 'rule',
|
|
1453
2438
|
selectedId: selectedRule, onPick: onPickRuleNode,
|
|
1454
2439
|
emptyText: evolutionEmptyText, ariaLabel: '经验关系图',
|
|
2440
|
+
legend: '实心 经验 · 空心 条件 · 虚线 共用条件 · 点经验查看',
|
|
1455
2441
|
}));
|
|
1456
2442
|
const evolutionTab = evolutionFailed ? evolutionFailed : h('div', { role: 'tabpanel', 'aria-label': '自进化', style: { flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', gap: '9px' } },
|
|
1457
2443
|
h('div', { style: { ...PANEL_CARD, padding: '9px 11px', display: 'flex', alignItems: 'flex-start', gap: '8px', flex: '0 0 auto' } },
|
|
@@ -1459,9 +2445,9 @@ window.__ModuleLoader__.load({
|
|
|
1459
2445
|
h('div', { style: { display: 'flex', flexDirection: 'column', gap: '2px', minWidth: 0 } },
|
|
1460
2446
|
h('div', { style: { fontSize: '12px', lineHeight: '1.55' } }, activityText || (rules.length === 0 ? '还没有整理出经验' : '暂无整理记录')),
|
|
1461
2447
|
countsUnknown ? h('div', { style: PANEL_MUTED }, '数量未知') : null,
|
|
1462
|
-
|
|
2448
|
+
activityReasonText
|
|
1463
2449
|
? (showReason
|
|
1464
|
-
? h('div', { style: { ...PANEL_MUTED, whiteSpace: 'pre-wrap', wordBreak: 'break-word' } },
|
|
2450
|
+
? h('div', { style: { ...PANEL_MUTED, whiteSpace: 'pre-wrap', wordBreak: 'break-word' } }, activityReasonText)
|
|
1465
2451
|
: h(Action, { onClick: () => setShowReason(true) }, '查看原因'))
|
|
1466
2452
|
: null,
|
|
1467
2453
|
),
|
|
@@ -1473,11 +2459,17 @@ window.__ModuleLoader__.load({
|
|
|
1473
2459
|
gridTemplateColumns: selectedRuleRow ? 'minmax(0, 1fr) minmax(280px, 340px)' : 'minmax(0, 1fr)',
|
|
1474
2460
|
},
|
|
1475
2461
|
},
|
|
1476
|
-
|
|
2462
|
+
evolutionViewActive === 'graph' ? evolutionGraphPane : evolutionListPane,
|
|
1477
2463
|
evolutionDetailPane),
|
|
1478
2464
|
);
|
|
1479
2465
|
|
|
1480
|
-
const
|
|
2466
|
+
const palaceLoose = activeTab === 'memory' && viewModeOf(display, memoryView) === 'palace' && !selectedMemory;
|
|
2467
|
+
const content = h('section', {
|
|
2468
|
+
style: {
|
|
2469
|
+
flex: palaceLoose ? '0 1 auto' : 1, display: 'flex', flexDirection: 'column', minWidth: 0, minHeight: 0,
|
|
2470
|
+
padding: '12px 18px 14px', gap: '10px', overflowY: palaceLoose ? 'auto' : 'visible',
|
|
2471
|
+
},
|
|
2472
|
+
},
|
|
1481
2473
|
tabBar,
|
|
1482
2474
|
activeTab === 'memory' ? memoryTab : evolutionTab,
|
|
1483
2475
|
);
|
|
@@ -1488,7 +2480,10 @@ window.__ModuleLoader__.load({
|
|
|
1488
2480
|
},
|
|
1489
2481
|
h('div', {
|
|
1490
2482
|
role: 'dialog', 'aria-modal': 'true', 'aria-label': '记忆面板',
|
|
1491
|
-
style: {
|
|
2483
|
+
style: {
|
|
2484
|
+
...PANEL_DIALOG, width: 'min(1120px, 100%)', boxSizing: 'border-box',
|
|
2485
|
+
height: palaceLoose ? 'auto' : 'min(760px, 100%)', maxHeight: 'min(760px, 100%)',
|
|
2486
|
+
},
|
|
1492
2487
|
}, ...children));
|
|
1493
2488
|
|
|
1494
2489
|
if (!sessionId) return null;
|
|
@@ -1533,12 +2528,22 @@ window.__ModuleLoader__.load({
|
|
|
1533
2528
|
update: list.filter((item) => item.exposure === 'update'),
|
|
1534
2529
|
};
|
|
1535
2530
|
}, [refs]);
|
|
2531
|
+
const noTriggerReason = typeof props.noTriggerReason === 'string' && props.noTriggerReason
|
|
2532
|
+
? props.noTriggerReason
|
|
2533
|
+
: (typeof refs.noTriggerReason === 'string' ? refs.noTriggerReason : '');
|
|
1536
2534
|
if (!sessionId && refs.length === 0) return null;
|
|
1537
|
-
if (groups.found.length === 0 && groups.provided.length === 0 && groups.update.length === 0)
|
|
2535
|
+
if (groups.found.length === 0 && groups.provided.length === 0 && groups.update.length === 0) {
|
|
2536
|
+
return noTriggerReason
|
|
2537
|
+
? h('div', { style: TAIL }, h(RefSection, { label: '搜索命中', tone: 'found', items: EMPTY, sessionId, turnKey: turn, noTriggerReason }))
|
|
2538
|
+
: null;
|
|
2539
|
+
}
|
|
2540
|
+
const section = (label, items, tone) => (items.length === 0
|
|
2541
|
+
? null
|
|
2542
|
+
: h(RefSection, { key: label, label, tone, items, sessionId, turnKey: turn }));
|
|
1538
2543
|
return h('div', { style: TAIL },
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
2544
|
+
section('搜索命中', groups.found, 'found'),
|
|
2545
|
+
section('读过正文', groups.provided, 'provided'),
|
|
2546
|
+
section('本轮更新', groups.update, 'update'),
|
|
1542
2547
|
);
|
|
1543
2548
|
};
|
|
1544
2549
|
|
|
@@ -1546,7 +2551,7 @@ window.__ModuleLoader__.load({
|
|
|
1546
2551
|
|
|
1547
2552
|
// 测试缝:Node 里验引用投影,浏览器里没有任何副作用。
|
|
1548
2553
|
if (typeof globalThis.__LITE_MEMORY_TEST__ === 'function') {
|
|
1549
|
-
globalThis.__LITE_MEMORY_TEST__({ metaOf, citationOf, citationsOfMeta, citationsOfTurn, dedupe, undoRefOf, graphOf, ruleGraphOf, MemoryPanel });
|
|
2554
|
+
globalThis.__LITE_MEMORY_TEST__({ metaOf, citationOf, citationsOfMeta, citationsOfTurn, dedupe, undoRefOf, graphOf, ruleGraphOf, processingStatusOf, currentBodyOf, historyDiffsOf, MemoryPanel, MemoryGraph, MemoryPalace, TurnMemory, RefSection, displayKeyOf, readDisplaySettings, writeDisplaySettings, graphViewOf, viewModeOf, filterRecords, projectionOf, failureStateOf, evidenceTextOf, evidenceMetaOf, palaceRoomsOf, palaceRoomPageOf, palacePageWindow, palaceToggleOf, palaceMotionOf, palaceRoomIdOf });
|
|
1550
2555
|
}
|
|
1551
2556
|
|
|
1552
2557
|
return {
|