summaryrefslogtreecommitdiff
path: root/web/app.js
diff options
context:
space:
mode:
Diffstat (limited to 'web/app.js')
-rw-r--r--web/app.js77
1 files changed, 76 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…';