Skip to content

Add Markdown rendering to AI Chat #13194

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 24 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
e04f0de
feat: markdown in AI messages
Yubo-Cao May 30, 2025
ebcd900
docs: update CHANGELOG
Yubo-Cao May 30, 2025
ae2e2fa
Merge branch 'JabRef:main' into markdown-ai
Yubo-Cao May 30, 2025
2b2fa66
fix: note that Markdown is always valid, it never throw.
Yubo-Cao May 30, 2025
be60b42
Merge branch 'markdown-ai' of https://github.com/Yubo-Cao/jabref into…
Yubo-Cao May 30, 2025
3336504
chore: move Flexmark dependency up
Yubo-Cao May 31, 2025
17becfb
fix: remove null check for scnee
Yubo-Cao May 31, 2025
abd52c0
docs: remove trivial documentation
Yubo-Cao May 31, 2025
99a6c3d
Improve whitespace and MarkdownTextflow's coping experience
Jun 1, 2025
9e4ccfc
Merge remote-tracking branch 'upstream/main' into markdown-ai
Jun 2, 2025
9c31517
Use Deque and StringJoiner per code review.
Jun 2, 2025
4e2c604
Remove == null check and fix getScene()'s NPE
Jun 4, 2025
abdc1d3
Use @NonNull, rather than Objects.requireNonNull
Jun 4, 2025
22ae09c
Get rid of width binding.
Jun 6, 2025
d9eaa0b
Merge branch 'main' into markdown-ai
Yubo-Cao Jun 6, 2025
2017cc4
Merge branch 'main' into markdown-ai
subhramit Jun 28, 2025
51e63d3
Apply Subhramit's suggestions
Jun 28, 2025
8348525
Fix drag event's problem as ThiloteE suggests.
Jun 29, 2025
aed3c58
Add Link support to the Markdown rendering
Jun 29, 2025
8edce1c
Fix according to Trag
Jun 29, 2025
fda54f7
Fix according to Trag
Jun 29, 2025
699d3eb
Fix according to Trag
Jun 29, 2025
e3fa14f
Fix according to Ruslan
Jun 29, 2025
2eca1a3
Fix according to Trag
Jun 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv
- We added an "Open example library" button to Welcome Tab. [#13014](https://github.com/JabRef/jabref/issues/13014)
- We added automatic detection and selection of the identifier type (e.g., DOI, ISBN, arXiv) based on clipboard content when opening the "New Entry" dialog [#13111](https://github.com/JabRef/jabref/pull/13111)
- We added support for import of a Refer/BibIX file format. [#13069](https://github.com/JabRef/jabref/issues/13069)
- We added markdown rendering and copy capabilities to AI chat responses. [#12234](https://github.com/JabRef/jabref/issues/12234)
- We added a new `jabkit` command `pseudonymize` to pseudonymize the library. [#13109](https://github.com/JabRef/jabref/issues/13109)
- We added functionality to focus running instance when trying to start a second instance. [#13129](https://github.com/JabRef/jabref/issues/13129)
- We added a highlighted diff regarding changes to the Group Tree Structure of a bib file, made outside JabRef. [#11221](https://github.com/JabRef/jabref/issues/11221)
Expand Down
6 changes: 3 additions & 3 deletions jabgui/src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -127,10 +127,10 @@
// requires org.apache.xmpbox;
// requires com.ibm.icu;

// requires flexmark;
requires flexmark;
requires flexmark.html2md.converter;
// requires flexmark.util.ast;
// requires flexmark.util.data;
requires flexmark.util.ast;
requires flexmark.util.data;

// requires com.h2database.mvstore;

Expand Down
9 changes: 5 additions & 4 deletions jabgui/src/main/java/org/jabref/gui/JabRefGUI.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import org.jabref.gui.help.VersionWorker;
import org.jabref.gui.icon.IconTheme;
import org.jabref.gui.keyboard.KeyBindingRepository;
import org.jabref.gui.keyboard.SelectableTextFlowKeyBindings;
import org.jabref.gui.keyboard.TextInputKeyBindings;
import org.jabref.gui.openoffice.OOBibBaseConnect;
import org.jabref.gui.preferences.GuiPreferences;
Expand Down Expand Up @@ -267,10 +268,10 @@ private void openWindow() {
themeManager.installCss(scene);

LOGGER.debug("Handle TextEditor key bindings");
scene.addEventFilter(KeyEvent.KEY_PRESSED, event -> TextInputKeyBindings.call(
scene,
event,
preferences.getKeyBindingRepository()));
scene.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
TextInputKeyBindings.call(scene, event, preferences.getKeyBindingRepository());
SelectableTextFlowKeyBindings.call(scene, event, preferences.getKeyBindingRepository());
});

mainStage.setTitle(JabRefFrame.FRAME_TITLE);
mainStage.getIcons().addAll(IconTheme.getLogoSetFX());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,18 @@
import javafx.beans.property.SimpleObjectProperty;
import javafx.fxml.FXML;
import javafx.geometry.NodeOrientation;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;

import org.jabref.gui.util.MarkdownTextFlow;
import org.jabref.logic.ai.util.ErrorMessage;
import org.jabref.logic.l10n.Localization;

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;
Expand All @@ -30,19 +33,26 @@ public class ChatMessageComponent extends HBox {
@FXML private HBox wrapperHBox;
@FXML private VBox vBox;
@FXML private Label sourceLabel;
@FXML private ExpandingTextArea contentTextArea;
@FXML private Pane markdownContentPane;
@FXML private VBox buttonsVBox;

private final MarkdownTextFlow markdownTextFlow;

public ChatMessageComponent() {
ViewLoader.view(this)
.root(this)
.load();

chatMessage.addListener((observable, oldValue, newValue) -> {
chatMessage.addListener((_, _, newValue) -> {
if (newValue != null) {
loadChatMessage();
}
});

markdownTextFlow = new MarkdownTextFlow(markdownContentPane);
markdownContentPane.getChildren().add(markdownTextFlow);
markdownContentPane.minHeightProperty().bind(markdownTextFlow.heightProperty());
markdownContentPane.prefHeightProperty().bind(markdownTextFlow.heightProperty());
}

public ChatMessageComponent(ChatMessage chatMessage, Consumer<ChatMessageComponent> onDeleteCallback) {
Expand All @@ -68,32 +78,35 @@ private void loadChatMessage() {
case UserMessage userMessage -> {
setColor("-jr-ai-message-user", "-jr-ai-message-user-border");
setNodeOrientation(NodeOrientation.RIGHT_TO_LEFT);
wrapperHBox.setAlignment(Pos.TOP_RIGHT);
sourceLabel.setText(Localization.lang("User"));
contentTextArea.setText(userMessage.singleText());
markdownTextFlow.setMarkdown(userMessage.singleText());
}

case AiMessage aiMessage -> {
setColor("-jr-ai-message-ai", "-jr-ai-message-ai-border");
setNodeOrientation(NodeOrientation.LEFT_TO_RIGHT);
wrapperHBox.setAlignment(Pos.TOP_LEFT);
sourceLabel.setText(Localization.lang("AI"));
contentTextArea.setText(aiMessage.text());
markdownTextFlow.setMarkdown(aiMessage.text());
}

case ErrorMessage errorMessage -> {
setColor("-jr-ai-message-error", "-jr-ai-message-error-border");
setNodeOrientation(NodeOrientation.LEFT_TO_RIGHT);
sourceLabel.setText(Localization.lang("Error"));
contentTextArea.setText(errorMessage.getText());
markdownTextFlow.setMarkdown(errorMessage.getText());
}

default ->
LOGGER.error("ChatMessageComponent supports only user, AI, or error messages, but other type was passed: {}", chatMessage.get().type().name());
LOGGER.error("ChatMessageComponent supports only user, AI, or error messages, but other type was passed: {}", chatMessage.get().type().name());
}
}

@FXML
private void initialize() {
buttonsVBox.visibleProperty().bind(wrapperHBox.hoverProperty());
HBox.setHgrow(this, Priority.ALWAYS);
}

@FXML
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package org.jabref.gui.keyboard;

import javafx.scene.Scene;
import javafx.scene.input.KeyEvent;

import org.jabref.gui.util.SelectableTextFlow;

public class SelectableTextFlowKeyBindings {
public static void call(Scene scene, KeyEvent event, KeyBindingRepository keyBindingRepository) {
if (scene.focusOwnerProperty().get() instanceof SelectableTextFlow selectableTextFlow) {
keyBindingRepository.mapToKeyBinding(event).ifPresent(binding -> {
switch (binding) {
case COPY -> {
selectableTextFlow.copySelectedText();
event.consume();
}
case SELECT_ALL -> {
selectableTextFlow.selectAll();
event.consume();
}
}
});
}
}
}
Loading