@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,341 @@
|
|
|
1
|
+
/* ─────────────────────────────────────────────────────────────
|
|
2
|
+
* Agent 实验室 · 数据流转视图
|
|
3
|
+
* 把当前轮次的 turn.stages 渲染成一条可点击、可回放的流水线:
|
|
4
|
+
* 每一步的输入、上下文、模型响应、工具结果、记忆写入与输出都能单独查看。
|
|
5
|
+
* 数据全部来自 snapshot().turn.stages[].detail —— 与检查器同一份事实来源。
|
|
6
|
+
* ───────────────────────────────────────────────────────────── */
|
|
7
|
+
|
|
8
|
+
const DF_BUDGETS = [1000, 2000, 8000, 32000, 128000, 1048576];
|
|
9
|
+
|
|
10
|
+
const DF_STAGE_META = {
|
|
11
|
+
input: { icon: 'terminal', label: '输入', tone: 'neutral' },
|
|
12
|
+
context: { icon: 'layers', label: '上下文', tone: 'brand' },
|
|
13
|
+
compress: { icon: 'compress', label: '压缩', tone: 'warn' },
|
|
14
|
+
model: { icon: 'cpu', label: '模型', tone: 'violet' },
|
|
15
|
+
tool: { icon: 'tools', label: '工具', tone: 'warn' },
|
|
16
|
+
memory: { icon: 'memory', label: '记忆', tone: 'good' },
|
|
17
|
+
output: { icon: 'send', label: '输出', tone: 'good' },
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const DF_EXPLAIN = {
|
|
21
|
+
input: '用户输入被记录为一条 user 消息,并创建本轮 turn。它后面的所有步骤都以这条消息为起点。',
|
|
22
|
+
context: '引擎把 system prompt、历史摘要、长期记忆、最近消息和本轮输入拼成一次模型请求。右侧「结构化数据」里的 segments 就是这次请求的 token 构成。',
|
|
23
|
+
compress: '上下文接近预算时,旧消息被折叠进历史摘要。before / after 是压缩前后的真实需求 token;removed 是被折叠的消息。',
|
|
24
|
+
model: '模型只做一次决策:返回一句最终回答,或者发起工具调用。真实模式下这里还能看到 provider、model、耗时与 token 用量。',
|
|
25
|
+
tool: '工具由插件执行,不是模型执行。模型只给出 name + args;结果作为 tool 消息写回上下文,供下一步模型读取。',
|
|
26
|
+
memory: 'memory_write 成功后,插件把这条事实写入跨会话长期记忆;下一轮组装上下文时可能被关键词召回。',
|
|
27
|
+
output: '最终回答写回会话并流式显示;同时重新组装一次上下文,让检查器看到本轮结束后的真实占用。',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const DF_CSS = `
|
|
31
|
+
.df-page {
|
|
32
|
+
--df-line: var(--al-border);
|
|
33
|
+
max-width: 1180px; margin: 0 auto; padding: 4px 4px 66px;
|
|
34
|
+
color: var(--al-text); font-size: 13px;
|
|
35
|
+
}
|
|
36
|
+
.df-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 18px; flex-wrap: wrap; padding: 14px 2px 18px; border-bottom: 1px solid var(--df-line); }
|
|
37
|
+
.df-eyebrow { font-family: var(--al-mono); font-size: 10px; letter-spacing: .16em; color: var(--al-ter); font-weight: 600; text-transform: uppercase; }
|
|
38
|
+
.df-title { margin: 7px 0 6px; font-size: 26px; line-height: 1.25; letter-spacing: -.02em; font-weight: 700; }
|
|
39
|
+
.df-sub { margin: 0; color: var(--al-sub); font-size: 12.5px; line-height: 1.7; max-width: 72ch; }
|
|
40
|
+
.df-head-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
|
41
|
+
.df-budget { display: inline-flex; align-items: center; gap: 6px; height: 32px; padding: 0 8px; border: 1px solid var(--al-border); border-radius: 9px; background: var(--al-layer); font-size: 11px; color: var(--al-sub); }
|
|
42
|
+
.df-budget select { height: 24px; border: 1px solid var(--al-border2); border-radius: 6px; background: var(--al-layer2); color: var(--al-text); font-size: 11px; padding: 0 4px; }
|
|
43
|
+
.df-controls { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; padding: 14px 2px 4px; }
|
|
44
|
+
.df-replay { display: flex; align-items: center; gap: 5px; }
|
|
45
|
+
.df-replay .al-btn { height: 30px; }
|
|
46
|
+
.df-stepinfo { font-size: 11px; color: var(--al-ter); font-variant-numeric: tabular-nums; }
|
|
47
|
+
.df-flow {
|
|
48
|
+
display: flex; align-items: stretch; gap: 0; overflow-x: auto; padding: 12px 2px 14px;
|
|
49
|
+
scrollbar-width: thin;
|
|
50
|
+
}
|
|
51
|
+
.df-node {
|
|
52
|
+
flex: none; width: 118px; display: flex; flex-direction: column; gap: 5px; align-items: flex-start;
|
|
53
|
+
padding: 9px 10px; border: 1px solid var(--al-border); border-radius: 10px;
|
|
54
|
+
background: color-mix(in srgb, var(--al-layer) 86%, transparent);
|
|
55
|
+
color: var(--al-text); text-align: left; position: relative;
|
|
56
|
+
transition: border-color .15s ease, background .15s ease, transform .15s ease, box-shadow .15s ease;
|
|
57
|
+
}
|
|
58
|
+
.df-node:hover { border-color: var(--al-border2); transform: translateY(-1px); }
|
|
59
|
+
.df-node.on { border-color: var(--al-accent); background: var(--al-accent-soft); box-shadow: 0 0 0 1px var(--al-accent) inset; }
|
|
60
|
+
.df-node .df-node-top { display: flex; align-items: center; gap: 5px; width: 100%; }
|
|
61
|
+
.df-node .df-node-kind { font-size: 9px; font-weight: 650; letter-spacing: .04em; color: var(--al-ter); }
|
|
62
|
+
.df-node.on .df-node-kind { color: var(--al-accent); }
|
|
63
|
+
.df-node .df-node-title { font-size: 11.5px; font-weight: 600; line-height: 1.35; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; width: 100%; }
|
|
64
|
+
.df-node .df-node-meta { font-size: 9.5px; color: var(--al-ter); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; width: 100%; font-variant-numeric: tabular-nums; }
|
|
65
|
+
.df-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--al-ter); flex: none; }
|
|
66
|
+
.df-dot.done { background: var(--al-good); }
|
|
67
|
+
.df-dot.active { background: var(--al-accent); animation: al-stage-pulse 1.6s infinite; }
|
|
68
|
+
.df-dot.error { background: var(--al-danger); }
|
|
69
|
+
.df-arrow { align-self: center; flex: none; padding: 0 4px; color: var(--al-ter); font-size: 12px; user-select: none; }
|
|
70
|
+
.df-body { display: grid; grid-template-columns: minmax(0, 1.45fr) minmax(260px, .85fr); gap: 13px; align-items: start; }
|
|
71
|
+
.df-card { border: 1px solid var(--al-border); border-radius: 13px; background: color-mix(in srgb, var(--al-layer) 88%, transparent); overflow: hidden; }
|
|
72
|
+
.df-card-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 9px 12px; border-bottom: 1px solid var(--al-border); }
|
|
73
|
+
.df-card-title { display: flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 640; }
|
|
74
|
+
.df-card-title svg { color: var(--al-accent); }
|
|
75
|
+
.df-card-meta { font-size: 10px; color: var(--al-ter); font-variant-numeric: tabular-nums; }
|
|
76
|
+
.df-card-body { padding: 11px 12px 13px; }
|
|
77
|
+
.df-explain { margin: 0 0 10px; padding: 8px 10px; border-left: 3px solid var(--al-accent); border-radius: 0 7px 7px 0; background: var(--al-accent-soft); color: var(--al-sub); font-size: 11.5px; line-height: 1.65; }
|
|
78
|
+
.df-row { display: flex; align-items: flex-start; gap: 8px; padding: 4px 0; border-bottom: 1px dashed color-mix(in srgb, var(--al-border) 70%, transparent); }
|
|
79
|
+
.df-row:last-child { border-bottom: 0; }
|
|
80
|
+
.df-k { flex: none; width: 56px; padding-top: 8px; font-size: 10px; color: var(--al-ter); }
|
|
81
|
+
.df-v { flex: 1; min-width: 0; font-size: 11.5px; color: var(--al-text); word-break: break-word; }
|
|
82
|
+
.df-text { display: block; margin: 0; padding: 8px 9px; border-radius: 8px; background: var(--al-layer2); border: 1px solid var(--al-border); font-family: var(--al-mono); font-size: 11px; line-height: 1.65; white-space: pre-wrap; word-break: break-word; max-height: 240px; overflow: auto; }
|
|
83
|
+
.df-msgs { display: flex; flex-direction: column; gap: 5px; }
|
|
84
|
+
.df-msg { display: flex; gap: 6px; align-items: flex-start; padding: 4px 6px; border-radius: 6px; background: color-mix(in srgb, var(--al-layer2) 55%, transparent); }
|
|
85
|
+
.df-msg .al-role { flex: none; margin-top: 1px; }
|
|
86
|
+
.df-msg span:last-child { font-size: 10.5px; line-height: 1.55; color: var(--al-sub); word-break: break-word; }
|
|
87
|
+
.df-hits { display: flex; flex-direction: column; gap: 4px; }
|
|
88
|
+
.df-hit { font-size: 10.5px; padding: 5px 7px; border-radius: 6px; background: color-mix(in srgb, #14b8a6 8%, transparent); border: 1px solid color-mix(in srgb, #14b8a6 22%, transparent); }
|
|
89
|
+
.df-hit b { color: #0f766e; }
|
|
90
|
+
.df-note { margin-top: 10px; font-size: 10.5px; color: var(--al-ter); line-height: 1.6; }
|
|
91
|
+
.df-empty { padding: 50px 0; }
|
|
92
|
+
.df-raw .al-json { max-height: 420px; margin: 0; }
|
|
93
|
+
@media (max-width: 900px) {
|
|
94
|
+
.df-body { grid-template-columns: minmax(0, 1fr); }
|
|
95
|
+
.df-title { font-size: 22px; }
|
|
96
|
+
.df-node { width: 106px; }
|
|
97
|
+
}
|
|
98
|
+
`;
|
|
99
|
+
|
|
100
|
+
function dfToneClass(tone) {
|
|
101
|
+
return tone === 'brand' ? 'al-chip-brand'
|
|
102
|
+
: tone === 'warn' ? 'al-chip-warn'
|
|
103
|
+
: tone === 'good' ? 'al-chip-good'
|
|
104
|
+
: tone === 'violet' ? 'al-chip-violet' : '';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function dfRoleLabel(role) {
|
|
108
|
+
if (role === 'user') return 'user';
|
|
109
|
+
if (role === 'assistant') return 'assistant';
|
|
110
|
+
if (role === 'tool') return 'tool';
|
|
111
|
+
return role || 'system';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function dfTokenText(tokens) {
|
|
115
|
+
return tokens === null || tokens === undefined ? '—' : alFormatTokens(tokens) + ' tok';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function DFStageDetail(props) {
|
|
119
|
+
const stage = props.stage;
|
|
120
|
+
const detail = stage.detail || {};
|
|
121
|
+
const view = props.view;
|
|
122
|
+
if (stage.kind === 'input') {
|
|
123
|
+
return e('div', null,
|
|
124
|
+
e(DFRow, { label: '输入' }, e('span', { className: 'df-text' }, detail.text || view.turn.input)),
|
|
125
|
+
e(DFRow, { label: '规模' }, (detail.chars || 0) + ' 字 · ' + dfTokenText(detail.tokens)),
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
if (stage.kind === 'context') {
|
|
129
|
+
return e('div', null,
|
|
130
|
+
e(TokenBar, { segments: detail.segments || [], limit: detail.limit || view.context.limit, height: 8 }),
|
|
131
|
+
e(DFRow, { label: '消息' }, String(detail.messageCount || 0) + ' 条 · ' + dfTokenText(detail.used)),
|
|
132
|
+
detail.summary ? e(DFRow, { label: '摘要' }, e('span', { className: 'df-text', style: { maxHeight: 130 } }, detail.summary)) : null,
|
|
133
|
+
(detail.hits || []).length
|
|
134
|
+
? e(DFRow, { label: '召回' }, e('div', { className: 'df-hits' }, detail.hits.map((hit) => e('div', { key: hit.id, className: 'df-hit' }, e('b', null, hit.key), ':' + hit.value))))
|
|
135
|
+
: e(DFRow, { label: '召回' }, '本轮没有召回长期记忆'),
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
if (stage.kind === 'compress') {
|
|
139
|
+
return e('div', null,
|
|
140
|
+
e(DFRow, { label: '前后' }, (detail.before || 0) + ' → ' + (detail.after || 0) + ' tok'),
|
|
141
|
+
e(DFRow, { label: '折叠' }, String(detail.removed || 0) + ' 条消息 · 保留最近 ' + String(detail.keep || 0) + ' 条'),
|
|
142
|
+
e(DFRow, { label: '触发' }, detail.reason === 'manual' ? '手动 /compact' : '自动(超过预算 90%)'),
|
|
143
|
+
detail.summary ? e(DFRow, { label: '摘要' }, e('span', { className: 'df-text', style: { maxHeight: 150 } }, detail.summary)) : null,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
if (stage.kind === 'model') {
|
|
147
|
+
const response = detail.response || {};
|
|
148
|
+
const real = detail.model;
|
|
149
|
+
return e('div', null,
|
|
150
|
+
e(DFRow, { label: '步骤' }, '第 ' + String(detail.step || 1) + ' 步'),
|
|
151
|
+
real
|
|
152
|
+
? e(DFRow, { label: '真实模型' }, (real.provider || '') + ' / ' + (real.model || '') + (real.latencyMs != null ? ' · ' + real.latencyMs + ' ms' : ''))
|
|
153
|
+
: e(DFRow, { label: '模型' }, '本地规则模拟模型'),
|
|
154
|
+
real && real.usage
|
|
155
|
+
? e(DFRow, { label: '用量' }, '输入 ' + (real.usage.inputTokens || 0) + ' / 输出 ' + (real.usage.outputTokens || 0) + ' tok')
|
|
156
|
+
: null,
|
|
157
|
+
e(DFRow, { label: '响应' }, response.type === 'tool_call'
|
|
158
|
+
? 'tool_call · ' + response.name + ' ' + JSON.stringify(response.args || {})
|
|
159
|
+
: ('final · ' + (response.text || ''))),
|
|
160
|
+
(detail.messages || []).length
|
|
161
|
+
? e(DFRow, { label: '请求消息' }, e('div', { className: 'df-msgs' }, detail.messages.map((message, index) => e('div', { key: index, className: 'df-msg' },
|
|
162
|
+
e('span', { className: 'al-role al-role-' + dfRoleLabel(message.role) }, dfRoleLabel(message.role)),
|
|
163
|
+
e('span', null, (message.content || '').slice(0, 160)),
|
|
164
|
+
))))
|
|
165
|
+
: null,
|
|
166
|
+
e(DFRow, { label: '工具' }, String((detail.toolSchemas || []).length) + ' 个 schema 随请求发送'),
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
if (stage.kind === 'tool') {
|
|
170
|
+
return e('div', null,
|
|
171
|
+
e(DFRow, { label: '工具' }, detail.name || stage.title),
|
|
172
|
+
e(DFRow, { label: '参数' }, e(JsonView, { value: detail.args || {}, style: { maxHeight: 130 } })),
|
|
173
|
+
e(DFRow, { label: '结果' }, e(JsonView, { value: detail.result, style: { maxHeight: 160 } })),
|
|
174
|
+
e(DFRow, { label: '状态' }, detail.ok ? '成功 · ' + (detail.durationMs != null ? detail.durationMs + ' ms' : '') : '失败'),
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
if (stage.kind === 'memory') {
|
|
178
|
+
return e('div', null,
|
|
179
|
+
e(DFRow, { label: '写入' }, (detail.key || '') + ' = ' + (detail.value || '')),
|
|
180
|
+
e(DFRow, { label: '记忆 ID' }, detail.id || '—'),
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
if (stage.kind === 'output') {
|
|
184
|
+
return e('div', null,
|
|
185
|
+
e(DFRow, { label: '回答' }, e('span', { className: 'df-text', style: { maxHeight: 220 } }, detail.text || '')),
|
|
186
|
+
e(DFRow, { label: '本轮' }, String(detail.steps || 0) + ' 步 · ' + String(detail.toolCalls || 0) + ' 次工具 · ' + dfTokenText(detail.tokens)),
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
return e(JsonView, { value: detail });
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function DFRow(props) {
|
|
193
|
+
return e('div', { className: 'df-row' }, e('span', { className: 'df-k' }, props.label), e('div', { className: 'df-v' }, props.children));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function DataFlowPage(props) {
|
|
197
|
+
const view = props.view;
|
|
198
|
+
const turn = view.turn;
|
|
199
|
+
const stages = turn ? turn.stages || [] : [];
|
|
200
|
+
const [selected, setSelected] = useState(0);
|
|
201
|
+
const [playing, setPlaying] = useState(false);
|
|
202
|
+
const timer = useRef(null);
|
|
203
|
+
|
|
204
|
+
const lastActive = useMemo(() => {
|
|
205
|
+
for (let i = stages.length - 1; i >= 0; i -= 1) if (stages[i].status === 'active') return i;
|
|
206
|
+
return stages.length ? stages.length - 1 : 0;
|
|
207
|
+
}, [stages]);
|
|
208
|
+
|
|
209
|
+
useEffect(() => {
|
|
210
|
+
setSelected(stages.length ? lastActive : 0);
|
|
211
|
+
setPlaying(false);
|
|
212
|
+
}, [turn && turn.id]);
|
|
213
|
+
|
|
214
|
+
useEffect(() => {
|
|
215
|
+
if (turn && turn.status === 'running' && stages.length) setSelected(stages.length - 1);
|
|
216
|
+
}, [stages.length, turn && turn.status]);
|
|
217
|
+
|
|
218
|
+
useEffect(() => {
|
|
219
|
+
if (!playing) {
|
|
220
|
+
if (timer.current) { clearInterval(timer.current); timer.current = null; }
|
|
221
|
+
return undefined;
|
|
222
|
+
}
|
|
223
|
+
timer.current = setInterval(() => {
|
|
224
|
+
setSelected((prev) => {
|
|
225
|
+
if (prev >= stages.length - 1) { setPlaying(false); return prev; }
|
|
226
|
+
return prev + 1;
|
|
227
|
+
});
|
|
228
|
+
}, 900);
|
|
229
|
+
return () => { if (timer.current) { clearInterval(timer.current); timer.current = null; } };
|
|
230
|
+
}, [playing, stages.length]);
|
|
231
|
+
|
|
232
|
+
useEffect(() => () => { if (timer.current) clearInterval(timer.current); }, []);
|
|
233
|
+
|
|
234
|
+
const current = stages[selected] || null;
|
|
235
|
+
const meta = current ? DF_STAGE_META[current.kind] || DF_STAGE_META.input : DF_STAGE_META.input;
|
|
236
|
+
const options = (DF_BUDGETS.indexOf(view.context.limit) >= 0 ? DF_BUDGETS : DF_BUDGETS.concat([view.context.limit])).filter((n) => Number.isFinite(n) && n > 0);
|
|
237
|
+
|
|
238
|
+
return e(
|
|
239
|
+
'div',
|
|
240
|
+
{ className: 'df-page' },
|
|
241
|
+
e('style', { dangerouslySetInnerHTML: { __html: DF_CSS } }),
|
|
242
|
+
e(
|
|
243
|
+
'header',
|
|
244
|
+
{ className: 'df-head' },
|
|
245
|
+
e(
|
|
246
|
+
'div',
|
|
247
|
+
null,
|
|
248
|
+
e('div', { className: 'df-eyebrow' }, 'AGENT DATAFLOW'),
|
|
249
|
+
e('h1', { className: 'df-title' }, 'Agent 数据流转'),
|
|
250
|
+
e('p', { className: 'df-sub' }, '还原当前轮次的每一个小步骤:输入、上下文拼装、压缩、模型请求与响应、工具执行、记忆写入、输出。点节点可跳转,也可以逐步回放。'),
|
|
251
|
+
),
|
|
252
|
+
e(
|
|
253
|
+
'div',
|
|
254
|
+
{ className: 'df-head-actions' },
|
|
255
|
+
e(
|
|
256
|
+
'label',
|
|
257
|
+
{ className: 'df-budget' },
|
|
258
|
+
'教学预算',
|
|
259
|
+
e(
|
|
260
|
+
'select',
|
|
261
|
+
{ value: view.context.limit, onChange: (event) => props.onBudget && props.onBudget(Number(event.target.value)) },
|
|
262
|
+
options.map((n) => e('option', { key: n, value: n }, alFormatTokens(n) + ' tok')),
|
|
263
|
+
),
|
|
264
|
+
),
|
|
265
|
+
e(Chip, { tone: turn && turn.status === 'running' ? 'brand' : turn && turn.status === 'error' ? 'danger' : 'good', icon: turn && turn.status === 'running' ? 'bolt' : 'check' }, turn ? (turn.status === 'running' ? '运行中' : turn.status === 'error' ? '出错' : '完成') : '等待输入'),
|
|
266
|
+
e(Chip, { icon: 'layers' }, view.modelMode === 'real' ? '真实模型' : '模拟模型'),
|
|
267
|
+
),
|
|
268
|
+
),
|
|
269
|
+
!turn
|
|
270
|
+
? e('div', { className: 'df-empty' }, e(Empty, { icon: 'route', title: '还没有可回放的数据', body: '先到「体验 TUI」发一条消息;这里会把本轮从输入到输出的每一步展开,可逐节点查看数据。' }))
|
|
271
|
+
: e(
|
|
272
|
+
'div',
|
|
273
|
+
null,
|
|
274
|
+
e(
|
|
275
|
+
'div',
|
|
276
|
+
{ className: 'df-controls' },
|
|
277
|
+
e(
|
|
278
|
+
'div',
|
|
279
|
+
{ className: 'df-replay' },
|
|
280
|
+
e(Btn, { size: 'sm', variant: 'ghost', icon: 'chevronLeft', disabled: selected <= 0, onClick: () => { setPlaying(false); setSelected(Math.max(0, selected - 1)); } }, '上一步'),
|
|
281
|
+
e(Btn, { size: 'sm', variant: playing ? 'soft' : 'primary', icon: playing ? 'pause' : 'play', onClick: () => { if (selected >= stages.length - 1) setSelected(0); setPlaying(!playing); } }, playing ? '暂停' : '播放'),
|
|
282
|
+
e(Btn, { size: 'sm', variant: 'ghost', icon: 'chevronRight', disabled: selected >= stages.length - 1, onClick: () => { setPlaying(false); setSelected(Math.min(stages.length - 1, selected + 1)); } }, '下一步'),
|
|
283
|
+
),
|
|
284
|
+
e('div', { className: 'df-stepinfo' }, '第 ' + String(selected + 1) + ' / ' + String(stages.length) + ' 步 · ' + (current ? current.title : '') + (turn.status === 'running' ? ' · 实时跟随中' : '')),
|
|
285
|
+
),
|
|
286
|
+
e(
|
|
287
|
+
'div',
|
|
288
|
+
{ className: 'df-flow' },
|
|
289
|
+
stages.map((stage, index) =>
|
|
290
|
+
e(
|
|
291
|
+
'div',
|
|
292
|
+
{ key: stage.id, style: { display: 'flex', alignItems: 'stretch' } },
|
|
293
|
+
e(
|
|
294
|
+
'button',
|
|
295
|
+
{
|
|
296
|
+
type: 'button',
|
|
297
|
+
className: 'df-node' + (index === selected ? ' on' : ''),
|
|
298
|
+
onClick: () => { setPlaying(false); setSelected(index); },
|
|
299
|
+
title: stage.title,
|
|
300
|
+
},
|
|
301
|
+
e('div', { className: 'df-node-top' },
|
|
302
|
+
e('span', { className: 'df-dot ' + (stage.status || '') }),
|
|
303
|
+
e('span', { className: 'df-node-kind' }, (DF_STAGE_META[stage.kind] || DF_STAGE_META.input).label),
|
|
304
|
+
),
|
|
305
|
+
e('span', { className: 'df-node-title' }, stage.title),
|
|
306
|
+
e('span', { className: 'df-node-meta' }, stage.meta || ''),
|
|
307
|
+
),
|
|
308
|
+
index < stages.length - 1 ? e('span', { className: 'df-arrow' }, '→') : null,
|
|
309
|
+
),
|
|
310
|
+
),
|
|
311
|
+
),
|
|
312
|
+
e(
|
|
313
|
+
'div',
|
|
314
|
+
{ className: 'df-body' },
|
|
315
|
+
e(
|
|
316
|
+
'section',
|
|
317
|
+
{ className: 'df-card' },
|
|
318
|
+
e(
|
|
319
|
+
'div',
|
|
320
|
+
{ className: 'df-card-head' },
|
|
321
|
+
e('div', { className: 'df-card-title' }, e(Icon, { name: meta.icon, size: 14 }), '第 ' + String(selected + 1) + ' 步 · ' + (current ? current.title : '')),
|
|
322
|
+
e('span', { className: 'df-card-meta' }, (current && current.meta) || ''),
|
|
323
|
+
),
|
|
324
|
+
e(
|
|
325
|
+
'div',
|
|
326
|
+
{ className: 'df-card-body' },
|
|
327
|
+
e('p', { className: 'df-explain' }, DF_EXPLAIN[current ? current.kind : 'input'] || ''),
|
|
328
|
+
current ? e(DFStageDetail, { stage: current, view }) : null,
|
|
329
|
+
),
|
|
330
|
+
),
|
|
331
|
+
e(
|
|
332
|
+
'aside',
|
|
333
|
+
{ className: 'df-card df-raw' },
|
|
334
|
+
e('div', { className: 'df-card-head' }, e('div', { className: 'df-card-title' }, e(Icon, { name: 'code', size: 13 }), '结构化数据'), e('span', { className: 'df-card-meta' }, 'stage.detail')),
|
|
335
|
+
e('div', { className: 'df-card-body' }, e(JsonView, { value: current ? current.detail : null, style: { maxHeight: 420 } })),
|
|
336
|
+
),
|
|
337
|
+
),
|
|
338
|
+
e('p', { className: 'df-note' }, '数据来源:lab.snapshot().turn.stages[].detail;页面只渲染快照,不额外编造状态。运行中会自动跟随最新步骤;点击任意节点或「上一步 / 下一步」可脱离实时回看。'),
|
|
339
|
+
),
|
|
340
|
+
);
|
|
341
|
+
}
|