scheduled tasks

This commit is contained in:
pavel 2026-02-10 19:32:33 +01:00
commit 937827d08b
17 changed files with 1445 additions and 136 deletions

View file

@ -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>

View file

@ -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",

View file

@ -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"
}
}

View file

@ -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();

View file

@ -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;