From bc3bd037bbda2735b37812434829cc31a122b398 Mon Sep 17 00:00:00 2001 From: Martin Lopez Date: Wed, 29 Oct 2025 15:57:40 -0300 Subject: [PATCH 1/6] build!: update markdowneditor to 2.0.1 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index c9c37a2..3b927e1 100644 --- a/pom.xml +++ b/pom.xml @@ -18,7 +18,7 @@ ${project.basedir}/drivers 11.0.12 3.10.0 - 1.1.0 + 2.0.1 true From c759f0f701b27c832906815cebbe0d2a7f5dedb0 Mon Sep 17 00:00:00 2001 From: Martin Lopez Date: Wed, 29 Oct 2025 16:25:29 -0300 Subject: [PATCH 2/6] build: update addon version to next major --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 3b927e1..e7f02e3 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.vaadin.addons.flowingcode chat-assistant-addon - 3.0.1-SNAPSHOT + 4.0.0-SNAPSHOT Chat Assistant Add-on Chat Assistant Add-on for Vaadin Flow From 97e5a6118f12b767ad74f7a5d86102a5542f3abf Mon Sep 17 00:00:00 2001 From: Martin Lopez Date: Tue, 28 Oct 2025 18:04:29 -0300 Subject: [PATCH 3/6] refactor: rename method to better reflect the internal logic --- .../flowingcode/vaadin/addons/chatassistant/ChatMessage.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatMessage.java b/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatMessage.java index 2748131..c4eda51 100644 --- a/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatMessage.java +++ b/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatMessage.java @@ -74,7 +74,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) { @@ -87,7 +87,7 @@ public void setMessage(T message) { } } - private void updateLoadingState(T message) { + private void updateMessage(T message) { if (message.isLoading()) { loader = new Div(new Div(),new Div(), new Div(), new Div()); loader.setClassName("lds-ellipsis"); From c2ba082d99a4a2049351adcd79b6eb5270aef7af Mon Sep 17 00:00:00 2001 From: Martin Lopez Date: Tue, 28 Oct 2025 18:05:34 -0300 Subject: [PATCH 4/6] refactor: remove unused internal variable --- .../flowingcode/vaadin/addons/chatassistant/ChatAssistant.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistant.java b/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistant.java index 10315ee..d44e773 100644 --- a/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistant.java +++ b/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistant.java @@ -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; @@ -70,7 +69,6 @@ public class ChatAssistant extends ReactAdapterComponent implements ClickNotifier> { private static final String CHAT_HEADER_CLASS_NAME = "chat-header"; - private static final String PADDING_SMALL = "0.5em"; private Component headerComponent; private VerticalLayout container; From d00917427173d8300250cb868134518042ecd459 Mon Sep 17 00:00:00 2001 From: Martin Lopez Date: Tue, 28 Oct 2025 18:33:44 -0300 Subject: [PATCH 5/6] fix: fix update message implementation Fix update message logic avoiding creation and removal of internal elements depending on the message and just focusing on updating the contents of the internal state, and moving the creation of the internal elements to the constructor of the component. Closes #47 --- .../addons/chatassistant/ChatMessage.java | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatMessage.java b/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatMessage.java index c4eda51..f1e4582 100644 --- a/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatMessage.java +++ b/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatMessage.java @@ -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; @@ -46,6 +45,7 @@ public class ChatMessage 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. @@ -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); } @@ -87,21 +95,17 @@ public void setMessage(T message) { } } + /** + * Updates the displayed message content and loading state. + * @param message + */ private void updateMessage(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; - } + 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()); } } From 928ea7afb484eaf33c7051932824cf971cc4dbf0 Mon Sep 17 00:00:00 2001 From: Martin Lopez Date: Mon, 27 Oct 2025 16:58:33 -0300 Subject: [PATCH 6/6] feat(demo): add generative ai simulation demo --- .../chatassistant/ChatAssistantDemoView.java | 1 + .../ChatAssistantGenerativeDemo.java | 143 ++++++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 src/test/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistantGenerativeDemo.java diff --git a/src/test/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistantDemoView.java b/src/test/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistantDemoView.java index 8c31721..c4d151b 100644 --- a/src/test/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistantDemoView.java +++ b/src/test/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistantDemoView.java @@ -35,6 +35,7 @@ public ChatAssistantDemoView() { addDemo(ChatAssistantDemo.class); addDemo(ChatAssistantLazyLoadingDemo.class); addDemo(ChatAssistantMarkdownDemo.class); + addDemo(ChatAssistantGenerativeDemo.class); setSizeFull(); } } diff --git a/src/test/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistantGenerativeDemo.java b/src/test/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistantGenerativeDemo.java new file mode 100644 index 0000000..aa18f3e --- /dev/null +++ b/src/test/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistantGenerativeDemo.java @@ -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 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(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(); + }); + 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 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 + " "; + }); +} + +}