feat: support multiple files

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
Ettore Di Giacinto 2025-05-30 18:31:30 +02:00
parent 956c4ff660
commit 0532adfada
2 changed files with 98 additions and 78 deletions

View file

@ -48,10 +48,10 @@ function submitSystemPrompt(event) {
document.getElementById("systemPrompt").blur(); document.getElementById("systemPrompt").blur();
} }
var image = ""; var images = [];
var audio = ""; var audios = [];
var fileContent = ""; var fileContents = [];
var currentFileName = ""; var currentFileNames = [];
async function extractTextFromPDF(pdfData) { async function extractTextFromPDF(pdfData) {
try { try {
@ -73,32 +73,34 @@ async function extractTextFromPDF(pdfData) {
} }
function readInputFile() { function readInputFile() {
if (!this.files || !this.files[0]) return; if (!this.files || !this.files.length) return;
const file = this.files[0]; Array.from(this.files).forEach(file => {
const FR = new FileReader(); const FR = new FileReader();
currentFileName = file.name; currentFileNames.push(file.name);
const fileExtension = file.name.split('.').pop().toLowerCase(); const fileExtension = file.name.split('.').pop().toLowerCase();
FR.addEventListener("load", async function(evt) { FR.addEventListener("load", async function(evt) {
if (fileExtension === 'pdf') { if (fileExtension === 'pdf') {
try { try {
fileContent = await extractTextFromPDF(evt.target.result); const content = await extractTextFromPDF(evt.target.result);
} catch (error) { fileContents.push({ name: file.name, content: content });
console.error('Error processing PDF:', error); } catch (error) {
fileContent = "Error processing PDF file"; console.error('Error processing PDF:', error);
fileContents.push({ name: file.name, content: "Error processing PDF file" });
}
} else {
// For text and markdown files
fileContents.push({ name: file.name, content: evt.target.result });
} }
});
if (fileExtension === 'pdf') {
FR.readAsArrayBuffer(file);
} else { } else {
// For text and markdown files FR.readAsText(file);
fileContent = evt.target.result;
} }
}); });
if (fileExtension === 'pdf') {
FR.readAsArrayBuffer(file);
} else {
FR.readAsText(file);
}
} }
function submitPrompt(event) { function submitPrompt(event) {
@ -107,19 +109,25 @@ function submitPrompt(event) {
const input = document.getElementById("input").value; const input = document.getElementById("input").value;
let fullInput = input; let fullInput = input;
// If there's file content, append it to the input for the LLM // If there are file contents, append them to the input for the LLM
if (fileContent) { if (fileContents.length > 0) {
fullInput += "\n\nFile content:\n" + fileContent; fullInput += "\n\nFile contents:\n";
fileContents.forEach(file => {
fullInput += `\n--- ${file.name} ---\n${file.content}\n`;
});
} }
// Show file icon in chat if there's a file // Show file icons in chat if there are files
let displayContent = input; let displayContent = input;
if (currentFileName) { if (currentFileNames.length > 0) {
displayContent += `\n\n<i class="fa-solid fa-file"></i> Attached file: ${currentFileName}`; displayContent += "\n\n";
currentFileNames.forEach(fileName => {
displayContent += `<i class="fa-solid fa-file"></i> Attached file: ${fileName}\n`;
});
} }
// Add the message to the chat UI with just the icon // Add the message to the chat UI with just the icons
Alpine.store("chat").add("user", displayContent, image, audio); Alpine.store("chat").add("user", displayContent, images, audios);
// Update the last message in the store with the full content // Update the last message in the store with the full content
const history = Alpine.store("chat").history; const history = Alpine.store("chat").history;
@ -132,33 +140,37 @@ function submitPrompt(event) {
Alpine.nextTick(() => { document.getElementById('messages').scrollIntoView(false); }); Alpine.nextTick(() => { document.getElementById('messages').scrollIntoView(false); });
promptGPT(systemPrompt, fullInput); promptGPT(systemPrompt, fullInput);
// Reset file content and name after sending // Reset file contents and names after sending
fileContent = ""; fileContents = [];
currentFileName = ""; currentFileNames = [];
} }
function readInputImage() { function readInputImage() {
if (!this.files || !this.files[0]) return; if (!this.files || !this.files.length) return;
const FR = new FileReader(); Array.from(this.files).forEach(file => {
const FR = new FileReader();
FR.addEventListener("load", function(evt) { FR.addEventListener("load", function(evt) {
image = evt.target.result; images.push(evt.target.result);
});
FR.readAsDataURL(file);
}); });
FR.readAsDataURL(this.files[0]);
} }
function readInputAudio() { function readInputAudio() {
if (!this.files || !this.files[0]) return; if (!this.files || !this.files.length) return;
const FR = new FileReader(); Array.from(this.files).forEach(file => {
const FR = new FileReader();
FR.addEventListener("load", function(evt) { FR.addEventListener("load", function(evt) {
audio = evt.target.result; audios.push(evt.target.result);
});
FR.readAsDataURL(file);
}); });
FR.readAsDataURL(this.files[0]);
} }
async function promptGPT(systemPrompt, input) { async function promptGPT(systemPrompt, input) {
@ -177,7 +189,7 @@ async function promptGPT(systemPrompt, input) {
// loop all messages, and check if there are images or audios. If there are, we need to change the content field // loop all messages, and check if there are images or audios. If there are, we need to change the content field
messages.forEach((message) => { messages.forEach((message) => {
if (message.image || message.audio) { if ((message.image && message.image.length > 0) || (message.audio && message.audio.length > 0)) {
// The content field now becomes an array // The content field now becomes an array
message.content = [ message.content = [
{ {
@ -186,37 +198,42 @@ async function promptGPT(systemPrompt, input) {
} }
] ]
if (message.image) { if (message.image && message.image.length > 0) {
message.content.push( message.image.forEach(img => {
{ message.content.push(
"type": "image_url", {
"image_url": { "type": "image_url",
"url": message.image, "image_url": {
"url": img,
}
} }
} );
); });
delete message.image; delete message.image;
} }
if (message.audio) { if (message.audio && message.audio.length > 0) {
message.content.push( message.audio.forEach(aud => {
{ message.content.push(
"type": "audio_url", {
"audio_url": { "type": "audio_url",
"url": message.audio, "audio_url": {
"url": aud,
}
} }
} );
); });
delete message.audio; delete message.audio;
} }
} }
}); });
// reset the form and the files // reset the form and the files
image = ""; images = [];
audio = ""; audios = [];
document.getElementById("input_image").value = null; document.getElementById("input_image").value = null;
document.getElementById("input_audio").value = null; document.getElementById("input_audio").value = null;
document.getElementById("input_file").value = null;
document.getElementById("fileName").innerHTML = ""; document.getElementById("fileName").innerHTML = "";
// Source: https://stackoverflow.com/a/75751803/11386095 // Source: https://stackoverflow.com/a/75751803/11386095

View file

@ -221,12 +221,11 @@ SOFTWARE.
<div class="flex-1 p-4 overflow-auto" id="chat" x-data="{history: $store.chat.history}"> <div class="flex-1 p-4 overflow-auto" id="chat" x-data="{history: $store.chat.history}">
<p id="usage" x-show="history.length === 0" class="text-gray-300"> <p id="usage" x-show="history.length === 0" class="text-gray-300">
Start chatting with the AI by typing a prompt in the input field below and pressing Enter.<br> Start chatting with the AI by typing a prompt in the input field below and pressing Enter.<br>
For models that support images, you can upload an image by clicking the paperclip <ul class="list-disc list-inside">
<i class="fa-solid fa-paperclip"></i> icon. <br> <li>For models that support images, you can upload an image by clicking the <i class="fa-solid fa-image"></i> icon.</li>
For models that support audio, you can upload an audio file by clicking the microphone <li>For models that support audio, you can upload an audio file by clicking the <i class="fa-solid fa-microphone"></i> icon.</li>
<i class="fa-solid fa-microphone"></i> icon. <br> <li>To send a text, markdown or PDF file, click the <i class="fa-solid fa-file"></i> icon.</li>
To send a text, markdown or PDF file, click the file </ul>
<i class="fa-solid fa-file"></i> icon.
</p> </p>
<div id="messages" class="max-w-3xl mx-auto"> <div id="messages" class="max-w-3xl mx-auto">
<template x-for="message in history"> <template x-for="message in history">
@ -296,8 +295,8 @@ SOFTWARE.
<button <button
type="button" type="button"
onclick="document.getElementById('input_image').click()" onclick="document.getElementById('input_image').click()"
class="fa-solid fa-paperclip text-gray-400 absolute right-12 top-4 text-lg p-2 hover:text-blue-400 transition-colors duration-200" class="fa-solid fa-image text-gray-400 absolute right-12 top-4 text-lg p-2 hover:text-blue-400 transition-colors duration-200"
title="Attach an image" title="Attach images"
></button> ></button>
<button <button
type="button" type="button"
@ -338,22 +337,26 @@ SOFTWARE.
<input <input
id="input_image" id="input_image"
type="file" type="file"
multiple
accept="image/*"
style="display: none;" style="display: none;"
@change="fileName = $event.target.files[0].name" @change="fileName = $event.target.files.length + ' image(s) selected'"
/> />
<input <input
id="input_audio" id="input_audio"
type="file" type="file"
multiple
accept="audio/*" accept="audio/*"
style="display: none;" style="display: none;"
@change="fileName = $event.target.files[0].name" @change="fileName = $event.target.files.length + ' audio file(s) selected'"
/> />
<input <input
id="input_file" id="input_file"
type="file" type="file"
multiple
accept=".txt,.md,.pdf" accept=".txt,.md,.pdf"
style="display: none;" style="display: none;"
@change="fileName = $event.target.files[0].name" @change="fileName = $event.target.files.length + ' file(s) selected'"
/> />
</div> </div>
</form> </form>