mirror of
https://github.com/spring-projects/spring-petclinic.git
synced 2025-07-15 12:15:50 +00:00
Chatbot for Spring Petclinic.
Supports quering the owners, also guides the user through adding a pet to an owner
This commit is contained in:
parent
91328af12d
commit
290bb439d1
22 changed files with 719 additions and 8 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -15,3 +15,4 @@ build/*
|
|||
_site/
|
||||
*.css
|
||||
!petclinic.css
|
||||
**/creds.yaml
|
25
.tanzu/config/httproute.yml
Normal file
25
.tanzu/config/httproute.yml
Normal file
|
@ -0,0 +1,25 @@
|
|||
apiVersion: gateway.networking.k8s.io/v1beta1
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
name: petclinic-route
|
||||
annotations:
|
||||
healthcheck.gslb.tanzu.vmware.com/service: spring-petclinic
|
||||
healthcheck.gslb.tanzu.vmware.com/path: /
|
||||
healthcheck.gslb.tanzu.vmware.com/port: "80"
|
||||
spec:
|
||||
parentRefs:
|
||||
- group: gateway.networking.k8s.io
|
||||
kind: Gateway
|
||||
name: default-gateway
|
||||
sectionName: http-petclinic
|
||||
rules:
|
||||
- backendRefs:
|
||||
- group: ""
|
||||
kind: Service
|
||||
name: spring-petclinic
|
||||
port: 8080
|
||||
weight: 1
|
||||
matches:
|
||||
- path:
|
||||
type: PathPrefix
|
||||
value: /
|
18
.tanzu/config/spring-petclinic.yml
Normal file
18
.tanzu/config/spring-petclinic.yml
Normal file
|
@ -0,0 +1,18 @@
|
|||
apiVersion: apps.tanzu.vmware.com/v1
|
||||
kind: ContainerApp
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
name: spring-petclinic
|
||||
spec:
|
||||
nonSecretEnv:
|
||||
- name: SPRING_PROFILES_ACTIVE
|
||||
value: openai
|
||||
build:
|
||||
buildpacks: {}
|
||||
nonSecretEnv:
|
||||
- name: BP_JVM_VERSION
|
||||
value: "17"
|
||||
path: ../..
|
||||
ports:
|
||||
- name: main
|
||||
port: 8080
|
11
build.gradle
11
build.gradle
|
@ -23,12 +23,15 @@ java {
|
|||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven { url 'https://repo.spring.io/milestone' }
|
||||
}
|
||||
|
||||
ext.webjarsFontawesomeVersion = "4.7.0"
|
||||
ext.webjarsBootstrapVersion = "5.3.3"
|
||||
ext.springAiVersion = "1.0.0-M2"
|
||||
|
||||
dependencies {
|
||||
implementation 'org.springframework.ai:spring-ai-azure-openai-spring-boot-starter'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-cache'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
|
||||
|
@ -53,6 +56,12 @@ dependencies {
|
|||
checkstyle 'com.puppycrawl.tools:checkstyle:10.16.0'
|
||||
}
|
||||
|
||||
dependencyManagement {
|
||||
imports {
|
||||
mavenBom "org.springframework.ai:spring-ai-bom:${springAiVersion}"
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named('test') {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
@ -80,4 +89,4 @@ checkFormatAot.enabled = false
|
|||
checkFormatAotTest.enabled = false
|
||||
|
||||
formatAot.enabled = false
|
||||
formatAotTest.enabled = false
|
||||
formatAotTest.enabled = false
|
18
pom.xml
18
pom.xml
|
@ -34,11 +34,15 @@
|
|||
<maven-checkstyle.version>3.3.1</maven-checkstyle.version>
|
||||
<nohttp-checkstyle.version>0.0.11</nohttp-checkstyle.version>
|
||||
<spring-format.version>0.0.41</spring-format.version>
|
||||
|
||||
<spring-ai.version>1.0.0-M2</spring-ai.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<!-- Spring and Spring Boot dependencies -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-azure-openai-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
|
@ -141,6 +145,18 @@
|
|||
|
||||
</dependencies>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-bom</artifactId>
|
||||
<version>${spring-ai.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
|
|
23
readme.md
23
readme.md
|
@ -75,6 +75,29 @@ or
|
|||
```bash
|
||||
docker-compose --profile postgres up
|
||||
```
|
||||
## Integrating the Spring AI Chatbot
|
||||
|
||||
Spring Petclinic integrates a Chatbot that allows you to interact with the application in a natural language. Here are some examples of what you could ask:
|
||||
|
||||
1. Please list the owners that come to the clinic.
|
||||
2. How many vets are there?
|
||||
3. Is there an owner named Betty?
|
||||
4. Which owners have dogs?
|
||||
5. Add a dog for Betty. Its name is Moopsie.
|
||||
|
||||

|
||||
|
||||
By default, The Spring AI Chatbot is disabled and will return the message `Chat is currently unavailable. Please try again later.`.
|
||||
|
||||
Spring Petclinic currently supports OpenAI or Azure's OpenAI as the LLM provider.
|
||||
In order to enable Spring AI, perform the following steps:
|
||||
|
||||
1. Decide which provider you want to use. By default, the `spring-ai-azure-openai-spring-boot-starter` dependency is enabled. You can change it to `spring-ai-openai-spring-boot-starter`in either`pom.xml` or in `build.gradle`, depending on your build tool of choice.
|
||||
2. Copy `src/main/resources/creds-template.yaml` into `src/main/resources/creds.yaml`, and edit its contents with your API key and API endpoint. Refer to OpenAI's or Azure's documentation for further information on how to obtain these. You only need to populate the provider you're using - either openai, or azure-openai.
|
||||
3. Boot your application with the `openai` profile. This profile will work for both LLM providers. You can boot the application with that profile using any of the following:
|
||||
- For maven: `mvn -Dspring-boot.run.profiles=openai spring-boot:run`
|
||||
- For Gradle: `./gradlew bootRun --args='--spring.profiles.active=openai'`
|
||||
- For a standard jar file: `SPRING_PROFILES_ACTIVE=openai java -jar build/libs/spring-petclinic-3.3.0.jar` or `SPRING_PROFILES_ACTIVE=openai java -jar target/spring-petclinic-3.3.0-SNAPSHOT.jar`.
|
||||
|
||||
## Test Applications
|
||||
|
||||
|
|
BIN
spring-ai.png
Normal file
BIN
spring-ai.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 107 KiB |
|
@ -18,11 +18,7 @@ package org.springframework.samples.petclinic;
|
|||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.ImportRuntimeHints;
|
||||
import org.springframework.web.servlet.i18n.SessionLocaleResolver;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* PetClinic Spring Boot Application.
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
package org.springframework.samples.petclinic.genai;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Description;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.samples.petclinic.owner.Owner;
|
||||
import org.springframework.samples.petclinic.owner.Pet;
|
||||
import org.springframework.samples.petclinic.vet.Vet;
|
||||
|
||||
/**
|
||||
* This class defines the @Bean functions that the LLM provider will invoke when it
|
||||
* requires more Information on a given topic. The currently available functions enable
|
||||
* the LLM to get the list of owners and their pets, get information about the
|
||||
* veterinarians, and add a pet to an owner.
|
||||
*
|
||||
* @author Oded Shopen
|
||||
*/
|
||||
@Configuration
|
||||
@Profile("openai")
|
||||
class AIFunctionConfiguration {
|
||||
|
||||
// The @Description annotation helps the model understand when to call the function
|
||||
@Bean
|
||||
@Description("List the owners that the pet clinic has")
|
||||
public Function<OwnerRequest, OwnersResponse> listOwners(PetclinicAIProvider petclinicAiProvider) {
|
||||
return request -> {
|
||||
return petclinicAiProvider.getAllOwners();
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Description("List the veterinarians that the pet clinic has")
|
||||
public Function<VetRequest, VetResponse> listVets(PetclinicAIProvider petclinicAiProvider) {
|
||||
return request -> {
|
||||
return petclinicAiProvider.getAllVets();
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Description("Add a pet with the specified petTypeId, " + "to an owner identified by the ownerId. "
|
||||
+ "The allowed Pet types IDs are only: " + "1 - cat" + "2 - dog" + "3 - lizard" + "4 - snake" + "5 - bird"
|
||||
+ "6 - hamster")
|
||||
public Function<AddPetRequest, AddedPetResponse> addPetToOwner(PetclinicAIProvider petclinicAiProvider) {
|
||||
return request -> {
|
||||
return petclinicAiProvider.addPetToOwner(request);
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
record AddPetRequest(Pet pet, String petType, Integer ownerId) {
|
||||
};
|
||||
|
||||
record OwnerRequest(Owner owner) {
|
||||
};
|
||||
|
||||
record OwnersResponse(List<Owner> owners) {
|
||||
};
|
||||
|
||||
record AddedPetResponse(Owner owner) {
|
||||
};
|
||||
|
||||
record VetResponse(List<Vet> vet) {
|
||||
};
|
||||
|
||||
record VetRequest(Vet vet) {
|
||||
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package org.springframework.samples.petclinic.genai;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.ai.chat.client.AdvisedRequest;
|
||||
import org.springframework.ai.chat.client.RequestResponseAdvisor;
|
||||
|
||||
/**
|
||||
* A ChatClient Advisor that adds logs on the requests being sent to the LLM.
|
||||
*
|
||||
* @author Oded Shopen
|
||||
*/
|
||||
public class LoggingAdvisor implements RequestResponseAdvisor {
|
||||
|
||||
private static final Log log = LogFactory.getLog(LoggingAdvisor.class);
|
||||
|
||||
@Override
|
||||
public AdvisedRequest adviseRequest(AdvisedRequest request, Map<String, Object> context) {
|
||||
log.info("Received Spring AI Request: " + request);
|
||||
return request;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package org.springframework.samples.petclinic.genai;
|
||||
|
||||
import org.springframework.ai.chat.memory.ChatMemory;
|
||||
import org.springframework.ai.chat.memory.InMemoryChatMemory;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
/**
|
||||
* A Configuration class for beans used by the Chat Client.
|
||||
*
|
||||
* @author Oded Shopen
|
||||
*/
|
||||
@Configuration
|
||||
@Profile("openai")
|
||||
public class PetclinicAIConfiguration {
|
||||
|
||||
@Bean
|
||||
public RestClient restClient() {
|
||||
return RestClient.create();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ChatMemory chatMemory() {
|
||||
return new InMemoryChatMemory();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package org.springframework.samples.petclinic.genai;
|
||||
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.samples.petclinic.owner.Owner;
|
||||
import org.springframework.samples.petclinic.owner.OwnerRepository;
|
||||
import org.springframework.samples.petclinic.vet.Vet;
|
||||
import org.springframework.samples.petclinic.vet.VetRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* Functions that are invoked by the LLM will use this bean to query the system of record
|
||||
* for information such as listing owners and vers, or adding pets to an owner.
|
||||
*
|
||||
* @author Oded Shopen
|
||||
*/
|
||||
@Service
|
||||
@Profile("openai")
|
||||
public class PetclinicAIProvider {
|
||||
|
||||
OwnerRepository ownerRepository;
|
||||
|
||||
VetRepository vetRepository;
|
||||
|
||||
public PetclinicAIProvider(OwnerRepository ownerRepository, VetRepository vetRepository) {
|
||||
this.ownerRepository = ownerRepository;
|
||||
this.vetRepository = vetRepository;
|
||||
}
|
||||
|
||||
public OwnersResponse getAllOwners() {
|
||||
Pageable pageable = PageRequest.of(0, Integer.MAX_VALUE);
|
||||
Page<Owner> ownerPage = ownerRepository.findAll(pageable);
|
||||
return new OwnersResponse(ownerPage.getContent());
|
||||
}
|
||||
|
||||
public VetResponse getAllVets() {
|
||||
Pageable pageable = PageRequest.of(0, Integer.MAX_VALUE);
|
||||
Page<Vet> vetsPage = vetRepository.findAll(pageable);
|
||||
return new VetResponse(vetsPage.getContent());
|
||||
}
|
||||
|
||||
public AddedPetResponse addPetToOwner(AddPetRequest request) {
|
||||
Owner owner = ownerRepository.findById(request.ownerId());
|
||||
owner.addPet(request.pet());
|
||||
this.ownerRepository.save(owner);
|
||||
return new AddedPetResponse(owner);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
package org.springframework.samples.petclinic.genai;
|
||||
|
||||
import static org.springframework.ai.chat.client.advisor.AbstractChatMemoryAdvisor.DEFAULT_CHAT_MEMORY_CONVERSATION_ID;
|
||||
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
|
||||
import org.springframework.ai.chat.memory.ChatMemory;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* This REST controller is being invoked by the in order to interact with the LLM
|
||||
*
|
||||
* @author Oded Shopen
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/")
|
||||
@Profile("openai")
|
||||
public class PetclinicChatClient {
|
||||
|
||||
// ChatModel is the primary interfaces for interacting with an LLM
|
||||
// it is a request/response interface that implements the ModelModel
|
||||
// interface. Make suer to visit the source code of the ChatModel and
|
||||
// checkout the interfaces in the core spring ai package.
|
||||
private final ChatClient chatClient;
|
||||
|
||||
public PetclinicChatClient(ChatClient.Builder builder, ChatMemory chatMemory) {
|
||||
// @formatter:off
|
||||
this.chatClient = builder
|
||||
.defaultSystem("""
|
||||
You are a friendly AI assistant designed to help with the management of a veterinarian pet clinic called Spring Petclinic.
|
||||
Your job is to answer questions about the existing veterinarians and to perform actions on the customer's behalf, mainly around
|
||||
pet owners, their pets and their visits.
|
||||
You are required to answer an a professional manner. If you don't know the answer, politely tell the customer
|
||||
you don't know the answer, then ask the customer a followup qusetion to try and clarify the question they are asking.
|
||||
If you do know the answer, provide the answer but do not provide any additional helpful followup questions.
|
||||
""")
|
||||
.defaultAdvisors(
|
||||
// Chat memory helps us keep context when using the chatbot for up to 10 previous messages.
|
||||
new MessageChatMemoryAdvisor(chatMemory, DEFAULT_CHAT_MEMORY_CONVERSATION_ID, 10), // CHAT MEMORY
|
||||
new LoggingAdvisor()
|
||||
)
|
||||
.build();
|
||||
}
|
||||
|
||||
@PostMapping("/chatclient")
|
||||
public String exchange(@RequestBody String query) {
|
||||
//All chatbot messages go through this endpoint and are passed to the LLM
|
||||
return
|
||||
this.chatClient
|
||||
.prompt()
|
||||
.user(
|
||||
u ->
|
||||
u.text(query)
|
||||
)
|
||||
.call()
|
||||
.content();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package org.springframework.samples.petclinic.genai;
|
||||
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* This REST controller implements a default behavior for the chat client when AI profile
|
||||
* is not in use. It will return a default message that chat is not available.
|
||||
*
|
||||
* @author Oded Shopen
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/")
|
||||
@Profile("!openai")
|
||||
public class PetclinicDisabledChatClient {
|
||||
|
||||
@PostMapping("/chatclient")
|
||||
public String exchange(@RequestBody String query) {
|
||||
return "Chat is currently unavailable. Please try again later.";
|
||||
}
|
||||
|
||||
}
|
15
src/main/resources/application-openai.properties
Normal file
15
src/main/resources/application-openai.properties
Normal file
|
@ -0,0 +1,15 @@
|
|||
spring.config.import=optional:classpath:/creds.yaml
|
||||
|
||||
#These apply when using spring-ai-azure-openai-spring-boot-starter
|
||||
spring.ai.azure.openai.chat.options.functions=listOwners,listVets,addPetToOwner
|
||||
spring.ai.azure.openai.chat.options.temperature: 0.7
|
||||
|
||||
#These apply when using spring-ai-openai-spring-boot-starter
|
||||
spring.ai.openai.chat.options.functions=listOwners,listVets,addPetToOwner
|
||||
spring.ai.openai.chat.options.temperature: 0.7
|
||||
|
||||
#Enable Spring AI by default
|
||||
spring.ai.chat.client.enabled=true
|
||||
|
||||
|
||||
|
|
@ -23,3 +23,9 @@ logging.level.org.springframework=INFO
|
|||
|
||||
# Maximum time static resources should be cached
|
||||
spring.web.resources.cache.cachecontrol.max-age=12h
|
||||
|
||||
#Disable Spring AI by default
|
||||
spring.ai.chat.client.enabled=false
|
||||
#Currently these properties require dummy values when a spring AI is in the classpath, even when chat is disabled.
|
||||
spring.ai.azure.openai.api-key=dummy
|
||||
spring.ai.azure.openai.endpoint=dummy
|
||||
|
|
17
src/main/resources/creds-template.yaml
Normal file
17
src/main/resources/creds-template.yaml
Normal file
|
@ -0,0 +1,17 @@
|
|||
spring:
|
||||
ai:
|
||||
#These parameters only apply when using the spring-ai-azure-openai-spring-boot-starter dependency:
|
||||
azure:
|
||||
openai:
|
||||
api-key: ""
|
||||
endpoint: ""
|
||||
chat:
|
||||
options:
|
||||
deployment-name: "gpt-4o"
|
||||
#These parameters only apply when using the spring-ai-openai-spring-boot-starter dependency:
|
||||
openai:
|
||||
api-key: ""
|
||||
endpoint: ""
|
||||
chat:
|
||||
options:
|
||||
deployment-name: "gpt-4o"
|
|
@ -9517,4 +9517,118 @@ strong {
|
|||
margin-top: 10px;
|
||||
margin-bottom: 30px; } }
|
||||
|
||||
/*# sourceMappingURL=../../../../../../target/petclinic.css.map */
|
||||
/*# sourceMappingURL=../../../../../../target/petclinic.css.map */
|
||||
|
||||
|
||||
|
||||
/* Chatbox container */
|
||||
.chatbox {
|
||||
position: fixed;
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
width: 300px;
|
||||
background-color: #f1f1f1;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Header styling */
|
||||
.chatbox-header {
|
||||
background-color: #075E54;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
border-top-left-radius: 10px;
|
||||
border-top-right-radius: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Chatbox content styling */
|
||||
.chatbox-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 400px; /* Adjust to desired height */
|
||||
overflow: hidden; /* Hide overflow to make it scrollable */
|
||||
}
|
||||
|
||||
/* Minimize style */
|
||||
.chatbox.minimized .chatbox-content {
|
||||
height: 40px; /* Height when minimized (header only) */
|
||||
}
|
||||
|
||||
.chatbox.minimized .chatbox-messages,
|
||||
.chatbox.minimized .chatbox-footer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.chatbox-messages {
|
||||
flex-grow: 1;
|
||||
overflow-y: auto; /* Allows vertical scrolling */
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
/* Chat bubbles styling */
|
||||
.chat-bubble {
|
||||
max-width: 80%;
|
||||
padding: 10px;
|
||||
border-radius: 20px;
|
||||
margin-bottom: 10px;
|
||||
position: relative;
|
||||
word-wrap: break-word;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Ensure bold and italic styles are handled */
|
||||
.chat-bubble strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.chat-bubble em {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.chat-bubble.user {
|
||||
background-color: #dcf8c6; /* WhatsApp-style light green */
|
||||
margin-left: auto;
|
||||
text-align: right;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.chat-bubble.bot {
|
||||
background-color: #ffffff;
|
||||
margin-right: auto;
|
||||
text-align: left;
|
||||
border-bottom-left-radius: 0;
|
||||
border: 1px solid #e1e1e1;
|
||||
}
|
||||
|
||||
/* Input field and button */
|
||||
.chatbox-footer {
|
||||
padding: 10px;
|
||||
background-color: #f9f9f9;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.chatbox-footer input {
|
||||
flex-grow: 1;
|
||||
padding: 10px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid #ccc;
|
||||
margin-right: 10px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.chatbox-footer button {
|
||||
background-color: #075E54;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.chatbox-footer button:hover {
|
||||
background-color: #128C7E;
|
||||
}
|
||||
|
|
|
@ -70,6 +70,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="container xd-container">
|
||||
|
||||
|
@ -87,7 +88,118 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<script th:src="@{/webjars/bootstrap/5.3.3/dist/js/bootstrap.bundle.min.js}"></script>
|
||||
|
||||
<div class="chatbox" id="chatbox">
|
||||
<div class="chatbox-header" onclick="toggleChatbox()">
|
||||
Chat with Us!
|
||||
</div>
|
||||
<div class="chatbox-content" id="chatbox-content">
|
||||
<div class="chatbox-messages" id="chatbox-messages">
|
||||
<!-- Chat messages will be dynamically inserted here -->
|
||||
</div>
|
||||
<div class="chatbox-footer">
|
||||
<input type="text" id="chatbox-input" placeholder="Type a message..." onkeydown="handleKeyPress(event)" />
|
||||
<button onclick="sendMessage()">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- JavaScript for handling chatbox interaction -->
|
||||
<script>
|
||||
function appendMessage(message, type) {
|
||||
const chatMessages = document.getElementById('chatbox-messages');
|
||||
const messageElement = document.createElement('div');
|
||||
messageElement.classList.add('chat-bubble', type);
|
||||
|
||||
// Convert Markdown to HTML
|
||||
const htmlContent = marked.parse(message); // Use marked.parse() for newer versions
|
||||
messageElement.innerHTML = htmlContent;
|
||||
|
||||
chatMessages.appendChild(messageElement);
|
||||
|
||||
// Scroll to the bottom of the chatbox to show the latest message
|
||||
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||||
}
|
||||
|
||||
function toggleChatbox() {
|
||||
const chatbox = document.getElementById('chatbox');
|
||||
const chatboxContent = document.getElementById('chatbox-content');
|
||||
|
||||
if (chatbox.classList.contains('minimized')) {
|
||||
chatbox.classList.remove('minimized');
|
||||
chatboxContent.style.height = '400px'; // Set to initial height when expanded
|
||||
} else {
|
||||
chatbox.classList.add('minimized');
|
||||
chatboxContent.style.height = '40px'; // Set to minimized height
|
||||
}
|
||||
}
|
||||
|
||||
function sendMessage() {
|
||||
const query = document.getElementById('chatbox-input').value;
|
||||
|
||||
// Only send if there's a message
|
||||
if (!query.trim()) return;
|
||||
|
||||
// Clear the input field after sending the message
|
||||
document.getElementById('chatbox-input').value = '';
|
||||
|
||||
// Display user message in the chatbox
|
||||
appendMessage(query, 'user');
|
||||
|
||||
// Send the message to the backend
|
||||
fetch('/chatclient', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(query),
|
||||
})
|
||||
.then(response => response.text())
|
||||
.then(responseText => {
|
||||
// Display the response from the server in the chatbox
|
||||
appendMessage(responseText, 'bot');
|
||||
})
|
||||
.catch(error => console.error('Error:', error));
|
||||
}
|
||||
|
||||
function handleKeyPress(event) {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault(); // Prevents adding a newline
|
||||
sendMessage(); // Send the message when Enter is pressed
|
||||
}
|
||||
}
|
||||
|
||||
// Save chat messages to localStorage
|
||||
function saveChatMessages() {
|
||||
const messages = document.getElementById('chatbox-messages').innerHTML;
|
||||
localStorage.setItem('chatMessages', messages);
|
||||
}
|
||||
|
||||
// Load chat messages from localStorage
|
||||
function loadChatMessages() {
|
||||
const messages = localStorage.getItem('chatMessages');
|
||||
if (messages) {
|
||||
document.getElementById('chatbox-messages').innerHTML = messages;
|
||||
document.getElementById('chatbox-messages').scrollTop = document.getElementById('chatbox-messages').scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
// Call loadChatMessages when the page loads
|
||||
window.onload = loadChatMessages;
|
||||
|
||||
// Ensure messages are saved when navigating away
|
||||
window.onbeforeunload = saveChatMessages;
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<script th:src="@{/webjars/bootstrap/5.3.3/dist/js/bootstrap.bundle.min.js}"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
|
||||
|
||||
</body>
|
||||
|
||||
|
|
88
src/main/scss/chatbot.scss
Normal file
88
src/main/scss/chatbot.scss
Normal file
|
@ -0,0 +1,88 @@
|
|||
.chatbox {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 300px;
|
||||
border-radius: 10px 10px 0 0;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #f0f0f0;
|
||||
overflow: hidden;
|
||||
|
||||
&.minimized {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
&-header {
|
||||
background-color: $spring-green;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&-content {
|
||||
height: 400px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
background-color: white;
|
||||
transition: height 0.3s ease;
|
||||
}
|
||||
|
||||
&-messages {
|
||||
padding: 10px;
|
||||
overflow-y: auto;
|
||||
height: 90%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
|
||||
.chat-bubble {
|
||||
padding: 10px;
|
||||
border-radius: 15px;
|
||||
max-width: 70%;
|
||||
word-wrap: break-word;
|
||||
|
||||
&.user {
|
||||
background-color: $spring-brown;
|
||||
color: white;
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
&.bot {
|
||||
background-color: $spring-grey;
|
||||
color: black;
|
||||
align-self: flex-start;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-footer {
|
||||
display: flex;
|
||||
padding: 10px;
|
||||
background-color: $spring-light-grey;
|
||||
|
||||
input {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid $spring-brown;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-left: 10px;
|
||||
padding: 10px 20px;
|
||||
background-color: $spring-green;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: darken($spring-green, 10%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -212,3 +212,4 @@ hr {
|
|||
@import "typography.scss";
|
||||
@import "header.scss";
|
||||
@import "responsive.scss";
|
||||
@import "chatbot.scss";
|
6
tanzu.yml
Normal file
6
tanzu.yml
Normal file
|
@ -0,0 +1,6 @@
|
|||
apiVersion: config.tanzu.vmware.com/v1
|
||||
configuration:
|
||||
dev:
|
||||
paths:
|
||||
- .tanzu/config/
|
||||
kind: TanzuConfig
|
Loading…
Reference in a new issue