From 5b5ff36881ff5a5d8dc1fce2dabce5e814bc44d9 Mon Sep 17 00:00:00 2001 From: Will Baird Date: Fri, 3 Nov 2023 11:55:31 +1100 Subject: [PATCH 01/22] task: first cut of Scite tab and associated preference --- .../jabref/gui/entryeditor/EntryEditor.java | 3 + .../entryeditor/EntryEditorPreferences.java | 17 +- .../org/jabref/gui/entryeditor/SciteTab.java | 121 +++++++++++ .../gui/entryeditor/SciteTabViewModel.java | 201 ++++++++++++++++++ .../entryeditor/EntryEditorTab.fxml | 1 + .../entryeditor/EntryEditorTab.java | 6 +- .../entryeditor/EntryEditorTabViewModel.java | 7 + .../jabref/preferences/JabRefPreferences.java | 7 +- src/main/resources/l10n/JabRef_en.properties | 1 + .../jabref/gui/entryeditor/SciteTabTest.java | 68 ++++++ .../entryeditor/SciteTabViewModelTest.java | 65 ++++++ 11 files changed, 493 insertions(+), 4 deletions(-) create mode 100644 src/main/java/org/jabref/gui/entryeditor/SciteTab.java create mode 100644 src/main/java/org/jabref/gui/entryeditor/SciteTabViewModel.java create mode 100644 src/test/java/org/jabref/gui/entryeditor/SciteTabTest.java create mode 100644 src/test/java/org/jabref/gui/entryeditor/SciteTabViewModelTest.java diff --git a/src/main/java/org/jabref/gui/entryeditor/EntryEditor.java b/src/main/java/org/jabref/gui/entryeditor/EntryEditor.java index 3d42d562a26..d75a09ed664 100644 --- a/src/main/java/org/jabref/gui/entryeditor/EntryEditor.java +++ b/src/main/java/org/jabref/gui/entryeditor/EntryEditor.java @@ -273,6 +273,7 @@ private List createTabs() { entryEditorTabList.remove(RelatedArticlesTab.NAME); entryEditorTabList.remove(LatexCitationsTab.NAME); entryEditorTabList.remove(FulltextSearchResultsTab.NAME); + entryEditorTabList.remove(SciteTab.NAME); entryEditorTabList.remove("Comments"); // Then show the remaining configured for (Map.Entry> tab : entryEditorTabList.entrySet()) { @@ -302,6 +303,8 @@ private List createTabs() { entryEditorTabs.add(new FulltextSearchResultsTab(stateManager, preferencesService, dialogService, taskExecutor)); + entryEditorTabs.add(new SciteTab(preferencesService, taskExecutor)); + return entryEditorTabs; } diff --git a/src/main/java/org/jabref/gui/entryeditor/EntryEditorPreferences.java b/src/main/java/org/jabref/gui/entryeditor/EntryEditorPreferences.java index 73431acb78d..c2afea3b0af 100644 --- a/src/main/java/org/jabref/gui/entryeditor/EntryEditorPreferences.java +++ b/src/main/java/org/jabref/gui/entryeditor/EntryEditorPreferences.java @@ -47,6 +47,7 @@ public static JournalPopupEnabled fromString(String status) { private final DoubleProperty dividerPosition; private final BooleanProperty autoLinkFiles; private final ObjectProperty enablementStatus; + private final BooleanProperty shouldShowSciteTab; public EntryEditorPreferences(Map> entryEditorTabList, Map> defaultEntryEditorTabList, @@ -58,7 +59,8 @@ public EntryEditorPreferences(Map> entryEditorTabList, boolean allowIntegerEditionBibtex, double dividerPosition, boolean autolinkFilesEnabled, - JournalPopupEnabled journalPopupEnabled) { + JournalPopupEnabled journalPopupEnabled, + boolean showSciteTab) { this.entryEditorTabList = new SimpleMapProperty<>(FXCollections.observableMap(entryEditorTabList)); this.defaultEntryEditorTabList = new SimpleMapProperty<>(FXCollections.observableMap(defaultEntryEditorTabList)); @@ -71,6 +73,7 @@ public EntryEditorPreferences(Map> entryEditorTabList, this.dividerPosition = new SimpleDoubleProperty(dividerPosition); this.autoLinkFiles = new SimpleBooleanProperty(autolinkFilesEnabled); this.enablementStatus = new SimpleObjectProperty<>(journalPopupEnabled); + this.shouldShowSciteTab = new SimpleBooleanProperty(showSciteTab); } public ObservableMap> getEntryEditorTabs() { @@ -196,4 +199,16 @@ public ObjectProperty enableJournalPopupProperty() { public void setEnableJournalPopup(JournalPopupEnabled journalPopupEnabled) { this.enablementStatus.set(journalPopupEnabled); } + + public boolean shouldShowSciteTab() { + return this.shouldShowSciteTab.get(); + } + + public BooleanProperty shouldShowLSciteTabProperty() { + return this.shouldShowSciteTab; + } + + public void setShouldShowSciteTab(boolean shouldShowSciteTab) { + this.shouldShowSciteTab.set(shouldShowSciteTab); + } } diff --git a/src/main/java/org/jabref/gui/entryeditor/SciteTab.java b/src/main/java/org/jabref/gui/entryeditor/SciteTab.java new file mode 100644 index 00000000000..bb0e2255768 --- /dev/null +++ b/src/main/java/org/jabref/gui/entryeditor/SciteTab.java @@ -0,0 +1,121 @@ +package org.jabref.gui.entryeditor; + +import com.tobiasdiez.easybind.EasyBind; +import javafx.geometry.HPos; +import javafx.scene.control.Hyperlink; +import javafx.scene.control.Label; +import javafx.scene.control.ProgressIndicator; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.*; +import javafx.scene.text.Text; +import org.controlsfx.control.HyperlinkLabel; +import org.jabref.gui.desktop.JabRefDesktop; +import org.jabref.gui.util.TaskExecutor; +import org.jabref.logic.l10n.Localization; +import org.jabref.model.entry.BibEntry; +import org.jabref.preferences.PreferencesService; +import java.net.URLEncoder; + +public class SciteTab extends EntryEditorTab { + + public static final String NAME = "Scite"; + + private final GridPane searchPane; + private final ProgressIndicator progressIndicator; + private final SciteTabViewModel viewModel; + private final PreferencesService preferencesService; + + public SciteTab(PreferencesService preferencesService, TaskExecutor taskExecutor) { + this.preferencesService = preferencesService; + this.viewModel = new SciteTabViewModel(preferencesService, taskExecutor); + this.searchPane = new GridPane(); + + this.progressIndicator = new ProgressIndicator(); + + setText(Localization.lang("Scite")); + setTooltip(new Tooltip(Localization.lang("Search scite.ai for Smart Citations"))); + setSearchPane(); + } + + private void setSearchPane() { + progressIndicator.setMaxSize(100, 100); + searchPane.add(progressIndicator, 0, 0); + + ColumnConstraints column = new ColumnConstraints(); + column.setPercentWidth(100); + column.setHalignment(HPos.CENTER); + + searchPane.getColumnConstraints().setAll(column); + searchPane.setId("scitePane"); + setContent(searchPane); + + EasyBind.subscribe(viewModel.statusProperty(), status -> { + searchPane.getChildren().clear(); + switch (status) { + case IN_PROGRESS: + searchPane.add(progressIndicator, 0, 0); + break; + case FOUND: + if (viewModel.getCurrentResult().isPresent()) { + searchPane.add(getTalliesPane(viewModel.getCurrentResult().get()), 0, 0); + } + break; + case ERROR: + searchPane.add(getErrorPane(), 0, 0); + break; + } + + }); + + } + + @Override + public boolean shouldShow(BibEntry entry) { + return viewModel.shouldShow(); + } + + @Override + protected void bindToEntry(BibEntry entry) { + viewModel.bindToEntry(entry); + } + + private VBox getErrorPane() { + Label titleLabel = new Label(Localization.lang("Error")); + titleLabel.setStyle("-fx-font-size: 1.5em;-fx-font-weight: bold;-fx-text-fill: -fx-accent;"); + Text errorMessageText = new Text(viewModel.searchErrorProperty().get()); + VBox errorMessageBox = new VBox(30, titleLabel, errorMessageText); + errorMessageBox.setStyle("-fx-padding: 30 0 0 30;"); + return errorMessageBox; + } + + private VBox getTalliesPane(SciteTabViewModel.SciteTallyDTO tallyDTO) { + Label titleLabel = new Label(Localization.lang("Tallies for " + tallyDTO.getDoi())); + titleLabel.setStyle("-fx-font-size: 1.5em;-fx-font-weight: bold;"); + Text message = new Text(String.format("Total Citations: %d\nSupporting: %d\nContradicting: %d\nMentioning: %d\nUnclassified: %d\nCiting Publications: %d", + tallyDTO.getTotal(), + tallyDTO.getSupporting(), + tallyDTO.getContradicting(), + tallyDTO.getMentioning(), + tallyDTO.getUnclassified(), + tallyDTO.getCitingPublications() + )); + + String url = "https://scite.ai/reports/" + URLEncoder.encode(tallyDTO.getDoi()); + HyperlinkLabel link = new HyperlinkLabel(String.format("See full report at [%s]", url)); + link.setOnAction((event) -> { + if (event.getSource() instanceof Hyperlink) { + var filePreferences = preferencesService.getFilePreferences(); + try { + JabRefDesktop.openBrowser(url, filePreferences); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + }); + + VBox messageBox = new VBox(30, titleLabel, message, link); + messageBox.setStyle("-fx-padding: 30 0 0 30;"); + return messageBox; + } + +} diff --git a/src/main/java/org/jabref/gui/entryeditor/SciteTabViewModel.java b/src/main/java/org/jabref/gui/entryeditor/SciteTabViewModel.java new file mode 100644 index 00000000000..0da3c8b606d --- /dev/null +++ b/src/main/java/org/jabref/gui/entryeditor/SciteTabViewModel.java @@ -0,0 +1,201 @@ +package org.jabref.gui.entryeditor; + +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import kong.unirest.json.JSONObject; +import org.jabref.gui.AbstractViewModel; +import org.jabref.gui.util.BackgroundTask; +import org.jabref.gui.util.TaskExecutor; +import org.jabref.logic.net.URLDownload; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.identifier.DOI; +import org.jabref.preferences.PreferencesService; +import org.tinylog.Logger; +import java.net.URL; +import java.util.Optional; +import java.util.concurrent.Future; + +public class SciteTabViewModel extends AbstractViewModel { + + enum Status { + IN_PROGRESS, + FOUND, + ERROR + } + + private static final String BASE_URL = "https://api.scite.ai/"; + + private final PreferencesService preferencesService; + private final TaskExecutor taskExecutor; + private final ObjectProperty status; + private final StringProperty searchError; + private Optional currentResult = Optional.empty(); + + private Future searchTask; + + public SciteTabViewModel(PreferencesService preferencesService, TaskExecutor taskExecutor) { + this.preferencesService = preferencesService; + this.taskExecutor = taskExecutor; + this.status = new SimpleObjectProperty<>(Status.IN_PROGRESS); + this.searchError = new SimpleStringProperty(""); + } + + public boolean shouldShow() { + return preferencesService.getEntryEditorPreferences().shouldShowSciteTab(); + } + + public void bindToEntry(BibEntry entry) { + cancelSearch(); + + if (entry == null) { + searchError.set("Null Entry!"); + status.set(Status.ERROR); + return; + } + + if (entry.getDOI().isEmpty()) { + searchError.set("This entry does not have a DOI"); + status.set(Status.ERROR); + return; + } + + searchTask = BackgroundTask.wrap(() -> fetchTallies(entry.getDOI().get())) + .onRunning(() -> status.set(Status.IN_PROGRESS)) + .onSuccess(result -> { + currentResult = Optional.of(result); + status.set(Status.FOUND); + }) + .onFailure(error -> { + searchError.set(error.getMessage()); + status.set(Status.ERROR); + }) + .executeWith(taskExecutor); + + } + + private void cancelSearch() { + if (searchTask == null || searchTask.isCancelled() || searchTask.isDone()) { + return; + } + + status.set(Status.IN_PROGRESS); + searchTask.cancel(true); + } + + + public SciteTallyDTO fetchTallies(DOI doi) { + try { + URL url = new URL(BASE_URL + "tallies/" + doi.getDOI()); + URLDownload download = new URLDownload(url); + String response = download.asString(); + Logger.debug("Response {}", response); + JSONObject tallies = new JSONObject(response); + if (tallies.has("detail")) { + String message = tallies.getString("detail"); + throw new RuntimeException(message); + } else if (!tallies.has("total")) { + throw new RuntimeException("Unexpected result data!"); + } + + return SciteTallyDTO.fromJSONObject(tallies); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + public ObjectProperty statusProperty() { + return status; + } + + public StringProperty searchErrorProperty() { + return searchError; + } + + public Optional getCurrentResult() { + return currentResult; + } + + public static class SciteTallyDTO { + + private String doi; + private int total; + private int supporting; + private int contradicting; + private int mentioning; + private int unclassified; + private int citingPublications; + + public static SciteTallyDTO fromJSONObject(JSONObject jsonObject) { + SciteTallyDTO dto = new SciteTallyDTO(); + + dto.setDoi(jsonObject.getString("doi")); + dto.setTotal(jsonObject.getInt("total")); + dto.setSupporting(jsonObject.getInt("supporting")); + dto.setContradicting(jsonObject.getInt("contradicting")); + dto.setMentioning(jsonObject.getInt("mentioning")); + dto.setUnclassified(jsonObject.getInt("unclassified")); + dto.setCitingPublications(jsonObject.getInt("citingPublications")); + return dto; + } + + public String getDoi() { + return doi; + } + + public void setDoi(String doi) { + this.doi = doi; + } + + public int getTotal() { + return total; + } + + public void setTotal(int total) { + this.total = total; + } + + public int getSupporting() { + return supporting; + } + + public void setSupporting(int supporting) { + this.supporting = supporting; + } + + public int getContradicting() { + return contradicting; + } + + public void setContradicting(int contradicting) { + this.contradicting = contradicting; + } + + public int getMentioning() { + return mentioning; + } + + public void setMentioning(int mentioning) { + this.mentioning = mentioning; + } + + public int getUnclassified() { + return unclassified; + } + + public void setUnclassified(int unclassified) { + this.unclassified = unclassified; + } + + public int getCitingPublications() { + return citingPublications; + } + + public void setCitingPublications(int citingPublications) { + this.citingPublications = citingPublications; + } + + } + +} diff --git a/src/main/java/org/jabref/gui/preferences/entryeditor/EntryEditorTab.fxml b/src/main/java/org/jabref/gui/preferences/entryeditor/EntryEditorTab.fxml index 373d531eb8d..85b787fd4cb 100644 --- a/src/main/java/org/jabref/gui/preferences/entryeditor/EntryEditorTab.fxml +++ b/src/main/java/org/jabref/gui/preferences/entryeditor/EntryEditorTab.fxml @@ -34,6 +34,7 @@ +