Introduce end-to-end chapterized audiobook generation with persistent storage, resumable workflows, and MP3/M4B support. API: - add /api/audio/convert/chapter (GET/DELETE) for per-chapter ops - add /api/audio/convert/chapters (GET/DELETE) for listing/reset - enhance /api/audio/convert: - accept mp3|m4b, stream combined file, cache complete output - robust chapter indexing, docstore persistence, list concat - AbortSignal-aware ffmpeg/ffprobe, 499 on cancel UI/UX: - add AudiobookExportModal with progress, resume, regenerate, download - add ProgressCard, enhance ProgressPopup (click-to-focus, richer info) - add Header, ZoomControl; move TTSPlayer to sticky bottom bar - redesign EPUB/HTML/PDF pages for full-height layout and controls - add HomeContent; compact uploader variant; document list polish TTS/Contexts: - EPUB/PDF contexts now generate per-chapter to disk, return bookId - support chapter regeneration; progress/cancel propagation - pass provider/model/instructions; standardize MP3 TTS output - HTML context updates for model/instructions Styling: - globals: overlay-dim, scrollbar styles, prism gradient utilities - theme vars: secondary-accent, prism-gradient; tailwind color addition Misc: - fix PDF scale calc to use container height - EPUB theme reader area fills height - time estimation update cadence stab - audio util passes format to combine endpoint
138 lines
5.1 KiB
TypeScript
138 lines
5.1 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useCallback } from 'react';
|
|
import { useDropzone } from 'react-dropzone';
|
|
import { UploadIcon } from '@/components/icons/Icons';
|
|
import { useDocuments } from '@/contexts/DocumentContext';
|
|
|
|
const isDev = process.env.NEXT_PUBLIC_NODE_ENV !== 'production' || process.env.NODE_ENV == null;
|
|
|
|
interface DocumentUploaderProps {
|
|
className?: string;
|
|
variant?: 'default' | 'compact';
|
|
}
|
|
|
|
export function DocumentUploader({ className = '', variant = 'default' }: DocumentUploaderProps) {
|
|
const {
|
|
addPDFDocument: addPDF,
|
|
addEPUBDocument: addEPUB,
|
|
addHTMLDocument: addHTML
|
|
} = useDocuments();
|
|
const [isUploading, setIsUploading] = useState(false);
|
|
const [isConverting, setIsConverting] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const convertDocxToPdf = async (file: File): Promise<File> => {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
|
|
const response = await fetch('/api/documents/docx-to-pdf', {
|
|
method: 'POST',
|
|
body: formData,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Failed to convert DOCX to PDF');
|
|
}
|
|
|
|
const pdfBlob = await response.blob();
|
|
return new File([pdfBlob], file.name.replace(/\.docx$/, '.pdf'), {
|
|
type: 'application/pdf',
|
|
});
|
|
};
|
|
|
|
const onDrop = useCallback(async (acceptedFiles: File[]) => {
|
|
if (!acceptedFiles || acceptedFiles.length === 0) return;
|
|
|
|
setIsUploading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
for (const file of acceptedFiles) {
|
|
if (file.type === 'application/pdf') {
|
|
await addPDF(file);
|
|
} else if (file.type === 'application/epub+zip') {
|
|
await addEPUB(file);
|
|
} else if (file.type === 'text/plain' || file.type === 'text/markdown' || file.name.endsWith('.md')) {
|
|
await addHTML(file);
|
|
} else if (isDev && file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') {
|
|
setIsUploading(false);
|
|
setIsConverting(true);
|
|
const pdfFile = await convertDocxToPdf(file);
|
|
await addPDF(pdfFile);
|
|
setIsConverting(false);
|
|
setIsUploading(true);
|
|
}
|
|
}
|
|
} catch (err) {
|
|
setError('Failed to upload file. Please try again.');
|
|
console.error('Upload error:', err);
|
|
} finally {
|
|
setIsUploading(false);
|
|
setIsConverting(false);
|
|
}
|
|
}, [addHTML, addPDF, addEPUB]);
|
|
|
|
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
|
onDrop,
|
|
accept: {
|
|
'application/pdf': ['.pdf'],
|
|
'application/epub+zip': ['.epub'],
|
|
'text/plain': ['.txt'],
|
|
'text/markdown': ['.md'],
|
|
...(isDev ? {
|
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx']
|
|
} : {})
|
|
},
|
|
multiple: true,
|
|
disabled: isUploading || isConverting
|
|
});
|
|
|
|
const containerBase = `w-full border-2 border-dashed rounded-lg ${isDragActive ? 'border-accent bg-base' : 'border-muted'} transform transition-transform duration-200 ease-in-out ${(isUploading || isConverting) ? 'cursor-not-allowed opacity-50' : 'cursor-pointer hover:border-accent hover:bg-base hover:scale-[1.008]'} ${className}`;
|
|
const paddingClass = variant === 'compact' ? 'py-1.5 px-2' : 'py-5 px-3';
|
|
|
|
return (
|
|
<div
|
|
{...getRootProps()}
|
|
className={`${containerBase} ${paddingClass}`}
|
|
>
|
|
<input {...getInputProps()} />
|
|
{variant === 'compact' ? (
|
|
<div className="flex items-center gap-2 text-left">
|
|
<UploadIcon className="w-5 h-5 text-muted" />
|
|
{isUploading ? (
|
|
<p className="text-xs font-medium text-foreground">Uploading…</p>
|
|
) : isConverting ? (
|
|
<p className="text-xs font-medium text-foreground">Converting DOCX…</p>
|
|
) : (
|
|
<div className="flex items-center gap-2">
|
|
<p className="text-xs font-medium text-foreground">
|
|
{isDragActive ? 'Drop files here' : 'Drop files or click'}
|
|
</p>
|
|
{error && <p className="text-xs text-red-500">{error}</p>}
|
|
</div>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<div className="flex flex-col items-center justify-center text-center">
|
|
<UploadIcon className="w-7 h-7 sm:w-10 sm:h-10 mb-2 text-muted" />
|
|
{isUploading ? (
|
|
<p className="text-sm sm:text-lg font-semibold text-foreground">Uploading file...</p>
|
|
) : isConverting ? (
|
|
<p className="text-sm sm:text-lg font-semibold text-foreground">Converting DOCX to PDF...</p>
|
|
) : (
|
|
<>
|
|
<p className="mb-2 text-sm sm:text-lg font-semibold text-foreground">
|
|
{isDragActive ? 'Drop your file(s) here' : 'Drop your file(s) here, or click to select'}
|
|
</p>
|
|
<p className="text-xs sm:text-sm text-muted">
|
|
{isDev ? 'PDF, EPUB, TXT, MD, or DOCX files are accepted' : 'PDF, EPUB, TXT, or MD files are accepted'}
|
|
</p>
|
|
{error && <p className="mt-2 text-sm text-red-500">{error}</p>}
|
|
</>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|