Skip to content

Commit

Permalink
Merge pull request #39 from InAnYan/fix-for-29
Browse files Browse the repository at this point in the history
  • Loading branch information
InAnYan authored Jun 4, 2024
2 parents 6ee1f0e + b2ab59e commit 51d682b
Show file tree
Hide file tree
Showing 10 changed files with 379 additions and 70 deletions.
1 change: 1 addition & 0 deletions src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -154,4 +154,5 @@
requires de.saxsys.mvvmfx.validation;
requires dd.plist;
requires mslinks;
requires com.dlsc.unitfx;
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,6 @@ public class AiChatTab extends EntryEditorTab {

private static final org.slf4j.Logger LOGGER = LoggerFactory.getLogger(AiChatTab.class.getName());

private static final String QA_SYSTEM_MESSAGE = """
You are an AI research assistant. You read and analyze scientific articles.
The user will send you a question regarding a paper. You will be supplied also with the relevant information found in the article.
Answer the question only by using the relevant information. Don't make up the answer.
If you can't answer the user question using the provided information, then reply that you couldn't do it.""";

private final DialogService dialogService;
private final FilePreferences filePreferences;
private final AiPreferences aiPreferences;
Expand Down Expand Up @@ -131,8 +125,7 @@ private void bindToCorrectEntry(BibEntry entry) {
}

private void createAiChat() {
aiChat = new AiChat(aiService, MetadataFilterBuilder.metadataKey("linkedFile").isIn(currentBibEntry.getFiles().stream().map(LinkedFile::getLink).toList()));
aiChat.setSystemMessage(QA_SYSTEM_MESSAGE);
aiChat = new AiChat(aiService, aiPreferences, MetadataFilterBuilder.metadataKey("linkedFile").isIn(currentBibEntry.getFiles().stream().map(LinkedFile::getLink).toList()));
}

private void ingestFiles(BibEntry entry) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,12 @@ protected ListProperty<PreferencesTab> filteredPreferenceTabsProperty() {

private static void scanLabeledControls(Parent parent, ArrayListMultimap<PreferencesTab, Labeled> prefsTabLabelMap, PreferencesTab preferencesTab) {
for (Node child : parent.getChildrenUnmodifiable()) {
if (!(child instanceof Labeled)) {
scanLabeledControls((Parent) child, prefsTabLabelMap, preferencesTab);
} else {
Labeled labeled = (Labeled) child;
if (child instanceof Labeled labeled) {
if (!labeled.getText().isEmpty()) {
prefsTabLabelMap.put(preferencesTab, labeled);
}
} else if (child instanceof Parent parentChild) {
scanLabeledControls(parentChild, prefsTabLabelMap, preferencesTab);
}
}
}
Expand Down
69 changes: 63 additions & 6 deletions src/main/java/org/jabref/gui/preferences/ai/AiTab.fxml
Original file line number Diff line number Diff line change
@@ -1,17 +1,74 @@
<?xml version="1.0" encoding="UTF-8"?>

<?import com.dlsc.unitfx.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.*?>
<?import org.controlsfx.control.textfield.*?>

<?import org.controlsfx.control.textfield.CustomPasswordField?>
<fx:root spacing="10.0" type="VBox" xmlns="http://javafx.com/javafx/17.0.2-ea" xmlns:fx="http://javafx.com/fxml/1" fx:controller="org.jabref.gui.preferences.ai.AiTab">
<children>
<CheckBox fx:id="enableChat" mnemonicParsing="false" text="%Enable chat with attached PDF files" />
<children>
<Label styleClass="titleHeader" text="%AI" />

<Label styleClass="sectionHeader" text="%General" />

<CheckBox fx:id="enableChat" mnemonicParsing="false" text="%Enable chat with attached PDF files" />

<HBox alignment="CENTER_LEFT" spacing="10.0">
<children>
<Label alignment="BASELINE_CENTER" text="%OpenAI API token" />
<CustomPasswordField fx:id="openAiToken" HBox.hgrow="ALWAYS" />
</children>
</HBox>

<Label styleClass="sectionHeader" text="%Expert settings" />
<VBox>
<children>

<Text strokeType="OUTSIDE" strokeWidth="0.0" text="%These parameters affect how AI will answer you questions." />
<Text strokeType="OUTSIDE" strokeWidth="0.0" text="%Leave these fields as is, if you are not sure of their purpose." />
</children>
</VBox>
<Label text="%System message" />
<TextArea fx:id="systemMessageTextArea" wrapText="true" />
<HBox alignment="CENTER_LEFT" spacing="10.0">
<children>
<Label alignment="BASELINE_CENTER" text="%OpenAI API token" />
<CustomPasswordField fx:id="openAiToken" HBox.hgrow="ALWAYS" />
<Label alignment="BASELINE_CENTER" text="%Message window size" />
<IntegerInputField fx:id="messageWindowSizeTextField" HBox.hgrow="ALWAYS" />
</children>
</HBox>
</children>
<HBox spacing="10.0">
<children>
<VBox HBox.hgrow="ALWAYS">
<children>
<Label text="%Document splitter - chunk size" />
<IntegerInputField fx:id="documentSplitterChunkSizeTextField" />
</children>
</VBox>
<VBox HBox.hgrow="ALWAYS">
<children>
<Label text="%Document splitter - overlap size" />
<IntegerInputField fx:id="documentSplitterOverlapSizeTextField" />
</children>
</VBox>
</children>
</HBox>

<HBox spacing="10.0">
<children>
<VBox HBox.hgrow="ALWAYS">
<children>
<Label text="%RAG - maximum results count" />
<IntegerInputField fx:id="ragMaxResultsCountTextField" />
</children>
</VBox>
<VBox HBox.hgrow="ALWAYS">
<children>
<Label text="%RAG - minimum score" />
<DoubleInputField fx:id="ragMinScoreTextField" />
</children>
</VBox>
</children>
</HBox>
</children>
</fx:root>
32 changes: 31 additions & 1 deletion src/main/java/org/jabref/gui/preferences/ai/AiTab.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,64 @@

import javafx.fxml.FXML;
import javafx.scene.control.CheckBox;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;

import org.jabref.gui.preferences.AbstractPreferenceTabView;
import org.jabref.gui.preferences.PreferencesTab;
import org.jabref.logic.l10n.Localization;

import com.airhacks.afterburner.views.ViewLoader;
import com.dlsc.unitfx.DoubleInputField;
import com.dlsc.unitfx.IntegerInputField;
import scala.Int;

public class AiTab extends AbstractPreferenceTabView<AiTabViewModel> implements PreferencesTab {
@FXML private CheckBox enableChat;
@FXML private TextField openAiToken;

@FXML private TextArea systemMessageTextArea;
@FXML private IntegerInputField messageWindowSizeTextField;
@FXML private IntegerInputField documentSplitterChunkSizeTextField;
@FXML private IntegerInputField documentSplitterOverlapSizeTextField;
@FXML private IntegerInputField ragMaxResultsCountTextField;
@FXML private DoubleInputField ragMinScoreTextField;

public AiTab() {
ViewLoader.view(this)
.root(this)
.load();
}

public void initialize() {
this.viewModel = new AiTabViewModel(preferencesService, dialogService);
this.viewModel = new AiTabViewModel(preferencesService);

enableChat.selectedProperty().bindBidirectional(viewModel.useAiProperty());
openAiToken.textProperty().bindBidirectional(viewModel.openAiTokenProperty());
systemMessageTextArea.textProperty().bindBidirectional(viewModel.systemMessageProperty());
messageWindowSizeTextField.valueProperty().bindBidirectional(viewModel.messageWindowSizeProperty().asObject());
documentSplitterChunkSizeTextField.valueProperty().bindBidirectional(viewModel.documentSplitterChunkSizeProperty().asObject());
documentSplitterOverlapSizeTextField.valueProperty().bindBidirectional(viewModel.documentSplitterOverlapSizeProperty().asObject());
ragMaxResultsCountTextField.valueProperty().bindBidirectional(viewModel.ragMaxResultsCountProperty().asObject());
ragMinScoreTextField.valueProperty().bindBidirectional(viewModel.ragMinScoreProperty().asObject());

openAiToken.setDisable(!enableChat.isSelected());
systemMessageTextArea.setDisable(!enableChat.isSelected());
messageWindowSizeTextField.setDisable(!enableChat.isSelected());
documentSplitterChunkSizeTextField.setDisable(!enableChat.isSelected());
documentSplitterOverlapSizeTextField.setDisable(!enableChat.isSelected());
ragMaxResultsCountTextField.setDisable(!enableChat.isSelected());
ragMinScoreTextField.setDisable(!enableChat.isSelected());

enableChat.selectedProperty().addListener((observable, oldValue, newValue) -> {
openAiToken.setDisable(!newValue);

systemMessageTextArea.setDisable(!newValue);
messageWindowSizeTextField.setDisable(!newValue);
documentSplitterChunkSizeTextField.setDisable(!newValue);
documentSplitterOverlapSizeTextField.setDisable(!newValue);
ragMaxResultsCountTextField.setDisable(!newValue);
ragMinScoreTextField.setDisable(!newValue);
});
}

Expand Down
112 changes: 95 additions & 17 deletions src/main/java/org/jabref/gui/preferences/ai/AiTabViewModel.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
package org.jabref.gui.preferences.ai;

import javafx.beans.property.BooleanProperty;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.FloatProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleFloatProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;

Expand All @@ -12,45 +18,93 @@
import org.jabref.preferences.AiPreferences;
import org.jabref.preferences.PreferencesService;

import com.dlsc.unitfx.DoubleInputField;
import de.saxsys.mvvmfx.utils.validation.FunctionBasedValidator;
import de.saxsys.mvvmfx.utils.validation.ValidationMessage;
import de.saxsys.mvvmfx.utils.validation.Validator;

public class AiTabViewModel implements PreferenceTabViewModel {
private final BooleanProperty useAi = new SimpleBooleanProperty();
private final StringProperty openAiToken = new SimpleStringProperty();
private final StringProperty systemMessage = new SimpleStringProperty();
private final IntegerProperty messageWindowSize = new SimpleIntegerProperty();
private final IntegerProperty documentSplitterChunkSize = new SimpleIntegerProperty();
private final IntegerProperty documentSplitterOverlapSize = new SimpleIntegerProperty();
private final IntegerProperty ragMaxResultsCount = new SimpleIntegerProperty();
private final DoubleProperty ragMinScore = new SimpleDoubleProperty();

private final AiPreferences aiPreferences;
private final DialogService dialogService;

public AiTabViewModel(PreferencesService preferencesService, DialogService dialogService) {
private final Validator openAiTokenValidator;
private final Validator messageWindowSizeValidator;
private final Validator documentSplitterChunkSizeValidator;
private final Validator documentSplitterOverlapSizeValidator;
private final Validator ragMaxResultsCountValidator;
private final Validator ragMinScoreValidator;

public AiTabViewModel(PreferencesService preferencesService) {
this.aiPreferences = preferencesService.getAiPreferences();
this.dialogService = dialogService;

this.openAiTokenValidator = new FunctionBasedValidator<>(
openAiToken,
(token) -> !StringUtil.isBlank(token),
ValidationMessage.error(Localization.lang("The OpenAI token cannot be empty")));

this.messageWindowSizeValidator = new FunctionBasedValidator<>(
messageWindowSize,
(size) -> (int)size > 0,
ValidationMessage.error(Localization.lang("Message window size must be greater than 0")));

this.documentSplitterChunkSizeValidator = new FunctionBasedValidator<>(
documentSplitterChunkSize,
(size) -> (int)size > 0,
ValidationMessage.error(Localization.lang("Document splitter chunk size must be greater than 0")));

this.documentSplitterOverlapSizeValidator = new FunctionBasedValidator<>(
documentSplitterOverlapSize,
(size) -> (int)size > 0 && (int)size < documentSplitterChunkSize.get(),
ValidationMessage.error(Localization.lang("Document splitter overlap size must be greater than 0 and less than chunk size")));

this.ragMaxResultsCountValidator = new FunctionBasedValidator<>(
ragMaxResultsCount,
(count) -> (int)count > 0,
ValidationMessage.error(Localization.lang("RAG max results count must be greater than 0")));

this.ragMinScoreValidator = new FunctionBasedValidator<>(
ragMinScore,
(score) -> (double)score > 0 && (double)score < 1,
ValidationMessage.error(Localization.lang("RAG min score must be greater than 0 and less than 1")));
}

@Override
public void setValues() {
useAi.setValue(aiPreferences.getEnableChatWithFiles());
openAiToken.setValue(aiPreferences.getOpenAiToken());

systemMessage.setValue(aiPreferences.getSystemMessage());
messageWindowSize.setValue(aiPreferences.getMessageWindowSize());
documentSplitterChunkSize.setValue(aiPreferences.getDocumentSplitterChunkSize());
documentSplitterOverlapSize.setValue(aiPreferences.getDocumentSplitterOverlapSize());
ragMaxResultsCount.setValue(aiPreferences.getRagMaxResultsCount());
ragMinScore.setValue(aiPreferences.getRagMinScore());
}

@Override
public void storeSettings() {
aiPreferences.setEnableChatWithFiles(useAi.get());
aiPreferences.setOpenAiToken(openAiToken.get());

aiPreferences.setSystemMessage(systemMessage.get());
aiPreferences.setMessageWindowSize(messageWindowSize.get());
aiPreferences.setDocumentSplitterChunkSize(documentSplitterChunkSize.get());
aiPreferences.setDocumentSplitterOverlapSize(documentSplitterOverlapSize.get());
aiPreferences.setRagMaxResultsCount(ragMaxResultsCount.get());
aiPreferences.setRagMinScore(ragMinScore.get());
}

@Override
public boolean validateSettings() {
if (useAi.get()) {
return validateOpenAiToken();
}

return true;
}

private boolean validateOpenAiToken() {
if (StringUtil.isBlank(openAiToken.get())) {
dialogService.showErrorDialogAndWait(Localization.lang("Format error"), Localization.lang("The OpenAI token cannot be empty"));
return false;
}

return true;
return openAiTokenValidator.getValidationStatus().isValid() && messageWindowSizeValidator.getValidationStatus().isValid() && documentSplitterChunkSizeValidator.getValidationStatus().isValid() && documentSplitterOverlapSizeValidator.getValidationStatus().isValid() && ragMaxResultsCountValidator.getValidationStatus().isValid() && ragMinScoreValidator.getValidationStatus().isValid();
}

public StringProperty openAiTokenProperty() {
Expand All @@ -60,4 +114,28 @@ public StringProperty openAiTokenProperty() {
public BooleanProperty useAiProperty() {
return useAi;
}

public StringProperty systemMessageProperty() {
return systemMessage;
}

public IntegerProperty messageWindowSizeProperty() {
return messageWindowSize;
}

public IntegerProperty documentSplitterChunkSizeProperty() {
return documentSplitterChunkSize;
}

public IntegerProperty documentSplitterOverlapSizeProperty() {
return documentSplitterOverlapSize;
}

public IntegerProperty ragMaxResultsCountProperty() {
return ragMaxResultsCount;
}

public DoubleProperty ragMinScoreProperty() {
return ragMinScore;
}
}
Loading

0 comments on commit 51d682b

Please sign in to comment.