Accept pasted images in inbox Other and Reply answers
This commit is contained in:
+2
-116
@@ -4,6 +4,7 @@ import { markdown } from "./markdown.js";
|
||||
import { previewURL } from "./preview.js";
|
||||
import { findingCount, reviewCounts, severityCounts, unignoredFindings } from "./reviews.js";
|
||||
import { composerText, endSend, flushDrafts, forgetLostImages, lostImageCount, setComposerImages, setComposerText, startSend } from "./drafts.js";
|
||||
import { imageDrafts, imageTray, releaseImages, uploadImages, type PendingImage } from "./images.js";
|
||||
import { currentSession, findTask, focusTab, focusTask, markSummaryRead, on, onEach, pickReview, pinSnapshot, setFocus, showReview, state, transcriptLoading, type Topic } from "./state.js";
|
||||
import { taskStyle } from "./taskcolor.js";
|
||||
import { fillTodoLinks } from "./todolink.js";
|
||||
@@ -771,121 +772,6 @@ function lostImagesNote(key: string): HTMLElement | null {
|
||||
return note;
|
||||
}
|
||||
|
||||
type PendingImage = { file: Blob; url: string };
|
||||
|
||||
const imageDrafts = new Map<string, PendingImage[]>();
|
||||
|
||||
const imageTypes = ["image/png", "image/jpeg", "image/gif", "image/webp"];
|
||||
const maxImageBytes = 5 << 20;
|
||||
|
||||
function imageTray(form: HTMLFormElement, input: HTMLTextAreaElement, key: string): { el: HTMLElement; take: () => PendingImage[]; restore: (imgs: PendingImage[]) => void } {
|
||||
const el = h("div", { class: "compose-images" });
|
||||
const render = () => {
|
||||
const imgs = imageDrafts.get(key) ?? [];
|
||||
setComposerImages(key, imgs.length);
|
||||
el.hidden = imgs.length === 0;
|
||||
replace(
|
||||
el,
|
||||
...imgs.map((img) => {
|
||||
const remove = h("button", { type: "button", class: "remove", title: "Remove image" }, "×");
|
||||
remove.onclick = () => {
|
||||
URL.revokeObjectURL(img.url);
|
||||
const rest = (imageDrafts.get(key) ?? []).filter((i) => i !== img);
|
||||
if (rest.length > 0) {
|
||||
imageDrafts.set(key, rest);
|
||||
} else {
|
||||
imageDrafts.delete(key);
|
||||
}
|
||||
render();
|
||||
};
|
||||
return h("div", { class: "thumb" }, h("img", { src: img.url, alt: "attached image" }), remove);
|
||||
}),
|
||||
);
|
||||
};
|
||||
const add = (files: Iterable<File>) => {
|
||||
const added: PendingImage[] = [];
|
||||
for (const f of files) {
|
||||
if (!imageTypes.includes(f.type)) {
|
||||
void showAlert(`${f.name || "That file"} isn't a PNG, JPEG, GIF or WebP image.`);
|
||||
continue;
|
||||
}
|
||||
if (f.size > maxImageBytes) {
|
||||
void showAlert(`${f.name || "That image"} is over ${maxImageBytes >> 20} MiB.`);
|
||||
continue;
|
||||
}
|
||||
added.push({ file: f, url: URL.createObjectURL(f) });
|
||||
}
|
||||
if (added.length === 0) {
|
||||
return false;
|
||||
}
|
||||
imageDrafts.set(key, [...(imageDrafts.get(key) ?? []), ...added]);
|
||||
render();
|
||||
return true;
|
||||
};
|
||||
|
||||
input.addEventListener("paste", (e) => {
|
||||
const files = [...(e.clipboardData?.files ?? [])].filter((f) => f.type.startsWith("image/"));
|
||||
if (files.length > 0) {
|
||||
e.preventDefault();
|
||||
add(files);
|
||||
}
|
||||
});
|
||||
form.addEventListener("dragover", (e) => {
|
||||
if (e.dataTransfer?.types.includes("Files")) {
|
||||
e.preventDefault();
|
||||
form.classList.add("dropping");
|
||||
}
|
||||
});
|
||||
form.addEventListener("dragleave", (e) => {
|
||||
if (!form.contains(e.relatedTarget as Node | null)) {
|
||||
form.classList.remove("dropping");
|
||||
}
|
||||
});
|
||||
form.addEventListener("drop", (e) => {
|
||||
form.classList.remove("dropping");
|
||||
const files = [...(e.dataTransfer?.files ?? [])];
|
||||
if (files.length > 0) {
|
||||
e.preventDefault();
|
||||
add(files);
|
||||
}
|
||||
});
|
||||
|
||||
render();
|
||||
return {
|
||||
el,
|
||||
take: () => {
|
||||
const imgs = imageDrafts.get(key) ?? [];
|
||||
imageDrafts.delete(key);
|
||||
render();
|
||||
return imgs;
|
||||
},
|
||||
restore: (imgs) => {
|
||||
if (imgs.length > 0) {
|
||||
imageDrafts.set(key, [...imgs, ...(imageDrafts.get(key) ?? [])]);
|
||||
}
|
||||
render();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function uploadImages(imgs: PendingImage[]): Promise<string[] | undefined> {
|
||||
if (imgs.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const ids: string[] = [];
|
||||
for (const img of imgs) {
|
||||
const blob = await post("/api/blobs", { body: img.file });
|
||||
ids.push(blob.id);
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
function releaseImages(imgs: PendingImage[]): void {
|
||||
for (const img of imgs) {
|
||||
URL.revokeObjectURL(img.url);
|
||||
}
|
||||
}
|
||||
|
||||
const modelChoices = new Map<string, string>();
|
||||
|
||||
function vendorOf(model: string): string {
|
||||
@@ -931,7 +817,7 @@ function composeForm(
|
||||
const modelKey = `message:${sessionId ?? key}`;
|
||||
const model = modelSelect(modelKey, currentModel, vendor);
|
||||
const form = h("form", { class: "compose" });
|
||||
const tray = imageTray(form, text, draftKey);
|
||||
const tray = imageTray(form, text, draftKey, (n) => setComposerImages(draftKey, n));
|
||||
form.append(lostImagesNote(draftKey) ?? "", tray.el, text, h("div", { class: "compose-actions" }, model, interrupt, send));
|
||||
form.onsubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
Reference in New Issue
Block a user