342 lines
21 KiB
JavaScript
342 lines
21 KiB
JavaScript
// ==========================================
|
|
// Chat Switchboard — Task Settings (User)
|
|
// ==========================================
|
|
// Settings → Tasks section. CRUD for personal tasks with schedule builder,
|
|
// persona/model picker, budget config, and starter templates.
|
|
// Loaded on the settings surface.
|
|
|
|
|
|
const PRESETS = [
|
|
{ label: 'Every morning (6am)', cron: '0 6 * * *' },
|
|
{ label: 'Weekday mornings (8am)', cron: '0 8 * * 1-5' },
|
|
{ label: 'Every hour', cron: '0 * * * *' },
|
|
{ label: 'Weekly (Monday 9am)', cron: '0 9 * * 1' },
|
|
{ label: 'Monthly (1st midnight)', cron: '0 0 1 * *' },
|
|
{ label: 'Webhook trigger', cron: 'webhook' },
|
|
{ label: 'Custom...', cron: '' },
|
|
];
|
|
|
|
const TEMPLATES = [
|
|
{ name: 'Morning News Digest', prompt: 'Summarize the top 5 tech news headlines from today. Be concise — one paragraph per story.', schedule: '0 6 * * *' },
|
|
{ name: 'Daily Standup Prep', prompt: 'Review my recent notes and conversations. Generate 3 concise standup talking points covering what I worked on yesterday, what I plan today, and any blockers.', schedule: '0 8 * * 1-5' },
|
|
{ name: 'Weekly Project Summary', prompt: 'Summarize this week\'s activity across my projects. Highlight completed items, open threads, and priorities for next week.', schedule: '0 17 * * 5' },
|
|
{ name: 'Stock Watchlist Check', prompt: 'Check for unusual pre-market activity on major tech stocks (AAPL, GOOGL, MSFT, NVDA, AMZN). Flag any moves > 2%.', schedule: '0 9 * * 1-5' },
|
|
{ name: 'Research Digest', prompt: 'Search for the latest papers and blog posts on AI agents and autonomous systems. Summarize the 3 most interesting findings.', schedule: '0 12 * * 1' },
|
|
];
|
|
|
|
function esc(s) { return s ? s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"') : ''; }
|
|
|
|
// Detect browser timezone
|
|
function browserTZ() {
|
|
try { return Intl.DateTimeFormat().resolvedOptions().timeZone; } catch (_) { return 'UTC'; }
|
|
}
|
|
|
|
// ── Task list ────────────────────────────
|
|
async function loadTaskList() {
|
|
var mount = document.getElementById('settingsTasksMount');
|
|
if (!mount) return;
|
|
|
|
try {
|
|
// Load personal tasks
|
|
var resp = await API._get('/api/v1/tasks');
|
|
var personalTasks = (resp.data || []).map(function(t) { t._source = 'personal'; return t; });
|
|
|
|
// Load team tasks from all user's teams
|
|
var teamTasks = [];
|
|
try {
|
|
var teamsResp = await API._get('/api/v1/teams/mine');
|
|
var teams = teamsResp.data || teamsResp || [];
|
|
for (var i = 0; i < teams.length; i++) {
|
|
try {
|
|
var tr = await API._get('/api/v1/teams/' + teams[i].id + '/tasks');
|
|
(tr.data || []).forEach(function(t) {
|
|
t._source = 'team';
|
|
t._teamName = teams[i].name;
|
|
teamTasks.push(t);
|
|
});
|
|
} catch (_) { /* team may not have tasks */ }
|
|
}
|
|
} catch (_) { /* no teams */ }
|
|
|
|
// Deduplicate (user might own a team task)
|
|
var seen = {};
|
|
personalTasks.forEach(function(t) { seen[t.id] = true; });
|
|
teamTasks = teamTasks.filter(function(t) { return !seen[t.id]; });
|
|
|
|
var tasks = personalTasks.concat(teamTasks);
|
|
|
|
var html = '<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px">' +
|
|
'<p style="color:var(--text-2);font-size:13px;margin:0">Your scheduled tasks. Tasks run automatically on their schedule.</p>' +
|
|
'<button class="btn-small btn-primary" id="taskNewBtn">+ New Task</button>' +
|
|
'</div>';
|
|
|
|
if (tasks.length > 0) {
|
|
html += tasks.map(function(t) {
|
|
var status = t.is_active ? '<span class="badge badge-success">active</span>' : '<span class="badge badge-muted">paused</span>';
|
|
var teamBadge = t._source === 'team' ? '<span class="badge" style="background:var(--accent-dim);color:var(--accent)">' + esc(t._teamName || 'team') + '</span>' : '';
|
|
var sched = t.schedule === 'once' ? 'One-shot' : t.schedule === 'webhook' ? 'Webhook trigger' : t.schedule;
|
|
var lastStatus = '';
|
|
if (t.last_run_at) {
|
|
lastStatus = ' \u00b7 last: ' + new Date(t.last_run_at).toLocaleString();
|
|
}
|
|
var nextRun = t.schedule === 'webhook' ? 'on trigger' : t.next_run_at ? new Date(t.next_run_at).toLocaleString() : '\u2014';
|
|
var triggerBtn = '';
|
|
if (t.schedule === 'webhook' && t.trigger_token) {
|
|
triggerBtn = '<button class="btn-small task-trigger-btn" data-token="' + esc(t.trigger_token) + '" title="Copy Trigger URL">\ud83d\udd17</button>';
|
|
}
|
|
|
|
return '<div class="settings-section" style="padding:12px 16px;margin-bottom:8px">' +
|
|
'<div style="display:flex;align-items:center;gap:12px">' +
|
|
'<div style="flex:1">' +
|
|
'<div style="font-weight:600;font-size:14px">' + esc(t.name) + '</div>' +
|
|
'<div style="font-size:11px;color:var(--text-2);margin-top:2px">' + esc(sched) + ' (' + esc(t.timezone) + ')' + lastStatus + '</div>' +
|
|
'<div style="font-size:11px;color:var(--text-2)">next: ' + nextRun + '</div>' +
|
|
'</div>' +
|
|
'<div style="display:flex;gap:6px;align-items:center">' +
|
|
teamBadge + status +
|
|
triggerBtn +
|
|
'<button class="btn-small task-run-btn" data-id="' + t.id + '" title="Run Now">\u25b6</button>' +
|
|
'<button class="btn-small task-toggle-btn" data-id="' + t.id + '" data-active="' + t.is_active + '" title="' + (t.is_active ? 'Pause' : 'Resume') + '">' + (t.is_active ? '\u23f8' : '\u25b6') + '</button>' +
|
|
'<button class="btn-small btn-danger task-del-btn" data-id="' + t.id + '" title="Delete">\u2715</button>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'</div>';
|
|
}).join('');
|
|
}
|
|
|
|
// Templates section
|
|
html += '<div style="margin-top:24px"><h4 style="margin:0 0 8px">Starter Templates</h4>' +
|
|
'<p style="color:var(--text-2);font-size:12px;margin:0 0 12px">Quick-start with a pre-configured task. You can customize after creation.</p>' +
|
|
'<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:8px">';
|
|
TEMPLATES.forEach(function(tmpl) {
|
|
html += '<div class="settings-section" style="padding:10px 12px;cursor:pointer" data-action="_createFromTemplate" data-args=\'' + JSON.stringify([esc(tmpl.name)]) + '\'>' +
|
|
'<div style="font-weight:600;font-size:13px">' + esc(tmpl.name) + '</div>' +
|
|
'<div style="font-size:11px;color:var(--text-2);margin-top:4px">' + esc(tmpl.schedule) + '</div>' +
|
|
'</div>';
|
|
});
|
|
html += '</div></div>';
|
|
|
|
mount.innerHTML = html;
|
|
|
|
// Wire buttons
|
|
document.getElementById('taskNewBtn').addEventListener('click', function() { showCreateForm(); });
|
|
mount.querySelectorAll('.task-run-btn').forEach(function(btn) {
|
|
btn.addEventListener('click', function() { runTask(btn.dataset.id); });
|
|
});
|
|
mount.querySelectorAll('.task-toggle-btn').forEach(function(btn) {
|
|
btn.addEventListener('click', function() { toggleTask(btn.dataset.id, btn.dataset.active === 'true'); });
|
|
});
|
|
mount.querySelectorAll('.task-del-btn').forEach(function(btn) {
|
|
btn.addEventListener('click', function() { deleteTask(btn.dataset.id); });
|
|
});
|
|
mount.querySelectorAll('.task-trigger-btn').forEach(function(btn) {
|
|
btn.addEventListener('click', function() {
|
|
var url = location.origin + (window.__BASE__ || '') + '/api/v1/hooks/t/' + btn.dataset.token;
|
|
navigator.clipboard.writeText(url).then(function() {
|
|
UI.toast('Trigger URL copied', 'success');
|
|
}).catch(function() {
|
|
prompt('Trigger URL:', url);
|
|
});
|
|
});
|
|
});
|
|
} catch (err) {
|
|
mount.innerHTML = '<p style="color:var(--danger)">Failed to load tasks: ' + esc(err.message) + '</p>';
|
|
}
|
|
}
|
|
|
|
// ── Create form ──────────────────────────
|
|
function showCreateForm(defaults) {
|
|
var d = defaults || {};
|
|
var mount = document.getElementById('settingsTasksMount');
|
|
if (!mount) return;
|
|
|
|
var schedOpts = PRESETS.map(function(p) {
|
|
var sel = (d.schedule && d.schedule === p.cron) ? ' selected' : '';
|
|
return '<option value="' + esc(p.cron) + '"' + sel + '>' + esc(p.label) + '</option>';
|
|
}).join('');
|
|
|
|
mount.innerHTML =
|
|
'<div style="margin-bottom:16px"><button class="btn-small" id="taskBackBtn">\u2190 Back</button></div>' +
|
|
'<div class="settings-section" style="padding:16px">' +
|
|
'<div class="form-row" style="gap:12px">' +
|
|
'<div class="form-group" style="flex:2"><label>Name</label>' +
|
|
'<input type="text" id="taskName" value="' + esc(d.name || '') + '" style="width:100%" placeholder="Morning News Digest"></div>' +
|
|
'<div class="form-group" style="flex:1"><label>Model</label>' +
|
|
'<input type="text" id="taskModel" value="' + esc(d.model || '') + '" style="width:100%" placeholder="(use default)"></div>' +
|
|
'</div>' +
|
|
'<div class="form-group" style="margin-top:8px"><label>Prompt</label>' +
|
|
'<textarea id="taskPrompt" rows="4" style="width:100%">' + esc(d.prompt || '') + '</textarea></div>' +
|
|
'<div class="form-row" style="gap:12px;margin-top:8px">' +
|
|
'<div class="form-group" style="flex:1"><label>Schedule</label>' +
|
|
'<select id="taskPreset" style="width:100%">' + schedOpts + '</select></div>' +
|
|
'<div class="form-group" style="flex:1" id="taskCustomCronGroup" style="display:none"><label>Custom Cron</label>' +
|
|
'<input type="text" id="taskCron" value="' + esc(d.schedule || '') + '" style="width:100%" placeholder="0 6 * * *"></div>' +
|
|
'<div class="form-group" style="flex:1"><label>Timezone</label>' +
|
|
'<input type="text" id="taskTZ" value="' + esc(d.timezone || browserTZ()) + '" style="width:100%"></div>' +
|
|
'</div>' +
|
|
'<div class="form-row" style="gap:12px;margin-top:8px">' +
|
|
'<div class="form-group" style="flex:1"><label>Output Mode</label>' +
|
|
'<select id="taskOutput" style="width:100%"><option value="channel">Channel</option><option value="note">Note</option><option value="webhook">Webhook</option></select></div>' +
|
|
'<div class="form-group" style="flex:1" id="taskWebhookGroup" style="display:none"><label>Webhook URL</label>' +
|
|
'<input type="text" id="taskWebhookURL" style="width:100%" placeholder="https://..."></div>' +
|
|
'</div>' +
|
|
'<div class="form-group" style="margin-top:8px" id="taskWebhookSecretGroup" style="display:none"><label>Webhook Secret (HMAC signing)</label>' +
|
|
'<input type="text" id="taskWebhookSecret" style="width:100%" placeholder="auto-generated if empty">' +
|
|
'<p style="font-size:11px;color:var(--text-3);margin:2px 0 0">Used to sign outbound webhook payloads. Leave blank for auto-generated secret.</p>' +
|
|
'</div>' +
|
|
'<div id="taskWebhookTriggerInfo" style="display:none;margin-top:12px;padding:12px;background:var(--bg-raised);border-radius:var(--radius);border:1px solid var(--border)">' +
|
|
'<div style="font-weight:600;font-size:12px;margin-bottom:4px">' +
|
|
'<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="vertical-align:-1px"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>' +
|
|
' Webhook-triggered task</div>' +
|
|
'<p style="font-size:12px;color:var(--text-2);margin:0;line-height:1.5">This task runs when an external service sends a POST request to its trigger URL. ' +
|
|
'The trigger URL will be shown after creation — copy it into your CI, cron, or another task\\u2019s webhook URL for task-to-task chaining.</p>' +
|
|
'</div>' +
|
|
'<div class="form-row" style="gap:12px;margin-top:8px">' +
|
|
'<div class="form-group" style="flex:1"><label>Max Tokens</label>' +
|
|
'<input type="number" id="taskMaxTokens" value="" style="width:100%" placeholder="default"></div>' +
|
|
'<div class="form-group" style="flex:1"><label>Max Tool Calls</label>' +
|
|
'<input type="number" id="taskMaxToolCalls" value="" style="width:100%" placeholder="default"></div>' +
|
|
'</div>' +
|
|
'<div style="display:flex;gap:8px;margin-top:12px;align-items:center">' +
|
|
'<label class="toggle-label"><input type="checkbox" id="taskNotifyComplete"><span class="toggle-track"></span><span>Notify on complete</span></label>' +
|
|
'<label class="toggle-label"><input type="checkbox" id="taskNotifyFail" checked><span class="toggle-track"></span><span>Notify on failure</span></label>' +
|
|
'</div>' +
|
|
'<button class="btn-small btn-primary" id="taskCreateBtn" style="margin-top:12px">Create Task</button>' +
|
|
'</div>';
|
|
|
|
document.getElementById('taskBackBtn').addEventListener('click', loadTaskList);
|
|
document.getElementById('taskCreateBtn').addEventListener('click', createTask);
|
|
|
|
// Toggle custom cron visibility
|
|
var preset = document.getElementById('taskPreset');
|
|
var customGroup = document.getElementById('taskCustomCronGroup');
|
|
var triggerInfo = document.getElementById('taskWebhookTriggerInfo');
|
|
preset.addEventListener('change', function() {
|
|
var isWebhook = preset.value === 'webhook';
|
|
var isCustom = preset.value === '';
|
|
customGroup.style.display = isCustom ? '' : 'none';
|
|
triggerInfo.style.display = isWebhook ? '' : 'none';
|
|
if (!isWebhook && !isCustom) document.getElementById('taskCron').value = preset.value;
|
|
if (isWebhook) document.getElementById('taskCron').value = 'webhook';
|
|
});
|
|
// Initial state
|
|
customGroup.style.display = preset.value === '' ? '' : 'none';
|
|
triggerInfo.style.display = preset.value === 'webhook' ? '' : 'none';
|
|
if (preset.value !== '' && preset.value !== 'webhook') document.getElementById('taskCron').value = preset.value;
|
|
|
|
// Toggle webhook URL and secret visibility
|
|
var output = document.getElementById('taskOutput');
|
|
var webhookGroup = document.getElementById('taskWebhookGroup');
|
|
var webhookSecretGroup = document.getElementById('taskWebhookSecretGroup');
|
|
output.addEventListener('change', function() {
|
|
var isWH = output.value === 'webhook';
|
|
webhookGroup.style.display = isWH ? '' : 'none';
|
|
webhookSecretGroup.style.display = isWH ? '' : 'none';
|
|
});
|
|
}
|
|
|
|
async function createTask() {
|
|
var schedule = document.getElementById('taskCron').value || document.getElementById('taskPreset').value;
|
|
if (!schedule) { UI.toast('Schedule is required', 'error'); return; }
|
|
|
|
var payload = {
|
|
name: document.getElementById('taskName').value,
|
|
task_type: 'prompt',
|
|
user_prompt: document.getElementById('taskPrompt').value,
|
|
schedule: schedule,
|
|
timezone: document.getElementById('taskTZ').value || 'UTC',
|
|
output_mode: document.getElementById('taskOutput').value,
|
|
notify_on_complete: document.getElementById('taskNotifyComplete').checked,
|
|
notify_on_failure: document.getElementById('taskNotifyFail').checked,
|
|
};
|
|
|
|
var model = document.getElementById('taskModel').value;
|
|
if (model) payload.model_id = model;
|
|
|
|
var maxTokens = parseInt(document.getElementById('taskMaxTokens').value);
|
|
if (maxTokens > 0) payload.max_tokens = maxTokens;
|
|
|
|
var maxTools = parseInt(document.getElementById('taskMaxToolCalls').value);
|
|
if (maxTools > 0) payload.max_tool_calls = maxTools;
|
|
|
|
var webhookURL = document.getElementById('taskWebhookURL')?.value;
|
|
if (payload.output_mode === 'webhook' && webhookURL) {
|
|
payload.webhook_url = webhookURL;
|
|
}
|
|
var webhookSecret = document.getElementById('taskWebhookSecret')?.value;
|
|
if (payload.output_mode === 'webhook' && webhookSecret) {
|
|
payload.webhook_secret = webhookSecret;
|
|
}
|
|
|
|
try {
|
|
var created = await API._post('/api/v1/tasks', payload);
|
|
UI.toast('Task created', 'success');
|
|
|
|
// Show trigger URL for webhook-scheduled tasks
|
|
if (created.schedule === 'webhook' && created.trigger_token) {
|
|
var triggerURL = location.origin + (window.__BASE__ || '') + '/api/v1/hooks/t/' + created.trigger_token;
|
|
UI.modal('Webhook Trigger URL',
|
|
'<p style="font-size:12px;color:var(--text-2);margin-bottom:8px">POST to this URL to trigger the task. ' +
|
|
'You can use this in CI pipelines, cron jobs, or as another task\\u2019s webhook URL for chaining.</p>' +
|
|
'<div style="display:flex;gap:8px;align-items:center">' +
|
|
'<input type="text" readonly value="' + esc(triggerURL) + '" style="flex:1;font-family:var(--mono);font-size:11px" id="_triggerURLInput">' +
|
|
'<button class="btn-small btn-primary" id="_copyTriggerBtn">Copy</button>' +
|
|
'</div>' +
|
|
'<p style="font-size:11px;color:var(--text-3);margin-top:8px">The request body is passed to the task as trigger_payload. ' +
|
|
'For task-to-task chaining: set Task A\\u2019s webhook_url to Task B\\u2019s trigger URL.</p>',
|
|
[{ label: 'Done', variant: 'primary' }]
|
|
);
|
|
setTimeout(function() {
|
|
var copyBtn = document.getElementById('_copyTriggerBtn');
|
|
if (copyBtn) copyBtn.addEventListener('click', function() {
|
|
var input = document.getElementById('_triggerURLInput');
|
|
if (input) { navigator.clipboard.writeText(input.value); UI.toast('Copied', 'success'); }
|
|
});
|
|
}, 100);
|
|
}
|
|
|
|
loadTaskList();
|
|
if (typeof TaskSidebar !== 'undefined') TaskSidebar.refresh();
|
|
} catch (err) {
|
|
UI.toast(err.message || 'Failed to create task', 'error');
|
|
}
|
|
}
|
|
|
|
// ── Actions ──────────────────────────────
|
|
async function runTask(id) {
|
|
try {
|
|
await API._post('/api/v1/tasks/' + id + '/run', {});
|
|
UI.toast('Task scheduled for immediate run', 'success');
|
|
} catch (err) { UI.toast(err.message || 'Failed', 'error'); }
|
|
}
|
|
|
|
async function toggleTask(id, isActive) {
|
|
try {
|
|
await API._put('/api/v1/tasks/' + id, { is_active: !isActive });
|
|
UI.toast(isActive ? 'Task paused' : 'Task resumed', 'success');
|
|
loadTaskList();
|
|
if (typeof TaskSidebar !== 'undefined') TaskSidebar.refresh();
|
|
} catch (err) { UI.toast(err.message || 'Failed', 'error'); }
|
|
}
|
|
|
|
async function deleteTask(id) {
|
|
if (!confirm('Delete this task?')) return;
|
|
try {
|
|
await API._delete('/api/v1/tasks/' + id);
|
|
UI.toast('Task deleted', 'success');
|
|
loadTaskList();
|
|
if (typeof TaskSidebar !== 'undefined') TaskSidebar.refresh();
|
|
} catch (err) { UI.toast(err.message || 'Failed', 'error'); }
|
|
}
|
|
|
|
// Template launcher
|
|
sb.register('_createFromTemplate', function(name) {
|
|
var tmpl = TEMPLATES.find(function(t) { return t.name === name; });
|
|
if (tmpl) showCreateForm({ name: tmpl.name, prompt: tmpl.prompt, schedule: tmpl.schedule });
|
|
});
|
|
|
|
// ── Entry point (called from settings loader) ──
|
|
sb.register('_loadSettingsTasks', function() {
|
|
loadTaskList();
|
|
});
|