1286 lines
45 KiB
HTML
1286 lines
45 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>问问ima - RAG AI助手</title>
|
||
<style>
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
|
||
background-color: #f5f5f5;
|
||
height: 100vh;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.container {
|
||
display: flex;
|
||
height: 100vh;
|
||
}
|
||
|
||
/* 左侧边栏 */
|
||
.sidebar {
|
||
width: 240px;
|
||
background-color: #ffffff;
|
||
border-right: 1px solid #e5e5e5;
|
||
display: flex;
|
||
flex-direction: column;
|
||
padding: 20px;
|
||
}
|
||
|
||
.logo-section {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
.logo {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-size: 20px;
|
||
font-weight: bold;
|
||
color: #000;
|
||
}
|
||
|
||
.panda-icon {
|
||
width: 24px;
|
||
height: 24px;
|
||
background: linear-gradient(135deg, #4ade80, #22c55e);
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: white;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.new-chat-btn {
|
||
width: 100%;
|
||
padding: 12px;
|
||
background: white;
|
||
border: 1px solid #e5e5e5;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 8px;
|
||
font-size: 14px;
|
||
color: #333;
|
||
transition: all 0.2s;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.new-chat-btn:hover {
|
||
background: #f5f5f5;
|
||
border-color: #d0d0d0;
|
||
}
|
||
|
||
.nav-links {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
.nav-link {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 10px 12px;
|
||
color: #666;
|
||
text-decoration: none;
|
||
border-radius: 6px;
|
||
transition: background 0.2s;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.nav-link:hover {
|
||
background: #f5f5f5;
|
||
}
|
||
|
||
.nav-link-icon {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.login-hint {
|
||
font-size: 12px;
|
||
color: #999;
|
||
margin-top: 20px;
|
||
text-align: center;
|
||
}
|
||
|
||
.about-link {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 10px 12px;
|
||
color: #666;
|
||
text-decoration: none;
|
||
border-radius: 6px;
|
||
transition: background 0.2s;
|
||
cursor: pointer;
|
||
margin-top: auto;
|
||
}
|
||
|
||
.about-link:hover {
|
||
background: #f5f5f5;
|
||
}
|
||
|
||
/* 主内容区 */
|
||
.main-content {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
background-color: #ffffff;
|
||
position: relative;
|
||
}
|
||
|
||
.chat-container {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding: 40px 20px 120px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.welcome-screen {
|
||
text-align: center;
|
||
max-width: 600px;
|
||
}
|
||
|
||
.panda-avatar {
|
||
width: 120px;
|
||
height: 120px;
|
||
margin: 0 auto 30px;
|
||
position: relative;
|
||
}
|
||
|
||
.panda-avatar-bg {
|
||
width: 100%;
|
||
height: 100%;
|
||
background: radial-gradient(circle, rgba(74, 222, 128, 0.2) 0%, rgba(34, 197, 94, 0.1) 50%, transparent 70%);
|
||
border-radius: 50%;
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
}
|
||
|
||
.panda-avatar-icon {
|
||
width: 80px;
|
||
height: 80px;
|
||
background: linear-gradient(135deg, #4ade80, #22c55e);
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: white;
|
||
font-size: 40px;
|
||
position: relative;
|
||
z-index: 1;
|
||
margin: 20px auto;
|
||
}
|
||
|
||
.welcome-title {
|
||
font-size: 32px;
|
||
font-weight: bold;
|
||
color: #000;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.welcome-desc {
|
||
font-size: 16px;
|
||
color: #666;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.messages {
|
||
width: 100%;
|
||
max-width: 800px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 20px;
|
||
}
|
||
|
||
.message {
|
||
display: flex;
|
||
gap: 12px;
|
||
padding: 16px;
|
||
border-radius: 12px;
|
||
}
|
||
|
||
.message.user {
|
||
flex-direction: row-reverse;
|
||
}
|
||
|
||
.message-avatar {
|
||
width: 36px;
|
||
height: 36px;
|
||
border-radius: 50%;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.message.user .message-avatar {
|
||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: white;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.message.assistant .message-avatar {
|
||
background: linear-gradient(135deg, #4ade80, #22c55e);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: white;
|
||
font-size: 20px;
|
||
}
|
||
|
||
.message-content {
|
||
flex: 1;
|
||
padding: 12px 16px;
|
||
background: #f5f5f5;
|
||
border-radius: 8px;
|
||
line-height: 1.6;
|
||
color: #333;
|
||
white-space: pre-wrap;
|
||
word-wrap: break-word;
|
||
}
|
||
|
||
.message.user .message-content {
|
||
background: #667eea;
|
||
color: white;
|
||
}
|
||
|
||
/* 输入区域 */
|
||
.input-area {
|
||
position: fixed;
|
||
bottom: 0;
|
||
left: 240px;
|
||
right: 0;
|
||
background: white;
|
||
border-top: 1px solid #e5e5e5;
|
||
padding: 16px 20px;
|
||
z-index: 100;
|
||
}
|
||
|
||
.input-controls {
|
||
display: flex;
|
||
gap: 8px;
|
||
margin-bottom: 12px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.control-select {
|
||
padding: 6px 12px;
|
||
border: 1px solid #e5e5e5;
|
||
border-radius: 6px;
|
||
background: white;
|
||
font-size: 13px;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.control-select:hover {
|
||
border-color: #d0d0d0;
|
||
background: #f9f9f9;
|
||
}
|
||
|
||
.input-wrapper {
|
||
display: flex;
|
||
align-items: flex-end;
|
||
gap: 8px;
|
||
background: #f5f5f5;
|
||
border-radius: 12px;
|
||
padding: 12px;
|
||
}
|
||
|
||
.file-input-wrapper {
|
||
position: relative;
|
||
}
|
||
|
||
.file-input {
|
||
position: absolute;
|
||
opacity: 0;
|
||
width: 0;
|
||
height: 0;
|
||
}
|
||
|
||
.file-btn, .send-btn {
|
||
width: 36px;
|
||
height: 36px;
|
||
border: none;
|
||
background: transparent;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 6px;
|
||
transition: background 0.2s;
|
||
color: #666;
|
||
}
|
||
|
||
.file-btn:hover, .send-btn:hover {
|
||
background: #e5e5e5;
|
||
}
|
||
|
||
.send-btn:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.text-input {
|
||
flex: 1;
|
||
border: none;
|
||
background: transparent;
|
||
font-size: 15px;
|
||
padding: 8px 12px;
|
||
resize: none;
|
||
max-height: 200px;
|
||
outline: none;
|
||
font-family: inherit;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
/* 知识库上传模态框 */
|
||
.modal {
|
||
display: none;
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
z-index: 1000;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.modal.active {
|
||
display: flex;
|
||
}
|
||
|
||
.modal-content {
|
||
background: white;
|
||
border-radius: 12px;
|
||
padding: 24px;
|
||
width: 90%;
|
||
max-width: 500px;
|
||
max-height: 80vh;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.modal-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.modal-title {
|
||
font-size: 18px;
|
||
font-weight: bold;
|
||
color: #000;
|
||
}
|
||
|
||
.close-btn {
|
||
width: 32px;
|
||
height: 32px;
|
||
border: none;
|
||
background: transparent;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 6px;
|
||
transition: background 0.2s;
|
||
font-size: 20px;
|
||
color: #666;
|
||
}
|
||
|
||
.close-btn:hover {
|
||
background: #f5f5f5;
|
||
}
|
||
|
||
.form-group {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.form-label {
|
||
display: block;
|
||
margin-bottom: 8px;
|
||
font-size: 14px;
|
||
color: #333;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.form-input {
|
||
width: 100%;
|
||
padding: 10px 12px;
|
||
border: 1px solid #e5e5e5;
|
||
border-radius: 6px;
|
||
font-size: 14px;
|
||
outline: none;
|
||
transition: border-color 0.2s;
|
||
}
|
||
|
||
.form-input:focus {
|
||
border-color: #4ade80;
|
||
}
|
||
|
||
.file-upload-area {
|
||
border: 2px dashed #e5e5e5;
|
||
border-radius: 8px;
|
||
padding: 24px;
|
||
text-align: center;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.file-upload-area:hover {
|
||
border-color: #4ade80;
|
||
background: #f0fdf4;
|
||
}
|
||
|
||
.file-upload-area.dragover {
|
||
border-color: #22c55e;
|
||
background: #f0fdf4;
|
||
}
|
||
|
||
.file-list {
|
||
margin-top: 12px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
.file-item {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 8px 12px;
|
||
background: #f5f5f5;
|
||
border-radius: 6px;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.remove-file {
|
||
color: #ef4444;
|
||
cursor: pointer;
|
||
padding: 4px 8px;
|
||
border-radius: 4px;
|
||
transition: background 0.2s;
|
||
}
|
||
|
||
.remove-file:hover {
|
||
background: #fee2e2;
|
||
}
|
||
|
||
.modal-actions {
|
||
display: flex;
|
||
gap: 12px;
|
||
justify-content: flex-end;
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.btn {
|
||
padding: 10px 20px;
|
||
border: none;
|
||
border-radius: 6px;
|
||
font-size: 14px;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.btn-primary {
|
||
background: #4ade80;
|
||
color: white;
|
||
}
|
||
|
||
.btn-primary:hover:not(:disabled) {
|
||
background: #22c55e;
|
||
}
|
||
|
||
.btn-secondary {
|
||
background: #f5f5f5;
|
||
color: #333;
|
||
}
|
||
|
||
.btn-secondary:hover {
|
||
background: #e5e5e5;
|
||
}
|
||
|
||
.btn:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.loading {
|
||
display: inline-block;
|
||
width: 16px;
|
||
height: 16px;
|
||
border: 2px solid #f3f3f3;
|
||
border-top: 2px solid #4ade80;
|
||
border-radius: 50%;
|
||
animation: spin 1s linear infinite;
|
||
}
|
||
|
||
@keyframes spin {
|
||
0% { transform: rotate(0deg); }
|
||
100% { transform: rotate(360deg); }
|
||
}
|
||
|
||
.error-message {
|
||
color: #ef4444;
|
||
font-size: 12px;
|
||
margin-top: 4px;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<!-- 左侧边栏 -->
|
||
<div class="sidebar">
|
||
<div class="logo-section">
|
||
<div class="logo">
|
||
<div class="panda-icon">🐼</div>
|
||
<span>ima</span>
|
||
</div>
|
||
</div>
|
||
<button class="new-chat-btn" onclick="newChat()">
|
||
<span>💬</span>
|
||
<span>新对话</span>
|
||
</button>
|
||
<div class="nav-links">
|
||
<div class="nav-link" onclick="openKnowledgeBaseModal()">
|
||
<div class="nav-link-icon">
|
||
<span>📍</span>
|
||
<span>我的知识库</span>
|
||
</div>
|
||
<span>→</span>
|
||
</div>
|
||
<div class="nav-link">
|
||
<div class="nav-link-icon">
|
||
<span>🌐</span>
|
||
<span>知识库广场</span>
|
||
</div>
|
||
<span>→</span>
|
||
</div>
|
||
<div class="nav-link">
|
||
<div class="nav-link-icon">
|
||
<span>🕐</span>
|
||
<span>问答历史</span>
|
||
</div>
|
||
<span>→</span>
|
||
</div>
|
||
</div>
|
||
<div class="login-hint">登录以同步历史会话</div>
|
||
<div class="about-link">
|
||
<div class="nav-link-icon">
|
||
<span>ℹ️</span>
|
||
<span>关于ima</span>
|
||
</div>
|
||
<span>→</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 主内容区 -->
|
||
<div class="main-content">
|
||
<div class="chat-container" id="chatContainer">
|
||
<div class="welcome-screen" id="welcomeScreen">
|
||
<div class="panda-avatar">
|
||
<div class="panda-avatar-bg"></div>
|
||
<div class="panda-avatar-icon">🐼</div>
|
||
</div>
|
||
<div class="welcome-title">问问ima</div>
|
||
<div class="welcome-desc">我可以基于全网或知识库内容,为你答疑解惑、创作内容</div>
|
||
</div>
|
||
<div class="messages" id="messages" style="display: none;"></div>
|
||
</div>
|
||
|
||
<!-- 输入区域 -->
|
||
<div class="input-area">
|
||
<div class="input-controls">
|
||
<select class="control-select" id="modelSelect">
|
||
<option value="deepseek-r1:7b">deepseek-r1:7b</option>
|
||
<option value="deepseek-r1:1.5b">deepseek-r1:1.5b</option>
|
||
</select>
|
||
<select class="control-select" id="knowledgeBaseSelect">
|
||
<option value="">选择知识库</option>
|
||
</select>
|
||
</div>
|
||
<div class="input-wrapper">
|
||
<div class="file-input-wrapper">
|
||
<input type="file" id="fileInput" class="file-input" multiple accept="*/*">
|
||
<button class="file-btn" onclick="document.getElementById('fileInput').click()" title="上传知识库">
|
||
📎
|
||
</button>
|
||
</div>
|
||
<textarea
|
||
class="text-input"
|
||
id="textInput"
|
||
placeholder="有问题尽管问"
|
||
rows="1"
|
||
onkeydown="handleKeyDown(event)"
|
||
oninput="autoResize(this)"
|
||
></textarea>
|
||
<button class="send-btn" id="sendBtn" onclick="sendMessage()" title="发送">
|
||
✈️
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 知识库上传模态框 -->
|
||
<div class="modal" id="uploadModal">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<div class="modal-title">上传知识库</div>
|
||
<button class="close-btn" onclick="closeUploadModal()">×</button>
|
||
</div>
|
||
<form id="uploadForm" onsubmit="uploadKnowledgeBase(event)">
|
||
<div class="form-group">
|
||
<label class="form-label">知识库名称 *</label>
|
||
<input
|
||
type="text"
|
||
class="form-input"
|
||
id="knowledgeBaseName"
|
||
placeholder="请输入知识库名称"
|
||
required
|
||
>
|
||
<div class="error-message" id="nameError"></div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">选择文件 *</label>
|
||
<div
|
||
class="file-upload-area"
|
||
id="fileUploadArea"
|
||
onclick="document.getElementById('uploadFileInput').click()"
|
||
ondrop="handleDrop(event)"
|
||
ondragover="handleDragOver(event)"
|
||
ondragleave="handleDragLeave(event)"
|
||
>
|
||
<div>📁 点击或拖拽文件到此处</div>
|
||
<div style="font-size: 12px; color: #999; margin-top: 8px;">支持多文件上传</div>
|
||
</div>
|
||
<input
|
||
type="file"
|
||
id="uploadFileInput"
|
||
class="file-input"
|
||
multiple
|
||
accept="*/*"
|
||
onchange="handleFileSelect(event)"
|
||
style="display: none;"
|
||
>
|
||
<div class="file-list" id="fileList"></div>
|
||
</div>
|
||
<div class="modal-actions">
|
||
<button type="button" class="btn btn-secondary" onclick="closeUploadModal()">取消</button>
|
||
<button type="submit" class="btn btn-primary" id="uploadBtn">
|
||
<span id="uploadBtnText">上传</span>
|
||
<span id="uploadLoading" class="loading" style="display: none; margin-left: 8px;"></span>
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
let selectedFiles = [];
|
||
let currentMessageId = null;
|
||
|
||
// 页面加载时获取知识库列表
|
||
window.addEventListener('DOMContentLoaded', () => {
|
||
loadKnowledgeBaseList();
|
||
// 测试后端连接
|
||
testBackendConnection();
|
||
});
|
||
|
||
// 测试后端连接
|
||
async function testBackendConnection() {
|
||
try {
|
||
console.log('========== 测试后端连接 ==========');
|
||
console.log('测试URL: http://localhost:8090/api/v1/rag/query_rag_tag_list');
|
||
const response = await fetch('http://localhost:8090/api/v1/rag/query_rag_tag_list', {
|
||
method: 'GET',
|
||
mode: 'cors'
|
||
});
|
||
console.log('✅ 后端连接测试成功,状态码:', response.status);
|
||
console.log('✅ CORS配置正常,可以正常通信');
|
||
} catch (error) {
|
||
console.error('❌ 后端连接测试失败');
|
||
console.error('错误:', error);
|
||
if (error.name === 'TypeError' && error.message.includes('Failed to fetch')) {
|
||
console.error('⚠️ 这是CORS跨域问题!');
|
||
console.error('后端需要配置CORS才能允许前端跨域请求');
|
||
console.error('请查看页面上的CORS配置说明');
|
||
}
|
||
}
|
||
}
|
||
|
||
// 加载知识库列表
|
||
async function loadKnowledgeBaseList() {
|
||
try {
|
||
const response = await fetch('http://localhost:8090/api/v1/rag/query_rag_tag_list');
|
||
|
||
if (!response.ok) {
|
||
console.error('加载知识库列表失败,HTTP状态:', response.status);
|
||
return;
|
||
}
|
||
|
||
const data = await response.json();
|
||
console.log('知识库列表响应:', data);
|
||
|
||
const select = document.getElementById('knowledgeBaseSelect');
|
||
select.innerHTML = '<option value="">选择知识库</option>';
|
||
|
||
if (data.code === '0000' && data.data) {
|
||
// 处理不同的数据格式
|
||
let tagList = [];
|
||
|
||
if (Array.isArray(data.data)) {
|
||
tagList = data.data;
|
||
} else if (data.data.tags && Array.isArray(data.data.tags)) {
|
||
tagList = data.data.tags;
|
||
} else if (data.data.list && Array.isArray(data.data.list)) {
|
||
tagList = data.data.list;
|
||
}
|
||
|
||
if (tagList.length > 0) {
|
||
tagList.forEach(item => {
|
||
const option = document.createElement('option');
|
||
// 如果是字符串,直接使用
|
||
if (typeof item === 'string') {
|
||
option.value = item;
|
||
option.textContent = item;
|
||
} else {
|
||
// 如果是对象,尝试获取tag或name字段
|
||
option.value = item.tag || item.name || item.id || String(item);
|
||
option.textContent = item.name || item.tag || item.label || String(item);
|
||
}
|
||
select.appendChild(option);
|
||
});
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('加载知识库列表失败:', error);
|
||
// 不显示错误提示,避免影响用户体验
|
||
}
|
||
}
|
||
|
||
// 新对话
|
||
function newChat() {
|
||
document.getElementById('messages').innerHTML = '';
|
||
document.getElementById('messages').style.display = 'none';
|
||
document.getElementById('welcomeScreen').style.display = 'block';
|
||
document.getElementById('textInput').value = '';
|
||
currentMessageId = null;
|
||
}
|
||
|
||
// 打开知识库上传模态框
|
||
function openKnowledgeBaseModal() {
|
||
document.getElementById('uploadModal').classList.add('active');
|
||
selectedFiles = [];
|
||
document.getElementById('fileList').innerHTML = '';
|
||
document.getElementById('knowledgeBaseName').value = '';
|
||
}
|
||
|
||
// 关闭上传模态框
|
||
function closeUploadModal() {
|
||
document.getElementById('uploadModal').classList.remove('active');
|
||
selectedFiles = [];
|
||
document.getElementById('fileList').innerHTML = '';
|
||
document.getElementById('knowledgeBaseName').value = '';
|
||
}
|
||
|
||
// 处理文件选择
|
||
function handleFileSelect(event) {
|
||
const files = Array.from(event.target.files);
|
||
addFiles(files);
|
||
}
|
||
|
||
// 处理拖拽
|
||
function handleDragOver(event) {
|
||
event.preventDefault();
|
||
event.stopPropagation();
|
||
event.currentTarget.classList.add('dragover');
|
||
}
|
||
|
||
function handleDragLeave(event) {
|
||
event.preventDefault();
|
||
event.stopPropagation();
|
||
event.currentTarget.classList.remove('dragover');
|
||
}
|
||
|
||
function handleDrop(event) {
|
||
event.preventDefault();
|
||
event.stopPropagation();
|
||
event.currentTarget.classList.remove('dragover');
|
||
const files = Array.from(event.dataTransfer.files);
|
||
addFiles(files);
|
||
}
|
||
|
||
// 添加文件到列表
|
||
function addFiles(files) {
|
||
files.forEach(file => {
|
||
if (!selectedFiles.find(f => f.name === file.name && f.size === file.size)) {
|
||
selectedFiles.push(file);
|
||
}
|
||
});
|
||
updateFileList();
|
||
}
|
||
|
||
// 更新文件列表显示
|
||
function updateFileList() {
|
||
const fileList = document.getElementById('fileList');
|
||
fileList.innerHTML = '';
|
||
|
||
selectedFiles.forEach((file, index) => {
|
||
const fileItem = document.createElement('div');
|
||
fileItem.className = 'file-item';
|
||
fileItem.innerHTML = `
|
||
<span>${file.name} (${formatFileSize(file.size)})</span>
|
||
<span class="remove-file" onclick="removeFile(${index})">删除</span>
|
||
`;
|
||
fileList.appendChild(fileItem);
|
||
});
|
||
}
|
||
|
||
// 移除文件
|
||
function removeFile(index) {
|
||
selectedFiles.splice(index, 1);
|
||
updateFileList();
|
||
}
|
||
|
||
// 格式化文件大小
|
||
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 Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
||
}
|
||
|
||
// 上传知识库
|
||
async function uploadKnowledgeBase(event) {
|
||
event.preventDefault();
|
||
|
||
const name = document.getElementById('knowledgeBaseName').value.trim();
|
||
const nameError = document.getElementById('nameError');
|
||
|
||
if (!name) {
|
||
nameError.textContent = '请输入知识库名称';
|
||
return;
|
||
}
|
||
nameError.textContent = '';
|
||
|
||
if (selectedFiles.length === 0) {
|
||
alert('请至少选择一个文件');
|
||
return;
|
||
}
|
||
|
||
const uploadBtn = document.getElementById('uploadBtn');
|
||
const uploadBtnText = document.getElementById('uploadBtnText');
|
||
const uploadLoading = document.getElementById('uploadLoading');
|
||
|
||
uploadBtn.disabled = true;
|
||
uploadBtnText.textContent = '上传中...';
|
||
uploadLoading.style.display = 'inline-block';
|
||
|
||
try {
|
||
const formData = new FormData();
|
||
formData.append('ragTag', name);
|
||
|
||
// 添加所有文件,使用相同的参数名 'file' 以匹配后端的 List<MultipartFile> files
|
||
selectedFiles.forEach((file, index) => {
|
||
formData.append('file', file);
|
||
console.log(`添加文件 ${index + 1}:`, file.name, '大小:', file.size, '类型:', file.type);
|
||
});
|
||
|
||
// 验证FormData内容
|
||
console.log('FormData内容:');
|
||
for (let pair of formData.entries()) {
|
||
if (pair[1] instanceof File) {
|
||
console.log(pair[0] + ':', pair[1].name, pair[1].size, 'bytes');
|
||
} else {
|
||
console.log(pair[0] + ':', pair[1]);
|
||
}
|
||
}
|
||
|
||
console.log('========== 开始上传请求 ==========');
|
||
console.log('知识库名称:', name);
|
||
console.log('文件数量:', selectedFiles.length);
|
||
console.log('请求URL: http://localhost:8090/api/v1/rag/file/upload');
|
||
console.log('请求方法: POST');
|
||
console.log('FormData参数:');
|
||
console.log(' - ragTag:', name);
|
||
selectedFiles.forEach((file, i) => {
|
||
console.log(` - file[${i}]:`, file.name, `(${file.size} bytes, ${file.type || 'unknown type'})`);
|
||
});
|
||
|
||
// 发送请求
|
||
const startTime = Date.now();
|
||
console.log('发送请求时间:', new Date().toISOString());
|
||
|
||
const response = await fetch('http://localhost:8090/api/v1/rag/file/upload', {
|
||
method: 'POST',
|
||
// 不设置 Content-Type,让浏览器自动设置 multipart/form-data 和 boundary
|
||
body: formData,
|
||
// 添加mode和credentials以处理CORS
|
||
mode: 'cors',
|
||
credentials: 'omit'
|
||
});
|
||
|
||
const endTime = Date.now();
|
||
console.log('收到响应时间:', new Date().toISOString());
|
||
console.log('请求耗时:', (endTime - startTime) + 'ms');
|
||
console.log('========== 响应信息 ==========');
|
||
|
||
console.log('收到响应,状态码:', response.status, response.statusText);
|
||
console.log('响应头:', Object.fromEntries(response.headers.entries()));
|
||
|
||
// 检查响应内容类型
|
||
const contentType = response.headers.get('content-type');
|
||
let result;
|
||
|
||
if (contentType && contentType.includes('application/json')) {
|
||
result = await response.json();
|
||
} else {
|
||
// 如果不是JSON,尝试读取文本
|
||
const text = await response.text();
|
||
console.log('响应文本:', text);
|
||
try {
|
||
result = JSON.parse(text);
|
||
} catch (e) {
|
||
// 如果解析失败,创建一个结果对象
|
||
result = {
|
||
code: response.ok ? '0000' : '9999',
|
||
info: text || '未知错误',
|
||
message: text || '未知错误'
|
||
};
|
||
}
|
||
}
|
||
|
||
console.log('响应结果:', result);
|
||
|
||
if (result.code === '0000' || response.ok) {
|
||
alert('上传成功!');
|
||
closeUploadModal();
|
||
// 延迟一下再加载列表,确保后端已处理完成
|
||
setTimeout(() => {
|
||
loadKnowledgeBaseList();
|
||
}, 500);
|
||
} else {
|
||
const errorMsg = result.info || result.message || result.error || '未知错误';
|
||
alert('上传失败: ' + errorMsg);
|
||
console.error('上传失败详情:', result);
|
||
}
|
||
} catch (error) {
|
||
console.error('========== 上传失败 ==========');
|
||
console.error('完整错误信息:', error);
|
||
console.error('错误类型:', error.name);
|
||
console.error('错误消息:', error.message);
|
||
console.error('错误堆栈:', error.stack);
|
||
|
||
let errorMessage = '';
|
||
let isCorsError = false;
|
||
|
||
if (error.name === 'TypeError' && error.message.includes('Failed to fetch')) {
|
||
isCorsError = true;
|
||
errorMessage = '❌ 请求被阻止:CORS跨域问题\n\n';
|
||
errorMessage += '🔍 问题诊断:\n';
|
||
errorMessage += '请求没有到达后端服务器,被浏览器CORS策略阻止。\n\n';
|
||
errorMessage += '✅ 解决方案:\n';
|
||
errorMessage += '后端需要添加CORS配置,允许前端跨域请求。\n\n';
|
||
errorMessage += '📝 后端配置示例(Spring Boot):\n';
|
||
errorMessage += '在Controller类或方法上添加:\n';
|
||
errorMessage += '@CrossOrigin(origins = "*")\n';
|
||
errorMessage += '或配置全局CORS:\n';
|
||
errorMessage += '@Configuration\n';
|
||
errorMessage += 'public class CorsConfig implements WebMvcConfigurer {\n';
|
||
errorMessage += ' @Override\n';
|
||
errorMessage += ' public void addCorsMappings(CorsRegistry registry) {\n';
|
||
errorMessage += ' registry.addMapping("/**")\n';
|
||
errorMessage += ' .allowedOrigins("*")\n';
|
||
errorMessage += ' .allowedMethods("*")\n';
|
||
errorMessage += ' .allowedHeaders("*");\n';
|
||
errorMessage += ' }\n';
|
||
errorMessage += '}\n\n';
|
||
errorMessage += '⚠️ 注意:生产环境应指定具体的前端域名,不要使用 "*"';
|
||
} else if (error.name === 'TypeError' && error.message.includes('fetch')) {
|
||
errorMessage = '网络请求失败\n\n请检查:\n1. 后端服务是否运行在 http://localhost:8090\n2. 网络连接是否正常';
|
||
} else if (error.message) {
|
||
errorMessage = '上传失败: ' + error.message;
|
||
} else {
|
||
errorMessage = '未知错误,请查看控制台(F12)获取详细信息';
|
||
}
|
||
|
||
// 显示详细的错误信息
|
||
console.error('========== 错误详情 ==========');
|
||
if (isCorsError) {
|
||
console.error('这是CORS跨域问题,后端需要配置允许跨域请求');
|
||
console.error('请将上述配置添加到后端代码中');
|
||
}
|
||
|
||
alert(errorMessage);
|
||
} finally {
|
||
uploadBtn.disabled = false;
|
||
uploadBtnText.textContent = '上传';
|
||
uploadLoading.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
// 处理文件输入(直接上传知识库)
|
||
document.getElementById('fileInput').addEventListener('change', (event) => {
|
||
if (event.target.files.length > 0) {
|
||
openKnowledgeBaseModal();
|
||
addFiles(Array.from(event.target.files));
|
||
}
|
||
});
|
||
|
||
// 发送消息
|
||
async function sendMessage() {
|
||
const input = document.getElementById('textInput');
|
||
const message = input.value.trim();
|
||
|
||
if (!message) return;
|
||
|
||
// 隐藏欢迎界面,显示消息区域
|
||
document.getElementById('welcomeScreen').style.display = 'none';
|
||
const messagesDiv = document.getElementById('messages');
|
||
messagesDiv.style.display = 'flex';
|
||
|
||
// 添加用户消息
|
||
addMessage('user', message);
|
||
input.value = '';
|
||
autoResize(input);
|
||
|
||
// 禁用发送按钮
|
||
const sendBtn = document.getElementById('sendBtn');
|
||
sendBtn.disabled = true;
|
||
|
||
// 获取选中的模型和知识库
|
||
const model = document.getElementById('modelSelect').value;
|
||
const knowledgeBase = document.getElementById('knowledgeBaseSelect').value;
|
||
|
||
// 创建助手消息容器
|
||
const assistantMessageId = 'msg-' + Date.now();
|
||
currentMessageId = assistantMessageId;
|
||
addMessage('assistant', '', assistantMessageId);
|
||
|
||
try {
|
||
// 构建请求URL
|
||
const params = new URLSearchParams({
|
||
model: model,
|
||
message: message
|
||
});
|
||
|
||
if (knowledgeBase) {
|
||
params.append('ragTag', knowledgeBase);
|
||
}
|
||
|
||
const url = `http://localhost:8090/api/v1/rag/generate_stream_rag?${params.toString()}`;
|
||
console.log('========== 发送聊天请求 ==========');
|
||
console.log('请求URL:', url);
|
||
console.log('模型:', model);
|
||
console.log('知识库:', knowledgeBase || '未选择');
|
||
console.log('消息:', message);
|
||
|
||
// 发送普通请求(非流式)
|
||
const startTime = Date.now();
|
||
const response = await fetch(url, {
|
||
method: 'GET',
|
||
headers: {
|
||
'Accept': 'application/json',
|
||
'Content-Type': 'application/json'
|
||
}
|
||
});
|
||
|
||
const endTime = Date.now();
|
||
console.log('响应状态:', response.status, response.statusText);
|
||
console.log('响应Content-Type:', response.headers.get('content-type'));
|
||
console.log('请求耗时:', (endTime - startTime) + 'ms');
|
||
|
||
if (!response.ok) {
|
||
const errorText = await response.text();
|
||
console.error('响应错误:', errorText);
|
||
throw new Error(`HTTP error! status: ${response.status}, message: ${errorText}`);
|
||
}
|
||
|
||
// 解析JSON响应
|
||
const data = await response.json();
|
||
console.log('收到响应数据:', data);
|
||
|
||
// 提取响应内容
|
||
let content = '';
|
||
|
||
// 处理ChatResponse对象,根据实际响应结构提取内容
|
||
// 优先处理 result.output.content 或 results[0].output.content
|
||
if (data.result && data.result.output && data.result.output.content !== undefined) {
|
||
content = String(data.result.output.content);
|
||
} else if (data.results && Array.isArray(data.results) && data.results.length > 0) {
|
||
// 处理 results 数组
|
||
const firstResult = data.results[0];
|
||
if (firstResult.output && firstResult.output.content !== undefined) {
|
||
content = String(firstResult.output.content);
|
||
} else if (firstResult.content !== undefined) {
|
||
content = String(firstResult.content);
|
||
}
|
||
} else if (data.content !== undefined && data.content !== null) {
|
||
content = String(data.content);
|
||
} else if (data.text !== undefined && data.text !== null) {
|
||
content = String(data.text);
|
||
} else if (data.message !== undefined && data.message !== null) {
|
||
content = String(data.message);
|
||
} else if (data.response !== undefined && data.response !== null) {
|
||
content = String(data.response);
|
||
} else if (data.answer !== undefined && data.answer !== null) {
|
||
content = String(data.answer);
|
||
} else if (data.output && typeof data.output === 'object' && data.output.content !== undefined) {
|
||
content = String(data.output.content);
|
||
} else if (data.output && typeof data.output === 'string') {
|
||
content = String(data.output);
|
||
} else if (data.result && typeof data.result === 'object') {
|
||
// 如果result是对象,尝试提取其中的content
|
||
if (data.result.content !== undefined) {
|
||
content = String(data.result.content);
|
||
} else if (data.result.output && data.result.output.content !== undefined) {
|
||
content = String(data.result.output.content);
|
||
}
|
||
} else if (data.choices && Array.isArray(data.choices) && data.choices.length > 0) {
|
||
// 处理类似OpenAI格式的响应
|
||
const choice = data.choices[0];
|
||
if (choice.message && choice.message.content) {
|
||
content = String(choice.message.content);
|
||
} else if (choice.text) {
|
||
content = String(choice.text);
|
||
}
|
||
} else if (data.data && data.data.content) {
|
||
// 如果响应被包装在data字段中
|
||
content = String(data.data.content);
|
||
} else if (typeof data === 'string') {
|
||
content = data;
|
||
} else {
|
||
// 如果无法提取内容,打印整个对象用于调试
|
||
console.warn('无法提取内容,原始数据:', data);
|
||
content = JSON.stringify(data, null, 2);
|
||
}
|
||
|
||
// 处理内容:移除推理过程
|
||
if (content) {
|
||
// 移除 <think>...</think> 标签及其内容
|
||
content = content.replace(/<think>[\s\S]*?<\/redacted_reasoning>/gi, '');
|
||
// 移除其他可能的推理标签(如果存在)
|
||
content = content.replace(/<think>[\s\S]*?<\/think>/gi, '');
|
||
// 清理多余的换行和空格(多个连续换行变为两个)
|
||
content = content.replace(/\n{3,}/g, '\n\n');
|
||
content = content.trim();
|
||
}
|
||
|
||
// 更新消息
|
||
if (content) {
|
||
console.log('提取的内容长度:', content.length);
|
||
updateMessage(assistantMessageId, content);
|
||
} else {
|
||
console.warn('未找到响应内容,显示原始数据');
|
||
updateMessage(assistantMessageId, JSON.stringify(data, null, 2));
|
||
}
|
||
|
||
console.log('========== 响应处理完成 ==========');
|
||
sendBtn.disabled = false;
|
||
|
||
} catch (error) {
|
||
console.error('========== 发送消息失败 ==========');
|
||
console.error('错误类型:', error.name);
|
||
console.error('错误消息:', error.message);
|
||
console.error('错误堆栈:', error.stack);
|
||
|
||
let errorMsg = '抱歉,发生了错误: ';
|
||
if (error.name === 'TypeError' && error.message.includes('Failed to fetch')) {
|
||
errorMsg += '无法连接到后端服务,请检查后端是否运行并配置了CORS';
|
||
} else if (error.name === 'SyntaxError' && error.message.includes('JSON')) {
|
||
errorMsg += '响应格式错误,无法解析JSON';
|
||
} else {
|
||
errorMsg += error.message;
|
||
}
|
||
|
||
updateMessage(assistantMessageId, errorMsg);
|
||
sendBtn.disabled = false;
|
||
}
|
||
}
|
||
|
||
// 添加消息
|
||
function addMessage(role, content, messageId = null) {
|
||
const messagesDiv = document.getElementById('messages');
|
||
const messageDiv = document.createElement('div');
|
||
messageDiv.className = `message ${role}`;
|
||
|
||
if (messageId) {
|
||
messageDiv.id = messageId;
|
||
}
|
||
|
||
const avatar = document.createElement('div');
|
||
avatar.className = 'message-avatar';
|
||
avatar.textContent = role === 'user' ? 'U' : '🐼';
|
||
|
||
const contentDiv = document.createElement('div');
|
||
contentDiv.className = 'message-content';
|
||
contentDiv.textContent = content;
|
||
|
||
messageDiv.appendChild(avatar);
|
||
messageDiv.appendChild(contentDiv);
|
||
messagesDiv.appendChild(messageDiv);
|
||
|
||
// 滚动到底部
|
||
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
||
}
|
||
|
||
// 更新消息内容
|
||
function updateMessage(messageId, content) {
|
||
const messageDiv = document.getElementById(messageId);
|
||
if (messageDiv) {
|
||
const contentDiv = messageDiv.querySelector('.message-content');
|
||
if (contentDiv) {
|
||
contentDiv.textContent = content;
|
||
// 滚动到底部
|
||
const messagesDiv = document.getElementById('messages');
|
||
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 处理键盘事件
|
||
function handleKeyDown(event) {
|
||
if (event.key === 'Enter' && !event.shiftKey) {
|
||
event.preventDefault();
|
||
sendMessage();
|
||
}
|
||
}
|
||
|
||
// 自动调整输入框高度
|
||
function autoResize(textarea) {
|
||
textarea.style.height = 'auto';
|
||
textarea.style.height = Math.min(textarea.scrollHeight, 200) + 'px';
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|