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
This commit is contained in:
SimolZimol
2025-12-09 15:31:20 +01:00
commit 8ac625a64d
88 changed files with 169343 additions and 0 deletions

629
local-chat-analyzer.js Normal file
View File

@@ -0,0 +1,629 @@
/* 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();