agentic-os/dashboard/pages/skills.js

336 lines
16 KiB
JavaScript

async function renderSkills() {
const content = document.getElementById('pageContent');
content.innerHTML = `
<div class="page-header">
<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" id="skillActions">
<input id="skillFilter" class="form-input" style="width:200px" placeholder="Filter skills..." oninput="filterSkills()">
<button class="btn btn-primary" onclick="showAddSkill()">+ New Skill</button>
</div>
</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();
window._allSkills = skills;
renderSkillGrid(skills);
} catch (err) {
document.getElementById('skillsContainer').innerHTML = `<div class="empty-state"><div class="empty-state-icon">⚠</div><div class="empty-state-title">${escapeHtml(err.message)}</div></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 iconIdx = s.name.split('').reduce((a, c) => a + c.charCodeAt(0), 0) % icons.length;
const icon = icons[iconIdx];
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 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('skillActions').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 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;
window._currentSkillName = name;
detail.innerHTML = `
<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>
<button class="btn btn-sm btn-ghost" style="margin-left:auto" onclick="editSkillMd('${name}')">✎ Edit</button>
</div>
<pre id="skillMdView" style="max-height:400px;overflow:auto;font-size:12px">${escapeHtml(skill.skill || 'No SKILL.md')}</pre>
</div>
<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 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>
<button class="btn btn-sm btn-ghost" style="margin-left:auto" onclick="addSkillContextFile('${name}')">+ Add File</button>
</div>
<div id="skillContextList">
${skill.context && skill.context.length > 0
? skill.context.map(f => `
<div style="display:flex;align-items:center;gap:8px;padding:6px 0;border-bottom:1px solid var(--border);font-size:13px">
<span style="flex:1">${escapeHtml(f)}</span>
<button class="btn btn-sm btn-ghost" onclick="editSkillContextFile('${name}','${escapeHtml(f)}')">✎</button>
<button class="btn btn-sm btn-ghost" style="color:var(--red)" onclick="deleteSkillContextFile('${name}','${escapeHtml(f)}')">🗑</button>
</div>`).join('')
: '<div style="color:var(--text-muted);font-size:13px">No context files</div>'}
</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) {
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('skillsContainer').style.display = '';
document.getElementById('skillTabs').style.display = '';
document.getElementById('skillActions').style.display = '';
document.getElementById('skillDetail').style.display = 'none';
}
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>
<div id="skillResult" style="display:none"></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;
const runBtn = document.querySelector('#modalContainer .btn-primary');
const resultArea = document.getElementById('skillResult');
if (runBtn) { runBtn.disabled = true; runBtn.textContent = '⏳ Running...'; }
if (resultArea) {
resultArea.style.display = 'block';
resultArea.innerHTML = '<div class="loading" style="padding:20px"><div class="loading-spinner"></div><span style="margin-left:8px">Executing skill...</span></div>';
}
try {
const r = await api.runSkill(name, input, agent);
if (resultArea) {
const outputText = r.output || '(no output)';
resultArea.innerHTML = `
<div class="card" style="margin-top:8px">
<div class="card-header" style="border-color:var(--green-dim)">
<span class="card-title" style="color:var(--green)">✓ Completed — ${r.agent} #${r.run_id}</span>
</div>
<pre style="max-height:400px;overflow:auto;font-size:12px;white-space:pre-wrap;margin:0;padding:12px;background:var(--bg-code, #1a1a2e);border-radius:0 0 8px 8px">${escapeHtml(outputText)}</pre>
</div>`;
}
if (runBtn) { runBtn.textContent = '✓ Done'; runBtn.disabled = false; }
} catch (err) {
if (resultArea) {
resultArea.innerHTML = `<div class="empty-state" style="padding:20px"><div class="empty-state-icon">⚠</div><div class="empty-state-title">Error</div><div class="empty-state-desc">${escapeHtml(err.message)}</div></div>`;
}
if (runBtn) { runBtn.textContent = '▶ Run'; runBtn.disabled = false; }
}
}
function showAddSkill() {
showModal('New Skill', `
<div class="form-group">
<label class="form-label">Name</label>
<input id="newSkillName" class="form-input" placeholder="e.g., my-custom-skill">
<div class="form-hint">Letters, numbers, dashes and underscores only.</div>
</div>
<div class="form-group">
<label class="form-label">SKILL.md</label>
<textarea id="newSkillMd" class="form-textarea" rows="12" placeholder="# My Custom Skill&#10;&#10;Describe what this skill does and how an agent should perform it..."></textarea>
</div>
`, `
<button class="btn btn-ghost" onclick="closeModal()">Cancel</button>
<button class="btn btn-primary" onclick="submitNewSkill()">Create Skill</button>
`);
}
async function submitNewSkill() {
const name = document.getElementById('newSkillName').value.trim();
const skillMd = document.getElementById('newSkillMd').value;
if (!name) { showToast('Skill name is required', 'error'); return; }
try {
await api.createSkill(name, skillMd);
showToast('Skill created!', 'success');
closeModal();
await renderSkills();
showSkillDetail(name);
} catch (err) {
showToast('Failed to create skill: ' + err.message, 'error');
}
}
function editSkillMd(name) {
const view = document.getElementById('skillMdView');
const current = view ? view.textContent : '';
view.outerHTML = `
<textarea id="skillMdEdit" class="form-textarea" rows="16" style="font-family:var(--font-mono);font-size:12px">${escapeHtml(current)}</textarea>
<div style="display:flex;gap:8px;margin-top:8px">
<button class="btn btn-sm btn-primary" onclick="saveSkillMd('${name}')">Save</button>
<button class="btn btn-sm btn-ghost" onclick="showSkillDetail('${name}')">Cancel</button>
</div>
`;
}
async function saveSkillMd(name) {
const content = document.getElementById('skillMdEdit').value;
try {
await api.updateSkill(name, content);
showToast('SKILL.md saved', 'success');
showSkillDetail(name);
} catch (err) {
showToast('Failed to save: ' + err.message, 'error');
}
}
function addSkillContextFile(name) {
showModal('Add Context File', `
<div class="form-group">
<label class="form-label">File Name</label>
<input id="newContextFilename" class="form-input" placeholder="e.g., reference.md">
</div>
<div class="form-group">
<label class="form-label">Content</label>
<textarea id="newContextContent" class="form-textarea" rows="10"></textarea>
</div>
`, `
<button class="btn btn-ghost" onclick="closeModal()">Cancel</button>
<button class="btn btn-primary" onclick="submitNewContextFile('${name}')">Add File</button>
`);
}
async function submitNewContextFile(name) {
const filename = document.getElementById('newContextFilename').value.trim();
const content = document.getElementById('newContextContent').value;
if (!filename) { showToast('File name is required', 'error'); return; }
try {
await api.putSkillContextFile(name, filename, content);
showToast('Context file added', 'success');
closeModal();
showSkillDetail(name);
} catch (err) {
showToast('Failed to add file: ' + err.message, 'error');
}
}
async function editSkillContextFile(name, filename) {
try {
const file = await api.getSkillContextFile(name, filename);
showModal(`Edit: ${filename}`, `
<textarea id="editContextContent" class="form-textarea" rows="14">${escapeHtml(file.content)}</textarea>
`, `
<button class="btn btn-ghost" onclick="closeModal()">Cancel</button>
<button class="btn btn-primary" onclick="saveSkillContextFile('${name}','${filename}')">Save</button>
`);
} catch (err) {
showToast('Failed to load file: ' + err.message, 'error');
}
}
async function saveSkillContextFile(name, filename) {
const content = document.getElementById('editContextContent').value;
try {
await api.putSkillContextFile(name, filename, content);
showToast('File saved', 'success');
closeModal();
showSkillDetail(name);
} catch (err) {
showToast('Failed to save: ' + err.message, 'error');
}
}
async function deleteSkillContextFile(name, filename) {
if (!confirm(`Delete "${filename}"?`)) return;
try {
await api.deleteSkillContextFile(name, filename);
showToast('File deleted', 'info');
showSkillDetail(name);
} catch (err) {
showToast('Failed to delete: ' + err.message, 'error');
}
}