Files
ai-rag-knowledge/docs/nginx/html/rag-ai.html

1286 lines
45 KiB
HTML
Raw Normal View History

2026-01-17 23:54:47 +08:00
<!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>