Prevent text click jump while processing + refeactor text comparison
This commit is contained in:
parent
9823cd9596
commit
28fe51c80d
2 changed files with 128 additions and 111 deletions
|
|
@ -15,7 +15,7 @@ interface PDFViewerProps {
|
||||||
|
|
||||||
export function PDFViewer({ pdfData }: PDFViewerProps) {
|
export function PDFViewer({ pdfData }: PDFViewerProps) {
|
||||||
const [numPages, setNumPages] = useState<number>();
|
const [numPages, setNumPages] = useState<number>();
|
||||||
const { setText, currentSentence, stopAndPlayFromIndex } = useTTS();
|
const { setText, currentSentence, stopAndPlayFromIndex, isProcessing } = useTTS();
|
||||||
const [pdfText, setPdfText] = useState('');
|
const [pdfText, setPdfText] = useState('');
|
||||||
const [pdfDataUrl, setPdfDataUrl] = useState<string>();
|
const [pdfDataUrl, setPdfDataUrl] = useState<string>();
|
||||||
const [loadingError, setLoadingError] = useState<string>();
|
const [loadingError, setLoadingError] = useState<string>();
|
||||||
|
|
@ -107,12 +107,18 @@ export function PDFViewer({ pdfData }: PDFViewerProps) {
|
||||||
const container = containerRef.current;
|
const container = containerRef.current;
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
const handleClick = (event: MouseEvent) => handleTextClick(event, pdfText, containerRef as RefObject<HTMLDivElement>, stopAndPlayFromIndex);
|
const handleClick = (event: MouseEvent) => handleTextClick(
|
||||||
|
event,
|
||||||
|
pdfText,
|
||||||
|
containerRef as RefObject<HTMLDivElement>,
|
||||||
|
stopAndPlayFromIndex,
|
||||||
|
isProcessing
|
||||||
|
);
|
||||||
container.addEventListener('click', handleClick);
|
container.addEventListener('click', handleClick);
|
||||||
return () => {
|
return () => {
|
||||||
container.removeEventListener('click', handleClick);
|
container.removeEventListener('click', handleClick);
|
||||||
};
|
};
|
||||||
}, [pdfText, handleTextClick, stopAndPlayFromIndex]);
|
}, [pdfText, handleTextClick, stopAndPlayFromIndex, isProcessing]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
/*
|
/*
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,14 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { createContext, useContext, useState, ReactNode, useEffect, useCallback } from 'react';
|
import {
|
||||||
|
createContext,
|
||||||
|
useContext,
|
||||||
|
useState,
|
||||||
|
ReactNode,
|
||||||
|
useEffect,
|
||||||
|
useCallback,
|
||||||
|
useMemo,
|
||||||
|
} from 'react';
|
||||||
import { indexedDBService, type PDFDocument } from '@/services/indexedDB';
|
import { indexedDBService, type PDFDocument } from '@/services/indexedDB';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { pdfjs } from 'react-pdf';
|
import { pdfjs } from 'react-pdf';
|
||||||
|
|
@ -20,7 +28,13 @@ interface PDFContextType {
|
||||||
extractTextFromPDF: (pdfData: Blob) => Promise<string>;
|
extractTextFromPDF: (pdfData: Blob) => Promise<string>;
|
||||||
highlightPattern: (text: string, pattern: string, containerRef: React.RefObject<HTMLDivElement>) => void;
|
highlightPattern: (text: string, pattern: string, containerRef: React.RefObject<HTMLDivElement>) => void;
|
||||||
clearHighlights: () => void;
|
clearHighlights: () => void;
|
||||||
handleTextClick: (event: MouseEvent, pdfText: string, containerRef: React.RefObject<HTMLDivElement>, stopAndPlayFromIndex: (index: number) => void) => void;
|
handleTextClick: (
|
||||||
|
event: MouseEvent,
|
||||||
|
pdfText: string,
|
||||||
|
containerRef: React.RefObject<HTMLDivElement>,
|
||||||
|
stopAndPlayFromIndex: (index: number) => void,
|
||||||
|
isProcessing: boolean
|
||||||
|
) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PDFContext = createContext<PDFContextType | undefined>(undefined);
|
const PDFContext = createContext<PDFContextType | undefined>(undefined);
|
||||||
|
|
@ -30,15 +44,8 @@ export function PDFProvider({ children }: { children: ReactNode }) {
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Load documents from IndexedDB on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
/*
|
|
||||||
* Initializes the PDF document storage and loads existing documents.
|
|
||||||
* Sets up IndexedDB and retrieves all stored documents on component mount.
|
|
||||||
* Handles errors if IndexedDB initialization fails.
|
|
||||||
*
|
|
||||||
* Dependencies:
|
|
||||||
* - Empty array: Only runs once on mount as initialization should only happen once
|
|
||||||
*/
|
|
||||||
const loadDocuments = async () => {
|
const loadDocuments = async () => {
|
||||||
try {
|
try {
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
@ -56,7 +63,8 @@ export function PDFProvider({ children }: { children: ReactNode }) {
|
||||||
loadDocuments();
|
loadDocuments();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const addDocument = async (file: File): Promise<string> => {
|
// Add a new document to IndexedDB
|
||||||
|
const addDocument = useCallback(async (file: File): Promise<string> => {
|
||||||
setError(null);
|
setError(null);
|
||||||
const id = uuidv4();
|
const id = uuidv4();
|
||||||
const newDoc: PDFDocument = {
|
const newDoc: PDFDocument = {
|
||||||
|
|
@ -69,16 +77,17 @@ export function PDFProvider({ children }: { children: ReactNode }) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await indexedDBService.addDocument(newDoc);
|
await indexedDBService.addDocument(newDoc);
|
||||||
setDocuments(prev => [...prev, newDoc]);
|
setDocuments((prev) => [...prev, newDoc]);
|
||||||
return id;
|
return id;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to add document:', error);
|
console.error('Failed to add document:', error);
|
||||||
setError('Failed to save the document. Please try again.');
|
setError('Failed to save the document. Please try again.');
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const getDocument = async (id: string): Promise<PDFDocument | undefined> => {
|
// Get a document by ID from IndexedDB
|
||||||
|
const getDocument = useCallback(async (id: string): Promise<PDFDocument | undefined> => {
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
return await indexedDBService.getDocument(id);
|
return await indexedDBService.getDocument(id);
|
||||||
|
|
@ -87,20 +96,22 @@ export function PDFProvider({ children }: { children: ReactNode }) {
|
||||||
setError('Failed to retrieve the document. Please try again.');
|
setError('Failed to retrieve the document. Please try again.');
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const removeDocument = async (id: string): Promise<void> => {
|
// Remove a document by ID from IndexedDB
|
||||||
|
const removeDocument = useCallback(async (id: string): Promise<void> => {
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
await indexedDBService.removeDocument(id);
|
await indexedDBService.removeDocument(id);
|
||||||
setDocuments(prev => prev.filter(doc => doc.id !== id));
|
setDocuments((prev) => prev.filter((doc) => doc.id !== id));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to remove document:', error);
|
console.error('Failed to remove document:', error);
|
||||||
setError('Failed to remove the document. Please try again.');
|
setError('Failed to remove the document. Please try again.');
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
|
// Extract text from a PDF file
|
||||||
const extractTextFromPDF = useCallback(async (pdfData: Blob): Promise<string> => {
|
const extractTextFromPDF = useCallback(async (pdfData: Blob): Promise<string> => {
|
||||||
try {
|
try {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
|
|
@ -112,20 +123,15 @@ export function PDFProvider({ children }: { children: ReactNode }) {
|
||||||
|
|
||||||
const base64Data = dataUrl.split(',')[1];
|
const base64Data = dataUrl.split(',')[1];
|
||||||
const binaryData = atob(base64Data);
|
const binaryData = atob(base64Data);
|
||||||
const length = binaryData.length;
|
const bytes = new Uint8Array(binaryData.length);
|
||||||
const bytes = new Uint8Array(length);
|
for (let i = 0; i < binaryData.length; i++) {
|
||||||
for (let i = 0; i < length; i++) {
|
|
||||||
bytes[i] = binaryData.charCodeAt(i);
|
bytes[i] = binaryData.charCodeAt(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadingTask = pdfjs.getDocument({
|
const loadingTask = pdfjs.getDocument({ data: bytes });
|
||||||
data: bytes,
|
|
||||||
disableAutoFetch: true,
|
|
||||||
disableStream: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const pdf = await loadingTask.promise;
|
const pdf = await loadingTask.promise;
|
||||||
let fullText = '';
|
let fullText = '';
|
||||||
|
|
||||||
for (let i = 1; i <= pdf.numPages; i++) {
|
for (let i = 1; i <= pdf.numPages; i++) {
|
||||||
const page = await pdf.getPage(i);
|
const page = await pdf.getPage(i);
|
||||||
const textContent = await page.getTextContent();
|
const textContent = await page.getTextContent();
|
||||||
|
|
@ -140,6 +146,7 @@ export function PDFProvider({ children }: { children: ReactNode }) {
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Clear all highlights in the PDF viewer
|
||||||
const clearHighlights = useCallback(() => {
|
const clearHighlights = useCallback(() => {
|
||||||
const textNodes = document.querySelectorAll('.react-pdf__Page__textContent span');
|
const textNodes = document.querySelectorAll('.react-pdf__Page__textContent span');
|
||||||
textNodes.forEach((node) => {
|
textNodes.forEach((node) => {
|
||||||
|
|
@ -149,25 +156,12 @@ export function PDFProvider({ children }: { children: ReactNode }) {
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const highlightPattern = useCallback((text: string, pattern: string, containerRef: React.RefObject<HTMLDivElement>) => {
|
// Find the best text match using string similarity
|
||||||
clearHighlights();
|
const findBestTextMatch = useCallback((
|
||||||
|
elements: Array<{ element: HTMLElement; text: string }>,
|
||||||
if (!pattern?.trim()) {
|
targetText: string,
|
||||||
return;
|
maxCombinedLength: number
|
||||||
}
|
) => {
|
||||||
|
|
||||||
const cleanPattern = pattern.trim().replace(/\s+/g, ' ');
|
|
||||||
const patternLength = cleanPattern.length;
|
|
||||||
|
|
||||||
const container = containerRef.current;
|
|
||||||
if (!container) return;
|
|
||||||
|
|
||||||
const textNodes = container.querySelectorAll('.react-pdf__Page__textContent span');
|
|
||||||
const allText = Array.from(textNodes).map(node => ({
|
|
||||||
element: node as HTMLElement,
|
|
||||||
text: (node.textContent || '').trim(),
|
|
||||||
})).filter(node => node.text.length > 0);
|
|
||||||
|
|
||||||
let bestMatch = {
|
let bestMatch = {
|
||||||
elements: [] as HTMLElement[],
|
elements: [] as HTMLElement[],
|
||||||
rating: 0,
|
rating: 0,
|
||||||
|
|
@ -175,22 +169,20 @@ export function PDFProvider({ children }: { children: ReactNode }) {
|
||||||
lengthDiff: Infinity,
|
lengthDiff: Infinity,
|
||||||
};
|
};
|
||||||
|
|
||||||
for (let i = 0; i < allText.length; i++) {
|
for (let i = 0; i < elements.length; i++) {
|
||||||
let combinedText = '';
|
let combinedText = '';
|
||||||
let currentElements = [];
|
let currentElements = [];
|
||||||
for (let j = i; j < Math.min(i + 10, allText.length); j++) {
|
for (let j = i; j < Math.min(i + 10, elements.length); j++) {
|
||||||
const node = allText[j];
|
const node = elements[j];
|
||||||
const newText = combinedText + (combinedText ? ' ' : '') + node.text;
|
const newText = combinedText ? `${combinedText} ${node.text}` : node.text;
|
||||||
if (newText.length > patternLength * 2) {
|
if (newText.length > maxCombinedLength) break;
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
combinedText = newText;
|
combinedText = newText;
|
||||||
currentElements.push(node.element);
|
currentElements.push(node.element);
|
||||||
|
|
||||||
const similarity = stringSimilarity.compareTwoStrings(cleanPattern, combinedText);
|
const similarity = stringSimilarity.compareTwoStrings(targetText, combinedText);
|
||||||
const lengthDiff = Math.abs(combinedText.length - patternLength);
|
const lengthDiff = Math.abs(combinedText.length - targetText.length);
|
||||||
const lengthPenalty = lengthDiff / patternLength;
|
const lengthPenalty = lengthDiff / targetText.length;
|
||||||
const adjustedRating = similarity * (1 - lengthPenalty * 0.5);
|
const adjustedRating = similarity * (1 - lengthPenalty * 0.5);
|
||||||
|
|
||||||
if (adjustedRating > bestMatch.rating) {
|
if (adjustedRating > bestMatch.rating) {
|
||||||
|
|
@ -204,9 +196,30 @@ export function PDFProvider({ children }: { children: ReactNode }) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const similarityThreshold = bestMatch.lengthDiff < patternLength * 0.3 ? 0.3 : 0.5;
|
return bestMatch;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Highlight matching text in the PDF viewer
|
||||||
|
const highlightPattern = useCallback((text: string, pattern: string, containerRef: React.RefObject<HTMLDivElement>) => {
|
||||||
|
clearHighlights();
|
||||||
|
|
||||||
|
if (!pattern?.trim()) return;
|
||||||
|
|
||||||
|
const cleanPattern = pattern.trim().replace(/\s+/g, ' ');
|
||||||
|
const container = containerRef.current;
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const textNodes = container.querySelectorAll('.react-pdf__Page__textContent span');
|
||||||
|
const allText = Array.from(textNodes).map((node) => ({
|
||||||
|
element: node as HTMLElement,
|
||||||
|
text: (node.textContent || '').trim(),
|
||||||
|
})).filter((node) => node.text.length > 0);
|
||||||
|
|
||||||
|
const bestMatch = findBestTextMatch(allText, cleanPattern, cleanPattern.length * 2);
|
||||||
|
const similarityThreshold = bestMatch.lengthDiff < cleanPattern.length * 0.3 ? 0.3 : 0.5;
|
||||||
|
|
||||||
if (bestMatch.rating >= similarityThreshold) {
|
if (bestMatch.rating >= similarityThreshold) {
|
||||||
bestMatch.elements.forEach(element => {
|
bestMatch.elements.forEach((element) => {
|
||||||
element.style.backgroundColor = 'grey';
|
element.style.backgroundColor = 'grey';
|
||||||
element.style.opacity = '0.4';
|
element.style.opacity = '0.4';
|
||||||
});
|
});
|
||||||
|
|
@ -226,9 +239,18 @@ export function PDFProvider({ children }: { children: ReactNode }) {
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [clearHighlights]);
|
}, [clearHighlights, findBestTextMatch]);
|
||||||
|
|
||||||
|
// Handle text click events in the PDF viewer
|
||||||
|
const handleTextClick = useCallback((
|
||||||
|
event: MouseEvent,
|
||||||
|
pdfText: string,
|
||||||
|
containerRef: React.RefObject<HTMLDivElement>,
|
||||||
|
stopAndPlayFromIndex: (index: number) => void,
|
||||||
|
isProcessing: boolean
|
||||||
|
) => {
|
||||||
|
if (isProcessing) return; // Don't process clicks while TTS is processing
|
||||||
|
|
||||||
const handleTextClick = useCallback((event: MouseEvent, pdfText: string, containerRef: React.RefObject<HTMLDivElement>, stopAndPlayFromIndex: (index: number) => void) => {
|
|
||||||
const target = event.target as HTMLElement;
|
const target = event.target as HTMLElement;
|
||||||
if (!target.matches('.react-pdf__Page__textContent span')) return;
|
if (!target.matches('.react-pdf__Page__textContent span')) return;
|
||||||
|
|
||||||
|
|
@ -242,64 +264,25 @@ export function PDFProvider({ children }: { children: ReactNode }) {
|
||||||
const endIndex = Math.min(spans.length - 1, clickedIndex + contextWindow);
|
const endIndex = Math.min(spans.length - 1, clickedIndex + contextWindow);
|
||||||
const contextText = spans
|
const contextText = spans
|
||||||
.slice(startIndex, endIndex + 1)
|
.slice(startIndex, endIndex + 1)
|
||||||
.map(span => span.textContent)
|
.map((span) => span.textContent)
|
||||||
.join(' ')
|
.join(' ')
|
||||||
.trim();
|
.trim();
|
||||||
|
|
||||||
if (!contextText?.trim()) return;
|
if (!contextText?.trim()) return;
|
||||||
|
|
||||||
const cleanContext = contextText.trim().replace(/\s+/g, ' ');
|
const cleanContext = contextText.trim().replace(/\s+/g, ' ');
|
||||||
const contextLength = cleanContext.length;
|
const allText = Array.from(parentElement.querySelectorAll('span')).map((node) => ({
|
||||||
|
|
||||||
const allText = Array.from(parentElement.querySelectorAll('span')).map(node => ({
|
|
||||||
element: node as HTMLElement,
|
element: node as HTMLElement,
|
||||||
text: (node.textContent || '').trim(),
|
text: (node.textContent || '').trim(),
|
||||||
})).filter(node => node.text.length > 0);
|
})).filter((node) => node.text.length > 0);
|
||||||
|
|
||||||
let bestMatch = {
|
const bestMatch = findBestTextMatch(allText, cleanContext, cleanContext.length * 2);
|
||||||
elements: [] as HTMLElement[],
|
const similarityThreshold = bestMatch.lengthDiff < cleanContext.length * 0.3 ? 0.3 : 0.5;
|
||||||
rating: 0,
|
|
||||||
text: '',
|
|
||||||
lengthDiff: Infinity,
|
|
||||||
};
|
|
||||||
|
|
||||||
for (let i = 0; i < allText.length; i++) {
|
|
||||||
let combinedText = '';
|
|
||||||
let currentElements = [];
|
|
||||||
for (let j = i; j < Math.min(i + 10, allText.length); j++) {
|
|
||||||
const node = allText[j];
|
|
||||||
const newText = combinedText + (combinedText ? ' ' : '') + node.text;
|
|
||||||
if (newText.length > contextLength * 2) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
combinedText = newText;
|
|
||||||
currentElements.push(node.element);
|
|
||||||
|
|
||||||
const similarity = stringSimilarity.compareTwoStrings(cleanContext, combinedText);
|
|
||||||
const lengthDiff = Math.abs(combinedText.length - contextLength);
|
|
||||||
const lengthPenalty = lengthDiff / contextLength;
|
|
||||||
const adjustedRating = similarity * (1 - lengthPenalty * 0.5);
|
|
||||||
|
|
||||||
if (adjustedRating > bestMatch.rating) {
|
|
||||||
bestMatch = {
|
|
||||||
elements: [...currentElements],
|
|
||||||
rating: adjustedRating,
|
|
||||||
text: combinedText,
|
|
||||||
lengthDiff,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const similarityThreshold = bestMatch.lengthDiff < contextLength * 0.3 ? 0.3 : 0.5;
|
|
||||||
if (bestMatch.rating >= similarityThreshold) {
|
if (bestMatch.rating >= similarityThreshold) {
|
||||||
const matchText = bestMatch.text;
|
const matchText = bestMatch.text;
|
||||||
const sentences = nlp(pdfText).sentences().out('array') as string[];
|
const sentences = nlp(pdfText).sentences().out('array') as string[];
|
||||||
let bestSentenceMatch = {
|
let bestSentenceMatch = { sentence: '', rating: 0 };
|
||||||
sentence: '',
|
|
||||||
rating: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const sentence of sentences) {
|
for (const sentence of sentences) {
|
||||||
const rating = stringSimilarity.compareTwoStrings(matchText, sentence);
|
const rating = stringSimilarity.compareTwoStrings(matchText, sentence);
|
||||||
|
|
@ -309,17 +292,45 @@ export function PDFProvider({ children }: { children: ReactNode }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bestSentenceMatch.rating >= 0.5) {
|
if (bestSentenceMatch.rating >= 0.5) {
|
||||||
const sentenceIndex = sentences.findIndex(sentence => sentence === bestSentenceMatch.sentence);
|
const sentenceIndex = sentences.findIndex((sentence) => sentence === bestSentenceMatch.sentence);
|
||||||
if (sentenceIndex !== -1) {
|
if (sentenceIndex !== -1) {
|
||||||
stopAndPlayFromIndex(sentenceIndex);
|
stopAndPlayFromIndex(sentenceIndex);
|
||||||
highlightPattern(pdfText, bestSentenceMatch.sentence, containerRef);
|
highlightPattern(pdfText, bestSentenceMatch.sentence, containerRef);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [highlightPattern]);
|
}, [highlightPattern, findBestTextMatch]);
|
||||||
|
|
||||||
|
// Memoize the context value to prevent unnecessary re-renders
|
||||||
|
const contextValue = useMemo(
|
||||||
|
() => ({
|
||||||
|
documents,
|
||||||
|
addDocument,
|
||||||
|
getDocument,
|
||||||
|
removeDocument,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
extractTextFromPDF,
|
||||||
|
highlightPattern,
|
||||||
|
clearHighlights,
|
||||||
|
handleTextClick,
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
documents,
|
||||||
|
addDocument,
|
||||||
|
getDocument,
|
||||||
|
removeDocument,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
extractTextFromPDF,
|
||||||
|
highlightPattern,
|
||||||
|
clearHighlights,
|
||||||
|
handleTextClick,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PDFContext.Provider value={{ documents, addDocument, getDocument, removeDocument, isLoading, error, extractTextFromPDF, highlightPattern, clearHighlights, handleTextClick }}>
|
<PDFContext.Provider value={contextValue}>
|
||||||
{children}
|
{children}
|
||||||
</PDFContext.Provider>
|
</PDFContext.Provider>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue