From b319dce942038319699237f3a146affe0e650e7a Mon Sep 17 00:00:00 2001 From: Laura Date: Tue, 26 Aug 2025 00:48:46 +0200 Subject: [PATCH] multi-file select --- static/js/chat.js | 57 +++++++++++++++++++++++------------------ static/js/lib.js | 65 +++++++++++++++++++++++++++++++---------------- 2 files changed, 75 insertions(+), 47 deletions(-) diff --git a/static/js/chat.js b/static/js/chat.js index 80153d6..35990e4 100644 --- a/static/js/chat.js +++ b/static/js/chat.js @@ -61,7 +61,7 @@ const notification = make("div", "notification", "off-screen"); - notification.textContent = msg; + notification.textContent = msg instanceof Error ? msg.message : msg; $notifications.appendChild(notification); @@ -84,8 +84,6 @@ notification.remove(); } - window.notify = notify; - function updateTitle() { const title = chatTitle || (messages.length ? "New Chat" : ""); @@ -1112,7 +1110,7 @@ return; } - notify(err.message); + notify(err); } titleController = null; @@ -1329,6 +1327,8 @@ attachments.splice(index, 1); + storeValue("attachments", attachments); + el.remove(); $attachments.classList.toggle("has-files", !!attachments.length); @@ -1506,34 +1506,34 @@ }); $upload.addEventListener("click", async () => { - const file = await selectFile( + const files = await selectFile( // the ultimate list ".adoc,.bash,.bashrc,.bat,.c,.cc,.cfg,.cjs,.cmd,.conf,.cpp,.cs,.css,.csv,.cxx,.dockerfile,.dockerignore,.editorconfig,.env,.fish,.fs,.fsx,.gitattributes,.gitignore,.go,.gradle,.groovy,.h,.hh,.hpp,.htm,.html,.ini,.ipynb,.java,.jl,.js,.json,.jsonc,.jsx,.kt,.kts,.less,.log,.lua,.m,.makefile,.markdown,.md,.mjs,.mk,.mm,.php,.phtml,.pl,.pm,.profile,.properties,.ps1,.psql,.py,.pyw,.r,.rb,.rs,.rst,.sass,.scala,.scss,.sh,.sql,.svelte,.swift,.t,.toml,.ts,.tsv,.tsx,.txt,.vb,.vue,.xhtml,.xml,.xsd,.xsl,.xslt,.yaml,.yml,.zig,.zsh", - false + true, + file => { + if (!file.name) { + file.name = "unknown.txt"; + } else if (file.name.length > 512) { + throw new Error("File name too long (max 512 characters)"); + } + + if (typeof file.content !== "string") { + throw new Error("File is not a text file"); + } else if (!file.content) { + throw new Error("File is empty"); + } else if (file.content.length > 4 * 1024 * 1024) { + throw new Error("File is too big (max 4MB)"); + } + }, + notify ); - if (!file) { + if (!files.length) { return; } - try { - if (!file.name) { - file.name = "unknown.txt"; - } else if (file.name.length > 512) { - throw new Error("File name too long (max 512 characters)"); - } - - if (typeof file.content !== "string") { - throw new Error("File is not a text file"); - } else if (!file.content) { - throw new Error("File is empty"); - } else if (file.content.length > 4 * 1024 * 1024) { - throw new Error("File is too big (max 4MB)"); - } - + for (const file of files) { pushAttachment(file); - } catch (err) { - notify(err.message); } }); @@ -1579,7 +1579,14 @@ return; } - const file = await selectFile("application/json", true), + const file = await selectFile( + "application/json", + false, + file => { + file.content = JSON.parse(file.content); + }, + notify + ), data = file?.content; if (!data) { diff --git a/static/js/lib.js b/static/js/lib.js index f99bf38..333f494 100644 --- a/static/js/lib.js +++ b/static/js/lib.js @@ -156,46 +156,67 @@ function lines(text) { return count + 1; } -function selectFile(accept, asJson = false) { +function readFile(file, handler, onError = false) { + return new Promise(resolve => { + const reader = new FileReader(); + + reader.onload = () => { + try { + const result = { + name: file.name, + content: reader.result, + }; + + handler(result); + + resolve(result); + } catch (err) { + onError?.(`${file.name}: ${err.message}`); + + resolve(false); + } + }; + + reader.onerror = () => resolve(false); + + reader.readAsText(file); + }); +} + +function selectFile(accept, multiple, handler, onError = false) { return new Promise(resolve => { const input = make("input"); input.type = "file"; input.accept = accept; + input.multiple = multiple; - input.onchange = () => { - const file = input.files[0]; + input.onchange = async () => { + const files = input.files; - if (!file) { + if (!files.length) { resolve(false); return; } - const reader = new FileReader(); + const results = []; - reader.onload = () => { - let content = reader.result; + for (const file of files) { + const result = await readFile(file, handler, onError); - if (asJson) { - try { - content = JSON.parse(content); - } catch { - resolve(false); - - return; - } + if (result) { + results.push(result); } + } - resolve({ - name: file.name, - content: content, - }); - }; + if (!results.length) { + resolve(false); - reader.onerror = () => resolve(false); + return; + } - reader.readAsText(file); + resolve(multiple ? results : results[0]); }; input.click();