// ============ THEME ============ const THEME_STORAGE_KEY = 'agentdash-theme'; const themeToggleBtn = document.getElementById('theme-toggle'); const systemThemeMedia = window.matchMedia('(prefers-color-scheme: dark)'); function getSystemTheme() { return systemThemeMedia.matches ? 'dark' : 'light'; } function getSavedTheme() { try { const saved = localStorage.getItem(THEME_STORAGE_KEY); return saved === 'light' || saved === 'dark' ? saved : null; } catch { return null; } } function setSavedTheme(theme) { try { localStorage.setItem(THEME_STORAGE_KEY, theme); } catch { // Ignore localStorage errors (privacy mode, quota, etc.). } } function updateThemeToggleLabel() { if (!themeToggleBtn) return; const isDarkTheme = document.documentElement.getAttribute('data-theme') === 'dark'; const nextTheme = isDarkTheme ? 'light' : 'dark'; themeToggleBtn.textContent = isDarkTheme ? 'Light Mode' : 'Dark Mode'; themeToggleBtn.setAttribute('aria-label', `Switch to ${nextTheme} mode`); } function applyTheme(theme, { persist = false } = {}) { document.documentElement.setAttribute('data-theme', theme); if (persist) setSavedTheme(theme); updateThemeToggleLabel(); if (usageStats) renderUsageCharts(); } function initTheme() { const savedTheme = getSavedTheme(); applyTheme(savedTheme || getSystemTheme()); if (themeToggleBtn) { themeToggleBtn.addEventListener('click', () => { const currentTheme = document.documentElement.getAttribute('data-theme') || getSystemTheme(); const nextTheme = currentTheme === 'dark' ? 'light' : 'dark'; applyTheme(nextTheme, { persist: true }); }); } systemThemeMedia.addEventListener('change', (event) => { if (!getSavedTheme()) { applyTheme(event.matches ? 'dark' : 'light'); } }); } // ============ NAVIGATION ============ const navLinks = document.querySelectorAll('.nav-link'); const pages = document.querySelectorAll('.page'); navLinks.forEach(link => { link.addEventListener('click', (e) => { e.preventDefault(); const targetPage = link.dataset.page; // Update active nav link navLinks.forEach(l => l.classList.remove('active')); link.classList.add('active'); // Show target page pages.forEach(page => { page.classList.remove('active'); if (page.id === `page-${targetPage}`) { page.classList.add('active'); } }); // Load page data if (targetPage === 'wiki') loadWiki(); if (targetPage === 'agents') loadAgents(); if (targetPage === 'usage') loadUsage(); }); }); // ============ TASK DASHBOARD ============ const COLUMNS = { 'Backlog': { title: '📋 Backlog', tasks: [] }, 'Todo': { title: '📝 Todo', tasks: [] }, 'In Progress': { title: '🔄 In Progress', tasks: [] }, 'Review': { title: '👀 Review', tasks: [] }, 'Done': { title: '✅ Done', tasks: [] } }; async function loadTasks() { const res = await fetch('/api/tasks'); const tasks = await res.json(); // Reset columns Object.keys(COLUMNS).forEach(status => { COLUMNS[status].tasks = []; }); // Group tasks by status tasks.forEach(task => { if (COLUMNS[task.status]) { COLUMNS[task.status].tasks.push(task); } }); renderBoard(); } function renderBoard() { const board = document.getElementById('board'); board.innerHTML = ''; Object.entries(COLUMNS).forEach(([status, column]) => { const columnEl = document.createElement('div'); columnEl.className = 'column'; columnEl.innerHTML = `

${column.title}

${column.tasks.length}
`; const cardsEl = columnEl.querySelector('.cards'); column.tasks.forEach(task => { const cardEl = document.createElement('div'); cardEl.className = 'card'; cardEl.innerHTML = `

${escapeHtml(task.title)}

${task.priority}

${escapeHtml(task.description || '')}

${task.assignee || 'Unassigned'}

${task.tags.map(t => `${escapeHtml(t)}`).join(' ')}

`; // Checkbox handler const checkbox = cardEl.querySelector('.card-check'); checkbox.addEventListener('change', async () => { await fetch(`/api/tasks/${task.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status: 'Done' }) }); loadTasks(); }); cardsEl.appendChild(cardEl); }); board.appendChild(columnEl); }); } // Task form document.getElementById('task-form').addEventListener('submit', async (e) => { e.preventDefault(); const formData = new FormData(e.target); const task = { title: formData.get('title'), description: formData.get('description'), assignee: formData.get('assignee'), priority: formData.get('priority'), status: formData.get('status') || 'Backlog', tags: formData.get('tags').split(',').map(t => t.trim()).filter(t => t) }; await fetch('/api/tasks', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(task) }); e.target.reset(); loadTasks(); }); // Populate agent dropdown async function populateAgentDropdown() { try { const res = await fetch('/api/agents'); const agents = await res.json(); const select = document.getElementById('assignee'); if (!select) return; // Keep the first option ("Select agent...") const firstOption = select.options[0]; select.innerHTML = ''; select.appendChild(firstOption); // Add agent options agents.forEach(agent => { const option = document.createElement('option'); option.value = agent.name; option.textContent = agent.name; select.appendChild(option); }); } catch (err) { console.error('Failed to load agents for dropdown:', err); } } // ============ WIKI ============ let wikiPages = []; let currentWikiPage = null; let isEditingWiki = false; async function loadWiki() { try { const res = await fetch('/api/wiki'); wikiPages = await res.json(); renderWikiList(); } catch (err) { console.error('Failed to load wiki:', err); } } function renderWikiList(filter = '') { const wikiList = document.getElementById('wiki-list'); wikiList.innerHTML = ''; const filtered = filter ? wikiPages.filter(p => p.title.toLowerCase().includes(filter.toLowerCase()) || p.filename.toLowerCase().includes(filter.toLowerCase())) : wikiPages; filtered.forEach(page => { const itemEl = document.createElement('div'); itemEl.className = 'wiki-item' + (currentWikiPage && currentWikiPage.filename === page.filename ? ' active' : ''); itemEl.innerHTML = `

${escapeHtml(page.title)}

${new Date(page.modified).toLocaleDateString()}

`; itemEl.addEventListener('click', () => selectWikiPage(page.filename)); wikiList.appendChild(itemEl); }); } // Wiki search document.getElementById('wiki-search').addEventListener('input', (e) => { renderWikiList(e.target.value); }); // New wiki page document.getElementById('wiki-new-btn').addEventListener('click', async () => { const title = prompt('Enter page title:'); if (!title) return; try { const res = await fetch('/api/wiki', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title }) }); if (res.ok) { const data = await res.json(); await loadWiki(); selectWikiPage(data.filename); } } catch (err) { console.error('Failed to create wiki page:', err); } }); async function selectWikiPage(filename) { try { const res = await fetch(`/api/wiki/${filename}`); if (!res.ok) throw new Error('Page not found'); currentWikiPage = await res.json(); // Update UI document.getElementById('wiki-page-title').textContent = currentWikiPage.metadata.title || filename; document.getElementById('wiki-page-actions').style.display = 'flex'; // Render markdown const contentEl = document.getElementById('wiki-content'); contentEl.style.display = 'block'; document.getElementById('wiki-editor').style.display = 'none'; if (typeof marked !== 'undefined') { contentEl.innerHTML = marked.parse(currentWikiPage.content); } else { contentEl.innerHTML = `
${escapeHtml(currentWikiPage.content)}
`; } // Update list selection renderWikiList(document.getElementById('wiki-search').value); } catch (err) { console.error('Failed to load wiki page:', err); } } // Edit wiki page document.getElementById('wiki-edit-btn').addEventListener('click', () => { if (!currentWikiPage) return; isEditingWiki = true; document.getElementById('wiki-content').style.display = 'none'; document.getElementById('wiki-editor').style.display = 'block'; document.getElementById('wiki-edit-title').value = currentWikiPage.metadata.title || ''; document.getElementById('wiki-edit-content').value = currentWikiPage.content; }); // Save wiki page document.getElementById('wiki-save-btn').addEventListener('click', async () => { if (!currentWikiPage) return; const content = document.getElementById('wiki-edit-content').value; try { const res = await fetch(`/api/wiki/${currentWikiPage.filename}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content }) }); if (res.ok) { isEditingWiki = false; await selectWikiPage(currentWikiPage.filename); } } catch (err) { console.error('Failed to save wiki page:', err); } }); // Cancel edit document.getElementById('wiki-cancel-btn').addEventListener('click', () => { isEditingWiki = false; document.getElementById('wiki-editor').style.display = 'none'; document.getElementById('wiki-content').style.display = 'block'; }); // Delete wiki page document.getElementById('wiki-delete-btn').addEventListener('click', async () => { if (!currentWikiPage) return; if (!confirm(`Delete "${currentWikiPage.metadata.title || currentWikiPage.filename}"?`)) return; try { const res = await fetch(`/api/wiki/${currentWikiPage.filename}`, { method: 'DELETE' }); if (res.ok) { currentWikiPage = null; document.getElementById('wiki-page-title').textContent = 'Select a page'; document.getElementById('wiki-page-actions').style.display = 'none'; document.getElementById('wiki-content').innerHTML = '

📚 Select a wiki page from the sidebar or create a new one.

'; await loadWiki(); } } catch (err) { console.error('Failed to delete wiki page:', err); } }); // ============ AGENTS ============ let allAgents = []; async function loadAgents() { try { const res = await fetch('/api/agents'); allAgents = await res.json(); renderAgents(); } catch (err) { console.error('Failed to load agents:', err); } } function renderAgents(filter = '', statusFilter = '') { const grid = document.getElementById('agents-grid'); grid.innerHTML = ''; let filtered = allAgents; if (filter) { filtered = filtered.filter(a => a.name.toLowerCase().includes(filter.toLowerCase()) || (a.currentTask && a.currentTask.toLowerCase().includes(filter.toLowerCase())) ); } if (statusFilter) { filtered = filtered.filter(a => a.status === statusFilter); } filtered.forEach(agent => { const cardEl = document.createElement('div'); cardEl.className = 'agent-card'; const statusClass = `status-${agent.status}`; cardEl.innerHTML = `

${escapeHtml(agent.name)}

${agent.status}
📋 ${agent.workload} active task${agent.workload !== 1 ? 's' : ''}

📋 Current Task

${agent.currentTask || 'No active task'}

🛠️ Tools

${agent.tools.length ? agent.tools.slice(0, 5).map(tool => `${escapeHtml(tool)}`).join('') : 'No tools'} ${agent.tools.length > 5 ? `+${agent.tools.length - 5} more` : ''}

📄 Recent Files

${agent.files.length ? agent.files.slice(0, 5).map(file => `${escapeHtml(file)}`).join('') : 'No files'}
`; // Details button cardEl.querySelector('.agent-details-btn').addEventListener('click', () => showAgentDetails(agent)); // Assign button cardEl.querySelector('.agent-assign-btn').addEventListener('click', () => showAssignModal(agent.name)); grid.appendChild(cardEl); }); } // Agent search document.getElementById('agent-search').addEventListener('input', (e) => { const statusFilter = document.getElementById('agent-status-filter').value; renderAgents(e.target.value, statusFilter); }); // Agent status filter document.getElementById('agent-status-filter').addEventListener('change', (e) => { const searchFilter = document.getElementById('agent-search').value; renderAgents(searchFilter, e.target.value); }); // Agent details modal function showAgentDetails(agent) { const modal = document.getElementById('agent-modal'); const body = document.getElementById('modal-agent-body'); document.getElementById('modal-agent-name').textContent = agent.name; body.innerHTML = `

Status

${agent.status}

Workload

${agent.workload} active task${agent.workload !== 1 ? 's' : ''}

Current Task

${agent.currentTask || 'No active task'}

Active Tasks

Recently Completed

Tools

${agent.tools.length ? agent.tools.map(t => `${escapeHtml(t)}`).join('') : 'No tools'}

Capabilities

${agent.capabilities.length ? agent.capabilities.map(c => `${escapeHtml(c)}`).join('') : 'No capabilities defined'}
`; modal.classList.add('active'); } // Close agent modal document.getElementById('modal-close').addEventListener('click', () => { document.getElementById('agent-modal').classList.remove('active'); }); // Assign task modal async function showAssignModal(agentName) { const modal = document.getElementById('assign-modal'); document.getElementById('assign-agent-name').textContent = agentName; // Load unassigned tasks try { const res = await fetch('/api/tasks'); const tasks = await res.json(); const unassignedTasks = tasks.filter(t => t.status !== 'Done' && (!t.assignee || t.assignee === '')); const select = document.getElementById('assign-task-select'); select.innerHTML = ''; unassignedTasks.forEach(task => { const option = document.createElement('option'); option.value = task.id; option.textContent = `${task.title} (${task.priority})`; select.appendChild(option); }); // Store agent name for assignment select.dataset.agent = agentName; modal.classList.add('active'); } catch (err) { console.error('Failed to load tasks for assignment:', err); } } // Close assign modal document.getElementById('assign-modal-close').addEventListener('click', () => { document.getElementById('assign-modal').classList.remove('active'); }); // Confirm assignment document.getElementById('confirm-assign-btn').addEventListener('click', async () => { const select = document.getElementById('assign-task-select'); const taskId = select.value; const agentName = select.dataset.agent; if (!taskId) { alert('Please select a task'); return; } try { const res = await fetch(`/api/agents/${agentName}/assign`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ taskId: parseInt(taskId) }) }); if (res.ok) { document.getElementById('assign-modal').classList.remove('active'); await loadAgents(); await loadTasks(); } } catch (err) { console.error('Failed to assign task:', err); } }); // ============ USAGE ============ let usageStats = null; let providerChart = null; let agentChart = null; async function loadUsage() { const from = document.getElementById('usage-from').value; const to = document.getElementById('usage-to').value; let statsUrl = '/api/usage/stats'; const params = []; if (from) params.push(`from=${from}`); if (to) params.push(`to=${to}`); if (params.length) statsUrl += '?' + params.join('&'); try { // Load stats const statsRes = await fetch(statsUrl); usageStats = await statsRes.json(); // Load basic usage info const usageRes = await fetch('/api/usage'); const usageData = await usageRes.json(); renderUsageStats(); renderUsageCharts(); renderProviderDetails(usageData); } catch (err) { console.error('Failed to load usage:', err); } } function renderUsageStats() { document.getElementById('stat-requests').textContent = usageStats.totalRequests.toLocaleString(); document.getElementById('stat-tokens').textContent = usageStats.totalTokens.toLocaleString(); document.getElementById('stat-cost').textContent = '$' + usageStats.totalCost.toFixed(2); } function renderUsageCharts() { const rootStyles = getComputedStyle(document.documentElement); const themeForeground = rootStyles.getPropertyValue('--fg').trim() || '#e0e0e0'; const themeBorder = rootStyles.getPropertyValue('--border').trim() || '#444'; const themePrimary = rootStyles.getPropertyValue('--primary').trim() || '#3498db'; // Provider chart const providerCtx = document.getElementById('chart-provider').getContext('2d'); if (providerChart) providerChart.destroy(); const providerLabels = Object.keys(usageStats.byProvider); const providerData = providerLabels.map(p => usageStats.byProvider[p].requests); providerChart = new Chart(providerCtx, { type: 'doughnut', data: { labels: providerLabels, datasets: [{ data: providerData, backgroundColor: [ '#3498db', '#2ecc71', '#e74c3c', '#f39c12', '#9c27b0', '#00bcd4' ] }] }, options: { responsive: true, plugins: { legend: { position: 'bottom', labels: { color: themeForeground } } } } }); // Agent chart const agentCtx = document.getElementById('chart-agent').getContext('2d'); if (agentChart) agentChart.destroy(); const agentLabels = Object.keys(usageStats.byAgent); const agentData = agentLabels.map(a => usageStats.byAgent[a].requests); agentChart = new Chart(agentCtx, { type: 'bar', data: { labels: agentLabels, datasets: [{ label: 'Requests', data: agentData, backgroundColor: themePrimary }] }, options: { responsive: true, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true, ticks: { color: themeForeground }, grid: { color: themeBorder } }, x: { ticks: { color: themeForeground }, grid: { color: themeBorder } } } } }); } function renderProviderDetails(usageData) { const grid = document.getElementById('provider-grid'); grid.innerHTML = ''; usageData.providers.forEach(provider => { const providerEl = document.createElement('div'); providerEl.className = 'provider-card'; const providerUsage = usageStats.byProvider[provider.name] || { requests: 0, tokens: 0, cost: 0 }; providerEl.innerHTML = `

${escapeHtml(provider.name)}

Requests ${providerUsage.requests.toLocaleString()}
Tokens ${providerUsage.tokens.toLocaleString()}
Cost $${providerUsage.cost.toFixed(2)}
Models
${provider.models.map(model => `
${escapeHtml(model.name)} ${escapeHtml(model.type)}
`).join('')}
`; grid.appendChild(providerEl); }); } // Apply date filter document.getElementById('usage-apply-filter').addEventListener('click', loadUsage); // Export JSON document.getElementById('export-json').addEventListener('click', () => { const from = document.getElementById('usage-from').value; const to = document.getElementById('usage-to').value; let url = '/api/usage/export?format=json'; if (from) url += `&from=${from}`; if (to) url += `&to=${to}`; window.open(url, '_blank'); }); // Export CSV document.getElementById('export-csv').addEventListener('click', () => { const from = document.getElementById('usage-from').value; const to = document.getElementById('usage-to').value; let url = '/api/usage/export?format=csv'; if (from) url += `&from=${from}`; if (to) url += `&to=${to}`; window.open(url, '_blank'); }); // ============ HELPERS ============ function escapeHtml(text) { if (typeof text !== 'string') return ''; const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; return text.replace(/[&<>"']/g, m => map[m]); } // ============ INITIALIZATION ============ document.addEventListener('DOMContentLoaded', () => { initTheme(); populateAgentDropdown(); loadTasks(); // Set default date range (last 30 days) const to = new Date(); const from = new Date(); from.setDate(from.getDate() - 30); document.getElementById('usage-from').value = from.toISOString().split('T')[0]; document.getElementById('usage-to').value = to.toISOString().split('T')[0]; }); // Close modals on outside click document.querySelectorAll('.modal').forEach(modal => { modal.addEventListener('click', (e) => { if (e.target === modal) { modal.classList.remove('active'); } }); });