ai-control-center 1.15.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +584 -0
- package/bin/aicc.js +772 -0
- package/lib/actions/approve.js +71 -0
- package/lib/actions/assign-project.js +132 -0
- package/lib/actions/browser-test.js +64 -0
- package/lib/actions/cleanup.js +174 -0
- package/lib/actions/debug.js +298 -0
- package/lib/actions/deploy.js +1229 -0
- package/lib/actions/fix-bug.js +134 -0
- package/lib/actions/new-feature.js +255 -0
- package/lib/actions/reject.js +307 -0
- package/lib/actions/review.js +706 -0
- package/lib/actions/status.js +47 -0
- package/lib/agents/browser-qa-agent.js +611 -0
- package/lib/agents/payment-agent.js +116 -0
- package/lib/agents/suggestion-agent.js +88 -0
- package/lib/cli.js +303 -0
- package/lib/config.js +243 -0
- package/lib/hub/hub-server.js +440 -0
- package/lib/hub/project-poller.js +75 -0
- package/lib/hub/skill-registry.js +89 -0
- package/lib/hub/state-aggregator.js +204 -0
- package/lib/index.js +471 -0
- package/lib/init/doctor.js +523 -0
- package/lib/init/presets.js +222 -0
- package/lib/init/skill-fetcher.js +77 -0
- package/lib/init/wizard.js +973 -0
- package/lib/integrations/codex-runner.js +128 -0
- package/lib/integrations/github-actions.js +248 -0
- package/lib/integrations/github-reporter.js +229 -0
- package/lib/integrations/screenshot-store.js +102 -0
- package/lib/openclaw/bridge.js +650 -0
- package/lib/openclaw/generate-skill.js +235 -0
- package/lib/openclaw/openclaw.json +64 -0
- package/lib/orchestrator/autonomous-loop.js +429 -0
- package/lib/orchestrator/thread-triggers.js +63 -0
- package/lib/roleplay/agent-messenger.js +75 -0
- package/lib/roleplay/discussion-threads.js +303 -0
- package/lib/roleplay/health-monitor.js +121 -0
- package/lib/roleplay/pm-agent.js +513 -0
- package/lib/roleplay/roleplay-config.js +25 -0
- package/lib/roleplay/room.js +164 -0
- package/lib/shared/action-runner.js +2330 -0
- package/lib/shared/event-bus.js +185 -0
- package/lib/slack/bot.js +378 -0
- package/lib/telegram/bot.js +416 -0
- package/lib/telegram/commands.js +1267 -0
- package/lib/telegram/keyboards.js +113 -0
- package/lib/telegram/notifications.js +247 -0
- package/lib/twitch/bot.js +354 -0
- package/lib/twitch/commands.js +302 -0
- package/lib/twitch/notifications.js +63 -0
- package/lib/utils/achievements.js +191 -0
- package/lib/utils/activity-log.js +182 -0
- package/lib/utils/agent-leaderboard.js +119 -0
- package/lib/utils/audit-logger.js +232 -0
- package/lib/utils/codebase-context.js +288 -0
- package/lib/utils/codebase-indexer.js +381 -0
- package/lib/utils/config-schema.js +230 -0
- package/lib/utils/context-compressor.js +172 -0
- package/lib/utils/correlation.js +63 -0
- package/lib/utils/cost-tracker.js +423 -0
- package/lib/utils/cron-scheduler.js +53 -0
- package/lib/utils/db-adapter.js +293 -0
- package/lib/utils/display.js +272 -0
- package/lib/utils/errors.js +116 -0
- package/lib/utils/format.js +134 -0
- package/lib/utils/intent-engine.js +464 -0
- package/lib/utils/mcp-client.js +238 -0
- package/lib/utils/model-ab-test.js +164 -0
- package/lib/utils/notify.js +122 -0
- package/lib/utils/persona-loader.js +80 -0
- package/lib/utils/pipeline-lock.js +73 -0
- package/lib/utils/pipeline.js +214 -0
- package/lib/utils/plugin-runner.js +234 -0
- package/lib/utils/rate-limiter.js +84 -0
- package/lib/utils/rbac.js +74 -0
- package/lib/utils/runner.js +1809 -0
- package/lib/utils/security.js +191 -0
- package/lib/utils/self-healer.js +144 -0
- package/lib/utils/skill-loader.js +255 -0
- package/lib/utils/spinner.js +132 -0
- package/lib/utils/stage-queue.js +50 -0
- package/lib/utils/state-machine.js +89 -0
- package/lib/utils/status-bar.js +327 -0
- package/lib/utils/token-estimator.js +101 -0
- package/lib/utils/ux-analyzer.js +101 -0
- package/lib/utils/webhook-emitter.js +83 -0
- package/lib/web/public/css/styles.css +417 -0
- package/lib/web/public/dark-mode.js +44 -0
- package/lib/web/public/hub/kanban.html +206 -0
- package/lib/web/public/index.html +45 -0
- package/lib/web/public/js/app.js +71 -0
- package/lib/web/public/js/ask.js +110 -0
- package/lib/web/public/js/dashboard.js +165 -0
- package/lib/web/public/js/deploy.js +72 -0
- package/lib/web/public/js/feature.js +79 -0
- package/lib/web/public/js/health.js +65 -0
- package/lib/web/public/js/logs.js +93 -0
- package/lib/web/public/js/review.js +123 -0
- package/lib/web/public/js/ws-client.js +82 -0
- package/lib/web/public/office/css/office.css +678 -0
- package/lib/web/public/office/index.html +148 -0
- package/lib/web/public/office/js/achievements-ui.js +117 -0
- package/lib/web/public/office/js/character.js +1056 -0
- package/lib/web/public/office/js/chat-bubbles.js +177 -0
- package/lib/web/public/office/js/cost-overlay.js +123 -0
- package/lib/web/public/office/js/day-night.js +68 -0
- package/lib/web/public/office/js/effects.js +632 -0
- package/lib/web/public/office/js/engine.js +146 -0
- package/lib/web/public/office/js/feature-ticket.js +216 -0
- package/lib/web/public/office/js/hub-client.js +60 -0
- package/lib/web/public/office/js/main.js +1757 -0
- package/lib/web/public/office/js/office-layout.js +1524 -0
- package/lib/web/public/office/js/pathfinding.js +144 -0
- package/lib/web/public/office/js/pixel-sprites.js +1454 -0
- package/lib/web/public/office/js/progress-bars.js +117 -0
- package/lib/web/public/office/js/replay.js +191 -0
- package/lib/web/public/office/js/sound-effects.js +91 -0
- package/lib/web/public/office/js/sprite-renderer.js +211 -0
- package/lib/web/public/office/js/stamina-system.js +89 -0
- package/lib/web/public/office/js/ui.js +107 -0
- package/lib/web/public/onboarding/index.html +243 -0
- package/lib/web/public/timeline/index.html +195 -0
- package/lib/web/routes/api.js +499 -0
- package/lib/web/routes/logs.js +20 -0
- package/lib/web/routes/metrics.js +99 -0
- package/lib/web/server.js +183 -0
- package/lib/web/ws/handler.js +65 -0
- package/package.json +67 -0
- package/templates/agent-architect.md +69 -0
- package/templates/agent-gemini-pm.md +49 -0
- package/templates/agent-gemini-reviewer.md +52 -0
- package/templates/copilot-instructions.md +36 -0
- package/templates/pipelines/mobile.json +27 -0
- package/templates/pipelines/nodejs-api.json +27 -0
- package/templates/pipelines/python.json +27 -0
- package/templates/pipelines/react.json +27 -0
- package/templates/pipelines/salesforce.json +27 -0
- package/templates/role-gemini.md +97 -0
- package/templates/skill-architect.md +114 -0
- package/templates/skill-browser-qa.md +50 -0
- package/templates/skill-bug-from-qa.md +58 -0
- package/templates/skill-chatbot.md +93 -0
- package/templates/skill-implement.md +78 -0
- package/templates/skill-openclaw.md +174 -0
- package/templates/skill-payment.md +110 -0
- package/templates/skill-pm-spec.md +77 -0
- package/templates/skill-requirement-capture.md +97 -0
- package/templates/skill-review.md +108 -0
- package/templates/skill-reviewer-qa.md +44 -0
- package/templates/skill-suggestion.md +45 -0
- package/templates/skill-template.md +142 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>AICC Hub — Kanban Board</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
9
|
+
body { font-family: system-ui, -apple-system, sans-serif; background: #0f172a; color: #e2e8f0; overflow-x: auto; }
|
|
10
|
+
.header { padding: 1rem 2rem; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #1e293b; position: sticky; top: 0; background: #0f172a; z-index: 10; }
|
|
11
|
+
.header h1 { font-size: 1.25rem; }
|
|
12
|
+
.header .nav { display: flex; gap: 0.5rem; }
|
|
13
|
+
.header .nav a { padding: 0.5rem 1rem; background: #1e293b; border: 1px solid #334155; border-radius: 6px; color: #e2e8f0; text-decoration: none; font-size: 0.8rem; }
|
|
14
|
+
.header .nav a:hover { background: #334155; }
|
|
15
|
+
.kanban-board { display: flex; gap: 12px; padding: 1rem; min-height: calc(100vh - 60px); overflow-x: auto; }
|
|
16
|
+
.kanban-column { min-width: 260px; max-width: 300px; flex-shrink: 0; background: #1e293b; border-radius: 12px; padding: 12px; display: flex; flex-direction: column; }
|
|
17
|
+
.kanban-column h3 { font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.05em; color: #94a3b8; margin-bottom: 8px; padding: 4px 8px; display: flex; justify-content: space-between; align-items: center; }
|
|
18
|
+
.kanban-column h3 .count { background: #334155; padding: 2px 8px; border-radius: 10px; font-size: 0.7rem; }
|
|
19
|
+
.column-cards { flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 8px; }
|
|
20
|
+
.kanban-card { background: #0f172a; border: 1px solid #334155; border-radius: 8px; padding: 12px; cursor: pointer; transition: all 0.2s; }
|
|
21
|
+
.kanban-card:hover { border-color: #475569; transform: translateY(-1px); }
|
|
22
|
+
.kanban-card .project-tag { font-size: 0.65rem; padding: 2px 6px; border-radius: 4px; font-weight: 600; display: inline-block; margin-bottom: 6px; }
|
|
23
|
+
.kanban-card .title { font-weight: 600; font-size: 0.85rem; margin-bottom: 6px; }
|
|
24
|
+
.kanban-card .meta { font-size: 0.7rem; color: #64748b; display: flex; gap: 8px; flex-wrap: wrap; }
|
|
25
|
+
.kanban-card .meta span { display: flex; align-items: center; gap: 2px; }
|
|
26
|
+
.empty-state { text-align: center; padding: 2rem 1rem; color: #475569; font-size: 0.8rem; }
|
|
27
|
+
.modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.6); display: none; z-index: 100; align-items: center; justify-content: center; }
|
|
28
|
+
.modal { background: #1e293b; border: 1px solid #334155; border-radius: 12px; padding: 24px; max-width: 500px; width: 90%; max-height: 80vh; overflow-y: auto; }
|
|
29
|
+
.modal h2 { font-size: 1.1rem; margin-bottom: 12px; }
|
|
30
|
+
.modal .detail-row { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #334155; font-size: 0.85rem; }
|
|
31
|
+
.modal .close { position: absolute; top: 12px; right: 16px; cursor: pointer; font-size: 1.5rem; color: #94a3b8; }
|
|
32
|
+
.modal button { margin-top: 12px; padding: 8px 16px; background: #3b82f6; border: none; color: white; border-radius: 6px; cursor: pointer; font-size: 0.85rem; }
|
|
33
|
+
.modal button:hover { background: #2563eb; }
|
|
34
|
+
</style>
|
|
35
|
+
</head>
|
|
36
|
+
<body>
|
|
37
|
+
<div class="header">
|
|
38
|
+
<h1>📋 Hub Kanban Board</h1>
|
|
39
|
+
<div class="nav">
|
|
40
|
+
<a href="/office">🎮 Office</a>
|
|
41
|
+
<a href="/timeline">📊 Timeline</a>
|
|
42
|
+
<a href="/">📊 Dashboard</a>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<div class="kanban-board" id="board"></div>
|
|
47
|
+
|
|
48
|
+
<div class="modal-overlay" id="modal" onclick="if(event.target===this)closeModal()">
|
|
49
|
+
<div class="modal" id="modalContent"></div>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<script>
|
|
53
|
+
const STAGES = [
|
|
54
|
+
{ id: 'inbox', label: '📥 Inbox', color: '#64748b' },
|
|
55
|
+
{ id: 'spec', label: '📝 Spec', color: '#4285F4' },
|
|
56
|
+
{ id: 'architecture', label: '🏗 Architecture', color: '#7C3AED' },
|
|
57
|
+
{ id: 'implementation', label: '⚡ Implementation', color: '#22C55E' },
|
|
58
|
+
{ id: 'review', label: '🔍 Review', color: '#F59E0B' },
|
|
59
|
+
{ id: 'deployed', label: '🚀 Deployed', color: '#10B981' },
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
let projects = {};
|
|
63
|
+
let cards = [];
|
|
64
|
+
|
|
65
|
+
function createBoard() {
|
|
66
|
+
const board = document.getElementById('board');
|
|
67
|
+
board.innerHTML = STAGES.map(stage => `
|
|
68
|
+
<div class="kanban-column" data-stage="${stage.id}">
|
|
69
|
+
<h3>
|
|
70
|
+
${stage.label}
|
|
71
|
+
<span class="count" id="count-${stage.id}">0</span>
|
|
72
|
+
</h3>
|
|
73
|
+
<div class="column-cards" id="cards-${stage.id}"></div>
|
|
74
|
+
</div>
|
|
75
|
+
`).join('');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function mapStageToColumn(stage) {
|
|
79
|
+
if (!stage) return 'inbox';
|
|
80
|
+
const map = {
|
|
81
|
+
idle: 'inbox', pending: 'inbox', queued: 'inbox',
|
|
82
|
+
spec: 'spec', plan: 'spec', pm: 'spec', spec_complete: 'spec',
|
|
83
|
+
arch: 'architecture', architect: 'architecture', arch_complete: 'architecture',
|
|
84
|
+
impl: 'implementation', implement: 'implementation', coding: 'implementation', impl_complete: 'implementation',
|
|
85
|
+
review: 'review', reviewing: 'review', review_complete: 'review',
|
|
86
|
+
approved: 'deployed', deployed: 'deployed', deploy: 'deployed', complete: 'deployed',
|
|
87
|
+
};
|
|
88
|
+
return map[stage] || 'inbox';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function renderCards() {
|
|
92
|
+
STAGES.forEach(s => {
|
|
93
|
+
const col = document.getElementById('cards-' + s.id);
|
|
94
|
+
if (col) col.innerHTML = '';
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const counts = {};
|
|
98
|
+
STAGES.forEach(s => counts[s.id] = 0);
|
|
99
|
+
|
|
100
|
+
for (const card of cards) {
|
|
101
|
+
const column = mapStageToColumn(card.stage);
|
|
102
|
+
counts[column] = (counts[column] || 0) + 1;
|
|
103
|
+
const col = document.getElementById('cards-' + column);
|
|
104
|
+
if (!col) continue;
|
|
105
|
+
|
|
106
|
+
const projectColor = card.projectColor || '#3b82f6';
|
|
107
|
+
const el = document.createElement('div');
|
|
108
|
+
el.className = 'kanban-card';
|
|
109
|
+
el.onclick = () => showDetail(card);
|
|
110
|
+
el.innerHTML = `
|
|
111
|
+
<span class="project-tag" style="background:${projectColor}22;color:${projectColor}">${card.projectIcon || '📁'} ${card.projectName || 'Local'}</span>
|
|
112
|
+
<div class="title">${card.title || card.featureId || 'Pipeline'}</div>
|
|
113
|
+
<div class="meta">
|
|
114
|
+
<span>🤖 ${card.model || 'auto'}</span>
|
|
115
|
+
${card.duration ? `<span>⏱ ${formatDuration(card.duration)}</span>` : ''}
|
|
116
|
+
${card.cost !== undefined ? `<span>💰 $${card.cost.toFixed(4)}</span>` : ''}
|
|
117
|
+
</div>
|
|
118
|
+
`;
|
|
119
|
+
col.appendChild(el);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
STAGES.forEach(s => {
|
|
123
|
+
const countEl = document.getElementById('count-' + s.id);
|
|
124
|
+
if (countEl) countEl.textContent = counts[s.id] || 0;
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function showDetail(card) {
|
|
129
|
+
const modal = document.getElementById('modal');
|
|
130
|
+
const content = document.getElementById('modalContent');
|
|
131
|
+
content.innerHTML = `
|
|
132
|
+
<h2>${card.projectIcon || '📁'} ${card.title || card.featureId || 'Pipeline'}</h2>
|
|
133
|
+
<div class="detail-row"><span>Project</span><span>${card.projectName || 'Local'}</span></div>
|
|
134
|
+
<div class="detail-row"><span>Stage</span><span>${card.stage || 'unknown'}</span></div>
|
|
135
|
+
<div class="detail-row"><span>Model</span><span>${card.model || 'auto'}</span></div>
|
|
136
|
+
${card.duration ? `<div class="detail-row"><span>Duration</span><span>${formatDuration(card.duration)}</span></div>` : ''}
|
|
137
|
+
${card.cost !== undefined ? `<div class="detail-row"><span>Cost</span><span>$${card.cost.toFixed(4)}</span></div>` : ''}
|
|
138
|
+
${card.checkpoint ? `<div class="detail-row"><span>Checkpoint</span><span>${card.checkpoint}</span></div>` : ''}
|
|
139
|
+
<button onclick="closeModal()">Close</button>
|
|
140
|
+
`;
|
|
141
|
+
modal.style.display = 'flex';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function closeModal() {
|
|
145
|
+
document.getElementById('modal').style.display = 'none';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function formatDuration(ms) {
|
|
149
|
+
if (!ms) return '0s';
|
|
150
|
+
const s = Math.round(ms / 1000);
|
|
151
|
+
return s < 60 ? s + 's' : Math.floor(s/60) + 'm ' + (s%60) + 's';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function fetchData() {
|
|
155
|
+
try {
|
|
156
|
+
const hubRes = await fetch('/api/projects').catch(() => null);
|
|
157
|
+
if (hubRes?.ok) {
|
|
158
|
+
const hubData = await hubRes.json();
|
|
159
|
+
cards = [];
|
|
160
|
+
for (const project of (hubData.projects || [])) {
|
|
161
|
+
cards.push({
|
|
162
|
+
projectName: project.name,
|
|
163
|
+
projectColor: project.color,
|
|
164
|
+
projectIcon: project.icon,
|
|
165
|
+
title: project.status?.currentFeature || project.status?.featureId || 'Pipeline',
|
|
166
|
+
stage: project.status?.stage || project.status?.phase || 'idle',
|
|
167
|
+
model: project.status?.model || 'auto',
|
|
168
|
+
duration: project.status?.duration,
|
|
169
|
+
cost: project.status?.totalCost,
|
|
170
|
+
featureId: project.status?.featureId,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
} else {
|
|
174
|
+
const statusRes = await fetch('/api/status');
|
|
175
|
+
if (statusRes.ok) {
|
|
176
|
+
const status = await statusRes.json();
|
|
177
|
+
cards = [{
|
|
178
|
+
projectName: 'Local',
|
|
179
|
+
projectColor: '#3b82f6',
|
|
180
|
+
projectIcon: '📁',
|
|
181
|
+
title: status.currentFeature || status.featureId || 'Pipeline',
|
|
182
|
+
stage: status.stage || status.phase || 'idle',
|
|
183
|
+
model: status.model || 'auto',
|
|
184
|
+
duration: status.duration,
|
|
185
|
+
cost: status.totalCost,
|
|
186
|
+
featureId: status.featureId,
|
|
187
|
+
}];
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
renderCards();
|
|
191
|
+
} catch (e) {
|
|
192
|
+
console.error('Fetch error:', e);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
createBoard();
|
|
197
|
+
fetchData();
|
|
198
|
+
setInterval(fetchData, 5000);
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
const ws = new WebSocket(`ws://${location.host}`);
|
|
202
|
+
ws.onmessage = () => fetchData();
|
|
203
|
+
} catch {}
|
|
204
|
+
</script>
|
|
205
|
+
</body>
|
|
206
|
+
</html>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>AI Control Center</title>
|
|
7
|
+
<link rel="stylesheet" href="/css/styles.css">
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<header>
|
|
11
|
+
<div class="header-brand">
|
|
12
|
+
<span class="diamond">◆</span>
|
|
13
|
+
<span class="brand">AI Control Center</span>
|
|
14
|
+
<span class="subtitle">AI Control Center</span>
|
|
15
|
+
</div>
|
|
16
|
+
<div class="header-status">
|
|
17
|
+
<span id="header-stage" class="badge">idle</span>
|
|
18
|
+
<span id="header-feature" class="dim"></span>
|
|
19
|
+
<span id="ws-indicator" class="ws-dot ws-disconnected" title="WebSocket"></span>
|
|
20
|
+
</div>
|
|
21
|
+
</header>
|
|
22
|
+
|
|
23
|
+
<nav>
|
|
24
|
+
<a href="#/dashboard" class="nav-link active" data-route="dashboard">Dashboard</a>
|
|
25
|
+
<a href="#/feature" class="nav-link" data-route="feature">New Feature</a>
|
|
26
|
+
<a href="#/review" class="nav-link" data-route="review">Review</a>
|
|
27
|
+
<a href="#/deploy" class="nav-link" data-route="deploy">Deploy</a>
|
|
28
|
+
<a href="#/logs" class="nav-link" data-route="logs">Logs</a>
|
|
29
|
+
<a href="#/health" class="nav-link" data-route="health">Health</a>
|
|
30
|
+
<a href="#/ask" class="nav-link" data-route="ask">Ask AI</a>
|
|
31
|
+
</nav>
|
|
32
|
+
|
|
33
|
+
<main id="app">
|
|
34
|
+
<!-- Views rendered here by app.js -->
|
|
35
|
+
</main>
|
|
36
|
+
|
|
37
|
+
<footer>
|
|
38
|
+
<span class="dim">AI Control Center Pipeline</span>
|
|
39
|
+
<span id="footer-clients" class="dim"></span>
|
|
40
|
+
</footer>
|
|
41
|
+
|
|
42
|
+
<script type="module" src="/js/ws-client.js"></script>
|
|
43
|
+
<script type="module" src="/js/app.js"></script>
|
|
44
|
+
</body>
|
|
45
|
+
</html>
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hash-based SPA router.
|
|
3
|
+
* Each view module exports a render(container) function.
|
|
4
|
+
*/
|
|
5
|
+
import { renderDashboard } from './dashboard.js';
|
|
6
|
+
import { renderFeature } from './feature.js';
|
|
7
|
+
import { renderReview } from './review.js';
|
|
8
|
+
import { renderDeploy } from './deploy.js';
|
|
9
|
+
import { renderLogs } from './logs.js';
|
|
10
|
+
import { renderHealth } from './health.js';
|
|
11
|
+
import { renderAsk } from './ask.js';
|
|
12
|
+
|
|
13
|
+
const routes = {
|
|
14
|
+
'/dashboard': renderDashboard,
|
|
15
|
+
'/feature': renderFeature,
|
|
16
|
+
'/review': renderReview,
|
|
17
|
+
'/deploy': renderDeploy,
|
|
18
|
+
'/logs': renderLogs,
|
|
19
|
+
'/health': renderHealth,
|
|
20
|
+
'/ask': renderAsk,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const app = document.getElementById('app');
|
|
24
|
+
|
|
25
|
+
function navigate() {
|
|
26
|
+
const hash = location.hash || '#/dashboard';
|
|
27
|
+
const route = hash.replace('#', '');
|
|
28
|
+
const renderer = routes[route] || renderDashboard;
|
|
29
|
+
|
|
30
|
+
// Update active nav link
|
|
31
|
+
document.querySelectorAll('.nav-link').forEach(link => {
|
|
32
|
+
link.classList.toggle('active', link.getAttribute('href') === hash);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
renderer(app);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
window.addEventListener('hashchange', navigate);
|
|
39
|
+
window.addEventListener('DOMContentLoaded', navigate);
|
|
40
|
+
|
|
41
|
+
// Toast notification system
|
|
42
|
+
const toastContainer = document.createElement('div');
|
|
43
|
+
toastContainer.className = 'toast-container';
|
|
44
|
+
document.body.appendChild(toastContainer);
|
|
45
|
+
|
|
46
|
+
window.showToast = function(message, type = 'info') {
|
|
47
|
+
const toast = document.createElement('div');
|
|
48
|
+
toast.className = `toast toast-${type}`;
|
|
49
|
+
toast.textContent = message;
|
|
50
|
+
toastContainer.appendChild(toast);
|
|
51
|
+
setTimeout(() => toast.remove(), 4000);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Listen for pipeline events and show toasts
|
|
55
|
+
window.addEventListener('xconn', (e) => {
|
|
56
|
+
const { type, data } = e.detail;
|
|
57
|
+
if (type === 'event') {
|
|
58
|
+
const messages = {
|
|
59
|
+
feature_created: 'New feature created',
|
|
60
|
+
feature_approved: 'Feature approved',
|
|
61
|
+
feature_rejected: 'Feature rejected',
|
|
62
|
+
deploy_success: 'Deploy successful!',
|
|
63
|
+
deploy_failed: 'Deploy failed',
|
|
64
|
+
};
|
|
65
|
+
const msg = messages[data.event];
|
|
66
|
+
if (msg) {
|
|
67
|
+
const toastType = data.event.includes('fail') || data.event.includes('reject') ? 'error' : 'success';
|
|
68
|
+
window.showToast(msg, toastType);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ask AI view — local Ollama-powered chat interface.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export function renderAsk(container) {
|
|
6
|
+
container.innerHTML = `
|
|
7
|
+
<div class="card">
|
|
8
|
+
<div class="card-title">Ask AI (Ollama)</div>
|
|
9
|
+
<div id="ai-status" style="margin-bottom: 12px; font-size: 13px; color: var(--text-dim);"></div>
|
|
10
|
+
|
|
11
|
+
<div id="ai-chat" style="max-height: 400px; overflow-y: auto; margin-bottom: 16px; padding: 8px; border: 1px solid var(--border); border-radius: 4px; background: var(--bg);"></div>
|
|
12
|
+
|
|
13
|
+
<div class="form-group">
|
|
14
|
+
<textarea id="ai-input" placeholder="Ask about your project, pipeline status, code patterns..." rows="3"></textarea>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<div class="btn-group">
|
|
18
|
+
<button class="btn btn-primary" id="ai-send">Send</button>
|
|
19
|
+
<button class="btn" id="ai-clear">Clear Chat</button>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
loadAIStatus();
|
|
25
|
+
document.getElementById('ai-send').addEventListener('click', sendQuestion);
|
|
26
|
+
document.getElementById('ai-clear').addEventListener('click', clearChat);
|
|
27
|
+
document.getElementById('ai-input').addEventListener('keydown', (e) => {
|
|
28
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
29
|
+
e.preventDefault();
|
|
30
|
+
sendQuestion();
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function loadAIStatus() {
|
|
36
|
+
const el = document.getElementById('ai-status');
|
|
37
|
+
try {
|
|
38
|
+
const res = await fetch('/api/ai/status');
|
|
39
|
+
const data = await res.json();
|
|
40
|
+
const ollamaOk = data.ollama?.available;
|
|
41
|
+
const model = data.ollama?.model || 'unknown';
|
|
42
|
+
const mode = data.mode || 'hybrid';
|
|
43
|
+
const count = data.ollama?.models?.length || 0;
|
|
44
|
+
|
|
45
|
+
el.innerHTML = ollamaOk
|
|
46
|
+
? `<span style="color: var(--green);">Ollama: connected</span> | Model: <code>${model}</code> | ${count} models installed | Mode: ${mode}`
|
|
47
|
+
: `<span style="color: var(--yellow);">Ollama: not running</span> — will fall back to cloud AI | Mode: ${mode}`;
|
|
48
|
+
} catch {
|
|
49
|
+
el.textContent = 'Could not check AI status.';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function addMessage(role, text) {
|
|
54
|
+
const chat = document.getElementById('ai-chat');
|
|
55
|
+
const msg = document.createElement('div');
|
|
56
|
+
msg.style.cssText = `margin-bottom: 12px; padding: 8px; border-radius: 4px; font-size: 13px; font-family: var(--mono); white-space: pre-wrap; word-wrap: break-word;`;
|
|
57
|
+
|
|
58
|
+
if (role === 'user') {
|
|
59
|
+
msg.style.background = 'var(--bg-hover)';
|
|
60
|
+
msg.style.borderLeft = '3px solid var(--cyan)';
|
|
61
|
+
msg.textContent = text;
|
|
62
|
+
} else if (role === 'ai') {
|
|
63
|
+
msg.style.borderLeft = '3px solid var(--green)';
|
|
64
|
+
msg.textContent = text;
|
|
65
|
+
} else {
|
|
66
|
+
msg.style.color = 'var(--red)';
|
|
67
|
+
msg.textContent = text;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
chat.appendChild(msg);
|
|
71
|
+
chat.scrollTop = chat.scrollHeight;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function sendQuestion() {
|
|
75
|
+
const input = document.getElementById('ai-input');
|
|
76
|
+
const btn = document.getElementById('ai-send');
|
|
77
|
+
const question = input.value.trim();
|
|
78
|
+
|
|
79
|
+
if (!question) return;
|
|
80
|
+
|
|
81
|
+
addMessage('user', question);
|
|
82
|
+
input.value = '';
|
|
83
|
+
btn.disabled = true;
|
|
84
|
+
btn.innerHTML = '<span class="spinner"></span> Thinking...';
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const res = await fetch('/api/ask', {
|
|
88
|
+
method: 'POST',
|
|
89
|
+
headers: { 'Content-Type': 'application/json' },
|
|
90
|
+
body: JSON.stringify({ question }),
|
|
91
|
+
});
|
|
92
|
+
const data = await res.json();
|
|
93
|
+
|
|
94
|
+
if (data.success) {
|
|
95
|
+
addMessage('ai', data.answer);
|
|
96
|
+
} else {
|
|
97
|
+
addMessage('error', `Error: ${data.error}`);
|
|
98
|
+
}
|
|
99
|
+
} catch (err) {
|
|
100
|
+
addMessage('error', `Network error: ${err.message}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
btn.disabled = false;
|
|
104
|
+
btn.textContent = 'Send';
|
|
105
|
+
input.focus();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function clearChat() {
|
|
109
|
+
document.getElementById('ai-chat').innerHTML = '';
|
|
110
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard view — pipeline status overview.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const STAGES = [
|
|
6
|
+
{ key: 'idle', label: 'Idle' },
|
|
7
|
+
{ key: 'spec_complete', label: 'Spec' },
|
|
8
|
+
{ key: 'arch_complete', label: 'Arch' },
|
|
9
|
+
{ key: 'implementation_complete', label: 'Impl' },
|
|
10
|
+
{ key: 'review_complete', label: 'Review' },
|
|
11
|
+
{ key: 'approved', label: 'Approved' },
|
|
12
|
+
{ key: 'deployed', label: 'Deployed' },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
async function callReset(btn, label) {
|
|
16
|
+
const orig = btn.textContent;
|
|
17
|
+
btn.disabled = true;
|
|
18
|
+
btn.textContent = 'Working...';
|
|
19
|
+
try {
|
|
20
|
+
const res = await fetch('/api/reset', { method: 'POST', headers: { 'Content-Type': 'application/json' } });
|
|
21
|
+
const data = await res.json();
|
|
22
|
+
if (data.success) {
|
|
23
|
+
window.showToast?.(`${label}: Pipeline reset to idle.`, 'success');
|
|
24
|
+
loadDashboardData();
|
|
25
|
+
} else {
|
|
26
|
+
window.showToast?.(`${label} failed: ${data.error}`, 'error');
|
|
27
|
+
}
|
|
28
|
+
} catch (e) {
|
|
29
|
+
window.showToast?.(`${label} failed: ${e.message}`, 'error');
|
|
30
|
+
} finally {
|
|
31
|
+
btn.disabled = false;
|
|
32
|
+
btn.textContent = orig;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function renderDashboard(container) {
|
|
37
|
+
container.innerHTML = `
|
|
38
|
+
<div class="card">
|
|
39
|
+
<div class="card-title">Pipeline Progress</div>
|
|
40
|
+
<div class="pipeline-stages" id="pipeline-stages"></div>
|
|
41
|
+
<div id="pipeline-info" style="margin-top: 16px; font-family: var(--mono); font-size: 13px;"></div>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<div class="card">
|
|
45
|
+
<div class="card-title">Latest Review</div>
|
|
46
|
+
<div id="latest-review" style="font-family: var(--mono); font-size: 13px; color: var(--text-dim);">Loading...</div>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<div class="card">
|
|
50
|
+
<div class="card-title">Quick Actions</div>
|
|
51
|
+
<div class="btn-group">
|
|
52
|
+
<a href="#/feature" class="btn btn-primary">New Feature</a>
|
|
53
|
+
<a href="#/review" class="btn">View Review</a>
|
|
54
|
+
<a href="#/deploy" class="btn btn-success">Deploy</a>
|
|
55
|
+
<button class="btn btn-danger" id="btn-reset" title="Reset pipeline to idle">Reset</button>
|
|
56
|
+
<button class="btn btn-danger" id="btn-abandon" title="Abandon current feature">Abandon</button>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<div class="card">
|
|
61
|
+
<div class="card-title">Recent Files</div>
|
|
62
|
+
<div id="recent-files" style="font-family: var(--mono); font-size: 12px;"></div>
|
|
63
|
+
</div>
|
|
64
|
+
`;
|
|
65
|
+
|
|
66
|
+
loadDashboardData();
|
|
67
|
+
|
|
68
|
+
// Wire reset/abandon buttons
|
|
69
|
+
const btnReset = document.getElementById('btn-reset');
|
|
70
|
+
const btnAbandon = document.getElementById('btn-abandon');
|
|
71
|
+
if (btnReset) btnReset.onclick = () => callReset(btnReset, 'Reset');
|
|
72
|
+
if (btnAbandon) btnAbandon.onclick = () => callReset(btnAbandon, 'Abandon');
|
|
73
|
+
|
|
74
|
+
// Listen for live updates
|
|
75
|
+
window.addEventListener('xconn', onDashboardUpdate);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function loadDashboardData() {
|
|
79
|
+
try {
|
|
80
|
+
const status = await fetch('/api/status').then(r => r.json());
|
|
81
|
+
renderStages(status);
|
|
82
|
+
renderInfo(status);
|
|
83
|
+
} catch { /* offline */ }
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const review = await fetch('/api/review/latest').then(r => r.json());
|
|
87
|
+
renderLatestReview(review);
|
|
88
|
+
} catch { /* no review */ }
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const [specs, reviews, tasks] = await Promise.all([
|
|
92
|
+
fetch('/api/files/specs').then(r => r.json()),
|
|
93
|
+
fetch('/api/files/reviews').then(r => r.json()),
|
|
94
|
+
fetch('/api/files/tasks').then(r => r.json()),
|
|
95
|
+
]);
|
|
96
|
+
renderRecentFiles(specs, reviews, tasks);
|
|
97
|
+
} catch { /* no files */ }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function renderStages(status) {
|
|
101
|
+
const el = document.getElementById('pipeline-stages');
|
|
102
|
+
if (!el) return;
|
|
103
|
+
|
|
104
|
+
const currentIdx = STAGES.findIndex(s => s.key === status.stage);
|
|
105
|
+
|
|
106
|
+
el.innerHTML = STAGES.map((stage, i) => {
|
|
107
|
+
let cls = 'stage-step';
|
|
108
|
+
if (i === currentIdx) cls += ' active';
|
|
109
|
+
else if (i < currentIdx) cls += ' completed';
|
|
110
|
+
return `<div class="${cls}">${stage.label}</div>`;
|
|
111
|
+
}).join('');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function renderInfo(status) {
|
|
115
|
+
const el = document.getElementById('pipeline-info');
|
|
116
|
+
if (!el) return;
|
|
117
|
+
|
|
118
|
+
const rows = [
|
|
119
|
+
`Stage: ${status.stage || 'idle'}`,
|
|
120
|
+
`Feature: ${status.current_feature || 'none'}`,
|
|
121
|
+
`Mode: ${status.pipeline_mode || 'manual'}`,
|
|
122
|
+
`Next: ${status.next || '-'}`,
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
el.innerHTML = rows.map(r => `<div style="padding: 2px 0;">${r}</div>`).join('');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function renderLatestReview(review) {
|
|
129
|
+
const el = document.getElementById('latest-review');
|
|
130
|
+
if (!el) return;
|
|
131
|
+
|
|
132
|
+
if (!review || !review.found) {
|
|
133
|
+
el.textContent = 'No reviews yet.';
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const color = review.verdict === 'APPROVED' ? 'var(--green)' : 'var(--red)';
|
|
138
|
+
el.innerHTML = `
|
|
139
|
+
<div style="color: ${color}; font-weight: 700; margin-bottom: 8px;">${review.verdict}</div>
|
|
140
|
+
<div style="color: var(--text-dim);">${review.name}</div>
|
|
141
|
+
<a href="#/review" style="color: var(--accent); font-size: 12px; text-decoration: none;">View full review</a>
|
|
142
|
+
`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function renderRecentFiles(specs, reviews, tasks) {
|
|
146
|
+
const el = document.getElementById('recent-files');
|
|
147
|
+
if (!el) return;
|
|
148
|
+
|
|
149
|
+
const section = (title, files) => {
|
|
150
|
+
if (!files.length) return '';
|
|
151
|
+
return `<div style="margin-bottom: 8px;">
|
|
152
|
+
<div style="color: var(--text-dim); margin-bottom: 4px;">${title}</div>
|
|
153
|
+
${files.slice(0, 3).map(f => `<div style="padding: 1px 0; color: var(--text);">${f.name}</div>`).join('')}
|
|
154
|
+
</div>`;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
el.innerHTML = section('Specs', specs) + section('Reviews', reviews) + section('Tasks', tasks) || '<span style="color: var(--text-dim);">No files yet.</span>';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function onDashboardUpdate(e) {
|
|
161
|
+
if (e.detail.type === 'status' && e.detail.data?.status) {
|
|
162
|
+
renderStages(e.detail.data.status);
|
|
163
|
+
renderInfo(e.detail.data.status);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deploy view — deploy to target org.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export function renderDeploy(container) {
|
|
6
|
+
container.innerHTML = `
|
|
7
|
+
<div class="card">
|
|
8
|
+
<div class="card-title">Deploy to Org</div>
|
|
9
|
+
|
|
10
|
+
<div class="form-group">
|
|
11
|
+
<label>Test Level</label>
|
|
12
|
+
<select id="deploy-test-level">
|
|
13
|
+
<option value="NoTestRun">No Tests (fast)</option>
|
|
14
|
+
<option value="RunLocalTests">Run Local Tests</option>
|
|
15
|
+
</select>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<div class="btn-group">
|
|
19
|
+
<button class="btn btn-success" id="deploy-btn">Deploy</button>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<div id="deploy-status" style="margin-top: 16px; font-family: var(--mono); font-size: 13px;"></div>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<div class="card" id="deploy-output-card" style="display: none;">
|
|
26
|
+
<div class="card-title">Deploy Output</div>
|
|
27
|
+
<div id="deploy-output" class="log-viewer"></div>
|
|
28
|
+
</div>
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
document.getElementById('deploy-btn').addEventListener('click', triggerDeploy);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function triggerDeploy() {
|
|
35
|
+
const btn = document.getElementById('deploy-btn');
|
|
36
|
+
const statusEl = document.getElementById('deploy-status');
|
|
37
|
+
const outputEl = document.getElementById('deploy-output');
|
|
38
|
+
const outputCard = document.getElementById('deploy-output-card');
|
|
39
|
+
const testLevel = document.getElementById('deploy-test-level').value;
|
|
40
|
+
|
|
41
|
+
btn.disabled = true;
|
|
42
|
+
btn.innerHTML = '<span class="spinner"></span> Deploying...';
|
|
43
|
+
statusEl.innerHTML = `<span style="color: var(--text-dim);">Deploying with test level: ${testLevel}...</span>`;
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const res = await fetch('/api/deploy', {
|
|
47
|
+
method: 'POST',
|
|
48
|
+
headers: { 'Content-Type': 'application/json' },
|
|
49
|
+
body: JSON.stringify({ testLevel }),
|
|
50
|
+
});
|
|
51
|
+
const data = await res.json();
|
|
52
|
+
|
|
53
|
+
if (data.success) {
|
|
54
|
+
statusEl.innerHTML = '<span style="color: var(--green);">Deploy successful!</span>';
|
|
55
|
+
window.showToast('Deploy successful!', 'success');
|
|
56
|
+
} else {
|
|
57
|
+
statusEl.innerHTML = `<span style="color: var(--red);">Deploy failed</span>`;
|
|
58
|
+
window.showToast('Deploy failed', 'error');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Show output
|
|
62
|
+
const output = data.stdout || data.stderr || data.error || 'No output';
|
|
63
|
+
outputCard.style.display = 'block';
|
|
64
|
+
outputEl.textContent = output;
|
|
65
|
+
|
|
66
|
+
} catch (err) {
|
|
67
|
+
statusEl.innerHTML = `<span style="color: var(--red);">${err.message}</span>`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
btn.disabled = false;
|
|
71
|
+
btn.textContent = 'Deploy';
|
|
72
|
+
}
|