From 90b1509e94d3ba3f82942528590315a6947d3b11 Mon Sep 17 00:00:00 2001 From: Laura Date: Sun, 2 Nov 2025 23:50:43 +0100 Subject: [PATCH] improve attaching to specific messages --- static/css/chat.css | 12 +++ static/css/icons/attach_message.svg | 11 +++ static/js/chat.js | 140 ++++++++++++++++------------ 3 files changed, 101 insertions(+), 62 deletions(-) create mode 100644 static/css/icons/attach_message.svg diff --git a/static/css/chat.css b/static/css/chat.css index 414b18d..f8e9869 100644 --- a/static/css/chat.css +++ b/static/css/chat.css @@ -629,11 +629,13 @@ body:not(.loading) #loading { transition: 150ms; } +.message.editing .options, .message:hover .options { opacity: 1; pointer-events: all; } +.message .attach, .message .collapse { position: relative; margin-right: 14px; @@ -651,6 +653,7 @@ body:not(.loading) #loading { transform: scaleY(-100%); } +.message .attach::after, .message .collapse::after { position: absolute; top: 4px; @@ -659,6 +662,8 @@ body:not(.loading) #loading { .message.errored .options .copy, .message.errored .options .edit, +.message:not(.editing) .options .attach, +.message.editing .options .collapse, .message.waiting .options, .message.reasoning .options, .message.tooling .options, @@ -966,6 +971,7 @@ select { gap: 4px; } +.message .options .attach::after, .message .options .collapse::after, #chat .option+.option::before { content: ""; @@ -996,6 +1002,7 @@ body.loading #version, .message .edit, .message .retry, .message .delete, +.message .attach, .pre-copy, .tool .call .name::after, .tool .call::before, @@ -1044,6 +1051,10 @@ input.invalid { background-image: url(icons/collapse.svg); } +.message .attach { + background-image: url(icons/attach_message.svg); +} + .pre-copy, .message .copy { background-image: url(icons/copy.svg); @@ -1159,6 +1170,7 @@ label[for="reasoning-tokens"] { background-image: url(icons/attach.svg); } +.message .attach.loading, #upload.loading { animation: rotating 1.2s linear infinite; background-image: url(icons/spinner.svg); diff --git a/static/css/icons/attach_message.svg b/static/css/icons/attach_message.svg new file mode 100644 index 0000000..535bb2d --- /dev/null +++ b/static/css/icons/attach_message.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/static/js/chat.js b/static/js/chat.js index 152dc9d..f21ae07 100644 --- a/static/js/chat.js +++ b/static/js/chat.js @@ -69,7 +69,6 @@ chatTitle = false; let searchAvailable = false, - activeMessage = null, isResizing = false, scrollResize = false, isUploading = false, @@ -372,6 +371,19 @@ this.#save(); }); + // attach option + if (this.#role === "user") { + const _attach = make("button", "attach"); + + _attach.title = "Add files to this message"; + + _opts.appendChild(_attach); + + _attach.addEventListener("click", () => { + uploadToMessage(_attach, this); + }); + } + // copy option const _optCopy = make("button", "copy"); @@ -443,6 +455,10 @@ _opts.appendChild(_optEdit); _optEdit.addEventListener("click", () => { + if (this.#_message.classList.contains("collapsed")) { + _optCollapse.click(); + } + this.toggleEdit(); }); @@ -926,8 +942,6 @@ this.#editing = !this.#editing; if (this.#editing) { - activeMessage = this; - this.#_edit.value = this.#text; this.setState("editing"); @@ -936,8 +950,6 @@ this.#_edit.focus(); } else { - activeMessage = null; - this.#text = this.#_edit.value; this.setState(false); @@ -1656,11 +1668,11 @@ return _file; } - function pushAttachment(file) { + function pushAttachment(file, message = false) { file.id = uid(); - if (activeMessage?.isUser()) { - activeMessage.addFile(file); + if (message) { + message.addFile(file); return; } @@ -1717,6 +1729,62 @@ return message; } + async function uploadToMessage(self, message) { + if (isUploading) { + return; + } + + const files = await selectFile( + // the ultimate list + "text/*", + 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 (!file.content) { + throw new Error("File is empty"); + } else if (file.content.includes("\0")) { + throw new Error("File is not a text file"); + } else if (file.content.length > 4 * 1024 * 1024) { + throw new Error("File is too big (max 4MB)"); + } + }, + notify + ); + + if (!files.length) { + return; + } + + isUploading = true; + + self.classList.add("loading"); + + const promises = []; + + for (const file of files) { + promises.push( + resolveTokenCount(file.content).then(tokens => { + file.tokens = tokens; + }) + ); + } + + await Promise.all(promises); + + for (const file of files) { + pushAttachment(file, message); + } + + self.classList.remove("loading"); + + isUploading = false; + } + $total.addEventListener("auxclick", event => { if (event.button !== 1) { return; @@ -1869,60 +1937,8 @@ storeValue("message", $message.value); }); - $upload.addEventListener("click", async () => { - if (isUploading) { - return; - } - - const files = await selectFile( - // the ultimate list - "text/*", - 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 (!file.content) { - throw new Error("File is empty"); - } else if (file.content.includes("\0")) { - throw new Error("File is not a text file"); - } else if (file.content.length > 4 * 1024 * 1024) { - throw new Error("File is too big (max 4MB)"); - } - }, - notify - ); - - if (!files.length) { - return; - } - - isUploading = true; - - $upload.classList.add("loading"); - - const promises = []; - - for (const file of files) { - promises.push( - resolveTokenCount(file.content).then(tokens => { - file.tokens = tokens; - }) - ); - } - - await Promise.all(promises); - - for (const file of files) { - pushAttachment(file); - } - - $upload.classList.remove("loading"); - - isUploading = false; + $upload.addEventListener("click", () => { + uploadToMessage($upload, false); }); $add.addEventListener("click", () => {