A task the AI never names (mostly one asked for in the AI app's own chat, with no begin_task) took the summary of its first step that did something as its title, so the list read "run_workflow" or "new_workflow: anima test". ai_task_title.js shows such a title (title_from "auto", now in the list's summaries and the task links) with the tool in the page's words, "新建了工作流:anima test"; a title the user or the AI gave stays as written, and every page naming a task uses it. Renaming starts from that name. The short set's note said "drawing in the AI app's own chat", which the user read as the picture being drawn in the AI app. It now says it is for telling the AI to draw right in the app's chat, the picture still drawn by ComfyUI, and that a task started with New task cannot use it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
99 lines
4.6 KiB
JavaScript
99 lines
4.6 KiB
JavaScript
/**
|
||
* 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. The ⋯ of a task that goes on from none picks the one it continues (pickTaskLink: 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';
|
||
import { taskTitle } from './ai_task_title.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 the box `place`, over everything: a row of the task list would cut it off. */
|
||
export function pickTaskLink(place, task, tasks, changed) {
|
||
document.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)) 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', taskTitle(item)),
|
||
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')));
|
||
document.body.append(menu);
|
||
const { width, height } = menu.getBoundingClientRect();
|
||
menu.style.left = `${Math.min(Math.max(8, place.right - width), window.innerWidth - width - 8)}px`;
|
||
menu.style.top = `${place.bottom + 4 + height > window.innerHeight - 8 ? Math.max(8, place.top - height - 4) : place.bottom + 4}px`;
|
||
setTimeout(() => document.addEventListener('pointerdown', outside, true));
|
||
document.addEventListener('keydown', escape);
|
||
}
|
||
|
||
/** The links of `task` (its `links` from the server). `ctx`: {open(id), changed()}. */
|
||
export function taskLinks(task, 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: taskTitle(from) }))], () => 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);
|
||
}
|
||
for (const item of task.links?.next || []) {
|
||
row.append(button('anomalous-ai-task-link is-next', [icon('arrowRight', 13), el('span', '', t('aiTaskNext', { title: taskTitle(item) }))], () => ctx.open(item.id)));
|
||
}
|
||
return row;
|
||
}
|