diff --git a/pom.xml b/pom.xml index c9c37a2..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 @@ -18,7 +18,7 @@ ${project.basedir}/drivers 11.0.12 3.10.0 - 1.1.0 + 2.0.1 true 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; 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..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); } @@ -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) { @@ -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()); } } 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 + " "; + }); +} + +}