Complete dashboard revamp: modern glassmorphism UI, all 12 pages functional
- Complete CSS redesign with glassmorphism, gradients, animations - All 12 SPA pages rewritten with working forms, modals, charts - Fixed server.py static file serving for dashboard assets - Enhanced utils: modal system, toast with icons, skeletons - Dashboard: stat cards, agent status, activity feed, quick run - Skills: grid/list views, filter, detail with performance chart - Memory: browse/edit brain files inline - Scheduler: CRUD jobs with cron input - Audit: filterable log viewer - Cost: Chart.js agent/time charts, test recording, free-tier alerts - Plugins: registry browser with install flow - Backups: create/restore with confirmation dialog - Prompts: card grid with preview, copy-to-clipboard - Standards: index + detail cards, pattern discovery - Settings: agent toggle, API keys, free-tier limits, dark mode - Setup Wizard: 4-step guided configuration
This commit is contained in:
parent
0c08c0c9fb
commit
43c3413bca
|
|
@ -1,2 +1,3 @@
|
|||
{"action": "skill_run", "skill": "heartbeat", "agent": "opencode", "run_id": "d52d517a", "timestamp": "2026-05-17T09:58:07.559900+00:00", "id": "36284cbc"}
|
||||
{"action": "skill_run", "skill": "devops-audit", "agent": "opencode", "run_id": "92cd4f97", "timestamp": "2026-05-17T09:59:08.213032+00:00", "id": "40695cd7"}
|
||||
{"action": "skill_run", "skill": "heartbeat", "agent": "opencode", "run_id": "f18c903b", "timestamp": "2026-05-17T10:18:44.356138+00:00", "id": "c9117c6d"}
|
||||
|
|
|
|||
|
|
@ -1,48 +1,35 @@
|
|||
const api = {
|
||||
async get(path) {
|
||||
const r = await fetch(path);
|
||||
if (!r.ok) throw new Error(`GET ${path} failed: ${r.status}`);
|
||||
if (!r.ok) { const e = await r.json().catch(() => ({})); throw new Error(e.detail || `Request failed: ${r.status}`); }
|
||||
return r.json();
|
||||
},
|
||||
|
||||
async post(path, body = {}) {
|
||||
const r = await fetch(path, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (!r.ok) throw new Error(`POST ${path} failed: ${r.status}`);
|
||||
const r = await fetch(path, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
||||
if (!r.ok) { const e = await r.json().catch(() => ({})); throw new Error(e.detail || `Request failed: ${r.status}`); }
|
||||
return r.json();
|
||||
},
|
||||
|
||||
async put(path, body = {}) {
|
||||
const r = await fetch(path, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (!r.ok) throw new Error(`PUT ${path} failed: ${r.status}`);
|
||||
const r = await fetch(path, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
||||
if (!r.ok) { const e = await r.json().catch(() => ({})); throw new Error(e.detail || `Request failed: ${r.status}`); }
|
||||
return r.json();
|
||||
},
|
||||
|
||||
async del(path) {
|
||||
const r = await fetch(path, { method: 'DELETE' });
|
||||
if (!r.ok) throw new Error(`DELETE ${path} failed: ${r.status}`);
|
||||
if (!r.ok) { const e = await r.json().catch(() => ({})); throw new Error(e.detail || `Request failed: ${r.status}`); }
|
||||
return r.json();
|
||||
},
|
||||
|
||||
// Convenience methods
|
||||
getStatus: () => api.get('/api/status'),
|
||||
getBrain: () => api.get('/api/brain'),
|
||||
getBrainFile: (name) => api.get(`/api/brain/${name}`),
|
||||
updateBrainFile: (name, content) => api.put(`/api/brain/${name}`, { content }),
|
||||
getBrainFile: (name) => api.get(`/api/brain/${encodeURIComponent(name)}`),
|
||||
updateBrainFile: (name, content) => api.put(`/api/brain/${encodeURIComponent(name)}`, { content }),
|
||||
getSkills: () => api.get('/api/skills'),
|
||||
getSkill: (name) => api.get(`/api/skills/${name}`),
|
||||
runSkill: (name, input = '', agent = 'auto') => api.post(`/api/skills/${name}/run`, { input, agent }),
|
||||
getSkillEval: (name) => api.get(`/api/skills/${name}/eval`),
|
||||
getSkill: (name) => api.get(`/api/skills/${encodeURIComponent(name)}`),
|
||||
runSkill: (name, input = '', agent = 'auto') => api.post(`/api/skills/${encodeURIComponent(name)}/run`, { input, agent }),
|
||||
getSkillEval: (name) => api.get(`/api/skills/${encodeURIComponent(name)}/eval`),
|
||||
getJobs: () => api.get('/api/scheduler/jobs'),
|
||||
createJob: (job) => api.post('/api/scheduler/jobs', job),
|
||||
deleteJob: (id) => api.del(`/api/scheduler/jobs/${id}`),
|
||||
deleteJob: (id) => api.del(`/api/scheduler/jobs/${encodeURIComponent(id)}`),
|
||||
getAudit: (limit = 100) => api.get(`/api/audit?limit=${limit}`),
|
||||
getCost: () => api.get('/api/cost'),
|
||||
recordCost: (data) => api.post('/api/cost/record', data),
|
||||
|
|
|
|||
128
dashboard/app.js
128
dashboard/app.js
|
|
@ -1,11 +1,22 @@
|
|||
const pageCache = {};
|
||||
|
||||
async function loadPage(name) {
|
||||
if (pageCache[name]) return pageCache[name];
|
||||
try {
|
||||
await loadScript(`pages/${name}.js`);
|
||||
pageCache[name] = true;
|
||||
} catch (err) {
|
||||
console.error(`Failed to load page: ${name}`, err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
function loadScript(src) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (pageCache[src]) { resolve(pageCache[src]); return; }
|
||||
if (document.querySelector(`script[src="${src}"]`)) { resolve(); return; }
|
||||
const script = document.createElement('script');
|
||||
script.src = src;
|
||||
script.onload = () => { pageCache[src] = true; resolve(); };
|
||||
script.onload = () => resolve();
|
||||
script.onerror = () => reject(new Error(`Failed to load ${src}`));
|
||||
document.body.appendChild(script);
|
||||
});
|
||||
|
|
@ -13,104 +24,53 @@ function loadScript(src) {
|
|||
|
||||
async function navigate(page) {
|
||||
const hash = page || window.location.hash.slice(1) || 'dashboard';
|
||||
const content = document.getElementById('pageContent');
|
||||
if (!hash) { window.location.hash = 'dashboard'; return; }
|
||||
|
||||
document.querySelectorAll('.nav-item').forEach(el => el.classList.remove('active'));
|
||||
const navItem = document.querySelector(`[data-page="${hash}"]`);
|
||||
if (navItem) navItem.classList.add('active');
|
||||
|
||||
content.innerHTML = '<div class="loading">Loading...</div>';
|
||||
const info = PAGE_TITLES[hash] || { title: 'Unknown', breadcrumb: '' };
|
||||
document.getElementById('pageTitle').textContent = info.title;
|
||||
document.getElementById('pageBreadcrumb').textContent = info.breadcrumb;
|
||||
|
||||
const content = document.getElementById('pageContent');
|
||||
content.innerHTML = `<div class="loading"><div class="loading-spinner"></div><span>Loading ${info.title}...</span></div>`;
|
||||
|
||||
try {
|
||||
switch (hash) {
|
||||
case 'dashboard':
|
||||
content.innerHTML = '<div class="loading">Loading dashboard...</div>';
|
||||
await loadScript('pages/dashboard.js');
|
||||
content.innerHTML = '';
|
||||
renderDashboard();
|
||||
break;
|
||||
case 'skills':
|
||||
content.innerHTML = '<div class="loading">Loading skills...</div>';
|
||||
await loadScript('pages/skills.js');
|
||||
content.innerHTML = '';
|
||||
renderSkills();
|
||||
break;
|
||||
case 'memory':
|
||||
content.innerHTML = '<div class="loading">Loading memory...</div>';
|
||||
await loadScript('pages/memory.js');
|
||||
content.innerHTML = '';
|
||||
renderMemory();
|
||||
break;
|
||||
case 'scheduler':
|
||||
content.innerHTML = '<div class="loading">Loading scheduler...</div>';
|
||||
await loadScript('pages/scheduler.js');
|
||||
content.innerHTML = '';
|
||||
renderScheduler();
|
||||
break;
|
||||
case 'audit':
|
||||
content.innerHTML = '<div class="loading">Loading audit...</div>';
|
||||
await loadScript('pages/audit.js');
|
||||
content.innerHTML = '';
|
||||
renderAudit();
|
||||
break;
|
||||
case 'cost':
|
||||
content.innerHTML = '<div class="loading">Loading cost analytics...</div>';
|
||||
await loadScript('pages/cost-analytics.js');
|
||||
content.innerHTML = '';
|
||||
renderCostAnalytics();
|
||||
break;
|
||||
case 'plugins':
|
||||
content.innerHTML = '<div class="loading">Loading plugins...</div>';
|
||||
await loadScript('pages/plugins.js');
|
||||
content.innerHTML = '';
|
||||
renderPlugins();
|
||||
break;
|
||||
case 'backups':
|
||||
content.innerHTML = '<div class="loading">Loading backups...</div>';
|
||||
await loadScript('pages/backups.js');
|
||||
content.innerHTML = '';
|
||||
renderBackups();
|
||||
break;
|
||||
case 'prompts':
|
||||
content.innerHTML = '<div class="loading">Loading prompts...</div>';
|
||||
await loadScript('pages/prompts.js');
|
||||
content.innerHTML = '';
|
||||
renderPrompts();
|
||||
break;
|
||||
case 'standards':
|
||||
content.innerHTML = '<div class="loading">Loading standards...</div>';
|
||||
await loadScript('pages/standards.js');
|
||||
content.innerHTML = '';
|
||||
renderStandards();
|
||||
break;
|
||||
case 'settings':
|
||||
content.innerHTML = '<div class="loading">Loading settings...</div>';
|
||||
await loadScript('pages/settings.js');
|
||||
content.innerHTML = '';
|
||||
renderSettings();
|
||||
break;
|
||||
default:
|
||||
content.innerHTML = '<div class="empty-state"><div class="icon">🔍</div><h3>Page not found</h3></div>';
|
||||
await loadPage(hash);
|
||||
const renderFn = window[`render${capitalize(hash.replace(/-./g, m => m[1].toUpperCase()))}`];
|
||||
if (renderFn) {
|
||||
content.innerHTML = '';
|
||||
content.className = 'page-content page-enter';
|
||||
await renderFn();
|
||||
} else {
|
||||
content.innerHTML = `<div class="empty-state"><div class="empty-state-icon">🔍</div><div class="empty-state-title">Page not found</div><div class="empty-state-desc">The page "${hash}" doesn't have a render function</div></div>`;
|
||||
}
|
||||
} catch (err) {
|
||||
content.innerHTML = `<div class="empty-state"><div class="icon">⚠</div><h3>Error loading page</h3><p>${escapeHtml(err.message)}</p></div>`;
|
||||
console.error(err);
|
||||
content.innerHTML = `<div class="empty-state"><div class="empty-state-icon">⚠</div><div class="empty-state-title">Failed to load</div><div class="empty-state-desc">${escapeHtml(err.message)}</div><button class="btn btn-primary mt-3" onclick="navigate('dashboard')">Go to Dashboard</button></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function capitalize(str) { return str.charAt(0).toUpperCase() + str.slice(1); }
|
||||
|
||||
async function updateAgentStatus() {
|
||||
try {
|
||||
const status = await api.getStatus();
|
||||
const el = document.getElementById('agentStatus');
|
||||
const agents = status.agents || [];
|
||||
const allOnline = agents.every(a => a.status === 'online');
|
||||
const anyOnline = agents.some(a => a.status === 'online');
|
||||
el.innerHTML = `
|
||||
<div class="status-dot ${allOnline ? 'online' : anyOnline ? 'warning' : 'offline'}"></div>
|
||||
<span>${agents.filter(a => a.status === 'online').length}/${agents.length} agents online</span>
|
||||
`;
|
||||
const bar = document.getElementById('agentStatusBar');
|
||||
const online = agents.filter(a => a.status === 'online').length;
|
||||
const total = agents.length;
|
||||
const dot = bar.querySelector('.agent-dot');
|
||||
if (online === total) { dot.className = 'agent-dot online'; bar.querySelector('span').textContent = 'All agents online'; }
|
||||
else if (online > 0) { dot.className = 'agent-dot warning'; bar.querySelector('span').textContent = `${online}/${total} online`; }
|
||||
else { dot.className = 'agent-dot offline'; bar.querySelector('span').textContent = 'All agents offline'; }
|
||||
|
||||
const badge = document.getElementById('skillCount');
|
||||
if (badge && status.skills_count !== undefined) badge.textContent = status.skills_count;
|
||||
} catch {
|
||||
document.getElementById('agentStatus').innerHTML = '<div class="status-dot offline"></div><span>Offline</span>';
|
||||
const bar = document.getElementById('agentStatusBar');
|
||||
if (bar) { bar.querySelector('.agent-dot').className = 'agent-dot offline'; bar.querySelector('span').textContent = 'Disconnected'; }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -119,5 +79,5 @@ window.addEventListener('DOMContentLoaded', () => {
|
|||
loadTheme();
|
||||
navigate(window.location.hash.slice(1) || 'dashboard');
|
||||
updateAgentStatus();
|
||||
setInterval(updateAgentStatus, 30000);
|
||||
setInterval(updateAgentStatus, 15000);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,74 +4,70 @@
|
|||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Agentic OS</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<nav id="sidebar" class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<div class="logo">
|
||||
<span class="logo-icon">⬡</span>
|
||||
<span class="logo-text">Agentic OS</span>
|
||||
<nav id="sidebar" class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<div class="logo">
|
||||
<div class="logo-icon">⬡</div>
|
||||
<span class="logo-text">Agentic OS</span>
|
||||
</div>
|
||||
<button class="sidebar-toggle" onclick="toggleSidebar()" title="Toggle sidebar">☰</button>
|
||||
</div>
|
||||
<button class="sidebar-toggle" onclick="toggleSidebar()">≡</button>
|
||||
</div>
|
||||
<div class="sidebar-nav" id="sidebarNav">
|
||||
<a href="#dashboard" class="nav-item active" data-page="dashboard" onclick="navigate('dashboard')">
|
||||
<span class="nav-icon">◉</span> Dashboard
|
||||
</a>
|
||||
<a href="#skills" class="nav-item" data-page="skills" onclick="navigate('skills')">
|
||||
<span class="nav-icon">⚡</span> Skills
|
||||
</a>
|
||||
<a href="#memory" class="nav-item" data-page="memory" onclick="navigate('memory')">
|
||||
<span class="nav-icon">🧠</span> Memory
|
||||
</a>
|
||||
<a href="#scheduler" class="nav-item" data-page="scheduler" onclick="navigate('scheduler')">
|
||||
<span class="nav-icon">⏱</span> Scheduler
|
||||
</a>
|
||||
<a href="#audit" class="nav-item" data-page="audit" onclick="navigate('audit')">
|
||||
<span class="nav-icon">📋</span> Audit
|
||||
</a>
|
||||
<a href="#cost" class="nav-item" data-page="cost" onclick="navigate('cost')">
|
||||
<span class="nav-icon">💰</span> Cost Analytics
|
||||
</a>
|
||||
<a href="#plugins" class="nav-item" data-page="plugins" onclick="navigate('plugins')">
|
||||
<span class="nav-icon">🔌</span> Plugins
|
||||
</a>
|
||||
<a href="#backups" class="nav-item" data-page="backups" onclick="navigate('backups')">
|
||||
<span class="nav-icon">💾</span> Backups
|
||||
</a>
|
||||
<a href="#prompts" class="nav-item" data-page="prompts" onclick="navigate('prompts')">
|
||||
<span class="nav-icon">📝</span> Prompts
|
||||
</a>
|
||||
<a href="#standards" class="nav-item" data-page="standards" onclick="navigate('standards')">
|
||||
<span class="nav-icon">📐</span> Standards
|
||||
</a>
|
||||
<a href="#settings" class="nav-item" data-page="settings" onclick="navigate('settings')">
|
||||
<span class="nav-icon">⚙</span> Settings
|
||||
</a>
|
||||
</div>
|
||||
<div class="sidebar-footer">
|
||||
<div class="agent-status" id="agentStatus">
|
||||
<div class="status-dot offline"></div>
|
||||
<span>Loading...</span>
|
||||
<div class="sidebar-nav">
|
||||
<div class="sidebar-section"><div class="sidebar-section-label">Core</div></div>
|
||||
<a href="#dashboard" class="nav-item active" data-page="dashboard"><span class="nav-icon">◉</span><span class="nav-label">Dashboard</span></a>
|
||||
<a href="#skills" class="nav-item" data-page="skills"><span class="nav-icon">⚡</span><span class="nav-label">Skills</span><span class="nav-badge" id="skillCount">0</span></a>
|
||||
<a href="#memory" class="nav-item" data-page="memory"><span class="nav-icon">🧠</span><span class="nav-label">Memory</span></a>
|
||||
<a href="#scheduler" class="nav-item" data-page="scheduler"><span class="nav-icon">⏱</span><span class="nav-label">Scheduler</span></a>
|
||||
<a href="#audit" class="nav-item" data-page="audit"><span class="nav-icon">📋</span><span class="nav-label">Audit</span></a>
|
||||
<div class="sidebar-section"><div class="sidebar-section-label">Management</div></div>
|
||||
<a href="#cost" class="nav-item" data-page="cost"><span class="nav-icon">💰</span><span class="nav-label">Cost Analytics</span></a>
|
||||
<a href="#plugins" class="nav-item" data-page="plugins"><span class="nav-icon">🔌</span><span class="nav-label">Plugins</span></a>
|
||||
<a href="#backups" class="nav-item" data-page="backups"><span class="nav-icon">💾</span><span class="nav-label">Backups</span></a>
|
||||
<div class="sidebar-section"><div class="sidebar-section-label">Resources</div></div>
|
||||
<a href="#prompts" class="nav-item" data-page="prompts"><span class="nav-icon">📝</span><span class="nav-label">Prompts</span></a>
|
||||
<a href="#standards" class="nav-item" data-page="standards"><span class="nav-icon">📐</span><span class="nav-label">Standards</span></a>
|
||||
<a href="#settings" class="nav-item" data-page="settings"><span class="nav-icon">⚙</span><span class="nav-label">Settings</span></a>
|
||||
<div class="sidebar-section"><div class="sidebar-section-label">Setup</div></div>
|
||||
<a href="#setup-wizard" class="nav-item" data-page="setup-wizard"><span class="nav-icon">🚀</span><span class="nav-label">Setup Wizard</span></a>
|
||||
</div>
|
||||
<div class="theme-toggle">
|
||||
<button onclick="toggleTheme()" title="Toggle theme">🌓</button>
|
||||
<div class="sidebar-footer">
|
||||
<div class="agent-status-bar" id="agentStatusBar">
|
||||
<div class="agent-dot offline"></div>
|
||||
<span class="agent-status-text">Connecting...</span>
|
||||
</div>
|
||||
<button class="theme-btn" onclick="toggleTheme()" title="Toggle theme"><span class="theme-label">🌓</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</nav>
|
||||
|
||||
<main id="mainContent" class="main-content">
|
||||
<div id="pageContent" class="page-content">
|
||||
<div class="loading">Loading Agentic OS...</div>
|
||||
</div>
|
||||
</main>
|
||||
<main class="main-content">
|
||||
<div class="topbar">
|
||||
<div class="topbar-left">
|
||||
<span class="topbar-title" id="pageTitle">Dashboard</span>
|
||||
<span class="topbar-breadcrumb" id="pageBreadcrumb">Overview</span>
|
||||
</div>
|
||||
<div class="topbar-right">
|
||||
<div class="topbar-search">
|
||||
<span>🔍</span>
|
||||
<input type="text" placeholder="Search skills..." id="globalSearch" oninput="handleGlobalSearch(this.value)">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="pageContent" class="page-content">
|
||||
<div class="loading"><div class="loading-spinner"></div><span>Loading Agentic OS...</span></div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div id="toastContainer" class="toast-container"></div>
|
||||
<div id="toastContainer" class="toast-container"></div>
|
||||
<div id="modalContainer"></div>
|
||||
|
||||
<script src="utils.js"></script>
|
||||
<script src="api.js"></script>
|
||||
<script src="app.js"></script>
|
||||
<script src="utils.js"></script>
|
||||
<script src="api.js"></script>
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -2,62 +2,84 @@ async function renderAudit() {
|
|||
const content = document.getElementById('pageContent');
|
||||
content.innerHTML = `
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">Audit Trail</h1>
|
||||
<p class="page-subtitle">Complete history of all agent actions</p>
|
||||
<div class="page-header-left">
|
||||
<h1 class="page-title">Audit Log</h1>
|
||||
<p class="page-subtitle">Complete system activity trail</p>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px">
|
||||
<select id="auditFilter" onchange="renderAudit()" style="width:auto">
|
||||
<option value="all">All Actions</option>
|
||||
<option value="skill_run">Skill Runs</option>
|
||||
<option value="brain_update">Memory Updates</option>
|
||||
<option value="backup_created">Backups</option>
|
||||
<option value="settings_updated">Settings</option>
|
||||
<option value="job_created">Jobs</option>
|
||||
</select>
|
||||
<input id="auditSearch" placeholder="Search..." style="width:200px" oninput="renderAudit()">
|
||||
<div class="btn-group">
|
||||
<button class="btn" onclick="refreshAudit()">🔄 Refresh</button>
|
||||
<button class="btn" onclick="clearAuditFilters()">✕ Clear Filters</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="auditEntries"><div class="loading">Loading...</div></div>
|
||||
<div class="flex gap-2 mb-3" style="flex-wrap:wrap">
|
||||
<input id="auditFilter" class="form-input" style="width:200px" placeholder="Filter by keyword..." oninput="applyAuditFilter()">
|
||||
<select id="auditActionFilter" class="form-select" style="width:150px" onchange="applyAuditFilter()">
|
||||
<option value="">All Actions</option>
|
||||
<option value="skill_run">Skill Run</option>
|
||||
<option value="brain_update">Brain Update</option>
|
||||
<option value="job_created">Job Created</option>
|
||||
<option value="job_deleted">Job Deleted</option>
|
||||
<option value="backup_created">Backup Created</option>
|
||||
<option value="backup_restored">Backup Restored</option>
|
||||
<option value="plugin_installed">Plugin Installed</option>
|
||||
<option value="settings_updated">Settings Updated</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="auditTable"><div class="loading"><div class="loading-spinner"></div></div></div>
|
||||
`;
|
||||
|
||||
await refreshAudit();
|
||||
}
|
||||
|
||||
let _allAuditEntries = [];
|
||||
|
||||
async function refreshAudit() {
|
||||
const container = document.getElementById('auditTable');
|
||||
container.innerHTML = '<div class="loading"><div class="loading-spinner"></div></div>';
|
||||
try {
|
||||
const audit = await api.getAudit(200);
|
||||
let entries = audit.entries || [];
|
||||
|
||||
const filter = document.getElementById('auditFilter')?.value || 'all';
|
||||
const search = (document.getElementById('auditSearch')?.value || '').toLowerCase();
|
||||
|
||||
if (filter !== 'all') entries = entries.filter(e => e.action === filter);
|
||||
if (search) entries = entries.filter(e => JSON.stringify(e).toLowerCase().includes(search));
|
||||
|
||||
const container = document.getElementById('auditEntries');
|
||||
if (entries.length === 0) {
|
||||
container.innerHTML = '<div class="empty-state"><div class="icon">📋</div><h3>No audit entries</h3></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="card" style="padding:0">
|
||||
<div class="table-wrapper">
|
||||
<table>
|
||||
<thead><tr><th>Time</th><th>Action</th><th>Details</th><th>ID</th></tr></thead>
|
||||
<tbody>
|
||||
${entries.reverse().map(e => `
|
||||
<tr>
|
||||
<td style="white-space:nowrap;font-size:11px">${formatDate(e.timestamp)}</td>
|
||||
<td><span class="badge badge-info">${e.action}</span></td>
|
||||
<td>${e.skill || e.file || e.job || e.plugin || e.job_id || '-'}</td>
|
||||
<td style="font-size:11px;color:var(--text2)">${e.id || '-'}</td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div style="font-size:12px;color:var(--text2);text-align:right">${entries.length} entries</div>
|
||||
`;
|
||||
const r = await api.getAudit(200);
|
||||
_allAuditEntries = r.entries || [];
|
||||
applyAuditFilter();
|
||||
} catch (err) {
|
||||
document.getElementById('auditEntries').innerHTML = `<div class="empty-state"><div class="icon">⚠</div><p>${escapeHtml(err.message)}</p></div>`;
|
||||
container.innerHTML = `<div class="empty-state"><div class="empty-state-icon">⚠</div><div class="empty-state-title">${escapeHtml(err.message)}</div></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function applyAuditFilter() {
|
||||
const q = (document.getElementById('auditFilter').value || '').toLowerCase();
|
||||
const action = document.getElementById('auditActionFilter').value;
|
||||
let filtered = _allAuditEntries;
|
||||
if (q) filtered = filtered.filter(e => JSON.stringify(e).toLowerCase().includes(q));
|
||||
if (action) filtered = filtered.filter(e => e.action === action);
|
||||
|
||||
const container = document.getElementById('auditTable');
|
||||
if (filtered.length === 0) {
|
||||
container.innerHTML = '<div class="empty-state"><div class="empty-state-icon">📭</div><div class="empty-state-title">No audit entries found</div></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="table-wrapper">
|
||||
<table>
|
||||
<thead><tr><th>Time</th><th>Action</th><th>Details</th><th>ID</th></tr></thead>
|
||||
<tbody>
|
||||
${filtered.slice(0, 100).map(e => `
|
||||
<tr>
|
||||
<td style="font-size:12px;white-space:nowrap">${formatDate(e.timestamp)}</td>
|
||||
<td><span class="badge ${e.action === 'skill_run' ? 'badge-success' : e.action === 'brain_update' ? 'badge-info' : e.action === 'backup_created' ? 'badge-accent' : 'badge-warning'}">${e.action}</span></td>
|
||||
<td style="font-size:13px">${e.skill ? `<strong>${e.skill}</strong>` : ''}${e.file ? `File: ${e.file}` : ''}${e.job ? `Job: ${e.job}` : ''}${e.plugin ? `Plugin: ${e.plugin}` : ''}</td>
|
||||
<td style="font-size:11px;color:var(--text-muted);font-family:var(--font-mono)">${e.id || ''}</td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div style="font-size:12px;color:var(--text-muted);text-align:right;margin-top:8px">${filtered.length > 100 ? 'Showing 100 of ' : ''}${filtered.length} entries</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function clearAuditFilters() {
|
||||
document.getElementById('auditFilter').value = '';
|
||||
document.getElementById('auditActionFilter').value = '';
|
||||
applyAuditFilter();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@ async function renderBackups() {
|
|||
const content = document.getElementById('pageContent');
|
||||
content.innerHTML = `
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<div class="page-header-left">
|
||||
<h1 class="page-title">Backups</h1>
|
||||
<p class="page-subtitle">System snapshots and disaster recovery</p>
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="createBackup()">+ New Backup</button>
|
||||
</div>
|
||||
<div id="backupList"><div class="loading">Loading...</div></div>
|
||||
<div id="backupList"><div class="loading"><div class="loading-spinner"></div></div></div>
|
||||
`;
|
||||
|
||||
try {
|
||||
|
|
@ -16,7 +16,7 @@ async function renderBackups() {
|
|||
const container = document.getElementById('backupList');
|
||||
|
||||
if (backups.length === 0) {
|
||||
container.innerHTML = '<div class="empty-state"><div class="icon">💾</div><h3>No backups yet</h3><p>Create your first backup to protect your system</p><button class="btn btn-primary" style="margin-top:12px" onclick="createBackup()">Create Backup</button></div>';
|
||||
container.innerHTML = '<div class="empty-state"><div class="empty-state-icon">💾</div><div class="empty-state-title">No backups yet</div><div class="empty-state-desc">Create your first backup to protect your system configuration</div><button class="btn btn-primary mt-3" onclick="createBackup()">Create Backup</button></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -29,26 +29,24 @@ async function renderBackups() {
|
|||
<tr>
|
||||
<td><strong>${b.name}</strong></td>
|
||||
<td>${formatBytes(b.size)}</td>
|
||||
<td>${formatDate(b.created)}</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-danger" onclick="restoreBackup('${b.name}')">Restore</button>
|
||||
</td>
|
||||
<td style="font-size:12px">${formatDate(b.created)}</td>
|
||||
<td><button class="btn btn-sm btn-danger" onclick="restoreBackup('${b.name}')">Restore</button></td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div style="font-size:12px;color:var(--text2);text-align:right;margin-top:8px">${backups.length} backups</div>
|
||||
<div style="font-size:12px;color:var(--text-muted);text-align:right;margin-top:8px">${backups.length} backup${backups.length !== 1 ? 's' : ''}</div>
|
||||
`;
|
||||
} catch (err) {
|
||||
document.getElementById('backupList').innerHTML = `<div class="empty-state"><div class="icon">⚠</div><p>${escapeHtml(err.message)}</p></div>`;
|
||||
document.getElementById('backupList').innerHTML = `<div class="empty-state"><div class="empty-state-icon">⚠</div><div class="empty-state-title">${escapeHtml(err.message)}</div></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function createBackup() {
|
||||
try {
|
||||
const r = await api.createBackup();
|
||||
showToast(`Backup created: ${r.file}`, 'success');
|
||||
showToast(`Backup created: ${r.file} (${formatBytes(r.size)})`, 'success');
|
||||
renderBackups();
|
||||
} catch (err) {
|
||||
showToast(`Error: ${err.message}`, 'error');
|
||||
|
|
@ -56,10 +54,23 @@ async function createBackup() {
|
|||
}
|
||||
|
||||
async function restoreBackup(name) {
|
||||
if (!confirm(`Restore backup: ${name}? This will overwrite current data.`)) return;
|
||||
showModal('Restore Backup', `
|
||||
<p style="font-size:13px;color:var(--text-secondary);margin-bottom:8px">Restore <strong>${name}</strong>? This will overwrite current brain, skills, agents, registry, standards, and prompts data.</p>
|
||||
<div class="card" style="background:var(--red-dim);border-color:transparent">
|
||||
<div class="flex items-center gap-2"><span>⚠</span><span style="font-size:13px;font-weight:500">This action cannot be undone</span></div>
|
||||
</div>
|
||||
`, `
|
||||
<button class="btn btn-ghost" onclick="closeModal()">Cancel</button>
|
||||
<button class="btn btn-danger" onclick="confirmRestore('${name}')">Restore</button>
|
||||
`);
|
||||
}
|
||||
|
||||
async function confirmRestore(name) {
|
||||
try {
|
||||
const r = await api.restoreBackup(name);
|
||||
closeModal();
|
||||
showToast('Backup restored successfully', 'success');
|
||||
renderBackups();
|
||||
} catch (err) {
|
||||
showToast(`Error: ${err.message}`, 'error');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,126 +1,158 @@
|
|||
let costChart = null;
|
||||
|
||||
async function renderCostAnalytics() {
|
||||
async function renderCost() {
|
||||
const content = document.getElementById('pageContent');
|
||||
content.innerHTML = `
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<div class="page-header-left">
|
||||
<h1 class="page-title">Cost Analytics</h1>
|
||||
<p class="page-subtitle">Token usage and spending across all agents</p>
|
||||
<p class="page-subtitle">Track API usage and spending across agents</p>
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="recordTestCost()">+ Record Test</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-4" id="costCards">
|
||||
<div class="card"><div class="card-value" id="totalTokens">0</div><div class="card-label">Total Tokens</div></div>
|
||||
<div class="card"><div class="card-value" id="todayTokens">0</div><div class="card-label">Today's Tokens</div></div>
|
||||
<div class="card"><div class="card-value" id="estimatedMonthly">$0</div><div class="card-label">Est. Monthly</div></div>
|
||||
<div class="card"><div class="card-value" id="freeTierStatus" style="font-size:24px">Free</div><div class="card-label">Free Tier Status</div></div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-3 mb-4" id="costStats"></div>
|
||||
<div class="grid grid-2">
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-title">Usage by Agent</span></div>
|
||||
<div id="costAgentBreakdown"></div>
|
||||
<div class="chart-container"><canvas id="agentChart"></canvas></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-title">Usage Trend</span></div>
|
||||
<div class="chart-container"><canvas id="costChart"></canvas></div>
|
||||
<div class="card-header"><span class="card-title">Usage Over Time</span></div>
|
||||
<div class="chart-container"><canvas id="timeChart"></canvas></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-title">Free Tier Alerts</span></div>
|
||||
<div id="freeTierAlerts"></div>
|
||||
<div class="card mt-3">
|
||||
<div class="card-header"><span class="card-title">Recent Cost Entries</span></div>
|
||||
<div id="costEntries"><div class="loading"><div class="loading-spinner"></div></div></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
try {
|
||||
const cost = await api.getCost();
|
||||
const entries = cost.entries || [];
|
||||
const data = await api.getCost();
|
||||
const entries = data.entries || [];
|
||||
const totals = data.daily_totals || {};
|
||||
const alerts = data.free_tier_alerts || [];
|
||||
|
||||
// Aggregate
|
||||
const totalTokens = entries.reduce((sum, e) => sum + (e.tokens || 0), 0);
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const todayTokens = entries.filter(e => e.timestamp && e.timestamp.startsWith(today)).reduce((sum, e) => sum + (e.tokens || 0), 0);
|
||||
const totalCost = entries.reduce((s, e) => s + (e.cost || 0), 0);
|
||||
const totalTokens = entries.reduce((s, e) => s + (e.tokens || 0), 0);
|
||||
const days = Object.keys(totals).length;
|
||||
|
||||
document.getElementById('totalTokens').textContent = totalTokens.toLocaleString();
|
||||
document.getElementById('todayTokens').textContent = todayTokens.toLocaleString();
|
||||
document.getElementById('estimatedMonthly').textContent = `$${(totalTokens * 0.000002).toFixed(2)}`;
|
||||
document.getElementById('costStats').innerHTML = `
|
||||
<div class="card stat-card"><div class="stat-icon purple">💰</div><div class="stat-value">$${totalCost.toFixed(4)}</div><div class="stat-label">Total Cost</div></div>
|
||||
<div class="card stat-card"><div class="stat-icon blue">🔤</div><div class="stat-value">${totalTokens.toLocaleString()}</div><div class="stat-label">Total Tokens</div></div>
|
||||
<div class="card stat-card"><div class="stat-icon ${alerts.length > 0 ? 'red' : 'green'}">${alerts.length > 0 ? '⚠' : '✓'}</div><div class="stat-value">${alerts.length}</div><div class="stat-label">Free Tier Alerts</div></div>
|
||||
`;
|
||||
|
||||
// Agent breakdown
|
||||
const byAgent = {};
|
||||
entries.forEach(e => {
|
||||
const agent = e.agent || 'unknown';
|
||||
byAgent[agent] = (byAgent[agent] || 0) + (e.tokens || 0);
|
||||
});
|
||||
const breakdown = document.getElementById('costAgentBreakdown');
|
||||
Object.entries(byAgent).forEach(([agent, tokens]) => {
|
||||
const pct = totalTokens > 0 ? (tokens / totalTokens * 100).toFixed(1) : 0;
|
||||
breakdown.innerHTML += `
|
||||
<div style="margin-bottom:12px">
|
||||
<div style="display:flex;justify-content:space-between;font-size:13px;margin-bottom:4px">
|
||||
<strong>${agent}</strong>
|
||||
<span>${tokens.toLocaleString()} (${pct}%)</span>
|
||||
</div>
|
||||
<div style="height:8px;background:var(--bg3);border-radius:4px;overflow:hidden">
|
||||
<div style="height:100%;width:${pct}%;background:var(--accent);border-radius:4px;transition:width 0.5s"></div>
|
||||
</div>
|
||||
const entriesContainer = document.getElementById('costEntries');
|
||||
if (entries.length === 0) {
|
||||
entriesContainer.innerHTML = '<div class="empty-state" style="padding:20px"><div class="empty-state-icon">📊</div><div class="empty-state-title">No cost data yet</div></div>';
|
||||
} else {
|
||||
entriesContainer.innerHTML = `
|
||||
<div class="table-wrapper">
|
||||
<table>
|
||||
<thead><tr><th>Time</th><th>Agent</th><th>Model</th><th>Tokens</th><th>Cost</th></tr></thead>
|
||||
<tbody>
|
||||
${entries.slice(-20).reverse().map(e => `
|
||||
<tr>
|
||||
<td style="font-size:12px">${formatDate(e.timestamp)}</td>
|
||||
<td><span class="badge badge-accent">${e.agent}</span></td>
|
||||
<td style="font-size:12px">${e.model}</td>
|
||||
<td>${(e.tokens || 0).toLocaleString()}</td>
|
||||
<td><span class="badge ${(e.cost || 0) > 0 ? 'badge-warning' : 'badge-success'}">$${(e.cost || 0).toFixed(6)}</span></td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
// Free tier alerts
|
||||
const alerts = cost.free_tier_alerts || [];
|
||||
const alertsContainer = document.getElementById('freeTierAlerts');
|
||||
if (alerts.length === 0) {
|
||||
alertsContainer.innerHTML = '<div class="empty-state" style="padding:16px"><span>No active alerts. All within free tier limits.</span></div>';
|
||||
} else {
|
||||
alertsContainer.innerHTML = alerts.map(a => `
|
||||
<div style="display:flex;align-items:center;gap:8px;padding:8px;border-bottom:1px solid var(--border);font-size:13px">
|
||||
<span class="badge badge-warning">ALERT</span>
|
||||
<span>${a}</span>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// Chart
|
||||
const ctx = document.getElementById('costChart');
|
||||
if (ctx && Chart) {
|
||||
if (costChart) costChart.destroy();
|
||||
const dailyTotals = {};
|
||||
entries.forEach(e => {
|
||||
if (e.timestamp) {
|
||||
const day = e.timestamp.slice(0, 10);
|
||||
dailyTotals[day] = (dailyTotals[day] || 0) + (e.tokens || 0);
|
||||
}
|
||||
if (alerts.length > 0) {
|
||||
const header = document.querySelector('.page-header');
|
||||
header.insertAdjacentHTML('afterend', `
|
||||
<div class="card mb-3" style="border-color:var(--yellow)">
|
||||
<div class="flex items-center gap-2">
|
||||
<span style="font-size:18px">⚠</span>
|
||||
<div>
|
||||
<strong style="font-size:13px">Free Tier Alerts</strong>
|
||||
${alerts.map(a => `<div style="font-size:12px;color:var(--text-muted)">${escapeHtml(a)}</div>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
// Build agent chart
|
||||
const agentTotals = {};
|
||||
entries.forEach(e => {
|
||||
const a = e.agent || 'unknown';
|
||||
agentTotals[a] = (agentTotals[a] || 0) + (e.tokens || 0);
|
||||
});
|
||||
const agentLabels = Object.keys(agentTotals);
|
||||
const agentData = Object.values(agentTotals);
|
||||
|
||||
if (entries.length > 0) {
|
||||
new Chart(document.getElementById('agentChart'), {
|
||||
type: 'doughnut',
|
||||
data: { labels: agentLabels, datasets: [{ data: agentData, backgroundColor: ['rgba(108,92,231,0.8)', 'rgba(0,212,170,0.8)', 'rgba(69,170,242,0.8)', 'rgba(255,165,2,0.8)'] }] },
|
||||
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom', labels: { color: getComputedStyle(document.documentElement).getPropertyValue('--text-secondary').trim(), font: { size: 11 } } } } }
|
||||
});
|
||||
const days = Object.keys(dailyTotals).slice(-14);
|
||||
const values = days.map(d => dailyTotals[d]);
|
||||
costChart = new Chart(ctx, {
|
||||
|
||||
const timeTotals = {};
|
||||
entries.forEach(e => {
|
||||
const day = e.timestamp ? e.timestamp.slice(0, 10) : 'unknown';
|
||||
timeTotals[day] = (timeTotals[day] || 0) + (e.tokens || 0);
|
||||
});
|
||||
const timeLabels = Object.keys(timeTotals).sort();
|
||||
const timeData = timeLabels.map(d => timeTotals[d]);
|
||||
|
||||
new Chart(document.getElementById('timeChart'), {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: days,
|
||||
datasets: [{
|
||||
label: 'Daily Tokens',
|
||||
data: values,
|
||||
borderColor: '#6c5ce7',
|
||||
backgroundColor: 'rgba(108,92,231,0.1)',
|
||||
fill: true,
|
||||
tension: 0.4,
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: { legend: { display: false } },
|
||||
scales: {
|
||||
x: { ticks: { color: '#8b8fa3', maxTicksLimit: 7 } },
|
||||
y: { ticks: { color: '#8b8fa3' } }
|
||||
}
|
||||
}
|
||||
data: { labels: timeLabels, datasets: [{ label: 'Tokens', data: timeData, borderColor: 'rgba(108,92,231,0.8)', backgroundColor: 'rgba(108,92,231,0.1)', fill: true, tension: 0.4, pointRadius: 3 }] },
|
||||
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { ticks: { color: getComputedStyle(document.documentElement).getPropertyValue('--text-muted').trim(), font: { size: 10 } } }, y: { ticks: { color: getComputedStyle(document.documentElement).getPropertyValue('--text-muted').trim(), font: { size: 10 } } } } }
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
document.getElementById('costCards').innerHTML = `<div class="card"><p style="color:var(--red)">Error: ${escapeHtml(err.message)}</p></div>`;
|
||||
document.getElementById('costStats').innerHTML = `<div class="card" style="grid-column:1/-1"><div class="empty-state"><div class="empty-state-icon">⚠</div><div class="empty-state-title">${escapeHtml(err.message)}</div></div></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function recordTestCost() {
|
||||
showModal('Record Cost Entry', `
|
||||
<div class="form-group">
|
||||
<label class="form-label">Agent</label>
|
||||
<select id="rcAgent" class="form-select"><option>opencode</option><option>hermes</option><option>gemini</option></select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Model</label>
|
||||
<input id="rcModel" class="form-input" value="deepseek-v4-flash-free">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Tokens</label>
|
||||
<input id="rcTokens" class="form-input" type="number" value="1000">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Cost ($)</label>
|
||||
<input id="rcCost" class="form-input" type="number" step="0.000001" value="0.0001">
|
||||
</div>
|
||||
</div>
|
||||
`, `
|
||||
<button class="btn btn-ghost" onclick="closeModal()">Cancel</button>
|
||||
<button class="btn btn-primary" onclick="submitCostRecord()">Record</button>
|
||||
`);
|
||||
}
|
||||
|
||||
async function submitCostRecord() {
|
||||
try {
|
||||
await api.recordCost({
|
||||
agent: document.getElementById('rcAgent').value,
|
||||
model: document.getElementById('rcModel').value,
|
||||
tokens: parseInt(document.getElementById('rcTokens').value) || 0,
|
||||
cost: parseFloat(document.getElementById('rcCost').value) || 0,
|
||||
});
|
||||
closeModal();
|
||||
showToast('Cost recorded', 'success');
|
||||
renderCost();
|
||||
} catch (err) {
|
||||
showToast(`Error: ${err.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,129 +1,141 @@
|
|||
let healthChart = null;
|
||||
|
||||
async function renderDashboard() {
|
||||
const content = document.getElementById('pageContent');
|
||||
let status;
|
||||
try {
|
||||
status = await api.getStatus();
|
||||
} catch {
|
||||
content.innerHTML = '<div class="empty-state"><div class="icon">⚠</div><h3>Cannot connect to server</h3><p>Make sure server.py is running</p></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
content.innerHTML = `
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<div class="page-header-left">
|
||||
<h1 class="page-title">Dashboard</h1>
|
||||
<p class="page-subtitle">System overview and quick actions</p>
|
||||
<p class="page-subtitle">Agentic OS system overview</p>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-primary" onclick="runQuickSkill()">▶ Quick Run</button>
|
||||
<button class="btn" onclick="navigate('backups')">💾 Backup Now</button>
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="refreshDashboard()">⟳ Refresh</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-4" id="statCards">
|
||||
<div class="card"><div class="card-value" id="agentsOnline">-</div><div class="card-label">Agents Online</div></div>
|
||||
<div class="card"><div class="card-value" id="skillsCount">-</div><div class="card-label">Skills Available</div></div>
|
||||
<div class="card"><div class="card-value" id="systemUptime">-</div><div class="card-label">System Uptime</div></div>
|
||||
<div class="card"><div class="card-value" id="healthStatus">-</div><div class="card-label">Health Status</div></div>
|
||||
</div>
|
||||
|
||||
<div id="dashStats" class="grid grid-4 mb-4"></div>
|
||||
<div class="grid grid-2">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">Agent Status</span>
|
||||
</div>
|
||||
<div id="agentList"></div>
|
||||
<div class="card-header"><span class="card-title">Agent Status</span></div>
|
||||
<div id="agentList"><div class="skeleton" style="height:100px"></div></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">Quick Run</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Select Skill</label>
|
||||
<select id="quickSkillSelect"></select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Input (optional)</label>
|
||||
<textarea id="quickSkillInput" rows="2" placeholder="Optional input for the skill..."></textarea>
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="quickRunSkill()">▶ Run Skill</button>
|
||||
<div id="quickRunResult" style="margin-top:12px"></div>
|
||||
<div class="card-header"><span class="card-title">Recent Activity</span></div>
|
||||
<div id="recentActivity"><div class="skeleton" style="height:100px"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">Recent Activity</span>
|
||||
</div>
|
||||
<div id="recentActivity"><div class="loading">Loading...</div></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Fill stat cards
|
||||
const agents = status.agents || [];
|
||||
document.getElementById('agentsOnline').textContent = `${agents.filter(a => a.status === 'online').length}/${agents.length}`;
|
||||
document.getElementById('skillsCount').textContent = status.skills_count || 0;
|
||||
document.getElementById('systemUptime').textContent = status.uptime ? Math.floor((Date.now()/1000 - status.uptime) / 60) + ' min' : '-';
|
||||
document.getElementById('healthStatus').textContent = status.status === 'healthy' ? 'Healthy' : 'Degraded';
|
||||
document.getElementById('healthStatus').style.color = status.status === 'healthy' ? 'var(--green)' : 'var(--red)';
|
||||
try {
|
||||
const [status, skills, audit] = await Promise.all([
|
||||
api.getStatus(),
|
||||
api.getSkills(),
|
||||
api.getAudit(10)
|
||||
]);
|
||||
|
||||
// Agent list
|
||||
const agentList = document.getElementById('agentList');
|
||||
agents.forEach(a => {
|
||||
agentList.innerHTML += `
|
||||
<div style="display:flex;align-items:center;gap:10px;padding:8px 0;border-bottom:1px solid var(--border)">
|
||||
<div class="status-dot ${a.status}"></div>
|
||||
<strong>${a.name}</strong>
|
||||
<span style="color:var(--text2);font-size:12px">${a.status}</span>
|
||||
const agents = status.agents || [];
|
||||
const skillsCount = status.skills_count || 0;
|
||||
const entries = audit.entries || [];
|
||||
const online = agents.filter(a => a.status === 'online').length;
|
||||
|
||||
document.getElementById('dashStats').innerHTML = `
|
||||
<div class="card stat-card">
|
||||
<div class="stat-icon purple">⬡</div>
|
||||
<div class="stat-value">${skillsCount}</div>
|
||||
<div class="stat-label">Skills Installed</div>
|
||||
<div class="stat-change up">↑ ready to execute</div>
|
||||
</div>
|
||||
<div class="card stat-card">
|
||||
<div class="stat-icon ${online === agents.length ? 'green' : online > 0 ? 'yellow' : 'red'}">
|
||||
${online === agents.length ? '✓' : online > 0 ? '⚠' : '✕'}
|
||||
</div>
|
||||
<div class="stat-value">${online}/${agents.length}</div>
|
||||
<div class="stat-label">Agents Online</div>
|
||||
<div class="stat-change ${online === agents.length ? 'up' : 'down'}">${online === agents.length ? 'all operational' : `${agents.length - online} offline`}</div>
|
||||
</div>
|
||||
<div class="card stat-card">
|
||||
<div class="stat-icon blue">📋</div>
|
||||
<div class="stat-value">${entries.length}</div>
|
||||
<div class="stat-label">Recent Events</div>
|
||||
<div class="stat-change up">last ${entries.length} activities</div>
|
||||
</div>
|
||||
<div class="card stat-card">
|
||||
<div class="stat-icon yellow">⚡</div>
|
||||
<div class="stat-value">${(skills || []).filter(s => s.scores && s.scores.length > 0).length}</div>
|
||||
<div class="stat-label">Skills w/ Eval Scores</div>
|
||||
<div class="stat-change up">performance tracked</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
// Skill select
|
||||
try {
|
||||
const skills = await api.getSkills();
|
||||
const select = document.getElementById('quickSkillSelect');
|
||||
skills.forEach(s => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = s.name;
|
||||
opt.textContent = s.name;
|
||||
select.appendChild(opt);
|
||||
});
|
||||
} catch {}
|
||||
document.getElementById('agentList').innerHTML = `
|
||||
<div class="agent-card-list">
|
||||
${agents.map(a => {
|
||||
const sc = statusColor(a.status);
|
||||
return `<div class="agent-card">
|
||||
<div class="agent-dot ${a.status}" style="width:10px;height:10px"></div>
|
||||
<div>
|
||||
<div style="font-weight:600;font-size:13px">${a.name}</div>
|
||||
<div style="font-size:11px;color:${sc.text}">${a.status}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('')}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Recent activity
|
||||
try {
|
||||
const audit = await api.getAudit(10);
|
||||
const activity = document.getElementById('recentActivity');
|
||||
if (audit.entries && audit.entries.length > 0) {
|
||||
activity.innerHTML = audit.entries.reverse().map(e => `
|
||||
<div style="display:flex;align-items:center;gap:8px;padding:6px 0;font-size:13px;border-bottom:1px solid var(--border)">
|
||||
<span style="color:var(--text2);font-size:11px">${formatDate(e.timestamp)}</span>
|
||||
<span class="badge badge-info">${e.action}</span>
|
||||
<span>${e.skill || e.file || ''}</span>
|
||||
document.getElementById('recentActivity').innerHTML = entries.length === 0
|
||||
? '<div class="empty-state" style="padding:20px"><div class="empty-state-icon" style="font-size:24px">📭</div><div class="empty-state-title" style="font-size:14px">No activity yet</div></div>'
|
||||
: `<div class="event-list">${entries.slice(0, 8).map(e => `
|
||||
<div class="event-item">
|
||||
<div class="event-dot" style="background:${e.action === 'skill_run' ? 'var(--accent)' : 'var(--blue)'}"></div>
|
||||
<div class="event-content">
|
||||
<div class="event-title">${e.action}${e.skill ? `: ${e.skill}` : ''}</div>
|
||||
<div class="event-meta">${e.agent ? `via ${e.agent}` : ''} ${e.run_id ? `#${e.run_id}` : ''}</div>
|
||||
</div>
|
||||
<div class="event-time">${timeAgo(e.timestamp)}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
} else {
|
||||
activity.innerHTML = '<div class="empty-state" style="padding:20px"><span>No activity yet</span></div>';
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
`).join('')}</div>`;
|
||||
|
||||
async function quickRunSkill() {
|
||||
const select = document.getElementById('quickSkillSelect');
|
||||
const input = document.getElementById('quickSkillInput');
|
||||
const result = document.getElementById('quickRunResult');
|
||||
if (!select.value) { showToast('Select a skill first', 'error'); return; }
|
||||
result.innerHTML = '<span style="color:var(--text2)">Running...</span>';
|
||||
try {
|
||||
const r = await api.runSkill(select.value, input.value);
|
||||
result.innerHTML = `<span style="color:var(--green)">✓ ${r.message}</span>`;
|
||||
showToast(`Skill '${select.value}' dispatched to ${r.agent}`, 'success');
|
||||
} catch (err) {
|
||||
result.innerHTML = `<span style="color:var(--red)">✗ ${err.message}</span>`;
|
||||
document.getElementById('dashStats').innerHTML = `<div class="card" style="grid-column:1/-1"><div class="empty-state"><div class="empty-state-icon">⚠</div><div class="empty-state-title">Connection Error</div><div class="empty-state-desc">${escapeHtml(err.message)}</div><button class="btn btn-primary mt-3" onclick="navigate('dashboard')">Retry</button></div></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshDashboard() {
|
||||
showToast('Refreshing dashboard...', 'info');
|
||||
renderDashboard();
|
||||
async function runQuickSkill() {
|
||||
showModal('Quick Run Skill', `
|
||||
<div class="form-group">
|
||||
<label class="form-label">Skill Name</label>
|
||||
<select id="qrSkill" class="form-select">
|
||||
<option value="">Select a skill...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Input (optional)</label>
|
||||
<textarea id="qrInput" class="form-textarea" rows="3" placeholder="Enter input for the skill..."></textarea>
|
||||
</div>
|
||||
`, `
|
||||
<button class="btn btn-ghost" onclick="closeModal()">Cancel</button>
|
||||
<button class="btn btn-primary" onclick="executeQuickRun()">▶ Run Skill</button>
|
||||
`);
|
||||
|
||||
try {
|
||||
const skills = await api.getSkills();
|
||||
const select = document.getElementById('qrSkill');
|
||||
skills.forEach(s => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = s.name;
|
||||
opt.textContent = s.name.replace(/-/g, ' ');
|
||||
select.appendChild(opt);
|
||||
});
|
||||
} catch {}
|
||||
}
|
||||
|
||||
async function executeQuickRun() {
|
||||
const name = document.getElementById('qrSkill').value;
|
||||
const input = document.getElementById('qrInput').value;
|
||||
if (!name) { showToast('Please select a skill', 'warning'); return; }
|
||||
try {
|
||||
const r = await api.runSkill(name, input);
|
||||
closeModal();
|
||||
showToast(`"${name}" dispatched to ${r.agent} #${r.run_id}`, 'success');
|
||||
} catch (err) {
|
||||
showToast(`Error: ${err.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,73 +2,64 @@ async function renderMemory() {
|
|||
const content = document.getElementById('pageContent');
|
||||
content.innerHTML = `
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<div class="page-header-left">
|
||||
<h1 class="page-title">Memory</h1>
|
||||
<p class="page-subtitle">Persistent context and shared brain</p>
|
||||
<p class="page-subtitle">Shared brain context across all agents</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="memoryFiles"></div>
|
||||
<div id="memoryList"><div class="loading"><div class="loading-spinner"></div></div></div>
|
||||
`;
|
||||
|
||||
try {
|
||||
const brain = await api.getBrain();
|
||||
const container = document.getElementById('memoryFiles');
|
||||
const fileNames = Object.keys(brain);
|
||||
const files = Object.entries(brain);
|
||||
const container = document.getElementById('memoryList');
|
||||
|
||||
if (fileNames.length === 0) {
|
||||
container.innerHTML = '<div class="empty-state"><div class="icon">🧠</div><h3>No memory files found</h3></div>';
|
||||
if (files.length === 0) {
|
||||
container.innerHTML = '<div class="empty-state"><div class="empty-state-icon">🧠</div><div class="empty-state-title">No memory files</div></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
fileNames.forEach(name => {
|
||||
const content = brain[name];
|
||||
const preview = content.slice(0, 300);
|
||||
container.innerHTML += `
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">${name}</span>
|
||||
<button class="btn btn-sm" onclick="editMemoryFile('${name}')">✏ Edit</button>
|
||||
</div>
|
||||
<pre style="max-height:200px;overflow:auto;font-size:12px">${escapeHtml(preview)}${content.length > 300 ? '\n...' : ''}</pre>
|
||||
<div style="margin-top:8px;font-size:11px;color:var(--text2)">${content.length} characters</div>
|
||||
container.innerHTML = `<div style="display:grid;gap:12px">${files.map(([name, content]) => {
|
||||
const preview = content ? content.slice(0, 200) : '';
|
||||
return `<div class="card" style="cursor:pointer" onclick="editMemory('${name}')">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div><span class="card-title">${name.replace('.md', '').replace(/-/g, ' ')}</span></div>
|
||||
<span class="badge badge-info">${content ? content.split('\n').length : 0} lines</span>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
<pre style="max-height:80px;overflow:hidden;font-size:11px;color:var(--text-muted)">${escapeHtml(preview)}${preview.length >= 200 ? '...' : ''}</pre>
|
||||
</div>`;
|
||||
}).join('')}</div>`;
|
||||
} catch (err) {
|
||||
content.innerHTML = `<div class="empty-state"><div class="icon">⚠</div><h3>Error</h3><p>${escapeHtml(err.message)}</p></div>`;
|
||||
document.getElementById('memoryList').innerHTML = `<div class="empty-state"><div class="empty-state-icon">⚠</div><div class="empty-state-title">${escapeHtml(err.message)}</div></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function editMemoryFile(name) {
|
||||
const content = document.getElementById('pageContent');
|
||||
let existingContent = '';
|
||||
async function editMemory(name) {
|
||||
const display = name.replace('.md', '').replace(/-/g, ' ');
|
||||
let content = '';
|
||||
try {
|
||||
const file = await api.getBrainFile(name);
|
||||
existingContent = file.content;
|
||||
const r = await api.getBrainFile(name);
|
||||
content = r.content || '';
|
||||
} catch {}
|
||||
|
||||
content.innerHTML = `
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">Edit: ${name}</h1>
|
||||
<p class="page-subtitle">Modify shared memory</p>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px">
|
||||
<button class="btn btn-primary" onclick="saveMemoryFile('${name}')">💾 Save</button>
|
||||
<button class="btn" onclick="renderMemory()">← Back</button>
|
||||
</div>
|
||||
showModal(`Edit: ${display}`, `
|
||||
<div class="form-group">
|
||||
<label class="form-label">Content</label>
|
||||
<textarea id="memContent" class="form-textarea" style="min-height:300px;font-size:12px">${escapeHtml(content)}</textarea>
|
||||
</div>
|
||||
<div class="card">
|
||||
<textarea id="memoryEditor" style="min-height:400px;font-family:monospace">${escapeHtml(existingContent)}</textarea>
|
||||
</div>
|
||||
`;
|
||||
`, `
|
||||
<button class="btn btn-ghost" onclick="closeModal()">Cancel</button>
|
||||
<button class="btn btn-primary" onclick="saveMemory('${name}')">💾 Save</button>
|
||||
`);
|
||||
}
|
||||
|
||||
async function saveMemoryFile(name) {
|
||||
const content = document.getElementById('memoryEditor').value;
|
||||
async function saveMemory(name) {
|
||||
const content = document.getElementById('memContent').value;
|
||||
try {
|
||||
await api.updateBrainFile(name, content);
|
||||
showToast(`'${name}' saved`, 'success');
|
||||
closeModal();
|
||||
showToast('Memory updated', 'success');
|
||||
renderMemory();
|
||||
} catch (err) {
|
||||
showToast(`Error: ${err.message}`, 'error');
|
||||
|
|
|
|||
|
|
@ -2,14 +2,13 @@ async function renderPlugins() {
|
|||
const content = document.getElementById('pageContent');
|
||||
content.innerHTML = `
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<div class="page-header-left">
|
||||
<h1 class="page-title">Plugin Registry</h1>
|
||||
<p class="page-subtitle">Manage installed skills and plugins</p>
|
||||
<p class="page-subtitle">Manage installed plugins and extensions</p>
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="showInstallPlugin()">+ Install</button>
|
||||
</div>
|
||||
<div id="pluginList"><div class="loading">Loading...</div></div>
|
||||
<div id="installPluginForm" style="display:none"></div>
|
||||
<div id="pluginList"><div class="loading"><div class="loading-spinner"></div></div></div>
|
||||
`;
|
||||
|
||||
try {
|
||||
|
|
@ -18,7 +17,7 @@ async function renderPlugins() {
|
|||
const container = document.getElementById('pluginList');
|
||||
|
||||
if (plugins.length === 0) {
|
||||
container.innerHTML = '<div class="empty-state"><div class="icon">🔌</div><h3>No plugins installed</h3></div>';
|
||||
container.innerHTML = '<div class="empty-state"><div class="empty-state-icon">🔌</div><div class="empty-state-title">No plugins installed</div><div class="empty-state-desc">Install plugins from the registry or create your own</div></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -30,48 +29,43 @@ async function renderPlugins() {
|
|||
${plugins.map(p => `
|
||||
<tr>
|
||||
<td><strong>${p.name}</strong></td>
|
||||
<td><code>${p.version}</code></td>
|
||||
<td><span class="badge badge-info">${p.type || 'unknown'}</span></td>
|
||||
<td style="font-size:12px">${formatDate(p.installed)}</td>
|
||||
<td><code>${p.version || '1.0.0'}</code></td>
|
||||
<td><span class="badge badge-info">${p.type || 'skill'}</span></td>
|
||||
<td style="font-size:12px;color:var(--text-muted)">${formatDate(p.installed)}</td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div style="font-size:12px;color:var(--text2);text-align:right;margin-top:8px">${plugins.length} plugins</div>
|
||||
<div style="font-size:12px;color:var(--text-muted);text-align:right;margin-top:8px">${plugins.length} plugin${plugins.length !== 1 ? 's' : ''}</div>
|
||||
`;
|
||||
} catch (err) {
|
||||
document.getElementById('pluginList').innerHTML = `<div class="empty-state"><div class="icon">⚠</div><p>${escapeHtml(err.message)}</p></div>`;
|
||||
document.getElementById('pluginList').innerHTML = `<div class="empty-state"><div class="empty-state-icon">⚠</div><div class="empty-state-title">${escapeHtml(err.message)}</div></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function showInstallPlugin() {
|
||||
document.getElementById('installPluginForm').style.display = 'block';
|
||||
document.getElementById('installPluginForm').innerHTML = `
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-title">Install Plugin</span></div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Plugin Name</label>
|
||||
<input id="pluginName" placeholder="e.g., my-custom-skill">
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="installPlugin()">Install</button>
|
||||
<button class="btn" onclick="cancelInstallPlugin()" style="margin-left:8px">Cancel</button>
|
||||
showModal('Install Plugin', `
|
||||
<div class="form-group">
|
||||
<label class="form-label">Plugin Name</label>
|
||||
<input id="pluginNameInput" class="form-input" placeholder="e.g., my-custom-skill">
|
||||
<div class="form-hint">Enter the name of the plugin to install from the registry</div>
|
||||
</div>
|
||||
`;
|
||||
`, `
|
||||
<button class="btn btn-ghost" onclick="closeModal()">Cancel</button>
|
||||
<button class="btn btn-primary" onclick="installPlugin()">Install</button>
|
||||
`);
|
||||
}
|
||||
|
||||
async function installPlugin() {
|
||||
const name = document.getElementById('pluginName').value.trim();
|
||||
if (!name) { showToast('Plugin name required', 'error'); return; }
|
||||
const name = document.getElementById('pluginNameInput').value.trim();
|
||||
if (!name) { showToast('Plugin name required', 'warning'); return; }
|
||||
try {
|
||||
const r = await api.installPlugin(name);
|
||||
showToast(r.status === 'already_installed' ? 'Already installed' : 'Plugin installed', 'success');
|
||||
closeModal();
|
||||
showToast(r.status === 'already_installed' ? 'Already installed' : `"${name}" installed`, r.status === 'already_installed' ? 'info' : 'success');
|
||||
renderPlugins();
|
||||
} catch (err) {
|
||||
showToast(`Error: ${err.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function cancelInstallPlugin() {
|
||||
document.getElementById('installPluginForm').style.display = 'none';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ async function renderPrompts() {
|
|||
const content = document.getElementById('pageContent');
|
||||
content.innerHTML = `
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<div class="page-header-left">
|
||||
<h1 class="page-title">Prompt Library</h1>
|
||||
<p class="page-subtitle">Reusable prompt templates for common tasks</p>
|
||||
<p class="page-subtitle">Reusable templates for common agent tasks</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="promptGrid" class="grid grid-3"></div>
|
||||
|
|
@ -12,62 +12,52 @@ async function renderPrompts() {
|
|||
|
||||
try {
|
||||
const prompts = await api.getPrompts();
|
||||
const entries = Object.entries(prompts);
|
||||
const grid = document.getElementById('promptGrid');
|
||||
|
||||
const entries = Object.entries(prompts);
|
||||
if (entries.length === 0) {
|
||||
grid.innerHTML = '<div class="empty-state"><div class="icon">📝</div><h3>No prompts in library</h3></div>';
|
||||
grid.innerHTML = '<div class="empty-state" style="grid-column:1/-1"><div class="empty-state-icon">📝</div><div class="empty-state-title">No prompt templates</div></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
entries.forEach(([name, content]) => {
|
||||
grid.innerHTML = entries.map(([name, content]) => {
|
||||
const displayName = name.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
||||
const preview = content.slice(0, 150);
|
||||
grid.innerHTML += `
|
||||
<div class="card" style="cursor:pointer" onclick="showPromptDetail('${name}')">
|
||||
<div style="font-size:14px;font-weight:600;margin-bottom:8px">${displayName}</div>
|
||||
<pre style="font-size:11px;max-height:120px;overflow:hidden">${escapeHtml(preview)}...</pre>
|
||||
<div style="margin-top:8px;display:flex;gap:4px">
|
||||
<span class="badge badge-info">${content.split('\n').length} lines</span>
|
||||
</div>
|
||||
const preview = content.slice(0, 180);
|
||||
const lines = content.split('\n').length;
|
||||
return `<div class="skill-card" onclick="viewPrompt('${name}')">
|
||||
<div class="skill-card-header">
|
||||
<div class="skill-card-icon">📝</div>
|
||||
<div class="skill-card-name">${displayName}</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
<div class="skill-card-desc"><pre style="background:none;border:none;padding:0;max-height:100px;overflow:hidden;font-size:11px;color:var(--text-muted)">${escapeHtml(preview)}${preview.length >= 180 ? '...' : ''}</pre></div>
|
||||
<div class="skill-card-footer"><span class="badge badge-info">${lines} lines</span></div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
} catch (err) {
|
||||
document.getElementById('promptGrid').innerHTML = `<div class="empty-state"><div class="icon">⚠</div><p>${escapeHtml(err.message)}</p></div>`;
|
||||
document.getElementById('promptGrid').innerHTML = `<div class="empty-state" style="grid-column:1/-1"><div class="empty-state-icon">⚠</div><div class="empty-state-title">${escapeHtml(err.message)}</div></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function showPromptDetail(name) {
|
||||
const content = document.getElementById('pageContent');
|
||||
let fullContent = '';
|
||||
async function viewPrompt(name) {
|
||||
let content = '';
|
||||
try {
|
||||
const prompts = await api.getPrompts();
|
||||
fullContent = prompts[name] || '';
|
||||
content = prompts[name] || '';
|
||||
} catch {}
|
||||
|
||||
const displayName = name.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
||||
content.innerHTML = `
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">${displayName}</h1>
|
||||
<p class="page-subtitle">Prompt template</p>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px">
|
||||
<button class="btn btn-primary" onclick="copyPrompt()" id="copyPromptBtn">📋 Copy</button>
|
||||
<button class="btn" onclick="renderPrompts()">← Back</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<pre id="promptContent" style="white-space:pre-wrap">${escapeHtml(fullContent)}</pre>
|
||||
</div>
|
||||
`;
|
||||
|
||||
showModal(`Prompt: ${displayName}`, `
|
||||
<pre style="white-space:pre-wrap;font-size:12px;max-height:60vh;overflow:auto">${escapeHtml(content)}</pre>
|
||||
`, `
|
||||
<button class="btn btn-primary" onclick="copyPromptContent('${escapeHtml(content).replace(/'/g, "\\'")}')">📋 Copy</button>
|
||||
<button class="btn btn-ghost" onclick="closeModal()">Close</button>
|
||||
`);
|
||||
}
|
||||
|
||||
function copyPrompt() {
|
||||
const pre = document.getElementById('promptContent');
|
||||
if (!pre) return;
|
||||
navigator.clipboard.writeText(pre.textContent).then(() => {
|
||||
function copyPromptContent(text) {
|
||||
const decoded = text.replace(/\\'/g, "'");
|
||||
navigator.clipboard.writeText(decoded).then(() => {
|
||||
showToast('Copied to clipboard', 'success');
|
||||
}).catch(() => {
|
||||
showToast('Failed to copy', 'error');
|
||||
|
|
|
|||
|
|
@ -2,94 +2,84 @@ async function renderScheduler() {
|
|||
const content = document.getElementById('pageContent');
|
||||
content.innerHTML = `
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<div class="page-header-left">
|
||||
<h1 class="page-title">Scheduler</h1>
|
||||
<p class="page-subtitle">Manage scheduled workflows and cron jobs</p>
|
||||
<p class="page-subtitle">Automated workflow scheduling</p>
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="showNewJobForm()">+ New Job</button>
|
||||
<button class="btn btn-primary" onclick="showAddJob()">+ Add Job</button>
|
||||
</div>
|
||||
<div id="jobList"></div>
|
||||
<div id="newJobForm" style="display:none"></div>
|
||||
<div id="jobList"><div class="loading"><div class="loading-spinner"></div></div></div>
|
||||
`;
|
||||
|
||||
try {
|
||||
const jobs = await api.getJobs();
|
||||
const container = document.getElementById('jobList');
|
||||
|
||||
if (jobs.length === 0) {
|
||||
container.innerHTML = '<div class="empty-state"><div class="icon">⏱</div><h3>No scheduled jobs</h3></div>';
|
||||
container.innerHTML = '<div class="empty-state"><div class="empty-state-icon">⏱</div><div class="empty-state-title">No scheduled jobs</div><div class="empty-state-desc">Create your first scheduled job to automate workflows</div><button class="btn btn-primary mt-3" onclick="showAddJob()">+ Add Job</button></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const html = `
|
||||
container.innerHTML = `
|
||||
<div class="table-wrapper">
|
||||
<table>
|
||||
<thead><tr><th>Name</th><th>Skill</th><th>Cron</th><th>Last Run</th><th>Next Run</th><th>Status</th><th></th></tr></thead>
|
||||
<thead><tr><th>Name</th><th>Skill</th><th>Cron</th><th>Status</th><th>Last Run</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
${jobs.map(j => `
|
||||
<tr>
|
||||
<td><strong>${j.name}</strong></td>
|
||||
<td><span class="badge badge-info">${j.skill}</span></td>
|
||||
<td><span class="badge badge-accent">${j.skill}</span></td>
|
||||
<td><code>${j.cron}</code></td>
|
||||
<td>${formatDate(j.last_run)}</td>
|
||||
<td>${formatDate(j.next_run)}</td>
|
||||
<td><span class="badge ${j.enabled ? 'badge-success' : 'badge-warning'}">${j.enabled ? 'Active' : 'Paused'}</span></td>
|
||||
<td><button class="btn btn-sm btn-danger" onclick="deleteJob('${j.id}')">✕</button></td>
|
||||
<td style="font-size:12px;color:var(--text-muted)">${j.last_run ? formatDate(j.last_run) : 'Never'}</td>
|
||||
<td><button class="btn btn-sm btn-danger" onclick="deleteJob('${j.id}')">Delete</button></td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div style="font-size:12px;color:var(--text-muted);text-align:right;margin-top:8px">${jobs.length} job${jobs.length !== 1 ? 's' : ''}</div>
|
||||
`;
|
||||
container.innerHTML = html;
|
||||
} catch (err) {
|
||||
document.getElementById('jobList').innerHTML = `<div class="empty-state"><div class="icon">⚠</div><p>${escapeHtml(err.message)}</p></div>`;
|
||||
document.getElementById('jobList').innerHTML = `<div class="empty-state"><div class="empty-state-icon">⚠</div><div class="empty-state-title">${escapeHtml(err.message)}</div></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function showNewJobForm() {
|
||||
document.getElementById('newJobForm').style.display = 'block';
|
||||
document.getElementById('newJobForm').innerHTML = `
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-title">New Scheduled Job</span></div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Name</label>
|
||||
<input id="jobName" placeholder="e.g., Weekly Backup">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Skill</label>
|
||||
<select id="jobSkill"></select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Cron Expression</label>
|
||||
<input id="jobCron" placeholder="e.g., 0 6 * * 1" value="0 6 * * *">
|
||||
<div style="font-size:11px;color:var(--text2);margin-top:4px">
|
||||
Common: <code>*/5 * * * *</code> (5 min), <code>0 6 * * *</code> (daily 6AM), <code>0 6 * * 1</code> (weekly Mon)
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="createJob()">Create Job</button>
|
||||
<button class="btn" onclick="cancelNewJob()" style="margin-left:8px">Cancel</button>
|
||||
</div>
|
||||
`;
|
||||
async function showAddJob() {
|
||||
let skills = [];
|
||||
try { const s = await api.getSkills(); skills = s; } catch {}
|
||||
|
||||
try {
|
||||
const skills = await api.getSkills();
|
||||
const select = document.getElementById('jobSkill');
|
||||
skills.forEach(s => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = s.name;
|
||||
opt.textContent = s.name;
|
||||
select.appendChild(opt);
|
||||
});
|
||||
} catch {}
|
||||
showModal('Add Scheduled Job', `
|
||||
<div class="form-group">
|
||||
<label class="form-label">Job Name</label>
|
||||
<input id="jobName" class="form-input" placeholder="e.g., Nightly Backup">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Skill</label>
|
||||
<select id="jobSkill" class="form-select">
|
||||
<option value="">Select a skill...</option>
|
||||
${skills.map(s => `<option value="${s.name}">${s.name.replace(/-/g, ' ')}</option>`).join('')}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Cron Expression</label>
|
||||
<input id="jobCron" class="form-input" placeholder="e.g., 0 2 * * *" value="0 0 * * *">
|
||||
<div class="form-hint">Format: minute hour day month weekday</div>
|
||||
</div>
|
||||
`, `
|
||||
<button class="btn btn-ghost" onclick="closeModal()">Cancel</button>
|
||||
<button class="btn btn-primary" onclick="createJob()">Create Job</button>
|
||||
`);
|
||||
}
|
||||
|
||||
async function createJob() {
|
||||
const name = document.getElementById('jobName').value.trim();
|
||||
const skill = document.getElementById('jobSkill').value;
|
||||
const cron = document.getElementById('jobCron').value.trim();
|
||||
if (!name || !skill || !cron) { showToast('All fields required', 'error'); return; }
|
||||
if (!name || !skill || !cron) { showToast('All fields required', 'warning'); return; }
|
||||
try {
|
||||
await api.createJob({ name, skill, cron });
|
||||
closeModal();
|
||||
showToast('Job created', 'success');
|
||||
renderScheduler();
|
||||
} catch (err) {
|
||||
|
|
@ -97,12 +87,8 @@ async function createJob() {
|
|||
}
|
||||
}
|
||||
|
||||
function cancelNewJob() {
|
||||
document.getElementById('newJobForm').style.display = 'none';
|
||||
}
|
||||
|
||||
async function deleteJob(id) {
|
||||
if (!confirm('Delete this job?')) return;
|
||||
if (!confirm('Delete this scheduled job?')) return;
|
||||
try {
|
||||
await api.deleteJob(id);
|
||||
showToast('Job deleted', 'success');
|
||||
|
|
|
|||
|
|
@ -2,156 +2,182 @@ async function renderSettings() {
|
|||
const content = document.getElementById('pageContent');
|
||||
content.innerHTML = `
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<div class="page-header-left">
|
||||
<h1 class="page-title">Settings</h1>
|
||||
<p class="page-subtitle">Configure Agentic OS</p>
|
||||
<p class="page-subtitle">Configure Agentic OS behavior</p>
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="saveSettings()">💾 Save Settings</button>
|
||||
<button class="btn btn-primary" onclick="saveAllSettings()">💾 Save All</button>
|
||||
</div>
|
||||
<div id="settingsForm"><div class="loading">Loading...</div></div>
|
||||
<div id="settingsForm"><div class="loading"><div class="loading-spinner"></div></div></div>
|
||||
`;
|
||||
|
||||
try {
|
||||
const settings = await api.getSettings();
|
||||
const container = document.getElementById('settingsForm');
|
||||
|
||||
const prefs = settings.agent_preferences || {};
|
||||
const dashboard = settings.dashboard || {};
|
||||
const limits = settings.free_tier_limits || {};
|
||||
const apiKeys = settings.api_keys || {};
|
||||
|
||||
container.innerHTML = `
|
||||
document.getElementById('settingsForm').innerHTML = `
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-title">Agent Preferences</span></div>
|
||||
<div class="grid grid-2">
|
||||
<div class="form-group">
|
||||
<label class="form-label">opencode Enabled</label>
|
||||
<select id="ocEnabled">
|
||||
<option value="true" ${prefs.opencode?.enabled !== false ? 'selected' : ''}>Enabled</option>
|
||||
<option value="false" ${prefs.opencode?.enabled === false ? 'selected' : ''}>Disabled</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Hermes Enabled</label>
|
||||
<select id="hermesEnabled">
|
||||
<option value="true" ${prefs.hermes?.enabled !== false ? 'selected' : ''}>Enabled</option>
|
||||
<option value="false" ${prefs.hermes?.enabled === false ? 'selected' : ''}>Disabled</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Gemini CLI Enabled</label>
|
||||
<select id="geminiEnabled">
|
||||
<option value="true" ${prefs.gemini?.enabled !== false ? 'selected' : ''}>Enabled</option>
|
||||
<option value="false" ${prefs.gemini?.enabled === false ? 'selected' : ''}>Disabled</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Dashboard Theme</label>
|
||||
<select id="themeSelect">
|
||||
<option value="dark" ${dashboard.dark_mode !== false ? 'selected' : ''}>Dark</option>
|
||||
<option value="light" ${dashboard.dark_mode === false ? 'selected' : ''}>Light</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="card-header"><span class="card-title">🤖 Agent Preferences</span></div>
|
||||
<div class="grid grid-3">
|
||||
${['opencode', 'hermes', 'gemini'].map(a => `
|
||||
<div class="card" style="padding:14px">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<div class="agent-dot ${prefs[a] && prefs[a].enabled !== false ? 'online' : 'offline'}" style="width:10px;height:10px"></div>
|
||||
<strong style="font-size:13px">${a}</strong>
|
||||
</div>
|
||||
<label class="switch" style="margin:8px 0">
|
||||
<input type="checkbox" id="agent_${a}" ${prefs[a] && prefs[a].enabled !== false ? 'checked' : ''} onchange="toggleAgent('${a}')">
|
||||
<span class="switch-slider"></span>
|
||||
</label>
|
||||
<div class="form-group" style="margin-bottom:0;margin-top:8px">
|
||||
<label class="form-label">Binary Path</label>
|
||||
<input id="bin_${a}" class="form-input" value="${(prefs[a] && prefs[a].binary) || a}" style="font-size:12px">
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-title">Dashboard</span></div>
|
||||
<div class="grid grid-2">
|
||||
<div class="card-header"><span class="card-title">🎨 Dashboard</span></div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Port</label>
|
||||
<input id="dashPort" value="${dashboard.port || 8080}" type="number">
|
||||
<input id="setPort" class="form-input" type="number" value="${dashboard.port || 8080}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Host</label>
|
||||
<input id="dashHost" value="${dashboard.host || '127.0.0.1'}">
|
||||
<input id="setHost" class="form-input" value="${dashboard.host || '127.0.0.1'}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="switch" style="width:auto;display:flex;align-items:center;gap:10px">
|
||||
<input type="checkbox" id="setDarkMode" ${dashboard.dark_mode !== false ? 'checked' : ''}>
|
||||
<span class="switch-slider" style="position:relative;display:inline-block;width:40px;height:22px"></span>
|
||||
<span style="font-size:13px">Dark Mode</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-title">🔑 API Keys</span></div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Gemini API Key</label>
|
||||
<input id="keyGemini" class="form-input" type="password" value="${apiKeys.gemini || ''}" placeholder="Enter Gemini API key">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">OpenRouter API Key</label>
|
||||
<input id="keyOpenrouter" class="form-input" type="password" value="${apiKeys.openrouter || ''}" placeholder="Enter OpenRouter API key">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-title">Free Tier Limits</span></div>
|
||||
<div class="grid grid-2">
|
||||
<div class="card-header"><span class="card-title">💰 Free Tier Limits</span></div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Gemini Flash — Requests/Day</label>
|
||||
<input id="geminiReqs" value="${limits.gemini_flash?.requests_per_day || 1500}" type="number">
|
||||
<input id="limGReqs" class="form-input" type="number" value="${(limits.gemini_flash && limits.gemini_flash.requests_per_day) || 1500}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Gemini Flash — Tokens/Day</label>
|
||||
<input id="geminiTokens" value="${limits.gemini_flash?.tokens_per_day || 1000000}" type="number">
|
||||
<input id="limGTokens" class="form-input" type="number" value="${(limits.gemini_flash && limits.gemini_flash.tokens_per_day) || 1000000}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label">OpenRouter Free — Requests/Day</label>
|
||||
<input id="orReqs" value="${limits.openrouter_free?.requests_per_day || 100}" type="number">
|
||||
<input id="limORReqs" class="form-input" type="number" value="${(limits.openrouter_free && limits.openrouter_free.requests_per_day) || 100}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">OpenRouter Free — Tokens/Day</label>
|
||||
<input id="orTokens" value="${limits.openrouter_free?.tokens_per_day || 200000}" type="number">
|
||||
<input id="limORTokens" class="form-input" type="number" value="${(limits.openrouter_free && limits.openrouter_free.tokens_per_day) || 200000}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="border-color:var(--red)">
|
||||
<div class="card-header"><span class="card-title" style="color:var(--red)">Danger Zone</span></div>
|
||||
<p style="font-size:13px;color:var(--text2);margin-bottom:12px">Reset all settings to defaults. This cannot be undone.</p>
|
||||
<div class="card-header"><span class="card-title" style="color:var(--red)">⚠ Danger Zone</span></div>
|
||||
<p style="font-size:13px;color:var(--text-secondary);margin-bottom:12px">Reset all settings to factory defaults.</p>
|
||||
<button class="btn btn-danger" onclick="resetSettings()">Reset to Defaults</button>
|
||||
</div>
|
||||
`;
|
||||
} catch (err) {
|
||||
document.getElementById('settingsForm').innerHTML = `<div class="empty-state"><div class="icon">⚠</div><p>${escapeHtml(err.message)}</p></div>`;
|
||||
document.getElementById('settingsForm').innerHTML = `<div class="empty-state"><div class="empty-state-icon">⚠</div><div class="empty-state-title">${escapeHtml(err.message)}</div></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSettings() {
|
||||
function toggleAgent(name) {
|
||||
const cb = document.getElementById(`agent_${name}`);
|
||||
const card = cb.closest('.card');
|
||||
const dot = card.querySelector('.agent-dot');
|
||||
dot.className = `agent-dot ${cb.checked ? 'online' : 'offline'}`;
|
||||
}
|
||||
|
||||
async function saveAllSettings() {
|
||||
try {
|
||||
const settings = {
|
||||
agent_preferences: {
|
||||
opencode: { enabled: document.getElementById('ocEnabled').value === 'true' },
|
||||
hermes: { enabled: document.getElementById('hermesEnabled').value === 'true' },
|
||||
gemini: { enabled: document.getElementById('geminiEnabled').value === 'true' },
|
||||
opencode: { enabled: document.getElementById('agent_opencode').checked, binary: document.getElementById('bin_opencode').value },
|
||||
hermes: { enabled: document.getElementById('agent_hermes').checked, binary: document.getElementById('bin_hermes').value },
|
||||
gemini: { enabled: document.getElementById('agent_gemini').checked, binary: document.getElementById('bin_gemini').value },
|
||||
},
|
||||
dashboard: {
|
||||
port: parseInt(document.getElementById('dashPort').value) || 8080,
|
||||
host: document.getElementById('dashHost').value || '127.0.0.1',
|
||||
dark_mode: document.getElementById('themeSelect').value === 'dark',
|
||||
port: parseInt(document.getElementById('setPort').value) || 8080,
|
||||
host: document.getElementById('setHost').value || '127.0.0.1',
|
||||
dark_mode: document.getElementById('setDarkMode').checked,
|
||||
},
|
||||
api_keys: {
|
||||
gemini: document.getElementById('keyGemini').value,
|
||||
openrouter: document.getElementById('keyOpenrouter').value,
|
||||
},
|
||||
free_tier_limits: {
|
||||
gemini_flash: {
|
||||
requests_per_day: parseInt(document.getElementById('geminiReqs').value) || 1500,
|
||||
tokens_per_day: parseInt(document.getElementById('geminiTokens').value) || 1000000,
|
||||
requests_per_day: parseInt(document.getElementById('limGReqs').value) || 1500,
|
||||
tokens_per_day: parseInt(document.getElementById('limGTokens').value) || 1000000,
|
||||
},
|
||||
openrouter_free: {
|
||||
requests_per_day: parseInt(document.getElementById('orReqs').value) || 100,
|
||||
tokens_per_day: parseInt(document.getElementById('orTokens').value) || 200000,
|
||||
requests_per_day: parseInt(document.getElementById('limORReqs').value) || 100,
|
||||
tokens_per_day: parseInt(document.getElementById('limORTokens').value) || 200000,
|
||||
},
|
||||
},
|
||||
};
|
||||
await api.updateSettings(settings);
|
||||
showToast('Settings saved', 'success');
|
||||
renderSettings();
|
||||
showToast('Settings saved successfully', 'success');
|
||||
} catch (err) {
|
||||
showToast(`Error: ${err.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function resetSettings() {
|
||||
if (!confirm('Reset all settings to defaults?')) return;
|
||||
async function resetSettings() {
|
||||
showModal('Reset to Defaults', `
|
||||
<div class="card" style="background:var(--red-dim);border-color:transparent">
|
||||
<div class="flex items-center gap-2"><span style="font-size:18px">⚠</span><div><strong style="font-size:13px">Warning</strong><div style="font-size:12px;color:var(--text-secondary);margin-top:2px">This will reset all settings to factory defaults and cannot be undone.</div></div></div>
|
||||
</div>
|
||||
`, `
|
||||
<button class="btn btn-ghost" onclick="closeModal()">Cancel</button>
|
||||
<button class="btn btn-danger" onclick="confirmReset()">Reset</button>
|
||||
`);
|
||||
}
|
||||
|
||||
async function confirmReset() {
|
||||
const defaults = {
|
||||
theme: 'dark',
|
||||
agent_preferences: {
|
||||
opencode: { enabled: true, binary: 'opencode', max_turns: 50 },
|
||||
hermes: { enabled: true, binary: 'hermes' },
|
||||
gemini: { enabled: true, binary: 'gemini', model: 'gemini-2.5-flash' },
|
||||
},
|
||||
agent_preferences: { opencode: { enabled: true, binary: 'opencode' }, hermes: { enabled: true, binary: 'hermes' }, gemini: { enabled: true, binary: 'gemini', model: 'gemini-2.5-flash' } },
|
||||
dashboard: { port: 8080, host: '127.0.0.1', dark_mode: true },
|
||||
free_tier_limits: {
|
||||
gemini_flash: { requests_per_day: 1500, tokens_per_day: 1000000 },
|
||||
openrouter_free: { requests_per_day: 100, tokens_per_day: 200000 },
|
||||
},
|
||||
api_keys: { gemini: '', openrouter: '' },
|
||||
free_tier_limits: { gemini_flash: { requests_per_day: 1500, tokens_per_day: 1000000 }, openrouter_free: { requests_per_day: 100, tokens_per_day: 200000 } },
|
||||
};
|
||||
api.updateSettings(defaults).then(() => {
|
||||
try {
|
||||
await api.updateSettings(defaults);
|
||||
closeModal();
|
||||
showToast('Settings reset to defaults', 'success');
|
||||
renderSettings();
|
||||
}).catch(err => {
|
||||
} catch (err) {
|
||||
showToast(`Error: ${err.message}`, 'error');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ let setupStep = 0;
|
|||
const setupSteps = [
|
||||
{ title: 'Welcome', desc: 'Configure Agentic OS for your environment' },
|
||||
{ title: 'Agent Discovery', desc: 'Detecting installed agents...' },
|
||||
{ title: 'Workspace', desc: 'Set your preferences' },
|
||||
{ title: 'Preferences', desc: 'Choose your defaults' },
|
||||
{ title: 'Ready', desc: 'All set!' },
|
||||
];
|
||||
|
||||
|
|
@ -11,121 +11,115 @@ async function renderSetupWizard() {
|
|||
setupStep = 0;
|
||||
content.innerHTML = `
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Setup Wizard</h1>
|
||||
<p class="page-subtitle">Configure Agentic OS for your environment</p>
|
||||
<div class="page-header-left">
|
||||
<h1 class="page-title">Setup Wizard</h1>
|
||||
<p class="page-subtitle">Guided configuration for Agentic OS</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card" style="max-width:640px;margin:0 auto">
|
||||
<div id="wizardBody"></div>
|
||||
</div>
|
||||
<div id="wizardContainer" class="card"></div>
|
||||
`;
|
||||
renderSetupStep();
|
||||
renderWizardStep();
|
||||
}
|
||||
|
||||
async function renderSetupStep() {
|
||||
const container = document.getElementById('wizardContainer');
|
||||
async function renderWizardStep() {
|
||||
const body = document.getElementById('wizardBody');
|
||||
if (!body) return;
|
||||
const step = setupSteps[setupStep];
|
||||
if (!step) { finishSetup(); return; }
|
||||
|
||||
container.innerHTML = `
|
||||
<div style="margin-bottom:20px">
|
||||
<div style="font-size:12px;color:var(--text2);margin-bottom:8px">Step ${setupStep + 1} of ${setupSteps.length}</div>
|
||||
<div style="display:flex;gap:6px;margin-bottom:16px">
|
||||
${setupSteps.map((s, i) => `
|
||||
<div style="flex:1;height:4px;border-radius:2px;background:${i <= setupStep ? 'var(--accent)' : 'var(--border)'}"></div>
|
||||
`).join('')}
|
||||
body.innerHTML = `
|
||||
<div style="margin-bottom:24px">
|
||||
<div style="display:flex;gap:6px;margin-bottom:20px">
|
||||
${setupSteps.map((s, i) => `<div style="flex:1;height:4px;border-radius:2px;background:${i <= setupStep ? 'var(--accent)' : 'var(--border)'}"></div>`).join('')}
|
||||
</div>
|
||||
<h2 style="margin-bottom:4px">${step.title}</h2>
|
||||
<p style="color:var(--text2);font-size:14px">${step.desc}</p>
|
||||
<h2 style="margin-bottom:4px;font-size:20px">${step.title}</h2>
|
||||
<p style="color:var(--text-muted);font-size:13px">${step.desc}</p>
|
||||
</div>
|
||||
<div id="wizardBody"></div>
|
||||
<div style="display:flex;justify-content:space-between;margin-top:20px">
|
||||
<button class="btn" onclick="prevSetupStep()" ${setupStep === 0 ? 'disabled style="opacity:0.4"' : ''}>← Back</button>
|
||||
<button class="btn btn-primary" onclick="nextSetupStep()">${setupStep === setupSteps.length - 1 ? 'Finish' : 'Continue →'}</button>
|
||||
<div id="wizardContent"></div>
|
||||
<div class="flex justify-between mt-4" style="padding-top:16px;border-top:1px solid var(--border)">
|
||||
<button class="btn btn-ghost" onclick="prevWizardStep()" ${setupStep === 0 ? 'disabled' : ''}>← Back</button>
|
||||
<button class="btn btn-primary" onclick="nextWizardStep()">${setupStep === setupSteps.length - 1 ? '✓ Complete' : 'Continue →'}</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const body = document.getElementById('wizardBody');
|
||||
const wc = document.getElementById('wizardContent');
|
||||
|
||||
switch (setupStep) {
|
||||
case 0:
|
||||
body.innerHTML = `
|
||||
<p style="margin-bottom:12px">Agentic OS coordinates <strong>opencode</strong>, <strong>Hermes Agent</strong>, and <strong>Gemini CLI</strong> into a unified orchestration platform.</p>
|
||||
<p style="margin-bottom:12px">This wizard will help you verify the setup and configure preferences.</p>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Your Name</label>
|
||||
<input id="wizName" value="Mihir N Modi" placeholder="Enter your name">
|
||||
wc.innerHTML = `
|
||||
<div style="text-align:center;padding:12px 0">
|
||||
<div style="font-size:48px;margin-bottom:16px">⬡</div>
|
||||
<p style="font-size:14px;color:var(--text-secondary);line-height:1.6">Agentic OS coordinates <strong>opencode</strong>, <strong>Hermes Agent</strong>, and <strong>Gemini CLI</strong> into a unified multi-agent orchestration platform. This wizard will help you get everything configured.</p>
|
||||
</div>
|
||||
`;
|
||||
break;
|
||||
|
||||
case 1:
|
||||
body.innerHTML = '<div class="loading">Detecting agents...</div>';
|
||||
wc.innerHTML = '<div class="loading"><div class="loading-spinner"></div></div>';
|
||||
try {
|
||||
const status = await api.getStatus();
|
||||
const agents = status.agents || [];
|
||||
body.innerHTML = `
|
||||
wc.innerHTML = `
|
||||
<div class="grid grid-2">
|
||||
${agents.map(a => `
|
||||
<div class="card" style="display:flex;align-items:center;gap:12px;padding:16px">
|
||||
<div class="status-dot ${a.status === 'online' ? 'online' : 'offline'}" style="width:12px;height:12px"></div>
|
||||
${agents.map(a => {
|
||||
const sc = statusColor(a.status);
|
||||
return `<div class="agent-card">
|
||||
<div class="agent-dot ${a.status}" style="width:14px;height:14px"></div>
|
||||
<div>
|
||||
<strong>${a.name}</strong>
|
||||
<div style="font-size:12px;color:${a.status === 'online' ? 'var(--green)' : 'var(--red)'}">${a.status}</div>
|
||||
<div style="font-weight:600;font-size:14px">${a.name}</div>
|
||||
<div style="font-size:12px;color:${sc.text}">${a.status}</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>`;
|
||||
}).join('')}
|
||||
</div>
|
||||
${agents.some(a => a.status === 'offline') ? '<p style="color:var(--yellow);font-size:13px;margin-top:12px">Note: Offline agents won\'t be available for skill execution. Install them to enable full functionality.</p>' : ''}
|
||||
${agents.some(a => a.status === 'offline') ? `<div class="card mt-3" style="background:var(--yellow-dim);border-color:transparent;padding:12px"><div class="flex items-center gap-2"><span>⚠</span><span style="font-size:13px">Some agents are offline. Install missing agents to enable full functionality.</span></div></div>` : ''}
|
||||
`;
|
||||
} catch (err) {
|
||||
body.innerHTML = `<p style="color:var(--red)">Could not connect to backend: ${escapeHtml(err.message)}</p>`;
|
||||
wc.innerHTML = `<div class="card" style="background:var(--red-dim);border-color:transparent;padding:12px"><div class="flex items-center gap-2"><span>✕</span><span style="font-size:13px">${escapeHtml(err.message)}</span></div></div>`;
|
||||
}
|
||||
break;
|
||||
|
||||
case 2:
|
||||
body.innerHTML = `
|
||||
wc.innerHTML = `
|
||||
<div class="form-group">
|
||||
<label class="form-label">Dashboard Theme</label>
|
||||
<select id="wizTheme">
|
||||
<option value="dark">Dark</option>
|
||||
<option value="light">Light</option>
|
||||
<label class="form-label">Default Agent for Unknown Tasks</label>
|
||||
<select id="wizDefault" class="form-select">
|
||||
<option value="opencode">opencode — Best for code/DevOps</option>
|
||||
<option value="hermes">Hermes — Best for memory/scheduling</option>
|
||||
<option value="gemini">Gemini CLI — Best for research</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Default Agent for Unknown Tasks</label>
|
||||
<select id="wizDefaultAgent">
|
||||
<option value="opencode">opencode (Best for code/DevOps)</option>
|
||||
<option value="hermes">Hermes (Best for memory/scheduling)</option>
|
||||
<option value="gemini">Gemini CLI (Best for research)</option>
|
||||
<label class="form-label">Dashboard Theme</label>
|
||||
<select id="wizTheme" class="form-select">
|
||||
<option value="dark">Dark (default)</option>
|
||||
<option value="light">Light</option>
|
||||
</select>
|
||||
</div>
|
||||
`;
|
||||
break;
|
||||
|
||||
case 3:
|
||||
body.innerHTML = `
|
||||
<div style="text-align:center;padding:20px">
|
||||
<div style="font-size:48px;margin-bottom:16px">✅</div>
|
||||
<h3>Agentic OS is ready!</h3>
|
||||
<p style="color:var(--text2);margin-top:8px">All components are configured. Start by exploring the Dashboard or running a skill.</p>
|
||||
wc.innerHTML = `
|
||||
<div style="text-align:center;padding:20px 0">
|
||||
<div style="font-size:56px;margin-bottom:16px">✓</div>
|
||||
<h3 style="font-size:18px;margin-bottom:8px">Agentic OS is ready!</h3>
|
||||
<p style="font-size:13px;color:var(--text-muted)">All components are configured. Start exploring the Dashboard or run a skill.</p>
|
||||
</div>
|
||||
`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function nextSetupStep() {
|
||||
if (setupStep < setupSteps.length - 1) {
|
||||
setupStep++;
|
||||
renderSetupStep();
|
||||
} else {
|
||||
finishSetup();
|
||||
}
|
||||
function nextWizardStep() {
|
||||
if (setupStep < setupSteps.length - 1) { setupStep++; renderWizardStep(); }
|
||||
else { finishSetup(); }
|
||||
}
|
||||
|
||||
function prevSetupStep() {
|
||||
if (setupStep > 0) {
|
||||
setupStep--;
|
||||
renderSetupStep();
|
||||
}
|
||||
function prevWizardStep() {
|
||||
if (setupStep > 0) { setupStep--; renderWizardStep(); }
|
||||
}
|
||||
|
||||
function finishSetup() {
|
||||
|
|
|
|||
|
|
@ -2,122 +2,172 @@ async function renderSkills() {
|
|||
const content = document.getElementById('pageContent');
|
||||
content.innerHTML = `
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">Skills</h1>
|
||||
<div class="page-header-left">
|
||||
<h1 class="page-title">Skills Hub</h1>
|
||||
<p class="page-subtitle">Browse, run, and monitor skill performance</p>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<input id="skillFilter" class="form-input" style="width:200px" placeholder="Filter skills..." oninput="filterSkills()">
|
||||
</div>
|
||||
</div>
|
||||
<div id="skillsGrid" class="grid grid-3"></div>
|
||||
<div class="tabs" id="skillTabs">
|
||||
<button class="tab active" data-view="grid" onclick="switchSkillView('grid')">📊 Grid</button>
|
||||
<button class="tab" data-view="list" onclick="switchSkillView('list')">📋 List</button>
|
||||
</div>
|
||||
<div id="skillsContainer"><div class="loading"><div class="loading-spinner"></div></div></div>
|
||||
<div id="skillDetail" style="display:none"></div>
|
||||
`;
|
||||
|
||||
try {
|
||||
const skills = await api.getSkills();
|
||||
const grid = document.getElementById('skillsGrid');
|
||||
if (skills.length === 0) {
|
||||
grid.innerHTML = '<div class="empty-state"><div class="icon">⚡</div><h3>No skills installed</h3></div>';
|
||||
return;
|
||||
}
|
||||
skills.forEach(s => {
|
||||
const lastScore = s.scores && s.scores.length > 0 ? s.scores[s.scores.length - 1] : null;
|
||||
const avgScore = lastScore ? (lastScore.criteria_scores || []).reduce((a, b) => a + b, 0) / (lastScore.criteria_scores || [1]).length : null;
|
||||
grid.innerHTML += `
|
||||
<div class="skill-card" onclick="showSkillDetail('${s.name}')">
|
||||
<div class="skill-name">${s.name.replace(/-/g, ' ')}</div>
|
||||
<div class="skill-desc">${s.description ? s.description.slice(0, 120) + '...' : 'No description'}</div>
|
||||
<div style="margin-top:8px;display:flex;gap:8px;align-items:center">
|
||||
${avgScore !== null ? `<span class="badge badge-success">Score: ${(avgScore * 100).toFixed(0)}%</span>` : ''}
|
||||
${s.has_learnings ? '<span class="badge badge-info">Learning</span>' : ''}
|
||||
<button class="btn btn-sm btn-primary" onclick="event.stopPropagation();quickRun('${s.name}')">▶ Run</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
window._allSkills = skills;
|
||||
renderSkillGrid(skills);
|
||||
} catch (err) {
|
||||
content.innerHTML = `<div class="empty-state"><div class="icon">⚠</div><h3>Error loading skills</h3><p>${escapeHtml(err.message)}</p></div>`;
|
||||
document.getElementById('skillsContainer').innerHTML = `<div class="empty-state"><div class="empty-state-icon">⚠</div><div class="empty-state-title">${escapeHtml(err.message)}</div></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function showSkillDetail(name) {
|
||||
const detail = document.getElementById('skillDetail');
|
||||
const grid = document.getElementById('skillsGrid');
|
||||
grid.style.display = 'none';
|
||||
detail.style.display = 'block';
|
||||
|
||||
detail.innerHTML = `
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">${name.replace(/-/g, ' ')}</span>
|
||||
<div style="display:flex;gap:8px">
|
||||
<button class="btn btn-primary btn-sm" onclick="quickRun('${name}')">▶ Run</button>
|
||||
<button class="btn btn-sm" onclick="backToSkills()">← Back</button>
|
||||
</div>
|
||||
function renderSkillGrid(skills) {
|
||||
const container = document.getElementById('skillsContainer');
|
||||
if (!skills || skills.length === 0) {
|
||||
container.innerHTML = '<div class="empty-state"><div class="empty-state-icon">⚡</div><div class="empty-state-title">No skills installed</div></div>';
|
||||
return;
|
||||
}
|
||||
container.innerHTML = `<div class="grid grid-3" id="skillGrid">${skills.map(s => {
|
||||
const lastScore = s.scores && s.scores.length > 0 ? s.scores[s.scores.length - 1] : null;
|
||||
const avg = lastScore && lastScore.criteria_scores ? (lastScore.criteria_scores.reduce((a, b) => a + b, 0) / lastScore.criteria_scores.length) : null;
|
||||
const icons = ['⚡', '🔧', '📝', '🔍', '🔄', '🎯', '📊', '🛠', '💡', '🧪', '📋', '💾', '💰', '🔄', '🎨'];
|
||||
const icon = icons[Math.floor(Math.random() * icons.length)];
|
||||
return `<div class="skill-card" onclick="showSkillDetail('${s.name}')">
|
||||
<div class="skill-card-header">
|
||||
<div class="skill-card-icon">${icon}</div>
|
||||
<div class="skill-card-name">${s.name.replace(/-/g, ' ')}</div>
|
||||
</div>
|
||||
<div id="skillDetailContent" class="loading">Loading...</div>
|
||||
</div>
|
||||
`;
|
||||
<div class="skill-card-desc">${s.description ? s.description.slice(0, 120) + (s.description.length > 120 ? '...' : '') : 'No description'}</div>
|
||||
<div class="skill-card-footer">
|
||||
${avg !== null ? `<span class="badge badge-success">${(avg * 100).toFixed(0)}%</span>` : '<span class="badge badge-info">New</span>'}
|
||||
${s.has_learnings ? '<span class="badge badge-accent">📖</span>' : ''}
|
||||
<button class="btn btn-sm btn-primary" style="margin-left:auto" onclick="event.stopPropagation();quickRunSkill('${s.name}')">▶ Run</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('')}</div>`;
|
||||
}
|
||||
|
||||
function switchSkillView(view) {
|
||||
document.querySelectorAll('#skillTabs .tab').forEach(t => t.classList.toggle('active', t.dataset.view === view));
|
||||
if (view === 'list') {
|
||||
const skills = window._allSkills || [];
|
||||
document.getElementById('skillsContainer').innerHTML = `<div class="table-wrapper"><table><thead><tr><th>Skill</th><th>Score</th><th>Learnings</th><th></th></tr></thead><tbody>${skills.map(s => {
|
||||
const lastScore = s.scores && s.scores.length > 0 ? s.scores[s.scores.length - 1] : null;
|
||||
const avg = lastScore && lastScore.criteria_scores ? (lastScore.criteria_scores.reduce((a, b) => a + b, 0) / lastScore.criteria_scores.length) : null;
|
||||
return `<tr onclick="showSkillDetail('${s.name}')" style="cursor:pointer">
|
||||
<td><strong>${s.name.replace(/-/g, ' ')}</strong></td>
|
||||
<td>${avg !== null ? `<span class="badge badge-success">${(avg * 100).toFixed(0)}%</span>` : '<span class="badge badge-info">—</span>'}</td>
|
||||
<td>${s.has_learnings ? '<span class="badge badge-accent">✓</span>' : '<span class="badge">—</span>'}</td>
|
||||
<td><button class="btn btn-sm btn-primary" onclick="event.stopPropagation();quickRunSkill('${s.name}')">▶</button></td>
|
||||
</tr>`;
|
||||
}).join('')}</tbody></table></div>`;
|
||||
} else {
|
||||
renderSkillGrid(window._allSkills || []);
|
||||
}
|
||||
}
|
||||
|
||||
function filterSkills() {
|
||||
const q = document.getElementById('skillFilter').value.toLowerCase();
|
||||
const skills = (window._allSkills || []).filter(s => s.name.toLowerCase().includes(q));
|
||||
renderSkillGrid(skills);
|
||||
}
|
||||
|
||||
async function showSkillDetail(name) {
|
||||
document.getElementById('skillsContainer').style.display = 'none';
|
||||
document.getElementById('skillTabs').style.display = 'none';
|
||||
document.getElementById('skillFilter').style.display = 'none';
|
||||
const detail = document.getElementById('skillDetail');
|
||||
detail.style.display = 'block';
|
||||
detail.innerHTML = `<div class="loading"><div class="loading-spinner"></div></div>`;
|
||||
|
||||
try {
|
||||
const skill = await api.getSkill(name);
|
||||
const evalHistory = await api.getSkillEval(name);
|
||||
const scores = evalHistory.scores || [];
|
||||
|
||||
let scoreHtml = '';
|
||||
if (scores.length > 0) {
|
||||
const recent = scores[scores.length - 1];
|
||||
const avg = recent.total_score || 0;
|
||||
scoreHtml = `
|
||||
<div style="margin:12px 0">
|
||||
<strong>Eval Score History:</strong>
|
||||
<div style="display:flex;gap:4px;margin-top:4px">
|
||||
${scores.slice(-10).map(s => `<span class="badge ${s.total_score > 0.7 ? 'badge-success' : 'badge-warning'}">${(s.total_score * 100).toFixed(0)}%</span>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
const scores = skill.score_history || [];
|
||||
const lastScore = scores.length > 0 ? scores[scores.length - 1] : null;
|
||||
const avg = lastScore && lastScore.criteria_scores ? (lastScore.criteria_scores.reduce((a, b) => a + b, 0) / lastScore.criteria_scores.length) : null;
|
||||
|
||||
detail.innerHTML = `
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">${name.replace(/-/g, ' ')}</span>
|
||||
<div style="display:flex;gap:8px">
|
||||
<button class="btn btn-primary btn-sm" onclick="quickRun('${name}')">▶ Run</button>
|
||||
<button class="btn btn-sm" onclick="backToSkills()">← Back</button>
|
||||
</div>
|
||||
<div style="margin-bottom:16px">
|
||||
<button class="btn btn-ghost" onclick="backToSkills()">← Back to Skills</button>
|
||||
<button class="btn btn-primary" style="margin-left:8px" onclick="quickRunSkill('${name}')">▶ Run ${name.replace(/-/g, ' ')}</button>
|
||||
</div>
|
||||
<div class="grid grid-2">
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-title">📄 SKILL.md</span></div>
|
||||
<pre style="max-height:400px;overflow:auto;font-size:12px">${escapeHtml(skill.skill || 'No SKILL.md')}</pre>
|
||||
</div>
|
||||
<div id="skillDetailContent">
|
||||
<div class="grid grid-2">
|
||||
<div>
|
||||
<h3 style="margin-bottom:8px">SKILL.md</h3>
|
||||
<pre style="max-height:400px;overflow:auto">${escapeHtml(skill.skill || 'No SKILL.md')}</pre>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-title">📖 Learnings</span></div>
|
||||
<pre style="max-height:400px;overflow:auto;font-size:12px">${escapeHtml(skill.learnings || 'No learnings yet')}</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-2 mt-3">
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-title">📊 Performance</span></div>
|
||||
${scores.length > 0 ? `
|
||||
<div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:12px">
|
||||
${scores.slice(-10).map(s => `<span class="badge ${(s.total_score || 0) > 0.7 ? 'badge-success' : 'badge-warning'}">${((s.total_score || 0) * 100).toFixed(0)}%</span>`).join('')}
|
||||
</div>
|
||||
<div>
|
||||
<h3 style="margin-bottom:8px">Learnings</h3>
|
||||
<pre style="max-height:400px;overflow:auto">${escapeHtml(skill.learnings || 'No learnings yet')}</pre>
|
||||
</div>
|
||||
</div>
|
||||
${scoreHtml}
|
||||
<div style="margin-top:12px">
|
||||
<strong>Context files:</strong> ${(skill.context || []).join(', ') || 'None'}
|
||||
</div>
|
||||
<div class="progress-bar"><div class="progress-fill" style="width:${(avg || 0) * 100}%"></div></div>
|
||||
<div style="font-size:12px;color:var(--text-muted);margin-top:6px">Average: ${avg !== null ? (avg * 100).toFixed(0) : 'N/A'}% (${scores.length} runs)</div>
|
||||
` : '<div style="color:var(--text-muted);font-size:13px">No evaluation scores yet</div>'}
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-title">📁 Context Files</span></div>
|
||||
${skill.context && skill.context.length > 0
|
||||
? `<div style="display:flex;flex-wrap:wrap;gap:6px">${skill.context.map(f => `<span class="badge badge-info">${f}</span>`).join('')}</div>`
|
||||
: '<div style="color:var(--text-muted);font-size:13px">No context files</div>'}
|
||||
${skill.eval && skill.eval.criteria ? `<div style="margin-top:12px"><strong style="font-size:12px">Eval Criteria:</strong><div style="display:flex;flex-wrap:wrap;gap:4px;margin-top:6px">${skill.eval.criteria.map(c => `<span class="badge badge-accent">${c}</span>`).join('')}</div></div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} catch (err) {
|
||||
document.getElementById('skillDetailContent').innerHTML = `<span style="color:var(--red)">Error: ${escapeHtml(err.message)}</span>`;
|
||||
detail.innerHTML = `<div class="empty-state"><div class="empty-state-icon">⚠</div><div class="empty-state-title">Error</div><div class="empty-state-desc">${escapeHtml(err.message)}</div><button class="btn btn-primary mt-3" onclick="backToSkills()">Back</button></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function backToSkills() {
|
||||
document.getElementById('skillsGrid').style.display = 'grid';
|
||||
document.getElementById('skillsContainer').style.display = '';
|
||||
document.getElementById('skillTabs').style.display = '';
|
||||
document.getElementById('skillFilter').style.display = '';
|
||||
document.getElementById('skillDetail').style.display = 'none';
|
||||
renderSkills();
|
||||
}
|
||||
|
||||
async function quickRun(name) {
|
||||
async function quickRunSkill(name) {
|
||||
const displayName = name.replace(/-/g, ' ');
|
||||
showModal(`Run: ${displayName}`, `
|
||||
<div class="form-group">
|
||||
<label class="form-label">Input (optional)</label>
|
||||
<textarea id="qrsInput" class="form-textarea" rows="3" placeholder="Enter input for ${displayName}..."></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Agent</label>
|
||||
<select id="qrsAgent" class="form-select">
|
||||
<option value="auto">Auto-detect</option>
|
||||
<option value="opencode">opencode</option>
|
||||
<option value="hermes">Hermes</option>
|
||||
<option value="gemini">Gemini CLI</option>
|
||||
</select>
|
||||
</div>
|
||||
`, `
|
||||
<button class="btn btn-ghost" onclick="closeModal()">Cancel</button>
|
||||
<button class="btn btn-primary" onclick="executeSkillRun('${name}')">▶ Run</button>
|
||||
`);
|
||||
}
|
||||
|
||||
async function executeSkillRun(name) {
|
||||
const input = document.getElementById('qrsInput').value;
|
||||
const agent = document.getElementById('qrsAgent').value;
|
||||
try {
|
||||
const r = await api.runSkill(name);
|
||||
showToast(`'${name}' dispatched to ${r.agent}`, 'success');
|
||||
const r = await api.runSkill(name, input, agent);
|
||||
closeModal();
|
||||
showToast(`"${name}" dispatched to ${r.agent} #${r.run_id}`, 'success');
|
||||
} catch (err) {
|
||||
showToast(`Error: ${err.message}`, 'error');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@ async function renderStandards() {
|
|||
const content = document.getElementById('pageContent');
|
||||
content.innerHTML = `
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<div class="page-header-left">
|
||||
<h1 class="page-title">Standards</h1>
|
||||
<p class="page-subtitle">Project standards and conventions</p>
|
||||
<p class="page-subtitle">Project conventions and coding standards</p>
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="runStandardsDiscovery()">🔍 Discover</button>
|
||||
<button class="btn btn-primary" onclick="runDiscovery()">🔍 Discover Patterns</button>
|
||||
</div>
|
||||
<div id="standardsContent"><div class="loading">Loading...</div></div>
|
||||
<div id="standardsContent"><div class="loading"><div class="loading-spinner"></div></div></div>
|
||||
`;
|
||||
|
||||
try {
|
||||
|
|
@ -20,37 +20,45 @@ async function renderStandards() {
|
|||
let html = '';
|
||||
|
||||
if (index) {
|
||||
html += `
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-title">Standards Index</span></div>
|
||||
<pre style="font-size:12px">${escapeHtml(index)}</pre>
|
||||
</div>
|
||||
`;
|
||||
html += `<div class="card"><div class="card-header"><span class="card-title">📐 Standards Index</span></div><pre style="font-size:12px">${escapeHtml(index)}</pre></div>`;
|
||||
}
|
||||
|
||||
if (standards.length === 0) {
|
||||
html += '<div class="empty-state"><div class="icon">📐</div><h3>No standards defined</h3><p>Run "Discover" to extract patterns from your codebase</p></div>';
|
||||
html += '<div class="empty-state"><div class="empty-state-icon">📐</div><div class="empty-state-title">No standards defined</div><div class="empty-state-desc">Run "Discover Patterns" to extract conventions from your codebase</div></div>';
|
||||
} else {
|
||||
standards.forEach(s => {
|
||||
html += `
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-title">${s.name}</span></div>
|
||||
<pre style="font-size:12px;max-height:300px;overflow:auto">${escapeHtml(s.content)}</pre>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
html += `<div class="grid grid-2">${standards.map(s => `
|
||||
<div class="card" style="cursor:pointer" onclick="viewStandard('${s.name}')">
|
||||
<div class="card-header"><span class="card-title">${s.name.replace(/-/g, ' ')}</span></div>
|
||||
<pre style="max-height:200px;overflow:hidden;font-size:12px">${escapeHtml(s.content.slice(0, 300))}${s.content.length > 300 ? '...' : ''}</pre>
|
||||
</div>
|
||||
`).join('')}</div>`;
|
||||
}
|
||||
|
||||
container.innerHTML = html;
|
||||
} catch (err) {
|
||||
document.getElementById('standardsContent').innerHTML = `<div class="empty-state"><div class="icon">⚠</div><p>${escapeHtml(err.message)}</p></div>`;
|
||||
document.getElementById('standardsContent').innerHTML = `<div class="empty-state"><div class="empty-state-icon">⚠</div><div class="empty-state-title">${escapeHtml(err.message)}</div></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function runStandardsDiscovery() {
|
||||
async function viewStandard(name) {
|
||||
let content = '';
|
||||
try {
|
||||
const data = await api.getStandards();
|
||||
const std = (data.standards || []).find(s => s.name === name);
|
||||
if (std) content = std.content;
|
||||
} catch {}
|
||||
|
||||
showModal(`Standard: ${name.replace(/-/g, ' ')}`, `
|
||||
<pre style="white-space:pre-wrap;font-size:12px;max-height:60vh;overflow:auto">${escapeHtml(content)}</pre>
|
||||
`, `
|
||||
<button class="btn btn-ghost" onclick="closeModal()">Close</button>
|
||||
`);
|
||||
}
|
||||
|
||||
async function runDiscovery() {
|
||||
try {
|
||||
const r = await api.discoverStandards();
|
||||
showToast(r.message, 'success');
|
||||
showToast(r.message || 'Discovery started', 'success');
|
||||
} catch (err) {
|
||||
showToast(`Error: ${err.message}`, 'error');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,328 +1,620 @@
|
|||
:root {
|
||||
--bg: #0f1117;
|
||||
--bg2: #1a1d2e;
|
||||
--bg3: #232738;
|
||||
--text: #e1e4ed;
|
||||
--text2: #8b8fa3;
|
||||
--bg-primary: #0a0e1a;
|
||||
--bg-secondary: #111627;
|
||||
--bg-card: #1a1f3a;
|
||||
--bg-card-hover: #222845;
|
||||
--bg-input: #0d1225;
|
||||
--border: #2a3158;
|
||||
--border-hover: #3d46a0;
|
||||
--text-primary: #e8eaf0;
|
||||
--text-secondary: #8b90b0;
|
||||
--text-muted: #5a6080;
|
||||
--accent: #6c5ce7;
|
||||
--accent2: #a29bfe;
|
||||
--green: #00b894;
|
||||
--red: #ff6b6b;
|
||||
--yellow: #fdcb6e;
|
||||
--blue: #74b9ff;
|
||||
--border: #2d3142;
|
||||
--radius: 8px;
|
||||
--shadow: 0 2px 8px rgba(0,0,0,0.3);
|
||||
--accent-light: #8b7cf7;
|
||||
--accent-glow: rgba(108, 92, 231, 0.25);
|
||||
--green: #00d4aa;
|
||||
--green-dim: rgba(0, 212, 170, 0.15);
|
||||
--red: #ff4757;
|
||||
--red-dim: rgba(255, 71, 87, 0.15);
|
||||
--yellow: #ffa502;
|
||||
--yellow-dim: rgba(255, 165, 2, 0.15);
|
||||
--blue: #45aaf2;
|
||||
--blue-dim: rgba(69, 170, 242, 0.15);
|
||||
--pink: #fd79a8;
|
||||
--purple: #a29bfe;
|
||||
--radius-sm: 6px;
|
||||
--radius: 10px;
|
||||
--radius-lg: 16px;
|
||||
--shadow: 0 4px 24px rgba(0, 0, 0, 0.3);
|
||||
--shadow-lg: 0 8px 48px rgba(0, 0, 0, 0.4);
|
||||
--transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--font: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
||||
--font-mono: 'JetBrains Mono', 'Fira Code', 'SF Mono', monospace;
|
||||
--sidebar-width: 260px;
|
||||
}
|
||||
|
||||
[data-theme="light"] {
|
||||
--bg: #f8f9fa;
|
||||
--bg2: #ffffff;
|
||||
--bg3: #e9ecef;
|
||||
--text: #212529;
|
||||
--text2: #6c757d;
|
||||
--border: #dee2e6;
|
||||
--shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||
--bg-primary: #f0f2f8;
|
||||
--bg-secondary: #ffffff;
|
||||
--bg-card: #ffffff;
|
||||
--bg-card-hover: #f5f7fe;
|
||||
--bg-input: #f0f2f8;
|
||||
--border: #e2e5f0;
|
||||
--border-hover: #6c5ce7;
|
||||
--text-primary: #1a1d2e;
|
||||
--text-secondary: #6b7194;
|
||||
--text-muted: #9ea3c0;
|
||||
--shadow: 0 4px 24px rgba(0, 0, 0, 0.06);
|
||||
--shadow-lg: 0 8px 48px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
html { font-size: 15px; scroll-behavior: smooth; }
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: var(--font);
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
transition: background 0.3s, color 0.3s;
|
||||
overflow: hidden;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
::selection { background: var(--accent-glow); color: white; }
|
||||
::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }
|
||||
|
||||
/* ─── Sidebar ─── */
|
||||
.sidebar {
|
||||
width: 240px;
|
||||
min-width: 240px;
|
||||
background: var(--bg2);
|
||||
width: var(--sidebar-width);
|
||||
min-width: var(--sidebar-width);
|
||||
background: var(--bg-secondary);
|
||||
border-right: 1px solid var(--border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
transition: width 0.3s, min-width 0.3s;
|
||||
z-index: 100;
|
||||
transition: var(--transition);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sidebar.closed {
|
||||
width: 60px;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.sidebar.closed .logo-text,
|
||||
.sidebar.closed .nav-item span:not(.nav-icon),
|
||||
.sidebar.closed .sidebar-footer span {
|
||||
display: none;
|
||||
.sidebar.collapsed {
|
||||
width: 68px;
|
||||
min-width: 68px;
|
||||
}
|
||||
.sidebar.collapsed .logo-text,
|
||||
.sidebar.collapsed .nav-label,
|
||||
.sidebar.collapsed .sidebar-section-label,
|
||||
.sidebar.collapsed .agent-status-text,
|
||||
.sidebar.collapsed .theme-label { display: none; }
|
||||
.sidebar.collapsed .nav-item { justify-content: center; padding: 12px; }
|
||||
.sidebar.collapsed .sidebar-header { justify-content: center; }
|
||||
|
||||
.sidebar-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px;
|
||||
padding: 18px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
min-height: 64px;
|
||||
}
|
||||
|
||||
.logo { display: flex; align-items: center; gap: 8px; }
|
||||
.logo-icon { font-size: 24px; color: var(--accent); }
|
||||
.logo-text { font-weight: 700; font-size: 16px; }
|
||||
.logo { display: flex; align-items: center; gap: 10px; }
|
||||
.logo-icon {
|
||||
width: 32px; height: 32px;
|
||||
background: linear-gradient(135deg, var(--accent), var(--pink));
|
||||
border-radius: 8px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 18px; font-weight: 800; color: white;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.logo-text { font-weight: 700; font-size: 16px; white-space: nowrap; }
|
||||
|
||||
.sidebar-toggle {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text2);
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
background: none; border: none; color: var(--text-muted);
|
||||
font-size: 20px; cursor: pointer; padding: 4px;
|
||||
border-radius: var(--radius-sm); transition: var(--transition);
|
||||
width: 28px; height: 28px; display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.sidebar-toggle:hover { background: var(--bg-card); color: var(--text-primary); }
|
||||
|
||||
.sidebar-section { padding: 8px 12px 4px; }
|
||||
.sidebar-section-label {
|
||||
font-size: 10px; font-weight: 700; text-transform: uppercase;
|
||||
letter-spacing: 1px; color: var(--text-muted);
|
||||
padding: 8px 8px 4px; white-space: nowrap;
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
flex: 1;
|
||||
padding: 8px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.sidebar-nav { flex: 1; overflow-y: auto; overflow-x: hidden; padding: 4px 8px; }
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
color: var(--text2);
|
||||
text-decoration: none;
|
||||
border-radius: var(--radius);
|
||||
margin-bottom: 2px;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s;
|
||||
cursor: pointer;
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
padding: 10px 12px; margin: 1px 0;
|
||||
color: var(--text-secondary); text-decoration: none;
|
||||
border-radius: var(--radius); font-size: 13px; font-weight: 500;
|
||||
transition: var(--transition); cursor: pointer; white-space: nowrap;
|
||||
position: relative;
|
||||
}
|
||||
.nav-item:hover { background: var(--bg-card); color: var(--text-primary); }
|
||||
.nav-item.active {
|
||||
background: var(--accent-glow); color: var(--accent-light);
|
||||
}
|
||||
.nav-item.active::before {
|
||||
content: ''; position: absolute; left: -8px; top: 50%; transform: translateY(-50%);
|
||||
width: 3px; height: 20px; background: var(--accent); border-radius: 0 3px 3px 0;
|
||||
}
|
||||
.nav-icon { font-size: 16px; width: 22px; text-align: center; flex-shrink: 0; }
|
||||
.nav-badge {
|
||||
margin-left: auto; font-size: 10px; font-weight: 700;
|
||||
padding: 2px 6px; border-radius: 10px;
|
||||
background: var(--accent-glow); color: var(--accent-light);
|
||||
}
|
||||
|
||||
.nav-item:hover { background: var(--bg3); color: var(--text); }
|
||||
.nav-item.active { background: var(--accent); color: white; }
|
||||
.nav-icon { font-size: 16px; width: 20px; text-align: center; }
|
||||
|
||||
.sidebar-footer {
|
||||
padding: 12px;
|
||||
border-top: 1px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px; border-top: 1px solid var(--border);
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
}
|
||||
|
||||
.agent-status { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text2); }
|
||||
.status-dot { width: 8px; height: 8px; border-radius: 50%; }
|
||||
.status-dot.online { background: var(--green); }
|
||||
.status-dot.offline { background: var(--red); }
|
||||
.status-dot.warning { background: var(--yellow); }
|
||||
|
||||
.theme-toggle button {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
.agent-status-bar { display: flex; align-items: center; gap: 8px; font-size: 12px; }
|
||||
.agent-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
||||
.agent-dot.online { background: var(--green); box-shadow: 0 0 8px var(--green-dim); }
|
||||
.agent-dot.offline { background: var(--red); box-shadow: 0 0 8px var(--red-dim); }
|
||||
.agent-dot.warning { background: var(--yellow); box-shadow: 0 0 8px var(--yellow-dim); }
|
||||
.agent-status-text { color: var(--text-muted); font-size: 11px; white-space: nowrap; }
|
||||
.theme-btn {
|
||||
background: var(--bg-card); border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm); cursor: pointer;
|
||||
width: 30px; height: 30px; display: flex; align-items: center; justify-content: center;
|
||||
font-size: 14px; transition: var(--transition);
|
||||
}
|
||||
.theme-btn:hover { background: var(--bg-card-hover); border-color: var(--border-hover); }
|
||||
|
||||
/* Main Content */
|
||||
/* ─── Main Content ─── */
|
||||
.main-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 24px;
|
||||
max-width: 1200px;
|
||||
flex: 1; display: flex; flex-direction: column;
|
||||
height: 100vh; overflow: hidden;
|
||||
}
|
||||
|
||||
.page-content { animation: fadeIn 0.3s ease; }
|
||||
@keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
|
||||
.topbar {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 12px 28px; border-bottom: 1px solid var(--border);
|
||||
min-height: 56px; background: var(--bg-secondary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.topbar-left { display: flex; align-items: center; gap: 12px; }
|
||||
.topbar-title { font-size: 16px; font-weight: 600; }
|
||||
.topbar-breadcrumb { font-size: 12px; color: var(--text-muted); }
|
||||
.topbar-right { display: flex; align-items: center; gap: 12px; }
|
||||
.topbar-search {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
background: var(--bg-input); border: 1px solid var(--border);
|
||||
border-radius: var(--radius); padding: 6px 12px;
|
||||
color: var(--text-muted); font-size: 13px;
|
||||
}
|
||||
.topbar-search input {
|
||||
background: none; border: none; color: var(--text-primary);
|
||||
font-size: 13px; outline: none; width: 180px;
|
||||
}
|
||||
.topbar-search input::placeholder { color: var(--text-muted); }
|
||||
|
||||
.page-content {
|
||||
flex: 1; overflow-y: auto; padding: 24px 28px;
|
||||
}
|
||||
|
||||
/* ─── Page Header ─── */
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.page-header-left {}
|
||||
.page-title { font-size: 22px; font-weight: 700; }
|
||||
.page-subtitle { font-size: 13px; color: var(--text-secondary); margin-top: 2px; }
|
||||
|
||||
.page-title { font-size: 24px; font-weight: 700; }
|
||||
.page-subtitle { font-size: 14px; color: var(--text2); margin-top: 4px; }
|
||||
|
||||
/* Cards */
|
||||
/* ─── Cards ─── */
|
||||
.card {
|
||||
background: var(--bg2);
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 20px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: var(--shadow);
|
||||
transition: var(--transition);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.card:hover { border-color: var(--border-hover); }
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.card-title { font-size: 14px; font-weight: 600; }
|
||||
|
||||
.card-title { font-size: 16px; font-weight: 600; }
|
||||
.card-value { font-size: 32px; font-weight: 700; color: var(--accent); }
|
||||
.card-label { font-size: 12px; color: var(--text2); margin-top: 2px; }
|
||||
.stat-card {
|
||||
position: relative; overflow: hidden;
|
||||
padding: 20px;
|
||||
}
|
||||
.stat-card::after {
|
||||
content: ''; position: absolute; top: 0; right: 0;
|
||||
width: 120px; height: 120px;
|
||||
background: radial-gradient(circle, rgba(108,92,231,0.08) 0%, transparent 70%);
|
||||
border-radius: 50%; transform: translate(30%, -30%);
|
||||
}
|
||||
.stat-icon {
|
||||
width: 40px; height: 40px; border-radius: var(--radius);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 18px; margin-bottom: 12px;
|
||||
}
|
||||
.stat-icon.purple { background: var(--accent-glow); color: var(--accent-light); }
|
||||
.stat-icon.green { background: var(--green-dim); color: var(--green); }
|
||||
.stat-icon.blue { background: var(--blue-dim); color: var(--blue); }
|
||||
.stat-icon.yellow { background: var(--yellow-dim); color: var(--yellow); }
|
||||
.stat-icon.red { background: var(--red-dim); color: var(--red); }
|
||||
.stat-value { font-size: 28px; font-weight: 800; letter-spacing: -0.5px; }
|
||||
.stat-label { font-size: 12px; color: var(--text-secondary); margin-top: 2px; }
|
||||
.stat-change {
|
||||
font-size: 11px; font-weight: 600; margin-top: 6px;
|
||||
display: inline-flex; align-items: center; gap: 3px;
|
||||
}
|
||||
.stat-change.up { color: var(--green); }
|
||||
.stat-change.down { color: var(--red); }
|
||||
|
||||
/* Grid */
|
||||
/* ─── Grid ─── */
|
||||
.grid { display: grid; gap: 16px; }
|
||||
.grid-2 { grid-template-columns: repeat(2, 1fr); }
|
||||
.grid-3 { grid-template-columns: repeat(3, 1fr); }
|
||||
.grid-4 { grid-template-columns: repeat(4, 1fr); }
|
||||
|
||||
/* Buttons */
|
||||
/* ─── Buttons ─── */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 16px;
|
||||
border-radius: var(--radius);
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
padding: 8px 16px; border-radius: var(--radius);
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg2);
|
||||
color: var(--text);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
background: var(--bg-card);
|
||||
color: var(--text-primary);
|
||||
font-size: 13px; font-weight: 500;
|
||||
cursor: pointer; transition: var(--transition);
|
||||
white-space: nowrap; font-family: var(--font);
|
||||
}
|
||||
.btn:hover { background: var(--bg-card-hover); border-color: var(--border-hover); }
|
||||
.btn:active { transform: scale(0.97); }
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--accent), #5a4bd1);
|
||||
color: white; border-color: transparent;
|
||||
}
|
||||
.btn-primary:hover {
|
||||
background: linear-gradient(135deg, #7b6df7, #6c5ce7);
|
||||
box-shadow: 0 4px 16px var(--accent-glow);
|
||||
}
|
||||
.btn-danger { background: var(--red); color: white; border-color: transparent; }
|
||||
.btn-danger:hover { box-shadow: 0 4px 16px var(--red-dim); }
|
||||
.btn-ghost { background: transparent; border-color: transparent; color: var(--text-secondary); }
|
||||
.btn-ghost:hover { background: var(--bg-card); color: var(--text-primary); }
|
||||
.btn-sm { padding: 5px 10px; font-size: 12px; }
|
||||
.btn-xs { padding: 3px 8px; font-size: 11px; }
|
||||
.btn-icon { width: 34px; height: 34px; padding: 0; justify-content: center; }
|
||||
.btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
|
||||
|
||||
.btn:hover { background: var(--bg3); }
|
||||
.btn-primary { background: var(--accent); color: white; border-color: var(--accent); }
|
||||
.btn-primary:hover { background: var(--accent2); }
|
||||
.btn-danger { background: var(--red); color: white; border-color: var(--red); }
|
||||
.btn-sm { padding: 4px 10px; font-size: 12px; }
|
||||
.btn-group { display: flex; gap: 6px; }
|
||||
|
||||
/* Tables */
|
||||
.table-wrapper { overflow-x: auto; }
|
||||
/* ─── Tables ─── */
|
||||
.table-wrapper {
|
||||
overflow-x: auto; border-radius: var(--radius);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th, td { padding: 10px 12px; text-align: left; border-bottom: 1px solid var(--border); font-size: 13px; }
|
||||
th { color: var(--text2); font-weight: 600; font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px; }
|
||||
tr:hover { background: var(--bg3); }
|
||||
|
||||
/* Forms */
|
||||
input, textarea, select {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-size: 13px;
|
||||
transition: border-color 0.2s;
|
||||
th, td { padding: 10px 14px; text-align: left; font-size: 13px; }
|
||||
th {
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-muted); font-weight: 600; font-size: 11px;
|
||||
text-transform: uppercase; letter-spacing: 0.5px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
td { border-bottom: 1px solid var(--border); }
|
||||
tr:last-child td { border-bottom: none; }
|
||||
tr:hover td { background: var(--bg-card-hover); }
|
||||
|
||||
input:focus, textarea:focus, select:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
textarea { min-height: 120px; resize: vertical; font-family: 'SF Mono', 'Fira Code', monospace; }
|
||||
|
||||
/* ─── Forms ─── */
|
||||
.form-group { margin-bottom: 16px; }
|
||||
.form-label { display: block; font-size: 12px; color: var(--text2); margin-bottom: 4px; font-weight: 600; }
|
||||
.form-label {
|
||||
display: block; font-size: 12px; font-weight: 600;
|
||||
color: var(--text-secondary); margin-bottom: 5px;
|
||||
}
|
||||
.form-input, .form-select, .form-textarea {
|
||||
width: 100%; padding: 9px 12px;
|
||||
border: 1px solid var(--border); border-radius: var(--radius);
|
||||
background: var(--bg-input); color: var(--text-primary);
|
||||
font-size: 13px; font-family: var(--font);
|
||||
transition: var(--transition); outline: none;
|
||||
}
|
||||
.form-input:focus, .form-select:focus, .form-textarea:focus {
|
||||
border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-glow);
|
||||
}
|
||||
.form-textarea { min-height: 100px; resize: vertical; font-family: var(--font-mono); font-size: 12px; }
|
||||
.form-hint { font-size: 11px; color: var(--text-muted); margin-top: 3px; }
|
||||
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
||||
.form-error { color: var(--red); font-size: 12px; margin-top: 3px; }
|
||||
|
||||
/* Badges */
|
||||
/* ─── Badges ─── */
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
display: inline-flex; align-items: center; gap: 4px;
|
||||
padding: 2px 8px; border-radius: 12px;
|
||||
font-size: 10px; font-weight: 600; letter-spacing: 0.3px;
|
||||
}
|
||||
.badge-success { background: var(--green-dim); color: var(--green); }
|
||||
.badge-warning { background: var(--yellow-dim); color: var(--yellow); }
|
||||
.badge-danger { background: var(--red-dim); color: var(--red); }
|
||||
.badge-info { background: var(--blue-dim); color: var(--blue); }
|
||||
.badge-accent { background: var(--accent-glow); color: var(--accent-light); }
|
||||
|
||||
/* ─── Skill Card ─── */
|
||||
.skill-card {
|
||||
background: var(--bg-card); border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg); padding: 18px;
|
||||
transition: var(--transition); cursor: pointer;
|
||||
display: flex; flex-direction: column;
|
||||
}
|
||||
.skill-card:hover {
|
||||
border-color: var(--accent); transform: translateY(-2px);
|
||||
box-shadow: 0 8px 32px rgba(108, 92, 231, 0.12);
|
||||
}
|
||||
.skill-card-header { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
|
||||
.skill-card-icon {
|
||||
width: 36px; height: 36px; border-radius: var(--radius);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 16px; flex-shrink: 0;
|
||||
background: var(--accent-glow); color: var(--accent-light);
|
||||
}
|
||||
.skill-card-name { font-weight: 600; font-size: 14px; }
|
||||
.skill-card-desc { font-size: 12px; color: var(--text-secondary); flex: 1; line-height: 1.4; }
|
||||
.skill-card-footer {
|
||||
display: flex; align-items: center; gap: 8px; margin-top: 12px; padding-top: 12px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.badge-success { background: rgba(0,184,148,0.15); color: var(--green); }
|
||||
.badge-warning { background: rgba(253,203,110,0.15); color: var(--yellow); }
|
||||
.badge-danger { background: rgba(255,107,107,0.15); color: var(--red); }
|
||||
.badge-info { background: rgba(116,185,255,0.15); color: var(--blue); }
|
||||
|
||||
/* Charts */
|
||||
/* ─── Chart ─── */
|
||||
.chart-container {
|
||||
position: relative;
|
||||
height: 250px;
|
||||
margin: 16px 0;
|
||||
position: relative; height: 240px; width: 100%;
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
/* Loading */
|
||||
.loading { text-align: center; padding: 60px; color: var(--text2); }
|
||||
|
||||
/* Toast */
|
||||
.toast-container {
|
||||
position: fixed;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
/* ─── Loading ─── */
|
||||
.loading {
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
padding: 60px; color: var(--text-muted); gap: 10px;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.loading-spinner {
|
||||
width: 32px; height: 32px;
|
||||
border: 3px solid var(--border);
|
||||
border-top-color: var(--accent);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
|
||||
.skeleton {
|
||||
background: linear-gradient(90deg, var(--bg-card) 25%, var(--bg-card-hover) 50%, var(--bg-card) 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.5s ease-in-out infinite;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
@keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
|
||||
|
||||
/* ─── Toast ─── */
|
||||
.toast-container {
|
||||
position: fixed; bottom: 24px; right: 24px; z-index: 9999;
|
||||
display: flex; flex-direction: column; gap: 8px;
|
||||
pointer-events: none;
|
||||
}
|
||||
.toast {
|
||||
background: var(--bg2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 12px 16px;
|
||||
box-shadow: var(--shadow);
|
||||
font-size: 13px;
|
||||
animation: slideIn 0.3s ease;
|
||||
max-width: 360px;
|
||||
pointer-events: auto;
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
background: var(--bg-card); border: 1px solid var(--border);
|
||||
border-radius: var(--radius); padding: 12px 18px;
|
||||
box-shadow: var(--shadow-lg);
|
||||
font-size: 13px; font-weight: 500;
|
||||
animation: toastSlideUp 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
max-width: 400px;
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
.toast-icon { font-size: 16px; flex-shrink: 0; }
|
||||
.toast.success { border-left: 3px solid var(--green); }
|
||||
.toast.error { border-left: 3px solid var(--red); }
|
||||
.toast.info { border-left: 3px solid var(--blue); }
|
||||
|
||||
@keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
|
||||
|
||||
/* Skill Cards */
|
||||
.skill-card {
|
||||
background: var(--bg2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 16px;
|
||||
transition: all 0.2s;
|
||||
cursor: pointer;
|
||||
.toast.warning { border-left: 3px solid var(--yellow); }
|
||||
@keyframes toastSlideUp {
|
||||
from { transform: translateY(20px); opacity: 0; }
|
||||
to { transform: translateY(0); opacity: 1; }
|
||||
}
|
||||
|
||||
.skill-card:hover { border-color: var(--accent); transform: translateY(-2px); box-shadow: var(--shadow); }
|
||||
.skill-card .skill-name { font-size: 14px; font-weight: 600; margin-bottom: 4px; }
|
||||
.skill-card .skill-desc { font-size: 12px; color: var(--text2); }
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.sidebar { width: 60px; min-width: 60px; }
|
||||
.sidebar .logo-text,
|
||||
.sidebar .nav-item span:not(.nav-icon),
|
||||
.sidebar .sidebar-footer span { display: none; }
|
||||
.grid-2, .grid-3, .grid-4 { grid-template-columns: 1fr; }
|
||||
.main-content { padding: 16px; }
|
||||
/* ─── Modal ─── */
|
||||
.modal-overlay {
|
||||
position: fixed; inset: 0; z-index: 5000;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
backdrop-filter: blur(4px);
|
||||
animation: fadeIn 0.2s ease;
|
||||
}
|
||||
|
||||
/* Code */
|
||||
pre, code {
|
||||
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
|
||||
font-size: 13px;
|
||||
.modal {
|
||||
background: var(--bg-card); border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg); width: 90%; max-width: 560px;
|
||||
max-height: 80vh; overflow-y: auto;
|
||||
box-shadow: var(--shadow-lg);
|
||||
animation: modalSlideUp 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
pre {
|
||||
background: var(--bg);
|
||||
padding: 12px;
|
||||
border-radius: var(--radius);
|
||||
overflow-x: auto;
|
||||
border: 1px solid var(--border);
|
||||
.modal-header {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 18px 20px; border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.modal-title { font-size: 16px; font-weight: 700; }
|
||||
.modal-close {
|
||||
background: none; border: none; color: var(--text-muted);
|
||||
font-size: 20px; cursor: pointer; padding: 4px; border-radius: var(--radius-sm);
|
||||
width: 30px; height: 30px; display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.modal-close:hover { background: var(--bg-card-hover); color: var(--text-primary); }
|
||||
.modal-body { padding: 20px; }
|
||||
.modal-footer {
|
||||
display: flex; align-items: center; justify-content: flex-end; gap: 8px;
|
||||
padding: 14px 20px; border-top: 1px solid var(--border);
|
||||
}
|
||||
@keyframes modalSlideUp { from { transform: translateY(20px) scale(0.97); opacity: 0; } to { transform: translateY(0) scale(1); opacity: 1; } }
|
||||
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
||||
|
||||
code { background: var(--bg3); padding: 2px 4px; border-radius: 3px; }
|
||||
|
||||
/* Empty state */
|
||||
/* ─── Empty State ─── */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: var(--text2);
|
||||
text-align: center; padding: 48px 24px; color: var(--text-muted);
|
||||
display: flex; flex-direction: column; align-items: center; gap: 12px;
|
||||
}
|
||||
.empty-state-icon { font-size: 48px; opacity: 0.4; }
|
||||
.empty-state-title { font-size: 16px; font-weight: 600; }
|
||||
.empty-state-desc { font-size: 13px; max-width: 360px; }
|
||||
|
||||
/* ─── Code / Pre ─── */
|
||||
code, pre { font-family: var(--font-mono); font-size: 12px; }
|
||||
pre {
|
||||
background: var(--bg-primary);
|
||||
padding: 14px; border-radius: var(--radius);
|
||||
overflow-x: auto; border: 1px solid var(--border);
|
||||
line-height: 1.5; color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.empty-state .icon { font-size: 40px; margin-bottom: 12px; }
|
||||
/* ─── Tabs ─── */
|
||||
.tabs {
|
||||
display: flex; gap: 2px; border-bottom: 1px solid var(--border);
|
||||
margin-bottom: 16px; overflow-x: auto;
|
||||
}
|
||||
.tab {
|
||||
padding: 10px 16px; font-size: 13px; font-weight: 500;
|
||||
color: var(--text-muted); cursor: pointer; white-space: nowrap;
|
||||
border-bottom: 2px solid transparent; transition: var(--transition);
|
||||
background: none; border-top: none; border-left: none; border-right: none;
|
||||
font-family: var(--font);
|
||||
}
|
||||
.tab:hover { color: var(--text-primary); }
|
||||
.tab.active { color: var(--accent-light); border-bottom-color: var(--accent); }
|
||||
|
||||
/* ─── Event / Activity Items ─── */
|
||||
.event-list { display: flex; flex-direction: column; gap: 0; }
|
||||
.event-item {
|
||||
display: flex; align-items: flex-start; gap: 12px;
|
||||
padding: 12px 0; border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.event-item:last-child { border-bottom: none; }
|
||||
.event-dot {
|
||||
width: 8px; height: 8px; border-radius: 50%;
|
||||
margin-top: 6px; flex-shrink: 0;
|
||||
}
|
||||
.event-content { flex: 1; }
|
||||
.event-title { font-size: 13px; font-weight: 500; }
|
||||
.event-meta { font-size: 11px; color: var(--text-muted); margin-top: 2px; }
|
||||
.event-time { font-size: 11px; color: var(--text-muted); white-space: nowrap; }
|
||||
|
||||
/* ─── Status Indicators ─── */
|
||||
.status-indicator {
|
||||
display: flex; align-items: center; gap: 6px;
|
||||
padding: 6px 12px; border-radius: 20px;
|
||||
font-size: 12px; font-weight: 600;
|
||||
}
|
||||
.status-indicator.online { background: var(--green-dim); color: var(--green); }
|
||||
.status-indicator.offline { background: var(--red-dim); color: var(--red); }
|
||||
.status-indicator.warning { background: var(--yellow-dim); color: var(--yellow); }
|
||||
|
||||
/* ─── Agent Status Cards ─── */
|
||||
.agent-card-list { display: flex; gap: 10px; flex-wrap: wrap; }
|
||||
.agent-card {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
padding: 10px 14px; border-radius: var(--radius);
|
||||
border: 1px solid var(--border); background: var(--bg-card);
|
||||
flex: 1; min-width: 140px; transition: var(--transition);
|
||||
}
|
||||
.agent-card:hover { border-color: var(--border-hover); }
|
||||
|
||||
/* ─── Page Transitions ─── */
|
||||
.page-enter {
|
||||
animation: pageIn 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
@keyframes pageIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* ─── Responsive ─── */
|
||||
@media (max-width: 900px) {
|
||||
.grid-4 { grid-template-columns: repeat(2, 1fr); }
|
||||
.grid-3 { grid-template-columns: repeat(2, 1fr); }
|
||||
.sidebar:not(.collapsed) { width: 68px; min-width: 68px; }
|
||||
.sidebar:not(.collapsed) .logo-text,
|
||||
.sidebar:not(.collapsed) .nav-label,
|
||||
.sidebar:not(.collapsed) .sidebar-section-label,
|
||||
.sidebar:not(.collapsed) .agent-status-text,
|
||||
.sidebar:not(.collapsed) .theme-label { display: none; }
|
||||
.sidebar:not(.collapsed) .nav-item { justify-content: center; padding: 12px; }
|
||||
.sidebar:not(.collapsed) .sidebar-header { justify-content: center; }
|
||||
.topbar-search { display: none; }
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.grid-2, .grid-3, .grid-4 { grid-template-columns: 1fr; }
|
||||
.page-content { padding: 16px; }
|
||||
.form-row { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
/* ─── Quick Run Input ─── */
|
||||
.quick-run {
|
||||
display: flex; gap: 8px; align-items: center;
|
||||
}
|
||||
.quick-run input { flex: 1; }
|
||||
|
||||
/* ─── Progress Bar ─── */
|
||||
.progress-bar {
|
||||
width: 100%; height: 4px; background: var(--bg-primary);
|
||||
border-radius: 2px; overflow: hidden;
|
||||
}
|
||||
.progress-fill {
|
||||
height: 100%; border-radius: 2px;
|
||||
background: linear-gradient(90deg, var(--accent), var(--pink));
|
||||
transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
/* ─── Switch / Toggle ─── */
|
||||
.switch {
|
||||
position: relative; width: 40px; height: 22px; flex-shrink: 0;
|
||||
}
|
||||
.switch input { opacity: 0; width: 0; height: 0; }
|
||||
.switch-slider {
|
||||
position: absolute; cursor: pointer; inset: 0;
|
||||
background: var(--bg-input); border-radius: 11px;
|
||||
border: 1px solid var(--border); transition: var(--transition);
|
||||
}
|
||||
.switch-slider::before {
|
||||
content: ''; position: absolute; width: 16px; height: 16px;
|
||||
left: 2px; bottom: 2px; background: var(--text-muted);
|
||||
border-radius: 50%; transition: var(--transition);
|
||||
}
|
||||
.switch input:checked + .switch-slider { background: var(--accent-glow); border-color: var(--accent); }
|
||||
.switch input:checked + .switch-slider::before { transform: translateX(18px); background: var(--accent); }
|
||||
|
||||
/* ─── Divider ─── */
|
||||
.divider { height: 1px; background: var(--border); margin: 16px 0; }
|
||||
|
||||
/* ─── Flex helpers ─── */
|
||||
.flex { display: flex; }
|
||||
.flex-col { flex-direction: column; }
|
||||
.items-center { align-items: center; }
|
||||
.justify-between { justify-content: space-between; }
|
||||
.gap-2 { gap: 8px; }
|
||||
.gap-3 { gap: 12px; }
|
||||
.gap-4 { gap: 16px; }
|
||||
.mt-2 { margin-top: 8px; }
|
||||
.mt-3 { margin-top: 12px; }
|
||||
.mt-4 { margin-top: 16px; }
|
||||
.mb-2 { margin-bottom: 8px; }
|
||||
.mb-3 { margin-bottom: 12px; }
|
||||
.mb-4 { margin-bottom: 16px; }
|
||||
.text-muted { color: var(--text-muted); }
|
||||
.text-sm { font-size: 12px; }
|
||||
.text-xs { font-size: 11px; }
|
||||
.font-mono { font-family: var(--font-mono); }
|
||||
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
function showToast(message, type = 'info') {
|
||||
const container = document.getElementById('toastContainer');
|
||||
const icons = { success: '✓', error: '✕', warning: '⚠', info: 'ℹ' };
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast ${type}`;
|
||||
toast.textContent = message;
|
||||
toast.innerHTML = `<span class="toast-icon">${icons[type] || icons.info}</span><span>${message}</span>`;
|
||||
container.appendChild(toast);
|
||||
setTimeout(() => { toast.remove(); }, 3000);
|
||||
setTimeout(() => { toast.style.opacity = '0'; toast.style.transform = 'translateX(20px)'; setTimeout(() => toast.remove(), 300); }, 3500);
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
if (!str) return '';
|
||||
const div = document.createElement('div');
|
||||
div.textContent = str;
|
||||
return div.innerHTML;
|
||||
|
|
@ -15,40 +17,99 @@ function escapeHtml(str) {
|
|||
|
||||
function formatDate(iso) {
|
||||
if (!iso) return '-';
|
||||
return new Date(iso).toLocaleString();
|
||||
try { return new Date(iso).toLocaleDateString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); }
|
||||
catch { return iso; }
|
||||
}
|
||||
|
||||
function timeAgo(iso) {
|
||||
if (!iso) return '-';
|
||||
const diff = Date.now() - new Date(iso).getTime();
|
||||
const mins = Math.floor(diff / 60000);
|
||||
if (mins < 1) return 'just now';
|
||||
if (mins < 60) return `${mins}m ago`;
|
||||
const hrs = Math.floor(mins / 60);
|
||||
if (hrs < 24) return `${hrs}h ago`;
|
||||
const days = Math.floor(hrs / 24);
|
||||
return `${days}d ago`;
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (bytes === 0) return '0 B';
|
||||
if (!bytes || bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
function getStatusColor(status) {
|
||||
switch (status) {
|
||||
case 'online': case 'healthy': case 'pass': return 'var(--green)';
|
||||
case 'warn': case 'warning': return 'var(--yellow)';
|
||||
case 'offline': case 'error': case 'fail': return 'var(--red)';
|
||||
default: return 'var(--text2)';
|
||||
}
|
||||
function statusColor(status) {
|
||||
const s = (status || '').toLowerCase();
|
||||
if (['online', 'healthy', 'active', 'pass', 'ok'].includes(s)) return { bg: 'var(--green-dim)', dot: 'var(--green)', text: 'var(--green)' };
|
||||
if (['warning', 'warn', 'degraded'].includes(s)) return { bg: 'var(--yellow-dim)', dot: 'var(--yellow)', text: 'var(--yellow)' };
|
||||
if (['offline', 'error', 'fail', 'down'].includes(s)) return { bg: 'var(--red-dim)', dot: 'var(--red)', text: 'var(--red)' };
|
||||
return { bg: 'var(--bg-card)', dot: 'var(--text-muted)', text: 'var(--text-muted)' };
|
||||
}
|
||||
|
||||
function toggleSidebar() {
|
||||
document.getElementById('sidebar').classList.toggle('closed');
|
||||
document.getElementById('sidebar').classList.toggle('collapsed');
|
||||
localStorage.setItem('sidebarCollapsed', document.getElementById('sidebar').classList.contains('collapsed'));
|
||||
}
|
||||
|
||||
function toggleTheme() {
|
||||
const html = document.documentElement;
|
||||
const current = html.getAttribute('data-theme');
|
||||
const next = current === 'dark' ? 'light' : 'dark';
|
||||
const next = html.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
|
||||
html.setAttribute('data-theme', next);
|
||||
localStorage.setItem('theme', next);
|
||||
api.updateSettings({ theme: next }).catch(() => {});
|
||||
}
|
||||
|
||||
function loadTheme() {
|
||||
const saved = localStorage.getItem('theme');
|
||||
if (saved) document.documentElement.setAttribute('data-theme', saved);
|
||||
const sidebarCollapsed = localStorage.getItem('sidebarCollapsed');
|
||||
if (sidebarCollapsed === 'true') document.getElementById('sidebar').classList.add('collapsed');
|
||||
}
|
||||
|
||||
function showModal(title, bodyHtml, footerHtml) {
|
||||
const container = document.getElementById('modalContainer');
|
||||
container.innerHTML = `
|
||||
<div class="modal-overlay" onclick="if(event.target===this)closeModal()">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<span class="modal-title">${title}</span>
|
||||
<button class="modal-close" onclick="closeModal()">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">${bodyHtml}</div>
|
||||
${footerHtml ? `<div class="modal-footer">${footerHtml}</div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
document.getElementById('modalContainer').innerHTML = '';
|
||||
}
|
||||
|
||||
function handleGlobalSearch(value) {
|
||||
if (value.length < 2) return;
|
||||
if (window.location.hash !== '#skills') navigate('skills');
|
||||
}
|
||||
|
||||
function renderSkeleton(count = 3) {
|
||||
return Array(count).fill(0).map(() =>
|
||||
`<div class="card"><div class="skeleton" style="height:20px;width:60%;margin-bottom:12px"></div><div class="skeleton" style="height:14px;width:90%;margin-bottom:8px"></div><div class="skeleton" style="height:14px;width:40%"></div></div>`
|
||||
).join('');
|
||||
}
|
||||
|
||||
const PAGE_TITLES = {
|
||||
dashboard: { title: 'Dashboard', breadcrumb: 'Overview' },
|
||||
skills: { title: 'Skills Hub', breadcrumb: 'Browse & execute skills' },
|
||||
memory: { title: 'Memory', breadcrumb: 'Shared brain context' },
|
||||
scheduler: { title: 'Scheduler', breadcrumb: 'Automated workflows' },
|
||||
audit: { title: 'Audit Log', breadcrumb: 'System activity trail' },
|
||||
cost: { title: 'Cost Analytics', breadcrumb: 'Usage & spending' },
|
||||
plugins: { title: 'Plugin Registry', breadcrumb: 'Manage plugins' },
|
||||
backups: { title: 'Backups', breadcrumb: 'Disaster recovery' },
|
||||
prompts: { title: 'Prompt Library', breadcrumb: 'Reusable templates' },
|
||||
standards: { title: 'Standards', breadcrumb: 'Project conventions' },
|
||||
settings: { title: 'Settings', breadcrumb: 'Configuration' },
|
||||
'setup-wizard': { title: 'Setup Wizard', breadcrumb: 'Guided configuration' },
|
||||
};
|
||||
|
|
|
|||
12
server.py
12
server.py
|
|
@ -415,11 +415,21 @@ def discover_standards():
|
|||
|
||||
# ─── Routes: Dashboard Static Files ──────────────────────────────
|
||||
|
||||
dashboard_dir = BASE_DIR / "dashboard"
|
||||
if dashboard_dir.exists():
|
||||
app.mount("/dashboard", StaticFiles(directory=str(dashboard_dir)), name="dashboard")
|
||||
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
def index():
|
||||
html_file = BASE_DIR / "dashboard" / "index.html"
|
||||
if html_file.exists():
|
||||
return HTMLResponse(content=html_file.read_text())
|
||||
content = html_file.read_text()
|
||||
content = content.replace('href="styles.css"', 'href="/dashboard/styles.css"')
|
||||
content = content.replace('src="utils.js"', 'src="/dashboard/utils.js"')
|
||||
content = content.replace('src="api.js"', 'src="/dashboard/api.js"')
|
||||
content = content.replace('src="app.js"', 'src="/dashboard/app.js"')
|
||||
content = content.replace('pages/', '/dashboard/pages/')
|
||||
return HTMLResponse(content=content)
|
||||
return HTMLResponse("<h1>Agentic OS</h1><p>Dashboard not built yet. Run <code>./install.sh</code> first.</p>")
|
||||
|
||||
# ─── Main ─────────────────────────────────────────────────────────
|
||||
|
|
|
|||
Loading…
Reference in New Issue