Skip to content

Commit abf7a87

Browse files
committed
Support file inputs
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
1 parent 45c5875 commit abf7a87

File tree

3 files changed

+103
-9
lines changed

3 files changed

+103
-9
lines changed

core/http/middleware/request.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,8 @@ func mergeOpenAIRequestAndBackendConfig(config *config.BackendConfig, input *sch
312312
// Decode content as base64 either if it's an URL or base64 text
313313
base64, err := utils.GetContentURIAsBase64(pp.AudioURL.URL)
314314
if err != nil {
315-
log.Error().Msgf("Failed encoding image: %s", err)
315+
log.Error().Msgf("Failed encoding audio: %+v", pp.AudioURL)
316+
log.Error().Msgf("Failed encoding audio: %s", err)
316317
continue CONTENT
317318
}
318319
input.Messages[i].StringAudios = append(input.Messages[i].StringAudios, base64) // TODO: make sure that we only return base64 stuff

core/http/static/chat.js

Lines changed: 78 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,16 +50,91 @@ function submitSystemPrompt(event) {
5050

5151
var image = "";
5252
var audio = "";
53+
var fileContent = "";
54+
var currentFileName = "";
55+
56+
async function extractTextFromPDF(pdfData) {
57+
try {
58+
const pdf = await pdfjsLib.getDocument({ data: pdfData }).promise;
59+
let fullText = '';
60+
61+
for (let i = 1; i <= pdf.numPages; i++) {
62+
const page = await pdf.getPage(i);
63+
const textContent = await page.getTextContent();
64+
const pageText = textContent.items.map(item => item.str).join(' ');
65+
fullText += pageText + '\n';
66+
}
67+
68+
return fullText;
69+
} catch (error) {
70+
console.error('Error extracting text from PDF:', error);
71+
throw error;
72+
}
73+
}
74+
75+
function readInputFile() {
76+
if (!this.files || !this.files[0]) return;
77+
78+
const file = this.files[0];
79+
const FR = new FileReader();
80+
currentFileName = file.name;
81+
const fileExtension = file.name.split('.').pop().toLowerCase();
82+
83+
FR.addEventListener("load", async function(evt) {
84+
if (fileExtension === 'pdf') {
85+
try {
86+
fileContent = await extractTextFromPDF(evt.target.result);
87+
} catch (error) {
88+
console.error('Error processing PDF:', error);
89+
fileContent = "Error processing PDF file";
90+
}
91+
} else {
92+
// For text and markdown files
93+
fileContent = evt.target.result;
94+
}
95+
});
96+
97+
if (fileExtension === 'pdf') {
98+
FR.readAsArrayBuffer(file);
99+
} else {
100+
FR.readAsText(file);
101+
}
102+
}
53103

54104
function submitPrompt(event) {
55105
event.preventDefault();
56106

57107
const input = document.getElementById("input").value;
58-
Alpine.store("chat").add("user", input, image, audio);
108+
let fullInput = input;
109+
110+
// If there's file content, append it to the input for the LLM
111+
if (fileContent) {
112+
fullInput += "\n\nFile content:\n" + fileContent;
113+
}
114+
115+
// Show file icon in chat if there's a file
116+
let displayContent = input;
117+
if (currentFileName) {
118+
displayContent += `\n\n<i class="fa-solid fa-file"></i> Attached file: ${currentFileName}`;
119+
}
120+
121+
// Add the message to the chat UI with just the icon
122+
Alpine.store("chat").add("user", displayContent, image, audio);
123+
124+
// Update the last message in the store with the full content
125+
const history = Alpine.store("chat").history;
126+
if (history.length > 0) {
127+
history[history.length - 1].content = fullInput;
128+
}
129+
59130
document.getElementById("input").value = "";
60131
const systemPrompt = localStorage.getItem("system_prompt");
61132
Alpine.nextTick(() => { document.getElementById('messages').scrollIntoView(false); });
62-
promptGPT(systemPrompt, input);
133+
promptGPT(systemPrompt, fullInput);
134+
135+
// Reset file content and name after sending
136+
fileContent = "";
137+
currentFileName = "";
63138
}
64139

65140
function readInputImage() {
@@ -88,9 +163,6 @@ function readInputAudio() {
88163

89164
async function promptGPT(systemPrompt, input) {
90165
const model = document.getElementById("chat-model").value;
91-
// Set class "loader" to the element with "loader" id
92-
//document.getElementById("loader").classList.add("loader");
93-
// Make the "loader" visible
94166
toggleLoader(true);
95167

96168
messages = Alpine.store("chat").messages();
@@ -261,6 +333,7 @@ document.getElementById("prompt").addEventListener("submit", submitPrompt);
261333
document.getElementById("input").focus();
262334
document.getElementById("input_image").addEventListener("change", readInputImage);
263335
document.getElementById("input_audio").addEventListener("change", readInputAudio);
336+
document.getElementById("input_file").addEventListener("change", readInputFile);
264337

265338
storesystemPrompt = localStorage.getItem("system_prompt");
266339
if (storesystemPrompt) {

core/http/views/chat.html

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@
2929
<html lang="en">
3030
{{template "views/partials/head" .}}
3131
<script defer src="static/chat.js"></script>
32+
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
33+
<script>
34+
// Initialize PDF.js worker
35+
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
36+
</script>
3237
{{ $allGalleryConfigs:=.GalleryConfig }}
3338
{{ $model:=.Model}}
3439
<body class="bg-slate-900 text-gray-100 flex flex-col h-screen" x-data="{ sidebarOpen: true }">
@@ -215,11 +220,13 @@ <h1 class="text-lg font-semibold">
215220
<!-- Chat messages area -->
216221
<div class="flex-1 p-4 overflow-auto" id="chat" x-data="{history: $store.chat.history}">
217222
<p id="usage" x-show="history.length === 0" class="text-gray-300">
218-
Start chatting with the AI by typing a prompt in the input field below and pressing Enter.
223+
Start chatting with the AI by typing a prompt in the input field below and pressing Enter.<br>
219224
For models that support images, you can upload an image by clicking the paperclip
220-
<i class="fa-solid fa-paperclip"></i> icon.
225+
<i class="fa-solid fa-paperclip"></i> icon. <br>
221226
For models that support audio, you can upload an audio file by clicking the microphone
222-
<i class="fa-solid fa-microphone"></i> icon.
227+
<i class="fa-solid fa-microphone"></i> icon. <br>
228+
To send a text, markdown or PDF file, click the file
229+
<i class="fa-solid fa-file"></i> icon.
223230
</p>
224231
<div id="messages" class="max-w-3xl mx-auto">
225232
<template x-for="message in history">
@@ -298,6 +305,12 @@ <h1 class="text-lg font-semibold">
298305
class="fa-solid fa-microphone text-gray-400 absolute right-20 top-4 text-lg p-2 hover:text-blue-400 transition-colors duration-200"
299306
title="Attach an audio file"
300307
></button>
308+
<button
309+
type="button"
310+
onclick="document.getElementById('input_file').click()"
311+
class="fa-solid fa-file text-gray-400 absolute right-28 top-4 text-lg p-2 hover:text-blue-400 transition-colors duration-200"
312+
title="Upload text, markdown or PDF file"
313+
></button>
301314

302315
<!-- Send button and loader in the same position -->
303316
<div class="absolute right-3 top-4">
@@ -335,6 +348,13 @@ <h1 class="text-lg font-semibold">
335348
style="display: none;"
336349
@change="fileName = $event.target.files[0].name"
337350
/>
351+
<input
352+
id="input_file"
353+
type="file"
354+
accept=".txt,.md,.pdf"
355+
style="display: none;"
356+
@change="fileName = $event.target.files[0].name"
357+
/>
338358
</div>
339359
</form>
340360
</div>

0 commit comments

Comments
 (0)