From 7f7726e7edd736507def67e1292cf701d1929863 Mon Sep 17 00:00:00 2001 From: Ruslan Date: Wed, 4 Sep 2024 16:34:39 +0300 Subject: [PATCH] Chat with groups + AI chat UI and logic overhaul (#11666) * AI chat UI overhaul * Chat history and AI chat overhaul * Implement ingestion logic * Implement ingestion logic * Accept list of entries * Group chat history logic * Move components to folders and introduce AiPrivacyNoticeGuardedComponent * Move components to folders and introduce AiPrivacyNoticeGuardedComponent * Introduce EmbeddingModelGuardedComponent * SummaryComponent overhaul * Summarization logic overhaul * Finish work on refactoring the summarization * Introduce group chats * Introduce group chat * Refactor AI code * AI chat UI overhaul * Chat history and AI chat overhaul * Implement ingestion logic * Implement ingestion logic * Accept list of entries * Group chat history logic * Move components to folders and introduce AiPrivacyNoticeGuardedComponent * Move components to folders and introduce AiPrivacyNoticeGuardedComponent * Introduce EmbeddingModelGuardedComponent * SummaryComponent overhaul * Summarization logic overhaul * Finish work on refactoring the summarization * Introduce group chats * Introduce group chat * Refactor AI code * Fix GenerateEmbeddingsTask * Introduce windows * Refactor notifications * No linting errors occurring * Improve chat history component * Discard changes to src/main/java/module-info.java * Refine JavaDoc * Fix for https://github.com/InAnYan/jabref/issues/173 * Fix for https://github.com/InAnYan/jabref/issues/171 * Fix from code review * Fix checkers * Fix for https://github.com/InAnYan/jabref/issues/175 and https://github.com/InAnYan/jabref/issues/174 * Follow-up for https://github.com/InAnYan/jabref/issues/175 * Fix for https://github.com/InAnYan/jabref/issues/166 * Fix for https://github.com/InAnYan/jabref/issues/164 * Fix checkers * Fix from code review * Update src/main/java/org/jabref/gui/ai/components/aichat/AiChatGuardedComponent.java Co-authored-by: Oliver Kopp * Fix for https://github.com/InAnYan/jabref/issues/166 and fix for https://github.com/InAnYan/jabref/issues/176 * Might be fix for https://github.com/InAnYan/jabref/issues/178 * Migrate from AbstractGroup to GroupTreeNode * Fix for https://github.com/InAnYan/jabref/issues/170 * Fix for https://github.com/InAnYan/jabref/issues/164 * https://github.com/InAnYan/jabref/issues/177 * Fix for https://github.com/InAnYan/jabref/issues/180 * Fix checkers * Fix the system message * Fix for https://github.com/InAnYan/jabref/issues/180 * Fix context menu order * Fix `startsWith` * Fix system message * Fix for ProgressCounter * Fix for system message --------- Co-authored-by: Oliver Kopp --- src/main/java/org/jabref/gui/Base.css | 21 + .../java/org/jabref/gui/DialogService.java | 8 + .../org/jabref/gui/JabRefDialogService.java | 10 + src/main/java/org/jabref/gui/JabRefGUI.java | 8 +- .../java/org/jabref/gui/StateManager.java | 7 + .../jabref/gui/actions/StandardActions.java | 1 + .../jabref/gui/ai/ClearEmbeddingsAction.java | 4 +- .../ai/components/aichat/AiChatComponent.fxml | 92 ++-- .../ai/components/aichat/AiChatComponent.java | 412 ++++++++---------- .../aichat/AiChatGuardedComponent.java | 74 ++++ .../ai/components/aichat/AiChatWindow.java | 67 +++ .../chathistory/ChatHistoryComponent.fxml | 22 + .../chathistory/ChatHistoryComponent.java | 53 +++ .../chatmessage/ChatMessageComponent.fxml | 12 +- .../chatmessage/ChatMessageComponent.java | 57 ++- .../chatprompt/ChatPromptComponent.fxml | 25 ++ .../chatprompt/ChatPromptComponent.java | 193 ++++++++ .../apikeymissing/ApiKeyMissingComponent.fxml | 25 -- .../apikeymissing/ApiKeyMissingComponent.java | 30 -- .../AiPrivacyNoticeGuardedComponent.java | 44 ++ .../privacynotice/PrivacyNoticeComponent.java | 17 +- .../components/summary/SummaryComponent.java | 172 ++++++-- ...nent.fxml => SummaryShowingComponent.fxml} | 2 +- .../summary/SummaryShowingComponent.java | 53 +++ .../util/EmbeddingModelGuardedComponent.java | 77 ++++ .../gui/ai/components/util/Loadable.java | 23 + .../errorstate/ErrorStateComponent.fxml | 2 +- .../errorstate/ErrorStateComponent.java | 2 +- .../util/notifications/Notification.java | 12 + .../notifications/NotificationComponent.java | 33 ++ .../notifications/NotificationsComponent.java | 31 ++ .../org/jabref/gui/entryeditor/AiChatTab.java | 217 ++------- .../jabref/gui/entryeditor/AiSummaryTab.java | 229 ++-------- .../jabref/gui/entryeditor/EntryEditor.java | 4 +- .../org/jabref/gui/groups/GroupTreeView.java | 15 +- .../jabref/gui/groups/GroupTreeViewModel.java | 32 +- ...ListenForCitationKeyChangeForAiAction.java | 23 - .../importer/actions/OpenDatabaseAction.java | 4 +- .../gui/sidepane/SidePaneContentFactory.java | 3 +- .../java/org/jabref/gui/util/BaseWindow.java | 51 +++ .../gui/util/DynamicallyChangeableNode.java | 24 + .../java/org/jabref/logic/ai/AiChatLogic.java | 147 ------- .../jabref/logic/ai/AiDefaultPreferences.java | 2 +- .../java/org/jabref/logic/ai/AiService.java | 176 +++++--- .../logic/ai/GenerateEmbeddingsTask.java | 125 ------ .../logic/ai/chathistory/AiChatHistory.java | 15 - .../BibDatabaseChatHistoryManager.java | 165 ------- .../ai/chathistory/InMemoryAiChatHistory.java | 30 -- .../jabref/logic/ai/chatting/AiChatLogic.java | 179 ++++++++ .../logic/ai/chatting/AiChatService.java | 56 +++ .../ai/chatting/JabRefContentInjector.java | 68 +++ .../chathistory/ChatHistoryService.java | 298 +++++++++++++ .../chathistory/ChatHistoryStorage.java | 20 + .../storages/MVStoreChatHistoryStorage.java | 135 ++++++ .../model}/JabRefChatLanguageModel.java | 7 +- .../model}/JvmOpenAiChatLanguageModel.java | 2 +- .../FileEmbeddingsManager.java | 53 +-- .../FileToDocument.java | 28 +- .../FullyIngestedDocumentsTracker.java | 20 + .../GenerateEmbeddingsForSeveralTask.java | 113 +++++ .../ai/ingestion/GenerateEmbeddingsTask.java | 132 ++++++ .../logic/ai/ingestion/IngestionService.java | 118 +++++ .../LowLevelIngestor.java | 2 +- .../MVStoreEmbeddingStore.java | 48 +- .../model}/DeepJavaEmbeddingModel.java | 2 +- .../model}/JabRefEmbeddingModel.java | 2 +- .../model}/UpdateEmbeddingModelTask.java | 2 +- ...MVStoreFullyIngestedDocumentsTracker.java} | 45 +- .../ai/processingstatus/ProcessingInfo.java | 57 +++ .../ai/processingstatus/ProcessingState.java | 8 + .../ai/summarization/GenerateSummaryTask.java | 106 ++--- .../ai/summarization/SummariesService.java | 137 ++++++ .../ai/summarization/SummariesStorage.java | 90 +--- .../logic/ai/summarization/Summary.java | 8 + .../storages/MVStoreSummariesStorage.java | 45 ++ .../logic/ai/util/CitationKeyCheck.java | 18 + .../logic/ai/{misc => util}/ErrorMessage.java | 2 +- .../org/jabref/logic/ai/util/MVStoreBase.java | 46 ++ .../pdf/InterruptablePDFTextStripper.java | 26 ++ .../jabref/logic/util/ProgressCounter.java | 10 +- src/main/java/org/jabref/model/TreeNode.java | 5 + .../java/org/jabref/model/entry/BibEntry.java | 8 + .../jabref/model/groups/GroupTreeNode.java | 49 ++- .../java/org/jabref/model/util/ListUtil.java | 18 + src/main/resources/l10n/JabRef_en.properties | 34 +- .../gui/groups/GroupTreeViewModelTest.java | 11 +- .../BibDatabaseChatHistoryManagerTest.java | 50 --- .../chathistory/ChatHistoryStorageTest.java | 63 +++ .../MVStoreChatHistoryStorageTest.java | 20 + .../FullyIngestedDocumentsTrackerTest.java | 52 +++ ...toreFullyIngestedDocumentsTrackerTest.java | 20 + .../MVStoreSummariesStorageTest.java | 20 + .../summarization/SummariesStorageTest.java | 24 +- 93 files changed, 3431 insertions(+), 1677 deletions(-) create mode 100644 src/main/java/org/jabref/gui/ai/components/aichat/AiChatGuardedComponent.java create mode 100644 src/main/java/org/jabref/gui/ai/components/aichat/AiChatWindow.java create mode 100644 src/main/java/org/jabref/gui/ai/components/aichat/chathistory/ChatHistoryComponent.fxml create mode 100644 src/main/java/org/jabref/gui/ai/components/aichat/chathistory/ChatHistoryComponent.java rename src/main/java/org/jabref/gui/ai/components/{ => aichat}/chatmessage/ChatMessageComponent.fxml (83%) rename src/main/java/org/jabref/gui/ai/components/{ => aichat}/chatmessage/ChatMessageComponent.java (66%) create mode 100644 src/main/java/org/jabref/gui/ai/components/aichat/chatprompt/ChatPromptComponent.fxml create mode 100644 src/main/java/org/jabref/gui/ai/components/aichat/chatprompt/ChatPromptComponent.java delete mode 100644 src/main/java/org/jabref/gui/ai/components/apikeymissing/ApiKeyMissingComponent.fxml delete mode 100644 src/main/java/org/jabref/gui/ai/components/apikeymissing/ApiKeyMissingComponent.java create mode 100644 src/main/java/org/jabref/gui/ai/components/privacynotice/AiPrivacyNoticeGuardedComponent.java rename src/main/java/org/jabref/gui/ai/components/summary/{SummaryComponent.fxml => SummaryShowingComponent.fxml} (93%) create mode 100644 src/main/java/org/jabref/gui/ai/components/summary/SummaryShowingComponent.java create mode 100644 src/main/java/org/jabref/gui/ai/components/util/EmbeddingModelGuardedComponent.java create mode 100644 src/main/java/org/jabref/gui/ai/components/util/Loadable.java rename src/main/java/org/jabref/gui/ai/components/{ => util}/errorstate/ErrorStateComponent.fxml (90%) rename src/main/java/org/jabref/gui/ai/components/{ => util}/errorstate/ErrorStateComponent.java (97%) create mode 100644 src/main/java/org/jabref/gui/ai/components/util/notifications/Notification.java create mode 100644 src/main/java/org/jabref/gui/ai/components/util/notifications/NotificationComponent.java create mode 100644 src/main/java/org/jabref/gui/ai/components/util/notifications/NotificationsComponent.java delete mode 100644 src/main/java/org/jabref/gui/importer/actions/ListenForCitationKeyChangeForAiAction.java create mode 100644 src/main/java/org/jabref/gui/util/BaseWindow.java create mode 100644 src/main/java/org/jabref/gui/util/DynamicallyChangeableNode.java delete mode 100644 src/main/java/org/jabref/logic/ai/AiChatLogic.java delete mode 100644 src/main/java/org/jabref/logic/ai/GenerateEmbeddingsTask.java delete mode 100644 src/main/java/org/jabref/logic/ai/chathistory/AiChatHistory.java delete mode 100644 src/main/java/org/jabref/logic/ai/chathistory/BibDatabaseChatHistoryManager.java delete mode 100644 src/main/java/org/jabref/logic/ai/chathistory/InMemoryAiChatHistory.java create mode 100644 src/main/java/org/jabref/logic/ai/chatting/AiChatLogic.java create mode 100644 src/main/java/org/jabref/logic/ai/chatting/AiChatService.java create mode 100644 src/main/java/org/jabref/logic/ai/chatting/JabRefContentInjector.java create mode 100644 src/main/java/org/jabref/logic/ai/chatting/chathistory/ChatHistoryService.java create mode 100644 src/main/java/org/jabref/logic/ai/chatting/chathistory/ChatHistoryStorage.java create mode 100644 src/main/java/org/jabref/logic/ai/chatting/chathistory/storages/MVStoreChatHistoryStorage.java rename src/main/java/org/jabref/logic/ai/{models => chatting/model}/JabRefChatLanguageModel.java (96%) rename src/main/java/org/jabref/logic/ai/{models => chatting/model}/JvmOpenAiChatLanguageModel.java (98%) rename src/main/java/org/jabref/logic/ai/{ => ingestion}/FileEmbeddingsManager.java (62%) rename src/main/java/org/jabref/logic/ai/{embeddings => ingestion}/FileToDocument.java (63%) create mode 100644 src/main/java/org/jabref/logic/ai/ingestion/FullyIngestedDocumentsTracker.java create mode 100644 src/main/java/org/jabref/logic/ai/ingestion/GenerateEmbeddingsForSeveralTask.java create mode 100644 src/main/java/org/jabref/logic/ai/ingestion/GenerateEmbeddingsTask.java create mode 100644 src/main/java/org/jabref/logic/ai/ingestion/IngestionService.java rename src/main/java/org/jabref/logic/ai/{embeddings => ingestion}/LowLevelIngestor.java (98%) rename src/main/java/org/jabref/logic/ai/{embeddings => ingestion}/MVStoreEmbeddingStore.java (75%) rename src/main/java/org/jabref/logic/ai/{models => ingestion/model}/DeepJavaEmbeddingModel.java (97%) rename src/main/java/org/jabref/logic/ai/{models => ingestion/model}/JabRefEmbeddingModel.java (99%) rename src/main/java/org/jabref/logic/ai/{models => ingestion/model}/UpdateEmbeddingModelTask.java (98%) rename src/main/java/org/jabref/logic/ai/{embeddings/FullyIngestedDocumentsTracker.java => ingestion/storages/MVStoreFullyIngestedDocumentsTracker.java} (53%) create mode 100644 src/main/java/org/jabref/logic/ai/processingstatus/ProcessingInfo.java create mode 100644 src/main/java/org/jabref/logic/ai/processingstatus/ProcessingState.java create mode 100644 src/main/java/org/jabref/logic/ai/summarization/SummariesService.java create mode 100644 src/main/java/org/jabref/logic/ai/summarization/Summary.java create mode 100644 src/main/java/org/jabref/logic/ai/summarization/storages/MVStoreSummariesStorage.java create mode 100644 src/main/java/org/jabref/logic/ai/util/CitationKeyCheck.java rename src/main/java/org/jabref/logic/ai/{misc => util}/ErrorMessage.java (96%) create mode 100644 src/main/java/org/jabref/logic/ai/util/MVStoreBase.java create mode 100644 src/main/java/org/jabref/logic/pdf/InterruptablePDFTextStripper.java delete mode 100644 src/test/java/org/jabref/logic/ai/chathistory/BibDatabaseChatHistoryManagerTest.java create mode 100644 src/test/java/org/jabref/logic/ai/chatting/chathistory/ChatHistoryStorageTest.java create mode 100644 src/test/java/org/jabref/logic/ai/chatting/chathistory/MVStoreChatHistoryStorageTest.java create mode 100644 src/test/java/org/jabref/logic/ai/ingestion/FullyIngestedDocumentsTrackerTest.java create mode 100644 src/test/java/org/jabref/logic/ai/ingestion/MVStoreFullyIngestedDocumentsTrackerTest.java create mode 100644 src/test/java/org/jabref/logic/ai/summarization/MVStoreSummariesStorageTest.java diff --git a/src/main/java/org/jabref/gui/Base.css b/src/main/java/org/jabref/gui/Base.css index 9dce2d09d23..3832348936a 100644 --- a/src/main/java/org/jabref/gui/Base.css +++ b/src/main/java/org/jabref/gui/Base.css @@ -1496,3 +1496,24 @@ We want to have a look that matches our icons in the tool-bar */ -fx-padding: 0.5em 2em; -fx-min-width: 10em; } + +.chat-message-text-area { + -fx-border-radius: 10; + -fx-background-radius: 10; +} + +/* region: fix for making text area round corners (source: https://stackoverflow.com/a/49617953) */ + +.chat-message-text-area .scroll-pane { + -fx-background-color: transparent; +} + +.chat-message-text-area .scroll-pane .viewport { + -fx-background-color: transparent; +} + +.chat-message-text-area .scroll-pane .content { + -fx-background-color: transparent; +} + +/* endregion */ diff --git a/src/main/java/org/jabref/gui/DialogService.java b/src/main/java/org/jabref/gui/DialogService.java index 5a6cc7d7919..03c505068a8 100644 --- a/src/main/java/org/jabref/gui/DialogService.java +++ b/src/main/java/org/jabref/gui/DialogService.java @@ -18,6 +18,7 @@ import javafx.util.StringConverter; import org.jabref.gui.util.BaseDialog; +import org.jabref.gui.util.BaseWindow; import org.jabref.gui.util.DirectoryDialogConfiguration; import org.jabref.gui.util.FileDialogConfiguration; import org.jabref.logic.importer.FetcherException; @@ -185,6 +186,13 @@ boolean showConfirmationDialogWithOptOutAndWait(String title, String content, */ void showCustomDialog(BaseDialog dialog); + /** + * Shows a custom window. + * + * @param window window to show + */ + void showCustomWindow(BaseWindow window); + /** * This will create and display a new dialog of the specified * {@link Alert.AlertType} but with user defined buttons as optional diff --git a/src/main/java/org/jabref/gui/JabRefDialogService.java b/src/main/java/org/jabref/gui/JabRefDialogService.java index b92954bff36..9d907840e43 100644 --- a/src/main/java/org/jabref/gui/JabRefDialogService.java +++ b/src/main/java/org/jabref/gui/JabRefDialogService.java @@ -43,6 +43,7 @@ import org.jabref.gui.icon.IconTheme; import org.jabref.gui.util.BackgroundTask; import org.jabref.gui.util.BaseDialog; +import org.jabref.gui.util.BaseWindow; import org.jabref.gui.util.DirectoryDialogConfiguration; import org.jabref.gui.util.FileDialogConfiguration; import org.jabref.gui.util.UiTaskExecutor; @@ -510,4 +511,13 @@ public void showCustomDialog(BaseDialog aboutDialogView) { } aboutDialogView.show(); } + + @Override + public void showCustomWindow(BaseWindow window) { + if (window.getOwner() == null) { + window.initOwner(mainWindow); + } + window.applyStylesheets(mainWindow.getScene().getStylesheets()); + window.show(); + } } diff --git a/src/main/java/org/jabref/gui/JabRefGUI.java b/src/main/java/org/jabref/gui/JabRefGUI.java index 6eecfb025e3..3e09d3f5c39 100644 --- a/src/main/java/org/jabref/gui/JabRefGUI.java +++ b/src/main/java/org/jabref/gui/JabRefGUI.java @@ -157,7 +157,13 @@ public void initialize() { JabRefGUI.clipBoardManager = new ClipBoardManager(); Injector.setModelOrService(ClipBoardManager.class, clipBoardManager); - JabRefGUI.aiService = new AiService(preferencesService.getAiPreferences(), Injector.instantiateModelOrService(AiApiKeyProvider.class), dialogService, taskExecutor); + JabRefGUI.aiService = new AiService( + preferencesService.getAiPreferences(), + preferencesService.getFilePreferences(), + preferencesService.getCitationKeyPatternPreferences(), + Injector.instantiateModelOrService(AiApiKeyProvider.class), + dialogService, + taskExecutor); Injector.setModelOrService(AiService.class, aiService); } diff --git a/src/main/java/org/jabref/gui/StateManager.java b/src/main/java/org/jabref/gui/StateManager.java index b2df5fe0251..017c5d8b320 100644 --- a/src/main/java/org/jabref/gui/StateManager.java +++ b/src/main/java/org/jabref/gui/StateManager.java @@ -18,6 +18,7 @@ import javafx.scene.Node; import javafx.util.Pair; +import org.jabref.gui.ai.components.aichat.AiChatWindow; import org.jabref.gui.edit.automaticfiededitor.LastAutomaticFieldEditorEdit; import org.jabref.gui.search.SearchType; import org.jabref.gui.sidepane.SidePaneType; @@ -45,6 +46,7 @@ *
  • active number of search results
  • *
  • focus owner
  • *
  • dialog window sizes/positions
  • + *
  • opened AI chat window (controlled by {@link org.jabref.logic.ai.AiService})
  • * */ public class StateManager { @@ -69,6 +71,7 @@ public class StateManager { private final ObservableList visibleSidePanes = FXCollections.observableArrayList(); private final ObjectProperty lastAutomaticFieldEditorEdit = new SimpleObjectProperty<>(); private final ObservableList searchHistory = FXCollections.observableArrayList(); + private final List aiChatWindows = new ArrayList<>(); public ObservableList getVisibleSidePaneComponents() { return visibleSidePanes; @@ -207,4 +210,8 @@ public List getLastSearchHistory(int size) { public void clearSearchHistory() { searchHistory.clear(); } + + public List getAiChatWindows() { + return aiChatWindows; + } } diff --git a/src/main/java/org/jabref/gui/actions/StandardActions.java b/src/main/java/org/jabref/gui/actions/StandardActions.java index f2a5f260e41..7f44fb8810f 100644 --- a/src/main/java/org/jabref/gui/actions/StandardActions.java +++ b/src/main/java/org/jabref/gui/actions/StandardActions.java @@ -193,6 +193,7 @@ public enum StandardActions implements Action { GROUP_REMOVE(Localization.lang("Remove group")), GROUP_REMOVE_KEEP_SUBGROUPS(Localization.lang("Keep subgroups")), GROUP_REMOVE_WITH_SUBGROUPS(Localization.lang("Also remove subgroups")), + GROUP_CHAT(Localization.lang("Chat with group")), GROUP_EDIT(Localization.lang("Edit group")), GROUP_SUBGROUP_ADD(Localization.lang("Add subgroup")), GROUP_SUBGROUP_REMOVE(Localization.lang("Remove subgroups")), diff --git a/src/main/java/org/jabref/gui/ai/ClearEmbeddingsAction.java b/src/main/java/org/jabref/gui/ai/ClearEmbeddingsAction.java index 2bb569fc587..6b76d7af67d 100644 --- a/src/main/java/org/jabref/gui/ai/ClearEmbeddingsAction.java +++ b/src/main/java/org/jabref/gui/ai/ClearEmbeddingsAction.java @@ -46,7 +46,7 @@ public void execute() { dialogService.notify(Localization.lang("Clearing embeddings cache...")); - List linkedFile = stateManager + List linkedFiles = stateManager .getActiveDatabase() .get() .getDatabase() @@ -55,7 +55,7 @@ public void execute() { .flatMap(entry -> entry.getFiles().stream()) .toList(); - BackgroundTask.wrap(() -> aiService.getEmbeddingsManager().clearEmbeddingsFor(linkedFile)) + BackgroundTask.wrap(() -> aiService.getIngestionService().clearEmbeddingsFor(linkedFiles)) .executeWith(taskExecutor); } } diff --git a/src/main/java/org/jabref/gui/ai/components/aichat/AiChatComponent.fxml b/src/main/java/org/jabref/gui/ai/components/aichat/AiChatComponent.fxml index 9e962a9d657..fa24208105e 100644 --- a/src/main/java/org/jabref/gui/ai/components/aichat/AiChatComponent.fxml +++ b/src/main/java/org/jabref/gui/ai/components/aichat/AiChatComponent.fxml @@ -3,47 +3,63 @@ - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + - + + + + + + + + diff --git a/src/main/java/org/jabref/gui/ai/components/aichat/AiChatComponent.java b/src/main/java/org/jabref/gui/ai/components/aichat/AiChatComponent.java index 2c0cb9c193a..839b9708cb0 100644 --- a/src/main/java/org/jabref/gui/ai/components/aichat/AiChatComponent.java +++ b/src/main/java/org/jabref/gui/ai/components/aichat/AiChatComponent.java @@ -1,79 +1,87 @@ package org.jabref.gui.ai.components.aichat; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; import java.util.stream.Stream; -import javafx.application.Platform; -import javafx.beans.property.BooleanProperty; -import javafx.beans.property.IntegerProperty; -import javafx.beans.property.SimpleBooleanProperty; -import javafx.beans.property.SimpleIntegerProperty; +import javafx.beans.property.StringProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.scene.control.Button; import javafx.scene.control.Label; -import javafx.scene.control.ProgressIndicator; -import javafx.scene.control.ScrollPane; -import javafx.scene.input.KeyCode; -import javafx.scene.layout.BorderPane; -import javafx.scene.layout.HBox; -import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; +import javafx.scene.paint.Color; import org.jabref.gui.DialogService; -import org.jabref.gui.ai.components.chatmessage.ChatMessageComponent; +import org.jabref.gui.ai.components.aichat.chathistory.ChatHistoryComponent; +import org.jabref.gui.ai.components.aichat.chatprompt.ChatPromptComponent; +import org.jabref.gui.ai.components.util.Loadable; +import org.jabref.gui.ai.components.util.notifications.Notification; +import org.jabref.gui.ai.components.util.notifications.NotificationsComponent; +import org.jabref.gui.icon.IconTheme; import org.jabref.gui.util.BackgroundTask; import org.jabref.gui.util.TaskExecutor; -import org.jabref.logic.ai.AiChatLogic; -import org.jabref.logic.ai.misc.ErrorMessage; +import org.jabref.logic.ai.AiService; +import org.jabref.logic.ai.chatting.AiChatLogic; +import org.jabref.logic.ai.util.CitationKeyCheck; +import org.jabref.logic.ai.util.ErrorMessage; import org.jabref.logic.l10n.Localization; +import org.jabref.logic.util.io.FileUtil; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.util.ListUtil; import org.jabref.preferences.ai.AiPreferences; import com.airhacks.afterburner.views.ViewLoader; -import com.dlsc.gemsfx.ExpandingTextArea; import dev.langchain4j.data.message.AiMessage; import dev.langchain4j.data.message.ChatMessage; import dev.langchain4j.data.message.UserMessage; +import org.controlsfx.control.PopOver; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class AiChatComponent extends VBox { private static final Logger LOGGER = LoggerFactory.getLogger(AiChatComponent.class); - // If current message that user is typing in prompt is non-existent, new, or empty, then we use - // this value in currentUserMessageScroll. - private static final int NEW_NON_EXISTENT_MESSAGE = -1; - + private final AiService aiService; + private final ObservableList entries; + private final BibDatabaseContext bibDatabaseContext; private final AiPreferences aiPreferences; - private final AiChatLogic aiChatLogic; - private final String citationKey; private final DialogService dialogService; private final TaskExecutor taskExecutor; - private final IntegerProperty blockScroll = new SimpleIntegerProperty(0); - - // This property stores index of a user history message. - // When user scrolls history in the prompt, this value is updated. - // Whenever user edits the prompt, this value is reset to NEW_NON_EXISTENT_MESSAGE. - private final IntegerProperty currentUserMessageScroll = new SimpleIntegerProperty(NEW_NON_EXISTENT_MESSAGE); + private final AiChatLogic aiChatLogic; - // If the current content of the prompt is a history message, then this property is true. - // If user begins to edit or type a new text, then this property is false. - private final BooleanProperty showingHistoryMessage = new SimpleBooleanProperty(false); + private final ObservableList notifications = FXCollections.observableArrayList(); - @FXML private ScrollPane scrollPane; - @FXML private VBox chatVBox; - @FXML private HBox promptHBox; - @FXML private ExpandingTextArea userPromptTextArea; - @FXML private Button submitButton; - @FXML private StackPane stackPane; + @FXML private Loadable uiLoadableChatHistory; + @FXML private ChatHistoryComponent uiChatHistory; + @FXML private Button notificationsButton; + @FXML private ChatPromptComponent chatPrompt; @FXML private Label noticeText; - public AiChatComponent(AiPreferences aiPreferences, AiChatLogic aiChatLogic, String citationKey, DialogService dialogService, TaskExecutor taskExecutor) { + public AiChatComponent(AiService aiService, + StringProperty name, + ObservableList chatHistory, + ObservableList entries, + BibDatabaseContext bibDatabaseContext, + AiPreferences aiPreferences, + DialogService dialogService, + TaskExecutor taskExecutor + ) { + this.aiService = aiService; + this.entries = entries; + this.bibDatabaseContext = bibDatabaseContext; this.aiPreferences = aiPreferences; - this.aiChatLogic = aiChatLogic; - this.citationKey = citationKey; this.dialogService = dialogService; this.taskExecutor = taskExecutor; + this.aiChatLogic = aiService.getAiChatService().makeChat(name, chatHistory, entries, bibDatabaseContext); + + aiService.getIngestionService().ingest(name, ListUtil.getLinkedFiles(entries).toList(), bibDatabaseContext); + ViewLoader.view(this) .root(this) .load(); @@ -81,237 +89,183 @@ public AiChatComponent(AiPreferences aiPreferences, AiChatLogic aiChatLogic, Str @FXML public void initialize() { - userPromptTextArea.setOnKeyPressed(keyEvent -> { - if (keyEvent.getCode() == KeyCode.DOWN) { - // Do not go down in the history. - if (currentUserMessageScroll.get() != NEW_NON_EXISTENT_MESSAGE) { - showingHistoryMessage.set(true); - currentUserMessageScroll.set(currentUserMessageScroll.get() - 1); - - // There could be two effects after setting the properties: - // 1) User scrolls to a recent message, then we should properly update the prompt text. - // 2) Scroll is set to -1 (which is NEW_NON_EXISTENT_MESSAGE) and we should clear the prompt text. - // On the second event currentUserMessageScroll will be set to -1 and showingHistoryMessage - // will be true (this is important). - } - } else if (keyEvent.getCode() == KeyCode.UP) { - if ((currentUserMessageScroll.get() < getReversedUserMessagesStream().count() - 1) && (userPromptTextArea.getText().isEmpty() || showingHistoryMessage.get())) { - // 1. We should not go up the maximum number of user messages. - // 2. We can scroll history only on two conditions: - // 1) The prompt is empty. - // 2) User has already been scrolling the history. - showingHistoryMessage.set(true); - currentUserMessageScroll.set(currentUserMessageScroll.get() + 1); - } - } else { - // Cursor left/right should not stop history scrolling - if (keyEvent.getCode() != KeyCode.RIGHT && keyEvent.getCode() != KeyCode.LEFT) { - // It is okay to go back and forth in the prompt while showing a history message. - // But if user begins doing something else, we should not track the history and reset - // all the properties. - showingHistoryMessage.set(false); - currentUserMessageScroll.set(NEW_NON_EXISTENT_MESSAGE); - } - - if (keyEvent.getCode() == KeyCode.ENTER) { - if (keyEvent.isControlDown()) { - userPromptTextArea.appendText("\n"); - } else { - onSendMessage(); - } - } - } - }); - - currentUserMessageScroll.addListener((observable, oldValue, newValue) -> { - // When currentUserMessageScroll is reset, then its value is - // 1) either to NEW_NON_EXISTENT_MESSAGE, - // 2) or to a new history entry. - if (newValue.intValue() != NEW_NON_EXISTENT_MESSAGE && showingHistoryMessage.get()) { - if (userPromptTextArea.getCaretPosition() == 0 || !userPromptTextArea.getText().contains("\n")) { - // If there are new lines in the prompt, then it is ambiguous whether the user tries to scroll up or down in history or editing lines in the current prompt. - // The easy way to get rid of this ambiguity is to disallow scrolling when there are new lines in the prompt. - // But the exception to this situation is when the caret position is at the beginning of the prompt. - getReversedUserMessagesStream() - .skip(newValue.intValue()) - .findFirst() - .ifPresent(userMessage -> - userPromptTextArea.setText(userMessage.singleText())); - } - } else { - // When currentUserMessageScroll is set to NEW_NON_EXISTENT_MESSAGE, then we should: - // 1) either clear the prompt, if user scrolls down the most recent history entry. - // 2) do nothing, if user starts to edit the history entry. - // We distinguish these two cases by checking showingHistoryMessage, which is true for -1 message, and false for others. - if (showingHistoryMessage.get()) { - userPromptTextArea.setText(""); - } - } - }); + uiChatHistory.setItems(aiChatLogic.getChatHistory()); + initializeChatPrompt(); + initializeNotice(); + initializeNotifications(); + } - scrollPane.needsLayoutProperty().addListener((observable, oldValue, newValue) -> { - if (!newValue) { - if (blockScroll.get() == 0) { - scrollPane.setVvalue(1.0); - } else { - blockScroll.set(blockScroll.get() - 1); - } - } - }); + private void initializeNotifications() { + ListUtil.getLinkedFiles(entries).forEach(file -> + aiService.getIngestionService().ingest(file, bibDatabaseContext).stateProperty().addListener(obs -> updateNotifications())); - chatVBox - .getChildren() - .addAll(aiChatLogic.getChatHistory() - .getMessages() - .stream() - .map(message -> new ChatMessageComponent(message, this::deleteMessage)) - .toList() - ); + updateNotifications(); + } + private void initializeNotice() { String newNotice = noticeText .getText() .replaceAll("%0", aiPreferences.getAiProvider().getLabel() + " " + aiPreferences.getSelectedChatModel()); noticeText.setText(newNotice); - - Platform.runLater(() -> userPromptTextArea.requestFocus()); } - private Stream getReversedUserMessagesStream() { - return aiChatLogic - .getChatHistory() - .getMessages() - .reversed() - .stream() - .filter(message -> message instanceof UserMessage) - .map(UserMessage.class::cast); - } + private void initializeChatPrompt() { + notificationsButton.setOnAction(event -> + new PopOver(new NotificationsComponent(notifications)) + .show(notificationsButton) + ); - @FXML - private void onSendMessage() { - String userPrompt = userPromptTextArea.getText().trim(); - - if (!userPrompt.isEmpty()) { - userPromptTextArea.clear(); - - UserMessage userMessage = new UserMessage(userPrompt); - addMessage(userMessage); - setLoading(true); - - BackgroundTask task = - BackgroundTask - .wrap(() -> aiChatLogic.execute(userMessage)) - .showToUser(true) - .onSuccess(aiMessage -> { - setLoading(false); - addMessage(aiMessage); - requestUserPromptTextFieldFocus(); - }) - .onFailure(e -> { - LOGGER.error("Got an error while sending a message to AI", e); - setLoading(false); - - if ("401 - null".equals(e.getMessage()) || "404 - null".equals(e.getMessage())) { - addError(Localization.lang("API base URL setting appears to be incorrect. Please check it in AI expert settings.")); - } else { - addError(e.getMessage()); - } - - switchToErrorState(userPrompt); - }); - - task.titleProperty().set(Localization.lang("Waiting for AI reply for %0...", citationKey)); - - task.executeWith(taskExecutor); - } - } + chatPrompt.setSendCallback(this::onSendMessage); - private void switchToErrorState(String userMessage) { - promptHBox.getChildren().clear(); + chatPrompt.setCancelCallback(() -> chatPrompt.switchToNormalState()); - Button retryButton = new Button(Localization.lang("Retry")); + chatPrompt.setRetryCallback(userMessage -> { + deleteLastMessage(); + deleteLastMessage(); + chatPrompt.switchToNormalState(); + onSendMessage(userMessage); + }); - retryButton.setOnAction(event -> { - userPromptTextArea.setText(userMessage); + chatPrompt.requestPromptFocus(); - removeLastMessage(); - removeLastMessage(); + updatePromptHistory(); + } - switchToNormalState(); + private void updateNotifications() { + notifications.clear(); + notifications.addAll(entries.stream().map(this::updateNotificationsForEntry).flatMap(List::stream).toList()); - onSendMessage(); - }); + notificationsButton.setVisible(!notifications.isEmpty()); + notificationsButton.setManaged(!notifications.isEmpty()); - Button cancelButton = new Button(Localization.lang("Cancel")); + if (!notifications.isEmpty()) { + notificationsButton.setGraphic(IconTheme.JabRefIcons.WARNING.withColor(Color.YELLOW).getGraphicNode()); + } + } + + private List updateNotificationsForEntry(BibEntry entry) { + List notifications = new ArrayList<>(); + + if (entries.size() == 1) { + if (entry.getCitationKey().isEmpty()) { + notifications.add(new Notification( + Localization.lang("No citation key for %0", entry.getAuthorTitleYear()), + Localization.lang("The chat history will not be stored in next sessions") + )); + } else if (!CitationKeyCheck.citationKeyIsPresentAndUnique(bibDatabaseContext, entry)) { + notifications.add(new Notification( + Localization.lang("Invalid citation key for %0 (%1)", entry.getCitationKey().get(), entry.getAuthorTitleYear()), + Localization.lang("The chat history will not be stored in next sessions") + )); + } + } - cancelButton.setOnAction(event -> { - switchToNormalState(); + entry.getFiles().forEach(file -> { + if (!FileUtil.isPDFFile(Path.of(file.getLink()))) { + notifications.add(new Notification( + Localization.lang("File %0 is not a PDF file", file.getLink()), + Localization.lang("Only PDF files can be used for chatting") + )); + } }); - promptHBox.getChildren().add(retryButton); - promptHBox.getChildren().add(cancelButton); - } + entry.getFiles().stream().map(file -> aiService.getIngestionService().ingest(file, bibDatabaseContext)).forEach(ingestionStatus -> { + switch (ingestionStatus.getState()) { + case PROCESSING -> notifications.add(new Notification( + Localization.lang("File %0 is currently being processed", ingestionStatus.getObject().getLink()), + Localization.lang("After the file will be ingested, you will be able to chat with it.") + )); - private void removeLastMessage() { - if (!chatVBox.getChildren().isEmpty()) { - ChatMessageComponent chatMessageComponent = (ChatMessageComponent) chatVBox.getChildren().getLast(); - deleteMessage(chatMessageComponent); - } - } + case ERROR -> { + assert ingestionStatus.getException().isPresent(); // When the state is ERROR, the exception must be present. - private void switchToNormalState() { - promptHBox.getChildren().clear(); - promptHBox.getChildren().add(userPromptTextArea); - promptHBox.getChildren().add(submitButton); + notifications.add(new Notification( + Localization.lang("File %0 could not be ingested", ingestionStatus.getObject().getLink()), + ingestionStatus.getException().get().getLocalizedMessage() + )); + } - Platform.runLater(() -> userPromptTextArea.requestFocus()); - } + case SUCCESS -> { } + } + }); - private void setLoading(boolean loading) { - userPromptTextArea.setDisable(loading); - submitButton.setDisable(loading); - - if (loading) { - stackPane.getChildren().add(new BorderPane(new ProgressIndicator())); - } else { - stackPane.getChildren().clear(); - stackPane.getChildren().add(scrollPane); - } + return notifications; } - private void addMessage(ChatMessage chatMessage) { - ChatMessageComponent component = new ChatMessageComponent(chatMessage, this::deleteMessage); - // chatMessage will be added to chat history in {@link AiChatLogic}. - chatVBox.getChildren().add(component); + private void onSendMessage(String userPrompt) { + UserMessage userMessage = new UserMessage(userPrompt); + updatePromptHistory(); + setLoading(true); + + BackgroundTask task = + BackgroundTask + .wrap(() -> aiChatLogic.execute(userMessage)) + .showToUser(true) + .onSuccess(aiMessage -> { + setLoading(false); + chatPrompt.requestPromptFocus(); + }) + .onFailure(e -> { + LOGGER.error("Got an error while sending a message to AI", e); + setLoading(false); + + // Typically, if user has entered an invalid API base URL, we get either "401 - null" or "404 - null" strings. + // Since there might be other strings returned from other API endpoints, we use startsWith() here. + if (e.getMessage().startsWith("404") || e.getMessage().startsWith("401")) { + addError(Localization.lang("API base URL setting appears to be incorrect. Please check it in AI expert settings.")); + } else { + addError(e.getMessage()); + } + + chatPrompt.switchToErrorState(userPrompt); + }); + + task.titleProperty().set(Localization.lang("Waiting for AI reply...")); + + task.executeWith(taskExecutor); } - private void addError(String message) { - ErrorMessage errorMessage = new ErrorMessage(message); - - addMessage(errorMessage); - aiChatLogic.getChatHistory().add(errorMessage); + private void addError(String error) { + ErrorMessage chatMessage = new ErrorMessage(error); + aiChatLogic.getChatHistory().add(chatMessage); } - private void deleteMessage(ChatMessageComponent chatMessageComponent) { - blockScroll.set(2); - - int index = chatVBox.getChildren().indexOf(chatMessageComponent); - chatVBox.getChildren().remove(chatMessageComponent); + private void updatePromptHistory() { + chatPrompt.getHistory().clear(); + chatPrompt.getHistory().addAll(getReversedUserMessagesStream().map(UserMessage::singleText).toList()); + } - aiChatLogic.getChatHistory().remove(index); + private Stream getReversedUserMessagesStream() { + return aiChatLogic + .getChatHistory() + .reversed() + .stream() + .filter(message -> message instanceof UserMessage) + .map(UserMessage.class::cast); } - private void requestUserPromptTextFieldFocus() { - userPromptTextArea.requestFocus(); + private void setLoading(boolean loading) { + uiLoadableChatHistory.setLoading(loading); + chatPrompt.setDisableToButtons(loading); } @FXML private void onClearChatHistory() { - boolean agreed = dialogService.showConfirmationDialogAndWait(Localization.lang("Clear chat history"), Localization.lang("Are you sure you want to clear the chat history of this entry?")); + boolean agreed = dialogService.showConfirmationDialogAndWait( + Localization.lang("Clear chat history"), + Localization.lang("Are you sure you want to clear the chat history of this entry?") + ); if (agreed) { - chatVBox.getChildren().clear(); aiChatLogic.getChatHistory().clear(); } } + + private void deleteLastMessage() { + if (!aiChatLogic.getChatHistory().isEmpty()) { + int index = aiChatLogic.getChatHistory().size() - 1; + aiChatLogic.getChatHistory().remove(index); + } + } } diff --git a/src/main/java/org/jabref/gui/ai/components/aichat/AiChatGuardedComponent.java b/src/main/java/org/jabref/gui/ai/components/aichat/AiChatGuardedComponent.java new file mode 100644 index 00000000000..e0c822774b9 --- /dev/null +++ b/src/main/java/org/jabref/gui/ai/components/aichat/AiChatGuardedComponent.java @@ -0,0 +1,74 @@ +package org.jabref.gui.ai.components.aichat; + +import javafx.beans.property.StringProperty; +import javafx.collections.ObservableList; +import javafx.scene.Node; + +import org.jabref.gui.DialogService; +import org.jabref.gui.ai.components.util.EmbeddingModelGuardedComponent; +import org.jabref.gui.util.TaskExecutor; +import org.jabref.logic.ai.AiService; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.entry.BibEntry; +import org.jabref.preferences.FilePreferences; +import org.jabref.preferences.ai.AiPreferences; + +import dev.langchain4j.data.message.ChatMessage; + +/** + * Main class for AI chatting. It checks if the AI features are enabled and if the embedding model is properly set up. + */ +public class AiChatGuardedComponent extends EmbeddingModelGuardedComponent { + /// This field is used for two purposes: + /// 1. Logging + /// 2. Title of group chat window + /// Thus, if you use {@link AiChatGuardedComponent} for one entry in {@link EntryEditor}, then you may not localize + /// this parameter. However, for group chat window, you should. + private final StringProperty name; + + private final ObservableList chatHistory; + private final BibDatabaseContext bibDatabaseContext; + private final ObservableList entries; + private final AiService aiService; + private final DialogService dialogService; + private final AiPreferences aiPreferences; + private final TaskExecutor taskExecutor; + + public AiChatGuardedComponent(StringProperty name, + ObservableList chatHistory, + BibDatabaseContext bibDatabaseContext, + ObservableList entries, + AiService aiService, + DialogService dialogService, + AiPreferences aiPreferences, + FilePreferences filePreferences, + TaskExecutor taskExecutor + ) { + super(aiService, aiPreferences, filePreferences, dialogService); + + this.name = name; + this.chatHistory = chatHistory; + this.bibDatabaseContext = bibDatabaseContext; + this.entries = entries; + this.aiService = aiService; + this.dialogService = dialogService; + this.aiPreferences = aiPreferences; + this.taskExecutor = taskExecutor; + + rebuildUi(); + } + + @Override + protected Node showEmbeddingModelGuardedContent() { + return new AiChatComponent( + aiService, + name, + chatHistory, + entries, + bibDatabaseContext, + aiPreferences, + dialogService, + taskExecutor + ); + } +} diff --git a/src/main/java/org/jabref/gui/ai/components/aichat/AiChatWindow.java b/src/main/java/org/jabref/gui/ai/components/aichat/AiChatWindow.java new file mode 100644 index 00000000000..8359eec5f05 --- /dev/null +++ b/src/main/java/org/jabref/gui/ai/components/aichat/AiChatWindow.java @@ -0,0 +1,67 @@ +package org.jabref.gui.ai.components.aichat; + +import javafx.beans.property.StringProperty; +import javafx.collections.ObservableList; +import javafx.scene.Scene; + +import org.jabref.gui.DialogService; +import org.jabref.gui.util.BaseWindow; +import org.jabref.gui.util.TaskExecutor; +import org.jabref.logic.ai.AiService; +import org.jabref.logic.l10n.Localization; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.entry.BibEntry; +import org.jabref.preferences.FilePreferences; +import org.jabref.preferences.ai.AiPreferences; + +import dev.langchain4j.data.message.ChatMessage; + +public class AiChatWindow extends BaseWindow { + private final AiService aiService; + private final DialogService dialogService; + private final AiPreferences aiPreferences; + private final FilePreferences filePreferences; + private final TaskExecutor taskExecutor; + + // This field is used for finding an existing AI chat window when user wants to chat with the same group again. + private String chatName; + + public AiChatWindow(AiService aiService, + DialogService dialogService, + AiPreferences aiPreferences, + FilePreferences filePreferences, + TaskExecutor taskExecutor + ) { + this.aiService = aiService; + this.dialogService = dialogService; + this.aiPreferences = aiPreferences; + this.filePreferences = filePreferences; + this.taskExecutor = taskExecutor; + } + + public void setChat(StringProperty name, ObservableList chatHistory, BibDatabaseContext bibDatabaseContext, ObservableList entries) { + setTitle(Localization.lang("AI chat with %0", name.getValue())); + chatName = name.getValue(); + setScene( + new Scene( + new AiChatGuardedComponent( + name, + chatHistory, + bibDatabaseContext, + entries, + aiService, + dialogService, + aiPreferences, + filePreferences, + taskExecutor + ), + 800, + 600 + ) + ); + } + + public String getChatName() { + return chatName; + } +} diff --git a/src/main/java/org/jabref/gui/ai/components/aichat/chathistory/ChatHistoryComponent.fxml b/src/main/java/org/jabref/gui/ai/components/aichat/chathistory/ChatHistoryComponent.fxml new file mode 100644 index 00000000000..b204fc467de --- /dev/null +++ b/src/main/java/org/jabref/gui/ai/components/aichat/chathistory/ChatHistoryComponent.fxml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + diff --git a/src/main/java/org/jabref/gui/ai/components/aichat/chathistory/ChatHistoryComponent.java b/src/main/java/org/jabref/gui/ai/components/aichat/chathistory/ChatHistoryComponent.java new file mode 100644 index 00000000000..c99e125b8e7 --- /dev/null +++ b/src/main/java/org/jabref/gui/ai/components/aichat/chathistory/ChatHistoryComponent.java @@ -0,0 +1,53 @@ +package org.jabref.gui.ai.components.aichat.chathistory; + +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.ScrollPane; +import javafx.scene.layout.VBox; + +import org.jabref.gui.ai.components.aichat.chatmessage.ChatMessageComponent; +import org.jabref.gui.util.UiTaskExecutor; + +import com.airhacks.afterburner.views.ViewLoader; +import dev.langchain4j.data.message.ChatMessage; + +public class ChatHistoryComponent extends ScrollPane { + @FXML private VBox vBox; + + public ChatHistoryComponent() { + ViewLoader.view(this) + .root(this) + .load(); + + this.needsLayoutProperty().addListener((obs, oldValue, newValue) -> { + if (newValue) { + scrollDown(); + } + }); + } + + /** + * @implNote You must call this method only once. + */ + public void setItems(ObservableList items) { + fill(items); + items.addListener((ListChangeListener) obs -> fill(items)); + } + + private void fill(ObservableList items) { + UiTaskExecutor.runInJavaFXThread(() -> { + vBox.getChildren().clear(); + items.forEach(chatMessage -> + vBox.getChildren().add(new ChatMessageComponent(chatMessage, chatMessageComponent -> { + int index = vBox.getChildren().indexOf(chatMessageComponent); + items.remove(index); + }))); + }); + } + + public void scrollDown() { + this.layout(); + this.setVvalue(this.getVmax()); + } +} diff --git a/src/main/java/org/jabref/gui/ai/components/chatmessage/ChatMessageComponent.fxml b/src/main/java/org/jabref/gui/ai/components/aichat/chatmessage/ChatMessageComponent.fxml similarity index 83% rename from src/main/java/org/jabref/gui/ai/components/chatmessage/ChatMessageComponent.fxml rename to src/main/java/org/jabref/gui/ai/components/aichat/chatmessage/ChatMessageComponent.fxml index 9f4eebca2f2..6c0e899a064 100644 --- a/src/main/java/org/jabref/gui/ai/components/chatmessage/ChatMessageComponent.fxml +++ b/src/main/java/org/jabref/gui/ai/components/aichat/chatmessage/ChatMessageComponent.fxml @@ -7,9 +7,9 @@ - + - + @@ -20,11 +20,11 @@ - + - + - - + + diff --git a/src/main/java/org/jabref/gui/ai/components/chatmessage/ChatMessageComponent.java b/src/main/java/org/jabref/gui/ai/components/aichat/chatmessage/ChatMessageComponent.java similarity index 66% rename from src/main/java/org/jabref/gui/ai/components/chatmessage/ChatMessageComponent.java rename to src/main/java/org/jabref/gui/ai/components/aichat/chatmessage/ChatMessageComponent.java index 60c0c4e8c2d..511c3d06628 100644 --- a/src/main/java/org/jabref/gui/ai/components/chatmessage/ChatMessageComponent.java +++ b/src/main/java/org/jabref/gui/ai/components/aichat/chatmessage/ChatMessageComponent.java @@ -1,14 +1,16 @@ -package org.jabref.gui.ai.components.chatmessage; +package org.jabref.gui.ai.components.aichat.chatmessage; import java.util.function.Consumer; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; import javafx.fxml.FXML; import javafx.geometry.NodeOrientation; import javafx.scene.control.Label; import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; -import org.jabref.logic.ai.misc.ErrorMessage; +import org.jabref.logic.ai.util.ErrorMessage; import org.jabref.logic.l10n.Localization; import com.airhacks.afterburner.views.ViewLoader; @@ -22,31 +24,47 @@ public class ChatMessageComponent extends HBox { private static final Logger LOGGER = LoggerFactory.getLogger(ChatMessageComponent.class); - private final ChatMessage chatMessage; - private final Consumer onDeleteCallback; + private final ObjectProperty chatMessage = new SimpleObjectProperty<>(); + private final ObjectProperty> onDelete = new SimpleObjectProperty<>(); - @FXML private VBox wrapperVBox; + @FXML private HBox wrapperHBox; @FXML private VBox vBox; @FXML private Label sourceLabel; @FXML private ExpandingTextArea contentTextArea; - @FXML private HBox buttonsHBox; - - public ChatMessageComponent(ChatMessage chatMessage, Consumer onDeleteCallback) { - this.chatMessage = chatMessage; - this.onDeleteCallback = onDeleteCallback; + @FXML private VBox buttonsVBox; + public ChatMessageComponent() { ViewLoader.view(this) .root(this) .load(); + + chatMessage.addListener((observable, oldValue, newValue) -> { + if (newValue != null) { + loadChatMessage(); + } + }); + } + + public ChatMessageComponent(ChatMessage chatMessage, Consumer onDeleteCallback) { + this(); + setChatMessage(chatMessage); + setOnDelete(onDeleteCallback); + } + + public void setChatMessage(ChatMessage chatMessage) { + this.chatMessage.set(chatMessage); } public ChatMessage getChatMessage() { - return chatMessage; + return chatMessage.get(); } - @FXML - private void initialize() { - switch (chatMessage) { + public void setOnDelete(Consumer onDeleteCallback) { + this.onDelete.set(onDeleteCallback); + } + + private void loadChatMessage() { + switch (chatMessage.get()) { case UserMessage userMessage -> { setColor("-jr-ai-message-user", "-jr-ai-message-user-border"); setNodeOrientation(NodeOrientation.RIGHT_TO_LEFT); @@ -69,15 +87,20 @@ private void initialize() { } default -> - LOGGER.error("ChatMessageComponent supports only user, AI, or error messages, but other type was passed: {}", chatMessage.type().name()); + LOGGER.error("ChatMessageComponent supports only user, AI, or error messages, but other type was passed: {}", chatMessage.get().type().name()); } + } - buttonsHBox.visibleProperty().bind(wrapperVBox.hoverProperty()); + @FXML + private void initialize() { + buttonsVBox.visibleProperty().bind(wrapperHBox.hoverProperty()); } @FXML private void onDeleteClick() { - onDeleteCallback.accept(this); + if (onDelete.get() != null) { + onDelete.get().accept(this); + } } private void setColor(String fillColor, String borderColor) { diff --git a/src/main/java/org/jabref/gui/ai/components/aichat/chatprompt/ChatPromptComponent.fxml b/src/main/java/org/jabref/gui/ai/components/aichat/chatprompt/ChatPromptComponent.fxml new file mode 100644 index 00000000000..b933fc2b7bf --- /dev/null +++ b/src/main/java/org/jabref/gui/ai/components/aichat/chatprompt/ChatPromptComponent.fxml @@ -0,0 +1,25 @@ + + + + + + + + + + + + +