Spaces:
Sleeping
Sleeping
Commit ·
894f93a
0
Parent(s):
Initial commit: Enriched AI Sentiment & Keywords App
Browse files- .gitignore +5 -0
- Dockerfile +18 -0
- README.md +49 -0
- pom.xml +43 -0
- src/main/java/com/example/sentiment/Application.java +17 -0
- src/main/java/com/example/sentiment/controller/ChatController.java +111 -0
- src/main/java/com/example/sentiment/model/ChatRequest.java +47 -0
- src/main/java/com/example/sentiment/model/ChatResponse.java +50 -0
- src/main/java/com/example/sentiment/service/SentimentService.java +78 -0
- src/main/resources/application.properties +12 -0
- src/main/resources/templates/index.html +199 -0
.gitignore
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
target/
|
| 2 |
+
*.class
|
| 3 |
+
.idea/
|
| 4 |
+
*.iml
|
| 5 |
+
.DS_Store
|
Dockerfile
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 使用多阶段构建来减小最终镜像大小
|
| 2 |
+
# 第一阶段:构建应用
|
| 3 |
+
FROM maven:3.9.1-eclipse-temurin-21 AS build
|
| 4 |
+
WORKDIR /app
|
| 5 |
+
COPY pom.xml .
|
| 6 |
+
COPY src ./src
|
| 7 |
+
RUN mvn clean package -DskipTests
|
| 8 |
+
|
| 9 |
+
# 第二阶段:运行应用
|
| 10 |
+
FROM eclipse-temurin:21-jre
|
| 11 |
+
WORKDIR /app
|
| 12 |
+
COPY --from=build /app/target/*.jar app.jar
|
| 13 |
+
|
| 14 |
+
# 暴露 Hugging Face Spaces 默认端口
|
| 15 |
+
EXPOSE 7860
|
| 16 |
+
|
| 17 |
+
# 运行命令
|
| 18 |
+
ENTRYPOINT ["java", "-jar", "app.jar"]
|
README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: "AI 智能文本分析助手"
|
| 3 |
+
emoji: "🧠"
|
| 4 |
+
colorFrom: "indigo"
|
| 5 |
+
colorTo: "purple"
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
short_description: "基于 Spring Boot 3 & DeepSeek AI 驱动的情感分析、关键词提取与对话建议(全中文汉化版)"
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
# AI 智能文本分析助手 (AI Text Analysis Pro)
|
| 12 |
+
|
| 13 |
+
这是一个使用 **Spring Boot 3** 和 **Java 21** 开发的高级文本分析应用,集成了 **DeepSeek-V3 (SiliconFlow API)**。
|
| 14 |
+
|
| 15 |
+
## 🌟 核心功能
|
| 16 |
+
|
| 17 |
+
- **📊 情感倾向分析**:深度分析文本情感,识别积极、消极或中性情绪,并提供专业建议。
|
| 18 |
+
- **🏷️ 核心关键词提取**:自动从长文本中提取最具代表性的 3-5 个关键词。
|
| 19 |
+
- **🕒 智能历史记录**:在当前会话中自动保存分析历史,方便对比查看。
|
| 20 |
+
- **📱 响应式 UI**:适配手机和电脑,提供流畅的中文交互体验。
|
| 21 |
+
|
| 22 |
+
## 🛠️ 技术实现
|
| 23 |
+
|
| 24 |
+
- **后端**: Spring Boot 3.4.3, Java 21 (LTS)
|
| 25 |
+
- **AI 引擎**: DeepSeek-V3 (经由 SiliconFlow 高速接口)
|
| 26 |
+
- **前端**: Thymeleaf + Bootstrap 5 + jQuery
|
| 27 |
+
- **容器化**: Docker (多阶段构建)
|
| 28 |
+
- **部署**: 已针对 Hugging Face Spaces 优化 (端口 7860)
|
| 29 |
+
|
| 30 |
+
## 🚀 快速启动
|
| 31 |
+
|
| 32 |
+
1. **配置 API Key**:
|
| 33 |
+
在 `src/main/resources/application.properties` 中填入你的 SiliconFlow API Key。
|
| 34 |
+
|
| 35 |
+
2. **本地运行**:
|
| 36 |
+
```bash
|
| 37 |
+
mvn spring-boot:run
|
| 38 |
+
```
|
| 39 |
+
访问 `http://localhost:7860`。
|
| 40 |
+
|
| 41 |
+
3. **Docker 构建**:
|
| 42 |
+
```bash
|
| 43 |
+
docker build -t sentiment-app .
|
| 44 |
+
docker run -p 7860:7860 sentiment-app
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
## 📄 开源说明
|
| 48 |
+
|
| 49 |
+
本项目为汉化增强版,旨在提供开箱即用的 Java AI 集成方案。
|
pom.xml
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?xml version="1.0" encoding="UTF-8"?>
|
| 2 |
+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
| 3 |
+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
| 4 |
+
<modelVersion>4.0.0</modelVersion>
|
| 5 |
+
<parent>
|
| 6 |
+
<groupId>org.springframework.boot</groupId>
|
| 7 |
+
<artifactId>spring-boot-starter-parent</artifactId>
|
| 8 |
+
<version>3.4.3</version>
|
| 9 |
+
<relativePath/> <!-- lookup parent from repository -->
|
| 10 |
+
</parent>
|
| 11 |
+
<groupId>com.example</groupId>
|
| 12 |
+
<artifactId>sentiment-analysis-app</artifactId>
|
| 13 |
+
<version>0.0.1-SNAPSHOT</version>
|
| 14 |
+
<name>sentiment-analysis-app</name>
|
| 15 |
+
<description>基于 Spring Boot 的情感分析与 AI 对话应用</description>
|
| 16 |
+
<properties>
|
| 17 |
+
<java.version>21</java.version>
|
| 18 |
+
</properties>
|
| 19 |
+
<dependencies>
|
| 20 |
+
<dependency>
|
| 21 |
+
<groupId>org.springframework.boot</groupId>
|
| 22 |
+
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
| 23 |
+
</dependency>
|
| 24 |
+
<dependency>
|
| 25 |
+
<groupId>org.springframework.boot</groupId>
|
| 26 |
+
<artifactId>spring-boot-starter-web</artifactId>
|
| 27 |
+
</dependency>
|
| 28 |
+
<dependency>
|
| 29 |
+
<groupId>org.springframework.boot</groupId>
|
| 30 |
+
<artifactId>spring-boot-starter-test</artifactId>
|
| 31 |
+
<scope>test</scope>
|
| 32 |
+
</dependency>
|
| 33 |
+
</dependencies>
|
| 34 |
+
|
| 35 |
+
<build>
|
| 36 |
+
<plugins>
|
| 37 |
+
<plugin>
|
| 38 |
+
<groupId>org.springframework.boot</groupId>
|
| 39 |
+
<artifactId>spring-boot-maven-plugin</artifactId>
|
| 40 |
+
</plugin>
|
| 41 |
+
</plugins>
|
| 42 |
+
</build>
|
| 43 |
+
</project>
|
src/main/java/com/example/sentiment/Application.java
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.example.sentiment;
|
| 2 |
+
|
| 3 |
+
import org.springframework.boot.SpringApplication;
|
| 4 |
+
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
| 5 |
+
|
| 6 |
+
/**
|
| 7 |
+
* Spring Boot 应用程序入口点
|
| 8 |
+
* 情感分析与 AI 对话助手
|
| 9 |
+
*/
|
| 10 |
+
@SpringBootApplication
|
| 11 |
+
public class Application {
|
| 12 |
+
|
| 13 |
+
public static void main(String[] args) {
|
| 14 |
+
// 启动 Spring Boot 应用
|
| 15 |
+
SpringApplication.run(Application.class, args);
|
| 16 |
+
}
|
| 17 |
+
}
|
src/main/java/com/example/sentiment/controller/ChatController.java
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.example.sentiment.controller;
|
| 2 |
+
|
| 3 |
+
import com.example.sentiment.service.SentimentService;
|
| 4 |
+
import jakarta.servlet.http.HttpSession;
|
| 5 |
+
import org.springframework.beans.factory.annotation.Autowired;
|
| 6 |
+
import org.springframework.stereotype.Controller;
|
| 7 |
+
import org.springframework.web.bind.annotation.GetMapping;
|
| 8 |
+
import org.springframework.web.bind.annotation.PostMapping;
|
| 9 |
+
import org.springframework.web.bind.annotation.RequestParam;
|
| 10 |
+
import org.springframework.web.bind.annotation.ResponseBody;
|
| 11 |
+
|
| 12 |
+
import java.util.ArrayList;
|
| 13 |
+
import java.util.HashMap;
|
| 14 |
+
import java.util.List;
|
| 15 |
+
import java.util.Map;
|
| 16 |
+
|
| 17 |
+
/**
|
| 18 |
+
* Web 控制器
|
| 19 |
+
* 处理页面渲染、情感分析、关键词提取和历史记录
|
| 20 |
+
*/
|
| 21 |
+
@Controller
|
| 22 |
+
public class ChatController {
|
| 23 |
+
|
| 24 |
+
@Autowired
|
| 25 |
+
private SentimentService sentimentService;
|
| 26 |
+
|
| 27 |
+
@GetMapping("/")
|
| 28 |
+
public String index() {
|
| 29 |
+
return "index";
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
/**
|
| 33 |
+
* 进行情感分析
|
| 34 |
+
*/
|
| 35 |
+
@PostMapping("/analyze")
|
| 36 |
+
@ResponseBody
|
| 37 |
+
public Map<String, String> analyze(@RequestParam String text, HttpSession session) {
|
| 38 |
+
Map<String, String> result = new HashMap<>();
|
| 39 |
+
if (text == null || text.trim().isEmpty()) {
|
| 40 |
+
result.put("error", "请输入内容后再试。");
|
| 41 |
+
return result;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
String analysis = sentimentService.analyzeSentiment(text);
|
| 45 |
+
saveHistory(session, "情感分析", text, analysis);
|
| 46 |
+
|
| 47 |
+
result.put("result", analysis);
|
| 48 |
+
return result;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
/**
|
| 52 |
+
* 提取关键词
|
| 53 |
+
*/
|
| 54 |
+
@PostMapping("/keywords")
|
| 55 |
+
@ResponseBody
|
| 56 |
+
public Map<String, String> keywords(@RequestParam String text, HttpSession session) {
|
| 57 |
+
Map<String, String> result = new HashMap<>();
|
| 58 |
+
if (text == null || text.trim().isEmpty()) {
|
| 59 |
+
result.put("error", "请输入内容后再试。");
|
| 60 |
+
return result;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
String keywords = sentimentService.extractKeywords(text);
|
| 64 |
+
saveHistory(session, "关键词提取", text, keywords);
|
| 65 |
+
|
| 66 |
+
result.put("result", keywords);
|
| 67 |
+
return result;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
/**
|
| 71 |
+
* 获取当前会话的历史记录
|
| 72 |
+
*/
|
| 73 |
+
@GetMapping("/history")
|
| 74 |
+
@ResponseBody
|
| 75 |
+
public List<Map<String, String>> getHistory(HttpSession session) {
|
| 76 |
+
List<Map<String, String>> history = (List<Map<String, String>>) session.getAttribute("history");
|
| 77 |
+
return history != null ? history : new ArrayList<>();
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
/**
|
| 81 |
+
* 清空历史记录
|
| 82 |
+
*/
|
| 83 |
+
@PostMapping("/clearHistory")
|
| 84 |
+
@ResponseBody
|
| 85 |
+
public String clearHistory(HttpSession session) {
|
| 86 |
+
session.removeAttribute("history");
|
| 87 |
+
return "success";
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
/**
|
| 91 |
+
* 私有方法:保存操作历史到 Session
|
| 92 |
+
*/
|
| 93 |
+
private void saveHistory(HttpSession session, String type, String input, String output) {
|
| 94 |
+
List<Map<String, String>> history = (List<Map<String, String>>) session.getAttribute("history");
|
| 95 |
+
if (history == null) {
|
| 96 |
+
history = new ArrayList<>();
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
Map<String, String> entry = new HashMap<>();
|
| 100 |
+
entry.put("type", type);
|
| 101 |
+
entry.put("input", input.length() > 50 ? input.substring(0, 47) + "..." : input);
|
| 102 |
+
entry.put("output", output);
|
| 103 |
+
|
| 104 |
+
// 只保留最近 5 条
|
| 105 |
+
if (history.size() >= 5) {
|
| 106 |
+
history.remove(0);
|
| 107 |
+
}
|
| 108 |
+
history.add(entry);
|
| 109 |
+
session.setAttribute("history", history);
|
| 110 |
+
}
|
| 111 |
+
}
|
src/main/java/com/example/sentiment/model/ChatRequest.java
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.example.sentiment.model;
|
| 2 |
+
|
| 3 |
+
import java.util.List;
|
| 4 |
+
|
| 5 |
+
/**
|
| 6 |
+
* SiliconFlow API 请求模型
|
| 7 |
+
*/
|
| 8 |
+
public class ChatRequest {
|
| 9 |
+
private String model;
|
| 10 |
+
private List<Message> messages;
|
| 11 |
+
private Double temperature;
|
| 12 |
+
|
| 13 |
+
public ChatRequest() {}
|
| 14 |
+
|
| 15 |
+
public ChatRequest(String model, List<Message> messages, Double temperature) {
|
| 16 |
+
this.model = model;
|
| 17 |
+
this.messages = messages;
|
| 18 |
+
this.temperature = temperature;
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
public String getModel() { return model; }
|
| 22 |
+
public void setModel(String model) { this.model = model; }
|
| 23 |
+
|
| 24 |
+
public List<Message> getMessages() { return messages; }
|
| 25 |
+
public void setMessages(List<Message> messages) { this.messages = messages; }
|
| 26 |
+
|
| 27 |
+
public Double getTemperature() { return temperature; }
|
| 28 |
+
public void setTemperature(Double temperature) { this.temperature = temperature; }
|
| 29 |
+
|
| 30 |
+
public static class Message {
|
| 31 |
+
private String role;
|
| 32 |
+
private String content;
|
| 33 |
+
|
| 34 |
+
public Message() {}
|
| 35 |
+
|
| 36 |
+
public Message(String role, String content) {
|
| 37 |
+
this.role = role;
|
| 38 |
+
this.content = content;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
public String getRole() { return role; }
|
| 42 |
+
public void setRole(String role) { this.role = role; }
|
| 43 |
+
|
| 44 |
+
public String getContent() { return content; }
|
| 45 |
+
public void setContent(String content) { this.content = content; }
|
| 46 |
+
}
|
| 47 |
+
}
|
src/main/java/com/example/sentiment/model/ChatResponse.java
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.example.sentiment.model;
|
| 2 |
+
|
| 3 |
+
import java.util.List;
|
| 4 |
+
|
| 5 |
+
/**
|
| 6 |
+
* SiliconFlow API 响应模型
|
| 7 |
+
*/
|
| 8 |
+
public class ChatResponse {
|
| 9 |
+
private List<Choice> choices;
|
| 10 |
+
|
| 11 |
+
public ChatResponse() {}
|
| 12 |
+
|
| 13 |
+
public ChatResponse(List<Choice> choices) {
|
| 14 |
+
this.choices = choices;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
public List<Choice> getChoices() { return choices; }
|
| 18 |
+
public void setChoices(List<Choice> choices) { this.choices = choices; }
|
| 19 |
+
|
| 20 |
+
public static class Choice {
|
| 21 |
+
private Message message;
|
| 22 |
+
|
| 23 |
+
public Choice() {}
|
| 24 |
+
|
| 25 |
+
public Choice(Message message) {
|
| 26 |
+
this.message = message;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
public Message getMessage() { return message; }
|
| 30 |
+
public void setMessage(Message message) { this.message = message; }
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
public static class Message {
|
| 34 |
+
private String role;
|
| 35 |
+
private String content;
|
| 36 |
+
|
| 37 |
+
public Message() {}
|
| 38 |
+
|
| 39 |
+
public Message(String role, String content) {
|
| 40 |
+
this.role = role;
|
| 41 |
+
this.content = content;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
public String getRole() { return role; }
|
| 45 |
+
public void setRole(String role) { this.role = role; }
|
| 46 |
+
|
| 47 |
+
public String getContent() { return content; }
|
| 48 |
+
public void setContent(String content) { this.content = content; }
|
| 49 |
+
}
|
| 50 |
+
}
|
src/main/java/com/example/sentiment/service/SentimentService.java
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.example.sentiment.service;
|
| 2 |
+
|
| 3 |
+
import com.example.sentiment.model.ChatRequest;
|
| 4 |
+
import com.example.sentiment.model.ChatResponse;
|
| 5 |
+
import org.springframework.beans.factory.annotation.Value;
|
| 6 |
+
import org.springframework.http.HttpEntity;
|
| 7 |
+
import org.springframework.http.HttpHeaders;
|
| 8 |
+
import org.springframework.http.MediaType;
|
| 9 |
+
import org.springframework.stereotype.Service;
|
| 10 |
+
import org.springframework.web.client.RestTemplate;
|
| 11 |
+
|
| 12 |
+
import java.util.ArrayList;
|
| 13 |
+
import java.util.List;
|
| 14 |
+
|
| 15 |
+
/**
|
| 16 |
+
* 情感分析与 AI 对话服务
|
| 17 |
+
* 负责与 SiliconFlow API 通信,支持多种分析模式
|
| 18 |
+
*/
|
| 19 |
+
@Service
|
| 20 |
+
public class SentimentService {
|
| 21 |
+
|
| 22 |
+
@Value("${siliconflow.api.key}")
|
| 23 |
+
private String apiKey;
|
| 24 |
+
|
| 25 |
+
@Value("${siliconflow.api.url}")
|
| 26 |
+
private String apiUrl;
|
| 27 |
+
|
| 28 |
+
@Value("${siliconflow.model}")
|
| 29 |
+
private String model;
|
| 30 |
+
|
| 31 |
+
private final RestTemplate restTemplate = new RestTemplate();
|
| 32 |
+
|
| 33 |
+
/**
|
| 34 |
+
* 调用 AI 模型进行对话或分析
|
| 35 |
+
* @param prompt 用户输入
|
| 36 |
+
* @param systemPrompt 系统提示词,定义分析模式
|
| 37 |
+
* @return AI 响应内容
|
| 38 |
+
*/
|
| 39 |
+
public String callAi(String prompt, String systemPrompt) {
|
| 40 |
+
try {
|
| 41 |
+
HttpHeaders headers = new HttpHeaders();
|
| 42 |
+
headers.setContentType(MediaType.APPLICATION_JSON);
|
| 43 |
+
headers.setBearerAuth(apiKey);
|
| 44 |
+
|
| 45 |
+
List<ChatRequest.Message> messages = new ArrayList<>();
|
| 46 |
+
messages.add(new ChatRequest.Message("system", systemPrompt));
|
| 47 |
+
messages.add(new ChatRequest.Message("user", prompt));
|
| 48 |
+
|
| 49 |
+
ChatRequest request = new ChatRequest(model, messages, 0.7);
|
| 50 |
+
|
| 51 |
+
HttpEntity<ChatRequest> entity = new HttpEntity<>(request, headers);
|
| 52 |
+
ChatResponse response = restTemplate.postForObject(apiUrl, entity, ChatResponse.class);
|
| 53 |
+
|
| 54 |
+
if (response != null && response.getChoices() != null && !response.getChoices().isEmpty()) {
|
| 55 |
+
return response.getChoices().get(0).getMessage().getContent();
|
| 56 |
+
}
|
| 57 |
+
return "抱歉,AI 响应为空。";
|
| 58 |
+
} catch (Exception e) {
|
| 59 |
+
return "AI 调用出错:" + e.getMessage();
|
| 60 |
+
}
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
/**
|
| 64 |
+
* 进行情感分析
|
| 65 |
+
*/
|
| 66 |
+
public String analyzeSentiment(String prompt) {
|
| 67 |
+
String systemPrompt = "你是一个专业的情感分析助手。请分析用户输入的情感倾向(积极、消极或中性),并给出简短的建议。请始终使用中文回复。";
|
| 68 |
+
return callAi(prompt, systemPrompt);
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
/**
|
| 72 |
+
* 提取关键词
|
| 73 |
+
*/
|
| 74 |
+
public String extractKeywords(String prompt) {
|
| 75 |
+
String systemPrompt = "你是一个语言专家。请从用户提供的文本中提取出最重要的 3-5 个关键词,并以列表形式展示。请始终使用中文回复。";
|
| 76 |
+
return callAi(prompt, systemPrompt);
|
| 77 |
+
}
|
| 78 |
+
}
|
src/main/resources/application.properties
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 应用名称
|
| 2 |
+
spring.application.name=sentiment-analysis-app
|
| 3 |
+
# 服务器端口 (Hugging Face Spaces 默认 7860)
|
| 4 |
+
server.port=7860
|
| 5 |
+
|
| 6 |
+
# SiliconFlow API 配置
|
| 7 |
+
siliconflow.api.key=sk-uuejewptzohwsbbutfnrkbcloaqxydjmxbeqptwphnhiuopl
|
| 8 |
+
siliconflow.api.url=https://api.siliconflow.cn/v1/chat/completions
|
| 9 |
+
siliconflow.model=deepseek-ai/DeepSeek-V3
|
| 10 |
+
|
| 11 |
+
# Thymeleaf 缓存配置(开发环境关闭)
|
| 12 |
+
spring.thymeleaf.cache=false
|
src/main/resources/templates/index.html
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html xmlns:th="http://www.thymeleaf.org" lang="zh-CN">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>AI 智能文本分析助手</title>
|
| 7 |
+
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet">
|
| 8 |
+
<style>
|
| 9 |
+
:root {
|
| 10 |
+
--primary-color: #4a90e2;
|
| 11 |
+
--secondary-color: #f5f7fa;
|
| 12 |
+
}
|
| 13 |
+
body {
|
| 14 |
+
background-color: #f0f2f5;
|
| 15 |
+
font-family: 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
|
| 16 |
+
}
|
| 17 |
+
.main-card {
|
| 18 |
+
border: none;
|
| 19 |
+
border-radius: 20px;
|
| 20 |
+
box-shadow: 0 10px 30px rgba(0,0,0,0.08);
|
| 21 |
+
overflow: hidden;
|
| 22 |
+
}
|
| 23 |
+
.header-gradient {
|
| 24 |
+
background: linear-gradient(135deg, #4a90e2 0%, #357abd 100%);
|
| 25 |
+
color: white;
|
| 26 |
+
padding: 40px 20px;
|
| 27 |
+
}
|
| 28 |
+
.nav-tabs .nav-link {
|
| 29 |
+
border: none;
|
| 30 |
+
color: #666;
|
| 31 |
+
padding: 12px 25px;
|
| 32 |
+
font-weight: 500;
|
| 33 |
+
}
|
| 34 |
+
.nav-tabs .nav-link.active {
|
| 35 |
+
color: var(--primary-color);
|
| 36 |
+
border-bottom: 3px solid var(--primary-color);
|
| 37 |
+
background: none;
|
| 38 |
+
}
|
| 39 |
+
.result-area {
|
| 40 |
+
background-color: white;
|
| 41 |
+
border-radius: 12px;
|
| 42 |
+
padding: 25px;
|
| 43 |
+
border: 1px solid #e1e4e8;
|
| 44 |
+
min-height: 100px;
|
| 45 |
+
margin-top: 20px;
|
| 46 |
+
}
|
| 47 |
+
.history-item {
|
| 48 |
+
background-color: white;
|
| 49 |
+
border-radius: 10px;
|
| 50 |
+
padding: 15px;
|
| 51 |
+
margin-bottom: 15px;
|
| 52 |
+
border-left: 4px solid var(--primary-color);
|
| 53 |
+
transition: transform 0.2s;
|
| 54 |
+
}
|
| 55 |
+
.history-item:hover {
|
| 56 |
+
transform: translateY(-2px);
|
| 57 |
+
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
|
| 58 |
+
}
|
| 59 |
+
.badge-type {
|
| 60 |
+
font-size: 0.75rem;
|
| 61 |
+
padding: 4px 8px;
|
| 62 |
+
border-radius: 4px;
|
| 63 |
+
background-color: #eef2f7;
|
| 64 |
+
color: #555;
|
| 65 |
+
}
|
| 66 |
+
#loading { display: none; }
|
| 67 |
+
</style>
|
| 68 |
+
</head>
|
| 69 |
+
<body>
|
| 70 |
+
|
| 71 |
+
<div class="container py-5">
|
| 72 |
+
<div class="row justify-content-center">
|
| 73 |
+
<div class="col-lg-9">
|
| 74 |
+
<div class="card main-card mb-4">
|
| 75 |
+
<div class="header-gradient text-center">
|
| 76 |
+
<h1 class="display-6 fw-bold mb-2">AI 智能文本分析助手</h1>
|
| 77 |
+
<p class="lead mb-0">情感分析 · 关键词提取 · 智能建议</p>
|
| 78 |
+
</div>
|
| 79 |
+
|
| 80 |
+
<div class="card-body p-4">
|
| 81 |
+
<div class="mb-4">
|
| 82 |
+
<label for="textInput" class="form-label fw-bold">请输入待分析的文本内容:</label>
|
| 83 |
+
<textarea class="form-control" id="textInput" rows="5" placeholder="在此输入文字,例如:最近工作压力有点大,但看到同事们的进步我也感到很欣慰..."></textarea>
|
| 84 |
+
</div>
|
| 85 |
+
|
| 86 |
+
<div class="row g-3">
|
| 87 |
+
<div class="col-md-6">
|
| 88 |
+
<button class="btn btn-primary w-100 py-3 fw-bold" id="analyzeBtn">
|
| 89 |
+
<span class="btn-text">🔍 情感倾向分析</span>
|
| 90 |
+
</button>
|
| 91 |
+
</div>
|
| 92 |
+
<div class="col-md-6">
|
| 93 |
+
<button class="btn btn-outline-primary w-100 py-3 fw-bold" id="keywordsBtn">
|
| 94 |
+
<span class="btn-text">🏷️ 提取核心关键词</span>
|
| 95 |
+
</button>
|
| 96 |
+
</div>
|
| 97 |
+
</div>
|
| 98 |
+
|
| 99 |
+
<div id="loading" class="text-center mt-4">
|
| 100 |
+
<div class="spinner-border text-primary" role="status"></div>
|
| 101 |
+
<p class="mt-2 text-muted">AI 正在思考中,请稍候...</p>
|
| 102 |
+
</div>
|
| 103 |
+
|
| 104 |
+
<div id="resultBox" style="display: none;">
|
| 105 |
+
<div class="result-area mt-4">
|
| 106 |
+
<div class="d-flex justify-content-between align-items-center mb-3">
|
| 107 |
+
<h5 class="mb-0 text-primary fw-bold" id="resultTitle">分析结果</h5>
|
| 108 |
+
<button class="btn btn-sm btn-light" onclick="$('#resultBox').hide()">关闭</button>
|
| 109 |
+
</div>
|
| 110 |
+
<div id="analysisResult" class="lh-lg"></div>
|
| 111 |
+
</div>
|
| 112 |
+
</div>
|
| 113 |
+
</div>
|
| 114 |
+
</div>
|
| 115 |
+
|
| 116 |
+
<!-- 历史记录部分 -->
|
| 117 |
+
<div class="card main-card">
|
| 118 |
+
<div class="card-header bg-white py-3 d-flex justify-content-between align-items-center">
|
| 119 |
+
<h5 class="mb-0 fw-bold">🕒 最近分析历史</h5>
|
| 120 |
+
<button class="btn btn-sm btn-outline-danger" id="clearHistoryBtn">清空历史</button>
|
| 121 |
+
</div>
|
| 122 |
+
<div class="card-body p-4 bg-light" id="historyList">
|
| 123 |
+
<p class="text-center text-muted py-4">暂无历史记录</p>
|
| 124 |
+
</div>
|
| 125 |
+
</div>
|
| 126 |
+
</div>
|
| 127 |
+
</div>
|
| 128 |
+
</div>
|
| 129 |
+
|
| 130 |
+
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
|
| 131 |
+
<script>
|
| 132 |
+
function updateHistory() {
|
| 133 |
+
$.get('/history', function(data) {
|
| 134 |
+
const container = $('#historyList');
|
| 135 |
+
if (data.length === 0) {
|
| 136 |
+
container.html('<p class="text-center text-muted py-4">暂无历史记录</p>');
|
| 137 |
+
return;
|
| 138 |
+
}
|
| 139 |
+
let html = '';
|
| 140 |
+
data.reverse().forEach(item => {
|
| 141 |
+
html += `
|
| 142 |
+
<div class="history-item">
|
| 143 |
+
<div class="d-flex justify-content-between mb-2">
|
| 144 |
+
<span class="badge-type">${item.type}</span>
|
| 145 |
+
<small class="text-muted">刚才</small>
|
| 146 |
+
</div>
|
| 147 |
+
<div class="text-truncate text-muted small mb-2">输入: ${item.input}</div>
|
| 148 |
+
<div class="fw-medium">${item.output.replace(/\n/g, '<br>')}</div>
|
| 149 |
+
</div>
|
| 150 |
+
`;
|
| 151 |
+
});
|
| 152 |
+
container.html(html);
|
| 153 |
+
});
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
function processAction(url, title) {
|
| 157 |
+
const text = $('#textInput').val().trim();
|
| 158 |
+
if (!text) {
|
| 159 |
+
alert('请输入内容后再试。');
|
| 160 |
+
return;
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
$('#loading').show();
|
| 164 |
+
$('.btn').prop('disabled', true);
|
| 165 |
+
$('#resultBox').hide();
|
| 166 |
+
|
| 167 |
+
$.post(url, { text: text }, function(data) {
|
| 168 |
+
if (data.error) {
|
| 169 |
+
alert(data.error);
|
| 170 |
+
} else {
|
| 171 |
+
$('#resultTitle').text(title);
|
| 172 |
+
$('#analysisResult').html(data.result.replace(/\n/g, '<br>'));
|
| 173 |
+
$('#resultBox').fadeIn();
|
| 174 |
+
updateHistory();
|
| 175 |
+
}
|
| 176 |
+
}).fail(function() {
|
| 177 |
+
alert('服务器请求失败,请检查后端运行状态。');
|
| 178 |
+
}).always(function() {
|
| 179 |
+
$('#loading').hide();
|
| 180 |
+
$('.btn').prop('disabled', false);
|
| 181 |
+
});
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
$(document).ready(function() {
|
| 185 |
+
$('#analyzeBtn').click(() => processAction('/analyze', '📊 情感分析报告'));
|
| 186 |
+
$('#keywordsBtn').click(() => processAction('/keywords', '🏷️ 核心关键词提取'));
|
| 187 |
+
|
| 188 |
+
$('#clearHistoryBtn').click(function() {
|
| 189 |
+
$.post('/clearHistory', function() {
|
| 190 |
+
updateHistory();
|
| 191 |
+
});
|
| 192 |
+
});
|
| 193 |
+
|
| 194 |
+
updateHistory();
|
| 195 |
+
});
|
| 196 |
+
</script>
|
| 197 |
+
|
| 198 |
+
</body>
|
| 199 |
+
</html>
|