306 lines
9.0 KiB
JavaScript
306 lines
9.0 KiB
JavaScript
function getPreferredTheme() {
|
|
const savedTheme = localStorage.getItem('theme');
|
|
if (savedTheme) {
|
|
return savedTheme;
|
|
}
|
|
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
}
|
|
|
|
function setTheme(theme) {
|
|
if (theme === 'dark') {
|
|
document.documentElement.setAttribute('data-theme', 'dark');
|
|
document.getElementById('theme-toggle').textContent = '☀️';
|
|
} else {
|
|
document.documentElement.removeAttribute('data-theme');
|
|
document.getElementById('theme-toggle').textContent = '🌙';
|
|
}
|
|
localStorage.setItem('theme', theme);
|
|
}
|
|
|
|
setTheme(getPreferredTheme());
|
|
|
|
document.getElementById('theme-toggle').addEventListener('click', () => {
|
|
const currentTheme = document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light';
|
|
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
|
setTheme(newTheme);
|
|
});
|
|
|
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
|
|
if (!localStorage.getItem('theme')) {
|
|
setTheme(e.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 = `
|
|
<div class="column-header">
|
|
<h3>${column.title}</h3>
|
|
<span class="column-count">${column.tasks.length}</span>
|
|
</div>
|
|
<div class="cards"></div>
|
|
`;
|
|
|
|
const cardsEl = columnEl.querySelector('.cards');
|
|
|
|
column.tasks.forEach(task => {
|
|
const cardEl = document.createElement('div');
|
|
cardEl.className = 'card';
|
|
cardEl.innerHTML = `
|
|
<div class="card-head">
|
|
<h3 class="card-title">${escapeHtml(task.title)}</h3>
|
|
<span class="badge priority-${task.priority}">${task.priority}</span>
|
|
</div>
|
|
<p class="card-desc">${escapeHtml(task.description || '')}</p>
|
|
<p class="meta assignee">${task.assignee || 'Unassigned'}</p>
|
|
<p class="meta tags">${task.tags.map(t => `<span class="tag">${escapeHtml(t)}</span>`).join(' ')}</p>
|
|
<label>
|
|
<input type="checkbox" class="card-check" ${task.status === 'Done' ? 'checked' : ''} />
|
|
Mark Complete
|
|
</label>
|
|
`;
|
|
|
|
// 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'),
|
|
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();
|
|
});
|
|
|
|
// Wiki
|
|
async function loadWiki() {
|
|
const res = await fetch('/api/wiki');
|
|
const pages = await res.json();
|
|
|
|
const wikiList = document.getElementById('wiki-list');
|
|
wikiList.innerHTML = '';
|
|
|
|
pages.forEach(page => {
|
|
const itemEl = document.createElement('div');
|
|
itemEl.className = 'wiki-item';
|
|
itemEl.innerHTML = `
|
|
<h4 class="wiki-title">${escapeHtml(page.filename.replace('.md', ''))}</h4>
|
|
<p class="wiki-date">${new Date(page.created).toLocaleDateString()}</p>
|
|
`;
|
|
|
|
itemEl.addEventListener('click', async () => {
|
|
// Mark active
|
|
wikiList.querySelectorAll('.wiki-item').forEach(i => i.classList.remove('active'));
|
|
itemEl.classList.add('active');
|
|
|
|
// Load content
|
|
const contentRes = await fetch(`/api/wiki/${page.filename}`);
|
|
const contentData = await contentRes.json();
|
|
|
|
const wikiContent = document.getElementById('wiki-content');
|
|
wikiContent.innerHTML = `<pre>${escapeHtml(contentData.content)}</pre>`;
|
|
});
|
|
|
|
wikiList.appendChild(itemEl);
|
|
});
|
|
}
|
|
|
|
// Agents
|
|
async function loadAgents() {
|
|
const res = await fetch('/api/agents');
|
|
const agents = await res.json();
|
|
|
|
const grid = document.getElementById('agents-grid');
|
|
grid.innerHTML = '';
|
|
|
|
agents.forEach(agent => {
|
|
const cardEl = document.createElement('div');
|
|
cardEl.className = 'agent-card';
|
|
|
|
cardEl.innerHTML = `
|
|
<div class="agent-header">
|
|
<h3 class="agent-name">${escapeHtml(agent.name)}</h3>
|
|
<span class="agent-status">${agent.status}</span>
|
|
</div>
|
|
<div class="agent-body">
|
|
<div class="agent-section">
|
|
<h4>📋 Current Task</h4>
|
|
<p class="agent-task">${agent.currentTask || 'No active task'}</p>
|
|
</div>
|
|
<div class="agent-section">
|
|
<h4>🛠️ Tools</h4>
|
|
<div class="agent-tools">
|
|
${agent.tools.map(tool => `<span class="tool-tag">${escapeHtml(tool)}</span>`).join('')}
|
|
</div>
|
|
</div>
|
|
<div class="agent-section">
|
|
<h4>📄 Workspace Files</h4>
|
|
<div class="agent-files">
|
|
${agent.files.map(file => `<span class="file-tag">${escapeHtml(file)}</span>`).join('')}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
grid.appendChild(cardEl);
|
|
});
|
|
}
|
|
|
|
// Usage
|
|
async function loadUsage() {
|
|
const res = await fetch('/api/usage');
|
|
const usage = await res.json();
|
|
|
|
const container = document.getElementById('usage-container');
|
|
container.innerHTML = '';
|
|
|
|
if (usage.providers.length === 0) {
|
|
container.innerHTML = '<p style="padding: 2rem; color: var(--text-secondary);">No provider data available</p>';
|
|
return;
|
|
}
|
|
|
|
usage.providers.forEach(provider => {
|
|
const cardEl = document.createElement('div');
|
|
cardEl.className = 'usage-card';
|
|
|
|
cardEl.innerHTML = `
|
|
<h3 class="provider-name">${escapeHtml(provider.name)}</h3>
|
|
<div class="provider-models">
|
|
${provider.models.map(model => `
|
|
<div class="model-item">
|
|
<div class="model-name">${escapeHtml(model.name)}</div>
|
|
<div class="model-meta">Type: ${model.type} | Context: ${model.contextWindow}</div>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
<div class="provider-quota">
|
|
<div class="quota-title">Quota & Limits</div>
|
|
<div class="quota-item">
|
|
<span class="quota-label">Requests</span>
|
|
<span class="quota-value">${provider.quota.requests} / ${provider.quota.limit}</span>
|
|
</div>
|
|
<div class="quota-item">
|
|
<span class="quota-label">Tokens</span>
|
|
<span class="quota-value">${provider.quota.tokens}</span>
|
|
</div>
|
|
<div class="quota-bar">
|
|
<div class="quota-fill" style="width: 0%"></div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
container.appendChild(cardEl);
|
|
});
|
|
}
|
|
|
|
// Utility functions
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
// Initialize
|
|
loadTasks();
|
|
|
|
// WebSocket for real-time updates
|
|
const ws = new WebSocket(`ws://${window.location.host}`);
|
|
ws.onmessage = (event) => {
|
|
const data = JSON.parse(event.data);
|
|
if (data.type === 'task_created' || data.type === 'task_updated') {
|
|
loadTasks();
|
|
}
|
|
};
|