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