summaryrefslogtreecommitdiff
path: root/web
diff options
context:
space:
mode:
Diffstat (limited to 'web')
-rw-r--r--web/app.js77
-rw-r--r--web/style.css44
2 files changed, 120 insertions, 1 deletions
diff --git a/web/app.js b/web/app.js
index 20d7a03..271a18e 100644
--- a/web/app.js
+++ b/web/app.js
@@ -71,7 +71,7 @@ function createTaskCard(task) {
// Footer: action buttons based on state
const RESTART_STATES = new Set(['FAILED', 'TIMED_OUT', 'CANCELLED']);
- if (task.state === 'PENDING' || task.state === 'RUNNING' || task.state === 'READY' || RESTART_STATES.has(task.state)) {
+ if (task.state === 'PENDING' || task.state === 'RUNNING' || task.state === 'READY' || task.state === 'BLOCKED' || RESTART_STATES.has(task.state)) {
const footer = document.createElement('div');
footer.className = 'task-card-footer';
@@ -110,6 +110,8 @@ function createTaskCard(task) {
});
footer.appendChild(acceptBtn);
footer.appendChild(rejectBtn);
+ } else if (task.state === 'BLOCKED') {
+ renderQuestionFooter(task, footer);
} else if (RESTART_STATES.has(task.state)) {
const btn = document.createElement('button');
btn.className = 'btn-restart';
@@ -312,6 +314,79 @@ async function restartTask(taskId) {
return res.json();
}
+function renderQuestionFooter(task, footer) {
+ let question = { text: 'Waiting for your input.', options: [] };
+ if (task.question) {
+ try { question = JSON.parse(task.question); } catch {}
+ }
+
+ const questionEl = document.createElement('p');
+ questionEl.className = 'task-question-text';
+ questionEl.textContent = question.text;
+ footer.appendChild(questionEl);
+
+ if (question.options && question.options.length > 0) {
+ question.options.forEach(opt => {
+ const btn = document.createElement('button');
+ btn.className = 'btn-answer';
+ btn.textContent = opt;
+ btn.addEventListener('click', (e) => {
+ e.stopPropagation();
+ handleAnswer(task.id, opt, footer);
+ });
+ footer.appendChild(btn);
+ });
+ } else {
+ const row = document.createElement('div');
+ row.className = 'task-answer-row';
+ const input = document.createElement('input');
+ input.type = 'text';
+ input.className = 'task-answer-input';
+ input.placeholder = 'Your answer…';
+ const btn = document.createElement('button');
+ btn.className = 'btn-answer';
+ btn.textContent = 'Submit';
+ btn.addEventListener('click', (e) => {
+ e.stopPropagation();
+ if (input.value.trim()) handleAnswer(task.id, input.value.trim(), footer);
+ });
+ input.addEventListener('keydown', (e) => {
+ if (e.key === 'Enter' && input.value.trim()) {
+ e.stopPropagation();
+ handleAnswer(task.id, input.value.trim(), footer);
+ }
+ });
+ row.append(input, btn);
+ footer.appendChild(row);
+ }
+}
+
+async function handleAnswer(taskId, answer, footer) {
+ const btns = footer.querySelectorAll('button, input');
+ btns.forEach(el => { el.disabled = true; });
+ const prev = footer.querySelector('.task-error');
+ if (prev) prev.remove();
+
+ try {
+ const res = await fetch(`${API_BASE}/api/tasks/${taskId}/answer`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ answer }),
+ });
+ if (!res.ok) {
+ const body = await res.json().catch(() => ({}));
+ throw new Error(body.error || `HTTP ${res.status}`);
+ }
+ await poll();
+ } catch (err) {
+ btns.forEach(el => { el.disabled = false; });
+ const errEl = document.createElement('span');
+ errEl.className = 'task-error';
+ errEl.textContent = `Failed: ${err.message}`;
+ footer.appendChild(errEl);
+ }
+}
+
async function handleCancel(taskId, btn, footer) {
btn.disabled = true;
btn.textContent = 'Cancelling…';
diff --git a/web/style.css b/web/style.css
index 1478b36..91466ee 100644
--- a/web/style.css
+++ b/web/style.css
@@ -8,6 +8,7 @@
--state-timed-out: #c084fc;
--state-cancelled: #9ca3af;
--state-budget-exceeded: #fb923c;
+ --state-blocked: #818cf8;
--bg: #0f172a;
--surface: #1e293b;
@@ -181,6 +182,7 @@ main {
.state-badge[data-state="TIMED_OUT"] { background: var(--state-timed-out); }
.state-badge[data-state="CANCELLED"] { background: var(--state-cancelled); }
.state-badge[data-state="BUDGET_EXCEEDED"] { background: var(--state-budget-exceeded); }
+.state-badge[data-state="BLOCKED"] { background: var(--state-blocked); }
/* Task meta */
.task-meta {
@@ -294,6 +296,48 @@ main {
cursor: not-allowed;
}
+.task-question-text {
+ font-size: 0.82rem;
+ color: var(--text);
+ margin: 0 0 0.5rem 0;
+ line-height: 1.4;
+ width: 100%;
+}
+
+.task-answer-row {
+ display: flex;
+ gap: 0.375rem;
+ width: 100%;
+}
+
+.task-answer-input {
+ flex: 1;
+ font-size: 0.8rem;
+ padding: 0.3em 0.6em;
+ border-radius: 0.375rem;
+ border: 1px solid var(--border);
+ background: var(--bg);
+ color: var(--text);
+}
+
+.btn-answer {
+ font-size: 0.8rem;
+ font-weight: 600;
+ padding: 0.35em 0.85em;
+ border-radius: 0.375rem;
+ border: none;
+ cursor: pointer;
+ background: var(--state-blocked);
+ color: #0f172a;
+ transition: opacity 0.15s;
+ margin-right: 0.375rem;
+}
+
+.btn-answer:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
.task-error {
font-size: 0.78rem;
color: var(--state-failed);