diff options
| author | Arseney300 <Arseney300@gmail.com> | 2026-03-05 09:47:09 +0700 |
|---|---|---|
| committer | Arseney300 <Arseney300@gmail.com> | 2026-03-05 09:47:09 +0700 |
| commit | 0885d20ac18c252f6735cd288448d5d93e95b80f (patch) | |
| tree | edfb6814e97d4000605d9f6a918c30c6aca9e10c /admin-panel/src/client/components/task-modal.ts | |
| parent | 22749b796d7a5e4efa9494156a3fdcfe39028da7 (diff) | |
WIP: admin_panel: add featurefeature/admin_panel
Diffstat (limited to 'admin-panel/src/client/components/task-modal.ts')
| -rw-r--r-- | admin-panel/src/client/components/task-modal.ts | 271 |
1 files changed, 271 insertions, 0 deletions
diff --git a/admin-panel/src/client/components/task-modal.ts b/admin-panel/src/client/components/task-modal.ts new file mode 100644 index 0000000..725d921 --- /dev/null +++ b/admin-panel/src/client/components/task-modal.ts @@ -0,0 +1,271 @@ +import { api, type Task } from '../lib/api.js'; +import { renderMarkdown } from '../lib/markdown.js'; + +const STATUS_ORDER = ['todo', 'plan', 'plan_review', 'impl', 'impl_review', 'test', 'done']; +const STATUS_LABELS: Record<string, string> = { + todo: 'Req', plan: 'Plan', plan_review: 'Plan Review', + impl: 'Impl', impl_review: 'Impl Review', test: 'Test', done: 'Done' +}; + +const TABS = ['Requirements', 'Plan', 'Implementation', 'Reviews', 'Tests', 'Agent Log']; + +export function showTaskModal(projectName: string, task: Task): void { + document.querySelector('.modal-overlay')?.remove(); + + const overlay = document.createElement('div'); + overlay.className = 'modal-overlay'; + overlay.innerHTML = ` + <div class="modal task-modal"> + <div class="modal-header"> + <div class="top-row"> + <span class="task-id" style="font-size:14px">#${task.id}</span> + <button class="btn-icon close-btn" style="font-size:20px">×</button> + </div> + <h2 style="width:100%">${escapeHtml(task.title)}</h2> + <div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap"> + <span class="badge badge-${task.priority}">${task.priority}</span> + <span class="badge badge-level">L${task.level}</span> + <span class="badge" style="background:var(--border);color:var(--text-secondary)"> + ${STATUS_LABELS[task.status] || task.status} + </span> + ${task.current_agent ? `<span class="badge badge-agent">${escapeHtml(task.current_agent)}</span>` : ''} + ${parseTags(task.tags).map(t => `<span class="tag">${escapeHtml(t)}</span>`).join('')} + </div> + </div> + + ${renderLifecycleBar(task)} + + <div class="tabs" id="task-tabs"> + ${TABS.map((tab, i) => ` + <button class="tab ${i === 0 ? 'active' : ''}" data-tab="${i}">${tab}</button> + `).join('')} + </div> + + <div class="tab-content" id="tab-content"> + ${renderTabContent(0, task)} + </div> + + <div class="modal-footer"> + <div class="pipeline-controls"> + <button class="btn btn-primary btn-sm" id="run-pipeline-btn" title="Run full pipeline">Run Pipeline</button> + <button class="btn btn-secondary btn-sm" id="step-pipeline-btn" title="Run next step">Step</button> + <button class="btn btn-danger btn-sm" id="stop-pipeline-btn" title="Stop pipeline">Stop</button> + </div> + <div style="flex:1"></div> + <button class="btn btn-danger btn-sm" id="delete-task-btn">Delete</button> + </div> + </div> + `; + + document.body.appendChild(overlay); + + // Tab switching + overlay.querySelectorAll('.tab').forEach(tab => { + tab.addEventListener('click', () => { + overlay.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); + tab.classList.add('active'); + const tabIdx = parseInt((tab as HTMLElement).dataset.tab!); + overlay.querySelector('#tab-content')!.innerHTML = renderTabContent(tabIdx, task); + }); + }); + + // Close + overlay.querySelectorAll('.close-btn').forEach(btn => { + btn.addEventListener('click', () => overlay.remove()); + }); + overlay.addEventListener('click', (e) => { + if (e.target === overlay) overlay.remove(); + }); + + // Pipeline controls + overlay.querySelector('#run-pipeline-btn')?.addEventListener('click', async () => { + try { + await api.runPipeline(projectName, task.id); + } catch (err) { + alert((err as Error).message); + } + }); + + overlay.querySelector('#step-pipeline-btn')?.addEventListener('click', async () => { + try { + await api.stepPipeline(projectName, task.id); + } catch (err) { + alert((err as Error).message); + } + }); + + overlay.querySelector('#stop-pipeline-btn')?.addEventListener('click', async () => { + try { + await api.stopPipeline(projectName, task.id); + } catch (err) { + alert((err as Error).message); + } + }); + + // Delete + overlay.querySelector('#delete-task-btn')?.addEventListener('click', async () => { + if (!confirm(`Delete task #${task.id}?`)) return; + try { + await api.deleteTask(projectName, task.id); + overlay.remove(); + } catch (err) { + alert((err as Error).message); + } + }); +} + +function renderLifecycleBar(task: Task): string { + const currentIdx = STATUS_ORDER.indexOf(task.status); + return ` + <div class="lifecycle-bar"> + ${STATUS_ORDER.map((s, i) => { + let cls = 'lifecycle-step'; + if (i < currentIdx) cls += ' completed'; + else if (i === currentIdx) cls += ' current'; + return `<div class="${cls}" title="${STATUS_LABELS[s]}"></div>`; + }).join('')} + </div> + `; +} + +function renderTabContent(tabIdx: number, task: Task): string { + switch (tabIdx) { + case 0: // Requirements + return ` + <div style="color:var(--text-secondary);font-size:13px"> + ${task.description + ? `<div>${renderMarkdown(task.description)}</div>` + : '<p>No description provided.</p>' + } + ${task.done_when ? ` + <h4 style="margin-top:16px;color:var(--text-primary)">Done When</h4> + <div>${renderMarkdown(task.done_when)}</div> + ` : ''} + </div> + `; + + case 1: // Plan + return ` + <div style="color:var(--text-secondary);font-size:13px"> + ${task.plan + ? `<div>${renderMarkdown(task.plan)}</div>` + : '<p>No plan generated yet. Run the pipeline to create one.</p>' + } + ${task.decision_log ? ` + <h4 style="margin-top:16px;color:var(--text-primary)">Decision Log</h4> + <div>${renderMarkdown(task.decision_log)}</div> + ` : ''} + </div> + `; + + case 2: // Implementation + return ` + <div style="color:var(--text-secondary);font-size:13px"> + ${task.implementation_notes + ? `<div>${renderMarkdown(task.implementation_notes)}</div>` + : '<p>No implementation notes yet.</p>' + } + ${task.branch_name ? `<p style="margin-top:12px">Branch: <code>${escapeHtml(task.branch_name)}</code></p>` : ''} + ${task.diff ? ` + <h4 style="margin-top:16px;color:var(--text-primary)">Diff</h4> + <div class="diff-viewer">${renderDiff(task.diff)}</div> + ` : ''} + </div> + `; + + case 3: // Reviews + return ` + <div style="color:var(--text-secondary);font-size:13px"> + <h4 style="color:var(--text-primary)">Plan Reviews (${task.plan_review_count})</h4> + ${renderJsonArray(task.plan_review_comments, 'No plan reviews yet.')} + + <h4 style="margin-top:16px;color:var(--text-primary)">Code Reviews (${task.impl_review_count})</h4> + ${renderJsonArray(task.review_comments, 'No code reviews yet.')} + + ${task.gerrit_change_number ? ` + <p style="margin-top:16px">Gerrit Change: <a href="/gerrit/${task.gerrit_change_number}" style="color:var(--accent-glow)">#${task.gerrit_change_number}</a></p> + ` : ''} + </div> + `; + + case 4: // Tests + return ` + <div style="color:var(--text-secondary);font-size:13px"> + ${renderJsonArray(task.test_results, 'No test results yet. Run the pipeline to generate tests.')} + </div> + `; + + case 5: // Agent Log + return ` + <div class="agent-log"> + ${renderAgentLog(task.agent_log)} + </div> + `; + + default: + return ''; + } +} + +function renderDiff(diff: string): string { + return diff.split('\n').map(line => { + let cls = 'diff-line'; + if (line.startsWith('+')) cls += ' diff-add'; + else if (line.startsWith('-')) cls += ' diff-del'; + else if (line.startsWith('@@')) cls += ' diff-hunk'; + return `<div class="${cls}">${escapeHtml(line)}</div>`; + }).join(''); +} + +function renderJsonArray(jsonStr: string, emptyMsg: string): string { + try { + const items = JSON.parse(jsonStr); + if (!Array.isArray(items) || items.length === 0) return `<p>${emptyMsg}</p>`; + return items.map((item: unknown) => { + if (typeof item === 'string') return `<div class="log-entry"><div class="log-message">${escapeHtml(item)}</div></div>`; + if (typeof item === 'object' && item !== null) { + const obj = item as Record<string, unknown>; + return `<div class="log-entry"> + ${obj.agent ? `<div class="log-agent">${escapeHtml(String(obj.agent))}</div>` : ''} + <div class="log-message">${escapeHtml(String(obj.message || obj.comment || JSON.stringify(obj)))}</div> + </div>`; + } + return ''; + }).join(''); + } catch { + return `<p>${emptyMsg}</p>`; + } +} + +function renderAgentLog(logStr: string): string { + try { + const entries = JSON.parse(logStr); + if (!Array.isArray(entries) || entries.length === 0) { + return '<div class="empty-state"><p>No agent activity yet.</p></div>'; + } + return entries.map((entry: Record<string, unknown>) => ` + <div class="log-entry"> + <span class="log-time">${entry.timestamp ? new Date(String(entry.timestamp)).toLocaleTimeString() : ''}</span> + <span class="log-agent">${escapeHtml(String(entry.agent || ''))}</span> + <span class="log-message">${escapeHtml(String(entry.message || ''))}</span> + </div> + `).join(''); + } catch { + return '<div class="empty-state"><p>No agent activity yet.</p></div>'; + } +} + +function parseTags(tagsStr: string): string[] { + try { + const parsed = JSON.parse(tagsStr); + return Array.isArray(parsed) ? parsed : []; + } catch { + return []; + } +} + +function escapeHtml(text: string): string { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; +} |
