oh-my-knowledge 0.51.2 → 0.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -6
- package/README.zh.md +27 -6
- package/dist/assets/agent-skills/omk/SKILL.md +8 -8
- package/dist/observability/codex-conversation-index.d.ts +88 -0
- package/dist/observability/codex-conversation-index.js +566 -0
- package/dist/observability/codex-protocol.d.ts +12 -0
- package/dist/observability/codex-protocol.js +78 -0
- package/dist/observability/codex-tool-status.d.ts +16 -0
- package/dist/observability/codex-tool-status.js +114 -0
- package/dist/observability/codex-trace-adapter.js +342 -119
- package/dist/observability/conversation-catalog.d.ts +38 -0
- package/dist/observability/conversation-catalog.js +573 -0
- package/dist/observability/conversation-index-process.d.ts +1 -0
- package/dist/observability/conversation-index-process.js +31 -0
- package/dist/observability/conversation-view-model.d.ts +5 -0
- package/dist/observability/conversation-view-model.js +100 -0
- package/dist/observability/experience.d.ts +3 -1
- package/dist/observability/experience.js +248 -7
- package/dist/observability/inbox.js +40 -2
- package/dist/observability/knowledge-debugger.d.ts +4 -0
- package/dist/observability/knowledge-debugger.js +364 -0
- package/dist/observability/polling-subscription-hub.d.ts +27 -0
- package/dist/observability/polling-subscription-hub.js +149 -0
- package/dist/observability/source-record-archive.d.ts +13 -0
- package/dist/observability/source-record-archive.js +324 -0
- package/dist/observability/task-window.d.ts +8 -0
- package/dist/observability/task-window.js +36 -0
- package/dist/observability/trace-ir.d.ts +66 -1
- package/dist/observability/trace-source.d.ts +1 -0
- package/dist/observability/trace-source.js +12 -5
- package/dist/observability/turn-index.d.ts +3 -0
- package/dist/observability/turn-index.js +251 -0
- package/dist/renderer/conversation-renderer.d.ts +9 -0
- package/dist/renderer/conversation-renderer.js +410 -0
- package/dist/renderer/icons.js +1 -0
- package/dist/renderer/inline-markdown.d.ts +6 -0
- package/dist/renderer/inline-markdown.js +158 -0
- package/dist/renderer/knowledge-debugger-renderer.d.ts +9 -0
- package/dist/renderer/knowledge-debugger-renderer.js +2280 -0
- package/dist/renderer/observation-inbox-renderer.js +9 -0
- package/dist/renderer/skill-list-renderer.js +2 -1
- package/dist/renderer/trajectory-live.d.ts +42 -0
- package/dist/renderer/trajectory-live.js +258 -0
- package/dist/renderer/trajectory-routing.d.ts +60 -0
- package/dist/renderer/trajectory-routing.js +351 -0
- package/dist/server/report-server.d.ts +4 -1
- package/dist/server/report-server.js +274 -6
- package/dist/types/observability.d.ts +225 -1
- package/package.json +8 -5
|
@@ -0,0 +1,2280 @@
|
|
|
1
|
+
import { inlineMarkdownText, renderSafeInlineMarkdown } from './inline-markdown.js';
|
|
2
|
+
import { icon } from './icons.js';
|
|
3
|
+
import { DEFAULT_LANG, e, layout } from './layout.js';
|
|
4
|
+
import { renderTrajectoryLiveClientSource } from './trajectory-live.js';
|
|
5
|
+
import { renderTrajectoryRoutingClientSource } from './trajectory-routing.js';
|
|
6
|
+
const ACCESS_LABELS = {
|
|
7
|
+
injected: { zh: '已注入', en: 'Injected' },
|
|
8
|
+
read: { zh: '已读取', en: 'Read' },
|
|
9
|
+
returned: { zh: '工具返回', en: 'Tool return' },
|
|
10
|
+
};
|
|
11
|
+
const STEP_LABELS = {
|
|
12
|
+
user_request: { zh: '用户消息', en: 'User message' },
|
|
13
|
+
user_message: { zh: '用户补充', en: 'User follow-up' },
|
|
14
|
+
user_correction: { zh: '用户纠正', en: 'User correction' },
|
|
15
|
+
runtime_context: { zh: '任务上下文', en: 'Task context' },
|
|
16
|
+
skill_context: { zh: 'Skill 上下文', en: 'Skill context' },
|
|
17
|
+
tool_exchange: { zh: '工具调用', en: 'Tool call' },
|
|
18
|
+
unmatched_tool_result: { zh: '未配对工具结果', en: 'Unmatched tool result' },
|
|
19
|
+
assistant_message: { zh: 'AI 回答', en: 'AI response' },
|
|
20
|
+
model_activity: { zh: '模型思考', en: 'Model reasoning' },
|
|
21
|
+
lifecycle: { zh: '运行状态', en: 'Lifecycle' },
|
|
22
|
+
observation: { zh: '观测事件', en: 'Observation' },
|
|
23
|
+
system_event: { zh: '系统事件', en: 'System event' },
|
|
24
|
+
};
|
|
25
|
+
const INTEGRITY_LABELS = {
|
|
26
|
+
task_boundary_unavailable: (_count, lang) => lang === 'zh' ? '无法从日志判定本次任务边界' : 'Task boundary could not be resolved from the trace',
|
|
27
|
+
timeline_truncated: (count, lang) => lang === 'zh' ? `语义轨迹省略 ${count} 个规范化事件` : `${count} normalized event${count === 1 ? '' : 's'} omitted from the semantic trajectory`,
|
|
28
|
+
malformed_records: (count, lang) => lang === 'zh' ? `${count} 条格式损坏记录` : `${count} malformed record${count === 1 ? '' : 's'}`,
|
|
29
|
+
ignored_values: (count, lang) => lang === 'zh' ? `${count} 个非对象值被忽略` : `${count} non-object value${count === 1 ? '' : 's'} ignored`,
|
|
30
|
+
unknown_events: (count, lang) => lang === 'zh' ? `${count} 个事件无法识别` : `${count} unrecognized event${count === 1 ? '' : 's'}`,
|
|
31
|
+
unmatched_tool_calls: (count, lang) => lang === 'zh' ? `${count} 次工具调用缺少结果` : `${count} tool call${count === 1 ? '' : 's'} without results`,
|
|
32
|
+
unmatched_tool_results: (count, lang) => lang === 'zh' ? `${count} 条工具结果无法配对` : `${count} tool result${count === 1 ? '' : 's'} could not be paired`,
|
|
33
|
+
missing_timestamps: (count, lang) => lang === 'zh' ? `${count} 个事件缺少时间戳` : `${count} event${count === 1 ? '' : 's'} without timestamps`,
|
|
34
|
+
};
|
|
35
|
+
const REPLAY_CARD_WIDTH = 190;
|
|
36
|
+
const REPLAY_CARD_MEDIUM_WIDTH = 154;
|
|
37
|
+
const REPLAY_CARD_SMALL_WIDTH = 118;
|
|
38
|
+
const REPLAY_COMPACT_CARD_WIDTH = 14;
|
|
39
|
+
const OPERATION_LANE_GAP = 20;
|
|
40
|
+
const OPERATION_FLOW_ADVANCE = 28;
|
|
41
|
+
const COMPACT_FLOW_ADVANCE = 18;
|
|
42
|
+
const TRACK_START_PADDING = 16;
|
|
43
|
+
const AXIS_TICK_PADDING = 7;
|
|
44
|
+
const AXIS_TICK_GLYPH_WIDTH = 5.5;
|
|
45
|
+
const AXIS_TICK_CLEARANCE = 8;
|
|
46
|
+
const LANE_LABELS = {
|
|
47
|
+
conversation: {
|
|
48
|
+
zh: { title: '对话', detail: '用户与 AI', empty: '未观测到对话消息' },
|
|
49
|
+
en: { title: 'Conversation', detail: 'User and AI', empty: 'No conversation messages observed' },
|
|
50
|
+
},
|
|
51
|
+
knowledge: {
|
|
52
|
+
zh: { title: '知识', detail: '何时、从何处出现', empty: '未识别到知识' },
|
|
53
|
+
en: { title: 'Knowledge', detail: 'When and where it appeared', empty: 'No Knowledge identified' },
|
|
54
|
+
},
|
|
55
|
+
action: {
|
|
56
|
+
zh: { title: '执行', detail: 'AI 发起的工具调用', empty: '未观测到工具调用' },
|
|
57
|
+
en: { title: 'Actions', detail: 'AI-initiated tool calls', empty: 'No tool calls observed' },
|
|
58
|
+
},
|
|
59
|
+
result: {
|
|
60
|
+
zh: { title: '结果', detail: '工具返回及调用状态', empty: '未观测到工具结果' },
|
|
61
|
+
en: { title: 'Results', detail: 'Tool returns and call status', empty: 'No tool results observed' },
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
export function renderKnowledgeDebuggerPage(model, lang = DEFAULT_LANG, options = {}) {
|
|
65
|
+
const zh = lang === 'zh';
|
|
66
|
+
const projection = projectReplay(model, lang, { pendingToolResults: Boolean(options.live) });
|
|
67
|
+
const pageTitle = model.summary.userGoal ?? (zh ? '未识别到用户要求' : 'No user request identified');
|
|
68
|
+
const normalizedEvents = model.normalizedEvents;
|
|
69
|
+
const normalizedEventCount = normalizedEvents.length;
|
|
70
|
+
const sourceRecordCount = model.sourceRecords.recordCount;
|
|
71
|
+
const sourceLabel = displaySourceKind(model.session.sourceKind);
|
|
72
|
+
const observedModels = model.summary.observedModels;
|
|
73
|
+
const observedModelLabel = observedModels.length === 1
|
|
74
|
+
? observedModels[0]
|
|
75
|
+
: observedModels.length > 1
|
|
76
|
+
? (zh ? `${observedModels.length} 个模型` : `${observedModels.length} models`)
|
|
77
|
+
: undefined;
|
|
78
|
+
const toolCallCount = model.summary.toolCallCount;
|
|
79
|
+
const toolFailureCount = model.summary.toolFailureCount;
|
|
80
|
+
const trajectoryRoutingClientSource = renderTrajectoryRoutingClientSource();
|
|
81
|
+
const trajectoryLiveClientSource = renderTrajectoryLiveClientSource();
|
|
82
|
+
return layout(zh ? '任务轨迹' : 'Task Trajectory', `
|
|
83
|
+
<style>
|
|
84
|
+
html{height:100%;overflow:hidden;scrollbar-gutter:auto}
|
|
85
|
+
body{height:100dvh;min-height:0;overflow:hidden;display:grid;grid-template-rows:44px minmax(0,1fr)}
|
|
86
|
+
.app-bar{position:relative;height:44px;padding:0 18px}
|
|
87
|
+
.app-main{width:100%;height:100%;min-height:0;max-width:none;margin:0;padding:10px 18px 14px;overflow:hidden}
|
|
88
|
+
.footer{display:none!important}
|
|
89
|
+
.trajectory-shell{height:100%;min-height:0;display:flex;flex-direction:column;margin:0;padding:0;letter-spacing:0}
|
|
90
|
+
.trajectory-mode{display:inline-flex;padding:2px;border:1px solid var(--border);border-radius:7px;background:var(--bg-elevated)}
|
|
91
|
+
.trajectory-mode button{height:28px;padding:3px 11px;border:0;border-radius:5px;background:transparent;color:var(--text-secondary);font:500 12px/1.4 inherit;letter-spacing:0;cursor:pointer}
|
|
92
|
+
.trajectory-mode button[aria-pressed="true"]{background:var(--text-primary);color:var(--bg-surface);box-shadow:0 1px 2px rgba(24,32,51,.14)}
|
|
93
|
+
.trajectory-mode button:focus-visible,.trajectory-event:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
|
|
94
|
+
.trajectory-heading{flex:none;display:grid;grid-template-columns:minmax(0,1fr) auto;gap:24px;align-items:center;min-height:32px;margin:0 0 8px}
|
|
95
|
+
.trajectory-heading h1{min-width:0;margin:0;overflow:hidden;font-size:21px;font-weight:650;line-height:1.28;letter-spacing:0;text-overflow:ellipsis;white-space:nowrap}
|
|
96
|
+
.trajectory-meta{display:flex;min-width:0;align-items:center;justify-content:flex-end;gap:0;color:var(--text-secondary);font-size:11px;white-space:nowrap}
|
|
97
|
+
.trajectory-meta span{display:inline-flex;align-items:center}
|
|
98
|
+
.trajectory-meta span+span:before{content:"";width:3px;height:3px;margin:0 10px;border-radius:50%;background:var(--border-hover)}
|
|
99
|
+
.trajectory-meta .trajectory-meta-source{color:var(--accent);font-weight:650}
|
|
100
|
+
.trajectory-meta .trajectory-meta-model{color:var(--text-primary);font-weight:600}
|
|
101
|
+
.trajectory-meta .is-failure{color:var(--red);font-weight:600}
|
|
102
|
+
.trajectory-warning{flex:none;margin:0 0 8px;padding:7px 10px;border-left:2px solid var(--yellow);background:rgba(217,119,6,.06);color:var(--text-secondary);font-size:10px;line-height:1.45}
|
|
103
|
+
.trajectory-warning strong{color:var(--text-primary)}
|
|
104
|
+
.trajectory-frame{flex:1;min-height:0;overflow:hidden;display:grid;grid-template-rows:40px minmax(0,1fr);border:1px solid var(--border);border-radius:8px;background:var(--bg-surface);box-shadow:var(--shadow-sm)}
|
|
105
|
+
.trajectory-frame-head{display:flex;align-items:center;min-height:0;padding:0 10px 0 14px;border-bottom:1px solid var(--border)}
|
|
106
|
+
.trajectory-frame-head h2{margin:0;font-size:13px;font-weight:600;letter-spacing:0}
|
|
107
|
+
.trajectory-live-controls{display:inline-flex;align-items:center;gap:3px;margin-left:8px}
|
|
108
|
+
.trajectory-live-state{display:inline-flex;align-items:center;gap:5px;color:var(--text-muted);font-size:9px;white-space:nowrap}
|
|
109
|
+
.trajectory-live-state:before{content:"";width:5px;height:5px;border-radius:50%;background:var(--green);box-shadow:0 0 0 3px rgba(31,157,99,.1)}
|
|
110
|
+
.trajectory-live-state[data-state="syncing"]:before{background:var(--accent);box-shadow:0 0 0 3px rgba(79,70,229,.1)}
|
|
111
|
+
.trajectory-live-state[data-state="reconnecting"]:before{background:var(--yellow);box-shadow:0 0 0 3px rgba(217,119,6,.1)}
|
|
112
|
+
.trajectory-live-follow{display:inline-flex;height:22px;align-items:center;gap:4px;padding:0 6px;border:1px solid transparent;border-radius:4px;background:transparent;color:var(--text-muted);font:500 9px/1 inherit;letter-spacing:0;cursor:pointer;white-space:nowrap}
|
|
113
|
+
.trajectory-live-follow svg{width:11px;height:11px;fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}
|
|
114
|
+
.trajectory-live-follow:hover{border-color:var(--border);background:var(--bg-elevated);color:var(--text-primary)}
|
|
115
|
+
.trajectory-live-follow:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
|
|
116
|
+
.trajectory-live-follow[data-state="following"]{color:var(--accent)}
|
|
117
|
+
.trajectory-live-follow[data-state="pending"],.trajectory-live-follow[data-state="completed"]{border-color:rgba(79,70,229,.2);background:rgba(79,70,229,.07);color:var(--accent)}
|
|
118
|
+
.trajectory-live-follow[data-state="aborted"],.trajectory-live-follow[data-state="interrupted"],.trajectory-live-follow[data-state="unknown"]{border-color:var(--border);background:var(--bg-elevated);color:var(--text-muted)}
|
|
119
|
+
.trajectory-frame-space{flex:1}
|
|
120
|
+
.trajectory-range{color:var(--text-secondary);font:400 10px/1 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-variant-numeric:tabular-nums}
|
|
121
|
+
.trajectory-boundary-info{position:relative;margin-left:4px}
|
|
122
|
+
.trajectory-boundary-info summary{display:grid;width:20px;height:20px;place-items:center;border:0;border-radius:3px;background:transparent;color:var(--text-muted);cursor:pointer;list-style:none}
|
|
123
|
+
.trajectory-boundary-info summary::-webkit-details-marker{display:none}
|
|
124
|
+
.trajectory-boundary-info summary svg{width:14px;height:14px;fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}
|
|
125
|
+
.trajectory-boundary-info summary:hover,.trajectory-boundary-info[open] summary{color:var(--text-primary)}
|
|
126
|
+
.trajectory-boundary-info summary:focus-visible{color:var(--text-primary);outline:2px solid var(--accent);outline-offset:1px}
|
|
127
|
+
.trajectory-boundary-popover{position:absolute;top:27px;left:0;z-index:20;width:min(430px,78vw);padding:12px 14px;border:1px solid var(--border);border-radius:7px;background:var(--bg-surface);color:var(--text-secondary);font-size:10px;line-height:1.55;box-shadow:var(--shadow-md)}
|
|
128
|
+
.trajectory-boundary-popover h3{margin:0 0 5px;color:var(--text-primary);font-size:11px;font-weight:650}
|
|
129
|
+
.trajectory-boundary-flow{margin:0 0 9px;color:var(--accent);font:600 10px/1.5 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace}
|
|
130
|
+
.trajectory-boundary-levels{display:grid;gap:7px;margin:0}
|
|
131
|
+
.trajectory-boundary-levels div{display:grid;grid-template-columns:76px minmax(0,1fr);gap:8px}
|
|
132
|
+
.trajectory-boundary-levels dt{color:var(--text-primary);font-weight:650}
|
|
133
|
+
.trajectory-boundary-levels dd{margin:0}
|
|
134
|
+
.trajectory-boundary-note{margin:10px 0 0;padding-top:9px;border-top:1px solid var(--border);color:var(--text-muted)}
|
|
135
|
+
.trajectory-focus{position:relative;margin-left:8px}
|
|
136
|
+
.trajectory-focus summary{position:relative;display:grid;width:28px;height:28px;place-items:center;border:0;border-radius:5px;background:transparent;color:var(--text-secondary);cursor:pointer;list-style:none}
|
|
137
|
+
.trajectory-focus summary::-webkit-details-marker{display:none}
|
|
138
|
+
.trajectory-focus summary svg{width:15px;height:15px;fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}
|
|
139
|
+
.trajectory-focus summary:hover,.trajectory-focus[open] summary,.trajectory-focus.has-active summary{background:var(--bg-elevated);color:var(--text-primary)}
|
|
140
|
+
.trajectory-focus summary:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
|
|
141
|
+
.trajectory-focus.has-active summary:after{content:"";position:absolute;top:4px;right:4px;width:4px;height:4px;border-radius:50%;background:var(--accent)}
|
|
142
|
+
.trajectory-focus-menu{position:absolute;top:34px;right:0;z-index:24;width:200px;padding:7px;border:1px solid var(--border);border-radius:7px;background:var(--bg-surface);box-shadow:var(--shadow-md)}
|
|
143
|
+
.trajectory-focus-title{display:block;padding:3px 7px 6px;color:var(--text-muted);font-size:9px;font-weight:600}
|
|
144
|
+
.trajectory-focus-option{display:grid;width:100%;grid-template-columns:8px minmax(0,1fr) auto;gap:8px;align-items:center;padding:6px 7px;border:0;border-radius:5px;background:transparent;color:var(--text-primary);font:500 11px/1.35 inherit;text-align:left;cursor:pointer}
|
|
145
|
+
.trajectory-focus-option:hover,.trajectory-focus-option[aria-pressed="true"]{background:var(--bg-elevated)}
|
|
146
|
+
.trajectory-focus-option:focus-visible{outline:2px solid var(--accent);outline-offset:-1px}
|
|
147
|
+
.trajectory-focus-option small{color:var(--text-muted);font-size:9px;font-weight:500}
|
|
148
|
+
.trajectory-focus-swatch{width:6px;height:6px;border-radius:50%;background:var(--text-muted)}
|
|
149
|
+
.trajectory-focus-option[data-facet-group="knowledge"] .trajectory-focus-swatch{background:var(--accent)}
|
|
150
|
+
.trajectory-focus-option[data-facet-group="tool"] .trajectory-focus-swatch{background:var(--yellow)}
|
|
151
|
+
.trajectory-focus-option[data-facet-group="status"] .trajectory-focus-swatch{background:var(--red)}
|
|
152
|
+
.trajectory-frame-head .trajectory-mode{margin-left:8px}
|
|
153
|
+
.trajectory-body{min-width:0;min-height:0;display:grid;grid-template-columns:minmax(0,1fr) 0;align-items:stretch;transition:grid-template-columns .18s cubic-bezier(.2,.8,.2,1)}
|
|
154
|
+
.trajectory-shell[data-inspector-open="true"] .trajectory-body{grid-template-columns:minmax(0,1fr) clamp(320px,27vw,400px)}
|
|
155
|
+
.trajectory-shell[data-inspector-restoring="true"] .trajectory-body{transition:none}
|
|
156
|
+
.trajectory-scroll{min-width:0;min-height:0;overflow-x:auto;overflow-y:hidden;overscroll-behavior-x:contain;outline-offset:-2px;scrollbar-color:var(--border-hover) transparent;scrollbar-width:thin}
|
|
157
|
+
.trajectory-scroll:focus-visible{outline:2px solid var(--accent)}
|
|
158
|
+
.trajectory-canvas{--lane-label-width:108px;--event-width:clamp(166px,15vw,190px);width:max(100%,var(--timeline-detail-width));height:100%;min-width:var(--timeline-detail-width);display:grid;grid-template-rows:32px minmax(0,1fr)}
|
|
159
|
+
.trajectory-axis,.trajectory-lane{display:grid;grid-template-columns:var(--lane-label-width) minmax(0,1fr)}
|
|
160
|
+
.trajectory-axis{min-height:0;border-bottom:1px solid var(--border);background:var(--bg-elevated)}
|
|
161
|
+
.trajectory-axis-label{position:sticky;left:0;z-index:6;display:flex;align-items:center;padding-left:14px;border-right:1px solid var(--border);background:var(--bg-elevated);color:var(--text-muted);font-size:10px}
|
|
162
|
+
.trajectory-ticks{position:relative;min-width:0;color:var(--text-muted);font:400 9px/1 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-variant-numeric:tabular-nums}
|
|
163
|
+
.trajectory-tick{position:absolute;top:0;bottom:0;display:flex;align-items:center;padding-left:7px;border-left:1px solid var(--border);white-space:nowrap}
|
|
164
|
+
.trajectory-gap-axis{position:absolute;top:4px;height:24px;display:flex;align-items:center;justify-content:center;border-right:1px dashed var(--border-hover);border-left:1px dashed var(--border-hover);background:rgba(246,248,252,.94);color:var(--text-secondary);font-size:8px;white-space:nowrap}
|
|
165
|
+
.trajectory-milestone-axis{position:absolute;top:4px;z-index:2;display:flex;height:24px;align-items:center;gap:5px;padding:0 7px;border-left:2px solid var(--accent);background:rgba(246,248,252,.96);color:var(--text-secondary);white-space:nowrap}
|
|
166
|
+
.trajectory-milestone-axis time{font-size:8px}
|
|
167
|
+
.trajectory-milestone-axis strong{color:var(--text-primary);font-size:9px;font-weight:650}
|
|
168
|
+
.trajectory-milestone-axis.is-end{border-right:2px solid var(--green);border-left:0;transform:translateX(-100%)}
|
|
169
|
+
.trajectory-milestone-axis.is-warning{border-left-color:var(--yellow)}
|
|
170
|
+
.trajectory-milestone-axis.is-neutral{border-left-color:var(--border-hover)}
|
|
171
|
+
.trajectory-lanes{position:relative;min-height:0;display:grid;grid-template-rows:repeat(4,minmax(0,1fr))}
|
|
172
|
+
.trajectory-guides{position:absolute;inset:0 0 0 var(--lane-label-width);z-index:0;pointer-events:none}
|
|
173
|
+
.trajectory-guide{position:absolute;top:0;bottom:0;border-left:1px solid rgba(228,232,241,.72)}
|
|
174
|
+
.trajectory-gap-band{position:absolute;top:0;bottom:0;border-right:1px dashed rgba(148,163,184,.42);border-left:1px dashed rgba(148,163,184,.42);background:rgba(246,248,252,.62)}
|
|
175
|
+
.trajectory-milestone-line{position:absolute;top:0;bottom:0;border-left:1px dashed rgba(79,70,229,.48)}
|
|
176
|
+
.trajectory-milestone-line.is-end{border-left-color:rgba(31,157,99,.52)}
|
|
177
|
+
.trajectory-milestone-line.is-warning{border-left-color:rgba(217,119,6,.58)}
|
|
178
|
+
.trajectory-milestone-line.is-neutral{border-left-color:rgba(148,163,184,.48)}
|
|
179
|
+
.trajectory-links{position:absolute;inset:0;z-index:1;width:100%;height:100%;overflow:visible;pointer-events:none}
|
|
180
|
+
.trajectory-link{fill:none;opacity:.38;stroke:rgba(99,112,131,.58);stroke-width:1.15;stroke-linecap:round;stroke-linejoin:round;vector-effect:non-scaling-stroke}
|
|
181
|
+
.trajectory-link.is-flow{opacity:.28;stroke:rgba(100,116,139,.66);stroke-width:1.05}
|
|
182
|
+
.trajectory-link.is-call-result{opacity:.56;stroke:rgba(71,85,105,.72);stroke-width:1.2}
|
|
183
|
+
.trajectory-link.is-knowledge{stroke:rgba(79,70,229,.46);stroke-width:1.1;stroke-dasharray:4 4}
|
|
184
|
+
.trajectory-link.is-active{opacity:1;stroke-width:1.45}
|
|
185
|
+
.trajectory-link-arrow{fill:rgba(99,112,131,.7);opacity:.4;stroke:none}
|
|
186
|
+
.trajectory-link-arrow.is-flow{fill:rgba(100,116,139,.72);opacity:.36}
|
|
187
|
+
.trajectory-link-arrow.is-call-result{fill:rgba(71,85,105,.76);opacity:.58}
|
|
188
|
+
.trajectory-link-arrow.is-active{opacity:1}
|
|
189
|
+
.trajectory-lane{position:relative;z-index:2;min-height:0;overflow:visible;border-bottom:1px solid var(--border);pointer-events:none}
|
|
190
|
+
.trajectory-lane:last-child{border-bottom:0}
|
|
191
|
+
.trajectory-lane-name{position:sticky;left:0;z-index:6;display:flex;flex-direction:column;justify-content:center;gap:2px;padding:9px 12px;border-right:1px solid var(--border);background:rgba(255,255,255,.96)}
|
|
192
|
+
.trajectory-lane-name strong{font-size:13px;font-weight:650}
|
|
193
|
+
.trajectory-lane-name span{color:var(--text-muted);font-size:10px;line-height:1.35}
|
|
194
|
+
.trajectory-track{position:relative;min-width:0;min-height:0;height:100%}
|
|
195
|
+
.trajectory-empty{position:absolute;left:14px;top:50%;color:var(--text-faint);font-size:10px;transform:translateY(-50%)}
|
|
196
|
+
.trajectory-event{position:absolute;left:var(--event-x);top:50%;z-index:3;display:flex;flex-direction:column;gap:2px;width:var(--event-card-width,var(--event-width));height:56px;overflow:hidden;padding:6px 7px;border:1px solid var(--border-hover);border-left:3px solid var(--event-color,var(--text-secondary));border-radius:6px;background:var(--event-bg,var(--bg-surface));color:var(--text-primary);text-align:left;cursor:pointer;pointer-events:auto;box-shadow:0 2px 8px rgba(31,41,55,.04);transform:translateY(-50%);transition:opacity .16s,border-color .16s,box-shadow .16s,transform .16s}
|
|
197
|
+
.trajectory-event.is-compact{top:50%;width:14px;height:14px;padding:0;border:1.5px solid var(--event-color);border-radius:50%;background:var(--bg-surface);box-shadow:0 0 0 3px var(--bg-surface);transform:translate(-50%,-50%)}
|
|
198
|
+
.trajectory-event.is-compact .trajectory-event-head,
|
|
199
|
+
.trajectory-event.is-compact .trajectory-event-body{display:none}
|
|
200
|
+
.trajectory-event.is-compact:hover{border-color:var(--text-primary);background:var(--bg-elevated);transform:translate(-50%,-50%) scale(1.08)}
|
|
201
|
+
.trajectory-event.is-compact.is-related{border-color:var(--accent);background:rgba(79,70,229,.08)}
|
|
202
|
+
.trajectory-event:hover{z-index:5;transform:translateY(-50%) translateY(-1px)}
|
|
203
|
+
.trajectory-event.is-dimmed{opacity:.76}
|
|
204
|
+
.trajectory-event.is-related{z-index:4;border-color:var(--event-color,var(--accent));box-shadow:0 0 0 1px var(--event-ring,rgba(79,70,229,.2)),0 4px 12px rgba(31,41,55,.06)}
|
|
205
|
+
.trajectory-event.is-primary{box-shadow:0 0 0 2px var(--event-ring,rgba(79,70,229,.28)),0 6px 14px rgba(31,41,55,.08)}
|
|
206
|
+
.trajectory-event-head{display:flex;min-width:0;align-items:center;gap:5px}
|
|
207
|
+
.trajectory-event-time{flex:0 0 auto;color:var(--event-color,var(--text-secondary));font:500 9px/1.2 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-variant-numeric:tabular-nums;white-space:nowrap}
|
|
208
|
+
.trajectory-event-body{display:block;min-width:0;width:100%}
|
|
209
|
+
.trajectory-event-kind{display:block;min-width:0;overflow:hidden;color:var(--text-muted);font:500 8px/1.25 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;text-overflow:ellipsis;white-space:nowrap}
|
|
210
|
+
.trajectory-event-model{min-width:0;overflow:hidden;color:var(--text-muted);font:500 8px/1.25 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;text-overflow:ellipsis;white-space:nowrap}
|
|
211
|
+
.trajectory-event-title{display:-webkit-box;overflow:hidden;font-size:11px;font-weight:650;line-height:1.3;-webkit-box-orient:vertical;-webkit-line-clamp:2;white-space:normal}
|
|
212
|
+
.trajectory-event-title strong{font-weight:750}
|
|
213
|
+
.trajectory-event-title em{font-style:italic}
|
|
214
|
+
.trajectory-event-title code{padding:1px 3px;border-radius:3px;background:rgba(99,112,131,.1);font:500 .88em/1.2 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace}
|
|
215
|
+
.trajectory-event.has-detail .trajectory-event-title{display:block;text-overflow:ellipsis;white-space:nowrap}
|
|
216
|
+
.trajectory-event-detail{display:block;overflow:hidden;margin-top:2px;color:var(--text-secondary);font-size:9px;line-height:1.3;text-overflow:ellipsis;white-space:nowrap}
|
|
217
|
+
.trajectory-event-raw{display:none}
|
|
218
|
+
.trajectory-lane.has-two-rows .trajectory-event[data-event-row="0"]{top:25%}
|
|
219
|
+
.trajectory-lane.has-two-rows .trajectory-event[data-event-row="1"]{top:75%}
|
|
220
|
+
.trajectory-lane.has-two-rows .trajectory-event.is-compact{top:50%}
|
|
221
|
+
.trajectory-event.is-message{--event-color:var(--text-secondary);--event-ring:rgba(99,112,131,.28);--event-bg:var(--bg-surface)}
|
|
222
|
+
.trajectory-event.is-reasoning{--event-color:#64748b;--event-ring:rgba(100,116,139,.3);--event-bg:rgba(100,116,139,.055)}
|
|
223
|
+
.trajectory-event.is-knowledge{--event-color:var(--accent);--event-ring:rgba(79,70,229,.28);--event-bg:rgba(79,70,229,.055)}
|
|
224
|
+
.trajectory-event.is-action{--event-color:var(--yellow);--event-ring:rgba(217,119,6,.3);--event-bg:rgba(217,119,6,.055)}
|
|
225
|
+
.trajectory-event.is-result{--event-color:var(--green);--event-ring:rgba(31,157,99,.3);--event-bg:rgba(31,157,99,.055)}
|
|
226
|
+
.trajectory-event.is-pending{--event-color:var(--accent);--event-ring:rgba(79,70,229,.28);--event-bg:rgba(79,70,229,.045)}
|
|
227
|
+
.trajectory-event.is-failure{--event-color:var(--red);--event-ring:rgba(220,38,38,.3);--event-bg:rgba(220,38,38,.055)}
|
|
228
|
+
.trajectory-event.is-warning{--event-color:var(--yellow);--event-ring:rgba(217,119,6,.3);--event-bg:rgba(217,119,6,.055)}
|
|
229
|
+
.trajectory-event.is-facet-muted{opacity:.14!important;filter:saturate(.35)}
|
|
230
|
+
.trajectory-event.is-facet-match{opacity:1!important}
|
|
231
|
+
.trajectory-event.is-live-entering{animation:trajectory-live-card-in .42s cubic-bezier(.2,.8,.2,1) both}
|
|
232
|
+
@keyframes trajectory-live-card-in{0%{opacity:.18;box-shadow:0 0 0 0 var(--event-ring,rgba(79,70,229,.2))}58%{opacity:1;box-shadow:0 0 0 4px var(--event-ring,rgba(79,70,229,.2))}100%{opacity:1}}
|
|
233
|
+
.trajectory-inspector{min-width:0;min-height:0;overflow:hidden;display:grid;grid-template-rows:auto minmax(0,1fr);border-left:1px solid var(--border);background:var(--bg-surface)}
|
|
234
|
+
.trajectory-inspector[hidden]{display:none}
|
|
235
|
+
.trajectory-operation-head{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:3px 12px;align-items:start;min-height:0;padding:12px 14px;border-bottom:1px solid var(--border)}
|
|
236
|
+
.trajectory-operation-type{grid-column:1/-1;color:var(--accent);font:500 10px/1.4 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;white-space:normal}
|
|
237
|
+
.trajectory-operation-copy{min-width:0}
|
|
238
|
+
.trajectory-operation-copy h3{margin:0 0 2px;font-size:14px;font-weight:650;line-height:1.35;letter-spacing:0;overflow-wrap:anywhere;white-space:normal}
|
|
239
|
+
.trajectory-operation-copy p{display:-webkit-box;margin:0;overflow:hidden;color:var(--text-secondary);font-size:11px;line-height:1.45;-webkit-box-orient:vertical;-webkit-line-clamp:2}
|
|
240
|
+
.trajectory-operation-actions{display:flex;align-items:center;gap:6px;padding-top:0}
|
|
241
|
+
.trajectory-evidence-count{color:var(--text-secondary);font-size:10px;white-space:nowrap}
|
|
242
|
+
.trajectory-inspector-close{display:grid;width:24px;height:24px;place-items:center;border:0;border-radius:4px;background:transparent;color:var(--text-muted);cursor:pointer}
|
|
243
|
+
.trajectory-inspector-close:hover{background:var(--bg-elevated);color:var(--text-primary)}
|
|
244
|
+
.trajectory-inspector-close:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
|
|
245
|
+
.trajectory-inspector-close svg{width:14px;height:14px;fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round}
|
|
246
|
+
.trajectory-operation-panel[hidden]{display:none}
|
|
247
|
+
.trajectory-semantic-panels{min-height:0;overflow:auto;overscroll-behavior:contain;scrollbar-color:var(--border-hover) transparent;scrollbar-width:thin}
|
|
248
|
+
.trajectory-fields{display:block;min-height:0}
|
|
249
|
+
.trajectory-field{min-width:0;padding:9px 14px;border-bottom:1px solid var(--border)}
|
|
250
|
+
.trajectory-field:last-child{border-bottom:0}
|
|
251
|
+
.trajectory-field-label{display:block;margin-bottom:3px;color:var(--text-muted);font-size:9px;font-weight:600}
|
|
252
|
+
.trajectory-field-value{display:block;margin-bottom:3px;font-size:12px;font-weight:600;overflow-wrap:anywhere}
|
|
253
|
+
.trajectory-field-content{display:block;max-height:min(280px,36vh);overflow:auto;color:var(--text-primary);font-size:12px;font-weight:500;line-height:1.6;white-space:pre-wrap;overflow-wrap:anywhere;scrollbar-color:var(--border-hover) transparent;scrollbar-width:thin}
|
|
254
|
+
.trajectory-field-content strong{font-weight:700}
|
|
255
|
+
.trajectory-field-content em{font-style:italic}
|
|
256
|
+
.trajectory-field-content code{padding:1px 3px;border-radius:3px;background:rgba(99,112,131,.1);font:500 .9em/1.35 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace}
|
|
257
|
+
.trajectory-field-content a{color:var(--accent);text-decoration:none}
|
|
258
|
+
.trajectory-field-content a:hover{text-decoration:underline}
|
|
259
|
+
.trajectory-field.is-content .trajectory-field-detail{max-height:none;margin-top:5px;overflow:visible}
|
|
260
|
+
.trajectory-field-detail{display:block;max-height:132px;overflow:auto;color:var(--text-secondary);font:400 10px/1.45 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;white-space:pre-wrap;overflow-wrap:anywhere}
|
|
261
|
+
.trajectory-field-detail-block{min-width:0}
|
|
262
|
+
.trajectory-field-detail-block[data-expandable="true"]:not([data-expanded="true"]) .trajectory-field-detail{overflow:hidden}
|
|
263
|
+
.trajectory-field-detail-block[data-expanded="true"] .trajectory-field-detail{max-height:min(320px,42vh);overflow:auto;color:var(--text-primary)}
|
|
264
|
+
.trajectory-field-detail-actions{display:flex;min-height:24px;align-items:center;justify-content:space-between;gap:8px;margin-top:6px;padding-top:6px;border-top:1px solid var(--border)}
|
|
265
|
+
.trajectory-field-detail-status{color:var(--text-muted);font-size:9px;white-space:nowrap}
|
|
266
|
+
.trajectory-field-detail-controls{display:flex;align-items:center;gap:3px}
|
|
267
|
+
.trajectory-field-detail-toggle,.trajectory-field-detail-copy{border:0;background:transparent;color:var(--accent);cursor:pointer}
|
|
268
|
+
.trajectory-field-detail-toggle{padding:3px 5px;border-radius:3px;font-size:9px;font-weight:600}
|
|
269
|
+
.trajectory-field-detail-copy{display:grid;width:24px;height:24px;place-items:center;border-radius:4px;color:var(--text-secondary)}
|
|
270
|
+
.trajectory-field-detail-toggle:hover,.trajectory-field-detail-copy:hover{background:var(--bg-elevated);color:var(--text-primary)}
|
|
271
|
+
.trajectory-field-detail-toggle:focus-visible,.trajectory-field-detail-copy:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
|
|
272
|
+
.trajectory-field-detail-copy.is-copied{color:var(--green)}
|
|
273
|
+
.trajectory-raw-list{display:none;min-width:0;min-height:0;overflow:auto;overscroll-behavior:contain;background:var(--bg-surface);scrollbar-color:var(--border-hover) transparent;scrollbar-width:thin}
|
|
274
|
+
.trajectory-raw-head,.trajectory-raw-row summary{display:grid;grid-template-columns:76px 126px minmax(240px,1fr) 138px;gap:12px;align-items:center}
|
|
275
|
+
.trajectory-raw-head{position:sticky;top:0;z-index:3;min-height:32px;padding:0 14px;border-bottom:1px solid var(--border);background:var(--bg-elevated);color:var(--text-muted);font-size:9px;font-weight:600}
|
|
276
|
+
.trajectory-raw-row{border-bottom:1px solid var(--border)}
|
|
277
|
+
.trajectory-raw-row[hidden]{display:none}
|
|
278
|
+
.trajectory-raw-row summary{min-height:46px;padding:7px 14px;color:var(--text-primary);cursor:pointer;list-style:none}
|
|
279
|
+
.trajectory-raw-row summary::-webkit-details-marker{display:none}
|
|
280
|
+
.trajectory-raw-row summary:hover{background:var(--bg-elevated)}
|
|
281
|
+
.trajectory-raw-row time,.trajectory-raw-kind,.trajectory-raw-id{overflow:hidden;color:var(--text-secondary);font:400 9px/1.35 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;text-overflow:ellipsis;white-space:nowrap}
|
|
282
|
+
.trajectory-raw-row strong{overflow:hidden;font-size:11px;font-weight:550;text-overflow:ellipsis;white-space:nowrap}
|
|
283
|
+
.trajectory-raw-row pre{max-height:180px;margin:0;padding:10px 14px 12px 226px;overflow:auto;border-top:1px solid var(--border);background:rgba(246,248,252,.62);color:var(--text-secondary);font:400 10px/1.5 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;white-space:pre-wrap;overflow-wrap:anywhere}
|
|
284
|
+
.trajectory-raw-empty{padding:28px 14px;color:var(--text-muted);font-size:11px;text-align:center}
|
|
285
|
+
.trajectory-record-notice{padding:8px 14px;border-bottom:1px solid var(--border);background:rgba(217,119,6,.045);color:var(--text-secondary);font-size:10px;line-height:1.45}
|
|
286
|
+
.trajectory-shell:not([data-mode="semantic"]) .trajectory-scroll{display:none}
|
|
287
|
+
.trajectory-shell[data-mode="normalized"] [data-event-view="normalized"],.trajectory-shell[data-mode="source"] [data-event-view="source"]{display:block}
|
|
288
|
+
.trajectory-shell:not([data-mode="semantic"]) .trajectory-body{grid-template-columns:minmax(0,1fr)}
|
|
289
|
+
.trajectory-shell:not([data-mode="semantic"]) .trajectory-inspector{display:none!important}
|
|
290
|
+
@media(prefers-reduced-motion:reduce){.trajectory-event{transition:none}.trajectory-event.is-live-entering{animation:none}}
|
|
291
|
+
@media(max-width:1100px){.trajectory-shell[data-inspector-open="true"] .trajectory-body{grid-template-columns:minmax(0,1fr) 320px}.trajectory-canvas{--lane-label-width:96px;--event-width:148px}.trajectory-lane-name{padding-inline:10px}.trajectory-lane-name span{font-size:9px}}
|
|
292
|
+
@media(max-width:1080px){.trajectory-meta-time{display:none!important}}
|
|
293
|
+
@media(max-width:860px){.app-main{padding-inline:10px}.trajectory-heading{grid-template-columns:1fr;gap:0}.trajectory-meta{display:none}.trajectory-shell[data-inspector-open="true"] .trajectory-body{grid-template-columns:1fr;grid-template-rows:minmax(0,3fr) minmax(160px,2fr)}.trajectory-inspector{border-top:1px solid var(--border);border-left:0}.trajectory-operation-head{padding-block:9px}}
|
|
294
|
+
@media(max-width:600px){.app-bar{padding-inline:10px}.app-brand-tag{display:none}.trajectory-heading h1{font-size:17px}.trajectory-range{display:none}.trajectory-frame-head .trajectory-mode{margin-left:auto}.trajectory-canvas{--lane-label-width:76px;--event-width:126px}.trajectory-lane-name{padding-inline:8px}.trajectory-lane-name span{display:none}.trajectory-event-time{display:none}.trajectory-raw-head,.trajectory-raw-row summary{grid-template-columns:66px 94px minmax(160px,1fr)}.trajectory-raw-id{display:none}.trajectory-raw-row pre{padding-left:14px}}
|
|
295
|
+
@media(prefers-reduced-motion:reduce){.trajectory-body{transition:none}}
|
|
296
|
+
</style>
|
|
297
|
+
<main class="trajectory-shell" data-mode="semantic"${options.live ? ` data-live-endpoint="${e(options.live.endpoint)}" data-live-revision="${e(options.live.revision)}"` : ''}>
|
|
298
|
+
<header class="trajectory-heading">
|
|
299
|
+
<h1 title="${e(pageTitle)}">${e(pageTitle)}</h1>
|
|
300
|
+
<div class="trajectory-meta">
|
|
301
|
+
<span class="trajectory-meta-source">${e(sourceLabel)}</span>
|
|
302
|
+
${observedModelLabel ? `<span class="trajectory-meta-model" title="${e(observedModels.join(' · '))}">${e(observedModelLabel)}</span>` : ''}
|
|
303
|
+
<span class="trajectory-meta-time">${e(formatDisplayTimestamp(projection.startTimestamp, lang))}</span>
|
|
304
|
+
<span>${e(formatElapsed(projection.durationMs, lang))}</span>
|
|
305
|
+
<span>${toolCallCount} ${zh ? '次调用' : toolCallCount === 1 ? 'call' : 'calls'}</span>
|
|
306
|
+
${toolFailureCount > 0 ? `<span class="is-failure">${toolFailureCount} ${zh ? '次失败' : toolFailureCount === 1 ? 'failure' : 'failures'}</span>` : ''}
|
|
307
|
+
<span>${normalizedEventCount} ${zh ? '个规范化事件' : normalizedEventCount === 1 ? 'normalized event' : 'normalized events'}</span>
|
|
308
|
+
${sourceRecordCount > 0 ? `<span>${sourceRecordCount} ${zh ? '条原始日志' : sourceRecordCount === 1 ? 'raw log entry' : 'raw log entries'}</span>` : ''}
|
|
309
|
+
</div>
|
|
310
|
+
</header>
|
|
311
|
+
|
|
312
|
+
${renderIntegrityWarning(model, lang)}
|
|
313
|
+
|
|
314
|
+
<section class="trajectory-frame" aria-labelledby="trajectory-title">
|
|
315
|
+
<header class="trajectory-frame-head">
|
|
316
|
+
<h2 id="trajectory-title">${zh ? '任务轨迹' : 'Task trajectory'}</h2>
|
|
317
|
+
${options.live ? `<div class="trajectory-live-controls"><span class="trajectory-live-state" data-live-state data-state="connecting">${zh ? '连接中' : 'Connecting'}</span><button class="trajectory-live-follow" type="button" data-live-follow data-state="following" data-following="true" aria-pressed="true" title="${zh ? '暂停自动跟随' : 'Pause automatic follow'}"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 12h13"></path><path d="m13 8 4 4-4 4"></path><path d="M20 5v14"></path></svg><span data-live-follow-label>${zh ? '跟随中' : 'Following'}</span></button></div>` : ''}
|
|
318
|
+
<details class="trajectory-boundary-info">
|
|
319
|
+
<summary aria-label="${zh ? '了解三类信息' : 'About the three views'}" title="${zh ? '三类信息说明' : 'About the three views'}"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="10"></circle><path d="M12 16v-4"></path><path d="M12 8h.01"></path></svg></summary>
|
|
320
|
+
<div class="trajectory-boundary-popover">${renderTrajectoryViewExplanation(model, lang)}</div>
|
|
321
|
+
</details>
|
|
322
|
+
<span class="trajectory-frame-space"></span>
|
|
323
|
+
<span class="trajectory-range">${e(formatRelativeTime(0))} — ${e(formatRelativeTime(projection.durationMs))}</span>
|
|
324
|
+
${projection.facets.length > 0 ? renderFacetFocus(projection.facets, lang) : ''}
|
|
325
|
+
<div class="trajectory-mode" aria-label="${zh ? '查看模式' : 'View mode'}">
|
|
326
|
+
<button type="button" data-trajectory-mode="semantic" aria-pressed="true">${zh ? '语义轨迹' : 'Semantic trajectory'}</button>
|
|
327
|
+
<button type="button" data-trajectory-mode="normalized" aria-pressed="false">${zh ? '规范化事件' : 'Normalized events'}</button>
|
|
328
|
+
<button type="button" data-trajectory-mode="source" aria-pressed="false">${zh ? '原始日志' : 'Raw logs'}</button>
|
|
329
|
+
</div>
|
|
330
|
+
</header>
|
|
331
|
+
|
|
332
|
+
<div class="trajectory-body">
|
|
333
|
+
<div class="trajectory-scroll" tabindex="0" aria-label="${zh ? '完整任务时间轴' : 'Full task timeline'}">
|
|
334
|
+
<div class="trajectory-canvas" style="--timeline-detail-width:${projection.detailWidth}px">
|
|
335
|
+
<div class="trajectory-axis" aria-hidden="true"><div class="trajectory-axis-label">${zh ? '时间' : 'Time'}</div><div class="trajectory-ticks">${renderAxis(projection, lang)}</div></div>
|
|
336
|
+
<div class="trajectory-lanes">
|
|
337
|
+
<div class="trajectory-guides" aria-hidden="true">${renderGuides(projection)}</div>
|
|
338
|
+
<svg class="trajectory-links" aria-hidden="true"></svg>
|
|
339
|
+
${['conversation', 'action', 'result', 'knowledge'].map((lane) => renderLane(lane, projection, undefined, lang)).join('')}
|
|
340
|
+
</div>
|
|
341
|
+
</div>
|
|
342
|
+
</div>
|
|
343
|
+
|
|
344
|
+
${renderNormalizedEventList(normalizedEvents, projection.operations, projection.startTimestamp, lang)}
|
|
345
|
+
${renderSourceRecordList(model, projection.startTimestamp, lang, options.sourceRecordsEndpoint)}
|
|
346
|
+
|
|
347
|
+
<aside class="trajectory-inspector" aria-live="polite" hidden>
|
|
348
|
+
<header class="trajectory-operation-head">
|
|
349
|
+
<div class="trajectory-operation-type" id="trajectory-operation-type"></div>
|
|
350
|
+
<div class="trajectory-operation-copy"><h3 id="trajectory-operation-title"></h3><p id="trajectory-operation-summary"></p></div>
|
|
351
|
+
<div class="trajectory-operation-actions"><div class="trajectory-evidence-count" id="trajectory-evidence-count"></div><button class="trajectory-inspector-close" type="button" data-inspector-close aria-label="${zh ? '关闭详情' : 'Close details'}" title="${zh ? '关闭详情' : 'Close details'}"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12"></path><path d="M18 6L6 18"></path></svg></button></div>
|
|
352
|
+
</header>
|
|
353
|
+
<div class="trajectory-semantic-panels">${projection.operations.map((operation) => renderSemanticPanel(operation, false, lang)).join('')}</div>
|
|
354
|
+
</aside>
|
|
355
|
+
</div>
|
|
356
|
+
</section>
|
|
357
|
+
</main>
|
|
358
|
+
<script>
|
|
359
|
+
(() => {
|
|
360
|
+
${trajectoryRoutingClientSource}
|
|
361
|
+
${trajectoryLiveClientSource}
|
|
362
|
+
const initializeTrajectoryPage = () => {
|
|
363
|
+
window.__omkTrajectoryDispose?.();
|
|
364
|
+
const pageLifecycle = new AbortController();
|
|
365
|
+
const shell = document.querySelector('.trajectory-shell');
|
|
366
|
+
if (!shell) return;
|
|
367
|
+
const cards = Array.from(document.querySelectorAll('[data-trajectory-operation]'));
|
|
368
|
+
const normalizedRows = Array.from(document.querySelectorAll('[data-trajectory-normalized-event]'));
|
|
369
|
+
const normalizedEmpty = document.querySelector('[data-trajectory-normalized-empty]');
|
|
370
|
+
const sourceRecordList = document.querySelector('[data-source-records-endpoint]');
|
|
371
|
+
const semanticPanels = Array.from(document.querySelectorAll('[data-trajectory-semantic-panel]'));
|
|
372
|
+
const semanticPanelContainer = document.querySelector('.trajectory-semantic-panels');
|
|
373
|
+
const detailBlocks = Array.from(document.querySelectorAll('[data-field-detail-block]'));
|
|
374
|
+
const operationType = document.querySelector('#trajectory-operation-type');
|
|
375
|
+
const operationTitle = document.querySelector('#trajectory-operation-title');
|
|
376
|
+
const operationSummary = document.querySelector('#trajectory-operation-summary');
|
|
377
|
+
const evidenceCount = document.querySelector('#trajectory-evidence-count');
|
|
378
|
+
const lanes = document.querySelector('.trajectory-lanes');
|
|
379
|
+
const operationLinks = document.querySelector('.trajectory-links');
|
|
380
|
+
const facetFocus = document.querySelector('.trajectory-focus');
|
|
381
|
+
const facetButtons = Array.from(document.querySelectorAll('[data-trajectory-facet]'));
|
|
382
|
+
const boundaryInfo = document.querySelector('.trajectory-boundary-info');
|
|
383
|
+
const boundarySummary = boundaryInfo?.querySelector('summary');
|
|
384
|
+
const inspector = document.querySelector('.trajectory-inspector');
|
|
385
|
+
const inspectorClose = document.querySelector('[data-inspector-close]');
|
|
386
|
+
const trajectoryBody = document.querySelector('.trajectory-body');
|
|
387
|
+
const timeline = document.querySelector('.trajectory-scroll');
|
|
388
|
+
const liveFollowButton = document.querySelector('[data-live-follow]');
|
|
389
|
+
let currentOperationId = '';
|
|
390
|
+
let pendingLayoutFrame;
|
|
391
|
+
let layoutTransitionTimer;
|
|
392
|
+
let layoutScrollReleaseTimer;
|
|
393
|
+
let sourceRecordsPromise;
|
|
394
|
+
const copyFeedbackTimers = new Set();
|
|
395
|
+
let pauseLiveFollow = () => {};
|
|
396
|
+
const sourceRecordLabels = ${JSON.stringify({
|
|
397
|
+
empty: zh ? '空记录' : 'Empty record',
|
|
398
|
+
redacted: zh ? '【不透明加密载荷已省略】' : '[Opaque encrypted payload omitted]',
|
|
399
|
+
truncated: zh ? '【该记录已按归档上限截断】' : '[Record truncated by archive limit]',
|
|
400
|
+
loadFailed: zh ? '原始日志读取失败。可以切换视图后重试。' : 'Raw logs could not be loaded. Switch views and retry.',
|
|
401
|
+
partial: zh ? '原始日志归档不完整:已保留 {retained} 条,省略 {omitted} 条。' : 'The raw-log archive is partial: {retained} retained, {omitted} omitted.',
|
|
402
|
+
})};
|
|
403
|
+
const cardLane = (card) => card.closest('.trajectory-lane')?.dataset.lane || '';
|
|
404
|
+
const operationIds = semanticPanels.map((panel) => panel.dataset.trajectorySemanticPanel).filter(Boolean);
|
|
405
|
+
const cardsByOperation = new Map(operationIds.map((id) => [id, []]));
|
|
406
|
+
cards.forEach((card) => {
|
|
407
|
+
const operationId = card.dataset.trajectoryOperation || '';
|
|
408
|
+
const relatedCards = cardsByOperation.get(operationId);
|
|
409
|
+
if (relatedCards) relatedCards.push(card);
|
|
410
|
+
});
|
|
411
|
+
const fullDetailText = (block) => {
|
|
412
|
+
if (typeof block.__omkFullDetail === 'string') return block.__omkFullDetail;
|
|
413
|
+
const sourceEventId = block.dataset.detailSourceEventId || '';
|
|
414
|
+
const sourceRow = normalizedRows.find((row) => row.dataset.trajectoryNormalizedEvent === sourceEventId);
|
|
415
|
+
const fullText = sourceRow?.querySelector('pre')?.textContent || block.querySelector('[data-field-detail]')?.textContent || '';
|
|
416
|
+
block.__omkFullDetail = fullText;
|
|
417
|
+
return fullText;
|
|
418
|
+
};
|
|
419
|
+
detailBlocks.forEach((block) => {
|
|
420
|
+
const detail = block.querySelector('[data-field-detail]');
|
|
421
|
+
const toggle = block.querySelector('[data-field-detail-toggle]');
|
|
422
|
+
const copy = block.querySelector('[data-field-detail-copy]');
|
|
423
|
+
const status = block.querySelector('[data-field-detail-status]');
|
|
424
|
+
const previewText = detail?.textContent || '';
|
|
425
|
+
toggle?.addEventListener('click', () => {
|
|
426
|
+
const expanded = block.dataset.expanded === 'true';
|
|
427
|
+
if (detail) detail.textContent = expanded ? previewText : fullDetailText(block);
|
|
428
|
+
block.dataset.expanded = String(!expanded);
|
|
429
|
+
toggle.textContent = expanded ? block.dataset.expandLabel : block.dataset.collapseLabel;
|
|
430
|
+
toggle.setAttribute('aria-expanded', String(!expanded));
|
|
431
|
+
if (status) status.textContent = expanded ? block.dataset.previewStatus : block.dataset.fullStatus;
|
|
432
|
+
}, { signal: pageLifecycle.signal });
|
|
433
|
+
copy?.addEventListener('click', async () => {
|
|
434
|
+
const text = fullDetailText(block);
|
|
435
|
+
try {
|
|
436
|
+
await navigator.clipboard.writeText(text);
|
|
437
|
+
} catch {
|
|
438
|
+
const textarea = document.createElement('textarea');
|
|
439
|
+
textarea.value = text;
|
|
440
|
+
textarea.style.position = 'fixed';
|
|
441
|
+
textarea.style.opacity = '0';
|
|
442
|
+
document.body.append(textarea);
|
|
443
|
+
textarea.select();
|
|
444
|
+
document.execCommand('copy');
|
|
445
|
+
textarea.remove();
|
|
446
|
+
}
|
|
447
|
+
copy.classList.add('is-copied');
|
|
448
|
+
copy.setAttribute('aria-label', block.dataset.copiedLabel);
|
|
449
|
+
copy.setAttribute('title', block.dataset.copiedLabel);
|
|
450
|
+
const copyFeedbackTimer = window.setTimeout(() => {
|
|
451
|
+
copyFeedbackTimers.delete(copyFeedbackTimer);
|
|
452
|
+
copy.classList.remove('is-copied');
|
|
453
|
+
copy.setAttribute('aria-label', block.dataset.copyLabel);
|
|
454
|
+
copy.setAttribute('title', block.dataset.copyLabel);
|
|
455
|
+
}, 1200);
|
|
456
|
+
copyFeedbackTimers.add(copyFeedbackTimer);
|
|
457
|
+
}, { signal: pageLifecycle.signal });
|
|
458
|
+
});
|
|
459
|
+
const operationEntries = operationIds.map((id, order) => {
|
|
460
|
+
const relatedCards = cardsByOperation.get(id) || [];
|
|
461
|
+
const actionCard = relatedCards.find((card) => cardLane(card) === 'action');
|
|
462
|
+
const resultCard = relatedCards.find((card) => cardLane(card) === 'result');
|
|
463
|
+
const knowledgeCards = relatedCards.filter((card) => cardLane(card) === 'knowledge');
|
|
464
|
+
const conversationCard = relatedCards.find((card) => cardLane(card) === 'conversation');
|
|
465
|
+
const startCard = actionCard || conversationCard || resultCard;
|
|
466
|
+
const endCard = resultCard || conversationCard || actionCard;
|
|
467
|
+
return { id, order, startCard, endCard, actionCard, resultCard, knowledgeCards };
|
|
468
|
+
});
|
|
469
|
+
const primaryOperations = operationEntries.filter((operation) => operation.startCard && operation.endCard);
|
|
470
|
+
const substantiveOperations = primaryOperations.filter((operation) => !operation.startCard.classList.contains('is-compact'));
|
|
471
|
+
const compactCards = cards.filter((card) => card.classList.contains('is-compact'));
|
|
472
|
+
const linksByOperation = new Map();
|
|
473
|
+
let activeLinkElements = [];
|
|
474
|
+
const registerLink = (link, relatedOperationIds) => {
|
|
475
|
+
const operationTokens = [...new Set(relatedOperationIds.filter(Boolean))];
|
|
476
|
+
link.setAttribute('data-link-operations', operationTokens.join(' '));
|
|
477
|
+
operationTokens.forEach((operationId) => {
|
|
478
|
+
const relatedLinks = linksByOperation.get(operationId) || [];
|
|
479
|
+
relatedLinks.push(link);
|
|
480
|
+
linksByOperation.set(operationId, relatedLinks);
|
|
481
|
+
});
|
|
482
|
+
};
|
|
483
|
+
const setLinkActivity = (activeId = '') => {
|
|
484
|
+
activeLinkElements.forEach((link) => link.classList.remove('is-active'));
|
|
485
|
+
activeLinkElements = activeId ? (linksByOperation.get(activeId) || []) : [];
|
|
486
|
+
activeLinkElements.forEach((link) => link.classList.add('is-active'));
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
const updateOperationLinks = () => {
|
|
490
|
+
if (!lanes || !operationLinks || shell.dataset.mode !== 'semantic') return;
|
|
491
|
+
activeLinkElements = [];
|
|
492
|
+
linksByOperation.clear();
|
|
493
|
+
operationLinks.replaceChildren();
|
|
494
|
+
const layoutStartedAt = performance.now();
|
|
495
|
+
const layoutMetrics = {
|
|
496
|
+
flowRoutes: 0,
|
|
497
|
+
obstacleQueries: 0,
|
|
498
|
+
obstacleCandidates: 0,
|
|
499
|
+
collisionChecks: 0,
|
|
500
|
+
curveCandidates: 0,
|
|
501
|
+
corridorCandidates: 0,
|
|
502
|
+
};
|
|
503
|
+
const lanesRect = lanes.getBoundingClientRect();
|
|
504
|
+
compactCards.forEach((card) => {
|
|
505
|
+
card.style.removeProperty('left');
|
|
506
|
+
card.style.removeProperty('top');
|
|
507
|
+
});
|
|
508
|
+
const edgePoint = (rect, targetX, targetY) => {
|
|
509
|
+
const centerX = (rect.left + rect.right) / 2;
|
|
510
|
+
const centerY = (rect.top + rect.bottom) / 2;
|
|
511
|
+
const dx = targetX - centerX;
|
|
512
|
+
const dy = targetY - centerY;
|
|
513
|
+
const scaleX = dx === 0 ? Number.POSITIVE_INFINITY : (rect.width / 2) / Math.abs(dx);
|
|
514
|
+
const scaleY = dy === 0 ? Number.POSITIVE_INFINITY : (rect.height / 2) / Math.abs(dy);
|
|
515
|
+
const scale = Math.min(scaleX, scaleY);
|
|
516
|
+
return {
|
|
517
|
+
x: centerX + dx * scale - lanesRect.left,
|
|
518
|
+
y: centerY + dy * scale - lanesRect.top,
|
|
519
|
+
};
|
|
520
|
+
};
|
|
521
|
+
const connectionPoints = (fromRect, toRect) => {
|
|
522
|
+
const fromCenterX = (fromRect.left + fromRect.right) / 2;
|
|
523
|
+
const fromCenterY = (fromRect.top + fromRect.bottom) / 2;
|
|
524
|
+
const toCenterX = (toRect.left + toRect.right) / 2;
|
|
525
|
+
const toCenterY = (toRect.top + toRect.bottom) / 2;
|
|
526
|
+
const moveToward = (point, target, distance) => {
|
|
527
|
+
const dx = target.x - point.x;
|
|
528
|
+
const dy = target.y - point.y;
|
|
529
|
+
const length = Math.hypot(dx, dy) || 1;
|
|
530
|
+
return { x: point.x + dx / length * distance, y: point.y + dy / length * distance };
|
|
531
|
+
};
|
|
532
|
+
const from = edgePoint(fromRect, toCenterX, toCenterY);
|
|
533
|
+
const to = edgePoint(toRect, fromCenterX, fromCenterY);
|
|
534
|
+
return {
|
|
535
|
+
from: moveToward(from, { x: toCenterX - lanesRect.left, y: toCenterY - lanesRect.top }, 2),
|
|
536
|
+
to: moveToward(to, { x: fromCenterX - lanesRect.left, y: fromCenterY - lanesRect.top }, 2),
|
|
537
|
+
};
|
|
538
|
+
};
|
|
539
|
+
const appendArrow = (fromX, fromY, toX, toY, linkKind, relatedOperationIds) => {
|
|
540
|
+
const arrow = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
541
|
+
const classes = 'trajectory-link-arrow is-' + linkKind;
|
|
542
|
+
const dx = toX - fromX;
|
|
543
|
+
const dy = toY - fromY;
|
|
544
|
+
const length = Math.hypot(dx, dy) || 1;
|
|
545
|
+
const unitX = dx / length;
|
|
546
|
+
const unitY = dy / length;
|
|
547
|
+
const perpendicularX = -unitY;
|
|
548
|
+
const perpendicularY = unitX;
|
|
549
|
+
const baseX = toX - unitX * 5;
|
|
550
|
+
const baseY = toY - unitY * 5;
|
|
551
|
+
const d = 'M ' + (baseX + perpendicularX * 2.7) + ' ' + (baseY + perpendicularY * 2.7)
|
|
552
|
+
+ ' L ' + toX + ' ' + toY
|
|
553
|
+
+ ' L ' + (baseX - perpendicularX * 2.7) + ' ' + (baseY - perpendicularY * 2.7)
|
|
554
|
+
+ ' Z';
|
|
555
|
+
arrow.setAttribute('class', classes);
|
|
556
|
+
arrow.setAttribute('d', d);
|
|
557
|
+
registerLink(arrow, relatedOperationIds);
|
|
558
|
+
operationLinks.appendChild(arrow);
|
|
559
|
+
};
|
|
560
|
+
const connectStraight = (from, to, linkKind, relatedOperationIds, withArrow = true) => {
|
|
561
|
+
if (!from || !to) return;
|
|
562
|
+
const fromRect = from.getBoundingClientRect();
|
|
563
|
+
const toRect = to.getBoundingClientRect();
|
|
564
|
+
const points = connectionPoints(fromRect, toRect);
|
|
565
|
+
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
566
|
+
path.setAttribute('class', 'trajectory-link is-' + linkKind);
|
|
567
|
+
path.setAttribute('data-link-kind', linkKind);
|
|
568
|
+
registerLink(path, relatedOperationIds);
|
|
569
|
+
path.setAttribute('d', 'M ' + points.from.x + ' ' + points.from.y + ' L ' + points.to.x + ' ' + points.to.y);
|
|
570
|
+
operationLinks.appendChild(path);
|
|
571
|
+
if (withArrow) appendArrow(points.from.x, points.from.y, points.to.x, points.to.y, linkKind, relatedOperationIds);
|
|
572
|
+
};
|
|
573
|
+
const toRoutingRect = (rect, owner) => ({
|
|
574
|
+
owner,
|
|
575
|
+
left: rect.left - lanesRect.left,
|
|
576
|
+
right: rect.right - lanesRect.left,
|
|
577
|
+
top: rect.top - lanesRect.top,
|
|
578
|
+
bottom: rect.bottom - lanesRect.top,
|
|
579
|
+
});
|
|
580
|
+
const createFlowPath = (from, to, relatedOperationIds) => {
|
|
581
|
+
if (!from || !to || from === to) return undefined;
|
|
582
|
+
const route = planFlowRoute({
|
|
583
|
+
fromRect: toRoutingRect(from.getBoundingClientRect(), from),
|
|
584
|
+
toRect: toRoutingRect(to.getBoundingClientRect(), to),
|
|
585
|
+
fromLane: cardLane(from),
|
|
586
|
+
toLane: cardLane(to),
|
|
587
|
+
obstacleIndex,
|
|
588
|
+
fromOwner: from,
|
|
589
|
+
toOwner: to,
|
|
590
|
+
});
|
|
591
|
+
if (!route) return undefined;
|
|
592
|
+
Object.keys(route.metrics).forEach((key) => {
|
|
593
|
+
layoutMetrics[key] += route.metrics[key];
|
|
594
|
+
});
|
|
595
|
+
layoutMetrics.flowRoutes += 1;
|
|
596
|
+
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
597
|
+
path.setAttribute('class', 'trajectory-link is-flow');
|
|
598
|
+
path.setAttribute('data-link-kind', 'flow');
|
|
599
|
+
path.setAttribute('data-flow-from', from.dataset.trajectoryOperation || '');
|
|
600
|
+
path.setAttribute('data-flow-to', to.dataset.trajectoryOperation || '');
|
|
601
|
+
registerLink(path, relatedOperationIds);
|
|
602
|
+
path.setAttribute('d', route.d);
|
|
603
|
+
operationLinks.appendChild(path);
|
|
604
|
+
appendArrow(route.arrowFrom.x, route.arrowFrom.y, route.to.x, route.to.y, 'flow', relatedOperationIds);
|
|
605
|
+
return path;
|
|
606
|
+
};
|
|
607
|
+
const blockingRects = cards.filter((card) => !card.classList.contains('is-compact')).map((card) => {
|
|
608
|
+
const rect = card.getBoundingClientRect();
|
|
609
|
+
return {
|
|
610
|
+
owner: card,
|
|
611
|
+
left: rect.left - lanesRect.left - 9,
|
|
612
|
+
right: rect.right - lanesRect.left + 9,
|
|
613
|
+
top: rect.top - lanesRect.top - 9,
|
|
614
|
+
bottom: rect.bottom - lanesRect.top + 9,
|
|
615
|
+
};
|
|
616
|
+
});
|
|
617
|
+
const reservedCallResultChannels = operationEntries.flatMap((operation) => {
|
|
618
|
+
if (!operation.actionCard || !operation.resultCard) return [];
|
|
619
|
+
const actionRect = operation.actionCard.getBoundingClientRect();
|
|
620
|
+
const resultRect = operation.resultCard.getBoundingClientRect();
|
|
621
|
+
const channel = connectionPoints(actionRect, resultRect);
|
|
622
|
+
const top = Math.min(channel.from.y, channel.to.y);
|
|
623
|
+
const bottom = Math.max(channel.from.y, channel.to.y);
|
|
624
|
+
const endpointInset = 18;
|
|
625
|
+
if (bottom - top <= endpointInset * 2) return [];
|
|
626
|
+
return [{
|
|
627
|
+
left: Math.min(channel.from.x, channel.to.x) - 12,
|
|
628
|
+
right: Math.max(channel.from.x, channel.to.x) + 12,
|
|
629
|
+
top: top + endpointInset,
|
|
630
|
+
bottom: bottom - endpointInset,
|
|
631
|
+
}];
|
|
632
|
+
});
|
|
633
|
+
const obstacleIndex = createHorizontalObstacleIndex([
|
|
634
|
+
...blockingRects,
|
|
635
|
+
...reservedCallResultChannels,
|
|
636
|
+
]);
|
|
637
|
+
const markerObstacles = [...blockingRects, ...reservedCallResultChannels];
|
|
638
|
+
const laneBoundaries = [...document.querySelectorAll('.trajectory-lane')]
|
|
639
|
+
.map((lane) => lane.getBoundingClientRect().bottom - lanesRect.top)
|
|
640
|
+
.filter((boundary) => boundary > 8 && boundary < lanesRect.height - 8);
|
|
641
|
+
const placedMarkerCenters = [];
|
|
642
|
+
const pointRectDistance = (x, y, rect) => {
|
|
643
|
+
const dx = Math.max(rect.left - x, 0, x - rect.right);
|
|
644
|
+
const dy = Math.max(rect.top - y, 0, y - rect.bottom);
|
|
645
|
+
return Math.hypot(dx, dy);
|
|
646
|
+
};
|
|
647
|
+
const markerClearance = (x, y) => markerObstacles.length === 0
|
|
648
|
+
? Number.POSITIVE_INFINITY
|
|
649
|
+
: Math.min(...markerObstacles.map((rect) => pointRectDistance(x, y, rect)));
|
|
650
|
+
const laneBoundaryClearance = (y) => laneBoundaries.length === 0
|
|
651
|
+
? Number.POSITIVE_INFINITY
|
|
652
|
+
: Math.min(...laneBoundaries.map((boundary) => Math.abs(boundary - y)));
|
|
653
|
+
const markerCollides = (x, y) => markerClearance(x, y) < 16
|
|
654
|
+
|| laneBoundaryClearance(y) < 16
|
|
655
|
+
|| placedMarkerCenters.some((point) => Math.hypot(point.x - x, point.y - y) < 32);
|
|
656
|
+
operationEntries.forEach((operation) => {
|
|
657
|
+
if (operation.actionCard && operation.resultCard) {
|
|
658
|
+
connectStraight(operation.actionCard, operation.resultCard, 'call-result', [operation.id]);
|
|
659
|
+
}
|
|
660
|
+
const knowledgeSource = operation.resultCard || operation.actionCard;
|
|
661
|
+
if (knowledgeSource) {
|
|
662
|
+
operation.knowledgeCards.forEach((card) => connectStraight(knowledgeSource, card, 'knowledge', [operation.id], false));
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
const markerPlacementTasks = [];
|
|
666
|
+
substantiveOperations.forEach((operation, index) => {
|
|
667
|
+
const previous = substantiveOperations[index - 1];
|
|
668
|
+
if (!previous) return;
|
|
669
|
+
const markerOperations = primaryOperations
|
|
670
|
+
.filter((candidate) => candidate.startCard.classList.contains('is-compact')
|
|
671
|
+
&& candidate.order > previous.order
|
|
672
|
+
&& candidate.order < operation.order);
|
|
673
|
+
const relatedOperationIds = [previous.id, ...markerOperations.map((candidate) => candidate.id), operation.id];
|
|
674
|
+
const path = createFlowPath(previous.endCard, operation.startCard, relatedOperationIds);
|
|
675
|
+
if (path && markerOperations.length > 0) markerPlacementTasks.push({ path, markerOperations });
|
|
676
|
+
});
|
|
677
|
+
operationEntries.filter((operation) => !operation.startCard && operation.knowledgeCards.length > 0).forEach((operation) => {
|
|
678
|
+
const previous = [...primaryOperations].reverse().find((candidate) => candidate.order < operation.order);
|
|
679
|
+
const next = primaryOperations.find((candidate) => candidate.order > operation.order);
|
|
680
|
+
const source = previous?.endCard || next?.startCard;
|
|
681
|
+
const relatedOperationIds = [operation.id, previous?.id, next?.id];
|
|
682
|
+
if (source) {
|
|
683
|
+
operation.knowledgeCards.forEach((card) => connectStraight(source, card, 'knowledge', relatedOperationIds, false));
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
const markerPlacements = [];
|
|
687
|
+
markerPlacementTasks.forEach(({ path, markerOperations }) => {
|
|
688
|
+
const pathLength = path.getTotalLength();
|
|
689
|
+
const idealProgresses = planFlowMarkerProgresses(pathLength, markerOperations.length);
|
|
690
|
+
const endpointClearance = Math.min(18, pathLength / 2);
|
|
691
|
+
markerOperations.forEach((markerOperation, markerIndex) => {
|
|
692
|
+
const marker = markerOperation.startCard;
|
|
693
|
+
const idealProgress = idealProgresses[markerIndex] ?? .5;
|
|
694
|
+
const trackRect = marker.closest('.trajectory-track')?.getBoundingClientRect();
|
|
695
|
+
if (!trackRect) return;
|
|
696
|
+
const progressCandidates = [idealProgress, ...Array.from({ length: 161 }, (_value, candidateIndex) => (
|
|
697
|
+
.04 + .92 * candidateIndex / 160
|
|
698
|
+
))];
|
|
699
|
+
const safeCandidates = progressCandidates
|
|
700
|
+
.map((progress) => ({
|
|
701
|
+
progress,
|
|
702
|
+
point: path.getPointAtLength(pathLength * progress),
|
|
703
|
+
sourceDistance: progress * pathLength,
|
|
704
|
+
destinationDistance: (1 - progress) * pathLength,
|
|
705
|
+
}))
|
|
706
|
+
.filter((candidate) => candidate.point.y >= 10
|
|
707
|
+
&& candidate.point.y <= lanesRect.height - 10
|
|
708
|
+
&& candidate.sourceDistance >= endpointClearance
|
|
709
|
+
&& candidate.destinationDistance >= endpointClearance
|
|
710
|
+
&& !markerCollides(candidate.point.x, candidate.point.y))
|
|
711
|
+
.map((candidate) => ({
|
|
712
|
+
...candidate,
|
|
713
|
+
clearance: markerClearance(candidate.point.x, candidate.point.y),
|
|
714
|
+
boundaryClearance: laneBoundaryClearance(candidate.point.y),
|
|
715
|
+
}))
|
|
716
|
+
.map((candidate) => ({
|
|
717
|
+
...candidate,
|
|
718
|
+
score: Math.abs(candidate.progress - idealProgress) * Math.min(pathLength, 420) * 4
|
|
719
|
+
+ Math.max(0, 36 - candidate.clearance) * 8
|
|
720
|
+
+ Math.max(0, 30 - candidate.boundaryClearance) * 10,
|
|
721
|
+
}));
|
|
722
|
+
let target = safeCandidates.sort((left, right) => left.score - right.score)[0]?.point;
|
|
723
|
+
target ||= path.getPointAtLength(pathLength * idealProgress);
|
|
724
|
+
placedMarkerCenters.push(target);
|
|
725
|
+
markerPlacements.push({ marker, target, trackRect });
|
|
726
|
+
});
|
|
727
|
+
});
|
|
728
|
+
markerPlacements.forEach(({ marker, target, trackRect }) => {
|
|
729
|
+
marker.style.left = (lanesRect.left + target.x - trackRect.left) + 'px';
|
|
730
|
+
marker.style.top = (lanesRect.top + target.y - trackRect.top) + 'px';
|
|
731
|
+
});
|
|
732
|
+
window.__omkTrajectoryMetrics = {
|
|
733
|
+
...layoutMetrics,
|
|
734
|
+
durationMs: performance.now() - layoutStartedAt,
|
|
735
|
+
cardCount: cards.length,
|
|
736
|
+
obstacleCount: markerObstacles.length,
|
|
737
|
+
linkElementCount: operationLinks.childElementCount,
|
|
738
|
+
};
|
|
739
|
+
setLinkActivity(currentOperationId);
|
|
740
|
+
};
|
|
741
|
+
|
|
742
|
+
const scheduleOperationLinks = () => {
|
|
743
|
+
if (pendingLayoutFrame !== undefined) cancelAnimationFrame(pendingLayoutFrame);
|
|
744
|
+
pendingLayoutFrame = requestAnimationFrame(() => {
|
|
745
|
+
pendingLayoutFrame = undefined;
|
|
746
|
+
updateOperationLinks();
|
|
747
|
+
});
|
|
748
|
+
};
|
|
749
|
+
|
|
750
|
+
const revealSelectedOperation = () => {
|
|
751
|
+
if (!timeline || !currentOperationId) return;
|
|
752
|
+
const relatedCards = cardsByOperation.get(currentOperationId) || [];
|
|
753
|
+
const card = relatedCards.find((item) => item.dataset.primary === 'true') || relatedCards[0];
|
|
754
|
+
if (!card) return;
|
|
755
|
+
const viewportRect = timeline.getBoundingClientRect();
|
|
756
|
+
const cardRect = card.getBoundingClientRect();
|
|
757
|
+
const canvas = timeline.querySelector('.trajectory-canvas');
|
|
758
|
+
const laneLabelWidth = Number.parseFloat(canvas ? getComputedStyle(canvas).getPropertyValue('--lane-label-width') : '') || 0;
|
|
759
|
+
const visibleLeft = viewportRect.left + laneLabelWidth + 12;
|
|
760
|
+
const visibleRight = viewportRect.right - 12;
|
|
761
|
+
const delta = cardRect.left < visibleLeft
|
|
762
|
+
? cardRect.left - visibleLeft
|
|
763
|
+
: cardRect.right > visibleRight
|
|
764
|
+
? cardRect.right - visibleRight
|
|
765
|
+
: 0;
|
|
766
|
+
if (Math.abs(delta) > 1) timeline.scrollLeft = Math.max(0, timeline.scrollLeft + delta);
|
|
767
|
+
};
|
|
768
|
+
const alignFollowingViewport = () => {
|
|
769
|
+
if (!timeline || liveFollowButton?.dataset.following !== 'true') return false;
|
|
770
|
+
timeline.scrollLeft = Math.max(0, timeline.scrollWidth - timeline.clientWidth);
|
|
771
|
+
return true;
|
|
772
|
+
};
|
|
773
|
+
const finishLayoutTransition = () => {
|
|
774
|
+
if (layoutTransitionTimer !== undefined) window.clearTimeout(layoutTransitionTimer);
|
|
775
|
+
layoutTransitionTimer = undefined;
|
|
776
|
+
if (!alignFollowingViewport()) revealSelectedOperation();
|
|
777
|
+
scheduleOperationLinks();
|
|
778
|
+
if (layoutScrollReleaseTimer !== undefined) window.clearTimeout(layoutScrollReleaseTimer);
|
|
779
|
+
layoutScrollReleaseTimer = window.setTimeout(() => {
|
|
780
|
+
delete shell.dataset.layoutTransition;
|
|
781
|
+
layoutScrollReleaseTimer = undefined;
|
|
782
|
+
}, 120);
|
|
783
|
+
};
|
|
784
|
+
const beginLayoutTransition = () => {
|
|
785
|
+
if (layoutTransitionTimer !== undefined) window.clearTimeout(layoutTransitionTimer);
|
|
786
|
+
if (layoutScrollReleaseTimer !== undefined) window.clearTimeout(layoutScrollReleaseTimer);
|
|
787
|
+
layoutScrollReleaseTimer = undefined;
|
|
788
|
+
shell.dataset.layoutTransition = 'true';
|
|
789
|
+
layoutTransitionTimer = window.setTimeout(finishLayoutTransition, 240);
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
const selectOperation = (id, animateLayout = true) => {
|
|
793
|
+
const panel = semanticPanels.find((item) => item.dataset.trajectorySemanticPanel === id);
|
|
794
|
+
if (!panel) return;
|
|
795
|
+
cards.forEach((card) => {
|
|
796
|
+
const related = card.dataset.trajectoryOperation === id;
|
|
797
|
+
card.classList.toggle('is-related', related);
|
|
798
|
+
card.classList.toggle('is-dimmed', !related);
|
|
799
|
+
card.classList.toggle('is-primary', related && card.dataset.primary === 'true');
|
|
800
|
+
card.setAttribute('aria-pressed', String(related));
|
|
801
|
+
});
|
|
802
|
+
semanticPanels.forEach((item) => { item.hidden = item.dataset.trajectorySemanticPanel !== id; });
|
|
803
|
+
if (operationType) operationType.textContent = panel.dataset.typeLabel || '—';
|
|
804
|
+
if (operationTitle) operationTitle.textContent = panel.dataset.title || '—';
|
|
805
|
+
if (operationSummary) operationSummary.textContent = panel.dataset.summary || '';
|
|
806
|
+
if (evidenceCount) evidenceCount.textContent = panel.dataset.evidenceLabel || '';
|
|
807
|
+
currentOperationId = id;
|
|
808
|
+
if (animateLayout && shell.dataset.inspectorOpen !== 'true') beginLayoutTransition();
|
|
809
|
+
shell.dataset.inspectorOpen = 'true';
|
|
810
|
+
if (inspector) inspector.hidden = false;
|
|
811
|
+
setLinkActivity(id);
|
|
812
|
+
scheduleOperationLinks();
|
|
813
|
+
};
|
|
814
|
+
|
|
815
|
+
const closeInspector = (shouldScheduleLayout = true) => {
|
|
816
|
+
const inspectorWasOpen = shell.dataset.inspectorOpen === 'true';
|
|
817
|
+
cards.forEach((card) => {
|
|
818
|
+
card.classList.remove('is-related', 'is-dimmed', 'is-primary');
|
|
819
|
+
card.setAttribute('aria-pressed', 'false');
|
|
820
|
+
});
|
|
821
|
+
semanticPanels.forEach((item) => { item.hidden = true; });
|
|
822
|
+
currentOperationId = '';
|
|
823
|
+
if (inspectorWasOpen) beginLayoutTransition();
|
|
824
|
+
delete shell.dataset.inspectorOpen;
|
|
825
|
+
if (inspector) inspector.hidden = true;
|
|
826
|
+
setLinkActivity();
|
|
827
|
+
if (shouldScheduleLayout) scheduleOperationLinks();
|
|
828
|
+
};
|
|
829
|
+
|
|
830
|
+
cards.forEach((card) => {
|
|
831
|
+
const operationId = card.dataset.trajectoryOperation || '';
|
|
832
|
+
if (card.classList.contains('is-live-entering')) {
|
|
833
|
+
card.addEventListener('animationend', () => card.classList.remove('is-live-entering'), {
|
|
834
|
+
once: true,
|
|
835
|
+
signal: pageLifecycle.signal,
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
const previewOperation = () => {
|
|
839
|
+
if (!currentOperationId) setLinkActivity(operationId);
|
|
840
|
+
};
|
|
841
|
+
const clearOperationPreview = () => {
|
|
842
|
+
if (!currentOperationId) setLinkActivity();
|
|
843
|
+
};
|
|
844
|
+
card.addEventListener('mouseenter', previewOperation, { signal: pageLifecycle.signal });
|
|
845
|
+
card.addEventListener('mouseleave', clearOperationPreview, { signal: pageLifecycle.signal });
|
|
846
|
+
card.addEventListener('focus', previewOperation, { signal: pageLifecycle.signal });
|
|
847
|
+
card.addEventListener('blur', clearOperationPreview, { signal: pageLifecycle.signal });
|
|
848
|
+
card.addEventListener('click', () => {
|
|
849
|
+
if (operationId === currentOperationId) closeInspector();
|
|
850
|
+
else selectOperation(operationId);
|
|
851
|
+
}, { signal: pageLifecycle.signal });
|
|
852
|
+
});
|
|
853
|
+
inspectorClose?.addEventListener('click', () => closeInspector(), { signal: pageLifecycle.signal });
|
|
854
|
+
const applyFacet = (facetId) => {
|
|
855
|
+
facetFocus?.classList.toggle('has-active', Boolean(facetId));
|
|
856
|
+
cards.forEach((card) => {
|
|
857
|
+
const matches = !facetId || (card.dataset.trajectoryFacets || '').split(' ').includes(facetId);
|
|
858
|
+
card.classList.toggle('is-facet-muted', Boolean(facetId) && !matches);
|
|
859
|
+
card.classList.toggle('is-facet-match', Boolean(facetId) && matches);
|
|
860
|
+
});
|
|
861
|
+
let visibleNormalizedRows = 0;
|
|
862
|
+
normalizedRows.forEach((row) => {
|
|
863
|
+
const matches = !facetId || (row.dataset.trajectoryFacets || '').split(' ').includes(facetId);
|
|
864
|
+
row.hidden = !matches;
|
|
865
|
+
if (matches) visibleNormalizedRows += 1;
|
|
866
|
+
});
|
|
867
|
+
if (normalizedEmpty) normalizedEmpty.hidden = visibleNormalizedRows > 0;
|
|
868
|
+
};
|
|
869
|
+
facetButtons.forEach((button) => button.addEventListener('click', () => {
|
|
870
|
+
const facetId = button.dataset.trajectoryFacet || '';
|
|
871
|
+
facetButtons.forEach((item) => item.setAttribute('aria-pressed', String(item === button)));
|
|
872
|
+
applyFacet(facetId);
|
|
873
|
+
if (facetFocus) facetFocus.open = false;
|
|
874
|
+
}, { signal: pageLifecycle.signal }));
|
|
875
|
+
const relativeSourceTime = (timestamp) => {
|
|
876
|
+
const valueMs = Date.parse(timestamp || '');
|
|
877
|
+
const startMs = Date.parse(sourceRecordList?.dataset.sourceRecordsStart || '');
|
|
878
|
+
if (!Number.isFinite(valueMs) || !Number.isFinite(startMs)) return '—';
|
|
879
|
+
const totalTenths = Math.max(0, Math.round((valueMs - startMs) / 100));
|
|
880
|
+
const minutes = Math.floor(totalTenths / 600);
|
|
881
|
+
const seconds = Math.floor((totalTenths % 600) / 10);
|
|
882
|
+
return String(minutes).padStart(2, '0') + ':' + String(seconds).padStart(2, '0') + '.' + (totalTenths % 10);
|
|
883
|
+
};
|
|
884
|
+
const appendSourceRecord = (record) => {
|
|
885
|
+
if (!sourceRecordList) return;
|
|
886
|
+
const details = document.createElement('details');
|
|
887
|
+
details.className = 'trajectory-raw-row';
|
|
888
|
+
const summary = document.createElement('summary');
|
|
889
|
+
const time = document.createElement('time');
|
|
890
|
+
time.textContent = relativeSourceTime(record.timestamp);
|
|
891
|
+
const kind = document.createElement('span');
|
|
892
|
+
kind.className = 'trajectory-raw-kind';
|
|
893
|
+
kind.textContent = String(record.sourceType || 'unknown');
|
|
894
|
+
const preview = document.createElement('strong');
|
|
895
|
+
preview.textContent = String(record.raw || sourceRecordLabels.empty).replace(/\\s+/g, ' ').trim().slice(0, 180);
|
|
896
|
+
const locator = document.createElement('code');
|
|
897
|
+
locator.className = 'trajectory-raw-id';
|
|
898
|
+
locator.textContent = '#' + String(record.sourceIndex ?? '—') + ' · ' + String(record.traceId || '—').slice(0, 12);
|
|
899
|
+
summary.append(time, kind, preview, locator);
|
|
900
|
+
const body = document.createElement('pre');
|
|
901
|
+
const notices = [record.redacted ? sourceRecordLabels.redacted : '', record.truncated ? sourceRecordLabels.truncated : ''].filter(Boolean);
|
|
902
|
+
body.textContent = String(record.raw || '') + (notices.length > 0 ? '\\n\\n' + notices.join('\\n') : '');
|
|
903
|
+
details.append(summary, body);
|
|
904
|
+
sourceRecordList.append(details);
|
|
905
|
+
};
|
|
906
|
+
const loadSourceRecords = () => {
|
|
907
|
+
if (!sourceRecordList || sourceRecordList.dataset.sourceRecordsLoaded === 'true') return Promise.resolve();
|
|
908
|
+
if (sourceRecordsPromise) return sourceRecordsPromise;
|
|
909
|
+
const state = sourceRecordList.querySelector('[data-source-records-state]');
|
|
910
|
+
sourceRecordsPromise = fetch(sourceRecordList.dataset.sourceRecordsEndpoint, {
|
|
911
|
+
headers: { Accept: 'application/json' },
|
|
912
|
+
cache: 'no-store',
|
|
913
|
+
signal: pageLifecycle.signal,
|
|
914
|
+
}).then((response) => {
|
|
915
|
+
if (!response.ok) throw new Error('source records request failed');
|
|
916
|
+
return response.json();
|
|
917
|
+
}).then((archive) => {
|
|
918
|
+
state?.remove();
|
|
919
|
+
if (archive.status === 'partial') {
|
|
920
|
+
const notice = document.createElement('div');
|
|
921
|
+
notice.className = 'trajectory-record-notice';
|
|
922
|
+
notice.textContent = sourceRecordLabels.partial
|
|
923
|
+
.replace('{retained}', String(archive.recordCount ?? archive.records?.length ?? 0))
|
|
924
|
+
.replace('{omitted}', String(archive.omittedRecordCount ?? 0));
|
|
925
|
+
sourceRecordList.append(notice);
|
|
926
|
+
}
|
|
927
|
+
(Array.isArray(archive.records) ? archive.records : []).forEach(appendSourceRecord);
|
|
928
|
+
sourceRecordList.dataset.sourceRecordsLoaded = 'true';
|
|
929
|
+
}).catch(() => {
|
|
930
|
+
sourceRecordsPromise = undefined;
|
|
931
|
+
if (state) state.textContent = sourceRecordLabels.loadFailed;
|
|
932
|
+
});
|
|
933
|
+
return sourceRecordsPromise;
|
|
934
|
+
};
|
|
935
|
+
const setTrajectoryMode = (mode) => {
|
|
936
|
+
shell.dataset.mode = mode;
|
|
937
|
+
if (mode !== 'semantic') pauseLiveFollow();
|
|
938
|
+
if (boundaryInfo) boundaryInfo.open = false;
|
|
939
|
+
if (mode !== 'semantic') closeInspector(false);
|
|
940
|
+
else scheduleOperationLinks();
|
|
941
|
+
if (mode === 'source') void loadSourceRecords();
|
|
942
|
+
document.querySelectorAll('[data-trajectory-mode]').forEach((item) => item.setAttribute('aria-pressed', String(item.dataset.trajectoryMode === mode)));
|
|
943
|
+
};
|
|
944
|
+
document.querySelectorAll('[data-trajectory-mode]').forEach((button) => {
|
|
945
|
+
button.addEventListener('click', () => {
|
|
946
|
+
const mode = button.dataset.trajectoryMode || 'semantic';
|
|
947
|
+
setTrajectoryMode(mode);
|
|
948
|
+
}, { signal: pageLifecycle.signal });
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
timeline?.addEventListener('keydown', (event) => {
|
|
952
|
+
if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return;
|
|
953
|
+
event.preventDefault();
|
|
954
|
+
const operationIds = semanticPanels.map((panel) => panel.dataset.trajectorySemanticPanel).filter(Boolean);
|
|
955
|
+
const direction = event.key === 'ArrowRight' ? 1 : -1;
|
|
956
|
+
const currentIndex = operationIds.indexOf(currentOperationId);
|
|
957
|
+
const nextIndex = currentIndex < 0
|
|
958
|
+
? (direction > 0 ? 0 : operationIds.length - 1)
|
|
959
|
+
: Math.min(operationIds.length - 1, Math.max(0, currentIndex + direction));
|
|
960
|
+
const nextId = operationIds[nextIndex];
|
|
961
|
+
if (nextId) selectOperation(nextId);
|
|
962
|
+
}, { signal: pageLifecycle.signal });
|
|
963
|
+
document.addEventListener('keydown', (event) => {
|
|
964
|
+
if (event.key !== 'Escape') return;
|
|
965
|
+
if (boundaryInfo?.open) {
|
|
966
|
+
boundaryInfo.open = false;
|
|
967
|
+
boundarySummary?.focus();
|
|
968
|
+
event.preventDefault();
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
if (currentOperationId) closeInspector();
|
|
972
|
+
}, { signal: pageLifecycle.signal });
|
|
973
|
+
document.addEventListener('pointerdown', (event) => {
|
|
974
|
+
if (boundaryInfo?.open && !boundaryInfo.contains(event.target)) boundaryInfo.open = false;
|
|
975
|
+
}, { signal: pageLifecycle.signal });
|
|
976
|
+
trajectoryBody?.addEventListener('transitionend', (event) => {
|
|
977
|
+
if (event.propertyName === 'grid-template-columns') finishLayoutTransition();
|
|
978
|
+
}, { signal: pageLifecycle.signal });
|
|
979
|
+
window.addEventListener('resize', scheduleOperationLinks, { passive: true, signal: pageLifecycle.signal });
|
|
980
|
+
const layoutResizeObserver = typeof ResizeObserver === 'function' && timeline
|
|
981
|
+
? new ResizeObserver(scheduleOperationLinks)
|
|
982
|
+
: undefined;
|
|
983
|
+
layoutResizeObserver?.observe(timeline);
|
|
984
|
+
const refreshTrajectorySnapshot = async () => {
|
|
985
|
+
const eventPosition = (card) => Number.parseFloat(card.style.getPropertyValue('--event-x')) || 0;
|
|
986
|
+
const currentCards = Array.from(shell.querySelectorAll('[data-trajectory-operation]'));
|
|
987
|
+
const previousEnd = currentCards.length > 0 ? Math.max(...currentCards.map(eventPosition)) : -1;
|
|
988
|
+
const response = await fetch(window.location.href, {
|
|
989
|
+
headers: { Accept: 'text/html' },
|
|
990
|
+
cache: 'no-store',
|
|
991
|
+
signal: pageLifecycle.signal,
|
|
992
|
+
});
|
|
993
|
+
if (!response.ok) throw new Error('trajectory snapshot request failed');
|
|
994
|
+
const incomingDocument = new DOMParser().parseFromString(await response.text(), 'text/html');
|
|
995
|
+
const incomingShell = incomingDocument.querySelector('.trajectory-shell');
|
|
996
|
+
if (!incomingShell) throw new Error('trajectory snapshot missing');
|
|
997
|
+
const replacement = document.importNode(incomingShell, true);
|
|
998
|
+
const selectedOperationId = currentOperationId;
|
|
999
|
+
const inspectorScrollTop = semanticPanelContainer?.scrollTop ?? 0;
|
|
1000
|
+
const expandedDetailSourceEventIds = detailBlocks
|
|
1001
|
+
.filter((block) => block.dataset.expanded === 'true')
|
|
1002
|
+
.map((block) => block.dataset.detailSourceEventId || '')
|
|
1003
|
+
.filter(Boolean);
|
|
1004
|
+
replacement.querySelectorAll('[data-trajectory-operation]').forEach((card) => {
|
|
1005
|
+
if (eventPosition(card) > previousEnd + 1) card.classList.add('is-live-entering');
|
|
1006
|
+
});
|
|
1007
|
+
if (selectedOperationId) {
|
|
1008
|
+
replacement.dataset.selectedOperationId = selectedOperationId;
|
|
1009
|
+
replacement.dataset.inspectorScrollTop = String(inspectorScrollTop);
|
|
1010
|
+
replacement.dataset.expandedDetailSourceEventIds = JSON.stringify(expandedDetailSourceEventIds);
|
|
1011
|
+
replacement.dataset.inspectorRestoring = 'true';
|
|
1012
|
+
}
|
|
1013
|
+
window.__omkTrajectoryDispose?.();
|
|
1014
|
+
shell.replaceWith(replacement);
|
|
1015
|
+
if (incomingDocument.title) document.title = incomingDocument.title;
|
|
1016
|
+
initializeTrajectoryPage();
|
|
1017
|
+
};
|
|
1018
|
+
const liveEndpoint = shell.dataset.liveEndpoint || '';
|
|
1019
|
+
const liveState = document.querySelector('[data-live-state]');
|
|
1020
|
+
const liveController = createTrajectoryLiveController({
|
|
1021
|
+
shell,
|
|
1022
|
+
timeline,
|
|
1023
|
+
liveState,
|
|
1024
|
+
followButton: liveFollowButton,
|
|
1025
|
+
followLabel: document.querySelector('[data-live-follow-label]'),
|
|
1026
|
+
liveEndpoint,
|
|
1027
|
+
labels: ${JSON.stringify({
|
|
1028
|
+
connecting: zh ? '连接中' : 'Connecting',
|
|
1029
|
+
live: zh ? '实时' : 'Live',
|
|
1030
|
+
syncing: zh ? '同步中' : 'Syncing',
|
|
1031
|
+
reconnecting: zh ? '重连中' : 'Reconnecting',
|
|
1032
|
+
failed: zh ? '实时更新失败' : 'Live update failed',
|
|
1033
|
+
following: zh ? '跟随中' : 'Following',
|
|
1034
|
+
resume: zh ? '跟随最新' : 'Follow latest',
|
|
1035
|
+
pending: zh ? '查看更新' : 'View update',
|
|
1036
|
+
completed: zh ? '任务已结束' : 'Task completed',
|
|
1037
|
+
aborted: zh ? '任务已中止' : 'Task aborted',
|
|
1038
|
+
interrupted: zh ? '任务已中断' : 'Task interrupted',
|
|
1039
|
+
unknown: zh ? '状态未知' : 'Status unknown',
|
|
1040
|
+
pauseTitle: zh ? '暂停自动跟随' : 'Pause automatic follow',
|
|
1041
|
+
})},
|
|
1042
|
+
getMode: () => shell.dataset.mode || 'semantic',
|
|
1043
|
+
setMode: setTrajectoryMode,
|
|
1044
|
+
isScrollTrackingSuppressed: () => shell.dataset.layoutTransition === 'true'
|
|
1045
|
+
|| shell.dataset.inspectorRestoring === 'true',
|
|
1046
|
+
refreshSnapshot: refreshTrajectorySnapshot,
|
|
1047
|
+
browserWindow: window,
|
|
1048
|
+
browserDocument: document,
|
|
1049
|
+
});
|
|
1050
|
+
pauseLiveFollow = liveController.pauseFollowing;
|
|
1051
|
+
const restoredOperationId = shell.dataset.selectedOperationId || '';
|
|
1052
|
+
const restoredInspectorScrollTop = Number(shell.dataset.inspectorScrollTop) || 0;
|
|
1053
|
+
let restoredExpandedDetailSourceEventIds = [];
|
|
1054
|
+
try {
|
|
1055
|
+
restoredExpandedDetailSourceEventIds = JSON.parse(shell.dataset.expandedDetailSourceEventIds || '[]');
|
|
1056
|
+
} catch {
|
|
1057
|
+
restoredExpandedDetailSourceEventIds = [];
|
|
1058
|
+
}
|
|
1059
|
+
delete shell.dataset.selectedOperationId;
|
|
1060
|
+
delete shell.dataset.inspectorScrollTop;
|
|
1061
|
+
delete shell.dataset.expandedDetailSourceEventIds;
|
|
1062
|
+
window.__omkTrajectoryDispose = () => {
|
|
1063
|
+
pageLifecycle.abort();
|
|
1064
|
+
layoutResizeObserver?.disconnect();
|
|
1065
|
+
liveController.dispose();
|
|
1066
|
+
if (pendingLayoutFrame !== undefined) cancelAnimationFrame(pendingLayoutFrame);
|
|
1067
|
+
pendingLayoutFrame = undefined;
|
|
1068
|
+
if (layoutTransitionTimer !== undefined) window.clearTimeout(layoutTransitionTimer);
|
|
1069
|
+
layoutTransitionTimer = undefined;
|
|
1070
|
+
if (layoutScrollReleaseTimer !== undefined) window.clearTimeout(layoutScrollReleaseTimer);
|
|
1071
|
+
layoutScrollReleaseTimer = undefined;
|
|
1072
|
+
copyFeedbackTimers.forEach((timer) => window.clearTimeout(timer));
|
|
1073
|
+
copyFeedbackTimers.clear();
|
|
1074
|
+
};
|
|
1075
|
+
if (restoredOperationId && operationIds.includes(restoredOperationId)) {
|
|
1076
|
+
selectOperation(restoredOperationId, false);
|
|
1077
|
+
detailBlocks.forEach((block) => {
|
|
1078
|
+
if (!restoredExpandedDetailSourceEventIds.includes(block.dataset.detailSourceEventId || '')) return;
|
|
1079
|
+
block.querySelector('[data-field-detail-toggle]')?.click();
|
|
1080
|
+
});
|
|
1081
|
+
requestAnimationFrame(() => {
|
|
1082
|
+
if (semanticPanelContainer) semanticPanelContainer.scrollTop = restoredInspectorScrollTop;
|
|
1083
|
+
delete shell.dataset.inspectorRestoring;
|
|
1084
|
+
});
|
|
1085
|
+
} else {
|
|
1086
|
+
delete shell.dataset.inspectorRestoring;
|
|
1087
|
+
scheduleOperationLinks();
|
|
1088
|
+
}
|
|
1089
|
+
};
|
|
1090
|
+
window.__omkInitializeTrajectory = initializeTrajectoryPage;
|
|
1091
|
+
initializeTrajectoryPage();
|
|
1092
|
+
})();
|
|
1093
|
+
</script>
|
|
1094
|
+
`, lang);
|
|
1095
|
+
}
|
|
1096
|
+
function projectReplay(model, lang, options) {
|
|
1097
|
+
const { steps, knowledgeEvidence, summary } = model;
|
|
1098
|
+
const evidenceById = new Map(knowledgeEvidence.map((item) => [item.id, item]));
|
|
1099
|
+
const startTimestamp = summary.observedStartTimestamp;
|
|
1100
|
+
const endTimestamp = summary.observedEndTimestamp;
|
|
1101
|
+
const startMs = parseTimestamp(startTimestamp);
|
|
1102
|
+
const endMs = parseTimestamp(endTimestamp);
|
|
1103
|
+
const durationMs = startMs !== undefined && endMs !== undefined && endMs >= startMs ? endMs - startMs : Math.max(1, steps.length - 1) * 1000;
|
|
1104
|
+
const cards = [];
|
|
1105
|
+
const operations = [];
|
|
1106
|
+
const milestones = [];
|
|
1107
|
+
const facetsById = new Map();
|
|
1108
|
+
const zh = lang === 'zh';
|
|
1109
|
+
const semanticSteps = steps.filter(projectsAsOperation);
|
|
1110
|
+
const operationLayout = buildOperationLayout(semanticSteps, startTimestamp, lang, options.pendingToolResults);
|
|
1111
|
+
const registerFacet = (group, key, label) => {
|
|
1112
|
+
const id = `${group}:${encodeURIComponent(key.trim().toLocaleLowerCase())}`;
|
|
1113
|
+
if (!facetsById.has(id))
|
|
1114
|
+
facetsById.set(id, { id, label, group });
|
|
1115
|
+
return id;
|
|
1116
|
+
};
|
|
1117
|
+
steps.forEach((step, index) => {
|
|
1118
|
+
if (!projectsToSemanticTrajectory(step))
|
|
1119
|
+
return;
|
|
1120
|
+
const operationId = `operation-${index}`;
|
|
1121
|
+
const evidence = evidenceForStep(step, evidenceById);
|
|
1122
|
+
if (step.stepKind === 'lifecycle') {
|
|
1123
|
+
const event = step.events[0];
|
|
1124
|
+
const tone = lifecycleMilestoneTone(event?.label);
|
|
1125
|
+
milestones.push({
|
|
1126
|
+
position: milestonePosition(index, steps, operationLayout.positionsByStepId, tone, step.timestamp, startTimestamp, lang, options.pendingToolResults),
|
|
1127
|
+
timestamp: step.timestamp,
|
|
1128
|
+
label: lifecycleEventLabel(event?.label, lang),
|
|
1129
|
+
tone,
|
|
1130
|
+
});
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
const position = operationLayout.positionsByStepId.get(step.id) ?? TRACK_START_PADDING;
|
|
1134
|
+
if (step.stepKind === 'tool_exchange') {
|
|
1135
|
+
const call = step.events[0];
|
|
1136
|
+
const result = step.events[1];
|
|
1137
|
+
const actionTimestamp = call?.timestamp ?? step.timestamp;
|
|
1138
|
+
const resultTimestamp = result?.timestamp ?? step.timestamp;
|
|
1139
|
+
const knowledgeTimestamp = evidenceTimestampForStep(evidence, step) ?? resultTimestamp;
|
|
1140
|
+
const input = toolInputPreview(call);
|
|
1141
|
+
const skillEvidence = evidence.filter((item) => item.knowledgeKind === 'skill');
|
|
1142
|
+
const operationTitle = skillEvidence.length > 0
|
|
1143
|
+
? (zh ? `读取 ${skillEvidence.map((item) => item.label).join('、')} Skill` : `Read ${skillEvidence.map((item) => item.label).join(', ')} skill`)
|
|
1144
|
+
: toolOperationTitle(call?.toolName ?? step.title, input, lang);
|
|
1145
|
+
const cardTitle = compactText(operationTitle, 72);
|
|
1146
|
+
const resultState = resolveToolResultState(step, options.pendingToolResults);
|
|
1147
|
+
const failed = resultState === 'failure';
|
|
1148
|
+
const duration = durationBetween(call?.timestamp, result?.timestamp, lang);
|
|
1149
|
+
const operationFacetIds = [
|
|
1150
|
+
registerFacet('tool', call?.toolName ?? step.title, call?.toolName ?? step.title),
|
|
1151
|
+
...evidence.map((item) => registerFacet('knowledge', item.knowledgeKind, knowledgeKindLabel(item, lang))),
|
|
1152
|
+
...(failed ? [registerFacet('status', 'failure', zh ? '失败' : 'Failure')] : []),
|
|
1153
|
+
].filter((id, facetIndex, all) => all.indexOf(id) === facetIndex);
|
|
1154
|
+
cards.push({
|
|
1155
|
+
id: `${operationId}-action`, operationId, lane: 'action', timestamp: actionTimestamp,
|
|
1156
|
+
position, tone: 'action', kindLabel: call?.toolName ?? step.title,
|
|
1157
|
+
title: cardTitle, detail: '',
|
|
1158
|
+
facetIds: operationFacetIds,
|
|
1159
|
+
rawId: call ? `${call.kind} · ${call.id}` : step.id, primary: true,
|
|
1160
|
+
});
|
|
1161
|
+
const firstEvidence = evidence[0];
|
|
1162
|
+
if (firstEvidence) {
|
|
1163
|
+
cards.push({
|
|
1164
|
+
id: `${operationId}-knowledge-${cards.length}`, operationId, lane: 'knowledge', timestamp: knowledgeTimestamp,
|
|
1165
|
+
position, tone: 'knowledge',
|
|
1166
|
+
kindLabel: evidence.length === 1
|
|
1167
|
+
? `${knowledgeKindLabel(firstEvidence, lang)} · ${ACCESS_LABELS[firstEvidence.accessKind][lang]}`
|
|
1168
|
+
: (zh ? `${evidence.length} 项 Knowledge` : `${evidence.length} Knowledge items`),
|
|
1169
|
+
title: evidence.length === 1 ? firstEvidence.label : `${firstEvidence.label} +${evidence.length - 1}`,
|
|
1170
|
+
detail: evidence.map((item) => item.sourceLocator ?? item.label).join(' · '),
|
|
1171
|
+
facetIds: operationFacetIds,
|
|
1172
|
+
rawId: evidence.flatMap((item) => item.evidenceRefs.map((ref) => ref.id)).join(' · '), primary: false,
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
cards.push({
|
|
1176
|
+
id: `${operationId}-result`, operationId, lane: 'result', timestamp: resultTimestamp,
|
|
1177
|
+
position, tone: resultCardTone(resultState),
|
|
1178
|
+
kindLabel: resultCardStatusLabel(resultState, lang),
|
|
1179
|
+
title: resultTitle(step, evidence, lang, input, call?.toolName ?? step.title, resultState),
|
|
1180
|
+
detail: resultCardDetail(step, result, duration, lang),
|
|
1181
|
+
facetIds: operationFacetIds,
|
|
1182
|
+
rawId: result ? `${result.kind} · ${result.id}` : step.id, primary: false,
|
|
1183
|
+
});
|
|
1184
|
+
operations.push({
|
|
1185
|
+
id: operationId,
|
|
1186
|
+
facetIds: operationFacetIds,
|
|
1187
|
+
selectionLabel: zh ? `${operationTitle} · ${step.events.length + evidence.length} 条关联证据` : `${operationTitle} · ${step.events.length + evidence.length} related records`,
|
|
1188
|
+
typeLabel: evidence.length > 0 ? (zh ? '语义操作 · Knowledge 访问' : 'Semantic operation · Knowledge access') : (zh ? '语义操作 · 工具执行' : 'Semantic operation · Tool execution'),
|
|
1189
|
+
title: operationTitle,
|
|
1190
|
+
summary: result
|
|
1191
|
+
? (zh ? `工具调用与返回结果已按调用标识配对${evidence.length > 0 ? ',相关 Knowledge 同组呈现' : ''}。` : `The tool call and result are paired by call identity${evidence.length > 0 ? ', with related Knowledge shown in the same group' : ''}.`)
|
|
1192
|
+
: resultState === 'pending'
|
|
1193
|
+
? (zh ? '已观测到工具调用,正在等待匹配的返回结果。' : 'The tool call is observable and its matching result is still pending.')
|
|
1194
|
+
: (zh ? '已观测到工具调用,但当前 trace 中没有匹配到返回结果。' : 'A tool call was observed, but no matching result appears in the trace.'),
|
|
1195
|
+
evidenceLabel: zh ? `${step.events.length + evidence.length} 条关联证据` : `${step.events.length + evidence.length} related records`,
|
|
1196
|
+
fields: [
|
|
1197
|
+
{ label: zh ? '执行' : 'Action', value: `${call?.toolName ?? step.title} · ${inferToolActionLabel(evidence, lang)}`, detail: compactText(input || (zh ? '未记录输入' : 'Input not recorded'), 520), detailKind: 'content' },
|
|
1198
|
+
{ label: zh ? '结果' : 'Result', value: `${toolStatusLabel(resultState, lang)}${duration ? ` · ${duration}` : ''}`, detail: result ? eventPreview(result, zh ? '工具没有返回内容。' : 'The tool returned no content.') : resultState === 'pending' ? (zh ? '正在等待工具结果写入 trace。' : 'Waiting for the tool result to appear in the trace.') : (zh ? '当前 trace 中没有匹配到工具结果。' : 'No matching tool result in this trace.'), detailSourceEventId: result?.id, detailKind: 'result' },
|
|
1199
|
+
{ label: 'Knowledge', value: evidence.length > 0 ? evidence.map((item) => `${knowledgeKindLabel(item, lang)} · ${item.label}`).join('、') : (zh ? '未关联' : 'Not associated'), detail: evidence.length > 0 ? evidence.map((item) => `${item.accessKind} · ${shortHash(item.contentHash)}`).join('\n') : (zh ? '未从本次工具交换投影出 Knowledge' : 'No Knowledge projected from this tool exchange') },
|
|
1200
|
+
],
|
|
1201
|
+
events: step.events,
|
|
1202
|
+
});
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
if (step.stepKind === 'runtime_context' || step.stepKind === 'skill_context') {
|
|
1206
|
+
const first = evidence[0];
|
|
1207
|
+
const contextEvent = step.events[0];
|
|
1208
|
+
const contextContent = observableContextContent(contextEvent);
|
|
1209
|
+
const knowledgeTimestamp = evidenceTimestampForStep(evidence, step) ?? step.timestamp;
|
|
1210
|
+
const operationFacetIds = first
|
|
1211
|
+
? [registerFacet('knowledge', first.knowledgeKind, knowledgeKindLabel(first, lang))]
|
|
1212
|
+
: [];
|
|
1213
|
+
cards.push({
|
|
1214
|
+
id: `${operationId}-knowledge-${cards.length}`, operationId, lane: 'knowledge', timestamp: knowledgeTimestamp,
|
|
1215
|
+
position, tone: 'knowledge',
|
|
1216
|
+
kindLabel: first
|
|
1217
|
+
? (evidence.length === 1 ? `${knowledgeKindLabel(first, lang)} · ${ACCESS_LABELS[first.accessKind][lang]}` : (zh ? `${evidence.length} 项 Knowledge` : `${evidence.length} Knowledge items`))
|
|
1218
|
+
: STEP_LABELS[step.stepKind][lang],
|
|
1219
|
+
title: first ? (evidence.length === 1 ? first.label : `${first.label} +${evidence.length - 1}`) : step.title,
|
|
1220
|
+
detail: first
|
|
1221
|
+
? evidence.map((item) => item.sourceLocator ?? item.label).join(' · ')
|
|
1222
|
+
: compactText(eventPreview(step.events[0], zh ? '任务上下文进入 trace' : 'Task context entered the trace'), 100),
|
|
1223
|
+
facetIds: operationFacetIds,
|
|
1224
|
+
rawId: first
|
|
1225
|
+
? evidence.flatMap((item) => item.evidenceRefs.map((ref) => ref.id)).join(' · ')
|
|
1226
|
+
: step.events.map((event) => event.id).join(' · '),
|
|
1227
|
+
primary: true,
|
|
1228
|
+
});
|
|
1229
|
+
operations.push({
|
|
1230
|
+
id: operationId,
|
|
1231
|
+
facetIds: operationFacetIds,
|
|
1232
|
+
selectionLabel: zh ? `${first?.label ?? STEP_LABELS[step.stepKind][lang]} · ${Math.max(1, step.events.length)} 条证据` : `${first?.label ?? STEP_LABELS[step.stepKind][lang]} · ${Math.max(1, step.events.length)} record${step.events.length === 1 ? '' : 's'}`,
|
|
1233
|
+
typeLabel: zh ? 'Knowledge · 进入上下文' : 'Knowledge · Entered context',
|
|
1234
|
+
title: first ? `${first.label} ${ACCESS_LABELS[first.accessKind][lang]}` : STEP_LABELS[step.stepKind][lang],
|
|
1235
|
+
summary: zh ? '该信息在本次任务的可见上下文中出现。' : 'This information appears in the observable context for the task.',
|
|
1236
|
+
evidenceLabel: zh ? `${step.events.length} 条规范化事件` : `${step.events.length} normalized event${step.events.length === 1 ? '' : 's'}`,
|
|
1237
|
+
fields: [
|
|
1238
|
+
{ label: 'Knowledge', value: evidence.map((item) => `${knowledgeKindLabel(item, lang)} · ${item.label}`).join('、') || step.title, detail: evidence.map((item) => item.sourceLocator ?? '').filter(Boolean).join('\n') || (zh ? '来源未记录' : 'Source not recorded') },
|
|
1239
|
+
{ label: zh ? '访问方式' : 'Access', value: first ? ACCESS_LABELS[first.accessKind][lang] : STEP_LABELS[step.stepKind][lang], detail: formatDisplayTimestamp(first?.firstSeen ?? step.timestamp, lang) },
|
|
1240
|
+
{
|
|
1241
|
+
label: zh ? '上下文内容' : 'Context content',
|
|
1242
|
+
value: contextContent ? (zh ? '源日志已记录可见内容' : 'Observable content recorded') : (zh ? '源日志未记录上下文内容' : 'Context content not recorded by the source log'),
|
|
1243
|
+
detail: contextContent ?? (zh ? '当前规范化事件只保留了上下文类型与元数据。' : 'The normalized event only retains the context type and metadata.'),
|
|
1244
|
+
copyable: Boolean(contextContent),
|
|
1245
|
+
detailSourceEventId: contextContent ? contextEvent?.id : undefined,
|
|
1246
|
+
detailKind: 'content',
|
|
1247
|
+
},
|
|
1248
|
+
{ label: zh ? '内容身份' : 'Content identity', value: first?.contentHash ? `sha256:${shortHash(first.contentHash)}` : (zh ? '未记录哈希' : 'Hash not recorded'), detail: zh ? '只标识观测到的内容,不推断是否被模型采用' : 'Identifies observed content without inferring model use' },
|
|
1249
|
+
],
|
|
1250
|
+
events: step.events,
|
|
1251
|
+
});
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
const event = step.events[0];
|
|
1255
|
+
const modelActivity = step.stepKind === 'model_activity';
|
|
1256
|
+
const eventModel = replayEventModel(step, event);
|
|
1257
|
+
const conversation = ['user_request', 'user_message', 'user_correction', 'assistant_message', 'model_activity'].includes(step.stepKind);
|
|
1258
|
+
const lane = conversation ? 'conversation' : 'result';
|
|
1259
|
+
const tone = step.stepKind === 'user_correction'
|
|
1260
|
+
? 'warning'
|
|
1261
|
+
: modelActivity
|
|
1262
|
+
? 'reasoning'
|
|
1263
|
+
: step.toolStatus === 'failure'
|
|
1264
|
+
? 'failure'
|
|
1265
|
+
: conversation ? 'message' : 'result';
|
|
1266
|
+
const opaqueModelActivity = modelActivity && event?.contentVisibility === 'opaque';
|
|
1267
|
+
const text = eventPreview(event, opaqueModelActivity
|
|
1268
|
+
? (zh ? '不可见' : 'Unavailable')
|
|
1269
|
+
: (zh ? '没有可展示的事件内容。' : 'No event content available.'));
|
|
1270
|
+
const operationFacetIds = step.toolStatus === 'failure'
|
|
1271
|
+
? [registerFacet('status', 'failure', zh ? '失败' : 'Failure')]
|
|
1272
|
+
: [];
|
|
1273
|
+
cards.push({
|
|
1274
|
+
id: `${operationId}-event`, operationId, lane, timestamp: step.timestamp, position, tone,
|
|
1275
|
+
conversationRole: conversation ? (step.stepKind === 'assistant_message' || modelActivity ? 'assistant' : 'user') : undefined,
|
|
1276
|
+
kindLabel: modelActivity
|
|
1277
|
+
? STEP_LABELS.model_activity[lang]
|
|
1278
|
+
: conversation
|
|
1279
|
+
? (step.stepKind === 'user_correction' ? STEP_LABELS.user_correction[lang] : roleLabel(event, lang))
|
|
1280
|
+
: STEP_LABELS[step.stepKind][lang],
|
|
1281
|
+
model: eventModel,
|
|
1282
|
+
title: text,
|
|
1283
|
+
detail: '',
|
|
1284
|
+
facetIds: operationFacetIds,
|
|
1285
|
+
rawId: event ? `${event.kind} · ${event.id}` : step.id, primary: true,
|
|
1286
|
+
compact: opaqueModelActivity,
|
|
1287
|
+
});
|
|
1288
|
+
operations.push({
|
|
1289
|
+
id: operationId,
|
|
1290
|
+
facetIds: operationFacetIds,
|
|
1291
|
+
selectionLabel: zh ? `${STEP_LABELS[step.stepKind][lang]} · ${step.events.length} 条证据` : `${STEP_LABELS[step.stepKind][lang]} · ${step.events.length} record${step.events.length === 1 ? '' : 's'}`,
|
|
1292
|
+
typeLabel: modelActivity
|
|
1293
|
+
? (zh ? '模型活动 · 可观测事实' : 'Model activity · Observable fact')
|
|
1294
|
+
: conversation ? (zh ? '消息 · 对话' : 'Message · Conversation') : (zh ? '事件 · 系统结果' : 'Event · System result'),
|
|
1295
|
+
title: STEP_LABELS[step.stepKind][lang],
|
|
1296
|
+
summary: modelActivity
|
|
1297
|
+
? (opaqueModelActivity
|
|
1298
|
+
? (zh ? 'Trace 记录了模型思考事件,但没有暴露可读内容。' : 'The trace records model reasoning, but exposes no readable content.')
|
|
1299
|
+
: (zh ? '以下内容来自 trace 暴露的明文摘要或文本,不是 OMK 对未公开模型内部状态的推断。' : 'The content below is plaintext exposed by the trace, not an OMK inference about unexposed model internals.'))
|
|
1300
|
+
: conversation ? (zh ? '按 trace 中记录的角色、时间和内容客观呈现。' : 'Shown from the role, time, and content recorded in the trace.') : (zh ? '该事件按原始 trace 顺序呈现。' : 'This event is shown in source trace order.'),
|
|
1301
|
+
evidenceLabel: zh ? `${step.events.length} 条规范化事件` : `${step.events.length} normalized event${step.events.length === 1 ? '' : 's'}`,
|
|
1302
|
+
fields: modelActivity
|
|
1303
|
+
? [
|
|
1304
|
+
{
|
|
1305
|
+
label: zh ? '可见性' : 'Visibility',
|
|
1306
|
+
value: opaqueModelActivity ? (zh ? '内容不可见' : 'Content unavailable') : (zh ? '可读明文' : 'Readable plaintext'),
|
|
1307
|
+
detail: opaqueModelActivity
|
|
1308
|
+
? (zh ? '来源只提供加密内容;OMK 不解密,也不推断其含义。' : 'The source only provides encrypted content; OMK neither decrypts nor infers it.')
|
|
1309
|
+
: reasoningContentSourceLabel(event?.contentSource, lang),
|
|
1310
|
+
},
|
|
1311
|
+
{ label: zh ? '时间' : 'Time', value: formatRelativeTimestamp(step.timestamp, startTimestamp), detail: formatDisplayTimestamp(step.timestamp, lang) },
|
|
1312
|
+
...(eventModel ? [{ label: zh ? '模型' : 'Model', value: eventModel, detail: zh ? '由 trace 明确记录的事件模型' : 'Event model explicitly recorded by the trace' }] : []),
|
|
1313
|
+
...(opaqueModelActivity ? [] : [{
|
|
1314
|
+
label: zh ? '可见内容' : 'Visible content',
|
|
1315
|
+
value: text,
|
|
1316
|
+
detail: reasoningContentSourceLabel(event?.contentSource, lang),
|
|
1317
|
+
presentation: 'content',
|
|
1318
|
+
}]),
|
|
1319
|
+
]
|
|
1320
|
+
: [
|
|
1321
|
+
{ label: zh ? '角色' : 'Role', value: roleLabel(event, lang), detail: event?.kind ?? step.stepKind },
|
|
1322
|
+
{ label: zh ? '时间' : 'Time', value: formatRelativeTimestamp(step.timestamp, startTimestamp), detail: formatDisplayTimestamp(step.timestamp, lang) },
|
|
1323
|
+
...(eventModel ? [{ label: zh ? '模型' : 'Model', value: eventModel, detail: zh ? '由 trace 明确记录的事件模型' : 'Event model explicitly recorded by the trace' }] : []),
|
|
1324
|
+
{
|
|
1325
|
+
label: zh ? '内容' : 'Content',
|
|
1326
|
+
value: text,
|
|
1327
|
+
detail: zh ? '未做隐藏意图或原因推断' : 'No hidden-intent or causal inference',
|
|
1328
|
+
presentation: 'content',
|
|
1329
|
+
},
|
|
1330
|
+
],
|
|
1331
|
+
events: step.events,
|
|
1332
|
+
});
|
|
1333
|
+
});
|
|
1334
|
+
const sizedCards = cards.map((card) => ({
|
|
1335
|
+
...card,
|
|
1336
|
+
row: card.lane === 'conversation' && card.conversationRole === 'assistant' ? 1 : 0,
|
|
1337
|
+
width: card.compact ? 14 : adaptiveCardWidth([card.kindLabel, card.model].filter(Boolean).join(' · '), compactText(inlineMarkdownText(card.title), 72), card.detail),
|
|
1338
|
+
}));
|
|
1339
|
+
const toolOperationCenters = new Map();
|
|
1340
|
+
sizedCards.filter((card) => card.lane === 'action').forEach((actionCard) => {
|
|
1341
|
+
const operationCards = sizedCards.filter((card) => card.operationId === actionCard.operationId);
|
|
1342
|
+
const columnWidth = Math.max(...operationCards.map((card) => card.width));
|
|
1343
|
+
toolOperationCenters.set(actionCard.operationId, actionCard.position + columnWidth / 2);
|
|
1344
|
+
});
|
|
1345
|
+
const placedCards = sizedCards.map((card) => {
|
|
1346
|
+
const operationCenter = toolOperationCenters.get(card.operationId);
|
|
1347
|
+
return operationCenter === undefined
|
|
1348
|
+
? card
|
|
1349
|
+
: { ...card, position: operationCenter - card.width / 2 };
|
|
1350
|
+
});
|
|
1351
|
+
const laneRows = {
|
|
1352
|
+
conversation: placedCards.some((card) => card.lane === 'conversation') ? 2 : 1,
|
|
1353
|
+
knowledge: 1,
|
|
1354
|
+
action: 1,
|
|
1355
|
+
result: 1,
|
|
1356
|
+
};
|
|
1357
|
+
const facetOrder = { knowledge: 0, tool: 1, status: 2 };
|
|
1358
|
+
const facets = [...facetsById.values()].sort((a, b) => facetOrder[a.group] - facetOrder[b.group] || a.label.localeCompare(b.label, lang));
|
|
1359
|
+
return {
|
|
1360
|
+
cards: placedCards,
|
|
1361
|
+
operations,
|
|
1362
|
+
facets,
|
|
1363
|
+
laneRows,
|
|
1364
|
+
gaps: operationLayout.gaps,
|
|
1365
|
+
axisTicks: operationLayout.axisTicks,
|
|
1366
|
+
milestones,
|
|
1367
|
+
startTimestamp,
|
|
1368
|
+
endTimestamp,
|
|
1369
|
+
durationMs,
|
|
1370
|
+
detailWidth: operationLayout.detailWidth,
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
1373
|
+
function lifecycleEventLabel(label, lang) {
|
|
1374
|
+
const labels = {
|
|
1375
|
+
session_started: { zh: '会话开始', en: 'Session started' },
|
|
1376
|
+
session_ended: { zh: '会话结束', en: 'Session ended' },
|
|
1377
|
+
turn_started: { zh: '本轮开始', en: 'Turn started' },
|
|
1378
|
+
turn_completed: { zh: '本轮完成', en: 'Turn completed' },
|
|
1379
|
+
turn_aborted: { zh: '本轮中止', en: 'Turn aborted' },
|
|
1380
|
+
turn_interrupted: { zh: '本轮被打断', en: 'Turn interrupted' },
|
|
1381
|
+
};
|
|
1382
|
+
return labels[label ?? '']?.[lang] ?? (label || (lang === 'zh' ? '运行状态变化' : 'Lifecycle event'));
|
|
1383
|
+
}
|
|
1384
|
+
function lifecycleMilestoneTone(label) {
|
|
1385
|
+
if (label === 'session_started' || label === 'turn_started')
|
|
1386
|
+
return 'start';
|
|
1387
|
+
if (label === 'session_ended' || label === 'turn_completed')
|
|
1388
|
+
return 'end';
|
|
1389
|
+
if (label === 'turn_aborted' || label === 'turn_interrupted')
|
|
1390
|
+
return 'warning';
|
|
1391
|
+
return 'neutral';
|
|
1392
|
+
}
|
|
1393
|
+
function projectsToSemanticTrajectory(step) {
|
|
1394
|
+
return step.stepKind !== 'observation' && step.stepKind !== 'system_event';
|
|
1395
|
+
}
|
|
1396
|
+
function projectsAsOperation(step) {
|
|
1397
|
+
return projectsToSemanticTrajectory(step) && step.stepKind !== 'lifecycle';
|
|
1398
|
+
}
|
|
1399
|
+
function displayWidthUnits(value) {
|
|
1400
|
+
return [...value].reduce((total, character) => total + (/[^\x00-\xff]/.test(character) ? 2 : 1), 0);
|
|
1401
|
+
}
|
|
1402
|
+
function adaptiveCardWidth(kindLabel, title, detail = '') {
|
|
1403
|
+
const headUnits = 8 + displayWidthUnits(kindLabel);
|
|
1404
|
+
const titleUnitsPerLine = Math.ceil(displayWidthUnits(title) / 2);
|
|
1405
|
+
const detailUnits = Math.min(32, displayWidthUnits(detail));
|
|
1406
|
+
const requiredUnits = Math.max(headUnits, titleUnitsPerLine, detailUnits);
|
|
1407
|
+
if (requiredUnits <= 18)
|
|
1408
|
+
return REPLAY_CARD_SMALL_WIDTH;
|
|
1409
|
+
if (requiredUnits <= 27)
|
|
1410
|
+
return REPLAY_CARD_MEDIUM_WIDTH;
|
|
1411
|
+
return REPLAY_CARD_WIDTH;
|
|
1412
|
+
}
|
|
1413
|
+
function replayEventModel(step, event) {
|
|
1414
|
+
return step.stepKind === 'assistant_message' || step.stepKind === 'model_activity'
|
|
1415
|
+
? event?.model?.trim() || undefined
|
|
1416
|
+
: undefined;
|
|
1417
|
+
}
|
|
1418
|
+
function replayCardWidth(step, lang, pendingToolResults) {
|
|
1419
|
+
const event = step.events[0];
|
|
1420
|
+
if (step.stepKind === 'model_activity' && event?.contentVisibility === 'opaque')
|
|
1421
|
+
return REPLAY_COMPACT_CARD_WIDTH;
|
|
1422
|
+
if (step.stepKind === 'runtime_context' || step.stepKind === 'skill_context')
|
|
1423
|
+
return REPLAY_CARD_WIDTH;
|
|
1424
|
+
if (step.stepKind === 'tool_exchange') {
|
|
1425
|
+
const call = step.events[0];
|
|
1426
|
+
const result = step.events[1];
|
|
1427
|
+
const input = toolInputPreview(call).replace(/\s+/g, ' ').trim();
|
|
1428
|
+
const resultState = resolveToolResultState(step, pendingToolResults);
|
|
1429
|
+
const actionTitle = toolOperationTitle(call?.toolName ?? step.title, input, lang);
|
|
1430
|
+
const actionWidth = adaptiveCardWidth(call?.toolName ?? step.title, actionTitle);
|
|
1431
|
+
const resultWidth = adaptiveCardWidth(resultCardStatusLabel(resultState, lang), resultTitle(step, [], lang, input, call?.toolName ?? step.title, resultState), resultCardDetail(step, result, durationBetween(call?.timestamp, result?.timestamp, lang), lang));
|
|
1432
|
+
return Math.max(actionWidth, resultWidth, step.knowledgeEvidenceIds.length > 0 ? REPLAY_CARD_WIDTH : 0);
|
|
1433
|
+
}
|
|
1434
|
+
const kindLabel = step.stepKind === 'model_activity'
|
|
1435
|
+
? STEP_LABELS.model_activity[lang]
|
|
1436
|
+
: STEP_LABELS[step.stepKind][lang];
|
|
1437
|
+
const eventModel = replayEventModel(step, event);
|
|
1438
|
+
return adaptiveCardWidth([kindLabel, eventModel].filter(Boolean).join(' · '), compactText(inlineMarkdownText(eventPreview(event, step.title)), 72));
|
|
1439
|
+
}
|
|
1440
|
+
function replayLayoutTracks(step) {
|
|
1441
|
+
if (step.stepKind === 'tool_exchange') {
|
|
1442
|
+
return step.knowledgeEvidenceIds.length > 0
|
|
1443
|
+
? ['action', 'result', 'knowledge']
|
|
1444
|
+
: ['action', 'result'];
|
|
1445
|
+
}
|
|
1446
|
+
if (step.stepKind === 'runtime_context' || step.stepKind === 'skill_context')
|
|
1447
|
+
return ['knowledge'];
|
|
1448
|
+
if (['user_request', 'user_message', 'user_correction', 'assistant_message', 'model_activity'].includes(step.stepKind)) {
|
|
1449
|
+
return ['conversation'];
|
|
1450
|
+
}
|
|
1451
|
+
return ['result'];
|
|
1452
|
+
}
|
|
1453
|
+
function operationFlowAdvance(previousStep, step) {
|
|
1454
|
+
const compact = (candidate) => candidate?.stepKind === 'model_activity' && candidate.events[0]?.contentVisibility === 'opaque';
|
|
1455
|
+
return compact(previousStep) || compact(step) ? COMPACT_FLOW_ADVANCE : OPERATION_FLOW_ADVANCE;
|
|
1456
|
+
}
|
|
1457
|
+
function milestonePosition(stepIndex, steps, positionsByStepId, tone, timestamp, taskStartTimestamp, lang, pendingToolResults) {
|
|
1458
|
+
const milestoneMs = parseTimestamp(timestamp);
|
|
1459
|
+
const taskStartMs = parseTimestamp(taskStartTimestamp);
|
|
1460
|
+
if (tone === 'start'
|
|
1461
|
+
&& milestoneMs !== undefined
|
|
1462
|
+
&& taskStartMs !== undefined
|
|
1463
|
+
&& milestoneMs <= taskStartMs)
|
|
1464
|
+
return 4;
|
|
1465
|
+
const operationPositions = [...positionsByStepId.values()];
|
|
1466
|
+
const lastPosition = operationPositions.at(-1) ?? TRACK_START_PADDING;
|
|
1467
|
+
const lastStep = [...steps].reverse().find(projectsAsOperation);
|
|
1468
|
+
const previousStep = [...steps.slice(0, stepIndex)].reverse().find(projectsAsOperation);
|
|
1469
|
+
const nextStep = steps.slice(stepIndex + 1).find(projectsAsOperation);
|
|
1470
|
+
const previousPosition = previousStep ? positionsByStepId.get(previousStep.id) : undefined;
|
|
1471
|
+
const nextPosition = nextStep ? positionsByStepId.get(nextStep.id) : undefined;
|
|
1472
|
+
if (previousPosition === undefined)
|
|
1473
|
+
return 4;
|
|
1474
|
+
if (nextPosition === undefined)
|
|
1475
|
+
return lastPosition + (lastStep ? replayCardWidth(lastStep, lang, pendingToolResults) : REPLAY_CARD_WIDTH) + 5;
|
|
1476
|
+
return tone === 'start'
|
|
1477
|
+
? Math.max(4, nextPosition - 5)
|
|
1478
|
+
: previousPosition + (previousStep ? replayCardWidth(previousStep, lang, pendingToolResults) : REPLAY_CARD_WIDTH) + 5;
|
|
1479
|
+
}
|
|
1480
|
+
function buildOperationLayout(steps, startTimestamp, lang, pendingToolResults) {
|
|
1481
|
+
const gapWidth = 96;
|
|
1482
|
+
const gapPadding = 16;
|
|
1483
|
+
const laneLabelWidth = 108;
|
|
1484
|
+
const idleGapThresholdMs = 60_000;
|
|
1485
|
+
const positionsByStepId = new Map();
|
|
1486
|
+
const positions = [];
|
|
1487
|
+
const gaps = [];
|
|
1488
|
+
const rightEdgeByTrack = new Map();
|
|
1489
|
+
const taskStartMs = parseTimestamp(startTimestamp);
|
|
1490
|
+
// Session-level context may predate the selected task; keep its card without expanding the task axis.
|
|
1491
|
+
const inTaskTimeDomain = (timestamp) => {
|
|
1492
|
+
const value = parseTimestamp(timestamp);
|
|
1493
|
+
if (value === undefined || taskStartMs === undefined)
|
|
1494
|
+
return value;
|
|
1495
|
+
return Math.max(value, taskStartMs);
|
|
1496
|
+
};
|
|
1497
|
+
let previousPosition = TRACK_START_PADDING;
|
|
1498
|
+
let previousEndMs;
|
|
1499
|
+
steps.forEach((step, index) => {
|
|
1500
|
+
const startMs = inTaskTimeDomain(step.timestamp);
|
|
1501
|
+
let position = index === 0
|
|
1502
|
+
? TRACK_START_PADDING
|
|
1503
|
+
: previousPosition + operationFlowAdvance(steps[index - 1], step);
|
|
1504
|
+
if (index > 0
|
|
1505
|
+
&& startMs !== undefined
|
|
1506
|
+
&& previousEndMs !== undefined
|
|
1507
|
+
&& startMs - previousEndMs >= idleGapThresholdMs) {
|
|
1508
|
+
const occupiedRight = Math.max(position, ...rightEdgeByTrack.values());
|
|
1509
|
+
const gapPosition = occupiedRight + gapPadding;
|
|
1510
|
+
gaps.push({ position: gapPosition, width: gapWidth, durationMs: startMs - previousEndMs });
|
|
1511
|
+
position = gapPosition + gapWidth + gapPadding;
|
|
1512
|
+
}
|
|
1513
|
+
const tracks = replayLayoutTracks(step);
|
|
1514
|
+
for (const track of tracks) {
|
|
1515
|
+
const rightEdge = rightEdgeByTrack.get(track);
|
|
1516
|
+
if (rightEdge !== undefined)
|
|
1517
|
+
position = Math.max(position, rightEdge + OPERATION_LANE_GAP);
|
|
1518
|
+
}
|
|
1519
|
+
positions.push(position);
|
|
1520
|
+
positionsByStepId.set(step.id, position);
|
|
1521
|
+
const rightEdge = position + replayCardWidth(step, lang, pendingToolResults);
|
|
1522
|
+
tracks.forEach((track) => rightEdgeByTrack.set(track, rightEdge));
|
|
1523
|
+
previousPosition = position;
|
|
1524
|
+
const eventTimes = step.events
|
|
1525
|
+
.map((event) => inTaskTimeDomain(event.timestamp))
|
|
1526
|
+
.filter((value) => value !== undefined);
|
|
1527
|
+
previousEndMs = eventTimes.length > 0 ? Math.max(...eventTimes) : startMs ?? previousEndMs;
|
|
1528
|
+
});
|
|
1529
|
+
const tickStride = Math.max(1, Math.ceil(steps.length / 9));
|
|
1530
|
+
const axisTicks = steps.flatMap((step, index) => (index === 0 || index === steps.length - 1 || index % tickStride === 0
|
|
1531
|
+
? [{ position: positions[index] ?? TRACK_START_PADDING, label: formatRelativeTimestamp(step.timestamp, startTimestamp) }]
|
|
1532
|
+
: []));
|
|
1533
|
+
const lastPosition = positions.at(-1) ?? TRACK_START_PADDING;
|
|
1534
|
+
const lastWidth = steps.length > 0 ? replayCardWidth(steps[steps.length - 1], lang, pendingToolResults) : REPLAY_CARD_WIDTH;
|
|
1535
|
+
const occupiedRight = Math.max(lastPosition + lastWidth, ...rightEdgeByTrack.values());
|
|
1536
|
+
const detailWidth = Math.max(960, laneLabelWidth + occupiedRight + 24);
|
|
1537
|
+
return { positionsByStepId, gaps, axisTicks, detailWidth };
|
|
1538
|
+
}
|
|
1539
|
+
function evidenceTimestampForStep(evidence, step) {
|
|
1540
|
+
const eventIds = new Set(step.events.map((event) => event.id));
|
|
1541
|
+
return evidence
|
|
1542
|
+
.flatMap((item) => item.evidenceRefs)
|
|
1543
|
+
.filter((ref) => eventIds.has(ref.id) && parseTimestamp(ref.timestamp) !== undefined)
|
|
1544
|
+
.map((ref) => ref.timestamp)
|
|
1545
|
+
.sort((a, b) => (parseTimestamp(a) ?? 0) - (parseTimestamp(b) ?? 0))[0];
|
|
1546
|
+
}
|
|
1547
|
+
function renderLane(lane, projection, selectedOperationId, lang) {
|
|
1548
|
+
const labels = LANE_LABELS[lane][lang];
|
|
1549
|
+
const cards = projection.cards.filter((card) => card.lane === lane);
|
|
1550
|
+
const laneRows = projection.laneRows[lane];
|
|
1551
|
+
return `<section class="trajectory-lane${laneRows === 2 ? ' has-two-rows' : ''}" data-lane="${lane}" style="--lane-rows:${laneRows};">
|
|
1552
|
+
<div class="trajectory-lane-name"><strong>${e(labels.title)}</strong><span>${e(labels.detail)}</span></div>
|
|
1553
|
+
<div class="trajectory-track">${cards.length > 0 ? cards.map((card) => renderCard(card, projection.startTimestamp, selectedOperationId)).join('') : `<span class="trajectory-empty">${e(labels.empty)}</span>`}</div>
|
|
1554
|
+
</section>`;
|
|
1555
|
+
}
|
|
1556
|
+
function renderCard(card, startTimestamp, selectedOperationId) {
|
|
1557
|
+
const hasSelection = Boolean(selectedOperationId);
|
|
1558
|
+
const related = hasSelection && card.operationId === selectedOperationId;
|
|
1559
|
+
const classes = [
|
|
1560
|
+
'trajectory-event',
|
|
1561
|
+
`is-${card.tone}`,
|
|
1562
|
+
card.compact ? 'is-compact' : '',
|
|
1563
|
+
card.detail ? 'has-detail' : '',
|
|
1564
|
+
hasSelection ? (related ? 'is-related' : 'is-dimmed') : '',
|
|
1565
|
+
related && card.primary ? 'is-primary' : '',
|
|
1566
|
+
].filter(Boolean).join(' ');
|
|
1567
|
+
const titleText = compactText(inlineMarkdownText(card.title), 72);
|
|
1568
|
+
const accessibleLabel = [
|
|
1569
|
+
formatRelativeTimestamp(card.timestamp, startTimestamp),
|
|
1570
|
+
card.kindLabel,
|
|
1571
|
+
card.model,
|
|
1572
|
+
titleText,
|
|
1573
|
+
].filter(Boolean).join(' · ');
|
|
1574
|
+
return `<button class="${classes}" type="button" data-event-row="${card.row}"${card.conversationRole ? ` data-conversation-role="${card.conversationRole}"` : ''} style="--event-x:${Math.round(card.position)}px;--event-row:${card.row};--event-card-width:${card.width}px" data-trajectory-operation="${e(card.operationId)}" data-trajectory-facets="${e(card.facetIds.join(' '))}" data-primary="${card.primary}" aria-pressed="${related}"${card.compact ? ` aria-label="${e(accessibleLabel)}"` : ''} title="${e(accessibleLabel)}">
|
|
1575
|
+
<span class="trajectory-event-head"><span class="trajectory-event-time">${e(formatRelativeTimestamp(card.timestamp, startTimestamp))}</span><span class="trajectory-event-kind">${e(card.kindLabel)}</span>${card.model ? `<span class="trajectory-event-model" title="${e(card.model)}">${e(card.model)}</span>` : ''}</span>
|
|
1576
|
+
<span class="trajectory-event-body"><span class="trajectory-event-title">${renderSafeInlineMarkdown(card.title, { links: 'text', maxLength: 72 })}</span>${card.detail ? `<span class="trajectory-event-detail">${e(card.detail)}</span>` : ''}<span class="trajectory-event-raw">${e(card.rawId)}</span></span>
|
|
1577
|
+
</button>`;
|
|
1578
|
+
}
|
|
1579
|
+
function renderFacetFocus(facets, lang) {
|
|
1580
|
+
const zh = lang === 'zh';
|
|
1581
|
+
const groupLabel = {
|
|
1582
|
+
knowledge: zh ? '知识' : 'Knowledge',
|
|
1583
|
+
tool: zh ? '工具' : 'Tool',
|
|
1584
|
+
status: zh ? '状态' : 'Status',
|
|
1585
|
+
};
|
|
1586
|
+
const options = facets.map((facet) => `<button class="trajectory-focus-option" type="button" data-trajectory-facet="${e(facet.id)}" data-facet-group="${facet.group}" aria-pressed="false"><span class="trajectory-focus-swatch" aria-hidden="true"></span><span>${e(facet.label)}</span><small>${e(groupLabel[facet.group])}</small></button>`).join('');
|
|
1587
|
+
return `<details class="trajectory-focus"><summary aria-label="${zh ? '类型筛选' : 'Filter by type'}" title="${zh ? '类型筛选' : 'Filter by type'}"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 6h18"></path><path d="M7 12h10"></path><path d="M10 18h4"></path></svg></summary><div class="trajectory-focus-menu" role="group" aria-label="${zh ? '类型筛选' : 'Type filter'}"><span class="trajectory-focus-title">${zh ? '类型筛选' : 'Type filter'}</span><button class="trajectory-focus-option" type="button" data-trajectory-facet="" aria-pressed="true"><span class="trajectory-focus-swatch" aria-hidden="true"></span><span>${zh ? '全部类型' : 'All types'}</span><small>${zh ? '清除' : 'Clear'}</small></button>${options}</div></details>`;
|
|
1588
|
+
}
|
|
1589
|
+
function renderSemanticPanel(operation, selected, lang) {
|
|
1590
|
+
return `<div class="trajectory-operation-panel trajectory-fields" data-trajectory-semantic-panel="${e(operation.id)}" data-selection-label="${e(operation.selectionLabel)}" data-type-label="${e(operation.typeLabel)}" data-title="${e(operation.title)}" data-summary="${e(operation.summary)}" data-evidence-label="${e(operation.evidenceLabel)}"${selected ? '' : ' hidden'}>${operation.fields.map((field) => {
|
|
1591
|
+
const content = field.presentation === 'content'
|
|
1592
|
+
? `<div class="trajectory-field-content">${renderSafeInlineMarkdown(field.value)}</div>`
|
|
1593
|
+
: `<strong class="trajectory-field-value">${e(field.value)}</strong>`;
|
|
1594
|
+
return `<div class="trajectory-field${field.presentation === 'content' ? ' is-content' : ''}"><span class="trajectory-field-label">${e(field.label)}</span>${content}${renderFieldDetail(field, lang)}</div>`;
|
|
1595
|
+
}).join('')}</div>`;
|
|
1596
|
+
}
|
|
1597
|
+
function renderFieldDetail(field, lang) {
|
|
1598
|
+
const preview = fieldDetailPreview(field.detail);
|
|
1599
|
+
if ((!preview.truncated || !field.detailSourceEventId) && !field.copyable) {
|
|
1600
|
+
return `<code class="trajectory-field-detail">${e(field.detail)}</code>`;
|
|
1601
|
+
}
|
|
1602
|
+
const zh = lang === 'zh';
|
|
1603
|
+
const result = field.detailKind === 'result';
|
|
1604
|
+
const previewStatus = preview.truncatedByLines
|
|
1605
|
+
? (zh ? `已显示 ${preview.visibleLineCount} / ${preview.totalLineCount} 行` : `Showing ${preview.visibleLineCount} of ${preview.totalLineCount} lines`)
|
|
1606
|
+
: (zh ? `已显示部分内容 · 共 ${preview.totalLineCount} 行` : `Showing a preview · ${preview.totalLineCount} lines total`);
|
|
1607
|
+
const fullStatus = zh ? `完整内容 · ${preview.totalLineCount} 行` : `Full content · ${preview.totalLineCount} lines`;
|
|
1608
|
+
const expandLabel = zh ? (result ? '展开完整结果' : '展开完整内容') : (result ? 'Show full result' : 'Show full content');
|
|
1609
|
+
const collapseLabel = zh ? (result ? '收起结果' : '收起内容') : (result ? 'Collapse result' : 'Collapse content');
|
|
1610
|
+
const copyLabel = zh ? (result ? '复制结果' : '复制内容') : (result ? 'Copy result' : 'Copy content');
|
|
1611
|
+
const copiedLabel = zh ? '已复制' : 'Copied';
|
|
1612
|
+
const canExpand = preview.truncated && Boolean(field.detailSourceEventId);
|
|
1613
|
+
const displayedDetail = canExpand ? preview.text : field.detail;
|
|
1614
|
+
const sourceEventAttribute = field.detailSourceEventId
|
|
1615
|
+
? ` data-detail-source-event-id="${e(field.detailSourceEventId)}"`
|
|
1616
|
+
: '';
|
|
1617
|
+
const status = canExpand
|
|
1618
|
+
? `<span class="trajectory-field-detail-status" data-field-detail-status>${e(previewStatus)}</span>`
|
|
1619
|
+
: '<span></span>';
|
|
1620
|
+
const toggle = canExpand
|
|
1621
|
+
? `<button class="trajectory-field-detail-toggle" type="button" data-field-detail-toggle aria-expanded="false">${e(expandLabel)}</button>`
|
|
1622
|
+
: '';
|
|
1623
|
+
return `<div class="trajectory-field-detail-block" data-field-detail-block data-expanded="false" data-expandable="${String(canExpand)}"${sourceEventAttribute} data-preview-status="${e(previewStatus)}" data-full-status="${e(fullStatus)}" data-expand-label="${e(expandLabel)}" data-collapse-label="${e(collapseLabel)}" data-copy-label="${e(copyLabel)}" data-copied-label="${e(copiedLabel)}"><code class="trajectory-field-detail" data-field-detail>${e(displayedDetail)}</code><div class="trajectory-field-detail-actions">${status}<span class="trajectory-field-detail-controls">${toggle}<button class="trajectory-field-detail-copy" type="button" data-field-detail-copy aria-label="${e(copyLabel)}" title="${e(copyLabel)}">${icon('copy', { size: 13 })}</button></span></div></div>`;
|
|
1624
|
+
}
|
|
1625
|
+
function fieldDetailPreview(detail) {
|
|
1626
|
+
const lines = detail.replace(/\r\n/g, '\n').split('\n');
|
|
1627
|
+
const visibleLines = lines.slice(0, 8);
|
|
1628
|
+
const truncatedByLines = lines.length > visibleLines.length;
|
|
1629
|
+
let text = visibleLines.join('\n');
|
|
1630
|
+
const truncatedByLength = text.length > 720;
|
|
1631
|
+
if (truncatedByLength)
|
|
1632
|
+
text = `${text.slice(0, 719).trimEnd()}…`;
|
|
1633
|
+
return {
|
|
1634
|
+
text,
|
|
1635
|
+
totalLineCount: lines.length,
|
|
1636
|
+
visibleLineCount: visibleLines.length,
|
|
1637
|
+
truncated: truncatedByLines || truncatedByLength,
|
|
1638
|
+
truncatedByLines,
|
|
1639
|
+
};
|
|
1640
|
+
}
|
|
1641
|
+
function renderNormalizedEventList(events, operations, startTimestamp, lang) {
|
|
1642
|
+
const zh = lang === 'zh';
|
|
1643
|
+
const facetsByEventId = new Map();
|
|
1644
|
+
for (const operation of operations) {
|
|
1645
|
+
for (const event of operation.events) {
|
|
1646
|
+
const facetIds = facetsByEventId.get(event.id) ?? new Set();
|
|
1647
|
+
operation.facetIds.forEach((facetId) => facetIds.add(facetId));
|
|
1648
|
+
facetsByEventId.set(event.id, facetIds);
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
const rows = events.map((event) => {
|
|
1652
|
+
const content = event.fullText ?? event.snippet ?? '';
|
|
1653
|
+
const preview = compactText(content || event.label || (zh ? '无事件内容' : 'No event content'), 180);
|
|
1654
|
+
const source = `${event.sourceLineIndex !== undefined ? `#${event.sourceLineIndex} · ` : ''}${shortHash(event.traceId ?? event.id)}`;
|
|
1655
|
+
const facetIds = [...(facetsByEventId.get(event.id) ?? [])];
|
|
1656
|
+
const kind = event.sourceType ? `${event.kind} · ${event.sourceType}` : event.kind;
|
|
1657
|
+
return `<details class="trajectory-raw-row" data-trajectory-normalized-event="${e(event.id)}" data-trajectory-facets="${e(facetIds.join(' '))}"><summary><time>${e(formatRelativeTimestamp(event.timestamp, startTimestamp))}</time><span class="trajectory-raw-kind">${e(kind)}</span><strong>${e(preview)}</strong><code class="trajectory-raw-id">${e(source)}</code></summary><pre>${e(content || event.label || '')}</pre></details>`;
|
|
1658
|
+
}).join('');
|
|
1659
|
+
return `<section class="trajectory-raw-list" data-event-view="normalized" aria-label="${zh ? '按来源顺序排列的规范化事件' : 'Normalized events in source order'}"><header class="trajectory-raw-head"><span>${zh ? '时间' : 'Time'}</span><span>${zh ? '规范化 / 来源类型' : 'Normalized / source type'}</span><span>${zh ? '内容' : 'Content'}</span><span>${zh ? '来源位置' : 'Source position'}</span></header>${rows}<div class="trajectory-raw-empty" data-trajectory-normalized-empty hidden>${zh ? '没有符合当前类型筛选的规范化事件' : 'No normalized events match the current type filter'}</div></section>`;
|
|
1660
|
+
}
|
|
1661
|
+
function renderTrajectoryViewExplanation(model, lang) {
|
|
1662
|
+
const basisLabel = taskWindowBasisLabel(model, lang);
|
|
1663
|
+
if (lang === 'zh') {
|
|
1664
|
+
return `<h3>本页如何划定与呈现任务</h3>
|
|
1665
|
+
<p class="trajectory-boundary-flow">原始日志 → 规范化事件 → 语义轨迹</p>
|
|
1666
|
+
<dl class="trajectory-boundary-levels">
|
|
1667
|
+
<div><dt>原始日志</dt><dd>经脱敏和有界归档后保留的来源日志原文。</dd></div>
|
|
1668
|
+
<div><dt>规范化事件</dt><dd>OMK 将不同来源的私有格式转换成统一的 Trace IR。</dd></div>
|
|
1669
|
+
<div><dt>语义轨迹</dt><dd>面向人,把事件组织为对话、执行、结果和知识。</dd></div>
|
|
1670
|
+
</dl>
|
|
1671
|
+
<p class="trajectory-boundary-note">当前任务范围依据:${e(basisLabel)}。OMK 优先按 Agent turn 划定任务起止;Skill 只用于定位它属于哪次任务,不决定边界。三层并非一一对应,知识出现也不代表模型实际采用了它。</p>`;
|
|
1672
|
+
}
|
|
1673
|
+
return `<h3>How the three views relate</h3>
|
|
1674
|
+
<p class="trajectory-boundary-flow">Raw logs → Normalized events → Semantic trajectory</p>
|
|
1675
|
+
<dl class="trajectory-boundary-levels">
|
|
1676
|
+
<div><dt>Raw logs</dt><dd>Source log records retained after redaction and bounded archiving.</dd></div>
|
|
1677
|
+
<div><dt>Normalized events</dt><dd>Source-specific records converted by OMK into source-neutral Trace IR.</dd></div>
|
|
1678
|
+
<div><dt>Semantic trajectory</dt><dd>A human-readable projection across conversation, action, result, and knowledge lanes.</dd></div>
|
|
1679
|
+
</dl>
|
|
1680
|
+
<p class="trajectory-boundary-note">Current task boundary: ${e(basisLabel)}. OMK prefers the agent turn lifecycle; Skill evidence only locates the relevant task and does not define its boundaries. The layers are not one-to-one, and Knowledge appearing in context does not prove that the model used it.</p>`;
|
|
1681
|
+
}
|
|
1682
|
+
function taskWindowBasisLabel(model, lang) {
|
|
1683
|
+
const turnId = model.taskScope.turnId ? ` · ${model.taskScope.turnId}` : '';
|
|
1684
|
+
if (model.taskScope.basis === 'turn_id') {
|
|
1685
|
+
return lang === 'zh' ? `Agent turn${turnId}` : `Agent turn${turnId}`;
|
|
1686
|
+
}
|
|
1687
|
+
if (model.taskScope.basis === 'turn_lifecycle') {
|
|
1688
|
+
return lang === 'zh' ? 'Agent turn 生命周期' : 'Agent turn lifecycle';
|
|
1689
|
+
}
|
|
1690
|
+
if (model.taskScope.basis === 'user_message') {
|
|
1691
|
+
return lang === 'zh' ? '用户消息边界(来源未提供 turn 标识)' : 'User-message boundary (source has no turn identity)';
|
|
1692
|
+
}
|
|
1693
|
+
return lang === 'zh' ? '无法判定' : 'Unresolved';
|
|
1694
|
+
}
|
|
1695
|
+
function renderSourceRecordList(model, startTimestamp, lang, sourceRecordsEndpoint) {
|
|
1696
|
+
const zh = lang === 'zh';
|
|
1697
|
+
const source = model.sourceRecords;
|
|
1698
|
+
const lazy = Boolean(sourceRecordsEndpoint && source.status !== 'unavailable' && source.recordCount > 0 && source.records.length === 0);
|
|
1699
|
+
const statusNotice = source.status === 'partial'
|
|
1700
|
+
? `<div class="trajectory-record-notice">${zh
|
|
1701
|
+
? `原始日志归档不完整:已保留 ${source.recordCount} 条,省略 ${source.omittedRecordCount} 条。`
|
|
1702
|
+
: `The raw-log archive is partial: ${source.recordCount} retained, ${source.omittedRecordCount} omitted.`}</div>`
|
|
1703
|
+
: '';
|
|
1704
|
+
const rows = source.records.map((record) => {
|
|
1705
|
+
const preview = compactText(record.raw || (zh ? '空记录' : 'Empty record'), 180);
|
|
1706
|
+
const locator = `#${record.sourceIndex} · ${shortHash(record.traceId)}`;
|
|
1707
|
+
const notices = [
|
|
1708
|
+
record.redacted ? (zh ? '【不透明加密载荷已省略】' : '[Opaque encrypted payload omitted]') : '',
|
|
1709
|
+
record.truncated ? (zh ? '【该记录已按归档上限截断】' : '[Record truncated by archive limit]') : '',
|
|
1710
|
+
].filter(Boolean).join('\n');
|
|
1711
|
+
return `<details class="trajectory-raw-row"><summary><time>${e(formatRelativeTimestamp(record.timestamp, startTimestamp))}</time><span class="trajectory-raw-kind">${e(record.sourceType)}</span><strong>${e(preview)}</strong><code class="trajectory-raw-id">${e(locator)}</code></summary><pre>${e(record.raw)}${notices ? `\n\n${e(notices)}` : ''}</pre></details>`;
|
|
1712
|
+
}).join('');
|
|
1713
|
+
const unavailable = source.status === 'unavailable'
|
|
1714
|
+
? `<div class="trajectory-raw-empty">${e(sourceRecordUnavailableLabel(source.reason, lang))}</div>`
|
|
1715
|
+
: '';
|
|
1716
|
+
const loading = lazy
|
|
1717
|
+
? `<div class="trajectory-raw-empty" data-source-records-state>${zh ? '正在读取原始日志…' : 'Loading raw logs…'}</div>`
|
|
1718
|
+
: '';
|
|
1719
|
+
const endpoint = lazy ? ` data-source-records-endpoint="${e(sourceRecordsEndpoint)}"` : '';
|
|
1720
|
+
const start = startTimestamp ? ` data-source-records-start="${e(startTimestamp)}"` : '';
|
|
1721
|
+
return `<section class="trajectory-raw-list" data-event-view="source"${endpoint}${start} data-source-records-loaded="${lazy ? 'false' : 'true'}" aria-label="${zh ? '按来源顺序排列的原始日志' : 'Raw logs in source order'}"><header class="trajectory-raw-head"><span>${zh ? '时间' : 'Time'}</span><span>${zh ? '来源类型' : 'Source type'}</span><span>${zh ? '原始 JSONL' : 'Raw JSONL'}</span><span>${zh ? '来源位置' : 'Source position'}</span></header>${statusNotice}${rows}${unavailable}${loading}</section>`;
|
|
1722
|
+
}
|
|
1723
|
+
function sourceRecordUnavailableLabel(reason, lang) {
|
|
1724
|
+
const zh = lang === 'zh';
|
|
1725
|
+
if (reason === 'source_missing')
|
|
1726
|
+
return zh ? '原始 trace 已不存在,无法生成原始日志归档。' : 'The original trace no longer exists.';
|
|
1727
|
+
if (reason === 'unsupported_source')
|
|
1728
|
+
return zh ? '当前来源暂不支持原始日志归档。' : 'Raw-log archiving is not supported for this source.';
|
|
1729
|
+
if (reason === 'read_failed')
|
|
1730
|
+
return zh ? '读取原始 trace 失败。' : 'The original trace could not be read.';
|
|
1731
|
+
if (reason === 'archive_limit')
|
|
1732
|
+
return zh ? '原始日志超过归档上限。' : 'Raw logs exceeded the archive limit.';
|
|
1733
|
+
if (reason === 'archive_invalid')
|
|
1734
|
+
return zh ? '原始日志归档无效或越过安全读取边界。' : 'The raw-log archive is invalid or outside the safe read boundary.';
|
|
1735
|
+
return zh
|
|
1736
|
+
? '此报告没有原始日志归档。请重新执行 observe ingest;这里不会用规范化事件冒充原始日志。'
|
|
1737
|
+
: 'This report has no raw-log archive. Re-run observe ingest; normalized events are never presented as raw logs.';
|
|
1738
|
+
}
|
|
1739
|
+
function renderAxis(projection, lang) {
|
|
1740
|
+
const ticks = visibleAxisTicks(projection)
|
|
1741
|
+
.map((tick) => `<span class="trajectory-tick" style="left:${Math.round(tick.position)}px">${e(tick.label)}</span>`)
|
|
1742
|
+
.join('');
|
|
1743
|
+
const gaps = projection.gaps.map((gap) => {
|
|
1744
|
+
const elapsed = formatElapsed(gap.durationMs, lang);
|
|
1745
|
+
const label = lang === 'zh' ? `${elapsed} · 无可观测事件` : `${elapsed} · no observable events`;
|
|
1746
|
+
return `<span class="trajectory-gap-axis" style="left:${Math.round(gap.position)}px;width:${Math.round(gap.width)}px" title="${e(label)}">${e(label)}</span>`;
|
|
1747
|
+
}).join('');
|
|
1748
|
+
const milestones = projection.milestones.map((milestone) => `<span class="trajectory-milestone-axis is-${milestone.tone}" style="left:${Math.round(milestone.position)}px" title="${e(milestone.label)}"><time>${e(formatRelativeTimestamp(milestone.timestamp, projection.startTimestamp))}</time><strong>${e(milestone.label)}</strong></span>`).join('');
|
|
1749
|
+
return `${ticks}${gaps}${milestones}`;
|
|
1750
|
+
}
|
|
1751
|
+
function renderGuides(projection) {
|
|
1752
|
+
const ticks = visibleAxisTicks(projection)
|
|
1753
|
+
.map((tick) => `<span class="trajectory-guide" style="left:${Math.round(tick.position)}px"></span>`)
|
|
1754
|
+
.join('');
|
|
1755
|
+
const gaps = projection.gaps
|
|
1756
|
+
.map((gap) => `<span class="trajectory-gap-band" style="left:${Math.round(gap.position)}px;width:${Math.round(gap.width)}px"></span>`)
|
|
1757
|
+
.join('');
|
|
1758
|
+
const milestones = projection.milestones
|
|
1759
|
+
.filter((milestone) => !(milestone.tone === 'start' && milestone.position <= TRACK_START_PADDING))
|
|
1760
|
+
.map((milestone) => `<span class="trajectory-milestone-line is-${milestone.tone}" style="left:${Math.round(milestone.position)}px"></span>`)
|
|
1761
|
+
.join('');
|
|
1762
|
+
return `${ticks}${gaps}${milestones}`;
|
|
1763
|
+
}
|
|
1764
|
+
function visibleAxisTicks(projection) {
|
|
1765
|
+
const candidates = projection.axisTicks
|
|
1766
|
+
.filter((tick) => !projection.milestones.some((milestone) => Math.abs(milestone.position - tick.position) < 110))
|
|
1767
|
+
.sort((left, right) => left.position - right.position);
|
|
1768
|
+
const selectedFromRight = [];
|
|
1769
|
+
for (let index = candidates.length - 1; index >= 0; index -= 1) {
|
|
1770
|
+
const tick = candidates[index];
|
|
1771
|
+
const rightNeighbor = selectedFromRight.at(-1);
|
|
1772
|
+
const occupiedWidth = AXIS_TICK_PADDING + tick.label.length * AXIS_TICK_GLYPH_WIDTH + AXIS_TICK_CLEARANCE;
|
|
1773
|
+
if (!rightNeighbor || rightNeighbor.position - tick.position >= occupiedWidth) {
|
|
1774
|
+
selectedFromRight.push(tick);
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
return selectedFromRight.reverse();
|
|
1778
|
+
}
|
|
1779
|
+
function renderIntegrityWarning(model, lang) {
|
|
1780
|
+
if (model.integrity.status === 'complete')
|
|
1781
|
+
return '';
|
|
1782
|
+
const zh = lang === 'zh';
|
|
1783
|
+
const details = model.integrity.notices.map((notice) => INTEGRITY_LABELS[notice.code](notice.count, lang)).join(zh ? ';' : '; ');
|
|
1784
|
+
const hasDataDamage = model.integrity.notices.some((notice) => notice.code !== 'timeline_truncated');
|
|
1785
|
+
const heading = hasDataDamage
|
|
1786
|
+
? (zh ? '轨迹数据存在限制:' : 'Trajectory data has limitations: ')
|
|
1787
|
+
: (zh ? '语义轨迹为摘要展示:' : 'Semantic trajectory is summarized: ');
|
|
1788
|
+
return `<div class="trajectory-warning"><strong>${heading}</strong>${e(details)}</div>`;
|
|
1789
|
+
}
|
|
1790
|
+
function evidenceForStep(step, evidenceById) {
|
|
1791
|
+
return step.knowledgeEvidenceIds
|
|
1792
|
+
.map((id) => evidenceById.get(id))
|
|
1793
|
+
.filter((item) => Boolean(item));
|
|
1794
|
+
}
|
|
1795
|
+
function toolInputPreview(event) {
|
|
1796
|
+
if (!event)
|
|
1797
|
+
return '';
|
|
1798
|
+
const text = event.fullText ?? event.snippet ?? '';
|
|
1799
|
+
try {
|
|
1800
|
+
const value = JSON.parse(text);
|
|
1801
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
1802
|
+
const input = value;
|
|
1803
|
+
for (const key of ['cmd', 'command', 'file_path', 'path', 'url', 'query']) {
|
|
1804
|
+
if (typeof input[key] === 'string' && input[key])
|
|
1805
|
+
return input[key];
|
|
1806
|
+
}
|
|
1807
|
+
return JSON.stringify(value, null, 2);
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
catch {
|
|
1811
|
+
// Keep the source text when the tool input is not JSON.
|
|
1812
|
+
}
|
|
1813
|
+
return text;
|
|
1814
|
+
}
|
|
1815
|
+
function toolOperationTitle(toolName, input, lang) {
|
|
1816
|
+
const patchTargets = extractPatchTargets(input);
|
|
1817
|
+
if (patchTargets.length > 0) {
|
|
1818
|
+
const first = patchTargets[0];
|
|
1819
|
+
if (!first)
|
|
1820
|
+
return toolName;
|
|
1821
|
+
const action = first.action === 'Add'
|
|
1822
|
+
? (lang === 'zh' ? '新增文件' : 'Add file')
|
|
1823
|
+
: first.action === 'Delete'
|
|
1824
|
+
? (lang === 'zh' ? '删除文件' : 'Delete file')
|
|
1825
|
+
: (lang === 'zh' ? '修改文件' : 'Update file');
|
|
1826
|
+
const suffix = patchTargets.length > 1 ? ` +${patchTargets.length - 1}` : '';
|
|
1827
|
+
const separator = lang === 'zh' ? ':' : ': ';
|
|
1828
|
+
return `${action}${separator}${first.path}${suffix}`;
|
|
1829
|
+
}
|
|
1830
|
+
const normalized = input.replace(/\s+/g, ' ').trim();
|
|
1831
|
+
const scriptedOperation = scriptedOperationTitle(normalized, lang);
|
|
1832
|
+
if (scriptedOperation)
|
|
1833
|
+
return scriptedOperation;
|
|
1834
|
+
const toolOperation = semanticToolOperation(toolName, normalized, lang);
|
|
1835
|
+
if (toolOperation)
|
|
1836
|
+
return toolOperation.title;
|
|
1837
|
+
if (/bash|exec|shell/i.test(toolName)) {
|
|
1838
|
+
const shellOperation = shellOperationTitle(normalized, lang);
|
|
1839
|
+
if (shellOperation)
|
|
1840
|
+
return shellOperation;
|
|
1841
|
+
if (normalized && normalized.length <= 72)
|
|
1842
|
+
return normalized;
|
|
1843
|
+
return lang === 'zh' ? '执行 Bash 命令' : 'Run Bash command';
|
|
1844
|
+
}
|
|
1845
|
+
if (normalized && normalized.length <= 72)
|
|
1846
|
+
return normalized;
|
|
1847
|
+
return lang === 'zh' ? `调用 ${toolName}` : `Call ${toolName}`;
|
|
1848
|
+
}
|
|
1849
|
+
function scriptedOperationTitle(input, lang) {
|
|
1850
|
+
if (!input)
|
|
1851
|
+
return undefined;
|
|
1852
|
+
if (/\bALL_TOOLS\.filter\s*\(/.test(input))
|
|
1853
|
+
return lang === 'zh' ? '筛选可用工具' : 'Filter available tools';
|
|
1854
|
+
if (/\btools\.update_plan\s*\(/.test(input))
|
|
1855
|
+
return lang === 'zh' ? '更新任务计划' : 'Update task plan';
|
|
1856
|
+
if (/\btools\.apply_patch\s*\(/.test(input))
|
|
1857
|
+
return lang === 'zh' ? '应用代码修改' : 'Apply code changes';
|
|
1858
|
+
const toolCall = input.match(/\btools\.([A-Za-z0-9_]+)\s*\(/)?.[1];
|
|
1859
|
+
return toolCall ? (lang === 'zh' ? `调用 ${toolCall}` : `Call ${toolCall}`) : undefined;
|
|
1860
|
+
}
|
|
1861
|
+
function shellOperationTitle(input, lang) {
|
|
1862
|
+
if (!input)
|
|
1863
|
+
return undefined;
|
|
1864
|
+
const searchCommand = input.match(/^\s*(?:rg|grep)\b/i);
|
|
1865
|
+
if (searchCommand) {
|
|
1866
|
+
const quotedTerm = input.match(/["']([^"']+)["']/)?.[1];
|
|
1867
|
+
const term = quotedTerm ? compactText(quotedTerm, 42) : undefined;
|
|
1868
|
+
return term
|
|
1869
|
+
? (lang === 'zh' ? `搜索:${term}` : `Search: ${term}`)
|
|
1870
|
+
: (lang === 'zh' ? '搜索内容' : 'Search content');
|
|
1871
|
+
}
|
|
1872
|
+
const sedRead = input.match(/^\s*sed\s+-n\s+(?:"[^"]*"|'[^']*'|\S+)\s+(?:"([^"]+)"|'([^']+)'|([^\s;|]+))/i);
|
|
1873
|
+
const simpleRead = input.match(/^\s*(?:cat|head|tail)\b(?:\s+-\S+(?:\s+\S+)?)?\s+(?:"([^"]+)"|'([^']+)'|([^\s;|]+))/i);
|
|
1874
|
+
const readTarget = sedRead?.slice(1).find(Boolean) ?? simpleRead?.slice(1).find(Boolean);
|
|
1875
|
+
if (readTarget)
|
|
1876
|
+
return lang === 'zh' ? `读取:${compactText(readTarget, 48)}` : `Read: ${compactText(readTarget, 48)}`;
|
|
1877
|
+
const nodeCommand = parseNodeCommand(input);
|
|
1878
|
+
if (nodeCommand) {
|
|
1879
|
+
const semanticTitle = semanticNodeOperation(nodeCommand.args, lang)?.title;
|
|
1880
|
+
if (semanticTitle)
|
|
1881
|
+
return semanticTitle;
|
|
1882
|
+
if (nodeCommand.args[0]?.toLocaleLowerCase() === 'call' && nodeCommand.args[1]) {
|
|
1883
|
+
return lang === 'zh' ? `调用:${nodeCommand.args[1]}` : `Call: ${nodeCommand.args[1]}`;
|
|
1884
|
+
}
|
|
1885
|
+
const scriptName = nodeCommand.script.split('/').at(-1) ?? nodeCommand.script;
|
|
1886
|
+
const suffix = nodeCommand.args[0] ? ` ${nodeCommand.args[0]}` : '';
|
|
1887
|
+
return lang === 'zh' ? `运行:${scriptName}${suffix}` : `Run: ${scriptName}${suffix}`;
|
|
1888
|
+
}
|
|
1889
|
+
const gitCommand = input.match(/^\s*git\s+([^\s;|]+)/i)?.[1];
|
|
1890
|
+
if (gitCommand)
|
|
1891
|
+
return lang === 'zh' ? `运行:git ${gitCommand}` : `Run: git ${gitCommand}`;
|
|
1892
|
+
return undefined;
|
|
1893
|
+
}
|
|
1894
|
+
function semanticToolOperation(toolName, input, lang) {
|
|
1895
|
+
const normalized = toolName.trim().toLocaleLowerCase().replace(/[\s-]+/g, '_');
|
|
1896
|
+
if (/^(?:edit|patch|apply_patch)$/.test(normalized)) {
|
|
1897
|
+
return { action: 'update', title: lang === 'zh' ? '编辑内容' : 'Edit content' };
|
|
1898
|
+
}
|
|
1899
|
+
if (/^(?:wait|write_stdin)$/.test(normalized)) {
|
|
1900
|
+
const background = /(?:cell_id|session_id)/i.test(input);
|
|
1901
|
+
return {
|
|
1902
|
+
action: 'wait',
|
|
1903
|
+
title: lang === 'zh'
|
|
1904
|
+
? (background ? '等待后台任务' : '等待任务完成')
|
|
1905
|
+
: (background ? 'Wait for background task' : 'Wait for task'),
|
|
1906
|
+
};
|
|
1907
|
+
}
|
|
1908
|
+
if (/^(?:view_image|image_view)$/.test(normalized)) {
|
|
1909
|
+
return { action: 'read', title: lang === 'zh' ? '查看图片' : 'View image' };
|
|
1910
|
+
}
|
|
1911
|
+
return undefined;
|
|
1912
|
+
}
|
|
1913
|
+
function parseNodeCommand(input) {
|
|
1914
|
+
const tokens = tokenizeShellCommand(input);
|
|
1915
|
+
if (tokens[0]?.toLocaleLowerCase() !== 'node')
|
|
1916
|
+
return undefined;
|
|
1917
|
+
const scriptIndex = tokens.findIndex((token, index) => index > 0 && !token.startsWith('-'));
|
|
1918
|
+
const script = scriptIndex >= 0 ? tokens[scriptIndex] : undefined;
|
|
1919
|
+
if (!script)
|
|
1920
|
+
return undefined;
|
|
1921
|
+
return { script, args: tokens.slice(scriptIndex + 1) };
|
|
1922
|
+
}
|
|
1923
|
+
function tokenizeShellCommand(input) {
|
|
1924
|
+
const tokens = [];
|
|
1925
|
+
const matcher = /"((?:\\.|[^"\\])*)"|'([^']*)'|([^\s;|]+)/g;
|
|
1926
|
+
for (const match of input.matchAll(matcher)) {
|
|
1927
|
+
tokens.push((match[1] ?? match[2] ?? match[3] ?? '').replace(/\\(["\\])/g, '$1'));
|
|
1928
|
+
}
|
|
1929
|
+
return tokens;
|
|
1930
|
+
}
|
|
1931
|
+
function semanticNodeOperation(args, lang) {
|
|
1932
|
+
const mode = args[0]?.toLocaleLowerCase();
|
|
1933
|
+
if (!mode)
|
|
1934
|
+
return undefined;
|
|
1935
|
+
if (mode === 'call') {
|
|
1936
|
+
const operation = args[1];
|
|
1937
|
+
if (!operation)
|
|
1938
|
+
return undefined;
|
|
1939
|
+
return semanticIdentifierOperation(operation, lang);
|
|
1940
|
+
}
|
|
1941
|
+
return semanticIdentifierOperation(mode, lang);
|
|
1942
|
+
}
|
|
1943
|
+
function semanticIdentifierOperation(identifier, lang) {
|
|
1944
|
+
const tokens = identifier
|
|
1945
|
+
.split(/[._:/-]+/)
|
|
1946
|
+
.map((token) => token.toLocaleLowerCase())
|
|
1947
|
+
.filter(Boolean);
|
|
1948
|
+
if (tokens.length === 0)
|
|
1949
|
+
return undefined;
|
|
1950
|
+
const actionByToken = {
|
|
1951
|
+
list: 'list', query: 'search', search: 'search', find: 'search',
|
|
1952
|
+
get: 'read', read: 'read', fetch: 'read', detail: 'read', details: 'read', toc: 'read',
|
|
1953
|
+
resolve: 'resolve', create: 'create', add: 'create', new: 'create',
|
|
1954
|
+
update: 'update', edit: 'update', modify: 'update', delete: 'delete', remove: 'delete',
|
|
1955
|
+
export: 'export', import: 'import', verify: 'verify', check: 'verify', validate: 'verify',
|
|
1956
|
+
publish: 'publish',
|
|
1957
|
+
};
|
|
1958
|
+
const actionTokenIndex = tokens.findIndex((token) => actionByToken[token]);
|
|
1959
|
+
if (actionTokenIndex < 0)
|
|
1960
|
+
return undefined;
|
|
1961
|
+
const actionToken = tokens[actionTokenIndex] ?? '';
|
|
1962
|
+
const action = actionByToken[actionToken];
|
|
1963
|
+
if (!action)
|
|
1964
|
+
return undefined;
|
|
1965
|
+
const knownResourceTokens = new Set([
|
|
1966
|
+
'api', 'book', 'content', 'detail', 'details', 'doc', 'docs', 'document', 'documents',
|
|
1967
|
+
'file', 'files', 'issue', 'markdown', 'md', 'page', 'plan', 'report', 'result', 'results',
|
|
1968
|
+
'toc', 'tool', 'tools', 'url', 'urls',
|
|
1969
|
+
]);
|
|
1970
|
+
const firstMeaningfulIndex = tokens.findIndex((token) => actionByToken[token] || knownResourceTokens.has(token));
|
|
1971
|
+
const meaningfulTokens = tokens.slice(Math.max(0, firstMeaningfulIndex));
|
|
1972
|
+
const targetTokens = meaningfulTokens.filter((token) => !actionByToken[token] || token === 'detail' || token === 'details' || token === 'toc');
|
|
1973
|
+
const target = semanticResourceLabel(targetTokens, lang);
|
|
1974
|
+
const actionLabels = {
|
|
1975
|
+
list: { zh: '列出', en: 'List' },
|
|
1976
|
+
read: { zh: '读取', en: 'Read' },
|
|
1977
|
+
resolve: { zh: '解析', en: 'Resolve' },
|
|
1978
|
+
create: { zh: '创建', en: 'Create' },
|
|
1979
|
+
update: { zh: '更新', en: 'Update' },
|
|
1980
|
+
delete: { zh: '删除', en: 'Delete' },
|
|
1981
|
+
export: { zh: '导出', en: 'Export' },
|
|
1982
|
+
import: { zh: '导入', en: 'Import' },
|
|
1983
|
+
search: { zh: '查询', en: 'Search' },
|
|
1984
|
+
verify: { zh: '校验', en: 'Verify' },
|
|
1985
|
+
publish: { zh: '发布', en: 'Publish' },
|
|
1986
|
+
wait: { zh: '等待', en: 'Wait for' },
|
|
1987
|
+
};
|
|
1988
|
+
const fallbackTarget = lang === 'zh' ? '可用项' : 'available items';
|
|
1989
|
+
const displayTarget = target || fallbackTarget;
|
|
1990
|
+
const separator = lang === 'en' || /^[A-Za-z0-9]/.test(displayTarget) ? ' ' : '';
|
|
1991
|
+
return { action, title: `${actionLabels[action][lang]}${separator}${displayTarget}` };
|
|
1992
|
+
}
|
|
1993
|
+
function semanticResourceLabel(tokens, lang) {
|
|
1994
|
+
if (tokens.length === 0)
|
|
1995
|
+
return '';
|
|
1996
|
+
const joined = tokens.join('_');
|
|
1997
|
+
const compoundLabels = {
|
|
1998
|
+
book_toc: { zh: '知识库目录', en: 'knowledge base contents' },
|
|
1999
|
+
doc_detail: { zh: '文档详情', en: 'document details' },
|
|
2000
|
+
document_detail: { zh: '文档详情', en: 'document details' },
|
|
2001
|
+
markdown_doc: { zh: 'Markdown 文档', en: 'Markdown document' },
|
|
2002
|
+
markdown_document: { zh: 'Markdown 文档', en: 'Markdown document' },
|
|
2003
|
+
};
|
|
2004
|
+
const compound = compoundLabels[joined];
|
|
2005
|
+
if (compound)
|
|
2006
|
+
return compound[lang];
|
|
2007
|
+
const labels = {
|
|
2008
|
+
api: { zh: 'API', en: 'API' }, book: { zh: '知识库', en: 'knowledge base' },
|
|
2009
|
+
content: { zh: '内容', en: 'content' }, detail: { zh: '详情', en: 'details' },
|
|
2010
|
+
details: { zh: '详情', en: 'details' }, doc: { zh: '文档', en: 'document' },
|
|
2011
|
+
docs: { zh: '文档', en: 'documents' }, document: { zh: '文档', en: 'document' },
|
|
2012
|
+
documents: { zh: '文档', en: 'documents' }, file: { zh: '文件', en: 'file' },
|
|
2013
|
+
files: { zh: '文件', en: 'files' }, issue: { zh: 'Issue', en: 'issue' },
|
|
2014
|
+
markdown: { zh: 'Markdown', en: 'Markdown' }, md: { zh: 'Markdown', en: 'Markdown' },
|
|
2015
|
+
page: { zh: '页面', en: 'page' }, plan: { zh: '计划', en: 'plan' },
|
|
2016
|
+
report: { zh: '报告', en: 'report' }, result: { zh: '结果', en: 'result' },
|
|
2017
|
+
results: { zh: '结果', en: 'results' }, toc: { zh: '目录', en: 'contents' },
|
|
2018
|
+
tool: { zh: '工具', en: 'tool' }, tools: { zh: '工具', en: 'tools' },
|
|
2019
|
+
url: { zh: 'URL', en: 'URL' }, urls: { zh: 'URL', en: 'URLs' },
|
|
2020
|
+
};
|
|
2021
|
+
const translated = tokens.map((token) => labels[token]?.[lang] ?? token);
|
|
2022
|
+
return lang === 'zh' ? translated.join('') : translated.join(' ');
|
|
2023
|
+
}
|
|
2024
|
+
function extractPatchTargets(input) {
|
|
2025
|
+
const expanded = input.replace(/\\r\\n|\\n/g, '\n');
|
|
2026
|
+
return [...expanded.matchAll(/\*\*\*\s+(Add|Update|Delete) File:\s*([^\n\\"']+)/g)]
|
|
2027
|
+
.map((match) => ({
|
|
2028
|
+
action: match[1],
|
|
2029
|
+
path: match[2]?.trim() ?? '',
|
|
2030
|
+
}))
|
|
2031
|
+
.filter((target) => target.path.length > 0);
|
|
2032
|
+
}
|
|
2033
|
+
function inferToolActionLabel(evidence, lang) {
|
|
2034
|
+
if (evidence.some((item) => item.accessKind === 'read'))
|
|
2035
|
+
return lang === 'zh' ? '文件读取' : 'File read';
|
|
2036
|
+
return lang === 'zh' ? '工具执行' : 'Tool execution';
|
|
2037
|
+
}
|
|
2038
|
+
function resultTitle(step, evidence, lang, input = '', toolName = '', resultState = resolveToolResultState(step, false)) {
|
|
2039
|
+
const zh = lang === 'zh';
|
|
2040
|
+
if (resultState === 'pending')
|
|
2041
|
+
return zh ? '结果获取中' : 'Waiting for result';
|
|
2042
|
+
if (resultState === 'missing')
|
|
2043
|
+
return zh ? '结果缺失' : 'Result missing';
|
|
2044
|
+
if (resultState === 'failure')
|
|
2045
|
+
return zh ? '工具执行失败' : 'Tool execution failed';
|
|
2046
|
+
if (resultState === 'cancelled')
|
|
2047
|
+
return zh ? '工具执行已取消' : 'Tool execution cancelled';
|
|
2048
|
+
const result = step.events[1];
|
|
2049
|
+
const skillRead = evidence.some((item) => item.knowledgeKind === 'skill' && item.accessKind === 'read');
|
|
2050
|
+
if (skillRead && result) {
|
|
2051
|
+
const text = result.fullText ?? result.snippet ?? '';
|
|
2052
|
+
const lineCount = text ? text.split(/\r?\n/).length : 0;
|
|
2053
|
+
return zh ? `返回 ${lineCount} 行内容` : `Returned ${lineCount} line${lineCount === 1 ? '' : 's'}`;
|
|
2054
|
+
}
|
|
2055
|
+
const facts = structuredResultFacts(result);
|
|
2056
|
+
if (facts.ok === false)
|
|
2057
|
+
return zh ? '返回错误信息' : 'Returned an error';
|
|
2058
|
+
const nodeCommand = parseNodeCommand(input);
|
|
2059
|
+
const action = nodeCommand
|
|
2060
|
+
? semanticNodeOperation(nodeCommand.args, lang)?.action
|
|
2061
|
+
: semanticToolOperation(toolName, input, lang)?.action;
|
|
2062
|
+
if (facts.itemCount !== undefined) {
|
|
2063
|
+
return zh ? `返回 ${facts.itemCount} 项` : `Returned ${facts.itemCount} item${facts.itemCount === 1 ? '' : 's'}`;
|
|
2064
|
+
}
|
|
2065
|
+
if (facts.title) {
|
|
2066
|
+
const title = compactText(facts.title, 52);
|
|
2067
|
+
if (action === 'create')
|
|
2068
|
+
return zh ? `已创建:${title}` : `Created: ${title}`;
|
|
2069
|
+
if (action === 'update')
|
|
2070
|
+
return zh ? `已更新:${title}` : `Updated: ${title}`;
|
|
2071
|
+
return zh ? `返回:${title}` : `Returned: ${title}`;
|
|
2072
|
+
}
|
|
2073
|
+
const completionLabels = {
|
|
2074
|
+
create: { zh: '创建完成', en: 'Creation completed' },
|
|
2075
|
+
update: { zh: '更新完成', en: 'Update completed' },
|
|
2076
|
+
delete: { zh: '删除完成', en: 'Deletion completed' },
|
|
2077
|
+
export: { zh: '导出完成', en: 'Export completed' },
|
|
2078
|
+
import: { zh: '导入完成', en: 'Import completed' },
|
|
2079
|
+
publish: { zh: '发布完成', en: 'Publish completed' },
|
|
2080
|
+
wait: { zh: '等待结束', en: 'Wait completed' },
|
|
2081
|
+
};
|
|
2082
|
+
if (action && completionLabels[action])
|
|
2083
|
+
return completionLabels[action][lang];
|
|
2084
|
+
return zh ? '工具返回结果' : 'Tool returned a result';
|
|
2085
|
+
}
|
|
2086
|
+
function structuredResultFacts(event) {
|
|
2087
|
+
const facts = {};
|
|
2088
|
+
const text = event?.fullText ?? event?.snippet ?? '';
|
|
2089
|
+
const parsed = parseStructuredResultText(text);
|
|
2090
|
+
if (parsed !== undefined)
|
|
2091
|
+
collectStructuredResultFacts(parsed, facts, undefined, 0);
|
|
2092
|
+
return facts;
|
|
2093
|
+
}
|
|
2094
|
+
function parseStructuredResultText(text) {
|
|
2095
|
+
const trimmed = text.trim();
|
|
2096
|
+
const outputIndex = trimmed.indexOf('Output:');
|
|
2097
|
+
const candidates = outputIndex >= 0
|
|
2098
|
+
? [trimmed.slice(outputIndex + 'Output:'.length).trim(), trimmed]
|
|
2099
|
+
: [trimmed];
|
|
2100
|
+
for (const candidate of candidates) {
|
|
2101
|
+
if (!candidate || !['{', '['].includes(candidate[0] ?? ''))
|
|
2102
|
+
continue;
|
|
2103
|
+
try {
|
|
2104
|
+
return JSON.parse(candidate);
|
|
2105
|
+
}
|
|
2106
|
+
catch {
|
|
2107
|
+
// Try the next observable representation.
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
return undefined;
|
|
2111
|
+
}
|
|
2112
|
+
function collectStructuredResultFacts(value, facts, parentKey, depth) {
|
|
2113
|
+
if (depth > 7 || value === null || value === undefined)
|
|
2114
|
+
return;
|
|
2115
|
+
if (typeof value === 'string') {
|
|
2116
|
+
const parsed = parseStructuredResultText(value);
|
|
2117
|
+
if (parsed !== undefined)
|
|
2118
|
+
collectStructuredResultFacts(parsed, facts, parentKey, depth + 1);
|
|
2119
|
+
return;
|
|
2120
|
+
}
|
|
2121
|
+
if (Array.isArray(value)) {
|
|
2122
|
+
if (['data', 'items', 'results', 'tools'].includes(parentKey ?? '') && facts.itemCount === undefined) {
|
|
2123
|
+
facts.itemCount = value.length;
|
|
2124
|
+
}
|
|
2125
|
+
value.slice(0, 4).forEach((item) => collectStructuredResultFacts(item, facts, parentKey, depth + 1));
|
|
2126
|
+
return;
|
|
2127
|
+
}
|
|
2128
|
+
if (typeof value !== 'object')
|
|
2129
|
+
return;
|
|
2130
|
+
const record = value;
|
|
2131
|
+
if (typeof record.ok === 'boolean' && facts.ok === undefined)
|
|
2132
|
+
facts.ok = record.ok;
|
|
2133
|
+
if (typeof record.title === 'string' && record.title.trim() && facts.title === undefined) {
|
|
2134
|
+
facts.title = record.title.trim();
|
|
2135
|
+
}
|
|
2136
|
+
for (const key of ['output', 'result', 'data', 'items', 'results', 'tools', 'content', 'text']) {
|
|
2137
|
+
if (key in record)
|
|
2138
|
+
collectStructuredResultFacts(record[key], facts, key, depth + 1);
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
function resolveToolResultState(step, pendingToolResults) {
|
|
2142
|
+
if (step.toolStatus === 'failure')
|
|
2143
|
+
return 'failure';
|
|
2144
|
+
if (step.toolStatus === 'cancelled')
|
|
2145
|
+
return 'cancelled';
|
|
2146
|
+
if (step.events.length > 1)
|
|
2147
|
+
return 'success';
|
|
2148
|
+
return pendingToolResults ? 'pending' : 'missing';
|
|
2149
|
+
}
|
|
2150
|
+
function resultCardTone(state) {
|
|
2151
|
+
if (state === 'failure')
|
|
2152
|
+
return 'failure';
|
|
2153
|
+
if (state === 'pending')
|
|
2154
|
+
return 'pending';
|
|
2155
|
+
if (state === 'missing' || state === 'cancelled')
|
|
2156
|
+
return 'warning';
|
|
2157
|
+
return 'result';
|
|
2158
|
+
}
|
|
2159
|
+
function resultCardStatusLabel(state, lang) {
|
|
2160
|
+
if (state === 'pending')
|
|
2161
|
+
return lang === 'zh' ? '获取中' : 'Pending';
|
|
2162
|
+
if (state === 'missing')
|
|
2163
|
+
return lang === 'zh' ? '结果缺失' : 'Missing';
|
|
2164
|
+
if (state === 'failure')
|
|
2165
|
+
return lang === 'zh' ? '失败' : 'Failed';
|
|
2166
|
+
if (state === 'cancelled')
|
|
2167
|
+
return lang === 'zh' ? '已取消' : 'Cancelled';
|
|
2168
|
+
return lang === 'zh' ? '成功' : 'Success';
|
|
2169
|
+
}
|
|
2170
|
+
function resultCardDetail(step, event, duration, lang) {
|
|
2171
|
+
if (!event)
|
|
2172
|
+
return '';
|
|
2173
|
+
if (step.toolStatus !== 'failure' && !event.isError)
|
|
2174
|
+
return duration;
|
|
2175
|
+
const failure = compactText(event.fullText ?? event.snippet ?? (lang === 'zh' ? '未记录失败信息' : 'Failure details not recorded'), 72);
|
|
2176
|
+
return [duration, failure].filter(Boolean).join(' · ');
|
|
2177
|
+
}
|
|
2178
|
+
function toolStatusLabel(state, lang) {
|
|
2179
|
+
const zh = lang === 'zh';
|
|
2180
|
+
if (state === 'pending')
|
|
2181
|
+
return zh ? '结果获取中' : 'Waiting for result';
|
|
2182
|
+
if (state === 'failure')
|
|
2183
|
+
return zh ? '失败' : 'Failed';
|
|
2184
|
+
if (state === 'cancelled')
|
|
2185
|
+
return zh ? '已取消' : 'Cancelled';
|
|
2186
|
+
if (state === 'success')
|
|
2187
|
+
return zh ? '成功返回' : 'Returned';
|
|
2188
|
+
return zh ? '结果缺失' : 'Result missing';
|
|
2189
|
+
}
|
|
2190
|
+
function knowledgeKindLabel(item, lang) {
|
|
2191
|
+
if (item.knowledgeKind === 'project_instruction')
|
|
2192
|
+
return lang === 'zh' ? '项目规则' : 'Project instruction';
|
|
2193
|
+
if (item.knowledgeKind === 'skill')
|
|
2194
|
+
return 'Skill';
|
|
2195
|
+
return lang === 'zh' ? '运行时证据' : 'Runtime evidence';
|
|
2196
|
+
}
|
|
2197
|
+
function roleLabel(event, lang) {
|
|
2198
|
+
if (event?.role === 'user')
|
|
2199
|
+
return lang === 'zh' ? '用户' : 'User';
|
|
2200
|
+
if (event?.role === 'assistant')
|
|
2201
|
+
return 'AI';
|
|
2202
|
+
if (event?.role === 'tool')
|
|
2203
|
+
return lang === 'zh' ? '工具' : 'Tool';
|
|
2204
|
+
return event?.role ?? (lang === 'zh' ? '系统' : 'System');
|
|
2205
|
+
}
|
|
2206
|
+
function reasoningContentSourceLabel(source, lang) {
|
|
2207
|
+
if (source === 'summary')
|
|
2208
|
+
return lang === 'zh' ? '来源:reasoning summary' : 'Source: reasoning summary';
|
|
2209
|
+
if (source === 'content')
|
|
2210
|
+
return lang === 'zh' ? '来源:reasoning content' : 'Source: reasoning content';
|
|
2211
|
+
return lang === 'zh' ? '来源:reasoning text' : 'Source: reasoning text';
|
|
2212
|
+
}
|
|
2213
|
+
function eventPreview(event, fallback) {
|
|
2214
|
+
return event?.fullText?.trim() || event?.snippet?.trim() || fallback;
|
|
2215
|
+
}
|
|
2216
|
+
function observableContextContent(event) {
|
|
2217
|
+
const fullText = event?.fullText?.trim();
|
|
2218
|
+
if (fullText && fullText !== '{}')
|
|
2219
|
+
return fullText;
|
|
2220
|
+
return event?.snippet?.trim() || undefined;
|
|
2221
|
+
}
|
|
2222
|
+
function compactText(value, maxLength) {
|
|
2223
|
+
const compact = value.replace(/\s+/g, ' ').trim();
|
|
2224
|
+
if (compact.length <= maxLength)
|
|
2225
|
+
return compact;
|
|
2226
|
+
return `${compact.slice(0, Math.max(0, maxLength - 1)).trimEnd()}…`;
|
|
2227
|
+
}
|
|
2228
|
+
function parseTimestamp(value) {
|
|
2229
|
+
if (!value)
|
|
2230
|
+
return undefined;
|
|
2231
|
+
const parsed = Date.parse(value);
|
|
2232
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
2233
|
+
}
|
|
2234
|
+
function durationBetween(start, end, lang) {
|
|
2235
|
+
const startMs = parseTimestamp(start);
|
|
2236
|
+
const endMs = parseTimestamp(end);
|
|
2237
|
+
if (startMs === undefined || endMs === undefined || endMs < startMs)
|
|
2238
|
+
return '';
|
|
2239
|
+
return formatElapsed(endMs - startMs, lang);
|
|
2240
|
+
}
|
|
2241
|
+
function formatRelativeTimestamp(value, start) {
|
|
2242
|
+
const valueMs = parseTimestamp(value);
|
|
2243
|
+
const startMs = parseTimestamp(start);
|
|
2244
|
+
if (valueMs === undefined || startMs === undefined)
|
|
2245
|
+
return '—';
|
|
2246
|
+
return formatRelativeTime(Math.max(0, valueMs - startMs));
|
|
2247
|
+
}
|
|
2248
|
+
function formatRelativeTime(milliseconds) {
|
|
2249
|
+
const totalTenths = Math.max(0, Math.round(milliseconds / 100));
|
|
2250
|
+
const minutes = Math.floor(totalTenths / 600);
|
|
2251
|
+
const seconds = Math.floor((totalTenths % 600) / 10);
|
|
2252
|
+
const tenths = totalTenths % 10;
|
|
2253
|
+
return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}.${tenths}`;
|
|
2254
|
+
}
|
|
2255
|
+
function formatDisplayTimestamp(value, lang) {
|
|
2256
|
+
if (!value)
|
|
2257
|
+
return lang === 'zh' ? '时间未知' : 'Time unknown';
|
|
2258
|
+
return value.slice(0, 19).replace('T', ' ');
|
|
2259
|
+
}
|
|
2260
|
+
function formatElapsed(milliseconds, lang) {
|
|
2261
|
+
const seconds = Math.max(0, milliseconds / 1000);
|
|
2262
|
+
if (seconds < 60) {
|
|
2263
|
+
const value = Number.isInteger(seconds) ? seconds.toFixed(0) : seconds.toFixed(1);
|
|
2264
|
+
return lang === 'zh' ? `${value} 秒` : `${value}s`;
|
|
2265
|
+
}
|
|
2266
|
+
const roundedSeconds = Math.round(seconds);
|
|
2267
|
+
const minutes = Math.floor(roundedSeconds / 60);
|
|
2268
|
+
const remainingSeconds = roundedSeconds % 60;
|
|
2269
|
+
return lang === 'zh' ? `${minutes} 分 ${remainingSeconds} 秒` : `${minutes}m ${remainingSeconds}s`;
|
|
2270
|
+
}
|
|
2271
|
+
function displaySourceKind(value) {
|
|
2272
|
+
if (value.toLowerCase() === 'codex')
|
|
2273
|
+
return 'Codex';
|
|
2274
|
+
if (value.toLowerCase() === 'claude')
|
|
2275
|
+
return 'Claude';
|
|
2276
|
+
return value;
|
|
2277
|
+
}
|
|
2278
|
+
function shortHash(value) {
|
|
2279
|
+
return value ? value.slice(0, 12) : '—';
|
|
2280
|
+
}
|