Files
DemonGatanjieu-Anomalous_Mo…/web/modules/ui_ai_task_links.js
T
DemonGatanjieuandClaude Opus 5.5 572caf9cb1 Tasks remember the task they go on from
A new task an AI begins while on another, or one the user starts while
looking at a task, keeps it as its "from". Links under the title lead both
ways, the list marks follow-ups, the user can link one by hand or take the
link off (never a loop), and an AI beginning a follow-up hears what was
decided in the task before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 15:46:20 +08:00

98 lines
4.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Where a task stands among others (api/ai_task_links.py), under its title in the AI workbench:
* "goes on from «…»" and "goes on in «…»", each leading to that task; the first has × to take the
* link off. A task that goes on from none has "Goes on from…" to pick the one it continues (a short
* list of the latest tasks; Esc or a click elsewhere closes it).
*/
import { translate as t } from './locales.js';
import { dayLabel, timeLabel } from './activity_log.js';
import { icon } from './ui_ai_icons.js';
import { anomalousAlert } from './ui_dialog.js';
const PICKS = 12;
function el(tag, className, text) {
const node = document.createElement(tag);
if (className) node.className = className;
if (text !== undefined) node.textContent = String(text);
return node;
}
function button(className, children, onClick, title) {
const node = el('button', className);
node.type = 'button';
node.append(...children);
node.onclick = onClick;
if (title) node.title = title;
return node;
}
async function link(taskId, parent) {
const response = await fetch(`/anomalous/ai_tasks/${encodeURIComponent(taskId)}`, {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ from: parent }),
});
const answer = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(answer.error || `HTTP ${response.status}`);
}
/** The list to pick the task `task` goes on from, under `anchor`. */
function pickMenu(anchor, task, tasks, changed) {
anchor.parentElement.querySelector('.anomalous-ai-task-links-menu')?.remove();
const menu = el('div', 'anomalous-ai-task-links-menu');
menu.dataset.anomalousEscape = '1'; // Esc closes this list, not the browser
menu.setAttribute('role', 'listbox');
menu.append(el('div', 'anomalous-ai-task-links-menu-head', t('aiTaskLinkPick')));
const later = new Set((task.links?.next || []).map(item => item.id));
const choices = tasks.filter(item => item.id !== task.id && !later.has(item.id)).slice(0, PICKS);
const close = () => {
menu.remove();
document.removeEventListener('pointerdown', outside, true);
document.removeEventListener('keydown', escape);
};
const outside = (event) => { if (!menu.contains(event.target) && event.target !== anchor) close(); };
const escape = (event) => { if (event.key === 'Escape') setTimeout(close); }; // after the browser's Esc saw the mark
for (const item of choices) {
menu.append(button('anomalous-ai-task-links-pick', [el('span', 'anomalous-ai-task-links-pick-title', item.title || t('aiUntitled')),
el('span', 'anomalous-ai-task-links-pick-when', `${dayLabel(item.updated)} ${timeLabel(item.updated)}`)], async () => {
close();
try {
await link(task.id, item.id);
} catch (error) {
await anomalousAlert(error.message);
}
changed();
}));
}
if (!choices.length) menu.append(el('div', 'anomalous-ai-task-links-menu-head', t('aiTaskLinkNone')));
anchor.after(menu);
setTimeout(() => document.addEventListener('pointerdown', outside, true));
document.addEventListener('keydown', escape);
}
/** The links of `task` (its `links` from the server). `tasks`: the list's summaries; `ctx`: {open(id), changed()}. */
export function taskLinks(task, tasks, ctx) {
const row = el('div', 'anomalous-ai-task-links');
const from = task.links?.from;
if (from) {
const chip = el('span', 'anomalous-ai-task-link is-from');
chip.append(button('anomalous-ai-task-link-go', [icon('undo', 13), el('span', '', t('aiTaskFrom', { title: from.title || t('aiUntitled') }))], () => ctx.open(from.id)),
button('anomalous-ai-task-link-x', [icon('close', 11)], async () => {
try {
await link(task.id, null);
} catch (error) {
await anomalousAlert(error.message);
}
ctx.changed();
}, t('aiTaskUnlink')));
row.append(chip);
} else {
const add = button('anomalous-ai-task-link is-add', [icon('plus', 12), el('span', '', t('aiTaskLink'))], () => pickMenu(add, task, tasks, ctx.changed), t('aiTaskLinkHint'));
row.append(add);
}
for (const item of task.links?.next || []) {
row.append(button('anomalous-ai-task-link is-next', [icon('arrowRight', 13), el('span', '', t('aiTaskNext', { title: item.title || t('aiUntitled') }))], () => ctx.open(item.id)));
}
return row;
}