├── .gitignore ├── .mvn └── wrapper │ └── maven-wrapper.properties ├── LICENSE ├── README.md ├── mvnw ├── mvnw.cmd ├── pom.xml └── src ├── main ├── frontend │ ├── index.html │ └── themes │ │ └── spring-ai-vaadin │ │ ├── chat-header.css │ │ ├── chat.css │ │ ├── main-view.css │ │ ├── settings-panel.css │ │ └── styles.css ├── java │ └── org │ │ └── spring │ │ └── framework │ │ └── ai │ │ └── vaadin │ │ ├── AiConfig.java │ │ ├── SpringAiVaadinApplication.java │ │ ├── service │ │ ├── Assistant.java │ │ ├── AttachmentFile.java │ │ ├── RagContextService.java │ │ └── package-info.java │ │ └── ui │ │ ├── component │ │ ├── Chat.java │ │ ├── ChatHeader.java │ │ ├── ChatMessage.java │ │ └── SettingsPanel.java │ │ ├── util │ │ ├── CustomMultipartFile.java │ │ └── ImageUtils.java │ │ └── view │ │ └── MainView.java └── resources │ ├── application.properties │ ├── mcp-servers-config.json │ └── vaadin-featureflags.properties └── test └── java └── org └── spring └── framework └── ai └── vaadin └── SpringAiVaadinApplicationTests.java /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | replay_pid* 25 | 26 | node_modules 27 | HELP.md 28 | target/ 29 | !.mvn/wrapper/maven-wrapper.jar 30 | !**/src/main/**/target/ 31 | !**/src/test/**/target/ 32 | 33 | ### STS ### 34 | .apt_generated 35 | .classpath 36 | .factorypath 37 | .project 38 | .settings 39 | .springBeans 40 | .sts4-cache 41 | 42 | ### IntelliJ IDEA ### 43 | .idea 44 | *.iws 45 | *.iml 46 | *.ipr 47 | 48 | ### NetBeans ### 49 | /nbproject/private/ 50 | /nbbuild/ 51 | /dist/ 52 | /nbdist/ 53 | /.nb-gradle/ 54 | build/ 55 | !**/src/main/**/build/ 56 | !**/src/test/**/build/ 57 | 58 | ### VS Code ### 59 | .vscode/ 60 | 61 | ### Vaadin ### 62 | src/main/frontend/generated 63 | src/main/bundles/prod.bundle 64 | src/main/bundles 65 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | wrapperVersion=3.3.2 18 | distributionType=only-script 19 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spring AI Vaadin 2 | 3 | An example application that provides a Vaadin user interface for Spring AI, enabling interactive AI-powered chat experiences for Java and Spring developers. 4 | 5 | ## Overview 6 | 7 | This starter project demonstrates how to build AI-powered chat interfaces in a Spring Boot application by combining Spring AI with Vaadin's rich UI components. 8 | 9 | > **Note:** A React-based frontend (using Vaadin [Hilla](https://vaadin.com/docs/latest/hilla/faq)) is available on the `hilla` branch. When switching between branches, delete the `src/main/frontend/generated` directory to avoid startup issues. 10 | 11 | ## Features 12 | 13 | - **AI-powered Chat Interface**: Interact with the AI assistant through a chat UI with support for file uploads as attachments 14 | - **RAG Support**: Upload and analyze documents to provide context for your AI queries 15 | - **Model Context Protocol (MCP)**: Optional MCP integration for enhanced capabilities 16 | 17 | ## Prerequisites 18 | 19 | - Java 21 or higher 20 | - OpenAI API key 21 | 22 | ## Getting Started 23 | 24 | ### 1. Configure your API key 25 | 26 | Add your OpenAI API key to your environment variables: 27 | 28 | ```bash 29 | export OPENAI_API_KEY=your-api-key 30 | ``` 31 | 32 | ### 2. Build and run the application 33 | 34 | ```bash 35 | ./mvnw spring-boot:run 36 | ``` 37 | 38 | The application will be available at http://localhost:8080. 39 | 40 | ## Using RAG (Retrieval Augmented Generation) 41 | 42 | 1. Open the settings panel by clicking the gear icon 43 | 2. Upload relevant documents (PDF, DOCX, TXT, etc.) 44 | 3. Ask questions related to the uploaded content 45 | 46 | The AI will then use the document content to provide more accurate and contextual responses. 47 | 48 | ## Technologies 49 | 50 | - **Spring Boot**: Application framework 51 | - **Spring AI**: AI capabilities integration 52 | - **Vaadin**: UI framework 53 | - **Model Context Protocol (MCP)**: For enhanced capabilities 54 | 55 | ## License 56 | 57 | This project is licensed under the Apache License 2.0 - see the LICENSE file for details. 58 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Apache Maven Wrapper startup batch script, version 3.3.2 23 | # 24 | # Optional ENV vars 25 | # ----------------- 26 | # JAVA_HOME - location of a JDK home dir, required when download maven via java source 27 | # MVNW_REPOURL - repo url base for downloading maven distribution 28 | # MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 29 | # MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output 30 | # ---------------------------------------------------------------------------- 31 | 32 | set -euf 33 | [ "${MVNW_VERBOSE-}" != debug ] || set -x 34 | 35 | # OS specific support. 36 | native_path() { printf %s\\n "$1"; } 37 | case "$(uname)" in 38 | CYGWIN* | MINGW*) 39 | [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" 40 | native_path() { cygpath --path --windows "$1"; } 41 | ;; 42 | esac 43 | 44 | # set JAVACMD and JAVACCMD 45 | set_java_home() { 46 | # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched 47 | if [ -n "${JAVA_HOME-}" ]; then 48 | if [ -x "$JAVA_HOME/jre/sh/java" ]; then 49 | # IBM's JDK on AIX uses strange locations for the executables 50 | JAVACMD="$JAVA_HOME/jre/sh/java" 51 | JAVACCMD="$JAVA_HOME/jre/sh/javac" 52 | else 53 | JAVACMD="$JAVA_HOME/bin/java" 54 | JAVACCMD="$JAVA_HOME/bin/javac" 55 | 56 | if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then 57 | echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 58 | echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 59 | return 1 60 | fi 61 | fi 62 | else 63 | JAVACMD="$( 64 | 'set' +e 65 | 'unset' -f command 2>/dev/null 66 | 'command' -v java 67 | )" || : 68 | JAVACCMD="$( 69 | 'set' +e 70 | 'unset' -f command 2>/dev/null 71 | 'command' -v javac 72 | )" || : 73 | 74 | if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then 75 | echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 76 | return 1 77 | fi 78 | fi 79 | } 80 | 81 | # hash string like Java String::hashCode 82 | hash_string() { 83 | str="${1:-}" h=0 84 | while [ -n "$str" ]; do 85 | char="${str%"${str#?}"}" 86 | h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) 87 | str="${str#?}" 88 | done 89 | printf %x\\n $h 90 | } 91 | 92 | verbose() { :; } 93 | [ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } 94 | 95 | die() { 96 | printf %s\\n "$1" >&2 97 | exit 1 98 | } 99 | 100 | trim() { 101 | # MWRAPPER-139: 102 | # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. 103 | # Needed for removing poorly interpreted newline sequences when running in more 104 | # exotic environments such as mingw bash on Windows. 105 | printf "%s" "${1}" | tr -d '[:space:]' 106 | } 107 | 108 | # parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties 109 | while IFS="=" read -r key value; do 110 | case "${key-}" in 111 | distributionUrl) distributionUrl=$(trim "${value-}") ;; 112 | distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; 113 | esac 114 | done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" 115 | [ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" 116 | 117 | case "${distributionUrl##*/}" in 118 | maven-mvnd-*bin.*) 119 | MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ 120 | case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in 121 | *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; 122 | :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; 123 | :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; 124 | :Linux*x86_64*) distributionPlatform=linux-amd64 ;; 125 | *) 126 | echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 127 | distributionPlatform=linux-amd64 128 | ;; 129 | esac 130 | distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" 131 | ;; 132 | maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; 133 | *) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; 134 | esac 135 | 136 | # apply MVNW_REPOURL and calculate MAVEN_HOME 137 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 138 | [ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" 139 | distributionUrlName="${distributionUrl##*/}" 140 | distributionUrlNameMain="${distributionUrlName%.*}" 141 | distributionUrlNameMain="${distributionUrlNameMain%-bin}" 142 | MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" 143 | MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" 144 | 145 | exec_maven() { 146 | unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : 147 | exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" 148 | } 149 | 150 | if [ -d "$MAVEN_HOME" ]; then 151 | verbose "found existing MAVEN_HOME at $MAVEN_HOME" 152 | exec_maven "$@" 153 | fi 154 | 155 | case "${distributionUrl-}" in 156 | *?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; 157 | *) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; 158 | esac 159 | 160 | # prepare tmp dir 161 | if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then 162 | clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } 163 | trap clean HUP INT TERM EXIT 164 | else 165 | die "cannot create temp dir" 166 | fi 167 | 168 | mkdir -p -- "${MAVEN_HOME%/*}" 169 | 170 | # Download and Install Apache Maven 171 | verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 172 | verbose "Downloading from: $distributionUrl" 173 | verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 174 | 175 | # select .zip or .tar.gz 176 | if ! command -v unzip >/dev/null; then 177 | distributionUrl="${distributionUrl%.zip}.tar.gz" 178 | distributionUrlName="${distributionUrl##*/}" 179 | fi 180 | 181 | # verbose opt 182 | __MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' 183 | [ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v 184 | 185 | # normalize http auth 186 | case "${MVNW_PASSWORD:+has-password}" in 187 | '') MVNW_USERNAME='' MVNW_PASSWORD='' ;; 188 | has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; 189 | esac 190 | 191 | if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then 192 | verbose "Found wget ... using wget" 193 | wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" 194 | elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then 195 | verbose "Found curl ... using curl" 196 | curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" 197 | elif set_java_home; then 198 | verbose "Falling back to use Java to download" 199 | javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" 200 | targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" 201 | cat >"$javaSource" <<-END 202 | public class Downloader extends java.net.Authenticator 203 | { 204 | protected java.net.PasswordAuthentication getPasswordAuthentication() 205 | { 206 | return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); 207 | } 208 | public static void main( String[] args ) throws Exception 209 | { 210 | setDefault( new Downloader() ); 211 | java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); 212 | } 213 | } 214 | END 215 | # For Cygwin/MinGW, switch paths to Windows format before running javac and java 216 | verbose " - Compiling Downloader.java ..." 217 | "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" 218 | verbose " - Running Downloader.java ..." 219 | "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" 220 | fi 221 | 222 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 223 | if [ -n "${distributionSha256Sum-}" ]; then 224 | distributionSha256Result=false 225 | if [ "$MVN_CMD" = mvnd.sh ]; then 226 | echo "Checksum validation is not supported for maven-mvnd." >&2 227 | echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 228 | exit 1 229 | elif command -v sha256sum >/dev/null; then 230 | if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then 231 | distributionSha256Result=true 232 | fi 233 | elif command -v shasum >/dev/null; then 234 | if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then 235 | distributionSha256Result=true 236 | fi 237 | else 238 | echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 239 | echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 240 | exit 1 241 | fi 242 | if [ $distributionSha256Result = false ]; then 243 | echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 244 | echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 245 | exit 1 246 | fi 247 | fi 248 | 249 | # unzip and move 250 | if command -v unzip >/dev/null; then 251 | unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" 252 | else 253 | tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" 254 | fi 255 | printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" 256 | mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" 257 | 258 | clean || : 259 | exec_maven "$@" 260 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | <# : batch portion 2 | @REM ---------------------------------------------------------------------------- 3 | @REM Licensed to the Apache Software Foundation (ASF) under one 4 | @REM or more contributor license agreements. See the NOTICE file 5 | @REM distributed with this work for additional information 6 | @REM regarding copyright ownership. The ASF licenses this file 7 | @REM to you under the Apache License, Version 2.0 (the 8 | @REM "License"); you may not use this file except in compliance 9 | @REM with the License. You may obtain a copy of the License at 10 | @REM 11 | @REM http://www.apache.org/licenses/LICENSE-2.0 12 | @REM 13 | @REM Unless required by applicable law or agreed to in writing, 14 | @REM software distributed under the License is distributed on an 15 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | @REM KIND, either express or implied. See the License for the 17 | @REM specific language governing permissions and limitations 18 | @REM under the License. 19 | @REM ---------------------------------------------------------------------------- 20 | 21 | @REM ---------------------------------------------------------------------------- 22 | @REM Apache Maven Wrapper startup batch script, version 3.3.2 23 | @REM 24 | @REM Optional ENV vars 25 | @REM MVNW_REPOURL - repo url base for downloading maven distribution 26 | @REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 27 | @REM MVNW_VERBOSE - true: enable verbose log; others: silence the output 28 | @REM ---------------------------------------------------------------------------- 29 | 30 | @IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) 31 | @SET __MVNW_CMD__= 32 | @SET __MVNW_ERROR__= 33 | @SET __MVNW_PSMODULEP_SAVE=%PSModulePath% 34 | @SET PSModulePath= 35 | @FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( 36 | IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) 37 | ) 38 | @SET PSModulePath=%__MVNW_PSMODULEP_SAVE% 39 | @SET __MVNW_PSMODULEP_SAVE= 40 | @SET __MVNW_ARG0_NAME__= 41 | @SET MVNW_USERNAME= 42 | @SET MVNW_PASSWORD= 43 | @IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) 44 | @echo Cannot start maven from wrapper >&2 && exit /b 1 45 | @GOTO :EOF 46 | : end batch / begin powershell #> 47 | 48 | $ErrorActionPreference = "Stop" 49 | if ($env:MVNW_VERBOSE -eq "true") { 50 | $VerbosePreference = "Continue" 51 | } 52 | 53 | # calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties 54 | $distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl 55 | if (!$distributionUrl) { 56 | Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" 57 | } 58 | 59 | switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { 60 | "maven-mvnd-*" { 61 | $USE_MVND = $true 62 | $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" 63 | $MVN_CMD = "mvnd.cmd" 64 | break 65 | } 66 | default { 67 | $USE_MVND = $false 68 | $MVN_CMD = $script -replace '^mvnw','mvn' 69 | break 70 | } 71 | } 72 | 73 | # apply MVNW_REPOURL and calculate MAVEN_HOME 74 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 75 | if ($env:MVNW_REPOURL) { 76 | $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } 77 | $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" 78 | } 79 | $distributionUrlName = $distributionUrl -replace '^.*/','' 80 | $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' 81 | $MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" 82 | if ($env:MAVEN_USER_HOME) { 83 | $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" 84 | } 85 | $MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' 86 | $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" 87 | 88 | if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { 89 | Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" 90 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 91 | exit $? 92 | } 93 | 94 | if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { 95 | Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" 96 | } 97 | 98 | # prepare tmp dir 99 | $TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile 100 | $TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" 101 | $TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null 102 | trap { 103 | if ($TMP_DOWNLOAD_DIR.Exists) { 104 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 105 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 106 | } 107 | } 108 | 109 | New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null 110 | 111 | # Download and Install Apache Maven 112 | Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 113 | Write-Verbose "Downloading from: $distributionUrl" 114 | Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 115 | 116 | $webclient = New-Object System.Net.WebClient 117 | if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { 118 | $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) 119 | } 120 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 121 | $webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null 122 | 123 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 124 | $distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum 125 | if ($distributionSha256Sum) { 126 | if ($USE_MVND) { 127 | Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." 128 | } 129 | Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash 130 | if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { 131 | Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." 132 | } 133 | } 134 | 135 | # unzip and move 136 | Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null 137 | Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null 138 | try { 139 | Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null 140 | } catch { 141 | if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { 142 | Write-Error "fail to move MAVEN_HOME" 143 | } 144 | } finally { 145 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 146 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 147 | } 148 | 149 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 150 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | org.springframework.boot 8 | spring-boot-starter-parent 9 | 3.4.5 10 | 11 | 12 | org.spring.framework.ai 13 | spring-ai-vaadin 14 | 0.0.1-SNAPSHOT 15 | spring-ai-vaadin 16 | Vaadin UIs for Spring AI 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 21 32 | 1.0.0 33 | 24.8.0.alpha8 34 | 2.44.4 35 | 36 | 37 | 38 | com.vaadin 39 | vaadin-spring-boot-starter 40 | 41 | 42 | org.springframework.ai 43 | spring-ai-starter-model-openai 44 | 45 | 46 | org.springframework.ai 47 | spring-ai-tika-document-reader 48 | 49 | 50 | org.springframework.ai 51 | spring-ai-rag 52 | 53 | 54 | org.springframework.ai 55 | spring-ai-starter-mcp-client 56 | 57 | 58 | 59 | org.springframework.boot 60 | spring-boot-starter-test 61 | test 62 | 63 | 64 | 65 | 66 | 67 | org.springframework.ai 68 | spring-ai-bom 69 | ${spring-ai.version} 70 | pom 71 | import 72 | 73 | 74 | com.vaadin 75 | vaadin-bom 76 | ${vaadin.version} 77 | pom 78 | import 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | org.springframework.boot 87 | spring-boot-maven-plugin 88 | 89 | 90 | com.diffplug.spotless 91 | spotless-maven-plugin 92 | ${spotless.version} 93 | 94 | 95 | 96 | src/main/java/**/*.java 97 | src/test/java/**/*.java 98 | 99 | 100 | 101 | java,javax,org,com, 102 | 103 | 104 | 105 | 106 | 107 | 1.17.0 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | src/main/resources/**/*.xml 116 | pom.xml 117 | 118 | 119 | XML 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | spring-milestones 130 | Spring Milestones 131 | https://repo.spring.io/milestone 132 | 133 | false 134 | 135 | 136 | 137 | 138 | 139 | true 140 | 141 | vaadin-prereleases 142 | https://maven.vaadin.com/vaadin-prereleases 143 | 144 | 145 | 146 | 147 | Central Portal Snapshots 148 | central-portal-snapshots 149 | https://central.sonatype.com/repository/maven-snapshots/ 150 | 151 | false 152 | 153 | 154 | true 155 | 156 | 157 | 158 | 159 | 160 | 161 | vaadin-prereleases 162 | https://maven.vaadin.com/vaadin-prereleases 163 | 164 | false 165 | 166 | 167 | 168 | 169 | 170 | 171 | production 172 | 173 | 174 | com.vaadin 175 | vaadin-core 176 | 177 | 178 | com.vaadin 179 | vaadin-dev 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | com.vaadin 189 | vaadin-maven-plugin 190 | ${vaadin.version} 191 | 192 | 193 | frontend 194 | compile 195 | 196 | prepare-frontend 197 | build-frontend 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | -------------------------------------------------------------------------------- /src/main/frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | -------------------------------------------------------------------------------- /src/main/frontend/themes/spring-ai-vaadin/chat-header.css: -------------------------------------------------------------------------------- 1 | /* Chat Header Styles */ 2 | .chat-header { 3 | padding: 0.5rem 1rem; 4 | border-bottom: 1px solid var(--lumo-contrast-10pct); 5 | display: flex; 6 | align-items: center; 7 | background-color: var(--lumo-base-color); 8 | z-index: 1; 9 | } 10 | 11 | .chat-heading { 12 | font-size: var(--lumo-font-size-l); 13 | margin: 0; 14 | display: flex; 15 | align-items: center; 16 | gap: 0.5rem; 17 | } 18 | -------------------------------------------------------------------------------- /src/main/frontend/themes/spring-ai-vaadin/chat.css: -------------------------------------------------------------------------------- 1 | /* Chat Component Styles */ 2 | .chat { 3 | display: flex; 4 | flex-direction: column; 5 | overflow: hidden; 6 | } 7 | 8 | .chat-scroller { 9 | flex: 1; 10 | scroll-snap-type: y proximity; 11 | } 12 | 13 | .chat-scroller vaadin-message:has([class*="typing-indicator"]) { 14 | opacity: 0; 15 | animation: waiting-message-fade-in 1s forwards 1s; 16 | } 17 | 18 | .chat-scroller::after { 19 | display: block; 20 | content: ""; 21 | scroll-snap-align: end; 22 | min-height: 1px; 23 | } 24 | 25 | @keyframes waiting-message-fade-in { 26 | to { 27 | opacity: 1; 28 | } 29 | } 30 | 31 | .chat-upload { 32 | border: none; 33 | } 34 | 35 | .chat-input { 36 | padding: 0; 37 | overflow: visible; 38 | } 39 | 40 | .typing-indicator { 41 | margin: 6px; 42 | width: 6px; 43 | height: 6px; 44 | border-radius: 50%; 45 | background: transparent; 46 | position: relative; 47 | animation: typing 1.2s infinite ease-in-out; 48 | } 49 | 50 | /* Attachment styles */ 51 | .attachments { 52 | display: flex; 53 | flex-wrap: wrap; 54 | gap: var(--lumo-space-s); 55 | margin-bottom: var(--lumo-space-s); 56 | } 57 | 58 | .attachment { 59 | position: relative; 60 | border-radius: var(--lumo-border-radius-m); 61 | box-shadow: var(--lumo-box-shadow-xs); 62 | background-color: var(--lumo-shade-5pct); 63 | overflow: hidden; 64 | width: 160px; 65 | height: 140px; 66 | text-align: center; 67 | } 68 | 69 | .attachment-icon { 70 | color: var(--lumo-primary-text-color); 71 | font-size: var(--lumo-font-size-xxxl); 72 | display: flex; 73 | justify-content: center; 74 | align-items: center; 75 | height: 100%; 76 | } 77 | 78 | .attachment-image { 79 | width: 100%; 80 | height: 100%; 81 | object-fit: cover; 82 | } 83 | 84 | .attachment-name { 85 | position: absolute; 86 | bottom: 0; 87 | left: 0; 88 | right: 0; 89 | font-size: var(--lumo-font-size-xs); 90 | font-weight: 500; 91 | color: var(--lumo-secondary-text-color); 92 | white-space: nowrap; 93 | overflow: hidden; 94 | text-overflow: ellipsis; 95 | padding: var(--lumo-space-xs); 96 | background-color: rgba(255, 255, 255, 0.8); 97 | text-align: center; 98 | } 99 | 100 | @keyframes typing { 101 | 0% { 102 | box-shadow: 103 | 0 6px 0 rgba(85, 85, 85, 0.3), 104 | 16px 6px 0 rgba(85, 85, 85, 0.3), 105 | 32px 6px 0 rgba(85, 85, 85, 0.3); 106 | } 107 | 15% { 108 | box-shadow: 109 | 0 6px 0 rgba(85, 85, 85, 1), 110 | 16px 6px 0 rgba(85, 85, 85, 0.3), 111 | 32px 6px 0 rgba(85, 85, 85, 0.3); 112 | } 113 | 30% { 114 | box-shadow: 115 | 0 6px 0 rgba(85, 85, 85, 0.3), 116 | 16px 6px 0 rgba(85, 85, 85, 1), 117 | 32px 6px 0 rgba(85, 85, 85, 0.3); 118 | } 119 | 45% { 120 | box-shadow: 121 | 0 6px 0 rgba(85, 85, 85, 0.3), 122 | 16px 6px 0 rgba(85, 85, 85, 0.3), 123 | 32px 6px 0 rgba(85, 85, 85, 1); 124 | } 125 | 60%, 126 | 100% { 127 | box-shadow: 128 | 0 6px 0 rgba(85, 85, 85, 0.3), 129 | 16px 6px 0 rgba(85, 85, 85, 0.3), 130 | 32px 6px 0 rgba(85, 85, 85, 0.3); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/main/frontend/themes/spring-ai-vaadin/main-view.css: -------------------------------------------------------------------------------- 1 | /* Main View Layout */ 2 | .settings-panel { 3 | border-left: 1px solid var(--lumo-contrast-10pct); 4 | } 5 | -------------------------------------------------------------------------------- /src/main/frontend/themes/spring-ai-vaadin/settings-panel.css: -------------------------------------------------------------------------------- 1 | /* Settings Panel Styles */ 2 | .settings-panel { 3 | padding: 1rem; 4 | overflow-y: auto; 5 | } 6 | 7 | .settings-header { 8 | display: flex; 9 | align-items: center; 10 | margin-bottom: 1rem; 11 | } 12 | -------------------------------------------------------------------------------- /src/main/frontend/themes/spring-ai-vaadin/styles.css: -------------------------------------------------------------------------------- 1 | @import "chat-header.css"; 2 | @import "chat.css"; 3 | @import "main-view.css"; 4 | @import "settings-panel.css"; 5 | -------------------------------------------------------------------------------- /src/main/java/org/spring/framework/ai/vaadin/AiConfig.java: -------------------------------------------------------------------------------- 1 | package org.spring.framework.ai.vaadin; 2 | 3 | import org.springframework.ai.embedding.EmbeddingModel; 4 | import org.springframework.ai.vectorstore.SimpleVectorStore; 5 | import org.springframework.ai.vectorstore.VectorStore; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | 9 | @Configuration 10 | public class AiConfig { 11 | 12 | @Bean 13 | public VectorStore vectorStore(EmbeddingModel embeddingModel) { 14 | return SimpleVectorStore.builder(embeddingModel).build(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/org/spring/framework/ai/vaadin/SpringAiVaadinApplication.java: -------------------------------------------------------------------------------- 1 | package org.spring.framework.ai.vaadin; 2 | 3 | import com.vaadin.flow.component.page.AppShellConfigurator; 4 | import com.vaadin.flow.component.page.Push; 5 | import com.vaadin.flow.theme.Theme; 6 | import org.springframework.boot.SpringApplication; 7 | import org.springframework.boot.autoconfigure.SpringBootApplication; 8 | 9 | @Push 10 | @SpringBootApplication 11 | @Theme("spring-ai-vaadin") 12 | public class SpringAiVaadinApplication implements AppShellConfigurator { 13 | 14 | public static void main(String[] args) { 15 | SpringApplication.run(SpringAiVaadinApplication.class, args); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/org/spring/framework/ai/vaadin/service/Assistant.java: -------------------------------------------------------------------------------- 1 | package org.spring.framework.ai.vaadin.service; 2 | 3 | import io.modelcontextprotocol.client.McpSyncClient; 4 | import jakarta.annotation.Nullable; 5 | import java.util.List; 6 | import org.springframework.ai.chat.client.ChatClient; 7 | import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; 8 | import org.springframework.ai.chat.client.advisor.SafeGuardAdvisor; 9 | import org.springframework.ai.chat.memory.ChatMemory; 10 | import org.springframework.ai.chat.messages.MessageType; 11 | import org.springframework.ai.content.Media; 12 | import org.springframework.ai.document.Document; 13 | import org.springframework.ai.mcp.SyncMcpToolCallbackProvider; 14 | import org.springframework.ai.rag.advisor.RetrievalAugmentationAdvisor; 15 | import org.springframework.ai.rag.generation.augmentation.ContextualQueryAugmenter; 16 | import org.springframework.ai.rag.preretrieval.query.transformation.RewriteQueryTransformer; 17 | import org.springframework.ai.rag.retrieval.search.VectorStoreDocumentRetriever; 18 | import org.springframework.ai.reader.tika.TikaDocumentReader; 19 | import org.springframework.ai.vectorstore.VectorStore; 20 | import org.springframework.core.io.ByteArrayResource; 21 | import org.springframework.stereotype.Service; 22 | import org.springframework.util.MimeType; 23 | import reactor.core.publisher.Flux; 24 | 25 | @Service 26 | public class Assistant { 27 | public record ChatOptions(String systemMessage, boolean useMcp) {} 28 | 29 | private final ChatOptions defaultOptions = new ChatOptions("", false); 30 | 31 | private final ChatClient chatClient; 32 | private final ChatMemory chatMemory; 33 | private final List mcpSyncClients; 34 | 35 | private static final String DEFAULT_SYSTEM = 36 | """ 37 | You are an expert on all things Java and Spring related. 38 | Answer questions in a friendly manner and give clear explanations. 39 | Always give example code snippets when explaining code. 40 | """; 41 | 42 | private static final String ATTACHMENT_TEMPLATE = 43 | """ 44 | 45 | %s 46 | 47 | """; 48 | 49 | public Assistant( 50 | ChatMemory chatMemory, 51 | ChatClient.Builder builder, 52 | VectorStore vectorStore, 53 | List mcpSyncClients) { 54 | this.chatMemory = chatMemory; 55 | this.mcpSyncClients = mcpSyncClients; 56 | 57 | chatClient = 58 | builder 59 | .defaultAdvisors( 60 | 61 | // Absolutely don't let people ask about PHP 😆 62 | new SafeGuardAdvisor(List.of("PHP")), 63 | 64 | // Remember the conversation 65 | MessageChatMemoryAdvisor.builder(chatMemory).build(), 66 | 67 | // Define RAG pipeline 68 | // See 69 | // https://docs.spring.io/spring-ai/reference/api/retrieval-augmented-generation.html#modules 70 | RetrievalAugmentationAdvisor.builder() 71 | .queryTransformers( 72 | // Rewrite the query for better search results 73 | RewriteQueryTransformer.builder() 74 | .chatClientBuilder(builder.build().mutate()) 75 | .build()) 76 | // Allow empty context (so you can try the assistant without context and 77 | // compare) 78 | .queryAugmenter( 79 | ContextualQueryAugmenter.builder().allowEmptyContext(true).build()) 80 | 81 | // Use the vector store to retrieve documents 82 | .documentRetriever( 83 | VectorStoreDocumentRetriever.builder() 84 | .similarityThreshold(0.50) 85 | .vectorStore(vectorStore) 86 | .build()) 87 | .build()) 88 | .build(); 89 | } 90 | 91 | public Flux stream( 92 | String chatId, 93 | String userMessage, 94 | List attachments, 95 | @Nullable ChatOptions options) { 96 | if (options == null) { 97 | options = defaultOptions; 98 | } 99 | 100 | var system = options.systemMessage().isBlank() ? DEFAULT_SYSTEM : options.systemMessage(); 101 | 102 | var processedAttachments = processAttachments(attachments); 103 | 104 | var prompt = 105 | chatClient 106 | .prompt() 107 | .system(system) 108 | .user( 109 | u -> { 110 | u.text(userMessage + processedAttachments.documentContent()); 111 | u.media(processedAttachments.mediaList().toArray(Media[]::new)); 112 | }) 113 | .advisors( 114 | a -> { 115 | a.param(ChatMemory.CONVERSATION_ID, chatId); 116 | }); 117 | 118 | if (options.useMcp) { 119 | prompt.toolCallbacks(new SyncMcpToolCallbackProvider(mcpSyncClients)); 120 | } 121 | 122 | return prompt.stream().content(); 123 | } 124 | 125 | public List getHistory(String chatId) { 126 | return chatMemory.get(chatId).stream() 127 | .filter( 128 | message -> 129 | message.getMessageType().equals(MessageType.USER) 130 | || message.getMessageType().equals(MessageType.ASSISTANT)) 131 | // TODO: Add attachments 132 | .map( 133 | message -> 134 | new Message( 135 | message.getMessageType().toString().toLowerCase(), 136 | message.getText(), 137 | List.of())) 138 | .toList(); 139 | } 140 | 141 | public void closeChat(String chatId) { 142 | chatMemory.clear(chatId); 143 | } 144 | 145 | private record ProcessedAttachments(String documentContent, List mediaList) {} 146 | 147 | private ProcessedAttachments processAttachments(List attachments) { 148 | // Map text and pdf attachments as documents wrapped in tags 149 | var documentList = 150 | attachments.stream() 151 | .filter( 152 | attachment -> 153 | attachment.contentType().contains("text") 154 | || attachment.contentType().contains("pdf")) 155 | .toList(); 156 | 157 | var documentBuilder = new StringBuilder("\n"); 158 | documentList.forEach( 159 | attachment -> { 160 | var data = new ByteArrayResource(attachment.data()); 161 | var documents = new TikaDocumentReader(data).read(); 162 | var content = String.join("\n", documents.stream().map(Document::getText).toList()); 163 | documentBuilder.append( 164 | String.format(ATTACHMENT_TEMPLATE, attachment.fileName(), content)); 165 | }); 166 | 167 | // Map image attachments to Media objects 168 | var mediaList = 169 | attachments.stream() 170 | .filter(attachment -> attachment.contentType().contains("image")) 171 | .map( 172 | attachment -> 173 | new Media( 174 | MimeType.valueOf(attachment.contentType()), 175 | new ByteArrayResource(attachment.data()))) 176 | .toList(); 177 | 178 | return new ProcessedAttachments(documentBuilder.toString(), mediaList); 179 | } 180 | 181 | public static record Attachment(String type, String key, String fileName, String url) {} 182 | 183 | public static record Message( 184 | String role, String content, @Nullable List attachments) {} 185 | } 186 | -------------------------------------------------------------------------------- /src/main/java/org/spring/framework/ai/vaadin/service/AttachmentFile.java: -------------------------------------------------------------------------------- 1 | package org.spring.framework.ai.vaadin.service; 2 | 3 | public record AttachmentFile(String fileName, String contentType, byte[] data) {} 4 | -------------------------------------------------------------------------------- /src/main/java/org/spring/framework/ai/vaadin/service/RagContextService.java: -------------------------------------------------------------------------------- 1 | package org.spring.framework.ai.vaadin.service; 2 | 3 | import java.io.IOException; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | import org.springframework.ai.reader.tika.TikaDocumentReader; 7 | import org.springframework.ai.transformer.splitter.TokenTextSplitter; 8 | import org.springframework.ai.vectorstore.VectorStore; 9 | import org.springframework.core.io.InputStreamResource; 10 | import org.springframework.stereotype.Service; 11 | import org.springframework.web.multipart.MultipartFile; 12 | 13 | /** 14 | * Service for managing contextual data for Retrieval-Augmented Generation (RAG) processes. 15 | * This service allows the addition of file data to a context that can be utilized for 16 | * document retrieval and text processing operations. 17 | * 18 | * Files are managed in an in-memory list for simplicity. 19 | */ 20 | @Service 21 | public class RagContextService { 22 | 23 | private final VectorStore vectorStore; 24 | private final List filesInContext = new ArrayList<>(); 25 | 26 | public RagContextService(VectorStore vectorStore) { 27 | this.vectorStore = vectorStore; 28 | } 29 | 30 | public void addFileToContext(MultipartFile file) throws IOException { 31 | var resource = new InputStreamResource(file.getInputStream()); 32 | vectorStore.write(new TokenTextSplitter().apply(new TikaDocumentReader(resource).read())); 33 | 34 | filesInContext.add(file.getOriginalFilename()); 35 | } 36 | 37 | public List getFilesInContext() { 38 | return filesInContext; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/org/spring/framework/ai/vaadin/service/package-info.java: -------------------------------------------------------------------------------- 1 | @NonNullApi 2 | package org.spring.framework.ai.vaadin.service; 3 | 4 | import org.springframework.lang.NonNullApi; 5 | -------------------------------------------------------------------------------- /src/main/java/org/spring/framework/ai/vaadin/ui/component/Chat.java: -------------------------------------------------------------------------------- 1 | package org.spring.framework.ai.vaadin.ui.component; 2 | 3 | import com.vaadin.flow.component.messages.MessageInput; 4 | import com.vaadin.flow.component.messages.MessageList; 5 | import com.vaadin.flow.component.orderedlayout.Scroller; 6 | import com.vaadin.flow.component.orderedlayout.VerticalLayout; 7 | import com.vaadin.flow.component.upload.Upload; 8 | import com.vaadin.flow.server.streams.UploadHandler; 9 | import java.util.ArrayList; 10 | import java.util.Base64; 11 | import java.util.List; 12 | import org.spring.framework.ai.vaadin.ui.component.ChatMessage.ChatAttachment; 13 | import org.spring.framework.ai.vaadin.ui.util.ImageUtils; 14 | 15 | /** 16 | * A chat component that provides message list, input field, and file upload functionality. This 17 | * component is used for interactions between user and AI assistant. Supports text messaging in 18 | * Markdown and file attachments. 19 | */ 20 | public class Chat extends VerticalLayout { 21 | private static final int MAX_FILE_COUNT = 10; 22 | private static final int MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB 23 | private static final String[] ACCEPTED_FILE_TYPES = {"image/*", "text/*", "application/pdf"}; 24 | 25 | private final Upload upload; 26 | private final MessageInput messageInput; 27 | private final MessageList messageList; 28 | 29 | private final List pendingAttachments = new ArrayList<>(); 30 | private ChatSubmitListener chatSubmitListener; 31 | 32 | public Chat() { 33 | addClassName("chat"); 34 | 35 | // Message list 36 | messageList = new MessageList(); 37 | messageList.setMarkdown(true); 38 | messageList.addClassName("message-list"); 39 | 40 | // Create upload component 41 | upload = new Upload(createUploadHandler()); 42 | upload.addFileRemovedListener( 43 | event -> { 44 | var fileName = event.getFileName(); 45 | pendingAttachments.removeIf(attachment -> attachment.fileName().equals(fileName)); 46 | }); 47 | 48 | upload.addClassName("chat-upload"); 49 | upload.setWidthFull(); 50 | upload.setMaxFiles(MAX_FILE_COUNT); 51 | upload.setMaxFileSize(MAX_FILE_SIZE); 52 | upload.setAcceptedFileTypes(ACCEPTED_FILE_TYPES); 53 | 54 | // Create message input 55 | messageInput = new MessageInput(); 56 | messageInput.addClassName("chat-input"); 57 | messageInput.setWidthFull(); 58 | 59 | // Set up message sending 60 | messageInput.addSubmitListener(event -> sendMessage(event.getValue())); 61 | 62 | // Layout components 63 | var scroller = new Scroller(messageList); 64 | scroller.addClassName("chat-scroller"); 65 | setFlexGrow(1, scroller); 66 | add(scroller); 67 | 68 | upload.getElement().appendChild(messageInput.getElement()); 69 | setFlexShrink(0, upload); 70 | add(upload); 71 | } 72 | 73 | private UploadHandler createUploadHandler() { 74 | return UploadHandler.inMemory( 75 | (meta, data) -> { 76 | var url = ""; 77 | if (meta.contentType().startsWith("image/")) { 78 | // Create thumbnail before encoding to Base64 data URL 79 | var thumbnailData = ImageUtils.createThumbnail(data, meta.contentType(), 160, 140); 80 | var base64 = Base64.getEncoder().encodeToString(thumbnailData); 81 | url = "data:" + meta.contentType() + ";base64," + base64; 82 | } 83 | pendingAttachments.add( 84 | new ChatAttachment(meta.contentType(), meta.fileName(), data, url)); 85 | }); 86 | } 87 | 88 | /** Clears all messages from the chat. */ 89 | public void clearMessages() { 90 | messageList.setItems(new ArrayList<>()); 91 | } 92 | 93 | /** 94 | * Sets the messages to display in the chat. 95 | * 96 | * @param messages List of chat messages to display 97 | */ 98 | public void setMessages(List messages) { 99 | messageList.setItems(messages.stream().map(message -> message.messageListItem).toList()); 100 | } 101 | 102 | /** Sets focus to the message input field. */ 103 | public void focusInput() { 104 | messageInput.focus(); 105 | } 106 | 107 | /** 108 | * Sends a new message in the chat. Creates both a user message and an assistant message 109 | * placeholder. 110 | * 111 | * @param message The message content to send 112 | */ 113 | private void sendMessage(String message) { 114 | if (!isEnabled() || message.isEmpty()) { 115 | return; 116 | } 117 | 118 | var userMessage = new ChatMessage("User", message, pendingAttachments); 119 | messageList.addItem(userMessage.messageListItem); 120 | 121 | var assistantMessage = new ChatMessage("Assistant", null, null); 122 | messageList.addItem(assistantMessage.messageListItem); 123 | 124 | chatSubmitListener.onSubmit(userMessage, assistantMessage); 125 | 126 | pendingAttachments.clear(); 127 | upload.clearFileList(); 128 | } 129 | 130 | /** 131 | * Sets the listener for chat message submissions. 132 | * 133 | * @param listener The listener to be notified when a message is submitted 134 | */ 135 | public void setSubmitListener(ChatSubmitListener listener) { 136 | this.chatSubmitListener = listener; 137 | } 138 | 139 | /** Listener interface for chat message submissions. */ 140 | public interface ChatSubmitListener { 141 | /** 142 | * Called when a user submits a message. 143 | * 144 | * @param userMessage The message sent by the user 145 | * @param assistantMessage The message placeholder for the assistant's response 146 | */ 147 | void onSubmit(ChatMessage userMessage, ChatMessage assistantMessage); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/main/java/org/spring/framework/ai/vaadin/ui/component/ChatHeader.java: -------------------------------------------------------------------------------- 1 | package org.spring.framework.ai.vaadin.ui.component; 2 | 3 | import com.vaadin.flow.component.ClickEvent; 4 | import com.vaadin.flow.component.ComponentEventListener; 5 | import com.vaadin.flow.component.button.Button; 6 | import com.vaadin.flow.component.html.H1; 7 | import com.vaadin.flow.component.html.Span; 8 | import com.vaadin.flow.component.icon.Icon; 9 | import com.vaadin.flow.component.icon.VaadinIcon; 10 | import com.vaadin.flow.component.orderedlayout.HorizontalLayout; 11 | import com.vaadin.flow.shared.Registration; 12 | 13 | public class ChatHeader extends HorizontalLayout { 14 | private final Button newChatButton; 15 | private final Button settingsButton; 16 | 17 | public ChatHeader() { 18 | addClassName("chat-header"); 19 | setWidthFull(); 20 | 21 | // Create heading 22 | var heading = new H1(new Span("🌱"), new Span("Spring AI Assistant")); 23 | heading.addClassName("chat-heading"); 24 | 25 | // Create buttons 26 | newChatButton = new Button(new Icon(VaadinIcon.PLUS)); 27 | newChatButton.addThemeNames("icon", "small", "contrast", "tertiary"); 28 | newChatButton.setAriaLabel("New chat"); 29 | newChatButton.setTooltipText("New chat"); 30 | 31 | settingsButton = new Button(new Icon(VaadinIcon.COG)); 32 | settingsButton.addThemeNames("icon", "small", "contrast", "tertiary"); 33 | settingsButton.setAriaLabel("Settings"); 34 | settingsButton.setTooltipText("Settings"); 35 | 36 | // Add components to layout 37 | addToStart(heading); 38 | addToEnd(newChatButton, settingsButton); 39 | } 40 | 41 | public Registration addNewChatListener(ComponentEventListener> listener) { 42 | return newChatButton.addClickListener(listener); 43 | } 44 | 45 | public Registration addToggleSettingsListener( 46 | ComponentEventListener> listener) { 47 | return settingsButton.addClickListener(listener); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/org/spring/framework/ai/vaadin/ui/component/ChatMessage.java: -------------------------------------------------------------------------------- 1 | package org.spring.framework.ai.vaadin.ui.component; 2 | 3 | import com.vaadin.flow.component.messages.MessageListItem; 4 | import java.util.List; 5 | 6 | public class ChatMessage { 7 | private List attachments; 8 | MessageListItem messageListItem = new MessageListItem(); 9 | private final String TYPING_INDICATOR = "
"; 10 | 11 | public ChatMessage(String role, String content, List attachments) { 12 | var contentBuilder = new StringBuilder(); 13 | 14 | if (attachments != null) { 15 | // Format message attachments attachments as thumbnails in HTML 16 | var attachmentsBuilder = new StringBuilder("
"); 17 | 18 | for (var attachment : attachments) { 19 | var isImage = attachment.type.startsWith("image/"); 20 | attachmentsBuilder.append("
"); 21 | if (isImage) { 22 | attachmentsBuilder.append( 23 | ""
26 |                   + attachment.fileName
27 |                   + ""); 28 | } else { 29 | attachmentsBuilder.append("
📄
"); 30 | } 31 | ; 32 | attachmentsBuilder.append( 33 | "" + attachment.fileName + ""); 34 | attachmentsBuilder.append("
"); 35 | } 36 | 37 | attachmentsBuilder.append("
"); 38 | contentBuilder.append(attachmentsBuilder); 39 | } 40 | if (content != null) { 41 | // Append the message text content 42 | contentBuilder.append(content); 43 | } 44 | 45 | if (contentBuilder.isEmpty()) { 46 | // If no content, show typing indicator 47 | contentBuilder.append(TYPING_INDICATOR); 48 | } 49 | 50 | messageListItem.setText(contentBuilder.toString()); 51 | messageListItem.setUserColorIndex(role.equals("User") ? 0 : 1); 52 | messageListItem.setUserName(role); 53 | this.attachments = attachments; 54 | } 55 | 56 | /** 57 | * Gets the role of the message sender. 58 | * 59 | * @return The role (e.g., "User" or "Assistant") 60 | */ 61 | public String getRole() { 62 | return messageListItem.getUserName(); 63 | } 64 | 65 | /** 66 | * Gets the text content of the message. 67 | * 68 | * @return The message content 69 | */ 70 | public String getText() { 71 | return messageListItem.getText(); 72 | } 73 | 74 | /** 75 | * Appends text to the message content. 76 | * 77 | * @param text The text to append 78 | */ 79 | public void appendText(String text) { 80 | if (messageListItem.getText().equals(TYPING_INDICATOR)) { 81 | messageListItem.setText(""); 82 | } 83 | messageListItem.appendText(text); 84 | } 85 | 86 | /** 87 | * Gets the attachments associated with this message. 88 | * 89 | * @return The list of attachments, or null if none 90 | */ 91 | public List getAttachments() { 92 | return attachments; 93 | } 94 | 95 | public static record ChatAttachment(String type, String fileName, byte[] data, String url) {} 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/org/spring/framework/ai/vaadin/ui/component/SettingsPanel.java: -------------------------------------------------------------------------------- 1 | package org.spring.framework.ai.vaadin.ui.component; 2 | 3 | import com.vaadin.flow.component.ClickEvent; 4 | import com.vaadin.flow.component.ComponentEventListener; 5 | import com.vaadin.flow.component.button.Button; 6 | import com.vaadin.flow.component.checkbox.Checkbox; 7 | import com.vaadin.flow.component.html.H3; 8 | import com.vaadin.flow.component.html.H4; 9 | import com.vaadin.flow.component.html.ListItem; 10 | import com.vaadin.flow.component.html.Span; 11 | import com.vaadin.flow.component.html.UnorderedList; 12 | import com.vaadin.flow.component.icon.Icon; 13 | import com.vaadin.flow.component.icon.VaadinIcon; 14 | import com.vaadin.flow.component.orderedlayout.HorizontalLayout; 15 | import com.vaadin.flow.component.orderedlayout.VerticalLayout; 16 | import com.vaadin.flow.component.textfield.TextArea; 17 | import com.vaadin.flow.component.upload.Upload; 18 | import com.vaadin.flow.server.streams.UploadHandler; 19 | import com.vaadin.flow.shared.Registration; 20 | import org.spring.framework.ai.vaadin.service.RagContextService; 21 | import org.spring.framework.ai.vaadin.ui.util.CustomMultipartFile; 22 | 23 | public class SettingsPanel extends VerticalLayout { 24 | 25 | private final RagContextService ragContextService; 26 | private final TextArea systemMessageField; 27 | private final Checkbox useMcpField; 28 | private final UnorderedList filesList; 29 | private final Button closeButton; 30 | private Upload upload; 31 | 32 | public SettingsPanel(RagContextService ragContextService) { 33 | this.ragContextService = ragContextService; 34 | 35 | addClassName("settings-panel"); 36 | setPadding(true); 37 | setSpacing(true); 38 | 39 | // Create header 40 | var header = new HorizontalLayout(); 41 | header.addClassName("settings-header"); 42 | header.setWidthFull(); 43 | 44 | var title = new H3("Settings"); 45 | 46 | closeButton = new Button(new Icon(VaadinIcon.CLOSE)); 47 | closeButton.addThemeNames("icon", "small", "contrast", "tertiary"); 48 | closeButton.setAriaLabel("Close settings"); 49 | closeButton.setTooltipText("Close settings"); 50 | 51 | header.add(title, closeButton); 52 | header.setFlexGrow(1, title); 53 | 54 | // Create general settings section 55 | var generalSettingsHeading = new H4("General settings"); 56 | generalSettingsHeading.addClassName("settings-sub-heading"); 57 | 58 | systemMessageField = new TextArea("System Message"); 59 | systemMessageField.setWidthFull(); 60 | systemMessageField.setMinHeight("100px"); 61 | 62 | useMcpField = new Checkbox("Use MCP"); 63 | 64 | // Create RAG data sources section 65 | var ragHeading = new H4("RAG data sources"); 66 | ragHeading.addClassName("settings-sub-heading"); 67 | 68 | // Files list 69 | filesList = new UnorderedList(); 70 | 71 | // File upload 72 | upload = new Upload(createUploadHandler()); 73 | upload.setMaxFiles(10); 74 | upload.setMaxFileSize(10 * 1024 * 1024); 75 | upload.setAcceptedFileTypes(".txt", ".pdf", ".md", ".doc", ".docx"); 76 | 77 | // Add everything to the layout 78 | add( 79 | header, 80 | generalSettingsHeading, 81 | systemMessageField, 82 | useMcpField, 83 | ragHeading, 84 | filesList, 85 | upload); 86 | 87 | updateFilesList(); 88 | } 89 | 90 | private UploadHandler createUploadHandler() { 91 | return UploadHandler.inMemory( 92 | (meta, data) -> { 93 | try { 94 | // Upload file to RAG context 95 | ragContextService.addFileToContext( 96 | new CustomMultipartFile(meta.fileName(), meta.contentType(), data)); 97 | 98 | // Update the files list 99 | getUI().get().access(() -> updateFilesList()); 100 | 101 | } catch (Exception e) { 102 | e.printStackTrace(); 103 | } 104 | }); 105 | } 106 | 107 | public Registration addCloseListener(ComponentEventListener> listener) { 108 | return closeButton.addClickListener(listener); 109 | } 110 | 111 | public void updateFilesList() { 112 | var files = ragContextService.getFilesInContext(); 113 | 114 | filesList.removeAll(); 115 | upload.clearFileList(); 116 | 117 | if (files == null || files.isEmpty()) { 118 | return; 119 | } 120 | 121 | for (var file : files) { 122 | var item = new ListItem(new Span(file)); 123 | filesList.add(item); 124 | } 125 | } 126 | 127 | public String getSystemMessage() { 128 | return systemMessageField.getValue(); 129 | } 130 | 131 | public void setSystemMessage(String message) { 132 | systemMessageField.setValue(message != null ? message : ""); 133 | } 134 | 135 | public boolean isUseMcp() { 136 | return useMcpField.getValue(); 137 | } 138 | 139 | public void setUseMcp(boolean useMcp) { 140 | useMcpField.setValue(useMcp); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/main/java/org/spring/framework/ai/vaadin/ui/util/CustomMultipartFile.java: -------------------------------------------------------------------------------- 1 | package org.spring.framework.ai.vaadin.ui.util; 2 | 3 | import java.io.ByteArrayInputStream; 4 | import java.io.File; 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | import org.springframework.web.multipart.MultipartFile; 8 | 9 | public class CustomMultipartFile implements MultipartFile { 10 | private final String name; 11 | private final String contentType; 12 | private final byte[] content; 13 | 14 | public CustomMultipartFile(String name, String contentType, byte[] content) { 15 | this.name = name; 16 | this.contentType = contentType; 17 | this.content = content; 18 | } 19 | 20 | @Override 21 | public String getName() { 22 | return name; 23 | } 24 | 25 | @Override 26 | public String getOriginalFilename() { 27 | return name; 28 | } 29 | 30 | @Override 31 | public String getContentType() { 32 | return contentType; 33 | } 34 | 35 | @Override 36 | public boolean isEmpty() { 37 | return content == null || content.length == 0; 38 | } 39 | 40 | @Override 41 | public long getSize() { 42 | return content.length; 43 | } 44 | 45 | @Override 46 | public byte[] getBytes() throws IOException { 47 | return content; 48 | } 49 | 50 | @Override 51 | public InputStream getInputStream() throws IOException { 52 | return new ByteArrayInputStream(content); 53 | } 54 | 55 | @Override 56 | public void transferTo(File dest) throws IOException, IllegalStateException { 57 | throw new UnsupportedOperationException("Transfer to file not supported"); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/org/spring/framework/ai/vaadin/ui/util/ImageUtils.java: -------------------------------------------------------------------------------- 1 | package org.spring.framework.ai.vaadin.ui.util; 2 | 3 | import java.awt.RenderingHints; 4 | import java.awt.image.BufferedImage; 5 | import java.io.ByteArrayInputStream; 6 | import java.io.ByteArrayOutputStream; 7 | import java.io.IOException; 8 | import javax.imageio.ImageIO; 9 | 10 | /** Utility class for image operations. */ 11 | public class ImageUtils { 12 | 13 | /** 14 | * Creates a thumbnail from the provided image data. 15 | * 16 | * @param imageData The original image data as byte array 17 | * @param contentType The content type of the image (e.g., "image/jpeg", "image/png") 18 | * @param thumbWidth The width of the thumbnail 19 | * @param thumbHeight The height of the thumbnail 20 | * @return The thumbnail as byte array, or the original data if thumbnail creation fails 21 | */ 22 | public static byte[] createThumbnail( 23 | byte[] imageData, String contentType, int thumbWidth, int thumbHeight) { 24 | try { 25 | var bis = new ByteArrayInputStream(imageData); 26 | var originalImage = ImageIO.read(bis); 27 | 28 | // Create thumbnail image 29 | var thumbnailImage = new BufferedImage(thumbWidth, thumbHeight, BufferedImage.TYPE_INT_RGB); 30 | var g = thumbnailImage.createGraphics(); 31 | g.setRenderingHint( 32 | RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); 33 | g.drawImage(originalImage, 0, 0, thumbWidth, thumbHeight, null); 34 | g.dispose(); 35 | 36 | // Convert thumbnail to byte array 37 | var bos = new ByteArrayOutputStream(); 38 | // Extract format from contentType (e.g., "jpeg" from "image/jpeg") 39 | var format = contentType.substring(6); 40 | ImageIO.write(thumbnailImage, format, bos); 41 | return bos.toByteArray(); 42 | } catch (IOException e) { 43 | // Fallback to original image if thumbnail creation fails 44 | System.err.println("Failed to create thumbnail: " + e.getMessage()); 45 | return imageData; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/org/spring/framework/ai/vaadin/ui/view/MainView.java: -------------------------------------------------------------------------------- 1 | package org.spring.framework.ai.vaadin.ui.view; 2 | 3 | import com.vaadin.flow.component.masterdetaillayout.MasterDetailLayout; 4 | import com.vaadin.flow.component.orderedlayout.VerticalLayout; 5 | import com.vaadin.flow.router.PageTitle; 6 | import com.vaadin.flow.router.Route; 7 | import java.util.UUID; 8 | import org.spring.framework.ai.vaadin.service.Assistant; 9 | import org.spring.framework.ai.vaadin.service.Assistant.ChatOptions; 10 | import org.spring.framework.ai.vaadin.service.Assistant.Message; 11 | import org.spring.framework.ai.vaadin.service.AttachmentFile; 12 | import org.spring.framework.ai.vaadin.service.RagContextService; 13 | import org.spring.framework.ai.vaadin.ui.component.Chat; 14 | import org.spring.framework.ai.vaadin.ui.component.ChatHeader; 15 | import org.spring.framework.ai.vaadin.ui.component.ChatMessage; 16 | import org.spring.framework.ai.vaadin.ui.component.ChatMessage.ChatAttachment; 17 | import org.spring.framework.ai.vaadin.ui.component.SettingsPanel; 18 | 19 | /** 20 | * Main view for the Spring AI Assistant application. Provides a chat interface with settings panel 21 | * in a master-detail layout. 22 | */ 23 | @Route("") 24 | @PageTitle("Spring AI Assistant") 25 | public class MainView extends MasterDetailLayout { 26 | 27 | private final Chat chat; 28 | private final SettingsPanel settingsPanel; 29 | private final Assistant assistant; 30 | private String chatId; 31 | 32 | public MainView(Assistant assistant, RagContextService ragContextService) { 33 | this.assistant = assistant; 34 | this.chatId = UUID.randomUUID().toString(); 35 | 36 | addClassNames("main-layout"); 37 | setSizeFull(); 38 | 39 | // Create chat header 40 | var chatHeader = new ChatHeader(); 41 | chatHeader.addNewChatListener(e -> resetChat()); 42 | chatHeader.addToggleSettingsListener(e -> toggleSettings()); 43 | 44 | // Create settings panel 45 | this.settingsPanel = new SettingsPanel(ragContextService); 46 | settingsPanel.updateFilesList(); 47 | settingsPanel.addClassName("settings-panel"); 48 | settingsPanel.setHeightFull(); 49 | // Add close listener to settings panel 50 | settingsPanel.addCloseListener(e -> toggleSettings()); 51 | 52 | // Create chat component 53 | this.chat = new Chat(); 54 | chat.addClassName("chat-component"); 55 | chat.setSizeFull(); 56 | chat.setSubmitListener(this::handleSubmit); 57 | 58 | // Create chat layout 59 | var chatContent = new VerticalLayout(chatHeader, chat); 60 | chatContent.setPadding(false); 61 | chatContent.setSpacing(false); 62 | chatContent.setSizeFull(); 63 | 64 | setMaster(chatContent); 65 | setAnimationEnabled(false); 66 | setDetailMinSize("400px"); 67 | setDetailSize("600px"); 68 | 69 | loadChatHistory(); 70 | } 71 | 72 | /** Handles the submit event from the chat component. */ 73 | private void handleSubmit(ChatMessage userMessage, ChatMessage assistantMessage) { 74 | var options = new ChatOptions(settingsPanel.getSystemMessage(), settingsPanel.isUseMcp()); 75 | var attachmentFiles = 76 | userMessage.getAttachments().stream().map(this::chatAttachmentToAttachmentFile).toList(); 77 | 78 | var ui = getUI().get(); 79 | assistant.stream(chatId, userMessage.getText(), attachmentFiles, options) 80 | .subscribe( 81 | // Append to the assistantMessage as it streams 82 | token -> ui.access(() -> assistantMessage.appendText(token))); 83 | } 84 | 85 | /** Loads the chat history for the current chat ID. */ 86 | private void loadChatHistory() { 87 | var history = assistant.getHistory(chatId); 88 | chat.setMessages(history.stream().map(this::messageToChatMessage).toList()); 89 | } 90 | 91 | /** 92 | * Converts an Assistant Message to a ChatMessage for UI display. 93 | * 94 | * @param message The Assistant message to convert 95 | * @return The corresponding ChatMessage for UI 96 | */ 97 | private ChatMessage messageToChatMessage(Message message) { 98 | return new ChatMessage( 99 | message.role(), 100 | message.content(), 101 | message.attachments().stream() 102 | .map(a -> new ChatAttachment(a.type(), a.fileName(), null, a.url())) 103 | .toList()); 104 | } 105 | 106 | /** 107 | * Converts a ChatAttachment to an AttachmentFile. 108 | * 109 | * @param chatAttachment The chat attachment to convert 110 | * @return The corresponding AttachmentFile 111 | */ 112 | private AttachmentFile chatAttachmentToAttachmentFile(ChatAttachment chatAttachment) { 113 | return new AttachmentFile( 114 | chatAttachment.fileName(), chatAttachment.type(), chatAttachment.data()); 115 | } 116 | 117 | /** Resets the chat by closing the current session and creating a new one. */ 118 | private void resetChat() { 119 | assistant.closeChat(chatId); 120 | chatId = UUID.randomUUID().toString(); 121 | chat.clearMessages(); 122 | } 123 | 124 | /** Toggles the visibility of the settings panel. */ 125 | private void toggleSettings() { 126 | setDetail(getDetail() == null ? settingsPanel : null); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | vaadin.launch-browser=true 2 | spring.application.name=spring-ai-vaadin 3 | 4 | # OpenAI 5 | spring.ai.openai.api-key=${OPENAI_API_KEY} 6 | spring.ai.openai.chat.options.model=gpt-4o 7 | 8 | # ollama 9 | #spring.ai.openai.api-key=local 10 | #spring.ai.openai.base-url=http://localhost:11434 11 | #spring.ai.openai.chat.options.model=phi4 12 | #spring.ai.openai.embedding.api-key=local 13 | #spring.ai.openai.embedding.base-url=http://localhost:11434 14 | #spring.ai.openai.embedding.options.model=nomic-embed-text 15 | 16 | # MCP 17 | spring.ai.mcp.client.stdio.servers-configuration=classpath:/mcp-servers-config.json 18 | 19 | # File Upload Configuration 20 | spring.servlet.multipart.max-file-size=10MB 21 | spring.servlet.multipart.max-request-size=10MB -------------------------------------------------------------------------------- /src/main/resources/mcp-servers-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "vaadin": { 4 | "command": "npx", 5 | "args": ["-y", "vaadin-docs-mcp-server@latest"], 6 | "env": {} 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/resources/vaadin-featureflags.properties: -------------------------------------------------------------------------------- 1 | com.vaadin.experimental.masterDetailLayoutComponent=true -------------------------------------------------------------------------------- /src/test/java/org/spring/framework/ai/vaadin/SpringAiVaadinApplicationTests.java: -------------------------------------------------------------------------------- 1 | package org.spring.framework.ai.vaadin; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class SpringAiVaadinApplicationTests { 8 | 9 | @Test 10 | void contextLoads() {} 11 | } 12 | --------------------------------------------------------------------------------