infinite-agents-public/claude_code_devtools/claude_devtool_9.html

999 lines
37 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Web Worker Event Processor - Claude Code DevTools</title>
<style>
:root {
--bg-primary: #0d1117;
--bg-secondary: #161b22;
--bg-tertiary: #1c2128;
--bg-quaternary: #21262d;
--text-primary: #c9d1d9;
--text-secondary: #8b949e;
--text-tertiary: #6e7681;
--border-primary: #30363d;
--border-secondary: #21262d;
--accent-blue: #58a6ff;
--accent-green: #3fb950;
--accent-yellow: #d29922;
--accent-red: #f85149;
--accent-purple: #bc8cff;
--shadow: rgba(0, 0, 0, 0.3);
--font-mono: 'Fira Code', 'Cascadia Code', 'SF Mono', Consolas, monospace;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: var(--font-mono);
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
min-height: 100vh;
}
header {
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-primary);
padding: 1.5rem 2rem;
}
h1 {
font-size: 1.75rem;
color: var(--accent-blue);
margin-bottom: 0.5rem;
}
.tagline {
color: var(--text-secondary);
font-size: 0.95rem;
}
main {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
.section {
background: var(--bg-secondary);
border: 1px solid var(--border-primary);
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 1.5rem;
}
.section h2 {
color: var(--accent-green);
font-size: 1.25rem;
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.controls {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
margin-bottom: 1.5rem;
}
.control-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
label {
color: var(--text-secondary);
font-size: 0.9rem;
font-weight: 500;
}
input[type="file"], select, button {
background: var(--bg-tertiary);
border: 1px solid var(--border-primary);
color: var(--text-primary);
padding: 0.6rem 0.8rem;
border-radius: 6px;
font-family: var(--font-mono);
font-size: 0.9rem;
transition: all 0.2s;
}
button {
cursor: pointer;
background: var(--accent-blue);
color: #000;
font-weight: 600;
border: none;
}
button:hover:not(:disabled) {
background: #79c0ff;
transform: translateY(-1px);
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
button.secondary {
background: var(--bg-quaternary);
color: var(--text-primary);
border: 1px solid var(--border-primary);
}
button.secondary:hover:not(:disabled) {
background: var(--bg-tertiary);
}
.worker-status {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1rem;
background: var(--bg-tertiary);
border-radius: 6px;
margin-bottom: 1rem;
}
.status-indicator {
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--text-tertiary);
}
.status-indicator.idle {
background: var(--accent-blue);
}
.status-indicator.working {
background: var(--accent-yellow);
animation: pulse 1s infinite;
}
.status-indicator.complete {
background: var(--accent-green);
}
.status-indicator.error {
background: var(--accent-red);
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.progress-container {
background: var(--bg-tertiary);
border-radius: 6px;
padding: 1rem;
margin-bottom: 1rem;
}
.progress-bar-wrapper {
background: var(--bg-quaternary);
border-radius: 4px;
height: 24px;
overflow: hidden;
margin-bottom: 0.5rem;
}
.progress-bar {
height: 100%;
background: linear-gradient(90deg, var(--accent-blue), var(--accent-purple));
transition: width 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
font-weight: 600;
color: #000;
}
.progress-text {
font-size: 0.85rem;
color: var(--text-secondary);
}
.results-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1rem;
}
.result-card {
background: var(--bg-tertiary);
border: 1px solid var(--border-primary);
border-radius: 6px;
padding: 1rem;
}
.result-card h3 {
color: var(--accent-purple);
font-size: 1rem;
margin-bottom: 0.75rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.result-item {
padding: 0.5rem;
background: var(--bg-quaternary);
border-radius: 4px;
margin-bottom: 0.5rem;
font-size: 0.85rem;
}
.result-item:last-child {
margin-bottom: 0;
}
.metric {
display: flex;
justify-content: space-between;
align-items: center;
}
.metric-value {
color: var(--accent-green);
font-weight: 600;
}
.pattern-list {
list-style: none;
}
.pattern-item {
display: flex;
justify-content: space-between;
padding: 0.5rem;
background: var(--bg-quaternary);
border-radius: 4px;
margin-bottom: 0.4rem;
}
.pattern-sequence {
font-family: var(--font-mono);
color: var(--accent-blue);
font-size: 0.8rem;
}
.pattern-count {
color: var(--accent-yellow);
font-weight: 600;
}
.docs {
background: var(--bg-secondary);
border: 1px solid var(--border-primary);
border-radius: 8px;
padding: 1.5rem;
margin-top: 2rem;
}
.docs h2 {
color: var(--accent-green);
margin-bottom: 1rem;
}
.docs h3 {
color: var(--accent-blue);
margin-top: 1.5rem;
margin-bottom: 0.75rem;
font-size: 1.1rem;
}
.docs ul, .docs ol {
margin-left: 1.5rem;
margin-bottom: 1rem;
}
.docs li {
margin-bottom: 0.5rem;
color: var(--text-primary);
}
.docs code {
background: var(--bg-tertiary);
padding: 0.2rem 0.4rem;
border-radius: 3px;
color: var(--accent-purple);
font-size: 0.9em;
}
.docs a {
color: var(--accent-blue);
text-decoration: none;
}
.docs a:hover {
text-decoration: underline;
}
footer {
text-align: center;
padding: 2rem;
color: var(--text-tertiary);
font-size: 0.85rem;
border-top: 1px solid var(--border-primary);
margin-top: 3rem;
}
footer a {
color: var(--accent-blue);
text-decoration: none;
}
.empty-state {
text-align: center;
padding: 3rem;
color: var(--text-tertiary);
}
.sample-data {
background: var(--bg-tertiary);
border: 1px solid var(--border-primary);
border-radius: 6px;
padding: 1rem;
margin-top: 1rem;
}
.sample-data pre {
overflow-x: auto;
font-size: 0.8rem;
color: var(--text-secondary);
}
</style>
</head>
<body>
<header>
<h1>⚙️ Web Worker Event Processor</h1>
<p class="tagline">Background analysis of Claude Code hook events using Web Workers</p>
</header>
<main>
<section class="section">
<h2>📥 Load Hook Events</h2>
<div class="controls">
<div class="control-group">
<label for="eventFile">Select Events File (JSON/JSONL)</label>
<input type="file" id="eventFile" accept=".json,.jsonl">
</div>
<div class="control-group">
<label for="analysisType">Analysis Type</label>
<select id="analysisType">
<option value="patterns">Pattern Detection</option>
<option value="errors">Error Correlation</option>
<option value="sessions">Session Analysis</option>
<option value="agents">Agent Comparison</option>
<option value="anomalies">Anomaly Detection</option>
<option value="all">Complete Analysis (All)</option>
</select>
</div>
<div class="control-group">
<label>&nbsp;</label>
<button id="loadSampleBtn" class="secondary">📊 Load Sample Data</button>
</div>
<div class="control-group">
<label>&nbsp;</label>
<button id="analyzeBtn" disabled>🚀 Start Analysis</button>
</div>
</div>
</section>
<section class="section">
<h2>🔄 Worker Status</h2>
<div class="worker-status">
<div class="status-indicator" id="workerIndicator"></div>
<span id="workerStatus">No worker initialized</span>
</div>
<div class="progress-container" id="progressContainer" style="display: none;">
<div class="progress-bar-wrapper">
<div class="progress-bar" id="progressBar" style="width: 0%">0%</div>
</div>
<div class="progress-text" id="progressText">Initializing...</div>
</div>
</section>
<section class="section">
<h2>📊 Analysis Results</h2>
<div id="resultsContainer">
<div class="empty-state">
<p>Load event data and run analysis to see results</p>
</div>
</div>
</section>
<section class="docs">
<h2>About This Tool</h2>
<h3>Purpose</h3>
<p>Process large volumes of Claude Code hook events in the background using Web Workers. Analyze patterns, detect errors, compare agent behavior, and identify anomalies without blocking the UI.</p>
<h3>Features</h3>
<ul>
<li><strong>Background Processing</strong>: Uses Web Workers to analyze thousands of events without freezing the UI</li>
<li><strong>Pattern Detection</strong>: Identifies common tool usage sequences and workflows</li>
<li><strong>Error Correlation</strong>: Finds tools that frequently fail together</li>
<li><strong>Session Analysis</strong>: Calculates productivity metrics and session duration</li>
<li><strong>Agent Comparison</strong>: Compares behavior across multiple agents/sessions</li>
<li><strong>Anomaly Detection</strong>: Identifies unusual patterns and outliers</li>
<li><strong>Real-time Progress</strong>: Shows analysis progress with percentage updates</li>
<li><strong>Sample Data Included</strong>: Test with realistic hook event scenarios</li>
</ul>
<h3>Web Research Integration</h3>
<p><strong>Source:</strong> <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers" target="_blank">MDN Web Workers API Documentation</a></p>
<p><strong>Techniques Applied:</strong></p>
<ul>
<li><strong>Worker Communication via postMessage</strong>: Main thread sends event data and receives analysis results through structured message passing</li>
<li><strong>Background Processing Pattern</strong>: Web Worker performs heavy computation (pattern matching, statistics) without blocking the UI thread</li>
<li><strong>Worker Lifecycle Management</strong>: Proper worker initialization, termination, and error handling for robust processing</li>
</ul>
<h3>Usage</h3>
<ol>
<li>Click "Load Sample Data" to test with example hook events, or upload your own JSON/JSONL file</li>
<li>Select an analysis type (Pattern Detection, Error Correlation, etc.)</li>
<li>Click "Start Analysis" to spawn a Web Worker and begin background processing</li>
<li>Watch the progress bar update in real-time as the worker processes events</li>
<li>Review results displayed in organized cards when analysis completes</li>
<li>Try different analysis types to explore various insights</li>
</ol>
<h3>Hook Event Structure</h3>
<p>Events follow this structure:</p>
<div class="sample-data">
<pre>{
"source_app": "demo-agent",
"session_id": "abc123",
"hook_event_type": "PreToolUse|PostToolUse|UserPromptSubmit|Notification|Stop|SubagentStop",
"payload": {
"tool_name": "Bash",
"tool_input": {...},
"tool_output": {...}
},
"timestamp": 1696867200000
}</pre>
</div>
<h3>Analysis Types</h3>
<ul>
<li><strong>Pattern Detection</strong>: Finds common tool sequences (e.g., "Read → Edit → Bash")</li>
<li><strong>Error Correlation</strong>: Identifies tools that fail together frequently</li>
<li><strong>Session Analysis</strong>: Calculates duration, tool counts, and productivity metrics</li>
<li><strong>Agent Comparison</strong>: Compares tool usage across different source apps/sessions</li>
<li><strong>Anomaly Detection</strong>: Finds outliers in timing, error rates, or usage patterns</li>
<li><strong>Complete Analysis</strong>: Runs all analyses in parallel for comprehensive insights</li>
</ul>
</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/Web_Workers_API/Using_web_workers" target="_blank">MDN Web Workers API</a></p>
</footer>
<script>
// Global state
let eventData = [];
let worker = null;
// DOM elements
const eventFileInput = document.getElementById('eventFile');
const analysisTypeSelect = document.getElementById('analysisType');
const loadSampleBtn = document.getElementById('loadSampleBtn');
const analyzeBtn = document.getElementById('analyzeBtn');
const workerIndicator = document.getElementById('workerIndicator');
const workerStatus = document.getElementById('workerStatus');
const progressContainer = document.getElementById('progressContainer');
const progressBar = document.getElementById('progressBar');
const progressText = document.getElementById('progressText');
const resultsContainer = document.getElementById('resultsContainer');
// Sample data generator
function generateSampleData() {
const apps = ['web-app', 'api-server', 'cli-tool'];
const sessions = ['session-001', 'session-002', 'session-003'];
const tools = ['Bash', 'Read', 'Write', 'Edit', 'Grep', 'Glob', 'WebFetch'];
const eventTypes = ['PreToolUse', 'PostToolUse', 'UserPromptSubmit', 'Notification', 'Stop'];
const events = [];
const baseTime = Date.now() - 3600000; // 1 hour ago
for (let i = 0; i < 500; i++) {
const app = apps[Math.floor(Math.random() * apps.length)];
const session = sessions[Math.floor(Math.random() * sessions.length)];
const tool = tools[Math.floor(Math.random() * tools.length)];
const eventType = eventTypes[Math.floor(Math.random() * eventTypes.length)];
events.push({
source_app: app,
session_id: session,
hook_event_type: eventType,
payload: {
tool_name: tool,
tool_input: { command: `test-${i}` },
success: Math.random() > 0.1, // 10% error rate
duration_ms: Math.floor(Math.random() * 5000) + 100
},
timestamp: baseTime + (i * 7000) + Math.floor(Math.random() * 3000)
});
}
return events;
}
// Web Worker code as Blob (inline worker pattern)
function createWorkerCode() {
return `
// Worker message handler
self.onmessage = function(e) {
const { type, data, analysisType } = e.data;
if (type === 'analyze') {
try {
const results = performAnalysis(data, analysisType);
self.postMessage({ type: 'complete', results });
} catch (error) {
self.postMessage({ type: 'error', error: error.message });
}
}
};
function performAnalysis(events, analysisType) {
const results = {};
const total = events.length;
// Progress updates
function reportProgress(current, message) {
const percent = Math.round((current / total) * 100);
self.postMessage({
type: 'progress',
percent,
message
});
}
if (analysisType === 'patterns' || analysisType === 'all') {
reportProgress(total * 0.1, 'Detecting patterns...');
results.patterns = detectPatterns(events);
}
if (analysisType === 'errors' || analysisType === 'all') {
reportProgress(total * 0.3, 'Analyzing errors...');
results.errors = analyzeErrors(events);
}
if (analysisType === 'sessions' || analysisType === 'all') {
reportProgress(total * 0.5, 'Analyzing sessions...');
results.sessions = analyzeSessions(events);
}
if (analysisType === 'agents' || analysisType === 'all') {
reportProgress(total * 0.7, 'Comparing agents...');
results.agents = compareAgents(events);
}
if (analysisType === 'anomalies' || analysisType === 'all') {
reportProgress(total * 0.9, 'Detecting anomalies...');
results.anomalies = detectAnomalies(events);
}
reportProgress(total, 'Analysis complete');
return results;
}
function detectPatterns(events) {
const sequences = {};
const toolEvents = events.filter(e => e.payload?.tool_name);
for (let i = 0; i < toolEvents.length - 2; i++) {
const pattern = [
toolEvents[i].payload.tool_name,
toolEvents[i + 1].payload.tool_name,
toolEvents[i + 2].payload.tool_name
].join(' → ');
sequences[pattern] = (sequences[pattern] || 0) + 1;
}
return Object.entries(sequences)
.sort((a, b) => b[1] - a[1])
.slice(0, 10)
.map(([pattern, count]) => ({ pattern, count }));
}
function analyzeErrors(events) {
const errorPairs = {};
const failures = events.filter(e =>
e.payload?.success === false ||
e.hook_event_type === 'PostToolUse' && e.payload?.error
);
for (let i = 0; i < failures.length - 1; i++) {
const tool1 = failures[i].payload?.tool_name || 'unknown';
const tool2 = failures[i + 1].payload?.tool_name || 'unknown';
const pair = tool1 + ' → ' + tool2;
errorPairs[pair] = (errorPairs[pair] || 0) + 1;
}
return {
totalErrors: failures.length,
errorRate: ((failures.length / events.length) * 100).toFixed(1) + '%',
correlations: Object.entries(errorPairs)
.sort((a, b) => b[1] - a[1])
.slice(0, 5)
.map(([pair, count]) => ({ pair, count }))
};
}
function analyzeSessions(events) {
const sessions = {};
events.forEach(event => {
const sid = event.session_id;
if (!sessions[sid]) {
sessions[sid] = {
id: sid,
events: [],
tools: new Set(),
startTime: event.timestamp,
endTime: event.timestamp
};
}
sessions[sid].events.push(event);
sessions[sid].endTime = Math.max(sessions[sid].endTime, event.timestamp);
if (event.payload?.tool_name) {
sessions[sid].tools.add(event.payload.tool_name);
}
});
return Object.values(sessions).map(s => ({
id: s.id,
eventCount: s.events.length,
toolCount: s.tools.size,
duration: Math.round((s.endTime - s.startTime) / 1000) + 's',
durationMs: s.endTime - s.startTime
})).sort((a, b) => b.eventCount - a.eventCount);
}
function compareAgents(events) {
const agents = {};
events.forEach(event => {
const app = event.source_app;
if (!agents[app]) {
agents[app] = {
name: app,
eventCount: 0,
tools: {},
avgDuration: []
};
}
agents[app].eventCount++;
if (event.payload?.tool_name) {
const tool = event.payload.tool_name;
agents[app].tools[tool] = (agents[app].tools[tool] || 0) + 1;
}
if (event.payload?.duration_ms) {
agents[app].avgDuration.push(event.payload.duration_ms);
}
});
return Object.values(agents).map(a => {
const avgDur = a.avgDuration.length > 0
? Math.round(a.avgDuration.reduce((s, v) => s + v, 0) / a.avgDuration.length)
: 0;
const topTools = Object.entries(a.tools)
.sort((x, y) => y[1] - x[1])
.slice(0, 3)
.map(([name, count]) => name + '(' + count + ')')
.join(', ');
return {
name: a.name,
eventCount: a.eventCount,
topTools: topTools || 'N/A',
avgDuration: avgDur + 'ms'
};
}).sort((a, b) => b.eventCount - a.eventCount);
}
function detectAnomalies(events) {
const durations = events
.filter(e => e.payload?.duration_ms)
.map(e => e.payload.duration_ms);
if (durations.length === 0) {
return { outliers: [], stats: {} };
}
const avg = durations.reduce((s, v) => s + v, 0) / durations.length;
const variance = durations.reduce((s, v) => s + Math.pow(v - avg, 2), 0) / durations.length;
const stdDev = Math.sqrt(variance);
const outliers = events
.filter(e => e.payload?.duration_ms)
.filter(e => Math.abs(e.payload.duration_ms - avg) > 2 * stdDev)
.map(e => ({
tool: e.payload.tool_name || 'unknown',
duration: e.payload.duration_ms + 'ms',
deviation: ((e.payload.duration_ms - avg) / stdDev).toFixed(1) + 'σ'
}))
.slice(0, 5);
return {
outliers,
stats: {
mean: Math.round(avg) + 'ms',
stdDev: Math.round(stdDev) + 'ms',
total: durations.length
}
};
}
`;
}
// Initialize Web Worker
function initWorker() {
if (worker) {
worker.terminate();
}
const blob = new Blob([createWorkerCode()], { type: 'application/javascript' });
const workerUrl = URL.createObjectURL(blob);
worker = new Worker(workerUrl);
worker.onmessage = function(e) {
const { type, percent, message, results, error } = e.data;
if (type === 'progress') {
updateProgress(percent, message);
} else if (type === 'complete') {
displayResults(results);
updateWorkerStatus('complete', 'Analysis complete');
progressContainer.style.display = 'none';
analyzeBtn.disabled = false;
} else if (type === 'error') {
updateWorkerStatus('error', 'Error: ' + error);
progressContainer.style.display = 'none';
analyzeBtn.disabled = false;
}
};
worker.onerror = function(error) {
updateWorkerStatus('error', 'Worker error: ' + error.message);
progressContainer.style.display = 'none';
analyzeBtn.disabled = false;
};
updateWorkerStatus('idle', 'Worker ready');
}
// Update worker status indicator
function updateWorkerStatus(status, message) {
workerIndicator.className = 'status-indicator ' + status;
workerStatus.textContent = message;
}
// Update progress bar
function updateProgress(percent, message) {
progressBar.style.width = percent + '%';
progressBar.textContent = percent + '%';
progressText.textContent = message;
}
// Display analysis results
function displayResults(results) {
let html = '<div class="results-grid">';
if (results.patterns) {
html += `
<div class="result-card">
<h3>🔍 Common Patterns</h3>
<ul class="pattern-list">
${results.patterns.map(p => `
<li class="pattern-item">
<span class="pattern-sequence">${p.pattern}</span>
<span class="pattern-count">${p.count}x</span>
</li>
`).join('')}
</ul>
</div>
`;
}
if (results.errors) {
html += `
<div class="result-card">
<h3>❌ Error Analysis</h3>
<div class="result-item">
<div class="metric">
<span>Total Errors:</span>
<span class="metric-value">${results.errors.totalErrors}</span>
</div>
</div>
<div class="result-item">
<div class="metric">
<span>Error Rate:</span>
<span class="metric-value">${results.errors.errorRate}</span>
</div>
</div>
${results.errors.correlations.length > 0 ? `
<div style="margin-top: 0.75rem; font-size: 0.85rem; color: var(--text-secondary);">
Correlated Failures:
</div>
${results.errors.correlations.map(c => `
<div class="result-item">
<div class="metric">
<span style="font-size: 0.8rem;">${c.pair}</span>
<span class="metric-value">${c.count}x</span>
</div>
</div>
`).join('')}
` : ''}
</div>
`;
}
if (results.sessions) {
html += `
<div class="result-card">
<h3>📊 Session Metrics</h3>
${results.sessions.slice(0, 5).map(s => `
<div class="result-item">
<div style="font-size: 0.75rem; color: var(--text-tertiary); margin-bottom: 0.25rem;">
${s.id}
</div>
<div class="metric">
<span>Events: ${s.eventCount}</span>
<span>Tools: ${s.toolCount}</span>
<span class="metric-value">${s.duration}</span>
</div>
</div>
`).join('')}
</div>
`;
}
if (results.agents) {
html += `
<div class="result-card">
<h3>🤖 Agent Comparison</h3>
${results.agents.map(a => `
<div class="result-item">
<div style="font-weight: 600; margin-bottom: 0.25rem; color: var(--accent-blue);">
${a.name}
</div>
<div style="font-size: 0.8rem; color: var(--text-secondary);">
Events: ${a.eventCount} | Avg: ${a.avgDuration}
</div>
<div style="font-size: 0.75rem; color: var(--text-tertiary); margin-top: 0.25rem;">
${a.topTools}
</div>
</div>
`).join('')}
</div>
`;
}
if (results.anomalies) {
html += `
<div class="result-card">
<h3>⚠️ Anomalies Detected</h3>
${results.anomalies.stats ? `
<div class="result-item">
<div class="metric">
<span>Mean Duration:</span>
<span class="metric-value">${results.anomalies.stats.mean}</span>
</div>
</div>
<div class="result-item">
<div class="metric">
<span>Std Deviation:</span>
<span class="metric-value">${results.anomalies.stats.stdDev}</span>
</div>
</div>
` : ''}
${results.anomalies.outliers.length > 0 ? `
<div style="margin-top: 0.75rem; font-size: 0.85rem; color: var(--text-secondary);">
Outliers (>2σ):
</div>
${results.anomalies.outliers.map(o => `
<div class="result-item">
<div class="metric">
<span style="font-size: 0.8rem;">${o.tool}</span>
<span>${o.duration}</span>
<span class="metric-value">${o.deviation}</span>
</div>
</div>
`).join('')}
` : '<div class="result-item">No outliers detected</div>'}
</div>
`;
}
html += '</div>';
resultsContainer.innerHTML = html;
}
// File upload handler
eventFileInput.addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
try {
const text = await file.text();
if (file.name.endsWith('.jsonl')) {
eventData = text.trim().split('\n').map(line => JSON.parse(line));
} else {
eventData = JSON.parse(text);
if (!Array.isArray(eventData)) {
eventData = [eventData];
}
}
analyzeBtn.disabled = false;
updateWorkerStatus('idle', `Loaded ${eventData.length} events`);
} catch (error) {
alert('Error loading file: ' + error.message);
}
});
// Load sample data
loadSampleBtn.addEventListener('click', () => {
eventData = generateSampleData();
analyzeBtn.disabled = false;
updateWorkerStatus('idle', `Loaded ${eventData.length} sample events`);
});
// Start analysis
analyzeBtn.addEventListener('click', () => {
if (!worker) {
initWorker();
}
const analysisType = analysisTypeSelect.value;
analyzeBtn.disabled = true;
progressContainer.style.display = 'block';
updateProgress(0, 'Starting analysis...');
updateWorkerStatus('working', 'Processing events...');
// Send data to worker
worker.postMessage({
type: 'analyze',
data: eventData,
analysisType
});
});
// Initialize worker on load
if (window.Worker) {
initWorker();
} else {
updateWorkerStatus('error', 'Web Workers not supported');
analyzeBtn.disabled = true;
}
</script>
</body>
</html>