improve reasoning toggle tests and refactor model capability checks

This commit is contained in:
2026-09-22 10:31:04 +02:00
parent 3b6c8969cc
commit 7602692341
8 changed files with 777 additions and 0 deletions
+106
View File
@@ -0,0 +1,106 @@
export function appendAttachments(container, attachments = []) {
if (!attachments.length) return;
const gallery = document.createElement("div");
gallery.className = "message-images";
for (const item of attachments) {
if (!/^[a-f0-9-]{36}$/.test(item.id)) continue;
const link = document.createElement("a");
link.href = `/attachments/${item.id}`;
link.target = "_blank";
link.rel = "noopener";
const image = document.createElement("img");
image.src = link.href;
image.alt = item.originalName || "Attached image";
image.loading = "lazy";
link.append(image);
gallery.append(link);
}
container.append(gallery);
}
export function createAttachmentComposer({onChange, onError}) {
const button = document.getElementById("attachImage");
const picker = document.getElementById("imagePicker");
const preview = document.getElementById("uploadPreviews");
const composer = document.querySelector(".input-container");
let items = [];
let busy = false;
let limits = {maxSizeMb: 10, maxFiles: 5};
fetch("/uploads/config").then(r => r.ok ? r.json() : limits).then(value => { limits = value; }).catch(() => {});
const render = () => {
preview.replaceChildren();
preview.hidden = !items.length;
for (const item of items) {
const card = document.createElement("div");
card.className = "upload-preview";
const image = document.createElement("img");
image.src = item.preview;
image.alt = item.file.name;
const label = document.createElement("span");
label.textContent = `${item.file.name} — ${item.error || (item.metadata ? "Ready" : "Uploading…")}`;
card.append(image, label);
if (item.error) {
const retry = document.createElement("button");
retry.type = "button"; retry.textContent = "Retry"; retry.disabled = busy;
retry.onclick = () => { void upload(item); };
card.append(retry);
}
const remove = document.createElement("button");
remove.type = "button"; remove.textContent = "×"; remove.disabled = busy;
remove.setAttribute("aria-label", `Remove ${item.file.name}`);
remove.onclick = () => {
items = items.filter(v => v !== item);
item.controller?.abort(); URL.revokeObjectURL(item.preview);
if (item.metadata) void fetch(`/attachments/${item.metadata.id}`, {method: "DELETE"});
render();
};
card.append(remove); preview.append(card);
}
onChange();
};
const upload = async item => {
item.error = null;
item.controller = new AbortController();
render();
try {
const form = new FormData(); form.append("image", item.file);
const response = await fetch("/uploads", {method: "POST", body: form, signal: item.controller.signal});
const result = await response.json();
if (!response.ok) throw new Error(result.error || "Upload failed");
item.metadata = result;
if (!items.includes(item)) void fetch(`/attachments/${result.id}`, {method: "DELETE"});
} catch (error) {
if (error.name !== "AbortError") item.error = error.message || "Upload failed";
}
render();
};
const add = files => {
if (busy) return;
for (const file of files) {
if (items.length >= limits.maxFiles) { onError(`Attach at most ${limits.maxFiles} images`); break; }
if (!["image/png", "image/jpeg", "image/webp"].includes(file.type)) { onError("Use PNG, JPEG or WebP images"); continue; }
if (!file.size || file.size > limits.maxSizeMb * 1024 * 1024) { onError(`Images must be non-empty and at most ${limits.maxSizeMb} MB`); continue; }
const item = {file, preview: URL.createObjectURL(file)};
items.push(item); void upload(item);
}
};
button.onclick = () => picker.click();
picker.onchange = () => { add(picker.files); picker.value = ""; };
composer.addEventListener("dragover", event => {
if (event.dataTransfer.types.includes("Files")) { event.preventDefault(); composer.classList.add("image-drag-target"); }
});
composer.addEventListener("dragleave", () => composer.classList.remove("image-drag-target"));
composer.addEventListener("drop", event => {
event.preventDefault(); composer.classList.remove("image-drag-target"); add(event.dataTransfer.files);
});
composer.addEventListener("paste", event => {
const images = [...(event.clipboardData?.files || [])].filter(file => file.type.startsWith("image/"));
if (images.length) { if (!event.clipboardData.getData("text/plain")) event.preventDefault(); add(images); }
});
return {
get attachments() { return items.filter(v => v.metadata).map(v => v.metadata); },
get pending() { return items.some(v => !v.metadata); },
setBusy(value) { busy = value; button.disabled = value; picker.disabled = value; render(); },
clear() { items.forEach(v => URL.revokeObjectURL(v.preview)); items = []; render(); }
};
}