dsh-lite-memory 0.16.4 → 0.18.1
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 +10 -10
- package/TRIGGERS.md +14 -13
- package/anchor.js +259 -24
- package/client.js +1334 -215
- package/docs/BENCH.md +10 -8
- package/docs/CONVENTIONS.md +143 -0
- package/docs/RELEASE-0.17.0.md +8 -0
- package/docs/RELEASE-0.18.0.md +35 -0
- package/docs/RELEASE-0.18.1.md +18 -0
- package/index.js +395 -105
- package/learning.js +456 -48
- package/package.json +9 -3
- package/state.js +119 -7
- package/store.js +627 -75
- package/test/anchor.test.mjs +240 -0
- package/test/client.test.mjs +216 -6
- package/test/contract.test.mjs +20 -9
- package/test/crossing.test.mjs +22 -1
- package/test/evolution-read.test.mjs +23 -0
- package/test/ha.test.mjs +136 -0
- package/test/learning.test.mjs +483 -1
- package/test/palace.test.mjs +102 -0
- package/test/recall.test.mjs +13 -0
- package/test/record.test.mjs +130 -0
- package/test/runtime-context.test.mjs +186 -0
- package/test/safety.test.mjs +119 -0
- package/test/state.test.mjs +121 -2
- package/test/store.test.mjs +454 -15
- package/view.js +71 -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,18 @@ 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
|
+
/** 条目类型:读正文看 recordKind,检索看 ref 自己的 kind;工具名不是类型。 */
|
|
102
|
+
entryKind: typeof meta.kind === 'string' && Object.prototype.hasOwnProperty.call(KIND_LABEL, meta.kind)
|
|
103
|
+
? meta.kind
|
|
104
|
+
: (typeof meta.recordKind === 'string' ? meta.recordKind : ''),
|
|
105
|
+
updated: typeof meta.updated === 'string' ? meta.updated : '',
|
|
106
|
+
reason: typeof meta.reason === 'string' ? meta.reason : '',
|
|
107
|
+
reasonCode: typeof meta.reasonCode === 'string' ? meta.reasonCode : '',
|
|
108
|
+
noTriggerReason: typeof meta.noTriggerReason === 'string' ? meta.noTriggerReason : '',
|
|
109
|
+
sourceExcerpt: meta.sourceExcerpt,
|
|
110
|
+
sourceRefs: meta.sourceRefs,
|
|
111
|
+
evidence: meta.evidence,
|
|
112
|
+
processingStatus: processingStatusOf(meta),
|
|
101
113
|
};
|
|
102
114
|
};
|
|
103
115
|
|
|
@@ -122,15 +134,22 @@ window.__ModuleLoader__.load({
|
|
|
122
134
|
const citationsOfTurn = (nodes) => {
|
|
123
135
|
if (!Array.isArray(nodes)) return EMPTY;
|
|
124
136
|
const out = [];
|
|
137
|
+
let noTriggerReason = '';
|
|
125
138
|
for (const node of nodes) {
|
|
126
139
|
const root = node && node.root;
|
|
127
140
|
if (root === null || typeof root !== 'object') continue;
|
|
128
141
|
for (const block of [root, ...(Array.isArray(root.subCalls) ? root.subCalls : [])]) {
|
|
129
142
|
const meta = metaOf(block);
|
|
130
143
|
if (!meta) continue;
|
|
131
|
-
|
|
144
|
+
const citations = citationsOfMeta(meta);
|
|
145
|
+
out.push(...citations);
|
|
146
|
+
if (citations.length === 0 && meta.kind === 'memory-search') {
|
|
147
|
+
const reason = [meta.noTriggerReason, meta.reason].find((value) => typeof value === 'string' && value);
|
|
148
|
+
if (reason) noTriggerReason = reason;
|
|
149
|
+
}
|
|
132
150
|
}
|
|
133
151
|
}
|
|
152
|
+
if (noTriggerReason) Object.defineProperty(out, 'noTriggerReason', { value: noTriggerReason, enumerable: false, configurable: true });
|
|
134
153
|
return out;
|
|
135
154
|
};
|
|
136
155
|
|
|
@@ -148,6 +167,9 @@ window.__ModuleLoader__.load({
|
|
|
148
167
|
out[at] = item;
|
|
149
168
|
}
|
|
150
169
|
}
|
|
170
|
+
if (items && typeof items.noTriggerReason === 'string' && items.noTriggerReason) {
|
|
171
|
+
Object.defineProperty(out, 'noTriggerReason', { value: items.noTriggerReason, enumerable: false, configurable: true });
|
|
172
|
+
}
|
|
151
173
|
return out;
|
|
152
174
|
};
|
|
153
175
|
|
|
@@ -196,7 +218,9 @@ window.__ModuleLoader__.load({
|
|
|
196
218
|
if (value !== lastNodes) {
|
|
197
219
|
lastNodes = value;
|
|
198
220
|
const next = dedupe(citationsOfTurn(value));
|
|
199
|
-
|
|
221
|
+
const nextReason = next.noTriggerReason || '';
|
|
222
|
+
const lastReason = snapshot.noTriggerReason || '';
|
|
223
|
+
if (!sameList(next, snapshot) || nextReason !== lastReason) snapshot = next;
|
|
200
224
|
}
|
|
201
225
|
return snapshot;
|
|
202
226
|
};
|
|
@@ -308,7 +332,9 @@ window.__ModuleLoader__.load({
|
|
|
308
332
|
setState('failed');
|
|
309
333
|
});
|
|
310
334
|
};
|
|
311
|
-
const
|
|
335
|
+
const processing = processingStatusOf(meta);
|
|
336
|
+
const baseLabel = firstLine(text) ?? RUNNING.memory_save;
|
|
337
|
+
const label = note || `${baseLabel}${processing ? ` · ${PROCESSING_LABEL[processing]}` : ''}`;
|
|
312
338
|
const button = changed && state !== 'dropped'
|
|
313
339
|
? h('button', { type: 'button', onClick: drop, disabled: !canDrop, 'aria-label': '撤销此次修改', style: state === 'idle' || state === 'failed' ? DROP : { ...DROP, opacity: 0.25, cursor: 'default' } }, '✕')
|
|
314
340
|
: null;
|
|
@@ -316,16 +342,272 @@ window.__ModuleLoader__.load({
|
|
|
316
342
|
};
|
|
317
343
|
|
|
318
344
|
const scopeTag = (scope) => (scope === 'global' ? '[全局]' : '[项目]');
|
|
345
|
+
|
|
346
|
+
/** 处理状态只认服务端枚举,绝不从中文 reason 反推。 */
|
|
347
|
+
const PROCESSING = ['applied', 'unchanged', 'superseded', 'failed'];
|
|
348
|
+
const PROCESSING_LABEL = {
|
|
349
|
+
applied: '已应用', unchanged: '无变化', superseded: '已被新版本取代', failed: '处理失败',
|
|
350
|
+
};
|
|
351
|
+
const processingStatusOf = (source) => {
|
|
352
|
+
if (!source || typeof source !== 'object') return '';
|
|
353
|
+
for (const field of ['processingStatus', 'processing', 'outcome', 'status']) {
|
|
354
|
+
const value = source[field];
|
|
355
|
+
if (typeof value === 'string' && PROCESSING.includes(value)) return value;
|
|
356
|
+
if (value && typeof value === 'object' && typeof value.status === 'string' && PROCESSING.includes(value.status)) return value.status;
|
|
357
|
+
}
|
|
358
|
+
return '';
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
const recordOfPayload = (payload) =>
|
|
362
|
+
(payload && payload.record && typeof payload.record === 'object' && !Array.isArray(payload.record) ? payload.record : null);
|
|
363
|
+
|
|
364
|
+
const bodyTextOf = (value) => {
|
|
365
|
+
if (typeof value === 'string') return value;
|
|
366
|
+
if (!value || typeof value !== 'object') return '';
|
|
367
|
+
for (const field of ['body', 'text', 'value']) {
|
|
368
|
+
if (typeof value[field] === 'string') return value[field];
|
|
369
|
+
}
|
|
370
|
+
return '';
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
/** 当前正文与历史版本分开,历史不拼进正文。 */
|
|
374
|
+
const currentBodyOf = (payload) => {
|
|
375
|
+
if (!payload || typeof payload !== 'object') return '';
|
|
376
|
+
if (typeof payload.currentBody === 'string') return payload.currentBody;
|
|
377
|
+
const record = recordOfPayload(payload);
|
|
378
|
+
if (record) {
|
|
379
|
+
const recordBody = bodyTextOf(record.current);
|
|
380
|
+
if (recordBody) return recordBody;
|
|
381
|
+
if (typeof record.body === 'string') return record.body;
|
|
382
|
+
}
|
|
383
|
+
if (payload.current !== undefined && typeof payload.current !== 'boolean') {
|
|
384
|
+
const current = bodyTextOf(payload.current);
|
|
385
|
+
if (current) return current;
|
|
386
|
+
}
|
|
387
|
+
return typeof payload.body === 'string' ? payload.body : '';
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
const historyOfPayload = (payload) => {
|
|
391
|
+
const record = recordOfPayload(payload);
|
|
392
|
+
const current = payload && payload.current && typeof payload.current === 'object' ? payload.current : null;
|
|
393
|
+
const lists = [
|
|
394
|
+
payload && payload.versions,
|
|
395
|
+
payload && payload.history,
|
|
396
|
+
record && record.versions,
|
|
397
|
+
record && record.history,
|
|
398
|
+
current && current.versions,
|
|
399
|
+
];
|
|
400
|
+
for (const list of lists) {
|
|
401
|
+
if (Array.isArray(list)) return list.filter((row) => row !== null && row !== undefined);
|
|
402
|
+
}
|
|
403
|
+
return EMPTY;
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
/** 历史行统一成 old → new,缺前后值用相邻版本补。 */
|
|
407
|
+
const historyDiffsOf = (payload) => {
|
|
408
|
+
const rows = historyOfPayload(payload);
|
|
409
|
+
if (rows.length === 0) return EMPTY;
|
|
410
|
+
const currentBody = currentBodyOf(payload);
|
|
411
|
+
const normalized = rows.map((row, index) => {
|
|
412
|
+
const value = row && typeof row === 'object' && !Array.isArray(row) ? row : { body: row };
|
|
413
|
+
const explicitOld = [value.old, value.from, value.before, value.previousBody]
|
|
414
|
+
.find((item) => typeof item === 'string');
|
|
415
|
+
const explicitNew = [value.next, value.to, value.after, value.newBody]
|
|
416
|
+
.find((item) => typeof item === 'string');
|
|
417
|
+
return {
|
|
418
|
+
key: `${Number.isInteger(value.revision) ? value.revision : 'v'}-${index}`,
|
|
419
|
+
at: typeof value.at === 'string' ? value.at : (typeof value.when === 'string' ? value.when : ''),
|
|
420
|
+
revision: Number.isInteger(value.revision) ? value.revision : null,
|
|
421
|
+
status: typeof value.status === 'string' ? value.status : '',
|
|
422
|
+
body: bodyTextOf(value), // ui-copy-ignore
|
|
423
|
+
old: explicitOld || '', // ui-copy-ignore
|
|
424
|
+
next: explicitNew || '', // ui-copy-ignore
|
|
425
|
+
};
|
|
426
|
+
});
|
|
427
|
+
const byRevision = normalized.every((row) => row.revision !== null);
|
|
428
|
+
if (byRevision) normalized.sort((a, b) => a.revision - b.revision);
|
|
429
|
+
else if (normalized.every((row) => Boolean(row.at))) normalized.sort((a, b) => a.at.localeCompare(b.at));
|
|
430
|
+
return normalized.map((row, index) => ({
|
|
431
|
+
...row,
|
|
432
|
+
old: row.old || row.body || '',
|
|
433
|
+
next: row.next || (index + 1 < normalized.length ? normalized[index + 1].body : currentBody),
|
|
434
|
+
}));
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
const payloadFieldOf = (payload, field) => {
|
|
438
|
+
const record = recordOfPayload(payload);
|
|
439
|
+
const current = payload && payload.current && typeof payload.current === 'object' ? payload.current : null;
|
|
440
|
+
for (const owner of [payload, record, current]) {
|
|
441
|
+
if (owner && owner[field] !== undefined) return owner[field];
|
|
442
|
+
}
|
|
443
|
+
return undefined;
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
const sourceExcerptOf = (payload) => bodyTextOf(payloadFieldOf(payload, 'sourceExcerpt'));
|
|
447
|
+
const refsOfPayload = (payload) => {
|
|
448
|
+
const value = payloadFieldOf(payload, 'sourceRefs');
|
|
449
|
+
if (typeof value === 'string' && value) return [value];
|
|
450
|
+
if (Array.isArray(value)) return value;
|
|
451
|
+
if (value && typeof value === 'object') return [value];
|
|
452
|
+
return EMPTY;
|
|
453
|
+
};
|
|
454
|
+
const evidenceOfPayload = (payload) => {
|
|
455
|
+
const value = payloadFieldOf(payload, 'evidence');
|
|
456
|
+
if (typeof value === 'string' && value) return [value];
|
|
457
|
+
if (Array.isArray(value)) return value;
|
|
458
|
+
if (value && typeof value === 'object') return [value];
|
|
459
|
+
return EMPTY;
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
const refLabelOf = (ref, index) => {
|
|
463
|
+
if (typeof ref === 'string') return ref;
|
|
464
|
+
if (!ref || typeof ref !== 'object') return '';
|
|
465
|
+
if (typeof ref.label === 'string' && ref.label) return ref.label;
|
|
466
|
+
if (typeof ref.title === 'string' && ref.title) return ref.title;
|
|
467
|
+
if (typeof ref.text === 'string' && ref.text) return ref.text;
|
|
468
|
+
if (ref.message !== undefined) return `消息 ${ref.message}`;
|
|
469
|
+
if (ref.messageId !== undefined) return `消息 ${ref.messageId}`;
|
|
470
|
+
if (ref.turn !== undefined) return `第 ${ref.turn} 轮`;
|
|
471
|
+
if (ref.turnIndex !== undefined) return `第 ${ref.turnIndex} 轮`;
|
|
472
|
+
return `引用 ${index + 1}`;
|
|
473
|
+
};
|
|
474
|
+
const refHrefOf = (ref) => (ref && typeof ref === 'object'
|
|
475
|
+
? [ref.href, ref.url, ref.link].find((value) => typeof value === 'string' && value)
|
|
476
|
+
: '');
|
|
477
|
+
|
|
478
|
+
const linkStyle = { color: 'inherit', textDecoration: 'underline', textUnderlineOffset: '2px', wordBreak: 'break-all' };
|
|
479
|
+
|
|
480
|
+
const HistoryVersions = (props) => {
|
|
481
|
+
const rows = Array.isArray(props.rows) ? props.rows : EMPTY;
|
|
482
|
+
const [open, setOpen] = React.useState(false);
|
|
483
|
+
if (rows.length === 0) return null;
|
|
484
|
+
return h('div', { style: { display: 'flex', flexDirection: 'column', gap: '2px' } },
|
|
485
|
+
h('button', {
|
|
486
|
+
type: 'button',
|
|
487
|
+
onClick: () => setOpen((value) => !value),
|
|
488
|
+
'aria-expanded': open,
|
|
489
|
+
'aria-label': `${open ? '收起' : '展开'}历史版本`,
|
|
490
|
+
style: CHIP,
|
|
491
|
+
}, `历史 ${rows.length}${open ? ' · 收起' : ''}`),
|
|
492
|
+
open ? h('div', { style: BODY }, rows.map((row) => h('div', {
|
|
493
|
+
key: row.key, style: { margin: '0 0 4px', minWidth: 0 },
|
|
494
|
+
},
|
|
495
|
+
h('div', { style: { opacity: 0.65, fontSize: '11px' } },
|
|
496
|
+
[row.at, row.revision === null ? '' : `v${row.revision}`, row.status].filter(Boolean).join(' · ')),
|
|
497
|
+
h('div', null,
|
|
498
|
+
h('span', { style: { opacity: 0.6 } }, SHORT(row.old, 120) || '—'),
|
|
499
|
+
' → ',
|
|
500
|
+
h('span', null, SHORT(row.next, 120) || '—'))))) : null,
|
|
501
|
+
);
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
const DetailExtras = (props) => {
|
|
505
|
+
const payload = props.payload;
|
|
506
|
+
const excerpt = sourceExcerptOf(payload);
|
|
507
|
+
const refs = refsOfPayload(payload);
|
|
508
|
+
const evidence = evidenceOfPayload(payload);
|
|
509
|
+
const history = historyDiffsOf(payload);
|
|
510
|
+
if (!excerpt && refs.length === 0 && evidence.length === 0 && history.length === 0) return null;
|
|
511
|
+
const links = (items, label) => (items.length === 0 ? null : h('div', { style: { display: 'flex', gap: '6px', minWidth: 0 } },
|
|
512
|
+
h('span', { style: { opacity: 0.6, flex: '0 0 auto' } }, label),
|
|
513
|
+
h('span', { style: { minWidth: 0, wordBreak: 'break-word' } }, items.map((item, index) => {
|
|
514
|
+
const href = refHrefOf(item);
|
|
515
|
+
const text = refLabelOf(item, index);
|
|
516
|
+
const key = `${index}-${text}`;
|
|
517
|
+
if (!href) return h('span', { key }, index > 0 ? ' · ' : '', text);
|
|
518
|
+
return h('span', { key }, index > 0 ? ' · ' : '', h('a', {
|
|
519
|
+
href, target: '_blank', rel: 'noreferrer', style: linkStyle,
|
|
520
|
+
}, text));
|
|
521
|
+
}))));
|
|
522
|
+
return h('div', { style: { display: 'flex', flexDirection: 'column', gap: '4px' } },
|
|
523
|
+
excerpt ? h('div', { style: BODY }, h('span', { style: { opacity: 0.65 } }, '来源'), ' ', excerpt) : null,
|
|
524
|
+
links(refs, '消息'),
|
|
525
|
+
links(evidence, '依据'),
|
|
526
|
+
h(HistoryVersions, { rows: history }));
|
|
527
|
+
};
|
|
528
|
+
|
|
319
529
|
const versionLine = (shown) => {
|
|
320
530
|
const scope = shown.scopeLabel || (shown.scope === 'global' ? '全局' : '项目');
|
|
321
531
|
const archived = shown.status === 'archived' ? ' · 已归档' : '';
|
|
322
|
-
|
|
532
|
+
const processing = processingStatusOf(shown.processingStatus ? { processingStatus: shown.processingStatus } : shown);
|
|
533
|
+
return `${scope}${archived}${processing ? ` · ${PROCESSING_LABEL[processing]}` : ''}`;
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* 引用三层的色调:中性 / 强调 / 成功。
|
|
538
|
+
* 三档色阶:中性 / 强调 / 成功;正文用主文本色。
|
|
539
|
+
*/
|
|
540
|
+
const REF_TONE = {
|
|
541
|
+
found: 'var(--dsw-alias-label-secondary)',
|
|
542
|
+
provided: 'var(--dsw-alias-brand-primary)',
|
|
543
|
+
update: 'var(--dsw-alias-state-success-primary)',
|
|
544
|
+
};
|
|
545
|
+
const REF_META = { display: 'flex', alignItems: 'center', gap: '6px', margin: '0 0 2px 2px', fontSize: '11px', lineHeight: '18px', color: 'var(--dsw-alias-label-secondary)' };
|
|
546
|
+
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' };
|
|
547
|
+
|
|
548
|
+
/** 出处一句话:类型、项目/全局、来源、状态。 */
|
|
549
|
+
/** 只管最近一个月:更早或没时间就不占位。 */
|
|
550
|
+
const agoLabel = (value) => {
|
|
551
|
+
const at = Date.parse(value);
|
|
552
|
+
if (!Number.isFinite(at)) return '';
|
|
553
|
+
const days = Math.floor((Date.now() - at) / 86_400_000);
|
|
554
|
+
if (days <= 0) return '今天更新';
|
|
555
|
+
if (days === 1) return '昨天更新';
|
|
556
|
+
if (days < 30) return `${days} 天前更新`;
|
|
557
|
+
return '';
|
|
558
|
+
};
|
|
559
|
+
const refMetaOf = (item) => {
|
|
560
|
+
const kind = KIND_LABEL[item.entryKind] || '';
|
|
561
|
+
const parts = kind
|
|
562
|
+
? [kind, item.scope === 'global' ? '全局' : '项目', item.origin === 'project_evolution' ? '自进化' : '你记的']
|
|
563
|
+
: [item.scope === 'global' ? '全局' : '项目', item.origin === 'project_evolution' ? '自进化' : '你记的'];
|
|
564
|
+
const ago = agoLabel(item.updated);
|
|
565
|
+
if (ago) parts.push(ago);
|
|
566
|
+
if (item.status === 'archived') parts.push('已归档');
|
|
567
|
+
const processing = PROCESSING_LABEL[item.processingStatus] || '';
|
|
568
|
+
if (processing) parts.push(processing);
|
|
569
|
+
return parts.join(' · ');
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
const RefChevron = (props) => h('svg', {
|
|
573
|
+
width: 8, height: 8, viewBox: '0 0 8 8', 'aria-hidden': 'true', focusable: 'false',
|
|
574
|
+
style: { flex: '0 0 auto', opacity: 0.65, transform: props.open ? 'rotate(90deg)' : 'none' },
|
|
575
|
+
}, 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' }));
|
|
576
|
+
|
|
577
|
+
/** 一条引用:标题一行、原因与来源一行,窄栏也不抢宽度。 */
|
|
578
|
+
const RefRow = (props) => {
|
|
579
|
+
const [hover, setHover] = React.useState(false);
|
|
580
|
+
const item = props.item;
|
|
581
|
+
const reason = item.noTriggerReason || item.reason || '';
|
|
582
|
+
return h('button', {
|
|
583
|
+
type: 'button', // ui-copy-ignore
|
|
584
|
+
onMouseEnter: () => setHover(true),
|
|
585
|
+
onMouseLeave: () => setHover(false),
|
|
586
|
+
onClick: props.onShow, // ui-copy-ignore
|
|
587
|
+
'aria-label': `查看记忆 ${item.title || '记忆'}${reason ? `,${reason}` : ''}`,
|
|
588
|
+
title: reason ? `${item.title || '记忆'},${reason}` : (item.title || '记忆'),
|
|
589
|
+
style: { // ui-copy-ignore
|
|
590
|
+
display: 'flex', alignItems: 'flex-start', gap: '7px', width: '100%', minWidth: 0, textAlign: 'left',
|
|
591
|
+
border: 'none', borderRadius: '8px', padding: '3px 7px', cursor: 'pointer', color: 'inherit',
|
|
592
|
+
background: props.active || hover ? 'var(--dsw-alias-bg-layer-2)' : 'transparent',
|
|
593
|
+
fontSize: '12px', lineHeight: '18px',
|
|
594
|
+
},
|
|
595
|
+
},
|
|
596
|
+
h('span', { 'aria-hidden': 'true', style: { flex: '0 0 auto', alignSelf: 'stretch', width: '2px', minHeight: '10px', borderRadius: '999px', background: props.tone, opacity: 0.85 } }),
|
|
597
|
+
h('span', { style: { display: 'flex', flexDirection: 'column', gap: '1px', flex: '1 1 auto', minWidth: 0 } },
|
|
598
|
+
h('span', { style: { minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, item.title || '记忆'),
|
|
599
|
+
h('span', { style: { display: 'flex', alignItems: 'center', gap: '6px', minWidth: 0, fontSize: '11px', lineHeight: '16px', color: 'var(--dsw-alias-label-secondary)' } },
|
|
600
|
+
reason ? h('span', { style: { minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, reason) : null,
|
|
601
|
+
h('span', { style: { marginLeft: 'auto', flex: '0 0 auto', fontVariantNumeric: 'tabular-nums' } }, refMetaOf(item)),
|
|
602
|
+
),
|
|
603
|
+
));
|
|
323
604
|
};
|
|
324
605
|
|
|
325
606
|
/** 一段引用:点开是名字,再点是当前正文。 */
|
|
326
607
|
const RefSection = (props) => {
|
|
327
608
|
const items = Array.isArray(props.items) ? props.items : EMPTY;
|
|
328
609
|
const sessionId = typeof props.sessionId === 'string' ? props.sessionId : '';
|
|
610
|
+
const tone = REF_TONE[props.tone] || REF_TONE.found;
|
|
329
611
|
const [open, setOpen] = React.useState(false);
|
|
330
612
|
const [shown, setShown] = React.useState(null);
|
|
331
613
|
const [note, setNote] = React.useState('');
|
|
@@ -341,7 +623,10 @@ window.__ModuleLoader__.load({
|
|
|
341
623
|
setShown(null);
|
|
342
624
|
setNote('');
|
|
343
625
|
}, [sessionId, props.turnKey]);
|
|
344
|
-
if (items.length === 0)
|
|
626
|
+
if (items.length === 0) {
|
|
627
|
+
const reason = typeof props.noTriggerReason === 'string' ? props.noTriggerReason : '';
|
|
628
|
+
return reason ? h('div', { style: { ...REF_META, margin: 0 } }, reason) : null;
|
|
629
|
+
}
|
|
345
630
|
const close = () => {
|
|
346
631
|
generation.current += 1;
|
|
347
632
|
setShown(null);
|
|
@@ -378,30 +663,35 @@ window.__ModuleLoader__.load({
|
|
|
378
663
|
scopeLabel: typeof payload.scopeLabel === 'string' ? payload.scopeLabel : '',
|
|
379
664
|
revision: Number.isInteger(payload.revision) ? payload.revision : item.revision,
|
|
380
665
|
status: typeof payload.status === 'string' ? payload.status : 'ok',
|
|
381
|
-
body:
|
|
666
|
+
body: currentBodyOf(payload),
|
|
667
|
+
processingStatus: processingStatusOf(payload) || item.processingStatus || '',
|
|
668
|
+
payload,
|
|
382
669
|
current: payload.revision === item.revision,
|
|
383
670
|
});
|
|
384
671
|
});
|
|
385
672
|
};
|
|
386
|
-
const rows = open ? items.map((item) => h(
|
|
673
|
+
const rows = open ? items.map((item) => h(RefRow, {
|
|
387
674
|
key: `${item.scope}|${item.id}|${item.revision}`, // ui-copy-ignore
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
title: item.title || item.id,
|
|
392
|
-
style: ITEM, // ui-copy-ignore
|
|
393
|
-
}, `${scopeTag(item.scope)} ${item.title || item.id}${item.origin === 'project_evolution' ? ' · 实验' : ''}`)) : [];
|
|
675
|
+
item, tone, active: Boolean(shown && shown.id === item.id),
|
|
676
|
+
onShow: () => show(item),
|
|
677
|
+
})) : [];
|
|
394
678
|
const detail = shown && shown.ref && items.some((item) => item.id === shown.id && item.revision === shown.ref.revision)
|
|
395
|
-
? h('div', { style:
|
|
396
|
-
shown.loading ? '
|
|
397
|
-
shown.gone ? '这条记忆已删除' : null,
|
|
398
|
-
shown.invalid ? '
|
|
679
|
+
? h('div', { style: { ...REF_BODY, borderLeft: `2px solid ${tone}` } },
|
|
680
|
+
shown.loading ? h('span', { style: REF_META }, '读取中…') : null,
|
|
681
|
+
shown.gone ? h('span', { style: REF_META }, '这条记忆已删除') : null,
|
|
682
|
+
shown.invalid ? h('span', { style: REF_META }, '读不到正文,点名字重试') : null,
|
|
399
683
|
!shown.loading && !shown.gone && !shown.invalid ? h(React.Fragment, null,
|
|
400
|
-
shown.title && shown.title !== shown.ref.title
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
684
|
+
shown.title && shown.title !== shown.ref.title
|
|
685
|
+
? h('div', { style: { fontSize: '12px', lineHeight: '1.55', fontWeight: 600 } }, shown.title)
|
|
686
|
+
: null,
|
|
687
|
+
h('div', { style: REF_META }, versionLine(shown)),
|
|
688
|
+
shown.when ? h('div', { style: REF_META }, shown.when) : null,
|
|
689
|
+
shown.body
|
|
690
|
+
? h('div', { style: { color: 'var(--dsw-alias-label-primary)', whiteSpace: 'pre-wrap', wordBreak: 'break-word' } }, shown.body)
|
|
691
|
+
: null,
|
|
692
|
+
h(DetailExtras, { payload: { ...shown.ref, ...shown.payload } }),
|
|
404
693
|
) : null,
|
|
694
|
+
note ? h('span', { style: REF_META }, note) : null,
|
|
405
695
|
)
|
|
406
696
|
: null;
|
|
407
697
|
return h('div', { style: DOCK, 'data-memory-section': props.label },
|
|
@@ -409,12 +699,20 @@ window.__ModuleLoader__.load({
|
|
|
409
699
|
type: 'button',
|
|
410
700
|
onClick: () => { if (open) close(); else setOpen(true); },
|
|
411
701
|
'aria-expanded': open,
|
|
412
|
-
'aria-label': `${open ? '收起' : '展开'}${props.label}
|
|
413
|
-
style:
|
|
414
|
-
|
|
702
|
+
'aria-label': `${open ? '收起' : '展开'}${props.label},${items.length} 条`,
|
|
703
|
+
style: {
|
|
704
|
+
alignSelf: 'flex-start', display: 'inline-flex', alignItems: 'center', gap: '6px',
|
|
705
|
+
border: 'none', background: 'transparent', padding: '1px 0', cursor: 'pointer',
|
|
706
|
+
color: 'var(--dsw-alias-label-secondary)', fontSize: '11px', lineHeight: '18px',
|
|
707
|
+
},
|
|
708
|
+
},
|
|
709
|
+
h('span', { 'aria-hidden': 'true', style: { width: '2px', height: '10px', borderRadius: '999px', background: tone, opacity: 0.9 } }),
|
|
710
|
+
h('span', null, props.label),
|
|
711
|
+
h('span', { style: { fontVariantNumeric: 'tabular-nums', opacity: 0.8 } }, String(items.length)),
|
|
712
|
+
h(RefChevron, { open }),
|
|
713
|
+
),
|
|
415
714
|
...rows,
|
|
416
715
|
detail,
|
|
417
|
-
note ? h('div', { style: BODY }, note) : null,
|
|
418
716
|
);
|
|
419
717
|
};
|
|
420
718
|
|
|
@@ -423,7 +721,7 @@ window.__ModuleLoader__.load({
|
|
|
423
721
|
/** 大脑:两瓣轮廓加一条中线,状态只靠透明度和面板文字。 */
|
|
424
722
|
const BrainIcon = () => h('svg', {
|
|
425
723
|
width: 18, height: 18, viewBox: '0 0 24 24', fill: 'none',
|
|
426
|
-
stroke: 'currentColor',
|
|
724
|
+
stroke: 'currentColor', 'stroke-width': 1.5, 'stroke-linecap': 'round', 'stroke-linejoin': 'round',
|
|
427
725
|
'aria-hidden': 'true', focusable: 'false',
|
|
428
726
|
},
|
|
429
727
|
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' }),
|
|
@@ -432,7 +730,7 @@ window.__ModuleLoader__.load({
|
|
|
432
730
|
|
|
433
731
|
const CloseIcon = () => h('svg', {
|
|
434
732
|
width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none',
|
|
435
|
-
stroke: 'currentColor',
|
|
733
|
+
stroke: 'currentColor', 'stroke-width': 1.6, 'stroke-linecap': 'round',
|
|
436
734
|
'aria-hidden': 'true', focusable: 'false',
|
|
437
735
|
}, h('path', { d: 'M6 6l12 12M18 6L6 18' }));
|
|
438
736
|
|
|
@@ -450,6 +748,224 @@ window.__ModuleLoader__.load({
|
|
|
450
748
|
const text = typeof value === 'string' ? value : '';
|
|
451
749
|
return text.length > size ? `${text.slice(0, size)}…` : text;
|
|
452
750
|
};
|
|
751
|
+
|
|
752
|
+
/** 显示设置只决定画不画,不改学习状态;按项目 key 存在本机。 */
|
|
753
|
+
const DISPLAY_DEFAULTS = Object.freeze({ graph: false, palace: false });
|
|
754
|
+
const displayKeyOf = (projectKey) => `lite-memory:display:${projectKey || 'default'}`;
|
|
755
|
+
const displayStoreOf = () => {
|
|
756
|
+
try {
|
|
757
|
+
const store = globalThis.localStorage;
|
|
758
|
+
return store && typeof store.getItem === 'function' ? store : null;
|
|
759
|
+
} catch {
|
|
760
|
+
return null;
|
|
761
|
+
}
|
|
762
|
+
};
|
|
763
|
+
const readDisplaySettings = (projectKey) => {
|
|
764
|
+
const store = displayStoreOf();
|
|
765
|
+
const next = { ...DISPLAY_DEFAULTS };
|
|
766
|
+
if (!store) return next;
|
|
767
|
+
try {
|
|
768
|
+
const raw = store.getItem(displayKeyOf(projectKey));
|
|
769
|
+
if (!raw) return next;
|
|
770
|
+
const saved = JSON.parse(raw);
|
|
771
|
+
if (saved && typeof saved === 'object') {
|
|
772
|
+
next.graph = saved.graph === true;
|
|
773
|
+
next.palace = saved.palace === true;
|
|
774
|
+
}
|
|
775
|
+
} catch {
|
|
776
|
+
return next;
|
|
777
|
+
}
|
|
778
|
+
return next;
|
|
779
|
+
};
|
|
780
|
+
const writeDisplaySettings = (projectKey, next) => {
|
|
781
|
+
const store = displayStoreOf();
|
|
782
|
+
if (!store) return false;
|
|
783
|
+
try {
|
|
784
|
+
store.setItem(displayKeyOf(projectKey), JSON.stringify({
|
|
785
|
+
graph: next && next.graph === true,
|
|
786
|
+
palace: next && next.palace === true,
|
|
787
|
+
}));
|
|
788
|
+
return true;
|
|
789
|
+
} catch {
|
|
790
|
+
return false;
|
|
791
|
+
}
|
|
792
|
+
};
|
|
793
|
+
/**
|
|
794
|
+
* 解释模式只在本机、按会话记;默认关,零命中原因不打扰。
|
|
795
|
+
* 面板开关与对话末尾共用同一条广播。
|
|
796
|
+
*/
|
|
797
|
+
const explainBus = (() => {
|
|
798
|
+
const key = '__liteMemoryExplainBus'; // ui-copy-ignore
|
|
799
|
+
if (!(globalThis[key] instanceof Set)) globalThis[key] = new Set();
|
|
800
|
+
return globalThis[key];
|
|
801
|
+
})();
|
|
802
|
+
const explainKeyOf = (sessionId) => `lite-memory:explain:${sessionId || 'default'}`;
|
|
803
|
+
const explainModeOf = (sessionId) => {
|
|
804
|
+
const store = displayStoreOf();
|
|
805
|
+
if (!store) return false;
|
|
806
|
+
try {
|
|
807
|
+
return store.getItem(explainKeyOf(sessionId)) === '1';
|
|
808
|
+
} catch {
|
|
809
|
+
return false;
|
|
810
|
+
}
|
|
811
|
+
};
|
|
812
|
+
const setExplainMode = (sessionId, on) => {
|
|
813
|
+
const store = displayStoreOf();
|
|
814
|
+
if (!store) return false;
|
|
815
|
+
try {
|
|
816
|
+
store.setItem(explainKeyOf(sessionId), on === true ? '1' : '0');
|
|
817
|
+
} catch {
|
|
818
|
+
return false;
|
|
819
|
+
}
|
|
820
|
+
for (const listener of [...explainBus]) {
|
|
821
|
+
try {
|
|
822
|
+
listener();
|
|
823
|
+
} catch {
|
|
824
|
+
// 单个订阅坏掉不影响其它面板。
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
return true;
|
|
828
|
+
};
|
|
829
|
+
const watchExplain = (listener) => {
|
|
830
|
+
explainBus.add(listener);
|
|
831
|
+
return () => explainBus.delete(listener);
|
|
832
|
+
};
|
|
833
|
+
/** 功能关闭时视图统一回列表,不保留一个画不出来的页签。 */
|
|
834
|
+
const graphViewOf = (enabled, view) => (enabled === true && view === 'graph' ? 'graph' : 'list');
|
|
835
|
+
const viewModeOf = (display, view) => {
|
|
836
|
+
if (view === 'graph' && display && display.graph === true) return 'graph';
|
|
837
|
+
if (view === 'palace' && display && display.palace === true) return 'palace';
|
|
838
|
+
return 'list';
|
|
839
|
+
};
|
|
840
|
+
const reduceMotionOf = () => {
|
|
841
|
+
try {
|
|
842
|
+
const query = globalThis.matchMedia && globalThis.matchMedia('(prefers-reduced-motion: reduce)');
|
|
843
|
+
return Boolean(query && query.matches === true);
|
|
844
|
+
} catch {
|
|
845
|
+
return false;
|
|
846
|
+
}
|
|
847
|
+
};
|
|
848
|
+
/** 跟随系统偏好切换,打开面板期间改设置也能立即生效。 */
|
|
849
|
+
const useReducedMotion = () => {
|
|
850
|
+
const [reduced, setReduced] = React.useState(reduceMotionOf);
|
|
851
|
+
React.useEffect(() => {
|
|
852
|
+
let query = null;
|
|
853
|
+
try {
|
|
854
|
+
query = globalThis.matchMedia && globalThis.matchMedia('(prefers-reduced-motion: reduce)');
|
|
855
|
+
} catch {
|
|
856
|
+
query = null;
|
|
857
|
+
}
|
|
858
|
+
if (!query || typeof query.addEventListener !== 'function') return undefined;
|
|
859
|
+
const update = () => setReduced(query.matches === true);
|
|
860
|
+
query.addEventListener('change', update);
|
|
861
|
+
return () => query.removeEventListener('change', update);
|
|
862
|
+
}, []);
|
|
863
|
+
return reduced;
|
|
864
|
+
};
|
|
865
|
+
|
|
866
|
+
/** 同标题(去空白后)出现几次;体检与重复标题筛选共用。 */
|
|
867
|
+
const titleCountsOf = (records) => {
|
|
868
|
+
const counts = new Map();
|
|
869
|
+
(Array.isArray(records) ? records : EMPTY).forEach((record) => {
|
|
870
|
+
const title = typeof record.title === 'string' ? record.title.trim() : '';
|
|
871
|
+
if (title) counts.set(title, (counts.get(title) || 0) + 1);
|
|
872
|
+
});
|
|
873
|
+
return counts;
|
|
874
|
+
};
|
|
875
|
+
/** 标签/话题按精确值筛选;只有自由搜索才用包含匹配。 */
|
|
876
|
+
const filterRecords = (records, query, hub) => {
|
|
877
|
+
const rows = Array.isArray(records) ? records : EMPTY;
|
|
878
|
+
const kind = hub && typeof hub.kind === 'string' ? hub.kind : '';
|
|
879
|
+
const value = hub && typeof hub.value === 'string' ? hub.value : '';
|
|
880
|
+
if (kind === 'tag' && value) {
|
|
881
|
+
return rows.filter((record) => Array.isArray(record.tags) && record.tags.includes(value));
|
|
882
|
+
}
|
|
883
|
+
if (kind === 'topic' && value) {
|
|
884
|
+
return rows.filter((record) => record.topicKey === value);
|
|
885
|
+
}
|
|
886
|
+
if (kind === 'duplicate' || kind === 'unread' || kind === 'noWhen') {
|
|
887
|
+
const counts = titleCountsOf(rows);
|
|
888
|
+
return rows.filter((record) => {
|
|
889
|
+
if (kind === 'unread') return !record.lastUsed;
|
|
890
|
+
if (kind === 'noWhen') return !record.when;
|
|
891
|
+
const title = typeof record.title === 'string' ? record.title.trim() : '';
|
|
892
|
+
return Boolean(title) && counts.get(title) > 1;
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
const needle = typeof query === 'string' ? query.trim().toLowerCase() : '';
|
|
896
|
+
if (!needle) return rows;
|
|
897
|
+
return rows.filter((record) => {
|
|
898
|
+
const tags = Array.isArray(record.tags) ? record.tags.join(' ') : '';
|
|
899
|
+
return `${record.title || ''} ${record.summary || ''} ${tags} ${record.topicKey || ''} ${KIND_LABEL[record.kind] || ''}`.toLowerCase().includes(needle);
|
|
900
|
+
});
|
|
901
|
+
};
|
|
902
|
+
|
|
903
|
+
/** 结构体检:只看现行记录,缺的数据给 null,不猜。 */
|
|
904
|
+
const checkupOf = (records, scopes, globalSelected) => {
|
|
905
|
+
const rows = Array.isArray(records) ? records : EMPTY;
|
|
906
|
+
const counts = titleCountsOf(rows);
|
|
907
|
+
let duplicate = 0;
|
|
908
|
+
counts.forEach((count) => { if (count > 1) duplicate += 1; });
|
|
909
|
+
const globalRow = (Array.isArray(scopes) ? scopes : EMPTY).find((scope) => scope && scope.kind === 'global');
|
|
910
|
+
return {
|
|
911
|
+
duplicate,
|
|
912
|
+
unread: rows.filter((record) => !record.lastUsed).length,
|
|
913
|
+
globalEntries: globalRow && Number.isInteger(globalRow.entries) ? globalRow.entries : null,
|
|
914
|
+
globalMissingWhen: globalSelected ? rows.filter((record) => !record.when).length : null,
|
|
915
|
+
};
|
|
916
|
+
};
|
|
917
|
+
|
|
918
|
+
/** 记忆宫殿:房间固定,槽位固定,换项目只换陈列内容。 */
|
|
919
|
+
const PALACE_PAGE_SIZE = 24;
|
|
920
|
+
const PALACE_PREVIEW = 4;
|
|
921
|
+
const PALACE_ROOMS = Object.freeze([
|
|
922
|
+
Object.freeze({ id: 'habits', name: '习惯与规范', kinds: Object.freeze(['preference', 'convention']) }),
|
|
923
|
+
Object.freeze({ id: 'methods', name: '方法与经验', kinds: Object.freeze(['procedure', 'pitfall']) }),
|
|
924
|
+
Object.freeze({ id: 'entries', name: '资料与入口', kinds: Object.freeze(['pointer', 'credential_hint']) }),
|
|
925
|
+
Object.freeze({ id: 'notes', name: '其他记录', kinds: Object.freeze(['note']) }),
|
|
926
|
+
]);
|
|
927
|
+
const PALACE_ROOM_OF_KIND = Object.freeze({
|
|
928
|
+
preference: 'habits', convention: 'habits',
|
|
929
|
+
procedure: 'methods', pitfall: 'methods',
|
|
930
|
+
pointer: 'entries', credential_hint: 'entries',
|
|
931
|
+
note: 'notes',
|
|
932
|
+
});
|
|
933
|
+
const palaceRoomIdOf = (kind) => PALACE_ROOM_OF_KIND[kind] || 'notes';
|
|
934
|
+
const palaceRoomsOf = (records) => {
|
|
935
|
+
const rows = Array.isArray(records) ? records : EMPTY;
|
|
936
|
+
return PALACE_ROOMS.map((room) => {
|
|
937
|
+
const items = rows.filter((record) => palaceRoomIdOf(record && record.kind) === room.id);
|
|
938
|
+
return { id: room.id, name: room.name, items, total: items.length, pages: Math.max(1, Math.ceil(items.length / PALACE_PAGE_SIZE)) };
|
|
939
|
+
});
|
|
940
|
+
};
|
|
941
|
+
const palaceRoomPageOf = (records, roomId, page) => {
|
|
942
|
+
const room = palaceRoomsOf(records).find((row) => row.id === roomId);
|
|
943
|
+
if (!room) return { items: EMPTY, total: 0, pages: 0, page: 1 };
|
|
944
|
+
const current = Math.min(room.pages, Math.max(1, Number.isFinite(page) ? Math.floor(page) : 1));
|
|
945
|
+
return {
|
|
946
|
+
items: room.items.slice((current - 1) * PALACE_PAGE_SIZE, current * PALACE_PAGE_SIZE),
|
|
947
|
+
total: room.total, pages: room.pages, page: current,
|
|
948
|
+
};
|
|
949
|
+
};
|
|
950
|
+
/** 页码最多铺 7 个,当前页始终在窗口里。 */
|
|
951
|
+
const palacePageWindow = (page, pages) => {
|
|
952
|
+
const total = Math.max(1, Math.floor(Number.isFinite(pages) ? pages : 1));
|
|
953
|
+
const current = Math.min(total, Math.max(1, Math.floor(Number.isFinite(page) ? page : 1)));
|
|
954
|
+
if (total <= 7) return Array.from({ length: total }, (_, index) => index + 1);
|
|
955
|
+
const start = Math.max(1, Math.min(current - 3, total - 6));
|
|
956
|
+
return Array.from({ length: 7 }, (_, index) => start + index);
|
|
957
|
+
};
|
|
958
|
+
const palaceToggleOf = (expanded, roomId) => (expanded === roomId ? '' : roomId);
|
|
959
|
+
const palaceMotionOf = (reduced) => ({ duration: reduced === true ? 0 : 180, easing: 'cubic-bezier(0.2, 0.7, 0.3, 1)' });
|
|
960
|
+
|
|
961
|
+
/** 失败回执分「当前」和「过去」:当前失败要修,过去的只说明发生过。 */
|
|
962
|
+
const failureStateOf = (evolution) => {
|
|
963
|
+
const rows = evolution && evolution.economy && Array.isArray(evolution.economy.recent) ? evolution.economy.recent : EMPTY;
|
|
964
|
+
if (rows.length === 0) return '';
|
|
965
|
+
if (rows[0] && rows[0].status === 'failed') return 'current';
|
|
966
|
+
return rows.slice(1).some((row) => row && row.status === 'failed') ? 'past' : '';
|
|
967
|
+
};
|
|
968
|
+
|
|
453
969
|
/** 面板开关。 */
|
|
454
970
|
const panelUi = { open: false, sessionId: '' };
|
|
455
971
|
const panelListeners = new Set();
|
|
@@ -534,74 +1050,124 @@ window.__ModuleLoader__.load({
|
|
|
534
1050
|
|
|
535
1051
|
/**
|
|
536
1052
|
* 图谱只画真实关系:显式关联,以及记录自己的标签 / 主题。
|
|
537
|
-
*
|
|
1053
|
+
* 标签 / 主题是组:枢纽画大点,成员围在它周围画小点。
|
|
1054
|
+
* 位置由 id 与成员关系决定,同一份数据每次打开都在同一处。
|
|
538
1055
|
*/
|
|
539
|
-
/** 位置由 id 决定;同一份数据每次打开都在同一处。 */
|
|
540
1056
|
const layoutGraph = (nodes, edges, primaryKind) => {
|
|
1057
|
+
const kindOf = new Map(nodes.map((node) => [node.id, node.kind]));
|
|
541
1058
|
const degree = new Map();
|
|
1059
|
+
const linkedOf = new Map();
|
|
1060
|
+
const membersOf = new Map();
|
|
1061
|
+
const addLink = (primaryId, hubId) => {
|
|
1062
|
+
if (!linkedOf.has(primaryId)) linkedOf.set(primaryId, new Set());
|
|
1063
|
+
linkedOf.get(primaryId).add(hubId);
|
|
1064
|
+
if (!membersOf.has(hubId)) membersOf.set(hubId, new Set());
|
|
1065
|
+
membersOf.get(hubId).add(primaryId);
|
|
1066
|
+
};
|
|
542
1067
|
for (const edge of edges) {
|
|
543
1068
|
degree.set(edge.from, (degree.get(edge.from) || 0) + 1);
|
|
544
1069
|
degree.set(edge.to, (degree.get(edge.to) || 0) + 1);
|
|
1070
|
+
if (kindOf.get(edge.from) === primaryKind && kindOf.get(edge.to) !== primaryKind) addLink(edge.from, edge.to);
|
|
1071
|
+
if (kindOf.get(edge.to) === primaryKind && kindOf.get(edge.from) !== primaryKind) addLink(edge.to, edge.from);
|
|
545
1072
|
}
|
|
546
|
-
const placed = new Map();
|
|
547
1073
|
const primaries = nodes.filter((node) => node.kind === primaryKind);
|
|
548
|
-
const count = primaries.length;
|
|
549
|
-
const spread = 0.55 + Math.min(0.45, count * 0.06);
|
|
550
|
-
primaries.forEach((node, index) => {
|
|
551
|
-
const seed = hashOf(node.id);
|
|
552
|
-
const ring = count <= 1 ? 0 : count <= 8 ? 128 : count <= 18 ? 168 : 206;
|
|
553
|
-
const wobble = count <= 8 ? 1 : 0.64 + ((seed % 100) / 100) * 0.36;
|
|
554
|
-
const angle = count <= 1 ? 0 : (index / count) * Math.PI * 2 - Math.PI / 2 + ((seed % 17) / 17) * 0.16;
|
|
555
|
-
placed.set(node.id, {
|
|
556
|
-
...node,
|
|
557
|
-
x: 320 + Math.cos(angle) * ring * 1.22 * wobble * spread,
|
|
558
|
-
y: 236 + Math.sin(angle) * ring * 0.8 * wobble * spread,
|
|
559
|
-
radius: 5.5 + Math.min(4, (degree.get(node.id) || 0) * 0.7),
|
|
560
|
-
});
|
|
561
|
-
});
|
|
562
1074
|
const hubs = nodes.filter((node) => node.kind !== primaryKind);
|
|
563
|
-
const
|
|
564
|
-
const linkedOf = new Map(hubs.map((node) => [node.id, new Set()]));
|
|
565
|
-
for (const edge of edges) {
|
|
566
|
-
if (linkedOf.has(edge.from) && kindOf.get(edge.to) === primaryKind) linkedOf.get(edge.from).add(edge.to);
|
|
567
|
-
if (linkedOf.has(edge.to) && kindOf.get(edge.from) === primaryKind) linkedOf.get(edge.to).add(edge.from);
|
|
568
|
-
}
|
|
1075
|
+
const weightOf = new Map(hubs.map((hub) => [hub.id, (membersOf.get(hub.id) || EMPTY).size]));
|
|
569
1076
|
const groups = new Map();
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
1077
|
+
const assignedHub = new Map();
|
|
1078
|
+
const loose = [];
|
|
1079
|
+
for (const node of primaries) {
|
|
1080
|
+
const options = [...(linkedOf.get(node.id) || EMPTY)]
|
|
1081
|
+
.filter((id) => (weightOf.get(id) || 0) > 1)
|
|
1082
|
+
.sort((a, b) => ((weightOf.get(b) || 0) - (weightOf.get(a) || 0)) || a.localeCompare(b));
|
|
1083
|
+
const hubId = options[0];
|
|
1084
|
+
if (!hubId) { loose.push(node); continue; }
|
|
1085
|
+
if (!groups.has(hubId)) groups.set(hubId, []);
|
|
1086
|
+
groups.get(hubId).push(node);
|
|
1087
|
+
assignedHub.set(node.id, hubId);
|
|
575
1088
|
}
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
const
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
1089
|
+
const placed = new Map();
|
|
1090
|
+
if (primaryKind === 'memory') {
|
|
1091
|
+
for (const [hubId, members] of [...groups]) {
|
|
1092
|
+
if (members.length > 1) continue;
|
|
1093
|
+
for (const member of members) {
|
|
1094
|
+
loose.push(member);
|
|
1095
|
+
assignedHub.delete(member.id);
|
|
1096
|
+
}
|
|
1097
|
+
groups.delete(hubId);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
const clusterList = [...groups.entries()]
|
|
1101
|
+
.sort((a, b) => (b[1].length - a[1].length) || a[0].localeCompare(b[0]));
|
|
1102
|
+
const groupCount = clusterList.length;
|
|
1103
|
+
const ring = groupCount <= 1 ? 0 : groupCount <= 4 ? 150 : groupCount <= 8 ? 182 : 212;
|
|
1104
|
+
const hubRadius = (size) => 8 + Math.min(10, Math.sqrt(size) * 3);
|
|
1105
|
+
const placePrimary = (node, x, y) => {
|
|
1106
|
+
placed.set(node.id, {
|
|
1107
|
+
...node, x, y,
|
|
1108
|
+
radius: 3.2 + Math.min(2.2, (degree.get(node.id) || 0) * 0.35),
|
|
1109
|
+
});
|
|
1110
|
+
};
|
|
1111
|
+
clusterList.forEach(([hubId, members], index) => {
|
|
1112
|
+
const seed = hashOf(hubId);
|
|
1113
|
+
const angle = groupCount <= 1
|
|
1114
|
+
? 0
|
|
1115
|
+
: (index / groupCount) * Math.PI * 2 - Math.PI / 2 + ((seed % 13) / 13) * 0.12;
|
|
1116
|
+
const cx = 320 + Math.cos(angle) * ring * 1.22;
|
|
1117
|
+
const cy = 236 + Math.sin(angle) * ring * 0.8;
|
|
1118
|
+
const hub = hubs.find((node) => node.id === hubId);
|
|
1119
|
+
const radius = hubRadius(members.length);
|
|
1120
|
+
placed.set(hubId, { ...hub, x: cx, y: cy, radius });
|
|
1121
|
+
const orbit = radius + 20 + Math.sqrt(members.length) * 13;
|
|
1122
|
+
const start = ((seed % 360) / 180) * Math.PI;
|
|
1123
|
+
members.forEach((node, memberIndex) => {
|
|
1124
|
+
const step = (memberIndex / members.length) * Math.PI * 2;
|
|
1125
|
+
const wobble = 0.88 + ((hashOf(node.id) % 25) / 100);
|
|
1126
|
+
placePrimary(node, cx + Math.cos(start + step) * orbit * wobble, cy + Math.sin(start + step) * orbit * 0.82 * wobble);
|
|
1127
|
+
});
|
|
1128
|
+
});
|
|
1129
|
+
if (loose.length > 0) {
|
|
1130
|
+
const orbit = groupCount === 0
|
|
1131
|
+
? (loose.length <= 1 ? 0 : loose.length <= 8 ? 128 : loose.length <= 18 ? 168 : 206)
|
|
1132
|
+
: ring + 96;
|
|
1133
|
+
loose.forEach((node, index) => {
|
|
582
1134
|
const seed = hashOf(node.id);
|
|
583
|
-
const
|
|
584
|
-
?
|
|
585
|
-
: ((seed %
|
|
586
|
-
const
|
|
587
|
-
|
|
588
|
-
placed.set(node.id, {
|
|
589
|
-
...node,
|
|
590
|
-
x: cx + Math.cos(angle) * distance,
|
|
591
|
-
y: cy + Math.sin(angle) * distance * 0.82,
|
|
592
|
-
radius: node.kind === 'topic' ? 5 : 4,
|
|
593
|
-
});
|
|
1135
|
+
const angle = loose.length <= 1
|
|
1136
|
+
? 0
|
|
1137
|
+
: (index / loose.length) * Math.PI * 2 - Math.PI / 2 + ((seed % 17) / 17) * 0.14;
|
|
1138
|
+
const wobble = loose.length <= 8 ? 1 : 0.64 + ((seed % 100) / 100) * 0.36;
|
|
1139
|
+
placePrimary(node, 320 + Math.cos(angle) * orbit * 1.22 * wobble, 236 + Math.sin(angle) * orbit * 0.8 * wobble);
|
|
594
1140
|
});
|
|
595
1141
|
}
|
|
596
1142
|
return {
|
|
597
1143
|
nodes: [...placed.values()],
|
|
598
|
-
edges: edges.filter((edge) =>
|
|
599
|
-
|
|
1144
|
+
edges: edges.filter((edge) => {
|
|
1145
|
+
if (!placed.has(edge.from) || !placed.has(edge.to)) return false;
|
|
1146
|
+
const fromPrimary = kindOf.get(edge.from) === primaryKind;
|
|
1147
|
+
const toPrimary = kindOf.get(edge.to) === primaryKind;
|
|
1148
|
+
if (fromPrimary && toPrimary) return true;
|
|
1149
|
+
if (primaryKind !== 'memory') return true;
|
|
1150
|
+
if (fromPrimary) return assignedHub.get(edge.from) === edge.to;
|
|
1151
|
+
if (toPrimary) return assignedHub.get(edge.to) === edge.from;
|
|
1152
|
+
return false;
|
|
1153
|
+
}),
|
|
1154
|
+
people: primaries.length,
|
|
600
1155
|
};
|
|
601
1156
|
};
|
|
602
1157
|
|
|
603
|
-
|
|
604
|
-
|
|
1158
|
+
/** 60 条投影之外也要保留当前选中项,点开的节点不能从图里消失。 */
|
|
1159
|
+
const projectionOf = (all, selectedId) => {
|
|
1160
|
+
const rows = all.slice(0, 60);
|
|
1161
|
+
if (selectedId && !rows.some((row) => row.id === selectedId)) {
|
|
1162
|
+
const selected = all.find((row) => row.id === selectedId);
|
|
1163
|
+
if (selected) rows[rows.length - 1] = selected;
|
|
1164
|
+
}
|
|
1165
|
+
return rows;
|
|
1166
|
+
};
|
|
1167
|
+
|
|
1168
|
+
const graphOf = (records, selectedId) => {
|
|
1169
|
+
const all = Array.isArray(records) ? records : EMPTY;
|
|
1170
|
+
const rows = projectionOf(all, selectedId);
|
|
605
1171
|
const byId = new Map(rows.map((record) => [record.id, record]));
|
|
606
1172
|
const tagCount = new Map();
|
|
607
1173
|
const topicCount = new Map();
|
|
@@ -609,7 +1175,7 @@ window.__ModuleLoader__.load({
|
|
|
609
1175
|
for (const tag of Array.isArray(record.tags) ? record.tags : EMPTY) tagCount.set(tag, (tagCount.get(tag) || 0) + 1);
|
|
610
1176
|
if (record.topicKey) topicCount.set(record.topicKey, (topicCount.get(record.topicKey) || 0) + 1);
|
|
611
1177
|
}
|
|
612
|
-
const tags = [...tagCount.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 8);
|
|
1178
|
+
const tags = [...tagCount.entries()].filter(([, count]) => count > 1).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 8);
|
|
613
1179
|
const topics = [...topicCount.entries()].filter(([, count]) => count > 1).sort((a, b) => b[1] - a[1]).slice(0, 4);
|
|
614
1180
|
const nodes = [];
|
|
615
1181
|
const edges = [];
|
|
@@ -618,7 +1184,7 @@ window.__ModuleLoader__.load({
|
|
|
618
1184
|
edges.push({ from, to, type });
|
|
619
1185
|
};
|
|
620
1186
|
for (const record of rows) {
|
|
621
|
-
nodes.push({ id: record.id, kind: 'memory', label: record.title ||
|
|
1187
|
+
nodes.push({ id: record.id, kind: 'memory', label: record.title || '记忆', experimental: record.experimental === true });
|
|
622
1188
|
}
|
|
623
1189
|
for (const [tag, count] of tags) {
|
|
624
1190
|
nodes.push({ id: `tag:${tag}`, kind: 'tag', label: `#${tag}`, count });
|
|
@@ -637,12 +1203,17 @@ window.__ModuleLoader__.load({
|
|
|
637
1203
|
if (byId.has(link.id)) addEdge(record.id, link.id, 'link');
|
|
638
1204
|
}
|
|
639
1205
|
}
|
|
640
|
-
return
|
|
1206
|
+
return {
|
|
1207
|
+
...layoutGraph(nodes, edges, 'memory'),
|
|
1208
|
+
shown: rows.length,
|
|
1209
|
+
total: all.length,
|
|
1210
|
+
};
|
|
641
1211
|
};
|
|
642
1212
|
|
|
643
1213
|
/** 经验图谱只连共享条件。 */
|
|
644
|
-
const ruleGraphOf = (rules) => {
|
|
645
|
-
const
|
|
1214
|
+
const ruleGraphOf = (rules, selectedId) => {
|
|
1215
|
+
const all = Array.isArray(rules) ? rules : EMPTY;
|
|
1216
|
+
const rows = projectionOf(all, selectedId);
|
|
646
1217
|
const conditionCount = new Map();
|
|
647
1218
|
for (const rule of rows) {
|
|
648
1219
|
const seen = new Set();
|
|
@@ -679,34 +1250,134 @@ window.__ModuleLoader__.load({
|
|
|
679
1250
|
}
|
|
680
1251
|
}
|
|
681
1252
|
}
|
|
682
|
-
return
|
|
1253
|
+
return {
|
|
1254
|
+
...layoutGraph(nodes, edges, 'rule'),
|
|
1255
|
+
shown: rows.length,
|
|
1256
|
+
total: all.length,
|
|
1257
|
+
};
|
|
683
1258
|
};
|
|
684
1259
|
|
|
685
1260
|
/** 关系图只画真实关系;节点沿用原始 id,位置由 id 决定。 */
|
|
1261
|
+
/** 图谱缩放:28px 命中区,悬停/聚焦才给底色。 */
|
|
1262
|
+
const ZoomButton = (props) => {
|
|
1263
|
+
const [hover, setHover] = React.useState(false);
|
|
1264
|
+
return h('button', {
|
|
1265
|
+
type: 'button', onClick: props.onClick, disabled: props.disabled,
|
|
1266
|
+
'aria-label': props.label, title: props.label,
|
|
1267
|
+
onMouseEnter: () => setHover(true),
|
|
1268
|
+
onMouseLeave: () => setHover(false),
|
|
1269
|
+
onFocus: () => setHover(true),
|
|
1270
|
+
onBlur: () => setHover(false),
|
|
1271
|
+
style: {
|
|
1272
|
+
width: '28px', height: '28px', padding: 0, display: 'inline-flex',
|
|
1273
|
+
alignItems: 'center', justifyContent: 'center', border: 'none',
|
|
1274
|
+
borderRadius: '8px', background: hover && !props.disabled ? 'var(--dsw-alias-bg-layer-2)' : 'transparent',
|
|
1275
|
+
color: 'inherit', opacity: props.disabled ? 0.35 : hover ? 0.95 : 0.72,
|
|
1276
|
+
cursor: props.disabled ? 'default' : 'pointer',
|
|
1277
|
+
},
|
|
1278
|
+
}, props.children);
|
|
1279
|
+
};
|
|
1280
|
+
|
|
686
1281
|
const MemoryGraph = (props) => {
|
|
687
1282
|
const primaryKind = props.primaryKind || 'memory';
|
|
688
|
-
const
|
|
1283
|
+
const selectedId = props.selectedId;
|
|
1284
|
+
const graph = React.useMemo(() => (props.graphOf || graphOf)(props.records, selectedId), [props.records, props.graphOf, selectedId]);
|
|
689
1285
|
const [zoom, setZoom] = React.useState(1);
|
|
1286
|
+
const [pan, setPan] = React.useState({ x: 0, y: 0 });
|
|
1287
|
+
const [dragging, setDragging] = React.useState(false);
|
|
690
1288
|
const [hoveredId, setHoveredId] = React.useState('');
|
|
691
|
-
const
|
|
1289
|
+
const svgRef = React.useRef(null);
|
|
1290
|
+
const dragRef = React.useRef(null);
|
|
692
1291
|
const labelSet = graph.people <= 40;
|
|
1292
|
+
const hubCount = graph.nodes.length - graph.people;
|
|
1293
|
+
const shownCount = Number.isInteger(graph.shown) ? graph.shown : graph.people;
|
|
1294
|
+
const totalCount = Number.isInteger(props.total) && props.total >= shownCount
|
|
1295
|
+
? props.total
|
|
1296
|
+
: (Number.isInteger(graph.total) ? graph.total : shownCount);
|
|
1297
|
+
const truncated = totalCount > shownCount;
|
|
1298
|
+
const nearIds = React.useMemo(() => {
|
|
1299
|
+
const set = new Set();
|
|
1300
|
+
if (!selectedId) return set;
|
|
1301
|
+
set.add(selectedId);
|
|
1302
|
+
for (const edge of graph.edges) {
|
|
1303
|
+
if (edge.from === selectedId) set.add(edge.to);
|
|
1304
|
+
if (edge.to === selectedId) set.add(edge.from);
|
|
1305
|
+
}
|
|
1306
|
+
return set;
|
|
1307
|
+
}, [graph.edges, selectedId]);
|
|
1308
|
+
const orderedEdges = selectedId
|
|
1309
|
+
? [...graph.edges].sort((a, b) => Number(a.from === selectedId || a.to === selectedId) - Number(b.from === selectedId || b.to === selectedId))
|
|
1310
|
+
: graph.edges;
|
|
1311
|
+
const orderedNodes = selectedId
|
|
1312
|
+
? [...graph.nodes].sort((a, b) => (Number(nearIds.has(a.id)) - Number(nearIds.has(b.id))) || (Number(a.id === selectedId) - Number(b.id === selectedId)))
|
|
1313
|
+
: graph.nodes;
|
|
693
1314
|
const pickHub = props.onPickHub;
|
|
694
|
-
const
|
|
1315
|
+
const MIN_ZOOM = 0.2;
|
|
1316
|
+
const MAX_ZOOM = 2.5;
|
|
1317
|
+
const stepZoom = (delta) => setZoom((value) => {
|
|
1318
|
+
const next = delta > 0 ? value * 1.25 : value / 1.25;
|
|
1319
|
+
return Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, Math.round(next * 100) / 100));
|
|
1320
|
+
});
|
|
1321
|
+
const pointAt = (event, matrix) => {
|
|
1322
|
+
const svg = svgRef.current;
|
|
1323
|
+
if (!svg || !matrix) return null;
|
|
1324
|
+
const point = svg.createSVGPoint();
|
|
1325
|
+
point.x = event.clientX;
|
|
1326
|
+
point.y = event.clientY;
|
|
1327
|
+
return point.matrixTransform(matrix);
|
|
1328
|
+
};
|
|
1329
|
+
const startPan = (event) => {
|
|
1330
|
+
if (event.button !== 0) return;
|
|
1331
|
+
if (event.target && event.target.closest && event.target.closest('[data-node]')) return;
|
|
1332
|
+
const svg = svgRef.current;
|
|
1333
|
+
if (!svg) return;
|
|
1334
|
+
const ctm = svg.getScreenCTM();
|
|
1335
|
+
if (!ctm) return;
|
|
1336
|
+
const inverse = ctm.inverse();
|
|
1337
|
+
const point = pointAt(event, inverse);
|
|
1338
|
+
if (!point) return;
|
|
1339
|
+
dragRef.current = [event.pointerId, inverse, point.x, point.y, pan.x, pan.y];
|
|
1340
|
+
setDragging(true);
|
|
1341
|
+
if (svg.setPointerCapture) svg.setPointerCapture(event.pointerId);
|
|
1342
|
+
event.preventDefault();
|
|
1343
|
+
};
|
|
1344
|
+
const movePan = (event) => {
|
|
1345
|
+
const drag = dragRef.current;
|
|
1346
|
+
if (!drag || drag[0] !== event.pointerId) return;
|
|
1347
|
+
const point = pointAt(event, drag[1]);
|
|
1348
|
+
if (!point) return;
|
|
1349
|
+
setPan({ x: drag[4] - (point.x - drag[2]), y: drag[5] - (point.y - drag[3]) });
|
|
1350
|
+
event.preventDefault();
|
|
1351
|
+
};
|
|
1352
|
+
const endPan = (event) => {
|
|
1353
|
+
const drag = dragRef.current;
|
|
1354
|
+
if (!drag || (event && drag[0] !== event.pointerId)) return;
|
|
1355
|
+
dragRef.current = null;
|
|
1356
|
+
setDragging(false);
|
|
1357
|
+
const svg = svgRef.current;
|
|
1358
|
+
if (event && svg && svg.releasePointerCapture && svg.hasPointerCapture && svg.hasPointerCapture(event.pointerId)) {
|
|
1359
|
+
svg.releasePointerCapture(event.pointerId);
|
|
1360
|
+
}
|
|
1361
|
+
};
|
|
1362
|
+
React.useEffect(() => {
|
|
1363
|
+
setPan({ x: 0, y: 0 });
|
|
1364
|
+
}, [props.records, primaryKind]);
|
|
695
1365
|
const frame = React.useMemo(() => {
|
|
696
1366
|
const list = graph.nodes;
|
|
697
1367
|
if (list.length === 0) return { x: 0, y: 0, width: 640, height: 472 };
|
|
698
1368
|
let minX = Infinity; let maxX = -Infinity; let minY = Infinity; let maxY = -Infinity;
|
|
699
1369
|
for (const node of list) {
|
|
700
|
-
const
|
|
1370
|
+
const wide = node.kind === primaryKind && (graph.people <= 12 || hubCount === 0 || node.id === selectedId);
|
|
1371
|
+
const half = node.kind === primaryKind ? (wide ? 62 : 54) : 38;
|
|
701
1372
|
minX = Math.min(minX, node.x - half);
|
|
702
1373
|
maxX = Math.max(maxX, node.x + half);
|
|
703
1374
|
minY = Math.min(minY, node.y - 16);
|
|
704
|
-
maxY = Math.max(maxY, node.y + (node.kind === primaryKind ? 28 :
|
|
1375
|
+
maxY = Math.max(maxY, node.y + (node.kind === primaryKind ? (wide ? 30 : 28) : 26));
|
|
705
1376
|
}
|
|
706
1377
|
const width = Math.max(380, maxX - minX + 36);
|
|
707
1378
|
const height = Math.max(250, maxY - minY + 36);
|
|
708
1379
|
return { x: (minX + maxX) / 2 - width / 2, y: (minY + maxY) / 2 - height / 2, width, height };
|
|
709
|
-
}, [graph, primaryKind]);
|
|
1380
|
+
}, [graph, primaryKind, selectedId]);
|
|
710
1381
|
if (!props.records || props.records.length === 0) {
|
|
711
1382
|
return h('div', {
|
|
712
1383
|
style: { flex: 1, minHeight: '220px', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--dsw-alias-label-secondary)', fontSize: '12px' },
|
|
@@ -714,102 +1385,184 @@ window.__ModuleLoader__.load({
|
|
|
714
1385
|
}
|
|
715
1386
|
const viewWidth = frame.width / zoom;
|
|
716
1387
|
const viewHeight = frame.height / zoom;
|
|
717
|
-
const zoomButton = (label, delta, icon) => h(
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
'aria-label': label, title: label,
|
|
721
|
-
style: {
|
|
722
|
-
width: '26px', height: '26px', padding: 0, display: 'inline-flex',
|
|
723
|
-
alignItems: 'center', justifyContent: 'center', border: 'none',
|
|
724
|
-
borderRadius: '6px', background: 'transparent', color: 'inherit',
|
|
725
|
-
opacity: delta > 0 ? (zoom >= 2.5 ? 0.28 : 0.68) : (zoom <= 0.75 ? 0.28 : 0.68),
|
|
726
|
-
cursor: 'pointer',
|
|
727
|
-
},
|
|
1388
|
+
const zoomButton = (label, delta, icon) => h(ZoomButton, {
|
|
1389
|
+
key: label, label, disabled: delta > 0 ? zoom >= MAX_ZOOM : zoom <= MIN_ZOOM,
|
|
1390
|
+
onClick: () => stepZoom(delta),
|
|
728
1391
|
}, icon);
|
|
729
1392
|
const magnifier = (sign) => h('svg', {
|
|
730
1393
|
width: '15', height: '15', viewBox: '0 0 16 16', fill: 'none',
|
|
731
|
-
stroke: 'currentColor',
|
|
1394
|
+
stroke: 'currentColor', 'stroke-width': '1.4', 'stroke-linecap': 'round',
|
|
732
1395
|
'aria-hidden': 'true',
|
|
733
1396
|
},
|
|
734
1397
|
h('circle', { cx: '6.8', cy: '6.8', r: '4.5' }),
|
|
735
1398
|
h('line', { x1: '10.1', y1: '10.1', x2: '14', y2: '14' }),
|
|
736
1399
|
h('line', { x1: '4.8', y1: '6.8', x2: '8.8', y2: '6.8' }),
|
|
737
1400
|
sign > 0 ? h('line', { x1: '6.8', y1: '4.8', x2: '6.8', y2: '8.8' }) : null);
|
|
1401
|
+
/** 标签避让:hub 常显、选中/悬停优先,记忆标签按度数贪心排布,重叠就省掉(悬停仍出标题)。 */
|
|
1402
|
+
const labelLayout = React.useMemo(() => {
|
|
1403
|
+
const nodes = graph.nodes;
|
|
1404
|
+
const fsOf = (primary) => (primary ? 10.5 : 10);
|
|
1405
|
+
const widthOf = (text, fs) => {
|
|
1406
|
+
let width = 0;
|
|
1407
|
+
for (const ch of text) width += ch.charCodeAt(0) > 255 ? fs : fs * 0.56;
|
|
1408
|
+
return width;
|
|
1409
|
+
};
|
|
1410
|
+
const rank = (node) => (node.id === selectedId ? 0 : node.id === hoveredId ? 1
|
|
1411
|
+
: nearIds.has(node.id) ? 2 : node.kind === primaryKind ? 4 : 3);
|
|
1412
|
+
const boxes = [];
|
|
1413
|
+
const placed = new Map();
|
|
1414
|
+
const hits = (box) => boxes.some((other) => !(box.x2 < other.x1 || box.x1 > other.x2 || box.y2 < other.y1 || box.y1 > other.y2));
|
|
1415
|
+
const list = nodes.slice().sort((a, b) => rank(a) - rank(b) || (b.degree || 0) - (a.degree || 0));
|
|
1416
|
+
/** 条目多时不全标,用度数最高的十条记忆给方向感。 */
|
|
1417
|
+
const topIds = new Set(list.filter((node) => node.kind === primaryKind).slice(0, 10).map((node) => node.id));
|
|
1418
|
+
for (const node of list) {
|
|
1419
|
+
const isPrimary = node.kind === primaryKind;
|
|
1420
|
+
if (isPrimary && !labelSet && rank(node) > 2 && !topIds.has(node.id)) continue;
|
|
1421
|
+
const chars = rank(node) <= 1 ? 16 : isPrimary ? (labelSet ? 10 : 8) : 13;
|
|
1422
|
+
const text = SHORT(node.label, chars);
|
|
1423
|
+
const fs = fsOf(isPrimary);
|
|
1424
|
+
const width = widthOf(text, fs);
|
|
1425
|
+
const gap = isPrimary ? 12 : 13;
|
|
1426
|
+
let spot = null;
|
|
1427
|
+
for (const below of [true, false]) {
|
|
1428
|
+
const baseline = below ? node.y + node.radius + gap : node.y - node.radius - gap + fs;
|
|
1429
|
+
const box = { x1: node.x - width / 2 - 3, x2: node.x + width / 2 + 3, y1: baseline - fs - 2, y2: baseline + 3 };
|
|
1430
|
+
if (box.y1 < 3 || box.y2 > graph.height - 3) continue;
|
|
1431
|
+
if (hits(box)) continue;
|
|
1432
|
+
spot = { box, baseline, below };
|
|
1433
|
+
break;
|
|
1434
|
+
}
|
|
1435
|
+
if (!spot) continue;
|
|
1436
|
+
boxes.push(spot.box);
|
|
1437
|
+
placed.set(node.id, { text, isPrimary, baseline: spot.baseline, fs, width });
|
|
1438
|
+
}
|
|
1439
|
+
return placed;
|
|
1440
|
+
}, [graph, primaryKind, selectedId, hoveredId, labelSet, nearIds]);
|
|
738
1441
|
return h('div', { style: { position: 'relative', flex: 1, minHeight: 0, display: 'flex' } },
|
|
739
1442
|
h('div', {
|
|
740
1443
|
style: {
|
|
741
|
-
position: 'absolute', top: '
|
|
742
|
-
display: 'flex', alignItems: 'center', gap: '
|
|
1444
|
+
position: 'absolute', top: '8px', right: '8px', zIndex: 2,
|
|
1445
|
+
display: 'flex', alignItems: 'center', gap: '2px', padding: '2px',
|
|
743
1446
|
border: '1px solid var(--dsw-alias-border-l1)', borderRadius: '8px',
|
|
744
1447
|
background: 'var(--dsw-alias-bg-layer-1)',
|
|
745
1448
|
},
|
|
746
1449
|
},
|
|
747
1450
|
zoomButton('缩小图谱', -0.25, magnifier(-1)),
|
|
748
|
-
zoomButton('放大图谱', 0.25, magnifier(1))
|
|
1451
|
+
zoomButton('放大图谱', 0.25, magnifier(1)),
|
|
1452
|
+
h('button', {
|
|
1453
|
+
type: 'button', onClick: () => setZoom(1),
|
|
1454
|
+
'aria-label': '重置缩放', title: '重置缩放',
|
|
1455
|
+
style: {
|
|
1456
|
+
minWidth: '44px', height: '28px', padding: '0 6px', border: 'none', borderRadius: '8px',
|
|
1457
|
+
background: 'transparent', color: 'var(--dsw-alias-label-secondary)',
|
|
1458
|
+
fontSize: '11px', lineHeight: '28px', fontVariantNumeric: 'tabular-nums', cursor: 'pointer',
|
|
1459
|
+
},
|
|
1460
|
+
}, `${Math.round(zoom * 100)}%`)),
|
|
749
1461
|
h('svg', {
|
|
750
|
-
|
|
1462
|
+
ref: svgRef,
|
|
1463
|
+
viewBox: `${frame.x + frame.width / 2 - viewWidth / 2 + pan.x} ${frame.y + frame.height / 2 - viewHeight / 2 + pan.y} ${viewWidth} ${viewHeight}`,
|
|
751
1464
|
preserveAspectRatio: 'xMidYMid meet',
|
|
752
|
-
style: {
|
|
1465
|
+
style: {
|
|
1466
|
+
width: '100%', height: '100%', flex: 1, minHeight: 0, display: 'block',
|
|
1467
|
+
cursor: dragging ? 'grabbing' : 'grab', touchAction: 'none',
|
|
1468
|
+
userSelect: 'none', WebkitUserSelect: 'none',
|
|
1469
|
+
},
|
|
1470
|
+
onPointerDown: startPan,
|
|
1471
|
+
onPointerMove: movePan,
|
|
1472
|
+
onPointerUp: endPan,
|
|
1473
|
+
onPointerCancel: endPan,
|
|
1474
|
+
onLostPointerCapture: endPan,
|
|
1475
|
+
onDragStart: (event) => event.preventDefault(),
|
|
753
1476
|
role: 'img', 'aria-label': props.ariaLabel || '关系图',
|
|
754
1477
|
},
|
|
755
|
-
h('g', null,
|
|
1478
|
+
h('g', null, orderedEdges.map((edge, index) => {
|
|
756
1479
|
const from = graph.nodes.find((node) => node.id === edge.from);
|
|
757
1480
|
const to = graph.nodes.find((node) => node.id === edge.to);
|
|
758
1481
|
if (!from || !to) return null;
|
|
759
1482
|
const linked = selectedId && (edge.from === selectedId || edge.to === selectedId);
|
|
1483
|
+
const dimmed = selectedId && !nearIds.has(edge.from) && !nearIds.has(edge.to);
|
|
760
1484
|
return h('line', {
|
|
761
1485
|
key: `e${index}`, x1: from.x, y1: from.y, x2: to.x, y2: to.y,
|
|
762
1486
|
stroke: linked ? 'var(--dsw-alias-brand-primary)' : 'var(--dsw-alias-label-secondary)',
|
|
763
|
-
|
|
764
|
-
opacity: linked ? 0.
|
|
765
|
-
|
|
1487
|
+
'stroke-width': linked ? 1.4 : 1,
|
|
1488
|
+
opacity: linked ? 0.72 : dimmed ? 0.08 : edge.type === 'link' ? 0.38 : 0.16,
|
|
1489
|
+
'stroke-dasharray': edge.type === 'link' ? undefined : '2 4',
|
|
766
1490
|
});
|
|
767
1491
|
})),
|
|
768
|
-
h('g', null,
|
|
1492
|
+
h('g', null, orderedNodes.map((node) => {
|
|
769
1493
|
const isPrimary = node.kind === primaryKind;
|
|
770
1494
|
const active = node.id === selectedId;
|
|
771
1495
|
const hovered = node.id === hoveredId;
|
|
1496
|
+
const near = nearIds.has(node.id);
|
|
1497
|
+
const dimmed = Boolean(selectedId) && !near;
|
|
772
1498
|
const pick = isPrimary ? props.onPick : (pickHub || undefined);
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
const
|
|
1499
|
+
/** 记忆是主体:实心、可读;主题/标签 hub 是背景:空心、只留轮廓。 */
|
|
1500
|
+
const fill = active ? 'var(--dsw-alias-brand-primary)'
|
|
1501
|
+
: isPrimary ? 'var(--dsw-alias-label-secondary)'
|
|
1502
|
+
: 'none';
|
|
1503
|
+
const stroke = active ? 'none'
|
|
1504
|
+
: hovered || near ? 'var(--dsw-alias-brand-primary)'
|
|
1505
|
+
: node.experimental ? 'var(--dsw-alias-state-warn-primary)'
|
|
1506
|
+
: isPrimary ? 'none' : 'var(--dsw-alias-label-secondary)';
|
|
1507
|
+
const strokeWidth = !active && (hovered || near) ? 1.3 : node.experimental ? 1 : isPrimary ? 0 : 1;
|
|
1508
|
+
const label = labelLayout.get(node.id) || null;
|
|
778
1509
|
return h('g', {
|
|
779
1510
|
key: node.id,
|
|
1511
|
+
'data-node': 'true',
|
|
780
1512
|
transform: `translate(${node.x} ${node.y})`,
|
|
781
1513
|
onClick: pick ? () => pick(node) : undefined,
|
|
1514
|
+
onKeyDown: pick ? (event) => {
|
|
1515
|
+
if (event.key === 'Enter' || event.key === ' ' || event.key === 'Spacebar') {
|
|
1516
|
+
event.preventDefault();
|
|
1517
|
+
pick(node);
|
|
1518
|
+
}
|
|
1519
|
+
} : undefined,
|
|
1520
|
+
onFocus: () => setHoveredId(node.id),
|
|
1521
|
+
onBlur: () => setHoveredId((value) => (value === node.id ? '' : value)),
|
|
782
1522
|
onMouseEnter: () => setHoveredId(node.id),
|
|
783
1523
|
onMouseLeave: () => setHoveredId((value) => (value === node.id ? '' : value)),
|
|
784
|
-
|
|
1524
|
+
tabIndex: pick ? 0 : undefined,
|
|
1525
|
+
role: pick ? 'button' : undefined,
|
|
1526
|
+
focusable: pick ? 'true' : 'false',
|
|
1527
|
+
'aria-label': pick ? (node.title || node.label) : undefined,
|
|
1528
|
+
style: { cursor: pick ? 'pointer' : 'default', outline: 'none' },
|
|
785
1529
|
},
|
|
786
|
-
h('circle', { r: Math.max(
|
|
787
|
-
active ? h('circle', { r: node.radius + 5, fill: 'none', stroke: 'var(--dsw-alias-brand-primary)',
|
|
1530
|
+
h('circle', { r: Math.max(isPrimary ? 14 : 18, node.radius + 6), fill: 'transparent' }),
|
|
1531
|
+
active ? h('circle', { r: node.radius + 5, fill: 'none', stroke: 'var(--dsw-alias-brand-primary)', 'stroke-width': 1.4, opacity: 0.45 }) : null,
|
|
788
1532
|
h('circle', {
|
|
789
1533
|
r: node.radius, fill,
|
|
790
|
-
opacity: active ? 1 : isPrimary ? 0.
|
|
791
|
-
stroke
|
|
792
|
-
strokeWidth: hovered ? 1.2 : 0,
|
|
1534
|
+
opacity: active ? 1 : dimmed ? (isPrimary ? 0.32 : 0.14) : isPrimary ? 0.92 : 0.34,
|
|
1535
|
+
stroke, 'stroke-width': strokeWidth,
|
|
793
1536
|
}),
|
|
794
|
-
|
|
795
|
-
y:
|
|
796
|
-
fill:
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
1537
|
+
label ? h('text', {
|
|
1538
|
+
y: label.baseline, 'text-anchor': 'middle',
|
|
1539
|
+
fill: label.isPrimary ? 'var(--dsw-alias-label-primary)' : 'var(--dsw-alias-label-secondary)',
|
|
1540
|
+
'font-size': label.fs,
|
|
1541
|
+
'font-weight': 500,
|
|
1542
|
+
opacity: active ? 1 : dimmed ? 0.2 : label.isPrimary ? 0.9 : 0.72,
|
|
1543
|
+
stroke: 'var(--dsw-alias-bg-layer-1)', 'stroke-width': 2.6, 'stroke-linejoin': 'round', 'paint-order': 'stroke',
|
|
1544
|
+
}, label.text) : null,
|
|
801
1545
|
h('title', null, node.title || node.label),
|
|
802
1546
|
);
|
|
803
1547
|
}))),
|
|
804
1548
|
h('div', {
|
|
805
1549
|
style: {
|
|
806
1550
|
position: 'absolute', left: '10px', bottom: '8px', zIndex: 1,
|
|
807
|
-
maxWidth: 'calc(100% - 60px)', padding: '2px 6px', borderRadius: '6px',
|
|
1551
|
+
maxWidth: truncated ? 'calc(100% - 178px)' : 'calc(100% - 60px)', padding: '2px 6px', borderRadius: '6px',
|
|
808
1552
|
background: 'var(--dsw-alias-bg-layer-1)',
|
|
809
1553
|
color: 'var(--dsw-alias-label-secondary)', fontSize: '11px', lineHeight: '1.5',
|
|
810
1554
|
pointerEvents: 'none',
|
|
811
1555
|
},
|
|
812
|
-
}, props.legend || '
|
|
1556
|
+
}, props.legend || '实心 记忆 · 空心 标签/主题 · 虚线 归类'),
|
|
1557
|
+
truncated ? h('div', {
|
|
1558
|
+
style: {
|
|
1559
|
+
position: 'absolute', right: '10px', bottom: '8px', zIndex: 1,
|
|
1560
|
+
padding: '2px 6px', borderRadius: '6px',
|
|
1561
|
+
background: 'var(--dsw-alias-bg-layer-1)',
|
|
1562
|
+
color: 'var(--dsw-alias-label-secondary)', fontSize: '11px', lineHeight: '1.5',
|
|
1563
|
+
pointerEvents: 'none', whiteSpace: 'nowrap',
|
|
1564
|
+
},
|
|
1565
|
+
}, `显示 ${shownCount} / 共 ${totalCount} 条`) : null,
|
|
813
1566
|
);
|
|
814
1567
|
};
|
|
815
1568
|
|
|
@@ -826,7 +1579,7 @@ window.__ModuleLoader__.load({
|
|
|
826
1579
|
style: {
|
|
827
1580
|
position: 'absolute', top: '2px', left: props.on ? '18px' : '2px', width: '16px', height: '16px',
|
|
828
1581
|
borderRadius: '999px', background: props.on ? 'var(--dsw-alias-bg-base)' : 'var(--dsw-alias-label-secondary)',
|
|
829
|
-
transition: 'left 120ms ease',
|
|
1582
|
+
transition: reduceMotionOf() ? 'none' : 'left 120ms ease',
|
|
830
1583
|
},
|
|
831
1584
|
}));
|
|
832
1585
|
|
|
@@ -896,22 +1649,41 @@ window.__ModuleLoader__.load({
|
|
|
896
1649
|
const evidenceTextOf = (row) => {
|
|
897
1650
|
if (typeof row === 'string') return row;
|
|
898
1651
|
if (!row || typeof row !== 'object') return '';
|
|
899
|
-
|
|
1652
|
+
for (const key of ['sourceExcerpt', 'excerpt', 'text', 'note']) {
|
|
1653
|
+
if (typeof row[key] === 'string' && row[key].trim()) return row[key];
|
|
1654
|
+
}
|
|
1655
|
+
return '';
|
|
1656
|
+
};
|
|
1657
|
+
/** 样例和证据都带版本/核对信息,不拿「样例」二字占位。 */
|
|
1658
|
+
const evidenceMetaOf = (row) => {
|
|
1659
|
+
if (!row || typeof row !== 'object') return '';
|
|
1660
|
+
const bits = [];
|
|
1661
|
+
if (Number.isInteger(row.artifactVersion)) bits.push(`第 ${row.artifactVersion} 版`);
|
|
1662
|
+
else if (typeof row.artifactVersion === 'string' && row.artifactVersion) bits.push(`第 ${row.artifactVersion} 版`);
|
|
1663
|
+
if (typeof row.dimension === 'string' && row.dimension) bits.push(row.dimension);
|
|
1664
|
+
if (row.verified === true) bits.push('已核对');
|
|
1665
|
+
else if (row.verified === false) bits.push('未核对');
|
|
1666
|
+
if (row.at) bits.push(EVO_DATE(row.at));
|
|
1667
|
+
return bits.join(' · ');
|
|
900
1668
|
};
|
|
901
1669
|
|
|
902
1670
|
const MemoryRow = (props) => {
|
|
903
1671
|
const record = props.record;
|
|
1672
|
+
const [hover, setHover] = React.useState(false);
|
|
904
1673
|
return h('button', {
|
|
905
1674
|
type: 'button', onClick: props.onClick,
|
|
1675
|
+
onMouseEnter: () => setHover(true),
|
|
1676
|
+
onMouseLeave: () => setHover(false),
|
|
906
1677
|
style: {
|
|
907
|
-
display: 'flex', flexDirection: 'column', gap:
|
|
908
|
-
border: 'none', borderBottom: '1px solid var(--dsw-alias-border-l1)',
|
|
909
|
-
|
|
910
|
-
|
|
1678
|
+
display: 'flex', flexDirection: 'column', gap: 0, width: '100%', textAlign: 'left',
|
|
1679
|
+
border: 'none', borderBottom: '1px solid var(--dsw-alias-border-l1)', borderRadius: '6px',
|
|
1680
|
+
borderLeft: `2px solid ${props.active ? 'var(--dsw-alias-brand-primary)' : 'transparent'}`,
|
|
1681
|
+
background: props.active || hover ? 'var(--dsw-alias-bg-layer-2)' : 'transparent',
|
|
1682
|
+
color: 'inherit', padding: '4px 8px', cursor: 'pointer', minWidth: 0,
|
|
911
1683
|
},
|
|
912
1684
|
},
|
|
913
|
-
h('span', { style: { fontSize: '12px', lineHeight: '
|
|
914
|
-
h('span', { style: { ...PANEL_MUTED, display: 'flex', alignItems: 'center', gap: '5px', minWidth: 0 } },
|
|
1685
|
+
h('span', { style: { fontSize: '12px', lineHeight: '16px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, record.title || '记忆'),
|
|
1686
|
+
h('span', { style: { ...PANEL_MUTED, display: 'flex', alignItems: 'center', gap: '5px', minWidth: 0, lineHeight: '14px' } },
|
|
915
1687
|
h('span', null, KIND_LABEL[record.kind] || record.kind),
|
|
916
1688
|
record.experimental ? h('span', { style: { color: 'var(--dsw-alias-state-warn-primary)' } }, '实验') : null,
|
|
917
1689
|
h('span', { style: { marginLeft: 'auto', flex: '0 0 auto', fontVariantNumeric: 'tabular-nums' } }, EVO_DATE(record.updated)),
|
|
@@ -919,6 +1691,29 @@ window.__ModuleLoader__.load({
|
|
|
919
1691
|
);
|
|
920
1692
|
};
|
|
921
1693
|
|
|
1694
|
+
/** 一条经验:状态用小标签,标题独占一行。 */
|
|
1695
|
+
const EvolutionRow = (props) => {
|
|
1696
|
+
const rule = props.rule;
|
|
1697
|
+
const [hover, setHover] = React.useState(false);
|
|
1698
|
+
return h('button', {
|
|
1699
|
+
type: 'button', onClick: props.onClick,
|
|
1700
|
+
onMouseEnter: () => setHover(true),
|
|
1701
|
+
onMouseLeave: () => setHover(false),
|
|
1702
|
+
style: {
|
|
1703
|
+
display: 'flex', flexDirection: 'column', gap: '1px', width: '100%', textAlign: 'left',
|
|
1704
|
+
border: 'none', borderRadius: '8px', minWidth: 0,
|
|
1705
|
+
borderLeft: `2px solid ${props.active ? 'var(--dsw-alias-brand-primary)' : 'transparent'}`,
|
|
1706
|
+
background: props.active || hover ? 'var(--dsw-alias-bg-layer-2)' : 'transparent',
|
|
1707
|
+
color: 'inherit', padding: '6px 9px', cursor: 'pointer',
|
|
1708
|
+
},
|
|
1709
|
+
},
|
|
1710
|
+
h('span', { style: { fontSize: '12px', lineHeight: '18px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, rule.title),
|
|
1711
|
+
h('span', { style: { ...PANEL_MUTED, display: 'flex', alignItems: 'center', gap: '6px', minWidth: 0, lineHeight: '16px' } },
|
|
1712
|
+
h(StagePill, { state: rule.state }),
|
|
1713
|
+
h('span', { style: { marginLeft: 'auto', flex: '0 0 auto', fontVariantNumeric: 'tabular-nums' } }, EVO_DATE(rule.updated)),
|
|
1714
|
+
));
|
|
1715
|
+
};
|
|
1716
|
+
|
|
922
1717
|
const EvolutionDetail = (props) => {
|
|
923
1718
|
const rule = props.rule;
|
|
924
1719
|
const ruleId = rule ? rule.id : '';
|
|
@@ -941,8 +1736,12 @@ window.__ModuleLoader__.load({
|
|
|
941
1736
|
const versions = Array.isArray(rule.learning?.versions) ? rule.learning.versions : EMPTY;
|
|
942
1737
|
const history = [
|
|
943
1738
|
...checkpoints.map((row) => ({ at: row.at || row.updated || '', text: typeof row.text === 'string' ? row.text : '' })),
|
|
944
|
-
...versions.map((row) => ({
|
|
1739
|
+
...versions.map((row, index) => ({
|
|
1740
|
+
at: row.at || row.updated || '',
|
|
1741
|
+
text: `第 ${versions.length - index} 版${row.body ? ` ${SHORT(row.body, 42)}` : row.digest ? ` ${SHORT(String(row.digest), 16)}` : ''}`,
|
|
1742
|
+
})),
|
|
945
1743
|
].filter((row) => row.text).sort((a, b) => String(b.at).localeCompare(String(a.at))).slice(0, 6);
|
|
1744
|
+
const failureState = failureStateOf(props.evolution);
|
|
946
1745
|
const canManage = props.canManage && !props.busy;
|
|
947
1746
|
const act = (name, extra) => { if (canManage) props.onManage(name, extra || {}); };
|
|
948
1747
|
const actions = props.canManage
|
|
@@ -994,8 +1793,24 @@ window.__ModuleLoader__.load({
|
|
|
994
1793
|
evidence.length === 0 && samples.length === 0
|
|
995
1794
|
? '暂无学习依据'
|
|
996
1795
|
: [
|
|
997
|
-
...evidence.slice(0, 3).map((row, index) =>
|
|
998
|
-
|
|
1796
|
+
...evidence.slice(0, 3).map((row, index) => {
|
|
1797
|
+
const text = evidenceTextOf(row);
|
|
1798
|
+
const meta = evidenceMetaOf(row);
|
|
1799
|
+
return text
|
|
1800
|
+
? h('div', { key: `e${index}`, style: { display: 'flex', flexDirection: 'column', gap: '1px' } },
|
|
1801
|
+
h('span', { style: { color: 'var(--dsw-alias-label-primary)', opacity: 0.8, whiteSpace: 'pre-wrap', wordBreak: 'break-word' } }, text),
|
|
1802
|
+
meta ? h('span', null, meta) : null)
|
|
1803
|
+
: null;
|
|
1804
|
+
}),
|
|
1805
|
+
...samples.slice(0, 2).map((row, index) => {
|
|
1806
|
+
const text = evidenceTextOf(row);
|
|
1807
|
+
const meta = evidenceMetaOf(row);
|
|
1808
|
+
return text || meta
|
|
1809
|
+
? h('div', { key: `s${index}`, style: { display: 'flex', flexDirection: 'column', gap: '1px' } },
|
|
1810
|
+
text ? h('span', { style: { whiteSpace: 'pre-wrap', wordBreak: 'break-word' } }, text) : null,
|
|
1811
|
+
meta ? h('span', null, meta) : null)
|
|
1812
|
+
: null;
|
|
1813
|
+
}),
|
|
999
1814
|
].filter(Boolean))
|
|
1000
1815
|
: null;
|
|
1001
1816
|
const historyPane = open === 'history'
|
|
@@ -1023,6 +1838,10 @@ window.__ModuleLoader__.load({
|
|
|
1023
1838
|
h(StagePill, { state: rule.state }),
|
|
1024
1839
|
),
|
|
1025
1840
|
h('div', { style: { fontSize: '12px', lineHeight: '1.55', opacity: 0.82 } }, usabilityOf(rule, props.evolution, props.memoryOn)),
|
|
1841
|
+
failureState
|
|
1842
|
+
? h('div', { style: { ...PANEL_MUTED, color: failureState === 'current' ? 'var(--dsw-alias-state-warn-primary)' : undefined } },
|
|
1843
|
+
failureState === 'current' ? '最近一次整理失败,现有经验未更新' : '以前有整理失败,最近一次已完成')
|
|
1844
|
+
: null,
|
|
1026
1845
|
actions,
|
|
1027
1846
|
),
|
|
1028
1847
|
editor,
|
|
@@ -1035,6 +1854,178 @@ window.__ModuleLoader__.load({
|
|
|
1035
1854
|
);
|
|
1036
1855
|
};
|
|
1037
1856
|
|
|
1857
|
+
|
|
1858
|
+
const PalaceSlot = (props) => {
|
|
1859
|
+
const record = props.record;
|
|
1860
|
+
if (!record) {
|
|
1861
|
+
return h('div', {
|
|
1862
|
+
'aria-hidden': 'true',
|
|
1863
|
+
style: { height: '46px', border: '1px dashed var(--dsw-alias-border-l1)', borderRadius: '8px', opacity: 0.28 },
|
|
1864
|
+
});
|
|
1865
|
+
}
|
|
1866
|
+
const title = String(record.title || '记忆');
|
|
1867
|
+
return h('button', {
|
|
1868
|
+
type: 'button', title, 'aria-label': title,
|
|
1869
|
+
onClick: () => props.onPick(record.id),
|
|
1870
|
+
style: {
|
|
1871
|
+
height: '46px', minWidth: 0, display: 'flex', flexDirection: 'column', justifyContent: 'center',
|
|
1872
|
+
padding: '4px 7px', textAlign: 'left', cursor: 'pointer',
|
|
1873
|
+
border: `1px solid ${props.active ? 'var(--dsw-alias-brand-primary)' : 'var(--dsw-alias-border-l1)'}`,
|
|
1874
|
+
borderRadius: '8px', color: 'inherit',
|
|
1875
|
+
background: props.active ? 'var(--dsw-alias-bg-layer-2)' : 'var(--dsw-alias-bg-base)',
|
|
1876
|
+
},
|
|
1877
|
+
},
|
|
1878
|
+
h('span', {
|
|
1879
|
+
style: {
|
|
1880
|
+
fontSize: '11px', lineHeight: '15px', height: '30px', overflow: 'hidden',
|
|
1881
|
+
display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', wordBreak: 'break-word',
|
|
1882
|
+
},
|
|
1883
|
+
}, title),
|
|
1884
|
+
);
|
|
1885
|
+
};
|
|
1886
|
+
|
|
1887
|
+
const PalaceRoomCard = (props) => {
|
|
1888
|
+
const room = props.room;
|
|
1889
|
+
const empty = room.total === 0;
|
|
1890
|
+
const preview = room.items.slice(0, PALACE_PREVIEW);
|
|
1891
|
+
const slots = Array.from({ length: PALACE_PREVIEW }, (_, index) => preview[index] || null);
|
|
1892
|
+
return h('div', {
|
|
1893
|
+
role: 'listitem',
|
|
1894
|
+
style: { ...PANEL_CARD, padding: 0, position: 'relative', overflow: 'hidden', display: 'flex', width: '100%', flexDirection: 'column' },
|
|
1895
|
+
},
|
|
1896
|
+
h('div', { style: { position: 'relative', display: 'flex', flexDirection: 'column', gap: '8px', padding: '9px 10px 10px', boxSizing: 'border-box' } },
|
|
1897
|
+
h('button', {
|
|
1898
|
+
type: 'button', 'aria-expanded': empty ? undefined : props.expanded, 'aria-controls': empty ? undefined : `lite-palace-room-${room.id}`,
|
|
1899
|
+
disabled: empty,
|
|
1900
|
+
onClick: () => { if (!empty) props.onToggle(room.id); },
|
|
1901
|
+
style: { ...PANEL_LINK, width: '100%', display: 'flex', alignItems: 'center', gap: '7px', textAlign: 'left', opacity: 1 },
|
|
1902
|
+
},
|
|
1903
|
+
h('span', { style: { fontSize: '12px', lineHeight: '18px', fontWeight: 600, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, room.name),
|
|
1904
|
+
h('span', { style: { ...PANEL_MUTED, flex: '0 0 auto', fontVariantNumeric: 'tabular-nums' } }, `${room.total} 条`),
|
|
1905
|
+
empty ? null : h('span', { style: { ...PANEL_MUTED, flex: '0 0 auto', marginLeft: 'auto', opacity: 0.6 } }, props.expanded ? '收起' : '展开'),
|
|
1906
|
+
),
|
|
1907
|
+
empty
|
|
1908
|
+
? h('span', { style: { ...PANEL_MUTED, padding: '2px 0 4px' } }, '还没有记录')
|
|
1909
|
+
: h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(96px, 1fr))', gap: '6px' } },
|
|
1910
|
+
slots.map((record, index) => h(PalaceSlot, {
|
|
1911
|
+
key: record ? record.id : `slot-${room.id}-${index}`,
|
|
1912
|
+
record, active: Boolean(record && record.id === props.selectedId), onPick: props.onPick,
|
|
1913
|
+
}))),
|
|
1914
|
+
),
|
|
1915
|
+
);
|
|
1916
|
+
};
|
|
1917
|
+
|
|
1918
|
+
const MemoryPalace = (props) => {
|
|
1919
|
+
const rows = Array.isArray(props.records) ? props.records : EMPTY;
|
|
1920
|
+
const [expanded, setExpanded] = React.useState('');
|
|
1921
|
+
const [page, setPage] = React.useState(1);
|
|
1922
|
+
const panelRef = React.useRef(null);
|
|
1923
|
+
const reduced = useReducedMotion();
|
|
1924
|
+
const motion = palaceMotionOf(reduced);
|
|
1925
|
+
const rooms = React.useMemo(() => palaceRoomsOf(rows), [rows]);
|
|
1926
|
+
const activeRoom = rooms.find((room) => room.id === expanded) || null;
|
|
1927
|
+
const pageInfo = activeRoom ? palaceRoomPageOf(rows, activeRoom.id, page) : null;
|
|
1928
|
+
|
|
1929
|
+
React.useEffect(() => {
|
|
1930
|
+
if (expanded && !rooms.some((room) => room.id === expanded)) setExpanded('');
|
|
1931
|
+
}, [rooms, expanded]);
|
|
1932
|
+
React.useEffect(() => {
|
|
1933
|
+
const node = panelRef.current;
|
|
1934
|
+
if (!expanded || !node || motion.duration === 0 || typeof node.animate !== 'function') return;
|
|
1935
|
+
node.animate([
|
|
1936
|
+
{ opacity: 0, transform: 'translateY(-6px)' },
|
|
1937
|
+
{ opacity: 1, transform: 'translateY(0)' },
|
|
1938
|
+
], { duration: motion.duration, easing: motion.easing });
|
|
1939
|
+
}, [expanded, page, motion.duration, motion.easing]);
|
|
1940
|
+
|
|
1941
|
+
if (rows.length === 0) {
|
|
1942
|
+
return h('div', { style: { ...PANEL_CARD, padding: '14px', minHeight: 0 } },
|
|
1943
|
+
h('div', { style: PANEL_MUTED }, props.emptyText || '这个项目还没有记忆'));
|
|
1944
|
+
}
|
|
1945
|
+
const toggle = (roomId) => {
|
|
1946
|
+
setExpanded((current) => palaceToggleOf(current, roomId));
|
|
1947
|
+
setPage(1);
|
|
1948
|
+
};
|
|
1949
|
+
const goPage = (target) => {
|
|
1950
|
+
if (!pageInfo) return;
|
|
1951
|
+
setPage(Math.min(pageInfo.pages, Math.max(1, target)));
|
|
1952
|
+
};
|
|
1953
|
+
const pageButton = (label, target, disabled, current) => h('button', {
|
|
1954
|
+
key: `page-${target}-${label}`, type: 'button', disabled,
|
|
1955
|
+
'aria-current': current ? 'page' : undefined, 'aria-label': `第 ${target} 页`,
|
|
1956
|
+
onClick: () => goPage(target),
|
|
1957
|
+
style: {
|
|
1958
|
+
...PANEL_LINK, minWidth: '22px', height: '22px', padding: '0 5px', borderRadius: '6px',
|
|
1959
|
+
border: 'none', opacity: disabled ? 0.28 : current ? 0.85 : 0.5,
|
|
1960
|
+
background: current ? 'var(--dsw-alias-bg-layer-2)' : 'transparent',
|
|
1961
|
+
fontVariantNumeric: 'tabular-nums', cursor: disabled ? 'default' : 'pointer',
|
|
1962
|
+
},
|
|
1963
|
+
}, label);
|
|
1964
|
+
const pageSlots = pageInfo
|
|
1965
|
+
? Array.from({ length: pageInfo.items.length ? Math.ceil(pageInfo.items.length / 6) * 6 : 0 }, (_, index) => pageInfo.items[index] || null)
|
|
1966
|
+
: EMPTY;
|
|
1967
|
+
return h('div', {
|
|
1968
|
+
role: 'group', 'aria-label': '记忆宫殿',
|
|
1969
|
+
onKeyDown: (event) => {
|
|
1970
|
+
if (!pageInfo || pageInfo.pages <= 1) return;
|
|
1971
|
+
if (event.key === 'ArrowRight' && pageInfo.page < pageInfo.pages) {
|
|
1972
|
+
event.preventDefault();
|
|
1973
|
+
goPage(pageInfo.page + 1);
|
|
1974
|
+
}
|
|
1975
|
+
if (event.key === 'ArrowLeft' && pageInfo.page > 1) {
|
|
1976
|
+
event.preventDefault();
|
|
1977
|
+
goPage(pageInfo.page - 1);
|
|
1978
|
+
}
|
|
1979
|
+
if (event.key === 'Home' && pageInfo.page > 1) {
|
|
1980
|
+
event.preventDefault();
|
|
1981
|
+
goPage(1);
|
|
1982
|
+
}
|
|
1983
|
+
if (event.key === 'End' && pageInfo.page < pageInfo.pages) {
|
|
1984
|
+
event.preventDefault();
|
|
1985
|
+
goPage(pageInfo.pages);
|
|
1986
|
+
}
|
|
1987
|
+
},
|
|
1988
|
+
style: { ...PANEL_CARD, padding: '10px', minHeight: 0, flex: '0 0 auto', overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: '10px' },
|
|
1989
|
+
},
|
|
1990
|
+
h('div', {
|
|
1991
|
+
role: 'list',
|
|
1992
|
+
'aria-label': '记忆房间',
|
|
1993
|
+
style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(232px, 1fr))', gap: '9px' },
|
|
1994
|
+
},
|
|
1995
|
+
rooms.map((room) => h(PalaceRoomCard, {
|
|
1996
|
+
key: room.id, room, expanded: room.id === expanded, selectedId: props.selectedId,
|
|
1997
|
+
onPick: props.onPick, onToggle: toggle,
|
|
1998
|
+
}))),
|
|
1999
|
+
h('div', { 'aria-live': 'polite', style: { ...PANEL_MUTED, marginTop: '-2px' } }, `${rooms.length} 个房间 · ${rows.length} 条`),
|
|
2000
|
+
activeRoom && activeRoom.total > 0 && pageInfo
|
|
2001
|
+
? h('section', {
|
|
2002
|
+
id: `lite-palace-room-${activeRoom.id}`, 'aria-label': activeRoom.name, ref: panelRef,
|
|
2003
|
+
style: { borderTop: '1px solid var(--dsw-alias-border-l1)', paddingTop: '9px', display: 'flex', flexDirection: 'column', gap: '8px' },
|
|
2004
|
+
},
|
|
2005
|
+
h('div', { style: { display: 'flex', alignItems: 'center', gap: '8px' } },
|
|
2006
|
+
h('span', { style: { fontSize: '12px', lineHeight: '1.4', fontWeight: 600 } }, activeRoom.name),
|
|
2007
|
+
h('span', { style: PANEL_MUTED, 'aria-live': 'polite', 'aria-atomic': 'true' }, `${pageInfo.total} 条${pageInfo.pages > 1 ? ` · 第 ${pageInfo.page} / ${pageInfo.pages} 页` : ''}`),
|
|
2008
|
+
h('button', {
|
|
2009
|
+
type: 'button', onClick: () => toggle(activeRoom.id),
|
|
2010
|
+
style: { ...PANEL_LINK, marginLeft: 'auto', height: '24px', padding: '0 7px', borderRadius: '6px', border: '1px solid var(--dsw-alias-border-l1)' },
|
|
2011
|
+
}, '收起'),
|
|
2012
|
+
),
|
|
2013
|
+
h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(96px, 1fr))', gap: '6px' } },
|
|
2014
|
+
pageSlots.map((record, index) => h(PalaceSlot, {
|
|
2015
|
+
key: record ? record.id : `page-${pageInfo.page}-slot-${index}`,
|
|
2016
|
+
record, active: Boolean(record && record.id === props.selectedId), onPick: props.onPick,
|
|
2017
|
+
}))),
|
|
2018
|
+
pageInfo.pages > 1
|
|
2019
|
+
? h('div', { role: 'navigation', 'aria-label': '分页', style: { display: 'flex', alignItems: 'center', gap: '4px', flexWrap: 'wrap' } },
|
|
2020
|
+
pageButton('上一页', pageInfo.page - 1, pageInfo.page <= 1, false),
|
|
2021
|
+
palacePageWindow(pageInfo.page, pageInfo.pages).map((number) => pageButton(String(number), number, false, number === pageInfo.page)),
|
|
2022
|
+
pageButton('下一页', pageInfo.page + 1, pageInfo.page >= pageInfo.pages, false))
|
|
2023
|
+
: null,
|
|
2024
|
+
)
|
|
2025
|
+
: null,
|
|
2026
|
+
);
|
|
2027
|
+
};
|
|
2028
|
+
|
|
1038
2029
|
const MemoryPanel = (props) => {
|
|
1039
2030
|
const sessionId = props.sessionId;
|
|
1040
2031
|
const [data, setData] = React.useState(null);
|
|
@@ -1052,9 +2043,16 @@ window.__ModuleLoader__.load({
|
|
|
1052
2043
|
const [detailNote, setDetailNote] = React.useState('');
|
|
1053
2044
|
const [busy, setBusy] = React.useState(false);
|
|
1054
2045
|
const [showReason, setShowReason] = React.useState(false);
|
|
2046
|
+
const [showSettings, setShowSettings] = React.useState(false);
|
|
2047
|
+
const [explainOn, setExplainOn] = React.useState(explainModeOf(sessionId));
|
|
2048
|
+
const [display, setDisplay] = React.useState({ ...DISPLAY_DEFAULTS });
|
|
2049
|
+
const [hubFilter, setHubFilter] = React.useState(null);
|
|
2050
|
+
const [checkup, setCheckup] = React.useState(false);
|
|
1055
2051
|
const alive = React.useRef(true);
|
|
1056
2052
|
const generation = React.useRef(0);
|
|
1057
2053
|
const scopeRef = React.useRef('');
|
|
2054
|
+
/** 先算出当前范围键,显示设置的读取/落盘都按它来,不能等后面的派生变量。 */
|
|
2055
|
+
const selectedKey = data && data.selected && data.selected.key ? data.selected.key : '';
|
|
1058
2056
|
React.useEffect(() => {
|
|
1059
2057
|
alive.current = true;
|
|
1060
2058
|
return () => { alive.current = false; };
|
|
@@ -1091,6 +2089,22 @@ window.__ModuleLoader__.load({
|
|
|
1091
2089
|
|
|
1092
2090
|
React.useEffect(() => { load(); }, [sessionId]);
|
|
1093
2091
|
|
|
2092
|
+
/** 显示设置跟着当前项目走;换项目或换范围时重新读。 */
|
|
2093
|
+
React.useEffect(() => {
|
|
2094
|
+
setDisplay(readDisplaySettings(selectedKey));
|
|
2095
|
+
setShowSettings(false);
|
|
2096
|
+
setHubFilter(null);
|
|
2097
|
+
}, [selectedKey]);
|
|
2098
|
+
React.useEffect(() => {
|
|
2099
|
+
setMemoryView((value) => viewModeOf(display, value));
|
|
2100
|
+
setEvolutionView((value) => viewModeOf(display, value));
|
|
2101
|
+
}, [display.graph, display.palace]);
|
|
2102
|
+
const setDisplayFlag = (name, value) => {
|
|
2103
|
+
const next = { ...display, [name]: value === true };
|
|
2104
|
+
setDisplay(next);
|
|
2105
|
+
writeDisplaySettings(selectedKey, next);
|
|
2106
|
+
};
|
|
2107
|
+
|
|
1094
2108
|
React.useEffect(() => {
|
|
1095
2109
|
if (typeof window === 'undefined') return undefined;
|
|
1096
2110
|
const onKey = (event) => { if (event.key === 'Escape') { event.stopPropagation(); props.onClose(); } };
|
|
@@ -1123,7 +2137,6 @@ window.__ModuleLoader__.load({
|
|
|
1123
2137
|
const records = selected && Array.isArray(selected.records) ? selected.records : EMPTY;
|
|
1124
2138
|
const total = selected && selected.stats && Number.isInteger(selected.stats.total) ? selected.stats.total : records.length;
|
|
1125
2139
|
const currentKey = current ? current.key : '';
|
|
1126
|
-
const selectedKey = selected ? selected.key : '';
|
|
1127
2140
|
const control = selected && selected.control
|
|
1128
2141
|
? selected.control
|
|
1129
2142
|
: (current && selected && selected.key === current.key && current.control ? current.control : { enabled: true });
|
|
@@ -1185,19 +2198,31 @@ window.__ModuleLoader__.load({
|
|
|
1185
2198
|
else if (rules.length === 0) activityText = countsUnknown ? '等待可学习的反馈' : '还没有整理出经验';
|
|
1186
2199
|
else activityText = '暂无整理记录';
|
|
1187
2200
|
}
|
|
1188
|
-
const filtered = React.useMemo(() =>
|
|
1189
|
-
const needle = query.trim().toLowerCase();
|
|
1190
|
-
if (!needle) return records;
|
|
1191
|
-
return records.filter((record) => {
|
|
1192
|
-
const tags = Array.isArray(record.tags) ? record.tags.join(' ') : '';
|
|
1193
|
-
return `${record.title} ${record.summary} ${tags} ${record.topicKey || ''} ${KIND_LABEL[record.kind] || ''}`.toLowerCase().includes(needle);
|
|
1194
|
-
});
|
|
1195
|
-
}, [records, query]);
|
|
2201
|
+
const filtered = React.useMemo(() => filterRecords(records, query, hubFilter), [records, query, hubFilter]);
|
|
1196
2202
|
const filteredRules = React.useMemo(() => {
|
|
1197
2203
|
if (stage === 'all') return rules;
|
|
1198
2204
|
return rules.filter((rule) => rule.state === stage);
|
|
1199
2205
|
}, [rules, stage]);
|
|
1200
2206
|
const selectedRuleRow = selectedRule ? rules.find((rule) => rule.id === selectedRule) : null;
|
|
2207
|
+
const checkupData = checkupOf(records, data && data.scopes, globalView);
|
|
2208
|
+
const globalScopeRow = (data && Array.isArray(data.scopes) ? data.scopes : EMPTY).find((scope) => scope && scope.kind === 'global');
|
|
2209
|
+
const checkupJump = (hub) => { setCheckup(false); setTab('memory'); setQuery(''); setHubFilter(hub); };
|
|
2210
|
+
const checkupJumpGlobal = (hub) => {
|
|
2211
|
+
setCheckup(false); setQuery(''); setHubFilter(hub || null);
|
|
2212
|
+
if (globalScopeRow) load(globalScopeRow.key);
|
|
2213
|
+
else setTab('memory');
|
|
2214
|
+
};
|
|
2215
|
+
const checkupRowOf = (key, label, value, unit, onClick) => h('button', {
|
|
2216
|
+
key, type: 'button', onClick,
|
|
2217
|
+
style: {
|
|
2218
|
+
display: 'flex', alignItems: 'center', gap: '8px', width: '100%', minWidth: 0,
|
|
2219
|
+
padding: '7px 9px', border: 'none', background: 'transparent', color: 'inherit',
|
|
2220
|
+
font: 'inherit', textAlign: 'left', cursor: 'pointer',
|
|
2221
|
+
borderBottom: '1px solid var(--dsw-alias-border-l1)',
|
|
2222
|
+
},
|
|
2223
|
+
},
|
|
2224
|
+
h('span', { style: { minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, label),
|
|
2225
|
+
h('span', { style: { marginLeft: 'auto', flex: '0 0 auto', fontVariantNumeric: 'tabular-nums', opacity: 0.72 } }, value === null || value === undefined ? '—' : `${value} ${unit}`));
|
|
1201
2226
|
const detailRecord = selectedMemory ? records.find((record) => record.id === selectedMemory) : null;
|
|
1202
2227
|
|
|
1203
2228
|
const onPickNode = (node) => {
|
|
@@ -1205,6 +2230,19 @@ window.__ModuleLoader__.load({
|
|
|
1205
2230
|
setSelectedMemory(node.id);
|
|
1206
2231
|
setSelectedRule('');
|
|
1207
2232
|
};
|
|
2233
|
+
/** 点 hub 用精确值筛选,标签不发散成子串匹配。 */
|
|
2234
|
+
const onPickHub = (node) => {
|
|
2235
|
+
if (!node || typeof node.id !== 'string') return;
|
|
2236
|
+
const cut = node.id.indexOf(':');
|
|
2237
|
+
const kind = cut > 0 ? node.id.slice(0, cut) : '';
|
|
2238
|
+
const value = cut > 0 ? node.id.slice(cut + 1) : '';
|
|
2239
|
+
if ((kind !== 'tag' && kind !== 'topic') || !value) return;
|
|
2240
|
+
setHubFilter({ kind, value, display: kind === 'tag' ? `#${value}` : value });
|
|
2241
|
+
setQuery('');
|
|
2242
|
+
setSelectedMemory('');
|
|
2243
|
+
setSelectedRule('');
|
|
2244
|
+
setMemoryView('list');
|
|
2245
|
+
};
|
|
1208
2246
|
const onPickRuleNode = (node) => {
|
|
1209
2247
|
if (node.kind !== 'rule') return;
|
|
1210
2248
|
setSelectedRule(node.id);
|
|
@@ -1276,31 +2314,60 @@ window.__ModuleLoader__.load({
|
|
|
1276
2314
|
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' },
|
|
1277
2315
|
},
|
|
1278
2316
|
data && Array.isArray(data.scopes) ? data.scopes.map((scope) => h('option', { key: scope.key, value: scope.key }, scope.kind === 'global' ? '全局' : scope.label)) : null) : null,
|
|
2317
|
+
h('button', {
|
|
2318
|
+
type: 'button', 'aria-expanded': showSettings, 'aria-label': '显示设置',
|
|
2319
|
+
onClick: () => setShowSettings((value) => !value),
|
|
2320
|
+
style: {
|
|
2321
|
+
...PANEL_LINK, height: '30px', padding: '0 9px', borderRadius: '8px',
|
|
2322
|
+
border: '1px solid var(--dsw-alias-border-l1)',
|
|
2323
|
+
background: showSettings ? 'var(--dsw-alias-bg-layer-2)' : 'transparent',
|
|
2324
|
+
opacity: showSettings ? 0.92 : 0.68,
|
|
2325
|
+
},
|
|
2326
|
+
}, '显示设置'),
|
|
1279
2327
|
closeButton,
|
|
1280
2328
|
),
|
|
1281
2329
|
);
|
|
1282
2330
|
|
|
1283
|
-
const
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
h(Toggle, { ariaLabel: '启用记忆', on: memoryOn, busy: busy || !canToggle, onClick: canToggle ? toggleControl : undefined }),
|
|
1287
|
-
),
|
|
1288
|
-
h('div', { style: PANEL_MUTED }, '读取项目和全局记忆'), // ui-copy-ignore 用户指定文案
|
|
2331
|
+
const displayToggle = (label, key) => switchItem(
|
|
2332
|
+
label, '', display[key] === true, !selectedKey,
|
|
2333
|
+
selectedKey ? () => setDisplayFlag(key, display[key] !== true) : undefined,
|
|
1289
2334
|
);
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
2335
|
+
|
|
2336
|
+
const explainToggle = () => switchItem(
|
|
2337
|
+
'解释模式',
|
|
2338
|
+
explainOn ? '零命中时说明原因' : '零命中时不出提示',
|
|
2339
|
+
explainOn,
|
|
2340
|
+
!sessionId,
|
|
2341
|
+
sessionId ? () => {
|
|
2342
|
+
const next = explainOn !== true;
|
|
2343
|
+
setExplainOn(next);
|
|
2344
|
+
setExplainMode(sessionId, next);
|
|
2345
|
+
} : undefined,
|
|
1297
2346
|
);
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
2347
|
+
|
|
2348
|
+
const switchItem = (label, hint, on, disabled, onClick) => h('div', {
|
|
2349
|
+
style: { display: 'flex', flexDirection: 'column', gap: '1px', minWidth: 0, opacity: disabled && !on ? 0.55 : 1 },
|
|
2350
|
+
},
|
|
2351
|
+
h('span', { style: { display: 'flex', alignItems: 'center', gap: '8px', minWidth: 0 } },
|
|
2352
|
+
h('span', { style: { fontSize: '12px', lineHeight: '20px', fontWeight: 600, whiteSpace: 'nowrap' } }, label),
|
|
2353
|
+
h(Toggle, { ariaLabel: label, on, busy: busy || disabled, onClick: disabled ? undefined : onClick }),
|
|
2354
|
+
),
|
|
2355
|
+
hint ? h('span', { style: { ...PANEL_MUTED, lineHeight: '16px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, hint) : null);
|
|
2356
|
+
const evolutionOn = Boolean(memoryOn && evolution && evolution.enabled === true);
|
|
2357
|
+
const switchBar = h('div', {
|
|
2358
|
+
style: { display: 'flex', flexDirection: 'column', gap: '3px', padding: '7px 18px 8px', borderBottom: '1px solid var(--dsw-alias-border-l1)', flex: '0 0 auto' },
|
|
2359
|
+
},
|
|
2360
|
+
globalView
|
|
2361
|
+
? h('div', { style: { ...PANEL_MUTED, lineHeight: '20px' } }, '全局记忆只读')
|
|
2362
|
+
: h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(190px, 1fr))', gap: '4px 18px' } },
|
|
2363
|
+
switchItem('启用记忆', memoryOn ? '读取项目和全局记忆' : '记忆已关闭,内容只读', memoryOn, busy || !canToggle, toggleControl),
|
|
2364
|
+
switchItem('自进化', evolutionOn ? '实验功能,会增加 token 消耗' : '从反馈里整理经验', evolutionOn, busy || !evolution || !canToggle || !memoryOn, toggleEvolution), // ui-copy-ignore 用户指定文案
|
|
2365
|
+
showSettings ? displayToggle('启用图谱', 'graph') : null,
|
|
2366
|
+
showSettings ? displayToggle('启用记忆宫殿', 'palace') : null,
|
|
2367
|
+
showSettings ? explainToggle() : null,
|
|
2368
|
+
),
|
|
2369
|
+
!globalView && !canToggle ? h('div', { style: { ...PANEL_MUTED, lineHeight: '18px' } }, '只读,切换回当前项目后可修改') : null,
|
|
2370
|
+
note ? h('div', { style: { ...PANEL_MUTED, lineHeight: '18px', color: 'var(--dsw-alias-state-error-primary)' } }, note) : null,
|
|
1304
2371
|
);
|
|
1305
2372
|
|
|
1306
2373
|
const switchTab = (key) => {
|
|
@@ -1324,15 +2391,20 @@ window.__ModuleLoader__.load({
|
|
|
1324
2391
|
globalView ? null : tabButton('evolution', '自进化'),
|
|
1325
2392
|
);
|
|
1326
2393
|
|
|
2394
|
+
const filteredNow = Boolean(query || hubFilter);
|
|
1327
2395
|
const memoryList = loading
|
|
1328
|
-
? h('div', { style: { ...PANEL_MUTED, padding: '
|
|
2396
|
+
? h('div', { style: { ...PANEL_MUTED, padding: '14px 9px' } }, '读取中…')
|
|
1329
2397
|
: filtered.length === 0
|
|
1330
|
-
? h('div', { style: {
|
|
2398
|
+
? h('div', { style: { display: 'flex', alignItems: 'center', gap: '8px', padding: '14px 9px', minWidth: 0 } },
|
|
2399
|
+
h('span', { style: { ...PANEL_MUTED, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, filteredNow ? '没有匹配的记忆' : '还没有记忆'),
|
|
2400
|
+
filteredNow ? h(Action, { onClick: () => { setHubFilter(null); setQuery(''); } }, '清除搜索') : null,
|
|
2401
|
+
)
|
|
1331
2402
|
: filtered.map((record) => h(MemoryRow, {
|
|
1332
2403
|
key: record.id, record, active: record.id === selectedMemory,
|
|
1333
2404
|
onClick: () => { setSelectedMemory(record.id); setSelectedRule(''); },
|
|
1334
2405
|
}));
|
|
1335
2406
|
|
|
2407
|
+
const detailProcessing = detail ? processingStatusOf(detail) : '';
|
|
1336
2408
|
const memoryDetail = selectedMemory
|
|
1337
2409
|
? h('div', { style: { ...PANEL_CARD, minHeight: 0, overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: '6px' } },
|
|
1338
2410
|
detail
|
|
@@ -1341,17 +2413,18 @@ window.__ModuleLoader__.load({
|
|
|
1341
2413
|
h('div', { style: { flex: 1, minWidth: 0, fontSize: '13px', lineHeight: '1.45', fontWeight: 600, wordBreak: 'break-word' } }, detail.title),
|
|
1342
2414
|
h(Action, { onClick: () => setSelectedMemory('') }, '关闭'),
|
|
1343
2415
|
),
|
|
1344
|
-
h('div', { style: PANEL_MUTED }, `${KIND_LABEL[detailRecord && detailRecord.kind] || '记忆'} · ${detail.scopeLabel || ''}`),
|
|
2416
|
+
h('div', { style: PANEL_MUTED }, `${KIND_LABEL[detailRecord && detailRecord.kind] || '记忆'} · ${detail.scopeLabel || ''}${detailProcessing ? ` · ${PROCESSING_LABEL[detailProcessing]}` : ''}`),
|
|
1345
2417
|
Array.isArray(detailRecord && detailRecord.tags) && detailRecord.tags.length
|
|
1346
2418
|
? h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: '4px' } },
|
|
1347
2419
|
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,
|
|
1348
|
-
h('div', { style: { fontSize: '12px', lineHeight: '1.65', whiteSpace: 'pre-wrap', wordBreak: 'break-word' } }, detail
|
|
2420
|
+
h('div', { style: { fontSize: '12px', lineHeight: '1.65', whiteSpace: 'pre-wrap', wordBreak: 'break-word' } }, currentBodyOf(detail) || '没有正文'),
|
|
2421
|
+
h(DetailExtras, { payload: detail }),
|
|
1349
2422
|
)
|
|
1350
2423
|
: h('div', { style: PANEL_MUTED }, detailNote || '读取中…'),
|
|
1351
2424
|
)
|
|
1352
2425
|
: null;
|
|
1353
2426
|
|
|
1354
|
-
const viewToggleOf = (current, setValue, ariaLabel) => {
|
|
2427
|
+
const viewToggleOf = (current, setValue, ariaLabel, modes) => {
|
|
1355
2428
|
const button = (key, label) => h('button', {
|
|
1356
2429
|
key, type: 'button', onClick: () => setValue(key),
|
|
1357
2430
|
'aria-pressed': current === key,
|
|
@@ -1361,6 +2434,7 @@ window.__ModuleLoader__.load({
|
|
|
1361
2434
|
opacity: current === key ? 0.9 : 0.5,
|
|
1362
2435
|
},
|
|
1363
2436
|
}, label);
|
|
2437
|
+
const options = Array.isArray(modes) ? modes : [['list', '列表'], ['graph', '图谱']];
|
|
1364
2438
|
return h('div', {
|
|
1365
2439
|
role: 'group', 'aria-label': ariaLabel,
|
|
1366
2440
|
style: {
|
|
@@ -1369,31 +2443,41 @@ window.__ModuleLoader__.load({
|
|
|
1369
2443
|
border: '1px solid var(--dsw-alias-border-l1)', borderRadius: '8px',
|
|
1370
2444
|
background: 'var(--dsw-alias-bg-base)',
|
|
1371
2445
|
},
|
|
1372
|
-
},
|
|
2446
|
+
}, options.map(([key, label]) => button(key, label)));
|
|
1373
2447
|
};
|
|
1374
|
-
const
|
|
2448
|
+
const memoryModes = [['list', '列表']];
|
|
2449
|
+
if (display.graph) memoryModes.push(['graph', '图谱']);
|
|
2450
|
+
if (display.palace) memoryModes.push(['palace', '宫殿']);
|
|
2451
|
+
const memoryToggle = memoryModes.length > 1 ? viewToggleOf(memoryView, setMemoryView, '记忆视图', memoryModes) : null;
|
|
2452
|
+
const memoryViewActive = viewModeOf(display, memoryView);
|
|
2453
|
+
const evolutionViewActive = graphViewOf(display.graph, evolutionView);
|
|
1375
2454
|
|
|
1376
2455
|
const memoryListPane = h('div', {
|
|
1377
2456
|
style: {
|
|
1378
2457
|
...PANEL_CARD, padding: '2px 4px', minHeight: 0, overflowY: 'auto',
|
|
2458
|
+
display: 'flex', flexDirection: 'column', gap: 0,
|
|
1379
2459
|
},
|
|
1380
2460
|
}, memoryList);
|
|
1381
2461
|
const memoryGraphPane = h('div', {
|
|
1382
2462
|
style: { ...PANEL_CARD, padding: 0, minHeight: 0, overflow: 'hidden', display: 'flex' },
|
|
1383
2463
|
},
|
|
1384
2464
|
h(MemoryGraph, {
|
|
1385
|
-
records: filtered,
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
emptyText: query ? '没有匹配的记忆' : '还没有记忆', ariaLabel: '记忆关系图',
|
|
1391
|
-
legend: '圆点 记忆 · 小点 标签/主题 · 虚线 关联 · 点任意点筛选或查看',
|
|
2465
|
+
records: filtered, total: hubFilter ? filtered.length : total,
|
|
2466
|
+
selectedId: selectedMemory, onPick: onPickNode,
|
|
2467
|
+
onPickHub,
|
|
2468
|
+
emptyText: query || hubFilter ? '没有匹配的记忆' : '还没有记忆', ariaLabel: '记忆关系图',
|
|
2469
|
+
legend: '实心 记忆 · 空心 标签/主题 · 虚线 归类 · 点标签筛选,点记忆看正文',
|
|
1392
2470
|
}));
|
|
2471
|
+
const memoryPalacePane = h(MemoryPalace, {
|
|
2472
|
+
key: selectedKey, records: filtered, selectedId: selectedMemory,
|
|
2473
|
+
onPick: (id) => { setSelectedMemory(id); setSelectedRule(''); },
|
|
2474
|
+
emptyText: query || hubFilter ? '没有匹配的记忆' : '这个项目还没有记忆',
|
|
2475
|
+
});
|
|
1393
2476
|
const memoryTab = h('div', { role: 'tabpanel', 'aria-label': '记忆', style: { flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', gap: '9px' } },
|
|
1394
2477
|
h('div', { style: { display: 'flex', alignItems: 'center', gap: '8px', flex: '0 0 auto' } },
|
|
1395
2478
|
h('input', {
|
|
1396
|
-
value:
|
|
2479
|
+
value: hubFilter ? hubFilter.display : query,
|
|
2480
|
+
onChange: (event) => { setHubFilter(null); setQuery(event.target.value); },
|
|
1397
2481
|
placeholder: '搜索标题或标签', 'aria-label': '搜索记忆',
|
|
1398
2482
|
style: { ...PANEL_INPUT, width: 'auto', flex: '1 1 0', minWidth: 0 },
|
|
1399
2483
|
}),
|
|
@@ -1405,7 +2489,7 @@ window.__ModuleLoader__.load({
|
|
|
1405
2489
|
gridTemplateColumns: selectedMemory ? 'minmax(0, 1fr) minmax(280px, 340px)' : 'minmax(0, 1fr)',
|
|
1406
2490
|
},
|
|
1407
2491
|
},
|
|
1408
|
-
|
|
2492
|
+
memoryViewActive === 'graph' ? memoryGraphPane : memoryViewActive === 'palace' ? memoryPalacePane : memoryListPane,
|
|
1409
2493
|
memoryDetail),
|
|
1410
2494
|
);
|
|
1411
2495
|
|
|
@@ -1431,7 +2515,15 @@ window.__ModuleLoader__.load({
|
|
|
1431
2515
|
fontVariantNumeric: 'tabular-nums',
|
|
1432
2516
|
},
|
|
1433
2517
|
}, `${label} ${countText(key)}`)),
|
|
1434
|
-
h('span', { style: { marginLeft: 'auto'
|
|
2518
|
+
h('span', { style: { marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: '8px' } },
|
|
2519
|
+
h('button', {
|
|
2520
|
+
type: 'button', 'aria-pressed': checkup, onClick: () => setCheckup((value) => !value),
|
|
2521
|
+
style: {
|
|
2522
|
+
...PANEL_LINK, height: '30px', opacity: checkup ? 0.92 : 0.68,
|
|
2523
|
+
borderBottom: checkup ? '1px solid currentColor' : '1px solid transparent',
|
|
2524
|
+
},
|
|
2525
|
+
}, '结构体检'),
|
|
2526
|
+
display.graph ? viewToggleOf(evolutionView, setEvolutionView, '自进化视图') : null),
|
|
1435
2527
|
);
|
|
1436
2528
|
|
|
1437
2529
|
const stageLabel = (stageFilters.find(([key]) => key === stage) || ['all', '全部'])[1];
|
|
@@ -1441,21 +2533,14 @@ window.__ModuleLoader__.load({
|
|
|
1441
2533
|
: rules.length === 0 ? (countsUnknown ? '数量未知' : '还没有整理出经验')
|
|
1442
2534
|
: `没有${stageLabel}的经验`;
|
|
1443
2535
|
const evolutionList = filteredRules.length === 0
|
|
1444
|
-
? h('div', { style: {
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
},
|
|
1453
|
-
},
|
|
1454
|
-
h('span', { style: { fontSize: '12px', lineHeight: '1.45', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', opacity: 0.92 } }, rule.title),
|
|
1455
|
-
h('span', { style: { ...PANEL_MUTED, display: 'flex', alignItems: 'center', gap: '6px', minWidth: 0 } },
|
|
1456
|
-
h(StagePill, { state: rule.state }),
|
|
1457
|
-
h('span', { style: { marginLeft: 'auto', flex: '0 0 auto', fontVariantNumeric: 'tabular-nums' } }, EVO_DATE(rule.updated)),
|
|
1458
|
-
)));
|
|
2536
|
+
? h('div', { style: { display: 'flex', alignItems: 'center', gap: '8px', padding: '14px 9px', minWidth: 0 } },
|
|
2537
|
+
h('span', { style: { ...PANEL_MUTED, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, evolutionEmptyText),
|
|
2538
|
+
stage !== 'all' && (memoryOn && evolution && evolution.enabled === true) ? h(Action, { onClick: () => setStage('all') }, '看全部') : null,
|
|
2539
|
+
)
|
|
2540
|
+
: filteredRules.map((rule) => h(EvolutionRow, {
|
|
2541
|
+
key: rule.id, rule, active: rule.id === selectedRule,
|
|
2542
|
+
onClick: () => { setSelectedRule(rule.id); setSelectedMemory(''); setShowReason(false); },
|
|
2543
|
+
}));
|
|
1459
2544
|
|
|
1460
2545
|
const evolutionReady = selectedKey === currentKey;
|
|
1461
2546
|
const evolutionFailed = !evolutionReady || failed
|
|
@@ -1476,18 +2561,31 @@ window.__ModuleLoader__.load({
|
|
|
1476
2561
|
: null;
|
|
1477
2562
|
const evolutionListPane = h('div', {
|
|
1478
2563
|
style: {
|
|
1479
|
-
...PANEL_CARD, padding: '
|
|
2564
|
+
...PANEL_CARD, padding: '4px', minHeight: 0, overflowY: 'auto',
|
|
2565
|
+
display: 'flex', flexDirection: 'column', gap: '2px',
|
|
1480
2566
|
},
|
|
1481
2567
|
}, evolutionList);
|
|
1482
2568
|
const evolutionGraphPane = h('div', {
|
|
1483
2569
|
style: { ...PANEL_CARD, padding: 0, minHeight: 0, overflow: 'hidden', display: 'flex' },
|
|
1484
2570
|
},
|
|
1485
2571
|
h(MemoryGraph, {
|
|
1486
|
-
records: filteredRules, graphOf: ruleGraphOf, primaryKind: 'rule',
|
|
2572
|
+
records: filteredRules, total: filteredRules.length, graphOf: ruleGraphOf, primaryKind: 'rule',
|
|
1487
2573
|
selectedId: selectedRule, onPick: onPickRuleNode,
|
|
1488
2574
|
emptyText: evolutionEmptyText, ariaLabel: '经验关系图',
|
|
1489
|
-
legend: '
|
|
2575
|
+
legend: '实心 经验 · 空心 条件 · 虚线 共用条件 · 点经验查看',
|
|
1490
2576
|
}));
|
|
2577
|
+
const evolutionCheckupPane = h('div', {
|
|
2578
|
+
style: {
|
|
2579
|
+
...PANEL_CARD, padding: '4px', flex: '0 0 auto', minHeight: 0, overflowY: 'auto',
|
|
2580
|
+
display: 'flex', flexDirection: 'column', gap: 0,
|
|
2581
|
+
},
|
|
2582
|
+
},
|
|
2583
|
+
checkupRowOf('duplicate', '重复标题', checkupData.duplicate, '组', () => checkupJump({ kind: 'duplicate', display: '重复标题' })),
|
|
2584
|
+
checkupRowOf('global', '全局条目超 200', checkupData.globalEntries, '条', () => checkupJumpGlobal()),
|
|
2585
|
+
checkupRowOf('globalWhen', '全局缺适用条件', checkupData.globalMissingWhen, '条',
|
|
2586
|
+
() => checkupJumpGlobal({ kind: 'noWhen', display: '全局缺适用条件' })),
|
|
2587
|
+
checkupRowOf('unread', '从未读过', checkupData.unread, '条', () => checkupJump({ kind: 'unread', display: '从未读过' })));
|
|
2588
|
+
|
|
1491
2589
|
const evolutionTab = evolutionFailed ? evolutionFailed : h('div', { role: 'tabpanel', 'aria-label': '自进化', style: { flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', gap: '9px' } },
|
|
1492
2590
|
h('div', { style: { ...PANEL_CARD, padding: '9px 11px', display: 'flex', alignItems: 'flex-start', gap: '8px', flex: '0 0 auto' } },
|
|
1493
2591
|
h('span', { style: { flex: '0 0 auto', width: '6px', height: '6px', marginTop: '5px', borderRadius: '999px', background: effectiveEnabled ? 'var(--dsw-alias-brand-primary)' : 'var(--dsw-alias-label-secondary)', opacity: 0.8 } }),
|
|
@@ -1505,14 +2603,20 @@ window.__ModuleLoader__.load({
|
|
|
1505
2603
|
h('div', {
|
|
1506
2604
|
style: {
|
|
1507
2605
|
flex: 1, minHeight: 0, display: 'grid', gap: '10px',
|
|
1508
|
-
gridTemplateColumns: selectedRuleRow ? 'minmax(0, 1fr) minmax(280px, 340px)' : 'minmax(0, 1fr)',
|
|
2606
|
+
gridTemplateColumns: !checkup && selectedRuleRow ? 'minmax(0, 1fr) minmax(280px, 340px)' : 'minmax(0, 1fr)',
|
|
1509
2607
|
},
|
|
1510
2608
|
},
|
|
1511
|
-
|
|
1512
|
-
evolutionDetailPane),
|
|
2609
|
+
checkup ? evolutionCheckupPane : (evolutionViewActive === 'graph' ? evolutionGraphPane : evolutionListPane),
|
|
2610
|
+
checkup ? null : evolutionDetailPane),
|
|
1513
2611
|
);
|
|
1514
2612
|
|
|
1515
|
-
const
|
|
2613
|
+
const palaceLoose = activeTab === 'memory' && viewModeOf(display, memoryView) === 'palace' && !selectedMemory;
|
|
2614
|
+
const content = h('section', {
|
|
2615
|
+
style: {
|
|
2616
|
+
flex: palaceLoose ? '0 1 auto' : 1, display: 'flex', flexDirection: 'column', minWidth: 0, minHeight: 0,
|
|
2617
|
+
padding: '12px 18px 14px', gap: '10px', overflowY: palaceLoose ? 'auto' : 'visible',
|
|
2618
|
+
},
|
|
2619
|
+
},
|
|
1516
2620
|
tabBar,
|
|
1517
2621
|
activeTab === 'memory' ? memoryTab : evolutionTab,
|
|
1518
2622
|
);
|
|
@@ -1523,7 +2627,10 @@ window.__ModuleLoader__.load({
|
|
|
1523
2627
|
},
|
|
1524
2628
|
h('div', {
|
|
1525
2629
|
role: 'dialog', 'aria-modal': 'true', 'aria-label': '记忆面板',
|
|
1526
|
-
style: {
|
|
2630
|
+
style: {
|
|
2631
|
+
...PANEL_DIALOG, width: 'min(1120px, 100%)', boxSizing: 'border-box',
|
|
2632
|
+
height: palaceLoose ? 'auto' : 'min(760px, 100%)', maxHeight: 'min(760px, 100%)',
|
|
2633
|
+
},
|
|
1527
2634
|
}, ...children));
|
|
1528
2635
|
|
|
1529
2636
|
if (!sessionId) return null;
|
|
@@ -1568,12 +2675,24 @@ window.__ModuleLoader__.load({
|
|
|
1568
2675
|
update: list.filter((item) => item.exposure === 'update'),
|
|
1569
2676
|
};
|
|
1570
2677
|
}, [refs]);
|
|
2678
|
+
const [explainOn, setExplainOn] = React.useState(explainModeOf(sessionId));
|
|
2679
|
+
React.useEffect(() => watchExplain(() => setExplainOn(explainModeOf(sessionId))), [sessionId]);
|
|
2680
|
+
const noTriggerReason = typeof props.noTriggerReason === 'string' && props.noTriggerReason
|
|
2681
|
+
? props.noTriggerReason
|
|
2682
|
+
: (typeof refs.noTriggerReason === 'string' ? refs.noTriggerReason : '');
|
|
1571
2683
|
if (!sessionId && refs.length === 0) return null;
|
|
1572
|
-
if (groups.found.length === 0 && groups.provided.length === 0 && groups.update.length === 0)
|
|
2684
|
+
if (groups.found.length === 0 && groups.provided.length === 0 && groups.update.length === 0) {
|
|
2685
|
+
return explainOn && noTriggerReason
|
|
2686
|
+
? h('div', { style: TAIL }, h(RefSection, { label: '搜索命中', tone: 'found', items: EMPTY, sessionId, turnKey: turn, noTriggerReason }))
|
|
2687
|
+
: null;
|
|
2688
|
+
}
|
|
2689
|
+
const section = (label, items, tone) => (items.length === 0
|
|
2690
|
+
? null
|
|
2691
|
+
: h(RefSection, { key: label, label, tone, items, sessionId, turnKey: turn }));
|
|
1573
2692
|
return h('div', { style: TAIL },
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
2693
|
+
section('搜索命中', groups.found, 'found'),
|
|
2694
|
+
section('读过正文', groups.provided, 'provided'),
|
|
2695
|
+
section('本轮更新', groups.update, 'update'),
|
|
1577
2696
|
);
|
|
1578
2697
|
};
|
|
1579
2698
|
|
|
@@ -1581,7 +2700,7 @@ window.__ModuleLoader__.load({
|
|
|
1581
2700
|
|
|
1582
2701
|
// 测试缝:Node 里验引用投影,浏览器里没有任何副作用。
|
|
1583
2702
|
if (typeof globalThis.__LITE_MEMORY_TEST__ === 'function') {
|
|
1584
|
-
globalThis.__LITE_MEMORY_TEST__({ metaOf, citationOf, citationsOfMeta, citationsOfTurn, dedupe, undoRefOf, graphOf, ruleGraphOf, MemoryPanel });
|
|
2703
|
+
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, refMetaOf, explainKeyOf, explainModeOf, setExplainMode, agoLabel, checkupOf, titleCountsOf });
|
|
1585
2704
|
}
|
|
1586
2705
|
|
|
1587
2706
|
return {
|