|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Tool Call Log Viewer</title> |
|
<style> |
|
* { |
|
box-sizing: border-box; |
|
margin: 0; |
|
padding: 0; |
|
} |
|
|
|
body { |
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; |
|
background: #232529; |
|
color: #cbcfd0; |
|
min-height: 100vh; |
|
padding: 20px; |
|
} |
|
|
|
.container { |
|
max-width: 1200px; |
|
margin: 0 auto; |
|
} |
|
|
|
h1 { |
|
text-align: center; |
|
margin-bottom: 20px; |
|
color: #cbcfd0; |
|
} |
|
|
|
/* Top controls */ |
|
.controls { |
|
background: #282a30; |
|
padding: 15px 20px; |
|
border-radius: 8px; |
|
margin-bottom: 15px; |
|
display: flex; |
|
gap: 15px; |
|
align-items: center; |
|
flex-wrap: wrap; |
|
} |
|
|
|
.file-input-wrapper { |
|
position: relative; |
|
} |
|
|
|
.file-input-wrapper input[type="file"] { |
|
display: none; |
|
} |
|
|
|
.file-input-wrapper label, |
|
button { |
|
background: #5fc9be; |
|
color: #232529; |
|
padding: 10px 20px; |
|
border-radius: 5px; |
|
cursor: pointer; |
|
font-weight: 600; |
|
border: none; |
|
font-size: 14px; |
|
transition: background 0.2s; |
|
} |
|
|
|
.file-input-wrapper label:hover, |
|
button:hover { |
|
background: #6cdfd3; |
|
} |
|
|
|
button.clear-btn { |
|
background: #cc6155; |
|
color: #e4e9ea; |
|
} |
|
|
|
button.clear-btn:hover { |
|
background: #ea695b; |
|
} |
|
|
|
button.quick-filter { |
|
background: transparent; |
|
border: 1px solid #414547; |
|
color: #9a9a9a; |
|
} |
|
|
|
button.quick-filter:hover { |
|
background: #414547; |
|
color: #e4e9ea; |
|
} |
|
|
|
button.quick-filter.active { |
|
background: rgba(95, 201, 190, 0.15); |
|
border-color: #5fc9be; |
|
color: #6cdfd3; |
|
} |
|
|
|
.search-box { |
|
flex: 1; |
|
min-width: 200px; |
|
} |
|
|
|
.search-box input { |
|
width: 100%; |
|
padding: 10px 15px; |
|
border-radius: 5px; |
|
border: 1px solid #414547; |
|
background: #1c1d21; |
|
color: #cbcfd0; |
|
font-size: 14px; |
|
} |
|
|
|
.search-box input:focus { |
|
outline: none; |
|
border-color: #5fc9be; |
|
} |
|
|
|
/* Filters row */ |
|
.filters { |
|
background: #282a30; |
|
padding: 15px 20px; |
|
border-radius: 8px; |
|
margin-bottom: 20px; |
|
display: flex; |
|
gap: 15px; |
|
align-items: center; |
|
flex-wrap: wrap; |
|
} |
|
|
|
.filter-group { |
|
display: flex; |
|
align-items: center; |
|
gap: 8px; |
|
} |
|
|
|
.filter-group label { |
|
font-size: 14px; |
|
color: #9a9a9a; |
|
} |
|
|
|
.filter-group select, |
|
.filter-group input[type="text"] { |
|
padding: 8px 12px; |
|
border-radius: 5px; |
|
border: 1px solid #414547; |
|
background: #1c1d21; |
|
color: #cbcfd0; |
|
font-size: 14px; |
|
font-family: monospace; |
|
} |
|
|
|
.filter-group select:focus, |
|
.filter-group input:focus { |
|
outline: none; |
|
border-color: #5fc9be; |
|
} |
|
|
|
/* Tool checkboxes */ |
|
.tool-filters-container { |
|
display: flex; |
|
flex-direction: column; |
|
gap: 8px; |
|
} |
|
|
|
.tool-filters-actions { |
|
display: flex; |
|
gap: 8px; |
|
} |
|
|
|
.tool-filters-actions button { |
|
padding: 4px 10px; |
|
font-size: 12px; |
|
background: transparent; |
|
border: 1px solid #414547; |
|
border-radius: 4px; |
|
color: #9a9a9a; |
|
cursor: pointer; |
|
} |
|
|
|
.tool-filters-actions button:hover { |
|
background: #414547; |
|
color: #e4e9ea; |
|
} |
|
|
|
.tool-filters { |
|
display: flex; |
|
flex-wrap: wrap; |
|
gap: 6px; |
|
} |
|
|
|
.tool-checkbox { |
|
display: flex; |
|
align-items: center; |
|
gap: 6px; |
|
padding: 5px 10px; |
|
background: #1c1d21; |
|
border-radius: 4px; |
|
cursor: pointer; |
|
font-size: 12px; |
|
white-space: nowrap; |
|
overflow: hidden; |
|
} |
|
|
|
.tool-checkbox:hover { |
|
background: #2a2b30; |
|
} |
|
|
|
.tool-checkbox input { |
|
cursor: pointer; |
|
margin: 0; |
|
flex-shrink: 0; |
|
} |
|
|
|
/* Stats bar */ |
|
.stats { |
|
background: #1c1d21; |
|
padding: 10px 20px; |
|
border-radius: 8px; |
|
margin-bottom: 20px; |
|
font-size: 14px; |
|
color: #9a9a9a; |
|
} |
|
|
|
/* Drop zone */ |
|
.drop-zone { |
|
border: 2px dashed #414547; |
|
border-radius: 8px; |
|
padding: 40px; |
|
text-align: center; |
|
margin-bottom: 20px; |
|
transition: all 0.2s; |
|
} |
|
|
|
.drop-zone.dragover { |
|
border-color: #5fc9be; |
|
background: rgba(95, 201, 190, 0.1); |
|
} |
|
|
|
.drop-zone p { |
|
color: #9a9a9a; |
|
font-size: 16px; |
|
} |
|
|
|
/* Log entries */ |
|
.entries { |
|
display: flex; |
|
flex-direction: column; |
|
gap: 10px; |
|
} |
|
|
|
.entry { |
|
background: #282a30; |
|
border-radius: 8px; |
|
overflow: hidden; |
|
border-left: 4px solid #9a9a9a; |
|
} |
|
|
|
/* Tool-specific colors */ |
|
.entry.tool-read { border-left-color: #91c382; } |
|
.entry.tool-edit { border-left-color: #cabc5c; } |
|
.entry.tool-write { border-left-color: #cc6155; } |
|
.entry.tool-bash { border-left-color: #cc6078; } |
|
.entry.tool-glob { border-left-color: #6d97ba; } |
|
.entry.tool-grep { border-left-color: #5fc9be; } |
|
.entry.tool-task { border-left-color: #eb6685; } |
|
.entry.tool-webfetch { border-left-color: #7ab3e1; } |
|
.entry.tool-websearch { border-left-color: #6cdfd3; } |
|
|
|
.entry-header { |
|
display: flex; |
|
align-items: center; |
|
padding: 12px 15px; |
|
gap: 15px; |
|
cursor: pointer; |
|
flex-wrap: wrap; |
|
} |
|
|
|
.entry-header:hover { |
|
background: #414547; |
|
} |
|
|
|
.tool-name { |
|
font-weight: 600; |
|
padding: 4px 10px; |
|
border-radius: 4px; |
|
font-size: 13px; |
|
min-width: 80px; |
|
text-align: center; |
|
line-height: 1.4; |
|
} |
|
|
|
.tool-read .tool-name { background: rgba(145, 195, 130, 0.2); color: #a4d992; } |
|
.tool-edit .tool-name { background: rgba(202, 188, 92, 0.2); color: #ebd96a; } |
|
.tool-write .tool-name { background: rgba(204, 97, 85, 0.2); color: #ea695b; } |
|
.tool-bash .tool-name { background: rgba(204, 96, 120, 0.2); color: #eb6685; } |
|
.tool-glob .tool-name { background: rgba(109, 151, 186, 0.2); color: #7ab3e1; } |
|
.tool-grep .tool-name { background: rgba(95, 201, 190, 0.2); color: #6cdfd3; } |
|
.tool-task .tool-name { background: rgba(235, 102, 133, 0.2); color: #eb6685; } |
|
.tool-webfetch .tool-name { background: rgba(122, 179, 225, 0.2); color: #7ab3e1; } |
|
.tool-websearch .tool-name { background: rgba(108, 223, 211, 0.2); color: #6cdfd3; } |
|
|
|
.timestamp { |
|
color: #9a9a9a; |
|
font-size: 13px; |
|
font-family: monospace; |
|
line-height: 1.4; |
|
position: relative; |
|
top: 2px; |
|
} |
|
|
|
.project-path { |
|
color: #7ab3e1; |
|
font-size: 13px; |
|
overflow: hidden; |
|
text-overflow: ellipsis; |
|
white-space: nowrap; |
|
max-width: 300px; |
|
line-height: 1.4; |
|
} |
|
|
|
.session-id { |
|
color: #9a9a9a; |
|
font-size: 13px; |
|
font-family: monospace; |
|
line-height: 1.4; |
|
position: relative; |
|
top: 2px; |
|
} |
|
|
|
.expand-icon { |
|
margin-left: auto; |
|
color: #9a9a9a; |
|
transition: transform 0.2s; |
|
} |
|
|
|
.entry.expanded .expand-icon { |
|
transform: rotate(180deg); |
|
} |
|
|
|
.entry-body { |
|
display: none; |
|
padding: 0 15px 15px; |
|
} |
|
|
|
.entry.expanded .entry-body { |
|
display: block; |
|
} |
|
|
|
.section { |
|
margin-top: 10px; |
|
} |
|
|
|
.section-header { |
|
display: flex; |
|
align-items: center; |
|
gap: 10px; |
|
padding: 8px 0; |
|
cursor: pointer; |
|
color: #9a9a9a; |
|
font-size: 13px; |
|
} |
|
|
|
.section-header:hover { |
|
color: #e4e9ea; |
|
} |
|
|
|
.section-content { |
|
background: #1c1d21; |
|
padding: 12px; |
|
border-radius: 5px; |
|
font-family: 'Consolas', 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; |
|
font-size: 12px; |
|
line-height: 1.5; |
|
white-space: pre-wrap; |
|
word-break: break-all; |
|
overflow-x: auto; |
|
max-height: 400px; |
|
overflow-y: auto; |
|
} |
|
|
|
.section.collapsed .section-content { |
|
display: none; |
|
} |
|
|
|
.section-header .section-icon { |
|
transition: transform 0.2s; |
|
} |
|
|
|
.section.collapsed .section-header .section-icon { |
|
transform: rotate(-90deg); |
|
} |
|
|
|
.truncated-notice { |
|
color: #cabc5c; |
|
font-style: italic; |
|
margin-top: 8px; |
|
} |
|
|
|
.show-more-btn { |
|
background: transparent; |
|
color: #5fc9be; |
|
padding: 5px 10px; |
|
font-size: 12px; |
|
margin-top: 8px; |
|
} |
|
|
|
.show-more-btn:hover { |
|
background: rgba(95, 201, 190, 0.1); |
|
} |
|
|
|
/* Error message */ |
|
.error { |
|
background: rgba(204, 97, 85, 0.2); |
|
border: 1px solid #cc6155; |
|
color: #ea695b; |
|
padding: 15px; |
|
border-radius: 8px; |
|
margin-bottom: 20px; |
|
} |
|
|
|
/* Empty state */ |
|
.empty-state { |
|
text-align: center; |
|
padding: 60px 20px; |
|
color: #9a9a9a; |
|
} |
|
|
|
.empty-state h2 { |
|
margin-bottom: 10px; |
|
color: #d5d8d9; |
|
} |
|
|
|
/* Highlight search matches */ |
|
.highlight { |
|
background: rgba(235, 217, 106, 0.3); |
|
padding: 1px 2px; |
|
border-radius: 2px; |
|
} |
|
|
|
/* JSON syntax highlighting */ |
|
.json-key { color: #7ab3e1; } |
|
.json-string { color: #91c382; } |
|
.json-number { color: #cabc5c; } |
|
.json-boolean { color: #cc6078; } |
|
.json-null { color: #cc6155; } |
|
.json-punctuation { color: #9a9a9a; } |
|
|
|
/* Responsive */ |
|
@media (max-width: 768px) { |
|
.controls, .filters { |
|
flex-direction: column; |
|
align-items: stretch; |
|
} |
|
|
|
.search-box { |
|
width: 100%; |
|
} |
|
|
|
.project-path { |
|
max-width: 150px; |
|
} |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<div class="container"> |
|
<h1>Tool Call Log Viewer</h1> |
|
|
|
<div class="controls"> |
|
<div class="file-input-wrapper"> |
|
<input type="file" id="fileInput" accept=".log,.jsonl,.txt"> |
|
<label for="fileInput">Load Log File</label> |
|
</div> |
|
<div class="search-box"> |
|
<input type="text" id="searchInput" placeholder="Search inputs and outputs..."> |
|
</div> |
|
</div> |
|
|
|
<div class="filters"> |
|
<div class="filter-group"> |
|
<label>Tools:</label> |
|
<div class="tool-filters-container"> |
|
<div class="tool-filters-actions"> |
|
<button id="selectAllTools">All</button> |
|
<button id="selectNoTools">None</button> |
|
</div> |
|
<div class="tool-filters" id="toolFilters"> |
|
<!-- Populated dynamically --> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div class="filters"> |
|
<button class="quick-filter" id="todayFilter">Today</button> |
|
<button class="quick-filter" id="latestSessionFilter">Latest Session</button> |
|
<div class="filter-group"> |
|
<label>From:</label> |
|
<input type="text" id="fromDate" placeholder="yyyy-mm-dd HH:MM:SS"> |
|
</div> |
|
<div class="filter-group"> |
|
<label>To:</label> |
|
<input type="text" id="toDate" placeholder="yyyy-mm-dd HH:MM:SS"> |
|
</div> |
|
<button class="clear-btn" id="clearFilters">Clear Filters</button> |
|
</div> |
|
|
|
<div class="stats" id="stats"> |
|
Load a log file to view tool calls |
|
</div> |
|
|
|
<div class="drop-zone" id="dropZone"> |
|
<p>Drag and drop a log file here, or use the Load button above</p> |
|
</div> |
|
|
|
<div id="errorContainer"></div> |
|
|
|
<div class="entries" id="entriesContainer"> |
|
<div class="empty-state"> |
|
<h2>No entries loaded</h2> |
|
<p>Load a tool-calls.log file to get started</p> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<script> |
|
// State |
|
let allEntries = []; |
|
let filteredEntries = []; |
|
let expandedEntries = new Set(); |
|
let expandedSections = new Set(); |
|
let showFullContent = new Set(); |
|
let activeSessionFilter = null; |
|
|
|
// Known tool types |
|
const TOOL_COLORS = { |
|
'Read': 'read', |
|
'Edit': 'edit', |
|
'Write': 'write', |
|
'Bash': 'bash', |
|
'Glob': 'glob', |
|
'Grep': 'grep', |
|
'Task': 'task', |
|
'WebFetch': 'webfetch', |
|
'WebSearch': 'websearch' |
|
}; |
|
|
|
// DOM elements |
|
const fileInput = document.getElementById('fileInput'); |
|
const searchInput = document.getElementById('searchInput'); |
|
const toolFilters = document.getElementById('toolFilters'); |
|
const fromDate = document.getElementById('fromDate'); |
|
const toDate = document.getElementById('toDate'); |
|
const clearFilters = document.getElementById('clearFilters'); |
|
const todayFilter = document.getElementById('todayFilter'); |
|
const latestSessionFilter = document.getElementById('latestSessionFilter'); |
|
const stats = document.getElementById('stats'); |
|
const dropZone = document.getElementById('dropZone'); |
|
const errorContainer = document.getElementById('errorContainer'); |
|
const entriesContainer = document.getElementById('entriesContainer'); |
|
|
|
// Load saved filter preferences |
|
function loadPreferences() { |
|
try { |
|
const prefs = JSON.parse(localStorage.getItem('toolLogViewerPrefs') || '{}'); |
|
if (prefs.search) searchInput.value = prefs.search; |
|
if (prefs.fromDate) fromDate.value = prefs.fromDate; |
|
if (prefs.toDate) toDate.value = prefs.toDate; |
|
return prefs.selectedTools || null; |
|
} catch (e) { |
|
return null; |
|
} |
|
} |
|
|
|
// Save filter preferences |
|
function savePreferences() { |
|
const selectedTools = Array.from(document.querySelectorAll('.tool-checkbox input:checked')) |
|
.map(cb => cb.value); |
|
localStorage.setItem('toolLogViewerPrefs', JSON.stringify({ |
|
search: searchInput.value, |
|
fromDate: fromDate.value, |
|
toDate: toDate.value, |
|
selectedTools |
|
})); |
|
} |
|
|
|
// File handling |
|
fileInput.addEventListener('change', (e) => { |
|
const file = e.target.files[0]; |
|
if (file) loadFile(file); |
|
}); |
|
|
|
// Drag and drop |
|
dropZone.addEventListener('dragover', (e) => { |
|
e.preventDefault(); |
|
dropZone.classList.add('dragover'); |
|
}); |
|
|
|
dropZone.addEventListener('dragleave', () => { |
|
dropZone.classList.remove('dragover'); |
|
}); |
|
|
|
dropZone.addEventListener('drop', (e) => { |
|
e.preventDefault(); |
|
dropZone.classList.remove('dragover'); |
|
const file = e.dataTransfer.files[0]; |
|
if (file) loadFile(file); |
|
}); |
|
|
|
function loadFile(file) { |
|
const reader = new FileReader(); |
|
reader.onload = (e) => { |
|
parseLog(e.target.result); |
|
}; |
|
reader.onerror = () => { |
|
showError('Failed to read file'); |
|
}; |
|
reader.readAsText(file); |
|
} |
|
|
|
function parseLog(content) { |
|
clearError(); |
|
allEntries = []; |
|
const lines = content.split('\n').filter(line => line.trim()); |
|
|
|
let parseErrors = 0; |
|
lines.forEach((line, index) => { |
|
try { |
|
const entry = JSON.parse(line); |
|
entry._index = index; |
|
allEntries.push(entry); |
|
} catch (e) { |
|
parseErrors++; |
|
} |
|
}); |
|
|
|
if (allEntries.length === 0) { |
|
showError('No valid JSON entries found in the file'); |
|
return; |
|
} |
|
|
|
if (parseErrors > 0) { |
|
showError(`Parsed ${allEntries.length} entries, skipped ${parseErrors} invalid lines`, 'warning'); |
|
} |
|
|
|
dropZone.style.display = 'none'; |
|
setupToolFilters(); |
|
applyFilters(); |
|
} |
|
|
|
function setupToolFilters() { |
|
const tools = new Set(); |
|
allEntries.forEach(entry => { |
|
if (entry.tool) tools.add(entry.tool); |
|
}); |
|
|
|
const savedTools = loadPreferences(); |
|
|
|
toolFilters.innerHTML = ''; |
|
Array.from(tools).sort().forEach(tool => { |
|
const label = document.createElement('label'); |
|
label.className = 'tool-checkbox'; |
|
const checked = savedTools ? savedTools.includes(tool) : true; |
|
label.innerHTML = ` |
|
<input type="checkbox" value="${tool}" ${checked ? 'checked' : ''}> |
|
${tool} |
|
`; |
|
toolFilters.appendChild(label); |
|
}); |
|
|
|
// Add event listeners |
|
toolFilters.querySelectorAll('input').forEach(cb => { |
|
cb.addEventListener('change', () => { |
|
savePreferences(); |
|
applyFilters(); |
|
}); |
|
}); |
|
} |
|
|
|
function showError(message, type = 'error') { |
|
errorContainer.innerHTML = `<div class="error">${message}</div>`; |
|
} |
|
|
|
function clearError() { |
|
errorContainer.innerHTML = ''; |
|
} |
|
|
|
// Parse date in yyyy-mm-dd HH:MM:SS format |
|
function parseFilterDate(str) { |
|
if (!str) return null; |
|
// Replace space with T for ISO format compatibility |
|
const isoStr = str.trim().replace(' ', 'T'); |
|
const date = new Date(isoStr); |
|
return isNaN(date.getTime()) ? null : date.getTime(); |
|
} |
|
|
|
// Filtering |
|
function applyFilters() { |
|
const searchTerm = searchInput.value.toLowerCase(); |
|
const selectedTools = Array.from(document.querySelectorAll('.tool-checkbox input:checked')) |
|
.map(cb => cb.value); |
|
const fromTime = parseFilterDate(fromDate.value); |
|
const toTime = parseFilterDate(toDate.value); |
|
|
|
filteredEntries = allEntries.filter(entry => { |
|
// Tool filter - if no tools selected, show nothing |
|
if (!selectedTools.includes(entry.tool)) { |
|
return false; |
|
} |
|
|
|
// Time filter |
|
if (entry.timestamp) { |
|
const entryTime = new Date(entry.timestamp).getTime(); |
|
if (fromTime && entryTime < fromTime) return false; |
|
if (toTime && entryTime > toTime) return false; |
|
} |
|
|
|
// Session filter |
|
if (activeSessionFilter && entry.session !== activeSessionFilter) { |
|
return false; |
|
} |
|
|
|
// Search filter |
|
if (searchTerm) { |
|
const inputStr = JSON.stringify(entry.input || '').toLowerCase(); |
|
const outputStr = JSON.stringify(entry.output || '').toLowerCase(); |
|
const toolStr = (entry.tool || '').toLowerCase(); |
|
if (!inputStr.includes(searchTerm) && |
|
!outputStr.includes(searchTerm) && |
|
!toolStr.includes(searchTerm)) { |
|
return false; |
|
} |
|
} |
|
|
|
return true; |
|
}); |
|
|
|
updateStats(); |
|
renderEntries(); |
|
} |
|
|
|
function updateStats() { |
|
const toolCounts = {}; |
|
filteredEntries.forEach(entry => { |
|
toolCounts[entry.tool] = (toolCounts[entry.tool] || 0) + 1; |
|
}); |
|
|
|
const toolSummary = Object.entries(toolCounts) |
|
.sort((a, b) => b[1] - a[1]) |
|
.map(([tool, count]) => `${tool}: ${count}`) |
|
.join(' | '); |
|
|
|
stats.textContent = `Showing ${filteredEntries.length} of ${allEntries.length} entries | ${toolSummary}`; |
|
} |
|
|
|
function renderEntries() { |
|
if (filteredEntries.length === 0) { |
|
entriesContainer.innerHTML = ` |
|
<div class="empty-state"> |
|
<h2>No matching entries</h2> |
|
<p>Try adjusting your filters</p> |
|
</div> |
|
`; |
|
return; |
|
} |
|
|
|
const searchTerm = searchInput.value.toLowerCase(); |
|
|
|
entriesContainer.innerHTML = filteredEntries.map(entry => { |
|
const toolClass = TOOL_COLORS[entry.tool] || ''; |
|
const isExpanded = expandedEntries.has(entry._index); |
|
const timestamp = entry.timestamp ? formatTimestamp(entry.timestamp) : 'No timestamp'; |
|
const projectPath = entry.project || ''; |
|
const projectName = projectPath ? projectPath.split('/').pop() || projectPath : 'Unknown project'; |
|
const sessionId = entry.session ? entry.session.substring(0, 8) + '...' : ''; |
|
|
|
const inputExpanded = !expandedSections.has(`${entry._index}-input`); |
|
const outputExpanded = !expandedSections.has(`${entry._index}-output`); |
|
|
|
return ` |
|
<div class="entry ${isExpanded ? 'expanded' : ''} tool-${toolClass}" data-index="${entry._index}"> |
|
<div class="entry-header" onclick="toggleEntry(${entry._index})"> |
|
<span class="tool-name">${entry.tool || 'Unknown'}</span> |
|
<span class="timestamp">${timestamp}</span> |
|
<span class="project-path" title="${projectPath}">${projectName}</span> |
|
<span class="session-id">${sessionId}</span> |
|
<span class="expand-icon">▼</span> |
|
</div> |
|
<div class="entry-body"> |
|
<div class="section ${inputExpanded ? '' : 'collapsed'}"> |
|
<div class="section-header" onclick="toggleSection(${entry._index}, 'input', event)"> |
|
<span class="section-icon">▼</span> Input |
|
</div> |
|
${renderContent(entry.input, entry._index, 'input', searchTerm)} |
|
</div> |
|
<div class="section ${outputExpanded ? '' : 'collapsed'}"> |
|
<div class="section-header" onclick="toggleSection(${entry._index}, 'output', event)"> |
|
<span class="section-icon">▼</span> Output |
|
</div> |
|
${renderContent(entry.output, entry._index, 'output', searchTerm)} |
|
</div> |
|
</div> |
|
</div> |
|
`; |
|
}).join(''); |
|
} |
|
|
|
function formatTimestamp(ts) { |
|
try { |
|
const date = new Date(ts); |
|
const year = date.getFullYear(); |
|
const month = String(date.getMonth() + 1).padStart(2, '0'); |
|
const day = String(date.getDate()).padStart(2, '0'); |
|
const hours = String(date.getHours()).padStart(2, '0'); |
|
const minutes = String(date.getMinutes()).padStart(2, '0'); |
|
const seconds = String(date.getSeconds()).padStart(2, '0'); |
|
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; |
|
} catch (e) { |
|
return ts; |
|
} |
|
} |
|
|
|
function formatJson(str, searchTerm) { |
|
if (!str || str === 'null') return '<span class="json-null">null</span>'; |
|
let highlighted = highlightJson(str); |
|
if (searchTerm) { |
|
highlighted = highlightSearch(highlighted, searchTerm); |
|
} |
|
return highlighted; |
|
} |
|
|
|
function highlightJson(str) { |
|
// Escape HTML first, then apply syntax highlighting |
|
const escaped = escapeHtml(str); |
|
|
|
// Match JSON tokens and wrap them in spans |
|
// Order matters: strings must be matched before other tokens |
|
return escaped.replace( |
|
/("(?:[^"\\]|\\.)*")(\s*:)?|(\b(?:true|false)\b)|(\bnull\b)|(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)|([{}\[\],:])/g, |
|
(match, str, colon, bool, nul, num, punct) => { |
|
if (str) { |
|
if (colon) { |
|
// It's a key (string followed by colon) |
|
return `<span class="json-key">${str}</span><span class="json-punctuation">${colon.trim()}</span>${colon.replace(':', '')}`; |
|
} |
|
// It's a string value |
|
return `<span class="json-string">${str}</span>`; |
|
} |
|
if (bool) return `<span class="json-boolean">${bool}</span>`; |
|
if (nul) return `<span class="json-null">${nul}</span>`; |
|
if (num) return `<span class="json-number">${num}</span>`; |
|
if (punct) return `<span class="json-punctuation">${punct}</span>`; |
|
return match; |
|
} |
|
); |
|
} |
|
|
|
function highlightSearch(text, term) { |
|
if (!term) return text; |
|
// Search highlighting works on already-highlighted HTML |
|
// We need to match text content while preserving tags |
|
const escapedTerm = escapeRegex(escapeHtml(term)); |
|
const regex = new RegExp(`(${escapedTerm})(?![^<]*>)`, 'gi'); |
|
return text.replace(regex, '<span class="highlight">$1</span>'); |
|
} |
|
|
|
function escapeHtml(text) { |
|
const div = document.createElement('div'); |
|
div.textContent = text; |
|
return div.innerHTML; |
|
} |
|
|
|
function escapeRegex(str) { |
|
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); |
|
} |
|
|
|
const TRUNCATE_LENGTH = 500; |
|
|
|
function renderContent(obj, index, type, searchTerm) { |
|
const key = `${index}-${type}`; |
|
const isFull = showFullContent.has(key); |
|
|
|
// Get raw JSON string first |
|
const rawJson = getRawJson(obj); |
|
const needsTruncation = rawJson.length > TRUNCATE_LENGTH; |
|
|
|
// Truncate raw string BEFORE highlighting to avoid breaking HTML tags |
|
let displayJson = rawJson; |
|
if (needsTruncation && !isFull) { |
|
displayJson = rawJson.substring(0, TRUNCATE_LENGTH); |
|
} |
|
|
|
// Apply highlighting after truncation |
|
const displayContent = formatJson(displayJson, searchTerm); |
|
|
|
return ` |
|
<div class="section-content">${displayContent}</div> |
|
${needsTruncation ? ` |
|
<button class="show-more-btn" onclick="toggleFullContent(${index}, '${type}', event)"> |
|
${isFull ? 'Show less' : `Show more (${rawJson.length} chars)`} |
|
</button> |
|
` : ''} |
|
`; |
|
} |
|
|
|
function getRawJson(obj) { |
|
if (obj === undefined || obj === null) return 'null'; |
|
try { |
|
return JSON.stringify(obj, null, 2); |
|
} catch (e) { |
|
return String(obj); |
|
} |
|
} |
|
|
|
// Event handlers |
|
window.toggleEntry = function(index) { |
|
if (expandedEntries.has(index)) { |
|
expandedEntries.delete(index); |
|
} else { |
|
expandedEntries.add(index); |
|
} |
|
renderEntries(); |
|
}; |
|
|
|
window.toggleSection = function(index, type, event) { |
|
event.stopPropagation(); |
|
const key = `${index}-${type}`; |
|
if (expandedSections.has(key)) { |
|
expandedSections.delete(key); |
|
} else { |
|
expandedSections.add(key); |
|
} |
|
renderEntries(); |
|
}; |
|
|
|
window.toggleFullContent = function(index, type, event) { |
|
event.stopPropagation(); |
|
const key = `${index}-${type}`; |
|
if (showFullContent.has(key)) { |
|
showFullContent.delete(key); |
|
} else { |
|
showFullContent.add(key); |
|
} |
|
renderEntries(); |
|
}; |
|
|
|
// Filter event listeners |
|
searchInput.addEventListener('input', debounce(() => { |
|
savePreferences(); |
|
applyFilters(); |
|
}, 300)); |
|
|
|
fromDate.addEventListener('input', () => { |
|
todayFilter.classList.remove('active'); |
|
savePreferences(); |
|
applyFilters(); |
|
}); |
|
|
|
toDate.addEventListener('input', () => { |
|
todayFilter.classList.remove('active'); |
|
savePreferences(); |
|
applyFilters(); |
|
}); |
|
|
|
clearFilters.addEventListener('click', () => { |
|
searchInput.value = ''; |
|
fromDate.value = ''; |
|
toDate.value = ''; |
|
activeSessionFilter = null; |
|
todayFilter.classList.remove('active'); |
|
latestSessionFilter.classList.remove('active'); |
|
document.querySelectorAll('.tool-checkbox input').forEach(cb => cb.checked = true); |
|
localStorage.removeItem('toolLogViewerPrefs'); |
|
applyFilters(); |
|
}); |
|
|
|
document.getElementById('selectAllTools').addEventListener('click', () => { |
|
document.querySelectorAll('.tool-checkbox input').forEach(cb => cb.checked = true); |
|
savePreferences(); |
|
applyFilters(); |
|
}); |
|
|
|
document.getElementById('selectNoTools').addEventListener('click', () => { |
|
document.querySelectorAll('.tool-checkbox input').forEach(cb => cb.checked = false); |
|
savePreferences(); |
|
applyFilters(); |
|
}); |
|
|
|
todayFilter.addEventListener('click', () => { |
|
const isActive = todayFilter.classList.contains('active'); |
|
if (isActive) { |
|
fromDate.value = ''; |
|
toDate.value = ''; |
|
todayFilter.classList.remove('active'); |
|
} else { |
|
const now = new Date(); |
|
const yyyy = now.getFullYear(); |
|
const mm = String(now.getMonth() + 1).padStart(2, '0'); |
|
const dd = String(now.getDate()).padStart(2, '0'); |
|
fromDate.value = `${yyyy}-${mm}-${dd} 00:00:00`; |
|
toDate.value = `${yyyy}-${mm}-${dd} 23:59:59`; |
|
todayFilter.classList.add('active'); |
|
} |
|
savePreferences(); |
|
applyFilters(); |
|
}); |
|
|
|
latestSessionFilter.addEventListener('click', () => { |
|
const isActive = latestSessionFilter.classList.contains('active'); |
|
if (isActive) { |
|
activeSessionFilter = null; |
|
latestSessionFilter.classList.remove('active'); |
|
} else { |
|
if (allEntries.length === 0) return; |
|
const lastEntry = allEntries[allEntries.length - 1]; |
|
activeSessionFilter = lastEntry.session || null; |
|
if (!activeSessionFilter) return; |
|
latestSessionFilter.classList.add('active'); |
|
} |
|
applyFilters(); |
|
}); |
|
|
|
function debounce(fn, delay) { |
|
let timeout; |
|
return function(...args) { |
|
clearTimeout(timeout); |
|
timeout = setTimeout(() => fn.apply(this, args), delay); |
|
}; |
|
} |
|
|
|
// Initialize |
|
loadPreferences(); |
|
</script> |
|
</body> |
|
</html> |