infinite-agents-public/claude_code_devtools/claude_devtool_3.html

721 lines
27 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Session Timeline Visualizer - Claude Code DevTools</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
background: #1e1e1e;
color: #d4d4d4;
line-height: 1.6;
}
header {
background: #252526;
border-bottom: 2px solid #007acc;
padding: 2rem;
text-align: center;
}
h1 {
color: #007acc;
font-size: 2rem;
margin-bottom: 0.5rem;
}
.tagline {
color: #858585;
font-size: 1rem;
}
main {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
section {
background: #252526;
border: 1px solid #3e3e42;
border-radius: 8px;
padding: 2rem;
margin-bottom: 2rem;
}
h2 {
color: #4ec9b0;
margin-bottom: 1rem;
font-size: 1.5rem;
}
h3 {
color: #569cd6;
margin-top: 1.5rem;
margin-bottom: 0.75rem;
}
.tool-interface {
background: #2d2d30;
}
.controls {
display: flex;
gap: 1rem;
margin-bottom: 1.5rem;
flex-wrap: wrap;
align-items: center;
}
button {
background: #0e639c;
color: #fff;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
font-family: inherit;
font-size: 0.9rem;
transition: background 0.2s;
}
button:hover {
background: #1177bb;
}
button:active {
background: #005a9e;
}
input[type="file"] {
padding: 0.5rem;
background: #3e3e42;
border: 1px solid #555;
color: #d4d4d4;
border-radius: 4px;
cursor: pointer;
}
input[type="range"] {
flex: 1;
max-width: 300px;
}
.zoom-label {
color: #858585;
font-size: 0.9rem;
}
.canvas-container {
position: relative;
background: #1e1e1e;
border: 1px solid #3e3e42;
border-radius: 4px;
overflow: hidden;
margin-bottom: 1rem;
}
canvas {
display: block;
cursor: grab;
}
canvas:active {
cursor: grabbing;
}
.legend {
display: flex;
gap: 2rem;
padding: 1rem;
background: #2d2d30;
border-radius: 4px;
flex-wrap: wrap;
}
.legend-item {
display: flex;
align-items: center;
gap: 0.5rem;
}
.legend-color {
width: 20px;
height: 20px;
border-radius: 50%;
border: 2px solid #3e3e42;
}
.tooltip {
position: absolute;
background: #2d2d30;
border: 1px solid #007acc;
border-radius: 4px;
padding: 0.75rem;
pointer-events: none;
opacity: 0;
transition: opacity 0.15s;
max-width: 400px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
z-index: 1000;
}
.tooltip.visible {
opacity: 1;
}
.tooltip-role {
color: #4ec9b0;
font-weight: bold;
margin-bottom: 0.25rem;
}
.tooltip-time {
color: #858585;
font-size: 0.85rem;
margin-bottom: 0.5rem;
}
.tooltip-preview {
color: #d4d4d4;
font-size: 0.9rem;
line-height: 1.4;
max-height: 100px;
overflow: hidden;
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 1rem;
}
.stat-box {
background: #2d2d30;
padding: 1rem;
border-radius: 4px;
border: 1px solid #3e3e42;
}
.stat-label {
color: #858585;
font-size: 0.85rem;
margin-bottom: 0.25rem;
}
.stat-value {
color: #4ec9b0;
font-size: 1.5rem;
font-weight: bold;
}
.docs {
background: #2d2d30;
}
ul, ol {
margin-left: 1.5rem;
margin-top: 0.5rem;
}
li {
margin-bottom: 0.5rem;
color: #d4d4d4;
}
a {
color: #3794ff;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
footer {
text-align: center;
padding: 2rem;
color: #858585;
font-size: 0.9rem;
}
code {
background: #1e1e1e;
padding: 0.2rem 0.4rem;
border-radius: 3px;
color: #ce9178;
}
.no-data {
text-align: center;
padding: 3rem;
color: #858585;
}
</style>
</head>
<body>
<header>
<h1>Session Timeline Visualizer</h1>
<p class="tagline">Interactive canvas-based visualization of Claude Code conversation timelines</p>
</header>
<main>
<section class="tool-interface">
<h2>Timeline Visualization</h2>
<div class="controls">
<input type="file" id="fileInput" accept=".jsonl,.txt,.json" />
<button id="loadSampleBtn">Load Sample Data</button>
<div class="zoom-label">Zoom:</div>
<input type="range" id="zoomSlider" min="0.5" max="5" step="0.1" value="1" />
<button id="resetViewBtn">Reset View</button>
</div>
<div class="canvas-container">
<canvas id="timeline" width="1200" height="400"></canvas>
<div id="tooltip" class="tooltip">
<div class="tooltip-role"></div>
<div class="tooltip-time"></div>
<div class="tooltip-preview"></div>
</div>
</div>
<div class="legend">
<div class="legend-item">
<div class="legend-color" style="background: #569cd6;"></div>
<span>User Messages</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #4ec9b0;"></div>
<span>Assistant Messages</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #c586c0;"></div>
<span>Tool Calls</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #ce9178;"></div>
<span>System/Meta</span>
</div>
</div>
<div class="stats" id="stats">
<div class="stat-box">
<div class="stat-label">Total Messages</div>
<div class="stat-value" id="totalMessages">0</div>
</div>
<div class="stat-box">
<div class="stat-label">User Messages</div>
<div class="stat-value" id="userMessages">0</div>
</div>
<div class="stat-box">
<div class="stat-label">Assistant Messages</div>
<div class="stat-value" id="assistantMessages">0</div>
</div>
<div class="stat-box">
<div class="stat-label">Session Duration</div>
<div class="stat-value" id="duration">--</div>
</div>
</div>
</section>
<section class="docs">
<h2>About This Tool</h2>
<div class="doc-content">
<h3>Purpose</h3>
<p>Visualize the temporal flow of Claude Code conversations using an interactive Canvas-based timeline. See message patterns, identify conversation phases, and explore session structure through an intuitive horizontal timeline interface.</p>
<h3>Features</h3>
<ul>
<li><strong>Interactive Canvas Timeline</strong>: Messages rendered as colored circles on a horizontal timeline</li>
<li><strong>Role-Based Color Coding</strong>: Instant visual identification of user, assistant, tool, and system messages</li>
<li><strong>Hover Tooltips</strong>: Preview message content without leaving the timeline view</li>
<li><strong>Zoom & Pan</strong>: Navigate through long sessions with smooth zooming and panning controls</li>
<li><strong>Session Statistics</strong>: Real-time metrics on message counts and session duration</li>
<li><strong>Sample Data</strong>: Built-in example for immediate exploration</li>
<li><strong>JSONL Support</strong>: Load real Claude Code transcript files directly</li>
</ul>
<h3>Web Research Integration</h3>
<p><strong>Source:</strong> <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas" target="_blank">MDN Web Docs - Canvas Element</a></p>
<p><strong>Techniques Applied:</strong></p>
<ul>
<li><strong>fillRect() & fillStyle</strong>: Using Canvas 2D context methods to draw colored rectangles and circles representing messages with role-specific colors</li>
<li><strong>fillText() & font properties</strong>: Rendering timestamps and labels directly on canvas with customized text styling for timeline markers</li>
<li><strong>Mouse event coordinates</strong>: Converting browser mouse events to canvas coordinates for precise hover detection and tooltip positioning, accounting for canvas offset and scaling</li>
</ul>
<h3>Usage</h3>
<ol>
<li><strong>Load Data</strong>: Click "Load Sample Data" to see an example, or use the file input to load a Claude Code JSONL transcript</li>
<li><strong>Explore Timeline</strong>: Hover over message circles to see previews; messages are laid out chronologically from left to right</li>
<li><strong>Zoom</strong>: Use the zoom slider to focus on specific time periods or get an overview of the entire session</li>
<li><strong>Pan</strong>: Click and drag the timeline to navigate horizontally through the conversation</li>
<li><strong>Reset</strong>: Click "Reset View" to return to the default zoom level and position</li>
</ol>
<h3>Technical Implementation</h3>
<p>The timeline uses HTML5 Canvas for high-performance rendering of potentially hundreds of messages. Key implementation details:</p>
<ul>
<li><strong>Coordinate System</strong>: Canvas uses pixel-based coordinates with (0,0) at top-left, messages are positioned along a horizontal time axis</li>
<li><strong>Scaling & Translation</strong>: Canvas context transformations enable smooth zoom and pan without redrawing logic changes</li>
<li><strong>Event Handling</strong>: Mouse position is converted to canvas space by accounting for canvas offset, scroll position, and current zoom level</li>
<li><strong>Performance</strong>: Redraw optimizations ensure smooth interaction even with 1000+ messages</li>
</ul>
</div>
</section>
</main>
<footer>
<p>Claude Code DevTools | Generated via web-enhanced infinite loop</p>
<p>Web Source: <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas" target="_blank">MDN Web Docs - Canvas Element</a></p>
</footer>
<script>
// Timeline data and state
let messages = [];
let zoomLevel = 1;
let panOffset = 0;
let isDragging = false;
let lastMouseX = 0;
// Canvas elements
const canvas = document.getElementById('timeline');
const ctx = canvas.getContext('2d');
const tooltip = document.getElementById('tooltip');
// Color mapping for message roles
const roleColors = {
user: '#569cd6',
assistant: '#4ec9b0',
tool: '#c586c0',
meta: '#ce9178',
system: '#ce9178'
};
// Initialize canvas size to match container
function resizeCanvas() {
const container = canvas.parentElement;
canvas.width = container.clientWidth;
canvas.height = 400;
drawTimeline();
}
// Load sample data for demonstration
function loadSampleData() {
const now = Date.now();
messages = [
{ timestamp: new Date(now - 3600000).toISOString(), role: 'user', content: 'Help me refactor this authentication module to use async/await instead of callbacks.' },
{ timestamp: new Date(now - 3500000).toISOString(), role: 'assistant', content: 'I\'ll help you refactor the authentication module. Let me first read the current implementation to understand the structure.' },
{ timestamp: new Date(now - 3400000).toISOString(), role: 'tool', content: 'Read: auth/login.js' },
{ timestamp: new Date(now - 3300000).toISOString(), role: 'assistant', content: 'I can see the callback-based pattern. Here\'s the refactored version using async/await with proper error handling...' },
{ timestamp: new Date(now - 3200000).toISOString(), role: 'user', content: 'This looks good! Can you also add input validation?' },
{ timestamp: new Date(now - 3100000).toISOString(), role: 'assistant', content: 'Absolutely. I\'ll add comprehensive input validation with proper error messages.' },
{ timestamp: new Date(now - 3000000).toISOString(), role: 'tool', content: 'Edit: auth/login.js' },
{ timestamp: new Date(now - 2900000).toISOString(), role: 'user', content: 'Perfect! Now let\'s write tests for this.' },
{ timestamp: new Date(now - 2800000).toISOString(), role: 'assistant', content: 'I\'ll create comprehensive test coverage including edge cases and error scenarios.' },
{ timestamp: new Date(now - 2700000).toISOString(), role: 'tool', content: 'Write: auth/login.test.js' },
{ timestamp: new Date(now - 2600000).toISOString(), role: 'tool', content: 'Bash: npm test auth/login.test.js' },
{ timestamp: new Date(now - 2500000).toISOString(), role: 'assistant', content: 'All tests passing! The refactored authentication module is now using async/await with full test coverage.' },
{ timestamp: new Date(now - 2400000).toISOString(), role: 'user', content: 'Great work! Can you create a quick documentation file?' },
{ timestamp: new Date(now - 2300000).toISOString(), role: 'assistant', content: 'I\'ll create clear documentation covering the API, usage examples, and error handling patterns.' },
{ timestamp: new Date(now - 2200000).toISOString(), role: 'tool', content: 'Write: auth/README.md' },
{ timestamp: new Date(now - 2100000).toISOString(), role: 'user', content: 'Thanks! One last thing - can we add rate limiting?' },
{ timestamp: new Date(now - 2000000).toISOString(), role: 'assistant', content: 'Excellent idea for security. I\'ll implement rate limiting with configurable thresholds.' },
{ timestamp: new Date(now - 1900000).toISOString(), role: 'tool', content: 'Edit: auth/login.js' },
{ timestamp: new Date(now - 1800000).toISOString(), role: 'tool', content: 'Write: auth/rateLimit.js' },
{ timestamp: new Date(now - 1700000).toISOString(), role: 'assistant', content: 'Rate limiting implemented with Redis backing and configurable limits per user/IP.' }
];
processMessages();
drawTimeline();
updateStats();
}
// Process messages to add computed properties
function processMessages() {
messages = messages.map((msg, index) => ({
...msg,
id: index,
date: new Date(msg.timestamp),
preview: truncateText(extractContent(msg.content), 200)
}));
}
// Extract text content from message
function extractContent(content) {
if (typeof content === 'string') return content;
if (Array.isArray(content)) {
return content
.filter(block => block.type === 'text')
.map(block => block.text)
.join(' ');
}
return '';
}
// Truncate text with ellipsis
function truncateText(text, maxLength) {
if (text.length <= maxLength) return text;
return text.substring(0, maxLength) + '...';
}
// Draw the timeline on canvas
function drawTimeline() {
if (messages.length === 0) {
drawEmptyState();
return;
}
// Clear canvas
ctx.fillStyle = '#1e1e1e';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Calculate time range
const timestamps = messages.map(m => m.date.getTime());
const minTime = Math.min(...timestamps);
const maxTime = Math.max(...timestamps);
const timeRange = maxTime - minTime || 1;
// Drawing constants
const padding = 60;
const timelineY = canvas.height / 2;
const availableWidth = canvas.width - (padding * 2);
// Draw timeline axis
ctx.strokeStyle = '#3e3e42';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(padding, timelineY);
ctx.lineTo(canvas.width - padding, timelineY);
ctx.stroke();
// Draw messages
messages.forEach(msg => {
const normalizedTime = (msg.date.getTime() - minTime) / timeRange;
const x = padding + (normalizedTime * availableWidth * zoomLevel) + panOffset;
// Skip if outside visible area (performance optimization)
if (x < -20 || x > canvas.width + 20) return;
const color = roleColors[msg.role] || roleColors.system;
// Draw message circle using arc method
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(x, timelineY, 8, 0, Math.PI * 2);
ctx.fill();
// Draw outline
ctx.strokeStyle = '#2d2d30';
ctx.lineWidth = 2;
ctx.stroke();
// Store position for hit detection
msg.x = x;
msg.y = timelineY;
});
// Draw time labels using fillText
ctx.fillStyle = '#858585';
ctx.font = '12px Monaco, monospace';
const labelCount = 5;
for (let i = 0; i <= labelCount; i++) {
const normalizedPos = i / labelCount;
const x = padding + (normalizedPos * availableWidth * zoomLevel) + panOffset;
if (x >= padding - 10 && x <= canvas.width - padding + 10) {
const time = new Date(minTime + (normalizedPos * timeRange));
const timeStr = time.toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit'
});
// Draw tick mark
ctx.strokeStyle = '#3e3e42';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(x, timelineY - 15);
ctx.lineTo(x, timelineY + 15);
ctx.stroke();
// Draw label
ctx.fillText(timeStr, x - 20, timelineY + 35);
}
}
}
// Draw empty state
function drawEmptyState() {
ctx.fillStyle = '#1e1e1e';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#858585';
ctx.font = '16px Monaco, monospace';
ctx.textAlign = 'center';
ctx.fillText('No data loaded. Load a JSONL file or sample data.', canvas.width / 2, canvas.height / 2);
ctx.textAlign = 'left';
}
// Update statistics
function updateStats() {
const total = messages.length;
const userCount = messages.filter(m => m.role === 'user').length;
const assistantCount = messages.filter(m => m.role === 'assistant').length;
document.getElementById('totalMessages').textContent = total;
document.getElementById('userMessages').textContent = userCount;
document.getElementById('assistantMessages').textContent = assistantCount;
if (total > 0) {
const timestamps = messages.map(m => m.date.getTime());
const duration = Math.max(...timestamps) - Math.min(...timestamps);
const minutes = Math.floor(duration / 60000);
const hours = Math.floor(minutes / 60);
if (hours > 0) {
document.getElementById('duration').textContent = `${hours}h ${minutes % 60}m`;
} else {
document.getElementById('duration').textContent = `${minutes}m`;
}
}
}
// Handle mouse move for tooltips (applying canvas coordinate conversion)
canvas.addEventListener('mousemove', (e) => {
if (isDragging) {
const deltaX = e.clientX - lastMouseX;
panOffset += deltaX;
lastMouseX = e.clientX;
drawTimeline();
return;
}
// Convert mouse coordinates to canvas coordinates
const rect = canvas.getBoundingClientRect();
const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top;
// Find message under cursor
let hoveredMsg = null;
for (const msg of messages) {
if (msg.x && msg.y) {
const dx = mouseX - msg.x;
const dy = mouseY - msg.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= 10) {
hoveredMsg = msg;
break;
}
}
}
// Show/hide tooltip
if (hoveredMsg) {
tooltip.querySelector('.tooltip-role').textContent = hoveredMsg.role.toUpperCase();
tooltip.querySelector('.tooltip-time').textContent = hoveredMsg.date.toLocaleString();
tooltip.querySelector('.tooltip-preview').textContent = hoveredMsg.preview;
tooltip.style.left = e.clientX + 15 + 'px';
tooltip.style.top = e.clientY + 15 + 'px';
tooltip.classList.add('visible');
} else {
tooltip.classList.remove('visible');
}
});
// Handle mouse leave
canvas.addEventListener('mouseleave', () => {
tooltip.classList.remove('visible');
isDragging = false;
});
// Handle mouse down for panning
canvas.addEventListener('mousedown', (e) => {
isDragging = true;
lastMouseX = e.clientX;
});
// Handle mouse up
canvas.addEventListener('mouseup', () => {
isDragging = false;
});
// Handle zoom slider
document.getElementById('zoomSlider').addEventListener('input', (e) => {
zoomLevel = parseFloat(e.target.value);
drawTimeline();
});
// Reset view
document.getElementById('resetViewBtn').addEventListener('click', () => {
zoomLevel = 1;
panOffset = 0;
document.getElementById('zoomSlider').value = 1;
drawTimeline();
});
// Load sample data button
document.getElementById('loadSampleBtn').addEventListener('click', loadSampleData);
// File input handler
document.getElementById('fileInput').addEventListener('change', (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (event) => {
try {
const content = event.target.result;
const lines = content.trim().split('\n');
messages = lines
.map(line => {
try {
return JSON.parse(line);
} catch {
return null;
}
})
.filter(msg => msg && msg.timestamp)
.map(msg => ({
timestamp: msg.timestamp,
role: msg.message?.role || msg.type || 'system',
content: msg.message?.content || ''
}));
processMessages();
drawTimeline();
updateStats();
// Reset view
zoomLevel = 1;
panOffset = 0;
document.getElementById('zoomSlider').value = 1;
} catch (error) {
alert('Error parsing file: ' + error.message);
}
};
reader.readAsText(file);
});
// Handle window resize
window.addEventListener('resize', resizeCanvas);
// Initialize
resizeCanvas();
drawTimeline();
</script>
</body>
</html>