@hsupu/copilot-api 0.7.17 → 0.7.18-beta.2
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 +47 -49
- package/README.zh.md +39 -0
- package/config.example.yaml +272 -0
- package/dist/main.mjs +7125 -5888
- package/dist/main.mjs.map +1 -1
- package/package.json +24 -15
- package/ui/history-v1/index.html +149 -0
- package/ui/history-v1/script.js +1799 -0
- package/ui/history-v1/styles.css +1467 -0
- package/ui/history-v3/dist/assets/index-BJHz2Wfg.js +3 -0
- package/ui/history-v3/dist/assets/index-DZDkeXE1.css +1 -0
- package/ui/history-v3/dist/assets/vendor-C3jfkhqq.js +125 -0
- package/ui/history-v3/dist/assets/vue-jlQnwi-P.js +1 -0
- package/ui/history-v3/dist/index.html +15 -0
|
@@ -0,0 +1,1799 @@
|
|
|
1
|
+
// State
|
|
2
|
+
let currentSessionId = null
|
|
3
|
+
let currentEntryId = null
|
|
4
|
+
let currentEntry = null
|
|
5
|
+
let currentPage = 1
|
|
6
|
+
let totalPages = 1
|
|
7
|
+
let listSearchTimer = null
|
|
8
|
+
let detailSearchTimer = null
|
|
9
|
+
let detailSearchQuery = ""
|
|
10
|
+
|
|
11
|
+
// SVG icons (Ionicons5 style, consistent with v2)
|
|
12
|
+
const ICON_EXPAND =
|
|
13
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none" stroke="currentColor" stroke-width="32" stroke-linecap="round" stroke-linejoin="round"><polyline points="432 368 432 432 368 432"/><line x1="432" y1="432" x2="336" y2="336"/><polyline points="80 144 80 80 144 80"/><line x1="80" y1="80" x2="176" y2="176"/><polyline points="368 80 432 80 432 144"/><line x1="432" y1="80" x2="336" y2="176"/><polyline points="144 432 80 432 80 368"/><line x1="80" y1="432" x2="176" y2="336"/></svg>'
|
|
14
|
+
const ICON_CONTRACT =
|
|
15
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none" stroke="currentColor" stroke-width="32" stroke-linecap="round" stroke-linejoin="round"><polyline points="336 368 336 432 400 432"/><line x1="336" y1="432" x2="432" y2="336"/><polyline points="176 144 176 80 112 80"/><line x1="176" y1="80" x2="80" y2="176"/><polyline points="400 80 336 80 336 144"/><line x1="336" y1="80" x2="432" y2="176"/><polyline points="112 432 176 432 176 368"/><line x1="176" y1="432" x2="80" y2="336"/></svg>'
|
|
16
|
+
// Copy - Ionicons CopyOutline
|
|
17
|
+
const ICON_COPY =
|
|
18
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none" stroke="currentColor" stroke-width="32" stroke-linejoin="round"><rect x="128" y="128" width="336" height="336" rx="57" ry="57"/><path d="M383.5 128l.5-24a56.16 56.16 0 00-56-56H112a64.19 64.19 0 00-64 64v216a56.16 56.16 0 0056 56h24" stroke-linecap="round"/></svg>'
|
|
19
|
+
// Code view - Ionicons CodeSlashOutline
|
|
20
|
+
const ICON_CODE =
|
|
21
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none" stroke="currentColor" stroke-width="32" stroke-linecap="round" stroke-linejoin="round"><path d="M160 368L32 256l128-112"/><path d="M352 368l128-112-128-112"/><path d="M304 96l-96 320"/></svg>'
|
|
22
|
+
// Close - Ionicons CloseOutline
|
|
23
|
+
const ICON_CLOSE =
|
|
24
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none" stroke="currentColor" stroke-width="32" stroke-linecap="round" stroke-linejoin="round"><path d="M368 368L144 144"/><path d="M368 144L144 368"/></svg>'
|
|
25
|
+
// Search - Ionicons SearchOutline
|
|
26
|
+
const ICON_SEARCH =
|
|
27
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none" stroke="currentColor" stroke-width="32" stroke-linecap="round" stroke-linejoin="round"><path d="M221.09 64a157.09 157.09 0 10157.09 157.09A157.1 157.1 0 00221.09 64z"/><path d="M338.29 338.29L448 448"/></svg>'
|
|
28
|
+
// Refresh - Ionicons RefreshOutline
|
|
29
|
+
const ICON_REFRESH =
|
|
30
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none" stroke="currentColor" stroke-width="32" stroke-linecap="round" stroke-linejoin="round"><path d="M320 146s24.36-12-64-12a160 160 0 10160 160" /><polyline points="256 58 336 138 256 218"/></svg>'
|
|
31
|
+
// Download - Ionicons DownloadOutline
|
|
32
|
+
const ICON_DOWNLOAD =
|
|
33
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none" stroke="currentColor" stroke-width="32" stroke-linecap="round" stroke-linejoin="round"><path d="M336 176h40a40 40 0 0140 40v208a40 40 0 01-40 40H136a40 40 0 01-40-40V216a40 40 0 0140-40h40"/><polyline points="176 272 256 352 336 272"/><line x1="256" y1="48" x2="256" y2="336"/></svg>'
|
|
34
|
+
// Trash - Ionicons TrashOutline
|
|
35
|
+
const ICON_TRASH =
|
|
36
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none" stroke="currentColor" stroke-width="32" stroke-linecap="round" stroke-linejoin="round"><path d="M112 112l20 320c.95 18.49 14.4 32 32 32h184c17.67 0 30.87-13.51 32-32l20-320"/><line x1="80" y1="112" x2="432" y2="112"/><path d="M192 112V72h0a23.93 23.93 0 0124-24h80a23.93 23.93 0 0124 24h0v40" stroke-linecap="round"/></svg>'
|
|
37
|
+
|
|
38
|
+
// Collapse state for sections
|
|
39
|
+
let sectionCollapseState = { meta: false, request: false, response: false }
|
|
40
|
+
|
|
41
|
+
// Raw data registry - stores data for showRaw to avoid inline JSON in onclick
|
|
42
|
+
let rawDataRegistry = []
|
|
43
|
+
function registerRawData(data) {
|
|
44
|
+
const index = rawDataRegistry.length
|
|
45
|
+
rawDataRegistry.push(data)
|
|
46
|
+
return index
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Formatting utilities
|
|
50
|
+
function formatTime(ts) {
|
|
51
|
+
const d = new Date(ts)
|
|
52
|
+
const h = String(d.getHours()).padStart(2, "0")
|
|
53
|
+
const m = String(d.getMinutes()).padStart(2, "0")
|
|
54
|
+
const s = String(d.getSeconds()).padStart(2, "0")
|
|
55
|
+
return h + ":" + m + ":" + s
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function formatDate(ts) {
|
|
59
|
+
const d = new Date(ts)
|
|
60
|
+
const today = new Date()
|
|
61
|
+
if (d.toDateString() === today.toDateString()) {
|
|
62
|
+
return formatTime(ts)
|
|
63
|
+
}
|
|
64
|
+
const y = d.getFullYear()
|
|
65
|
+
const mo = String(d.getMonth() + 1).padStart(2, "0")
|
|
66
|
+
const day = String(d.getDate()).padStart(2, "0")
|
|
67
|
+
return y + "-" + mo + "-" + day + " " + formatTime(ts)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function formatNumber(n) {
|
|
71
|
+
if (n === undefined || n === null) return "-"
|
|
72
|
+
if (n >= 1000000) return (n / 1000000).toFixed(1) + "M"
|
|
73
|
+
if (n >= 1000) return (n / 1000).toFixed(1) + "K"
|
|
74
|
+
return n.toString()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function formatDuration(ms) {
|
|
78
|
+
if (!ms) return "-"
|
|
79
|
+
if (ms < 1000) return ms + "ms"
|
|
80
|
+
return (ms / 1000).toFixed(1) + "s"
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function escapeHtml(text) {
|
|
84
|
+
if (!text) return ""
|
|
85
|
+
const div = document.createElement("div")
|
|
86
|
+
div.textContent = text
|
|
87
|
+
return div.innerHTML
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// JSON Tree view builder (pure JS, no external deps)
|
|
91
|
+
function buildJsonTree(data, depth) {
|
|
92
|
+
if (depth === undefined) depth = 3
|
|
93
|
+
const container = document.createElement("div")
|
|
94
|
+
container.className = "json-tree"
|
|
95
|
+
const ul = document.createElement("ul")
|
|
96
|
+
buildNode(ul, null, data, depth, 0, false)
|
|
97
|
+
container.append(ul)
|
|
98
|
+
return container
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function buildNode(parent, key, value, maxDepth, currentDepth, isLast) {
|
|
102
|
+
const li = document.createElement("li")
|
|
103
|
+
|
|
104
|
+
if (value !== null && typeof value === "object") {
|
|
105
|
+
const isArray = Array.isArray(value)
|
|
106
|
+
const entries = isArray ? value : Object.entries(value)
|
|
107
|
+
const len = isArray ? value.length : Object.keys(value).length
|
|
108
|
+
const openBracket = isArray ? "[" : "{"
|
|
109
|
+
const closeBracket = isArray ? "]" : "}"
|
|
110
|
+
|
|
111
|
+
const toggle = document.createElement("span")
|
|
112
|
+
toggle.className = "jt-toggle"
|
|
113
|
+
toggle.textContent = currentDepth < maxDepth ? "\u25BE" : "\u25B8"
|
|
114
|
+
li.append(toggle)
|
|
115
|
+
|
|
116
|
+
if (key !== null) {
|
|
117
|
+
const keySpan = document.createElement("span")
|
|
118
|
+
keySpan.className = "jt-key"
|
|
119
|
+
keySpan.textContent = '"' + key + '"'
|
|
120
|
+
li.append(keySpan)
|
|
121
|
+
li.append(document.createTextNode(": "))
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const bracket = document.createElement("span")
|
|
125
|
+
bracket.className = "jt-bracket"
|
|
126
|
+
bracket.textContent = openBracket
|
|
127
|
+
li.append(bracket)
|
|
128
|
+
|
|
129
|
+
// Collapsed info
|
|
130
|
+
const collapsedInfo = document.createElement("span")
|
|
131
|
+
collapsedInfo.className = "jt-collapsed-info"
|
|
132
|
+
collapsedInfo.textContent = " " + len + (len === 1 ? " item" : " items") + " "
|
|
133
|
+
collapsedInfo.style.display = currentDepth < maxDepth ? "none" : "inline"
|
|
134
|
+
li.append(collapsedInfo)
|
|
135
|
+
|
|
136
|
+
// Children
|
|
137
|
+
const childUl = document.createElement("ul")
|
|
138
|
+
childUl.style.display = currentDepth < maxDepth ? "block" : "none"
|
|
139
|
+
|
|
140
|
+
if (isArray) {
|
|
141
|
+
for (let i = 0; i < value.length; i++) {
|
|
142
|
+
buildNode(childUl, null, value[i], maxDepth, currentDepth + 1, i === value.length - 1)
|
|
143
|
+
}
|
|
144
|
+
} else {
|
|
145
|
+
const keys = Object.keys(value)
|
|
146
|
+
for (let i = 0; i < keys.length; i++) {
|
|
147
|
+
buildNode(childUl, keys[i], value[keys[i]], maxDepth, currentDepth + 1, i === keys.length - 1)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
li.append(childUl)
|
|
151
|
+
|
|
152
|
+
// Close bracket
|
|
153
|
+
const closeBr = document.createElement("span")
|
|
154
|
+
closeBr.className = "jt-bracket"
|
|
155
|
+
closeBr.textContent = closeBracket
|
|
156
|
+
li.append(closeBr)
|
|
157
|
+
|
|
158
|
+
if (!isLast) {
|
|
159
|
+
const comma = document.createElement("span")
|
|
160
|
+
comma.className = "jt-comma"
|
|
161
|
+
comma.textContent = ","
|
|
162
|
+
li.append(comma)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Toggle handler
|
|
166
|
+
toggle.addEventListener("click", function () {
|
|
167
|
+
const isOpen = childUl.style.display !== "none"
|
|
168
|
+
childUl.style.display = isOpen ? "none" : "block"
|
|
169
|
+
collapsedInfo.style.display = isOpen ? "inline" : "none"
|
|
170
|
+
toggle.textContent = isOpen ? "\u25B8" : "\u25BE"
|
|
171
|
+
})
|
|
172
|
+
} else {
|
|
173
|
+
// Leaf: spacer for alignment
|
|
174
|
+
const spacer = document.createElement("span")
|
|
175
|
+
spacer.style.display = "inline-block"
|
|
176
|
+
spacer.style.width = "14px"
|
|
177
|
+
spacer.style.marginRight = "2px"
|
|
178
|
+
li.append(spacer)
|
|
179
|
+
|
|
180
|
+
if (key !== null) {
|
|
181
|
+
const keySpan = document.createElement("span")
|
|
182
|
+
keySpan.className = "jt-key"
|
|
183
|
+
keySpan.textContent = '"' + key + '"'
|
|
184
|
+
li.append(keySpan)
|
|
185
|
+
li.append(document.createTextNode(": "))
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const valSpan = document.createElement("span")
|
|
189
|
+
if (value === null) {
|
|
190
|
+
valSpan.className = "jt-null"
|
|
191
|
+
valSpan.textContent = "null"
|
|
192
|
+
} else if (typeof value === "string") {
|
|
193
|
+
valSpan.className = "jt-string"
|
|
194
|
+
if (value.length > 300) {
|
|
195
|
+
// Long string: show truncated with expand toggle
|
|
196
|
+
const previewText = value.slice(0, 300)
|
|
197
|
+
valSpan.textContent = '"' + previewText + '..."'
|
|
198
|
+
valSpan.classList.add("jt-string-truncated")
|
|
199
|
+
|
|
200
|
+
const expandBtn = document.createElement("span")
|
|
201
|
+
expandBtn.className = "jt-expand-string"
|
|
202
|
+
expandBtn.textContent = "(" + value.length.toLocaleString() + " chars - click to expand)"
|
|
203
|
+
let expanded = false
|
|
204
|
+
expandBtn.addEventListener("click", function (e) {
|
|
205
|
+
e.stopPropagation()
|
|
206
|
+
expanded = !expanded
|
|
207
|
+
if (expanded) {
|
|
208
|
+
valSpan.textContent = '"' + value + '"'
|
|
209
|
+
expandBtn.textContent = "(click to collapse)"
|
|
210
|
+
} else {
|
|
211
|
+
valSpan.textContent = '"' + previewText + '..."'
|
|
212
|
+
expandBtn.textContent = "(" + value.length.toLocaleString() + " chars - click to expand)"
|
|
213
|
+
}
|
|
214
|
+
})
|
|
215
|
+
li.append(valSpan)
|
|
216
|
+
li.append(expandBtn)
|
|
217
|
+
if (!isLast) {
|
|
218
|
+
const comma = document.createElement("span")
|
|
219
|
+
comma.className = "jt-comma"
|
|
220
|
+
comma.textContent = ","
|
|
221
|
+
li.append(comma)
|
|
222
|
+
}
|
|
223
|
+
parent.append(li)
|
|
224
|
+
return // Early return since we handled appending
|
|
225
|
+
}
|
|
226
|
+
valSpan.textContent = '"' + value + '"'
|
|
227
|
+
} else if (typeof value === "number") {
|
|
228
|
+
valSpan.className = "jt-number"
|
|
229
|
+
valSpan.textContent = String(value)
|
|
230
|
+
} else if (typeof value === "boolean") {
|
|
231
|
+
valSpan.className = "jt-boolean"
|
|
232
|
+
valSpan.textContent = String(value)
|
|
233
|
+
} else {
|
|
234
|
+
valSpan.textContent = String(value)
|
|
235
|
+
}
|
|
236
|
+
li.append(valSpan)
|
|
237
|
+
|
|
238
|
+
if (!isLast) {
|
|
239
|
+
const comma = document.createElement("span")
|
|
240
|
+
comma.className = "jt-comma"
|
|
241
|
+
comma.textContent = ","
|
|
242
|
+
li.append(comma)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
parent.append(li)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function highlightSearch(text, query) {
|
|
250
|
+
if (!query || !text) return escapeHtml(text)
|
|
251
|
+
const escaped = escapeHtml(text)
|
|
252
|
+
const regex = new RegExp("(" + query.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`) + ")", "gi")
|
|
253
|
+
return escaped.replace(regex, '<span class="search-highlight">$1</span>')
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Get preview text from messages
|
|
257
|
+
function getPreviewText(request) {
|
|
258
|
+
if (!request || !request.request) return ""
|
|
259
|
+
const messages = request.request.messages || []
|
|
260
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
261
|
+
const msg = messages[i]
|
|
262
|
+
if (msg.role === "user") {
|
|
263
|
+
const content = msg.content
|
|
264
|
+
if (typeof content === "string") {
|
|
265
|
+
return content.replaceAll(/<[^>]+>/g, "").slice(0, 100)
|
|
266
|
+
}
|
|
267
|
+
if (Array.isArray(content)) {
|
|
268
|
+
for (const block of content) {
|
|
269
|
+
if (block.type === "text" && block.text) {
|
|
270
|
+
return block.text.replaceAll(/<[^>]+>/g, "").slice(0, 100)
|
|
271
|
+
}
|
|
272
|
+
if (block.type === "tool_result") {
|
|
273
|
+
return "[tool_result: " + (block.tool_use_id || "").slice(0, 8) + "]"
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return ""
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function getMessageSummary(content) {
|
|
283
|
+
if (typeof content === "string") {
|
|
284
|
+
return content.length > 80 ? content.slice(0, 80) + "..." : content
|
|
285
|
+
}
|
|
286
|
+
if (Array.isArray(content)) {
|
|
287
|
+
if (content.length === 1 && content[0].type === "text") {
|
|
288
|
+
const text = content[0].text || ""
|
|
289
|
+
return text.length > 80 ? text.slice(0, 80) + "..." : text
|
|
290
|
+
}
|
|
291
|
+
const counts = {}
|
|
292
|
+
for (const b of content) {
|
|
293
|
+
counts[b.type] = (counts[b.type] || 0) + 1
|
|
294
|
+
}
|
|
295
|
+
return Object.entries(counts)
|
|
296
|
+
.map(function (e) {
|
|
297
|
+
return e[1] + " " + e[0]
|
|
298
|
+
})
|
|
299
|
+
.join(", ")
|
|
300
|
+
}
|
|
301
|
+
return ""
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function getContentBlockSummary(block) {
|
|
305
|
+
if (block.type === "text") {
|
|
306
|
+
const text = block.text || ""
|
|
307
|
+
return text.length > 60 ? text.slice(0, 60) + "..." : text
|
|
308
|
+
}
|
|
309
|
+
if (block.type === "thinking") {
|
|
310
|
+
const text = block.thinking || ""
|
|
311
|
+
return text.length > 60 ? text.slice(0, 60) + "..." : text
|
|
312
|
+
}
|
|
313
|
+
if (block.type === "tool_use") return block.name || ""
|
|
314
|
+
if (block.type === "tool_result") return "for " + (block.tool_use_id || "")
|
|
315
|
+
return block.type
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Load sessions for dropdown
|
|
319
|
+
async function loadSessions() {
|
|
320
|
+
try {
|
|
321
|
+
const response = await fetch("/history/api/sessions")
|
|
322
|
+
const data = await response.json()
|
|
323
|
+
const select = document.querySelector("#session-select")
|
|
324
|
+
select.innerHTML = '<option value="">All Sessions</option>'
|
|
325
|
+
for (const session of data.sessions || []) {
|
|
326
|
+
const opt = document.createElement("option")
|
|
327
|
+
opt.value = session.id
|
|
328
|
+
const time = formatDate(session.startTime)
|
|
329
|
+
opt.textContent = time + " (" + session.requestCount + " reqs)"
|
|
330
|
+
select.append(opt)
|
|
331
|
+
}
|
|
332
|
+
} catch (e) {
|
|
333
|
+
console.error("Failed to load sessions:", e)
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Load stats
|
|
338
|
+
async function loadStats() {
|
|
339
|
+
try {
|
|
340
|
+
const response = await fetch("/history/api/stats")
|
|
341
|
+
const stats = await response.json()
|
|
342
|
+
document.querySelector("#stat-total").textContent = formatNumber(stats.totalRequests)
|
|
343
|
+
document.querySelector("#stat-success").textContent = formatNumber(stats.successfulRequests)
|
|
344
|
+
document.querySelector("#stat-failed").textContent = formatNumber(stats.failedRequests)
|
|
345
|
+
document.querySelector("#stat-input").textContent = formatNumber(stats.totalInputTokens)
|
|
346
|
+
document.querySelector("#stat-output").textContent = formatNumber(stats.totalOutputTokens)
|
|
347
|
+
} catch (e) {
|
|
348
|
+
console.error("Failed to load stats:", e)
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Load entries
|
|
353
|
+
async function loadEntries() {
|
|
354
|
+
const listEl = document.querySelector("#request-list")
|
|
355
|
+
const search = document.querySelector("#list-search").value
|
|
356
|
+
const endpoint = document.querySelector("#filter-endpoint").value
|
|
357
|
+
const success = document.querySelector("#filter-status").value
|
|
358
|
+
|
|
359
|
+
const params = new URLSearchParams()
|
|
360
|
+
params.set("page", currentPage)
|
|
361
|
+
params.set("limit", 20)
|
|
362
|
+
if (currentSessionId) params.set("sessionId", currentSessionId)
|
|
363
|
+
if (search) params.set("search", search)
|
|
364
|
+
if (endpoint) params.set("endpoint", endpoint)
|
|
365
|
+
if (success) params.set("success", success)
|
|
366
|
+
|
|
367
|
+
try {
|
|
368
|
+
const response = await fetch("/history/api/entries?" + params.toString())
|
|
369
|
+
const data = await response.json()
|
|
370
|
+
totalPages = data.totalPages || 1
|
|
371
|
+
|
|
372
|
+
if (!data.entries || data.entries.length === 0) {
|
|
373
|
+
listEl.innerHTML = '<div class="empty-state"><h3>No requests found</h3><p>Try adjusting your filters</p></div>'
|
|
374
|
+
updateSearchCount(0, search)
|
|
375
|
+
renderPagination()
|
|
376
|
+
return
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
updateSearchCount(data.total, search)
|
|
380
|
+
|
|
381
|
+
let html = ""
|
|
382
|
+
for (const entry of data.entries) {
|
|
383
|
+
const isSelected = entry.id === currentEntryId
|
|
384
|
+
const statusClass =
|
|
385
|
+
!entry.response ? "pending"
|
|
386
|
+
: entry.response.success !== false ? "success"
|
|
387
|
+
: "error"
|
|
388
|
+
const model = entry.response?.model || entry.request?.model || "unknown"
|
|
389
|
+
const endpoint = entry.endpoint || "unknown"
|
|
390
|
+
const inputTokens = entry.response?.usage?.input_tokens
|
|
391
|
+
const outputTokens = entry.response?.usage?.output_tokens
|
|
392
|
+
const preview = getPreviewText(entry)
|
|
393
|
+
const isStream = entry.request?.stream
|
|
394
|
+
|
|
395
|
+
html +=
|
|
396
|
+
'<div class="request-item'
|
|
397
|
+
+ (isSelected ? " selected" : "")
|
|
398
|
+
+ '" data-id="'
|
|
399
|
+
+ entry.id
|
|
400
|
+
+ '" onclick="selectEntry(\''
|
|
401
|
+
+ entry.id
|
|
402
|
+
+ "')\">"
|
|
403
|
+
html += '<div class="request-item-header">'
|
|
404
|
+
html += '<span class="request-status ' + statusClass + '"></span>'
|
|
405
|
+
html += '<span class="request-time">' + formatDate(entry.timestamp) + "</span>"
|
|
406
|
+
html += "</div>"
|
|
407
|
+
html += '<div class="request-item-body">'
|
|
408
|
+
html += '<span class="request-model">' + escapeHtml(model) + "</span>"
|
|
409
|
+
html += '<span class="badge ' + endpoint + '">' + endpoint + "</span>"
|
|
410
|
+
if (isStream) html += '<span class="badge stream">stream</span>'
|
|
411
|
+
html += "</div>"
|
|
412
|
+
html += '<div class="request-item-meta">'
|
|
413
|
+
html += "<span>\u2193" + formatNumber(inputTokens) + "</span>"
|
|
414
|
+
html += "<span>\u2191" + formatNumber(outputTokens) + "</span>"
|
|
415
|
+
html += "<span>" + formatDuration(entry.durationMs) + "</span>"
|
|
416
|
+
html += "</div>"
|
|
417
|
+
if (preview) {
|
|
418
|
+
html += '<div class="request-preview">' + escapeHtml(preview) + "</div>"
|
|
419
|
+
}
|
|
420
|
+
html += "</div>"
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
listEl.innerHTML = html
|
|
424
|
+
renderPagination()
|
|
425
|
+
|
|
426
|
+
// Auto-select the first (newest) entry if nothing is selected
|
|
427
|
+
if (!currentEntryId && data.entries.length > 0) {
|
|
428
|
+
selectEntry(data.entries[0].id)
|
|
429
|
+
}
|
|
430
|
+
} catch (e) {
|
|
431
|
+
console.error("Failed to load entries:", e)
|
|
432
|
+
listEl.innerHTML =
|
|
433
|
+
'<div class="empty-state"><h3>Error loading requests</h3><p>' + escapeHtml(e.message) + "</p></div>"
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function updateSearchCount(total, search) {
|
|
438
|
+
let countEl = document.querySelector("#search-count")
|
|
439
|
+
if (!countEl) {
|
|
440
|
+
const searchInput = document.querySelector("#list-search")
|
|
441
|
+
countEl = document.createElement("span")
|
|
442
|
+
countEl.id = "search-count"
|
|
443
|
+
countEl.style.cssText = "font-size:11px;color:var(--text-secondary);margin-left:8px;"
|
|
444
|
+
searchInput.parentElement.append(countEl)
|
|
445
|
+
}
|
|
446
|
+
countEl.textContent = search ? `${total} hit${total !== 1 ? "s" : ""}` : ""
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function renderPagination() {
|
|
450
|
+
const el = document.querySelector("#list-pagination")
|
|
451
|
+
if (totalPages <= 1) {
|
|
452
|
+
el.innerHTML = ""
|
|
453
|
+
return
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
let html = ""
|
|
457
|
+
html +=
|
|
458
|
+
"<button " + (currentPage <= 1 ? "disabled" : "") + ' onclick="goToPage(' + (currentPage - 1) + ')">\u25C0</button>'
|
|
459
|
+
|
|
460
|
+
const maxVisible = 5
|
|
461
|
+
let startPage = Math.max(1, currentPage - Math.floor(maxVisible / 2))
|
|
462
|
+
let endPage = Math.min(totalPages, startPage + maxVisible - 1)
|
|
463
|
+
if (endPage - startPage < maxVisible - 1) {
|
|
464
|
+
startPage = Math.max(1, endPage - maxVisible + 1)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (startPage > 1) {
|
|
468
|
+
html += '<button onclick="goToPage(1)">1</button>'
|
|
469
|
+
if (startPage > 2) html += "<span>...</span>"
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
for (let i = startPage; i <= endPage; i++) {
|
|
473
|
+
html +=
|
|
474
|
+
'<button class="' + (i === currentPage ? "active" : "") + '" onclick="goToPage(' + i + ')">' + i + "</button>"
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if (endPage < totalPages) {
|
|
478
|
+
if (endPage < totalPages - 1) html += "<span>...</span>"
|
|
479
|
+
html += '<button onclick="goToPage(' + totalPages + ')">' + totalPages + "</button>"
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
html +=
|
|
483
|
+
"<button "
|
|
484
|
+
+ (currentPage >= totalPages ? "disabled" : "")
|
|
485
|
+
+ ' onclick="goToPage('
|
|
486
|
+
+ (currentPage + 1)
|
|
487
|
+
+ ')">\u25B6</button>'
|
|
488
|
+
el.innerHTML = html
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function goToPage(page) {
|
|
492
|
+
currentPage = page
|
|
493
|
+
loadEntries()
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Select entry
|
|
497
|
+
async function selectEntry(id) {
|
|
498
|
+
currentEntryId = id
|
|
499
|
+
|
|
500
|
+
// Update selection UI
|
|
501
|
+
for (const el of document.querySelectorAll(".request-item")) {
|
|
502
|
+
el.classList.toggle("selected", el.dataset.id === id)
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
try {
|
|
506
|
+
const response = await fetch("/history/api/entries/" + id)
|
|
507
|
+
currentEntry = await response.json()
|
|
508
|
+
sectionCollapseState = { meta: false, request: false, response: false }
|
|
509
|
+
showDetailView()
|
|
510
|
+
renderDetail()
|
|
511
|
+
// Scroll detail panel to bottom so the latest messages/response are visible
|
|
512
|
+
const detailContent = document.querySelector("#detail-content")
|
|
513
|
+
if (detailContent) detailContent.scrollTop = detailContent.scrollHeight
|
|
514
|
+
} catch (e) {
|
|
515
|
+
console.error("Failed to load entry:", e)
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function showDetailView() {
|
|
520
|
+
document.querySelector("#detail-empty").style.display = "none"
|
|
521
|
+
const detailView = document.querySelector("#detail-view")
|
|
522
|
+
detailView.style.display = "flex"
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function hideDetailView() {
|
|
526
|
+
document.querySelector("#detail-empty").style.display = "flex"
|
|
527
|
+
document.querySelector("#detail-view").style.display = "none"
|
|
528
|
+
currentEntry = null
|
|
529
|
+
currentEntryId = null
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Toggle a section collapse
|
|
533
|
+
function toggleSection(section) {
|
|
534
|
+
sectionCollapseState[section] = !sectionCollapseState[section]
|
|
535
|
+
renderDetail()
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Toggle message collapse
|
|
539
|
+
function toggleMessage(msgId) {
|
|
540
|
+
const block = document.getElementById(msgId)
|
|
541
|
+
if (!block) return
|
|
542
|
+
const bodies = block.querySelectorAll(".message-body")
|
|
543
|
+
const summary = block.querySelector(".collapsed-summary")
|
|
544
|
+
const icon = block.querySelector(".collapse-icon")
|
|
545
|
+
// Check if currently collapsed (first body hidden)
|
|
546
|
+
const isCollapsed = bodies.length > 0 && bodies[0].dataset.collapsed === "true"
|
|
547
|
+
if (isCollapsed) {
|
|
548
|
+
for (const b of bodies) {
|
|
549
|
+
// Restore previous display state (only the active view should be visible)
|
|
550
|
+
b.style.display = b.dataset.prevDisplay || ""
|
|
551
|
+
delete b.dataset.collapsed
|
|
552
|
+
delete b.dataset.prevDisplay
|
|
553
|
+
}
|
|
554
|
+
if (summary) summary.style.display = "none"
|
|
555
|
+
if (icon) icon.textContent = "\u25BE"
|
|
556
|
+
} else {
|
|
557
|
+
for (const b of bodies) {
|
|
558
|
+
b.dataset.prevDisplay = b.style.display
|
|
559
|
+
b.dataset.collapsed = "true"
|
|
560
|
+
b.style.display = "none"
|
|
561
|
+
}
|
|
562
|
+
if (summary) summary.style.display = ""
|
|
563
|
+
if (icon) icon.textContent = "\u25B8"
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Toggle content block collapse
|
|
568
|
+
function toggleContentBlock(blockId) {
|
|
569
|
+
const block = document.getElementById(blockId)
|
|
570
|
+
if (!block) return
|
|
571
|
+
const body = block.querySelector(".content-block-body")
|
|
572
|
+
const summary = block.querySelector(".collapsed-summary")
|
|
573
|
+
const icon = block.querySelector(".collapse-icon")
|
|
574
|
+
if (body.style.display === "none") {
|
|
575
|
+
body.style.display = ""
|
|
576
|
+
if (summary) summary.style.display = "none"
|
|
577
|
+
if (icon) icon.textContent = "\u25BE"
|
|
578
|
+
} else {
|
|
579
|
+
body.style.display = "none"
|
|
580
|
+
if (summary) summary.style.display = ""
|
|
581
|
+
if (icon) icon.textContent = "\u25B8"
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Render detail view
|
|
586
|
+
function renderDetail() {
|
|
587
|
+
if (!currentEntry) return
|
|
588
|
+
|
|
589
|
+
// Clear raw data registry and reset ID counters for fresh render
|
|
590
|
+
rawDataRegistry = []
|
|
591
|
+
msgIdCounter = 0
|
|
592
|
+
blockIdCounter = 0
|
|
593
|
+
|
|
594
|
+
const content = document.querySelector("#detail-content")
|
|
595
|
+
const filterRole = document.querySelector("#filter-role").value
|
|
596
|
+
const filterType = document.querySelector("#filter-type").value
|
|
597
|
+
const aggregateTools = document.querySelector("#toggle-aggregate").checked
|
|
598
|
+
const rewrites = currentEntry.rewrites
|
|
599
|
+
const truncation = rewrites?.truncation
|
|
600
|
+
|
|
601
|
+
// Build tool result map for aggregation
|
|
602
|
+
const toolResultMap = {}
|
|
603
|
+
// Build tool_use_id → tool name map for tool_result headers
|
|
604
|
+
const toolUseNameMap = {}
|
|
605
|
+
if (aggregateTools) {
|
|
606
|
+
const messages = currentEntry.request?.messages || []
|
|
607
|
+
for (const msg of messages) {
|
|
608
|
+
if (Array.isArray(msg.content)) {
|
|
609
|
+
for (const block of msg.content) {
|
|
610
|
+
if (block.type === "tool_result" && block.tool_use_id) {
|
|
611
|
+
toolResultMap[block.tool_use_id] = block
|
|
612
|
+
}
|
|
613
|
+
if (block.type === "tool_use" && block.id) {
|
|
614
|
+
toolUseNameMap[block.id] = block.name || ""
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
} else {
|
|
620
|
+
const messages = currentEntry.request?.messages || []
|
|
621
|
+
for (const msg of messages) {
|
|
622
|
+
if (Array.isArray(msg.content)) {
|
|
623
|
+
for (const block of msg.content) {
|
|
624
|
+
if (block.type === "tool_use" && block.id) {
|
|
625
|
+
toolUseNameMap[block.id] = block.name || ""
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
let html = '<div class="conversation">'
|
|
633
|
+
|
|
634
|
+
// REQUEST section
|
|
635
|
+
const fullEntryRawIdx = registerRawData(currentEntry)
|
|
636
|
+
const reqIcon = sectionCollapseState.request ? "\u25B8" : "\u25BE"
|
|
637
|
+
const msgCount = (currentEntry.request?.messages || []).length
|
|
638
|
+
const reqRawIdx = registerRawData(currentEntry.request)
|
|
639
|
+
html += '<div class="section-block">'
|
|
640
|
+
html += '<div class="section-header">'
|
|
641
|
+
html += '<div class="section-header-left" onclick="toggleSection(\'request\')">'
|
|
642
|
+
html += '<span class="collapse-icon">' + reqIcon + "</span>"
|
|
643
|
+
html += "REQUEST"
|
|
644
|
+
html += '<span class="section-badge">' + msgCount + " messages</span>"
|
|
645
|
+
html += "</div>"
|
|
646
|
+
html += '<div class="section-header-actions">'
|
|
647
|
+
html +=
|
|
648
|
+
'<button class="action-btn" onclick="event.stopPropagation();showRaw(\'Request\', '
|
|
649
|
+
+ reqRawIdx
|
|
650
|
+
+ ')" title="View Raw">'
|
|
651
|
+
+ ICON_CODE
|
|
652
|
+
+ "Raw</button>"
|
|
653
|
+
html += "</div>"
|
|
654
|
+
html += "</div>"
|
|
655
|
+
|
|
656
|
+
if (!sectionCollapseState.request) {
|
|
657
|
+
html += '<div class="section-body">'
|
|
658
|
+
|
|
659
|
+
// System message
|
|
660
|
+
const system = currentEntry.request?.system
|
|
661
|
+
const rewrittenSystem = rewrites?.rewrittenSystem
|
|
662
|
+
if (system && (!filterRole || filterRole === "system")) {
|
|
663
|
+
html += renderSystemMessage(system, rewrittenSystem)
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Messages
|
|
667
|
+
const messages = currentEntry.request?.messages || []
|
|
668
|
+
const rewrittenMessages = rewrites?.rewrittenMessages
|
|
669
|
+
const messageMapping = rewrites?.messageMapping
|
|
670
|
+
const removedCount = truncation ? truncation.removedMessageCount : 0
|
|
671
|
+
|
|
672
|
+
// Build reverse lookup: origIdx → rwIdx (first occurrence)
|
|
673
|
+
var origToRwIdx = {}
|
|
674
|
+
if (messageMapping && Array.isArray(messageMapping)) {
|
|
675
|
+
for (var [mi, oi] of messageMapping.entries()) {
|
|
676
|
+
if (oi >= 0 && !(oi in origToRwIdx)) {
|
|
677
|
+
origToRwIdx[oi] = mi
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
for (const [i, msg] of messages.entries()) {
|
|
683
|
+
if (filterRole && msg.role !== filterRole) continue
|
|
684
|
+
const isTruncated = i < removedCount
|
|
685
|
+
|
|
686
|
+
// Find corresponding rewritten message via mapping
|
|
687
|
+
let rewrittenMsg = null
|
|
688
|
+
if (!isTruncated && rewrittenMessages) {
|
|
689
|
+
if (messageMapping && i in origToRwIdx) {
|
|
690
|
+
rewrittenMsg = rewrittenMessages[origToRwIdx[i]]
|
|
691
|
+
} else if (!messageMapping) {
|
|
692
|
+
// Fallback for old data without mapping: use offset
|
|
693
|
+
const rwIdx = i - removedCount
|
|
694
|
+
if (rwIdx >= 0 && rwIdx < rewrittenMessages.length) {
|
|
695
|
+
rewrittenMsg = rewrittenMessages[rwIdx]
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Determine if content was actually rewritten by comparing text
|
|
701
|
+
const isRewritten = rewrittenMsg != null && !messagesContentEqual(msg, rewrittenMsg)
|
|
702
|
+
|
|
703
|
+
html += renderMessage(
|
|
704
|
+
msg,
|
|
705
|
+
filterType,
|
|
706
|
+
aggregateTools,
|
|
707
|
+
toolResultMap,
|
|
708
|
+
toolUseNameMap,
|
|
709
|
+
isTruncated,
|
|
710
|
+
isRewritten,
|
|
711
|
+
rewrittenMsg,
|
|
712
|
+
)
|
|
713
|
+
// Insert truncation divider after the last removed message
|
|
714
|
+
if (isTruncated && i === removedCount - 1) {
|
|
715
|
+
const pct = Math.round((1 - truncation.compactedTokens / truncation.originalTokens) * 100)
|
|
716
|
+
html += '<div class="truncation-divider">'
|
|
717
|
+
html += '<span class="truncation-divider-line"></span>'
|
|
718
|
+
html +=
|
|
719
|
+
'<span class="truncation-divider-label">'
|
|
720
|
+
+ removedCount
|
|
721
|
+
+ " messages truncated ("
|
|
722
|
+
+ formatNumber(truncation.originalTokens)
|
|
723
|
+
+ " \u2192 "
|
|
724
|
+
+ formatNumber(truncation.compactedTokens)
|
|
725
|
+
+ " tokens, -"
|
|
726
|
+
+ pct
|
|
727
|
+
+ "%)</span>"
|
|
728
|
+
html += '<span class="truncation-divider-line"></span>'
|
|
729
|
+
html += "</div>"
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
html += "</div>"
|
|
734
|
+
}
|
|
735
|
+
html += "</div>"
|
|
736
|
+
|
|
737
|
+
// RESPONSE section
|
|
738
|
+
const resIcon = sectionCollapseState.response ? "\u25B8" : "\u25BE"
|
|
739
|
+
const resRawIdx = registerRawData(currentEntry.response)
|
|
740
|
+
html += '<div class="section-block">'
|
|
741
|
+
html += '<div class="section-header">'
|
|
742
|
+
html += '<div class="section-header-left" onclick="toggleSection(\'response\')">'
|
|
743
|
+
html += '<span class="collapse-icon">' + resIcon + "</span>"
|
|
744
|
+
html += "RESPONSE"
|
|
745
|
+
html += "</div>"
|
|
746
|
+
html += '<div class="section-header-actions">'
|
|
747
|
+
html +=
|
|
748
|
+
'<button class="action-btn" onclick="event.stopPropagation();showRaw(\'Response\', '
|
|
749
|
+
+ resRawIdx
|
|
750
|
+
+ ')" title="View Raw">'
|
|
751
|
+
+ ICON_CODE
|
|
752
|
+
+ "Raw</button>"
|
|
753
|
+
html += "</div>"
|
|
754
|
+
html += "</div>"
|
|
755
|
+
|
|
756
|
+
if (!sectionCollapseState.response) {
|
|
757
|
+
html += '<div class="section-body">'
|
|
758
|
+
|
|
759
|
+
// Response content (assistant message)
|
|
760
|
+
const responseContent = currentEntry.response?.content
|
|
761
|
+
if (responseContent && (!filterRole || filterRole === "assistant")) {
|
|
762
|
+
// Use full message object if it has role (preserves tool_calls for OpenAI format)
|
|
763
|
+
const responseMsg = responseContent.role
|
|
764
|
+
? responseContent
|
|
765
|
+
: { role: "assistant", content: responseContent.content ?? responseContent }
|
|
766
|
+
html += renderMessage(responseMsg, filterType, false, null, toolUseNameMap)
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// Error message
|
|
770
|
+
if (currentEntry.response?.error) {
|
|
771
|
+
html +=
|
|
772
|
+
'<div class="error-block"><strong>Error:</strong> ' + escapeHtml(String(currentEntry.response.error)) + "</div>"
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
html += "</div>"
|
|
776
|
+
}
|
|
777
|
+
html += "</div>"
|
|
778
|
+
|
|
779
|
+
// META INFO section (after response for better reading flow)
|
|
780
|
+
const metaIcon = sectionCollapseState.meta ? "\u25B8" : "\u25BE"
|
|
781
|
+
html += '<div class="section-block">'
|
|
782
|
+
html += '<div class="section-header">'
|
|
783
|
+
html += '<div class="section-header-left" onclick="toggleSection(\'meta\')">'
|
|
784
|
+
html += '<span class="collapse-icon">' + metaIcon + "</span>"
|
|
785
|
+
html += "META INFO"
|
|
786
|
+
html += "</div>"
|
|
787
|
+
html += '<div class="section-header-actions">'
|
|
788
|
+
html +=
|
|
789
|
+
'<button class="action-btn" onclick="event.stopPropagation();showRaw(\'Full Entry\', '
|
|
790
|
+
+ fullEntryRawIdx
|
|
791
|
+
+ ')" title="View Raw">'
|
|
792
|
+
+ ICON_CODE
|
|
793
|
+
+ "Raw</button>"
|
|
794
|
+
html += "</div>"
|
|
795
|
+
html += "</div>"
|
|
796
|
+
|
|
797
|
+
if (!sectionCollapseState.meta) {
|
|
798
|
+
// Collect all meta items into a flat array
|
|
799
|
+
const items = []
|
|
800
|
+
items.push(
|
|
801
|
+
'<div class="info-item"><div class="info-label">Time</div><div class="info-value">'
|
|
802
|
+
+ formatDate(currentEntry.timestamp)
|
|
803
|
+
+ "</div></div>",
|
|
804
|
+
)
|
|
805
|
+
items.push(
|
|
806
|
+
'<div class="info-item"><div class="info-label">Model</div><div class="info-value">'
|
|
807
|
+
+ escapeHtml(currentEntry.request?.model || "-")
|
|
808
|
+
+ "</div></div>",
|
|
809
|
+
'<div class="info-item"><div class="info-label">Endpoint</div><div class="info-value"><span class="badge '
|
|
810
|
+
+ currentEntry.endpoint
|
|
811
|
+
+ '">'
|
|
812
|
+
+ currentEntry.endpoint
|
|
813
|
+
+ "</span></div></div>",
|
|
814
|
+
)
|
|
815
|
+
if (currentEntry.request?.stream) {
|
|
816
|
+
items.push(
|
|
817
|
+
'<div class="info-item"><div class="info-label">Stream</div><div class="info-value"><span class="badge stream">yes</span></div></div>',
|
|
818
|
+
)
|
|
819
|
+
}
|
|
820
|
+
if (currentEntry.request?.max_tokens) {
|
|
821
|
+
items.push(
|
|
822
|
+
'<div class="info-item"><div class="info-label">Max Tokens</div><div class="info-value">'
|
|
823
|
+
+ currentEntry.request.max_tokens
|
|
824
|
+
+ "</div></div>",
|
|
825
|
+
)
|
|
826
|
+
}
|
|
827
|
+
if (currentEntry.request?.temperature != null) {
|
|
828
|
+
items.push(
|
|
829
|
+
'<div class="info-item"><div class="info-label">Temperature</div><div class="info-value">'
|
|
830
|
+
+ currentEntry.request.temperature
|
|
831
|
+
+ "</div></div>",
|
|
832
|
+
)
|
|
833
|
+
}
|
|
834
|
+
if (currentEntry.request?.tools?.length) {
|
|
835
|
+
items.push(
|
|
836
|
+
'<div class="info-item"><div class="info-label">Tools</div><div class="info-value">'
|
|
837
|
+
+ currentEntry.request.tools.length
|
|
838
|
+
+ " defined</div></div>",
|
|
839
|
+
)
|
|
840
|
+
}
|
|
841
|
+
if (currentEntry.response?.stop_reason) {
|
|
842
|
+
items.push(
|
|
843
|
+
'<div class="info-item"><div class="info-label">Stop Reason</div><div class="info-value">'
|
|
844
|
+
+ escapeHtml(currentEntry.response.stop_reason)
|
|
845
|
+
+ "</div></div>",
|
|
846
|
+
)
|
|
847
|
+
}
|
|
848
|
+
if (currentEntry.response?.success !== undefined) {
|
|
849
|
+
items.push(
|
|
850
|
+
'<div class="info-item"><div class="info-label">Status</div><div class="info-value '
|
|
851
|
+
+ (currentEntry.response.success !== false ? "status-ok" : "status-fail")
|
|
852
|
+
+ '">'
|
|
853
|
+
+ (currentEntry.response.success !== false ? "OK" : "Failed")
|
|
854
|
+
+ "</div></div>",
|
|
855
|
+
)
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// Usage items
|
|
859
|
+
const usage = currentEntry.response?.usage
|
|
860
|
+
if (usage) {
|
|
861
|
+
items.push(
|
|
862
|
+
'<div class="info-item"><div class="info-label">Input Tokens</div><div class="info-value number">'
|
|
863
|
+
+ formatNumber(usage.input_tokens)
|
|
864
|
+
+ "</div></div>",
|
|
865
|
+
)
|
|
866
|
+
items.push(
|
|
867
|
+
'<div class="info-item"><div class="info-label">Output Tokens</div><div class="info-value number">'
|
|
868
|
+
+ formatNumber(usage.output_tokens)
|
|
869
|
+
+ "</div></div>",
|
|
870
|
+
)
|
|
871
|
+
if (usage.cache_read_input_tokens) {
|
|
872
|
+
items.push(
|
|
873
|
+
'<div class="info-item"><div class="info-label">Cache Read</div><div class="info-value number">'
|
|
874
|
+
+ formatNumber(usage.cache_read_input_tokens)
|
|
875
|
+
+ "</div></div>",
|
|
876
|
+
)
|
|
877
|
+
}
|
|
878
|
+
if (usage.cache_creation_input_tokens) {
|
|
879
|
+
items.push(
|
|
880
|
+
'<div class="info-item"><div class="info-label">Cache Create</div><div class="info-value number">'
|
|
881
|
+
+ formatNumber(usage.cache_creation_input_tokens)
|
|
882
|
+
+ "</div></div>",
|
|
883
|
+
)
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// Duration
|
|
888
|
+
if (currentEntry.durationMs) {
|
|
889
|
+
items.push(
|
|
890
|
+
'<div class="info-item"><div class="info-label">Duration</div><div class="info-value">'
|
|
891
|
+
+ formatDuration(currentEntry.durationMs)
|
|
892
|
+
+ "</div></div>",
|
|
893
|
+
)
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// Truncation info
|
|
897
|
+
if (truncation) {
|
|
898
|
+
const pct = Math.round((1 - truncation.compactedTokens / truncation.originalTokens) * 100)
|
|
899
|
+
items.push(
|
|
900
|
+
'<div class="info-item"><div class="info-label">Truncated</div><div class="info-value truncation-value">'
|
|
901
|
+
+ truncation.removedMessageCount
|
|
902
|
+
+ " msgs removed ("
|
|
903
|
+
+ pct
|
|
904
|
+
+ "%)</div></div>",
|
|
905
|
+
)
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// Sanitization info
|
|
909
|
+
if (rewrites?.sanitization) {
|
|
910
|
+
const s = rewrites.sanitization
|
|
911
|
+
if (s.totalBlocksRemoved > 0) {
|
|
912
|
+
items.push(
|
|
913
|
+
'<div class="info-item"><div class="info-label">Orphaned</div><div class="info-value truncation-value">'
|
|
914
|
+
+ s.totalBlocksRemoved
|
|
915
|
+
+ " blocks removed</div></div>",
|
|
916
|
+
)
|
|
917
|
+
}
|
|
918
|
+
if (s.systemReminderRemovals > 0) {
|
|
919
|
+
items.push(
|
|
920
|
+
'<div class="info-item"><div class="info-label">Reminders</div><div class="info-value">'
|
|
921
|
+
+ s.systemReminderRemovals
|
|
922
|
+
+ " tags filtered</div></div>",
|
|
923
|
+
)
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// Render as a single grid with two equal rows
|
|
928
|
+
const cols = Math.ceil(items.length / 2)
|
|
929
|
+
html += '<div class="info-card" style="grid-template-columns: repeat(' + cols + ', 1fr)">'
|
|
930
|
+
html += items.join("")
|
|
931
|
+
html += "</div>"
|
|
932
|
+
}
|
|
933
|
+
html += "</div>"
|
|
934
|
+
|
|
935
|
+
html += "</div>"
|
|
936
|
+
content.innerHTML = html
|
|
937
|
+
|
|
938
|
+
// Show expand buttons for blocks that actually overflow
|
|
939
|
+
updateExpandButtons()
|
|
940
|
+
|
|
941
|
+
// Apply search highlighting if needed
|
|
942
|
+
if (detailSearchQuery) {
|
|
943
|
+
applySearchHighlight()
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
let msgIdCounter = 0
|
|
948
|
+
function renderSystemMessage(system, rewrittenSystem) {
|
|
949
|
+
let text = ""
|
|
950
|
+
if (typeof system === "string") {
|
|
951
|
+
text = system
|
|
952
|
+
} else if (Array.isArray(system)) {
|
|
953
|
+
text = system.map((b) => b.text || "").join("\n")
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
const isRewritten = rewrittenSystem != null && rewrittenSystem !== text
|
|
957
|
+
|
|
958
|
+
const msgId = "msg-sys-" + msgIdCounter++
|
|
959
|
+
const summary = escapeHtml(text.length > 80 ? text.slice(0, 80) + "..." : text)
|
|
960
|
+
const displayText = detailSearchQuery ? highlightSearch(text, detailSearchQuery) : escapeHtml(text)
|
|
961
|
+
const sysRawIdx = registerRawData(system)
|
|
962
|
+
|
|
963
|
+
let classes = "message-block"
|
|
964
|
+
if (isRewritten) classes += " rewritten"
|
|
965
|
+
let html = '<div class="' + classes + '" id="' + msgId + '">'
|
|
966
|
+
html += '<div class="message-header">'
|
|
967
|
+
html += '<div class="message-header-left">'
|
|
968
|
+
html += '<span class="collapse-icon" onclick="toggleMessage(\'' + msgId + "')\">\u25BE</span>"
|
|
969
|
+
html += '<span class="message-role system">SYSTEM</span>'
|
|
970
|
+
if (isRewritten) {
|
|
971
|
+
html += '<span class="rewrite-badge rewritten">(rewritten)</span>'
|
|
972
|
+
}
|
|
973
|
+
html += '<span class="collapsed-summary" style="display:none">' + summary + "</span>"
|
|
974
|
+
html += "</div>"
|
|
975
|
+
html += '<div class="message-header-actions">'
|
|
976
|
+
|
|
977
|
+
if (isRewritten) {
|
|
978
|
+
html += '<div class="rewrite-toggle">'
|
|
979
|
+
html +=
|
|
980
|
+
'<button class="rewrite-tab active" data-mode="original" onclick="event.stopPropagation();switchRewriteView(\''
|
|
981
|
+
+ msgId
|
|
982
|
+
+ "','original')\">Original</button>"
|
|
983
|
+
html +=
|
|
984
|
+
'<button class="rewrite-tab" data-mode="rewritten" onclick="event.stopPropagation();switchRewriteView(\''
|
|
985
|
+
+ msgId
|
|
986
|
+
+ "','rewritten')\">Rewritten</button>"
|
|
987
|
+
html +=
|
|
988
|
+
'<button class="rewrite-tab" data-mode="diff" onclick="event.stopPropagation();switchRewriteView(\''
|
|
989
|
+
+ msgId
|
|
990
|
+
+ "','diff')\">Diff</button>"
|
|
991
|
+
html += "</div>"
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
html +=
|
|
995
|
+
'<button class="action-btn expand-toggle" data-target="'
|
|
996
|
+
+ msgId
|
|
997
|
+
+ '" onclick="event.stopPropagation();toggleBodyExpand(this)" style="display:none">'
|
|
998
|
+
+ ICON_EXPAND
|
|
999
|
+
+ "Expand</button>"
|
|
1000
|
+
html +=
|
|
1001
|
+
'<button class="action-btn" onclick="event.stopPropagation();copyText(this)" data-text="'
|
|
1002
|
+
+ escapeHtml(text).replaceAll('"', """)
|
|
1003
|
+
+ '" title="Copy">'
|
|
1004
|
+
+ ICON_COPY
|
|
1005
|
+
+ "Copy</button>"
|
|
1006
|
+
html +=
|
|
1007
|
+
'<button class="action-btn" onclick="event.stopPropagation();showRaw(\'System\', '
|
|
1008
|
+
+ sysRawIdx
|
|
1009
|
+
+ ')" title="View Raw">'
|
|
1010
|
+
+ ICON_CODE
|
|
1011
|
+
+ "Raw</button>"
|
|
1012
|
+
html += "</div>"
|
|
1013
|
+
html += "</div>"
|
|
1014
|
+
|
|
1015
|
+
if (isRewritten) {
|
|
1016
|
+
html += '<div class="message-body message-body-original body-collapsed">'
|
|
1017
|
+
html += '<div class="content-text">' + displayText + "</div>"
|
|
1018
|
+
html += "</div>"
|
|
1019
|
+
|
|
1020
|
+
const rewrittenDisplay =
|
|
1021
|
+
detailSearchQuery ? highlightSearch(rewrittenSystem, detailSearchQuery) : escapeHtml(rewrittenSystem)
|
|
1022
|
+
html += '<div class="message-body message-body-rewritten body-collapsed" style="display:none">'
|
|
1023
|
+
html += '<div class="content-text">' + rewrittenDisplay + "</div>"
|
|
1024
|
+
html += "</div>"
|
|
1025
|
+
|
|
1026
|
+
html += '<div class="message-body message-body-diff body-collapsed" style="display:none">'
|
|
1027
|
+
var sysDiffHtml = computeTextDiff(text, rewrittenSystem)
|
|
1028
|
+
html += sysDiffHtml || '<div class="diff-no-changes">No differences</div>'
|
|
1029
|
+
html += "</div>"
|
|
1030
|
+
} else {
|
|
1031
|
+
html += '<div class="message-body body-collapsed">'
|
|
1032
|
+
html += '<div class="content-text">' + displayText + "</div>"
|
|
1033
|
+
html += "</div>"
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
html += "</div>"
|
|
1037
|
+
return html
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
/**
|
|
1041
|
+
* Compare two messages' content for equality.
|
|
1042
|
+
* Handles both Anthropic format (content as array of blocks) and
|
|
1043
|
+
* OpenAI format (content as string). Returns true if content is equivalent.
|
|
1044
|
+
*/
|
|
1045
|
+
function messagesContentEqual(msgA, msgB) {
|
|
1046
|
+
var a = msgA.content
|
|
1047
|
+
var b = msgB.content
|
|
1048
|
+
// Both strings: direct compare
|
|
1049
|
+
if (typeof a === "string" && typeof b === "string") return a === b
|
|
1050
|
+
// Both arrays: compare JSON (handles tool_use, tool_result, etc.)
|
|
1051
|
+
if (Array.isArray(a) && Array.isArray(b)) return JSON.stringify(a) === JSON.stringify(b)
|
|
1052
|
+
// Mixed formats (Anthropic vs OpenAI): compare extracted text
|
|
1053
|
+
return extractMessageText(msgA) === extractMessageText(msgB)
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
function renderMessageContent(content, filterType, aggregateTools, toolResultMap, toolUseNameMap, msg) {
|
|
1057
|
+
let html = ""
|
|
1058
|
+
if (typeof content === "string") {
|
|
1059
|
+
if (!filterType || filterType === "text") {
|
|
1060
|
+
html += renderTextBlock(content, null)
|
|
1061
|
+
}
|
|
1062
|
+
} else if (Array.isArray(content)) {
|
|
1063
|
+
let hasVisibleBlocks = false
|
|
1064
|
+
for (const block of content) {
|
|
1065
|
+
if (aggregateTools && block.type === "tool_result") continue
|
|
1066
|
+
if (filterType && block.type !== filterType) continue
|
|
1067
|
+
hasVisibleBlocks = true
|
|
1068
|
+
switch (block.type) {
|
|
1069
|
+
case "text": {
|
|
1070
|
+
html += renderTextBlock(block.text, block)
|
|
1071
|
+
|
|
1072
|
+
break
|
|
1073
|
+
}
|
|
1074
|
+
case "tool_use": {
|
|
1075
|
+
html += renderToolUseBlock(block, aggregateTools, toolResultMap)
|
|
1076
|
+
|
|
1077
|
+
break
|
|
1078
|
+
}
|
|
1079
|
+
case "tool_result": {
|
|
1080
|
+
html += renderToolResultBlock(block, toolUseNameMap)
|
|
1081
|
+
|
|
1082
|
+
break
|
|
1083
|
+
}
|
|
1084
|
+
case "image":
|
|
1085
|
+
case "image_url": {
|
|
1086
|
+
html += renderImageBlock(block)
|
|
1087
|
+
|
|
1088
|
+
break
|
|
1089
|
+
}
|
|
1090
|
+
case "thinking": {
|
|
1091
|
+
html += renderThinkingBlock(block)
|
|
1092
|
+
|
|
1093
|
+
break
|
|
1094
|
+
}
|
|
1095
|
+
default: {
|
|
1096
|
+
html += renderGenericBlock(block)
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
if (!hasVisibleBlocks && aggregateTools) {
|
|
1102
|
+
const toolIds = content
|
|
1103
|
+
.filter(function (b) {
|
|
1104
|
+
return b.type === "tool_result" && b.tool_use_id
|
|
1105
|
+
})
|
|
1106
|
+
.map(function (b) {
|
|
1107
|
+
return b.tool_use_id
|
|
1108
|
+
})
|
|
1109
|
+
if (toolIds.length > 0) {
|
|
1110
|
+
html += '<div class="aggregated-links">'
|
|
1111
|
+
html += '<span class="aggregated-label">Tool results aggregated to:</span>'
|
|
1112
|
+
for (const id of toolIds) {
|
|
1113
|
+
html += ' <a class="tool-link" onclick="scrollToToolUse(\'' + id + "')\">\u2190 " + escapeHtml(id) + "</a>"
|
|
1114
|
+
}
|
|
1115
|
+
html += "</div>"
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
// OpenAI format: render tool_calls from the message object
|
|
1120
|
+
if (msg && msg.tool_calls && Array.isArray(msg.tool_calls)) {
|
|
1121
|
+
for (const tc of msg.tool_calls) {
|
|
1122
|
+
if (filterType && filterType !== "tool_use") continue
|
|
1123
|
+
// Convert OpenAI tool_call to Anthropic-like tool_use block for rendering
|
|
1124
|
+
var toolUseBlock = {
|
|
1125
|
+
type: "tool_use",
|
|
1126
|
+
id: tc.id,
|
|
1127
|
+
name: tc.function ? tc.function.name : "unknown",
|
|
1128
|
+
input: tc.function ? tc.function.arguments : "{}",
|
|
1129
|
+
}
|
|
1130
|
+
// Try to parse arguments as JSON for prettier display
|
|
1131
|
+
try { toolUseBlock.input = JSON.parse(toolUseBlock.input) } catch (e) { /* keep as string */ }
|
|
1132
|
+
html += renderToolUseBlock(toolUseBlock, aggregateTools, toolResultMap)
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
// OpenAI format: tool response message (role=tool with tool_call_id)
|
|
1136
|
+
if (msg && msg.role === "tool" && msg.tool_call_id && typeof content === "string") {
|
|
1137
|
+
if (!filterType || filterType === "tool_result") {
|
|
1138
|
+
var toolResultBlock = {
|
|
1139
|
+
type: "tool_result",
|
|
1140
|
+
tool_use_id: msg.tool_call_id,
|
|
1141
|
+
content: content,
|
|
1142
|
+
}
|
|
1143
|
+
html += renderToolResultBlock(toolResultBlock, toolUseNameMap)
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
return html
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
function extractMessageText(msg) {
|
|
1150
|
+
const content = msg.content
|
|
1151
|
+
if (typeof content === "string") return content
|
|
1152
|
+
if (Array.isArray(content)) {
|
|
1153
|
+
var parts = []
|
|
1154
|
+
for (var b of content) {
|
|
1155
|
+
if (b.type === "text" && b.text) {
|
|
1156
|
+
parts.push(b.text)
|
|
1157
|
+
} else if (b.type === "tool_use") {
|
|
1158
|
+
var input = typeof b.input === "string" ? b.input : JSON.stringify(b.input || {}, null, 2)
|
|
1159
|
+
parts.push("[tool_use: " + (b.name || "") + "]\n" + input)
|
|
1160
|
+
} else if (b.type === "tool_result") {
|
|
1161
|
+
var rc = typeof b.content === "string" ? b.content : JSON.stringify(b.content, null, 2)
|
|
1162
|
+
parts.push("[tool_result: " + (b.tool_use_id || "") + "]\n" + (rc || ""))
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
return parts.join("\n")
|
|
1166
|
+
}
|
|
1167
|
+
return ""
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
function computeTextDiff(oldText, newText) {
|
|
1171
|
+
if (oldText === newText) return ""
|
|
1172
|
+
// Use jsdiff to create unified diff, then diff2html to render side-by-side
|
|
1173
|
+
var diffStr = Diff.createPatch("message", oldText, newText, "original", "rewritten", { context: 3 })
|
|
1174
|
+
return Diff2Html.html(diffStr, {
|
|
1175
|
+
outputFormat: "side-by-side",
|
|
1176
|
+
drawFileList: false,
|
|
1177
|
+
matching: "words",
|
|
1178
|
+
diffStyle: "word",
|
|
1179
|
+
})
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
function switchRewriteView(msgId, mode) {
|
|
1183
|
+
const block = document.getElementById(msgId)
|
|
1184
|
+
if (!block) return
|
|
1185
|
+
|
|
1186
|
+
// Update tab buttons
|
|
1187
|
+
const tabs = block.querySelectorAll(".rewrite-tab")
|
|
1188
|
+
for (const tab of tabs) {
|
|
1189
|
+
tab.classList.toggle("active", tab.dataset.mode === mode)
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
// Show/hide body containers
|
|
1193
|
+
const original = block.querySelector(".message-body-original")
|
|
1194
|
+
const rewritten = block.querySelector(".message-body-rewritten")
|
|
1195
|
+
const diff = block.querySelector(".message-body-diff")
|
|
1196
|
+
if (original) original.style.display = mode === "original" ? "" : "none"
|
|
1197
|
+
if (rewritten) rewritten.style.display = mode === "rewritten" ? "" : "none"
|
|
1198
|
+
if (diff) diff.style.display = mode === "diff" ? "" : "none"
|
|
1199
|
+
|
|
1200
|
+
// Reset message-level expand toggle text to match newly visible body's state
|
|
1201
|
+
var expandToggle = block.querySelector(":scope > .message-header .expand-toggle")
|
|
1202
|
+
if (expandToggle) {
|
|
1203
|
+
var visibleBody =
|
|
1204
|
+
mode === "original" ? original
|
|
1205
|
+
: mode === "rewritten" ? rewritten
|
|
1206
|
+
: diff
|
|
1207
|
+
if (visibleBody) {
|
|
1208
|
+
var isCollapsed = visibleBody.classList.contains("body-collapsed")
|
|
1209
|
+
expandToggle.innerHTML = isCollapsed ? ICON_EXPAND + "Expand" : ICON_CONTRACT + "Collapse"
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
// Re-check expand buttons for newly visible content
|
|
1214
|
+
updateExpandButtons()
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
function renderMessage(
|
|
1218
|
+
msg,
|
|
1219
|
+
filterType,
|
|
1220
|
+
aggregateTools,
|
|
1221
|
+
toolResultMap,
|
|
1222
|
+
toolUseNameMap,
|
|
1223
|
+
isTruncated,
|
|
1224
|
+
isRewritten,
|
|
1225
|
+
rewrittenMsg,
|
|
1226
|
+
) {
|
|
1227
|
+
const role = msg.role || "unknown"
|
|
1228
|
+
const content = msg.content
|
|
1229
|
+
|
|
1230
|
+
const msgId = "msg-" + role + "-" + msgIdCounter++
|
|
1231
|
+
const summary = escapeHtml(getMessageSummary(content))
|
|
1232
|
+
const msgRawIdx = registerRawData(msg)
|
|
1233
|
+
|
|
1234
|
+
let classes = "message-block"
|
|
1235
|
+
if (isTruncated) classes += " truncated"
|
|
1236
|
+
if (isRewritten && !isTruncated) classes += " rewritten"
|
|
1237
|
+
let html = '<div class="' + classes + '" id="' + msgId + '">'
|
|
1238
|
+
html += '<div class="message-header">'
|
|
1239
|
+
html += '<div class="message-header-left">'
|
|
1240
|
+
html += '<span class="collapse-icon" onclick="toggleMessage(\'' + msgId + "')\">\u25BE</span>"
|
|
1241
|
+
html += '<span class="message-role ' + role + '">' + role.toUpperCase() + "</span>"
|
|
1242
|
+
if (isTruncated) {
|
|
1243
|
+
html += '<span class="rewrite-badge deleted">(deleted)</span>'
|
|
1244
|
+
} else if (isRewritten) {
|
|
1245
|
+
html += '<span class="rewrite-badge rewritten">(rewritten)</span>'
|
|
1246
|
+
}
|
|
1247
|
+
html += '<span class="collapsed-summary" style="display:none">' + summary + "</span>"
|
|
1248
|
+
html += "</div>"
|
|
1249
|
+
html += '<div class="message-header-actions">'
|
|
1250
|
+
|
|
1251
|
+
// Rewrite toggle buttons (only for rewritten messages with available rewritten data)
|
|
1252
|
+
if (isRewritten && !isTruncated && rewrittenMsg) {
|
|
1253
|
+
html += '<div class="rewrite-toggle">'
|
|
1254
|
+
html +=
|
|
1255
|
+
'<button class="rewrite-tab active" data-mode="original" onclick="event.stopPropagation();switchRewriteView(\''
|
|
1256
|
+
+ msgId
|
|
1257
|
+
+ "','original')\">Original</button>"
|
|
1258
|
+
html +=
|
|
1259
|
+
'<button class="rewrite-tab" data-mode="rewritten" onclick="event.stopPropagation();switchRewriteView(\''
|
|
1260
|
+
+ msgId
|
|
1261
|
+
+ "','rewritten')\">Rewritten</button>"
|
|
1262
|
+
html +=
|
|
1263
|
+
'<button class="rewrite-tab" data-mode="diff" onclick="event.stopPropagation();switchRewriteView(\''
|
|
1264
|
+
+ msgId
|
|
1265
|
+
+ "','diff')\">Diff</button>"
|
|
1266
|
+
html += "</div>"
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
html +=
|
|
1270
|
+
'<button class="action-btn expand-toggle" data-target="'
|
|
1271
|
+
+ msgId
|
|
1272
|
+
+ '" onclick="event.stopPropagation();toggleBodyExpand(this)" style="display:none">'
|
|
1273
|
+
+ ICON_EXPAND
|
|
1274
|
+
+ "Expand</button>"
|
|
1275
|
+
html +=
|
|
1276
|
+
'<button class="action-btn" onclick="event.stopPropagation();showRaw(\''
|
|
1277
|
+
+ role
|
|
1278
|
+
+ "', "
|
|
1279
|
+
+ msgRawIdx
|
|
1280
|
+
+ ')" title="View Raw">'
|
|
1281
|
+
+ ICON_CODE
|
|
1282
|
+
+ "Raw</button>"
|
|
1283
|
+
html += "</div>"
|
|
1284
|
+
html += "</div>"
|
|
1285
|
+
|
|
1286
|
+
if (isRewritten && !isTruncated && rewrittenMsg) {
|
|
1287
|
+
// Three switchable body containers
|
|
1288
|
+
html += '<div class="message-body message-body-original">'
|
|
1289
|
+
html += renderMessageContent(content, filterType, aggregateTools, toolResultMap, toolUseNameMap, msg)
|
|
1290
|
+
html += "</div>"
|
|
1291
|
+
|
|
1292
|
+
html += '<div class="message-body message-body-rewritten" style="display:none">'
|
|
1293
|
+
html += renderMessageContent(rewrittenMsg.content, filterType, aggregateTools, toolResultMap, toolUseNameMap, rewrittenMsg)
|
|
1294
|
+
html += "</div>"
|
|
1295
|
+
|
|
1296
|
+
html += '<div class="message-body message-body-diff body-collapsed" style="display:none">'
|
|
1297
|
+
var diffHtml = computeTextDiff(extractMessageText(msg), extractMessageText(rewrittenMsg))
|
|
1298
|
+
html += diffHtml || '<div class="diff-no-changes">No differences</div>'
|
|
1299
|
+
html += "</div>"
|
|
1300
|
+
} else {
|
|
1301
|
+
// Standard single body
|
|
1302
|
+
html += '<div class="message-body">'
|
|
1303
|
+
html += renderMessageContent(content, filterType, aggregateTools, toolResultMap, toolUseNameMap, msg)
|
|
1304
|
+
html += "</div>"
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
html += "</div>"
|
|
1308
|
+
return html
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
let blockIdCounter = 0
|
|
1312
|
+
function renderTextBlock(text, block) {
|
|
1313
|
+
const blockId = "cb-" + blockIdCounter++
|
|
1314
|
+
const summary = escapeHtml(getContentBlockSummary(block || { type: "text", text: text || "" }))
|
|
1315
|
+
const displayText = detailSearchQuery ? highlightSearch(text, detailSearchQuery) : escapeHtml(text)
|
|
1316
|
+
const blockData = block || { type: "text", text: text || "" }
|
|
1317
|
+
const blockRawIdx = registerRawData(blockData)
|
|
1318
|
+
|
|
1319
|
+
let html = '<div class="content-block" id="' + blockId + '">'
|
|
1320
|
+
html += '<div class="content-block-header">'
|
|
1321
|
+
html += '<div class="content-block-header-left">'
|
|
1322
|
+
html += '<span class="collapse-icon" onclick="toggleContentBlock(\'' + blockId + "')\">\u25BE</span>"
|
|
1323
|
+
html += '<span class="content-type text">TEXT</span>'
|
|
1324
|
+
html += '<span class="collapsed-summary" style="display:none">' + summary + "</span>"
|
|
1325
|
+
html += "</div>"
|
|
1326
|
+
html += '<div class="content-block-actions">'
|
|
1327
|
+
html +=
|
|
1328
|
+
'<button class="action-btn expand-toggle" data-target="'
|
|
1329
|
+
+ blockId
|
|
1330
|
+
+ '" onclick="event.stopPropagation();toggleBodyExpand(this)" style="display:none">'
|
|
1331
|
+
+ ICON_EXPAND
|
|
1332
|
+
+ "Expand</button>"
|
|
1333
|
+
html +=
|
|
1334
|
+
'<button class="action-btn" onclick="event.stopPropagation();copyText(this)" data-text="'
|
|
1335
|
+
+ escapeHtml(text || "").replaceAll('"', """)
|
|
1336
|
+
+ '" title="Copy">'
|
|
1337
|
+
+ ICON_COPY
|
|
1338
|
+
+ "Copy</button>"
|
|
1339
|
+
html +=
|
|
1340
|
+
'<button class="action-btn" onclick="event.stopPropagation();showRaw(\'Text\', '
|
|
1341
|
+
+ blockRawIdx
|
|
1342
|
+
+ ')" title="View Raw">'
|
|
1343
|
+
+ ICON_CODE
|
|
1344
|
+
+ "Raw</button>"
|
|
1345
|
+
html += "</div>"
|
|
1346
|
+
html += "</div>"
|
|
1347
|
+
html += '<div class="content-block-body body-collapsed">'
|
|
1348
|
+
html += '<div class="content-text">' + displayText + "</div>"
|
|
1349
|
+
html += "</div>"
|
|
1350
|
+
html += "</div>"
|
|
1351
|
+
return html
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
function renderToolUseBlock(block, aggregateTools, toolResultMap) {
|
|
1355
|
+
const blockId = "cb-" + blockIdCounter++
|
|
1356
|
+
const summary = escapeHtml(block.name || "")
|
|
1357
|
+
const blockRawIdx = registerRawData(block)
|
|
1358
|
+
const inputJson = typeof block.input === "string" ? block.input : JSON.stringify(block.input || {}, null, 2)
|
|
1359
|
+
|
|
1360
|
+
let html = '<div class="content-block" id="tool-' + block.id + '">'
|
|
1361
|
+
html += '<div class="content-block-header">'
|
|
1362
|
+
html += '<div class="content-block-header-left">'
|
|
1363
|
+
html += '<span class="collapse-icon" onclick="toggleContentBlock(\'tool-' + block.id + "')\">\u25BE</span>"
|
|
1364
|
+
html += '<span class="content-type tool_use">TOOL USE</span>'
|
|
1365
|
+
html += '<span class="tool-name">' + escapeHtml(block.name) + "</span>"
|
|
1366
|
+
html += '<span class="tool-id">' + escapeHtml(block.id) + "</span>"
|
|
1367
|
+
html += '<span class="collapsed-summary" style="display:none">' + summary + "</span>"
|
|
1368
|
+
html += "</div>"
|
|
1369
|
+
html += '<div class="content-block-actions">'
|
|
1370
|
+
html +=
|
|
1371
|
+
'<button class="action-btn expand-toggle" data-target="tool-'
|
|
1372
|
+
+ block.id
|
|
1373
|
+
+ '" onclick="event.stopPropagation();toggleBodyExpand(this)" style="display:none">'
|
|
1374
|
+
+ ICON_EXPAND
|
|
1375
|
+
+ "Expand</button>"
|
|
1376
|
+
html +=
|
|
1377
|
+
'<button class="action-btn" onclick="event.stopPropagation();copyText(this)" data-text="'
|
|
1378
|
+
+ escapeHtml(inputJson).replaceAll('"', """)
|
|
1379
|
+
+ '" title="Copy">'
|
|
1380
|
+
+ ICON_COPY
|
|
1381
|
+
+ "Copy</button>"
|
|
1382
|
+
html +=
|
|
1383
|
+
'<button class="action-btn" onclick="event.stopPropagation();showRaw(\'Tool Use\', '
|
|
1384
|
+
+ blockRawIdx
|
|
1385
|
+
+ ')" title="View Raw">'
|
|
1386
|
+
+ ICON_CODE
|
|
1387
|
+
+ "Raw</button>"
|
|
1388
|
+
html += "</div>"
|
|
1389
|
+
html += "</div>"
|
|
1390
|
+
html += '<div class="content-block-body body-collapsed">'
|
|
1391
|
+
html += '<div class="tool-input">' + escapeHtml(inputJson) + "</div>"
|
|
1392
|
+
|
|
1393
|
+
if (aggregateTools && toolResultMap[block.id]) {
|
|
1394
|
+
const result = toolResultMap[block.id]
|
|
1395
|
+
const resultContent = typeof result.content === "string" ? result.content : JSON.stringify(result.content, null, 2)
|
|
1396
|
+
const resultRawIdx = registerRawData(result)
|
|
1397
|
+
html += '<div class="tool-result-inline">'
|
|
1398
|
+
html += '<div class="tool-result-inline-header">'
|
|
1399
|
+
html += "<span>RESULT</span>"
|
|
1400
|
+
html +=
|
|
1401
|
+
'<button class="action-btn" onclick="event.stopPropagation();showRaw(\'Tool Result\', '
|
|
1402
|
+
+ resultRawIdx
|
|
1403
|
+
+ ')" title="View Raw">'
|
|
1404
|
+
+ ICON_CODE
|
|
1405
|
+
+ "Raw</button>"
|
|
1406
|
+
html += "</div>"
|
|
1407
|
+
html += '<div class="tool-result-inline-body">' + escapeHtml(resultContent) + "</div>"
|
|
1408
|
+
html += "</div>"
|
|
1409
|
+
} else if (!aggregateTools && toolResultMap) {
|
|
1410
|
+
// Only show "Jump to result" when tool results exist in the conversation
|
|
1411
|
+
// (response section passes null toolResultMap since responses never contain tool_result)
|
|
1412
|
+
html += '<a class="tool-link" onclick="scrollToToolResult(\'' + block.id + "')\">\u2192 Jump to result</a>"
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
html += "</div></div>"
|
|
1416
|
+
return html
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
function renderToolResultBlock(block, toolUseNameMap) {
|
|
1420
|
+
const blockRawIdx = registerRawData(block)
|
|
1421
|
+
const content = typeof block.content === "string" ? block.content : JSON.stringify(block.content, null, 2)
|
|
1422
|
+
const toolName = toolUseNameMap && block.tool_use_id ? toolUseNameMap[block.tool_use_id] : ""
|
|
1423
|
+
const summary = escapeHtml("for " + (toolName || block.tool_use_id || ""))
|
|
1424
|
+
|
|
1425
|
+
let html = '<div class="content-block" id="result-' + block.tool_use_id + '">'
|
|
1426
|
+
html += '<div class="content-block-header">'
|
|
1427
|
+
html += '<div class="content-block-header-left">'
|
|
1428
|
+
html += '<span class="collapse-icon" onclick="toggleContentBlock(\'result-' + block.tool_use_id + "')\">\u25BE</span>"
|
|
1429
|
+
html += '<span class="content-type tool_result">TOOL RESULT</span>'
|
|
1430
|
+
if (toolName) {
|
|
1431
|
+
html += '<span class="tool-name">' + escapeHtml(toolName) + "</span>"
|
|
1432
|
+
}
|
|
1433
|
+
html += '<span class="tool-id">for ' + escapeHtml(block.tool_use_id) + "</span>"
|
|
1434
|
+
html += '<span class="collapsed-summary" style="display:none">' + summary + "</span>"
|
|
1435
|
+
html += "</div>"
|
|
1436
|
+
html += '<div class="content-block-actions">'
|
|
1437
|
+
html +=
|
|
1438
|
+
'<button class="action-btn expand-toggle" data-target="result-'
|
|
1439
|
+
+ block.tool_use_id
|
|
1440
|
+
+ '" onclick="event.stopPropagation();toggleBodyExpand(this)" style="display:none">'
|
|
1441
|
+
+ ICON_EXPAND
|
|
1442
|
+
+ "Expand</button>"
|
|
1443
|
+
html +=
|
|
1444
|
+
'<button class="action-btn" onclick="event.stopPropagation();copyText(this)" data-text="'
|
|
1445
|
+
+ escapeHtml(content).replaceAll('"', """)
|
|
1446
|
+
+ '" title="Copy">'
|
|
1447
|
+
+ ICON_COPY
|
|
1448
|
+
+ "Copy</button>"
|
|
1449
|
+
html +=
|
|
1450
|
+
'<a class="tool-link" onclick="event.stopPropagation();scrollToToolUse(\''
|
|
1451
|
+
+ block.tool_use_id
|
|
1452
|
+
+ "')\">\u2190 Jump to call</a>"
|
|
1453
|
+
html +=
|
|
1454
|
+
'<button class="action-btn" onclick="event.stopPropagation();showRaw(\'Tool Result\', '
|
|
1455
|
+
+ blockRawIdx
|
|
1456
|
+
+ ')" title="View Raw">'
|
|
1457
|
+
+ ICON_CODE
|
|
1458
|
+
+ "Raw</button>"
|
|
1459
|
+
html += "</div>"
|
|
1460
|
+
html += "</div>"
|
|
1461
|
+
html += '<div class="content-block-body body-collapsed">'
|
|
1462
|
+
html += '<div class="content-text">' + escapeHtml(content) + "</div>"
|
|
1463
|
+
html += "</div>"
|
|
1464
|
+
html += "</div>"
|
|
1465
|
+
return html
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
function renderImageBlock(block) {
|
|
1469
|
+
const blockRawIdx = registerRawData(block)
|
|
1470
|
+
const mediaType = block.source?.media_type || block.media_type || "unknown"
|
|
1471
|
+
|
|
1472
|
+
let html = '<div class="content-block">'
|
|
1473
|
+
html += '<div class="content-block-header">'
|
|
1474
|
+
html += '<div class="content-block-header-left">'
|
|
1475
|
+
html += '<span class="content-type image">IMAGE</span>'
|
|
1476
|
+
html += '<span style="font-size:10px;color:var(--text-dim);">' + escapeHtml(mediaType) + "</span>"
|
|
1477
|
+
html += "</div>"
|
|
1478
|
+
html += '<div class="content-block-actions">'
|
|
1479
|
+
html +=
|
|
1480
|
+
'<button class="action-btn" onclick="event.stopPropagation();showRaw(\'Image\', '
|
|
1481
|
+
+ blockRawIdx
|
|
1482
|
+
+ ')" title="View Raw">'
|
|
1483
|
+
+ ICON_CODE
|
|
1484
|
+
+ "Raw</button>"
|
|
1485
|
+
html += "</div>"
|
|
1486
|
+
html += "</div>"
|
|
1487
|
+
html += '<div class="content-block-body">'
|
|
1488
|
+
html += '<div style="color:var(--text-muted);font-size:12px;">[Image content - base64 encoded]</div>'
|
|
1489
|
+
html += "</div>"
|
|
1490
|
+
html += "</div>"
|
|
1491
|
+
return html
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
function renderThinkingBlock(block) {
|
|
1495
|
+
const blockId = "cb-" + blockIdCounter++
|
|
1496
|
+
const blockRawIdx = registerRawData(block)
|
|
1497
|
+
const text = block.thinking || ""
|
|
1498
|
+
const summary = escapeHtml(text.length > 60 ? text.slice(0, 60) + "..." : text)
|
|
1499
|
+
|
|
1500
|
+
let html = '<div class="content-block" id="' + blockId + '">'
|
|
1501
|
+
html += '<div class="content-block-header">'
|
|
1502
|
+
html += '<div class="content-block-header-left">'
|
|
1503
|
+
html += '<span class="collapse-icon" onclick="toggleContentBlock(\'' + blockId + "')\">\u25BE</span>"
|
|
1504
|
+
html += '<span class="content-type thinking">THINKING</span>'
|
|
1505
|
+
html += '<span class="collapsed-summary" style="display:none">' + summary + "</span>"
|
|
1506
|
+
html += "</div>"
|
|
1507
|
+
html += '<div class="content-block-actions">'
|
|
1508
|
+
html +=
|
|
1509
|
+
'<button class="action-btn expand-toggle" data-target="'
|
|
1510
|
+
+ blockId
|
|
1511
|
+
+ '" onclick="event.stopPropagation();toggleBodyExpand(this)" style="display:none">'
|
|
1512
|
+
+ ICON_EXPAND
|
|
1513
|
+
+ "Expand</button>"
|
|
1514
|
+
html +=
|
|
1515
|
+
'<button class="action-btn" onclick="event.stopPropagation();copyText(this)" data-text="'
|
|
1516
|
+
+ escapeHtml(text).replaceAll('"', """)
|
|
1517
|
+
+ '" title="Copy">'
|
|
1518
|
+
+ ICON_COPY
|
|
1519
|
+
+ "Copy</button>"
|
|
1520
|
+
html +=
|
|
1521
|
+
'<button class="action-btn" onclick="event.stopPropagation();showRaw(\'Thinking\', '
|
|
1522
|
+
+ blockRawIdx
|
|
1523
|
+
+ ')" title="View Raw">'
|
|
1524
|
+
+ ICON_CODE
|
|
1525
|
+
+ "Raw</button>"
|
|
1526
|
+
html += "</div>"
|
|
1527
|
+
html += "</div>"
|
|
1528
|
+
html += '<div class="content-block-body body-collapsed">'
|
|
1529
|
+
html += '<div class="content-text">' + escapeHtml(text) + "</div>"
|
|
1530
|
+
html += "</div>"
|
|
1531
|
+
html += "</div>"
|
|
1532
|
+
return html
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
function renderGenericBlock(block) {
|
|
1536
|
+
const blockRawIdx = registerRawData(block)
|
|
1537
|
+
|
|
1538
|
+
let html = '<div class="content-block">'
|
|
1539
|
+
html += '<div class="content-block-header">'
|
|
1540
|
+
html +=
|
|
1541
|
+
'<div class="content-block-header-left"><span class="content-type text">'
|
|
1542
|
+
+ escapeHtml(block.type || "UNKNOWN")
|
|
1543
|
+
+ "</span></div>"
|
|
1544
|
+
html += '<div class="content-block-actions">'
|
|
1545
|
+
html +=
|
|
1546
|
+
'<button class="action-btn" onclick="event.stopPropagation();showRaw(\'Block\', '
|
|
1547
|
+
+ blockRawIdx
|
|
1548
|
+
+ ')" title="View Raw">'
|
|
1549
|
+
+ ICON_CODE
|
|
1550
|
+
+ "Raw</button>"
|
|
1551
|
+
html += "</div>"
|
|
1552
|
+
html += "</div>"
|
|
1553
|
+
html += '<div class="content-block-body">'
|
|
1554
|
+
html += '<pre style="font-size:11px;margin:0;">' + escapeHtml(JSON.stringify(block, null, 2)) + "</pre>"
|
|
1555
|
+
html += "</div>"
|
|
1556
|
+
html += "</div>"
|
|
1557
|
+
return html
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
function applySearchHighlight() {
|
|
1561
|
+
const firstMatch = document.querySelector(".search-highlight")
|
|
1562
|
+
if (firstMatch) {
|
|
1563
|
+
firstMatch.scrollIntoView({ behavior: "smooth", block: "center" })
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
// UI Actions
|
|
1568
|
+
function toggleBodyExpand(toggleEl) {
|
|
1569
|
+
var targetId = toggleEl.dataset.target
|
|
1570
|
+
var container = document.getElementById(targetId)
|
|
1571
|
+
if (!container) return
|
|
1572
|
+
|
|
1573
|
+
var body
|
|
1574
|
+
if (container.classList.contains("message-block")) {
|
|
1575
|
+
// Message-level: find the currently visible direct message-body
|
|
1576
|
+
var bodies = container.querySelectorAll(":scope > .message-body")
|
|
1577
|
+
body = null
|
|
1578
|
+
for (const body_ of bodies) {
|
|
1579
|
+
if (body_.style.display !== "none") {
|
|
1580
|
+
body = body_
|
|
1581
|
+
break
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
} else {
|
|
1585
|
+
// Content-block level: find the content-block-body
|
|
1586
|
+
body = container.querySelector(".content-block-body")
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
if (!body) return
|
|
1590
|
+
var isCollapsed = body.classList.toggle("body-collapsed")
|
|
1591
|
+
toggleEl.innerHTML = isCollapsed ? ICON_EXPAND + "Expand" : ICON_CONTRACT + "Collapse"
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
function updateExpandButtons() {
|
|
1595
|
+
// Handle content-block level expand buttons
|
|
1596
|
+
var contentBlocks = document.querySelectorAll(".content-block")
|
|
1597
|
+
for (var i = 0; i < contentBlocks.length; i++) {
|
|
1598
|
+
var toggle = contentBlocks[i].querySelector(".expand-toggle")
|
|
1599
|
+
if (!toggle) continue
|
|
1600
|
+
var body = contentBlocks[i].querySelector(".content-block-body")
|
|
1601
|
+
toggle.style.display = body && body.classList.contains("body-collapsed") && body.scrollHeight > 200 ? "" : "none"
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
// Handle message-block level expand buttons
|
|
1605
|
+
var msgBlocks = document.querySelectorAll(".message-block")
|
|
1606
|
+
for (var i = 0; i < msgBlocks.length; i++) {
|
|
1607
|
+
// Only look at direct expand-toggle in message-header (not nested in content-blocks)
|
|
1608
|
+
var toggle = msgBlocks[i].querySelector(":scope > .message-header .expand-toggle")
|
|
1609
|
+
if (!toggle) continue
|
|
1610
|
+
// Find the currently visible message-body
|
|
1611
|
+
var bodies = msgBlocks[i].querySelectorAll(":scope > .message-body")
|
|
1612
|
+
var visibleBody = null
|
|
1613
|
+
for (const body_ of bodies) {
|
|
1614
|
+
if (body_.style.display !== "none") {
|
|
1615
|
+
visibleBody = body_
|
|
1616
|
+
break
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
toggle.style.display =
|
|
1620
|
+
visibleBody && visibleBody.classList.contains("body-collapsed") && visibleBody.scrollHeight > 200 ? "" : "none"
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
function scrollToToolUse(id) {
|
|
1625
|
+
const el = document.getElementById("tool-" + id)
|
|
1626
|
+
if (el) {
|
|
1627
|
+
el.scrollIntoView({ behavior: "smooth", block: "center" })
|
|
1628
|
+
highlightBlock(el)
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
function scrollToToolResult(id) {
|
|
1633
|
+
const el = document.getElementById("result-" + id)
|
|
1634
|
+
if (el) {
|
|
1635
|
+
el.scrollIntoView({ behavior: "smooth", block: "center" })
|
|
1636
|
+
highlightBlock(el)
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
function highlightBlock(el) {
|
|
1641
|
+
el.classList.remove("highlight-flash")
|
|
1642
|
+
void el.offsetWidth
|
|
1643
|
+
el.classList.add("highlight-flash")
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
function copyText(btn) {
|
|
1647
|
+
const text = btn.dataset.text
|
|
1648
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
1649
|
+
const origHtml = btn.innerHTML
|
|
1650
|
+
btn.innerHTML = "\u2713 Copied"
|
|
1651
|
+
setTimeout(() => (btn.innerHTML = origHtml), 1000)
|
|
1652
|
+
})
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
function showRaw(title, data) {
|
|
1656
|
+
const resolvedData = typeof data === "number" ? rawDataRegistry[data] : data
|
|
1657
|
+
document.querySelector("#raw-modal-title").textContent = title + " - Raw JSON"
|
|
1658
|
+
|
|
1659
|
+
// Use JSON tree view
|
|
1660
|
+
const modalBody = document.querySelector("#raw-modal .modal-body")
|
|
1661
|
+
modalBody.innerHTML = ""
|
|
1662
|
+
const tree = buildJsonTree(resolvedData, 3)
|
|
1663
|
+
modalBody.append(tree)
|
|
1664
|
+
|
|
1665
|
+
document.querySelector("#raw-modal").classList.add("open")
|
|
1666
|
+
// Store the data for copy
|
|
1667
|
+
modalBody.dataset.rawJson = JSON.stringify(resolvedData, null, 2)
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
function closeRawModal(event) {
|
|
1671
|
+
if (!event || event.target === event.currentTarget) {
|
|
1672
|
+
document.querySelector("#raw-modal").classList.remove("open")
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
function copyRawContent() {
|
|
1677
|
+
const modalBody = document.querySelector("#raw-modal .modal-body")
|
|
1678
|
+
navigator.clipboard.writeText(modalBody.dataset.rawJson || "")
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
function toggleExportMenu() {
|
|
1682
|
+
document.querySelector("#export-menu").classList.toggle("open")
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
function closeExportMenu() {
|
|
1686
|
+
document.querySelector("#export-menu").classList.remove("open")
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
function exportData(format) {
|
|
1690
|
+
globalThis.location.href = "/history/api/export?format=" + format
|
|
1691
|
+
closeExportMenu()
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
function onSessionChange() {
|
|
1695
|
+
currentSessionId = document.querySelector("#session-select").value || null
|
|
1696
|
+
currentPage = 1
|
|
1697
|
+
loadEntries()
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
function debounceListSearch() {
|
|
1701
|
+
clearTimeout(listSearchTimer)
|
|
1702
|
+
listSearchTimer = setTimeout(() => {
|
|
1703
|
+
currentPage = 1
|
|
1704
|
+
loadEntries()
|
|
1705
|
+
}, 300)
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
function debounceDetailSearch() {
|
|
1709
|
+
clearTimeout(detailSearchTimer)
|
|
1710
|
+
detailSearchTimer = setTimeout(() => {
|
|
1711
|
+
detailSearchQuery = document.querySelector("#detail-search").value
|
|
1712
|
+
renderDetail()
|
|
1713
|
+
}, 300)
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
async function clearAll() {
|
|
1717
|
+
if (!confirm("Are you sure you want to clear all history?")) return
|
|
1718
|
+
try {
|
|
1719
|
+
await fetch("/history/api/entries", { method: "DELETE" })
|
|
1720
|
+
hideDetailView()
|
|
1721
|
+
loadSessions()
|
|
1722
|
+
loadStats()
|
|
1723
|
+
loadEntries()
|
|
1724
|
+
} catch (e) {
|
|
1725
|
+
console.error("Failed to clear history:", e)
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
function refresh() {
|
|
1730
|
+
const listEl = document.querySelector("#request-list")
|
|
1731
|
+
const prevOpacity = listEl.style.opacity
|
|
1732
|
+
listEl.style.opacity = "0.5"
|
|
1733
|
+
listEl.style.transition = "opacity 0.15s"
|
|
1734
|
+
|
|
1735
|
+
Promise.all([loadSessions(), loadStats(), loadEntries()]).then(() => {
|
|
1736
|
+
listEl.style.opacity = prevOpacity || ""
|
|
1737
|
+
if (currentEntryId) {
|
|
1738
|
+
selectEntry(currentEntryId)
|
|
1739
|
+
}
|
|
1740
|
+
})
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
// Keyboard navigation
|
|
1744
|
+
document.addEventListener("keydown", (e) => {
|
|
1745
|
+
if (e.key === "Escape") {
|
|
1746
|
+
closeRawModal()
|
|
1747
|
+
closeExportMenu()
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
if (document.activeElement.tagName !== "INPUT" && document.activeElement.tagName !== "SELECT") {
|
|
1751
|
+
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
|
|
1752
|
+
e.preventDefault()
|
|
1753
|
+
const items = document.querySelectorAll(".request-item")
|
|
1754
|
+
if (items.length === 0) return
|
|
1755
|
+
|
|
1756
|
+
let currentIndex = -1
|
|
1757
|
+
for (const [index, item] of items.entries()) {
|
|
1758
|
+
if (item.classList.contains("selected")) currentIndex = index
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
let newIndex
|
|
1762
|
+
if (e.key === "ArrowDown") {
|
|
1763
|
+
newIndex = currentIndex < items.length - 1 ? currentIndex + 1 : 0
|
|
1764
|
+
} else {
|
|
1765
|
+
newIndex = currentIndex > 0 ? currentIndex - 1 : items.length - 1
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
const newItem = items[newIndex]
|
|
1769
|
+
selectEntry(newItem.dataset.id)
|
|
1770
|
+
newItem.scrollIntoView({ block: "nearest" })
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
if (e.key === "/") {
|
|
1774
|
+
e.preventDefault()
|
|
1775
|
+
document.querySelector("#list-search").focus()
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
})
|
|
1779
|
+
|
|
1780
|
+
// Initialize
|
|
1781
|
+
loadSessions()
|
|
1782
|
+
loadStats()
|
|
1783
|
+
loadEntries()
|
|
1784
|
+
|
|
1785
|
+
// Inject SVG icons into static elements
|
|
1786
|
+
document.querySelector("#list-search-icon").innerHTML = ICON_SEARCH
|
|
1787
|
+
document.querySelector("#detail-search-icon").innerHTML = ICON_SEARCH
|
|
1788
|
+
document.querySelector("#btn-close-raw").innerHTML = ICON_CLOSE
|
|
1789
|
+
document.querySelector("#btn-refresh").innerHTML = ICON_REFRESH + "Refresh"
|
|
1790
|
+
document.querySelector("#btn-export").innerHTML = ICON_DOWNLOAD + "Export"
|
|
1791
|
+
document.querySelector("#btn-clear").innerHTML = ICON_TRASH + "Clear"
|
|
1792
|
+
|
|
1793
|
+
// Close export menu on click outside
|
|
1794
|
+
document.addEventListener("click", function (e) {
|
|
1795
|
+
var wrapper = document.querySelector(".export-wrapper")
|
|
1796
|
+
if (wrapper && !wrapper.contains(e.target)) {
|
|
1797
|
+
closeExportMenu()
|
|
1798
|
+
}
|
|
1799
|
+
})
|