Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 2
New major version with several enhancements#16
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
Uh oh!
There was an error while loading. Please reload this page.
Merged
Changes from all commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
0f2be8d
build: update version to 2.0.0-SNAPSHOT
mlopezFC 3cfe03f
build: update to Vaadin 24.4.11
mlopezFC eccdad8
feat: add support for replacing header and footer
mlopezFC d17eb22
chore: organize imports
mlopezFC df6551e
feat!: use vaadin-based message implementation with VirtualList
mlopezFC 49b7222
feat(demo): modify demo for showcasing vaadin components support
mlopezFC 05a70f4
fix: fix visual issue when changing source code position
mlopezFC 5757e07
feat: add support for who is typing
mlopezFC f3a7983
docs: add missing javadoc
mlopezFC 9e532f9
fix: fix visual issues when reattaching component
mlopezFC ff00e58
feat: allow setting a custom data provider to the virtual list
mlopezFC 91108b0
feat: add support for changing the submit listener
mlopezFC b65afbc
feat: add support for controlling scrolling of inner virtual list
mlopezFC 0f8b6d9
feat(demo): add new demo for showcasing backend messages lazy loading
mlopezFC 6ee6567
feat: simplify model by removing unnecessary Sender class
mlopezFC 4311aa0
feat: make Message serializable
mlopezFC e288228
perf: use setTimeout() instead of return to avoid server roundtrip
mlopezFC 5f1e364
remove!: remove sendMessage string receiving variant
mlopezFC 591f5e5
feat: allow to set and obtain minimized state
mlopezFC 4a42510
feat(demo): add big initial set of messages
mlopezFC fb5debe
feat(demo): reduce font size of the chat assistant
mlopezFC 7a1fcc9
docs: update license headers
mlopezFC 393848e
refactor: simplify method to improve readability
mlopezFC c4a9cb8
feat: override default web component header implementation
mlopezFC b3e13a1
refactor: remove javascript debugger line
mlopezFC 76d20b1
docs: add missing javadocs
mlopezFC File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Jump to file
Failed to load files.
Loading
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
321 changes: 270 additions & 51 deletions
321 src/main/java/com/flowingcode/vaadin/addons/chatassistant/ChatAssistant.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -2,14 +2,14 @@ | ||
| * #%L | ||
| * Chat Assistant Add-on | ||
| * %% | ||
| * Copyright (C) 2023 Flowing Code | ||
| * Copyright (C) 2023 - 2024 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. | ||
| @@ -20,16 +20,30 @@ | ||
| package com.flowingcode.vaadin.addons.chatassistant; | ||
| import com.vaadin.flow.component.ComponentEvent; | ||
| import com.flowingcode.vaadin.addons.chatassistant.model.Message; | ||
| import com.vaadin.flow.component.AttachEvent; | ||
| import com.vaadin.flow.component.Component; | ||
| import com.vaadin.flow.component.ComponentEventListener; | ||
| import com.vaadin.flow.component.DomEvent; | ||
| import com.vaadin.flow.component.EventData; | ||
| import com.vaadin.flow.component.Tag; | ||
| import com.vaadin.flow.component.dependency.CssImport; | ||
| import com.vaadin.flow.component.dependency.JsModule; | ||
| import com.vaadin.flow.component.dependency.NpmPackage; | ||
| import com.vaadin.flow.component.html.Div; | ||
| 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.messages.MessageInput; | ||
| import com.vaadin.flow.component.messages.MessageInput.SubmitEvent; | ||
| import com.vaadin.flow.component.orderedlayout.HorizontalLayout; | ||
| import com.vaadin.flow.component.orderedlayout.VerticalLayout; | ||
| import com.vaadin.flow.component.virtuallist.VirtualList; | ||
| import com.vaadin.flow.data.provider.DataProvider; | ||
| import com.vaadin.flow.data.renderer.ComponentRenderer; | ||
| import com.vaadin.flow.dom.DomEvent; | ||
| import com.vaadin.flow.shared.Registration; | ||
| import java.time.LocalDateTime; | ||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
| /** | ||
| * Component that allows to create a floating chat button that will open a chat window that can be | ||
| @@ -38,75 +52,280 @@ | ||
| * @author mmlopez | ||
| */ | ||
| @SuppressWarnings("serial") | ||
| @NpmPackage(value = "wc-chatbot", version = "0.1.1") | ||
| @NpmPackage(value = "wc-chatbot", version = "0.2.0") | ||
| @JsModule("wc-chatbot/dist/wc-chatbot.js") | ||
| @CssImport("./styles/chat-assistant-styles.css") | ||
| @Tag("chat-bot") | ||
| public class ChatAssistant extends Div { | ||
| private static final String CHAT_HEADER_CLASS_NAME = "chat-header"; | ||
| private Component headerComponent; | ||
| private Component footerComponent; | ||
| private VirtualList<Message> content = new VirtualList<>(); | ||
| private List<Message> messages; | ||
| private MessageInput messageInput; | ||
| private Span whoIsTyping; | ||
| private boolean minimized = false; | ||
| private Registration defaultSubmitListenerRegistration; | ||
| /** | ||
| * Sends a message represented by the string message programmatically to the component, with | ||
| * default settings. | ||
| * | ||
| * @param message | ||
| * Default constructor. Creates a ChatAssistant with no messages. | ||
| */ | ||
| public void sendMessage(String message) { | ||
| sendMessage(Message.builder().content(message).build()); | ||
| public ChatAssistant() { | ||
| this(new ArrayList<>()); | ||
| } | ||
| /** Shows or hides the chat window */ | ||
| public void toggle() { | ||
| getElement().executeJs("setTimeout(() => {this.toggle();})"); | ||
| /** | ||
| * Creates a ChatAssistant with the given list of messages. | ||
| * | ||
| * @param messages the list of messages | ||
| */ | ||
| public ChatAssistant(List<Message> messages) { | ||
paodb marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| this.messages = messages; | ||
| content.getElement().setAttribute("slot", "content"); | ||
| content.setItems(messages); | ||
| content.setRenderer(new ComponentRenderer<ChatMessage, Message>( | ||
| message -> new ChatMessage(message), (component, message) -> { | ||
| ((ChatMessage) component).setMessage(message); | ||
| return component; | ||
| })); | ||
| this.add(content); | ||
| messageInput = new MessageInput(); | ||
| messageInput.setSizeFull(); | ||
| defaultSubmitListenerRegistration = messageInput | ||
| .addSubmitListener(se -> this.sendMessage(Message.builder().messageTime(LocalDateTime.now()) | ||
| .name("User").content(se.getValue()).build())); | ||
| whoIsTyping = new Span(); | ||
| whoIsTyping.setClassName("chat-assistant-who-is-typing"); | ||
| whoIsTyping.setVisible(false); | ||
| VerticalLayout vl = new VerticalLayout(whoIsTyping, messageInput); | ||
| vl.setSpacing(false); | ||
| vl.setMargin(false); | ||
| vl.setPadding(false); | ||
| this.setFooterComponent(vl); | ||
| this.getElement().addEventListener("bot-button-clicked", this::handleClick).addEventData("event.detail"); | ||
| Icon minimize = VaadinIcon.CHEVRON_DOWN_SMALL.create(); | ||
| minimize.addClickListener(ev -> this.setMinimized(!minimized)); | ||
| Span title = new Span("Chat Assistant"); | ||
| title.setWidthFull(); | ||
| HorizontalLayout headerBar = new HorizontalLayout(title, minimize); | ||
| headerBar.setWidthFull(); | ||
| this.setHeaderComponent(headerBar); | ||
| } | ||
| private void handleClick(DomEvent event) { | ||
| minimized = event.getEventData().getObject("event.detail").getBoolean("minimized"); | ||
| if (!minimized) { | ||
| refreshContent(); | ||
| } | ||
| } | ||
| /** | ||
| * Sends a message to the component, by using the supplied Message object. | ||
| * | ||
| * @param message | ||
| * Sets the data provider of the internal VirtualList. | ||
| * | ||
| * @param dataProvider the data provider to be used | ||
| */ | ||
| public void sendMessage(Message message) { | ||
| getElement() | ||
| .executeJs( | ||
| "setTimeout(() => {this.sendMessage(null, JSON.parse($0));})", message.getJsonObject().toJson()); | ||
| public void setDataProvider(DataProvider<Message, ?> dataProvider) { | ||
| content.setDataProvider(dataProvider); | ||
| } | ||
| /** | ||
| * Adds a listener that will be notified when the ChatSentEvent is fired, allowing to react when a | ||
| * message is sent by the user or programmatically. | ||
| * | ||
| * @param listener | ||
| * @return Registration object to allow removing the listener | ||
| * Uses the provided string as the text shown over the message input to indicate that someone is typing. | ||
| * | ||
| * @param whoIsTyping string to be shown as an indication of someone typing | ||
| */ | ||
| public void setWhoIsTyping(String whoIsTyping) { | ||
| this.whoIsTyping.setText(whoIsTyping); | ||
| this.whoIsTyping.setVisible(true); | ||
| } | ||
| /** | ||
| * Returns the current text shown over the message input to indicate that someone is typing. | ||
| * | ||
| * @return the current text or null if not configured | ||
| */ | ||
| public Registration addChatSentListener(ComponentEventListener<ChatSentEvent> listener) { | ||
| return addListener(ChatSentEvent.class, listener); | ||
| public String getWhoIsTyping() { | ||
| return whoIsTyping.getText(); | ||
| } | ||
| /** | ||
| * Clears the text shown over the message input to indicate that someone is typing. | ||
| */ | ||
| public void clearWhoIsTyping() { | ||
| this.whoIsTyping.setText(null); | ||
| this.whoIsTyping.setVisible(false); | ||
| } | ||
| /** | ||
| * Sets the SubmitListener that will be notified when the user submits a message on the underlying messageInput. | ||
| * | ||
| * @param listener the listener that will be notified when the SubmitEvent is fired | ||
| * @return registration for removal of the listener | ||
| */ | ||
| public Registration setSubmitListener(ComponentEventListener<SubmitEvent> listener) { | ||
| defaultSubmitListenerRegistration.remove(); | ||
| return messageInput.addSubmitListener(listener); | ||
| } | ||
| protected void onAttach(AttachEvent attachEvent) { | ||
| if (!minimized) { | ||
| getElement().executeJs("setTimeout(() => this.toggle())"); | ||
| this.getElement().executeJs("return;").then((ev) -> { | ||
| refreshContent(); | ||
| }); | ||
| } | ||
| this.getElement().executeJs("setTimeout(() => this.shadowRoot.querySelector($0).innerHTML = $1)", | ||
| ".chatbot-body", "<slot name='content'></slot>"); | ||
| this.getElement().executeJs( | ||
| "this.shadowRoot.querySelector($0).style.setProperty('padding', '0px');", | ||
| ".chatbot-body"); | ||
| this.getElement().executeJs(""" | ||
| setTimeout(() => { | ||
| let chatbot = this; | ||
| let chatBotContainer = this.shadowRoot.querySelector($1); | ||
| this.shadowRoot.querySelector($0).addEventListener("click", function() { | ||
| let buttonClickedEvent = new CustomEvent("bot-button-clicked", { | ||
| detail: { | ||
| minimized: chatBotContainer.classList.contains('animation-scale-out'), | ||
| }, | ||
| }); | ||
| chatbot.dispatchEvent(buttonClickedEvent); | ||
| }); | ||
| }) | ||
| """, ".bot-button", ".chatbot-container"); | ||
| if (footerComponent!=null) { | ||
| this.setFooterComponent(footerComponent); | ||
| } | ||
| if (headerComponent!=null) { | ||
| this.setHeaderComponent(headerComponent); | ||
| } | ||
| } | ||
| private void refreshContent() { | ||
| this.content.getDataProvider().refreshAll(); | ||
| this.content.getElement().executeJs("this.requestContentUpdate();"); | ||
| this.content.scrollToEnd(); | ||
| } | ||
| /** | ||
| * Event that represents a chat being sent to the component. | ||
| * Sends a message programmatically to the component. Should not be used when a custom | ||
| * DataProvider is used. Instead just refresh the custom DataProvider. | ||
| * | ||
| * @author mmlopez | ||
| * @param message the message to be sent programmatically | ||
| */ | ||
| @DomEvent("sent") | ||
| public static class ChatSentEvent extends ComponentEvent<ChatAssistant> { | ||
| private final String message; | ||
| private boolean right; | ||
| public ChatSentEvent( | ||
| ChatAssistant source, | ||
| boolean fromClient, | ||
| @EventData("event.detail.message.message") String message, | ||
| @EventData("event.detail.message.right") boolean right) { | ||
| super(source, fromClient); | ||
| this.message = message.replaceAll("^<[^>]+>|<[^>]+>$", ""); | ||
| this.right = right; | ||
| } | ||
| public void sendMessage(Message message) { | ||
| messages.add(message); | ||
| content.getDataProvider().refreshAll(); | ||
| content.scrollToEnd(); | ||
| } | ||
| /** | ||
| * Updates a previously entered message. | ||
| * | ||
| * @param message the message to be updated | ||
| */ | ||
| public void updateMessage(Message message) { | ||
| this.content.getDataProvider().refreshItem(message); | ||
| } | ||
| public String getMessage() { | ||
| return message; | ||
| /** | ||
| * Shows or hides chat window. | ||
| * | ||
| * @param minimized true for hiding the chat window and false for displaying it | ||
| */ | ||
| public void setMinimized(boolean minimized) { | ||
| if (!minimized && this.minimized) { | ||
| getElement().executeJs("setTimeout(() => {this.toggle();})"); | ||
| this.refreshContent(); | ||
| } else if (minimized && !this.minimized) { | ||
| getElement().executeJs("setTimeout(() => {this.toggle();})"); | ||
| } | ||
| public boolean isRight() { | ||
| return right; | ||
| this.minimized = minimized; | ||
| } | ||
| /** | ||
| * Returns the visibility of the chat window. | ||
| * | ||
| * @return true if the chat window is minimized false otherwise | ||
| */ | ||
| public boolean isMinimized() { | ||
| return minimized; | ||
| } | ||
| /** | ||
| * Allows changing the header of the chat window. | ||
| * | ||
| * @param component to be used as a replacement for the header | ||
| */ | ||
| public void setHeaderComponent(Component component) { | ||
| if (headerComponent!=null) { | ||
| this.remove(headerComponent); | ||
| } | ||
| component.addClassName(CHAT_HEADER_CLASS_NAME); | ||
| this.headerComponent = component; | ||
| this.getElement().executeJs("setTimeout(() => this.shadowRoot.querySelector($0).innerHTML = $1)", ".chatbot-header", "<slot name='header'></slot>"); | ||
| component.getElement().setAttribute("slot", "header"); | ||
| this.add(headerComponent); | ||
| } | ||
| /** | ||
| * Returns the current component configured as the header of the chat window. | ||
| * | ||
| * @return component used as the header of the chat window | ||
| */ | ||
| public Component getHeaderComponent() { | ||
| return headerComponent; | ||
| } | ||
| /** | ||
| * Allows changing the footer of the chat window. | ||
| * | ||
| * @param component to be used as a replacement for the footer | ||
| */ | ||
| public void setFooterComponent(Component component) { | ||
| this.footerComponent = component; | ||
| this.getElement().executeJs("setTimeout(() => this.shadowRoot.querySelector($0).innerHTML = $1)", ".chat-footer", "<slot name='footer'></slot>"); | ||
| component.getElement().setAttribute("slot", "footer"); | ||
| this.add(footerComponent); | ||
| } | ||
| /** | ||
| * Returns the current component configured as the footer of the chat window. | ||
| * | ||
| * @return component used as the footer of the chat window | ||
| */ | ||
| public Component getFooterComponent() { | ||
| return footerComponent; | ||
| } | ||
| /** | ||
| * Scrolls to the given row index. Scrolls so that the element is shown at | ||
| * the start of the visible area whenever possible. | ||
| * <p> | ||
| * If the index parameter exceeds current item set size the grid will scroll | ||
| * to the end. | ||
| * | ||
| * @param rowIndex | ||
| * zero based index of the item to scroll to in the current view. | ||
| */ | ||
| public void scrollToIndex(int position) { | ||
| this.content.scrollToIndex(position); | ||
| } | ||
| /** | ||
| * Scrolls to the first element. | ||
| */ | ||
| public void scrollToStart() { | ||
| this.content.scrollToStart(); | ||
| } | ||
| /** | ||
| * Scrolls to the last element of the list. | ||
| */ | ||
| public void scrollToEnd() { | ||
| this.content.scrollToEnd(); | ||
| } | ||
| } | ||
Oops, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.