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