File size: 11,346 Bytes
9ded9e1
9f60175
 
a706099
9f60175
9ded9e1
 
9f60175
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9ded9e1
9f60175
 
 
 
9ded9e1
9f60175
 
9ded9e1
d38750a
9f60175
 
bca6bc0
9f60175
 
 
3ecd1bf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
aa7e9a7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
045b303
 
aa7e9a7
045b303
 
 
 
 
aa7e9a7
045b303
 
 
aa7e9a7
 
 
 
3ecd1bf
70e74ea
3ecd1bf
 
 
 
70e74ea
9f60175
 
 
 
 
 
4a6c290
9f60175
 
 
 
 
 
9ded9e1
 
9f60175
 
 
 
 
 
 
 
 
 
 
 
9ded9e1
9f60175
9ded9e1
3ecd1bf
9f60175
 
 
 
3ecd1bf
 
 
9f60175
 
 
 
3ecd1bf
9f60175
 
 
 
 
 
 
 
3ecd1bf
9f60175
 
 
 
9ded9e1
3ecd1bf
 
 
 
 
d38750a
 
 
 
 
 
3ecd1bf
 
 
 
 
d38750a
9f60175
 
 
 
 
 
 
9ded9e1
9f60175
 
 
 
 
 
 
 
9ded9e1
 
9f60175
 
 
9ded9e1
 
9f60175
 
 
9ded9e1
9f60175
9ded9e1
 
 
 
 
 
 
 
a706099
9ded9e1
045b303
 
 
 
 
 
 
 
 
 
 
aa7e9a7
 
 
 
 
 
045b303
aa7e9a7
045b303
 
 
 
 
 
 
 
 
 
a706099
9f60175
 
 
 
 
 
045b303
9f60175
 
 
 
 
 
 
9ded9e1
9f60175
 
 
 
 
a706099
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
import { useState, useRef, useEffect } from 'react';
import { PdfLoader, PdfHighlighter, useHighlightContainerContext, TextHighlight, AreaHighlight } from 'react-pdf-highlighter-extended';
import * as pdfjs from "pdfjs-dist";

// Tell pdf.js to use the local worker file
pdfjs.GlobalWorkerOptions.workerSrc = '/pdf.worker.min.js';

// Copy exact example from documentation
const MyHighlightContainer = () => {
  const {
    highlight, // The highlight being rendered
    viewportToScaled, // Convert a highlight position to platform agnostic coords (useful for saving edits)
    screenshot, // Screenshot a bounding rectangle
    isScrolledTo, // Whether the highlight has been auto-scrolled to
    highlightBindings, // Whether the highlight has been auto-scrolled to
  } = useHighlightContainerContext();

  const isTextHighlight = !Boolean(
    highlight.content && highlight.content.image
  );

  const component = isTextHighlight ? (
    <TextHighlight
      isScrolledTo={isScrolledTo}
      highlight={highlight}
    />
  ) : (
    <AreaHighlight
      isScrolledTo={isScrolledTo}
      highlight={highlight}
      onChange={(boundingRect) => {
        const edit = {
          position: {
            boundingRect: viewportToScaled(boundingRect),
            rects: [],
          },
          content: {
            image: screenshot(boundingRect),
          },
        };
      }}
      bounds={highlightBindings.textLayer}
    />
  );

  return component;
};

const DocumentViewer = ({ selectedFile, documentData, onPageChange, preloadedHighlights = null, currentChunkIndex = null, onDocumentReady = null, isChunkLoading = null }) => {
    const [highlights, setHighlights] = useState([]);
    const [pdfUrl, setPdfUrl] = useState(null);
    const [zoom, setZoom] = useState(1);
    
    /** Refs for PdfHighlighter utilities */
    const highlighterUtilsRef = useRef();
    const documentReadyCalledRef = useRef(false);
    
    // Function to scroll to a specific chunk's highlight
    const scrollToChunk = (chunkIndex) => {
        if (highlighterUtilsRef.current && preloadedHighlights) {
            const chunkHighlights = preloadedHighlights[chunkIndex];
            if (chunkHighlights && chunkHighlights.length > 0) {
                const firstHighlightInChunk = chunkHighlights[0];
                highlighterUtilsRef.current.scrollToHighlight(firstHighlightInChunk);
            }
        }
    };

    // Function to scroll to the first highlight (for backwards compatibility)
    const scrollToFirstChunk = () => {
        scrollToChunk(0);
    };

    const zoomWithCenter = (zoomDelta) => {
        const container = document.querySelector('.PdfHighlighter');
        if (!container) {
            setZoom(prev => prev + zoomDelta);
            return;
        }

        const scrollLeft = container.scrollLeft;
        const scrollTop = container.scrollTop;
        const containerWidth = container.clientWidth;
        const containerHeight = container.clientHeight;
        
        // Calculate center point before zoom
        const centerX = scrollLeft + containerWidth / 2;
        const centerY = scrollTop + containerHeight / 2;
        
        setZoom(prev => {
            const newZoom = prev + zoomDelta;
            const zoomRatio = newZoom / prev;
            
            // Use requestAnimationFrame for smoother transition
            requestAnimationFrame(() => {
                const newScrollLeft = centerX * zoomRatio - containerWidth / 2;
                const newScrollTop = centerY * zoomRatio - containerHeight / 2;
                container.scrollTo({
                    left: newScrollLeft,
                    top: newScrollTop,
                    behavior: 'auto'
                });
            });
            
            return newZoom;
        });
    };

    const zoomIn = () => {
        if (zoom < 4) {
            zoomWithCenter(0.1);
        }
    };

    const zoomOut = () => {
        if (zoom > 0.2) {
            zoomWithCenter(-0.1);
        }
    };

    const resetZoom = () => {
        setZoom(1);
    };

    // Call onDocumentReady only once when utils become available
    useEffect(() => {
        if (onDocumentReady && !documentReadyCalledRef.current && highlighterUtilsRef.current) {
            documentReadyCalledRef.current = true;
            onDocumentReady({ scrollToFirstChunk });
        }
    }, [onDocumentReady, scrollToFirstChunk, highlighterUtilsRef.current]);

    // Utility function to normalize highlight data
    const normalizeHighlight = (highlightData) => {
        // Ensure the highlight has the required structure
        if (!highlightData.id || !highlightData.position || !highlightData.content) {
            return null;
        }
        
        return {
            id: highlightData.id,
            position: highlightData.position,
            content: highlightData.content
        };
    };

    // Convert File object to URL for PdfLoader
    useEffect(() => {
        if (selectedFile) {
            if (typeof selectedFile === 'string') {
                setPdfUrl(selectedFile);
            } else if (selectedFile instanceof File) {
                const url = URL.createObjectURL(selectedFile);
                setPdfUrl(url);
                return () => URL.revokeObjectURL(url);
            }
        } else {
            setPdfUrl(null);
        }
    }, [selectedFile]);

    // Load preloaded highlights when component mounts or when currentChunkIndex changes
    useEffect(() => {
        if (preloadedHighlights) {
            let highlightsToLoad = [];
            
            if (currentChunkIndex !== null && currentChunkIndex !== undefined && preloadedHighlights[currentChunkIndex]) {
                // Load highlights for specific chunk
                highlightsToLoad = preloadedHighlights[currentChunkIndex];
            } else if (Array.isArray(preloadedHighlights)) {
                // Load all highlights if it's an array
                highlightsToLoad = preloadedHighlights;
            } else if (typeof preloadedHighlights === 'object') {
                // If it's an object without chunkIndex, take all values
                highlightsToLoad = Object.values(preloadedHighlights).flat();
            }
            
            // Normalize and filter valid highlights
            const validHighlights = highlightsToLoad
                .map(normalizeHighlight)
                .filter(Boolean);
            
            console.log(`🎨 Loading ${validHighlights.length} preloaded highlights${currentChunkIndex !== null ? ` for chunk ${currentChunkIndex}` : ''}`);
            setHighlights(validHighlights);
        } else {
            // Clear highlights if no preloaded data
            setHighlights([]);
        }
    }, [preloadedHighlights, currentChunkIndex]);

    // Auto-scroll to current chunk when currentChunkIndex changes (only on navigation, not during streaming)
    useEffect(() => {
        // Only auto-scroll if we have highlighter utils and this is a valid chunk navigation
        // Don't auto-scroll during streaming (when isChunkLoading is true for currentChunkIndex)
        if (highlighterUtilsRef.current && 
            currentChunkIndex !== null && 
            currentChunkIndex !== undefined && 
            currentChunkIndex >= 0 &&
            (!isChunkLoading || !isChunkLoading(currentChunkIndex))) {
            // Small delay to ensure highlights are loaded
            setTimeout(() => {
                scrollToChunk(currentChunkIndex);
            }, 200);
        }
    }, [currentChunkIndex, isChunkLoading]); // Only depend on currentChunkIndex, not preloadedHighlights

    // Handle selection - log coordinates and add debugging
    const handleSelection = (selection) => {
        console.log("🎯 SELECTION MADE! Full selection object:", selection);
        console.log("πŸ“ Position:", selection.position);
        console.log("πŸ“ Content:", selection.content);
        console.log("πŸ” Type:", selection.type);
        
        const newHighlight = {
            id: `highlight_${Date.now()}`,
            position: selection.position,
            content: selection.content
        };
        
        console.log("βœ… Adding highlight:", newHighlight);
        setHighlights(prev => [...prev, newHighlight]);
    };

    // Additional debugging handlers
    const handleCreateGhost = (ghost) => {
        console.log("πŸ‘» Ghost highlight created:", ghost);
    };

    const handleRemoveGhost = (ghost) => {
        console.log("❌ Ghost highlight removed:", ghost);
    };

    if (!selectedFile || !pdfUrl) {
        return (
            <div className="bg-white rounded-lg shadow-sm flex items-center justify-center h-full">
                <div className="text-center text-gray-500">
                    <p>No PDF selected</p>
                </div>
            </div>
        );
    }
    return (
        <div className="bg-white rounded-lg shadow-sm flex flex-col relative" style={{ width: '100%', height: '100%' }}>
            <div className="flex justify-between items-center p-2 border-b">
                <h2>{documentData?.filename || 'Document'}</h2>
                <div className="flex items-center gap-2">
                    <button 
                        title="Zoom out" 
                        onClick={zoomOut}
                        className="px-2 py-1 border rounded hover:bg-gray-100"
                        disabled={zoom <= 0.2}
                    >
                        -
                    </button>
                    <button 
                        title="Reset zoom" 
                        onClick={resetZoom}
                        className="px-2 py-1 border rounded hover:bg-gray-100 text-sm"
                        disabled={zoom === 1}
                    >
                        {(zoom * 100).toFixed(0)}%
                    </button>
                    <button 
                        title="Zoom in" 
                        onClick={zoomIn}
                        className="px-2 py-1 border rounded hover:bg-gray-100"
                        disabled={zoom >= 4}
                    >
                        +
                    </button>
                </div>
            </div>
            
            <div style={{ height: '500px' }}>
                <PdfLoader document={pdfUrl} workerSrc='/pdf.worker.min.js'>
                    {(pdfDocument) => (
                        <PdfHighlighter
                            enableAreaSelection={(event) => event.altKey}
                            pdfDocument={pdfDocument}
                            pdfScaleValue={zoom}
                            utilsRef={(_pdfHighlighterUtils) => {
                                highlighterUtilsRef.current = _pdfHighlighterUtils;
                            }}
                            highlights={highlights}
                            onSelection={handleSelection}
                            onCreateGhostHighlight={handleCreateGhost}
                            onRemoveGhostHighlight={handleRemoveGhost}
                        >
                            <MyHighlightContainer />
                        </PdfHighlighter>
                    )}
                </PdfLoader>
            </div>
        </div>
    );
};

export default DocumentViewer;