sync: PDF file upload from private repo

- Add drag-and-drop PDF upload to Quick Capture
- New upload endpoint with size limits (warn 10MB, reject 50MB)
- Add extractFromBuffer method to PaperExtractor
- Visual feedback: drag-over highlight, file preview card

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
“BeeRad”
2026-01-14 20:13:13 +11:00
co-authored by Claude Opus 4.5
parent 2cbc950fd4
commit 419d29dbe8
3 changed files with 427 additions and 53 deletions
+104
View File
@@ -0,0 +1,104 @@
import { NextRequest, NextResponse } from 'next/server';
import { PaperExtractor } from '@/services/typescript/extractors/paper';
export const runtime = 'nodejs';
// Size limits in bytes
const WARN_SIZE = 10 * 1024 * 1024; // 10MB
const MAX_SIZE = 50 * 1024 * 1024; // 50MB
export async function POST(request: NextRequest) {
try {
const formData = await request.formData();
const file = formData.get('file');
// Validate file presence
if (!file || !(file instanceof File)) {
return NextResponse.json(
{ success: false, error: 'PDF file is required' },
{ status: 400 }
);
}
// Validate MIME type
if (file.type !== 'application/pdf') {
return NextResponse.json(
{ success: false, error: `Invalid file type: ${file.type}. Only PDF files are accepted.` },
{ status: 400 }
);
}
// Check file size
if (file.size > MAX_SIZE) {
return NextResponse.json(
{ success: false, error: `File too large (${Math.round(file.size / 1024 / 1024)}MB). Maximum size is 50MB.` },
{ status: 413 }
);
}
const isLargeFile = file.size > WARN_SIZE;
// Get buffer from file
const buffer = Buffer.from(await file.arrayBuffer());
// Extract PDF content using PaperExtractor
const extractor = new PaperExtractor();
const extraction = await extractor.extractFromBuffer(buffer, file.name);
// Derive title from metadata or filename
const title = extraction.metadata.title || file.name.replace(/\.pdf$/i, '');
// Create node via internal API call
// IMPORTANT: Use request.url origin for packaged Tauri app compatibility
const nodeApiUrl = new URL('/api/nodes', request.url);
const createResponse = await fetch(nodeApiUrl.toString(), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title,
type: 'pdf',
// Keep content readable in UI (not the full extracted text)
content: `Imported PDF: ${file.name} (${extraction.metadata.pages} pages, ${Math.round(extraction.metadata.text_length / 1000)}k characters)`,
// Full extracted text goes in chunk for universal chunking/embedding
chunk: extraction.chunk,
metadata: {
source: 'pdf_upload',
original_filename: file.name,
pages: extraction.metadata.pages,
text_length: extraction.metadata.text_length,
extraction_method: extraction.metadata.extraction_method,
imported_at: new Date().toISOString(),
},
}),
});
if (!createResponse.ok) {
const errorData = await createResponse.json().catch(() => ({}));
throw new Error(errorData.error || `Failed to create node: ${createResponse.status}`);
}
const nodeResult = await createResponse.json();
if (!nodeResult.success || !nodeResult.data?.id) {
throw new Error(nodeResult.error || 'Failed to create node');
}
return NextResponse.json({
success: true,
nodeId: nodeResult.data.id,
title,
pages: extraction.metadata.pages,
textLength: extraction.metadata.text_length,
warning: isLargeFile ? `Large file (${Math.round(file.size / 1024 / 1024)}MB) - processing may take longer` : undefined,
});
} catch (error) {
console.error('[PDF Upload API] Error:', error);
const message = error instanceof Error ? error.message : 'Failed to process PDF upload';
return NextResponse.json(
{ success: false, error: message },
{ status: 500 }
);
}
}