完成基本功能

This commit is contained in:
2025-02-25 00:30:24 +08:00
commit 57ecdb62fa
19 changed files with 4131 additions and 0 deletions

139
.gitignore vendored Normal file
View File

@@ -0,0 +1,139 @@
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
.idea/**/aws.xml
.idea/**/contentModel.xml
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
.idea/**/gradle.xml
.idea/**/libraries
cmake-build-*/
.idea/**/mongoSettings.xml
*.iws
out/
.idea_modules/
atlassian-ide-plugin.xml
.idea/replstate.xml
.idea/sonarlint/
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
.idea/httpRequests
.idea/caches/build_file_checksums.ser
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
.mvn/wrapper/maven-wrapper.jar
.project
.classpath
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
.history/
*.vsix
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
pids
*.pid
*.seed
*.pid.lock
lib-cov
coverage
*.lcov
.nyc_output
.grunt
bower_components
.lock-wscript
build/Release
node_modules/
jspm_packages/
web_modules/
*.tsbuildinfo
.npm
.eslintcache
.stylelintcache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
.node_repl_history
*.tgz
.yarn-integrity
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
.cache
.parcel-cache
.next
out
.nuxt
dist
.cache/
.vuepress/dist
.temp
.docusaurus
.serverless/
.fusebox/
.dynamodb/
.tern-port
.vscode-test
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
*.stackdump
[Dd]esktop.ini
$RECYCLE.BIN/
*.cab
*.msi
*.msix
*.msm
*.msp
*.lnk
.DS_Store
.AppleDouble
.LSOverride
Icon
._*
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk

8
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,8 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

19
.idea/compiler.xml generated Normal file
View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<annotationProcessing>
<profile default="true" name="Default" enabled="true" />
<profile name="Maven default annotation processors profile" enabled="true">
<sourceOutputDir name="target/generated-sources/annotations" />
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" />
<module name="ai-analysis" />
</profile>
</annotationProcessing>
</component>
<component name="JavacSettings">
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
<module name="ai-analysis" options="-parameters" />
</option>
</component>
</project>

6
.idea/encodings.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
</component>
</project>

20
.idea/jarRepositories.xml generated Normal file
View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="https://repo.maven.apache.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
</component>
</project>

14
.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="temurin-17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

19
client/index.html Normal file
View File

@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="UTF-8">
<link href="/favicon.ico" rel="icon">
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<title>AI Analysis</title>
<style>
html, body {
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<div id="app"></div>
<script src="/src/main.js" type="module"></script>
</body>
</html>

13
client/jsconfig.json Normal file
View File

@@ -0,0 +1,13 @@
{
"compilerOptions": {
"paths": {
"@/*": [
"./src/*"
]
}
},
"exclude": [
"node_modules",
"dist"
]
}

24
client/package.json Normal file
View File

@@ -0,0 +1,24 @@
{
"name": "ai-analysis",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@microsoft/fetch-event-source": "^2.0.1",
"markdown-it": "^14.1.0",
"mermaid": "^11.4.1",
"mermaid-it-markdown": "^1.0.8",
"vue": "^3.5.13",
"vue3-markdown-it": "^1.0.10"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.1",
"vite": "^6.1.0",
"vite-plugin-vue-devtools": "^7.7.2"
}
}

3355
client/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

345
client/src/App.vue Normal file
View File

@@ -0,0 +1,345 @@
<script setup>
import {ref} from 'vue'
import MarkdownIt from 'markdown-it'
import MarkDownMermaidPlugin from 'mermaid-it-markdown'
import {fetchEventSource} from '@microsoft/fetch-event-source'
// 初始化 markdown-it 实例
const md = new MarkdownIt({
html: true, // 启用 HTML 标签
breaks: true, // 转换换行符为 <br>
linkify: true, // 自动转换 URL 为链接
typographer: true // 启用一些语言中性的替换 + 引号美化
})
md.use(MarkDownMermaidPlugin)
const question = ref('')
const answer = ref('')
const loading = ref(false)
// 使用计算属性来渲染 markdown
const renderedAnswer = ref('')
answer.value = "```mermaid\ngraph TD;\n A-->B;\n A-->C;\n B-->D;\n C-->D;\n```"
renderedAnswer.value = md.render(answer.value)
const handleSubmit = async () => {
if (!question.value.trim() || loading.value) return
loading.value = true
answer.value = ''
renderedAnswer.value = ''
try {
await fetchEventSource('http://localhost:7891/chat/stream', {
method: 'POST',
body: question.value.trim(),
onmessage(event) {
// 处理每个消息事件
answer.value += event.data
// 实时渲染 markdown
renderedAnswer.value = md.render(answer.value)
},
onclose() {
loading.value = false
console.log(answer.value)
},
onerror(err) {
console.error('请求出错:', err)
loading.value = false
return false // 不进行重试
}
})
} catch (error) {
console.error('请求出错:', error)
answer.value = '抱歉,请求出现错误'
renderedAnswer.value = answer.value
loading.value = false
}
}
</script>
<template>
<div class="chat-container">
<div class="chat-header">
<h1>AI 助手</h1>
</div>
<div class="messages-container">
<div class="answer-area">
<div
v-if="renderedAnswer"
class="markdown-body"
v-html="renderedAnswer"
/>
<div v-else class="placeholder">
<template v-if="loading">
<div class="thinking">
<span>思考中</span>
<span class="thinking-spinner"></span>
</div>
</template>
<template v-else>
在下方输入您的问题AI 助手会为您解答...
</template>
</div>
</div>
</div>
<div class="input-area">
<div class="input-container">
<textarea
v-model="question"
placeholder="请输入您的问题... (Ctrl + Enter 快速发送)"
@keyup.ctrl.enter="handleSubmit"
></textarea>
<button
:disabled="loading"
class="send-button"
@click="handleSubmit"
>
<span v-if="loading" class="loading-spinner"></span>
<span>{{ loading ? '思考中...' : '发送' }}</span>
</button>
</div>
</div>
</div>
</template>
<style scoped>
.chat-container {
margin: 0;
height: 100vh;
display: flex;
flex-direction: column;
background-color: #fafafa; /* 更柔和的背景色 */
}
.chat-header {
padding: 20px 32px;
background: linear-gradient(to right, #2563eb, #3b82f6); /* 渐变背景 */
box-shadow: 0 2px 12px rgba(37, 99, 235, 0.15);
position: sticky;
top: 0;
z-index: 10;
}
.chat-header h1 {
margin: 0;
font-size: 1.5rem;
color: #ffffff;
font-weight: 600;
letter-spacing: -0.5px;
}
.messages-container {
flex: 1;
overflow-y: auto;
padding: 32px;
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%); /* 渐变背景 */
scroll-behavior: smooth;
}
.answer-area {
max-width: 1200px;
margin: 0 auto;
background-color: #ffffff;
border-radius: 20px;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.08);
padding: 40px;
min-height: 200px;
transition: all 0.3s ease;
border: 1px solid rgba(0, 0, 0, 0.05);
}
.placeholder {
color: #94a3b8;
font-size: 1.1rem;
text-align: center;
margin-top: 80px;
line-height: 1.6;
font-weight: 500;
}
.input-area {
padding: 24px 32px;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(20px);
border-top: 1px solid rgba(0, 0, 0, 0.06);
position: sticky;
bottom: 0;
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.05);
}
.input-container {
max-width: 1200px;
margin: 0 auto;
width: 100%;
display: flex;
gap: 20px;
}
textarea {
flex: 1;
min-height: 64px;
max-height: 200px;
padding: 18px 24px;
border: 2px solid #e2e8f0;
border-radius: 16px;
resize: none; /* 禁用调整大小功能 */
font-size: 1rem;
line-height: 1.6;
transition: all 0.2s ease;
background-color: #ffffff;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.03);
}
textarea:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.15);
}
textarea::placeholder {
color: #94a3b8;
font-weight: 500;
}
.send-button {
padding: 0 32px;
background: linear-gradient(135deg, #2563eb 0%, #3b82f6 100%);
color: white;
border: none;
border-radius: 16px;
cursor: pointer;
font-weight: 600;
font-size: 1rem;
display: flex;
align-items: center;
gap: 10px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.2);
}
.send-button:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(37, 99, 235, 0.25);
background: linear-gradient(135deg, #1d4ed8 0%, #2563eb 100%);
}
.send-button:active:not(:disabled) {
transform: translateY(0);
box-shadow: 0 2px 8px rgba(37, 99, 235, 0.2);
}
.send-button:disabled {
background: linear-gradient(135deg, #94a3b8 0%, #cbd5e1 100%);
cursor: not-allowed;
transform: none;
box-shadow: none;
}
/* Markdown 样式优化 */
.markdown-body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-size: 1.05rem;
line-height: 1.8;
color: #334155;
}
.markdown-body :deep(pre) {
background-color: #f8fafc;
border-radius: 16px;
padding: 24px;
overflow-x: auto;
border: 1px solid #e2e8f0;
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.05);
}
.markdown-body :deep(code) {
font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 0.95em;
background-color: #f1f5f9;
padding: 0.2em 0.4em;
border-radius: 6px;
color: #2563eb;
}
.markdown-body :deep(pre code) {
background-color: transparent;
padding: 0;
border-radius: 0;
color: #334155;
}
.markdown-body :deep(blockquote) {
margin: 2em 0;
padding: 1em 1.5em;
border-left: 4px solid #3b82f6;
background-color: #f8fafc;
border-radius: 0 16px 16px 0;
color: #475569;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.03);
}
/* 加载动画优化 */
.loading-spinner {
width: 20px;
height: 20px;
border: 3px solid rgba(255, 255, 255, 0.9);
border-top-color: transparent;
border-radius: 50%;
animation: spin 0.8s linear infinite;
display: inline-block;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* 响应式设计优化 */
@media (max-width: 768px) {
.messages-container {
padding: 20px;
}
.answer-area {
padding: 24px;
}
.input-area {
padding: 16px 20px;
}
textarea {
padding: 16px 20px;
font-size: 16px;
}
.send-button {
padding: 0 24px;
}
}
/* 思考中的样式 */
.thinking {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
font-size: 1.2rem;
color: #3b82f6;
}
.thinking-spinner {
width: 16px;
height: 16px;
border: 2.5px solid #3b82f6;
border-top-color: transparent;
border-radius: 50%;
animation: spin 0.8s linear infinite;
display: inline-block;
}
</style>

7
client/src/main.js Normal file
View File

@@ -0,0 +1,7 @@
import {createApp} from 'vue'
import App from './App.vue'
import Markdown from 'vue3-markdown-it'
const app = createApp(App)
.use(Markdown)
.mount('#app')

21
client/vite.config.js Normal file
View File

@@ -0,0 +1,21 @@
import {
fileURLToPath,
URL,
} from 'node:url'
import {defineConfig} from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
vueDevTools(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
})

64
pom.xml Normal file
View File

@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://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.4.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.lanyuanxiaoyao</groupId>
<artifactId>ai-analysis</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>ai-analysis</name>
<description>ai-analysis</description>
<properties>
<java.version>17</java.version>
<spring-ai.version>1.0.0-M6</spring-ai.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.32</version>
</dependency>
<!-- <dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-qdrant-store-spring-boot-starter</artifactId>
</dependency>-->
</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>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,11 @@
package com.lanyuanxiaoyao.ai.analysis;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class AnalysisApplication {
public static void main(String[] args) {
SpringApplication.run(AnalysisApplication.class, args);
}
}

View File

@@ -0,0 +1,43 @@
package com.lanyuanxiaoyao.ai.analysis.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CrossOrigin;
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.ResponseBody;
import reactor.core.publisher.Flux;
@Slf4j
@CrossOrigin
@Controller
@RequestMapping("chat")
public class ChatController {
private final ChatClient client;
public ChatController(ChatClient.Builder builder) {
this.client = builder.build();
}
@ResponseBody
@PostMapping(value = "", produces = MediaType.TEXT_PLAIN_VALUE + ";charset=utf-8")
public String chat(@RequestBody String prompt) {
return client.prompt()
.system("始终在中文语境下回答")
.user(prompt)
.call()
.content();
}
@PostMapping(value = "stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE + ";charset=utf-8")
public Flux<String> chatStream(@RequestBody String prompt) {
return client.prompt()
.system("始终在中文语境下回答")
.user(prompt)
.stream()
.content();
}
}

View File

@@ -0,0 +1,12 @@
server:
port: 7891
spring:
application:
name: ai-analysis
ai:
openai:
base-url: https://api.deepseek.com
api-key: "sk-3e1935e3ffb64ab096384bca071e2841"
chat:
options:
model: "deepseek-chat"

5
test.http Normal file
View File

@@ -0,0 +1,5 @@
### Chat
POST http://localhost:7891/chat/stream
Content-Type: text/plain
你好