721 lines
27 KiB
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>
|