@wonanut/dsh-agent-lab 0.0.0-stage → 0.2.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/CONTRACT.md +276 -0
- package/LICENSE +21 -0
- package/README.md +299 -2
- package/build.mjs +89 -0
- package/client.js +7871 -0
- package/cordis.patch.yml +3 -0
- package/icon.svg +26 -0
- package/index.js +390 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +66 -4
- package/preview/dataflow.png +0 -0
- package/preview/inspector-context.png +0 -0
- package/preview/model-switch.png +0 -0
- package/preview/overview-dark.png +0 -0
- package/preview/overview-light.png +0 -0
- package/preview/principles.png +0 -0
- package/preview/source.png +0 -0
- package/src/00-core.js +209 -0
- package/src/10-engine.js +1960 -0
- package/src/20-tools.js +600 -0
- package/src/30-llm.js +495 -0
- package/src/40-ui-kit.js +596 -0
- package/src/45-real-client.js +190 -0
- package/src/50-tui.js +472 -0
- package/src/60-inspector.js +621 -0
- package/src/65-dataflow.js +341 -0
- package/src/70-docs.js +1226 -0
- package/src/80-source.js +576 -0
- package/src/90-page.js +476 -0
- package/src/95-apply.js +58 -0
- package/test/engine.test.mjs +800 -0
- package/test/extra-tools.test.mjs +168 -0
- package/test/real-mode.test.mjs +177 -0
|
@@ -0,0 +1,621 @@
|
|
|
1
|
+
/* ─────────────────────────────────────────────────────────────
|
|
2
|
+
* Agent 实验室 · 检查器
|
|
3
|
+
* Three live views over the same snapshot:
|
|
4
|
+
* 流转 – the stage-by-stage pipeline of the current turn
|
|
5
|
+
* 上下文 – token budget, retrieved memories, summary and the exact messages
|
|
6
|
+
* 记忆 – short-term (session) vs long-term (cross-session) memory
|
|
7
|
+
* ───────────────────────────────────────────────────────────── */
|
|
8
|
+
|
|
9
|
+
const INSPECTOR_CSS = `
|
|
10
|
+
.al-inspector-panel { grid-area: inspector; }
|
|
11
|
+
.al-inspector-tabs { display: flex; gap: 3px; padding: 7px 8px; border-bottom: 1px solid var(--al-border); flex: none; background: color-mix(in srgb, var(--al-layer) 50%, transparent); }
|
|
12
|
+
.al-itab {
|
|
13
|
+
flex: 1; min-width: 0; display: inline-flex; align-items: center; justify-content: center; gap: 4px;
|
|
14
|
+
height: 27px; padding: 0 6px; border: 1px solid transparent; border-radius: 7px;
|
|
15
|
+
background: transparent; color: var(--al-ter); font-size: 11.5px; font-weight: 560; white-space: nowrap;
|
|
16
|
+
transition: color .15s ease, background .15s ease, border-color .15s ease;
|
|
17
|
+
}
|
|
18
|
+
.al-itab:hover { color: var(--al-text); background: var(--al-layer2); }
|
|
19
|
+
.al-itab.on { color: var(--al-accent); background: var(--al-accent-soft); border-color: color-mix(in srgb, var(--al-accent) 22%, transparent); }
|
|
20
|
+
.al-itab-badge { font-size: 9.5px; font-weight: 650; padding: 0 4px; height: 14px; line-height: 14px; border-radius: 999px; background: var(--al-layer2); color: var(--al-ter); }
|
|
21
|
+
.al-itab.on .al-itab-badge { background: color-mix(in srgb, var(--al-accent) 18%, transparent); color: var(--al-accent); }
|
|
22
|
+
.al-insp-body { flex: 1; min-height: 0; overflow-y: auto; padding: 10px 12px 16px; }
|
|
23
|
+
.al-insp-section { margin-bottom: 14px; }
|
|
24
|
+
.al-insp-section:last-child { margin-bottom: 2px; }
|
|
25
|
+
.al-insp-label {
|
|
26
|
+
display: flex; align-items: center; justify-content: space-between; gap: 8px;
|
|
27
|
+
margin: 0 0 6px; font-size: 10.5px; font-weight: 620; color: var(--al-ter);
|
|
28
|
+
letter-spacing: .04em; text-transform: none;
|
|
29
|
+
}
|
|
30
|
+
.al-insp-label .al-chip { height: 19px; font-size: 10px; }
|
|
31
|
+
.al-insp-note { font-size: 10.5px; color: var(--al-ter); line-height: 1.55; }
|
|
32
|
+
.al-insp-note code, .al-insp-note .al-code-inline { font-size: 10px; }
|
|
33
|
+
|
|
34
|
+
/* flow */
|
|
35
|
+
.al-flow-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 10px; }
|
|
36
|
+
.al-flow-input {
|
|
37
|
+
margin: 0 0 10px; padding: 8px 10px; border-radius: 9px;
|
|
38
|
+
border: 1px solid var(--al-border); border-left: 3px solid var(--al-accent);
|
|
39
|
+
background: color-mix(in srgb, var(--al-accent) 6%, transparent);
|
|
40
|
+
font-size: 11.5px; line-height: 1.55; word-break: break-word;
|
|
41
|
+
}
|
|
42
|
+
.al-flow-input b { font-size: 10px; color: var(--al-ter); font-weight: 600; display: block; margin-bottom: 2px; }
|
|
43
|
+
.al-stage { display: flex; gap: 8px; }
|
|
44
|
+
.al-stage-rail { flex: none; display: flex; flex-direction: column; align-items: center; width: 14px; padding-top: 5px; }
|
|
45
|
+
.al-stage-dot {
|
|
46
|
+
width: 9px; height: 9px; border-radius: 50%; flex: none;
|
|
47
|
+
border: 2px solid var(--al-border2); background: var(--al-layer);
|
|
48
|
+
transition: background .2s ease, border-color .2s ease, box-shadow .2s ease;
|
|
49
|
+
}
|
|
50
|
+
.al-stage-dot.done { background: var(--al-good); border-color: var(--al-good); }
|
|
51
|
+
.al-stage-dot.active { background: var(--al-accent); border-color: var(--al-accent); animation: al-stage-pulse 1.6s infinite; }
|
|
52
|
+
.al-stage-dot.error { background: var(--al-danger); border-color: var(--al-danger); }
|
|
53
|
+
@keyframes al-stage-pulse {
|
|
54
|
+
0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--al-accent) 42%, transparent); }
|
|
55
|
+
70% { box-shadow: 0 0 0 7px transparent; }
|
|
56
|
+
100% { box-shadow: 0 0 0 0 transparent; }
|
|
57
|
+
}
|
|
58
|
+
.al-stage-line { flex: 1; width: 1px; background: var(--al-border); min-height: 12px; margin: 3px 0; }
|
|
59
|
+
.al-stage:last-child .al-stage-line { display: none; }
|
|
60
|
+
.al-stage-main { flex: 1; min-width: 0; padding-bottom: 9px; }
|
|
61
|
+
.al-stage-head {
|
|
62
|
+
width: 100%; display: flex; align-items: center; gap: 7px; padding: 4px 6px;
|
|
63
|
+
border: 1px solid transparent; border-radius: 8px; background: transparent; text-align: left;
|
|
64
|
+
transition: background .15s ease, border-color .15s ease;
|
|
65
|
+
}
|
|
66
|
+
.al-stage-head:hover { background: var(--al-layer2); }
|
|
67
|
+
.al-stage[data-open='true'] .al-stage-head { background: var(--al-layer2); border-color: var(--al-border); }
|
|
68
|
+
.al-stage-kind { width: 22px; height: 22px; flex: none; border-radius: 6px; display: grid; place-items: center; background: var(--al-layer2); color: var(--al-sub); }
|
|
69
|
+
.al-stage-kind.active { color: var(--al-accent); background: var(--al-accent-soft); }
|
|
70
|
+
.al-stage-kind.error { color: var(--al-danger); background: color-mix(in srgb, var(--al-danger) 10%, transparent); }
|
|
71
|
+
.al-stage-title { flex: 1; min-width: 0; font-size: 11.5px; font-weight: 560; color: var(--al-text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
72
|
+
.al-stage-meta { flex: none; font-size: 10px; color: var(--al-ter); font-variant-numeric: tabular-nums; max-width: 92px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
73
|
+
.al-stage-chev { flex: none; color: var(--al-ter); transition: transform .15s ease; }
|
|
74
|
+
.al-stage[data-open='true'] .al-stage-chev { transform: rotate(90deg); }
|
|
75
|
+
.al-stage-detail {
|
|
76
|
+
margin: 3px 0 0 28px; padding: 8px; border-radius: 8px;
|
|
77
|
+
border: 1px solid var(--al-border); background: color-mix(in srgb, var(--al-layer2) 52%, transparent);
|
|
78
|
+
font-size: 11px; color: var(--al-sub); animation: al-detail-in .16s ease;
|
|
79
|
+
}
|
|
80
|
+
@keyframes al-detail-in { from { opacity: 0; transform: translateY(-2px); } to { opacity: 1; transform: translateY(0); } }
|
|
81
|
+
.al-detail-text { margin: 0; font-size: 11px; line-height: 1.65; white-space: pre-wrap; word-break: break-word; }
|
|
82
|
+
.al-detail-row { display: flex; align-items: baseline; gap: 6px; margin-bottom: 5px; }
|
|
83
|
+
.al-detail-row:last-child { margin-bottom: 0; }
|
|
84
|
+
.al-detail-k { flex: none; font-size: 10px; color: var(--al-ter); min-width: 42px; }
|
|
85
|
+
.al-detail-v { flex: 1; min-width: 0; font-size: 11px; color: var(--al-text); word-break: break-word; }
|
|
86
|
+
.al-detail-kv { display: flex; flex-wrap: wrap; gap: 4px; }
|
|
87
|
+
.al-kv-pill { display: inline-flex; gap: 4px; align-items: baseline; padding: 2px 7px; border-radius: 6px; background: var(--al-layer2); font-size: 10.5px; }
|
|
88
|
+
.al-kv-pill b { color: var(--al-accent); font-weight: 600; }
|
|
89
|
+
.al-role { display: inline-flex; align-items: center; height: 15px; padding: 0 5px; border-radius: 4px; font-size: 9px; font-weight: 650; letter-spacing: .02em; }
|
|
90
|
+
.al-role-user { background: rgba(34, 197, 94, .13); color: #15803d; }
|
|
91
|
+
.al-role-assistant { background: rgba(139, 92, 246, .13); color: #7c3aed; }
|
|
92
|
+
.al-role-tool { background: rgba(245, 158, 11, .14); color: #b45309; }
|
|
93
|
+
.al-role-system { background: rgba(107, 114, 128, .14); color: var(--al-sub); }
|
|
94
|
+
.al-stage-io { margin-top: 6px; }
|
|
95
|
+
.al-stage-io > * + * { margin-top: 5px; }
|
|
96
|
+
.al-mini-msg { display: flex; gap: 5px; align-items: baseline; padding: 2px 0; font-size: 10.5px; line-height: 1.5; }
|
|
97
|
+
.al-mini-msg .al-role { flex: none; transform: scale(.92); transform-origin: left center; }
|
|
98
|
+
.al-mini-msg span:last-child { color: var(--al-sub); word-break: break-word; }
|
|
99
|
+
.al-flow-stats { display: flex; flex-wrap: wrap; gap: 5px 12px; margin-top: 8px; padding-top: 8px; border-top: 1px dashed var(--al-border); font-size: 10px; color: var(--al-ter); }
|
|
100
|
+
.al-flow-stats b { color: var(--al-text); font-weight: 600; font-variant-numeric: tabular-nums; }
|
|
101
|
+
|
|
102
|
+
/* context */
|
|
103
|
+
.al-budget { padding: 10px 11px; border-radius: 10px; border: 1px solid var(--al-border); background: color-mix(in srgb, var(--al-layer2) 45%, transparent); margin-bottom: 12px; }
|
|
104
|
+
.al-budget-top { display: flex; align-items: flex-end; justify-content: space-between; gap: 8px; margin-bottom: 8px; }
|
|
105
|
+
.al-budget-num { font-size: 20px; font-weight: 700; letter-spacing: -.03em; line-height: 1; font-variant-numeric: tabular-nums; }
|
|
106
|
+
.al-budget-num span { font-size: 10.5px; font-weight: 500; color: var(--al-ter); letter-spacing: 0; }
|
|
107
|
+
.al-budget-sub { font-size: 10px; color: var(--al-ter); text-align: right; line-height: 1.4; }
|
|
108
|
+
.al-win-row { display: flex; flex-wrap: wrap; gap: 5px 14px; margin-top: 9px; padding-top: 8px; border-top: 1px dashed var(--al-border); font-size: 10.5px; color: var(--al-sub); }
|
|
109
|
+
.al-win-row b { color: var(--al-text); font-weight: 620; font-variant-numeric: tabular-nums; }
|
|
110
|
+
.al-win-row .al-win-real b { color: #7c3aed; }
|
|
111
|
+
.al-budget-control { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: 8px; padding-top: 8px; border-top: 1px dashed var(--al-border); }
|
|
112
|
+
.al-budget-control span { font-size: 10.5px; color: var(--al-ter); }
|
|
113
|
+
.al-budget-control select {
|
|
114
|
+
height: 26px; border: 1px solid var(--al-border2); border-radius: 7px;
|
|
115
|
+
background: var(--al-layer); color: var(--al-text); font-size: 11px; padding: 0 6px;
|
|
116
|
+
}
|
|
117
|
+
.al-hit-list { display: flex; flex-direction: column; gap: 5px; }
|
|
118
|
+
.al-hit { padding: 6px 8px; border-radius: 7px; border: 1px solid color-mix(in srgb, #14b8a6 24%, transparent); background: color-mix(in srgb, #14b8a6 7%, transparent); }
|
|
119
|
+
.al-hit b { display: block; font-size: 10px; color: #0f766e; font-weight: 640; }
|
|
120
|
+
.al-hit span { font-size: 10.5px; color: var(--al-sub); word-break: break-word; }
|
|
121
|
+
.al-summary-box { padding: 8px 10px; border-radius: 8px; border: 1px dashed color-mix(in srgb, var(--al-warn) 40%, transparent); background: color-mix(in srgb, var(--al-warn) 7%, transparent); }
|
|
122
|
+
.al-summary-box p { margin: 0; font-size: 10.5px; line-height: 1.6; color: var(--al-sub); white-space: pre-wrap; word-break: break-word; max-height: 130px; overflow: auto; }
|
|
123
|
+
.al-msg-list { display: flex; flex-direction: column; gap: 5px; }
|
|
124
|
+
.al-msg {
|
|
125
|
+
border: 1px solid var(--al-border); border-radius: 8px; background: color-mix(in srgb, var(--al-layer) 70%, transparent);
|
|
126
|
+
overflow: hidden;
|
|
127
|
+
}
|
|
128
|
+
.al-msg-head { display: flex; align-items: center; gap: 5px; padding: 5px 7px; background: color-mix(in srgb, var(--al-layer2) 48%, transparent); }
|
|
129
|
+
.al-msg-kind { font-size: 9px; color: var(--al-ter); padding: 0 4px; border-radius: 3px; background: var(--al-layer2); }
|
|
130
|
+
.al-msg-tok { margin-left: auto; font-size: 9.5px; color: var(--al-ter); font-variant-numeric: tabular-nums; flex: none; }
|
|
131
|
+
.al-msg-body {
|
|
132
|
+
padding: 5px 8px 7px; font-size: 10.5px; line-height: 1.58; color: var(--al-sub);
|
|
133
|
+
white-space: pre-wrap; word-break: break-word; max-height: 92px; overflow: hidden;
|
|
134
|
+
display: -webkit-box; -webkit-line-clamp: 5; -webkit-box-orient: vertical;
|
|
135
|
+
}
|
|
136
|
+
.al-msg-body.open { display: block; max-height: 260px; overflow: auto; }
|
|
137
|
+
.al-msg-more { border: 0; background: transparent; color: var(--al-accent); font-size: 10px; padding: 0 8px 6px; }
|
|
138
|
+
.al-context-foot { margin-top: 10px; padding-top: 8px; border-top: 1px dashed var(--al-border); }
|
|
139
|
+
|
|
140
|
+
/* memory */
|
|
141
|
+
.al-mem-list { display: flex; flex-direction: column; gap: 6px; }
|
|
142
|
+
.al-mem {
|
|
143
|
+
display: flex; align-items: flex-start; gap: 7px; padding: 7px 8px;
|
|
144
|
+
border: 1px solid var(--al-border); border-radius: 8px; background: color-mix(in srgb, var(--al-layer2) 40%, transparent);
|
|
145
|
+
}
|
|
146
|
+
.al-mem-icon { flex: none; width: 20px; height: 20px; border-radius: 6px; display: grid; place-items: center; background: var(--al-accent-soft); color: var(--al-accent); margin-top: 1px; }
|
|
147
|
+
.al-mem-main { flex: 1; min-width: 0; }
|
|
148
|
+
.al-mem-key { font-size: 11px; font-weight: 620; color: var(--al-text); word-break: break-word; }
|
|
149
|
+
.al-mem-value { font-size: 10.5px; color: var(--al-sub); line-height: 1.55; word-break: break-word; margin-top: 1px; white-space: pre-wrap; }
|
|
150
|
+
.al-mem-meta { display: flex; align-items: center; gap: 6px; margin-top: 3px; font-size: 9px; color: var(--al-ter); }
|
|
151
|
+
.al-mem-del { flex: none; }
|
|
152
|
+
.al-mem-del.confirm { color: var(--al-danger); }
|
|
153
|
+
.al-mini-stats { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 6px; margin-bottom: 12px; }
|
|
154
|
+
`;
|
|
155
|
+
|
|
156
|
+
const STAGE_META = {
|
|
157
|
+
input: { icon: 'terminal', label: '输入' },
|
|
158
|
+
context: { icon: 'layers', label: '上下文' },
|
|
159
|
+
compress: { icon: 'compress', label: '压缩' },
|
|
160
|
+
model: { icon: 'cpu', label: '模型' },
|
|
161
|
+
tool: { icon: 'tools', label: '工具' },
|
|
162
|
+
memory: { icon: 'memory', label: '记忆' },
|
|
163
|
+
output: { icon: 'send', label: '输出' },
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
function fmtDuration(ms) {
|
|
167
|
+
const value = Math.max(0, Math.round(ms || 0));
|
|
168
|
+
if (value < 1000) return value + ' ms';
|
|
169
|
+
return (value / 1000).toFixed(value < 10000 ? 1 : 0) + ' s';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function truncateText(text, max) {
|
|
173
|
+
const value = String(text == null ? '' : text);
|
|
174
|
+
return value.length > max ? value.slice(0, max) + '…' : value;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function StageDetail(props) {
|
|
178
|
+
const stage = props.stage;
|
|
179
|
+
const detail = stage.detail;
|
|
180
|
+
if (detail == null) return e('p', { className: 'al-detail-text' }, '(无额外细节)');
|
|
181
|
+
|
|
182
|
+
if (stage.kind === 'input') {
|
|
183
|
+
return e('p', { className: 'al-detail-text' }, typeof detail === 'string' ? detail : (detail.text || prettyJson(detail)));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (stage.kind === 'context') {
|
|
187
|
+
const info = detail || {};
|
|
188
|
+
return e(
|
|
189
|
+
'div',
|
|
190
|
+
null,
|
|
191
|
+
info.segments
|
|
192
|
+
? e('div', { style: { marginBottom: 8 } }, e(TokenBar, { segments: info.segments, limit: info.limit, height: 7, showLegend: true }))
|
|
193
|
+
: null,
|
|
194
|
+
e('div', { className: 'al-detail-row' }, e('span', { className: 'al-detail-k' }, '消息'), e('span', { className: 'al-detail-v' }, (info.messageCount != null ? info.messageCount : (info.messages || []).length) + ' 条 · 约 ' + (info.used != null ? info.used : '?') + ' tok')),
|
|
195
|
+
info.hits && info.hits.length
|
|
196
|
+
? e('div', { className: 'al-detail-row' }, e('span', { className: 'al-detail-k' }, '召回'), e('div', { className: 'al-detail-v al-detail-kv' }, info.hits.map((hit) => e('span', { key: hit.id, className: 'al-kv-pill' }, e('b', null, hit.key), '= ' + truncateText(hit.value, 26)))))
|
|
197
|
+
: null,
|
|
198
|
+
info.summary ? e('div', { className: 'al-detail-row' }, e('span', { className: 'al-detail-k' }, '摘要'), e('span', { className: 'al-detail-v' }, truncateText(info.summary, 160))) : null,
|
|
199
|
+
info.messages && info.messages.length
|
|
200
|
+
? e('div', { className: 'al-stage-io' }, info.messages.map((message, index) => e('div', { className: 'al-mini-msg', key: index }, e('span', { className: 'al-role al-role-' + (message.role || 'system') }, message.role || 'system'), e('span', null, truncateText(message.content, 110)))))
|
|
201
|
+
: null,
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (stage.kind === 'compress') {
|
|
206
|
+
return e(
|
|
207
|
+
'div',
|
|
208
|
+
null,
|
|
209
|
+
e('div', { className: 'al-detail-row' }, e('span', { className: 'al-detail-k' }, 'token'), e('span', { className: 'al-detail-v' }, (detail.before != null ? detail.before : '?') + ' → ' + (detail.after != null ? detail.after : '?') + ' tok · 折叠 ' + (detail.removed != null ? detail.removed : '?') + ' 条消息')),
|
|
210
|
+
detail.summary ? e('div', { className: 'al-summary-box' }, e('p', null, detail.summary)) : null,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (stage.kind === 'model') {
|
|
215
|
+
const response = detail.response || {};
|
|
216
|
+
const isTool = response.type === 'tool_call';
|
|
217
|
+
return e(
|
|
218
|
+
'div',
|
|
219
|
+
null,
|
|
220
|
+
e('div', { className: 'al-detail-row' }, e('span', { className: 'al-detail-k' }, '第 ' + (detail.step || 1) + ' 步'), e('span', { className: 'al-detail-v' }, isTool ? '模型决定调用工具 ' + response.name : '模型直接给出最终回答')),
|
|
221
|
+
detail.model
|
|
222
|
+
? e('div', { className: 'al-detail-row' }, e('span', { className: 'al-detail-k' }, '模型'), e('span', { className: 'al-detail-v' }, (detail.model.provider || '') + ' / ' + (detail.model.model || '') + (detail.model.latencyMs != null ? ' · ' + detail.model.latencyMs + ' ms' : '') + (detail.model.usage ? ' · 输入 ' + (detail.model.usage.inputTokens || 0) + ' / 输出 ' + (detail.model.usage.outputTokens || 0) + ' tok' : '')))
|
|
223
|
+
: null,
|
|
224
|
+
isTool
|
|
225
|
+
? e('div', { className: 'al-stage-io' }, e(JsonView, { value: response.args, style: { maxHeight: 120 } }))
|
|
226
|
+
: e('p', { className: 'al-detail-text' }, truncateText(response.text || '', 260)),
|
|
227
|
+
detail.toolSchemas
|
|
228
|
+
? e('details', { className: 'al-stage-io' }, e('summary', { className: 'al-insp-note', style: { cursor: 'pointer', marginBottom: 4 } }, '模型这次看到的工具定义(' + detail.toolSchemas.length + ' 个)'), e(JsonView, { value: detail.toolSchemas, style: { maxHeight: 180 } }))
|
|
229
|
+
: null,
|
|
230
|
+
detail.messages
|
|
231
|
+
? e('details', { className: 'al-stage-io' }, e('summary', { className: 'al-insp-note', style: { cursor: 'pointer', marginBottom: 4 } }, '发给模型的消息(' + detail.messages.length + ' 条)'), e(JsonView, { value: detail.messages, style: { maxHeight: 180 } }))
|
|
232
|
+
: null,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (stage.kind === 'tool') {
|
|
237
|
+
const info = detail || {};
|
|
238
|
+
return e(
|
|
239
|
+
'div',
|
|
240
|
+
null,
|
|
241
|
+
e('div', { className: 'al-detail-row' }, e('span', { className: 'al-detail-k' }, '工具'), e('span', { className: 'al-detail-v' }, info.name || stage.title)),
|
|
242
|
+
info.args ? e('div', { className: 'al-stage-io' }, e(JsonView, { value: info.args, style: { maxHeight: 110 } })) : null,
|
|
243
|
+
e('div', { className: 'al-stage-io' }, e('div', { className: 'al-insp-label', style: { marginBottom: 4 } }, info.ok === false ? '结果 · 失败' : '结果'), e(JsonView, { value: info.result, style: { maxHeight: 130 } })),
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (stage.kind === 'output') {
|
|
248
|
+
return e('p', { className: 'al-detail-text' }, typeof detail === 'string' ? detail : (detail.text || prettyJson(detail)));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return e(JsonView, { value: detail, style: { maxHeight: 220 } });
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function StageRow(props) {
|
|
255
|
+
const stage = props.stage;
|
|
256
|
+
const meta = STAGE_META[stage.kind] || STAGE_META.input;
|
|
257
|
+
const open = props.open;
|
|
258
|
+
return e(
|
|
259
|
+
'div',
|
|
260
|
+
{ className: 'al-stage', 'data-open': open ? 'true' : 'false' },
|
|
261
|
+
e(
|
|
262
|
+
'div',
|
|
263
|
+
{ className: 'al-stage-rail' },
|
|
264
|
+
e('span', { className: 'al-stage-dot ' + (stage.status || '') }),
|
|
265
|
+
e('span', { className: 'al-stage-line' }),
|
|
266
|
+
),
|
|
267
|
+
e(
|
|
268
|
+
'div',
|
|
269
|
+
{ className: 'al-stage-main' },
|
|
270
|
+
e(
|
|
271
|
+
'button',
|
|
272
|
+
{ type: 'button', className: 'al-stage-head', onClick: props.onToggle, 'aria-expanded': open ? 'true' : 'false' },
|
|
273
|
+
e('span', { className: 'al-stage-kind ' + (stage.status || '') }, e(Icon, { name: meta.icon, size: 12 })),
|
|
274
|
+
e('span', { className: 'al-stage-title' }, stage.title || meta.label),
|
|
275
|
+
e('span', { className: 'al-stage-meta' }, stage.meta || ''),
|
|
276
|
+
e('span', { className: 'al-stage-chev' }, e(Icon, { name: 'chevronRight', size: 12 })),
|
|
277
|
+
),
|
|
278
|
+
open ? e('div', { className: 'al-stage-detail' }, e(StageDetail, { stage })) : null,
|
|
279
|
+
),
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function FlowTab(props) {
|
|
284
|
+
const view = props.view;
|
|
285
|
+
const turn = view.turn;
|
|
286
|
+
const [openId, setOpenId] = useState(null);
|
|
287
|
+
const activeStage = turn ? (turn.stages || []).slice().reverse().find((stage) => stage.status === 'active') : null;
|
|
288
|
+
const activeId = activeStage ? activeStage.id : null;
|
|
289
|
+
|
|
290
|
+
useEffect(() => {
|
|
291
|
+
if (!turn) {
|
|
292
|
+
setOpenId(null);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
if (activeId) setOpenId(activeId);
|
|
296
|
+
}, [turn && turn.id, activeId]);
|
|
297
|
+
|
|
298
|
+
if (!turn) {
|
|
299
|
+
return e(
|
|
300
|
+
'div',
|
|
301
|
+
null,
|
|
302
|
+
e(
|
|
303
|
+
'div',
|
|
304
|
+
{ className: 'al-flow-head' },
|
|
305
|
+
e(Chip, { tone: 'brand', icon: 'route' }, '等待输入'),
|
|
306
|
+
e('span', { className: 'al-insp-note' }, '实时'),
|
|
307
|
+
),
|
|
308
|
+
e(Empty, {
|
|
309
|
+
icon: 'route',
|
|
310
|
+
title: '还没有处理链路',
|
|
311
|
+
body: '在左边终端发一条消息,这里会逐步出现:输入 → 上下文组装 → 模型推理 → 工具执行 → 记忆 → 输出。点任一环节可展开细节。',
|
|
312
|
+
}),
|
|
313
|
+
e(
|
|
314
|
+
'div',
|
|
315
|
+
{ className: 'al-insp-section', style: { marginTop: 10 } },
|
|
316
|
+
e('div', { className: 'al-insp-label' }, '一轮消息里发生了什么'),
|
|
317
|
+
e(
|
|
318
|
+
'div',
|
|
319
|
+
{ className: 'al-stages-preview', style: { display: 'flex', flexDirection: 'column', gap: 7 } },
|
|
320
|
+
[
|
|
321
|
+
['terminal', '输入被追加到会话日志'],
|
|
322
|
+
['layers', '系统提示 + 摘要 + 召回记忆 + 最近消息 → 上下文'],
|
|
323
|
+
['compress', '超过 token 预算时,旧消息折叠成摘要'],
|
|
324
|
+
['cpu', '模型只返回「工具调用」或「最终回答」'],
|
|
325
|
+
['tools', '框架执行工具,把结果作为消息写回上下文'],
|
|
326
|
+
['memory', '需要记住的事实写进跨会话长期记忆'],
|
|
327
|
+
['send', '最终回答流式显示,并持久化整个会话'],
|
|
328
|
+
].map((row) =>
|
|
329
|
+
e(
|
|
330
|
+
'div',
|
|
331
|
+
{ key: row[1], style: { display: 'flex', gap: 7, alignItems: 'flex-start', fontSize: 10.5, color: 'var(--al-sub)', lineHeight: 1.5 } },
|
|
332
|
+
e('span', { style: { color: 'var(--al-accent)', flex: 'none', marginTop: 1 } }, e(Icon, { name: row[0], size: 12 })),
|
|
333
|
+
e('span', null, row[1]),
|
|
334
|
+
),
|
|
335
|
+
),
|
|
336
|
+
),
|
|
337
|
+
),
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const stages = turn.stages || [];
|
|
342
|
+
const toolCalls = stages.filter((stage) => stage.kind === 'tool').length;
|
|
343
|
+
const modelCalls = stages.filter((stage) => stage.kind === 'model').length;
|
|
344
|
+
const duration = turn.endedAt ? turn.endedAt - turn.startedAt : Date.now() - turn.startedAt;
|
|
345
|
+
|
|
346
|
+
return e(
|
|
347
|
+
'div',
|
|
348
|
+
null,
|
|
349
|
+
e(
|
|
350
|
+
'div',
|
|
351
|
+
{ className: 'al-flow-head' },
|
|
352
|
+
e(Chip, { tone: turn.status === 'running' ? 'brand' : turn.status === 'error' ? 'danger' : 'good', icon: turn.status === 'running' ? 'bolt' : turn.status === 'error' ? 'alert' : 'check' }, turn.status === 'running' ? '运行中' : turn.status === 'error' ? '出错' : '完成'),
|
|
353
|
+
e(Chip, { tone: view.modelMode === 'real' ? 'violet' : 'neutral', icon: view.modelMode === 'real' ? 'bolt' : 'cpu' }, view.modelMode === 'real' ? '真实模型' : '模拟模型'),
|
|
354
|
+
e('span', { className: 'al-insp-note' }, fmtDuration(duration)),
|
|
355
|
+
),
|
|
356
|
+
e('div', { className: 'al-flow-input' }, e('b', null, '用户输入'), turn.input),
|
|
357
|
+
e(
|
|
358
|
+
'div',
|
|
359
|
+
null,
|
|
360
|
+
stages.map((stage) =>
|
|
361
|
+
e(StageRow, {
|
|
362
|
+
key: stage.id,
|
|
363
|
+
stage,
|
|
364
|
+
open: openId === stage.id,
|
|
365
|
+
onToggle: () => setOpenId(openId === stage.id ? null : stage.id),
|
|
366
|
+
}),
|
|
367
|
+
),
|
|
368
|
+
),
|
|
369
|
+
e(
|
|
370
|
+
'div',
|
|
371
|
+
{ className: 'al-flow-stats' },
|
|
372
|
+
e('span', null, '模型调用 ', e('b', null, String(modelCalls))),
|
|
373
|
+
e('span', null, '工具调用 ', e('b', null, String(toolCalls))),
|
|
374
|
+
e('span', null, '上下文 ', e('b', null, (view.context ? view.context.used : '?') + ' tok')),
|
|
375
|
+
e('span', null, '压缩 ', e('b', null, String(view.stats ? view.stats.compressions : 0))),
|
|
376
|
+
),
|
|
377
|
+
e(
|
|
378
|
+
'div',
|
|
379
|
+
{ className: 'al-insp-section', style: { marginTop: 10 } },
|
|
380
|
+
e('div', { className: 'al-insp-note' }, '提示:模型只看得到「上下文」里的消息;工具结果必须写回上下文,模型才能继续。'),
|
|
381
|
+
),
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function ContextMsg(props) {
|
|
386
|
+
const message = props.message;
|
|
387
|
+
const [open, setOpen] = useState(false);
|
|
388
|
+
const content = message.content || '';
|
|
389
|
+
const canExpand = content.length > 180;
|
|
390
|
+
return e(
|
|
391
|
+
'div',
|
|
392
|
+
{ className: 'al-msg' },
|
|
393
|
+
e(
|
|
394
|
+
'div',
|
|
395
|
+
{ className: 'al-msg-head' },
|
|
396
|
+
e('span', { className: 'al-role al-role-' + (message.role || 'system') }, message.role || 'system'),
|
|
397
|
+
message.kind ? e('span', { className: 'al-msg-kind' }, message.kind) : null,
|
|
398
|
+
message.toolName ? e('span', { className: 'al-msg-kind' }, message.toolName) : null,
|
|
399
|
+
e('span', { className: 'al-msg-tok' }, message.tokens + ' tok'),
|
|
400
|
+
),
|
|
401
|
+
e('div', { className: 'al-msg-body' + (open ? ' open' : '') }, content || '(空)'),
|
|
402
|
+
canExpand ? e('button', { type: 'button', className: 'al-msg-more', onClick: () => setOpen(!open) }, open ? '收起' : '展开全文') : null,
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function ContextTab(props) {
|
|
407
|
+
const view = props.view;
|
|
408
|
+
const budgetOptions = [1000, 2000, 8000, 32000, 128000, 1048576];
|
|
409
|
+
const context = view.context || { limit: 0, used: 0, segments: [], hits: [], summary: null };
|
|
410
|
+
const messages = view.messages || [];
|
|
411
|
+
const pct = context.limit > 0 ? Math.round((context.used / context.limit) * 100) : 0;
|
|
412
|
+
return e(
|
|
413
|
+
'div',
|
|
414
|
+
null,
|
|
415
|
+
e(
|
|
416
|
+
'div',
|
|
417
|
+
{ className: 'al-budget' },
|
|
418
|
+
e(
|
|
419
|
+
'div',
|
|
420
|
+
{ className: 'al-budget-top' },
|
|
421
|
+
e('div', { className: 'al-budget-num' }, String(context.used), e('span', null, ' / ' + context.limit + ' tok')),
|
|
422
|
+
e('div', { className: 'al-budget-sub' }, '预算占用 ' + pct + '%', e('br'), '超出 90% 自动压缩'),
|
|
423
|
+
),
|
|
424
|
+
e(TokenBar, { segments: context.segments || [], limit: context.limit, height: 9 }),
|
|
425
|
+
e(
|
|
426
|
+
'div',
|
|
427
|
+
{ className: 'al-win-row' },
|
|
428
|
+
e('span', null, '教学预算 ', e('b', null, alFormatTokens(context.limit) + ' tok')),
|
|
429
|
+
e(
|
|
430
|
+
'span',
|
|
431
|
+
{ className: 'al-win-real' },
|
|
432
|
+
'真实模型窗口 ',
|
|
433
|
+
e('b', null, view.model && view.model.contextWindow ? alFormatTokens(view.model.contextWindow) + ' tok' : '—'),
|
|
434
|
+
),
|
|
435
|
+
view.model && view.model.maxOutputTokens
|
|
436
|
+
? e('span', null, '输出上限 ', e('b', null, alFormatTokens(view.model.maxOutputTokens) + ' tok'))
|
|
437
|
+
: null,
|
|
438
|
+
),
|
|
439
|
+
e(
|
|
440
|
+
'div',
|
|
441
|
+
{ className: 'al-budget-control' },
|
|
442
|
+
e('span', null, '教学预算(实时生效)'),
|
|
443
|
+
e(
|
|
444
|
+
'select',
|
|
445
|
+
{
|
|
446
|
+
value: context.limit,
|
|
447
|
+
onChange: (event) => props.onBudget && props.onBudget(Number(event.target.value)),
|
|
448
|
+
title: '调整教学上下文预算;数值越大压缩触发越少',
|
|
449
|
+
},
|
|
450
|
+
(budgetOptions.indexOf(context.limit) >= 0 ? budgetOptions : budgetOptions.concat([context.limit]))
|
|
451
|
+
.filter((n) => Number.isFinite(n) && n > 0)
|
|
452
|
+
.map((n) => e('option', { key: n, value: n }, alFormatTokens(n) + ' tok')),
|
|
453
|
+
),
|
|
454
|
+
),
|
|
455
|
+
e(
|
|
456
|
+
'div',
|
|
457
|
+
{ className: 'al-insp-note', style: { marginTop: 5 } },
|
|
458
|
+
'压缩阈值基于教学预算;真实模型窗口来自 DSH 模型元数据,只作参考。默认 8000 tok,可按需调大。',
|
|
459
|
+
),
|
|
460
|
+
),
|
|
461
|
+
e(
|
|
462
|
+
'div',
|
|
463
|
+
{ className: 'al-insp-section' },
|
|
464
|
+
e('div', { className: 'al-insp-label' }, e('span', null, '本轮自动召回'), e(Chip, { tone: (context.hits || []).length ? 'good' : 'neutral' }, String((context.hits || []).length) + ' 条')),
|
|
465
|
+
(context.hits || []).length
|
|
466
|
+
? e('div', { className: 'al-hit-list' }, context.hits.map((hit) => e('div', { key: hit.id, className: 'al-hit' }, e('b', null, hit.key), e('span', null, hit.value))))
|
|
467
|
+
: e('div', { className: 'al-insp-note' }, '没有匹配的长期记忆。长期记忆只在关键词相关时才会注入,避免污染上下文。'),
|
|
468
|
+
),
|
|
469
|
+
e(
|
|
470
|
+
'div',
|
|
471
|
+
{ className: 'al-insp-section' },
|
|
472
|
+
e('div', { className: 'al-insp-label' }, e('span', null, '历史摘要 · 关键内容 / 未完成任务 / 下一步'), context.summary ? e(Chip, { tone: 'warn' }, '已生效') : null),
|
|
473
|
+
context.summary
|
|
474
|
+
? e('div', { className: 'al-summary-box' }, e('p', null, context.summary))
|
|
475
|
+
: e('div', { className: 'al-insp-note' }, '还没有压缩。等上下文接近预算,旧消息会被折叠成摘要;也可以点终端里的 /compact。'),
|
|
476
|
+
),
|
|
477
|
+
e(
|
|
478
|
+
'div',
|
|
479
|
+
{ className: 'al-insp-section' },
|
|
480
|
+
e('div', { className: 'al-insp-label' }, e('span', null, '模型将看到的完整消息'), e('span', { className: 'al-insp-note' }, String(messages.length) + ' 条')),
|
|
481
|
+
messages.length
|
|
482
|
+
? e('div', { className: 'al-msg-list' }, messages.map((message) => e(ContextMsg, { key: message.id, message })))
|
|
483
|
+
: e('div', { className: 'al-insp-note' }, '还没有消息。'),
|
|
484
|
+
e(
|
|
485
|
+
'div',
|
|
486
|
+
{ className: 'al-context-foot al-insp-note' },
|
|
487
|
+
'顺序:system 提示 → 历史摘要 → 召回记忆 → 对话消息 → 本轮输入。工具调用和结果也是消息,所以模型能「看见」自己刚才做了什么。',
|
|
488
|
+
),
|
|
489
|
+
),
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function MemoryTab(props) {
|
|
494
|
+
const view = props.view;
|
|
495
|
+
const memory = view.memory || [];
|
|
496
|
+
const [confirmId, setConfirmId] = useState(null);
|
|
497
|
+
const [confirmClear, setConfirmClear] = useState(false);
|
|
498
|
+
const stats = view.stats || {};
|
|
499
|
+
const session = (view.sessions || []).find((item) => item.id === view.activeId);
|
|
500
|
+
return e(
|
|
501
|
+
'div',
|
|
502
|
+
null,
|
|
503
|
+
e(
|
|
504
|
+
'div',
|
|
505
|
+
{ className: 'al-mini-stats' },
|
|
506
|
+
e(Stat, { value: String(memory.length), label: '长期记忆' }),
|
|
507
|
+
e(Stat, { value: String((view.messages || []).length), label: '本会话消息' }),
|
|
508
|
+
e(Stat, { value: String(stats.compressions || 0), label: '累计压缩' }),
|
|
509
|
+
),
|
|
510
|
+
e(
|
|
511
|
+
'div',
|
|
512
|
+
{ className: 'al-insp-section' },
|
|
513
|
+
e('div', { className: 'al-insp-label' }, e('span', null, '长期记忆 · 跨会话'), e(Chip, { tone: 'violet', icon: 'memory' }, 'localStorage')),
|
|
514
|
+
memory.length
|
|
515
|
+
? e(
|
|
516
|
+
'div',
|
|
517
|
+
{ className: 'al-mem-list' },
|
|
518
|
+
memory.map((entry) =>
|
|
519
|
+
e(
|
|
520
|
+
'div',
|
|
521
|
+
{ key: entry.id, className: 'al-mem' },
|
|
522
|
+
e('span', { className: 'al-mem-icon' }, e(Icon, { name: 'key', size: 11 })),
|
|
523
|
+
e(
|
|
524
|
+
'div',
|
|
525
|
+
{ className: 'al-mem-main' },
|
|
526
|
+
e('div', { className: 'al-mem-key' }, entry.key),
|
|
527
|
+
e('div', { className: 'al-mem-value' }, entry.value),
|
|
528
|
+
e(
|
|
529
|
+
'div',
|
|
530
|
+
{ className: 'al-mem-meta' },
|
|
531
|
+
e('span', null, entry.source || '记忆工具'),
|
|
532
|
+
e('span', null, '·'),
|
|
533
|
+
e('span', null, fmtTime(entry.updatedAt || entry.createdAt)),
|
|
534
|
+
),
|
|
535
|
+
),
|
|
536
|
+
e(IconBtn, {
|
|
537
|
+
icon: confirmId === entry.id ? 'alert' : 'trash',
|
|
538
|
+
size: 13,
|
|
539
|
+
danger: confirmId === entry.id,
|
|
540
|
+
className: 'al-mem-del' + (confirmId === entry.id ? ' confirm' : ''),
|
|
541
|
+
title: confirmId === entry.id ? '再点一次删除' : '删除这条记忆',
|
|
542
|
+
onClick: () => {
|
|
543
|
+
if (confirmId === entry.id) {
|
|
544
|
+
props.onDeleteMemory(entry.id);
|
|
545
|
+
setConfirmId(null);
|
|
546
|
+
} else {
|
|
547
|
+
setConfirmId(entry.id);
|
|
548
|
+
}
|
|
549
|
+
},
|
|
550
|
+
}),
|
|
551
|
+
),
|
|
552
|
+
),
|
|
553
|
+
)
|
|
554
|
+
: e(Empty, { icon: 'memory', title: '还没有长期记忆', body: '对它说「记住 我的项目代号是 Orion」,下次换一个会话它也能想起来。' }),
|
|
555
|
+
),
|
|
556
|
+
e(
|
|
557
|
+
'div',
|
|
558
|
+
{ className: 'al-insp-section' },
|
|
559
|
+
e('div', { className: 'al-insp-label' }, e('span', null, '短期记忆 · 当前会话'), session ? e('span', { className: 'al-insp-note' }, session.messageCount + ' 条 / ' + session.tokens + ' tok') : null),
|
|
560
|
+
e('div', { className: 'al-insp-note' }, '短期记忆就是当前会话的消息窗口:每次请求都会把最近的消息重新发回模型。窗口满之前靠压缩摘要续命,跨会话则靠上面的长期记忆。'),
|
|
561
|
+
e(
|
|
562
|
+
'div',
|
|
563
|
+
{ style: { marginTop: 8 } },
|
|
564
|
+
confirmClear
|
|
565
|
+
? e(
|
|
566
|
+
'div',
|
|
567
|
+
{ style: { display: 'flex', gap: 6, flexWrap: 'wrap' } },
|
|
568
|
+
e(Btn, { size: 'sm', variant: 'danger', onClick: () => { props.onClearMemory(); setConfirmClear(false); } }, '确认清空 ' + memory.length + ' 条'),
|
|
569
|
+
e(Btn, { size: 'sm', variant: 'ghost', onClick: () => setConfirmClear(false) }, '取消'),
|
|
570
|
+
)
|
|
571
|
+
: e(Btn, { size: 'sm', variant: 'ghost', icon: 'trash', disabled: memory.length === 0, onClick: () => setConfirmClear(true) }, '清空长期记忆'),
|
|
572
|
+
),
|
|
573
|
+
),
|
|
574
|
+
e(
|
|
575
|
+
'div',
|
|
576
|
+
{ className: 'al-insp-section' },
|
|
577
|
+
e('div', { className: 'al-insp-label' }, '记忆的写入路径'),
|
|
578
|
+
e('div', { className: 'al-insp-note' }, '模型决定调用 memory_write → 工具把 key/value 写进长期记忆 → 下一轮按输入关键词召回并注入 system 消息。所有过程都能在「流转」里看到。'),
|
|
579
|
+
),
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function Inspector(props) {
|
|
584
|
+
const view = props.view;
|
|
585
|
+
const tab = props.tab;
|
|
586
|
+
const memoryCount = (view.memory || []).length;
|
|
587
|
+
const context = view.context || { used: 0 };
|
|
588
|
+
const stages = view.turn ? (view.turn.stages || []).length : 0;
|
|
589
|
+
return e(
|
|
590
|
+
'section',
|
|
591
|
+
{ className: 'al-panel al-inspector-panel', 'aria-label': '检查器' },
|
|
592
|
+
e(
|
|
593
|
+
'header',
|
|
594
|
+
{ className: 'al-panel-head' },
|
|
595
|
+
e('div', { className: 'al-panel-title' }, e(Icon, { name: 'search', size: 14 }), '检查器', e('span', { className: 'al-panel-sub' }, '实时')),
|
|
596
|
+
e(Chip, { tone: view.busy ? 'brand' : 'neutral', icon: view.busy ? 'bolt' : 'check' }, view.busy ? '运行中' : '空闲'),
|
|
597
|
+
),
|
|
598
|
+
e(
|
|
599
|
+
'div',
|
|
600
|
+
{ className: 'al-inspector-tabs', role: 'tablist', 'aria-label': '检查器视图' },
|
|
601
|
+
[
|
|
602
|
+
{ id: 'flow', icon: 'route', label: '流转', badge: stages || null },
|
|
603
|
+
{ id: 'context', icon: 'layers', label: '上下文', badge: context.used + '' },
|
|
604
|
+
{ id: 'memory', icon: 'memory', label: '记忆', badge: memoryCount + '' },
|
|
605
|
+
].map((item) =>
|
|
606
|
+
e(
|
|
607
|
+
'button',
|
|
608
|
+
{ key: item.id, type: 'button', role: 'tab', 'aria-selected': tab === item.id, className: 'al-itab' + (tab === item.id ? ' on' : ''), onClick: () => props.onTab(item.id) },
|
|
609
|
+
e(Icon, { name: item.icon, size: 12 }),
|
|
610
|
+
item.label,
|
|
611
|
+
item.badge ? e('span', { className: 'al-itab-badge' }, item.badge) : null,
|
|
612
|
+
),
|
|
613
|
+
),
|
|
614
|
+
),
|
|
615
|
+
e(
|
|
616
|
+
'div',
|
|
617
|
+
{ className: 'al-insp-body' },
|
|
618
|
+
tab === 'flow' ? e(FlowTab, { view }) : tab === 'context' ? e(ContextTab, { view, onBudget: props.onBudget }) : e(MemoryTab, { view, onDeleteMemory: props.onDeleteMemory, onClearMemory: props.onClearMemory }),
|
|
619
|
+
),
|
|
620
|
+
);
|
|
621
|
+
}
|