scheduled tasks
This commit is contained in:
parent
0fa627ca6d
commit
937827d08b
17 changed files with 1445 additions and 136 deletions
|
|
@ -17,10 +17,10 @@
|
|||
<div id="app">
|
||||
<aside class="sidebar glass">
|
||||
<header class="sidebar-header">
|
||||
<div class="logo">
|
||||
<a href="#" id="logo-link" class="logo">
|
||||
<span class="logo-icon">▲</span>
|
||||
<h1>Agency</h1>
|
||||
</div>
|
||||
</a>
|
||||
<button id="new-task-btn" class="btn btn-primary">
|
||||
<span>+</span> New Task
|
||||
</button>
|
||||
|
|
@ -34,53 +34,121 @@
|
|||
</aside>
|
||||
|
||||
<main class="main-content">
|
||||
<div id="empty-state" class="empty-state">
|
||||
<div id="empty-state" class="empty-state hidden">
|
||||
<div class="empty-icon">⌘</div>
|
||||
<h2>Ready for a new directive?</h2>
|
||||
<p>Select a task from the sidebar or create a new one to begin.</p>
|
||||
</div>
|
||||
|
||||
<div id="dashboard-view" class="dashboard-view">
|
||||
<header class="dashboard-header">
|
||||
<h2>Recent Activity</h2>
|
||||
<p>Track latest agent executions across all directives.</p>
|
||||
</header>
|
||||
<div class="dashboard-content glass">
|
||||
<table class="activity-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Directive</th>
|
||||
<th>Status</th>
|
||||
<th>Date</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="recent-runs-list">
|
||||
<!-- Recent runs will be injected here -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="task-view" class="task-view hidden">
|
||||
<header class="view-header">
|
||||
<div class="header-main">
|
||||
<div id="view-status" class="status-badge">Running</div>
|
||||
<h2 id="view-goal">Researching local LLM performance trends</h2>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button id="toggle-logs-btn" class="btn btn-ghost">
|
||||
<span>⌨</span> Inspect Logs
|
||||
</button>
|
||||
<button id="edit-task-btn" class="btn btn-ghost">
|
||||
<span>✎</span> Edit Goal
|
||||
</button>
|
||||
<button id="rerun-btn" class="btn btn-ghost">
|
||||
<span>↻</span> Re-run Task
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section id="answer-container" class="answer-container glass hidden">
|
||||
<div class="answer-header">
|
||||
<h3>Final Answer</h3>
|
||||
</div>
|
||||
<div id="answer-output" class="answer-output">
|
||||
<!-- Answer will be injected here -->
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="logs-container glass">
|
||||
<div class="logs-header">
|
||||
<h3>Execution Logs</h3>
|
||||
<div class="logs-actions">
|
||||
<span class="logs-timestamp" id="view-date">Feb 9, 2026</span>
|
||||
<div class="task-content">
|
||||
<aside class="run-history glass">
|
||||
<div class="runs-header">
|
||||
<h3>Run History</h3>
|
||||
</div>
|
||||
<ul id="run-list">
|
||||
<!-- Runs will be injected here -->
|
||||
</ul>
|
||||
</aside>
|
||||
|
||||
<div class="run-details">
|
||||
<section id="answer-container" class="answer-container glass hidden">
|
||||
<div class="answer-header">
|
||||
<h3>Final Answer</h3>
|
||||
</div>
|
||||
<div id="answer-output" class="answer-output">
|
||||
<!-- Answer will be injected here -->
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="logs-container" class="logs-container glass">
|
||||
<div class="logs-header">
|
||||
<h3>Execution Logs</h3>
|
||||
<div class="logs-actions">
|
||||
<span class="logs-timestamp" id="view-date">Feb 9, 2026</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="logs-output" class="logs-output">
|
||||
<!-- Logs will be injected here -->
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div id="logs-output" class="logs-output">
|
||||
<!-- Logs will be injected here -->
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="modal-container" class="modal-container hidden">
|
||||
<div class="modal glass">
|
||||
<h2>New Agent Task</h2>
|
||||
<h2 id="modal-title">New Agent Task</h2>
|
||||
<p>Provide a goal for the agent to execute.</p>
|
||||
<form id="new-task-form">
|
||||
<textarea name="goal"
|
||||
<textarea name="goal" id="goal-textarea"
|
||||
placeholder="e.g. Research the latest trending AI agents and write a report..."
|
||||
required></textarea>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Execution Schedule</label>
|
||||
<div id="schedule-presets" class="preset-group">
|
||||
<button type="button" class="btn btn-preset active" data-cron="">One-time</button>
|
||||
<button type="button" class="btn btn-preset" data-cron="0 */5 * * * *">Every 5m</button>
|
||||
<button type="button" class="btn btn-preset" data-cron="0 0 * * * *">Hourly</button>
|
||||
<button type="button" class="btn btn-preset" data-cron="0 0 0 * * *">Daily</button>
|
||||
</div>
|
||||
|
||||
<div class="custom-cron-toggle">
|
||||
<button type="button" id="toggle-custom-cron" class="btn-link">Manual Cron
|
||||
expression...</button>
|
||||
</div>
|
||||
|
||||
<div id="custom-cron-container" class="hidden">
|
||||
<input type="text" name="cron" id="cron-input"
|
||||
placeholder="e.g. 0 * * * * * (every minute)">
|
||||
<small class="form-help">Uses standard cron syntax (seconds included).</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-actions">
|
||||
<button type="button" class="btn btn-ghost" id="cancel-task">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">Execute Directive</button>
|
||||
<button type="submit" class="btn btn-primary" id="submit-task-btn">Execute
|
||||
Directive</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
32
frontend/package-lock.json
generated
32
frontend/package-lock.json
generated
|
|
@ -9,7 +9,9 @@
|
|||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"lucide-static": "^0.563.0"
|
||||
"dompurify": "^3.3.1",
|
||||
"lucide-static": "^0.563.0",
|
||||
"marked": "^17.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^7.3.1"
|
||||
|
|
@ -814,6 +816,22 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/trusted-types": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz",
|
||||
"integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==",
|
||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||
"optionalDependencies": {
|
||||
"@types/trusted-types": "^2.0.7"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
|
||||
|
|
@ -895,6 +913,18 @@
|
|||
"integrity": "sha512-O7ZlK+VFfc/G5KYwUb9jO60RlPNKV6Dyu+OgQC/QCiO4FMPIK9qrIIkYHHG6PLgLKvmELmf1eKSAso+D/vAvcg==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/marked": {
|
||||
"version": "17.0.1",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-17.0.1.tgz",
|
||||
"integrity": "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"marked": "bin/marked.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@
|
|||
"vite": "^7.3.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"lucide-static": "^0.563.0"
|
||||
"dompurify": "^3.3.1",
|
||||
"lucide-static": "^0.563.0",
|
||||
"marked": "^17.0.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,14 @@
|
|||
import { marked } from 'marked';
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
const API_URL = 'http://localhost:3000';
|
||||
|
||||
const state = {
|
||||
tasks: [],
|
||||
selectedTaskId: null,
|
||||
selectedRunId: null,
|
||||
currentView: 'dashboard', // 'dashboard' or 'task'
|
||||
isEditing: false,
|
||||
};
|
||||
|
||||
// DOM elements
|
||||
|
|
@ -19,17 +25,59 @@ const viewStatusEl = document.getElementById('view-status');
|
|||
const viewDateEl = document.getElementById('view-date');
|
||||
const answerContainerEl = document.getElementById('answer-container');
|
||||
const answerOutputEl = document.getElementById('answer-output');
|
||||
const rerunBtn = document.getElementById('rerun-btn');
|
||||
const runListEl = document.getElementById('run-list');
|
||||
const toggleLogsBtn = document.getElementById('toggle-logs-btn');
|
||||
const logsContainerEl = document.getElementById('logs-container');
|
||||
const dashboardViewEl = document.getElementById('dashboard-view');
|
||||
const recentRunsListEl = document.getElementById('recent-runs-list');
|
||||
const logoLink = document.getElementById('logo-link');
|
||||
const editTaskBtn = document.getElementById('edit-task-btn');
|
||||
const modalTitle = document.getElementById('modal-title');
|
||||
const submitTaskBtn = document.getElementById('submit-task-btn');
|
||||
const goalTextarea = document.getElementById('goal-textarea');
|
||||
const cronInput = document.getElementById('cron-input');
|
||||
const schedulePresets = document.getElementById('schedule-presets');
|
||||
const toggleCustomCronBtn = document.getElementById('toggle-custom-cron');
|
||||
const customCronContainer = document.getElementById('custom-cron-container');
|
||||
const presetBtns = document.querySelectorAll('.btn-preset');
|
||||
|
||||
async function fetchTasks() {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/tasks`);
|
||||
state.tasks = await response.json();
|
||||
const newTasks = await response.json();
|
||||
|
||||
// Check if we should follow the latest run (if we were already watching it)
|
||||
let shouldFollowLatest = false;
|
||||
if (state.selectedTaskId) {
|
||||
const currentTask = state.tasks.find(t => t.id === state.selectedTaskId);
|
||||
if (currentTask && currentTask.runs && currentTask.runs.length > 0) {
|
||||
const latestRunId = currentTask.runs[currentTask.runs.length - 1].id;
|
||||
if (state.selectedRunId === latestRunId) {
|
||||
shouldFollowLatest = true;
|
||||
}
|
||||
} else if (!state.selectedRunId) {
|
||||
shouldFollowLatest = true;
|
||||
}
|
||||
}
|
||||
|
||||
state.tasks = newTasks;
|
||||
renderTaskList();
|
||||
|
||||
// Update current view if a task is selected
|
||||
// If we are on the dashboard, refresh it too
|
||||
if (state.currentView === 'dashboard') {
|
||||
fetchRecentRuns();
|
||||
}
|
||||
|
||||
// If a task is selected, update it
|
||||
if (state.selectedTaskId) {
|
||||
const task = state.tasks.find(t => t.id === state.selectedTaskId);
|
||||
const task = state.tasks.find((t) => t.id === state.selectedTaskId);
|
||||
if (task) {
|
||||
if (shouldFollowLatest && task.runs && task.runs.length > 0) {
|
||||
state.selectedRunId = task.runs[task.runs.length - 1].id;
|
||||
}
|
||||
|
||||
renderRunHistory(task);
|
||||
showTaskView(task);
|
||||
}
|
||||
}
|
||||
|
|
@ -38,19 +86,45 @@ async function fetchTasks() {
|
|||
}
|
||||
}
|
||||
|
||||
async function fetchRecentRuns() {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/runs/recent`);
|
||||
const recentRuns = await response.json();
|
||||
renderDashboard(recentRuns);
|
||||
} catch (error) {
|
||||
console.error('Error fetching recent runs:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function renderTaskList() {
|
||||
taskListEl.innerHTML = state.tasks
|
||||
.map(
|
||||
(task) => `
|
||||
<li class="task-item ${state.selectedTaskId === task.id ? 'active' : ''}" data-id="${task.id}">
|
||||
<div class="task-item-title">${task.goal}</div>
|
||||
<div class="task-item-meta">
|
||||
<span class="status-dot ${task.status}"></span>
|
||||
<span>${new Date(task.created_at).toLocaleDateString()}</span>
|
||||
</div>
|
||||
</li>
|
||||
`
|
||||
)
|
||||
const sortedTasks = [...state.tasks].sort((a, b) => {
|
||||
const aDate = a.runs && a.runs.length > 0
|
||||
? new Date(a.runs[a.runs.length - 1].created_at)
|
||||
: new Date(a.created_at);
|
||||
const bDate = b.runs && b.runs.length > 0
|
||||
? new Date(b.runs[b.runs.length - 1].created_at)
|
||||
: new Date(b.created_at);
|
||||
return bDate - aDate;
|
||||
});
|
||||
|
||||
taskListEl.innerHTML = sortedTasks
|
||||
.map((task) => {
|
||||
const latestRun = task.runs && task.runs.length > 0
|
||||
? task.runs[task.runs.length - 1]
|
||||
: null;
|
||||
const status = latestRun ? latestRun.status : 'pending';
|
||||
const date = latestRun ? new Date(latestRun.created_at) : new Date(task.created_at);
|
||||
|
||||
return `
|
||||
<li class="task-item ${state.selectedTaskId === task.id ? 'active' : ''}" data-id="${task.id}">
|
||||
<div class="task-item-title">${task.goal}</div>
|
||||
<div class="task-item-meta">
|
||||
<span class="status-dot ${status}"></span>
|
||||
<span>${date.toLocaleDateString()} ${date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span>
|
||||
</div>
|
||||
</li>
|
||||
`;
|
||||
})
|
||||
.join('');
|
||||
|
||||
// Add event listeners
|
||||
|
|
@ -61,31 +135,120 @@ function renderTaskList() {
|
|||
});
|
||||
}
|
||||
|
||||
function selectTask(id) {
|
||||
function selectTask(id, runId = null) {
|
||||
state.selectedTaskId = id;
|
||||
state.currentView = 'task';
|
||||
const task = state.tasks.find((t) => t.id === id);
|
||||
if (!task) return;
|
||||
|
||||
if (runId) {
|
||||
state.selectedRunId = runId;
|
||||
} else if (task.runs && task.runs.length > 0) {
|
||||
// Default to latest run if not specified
|
||||
state.selectedRunId = task.runs[task.runs.length - 1].id;
|
||||
} else {
|
||||
state.selectedRunId = null;
|
||||
}
|
||||
|
||||
renderTaskList();
|
||||
renderRunHistory(task);
|
||||
showTaskView(task);
|
||||
}
|
||||
|
||||
function showTaskView(task) {
|
||||
function renderRunHistory(task) {
|
||||
runListEl.innerHTML = task.runs
|
||||
.slice()
|
||||
.reverse()
|
||||
.map(
|
||||
(run, index) => `
|
||||
<li class="run-item ${state.selectedRunId === run.id ? 'active' : ''}" data-id="${run.id}">
|
||||
<span>Run #${task.runs.length - index}</span>
|
||||
<span class="run-item-date">${new Date(run.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span>
|
||||
</li>
|
||||
`
|
||||
)
|
||||
.join('');
|
||||
|
||||
document.querySelectorAll('.run-item').forEach((item) => {
|
||||
item.addEventListener('click', () => {
|
||||
selectTask(task.id, item.dataset.id);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function showDashboard() {
|
||||
state.currentView = 'dashboard';
|
||||
state.selectedTaskId = null;
|
||||
state.selectedRunId = null;
|
||||
|
||||
emptyStateEl.classList.add('hidden');
|
||||
taskViewEl.classList.add('hidden');
|
||||
dashboardViewEl.classList.remove('hidden');
|
||||
|
||||
renderTaskList();
|
||||
fetchRecentRuns();
|
||||
}
|
||||
|
||||
function renderDashboard(recentRuns) {
|
||||
recentRunsListEl.innerHTML = recentRuns
|
||||
.map(
|
||||
(run) => `
|
||||
<tr data-task-id="${run.task_id}" data-run-id="${run.id}">
|
||||
<td>${escapeHtml(run.goal)}</td>
|
||||
<td><span class="status-badge ${run.status}">${run.status}</span></td>
|
||||
<td>${new Date(run.created_at).toLocaleString()}</td>
|
||||
</tr>
|
||||
`
|
||||
)
|
||||
.join('');
|
||||
|
||||
document.querySelectorAll('#recent-runs-list tr').forEach((row) => {
|
||||
row.addEventListener('click', () => {
|
||||
selectTask(row.dataset.taskId, row.dataset.runId);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function showTaskView(task) {
|
||||
state.currentView = 'task';
|
||||
emptyStateEl.classList.add('hidden');
|
||||
dashboardViewEl.classList.add('hidden');
|
||||
taskViewEl.classList.remove('hidden');
|
||||
|
||||
const run = task.runs.find(r => r.id === state.selectedRunId) || task.runs[task.runs.length - 1];
|
||||
|
||||
viewGoalEl.textContent = task.goal;
|
||||
viewStatusEl.textContent = task.status;
|
||||
viewStatusEl.className = `status-badge ${task.status}`;
|
||||
viewDateEl.textContent = new Date(task.created_at).toLocaleDateString(undefined, {
|
||||
|
||||
// Show schedule info if exists
|
||||
const scheduleInfo = task.cron ? `<div class="schedule-badge">🕒 Scheduled: ${task.cron}</div>` : '';
|
||||
const headerMain = document.querySelector('.header-main');
|
||||
const existingBadge = headerMain.querySelector('.schedule-badge');
|
||||
if (existingBadge) existingBadge.remove();
|
||||
if (scheduleInfo) {
|
||||
headerMain.insertAdjacentHTML('beforeend', scheduleInfo);
|
||||
}
|
||||
|
||||
if (!run) {
|
||||
viewStatusEl.textContent = 'No runs';
|
||||
logsOutputEl.innerHTML = '';
|
||||
answerContainerEl.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
viewStatusEl.textContent = run.status;
|
||||
viewStatusEl.className = `status-badge ${run.status}`;
|
||||
viewDateEl.textContent = new Date(run.created_at).toLocaleString(undefined, {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric'
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
|
||||
if (task.answer) {
|
||||
if (run.answer) {
|
||||
answerContainerEl.classList.remove('hidden');
|
||||
answerOutputEl.textContent = task.answer;
|
||||
const rawHtml = marked.parse(run.answer);
|
||||
answerOutputEl.innerHTML = DOMPurify.sanitize(rawHtml);
|
||||
} else {
|
||||
answerContainerEl.classList.add('hidden');
|
||||
}
|
||||
|
|
@ -95,7 +258,7 @@ function showTaskView(task) {
|
|||
const isFirstLoad = logsOutputEl.innerHTML === '';
|
||||
|
||||
// Simple log format
|
||||
logsOutputEl.innerHTML = task.logs
|
||||
logsOutputEl.innerHTML = run.logs
|
||||
.split('\n')
|
||||
.map((line) => `<div class="log-entry">${escapeHtml(line)}</div>`)
|
||||
.join('');
|
||||
|
|
@ -112,31 +275,140 @@ function escapeHtml(text) {
|
|||
}
|
||||
|
||||
// Event Listeners
|
||||
rerunBtn.addEventListener('click', async () => {
|
||||
if (!state.selectedTaskId) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/tasks/${state.selectedTaskId}/runs`, {
|
||||
method: 'POST',
|
||||
});
|
||||
const updatedTask = await response.json();
|
||||
const index = state.tasks.findIndex(t => t.id === updatedTask.id);
|
||||
if (index !== -1) {
|
||||
state.tasks[index] = updatedTask;
|
||||
}
|
||||
selectTask(updatedTask.id);
|
||||
} catch (error) {
|
||||
console.error('Error re-running task:', error);
|
||||
alert('Failed to re-run task.');
|
||||
}
|
||||
});
|
||||
|
||||
newTaskBtn.addEventListener('click', () => {
|
||||
state.isEditing = false;
|
||||
modalTitle.textContent = 'New Agent Task';
|
||||
submitTaskBtn.textContent = 'Execute Directive';
|
||||
goalTextarea.value = '';
|
||||
cronInput.value = '';
|
||||
updateScheduleUI('');
|
||||
modalContainer.classList.remove('hidden');
|
||||
});
|
||||
|
||||
toggleLogsBtn.addEventListener('click', () => {
|
||||
logsContainerEl.classList.toggle('hidden');
|
||||
const isHidden = logsContainerEl.classList.contains('hidden');
|
||||
toggleLogsBtn.classList.toggle('active', !isHidden);
|
||||
toggleLogsBtn.innerHTML = isHidden ? '<span>⌨</span> Inspect Logs' : '<span>✕</span> Hide Logs';
|
||||
});
|
||||
|
||||
editTaskBtn.addEventListener('click', () => {
|
||||
const task = state.tasks.find((t) => t.id === state.selectedTaskId);
|
||||
if (!task) return;
|
||||
|
||||
state.isEditing = true;
|
||||
modalTitle.textContent = 'Edit Directive';
|
||||
submitTaskBtn.textContent = 'Update Directive';
|
||||
goalTextarea.value = task.goal;
|
||||
cronInput.value = task.cron || '';
|
||||
updateScheduleUI(task.cron || '');
|
||||
modalContainer.classList.remove('hidden');
|
||||
});
|
||||
|
||||
cancelTaskBtn.addEventListener('click', () => {
|
||||
modalContainer.classList.add('hidden');
|
||||
state.isEditing = false;
|
||||
});
|
||||
|
||||
logoLink.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
showDashboard();
|
||||
});
|
||||
|
||||
function updateScheduleUI(cron) {
|
||||
let matched = false;
|
||||
presetBtns.forEach(btn => {
|
||||
if (btn.dataset.cron === (cron || '')) {
|
||||
btn.classList.add('active');
|
||||
matched = true;
|
||||
} else {
|
||||
btn.classList.remove('active');
|
||||
}
|
||||
});
|
||||
|
||||
if (matched) {
|
||||
customCronContainer.classList.add('hidden');
|
||||
} else if (cron) {
|
||||
customCronContainer.classList.remove('hidden');
|
||||
} else {
|
||||
customCronContainer.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
presetBtns.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const cron = btn.dataset.cron;
|
||||
cronInput.value = cron;
|
||||
updateScheduleUI(cron);
|
||||
});
|
||||
});
|
||||
|
||||
toggleCustomCronBtn.addEventListener('click', () => {
|
||||
customCronContainer.classList.toggle('hidden');
|
||||
});
|
||||
|
||||
cronInput.addEventListener('input', () => {
|
||||
// If user types manually, update presets active state
|
||||
updateScheduleUI(cronInput.value);
|
||||
});
|
||||
|
||||
newTaskForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(newTaskForm);
|
||||
const goal = formData.get('goal');
|
||||
const cron = formData.get('cron') || null;
|
||||
|
||||
modalContainer.classList.add('hidden');
|
||||
newTaskForm.reset();
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/tasks`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ goal }),
|
||||
});
|
||||
const newTask = await response.json();
|
||||
state.tasks.unshift(newTask);
|
||||
selectTask(newTask.id);
|
||||
let response;
|
||||
if (state.isEditing) {
|
||||
response = await fetch(`${API_URL}/tasks/${state.selectedTaskId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ goal, cron }),
|
||||
});
|
||||
} else {
|
||||
response = await fetch(`${API_URL}/tasks`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ goal, cron }),
|
||||
});
|
||||
}
|
||||
const updatedTask = await response.json();
|
||||
|
||||
if (state.isEditing) {
|
||||
const index = state.tasks.findIndex(t => t.id === updatedTask.id);
|
||||
if (index !== -1) {
|
||||
state.tasks[index] = updatedTask;
|
||||
}
|
||||
} else {
|
||||
state.tasks.unshift(updatedTask);
|
||||
}
|
||||
|
||||
state.isEditing = false;
|
||||
selectTask(updatedTask.id);
|
||||
renderTaskList();
|
||||
} catch (error) {
|
||||
console.error('Error creating task:', error);
|
||||
alert('Failed to execute task. Check console.');
|
||||
|
|
@ -144,6 +416,21 @@ newTaskForm.addEventListener('submit', async (e) => {
|
|||
});
|
||||
|
||||
// Initial load
|
||||
// Initial fetch
|
||||
fetchTasks();
|
||||
// Poll for updates every 5 seconds (simplistic for now)
|
||||
setInterval(fetchTasks, 5000);
|
||||
|
||||
// Auto-refresh every 3 seconds
|
||||
let isPolling = false;
|
||||
async function startAutoRefresh() {
|
||||
setInterval(async () => {
|
||||
if (isPolling) return;
|
||||
isPolling = true;
|
||||
try {
|
||||
await fetchTasks();
|
||||
} finally {
|
||||
isPolling = false;
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
startAutoRefresh();
|
||||
|
|
|
|||
|
|
@ -54,6 +54,80 @@ body {
|
|||
border-bottom: 1px solid var(--glass-border);
|
||||
}
|
||||
|
||||
#logo-link {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
#logo-link:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Dashboard View */
|
||||
.dashboard-view {
|
||||
padding: 48px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.dashboard-header h2 {
|
||||
font-size: 32px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.dashboard-header p {
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.dashboard-content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.activity-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.activity-table th {
|
||||
padding: 16px 24px;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border-bottom: 1px solid var(--glass-border);
|
||||
font-size: 13px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.activity-table td {
|
||||
padding: 16px 24px;
|
||||
border-bottom: 1px solid var(--glass-border);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.activity-table tr {
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.activity-table tr:hover {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
.activity-table .status-badge {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -157,6 +231,9 @@ body {
|
|||
|
||||
.view-header {
|
||||
margin-bottom: 32px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header-main {
|
||||
|
|
@ -165,6 +242,75 @@ body {
|
|||
gap: 16px;
|
||||
}
|
||||
|
||||
.task-content {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.run-history {
|
||||
width: 240px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.runs-header {
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--glass-border);
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
|
||||
.runs-header h3 {
|
||||
font-size: 13px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
#run-list {
|
||||
list-style: none;
|
||||
overflow-y: auto;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.run-item {
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
margin-bottom: 4px;
|
||||
font-size: 13px;
|
||||
transition: var(--transition);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.run-item:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.run-item.active {
|
||||
background: rgba(93, 93, 255, 0.1);
|
||||
color: var(--primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.run-item-date {
|
||||
font-size: 11px;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.run-details {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
overflow: hidden;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 4px 12px;
|
||||
border-radius: 100px;
|
||||
|
|
@ -186,13 +332,28 @@ body {
|
|||
border: 1px solid rgba(81, 207, 102, 0.2);
|
||||
}
|
||||
|
||||
.schedule-badge {
|
||||
font-size: 11px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
padding: 4px 10px;
|
||||
border-radius: 100px;
|
||||
color: var(--text-dim);
|
||||
border: 1px solid var(--glass-border);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.logs-container {
|
||||
flex: 1;
|
||||
min-height: 200px;
|
||||
width: 450px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
transition: transform 0.3s ease, width 0.3s ease, opacity 0.3s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.logs-container.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.logs-header {
|
||||
|
|
@ -226,11 +387,10 @@ body {
|
|||
|
||||
/* Answer Section */
|
||||
.answer-container {
|
||||
margin-bottom: 32px;
|
||||
flex: 1;
|
||||
border-radius: 12px;
|
||||
border-left: 4px solid var(--primary);
|
||||
background: linear-gradient(to right, rgba(93, 93, 255, 0.05), transparent);
|
||||
max-height: 40vh;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -255,10 +415,93 @@ body {
|
|||
|
||||
.answer-output {
|
||||
padding: 24px;
|
||||
font-size: 16px;
|
||||
line-height: 1.7;
|
||||
color: var(--text-main);
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.6;
|
||||
color: var(--text);
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.answer-output h1,
|
||||
.answer-output h2,
|
||||
.answer-output h3 {
|
||||
margin-top: 24px;
|
||||
margin-bottom: 12px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.answer-output h1 {
|
||||
font-size: 24px;
|
||||
border-bottom: 1px solid var(--glass-border);
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.answer-output h2 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.answer-output h3 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.answer-output p {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.answer-output ul,
|
||||
.answer-output ol {
|
||||
margin-bottom: 16px;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.answer-output li {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.answer-output code {
|
||||
font-family: var(--font-mono);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.answer-output pre {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
margin-bottom: 16px;
|
||||
border: 1px solid var(--glass-border);
|
||||
}
|
||||
|
||||
.answer-output pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.answer-output hr {
|
||||
border: none;
|
||||
border-top: 1px solid var(--glass-border);
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
.answer-output a {
|
||||
color: var(--primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.answer-output a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.answer-output blockquote {
|
||||
border-left: 4px solid var(--primary);
|
||||
background: rgba(93, 93, 255, 0.05);
|
||||
padding: 12px 20px;
|
||||
margin: 0 0 16px 0;
|
||||
color: var(--text-dim);
|
||||
font-style: italic;
|
||||
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
|
|
@ -344,6 +587,104 @@ textarea:focus {
|
|||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.form-group input[type="text"] {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: 8px;
|
||||
padding: 12px 16px;
|
||||
color: var(--text-main);
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.form-group input[type="text"]:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.form-help {
|
||||
font-size: 11px;
|
||||
color: var(--text-dim);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.preset-group {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.btn-preset {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid var(--glass-border);
|
||||
color: var(--text-dim);
|
||||
padding: 10px;
|
||||
font-size: 13px;
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-preset:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.btn-preset.active {
|
||||
background: rgba(0, 150, 255, 0.1);
|
||||
border-color: var(--primary);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.custom-cron-toggle {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-dim);
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.btn-link:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
#custom-cron-container {
|
||||
margin-top: 8px;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue