Use indexed db
This commit is contained in:
parent
109d43f585
commit
355e4ebe29
9 changed files with 492 additions and 120 deletions
23
package-lock.json
generated
23
package-lock.json
generated
|
|
@ -10,6 +10,7 @@
|
|||
"dependencies": {
|
||||
"@headlessui/react": "^2.2.0",
|
||||
"@types/string-similarity": "^4.0.2",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"compromise": "^14.14.4",
|
||||
"lru-cache": "^11.0.2",
|
||||
"next": "15.1.5",
|
||||
|
|
@ -19,7 +20,8 @@
|
|||
"react-dom": "^19.0.0",
|
||||
"react-dropzone": "^14.3.5",
|
||||
"react-pdf": "^9.2.1",
|
||||
"string-similarity": "^4.0.4"
|
||||
"string-similarity": "^4.0.4",
|
||||
"uuid": "^11.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
|
|
@ -1165,6 +1167,12 @@
|
|||
"integrity": "sha512-LkJQ/jsXtCVMK+sKYAmX/8zEq+/46f1PTQw7YtmQwb74jemS1SlNLmARM2Zml9DgdDTWKAtc5L13WorpHPDjDA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/uuid": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
|
||||
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.20.0.tgz",
|
||||
|
|
@ -6751,6 +6759,19 @@
|
|||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "11.0.5",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.5.tgz",
|
||||
"integrity": "sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/esm/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/warning": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
"dependencies": {
|
||||
"@headlessui/react": "^2.2.0",
|
||||
"@types/string-similarity": "^4.0.2",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"compromise": "^14.14.4",
|
||||
"lru-cache": "^11.0.2",
|
||||
"next": "15.1.5",
|
||||
|
|
@ -20,7 +21,8 @@
|
|||
"react-dom": "^19.0.0",
|
||||
"react-dropzone": "^14.3.5",
|
||||
"react-pdf": "^9.2.1",
|
||||
"string-similarity": "^4.0.4"
|
||||
"string-similarity": "^4.0.4",
|
||||
"uuid": "^11.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
|
|
|
|||
|
|
@ -4,13 +4,53 @@ import { PDFViewer } from '@/components/PDFViewer';
|
|||
import { usePDF } from '@/context/PDFContext';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { PDFSkeleton } from '@/components/PDFSkeleton';
|
||||
|
||||
export default function PDFViewerPage() {
|
||||
const { id } = useParams();
|
||||
const { getDocument } = usePDF();
|
||||
const router = useRouter();
|
||||
const [document, setDocument] = useState<{ name: string; data: Blob } | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const document = getDocument(id as string);
|
||||
useEffect(() => {
|
||||
async function loadDocument() {
|
||||
try {
|
||||
const doc = await getDocument(id as string);
|
||||
if (!doc) {
|
||||
setError('Document not found');
|
||||
return;
|
||||
}
|
||||
setDocument(doc);
|
||||
} catch (err) {
|
||||
console.error('Error loading document:', err);
|
||||
setError('Failed to load document');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
loadDocument();
|
||||
}, [id, getDocument]);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-screen">
|
||||
<p className="text-red-500 mb-4">{error}</p>
|
||||
<Link
|
||||
href="/"
|
||||
className="inline-flex items-center px-3 py-1 bg-base text-foreground rounded-lg hover:bg-offbase transition-colors"
|
||||
>
|
||||
<svg className="w-4 h-4 mr-2 text-muted" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||||
</svg>
|
||||
Back to Documents
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -25,10 +65,18 @@ export default function PDFViewerPage() {
|
|||
</svg>
|
||||
Documents
|
||||
</Link>
|
||||
<h1 className="mr-2 text-xl font-semibold text-foreground">{document?.name}</h1>
|
||||
<h1 className="mr-2 text-xl font-semibold text-foreground">
|
||||
{isLoading ? 'Loading...' : document?.name}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
<PDFViewer pdfFile={document?.data} />
|
||||
{isLoading ? (
|
||||
<div className="p-4">
|
||||
<PDFSkeleton />
|
||||
</div>
|
||||
) : (
|
||||
<PDFViewer pdfData={document?.data} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,23 @@ import { usePDF } from '@/context/PDFContext';
|
|||
import Link from 'next/link';
|
||||
|
||||
export function DocumentList() {
|
||||
const { documents, removeDocument } = usePDF();
|
||||
const { documents, removeDocument, isLoading, error } = usePDF();
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="w-full text-center text-muted">
|
||||
Loading documents...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="w-full text-center text-red-500">
|
||||
{error}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (documents.length === 0) {
|
||||
return (
|
||||
|
|
@ -30,17 +46,23 @@ export function DocumentList() {
|
|||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<h3 className="font-medium text-foreground truncate">{doc.name}</h3>
|
||||
<p className="text-sm text-muted">
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-foreground font-medium truncate">{doc.name}</p>
|
||||
<p className="text-sm text-muted truncate">
|
||||
{(doc.size / 1024 / 1024).toFixed(2)} MB
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => removeDocument(doc.id)}
|
||||
className="p-2 text-muted hover:text-accent transition-colors flex-shrink-0 ml-2"
|
||||
title="Remove document"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await removeDocument(doc.id);
|
||||
} catch (err) {
|
||||
console.error('Failed to remove document:', err);
|
||||
}
|
||||
}}
|
||||
className="ml-4 p-2 text-muted hover:text-red-500 rounded-lg hover:bg-red-50 transition-colors"
|
||||
aria-label="Delete document"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
|
|
|
|||
|
|
@ -9,22 +9,33 @@ interface PDFUploaderProps {
|
|||
}
|
||||
|
||||
export function PDFUploader({ className = '' }: PDFUploaderProps) {
|
||||
const { addDocument } = usePDF();
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const { addDocument, error: contextError } = usePDF();
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const onDrop = useCallback(async (acceptedFiles: File[]) => {
|
||||
const file = acceptedFiles[0];
|
||||
if (file && file.type === 'application/pdf') {
|
||||
await addDocument(file);
|
||||
setIsUploading(true);
|
||||
setError(null);
|
||||
try {
|
||||
await addDocument(file);
|
||||
} catch (err) {
|
||||
setError(contextError || 'Failed to upload PDF. Please try again.');
|
||||
console.error('Upload error:', err);
|
||||
} finally {
|
||||
setIsUploading(false);
|
||||
}
|
||||
}
|
||||
}, [addDocument]);
|
||||
}, [addDocument, contextError]);
|
||||
|
||||
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
||||
onDrop,
|
||||
accept: {
|
||||
'application/pdf': ['.pdf']
|
||||
},
|
||||
multiple: false
|
||||
multiple: false,
|
||||
disabled: isUploading
|
||||
});
|
||||
|
||||
return (
|
||||
|
|
@ -33,8 +44,8 @@ export function PDFUploader({ className = '' }: PDFUploaderProps) {
|
|||
className={`
|
||||
w-full p-8 border-2 border-dashed rounded-lg
|
||||
${isDragActive ? 'border-accent bg-base' : 'border-muted'}
|
||||
transition-colors duration-200 ease-in-out cursor-pointer
|
||||
hover:border-accent hover:bg-base
|
||||
transition-colors duration-200 ease-in-out
|
||||
${isUploading ? 'cursor-not-allowed opacity-50' : 'cursor-pointer hover:border-accent hover:bg-base'}
|
||||
${className}
|
||||
`}
|
||||
>
|
||||
|
|
@ -55,12 +66,21 @@ export function PDFUploader({ className = '' }: PDFUploaderProps) {
|
|||
/>
|
||||
</svg>
|
||||
|
||||
<p className="mb-2 text-lg font-semibold text-foreground">
|
||||
Drop your PDF here, or click to select
|
||||
</p>
|
||||
<p className="text-sm text-muted">
|
||||
Only PDF files are accepted
|
||||
</p>
|
||||
{isUploading ? (
|
||||
<p className="text-lg font-semibold text-foreground">
|
||||
Uploading PDF...
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
<p className="mb-2 text-lg font-semibold text-foreground">
|
||||
{isDragActive ? 'Drop your PDF here' : 'Drop your PDF here, or click to select'}
|
||||
</p>
|
||||
<p className="text-sm text-muted">
|
||||
Only PDF files are accepted
|
||||
</p>
|
||||
{error && <p className="mt-2 text-sm text-red-500">{error}</p>}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
import { Document, Page, pdfjs } from 'react-pdf';
|
||||
import 'react-pdf/dist/Page/AnnotationLayer.css';
|
||||
import 'react-pdf/dist/Page/TextLayer.css';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { PDFSkeleton } from './PDFSkeleton';
|
||||
import { useTTS } from '@/context/TTSContext';
|
||||
import stringSimilarity from 'string-similarity';
|
||||
|
|
@ -12,7 +12,7 @@ import stringSimilarity from 'string-similarity';
|
|||
pdfjs.GlobalWorkerOptions.workerSrc = '/pdf.worker.mjs';
|
||||
|
||||
interface PDFViewerProps {
|
||||
pdfFile: string | undefined;
|
||||
pdfData: Blob | undefined;
|
||||
}
|
||||
|
||||
interface TextHighlight {
|
||||
|
|
@ -23,11 +23,98 @@ interface TextHighlight {
|
|||
};
|
||||
}
|
||||
|
||||
export function PDFViewer({ pdfFile }: PDFViewerProps) {
|
||||
export function PDFViewer({ pdfData }: PDFViewerProps) {
|
||||
const [numPages, setNumPages] = useState<number>();
|
||||
const { setText, currentSentence } = useTTS();
|
||||
const [pdfText, setPdfText] = useState('');
|
||||
const [highlights, setHighlights] = useState<TextHighlight[]>([]);
|
||||
const [pdfDataUrl, setPdfDataUrl] = useState<string>();
|
||||
const [loadingError, setLoadingError] = useState<string>();
|
||||
|
||||
// Convert Blob to data URL when pdfData changes
|
||||
useEffect(() => {
|
||||
if (!pdfData) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
setPdfDataUrl(reader.result as string);
|
||||
};
|
||||
reader.onerror = () => {
|
||||
console.error('Error reading file:', reader.error);
|
||||
setLoadingError('Failed to load PDF');
|
||||
};
|
||||
reader.readAsDataURL(pdfData);
|
||||
|
||||
return () => {
|
||||
setPdfDataUrl(undefined);
|
||||
};
|
||||
}, [pdfData]);
|
||||
|
||||
// Load PDF text content
|
||||
useEffect(() => {
|
||||
if (!pdfDataUrl) return;
|
||||
|
||||
let isCurrentPdf = true;
|
||||
let currentLoadingTask: any = null;
|
||||
setLoadingError(undefined);
|
||||
|
||||
const loadPdfText = async () => {
|
||||
try {
|
||||
// Create a typed array from the base64 data
|
||||
const base64Data = pdfDataUrl.split(',')[1];
|
||||
const binaryData = atob(base64Data);
|
||||
const length = binaryData.length;
|
||||
const bytes = new Uint8Array(length);
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
bytes[i] = binaryData.charCodeAt(i);
|
||||
}
|
||||
|
||||
const loadingTask = pdfjs.getDocument({
|
||||
data: bytes,
|
||||
disableAutoFetch: true,
|
||||
disableStream: false,
|
||||
});
|
||||
|
||||
currentLoadingTask = loadingTask;
|
||||
const pdf = await loadingTask.promise;
|
||||
|
||||
if (!isCurrentPdf) return;
|
||||
|
||||
let fullText = '';
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
if (!isCurrentPdf) break;
|
||||
|
||||
const page = await pdf.getPage(i);
|
||||
const textContent = await page.getTextContent();
|
||||
const pageText = textContent.items
|
||||
.map((item: any) => item.str)
|
||||
.join(' ');
|
||||
fullText += pageText + ' ';
|
||||
}
|
||||
|
||||
if (!isCurrentPdf) return;
|
||||
|
||||
console.log('Loaded PDF text sample:', fullText.substring(0, 100));
|
||||
setPdfText(fullText);
|
||||
setText(fullText);
|
||||
} catch (error) {
|
||||
if (!isCurrentPdf) return;
|
||||
console.error('Error loading PDF text:', error);
|
||||
setLoadingError('Failed to extract PDF text');
|
||||
}
|
||||
};
|
||||
|
||||
loadPdfText();
|
||||
|
||||
return () => {
|
||||
isCurrentPdf = false;
|
||||
if (currentLoadingTask) {
|
||||
currentLoadingTask.destroy();
|
||||
}
|
||||
setPdfText('');
|
||||
};
|
||||
}, [pdfDataUrl, setText]);
|
||||
|
||||
// Function to clear all highlights
|
||||
const clearHighlights = useCallback(() => {
|
||||
|
|
@ -137,12 +224,12 @@ export function PDFViewer({ pdfFile }: PDFViewerProps) {
|
|||
const lengthPenalty = lengthDiff / patternLength; // Normalized length difference
|
||||
const adjustedRating = similarity * (1 - lengthPenalty * 0.5); // Reduce score based on length difference
|
||||
|
||||
console.log('Comparing:', {
|
||||
text: combinedText,
|
||||
similarity,
|
||||
lengthDiff,
|
||||
adjustedRating
|
||||
});
|
||||
// console.log('Comparing:', {
|
||||
// text: combinedText,
|
||||
// similarity,
|
||||
// lengthDiff,
|
||||
// adjustedRating
|
||||
// });
|
||||
|
||||
// Update best match if we have better adjusted rating
|
||||
if (adjustedRating > bestMatch.rating) {
|
||||
|
|
@ -181,37 +268,6 @@ export function PDFViewer({ pdfFile }: PDFViewerProps) {
|
|||
}
|
||||
}, [clearHighlights]);
|
||||
|
||||
useEffect(() => {
|
||||
if (pdfFile) {
|
||||
const loadPdfText = async () => {
|
||||
try {
|
||||
const pdf = await pdfjs.getDocument(pdfFile).promise;
|
||||
let fullText = '';
|
||||
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
const page = await pdf.getPage(i);
|
||||
const textContent = await page.getTextContent();
|
||||
const pageText = textContent.items
|
||||
.map((item: any) => item.str)
|
||||
.join(' ');
|
||||
fullText += pageText + ' ';
|
||||
}
|
||||
|
||||
console.log('Loaded PDF text sample:', fullText.substring(0, 100));
|
||||
setPdfText(fullText);
|
||||
setText(fullText);
|
||||
} catch (error) {
|
||||
console.error('Error loading PDF text:', error);
|
||||
}
|
||||
};
|
||||
|
||||
loadPdfText();
|
||||
}
|
||||
|
||||
// Clear highlights when PDF changes
|
||||
return () => clearHighlights();
|
||||
}, [pdfFile, setText, clearHighlights]);
|
||||
|
||||
// Update highlights when current sentence changes
|
||||
useEffect(() => {
|
||||
console.log('Current sentence changed:', currentSentence);
|
||||
|
|
@ -229,11 +285,14 @@ export function PDFViewer({ pdfFile }: PDFViewerProps) {
|
|||
|
||||
return (
|
||||
<div className="flex flex-col items-center">
|
||||
{loadingError ? (
|
||||
<div className="text-red-500 mb-4">{loadingError}</div>
|
||||
) : null}
|
||||
<Document
|
||||
file={pdfFile}
|
||||
onLoadSuccess={onDocumentLoadSuccess}
|
||||
loading={<PDFSkeleton />}
|
||||
noData={<PDFSkeleton />}
|
||||
file={pdfDataUrl}
|
||||
onLoadSuccess={onDocumentLoadSuccess}
|
||||
className="flex flex-col items-center"
|
||||
>
|
||||
{Array.from(
|
||||
|
|
|
|||
|
|
@ -1,72 +1,90 @@
|
|||
'use client';
|
||||
|
||||
import { createContext, useContext, useState, ReactNode, useEffect } from 'react';
|
||||
|
||||
interface PDFDocument {
|
||||
id: string;
|
||||
name: string;
|
||||
size: number;
|
||||
lastModified: number;
|
||||
data: string; // Base64 encoded file data
|
||||
}
|
||||
import { indexedDBService, type PDFDocument } from '@/services/indexedDB';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
interface PDFContextType {
|
||||
documents: PDFDocument[];
|
||||
addDocument: (file: File) => Promise<string>;
|
||||
getDocument: (id: string) => PDFDocument | undefined;
|
||||
removeDocument: (id: string) => void;
|
||||
getDocument: (id: string) => Promise<PDFDocument | undefined>;
|
||||
removeDocument: (id: string) => Promise<void>;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
const PDFContext = createContext<PDFContextType | undefined>(undefined);
|
||||
|
||||
const LOCAL_STORAGE_KEY = 'pdf-documents';
|
||||
|
||||
export function PDFProvider({ children }: { children: ReactNode }) {
|
||||
const [documents, setDocuments] = useState<PDFDocument[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Load documents from local storage on mount
|
||||
useEffect(() => {
|
||||
const storedDocs = localStorage.getItem(LOCAL_STORAGE_KEY);
|
||||
if (storedDocs) {
|
||||
setDocuments(JSON.parse(storedDocs));
|
||||
}
|
||||
const loadDocuments = async () => {
|
||||
try {
|
||||
setError(null);
|
||||
await indexedDBService.init();
|
||||
const docs = await indexedDBService.getAllDocuments();
|
||||
setDocuments(docs);
|
||||
} catch (error) {
|
||||
console.error('Failed to load documents:', error);
|
||||
setError('Failed to initialize document storage. Please check if your browser supports IndexedDB.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadDocuments();
|
||||
}, []);
|
||||
|
||||
// Save documents to local storage whenever they change
|
||||
useEffect(() => {
|
||||
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(documents));
|
||||
}, [documents]);
|
||||
|
||||
const addDocument = async (file: File): Promise<string> => {
|
||||
return new Promise((resolve) => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
const base64Data = reader.result as string;
|
||||
const newDoc: PDFDocument = {
|
||||
id: `${file.name.replace(/[^a-zA-Z0-9]/g, '-').replace(/-+/g, '-')}-${Date.now()}`,
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
lastModified: file.lastModified,
|
||||
data: base64Data
|
||||
};
|
||||
|
||||
setDocuments(prev => [...prev, newDoc]);
|
||||
resolve(newDoc.id);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
setError(null);
|
||||
const id = uuidv4();
|
||||
const newDoc: PDFDocument = {
|
||||
id,
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
lastModified: file.lastModified,
|
||||
data: new Blob([file], { type: file.type })
|
||||
};
|
||||
|
||||
try {
|
||||
await indexedDBService.addDocument(newDoc);
|
||||
setDocuments(prev => [...prev, newDoc]);
|
||||
return id;
|
||||
} catch (error) {
|
||||
console.error('Failed to add document:', error);
|
||||
setError('Failed to save the document. Please try again.');
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const getDocument = (id: string) => {
|
||||
return documents.find(doc => doc.id === id);
|
||||
const getDocument = async (id: string): Promise<PDFDocument | undefined> => {
|
||||
setError(null);
|
||||
try {
|
||||
return await indexedDBService.getDocument(id);
|
||||
} catch (error) {
|
||||
console.error('Failed to get document:', error);
|
||||
setError('Failed to retrieve the document. Please try again.');
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const removeDocument = (id: string) => {
|
||||
setDocuments(prev => prev.filter(doc => doc.id !== id));
|
||||
const removeDocument = async (id: string): Promise<void> => {
|
||||
setError(null);
|
||||
try {
|
||||
await indexedDBService.removeDocument(id);
|
||||
setDocuments(prev => prev.filter(doc => doc.id !== id));
|
||||
} catch (error) {
|
||||
console.error('Failed to remove document:', error);
|
||||
setError('Failed to remove the document. Please try again.');
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<PDFContext.Provider value={{ documents, addDocument, getDocument, removeDocument }}>
|
||||
<PDFContext.Provider value={{ documents, addDocument, getDocument, removeDocument, isLoading, error }}>
|
||||
{children}
|
||||
</PDFContext.Provider>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -49,6 +49,8 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
|||
const [currentAudioIndex, setCurrentAudioIndex] = useState(0);
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const skipTriggeredRef = useRef(false);
|
||||
const skipTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const isPausingRef = useRef(false);
|
||||
|
||||
// Create OpenAI instance
|
||||
const [openai] = useState(
|
||||
|
|
@ -151,9 +153,11 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
|||
source.onended = () => {
|
||||
setActiveSource(null);
|
||||
setIsProcessing(false);
|
||||
if (isPlaying && !skipTriggeredRef.current) {
|
||||
// Only advance if we're playing and not pausing or skipping
|
||||
if (isPlaying && !skipTriggeredRef.current && !isPausingRef.current) {
|
||||
processNextSentence();
|
||||
}
|
||||
isPausingRef.current = false; // Reset pause flag
|
||||
};
|
||||
|
||||
source.start(0);
|
||||
|
|
@ -173,19 +177,24 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
|||
const togglePlay = useCallback(() => {
|
||||
setIsPlaying((prev) => {
|
||||
if (!prev) {
|
||||
return true; // Start playing
|
||||
isPausingRef.current = false;
|
||||
return true;
|
||||
} else {
|
||||
// Pause playback
|
||||
if (activeSource) {
|
||||
activeSource.stop(); // Stop the audio source
|
||||
setActiveSource(null); // Clear the active source
|
||||
isPausingRef.current = true;
|
||||
activeSource.stop();
|
||||
setActiveSource(null);
|
||||
}
|
||||
return false; // Set isPlaying to false
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}, [activeSource]);
|
||||
|
||||
const skipForward = useCallback(() => {
|
||||
if (skipTimeoutRef.current) {
|
||||
clearTimeout(skipTimeoutRef.current);
|
||||
}
|
||||
|
||||
skipTriggeredRef.current = true;
|
||||
// Cancel any ongoing request
|
||||
if (currentRequestRef.current) {
|
||||
|
|
@ -206,12 +215,16 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
|||
});
|
||||
|
||||
// Reset skip flag after a short delay
|
||||
setTimeout(() => {
|
||||
skipTimeoutRef.current = setTimeout(() => {
|
||||
skipTriggeredRef.current = false;
|
||||
}, 100);
|
||||
}, [sentences, activeSource]);
|
||||
|
||||
const skipBackward = useCallback(() => {
|
||||
if (skipTimeoutRef.current) {
|
||||
clearTimeout(skipTimeoutRef.current);
|
||||
}
|
||||
|
||||
skipTriggeredRef.current = true;
|
||||
// Cancel any ongoing request
|
||||
if (currentRequestRef.current) {
|
||||
|
|
@ -232,7 +245,7 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
|||
});
|
||||
|
||||
// Reset skip flag after a short delay
|
||||
setTimeout(() => {
|
||||
skipTimeoutRef.current = setTimeout(() => {
|
||||
skipTriggeredRef.current = false;
|
||||
}, 100);
|
||||
}, [sentences, activeSource]);
|
||||
|
|
|
|||
169
src/services/indexedDB.ts
Normal file
169
src/services/indexedDB.ts
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
const DB_NAME = 'openreader-db';
|
||||
const DB_VERSION = 1;
|
||||
const STORE_NAME = 'pdf-documents';
|
||||
|
||||
export interface PDFDocument {
|
||||
id: string;
|
||||
name: string;
|
||||
size: number;
|
||||
lastModified: number;
|
||||
data: Blob;
|
||||
}
|
||||
|
||||
class IndexedDBService {
|
||||
private db: IDBDatabase | null = null;
|
||||
|
||||
async init(): Promise<void> {
|
||||
if (!window.indexedDB) {
|
||||
throw new Error('IndexedDB is not supported in this browser');
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log('Initializing IndexedDB...');
|
||||
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
||||
|
||||
request.onerror = (event) => {
|
||||
const error = (event.target as IDBRequest).error;
|
||||
console.error('IndexedDB initialization error:', error);
|
||||
reject(error);
|
||||
};
|
||||
|
||||
request.onsuccess = (event) => {
|
||||
console.log('IndexedDB initialized successfully');
|
||||
this.db = (event.target as IDBOpenDBRequest).result;
|
||||
resolve();
|
||||
};
|
||||
|
||||
request.onupgradeneeded = (event) => {
|
||||
console.log('Upgrading IndexedDB schema...');
|
||||
const db = (event.target as IDBOpenDBRequest).result;
|
||||
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
||||
console.log('Creating PDF documents store...');
|
||||
db.createObjectStore(STORE_NAME, { keyPath: 'id' });
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async addDocument(document: PDFDocument): Promise<void> {
|
||||
if (!this.db) {
|
||||
console.log('Database not initialized, initializing now...');
|
||||
await this.init();
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
console.log('Adding document to IndexedDB:', document.name);
|
||||
const transaction = this.db!.transaction([STORE_NAME], 'readwrite');
|
||||
const store = transaction.objectStore(STORE_NAME);
|
||||
const request = store.put(document);
|
||||
|
||||
request.onerror = (event) => {
|
||||
const error = (event.target as IDBRequest).error;
|
||||
console.error('Error adding document:', error);
|
||||
reject(error);
|
||||
};
|
||||
|
||||
transaction.oncomplete = () => {
|
||||
console.log('Document added successfully:', document.name);
|
||||
resolve();
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error in addDocument transaction:', error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async getDocument(id: string): Promise<PDFDocument | undefined> {
|
||||
if (!this.db) {
|
||||
console.log('Database not initialized, initializing now...');
|
||||
await this.init();
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
console.log('Fetching document:', id);
|
||||
const transaction = this.db!.transaction([STORE_NAME], 'readonly');
|
||||
const store = transaction.objectStore(STORE_NAME);
|
||||
const request = store.get(id);
|
||||
|
||||
request.onerror = (event) => {
|
||||
const error = (event.target as IDBRequest).error;
|
||||
console.error('Error fetching document:', error);
|
||||
reject(error);
|
||||
};
|
||||
|
||||
request.onsuccess = () => {
|
||||
console.log('Document fetch result:', request.result ? 'found' : 'not found');
|
||||
resolve(request.result);
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error in getDocument transaction:', error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async getAllDocuments(): Promise<PDFDocument[]> {
|
||||
if (!this.db) {
|
||||
console.log('Database not initialized, initializing now...');
|
||||
await this.init();
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
console.log('Fetching all documents');
|
||||
const transaction = this.db!.transaction([STORE_NAME], 'readonly');
|
||||
const store = transaction.objectStore(STORE_NAME);
|
||||
const request = store.getAll();
|
||||
|
||||
request.onerror = (event) => {
|
||||
const error = (event.target as IDBRequest).error;
|
||||
console.error('Error fetching all documents:', error);
|
||||
reject(error);
|
||||
};
|
||||
|
||||
request.onsuccess = () => {
|
||||
console.log('Retrieved documents count:', request.result?.length || 0);
|
||||
resolve(request.result || []);
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error in getAllDocuments transaction:', error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async removeDocument(id: string): Promise<void> {
|
||||
if (!this.db) {
|
||||
console.log('Database not initialized, initializing now...');
|
||||
await this.init();
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
console.log('Removing document:', id);
|
||||
const transaction = this.db!.transaction([STORE_NAME], 'readwrite');
|
||||
const store = transaction.objectStore(STORE_NAME);
|
||||
const request = store.delete(id);
|
||||
|
||||
request.onerror = (event) => {
|
||||
const error = (event.target as IDBRequest).error;
|
||||
console.error('Error removing document:', error);
|
||||
reject(error);
|
||||
};
|
||||
|
||||
transaction.oncomplete = () => {
|
||||
console.log('Document removed successfully:', id);
|
||||
resolve();
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error in removeDocument transaction:', error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const indexedDBService = new IndexedDBService();
|
||||
Loading…
Reference in a new issue