Skip to content

Commit

Permalink
Add support for scite.ai (JabRef#10617)
Browse files Browse the repository at this point in the history
* task: first cut of Scite tab and associated preference

* task: comments and minor refactoring

* task: Updated readme.md for issue #375

* fix: Import ordering and markdown space

* fix: imports and whitespace (checkstyle)

* fix: imports and whitespace (checkstyle)

* fix: imports and whitespace (checkstyle)

* fix: Removed unnecessary parentheses (OpenRewrite)

* fix: Unit tests and localisation keys

* fix: Removed unnecessary heading in CHANGELOG.md

* fix: Made SciteTallyDTO a record type and moved it to new file

* fix: Made the scite.ai base url a constant

* fix: Exception handling now catches most specific exceptions, and rethrows them as FetcherException

* fix: style for scite message box

* fix: Catch specific exception and display error message rather than throwing a generic unchecked exception

* fix: Localization for SciteTab error messages, and "Scite" name is no longer localized

* some small refactoring

* checkstyle
and rename

* Update CHANGELOG.md

* Update EntryEditorTab.java

* move and checkstyle

* fix l10n and rename

---------

Co-authored-by: Siedlerchr <siedlerkiller@gmail.com>
Co-authored-by: Carl Christian Snethlage <50491877+calixtus@users.noreply.github.com>
  • Loading branch information
3 people authored Nov 9, 2023
1 parent 934a0c8 commit 8a800e6
Show file tree
Hide file tree
Showing 14 changed files with 490 additions and 2 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 the option to use System Preference for Light/Dark Theme [#8729](https://github.com/JabRef/jabref/issues/8729).
Expand Down
18 changes: 18 additions & 0 deletions src/main/java/org/jabref/gui/entryeditor/EntryEditor.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
3 changes: 3 additions & 0 deletions src/main/java/org/jabref/gui/entryeditor/EntryEditor.java
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ private List<EntryEditorTab> 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<String, Set<Field>> tab : entryEditorTabList.entrySet()) {
Expand Down Expand Up @@ -302,6 +303,8 @@ private List<EntryEditorTab> createTabs() {

entryEditorTabs.add(new FulltextSearchResultsTab(stateManager, preferencesService, dialogService, taskExecutor));

entryEditorTabs.add(new SciteTab(preferencesService, taskExecutor, dialogService));

return entryEditorTabs;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ public static JournalPopupEnabled fromString(String status) {
private final DoubleProperty dividerPosition;
private final BooleanProperty autoLinkFiles;
private final ObjectProperty<JournalPopupEnabled> enablementStatus;
private final BooleanProperty shouldShowSciteTab;

public EntryEditorPreferences(Map<String, Set<Field>> entryEditorTabList,
Map<String, Set<Field>> defaultEntryEditorTabList,
Expand All @@ -58,7 +59,8 @@ public EntryEditorPreferences(Map<String, Set<Field>> 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));
Expand All @@ -71,6 +73,7 @@ public EntryEditorPreferences(Map<String, Set<Field>> entryEditorTabList,
this.dividerPosition = new SimpleDoubleProperty(dividerPosition);
this.autoLinkFiles = new SimpleBooleanProperty(autolinkFilesEnabled);
this.enablementStatus = new SimpleObjectProperty<>(journalPopupEnabled);
this.shouldShowSciteTab = new SimpleBooleanProperty(showSciteTab);
}

public ObservableMap<String, Set<Field>> getEntryEditorTabs() {
Expand Down Expand Up @@ -196,4 +199,16 @@ public ObjectProperty<JournalPopupEnabled> 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);
}
}
131 changes: 131 additions & 0 deletions src/main/java/org/jabref/gui/entryeditor/SciteTab.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
131 changes: 131 additions & 0 deletions src/main/java/org/jabref/gui/entryeditor/SciteTabViewModel.java
Original file line number Diff line number Diff line change
@@ -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<SciteStatus> status;
private final StringProperty searchError;
private Optional<SciteTallyModel> 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<SciteStatus> statusProperty() {
return status;
}

public StringProperty searchErrorProperty() {
return searchError;
}

public Optional<SciteTallyModel> getCurrentResult() {
return currentResult;
}
}
34 changes: 34 additions & 0 deletions src/main/java/org/jabref/gui/entryeditor/SciteTallyModel.java
Original file line number Diff line number Diff line change
@@ -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")
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
<CheckBox fx:id="enableValidation" text="%Show validation messages"/>
<CheckBox fx:id="allowIntegerEdition" text="%Allow integers in 'edition' field in BibTeX mode"/>
<CheckBox fx:id="journalPopupEnabled" text="%Fetch journal information online to show"/>
<CheckBox fx:id="enableSciteTab" text="%Show 'Scite' tab"/>

<Label styleClass="sectionHeader" text="%Custom editor tabs"/>
<HBox>
Expand Down
Loading

0 comments on commit 8a800e6

Please sign in to comment.