@saltcorn/copilot 0.8.2 → 0.8.3
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/actions/generate-page.js +6 -2
- package/actions/generate-tables.js +56 -6
- package/actions/generate-workflow.js +54 -3
- package/agent-skills/pagegen.js +155 -57
- package/agent-skills/registry-editor.js +15 -2
- package/app-constructor/common.js +7 -1
- package/app-constructor/errors.js +749 -61
- package/app-constructor/feedback-action.js +62 -60
- package/app-constructor/feedback.js +1294 -67
- package/app-constructor/fixed-prompts.js +829 -0
- package/app-constructor/phases.js +1485 -0
- package/app-constructor/prompt-generator.js +587 -0
- package/app-constructor/requirements.js +113 -54
- package/app-constructor/research.js +350 -0
- package/app-constructor/run_task.js +195 -64
- package/app-constructor/schema.js +163 -324
- package/app-constructor/tasks.js +90 -886
- package/app-constructor/tools.js +13 -1
- package/app-constructor/view.js +307 -54
- package/builder-gen.js +11 -8
- package/builder-schema.js +6 -0
- package/package.json +1 -1
- package/app-constructor/prompts.js +0 -120
|
@@ -0,0 +1,1485 @@
|
|
|
1
|
+
const MetaData = require("@saltcorn/data/models/metadata");
|
|
2
|
+
const Table = require("@saltcorn/data/models/table");
|
|
3
|
+
const View = require("@saltcorn/data/models/view");
|
|
4
|
+
const Page = require("@saltcorn/data/models/page");
|
|
5
|
+
const Trigger = require("@saltcorn/data/models/trigger");
|
|
6
|
+
const Plugin = require("@saltcorn/data/models/plugin");
|
|
7
|
+
const {
|
|
8
|
+
div,
|
|
9
|
+
h6,
|
|
10
|
+
p,
|
|
11
|
+
span,
|
|
12
|
+
button,
|
|
13
|
+
i,
|
|
14
|
+
small,
|
|
15
|
+
ul,
|
|
16
|
+
li,
|
|
17
|
+
a,
|
|
18
|
+
pre,
|
|
19
|
+
} = require("@saltcorn/markup/tags");
|
|
20
|
+
const { mkTable } = require("@saltcorn/markup");
|
|
21
|
+
const { getState, features } = require("@saltcorn/data/db/state");
|
|
22
|
+
const db = require("@saltcorn/data/db");
|
|
23
|
+
const { viewname, tool_choice } = require("./common");
|
|
24
|
+
const { task_tool } = require("./tools");
|
|
25
|
+
const { runTask } = require("./run_task");
|
|
26
|
+
const { PromptGenerator } = require("./prompt-generator");
|
|
27
|
+
|
|
28
|
+
// ── Static client-side script ─────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
const phasesStaticScript = `<script>
|
|
31
|
+
const _phasesVn = ${JSON.stringify(viewname)};
|
|
32
|
+
|
|
33
|
+
function phasesStartPoll() {
|
|
34
|
+
const poll = () => {
|
|
35
|
+
view_post(_phasesVn, 'phases_status', {}, (resp) => {
|
|
36
|
+
if (resp && !resp.generating) {
|
|
37
|
+
view_post(_phasesVn, 'phases_html', {}, (r) => {
|
|
38
|
+
if (r && r.html) document.getElementById('phases-panel').innerHTML = r.html;
|
|
39
|
+
});
|
|
40
|
+
} else setTimeout(poll, 3000);
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
setTimeout(poll, 3000);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function _setPhaseParam(idx) {
|
|
47
|
+
const url = new URL(location.href);
|
|
48
|
+
if (idx !== null) url.searchParams.set('phase', idx);
|
|
49
|
+
else url.searchParams.delete('phase');
|
|
50
|
+
history.replaceState(null, '', url.toString());
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
window.copilotDelAllPhases = function() {
|
|
54
|
+
if (!confirm('Delete all phases and their tasks?')) return;
|
|
55
|
+
_setPhaseParam(null);
|
|
56
|
+
view_post(_phasesVn, 'del_all_phases', {}, () => {
|
|
57
|
+
view_post(_phasesVn, 'phases_html', {}, (r) => {
|
|
58
|
+
if (r && r.html) document.getElementById('phases-panel').innerHTML = r.html;
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
window.copilotGenPhases = function() {
|
|
64
|
+
const panel = document.getElementById('phases-panel');
|
|
65
|
+
const hasPhases = panel && panel.querySelector('.card');
|
|
66
|
+
if (hasPhases && !confirm('Regenerate phases? This will replace all existing phases and delete all phase tasks.')) return;
|
|
67
|
+
_setPhaseParam(null);
|
|
68
|
+
panel.innerHTML = '<p><i class="fas fa-spinner fa-spin me-2"></i>Generating phases, please wait...</p>';
|
|
69
|
+
view_post(_phasesVn, 'gen_phases', {}, () => {});
|
|
70
|
+
if (!window.dynamic_updates_cfg?.enabled) phasesStartPoll();
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
window.copilotRefreshPhases = function() {
|
|
74
|
+
_setPhaseParam(null);
|
|
75
|
+
view_post(_phasesVn, 'phases_html', {}, (r) => {
|
|
76
|
+
if (r && r.html) document.getElementById('phases-panel').innerHTML = r.html;
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
function _refreshPhaseArea(idx, taskType) {
|
|
81
|
+
const areaId = taskType === 'plugin' ? 'phase-plugins-area'
|
|
82
|
+
: taskType === 'data_model' ? 'phase-data-model-area'
|
|
83
|
+
: 'phase-features-area';
|
|
84
|
+
view_post(_phasesVn, 'phase_tasks_html', { idx, task_type: taskType }, (r) => {
|
|
85
|
+
const el = document.getElementById(areaId);
|
|
86
|
+
if (r && r.html && el) el.innerHTML = r.html;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function _refreshAllAreas(idx) {
|
|
91
|
+
_refreshPhaseArea(idx, 'plugin');
|
|
92
|
+
_refreshPhaseArea(idx, 'data_model');
|
|
93
|
+
_refreshPhaseArea(idx, 'feature');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
window.copilotRefreshPhaseProgress = function(idx) {
|
|
97
|
+
const el = document.getElementById('phase-progress-area-' + idx);
|
|
98
|
+
if (!el) return;
|
|
99
|
+
view_post(_phasesVn, 'phase_progress_html', { idx }, (r) => {
|
|
100
|
+
if (r && r.html) el.innerHTML = r.html;
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
window.openPhaseDetail = function(idx) {
|
|
105
|
+
_setPhaseParam(idx);
|
|
106
|
+
view_post(_phasesVn, 'phase_detail_html', { idx }, (r) => {
|
|
107
|
+
if (r && r.html) {
|
|
108
|
+
const panel = document.getElementById('phases-panel');
|
|
109
|
+
panel.innerHTML = r.html;
|
|
110
|
+
panel.dataset.phaseIdx = idx;
|
|
111
|
+
|
|
112
|
+
for (const tt of ['plugin', 'data_model', 'feature']) {
|
|
113
|
+
view_post(_phasesVn, 'phase_tasks_status', { idx, task_type: tt }, (resp) => {
|
|
114
|
+
if (resp && resp.generating && !window.dynamic_updates_cfg?.enabled) phaseTasksPoll(idx, tt);
|
|
115
|
+
});
|
|
116
|
+
if (!window.dynamic_updates_cfg?.enabled) {
|
|
117
|
+
view_post(_phasesVn, 'phase_run_status', { idx, task_type: tt }, (resp) => {
|
|
118
|
+
if (resp && (resp.isRunning || resp.anyRunning)) phasePollRunning(idx, tt);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
window.startPhaseTasks = function(btn, idx, taskType) {
|
|
127
|
+
btn.disabled = true;
|
|
128
|
+
btn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Running…';
|
|
129
|
+
view_post(_phasesVn, 'run_phase_tasks', { idx, task_type: taskType }, () => {});
|
|
130
|
+
if (!window.dynamic_updates_cfg?.enabled) phasePollRunning(idx, taskType);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
window.stopPhaseTasks = function(idx, taskType) {
|
|
134
|
+
view_post(_phasesVn, 'stop_phase_tasks', { idx, task_type: taskType }, (resp) => {
|
|
135
|
+
if (resp && resp.taskStillRunning) {
|
|
136
|
+
const statusEl = document.getElementById('phase-run-status-' + idx + '-' + taskType);
|
|
137
|
+
if (statusEl) statusEl.textContent = 'Stopping after current task…';
|
|
138
|
+
if (!window.dynamic_updates_cfg?.enabled) phasePollRunning(idx, taskType);
|
|
139
|
+
} else {
|
|
140
|
+
_refreshPhaseArea(idx, taskType);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
function phasePollRunning(idx, taskType) {
|
|
146
|
+
let lastTaskName;
|
|
147
|
+
const poll = () => {
|
|
148
|
+
view_post(_phasesVn, 'phase_run_status', { idx, task_type: taskType }, (resp) => {
|
|
149
|
+
if (!resp) return;
|
|
150
|
+
if (resp.isRunning || resp.anyRunning) {
|
|
151
|
+
if (resp.runningTaskName !== lastTaskName) {
|
|
152
|
+
lastTaskName = resp.runningTaskName;
|
|
153
|
+
_refreshPhaseArea(idx, taskType);
|
|
154
|
+
}
|
|
155
|
+
setTimeout(poll, 3000);
|
|
156
|
+
} else {
|
|
157
|
+
_refreshPhaseArea(idx, taskType);
|
|
158
|
+
if (!window.dynamic_updates_cfg?.enabled && typeof copilotRefreshSchema === 'function') copilotRefreshSchema();
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
};
|
|
162
|
+
setTimeout(poll, 2000);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
window.runPhaseTask = function(btn, id, phaseIdx, taskType, force) {
|
|
166
|
+
view_post(_phasesVn, 'run_task', { id, force: !!force }, (resp) => {
|
|
167
|
+
if (resp && resp.unmet_deps && resp.unmet_deps.length > 0) {
|
|
168
|
+
const msg = 'These dependencies are not yet done:\\n\\n ' + resp.unmet_deps.join('\\n ') + '\\n\\nRun this task anyway?';
|
|
169
|
+
if (!confirm(msg)) return;
|
|
170
|
+
window.runPhaseTask(btn, id, phaseIdx, taskType, true);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
|
|
174
|
+
btn.disabled = true;
|
|
175
|
+
const poll = () => {
|
|
176
|
+
view_post(_phasesVn, 'task_status', { ids: [String(id)] }, (statusResp) => {
|
|
177
|
+
if (statusResp && statusResp.any_done) {
|
|
178
|
+
_refreshPhaseArea(phaseIdx, taskType);
|
|
179
|
+
if (typeof copilotRefreshSchema === 'function') copilotRefreshSchema();
|
|
180
|
+
} else setTimeout(poll, 3000);
|
|
181
|
+
});
|
|
182
|
+
};
|
|
183
|
+
if (!window.dynamic_updates_cfg?.enabled) setTimeout(poll, 3000);
|
|
184
|
+
});
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
window.delPhaseTask = function(id, phaseIdx, taskType) {
|
|
188
|
+
if (!confirm('Delete this task?')) return;
|
|
189
|
+
view_post(_phasesVn, 'del_task', { id }, () => {
|
|
190
|
+
_refreshPhaseArea(phaseIdx, taskType);
|
|
191
|
+
});
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
window.resetPhaseTask = function(id, phaseIdx, taskType) {
|
|
195
|
+
if (!confirm('Reset this task to To do?')) return;
|
|
196
|
+
view_post(_phasesVn, 'reset_task', { id }, () => {
|
|
197
|
+
_refreshPhaseArea(phaseIdx, taskType);
|
|
198
|
+
});
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
window.delAllPhaseTasks = function(idx, taskType) {
|
|
202
|
+
if (!confirm('Delete all tasks in this tab?')) return;
|
|
203
|
+
view_post(_phasesVn, 'del_phase_type_tasks', { idx, task_type: taskType }, () => {
|
|
204
|
+
_refreshPhaseArea(idx, taskType);
|
|
205
|
+
});
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
function phaseTasksPoll(idx, taskType) {
|
|
209
|
+
const poll = () => {
|
|
210
|
+
view_post(_phasesVn, 'phase_tasks_status', { idx, task_type: taskType }, (resp) => {
|
|
211
|
+
if (resp && !resp.generating) {
|
|
212
|
+
_refreshPhaseArea(idx, taskType);
|
|
213
|
+
} else setTimeout(poll, 3000);
|
|
214
|
+
});
|
|
215
|
+
};
|
|
216
|
+
setTimeout(poll, 3000);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
window.generatePhaseTasks = function(idx, taskType) {
|
|
220
|
+
const areaId = taskType === 'plugin' ? 'phase-plugins-area'
|
|
221
|
+
: taskType === 'data_model' ? 'phase-data-model-area'
|
|
222
|
+
: 'phase-features-area';
|
|
223
|
+
const el = document.getElementById(areaId);
|
|
224
|
+
const hasTasks = el && el.querySelector('[data-task-id]');
|
|
225
|
+
if (hasTasks && !confirm('Regenerate tasks? This will delete all existing tasks in this tab.')) return;
|
|
226
|
+
if (el) el.innerHTML = '<p><i class="fas fa-spinner fa-spin me-2"></i>Generating tasks, please wait...</p>';
|
|
227
|
+
view_post(_phasesVn, 'generate_phase_tasks', { idx, task_type: taskType }, () => {});
|
|
228
|
+
if (!window.dynamic_updates_cfg?.enabled) phaseTasksPoll(idx, taskType);
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
window.copilotPhaseTasksDone = function(idx) {
|
|
232
|
+
_refreshAllAreas(idx);
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
window.copilotRefreshTasks = function() {
|
|
236
|
+
const panel = document.getElementById('phases-panel');
|
|
237
|
+
if (!panel || panel.dataset.phaseIdx === undefined) return;
|
|
238
|
+
const idx = parseInt(panel.dataset.phaseIdx);
|
|
239
|
+
if (!isNaN(idx)) _refreshAllAreas(idx);
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
function copilotInitPhasesState() {
|
|
243
|
+
if (document.getElementById('phases-generating-state')) {
|
|
244
|
+
if (!window.dynamic_updates_cfg?.enabled) phasesStartPoll();
|
|
245
|
+
}
|
|
246
|
+
const phaseParam = new URLSearchParams(location.search).get('phase');
|
|
247
|
+
if (phaseParam !== null) openPhaseDetail(parseInt(phaseParam));
|
|
248
|
+
}
|
|
249
|
+
(function() {
|
|
250
|
+
if (document.readyState !== 'loading') copilotInitPhasesState();
|
|
251
|
+
else document.addEventListener('DOMContentLoaded', copilotInitPhasesState);
|
|
252
|
+
})();
|
|
253
|
+
</script>`;
|
|
254
|
+
|
|
255
|
+
// ── Shared helpers ────────────────────────────────────────────────────────────
|
|
256
|
+
|
|
257
|
+
const phases_tool = {
|
|
258
|
+
type: "function",
|
|
259
|
+
function: {
|
|
260
|
+
name: "set_phases",
|
|
261
|
+
description:
|
|
262
|
+
"Set the development phases for the application. Each phase groups a set of requirements that belong together and should be built in the same iteration.",
|
|
263
|
+
parameters: {
|
|
264
|
+
type: "object",
|
|
265
|
+
required: ["phases"],
|
|
266
|
+
additionalProperties: false,
|
|
267
|
+
properties: {
|
|
268
|
+
phases: {
|
|
269
|
+
type: "array",
|
|
270
|
+
minItems: 1,
|
|
271
|
+
description: "Ordered list of development phases",
|
|
272
|
+
items: {
|
|
273
|
+
type: "object",
|
|
274
|
+
required: ["name", "description", "requirements"],
|
|
275
|
+
additionalProperties: false,
|
|
276
|
+
properties: {
|
|
277
|
+
name: {
|
|
278
|
+
type: "string",
|
|
279
|
+
description:
|
|
280
|
+
"Short phase name, e.g. 'Phase 1: Core data entry'",
|
|
281
|
+
},
|
|
282
|
+
description: {
|
|
283
|
+
type: "string",
|
|
284
|
+
description:
|
|
285
|
+
"1–3 sentences describing what this phase delivers and why it forms a coherent milestone",
|
|
286
|
+
},
|
|
287
|
+
requirements: {
|
|
288
|
+
type: "array",
|
|
289
|
+
description:
|
|
290
|
+
"The requirements that belong to this phase, in the same format as make_requirements",
|
|
291
|
+
items: {
|
|
292
|
+
type: "object",
|
|
293
|
+
required: ["requirement", "priority"],
|
|
294
|
+
additionalProperties: false,
|
|
295
|
+
properties: {
|
|
296
|
+
requirement: {
|
|
297
|
+
type: "string",
|
|
298
|
+
description: "A statement of the requirement",
|
|
299
|
+
},
|
|
300
|
+
priority: {
|
|
301
|
+
type: "number",
|
|
302
|
+
description:
|
|
303
|
+
"Priority 1-5. 5: Must-have for this phase, 1: Nice-to-have",
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const phasesSpinner =
|
|
317
|
+
`<p id="phases-generating-state">` +
|
|
318
|
+
i({ class: "fas fa-spinner fa-spin me-2" }) +
|
|
319
|
+
"Generating phases, please wait...</p>";
|
|
320
|
+
|
|
321
|
+
const tasksSpinner =
|
|
322
|
+
"<p>" +
|
|
323
|
+
i({ class: "fas fa-spinner fa-spin me-2" }) +
|
|
324
|
+
"Generating tasks, please wait...</p>";
|
|
325
|
+
|
|
326
|
+
// ── Phase list view ───────────────────────────────────────────────────────────
|
|
327
|
+
|
|
328
|
+
const phaseCard = (phase, idx, req, allDone, hasFeedback) => {
|
|
329
|
+
const starFieldview = getState().types.Integer.fieldviews.show_star_rating;
|
|
330
|
+
const reqs = phase.requirements || [];
|
|
331
|
+
const reqList = reqs.length
|
|
332
|
+
? ul(
|
|
333
|
+
{ class: "list-unstyled mb-0 mt-2" },
|
|
334
|
+
...reqs.map((r) =>
|
|
335
|
+
li(
|
|
336
|
+
{ class: "d-flex align-items-start gap-2 mb-2" },
|
|
337
|
+
span(
|
|
338
|
+
{ class: "flex-shrink-0" },
|
|
339
|
+
starFieldview.run(r.priority, req, { min: 1, max: 5 })
|
|
340
|
+
),
|
|
341
|
+
span({ class: "text-muted small" }, r.requirement)
|
|
342
|
+
)
|
|
343
|
+
)
|
|
344
|
+
)
|
|
345
|
+
: "";
|
|
346
|
+
|
|
347
|
+
const numberBadge = allDone
|
|
348
|
+
? span(
|
|
349
|
+
{
|
|
350
|
+
class:
|
|
351
|
+
"badge bg-success rounded-circle d-flex align-items-center justify-content-center fs-6 flex-shrink-0",
|
|
352
|
+
style: "min-width:2rem;height:2rem;",
|
|
353
|
+
title: "All tasks done",
|
|
354
|
+
},
|
|
355
|
+
i({ class: "fas fa-check" })
|
|
356
|
+
)
|
|
357
|
+
: span(
|
|
358
|
+
{
|
|
359
|
+
class:
|
|
360
|
+
"badge bg-primary rounded-circle d-flex align-items-center justify-content-center fs-6 flex-shrink-0",
|
|
361
|
+
style: "min-width:2rem;height:2rem;",
|
|
362
|
+
},
|
|
363
|
+
String(idx + 1)
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
return div(
|
|
367
|
+
{
|
|
368
|
+
class: `card mb-3 shadow-sm border-start border-4 ${
|
|
369
|
+
allDone ? "border-success" : "border-primary"
|
|
370
|
+
}`,
|
|
371
|
+
},
|
|
372
|
+
div(
|
|
373
|
+
{ class: "card-body" },
|
|
374
|
+
div(
|
|
375
|
+
{ class: "d-flex align-items-start gap-3 mb-2" },
|
|
376
|
+
numberBadge,
|
|
377
|
+
div(
|
|
378
|
+
{ class: "flex-grow-1" },
|
|
379
|
+
h6({ class: "card-title fw-semibold mb-1" }, phase.name),
|
|
380
|
+
p({ class: "card-text text-muted mb-0 small" }, phase.description)
|
|
381
|
+
),
|
|
382
|
+
div(
|
|
383
|
+
{
|
|
384
|
+
class:
|
|
385
|
+
"d-flex align-items-center gap-2 flex-shrink-0 align-self-start",
|
|
386
|
+
},
|
|
387
|
+
hasFeedback
|
|
388
|
+
? i({
|
|
389
|
+
class: "fas fa-comment-alt text-warning",
|
|
390
|
+
title: "This phase has feedback",
|
|
391
|
+
style: "font-size:0.95rem;",
|
|
392
|
+
})
|
|
393
|
+
: "",
|
|
394
|
+
button(
|
|
395
|
+
{
|
|
396
|
+
class: "btn btn-outline-primary btn-sm",
|
|
397
|
+
onclick: `openPhaseDetail(${idx})`,
|
|
398
|
+
title: "Open phase",
|
|
399
|
+
},
|
|
400
|
+
i({ class: "fas fa-arrow-right" })
|
|
401
|
+
)
|
|
402
|
+
)
|
|
403
|
+
),
|
|
404
|
+
reqs.length
|
|
405
|
+
? div(
|
|
406
|
+
{ class: "border-top pt-2 mt-1" },
|
|
407
|
+
small(
|
|
408
|
+
{
|
|
409
|
+
class: "text-muted text-uppercase fw-semibold d-block mb-2",
|
|
410
|
+
style: "font-size:0.7rem;letter-spacing:.05em;",
|
|
411
|
+
},
|
|
412
|
+
`${reqs.length} requirement${reqs.length !== 1 ? "s" : ""}`
|
|
413
|
+
),
|
|
414
|
+
reqList
|
|
415
|
+
)
|
|
416
|
+
: ""
|
|
417
|
+
)
|
|
418
|
+
);
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
const phasesHtml = async (req) => {
|
|
422
|
+
const generating = await MetaData.findOne({
|
|
423
|
+
type: "CopilotConstructMgr",
|
|
424
|
+
name: "generating_phases",
|
|
425
|
+
});
|
|
426
|
+
if (generating) return phasesSpinner;
|
|
427
|
+
|
|
428
|
+
const phasesMd = await MetaData.findOne({
|
|
429
|
+
type: "CopilotConstructMgr",
|
|
430
|
+
name: "phases",
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
const generateBtn = (label) =>
|
|
434
|
+
button(
|
|
435
|
+
{ class: "btn btn-primary btn-sm", onclick: "copilotGenPhases()" },
|
|
436
|
+
i({ class: "fas fa-magic me-1" }),
|
|
437
|
+
label
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
if (!phasesMd || !phasesMd.body?.phases?.length) {
|
|
441
|
+
return (
|
|
442
|
+
p(
|
|
443
|
+
{ class: "text-muted" },
|
|
444
|
+
"No phases yet. Generate them from your specification and research answers."
|
|
445
|
+
) + generateBtn("Generate phases")
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const phases = phasesMd.body.phases;
|
|
450
|
+
const totalReqs = phases.reduce(
|
|
451
|
+
(s, ph) => s + (ph.requirements?.length || 0),
|
|
452
|
+
0
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
const allTasks = await MetaData.find({
|
|
456
|
+
type: "CopilotConstructMgr",
|
|
457
|
+
name: "task",
|
|
458
|
+
});
|
|
459
|
+
const pluginMarkers = await MetaData.find({
|
|
460
|
+
type: "CopilotConstructMgr",
|
|
461
|
+
name: "phase_plugin_generated",
|
|
462
|
+
});
|
|
463
|
+
const pluginMarkerIdxs = new Set(pluginMarkers.map((m) => m.body?.phase_idx));
|
|
464
|
+
|
|
465
|
+
const phaseAllDone = phases.map((_, idx) => {
|
|
466
|
+
const phaseTasks = allTasks.filter((t) => t.body?.phase_idx === idx);
|
|
467
|
+
const byType = (type) =>
|
|
468
|
+
phaseTasks.filter((t) => (t.body.task_type || "feature") === type);
|
|
469
|
+
const dmTasks = byType("data_model");
|
|
470
|
+
const ftTasks = byType("feature");
|
|
471
|
+
const plTasks = byType("plugin");
|
|
472
|
+
const allDone = (tasks) =>
|
|
473
|
+
tasks.length > 0 && tasks.every((t) => t.body?.status === "Done");
|
|
474
|
+
// plugin: ok if marker says 0 were needed, or if tasks exist and all done
|
|
475
|
+
const pluginOk = pluginMarkerIdxs.has(idx) || allDone(plTasks);
|
|
476
|
+
return allDone(dmTasks) && allDone(ftTasks) && pluginOk;
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
const phasesWithFeedback = new Set();
|
|
480
|
+
try {
|
|
481
|
+
const fbMds = await MetaData.find({
|
|
482
|
+
type: "CopilotConstructMgr",
|
|
483
|
+
name: "feedback_pending",
|
|
484
|
+
});
|
|
485
|
+
for (const r of fbMds)
|
|
486
|
+
if (r.body?.phase_idx != null) phasesWithFeedback.add(r.body.phase_idx);
|
|
487
|
+
} catch (_) {}
|
|
488
|
+
|
|
489
|
+
return (
|
|
490
|
+
div(
|
|
491
|
+
{ class: "d-flex justify-content-between align-items-center mb-3" },
|
|
492
|
+
small(
|
|
493
|
+
{ class: "text-muted" },
|
|
494
|
+
`${phases.length} phase${
|
|
495
|
+
phases.length !== 1 ? "s" : ""
|
|
496
|
+
}, ${totalReqs} requirement${totalReqs !== 1 ? "s" : ""}`
|
|
497
|
+
),
|
|
498
|
+
generateBtn("Regenerate")
|
|
499
|
+
) +
|
|
500
|
+
phases
|
|
501
|
+
.map((ph, idx) =>
|
|
502
|
+
phaseCard(ph, idx, req, phaseAllDone[idx], phasesWithFeedback.has(idx))
|
|
503
|
+
)
|
|
504
|
+
.join("") +
|
|
505
|
+
div(
|
|
506
|
+
{ class: "mt-3" },
|
|
507
|
+
button(
|
|
508
|
+
{
|
|
509
|
+
class: "btn btn-outline-danger btn-sm",
|
|
510
|
+
onclick: "copilotDelAllPhases()",
|
|
511
|
+
},
|
|
512
|
+
i({ class: "fas fa-trash me-1" }),
|
|
513
|
+
"Delete all phases & tasks"
|
|
514
|
+
)
|
|
515
|
+
)
|
|
516
|
+
);
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
// ── Phase detail: tasks tab ───────────────────────────────────────────────────
|
|
520
|
+
|
|
521
|
+
const taskStatusBadge = (status) => {
|
|
522
|
+
const cls =
|
|
523
|
+
status === "Done"
|
|
524
|
+
? "bg-success"
|
|
525
|
+
: status === "Running"
|
|
526
|
+
? "bg-warning text-dark"
|
|
527
|
+
: "bg-secondary";
|
|
528
|
+
return span({ class: `badge ${cls}` }, status || "To do");
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
const phaseTasksHtml = async (phaseIdx, taskType) => {
|
|
532
|
+
const generating = await MetaData.findOne({
|
|
533
|
+
type: "CopilotConstructMgr",
|
|
534
|
+
name: "generating_phase_tasks",
|
|
535
|
+
});
|
|
536
|
+
const isGenerating = !!(
|
|
537
|
+
generating &&
|
|
538
|
+
generating.body?.phase_idx === phaseIdx &&
|
|
539
|
+
(!generating.body?.task_type || generating.body?.task_type === taskType)
|
|
540
|
+
);
|
|
541
|
+
if (isGenerating) return tasksSpinner;
|
|
542
|
+
|
|
543
|
+
const allTasks = await MetaData.find({
|
|
544
|
+
type: "CopilotConstructMgr",
|
|
545
|
+
name: "task",
|
|
546
|
+
});
|
|
547
|
+
const phaseTasks = allTasks.filter((t) => t.body?.phase_idx === phaseIdx);
|
|
548
|
+
const tasks = phaseTasks.filter(
|
|
549
|
+
(t) => (t.body.task_type || "feature") === taskType
|
|
550
|
+
);
|
|
551
|
+
const doneNames = new Set(
|
|
552
|
+
phaseTasks.filter((t) => t.body.status === "Done").map((t) => t.body.name)
|
|
553
|
+
);
|
|
554
|
+
|
|
555
|
+
const phaseRunning = await MetaData.findOne({
|
|
556
|
+
type: "CopilotConstructMgr",
|
|
557
|
+
name: `phase_running_${phaseIdx}_${taskType}`,
|
|
558
|
+
});
|
|
559
|
+
const isRunning = !!phaseRunning;
|
|
560
|
+
const runningTask = tasks.find((t) => t.body.status === "Running");
|
|
561
|
+
const isStopping = !isRunning && !!runningTask;
|
|
562
|
+
const hasTodo = tasks.some(
|
|
563
|
+
(t) => !t.body.status || t.body.status === "To do"
|
|
564
|
+
);
|
|
565
|
+
|
|
566
|
+
const allDone = tasks.length > 0 && !hasTodo && !isRunning && !runningTask;
|
|
567
|
+
|
|
568
|
+
const genLabel = tasks.length ? "Regenerate" : "Generate tasks";
|
|
569
|
+
const genOnclick = tasks.length
|
|
570
|
+
? `if(confirm('Regenerate tasks? This will replace the existing tasks.')) generatePhaseTasks(${phaseIdx},'${taskType}')`
|
|
571
|
+
: `generatePhaseTasks(${phaseIdx},'${taskType}')`;
|
|
572
|
+
const genBtn = button(
|
|
573
|
+
{
|
|
574
|
+
class: `btn btn-primary btn-sm${tasks.length ? " ms-auto" : ""}`,
|
|
575
|
+
onclick: genOnclick,
|
|
576
|
+
},
|
|
577
|
+
i({ class: "fas fa-magic me-1" }),
|
|
578
|
+
genLabel
|
|
579
|
+
);
|
|
580
|
+
|
|
581
|
+
const delAllBtn = tasks.length
|
|
582
|
+
? button(
|
|
583
|
+
{
|
|
584
|
+
class: "btn btn-outline-danger btn-sm",
|
|
585
|
+
onclick: `delAllPhaseTasks(${phaseIdx},'${taskType}')`,
|
|
586
|
+
title: "Delete all tasks",
|
|
587
|
+
},
|
|
588
|
+
i({ class: "fas fa-trash me-1" }),
|
|
589
|
+
"Delete all"
|
|
590
|
+
)
|
|
591
|
+
: "";
|
|
592
|
+
|
|
593
|
+
const runBtn = isRunning
|
|
594
|
+
? button(
|
|
595
|
+
{ class: "btn btn-success btn-sm", disabled: true },
|
|
596
|
+
i({ class: "fas fa-spinner fa-spin me-1" }),
|
|
597
|
+
"Running…"
|
|
598
|
+
)
|
|
599
|
+
: isStopping
|
|
600
|
+
? button(
|
|
601
|
+
{ class: "btn btn-warning btn-sm", disabled: true },
|
|
602
|
+
i({ class: "fas fa-spinner fa-spin me-1" }),
|
|
603
|
+
"Stopping…"
|
|
604
|
+
)
|
|
605
|
+
: hasTodo
|
|
606
|
+
? button(
|
|
607
|
+
{
|
|
608
|
+
class: "btn btn-success btn-sm",
|
|
609
|
+
onclick: `startPhaseTasks(this,${phaseIdx},'${taskType}')`,
|
|
610
|
+
},
|
|
611
|
+
i({ class: "fas fa-play me-1" }),
|
|
612
|
+
"Start running"
|
|
613
|
+
)
|
|
614
|
+
: tasks.length
|
|
615
|
+
? button(
|
|
616
|
+
{ class: "btn btn-success btn-sm", disabled: true },
|
|
617
|
+
i({ class: "fas fa-play me-1" }),
|
|
618
|
+
"Start running"
|
|
619
|
+
)
|
|
620
|
+
: "";
|
|
621
|
+
|
|
622
|
+
const stopBtn = isRunning
|
|
623
|
+
? button(
|
|
624
|
+
{
|
|
625
|
+
class: "btn btn-danger btn-sm",
|
|
626
|
+
onclick: `stopPhaseTasks(${phaseIdx},'${taskType}')`,
|
|
627
|
+
},
|
|
628
|
+
i({ class: "fas fa-stop me-1" }),
|
|
629
|
+
"Stop"
|
|
630
|
+
)
|
|
631
|
+
: "";
|
|
632
|
+
|
|
633
|
+
const statusBar = div(
|
|
634
|
+
{ class: "d-flex align-items-center gap-2 mb-3 flex-wrap" },
|
|
635
|
+
span(
|
|
636
|
+
{
|
|
637
|
+
id: `phase-run-status-${phaseIdx}-${taskType}`,
|
|
638
|
+
class: "small text-muted me-1",
|
|
639
|
+
},
|
|
640
|
+
isStopping
|
|
641
|
+
? "Stopping after current task…"
|
|
642
|
+
: runningTask
|
|
643
|
+
? span(
|
|
644
|
+
"Running: ",
|
|
645
|
+
span({ class: "fw-bold text-body" }, runningTask.body.name)
|
|
646
|
+
)
|
|
647
|
+
: tasks.length
|
|
648
|
+
? "Not running"
|
|
649
|
+
: ""
|
|
650
|
+
),
|
|
651
|
+
runBtn,
|
|
652
|
+
stopBtn,
|
|
653
|
+
genBtn
|
|
654
|
+
);
|
|
655
|
+
|
|
656
|
+
const renderTask = (t) => {
|
|
657
|
+
const status = t.body.status || "To do";
|
|
658
|
+
const isRunning = status === "Running";
|
|
659
|
+
const isDone = status === "Done";
|
|
660
|
+
const isTodo = !isDone && !isRunning;
|
|
661
|
+
|
|
662
|
+
const deps = t.body.depends_on || [];
|
|
663
|
+
const depsHtml = deps.length
|
|
664
|
+
? div(
|
|
665
|
+
{ class: "d-flex flex-wrap gap-1 mt-2" },
|
|
666
|
+
...deps.map((dep) =>
|
|
667
|
+
span(
|
|
668
|
+
{
|
|
669
|
+
class: "d-inline-flex align-items-center gap-1 text-muted",
|
|
670
|
+
style: `font-size:0.7rem;background:${
|
|
671
|
+
doneNames.has(dep)
|
|
672
|
+
? "rgba(25,135,84,.18)"
|
|
673
|
+
: "rgba(220,53,69,.18)"
|
|
674
|
+
};border-radius:4px;padding:1px 5px;`,
|
|
675
|
+
},
|
|
676
|
+
i({
|
|
677
|
+
class: `fas ${
|
|
678
|
+
doneNames.has(dep)
|
|
679
|
+
? "fa-check-circle text-success"
|
|
680
|
+
: "fa-circle text-danger"
|
|
681
|
+
} `,
|
|
682
|
+
style: "font-size:0.6rem;opacity:0.9",
|
|
683
|
+
}),
|
|
684
|
+
dep
|
|
685
|
+
)
|
|
686
|
+
)
|
|
687
|
+
)
|
|
688
|
+
: "";
|
|
689
|
+
|
|
690
|
+
const taskRunBtn = isRunning
|
|
691
|
+
? span(
|
|
692
|
+
{ class: "task-spinner", "data-task-id": t.id },
|
|
693
|
+
i({ class: "fas fa-spinner fa-spin text-warning" })
|
|
694
|
+
)
|
|
695
|
+
: isDone
|
|
696
|
+
? t.body.run_id
|
|
697
|
+
? a(
|
|
698
|
+
{
|
|
699
|
+
target: "_blank",
|
|
700
|
+
href: `/view/Saltcorn%20Agent%20copilot?run_id=${t.body.run_id}`,
|
|
701
|
+
class: "btn btn-outline-secondary btn-sm",
|
|
702
|
+
title: "View run",
|
|
703
|
+
},
|
|
704
|
+
i({ class: "fas fa-external-link-alt" })
|
|
705
|
+
)
|
|
706
|
+
: ""
|
|
707
|
+
: button(
|
|
708
|
+
{
|
|
709
|
+
class: "btn btn-outline-success btn-sm",
|
|
710
|
+
"data-task-run": t.id,
|
|
711
|
+
onclick: `runPhaseTask(this,${t.id},${phaseIdx},'${taskType}')`,
|
|
712
|
+
title: "Run task",
|
|
713
|
+
},
|
|
714
|
+
i({ class: "fas fa-play" })
|
|
715
|
+
);
|
|
716
|
+
|
|
717
|
+
const editBtn =
|
|
718
|
+
isTodo && features.view_route_modal
|
|
719
|
+
? button(
|
|
720
|
+
{
|
|
721
|
+
class: "btn btn-outline-primary btn-sm",
|
|
722
|
+
title: "Edit description",
|
|
723
|
+
onclick: `ajax_modal('/view/${encodeURIComponent(
|
|
724
|
+
viewname
|
|
725
|
+
)}/edit_task_desc?id=${t.id}', {method:'POST'})`,
|
|
726
|
+
},
|
|
727
|
+
i({ class: "fas fa-edit" })
|
|
728
|
+
)
|
|
729
|
+
: "";
|
|
730
|
+
|
|
731
|
+
const resetBtn =
|
|
732
|
+
isRunning || isDone
|
|
733
|
+
? button(
|
|
734
|
+
{
|
|
735
|
+
class: "btn btn-outline-secondary btn-sm",
|
|
736
|
+
onclick: `resetPhaseTask(${t.id},${phaseIdx},'${taskType}')`,
|
|
737
|
+
title: "Reset to To do",
|
|
738
|
+
},
|
|
739
|
+
i({ class: "fas fa-undo" })
|
|
740
|
+
)
|
|
741
|
+
: "";
|
|
742
|
+
|
|
743
|
+
const deleteBtn = button(
|
|
744
|
+
{
|
|
745
|
+
class: "btn btn-outline-danger btn-sm",
|
|
746
|
+
onclick: `delPhaseTask(${t.id},${phaseIdx},'${taskType}')`,
|
|
747
|
+
title: "Delete",
|
|
748
|
+
},
|
|
749
|
+
i({ class: "fas fa-trash-alt" })
|
|
750
|
+
);
|
|
751
|
+
|
|
752
|
+
return div(
|
|
753
|
+
{ class: "card mb-2 shadow-sm" },
|
|
754
|
+
div(
|
|
755
|
+
{ class: "card-body py-2" },
|
|
756
|
+
div(
|
|
757
|
+
{ class: "d-flex align-items-start gap-2" },
|
|
758
|
+
div(
|
|
759
|
+
{ class: "flex-grow-1" },
|
|
760
|
+
div(
|
|
761
|
+
{ class: "d-flex align-items-center flex-wrap gap-1 mb-1" },
|
|
762
|
+
span({ class: "fw-semibold small me-1" }, t.body.name),
|
|
763
|
+
taskStatusBadge(status),
|
|
764
|
+
span(
|
|
765
|
+
{ class: "badge bg-secondary", title: "Priority" },
|
|
766
|
+
String(t.body.priority ?? "?")
|
|
767
|
+
)
|
|
768
|
+
),
|
|
769
|
+
p({ class: "text-muted small mb-0" }, t.body.description),
|
|
770
|
+
depsHtml
|
|
771
|
+
),
|
|
772
|
+
div(
|
|
773
|
+
{ class: "d-flex gap-1 flex-shrink-0 ms-2" },
|
|
774
|
+
taskRunBtn,
|
|
775
|
+
editBtn,
|
|
776
|
+
resetBtn,
|
|
777
|
+
deleteBtn
|
|
778
|
+
)
|
|
779
|
+
)
|
|
780
|
+
)
|
|
781
|
+
);
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
const plannedTasks = tasks.filter((t) => t.body.source !== "feedback");
|
|
785
|
+
const feedbackTasks = tasks.filter((t) => t.body.source === "feedback");
|
|
786
|
+
|
|
787
|
+
if (!tasks.length) {
|
|
788
|
+
let emptyMsg = "No tasks yet.";
|
|
789
|
+
if (taskType === "plugin") {
|
|
790
|
+
const markers = await MetaData.find({
|
|
791
|
+
type: "CopilotConstructMgr",
|
|
792
|
+
name: "phase_plugin_generated",
|
|
793
|
+
});
|
|
794
|
+
if (markers.some((m) => m.body?.phase_idx === phaseIdx))
|
|
795
|
+
emptyMsg = "No plugin installations needed for this phase.";
|
|
796
|
+
} else if (taskType === "data_model") {
|
|
797
|
+
const markers = await MetaData.find({
|
|
798
|
+
type: "CopilotConstructMgr",
|
|
799
|
+
name: "phase_data_model_generated",
|
|
800
|
+
});
|
|
801
|
+
if (markers.some((m) => m.body?.phase_idx === phaseIdx))
|
|
802
|
+
emptyMsg = "No schema changes needed for this phase.";
|
|
803
|
+
}
|
|
804
|
+
return statusBar + p({ class: "text-muted small mt-2" }, emptyMsg);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
const feedbackSection = feedbackTasks.length
|
|
808
|
+
? div(
|
|
809
|
+
{ class: "mt-4" },
|
|
810
|
+
div(
|
|
811
|
+
{ class: "d-flex align-items-center gap-2 mb-2" },
|
|
812
|
+
small(
|
|
813
|
+
{
|
|
814
|
+
class: "text-uppercase fw-semibold text-muted",
|
|
815
|
+
style: "font-size:0.7rem;letter-spacing:.05em;",
|
|
816
|
+
},
|
|
817
|
+
i({ class: "fas fa-comment-alt me-1" }),
|
|
818
|
+
"From feedback"
|
|
819
|
+
),
|
|
820
|
+
div({ class: "flex-grow-1 border-top" })
|
|
821
|
+
),
|
|
822
|
+
feedbackTasks.map(renderTask).join("")
|
|
823
|
+
)
|
|
824
|
+
: "";
|
|
825
|
+
|
|
826
|
+
return (
|
|
827
|
+
statusBar +
|
|
828
|
+
plannedTasks.map(renderTask).join("") +
|
|
829
|
+
feedbackSection +
|
|
830
|
+
div({ class: "mt-3" }, delAllBtn)
|
|
831
|
+
);
|
|
832
|
+
};
|
|
833
|
+
|
|
834
|
+
// ── Phase detail view ─────────────────────────────────────────────────────────
|
|
835
|
+
|
|
836
|
+
const phaseProgressHtml = async (idx) => {
|
|
837
|
+
const allTasks = await MetaData.find({
|
|
838
|
+
type: "CopilotConstructMgr",
|
|
839
|
+
name: "task",
|
|
840
|
+
});
|
|
841
|
+
const taskNameById = Object.fromEntries(
|
|
842
|
+
allTasks.map((t) => [t.id, t.body.name])
|
|
843
|
+
);
|
|
844
|
+
|
|
845
|
+
const allProgress = await MetaData.find(
|
|
846
|
+
{ type: "CopilotConstructMgr", name: "progress" },
|
|
847
|
+
{ orderBy: "written_at", orderDesc: true }
|
|
848
|
+
);
|
|
849
|
+
const entries = allProgress.filter((p) => p.body?.phase_idx === idx);
|
|
850
|
+
|
|
851
|
+
if (!entries.length)
|
|
852
|
+
return p({ class: "text-muted mt-2" }, "No completed tasks yet.");
|
|
853
|
+
|
|
854
|
+
return (
|
|
855
|
+
mkTable(
|
|
856
|
+
[
|
|
857
|
+
{
|
|
858
|
+
label: "When",
|
|
859
|
+
key: (m) => {
|
|
860
|
+
const d = m.written_at ? new Date(m.written_at) : null;
|
|
861
|
+
if (!d) return "";
|
|
862
|
+
return small(
|
|
863
|
+
{ class: "text-muted", style: "white-space:nowrap" },
|
|
864
|
+
d.toLocaleDateString([], { month: "short", day: "numeric" }),
|
|
865
|
+
" ",
|
|
866
|
+
d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })
|
|
867
|
+
);
|
|
868
|
+
},
|
|
869
|
+
},
|
|
870
|
+
{
|
|
871
|
+
label: "Task",
|
|
872
|
+
key: (m) =>
|
|
873
|
+
small(
|
|
874
|
+
{ class: "text-muted", style: "white-space:nowrap" },
|
|
875
|
+
taskNameById[m.body?.task_id] || ""
|
|
876
|
+
),
|
|
877
|
+
},
|
|
878
|
+
{
|
|
879
|
+
label: "Summary",
|
|
880
|
+
key: (m) =>
|
|
881
|
+
div(
|
|
882
|
+
{
|
|
883
|
+
style:
|
|
884
|
+
"white-space:pre-wrap;font-size:0.82rem;max-width:520px;",
|
|
885
|
+
},
|
|
886
|
+
m.body.text || ""
|
|
887
|
+
),
|
|
888
|
+
},
|
|
889
|
+
],
|
|
890
|
+
entries
|
|
891
|
+
) +
|
|
892
|
+
button(
|
|
893
|
+
{
|
|
894
|
+
class: "btn btn-outline-danger btn-sm mt-2",
|
|
895
|
+
onclick: `view_post(${JSON.stringify(
|
|
896
|
+
viewname
|
|
897
|
+
)}, 'del_phase_progress', {idx:${idx}}, () => copilotRefreshPhaseProgress(${idx}))`,
|
|
898
|
+
},
|
|
899
|
+
"Delete all"
|
|
900
|
+
)
|
|
901
|
+
);
|
|
902
|
+
};
|
|
903
|
+
|
|
904
|
+
const phaseDetailHtml = async (phase, idx) => {
|
|
905
|
+
const tabId = `phase-detail-tabs-${idx}`;
|
|
906
|
+
const [plContent, dmContent, ftContent, pgContent] = await Promise.all([
|
|
907
|
+
phaseTasksHtml(idx, "plugin"),
|
|
908
|
+
phaseTasksHtml(idx, "data_model"),
|
|
909
|
+
phaseTasksHtml(idx, "feature"),
|
|
910
|
+
phaseProgressHtml(idx),
|
|
911
|
+
]);
|
|
912
|
+
|
|
913
|
+
const backBtn = button(
|
|
914
|
+
{
|
|
915
|
+
class: "btn btn-sm btn-outline-secondary mb-3",
|
|
916
|
+
onclick: "copilotRefreshPhases()",
|
|
917
|
+
},
|
|
918
|
+
i({ class: "fas fa-arrow-left me-1" }),
|
|
919
|
+
"Back to phases"
|
|
920
|
+
);
|
|
921
|
+
|
|
922
|
+
const feedbackBtn = features.view_route_modal
|
|
923
|
+
? button(
|
|
924
|
+
{
|
|
925
|
+
class: "btn btn-outline-secondary btn-sm flex-shrink-0 ms-4",
|
|
926
|
+
onclick: `ajax_modal('/view/${encodeURIComponent(
|
|
927
|
+
viewname
|
|
928
|
+
)}/get_feedback_form?scope=phase_${idx}', {method:'POST'})`,
|
|
929
|
+
title: "Add feedback for this phase",
|
|
930
|
+
},
|
|
931
|
+
i({ class: "fas fa-comment-alt me-1" }),
|
|
932
|
+
"Feedback"
|
|
933
|
+
)
|
|
934
|
+
: "";
|
|
935
|
+
|
|
936
|
+
const header = div(
|
|
937
|
+
{ class: "d-flex align-items-start gap-3 mb-2" },
|
|
938
|
+
span(
|
|
939
|
+
{
|
|
940
|
+
class:
|
|
941
|
+
"badge bg-primary rounded-circle d-flex align-items-center justify-content-center fs-6 flex-shrink-0",
|
|
942
|
+
style: "min-width:2rem;height:2rem;",
|
|
943
|
+
},
|
|
944
|
+
String(idx + 1)
|
|
945
|
+
),
|
|
946
|
+
div(
|
|
947
|
+
{ class: "flex-grow-1" },
|
|
948
|
+
h6({ class: "fw-semibold mb-1" }, phase.name),
|
|
949
|
+
p({ class: "text-muted small mb-0" }, phase.description)
|
|
950
|
+
),
|
|
951
|
+
feedbackBtn
|
|
952
|
+
);
|
|
953
|
+
|
|
954
|
+
const tabs = `
|
|
955
|
+
<ul class="nav nav-tabs" id="${tabId}" role="tablist">
|
|
956
|
+
<li class="nav-item" role="presentation">
|
|
957
|
+
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#${tabId}-pl" type="button">Plugins</button>
|
|
958
|
+
</li>
|
|
959
|
+
<li class="nav-item" role="presentation">
|
|
960
|
+
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#${tabId}-dm" type="button">Data model</button>
|
|
961
|
+
</li>
|
|
962
|
+
<li class="nav-item" role="presentation">
|
|
963
|
+
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#${tabId}-ft" type="button">Features</button>
|
|
964
|
+
</li>
|
|
965
|
+
<li class="nav-item" role="presentation">
|
|
966
|
+
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#${tabId}-pg" type="button">Progress</button>
|
|
967
|
+
</li>
|
|
968
|
+
</ul>
|
|
969
|
+
<div class="tab-content pt-3">
|
|
970
|
+
<div class="tab-pane fade show active" id="${tabId}-pl">
|
|
971
|
+
<div id="phase-plugins-area">${plContent}</div>
|
|
972
|
+
</div>
|
|
973
|
+
<div class="tab-pane fade" id="${tabId}-dm">
|
|
974
|
+
<div id="phase-data-model-area">${dmContent}</div>
|
|
975
|
+
</div>
|
|
976
|
+
<div class="tab-pane fade" id="${tabId}-ft">
|
|
977
|
+
<div id="phase-features-area">${ftContent}</div>
|
|
978
|
+
</div>
|
|
979
|
+
<div class="tab-pane fade" id="${tabId}-pg">
|
|
980
|
+
<div id="phase-progress-area-${idx}">${pgContent}</div>
|
|
981
|
+
</div>
|
|
982
|
+
</div>`;
|
|
983
|
+
|
|
984
|
+
return backBtn + header + tabs;
|
|
985
|
+
};
|
|
986
|
+
|
|
987
|
+
// ── Panel wrapper (rendered on page load) ─────────────────────────────────────
|
|
988
|
+
|
|
989
|
+
const phasesPanel = async (req) => {
|
|
990
|
+
const innerHtml = await phasesHtml(req);
|
|
991
|
+
return div({ class: "mt-2" }, div({ id: "phases-panel" }, innerHtml));
|
|
992
|
+
};
|
|
993
|
+
|
|
994
|
+
// ── Phase generation ──────────────────────────────────────────────────────────
|
|
995
|
+
|
|
996
|
+
const doGenPhases = async (userId) => {
|
|
997
|
+
const generatingMd = await MetaData.create({
|
|
998
|
+
type: "CopilotConstructMgr",
|
|
999
|
+
name: "generating_phases",
|
|
1000
|
+
body: {},
|
|
1001
|
+
user_id: userId,
|
|
1002
|
+
});
|
|
1003
|
+
try {
|
|
1004
|
+
const generator = await PromptGenerator.createInstance();
|
|
1005
|
+
if (!generator.spec) throw new Error("Specification not found");
|
|
1006
|
+
const answer = await getState().functions.llm_generate.run(
|
|
1007
|
+
generator.phasesPlanPrompt(),
|
|
1008
|
+
{
|
|
1009
|
+
tools: [phases_tool],
|
|
1010
|
+
...tool_choice("set_phases"),
|
|
1011
|
+
systemPrompt:
|
|
1012
|
+
"You are a senior software architect and project manager. " +
|
|
1013
|
+
"Break the application into logical delivery phases, each containing the requirements that belong to that phase. " +
|
|
1014
|
+
"Only include what is explicitly stated in the specification — do not infer or add plausible extras.",
|
|
1015
|
+
}
|
|
1016
|
+
);
|
|
1017
|
+
|
|
1018
|
+
const tc = answer.getToolCalls()[0];
|
|
1019
|
+
|
|
1020
|
+
// Delete all phase tasks before replacing phases (phase indices will shift)
|
|
1021
|
+
const allTasks = await MetaData.find({
|
|
1022
|
+
type: "CopilotConstructMgr",
|
|
1023
|
+
name: "task",
|
|
1024
|
+
});
|
|
1025
|
+
for (const t of allTasks.filter((t) => t.body?.phase_idx !== undefined))
|
|
1026
|
+
await t.delete();
|
|
1027
|
+
|
|
1028
|
+
const existing = await MetaData.findOne({
|
|
1029
|
+
type: "CopilotConstructMgr",
|
|
1030
|
+
name: "phases",
|
|
1031
|
+
});
|
|
1032
|
+
if (existing) {
|
|
1033
|
+
await existing.update({ body: { phases: tc.input.phases } });
|
|
1034
|
+
} else {
|
|
1035
|
+
await MetaData.create({
|
|
1036
|
+
type: "CopilotConstructMgr",
|
|
1037
|
+
name: "phases",
|
|
1038
|
+
body: { phases: tc.input.phases },
|
|
1039
|
+
user_id: userId,
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
} finally {
|
|
1043
|
+
await generatingMd.delete();
|
|
1044
|
+
try {
|
|
1045
|
+
getState().emitDynamicUpdate(db.getTenantSchema(), {
|
|
1046
|
+
eval_js:
|
|
1047
|
+
"if(typeof copilotRefreshPhases==='function')copilotRefreshPhases();",
|
|
1048
|
+
});
|
|
1049
|
+
} catch (_) {}
|
|
1050
|
+
}
|
|
1051
|
+
};
|
|
1052
|
+
|
|
1053
|
+
// ── Task generation for a phase ───────────────────────────────────────────────
|
|
1054
|
+
|
|
1055
|
+
// taskType: "data_model" | "feature" | null (null = generate both)
|
|
1056
|
+
const doGenPhaseTasks = async (phase, userId, taskType) => {
|
|
1057
|
+
const generatingMd = await MetaData.create({
|
|
1058
|
+
type: "CopilotConstructMgr",
|
|
1059
|
+
name: "generating_phase_tasks",
|
|
1060
|
+
body: { phase_idx: phase.idx, task_type: taskType },
|
|
1061
|
+
user_id: userId,
|
|
1062
|
+
});
|
|
1063
|
+
try {
|
|
1064
|
+
const generator = await PromptGenerator.createInstance({ phase });
|
|
1065
|
+
const answer = await getState().functions.llm_generate.run(
|
|
1066
|
+
generator.taskPlanPrompt(taskType),
|
|
1067
|
+
{
|
|
1068
|
+
tools: [task_tool],
|
|
1069
|
+
...tool_choice("plan_tasks"),
|
|
1070
|
+
systemPrompt:
|
|
1071
|
+
"You are a project manager planning implementation tasks for a Saltcorn application. " +
|
|
1072
|
+
"Each task must map to a concrete deliverable (a view, page, trigger, or schema change). " +
|
|
1073
|
+
"Keep tasks small and focused.",
|
|
1074
|
+
}
|
|
1075
|
+
);
|
|
1076
|
+
|
|
1077
|
+
const tc = answer.getToolCalls()[0];
|
|
1078
|
+
|
|
1079
|
+
// Remove existing tasks of the relevant type(s) before storing new ones
|
|
1080
|
+
const existing = await MetaData.find({
|
|
1081
|
+
type: "CopilotConstructMgr",
|
|
1082
|
+
name: "task",
|
|
1083
|
+
});
|
|
1084
|
+
const phaseTasks = existing.filter((t) => t.body?.phase_idx === phase.idx);
|
|
1085
|
+
for (const t of phaseTasks) {
|
|
1086
|
+
const tType = t.body.task_type || "feature";
|
|
1087
|
+
if (tType === taskType) await t.delete();
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
// Clear any existing "no tasks needed" markers for this phase
|
|
1091
|
+
if (taskType === "plugin") {
|
|
1092
|
+
const oldMarkers = await MetaData.find({
|
|
1093
|
+
type: "CopilotConstructMgr",
|
|
1094
|
+
name: "phase_plugin_generated",
|
|
1095
|
+
});
|
|
1096
|
+
for (const m of oldMarkers.filter((m) => m.body?.phase_idx === phase.idx))
|
|
1097
|
+
await m.delete();
|
|
1098
|
+
} else if (taskType === "data_model") {
|
|
1099
|
+
const oldMarkers = await MetaData.find({
|
|
1100
|
+
type: "CopilotConstructMgr",
|
|
1101
|
+
name: "phase_data_model_generated",
|
|
1102
|
+
});
|
|
1103
|
+
for (const m of oldMarkers.filter((m) => m.body?.phase_idx === phase.idx))
|
|
1104
|
+
await m.delete();
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
for (const task of tc.input.tasks)
|
|
1108
|
+
await MetaData.create({
|
|
1109
|
+
type: "CopilotConstructMgr",
|
|
1110
|
+
name: "task",
|
|
1111
|
+
body: { ...task, phase_idx: phase.idx, phase_name: phase.name },
|
|
1112
|
+
user_id: userId,
|
|
1113
|
+
});
|
|
1114
|
+
|
|
1115
|
+
// If generation produced 0 tasks, record that it was considered
|
|
1116
|
+
if (
|
|
1117
|
+
taskType === "plugin" &&
|
|
1118
|
+
tc.input.tasks.filter((t) => t.task_type === "plugin").length === 0
|
|
1119
|
+
) {
|
|
1120
|
+
await MetaData.create({
|
|
1121
|
+
type: "CopilotConstructMgr",
|
|
1122
|
+
name: "phase_plugin_generated",
|
|
1123
|
+
body: { phase_idx: phase.idx },
|
|
1124
|
+
user_id: userId,
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
if (
|
|
1128
|
+
taskType === "data_model" &&
|
|
1129
|
+
tc.input.tasks.filter((t) => t.task_type === "data_model").length === 0
|
|
1130
|
+
) {
|
|
1131
|
+
await MetaData.create({
|
|
1132
|
+
type: "CopilotConstructMgr",
|
|
1133
|
+
name: "phase_data_model_generated",
|
|
1134
|
+
body: { phase_idx: phase.idx },
|
|
1135
|
+
user_id: userId,
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
} finally {
|
|
1139
|
+
await generatingMd.delete();
|
|
1140
|
+
try {
|
|
1141
|
+
getState().emitDynamicUpdate(db.getTenantSchema(), {
|
|
1142
|
+
eval_js: `if(typeof copilotPhaseTasksDone==='function')copilotPhaseTasksDone(${phase.idx});`,
|
|
1143
|
+
});
|
|
1144
|
+
} catch (_) {}
|
|
1145
|
+
}
|
|
1146
|
+
};
|
|
1147
|
+
|
|
1148
|
+
// ── Phase task chain runner ───────────────────────────────────────────────────
|
|
1149
|
+
|
|
1150
|
+
const doRunPhaseTasks = async (phaseIdx, taskType, req) => {
|
|
1151
|
+
const flagName = `phase_running_${phaseIdx}_${taskType}`;
|
|
1152
|
+
const running = await MetaData.findOne({
|
|
1153
|
+
type: "CopilotConstructMgr",
|
|
1154
|
+
name: flagName,
|
|
1155
|
+
});
|
|
1156
|
+
if (!running) return;
|
|
1157
|
+
|
|
1158
|
+
const allTasks = await MetaData.find({
|
|
1159
|
+
type: "CopilotConstructMgr",
|
|
1160
|
+
name: "task",
|
|
1161
|
+
});
|
|
1162
|
+
const phaseTasks = allTasks.filter((t) => t.body?.phase_idx === phaseIdx);
|
|
1163
|
+
const typedTasks = phaseTasks.filter(
|
|
1164
|
+
(t) => (t.body.task_type || "feature") === taskType
|
|
1165
|
+
);
|
|
1166
|
+
|
|
1167
|
+
if (typedTasks.some((t) => t.body.status === "Running")) return;
|
|
1168
|
+
|
|
1169
|
+
const doneNames = new Set(
|
|
1170
|
+
phaseTasks.filter((t) => t.body.status === "Done").map((t) => t.body.name)
|
|
1171
|
+
);
|
|
1172
|
+
// Only block on same-type dependencies; cross-type deps are the user's ordering concern
|
|
1173
|
+
const sameTypeNames = new Set(
|
|
1174
|
+
typedTasks.map((t) => t.body.name).filter(Boolean)
|
|
1175
|
+
);
|
|
1176
|
+
const todos = typedTasks.filter(
|
|
1177
|
+
(t) => !t.body.status || t.body.status === "To do"
|
|
1178
|
+
);
|
|
1179
|
+
const startable = todos.filter((t) =>
|
|
1180
|
+
(t.body.depends_on || []).every(
|
|
1181
|
+
(nm) => doneNames.has(nm) || !sameTypeNames.has(nm)
|
|
1182
|
+
)
|
|
1183
|
+
);
|
|
1184
|
+
|
|
1185
|
+
if (startable[0]) {
|
|
1186
|
+
await runTask(startable[0].id, req);
|
|
1187
|
+
await doRunPhaseTasks(phaseIdx, taskType, req);
|
|
1188
|
+
} else {
|
|
1189
|
+
const runningMd = await MetaData.findOne({
|
|
1190
|
+
type: "CopilotConstructMgr",
|
|
1191
|
+
name: flagName,
|
|
1192
|
+
});
|
|
1193
|
+
if (runningMd) await runningMd.delete();
|
|
1194
|
+
try {
|
|
1195
|
+
getState().emitDynamicUpdate(db.getTenantSchema(), {
|
|
1196
|
+
eval_js: `if(typeof copilotPhaseTasksDone==='function')copilotPhaseTasksDone(${phaseIdx});`,
|
|
1197
|
+
});
|
|
1198
|
+
} catch (_) {}
|
|
1199
|
+
}
|
|
1200
|
+
};
|
|
1201
|
+
|
|
1202
|
+
// ── Routes ────────────────────────────────────────────────────────────────────
|
|
1203
|
+
|
|
1204
|
+
const gen_phases = async (table_id, vn, config, body, { req, res }) => {
|
|
1205
|
+
doGenPhases(req.user?.id).catch((e) => console.error("gen_phases error", e));
|
|
1206
|
+
return { json: { success: true } };
|
|
1207
|
+
};
|
|
1208
|
+
|
|
1209
|
+
const phases_status = async (table_id, vn, config, body, { req, res }) => {
|
|
1210
|
+
const generating = await MetaData.findOne({
|
|
1211
|
+
type: "CopilotConstructMgr",
|
|
1212
|
+
name: "generating_phases",
|
|
1213
|
+
});
|
|
1214
|
+
return { json: { generating: !!generating } };
|
|
1215
|
+
};
|
|
1216
|
+
|
|
1217
|
+
const phases_html = async (table_id, vn, config, body, { req, res }) => {
|
|
1218
|
+
const html = await phasesHtml(req);
|
|
1219
|
+
return { json: { html } };
|
|
1220
|
+
};
|
|
1221
|
+
|
|
1222
|
+
const phase_detail_html = async (table_id, vn, config, body, { req, res }) => {
|
|
1223
|
+
const idx = parseInt(body.idx);
|
|
1224
|
+
const phasesMd = await MetaData.findOne({
|
|
1225
|
+
type: "CopilotConstructMgr",
|
|
1226
|
+
name: "phases",
|
|
1227
|
+
});
|
|
1228
|
+
const phase = phasesMd?.body?.phases?.[idx];
|
|
1229
|
+
if (!phase) return { json: { error: "Phase not found" } };
|
|
1230
|
+
return { json: { html: await phaseDetailHtml(phase, idx) } };
|
|
1231
|
+
};
|
|
1232
|
+
|
|
1233
|
+
const generate_phase_tasks = async (
|
|
1234
|
+
table_id,
|
|
1235
|
+
vn,
|
|
1236
|
+
config,
|
|
1237
|
+
body,
|
|
1238
|
+
{ req, res }
|
|
1239
|
+
) => {
|
|
1240
|
+
const idx = parseInt(body.idx);
|
|
1241
|
+
const taskType = body.task_type || null;
|
|
1242
|
+
const phasesMd = await MetaData.findOne({
|
|
1243
|
+
type: "CopilotConstructMgr",
|
|
1244
|
+
name: "phases",
|
|
1245
|
+
});
|
|
1246
|
+
const phase = phasesMd?.body?.phases?.[idx];
|
|
1247
|
+
if (!phase) return { json: { error: "Phase not found" } };
|
|
1248
|
+
phase.idx = idx;
|
|
1249
|
+
doGenPhaseTasks(phase, req.user?.id, taskType).catch((e) =>
|
|
1250
|
+
console.error("generate_phase_tasks error", e)
|
|
1251
|
+
);
|
|
1252
|
+
return { json: { success: true } };
|
|
1253
|
+
};
|
|
1254
|
+
|
|
1255
|
+
const phase_tasks_status = async (table_id, vn, config, body, { req, res }) => {
|
|
1256
|
+
const idx = parseInt(body.idx);
|
|
1257
|
+
const taskType = body.task_type || null;
|
|
1258
|
+
const generating = await MetaData.findOne({
|
|
1259
|
+
type: "CopilotConstructMgr",
|
|
1260
|
+
name: "generating_phase_tasks",
|
|
1261
|
+
});
|
|
1262
|
+
const isGenerating = !!(
|
|
1263
|
+
generating &&
|
|
1264
|
+
generating.body?.phase_idx === idx &&
|
|
1265
|
+
(!taskType ||
|
|
1266
|
+
!generating.body?.task_type ||
|
|
1267
|
+
generating.body?.task_type === taskType)
|
|
1268
|
+
);
|
|
1269
|
+
return { json: { generating: isGenerating } };
|
|
1270
|
+
};
|
|
1271
|
+
|
|
1272
|
+
const phase_tasks_html = async (table_id, vn, config, body, { req, res }) => {
|
|
1273
|
+
const idx = parseInt(body.idx);
|
|
1274
|
+
const taskType = body.task_type || "feature";
|
|
1275
|
+
const html = await phaseTasksHtml(idx, taskType);
|
|
1276
|
+
return { json: { html } };
|
|
1277
|
+
};
|
|
1278
|
+
|
|
1279
|
+
const run_phase_tasks = async (table_id, vn, config, body, { req, res }) => {
|
|
1280
|
+
const idx = parseInt(body.idx);
|
|
1281
|
+
const taskType = body.task_type || "feature";
|
|
1282
|
+
const flagName = `phase_running_${idx}_${taskType}`;
|
|
1283
|
+
const existing = await MetaData.findOne({
|
|
1284
|
+
type: "CopilotConstructMgr",
|
|
1285
|
+
name: flagName,
|
|
1286
|
+
});
|
|
1287
|
+
if (!existing)
|
|
1288
|
+
await MetaData.create({
|
|
1289
|
+
type: "CopilotConstructMgr",
|
|
1290
|
+
name: flagName,
|
|
1291
|
+
body: { started: Date.now() },
|
|
1292
|
+
user_id: req.user?.id,
|
|
1293
|
+
});
|
|
1294
|
+
doRunPhaseTasks(idx, taskType, {
|
|
1295
|
+
user: req.user,
|
|
1296
|
+
__: req.__ || ((s) => s),
|
|
1297
|
+
getLocale: req.getLocale || (() => "en"),
|
|
1298
|
+
}).catch((e) => console.error("run_phase_tasks error", e));
|
|
1299
|
+
return { json: { success: true } };
|
|
1300
|
+
};
|
|
1301
|
+
|
|
1302
|
+
const stop_phase_tasks = async (table_id, vn, config, body, { req, res }) => {
|
|
1303
|
+
const idx = parseInt(body.idx);
|
|
1304
|
+
const taskType = body.task_type || "feature";
|
|
1305
|
+
const running = await MetaData.findOne({
|
|
1306
|
+
type: "CopilotConstructMgr",
|
|
1307
|
+
name: `phase_running_${idx}_${taskType}`,
|
|
1308
|
+
});
|
|
1309
|
+
if (running) await running.delete();
|
|
1310
|
+
const allTasks = await MetaData.find({
|
|
1311
|
+
type: "CopilotConstructMgr",
|
|
1312
|
+
name: "task",
|
|
1313
|
+
});
|
|
1314
|
+
const taskStillRunning = allTasks.some(
|
|
1315
|
+
(t) =>
|
|
1316
|
+
t.body?.phase_idx === idx &&
|
|
1317
|
+
(t.body?.task_type || "feature") === taskType &&
|
|
1318
|
+
t.body?.status === "Running"
|
|
1319
|
+
);
|
|
1320
|
+
return { json: { success: true, taskStillRunning } };
|
|
1321
|
+
};
|
|
1322
|
+
|
|
1323
|
+
const del_phase_type_tasks = async (
|
|
1324
|
+
table_id,
|
|
1325
|
+
vn,
|
|
1326
|
+
config,
|
|
1327
|
+
body,
|
|
1328
|
+
{ req, res }
|
|
1329
|
+
) => {
|
|
1330
|
+
const idx = parseInt(body.idx);
|
|
1331
|
+
const taskType = body.task_type || "feature";
|
|
1332
|
+
const allTasks = await MetaData.find({
|
|
1333
|
+
type: "CopilotConstructMgr",
|
|
1334
|
+
name: "task",
|
|
1335
|
+
});
|
|
1336
|
+
for (const t of allTasks) {
|
|
1337
|
+
if (
|
|
1338
|
+
t.body?.phase_idx === idx &&
|
|
1339
|
+
(t.body?.task_type || "feature") === taskType
|
|
1340
|
+
)
|
|
1341
|
+
await t.delete();
|
|
1342
|
+
}
|
|
1343
|
+
if (taskType === "plugin") {
|
|
1344
|
+
const markers = await MetaData.find({
|
|
1345
|
+
type: "CopilotConstructMgr",
|
|
1346
|
+
name: "phase_plugin_generated",
|
|
1347
|
+
});
|
|
1348
|
+
for (const m of markers.filter((m) => m.body?.phase_idx === idx))
|
|
1349
|
+
await m.delete();
|
|
1350
|
+
const pluginPhase = await MetaData.find({
|
|
1351
|
+
type: "CopilotConstructMgr",
|
|
1352
|
+
name: "plugin_phase",
|
|
1353
|
+
});
|
|
1354
|
+
for (const m of pluginPhase.filter((m) => m.body?.phase_idx === idx))
|
|
1355
|
+
await m.delete();
|
|
1356
|
+
}
|
|
1357
|
+
if (taskType === "data_model") {
|
|
1358
|
+
const tablePhase = await MetaData.find({
|
|
1359
|
+
type: "CopilotConstructMgr",
|
|
1360
|
+
name: "table_phase",
|
|
1361
|
+
});
|
|
1362
|
+
for (const m of tablePhase.filter((m) => m.body?.phase_idx === idx))
|
|
1363
|
+
await m.delete();
|
|
1364
|
+
const dmMarkers = await MetaData.find({
|
|
1365
|
+
type: "CopilotConstructMgr",
|
|
1366
|
+
name: "phase_data_model_generated",
|
|
1367
|
+
});
|
|
1368
|
+
for (const m of dmMarkers.filter((m) => m.body?.phase_idx === idx))
|
|
1369
|
+
await m.delete();
|
|
1370
|
+
}
|
|
1371
|
+
if (taskType === "feature") {
|
|
1372
|
+
const viewPhase = await MetaData.find({
|
|
1373
|
+
type: "CopilotConstructMgr",
|
|
1374
|
+
name: "view_phase",
|
|
1375
|
+
});
|
|
1376
|
+
for (const m of viewPhase.filter((m) => m.body?.phase_idx === idx))
|
|
1377
|
+
await m.delete();
|
|
1378
|
+
}
|
|
1379
|
+
return { json: { success: true } };
|
|
1380
|
+
};
|
|
1381
|
+
|
|
1382
|
+
const phase_run_status = async (table_id, vn, config, body, { req, res }) => {
|
|
1383
|
+
const idx = parseInt(body.idx);
|
|
1384
|
+
const taskType = body.task_type || "feature";
|
|
1385
|
+
const runningMd = await MetaData.findOne({
|
|
1386
|
+
type: "CopilotConstructMgr",
|
|
1387
|
+
name: `phase_running_${idx}_${taskType}`,
|
|
1388
|
+
});
|
|
1389
|
+
const isRunning = !!runningMd;
|
|
1390
|
+
const allTasks = await MetaData.find({
|
|
1391
|
+
type: "CopilotConstructMgr",
|
|
1392
|
+
name: "task",
|
|
1393
|
+
});
|
|
1394
|
+
const runningTask = allTasks.find(
|
|
1395
|
+
(t) =>
|
|
1396
|
+
t.body?.phase_idx === idx &&
|
|
1397
|
+
(t.body?.task_type || "feature") === taskType &&
|
|
1398
|
+
t.body?.status === "Running"
|
|
1399
|
+
);
|
|
1400
|
+
const anyRunning = !!runningTask;
|
|
1401
|
+
return {
|
|
1402
|
+
json: {
|
|
1403
|
+
isRunning,
|
|
1404
|
+
anyRunning,
|
|
1405
|
+
runningTaskName: runningTask?.body?.name || null,
|
|
1406
|
+
},
|
|
1407
|
+
};
|
|
1408
|
+
};
|
|
1409
|
+
|
|
1410
|
+
const del_all_phases = async (table_id, vn, config, body, { req, res }) => {
|
|
1411
|
+
const allTasks = await MetaData.find({
|
|
1412
|
+
type: "CopilotConstructMgr",
|
|
1413
|
+
name: "task",
|
|
1414
|
+
});
|
|
1415
|
+
for (const t of allTasks.filter((t) => t.body?.phase_idx !== undefined))
|
|
1416
|
+
await t.delete();
|
|
1417
|
+
const markers = await MetaData.find({
|
|
1418
|
+
type: "CopilotConstructMgr",
|
|
1419
|
+
name: "phase_plugin_generated",
|
|
1420
|
+
});
|
|
1421
|
+
for (const m of markers) await m.delete();
|
|
1422
|
+
const tablePhase = await MetaData.find({
|
|
1423
|
+
type: "CopilotConstructMgr",
|
|
1424
|
+
name: "table_phase",
|
|
1425
|
+
});
|
|
1426
|
+
for (const m of tablePhase) await m.delete();
|
|
1427
|
+
const viewPhase = await MetaData.find({
|
|
1428
|
+
type: "CopilotConstructMgr",
|
|
1429
|
+
name: "view_phase",
|
|
1430
|
+
});
|
|
1431
|
+
for (const m of viewPhase) await m.delete();
|
|
1432
|
+
const pluginPhaseAll = await MetaData.find({
|
|
1433
|
+
type: "CopilotConstructMgr",
|
|
1434
|
+
name: "plugin_phase",
|
|
1435
|
+
});
|
|
1436
|
+
for (const m of pluginPhaseAll) await m.delete();
|
|
1437
|
+
const phasesMd = await MetaData.findOne({
|
|
1438
|
+
type: "CopilotConstructMgr",
|
|
1439
|
+
name: "phases",
|
|
1440
|
+
});
|
|
1441
|
+
if (phasesMd) await phasesMd.delete();
|
|
1442
|
+
return { json: { success: true } };
|
|
1443
|
+
};
|
|
1444
|
+
|
|
1445
|
+
const phase_progress_html = async (
|
|
1446
|
+
table_id,
|
|
1447
|
+
vn,
|
|
1448
|
+
config,
|
|
1449
|
+
body,
|
|
1450
|
+
{ req, res }
|
|
1451
|
+
) => {
|
|
1452
|
+
const idx = parseInt(body.idx);
|
|
1453
|
+
const html = await phaseProgressHtml(idx);
|
|
1454
|
+
return { json: { html } };
|
|
1455
|
+
};
|
|
1456
|
+
|
|
1457
|
+
const del_phase_progress = async (table_id, vn, config, body, { req, res }) => {
|
|
1458
|
+
const idx = parseInt(body.idx);
|
|
1459
|
+
const all = await MetaData.find({
|
|
1460
|
+
type: "CopilotConstructMgr",
|
|
1461
|
+
name: "progress",
|
|
1462
|
+
});
|
|
1463
|
+
for (const r of all.filter((p) => p.body?.phase_idx === idx))
|
|
1464
|
+
await r.delete();
|
|
1465
|
+
return { json: { success: true } };
|
|
1466
|
+
};
|
|
1467
|
+
|
|
1468
|
+
const phase_routes = {
|
|
1469
|
+
gen_phases,
|
|
1470
|
+
phases_status,
|
|
1471
|
+
phases_html,
|
|
1472
|
+
phase_detail_html,
|
|
1473
|
+
generate_phase_tasks,
|
|
1474
|
+
phase_tasks_status,
|
|
1475
|
+
phase_tasks_html,
|
|
1476
|
+
run_phase_tasks,
|
|
1477
|
+
stop_phase_tasks,
|
|
1478
|
+
phase_run_status,
|
|
1479
|
+
del_phase_type_tasks,
|
|
1480
|
+
del_all_phases,
|
|
1481
|
+
phase_progress_html,
|
|
1482
|
+
del_phase_progress,
|
|
1483
|
+
};
|
|
1484
|
+
|
|
1485
|
+
module.exports = { phasesPanel, phasesStaticScript, phase_routes };
|