861 lines
29 KiB
HTML
861 lines
29 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Transcript Loader - Claude Code DevTools</title>
|
|
<style>
|
|
/* Modern developer-focused UI with dark theme */
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
:root {
|
|
--bg-primary: #1e1e1e;
|
|
--bg-secondary: #252526;
|
|
--bg-tertiary: #2d2d30;
|
|
--bg-hover: #37373d;
|
|
--border: #3e3e42;
|
|
--text-primary: #cccccc;
|
|
--text-secondary: #858585;
|
|
--accent: #007acc;
|
|
--accent-hover: #0098ff;
|
|
--success: #4ec9b0;
|
|
--warning: #ce9178;
|
|
--error: #f48771;
|
|
--code-bg: #1e1e1e;
|
|
}
|
|
|
|
body {
|
|
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
|
background-color: var(--bg-primary);
|
|
color: var(--text-primary);
|
|
line-height: 1.6;
|
|
}
|
|
|
|
header {
|
|
background-color: var(--bg-secondary);
|
|
padding: 2rem;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
header h1 {
|
|
color: var(--accent);
|
|
font-size: 1.8rem;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.tagline {
|
|
color: var(--text-secondary);
|
|
font-size: 0.95rem;
|
|
}
|
|
|
|
main {
|
|
max-width: 1400px;
|
|
margin: 0 auto;
|
|
padding: 2rem;
|
|
}
|
|
|
|
/* File Upload Interface */
|
|
.tool-interface {
|
|
background-color: var(--bg-secondary);
|
|
border: 2px dashed var(--border);
|
|
border-radius: 8px;
|
|
padding: 2rem;
|
|
margin-bottom: 2rem;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.tool-interface.drag-over {
|
|
border-color: var(--accent);
|
|
background-color: var(--bg-tertiary);
|
|
}
|
|
|
|
.upload-area {
|
|
text-align: center;
|
|
}
|
|
|
|
.upload-area h2 {
|
|
color: var(--text-primary);
|
|
font-size: 1.3rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.upload-area p {
|
|
color: var(--text-secondary);
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.file-input-wrapper {
|
|
position: relative;
|
|
display: inline-block;
|
|
}
|
|
|
|
input[type="file"] {
|
|
display: none;
|
|
}
|
|
|
|
.btn-upload {
|
|
background-color: var(--accent);
|
|
color: white;
|
|
padding: 0.75rem 1.5rem;
|
|
border: none;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-family: inherit;
|
|
font-size: 1rem;
|
|
transition: background-color 0.2s;
|
|
}
|
|
|
|
.btn-upload:hover {
|
|
background-color: var(--accent-hover);
|
|
}
|
|
|
|
/* Progress Bar */
|
|
.progress-container {
|
|
display: none;
|
|
margin-top: 1.5rem;
|
|
}
|
|
|
|
.progress-bar {
|
|
width: 100%;
|
|
height: 4px;
|
|
background-color: var(--bg-tertiary);
|
|
border-radius: 2px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.progress-fill {
|
|
height: 100%;
|
|
background-color: var(--accent);
|
|
transition: width 0.3s ease;
|
|
width: 0%;
|
|
}
|
|
|
|
.progress-text {
|
|
color: var(--text-secondary);
|
|
font-size: 0.85rem;
|
|
margin-top: 0.5rem;
|
|
text-align: center;
|
|
}
|
|
|
|
/* File Info */
|
|
.file-info {
|
|
display: none;
|
|
background-color: var(--bg-tertiary);
|
|
padding: 1rem;
|
|
border-radius: 4px;
|
|
margin-top: 1rem;
|
|
}
|
|
|
|
.file-info.show {
|
|
display: block;
|
|
}
|
|
|
|
.file-info-item {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
padding: 0.3rem 0;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.file-info-label {
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.file-info-value {
|
|
color: var(--success);
|
|
}
|
|
|
|
/* Results Section */
|
|
.results {
|
|
display: none;
|
|
}
|
|
|
|
.results.show {
|
|
display: block;
|
|
}
|
|
|
|
.results-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 1.5rem;
|
|
padding-bottom: 1rem;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
.results-header h2 {
|
|
color: var(--accent);
|
|
}
|
|
|
|
.stats {
|
|
display: flex;
|
|
gap: 2rem;
|
|
}
|
|
|
|
.stat-item {
|
|
text-align: right;
|
|
}
|
|
|
|
.stat-label {
|
|
color: var(--text-secondary);
|
|
font-size: 0.85rem;
|
|
}
|
|
|
|
.stat-value {
|
|
color: var(--success);
|
|
font-size: 1.3rem;
|
|
font-weight: bold;
|
|
}
|
|
|
|
/* Message List */
|
|
.message-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.message {
|
|
background-color: var(--bg-secondary);
|
|
border-left: 3px solid var(--border);
|
|
border-radius: 4px;
|
|
padding: 1rem;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.message:hover {
|
|
background-color: var(--bg-hover);
|
|
}
|
|
|
|
.message.role-user {
|
|
border-left-color: var(--accent);
|
|
}
|
|
|
|
.message.role-assistant {
|
|
border-left-color: var(--success);
|
|
}
|
|
|
|
.message-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 0.75rem;
|
|
padding-bottom: 0.5rem;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
.message-role {
|
|
font-weight: bold;
|
|
text-transform: uppercase;
|
|
font-size: 0.85rem;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.message.role-user .message-role {
|
|
color: var(--accent);
|
|
}
|
|
|
|
.message.role-assistant .message-role {
|
|
color: var(--success);
|
|
}
|
|
|
|
.message-timestamp {
|
|
color: var(--text-secondary);
|
|
font-size: 0.8rem;
|
|
}
|
|
|
|
.message-content {
|
|
color: var(--text-primary);
|
|
white-space: pre-wrap;
|
|
word-wrap: break-word;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.message-meta {
|
|
margin-top: 0.75rem;
|
|
padding-top: 0.75rem;
|
|
border-top: 1px solid var(--border);
|
|
display: flex;
|
|
gap: 1.5rem;
|
|
font-size: 0.8rem;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.meta-item {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.meta-label {
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.meta-value {
|
|
color: var(--warning);
|
|
font-family: monospace;
|
|
}
|
|
|
|
/* Code Blocks */
|
|
code {
|
|
background-color: var(--code-bg);
|
|
color: var(--warning);
|
|
padding: 0.2rem 0.4rem;
|
|
border-radius: 3px;
|
|
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
|
font-size: 0.9em;
|
|
}
|
|
|
|
pre {
|
|
background-color: var(--code-bg);
|
|
border: 1px solid var(--border);
|
|
border-radius: 4px;
|
|
padding: 1rem;
|
|
overflow-x: auto;
|
|
margin: 0.5rem 0;
|
|
}
|
|
|
|
pre code {
|
|
background: none;
|
|
padding: 0;
|
|
}
|
|
|
|
/* Error Messages */
|
|
.error-message {
|
|
background-color: var(--bg-secondary);
|
|
border-left: 3px solid var(--error);
|
|
color: var(--error);
|
|
padding: 1rem;
|
|
border-radius: 4px;
|
|
margin-bottom: 1rem;
|
|
display: none;
|
|
}
|
|
|
|
.error-message.show {
|
|
display: block;
|
|
}
|
|
|
|
/* Documentation Section */
|
|
.docs {
|
|
background-color: var(--bg-secondary);
|
|
border-radius: 8px;
|
|
padding: 2rem;
|
|
margin-top: 3rem;
|
|
}
|
|
|
|
.docs h2 {
|
|
color: var(--accent);
|
|
margin-bottom: 1.5rem;
|
|
font-size: 1.5rem;
|
|
}
|
|
|
|
.doc-content h3 {
|
|
color: var(--success);
|
|
margin-top: 1.5rem;
|
|
margin-bottom: 0.75rem;
|
|
font-size: 1.2rem;
|
|
}
|
|
|
|
.doc-content p {
|
|
margin-bottom: 1rem;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.doc-content ul, .doc-content ol {
|
|
margin-left: 2rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.doc-content li {
|
|
margin-bottom: 0.5rem;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.doc-content a {
|
|
color: var(--accent);
|
|
text-decoration: none;
|
|
}
|
|
|
|
.doc-content a:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.doc-content strong {
|
|
color: var(--success);
|
|
}
|
|
|
|
/* Footer */
|
|
footer {
|
|
text-align: center;
|
|
padding: 2rem;
|
|
color: var(--text-secondary);
|
|
font-size: 0.85rem;
|
|
border-top: 1px solid var(--border);
|
|
margin-top: 3rem;
|
|
}
|
|
|
|
footer a {
|
|
color: var(--accent);
|
|
text-decoration: none;
|
|
}
|
|
|
|
footer a:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
/* Utility Classes */
|
|
.hidden {
|
|
display: none !important;
|
|
}
|
|
|
|
/* Scrollbar Styling */
|
|
::-webkit-scrollbar {
|
|
width: 10px;
|
|
}
|
|
|
|
::-webkit-scrollbar-track {
|
|
background: var(--bg-primary);
|
|
}
|
|
|
|
::-webkit-scrollbar-thumb {
|
|
background: var(--bg-tertiary);
|
|
border-radius: 5px;
|
|
}
|
|
|
|
::-webkit-scrollbar-thumb:hover {
|
|
background: var(--border);
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<header>
|
|
<h1>Transcript Loader</h1>
|
|
<p class="tagline">Load and view Claude Code JSONL transcripts with syntax highlighting</p>
|
|
</header>
|
|
|
|
<main>
|
|
<!-- File Upload Interface -->
|
|
<section class="tool-interface" id="uploadArea">
|
|
<div class="upload-area">
|
|
<h2>Load Transcript File</h2>
|
|
<p>Drag and drop a .jsonl transcript file here, or click to browse</p>
|
|
|
|
<div class="file-input-wrapper">
|
|
<input type="file" id="fileInput" accept=".jsonl,.json">
|
|
<button class="btn-upload" onclick="document.getElementById('fileInput').click()">
|
|
Select File
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Progress Bar -->
|
|
<div class="progress-container" id="progressContainer">
|
|
<div class="progress-bar">
|
|
<div class="progress-fill" id="progressFill"></div>
|
|
</div>
|
|
<div class="progress-text" id="progressText">Loading...</div>
|
|
</div>
|
|
|
|
<!-- File Info -->
|
|
<div class="file-info" id="fileInfo">
|
|
<div class="file-info-item">
|
|
<span class="file-info-label">File Name:</span>
|
|
<span class="file-info-value" id="fileName"></span>
|
|
</div>
|
|
<div class="file-info-item">
|
|
<span class="file-info-label">File Size:</span>
|
|
<span class="file-info-value" id="fileSize"></span>
|
|
</div>
|
|
<div class="file-info-item">
|
|
<span class="file-info-label">Status:</span>
|
|
<span class="file-info-value" id="fileStatus">Ready</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Error Message -->
|
|
<div class="error-message" id="errorMessage"></div>
|
|
|
|
<!-- Results Section -->
|
|
<section class="results" id="results">
|
|
<div class="results-header">
|
|
<h2>Transcript Messages</h2>
|
|
<div class="stats">
|
|
<div class="stat-item">
|
|
<div class="stat-label">Total Messages</div>
|
|
<div class="stat-value" id="totalMessages">0</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-label">User Messages</div>
|
|
<div class="stat-value" id="userMessages">0</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-label">Assistant Messages</div>
|
|
<div class="stat-value" id="assistantMessages">0</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="message-list" id="messageList"></div>
|
|
</section>
|
|
|
|
<!-- Documentation -->
|
|
<section class="docs">
|
|
<h2>About This Tool</h2>
|
|
<div class="doc-content">
|
|
<h3>Purpose</h3>
|
|
<p>The Transcript Loader enables Claude Code developers to load, parse, and view conversation transcripts stored in JSONL format. It provides a clean, readable interface for inspecting message history, understanding conversation flow, and debugging AI interactions.</p>
|
|
|
|
<h3>Features</h3>
|
|
<ul>
|
|
<li><strong>Drag-and-Drop Loading:</strong> Simply drag your .jsonl transcript file onto the upload area</li>
|
|
<li><strong>Progress Tracking:</strong> Real-time progress bar for large file uploads</li>
|
|
<li><strong>JSONL Parsing:</strong> Line-by-line JSON parsing for efficient memory usage</li>
|
|
<li><strong>Message Display:</strong> Clean, color-coded display of user and assistant messages</li>
|
|
<li><strong>Metadata Visibility:</strong> Shows timestamps, session IDs, working directory, and git branch</li>
|
|
<li><strong>Error Handling:</strong> Graceful error messages for invalid files or parsing issues</li>
|
|
<li><strong>Statistics Dashboard:</strong> Quick overview of message counts by role</li>
|
|
<li><strong>Syntax Highlighting:</strong> Code-friendly monospace fonts and color schemes</li>
|
|
<li><strong>Dark Theme:</strong> Developer-friendly dark UI for long viewing sessions</li>
|
|
</ul>
|
|
|
|
<h3>Web Research Integration</h3>
|
|
<p><strong>Source:</strong> <a href="https://developer.mozilla.org/en-US/docs/Web/API/FileReader" target="_blank">MDN Web Docs - FileReader API</a></p>
|
|
<p><strong>Techniques Applied:</strong></p>
|
|
<ul>
|
|
<li><strong>readAsText() Method:</strong> Using FileReader.readAsText() to read the entire JSONL file as text, then parsing line-by-line for efficient memory usage</li>
|
|
<li><strong>onload Event Handling:</strong> Implementing the onload event handler to process file contents after reading completes, with validation before parsing</li>
|
|
<li><strong>Progress Tracking:</strong> Using the onprogress event to display a real-time progress bar (percentage loaded) for large transcript files</li>
|
|
<li><strong>Error Handling Pattern:</strong> Implementing onerror handler with user-friendly error messages and visual feedback</li>
|
|
<li><strong>File Validation:</strong> Checking file type and size before reading to prevent issues with invalid files</li>
|
|
</ul>
|
|
|
|
<h3>Usage</h3>
|
|
<ol>
|
|
<li><strong>Locate Your Transcript:</strong> Find a Claude Code transcript file (typically .jsonl format)</li>
|
|
<li><strong>Load the File:</strong> Either drag-and-drop the file onto the upload area, or click "Select File" to browse</li>
|
|
<li><strong>Monitor Progress:</strong> Watch the progress bar as the file loads (especially useful for large files)</li>
|
|
<li><strong>View Messages:</strong> Once loaded, scroll through the message list to inspect conversation history</li>
|
|
<li><strong>Examine Metadata:</strong> Check timestamps, session IDs, working directories, and other metadata for each message</li>
|
|
<li><strong>Review Statistics:</strong> See the total message count and breakdown by role at the top of the results</li>
|
|
</ol>
|
|
|
|
<h3>Technical Notes</h3>
|
|
<p>This tool parses JSONL (JSON Lines) format where each line is a separate JSON object. The parser handles:</p>
|
|
<ul>
|
|
<li>Line-by-line parsing to avoid loading entire file into memory at once</li>
|
|
<li>Malformed JSON lines (skips and continues)</li>
|
|
<li>Missing or optional fields with graceful defaults</li>
|
|
<li>Different content formats (string vs. array of content blocks)</li>
|
|
<li>Timestamp formatting and display</li>
|
|
</ul>
|
|
|
|
<h3>Browser Compatibility</h3>
|
|
<p>This tool requires modern browser support for FileReader API. Compatible with:</p>
|
|
<ul>
|
|
<li>Chrome/Edge 88+</li>
|
|
<li>Firefox 78+</li>
|
|
<li>Safari 14+</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/API/FileReader" target="_blank">MDN Web Docs - FileReader API</a></p>
|
|
</footer>
|
|
|
|
<script>
|
|
// Global state
|
|
let transcriptData = [];
|
|
|
|
// DOM Elements
|
|
const uploadArea = document.getElementById('uploadArea');
|
|
const fileInput = document.getElementById('fileInput');
|
|
const progressContainer = document.getElementById('progressContainer');
|
|
const progressFill = document.getElementById('progressFill');
|
|
const progressText = document.getElementById('progressText');
|
|
const fileInfo = document.getElementById('fileInfo');
|
|
const fileName = document.getElementById('fileName');
|
|
const fileSize = document.getElementById('fileSize');
|
|
const fileStatus = document.getElementById('fileStatus');
|
|
const errorMessage = document.getElementById('errorMessage');
|
|
const results = document.getElementById('results');
|
|
const messageList = document.getElementById('messageList');
|
|
const totalMessages = document.getElementById('totalMessages');
|
|
const userMessages = document.getElementById('userMessages');
|
|
const assistantMessages = document.getElementById('assistantMessages');
|
|
|
|
// File Input Event Listener
|
|
fileInput.addEventListener('change', (e) => {
|
|
const file = e.target.files[0];
|
|
if (file) {
|
|
loadTranscriptFile(file);
|
|
}
|
|
});
|
|
|
|
// Drag and Drop Event Listeners
|
|
uploadArea.addEventListener('dragover', (e) => {
|
|
e.preventDefault();
|
|
uploadArea.classList.add('drag-over');
|
|
});
|
|
|
|
uploadArea.addEventListener('dragleave', (e) => {
|
|
e.preventDefault();
|
|
uploadArea.classList.remove('drag-over');
|
|
});
|
|
|
|
uploadArea.addEventListener('drop', (e) => {
|
|
e.preventDefault();
|
|
uploadArea.classList.remove('drag-over');
|
|
|
|
const file = e.dataTransfer.files[0];
|
|
if (file) {
|
|
loadTranscriptFile(file);
|
|
}
|
|
});
|
|
|
|
// Main File Loading Function (Using FileReader API)
|
|
function loadTranscriptFile(file) {
|
|
// Reset state
|
|
transcriptData = [];
|
|
errorMessage.classList.remove('show');
|
|
results.classList.remove('show');
|
|
|
|
// Display file info
|
|
fileName.textContent = file.name;
|
|
fileSize.textContent = formatFileSize(file.size);
|
|
fileStatus.textContent = 'Loading...';
|
|
fileInfo.classList.add('show');
|
|
|
|
// Show progress container
|
|
progressContainer.style.display = 'block';
|
|
progressFill.style.width = '0%';
|
|
progressText.textContent = 'Loading file...';
|
|
|
|
// Create FileReader instance
|
|
const reader = new FileReader();
|
|
|
|
// TECHNIQUE 1: Progress Event Handler for Large Files
|
|
reader.onprogress = (event) => {
|
|
if (event.lengthComputable) {
|
|
const percentLoaded = Math.round((event.loaded / event.total) * 100);
|
|
progressFill.style.width = percentLoaded + '%';
|
|
progressText.textContent = `Loading: ${percentLoaded}%`;
|
|
}
|
|
};
|
|
|
|
// TECHNIQUE 2: onload Event Handler with File Validation
|
|
reader.onload = () => {
|
|
try {
|
|
progressText.textContent = 'Parsing transcript...';
|
|
|
|
// Validate file type (basic check)
|
|
if (!file.name.endsWith('.jsonl') && !file.name.endsWith('.json')) {
|
|
showError('Invalid file type. Please select a .jsonl or .json file.');
|
|
resetUploadUI();
|
|
return;
|
|
}
|
|
|
|
// Parse JSONL content
|
|
const content = reader.result;
|
|
parseJSONL(content);
|
|
|
|
// Update UI
|
|
fileStatus.textContent = 'Loaded Successfully';
|
|
progressFill.style.width = '100%';
|
|
progressText.textContent = 'Complete!';
|
|
|
|
// Display results
|
|
displayTranscript();
|
|
|
|
// Hide progress after a delay
|
|
setTimeout(() => {
|
|
progressContainer.style.display = 'none';
|
|
}, 1000);
|
|
|
|
} catch (error) {
|
|
showError(`Error parsing transcript: ${error.message}`);
|
|
resetUploadUI();
|
|
}
|
|
};
|
|
|
|
// TECHNIQUE 3: Error Handling Pattern
|
|
reader.onerror = () => {
|
|
showError('Error reading file. Please try again.');
|
|
resetUploadUI();
|
|
};
|
|
|
|
// TECHNIQUE 4: readAsText() Method for JSONL Files
|
|
reader.readAsText(file);
|
|
}
|
|
|
|
// Parse JSONL (line-by-line JSON)
|
|
function parseJSONL(content) {
|
|
const lines = content.trim().split('\n');
|
|
|
|
lines.forEach((line, index) => {
|
|
if (!line.trim()) return; // Skip empty lines
|
|
|
|
try {
|
|
const message = JSON.parse(line);
|
|
transcriptData.push(message);
|
|
} catch (error) {
|
|
console.warn(`Failed to parse line ${index + 1}:`, error);
|
|
// Continue parsing other lines
|
|
}
|
|
});
|
|
|
|
if (transcriptData.length === 0) {
|
|
throw new Error('No valid messages found in transcript');
|
|
}
|
|
}
|
|
|
|
// Display Transcript Messages
|
|
function displayTranscript() {
|
|
messageList.innerHTML = '';
|
|
|
|
// Calculate statistics
|
|
let userCount = 0;
|
|
let assistantCount = 0;
|
|
|
|
transcriptData.forEach((msg) => {
|
|
const role = msg.message?.role || msg.role || 'unknown';
|
|
if (role === 'user') userCount++;
|
|
if (role === 'assistant') assistantCount++;
|
|
});
|
|
|
|
// Update statistics
|
|
totalMessages.textContent = transcriptData.length;
|
|
userMessages.textContent = userCount;
|
|
assistantMessages.textContent = assistantCount;
|
|
|
|
// Render messages
|
|
transcriptData.forEach((msg, index) => {
|
|
const messageEl = createMessageElement(msg, index);
|
|
messageList.appendChild(messageEl);
|
|
});
|
|
|
|
// Show results section
|
|
results.classList.add('show');
|
|
}
|
|
|
|
// Create Message Element
|
|
function createMessageElement(msg, index) {
|
|
const messageDiv = document.createElement('div');
|
|
const role = msg.message?.role || msg.role || 'unknown';
|
|
messageDiv.className = `message role-${role}`;
|
|
|
|
// Message Header
|
|
const header = document.createElement('div');
|
|
header.className = 'message-header';
|
|
|
|
const roleSpan = document.createElement('span');
|
|
roleSpan.className = 'message-role';
|
|
roleSpan.textContent = role;
|
|
|
|
const timestamp = document.createElement('span');
|
|
timestamp.className = 'message-timestamp';
|
|
timestamp.textContent = formatTimestamp(msg.timestamp);
|
|
|
|
header.appendChild(roleSpan);
|
|
header.appendChild(timestamp);
|
|
|
|
// Message Content
|
|
const content = document.createElement('div');
|
|
content.className = 'message-content';
|
|
|
|
let contentText = '';
|
|
if (msg.message?.content) {
|
|
if (typeof msg.message.content === 'string') {
|
|
contentText = msg.message.content;
|
|
} else if (Array.isArray(msg.message.content)) {
|
|
// Handle content blocks
|
|
contentText = msg.message.content
|
|
.map(block => {
|
|
if (typeof block === 'string') return block;
|
|
if (block.text) return block.text;
|
|
if (block.type === 'tool_use') return `[Tool: ${block.name}]`;
|
|
return JSON.stringify(block, null, 2);
|
|
})
|
|
.join('\n\n');
|
|
}
|
|
}
|
|
|
|
content.textContent = contentText || '[No content]';
|
|
|
|
// Message Metadata
|
|
const meta = document.createElement('div');
|
|
meta.className = 'message-meta';
|
|
|
|
if (msg.sessionId) {
|
|
const sessionItem = document.createElement('div');
|
|
sessionItem.className = 'meta-item';
|
|
sessionItem.innerHTML = `<span class="meta-label">Session:</span><span class="meta-value">${msg.sessionId.substring(0, 8)}...</span>`;
|
|
meta.appendChild(sessionItem);
|
|
}
|
|
|
|
if (msg.cwd) {
|
|
const cwdItem = document.createElement('div');
|
|
cwdItem.className = 'meta-item';
|
|
cwdItem.innerHTML = `<span class="meta-label">CWD:</span><span class="meta-value">${msg.cwd}</span>`;
|
|
meta.appendChild(cwdItem);
|
|
}
|
|
|
|
if (msg.gitBranch) {
|
|
const branchItem = document.createElement('div');
|
|
branchItem.className = 'meta-item';
|
|
branchItem.innerHTML = `<span class="meta-label">Branch:</span><span class="meta-value">${msg.gitBranch}</span>`;
|
|
meta.appendChild(branchItem);
|
|
}
|
|
|
|
// Assemble message
|
|
messageDiv.appendChild(header);
|
|
messageDiv.appendChild(content);
|
|
if (meta.children.length > 0) {
|
|
messageDiv.appendChild(meta);
|
|
}
|
|
|
|
return messageDiv;
|
|
}
|
|
|
|
// Utility Functions
|
|
function formatFileSize(bytes) {
|
|
if (bytes === 0) return '0 Bytes';
|
|
const k = 1024;
|
|
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
|
}
|
|
|
|
function formatTimestamp(timestamp) {
|
|
if (!timestamp) return 'No timestamp';
|
|
const date = new Date(timestamp);
|
|
return date.toLocaleString();
|
|
}
|
|
|
|
function showError(message) {
|
|
errorMessage.textContent = message;
|
|
errorMessage.classList.add('show');
|
|
}
|
|
|
|
function resetUploadUI() {
|
|
progressContainer.style.display = 'none';
|
|
fileStatus.textContent = 'Error';
|
|
progressFill.style.width = '0%';
|
|
}
|
|
|
|
// Initialize
|
|
console.log('Claude Code Transcript Loader initialized');
|
|
console.log('FileReader API techniques applied:');
|
|
console.log('- readAsText() for JSONL parsing');
|
|
console.log('- onprogress for file loading progress');
|
|
console.log('- onload event handling with validation');
|
|
console.log('- onerror for graceful error handling');
|
|
</script>
|
|
</body>
|
|
</html>
|