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:
629
local-chat-analyzer.js
Normal file
629
local-chat-analyzer.js
Normal 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();
|
||||
Reference in New Issue
Block a user