dsh-topic-graph 0.0.0-stage → 0.6.41

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/lib/client.js ADDED
@@ -0,0 +1,3502 @@
1
+ /**
2
+ * dsh-topic-graph — 会话话题图谱(Client 半)
3
+ *
4
+ * dsh 模块加载器的私有 bundle 格式:window.__ModuleLoader__.load({ id, factory })。
5
+ * factory 是浏览器 CJS 形状,必须自行声明 module/exports;react 由 shell 提供。
6
+ *
7
+ * 注册进 conversation.view(session 级 list 槽位,「对话 / 轨迹」之后的新视图),
8
+ * 渲染一个可拖动 / 可缩放 / 可收起的悬浮窗,含两个 Tab:
9
+ * 1. 链式:阶段 → 话题(含子话题)→ 轮次明细(token、工具、Agent 耗时与占比);
10
+ * 2. 图谱:阶段 / 话题 / 子话题的树状图,Ctrl+滚轮缩放、滚轮上下滚动、
11
+ * 鼠标悬停显示完整信息。
12
+ *
13
+ * 数据全部来自 Host 半的 /plugins/topic-graph/* 路由;样式只使用
14
+ * --dsw-alias-* 主题 token(带兜底值),跟随明暗主题。
15
+ */
16
+ window.__ModuleLoader__.load({
17
+ id: 'dsh-topic-graph',
18
+ factory: (require) => {
19
+ var module = { exports: {} };
20
+ var exports = module.exports;
21
+
22
+ var React = require('react');
23
+ var useState = React.useState;
24
+ var useEffect = React.useEffect;
25
+ var useRef = React.useRef;
26
+ var useMemo = React.useMemo;
27
+ var useCallback = React.useCallback;
28
+ var h = React.createElement;
29
+
30
+ var ROUTE = '/plugins/topic-graph';
31
+ var CLIENT_VERSION = '0.6.41';
32
+ var LOC_NS = 'dsh-topic-graph';
33
+ var UI_KEY = 'dsh-topic-graph.ui.v2';
34
+ var UI_KEY_V1 = 'dsh-topic-graph.ui.v1';
35
+ var DEFAULT_PANEL_W = 900;
36
+ var DEFAULT_PANEL_H = 680;
37
+
38
+ // ── 文案(同时注册进 dsh 的 locale 服务) ────────────────────────────────
39
+ var DICT = {
40
+ zh: {
41
+ title: '话题图谱',
42
+ panelTitle: '话题',
43
+ tabChain: '话题·工作流',
44
+ tabGraph: '话题·图谱',
45
+ followLatest: '跟随左侧当前会话',
46
+ pickSession: '选择会话',
47
+ noSession: '没有可用的会话',
48
+ loading: '加载中…',
49
+ noTurns: '本会话还没有轮次',
50
+ noTopics: '还没有切分话题',
51
+ segmenting: 'AI 正在切分…',
52
+ waitSegment: '等待 AI 切分',
53
+ pendingTurns: '待切分 {n} 轮',
54
+ aiFailed: 'AI 切分失败',
55
+ retry: '重试',
56
+ segNow: '增量切分',
57
+ resegment: '全部重切',
58
+ collapsed: '已收起',
59
+ expand: '展开',
60
+ collapse: '收起',
61
+ refresh: '刷新',
62
+ zoomIn: '放大',
63
+ zoomOut: '缩小',
64
+ zoomReset: '重置缩放',
65
+ graphHint: 'Ctrl+滚轮缩放 · 滚轮上下滑动 · 悬停看详情',
66
+ modelLabel: '模型',
67
+ modelDefault: '默认(配置)',
68
+ workflowEmpty: '点击左侧话题或阶段查看详情',
69
+ turns: '轮次',
70
+ topics: '话题',
71
+ tokens: 'tokens',
72
+ toolCalls: '工具调用',
73
+ agentTime: 'Agent 耗时',
74
+ stepTime: '步骤活动时间',
75
+ requestCount: '模型调用',
76
+ steps: '步骤',
77
+ statusActive: '进行中',
78
+ statusDone: '已完成',
79
+ statusAborted: '被中断',
80
+ statusError: '出错',
81
+ statusOpen: '运行中',
82
+ sourceLlm: 'AI',
83
+ sourceRule: '规则',
84
+ stage: '阶段',
85
+ topic: '话题',
86
+ subtopics: '子话题',
87
+ tokenBreakdown: 'Token 明细',
88
+ input: '输入',
89
+ output: '输出',
90
+ cacheRead: '缓存读',
91
+ cacheWrite: '缓存写',
92
+ toolErrors: '工具错误',
93
+ prompt: '用户输入',
94
+ assistant: 'AI 回复',
95
+ emptyPrompt: '(无文本输入)',
96
+ toolDistribution: '工具分布',
97
+ range: '轮次',
98
+ ratioOfTotal: '占会话',
99
+ sessionStats: '{turns} 轮 · {topics} 话题 · {tokens} tokens',
100
+ updatedAt: '更新于 {time}',
101
+ aiMode: '切分模式:{mode}',
102
+ noTopicsYet: 'AI 正在为这个会话生成话题结构…',
103
+ ruleFallback: '当前为规则切分(未使用 AI)',
104
+ turnLabel: '轮 {n}',
105
+ openTurn: '进行中',
106
+ sessionPickerSearch: '筛选会话…',
107
+ unnamed: '未命名会话',
108
+ dragHint: '可拖动标题栏移动,右下角拖拽缩放',
109
+ subagent: '子智能体',
110
+ subagents: '子智能体',
111
+ subagentCount: '子智能体 {n}',
112
+ branchTurns: '{n} 轮',
113
+ branchOpen: '查看该子智能体会话',
114
+ backToParent: '返回父会话',
115
+ relations: '关联话题',
116
+ relationFrom: '→ {title} · {label}',
117
+ relationTo: '← {title} · {label}',
118
+ edgeNext: '顺序',
119
+ edgeBelong: '归属',
120
+ edgeRelation: '关联',
121
+ edgeDelegate: '委托',
122
+ parentTopic: '挂靠话题',
123
+ childTopics: '子话题',
124
+ branchTopics: '话题',
125
+ branchPending: '切分中…',
126
+ subagentTotals: '子智能体合计',
127
+ graphLegendNext: 'next 顺序',
128
+ graphLegendBelong: 'belongTo 归属',
129
+ graphLegendRelation: 'has_relation 关联',
130
+ graphModeTimeline: '时间线',
131
+ graphModeTree: '结构图',
132
+ graphLegendDelegate: 'delegates 委托',
133
+ statsBar: '统计',
134
+ statTurns: '轮次',
135
+ statTopics: '话题',
136
+ statStage: '阶段',
137
+ statTokens: 'Token',
138
+ statTools: '工具',
139
+ totalSteps: '总步数',
140
+ jumpToTurn: '跳转到对话',
141
+ expandAll: '全部展开',
142
+ collapseAll: '全部收缩',
143
+ viewWorkflow: '工作流',
144
+ viewGraph: '图谱',
145
+ noSessionTitle: '未选择会话',
146
+ statStepTime: '步骤耗时',
147
+ statWallTime: '总耗时',
148
+ statRelations: '关联',
149
+ stageTopicRelations: '阶段内话题关系',
150
+ statSubagentTokens: '子智能体 Token',
151
+ detailTitle: '话题详情',
152
+ detailOpen: '查看详情',
153
+ openAtFirstTurn: '在对话中打开首轮',
154
+ locating: '正在定位…',
155
+ located: '已在对话中定位到该轮',
156
+ openedOnly: '已打开会话(历史未完全加载,未能自动定位)',
157
+ detailStage: '所属阶段',
158
+ detailParent: '父话题',
159
+ detailChildren: '子话题',
160
+ detailBranches: '子智能体分支',
161
+ detailRelated: '关联话题',
162
+ detailTurns: '本轮话题的轮次',
163
+ close: '关闭',
164
+ jumpHint: '点击话题打开详情;详情内可跳回该话题首轮',
165
+ collapseStage: '收起阶段下的内容',
166
+ expandStage: '展开阶段下的内容',
167
+ collapseTopic: '收起子话题',
168
+ expandTopic: '展开子话题',
169
+ collapseBranch: '收起分支内的话题',
170
+ expandBranch: '展开分支内的话题',
171
+ statIncludesSubtopic: '统计含子话题',
172
+ aiCost: 'AI 切分消耗',
173
+ aiCallsUnit: '次',
174
+ analyzeTurn: 'AI 分析',
175
+ reanalyzeTurn: '重新分析',
176
+ analyzing: 'AI 分析中…',
177
+ analyzeFailed: 'AI 分析失败',
178
+ howtoTitle: '这一轮做了什么(AI 生成)',
179
+ howtoEmpty: '暂无分析,点「AI 分析」生成一份通俗的分步说明',
180
+ stepTools: '工具',
181
+ },
182
+ en: {
183
+ title: 'Topic Graph',
184
+ panelTitle: 'Topics',
185
+ tabChain: 'Topics · Workflow',
186
+ tabGraph: 'Topics · Graph',
187
+ followLatest: 'Follow sidebar session',
188
+ pickSession: 'Sessions',
189
+ noSession: 'No sessions available',
190
+ loading: 'Loading…',
191
+ noTurns: 'This session has no turns yet',
192
+ noTopics: 'No topics segmented yet',
193
+ segmenting: 'AI is segmenting…',
194
+ waitSegment: 'Waiting for AI segmentation',
195
+ pendingTurns: '{n} turns pending',
196
+ aiFailed: 'AI segmentation failed',
197
+ retry: 'Retry',
198
+ segNow: 'Incremental',
199
+ resegment: 'Re-segment all',
200
+ collapsed: 'Collapsed',
201
+ expand: 'Expand',
202
+ collapse: 'Collapse',
203
+ refresh: 'Refresh',
204
+ zoomIn: 'Zoom in',
205
+ zoomOut: 'Zoom out',
206
+ zoomReset: 'Reset zoom',
207
+ graphHint: 'Ctrl+wheel to zoom · wheel to scroll · hover for details',
208
+ turns: 'turns',
209
+ topics: 'topics',
210
+ tokens: 'tokens',
211
+ toolCalls: 'Tool calls',
212
+ agentTime: 'Agent time',
213
+ stepTime: 'Step active time',
214
+ requestCount: 'Model calls',
215
+ steps: 'Steps',
216
+ statusActive: 'Active',
217
+ statusDone: 'Done',
218
+ statusAborted: 'Aborted',
219
+ statusError: 'Error',
220
+ statusOpen: 'Running',
221
+ sourceLlm: 'AI',
222
+ sourceRule: 'Rule',
223
+ stage: 'Stage',
224
+ topic: 'Topic',
225
+ subtopics: 'subtopics',
226
+ tokenBreakdown: 'Token breakdown',
227
+ input: 'Input',
228
+ output: 'Output',
229
+ cacheRead: 'Cache read',
230
+ cacheWrite: 'Cache write',
231
+ toolErrors: 'Tool errors',
232
+ prompt: 'User',
233
+ assistant: 'AI',
234
+ emptyPrompt: '(no text input)',
235
+ toolDistribution: 'Tool distribution',
236
+ range: 'Turns',
237
+ ratioOfTotal: 'of session',
238
+ sessionStats: '{turns} turns · {topics} topics · {tokens} tokens',
239
+ updatedAt: 'updated {time}',
240
+ aiMode: 'Mode: {mode}',
241
+ noTopicsYet: 'AI is building the topic structure for this session…',
242
+ ruleFallback: 'Rule-based segmentation (AI not used)',
243
+ turnLabel: 'Turn {n}',
244
+ openTurn: 'running',
245
+ sessionPickerSearch: 'Filter sessions…',
246
+ unnamed: 'Untitled session',
247
+ dragHint: 'Drag the header to move, drag the corner to resize',
248
+ subagent: 'Subagent',
249
+ subagents: 'Subagents',
250
+ subagentCount: '{n} subagents',
251
+ branchTurns: '{n} turns',
252
+ branchOpen: 'Open this subagent session',
253
+ backToParent: 'Back to parent',
254
+ relations: 'Related topics',
255
+ relationFrom: '→ {title} · {label}',
256
+ relationTo: '← {title} · {label}',
257
+ edgeNext: 'next',
258
+ edgeBelong: 'belongTo',
259
+ edgeRelation: 'has_relation',
260
+ edgeDelegate: 'delegates',
261
+ parentTopic: 'attached topic',
262
+ childTopics: 'Subtopics',
263
+ branchTopics: 'topics',
264
+ branchPending: 'segmenting…',
265
+ subagentTotals: 'Subagent totals',
266
+ graphLegendNext: 'next',
267
+ graphLegendBelong: 'belongTo',
268
+ graphLegendRelation: 'has_relation',
269
+ graphModeTimeline: 'Timeline',
270
+ graphModeTree: 'Structure',
271
+ graphLegendDelegate: 'delegates',
272
+ statsBar: 'Stats',
273
+ statTurns: 'Turns',
274
+ statTopics: 'Topics',
275
+ statStage: 'Stages',
276
+ statTokens: 'Tokens',
277
+ statTools: 'Tools',
278
+ totalSteps: 'Steps',
279
+ jumpToTurn: 'Jump to chat',
280
+ expandAll: 'Expand all',
281
+ collapseAll: 'Collapse all',
282
+ viewWorkflow: 'Workflow',
283
+ viewGraph: 'Graph',
284
+ noSessionTitle: 'No session',
285
+ statStepTime: 'Step time',
286
+ statWallTime: 'Wall time',
287
+ statRelations: 'Relations',
288
+ stageTopicRelations: 'Topic relations in this stage',
289
+ statSubagentTokens: 'Subagent tokens',
290
+ detailTitle: 'Topic detail',
291
+ detailOpen: 'Details',
292
+ openAtFirstTurn: 'Open first turn in chat',
293
+ locating: 'Locating…',
294
+ located: 'Located in chat',
295
+ openedOnly: 'Session opened (history not fully loaded; could not locate)',
296
+ detailStage: 'Stage',
297
+ detailParent: 'Parent topic',
298
+ detailChildren: 'Subtopics',
299
+ detailBranches: 'Subagent branches',
300
+ detailRelated: 'Related topics',
301
+ detailTurns: 'Turns in this topic',
302
+ close: 'Close',
303
+ jumpHint: 'Click a topic for details; jump to its first turn from there',
304
+ collapseStage: 'Collapse topics in this stage',
305
+ expandStage: 'Expand topics in this stage',
306
+ collapseTopic: 'Collapse subtopics',
307
+ expandTopic: 'Expand subtopics',
308
+ collapseBranch: 'Collapse branch topics',
309
+ expandBranch: 'Expand branch topics',
310
+ statIncludesSubtopic: 'incl. subtopics',
311
+ aiCost: 'AI cost',
312
+ aiCallsUnit: 'calls',
313
+ analyzeTurn: 'AI analyze',
314
+ reanalyzeTurn: 'Re-analyze',
315
+ analyzing: 'Analyzing…',
316
+ analyzeFailed: 'AI analysis failed',
317
+ howtoTitle: 'What this turn did (AI)',
318
+ howtoEmpty: 'No analysis yet. Click "AI analyze" for a plain-language step-by-step.',
319
+ stepTools: 'Tools',
320
+ },
321
+ };
322
+
323
+ var localeService = null;
324
+ var translate = null;
325
+ var appCtx = null;
326
+
327
+ /**
328
+ * 左侧工作区当前选中的会话由 shell 持久化在 localStorage 的 `dsh.sessions.current`
329
+ * (快照存储的 persist 名称),值形如 { sessionId, subagentAddress? }。
330
+ * 这里只读取这一个公开持久化键,轮询即可跟随左侧切换。
331
+ */
332
+ function readSidebarCurrentSession() {
333
+ try {
334
+ var raw = localStorage.getItem('dsh.sessions.current');
335
+ if (!raw) return null;
336
+ var value = JSON.parse(raw);
337
+ var id = value && (value.sessionId || (value.state && value.state.sessionId));
338
+ return typeof id === 'string' && id ? id : null;
339
+ } catch (error) {
340
+ return null;
341
+ }
342
+ }
343
+
344
+ function summarizeText(text, max) {
345
+ var value = String(text == null ? '' : text).replace(/\s+/g, ' ').trim();
346
+ if (value.length <= max) return value;
347
+ return value.slice(0, max) + '…';
348
+ }
349
+
350
+ function sleep(ms) {
351
+ return new Promise(function (resolve) { setTimeout(resolve, ms); });
352
+ }
353
+
354
+ /** 找聊天区的滚动容器(从聊天 DOM 反查最近的可滚动祖先,只用于跳转定位)。 */
355
+ function findChatScroller() {
356
+ try {
357
+ var el = document.querySelector('[data-chat-flow]') || document.querySelector('[data-chat-turn]');
358
+ while (el && el !== document.body) {
359
+ var style = null;
360
+ try { style = window.getComputedStyle(el); } catch (error) { style = null; }
361
+ if (style && /(auto|scroll)/.test(style.overflowY) && el.scrollHeight > el.clientHeight + 40) return el;
362
+ el = el.parentElement;
363
+ }
364
+ } catch (error) { /* ignore */ }
365
+ return null;
366
+ }
367
+
368
+ /** 尽力把聊天滚到某一轮:轮次未加载时反复滚动顶部触发历史分页。 */
369
+ async function revealChatTurn(turnNumber) {
370
+ if (turnNumber == null) return false;
371
+ for (var attempt = 0; attempt < 16; attempt += 1) {
372
+ var target = null;
373
+ try { target = document.querySelector('[data-chat-turn="' + String(turnNumber) + '"]'); } catch (error) { target = null; }
374
+ if (target) {
375
+ try { target.scrollIntoView({ block: 'center', behavior: 'smooth' }); } catch (error) { try { target.scrollIntoView(); } catch (e2) { /* ignore */ } }
376
+ return true;
377
+ }
378
+ var scroller = findChatScroller();
379
+ if (scroller) {
380
+ try { scroller.scrollTop = 0; } catch (error) { /* ignore */ }
381
+ }
382
+ await sleep(attempt < 4 ? 220 : 380);
383
+ }
384
+ return false;
385
+ }
386
+
387
+ function initLocale(ctx) {
388
+ try {
389
+ var service = typeof ctx.get === 'function' ? ctx.get('locale') : ctx.locale;
390
+ if (service && typeof service.register === 'function') {
391
+ localeService = service;
392
+ service.register(LOC_NS, DICT);
393
+ if (typeof service.bind === 'function') translate = service.bind(LOC_NS);
394
+ }
395
+ } catch (error) {
396
+ localeService = null;
397
+ translate = null;
398
+ }
399
+ }
400
+
401
+ function localeId() {
402
+ try {
403
+ if (localeService && typeof localeService.getSnapshot === 'function') {
404
+ var snap = localeService.getSnapshot();
405
+ var active = snap && snap.active ? String(snap.active) : '';
406
+ if (active) return active.toLowerCase().indexOf('zh') === 0 ? 'zh' : 'en';
407
+ }
408
+ } catch (error) { /* 忽略 */ }
409
+ try {
410
+ return String(navigator.language || '').toLowerCase().indexOf('zh') === 0 ? 'zh' : 'en';
411
+ } catch (error) {
412
+ return 'zh';
413
+ }
414
+ }
415
+
416
+ /** 文案:优先走 dsh locale 服务(已注册同名 namespace),无服务时用内置字典兜底。 */
417
+ function t(key, params) {
418
+ var value = '';
419
+ if (translate) {
420
+ try { value = translate(key, params); } catch (error) { value = ''; }
421
+ }
422
+ if (!value || value === key) {
423
+ var dict = DICT[localeId()] || DICT.zh;
424
+ value = dict[key] != null ? dict[key] : (DICT.zh[key] != null ? DICT.zh[key] : key);
425
+ }
426
+ if (params && typeof value === 'string') {
427
+ value = value.replace(/\{(\w+)\}/g, function (match, name) {
428
+ return params[name] != null ? String(params[name]) : match;
429
+ });
430
+ }
431
+ return value;
432
+ }
433
+
434
+ /** 订阅 locale 变化:切换语言后重渲染。 */
435
+ function useLocaleTick() {
436
+ var subscribe = useCallback(function (onChange) {
437
+ if (!localeService || typeof localeService.subscribe !== 'function') return function () {};
438
+ try { return localeService.subscribe(onChange); } catch (error) { return function () {}; }
439
+ }, []);
440
+ var snapshot = useCallback(function () {
441
+ try {
442
+ if (localeService && typeof localeService.getSnapshot === 'function') {
443
+ var snap = localeService.getSnapshot();
444
+ return snap && snap.revision != null ? snap.revision : 0;
445
+ }
446
+ } catch (error) { /* 忽略 */ }
447
+ return 0;
448
+ }, []);
449
+ if (typeof React.useSyncExternalStore === 'function') {
450
+ try { React.useSyncExternalStore(subscribe, snapshot, snapshot); } catch (error) { /* 忽略 */ }
451
+ }
452
+ }
453
+
454
+ // ── 小工具 ──────────────────────────────────────────────────────────────
455
+ function clamp(value, min, max) {
456
+ return Math.max(min, Math.min(max, value));
457
+ }
458
+
459
+ function fmtK(value) {
460
+ var n = Number(value) || 0;
461
+ if (n >= 1e9) return (n / 1e9).toFixed(2) + 'B';
462
+ if (n >= 1e6) return (n / 1e6).toFixed(2) + 'M';
463
+ if (n >= 1e3) return (n / 1e3).toFixed(1) + 'k';
464
+ return String(Math.round(n));
465
+ }
466
+
467
+ function fmtPct(ratio) {
468
+ var n = Number(ratio);
469
+ if (!Number.isFinite(n) || n <= 0) return '0%';
470
+ var pct = n * 100;
471
+ if (pct >= 10) return pct.toFixed(0) + '%';
472
+ if (pct >= 1) return pct.toFixed(1) + '%';
473
+ return pct.toFixed(2) + '%';
474
+ }
475
+
476
+ function fmtDuration(ms) {
477
+ var n = Number(ms);
478
+ if (!Number.isFinite(n) || n <= 0) return '0s';
479
+ var sec = Math.round(n / 1000);
480
+ if (sec < 60) return sec + 's';
481
+ var min = Math.floor(sec / 60);
482
+ var rem = sec % 60;
483
+ if (min < 60) return min + 'm' + (rem ? String(rem).padStart(2, '0') + 's' : '');
484
+ var hr = Math.floor(min / 60);
485
+ return hr + 'h' + String(min % 60).padStart(2, '0') + 'm';
486
+ }
487
+
488
+ function fmtClock(ms) {
489
+ if (!ms) return '';
490
+ try {
491
+ var d = new Date(ms);
492
+ var hh = String(d.getHours()).padStart(2, '0');
493
+ var mm = String(d.getMinutes()).padStart(2, '0');
494
+ return hh + ':' + mm;
495
+ } catch (error) { return ''; }
496
+ }
497
+
498
+ function sessionShortId(id) {
499
+ return String(id || '').replace(/^session-/, '').slice(0, 8);
500
+ }
501
+
502
+ function sameSession(a, b) {
503
+ return String(a || '').replace(/^session-/, '') === String(b || '').replace(/^session-/, '');
504
+ }
505
+
506
+ /** 面板默认宽度:尽量宽(用户反馈可以加倍),小屏时自动收进视口。 */
507
+ function defaultPanelWidth() {
508
+ try {
509
+ return clamp(Math.min(DEFAULT_PANEL_W, window.innerWidth - 48), 380, 1600);
510
+ } catch (error) {
511
+ return DEFAULT_PANEL_W;
512
+ }
513
+ }
514
+
515
+ function loadUi() {
516
+ var fallback = {
517
+ x: null, y: null, w: defaultPanelWidth(),
518
+ h: clamp(DEFAULT_PANEL_H, 320, Math.max(360, (window.innerHeight || 900) - 16)),
519
+ collapsed: false, tab: 'chain', pinned: null,
520
+ graphCollapsed: { stages: {}, topics: {}, branches: {} },
521
+ scale: 1,
522
+ };
523
+ function normalize(raw, keepWidth) {
524
+ if (!raw || typeof raw !== 'object') return fallback;
525
+ var width = defaultPanelWidth();
526
+ if (keepWidth) {
527
+ width = clamp(Number(raw.w) || width, 380, Math.max(420, (window.innerWidth || 1200) - 16));
528
+ }
529
+ // 迁移旧版窄面板的位置:宽度变大后要收进视口,否则会跑到屏幕外
530
+ var maxX = Math.max(4, (window.innerWidth || 1200) - width - 8);
531
+ var rawCollapsed = raw.graphCollapsed;
532
+ var graphCollapsed = {
533
+ stages: (rawCollapsed && typeof rawCollapsed.stages === 'object' && rawCollapsed.stages) || {},
534
+ topics: (rawCollapsed && typeof rawCollapsed.topics === 'object' && rawCollapsed.topics) || {},
535
+ branches: (rawCollapsed && typeof rawCollapsed.branches === 'object' && rawCollapsed.branches) || {},
536
+ };
537
+ return {
538
+ x: typeof raw.x === 'number' ? clamp(raw.x, 4, maxX) : null,
539
+ y: typeof raw.y === 'number' ? clamp(raw.y, 4, Math.max(4, (window.innerHeight || 900) - 60)) : null,
540
+ w: width,
541
+ h: clamp(Number(raw.h) || DEFAULT_PANEL_H, 320, Math.max(360, (window.innerHeight || 900) - 16)),
542
+ collapsed: raw.collapsed === true,
543
+ tab: raw.tab === 'graph' ? 'graph' : 'chain',
544
+ pinned: typeof raw.pinned === 'string' && raw.pinned ? raw.pinned : null,
545
+ graphCollapsed: graphCollapsed,
546
+ scale: (typeof raw.scale === 'number' && raw.scale >= 0.35 && raw.scale <= 2.5) ? raw.scale : 1,
547
+ };
548
+ }
549
+ try {
550
+ var raw = JSON.parse(localStorage.getItem(UI_KEY) || 'null');
551
+ if (raw && typeof raw === 'object') return normalize(raw, true);
552
+ // v1 → v2:保留位置 / 收起 / 标签 / 固定会话,但宽度改用新的默认宽度(旧默认太窄)
553
+ var old = JSON.parse(localStorage.getItem(UI_KEY_V1) || 'null');
554
+ if (old && typeof old === 'object') return normalize(old, false);
555
+ return fallback;
556
+ } catch (error) {
557
+ return fallback;
558
+ }
559
+ }
560
+
561
+ function saveUi(ui) {
562
+ try { localStorage.setItem(UI_KEY, JSON.stringify(ui)); } catch (error) { /* 忽略 */ }
563
+ }
564
+
565
+ async function fetchJson(url) {
566
+ try {
567
+ var response = await fetch(url, { headers: { Accept: 'application/json' } });
568
+ if (!response.ok) return null;
569
+ return await response.json();
570
+ } catch (error) {
571
+ return null;
572
+ }
573
+ }
574
+
575
+ async function postJson(url, body) {
576
+ try {
577
+ var response = await fetch(url, {
578
+ method: 'POST',
579
+ headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
580
+ body: JSON.stringify(body || {}),
581
+ });
582
+ if (!response.ok) return null;
583
+ return await response.json();
584
+ } catch (error) {
585
+ return null;
586
+ }
587
+ }
588
+
589
+ // ── 图谱布局(纯函数) ──────────────────────────────────────────────────
590
+ var LAYOUT = {
591
+ padX: 22,
592
+ indent: 30,
593
+ nodeH: 44,
594
+ gap: 16,
595
+ branchW: 300,
596
+ branchHead: 26,
597
+ childNodeH: 46,
598
+ childGap: 8,
599
+ childIndent: 14,
600
+ railW: 56,
601
+ railGap: 12,
602
+ stageHead: 36,
603
+ stageGap: 24,
604
+ width: 1000,
605
+ };
606
+
607
+ /**
608
+ * 图谱布局:左侧主列是阶段 → 根话题 → 子话题的树;右侧把子智能体会话
609
+ * 展开成「分支框 + 子智能体自己的话题树」,与主图同屏显示。
610
+ * 同时把 Host 的逻辑边(next / belongTo / has_relation / delegates)
611
+ * 以及子智能体内部的话题归属换算成 SVG 路径。
612
+ */
613
+ function buildLayout(graph, options) {
614
+ var opts = options || {};
615
+ var canvasW = Number(opts.width) > 0 ? Math.max(620, Number(opts.width)) : LAYOUT.width;
616
+ // 兼容两种传法:{ stages, topics, branches } 或 { collapsedStages, ... }
617
+ var collapsedStages = opts.stages || opts.collapsedStages || {};
618
+ var collapsedTopics = opts.topics || opts.collapsedTopics || {};
619
+ var collapsedBranches = opts.branches || opts.collapsedBranches || {};
620
+ var topics = Array.isArray(graph && graph.topics) ? graph.topics : [];
621
+ var stages = Array.isArray(graph && graph.stages) ? graph.stages : [];
622
+ var branchList = Array.isArray(graph && graph.branches) ? graph.branches : [];
623
+ var graphEdges = (Array.isArray(graph && graph.edges) ? graph.edges : []).filter(function (edge) {
624
+ return !(edge.type === 'has_relation' && edge.kind === 'extension');
625
+ });
626
+ if (!graphEdges.length && topics.length) {
627
+ var rootList = topics.filter(function (topic) { return !topic.parentId; });
628
+ graphEdges = [];
629
+ for (var ri = 0; ri < rootList.length; ri += 1) {
630
+ var rootNode = rootList[ri];
631
+ if (rootNode.stageId) graphEdges.push({ id: 'belong:' + rootNode.id, type: 'belongTo', kind: 'stage', from: rootNode.id, to: rootNode.stageId });
632
+ if (ri > 0) graphEdges.push({ id: 'next:' + rootList[ri - 1].id + '>' + rootNode.id, type: 'next', from: rootList[ri - 1].id, to: rootNode.id });
633
+ }
634
+ topics.forEach(function (topic) {
635
+ if (topic.parentId) graphEdges.push({ id: 'belong:' + topic.id, type: 'belongTo', kind: 'topic', from: topic.id, to: topic.parentId });
636
+ (Array.isArray(topic.branchIds) ? topic.branchIds : []).forEach(function (sessionId) {
637
+ graphEdges.push({ id: 'deleg:' + topic.id + '>' + sessionId, type: 'delegates', from: topic.id, to: sessionId, label: '子智能体' });
638
+ });
639
+ });
640
+ }
641
+ var byId = {};
642
+ topics.forEach(function (topic) { byId[topic.id] = topic; });
643
+ var branchBySession = {};
644
+ branchList.forEach(function (branch) { branchBySession[branch.sessionId] = branch; });
645
+ var children = {};
646
+ topics.forEach(function (topic) {
647
+ if (!topic.parentId || !byId[topic.parentId]) return;
648
+ if (!children[topic.parentId]) children[topic.parentId] = [];
649
+ children[topic.parentId].push(topic);
650
+ });
651
+ var hasBranches = branchList.length > 0;
652
+ var branchW = hasBranches ? LAYOUT.branchW : 0;
653
+ var mainW = Math.min(canvasW - 8 - branchW - LAYOUT.railGap * 2 - LAYOUT.railW, 660);
654
+ var railLeft = mainW + LAYOUT.railGap;
655
+ var railRight = railLeft + LAYOUT.railW;
656
+ var railX = (railLeft + railRight) / 2;
657
+ var branchLeft = railRight + LAYOUT.railGap;
658
+ var nodes = [];
659
+ var branchNodes = [];
660
+ var branchBoxes = [];
661
+ var stageBoxes = [];
662
+ var synthesizedEdges = [];
663
+ var y = 14;
664
+
665
+ function branchesOf(topic) {
666
+ var out = [];
667
+ (Array.isArray(topic.branchIds) ? topic.branchIds : []).forEach(function (sessionId) {
668
+ if (branchBySession[sessionId] && out.indexOf(branchBySession[sessionId]) === -1) out.push(branchBySession[sessionId]);
669
+ });
670
+ branchList.forEach(function (branch) {
671
+ if (branch.parentTopicId === topic.id && out.indexOf(branch) === -1) out.push(branch);
672
+ });
673
+ return out;
674
+ }
675
+
676
+ /** 子智能体分支:头部节点 + 它自己的话题树。返回该分支占用的高度。 */
677
+ function layoutBranch(branch, parentNode, top) {
678
+ var left = branchLeft;
679
+ var header = {
680
+ kind: 'branchHeader',
681
+ id: 'branchh:' + branch.sessionId,
682
+ branch: branch,
683
+ parentNodeId: parentNode.id,
684
+ x: left,
685
+ y: top,
686
+ w: LAYOUT.branchW,
687
+ h: LAYOUT.branchHead,
688
+ depth: parentNode.depth + 1,
689
+ };
690
+ var branchCollapsed = collapsedBranches[branch.sessionId] === true;
691
+ header.collapsed = branchCollapsed;
692
+ header.topicCount = Array.isArray(branch.topics) ? branch.topics.length : 0;
693
+ branchNodes.push(header);
694
+ var branchTopics = Array.isArray(branch.topics) ? branch.topics : [];
695
+ var childById = {};
696
+ branchTopics.forEach(function (topic) { childById[topic.id] = topic; });
697
+ var childChildren = {};
698
+ branchTopics.forEach(function (topic) {
699
+ if (!topic.parentId || !childById[topic.parentId]) return;
700
+ if (!childChildren[topic.parentId]) childChildren[topic.parentId] = [];
701
+ childChildren[topic.parentId].push(topic);
702
+ });
703
+ var cursor = top + LAYOUT.branchHead + (branchTopics.length ? 6 : 2);
704
+ var childNodeById = {};
705
+ function layoutChild(topic, depth) {
706
+ var node = {
707
+ kind: 'branchTopic',
708
+ id: 'brancht:' + branch.sessionId + ':' + topic.id,
709
+ branch: branch,
710
+ topic: topic,
711
+ parentNodeId: parentNode.id,
712
+ x: left + 8 + depth * LAYOUT.childIndent,
713
+ y: cursor,
714
+ w: LAYOUT.branchW - 16 - depth * LAYOUT.childIndent,
715
+ h: LAYOUT.childNodeH,
716
+ depth: parentNode.depth + 1 + depth,
717
+ childDepth: depth,
718
+ };
719
+ branchNodes.push(node);
720
+ childNodeById[topic.id] = node;
721
+ cursor += LAYOUT.childNodeH + LAYOUT.childGap;
722
+ (childChildren[topic.id] || []).forEach(function (child) {
723
+ var childNode = layoutChild(child, depth + 1);
724
+ synthesizedEdges.push({ id: 'belong:' + node.id + '>' + childNode.id, type: 'belongTo', kind: 'topic', path: directPath(node, childNode) });
725
+ });
726
+ return node;
727
+ }
728
+ var roots = branchTopics.filter(function (topic) { return !topic.parentId || !childById[topic.parentId]; });
729
+ if (!branchCollapsed) {
730
+ roots.forEach(function (topic) {
731
+ var childNode = layoutChild(topic, 0);
732
+ synthesizedEdges.push({ id: 'belong:' + header.id + '>' + childNode.id, type: 'belongTo', kind: 'branch', path: headerToChildPath(header, childNode) });
733
+ });
734
+ }
735
+ (branch.relations || []).forEach(function (rel) {
736
+ var fromNode = childNodeById[rel.from];
737
+ var toNode = childNodeById[rel.to];
738
+ if (!fromNode || !toNode) return;
739
+ var fY = fromNode.y + fromNode.h / 2;
740
+ var tY = toNode.y + toNode.h / 2;
741
+ synthesizedEdges.push({
742
+ id: 'rel:' + branch.sessionId + ':' + rel.from + '>' + rel.to,
743
+ type: 'has_relation',
744
+ label: rel.label,
745
+ marker: 'rel',
746
+ path: railElbow(fromNode.x, fY, toNode.x, tY),
747
+ labelX: railX + 6,
748
+ labelY: (fY + tY) / 2 + 3,
749
+ labelBg: labelBox(rel.label, railX, (fY + tY) / 2),
750
+ });
751
+ });
752
+ var bottom = Math.max(cursor - LAYOUT.childGap + 3, header.y + LAYOUT.branchHead + 4);
753
+ branchBoxes.push({
754
+ kind: 'branchBox',
755
+ id: 'branchb:' + branch.sessionId,
756
+ branch: branch,
757
+ x: left,
758
+ y: top - 4,
759
+ w: LAYOUT.branchW,
760
+ h: bottom - top + 8,
761
+ });
762
+ return bottom - top;
763
+ }
764
+
765
+ function directPath(parent, child) {
766
+ var y1 = parent.y + parent.h;
767
+ var y2 = child.y;
768
+ var dy = Math.max(12, (y2 - y1) / 2);
769
+ return 'M ' + (parent.x + 18) + ' ' + y1
770
+ + ' C ' + (parent.x + 18) + ' ' + (y1 + dy) + ', ' + (child.x + 18) + ' ' + (y2 - dy) + ', ' + (child.x + 18) + ' ' + y2;
771
+ }
772
+
773
+ function headerToChildPath(header, child) {
774
+ var y1 = header.y + header.h;
775
+ var y2 = child.y;
776
+ var dy = Math.max(10, (y2 - y1) / 2);
777
+ return 'M ' + (header.x + 16) + ' ' + y1
778
+ + ' C ' + (header.x + 16) + ' ' + (y1 + dy) + ', ' + (child.x + 16) + ' ' + (y2 - dy) + ', ' + (child.x + 16) + ' ' + y2;
779
+ }
780
+
781
+ function layoutTopic(topic, depth) {
782
+ var collapsed = collapsedTopics[topic.id] === true;
783
+ var node = {
784
+ kind: 'topic',
785
+ id: topic.id,
786
+ topic: topic,
787
+ collapsed: collapsed,
788
+ childCount: (children[topic.id] || []).length,
789
+ x: LAYOUT.padX + depth * LAYOUT.indent,
790
+ y: y,
791
+ w: Math.min(mainW - LAYOUT.padX * 2 - depth * LAYOUT.indent, 560),
792
+ h: LAYOUT.nodeH,
793
+ depth: depth,
794
+ };
795
+ nodes.push(node);
796
+ var attached = branchesOf(topic);
797
+ var branchStack = 0;
798
+ var branchTop = node.y + 4;
799
+ attached.forEach(function (branch) {
800
+ branchStack += layoutBranch(branch, node, branchTop) + 26;
801
+ branchTop = node.y + 4 + branchStack;
802
+ });
803
+ y = node.y + Math.max(node.h, branchStack) + LAYOUT.gap;
804
+ if (!collapsed) {
805
+ (children[topic.id] || []).forEach(function (child) {
806
+ layoutTopic(child, depth + 1);
807
+ });
808
+ }
809
+ return node;
810
+ }
811
+
812
+ stages.forEach(function (stage) {
813
+ var collapsed = collapsedStages[stage.id] === true;
814
+ var roots = (stage.topicIds || []).map(function (id) { return byId[id]; }).filter(Boolean);
815
+ var top = y;
816
+ y += LAYOUT.stageHead + 4;
817
+ if (!collapsed) {
818
+ roots.forEach(function (root) { layoutTopic(root, 0); });
819
+ }
820
+ var bottom = collapsed
821
+ ? top + LAYOUT.stageHead + 8
822
+ : Math.max(y - LAYOUT.gap + 10, top + LAYOUT.stageHead + 8);
823
+ stageBoxes.push({
824
+ kind: 'stage',
825
+ id: stage.id,
826
+ stage: stage,
827
+ collapsed: collapsed,
828
+ topicCount: roots.length,
829
+ x: 8,
830
+ y: top,
831
+ w: mainW - 8,
832
+ h: bottom - top,
833
+ depth: 0,
834
+ });
835
+ y = bottom + LAYOUT.stageGap;
836
+ });
837
+
838
+ var nodeByTopic = {};
839
+ nodes.forEach(function (node) { nodeByTopic[node.id] = node; });
840
+ var boxByStage = {};
841
+ stageBoxes.forEach(function (box) { boxByStage[box.id] = box; });
842
+ var headerByBranch = {};
843
+ branchNodes.forEach(function (node) { if (node.kind === 'branchHeader') headerByBranch[node.branch.sessionId] = node; });
844
+
845
+ /** 平滑曲线:端点先向轨道方向伸展,再平滑折回另一端点(单段三次贝塞尔)。 */
846
+ function railElbow(x1, y1, x2, y2) {
847
+ return 'M ' + x1 + ' ' + y1
848
+ + ' C ' + railX + ' ' + y1
849
+ + ', ' + railX + ' ' + y2
850
+ + ', ' + x2 + ' ' + y2;
851
+ }
852
+
853
+ /** 关系标签的底框(按字符宽度估算)。 */
854
+ function labelBox(text, x, y) {
855
+ var str = String(text == null ? '' : text);
856
+ var w = 10;
857
+ for (var i = 0; i < str.length; i += 1) {
858
+ w += str.charCodeAt(i) > 0x2e80 ? 10 : 6;
859
+ }
860
+ return { x: x - w / 2, y: y - 8, w: w, h: 16 };
861
+ }
862
+
863
+ function curve(x1, y1, x2, y2, horizontal) {
864
+ if (horizontal) {
865
+ var dx = Math.max(24, Math.abs(x2 - x1) / 2);
866
+ return 'M ' + x1 + ' ' + y1
867
+ + ' C ' + (x1 + dx) + ' ' + y1 + ', ' + (x2 - dx) + ' ' + y2 + ', ' + x2 + ' ' + y2;
868
+ }
869
+ var dy = Math.max(24, Math.abs(y2 - y1) / 2);
870
+ return 'M ' + x1 + ' ' + y1
871
+ + ' C ' + x1 + ' ' + (y1 + dy) + ', ' + x2 + ' ' + (y2 - dy) + ', ' + x2 + ' ' + y2;
872
+ }
873
+
874
+ var edges = []
875
+ graphEdges.forEach(function (edge, index) {
876
+ if (!edge || !edge.type) return;
877
+ var out = { id: edge.id || ('e' + index), type: edge.type, label: edge.label || '' };
878
+ if (edge.type === 'next') {
879
+ var a = nodeByTopic[edge.from];
880
+ var b = nodeByTopic[edge.to];
881
+ if (!a || !b) return;
882
+ out.path = railElbow(a.x + a.w, a.y + a.h / 2, b.x + b.w, b.y + b.h / 2);
883
+ out.marker = 'next';
884
+ } else if (edge.type === 'belongTo' && edge.kind === 'stage') {
885
+ var node = nodeByTopic[edge.from];
886
+ var box = boxByStage[edge.to];
887
+ if (!node || !box) return;
888
+ out.path = 'M ' + (box.x + 10) + ' ' + (node.y + 16) + ' L ' + node.x + ' ' + (node.y + 16);
889
+ } else if (edge.type === 'belongTo') {
890
+ var parent = nodeByTopic[edge.to];
891
+ var child = nodeByTopic[edge.from];
892
+ if (!parent || !child) return;
893
+ out.path = curve(parent.x + 26, parent.y + parent.h, child.x + 26, child.y, false);
894
+ } else if (edge.type === 'has_relation') {
895
+ var src = nodeByTopic[edge.from];
896
+ var dst = nodeByTopic[edge.to];
897
+ if (!src || !dst) return;
898
+ var srcY = src.y + src.h / 2;
899
+ var dstY = dst.y + dst.h / 2;
900
+ out.path = railElbow(src.x + src.w, srcY, dst.x + dst.w, dstY);
901
+ out.marker = 'rel';
902
+ out.label = edge.label || '';
903
+ out.labelX = railX;
904
+ out.labelY = (srcY + dstY) / 2 + 3;
905
+ out.labelBg = labelBox(out.label, railX, (srcY + dstY) / 2);
906
+ } else if (edge.type === 'delegates') {
907
+ var sourceNode = nodeByTopic[edge.from];
908
+ var headerNode = headerByBranch[edge.to];
909
+ if (!sourceNode || !headerNode) return;
910
+ var sY = sourceNode.y + sourceNode.h / 2;
911
+ var hY = headerNode.y + headerNode.h / 2;
912
+ out.path = railElbow(sourceNode.x + sourceNode.w, sY, headerNode.x, hY);
913
+ out.marker = 'deleg';
914
+ out.label = edge.label || '';
915
+ out.labelX = railX;
916
+ out.labelY = (sY + hY) / 2 + 3;
917
+ out.labelBg = labelBox(out.label, railX, (sY + hY) / 2);
918
+ } else {
919
+ return;
920
+ }
921
+ edges.push(out);
922
+ });
923
+ synthesizedEdges.forEach(function (edge) { edges.push(edge); });
924
+
925
+ return {
926
+ width: canvasW,
927
+ height: Math.max(y + 10, 240),
928
+ nodes: nodes,
929
+ branchNodes: branchNodes,
930
+ branchBoxes: branchBoxes,
931
+ edges: edges,
932
+ stageBoxes: stageBoxes,
933
+ edgeTypes: edges.map(function (edge) { return edge.type; }),
934
+ };
935
+ }
936
+
937
+ var TIMELINE_PALETTE = ['#679efe', '#22c55e', '#eab308', '#a855f7', '#f97316', '#06b6d4', '#ec4899'];
938
+
939
+ /** 时间线布局:X = 轮次(时间),Y = 阶段泳道;话题是按其轮次跨度定位的细横线/点。 */
940
+ function buildTimelineLayout(graph, viewportWidth) {
941
+ var topics = Array.isArray(graph && graph.topics) ? graph.topics : [];
942
+ var stages = Array.isArray(graph && graph.stages) ? graph.stages : [];
943
+ var edges = Array.isArray(graph && graph.edges) ? graph.edges : [];
944
+ var roots = topics.filter(function (t) { return !t.parentId; })
945
+ .sort(function (a, b) { return (a.turnStart || 0) - (b.turnStart || 0); });
946
+ var indexById = {};
947
+ roots.forEach(function (t, i) { indexById[t.id] = i + 1; });
948
+ var padLeft = 16;
949
+ var padRight = 16;
950
+ var rulerH = 22;
951
+ var laneH = 38;
952
+ var barH = 4;
953
+ var totalTurns = 1;
954
+ topics.forEach(function (t) { totalTurns = Math.max(totalTurns, t.turnEnd || t.turnStart || 1); });
955
+ // 尽量一页显示:按可视宽度反推每轮像素
956
+ var avail = Math.max(320, (viewportWidth || 900) - padLeft - padRight);
957
+ var turnW = Math.min(40, Math.max(2.5, avail / Math.max(1, totalTurns)));
958
+ var width = padLeft + totalTurns * turnW + padRight;
959
+ var lanes = [];
960
+ var posById = {};
961
+ stages.forEach(function (stage, si) {
962
+ var laneTopics = roots.filter(function (t) { return t.stageId === stage.id; });
963
+ var y = rulerH + si * laneH;
964
+ lanes.push({ stage: stage, index: si, y: y, h: laneH, topics: laneTopics });
965
+ laneTopics.forEach(function (topic) {
966
+ var startTurn = topic.turnStart || 1;
967
+ var endTurn = topic.turnEnd || startTurn;
968
+ var span = Math.max(1, endTurn - startTurn + 1);
969
+ posById[topic.id] = {
970
+ x: padLeft + (startTurn - 1) * turnW + 0.5,
971
+ y: y + (laneH - barH) / 2,
972
+ w: Math.max(6, span * turnW - 1),
973
+ h: barH,
974
+ topic: topic,
975
+ stage: stage,
976
+ index: indexById[topic.id],
977
+ };
978
+ });
979
+ });
980
+ var height = rulerH + Math.max(lanes.length, 1) * laneH + 10;
981
+ var rawStep = Math.max(1, 56 / turnW);
982
+ var niceSteps = [1, 2, 5, 10, 20, 50, 100, 200, 500];
983
+ var step = niceSteps.filter(function (n) { return n >= rawStep; })[0] || 500;
984
+ var ticks = [];
985
+ for (var tn = 1; tn <= totalTurns; tn += step) ticks.push({ turn: tn, x: padLeft + (tn - 1) * turnW });
986
+ var relations = [];
987
+ function bez(t, p0, p1, p2, p3) {
988
+ var mt = 1 - t;
989
+ return mt * mt * mt * p0 + 3 * mt * mt * t * p1 + 3 * mt * t * t * p2 + t * t * t * p3;
990
+ }
991
+ // 时间上相邻的 topic 对(同阶段相邻 / 相邻阶段首尾):若它们之间只有 1 条关系边,视为冗余不画
992
+ var adjacentPairs = {};
993
+ for (var ap = 0; ap < roots.length - 1; ap += 1) {
994
+ adjacentPairs[roots[ap].id + '>' + roots[ap + 1].id] = true;
995
+ adjacentPairs[roots[ap + 1].id + '>' + roots[ap].id] = true;
996
+ }
997
+ var relEdges = edges.filter(function (e) { return e.type === 'has_relation'; });
998
+ var pairCount = {};
999
+ relEdges.forEach(function (e) {
1000
+ var pairKey = [e.from, e.to].sort().join('>');
1001
+ pairCount[pairKey] = (pairCount[pairKey] || 0) + 1;
1002
+ });
1003
+ relEdges = relEdges.filter(function (e) {
1004
+ var pairKey = [e.from, e.to].sort().join('>');
1005
+ var isAdjacent = adjacentPairs[e.from + '>' + e.to] === true;
1006
+ if (isAdjacent && pairCount[pairKey] <= 1) return false;
1007
+ return true;
1008
+ });
1009
+ relEdges.forEach(function (e) {
1010
+ var a = posById[e.from];
1011
+ var b = posById[e.to];
1012
+ if (!a || !b) return;
1013
+ var startX;
1014
+ var startY;
1015
+ var endX;
1016
+ var endY;
1017
+ var c1y;
1018
+ var c2y;
1019
+ var path;
1020
+ var sameLane = a.stage === b.stage;
1021
+ if (sameLane) {
1022
+ if (b.x >= a.x) { startX = a.x + a.w + 2; endX = b.x - 2; }
1023
+ else { startX = a.x - 2; endX = b.x + b.w + 2; }
1024
+ startY = a.y + a.h / 2;
1025
+ endY = b.y + b.h / 2;
1026
+ var lift = 14;
1027
+ c1y = startY - lift;
1028
+ c2y = endY - lift;
1029
+ path = 'M ' + startX + ' ' + startY + ' C ' + startX + ' ' + c1y + ', ' + endX + ' ' + c2y + ', ' + endX + ' ' + endY;
1030
+ } else {
1031
+ var down = b.y > a.y;
1032
+ startX = a.x + a.w / 2;
1033
+ endX = b.x + b.w / 2;
1034
+ startY = down ? a.y + a.h + 2 : a.y - 2;
1035
+ endY = down ? b.y - 2 : b.y + b.h + 2;
1036
+ var spanY = endY - startY;
1037
+ c1y = startY + spanY * 0.45;
1038
+ c2y = endY - spanY * 0.45;
1039
+ path = 'M ' + startX + ' ' + startY + ' C ' + startX + ' ' + c1y + ', ' + endX + ' ' + c2y + ', ' + endX + ' ' + endY;
1040
+ }
1041
+ // 标签沿曲线错开分布 + 向右偏移,避免压线/互相重叠
1042
+ var idx = relations.length;
1043
+ var t = 0.3 + ((idx * 0.173) % 0.4);
1044
+ var labelX = bez(t, startX, startX, endX, endX) + 10;
1045
+ var labelY = bez(t, startY, c1y, c2y, endY) - 2;
1046
+ relations.push({
1047
+ id: e.id || ('tlrel:' + e.from + '>' + e.to),
1048
+ path: path,
1049
+ label: e.label || '',
1050
+ labelX: labelX,
1051
+ labelY: labelY,
1052
+ from: e.from,
1053
+ to: e.to,
1054
+ sameLane: sameLane,
1055
+ });
1056
+ });
1057
+ var nexts = [];
1058
+ edges.filter(function (e) { return e.type === 'next'; }).forEach(function (e) {
1059
+ var a = posById[e.from];
1060
+ var b = posById[e.to];
1061
+ if (!a || !b) return;
1062
+ var ax = a.x + a.w;
1063
+ var ay = a.y + a.h / 2;
1064
+ var bx = b.x;
1065
+ var by = b.y + b.h / 2;
1066
+ nexts.push({ id: e.id || ('tlnext:' + e.from + '>' + e.to), path: 'M ' + ax + ' ' + ay + ' C ' + (ax + 8) + ' ' + ay + ', ' + (bx - 8) + ' ' + by + ', ' + bx + ' ' + by });
1067
+ });
1068
+ return {
1069
+ width: width, height: height, lanes: lanes, posById: posById,
1070
+ relations: relations, nexts: nexts, ticks: ticks,
1071
+ rulerH: rulerH, laneH: laneH, barH: barH, padLeft: padLeft, turnW: turnW,
1072
+ totalTurns: totalTurns, indexById: indexById,
1073
+ };
1074
+ }
1075
+
1076
+ // ── 样式(只使用主题 token,带兜底值) ─────────────────────────────────
1077
+ var CSS = [
1078
+ '[class*="viewArea"]:has(> .tg-root),:has(> .tg-root){flex:1 1 0% !important;min-height:0 !important;overflow:hidden !important}',
1079
+ '.tg-root{position:relative;width:100%;height:100%;flex:1;min-height:0;display:flex;flex-direction:column;overflow:hidden;',
1080
+ ' background:transparent;color:var(--dsw-alias-label-primary,#f4f4f5)}',
1081
+ '.tg-debug-banner{position:fixed;top:0;left:0;z-index:99999;background:#e11;color:#fff;padding:3px 8px;font-size:11px;font-weight:700}',
1082
+ '.tg-root-docked{position:relative;left:auto;top:auto;width:100%;height:100%;flex:1;min-height:0;z-index:auto;',
1083
+ ' border-radius:0;border:0;box-shadow:none;overflow:hidden;background:transparent}',
1084
+ '.tg-head-docked{cursor:default}',
1085
+ '.tg-model-select{flex:none;max-width:220px;height:26px;border-radius:7px;border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.22));',
1086
+ ' background:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.08));color:var(--dsw-alias-label-primary,#f4f4f5);font-size:11px;padding:0 6px}',
1087
+ '.tg-topbar{flex:none;border-bottom:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.16))}',
1088
+ '.tg-topbar-row{display:flex;align-items:center;gap:8px;padding:9px 12px}',
1089
+ '.tg-topbar-spacer{flex:1}',
1090
+ ' font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Hiragino Sans GB","Microsoft YaHei","Helvetica Neue",Arial,sans-serif}',
1091
+ '.tg-root *{box-sizing:border-box}',
1092
+ '.tg-head{display:flex;align-items:center;gap:8px;padding:10px 12px;cursor:move;user-select:none;',
1093
+ ' background:linear-gradient(180deg,rgba(127,127,127,.10),rgba(127,127,127,.02));',
1094
+ ' border-bottom:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.18))}',
1095
+ '.tg-logo{width:22px;height:22px;border-radius:7px;flex:none;display:flex;align-items:center;justify-content:center;',
1096
+ ' background:linear-gradient(135deg,var(--dsw-alias-brand-primary,#679efe),#2563eb);color:#fff;font-size:12px;font-weight:700}',
1097
+ '.tg-title{font-weight:600;font-size:13px;letter-spacing:.2px;flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
1098
+ '.tg-head-sub{font-size:10px;font-weight:500;color:var(--dsw-alias-label-secondary,#adb2b8);margin-left:6px}',
1099
+ '.tg-dot{width:7px;height:7px;border-radius:50%;flex:none;background:var(--dsw-alias-state-idle-primary,#8b8f96)}',
1100
+ '.tg-dot.running{background:var(--dsw-alias-state-warn-primary,#eab308);animation:tg-pulse 1.1s ease-in-out infinite}',
1101
+ '.tg-dot.error{background:var(--dsw-alias-state-error-primary,#ef4444)}',
1102
+ '.tg-dot.ok{background:var(--dsw-alias-state-success-primary,#22c55e)}',
1103
+ '@keyframes tg-pulse{0%,100%{opacity:1}50%{opacity:.35}}',
1104
+ '.tg-btn{border:0;background:transparent;cursor:pointer;color:var(--dsw-alias-label-secondary,#adb2b8);border-radius:7px;',
1105
+ ' min-width:26px;height:26px;display:inline-flex;align-items:center;justify-content:center;font-size:13px;line-height:1;padding:0 6px;flex:none;',
1106
+ ' transition:background .12s ease,color .12s ease}',
1107
+ '.tg-btn:hover{background:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.14));color:var(--dsw-alias-label-primary,#f4f4f5)}',
1108
+ '.tg-btn:active{opacity:.75}',
1109
+ '.tg-btn.primary{color:var(--dsw-alias-brand-primary,#679efe);border:1px solid color-mix(in srgb,var(--dsw-alias-brand-primary,#679efe) 35%,transparent)}',
1110
+ '.tg-btn.primary:hover{background:var(--dsw-alias-bg-layer-2,rgba(103,158,254,.14))}',
1111
+ '.tg-session-row{display:flex;align-items:center;gap:6px;padding:8px 10px 6px;position:relative}',
1112
+ '.tg-session-btn{flex:1;min-width:0;display:flex;align-items:center;gap:6px;cursor:pointer;background:transparent;border:0;',
1113
+ ' color:var(--dsw-alias-label-primary,#f4f4f5);font-size:12px;padding:4px 6px;border-radius:8px;text-align:left}',
1114
+ '.tg-session-btn:hover{background:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.12))}',
1115
+ '.tg-session-name{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:600}',
1116
+ '.tg-session-meta{font-size:10.5px;color:var(--dsw-alias-label-secondary,#adb2b8);white-space:nowrap}',
1117
+ '.tg-pin{font-size:10px;color:var(--dsw-alias-brand-primary,#679efe)}',
1118
+ '.tg-picker{position:absolute;left:8px;right:8px;top:100%;z-index:20;max-height:320px;overflow-y:auto;padding:6px;',
1119
+ ' border-radius:12px;background:var(--dsw-alias-bg-overlay,#2c2c2e);border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.22));',
1120
+ ' box-shadow:0 16px 40px rgba(0,0,0,.4)}',
1121
+ '.tg-picker input{width:100%;box-sizing:border-box;margin-bottom:5px;padding:5px 8px;border-radius:7px;font-size:12px;outline:none;',
1122
+ ' background:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.12));border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.22));',
1123
+ ' color:var(--dsw-alias-label-primary,#f4f4f5)}',
1124
+ '.tg-picker-item{display:flex;align-items:center;gap:6px;padding:6px 8px;border-radius:7px;cursor:pointer;font-size:12px;',
1125
+ ' white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--dsw-alias-label-primary,#e6e8ea)}',
1126
+ '.tg-picker-item:hover{background:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.14))}',
1127
+ '.tg-picker-item.sel{background:var(--dsw-alias-bg-layer-2,rgba(103,158,254,.16));color:var(--dsw-alias-brand-primary,#a8c2ff)}',
1128
+ '.tg-picker-item .tg-picker-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis}',
1129
+ '.tg-picker-item .tg-picker-time{font-size:10px;color:var(--dsw-alias-label-secondary,#adb2b8);flex:none}',
1130
+ '.tg-tabs{display:flex;gap:4px;padding:0 10px;border-bottom:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.18))}',
1131
+ '.tg-tab{flex:1;border:0;background:transparent;cursor:pointer;padding:8px 4px;font-size:12px;font-weight:600;color:var(--dsw-alias-label-secondary,#adb2b8);',
1132
+ ' border-bottom:2px solid transparent;margin-bottom:-1px;transition:color .12s ease,border-color .12s ease}',
1133
+ '.tg-tab.active{color:var(--dsw-alias-brand-primary,#679efe);border-bottom-color:var(--dsw-alias-brand-primary,#679efe)}',
1134
+ '.tg-tab .tg-tab-badge{display:inline-block;margin-left:5px;font-size:10px;padding:0 5px;border-radius:8px;',
1135
+ ' background:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.16));color:var(--dsw-alias-label-secondary,#adb2b8)}',
1136
+ '.tg-body{flex:1;min-height:0;display:flex;flex-direction:column;position:relative}',
1137
+ '.tg-workflow{flex:1;min-height:0;display:flex;overflow:hidden}',
1138
+ '.tg-workflow-tree{flex:none;width:300px;min-width:200px;max-width:46%;min-height:0;border-right:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.16));',
1139
+ ' overflow-y:auto;background:var(--dsw-alias-bg-layer-1,rgba(0,0,0,.04))}',
1140
+ '.tg-workflow-tree::-webkit-scrollbar{width:8px}',
1141
+ '.tg-workflow-tree::-webkit-scrollbar-thumb{background:var(--dsw-alias-border-l2,rgba(127,127,127,.3));border-radius:4px}',
1142
+ '.tg-workflow-detail{flex:1;min-width:0;min-height:0;overflow-y:auto}',
1143
+ '.tg-workflow-detail::-webkit-scrollbar{width:8px}',
1144
+ '.tg-workflow-detail::-webkit-scrollbar-thumb{background:var(--dsw-alias-border-l2,rgba(127,127,127,.3));border-radius:4px}',
1145
+ '.tg-wf-stage{margin:6px 8px}',
1146
+ '.tg-wf-stage-head{display:flex;flex-direction:column;align-items:stretch;gap:4px;padding:7px 8px;border-radius:9px;cursor:pointer;user-select:none;',
1147
+ ' background:linear-gradient(90deg,color-mix(in srgb,var(--dsw-alias-brand-primary,#679efe) 9%,transparent),color-mix(in srgb,var(--dsw-alias-brand-primary,#679efe) 4%,transparent));',
1148
+ ' border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.18))}',
1149
+ '.tg-wf-stage-head.active{border-color:var(--dsw-alias-brand-primary,#679efe)}',
1150
+ '.tg-wf-stage-idx{flex:none;width:17px;height:17px;border-radius:5px;display:flex;align-items:center;justify-content:center;',
1151
+ ' background:var(--dsw-alias-brand-primary,#679efe);color:#fff;font-size:10px;font-weight:700}',
1152
+ '.tg-wf-stage-title{flex:1;min-width:0;font-size:11.5px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--dsw-alias-label-primary,#f4f4f5)}',
1153
+ '.tg-wf-stage-meta{flex:none;font-size:9.5px;color:var(--dsw-alias-label-secondary,#adb2b8)}',
1154
+ '.tg-wf-stage-title-row{display:flex;align-items:center;gap:6px}',
1155
+ '.tg-wf-stage-metrics{display:flex;flex-wrap:wrap;justify-content:flex-end;text-align:right;gap:2px 10px;font-size:9px;',
1156
+ ' color:var(--dsw-alias-label-secondary,#adb2b8);padding:0 2px}',
1157
+ '.tg-wf-stage-toggle{flex:none;display:inline-flex;align-items:center;cursor:pointer;color:var(--dsw-alias-label-secondary,#adb2b8)}',
1158
+ '.tg-wf-stage-toggle:hover{color:var(--dsw-alias-label-primary,#f4f4f5)}',
1159
+ '.tg-wf-tree-head{display:flex;align-items:center;gap:5px;padding:7px 8px 5px;font-size:9.5px;color:var(--dsw-alias-label-secondary,#adb2b8)}',
1160
+ '.tg-wf-tree-head-title{flex:1;min-width:0}',
1161
+ '.tg-wf-tree-btn{font-size:9.5px;padding:0 6px;height:20px;border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.2))}',
1162
+ '.tg-wf-metric{font-variant-numeric:tabular-nums}',
1163
+ '.tg-wf-metric-time{color:var(--dsw-alias-state-success-primary,#34d399)}',
1164
+ '.tg-wf-stage-topics{margin:4px 0 2px}',
1165
+ '.tg-wf-topic{display:flex;align-items:center;gap:6px;padding:5px 8px;border-radius:7px;cursor:pointer;user-select:none;min-width:0}',
1166
+ '.tg-wf-topic:hover{background:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.08))}',
1167
+ '.tg-wf-topic.active{background:color-mix(in srgb,var(--dsw-alias-brand-primary,#679efe) 3%,transparent)}',
1168
+ '.tg-wf-topic-title{flex:1;min-width:0;font-size:11px;color:var(--dsw-alias-label-primary,#e6e8ea);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
1169
+ '.tg-wf-topic.active .tg-wf-topic-title{color:var(--dsw-alias-brand-primary,#679efe);font-weight:600}',
1170
+ '.tg-wf-topic-meta{flex:none;font-size:9px;color:var(--dsw-alias-label-secondary,#adb2b8)}',
1171
+ '.tg-wf-empty{padding:18px 14px;font-size:11px;color:var(--dsw-alias-label-secondary,#adb2b8)}',
1172
+ '.tg-wf-stage-detail{padding:12px 14px}',
1173
+ '.tg-wf-stage-detail-title{font-size:14px;font-weight:700;color:var(--dsw-alias-label-primary,#f4f4f5);margin-bottom:6px}',
1174
+ '.tg-wf-stage-detail-summary{font-size:11px;color:var(--dsw-alias-label-secondary,#c2c6cc);line-height:1.6;margin-bottom:8px}',
1175
+ '.tg-wf-stage-detail-meta{font-size:10.5px;color:var(--dsw-alias-label-secondary,#adb2b8);margin-bottom:10px}',
1176
+ '.tg-wf-stage-topics{display:flex;flex-direction:column;gap:8px;margin-top:10px}',
1177
+ '.tg-wf-topic-card{border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.16));border-radius:10px;overflow:hidden;',
1178
+ ' background:var(--dsw-alias-bg-layer-1,rgba(0,0,0,.03))}',
1179
+ '.tg-wf-topic-card.open{border-color:var(--dsw-alias-border-l2,rgba(127,127,127,.3))}',
1180
+ '.tg-wf-topic-card-head{display:flex;align-items:center;gap:7px;padding:8px 10px;user-select:none}',
1181
+ '.tg-wf-topic-card-toggle{display:flex;align-items:center;gap:6px;flex:0 1 auto;min-width:0;max-width:72%;cursor:pointer}',
1182
+ '.tg-wf-topic-card-toggle:hover .tg-wf-topic-card-title{color:var(--dsw-alias-brand-primary,#679efe)}',
1183
+ '.tg-wf-topic-card-head:hover{background:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.06))}',
1184
+ '.tg-wf-topic-card-title{flex:0 1 auto;min-width:0;font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary,#e6e8ea);',
1185
+ ' white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
1186
+ '.tg-wf-topic-card-meta{flex:none;margin-left:auto;font-size:9.5px;color:var(--dsw-alias-label-secondary,#adb2b8);font-variant-numeric:tabular-nums}',
1187
+ '.tg-wf-topic-card-body{padding:4px 10px 10px;border-top:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.12))}',
1188
+ '.tg-wf-topic-summary{font-size:11px;color:var(--dsw-alias-label-secondary,#c2c6cc);line-height:1.6;margin:6px 0;white-space:pre-wrap;word-break:break-word}',
1189
+ '.tg-wf-mini-wrap{margin:10px 0 4px;padding:8px 10px;border-radius:10px;',
1190
+ ' border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.16));background:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.04))}',
1191
+ '.tg-wf-mini-title{font-size:10px;font-weight:600;color:var(--dsw-alias-label-secondary,#adb2b8);margin-bottom:2px}',
1192
+ '.tg-wf-mini{overflow:visible}',
1193
+ '.tg-wf-mini-base{stroke:var(--dsw-alias-border-l2,rgba(127,127,127,.3));stroke-width:1}',
1194
+ '.tg-wf-mini-arc path{fill:none;stroke:var(--dsw-alias-brand-primary,#679efe);stroke-width:1.2;opacity:.3;stroke-dasharray:3 3;',
1195
+ ' transition:opacity .12s ease,stroke-width .12s ease}',
1196
+ '.tg-wf-mini-arc.active path{opacity:1;stroke-width:2;stroke-dasharray:none}',
1197
+ '.tg-wf-mini-arc text{font-size:8px;fill:var(--dsw-alias-brand-primary,#679efe);text-anchor:middle;opacity:.45;font-weight:600}',
1198
+ '.tg-wf-mini-arc.active text{opacity:1;fill:var(--dsw-alias-state-warn-primary,#eab308)}',
1199
+ '.tg-wf-mini-dot{cursor:pointer}',
1200
+ '.tg-wf-mini-dot circle{opacity:.8;transition:opacity .12s ease,stroke .12s ease}',
1201
+ '.tg-wf-mini-dot:hover circle,.tg-wf-mini-dot.active circle{opacity:1;stroke:var(--dsw-alias-label-primary,#f4f4f5);stroke-width:1.5}',
1202
+ '.tg-wf-mini-idx{font-size:8px;fill:var(--dsw-alias-label-secondary,#adb2b8);text-anchor:middle}',
1203
+ '.tg-wf-statgrid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;margin:10px 0 6px}',
1204
+ '.tg-wf-stat{padding:8px 10px;border-radius:9px;border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.16));',
1205
+ ' background:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.04))}',
1206
+ '.tg-wf-stat-top{display:flex;align-items:baseline;justify-content:space-between;gap:6px;font-size:9.5px;color:var(--dsw-alias-label-secondary,#adb2b8)}',
1207
+ '.tg-wf-stat-top b{font-size:13px;font-weight:700;color:var(--dsw-alias-label-primary,#f4f4f5)}',
1208
+ '.tg-wf-bar{height:5px;border-radius:3px;overflow:hidden;background:var(--dsw-alias-border-l1,rgba(127,127,127,.16));margin:6px 0 3px}',
1209
+ '.tg-wf-bar i{display:block;height:100%;border-radius:3px;background:var(--dsw-alias-brand-primary,#679efe)}',
1210
+ '.tg-wf-bar i.green{background:var(--dsw-alias-state-success-primary,#34d399)}',
1211
+ '.tg-wf-bar i.warn{background:var(--dsw-alias-state-warn-primary,#eab308)}',
1212
+ '.tg-wf-stat-ratio{font-size:9.5px;color:var(--dsw-alias-label-secondary,#adb2b8);font-variant-numeric:tabular-nums}',
1213
+ '.tg-jump-link{color:var(--dsw-alias-brand-primary,#679efe);cursor:pointer;font-size:10px;white-space:nowrap}',
1214
+ '.tg-jump-link:hover{text-decoration:underline}',
1215
+ '.tg-howto-foot{display:flex;align-items:center;gap:10px;margin-top:6px;font-size:10px;color:var(--dsw-alias-label-secondary,#adb2b8)}',
1216
+ '.tg-scroll{flex:1;min-height:0;overflow-y:auto;padding:8px 10px 10px}',
1217
+ '.tg-scroll::-webkit-scrollbar{width:8px;height:8px}',
1218
+ '.tg-scroll::-webkit-scrollbar-thumb{background:var(--dsw-alias-border-l2,rgba(127,127,127,.35));border-radius:4px}',
1219
+ '.tg-scroll::-webkit-scrollbar-track{background:transparent}',
1220
+ '.tg-banner{margin:8px 10px 0;padding:7px 10px;border-radius:9px;font-size:11px;display:flex;align-items:center;gap:6px;',
1221
+ ' background:color-mix(in srgb,var(--dsw-alias-state-error-primary,#ef4444) 12%,transparent);',
1222
+ ' border:1px solid color-mix(in srgb,var(--dsw-alias-state-error-primary,#ef4444) 28%,transparent);',
1223
+ ' color:var(--dsw-alias-state-error-primary,#ef4444)}',
1224
+ '.tg-banner.info{background:var(--dsw-alias-bg-layer-2,rgba(103,158,254,.10));border-color:var(--dsw-alias-border-l1,rgba(103,158,254,.24));color:var(--dsw-alias-brand-primary,#679efe)}',
1225
+ '.tg-banner .tg-banner-text{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
1226
+ '.tg-banner button{border:0;background:transparent;color:inherit;text-decoration:underline;cursor:pointer;font-size:11px;padding:0}',
1227
+ '.tg-empty{padding:26px 12px;text-align:center;font-size:12px;color:var(--dsw-alias-label-secondary,#adb2b8)}',
1228
+ '.tg-empty .tg-spin{margin:0 auto 10px}',
1229
+ '.tg-spin{width:18px;height:18px;border-radius:50%;border:2px solid var(--dsw-alias-border-l2,rgba(127,127,127,.3));',
1230
+ ' border-top-color:var(--dsw-alias-brand-primary,#679efe);animation:tg-spin .8s linear infinite}',
1231
+ '@keyframes tg-spin{to{transform:rotate(360deg)}}',
1232
+ // 阶段
1233
+ '.tg-stage{margin-bottom:10px}',
1234
+ '.tg-stage-head{display:flex;align-items:center;gap:7px;padding:7px 9px;border-radius:10px;cursor:pointer;user-select:none;',
1235
+ ' background:linear-gradient(90deg,color-mix(in srgb,var(--dsw-alias-brand-primary,#679efe) 13%,transparent),var(--dsw-alias-bg-layer-2,rgba(127,127,127,.06)));',
1236
+ ' border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.18))}',
1237
+ '.tg-stage-head:hover{border-color:var(--dsw-alias-border-l2,rgba(127,127,127,.32))}',
1238
+ '.tg-stage-index{width:19px;height:19px;border-radius:6px;flex:none;display:flex;align-items:center;justify-content:center;',
1239
+ ' background:var(--dsw-alias-brand-primary,#679efe);color:#fff;font-size:10.5px;font-weight:700}',
1240
+ '.tg-stage-title{font-weight:700;font-size:12.5px;flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
1241
+ '.tg-stage-chips{display:flex;gap:4px;flex:none;font-size:10px;color:var(--dsw-alias-label-secondary,#adb2b8)}',
1242
+ '.tg-chip{padding:1px 6px;border-radius:7px;background:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.14));white-space:nowrap}',
1243
+ '.tg-chev{font-size:9px;color:var(--dsw-alias-label-secondary,#adb2b8);transition:transform .15s ease;flex:none}',
1244
+ '.tg-chev.open{transform:rotate(90deg)}',
1245
+ '.tg-stage-body{padding:7px 0 0 4px}',
1246
+ // 话题
1247
+ '.tg-topic{border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.16));border-radius:10px;margin:0 0 7px;overflow:hidden;',
1248
+ ' background:var(--dsw-alias-bg-layer-1,transparent)}',
1249
+ '.tg-topic-head{display:flex;align-items:center;gap:6px;padding:7px 9px;cursor:pointer;user-select:none}',
1250
+ '.tg-topic-head:hover{background:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.08))}',
1251
+ '.tg-topic-main{flex:1;min-width:0}',
1252
+ '.tg-topic-title{font-size:12.5px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
1253
+ '.tg-topic-sub{margin-top:2px;display:flex;gap:5px;align-items:center;font-size:10.5px;color:var(--dsw-alias-label-secondary,#adb2b8);white-space:nowrap;overflow:hidden}',
1254
+ '.tg-topic-metrics{flex:none;display:flex;flex-direction:column;align-items:flex-end;gap:2px;font-size:10px;color:var(--dsw-alias-label-secondary,#adb2b8)}',
1255
+ '.tg-topic-metrics b{color:var(--dsw-alias-label-primary,#f4f4f5);font-weight:600;font-size:11px}',
1256
+ '.tg-badge{display:inline-flex;align-items:center;padding:0 5px;border-radius:7px;font-size:9.5px;line-height:16px;flex:none;',
1257
+ ' background:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.16));color:var(--dsw-alias-label-secondary,#adb2b8)}',
1258
+ '.tg-badge.active{color:var(--dsw-alias-state-success-primary,#22c55e);background:color-mix(in srgb,var(--dsw-alias-state-success-primary,#22c55e) 14%,transparent)}',
1259
+ '.tg-badge.llm{color:var(--dsw-alias-brand-primary,#679efe);background:color-mix(in srgb,var(--dsw-alias-brand-primary,#679efe) 13%,transparent)}',
1260
+ '.tg-topic-body{padding:0 9px 9px}',
1261
+ '.tg-topic-summary{font-size:11.5px;color:var(--dsw-alias-label-secondary,#adb2b8);margin:0 0 7px}',
1262
+ '.tg-statgrid{display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-bottom:8px}',
1263
+ '.tg-stat{border-radius:8px;padding:6px 8px;background:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.07));min-width:0}',
1264
+ '.tg-stat-label{font-size:9.5px;color:var(--dsw-alias-label-secondary,#adb2b8);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
1265
+ '.tg-stat-value{font-size:12px;font-weight:600;margin-top:1px;display:flex;align-items:baseline;gap:4px;flex-wrap:wrap}',
1266
+ '.tg-stat-value span.ratio{font-size:10px;font-weight:500;color:var(--dsw-alias-label-secondary,#adb2b8)}',
1267
+ '.tg-bar{height:3px;border-radius:2px;margin-top:4px;overflow:hidden;background:var(--dsw-alias-border-l1,rgba(127,127,127,.18))}',
1268
+ '.tg-bar i{display:block;height:100%;border-radius:2px;background:var(--dsw-alias-brand-primary,#679efe);opacity:.85}',
1269
+ '.tg-bar.tool i{background:var(--dsw-alias-state-warn-primary,#eab308)}',
1270
+ '.tg-bar.time i{background:var(--dsw-alias-state-success-primary,#22c55e)}',
1271
+ '.tg-turn{border-radius:8px;margin-bottom:5px;background:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.06));overflow:hidden;',
1272
+ ' border:1px solid transparent;cursor:pointer}',
1273
+ '.tg-turn:hover{border-color:var(--dsw-alias-border-l1,rgba(127,127,127,.22))}',
1274
+ '.tg-turn.running{border-color:color-mix(in srgb,var(--dsw-alias-state-warn-primary,#eab308) 40%,transparent)}',
1275
+ '.tg-turn-row{display:flex;align-items:center;gap:6px;padding:5px 7px;min-width:0}',
1276
+ '.tg-turn-no{flex:none;font-size:10px;font-weight:700;color:var(--dsw-alias-brand-primary,#679efe);min-width:26px}',
1277
+ '.tg-turn-prompt{flex:1;min-width:0;font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--dsw-alias-label-primary,#e6e8ea)}',
1278
+ '.tg-turn-meta{flex:none;display:flex;gap:5px;font-size:9.5px;color:var(--dsw-alias-label-secondary,#adb2b8);white-space:nowrap}',
1279
+ '.tg-turn-meta b{font-weight:600;color:var(--dsw-alias-label-primary,#d4d4d8)}',
1280
+ '.tg-turn-detail{padding:2px 8px 8px;display:grid;grid-template-columns:1fr 1fr;gap:4px 10px;font-size:10.5px;color:var(--dsw-alias-label-secondary,#adb2b8)}',
1281
+ '.tg-turn-detail .full{grid-column:1/-1;white-space:pre-wrap;word-break:break-word;line-height:1.5;',
1282
+ ' background:var(--dsw-alias-bg-layer-1,rgba(0,0,0,.10));border-radius:6px;padding:5px 7px;max-height:120px;overflow:hidden}',
1283
+ '.tg-turn-detail .full b{color:var(--dsw-alias-label-primary,#d4d4d8);display:block;margin-bottom:1px;font-size:10px;font-weight:600}',
1284
+ '.tg-turn-detail .kv{display:flex;justify-content:space-between;gap:6px}',
1285
+ '.tg-turn-detail .kv span:last-child{color:var(--dsw-alias-label-primary,#d4d4d8);font-weight:600;text-align:right}',
1286
+ '.tg-turn-analysis{grid-column:1/-1;margin-top:6px;padding-top:8px;border-top:1px dashed var(--dsw-alias-border-l1,rgba(127,127,127,.18))}',
1287
+ '.tg-turn-analysis-head{display:flex;align-items:center;gap:6px;font-size:10.5px;font-weight:600;color:var(--dsw-alias-label-primary,#d4d4d8);margin-bottom:6px}',
1288
+ '.tg-howto-steps{list-style:none;margin:0 0 6px;padding:0;display:flex;flex-direction:column;gap:6px}',
1289
+ '.tg-howto-steps li{padding:6px 8px;border-radius:8px;background:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.06));',
1290
+ ' border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.16))}',
1291
+ '.tg-howto-title{font-size:11px;font-weight:600;color:var(--dsw-alias-label-primary,#e6e8ea)}',
1292
+ '.tg-howto-detail{font-size:10.5px;color:var(--dsw-alias-label-secondary,#c2c6cc);margin-top:2px;line-height:1.5;white-space:pre-wrap;word-break:break-word}',
1293
+ '.tg-howto-tools{font-size:9.5px;color:var(--dsw-alias-brand-primary,#679efe);margin-top:3px}',
1294
+ '.tg-howto-outcome{font-size:10px;color:var(--dsw-alias-state-success-primary,#34d399);margin-top:3px}',
1295
+ '.tg-howto-error{font-size:10.5px;color:var(--dsw-alias-state-error-primary,#ef4444);margin-bottom:4px}',
1296
+ '.tg-howto-empty{font-size:10.5px;color:var(--dsw-alias-label-secondary,#adb2b8);margin-bottom:4px}',
1297
+ '.tg-turn-toggle{display:flex;align-items:center;gap:6px;flex:1;min-width:0;cursor:pointer}',
1298
+ '.tg-turn-toggle:hover .tg-turn-prompt,.tg-turn-toggle:hover .tg-turn-no{color:var(--dsw-alias-brand-primary,#679efe)}',
1299
+ '.tg-howto-stepcount{margin-left:6px;font-size:9px;font-weight:600;color:var(--dsw-alias-brand-primary,#679efe);',
1300
+ ' background:color-mix(in srgb,var(--dsw-alias-brand-primary,#679efe) 12%,transparent);border-radius:6px;padding:0 5px}',
1301
+ '.tg-analyze-btn{margin-top:2px;border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.22));font-size:10.5px;padding:0 8px;height:24px}',
1302
+ // 图谱
1303
+ '.tg-graph{flex:1;min-height:0;display:flex;flex-direction:column}',
1304
+ '.tg-graph-toolbar{display:flex;align-items:center;gap:5px;padding:6px 10px;font-size:10px;color:var(--dsw-alias-label-secondary,#adb2b8);',
1305
+ ' border-bottom:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.14))}',
1306
+ '.tg-graph-hint{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
1307
+ '.tg-graph-modes{display:inline-flex;gap:2px;padding:2px;border-radius:7px;background:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.08))}',
1308
+ '.tg-mode-btn{border:0;background:transparent;cursor:pointer;font-size:10px;font-family:inherit;padding:2px 8px;border-radius:5px;',
1309
+ ' color:var(--dsw-alias-label-secondary,#adb2b8);white-space:nowrap}',
1310
+ '.tg-mode-btn:hover{color:var(--dsw-alias-label-primary,#f4f4f5)}',
1311
+ '.tg-mode-btn.active{background:var(--dsw-alias-brand-primary,#679efe);color:#fff;font-weight:600}',
1312
+ '.tg-tl-split{flex:1;min-height:0;display:flex;overflow:hidden}',
1313
+ '.tg-tl-outline{flex:none;width:236px;min-height:0;overflow-y:auto;border-right:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.16));padding:6px}',
1314
+ '.tg-tl-outline::-webkit-scrollbar{width:8px}',
1315
+ '.tg-tl-outline::-webkit-scrollbar-thumb{background:var(--dsw-alias-border-l2,rgba(127,127,127,.3));border-radius:4px}',
1316
+ '.tg-tl-stage{margin-bottom:6px}',
1317
+ '.tg-tl-stage-head{display:flex;align-items:center;gap:6px;padding:5px 6px;border-radius:7px;background:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.07));',
1318
+ ' font-weight:600;font-size:10px;color:var(--dsw-alias-label-primary,#e6e8ea)}',
1319
+ '.tg-tl-stage-idx{width:16px;height:16px;border-radius:5px;display:flex;align-items:center;justify-content:center;flex:none;',
1320
+ ' background:var(--dsw-alias-brand-primary,#679efe);color:#fff;font-size:9px;font-weight:700}',
1321
+ '.tg-tl-stage-title{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
1322
+ '.tg-tl-topic{display:flex;align-items:center;gap:6px;padding:3px 6px 3px 14px;border-radius:6px;cursor:pointer;font-size:10px;',
1323
+ ' color:var(--dsw-alias-label-secondary,#adb2b8)}',
1324
+ '.tg-tl-topic:hover{background:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.08));color:var(--dsw-alias-label-primary,#f4f4f5)}',
1325
+ '.tg-tl-topic.active{background:color-mix(in srgb,var(--dsw-alias-brand-primary,#679efe) 8%,transparent);',
1326
+ ' color:var(--dsw-alias-brand-primary,#679efe);font-weight:600}',
1327
+ '.tg-tl-topic-idx{flex:none;font-size:8.5px;font-weight:700;color:var(--dsw-alias-brand-primary,#679efe);opacity:.85}',
1328
+ '.tg-tl-topic-title{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
1329
+ '.tg-tl-scroll{flex:1;min-width:0;min-height:0;overflow:auto;position:relative;padding-bottom:150px}',
1330
+ '.tg-tl-scroll::-webkit-scrollbar{width:9px;height:9px}',
1331
+ '.tg-tl-scroll::-webkit-scrollbar-thumb{background:var(--dsw-alias-border-l2,rgba(127,127,127,.35));border-radius:5px}',
1332
+ '.tg-tl-svg{display:block;overflow:visible}',
1333
+ '.tg-tl-grid{stroke:var(--dsw-alias-border-l1,rgba(127,127,127,.12));stroke-width:1}',
1334
+ '.tg-tl-tick{font-size:8px;fill:var(--dsw-alias-label-secondary,#adb2b8);text-anchor:middle}',
1335
+ '.tg-tl-lane{fill:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.03))}',
1336
+ '.tg-tl-lane.alt{fill:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.07))}',
1337
+ '.tg-tl-lane-label{font-size:8px;fill:var(--dsw-alias-label-secondary,#adb2b8);opacity:.75}',
1338
+ '.tg-tl-next{fill:none;stroke:var(--dsw-alias-border-l2,rgba(127,127,127,.3));stroke-width:1;stroke-dasharray:2 3;opacity:.5}',
1339
+ '.tg-tl-arrow{fill:var(--dsw-alias-brand-primary,#679efe)}',
1340
+ '.tg-tl-rel-hit{fill:none;stroke:transparent;stroke-width:14;pointer-events:stroke;cursor:pointer}',
1341
+ '.tg-tl-rel-path{fill:none;stroke:var(--dsw-alias-brand-primary,#679efe);stroke-width:1.2;opacity:.42;stroke-dasharray:4 3;',
1342
+ ' transition:opacity .12s ease,stroke-width .12s ease}',
1343
+ '.tg-tl-rel.same-lane .tg-tl-rel-path{opacity:.52}',
1344
+ '.tg-tl-rel.active .tg-tl-rel-path{opacity:1;stroke-width:2.2;stroke-dasharray:none}',
1345
+ '.tg-tl-rel-label{font-size:8px;fill:var(--dsw-alias-brand-primary,#679efe);text-anchor:middle;opacity:.18;font-weight:600;',
1346
+ ' transition:opacity .12s ease,fill .12s ease}',
1347
+ '.tg-tl-svg.has-active .tg-tl-rel:not(.active) .tg-tl-rel-label{opacity:0}',
1348
+ '.tg-tl-rel.same-lane .tg-tl-rel-label{opacity:0}',
1349
+ '.tg-tl-rel.active .tg-tl-rel-label{opacity:1;fill:var(--dsw-alias-state-warn-primary,#eab308);font-weight:700}',
1350
+ '.tg-tl-bar{cursor:pointer}',
1351
+ '.tg-tl-bar-rect{fill:var(--tg-bar-color,var(--dsw-alias-brand-primary,#679efe));opacity:.85;stroke:none;',
1352
+ ' transition:opacity .12s ease,filter .12s ease}',
1353
+ '.tg-tl-bar:hover .tg-tl-bar-rect,.tg-tl-bar.active .tg-tl-bar-rect{',
1354
+ ' opacity:1;filter:brightness(1.25) drop-shadow(0 0 3px var(--tg-bar-color,rgba(103,158,254,.7)))}',
1355
+ '.tg-tl-bar-label{font-size:8.5px;fill:var(--dsw-alias-label-primary,#f4f4f5);text-anchor:middle;font-weight:700;pointer-events:none}',
1356
+ '.tg-tl-tip{position:fixed;z-index:9999;max-width:300px;padding:7px 9px;border-radius:8px;background:var(--dsw-alias-bg-overlay,#26262a);',
1357
+ ' border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.22));box-shadow:0 8px 24px rgba(0,0,0,.4);pointer-events:none}',
1358
+ '.tg-tl-tip-title{font-size:11px;font-weight:700;color:var(--dsw-alias-label-primary,#f4f4f5);margin-bottom:3px}',
1359
+ '.tg-tl-tip-line{font-size:10px;color:var(--dsw-alias-label-secondary,#adb2b8);line-height:1.5}',
1360
+ '.tg-tl-tip-rels{color:var(--dsw-alias-brand-primary,#679efe)}',
1361
+ '.tg-zoom-pct{min-width:38px;text-align:right;font-variant-numeric:tabular-nums}',
1362
+ '.tg-graph-viewport{padding-bottom:150px;flex:1;min-height:0;overflow:auto;position:relative;overscroll-behavior:contain}',
1363
+ '.tg-graph-viewport::-webkit-scrollbar{width:9px;height:9px}',
1364
+ '.tg-graph-viewport::-webkit-scrollbar-thumb{background:var(--dsw-alias-border-l2,rgba(127,127,127,.35));border-radius:5px}',
1365
+ '.tg-graph-canvas{position:relative;overflow:hidden}',
1366
+ '.tg-graph-content{position:absolute;left:0;top:0;transform-origin:0 0}',
1367
+ '.tg-edges{position:absolute;left:0;top:0;pointer-events:none;overflow:visible;z-index:1}',
1368
+ '.tg-edges path{fill:none;stroke:var(--dsw-alias-border-l2,rgba(127,127,127,.45));stroke-width:1.4}',
1369
+ '.tg-stage-box{position:absolute;border-radius:13px;pointer-events:none;z-index:0;border:1px dashed var(--dsw-alias-border-l2,rgba(127,127,127,.3));',
1370
+ ' background:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.04))}',
1371
+ '.tg-stage-box-head{position:absolute;left:12px;right:12px;top:8px;height:24px;display:flex;align-items:center;gap:6px;',
1372
+ ' pointer-events:none;font-size:11.5px;font-weight:700;color:var(--dsw-alias-label-primary,#f4f4f5)}',
1373
+ '.tg-stage-toggle{pointer-events:auto;flex:none;font-size:9px;line-height:1;padding:3px;border-radius:4px;cursor:pointer;opacity:.6;',
1374
+ ' color:var(--dsw-alias-label-secondary,#adb2b8)}',
1375
+ '.tg-stage-toggle:hover{opacity:1;background:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.16))}',
1376
+ '.tg-stage-box-idx{width:16px;height:16px;border-radius:5px;background:var(--dsw-alias-brand-primary,#679efe);color:#fff;',
1377
+ ' display:flex;align-items:center;justify-content:center;font-size:9.5px;flex:none}',
1378
+ '.tg-stage-box-title{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
1379
+ '.tg-stage-box-chip{font-size:9.5px;font-weight:500;color:var(--dsw-alias-label-secondary,#adb2b8);flex:none}',
1380
+ '.tg-node{position:absolute;border-radius:10px;padding:4px 9px;cursor:pointer;overflow:hidden;z-index:2;',
1381
+ ' background:var(--dsw-alias-bg-layer-1,#2c2c2e);border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.26));',
1382
+ ' box-shadow:0 2px 10px rgba(0,0,0,.16);transition:border-color .12s ease,transform .12s ease}',
1383
+ '.tg-node:hover{border-color:var(--dsw-alias-brand-primary,#679efe);transform:translateY(-1px)}',
1384
+ '.tg-node.active{border-color:color-mix(in srgb,var(--dsw-alias-state-success-primary,#22c55e) 45%,transparent)}',
1385
+ '.tg-node-title{display:flex;align-items:center;gap:4px;font-size:11px;font-weight:650;color:var(--dsw-alias-label-primary,#f4f4f5)}',
1386
+ '.tg-node-title-text{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
1387
+ '.tg-node-toggle{flex:none;font-size:9px;line-height:1;padding:2px;border-radius:4px;cursor:pointer;opacity:.55;',
1388
+ ' color:var(--dsw-alias-label-secondary,#adb2b8);transition:opacity .12s ease,background .12s ease}',
1389
+ '.tg-node-toggle:hover{opacity:1;background:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.16))}',
1390
+ '.tg-node-meta{display:flex;align-items:center;gap:6px;font-size:9px;color:var(--dsw-alias-label-secondary,#adb2b8);margin-top:2px;white-space:nowrap;overflow:hidden}',
1391
+ '.tg-node-meta b{color:var(--dsw-alias-label-primary,#e4e4e7);font-weight:600}',
1392
+ '.tg-node-bar{height:3px;border-radius:2px;margin-top:3px;background:var(--dsw-alias-border-l1,rgba(127,127,127,.2));overflow:hidden}',
1393
+ '.tg-node-bar i{display:block;height:100%;background:var(--dsw-alias-brand-primary,#679efe);opacity:.85}',
1394
+ '.tg-node-depth-tag{font-size:9px;padding:0 4px;border-radius:5px;background:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.16));color:var(--dsw-alias-label-secondary,#adb2b8)}',
1395
+ // 图谱边(next / belongTo / has_relation / delegates)
1396
+ '.tg-edge{fill:none;stroke-linecap:round}',
1397
+ '.tg-edge-hit{fill:none;stroke:transparent;stroke-width:14;pointer-events:stroke;cursor:pointer}',
1398
+ '.tg-edge.selected{stroke:#22d3ee;stroke-width:3.4;opacity:1;filter:drop-shadow(0 0 3px rgba(34,211,238,.85))}',
1399
+ '.tg-edge-next{stroke:var(--dsw-alias-label-secondary,#8b8f96);stroke-width:1.6;opacity:.55}',
1400
+ '.tg-edge-belongTo{stroke:var(--dsw-alias-border-l2,rgba(127,127,127,.45));stroke-width:1.1;stroke-dasharray:3 4;opacity:.75}',
1401
+ '.tg-edge-has_relation{stroke:var(--dsw-alias-brand-primary,#679efe);stroke-width:1.8;stroke-dasharray:3 3;opacity:1}',
1402
+ '.tg-edge-delegates{stroke:var(--dsw-alias-state-warn-primary,#eab308);stroke-width:1.4;stroke-dasharray:5 4;opacity:.9}',
1403
+ '.tg-edge-label{font-size:9.5px;fill:var(--dsw-alias-brand-primary,#679efe);text-anchor:start;',
1404
+ ' pointer-events:none;user-select:none;font-weight:600;text-anchor:middle}',
1405
+ '.tg-edge-label-bg{fill:var(--dsw-alias-bg-overlay,var(--dsw-alias-bg-layer-1,#26262a));',
1406
+ ' stroke:var(--dsw-alias-border-l1,rgba(127,127,127,.22));cursor:pointer}',
1407
+ '.tg-edge-label-bg.selected{stroke:#22d3ee;stroke-width:1.5}',
1408
+ '.tg-edge-label.selected{fill:#22d3ee;font-weight:700}',
1409
+ '.tg-arrow{fill:var(--dsw-alias-label-secondary,#8b8f96)}',
1410
+ '.tg-arrow-rel{fill:var(--dsw-alias-brand-primary,#679efe)}',
1411
+ '.tg-arrow-deleg{fill:var(--dsw-alias-state-warn-primary,#eab308)}',
1412
+ '.tg-arrow-sel{fill:#22d3ee}',
1413
+ '.tg-legend{display:flex;gap:7px;align-items:center;flex-wrap:wrap;padding:4px 10px 0;font-size:9px;color:var(--dsw-alias-label-secondary,#adb2b8)}',
1414
+ '.tg-legend i{display:inline-block;width:14px;height:0;border-top:1.5px solid currentColor;margin-right:3px;vertical-align:middle}',
1415
+ '.tg-legend .n i{color:var(--dsw-alias-label-secondary,#8b8f96)}',
1416
+ '.tg-legend .b i{color:var(--dsw-alias-border-l2,rgba(127,127,127,.55));border-top-style:dashed}',
1417
+ '.tg-legend .r i{color:var(--dsw-alias-brand-primary,#679efe);border-top-style:dotted}',
1418
+ '.tg-legend .d i{color:var(--dsw-alias-state-warn-primary,#eab308);border-top-style:dashed}',
1419
+ // 统计条
1420
+ '.tg-stats{display:flex;flex-wrap:wrap;align-items:center;gap:4px;padding:6px 10px;border-bottom:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.14))}',
1421
+ '.tg-view-toggle{display:inline-flex;gap:2px;padding:3px;border-radius:9px;background:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.08))}',
1422
+ '.tg-view-btn{border:0;background:transparent;cursor:pointer;font-size:12px;font-family:inherit;padding:4px 14px;border-radius:7px;',
1423
+ ' color:var(--dsw-alias-label-secondary,#adb2b8);white-space:nowrap}',
1424
+ '.tg-view-btn:hover{color:var(--dsw-alias-label-primary,#f4f4f5)}',
1425
+ '.tg-view-btn.active{background:var(--dsw-alias-brand-primary,#679efe);color:#fff;font-weight:600}',
1426
+ '.tg-title-session{flex:0 1 auto;min-width:0;max-width:46%;font-size:11px;font-weight:500;color:var(--dsw-alias-label-secondary,#c2c6cc);',
1427
+ ' white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
1428
+ '.tg-stat-chip{display:inline-flex;align-items:baseline;gap:4px;padding:1px 7px;border-radius:8px;font-size:9.5px;',
1429
+ ' color:var(--dsw-alias-label-secondary,#adb2b8);background:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.08));white-space:nowrap}',
1430
+ '.tg-stat-chip b{font-size:11px;font-weight:700;color:var(--dsw-alias-label-primary,#f4f4f5)}',
1431
+ '.tg-topic-detail-btn{width:22px;min-width:22px;height:22px;font-size:12px;opacity:.65}',
1432
+ '.tg-topic-detail-btn:hover{opacity:1}',
1433
+ // 话题详情浮层
1434
+ '.tg-detail{position:absolute;inset:0;z-index:30;display:flex;flex-direction:column;',
1435
+ ' background:var(--dsw-alias-bg-layer-1,#26262a);',
1436
+ ' animation:tg-fade .14s ease both}',
1437
+ '@keyframes tg-fade{from{opacity:0;transform:translateY(3px)}to{opacity:1;transform:none}}',
1438
+ '.tg-detail-head{display:flex;align-items:center;gap:8px;padding:9px 11px;',
1439
+ ' border-bottom:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.18))}',
1440
+ '.tg-detail-title{flex:1;min-width:0;font-size:13px;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
1441
+ '.tg-detail-body{padding:9px 11px 12px;overflow-y:auto}',
1442
+ '.tg-detail-body::-webkit-scrollbar{width:8px}',
1443
+ '.tg-detail-body::-webkit-scrollbar-thumb{background:var(--dsw-alias-border-l2,rgba(127,127,127,.35));border-radius:4px}',
1444
+ '.tg-detail-kicker{font-size:10px;color:var(--dsw-alias-label-secondary,#adb2b8);margin-bottom:4px}',
1445
+ '.tg-detail-kv{font-size:9.5px;color:var(--dsw-alias-label-secondary,#adb2b8);margin-top:3px;word-break:break-word}',
1446
+ '.tg-detail-section{margin-top:10px}',
1447
+ '.tg-detail-section-title{font-size:10px;font-weight:600;color:var(--dsw-alias-label-secondary,#adb2b8);margin-bottom:4px}',
1448
+ '.tg-detail-foot{display:flex;gap:6px;justify-content:flex-end;padding:8px 11px;',
1449
+ ' border-top:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.16))}',
1450
+ '.tg-detail-foot .tg-btn{border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.22));font-size:11px;padding:0 10px;height:26px}',
1451
+ '.tg-detail-status{margin-top:8px;font-size:10px;color:var(--dsw-alias-brand-primary,#679efe)}',
1452
+ // 子智能体「分支框 + 内部话题树」(与主图同屏)
1453
+ '.tg-branch-ico{flex:none;font-size:10px}',
1454
+ '.tg-branch-toggle{flex:none;font-size:9px;line-height:1;padding:2px;border-radius:4px;cursor:pointer;opacity:.6;',
1455
+ ' color:var(--dsw-alias-label-secondary,#adb2b8)}',
1456
+ '.tg-branch-toggle:hover{opacity:1;background:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.16))}',
1457
+ '.tg-branch-depth{position:absolute;right:5px;top:4px;font-size:8px;padding:0 3px;border-radius:4px;',
1458
+ ' background:var(--dsw-alias-bg-layer-1,rgba(127,127,127,.16));color:var(--dsw-alias-label-secondary,#adb2b8)}',
1459
+ '.tg-branch-box{position:absolute;border-radius:12px;pointer-events:none;z-index:0;',
1460
+ ' border:1px dashed color-mix(in srgb,var(--dsw-alias-state-warn-primary,#eab308) 38%,transparent);',
1461
+ ' background:color-mix(in srgb,var(--dsw-alias-state-warn-primary,#eab308) 4%,transparent)}',
1462
+ '.tg-branch-box-label{position:absolute;left:8px;top:-9px;max-width:calc(100% - 16px);padding:0 5px;border-radius:6px;',
1463
+ ' font-size:9px;color:var(--dsw-alias-label-secondary,#adb2b8);background:var(--dsw-alias-bg-overlay,#26262a);',
1464
+ ' white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
1465
+ '.tg-branch-header{position:absolute;display:flex;align-items:center;gap:5px;padding:0 8px;border-radius:8px;cursor:pointer;z-index:2;',
1466
+ ' background:color-mix(in srgb,var(--dsw-alias-state-warn-primary,#eab308) 12%,transparent);',
1467
+ ' border:1px dashed color-mix(in srgb,var(--dsw-alias-state-warn-primary,#eab308) 45%,transparent)}',
1468
+ '.tg-branch-header:hover{border-style:solid}',
1469
+ '.tg-branch-header.pending{opacity:.72}',
1470
+ '.tg-branch-title-text{flex:1;min-width:0;font-size:11px;font-weight:600;color:var(--dsw-alias-label-primary,#f4f4f5);',
1471
+ ' white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
1472
+ '.tg-branch-header-meta{flex:none;font-size:9px;color:var(--dsw-alias-label-secondary,#adb2b8);white-space:nowrap}',
1473
+ '.tg-branch-topic{position:absolute;border-radius:8px;padding:5px 8px;cursor:pointer;overflow:hidden;z-index:2;',
1474
+ ' background:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.08));border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.22))}',
1475
+ '.tg-branch-topic:hover{border-color:var(--dsw-alias-brand-primary,#679efe)}',
1476
+ '.tg-branch-topic-title{font-size:10.5px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;',
1477
+ ' color:var(--dsw-alias-label-primary,#f4f4f5)}',
1478
+ '.tg-branch-topic-meta{display:flex;gap:6px;margin-top:2px;font-size:8.5px;color:var(--dsw-alias-label-secondary,#adb2b8);',
1479
+ ' white-space:nowrap;overflow:hidden}',
1480
+ '.tg-subbranch-group{margin-top:3px}',
1481
+ '.tg-subbranch-topics{padding-left:16px}',
1482
+ '.tg-subbranch-topic{display:flex;align-items:center;gap:6px;padding:2px 4px;border-radius:5px;cursor:pointer;',
1483
+ ' font-size:10px;color:var(--dsw-alias-label-primary,#e6e8ea);white-space:nowrap;overflow:hidden}',
1484
+ '.tg-subbranch-topic:hover{background:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.12))}',
1485
+ '.tg-subbranch-topic-title{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
1486
+ '.tg-subbranch-topic.dim{color:var(--dsw-alias-label-secondary,#adb2b8);cursor:default}',
1487
+ // 链式里的关联话题 / 子智能体
1488
+ '.tg-relations{display:flex;flex-wrap:wrap;gap:4px;margin:0 0 7px}',
1489
+ '.tg-rel-chip{display:inline-flex;align-items:center;gap:3px;max-width:100%;padding:1px 7px;border-radius:8px;cursor:pointer;',
1490
+ ' font-size:9.5px;border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.22));',
1491
+ ' background:color-mix(in srgb,var(--dsw-alias-brand-primary,#679efe) 8%,transparent);',
1492
+ ' color:var(--dsw-alias-brand-primary,#679efe);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
1493
+ '.tg-rel-chip:hover{background:color-mix(in srgb,var(--dsw-alias-brand-primary,#679efe) 16%,transparent)}',
1494
+ '.tg-rel-chip b{color:var(--dsw-alias-label-secondary,#adb2b8);font-weight:500}',
1495
+ '.tg-subbranches{margin:2px 0 7px;border-radius:9px;border:1px dashed color-mix(in srgb,var(--dsw-alias-state-warn-primary,#eab308) 40%,transparent);',
1496
+ ' background:color-mix(in srgb,var(--dsw-alias-state-warn-primary,#eab308) 5%,transparent);padding:5px 7px}',
1497
+ '.tg-subbranches-head{font-size:9.5px;font-weight:600;color:var(--dsw-alias-state-warn-primary,#ca8a04);margin-bottom:3px;display:flex;gap:5px;align-items:center}',
1498
+ '.tg-subbranch{display:flex;align-items:center;gap:6px;padding:3px 4px;border-radius:6px;cursor:pointer;font-size:10.5px;',
1499
+ ' color:var(--dsw-alias-label-primary,#e6e8ea);white-space:nowrap;overflow:hidden}',
1500
+ '.tg-subbranch:hover{background:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.12))}',
1501
+ '.tg-subbranch-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis}',
1502
+ '.tg-subbranch-meta{flex:none;font-size:9px;color:var(--dsw-alias-label-secondary,#adb2b8)}',
1503
+ '.tg-back-parent{flex:none;border:0;background:transparent;cursor:pointer;color:var(--dsw-alias-brand-primary,#679efe);',
1504
+ ' font-size:10.5px;padding:2px 5px;border-radius:6px}',
1505
+ '.tg-back-parent:hover{background:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.12))}',
1506
+ // tooltip
1507
+ '.tg-tip{position:fixed;z-index:10060;max-width:330px;pointer-events:none;border-radius:11px;padding:9px 11px;font-size:11px;line-height:1.55;',
1508
+ ' background:var(--dsw-alias-bg-overlay,#2c2c2e);color:var(--dsw-alias-label-primary,#f4f4f5);',
1509
+ ' border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.32));box-shadow:0 14px 40px rgba(0,0,0,.45);',
1510
+ ' font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Hiragino Sans GB","Microsoft YaHei","Helvetica Neue",Arial,sans-serif}',
1511
+ '.tg-tip-title{font-weight:700;font-size:12.5px;margin-bottom:2px}',
1512
+ '.tg-tip-kind{font-size:9.5px;color:var(--dsw-alias-brand-primary,#679efe);margin-bottom:4px}',
1513
+ '.tg-tip-summary{color:var(--dsw-alias-label-secondary,#adb2b8);margin-bottom:6px;white-space:pre-wrap;word-break:break-word}',
1514
+ '.tg-tip-grid{display:grid;grid-template-columns:auto 1fr;gap:2px 10px}',
1515
+ '.tg-tip-grid .k{color:var(--dsw-alias-label-secondary,#adb2b8);white-space:nowrap}',
1516
+ '.tg-tip-grid .v{font-weight:600;text-align:right}',
1517
+ '.tg-tip-list{margin-top:5px;color:var(--dsw-alias-label-secondary,#adb2b8);word-break:break-word}',
1518
+ // footer
1519
+ '.tg-foot{display:flex;align-items:center;gap:6px;padding:7px 10px;border-top:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.16));',
1520
+ ' background:var(--dsw-alias-bg-layer-1,rgba(0,0,0,.06))}',
1521
+ '.tg-foot-status{flex:1;min-width:0;font-size:10px;color:var(--dsw-alias-label-secondary,#adb2b8);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
1522
+ '.tg-foot-status.error{color:var(--dsw-alias-state-error-primary,#ef4444)}',
1523
+ '.tg-foot-status.running{color:var(--dsw-alias-state-warn-primary,#eab308)}',
1524
+ '.tg-foot .tg-btn{font-size:10.5px;padding:0 8px;height:24px;border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.22))}',
1525
+ '.tg-foot .tg-btn.primary{border-color:color-mix(in srgb,var(--dsw-alias-brand-primary,#679efe) 40%,transparent)}',
1526
+ '.tg-progress{flex:none;width:64px;height:4px;border-radius:2px;overflow:hidden;background:var(--dsw-alias-border-l1,rgba(127,127,127,.18))}',
1527
+ '.tg-progress i{display:block;height:100%;background:var(--dsw-alias-brand-primary,#679efe);transition:width .3s ease}',
1528
+ // collapsed pill
1529
+ '.tg-pill{position:fixed;z-index:9998;pointer-events:auto;display:flex;align-items:center;gap:7px;padding:7px 11px;border-radius:999px;',
1530
+ ' cursor:grab;user-select:none;font-size:12px;font-weight:600;',
1531
+ ' background:var(--dsw-alias-bg-overlay,#26262a);color:var(--dsw-alias-label-primary,#f4f4f5);',
1532
+ ' border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.25));box-shadow:0 10px 28px rgba(0,0,0,.35)}',
1533
+ '.tg-pill:hover{border-color:var(--dsw-alias-brand-primary,#679efe)}',
1534
+ '.tg-pill:active{cursor:grabbing}',
1535
+ '.tg-pill .tg-dot.running{animation:tg-pulse 1.1s ease-in-out infinite}',
1536
+ '.tg-version{flex:none;font-size:9px;color:var(--dsw-alias-label-secondary,#adb2b8);opacity:.55;margin-right:2px}',
1537
+ '.tg-resize{position:absolute;right:0;bottom:0;width:16px;height:16px;cursor:nwse-resize;z-index:12}',
1538
+ '.tg-resize::after{content:"";position:absolute;right:4px;bottom:4px;width:8px;height:8px;',
1539
+ ' border-right:2px solid var(--dsw-alias-border-l2,rgba(127,127,127,.5));border-bottom:2px solid var(--dsw-alias-border-l2,rgba(127,127,127,.5));border-radius:0 0 2px 0}',
1540
+ ].join('\n');
1541
+
1542
+ // ── 悬浮窗组件 ──────────────────────────────────────────────────────────
1543
+
1544
+ function TopicGraphPanel(props) {
1545
+ useLocaleTick();
1546
+ var propsSessionId = props && typeof props.sessionId === 'string' ? props.sessionId : '';
1547
+ var uiState = useState(loadUi);
1548
+ var ui = uiState[0];
1549
+ var setUi = uiState[1];
1550
+ var sessionsState = useState([]);
1551
+ var sessions = sessionsState[0];
1552
+ var setSessions = sessionsState[1];
1553
+ var graphState = useState(null);
1554
+ var graph = graphState[0];
1555
+ var setGraph = graphState[1];
1556
+ var errorState = useState('');
1557
+ var error = errorState[0];
1558
+ var setError = errorState[1];
1559
+ var expandedStagesState = useState({});
1560
+ var expandedStages = expandedStagesState[0];
1561
+ var setExpandedStages = expandedStagesState[1];
1562
+ var expandedTopicsState = useState({});
1563
+ var expandedTopics = expandedTopicsState[0];
1564
+ var setExpandedTopics = expandedTopicsState[1];
1565
+ var expandedTurnsState = useState({});
1566
+ var expandedTurns = expandedTurnsState[0];
1567
+ var setExpandedTurns = expandedTurnsState[1];
1568
+ var hoverState = useState(null);
1569
+ var hover = hoverState[0];
1570
+ var setHover = hoverState[1];
1571
+ var reloadState = useState(0);
1572
+ var reloadKey = reloadState[0];
1573
+ var setReloadKey = reloadState[1];
1574
+ var scaleState = useState(function () {
1575
+ return (typeof ui.scale === 'number' && ui.scale >= 0.35 && ui.scale <= 2.5) ? ui.scale : 1;
1576
+ });
1577
+ var scale = scaleState[0];
1578
+ var setScale = scaleState[1];
1579
+ var actionState = useState('');
1580
+ var actionBusy = actionState[0];
1581
+ var setActionBusy = actionState[1];
1582
+ var detailState = useState(null);
1583
+ var detailTopicId = detailState[0];
1584
+ var setDetailTopicId = detailState[1];
1585
+ var detailSessionState = useState(null);
1586
+ var detailSessionId = detailSessionState[0];
1587
+ var setDetailSessionId = detailSessionState[1];
1588
+ var jumpState = useState(null);
1589
+ var jumpStatus = jumpState[0];
1590
+ var setJumpStatus = jumpState[1];
1591
+ var graphCollapsedState = useState(function () {
1592
+ return ui.graphCollapsed || { stages: {}, topics: {}, branches: {} };
1593
+ });
1594
+ var graphCollapsed = graphCollapsedState[0];
1595
+ var setGraphCollapsed = graphCollapsedState[1];
1596
+ var selectedEdgeState = useState(null);
1597
+ var selectedEdgeId = selectedEdgeState[0];
1598
+ var setSelectedEdgeId = selectedEdgeState[1];
1599
+ var localTurnAnalysesState = useState({});
1600
+ var localTurnAnalyses = localTurnAnalysesState[0];
1601
+ var setLocalTurnAnalyses = localTurnAnalysesState[1];
1602
+ var selectedStageState = useState(null);
1603
+ var selectedStageId = selectedStageState[0];
1604
+ var setSelectedStageId = selectedStageState[1];
1605
+ var selectedTopicState = useState(null);
1606
+ var selectedTopicId = selectedTopicState[0];
1607
+ var setSelectedTopicId = selectedTopicState[1];
1608
+ var wfMiniHoverState = useState(null);
1609
+ var wfMiniHover = wfMiniHoverState[0];
1610
+ var setWfMiniHover = wfMiniHoverState[1];
1611
+ var graphModeState = useState('timeline');
1612
+ var graphMode = graphModeState[0];
1613
+ var setGraphMode = graphModeState[1];
1614
+ var tlHoverState = useState(null);
1615
+ var tlHover = tlHoverState[0];
1616
+ var setTlHover = tlHoverState[1];
1617
+ var tlBoxState = useState(0);
1618
+ var tlBoxW = tlBoxState[0];
1619
+ var setTlBoxW = tlBoxState[1];
1620
+ var tlSelRelState = useState(null);
1621
+ var tlSelRel = tlSelRelState[0];
1622
+ var setTlSelRel = tlSelRelState[1];
1623
+ var structWState = useState(0);
1624
+ var structW = structWState[0];
1625
+ var setStructW = structWState[1];
1626
+ var modelProvidersState = useState([]);
1627
+ var modelProviders = modelProvidersState[0];
1628
+ var setModelProviders = modelProvidersState[1];
1629
+ var modelSelState = useState('');
1630
+ var modelSel = modelSelState[0];
1631
+ var setModelSel = modelSelState[1];
1632
+
1633
+ var uiRef = useRef(ui);
1634
+ var sessionsRef = useRef({ sessions: [], current: null });
1635
+ var failCountRef = useRef(0);
1636
+ var scaleRef = useRef(scale);
1637
+ var versionRef = useRef(null);
1638
+ var graphSessionRef = useRef(null);
1639
+ var viewportRef = useRef(null);
1640
+ var wfDetailRef = useRef(null);
1641
+ var tlScrollRef = useRef(null);
1642
+ var panelRef = useRef(null);
1643
+
1644
+ uiRef.current = ui;
1645
+ scaleRef.current = scale;
1646
+
1647
+ // UI 状态持久化(拖动时防抖,避免每帧写 localStorage)
1648
+ useEffect(function () {
1649
+ var timer = setTimeout(function () {
1650
+ saveUi(Object.assign({}, ui, { graphCollapsed: graphCollapsed, scale: scale }));
1651
+ }, 200);
1652
+ return function () { clearTimeout(timer); };
1653
+ }, [ui, graphCollapsed, scale]);
1654
+
1655
+ // 切换会话时关闭话题详情浮层,并恢复图谱默认展开
1656
+ useEffect(function () {
1657
+ setDetailSessionId(null);
1658
+ setDetailTopicId(null);
1659
+ setJumpStatus(null);
1660
+ setGraphCollapsed({ stages: {}, topics: {}, branches: {} });
1661
+ setSelectedEdgeId(null);
1662
+ setSelectedTopicId(null);
1663
+ setSelectedStageId(null);
1664
+ }, [graph && graph.sessionId]);
1665
+
1666
+ // 载入可选模型(切分/关系/单轮分析共用)
1667
+ useEffect(function () {
1668
+ loadModels();
1669
+ }, []);
1670
+
1671
+ // 默认展开第一个阶段 / 第一层话题
1672
+ useEffect(function () {
1673
+ if (!graph) return;
1674
+ setExpandedStages(function (prev) {
1675
+ if (Object.keys(prev).length) return prev;
1676
+ var next = {};
1677
+ (graph.stages || []).forEach(function (stage, index) { next[stage.id] = index === 0; });
1678
+ return next;
1679
+ });
1680
+ }, [graph]);
1681
+
1682
+ // dsh 的 conversation view 在会话激活时会按内容撑高,导致整页一起滚。
1683
+ // 这里测量最近的可滚动祖先的可见高度,把面板锁成固定高度,让内部左右两栏独立滚动。
1684
+ useEffect(function () {
1685
+ var el = panelRef.current;
1686
+ if (!el) return undefined;
1687
+ var target = null;
1688
+ var node = el.parentElement;
1689
+ var guard = 0;
1690
+ while (node && guard < 12) {
1691
+ var cs = null;
1692
+ try { cs = window.getComputedStyle(node); } catch (error) { cs = null; }
1693
+ if (cs && (cs.overflowY === 'auto' || cs.overflowY === 'scroll')) { target = node; break; }
1694
+ node = node.parentElement;
1695
+ guard += 1;
1696
+ }
1697
+ function apply() {
1698
+ if (!el) return;
1699
+ var h = target ? target.clientHeight : 0;
1700
+ if (h > 0) {
1701
+ el.style.flex = '0 0 auto';
1702
+ el.style.height = h + 'px';
1703
+ el.style.maxHeight = h + 'px';
1704
+ el.style.minHeight = '0';
1705
+ }
1706
+ }
1707
+ apply();
1708
+ var ro = null;
1709
+ try {
1710
+ if (typeof ResizeObserver !== 'undefined' && target) {
1711
+ ro = new ResizeObserver(function () { apply(); });
1712
+ ro.observe(target);
1713
+ }
1714
+ } catch (error) { /* ResizeObserver 不可用 */ }
1715
+ window.addEventListener('resize', apply);
1716
+ return function () {
1717
+ if (ro) { try { ro.disconnect(); } catch (error) { /* ignore */ } }
1718
+ window.removeEventListener('resize', apply);
1719
+ };
1720
+ }, []);
1721
+
1722
+ // 时间线:测量可视宽度,尽量一页显示
1723
+ useEffect(function () {
1724
+ var el = tlScrollRef.current;
1725
+ if (!el) return undefined;
1726
+ function measure() { setTlBoxW(el.clientWidth || 0); }
1727
+ measure();
1728
+ var ro = typeof ResizeObserver === 'function' ? new ResizeObserver(measure) : null;
1729
+ if (ro) { try { ro.observe(el); } catch (error) { /* ignore */ } }
1730
+ window.addEventListener('resize', measure);
1731
+ return function () {
1732
+ if (ro) { try { ro.disconnect(); } catch (error) { /* ignore */ } }
1733
+ window.removeEventListener('resize', measure);
1734
+ };
1735
+ }, [ui.tab, graphMode]);
1736
+
1737
+ // 结构图:测量可视宽度,让画布铺满视口、不横向裁切
1738
+ useEffect(function () {
1739
+ var el = viewportRef.current;
1740
+ if (!el) return undefined;
1741
+ function measure() { setStructW(el.clientWidth || 0); }
1742
+ measure();
1743
+ var ro = typeof ResizeObserver === 'function' ? new ResizeObserver(measure) : null;
1744
+ if (ro) { try { ro.observe(el); } catch (error) { /* ignore */ } }
1745
+ window.addEventListener('resize', measure);
1746
+ return function () {
1747
+ if (ro) { try { ro.disconnect(); } catch (error) { /* ignore */ } }
1748
+ window.removeEventListener('resize', measure);
1749
+ };
1750
+ }, [ui.tab, graphMode]);
1751
+
1752
+ // 切换阶段/话题时,右栏滚回顶部
1753
+ useEffect(function () {
1754
+ var el = wfDetailRef.current;
1755
+ if (el) { try { el.scrollTop = 0; } catch (error) { /* ignore */ } }
1756
+ }, [selectedStageId, selectedTopicId]);
1757
+
1758
+ // 工作流右栏默认显示第一个阶段,并展开其第一个话题
1759
+ useEffect(function () {
1760
+ if (!graph) return;
1761
+ var stages = graph.stages || [];
1762
+ if (!stages.length) return;
1763
+ var topics = graph.topics || [];
1764
+ setSelectedStageId(function (prev) { return prev || stages[0].id; });
1765
+ var firstTopic = topics.filter(function (t) { return !t.parentId && t.stageId === stages[0].id; })[0];
1766
+ if (firstTopic) setSelectedTopicId(function (prev) { return prev || firstTopic.id; });
1767
+ setExpandedTopics(function (prev) {
1768
+ if (Object.keys(prev).length) return prev;
1769
+ if (!firstTopic) return prev;
1770
+ var next = {}; next[firstTopic.id] = true; return next;
1771
+ });
1772
+ }, [graph, graph && graph.sessionId]);
1773
+
1774
+ // 轮询:会话列表 5 拍一次,图谱每拍一次(版本未变时 Host 只回 unchanged)
1775
+ useEffect(function () {
1776
+ var cancelled = false;
1777
+ var timer = null;
1778
+ var tick = 0;
1779
+
1780
+ async function refreshSessions() {
1781
+ var payload = await fetchJson(ROUTE + '/sessions');
1782
+ if (cancelled || !payload || !Array.isArray(payload.sessions)) return;
1783
+ sessionsRef.current = { sessions: payload.sessions, current: payload.current };
1784
+ setSessions(payload.sessions);
1785
+ var current = payload.current;
1786
+ if (current && graphSessionRef.current && !sameSession(current, graphSessionRef.current)) {
1787
+ versionRef.current = null;
1788
+ }
1789
+ }
1790
+
1791
+ async function refreshGraph() {
1792
+ var sidebarSession = propsSessionId || readSidebarCurrentSession();
1793
+ var target = sidebarSession || (sessionsRef.current && sessionsRef.current.current);
1794
+ if (!target) return;
1795
+ var url = ROUTE + '/graph?sessionId=' + encodeURIComponent(target);
1796
+ if (versionRef.current != null && graphSessionRef.current && sameSession(graphSessionRef.current, target)) {
1797
+ url += '&version=' + versionRef.current;
1798
+ }
1799
+ var payload = await fetchJson(url);
1800
+ if (cancelled) return;
1801
+ if (!payload) {
1802
+ failCountRef.current += 1;
1803
+ if (failCountRef.current >= 4) {
1804
+ setError(t('aiFailed') + ':' + ROUTE + ' 无响应');
1805
+ }
1806
+ return;
1807
+ }
1808
+ failCountRef.current = 0;
1809
+ if (payload.unchanged) return;
1810
+ if (payload.ok) {
1811
+ versionRef.current = payload.version;
1812
+ graphSessionRef.current = payload.sessionId || target;
1813
+ setGraph(payload);
1814
+ setError('');
1815
+ } else if (payload.empty) {
1816
+ setGraph(null);
1817
+ }
1818
+ }
1819
+
1820
+ async function loop() {
1821
+ tick += 1;
1822
+ try {
1823
+ if (tick === 1 || tick % 5 === 0) await refreshSessions();
1824
+ await refreshGraph();
1825
+ } catch (loopError) {
1826
+ setError(String(loopError && loopError.message || loopError));
1827
+ }
1828
+ if (!cancelled) timer = setTimeout(loop, 2500);
1829
+ }
1830
+ loop();
1831
+ return function () { cancelled = true; clearTimeout(timer); };
1832
+ }, [reloadKey, propsSessionId]);
1833
+
1834
+ // Ctrl+滚轮缩放(原生监听,需要 non-passive 才能 preventDefault)
1835
+ useEffect(function () {
1836
+ var element = viewportRef.current;
1837
+ if (!element || ui.tab !== 'graph') return undefined;
1838
+ function onWheel(event) {
1839
+ if (!event.ctrlKey) return; // 普通滚轮交给容器原生滚动
1840
+ event.preventDefault();
1841
+ var rect = element.getBoundingClientRect();
1842
+ var pointerX = event.clientX - rect.left;
1843
+ var pointerY = event.clientY - rect.top;
1844
+ var oldScale = scaleRef.current;
1845
+ var factor = Math.exp(-event.deltaY * 0.0016);
1846
+ var nextScale = clamp(oldScale * factor, 0.35, 2.5);
1847
+ if (Math.abs(nextScale - oldScale) < 0.0005) return;
1848
+ var contentX = (element.scrollLeft + pointerX) / oldScale;
1849
+ var contentY = (element.scrollTop + pointerY) / oldScale;
1850
+ setScale(nextScale);
1851
+ requestAnimationFrame(function () {
1852
+ element.scrollLeft = contentX * nextScale - pointerX;
1853
+ element.scrollTop = contentY * nextScale - pointerY;
1854
+ });
1855
+ }
1856
+ element.addEventListener('wheel', onWheel, { passive: false });
1857
+ return function () { element.removeEventListener('wheel', onWheel); };
1858
+ }, [ui.tab]);
1859
+
1860
+ // ── 操作 ────────────────────────────────────────────────────────────
1861
+ function toggleStage(stageId) {
1862
+ setExpandedStages(function (prev) {
1863
+ var next = Object.assign({}, prev);
1864
+ next[stageId] = !next[stageId];
1865
+ return next;
1866
+ });
1867
+ }
1868
+
1869
+ function setAllStages(open) {
1870
+ setExpandedStages(function () {
1871
+ var next = {};
1872
+ (graph && graph.stages ? graph.stages : []).forEach(function (stage) { next[stage.id] = open; });
1873
+ return next;
1874
+ });
1875
+ }
1876
+
1877
+ function toggleTopic(topicId) {
1878
+ setExpandedTopics(function (prev) {
1879
+ var next = Object.assign({}, prev);
1880
+ next[topicId] = !next[topicId];
1881
+ return next;
1882
+ });
1883
+ }
1884
+
1885
+ function toggleTurn(turnNo) {
1886
+ setExpandedTurns(function (prev) {
1887
+ var next = Object.assign({}, prev);
1888
+ next[turnNo] = !next[turnNo];
1889
+ return next;
1890
+ });
1891
+ }
1892
+
1893
+ function selectSession(sessionId) {
1894
+ versionRef.current = null;
1895
+ graphSessionRef.current = null;
1896
+ setGraph(null);
1897
+ // 在面板里选会话 = 同时切换 dsh 主对话,避免面板与左侧不一致
1898
+ if (sessionId) {
1899
+ try {
1900
+ var navigation = appCtx && typeof appCtx.get === 'function' ? appCtx.get('uiWorkspace') : null;
1901
+ if (navigation && typeof navigation.openSession === 'function') navigation.openSession(sessionId);
1902
+ } catch (error) { /* 导航不可用时只切换面板 */ }
1903
+ }
1904
+ }
1905
+
1906
+ async function triggerSegment(full) {
1907
+ var target = (graph && graph.sessionId) || (sessionsRef.current && sessionsRef.current.current);
1908
+ if (!target || actionBusy) return;
1909
+ setActionBusy(full ? 'full' : 'incremental');
1910
+ var result = await postJson(ROUTE + '/segment', { sessionId: target, full: full === true });
1911
+ setActionBusy('');
1912
+ if (!result || !result.ok) {
1913
+ setError(t('aiFailed'));
1914
+ return;
1915
+ }
1916
+ versionRef.current = null;
1917
+ setReloadKey(function (n) { return n + 1; });
1918
+ }
1919
+
1920
+ // ── 派生数据 ────────────────────────────────────────────────────────
1921
+ var layout = useMemo(function () {
1922
+ var opts = Object.assign({}, graphCollapsed);
1923
+ opts.width = structW || 0;
1924
+ return buildLayout(graph || { topics: [], stages: [] }, opts);
1925
+ }, [graph, graphCollapsed, structW]);
1926
+ var byId = useMemo(function () {
1927
+ var map = {};
1928
+ var list = graph && graph.topics ? graph.topics : [];
1929
+ list.forEach(function (topic) { map[topic.id] = topic; });
1930
+ return map;
1931
+ }, [graph]);
1932
+ var turnByNo = useMemo(function () {
1933
+ var map = {};
1934
+ var list = graph && graph.turns ? graph.turns : [];
1935
+ list.forEach(function (turn) { map[turn.turn] = turn; });
1936
+ return map;
1937
+ }, [graph]);
1938
+ var childMap = useMemo(function () {
1939
+ var map = {};
1940
+ var list = graph && graph.topics ? graph.topics : [];
1941
+ list.forEach(function (topic) {
1942
+ if (!topic.parentId) return;
1943
+ if (!map[topic.parentId]) map[topic.parentId] = [];
1944
+ map[topic.parentId].push(topic);
1945
+ });
1946
+ return map;
1947
+ }, [graph]);
1948
+ var branchBySession = useMemo(function () {
1949
+ var map = {};
1950
+ var list = graph && graph.branches ? graph.branches : [];
1951
+ list.forEach(function (branch) { map[branch.sessionId] = branch; });
1952
+ return map;
1953
+ }, [graph]);
1954
+ var branchesByTopic = useMemo(function () {
1955
+ var map = {};
1956
+ var list = graph && graph.branches ? graph.branches : [];
1957
+ list.forEach(function (branch) {
1958
+ if (!branch.parentTopicId) return;
1959
+ if (!map[branch.parentTopicId]) map[branch.parentTopicId] = [];
1960
+ map[branch.parentTopicId].push(branch);
1961
+ });
1962
+ return map;
1963
+ }, [graph]);
1964
+ var relationsByTopic = useMemo(function () {
1965
+ var map = {};
1966
+ var list = graph && graph.relations ? graph.relations : [];
1967
+ list.forEach(function (rel) {
1968
+ if (!map[rel.from]) map[rel.from] = [];
1969
+ map[rel.from].push({ direction: 'out', other: rel.to, label: rel.label });
1970
+ if (!map[rel.to]) map[rel.to] = [];
1971
+ map[rel.to].push({ direction: 'in', other: rel.from, label: rel.label });
1972
+ });
1973
+ return map;
1974
+ }, [graph]);
1975
+
1976
+ function topicTitleOf(id) {
1977
+ return byId[id] ? byId[id].title : id;
1978
+ }
1979
+
1980
+ function topicBranches(topicId) {
1981
+ return branchesByTopic[topicId] || [];
1982
+ }
1983
+
1984
+ function openSession(sessionId) {
1985
+ if (!sessionId) return;
1986
+ selectSession(sessionId);
1987
+ }
1988
+
1989
+ // ── 渲染:轮次行 ────────────────────────────────────────────────────
1990
+ function renderTurn(turnNo) {
1991
+ var turn = turnByNo[turnNo];
1992
+ if (!turn) return null;
1993
+ var open = expandedTurns[turnNo];
1994
+ var running = turn.status === 'open';
1995
+ var statusLabel = running ? t('statusOpen') : (turn.status === 'aborted' ? t('statusAborted') : '');
1996
+ return h('div', {
1997
+ key: 'turn-' + turnNo,
1998
+ className: 'tg-turn' + (running ? ' running' : ''),
1999
+ },
2000
+ h('div', { className: 'tg-turn-row' },
2001
+ h('span', {
2002
+ className: 'tg-turn-toggle',
2003
+ onClick: function (event) { event.stopPropagation(); toggleTurn(turnNo); },
2004
+ title: open ? t('collapse') : t('expand'),
2005
+ },
2006
+ h('span', { className: 'tg-turn-no' }, t('turnLabel', { n: turnNo })),
2007
+ h('span', { className: 'tg-turn-prompt' }, turn.prompt || t('emptyPrompt')),
2008
+ ),
2009
+ statusLabel ? h('span', { className: 'tg-badge active' }, statusLabel) : null,
2010
+ h('span', { className: 'tg-turn-meta' },
2011
+ h('span', null, h('b', null, String(turn.steps || 0)), ' ' + t('totalSteps')),
2012
+ h('span', null, h('b', null, fmtK(turn.tokens.total)), ' ' + t('tokens')),
2013
+ h('span', null, t('toolCalls') + ' ', h('b', null, String(turn.toolCalls.total))),
2014
+ h('span', null, h('b', null, fmtDuration(turn.wallMs))),
2015
+ ),
2016
+ ),
2017
+ open ? h('div', { className: 'tg-turn-detail' },
2018
+ h('div', { className: 'kv' }, h('span', null, t('tokens')), h('span', null, fmtK(turn.tokens.total) + ' · ' + fmtPct(turn.tokenRatio))),
2019
+ h('div', { className: 'kv' }, h('span', null, t('agentTime')), h('span', null, fmtDuration(turn.wallMs) + ' · ' + fmtPct(turn.wallRatio))),
2020
+ h('div', { className: 'kv' }, h('span', null, t('stepTime')), h('span', null, fmtDuration(turn.agentMs))),
2021
+ h('div', { className: 'kv' }, h('span', null, t('toolCalls')), h('span', null, String(turn.toolCalls.total) + (turn.toolErrors ? ' (' + turn.toolErrors + ' err)' : ''))),
2022
+ h('div', { className: 'kv' }, h('span', null, t('requestCount')), h('span', null, String(turn.requests))),
2023
+ h('div', { className: 'kv' },
2024
+ h('span', null, t('totalSteps')),
2025
+ h('span', null,
2026
+ h('b', null, String(turn.steps || 0)), ' ',
2027
+ h('span', {
2028
+ className: 'tg-jump-link',
2029
+ title: t('jumpToTurn'),
2030
+ onClick: function (event) { event.stopPropagation(); void jumpToTurn(turnNo); },
2031
+ }, '↗ ' + t('jumpToTurn')),
2032
+ ),
2033
+ ),
2034
+ h('div', { className: 'kv' }, h('span', null, t('input') + '/' + t('output')), h('span', null, fmtK(turn.tokens.input) + ' / ' + fmtK(turn.tokens.output))),
2035
+ h('div', { className: 'kv' }, h('span', null, t('cacheRead')), h('span', null, fmtK(turn.tokens.cacheRead))),
2036
+ turn.model ? h('div', { className: 'kv' }, h('span', null, 'model'), h('span', null, turn.model)) : null,
2037
+ turn.prompt ? h('div', { className: 'full' }, h('b', null, t('prompt')), turn.prompt) : null,
2038
+ turn.assistant ? h('div', { className: 'full' }, h('b', null, t('assistant')), turn.assistant) : null,
2039
+ (function () {
2040
+ var local = localTurnAnalyses[turnNo];
2041
+ var cached = graph.turnAnalyses && graph.turnAnalyses[turnNo];
2042
+ var cachedSteps = cached && Array.isArray(cached.steps) ? cached.steps : null;
2043
+ var status = local && local.status ? local.status : null;
2044
+ var steps = (local && local.status === 'done' && Array.isArray(local.steps) && local.steps.length)
2045
+ ? local.steps
2046
+ : (cachedSteps && cachedSteps.length ? cachedSteps : null);
2047
+ var hasSteps = Boolean(steps && steps.length);
2048
+ var errorText = local && local.status === 'error' ? (local.error || t('analyzeFailed')) : null;
2049
+ var busy = status === 'loading';
2050
+ return h('div', { className: 'tg-turn-analysis' },
2051
+ h('div', { className: 'tg-turn-analysis-head' },
2052
+ h('span', null, t('howtoTitle')),
2053
+ busy ? h('span', { className: 'tg-badge active' }, t('analyzing')) : null,
2054
+ ),
2055
+ busy ? null : (hasSteps
2056
+ ? h(React.Fragment, null,
2057
+ h('ol', { className: 'tg-howto-steps' }, steps.map(function (step, i) {
2058
+ return h('li', { key: 'howto-' + i },
2059
+ h('div', { className: 'tg-howto-title' },
2060
+ (i + 1) + '. ' + (step.title || '步骤'),
2061
+ step.stepCount ? h('span', { className: 'tg-howto-stepcount' }, String(step.stepCount) + ' 步') : null,
2062
+ ),
2063
+ step.detail ? h('div', { className: 'tg-howto-detail' }, step.detail) : null,
2064
+ step.tools && step.tools.length ? h('div', { className: 'tg-howto-tools' }, t('stepTools') + ':' + step.tools.join('、')) : null,
2065
+ step.outcome ? h('div', { className: 'tg-howto-outcome' }, '→ ' + step.outcome) : null,
2066
+ );
2067
+ })),
2068
+ h('div', { className: 'tg-howto-foot' },
2069
+ h('span', null, t('totalSteps') + ' ' + String(turn.steps || 0)),
2070
+ h('span', {
2071
+ className: 'tg-jump-link',
2072
+ onClick: function (event) { event.stopPropagation(); void jumpToTurn(turnNo); },
2073
+ }, '↗ ' + t('jumpToTurn')),
2074
+ ),
2075
+ )
2076
+ : (errorText
2077
+ ? h('div', { className: 'tg-howto-error' }, errorText)
2078
+ : h('div', { className: 'tg-howto-empty' }, t('howtoEmpty')))),
2079
+ busy ? null : h('button', {
2080
+ className: 'tg-btn tg-analyze-btn',
2081
+ onClick: function (event) { event.stopPropagation(); analyzeTurn(turnNo, hasSteps); },
2082
+ }, hasSteps ? t('reanalyzeTurn') : t('analyzeTurn')),
2083
+ );
2084
+ })(),
2085
+ ) : null,
2086
+ );
2087
+ }
2088
+
2089
+ // ── 渲染:话题(含子话题) ─────────────────────────────────────────
2090
+ function renderTopic(topic, depth) {
2091
+ if (!topic) return null;
2092
+ var open = expandedTopics[topic.id];
2093
+ var stats = topic.stats || {};
2094
+ var tokens = stats.tokens || {};
2095
+ var kids = childMap[topic.id] || [];
2096
+ var range = topic.turnStart != null ? t('range') + ' ' + topic.turnStart + '-' + topic.turnEnd : '';
2097
+ return h('div', { key: topic.id, className: 'tg-topic', style: { marginLeft: depth ? depth * 10 : 0 } },
2098
+ h('div', { className: 'tg-topic-head', onClick: function () { toggleTopic(topic.id); } },
2099
+ h('span', { className: 'tg-chev' + (open ? ' open' : '') }, '▶'),
2100
+ h('div', { className: 'tg-topic-main' },
2101
+ h('div', { className: 'tg-topic-title' }, (depth ? '· ' : '') + topic.title),
2102
+ h('div', { className: 'tg-topic-sub' },
2103
+ h('span', null, range),
2104
+ h('span', { className: 'tg-badge' + (topic.status === 'active' ? ' active' : '') },
2105
+ topic.status === 'active' ? t('statusActive') : t('statusDone')),
2106
+ h('span', { className: 'tg-badge' + (topic.source === 'llm' ? ' llm' : '') },
2107
+ topic.source === 'llm' ? t('sourceLlm') : t('sourceRule')),
2108
+ kids.length ? h('span', null, kids.length + ' ' + t('subtopics')) : null,
2109
+ kids.length ? h('span', { className: 'tg-badge llm', title: t('statIncludesSubtopic') }, t('statIncludesSubtopic')) : null,
2110
+ topicBranches(topic.id).length
2111
+ ? h('span', { className: 'tg-badge', style: { color: 'var(--dsw-alias-state-warn-primary,#ca8a04)' } },
2112
+ '🤖 ' + t('subagentCount', { n: topicBranches(topic.id).length }))
2113
+ : null,
2114
+ ),
2115
+ ),
2116
+ h('div', { className: 'tg-topic-metrics' },
2117
+ h('span', null, h('b', null, fmtK(tokens.total)), ' · ' + fmtPct(stats.tokenRatio)),
2118
+ h('span', null, fmtDuration(stats.wallMs) + ' · ' + fmtPct(stats.wallRatio)),
2119
+ ),
2120
+ h('button', {
2121
+ className: 'tg-btn tg-topic-detail-btn',
2122
+ title: t('detailOpen'),
2123
+ onClick: function (event) { event.stopPropagation(); openDetail(topic.id); },
2124
+ }, 'ⓘ'),
2125
+ ),
2126
+ open ? h('div', { className: 'tg-topic-body' },
2127
+ topic.summary ? h('div', { className: 'tg-topic-summary' }, topic.summary) : null,
2128
+ h('div', { className: 'tg-statgrid' },
2129
+ h('div', { className: 'tg-stat' },
2130
+ h('div', { className: 'tg-stat-label' }, t('tokens') + ' · ' + t('ratioOfTotal') + ' ' + fmtPct(stats.tokenRatio)),
2131
+ h('div', { className: 'tg-stat-value' }, fmtK(tokens.total), h('span', { className: 'ratio' }, t('input') + ' ' + fmtK(tokens.input) + ' / ' + t('output') + ' ' + fmtK(tokens.output))),
2132
+ h('div', { className: 'tg-bar' }, h('i', { style: { width: Math.max(1, (stats.tokenRatio || 0) * 100) + '%' } })),
2133
+ ),
2134
+ h('div', { className: 'tg-stat' },
2135
+ h('div', { className: 'tg-stat-label' }, t('toolCalls') + ' · ' + fmtPct(stats.toolRatio)),
2136
+ h('div', { className: 'tg-stat-value' }, String((stats.toolCalls && stats.toolCalls.total) || 0)),
2137
+ h('div', { className: 'tg-bar tool' }, h('i', { style: { width: Math.max(1, (stats.toolRatio || 0) * 100) + '%' } })),
2138
+ ),
2139
+ h('div', { className: 'tg-stat' },
2140
+ h('div', { className: 'tg-stat-label' }, t('agentTime') + ' · ' + fmtPct(stats.wallRatio)),
2141
+ h('div', { className: 'tg-stat-value' }, fmtDuration(stats.wallMs), h('span', { className: 'ratio' }, t('stepTime') + ' ' + fmtDuration(stats.agentMs))),
2142
+ h('div', { className: 'tg-bar time' }, h('i', { style: { width: Math.max(1, (stats.wallRatio || 0) * 100) + '%' } })),
2143
+ ),
2144
+ h('div', { className: 'tg-stat' },
2145
+ h('div', { className: 'tg-stat-label' }, t('turns')),
2146
+ h('div', { className: 'tg-stat-value' }, String(stats.turnCount || 0)),
2147
+ ),
2148
+ ),
2149
+ (relationsByTopic[topic.id] && relationsByTopic[topic.id].length)
2150
+ ? h('div', { className: 'tg-relations' },
2151
+ h('span', { className: 'tg-badge' }, t('relations')),
2152
+ relationsByTopic[topic.id].map(function (rel, index) {
2153
+ return h('span', {
2154
+ key: 'rel-' + index,
2155
+ className: 'tg-rel-chip',
2156
+ title: topicTitleOf(rel.other),
2157
+ onClick: function (event) {
2158
+ event.stopPropagation();
2159
+ setExpandedTopics(function (prev) {
2160
+ var next = Object.assign({}, prev);
2161
+ next[rel.other] = true;
2162
+ return next;
2163
+ });
2164
+ },
2165
+ },
2166
+ rel.direction === 'out' ? '→ ' : '← ',
2167
+ topicTitleOf(rel.other),
2168
+ h('b', null, ' ' + rel.label),
2169
+ );
2170
+ }),
2171
+ )
2172
+ : null,
2173
+ topicBranches(topic.id).length
2174
+ ? h('div', { className: 'tg-subbranches' },
2175
+ h('div', { className: 'tg-subbranches-head' }, '🤖 ' + t('subagents') + ' (' + topicBranches(topic.id).length + ')'),
2176
+ topicBranches(topic.id).map(function (branch) {
2177
+ var totals = branch.totals || {};
2178
+ var childTopics = (branch.topics || []).slice(0, 6);
2179
+ var more = Math.max(0, (branch.topics || []).length - childTopics.length);
2180
+ return h('div', { key: branch.sessionId, className: 'tg-subbranch-group' },
2181
+ h('div', {
2182
+ className: 'tg-subbranch',
2183
+ title: t('branchOpen'),
2184
+ onClick: function () { selectSession(branch.sessionId); },
2185
+ },
2186
+ h('span', { className: 'tg-subbranch-name' }, '🤖 ' + (branch.title || sessionShortId(branch.sessionId))),
2187
+ branch.pending ? h('span', { className: 'tg-subbranch-meta' }, t('branchPending')) : null,
2188
+ h('span', { className: 'tg-subbranch-meta' },
2189
+ (totals.turnCount || 0) + ' ' + t('turns') + ' · ' + fmtK((totals.tokens || {}).total || 0)),
2190
+ ),
2191
+ childTopics.length
2192
+ ? h('div', { className: 'tg-subbranch-topics' }, childTopics.map(function (childTopic) {
2193
+ var childStats = childTopic.stats || {};
2194
+ return h('div', {
2195
+ key: childTopic.id,
2196
+ className: 'tg-subbranch-topic',
2197
+ title: t('detailOpen'),
2198
+ onClick: function (event) {
2199
+ event.stopPropagation();
2200
+ openChildDetail(branch.sessionId, childTopic.id);
2201
+ },
2202
+ },
2203
+ h('span', { className: 'tg-subbranch-topic-title' }, childTopic.title),
2204
+ h('span', { className: 'tg-subbranch-meta' }, fmtK((childStats.tokens || {}).total || 0)),
2205
+ );
2206
+ }), more ? h('div', { className: 'tg-subbranch-topic dim' }, '+' + more + ' …') : null)
2207
+ : null,
2208
+ );
2209
+ }),
2210
+ )
2211
+ : null,
2212
+ topic.ownTurns && topic.ownTurns.length
2213
+ ? h('div', null, topic.ownTurns.map(renderTurn))
2214
+ : h('div', { className: 'tg-topic-summary' }, t('noTurns')),
2215
+ kids.length ? h('div', { style: { marginTop: '4px' } }, kids.map(function (kid) { return renderTopic(kid, depth + 1); })) : null,
2216
+ ) : null,
2217
+ );
2218
+ }
2219
+
2220
+ // ── 统计条 / 话题详情 / 跳转 ────────────────────────────────────────
2221
+ function renderStatsStrip() {
2222
+ if (!graph || !graph.totals) return null;
2223
+ var totals = graph.totals || {};
2224
+ var tokens = totals.tokens || {};
2225
+ var toolCalls = totals.toolCalls || { total: 0, byName: {} };
2226
+ var toolEntries = Object.keys(toolCalls.byName || {}).sort(function (a, b) {
2227
+ return toolCalls.byName[b] - toolCalls.byName[a];
2228
+ }).slice(0, 8);
2229
+ var items = [
2230
+ { key: 'turns', label: t('statTurns'), value: String(totals.turnCount || 0) },
2231
+ {
2232
+ key: 'topics', label: t('statTopics'),
2233
+ value: String(totals.topicCount || 0) + (totals.subtopicCount ? ' + ' + totals.subtopicCount : ''),
2234
+ title: (function () {
2235
+ var subTopics = 0;
2236
+ (graph.branches || []).forEach(function (branch) { subTopics += (branch.topics || []).length; });
2237
+ return t('statTopics') + ' ' + String(totals.topicCount || 0) + ' · ' + t('subtopics') + ' ' + String(totals.subtopicCount || 0)
2238
+ + (subTopics ? ' · 🤖 ' + subTopics : '');
2239
+ })(),
2240
+ },
2241
+ { key: 'stages', label: t('statStage'), value: String(totals.stageCount || 0) },
2242
+ {
2243
+ key: 'tokens', label: t('statTokens'), value: fmtK(tokens.total || 0),
2244
+ title: t('input') + ' ' + fmtK(tokens.input) + ' · ' + t('output') + ' ' + fmtK(tokens.output)
2245
+ + ' · ' + t('cacheRead') + ' ' + fmtK(tokens.cacheRead) + ' · ' + t('cacheWrite') + ' ' + fmtK(tokens.cacheWrite),
2246
+ },
2247
+ {
2248
+ key: 'tools', label: t('statTools'), value: String(toolCalls.total || 0),
2249
+ title: toolEntries.map(function (name) { return name + ' × ' + toolCalls.byName[name]; }).join(' · ') || t('toolCalls'),
2250
+ },
2251
+ { key: 'step', label: t('statStepTime'), value: fmtDuration(totals.agentMs || 0), title: t('stepTime') },
2252
+ { key: 'wall', label: t('statWallTime'), value: fmtDuration(totals.wallMs || 0), title: t('agentTime') },
2253
+ { key: 'relations', label: t('statRelations'), value: String(totals.relationCount || 0) },
2254
+ ];
2255
+ var ai = graph.ai || {};
2256
+ var aiTokens = ai.tokens || {};
2257
+ items.push({
2258
+ key: 'aicost', label: t('aiCost'), value: fmtK(aiTokens.total || 0) + ' · ' + (ai.calls || 0) + t('aiCallsUnit'),
2259
+ title: t('aiCost') + ' ' + fmtK(aiTokens.total || 0) + ' tokens · ' + (ai.calls || 0) + ' ' + t('aiCallsUnit') + '(仅切分/关系分析,不含主对话)',
2260
+ });
2261
+ var sub = totals.subagent;
2262
+ if (sub && sub.branchCount) {
2263
+ items.push({
2264
+ key: 'subagent', label: '🤖 ' + t('subagent'), value: fmtK((sub.tokens || {}).total || 0),
2265
+ title: t('statSubagentTokens') + ' ' + fmtK((sub.tokens || {}).total || 0) + ' · ' + t('stepTime') + ' ' + fmtDuration(sub.agentMs || 0),
2266
+ });
2267
+ }
2268
+ return h('div', { className: 'tg-stats' },
2269
+ items.map(function (item) {
2270
+ return h('span', { key: item.key, className: 'tg-stat-chip', title: item.title || '' },
2271
+ h('b', null, item.value),
2272
+ h('span', null, item.label),
2273
+ );
2274
+ }),
2275
+ );
2276
+ }
2277
+
2278
+ /** 解析详情:主图话题用 byId;子智能体话题在 branches[].topics 里。 */
2279
+ function resolveDetail() {
2280
+ if (!detailTopicId) return null;
2281
+ if (!detailSessionId) {
2282
+ var parentTopic = byId[detailTopicId];
2283
+ return parentTopic ? { topic: parentTopic, sessionId: graph.sessionId, branch: null, isChild: false } : null;
2284
+ }
2285
+ var branches = graph.branches || [];
2286
+ for (var i = 0; i < branches.length; i += 1) {
2287
+ var branch = branches[i];
2288
+ if (!sameSession(branch.sessionId, detailSessionId)) continue;
2289
+ var childTopic = (branch.topics || []).filter(function (item) { return item.id === detailTopicId; })[0];
2290
+ if (childTopic) return { topic: childTopic, sessionId: branch.sessionId, branch: branch, isChild: true };
2291
+ }
2292
+ return null;
2293
+ }
2294
+
2295
+ function childTopicTitle(branch, topicId) {
2296
+ if (!branch || !topicId) return topicId;
2297
+ var found = (branch.topics || []).filter(function (item) { return item.id === topicId; })[0];
2298
+ return found ? found.title : topicId;
2299
+ }
2300
+
2301
+ /** 图谱页的折叠箭头:阶段 / 话题 / 子智能体分支,各自独立记忆。 */
2302
+ function toggleGraphCollapse(kind, id) {
2303
+ setGraphCollapsed(function (prev) {
2304
+ var next = {
2305
+ stages: Object.assign({}, prev.stages),
2306
+ topics: Object.assign({}, prev.topics),
2307
+ branches: Object.assign({}, prev.branches),
2308
+ };
2309
+ next[kind][id] = !next[kind][id];
2310
+ return next;
2311
+ });
2312
+ }
2313
+
2314
+ function isCollapsed(kind, id) {
2315
+ return Boolean(graphCollapsed[kind] && graphCollapsed[kind][id]);
2316
+ }
2317
+
2318
+ function loadModels() {
2319
+ fetchJson(ROUTE + '/models').then(function (payload) {
2320
+ if (!payload || !payload.ok) return;
2321
+ setModelProviders(Array.isArray(payload.providers) ? payload.providers : []);
2322
+ if (payload.current && payload.current.provider && payload.current.model) {
2323
+ setModelSel(payload.current.provider + ':' + payload.current.model);
2324
+ }
2325
+ }).catch(function () { /* 模型列表失败不影响主功能 */ });
2326
+ }
2327
+
2328
+ function onModelChange(value) {
2329
+ setModelSel(value || '');
2330
+ if (!value) {
2331
+ // 回退默认(config):清掉覆盖文件
2332
+ postJson(ROUTE + '/model-clear', {}).catch(function () {});
2333
+ return;
2334
+ }
2335
+ var idx = value.indexOf(':');
2336
+ if (idx <= 0) return;
2337
+ var provider = value.slice(0, idx);
2338
+ var model = value.slice(idx + 1);
2339
+ postJson(ROUTE + '/model', { provider: provider, model: model }).catch(function () {});
2340
+ }
2341
+
2342
+ function analyzeTurn(turnNo, force) {
2343
+ if (!graph || !graph.sessionId) return;
2344
+ setLocalTurnAnalyses(function (prev) {
2345
+ var next = Object.assign({}, prev);
2346
+ next[turnNo] = { status: 'loading' };
2347
+ return next;
2348
+ });
2349
+ postJson(ROUTE + '/analyze-turn', { sessionId: graph.sessionId, turn: turnNo, force: force }).then(function (payload) {
2350
+ setLocalTurnAnalyses(function (prev) {
2351
+ var next = Object.assign({}, prev);
2352
+ if (payload && payload.ok && payload.analysis && Array.isArray(payload.analysis.steps)) {
2353
+ next[turnNo] = { status: 'done', steps: payload.analysis.steps };
2354
+ } else {
2355
+ next[turnNo] = { status: 'error', error: (payload && payload.error) || t('analyzeFailed') };
2356
+ }
2357
+ return next;
2358
+ });
2359
+ }).catch(function (err) {
2360
+ setLocalTurnAnalyses(function (prev) {
2361
+ var next = Object.assign({}, prev);
2362
+ next[turnNo] = { status: 'error', error: String((err && err.message) || err) };
2363
+ return next;
2364
+ });
2365
+ });
2366
+ }
2367
+
2368
+ function selectTopicInStage(topic) {
2369
+ if (!topic) return;
2370
+ setDetailTopicId(null);
2371
+ setDetailSessionId(null);
2372
+ setSelectedTopicId(topic.id);
2373
+ setSelectedStageId(topic.stageId || null);
2374
+ setExpandedTopics(function (prev) {
2375
+ var next = Object.assign({}, prev);
2376
+ next[topic.id] = true;
2377
+ return next;
2378
+ });
2379
+ }
2380
+
2381
+ function openDetail(topicId) {
2382
+ if (!topicId || !byId[topicId]) return;
2383
+ setDetailSessionId(null);
2384
+ setDetailTopicId(topicId);
2385
+ setJumpStatus(null);
2386
+ }
2387
+
2388
+ function openChildDetail(sessionId, topicId) {
2389
+ if (!sessionId || !topicId) return;
2390
+ setDetailSessionId(sessionId);
2391
+ setDetailTopicId(topicId);
2392
+ setJumpStatus(null);
2393
+ }
2394
+
2395
+ function closeDetail() {
2396
+ setDetailSessionId(null);
2397
+ setDetailTopicId(null);
2398
+ setJumpStatus(null);
2399
+ }
2400
+
2401
+ async function jumpToTurn(turnNo) {
2402
+ if (turnNo == null) return;
2403
+ var sessionId = (graph && graph.sessionId) || '';
2404
+ try {
2405
+ var navigation = appCtx && typeof appCtx.get === 'function' ? appCtx.get('uiWorkspace') : null;
2406
+ if (navigation && typeof navigation.openSession === 'function') navigation.openSession(sessionId);
2407
+ } catch (error) { /* 导航不可用时就地定位 */ }
2408
+ await revealChatTurn(turnNo);
2409
+ }
2410
+
2411
+ async function jumpToTopicStart(topic, sessionIdOverride) {
2412
+ if (!topic || (topic.turnStart == null && !(Array.isArray(topic.ownTurns) && topic.ownTurns.length))) {
2413
+ setJumpStatus('openedOnly');
2414
+ return;
2415
+ }
2416
+ var sessionId = sessionIdOverride || (graph && graph.sessionId);
2417
+ var firstTurn = Array.isArray(topic.ownTurns) && topic.ownTurns.length ? topic.ownTurns[0] : topic.turnStart;
2418
+ setJumpStatus('locating');
2419
+ try {
2420
+ var navigation = appCtx && typeof appCtx.get === 'function' ? appCtx.get('uiWorkspace') : null;
2421
+ if (navigation && typeof navigation.openSession === 'function') navigation.openSession(sessionId);
2422
+ } catch (error) { /* 导航失败时仍尝试在当前会话里定位 */ }
2423
+ var located = await revealChatTurn(firstTurn);
2424
+ setJumpStatus(located ? 'located' : 'openedOnly');
2425
+ setTimeout(function () { setJumpStatus(null); }, 3500);
2426
+ }
2427
+
2428
+ function renderDetail(inline) {
2429
+ var resolved = resolveDetail();
2430
+ if (!resolved) return null;
2431
+ var topic = resolved.topic;
2432
+ var branch = resolved.branch;
2433
+ var isChild = resolved.isChild;
2434
+ var stats = topic.stats || {};
2435
+ var tokens = stats.tokens || {};
2436
+ var toolCalls = stats.toolCalls || { total: 0, byName: {} };
2437
+ var stagePool = isChild ? (branch.stages || []) : (graph.stages || []);
2438
+ var stage = stagePool.filter(function (item) { return item.id === topic.stageId; })[0] || null;
2439
+ var parent = null;
2440
+ var kids = [];
2441
+ var relations = [];
2442
+ if (isChild) {
2443
+ parent = topic.parentId
2444
+ ? (branch.topics || []).filter(function (item) { return item.id === topic.parentId; })[0] || null
2445
+ : null;
2446
+ kids = (branch.topics || []).filter(function (item) { return item.parentId === topic.id; });
2447
+ relations = (branch.relations || []).filter(function (rel) {
2448
+ return rel.from === topic.id || rel.to === topic.id;
2449
+ }).map(function (rel) {
2450
+ var other = rel.from === topic.id ? rel.to : rel.from;
2451
+ return { direction: rel.from === topic.id ? 'out' : 'in', other: other, label: rel.label, otherTitle: childTopicTitle(branch, other) };
2452
+ });
2453
+ } else {
2454
+ parent = topic.parentId ? byId[topic.parentId] : null;
2455
+ kids = childMap[topic.id] || [];
2456
+ relations = (relationsByTopic[topic.id] || []).map(function (rel) {
2457
+ return { direction: rel.direction, other: rel.other, label: rel.label, otherTitle: topicTitleOf(rel.other) };
2458
+ });
2459
+ }
2460
+ var delegatingTopic = isChild && branch.parentTopicId ? byId[branch.parentTopicId] : null;
2461
+ var toolEntries = Object.keys(toolCalls.byName || {}).sort(function (a, b) {
2462
+ return toolCalls.byName[b] - toolCalls.byName[a];
2463
+ }).slice(0, 10);
2464
+ var ownTurns = Array.isArray(topic.ownTurns) ? topic.ownTurns : [];
2465
+ return h('div', { className: 'tg-detail' + (inline ? ' tg-detail-inline' : '') },
2466
+ h('div', { className: 'tg-detail-head' },
2467
+ h('span', { className: 'tg-detail-title' }, (isChild ? '🤖 ' : '') + topic.title),
2468
+ h('button', { className: 'tg-btn', title: t('close'), onClick: closeDetail }, '✕'),
2469
+ ),
2470
+ h('div', { className: 'tg-detail-body', style: { flex: 1, minHeight: 0, overflowY: 'auto' } },
2471
+ h('div', { className: 'tg-detail-kicker' },
2472
+ (isChild ? t('subagent') + ' · ' + (branch.title || sessionShortId(branch.sessionId)) + ' · ' : '')
2473
+ + t('topic') + ' · ' + (topic.status === 'active' ? t('statusActive') : t('statusDone'))
2474
+ + (topic.turnStart != null ? ' · ' + t('range') + ' ' + topic.turnStart + '-' + topic.turnEnd : '')),
2475
+ topic.summary ? h('div', { className: 'tg-topic-summary' }, topic.summary) : null,
2476
+ h('div', { className: 'tg-statgrid' },
2477
+ h('div', { className: 'tg-stat' },
2478
+ h('div', { className: 'tg-stat-label' }, t('tokens') + ' · ' + fmtPct(stats.tokenRatio)),
2479
+ h('div', { className: 'tg-stat-value' }, fmtK(tokens.total || 0)),
2480
+ h('div', { className: 'tg-detail-kv' }, t('input') + ' ' + fmtK(tokens.input) + ' · ' + t('output') + ' ' + fmtK(tokens.output)),
2481
+ h('div', { className: 'tg-detail-kv' }, t('cacheRead') + ' ' + fmtK(tokens.cacheRead) + ' · ' + t('cacheWrite') + ' ' + fmtK(tokens.cacheWrite)),
2482
+ ),
2483
+ h('div', { className: 'tg-stat' },
2484
+ h('div', { className: 'tg-stat-label' }, t('toolCalls') + ' · ' + fmtPct(stats.toolRatio)),
2485
+ h('div', { className: 'tg-stat-value' }, String(toolCalls.total || 0)),
2486
+ h('div', { className: 'tg-detail-kv' }, toolEntries.map(function (name) { return name + '×' + toolCalls.byName[name]; }).join(' · ') || '—'),
2487
+ ),
2488
+ h('div', { className: 'tg-stat' },
2489
+ h('div', { className: 'tg-stat-label' }, t('agentTime') + ' · ' + fmtPct(stats.wallRatio)),
2490
+ h('div', { className: 'tg-stat-value' }, fmtDuration(stats.wallMs || 0)),
2491
+ h('div', { className: 'tg-detail-kv' }, t('stepTime') + ' ' + fmtDuration(stats.agentMs || 0)),
2492
+ ),
2493
+ h('div', { className: 'tg-stat' },
2494
+ h('div', { className: 'tg-stat-label' }, t('statTurns')),
2495
+ h('div', { className: 'tg-stat-value' }, String(stats.turnCount || 0)),
2496
+ h('div', { className: 'tg-detail-kv' }, stage ? t('detailStage') + ':' + stage.title : ''),
2497
+ ),
2498
+ ),
2499
+ delegatingTopic ? h('div', { className: 'tg-detail-section' },
2500
+ h('div', { className: 'tg-detail-section-title' }, t('parentTopic')),
2501
+ h('span', { className: 'tg-rel-chip', onClick: function () { openDetail(delegatingTopic.id); } }, delegatingTopic.title),
2502
+ ) : null,
2503
+ parent ? h('div', { className: 'tg-detail-section' },
2504
+ h('div', { className: 'tg-detail-section-title' }, t('detailParent')),
2505
+ h('span', {
2506
+ className: 'tg-rel-chip',
2507
+ onClick: function () {
2508
+ if (isChild) openChildDetail(resolved.sessionId, parent.id);
2509
+ else openDetail(parent.id);
2510
+ },
2511
+ }, parent.title),
2512
+ ) : null,
2513
+ relations.length ? h('div', { className: 'tg-detail-section' },
2514
+ h('div', { className: 'tg-detail-section-title' }, t('detailRelated')),
2515
+ h('div', { className: 'tg-relations' }, relations.map(function (rel, index) {
2516
+ return h('span', {
2517
+ key: 'drel-' + index,
2518
+ className: 'tg-rel-chip',
2519
+ onClick: function () {
2520
+ if (isChild) openChildDetail(resolved.sessionId, rel.other);
2521
+ else openDetail(rel.other);
2522
+ },
2523
+ }, (rel.direction === 'out' ? '→ ' : '← ') + rel.otherTitle + ' ', h('b', null, rel.label));
2524
+ })),
2525
+ ) : null,
2526
+ kids.length ? h('div', { className: 'tg-detail-section' },
2527
+ h('div', { className: 'tg-detail-section-title' }, t('detailChildren')),
2528
+ h('div', { className: 'tg-relations' }, kids.map(function (kid) {
2529
+ return h('span', {
2530
+ key: kid.id,
2531
+ className: 'tg-rel-chip',
2532
+ onClick: function () {
2533
+ if (isChild) openChildDetail(resolved.sessionId, kid.id);
2534
+ else openDetail(kid.id);
2535
+ },
2536
+ }, kid.title);
2537
+ })),
2538
+ ) : null,
2539
+ !isChild && topicBranches(topic.id).length ? h('div', { className: 'tg-detail-section' },
2540
+ h('div', { className: 'tg-detail-section-title' }, t('detailBranches')),
2541
+ topicBranches(topic.id).map(function (item) {
2542
+ return h('div', { key: item.sessionId, className: 'tg-subbranch', onClick: function () { selectSession(item.sessionId); closeDetail(); } },
2543
+ h('span', { className: 'tg-subbranch-name' }, '🤖 ' + (item.title || sessionShortId(item.sessionId))),
2544
+ h('span', { className: 'tg-subbranch-meta' }, (item.totals ? item.totals.turnCount : 0) + ' ' + t('turns') + ' · ' + fmtK(item.totals ? (item.totals.tokens || {}).total || 0 : 0)),
2545
+ );
2546
+ }),
2547
+ ) : null,
2548
+ h('div', { className: 'tg-detail-section' },
2549
+ h('div', { className: 'tg-detail-section-title' }, t('detailTurns')),
2550
+ ownTurns.length
2551
+ ? ownTurns.map(renderTurn)
2552
+ : h('div', { className: 'tg-topic-summary' },
2553
+ (isChild ? t('subagent') + ' · ' + (branch.title || sessionShortId(resolved.sessionId)) + ' · ' : '')
2554
+ + String(stats.turnCount || 0) + ' ' + t('turns') + ' · ' + t('openAtFirstTurn')),
2555
+ ),
2556
+ jumpStatus ? h('div', { className: 'tg-detail-status' },
2557
+ jumpStatus === 'locating' ? t('locating') : (jumpStatus === 'located' ? t('located') : t('openedOnly')),
2558
+ ) : null,
2559
+ ),
2560
+ h('div', { className: 'tg-detail-foot' },
2561
+ isChild ? h('button', { className: 'tg-btn', onClick: function () { selectSession(resolved.sessionId); closeDetail(); } }, '🤖 ' + t('branchOpen')) : null,
2562
+ h('button', { className: 'tg-btn primary', disabled: jumpStatus === 'locating', onClick: function () { void jumpToTopicStart(topic, resolved.sessionId); } }, t('openAtFirstTurn')),
2563
+ h('button', { className: 'tg-btn', onClick: closeDetail }, t('close')),
2564
+ ),
2565
+ );
2566
+ }
2567
+
2568
+ // ── 渲染:链式 Tab ──────────────────────────────────────────────────
2569
+ function renderWorkflowTree() {
2570
+ if (!graph) return renderLoading();
2571
+ var stages = graph.stages || [];
2572
+ var topics = graph.topics || [];
2573
+ var rootsByStage = {};
2574
+ topics.forEach(function (topic) {
2575
+ if (topic.parentId) return;
2576
+ if (!rootsByStage[topic.stageId]) rootsByStage[topic.stageId] = [];
2577
+ rootsByStage[topic.stageId].push(topic);
2578
+ });
2579
+ var renderWfTopic = function (topic, depth) {
2580
+ var kids = childMap[topic.id] || [];
2581
+ var active = selectedTopicId === topic.id;
2582
+ var branches = topicBranches(topic.id);
2583
+ var stats = topic.stats || {};
2584
+ return h('div', { key: topic.id },
2585
+ h('div', {
2586
+ className: 'tg-wf-topic' + (active ? ' active' : ''),
2587
+ style: { paddingLeft: (8 + depth * 14) + 'px' },
2588
+ onClick: function () { selectTopicInStage(topic); },
2589
+ title: t('detailOpen'),
2590
+ },
2591
+ h('span', { className: 'tg-wf-topic-title' }, topic.title),
2592
+ h('span', { className: 'tg-wf-topic-meta' },
2593
+ (topic.turnStart != null ? '轮' + topic.turnStart + '-' + topic.turnEnd + ' · ' : '')
2594
+ + '⏱' + fmtPct(stats.wallRatio) + ' ◈' + fmtPct(stats.tokenRatio)
2595
+ + (branches.length ? ' · 🤖' + branches.length : '')),
2596
+ ),
2597
+ kids.map(function (kid) { return renderWfTopic(kid, depth + 1); }),
2598
+ );
2599
+ };
2600
+ return h('div', { className: 'tg-wf-tree' },
2601
+ h('div', { className: 'tg-wf-tree-head' },
2602
+ h('span', { className: 'tg-wf-tree-head-title' }, stages.length + ' ' + t('statStage')),
2603
+ h('button', { className: 'tg-btn tg-wf-tree-btn', onClick: function () { setAllStages(true); } }, t('expandAll')),
2604
+ h('button', { className: 'tg-btn tg-wf-tree-btn', onClick: function () { setAllStages(false); } }, t('collapseAll')),
2605
+ ),
2606
+ stages.length ? stages.map(function (stage) {
2607
+ var roots = rootsByStage[stage.id] || [];
2608
+ var open = expandedStages[stage.id] !== false;
2609
+ var stageActive = selectedStageId === stage.id && !detailTopicId;
2610
+ var stageStats = stage.stats || {};
2611
+ var stageTokens = stageStats.tokens || {};
2612
+ return h('div', { key: stage.id, className: 'tg-wf-stage' },
2613
+ h('div', {
2614
+ className: 'tg-wf-stage-head' + (stageActive ? ' active' : ''),
2615
+ onClick: function () {
2616
+ setDetailTopicId(null);
2617
+ setDetailSessionId(null);
2618
+ setSelectedTopicId(null);
2619
+ setSelectedStageId(stage.id);
2620
+ },
2621
+ },
2622
+ h('div', { className: 'tg-wf-stage-title-row' },
2623
+ h('span', {
2624
+ className: 'tg-wf-stage-toggle',
2625
+ onClick: function (event) { event.stopPropagation(); toggleStage(stage.id); },
2626
+ title: open ? t('collapse') : t('expand'),
2627
+ }, h('span', { className: 'tg-chev' + (open ? ' open' : '') }, '▶')),
2628
+ h('span', { className: 'tg-wf-stage-idx' }, String(stage.order + 1)),
2629
+ h('span', { className: 'tg-wf-stage-title' }, stage.title),
2630
+ h('span', { className: 'tg-wf-stage-meta' }, roots.length + ' ' + t('topics')),
2631
+ ),
2632
+ h('div', { className: 'tg-wf-stage-metrics' },
2633
+ h('span', { className: 'tg-wf-metric' }, 'Token ' + fmtK(stageTokens.total || 0) + ' · ' + fmtPct(stageStats.tokenRatio)),
2634
+ h('span', { className: 'tg-wf-metric tg-wf-metric-time' }, '时间 ' + fmtDuration(stageStats.wallMs || 0) + ' · ' + fmtPct(stageStats.wallRatio)),
2635
+ ),
2636
+ ),
2637
+ open && roots.length ? h('div', { className: 'tg-wf-stage-topics' }, roots.map(function (root) { return renderWfTopic(root, 0); })) : null,
2638
+ );
2639
+ }) : h('div', { className: 'tg-wf-empty' }, t('noTopics')),
2640
+ );
2641
+ }
2642
+
2643
+ /** 阶段内 mini 时间线:X=轮次,展示该阶段各话题的点与它们之间的关系边。 */
2644
+ function renderStageMiniTimeline(stage, roots) {
2645
+ var ids = {};
2646
+ roots.forEach(function (topic) { ids[topic.id] = true; });
2647
+ var rels = (graph.relations || []).filter(function (rel) { return ids[rel.from] && ids[rel.to]; });
2648
+ // 阶段内相邻 topic 之间只有 1 条关系边时不画(避免和顺序流重复、显得乱)
2649
+ var adjacentPairs = {};
2650
+ for (var ap = 0; ap < roots.length - 1; ap += 1) {
2651
+ adjacentPairs[roots[ap].id + '>' + roots[ap + 1].id] = true;
2652
+ adjacentPairs[roots[ap + 1].id + '>' + roots[ap].id] = true;
2653
+ }
2654
+ var pairCount = {};
2655
+ rels.forEach(function (rel) {
2656
+ var key = [rel.from, rel.to].sort().join('>');
2657
+ pairCount[key] = (pairCount[key] || 0) + 1;
2658
+ });
2659
+ rels = rels.filter(function (rel) {
2660
+ if (adjacentPairs[rel.from + '>' + rel.to] === true) return false;
2661
+ return true;
2662
+ });
2663
+ var minTurn = Infinity;
2664
+ var maxTurn = -Infinity;
2665
+ roots.forEach(function (topic) {
2666
+ minTurn = Math.min(minTurn, topic.turnStart || 1);
2667
+ maxTurn = Math.max(maxTurn, topic.turnEnd || topic.turnStart || 1);
2668
+ });
2669
+ if (!isFinite(minTurn)) { minTurn = 1; maxTurn = 1; }
2670
+ var span = Math.max(1, maxTurn - minTurn);
2671
+ var W = 600;
2672
+ var laneY = 52;
2673
+ var padX = 30;
2674
+ var pos = {};
2675
+ roots.forEach(function (topic) {
2676
+ var ratio = span > 0 ? ((topic.turnStart || 1) - minTurn) / span : 0.5;
2677
+ pos[topic.id] = { x: padX + ratio * (W - padX * 2), y: laneY, topic: topic };
2678
+ });
2679
+ // 关系边按 x 区间分层,重叠的边抬到不同高度,避免叠在一起
2680
+ var rawArcs = [];
2681
+ rels.forEach(function (rel) {
2682
+ var a = pos[rel.from];
2683
+ var b = pos[rel.to];
2684
+ if (!a || !b) return;
2685
+ rawArcs.push({
2686
+ id: rel.from + '>' + rel.to,
2687
+ x1: a.x,
2688
+ x2: b.x,
2689
+ left: Math.min(a.x, b.x),
2690
+ right: Math.max(a.x, b.x),
2691
+ from: rel.from,
2692
+ to: rel.to,
2693
+ label: rel.label || '',
2694
+ });
2695
+ });
2696
+ rawArcs.sort(function (p, q) { return p.left - q.left || p.right - q.right; });
2697
+ var layerEnd = [];
2698
+ rawArcs.forEach(function (arc) {
2699
+ var layer = 0;
2700
+ while (layer < layerEnd.length && layerEnd[layer] > arc.left) layer += 1;
2701
+ if (layer === layerEnd.length) layerEnd.push(arc.right);
2702
+ else layerEnd[layer] = arc.right;
2703
+ arc.layer = layer;
2704
+ });
2705
+ var maxLayer = Math.max(0, layerEnd.length - 1);
2706
+ var baseLift = 20;
2707
+ var liftStep = 14;
2708
+ var topMost = laneY - baseLift - maxLayer * liftStep - 10;
2709
+ var viewTop = Math.min(6, topMost);
2710
+ var viewH = (laneY + 26) - viewTop;
2711
+ var arcs = rawArcs.map(function (arc) {
2712
+ var lift = baseLift + arc.layer * liftStep;
2713
+ return {
2714
+ id: arc.id,
2715
+ d: 'M ' + arc.x1 + ' ' + laneY + ' C ' + arc.x1 + ' ' + (laneY - lift) + ', ' + arc.x2 + ' ' + (laneY - lift) + ', ' + arc.x2 + ' ' + laneY,
2716
+ label: arc.label,
2717
+ labelX: (arc.x1 + arc.x2) / 2,
2718
+ labelY: laneY - lift - 6,
2719
+ from: arc.from,
2720
+ to: arc.to,
2721
+ };
2722
+ });
2723
+ return h('div', { className: 'tg-wf-mini-wrap' },
2724
+ h('div', { className: 'tg-wf-mini-title' }, t('stageTopicRelations') || '话题关系'),
2725
+ h('svg', {
2726
+ className: 'tg-wf-mini',
2727
+ viewBox: '0 ' + viewTop + ' ' + W + ' ' + viewH,
2728
+ preserveAspectRatio: 'xMidYMid meet',
2729
+ style: { display: 'block', width: '100%', height: 'auto' },
2730
+ },
2731
+ h('line', { className: 'tg-wf-mini-base', x1: 16, y1: laneY, x2: W - 16, y2: laneY }),
2732
+ arcs.map(function (arc) {
2733
+ var active = wfMiniHover && (arc.from === wfMiniHover || arc.to === wfMiniHover);
2734
+ return h('g', { key: 'arc-' + arc.id, className: 'tg-wf-mini-arc' + (active ? ' active' : '') },
2735
+ h('path', { d: arc.d }),
2736
+ arc.label ? h('text', { x: arc.labelX, y: arc.labelY }, arc.label) : null,
2737
+ );
2738
+ }),
2739
+ roots.map(function (topic, i) {
2740
+ var p = pos[topic.id];
2741
+ var active = wfMiniHover === topic.id || selectedTopicId === topic.id;
2742
+ return h('g', {
2743
+ key: 'dot-' + topic.id,
2744
+ className: 'tg-wf-mini-dot' + (active ? ' active' : ''),
2745
+ onMouseEnter: function () { setWfMiniHover(topic.id); },
2746
+ onMouseLeave: function () { setWfMiniHover(null); },
2747
+ onClick: function () { setSelectedTopicId(topic.id); },
2748
+ },
2749
+ h('circle', { cx: p.x, cy: p.y, r: 5, style: { fill: TIMELINE_PALETTE[i % TIMELINE_PALETTE.length] } }),
2750
+ h('text', { className: 'tg-wf-mini-idx', x: p.x, y: laneY + 22 }, topic.turnStart != null ? '轮' + topic.turnStart + '-' + topic.turnEnd : ''),
2751
+ h('title', null, topic.title),
2752
+ );
2753
+ }),
2754
+ ),
2755
+ );
2756
+ }
2757
+
2758
+ function renderStageTopicCard(topic, depth) {
2759
+ var open = expandedTopics[topic.id] === true;
2760
+ var kids = childMap[topic.id] || [];
2761
+ var stats = topic.stats || {};
2762
+ var tokens = stats.tokens || {};
2763
+ var branches = topicBranches(topic.id);
2764
+ return h('div', { key: topic.id, className: 'tg-wf-topic-card' + (open ? ' open' : '') },
2765
+ h('div', {
2766
+ className: 'tg-wf-topic-card-head',
2767
+ style: { paddingLeft: (10 + depth * 14) + 'px' },
2768
+ },
2769
+ h('span', {
2770
+ className: 'tg-wf-topic-card-toggle',
2771
+ onClick: function (event) { event.stopPropagation(); toggleTopic(topic.id); },
2772
+ title: open ? t('collapse') : t('expand'),
2773
+ },
2774
+ h('span', { className: 'tg-chev' + (open ? ' open' : '') }, '▶'),
2775
+ h('span', { className: 'tg-wf-topic-card-title' }, topic.title),
2776
+ ),
2777
+ h('span', { className: 'tg-wf-topic-card-meta' },
2778
+ (topic.turnStart != null ? '轮' + topic.turnStart + '-' + topic.turnEnd + ' · ' : '')
2779
+ + fmtK(tokens.total || 0) + ' · ' + fmtDuration(stats.wallMs || 0)
2780
+ + (branches.length ? ' · 🤖' + branches.length : '')),
2781
+ ),
2782
+ open ? h('div', { className: 'tg-wf-topic-card-body' },
2783
+ topic.summary ? h('div', { className: 'tg-wf-topic-summary' }, topic.summary) : null,
2784
+ (topic.ownTurns && topic.ownTurns.length) ? h('div', { className: 'tg-detail-section' },
2785
+ h('div', { className: 'tg-detail-section-title' }, t('detailTurns')),
2786
+ topic.ownTurns.map(renderTurn),
2787
+ ) : null,
2788
+ kids.map(function (kid) { return renderStageTopicCard(kid, depth + 1); }),
2789
+ ) : null,
2790
+ );
2791
+ }
2792
+
2793
+ function barPct(ratio) {
2794
+ var value = Number(ratio) || 0;
2795
+ return Math.max(1.5, Math.min(100, value * 100));
2796
+ }
2797
+
2798
+ function renderStageDetail(stage) {
2799
+ var topics = graph.topics || [];
2800
+ var roots = topics.filter(function (t) { return !t.parentId && t.stageId === stage.id; })
2801
+ .sort(function (a, b) { return (a.turnStart || 0) - (b.turnStart || 0); });
2802
+ var stats = stage.stats || {};
2803
+ var tokens = stats.tokens || {};
2804
+ var toolCalls = stats.toolCalls || { total: 0 };
2805
+ var agentMs = stats.agentMs || 0;
2806
+ return h('div', { className: 'tg-wf-stage-detail' },
2807
+ h('div', { className: 'tg-wf-stage-detail-title' }, stage.title),
2808
+ stage.summary ? h('div', { className: 'tg-wf-stage-detail-summary' }, stage.summary) : null,
2809
+ h('div', { className: 'tg-wf-stage-detail-meta' },
2810
+ roots.length + ' ' + t('topics') + ' · ' + (stats.turnCount || 0) + ' ' + t('turns') + ' · ' + t('agentTime') + ' ' + fmtDuration(agentMs)),
2811
+ h('div', { className: 'tg-wf-statgrid' },
2812
+ h('div', { className: 'tg-wf-stat' },
2813
+ h('div', { className: 'tg-wf-stat-top' }, h('span', null, t('statTokens')), h('b', null, fmtK(tokens.total || 0))),
2814
+ h('div', { className: 'tg-wf-bar' }, h('i', { className: 'brand', style: { width: barPct(stats.tokenRatio) + '%' } })),
2815
+ h('div', { className: 'tg-wf-stat-ratio' }, t('statTokens') + ' ' + fmtPct(stats.tokenRatio)),
2816
+ ),
2817
+ h('div', { className: 'tg-wf-stat' },
2818
+ h('div', { className: 'tg-wf-stat-top' }, h('span', null, t('statWallTime')), h('b', null, fmtDuration(stats.wallMs || 0))),
2819
+ h('div', { className: 'tg-wf-bar' }, h('i', { className: 'green', style: { width: barPct(stats.wallRatio) + '%' } })),
2820
+ h('div', { className: 'tg-wf-stat-ratio' }, t('statWallTime') + ' ' + fmtPct(stats.wallRatio)),
2821
+ ),
2822
+ h('div', { className: 'tg-wf-stat' },
2823
+ h('div', { className: 'tg-wf-stat-top' }, h('span', null, t('statTools')), h('b', null, String(toolCalls.total || 0))),
2824
+ h('div', { className: 'tg-wf-bar' }, h('i', { className: 'warn', style: { width: barPct(stats.toolRatio) + '%' } })),
2825
+ h('div', { className: 'tg-wf-stat-ratio' }, t('statTools') + ' ' + fmtPct(stats.toolRatio)),
2826
+ ),
2827
+ ),
2828
+ roots.length >= 2 ? renderStageMiniTimeline(stage, roots) : null,
2829
+ roots.length ? h('div', { className: 'tg-wf-stage-topics' }, roots.map(function (root) { return renderStageTopicCard(root, 0); })) : null,
2830
+ );
2831
+ }
2832
+
2833
+ function renderWorkflowDetail() {
2834
+ if (!graph) return null;
2835
+ var stages = graph.stages || [];
2836
+ var stage = selectedStageId ? stages.filter(function (s) { return s.id === selectedStageId; })[0] : stages[0];
2837
+ if (!stage) return h('div', { className: 'tg-wf-empty' }, t('workflowEmpty'));
2838
+ return renderStageDetail(stage);
2839
+ }
2840
+
2841
+ function renderChain() {
2842
+ if (!graph) return renderLoading();
2843
+ if (!graph.turns || !graph.turns.length) {
2844
+ return h('div', { className: 'tg-empty' }, t('noTurns'));
2845
+ }
2846
+ var stages = graph.stages || [];
2847
+ if (!stages.length) return renderNoTopics();
2848
+ return h('div', { className: 'tg-chain' },
2849
+ stages.map(function (stage, index) {
2850
+ var open = expandedStages[stage.id] !== false;
2851
+ var stats = stage.stats || {};
2852
+ var topicCount = (stage.topicIds || []).length;
2853
+ return h('div', { key: stage.id, className: 'tg-stage' },
2854
+ h('div', { className: 'tg-stage-head', onClick: function () { toggleStage(stage.id); } },
2855
+ h('span', { className: 'tg-stage-index' }, String(index + 1)),
2856
+ h('span', { className: 'tg-stage-title' }, stage.title || t('stage')),
2857
+ h('span', { className: 'tg-stage-chips' },
2858
+ h('span', { className: 'tg-chip' }, fmtK((stats.tokens && stats.tokens.total) || 0)),
2859
+ h('span', { className: 'tg-chip' }, fmtPct(stats.tokenRatio)),
2860
+ h('span', { className: 'tg-chip' }, topicCount + ' ' + t('topics')),
2861
+ ),
2862
+ h('span', { className: 'tg-chev' + (open ? ' open' : '') }, '▶'),
2863
+ ),
2864
+ open ? h('div', { className: 'tg-stage-body' },
2865
+ (stage.topicIds || []).map(function (topicId) { return renderTopic(byId[topicId], 0); }),
2866
+ ) : null,
2867
+ );
2868
+ }),
2869
+ );
2870
+ }
2871
+
2872
+ function renderNoTopics() {
2873
+ var ai = (graph && graph.ai) || {};
2874
+ var running = ai.status === 'running' || (graph && graph.totals && graph.totals.openTurn);
2875
+ return h('div', { className: 'tg-empty' },
2876
+ running ? h('div', { className: 'tg-spin' }) : null,
2877
+ h('div', null, running ? t('segmenting') : t('noTopics')),
2878
+ ai.error ? h('div', { style: { marginTop: '8px', color: 'var(--dsw-alias-state-error-primary,#ef4444)' } }, ai.error) : null,
2879
+ !running && !ai.error ? h('div', { style: { marginTop: '10px' } },
2880
+ h('button', { className: 'tg-btn primary', onClick: function () { void triggerSegment(false); } }, t('segNow')),
2881
+ ) : null,
2882
+ );
2883
+ }
2884
+
2885
+ function renderLoading() {
2886
+ return h('div', { className: 'tg-empty' },
2887
+ h('div', { className: 'tg-spin' }),
2888
+ h('div', null, t('loading')),
2889
+ );
2890
+ }
2891
+
2892
+ // ── 渲染:图谱 Tab ──────────────────────────────────────────────────
2893
+ function branchTooltipContent(branch) {
2894
+ var totals = branch.totals || {};
2895
+ var tokens = totals.tokens || {};
2896
+ var children = branch.topics || [];
2897
+ return h('div', null,
2898
+ h('div', { className: 'tg-tip-kind' }, '🤖 ' + t('subagent') + (branch.depth > 1 ? ' L' + branch.depth : '')),
2899
+ h('div', { className: 'tg-tip-title' }, branch.title || sessionShortId(branch.sessionId)),
2900
+ h('div', { className: 'tg-tip-grid' },
2901
+ h('span', { className: 'k' }, t('parentTopic')),
2902
+ h('span', { className: 'v' }, branch.parentTopicId ? topicTitleOf(branch.parentTopicId) : '-'),
2903
+ h('span', { className: 'k' }, t('range')),
2904
+ h('span', { className: 'v' }, '父轮 ' + (branch.parentTurn != null ? branch.parentTurn : '-') + ' · ' + totals.turnCount + ' ' + t('turns')),
2905
+ h('span', { className: 'k' }, t('tokens')),
2906
+ h('span', { className: 'v' }, fmtK(tokens.total)),
2907
+ h('span', { className: 'k' }, t('input') + '/' + t('output')),
2908
+ h('span', { className: 'v' }, fmtK(tokens.input) + ' / ' + fmtK(tokens.output)),
2909
+ h('span', { className: 'k' }, t('toolCalls')),
2910
+ h('span', { className: 'v' }, String((totals.toolCalls && totals.toolCalls.total) || 0)),
2911
+ h('span', { className: 'k' }, t('agentTime')),
2912
+ h('span', { className: 'v' }, fmtDuration(totals.wallMs)),
2913
+ ),
2914
+ children.length ? h('div', { className: 'tg-tip-list' },
2915
+ h('div', null, t('branchTopics') + ' (' + children.length + ')'),
2916
+ children.slice(0, 8).map(function (topic) {
2917
+ return h('div', { key: topic.id }, '· ' + topic.title + (topic.stats ? ' · ' + fmtK(topic.stats.tokens.total) : ''));
2918
+ }),
2919
+ ) : h('div', { className: 'tg-tip-list' }, branch.pending ? t('branchPending') : ''),
2920
+ h('div', { className: 'tg-tip-list', style: { marginTop: '4px' } }, '点击查看该子智能体会话'),
2921
+ );
2922
+ }
2923
+
2924
+ function branchTopicTooltipContent(node) {
2925
+ var branch = node.branch;
2926
+ var topic = node.topic;
2927
+ var stats = topic.stats || {};
2928
+ var tokens = stats.tokens || {};
2929
+ var toolCalls = stats.toolCalls || { total: 0, byName: {} };
2930
+ var stage = (branch.stages || []).filter(function (item) { return item.id === topic.stageId; })[0] || null;
2931
+ var toolEntries = Object.keys(toolCalls.byName || {}).sort(function (a, b) {
2932
+ return toolCalls.byName[b] - toolCalls.byName[a];
2933
+ }).slice(0, 6);
2934
+ return h('div', null,
2935
+ h('div', { className: 'tg-tip-kind' }, '🤖 ' + t('subagent') + ' · ' + (branch.title || sessionShortId(branch.sessionId))),
2936
+ h('div', { className: 'tg-tip-title' }, topic.title || ''),
2937
+ topic.summary ? h('div', { className: 'tg-tip-summary' }, topic.summary) : null,
2938
+ h('div', { className: 'tg-tip-grid' },
2939
+ h('span', { className: 'k' }, t('range')),
2940
+ h('span', { className: 'v' }, (topic.turnStart != null ? topic.turnStart + '-' + topic.turnEnd : '-') + ' · ' + (stats.turnCount || 0) + ' ' + t('turns')),
2941
+ h('span', { className: 'k' }, t('tokens')),
2942
+ h('span', { className: 'v' }, fmtK(tokens.total || 0) + ' · ' + fmtPct(stats.tokenRatio)),
2943
+ h('span', { className: 'k' }, t('input') + '/' + t('output')),
2944
+ h('span', { className: 'v' }, fmtK(tokens.input) + ' / ' + fmtK(tokens.output)),
2945
+ h('span', { className: 'k' }, t('toolCalls')),
2946
+ h('span', { className: 'v' }, String(toolCalls.total || 0) + ' · ' + fmtPct(stats.toolRatio)),
2947
+ h('span', { className: 'k' }, t('agentTime')),
2948
+ h('span', { className: 'v' }, fmtDuration(stats.wallMs || 0) + ' · ' + fmtPct(stats.wallRatio)),
2949
+ stage ? h('span', { className: 'k' }, t('detailStage')) : null,
2950
+ stage ? h('span', { className: 'v' }, stage.title) : null,
2951
+ ),
2952
+ toolEntries.length ? h('div', { className: 'tg-tip-list' },
2953
+ h('div', null, t('toolDistribution')),
2954
+ toolEntries.map(function (name) { return h('div', { key: name }, name + ' × ' + toolCalls.byName[name]); }),
2955
+ ) : null,
2956
+ h('div', { className: 'tg-tip-list', style: { marginTop: '4px' } }, t('detailOpen') + ' · ' + t('branchOpen')),
2957
+ );
2958
+ }
2959
+
2960
+ function nodeTooltipContent(node) {
2961
+ if (!node) return null;
2962
+ if (node.kind === 'branchHeader' || node.kind === 'branch') return branchTooltipContent(node.branch);
2963
+ if (node.kind === 'branchTopic') return branchTopicTooltipContent(node);
2964
+ var item = node.kind === 'stage' ? node.stage : node.topic;
2965
+ var stats = item.stats || {};
2966
+ var tokens = stats.tokens || {};
2967
+ var toolCalls = stats.toolCalls || { total: 0, byName: {} };
2968
+ var toolEntries = Object.keys(toolCalls.byName || {}).sort(function (a, b) {
2969
+ return toolCalls.byName[b] - toolCalls.byName[a];
2970
+ }).slice(0, 8);
2971
+ return h('div', null,
2972
+ h('div', { className: 'tg-tip-kind' }, node.kind === 'stage' ? t('stage') : (item.parentId ? t('subtopics') : t('topic'))),
2973
+ h('div', { className: 'tg-tip-title' }, item.title || ''),
2974
+ item.summary ? h('div', { className: 'tg-tip-summary' }, item.summary) : null,
2975
+ h('div', { className: 'tg-tip-grid' },
2976
+ h('span', { className: 'k' }, t('range')),
2977
+ h('span', { className: 'v' }, (item.turnStart != null ? item.turnStart + '-' + item.turnEnd : '-') + ' · ' + (stats.turnCount || 0) + ' ' + t('turns')),
2978
+ h('span', { className: 'k' }, t('tokens')),
2979
+ h('span', { className: 'v' }, fmtK(tokens.total) + ' · ' + fmtPct(stats.tokenRatio)),
2980
+ h('span', { className: 'k' }, t('input')),
2981
+ h('span', { className: 'v' }, fmtK(tokens.input)),
2982
+ h('span', { className: 'k' }, t('output')),
2983
+ h('span', { className: 'v' }, fmtK(tokens.output)),
2984
+ h('span', { className: 'k' }, t('cacheRead')),
2985
+ h('span', { className: 'v' }, fmtK(tokens.cacheRead)),
2986
+ h('span', { className: 'k' }, t('toolCalls')),
2987
+ h('span', { className: 'v' }, String(toolCalls.total || 0) + ' · ' + fmtPct(stats.toolRatio)),
2988
+ h('span', { className: 'k' }, t('agentTime')),
2989
+ h('span', { className: 'v' }, fmtDuration(stats.wallMs) + ' · ' + fmtPct(stats.wallRatio)),
2990
+ h('span', { className: 'k' }, t('stepTime')),
2991
+ h('span', { className: 'v' }, fmtDuration(stats.agentMs)),
2992
+ node.kind === 'topic' ? h('span', { className: 'k' }, t('statusActive')) : null,
2993
+ node.kind === 'topic' ? h('span', { className: 'v' }, item.status === 'active' ? t('statusActive') : t('statusDone')) : null,
2994
+ node.kind === 'topic' && item.branchIds && item.branchIds.length ? h('span', { className: 'k' }, t('subagents')) : null,
2995
+ node.kind === 'topic' && item.branchIds && item.branchIds.length ? h('span', { className: 'v' }, String(item.branchIds.length)) : null,
2996
+ ),
2997
+ node.kind === 'topic' && item.branchIds && item.branchIds.length
2998
+ ? h('div', { className: 'tg-tip-list' },
2999
+ h('div', null, t('subagents')),
3000
+ item.branchIds.slice(0, 6).map(function (sessionId) {
3001
+ var branch = branchBySession[sessionId];
3002
+ return h('div', { key: sessionId }, '🤖 ' + (branch ? (branch.title || sessionShortId(sessionId)) : sessionShortId(sessionId))
3003
+ + (branch && branch.totals ? ' · ' + fmtK(branch.totals.tokens.total) : ''));
3004
+ }),
3005
+ )
3006
+ : null,
3007
+ toolEntries.length ? h('div', { className: 'tg-tip-list' },
3008
+ h('div', null, t('toolDistribution')),
3009
+ toolEntries.map(function (toolName) {
3010
+ return h('div', { key: toolName }, toolName + ' × ' + toolCalls.byName[toolName]);
3011
+ }),
3012
+ ) : null,
3013
+ );
3014
+ }
3015
+
3016
+ function renderGraphTooltip() {
3017
+ if (!hover) return null;
3018
+ var tipWidth = 330;
3019
+ var x = clamp(hover.clientX + 16, 8, Math.max(8, window.innerWidth - tipWidth - 12));
3020
+ var y = clamp(hover.clientY + 16, 8, Math.max(8, window.innerHeight - 300));
3021
+ return h('div', { className: 'tg-tip', style: { left: x + 'px', top: y + 'px', width: tipWidth + 'px' } }, nodeTooltipContent(hover.node));
3022
+ }
3023
+
3024
+ function renderGraphToolbar() {
3025
+ var scalePct = Math.round(scale * 100);
3026
+ var modes = [
3027
+ { key: 'tree', label: t('graphModeTree') },
3028
+ { key: 'timeline', label: t('graphModeTimeline') },
3029
+ ];
3030
+ return h('div', { className: 'tg-graph-toolbar' },
3031
+ h('span', { className: 'tg-graph-hint' }, t('graphHint')),
3032
+ h('span', { className: 'tg-graph-modes' }, modes.map(function (mode) {
3033
+ return h('button', {
3034
+ key: mode.key,
3035
+ className: 'tg-mode-btn' + (graphMode === mode.key ? ' active' : ''),
3036
+ title: mode.label,
3037
+ onClick: function () { setGraphMode(mode.key); },
3038
+ }, mode.label);
3039
+ })),
3040
+ graphMode === 'tree'
3041
+ ? h(React.Fragment, null,
3042
+ h('button', { className: 'tg-btn', title: t('zoomOut'), onClick: function () { setScale(clamp(scale * 0.85, 0.35, 2.5)); } }, '−'),
3043
+ h('span', { className: 'tg-zoom-pct' }, scalePct + '%'),
3044
+ h('button', { className: 'tg-btn', title: t('zoomIn'), onClick: function () { setScale(clamp(scale * 1.18, 0.35, 2.5)); } }, '+'),
3045
+ h('button', { className: 'tg-btn', title: t('zoomReset'), onClick: function () { setScale(1); } }, '⟲'))
3046
+ : null,
3047
+ );
3048
+ }
3049
+
3050
+ function scrollTimelineTo(topicId) {
3051
+ var el = tlScrollRef.current;
3052
+ if (!el) return;
3053
+ var pos = buildTimelineLayout(graph || {}, tlBoxW || 900).posById[topicId];
3054
+ if (!pos) return;
3055
+ try {
3056
+ el.scrollLeft = Math.max(0, pos.x - 80);
3057
+ el.scrollTop = Math.max(0, pos.y - 100);
3058
+ } catch (error) { /* ignore */ }
3059
+ }
3060
+
3061
+ function renderTimelineTooltip() {
3062
+ if (!tlHover || (!tlHover.topicId && !tlHover.relId)) return null;
3063
+ var style = { left: Math.max(8, tlHover.clientX + 14) + 'px', top: Math.max(8, tlHover.clientY + 12) + 'px' };
3064
+ if (tlHover.topicId) {
3065
+ var topic = byId[tlHover.topicId];
3066
+ if (!topic) return null;
3067
+ var stats = topic.stats || {};
3068
+ var tokens = stats.tokens || {};
3069
+ var rels = (graph.relations || []).filter(function (r) { return r.from === tlHover.topicId || r.to === tlHover.topicId; });
3070
+ return h('div', { className: 'tg-tl-tip', style: style },
3071
+ h('div', { className: 'tg-tl-tip-title' }, topic.title),
3072
+ h('div', { className: 'tg-tl-tip-line' },
3073
+ (topic.turnStart != null ? '轮' + topic.turnStart + '-' + topic.turnEnd + ' · ' : '') + fmtK(tokens.total || 0) + ' tokens · ' + fmtDuration(stats.wallMs || 0)),
3074
+ rels.length ? h('div', { className: 'tg-tl-tip-line tg-tl-tip-rels' },
3075
+ rels.map(function (r) { return (r.from === topic.id ? '→ ' : '← ') + (r.label || ''); }).join(' ')) : null,
3076
+ );
3077
+ }
3078
+ var rel = (graph.relations || []).filter(function (r) {
3079
+ return ('tlrel:' + r.from + '>' + r.to) === tlHover.relId;
3080
+ })[0];
3081
+ if (!rel) return null;
3082
+ return h('div', { className: 'tg-tl-tip', style: style },
3083
+ h('div', { className: 'tg-tl-tip-title' }, rel.label || t('statRelations')),
3084
+ h('div', { className: 'tg-tl-tip-line' }, topicTitleOf(rel.from) + ' → ' + topicTitleOf(rel.to)),
3085
+ );
3086
+ }
3087
+
3088
+ function renderTimelineViewport() {
3089
+ var lay = buildTimelineLayout(graph || {}, tlBoxW || 900);
3090
+ var hoverTopic = tlHover && tlHover.topicId ? tlHover.topicId : null;
3091
+ var hoverRel = (tlHover && tlHover.relId ? tlHover.relId : null) || tlSelRel || null;
3092
+ var hoverRelObj = hoverRel ? lay.relations.filter(function (r) { return r.id === hoverRel; })[0] : null;
3093
+ function relActive(rel) {
3094
+ if (hoverRel) return rel.id === hoverRel;
3095
+ if (hoverTopic) return rel.from === hoverTopic || rel.to === hoverTopic;
3096
+ return false;
3097
+ }
3098
+ function barActive(topicId) {
3099
+ if (hoverTopic) return topicId === hoverTopic;
3100
+ if (hoverRelObj) return hoverRelObj.from === topicId || hoverRelObj.to === topicId;
3101
+ return selectedTopicId === topicId;
3102
+ }
3103
+ var outline = h('div', { className: 'tg-tl-outline' },
3104
+ lay.lanes.map(function (lane) {
3105
+ return h('div', { key: 'ol-' + lane.stage.id, className: 'tg-tl-stage' },
3106
+ h('div', { className: 'tg-tl-stage-head' },
3107
+ h('span', { className: 'tg-tl-stage-idx' }, String(lane.index + 1)),
3108
+ h('span', { className: 'tg-tl-stage-title', title: lane.stage.title }, lane.stage.title)),
3109
+ lane.topics.map(function (topic) {
3110
+ var idx = lay.indexById[topic.id];
3111
+ var active = selectedTopicId === topic.id || hoverTopic === topic.id;
3112
+ return h('div', {
3113
+ key: 'olt-' + topic.id,
3114
+ className: 'tg-tl-topic' + (active ? ' active' : ''),
3115
+ onClick: function () { setSelectedTopicId(topic.id); scrollTimelineTo(topic.id); },
3116
+ title: topic.title,
3117
+ },
3118
+ h('span', { className: 'tg-tl-topic-idx' }, 'T' + idx),
3119
+ h('span', { className: 'tg-tl-topic-title' }, topic.title));
3120
+ }));
3121
+ }));
3122
+ var svg = h('svg', {
3123
+ className: 'tg-tl-svg' + (hoverRel ? ' has-active' : ''), width: lay.width, height: lay.height,
3124
+ onMouseLeave: function () { setTlHover(null); },
3125
+ onClick: function () { setTlSelRel(null); },
3126
+ },
3127
+ lay.ticks.map(function (tick) {
3128
+ return h('g', { key: 'tk-' + tick.turn },
3129
+ h('line', { className: 'tg-tl-grid', x1: tick.x, y1: lay.rulerH, x2: tick.x, y2: lay.height }),
3130
+ h('text', { className: 'tg-tl-tick', x: tick.x, y: 16 }, String(tick.turn)));
3131
+ }),
3132
+ lay.lanes.map(function (lane) {
3133
+ return h('g', { key: 'lane-' + lane.stage.id },
3134
+ h('rect', { className: 'tg-tl-lane' + (lane.index % 2 ? ' alt' : ''), x: 0, y: lane.y, width: lay.width, height: lane.h }));
3135
+ }),
3136
+ lay.nexts.map(function (n) {
3137
+ return h('path', { key: n.id, className: 'tg-tl-next', d: n.path });
3138
+ }),
3139
+ h('defs', null,
3140
+ h('marker', { id: 'tg-tl-arrow', viewBox: '0 0 8 8', refX: 8, refY: 4, markerWidth: 7, markerHeight: 7, orient: 'auto' },
3141
+ h('path', { d: 'M0,0 L8,4 L0,8 Z', className: 'tg-tl-arrow' }))),
3142
+ lay.relations.map(function (rel) {
3143
+ var active = relActive(rel);
3144
+ return h('g', {
3145
+ key: rel.id,
3146
+ className: 'tg-tl-rel' + (active ? ' active' : '') + (rel.sameLane ? ' same-lane' : ''),
3147
+ onClick: function (event) { event.stopPropagation(); setTlSelRel(rel.id); },
3148
+ onMouseEnter: function (event) { setTlHover({ relId: rel.id, clientX: event.clientX, clientY: event.clientY }); },
3149
+ onMouseMove: function (event) { setTlHover({ relId: rel.id, clientX: event.clientX, clientY: event.clientY }); },
3150
+ onMouseLeave: function () { setTlHover(null); },
3151
+ },
3152
+ h('path', { className: 'tg-tl-rel-hit', d: rel.path }),
3153
+ h('path', { className: 'tg-tl-rel-path', d: rel.path }),
3154
+ h('text', { className: 'tg-tl-rel-label', x: rel.labelX, y: rel.labelY }, rel.label));
3155
+ }),
3156
+ lay.lanes.map(function (lane) {
3157
+ return lane.topics.map(function (topic) {
3158
+ var pos = lay.posById[topic.id];
3159
+ var active = barActive(topic.id);
3160
+ return h('g', {
3161
+ key: 'bar-' + topic.id,
3162
+ className: 'tg-tl-bar' + (active ? ' active' : ''),
3163
+ style: { '--tg-bar-color': TIMELINE_PALETTE[lane.index % TIMELINE_PALETTE.length] },
3164
+ onClick: function () { openDetail(topic.id); },
3165
+ onMouseEnter: function (event) { setTlHover({ topicId: topic.id, clientX: event.clientX, clientY: event.clientY }); },
3166
+ onMouseMove: function (event) { setTlHover({ topicId: topic.id, clientX: event.clientX, clientY: event.clientY }); },
3167
+ onMouseLeave: function () { setTlHover(null); },
3168
+ },
3169
+ h('rect', { className: 'tg-tl-bar-rect', x: pos.x, y: pos.y, width: pos.w, height: pos.h, rx: 2 }));
3170
+ });
3171
+ }),
3172
+ );
3173
+ return h(React.Fragment, null,
3174
+ renderGraphToolbar(),
3175
+ h('div', { className: 'tg-tl-split' },
3176
+ outline,
3177
+ h('div', { className: 'tg-tl-scroll', ref: tlScrollRef, onScroll: function () { setTlHover(null); } }, svg),
3178
+ ),
3179
+ renderTimelineTooltip(),
3180
+ );
3181
+ }
3182
+
3183
+ function renderGraph() {
3184
+ if (!graph) return renderLoading();
3185
+ if (!graph.turns || !graph.turns.length) return h('div', { className: 'tg-empty' }, t('noTurns'));
3186
+ if (!(graph.stages || []).length) return renderNoTopics();
3187
+ if (graphMode === 'timeline') return renderTimelineViewport();
3188
+ var scalePct = Math.round(scale * 100);
3189
+ var edgeTypes = layout.edgeTypes || [];
3190
+ var legend = [
3191
+ { key: 'next', label: t('graphLegendNext'), cls: 'n' },
3192
+ { key: 'belongTo', label: t('graphLegendBelong'), cls: 'b' },
3193
+ { key: 'has_relation', label: t('graphLegendRelation'), cls: 'r' },
3194
+ { key: 'delegates', label: t('graphLegendDelegate'), cls: 'd' },
3195
+ ].filter(function (item) { return edgeTypes.indexOf(item.key) !== -1; });
3196
+ return h(React.Fragment, null,
3197
+ renderGraphToolbar(),
3198
+ legend.length
3199
+ ? h('div', { className: 'tg-legend' }, legend.map(function (item) {
3200
+ return h('span', { key: item.key, className: item.cls }, h('i'), item.label);
3201
+ }))
3202
+ : null,
3203
+ h('div', {
3204
+ className: 'tg-graph-viewport',
3205
+ ref: viewportRef,
3206
+ onMouseLeave: function () { setHover(null); },
3207
+ onClick: function () { setSelectedEdgeId(null); },
3208
+ },
3209
+ h('div', {
3210
+ className: 'tg-graph-canvas',
3211
+ style: { width: layout.width * scale + 'px', height: layout.height * scale + 'px' },
3212
+ },
3213
+ h('div', {
3214
+ className: 'tg-graph-content',
3215
+ style: { width: layout.width + 'px', height: layout.height + 'px', transform: 'scale(' + scale + ')' },
3216
+ },
3217
+ h('svg', { className: 'tg-edges', width: layout.width, height: layout.height },
3218
+ h('defs', null,
3219
+ h('marker', { id: 'tg-arrow-rel', viewBox: '0 0 8 8', refX: 8, refY: 4, markerWidth: 8, markerHeight: 8, orient: 'auto' },
3220
+ h('path', { d: 'M0,0 L8,4 L0,8 Z', className: 'tg-arrow tg-arrow-rel' })),
3221
+ h('marker', { id: 'tg-arrow-next', viewBox: '0 0 8 8', refX: 8, refY: 4, markerWidth: 8, markerHeight: 8, orient: 'auto' },
3222
+ h('path', { d: 'M0,0 L8,4 L0,8 Z', className: 'tg-arrow tg-arrow-next' })),
3223
+ h('marker', { id: 'tg-arrow-deleg', viewBox: '0 0 8 8', refX: 8, refY: 4, markerWidth: 8, markerHeight: 8, orient: 'auto' },
3224
+ h('path', { d: 'M0,0 L8,4 L0,8 Z', className: 'tg-arrow tg-arrow-deleg' })),
3225
+ h('marker', { id: 'tg-arrow-sel', viewBox: '0 0 8 8', refX: 8, refY: 4, markerWidth: 8, markerHeight: 8, orient: 'auto' },
3226
+ h('path', { d: 'M0,0 L8,4 L0,8 Z', className: 'tg-arrow tg-arrow-sel' })),
3227
+ ),
3228
+ layout.edges.map(function (edge) {
3229
+ var selected = selectedEdgeId != null && selectedEdgeId === edge.id;
3230
+ var toggleSelect = function (event) {
3231
+ event.stopPropagation();
3232
+ setSelectedEdgeId(selected ? null : edge.id);
3233
+ };
3234
+ return h('g', { key: edge.id },
3235
+ edge.labelBg && edge.label
3236
+ ? h('rect', {
3237
+ className: 'tg-edge-label-bg' + (selected ? ' selected' : ''),
3238
+ x: edge.labelBg.x, y: edge.labelBg.y, width: edge.labelBg.w, height: edge.labelBg.h, rx: 8,
3239
+ pointerEvents: 'auto',
3240
+ onClick: toggleSelect,
3241
+ })
3242
+ : null,
3243
+ h('path', {
3244
+ className: 'tg-edge-hit',
3245
+ d: edge.path,
3246
+ title: edge.label || '',
3247
+ onClick: toggleSelect,
3248
+ }),
3249
+ h('path', {
3250
+ className: 'tg-edge tg-edge-' + edge.type + (selected ? ' selected' : ''),
3251
+ d: edge.path,
3252
+ markerEnd: selected ? 'url(#tg-arrow-sel)' : (edge.marker ? 'url(#tg-arrow-' + edge.marker + ')' : undefined),
3253
+ }),
3254
+ edge.label ? h('text', { className: 'tg-edge-label' + (selected ? ' selected' : ''), x: edge.labelX, y: edge.labelY }, edge.label) : null,
3255
+ );
3256
+ }),
3257
+ ),
3258
+ // 阶段分组框(belongTo 的容器可视化)
3259
+ layout.stageBoxes.map(function (box) {
3260
+ var stageIndex = (graph.stages || []).findIndex(function (item) { return item.id === box.id; });
3261
+ var branchCount = (graph.branches || []).filter(function (branch) {
3262
+ var topic = byId[branch.parentTopicId];
3263
+ return topic && topic.stageId === box.id;
3264
+ }).length;
3265
+ return h('div', {
3266
+ key: 'stage-box-' + box.id,
3267
+ className: 'tg-stage-box',
3268
+ style: { left: box.x + 'px', top: box.y + 'px', width: box.w + 'px', height: box.h + 'px' },
3269
+ },
3270
+ h('div', { className: 'tg-stage-box-head' },
3271
+ h('span', {
3272
+ className: 'tg-stage-toggle',
3273
+ title: box.collapsed ? t('expandStage') : t('collapseStage'),
3274
+ onClick: function (event) { event.stopPropagation(); toggleGraphCollapse('stages', box.id); },
3275
+ }, box.collapsed ? '▸' : '▾'),
3276
+ h('span', { className: 'tg-stage-box-idx' }, String(stageIndex + 1)),
3277
+ h('span', { className: 'tg-stage-box-title' }, box.stage.title),
3278
+ box.collapsed ? h('span', { className: 'tg-stage-box-chip' }, box.topicCount + ' ' + t('topics')) : null,
3279
+ branchCount ? h('span', { className: 'tg-stage-box-chip' }, '🤖' + branchCount) : null,
3280
+ h('span', { className: 'tg-stage-box-chip' }, t('tokens') + ' ' + fmtK(((box.stage.stats || {}).tokens || {}).total || 0)),
3281
+ ),
3282
+ );
3283
+ }),
3284
+ // 子智能体分支:分支框 + 会话头 + 它自己的话题树(与主图同屏)
3285
+ (layout.branchBoxes || []).map(function (box) {
3286
+ var branch = box.branch;
3287
+ var headerText = '🤖 ' + t('subagent') + ' · ' + (branch.title || sessionShortId(branch.sessionId));
3288
+ return h('div', {
3289
+ key: 'branchbox-' + branch.sessionId,
3290
+ className: 'tg-branch-box',
3291
+ style: { left: box.x + 'px', top: box.y + 'px', width: box.w + 'px', height: box.h + 'px' },
3292
+ }, h('span', { className: 'tg-branch-box-label' }, headerText));
3293
+ }),
3294
+ layout.branchNodes.map(function (node) {
3295
+ var branch = node.branch;
3296
+ if (node.kind === 'branchHeader') {
3297
+ var totals = branch.totals || {};
3298
+ return h('div', {
3299
+ key: node.id,
3300
+ className: 'tg-branch-header' + (branch.pending ? ' pending' : ''),
3301
+ style: { left: node.x + 'px', top: node.y + 'px', width: node.w + 'px', height: node.h + 'px', zIndex: 6 },
3302
+ onMouseEnter: function (event) { setHover({ node: node, clientX: event.clientX, clientY: event.clientY }); },
3303
+ onMouseMove: function (event) { setHover({ node: node, clientX: event.clientX, clientY: event.clientY }); },
3304
+ onMouseLeave: function () { setHover(null); },
3305
+ onClick: function () { selectSession(branch.sessionId); },
3306
+ },
3307
+ node.topicCount ? h('span', {
3308
+ className: 'tg-branch-toggle',
3309
+ title: node.collapsed ? t('expandBranch') : t('collapseBranch'),
3310
+ onClick: function (event) { event.stopPropagation(); toggleGraphCollapse('branches', branch.sessionId); },
3311
+ }, node.collapsed ? '▸' : '▾') : null,
3312
+ h('span', { className: 'tg-branch-ico' }, '🤖'),
3313
+ h('span', { className: 'tg-branch-title-text' }, branch.title || sessionShortId(branch.sessionId)),
3314
+ node.collapsed ? h('span', { className: 'tg-branch-header-meta' }, node.topicCount + ' ' + t('topics')) : null,
3315
+ h('span', { className: 'tg-branch-header-meta' },
3316
+ (totals.turnCount || 0) + ' ' + t('turns') + ' · ' + fmtK((totals.tokens || {}).total || 0)),
3317
+ node.depth > 1 ? h('span', { className: 'tg-branch-depth' }, 'L' + node.depth) : null,
3318
+ );
3319
+ }
3320
+ var topic = node.topic;
3321
+ var stats = topic.stats || {};
3322
+ var tokens = stats.tokens || {};
3323
+ return h('div', {
3324
+ key: node.id,
3325
+ className: 'tg-branch-topic',
3326
+ style: { left: node.x + 'px', top: node.y + 'px', width: node.w + 'px', height: node.h + 'px', zIndex: 6 },
3327
+ onMouseEnter: function (event) { setHover({ node: node, clientX: event.clientX, clientY: event.clientY }); },
3328
+ onMouseMove: function (event) { setHover({ node: node, clientX: event.clientX, clientY: event.clientY }); },
3329
+ onMouseLeave: function () { setHover(null); },
3330
+ onClick: function () { openChildDetail(branch.sessionId, topic.id); },
3331
+ },
3332
+ h('div', { className: 'tg-branch-topic-title' }, topic.title),
3333
+ h('div', { className: 'tg-branch-topic-meta' },
3334
+ h('span', null, topic.turnStart != null ? '轮' + topic.turnStart + '-' + topic.turnEnd : ''),
3335
+ h('span', null, fmtK(tokens.total || 0)),
3336
+ h('span', null, fmtPct(stats.tokenRatio)),
3337
+ h('span', null, fmtDuration(stats.wallMs || 0)),
3338
+ ),
3339
+ h('div', { className: 'tg-node-bar' }, h('i', { style: { width: Math.max(1, (stats.tokenRatio || 0) * 100) + '%' } })),
3340
+ );
3341
+ }),
3342
+ // 主列:阶段 → 话题 → 子话题
3343
+ layout.nodes.map(function (node) {
3344
+ var topic = node.topic;
3345
+ var stats = topic.stats || {};
3346
+ var tokens = stats.tokens || {};
3347
+ var isActive = topic.status === 'active';
3348
+ var branchCount = (topic.branchIds || []).length;
3349
+ return h('div', {
3350
+ key: 'node-' + node.id,
3351
+ className: 'tg-node' + (isActive ? ' active' : ''),
3352
+ style: {
3353
+ left: node.x + 'px',
3354
+ top: node.y + 'px',
3355
+ width: node.w + 'px',
3356
+ height: node.h + 'px',
3357
+ zIndex: node.depth + 2,
3358
+ },
3359
+ onMouseEnter: function (event) { setHover({ node: node, clientX: event.clientX, clientY: event.clientY }); },
3360
+ onMouseMove: function (event) { setHover({ node: node, clientX: event.clientX, clientY: event.clientY }); },
3361
+ onMouseLeave: function () { setHover(null); },
3362
+ onClick: function () { openDetail(topic.id); },
3363
+ },
3364
+ h('div', { className: 'tg-node-title' },
3365
+ node.childCount ? h('span', {
3366
+ className: 'tg-node-toggle',
3367
+ title: node.collapsed ? t('expandTopic') : t('collapseTopic'),
3368
+ onClick: function (event) { event.stopPropagation(); toggleGraphCollapse('topics', topic.id); },
3369
+ }, node.collapsed ? '▸' : '▾') : null,
3370
+ h('span', { className: 'tg-node-title-text' }, topic.title),
3371
+ ),
3372
+ h('div', { className: 'tg-node-meta' },
3373
+ h('span', null, (topic.turnStart != null ? '轮' + topic.turnStart + '-' + topic.turnEnd : '')),
3374
+ node.collapsed && node.childCount ? h('span', { className: 'tg-node-depth-tag' }, node.childCount + ' ' + t('subtopics')) : null,
3375
+ node.depth > 0 ? h('span', { className: 'tg-node-depth-tag' }, 'L' + (node.depth + 1)) : null,
3376
+ branchCount ? h('span', { className: 'tg-node-depth-tag', style: { color: 'var(--dsw-alias-state-warn-primary,#ca8a04)' } }, '🤖' + branchCount) : null,
3377
+ h('span', null, fmtK(tokens.total)),
3378
+ h('span', null, fmtPct(stats.tokenRatio)),
3379
+ h('span', null, t('toolCalls') + ' ' + String((stats.toolCalls && stats.toolCalls.total) || 0)),
3380
+ h('span', null, fmtDuration(stats.wallMs)),
3381
+ ),
3382
+ h('div', { className: 'tg-node-bar' }, h('i', { style: { width: Math.max(1, (stats.tokenRatio || 0) * 100) + '%' } })),
3383
+ );
3384
+ }),
3385
+ ),
3386
+ ),
3387
+ ),
3388
+ renderGraphTooltip(),
3389
+ );
3390
+ }
3391
+
3392
+ // ── 渲染:会话选择器 ────────────────────────────────────────────────
3393
+ // ── 渲染:整体 ─────────────────────────────────────────────────────
3394
+ var styleElement = h('style', { key: 'tg-style' }, CSS);
3395
+
3396
+ var totals = (graph && graph.totals) || null;
3397
+ var ai = (graph && graph.ai) || {};
3398
+ var statusClass = ai.status === 'running' ? 'running' : (ai.error ? 'error' : 'ok');
3399
+ var subagentBranchCount = totals && totals.subagent ? (totals.subagent.branchCount || 0) : 0;
3400
+ var subagentPending = (graph && graph.branches ? graph.branches : []).filter(function (branch) {
3401
+ return branch.pending || (branch.ai && (branch.ai.status === 'running' || branch.ai.pendingTurns > 0));
3402
+ }).length;
3403
+ var statusText = ai.status === 'running'
3404
+ ? t('segmenting')
3405
+ : (ai.error ? (t('aiFailed') + ':' + ai.error)
3406
+ : (ai.pendingTurns > 0 ? t('pendingTurns', { n: ai.pendingTurns })
3407
+ : (subagentPending > 0 ? t('segmenting') + ' · 🤖' + subagentPending
3408
+ : t('updatedAt', { time: fmtClock(Date.parse(graph && graph.updatedAt) || 0) })
3409
+ + (ai.relationError ? ' · ' + t('statRelations') + ':' + summarizeText(ai.relationError, 40) : ''))));
3410
+
3411
+ var modelSelect = h('select', {
3412
+ className: 'tg-model-select',
3413
+ value: modelSel,
3414
+ title: t('modelLabel'),
3415
+ onChange: function (event) { onModelChange(event.target.value); },
3416
+ },
3417
+ h('option', { value: '' }, t('modelDefault')),
3418
+ modelProviders.map(function (p) {
3419
+ return h('optgroup', { key: p.id, label: p.name || p.id },
3420
+ (p.models || []).map(function (m) {
3421
+ return h('option', { key: p.id + ':' + m.id, value: p.id + ':' + m.id }, m.name || m.id);
3422
+ }));
3423
+ }),
3424
+ );
3425
+
3426
+ return h(React.Fragment, null, styleElement,
3427
+ h('div', { className: 'tg-root tg-root-docked', ref: panelRef },
3428
+ h('div', { className: 'tg-topbar' },
3429
+ h('div', { className: 'tg-topbar-row' },
3430
+ h('span', { className: 'tg-view-toggle' },
3431
+ h('button', {
3432
+ className: 'tg-view-btn' + (ui.tab === 'chain' ? ' active' : ''),
3433
+ onClick: function () { setUi(function (prev) { return Object.assign({}, prev, { tab: 'chain' }); }); },
3434
+ title: t('tabChain'),
3435
+ }, t('viewWorkflow')),
3436
+ h('button', {
3437
+ className: 'tg-view-btn' + (ui.tab === 'graph' ? ' active' : ''),
3438
+ onClick: function () { setUi(function (prev) { return Object.assign({}, prev, { tab: 'graph' }); }); },
3439
+ title: t('tabGraph'),
3440
+ }, t('viewGraph')),
3441
+ ),
3442
+ h('span', { className: 'tg-topbar-spacer' }),
3443
+ h('span', { className: 'tg-dot ' + statusClass }),
3444
+ modelSelect,
3445
+ h('button', { className: 'tg-btn', title: t('refresh'), onClick: function () { versionRef.current = null; setReloadKey(function (n) { return n + 1; }); } }, '↻'),
3446
+ h('button', { className: 'tg-btn', disabled: actionBusy, onClick: function () { void triggerSegment(true); } }, t('resegment')),
3447
+ h('button', { className: 'tg-btn primary', disabled: actionBusy, onClick: function () { void triggerSegment(false); } }, t('segNow')),
3448
+ ),
3449
+ renderStatsStrip(),
3450
+ ),
3451
+ error ? h('div', { className: 'tg-banner' },
3452
+ h('span', { className: 'tg-banner-text', title: error }, error),
3453
+ h('button', { onClick: function () { versionRef.current = null; setError(''); setReloadKey(function (n) { return n + 1; }); } }, t('retry')),
3454
+ ) : null,
3455
+ ai.error && !error ? h('div', { className: 'tg-banner' },
3456
+ h('span', { className: 'tg-banner-text', title: ai.error }, t('aiFailed') + ':' + ai.error),
3457
+ h('button', { onClick: function () { void triggerSegment(false); } }, t('retry')),
3458
+ ) : null,
3459
+ h('div', { className: 'tg-body' },
3460
+ ui.tab === 'chain'
3461
+ ? h('div', { className: 'tg-workflow' },
3462
+ h('div', { className: 'tg-workflow-tree' }, renderWorkflowTree()),
3463
+ h('div', { className: 'tg-workflow-detail', ref: wfDetailRef }, renderWorkflowDetail()),
3464
+ )
3465
+ : h('div', { className: 'tg-graph' }, renderGraph(), renderDetail(false)),
3466
+ ),
3467
+ h('div', { className: 'tg-foot' },
3468
+ ai.status === 'running'
3469
+ ? h('div', { className: 'tg-progress', title: Math.round((ai.progress || 0) * 100) + '%' },
3470
+ h('i', { style: { width: Math.max(2, Math.round((ai.progress || 0) * 100)) + '%' } }))
3471
+ : null,
3472
+ h('span', { className: 'tg-foot-status' + (ai.status === 'running' ? ' running' : (ai.error ? ' error' : '')) }, statusText),
3473
+ h('span', { className: 'tg-version', title: 'client bundle v' + CLIENT_VERSION }, 'v' + CLIENT_VERSION),
3474
+ ),
3475
+ ),
3476
+ );
3477
+ }
3478
+
3479
+ // ── 导出 ────────────────────────────────────────────────────────────────
3480
+
3481
+ exports.name = 'dsh-topic-graph';
3482
+ exports.inject = ['slots'];
3483
+ // 仅供本地测试的纯函数钩子(Loader 只读取 name/inject/apply)
3484
+ exports.__test = { buildLayout, buildTimelineLayout, loadUi, defaultPanelWidth, readSidebarCurrentSession };
3485
+ exports.apply = function (ctx) {
3486
+ appCtx = ctx;
3487
+ initLocale(ctx);
3488
+ // 停靠在「对话 / 轨迹」之后的新视图(与 dsh-context 同款 conversation.view 注册)
3489
+ return ctx.slots.inject('conversation.view', function () {
3490
+ return ctx.slots.register({
3491
+ name: 'conversation.view',
3492
+ id: 'topic-graph',
3493
+ order: 20,
3494
+ locale: LOC_NS,
3495
+ label: function () { return t('panelTitle'); },
3496
+ }, TopicGraphPanel);
3497
+ });
3498
+ };
3499
+
3500
+ return module.exports;
3501
+ },
3502
+ });