对接OpenAI模型并完善功能
2024年5月15日...大约 24 分钟
一、介绍
扩展本地自定义知识库的解析功能,新增 Git 仓库解析。用户输入 Git 仓库地址和账号密码,即可拉取代码并上传至知识库,随后便可基于此代码进行使用。
二、技术方案
采用Spring AI 框架实现对接 OpenAI ,优势在于能够以统一方式直接配置和使用各类大模型。对于未直接对接的大模型,可通过 one-api 配置转发,采用统一的 OpenAI 方式进行对接。
三、具体实现
1. 项目结构

2. 引入组件
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
3. 配置文件
spring:
ai:
vectorstore:
pgvector:
index-type: HNSW
distance-type: COSINE_DISTANCE
dimensions: 1536
batching-strategy: TOKEN_COUNT # Optional: Controls how documents are batched for embedding
max-document-batch-size: 10000 # Optional: Maximum number of documents per batch
ollama:
base-url: http://192.168.1.23:11434/
embedding:
options:
num-batch: 512
model: nomic-embed-text
openai:
base-url: https://new.crond.dev/v1
api-key: sk-qhRbQfwzsvkeiFNNXzShlmrdta7pGWMyBmWt05sbAOkW2wnI
embedding:
options:
model: text-embedding-ada-002
rag:
embedding: text-embedding-ada-002 #nomic-embed-text、text-embedding-ada-002
其中 rag
是扩展的向量模型,可以指定使用 OpenAi
的向量模型或者使用 Ollama DeepSeek
。
4. 多模型向量库
package cn.cactusli.lxf.rag.config;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.ai.ollama.OllamaEmbeddingModel;
import org.springframework.ai.ollama.api.OllamaApi;
import org.springframework.ai.ollama.api.OllamaOptions;
import org.springframework.ai.openai.OpenAiEmbeddingModel;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.ai.vectorstore.SimpleVectorStore;
import org.springframework.ai.vectorstore.pgvector.PgVectorStore;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import static org.springframework.ai.ollama.api.OllamaModel.NOMIC_EMBED_TEXT;
/**
* Package: cn.cactusli.lxf.rag.app.config
* Description:
*
* @Author 仙人球⁶ᴳ |
* @Date 2025/3/14 14:25
* @Github https://github.com/lixuanfengs
*/
@Configuration
public class OllamaConfig {
@Bean
public OllamaApi ollamaApi(@Value("${spring.ai.ollama.base-url}") String baseUrl) {
return new OllamaApi(baseUrl);
}
@Bean
public OpenAiApi openAiApi(@Value("${spring.ai.openai.base-url}") String baseUrl, @Value("${spring.ai.openai.api-key}") String apiKey) {
return OpenAiApi.builder().baseUrl(baseUrl)
.apiKey(apiKey)
.build();
}
@Bean
public OllamaChatModel ollamaChatModel(OllamaApi ollamaApi) {
return OllamaChatModel.builder()
.ollamaApi(ollamaApi)
.defaultOptions(
OllamaOptions.builder()
.temperature(0.9)
.build())
.build();
}
@Bean
public TokenTextSplitter tokenTextSplitter() {
return new TokenTextSplitter();
}
@Bean
public SimpleVectorStore simpleVectorStore(@Value("${spring.ai.rag.embedding}") String model, OllamaApi ollamaApi, OpenAiApi openAiApi) {
if ("nomic-embed-text".equalsIgnoreCase(model)) {
// 嵌入生成客户端指定使用"nomic-embed-text"这个模型来生成嵌入向量
OllamaEmbeddingModel embeddingModel = OllamaEmbeddingModel.builder()
.ollamaApi(ollamaApi)
.defaultOptions(
OllamaOptions.builder()
.model(NOMIC_EMBED_TEXT)
.build())
.build();
return SimpleVectorStore.builder(embeddingModel).build();
} else {
OpenAiEmbeddingModel openAiEmbeddingModel = new OpenAiEmbeddingModel(openAiApi);
return SimpleVectorStore.builder(openAiEmbeddingModel).build();
}
}
/**
* -- 删除旧的表(如果存在)
* DROP TABLE IF EXISTS public.vector_store_ollama_deepseek;
*
* -- 创建新的表,使用UUID作为主键
* CREATE TABLE public.vector_store_ollama_deepseek (
* id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
* content TEXT NOT NULL,
* metadata JSONB,
* embedding VECTOR(768)
* );
*
* SELECT * FROM vector_store_ollama_deepseek
*/
/**
* -- 删除旧的表(如果存在)
* DROP TABLE IF EXISTS public.vector_store_openai;
*
* -- 创建新的表,使用UUID作为主键
* CREATE TABLE public.vector_store_openai (
* id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
* content TEXT NOT NULL,
* metadata JSONB,
* embedding VECTOR(1536)
* );
*
* SELECT * FROM vector_store_openai
*/
@Bean
public PgVectorStore pgVectorStore(@Value("${spring.ai.rag.embedding}") String model, OllamaApi ollamaApi, OpenAiApi openAiApi, JdbcTemplate jdbcTemplate) {
if ("nomic-embed-text".equalsIgnoreCase(model)) {
OllamaEmbeddingModel embeddingModel = OllamaEmbeddingModel.builder()
// 如果 nomic-embed-text 和 deepseek-r1 不在同一个 ollama 中
.ollamaApi(new OllamaApi("http://192.168.1.23:11434/"))
.defaultOptions(
OllamaOptions.builder()
.model(NOMIC_EMBED_TEXT)
.build())
.build();
return PgVectorStore.builder(jdbcTemplate, embeddingModel).vectorTableName("vector_store_ollama_deepseek").build();
} else {
OpenAiEmbeddingModel openAiEmbeddingModel = new OpenAiEmbeddingModel(openAiApi);
return PgVectorStore.builder(jdbcTemplate, openAiEmbeddingModel)
.vectorTableName("vector_store_openai")
.build();
}
}
}
使用 OllamaConfig
直接配置出OpenAI、Ollama
两套模型,之后根据 spring.ai.rag.embedding
属性的不同,实现出不同的向量库。
5. OpenAI 接口封装
package cn.cactusli.lxf.rag.trigger.http;
import cn.cactusli.lxf.rag.api.IAiService;
import jakarta.annotation.Resource;
import org.springframework.ai.chat.messages.Message;
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.chat.prompt.SystemPromptTemplate;
import org.springframework.ai.document.Document;
import org.springframework.ai.ollama.api.OllamaOptions;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.pgvector.PgVectorStore;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* Package: cn.cactusli.lxf.rag.trigger.http
* Description:
*
* @Author 仙人球⁶ᴳ |
* @Date 2025/4/14 16:03
* @Github https://github.com/lixuanfengs
*/
@RestController
@CrossOrigin(origins = "*")
@RequestMapping("/api/v1/openai")
public class OpenAiController implements IAiService {
@Resource
private OpenAiChatModel openAiChatModel;
@Resource
private PgVectorStore pgVectorStore;
/**
* http://localhost:7080/api/v1/openai/generate?model=deepseek-r1:1.5b&message=你是?
*/
@GetMapping("generate")
@Override
public ChatResponse generate(@RequestParam("model") String model, @RequestParam("message") String message) {
return openAiChatModel.call(new Prompt(
message,
OllamaOptions.builder()
.model(model)
.build()
));
}
/**
* http://localhost:7080/api/v1/openai/generate_stream?model=deepseek-r1:1.5b&message=你是?
*/
@GetMapping("generate_stream")
@Override
public Flux<ChatResponse> generateStream(@RequestParam("model") String model, @RequestParam("message") String message) {
return openAiChatModel.stream(new Prompt(
message,
OllamaOptions.builder()
.model(model)
.build()
));
}
@GetMapping(value = "generate_stream_rag")
@Override
public Flux<ChatResponse> generateStreamRag(@RequestParam("model") String model, @RequestParam("ragTag") String ragTag, @RequestParam("message") String message) {
String SYSTEM_PROMPT = """
Use the information from the DOCUMENTS section to provide accurate answers but act as if you knew this information innately.
If unsure, simply state that you don't know.
Another thing you need to note is that your reply must be in Chinese!
DOCUMENTS:
{documents}
""";
// 指定文档搜索
SearchRequest request = SearchRequest.builder()
.query(message)
.topK(5)
.filterExpression("cactusli == '" + ragTag + "'")
.build();
List<Document> documents = pgVectorStore.similaritySearch(request);
String documentCollectors = documents.stream().map(Document::getText).collect(Collectors.joining());
Message ragMessage = new SystemPromptTemplate(SYSTEM_PROMPT).createMessage(Map.of("documents", documentCollectors));
// 组装消息
ArrayList<Message> messages = new ArrayList<>();
messages.add(new UserMessage(message));
messages.add(ragMessage);
return openAiChatModel.stream(new Prompt(
messages,
OllamaOptions.builder()
.model(model)
.build()
));
}
}
通过接口的不同 /api/v1/ollma/
、/api/v1/openai/
区分出两套模型对接。
四、UI 对接
1. 对话页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AiRagKnowledge - By 仙人球🌵</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dompurify/dist/purify.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/highlight.js/highlight.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/highlight.js/styles/github.min.css">
<!-- Link to external CSS if you have one, otherwise styles are below -->
<!-- <link rel="stylesheet" href="css/index.css"> -->
<style>
/* Dropdown Menu Animation */
.dropdown-menu {
transform-origin: top right;
transform: scale(0.95);
opacity: 0;
visibility: hidden;
transition: opacity 0.1s, transform 0.1s, visibility 0.1s;
}
.dropdown-menu.active {
transform: scale(1);
opacity: 1;
visibility: visible;
}
/* Custom Scrollbar */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: #f1f1f1; }
::-webkit-scrollbar-thumb { background: #ccc; border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: #aaa; }
/* Message Bubble Animation */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.message-bubble {
animation: fadeIn 0.3s ease-out forwards;
}
/* Status Indicator Animation */
@keyframes pulse {
0% { transform: scale(0.95); opacity: 0.7; }
50% { transform: scale(1.05); opacity: 1; }
100% { transform: scale(0.95); opacity: 0.7; }
}
.status-indicator { animation: pulse 2s infinite; }
/* --- Markdown Body Styles --- */
.markdown-body {
line-height: 1.6;
}
.markdown-body > *:first-child { margin-top: 0 !important; }
.markdown-body > *:last-child { margin-bottom: 0 !important; }
.markdown-body h1, .markdown-body h2, .markdown-body h3, .markdown-body h4, .markdown-body h5, .markdown-body h6 {
margin-top: 1.2em; margin-bottom: 0.6em; font-weight: 600; line-height: 1.25;
}
.markdown-body h1 { font-size: 1.8em; border-bottom: 1px solid #eee; padding-bottom: 0.3em; }
.markdown-body h2 { font-size: 1.5em; border-bottom: 1px solid #eee; padding-bottom: 0.3em; }
.markdown-body h3 { font-size: 1.3em; }
.markdown-body h4 { font-size: 1.1em; }
.markdown-body h5 { font-size: 1em; }
.markdown-body h6 { font-size: 0.9em; color: #666; }
.markdown-body p { margin-bottom: 1em; }
.markdown-body ul, .markdown-body ol {
margin-left: 1.5em; margin-bottom: 1em; padding-left: 1.5em;
}
.markdown-body ul { list-style-type: disc; }
.markdown-body ol { list-style-type: decimal; }
.markdown-body li { margin-bottom: 0.4em; }
.markdown-body li > p { margin-bottom: 0.4em; }
.markdown-body li > ul, .markdown-body li > ol { margin-top: 0.4em; margin-bottom: 0.4em; }
.markdown-body blockquote {
margin: 1em 0; padding: 0.5em 1em; color: #6a737d;
border-left: 0.25em solid #dfe2e5; background-color: #f9f9f9;
}
.markdown-body blockquote > p { margin-bottom: 0; }
.markdown-body code {
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
font-size: 0.9em; background-color: rgba(27,31,35,0.07);
border-radius: 3px; padding: 0.2em 0.4em; margin: 0 2px;
}
.markdown-body pre { /* Container for code blocks */
margin-bottom: 1em; padding: 1em; overflow: auto;
border-radius: 6px; line-height: 1.45;
position: relative; /* Needed for copy button positioning */
/* Background color is often set by highlight.js theme */
background-color: #f6f8fa; /* Default fallback */
}
.markdown-body pre code { /* Actual code inside the block */
background-color: transparent; padding: 0; margin: 0;
font-size: 0.9em; border-radius: 0; line-height: inherit;
word-wrap: normal; white-space: pre; overflow: visible; /* Ensure code flows */
}
/* Highlight.js specific styling adjustment if needed */
.markdown-body pre code.hljs {
display: block; /* Or inline-block depending on desired layout */
padding: 0; /* Reset padding if hljs adds it */
background: none; /* Ensure theme background from 'pre' is used */
}
.markdown-body hr {
border: 0; height: 0.25em; padding: 0; margin: 1.5em 0; background-color: #e1e4e8;
}
.markdown-body table {
border-collapse: collapse; margin-bottom: 1em; width: auto; display: block; overflow-x: auto; /* Make tables scrollable */
}
.markdown-body th, .markdown-body td {
border: 1px solid #dfe2e5; padding: 0.6em 1em;
}
.markdown-body th { font-weight: 600; background-color: #f6f8fa; }
/* --- Copy Code Button --- */
.copy-code-button {
position: absolute; top: 8px; right: 8px; z-index: 10; /* Ensure button is clickable */
padding: 4px 8px; font-size: 12px;
background-color: #e2e8f0; color: #4a5568;
border: none; border-radius: 4px; cursor: pointer;
opacity: 0; /* Hidden by default */
transition: opacity 0.2s ease-in-out;
}
.markdown-body pre:hover .copy-code-button {
opacity: 1; /* Show on hover */
}
.copy-code-button:hover { background-color: #cbd5e0; }
.copy-code-button:active { background-color: #a0aec0; }
/* --- Chat Actions Visibility --- */
.chat-item:hover .chat-actions { opacity: 1; }
.chat-item .chat-actions { opacity: 0; transition: opacity 0.2s ease-in-out; }
/* Add these styles inside the <style> tag in index.html */
/* Style for the thinking process details */
.thinking-process summary {
user-select: none; /* Prevent text selection on summary */
}
.thinking-process summary::marker { /* Style the default disclosure triangle (optional) */
color: #9ca3af; /* gray-400 */
}
.thinking-process summary:focus {
outline: none; /* Remove default focus outline */
}
.thinking-steps-content {
max-height: 250px; /* Limit height */
overflow-y: auto; /* Add scroll if content exceeds max height */
background-color: #f9fafb; /* Slightly different background */
padding: 8px 12px; /* Add padding */
border-radius: 4px;
margin-top: 6px; /* Space below summary */
line-height: 1.5; /* Adjust line height for readability */
}
/* Ensure code blocks inside thinking steps are styled correctly */
.thinking-steps-content pre {
margin-bottom: 0.6em;
font-size: 0.875em; /* Slightly smaller */
/* background-color: #f3f4f6; */ /* Or use HLJS theme */
}
.thinking-steps-content code:not(pre code) { /* Inline code */
font-size: 0.875em;
background-color: rgba(27,31,35,0.07);
padding: 0.15em 0.3em;
border-radius: 3px;
}
.thinking-steps-content p {
margin-bottom: 0.5em;
}
/* Style for the final answer section (when separate) */
.final-answer {
margin-top: 0.5rem; /* Add some space above the final answer */
}
/* Hide the default marker for Webkit browsers if using custom indicator */
.thinking-process summary::-webkit-details-marker {
display: none;
}
/* Simple custom indicator (optional) */
.thinking-process summary {
position: relative;
padding-left: 1.3em; /* Make space for custom indicator */
}
.thinking-process summary::before {
content: '▶';
position: absolute;
left: 2px;
top: 1px; /* Adjust vertical alignment */
font-size: 0.8em;
color: #6b7280; /* gray-500 */
transition: transform 0.2s ease-in-out;
transform-origin: center;
display: inline-block; /* Ensure transform works */
}
.thinking-process[open] > summary::before {
transform: rotate(90deg);
}
</style>
</head>
<body class="h-screen flex flex-col bg-gray-50 text-gray-800">
<!-- Top Navigation -->
<nav class="border-b bg-white shadow-sm px-4 py-2 flex items-center gap-3 flex-shrink-0">
<button id="toggleSidebar" class="p-2 text-gray-500 hover:bg-gray-100 hover:text-gray-700 rounded-lg transition-colors duration-200 md:hidden"> <!-- Hide on medium screens and up -->
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path id="sidebarIconPath" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
<button id="newChatBtn" class="flex items-center gap-2 px-3 py-2 bg-blue-50 text-blue-600 hover:bg-blue-100 rounded-lg transition-colors duration-200">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
<span class="font-medium text-sm">新聊天</span>
</button>
<div class="flex-1 flex items-center gap-4 px-2">
<div class="flex flex-col w-64">
<label for="aiModel" class="text-xs text-gray-500 mb-1 pl-1">AI 模型</label>
<select id="aiModel" class="text-sm px-3 py-2 border border-gray-300 rounded-lg bg-white hover:border-blue-400 focus:border-blue-500 focus:ring focus:ring-blue-200 focus:ring-opacity-50 transition-all duration-200">
<option value="ollama" model="deepseek-r1:70b">deepseek-r1:70b</option>
<option value="openai" model="gpt-4o-mini">gpt-4o-mini</option>
<!-- Add more models as needed -->
</select>
</div>
<div class="flex flex-col w-64">
<label for="ragSelect" class="text-xs text-gray-500 mb-1 pl-1">知识库</label>
<select id="ragSelect" class="text-sm px-3 py-2 border border-gray-300 rounded-lg bg-white hover:border-blue-400 focus:border-blue-500 focus:ring focus:ring-blue-200 focus:ring-opacity-50 transition-all duration-200">
<option value="">选择一个知识库 (可选)</option>
<!-- Options will be populated by JS -->
</select>
</div>
</div>
<div class="flex items-center">
<div class="relative">
<button id="uploadMenuButton" class="px-4 py-2 bg-green-50 text-green-600 hover:bg-green-100 rounded-lg flex items-center gap-2 transition-colors duration-200">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg>
<span class="font-medium text-sm">上传知识</span>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</button>
<!-- Dropdown Menu -->
<div class="dropdown-menu hidden absolute right-0 mt-2 w-56 bg-white border rounded-md shadow-lg z-50" id="uploadMenu">
<a href="upload.html" target="_blank" class="flex items-center gap-2 px-4 py-3 text-sm text-gray-700 hover:bg-gray-50 hover:text-blue-600 transition-colors duration-150">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /></svg>
<span>上传文件</span>
</a>
<a href="git.html" target="_blank" class="flex items-center gap-2 px-4 py-3 text-sm text-gray-700 hover:bg-gray-50 hover:text-blue-600 transition-colors duration-150">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>
<span>解析仓库 (Git)</span>
</a>
</div>
</div>
</div>
</nav>
<div class="flex-1 flex overflow-hidden">
<!-- Sidebar -->
<aside id="sidebar" class="w-72 bg-white border-r shadow-inner flex-shrink-0 overflow-y-auto transition-transform duration-300 ease-in-out md:translate-x-0">
<!-- Sidebar Content -->
<div class="p-4">
<h2 class="font-semibold mb-4 text-base flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-blue-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" /></svg>
聊天列表
</h2>
<!-- Search Input (Optional) -->
<!--
<div class="relative mb-4">
<input type="text" placeholder="搜索聊天..." class="w-full px-3 py-2 border border-gray-300 rounded-lg pl-9 focus:outline-none focus:border-blue-500 focus:ring focus:ring-blue-200 focus:ring-opacity-50 transition-all duration-200 text-sm">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-gray-400 absolute left-3 top-1/2 transform -translate-y-1/2" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>
</div>
-->
<ul id="chatList" class="space-y-1">
<!-- Chat list items will be populated by JS -->
<!-- Example Structure (JS will generate this):
<li class="chat-item flex items-center justify-between p-2 hover:bg-gray-100 rounded-lg cursor-pointer transition-colors duration-150">
<div class="flex-1 overflow-hidden pr-2" onclick="loadChat('chatId')">
<div class="text-sm font-medium text-gray-800 truncate">聊天名称</div>
<div class="text-xs text-gray-500 truncate">日期 或 最新消息预览</div>
</div>
<div class="chat-actions flex items-center gap-1 flex-shrink-0">
<button class="p-1 hover:bg-gray-200 rounded text-gray-500" title="重命名" onclick="event.stopPropagation(); renameChat('chatId')">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>
</button>
<button class="p-1 hover:bg-red-100 rounded text-red-500 hover:text-red-600" title="删除" onclick="event.stopPropagation(); deleteChat('chatId')">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
</button>
</div>
</li>
-->
</ul>
</div>
</aside>
<!-- Main Content Area -->
<div class="flex-1 flex flex-col overflow-hidden bg-gray-50">
<!-- Chat Area -->
<main class="flex-1 overflow-y-auto p-4 md:p-6" id="chatArea">
</main>
<!-- Welcome Message (shown when chat is empty) -->
<div id="welcomeMessage" class="flex items-center justify-center h-full">
<div class="bg-white p-8 rounded-xl shadow-md text-center max-w-md">
<div class="mb-6">
<div class="w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center mx-auto mb-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-blue-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 14.042 3 12.574 3 11c0-4.418 4.03-8 9-8s9 3.582 9 8z" /></svg>
</div>
<h2 class="text-xl font-bold mb-2 text-gray-800">欢迎使用 AiRagKnowledge</h2>
<p class="text-gray-500 mb-4">一款智能的知识库聊天助手</p>
</div>
<div class="flex items-center gap-2 justify-center text-gray-600 mb-4 bg-green-50 py-2 px-3 rounded-lg">
<span class="w-2 h-2 bg-green-500 rounded-full status-indicator"></span>
Ollama 正在运行 🐏
</div>
<!-- <div class="grid grid-cols-2 gap-3 mt-6">-->
<!-- <button onclick="newChatBtn.click()" class="flex flex-col items-center bg-blue-50 hover:bg-blue-100 text-blue-600 p-4 rounded-lg transition-colors duration-200">-->
<!-- <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 mb-2" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" /></svg>-->
<!-- <span class="text-sm font-medium">新建聊天</span>-->
<!-- </button>-->
<!-- <button onclick="document.getElementById('uploadMenuButton').click()" class="flex flex-col items-center bg-green-50 hover:bg-green-100 text-green-600 p-4 rounded-lg transition-colors duration-200">-->
<!-- <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 mb-2" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" /></svg>-->
<!-- <span class="text-sm font-medium">上传知识</span>-->
<!-- </button>-->
<!-- </div>-->
</div>
</div>
<!-- Chat messages will be appended here by JS -->
<!-- Input Area -->
<div class="p-4 bg-white border-t flex-shrink-0">
<div class="max-w-4xl mx-auto">
<div class="border border-gray-300 rounded-xl bg-white shadow-sm focus-within:ring-2 focus-within:ring-blue-300 focus-within:border-blue-400 transition-all duration-200">
<div class="flex flex-col">
<textarea
id="messageInput"
class="w-full px-4 py-3 text-sm min-h-[80px] max-h-[250px] focus:outline-none resize-none rounded-t-xl bg-transparent"
placeholder="输入消息... (Shift+Enter 换行)"
></textarea>
<div class="flex items-center justify-between px-4 py-2 border-t bg-gray-50 rounded-b-xl">
<div class="flex items-center gap-3">
<button class="p-2 hover:bg-gray-200 rounded-lg text-gray-500 hover:text-gray-700 transition-colors duration-150" title="上传文件 (暂未实现)">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13" /></svg>
</button>
<!-- <button class="p-2 hover:bg-gray-200 rounded-lg text-gray-500 hover:text-gray-700 transition-colors duration-150" title="表情 (暂未实现)">-->
<!-- <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.828 14.828a4 4 0 01-5.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>-->
<!-- </button>-->
</div>
<button
id="submitBtn"
class="px-5 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-300 focus:ring-offset-1 transition-all duration-200 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
> <!-- Add disabled styles -->
<span class="font-medium text-sm">发送</span>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" /> <!-- Changed icon -->
</svg>
</button>
</div>
</div>
</div>
<p class="text-xs text-gray-400 mt-2 text-center">由 🌵仙人球 提供技术支持</p>
</div>
</div>
</div>
</div>
<script src="js/index.js"></script>
</body>
</html>
2. 上传知识 - 文件
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>文件上传 - AiRagKnowledge</title>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="https://cdn.tailwindcss.com"></script>
<style>
/* 加载动画 */
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loader {
animation: spin 1.5s linear infinite;
}
/* 拖放区域高亮 */
.drag-over {
background-color: rgba(59, 130, 246, 0.1);
border-color: rgba(59, 130, 246, 0.5);
}
/* 文件项目动画 */
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.file-item {
animation: slideIn 0.3s ease-out forwards;
}
/* 自定义滚动条 */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: #ccc;
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: #aaa;
}
/* 按钮点击反馈 */
.btn-active {
transform: scale(0.98);
}
</style>
</head>
<body class="flex justify-center items-center min-h-screen bg-gradient-to-br from-gray-50 to-blue-50">
<!-- 页面头部 -->
<div class="fixed top-0 left-0 right-0 border-b shadow-sm bg-white px-6 py-3 flex items-center justify-between">
<div class="flex items-center gap-3">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-blue-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg>
<h1 class="text-lg font-semibold">文件上传 - AiRagKnowledge</h1>
</div>
<a href="index.html" class="text-blue-600 hover:text-blue-700 transition-colors duration-150 flex items-center gap-1 text-sm">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
</svg>
返回主页
</a>
</div>
<!-- 上传文件卡片 -->
<div class="bg-white p-8 rounded-xl shadow-lg w-full max-w-md relative mt-16">
<!-- 加载遮罩层 -->
<div id="loadingOverlay" class="hidden absolute inset-0 bg-white bg-opacity-95 flex flex-col items-center justify-center rounded-xl z-50">
<div class="loader mb-4">
<svg class="h-12 w-12 text-blue-600" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2V6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<path d="M12 18V22" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<path d="M4.93 4.93L7.76 7.76" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<path d="M16.24 16.24L19.07 19.07" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<path d="M2 12H6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<path d="M18 12H22" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<path d="M4.93 19.07L7.76 16.24" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<path d="M16.24 7.76L19.07 4.93" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
</div>
<p class="text-gray-700 font-medium">文件处理中,请稍候...</p>
<p class="text-gray-500 text-sm mt-2">正在解析文件内容并建立知识索引</p>
<div class="w-48 h-2 bg-gray-200 rounded-full mt-4 overflow-hidden">
<div id="progressBar" class="h-full bg-blue-500 rounded-full" style="width: 0%"></div>
</div>
</div>
<!-- 表单头部 -->
<div class="text-center mb-6">
<div class="inline-flex items-center justify-center w-16 h-16 bg-blue-100 rounded-full mb-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-blue-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
<h2 class="text-2xl font-bold text-gray-800">添加知识库</h2>
<p class="text-gray-500 mt-1">上传文件并添加到您的知识库中</p>
</div>
<form id="uploadForm" class="space-y-5" enctype="multipart/form-data">
<!-- 知识标题输入 -->
<div>
<label for="title" class="block text-sm font-medium text-gray-700 mb-1">知识库名称</label>
<input
type="text"
id="title"
name="title"
class="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-300 focus:border-blue-500 transition-all duration-200"
placeholder="为这组知识命名(例如:产品手册)"
required
/>
</div>
<!-- 上传文件区域 -->
<div>
<label for="file" class="block text-sm font-medium text-gray-700 mb-1">上传文件</label>
<div id="dropArea" class="mt-1 border-dashed border-2 border-gray-300 rounded-lg p-6 text-center transition-all duration-200 hover:bg-gray-50">
<input type="file" id="file" name="file" accept=".pdf,.csv,.txt,.md,.sql,.java" class="hidden" multiple />
<label for="file" class="cursor-pointer block">
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 text-gray-400 mx-auto mb-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg>
<span class="text-blue-600 font-medium">点击选择文件</span>
<span class="text-gray-500"> 或将文件拖放到此处</span>
<p class="mt-2 text-sm text-gray-500">支持 PDF, CSV, TXT, MD, SQL, JAVA 等格式</p>
</label>
</div>
</div>
<!-- 待上传文件列表 -->
<div id="fileListContainer" class="hidden">
<h3 class="text-sm font-medium text-gray-700 mb-2 flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg>
已选择的文件
</h3>
<div class="max-h-40 overflow-y-auto bg-gray-50 rounded-lg">
<ul id="fileList" class="divide-y divide-gray-200"></ul>
</div>
</div>
<!-- 提交按钮 -->
<div class="pt-2">
<button
type="submit"
id="submitBtn"
class="w-full bg-blue-600 text-white py-3 px-4 rounded-lg hover:bg-blue-700 focus:ring-4 focus:ring-blue-300 focus:ring-opacity-50 font-medium transition-all duration-200 flex items-center justify-center gap-2"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
</svg>
上传并创建知识库
</button>
</div>
</form>
<!-- 帮助文本 -->
<div class="mt-6 text-center text-sm text-gray-500 space-y-1">
<p>通过上传文件,AI 将能够基于这些知识回答问题</p>
<p>文件将被索引并用于检索相关信息</p>
</div>
</div>
<script>
const fileListElement = document.getElementById('fileList');
const fileListContainer = document.getElementById('fileListContainer');
const dropArea = document.getElementById('dropArea');
const fileInput = document.getElementById('file');
const loadingOverlay = document.getElementById('loadingOverlay');
const progressBar = document.getElementById('progressBar');
const submitBtn = document.getElementById('submitBtn');
// 随机进度模拟函数
function simulateProgress() {
let progress = 0;
const interval = setInterval(() => {
progress += Math.random() * 15;
if (progress > 90) {
progress = 90; // 最高到90%,剩下的10%留给完成时
clearInterval(interval);
}
progressBar.style.width = `${progress}%`;
}, 300);
return interval;
}
// 拖放相关事件
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
dropArea.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
['dragenter', 'dragover'].forEach(eventName => {
dropArea.addEventListener(eventName, highlight, false);
});
['dragleave', 'drop'].forEach(eventName => {
dropArea.addEventListener(eventName, unhighlight, false);
});
function highlight() {
dropArea.classList.add('drag-over');
}
function unhighlight() {
dropArea.classList.remove('drag-over');
}
// 处理拖放文件
dropArea.addEventListener('drop', handleDrop, false);
function handleDrop(e) {
const dt = e.dataTransfer;
const files = dt.files;
fileInput.files = files;
// 触发change事件
const event = new Event('change');
fileInput.dispatchEvent(event);
}
// 文件选择变更处理
fileInput.addEventListener('change', function (e) {
const files = Array.from(e.target.files);
fileListElement.innerHTML = ''; // 清空列表
if (files.length > 0) {
fileListContainer.classList.remove('hidden');
files.forEach((file, index) => {
// 获取文件图标
let fileIcon = getFileIcon(file.name);
// 获取文件大小
let fileSize = formatFileSize(file.size);
const listItem = document.createElement('li');
listItem.className = 'py-2 px-3 flex justify-between items-center file-item hover:bg-gray-100';
listItem.innerHTML = `
<div class="flex items-center gap-3 text-gray-700">
${fileIcon}
<div>
<div class="font-medium">${file.name}</div>
<div class="text-xs text-gray-500">${fileSize}</div>
</div>
</div>
<button type="button" class="text-gray-500 hover:text-red-500 transition-colors duration-150" onclick="removeFile(${index})">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
`;
fileListElement.appendChild(listItem);
});
} else {
fileListContainer.classList.add('hidden');
}
});
// 根据文件类型获取图标
function getFileIcon(fileName) {
const extension = fileName.split('.').pop().toLowerCase();
let iconColor = 'text-blue-500';
let iconPath = '';
switch(extension) {
case 'pdf':
iconColor = 'text-red-500';
iconPath = 'M14 3v4a1 1 0 001 1h4M17 21h-10a2 2 0 01-2-2V5a2 2 0 012-2h7l5 5v11a2 2 0 01-2 2z';
break;
case 'csv':
iconColor = 'text-green-500';
iconPath = 'M10 3v4a1 1 0 001 1h4M14 3v4a1 1 0 001 1h2m-2 3v4m0 4v.01M8 7v4m0 4v4';
break;
case 'txt':
case 'md':
iconColor = 'text-gray-500';
iconPath = 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z';
break;
case 'sql':
iconColor = 'text-purple-500';
iconPath = 'M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4';
break;
case 'java':
iconColor = 'text-orange-500';
iconPath = 'M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4';
break;
default:
iconPath = 'M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z';
}
return `<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 ${iconColor}" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="${iconPath}" />
</svg>`;
}
// 格式化文件大小
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
// 移除文件
function removeFile(index) {
const input = document.getElementById('file');
let files = Array.from(input.files);
files.splice(index, 1);
// 创建一个新的DataTransfer对象
const dataTransfer = new DataTransfer();
files.forEach(file => dataTransfer.items.add(file));
// 更新文件输入对象的文件列表
input.files = dataTransfer.files;
// 更新文件列表UI
const fileListItems = fileListElement.children;
if (fileListItems[index]) {
fileListItems[index].style.height = '0';
fileListItems[index].style.opacity = '0';
fileListItems[index].style.overflow = 'hidden';
fileListItems[index].style.padding = '0';
fileListItems[index].style.margin = '0';
fileListItems[index].style.transition = 'all 0.3s ease-out';
setTimeout(() => {
if (fileListItems[index]) {
fileListItems[index].remove();
}
// 如果没有文件了,隐藏文件列表容器
if (fileListElement.children.length === 0) {
fileListContainer.classList.add('hidden');
}
}, 300);
}
}
// 按钮点击效果
submitBtn.addEventListener('mousedown', function() {
this.classList.add('btn-active');
});
document.addEventListener('mouseup', function() {
submitBtn.classList.remove('btn-active');
});
// 提交事件处理
document.getElementById('uploadForm').addEventListener('submit', function (e) {
e.preventDefault();
const input = document.getElementById('file');
const files = Array.from(input.files);
if (files.length === 0) {
// 使用更友好的提示
dropArea.classList.add('border-red-300');
dropArea.classList.add('bg-red-50');
setTimeout(() => {
dropArea.classList.remove('border-red-300');
dropArea.classList.remove('bg-red-50');
}, 1500);
return;
}
// 显示加载状态
loadingOverlay.classList.remove('hidden');
// 开始模拟上传进度
const progressInterval = simulateProgress();
const formData = new FormData();
formData.append('ragTag', document.getElementById('title').value);
files.forEach(file => formData.append('file', file));
axios.post('http://localhost:7080/api/v1/rag/file/upload', formData)
.then(response => {
// 清除进度模拟
clearInterval(progressInterval);
if (response.data.code === '1000') {
// 显示100%完成
progressBar.style.width = '100%';
// 成功提示并关闭窗口
setTimeout(() => {
// 替换为更现代的成功提示
loadingOverlay.innerHTML = `
<div class="bg-green-100 rounded-full p-3 mb-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
</div>
<p class="text-gray-800 font-medium">上传成功!</p>
<p class="text-gray-500 text-sm mt-2">知识库已创建,窗口即将关闭</p>
`;
setTimeout(() => {
window.close();
}, 1500);
}, 500);
} else {
throw new Error(response.data.info || '上传失败');
}
})
.catch(error => {
// 清除进度模拟
clearInterval(progressInterval);
// 显示错误提示
loadingOverlay.innerHTML = `
<div class="bg-red-100 rounded-full p-3 mb-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
</div>
<p class="text-gray-800 font-medium">上传失败</p>
<p class="text-gray-500 text-sm mt-2">${error.message}</p>
<button id="closeErrorBtn" class="mt-4 px-4 py-2 bg-gray-200 hover:bg-gray-300 rounded-lg text-gray-700 transition-colors duration-150">返回</button>
`;
// 添加关闭错误提示的事件
document.getElementById('closeErrorBtn').addEventListener('click', function() {
loadingOverlay.classList.add('hidden');
});
});
});
</script>
</body>
</html>
3. 上传知识 - Git
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>解析仓库</title>
<style>
body {
font-family: 'Microsoft YaHei', '微软雅黑', sans-serif;
background-color: #f0f0f0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
}
.container {
background-color: white;
padding: 2rem;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
text-align: center;
width: 300px;
}
h1 {
color: #333;
margin-bottom: 1.5rem;
}
.form-group {
margin-bottom: 1rem;
}
input {
width: 100%;
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 1rem;
box-sizing: border-box;
}
button {
background-color: #1E90FF;
color: white;
border: none;
padding: 0.5rem 1rem;
font-size: 1rem;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s;
width: 100%;
}
button:hover {
background-color: #4169E1;
}
#status {
margin-top: 1rem;
font-weight: bold;
}
.overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000;
justify-content: center;
align-items: center;
}
.loading-spinner {
border: 5px solid #f3f3f3;
border-top: 5px solid #3498db;
border-radius: 50%;
width: 50px;
height: 50px;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div class="container">
<h1>上传Git仓库</h1>
<form id="uploadForm">
<div class="form-group">
<input type="text" id="repoUrl" placeholder="Git仓库地址" required>
</div>
<div class="form-group">
<input type="text" id="userName" placeholder="用户名" required>
</div>
<div class="form-group">
<input type="password" id="token" placeholder="密码/Token" required>
</div>
<button type="submit">提交</button>
</form>
<div id="status"></div>
</div>
<div class="overlay" id="loadingOverlay">
<div class="loading-spinner"></div>
</div>
<script>
const loadingOverlay = document.getElementById('loadingOverlay');
document.getElementById('uploadForm').addEventListener('submit', function(e) {
e.preventDefault();
const repoUrl = document.getElementById('repoUrl').value;
const userName = document.getElementById('userName').value;
const token = document.getElementById('token').value;
loadingOverlay.style.display = 'flex';
document.getElementById('status').textContent = '';
fetch('http://localhost:8090/api/v1/rag/analyze_git_repository', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `repoUrl=${encodeURIComponent(repoUrl)}&userName=${encodeURIComponent(userName)}&token=${encodeURIComponent(token)}`
})
.then(response => response.json())
.then(data => {
loadingOverlay.style.display = 'none';
if (data.code === '1000') {
document.getElementById('status').textContent = '上传成功';
// 成功提示并关闭窗口
setTimeout(() => {
alert('上传成功,窗口即将关闭');
window.close();
}, 500);
} else {
document.getElementById('status').textContent = '上传失败';
}
})
.catch(error => {
loadingOverlay.style.display = 'none';
document.getElementById('status').textContent = '上传仓库时出错';
});
});
</script>
</body>
</html>
五、效果演示
1. 对话页面


如图对话界面是通过 Gemini 2.5 Pro Preview 03-25 实现的,效果还是不错的吧!
2. 上传知识



3. 解析知识 - 后台日志


在聊天界面点击上传知识库后,就可以在后台日志中查看到解析日志。
赞助