Files
minecraft-chat-viewer/local-chat-analyzer.js
SimolZimol 8ac625a64d new file: .gitignore
new file:   Dockerfile
	new file:   README.md
	new file:   app.py
	new file:   chat-logs/chat-index.json
	new file:   chat-logs/crea-1-10.08.2020-merged.txt
	new file:   chat-logs/crea-1-11.08.2020-merged.txt
	new file:   chat-logs/crea-1-12.08.2020-merged.txt
	new file:   chat-logs/crea-1-13.08.2020-merged.txt
	new file:   chat-logs/crea-1-14.08.2020-merged.txt
	new file:   chat-logs/crea-1-15.08.2020-merged.txt
	new file:   chat-logs/crea-1-18.08.2020-merged.txt
	new file:   chat-logs/crea-1-20.08.2020-merged.txt
	new file:   chat-logs/crea-1-2020-07-27-1-filtered.txt
	new file:   chat-logs/crea-1-2020-07-28-1-filtered.txt
	new file:   chat-logs/crea-1-2020-07-29-1-filtered.txt
	new file:   chat-logs/crea-1-2020-07-30-1-filtered.txt
	new file:   chat-logs/crea-1-2020-08-03-1-filtered.txt
	new file:   chat-logs/crea-1-2020-08-04-1-filtered.txt
	new file:   chat-logs/crea-1-2020-08-08-1-filtered.txt
	new file:   chat-logs/crea-1-2020-08-09-1-filtered.txt
	new file:   chat-logs/crea-1-2020-08-10-1-filtered.txt
	new file:   chat-logs/crea-1-2020-08-11-1-filtered.txt
	new file:   chat-logs/crea-1-2020-08-13-1-filtered.txt
	new file:   chat-logs/crea-1-2020-08-16-1-filtered.txt
	new file:   chat-logs/crea-1-2020-08-17-1-filtered.txt
	new file:   chat-logs/crea-1-2020-08-18-1-filtered.txt
	new file:   chat-logs/crea-1-2020-08-20-1-filtered.txt
	new file:   chat-logs/crea-1-2020-08-24-1-filtered.txt
	new file:   chat-logs/crea-1-2020-08-29-1-filtered.txt
	new file:   chat-logs/crea-1-2020-08-30-1-filtered.txt
	new file:   chat-logs/crea-1-21.08.2020-merged.txt
	new file:   chat-logs/crea-1-22.08.2020-merged.txt
	new file:   chat-logs/crea-1-23.08.2020-merged.txt
	new file:   chat-logs/crea-1-24.07.2020-merged.txt
	new file:   chat-logs/crea-1-25.07.2020-merged.txt
	new file:   chat-logs/crea-1-25.08.2020-merged.txt
	new file:   chat-logs/crea-1-26.07.2020-merged.txt
	new file:   chat-logs/crea-1-26.08.2020-merged.txt
	new file:   chat-logs/crea-1-27.08.2020-merged.txt
	new file:   chat-logs/crea-1-28.08.2020-merged.txt
	new file:   chat-logs/crea-1-crea-1-10.08.2020-merged-filtered.txt
	new file:   chat-logs/crea-1-crea-1-11.08.2020-merged-filtered.txt
	new file:   chat-logs/crea-1-crea-1-12.08.2020-merged-filtered.txt
	new file:   chat-logs/crea-1-crea-1-14.08.2020-merged-filtered.txt
	new file:   chat-logs/crea-1-crea-1-15.08.2020-merged-filtered.txt
	new file:   chat-logs/crea-1-crea-1-18.08.2020-merged-filtered.txt
	new file:   chat-logs/crea-1-crea-1-20.08.2020-merged-filtered.txt
	new file:   chat-logs/crea-1-crea-1-21.08.2020-merged-filtered.txt
	new file:   chat-logs/crea-1-crea-1-22.08.2020-merged-filtered.txt
	new file:   chat-logs/crea-1-crea-1-23.08.2020-merged-filtered.txt
	new file:   chat-logs/crea-1-crea-1-24.07.2020-merged-filtered.txt
	new file:   chat-logs/crea-1-crea-1-25.07.2020-merged-filtered.txt
	new file:   chat-logs/crea-1-crea-1-25.08.2020-merged-filtered.txt
	new file:   chat-logs/crea-1-crea-1-26.07.2020-merged-filtered.txt
	new file:   chat-logs/crea-1-crea-1-26.08.2020-merged-filtered.txt
	new file:   chat-logs/crea-1-crea-1-27.08.2020-merged-filtered.txt
	new file:   chat-logs/crea-1-crea-1-28.08.2020-merged-filtered.txt
	new file:   chat-logs/survival-1-15.08.2020-merged.txt
	new file:   chat-logs/survival-1-2020-07-27-1-filtered.txt
	new file:   chat-logs/survival-1-2020-07-28-1-filtered.txt
	new file:   chat-logs/survival-1-2020-08-07-1-filtered.txt
	new file:   chat-logs/survival-1-2020-08-08-1-filtered.txt
	new file:   chat-logs/survival-1-2020-08-11-1-filtered.txt
	new file:   chat-logs/survival-1-2020-08-13-1-filtered.txt
	new file:   chat-logs/survival-1-2020-08-14-1-filtered.txt
	new file:   chat-logs/survival-1-2020-08-17-1-filtered.txt
	new file:   chat-logs/survival-1-2020-08-18-1-filtered.txt
	new file:   chat-logs/survival-1-2020-08-19-1-filtered.txt
	new file:   chat-logs/survival-1-25.07.2020-merged.txt
	new file:   chat-logs/survival-1-survival-1-15.08.2020-merged-filtered.txt
	new file:   chat-logs/survival-1-survival-1-25.07.2020-merged-filtered.txt
	new file:   chat-logs/thesur-1-2020-08-17-1-filtered.txt
	new file:   chat-logs/thesur-1-2020-08-31-1-filtered.txt
	new file:   count_all_sessions.py
	new file:   count_sessions.py
	new file:   index.html
	new file:   local-chat-analyzer.js
	new file:   merge_daily_logs.py
	new file:   process_thesur_logs.py
	new file:   quick_add.py
	new file:   requirements.txt
	new file:   script.js
	new file:   server.py
	new file:   statistics-integration.js
	new file:   statistics.css
	new file:   statistics.js
	new file:   style.css
2025-12-09 15:31:20 +01:00

630 lines
26 KiB
JavaScript
Raw Permalink 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.
/* Enhanced Local Chat Statistics */
class LocalChatAnalyzer {
constructor() {
this.currentChatData = null;
this.isAnalyzing = false;
}
async analyzeCurrentChat() {
if (this.isAnalyzing) return;
this.isAnalyzing = true;
try {
console.log('Starting local chat analysis...');
// Try multiple methods to get chat messages
const messages = this.extractChatMessages();
if (messages.length === 0) {
console.log('No messages found for analysis');
this.showNoDataMessage();
return;
}
console.log(`Found ${messages.length} messages for analysis`);
// Analyze the messages
const analysis = this.performDetailedAnalysis(messages);
// Store the results
this.currentChatData = analysis;
// Show the statistics
this.displayLocalStatistics(analysis);
} catch (error) {
console.error('Error analyzing chat:', error);
this.showErrorMessage(error.message);
} finally {
this.isAnalyzing = false;
}
}
extractChatMessages() {
const messages = [];
// Method 1: Try to get from existing chat viewer
if (window.chatViewer && window.chatViewer.currentMessages) {
console.log('Using chatViewer.currentMessages');
return window.chatViewer.currentMessages;
}
// Method 2: Parse from DOM elements
const messageElements = document.querySelectorAll('.message, .chat-message, [data-message]');
console.log(`Found ${messageElements.length} message elements in DOM`);
messageElements.forEach((element, index) => {
const parsedMessage = this.parseMessageElement(element, index);
if (parsedMessage) {
messages.push(parsedMessage);
}
});
// Method 3: Fallback - parse from chat content text
if (messages.length === 0) {
const chatContent = document.getElementById('chatContent');
if (chatContent) {
const textMessages = this.parseFromText(chatContent.textContent);
messages.push(...textMessages);
}
}
return messages;
}
parseMessageElement(element, index) {
const text = element.textContent || element.innerText;
if (!text.trim()) return null;
// Extract components
const timestamp = element.querySelector('.timestamp')?.textContent ||
text.match(/\[(\d{1,2}:\d{2}:\d{2})\]/)?.[1] || '';
const roleElement = element.querySelector('.role-badge');
const role = roleElement ? roleElement.textContent.toLowerCase() : 'member';
const playerElement = element.querySelector('.player-name');
const messageElement = element.querySelector('.message-text');
if (playerElement && messageElement) {
return {
type: 'chat',
timestamp: timestamp.replace(/[\[\]]/g, ''),
role: role,
player: playerElement.textContent.trim(),
message: messageElement.textContent.trim(),
index: index
};
}
// Try to parse join/leave messages
if (element.classList.contains('join-leave') || text.includes('joined') || text.includes('left')) {
const playerMatch = text.match(/(\w+)\s+(joined|left|connected|disconnected)/i);
if (playerMatch) {
return {
type: playerMatch[2].toLowerCase().includes('join') || playerMatch[2].toLowerCase().includes('connect') ? 'join' : 'leave',
timestamp: timestamp.replace(/[\[\]]/g, ''),
player: playerMatch[1],
message: text.trim(),
index: index
};
}
}
// Generic message parsing
const messageMatch = text.match(/\[(\d{1,2}:\d{2}:\d{2})\]\s*<([^>]+)>\s*(.+)/);
if (messageMatch) {
return {
type: 'chat',
timestamp: messageMatch[1],
player: messageMatch[2].trim(),
message: messageMatch[3].trim(),
role: this.detectRole(text),
index: index
};
}
return null;
}
parseFromText(text) {
const messages = [];
const lines = text.split('\n').filter(line => line.trim());
lines.forEach((line, index) => {
// Chat message patterns
const patterns = [
/\[(\d{1,2}:\d{2}:\d{2})\]\s*<([^>]+)>\s*(.+)/,
/\[(\d{1,2}:\d{2}:\d{2})\]\s*([^:]+):\s*(.+)/,
/(\d{1,2}:\d{2}:\d{2})\s*<([^>]+)>\s*(.+)/
];
for (const pattern of patterns) {
const match = line.match(pattern);
if (match) {
messages.push({
type: 'chat',
timestamp: match[1],
player: match[2].trim(),
message: match[3].trim(),
role: this.detectRole(line),
index: index
});
break;
}
}
// Join/leave patterns
const joinLeaveMatch = line.match(/\[(\d{1,2}:\d{2}:\d{2})\]\s*([^\s]+)\s+(joined|left|connected|disconnected)/i);
if (joinLeaveMatch) {
messages.push({
type: joinLeaveMatch[3].toLowerCase().includes('join') || joinLeaveMatch[3].toLowerCase().includes('connect') ? 'join' : 'leave',
timestamp: joinLeaveMatch[1],
player: joinLeaveMatch[2],
message: line.trim(),
index: index
});
}
});
return messages;
}
detectRole(text) {
const rolePatterns = {
'admin': /admin|administrator/i,
'moderator': /mod|moderator/i,
'm-builder': /m-builder|master.builder/i,
'praetorian': /praetorian/i,
'builder': /builder/i,
'member': /member/i
};
for (const [role, pattern] of Object.entries(rolePatterns)) {
if (pattern.test(text)) {
return role;
}
}
return 'member';
}
performDetailedAnalysis(messages) {
const analysis = {
totalMessages: messages.length,
chatMessages: 0,
systemMessages: 0,
joinMessages: 0,
leaveMessages: 0,
players: new Map(),
timeDistribution: new Map(),
wordFrequency: new Map(),
roleDistribution: new Map(),
messageLengths: [],
sessionInfo: {
startTime: null,
endTime: null,
duration: null
},
topWords: [],
topPlayers: [],
conversationFlow: []
};
// Process each message
messages.forEach((message, index) => {
// Session timing
if (message.timestamp) {
if (!analysis.sessionInfo.startTime || message.timestamp < analysis.sessionInfo.startTime) {
analysis.sessionInfo.startTime = message.timestamp;
}
if (!analysis.sessionInfo.endTime || message.timestamp > analysis.sessionInfo.endTime) {
analysis.sessionInfo.endTime = message.timestamp;
}
}
// Message type counting
switch (message.type) {
case 'chat':
analysis.chatMessages++;
this.processChatMessage(message, analysis);
break;
case 'join':
analysis.joinMessages++;
break;
case 'leave':
analysis.leaveMessages++;
break;
default:
analysis.systemMessages++;
}
// Time distribution (hourly)
if (message.timestamp) {
const hour = message.timestamp.split(':')[0];
analysis.timeDistribution.set(hour, (analysis.timeDistribution.get(hour) || 0) + 1);
}
});
// Calculate session duration
if (analysis.sessionInfo.startTime && analysis.sessionInfo.endTime) {
const start = this.parseTime(analysis.sessionInfo.startTime);
const end = this.parseTime(analysis.sessionInfo.endTime);
if (start && end) {
analysis.sessionInfo.duration = Math.max(0, (end - start) / 1000 / 60); // minutes
}
}
// Generate top lists
analysis.topWords = Array.from(analysis.wordFrequency.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, 20);
analysis.topPlayers = Array.from(analysis.players.values())
.sort((a, b) => b.messageCount - a.messageCount)
.slice(0, 10);
return analysis;
}
processChatMessage(message, analysis) {
const player = message.player;
// Player statistics
if (!analysis.players.has(player)) {
analysis.players.set(player, {
name: player,
role: message.role,
messageCount: 0,
totalChars: 0,
averageLength: 0,
firstMessage: message.timestamp,
lastMessage: message.timestamp,
wordCount: 0
});
}
const playerStats = analysis.players.get(player);
playerStats.messageCount++;
playerStats.totalChars += message.message.length;
playerStats.lastMessage = message.timestamp;
playerStats.averageLength = Math.round(playerStats.totalChars / playerStats.messageCount);
// Role distribution
analysis.roleDistribution.set(message.role, (analysis.roleDistribution.get(message.role) || 0) + 1);
// Message length
analysis.messageLengths.push(message.message.length);
// Word frequency analysis
const words = message.message.toLowerCase()
.replace(/[^\w\s]/g, ' ')
.split(/\s+/)
.filter(word => word.length > 2);
words.forEach(word => {
analysis.wordFrequency.set(word, (analysis.wordFrequency.get(word) || 0) + 1);
playerStats.wordCount++;
});
}
parseTime(timeString) {
const match = timeString.match(/(\d{1,2}):(\d{2}):(\d{2})/);
if (match) {
const [, hours, minutes, seconds] = match;
const date = new Date();
date.setHours(parseInt(hours), parseInt(minutes), parseInt(seconds), 0);
return date;
}
return null;
}
displayLocalStatistics(analysis) {
this.createLocalStatsModal(analysis);
}
createLocalStatsModal(analysis) {
// Remove existing modal if any
const existingModal = document.getElementById('localStatsModal');
if (existingModal) {
existingModal.remove();
}
const modal = document.createElement('div');
modal.id = 'localStatsModal';
modal.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
z-index: 3000;
display: flex;
align-items: center;
justify-content: center;
`;
modal.innerHTML = `
<div class="local-stats-content" style="
background: #1e1e1e;
color: white;
border-radius: 12px;
width: 90%;
max-width: 1200px;
max-height: 85%;
overflow-y: auto;
padding: 0;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
">
<div class="local-stats-header" style="
padding: 20px;
border-bottom: 1px solid #333;
display: flex;
justify-content: space-between;
align-items: center;
background: #2d2d2d;
border-radius: 12px 12px 0 0;
position: sticky;
top: 0;
z-index: 10;
">
<h2 style="margin: 0; color: #007acc;">📊 Chat Session Statistics</h2>
<button id="closeLocalStats" style="
background: none;
border: none;
color: white;
font-size: 24px;
cursor: pointer;
padding: 4px 8px;
border-radius: 4px;
transition: background 0.2s;
">×</button>
</div>
<div class="local-stats-body" style="padding: 20px;">
${this.generateStatsHTML(analysis)}
</div>
</div>
`;
// Event listeners
modal.addEventListener('click', (e) => {
if (e.target === modal) modal.remove();
});
const closeBtn = modal.querySelector('#closeLocalStats');
closeBtn.addEventListener('click', () => modal.remove());
closeBtn.addEventListener('mouseenter', () => {
closeBtn.style.background = '#444';
});
closeBtn.addEventListener('mouseleave', () => {
closeBtn.style.background = 'none';
});
document.body.appendChild(modal);
}
generateStatsHTML(analysis) {
const avgMessageLength = analysis.messageLengths.length > 0
? Math.round(analysis.messageLengths.reduce((a, b) => a + b, 0) / analysis.messageLengths.length)
: 0;
return `
<!-- Overview Cards -->
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 30px;">
<div class="stat-card" style="background: #2d2d2d; padding: 20px; border-radius: 8px; text-align: center; border: 1px solid #444;">
<div style="font-size: 2rem; color: #40a9ff; font-weight: bold; margin-bottom: 8px;">${analysis.totalMessages}</div>
<div style="color: #888; text-transform: uppercase; font-size: 0.9rem;">Total Messages</div>
</div>
<div class="stat-card" style="background: #2d2d2d; padding: 20px; border-radius: 8px; text-align: center; border: 1px solid #444;">
<div style="font-size: 2rem; color: #52c41a; font-weight: bold; margin-bottom: 8px;">${analysis.players.size}</div>
<div style="color: #888; text-transform: uppercase; font-size: 0.9rem;">Active Players</div>
</div>
<div class="stat-card" style="background: #2d2d2d; padding: 20px; border-radius: 8px; text-align: center; border: 1px solid #444;">
<div style="font-size: 2rem; color: #ff7875; font-weight: bold; margin-bottom: 8px;">${Math.round(analysis.sessionInfo.duration || 0)}m</div>
<div style="color: #888; text-transform: uppercase; font-size: 0.9rem;">Session Length</div>
</div>
<div class="stat-card" style="background: #2d2d2d; padding: 20px; border-radius: 8px; text-align: center; border: 1px solid #444;">
<div style="font-size: 2rem; color: #ffc53d; font-weight: bold; margin-bottom: 8px;">${avgMessageLength}</div>
<div style="color: #888; text-transform: uppercase; font-size: 0.9rem;">Avg Length</div>
</div>
</div>
<!-- Main Content Grid -->
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 30px; margin-bottom: 30px;">
<!-- Top Players -->
<div class="stat-section" style="background: #2d2d2d; padding: 20px; border-radius: 8px; border: 1px solid #444;">
<h3 style="color: #007acc; margin: 0 0 20px 0; display: flex; align-items: center; gap: 8px;">
👥 Top Players
</h3>
<div style="display: grid; gap: 12px;">
${analysis.topPlayers.map((player, index) => `
<div style="display: flex; align-items: center; gap: 12px; padding: 8px; border-radius: 6px; background: #1a1a1a;">
<div style="
width: 24px;
height: 24px;
background: ${this.getPlayerColor(index)};
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
font-size: 0.8rem;
">${index + 1}</div>
<span class="role-badge ${player.role}" style="
padding: 2px 8px;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 500;
">${player.role}</span>
<span style="flex: 1; font-weight: 500;">${player.name}</span>
<div style="text-align: right;">
<div style="color: #40a9ff; font-weight: bold;">${player.messageCount}</div>
<div style="color: #888; font-size: 0.8rem;">${player.averageLength} chars avg</div>
</div>
</div>
`).join('')}
</div>
</div>
<!-- Popular Words -->
<div class="stat-section" style="background: #2d2d2d; padding: 20px; border-radius: 8px; border: 1px solid #444;">
<h3 style="color: #007acc; margin: 0 0 20px 0; display: flex; align-items: center; gap: 8px;">
💬 Popular Words
</h3>
<div style="display: flex; flex-wrap: wrap; gap: 8px;">
${analysis.topWords.slice(0, 15).map(([word, count]) => {
const size = Math.max(0.8, Math.min(1.4, count / Math.max(...analysis.topWords.map(w => w[1])) * 0.8 + 0.6));
return `
<span style="
background: #007acc20;
color: #40a9ff;
padding: 4px 12px;
border-radius: 16px;
font-size: ${size}rem;
font-weight: 500;
border: 1px solid #007acc40;
white-space: nowrap;
">${word} <small style="opacity: 0.7;">${count}</small></span>
`;
}).join('')}
</div>
</div>
</div>
<!-- Activity Timeline -->
<div class="stat-section" style="background: #2d2d2d; padding: 20px; border-radius: 8px; border: 1px solid #444; margin-bottom: 30px;">
<h3 style="color: #007acc; margin: 0 0 20px 0;">⏰ Activity Timeline</h3>
<div style="display: flex; align-items: end; gap: 4px; height: 120px; padding: 10px 0;">
${this.generateTimelineHTML(analysis.timeDistribution)}
</div>
</div>
<!-- Session Details -->
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 30px;">
<div class="stat-section" style="background: #2d2d2d; padding: 20px; border-radius: 8px; border: 1px solid #444;">
<h3 style="color: #007acc; margin: 0 0 20px 0;">📋 Session Details</h3>
<div style="display: grid; gap: 12px;">
<div style="display: flex; justify-content: space-between;">
<span>Start Time:</span>
<strong style="color: #40a9ff;">${analysis.sessionInfo.startTime || 'N/A'}</strong>
</div>
<div style="display: flex; justify-content: space-between;">
<span>End Time:</span>
<strong style="color: #40a9ff;">${analysis.sessionInfo.endTime || 'N/A'}</strong>
</div>
<div style="display: flex; justify-content: space-between;">
<span>Chat Messages:</span>
<strong style="color: #52c41a;">${analysis.chatMessages}</strong>
</div>
<div style="display: flex; justify-content: space-between;">
<span>Join/Leave:</span>
<strong style="color: #ffc53d;">${analysis.joinMessages + analysis.leaveMessages}</strong>
</div>
<div style="display: flex; justify-content: space-between;">
<span>System Messages:</span>
<strong style="color: #ff7875;">${analysis.systemMessages}</strong>
</div>
</div>
</div>
<div class="stat-section" style="background: #2d2d2d; padding: 20px; border-radius: 8px; border: 1px solid #444;">
<h3 style="color: #007acc; margin: 0 0 20px 0;">🏷️ Role Distribution</h3>
<div style="display: grid; gap: 8px;">
${Array.from(analysis.roleDistribution.entries())
.sort((a, b) => b[1] - a[1])
.map(([role, count]) => {
const percentage = Math.round((count / analysis.chatMessages) * 100);
return `
<div style="display: flex; align-items: center; gap: 12px;">
<span class="role-badge ${role}" style="
padding: 2px 8px;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 500;
min-width: 70px;
text-align: center;
">${role}</span>
<div style="flex: 1; height: 16px; background: #1a1a1a; border-radius: 8px; overflow: hidden;">
<div style="
height: 100%;
background: linear-gradient(90deg, #007acc, #40a9ff);
width: ${percentage}%;
transition: width 0.5s ease;
"></div>
</div>
<span style="min-width: 60px; text-align: right; color: #40a9ff; font-weight: 500;">
${count} (${percentage}%)
</span>
</div>
`;
}).join('')}
</div>
</div>
</div>
`;
}
generateTimelineHTML(timeDistribution) {
const hours = Array.from({ length: 24 }, (_, i) => {
const hour = i.toString().padStart(2, '0');
const count = timeDistribution.get(hour) || 0;
return { hour, count };
});
const maxCount = Math.max(...hours.map(h => h.count), 1);
return hours.map(({ hour, count }) => {
const height = (count / maxCount) * 100;
return `
<div style="
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
min-width: 20px;
">
<div style="
width: 100%;
height: 80px;
background: #1a1a1a;
border-radius: 2px;
display: flex;
align-items: end;
margin-bottom: 4px;
">
<div style="
width: 100%;
background: linear-gradient(180deg, #40a9ff, #007acc);
border-radius: 2px;
height: ${height}%;
min-height: ${count > 0 ? '2px' : '0'};
transition: height 0.3s ease;
"></div>
</div>
<div style="font-size: 0.7rem; color: #888;">${hour}</div>
${count > 0 ? `<div style="font-size: 0.6rem; color: #40a9ff;">${count}</div>` : ''}
</div>
`;
}).join('');
}
getPlayerColor(index) {
const colors = ['#007acc', '#52c41a', '#ffc53d', '#ff7875', '#9254de', '#13c2c2', '#fa8c16', '#eb2f96'];
return colors[index % colors.length];
}
showNoDataMessage() {
alert('Keine Chat-Daten für die Analyse gefunden. Bitte wähle zuerst einen Chat aus.');
}
showErrorMessage(message) {
alert(`Fehler bei der Chat-Analyse: ${message}`);
}
}
// Create global instance
window.localChatAnalyzer = new LocalChatAnalyzer();