20250719 spring-ai搭建
This commit is contained in:
commit
0ee73fd3de
190
README.md
Normal file
190
README.md
Normal file
@ -0,0 +1,190 @@
|
||||
# Spring AI 聊天演示项目
|
||||
|
||||
这是一个基于 Spring AI 的简单聊天应用演示项目,展示了如何与大语言模型进行对话。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- 🤖 支持多种大语言模型(OpenAI、Ollama)
|
||||
- 💬 简洁美观的聊天界面
|
||||
- 🔧 简单的配置和部署
|
||||
- 📱 响应式设计,支持移动端
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **Spring Boot 3.2.0** - 应用框架
|
||||
- **Spring AI 0.8.1** - AI 集成框架
|
||||
- **Thymeleaf** - 模板引擎
|
||||
- **HTML/CSS/JavaScript** - 前端界面
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 环境要求
|
||||
|
||||
- Java 17 或更高版本
|
||||
- Maven 3.6 或更高版本
|
||||
|
||||
### 2. 配置大语言模型
|
||||
|
||||
#### 使用 OpenAI(推荐)
|
||||
|
||||
在 `src/main/resources/application.yml` 中配置:
|
||||
|
||||
```yaml
|
||||
spring:
|
||||
ai:
|
||||
openai:
|
||||
api-key: your-openai-api-key-here
|
||||
base-url: https://api.openai.com # 可选,默认值
|
||||
```
|
||||
|
||||
或者通过环境变量设置:
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY=your-openai-api-key-here
|
||||
```
|
||||
|
||||
#### 使用 Ollama(本地部署)
|
||||
|
||||
1. 首先安装并启动 Ollama:
|
||||
```bash
|
||||
# 安装 Ollama
|
||||
curl -fsSL https://ollama.ai/install.sh | sh
|
||||
|
||||
# 拉取模型(例如 llama2)
|
||||
ollama pull llama2
|
||||
|
||||
# 启动 Ollama 服务
|
||||
ollama serve
|
||||
```
|
||||
|
||||
2. 在配置文件中启用 Ollama:
|
||||
```yaml
|
||||
spring:
|
||||
ai:
|
||||
ollama:
|
||||
base-url: http://localhost:11434
|
||||
chat:
|
||||
options:
|
||||
model: llama2
|
||||
```
|
||||
|
||||
### 3. 运行应用
|
||||
|
||||
```bash
|
||||
# 编译项目
|
||||
mvn clean compile
|
||||
|
||||
# 运行应用
|
||||
mvn spring-boot:run
|
||||
```
|
||||
|
||||
应用启动后,访问 http://localhost:8080 即可使用聊天界面。
|
||||
|
||||
### 4. API 接口
|
||||
|
||||
除了网页界面,还提供了简单的 API 接口:
|
||||
|
||||
```bash
|
||||
# 简单对话接口
|
||||
curl "http://localhost:8080/simple?q=你好"
|
||||
|
||||
# POST 接口
|
||||
curl -X POST http://localhost:8080/chat \
|
||||
-H "Content-Type: application/x-www-form-urlencoded" \
|
||||
-d "message=介绍一下 Spring AI"
|
||||
```
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── main/
|
||||
│ ├── java/com/example/springaidemo/
|
||||
│ │ ├── SpringAiDemoApplication.java # 应用主类
|
||||
│ │ ├── config/
|
||||
│ │ │ └── AiConfig.java # AI 配置类
|
||||
│ │ └── controller/
|
||||
│ │ └── ChatController.java # 聊天控制器
|
||||
│ └── resources/
|
||||
│ ├── application.yml # 应用配置
|
||||
│ └── templates/
|
||||
│ └── chat.html # 聊天界面模板
|
||||
```
|
||||
|
||||
## 配置说明
|
||||
|
||||
### OpenAI 配置
|
||||
|
||||
```yaml
|
||||
spring:
|
||||
ai:
|
||||
openai:
|
||||
api-key: ${OPENAI_API_KEY:your-openai-api-key} # OpenAI API Key
|
||||
base-url: ${OPENAI_BASE_URL:https://api.openai.com} # API 基础 URL
|
||||
chat:
|
||||
options:
|
||||
model: gpt-3.5-turbo # 使用的模型
|
||||
temperature: 0.7 # 温度参数(0-1)
|
||||
```
|
||||
|
||||
### Ollama 配置
|
||||
|
||||
```yaml
|
||||
spring:
|
||||
ai:
|
||||
ollama:
|
||||
base-url: ${OLLAMA_BASE_URL:http://localhost:11434} # Ollama 服务地址
|
||||
chat:
|
||||
options:
|
||||
model: ${OLLAMA_MODEL:llama2} # 使用的模型
|
||||
```
|
||||
|
||||
## 自定义和扩展
|
||||
|
||||
### 添加新的对话端点
|
||||
|
||||
在 `ChatController` 中添加新的方法:
|
||||
|
||||
```java
|
||||
@GetMapping("/custom")
|
||||
@ResponseBody
|
||||
public String customChat(@RequestParam("message") String message) {
|
||||
// 自定义处理逻辑
|
||||
String prompt = "请用专业的语言回答:" + message;
|
||||
return chatClient.call(prompt);
|
||||
}
|
||||
```
|
||||
|
||||
### 修改聊天界面
|
||||
|
||||
编辑 `src/main/resources/templates/chat.html` 文件来自定义界面样式和功能。
|
||||
|
||||
### 添加更多 AI 提供商
|
||||
|
||||
Spring AI 支持多种 AI 提供商,包括:
|
||||
- OpenAI
|
||||
- Azure OpenAI
|
||||
- Hugging Face
|
||||
- Ollama
|
||||
- Anthropic Claude
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 1. OpenAI API Key 错误
|
||||
- 确保 API Key 正确设置
|
||||
- 检查 API Key 是否有足够的配额
|
||||
- 验证网络连接是否正常
|
||||
|
||||
### 2. Ollama 连接失败
|
||||
- 确保 Ollama 服务正在运行
|
||||
- 检查端口 11434 是否可访问
|
||||
- 确认模型已正确下载
|
||||
|
||||
### 3. 应用启动失败
|
||||
- 检查 Java 版本是否为 17 或更高
|
||||
- 确保网络连接正常
|
||||
- 查看控制台错误日志
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT License
|
140
pom.xml
Normal file
140
pom.xml
Normal file
@ -0,0 +1,140 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.2.5</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<groupId>com.example</groupId>
|
||||
<artifactId>spring-ai-demo</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<name>spring-ai-demo</name>
|
||||
<description>Spring AI Demo Project</description>
|
||||
|
||||
<properties>
|
||||
<java.version>17</java.version>
|
||||
<spring-ai.version>0.8.1</spring-ai.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<!-- Spring Boot Starter Web -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Starter Thymeleaf (用于前端模板) -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-starter-model-openai</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-starter-model-deepseek</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-starter-vector-store-elasticsearch</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>co.elastic.clients</groupId>
|
||||
<artifactId>elasticsearch-java</artifactId>
|
||||
<version>8.13.3</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
<!-- Spring Boot DevTools (开发工具) -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-devtools</artifactId>
|
||||
<scope>runtime</scope>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Test -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-all</artifactId>
|
||||
<version>5.8.25</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-bom</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.8.1</version>
|
||||
<configuration>
|
||||
<source>17</source>
|
||||
<target>17</target>
|
||||
<encoding>UTF-8</encoding>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>spring-snapshots</id>
|
||||
<name>Spring Snapshots</name>
|
||||
<url>https://repo.spring.io/snapshot</url>
|
||||
<releases>
|
||||
<enabled>false</enabled>
|
||||
</releases>
|
||||
</repository>
|
||||
<repository>
|
||||
<name>Central Portal Snapshots</name>
|
||||
<id>central-portal-snapshots</id>
|
||||
<url>https://central.sonatype.com/repository/maven-snapshots/</url>
|
||||
<releases>
|
||||
<enabled>false</enabled>
|
||||
</releases>
|
||||
<snapshots>
|
||||
<enabled>true</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
</project>
|
@ -0,0 +1,18 @@
|
||||
package com.example.springaidemo;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class SpringAiDemoApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
String proxy = "127.0.0.1";
|
||||
int port = 7897;
|
||||
System.setProperty("proxyType", "4");
|
||||
System.setProperty("proxyPort", String.valueOf(port));
|
||||
System.setProperty("proxyHost", proxy);
|
||||
System.setProperty("proxySet", "true");
|
||||
SpringApplication.run(SpringAiDemoApplication.class, args);
|
||||
}
|
||||
}
|
11
src/main/java/com/example/springaidemo/bean/ChatRequest.java
Normal file
11
src/main/java/com/example/springaidemo/bean/ChatRequest.java
Normal file
@ -0,0 +1,11 @@
|
||||
package com.example.springaidemo.bean;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class ChatRequest {
|
||||
|
||||
private String message;
|
||||
|
||||
private String sessionId;
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
package com.example.springaidemo.controller;
|
||||
|
||||
|
||||
import com.example.springaidemo.bean.ChatRequest;
|
||||
import com.example.springaidemo.service.DeepseekChatService;
|
||||
import org.springframework.ai.chat.client.ChatClientRequest;
|
||||
import org.springframework.ai.chat.model.ChatResponse;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/chatController")
|
||||
public class ChatController {
|
||||
|
||||
@Autowired
|
||||
private DeepseekChatService deepseekChatService;
|
||||
|
||||
@GetMapping("/ai/chat")
|
||||
public Map generate(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
|
||||
return deepseekChatService.chat(message);
|
||||
}
|
||||
|
||||
@GetMapping("/ai/generateStream")
|
||||
public Flux<ChatResponse> generateStream(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
|
||||
return deepseekChatService.streamChat(message);
|
||||
}
|
||||
|
||||
@PostMapping(value = "/ai/sseChat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||
public SseEmitter streamChat(@RequestBody ChatRequest chatRequest) {
|
||||
return deepseekChatService.sseChat(chatRequest.getMessage());
|
||||
}
|
||||
|
||||
@PostMapping(value = "/ai/thinkChat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||
public SseEmitter thinkChat(@RequestBody ChatRequest chatRequest) {
|
||||
return deepseekChatService.thinkChat(chatRequest.getMessage());
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,123 @@
|
||||
package com.example.springaidemo.service;
|
||||
|
||||
import io.micrometer.common.util.StringUtils;
|
||||
import org.springframework.ai.chat.messages.UserMessage;
|
||||
import org.springframework.ai.chat.model.ChatResponse;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.deepseek.DeepSeekAssistantMessage;
|
||||
import org.springframework.ai.deepseek.DeepSeekChatModel;
|
||||
import org.springframework.ai.deepseek.DeepSeekChatOptions;
|
||||
import org.springframework.ai.deepseek.api.DeepSeekApi;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
@Service
|
||||
public class DeepseekChatService {
|
||||
|
||||
private final DeepSeekChatModel chatModel;
|
||||
|
||||
@Autowired
|
||||
public DeepseekChatService(DeepSeekChatModel chatModel) {
|
||||
this.chatModel = chatModel;
|
||||
}
|
||||
|
||||
public Map chat(String message) {
|
||||
return Map.of("generation", chatModel.call(message));
|
||||
}
|
||||
|
||||
public Flux<ChatResponse> streamChat(String message) {
|
||||
var prompt = new Prompt(new UserMessage(message));
|
||||
return chatModel.stream(prompt);
|
||||
}
|
||||
|
||||
public SseEmitter sseChat(String message) {
|
||||
SseEmitter emitter = new SseEmitter(30000L); // 30秒超时
|
||||
|
||||
// 异步处理
|
||||
CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
// 使用 Spring AI 的流式调用
|
||||
var prompt = new Prompt(new UserMessage(message));
|
||||
Flux<ChatResponse> responseFlux = chatModel.stream(prompt);
|
||||
|
||||
// 处理流式响应
|
||||
responseFlux.subscribe(
|
||||
response -> {
|
||||
try {
|
||||
// 发送数据到客户端
|
||||
String content = response.getResult().getOutput().getText();
|
||||
emitter.send(SseEmitter.event()
|
||||
.name("message")
|
||||
.data(content));
|
||||
} catch (IOException e) {
|
||||
emitter.completeWithError(e);
|
||||
}
|
||||
},
|
||||
error -> {
|
||||
emitter.completeWithError(error);
|
||||
},
|
||||
() -> {
|
||||
emitter.complete();
|
||||
}
|
||||
);
|
||||
} catch (Exception e) {
|
||||
emitter.completeWithError(e);
|
||||
}
|
||||
});
|
||||
|
||||
return emitter;
|
||||
}
|
||||
|
||||
public SseEmitter thinkChat(String message) {
|
||||
SseEmitter emitter = new SseEmitter(30000L);
|
||||
CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
DeepSeekChatOptions promptOptions = DeepSeekChatOptions.builder()
|
||||
.model(DeepSeekApi.ChatModel.DEEPSEEK_REASONER.getValue())
|
||||
.build();
|
||||
Prompt prompt = new Prompt(message, promptOptions);
|
||||
Flux<ChatResponse> responseFlux = chatModel.stream(prompt);
|
||||
|
||||
responseFlux.subscribe(
|
||||
response -> {
|
||||
try {
|
||||
// 发送数据到客户端
|
||||
DeepSeekAssistantMessage deepSeekAssistantMessage = (DeepSeekAssistantMessage) response.getResult().getOutput();
|
||||
String reasoningContent = deepSeekAssistantMessage.getReasoningContent();
|
||||
String text = deepSeekAssistantMessage.getText();
|
||||
if (StringUtils.isNotEmpty(reasoningContent)) {
|
||||
emitter.send(SseEmitter.event()
|
||||
.name("message")
|
||||
.data(reasoningContent));
|
||||
}
|
||||
if (StringUtils.isNotEmpty(text)) {
|
||||
emitter.send(SseEmitter.event()
|
||||
.name("message")
|
||||
.data(text));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
emitter.completeWithError(e);
|
||||
}
|
||||
},
|
||||
error -> {
|
||||
emitter.completeWithError(error);
|
||||
},
|
||||
() -> {
|
||||
emitter.complete();
|
||||
}
|
||||
);
|
||||
|
||||
} catch (Exception e) {
|
||||
emitter.completeWithError(e);
|
||||
}
|
||||
});
|
||||
|
||||
return emitter;
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
package com.example.springaidemo.service;
|
||||
|
||||
import org.springframework.ai.vectorstore.VectorStore;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
public class VectorService {
|
||||
|
||||
@Autowired
|
||||
private VectorStore vectorStore;
|
||||
|
||||
|
||||
}
|
41
src/main/resources/application.yml
Normal file
41
src/main/resources/application.yml
Normal file
@ -0,0 +1,41 @@
|
||||
spring:
|
||||
application:
|
||||
name: spring-ai-demo
|
||||
elasticsearch:
|
||||
uris: http://154.12.80.119:9200
|
||||
username: elastic
|
||||
password: 123456
|
||||
ai:
|
||||
deepseek:
|
||||
api-key: sk-3043bb4777404970a22c7544dd30aaa2
|
||||
openai:
|
||||
api-key: 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
|
||||
# 如果您有代理服务,可以修改为代理地址
|
||||
# base-url: https://your-proxy-service.com/v1
|
||||
base-url: https://api.openai.com
|
||||
chat:
|
||||
options:
|
||||
model: gpt-3.5-turbo
|
||||
temperature: 0.7
|
||||
# 增加超时配置
|
||||
client:
|
||||
connect-timeout: 30000 # 30秒连接超时
|
||||
read-timeout: 60000 # 60秒读取超时
|
||||
vectorstore:
|
||||
elasticsearch:
|
||||
initialize-schema: true
|
||||
index-name: custom-index
|
||||
dimensions: 1536
|
||||
similarity: cosine
|
||||
# ollama:
|
||||
# base-url: ${OLLAMA_BASE_URL:http://localhost:11434}
|
||||
# chat:
|
||||
# options:
|
||||
# model: ${OLLAMA_MODEL:llama2}
|
||||
|
||||
server:
|
||||
port: 8080
|
||||
|
||||
logging:
|
||||
level:
|
||||
org.springframework.ai: DEBUG
|
174
src/main/resources/templates/chat.html
Normal file
174
src/main/resources/templates/chat.html
Normal file
@ -0,0 +1,174 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Spring AI 聊天演示</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Microsoft YaHei', sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.chat-container {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
padding: 20px;
|
||||
}
|
||||
.header {
|
||||
text-align: center;
|
||||
color: #333;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.chat-box {
|
||||
height: 400px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
padding: 15px;
|
||||
overflow-y: auto;
|
||||
background-color: #fafafa;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.message {
|
||||
margin-bottom: 15px;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.user-message {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
text-align: right;
|
||||
margin-left: 50px;
|
||||
}
|
||||
.ai-message {
|
||||
background-color: #e9ecef;
|
||||
color: #333;
|
||||
margin-right: 50px;
|
||||
}
|
||||
.input-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
#messageInput {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
font-size: 16px;
|
||||
}
|
||||
#sendButton {
|
||||
padding: 10px 20px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
}
|
||||
#sendButton:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
#sendButton:disabled {
|
||||
background-color: #6c757d;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.loading {
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="chat-container">
|
||||
<h1 class="header">🤖 Spring AI 聊天演示</h1>
|
||||
<div id="chatBox" class="chat-box">
|
||||
<div class="message ai-message">
|
||||
<strong>AI助手:</strong> 你好!我是基于 Spring AI 的聊天助手,有什么可以帮助你的吗?
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<input type="text" id="messageInput" placeholder="输入你的消息..."
|
||||
onkeypress="if(event.key==='Enter') sendMessage()">
|
||||
<button id="sendButton" onclick="sendMessage()">发送</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const chatBox = document.getElementById('chatBox');
|
||||
const messageInput = document.getElementById('messageInput');
|
||||
const sendButton = document.getElementById('sendButton');
|
||||
|
||||
function addMessage(content, isUser = false) {
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = `message ${isUser ? 'user-message' : 'ai-message'}`;
|
||||
messageDiv.innerHTML = `<strong>${isUser ? '你' : 'AI助手'}:</strong> ${content}`;
|
||||
chatBox.appendChild(messageDiv);
|
||||
chatBox.scrollTop = chatBox.scrollHeight;
|
||||
}
|
||||
|
||||
function addLoadingMessage() {
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = 'message ai-message loading';
|
||||
messageDiv.id = 'loadingMessage';
|
||||
messageDiv.innerHTML = '<strong>AI助手:</strong> 正在思考中...';
|
||||
chatBox.appendChild(messageDiv);
|
||||
chatBox.scrollTop = chatBox.scrollHeight;
|
||||
return messageDiv;
|
||||
}
|
||||
|
||||
function removeLoadingMessage() {
|
||||
const loadingMessage = document.getElementById('loadingMessage');
|
||||
if (loadingMessage) {
|
||||
loadingMessage.remove();
|
||||
}
|
||||
}
|
||||
|
||||
async function sendMessage() {
|
||||
const message = messageInput.value.trim();
|
||||
if (!message) return;
|
||||
|
||||
// 添加用户消息
|
||||
addMessage(message, true);
|
||||
messageInput.value = '';
|
||||
|
||||
// 禁用发送按钮
|
||||
sendButton.disabled = true;
|
||||
|
||||
// 显示加载消息
|
||||
const loadingMessage = addLoadingMessage();
|
||||
|
||||
try {
|
||||
const response = await fetch('/chat', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: `message=${encodeURIComponent(message)}`
|
||||
});
|
||||
|
||||
const aiResponse = await response.text();
|
||||
|
||||
// 移除加载消息
|
||||
removeLoadingMessage();
|
||||
|
||||
// 添加AI响应
|
||||
addMessage(aiResponse);
|
||||
} catch (error) {
|
||||
removeLoadingMessage();
|
||||
addMessage('抱歉,发送消息时出现错误: ' + error.message);
|
||||
} finally {
|
||||
sendButton.disabled = false;
|
||||
messageInput.focus();
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载完成后聚焦输入框
|
||||
window.onload = function() {
|
||||
messageInput.focus();
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in New Issue
Block a user