Skip to content

Commit 93dd2ba

Browse files
committed
feat(demo): add PDF viewer panel with page navigation
Adds split-pane layout with PDF viewer on the left side: - PDFViewerPanel component renders PDF pages using PDFBox - Page navigation with previous/next buttons - PDF displays immediately on upload while indexing runs in background - Added javafx.swing module for SwingFXUtils image conversion - CSS styling for PDF viewer, navigation controls, and split pane
1 parent b7ec66a commit 93dd2ba

File tree

4 files changed

+333
-7
lines changed

4 files changed

+333
-7
lines changed

demos/rag-multimodal/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ repositories {
2424

2525
javafx {
2626
version = "21"
27-
modules = listOf("javafx.controls", "javafx.fxml", "javafx.web")
27+
modules = listOf("javafx.controls", "javafx.fxml", "javafx.web", "javafx.swing")
2828
}
2929

3030
application {

demos/rag-multimodal/src/main/java/com/redis/vl/demo/rag/ui/ChatController.java

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,18 @@
55
import com.redis.vl.demo.rag.model.LLMConfig;
66
import com.redis.vl.demo.rag.service.JTokKitCostTracker;
77
import com.redis.vl.demo.rag.service.RAGService;
8+
import java.io.File;
9+
import java.util.concurrent.ExecutorService;
10+
import java.util.concurrent.Executors;
811
import javafx.application.Platform;
912
import javafx.collections.FXCollections;
1013
import javafx.collections.ObservableList;
1114
import javafx.geometry.Insets;
15+
import javafx.geometry.Orientation;
1216
import javafx.geometry.Pos;
1317
import javafx.scene.control.*;
1418
import javafx.scene.layout.*;
1519
import javafx.stage.FileChooser;
16-
import java.io.File;
17-
import java.util.concurrent.ExecutorService;
18-
import java.util.concurrent.Executors;
1920

2021
/**
2122
* Main chat interface controller.
@@ -34,6 +35,8 @@ public class ChatController extends BorderPane {
3435
private final Label modelLabel;
3536
private final Label loadedPdfLabel;
3637
private Button uploadPdfButton;
38+
private final PDFViewerPanel pdfViewer;
39+
private File currentPdfFile;
3740

3841
private RAGService ragService;
3942
private com.redis.vl.demo.rag.service.ServiceFactory serviceFactory;
@@ -47,7 +50,12 @@ public ChatController() {
4750
uploadPdfButton = new Button("Upload PDF");
4851
uploadPdfButton.setOnAction(e -> uploadPdf());
4952

50-
// Message list (center)
53+
// PDF viewer panel (left side)
54+
pdfViewer = new PDFViewerPanel();
55+
pdfViewer.setPrefWidth(450);
56+
pdfViewer.setMinWidth(300);
57+
58+
// Message list (center/right side)
5159
messageListView = new ListView<>(messages);
5260
messageListView.setCellFactory(
5361
lv ->
@@ -64,7 +72,15 @@ protected void updateItem(ChatMessage item, boolean empty) {
6472
});
6573
messageListView.setPadding(new Insets(10));
6674
messageListView.getStyleClass().add("message-list");
67-
setCenter(messageListView);
75+
76+
// Use SplitPane for PDF viewer and chat
77+
SplitPane splitPane = new SplitPane();
78+
splitPane.setOrientation(Orientation.HORIZONTAL);
79+
splitPane.getItems().addAll(pdfViewer, messageListView);
80+
splitPane.setDividerPositions(0.4);
81+
SplitPane.setResizableWithParent(pdfViewer, false);
82+
83+
setCenter(splitPane);
6884

6985
// Right control panel
7086
VBox rightPanel = new VBox(15);
@@ -250,9 +266,18 @@ private void uploadPdf() {
250266

251267
File file = fileChooser.showOpenDialog(getScene().getWindow());
252268
if (file != null) {
269+
currentPdfFile = file;
253270
statusLabel.setText("Processing PDF: " + file.getName());
254271
addSystemMessage("Processing PDF: " + file.getName() + "...");
255272

273+
// Load PDF into viewer immediately (on JavaFX thread)
274+
try {
275+
pdfViewer.loadPDF(file);
276+
} catch (Exception e) {
277+
addSystemMessage("Warning: Could not display PDF preview: " + e.getMessage());
278+
}
279+
280+
// Index PDF in background
256281
executor.submit(
257282
() -> {
258283
try {
@@ -336,9 +361,21 @@ private void updateStatusLabel(String text) {
336361
}
337362

338363
/**
339-
* Shutdown executor on close.
364+
* Shutdown executor and close resources.
340365
*/
341366
public void shutdown() {
342367
executor.shutdown();
368+
if (pdfViewer != null) {
369+
pdfViewer.close();
370+
}
371+
}
372+
373+
/**
374+
* Gets the PDF viewer panel for external navigation.
375+
*
376+
* @return PDFViewerPanel instance
377+
*/
378+
public PDFViewerPanel getPdfViewer() {
379+
return pdfViewer;
343380
}
344381
}
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
package com.redis.vl.demo.rag.ui;
2+
3+
import java.awt.image.BufferedImage;
4+
import java.io.File;
5+
import java.io.IOException;
6+
import javafx.embed.swing.SwingFXUtils;
7+
import javafx.geometry.Insets;
8+
import javafx.geometry.Pos;
9+
import javafx.scene.control.Button;
10+
import javafx.scene.control.Label;
11+
import javafx.scene.control.ScrollPane;
12+
import javafx.scene.image.ImageView;
13+
import javafx.scene.layout.BorderPane;
14+
import javafx.scene.layout.HBox;
15+
import javafx.scene.layout.StackPane;
16+
import javafx.scene.layout.VBox;
17+
import org.apache.pdfbox.Loader;
18+
import org.apache.pdfbox.pdmodel.PDDocument;
19+
import org.apache.pdfbox.rendering.PDFRenderer;
20+
21+
/**
22+
* Panel for displaying PDF documents with page navigation.
23+
*
24+
* <p>Uses PDFBox to render PDF pages as images for display in JavaFX.
25+
*/
26+
public class PDFViewerPanel extends BorderPane {
27+
28+
private static final float RENDER_DPI = 100f;
29+
30+
private PDDocument document;
31+
private PDFRenderer renderer;
32+
private int currentPage = 0;
33+
private int totalPages = 0;
34+
35+
private final ImageView pageImageView;
36+
private final Label pageLabel;
37+
private final Label titleLabel;
38+
private final Button prevButton;
39+
private final Button nextButton;
40+
private final StackPane placeholder;
41+
private final ScrollPane scrollPane;
42+
43+
public PDFViewerPanel() {
44+
getStyleClass().add("pdf-viewer-panel");
45+
46+
// Title bar
47+
titleLabel = new Label("No PDF loaded");
48+
titleLabel.getStyleClass().add("pdf-title");
49+
50+
// Page image view
51+
pageImageView = new ImageView();
52+
pageImageView.setPreserveRatio(true);
53+
pageImageView.setSmooth(true);
54+
55+
// Wrap in ScrollPane for large pages
56+
scrollPane = new ScrollPane(pageImageView);
57+
scrollPane.setFitToWidth(true);
58+
scrollPane.setPannable(true);
59+
scrollPane.getStyleClass().add("pdf-scroll-pane");
60+
61+
// Placeholder when no PDF loaded
62+
placeholder = new StackPane();
63+
placeholder.getStyleClass().add("pdf-placeholder");
64+
Label placeholderLabel = new Label("Upload a PDF to view it here");
65+
placeholderLabel.getStyleClass().add("placeholder-text");
66+
placeholder.getChildren().add(placeholderLabel);
67+
68+
// Navigation controls
69+
prevButton = new Button("\u25C0");
70+
prevButton.getStyleClass().add("nav-button");
71+
prevButton.setOnAction(e -> previousPage());
72+
prevButton.setDisable(true);
73+
74+
nextButton = new Button("\u25B6");
75+
nextButton.getStyleClass().add("nav-button");
76+
nextButton.setOnAction(e -> nextPage());
77+
nextButton.setDisable(true);
78+
79+
pageLabel = new Label("0 / 0");
80+
pageLabel.getStyleClass().add("page-label");
81+
82+
HBox navBox = new HBox(10, prevButton, pageLabel, nextButton);
83+
navBox.setAlignment(Pos.CENTER);
84+
navBox.setPadding(new Insets(10));
85+
navBox.getStyleClass().add("nav-box");
86+
87+
// Header with title
88+
VBox header = new VBox(5, titleLabel);
89+
header.setAlignment(Pos.CENTER);
90+
header.setPadding(new Insets(10));
91+
header.getStyleClass().add("pdf-header");
92+
93+
setTop(header);
94+
setCenter(placeholder);
95+
setBottom(navBox);
96+
}
97+
98+
/**
99+
* Loads a PDF file for display.
100+
*
101+
* @param file PDF file to load
102+
* @throws IOException if loading fails
103+
*/
104+
public void loadPDF(File file) throws IOException {
105+
// Close previous document if open
106+
if (document != null) {
107+
document.close();
108+
}
109+
110+
document = Loader.loadPDF(file);
111+
renderer = new PDFRenderer(document);
112+
totalPages = document.getNumberOfPages();
113+
currentPage = 0;
114+
115+
titleLabel.setText(file.getName());
116+
117+
// Switch from placeholder to scroll pane
118+
setCenter(scrollPane);
119+
120+
updateNavigation();
121+
renderCurrentPage();
122+
}
123+
124+
/** Navigates to the previous page. */
125+
public void previousPage() {
126+
if (currentPage > 0) {
127+
currentPage--;
128+
updateNavigation();
129+
renderCurrentPage();
130+
}
131+
}
132+
133+
/** Navigates to the next page. */
134+
public void nextPage() {
135+
if (currentPage < totalPages - 1) {
136+
currentPage++;
137+
updateNavigation();
138+
renderCurrentPage();
139+
}
140+
}
141+
142+
/**
143+
* Navigates to a specific page.
144+
*
145+
* @param pageNumber Zero-based page number
146+
*/
147+
public void goToPage(int pageNumber) {
148+
if (pageNumber >= 0 && pageNumber < totalPages) {
149+
currentPage = pageNumber;
150+
updateNavigation();
151+
renderCurrentPage();
152+
}
153+
}
154+
155+
/**
156+
* Gets the current page number.
157+
*
158+
* @return Zero-based current page number
159+
*/
160+
public int getCurrentPage() {
161+
return currentPage;
162+
}
163+
164+
/**
165+
* Gets the total number of pages.
166+
*
167+
* @return Total page count
168+
*/
169+
public int getTotalPages() {
170+
return totalPages;
171+
}
172+
173+
private void updateNavigation() {
174+
prevButton.setDisable(currentPage <= 0);
175+
nextButton.setDisable(currentPage >= totalPages - 1);
176+
pageLabel.setText((currentPage + 1) + " / " + totalPages);
177+
}
178+
179+
private void renderCurrentPage() {
180+
if (renderer == null) return;
181+
182+
try {
183+
BufferedImage bufferedImage = renderer.renderImageWithDPI(currentPage, RENDER_DPI);
184+
pageImageView.setImage(SwingFXUtils.toFXImage(bufferedImage, null));
185+
186+
// Fit to width of scroll pane
187+
pageImageView.fitWidthProperty().bind(scrollPane.widthProperty().subtract(20));
188+
} catch (IOException e) {
189+
System.err.println("Failed to render page: " + e.getMessage());
190+
}
191+
}
192+
193+
/** Closes the PDF document and releases resources. */
194+
public void close() {
195+
if (document != null) {
196+
try {
197+
document.close();
198+
} catch (IOException e) {
199+
// Ignore
200+
}
201+
document = null;
202+
renderer = null;
203+
}
204+
}
205+
}

0 commit comments

Comments
 (0)