Skip to content

Commit 984f525

Browse files
haoozhangHao Zhang
andauthored
Integrate chat agent web interaction to api-gateway UI (#80)
## Purpose + Update gateway frontend to add chat-agent part. + Reactive web socket server in api-gateway. + Call by name for chat agent. + Spring cloud gateway route config for chat agent. + Make chat agent as Eureka client, because it will be called by spring cloud gateway, which underlying uses load balancing. ![image](https://github.com/user-attachments/assets/324242bd-ef2d-4ecc-b11c-49a0659f3288) ## Does this introduce a breaking change? <!-- Mark one with an "x". --> ``` [ ] Yes [ ] No ``` ## Pull Request Type What kind of change does this Pull Request introduce? <!-- Please check the one that applies to this PR using "x". --> ``` [ ] Bugfix [ ] Feature [ ] Code style update (formatting, local variables) [ ] Refactoring (no functional changes, no api changes) [ ] Documentation content changes [ ] Other... Please describe: ``` ## How to Test * Get the code ``` git clone [repo-address] cd [repo-name] git checkout [branch-name] npm install ``` * Test the code <!-- Add steps to run the tests suite and/or manually test --> ``` ``` ## What to Check Verify that the following are valid * ... ## Other Information <!-- Add any other helpful information that may be needed here. --> --------- Co-authored-by: Hao Zhang <[email protected]>
1 parent 02eaf71 commit 984f525

File tree

17 files changed

+551
-4
lines changed

17 files changed

+551
-4
lines changed

src/spring-petclinic-api-gateway/pom.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@
9090
<groupId>io.opentelemetry</groupId>
9191
<artifactId>opentelemetry-exporter-zipkin</artifactId>
9292
</dependency>
93+
<dependency>
94+
<groupId>com.fasterxml.jackson.core</groupId>
95+
<artifactId>jackson-databind</artifactId>
96+
</dependency>
9397

9498
<!-- Webjars -->
9599
<dependency>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.samples.petclinic.api.application;
17+
18+
import lombok.RequiredArgsConstructor;
19+
import org.springframework.stereotype.Component;
20+
import org.springframework.web.reactive.function.client.WebClient;
21+
import reactor.core.publisher.Mono;
22+
23+
@Component
24+
@RequiredArgsConstructor
25+
public class ChatServiceClient {
26+
27+
private final WebClient.Builder webClientBuilder;
28+
29+
public Mono<String> chat(String sender, String message) {
30+
return webClientBuilder.build().get()
31+
.uri("http://chat-agent/chat/{sender}/{message}", sender, message)
32+
.retrieve()
33+
.bodyToMono(String.class);
34+
}
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.samples.petclinic.api.dto;
17+
18+
import lombok.*;
19+
20+
/**
21+
* Represents a chat message in the chat application.
22+
*/
23+
@Getter
24+
@Setter
25+
@AllArgsConstructor
26+
@NoArgsConstructor
27+
@Builder
28+
public class ChatMessage {
29+
30+
private String content;
31+
32+
private String sender;
33+
34+
private MessageType type;
35+
36+
/**
37+
* Enum representing the type of the chat message.
38+
*/
39+
public enum MessageType {
40+
41+
CHAT, LEAVE, JOIN
42+
43+
}
44+
45+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.samples.petclinic.api.websocket;
17+
18+
import com.fasterxml.jackson.databind.ObjectMapper;
19+
import org.slf4j.Logger;
20+
import org.slf4j.LoggerFactory;
21+
import org.springframework.beans.factory.annotation.Autowired;
22+
import org.springframework.beans.factory.annotation.Value;
23+
import org.springframework.samples.petclinic.api.application.ChatServiceClient;
24+
import org.springframework.samples.petclinic.api.dto.ChatMessage;
25+
import org.springframework.stereotype.Component;
26+
import org.springframework.web.reactive.socket.WebSocketHandler;
27+
import org.springframework.web.reactive.socket.WebSocketMessage;
28+
import org.springframework.web.reactive.socket.WebSocketSession;
29+
import reactor.core.publisher.Mono;
30+
31+
@Component
32+
public class ChatWebSocketHandler implements WebSocketHandler {
33+
34+
private final Logger logger = LoggerFactory.getLogger(ChatWebSocketHandler.class);
35+
36+
@Autowired
37+
private ChatServiceClient chatServiceClient;
38+
39+
@Value("${petclinic.agent.name:petclinic agent}")
40+
private String agentName;
41+
42+
@Override
43+
public Mono<Void> handle(WebSocketSession session) {
44+
return session.send(
45+
session.receive()
46+
.map(WebSocketMessage::getPayloadAsText)
47+
.flatMap(
48+
payload -> processMessage(session, payload)
49+
)
50+
);
51+
}
52+
53+
private Mono<WebSocketMessage> processMessage(WebSocketSession session, String payload) {
54+
logger.info("received payload: {}", payload);
55+
56+
var objectMapper = new ObjectMapper();
57+
ChatMessage receivedMessage = null;
58+
try {
59+
receivedMessage = objectMapper.readValue(payload, ChatMessage.class);
60+
} catch (Exception e) {
61+
logger.error("Exception thrown when deserializing client chat message: {}", payload);
62+
return Mono.empty();
63+
}
64+
65+
if (receivedMessage.getType() == ChatMessage.MessageType.JOIN) {
66+
return Mono.just(session.textMessage(payload));
67+
} else if (receivedMessage.getType() == ChatMessage.MessageType.CHAT) {
68+
return chatServiceClient.chat(receivedMessage.getSender(), receivedMessage.getContent())
69+
.flatMap(responseContent -> {
70+
ChatMessage responseMessage = new ChatMessage();
71+
responseMessage.setContent(responseContent);
72+
responseMessage.setSender(agentName);
73+
74+
String responsePayload = null;
75+
try {
76+
responsePayload = objectMapper.writeValueAsString(responseMessage);
77+
} catch (Exception e) {
78+
logger.error("Exception thrown when serializing CHAT message: {}", responseMessage);
79+
return Mono.empty();
80+
}
81+
82+
logger.info("response payload: {}", responsePayload);
83+
return Mono.just(session.textMessage(responsePayload));
84+
});
85+
}
86+
87+
return Mono.empty();
88+
}
89+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.samples.petclinic.api.websocket;
17+
18+
import org.springframework.context.annotation.Bean;
19+
import org.springframework.context.annotation.Configuration;
20+
import org.springframework.web.reactive.HandlerMapping;
21+
import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping;
22+
import org.springframework.web.reactive.socket.WebSocketHandler;
23+
import org.springframework.web.reactive.socket.server.support.WebSocketHandlerAdapter;
24+
25+
import java.util.HashMap;
26+
import java.util.Map;
27+
28+
@Configuration
29+
public class WebSocketConfig {
30+
31+
@Bean
32+
public HandlerMapping webSocketMapping(ChatWebSocketHandler webSocketHandler) {
33+
Map<String, WebSocketHandler> map = new HashMap<>();
34+
map.put("/websocket", webSocketHandler);
35+
36+
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
37+
mapping.setUrlMap(map);
38+
mapping.setOrder(-1); // give it higher precedence
39+
return mapping;
40+
}
41+
42+
// This bean is necessary for WebSocket support in WebFlux
43+
@Bean
44+
public WebSocketHandlerAdapter handlerAdapter() {
45+
return new WebSocketHandlerAdapter();
46+
}
47+
}

src/spring-petclinic-api-gateway/src/main/less/petclinic.less

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,74 @@
8484
}
8585
}
8686

87+
.form-control {
88+
display: block;
89+
width: 100%;
90+
padding: 0.375rem 0.75rem;
91+
font-size: 1rem;
92+
font-weight: 400;
93+
line-height: 1.5;
94+
appearance: none;
95+
background-clip: padding-box;
96+
}
97+
98+
.no-marker {
99+
list-style-type: none;
100+
}
101+
102+
#chat-page {
103+
.event-message {
104+
width: 100%;
105+
text-align: center;
106+
clear: both;
107+
108+
p {
109+
font-size: 14px;
110+
word-wrap: break-word;
111+
}
112+
}
113+
114+
.own-message {
115+
text-align: right;
116+
117+
i {
118+
visibility: hidden !important;
119+
}
120+
}
121+
}
122+
123+
.input-group {
124+
position: relative;
125+
display: flex;
126+
flex-wrap: wrap;
127+
align-items: stretch;
128+
width: 100%;
129+
130+
> .form-control,
131+
> .form-select,
132+
> .form-floating {
133+
position: relative;
134+
flex: 1 1 auto;
135+
width: 1%;
136+
min-width: 0;
137+
}
138+
139+
> .form-control:focus,
140+
> .form-select:focus,
141+
> .form-floating:focus-within {
142+
z-index: 5;
143+
}
144+
145+
.btn {
146+
position: relative;
147+
z-index: 2;
148+
149+
&:focus {
150+
z-index: 5;
151+
}
152+
}
153+
}
154+
87155

88156
.container .text-muted {
89157
margin: 20px 0;

src/spring-petclinic-api-gateway/src/main/resources/application.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ spring:
2424
- Path=/api/customer/**
2525
filters:
2626
- StripPrefix=2
27+
- id: chat-agent
28+
uri: lb://chat-agent
29+
predicates:
30+
- Path=/api/chat/**
31+
filters:
32+
- StripPrefix=2
2733

2834

2935

src/spring-petclinic-api-gateway/src/main/resources/static/index.html

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,16 +47,18 @@
4747
<script src="/scripts/vet-list/vet-list.controller.js"></script>
4848
<script src="/scripts/vet-list/vet-list.component.js"></script>
4949

50+
<script src="/scripts/agent/agent.js"></script>
51+
<script src="/scripts/agent/agent.component.js"></script>
52+
5053
<script src="/scripts/infrastructure/infrastructure.js"></script>
5154
<script src="/scripts/infrastructure/httpErrorHandlingInterceptor.js"></script>
5255
</head>
5356

5457
<body class="container">
5558
<layout-nav></layout-nav>
5659
<div class="container-fluid">
57-
<div class="container xd-container">
58-
<div ui-view=""></div>
59-
</div>
60+
<div class="row" style="height: 30px;"></div>
61+
<div ui-view=""></div>
6062
</div>
6163
<layout-footer></layout-footer>
6264
</body>

0 commit comments

Comments
 (0)