F005 - Document Upload System
Objective
Enable authors to upload manuscripts (PDF, DOCX, TXT) with drag-and-drop interface, progress tracking, and secure storage using MyStoryFlow components in Backblaze B2.
Quick Implementation
Using MyStoryFlow Components
FileUploadercomponent from@mystoryflow/ui- Progress tracking with
@mystoryflow/uicomponents - Backblaze B2 storage integration via
@mystoryflow/shared - Upload API routes in
apps/analyzer-app
New Requirements
- File type validation (PDF, DOCX, TXT only)
- Size limit enforcement (10MB max)
- Manuscript metadata extraction
- Upload session management
MVP Implementation
1. Database Schema
-- Manuscripts table (in analyzer schema)
CREATE TABLE analyzer.manuscripts (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID REFERENCES auth.users(id),
title VARCHAR(255) NOT NULL,
author_name VARCHAR(255),
file_name VARCHAR(255) NOT NULL,
file_size INTEGER NOT NULL,
file_type VARCHAR(50) NOT NULL,
storage_path TEXT NOT NULL,
word_count INTEGER,
status VARCHAR(50) DEFAULT 'uploaded',
metadata JSONB DEFAULT '{}',
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- Upload sessions for progress tracking
CREATE TABLE analyzer.upload_sessions (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID REFERENCES users(id),
manuscript_id UUID REFERENCES manuscripts(id),
progress INTEGER DEFAULT 0,
status VARCHAR(50) DEFAULT 'uploading',
error_message TEXT,
created_at TIMESTAMP DEFAULT NOW()
);
-- Indexes
CREATE INDEX idx_manuscripts_user_id ON manuscripts(user_id);
CREATE INDEX idx_manuscripts_status ON manuscripts(status);2. Enhanced File Uploader Component
// apps/analyzer-app/src/components/manuscript/ManuscriptUploader.tsx
'use client'
import { useState } from 'react'
import { FileUploader, Progress, Alert } from '@mystoryflow/ui'
import { uploadManuscript } from '@/lib/manuscript-upload'
import { trackAIUsage } from '@mystoryflow/analytics'
const ALLOWED_TYPES = [
'application/pdf',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'text/plain'
]
const MAX_FILE_SIZE = 10 * 1024 * 1024 // 10MB
export function ManuscriptUploader({ onSuccess }: { onSuccess: (id: string) => void }) {
const [uploading, setUploading] = useState(false)
const [progress, setProgress] = useState(0)
const [error, setError] = useState<string | null>(null)
const handleUpload = async (file: File) => {
// Validate file type
if (!ALLOWED_TYPES.includes(file.type)) {
setError('Please upload a PDF, DOCX, or TXT file')
return
}
// Validate file size
if (file.size > MAX_FILE_SIZE) {
setError('File size must be less than 10MB')
return
}
setUploading(true)
setError(null)
setProgress(0)
try {
const manuscriptId = await uploadManuscript(file, {
onProgress: (percent) => setProgress(percent)
})
onSuccess(manuscriptId)
} catch (err) {
setError(err instanceof Error ? err.message : 'Upload failed')
} finally {
setUploading(false)
}
}
return (
<div className="space-y-4">
<FileUploader
accept=".pdf,.docx,.txt"
onUpload={handleUpload}
disabled={uploading}
description="Upload your manuscript (PDF, DOCX, or TXT - Max 10MB)"
/>
{uploading && (
<div className="space-y-2">
<Progress value={progress} />
<p className="text-sm text-muted-foreground">
Uploading manuscript... {progress}%
</p>
</div>
)}
{error && (
<Alert variant="error">
{error}
</Alert>
)}
</div>
)
}3. Upload Service
// packages/manuscript-analysis/src/services/upload-service.ts
import { getSupabaseBrowserClient } from '@mystoryflow/database'
import { uploadToB2 } from '@mystoryflow/shared/storage'
interface UploadOptions {
onProgress?: (percent: number) => void
}
export async function uploadManuscript(
file: File,
options: UploadOptions = {}
): Promise<string> {
const supabase = getSupabaseBrowserClient()
// Get current user
const { data: { user } } = await supabase.auth.getUser()
if (!user) throw new Error('Not authenticated')
// Create manuscript record
const { data: manuscript, error: dbError } = await supabase
.from('analyzer.manuscripts')
.insert({
user_id: user.id,
title: file.name.replace(/\.[^/.]+$/, ''), // Remove extension
file_name: file.name,
file_size: file.size,
file_type: file.type,
status: 'uploading'
})
.select()
.single()
if (dbError) throw dbError
// Upload to Backblaze B2 with progress tracking
const storagePath = `manuscripts/${user.id}/${manuscript.id}/${file.name}`
// Create upload session
await supabase.from('analyzer.upload_sessions').insert({
user_id: user.id,
manuscript_id: manuscript.id
})
try {
// Upload file to Backblaze B2
const { error: uploadError } = await uploadToB2({
bucket: 'story-analyzer',
path: storagePath,
file,
onProgress: (percent) => {
options.onProgress?.(percent)
// Update session progress
supabase.from('analyzer.upload_sessions')
.update({ progress: percent })
.eq('manuscript_id', manuscript.id)
.then(() => {})
}
})
if (uploadError) throw uploadError
// Update manuscript with storage path
await supabase
.from('analyzer.manuscripts')
.update({
storage_path: storagePath,
status: 'uploaded'
})
.eq('id', manuscript.id)
// Mark session complete
await supabase
.from('analyzer.upload_sessions')
.update({ status: 'completed', progress: 100 })
.eq('manuscript_id', manuscript.id)
// Track storage operation for analytics
await trackAIUsage({
userId: user.id,
operation: 'manuscript-upload',
metadata: { fileSize: file.size, fileType: file.type }
})
return manuscript.id
} catch (error) {
// Mark as failed
await supabase
.from('analyzer.manuscripts')
.update({ status: 'failed' })
.eq('id', manuscript.id)
await supabase
.from('analyzer.upload_sessions')
.update({
status: 'failed',
error_message: error instanceof Error ? error.message : 'Unknown error'
})
.eq('manuscript_id', manuscript.id)
throw error
}
}4. API Route
// apps/analyzer-app/src/app/api/manuscripts/upload/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { withAuth } from '@mystoryflow/auth'
export async function POST(req: NextRequest) {
const session = await withAuth(req)
if (!session) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
try {
const formData = await req.formData()
const file = formData.get('file') as File
if (!file) {
return NextResponse.json({ error: 'No file provided' }, { status: 400 })
}
// File validation is handled in the upload service
// This endpoint is mainly for future enhancements
return NextResponse.json({
message: 'Use client-side upload for better progress tracking'
})
} catch (error) {
return NextResponse.json(
{ error: 'Upload failed' },
{ status: 500 }
)
}
}5. Backblaze B2 Storage Configuration
// Backblaze B2 bucket configuration
// Bucket name: story-analyzer
// Access: Private
// File structure: manuscripts/{user_id}/{manuscript_id}/{filename}
// Environment variables needed:
// B2_APPLICATION_KEY_ID
// B2_APPLICATION_KEY
// B2_BUCKET_NAME=story-analyzerMVP Acceptance Criteria
- Drag-and-drop file upload interface
- Support for PDF, DOCX, TXT formats
- 10MB file size limit
- Real-time progress tracking
- Secure storage in Backblaze B2
- AI usage tracking for storage operations
- Error handling and validation
- Mobile-responsive upload UI
Post-MVP Enhancements
- Multiple file upload support
- Google Docs integration
- Dropbox/OneDrive import
- Automatic metadata extraction
- Virus scanning
- Larger file size support
- Resume interrupted uploads
Implementation Time
- Development: 1 day
- Testing: 0.5 days
- Total: 1.5 days
Dependencies
- F004-AI-SERVICES must be completed (for future processing)
- Backblaze B2 bucket must be configured
- @mystoryflow/shared storage utilities must be available
Next Feature
After completion, proceed to F006-CONTENT-EXTRACTION to extract text from uploaded files.