The user took an option for something to select and then pressed "skip" to confirm; boxes shown open read as fields to fill. - Questions (stage panels, the strip over the message box): options are buttons with an arrow under "One press sends it to the AI" and show the pressed one at once; "Let the AI decide" moves to the question's top right (the panel title for ask_user); a line of one's own is folded as "None of these, I'll say it…". The Gates card folds its reason the same. - A waiting task is one page: the sentence for the AI leads and sticks under the task header while the page scrolls (the workbench measures the header); "Say what to do" and the gallery are optional folds that say how many pictures are in. The sentence has the AI do what the task says, asking only when it says nothing. The Get started / Put ready tabs and their words are gone. - Gates window texts fixed (canvas covers building, lines are over the message box, "Ask me" named as on the button); run_workflow waits up to 55 s by default, so a quick run comes back with its pictures. - The UI map counts words the backend names (label_key) as used. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
131 lines
6.0 KiB
JavaScript
131 lines
6.0 KiB
JavaScript
/**
|
|
* The AI workbench's message box (ui_ai_workbench.js keeps it at the foot of a task's column, in
|
|
* view as the timeline scrolls): where the user writes to the AI on that task. Over the input
|
|
* what the message carries (ui_ai_tray.js: pictures to mark up or leave out, what "@" names);
|
|
* beside it the gallery (ui_ai_gallery_pick.js) and the materials picker, each lit with how many
|
|
* it gave; pictures dropped on it or pasted (ui_ai_drop.js); the input grows with what is typed
|
|
* (Enter sends, Shift+Enter breaks the line), with "@" to point at things and "/" for commands
|
|
* (ui_ai_mention.js). The words and what is attached
|
|
* go to `onSend(text, refs)`, words optional when something is attached; the AI gets them the
|
|
* next time it asks (get_answers), which the hint says, and says to ask in the AI app once the
|
|
* task ended. Made once per task, so half-typed text survives the task's redraws.
|
|
*/
|
|
|
|
import { translate as t } from './locales.js';
|
|
import { mentions } from './ui_ai_mention.js';
|
|
import { galleryPicker } from './ui_ai_gallery_pick.js';
|
|
import { takePictures } from './ui_ai_drop.js';
|
|
import { iconButton } from './ui_ai_icons.js';
|
|
import { noteTyping } from './ui_ai_panel.js';
|
|
|
|
const MAX_REFS = 12; // api/ai_message_refs.py
|
|
const MAX_PICTURES = 8; // a message's pictures, as ai_pictures.py hands them
|
|
const MAX_HEIGHT = 168; // the input grows up to about seven lines, then scrolls
|
|
|
|
function el(tag, className, text) {
|
|
const node = document.createElement(tag);
|
|
if (className) node.className = className;
|
|
if (text !== undefined) node.textContent = text;
|
|
return node;
|
|
}
|
|
|
|
/** A button beside the input, with the number of what it gave on its corner. */
|
|
function sideTool(className, name, label, run) {
|
|
const node = iconButton(`anomalous-ai-message-tool ${className}`, name, label, run, { iconOnly: true, size: 17 });
|
|
const count = el('span', 'anomalous-ai-message-tool-count');
|
|
node.append(count);
|
|
node.setCount = (value) => {
|
|
count.textContent = value ? String(value) : '';
|
|
node.classList.toggle('has-items', value > 0);
|
|
};
|
|
return node;
|
|
}
|
|
|
|
/**
|
|
* The box; `source`: {task(), commands(), mark(image, sent), openMaterials()} for "@", "/", the
|
|
* tray's ✎ and the materials button. With them, `box.refs` attaches things from outside (the
|
|
* "Put ready" page's gallery) and `box.setMaterials(count)` shows how many materials are picked.
|
|
*/
|
|
export function messageBox(onSend, source) {
|
|
const box = el('div', 'anomalous-ai-message');
|
|
const tray = el('div', 'anomalous-ai-tray');
|
|
tray.hidden = true;
|
|
const line = el('div', 'anomalous-ai-message-line');
|
|
const field = el('textarea', 'anomalous-ai-message-input');
|
|
field.rows = 1;
|
|
field.maxLength = 2000;
|
|
field.placeholder = t('aiMessagePlaceholder');
|
|
const note = el('div', 'anomalous-ai-message-hint');
|
|
const send = el('button', 'anomalous-ai-panel-send', t('aiMessageSend'));
|
|
send.type = 'button';
|
|
let at = null; // "@" and "/" (made once the box is in place)
|
|
const fit = () => {
|
|
field.style.height = 'auto';
|
|
field.style.height = `${Math.min(field.scrollHeight, MAX_HEIGHT)}px`;
|
|
};
|
|
send.onclick = async () => {
|
|
const text = field.value.trim();
|
|
const refs = at?.refs() || [];
|
|
if (!text && !refs.length) return;
|
|
send.disabled = true;
|
|
try {
|
|
await onSend(text, refs);
|
|
field.value = '';
|
|
fit();
|
|
at?.clear();
|
|
} catch (error) {
|
|
note.textContent = error.message;
|
|
note.classList.add('is-error');
|
|
} finally {
|
|
send.disabled = false;
|
|
}
|
|
};
|
|
field.addEventListener('keydown', (event) => {
|
|
event.stopPropagation(); // typing here is not the browser's shortcuts
|
|
if (at?.keydown(event)) return;
|
|
if (event.key === 'Enter' && !event.shiftKey && !event.isComposing) {
|
|
event.preventDefault();
|
|
send.click();
|
|
}
|
|
});
|
|
field.addEventListener('input', fit);
|
|
field.addEventListener('input', () => noteTyping(source?.task?.()?.id));
|
|
line.append(field, send);
|
|
box.append(tray, line, note);
|
|
if (source) {
|
|
at = mentions(field, box, source, tray);
|
|
const room = () => Math.min(MAX_PICTURES - at.pictures(), MAX_REFS - at.count());
|
|
const pics = sideTool('anomalous-ai-message-pics', 'image', t('aiPicsButton'), () => picker.toggle());
|
|
pics.setAttribute('aria-expanded', 'false');
|
|
const picker = galleryPicker(box, room, (items) => {
|
|
for (const item of items) at.add(item);
|
|
field.focus();
|
|
}, pics);
|
|
const materials = sideTool('anomalous-ai-message-materials', 'sparkle', t('aiMessageMaterials'), () => source.openMaterials());
|
|
const counted = () => pics.setCount(at.pictures());
|
|
box.addEventListener('anomalous-refs', counted);
|
|
field.addEventListener('input', counted);
|
|
line.prepend(pics, materials);
|
|
takePictures(box, field, { add: item => at.add(item), room, say: (text) => {
|
|
note.textContent = text;
|
|
note.classList.add('is-error');
|
|
} });
|
|
box.refs = { add: item => at.add(item), remove: id => at.remove(id), has: id => at.has(id), room, pictures: () => at.pictures() }; // for ui_ai_ready.js
|
|
box.setMaterials = count => materials.setCount(count);
|
|
}
|
|
box.note = note;
|
|
box.field = field;
|
|
return box;
|
|
}
|
|
|
|
/** The hint for where the task stands (the input's tooltip too: a short window shows the hint only
|
|
* for a failure); quieter while the AI is not working on it. While a task waits for an AI
|
|
* (`status` "waiting"), what is sent waits with it. */
|
|
export function updateMessageBox(box, ended, status, materials = 0) {
|
|
box.classList.toggle('is-quiet', !['live', 'asks', 'waiting'].includes(status));
|
|
box.note.textContent = t(ended ? 'aiMessageEnded' : status === 'waiting' ? 'aiMessageWaiting' : 'aiMessageHint');
|
|
box.note.classList.remove('is-error');
|
|
box.field.title = box.note.textContent;
|
|
box.setMaterials?.(materials);
|
|
}
|