server: (webui) file upload and pdf parsing

This commit is contained in:
dannyl1u 2025-02-03 22:45:52 -08:00
parent 496e5bf46b
commit 8d721dcca8
6 changed files with 284 additions and 12 deletions

Binary file not shown.

View file

@ -149,19 +149,42 @@
<!-- chat input -->
<div class="flex flex-row items-center mt-8 mb-6">
<textarea
class="textarea textarea-bordered w-full"
placeholder="Type a message (Shift+Enter to add a new line)"
v-model="inputMsg"
@keydown.enter.exact.prevent="sendMessage"
@keydown.enter.shift.exact.prevent="inputMsg += '\n'"
:disabled="isGenerating"
id="msg-input"
dir="auto"
></textarea>
<div class="relative w-full">
<textarea
class="textarea textarea-bordered w-full"
placeholder="Type a message (Shift+Enter to add a new line)"
v-model="inputMsg"
@keydown.enter.exact.prevent="sendMessage"
@keydown.enter.shift.exact.prevent="inputMsg += '\n'"
:disabled="isGenerating"
id="msg-input"
dir="auto"
></textarea>
<!-- file-upload button -->
<label for="pdf-upload" class="absolute top-2 right-2 cursor-pointer">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 text-gray-500 hover:text-black">
<path stroke-linecap="round" stroke-linejoin="round" d="M18.364 5.636a4.5 4.5 0 0 1 0 6.364l-6.364 6.364a4.5 4.5 0 0 1-6.364-6.364l6.364-6.364a3 3 0 0 1 4.243 4.243l-6.364 6.364a1.5 1.5 0 0 1-2.121-2.121l6.364-6.364" />
</svg>
</label>
<input id="pdf-upload" type="file" class="hidden" accept=".pdf" @change="handlePdfUpload" />
</div>
<button v-if="!isGenerating" class="btn btn-primary ml-2" @click="sendMessage" :disabled="inputMsg.length === 0">Send</button>
<button v-else class="btn btn-neutral ml-2" @click="stopGeneration">Stop</button>
</div>
<!-- section to display uploaded files -->
<div class="flex flex-wrap">
<div v-for="(file, index) in uploadedFiles" :key="index" class="flex items-center mr-4 mb-2">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 text-blue-500">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3.75v16.5m4.5-4.5H7.5M9 21h6a3 3 0 003-3V6a3 3 0 00-3-3H9a3 3 0 00-3 3v12a3 3 0 003 3z" />
</svg>
<span class="text-sm ml-1">{{ file.name }}</span>
<button @click="removeFile(index)" class="ml-2 text-grey-500 hover:text-red-700">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
</div>
</div>

View file

@ -15,6 +15,7 @@
"highlight.js": "^11.10.0",
"katex": "^0.16.15",
"markdown-it": "^14.1.0",
"pdfjs-dist": "^4.10.38",
"postcss": "^8.4.49",
"tailwindcss": "^3.4.15",
"textlinestream": "^1.1.1",
@ -397,6 +398,177 @@
"node": ">=12"
}
},
"node_modules/@napi-rs/canvas": {
"version": "0.1.66",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.66.tgz",
"integrity": "sha512-NE/eQKLbUS+LCbMHRa5HnR7cc1Q4ibg/qfLUN4Ukl3CC0lq6LfHE0YbvFm/l4i5RyyS+aUjL+8IuZDD9EH3amg==",
"optional": true,
"engines": {
"node": ">= 10"
},
"optionalDependencies": {
"@napi-rs/canvas-android-arm64": "0.1.66",
"@napi-rs/canvas-darwin-arm64": "0.1.66",
"@napi-rs/canvas-darwin-x64": "0.1.66",
"@napi-rs/canvas-linux-arm-gnueabihf": "0.1.66",
"@napi-rs/canvas-linux-arm64-gnu": "0.1.66",
"@napi-rs/canvas-linux-arm64-musl": "0.1.66",
"@napi-rs/canvas-linux-riscv64-gnu": "0.1.66",
"@napi-rs/canvas-linux-x64-gnu": "0.1.66",
"@napi-rs/canvas-linux-x64-musl": "0.1.66",
"@napi-rs/canvas-win32-x64-msvc": "0.1.66"
}
},
"node_modules/@napi-rs/canvas-android-arm64": {
"version": "0.1.66",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.66.tgz",
"integrity": "sha512-77Yq9yaUYN90zCovYOpw7LhidJiswU9wLIWWBGF6iiEJyQdt6tkiXpGRZpOMJVO70afkcdc4T7532cxMIBhk0Q==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-darwin-arm64": {
"version": "0.1.66",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.66.tgz",
"integrity": "sha512-cz3aJ06b8BZGtwRxKTiE0OVUlB17MH8j+BnE4A5+wD9aD1guCCqECsz+k7tpXdAdTAYKRIz2pq6ZuiJ76NyUbQ==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-darwin-x64": {
"version": "0.1.66",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.66.tgz",
"integrity": "sha512-szIWqJgFm2OTyGzM+hSiJOaOtjI73VYRC2KN30zZTt7i1+0sgpm5exK5ltDBPOmCdnLt7SbUfpInLj8VvxYlKA==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-linux-arm-gnueabihf": {
"version": "0.1.66",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.66.tgz",
"integrity": "sha512-h/TZJFc6JLvp8FwbA5mu+yXiblN0iKqshU7xzd6L+ks5uNYgjS7XWLkNiyPQkMaXQgVczOJfZy7r4NSPK3V8Hg==",
"cpu": [
"arm"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-linux-arm64-gnu": {
"version": "0.1.66",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.66.tgz",
"integrity": "sha512-RGFUdBdi0Xmf+TfwZcB89Ap6hDYh4nzyJhXhNJIgve6ELrIPFhf7sDHvUHxjgW0YzczGoo+ophyCm03cJggu+w==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-linux-arm64-musl": {
"version": "0.1.66",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.66.tgz",
"integrity": "sha512-2cFViDIZ0xQlAHyJmyym+rj3p04V16vgAiz64sCAfwOOiW6e19agv1HQWHUsro3G2lF3PaHGAnp0WRPXGqLOfg==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-linux-riscv64-gnu": {
"version": "0.1.66",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.66.tgz",
"integrity": "sha512-Vm5ZWS2RDPeBpnfx83eJpZfJT07xl0jqp8d83PklKqiDNa3BmDZZ/uuI40/ICgejGLymXXYo5N21b7oAxhRTSA==",
"cpu": [
"riscv64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-linux-x64-gnu": {
"version": "0.1.66",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.66.tgz",
"integrity": "sha512-/ptGBhErNBCgWff3khtuEjhiiYWf70oWvBPRj8y5EMB0nLYpve7RxxFnavVvxN49kJ0MQHRIwgfyd47RSOOKPw==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-linux-x64-musl": {
"version": "0.1.66",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.66.tgz",
"integrity": "sha512-XunvXisTkIG+bpq6BcXmsUstoLX3RLS6N9Uz9Pg9RpWIMeM6ObR5shr3NgpGRJq93769I1hS4mJW0DX2Au3WBw==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-win32-x64-msvc": {
"version": "0.1.66",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.66.tgz",
"integrity": "sha512-3n34watNFqpwACDA+pt4jfQD6zR8PzfK86FBajdsgDVVZhSp6ohgbbJv+eUrXM08VUtjxTq7+U4sWspTu9+4Ug==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.28.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.28.0.tgz",
@ -1732,6 +1904,17 @@
"node": ">= 6"
}
},
"node_modules/pdfjs-dist": {
"version": "4.10.38",
"resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-4.10.38.tgz",
"integrity": "sha512-/Y3fcFrXEAsMjJXeL9J8+ZG9U01LbuWaYypvDW2ycW1jL269L3js3DVBjDJ0Up9Np1uqDXsDrRihHANhZOlwdQ==",
"engines": {
"node": ">=20"
},
"optionalDependencies": {
"@napi-rs/canvas": "^0.1.65"
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",

View file

@ -21,6 +21,7 @@
"highlight.js": "^11.10.0",
"katex": "^0.16.15",
"markdown-it": "^14.1.0",
"pdfjs-dist": "^4.10.38",
"postcss": "^8.4.49",
"tailwindcss": "^3.4.15",
"textlinestream": "^1.1.1",

View file

@ -15,6 +15,11 @@ import daisyuiThemes from 'daisyui/src/theming/themes';
// ponyfill for missing ReadableStream asyncIterator on Safari
import { asyncIterator } from '@sec-ant/readable-stream/ponyfill/asyncIterator';
// pdf parsing
import * as pdfjsLib from "pdfjs-dist";
import pdfWorker from "pdfjs-dist/build/pdf.worker.mjs?url";
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfWorker;
const isDev = import.meta.env.MODE === 'development';
// types
@ -387,6 +392,8 @@ const mainApp = createApp({
viewingConvId: StorageUtils.getNewConvId(),
inputMsg: '',
isGenerating: false,
uploadedFiles: [],
fileText: '',
/** @type {Array<Message> | null} */
pendingMsg: null, // the on-going message from assistant
stopGeneration: () => {},
@ -467,6 +474,36 @@ const mainApp = createApp({
document.body.removeChild(a);
URL.revokeObjectURL(url);
},
removeFile(index){
this.uploadedFiles.splice(index, 1);
},
async handlePdfUpload(event) {
const file = event.target.files[0];
if (file && file.type === "application/pdf") {
try {
const arrayBuffer = await file.arrayBuffer();
const loadingTask = pdfjsLib.getDocument({ data: new Uint8Array(arrayBuffer) });
loadingTask.promise.then(pdfDocument => {
console.log("PDF loaded:", pdfDocument);
const pdfPromise = extractPdfText(file);
pdfPromise.then((data) => {
this.uploadedFiles.push(file);
this.fileText += data;
})
.catch((error) => {
console.log(error)
});
}).catch(error => {
console.error("Error loading PDF:", error);
});
} catch (error) {
console.error("Error extracting PDF text:", error);
}
} else {
alert("Please upload a valid PDF file.");
}
},
async sendMessage() {
if (!this.inputMsg) return;
const currConvId = this.viewingConvId;
@ -474,11 +511,13 @@ const mainApp = createApp({
StorageUtils.appendMsg(currConvId, {
id: Date.now(),
role: 'user',
content: this.inputMsg,
content: this.fileText + '\n' + this.inputMsg,
});
this.fetchConversation();
this.fetchMessages();
this.inputMsg = '';
this.uploadedFiles = [];
this.fileText = '';
this.generateMessage(currConvId);
chatScrollToBottom();
},
@ -669,6 +708,32 @@ try {
<button class="btn" onClick="localStorage.clear(); window.location.reload();">Clear localStorage</button>
</div>`;
}
/**
* extracts text content from a given PDF file using pdf.js
* @param {File} file
* @returns {Promise<string>}
*/
async function extractPdfText(file) {
const fileReader = new FileReader();
return new Promise((resolve, reject) => {
fileReader.onload = async (e) => {
const pdfData = new Uint8Array(e.target.result);
try {
const pdf = await pdfjsLib.getDocument(pdfData).promise;
let textContent = "";
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const textContentPage = await page.getTextContent();
textContent += textContentPage.items.map(item => item.str).join(" ") + "\n";
}
resolve(textContent.trim());
} catch (error) {
reject(error);
}
};
fileReader.readAsArrayBuffer(file);
});
}
/**
* filter out redundant fields upon sending to API

View file

@ -4,7 +4,7 @@ import path from 'path';
import fs from 'fs';
import zlib from 'zlib';
const MAX_BUNDLE_SIZE = 1.5 * 1024 * 1024; // only increase when absolutely necessary
const MAX_BUNDLE_SIZE = 2 * 1024 * 1024; // only increase when absolutely necessary
const GUIDE_FOR_FRONTEND = `
<!--