stratagate-dsh 0.2.1 → 0.2.16

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/dist/client.js CHANGED
@@ -1,4 +1,4 @@
1
- // StrataGate Memory UI read-only DSH Web settings page.
1
+ // StrataGate Memory UI - read-only DSH settings page.
2
2
  window.__ModuleLoader__.load({
3
3
  id: 'stratagate-dsh',
4
4
  factory: (require) => {
@@ -11,29 +11,46 @@ window.__ModuleLoader__.load({
11
11
  const STAR_DISMISSED_KEY = 'stratagate.starPrompt.dismissed.v1'
12
12
  const STAR_PROMPT_USAGE_THRESHOLD = 3
13
13
 
14
- const panel = {
15
- padding: '16px', display: 'flex', flexDirection: 'column', gap: '14px', maxWidth: '1180px', margin: '0 auto',
16
- }
17
- const row = { display: 'flex', gap: '8px', alignItems: 'center', flexWrap: 'wrap' }
18
- const card = { border: '1px solid rgba(128,128,128,.28)', borderRadius: '10px', padding: '12px', background: 'rgba(128,128,128,.055)' }
19
- const muted = { opacity: 0.68, fontSize: '12px' }
20
- const code = { fontFamily: 'ui-monospace, SFMono-Regular, Consolas, monospace', fontSize: '12px' }
21
-
22
- function wasStarPromptDismissed() {
23
- try {
24
- return window.localStorage?.getItem(STAR_DISMISSED_KEY) === '1'
25
- } catch {
26
- return false
27
- }
28
- }
29
-
30
- function rememberStarPromptDismissal() {
31
- try {
32
- window.localStorage?.setItem(STAR_DISMISSED_KEY, '1')
33
- } catch {
34
- // The prompt can still be hidden for this render when storage is unavailable.
14
+ const css = `
15
+ .sg-memory {
16
+ color-scheme:light dark;
17
+ --sg-page:var(--dsh-color-background,var(--color-background,#fff));
18
+ --sg-surface:var(--dsh-color-surface,var(--color-surface,#fff));
19
+ --sg-soft:var(--dsh-color-surface-secondary,var(--color-fill-secondary,#f6f7f9));
20
+ --sg-text:var(--dsh-color-text,var(--color-text,#1c2028));
21
+ --sg-muted:var(--dsh-color-text-secondary,var(--color-text-secondary,#707782));
22
+ --sg-border:var(--dsh-color-border,var(--color-border,#e2e5e9));
23
+ --sg-accent:var(--dsh-color-primary,var(--color-primary,#2563d9));
24
+ --sg-accent-soft:var(--dsh-color-primary-soft,#edf3ff);
25
+ --sg-good:var(--dsh-color-success,#16835b);
26
+ --sg-good-soft:var(--dsh-color-success-soft,#eaf8f1);
27
+ --sg-warn:var(--dsh-color-warning,#ad6200);
28
+ --sg-warn-soft:var(--dsh-color-warning-soft,#fff5e5);
29
+ --sg-danger:var(--dsh-color-danger,#c73b36);
30
+ --sg-danger-soft:var(--dsh-color-danger-soft,#fff0ef);
31
+ box-sizing:border-box;width:100%;max-width:680px;min-width:0;margin:0 auto;padding:16px 18px 32px;
32
+ background:var(--sg-page);color:var(--sg-text);font:14px/1.55 ui-sans-serif,system-ui,-apple-system,"Segoe UI","Microsoft YaHei",sans-serif;
33
+ letter-spacing:0;overflow-wrap:anywhere;
35
34
  }
36
- }
35
+ .sg-memory *{box-sizing:border-box;letter-spacing:0}.sg-memory button,.sg-memory input,.sg-memory select{font:inherit;color:inherit}
36
+ .sg-header{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:11px}.sg-brand{display:flex;align-items:center;gap:9px;min-width:0}.sg-logo{width:30px;height:30px;display:grid;place-items:center;flex:0 0 auto;border-radius:7px;background:var(--sg-accent);color:#fff;font-size:17px;font-weight:750}.sg-brand-name{font-size:15px;font-weight:720;white-space:nowrap}
37
+ .sg-icon-button,.sg-back,.sg-quiet-button{border:0;background:transparent;cursor:pointer}.sg-icon-button{width:32px;height:32px;border-radius:6px;font-size:20px}.sg-icon-button:hover,.sg-back:hover,.sg-quiet-button:hover{background:var(--sg-soft)}
38
+ .sg-project{display:flex;align-items:center;gap:7px;min-width:0;margin:0 0 9px;color:var(--sg-muted);font-size:12px}.sg-project-label{flex:0 0 auto}.sg-project-select{min-width:0;max-width:100%;padding:3px 22px 3px 5px;border:0;border-radius:5px;background:transparent;color:var(--sg-text);font-weight:620;cursor:pointer;text-overflow:ellipsis}
39
+ .sg-tabs{display:grid;grid-template-columns:repeat(3,1fr);border-bottom:1px solid var(--sg-border);margin-bottom:20px}.sg-tab{position:relative;min-width:0;padding:10px 4px;border:0;background:transparent;color:var(--sg-muted);cursor:pointer;white-space:nowrap}.sg-tab.active{color:var(--sg-accent);font-weight:700}.sg-tab.active:after{content:"";position:absolute;left:18%;right:18%;bottom:-1px;height:2px;border-radius:2px;background:var(--sg-accent)}
40
+ .sg-alert{display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:10px;width:100%;padding:11px 12px;margin:0 0 18px;border:1px solid color-mix(in srgb,var(--sg-warn) 28%,var(--sg-border));border-radius:8px;background:var(--sg-warn-soft);text-align:left;cursor:pointer}.sg-alert-mark{color:var(--sg-warn);font-size:17px}.sg-alert-title{font-weight:700}.sg-alert-copy{color:var(--sg-muted);font-size:12px}.sg-chevron{color:var(--sg-muted);font-size:18px}.sg-processing-alert{display:flex;align-items:center;gap:9px;width:100%;padding:11px 12px;margin:0 0 18px;border:1px solid color-mix(in srgb,var(--sg-danger) 34%,var(--sg-border));border-radius:8px;background:var(--sg-danger-soft);color:var(--sg-danger)}.sg-processing-icon{display:inline-grid;place-items:center;width:20px;height:20px;flex:0 0 auto;font-size:19px;font-weight:700;line-height:1;animation:sg-spin 1s linear infinite}.sg-processing-title{display:block;font-weight:720}.sg-processing-copy{display:block;margin-top:2px;color:var(--sg-muted);font-size:12px}@keyframes sg-spin{to{transform:rotate(360deg)}}
41
+ .sg-intro{margin-bottom:15px}.sg-intro h2,.sg-detail-title{margin:0;font-size:18px;line-height:1.35;font-weight:740}.sg-intro p,.sg-detail-subtitle{margin:5px 0 0;color:var(--sg-muted);font-size:13px}.sg-search{position:relative;margin-bottom:5px}.sg-search-mark{position:absolute;left:11px;top:50%;transform:translateY(-50%);color:var(--sg-muted);font-size:16px;pointer-events:none}.sg-search input{width:100%;height:39px;padding:0 11px 0 35px;border:1px solid var(--sg-border);border-radius:7px;background:var(--sg-surface);outline:0}.sg-search input:focus{border-color:var(--sg-accent);box-shadow:0 0 0 2px color-mix(in srgb,var(--sg-accent) 14%,transparent)}
42
+ .sg-feed{border-top:1px solid var(--sg-border)}.sg-entry{position:relative;width:100%;min-width:0;padding:17px 2px;border:0;border-bottom:1px solid var(--sg-border);background:transparent;text-align:left}.sg-entry-button{cursor:pointer}.sg-entry-button:hover .sg-entry-title{color:var(--sg-accent)}.sg-entry-title{padding-right:22px;font-size:15px;line-height:1.45;font-weight:720}.sg-entry-summary{margin-top:5px;color:var(--sg-text);white-space:pre-wrap}.sg-entry-chevron{position:absolute;right:2px;top:18px;color:var(--sg-muted);font-size:18px}.sg-meta{display:flex;align-items:center;gap:7px;flex-wrap:wrap;margin-top:9px;color:var(--sg-muted);font-size:12px}.sg-meta-sep:before{content:"·";margin-right:7px}.sg-tags{display:flex;gap:6px;flex-wrap:wrap;margin-top:10px}.sg-tag{max-width:100%;padding:3px 8px;border:1px solid var(--sg-border);border-radius:999px;background:var(--sg-soft);color:var(--sg-text);font-size:12px;line-height:1.45;text-overflow:ellipsis;overflow:hidden;white-space:nowrap}.sg-tag-button{cursor:pointer}.sg-tag-button:hover{border-color:var(--sg-accent);color:var(--sg-accent)}
43
+ .sg-status{display:inline-flex;align-items:center;gap:5px;padding:2px 7px;border-radius:5px;font-size:12px;font-weight:650}.sg-status:before{content:"";width:6px;height:6px;border-radius:50%;background:currentColor}.sg-status.organized{color:var(--sg-good);background:var(--sg-good-soft)}.sg-status.processing{color:var(--sg-accent);background:var(--sg-accent-soft)}.sg-status.waiting{color:var(--sg-muted);background:var(--sg-soft)}.sg-status.failed{color:var(--sg-warn);background:var(--sg-warn-soft)}
44
+ .sg-backbar{display:flex;align-items:center;min-height:35px;margin:-4px 0 13px}.sg-back{display:inline-flex;align-items:center;gap:6px;margin-left:-7px;padding:6px 7px;border-radius:6px;font-weight:650}.sg-detail-header{padding-bottom:16px;border-bottom:1px solid var(--sg-border)}.sg-detail-section{padding:18px 0;border-bottom:1px solid var(--sg-border)}.sg-detail-section:last-child{border-bottom:0}.sg-section-title{margin:0 0 11px;font-size:14px;font-weight:730}.sg-prose{margin:0;white-space:pre-wrap}.sg-facts{margin:0;padding-left:20px}.sg-facts li+li{margin-top:7px}.sg-related-list{display:flex;flex-direction:column}.sg-related{display:flex;justify-content:space-between;gap:12px;padding:9px 0;border:0;border-bottom:1px solid var(--sg-border);background:transparent;text-align:left;cursor:pointer}.sg-related:last-child{border-bottom:0}.sg-related-name{color:var(--sg-accent)}.sg-related-time{flex:0 0 auto;color:var(--sg-muted);font-size:12px}
45
+ .sg-source-label{display:flex;align-items:center;gap:8px}.sg-source-icon{color:var(--sg-muted)}.sg-tech{margin-top:13px}.sg-tech summary{color:var(--sg-muted);font-size:12px;cursor:pointer}.sg-tech-body{margin-top:10px;padding:11px;border-radius:7px;background:var(--sg-soft);font-size:12px}.sg-tech-row{display:grid;grid-template-columns:88px minmax(0,1fr);gap:9px;padding:3px 0}.sg-code{font:12px/1.55 ui-monospace,SFMono-Regular,Consolas,monospace;white-space:pre-wrap;overflow-wrap:anywhere}.sg-raw-message{padding-top:10px;margin-top:10px;border-top:1px solid var(--sg-border)}
46
+ .sg-result-count{margin:0 0 9px;color:var(--sg-muted);font-size:12px}.sg-result-event{padding:8px 0;border-bottom:1px solid var(--sg-border)}.sg-result-event:last-child{border-bottom:0}.sg-pipeline{display:flex;flex-direction:column}.sg-stage{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:12px;padding:9px 0;border-bottom:1px solid var(--sg-border)}.sg-stage:last-child{border-bottom:0}.sg-stage-value{font-size:12px}.sg-stage-value.done{color:var(--sg-good)}.sg-stage-value.failed{color:var(--sg-danger)}.sg-stage-value.waiting{color:var(--sg-muted)}.sg-safe-note{padding:11px 12px;margin-bottom:12px;border-radius:7px;background:var(--sg-good-soft);color:var(--sg-good);font-weight:650}.sg-error-note{margin:8px 0 0;color:var(--sg-muted);font-size:12px}
47
+ .sg-menu{border-top:1px solid var(--sg-border)}.sg-menu-row{display:grid;grid-template-columns:34px minmax(0,1fr) auto;align-items:center;gap:9px;width:100%;padding:14px 2px;border:0;border-bottom:1px solid var(--sg-border);background:transparent;text-align:left;cursor:pointer}.sg-menu-row:hover .sg-menu-title{color:var(--sg-accent)}.sg-menu-icon{width:28px;height:28px;display:grid;place-items:center;border-radius:6px;background:var(--sg-soft);color:var(--sg-muted)}.sg-menu-title{font-weight:680}.sg-menu-subtitle{color:var(--sg-muted);font-size:12px}.sg-counts{display:flex;gap:22px;padding:5px 0 18px;border-bottom:1px solid var(--sg-border)}.sg-count-value{font-size:22px;font-weight:740}.sg-count-label{color:var(--sg-muted);font-size:12px}.sg-structured-group{padding-top:17px}.sg-raw-group{padding:12px 0;border-bottom:1px solid var(--sg-border)}.sg-raw-group summary{cursor:pointer;font-weight:680}.sg-raw-json{max-height:360px;padding:11px;margin:10px 0 0;overflow:auto;border-radius:7px;background:var(--sg-soft)}
48
+ .sg-audit{padding:14px 0;border-bottom:1px solid var(--sg-border)}.sg-audit summary{cursor:pointer}.sg-audit-body{margin-top:10px}.sg-audit-evidence{margin-top:9px}.sg-star{padding:14px 0;margin-top:14px;border-top:1px solid var(--sg-border)}.sg-star-actions{display:flex;gap:10px;align-items:center;margin-top:8px}.sg-link{color:var(--sg-accent);text-decoration:none}.sg-quiet-button{padding:5px 7px;border-radius:5px;color:var(--sg-muted);font-size:12px}
49
+ .sg-loading{padding:32px 0}.sg-skeleton{height:12px;margin:10px 0;border-radius:4px;background:var(--sg-soft)}.sg-skeleton:nth-child(2){width:72%}.sg-empty{padding:34px 14px;text-align:center;color:var(--sg-muted)}.sg-empty strong{display:block;margin-bottom:5px;color:var(--sg-text)}.sg-error{padding:13px;margin-bottom:16px;border:1px solid color-mix(in srgb,var(--sg-danger) 28%,var(--sg-border));border-radius:8px;background:var(--sg-danger-soft)}.sg-error-title{font-weight:700;color:var(--sg-danger)}.sg-error details{margin-top:7px;font-size:12px}
50
+ .sg-muted{color:var(--sg-muted);font-size:12px}
51
+ @media (prefers-color-scheme:dark){.sg-memory{--sg-page:var(--dsh-color-background,var(--color-background,#15171b));--sg-surface:var(--dsh-color-surface,var(--color-surface,#191c21));--sg-soft:var(--dsh-color-surface-secondary,var(--color-fill-secondary,#22262d));--sg-text:var(--dsh-color-text,var(--color-text,#edf0f4));--sg-muted:var(--dsh-color-text-secondary,var(--color-text-secondary,#9da5b0));--sg-border:var(--dsh-color-border,var(--color-border,#343942));--sg-accent:var(--dsh-color-primary,var(--color-primary,#78a7ff));--sg-accent-soft:var(--dsh-color-primary-soft,#192842);--sg-good:var(--dsh-color-success,#67ce9a);--sg-good-soft:var(--dsh-color-success-soft,#162a22);--sg-warn:var(--dsh-color-warning,#f0ad58);--sg-warn-soft:var(--dsh-color-warning-soft,#302519);--sg-danger:var(--dsh-color-danger,#ff8179);--sg-danger-soft:var(--dsh-color-danger-soft,#321e1f)}}
52
+ @media (max-width:440px){.sg-memory{padding:12px 12px 26px}.sg-brand-name{font-size:14px}.sg-tabs{margin-left:-2px;margin-right:-2px}.sg-tab{padding-left:0;padding-right:0}.sg-alert{grid-template-columns:auto minmax(0,1fr)}.sg-alert>.sg-chevron{display:none}.sg-tech-row{grid-template-columns:1fr;gap:1px}.sg-counts{gap:16px}.sg-entry-title{font-size:14px}}
53
+ `
37
54
 
38
55
  function api(path, params) {
39
56
  const query = new URLSearchParams(params || {})
@@ -44,190 +61,344 @@ window.__ModuleLoader__.load({
44
61
  }))
45
62
  }
46
63
 
47
- function Stat({ label, value }) {
48
- return h('div', { style: { ...card, minWidth: '112px', flex: '1 1 112px' } },
49
- h('div', { style: { fontSize: '22px', fontWeight: 700 } }, String(value)),
50
- h('div', { style: muted }, label))
64
+ function projectName(namespace) {
65
+ const value = String(namespace || '')
66
+ const marker = ':project:'
67
+ if (value.includes(marker)) return value.slice(value.indexOf(marker) + marker.length) || '当前项目'
68
+ if (value.includes(':global:')) return value.split(':global:').pop() || '全局记忆'
69
+ if (value.includes(':session:')) return '当前对话'
70
+ return value || '当前项目'
71
+ }
72
+
73
+ function formatTime(value) {
74
+ if (!value) return '时间未知'
75
+ const date = new Date(value)
76
+ if (Number.isNaN(date.getTime())) return String(value)
77
+ const seconds = Math.round((Date.now() - date.getTime()) / 1000)
78
+ if (seconds >= 0 && seconds < 60) return '刚刚'
79
+ if (seconds >= 60 && seconds < 3600) return Math.floor(seconds / 60) + ' 分钟前'
80
+ const timeZone = 'Asia/Shanghai'
81
+ const dayKey = (input) => new Intl.DateTimeFormat('en-CA', { timeZone, year: 'numeric', month: '2-digit', day: '2-digit' }).format(input)
82
+ const clock = (input) => new Intl.DateTimeFormat('zh-CN', { timeZone, hour: '2-digit', minute: '2-digit', hour12: false }).format(input)
83
+ const now = new Date()
84
+ if (dayKey(date) === dayKey(now)) return '今天 ' + clock(date)
85
+ const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000)
86
+ if (dayKey(date) === dayKey(yesterday)) return '昨天 ' + clock(date)
87
+ return new Intl.DateTimeFormat('zh-CN', { timeZone, month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit', hour12: false }).format(date)
88
+ }
89
+
90
+ function statusText(status) {
91
+ if (status === 'organized') return '已整理'
92
+ if (status === 'failed') return '整理失败'
93
+ if (status === 'waiting') return '等待整理'
94
+ return '整理中'
95
+ }
96
+
97
+ function wasStarPromptDismissed() { try { return window.localStorage?.getItem(STAR_DISMISSED_KEY) === '1' } catch { return false } }
98
+ function rememberStarPromptDismissal() { try { window.localStorage?.setItem(STAR_DISMISSED_KEY, '1') } catch { /* no-op */ } }
99
+
100
+ function Loading() {
101
+ return h('div', { className: 'sg-loading', role: 'status' }, h('div', { className: 'sg-skeleton' }), h('div', { className: 'sg-skeleton' }), h('div', { className: 'sg-skeleton' }))
102
+ }
103
+
104
+ function Empty({ title, copy }) {
105
+ return h('div', { className: 'sg-empty' }, h('strong', null, title), h('span', null, copy))
106
+ }
107
+
108
+ function BackBar({ label, onBack }) {
109
+ return h('div', { className: 'sg-backbar' }, h('button', { className: 'sg-back', onClick: onBack, title: '返回' }, h('span', { 'aria-hidden': 'true' }, '←'), label))
110
+ }
111
+
112
+ function FailureAlert({ count, onOpen }) {
113
+ if (!count) return null
114
+ return h('button', { className: 'sg-alert', onClick: onOpen },
115
+ h('span', { className: 'sg-alert-mark', 'aria-hidden': 'true' }, '⚠'),
116
+ h('span', null, h('span', { className: 'sg-alert-title' }, count + ' 条最近记忆尚未整理完成'), h('br'), h('span', { className: 'sg-alert-copy' }, '原始内容已经保存,不会丢失。')),
117
+ h('span', { className: 'sg-chevron', 'aria-hidden': 'true' }, '›'))
118
+ }
119
+
120
+ function ProcessingAlert({ visible }) {
121
+ if (!visible) return null
122
+ return h('div', { className: 'sg-processing-alert', role: 'status', 'aria-live': 'polite' },
123
+ h('span', { className: 'sg-processing-icon', 'aria-hidden': 'true' }, '↻'),
124
+ h('span', null,
125
+ h('span', { className: 'sg-processing-title' }, '正在触发记忆整理'),
126
+ h('span', { className: 'sg-processing-copy' }, 'Block、Event 和 Element 正在生成,请稍候。')))
127
+ }
128
+
129
+ function SearchBox({ value, onChange }) {
130
+ return h('div', { className: 'sg-search' }, h('span', { className: 'sg-search-mark', 'aria-hidden': 'true' }, '⌕'), h('input', { value, onChange: (event) => onChange(event.target.value), placeholder: '搜索记忆、人物、项目、概念…', 'aria-label': '搜索长期记忆' }))
131
+ }
132
+
133
+ function ElementTags({ elements, onOpen }) {
134
+ if (!Array.isArray(elements) || !elements.length) return null
135
+ return h('div', { className: 'sg-tags', 'aria-label': '相关事物' }, elements.map((element) => h('button', { key: element.id, className: 'sg-tag sg-tag-button', onClick: (event) => { event.stopPropagation(); onOpen(element.id) } }, element.name)))
136
+ }
137
+
138
+ function LongTermPage({ events, elements, project, query, setQuery, openEvent, openElement }) {
139
+ const normalized = query.trim().toLocaleLowerCase()
140
+ const visible = [...events]
141
+ .filter((event) => event.status !== 'forgotten' && event.status !== 'archived')
142
+ .filter((event) => !normalized || JSON.stringify([event.title, event.summary, event.tags, event.relatedElements, project]).toLocaleLowerCase().includes(normalized))
143
+ .sort((a, b) => String(b.updatedAt || b.createdAt).localeCompare(String(a.updatedAt || a.createdAt)))
144
+ const elementMap = new Map(elements.map((element) => [element.id, element]))
145
+ return h(React.Fragment, null,
146
+ h('div', { className: 'sg-intro' }, h('h2', null, 'AI 已形成的长期记忆'), h('p', null, '围绕当前项目整理出的经历与相关事物')),
147
+ h(SearchBox, { value: query, onChange: setQuery }),
148
+ visible.length ? h('div', { className: 'sg-feed' }, visible.map((event) => h('article', { key: event.id, className: 'sg-entry' },
149
+ h('button', { className: 'sg-entry sg-entry-button', style: { padding: 0, borderBottom: 0 }, onClick: () => openEvent(event) },
150
+ h('div', { className: 'sg-entry-title' }, event.title || '未命名记忆'),
151
+ event.summary ? h('div', { className: 'sg-entry-summary' }, event.summary) : null,
152
+ h('span', { className: 'sg-entry-chevron', 'aria-hidden': 'true' }, '›')),
153
+ h('div', { className: 'sg-meta' }, h('span', null, formatTime(event.updatedAt || event.createdAt)), h('span', { className: 'sg-meta-sep' }, project)),
154
+ h(ElementTags, { elements: (event.relatedElements || []).map((item) => elementMap.get(item.id) || item), onOpen: openElement }))))
155
+ : h(Empty, { title: normalized ? '没有找到匹配的长期记忆' : '还没有形成长期记忆', copy: normalized ? '换一个关键词试试。' : '近期内容完成整理后会出现在这里。' }))
156
+ }
157
+
158
+ function RecentPage({ blocks, project, openBlock }) {
159
+ const visible = [...blocks].sort((a, b) => String(b.createdAt).localeCompare(String(a.createdAt)))
160
+ return h(React.Fragment, null,
161
+ h('div', { className: 'sg-intro' }, h('h2', null, '最近记忆'), h('p', null, 'AI 最近完整记下、正在整理的内容')),
162
+ visible.length ? h('div', { className: 'sg-feed' }, visible.map((block) => h('button', { key: block.id, className: 'sg-entry sg-entry-button', onClick: () => openBlock(block) },
163
+ h('div', { className: 'sg-entry-title' }, block.title || '近期记忆'),
164
+ block.summary ? h('div', { className: 'sg-entry-summary' }, block.summary) : null,
165
+ h('div', { className: 'sg-meta' }, h('span', null, formatTime(block.createdAt)), h('span', { className: 'sg-meta-sep' }, project), h('span', { className: 'sg-status ' + block.status }, statusText(block.status))),
166
+ block.status === 'organized' ? h('div', { className: 'sg-meta' }, '形成 ' + (block.relatedEvents || []).length + ' 条长期记忆,识别 ' + (block.relatedElements || []).length + ' 个相关事物') : null,
167
+ block.status === 'failed' ? h('div', { className: 'sg-error-note' }, '原始记忆已保存,不会丢失。') : null,
168
+ h('span', { className: 'sg-entry-chevron', 'aria-hidden': 'true' }, '›'))))
169
+ : h(Empty, { title: '还没有最近记忆', copy: '完成一些对话后,AI 完整记下的内容会出现在这里。' }))
170
+ }
171
+
172
+ function SourceDetails({ item, source, kind }) {
173
+ const messages = source?.messages || []
174
+ return h('div', { className: 'sg-detail-section' },
175
+ h('h3', { className: 'sg-section-title' }, '来源'),
176
+ h('div', { className: 'sg-source-label' }, h('span', { className: 'sg-source-icon', 'aria-hidden': 'true' }, '▣'), h('span', null, '当前 DeepSeek 对话')),
177
+ h('details', { className: 'sg-tech' }, h('summary', null, '技术详情'), h('div', { className: 'sg-tech-body' },
178
+ h('div', { className: 'sg-tech-row' }, h('span', { className: 'sg-muted' }, kind === 'block' ? 'Block ID' : kind === 'event' ? 'Event ID' : 'Element ID'), h('span', { className: 'sg-code' }, item.id)),
179
+ kind === 'block' && item.turnRange ? h('div', { className: 'sg-tech-row' }, h('span', { className: 'sg-muted' }, 'Turn'), h('span', null, item.turnRange.join(' - '))) : null,
180
+ messages.length ? messages.map((message) => h('div', { key: message.id, className: 'sg-raw-message' }, h('div', { className: 'sg-muted' }, String(message.role || '') + ' · ' + formatTime(message.createdAt)), h('div', { className: 'sg-code' }, String(message.content || '')))) : h('div', { className: 'sg-muted' }, '当前数据中没有可显示的来源消息。'))))
181
+ }
182
+
183
+ function EventDetail({ event, project, source, openElement, onBack, backLabel }) {
184
+ return h(React.Fragment, null,
185
+ h(BackBar, { label: backLabel, onBack }),
186
+ h('header', { className: 'sg-detail-header' }, h('h2', { className: 'sg-detail-title' }, event.title || '记忆详情'), event.summary ? h('p', { className: 'sg-detail-subtitle' }, event.summary) : null, h('div', { className: 'sg-meta' }, h('span', null, formatTime(event.updatedAt || event.createdAt)), h('span', { className: 'sg-meta-sep' }, project))),
187
+ event.narrative ? h('div', { className: 'sg-detail-section' }, h('h3', { className: 'sg-section-title' }, 'AI 对这段经历的理解'), h('p', { className: 'sg-prose' }, event.narrative)) : null,
188
+ h('div', { className: 'sg-detail-section' }, h('h3', { className: 'sg-section-title' }, '相关事物'), h(ElementTags, { elements: event.relatedElements || [], onOpen: openElement })),
189
+ h(SourceDetails, { item: event, source, kind: 'event' }))
190
+ }
191
+
192
+ function ElementDetail({ element, events, source, openEvent, onBack, backLabel }) {
193
+ const facts = []
194
+ if (element.currentState) facts.push(element.currentState)
195
+ for (const fact of element.facts || []) {
196
+ const value = Array.isArray(fact.value) ? fact.value.join('、') : fact.value
197
+ if (value && !facts.includes(String(value))) facts.push(String(value))
198
+ }
199
+ const related = events.filter((event) => (element.sourceEventIds || []).includes(event.id)).sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt)))
200
+ return h(React.Fragment, null,
201
+ h(BackBar, { label: backLabel, onBack }),
202
+ h('header', { className: 'sg-detail-header' }, h('h2', { className: 'sg-detail-title' }, element.name || '相关事物'), h('p', { className: 'sg-detail-subtitle' }, 'AI 关于它目前知道这些'), h('div', { className: 'sg-meta' }, h('span', null, '相关经历 ' + related.length + ' 条'), h('span', { className: 'sg-meta-sep' }, '最近更新 ' + formatTime(element.updatedAt)))),
203
+ h('div', { className: 'sg-detail-section' }, facts.length ? h('ul', { className: 'sg-facts' }, facts.map((fact, index) => h('li', { key: index }, fact))) : h('div', { className: 'sg-muted' }, '暂时没有形成可展示的认识。')),
204
+ h('div', { className: 'sg-detail-section' }, h('h3', { className: 'sg-section-title' }, '相关经历'), related.length ? h('div', { className: 'sg-related-list' }, related.map((event) => h('button', { key: event.id, className: 'sg-related', onClick: () => openEvent(event) }, h('span', { className: 'sg-related-name' }, event.title), h('span', { className: 'sg-related-time' }, formatTime(event.updatedAt || event.createdAt))))) : h('div', { className: 'sg-muted' }, '没有关联经历。')),
205
+ h(SourceDetails, { item: element, source, kind: 'element' }))
206
+ }
207
+
208
+ function BlockDetail({ block, project, source, openEvent, openElement, onBack }) {
209
+ const events = source?.events?.length ? source.events : (block.relatedEvents || [])
210
+ const elements = source?.elements?.length ? source.elements : (block.relatedElements || [])
211
+ return h(React.Fragment, null,
212
+ h(BackBar, { label: '最近记忆', onBack }),
213
+ h('header', { className: 'sg-detail-header' }, h('h2', { className: 'sg-detail-title' }, '记忆内容'), h('p', { className: 'sg-detail-subtitle sg-prose' }, block.summary || block.title || '暂无可显示内容'), h('div', { className: 'sg-meta' }, h('span', null, formatTime(block.createdAt)), h('span', { className: 'sg-meta-sep' }, project))),
214
+ h('div', { className: 'sg-detail-section' }, h('h3', { className: 'sg-section-title' }, '整理结果'), block.status === 'failed' ? h('div', { className: 'sg-safe-note' }, '原始记忆已保存,不会丢失。') : null, h('div', { className: 'sg-status ' + block.status }, statusText(block.status)),
215
+ events.length ? h(React.Fragment, null, h('p', { className: 'sg-result-count' }, '形成经历 ' + events.length + ' 条'), h('div', null, events.map((event) => h('button', { key: event.id, className: 'sg-related', onClick: () => openEvent(event) }, h('span', { className: 'sg-related-name' }, event.title), h('span', { className: 'sg-chevron' }, '›'))))) : null,
216
+ elements.length ? h(React.Fragment, null, h('p', { className: 'sg-result-count', style: { marginTop: '15px' } }, '识别相关事物 ' + elements.length + ' 个'), h(ElementTags, { elements, onOpen: openElement })) : null,
217
+ block.status === 'processing' && !events.length ? h('p', { className: 'sg-error-note' }, '后续结构正在生成,完成后会显示在这里。') : null),
218
+ h(SourceDetails, { item: block, source, kind: 'block' }))
219
+ }
220
+
221
+ function ProcessingStatus({ overview, blocks, onBack, refresh }) {
222
+ const failures = overview.failedJobDetails || []
223
+ const failedBlocks = blocks.filter((block) => block.status === 'failed')
224
+ const first = failures[0]
225
+ return h(React.Fragment, null,
226
+ h(BackBar, { label: '返回', onBack }),
227
+ h('div', { className: 'sg-intro' }, h('h2', null, '处理状态'), h('p', null, failures.length + ' 个任务需要继续处理')),
228
+ h('div', { className: 'sg-safe-note' }, '原始记忆已保存,不会丢失。'),
229
+ h('div', { className: 'sg-detail-section' }, h('div', { className: 'sg-pipeline' },
230
+ h('div', { className: 'sg-stage' }, h('span', null, '记忆保存'), h('span', { className: 'sg-stage-value done' }, '✓ 已完成')),
231
+ h('div', { className: 'sg-stage' }, h('span', null, '事件提取'), h('span', { className: 'sg-stage-value ' + (first?.kind === 'event-extraction' ? 'failed' : 'done') }, first?.kind === 'event-extraction' ? '× 失败' : '✓ 已完成')),
232
+ h('div', { className: 'sg-stage' }, h('span', null, '元素投影'), h('span', { className: 'sg-stage-value ' + (first?.kind === 'element-projection' ? 'failed' : first?.kind === 'event-extraction' ? 'waiting' : 'done') }, first?.kind === 'element-projection' ? '× 失败' : first?.kind === 'event-extraction' ? '— 等待' : '✓ 已完成')))),
233
+ failedBlocks.length ? h('div', { className: 'sg-detail-section' }, h('h3', { className: 'sg-section-title' }, '受影响的最近记忆'), failedBlocks.map((block) => h('div', { key: block.id, className: 'sg-result-event' }, block.title || block.summary || '近期记忆'))) : null,
234
+ h('button', { className: 'sg-alert', onClick: refresh, style: { marginTop: '16px' } }, h('span', { className: 'sg-alert-mark' }, '↻'), h('span', null, h('span', { className: 'sg-alert-title' }, '重新检查状态'), h('br'), h('span', { className: 'sg-alert-copy' }, '任务会沿用现有重试机制继续处理。')), h('span', { className: 'sg-chevron' }, '›')),
235
+ h('details', { className: 'sg-tech' }, h('summary', null, '技术错误详情'), h('pre', { className: 'sg-tech-body sg-code' }, first?.lastErrorFull || first?.lastError || '没有记录技术错误。')))
51
236
  }
52
237
 
53
238
  function StarPrompt({ usageRecords }) {
54
239
  const [dismissed, setDismissed] = React.useState(wasStarPromptDismissed)
55
240
  if (dismissed || Number(usageRecords || 0) < STAR_PROMPT_USAGE_THRESHOLD) return null
241
+ const dismiss = () => { rememberStarPromptDismissal(); setDismissed(true) }
242
+ return h('div', { className: 'sg-star', 'data-testid': 'stratagate-star-prompt' }, h('strong', null, 'StrataGate 已在当前项目中帮助使用记忆 ', usageRecords, ' 次。'), h('div', { className: 'sg-star-actions' }, h('a', { className: 'sg-link', href: STAR_REPOSITORY_URL, target: '_blank', rel: 'noopener noreferrer', onClick: dismiss }, '在 GitHub 支持项目'), h('button', { className: 'sg-quiet-button', onClick: dismiss }, '不再提示')))
243
+ }
56
244
 
57
- const dismiss = () => {
58
- rememberStarPromptDismissal()
59
- setDismissed(true)
60
- }
245
+ function MoreHome({ selected, setView }) {
246
+ const rows = [
247
+ ['structure', '◇', '记忆结构', '浏览经历与相关事物'],
248
+ ['system', '✓', '系统状态', '处理任务和最近整理时间'],
249
+ ['audit', '↗', '使用记录', '长期记忆何时被使用'],
250
+ ['raw', '{}', '原始数据', 'Block、Event、Element 与模型响应'],
251
+ ['settings', '⚙', '高级设置', 'Schema、提取间隔与项目空间'],
252
+ ]
253
+ return h(React.Fragment, null, h('div', { className: 'sg-intro' }, h('h2', null, '更多'), h('p', null, '高级信息与工程视图')), h('div', { className: 'sg-menu' }, rows.map(([id, icon, title, subtitle]) => h('button', { key: id, className: 'sg-menu-row', onClick: () => setView({ name: id }) }, h('span', { className: 'sg-menu-icon', 'aria-hidden': 'true' }, icon), h('span', null, h('span', { className: 'sg-menu-title' }, title), h('br'), h('span', { className: 'sg-menu-subtitle' }, subtitle)), h('span', { className: 'sg-chevron' }, '›')))), h(StarPrompt, { usageRecords: selected.usageReceipts }))
254
+ }
255
+
256
+ function StructurePage({ events, elements, openEvent, openElement, onBack }) {
257
+ return h(React.Fragment, null,
258
+ h(BackBar, { label: '更多', onBack }),
259
+ h('div', { className: 'sg-intro' }, h('h2', null, '记忆结构'), h('p', null, '完整浏览现有结构化记忆')),
260
+ h('div', { className: 'sg-counts' },
261
+ h('div', null, h('div', { className: 'sg-count-value' }, events.length), h('div', { className: 'sg-count-label' }, '经历 / Event')),
262
+ h('div', null, h('div', { className: 'sg-count-value' }, elements.length), h('div', { className: 'sg-count-label' }, '相关事物 / Element'))),
263
+ h('div', { className: 'sg-structured-group' },
264
+ h('h3', { className: 'sg-section-title' }, '经历'),
265
+ events.map((event) => h('button', { key: event.id, className: 'sg-related', onClick: () => openEvent(event) }, h('span', null, event.title), h('span', { className: 'sg-related-time' }, formatTime(event.updatedAt))))),
266
+ h('div', { className: 'sg-structured-group' },
267
+ h('h3', { className: 'sg-section-title' }, '相关事物'),
268
+ elements.map((element) => h('button', { key: element.id, className: 'sg-related', onClick: () => openElement(element.id) }, h('span', null, element.name), h('span', { className: 'sg-related-time' }, (element.sourceEventIds || []).length + ' 条经历')))))
269
+ }
270
+
271
+ function SystemPage({ selected, blocks, onBack, refresh }) {
272
+ const processing = blocks.filter((block) => block.status === 'processing').length
273
+ return h(React.Fragment, null, h(BackBar, { label: '更多', onBack }), h('div', { className: 'sg-intro' }, h('h2', null, '系统状态'), h('p', null, '仅在这里展示记忆处理的工程状态')), h('div', { className: 'sg-pipeline' },
274
+ [['Block 数量', selected.blocks], ['待处理任务', processing], ['失败任务', selected.failedJobs], ['最近整理时间', formatTime(selected.lastActivityAt)], ['Event 提取', selected.failedJobDetails?.some((item) => item.kind === 'event-extraction') ? '有失败' : '正常'], ['Element 投影', selected.failedJobDetails?.some((item) => item.kind === 'element-projection') ? '有失败' : '正常']].map(([label, value]) => h('div', { key: label, className: 'sg-stage' }, h('span', null, label), h('span', { className: 'sg-stage-value ' + (label === '失败任务' && value ? 'failed' : 'waiting') }, String(value))))), h('button', { className: 'sg-quiet-button', onClick: refresh, style: { marginTop: '14px' } }, '↻ 重新检查'))
275
+ }
61
276
 
62
- return h('div', { style: { ...card, borderColor: '#d6a84b' }, 'data-testid': 'stratagate-star-prompt' },
63
- h('div', { style: { fontWeight: 650 } },
64
- 'StrataGate 已帮助当前项目完成 ', String(usageRecords), ' 次有证据支持的记忆召回。'),
65
- h('div', { style: { ...muted, marginTop: '5px' } },
66
- '如果它对你有帮助,欢迎给项目一个 Star,让更多 DeepSeek Harness 用户发现它。'),
67
- h('div', { style: { ...row, marginTop: '10px' } },
68
- h('a', {
69
- href: STAR_REPOSITORY_URL,
70
- target: '_blank',
71
- rel: 'noopener noreferrer',
72
- onClick: dismiss,
73
- style: { color: 'inherit', fontWeight: 650 },
74
- }, '⭐ GitHub StrataGate 点 Star'),
75
- h('button', { onClick: dismiss }, '不再提示')))
76
- }
77
-
78
- function SourceMessages({ messages }) {
79
- if (!Array.isArray(messages) || messages.length === 0) return h('div', { style: muted }, '没有可显示的原始消息。')
80
- return h('div', { style: { display: 'flex', flexDirection: 'column', gap: '8px' } }, messages.map((message) =>
81
- h('div', { key: message.id, style: { ...card, borderLeft: '3px solid #5794f2' } },
82
- h('div', { style: { ...row, justifyContent: 'space-between' } },
83
- h('strong', null, String(message.role)),
84
- h('span', { style: muted }, String(message.createdAt || '') + (message.blockId ? ' · ' + message.blockId : ' · open tail'))),
85
- h('pre', { style: { ...code, whiteSpace: 'pre-wrap', margin: '8px 0 0', overflowWrap: 'anywhere' } }, String(message.content || '')),
86
- message.toolCalls && message.toolCalls.length
87
- ? h('pre', { style: { ...code, whiteSpace: 'pre-wrap', margin: '8px 0 0', overflowWrap: 'anywhere', opacity: 0.82 } }, JSON.stringify(message.toolCalls, null, 2))
88
- : null)))
89
- }
90
-
91
- function Detail({ detail, onClose }) {
92
- if (!detail) return null
93
- return h('div', { style: { ...card, borderColor: '#5794f2' } },
94
- h('div', { style: { ...row, justifyContent: 'space-between' } },
95
- h('strong', null, '来源证据'),
96
- h('button', { onClick: onClose }, '关闭')),
97
- Array.isArray(detail.events) && detail.events.length
98
- ? h('div', { style: { margin: '10px 0' } }, detail.events.map((event) =>
99
- h('div', { key: event.id, style: { marginBottom: '8px' } },
100
- h('div', { style: { fontWeight: 600 } }, event.title),
101
- h('div', { style: muted }, event.summary))))
102
- : null,
103
- h(SourceMessages, { messages: detail.messages || detail.sourceMessages }))
104
- }
105
-
106
- function MemoryItem({ item, kind, openSource }) {
107
- const title = kind === 'events' ? item.title : kind === 'elements' ? item.name : item.title
108
- const body = kind === 'events' ? item.summary : kind === 'elements' ? item.currentState : item.summary
109
- const sourceParams = kind === 'events' ? { eventId: item.id } : kind === 'elements' ? { elementId: item.id } : { blockId: item.id }
110
- return h('div', { style: card },
111
- h('div', { style: { ...row, justifyContent: 'space-between' } },
112
- h('strong', null, String(title || item.id)),
113
- h('button', { onClick: () => openSource(sourceParams) }, '查看原始证据')),
114
- h('div', { style: { marginTop: '6px', whiteSpace: 'pre-wrap' } }, String(body || '')),
115
- h('div', { style: { ...muted, marginTop: '8px' } },
116
- kind === 'events'
117
- ? [item.status, item.scope, item.criticality, 'confidence ' + item.confidence].join(' · ')
118
- : kind === 'elements'
119
- ? [item.type, (item.facts || []).length + ' facts', (item.sourceEventIds || []).length + ' events'].join(' · ')
120
- : ['turns ' + (item.turnRange || []).join('–'), item.sourceMessages + ' messages', 'L' + item.currentLevel].join(' · ')))
121
- }
122
-
123
- function AuditItem({ item }) {
124
- const audit = item.audit || {}
125
- return h('details', { style: card },
126
- h('summary', { style: { cursor: 'pointer' } },
127
- h('strong', null, audit.sessionId ? '回答 · Session ' + audit.sessionId + (audit.turn !== undefined ? ' · Turn ' + audit.turn : '') : item.id),
128
- h('span', { style: { ...muted, marginLeft: '8px' } }, String(item.createdAt || ''))),
129
- h('div', { style: { marginTop: '10px' } },
130
- h('div', { style: muted }, [audit.verdict || 'legacy receipt', audit.batchId, audit.nextStrategy].filter(Boolean).join(' · ')),
131
- audit.fit ? h('p', null, h('strong', null, 'Fit: '), audit.fit) : null,
132
- h('div', { style: code }, 'Evidence: ' + ((audit.evidenceRefs || []).join(', ') || 'legacy IDs only')),
133
- h('div', { style: { ...muted, margin: '8px 0' } },
134
- (item.events || []).length + ' events · ' + (item.elements || []).length + ' elements · ' + (item.sourceMessages || []).length + ' source messages'),
135
- h('div', { style: { fontWeight: 600, margin: '10px 0 6px' } }, '采用的 Memory'),
136
- [...(item.events || []), ...(item.elements || [])].map((memory) =>
137
- h('div', { key: memory.id, style: { ...card, marginBottom: '6px' } },
138
- h('div', { style: { ...row, justifyContent: 'space-between' } },
139
- h('strong', null, memory.title || memory.name || memory.id),
140
- h('span', { style: code }, memory.id)),
141
- h('div', { style: muted }, memory.summary || memory.currentState || ''))),
142
- h('div', { style: { fontWeight: 600, margin: '10px 0 6px' } }, '追溯到的原始证据'),
143
- h(SourceMessages, { messages: item.sourceMessages })))
277
+ function AuditPage({ audit, onBack }) {
278
+ return h(React.Fragment, null, h(BackBar, { label: '更多', onBack }), h('div', { className: 'sg-intro' }, h('h2', null, '使用记录'), h('p', null, '长期记忆被使用的时间与来源')), audit.length ? audit.map((item) => { const value = item.audit || {}; return h('details', { key: item.id, className: 'sg-audit' }, h('summary', null, h('strong', null, '使用了 ' + ((item.events || []).length + (item.elements || []).length) + ' 条记忆'), h('span', { className: 'sg-muted' }, ' · ' + formatTime(item.createdAt))), h('div', { className: 'sg-audit-body' }, h('div', null, (item.events || []).map((event) => event.title).join('、') || (item.elements || []).map((element) => element.name).join('、') || '旧版使用记录'), h('div', { className: 'sg-tech-row sg-audit-evidence' }, h('span', { className: 'sg-muted' }, '来源会话'), h('span', { className: 'sg-code' }, value.sessionId || '未记录')), value.turn !== undefined ? h('div', { className: 'sg-tech-row' }, h('span', { className: 'sg-muted' }, 'Turn'), h('span', null, value.turn)) : null)) }) : h(Empty, { title: '还没有使用记录', copy: 'AI 在回答中采用长期记忆后会记录在这里。' }))
279
+ }
280
+
281
+ function RawPage({ data, selected, onBack }) {
282
+ const groups = [['Block raw', data.blocks], ['Event raw', data.events], ['Element raw', data.elements], ['Usage raw', data.audit], ['模型响应', selected.successfulModelResponses || []]]
283
+ return h(React.Fragment, null, h(BackBar, { label: '更多', onBack }), h('div', { className: 'sg-intro' }, h('h2', null, '原始数据'), h('p', null, '供排查问题使用的内部字段与 JSON')), groups.map(([label, value]) => h('details', { key: label, className: 'sg-raw-group' }, h('summary', null, label + ' (' + value.length + ')'), h('pre', { className: 'sg-raw-json sg-code' }, JSON.stringify(value, null, 2)))))
284
+ }
285
+
286
+ function SettingsPage({ selected, namespace, onBack }) {
287
+ const rows = [['Schema 版本', 'v' + selected.schemaVersion], ['提取间隔', '每 ' + selected.blockTurnSize + ' 轮形成一个 Block'], ['模型', '由 DSH 当前模型配置提供'], ['项目空间 ID', namespace], ['已处理轮次', selected.currentTurn]]
288
+ return h(React.Fragment, null,
289
+ h(BackBar, { label: '更多', onBack }),
290
+ h('div', { className: 'sg-intro' }, h('h2', null, '高级设置'), h('p', null, '当前记忆空间的只读运行参数')),
291
+ h('div', { className: 'sg-pipeline' }, rows.map(([label, value]) => h('div', { key: label, className: 'sg-stage' }, h('span', null, label), h('span', { className: label === '项目空间 ID' ? 'sg-stage-value sg-code' : 'sg-stage-value' }, String(value))))))
144
292
  }
145
293
 
146
294
  function MemoryPage() {
147
295
  const [overview, setOverview] = React.useState({ namespaces: [] })
148
296
  const [namespace, setNamespace] = React.useState('')
149
- const [tab, setTab] = React.useState('overview')
150
- const [payload, setPayload] = React.useState({ items: [] })
297
+ const [section, setSection] = React.useState('long')
298
+ const [view, setView] = React.useState({ name: 'root' })
299
+ const [data, setData] = React.useState({ events: [], elements: [], blocks: [], audit: [] })
151
300
  const [query, setQuery] = React.useState('')
152
- const [detail, setDetail] = React.useState(null)
301
+ const [source, setSource] = React.useState(null)
153
302
  const [loading, setLoading] = React.useState(true)
154
303
  const [error, setError] = React.useState('')
155
304
 
156
- const refreshOverview = React.useCallback(() => {
157
- setLoading(true)
305
+ const loadOverview = React.useCallback(() => {
158
306
  setError('')
159
- return api('overview').then((data) => {
160
- setOverview(data)
161
- if (!namespace && data.namespaces && data.namespaces[0]) setNamespace(data.namespaces[0].namespace)
162
- }).catch((err) => setError(String(err.message || err))).finally(() => setLoading(false))
163
- }, [namespace])
307
+ return api('overview').then((next) => { setOverview(next); setNamespace((current) => next.namespaces?.some((item) => item.namespace === current) ? current : (next.namespaces?.[0]?.namespace || '')); return next }).catch((reason) => { setError(String(reason.message || reason)); return null })
308
+ }, [])
164
309
 
165
- React.useEffect(() => { void refreshOverview() }, [])
310
+ const loadMemoryData = React.useCallback((activeNamespace, options = {}) => {
311
+ const background = options.background === true
312
+ if (!activeNamespace) { setData({ events: [], elements: [], blocks: [], audit: [] }); if (!background) setLoading(false); return Promise.resolve() }
313
+ if (!background) setLoading(true)
314
+ return Promise.allSettled([
315
+ api('memories', { namespace: activeNamespace, kind: 'events', limit: '200' }),
316
+ api('memories', { namespace: activeNamespace, kind: 'elements', limit: '200' }),
317
+ api('memories', { namespace: activeNamespace, kind: 'blocks', limit: '200' }),
318
+ api('audit', { namespace: activeNamespace, limit: '100' }),
319
+ ]).then((results) => {
320
+ const [events, elements, blocks, audit] = results
321
+ setData((previous) => ({
322
+ events: events.status === 'fulfilled' ? events.value.items || [] : previous.events,
323
+ elements: elements.status === 'fulfilled' ? elements.value.items || [] : previous.elements,
324
+ blocks: blocks.status === 'fulfilled' ? blocks.value.items || [] : previous.blocks,
325
+ audit: audit.status === 'fulfilled' ? audit.value.items || [] : previous.audit,
326
+ }))
327
+ const failure = results.find((result) => result.status === 'rejected')
328
+ if (failure && failure.status === 'rejected') setError(String(failure.reason?.message || failure.reason))
329
+ else setError('')
330
+ }).finally(() => { if (!background) setLoading(false) })
331
+ }, [])
166
332
 
333
+ React.useEffect(() => { setLoading(true); void loadOverview().finally(() => setLoading(false)) }, [])
167
334
  React.useEffect(() => {
168
- if (!namespace || tab === 'overview') return
169
- setLoading(true)
170
- setError('')
171
- setDetail(null)
172
- const request = tab === 'audit'
173
- ? api('audit', { namespace, limit: '100' })
174
- : api('memories', { namespace, kind: tab, limit: '200' })
175
- request.then(setPayload).catch((err) => setError(String(err.message || err))).finally(() => setLoading(false))
176
- }, [namespace, tab])
177
-
178
- const openSource = (params) => {
179
- setError('')
180
- api('sources', { namespace, ...params }).then(setDetail).catch((err) => setError(String(err.message || err)))
181
- }
335
+ setView({ name: 'root' }); setSource(null); setData({ events: [], elements: [], blocks: [], audit: [] }); void loadMemoryData(namespace)
336
+ }, [namespace])
337
+ React.useEffect(() => {
338
+ if (!namespace) return undefined
339
+ const timer = window.setInterval(() => {
340
+ void Promise.all([loadOverview(), loadMemoryData(namespace, { background: true })])
341
+ }, 2500)
342
+ return () => window.clearInterval(timer)
343
+ }, [namespace, loadOverview, loadMemoryData])
182
344
 
183
345
  const selected = (overview.namespaces || []).find((item) => item.namespace === namespace)
184
- const filtered = (payload.items || []).filter((item) => !query.trim()
185
- || JSON.stringify(item).toLowerCase().includes(query.trim().toLowerCase()))
186
- const tabs = [['overview', '概览'], ['events', 'Events'], ['elements', 'Elements'], ['blocks', 'Sources'], ['audit', 'Usage Audit']]
187
-
188
- return h('div', { style: panel, 'data-testid': 'stratagate-memory-ui' },
189
- h('div', { style: { ...row, justifyContent: 'space-between' } },
190
- h('div', null,
191
- h('div', { style: { fontSize: '20px', fontWeight: 700 } }, 'StrataGate Memory'),
192
- h('div', { style: muted }, '只读记忆、来源和回答证据审计')),
193
- h('button', { onClick: () => tab === 'overview' ? refreshOverview() : setTab('overview') }, tab === 'overview' ? '刷新' : '返回概览')),
194
- h('div', { style: row },
195
- h('select', { value: namespace, onChange: (event) => setNamespace(event.target.value), style: { minWidth: '280px' } },
196
- (overview.namespaces || []).map((item) => h('option', { key: item.namespace, value: item.namespace }, item.namespace))),
197
- tabs.map(([id, label]) => h('button', { key: id, onClick: () => setTab(id), disabled: id !== 'overview' && !namespace, style: id === tab ? { fontWeight: 700 } : {} }, label))),
198
- error ? h('div', { style: { ...card, color: '#e06c75' } }, error) : null,
199
- loading ? h('div', { style: muted }, '加载中…') : null,
200
- tab === 'overview'
201
- ? selected
202
- ? h(React.Fragment, null,
203
- h('div', { style: row },
204
- h(Stat, { label: 'Blocks', value: selected.blocks }),
205
- h(Stat, { label: 'Events', value: selected.events }),
206
- h(Stat, { label: 'Elements', value: selected.elements }),
207
- h(Stat, { label: 'Usage records', value: selected.usageReceipts }),
208
- h(Stat, { label: 'Failed jobs', value: selected.failedJobs })),
209
- h(StarPrompt, { usageRecords: selected.usageReceipts }),
210
- h('div', { style: card },
211
- h('div', { style: code }, selected.namespace),
212
- h('div', { style: muted }, 'Schema v' + selected.schemaVersion + ' · turn ' + selected.currentTurn + ' · last activity ' + (selected.lastActivityAt || 'none'))))
213
- : h('div', { style: muted }, '数据库中还没有记忆 namespace。完成一些 DSH 回合后再回来查看。')
214
- : h(React.Fragment, null,
215
- tab !== 'audit' ? h('input', { value: query, onChange: (event) => setQuery(event.target.value), placeholder: '过滤当前列表…', style: { padding: '8px' } }) : null,
216
- h(Detail, { detail, onClose: () => setDetail(null) }),
217
- h('div', { style: { display: 'flex', flexDirection: 'column', gap: '8px' } },
218
- filtered.length === 0 && !loading ? h('div', { style: muted }, '没有匹配记录。') : null,
219
- filtered.map((item) => tab === 'audit'
220
- ? h(AuditItem, { key: item.id, item })
221
- : h(MemoryItem, { key: item.id, item, kind: tab, openSource })))) )
346
+ const project = projectName(namespace)
347
+ const failedCount = Number(selected?.failedJobs || 0)
348
+ const processing = !error && (Number(selected?.processingJobs || 0) > 0
349
+ || data.blocks.some((block) => block.status === 'processing'))
350
+ const goSection = (next) => { setSection(next); setView({ name: 'root' }); setSource(null) }
351
+ const sourceParams = (kind, item) => kind === 'event' ? { eventId: item.id } : kind === 'element' ? { elementId: item.id } : { blockId: item.id }
352
+ const loadSource = (kind, item) => {
353
+ setSource(null)
354
+ void api('sources', { namespace, ...sourceParams(kind, item) }).then(setSource).catch((reason) => setError(String(reason.message || reason)))
355
+ }
356
+ const openWithSource = (kind, item) => {
357
+ setView((current) => ({ name: kind, item, back: current })); loadSource(kind, item)
358
+ }
359
+ const openEvent = (event) => openWithSource('event', data.events.find((item) => item.id === event.id) || event)
360
+ const openElement = (id) => { const element = data.elements.find((item) => item.id === id); if (element) openWithSource('element', element) }
361
+ const openBlock = (block) => openWithSource('block', block)
362
+ const goBack = () => {
363
+ const previous = view.back || { name: 'root' }
364
+ setView(previous)
365
+ if (previous.item && ['event', 'element', 'block'].includes(previous.name)) loadSource(previous.name, previous.item)
366
+ else setSource(null)
367
+ }
368
+ const backLabel = view.back?.name === 'block' ? '最近记忆' : view.back?.name === 'element' ? '相关事物' : view.back?.name === 'event' ? '长期记忆' : view.back?.name === 'structure' ? '记忆结构' : section === 'recent' ? '最近记忆' : '长期记忆'
369
+ const refresh = () => Promise.all([loadOverview(), loadMemoryData(namespace)])
370
+ const moreBack = () => setView({ name: 'root' })
371
+
372
+ let content = null
373
+ if (loading && !selected) content = h(Loading)
374
+ else if (!selected) content = h(Empty, { title: '还没有记忆', copy: '完成一些 DSH 对话后,长期记忆和最近记忆会出现在这里。' })
375
+ else if (view.name === 'event') content = h(EventDetail, { event: view.item, project, source, openElement, onBack: goBack, backLabel })
376
+ else if (view.name === 'element') content = h(ElementDetail, { element: view.item, events: data.events, source, openEvent, onBack: goBack, backLabel })
377
+ else if (view.name === 'block') content = h(BlockDetail, { block: view.item, project, source, openEvent, openElement, onBack: goBack })
378
+ else if (view.name === 'status') content = h(ProcessingStatus, { overview: selected, blocks: data.blocks, onBack: () => setView({ name: 'root' }), refresh })
379
+ else if (view.name === 'structure') content = h(StructurePage, { events: data.events, elements: data.elements, openEvent, openElement, onBack: moreBack })
380
+ else if (view.name === 'system') content = h(SystemPage, { selected, blocks: data.blocks, onBack: moreBack, refresh })
381
+ else if (view.name === 'audit') content = h(AuditPage, { audit: data.audit, onBack: moreBack })
382
+ else if (view.name === 'raw') content = h(RawPage, { data, selected, onBack: moreBack })
383
+ else if (view.name === 'settings') content = h(SettingsPage, { selected, namespace, onBack: moreBack })
384
+ else content = h(React.Fragment, null,
385
+ h(FailureAlert, { count: failedCount, onOpen: () => setView({ name: 'status' }) }),
386
+ loading ? h(Loading) : section === 'long' ? h(LongTermPage, { events: data.events, elements: data.elements, project, query, setQuery, openEvent, openElement }) : section === 'recent' ? h(RecentPage, { blocks: data.blocks, project, openBlock }) : h(MoreHome, { selected, setView }))
387
+
388
+ return h('main', { className: 'sg-memory', 'data-testid': 'stratagate-memory-ui' },
389
+ h('style', null, css),
390
+ h('header', { className: 'sg-header' }, h('div', { className: 'sg-brand' }, h('div', { className: 'sg-logo', 'aria-hidden': 'true' }, '◎'), h('div', { className: 'sg-brand-name' }, 'StrataGate Memory')), h('button', { className: 'sg-icon-button', title: '重新加载', onClick: refresh, 'aria-label': '重新加载' }, '↻')),
391
+ h('div', { className: 'sg-project' }, h('span', { className: 'sg-project-label' }, '当前项目:'), h('select', { className: 'sg-project-select', value: namespace, onChange: (event) => setNamespace(event.target.value), 'aria-label': '当前项目' }, (overview.namespaces || []).map((item) => h('option', { key: item.namespace, value: item.namespace }, projectName(item.namespace))))),
392
+ h('nav', { className: 'sg-tabs', 'aria-label': '记忆视图' }, [['long', '长期记忆'], ['recent', '最近记忆'], ['more', '更多']].map(([id, label]) => h('button', { key: id, className: 'sg-tab ' + (section === id ? 'active' : ''), onClick: () => goSection(id) }, label))),
393
+ error ? h('div', { className: 'sg-error' }, h('div', { className: 'sg-error-title' }, '暂时无法读取完整记忆'), h('div', null, '已显示能够读取的内容,请稍后重新加载。'), h('details', null, h('summary', null, '技术详情'), h('div', { className: 'sg-code' }, error))) : null,
394
+ h(ProcessingAlert, { visible: processing }),
395
+ content)
222
396
  }
223
397
 
224
398
  function apply(ctx) {
225
399
  const slots = ctx.get('slots')
226
400
  if (!slots) return
227
- slots.inject('settings.section', () => slots.register(
228
- { name: 'settings.section', id: 'stratagate-memory', order: 32, label: () => 'StrataGate Memory' },
229
- () => h(MemoryPage, null),
230
- ))
401
+ slots.inject('settings.section', () => slots.register({ name: 'settings.section', id: 'stratagate-memory', order: 32, label: () => 'StrataGate Memory' }, () => h(MemoryPage, null)))
231
402
  }
232
403
 
233
404
  exports.name = 'stratagate-dsh'