diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fc518a512a..ce3eda2907b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv ### Added +- We added a scite.ai tab in the entry editor that retrieves 'Smart Citation' tallies for citations that have a DOI. [koppor#375](https://github.com/koppor/jabref/issues/375) - We added a dropdown menu to let users change the reference library during AUX file import. [#10472](https://github.com/JabRef/jabref/issues/10472) - We added a button to let users reset the cite command to the default value. [#10569](https://github.com/JabRef/jabref/issues/10569) - We added [scholar.archive.org](https://scholar.archive.org/) as a new fetcher. [#10498](https://github.com/JabRef/jabref/issues/10498) diff --git a/src/main/java/org/jabref/gui/entryeditor/EntryEditor.css b/src/main/java/org/jabref/gui/entryeditor/EntryEditor.css index 4b2befd4dd3..fc3a3446644 100644 --- a/src/main/java/org/jabref/gui/entryeditor/EntryEditor.css +++ b/src/main/java/org/jabref/gui/entryeditor/EntryEditor.css @@ -146,3 +146,21 @@ .description { -fx-font-style: italic; } + +.scite-tallies-label { + -fx-font-size: 1.5em; + -fx-font-weight: bold; +} + +.scite-error-box { + -fx-padding: 30 0 0 30; +} +.scite-message-box { + -fx-padding: 30 0 0 30; +} + +.scite-error-label { + -fx-font-size: 1.5em; + -fx-font-weight: bold; + -fx-text-fill: -fx-accent; +} diff --git a/src/main/java/org/jabref/gui/entryeditor/EntryEditor.java b/src/main/java/org/jabref/gui/entryeditor/EntryEditor.java index 3d42d562a26..7ee6f1b6968 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, dialogService)); + 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..2033a945a5c --- /dev/null +++ b/src/main/java/org/jabref/gui/entryeditor/SciteTab.java @@ -0,0 +1,131 @@ +package org.jabref.gui.entryeditor; + +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + +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.ColumnConstraints; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.VBox; +import javafx.scene.text.Text; + +import org.jabref.gui.DialogService; +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 com.tobiasdiez.easybind.EasyBind; +import org.controlsfx.control.HyperlinkLabel; + +public class SciteTab extends EntryEditorTab { + + public static final String NAME = "Scite"; + public static final String SCITE_REPORTS_URL_BASE = "https://scite.ai/reports/"; + + private final GridPane sciteResultsPane; + private final ProgressIndicator progressIndicator; + private final SciteTabViewModel viewModel; + private final PreferencesService preferencesService; + private final DialogService dialogService; + + public SciteTab(PreferencesService preferencesService, TaskExecutor taskExecutor, DialogService dialogService) { + this.preferencesService = preferencesService; + this.viewModel = new SciteTabViewModel(preferencesService, taskExecutor); + this.dialogService = dialogService; + this.sciteResultsPane = new GridPane(); + this.progressIndicator = new ProgressIndicator(); + setText(NAME); + setTooltip(new Tooltip(Localization.lang("Search scite.ai for Smart Citations"))); + setSciteResultsPane(); + } + + private void setSciteResultsPane() { + progressIndicator.setMaxSize(100, 100); + sciteResultsPane.add(progressIndicator, 0, 0); + + ColumnConstraints column = new ColumnConstraints(); + column.setPercentWidth(100); + column.setHalignment(HPos.CENTER); + + sciteResultsPane.getColumnConstraints().setAll(column); + sciteResultsPane.setId("scitePane"); + setContent(sciteResultsPane); + + EasyBind.subscribe(viewModel.statusProperty(), status -> { + sciteResultsPane.getChildren().clear(); + switch (status) { + case IN_PROGRESS -> + sciteResultsPane.add(progressIndicator, 0, 0); + case FOUND -> + viewModel.getCurrentResult().ifPresent(result -> sciteResultsPane.add(getTalliesPane(result), 0, 0)); + case ERROR -> + sciteResultsPane.add(getErrorPane(), 0, 0); + } + }); + } + + @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.getStyleClass().add("scite-error-label"); + Text errorMessageText = new Text(viewModel.searchErrorProperty().get()); + VBox errorMessageBox = new VBox(30, titleLabel, errorMessageText); + errorMessageBox.getStyleClass().add("scite-error-box"); + return errorMessageBox; + } + + private VBox getTalliesPane(SciteTallyModel tallModel) { + Label titleLabel = new Label(Localization.lang("Tallies for %0", tallModel.doi())); + titleLabel.getStyleClass().add("scite-tallies-label"); + Text message = new Text(String.format("Total Citations: %d\nSupporting: %d\nContradicting: %d\nMentioning: %d\nUnclassified: %d\nCiting Publications: %d", + tallModel.total(), + tallModel.supporting(), + tallModel.contradicting(), + tallModel.mentioning(), + tallModel.unclassified(), + tallModel.citingPublications() + )); + + String url = SCITE_REPORTS_URL_BASE + URLEncoder.encode(tallModel.doi(), StandardCharsets.UTF_8); + VBox messageBox = getMessageBox(url, titleLabel, message); + messageBox.getStyleClass().add("scite-message-box"); + return messageBox; + } + + private VBox getMessageBox(String url, Label titleLabel, Text message) { + HyperlinkLabel link = new HyperlinkLabel(Localization.lang("See full report at [%0]", url)); + link.setOnAction(event -> { + if (event.getSource() instanceof Hyperlink) { + var filePreferences = preferencesService.getFilePreferences(); + try { + JabRefDesktop.openBrowser(url, filePreferences); + } catch (IOException ioex) { + // Can't throw a checked exception from here, so display a message to the user instead. + dialogService.showErrorDialogAndWait( + "An error occurred opening web browser", + "JabRef was unable to open a web browser for link:\n\n" + url + "\n\nError Message:\n\n" + ioex.getMessage(), + ioex + ); + } + } + }); + + return new VBox(30, titleLabel, message, link); + } +} 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..f05ab1ac94b --- /dev/null +++ b/src/main/java/org/jabref/gui/entryeditor/SciteTabViewModel.java @@ -0,0 +1,131 @@ +package org.jabref.gui.entryeditor; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Optional; +import java.util.concurrent.Future; + +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; + +import org.jabref.gui.AbstractViewModel; +import org.jabref.gui.util.BackgroundTask; +import org.jabref.gui.util.TaskExecutor; +import org.jabref.logic.importer.FetcherException; +import org.jabref.logic.l10n.Localization; +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 kong.unirest.json.JSONObject; +import org.tinylog.Logger; + +public class SciteTabViewModel extends AbstractViewModel { + + /** + * Status enum for Scite tab + */ + public enum SciteStatus { + 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<>(SciteStatus.IN_PROGRESS); + this.searchError = new SimpleStringProperty(""); + } + + public boolean shouldShow() { + return preferencesService.getEntryEditorPreferences().shouldShowSciteTab(); + } + + public void bindToEntry(BibEntry entry) { + // If a search is already running, cancel it + cancelSearch(); + + if (entry == null) { + searchError.set(Localization.lang("No active entry")); + status.set(SciteStatus.ERROR); + return; + } + + // The scite.ai api requires a DOI + if (entry.getDOI().isEmpty()) { + searchError.set(Localization.lang("This entry does not have a DOI")); + status.set(SciteStatus.ERROR); + return; + } + + searchTask = BackgroundTask.wrap(() -> fetchTallies(entry.getDOI().get())) + .onRunning(() -> status.set(SciteStatus.IN_PROGRESS)) + .onSuccess(result -> { + currentResult = Optional.of(result); + status.set(SciteStatus.FOUND); + }) + .onFailure(error -> { + searchError.set(error.getMessage()); + status.set(SciteStatus.ERROR); + }) + .executeWith(taskExecutor); + } + + private void cancelSearch() { + if (searchTask == null || searchTask.isCancelled() || searchTask.isDone()) { + return; + } + + status.set(SciteStatus.IN_PROGRESS); + searchTask.cancel(true); + } + + public SciteTallyModel fetchTallies(DOI doi) throws FetcherException { + try { + URL url = new URI(BASE_URL + "tallies/" + doi.getDOI()).toURL(); + 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 FetcherException(message); + } else if (!tallies.has("total")) { + throw new FetcherException("Unexpected result data!"); + } + return SciteTallyModel.fromJSONObject(tallies); + } catch (MalformedURLException | URISyntaxException ex) { + throw new FetcherException("Malformed url for DOs", ex); + } catch (IOException ioex) { + throw new FetcherException("Failed to retrieve tallies for DOI - IO Exception", ioex); + } + } + + public ObjectProperty statusProperty() { + return status; + } + + public StringProperty searchErrorProperty() { + return searchError; + } + + public Optional getCurrentResult() { + return currentResult; + } +} diff --git a/src/main/java/org/jabref/gui/entryeditor/SciteTallyModel.java b/src/main/java/org/jabref/gui/entryeditor/SciteTallyModel.java new file mode 100644 index 00000000000..b3fbe24ea58 --- /dev/null +++ b/src/main/java/org/jabref/gui/entryeditor/SciteTallyModel.java @@ -0,0 +1,34 @@ +package org.jabref.gui.entryeditor; + +import kong.unirest.json.JSONObject; + +/** + * Simple model object to hold the scite.ai tallies data for a given DOI + */ +public record SciteTallyModel( + String doi, + int total, + int supporting, + int contradicting, + int mentioning, + int unclassified, + int citingPublications) { + + /** + * Creates a {@link SciteTallyModel} from a JSONObject (dictionary/map) + * + * @param jsonObject The JSON object holding the tally values + * @return a new {@link SciteTallyModel} + */ + public static SciteTallyModel fromJSONObject(JSONObject jsonObject) { + return new SciteTallyModel( + jsonObject.getString("doi"), + jsonObject.getInt("total"), + jsonObject.getInt("supporting"), + jsonObject.getInt("contradicting"), + jsonObject.getInt("mentioning"), + jsonObject.getInt("unclassified"), + jsonObject.getInt("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 @@ +