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",