Files
ai-rag-knowledge/docs/nginx/html/rag-ai.html
2026-01-17 23:54:47 +08:00

1286 lines
45 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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>