aki-008 commited on
Commit
dd30f85
·
1 Parent(s): 1591012

feat: replace iframe with secure canvas pdf viewer

Browse files
Frontend/src/components/SecurePdfViewer.tsx ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect, useState, useRef } from "react";
2
+ import * as pdfjsLib from "pdfjs-dist";
3
+ import { fetchNoteBlob } from "../api/notesService";
4
+ import {
5
+ ChevronLeft,
6
+ ChevronRight,
7
+ ZoomIn,
8
+ ZoomOut,
9
+ Loader2,
10
+ } from "lucide-react";
11
+
12
+ // Configure PDF Worker (Required for pdfjs-dist)
13
+ pdfjsLib.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjsLib.version}/build/pdf.worker.min.mjs`;
14
+
15
+ interface Props {
16
+ noteId: number;
17
+ }
18
+
19
+ const SecurePdfViewer: React.FC<Props> = ({ noteId }) => {
20
+ const [pdfDoc, setPdfDoc] = useState<pdfjsLib.PDFDocumentProxy | null>(null);
21
+ const [pageNum, setPageNum] = useState(1);
22
+ const [scale, setScale] = useState(1.2);
23
+ const [loading, setLoading] = useState(true);
24
+ const [error, setError] = useState<string | null>(null);
25
+ const canvasRef = useRef<HTMLCanvasElement>(null);
26
+
27
+ // 1. Fetch and Load PDF Data
28
+ useEffect(() => {
29
+ let isMounted = true;
30
+ const loadPdf = async () => {
31
+ try {
32
+ setLoading(true);
33
+ setError(null);
34
+
35
+ // Fetch securely using existing Axios setup (sends Auth header)
36
+ const blob = await fetchNoteBlob(noteId);
37
+ const arrayBuffer = await blob.arrayBuffer();
38
+
39
+ // Load into PDF.js
40
+ const loadedPdf = await pdfjsLib.getDocument({ data: arrayBuffer })
41
+ .promise;
42
+
43
+ if (isMounted) {
44
+ setPdfDoc(loadedPdf);
45
+ setPageNum(1);
46
+ setLoading(false);
47
+ }
48
+ } catch (err) {
49
+ console.error("PDF Load Error:", err);
50
+ if (isMounted) setError("Failed to load PDF. Please try again.");
51
+ setLoading(false);
52
+ }
53
+ };
54
+
55
+ if (noteId) loadPdf();
56
+
57
+ return () => {
58
+ isMounted = false;
59
+ };
60
+ }, [noteId]);
61
+
62
+ // 2. Render Page on Canvas
63
+ useEffect(() => {
64
+ if (!pdfDoc || !canvasRef.current) return;
65
+
66
+ const renderPage = async () => {
67
+ try {
68
+ const page = await pdfDoc.getPage(pageNum);
69
+ const viewport = page.getViewport({ scale });
70
+ const canvas = canvasRef.current!;
71
+ const context = canvas.getContext("2d")!;
72
+
73
+ // Handle High DPI screens
74
+ const outputScale = window.devicePixelRatio || 1;
75
+ canvas.width = Math.floor(viewport.width * outputScale);
76
+ canvas.height = Math.floor(viewport.height * outputScale);
77
+ canvas.style.width = Math.floor(viewport.width) + "px";
78
+ canvas.style.height = Math.floor(viewport.height) + "px";
79
+
80
+ const transform =
81
+ outputScale !== 1
82
+ ? [outputScale, 0, 0, outputScale, 0, 0]
83
+ : undefined;
84
+
85
+ await page.render({
86
+ canvasContext: context,
87
+ viewport: viewport,
88
+ transform: transform,
89
+ }).promise;
90
+ } catch (err) {
91
+ console.error("Page Render Error:", err);
92
+ }
93
+ };
94
+
95
+ renderPage();
96
+ }, [pdfDoc, pageNum, scale]);
97
+
98
+ if (loading) {
99
+ return (
100
+ <div className="flex flex-col items-center justify-center h-full text-white">
101
+ <Loader2 className="w-8 h-8 animate-spin text-[#F7E396] mb-2" />
102
+ <p>Securely loading document...</p>
103
+ </div>
104
+ );
105
+ }
106
+
107
+ if (error) {
108
+ return <div className="text-red-400 p-4 text-center">{error}</div>;
109
+ }
110
+
111
+ return (
112
+ <div className="flex flex-col h-full bg-[#525f88] rounded-xl overflow-hidden relative">
113
+ {/* Toolbar */}
114
+ <div className="flex items-center justify-between p-2 bg-[#434E78] border-b border-white/10 text-white z-10 shadow-md">
115
+ <div className="flex items-center gap-2">
116
+ <button
117
+ disabled={pageNum <= 1}
118
+ onClick={() => setPageNum((p) => p - 1)}
119
+ className="p-1 hover:bg-white/10 rounded disabled:opacity-30 transition"
120
+ >
121
+ <ChevronLeft size={20} />
122
+ </button>
123
+ <span className="text-sm font-medium w-16 text-center">
124
+ {pageNum} / {pdfDoc?.numPages}
125
+ </span>
126
+ <button
127
+ disabled={!pdfDoc || pageNum >= pdfDoc.numPages}
128
+ onClick={() => setPageNum((p) => p + 1)}
129
+ className="p-1 hover:bg-white/10 rounded disabled:opacity-30 transition"
130
+ >
131
+ <ChevronRight size={20} />
132
+ </button>
133
+ </div>
134
+
135
+ <div className="flex items-center gap-2">
136
+ <button
137
+ onClick={() => setScale((s) => Math.max(0.5, s - 0.2))}
138
+ className="p-1 hover:bg-white/10 rounded transition"
139
+ >
140
+ <ZoomOut size={18} />
141
+ </button>
142
+ <span className="text-xs w-12 text-center">
143
+ {Math.round(scale * 100)}%
144
+ </span>
145
+ <button
146
+ onClick={() => setScale((s) => Math.min(3.0, s + 0.2))}
147
+ className="p-1 hover:bg-white/10 rounded transition"
148
+ >
149
+ <ZoomIn size={18} />
150
+ </button>
151
+ </div>
152
+ </div>
153
+
154
+ {/* Scrollable Canvas Area */}
155
+ <div className="flex-1 overflow-auto flex justify-center p-4 bg-[#525f88] custom-scrollbar">
156
+ <canvas ref={canvasRef} className="shadow-2xl" />
157
+ </div>
158
+ </div>
159
+ );
160
+ };
161
+
162
+ export default SecurePdfViewer;
Frontend/src/pages/note.tsx CHANGED
@@ -2,6 +2,7 @@ import ReactMarkdown from "react-markdown";
2
  import remarkGfm from "remark-gfm";
3
  import React, { useEffect, useState, useRef, useCallback } from "react";
4
  import { getNoteContentUrl } from "../api/notesService";
 
5
  import {
6
  Upload,
7
  Menu,
@@ -19,7 +20,7 @@ import {
19
  import {
20
  fetchNotes,
21
  uploadNote,
22
- fetchNoteBlob,
23
  createChatSession,
24
  streamChatRequest,
25
  fetchChatHistory,
@@ -44,7 +45,7 @@ const Notes: React.FC = () => {
44
  // --- Data State ---
45
  const [notes, setNotes] = useState<Note[]>([]);
46
  const [currentNote, setCurrentNote] = useState<Note | null>(null);
47
- const [pdfUrl, setPdfUrl] = useState<string | null>(null);
48
 
49
  // --- Edit/Rename State ---
50
  const [editingNoteId, setEditingNoteId] = useState<number | null>(null);
@@ -139,7 +140,7 @@ const Notes: React.FC = () => {
139
  setNotes((prev) => prev.filter((n) => n.id !== noteId));
140
  if (currentNote?.id === noteId) {
141
  setCurrentNote(null);
142
- setPdfUrl(null);
143
  setMessages([]);
144
  setSessionId(null);
145
  }
@@ -191,11 +192,10 @@ const Notes: React.FC = () => {
191
  setSessionId(null);
192
  setIsChatOpen(true);
193
 
194
- const token = localStorage.getItem("token");
195
-
196
- const secureUrl = `${getNoteContentUrl(note.id)}?token=${token}`;
197
-
198
- setPdfUrl(secureUrl);
199
 
200
  try {
201
  const existingSessions = await fetchSessions(note.id);
@@ -225,6 +225,7 @@ const Notes: React.FC = () => {
225
  console.error("Failed to init chat", error);
226
  }
227
  };
 
228
  const handleSendMessage = async () => {
229
  if (!inputMessage.trim() || !sessionId) return;
230
  const userMsg = inputMessage;
@@ -405,15 +406,10 @@ const Notes: React.FC = () => {
405
  </header>
406
 
407
  <div className="flex-1 w-full h-full relative p-4">
408
- {pdfUrl ? (
409
  <div className="w-full h-full rounded-xl overflow-hidden shadow-2xl border border-white/10 bg-white/5 backdrop-blur-sm">
410
- <iframe
411
- src={pdfUrl}
412
- className={`w-full h-full border-none ${
413
- isResizing ? "pointer-events-none" : ""
414
- }`}
415
- title="PDF Viewer"
416
- />
417
  </div>
418
  ) : (
419
  <div className="flex flex-col items-center justify-center h-full text-gray-400/50">
 
2
  import remarkGfm from "remark-gfm";
3
  import React, { useEffect, useState, useRef, useCallback } from "react";
4
  import { getNoteContentUrl } from "../api/notesService";
5
+ import SecurePdfViewer from "../components/SecurePdfViewer"; // <--- Imported new component
6
  import {
7
  Upload,
8
  Menu,
 
20
  import {
21
  fetchNotes,
22
  uploadNote,
23
+ // fetchNoteBlob, // Not needed as the component handles the fetch
24
  createChatSession,
25
  streamChatRequest,
26
  fetchChatHistory,
 
45
  // --- Data State ---
46
  const [notes, setNotes] = useState<Note[]>([]);
47
  const [currentNote, setCurrentNote] = useState<Note | null>(null);
48
+ // const [pdfUrl, setPdfUrl] = useState<string | null>(null); // <--- REMOVED
49
 
50
  // --- Edit/Rename State ---
51
  const [editingNoteId, setEditingNoteId] = useState<number | null>(null);
 
140
  setNotes((prev) => prev.filter((n) => n.id !== noteId));
141
  if (currentNote?.id === noteId) {
142
  setCurrentNote(null);
143
+ // setPdfUrl(null); // <--- REMOVED
144
  setMessages([]);
145
  setSessionId(null);
146
  }
 
192
  setSessionId(null);
193
  setIsChatOpen(true);
194
 
195
+ // --- PDF VIEWER LOGIC ---
196
+ // We no longer set pdfUrl state or append the token here.
197
+ // The SecurePdfViewer component handles the authenticated fetch internally
198
+ // using the currentNote.id prop.
 
199
 
200
  try {
201
  const existingSessions = await fetchSessions(note.id);
 
225
  console.error("Failed to init chat", error);
226
  }
227
  };
228
+
229
  const handleSendMessage = async () => {
230
  if (!inputMessage.trim() || !sessionId) return;
231
  const userMsg = inputMessage;
 
406
  </header>
407
 
408
  <div className="flex-1 w-full h-full relative p-4">
409
+ {currentNote ? (
410
  <div className="w-full h-full rounded-xl overflow-hidden shadow-2xl border border-white/10 bg-white/5 backdrop-blur-sm">
411
+ {/* FIX: Use the SecurePdfViewer component, passing the noteId */}
412
+ <SecurePdfViewer noteId={currentNote.id} />
 
 
 
 
 
413
  </div>
414
  ) : (
415
  <div className="flex flex-col items-center justify-center h-full text-gray-400/50">