claude-coding-flow 1.0.0 → 1.1.0
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/bin/flow.js +57 -16
- package/dashboard/db.py +296 -0
- package/dashboard/logger.py +110 -0
- package/dashboard/main.py +253 -0
- package/dashboard/models.py +26 -0
- package/dashboard/requirements.txt +2 -0
- package/dashboard/static/app.js +639 -0
- package/dashboard/static/index.html +190 -0
- package/dashboard/static/style.css +1034 -0
- package/package.json +3 -2
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════
|
|
2
|
+
// Flow Dashboard — app.js
|
|
3
|
+
// ═══════════════════════════════════════════
|
|
4
|
+
|
|
5
|
+
const PHASE_ICONS = {
|
|
6
|
+
pending: "schedule", running: "autorenew",
|
|
7
|
+
completed: "check_circle", failed: "error",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const TAB_CONFIG = {
|
|
11
|
+
"doc-gen": { color: "amber", label: "需求分析" },
|
|
12
|
+
"code-gen": { color: "blue", label: "代码开发" },
|
|
13
|
+
"bug-fix": { color: "red", label: "Bug修复" },
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
let currentTab = "doc-gen";
|
|
17
|
+
let _ctxTaskId = null;
|
|
18
|
+
let _ctxModuleId = null;
|
|
19
|
+
let _ctxTabType = null;
|
|
20
|
+
|
|
21
|
+
// ── Tab Switching ──
|
|
22
|
+
|
|
23
|
+
document.querySelectorAll(".nav-item").forEach(item => {
|
|
24
|
+
item.addEventListener("click", () => switchTab(item.dataset.tab));
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
function switchTab(tab) {
|
|
28
|
+
currentTab = tab;
|
|
29
|
+
document.querySelectorAll(".nav-item").forEach(n => n.classList.remove("active"));
|
|
30
|
+
document.querySelector(`.nav-item[data-tab="${tab}"]`).classList.add("active");
|
|
31
|
+
document.querySelectorAll(".tab-panel").forEach(p => p.classList.remove("active"));
|
|
32
|
+
document.getElementById(`tab-${tab}`).classList.add("active");
|
|
33
|
+
loadTabContent(tab);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function loadTabContent(tab) {
|
|
37
|
+
const container = document.getElementById(`content-${tab}`);
|
|
38
|
+
try {
|
|
39
|
+
const res = await fetch(`/api/modules/type/${tab}`);
|
|
40
|
+
const modules = await res.json();
|
|
41
|
+
|
|
42
|
+
// Update sidebar count
|
|
43
|
+
const countEl = document.getElementById(`count-${tab}`);
|
|
44
|
+
if (countEl) {
|
|
45
|
+
const total = modules.reduce((s, m) => s + (m.task_count || 0), 0);
|
|
46
|
+
countEl.textContent = total;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (modules.length === 0) {
|
|
50
|
+
container.innerHTML = `<div class="empty-state"><span class="material-icons">inbox</span><p>暂无${TAB_CONFIG[tab].label}任务</p></div>`;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const detailed = await Promise.all(
|
|
55
|
+
modules.map(m => fetch(`/api/modules/${m.id}`).then(r => r.json()))
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
if (tab === "doc-gen") await renderDocGen(container, detailed);
|
|
59
|
+
else if (tab === "code-gen") await renderCodeGen(container, detailed);
|
|
60
|
+
else if (tab === "bug-fix") renderBugFix(container, detailed);
|
|
61
|
+
} catch (e) {
|
|
62
|
+
console.error(`Failed to load ${tab}`, e);
|
|
63
|
+
container.innerHTML = `<div class="empty-state"><span class="material-icons">error</span><p>加载失败</p></div>`;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ── Shared: Module Rendering ──
|
|
68
|
+
|
|
69
|
+
function renderModuleShell(mod, innerHtml, color) {
|
|
70
|
+
const tasks = mod.tasks || [];
|
|
71
|
+
return `<div class="module-section">
|
|
72
|
+
<div class="module-header" data-module-id="${mod.id}" onclick="toggleModule('${mod.id}')">
|
|
73
|
+
<div class="module-left">
|
|
74
|
+
<span class="material-icons module-expand" id="expand-${mod.id}">expand_more</span>
|
|
75
|
+
<span class="module-name">${esc(mod.name)}</span>
|
|
76
|
+
<span class="module-id">${mod.id}</span>
|
|
77
|
+
<span class="module-count">${tasks.length} 个任务</span>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
<div class="module-drawer" id="drawer-${mod.id}">
|
|
81
|
+
${innerHtml}
|
|
82
|
+
</div>
|
|
83
|
+
</div>`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function toggleModule(moduleId) {
|
|
87
|
+
const drawer = document.getElementById("drawer-" + moduleId);
|
|
88
|
+
const icon = document.getElementById("expand-" + moduleId);
|
|
89
|
+
if (drawer) {
|
|
90
|
+
drawer.classList.toggle("open");
|
|
91
|
+
icon.classList.toggle("open");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── Task Chain Rendering ──
|
|
96
|
+
|
|
97
|
+
async function renderTaskChain(tasks, tabType) {
|
|
98
|
+
if (tasks.length <= 1 && tabType !== "code-gen") return "";
|
|
99
|
+
const taskMap = {};
|
|
100
|
+
tasks.forEach(t => taskMap[t.id] = t);
|
|
101
|
+
const lines = [];
|
|
102
|
+
|
|
103
|
+
// Collect cross-type relations for all tasks
|
|
104
|
+
const crossMap = {};
|
|
105
|
+
if (tabType === "code-gen" || tabType === "doc-gen") {
|
|
106
|
+
const relPromises = tasks.map(t =>
|
|
107
|
+
fetch(`/api/tasks/${t.id}/cross-relations`).then(r => r.json()).then(data => {
|
|
108
|
+
if (data.relations && data.relations.length > 0) crossMap[t.id] = data.relations;
|
|
109
|
+
}).catch(() => {})
|
|
110
|
+
);
|
|
111
|
+
await Promise.all(relPromises);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const roots = tasks.filter(t => !t.prev_task_id);
|
|
115
|
+
const visited = new Set();
|
|
116
|
+
|
|
117
|
+
for (const root of roots) {
|
|
118
|
+
let cur = root;
|
|
119
|
+
while (cur) {
|
|
120
|
+
if (visited.has(cur.id)) break;
|
|
121
|
+
visited.add(cur.id);
|
|
122
|
+
const next = tasks.find(t => t.prev_task_id === cur.id);
|
|
123
|
+
|
|
124
|
+
lines.push(`<div class="chain-node">
|
|
125
|
+
<span class="chain-dot ${cur.status}"></span>
|
|
126
|
+
<span class="chain-title">${esc(cur.title)}</span>
|
|
127
|
+
${_renderCrossBadge(crossMap[cur.id])}
|
|
128
|
+
</div>`);
|
|
129
|
+
|
|
130
|
+
if (next) {
|
|
131
|
+
lines.push(`<div class="chain-link">
|
|
132
|
+
<span class="material-icons chain-arrow">arrow_downward</span>
|
|
133
|
+
<span class="chain-link-label">迭代</span>
|
|
134
|
+
</div>`);
|
|
135
|
+
}
|
|
136
|
+
cur = next;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// Orphan tasks
|
|
140
|
+
for (const t of tasks) {
|
|
141
|
+
if (!visited.has(t.id)) {
|
|
142
|
+
lines.push(`<div class="chain-node">
|
|
143
|
+
<span class="chain-dot ${t.status}"></span>
|
|
144
|
+
<span class="chain-title">${esc(t.title)}</span>
|
|
145
|
+
${_renderCrossBadge(crossMap[t.id])}
|
|
146
|
+
</div>`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return `<div class="task-chain">${lines.join("")}</div>`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function _renderCrossBadge(relations) {
|
|
153
|
+
if (!relations || relations.length === 0) return "";
|
|
154
|
+
const badges = relations.map(r => {
|
|
155
|
+
if (r.type === "based-on") {
|
|
156
|
+
return `<span class="cross-badge based-on" title="基于 ${r.to_task_title} 的 ${r.artifact}">基于: ${esc(r.to_task_title)} → ${esc(r.artifact)}</span>`;
|
|
157
|
+
}
|
|
158
|
+
if (r.type === "used-by") {
|
|
159
|
+
return `<span class="cross-badge used-by" title="被 ${r.from_task_title} 引用">引用方: ${esc(r.from_task_title)}</span>`;
|
|
160
|
+
}
|
|
161
|
+
if (r.type === "traces-to") {
|
|
162
|
+
return `<span class="cross-badge traces-to" title="溯源至 ${r.to_task_title}">溯源: ${esc(r.to_task_title)}</span>`;
|
|
163
|
+
}
|
|
164
|
+
return "";
|
|
165
|
+
});
|
|
166
|
+
return badges.join("");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ── Code-gen Rendering ──
|
|
170
|
+
|
|
171
|
+
async function renderCodeGen(container, modules) {
|
|
172
|
+
const parts = [];
|
|
173
|
+
for (const mod of modules) {
|
|
174
|
+
const tasks = mod.tasks || [];
|
|
175
|
+
const chain = await renderTaskChain(tasks, "code-gen");
|
|
176
|
+
const cards = tasks.map(t => renderCodeGenCard(t)).join("");
|
|
177
|
+
parts.push(renderModuleShell(mod, chain + cards, "blue"));
|
|
178
|
+
}
|
|
179
|
+
container.innerHTML = parts.join("");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function renderCodeGenCard(task) {
|
|
183
|
+
const phases = (task.phases || []).map(p => {
|
|
184
|
+
const icon = PHASE_ICONS[p.status] || "schedule";
|
|
185
|
+
return `<span class="phase-chip ${p.status}"><span class="material-icons">${icon}</span>${p.name}</span>`;
|
|
186
|
+
}).join("");
|
|
187
|
+
|
|
188
|
+
const progress = calcProgress(task);
|
|
189
|
+
|
|
190
|
+
return `<div class="task-card" data-id="${task.id}" data-type="code-gen">
|
|
191
|
+
<div class="task-header" onclick="toggleDetail('${task.id}')">
|
|
192
|
+
<div>
|
|
193
|
+
<div class="task-title">${esc(task.title)}</div>
|
|
194
|
+
<div class="task-meta">${task.id}</div>
|
|
195
|
+
</div>
|
|
196
|
+
<div class="task-right">
|
|
197
|
+
<span class="status-badge ${task.status}">${statusLabel(task.status)}</span>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
<div class="phases-row">${phases}</div>
|
|
201
|
+
<div class="progress-bar-wrap"><div class="progress-bar blue ${task.status === 'failed' ? 'failed' : ''}" style="width:${progress}%"></div></div>
|
|
202
|
+
<div class="task-footer">
|
|
203
|
+
<span>${task.created_at || ''}</span>
|
|
204
|
+
<a class="log-toggle" onclick="event.stopPropagation();loadTaskDetail('${task.id}')">日志详情</a>
|
|
205
|
+
</div>
|
|
206
|
+
<div class="task-detail-content hidden" id="detail-${task.id}"></div>
|
|
207
|
+
</div>`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ── Doc-gen Rendering ──
|
|
211
|
+
|
|
212
|
+
async function renderDocGen(container, modules) {
|
|
213
|
+
const parts = [];
|
|
214
|
+
for (const mod of modules) {
|
|
215
|
+
const tasks = mod.tasks || [];
|
|
216
|
+
const chain = await renderTaskChain(tasks, "doc-gen");
|
|
217
|
+
const cards = tasks.map(t => renderDocGenCard(t)).join("");
|
|
218
|
+
parts.push(renderModuleShell(mod, chain + cards, "amber"));
|
|
219
|
+
}
|
|
220
|
+
container.innerHTML = parts.join("");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function renderDocGenCard(task) {
|
|
224
|
+
const reqDoc = task.requirement_doc || "未指定";
|
|
225
|
+
const sketchFolder = task.sketch_folder || "无";
|
|
226
|
+
const outputDoc = task.output_doc || "未生成";
|
|
227
|
+
const imgCount = task._image_count || 0;
|
|
228
|
+
|
|
229
|
+
const progress = calcProgress(task);
|
|
230
|
+
|
|
231
|
+
return `<div class="task-card" data-id="${task.id}" data-type="doc-gen">
|
|
232
|
+
<div class="task-header" onclick="toggleDetail('${task.id}')">
|
|
233
|
+
<div>
|
|
234
|
+
<div class="task-title">${esc(task.title)}</div>
|
|
235
|
+
<div class="task-meta">${task.id}</div>
|
|
236
|
+
</div>
|
|
237
|
+
<div class="task-right">
|
|
238
|
+
<span class="status-badge ${task.status}">${statusLabel(task.status)}</span>
|
|
239
|
+
</div>
|
|
240
|
+
</div>
|
|
241
|
+
<div class="artifact-tags">
|
|
242
|
+
<span class="artifact-tag"><span class="material-icons">article</span>${esc(reqDoc)}</span>
|
|
243
|
+
<span class="artifact-tag"><span class="material-icons">image</span>${imgCount} 张草图</span>
|
|
244
|
+
<span class="artifact-tag"><span class="material-icons">auto_awesome</span>${esc(outputDoc)}</span>
|
|
245
|
+
</div>
|
|
246
|
+
<div class="progress-bar-wrap"><div class="progress-bar amber" style="width:${progress}%"></div></div>
|
|
247
|
+
<div class="task-footer">
|
|
248
|
+
<span>${task.created_at || ''}</span>
|
|
249
|
+
<a class="log-toggle" onclick="event.stopPropagation();loadTaskDetail('${task.id}')">日志详情</a>
|
|
250
|
+
</div>
|
|
251
|
+
<div class="task-detail-content hidden" id="detail-${task.id}"></div>
|
|
252
|
+
</div>`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ── Bug-fix Rendering ──
|
|
256
|
+
|
|
257
|
+
function renderBugFix(container, modules) {
|
|
258
|
+
container.innerHTML = modules.map(mod => {
|
|
259
|
+
const tasks = mod.tasks || [];
|
|
260
|
+
const cards = tasks.map(t => renderBugFixCard(t)).join("");
|
|
261
|
+
return renderModuleShell(mod, cards, "red");
|
|
262
|
+
}).join("");
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function renderBugFixCard(task) {
|
|
266
|
+
const bugStatus = task.bug_status || "unresolved";
|
|
267
|
+
const resolved = bugStatus === "resolved";
|
|
268
|
+
const src = task.source_info || {};
|
|
269
|
+
|
|
270
|
+
const progress = calcProgress(task);
|
|
271
|
+
const phases = (task.phases || []).map(p => {
|
|
272
|
+
const icon = PHASE_ICONS[p.status] || "schedule";
|
|
273
|
+
return `<span class="phase-chip ${p.status}"><span class="material-icons">${icon}</span>${p.name}</span>`;
|
|
274
|
+
}).join("");
|
|
275
|
+
|
|
276
|
+
let sourceHtml = "";
|
|
277
|
+
if (src.requirement || src.iteration || src.location) {
|
|
278
|
+
sourceHtml = `<div class="bug-source">
|
|
279
|
+
${src.requirement ? `<span class="bug-source-tag"><span class="material-icons" style="font-size:12px">link</span>需求: ${esc(src.requirement)}</span>` : ""}
|
|
280
|
+
${src.iteration ? `<span class="bug-source-tag"><span class="material-icons" style="font-size:12px">history</span>迭代: ${esc(src.iteration)}</span>` : ""}
|
|
281
|
+
${src.location ? `<span class="bug-source-tag"><span class="material-icons" style="font-size:12px">place</span>${esc(src.location)}</span>` : ""}
|
|
282
|
+
</div>`;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return `<div class="bug-card" data-id="${task.id}" data-type="bug-fix">
|
|
286
|
+
<div class="bug-header">
|
|
287
|
+
<span class="material-icons bug-status-icon ${resolved ? 'resolved' : ''}">${resolved ? 'check_circle' : 'error_outline'}</span>
|
|
288
|
+
<div class="bug-info">
|
|
289
|
+
<div class="bug-title">${esc(task.title)}</div>
|
|
290
|
+
<div class="bug-desc">${esc(task.bug_description || '')}</div>
|
|
291
|
+
${sourceHtml}
|
|
292
|
+
</div>
|
|
293
|
+
<span class="status-badge ${resolved ? 'resolved' : 'unresolved'}">${resolved ? '已解决' : '未解决'}</span>
|
|
294
|
+
</div>
|
|
295
|
+
<div class="phases-row" style="padding-left:44px">${phases}</div>
|
|
296
|
+
<div class="progress-bar-wrap"><div class="progress-bar red" style="width:${progress}%"></div></div>
|
|
297
|
+
<div class="bug-footer">
|
|
298
|
+
<span>${task.id}</span>
|
|
299
|
+
<span>${task.created_at || ''}</span>
|
|
300
|
+
<a class="log-toggle" onclick="event.stopPropagation();loadTaskDetail('${task.id}')">日志详情</a>
|
|
301
|
+
</div>
|
|
302
|
+
<div class="task-detail-content hidden" id="detail-${task.id}"></div>
|
|
303
|
+
</div>`;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ── Task Detail (inline) ──
|
|
307
|
+
|
|
308
|
+
async function loadTaskDetail(taskId) {
|
|
309
|
+
const container = document.getElementById("detail-" + taskId);
|
|
310
|
+
if (!container) return;
|
|
311
|
+
if (container.dataset.loaded === "true") {
|
|
312
|
+
container.classList.toggle("hidden");
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
const logRes = await fetch(`/api/tasks/${taskId}/logs`).then(r => r.json());
|
|
316
|
+
let html = "";
|
|
317
|
+
if (logRes.text_log) {
|
|
318
|
+
html += `<div class="log-md-content">${renderDiffMarkers(marked.parse(logRes.text_log))}</div>`;
|
|
319
|
+
} else {
|
|
320
|
+
html = '<div class="no-data">暂无日志</div>';
|
|
321
|
+
}
|
|
322
|
+
container.innerHTML = html;
|
|
323
|
+
container.dataset.loaded = "true";
|
|
324
|
+
container.classList.remove("hidden");
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ── Context Menu ──
|
|
328
|
+
|
|
329
|
+
document.addEventListener("contextmenu", e => {
|
|
330
|
+
hideAllCtx();
|
|
331
|
+
|
|
332
|
+
// Module header
|
|
333
|
+
const modHeader = e.target.closest(".module-header");
|
|
334
|
+
if (modHeader && !e.target.closest(".task-card") && !e.target.closest(".bug-card")) {
|
|
335
|
+
e.preventDefault();
|
|
336
|
+
_ctxModuleId = modHeader.dataset.moduleId;
|
|
337
|
+
showCtx("ctx-module", e.clientX, e.clientY);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Code-gen task card
|
|
342
|
+
const codeCard = e.target.closest(".task-card[data-type='code-gen']");
|
|
343
|
+
if (codeCard) {
|
|
344
|
+
e.preventDefault();
|
|
345
|
+
_ctxTaskId = codeCard.dataset.id;
|
|
346
|
+
_ctxTabType = "code-gen";
|
|
347
|
+
showCtx("ctx-code-gen", e.clientX, e.clientY);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Doc-gen task card
|
|
352
|
+
const docCard = e.target.closest(".task-card[data-type='doc-gen']");
|
|
353
|
+
if (docCard) {
|
|
354
|
+
e.preventDefault();
|
|
355
|
+
_ctxTaskId = docCard.dataset.id;
|
|
356
|
+
_ctxTabType = "doc-gen";
|
|
357
|
+
showCtx("ctx-doc-gen", e.clientX, e.clientY);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Bug-fix card
|
|
362
|
+
const bugCard = e.target.closest(".bug-card");
|
|
363
|
+
if (bugCard) {
|
|
364
|
+
e.preventDefault();
|
|
365
|
+
_ctxTaskId = bugCard.dataset.id;
|
|
366
|
+
_ctxTabType = "bug-fix";
|
|
367
|
+
showCtx("ctx-bug-fix", e.clientX, e.clientY);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
document.addEventListener("click", hideAllCtx);
|
|
373
|
+
|
|
374
|
+
function showCtx(id, x, y) {
|
|
375
|
+
const menu = document.getElementById(id);
|
|
376
|
+
menu.classList.remove("hidden");
|
|
377
|
+
const mw = 200, mh = menu.offsetHeight || 180;
|
|
378
|
+
if (x + mw > window.innerWidth) x = window.innerWidth - mw - 8;
|
|
379
|
+
if (y + mh > window.innerHeight) y = window.innerHeight - mh - 8;
|
|
380
|
+
menu.style.left = x + "px";
|
|
381
|
+
menu.style.top = y + "px";
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function hideAllCtx() {
|
|
385
|
+
document.querySelectorAll(".ctx-menu").forEach(m => m.classList.add("hidden"));
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// ── Context Menu Actions ──
|
|
389
|
+
|
|
390
|
+
document.querySelectorAll(".ctx-menu").forEach(menu => {
|
|
391
|
+
menu.addEventListener("click", e => {
|
|
392
|
+
const item = e.target.closest(".ctx-item");
|
|
393
|
+
if (!item) return;
|
|
394
|
+
const action = item.dataset.action;
|
|
395
|
+
|
|
396
|
+
if (action === "copy-id" && _ctxTaskId) copyId(_ctxTaskId);
|
|
397
|
+
else if (action === "copy-module-id" && _ctxModuleId) copyId(_ctxModuleId);
|
|
398
|
+
else if (action === "view-detail") viewDetail();
|
|
399
|
+
else if (action === "view-snapshot") viewSnapshot();
|
|
400
|
+
else if (action === "delete-task") deleteTask();
|
|
401
|
+
else if (action === "view-req-doc") viewReqDoc();
|
|
402
|
+
else if (action === "view-images") viewImages();
|
|
403
|
+
else if (action === "view-dev-doc") viewDevDoc();
|
|
404
|
+
|
|
405
|
+
hideAllCtx();
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
function copyId(id) {
|
|
410
|
+
navigator.clipboard.writeText(id)
|
|
411
|
+
.then(() => showToast("已拷贝: " + id))
|
|
412
|
+
.catch(() => showToast("拷贝失败"));
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
async function deleteTask() {
|
|
416
|
+
if (!_ctxTaskId) return;
|
|
417
|
+
if (!confirm(`确定删除任务 ${_ctxTaskId}?`)) return;
|
|
418
|
+
const res = await fetch(`/api/tasks/${_ctxTaskId}`, { method: "DELETE" });
|
|
419
|
+
if (res.ok) { showToast("已删除"); loadTabContent(currentTab); }
|
|
420
|
+
else showToast("删除失败");
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// ── View Detail Modal ──
|
|
424
|
+
|
|
425
|
+
async function viewDetail() {
|
|
426
|
+
if (!_ctxTaskId) return;
|
|
427
|
+
const logRes = await fetch(`/api/tasks/${_ctxTaskId}/logs`).then(r => r.json());
|
|
428
|
+
let html = "";
|
|
429
|
+
if (logRes.text_log) {
|
|
430
|
+
html = `<div class="log-md-content">${renderDiffMarkers(marked.parse(logRes.text_log))}</div>`;
|
|
431
|
+
} else {
|
|
432
|
+
html = '<div class="no-data">暂无日志</div>';
|
|
433
|
+
}
|
|
434
|
+
document.getElementById("modal-detail-body").innerHTML = html;
|
|
435
|
+
openModal("modal-detail");
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// ── View Snapshot Modal ──
|
|
439
|
+
|
|
440
|
+
async function viewSnapshot() {
|
|
441
|
+
if (!_ctxTaskId) return;
|
|
442
|
+
const snapshots = await fetch(`/api/tasks/${_ctxTaskId}/snapshots`).then(r => r.json());
|
|
443
|
+
const body = document.getElementById("modal-snapshot-body");
|
|
444
|
+
|
|
445
|
+
if (!snapshots || snapshots.length === 0) {
|
|
446
|
+
body.innerHTML = '<div class="no-data">暂无代码快照</div>';
|
|
447
|
+
openModal("modal-snapshot");
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const filesGroup = snapshots.find(s => s.name === "files");
|
|
452
|
+
const hasDiff = filesGroup && filesGroup.files && filesGroup.files.includes("changes.diff");
|
|
453
|
+
|
|
454
|
+
if (hasDiff) {
|
|
455
|
+
const res = await fetch(`/api/tasks/${_ctxTaskId}/snapshots/files/changes.diff`);
|
|
456
|
+
if (res.ok) {
|
|
457
|
+
const data = await res.json();
|
|
458
|
+
body.innerHTML = renderDiff(data.content || "");
|
|
459
|
+
} else {
|
|
460
|
+
body.innerHTML = '<div class="no-data">加载 diff 失败</div>';
|
|
461
|
+
}
|
|
462
|
+
} else {
|
|
463
|
+
const items = snapshots.flatMap(s =>
|
|
464
|
+
(s.files || []).map(f =>
|
|
465
|
+
`<div class="snap-file-item" onclick="viewFile('${esc(_ctxTaskId)}','${esc(s.name)}','${esc(f)}')">
|
|
466
|
+
<span class="material-icons">description</span>${esc(f)}
|
|
467
|
+
</div>`
|
|
468
|
+
)
|
|
469
|
+
).join("");
|
|
470
|
+
body.innerHTML = items || '<div class="no-data">无变更文件</div>';
|
|
471
|
+
}
|
|
472
|
+
openModal("modal-snapshot");
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async function viewFile(taskId, snapName, filepath) {
|
|
476
|
+
const res = await fetch(`/api/tasks/${taskId}/snapshots/${snapName}/${filepath}`);
|
|
477
|
+
if (!res.ok) { showToast("加载失败"); return; }
|
|
478
|
+
const data = await res.json();
|
|
479
|
+
const content = data.content || "";
|
|
480
|
+
|
|
481
|
+
document.getElementById("modal-file-title").textContent = filepath;
|
|
482
|
+
document.getElementById("modal-file-body").innerHTML = renderFileContent(filepath, content);
|
|
483
|
+
openModal("modal-file");
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// ── Doc-gen: View Requirement Doc ──
|
|
487
|
+
|
|
488
|
+
async function viewReqDoc() {
|
|
489
|
+
if (!_ctxTaskId) return;
|
|
490
|
+
const art = await fetch(`/api/tasks/${_ctxTaskId}/doc-artifacts`).then(r => r.json());
|
|
491
|
+
if (!art.requirement_doc) { showToast("无需求文档"); return; }
|
|
492
|
+
|
|
493
|
+
const res = await fetch(`/api/tasks/${_ctxTaskId}/doc-artifacts/requirement/${art.requirement_doc}`);
|
|
494
|
+
if (!res.ok) { showToast("加载失败"); return; }
|
|
495
|
+
const data = await res.json();
|
|
496
|
+
document.getElementById("modal-file-title").textContent = art.requirement_doc;
|
|
497
|
+
document.getElementById("modal-file-body").innerHTML = `<div class="log-md-content">${renderDiffMarkers(marked.parse(data.content || ''))}</div>`;
|
|
498
|
+
openModal("modal-file");
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// ── Doc-gen: View Development Doc ──
|
|
502
|
+
|
|
503
|
+
async function viewDevDoc() {
|
|
504
|
+
if (!_ctxTaskId) return;
|
|
505
|
+
const art = await fetch(`/api/tasks/${_ctxTaskId}/doc-artifacts`).then(r => r.json());
|
|
506
|
+
if (!art.develop_doc) { showToast("无开发文档"); return; }
|
|
507
|
+
|
|
508
|
+
const res = await fetch(`/api/tasks/${_ctxTaskId}/doc-artifacts/develop/${art.develop_doc}`);
|
|
509
|
+
if (!res.ok) { showToast("加载失败"); return; }
|
|
510
|
+
const data = await res.json();
|
|
511
|
+
document.getElementById("modal-file-title").textContent = art.develop_doc;
|
|
512
|
+
document.getElementById("modal-file-body").innerHTML = `<div class="log-md-content">${renderDiffMarkers(marked.parse(data.content || ''))}</div>`;
|
|
513
|
+
openModal("modal-file");
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// ── Doc-gen: View Sketch Images ──
|
|
517
|
+
|
|
518
|
+
async function viewImages() {
|
|
519
|
+
if (!_ctxTaskId) return;
|
|
520
|
+
const art = await fetch(`/api/tasks/${_ctxTaskId}/doc-artifacts`).then(r => r.json());
|
|
521
|
+
const body = document.getElementById("modal-gallery-body");
|
|
522
|
+
|
|
523
|
+
if (!art.images || art.images.length === 0) {
|
|
524
|
+
body.innerHTML = '<div class="no-data">暂无草图图片</div>';
|
|
525
|
+
} else {
|
|
526
|
+
body.innerHTML = `<div class="image-grid">
|
|
527
|
+
${art.images.map(img =>
|
|
528
|
+
`<div class="image-thumb" onclick="openLightbox('${esc(_ctxTaskId)}','${esc(img)}')">
|
|
529
|
+
<img src="/api/tasks/${_ctxTaskId}/doc-artifacts/images/${img}" alt="${esc(img)}" loading="lazy">
|
|
530
|
+
<span class="image-name">${esc(img)}</span>
|
|
531
|
+
</div>`
|
|
532
|
+
).join("")}
|
|
533
|
+
</div>`;
|
|
534
|
+
}
|
|
535
|
+
openModal("modal-gallery");
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function openLightbox(taskId, imgName) {
|
|
539
|
+
document.getElementById("lightbox-img").src = `/api/tasks/${taskId}/doc-artifacts/images/${imgName}`;
|
|
540
|
+
document.getElementById("lightbox-name").textContent = imgName;
|
|
541
|
+
openModal("modal-lightbox");
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// ── Diff / File Rendering ──
|
|
545
|
+
|
|
546
|
+
function renderDiff(content) {
|
|
547
|
+
const lines = content.split("\n");
|
|
548
|
+
const html = lines.map(line => {
|
|
549
|
+
let cls = "diff-line";
|
|
550
|
+
if (line.startsWith("+++ ") || line.startsWith("--- ")) cls += " diff-header";
|
|
551
|
+
else if (line.startsWith("@@")) cls += " diff-hunk";
|
|
552
|
+
else if (line.startsWith("+")) cls += " diff-add";
|
|
553
|
+
else if (line.startsWith("-")) cls += " diff-del";
|
|
554
|
+
return `<div class="${cls}">${esc(line)}</div>`;
|
|
555
|
+
}).join("");
|
|
556
|
+
return `<div class="diff-viewer">${html}</div>`;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function renderFileContent(filepath, content) {
|
|
560
|
+
const ext = filepath.split(".").pop().toLowerCase();
|
|
561
|
+
const langMap = {
|
|
562
|
+
java: "Java", kt: "Kotlin", py: "Python", js: "JavaScript",
|
|
563
|
+
ts: "TypeScript", html: "HTML", css: "CSS", json: "JSON",
|
|
564
|
+
xml: "XML", yml: "YAML", md: "Markdown",
|
|
565
|
+
go: "Go", rs: "Rust", swift: "Swift", dart: "Dart",
|
|
566
|
+
};
|
|
567
|
+
const lang = langMap[ext] || "";
|
|
568
|
+
const lines = content.split("\n").map((line, i) =>
|
|
569
|
+
`<div class="code-line"><span class="code-line-no">${i + 1}</span>${esc(line)}</div>`
|
|
570
|
+
).join("");
|
|
571
|
+
return `<div class="file-header">${esc(filepath)}${lang ? ` <span class="code-lang">${lang}</span>` : ""}</div><div class="code-viewer">${lines}</div>`;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// ── Modal Helpers ──
|
|
575
|
+
|
|
576
|
+
function openModal(id) {
|
|
577
|
+
document.getElementById(id).classList.remove("hidden");
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function closeModal(id) {
|
|
581
|
+
document.getElementById(id).classList.add("hidden");
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
document.querySelectorAll(".modal-overlay").forEach(el => {
|
|
585
|
+
el.addEventListener("click", e => {
|
|
586
|
+
if (e.target === el) el.classList.add("hidden");
|
|
587
|
+
});
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
document.addEventListener("keydown", e => {
|
|
591
|
+
if (e.key === "Escape") {
|
|
592
|
+
document.querySelectorAll(".modal-overlay:not(.hidden)").forEach(el => el.classList.add("hidden"));
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
// ── Utilities ──
|
|
597
|
+
|
|
598
|
+
function renderDiffMarkers(html) {
|
|
599
|
+
return html.replace(/\[(新增|修改|删除)\]/g, '<span style="color:red">[$1]</span>');
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function esc(text) {
|
|
603
|
+
const div = document.createElement("div");
|
|
604
|
+
div.textContent = text || "";
|
|
605
|
+
return div.innerHTML;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function statusLabel(status) {
|
|
609
|
+
return { running: "运行中", completed: "已完成", failed: "失败" }[status] || status;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function calcProgress(task) {
|
|
613
|
+
if (!task.phases || task.phases.length === 0) return 0;
|
|
614
|
+
const done = task.phases.filter(p => p.status === "completed").length;
|
|
615
|
+
return Math.round((done / task.phases.length) * 100);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function showToast(msg) {
|
|
619
|
+
const toast = document.getElementById("toast");
|
|
620
|
+
toast.textContent = msg;
|
|
621
|
+
toast.classList.remove("hidden");
|
|
622
|
+
clearTimeout(toast._timer);
|
|
623
|
+
toast._timer = setTimeout(() => toast.classList.add("hidden"), 2200);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// ── Init ──
|
|
627
|
+
|
|
628
|
+
loadTabContent("doc-gen");
|
|
629
|
+
|
|
630
|
+
// Also preload counts for other tabs
|
|
631
|
+
["code-gen", "bug-fix"].forEach(tab => {
|
|
632
|
+
fetch(`/api/modules/type/${tab}`)
|
|
633
|
+
.then(r => r.json())
|
|
634
|
+
.then(modules => {
|
|
635
|
+
const el = document.getElementById(`count-${tab}`);
|
|
636
|
+
if (el) el.textContent = modules.reduce((s, m) => s + (m.task_count || 0), 0);
|
|
637
|
+
})
|
|
638
|
+
.catch(() => {});
|
|
639
|
+
});
|