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."
This commit is contained in:
Andrew Beal 2026-03-15 19:48:28 +00:00
parent 562ab8d036
commit a4c65f16c2
21 changed files with 919 additions and 61 deletions

View file

@ -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: {

View file

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

View file

@ -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

View file

@ -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

View file

@ -250,8 +250,10 @@
{#if message.role === Role.User}
<div class="message-container {Role.User}" use:trackingAction={index}>
<div class="message-bubble {Role.User}">
<div class="message-text-user content-fade-in" contenteditable="false">
{@html content}
<div class="message-text-user-container content-fade-in" contenteditable="false">
<div class="message-text-user">
{@html content}
</div>
</div>
{#if message.references.length > 0}
<hr class="message-attachment-break"/>
@ -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;

View file

@ -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);
});

View file

@ -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) {

View file

@ -143,6 +143,14 @@ export const FileTypeToMimeType: Record<FileType, MimeType> = {
[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, FileType[]> = {
[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]
};

View file

@ -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 ||

View file

@ -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<IPageText[]> {
try {
const pdf = await getDocumentProxy(new Uint8Array(arrayBuffer));
@ -27,4 +29,20 @@ export async function readPDF(arrayBuffer: ArrayBuffer): Promise<IPageText[]> {
}
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<IPageText[]> {
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[];
}
}

View file

@ -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<ArrayBuffer> {
const binaryString = atob(input);
const bytes = new Uint8Array(binaryString.length);

View file

@ -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())
));
}
}
}

View file

@ -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[];
}

View file

@ -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', () => {

View file

@ -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', () => {

View file

@ -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'

View file

@ -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',

View file

@ -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(

View file

@ -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';
/**

675
package-lock.json generated
View file

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

View file

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