diff --git a/CHANGELOG.md b/CHANGELOG.md index d0e68618dd9..fccec686bf2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We fixed an issue where a redundant validation listener was causing duplicate error dialogs when invalid BibTeX source was detected in the SourceTab. [#14805](https://github.com/JabRef/jabref/issues/14805) - We added support for selecting citation fetcher in Citations Tab. [#14430](https://github.com/JabRef/jabref/issues/14430) - In the "New Entry" dialog the identifier type is now automatically updated on typing. [#14660](https://github.com/JabRef/jabref/issues/14660) +- We added the ability to copy selected text from AI chat interface. [#14655](https://github.com/JabRef/jabref/issues/14655) - We added cover images for books, which will display in entry previews if available, and can be automatically downloaded when adding an entry via ISBN. [#10120](https://github.com/JabRef/jabref/issues/10120) ### Changed diff --git a/jabgui/src/main/java/org/jabref/gui/ai/components/aichat/chatmessage/ChatMessageComponent.java b/jabgui/src/main/java/org/jabref/gui/ai/components/aichat/chatmessage/ChatMessageComponent.java index f620938dea3..bd90c30dbdb 100644 --- a/jabgui/src/main/java/org/jabref/gui/ai/components/aichat/chatmessage/ChatMessageComponent.java +++ b/jabgui/src/main/java/org/jabref/gui/ai/components/aichat/chatmessage/ChatMessageComponent.java @@ -7,13 +7,18 @@ import javafx.fxml.FXML; import javafx.geometry.NodeOrientation; import javafx.geometry.Pos; +import javafx.scene.control.ContextMenu; import javafx.scene.control.Label; +import javafx.scene.control.MenuItem; +import javafx.scene.input.MouseEvent; import javafx.scene.layout.HBox; import javafx.scene.layout.Pane; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; +import org.jabref.gui.clipboard.ClipBoardManager; import org.jabref.gui.util.MarkdownTextFlow; +import org.jabref.logic.ai.util.ChatMessageUtils; import org.jabref.logic.ai.util.ErrorMessage; import org.jabref.logic.l10n.Localization; @@ -21,6 +26,7 @@ import dev.langchain4j.data.message.AiMessage; import dev.langchain4j.data.message.ChatMessage; import dev.langchain4j.data.message.UserMessage; +import jakarta.inject.Inject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -37,6 +43,7 @@ public class ChatMessageComponent extends HBox { @FXML private VBox buttonsVBox; private final MarkdownTextFlow markdownTextFlow; + @Inject private ClipBoardManager clipBoardManager; public ChatMessageComponent() { ViewLoader.view(this) @@ -53,6 +60,7 @@ public ChatMessageComponent() { markdownContentPane.getChildren().add(markdownTextFlow); markdownContentPane.minHeightProperty().bind(markdownTextFlow.heightProperty()); markdownContentPane.prefHeightProperty().bind(markdownTextFlow.heightProperty()); + setupContextMenu(); } public ChatMessageComponent(ChatMessage chatMessage, Consumer onDeleteCallback) { @@ -61,6 +69,36 @@ public ChatMessageComponent(ChatMessage chatMessage, Consumer { + if (event.isSecondaryButtonDown() && markdownTextFlow.isSelectionActive()) { + // Consume the event to prevent JavaFX from clearing the selection highlight + event.consume(); + // Manually trigger the context menu since we consumed the event that usually triggers it + contextMenu.show(markdownContentPane, event.getScreenX(), event.getScreenY()); + } + }); + + copyItem.setOnAction(_ -> { + if (markdownTextFlow.isSelectionActive()) { + markdownTextFlow.copySelectedText(); + } else { + copyFullMessage(); + } + }); + + markdownContentPane.setOnContextMenuRequested(event -> { + if (!markdownTextFlow.isSelectionActive()) { + contextMenu.show(markdownContentPane, event.getScreenX(), event.getScreenY()); + } + }); + } + public void setChatMessage(ChatMessage chatMessage) { this.chatMessage.set(chatMessage); } @@ -119,4 +157,12 @@ private void onDeleteClick() { private void setColor(String fillColor, String borderColor) { vBox.setStyle("-fx-background-color: " + fillColor + "; -fx-border-radius: 10; -fx-background-radius: 10; -fx-border-color: " + borderColor + "; -fx-border-width: 3;"); } + + private void copyFullMessage() { + ChatMessageUtils.getContent(chatMessage.get()).ifPresent(content -> { + if (!content.isEmpty()) { + clipBoardManager.setContent(content); + } + }); + } } diff --git a/jablib/src/main/java/org/jabref/logic/ai/util/ChatMessageUtils.java b/jablib/src/main/java/org/jabref/logic/ai/util/ChatMessageUtils.java new file mode 100644 index 00000000000..cd36d330377 --- /dev/null +++ b/jablib/src/main/java/org/jabref/logic/ai/util/ChatMessageUtils.java @@ -0,0 +1,32 @@ +package org.jabref.logic.ai.util; + +import java.util.Optional; + +import dev.langchain4j.data.message.AiMessage; +import dev.langchain4j.data.message.ChatMessage; +import dev.langchain4j.data.message.UserMessage; + +public class ChatMessageUtils { + + private ChatMessageUtils() { + } + + public static Optional getContent(ChatMessage chatMessage) { + if (chatMessage == null) { + return Optional.empty(); + } + + String content = switch (chatMessage) { + case UserMessage user -> + user.singleText(); + case AiMessage ai -> + ai.text(); + case ErrorMessage err -> + err.getText(); + default -> + null; + }; + + return Optional.ofNullable(content); + } +}