From 50615a8e241137905ba6324942fafc1f811644bb Mon Sep 17 00:00:00 2001 From: Martin Lopez Date: Thu, 24 Jul 2025 18:34:36 -0300 Subject: [PATCH 1/2] feat: add avatar provider support in ChatAssistant Closes #39 --- .../addons/chatassistant/ChatAssistant.java | 16 +++++++++++++++- .../addons/chatassistant/ChatAssistantDemo.java | 4 +++- 2 files changed, 18 insertions(+), 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 1e39224..b2124fb 100644 --- a/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistant.java +++ b/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistant.java @@ -43,6 +43,7 @@ import com.vaadin.flow.data.provider.DataProvider; import com.vaadin.flow.data.renderer.ComponentRenderer; 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; @@ -79,6 +80,8 @@ public class ChatAssistant extends ReactAdapterComponent implements ClickNotifie private Span whoIsTyping; private boolean minimized = false; private Registration defaultSubmitListenerRegistration; + private SerializableSupplier avatarProvider = () -> new Avatar("Chat Assistant"); + private Avatar avatar; /** * Default constructor. Creates a ChatAssistant with no messages. @@ -177,7 +180,8 @@ private void initializeChatWindow() { } private void initializeAvatar() { - Avatar avatar = new Avatar("AI"); + if (avatar!=null) avatar.removeFromParent(); + avatar = avatarProvider.get(); avatar.setSizeFull(); this.getElement().appendChild(avatar.getElement()); this.addAttachListener(ev -> this.getElement().executeJs("return;") @@ -368,4 +372,14 @@ public void setMessagesRenderer(Renderer renderer) { content.setRenderer(renderer); } + /** + * Sets the avatar provider that will be used to create the avatar + * + * @param avatarProvider + */ + public void setAvatarProvider(SerializableSupplier avatarProvider) { + this.avatarProvider = avatarProvider; + this.initializeAvatar(); + } + } diff --git a/src/test/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistantDemo.java b/src/test/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistantDemo.java index 127ecc7..ca3d7bf 100644 --- a/src/test/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistantDemo.java +++ b/src/test/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistantDemo.java @@ -24,6 +24,7 @@ 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; @@ -41,7 +42,8 @@ public class ChatAssistantDemo extends VerticalLayout { public ChatAssistantDemo() { - ChatAssistant chatAssistant = new ChatAssistant(); + 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(); From 7e348785102b09c63bdaf813eb8ded447a8b44e3 Mon Sep 17 00:00:00 2001 From: Martin Lopez Date: Thu, 24 Jul 2025 15:43:11 -0300 Subject: [PATCH 2/2] feat: make ChatAssistant generic to support custom message types Close #36 --- .../addons/chatassistant/ChatAssistant.java | 36 ++++++++++--------- .../addons/chatassistant/ChatMessage.java | 14 ++++---- .../addons/chatassistant/model/Message.java | 3 +- .../chatassistant/ChatAssistantDemo.java | 27 +++++++++----- .../ChatAssistantLazyLoadingDemo.java | 2 +- .../ChatAssistantMarkdownDemo.java | 8 +---- .../chatassistant/CustomChatMessage.java | 20 +++++++++++ .../addons/chatassistant/CustomMessage.java | 18 ++++++++++ 8 files changed, 88 insertions(+), 40 deletions(-) create mode 100644 src/test/java/com/flowingcode/vaadin/addons/chatassistant/CustomChatMessage.java create mode 100644 src/test/java/com/flowingcode/vaadin/addons/chatassistant/CustomMessage.java 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 b2124fb..6bb4b25 100644 --- a/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistant.java +++ b/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistant.java @@ -21,7 +21,6 @@ package com.flowingcode.vaadin.addons.chatassistant; import com.flowingcode.vaadin.addons.chatassistant.model.Message; -import com.vaadin.flow.component.AttachEvent; import com.vaadin.flow.component.ClickNotifier; import com.vaadin.flow.component.Component; import com.vaadin.flow.component.ComponentEventListener; @@ -65,7 +64,7 @@ @JsModule("./react/animated-fab.tsx") @Tag("animated-fab") @CssImport("./styles/chat-assistant-styles.css") -public class ChatAssistant extends ReactAdapterComponent implements ClickNotifier { +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"; @@ -73,9 +72,9 @@ public class ChatAssistant extends ReactAdapterComponent implements ClickNotifie private Component headerComponent; private VerticalLayout container; private Component footerContainer; - private VirtualList content = new VirtualList<>(); + private VirtualList content = new VirtualList<>(); private Popover chatWindow; - private List messages; + private List messages; private MessageInput messageInput; private Span whoIsTyping; private boolean minimized = false; @@ -105,7 +104,7 @@ public ChatAssistant(boolean markdownEnabled) { * @param messages the list of messages * @param markdownEnabled flag to enable or disable markdown support */ - public ChatAssistant(List messages, boolean markdownEnabled) { + public ChatAssistant(List messages, boolean markdownEnabled) { this.messages = messages; initializeHeader(); initializeFooter(); @@ -124,13 +123,15 @@ private void initializeHeader() { headerComponent = header; } + @SuppressWarnings("unchecked") private void initializeFooter() { messageInput = new MessageInput(); messageInput.setSizeFull(); messageInput.getStyle().set("padding", PADDING_SMALL); - defaultSubmitListenerRegistration = messageInput.addSubmitListener(se -> - sendMessage(Message.builder().messageTime(LocalDateTime.now()) - .name("User").content(se.getValue()).build())); + defaultSubmitListenerRegistration = messageInput.addSubmitListener(se -> { + sendMessage((T) Message.builder().messageTime(LocalDateTime.now()) + .name("User").content(se.getValue()).build()); + }); whoIsTyping = new Span(); whoIsTyping.setClassName("chat-assistant-who-is-typing"); whoIsTyping.setVisible(false); @@ -142,13 +143,14 @@ private void initializeFooter() { footerContainer = footer; } + @SuppressWarnings("unchecked") private void initializeContent(boolean markdownEnabled) { - content.setItems(messages); - content.setRenderer(new ComponentRenderer<>(message -> new ChatMessage(message, markdownEnabled), + content.setRenderer(new ComponentRenderer<>(message -> new ChatMessage(message, markdownEnabled), (component, message) -> { - ((ChatMessage) component).setMessage(message); + ((ChatMessage) component).setMessage(message); return component; })); + content.setItems(messages); content.setMinHeight("400px"); content.setMinWidth("400px"); container = new VerticalLayout(headerComponent, content, footerContainer); @@ -180,7 +182,9 @@ private void initializeChatWindow() { } private void initializeAvatar() { - if (avatar!=null) avatar.removeFromParent(); + if (avatar!=null) { + avatar.removeFromParent(); + } avatar = avatarProvider.get(); avatar.setSizeFull(); this.getElement().appendChild(avatar.getElement()); @@ -194,7 +198,7 @@ private void initializeAvatar() { * * @param dataProvider the data provider to be used */ - public void setDataProvider(DataProvider dataProvider) { + public void setDataProvider(DataProvider dataProvider) { content.setDataProvider(dataProvider); } @@ -247,7 +251,7 @@ private void refreshContent() { * * @param message the message to be sent programmatically */ - public void sendMessage(Message message) { + public void sendMessage(T message) { messages.add(message); content.getDataProvider().refreshAll(); content.scrollToEnd(); @@ -258,7 +262,7 @@ public void sendMessage(Message message) { * * @param message the message to be updated */ - public void updateMessage(Message message) { + public void updateMessage(T message) { this.content.getDataProvider().refreshItem(message); } @@ -367,7 +371,7 @@ public void scrollToEnd() { * * @param renderer the renderer to use for rendering {@link Message} objects, it cannot be null */ - public void setMessagesRenderer(Renderer renderer) { + public void setMessagesRenderer(Renderer renderer) { Objects.requireNonNull(renderer, "Renderer cannot not be null"); content.setRenderer(renderer); } 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 5f04897..2748131 100644 --- a/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatMessage.java +++ b/src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatMessage.java @@ -41,9 +41,9 @@ @Tag("vaadin-message") @CssImport("./styles/chat-message-styles.css") @EqualsAndHashCode(callSuper=false) -public class ChatMessage extends Component implements HasComponents { +public class ChatMessage extends Component implements HasComponents { - private Message message; + private T message; private boolean markdownEnabled; private Div loader; @@ -52,7 +52,7 @@ public class ChatMessage extends Component implements HasComponents { * * @param message message used to populate the ChatMessage instance */ - public ChatMessage(Message message) { + public ChatMessage(T message) { this(message, false); } @@ -62,7 +62,7 @@ public ChatMessage(Message message) { * @param message message used to populate the ChatMessage instance * @param markdownEnabled whether the message supports markdown or not */ - public ChatMessage(Message message, boolean markdownEnabled) { + public ChatMessage(T message, boolean markdownEnabled) { this.markdownEnabled = markdownEnabled; setMessage(message); } @@ -72,7 +72,7 @@ public ChatMessage(Message message, boolean markdownEnabled) { * * @param message message used to populate the ChatMessage instance */ - public void setMessage(Message message) { + public void setMessage(T message) { this.message = message; updateLoadingState(message); if (message.getName()!=null) { @@ -87,7 +87,7 @@ public void setMessage(Message message) { } } - private void updateLoadingState(Message message) { + private void updateLoadingState(T message) { if (message.isLoading()) { loader = new Div(new Div(),new Div(), new Div(), new Div()); loader.setClassName("lds-ellipsis"); @@ -112,7 +112,7 @@ private void updateLoadingState(Message message) { * * @return the message object used to populate this ChatMessage */ - public Message getMessage() { + public T getMessage() { return message; } diff --git a/src/main/java/com/flowingcode/vaadin/addons/chatassistant/model/Message.java b/src/main/java/com/flowingcode/vaadin/addons/chatassistant/model/Message.java index c5803ee..5ffdafc 100644 --- a/src/main/java/com/flowingcode/vaadin/addons/chatassistant/model/Message.java +++ b/src/main/java/com/flowingcode/vaadin/addons/chatassistant/model/Message.java @@ -26,6 +26,7 @@ import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; +import lombok.experimental.SuperBuilder; /** * Class that represents a chat message @@ -35,7 +36,7 @@ @SuppressWarnings("serial") @Getter @Setter -@Builder +@SuperBuilder @EqualsAndHashCode(of = "id") public class Message implements Serializable { diff --git a/src/test/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistantDemo.java b/src/test/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistantDemo.java index ca3d7bf..8aa03d0 100644 --- a/src/test/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistantDemo.java +++ b/src/test/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistantDemo.java @@ -19,7 +19,6 @@ */ package com.flowingcode.vaadin.addons.chatassistant; -import com.flowingcode.vaadin.addons.chatassistant.model.Message; import com.flowingcode.vaadin.addons.demo.DemoSource; import com.flowingcode.vaadin.addons.demo.SourcePosition; import com.google.common.base.Strings; @@ -29,6 +28,7 @@ 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; @@ -42,7 +42,7 @@ public class ChatAssistantDemo extends VerticalLayout { public ChatAssistantDemo() { - ChatAssistant chatAssistant = new ChatAssistant<>(); + ChatAssistant chatAssistant = new ChatAssistant<>(); chatAssistant.setAvatarProvider(()->new Avatar("Chat Assistant","chatbot.png")); TextArea message = new TextArea(); message.setLabel("Enter a message from the assistant"); @@ -53,20 +53,31 @@ public ChatAssistantDemo() { } }); 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 -> { - Message m = Message.builder().content(message.getValue()).messageTime(LocalDateTime.now()) - .name("Assistant").avatar("chatbot.png").build(); + 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 Thinking"); chatWithThinking.addClickListener(ev -> { - Message delayedMessage = Message.builder().loading(true).content(message.getValue()) + CustomMessage delayedMessage = CustomMessage.builder().loading(true).content(message.getValue()) .messageTime(LocalDateTime.now()) - .name("Assistant").avatar("chatbot.png").build(); + .name("Assistant").avatar("chatbot.png").tagline("Generated by assistant").build(); UI currentUI = UI.getCurrent(); chatAssistant.sendMessage(delayedMessage); @@ -83,9 +94,9 @@ public void run() { message.clear(); }); - chatAssistant.sendMessage(Message.builder().content("Hello, I am here to assist you") + chatAssistant.sendMessage(CustomMessage.builder().content("Hello, I am here to assist you") .messageTime(LocalDateTime.now()) - .name("Assistant").avatar("chatbot.png").build()); + .name("Assistant").avatar("chatbot.png").tagline("Generated by assistant").build()); add(message, chat, chatWithThinking, chatAssistant); } diff --git a/src/test/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistantLazyLoadingDemo.java b/src/test/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistantLazyLoadingDemo.java index ea8fdad..30fe247 100644 --- a/src/test/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistantLazyLoadingDemo.java +++ b/src/test/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistantLazyLoadingDemo.java @@ -123,7 +123,7 @@ public class ChatAssistantLazyLoadingDemo extends VerticalLayout { )); public ChatAssistantLazyLoadingDemo() { - ChatAssistant chatAssistant = new ChatAssistant(); + ChatAssistant chatAssistant = new ChatAssistant<>(); chatAssistant.setClassName("small"); Span lazyLoadingData = new Span(); DataProvider dataProvider = DataProvider.fromCallbacks(query->{ diff --git a/src/test/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistantMarkdownDemo.java b/src/test/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistantMarkdownDemo.java index 15854f3..59e9c06 100644 --- a/src/test/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistantMarkdownDemo.java +++ b/src/test/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistantMarkdownDemo.java @@ -23,19 +23,13 @@ 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.button.Button; import com.vaadin.flow.component.dependency.CssImport; -import com.vaadin.flow.component.html.Span; -import com.vaadin.flow.component.icon.Icon; -import com.vaadin.flow.component.icon.VaadinIcon; -import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.component.textfield.TextArea; import com.vaadin.flow.router.PageTitle; import com.vaadin.flow.router.Route; import java.time.LocalDateTime; -import java.util.Timer; @DemoSource(sourcePosition = SourcePosition.PRIMARY) @PageTitle("Markdown Demo") @@ -45,7 +39,7 @@ public class ChatAssistantMarkdownDemo extends VerticalLayout { public ChatAssistantMarkdownDemo() { - ChatAssistant chatAssistant = new ChatAssistant(true); + ChatAssistant chatAssistant = new ChatAssistant<>(true); TextArea message = new TextArea(); message.setLabel("Enter a message from the assistant (try using Markdown)"); message.setSizeFull(); diff --git a/src/test/java/com/flowingcode/vaadin/addons/chatassistant/CustomChatMessage.java b/src/test/java/com/flowingcode/vaadin/addons/chatassistant/CustomChatMessage.java new file mode 100644 index 0000000..47e2627 --- /dev/null +++ b/src/test/java/com/flowingcode/vaadin/addons/chatassistant/CustomChatMessage.java @@ -0,0 +1,20 @@ +package com.flowingcode.vaadin.addons.chatassistant; + +import com.vaadin.flow.component.html.Span; + +@SuppressWarnings("serial") +public class CustomChatMessage extends ChatMessage { + + private Span tagline = new Span(); + + public CustomChatMessage(CustomMessage message) { + super(message); + tagline.setText(message.getTagline()); + tagline.getStyle().set("display", "block"); + tagline.getStyle().set("font-size", "x-small"); + this.add(tagline); + } + + + +} diff --git a/src/test/java/com/flowingcode/vaadin/addons/chatassistant/CustomMessage.java b/src/test/java/com/flowingcode/vaadin/addons/chatassistant/CustomMessage.java new file mode 100644 index 0000000..d121cce --- /dev/null +++ b/src/test/java/com/flowingcode/vaadin/addons/chatassistant/CustomMessage.java @@ -0,0 +1,18 @@ +package com.flowingcode.vaadin.addons.chatassistant; + +import com.flowingcode.vaadin.addons.chatassistant.model.Message; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.SuperBuilder; + +@SuppressWarnings("serial") +@EqualsAndHashCode(callSuper = true) +@Getter +@Setter +@SuperBuilder +public class CustomMessage extends Message { + + private String tagline; + +}