// State management
let allConversations = [];
let filteredConversations = [];
let currentPage = 1;
const itemsPerPage = 50;
let dashboardLoaded = false; // Flag to avoid reloading stats
// Initialize app
document.addEventListener('DOMContentLoaded', () => {
loadData();
setupEventListeners();
});
/**
* Load all data from API
*/
async function loadData() {
try {
// Load configuration
await loadConfig();
// Load conversations
const convResponse = await fetch('/api/conversations');
const convData = await convResponse.json();
// Filter out subagents from main list - they will only appear inline in parent conversations
allConversations = convData.conversations.filter(c => !c.isAgent);
filteredConversations = [...allConversations];
// Populate UI (WITHOUT stats - will load on demand)
populateFilters();
renderConversations();
updateLastUpdated();
} catch (error) {
console.error('Error loading data:', error);
alert('Error loading data. Verify that the server is running.');
}
}
/**
* Load Claude Code configuration
*/
async function loadConfig() {
try {
const response = await fetch('/api/config');
const data = await response.json();
if (data.success) {
updateConfigDisplay(data);
}
} catch (error) {
console.error('Error loading config:', error);
}
}
/**
* Update config display in header
*/
function updateConfigDisplay(config) {
const retentionInfo = document.getElementById('retention-info');
if (retentionInfo) {
const days = config.cleanupPeriodDays;
const statusText = config.configured ? 'Custom' : 'Default';
retentionInfo.textContent = `Actual retention period: ${days} days`;
retentionInfo.title = `Session Retention: ${days} days (${statusText})\n\nClaude Code deletes sessions older than ${days} days.\n\nTo change: Edit ${config.settingsPath}\nSet "cleanupPeriodDays": 365 for 1 year retention.`;
}
}
/**
* Populate filter dropdowns
*/
function populateFilters() {
const users = [...new Set(allConversations.map(c => c.username))].sort();
const projects = [...new Set(allConversations.map(c => c.project))].sort();
const userSelect = document.getElementById('filter-user');
const projectSelect = document.getElementById('filter-project');
// Populate users
users.forEach(user => {
const option = document.createElement('option');
option.value = user;
option.textContent = user;
userSelect.appendChild(option);
});
// Populate projects
projects.forEach(project => {
const option = document.createElement('option');
option.value = project;
option.textContent = project.split('\\').pop(); // Show only last folder name
projectSelect.appendChild(option);
});
}
/**
* Helper: Sanitize text for display
*/
function sanitizeText(text) {
if (typeof text !== 'string') {
return 'Unknown';
}
return text.replace(/[<>]/g, '');
}
/**
* Helper: Sanitize text for HTML attributes
*/
function sanitizeAttribute(text) {
if (typeof text !== 'string') {
return '';
}
return text.replace(/['"/]/g, '');
}
/**
* Select conversation and show details panel
*/
function selectConversation(sessionId, username, itemElement) {
if (!sessionId) {
console.warn('Conversation has no sessionId');
return;
}
// Switch to conversation tab
switchToConversationTab();
// Update selected state
document.querySelectorAll('.conversation-item').forEach(item => {
item.classList.remove('selected');
});
itemElement.classList.add('selected');
// Show details panel and fetch conversation
showConversationDetails(sessionId, username);
}
/**
* Close details panel
*/
function closeDetailsPanel() {
document.getElementById('conversation-details').style.display = 'none';
document.getElementById('empty-state').style.display = 'flex';
document.querySelectorAll('.conversation-item').forEach(item => {
item.classList.remove('selected');
});
}
/**
* Render conversations list (card-based)
*/
function renderConversations() {
const list = document.getElementById('conversations-list');
list.innerHTML = '';
const start = (currentPage - 1) * itemsPerPage;
const end = start + itemsPerPage;
const pageConversations = filteredConversations.slice(start, end);
if (pageConversations.length === 0) {
list.innerHTML = '<div class="no-data" style="text-align:center; padding:20px; color:#999;">No conversation found</div>';
return;
}
pageConversations.forEach((conv, index) => {
const item = document.createElement('div');
// Add special class for subagents
item.className = conv.isAgent ? 'conversation-item subagent-item' : 'conversation-item';
item.dataset.sessionId = conv.sessionId || '';
item.dataset.username = sanitizeAttribute(conv.username || '');
item.dataset.index = start + index;
item.dataset.isAgent = conv.isAgent || false;
const timestamp = new Date(conv.timestamp);
const timeStr = timestamp.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
const dateStr = timestamp.toLocaleDateString('en-US', { month: '2-digit', day: '2-digit' });
// Clean display text (remove double spaces, limit length)
const displayText = (conv.display || '')
.replace(/\s+/g, ' ')
.trim()
.substring(0, 70);
// Safe username
const username = sanitizeText(conv.username || 'Unknown');
// Metadata
const messageCount = conv.messageCount || 0;
const tokens = conv.totalTokens ? Math.round(conv.totalTokens / 1000) + 'K' : '0';
item.innerHTML = `
<div class="timestamp">${timeStr} ${dateStr}</div>
<div class="username">๐ค ${escapeHtml(username)}</div>
<div class="preview">${highlightSearchTerm(escapeHtml(displayText))}</div>
<div class="meta">${messageCount} msg โข ${tokens} tokens</div>
`;
// Add click event
item.addEventListener('click', () => selectConversation(conv.sessionId, conv.username, item));
list.appendChild(item);
});
// Update pagination and results count
updatePaginationInfo();
document.getElementById('results-count').textContent = `${filteredConversations.length}`;
}
/**
* Apply filters
*/
function applyFilters() {
const searchTerm = document.getElementById('search-input').value.toLowerCase();
const userFilter = document.getElementById('filter-user').value;
const projectFilter = document.getElementById('filter-project').value;
const dateFrom = document.getElementById('filter-date-from').value;
const dateTo = document.getElementById('filter-date-to').value;
filteredConversations = allConversations.filter(conv => {
// Search term - now searches in full conversation text (user + Claude messages)
if (searchTerm) {
const searchIn = conv.searchableText || conv.display.toLowerCase();
if (!searchIn.includes(searchTerm)) {
return false;
}
}
// User filter
if (userFilter && conv.username !== userFilter) {
return false;
}
// Project filter
if (projectFilter && conv.project !== projectFilter) {
return false;
}
// Date range
const convDate = new Date(conv.timestamp).toISOString().split('T')[0];
if (dateFrom && convDate < dateFrom) {
return false;
}
if (dateTo && convDate > dateTo) {
return false;
}
return true;
});
currentPage = 1;
renderConversations();
}
/**
* Export to CSV
*/
function exportToCSV() {
const headers = ['Timestamp', 'User', 'Project', 'Prompt', 'Messages', 'Tokens', 'Model', 'Tools'];
const rows = filteredConversations.map(conv => [
new Date(conv.timestamp).toISOString(),
conv.username,
conv.project,
`"${conv.display.replace(/"/g, '""')}"`, // Escape quotes
conv.messageCount || 0,
conv.totalTokens || 0,
conv.model || '',
conv.toolsUsed && conv.toolsUsed.length > 0 ? `"${conv.toolsUsed.join(', ')}"` : ''
]);
const csv = [headers.join(','), ...rows.map(r => r.join(','))].join('\n');
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = `claude-conversations-${new Date().toISOString().slice(0, 10)}.csv`;
link.click();
}
/**
* Pagination
*/
function nextPage() {
const maxPage = Math.ceil(filteredConversations.length / itemsPerPage);
if (currentPage < maxPage) {
currentPage++;
renderConversations();
}
}
function prevPage() {
if (currentPage > 1) {
currentPage--;
renderConversations();
}
}
function updatePaginationInfo() {
const maxPage = Math.ceil(filteredConversations.length / itemsPerPage);
document.getElementById('page-info').textContent = `Page ${currentPage} of ${maxPage}`;
document.getElementById('prev-page').disabled = currentPage === 1;
document.getElementById('next-page').disabled = currentPage >= maxPage;
}
/**
* Toggle filters panel expand/collapse
*/
function toggleFiltersPanel() {
const filtersSection = document.querySelector('.filters-section');
const filtersContent = document.getElementById('filters-content');
const toggleIcon = filtersSection.querySelector('.toggle-icon');
if (filtersSection.classList.contains('collapsed')) {
// Expand
filtersSection.classList.remove('collapsed');
filtersContent.style.display = 'block';
if (toggleIcon) toggleIcon.style.transform = 'rotate(0deg)';
} else {
// Collapse
filtersSection.classList.add('collapsed');
filtersContent.style.display = 'none';
if (toggleIcon) toggleIcon.style.transform = 'rotate(-90deg)';
}
}
/**
* Setup event listeners
*/
function setupEventListeners() {
// Search
document.getElementById('search-btn').addEventListener('click', applyFilters);
document.getElementById('search-input').addEventListener('keypress', (e) => {
if (e.key === 'Enter') applyFilters();
});
document.getElementById('clear-search-btn').addEventListener('click', () => {
document.getElementById('search-input').value = '';
applyFilters();
});
// Filters
document.getElementById('filter-user').addEventListener('change', applyFilters);
document.getElementById('filter-project').addEventListener('change', applyFilters);
document.getElementById('filter-date-from').addEventListener('change', applyFilters);
document.getElementById('filter-date-to').addEventListener('change', applyFilters);
// Export
document.getElementById('export-btn').addEventListener('click', exportToCSV);
// Pagination
document.getElementById('prev-page').addEventListener('click', prevPage);
document.getElementById('next-page').addEventListener('click', nextPage);
// Details Panel Close Button
const closeDetailsBtn = document.getElementById('details-close-btn');
if (closeDetailsBtn) {
closeDetailsBtn.addEventListener('click', closeDetailsPanel);
}
// Filters Toggle
const filtersToggle = document.getElementById('filters-toggle');
if (filtersToggle) {
filtersToggle.addEventListener('click', toggleFiltersPanel);
}
// Keyboard navigation
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
const detailsPanel = document.getElementById('conversation-details');
if (detailsPanel && detailsPanel.style.display !== 'none') {
closeDetailsPanel();
}
}
});
}
/**
* Utility: Escape HTML
*/
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
/**
* Utility: Highlight search term
*/
function highlightSearchTerm(text) {
const searchTerm = document.getElementById('search-input').value;
if (!searchTerm) return text;
const regex = new RegExp(`(${searchTerm})`, 'gi');
return text.replace(regex, '<mark>$1</mark>');
}
/**
* Update last updated timestamp
*/
function updateLastUpdated() {
document.getElementById('last-updated').textContent = `Last updated: ${new Date().toLocaleString('en-US')}`;
}
/**
* Show conversation details in side panel
*/
async function showConversationDetails(sessionId, username) {
const emptyState = document.getElementById('empty-state');
const detailsPanel = document.getElementById('conversation-details');
const detailsUsername = document.getElementById('details-username');
const detailsProject = document.getElementById('details-project');
const detailsSessionId = document.getElementById('details-session-id');
const detailsMessageCount = document.getElementById('details-message-count');
const detailsTotalTokens = document.getElementById('details-total-tokens');
const threadContainer = document.getElementById('conversation-thread');
// Show details panel, hide empty state
emptyState.style.display = 'none';
detailsPanel.style.display = 'flex';
// Show loading
threadContainer.innerHTML = '<div class="loading">โณ Loading conversation...</div>';
try {
const response = await fetch(`/api/conversation/${sessionId}/${username}`);
const data = await response.json();
if (!data.success) {
throw new Error(data.error || 'Loading error');
}
// Update metadata
detailsUsername.textContent = sanitizeText(username);
detailsSessionId.textContent = sessionId.substring(0, 8) + '...';
detailsProject.textContent = (data.projectDir || 'N/A').split('\\').pop();
detailsMessageCount.textContent = data.messageCount || '0';
// Show token breakdown: input / output (cache if present)
if (data.inputTokens || data.outputTokens) {
const inK = (data.inputTokens / 1000).toFixed(1);
const outK = (data.outputTokens / 1000).toFixed(1);
let tokenText = `${inK}K in / ${outK}K out`;
if (data.cacheTokens > 0) {
const cacheK = (data.cacheTokens / 1000).toFixed(1);
tokenText += ` (+${cacheK}K cache)`;
}
detailsTotalTokens.textContent = tokenText;
} else {
detailsTotalTokens.textContent = data.totalTokens ? (Math.round(data.totalTokens / 1000) + 'K') : '0';
}
// Render conversation thread with subagents
renderConversationThread(data.thread, threadContainer, data.subagents || []);
} catch (error) {
console.error('Error loading conversation:', error);
threadContainer.innerHTML = `<div class="error">โ Error: ${escapeHtml(error.message)}</div>`;
}
}
/**
* Render subagent thread inline (simplified version without recursion)
*/
function renderSubagentThread(subagent) {
if (!subagent.thread || subagent.thread.length === 0) {
return '<div class="no-data">No subagent messages</div>';
}
let html = '';
subagent.thread.forEach(item => {
const timestamp = new Date(item.timestamp).toLocaleString('en-US', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
if (item.role === 'user') {
const contentHtml = renderUserContent(item.content);
html += `
<div class="message message-user">
<div class="message-header">
<strong>๐ค User</strong>
<span class="message-time">${timestamp}</span>
</div>
<div class="message-content">${contentHtml}</div>
</div>
`;
} else if (item.role === 'assistant') {
const contentHtml = renderAssistantContent(item.content);
html += `
<div class="message message-assistant">
<div class="message-header">
<strong>๐ค Claude</strong>
<span class="message-model">${item.model}</span>
<span class="message-time">${timestamp}</span>
</div>
<div class="message-content">${contentHtml}</div>
</div>
`;
} else if (item.type === 'tool_use') {
const toolInputStr = typeof item.toolInput === 'object'
? JSON.stringify(item.toolInput)
: item.toolInput;
html += `<div class="message message-tool_use"><div class="message-header"><strong>๐ง Tool: ${item.toolName}</strong><span class="message-time">${timestamp}</span></div><div class="message-content tool-input"><pre><code>${escapeHtml(toolInputStr)}</code></pre></div></div>`;
} else if (item.type === 'tool_result') {
const resultStr = typeof item.toolResult === 'object'
? JSON.stringify(item.toolResult)
: item.toolResult;
const statusClass = item.isError ? 'tool-error' : 'tool-success';
html += `<div class="message message-tool_result"><div class="message-header"><strong>๐ค Result: ${item.toolName}</strong><span class="message-time">${timestamp}</span></div><div class="message-content tool-result ${statusClass}"><pre><code>${escapeHtml(resultStr)}</code></pre></div></div>`;
}
});
return html;
}
/**
* Render conversation thread in modal
*/
function renderConversationThread(thread, container, subagents = []) {
console.log('[renderConversationThread]', {
threadLength: thread.length,
subagentsCount: subagents.length,
subagents: subagents
});
container.innerHTML = '';
if (thread.length === 0) {
container.innerHTML = '<div class="no-data">No messages found</div>';
return;
}
thread.forEach((item, index) => {
const messageDiv = document.createElement('div');
messageDiv.className = `message message-${item.role || item.type}`;
const timestamp = new Date(item.timestamp).toLocaleString('en-US', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
if (item.role === 'user') {
const contentHtml = renderUserContent(item.content);
messageDiv.innerHTML = `
<div class="message-header">
<strong>๐ค User</strong>
<span class="message-time">${timestamp}</span>
</div>
<div class="message-content">${contentHtml}</div>
`;
} else if (item.role === 'assistant') {
const contentHtml = renderAssistantContent(item.content);
messageDiv.innerHTML = `
<div class="message-header">
<strong>๐ค Claude</strong>
<span class="message-model">${item.model}</span>
<span class="message-time">${timestamp}</span>
</div>
<div class="message-content">${contentHtml}</div>
`;
} else if (item.type === 'tool_use') {
console.log('[tool_use detected]', {
toolName: item.toolName,
timestamp: item.timestamp,
subagentsAvailable: subagents ? subagents.length : 0
});
const toolInputStr = typeof item.toolInput === 'object'
? JSON.stringify(item.toolInput)
: item.toolInput;
let toolHtml = `<div class="message-header"><strong>๐ง Tool: ${item.toolName}</strong><span class="message-time">${timestamp}</span></div><div class="message-content tool-input"><pre><code>${escapeHtml(toolInputStr)}</code></pre></div>`;
// If this is a Task tool, check for matching subagents
if (item.toolName === 'Task' && subagents && subagents.length > 0) {
console.log('[Task tool detected]', {
timestamp: item.timestamp,
subagentsCount: subagents.length,
subagents: subagents.map(s => ({
timestamp: s.timestamp,
agentId: s.agentId
}))
});
const itemTime = new Date(item.timestamp).getTime();
// Find subagents within ยฑ30 minutes of this tool invocation
const matchingSubagents = subagents.filter(sub => {
const subTime = new Date(sub.timestamp).getTime();
const timeDiff = Math.abs(subTime - itemTime);
console.log('[Subagent matching]', {
taskTime: new Date(itemTime).toISOString(),
subTime: new Date(subTime).toISOString(),
timeDiffMinutes: Math.round(timeDiff / 1000 / 60),
matches: timeDiff < 30 * 60 * 1000
});
return timeDiff < 30 * 60 * 1000; // 30 minutes tolerance
});
if (matchingSubagents.length > 0) {
matchingSubagents.forEach(subagent => {
const subagentThreadHtml = renderSubagentThread(subagent);
toolHtml += `
<details class="subagent-details" style="margin-top: 10px;">
<summary class="subagent-summary">
<strong>๐ค Subagent</strong>
<span class="subagent-badge">${subagent.model}</span>
<span class="subagent-stats">${subagent.messageCount} messages ยท ${subagent.totalTokens.toLocaleString()} tokens</span>
</summary>
<div class="subagent-content">
${subagentThreadHtml}
</div>
</details>
`;
});
}
}
messageDiv.innerHTML = toolHtml;
} else if (item.type === 'tool_result') {
const resultStr = typeof item.toolResult === 'object'
? JSON.stringify(item.toolResult)
: item.toolResult;
const statusClass = item.isError ? 'tool-error' : 'tool-success';
messageDiv.innerHTML = `<div class="message-header"><strong>๐ค Result: ${item.toolName}</strong><span class="message-time">${timestamp}</span></div><div class="message-content tool-result ${statusClass}"><pre><code>${escapeHtml(resultStr)}</code></pre></div>`;
}
container.appendChild(messageDiv);
});
}
/**
* Render user content (handles string or array of content blocks)
*/
function renderUserContent(content) {
// Handle string content
if (typeof content === 'string') {
const cleanedText = content.replace(/\s+/g, ' ').trim();
return `<div class="text-block">${escapeHtml(cleanedText)}</div>`;
}
// Handle array content (multimodal)
if (Array.isArray(content)) {
let html = '';
content.forEach(block => {
if (!block) return;
if (block.type === 'text') {
const cleanedText = (block.text || '').replace(/\s+/g, ' ').trim();
html += `<div class="text-block">${escapeHtml(cleanedText)}</div>`;
} else if (block.type === 'image') {
html += `<div class="text-block">[Image: ${escapeHtml(block.source?.url || 'unknown')}]</div>`;
} else if (block.type === 'document') {
html += `<div class="text-block">[Document: ${escapeHtml(block.source?.url || 'unknown')}]</div>`;
}
});
return html || '<div class="text-block">[No content]</div>';
}
// Handle object content (fallback)
if (typeof content === 'object') {
try {
const jsonStr = JSON.stringify(content);
return `<pre class="message-content">${escapeHtml(jsonStr)}</pre>`;
} catch (e) {
return `<div class="text-block">[Unrenderable content]</div>`;
}
}
// Fallback
return `<div class="text-block">${escapeHtml(String(content))}</div>`;
}
/**
* Render assistant content (handles thinking blocks, text, tool_use)
* FIX: Properly handle content as string, array, or object
*/
function renderAssistantContent(content) {
// Handle string content
if (typeof content === 'string') {
const cleanedText = content.replace(/\s+/g, ' ').trim();
return `<div class="text-block">${escapeHtml(cleanedText)}</div>`;
}
// Handle array content
if (Array.isArray(content)) {
let html = '';
content.forEach(block => {
if (!block) return;
if (block.type === 'text') {
const cleanedText = (block.text || '').replace(/\s+/g, ' ').trim();
html += `<div class="text-block">${escapeHtml(cleanedText)}</div>`;
} else if (block.type === 'thinking') {
html += `<details class="thinking-block"><summary>๐ญ Thinking (${block.thinking?.length || 0} chars)</summary><pre>${escapeHtml(block.thinking || '')}</pre></details>`;
} else if (block.type === 'tool_use') {
try {
const inputStr = JSON.stringify(block.input || {});
html += `<div class="tool-use-block"><strong>๐ง ${escapeHtml(block.name || 'Tool')}</strong><pre><code>${escapeHtml(inputStr)}</code></pre></div>`;
} catch (e) {
console.error('Error rendering tool_use block:', e);
html += `<div class="tool-use-block"><strong>๐ง ${escapeHtml(block.name || 'Tool')}</strong><pre>[Error rendering tool input]</pre></div>`;
}
}
});
return html || '<div class="text-block">[No content]</div>';
}
// Handle object content (malformed response)
if (typeof content === 'object') {
try {
const jsonStr = JSON.stringify(content);
return `<pre class="message-content">${escapeHtml(jsonStr)}</pre>`;
} catch (e) {
return `<div class="text-block">[Unrenderable content]</div>`;
}
}
// Fallback
return `<div class="text-block">${escapeHtml(String(content))}</div>`;
}
// Old modal code removed - replaced with side panel in v3.0
// ===== TAB NAVIGATION =====
function initTabNavigation() {
const tabBtns = document.querySelectorAll('.tab-btn');
const dashboardView = document.getElementById('dashboard-view');
const conversationView = document.getElementById('conversation-view');
tabBtns.forEach(btn => {
btn.addEventListener('click', () => {
const targetTab = btn.dataset.tab;
// Update active states
tabBtns.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
// Show corresponding view
if (targetTab === 'dashboard') {
dashboardView.classList.add('active');
conversationView.classList.remove('active');
loadDashboard(); // Load stats on demand
} else if (targetTab === 'conversation') {
dashboardView.classList.remove('active');
conversationView.classList.add('active');
}
});
});
// Dashboard will load on-demand when user clicks the tab (not on page load)
}
// Switch to conversation tab (called when selecting a conversation)
function switchToConversationTab() {
document.getElementById('tab-conversation').click();
}
// ===== DASHBOARD POPULATION =====
async function loadDashboard() {
// Skip if already loaded (avoid expensive recalculation)
if (dashboardLoaded) {
console.log('[Dashboard] Already loaded, skipping');
return;
}
try {
console.log('[Dashboard] Fetching stats (first load)...');
const response = await fetch('/api/stats');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log('[Dashboard] Full response:', data);
// Extract stats from the wrapper object
const stats = data.stats;
console.log('[Dashboard] Stats extracted:', stats);
// Check if stats is valid
if (!stats || typeof stats !== 'object') {
throw new Error('Invalid stats data received');
}
// Populate summary cards (with fallbacks)
document.getElementById('dash-total-conversations').textContent = (stats.totalConversations || 0).toLocaleString();
document.getElementById('dash-total-users').textContent = stats.totalUsers || 0;
document.getElementById('dash-total-messages').textContent = (stats.totalMessages || 0).toLocaleString();
document.getElementById('dash-total-tokens').textContent = ((stats.totalTokens || 0) / 1000000).toFixed(2) + 'M';
// Token breakdown
document.getElementById('dash-input-tokens').textContent = ((stats.inputTokens || 0) / 1000000).toFixed(2) + 'M';
document.getElementById('dash-output-tokens').textContent = ((stats.outputTokens || 0) / 1000000).toFixed(2) + 'M';
document.getElementById('dash-cache-tokens').textContent = ((stats.cacheTokens || 0) / 1000000).toFixed(2) + 'M';
// Daily chart
if (stats.dailyStats) {
renderDashboardChart(stats.dailyStats);
}
// Model stats
if (stats.modelStats) {
renderModelStats(stats.modelStats);
}
// Tool stats
if (stats.toolStats) {
renderToolStats(stats.toolStats);
}
// User stats
if (stats.userStats && stats.tokensByUser) {
renderUserStats(stats.userStats, stats.tokensByUser);
}
// Project stats
if (stats.projectStats) {
renderProjectStats(stats.projectStats);
}
// Keywords cloud removed - too expensive and not very useful
// Mark as loaded to avoid reloading
dashboardLoaded = true;
console.log('[Dashboard] Loaded successfully');
} catch (error) {
console.error('[Dashboard] Error loading:', error);
alert('Error loading dashboard: ' + error.message);
}
}
// Render daily activity chart
function renderDashboardChart(dailyStats) {
const canvas = document.getElementById('dash-daily-chart');
if (!canvas) return;
const ctx = canvas.getContext('2d');
const dates = Object.keys(dailyStats).sort().slice(-30); // Last 30 days
const counts = dates.map(date => dailyStats[date]);
const maxCount = Math.max(...counts, 1);
// Clear canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw bars
const barWidth = canvas.width / dates.length;
ctx.fillStyle = '#4a9eff';
counts.forEach((count, i) => {
const barHeight = (count / maxCount) * (canvas.height - 20);
const x = i * barWidth;
const y = canvas.height - barHeight;
ctx.fillRect(x + 2, y, barWidth - 4, barHeight);
});
// Draw axis
ctx.strokeStyle = '#e0e0e0';
ctx.beginPath();
ctx.moveTo(0, canvas.height - 1);
ctx.lineTo(canvas.width, canvas.height - 1);
ctx.stroke();
}
// Render model stats
function renderModelStats(modelStats) {
const container = document.getElementById('dash-model-stats');
if (!container) return;
const sortedModels = Object.entries(modelStats)
.sort(([, a], [, b]) => b - a)
.slice(0, 5); // Top 5
container.innerHTML = sortedModels.map(([model, count]) => `
<div class="stats-list-item">
<span class="label">${escapeHtml(model || 'Unknown')}</span>
<span class="value">${count}</span>
</div>
`).join('');
}
// Render tool stats
function renderToolStats(toolStats) {
const container = document.getElementById('dash-tool-stats');
if (!container) return;
const sortedTools = Object.entries(toolStats)
.sort(([, a], [, b]) => b - a)
.slice(0, 5); // Top 5
container.innerHTML = sortedTools.map(([tool, count]) => `
<div class="stats-list-item">
<span class="label">${escapeHtml(tool)}</span>
<span class="value">${count}</span>
</div>
`).join('');
}
// Render user stats
function renderUserStats(userStats, tokensByUser) {
const container = document.getElementById('dash-user-stats');
if (!container) return;
const sortedUsers = Object.entries(userStats)
.sort(([, a], [, b]) => b - a);
container.innerHTML = sortedUsers.map(([username, convCount]) => {
const tokens = tokensByUser[username] || 0;
return `
<div class="user-stat-card">
<div class="username">${escapeHtml(username)}</div>
<div class="stats">
<span>
<div>Conversations</div>
<div class="value">${convCount}</div>
</span>
<span>
<div>Tokens</div>
<div class="value">${(tokens / 1000).toFixed(1)}K</div>
</span>
</div>
</div>
`;
}).join('');
}
// Render project stats
function renderProjectStats(projectStats) {
const container = document.getElementById('dash-project-stats');
if (!container) return;
const sortedProjects = Object.entries(projectStats)
.sort(([, a], [, b]) => b - a)
.slice(0, 10); // Top 10
container.innerHTML = sortedProjects.map(([project, count]) => {
const projectName = project.split('/').pop() || project;
return `
<div class="stats-list-item">
<span class="label" title="${escapeHtml(project)}">${escapeHtml(projectName)}</span>
<span class="value">${count}</span>
</div>
`;
}).join('');
}
// Render keywords cloud
function renderKeywordsCloud(keywords) {
const container = document.getElementById('dash-keywords-cloud');
if (!container) return;
const sortedKeywords = Object.entries(keywords)
.sort(([, a], [, b]) => b - a)
.slice(0, 20); // Top 20
const maxCount = sortedKeywords[0]?.[1] || 1;
container.innerHTML = sortedKeywords.map(([keyword, count]) => {
const size = Math.max(0.8, (count / maxCount) * 2);
return `<span class="keyword" style="font-size: ${size}em;" title="${count} occurrences">${escapeHtml(keyword)}</span>`;
}).join(' ');
}
// Initialize on page load
document.addEventListener('DOMContentLoaded', () => {
initTabNavigation();
});