Accept pasted images in inbox Other and Reply answers

This commit is contained in:
Ian Gulliver
2026-10-16 22:49:06 -07:00
parent a9bec7a7ea
commit ea90178fce
19 changed files with 579 additions and 469 deletions
+32 -20
View File
@@ -3,6 +3,7 @@ import { mountChanges } from "./changes.js";
import { ago, errorText, h, replace, showAlert } from "./dom.js";
import { image, inlineMarkdown, markdown } from "./markdown.js";
import { checkedAnswer, clearAnswer, dropTextAnswer, flushDrafts, hasTextAnswer, setCheckedAnswer, setTextAnswer, textAnswer } from "./drafts.js";
import { imageDrafts, imageTray, releaseImages, uploadImages } from "./images.js";
import { diagram } from "./mermaid.js";
import { focusTab, onEach, state } from "./state.js";
import { taskStyle } from "./taskcolor.js";
@@ -90,13 +91,17 @@ function card(item) {
c.disabled = on;
}
};
const submit = async (response) => {
const imageKey = `inbox:${item.id}`;
const submit = async (response, tray) => {
if (busy()) {
return;
}
lock(true);
const imgs = tray?.take() ?? [];
try {
await post("/api/inbox/responses", { body: { responses: [response] } });
const images = await uploadImages(imgs);
await post("/api/inbox/responses", { body: { responses: [{ ...response, images }] } });
releaseImages(imgs);
clearAnswer(item.id);
const others = state.inbox.some((i) => i.task_id === item.task_id && i.id !== item.id);
if (!others && state.focus.tab === "inbox" && state.selectedTaskId === item.task_id) {
@@ -104,6 +109,7 @@ function card(item) {
}
}
catch (err) {
tray?.restore(imgs);
void showAlert(errorText(err));
lock(false);
renderActions();
@@ -139,10 +145,16 @@ function card(item) {
return h("label", { class: rec ? "option choice-box rec" : "option choice-box" }, h("div", { class: "choice-head" }, input, name, tag));
};
const textEntry = (label, placeholder, shared) => {
const open = hasTextAnswer(item.id);
const open = hasTextAnswer(item.id) || imageDrafts.has(imageKey);
const text = h("textarea", { rows: 3, placeholder, value: textAnswer(item.id) ?? "" });
const send = h("button", { type: "button", class: "primary", disabled: !text.value.trim() }, "Submit");
const editor = h("div", { class: "text-editor", hidden: !open }, text, !shared && h("div", { class: "submit-row" }, send));
const send = h("button", { type: "button", class: "primary" }, "Submit");
const editor = h("div", { class: "text-editor", hidden: !open });
const empty = () => !text.value.trim() && !imageDrafts.has(imageKey);
const tray = imageTray(editor, text, imageKey, () => {
send.disabled = empty();
shared?.onInput();
});
editor.append(tray.el, text, shared ? "" : h("div", { class: "submit-row" }, send));
const block = clickable(h("div", { class: "pick other", role: "button", tabIndex: 0, hidden: open }, label), () => {
setTextAnswer(item.id, "");
block.hidden = true;
@@ -151,21 +163,21 @@ function card(item) {
});
const go = () => {
if (shared) {
shared.onSubmit();
shared.onSubmit(tray);
return;
}
if (text.value.trim()) {
void submit({ item_id: item.id, type: "other", text: text.value });
if (!empty()) {
void submit({ item_id: item.id, type: "other", text: text.value }, tray);
}
};
send.onclick = go;
text.oninput = () => {
setTextAnswer(item.id, text.value);
send.disabled = !text.value.trim();
send.disabled = empty();
shared?.onInput();
};
text.onblur = () => {
if (!text.value.trim() && !busy()) {
if (empty() && !busy()) {
dropTextAnswer(item.id);
editor.hidden = true;
block.hidden = false;
@@ -179,7 +191,7 @@ function card(item) {
go();
}
};
return h("div", { class: "text-entry" }, block, editor);
return { el: h("div", { class: "text-entry" }, block, editor), tray };
};
const delegate = () => {
const button = h("button", { type: "button", class: "delegate" }, "Let the agent decide");
@@ -193,28 +205,28 @@ function card(item) {
const send = h("button", { type: "button", class: "primary" }, "Submit");
const typed = () => (textAnswer(item.id) ?? "").trim();
const refresh = () => {
send.disabled = !typed() && (!multiple || checkedAnswer(item.id).length === 0);
send.disabled = !typed() && !imageDrafts.has(imageKey) && (!multiple || checkedAnswer(item.id).length === 0);
};
const go = () => {
const go = (tray) => {
const options = multiple ? checkedAnswer(item.id) : [];
if (options.length > 0) {
void submit({ item_id: item.id, type: "option", options, text: typed() ? textAnswer(item.id) : undefined });
void submit({ item_id: item.id, type: "option", options, text: typed() ? textAnswer(item.id) : undefined }, tray);
return;
}
if (typed()) {
void submit({ item_id: item.id, type: "other", text: textAnswer(item.id) });
if (typed() || imageDrafts.has(imageKey)) {
void submit({ item_id: item.id, type: "other", text: textAnswer(item.id) ?? "" }, tray);
}
};
const other = textEntry("Other…", placeholder, { onInput: refresh, onSubmit: go });
send.onclick = go;
send.onclick = () => go(other.tray);
refresh();
replace(actions, other, h("div", { class: "submit-row" }, delegate(), send));
replace(actions, other.el, h("div", { class: "submit-row" }, delegate(), send));
return;
}
default: {
const dismiss = h("button", { type: "button" }, "Dismiss");
dismiss.onclick = () => void submit({ item_id: item.id, type: "dismiss" });
replace(actions, h("div", { class: "reply-row" }, textEntry("Reply…", "Reply to the agent"), dismiss));
replace(actions, h("div", { class: "reply-row" }, textEntry("Reply…", "Reply to the agent").el, dismiss));
}
}
};
@@ -251,7 +263,7 @@ function thread(item) {
if (item.responses.length === 0) {
return null;
}
return h("ul", { class: "thread" }, ...item.responses.map((r) => h("li", {}, h("strong", {}, r.user), ` ${r.type.replace("_", " ")}`, r.options ? `: ${r.options.join("; ")}` : "", r.text ? ` — ${r.text}` : "")));
return h("ul", { class: "thread" }, ...item.responses.map((r) => h("li", {}, h("strong", {}, r.user), ` ${r.type.replace("_", " ")}`, r.options ? `: ${r.options.join("; ")}` : "", r.text ? ` — ${r.text}` : "", r.images.length > 0 ? ` (${r.images.length === 1 ? "1 image" : `${r.images.length} images`})` : "")));
}
function attachments(list, taskId) {
if (list.length === 0) {