From a4c65f16c24bbdb5cab1245b46df0b78b97c1d65 Mon Sep 17 00:00:00 2001 From: Andrew Beal Date: Sun, 15 Mar 2026 19:48:28 +0000 Subject: [PATCH] Update file attachment messaging and add document file support Add support for DOCX, PPTX, XLSX, ODT, ODP, and ODS document formats by converting them to plain text. Standardize attachment introduction messages across all AI providers from "Binary data for X follows" to "The contents of the file 'X' are provided below." --- AIClasses/Claude/Claude.ts | 2 +- AIClasses/Gemini/Gemini.ts | 2 +- AIClasses/Mistral/Mistral.ts | 4 +- AIClasses/OpenAI/OpenAI.ts | 2 +- Components/ChatArea.svelte | 14 +- Conversations/Conversation.ts | 12 +- Enums/FileType.ts | 19 +- Enums/FileTypeMimeTypeMapping.ts | 16 + Enums/MimeType.ts | 17 + Helpers/{PDFHelper.ts => DocumentHelper.ts} | 18 + Helpers/StringTools.ts | 9 + Services/InputService.ts | 26 +- Services/VaultService.ts | 22 +- __tests__/AIClasses/Claude.test.ts | 24 +- __tests__/AIClasses/Gemini.test.ts | 18 +- __tests__/AIClasses/Mistral.test.ts | 8 +- __tests__/AIClasses/OpenAI.test.ts | 20 +- __tests__/Conversations/Conversation.test.ts | 69 +- __tests__/Services/VaultService.pdf.test.ts | 2 +- package-lock.json | 675 +++++++++++++++++++ package.json | 1 + 21 files changed, 919 insertions(+), 61 deletions(-) rename Helpers/{PDFHelper.ts => DocumentHelper.ts} (65%) diff --git a/AIClasses/Claude/Claude.ts b/AIClasses/Claude/Claude.ts index 6a71549..b8c2279 100644 --- a/AIClasses/Claude/Claude.ts +++ b/AIClasses/Claude/Claude.ts @@ -308,7 +308,7 @@ export class Claude extends BaseAIClass { } return [ - {type: "text", text: `Binary data for ${attachment.fileName} follows in next message` }, + {type: "text", text: `The contents of the file '${attachment.fileName}' are provided below.` }, { type: isPlainText || mimeType === MimeType.APPLICATION_PDF ? "document" : "image", source: { diff --git a/AIClasses/Gemini/Gemini.ts b/AIClasses/Gemini/Gemini.ts index 85cf9be..e870fd5 100644 --- a/AIClasses/Gemini/Gemini.ts +++ b/AIClasses/Gemini/Gemini.ts @@ -348,7 +348,7 @@ export class Gemini extends BaseAIClass { continue; } - parts.push({ text: `Binary data for ${attachment.fileName} follows in next message` }); + parts.push({ text: `The contents of the file '${attachment.fileName}' are provided below.` }); parts.push({ fileData: { mimeType: mimeType, diff --git a/AIClasses/Mistral/Mistral.ts b/AIClasses/Mistral/Mistral.ts index 8b5a419..b92bfa5 100644 --- a/AIClasses/Mistral/Mistral.ts +++ b/AIClasses/Mistral/Mistral.ts @@ -355,7 +355,7 @@ export class Mistral extends BaseAIClass { const signedUrl = fileService.getSignedUrl(fileID); if (signedUrl) { contentParts.push( - { type: "text", text: `Document: ${attachment.fileName}` }, + { type: "text", text: `The contents of the file '${attachment.fileName}' are provided below.` }, { type: "document_url", document_url: signedUrl } ); } else { @@ -371,7 +371,7 @@ export class Mistral extends BaseAIClass { const signedUrl = fileService.getSignedUrl(fileID); if (signedUrl) { contentParts.push( - { type: "text", text: `Image: ${attachment.fileName}` }, + { type: "text", text: `The contents of the file '${attachment.fileName}' are provided below.` }, { type: "image_url", image_url: signedUrl diff --git a/AIClasses/OpenAI/OpenAI.ts b/AIClasses/OpenAI/OpenAI.ts index a449e45..96c4e52 100644 --- a/AIClasses/OpenAI/OpenAI.ts +++ b/AIClasses/OpenAI/OpenAI.ts @@ -352,7 +352,7 @@ export class OpenAI extends BaseAIClass { } contentBlocks.push( - { type: "input_text", text: `Binary data for ${attachment.fileName} follows in next message` }, + { type: "input_text", text: `The contents of the file '${attachment.fileName}' are provided below.` }, { type: isPlainText || mimeType === MimeType.APPLICATION_PDF ? "input_file" : "input_image", file_id: fileID diff --git a/Components/ChatArea.svelte b/Components/ChatArea.svelte index 3e8fd30..51a43dd 100644 --- a/Components/ChatArea.svelte +++ b/Components/ChatArea.svelte @@ -250,8 +250,10 @@ {#if message.role === Role.User}
-
- {@html content} +
+
+ {@html content} +
{#if message.references.length > 0}
@@ -383,16 +385,20 @@ max-width: 100%; } - .message-text-user { + .message-text-user-container { max-height: 15vh; overflow: scroll; padding-top: var(--size-4-2); white-space: pre-wrap; } - .message-text-user::-webkit-scrollbar { + .message-text-user-container::-webkit-scrollbar { display: none; } + + .message-text-user-container { + padding-bottom: var(--size-4-2); + } .conversation-empty-state { margin: auto; diff --git a/Conversations/Conversation.ts b/Conversations/Conversation.ts index 4bf3d04..e49d661 100644 --- a/Conversations/Conversation.ts +++ b/Conversations/Conversation.ts @@ -6,6 +6,7 @@ import { Role } from "Enums/Role"; import { AITool } from "Enums/AITool"; import { isTextFile, toFileType } from "Enums/FileType"; import { FileTypeToMimeType } from "Enums/FileTypeMimeTypeMapping"; +import { isDocumentMimeType, MimeType } from "Enums/MimeType"; export class Conversation { @@ -73,7 +74,7 @@ export class Conversation { name: functionResponse.name, response: { results: responseResults, - ...(binaryResults.length > 0 && {message: "Binary files follow in next message"}) + ...(binaryResults.length > 0 && {message: "The contents of the files are provided below."}) } } }; @@ -84,7 +85,7 @@ export class Conversation { functionResponse: { name: functionResponse.name, response: { - message: "Files retrieved successfully. Binary content follows in next message.", + message: "Files retrieved successfully. The contents of the files are provided below.", count: binaryResults.length } } @@ -104,7 +105,12 @@ export class Conversation { .filter(file => file.type && file.contents !== undefined) .map(file => { const fileName = file.path.split('/').pop() || file.path; - const mimeType = FileTypeToMimeType[toFileType(file.type as string)]; + let mimeType = FileTypeToMimeType[toFileType(file.type as string)]; + + if (isDocumentMimeType(mimeType)) { + mimeType = MimeType.TEXT_PLAIN; + return new Attachment(fileName, mimeType, StringTools.toBase64(file.contents as string)); + } return new Attachment(fileName, mimeType, file.contents as string); }); diff --git a/Enums/FileType.ts b/Enums/FileType.ts index 465c6b7..2e10c2c 100644 --- a/Enums/FileType.ts +++ b/Enums/FileType.ts @@ -136,6 +136,14 @@ export enum FileType { DIFF = "diff", PATCH = "patch", + // Document formats + DOCX = "docx", + PPTX = "pptx", + XLSX = "xlsx", + ODT = "odt", + ODP = "odp", + ODS = "ods", + // Logs LOG = "log", @@ -158,7 +166,16 @@ export function isFileType(value: unknown, fileType: FileType): value is FileTyp } export function isBinaryFile(extension: string) { - return isKnownFileType(extension) && !isTextFile(extension); + return isKnownFileType(extension) && !isTextFile(extension) && !isDocumentFile(extension); +} + +export function isDocumentFile(extension: string) { + return isFileType(extension, FileType.DOCX) + || isFileType(extension, FileType.PPTX) + || isFileType(extension, FileType.XLSX) + || isFileType(extension, FileType.ODT) + || isFileType(extension, FileType.ODP) + || isFileType(extension, FileType.ODS); } export function isTextFile(extension: string) { diff --git a/Enums/FileTypeMimeTypeMapping.ts b/Enums/FileTypeMimeTypeMapping.ts index 75f1239..6f6ba4a 100644 --- a/Enums/FileTypeMimeTypeMapping.ts +++ b/Enums/FileTypeMimeTypeMapping.ts @@ -143,6 +143,14 @@ export const FileTypeToMimeType: Record = { [FileType.DIFF]: MimeType.TEXT_PLAIN, [FileType.PATCH]: MimeType.TEXT_PLAIN, + // Document formats + [FileType.DOCX]: MimeType.APPLICATION_DOCX, + [FileType.PPTX]: MimeType.APPLICATION_PPTX, + [FileType.XLSX]: MimeType.APPLICATION_XLSX, + [FileType.ODT]: MimeType.APPLICATION_ODT, + [FileType.ODP]: MimeType.APPLICATION_ODP, + [FileType.ODS]: MimeType.APPLICATION_ODS, + // Logs [FileType.LOG]: MimeType.TEXT_PLAIN, @@ -245,5 +253,13 @@ export const MimeTypeToFileTypes: Record = { [MimeType.VIDEO_OGG]: [FileType.OGV], [MimeType.VIDEO_WEBM]: [FileType.WEBM, FileType.WEBM_VIDEO], + // Office document formats + [MimeType.APPLICATION_DOCX]: [FileType.DOCX], + [MimeType.APPLICATION_PPTX]: [FileType.PPTX], + [MimeType.APPLICATION_XLSX]: [FileType.XLSX], + [MimeType.APPLICATION_ODT]: [FileType.ODT], + [MimeType.APPLICATION_ODP]: [FileType.ODP], + [MimeType.APPLICATION_ODS]: [FileType.ODS], + [MimeType.UNKNOWN]: [FileType.UNKNOWN] }; diff --git a/Enums/MimeType.ts b/Enums/MimeType.ts index d7908df..b125409 100644 --- a/Enums/MimeType.ts +++ b/Enums/MimeType.ts @@ -63,6 +63,14 @@ export enum MimeType { APPLICATION_TYPESCRIPT = "application/x-typescript", APPLICATION_SH = "application/x-sh", + // Office document formats + APPLICATION_DOCX = "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + APPLICATION_PPTX = "application/vnd.openxmlformats-officedocument.presentationml.presentation", + APPLICATION_XLSX = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + APPLICATION_ODT = "application/vnd.oasis.opendocument.text", + APPLICATION_ODP = "application/vnd.oasis.opendocument.presentation", + APPLICATION_ODS = "application/vnd.oasis.opendocument.spreadsheet", + // Markup formats TEXT_RST = "text/x-rst", TEXT_ASCIIDOC = "text/x-asciidoc", @@ -108,6 +116,15 @@ export function isKnownMimeType(value: string): value is MimeType { return Object.values(MimeType).includes(value as MimeType) && value !== MimeType.UNKNOWN.toString(); } +export function isDocumentMimeType(mimeType: MimeType) { + return mimeType === MimeType.APPLICATION_DOCX || + mimeType === MimeType.APPLICATION_PPTX || + mimeType === MimeType.APPLICATION_XLSX || + mimeType === MimeType.APPLICATION_ODT || + mimeType === MimeType.APPLICATION_ODP || + mimeType === MimeType.APPLICATION_ODS; +} + export function isImageMimeType(mimeType: MimeType) { return mimeType === MimeType.IMAGE_AVIF || mimeType === MimeType.IMAGE_BMP || diff --git a/Helpers/PDFHelper.ts b/Helpers/DocumentHelper.ts similarity index 65% rename from Helpers/PDFHelper.ts rename to Helpers/DocumentHelper.ts index 9db6538..0f8bc66 100644 --- a/Helpers/PDFHelper.ts +++ b/Helpers/DocumentHelper.ts @@ -1,7 +1,9 @@ import { extractText, getDocumentProxy } from 'unpdf'; import type { IPageText } from '../Types/SearchTypes'; import { Exception } from './Exception'; +import OfficeParser from 'officeparser'; +// Handles PDF format export async function readPDF(arrayBuffer: ArrayBuffer): Promise { try { const pdf = await getDocumentProxy(new Uint8Array(arrayBuffer)); @@ -27,4 +29,20 @@ export async function readPDF(arrayBuffer: ArrayBuffer): Promise { } return [{ text: `Failed to read PDF: ${Exception.messageFrom(error)}`, pageNumber: 1 }] as IPageText[]; } +} + +// Handles document formats: DOCX, PPTX, XLSX, ODT, ODP, ODS +export async function readDocument(arrayBuffer: ArrayBuffer): Promise { + try { + const ast = await OfficeParser.parseOffice(arrayBuffer, { + extractAttachments: true, + ocr: true, + ocrLanguage: "eng+esp" + }); + + // OfficeParser doesn't currently expose page data (page number etc) + return [{ text: ast.toText(), pageNumber: 1 }] as IPageText[]; + } catch (error) { + return [{ text: `Failed to read document: ${Exception.messageFrom(error)}`, pageNumber: 1 }] as IPageText[]; + } } \ No newline at end of file diff --git a/Helpers/StringTools.ts b/Helpers/StringTools.ts index 7f821b3..e242aef 100644 --- a/Helpers/StringTools.ts +++ b/Helpers/StringTools.ts @@ -68,6 +68,15 @@ export abstract class StringTools { return regex; } + public static toBase64(text: string): string { + const bytes = new TextEncoder().encode(text); + let binary = ""; + for (let i = 0; i < bytes.length; i++) { + binary += String.fromCharCode(bytes[i]); + } + return btoa(binary); + } + public static toBytes(input: string): Uint8Array { const binaryString = atob(input); const bytes = new Uint8Array(binaryString.length); diff --git a/Services/InputService.ts b/Services/InputService.ts index 04b6e54..3b1f402 100644 --- a/Services/InputService.ts +++ b/Services/InputService.ts @@ -5,10 +5,13 @@ import { arrayBufferToBase64, Notice } from "obsidian"; import { FileTypeToMimeType } from "Enums/FileTypeMimeTypeMapping"; import * as path from "path-browserify"; import { pathExtname } from "Helpers/Helpers"; -import { FileType, toFileType } from "Enums/FileType"; +import { FileType, isDocumentFile, toFileType } from "Enums/FileType"; import type { FileSystemService } from "./FileSystemService"; import { Resolve } from "./DependencyService"; import { Services } from "./Services"; +import { readDocument } from "Helpers/DocumentHelper"; +import { MimeType } from "Enums/MimeType"; +import { StringTools } from "Helpers/StringTools"; export class InputService { @@ -43,12 +46,21 @@ export class InputService { new Notice(`Unsupported file '${file.name}'`); continue; } - - attachments.push(new Attachment( - file.name, - FileTypeToMimeType[fileType], - arrayBufferToBase64(await file.arrayBuffer()) - )); + + if (isDocumentFile(fileType)) { + const content = await readDocument(await file.arrayBuffer()); + attachments.push(new Attachment( + file.name, + MimeType.TEXT_PLAIN, + StringTools.toBase64(content[0].text) + )); + } else { + attachments.push(new Attachment( + file.name, + FileTypeToMimeType[fileType], + arrayBufferToBase64(await file.arrayBuffer()) + )); + } } } diff --git a/Services/VaultService.ts b/Services/VaultService.ts index 3588917..1efa629 100644 --- a/Services/VaultService.ts +++ b/Services/VaultService.ts @@ -16,8 +16,8 @@ import * as path from "path-browserify"; import { Event } from "Enums/Event"; import { AbortService } from "./AbortService"; import { AIToolResponse } from "AIClasses/ToolDefinitions/AIToolResponse"; -import { FileType, isBinaryFile, isFileType } from "Enums/FileType"; -import { readPDF } from "Helpers/PDFHelper"; +import { FileType, isBinaryFile, isDocumentFile, isFileType } from "Enums/FileType"; +import { readDocument, readPDF } from "Helpers/DocumentHelper"; interface IFileEventArgs { oldPath: string; @@ -86,13 +86,22 @@ export class VaultService { return Exception.new(`File does not exist: ${filePath}`); } - if (isBinaryFile(file.extension.toLowerCase())) { + const fileExtension = file.extension.toLowerCase(); + + if (isBinaryFile(fileExtension)) { const arrayBuffer = await this.readBinaryData(file, allowAccessToPluginRoot); if (arrayBuffer) { return arrayBufferToBase64(arrayBuffer); } } + if (isDocumentFile(fileExtension)) { + const arrayBuffer = await this.readBinaryData(file, allowAccessToPluginRoot); + if (arrayBuffer) { + return (await readDocument(arrayBuffer))[0].text; + } + } + return await this.vault.read(file); } @@ -380,9 +389,14 @@ export class VaultService { const batchPromises = batch.map(async (file) => { try { let content; - if (isFileType(file.extension.toLocaleLowerCase(), FileType.PDF)) { + const fileExtension = file.extension.toLocaleLowerCase(); + + if (isFileType(fileExtension, FileType.PDF)) { const arrayBuffer = await this.vault.readBinary(file); content = await readPDF(arrayBuffer); + } else if (isDocumentFile(fileExtension)) { + const arrayBuffer = await this.vault.readBinary(file); + content = await readDocument(arrayBuffer); } else { content = [{ text: await this.vault.cachedRead(file), pageNumber: 1 }] as IPageText[]; } diff --git a/__tests__/AIClasses/Claude.test.ts b/__tests__/AIClasses/Claude.test.ts index f4510f7..d684dfa 100644 --- a/__tests__/AIClasses/Claude.test.ts +++ b/__tests__/AIClasses/Claude.test.ts @@ -795,7 +795,7 @@ describe('Claude', () => { expect(result[0].content.length).toBeGreaterThan(1); // Should have filename text block from formatBinaryFiles - const filenameBlock = result[0].content.find((block: any) => block.type === 'text' && block.text === 'Binary data for test-image.png follows in next message'); + const filenameBlock = result[0].content.find((block: any) => block.type === 'text' && block.text === `The contents of the file 'test-image.png' are provided below.`); expect(filenameBlock).toBeDefined(); // Should have image content blocks from formatBinaryFiles @@ -828,7 +828,7 @@ describe('Claude', () => { expect(result[0].content.length).toBeGreaterThan(1); // Should have filename text block from formatBinaryFiles - const filenameBlock = result[0].content.find((block: any) => block.type === 'text' && block.text === 'Binary data for document.pdf follows in next message'); + const filenameBlock = result[0].content.find((block: any) => block.type === 'text' && block.text === `The contents of the file 'document.pdf' are provided below.`); expect(filenameBlock).toBeDefined(); // Should have document content blocks from formatBinaryFiles @@ -1241,7 +1241,7 @@ describe('Claude', () => { expect(parsed).toHaveLength(2); expect(parsed[0]).toEqual({ type: 'text', - text: 'Binary data for report.pdf follows in next message' + text: `The contents of the file 'report.pdf' are provided below.` }); expect(parsed[1]).toEqual({ type: 'document', @@ -1269,7 +1269,7 @@ describe('Claude', () => { expect(parsed).toHaveLength(2); expect(parsed[0]).toEqual({ type: 'text', - text: 'Binary data for photo.jpg follows in next message' + text: `The contents of the file 'photo.jpg' are provided below.` }); expect(parsed[1]).toEqual({ type: 'image', @@ -1410,7 +1410,7 @@ describe('Claude', () => { expect(parsed).toHaveLength(6); // PDF file - expect(parsed[0]).toEqual({ type: 'text', text: 'Binary data for doc.pdf follows in next message' }); + expect(parsed[0]).toEqual({ type: 'text', text: `The contents of the file 'doc.pdf' are provided below.` }); expect(parsed[1]).toEqual({ type: 'document', source: { @@ -1420,7 +1420,7 @@ describe('Claude', () => { }); // JPEG image - expect(parsed[2]).toEqual({ type: 'text', text: 'Binary data for image.jpg follows in next message' }); + expect(parsed[2]).toEqual({ type: 'text', text: `The contents of the file 'image.jpg' are provided below.` }); expect(parsed[3]).toEqual({ type: 'image', source: { @@ -1430,7 +1430,7 @@ describe('Claude', () => { }); // PNG image - expect(parsed[4]).toEqual({ type: 'text', text: 'Binary data for screenshot.png follows in next message' }); + expect(parsed[4]).toEqual({ type: 'text', text: `The contents of the file 'screenshot.png' are provided below.` }); expect(parsed[5]).toEqual({ type: 'image', source: { @@ -1477,14 +1477,14 @@ describe('Claude', () => { expect(parsed).toHaveLength(5); expect(parsed[0].type).toBe('text'); - expect(parsed[0].text).toBe('Binary data for good.jpg follows in next message'); + expect(parsed[0].text).toBe(`The contents of the file 'good.jpg' are provided below.`); expect(parsed[1].type).toBe('image'); expect(parsed[2]).toEqual({ type: 'text', text: 'Unsupported mime type \'image/bmp\': bad.bmp' }); expect(parsed[3].type).toBe('text'); - expect(parsed[3].text).toBe('Binary data for doc.pdf follows in next message'); + expect(parsed[3].text).toBe(`The contents of the file 'doc.pdf' are provided below.`); expect(parsed[4].type).toBe('document'); }); @@ -1532,9 +1532,9 @@ describe('Claude', () => { const parsed = JSON.parse(result); expect(parsed).toHaveLength(4); - expect(parsed[0].text).toBe('Binary data for document.PDF follows in next message'); + expect(parsed[0].text).toBe(`The contents of the file 'document.PDF' are provided below.`); expect(parsed[1].type).toBe('document'); - expect(parsed[2].text).toBe('Binary data for photo.JPG follows in next message'); + expect(parsed[2].text).toBe(`The contents of the file 'photo.JPG' are provided below.`); expect(parsed[3].type).toBe('image'); }); @@ -1561,7 +1561,7 @@ describe('Claude', () => { const result = claude.formatBinaryFiles([attachment as any]); const parsed = JSON.parse(result); - expect(parsed[0].text).toBe('Binary data for report (final) v2.pdf follows in next message'); + expect(parsed[0].text).toBe(`The contents of the file 'report (final) v2.pdf' are provided below.`); }); it('should handle JPEG files with .jpeg extension', () => { diff --git a/__tests__/AIClasses/Gemini.test.ts b/__tests__/AIClasses/Gemini.test.ts index a158d3f..8033e30 100644 --- a/__tests__/AIClasses/Gemini.test.ts +++ b/__tests__/AIClasses/Gemini.test.ts @@ -1135,7 +1135,7 @@ describe('Gemini', () => { expect(parsed).toHaveLength(2); expect(parsed[0]).toEqual({ - text: 'Binary data for report.pdf follows in next message' + text: `The contents of the file 'report.pdf' are provided below.` }); expect(parsed[1]).toEqual({ fileData: { @@ -1161,7 +1161,7 @@ describe('Gemini', () => { expect(parsed).toHaveLength(2); expect(parsed[0]).toEqual({ - text: 'Binary data for photo.jpg follows in next message' + text: `The contents of the file 'photo.jpg' are provided below.` }); expect(parsed[1]).toEqual({ fileData: { @@ -1271,7 +1271,7 @@ describe('Gemini', () => { expect(parsed).toHaveLength(6); // PDF file - expect(parsed[0]).toEqual({ text: 'Binary data for doc.pdf follows in next message' }); + expect(parsed[0]).toEqual({ text: `The contents of the file 'doc.pdf' are provided below.` }); expect(parsed[1]).toEqual({ fileData: { mimeType: 'application/pdf', @@ -1280,7 +1280,7 @@ describe('Gemini', () => { }); // JPEG image - expect(parsed[2]).toEqual({ text: 'Binary data for image.jpg follows in next message' }); + expect(parsed[2]).toEqual({ text: `The contents of the file 'image.jpg' are provided below.` }); expect(parsed[3]).toEqual({ fileData: { mimeType: 'image/jpeg', @@ -1289,7 +1289,7 @@ describe('Gemini', () => { }); // PNG image - expect(parsed[4]).toEqual({ text: 'Binary data for screenshot.png follows in next message' }); + expect(parsed[4]).toEqual({ text: `The contents of the file 'screenshot.png' are provided below.` }); expect(parsed[5]).toEqual({ fileData: { mimeType: 'image/png', @@ -1334,12 +1334,12 @@ describe('Gemini', () => { expect(parsed).toHaveLength(5); - expect(parsed[0]).toEqual({ text: 'Binary data for good.jpg follows in next message' }); + expect(parsed[0]).toEqual({ text: `The contents of the file 'good.jpg' are provided below.` }); expect(parsed[1]).toHaveProperty('fileData'); expect(parsed[2]).toEqual({ text: 'Unsupported mime type \'image/bmp\': bad.bmp' }); - expect(parsed[3]).toEqual({ text: 'Binary data for doc.pdf follows in next message' }); + expect(parsed[3]).toEqual({ text: `The contents of the file 'doc.pdf' are provided below.` }); expect(parsed[4]).toHaveProperty('fileData'); }); @@ -1369,7 +1369,7 @@ describe('Gemini', () => { const parsed = JSON.parse(result); expect(parsed).toHaveLength(2); // Only successful upload - expect(parsed[0]).toEqual({ text: 'Binary data for success.pdf follows in next message' }); + expect(parsed[0]).toEqual({ text: `The contents of the file 'success.pdf' are provided below.` }); expect(parsed[1]).toEqual({ fileData: { mimeType: 'application/pdf', @@ -1399,7 +1399,7 @@ describe('Gemini', () => { const result = gemini.formatBinaryFiles([attachment as any]); const parsed = JSON.parse(result); - expect(parsed[0].text).toBe('Binary data for report (final) v2.pdf follows in next message'); + expect(parsed[0].text).toBe(`The contents of the file 'report (final) v2.pdf' are provided below.`); }); it('should handle JPEG files with .jpeg extension', () => { diff --git a/__tests__/AIClasses/Mistral.test.ts b/__tests__/AIClasses/Mistral.test.ts index 925a315..ba77cd9 100644 --- a/__tests__/AIClasses/Mistral.test.ts +++ b/__tests__/AIClasses/Mistral.test.ts @@ -705,7 +705,7 @@ describe('Mistral', () => { expect(parsed).toHaveLength(2); expect(parsed[0]).toEqual({ type: 'text', - text: 'Document: report.pdf' + text: `The contents of the file 'report.pdf' are provided below.` }); expect(parsed[1]).toEqual({ type: 'document_url', @@ -730,7 +730,7 @@ describe('Mistral', () => { expect(parsed).toHaveLength(2); expect(parsed[0]).toEqual({ type: 'text', - text: 'Image: photo.jpg' + text: `The contents of the file 'photo.jpg' are provided below.` }); expect(parsed[1]).toEqual({ type: 'image_url', @@ -808,14 +808,14 @@ describe('Mistral', () => { expect(parsed).toHaveLength(4); // PDF file - expect(parsed[0]).toEqual({ type: 'text', text: 'Document: doc.pdf' }); + expect(parsed[0]).toEqual({ type: 'text', text: `The contents of the file 'doc.pdf' are provided below.` }); expect(parsed[1]).toEqual({ type: 'document_url', document_url: 'https://signed-url.com/file_1' }); // JPEG image - expect(parsed[2]).toEqual({ type: 'text', text: 'Image: image.jpg' }); + expect(parsed[2]).toEqual({ type: 'text', text: `The contents of the file 'image.jpg' are provided below.` }); expect(parsed[3]).toEqual({ type: 'image_url', image_url: 'https://signed-url.com/file_2' diff --git a/__tests__/AIClasses/OpenAI.test.ts b/__tests__/AIClasses/OpenAI.test.ts index 2bdb62d..0085292 100644 --- a/__tests__/AIClasses/OpenAI.test.ts +++ b/__tests__/AIClasses/OpenAI.test.ts @@ -1016,7 +1016,7 @@ describe('OpenAI', () => { expect(parsed[0].content).toHaveLength(2); expect(parsed[0].content[0]).toEqual({ type: 'input_text', - text: 'Binary data for report.pdf follows in next message' + text: `The contents of the file 'report.pdf' are provided below.` }); expect(parsed[0].content[1]).toEqual({ type: 'input_file', @@ -1043,7 +1043,7 @@ describe('OpenAI', () => { expect(parsed[0].content).toHaveLength(2); expect(parsed[0].content[0]).toEqual({ type: 'input_text', - text: 'Binary data for photo.jpg follows in next message' + text: `The contents of the file 'photo.jpg' are provided below.` }); expect(parsed[0].content[1]).toEqual({ type: 'input_image', @@ -1069,7 +1069,7 @@ describe('OpenAI', () => { expect(parsed[0].content).toHaveLength(2); expect(parsed[0].content[0]).toEqual({ type: 'input_text', - text: 'Binary data for diagram.png follows in next message' + text: `The contents of the file 'diagram.png' are provided below.` }); expect(parsed[0].content[1]).toEqual({ type: 'input_image', @@ -1095,7 +1095,7 @@ describe('OpenAI', () => { expect(parsed[0].content).toHaveLength(2); expect(parsed[0].content[0]).toEqual({ type: 'input_text', - text: 'Binary data for modern.webp follows in next message' + text: `The contents of the file 'modern.webp' are provided below.` }); expect(parsed[0].content[1]).toEqual({ type: 'input_image', @@ -1184,7 +1184,7 @@ describe('OpenAI', () => { expect(parsed[0].content[0]).toEqual({ type: 'input_text', - text: 'Binary data for doc.pdf follows in next message' + text: `The contents of the file 'doc.pdf' are provided below.` }); expect(parsed[0].content[1]).toEqual({ type: 'input_file', @@ -1193,7 +1193,7 @@ describe('OpenAI', () => { expect(parsed[0].content[2]).toEqual({ type: 'input_text', - text: 'Binary data for image.jpg follows in next message' + text: `The contents of the file 'image.jpg' are provided below.` }); expect(parsed[0].content[3]).toEqual({ type: 'input_image', @@ -1202,7 +1202,7 @@ describe('OpenAI', () => { expect(parsed[0].content[4]).toEqual({ type: 'input_text', - text: 'Binary data for screenshot.png follows in next message' + text: `The contents of the file 'screenshot.png' are provided below.` }); expect(parsed[0].content[5]).toEqual({ type: 'input_image', @@ -1249,7 +1249,7 @@ describe('OpenAI', () => { expect(parsed[0].content[0]).toEqual({ type: 'input_text', - text: 'Binary data for good.jpg follows in next message' + text: `The contents of the file 'good.jpg' are provided below.` }); expect(parsed[0].content[1].type).toBe('input_image'); expect(parsed[0].content[2]).toEqual({ @@ -1258,7 +1258,7 @@ describe('OpenAI', () => { }); expect(parsed[0].content[3]).toEqual({ type: 'input_text', - text: 'Binary data for doc.pdf follows in next message' + text: `The contents of the file 'doc.pdf' are provided below.` }); expect(parsed[0].content[4].type).toBe('input_file'); }); @@ -1292,7 +1292,7 @@ describe('OpenAI', () => { expect(parsed[0].content).toHaveLength(2); // Text + file reference expect(parsed[0].content[0]).toEqual({ type: 'input_text', - text: 'Binary data for success.pdf follows in next message' + text: `The contents of the file 'success.pdf' are provided below.` }); expect(parsed[0].content[1]).toEqual({ type: 'input_file', diff --git a/__tests__/Conversations/Conversation.test.ts b/__tests__/Conversations/Conversation.test.ts index eb0e904..8d3c92c 100644 --- a/__tests__/Conversations/Conversation.test.ts +++ b/__tests__/Conversations/Conversation.test.ts @@ -4,6 +4,8 @@ import { ConversationContent } from '../../Conversations/ConversationContent'; import { Role } from '../../Enums/Role'; import { AIToolResponse } from '../../AIClasses/ToolDefinitions/AIToolResponse'; import { AITool } from '../../Enums/AITool'; +import { MimeType } from '../../Enums/MimeType'; +import { StringTools } from '../../Helpers/StringTools'; describe('Conversation', () => { describe('constructor', () => { @@ -544,7 +546,72 @@ describe('Conversation', () => { }); }); - describe('non-read_vault_files functions', () => { + describe('read_vault_files with document files', () => { + it('should base64-encode document file contents and set mime type to text/plain', () => { + const conversation = new Conversation(); + const documentText = 'This is the extracted text from a document file.'; + const functionResponse = new AIToolResponse( + AITool.ReadVaultFiles, + { + results: [ + { path: 'report.odt', type: 'odt', contents: documentText } + ] + }, + 'tool-123' + ); + + conversation.addFunctionResponse(functionResponse); + + // Should have 2 items: function response + attachments + expect(conversation.contents).toHaveLength(2); + expect(conversation.contents[1].attachments).toHaveLength(1); + + const attachment = conversation.contents[1].attachments[0]; + expect(attachment.fileName).toBe('report.odt'); + expect(attachment.mimeType).toBe(MimeType.TEXT_PLAIN); + // Content should be base64-encoded, not plain text + expect(attachment.base64).toBe(StringTools.toBase64(documentText)); + expect(attachment.base64).not.toBe(documentText); + }); + + it('should handle document files alongside text and binary files', () => { + const conversation = new Conversation(); + const functionResponse = new AIToolResponse( + AITool.ReadVaultFiles, + { + results: [ + { path: 'notes.md', type: 'md', contents: '# Notes' }, + { path: 'spreadsheet.xlsx', type: 'xlsx', contents: 'Extracted spreadsheet text' }, + { path: 'photo.png', type: 'png', contents: 'base64imagedata...' } + ] + }, + 'tool-123' + ); + + conversation.addFunctionResponse(functionResponse); + + // Should have 2 items: function response (text) + attachments (document + binary) + expect(conversation.contents).toHaveLength(2); + + // Text file in function response + const parsedResponse = JSON.parse(conversation.contents[0].functionResponse!); + expect(parsedResponse.functionResponse.response.results).toHaveLength(1); + expect(parsedResponse.functionResponse.response.results[0].path).toBe('notes.md'); + + // Document and binary as attachments + const attachments = conversation.contents[1].attachments; + expect(attachments).toHaveLength(2); + + const docAttachment = attachments.find(a => a.fileName === 'spreadsheet.xlsx')!; + expect(docAttachment.mimeType).toBe(MimeType.TEXT_PLAIN); + expect(docAttachment.base64).toBe(StringTools.toBase64('Extracted spreadsheet text')); + + const imgAttachment = attachments.find(a => a.fileName === 'photo.png')!; + expect(imgAttachment.base64).toBe('base64imagedata...'); + }); + }); + + describe('non-read_vault_files functions', () => { it('should handle other functions normally', () => { const conversation = new Conversation(); const functionResponse = new AIToolResponse( diff --git a/__tests__/Services/VaultService.pdf.test.ts b/__tests__/Services/VaultService.pdf.test.ts index 9517bb5..e665a77 100644 --- a/__tests__/Services/VaultService.pdf.test.ts +++ b/__tests__/Services/VaultService.pdf.test.ts @@ -7,7 +7,7 @@ import { SanitiserService } from '../../Services/SanitiserService'; import { SettingsService, type IVaultkeeperAISettings } from '../../Services/SettingsService'; import { AIProviderModel } from '../../Enums/ApiProvider'; import { Exception } from '../../Helpers/Exception'; -import * as PDFHelper from '../../Helpers/PDFHelper'; +import * as PDFHelper from '../../Helpers/DocumentHelper'; import type { IPageText } from '../../Types/SearchTypes'; /** diff --git a/package-lock.json b/package-lock.json index 56daf39..aeaa79b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "highlight.js": "^11.11.1", "katex": "^0.16.33", "lowlight": "^3.3.0", + "officeparser": "^6.0.4", "openai": "^6.25.0", "path-browserify": "^1.0.1", "regex-parser": "^2.3.1", @@ -1002,6 +1003,256 @@ "ret": "~0.1.10" } }, + "node_modules/@napi-rs/canvas": { + "version": "0.1.96", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.96.tgz", + "integrity": "sha512-6NNmNxvoJKeucVjxaaRUt3La2i5jShgiAbaY3G/72s1Vp3U06XPrAIxkAjBxpDcamEn/t+WJ4OOlGmvILo4/Ew==", + "license": "MIT", + "optional": true, + "workspaces": [ + "e2e/*" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/canvas-android-arm64": "0.1.96", + "@napi-rs/canvas-darwin-arm64": "0.1.96", + "@napi-rs/canvas-darwin-x64": "0.1.96", + "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.96", + "@napi-rs/canvas-linux-arm64-gnu": "0.1.96", + "@napi-rs/canvas-linux-arm64-musl": "0.1.96", + "@napi-rs/canvas-linux-riscv64-gnu": "0.1.96", + "@napi-rs/canvas-linux-x64-gnu": "0.1.96", + "@napi-rs/canvas-linux-x64-musl": "0.1.96", + "@napi-rs/canvas-win32-arm64-msvc": "0.1.96", + "@napi-rs/canvas-win32-x64-msvc": "0.1.96" + } + }, + "node_modules/@napi-rs/canvas-android-arm64": { + "version": "0.1.96", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.96.tgz", + "integrity": "sha512-ew1sPrN3dGdZ3L4FoohPfnjq0f9/Jk7o+wP7HkQZokcXgIUD6FIyICEWGhMYzv53j63wUcPvZeAwgewX58/egg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-darwin-arm64": { + "version": "0.1.96", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.96.tgz", + "integrity": "sha512-Q/wOXZ5PzTqpdmA5eUOcegCf4Go/zz3aZ5DlzSeDpOjFmfwMKh8EzLAoweQ+mJVagcHQyzoJhaTEnrO68TNyNg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-darwin-x64": { + "version": "0.1.96", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.96.tgz", + "integrity": "sha512-UrXiQz28tQEvGM1qvyptewOAfmUrrd5+wvi6Rzjj2VprZI8iZ2KIvBD2lTTG1bVF95AbeDeG7PJA0D9sLKaOFA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { + "version": "0.1.96", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.96.tgz", + "integrity": "sha512-I90ODxweD8aEP6XKU/NU+biso95MwCtQ2F46dUvhec1HesFi0tq/tAJkYic/1aBSiO/1kGKmSeD1B0duOHhEHQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-gnu": { + "version": "0.1.96", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.96.tgz", + "integrity": "sha512-Dx/0+RFV++w3PcRy+4xNXkghhXjA5d0Mw1bs95emn5Llinp1vihMaA6WJt3oYv2LAHc36+gnrhIBsPhUyI2SGw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-musl": { + "version": "0.1.96", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.96.tgz", + "integrity": "sha512-UvOi7fii3IE2KDfEfhh8m+LpzSRvhGK7o1eho99M2M0HTik11k3GX+2qgVx9EtujN3/bhFFS1kSO3+vPMaJ0Mg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-riscv64-gnu": { + "version": "0.1.96", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.96.tgz", + "integrity": "sha512-MBSukhGCQ5nRtf9NbFYWOU080yqkZU1PbuH4o1ROvB4CbPl12fchDR35tU83Wz8gWIM9JTn99lBn9DenPIv7Ig==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-gnu": { + "version": "0.1.96", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.96.tgz", + "integrity": "sha512-I/ccu2SstyKiV3HIeVzyBIWfrJo8cN7+MSQZPnabewWV6hfJ2nY7Df2WqOHmobBRUw84uGR6zfQHsUEio/m5Vg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-musl": { + "version": "0.1.96", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.96.tgz", + "integrity": "sha512-H3uov7qnTl73GDT4h52lAqpJPsl1tIUyNPWJyhQ6gHakohNqqRq3uf80+NEpzcytKGEOENP1wX3yGwZxhjiWEQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-win32-arm64-msvc": { + "version": "0.1.96", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-0.1.96.tgz", + "integrity": "sha512-ATp6Y+djOjYtkfV/VRH7CZ8I1MEtkUQBmKUbuWw5zWEHHqfL0cEcInE4Cxgx7zkNAhEdBbnH8HMVrqNp+/gwxA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-win32-x64-msvc": { + "version": "0.1.96", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.96.tgz", + "integrity": "sha512-UYGdTltVd+Z8mcIuoqGmAXXUvwH5CLf2M6mIB5B0/JmX5J041jETjqtSYl7gN+aj3k1by/SG6sS0hAwCqyK7zw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -1671,6 +1922,12 @@ "svelte": "^3 || ^4 || ^5 || ^5.0.0-next.0" } }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, "node_modules/@types/aria-query": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", @@ -2299,12 +2556,33 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.11", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", + "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "license": "ISC" }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -2663,6 +2941,12 @@ "node": "*" } }, + "node_modules/bmp-js": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/bmp-js/-/bmp-js-0.1.0.tgz", + "integrity": "sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw==", + "license": "MIT" + }, "node_modules/body-parser": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", @@ -2700,12 +2984,51 @@ "node": "18 || 20 || >=22" } }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", "license": "BSD-3-Clause" }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, "node_modules/builtin-modules": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-5.0.0.tgz", @@ -2942,6 +3265,21 @@ "dev": true, "license": "MIT" }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, "node_modules/content-disposition": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", @@ -4433,6 +4771,24 @@ "node": ">= 0.6" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/expect-type": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", @@ -4591,6 +4947,23 @@ "node": ">=16.0.0" } }, + "node_modules/file-type": { + "version": "16.5.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-16.5.4.tgz", + "integrity": "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==", + "license": "MIT", + "dependencies": { + "readable-web-to-node-stream": "^3.0.0", + "strtok3": "^6.2.4", + "token-types": "^4.1.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, "node_modules/finalhandler": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", @@ -5374,6 +5747,32 @@ "url": "https://opencollective.com/express" } }, + "node_modules/idb-keyval": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.2.tgz", + "integrity": "sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==", + "license": "Apache-2.0" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", @@ -5858,6 +6257,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-url": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", + "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==", + "license": "MIT" + }, "node_modules/is-weakmap": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", @@ -7593,6 +7998,26 @@ ], "license": "MIT" }, + "node_modules/officeparser": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/officeparser/-/officeparser-6.0.4.tgz", + "integrity": "sha512-zmxst4xbrUxCCY6w5BLmmtewJY5eHkLKNJ+2Z5OKY0JPh5qmtUSslFkg0fC3xc+0RG4DFdfQ36107B99yLMfuQ==", + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.8.10", + "concat-stream": "^2.0.0", + "file-type": "^16.5.4", + "pdfjs-dist": "5.4.530", + "tesseract.js": "^6.0.0", + "yauzl": "^3.1.3" + }, + "bin": { + "officeparser": "dist/index.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -7652,6 +8077,15 @@ } } }, + "node_modules/opencollective-postinstall": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz", + "integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==", + "license": "MIT", + "bin": { + "opencollective-postinstall": "index.js" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -7888,6 +8322,37 @@ "dev": true, "license": "MIT" }, + "node_modules/pdfjs-dist": { + "version": "5.4.530", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.530.tgz", + "integrity": "sha512-r1hWsSIGGmyYUAHR26zSXkxYWLXLMd6AwqcaFYG9YUZ0GBf5GvcjJSeo512tabM4GYFhxhl5pMCmPr7Q72Rq2Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=20.16.0 || >=22.3.0" + }, + "optionalDependencies": { + "@napi-rs/canvas": "^0.1.84" + } + }, + "node_modules/peek-readable": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz", + "integrity": "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -7985,6 +8450,15 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -8107,6 +8581,52 @@ "dev": true, "license": "MIT" }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readable-web-to-node-stream": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.4.tgz", + "integrity": "sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw==", + "license": "MIT", + "dependencies": { + "readable-stream": "^4.7.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/readable-web-to-node-stream/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", @@ -8144,6 +8664,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT" + }, "node_modules/regex": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", @@ -8952,6 +9478,15 @@ "node": ">= 0.4" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -9177,6 +9712,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strtok3": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz", + "integrity": "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "peek-readable": "^4.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/style-mod": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", @@ -9360,6 +9912,50 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/tesseract.js": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tesseract.js/-/tesseract.js-6.0.1.tgz", + "integrity": "sha512-/sPvMvrCtgxnNRCjbTYbr7BRu0yfWDsMZQ2a/T5aN/L1t8wUQN6tTWv6p6FwzpoEBA0jrN2UD2SX4QQFRdoDbA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "bmp-js": "^0.1.0", + "idb-keyval": "^6.2.0", + "is-url": "^1.2.4", + "node-fetch": "^2.6.9", + "opencollective-postinstall": "^2.0.3", + "regenerator-runtime": "^0.13.3", + "tesseract.js-core": "^6.0.0", + "wasm-feature-detect": "^1.2.11", + "zlibjs": "^0.3.1" + } + }, + "node_modules/tesseract.js-core": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/tesseract.js-core/-/tesseract.js-core-6.1.2.tgz", + "integrity": "sha512-pv4GjmramjdObhDyR1q85Td8X60Puu/lGQn7Kw2id05LLgHhAcWgnz6xSdMCSxBMWjQDmMyDXPTC2aqADdpiow==", + "license": "Apache-2.0" + }, + "node_modules/tesseract.js/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -9413,6 +10009,23 @@ "node": ">=0.6" } }, + "node_modules/token-types": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-4.2.1.tgz", + "integrity": "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, "node_modules/toml-eslint-parser": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/toml-eslint-parser/-/toml-eslint-parser-0.9.3.tgz", @@ -9439,6 +10052,12 @@ "node": ">=6" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -9616,6 +10235,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -9837,6 +10462,12 @@ "punycode": "^2.1.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/uuid": { "version": "13.0.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", @@ -10062,6 +10693,12 @@ "license": "MIT", "peer": true }, + "node_modules/wasm-feature-detect": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.8.0.tgz", + "integrity": "sha512-zksaLKM2fVlnB5jQQDqKXXwYHLQUVH9es+5TOOHwGOVJOCeRBCiPjwSg+3tN2AdTCzjgli4jijCH290kXb/zWQ==", + "license": "Apache-2.0" + }, "node_modules/web-namespaces": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", @@ -10081,6 +10718,12 @@ "node": ">= 8" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, "node_modules/whatwg-mimetype": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", @@ -10091,6 +10734,16 @@ "node": ">=12" } }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -10361,6 +11014,19 @@ "url": "https://github.com/sponsors/ota-meshi" } }, + "node_modules/yauzl": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.2.1.tgz", + "integrity": "sha512-k1isifdbpNSFEHFJ1ZY4YDewv0IH9FR61lDetaRMD3j2ae3bIXGV+7c+LHCqtQGofSd8PIyV4X6+dHMAnSr60A==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "pend": "~1.2.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -10381,6 +11047,15 @@ "dev": true, "license": "MIT" }, + "node_modules/zlibjs": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/zlibjs/-/zlibjs-0.3.1.tgz", + "integrity": "sha512-+J9RrgTKOmlxFSDHo0pI1xM6BLVUv+o0ZT9ANtCxGkjIVCCUdx9alUF8Gm+dGLKbkkkidWIHFDZHDMpfITt4+w==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/zod": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", diff --git a/package.json b/package.json index 1ace3fb..33992e7 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "highlight.js": "^11.11.1", "katex": "^0.16.33", "lowlight": "^3.3.0", + "officeparser": "^6.0.4", "openai": "^6.25.0", "path-browserify": "^1.0.1", "regex-parser": "^2.3.1",