Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

<groupId>org.vaadin.addons.flowingcode</groupId>
<artifactId>chat-assistant-addon</artifactId>
<version>3.0.1-SNAPSHOT</version>
<version>4.0.0-SNAPSHOT</version>
<name>Chat Assistant Add-on</name>
<description>Chat Assistant Add-on for Vaadin Flow</description>

Expand All @@ -18,7 +18,7 @@
<drivers.dir>${project.basedir}/drivers</drivers.dir>
<jetty.version>11.0.12</jetty.version>
<flowingcode.commons.demo.version>3.10.0</flowingcode.commons.demo.version>
<markdown-editor.version>1.1.0</markdown-editor.version>
<markdown-editor.version>2.0.1</markdown-editor.version>
<frontend.hotdeploy>true</frontend.hotdeploy>
</properties>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@
import com.vaadin.flow.data.renderer.Renderer;
import com.vaadin.flow.function.SerializableSupplier;
import com.vaadin.flow.shared.Registration;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
Expand All @@ -70,7 +69,6 @@
public class ChatAssistant<T extends Message> extends ReactAdapterComponent implements ClickNotifier<ChatAssistant<T>> {

private static final String CHAT_HEADER_CLASS_NAME = "chat-header";
private static final String PADDING_SMALL = "0.5em";

private Component headerComponent;
private VerticalLayout container;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
package com.flowingcode.vaadin.addons.chatassistant;

import com.flowingcode.vaadin.addons.chatassistant.model.Message;
import com.flowingcode.vaadin.addons.markdown.BaseMarkdownComponent.DataColorMode;
import com.flowingcode.vaadin.addons.markdown.MarkdownViewer;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.HasComponents;
Expand All @@ -46,6 +45,7 @@ public class ChatMessage<T extends Message> extends Component implements HasComp
private T message;
private boolean markdownEnabled;
private Div loader;
private MarkdownViewer markdownViewer;

/**
* Creates a new ChatMessage based on the supplied message without markdown support.
Expand All @@ -64,6 +64,14 @@ public ChatMessage(T message) {
*/
public ChatMessage(T message, boolean markdownEnabled) {
this.markdownEnabled = markdownEnabled;
loader = new Div(new Div(),new Div(), new Div(), new Div());
loader.setClassName("lds-ellipsis");
loader.setVisible(false);
this.add(loader);
if (markdownEnabled) {
markdownViewer = new MarkdownViewer(message.getContent());
this.add(markdownViewer);
}
setMessage(message);
}

Expand All @@ -74,7 +82,7 @@ public ChatMessage(T message, boolean markdownEnabled) {
*/
public void setMessage(T message) {
this.message = message;
updateLoadingState(message);
updateMessage(message);
if (message.getName()!=null) {
this.setUserName(message.getName());
if (message.getAvatar()!=null) {
Expand All @@ -87,21 +95,17 @@ public void setMessage(T message) {
}
}

private void updateLoadingState(T message) {
if (message.isLoading()) {
loader = new Div(new Div(),new Div(), new Div(), new Div());
loader.setClassName("lds-ellipsis");
this.add(loader);
} else {
if (loader!=null) {
this.remove(loader);
loader = null;
}
/**
* Updates the displayed message content and loading state.
* @param message
*/
private void updateMessage(T message) {
loader.setVisible(message.isLoading());
if (!message.isLoading()) {
if (markdownEnabled) {
MarkdownViewer mdv = new MarkdownViewer(message.getContent());
mdv.setDataColorMode(DataColorMode.LIGHT);
this.add(mdv);
markdownViewer.setContent(message.getContent());
} else {
this.getElement().executeJs("[...this.childNodes].forEach(node => node.nodeType === 3 && this.removeChild(node));");
this.getElement().executeJs("this.appendChild(document.createTextNode($0));", message.getContent());
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public ChatAssistantDemoView() {
addDemo(ChatAssistantDemo.class);
addDemo(ChatAssistantLazyLoadingDemo.class);
addDemo(ChatAssistantMarkdownDemo.class);
addDemo(ChatAssistantGenerativeDemo.class);
setSizeFull();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/*-
* #%L
* Chat Assistant Add-on
* %%
* Copyright (C) 2023 - 2025 Flowing Code
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package com.flowingcode.vaadin.addons.chatassistant;

import com.flowingcode.vaadin.addons.demo.DemoSource;
import com.flowingcode.vaadin.addons.demo.SourcePosition;
import com.google.common.base.Strings;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.avatar.Avatar;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.dependency.CssImport;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.TextArea;
import com.vaadin.flow.data.renderer.ComponentRenderer;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;

@DemoSource(sourcePosition = SourcePosition.PRIMARY)
@PageTitle("Generative Answer Demo")
@SuppressWarnings("serial")
@Route(value = "chat-assistant/generative-demo", layout = ChatAssistantDemoView.class)
@CssImport("./styles/chat-assistant-styles-demo.css")
public class ChatAssistantGenerativeDemo extends VerticalLayout {

public ChatAssistantGenerativeDemo() {
String sampleText = "Hi, I'm an advanced language model. I'm here to help you demonstrate"
+ " how a text-streaming chat component works in Vaadin. As you can see, each word appears"
+ " with a slight pause, simulating the time it would take me to \"think\" and generate"
+ " the next word. I hope this is useful for your demonstration!";

ChatAssistant<CustomMessage> chatAssistant = new ChatAssistant<>();
chatAssistant.setAvatarProvider(()->new Avatar("Chat Assistant","chatbot.png"));
TextArea message = new TextArea();
message.setLabel("Enter a message from the assistant");
message.setSizeFull();
message.setValue(sampleText);
message.addKeyPressListener(ev->{
if (Strings.isNullOrEmpty(chatAssistant.getWhoIsTyping())) {
chatAssistant.setWhoIsTyping("Assistant is generating an answer ...");
}
});
message.addBlurListener(ev->chatAssistant.clearWhoIsTyping());
chatAssistant.setMessagesRenderer(new ComponentRenderer<CustomChatMessage,CustomMessage>(m -> {
return new CustomChatMessage(m);
},
(component, m) -> {
((CustomChatMessage) component).setMessage(m);
return component;
}));
chatAssistant.setSubmitListener(se -> {
chatAssistant.sendMessage(CustomMessage.builder().messageTime(LocalDateTime.now())
.name("User").content(se.getValue()).tagline("Generated by user").build());
});

Button chat = new Button("Chat");
chat.addClickListener(ev -> {
CustomMessage m = CustomMessage.builder().content(message.getValue()).messageTime(LocalDateTime.now())
.name("Assistant").avatar("chatbot.png").tagline("Generated by assistant").build();

chatAssistant.sendMessage(m);
message.clear();
});
Button chatWithThinking = new Button("Chat With Generative Thinking");
chatWithThinking.addClickListener(ev -> {
String messageToSend = message.getValue();
CustomMessage delayedMessage = CustomMessage.builder().loading(true).content("")
.messageTime(LocalDateTime.now())
.name("Assistant").avatar("chatbot.png").tagline("Generated by assistant").build();

UI currentUI = UI.getCurrent();
chatAssistant.sendMessage(delayedMessage);

CompletableFuture.runAsync(() -> {
try {
TimeUnit.MILLISECONDS.sleep(500);
currentUI.access(() -> {
delayedMessage.setLoading(false);
});
streamWords(messageToSend)
.forEach(item -> {
currentUI.access(() -> {
delayedMessage.setContent(delayedMessage.getContent() + item);
chatAssistant.updateMessage(delayedMessage);
});
});
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});

message.clear();
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove redundant clear() call.

Line 115 calls message.clear(), but the TextArea was already cleared at line 89. This is redundant.

Apply this diff:

       });

-      message.clear();
     });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
message.clear();
});
});
🤖 Prompt for AI Agents
In
src/test/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistantGenerativeDemo.java
around line 115, there is a redundant call to message.clear() — the TextArea is
already cleared earlier at line 89; remove the second message.clear() at line
115 so the duplicate clearing is eliminated and the test/demo code remains
concise.

});
chatAssistant.sendMessage(CustomMessage.builder().content("Hello, I am here to assist you")
.messageTime(LocalDateTime.now())
.name("Assistant").avatar("chatbot.png").tagline("Generated by assistant").build());

add(message, chat, chatWithThinking, chatAssistant);
}

public Stream<String> streamWords(String fullText) {
if (fullText == null || fullText.isEmpty()) {
return Stream.empty();
}

String[] words = fullText.split("\\s+");

return Arrays.stream(words)
.map(word -> {
try {
long delay = ThreadLocalRandom.current().nextLong(50, 250);
Thread.sleep(delay);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}

return word + " ";
});
}

}