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 |
-
|
| 195 |
-
|
| 196 |
-
|
| 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 |
-
{
|
| 409 |
<div className="w-full h-full rounded-xl overflow-hidden shadow-2xl border border-white/10 bg-white/5 backdrop-blur-sm">
|
| 410 |
-
|
| 411 |
-
|
| 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">
|