dtSpark 1.0.4__py3-none-any.whl
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.
- dtSpark/__init__.py +0 -0
- dtSpark/_description.txt +1 -0
- dtSpark/_full_name.txt +1 -0
- dtSpark/_licence.txt +21 -0
- dtSpark/_metadata.yaml +6 -0
- dtSpark/_name.txt +1 -0
- dtSpark/_version.txt +1 -0
- dtSpark/aws/__init__.py +7 -0
- dtSpark/aws/authentication.py +296 -0
- dtSpark/aws/bedrock.py +578 -0
- dtSpark/aws/costs.py +318 -0
- dtSpark/aws/pricing.py +580 -0
- dtSpark/cli_interface.py +2645 -0
- dtSpark/conversation_manager.py +3050 -0
- dtSpark/core/__init__.py +12 -0
- dtSpark/core/application.py +3355 -0
- dtSpark/core/context_compaction.py +735 -0
- dtSpark/daemon/__init__.py +104 -0
- dtSpark/daemon/__main__.py +10 -0
- dtSpark/daemon/action_monitor.py +213 -0
- dtSpark/daemon/daemon_app.py +730 -0
- dtSpark/daemon/daemon_manager.py +289 -0
- dtSpark/daemon/execution_coordinator.py +194 -0
- dtSpark/daemon/pid_file.py +169 -0
- dtSpark/database/__init__.py +482 -0
- dtSpark/database/autonomous_actions.py +1191 -0
- dtSpark/database/backends.py +329 -0
- dtSpark/database/connection.py +122 -0
- dtSpark/database/conversations.py +520 -0
- dtSpark/database/credential_prompt.py +218 -0
- dtSpark/database/files.py +205 -0
- dtSpark/database/mcp_ops.py +355 -0
- dtSpark/database/messages.py +161 -0
- dtSpark/database/schema.py +673 -0
- dtSpark/database/tool_permissions.py +186 -0
- dtSpark/database/usage.py +167 -0
- dtSpark/files/__init__.py +4 -0
- dtSpark/files/manager.py +322 -0
- dtSpark/launch.py +39 -0
- dtSpark/limits/__init__.py +10 -0
- dtSpark/limits/costs.py +296 -0
- dtSpark/limits/tokens.py +342 -0
- dtSpark/llm/__init__.py +17 -0
- dtSpark/llm/anthropic_direct.py +446 -0
- dtSpark/llm/base.py +146 -0
- dtSpark/llm/context_limits.py +438 -0
- dtSpark/llm/manager.py +177 -0
- dtSpark/llm/ollama.py +578 -0
- dtSpark/mcp_integration/__init__.py +5 -0
- dtSpark/mcp_integration/manager.py +653 -0
- dtSpark/mcp_integration/tool_selector.py +225 -0
- dtSpark/resources/config.yaml.template +631 -0
- dtSpark/safety/__init__.py +22 -0
- dtSpark/safety/llm_service.py +111 -0
- dtSpark/safety/patterns.py +229 -0
- dtSpark/safety/prompt_inspector.py +442 -0
- dtSpark/safety/violation_logger.py +346 -0
- dtSpark/scheduler/__init__.py +20 -0
- dtSpark/scheduler/creation_tools.py +599 -0
- dtSpark/scheduler/execution_queue.py +159 -0
- dtSpark/scheduler/executor.py +1152 -0
- dtSpark/scheduler/manager.py +395 -0
- dtSpark/tools/__init__.py +4 -0
- dtSpark/tools/builtin.py +833 -0
- dtSpark/web/__init__.py +20 -0
- dtSpark/web/auth.py +152 -0
- dtSpark/web/dependencies.py +37 -0
- dtSpark/web/endpoints/__init__.py +17 -0
- dtSpark/web/endpoints/autonomous_actions.py +1125 -0
- dtSpark/web/endpoints/chat.py +621 -0
- dtSpark/web/endpoints/conversations.py +353 -0
- dtSpark/web/endpoints/main_menu.py +547 -0
- dtSpark/web/endpoints/streaming.py +421 -0
- dtSpark/web/server.py +578 -0
- dtSpark/web/session.py +167 -0
- dtSpark/web/ssl_utils.py +195 -0
- dtSpark/web/static/css/dark-theme.css +427 -0
- dtSpark/web/static/js/actions.js +1101 -0
- dtSpark/web/static/js/chat.js +614 -0
- dtSpark/web/static/js/main.js +496 -0
- dtSpark/web/static/js/sse-client.js +242 -0
- dtSpark/web/templates/actions.html +408 -0
- dtSpark/web/templates/base.html +93 -0
- dtSpark/web/templates/chat.html +814 -0
- dtSpark/web/templates/conversations.html +350 -0
- dtSpark/web/templates/goodbye.html +81 -0
- dtSpark/web/templates/login.html +90 -0
- dtSpark/web/templates/main_menu.html +983 -0
- dtSpark/web/templates/new_conversation.html +191 -0
- dtSpark/web/web_interface.py +137 -0
- dtspark-1.0.4.dist-info/METADATA +187 -0
- dtspark-1.0.4.dist-info/RECORD +96 -0
- dtspark-1.0.4.dist-info/WHEEL +5 -0
- dtspark-1.0.4.dist-info/entry_points.txt +3 -0
- dtspark-1.0.4.dist-info/licenses/LICENSE +21 -0
- dtspark-1.0.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Autonomous Actions JavaScript for Spark web interface
|
|
3
|
+
*
|
|
4
|
+
* Handles action management, runs viewing, and API interactions
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Store for current context
|
|
8
|
+
let currentActionId = null;
|
|
9
|
+
let currentRunId = null;
|
|
10
|
+
let availableModels = [];
|
|
11
|
+
let availableTools = [];
|
|
12
|
+
|
|
13
|
+
// =============================================================================
|
|
14
|
+
// LOAD AND DISPLAY ACTIONS
|
|
15
|
+
// =============================================================================
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Load all actions from API
|
|
19
|
+
*/
|
|
20
|
+
async function loadActions() {
|
|
21
|
+
const tbody = document.getElementById('actions-tbody');
|
|
22
|
+
const includeDisabled = document.getElementById('showDisabled').checked;
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const response = await fetch(`/api/actions?include_disabled=${includeDisabled}`);
|
|
26
|
+
if (!response.ok) throw new Error('Failed to load actions');
|
|
27
|
+
|
|
28
|
+
const actions = await response.json();
|
|
29
|
+
|
|
30
|
+
if (actions.length === 0) {
|
|
31
|
+
tbody.innerHTML = `
|
|
32
|
+
<tr>
|
|
33
|
+
<td colspan="9" class="text-center text-muted py-4">
|
|
34
|
+
<i class="bi bi-inbox"></i> No actions found.
|
|
35
|
+
<button class="btn btn-link" onclick="showCreateModal()">Create your first action</button>
|
|
36
|
+
</td>
|
|
37
|
+
</tr>
|
|
38
|
+
`;
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
tbody.innerHTML = actions.map(action => `
|
|
43
|
+
<tr class="${!action.is_enabled ? 'table-secondary' : ''}">
|
|
44
|
+
<td>${action.id}</td>
|
|
45
|
+
<td>
|
|
46
|
+
<a href="#" onclick="viewAction(${action.id}); return false;">
|
|
47
|
+
${escapeHtml(action.name)}
|
|
48
|
+
</a>
|
|
49
|
+
</td>
|
|
50
|
+
<td><small class="text-muted">${escapeHtml(action.model_id)}</small></td>
|
|
51
|
+
<td>${formatSchedule(action)}</td>
|
|
52
|
+
<td>
|
|
53
|
+
<span class="badge ${action.context_mode === 'cumulative' ? 'bg-info' : 'bg-secondary'}">
|
|
54
|
+
${action.context_mode}
|
|
55
|
+
</span>
|
|
56
|
+
</td>
|
|
57
|
+
<td>
|
|
58
|
+
${action.is_enabled
|
|
59
|
+
? '<span class="badge bg-success">Enabled</span>'
|
|
60
|
+
: '<span class="badge bg-danger">Disabled</span>'}
|
|
61
|
+
</td>
|
|
62
|
+
<td>${action.last_run_at ? formatTimestamp(action.last_run_at) : '<span class="text-muted">Never</span>'}</td>
|
|
63
|
+
<td>${formatFailures(action)}</td>
|
|
64
|
+
<td>
|
|
65
|
+
<div class="btn-group btn-group-sm">
|
|
66
|
+
<button class="btn btn-outline-primary" onclick="runActionNow(${action.id})" title="Run now">
|
|
67
|
+
<i class="bi bi-play-fill"></i>
|
|
68
|
+
</button>
|
|
69
|
+
<button class="btn btn-outline-secondary" onclick="viewActionRuns(${action.id}, '${escapeHtml(action.name)}')" title="View runs">
|
|
70
|
+
<i class="bi bi-clock-history"></i>
|
|
71
|
+
</button>
|
|
72
|
+
<button class="btn btn-outline-secondary" onclick="editAction(${action.id})" title="Edit">
|
|
73
|
+
<i class="bi bi-pencil"></i>
|
|
74
|
+
</button>
|
|
75
|
+
${action.is_enabled
|
|
76
|
+
? `<button class="btn btn-outline-warning" onclick="disableAction(${action.id})" title="Disable">
|
|
77
|
+
<i class="bi bi-pause-fill"></i>
|
|
78
|
+
</button>`
|
|
79
|
+
: `<button class="btn btn-outline-success" onclick="enableAction(${action.id})" title="Enable">
|
|
80
|
+
<i class="bi bi-play"></i>
|
|
81
|
+
</button>`}
|
|
82
|
+
<button class="btn btn-outline-danger" onclick="confirmDeleteAction(${action.id}, '${escapeHtml(action.name)}')" title="Delete">
|
|
83
|
+
<i class="bi bi-trash"></i>
|
|
84
|
+
</button>
|
|
85
|
+
</div>
|
|
86
|
+
</td>
|
|
87
|
+
</tr>
|
|
88
|
+
`).join('');
|
|
89
|
+
|
|
90
|
+
} catch (error) {
|
|
91
|
+
console.error('Error loading actions:', error);
|
|
92
|
+
tbody.innerHTML = `
|
|
93
|
+
<tr>
|
|
94
|
+
<td colspan="9" class="text-center text-danger py-4">
|
|
95
|
+
<i class="bi bi-exclamation-triangle"></i> Failed to load actions
|
|
96
|
+
</td>
|
|
97
|
+
</tr>
|
|
98
|
+
`;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Load recent runs across all actions
|
|
104
|
+
*/
|
|
105
|
+
async function loadRecentRuns() {
|
|
106
|
+
const tbody = document.getElementById('runs-tbody');
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const response = await fetch('/api/actions/runs/recent?limit=10');
|
|
110
|
+
if (!response.ok) throw new Error('Failed to load recent runs');
|
|
111
|
+
|
|
112
|
+
const runs = await response.json();
|
|
113
|
+
|
|
114
|
+
if (runs.length === 0) {
|
|
115
|
+
tbody.innerHTML = `
|
|
116
|
+
<tr>
|
|
117
|
+
<td colspan="7" class="text-center text-muted py-4">
|
|
118
|
+
<i class="bi bi-inbox"></i> No runs yet
|
|
119
|
+
</td>
|
|
120
|
+
</tr>
|
|
121
|
+
`;
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
tbody.innerHTML = runs.map(run => `
|
|
126
|
+
<tr>
|
|
127
|
+
<td>${run.id}</td>
|
|
128
|
+
<td>${escapeHtml(run.action_name || 'Unknown')}</td>
|
|
129
|
+
<td>${formatTimestamp(run.started_at)}</td>
|
|
130
|
+
<td>${formatRunStatus(run.status)}</td>
|
|
131
|
+
<td>${formatDuration(run.started_at, run.completed_at)}</td>
|
|
132
|
+
<td>${formatTokens(run.input_tokens, run.output_tokens)}</td>
|
|
133
|
+
<td>
|
|
134
|
+
<button class="btn btn-sm btn-outline-secondary" onclick="viewRunDetails(${run.action_id}, ${run.id})">
|
|
135
|
+
<i class="bi bi-eye"></i> View
|
|
136
|
+
</button>
|
|
137
|
+
</td>
|
|
138
|
+
</tr>
|
|
139
|
+
`).join('');
|
|
140
|
+
|
|
141
|
+
} catch (error) {
|
|
142
|
+
console.error('Error loading recent runs:', error);
|
|
143
|
+
tbody.innerHTML = `
|
|
144
|
+
<tr>
|
|
145
|
+
<td colspan="7" class="text-center text-danger py-4">
|
|
146
|
+
<i class="bi bi-exclamation-triangle"></i> Failed to load recent runs
|
|
147
|
+
</td>
|
|
148
|
+
</tr>
|
|
149
|
+
`;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Check for failed actions and show warning
|
|
155
|
+
*/
|
|
156
|
+
async function checkFailedActions() {
|
|
157
|
+
try {
|
|
158
|
+
const response = await fetch('/api/actions/status/failed-count');
|
|
159
|
+
if (!response.ok) return;
|
|
160
|
+
|
|
161
|
+
const data = await response.json();
|
|
162
|
+
const warning = document.getElementById('failed-warning');
|
|
163
|
+
const text = document.getElementById('failed-count-text');
|
|
164
|
+
|
|
165
|
+
if (data.failed_count > 0) {
|
|
166
|
+
text.textContent = `${data.failed_count} action(s) have been auto-disabled due to failures.`;
|
|
167
|
+
warning.classList.remove('d-none');
|
|
168
|
+
} else {
|
|
169
|
+
warning.classList.add('d-none');
|
|
170
|
+
}
|
|
171
|
+
} catch (error) {
|
|
172
|
+
console.error('Error checking failed actions:', error);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// =============================================================================
|
|
177
|
+
// ACTION CRUD OPERATIONS
|
|
178
|
+
// =============================================================================
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Show create action modal
|
|
182
|
+
*/
|
|
183
|
+
function showCreateModal() {
|
|
184
|
+
currentActionId = null;
|
|
185
|
+
document.getElementById('actionModalTitle').textContent = 'Create Action';
|
|
186
|
+
document.getElementById('actionForm').reset();
|
|
187
|
+
document.getElementById('actionId').value = '';
|
|
188
|
+
|
|
189
|
+
// Set default run date to tomorrow at 9am
|
|
190
|
+
const tomorrow = new Date();
|
|
191
|
+
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
192
|
+
tomorrow.setHours(9, 0, 0, 0);
|
|
193
|
+
document.getElementById('runDate').value = tomorrow.toISOString().slice(0, 16);
|
|
194
|
+
|
|
195
|
+
updateScheduleConfig();
|
|
196
|
+
resetToolPermissions();
|
|
197
|
+
|
|
198
|
+
const modal = new bootstrap.Modal(document.getElementById('actionModal'));
|
|
199
|
+
modal.show();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Edit an existing action
|
|
204
|
+
*/
|
|
205
|
+
async function editAction(actionId) {
|
|
206
|
+
try {
|
|
207
|
+
const response = await fetch(`/api/actions/${actionId}`);
|
|
208
|
+
if (!response.ok) throw new Error('Failed to load action');
|
|
209
|
+
|
|
210
|
+
const action = await response.json();
|
|
211
|
+
currentActionId = actionId;
|
|
212
|
+
|
|
213
|
+
document.getElementById('actionModalTitle').textContent = 'Edit Action';
|
|
214
|
+
document.getElementById('actionId').value = actionId;
|
|
215
|
+
document.getElementById('actionName').value = action.name;
|
|
216
|
+
document.getElementById('actionDescription').value = action.description;
|
|
217
|
+
document.getElementById('actionPrompt').value = action.action_prompt;
|
|
218
|
+
document.getElementById('actionModel').value = action.model_id;
|
|
219
|
+
document.getElementById('scheduleType').value = action.schedule_type;
|
|
220
|
+
document.getElementById('contextMode').value = action.context_mode;
|
|
221
|
+
document.getElementById('maxFailures').value = action.max_failures;
|
|
222
|
+
|
|
223
|
+
updateScheduleConfig();
|
|
224
|
+
|
|
225
|
+
// Set schedule config
|
|
226
|
+
if (action.schedule_type === 'one_off' && action.schedule_config?.run_date) {
|
|
227
|
+
document.getElementById('runDate').value = action.schedule_config.run_date.slice(0, 16);
|
|
228
|
+
} else if (action.schedule_type === 'recurring' && action.schedule_config?.cron_expression) {
|
|
229
|
+
document.getElementById('cronExpression').value = action.schedule_config.cron_expression;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Set tool permissions
|
|
233
|
+
setToolPermissions(action.tool_permissions || []);
|
|
234
|
+
|
|
235
|
+
const modal = new bootstrap.Modal(document.getElementById('actionModal'));
|
|
236
|
+
modal.show();
|
|
237
|
+
|
|
238
|
+
} catch (error) {
|
|
239
|
+
console.error('Error loading action:', error);
|
|
240
|
+
showToast('Failed to load action', 'error');
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Save action (create or update)
|
|
246
|
+
*/
|
|
247
|
+
async function saveAction() {
|
|
248
|
+
const actionId = document.getElementById('actionId').value;
|
|
249
|
+
const isEdit = !!actionId;
|
|
250
|
+
|
|
251
|
+
// Gather form data
|
|
252
|
+
const scheduleType = document.getElementById('scheduleType').value;
|
|
253
|
+
let scheduleConfig = {};
|
|
254
|
+
|
|
255
|
+
if (scheduleType === 'one_off') {
|
|
256
|
+
const runDate = document.getElementById('runDate').value;
|
|
257
|
+
if (!runDate) {
|
|
258
|
+
showToast('Please select a run date', 'error');
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
scheduleConfig = { run_date: new Date(runDate).toISOString() };
|
|
262
|
+
} else {
|
|
263
|
+
const cronExpr = document.getElementById('cronExpression').value.trim();
|
|
264
|
+
if (!cronExpr) {
|
|
265
|
+
showToast('Please enter a cron expression', 'error');
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
scheduleConfig = { cron_expression: cronExpr };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const data = {
|
|
272
|
+
name: document.getElementById('actionName').value.trim(),
|
|
273
|
+
description: document.getElementById('actionDescription').value.trim(),
|
|
274
|
+
action_prompt: document.getElementById('actionPrompt').value.trim(),
|
|
275
|
+
model_id: document.getElementById('actionModel').value,
|
|
276
|
+
schedule_type: scheduleType,
|
|
277
|
+
schedule_config: scheduleConfig,
|
|
278
|
+
context_mode: document.getElementById('contextMode').value,
|
|
279
|
+
max_failures: parseInt(document.getElementById('maxFailures').value),
|
|
280
|
+
tool_permissions: getToolPermissions()
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
// Validate required fields
|
|
284
|
+
if (!data.name || !data.description || !data.action_prompt || !data.model_id) {
|
|
285
|
+
showToast('Please fill in all required fields', 'error');
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
const url = isEdit ? `/api/actions/${actionId}` : '/api/actions';
|
|
291
|
+
const method = isEdit ? 'PUT' : 'POST';
|
|
292
|
+
|
|
293
|
+
const response = await fetch(url, {
|
|
294
|
+
method: method,
|
|
295
|
+
headers: { 'Content-Type': 'application/json' },
|
|
296
|
+
body: JSON.stringify(data)
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
if (!response.ok) {
|
|
300
|
+
const error = await response.json();
|
|
301
|
+
throw new Error(error.detail || 'Failed to save action');
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
showToast(isEdit ? 'Action updated successfully' : 'Action created successfully', 'success');
|
|
305
|
+
|
|
306
|
+
// Close modal and reload
|
|
307
|
+
bootstrap.Modal.getInstance(document.getElementById('actionModal')).hide();
|
|
308
|
+
loadActions();
|
|
309
|
+
checkFailedActions();
|
|
310
|
+
|
|
311
|
+
} catch (error) {
|
|
312
|
+
console.error('Error saving action:', error);
|
|
313
|
+
showToast(error.message, 'error');
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* View action details
|
|
319
|
+
*/
|
|
320
|
+
async function viewAction(actionId) {
|
|
321
|
+
try {
|
|
322
|
+
const response = await fetch(`/api/actions/${actionId}`);
|
|
323
|
+
if (!response.ok) throw new Error('Failed to load action');
|
|
324
|
+
|
|
325
|
+
const action = await response.json();
|
|
326
|
+
|
|
327
|
+
const body = document.getElementById('viewActionBody');
|
|
328
|
+
body.innerHTML = `
|
|
329
|
+
<div class="row">
|
|
330
|
+
<div class="col-md-6">
|
|
331
|
+
<h6>Name</h6>
|
|
332
|
+
<p>${escapeHtml(action.name)}</p>
|
|
333
|
+
|
|
334
|
+
<h6>Description</h6>
|
|
335
|
+
<p>${escapeHtml(action.description)}</p>
|
|
336
|
+
|
|
337
|
+
<h6>Model</h6>
|
|
338
|
+
<p><code>${escapeHtml(action.model_id)}</code></p>
|
|
339
|
+
|
|
340
|
+
<h6>Schedule</h6>
|
|
341
|
+
<p>${formatSchedule(action)}</p>
|
|
342
|
+
</div>
|
|
343
|
+
<div class="col-md-6">
|
|
344
|
+
<h6>Context Mode</h6>
|
|
345
|
+
<p><span class="badge ${action.context_mode === 'cumulative' ? 'bg-info' : 'bg-secondary'}">${action.context_mode}</span></p>
|
|
346
|
+
|
|
347
|
+
<h6>Status</h6>
|
|
348
|
+
<p>${action.is_enabled ? '<span class="badge bg-success">Enabled</span>' : '<span class="badge bg-danger">Disabled</span>'}</p>
|
|
349
|
+
|
|
350
|
+
<h6>Failures</h6>
|
|
351
|
+
<p>${action.failure_count} / ${action.max_failures}</p>
|
|
352
|
+
|
|
353
|
+
<h6>Last Run</h6>
|
|
354
|
+
<p>${action.last_run_at ? formatTimestamp(action.last_run_at) : 'Never'}</p>
|
|
355
|
+
|
|
356
|
+
<h6>Next Run</h6>
|
|
357
|
+
<p>${action.next_run_at ? formatTimestamp(action.next_run_at) : 'Not scheduled'}</p>
|
|
358
|
+
</div>
|
|
359
|
+
</div>
|
|
360
|
+
|
|
361
|
+
<h6>Action Prompt</h6>
|
|
362
|
+
<pre class="bg-dark p-3 rounded">${escapeHtml(action.action_prompt)}</pre>
|
|
363
|
+
|
|
364
|
+
${action.tool_permissions && action.tool_permissions.length > 0 ? `
|
|
365
|
+
<h6>Tool Permissions</h6>
|
|
366
|
+
<ul class="list-unstyled">
|
|
367
|
+
${action.tool_permissions.map(tp => `
|
|
368
|
+
<li>
|
|
369
|
+
<i class="bi ${tp.permission_state === 'allowed' ? 'bi-check-circle text-success' : 'bi-x-circle text-danger'}"></i>
|
|
370
|
+
${escapeHtml(tp.tool_name)}
|
|
371
|
+
${tp.server_name ? `<small class="text-muted">(${escapeHtml(tp.server_name)})</small>` : ''}
|
|
372
|
+
</li>
|
|
373
|
+
`).join('')}
|
|
374
|
+
</ul>
|
|
375
|
+
` : ''}
|
|
376
|
+
`;
|
|
377
|
+
|
|
378
|
+
const modal = new bootstrap.Modal(document.getElementById('viewActionModal'));
|
|
379
|
+
modal.show();
|
|
380
|
+
|
|
381
|
+
} catch (error) {
|
|
382
|
+
console.error('Error viewing action:', error);
|
|
383
|
+
showToast('Failed to load action details', 'error');
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Enable an action
|
|
389
|
+
*/
|
|
390
|
+
async function enableAction(actionId) {
|
|
391
|
+
try {
|
|
392
|
+
const response = await fetch(`/api/actions/${actionId}/enable`, { method: 'POST' });
|
|
393
|
+
if (!response.ok) throw new Error('Failed to enable action');
|
|
394
|
+
|
|
395
|
+
showToast('Action enabled', 'success');
|
|
396
|
+
loadActions();
|
|
397
|
+
checkFailedActions();
|
|
398
|
+
|
|
399
|
+
} catch (error) {
|
|
400
|
+
console.error('Error enabling action:', error);
|
|
401
|
+
showToast('Failed to enable action', 'error');
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Disable an action
|
|
407
|
+
*/
|
|
408
|
+
async function disableAction(actionId) {
|
|
409
|
+
try {
|
|
410
|
+
const response = await fetch(`/api/actions/${actionId}/disable`, { method: 'POST' });
|
|
411
|
+
if (!response.ok) throw new Error('Failed to disable action');
|
|
412
|
+
|
|
413
|
+
showToast('Action disabled', 'success');
|
|
414
|
+
loadActions();
|
|
415
|
+
|
|
416
|
+
} catch (error) {
|
|
417
|
+
console.error('Error disabling action:', error);
|
|
418
|
+
showToast('Failed to disable action', 'error');
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Run action now (manual trigger)
|
|
424
|
+
*/
|
|
425
|
+
async function runActionNow(actionId) {
|
|
426
|
+
try {
|
|
427
|
+
showToast('Running action...', 'info');
|
|
428
|
+
|
|
429
|
+
const response = await fetch(`/api/actions/${actionId}/run-now`, { method: 'POST' });
|
|
430
|
+
if (!response.ok) throw new Error('Failed to trigger action');
|
|
431
|
+
|
|
432
|
+
showToast('Action triggered successfully', 'success');
|
|
433
|
+
|
|
434
|
+
// Reload after a short delay to show the new run
|
|
435
|
+
setTimeout(() => {
|
|
436
|
+
loadActions();
|
|
437
|
+
loadRecentRuns();
|
|
438
|
+
}, 2000);
|
|
439
|
+
|
|
440
|
+
} catch (error) {
|
|
441
|
+
console.error('Error running action:', error);
|
|
442
|
+
showToast('Failed to trigger action', 'error');
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Show delete confirmation
|
|
448
|
+
*/
|
|
449
|
+
function confirmDeleteAction(actionId, actionName) {
|
|
450
|
+
currentActionId = actionId;
|
|
451
|
+
document.getElementById('deleteActionName').textContent = actionName;
|
|
452
|
+
|
|
453
|
+
document.getElementById('confirmDeleteBtn').onclick = () => deleteAction(actionId);
|
|
454
|
+
|
|
455
|
+
const modal = new bootstrap.Modal(document.getElementById('confirmDeleteModal'));
|
|
456
|
+
modal.show();
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Delete an action
|
|
461
|
+
*/
|
|
462
|
+
async function deleteAction(actionId) {
|
|
463
|
+
try {
|
|
464
|
+
const response = await fetch(`/api/actions/${actionId}`, { method: 'DELETE' });
|
|
465
|
+
if (!response.ok) throw new Error('Failed to delete action');
|
|
466
|
+
|
|
467
|
+
showToast('Action deleted', 'success');
|
|
468
|
+
|
|
469
|
+
bootstrap.Modal.getInstance(document.getElementById('confirmDeleteModal')).hide();
|
|
470
|
+
loadActions();
|
|
471
|
+
loadRecentRuns();
|
|
472
|
+
checkFailedActions();
|
|
473
|
+
|
|
474
|
+
} catch (error) {
|
|
475
|
+
console.error('Error deleting action:', error);
|
|
476
|
+
showToast('Failed to delete action', 'error');
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// =============================================================================
|
|
481
|
+
// RUNS MANAGEMENT
|
|
482
|
+
// =============================================================================
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* View runs for a specific action
|
|
486
|
+
*/
|
|
487
|
+
async function viewActionRuns(actionId, actionName) {
|
|
488
|
+
currentActionId = actionId;
|
|
489
|
+
document.getElementById('actionRunsTitle').textContent = `Runs: ${actionName}`;
|
|
490
|
+
|
|
491
|
+
const tbody = document.getElementById('actionRunsTbody');
|
|
492
|
+
tbody.innerHTML = '<tr><td colspan="6" class="text-center"><div class="spinner-border spinner-border-sm"></div> Loading...</td></tr>';
|
|
493
|
+
|
|
494
|
+
const modal = new bootstrap.Modal(document.getElementById('actionRunsModal'));
|
|
495
|
+
modal.show();
|
|
496
|
+
|
|
497
|
+
try {
|
|
498
|
+
const response = await fetch(`/api/actions/${actionId}/runs?limit=50`);
|
|
499
|
+
if (!response.ok) throw new Error('Failed to load runs');
|
|
500
|
+
|
|
501
|
+
const runs = await response.json();
|
|
502
|
+
|
|
503
|
+
if (runs.length === 0) {
|
|
504
|
+
tbody.innerHTML = '<tr><td colspan="6" class="text-center text-muted">No runs yet</td></tr>';
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
tbody.innerHTML = runs.map(run => `
|
|
509
|
+
<tr>
|
|
510
|
+
<td>${run.id}</td>
|
|
511
|
+
<td>${formatTimestamp(run.started_at)}</td>
|
|
512
|
+
<td>${run.completed_at ? formatTimestamp(run.completed_at) : '-'}</td>
|
|
513
|
+
<td>${formatRunStatus(run.status)}</td>
|
|
514
|
+
<td>${formatTokens(run.input_tokens, run.output_tokens)}</td>
|
|
515
|
+
<td>
|
|
516
|
+
<button class="btn btn-sm btn-outline-secondary" onclick="viewRunDetails(${actionId}, ${run.id})">
|
|
517
|
+
<i class="bi bi-eye"></i> View
|
|
518
|
+
</button>
|
|
519
|
+
</td>
|
|
520
|
+
</tr>
|
|
521
|
+
`).join('');
|
|
522
|
+
|
|
523
|
+
} catch (error) {
|
|
524
|
+
console.error('Error loading runs:', error);
|
|
525
|
+
tbody.innerHTML = '<tr><td colspan="6" class="text-center text-danger">Failed to load runs</td></tr>';
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* View details of a specific run
|
|
531
|
+
*/
|
|
532
|
+
async function viewRunDetails(actionId, runId) {
|
|
533
|
+
currentActionId = actionId;
|
|
534
|
+
currentRunId = runId;
|
|
535
|
+
|
|
536
|
+
const body = document.getElementById('runDetailsBody');
|
|
537
|
+
body.innerHTML = '<div class="text-center"><div class="spinner-border"></div> Loading...</div>';
|
|
538
|
+
|
|
539
|
+
const modal = new bootstrap.Modal(document.getElementById('runDetailsModal'));
|
|
540
|
+
modal.show();
|
|
541
|
+
|
|
542
|
+
try {
|
|
543
|
+
const response = await fetch(`/api/actions/${actionId}/runs/${runId}`);
|
|
544
|
+
if (!response.ok) throw new Error('Failed to load run details');
|
|
545
|
+
|
|
546
|
+
const run = await response.json();
|
|
547
|
+
|
|
548
|
+
body.innerHTML = `
|
|
549
|
+
<div class="row mb-3">
|
|
550
|
+
<div class="col-md-3">
|
|
551
|
+
<h6>Status</h6>
|
|
552
|
+
<p>${formatRunStatus(run.status)}</p>
|
|
553
|
+
</div>
|
|
554
|
+
<div class="col-md-3">
|
|
555
|
+
<h6>Started</h6>
|
|
556
|
+
<p>${formatTimestamp(run.started_at)}</p>
|
|
557
|
+
</div>
|
|
558
|
+
<div class="col-md-3">
|
|
559
|
+
<h6>Completed</h6>
|
|
560
|
+
<p>${run.completed_at ? formatTimestamp(run.completed_at) : '-'}</p>
|
|
561
|
+
</div>
|
|
562
|
+
<div class="col-md-3">
|
|
563
|
+
<h6>Tokens</h6>
|
|
564
|
+
<p>${formatTokens(run.input_tokens, run.output_tokens)}</p>
|
|
565
|
+
</div>
|
|
566
|
+
</div>
|
|
567
|
+
|
|
568
|
+
${run.error_message ? `
|
|
569
|
+
<div class="alert alert-danger">
|
|
570
|
+
<h6><i class="bi bi-exclamation-triangle"></i> Error</h6>
|
|
571
|
+
<pre class="mb-0">${escapeHtml(run.error_message)}</pre>
|
|
572
|
+
</div>
|
|
573
|
+
` : ''}
|
|
574
|
+
|
|
575
|
+
<h6>Result</h6>
|
|
576
|
+
<div class="card">
|
|
577
|
+
<div class="card-body markdown-content" style="max-height: 400px; overflow-y: auto;">
|
|
578
|
+
${run.result_html || (run.result_text ? `<pre>${escapeHtml(run.result_text)}</pre>` : '<p class="text-muted">No result</p>')}
|
|
579
|
+
</div>
|
|
580
|
+
</div>
|
|
581
|
+
`;
|
|
582
|
+
|
|
583
|
+
} catch (error) {
|
|
584
|
+
console.error('Error loading run details:', error);
|
|
585
|
+
body.innerHTML = '<div class="alert alert-danger">Failed to load run details</div>';
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Export run result
|
|
591
|
+
*/
|
|
592
|
+
async function exportRun(format) {
|
|
593
|
+
if (!currentActionId || !currentRunId) return;
|
|
594
|
+
|
|
595
|
+
try {
|
|
596
|
+
const response = await fetch(`/api/actions/${currentActionId}/runs/${currentRunId}/export?format=${format}`);
|
|
597
|
+
if (!response.ok) throw new Error('Failed to export run');
|
|
598
|
+
|
|
599
|
+
const blob = await response.blob();
|
|
600
|
+
const url = URL.createObjectURL(blob);
|
|
601
|
+
const link = document.createElement('a');
|
|
602
|
+
link.href = url;
|
|
603
|
+
link.download = `run_${currentRunId}.${format === 'markdown' ? 'md' : format}`;
|
|
604
|
+
link.click();
|
|
605
|
+
URL.revokeObjectURL(url);
|
|
606
|
+
|
|
607
|
+
showToast('Export downloaded', 'success');
|
|
608
|
+
|
|
609
|
+
} catch (error) {
|
|
610
|
+
console.error('Error exporting run:', error);
|
|
611
|
+
showToast('Failed to export run', 'error');
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// =============================================================================
|
|
616
|
+
// HELPERS AND FORMATTING
|
|
617
|
+
// =============================================================================
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Load available models for dropdown
|
|
621
|
+
*/
|
|
622
|
+
async function loadModels() {
|
|
623
|
+
try {
|
|
624
|
+
const response = await fetch('/api/models');
|
|
625
|
+
if (!response.ok) throw new Error('Failed to load models');
|
|
626
|
+
|
|
627
|
+
availableModels = await response.json();
|
|
628
|
+
|
|
629
|
+
const select = document.getElementById('actionModel');
|
|
630
|
+
select.innerHTML = '<option value="">Select a model...</option>';
|
|
631
|
+
|
|
632
|
+
availableModels.forEach(model => {
|
|
633
|
+
const option = document.createElement('option');
|
|
634
|
+
option.value = model.id;
|
|
635
|
+
option.textContent = `${model.name} (${model.provider})`;
|
|
636
|
+
select.appendChild(option);
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
} catch (error) {
|
|
640
|
+
console.error('Error loading models:', error);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Load available tools for permissions
|
|
646
|
+
*/
|
|
647
|
+
async function loadTools() {
|
|
648
|
+
try {
|
|
649
|
+
const response = await fetch('/api/tools');
|
|
650
|
+
if (!response.ok) throw new Error('Failed to load tools');
|
|
651
|
+
|
|
652
|
+
const data = await response.json();
|
|
653
|
+
availableTools = data.tools || [];
|
|
654
|
+
|
|
655
|
+
resetToolPermissions();
|
|
656
|
+
|
|
657
|
+
} catch (error) {
|
|
658
|
+
console.error('Error loading tools:', error);
|
|
659
|
+
document.getElementById('toolPermissions').innerHTML = '<p class="text-muted">Failed to load tools</p>';
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Reset tool permissions checkboxes
|
|
665
|
+
*/
|
|
666
|
+
function resetToolPermissions() {
|
|
667
|
+
const container = document.getElementById('toolPermissions');
|
|
668
|
+
|
|
669
|
+
if (availableTools.length === 0) {
|
|
670
|
+
container.innerHTML = '<p class="text-muted mb-0">No tools available</p>';
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
container.innerHTML = availableTools.map(tool => `
|
|
675
|
+
<div class="form-check">
|
|
676
|
+
<input class="form-check-input tool-permission" type="checkbox"
|
|
677
|
+
id="tool-${tool.name}" data-tool="${tool.name}" data-server="${tool.server || ''}">
|
|
678
|
+
<label class="form-check-label" for="tool-${tool.name}">
|
|
679
|
+
${escapeHtml(tool.name)}
|
|
680
|
+
${tool.server ? `<small class="text-muted">(${escapeHtml(tool.server)})</small>` : ''}
|
|
681
|
+
</label>
|
|
682
|
+
</div>
|
|
683
|
+
`).join('');
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Set tool permissions from action data
|
|
688
|
+
*/
|
|
689
|
+
function setToolPermissions(permissions) {
|
|
690
|
+
// First reset all
|
|
691
|
+
document.querySelectorAll('.tool-permission').forEach(cb => cb.checked = false);
|
|
692
|
+
|
|
693
|
+
// Then set the allowed ones
|
|
694
|
+
permissions.forEach(perm => {
|
|
695
|
+
if (perm.permission_state === 'allowed') {
|
|
696
|
+
const cb = document.querySelector(`.tool-permission[data-tool="${perm.tool_name}"]`);
|
|
697
|
+
if (cb) cb.checked = true;
|
|
698
|
+
}
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Get tool permissions from checkboxes
|
|
704
|
+
*/
|
|
705
|
+
function getToolPermissions() {
|
|
706
|
+
const permissions = [];
|
|
707
|
+
|
|
708
|
+
document.querySelectorAll('.tool-permission:checked').forEach(cb => {
|
|
709
|
+
permissions.push({
|
|
710
|
+
tool_name: cb.dataset.tool,
|
|
711
|
+
server_name: cb.dataset.server || null,
|
|
712
|
+
permission_state: 'allowed'
|
|
713
|
+
});
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
return permissions;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* Update schedule config visibility
|
|
721
|
+
*/
|
|
722
|
+
function updateScheduleConfig() {
|
|
723
|
+
const scheduleType = document.getElementById('scheduleType').value;
|
|
724
|
+
document.getElementById('oneOffConfig').classList.toggle('d-none', scheduleType !== 'one_off');
|
|
725
|
+
document.getElementById('recurringConfig').classList.toggle('d-none', scheduleType !== 'recurring');
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Format schedule for display
|
|
730
|
+
*/
|
|
731
|
+
function formatSchedule(action) {
|
|
732
|
+
if (action.schedule_type === 'one_off') {
|
|
733
|
+
const date = action.schedule_config?.run_date;
|
|
734
|
+
return date ? `<i class="bi bi-calendar-event"></i> ${formatTimestamp(date)}` : 'One-off';
|
|
735
|
+
} else {
|
|
736
|
+
const cron = action.schedule_config?.cron_expression;
|
|
737
|
+
return cron ? `<i class="bi bi-arrow-repeat"></i> <code>${escapeHtml(cron)}</code>` : 'Recurring';
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* Format failures with colour coding
|
|
743
|
+
*/
|
|
744
|
+
function formatFailures(action) {
|
|
745
|
+
const ratio = action.failure_count / action.max_failures;
|
|
746
|
+
let badgeClass = 'bg-secondary';
|
|
747
|
+
if (ratio >= 1) badgeClass = 'bg-danger';
|
|
748
|
+
else if (ratio >= 0.5) badgeClass = 'bg-warning text-dark';
|
|
749
|
+
|
|
750
|
+
return `<span class="badge ${badgeClass}">${action.failure_count}/${action.max_failures}</span>`;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* Format run status badge
|
|
755
|
+
*/
|
|
756
|
+
function formatRunStatus(status) {
|
|
757
|
+
const badges = {
|
|
758
|
+
'completed': '<span class="badge bg-success">Completed</span>',
|
|
759
|
+
'running': '<span class="badge bg-primary">Running</span>',
|
|
760
|
+
'failed': '<span class="badge bg-danger">Failed</span>'
|
|
761
|
+
};
|
|
762
|
+
return badges[status] || `<span class="badge bg-secondary">${status}</span>`;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* Format duration between two timestamps
|
|
767
|
+
*/
|
|
768
|
+
function formatDuration(start, end) {
|
|
769
|
+
if (!start || !end) return '-';
|
|
770
|
+
|
|
771
|
+
const startDate = new Date(start);
|
|
772
|
+
const endDate = new Date(end);
|
|
773
|
+
const diffMs = endDate - startDate;
|
|
774
|
+
|
|
775
|
+
if (diffMs < 1000) return '<1s';
|
|
776
|
+
if (diffMs < 60000) return `${Math.round(diffMs / 1000)}s`;
|
|
777
|
+
if (diffMs < 3600000) return `${Math.round(diffMs / 60000)}m`;
|
|
778
|
+
return `${Math.round(diffMs / 3600000)}h`;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* Format token counts
|
|
783
|
+
*/
|
|
784
|
+
function formatTokens(input, output) {
|
|
785
|
+
if (!input && !output) return '-';
|
|
786
|
+
return `${(input || 0).toLocaleString()} / ${(output || 0).toLocaleString()}`;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* Format timestamp for display
|
|
791
|
+
*/
|
|
792
|
+
function formatTimestamp(timestamp) {
|
|
793
|
+
if (!timestamp) return '-';
|
|
794
|
+
try {
|
|
795
|
+
const date = new Date(timestamp);
|
|
796
|
+
return date.toLocaleString();
|
|
797
|
+
} catch (e) {
|
|
798
|
+
return timestamp;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* Escape HTML to prevent XSS
|
|
804
|
+
*/
|
|
805
|
+
function escapeHtml(text) {
|
|
806
|
+
if (typeof text !== 'string') return text;
|
|
807
|
+
const div = document.createElement('div');
|
|
808
|
+
div.textContent = text;
|
|
809
|
+
return div.innerHTML;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
|
|
813
|
+
// =============================================================================
|
|
814
|
+
// AI-ASSISTED ACTION CREATION
|
|
815
|
+
// =============================================================================
|
|
816
|
+
|
|
817
|
+
// State for AI creation session
|
|
818
|
+
let aiCreationId = null;
|
|
819
|
+
let aiCreatedActionId = null;
|
|
820
|
+
|
|
821
|
+
/**
|
|
822
|
+
* Show the AI creation modal
|
|
823
|
+
*/
|
|
824
|
+
function showAICreateModal() {
|
|
825
|
+
// Reset form and state
|
|
826
|
+
aiCreationId = null;
|
|
827
|
+
aiCreatedActionId = null;
|
|
828
|
+
document.getElementById('aiActionName').value = '';
|
|
829
|
+
document.getElementById('aiActionDescription').value = '';
|
|
830
|
+
document.getElementById('aiActionModel').value = '';
|
|
831
|
+
document.getElementById('aiChatMessages').innerHTML = '';
|
|
832
|
+
|
|
833
|
+
// Populate model dropdown from the main form's dropdown
|
|
834
|
+
const mainModelSelect = document.getElementById('actionModel');
|
|
835
|
+
const aiModelSelect = document.getElementById('aiActionModel');
|
|
836
|
+
aiModelSelect.innerHTML = mainModelSelect.innerHTML;
|
|
837
|
+
|
|
838
|
+
// Show setup form, hide chat interface
|
|
839
|
+
document.getElementById('aiCreateSetup').classList.remove('d-none');
|
|
840
|
+
document.getElementById('aiCreateChat').classList.add('d-none');
|
|
841
|
+
document.getElementById('aiCreateLoading').classList.add('d-none');
|
|
842
|
+
|
|
843
|
+
// Show footer with cancel button
|
|
844
|
+
document.getElementById('aiCreateFooter').classList.remove('d-none');
|
|
845
|
+
|
|
846
|
+
const modal = new bootstrap.Modal(document.getElementById('aiCreateModal'));
|
|
847
|
+
modal.show();
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/**
|
|
851
|
+
* Start the AI-assisted creation session
|
|
852
|
+
*/
|
|
853
|
+
async function startAICreation() {
|
|
854
|
+
const name = document.getElementById('aiActionName').value.trim();
|
|
855
|
+
const description = document.getElementById('aiActionDescription').value.trim();
|
|
856
|
+
const modelId = document.getElementById('aiActionModel').value;
|
|
857
|
+
|
|
858
|
+
// Validate inputs
|
|
859
|
+
if (!name) {
|
|
860
|
+
showToast('Please enter an action name', 'error');
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
if (!description) {
|
|
864
|
+
showToast('Please enter a description', 'error');
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
if (!modelId) {
|
|
868
|
+
showToast('Please select a model', 'error');
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// Show loading
|
|
873
|
+
document.getElementById('aiCreateSetup').classList.add('d-none');
|
|
874
|
+
document.getElementById('aiCreateLoading').classList.remove('d-none');
|
|
875
|
+
|
|
876
|
+
try {
|
|
877
|
+
const response = await fetch('/api/actions/ai-create/start', {
|
|
878
|
+
method: 'POST',
|
|
879
|
+
headers: { 'Content-Type': 'application/json' },
|
|
880
|
+
body: JSON.stringify({
|
|
881
|
+
name: name,
|
|
882
|
+
description: description,
|
|
883
|
+
model_id: modelId
|
|
884
|
+
})
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
if (!response.ok) {
|
|
888
|
+
const error = await response.json();
|
|
889
|
+
throw new Error(error.detail || 'Failed to start AI creation');
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
const data = await response.json();
|
|
893
|
+
aiCreationId = data.creation_id;
|
|
894
|
+
|
|
895
|
+
// Update chat header
|
|
896
|
+
document.getElementById('aiChatActionName').textContent = name;
|
|
897
|
+
document.getElementById('aiChatModelName').textContent = modelId;
|
|
898
|
+
|
|
899
|
+
// Show chat interface
|
|
900
|
+
document.getElementById('aiCreateLoading').classList.add('d-none');
|
|
901
|
+
document.getElementById('aiCreateChat').classList.remove('d-none');
|
|
902
|
+
|
|
903
|
+
// Add initial AI response to chat
|
|
904
|
+
addAIChatMessage(data.response, 'assistant');
|
|
905
|
+
|
|
906
|
+
// Focus on input
|
|
907
|
+
document.getElementById('aiChatInput').focus();
|
|
908
|
+
|
|
909
|
+
// Check if action was created immediately
|
|
910
|
+
if (data.completed) {
|
|
911
|
+
handleAICreationComplete(data.action_id);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
} catch (error) {
|
|
915
|
+
console.error('Error starting AI creation:', error);
|
|
916
|
+
showToast(error.message, 'error');
|
|
917
|
+
|
|
918
|
+
// Show setup form again
|
|
919
|
+
document.getElementById('aiCreateLoading').classList.add('d-none');
|
|
920
|
+
document.getElementById('aiCreateSetup').classList.remove('d-none');
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/**
|
|
925
|
+
* Send a message in the AI creation chat
|
|
926
|
+
*/
|
|
927
|
+
async function sendAICreationMessage() {
|
|
928
|
+
const input = document.getElementById('aiChatInput');
|
|
929
|
+
const message = input.value.trim();
|
|
930
|
+
|
|
931
|
+
if (!message) return;
|
|
932
|
+
if (!aiCreationId) {
|
|
933
|
+
showToast('No active creation session', 'error');
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// Add user message to chat
|
|
938
|
+
addAIChatMessage(message, 'user');
|
|
939
|
+
input.value = '';
|
|
940
|
+
|
|
941
|
+
// Disable input while waiting
|
|
942
|
+
input.disabled = true;
|
|
943
|
+
document.querySelector('#aiChatInputArea button').disabled = true;
|
|
944
|
+
|
|
945
|
+
// Show typing indicator
|
|
946
|
+
const typingDiv = document.createElement('div');
|
|
947
|
+
typingDiv.id = 'aiTypingIndicator';
|
|
948
|
+
typingDiv.className = 'mb-2 text-muted small';
|
|
949
|
+
typingDiv.innerHTML = '<i class="bi bi-three-dots"></i> AI is thinking...';
|
|
950
|
+
document.getElementById('aiChatMessages').appendChild(typingDiv);
|
|
951
|
+
scrollChatToBottom();
|
|
952
|
+
|
|
953
|
+
try {
|
|
954
|
+
const response = await fetch(`/api/actions/ai-create/${aiCreationId}/message`, {
|
|
955
|
+
method: 'POST',
|
|
956
|
+
headers: { 'Content-Type': 'application/json' },
|
|
957
|
+
body: JSON.stringify({ message: message })
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
// Remove typing indicator
|
|
961
|
+
const indicator = document.getElementById('aiTypingIndicator');
|
|
962
|
+
if (indicator) indicator.remove();
|
|
963
|
+
|
|
964
|
+
if (!response.ok) {
|
|
965
|
+
const error = await response.json();
|
|
966
|
+
throw new Error(error.detail || 'Failed to send message');
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
const data = await response.json();
|
|
970
|
+
|
|
971
|
+
// Add AI response to chat
|
|
972
|
+
if (data.response) {
|
|
973
|
+
addAIChatMessage(data.response, 'assistant');
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// Check if action was created
|
|
977
|
+
if (data.completed) {
|
|
978
|
+
handleAICreationComplete(data.action_id);
|
|
979
|
+
} else {
|
|
980
|
+
// Re-enable input
|
|
981
|
+
input.disabled = false;
|
|
982
|
+
document.querySelector('#aiChatInputArea button').disabled = false;
|
|
983
|
+
input.focus();
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
} catch (error) {
|
|
987
|
+
console.error('Error sending message:', error);
|
|
988
|
+
showToast(error.message, 'error');
|
|
989
|
+
|
|
990
|
+
// Remove typing indicator
|
|
991
|
+
const indicator = document.getElementById('aiTypingIndicator');
|
|
992
|
+
if (indicator) indicator.remove();
|
|
993
|
+
|
|
994
|
+
// Re-enable input
|
|
995
|
+
input.disabled = false;
|
|
996
|
+
document.querySelector('#aiChatInputArea button').disabled = false;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
/**
|
|
1001
|
+
* Handle completion of AI creation
|
|
1002
|
+
*/
|
|
1003
|
+
function handleAICreationComplete(actionId) {
|
|
1004
|
+
aiCreatedActionId = actionId;
|
|
1005
|
+
|
|
1006
|
+
// Update status badge
|
|
1007
|
+
document.getElementById('aiChatStatus').textContent = 'Complete';
|
|
1008
|
+
document.getElementById('aiChatStatus').classList.remove('bg-primary');
|
|
1009
|
+
document.getElementById('aiChatStatus').classList.add('bg-success');
|
|
1010
|
+
|
|
1011
|
+
// Hide input, show completion message
|
|
1012
|
+
document.getElementById('aiChatInputArea').classList.add('d-none');
|
|
1013
|
+
document.getElementById('aiChatComplete').classList.remove('d-none');
|
|
1014
|
+
|
|
1015
|
+
// Update footer
|
|
1016
|
+
document.getElementById('aiCreateFooter').innerHTML = `
|
|
1017
|
+
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
|
1018
|
+
<button type="button" class="btn btn-primary" onclick="viewCreatedAction()">
|
|
1019
|
+
<i class="bi bi-eye"></i> View Action
|
|
1020
|
+
</button>
|
|
1021
|
+
`;
|
|
1022
|
+
|
|
1023
|
+
// Refresh actions list
|
|
1024
|
+
loadActions();
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
/**
|
|
1028
|
+
* Cancel the AI creation session
|
|
1029
|
+
*/
|
|
1030
|
+
async function cancelAICreation() {
|
|
1031
|
+
if (aiCreationId) {
|
|
1032
|
+
try {
|
|
1033
|
+
await fetch(`/api/actions/ai-create/${aiCreationId}`, { method: 'DELETE' });
|
|
1034
|
+
} catch (error) {
|
|
1035
|
+
console.error('Error cancelling creation:', error);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
aiCreationId = null;
|
|
1040
|
+
aiCreatedActionId = null;
|
|
1041
|
+
|
|
1042
|
+
// Close modal
|
|
1043
|
+
const modal = bootstrap.Modal.getInstance(document.getElementById('aiCreateModal'));
|
|
1044
|
+
if (modal) modal.hide();
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
/**
|
|
1048
|
+
* View the action that was just created
|
|
1049
|
+
*/
|
|
1050
|
+
function viewCreatedAction() {
|
|
1051
|
+
if (aiCreatedActionId) {
|
|
1052
|
+
// Close AI modal
|
|
1053
|
+
const modal = bootstrap.Modal.getInstance(document.getElementById('aiCreateModal'));
|
|
1054
|
+
if (modal) modal.hide();
|
|
1055
|
+
|
|
1056
|
+
// View the action
|
|
1057
|
+
viewAction(aiCreatedActionId);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
/**
|
|
1062
|
+
* Add a message to the AI chat
|
|
1063
|
+
*/
|
|
1064
|
+
function addAIChatMessage(text, role) {
|
|
1065
|
+
const container = document.getElementById('aiChatMessages');
|
|
1066
|
+
|
|
1067
|
+
const messageDiv = document.createElement('div');
|
|
1068
|
+
messageDiv.className = `mb-2 p-2 rounded ${role === 'user' ? 'bg-primary text-white ms-5' : 'bg-white border me-5'}`;
|
|
1069
|
+
|
|
1070
|
+
// Simple markdown rendering for AI responses
|
|
1071
|
+
if (role === 'assistant') {
|
|
1072
|
+
// Convert markdown bold and line breaks
|
|
1073
|
+
let html = escapeHtml(text);
|
|
1074
|
+
html = html.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
|
|
1075
|
+
html = html.replace(/\n/g, '<br>');
|
|
1076
|
+
messageDiv.innerHTML = html;
|
|
1077
|
+
} else {
|
|
1078
|
+
messageDiv.textContent = text;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
container.appendChild(messageDiv);
|
|
1082
|
+
scrollChatToBottom();
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
/**
|
|
1086
|
+
* Scroll the chat container to the bottom
|
|
1087
|
+
*/
|
|
1088
|
+
function scrollChatToBottom() {
|
|
1089
|
+
const container = document.getElementById('aiChatMessages');
|
|
1090
|
+
container.scrollTop = container.scrollHeight;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
/**
|
|
1094
|
+
* Handle Enter key press in AI chat input
|
|
1095
|
+
*/
|
|
1096
|
+
function handleAIChatKeypress(event) {
|
|
1097
|
+
if (event.key === 'Enter' && !event.shiftKey) {
|
|
1098
|
+
event.preventDefault();
|
|
1099
|
+
sendAICreationMessage();
|
|
1100
|
+
}
|
|
1101
|
+
}
|