feat: add PDF attachment support using unpdf, extend attachment handling and runtime to process and extract text from PDFs

This commit is contained in:
shine1i 2026-02-02 16:11:39 +01:00
commit 3a15b915fc
4 changed files with 54 additions and 6 deletions

View file

@ -51,6 +51,7 @@
"tailwindcss": "^4.1.17",
"tw-animate-css": "^1.4.0",
"tw-shimmer": "^0.4.4",
"unpdf": "^1.4.0",
"zustand": "^5.0.10",
},
"devDependencies": {
@ -1809,6 +1810,8 @@
"universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
"unpdf": ["unpdf@1.4.0", "", { "peerDependencies": { "@napi-rs/canvas": "^0.1.69" }, "optionalPeers": ["@napi-rs/canvas"] }, "sha512-TahIk0xdH/4jh/MxfclzU79g40OyxtP00VnEUZdEkJoYtXAHWLiir6t3FC6z3vDqQTzc2ZHcla6uEiVTNjejuA=="],
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
"until-async": ["until-async@3.0.2", "", {}, "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw=="],

View file

@ -59,6 +59,7 @@
"tailwindcss": "^4.1.17",
"tw-animate-css": "^1.4.0",
"tw-shimmer": "^0.4.4",
"unpdf": "^1.4.0",
"zustand": "^5.0.10"
},
"devDependencies": {

View file

@ -6,13 +6,21 @@ type RunMessages = Parameters<ChatModelAdapter["run"]>[0]["messages"];
function makeBody(messages: RunMessages): string {
return JSON.stringify({
messages: messages.map((m) => ({
role: m.role,
content: m.content
messages: messages.map((m) => {
const textParts = m.content
.filter((c) => c.type === "text")
.map((c) => c.text)
.join(""),
})),
.map((c) => c.text);
if ("attachments" in m && m.attachments?.length) {
for (const att of m.attachments) {
for (const part of att.content ?? []) {
if (part.type === "text") textParts.push(part.text);
}
}
}
return { role: m.role, content: textParts.join("\n") };
}),
});
}

View file

@ -1,8 +1,11 @@
import {
type AttachmentAdapter,
AssistantRuntimeProvider,
type CompleteAttachment,
CompositeAttachmentAdapter,
ExportedMessageRepository,
type ExportedMessageRepositoryItem,
type PendingAttachment,
RuntimeAdapterProvider,
SimpleImageAttachmentAdapter,
SimpleTextAttachmentAdapter,
@ -19,10 +22,42 @@ import {
} from "@assistant-ui/react";
import { createAssistantStream } from "assistant-stream";
import { type ReactElement, type ReactNode, useEffect, useMemo } from "react";
import { extractText, getDocumentProxy } from "unpdf";
import { createStreamAdapter } from "./adapter";
import { db } from "./db";
import type { MessageRecord, ModelType } from "./types";
class PDFAttachmentAdapter implements AttachmentAdapter {
accept = "application/pdf";
async add({ file }: { file: File }): Promise<PendingAttachment> {
return {
id: crypto.randomUUID(),
type: "document",
name: file.name,
contentType: file.type,
file,
status: { type: "requires-action", reason: "composer-send" },
};
}
async send(attachment: PendingAttachment): Promise<CompleteAttachment> {
const buffer = new Uint8Array(await attachment.file.arrayBuffer());
const pdf = await getDocumentProxy(buffer);
const { text } = await extractText(pdf, { mergePages: true });
return {
id: attachment.id,
type: "document",
name: attachment.name,
contentType: attachment.contentType,
content: [{ type: "text", text: `[PDF: ${attachment.name}]\n${text}` }],
status: { type: "complete" },
};
}
async remove(): Promise<void> {}
}
function toThreadMessage(m: MessageRecord): ThreadMessage {
const base = {
id: m.id,
@ -200,6 +235,7 @@ function ThreadHistoryProvider({
new CompositeAttachmentAdapter([
new SimpleImageAttachmentAdapter(),
new SimpleTextAttachmentAdapter(),
new PDFAttachmentAdapter(),
]),
[],
);