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 12 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 @@ -26,6 +26,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-paste 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)

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 @@ -130,10 +130,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 @@ -256,10 +257,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 @@ -2,19 +2,24 @@

import java.util.function.Consumer;

import javafx.beans.binding.Bindings;
import javafx.beans.binding.NumberBinding;
import javafx.beans.property.ObjectProperty;
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 @@ -27,18 +32,25 @@ public class ChatMessageComponent extends HBox {
private final ObjectProperty<ChatMessage> chatMessage = new SimpleObjectProperty<>();
private final ObjectProperty<Consumer<ChatMessageComponent>> onDelete = new SimpleObjectProperty<>();

@FXML private HBox wrapperHBox;
@FXML private VBox vBox;
@FXML private Label sourceLabel;
@FXML private ExpandingTextArea contentTextArea;
@FXML private VBox buttonsVBox;
@FXML
private HBox wrapperHBox;
@FXML
private VBox vBox;
@FXML
private Label sourceLabel;
@FXML
private Pane markdownContentPane;
@FXML
private VBox buttonsVBox;

private MarkdownTextFlow markdownTextFlow;

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

chatMessage.addListener((observable, oldValue, newValue) -> {
chatMessage.addListener((_, _, newValue) -> {
if (newValue != null) {
loadChatMessage();
}
Expand Down Expand Up @@ -68,32 +80,46 @@ 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());
markdownTextFlow = new MarkdownTextFlow(markdownContentPane);
markdownContentPane.getChildren().add(markdownTextFlow);
markdownContentPane.minHeightProperty().bind(markdownTextFlow.heightProperty());
markdownContentPane.prefHeightProperty().bind(markdownTextFlow.heightProperty());
NumberBinding maxUsableWidth = Bindings.createDoubleBinding(
() -> getScene() == null ? 600 : getScene().getWidth() - 20, sceneProperty(),
getScene() == null ? widthProperty() : getScene().widthProperty()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to add a comment in which cases Scene is null?

Or is it "magically" null and we don't know - then maybe also comment that it is unclar, but we need to check

Reason null is an indicaton that there is some bug in the code. In this case, it seems to be life-cycle related.

--> Could help to create an MWE and mayb file a JavaFX issue - its not always on our side that something is wrong - see e.g., JabRef#713

Copy link
Member

@InAnYan InAnYan Jun 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I proposed my fix, I used this answer of StackOverflow (warning: non-English):

link

I think the author gives a good, if you let me say this, "idiom" of handling this

);
markdownTextFlow.maxWidthProperty().bind(maxUsableWidth);
wrapperHBox.maxWidthProperty().bind(maxUsableWidth);
setMaxWidth(Double.MAX_VALUE);
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
Loading