502 lines
16 KiB
HTML
502 lines
16 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>智能知识库问答系统</title>
|
||
<style>
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||
background: #f5f5f7;
|
||
min-height: 100vh;
|
||
padding: 20px;
|
||
color: #1d1d1f;
|
||
}
|
||
|
||
.container {
|
||
max-width: 800px;
|
||
margin: 40px auto;
|
||
background: white;
|
||
border-radius: 18px;
|
||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
|
||
padding: 40px;
|
||
}
|
||
|
||
h1 {
|
||
text-align: center;
|
||
color: #1d1d1f;
|
||
margin-bottom: 10px;
|
||
font-size: 2.2em;
|
||
font-weight: 700;
|
||
}
|
||
|
||
h3 {
|
||
color: #1d1d1f;
|
||
margin-bottom: 24px;
|
||
font-size: 1.3em;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.section {
|
||
margin-bottom: 40px;
|
||
padding: 24px;
|
||
background: #ffffff;
|
||
border-radius: 12px;
|
||
border: 1px solid #e6e6e6;
|
||
}
|
||
|
||
.form-group {
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
label {
|
||
display: block;
|
||
font-size: 1em;
|
||
margin-bottom: 8px;
|
||
color: #86868b;
|
||
font-weight: 500;
|
||
}
|
||
|
||
input[type="text"], input[type="file"] {
|
||
width: 100%;
|
||
padding: 12px 16px;
|
||
font-size: 1.05em;
|
||
border: 1px solid #d2d2d7;
|
||
border-radius: 8px;
|
||
transition: border-color 0.2s, background-color 0.2s;
|
||
background: #ffffff;
|
||
}
|
||
|
||
input[type="text"]:focus, input[type="file"]:focus {
|
||
outline: none;
|
||
border-color: #0071e3;
|
||
background: #ffffff;
|
||
}
|
||
|
||
button {
|
||
display: inline-block;
|
||
padding: 12px 24px;
|
||
font-size: 1em;
|
||
background: #0071e3;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 980px;
|
||
cursor: pointer;
|
||
transition: background-color 0.2s;
|
||
font-weight: 600;
|
||
margin-right: 10px;
|
||
}
|
||
|
||
button:hover {
|
||
background: #0077ed;
|
||
}
|
||
|
||
button:disabled {
|
||
background: #d1d1d6;
|
||
color: #86868b;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.response-box {
|
||
margin-top: 16px;
|
||
padding: 20px;
|
||
background: white;
|
||
border-radius: 12px;
|
||
border: 1px solid #e6e6e6;
|
||
min-height: 120px;
|
||
font-size: 1em;
|
||
line-height: 1.7;
|
||
color: #1d1d1f;
|
||
white-space: pre-wrap;
|
||
}
|
||
|
||
.loading {
|
||
display: inline-block;
|
||
width: 16px;
|
||
height: 16px;
|
||
border: 2px solid rgba(255,255,255,.3);
|
||
border-radius: 50%;
|
||
border-top-color: #fff;
|
||
animation: spin 1s linear infinite;
|
||
margin-right: 8px;
|
||
}
|
||
|
||
@keyframes spin {
|
||
to { transform: rotate(360deg); }
|
||
}
|
||
|
||
#document-list {
|
||
margin-top: 16px;
|
||
}
|
||
|
||
.document-item {
|
||
padding: 16px;
|
||
background: white;
|
||
border-radius: 12px;
|
||
margin-bottom: 8px;
|
||
border: 1px solid #e6e6e6;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.document-info {
|
||
flex: 1;
|
||
}
|
||
|
||
.document-title {
|
||
font-weight: 600;
|
||
color: #1d1d1f;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.document-meta {
|
||
font-size: 0.9em;
|
||
color: #86868b;
|
||
}
|
||
|
||
.delete-btn {
|
||
background: #ff453a;
|
||
padding: 8px 16px;
|
||
font-size: 0.9em;
|
||
}
|
||
|
||
.delete-btn:hover {
|
||
background: #ff3b30;
|
||
}
|
||
|
||
.success-message, .error-message {
|
||
padding: 12px 16px;
|
||
border-radius: 12px;
|
||
margin-bottom: 16px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.success-message {
|
||
background: #32d74b;
|
||
color: white;
|
||
}
|
||
|
||
.error-message {
|
||
background: #ff453a;
|
||
color: white;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
body {
|
||
padding: 16px;
|
||
}
|
||
|
||
.container {
|
||
padding: 24px;
|
||
margin: 16px auto;
|
||
}
|
||
|
||
h1 {
|
||
font-size: 1.8em;
|
||
}
|
||
|
||
.section {
|
||
padding: 16px;
|
||
}
|
||
|
||
button {
|
||
width: 100%;
|
||
margin-right: 0;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.document-item {
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.delete-btn {
|
||
width: auto;
|
||
margin-top: 12px;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<h1>智能知识库问答系统</h1>
|
||
|
||
<!-- 文档上传区 -->
|
||
<div class="section">
|
||
<h3>文档上传</h3>
|
||
<div id="upload-message"></div>
|
||
<div class="form-group">
|
||
<label for="file-upload">选择文档(支持PDF、Word、TXT):</label>
|
||
<input type="file" id="file-upload" accept=".pdf,.doc,.docx,.txt">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="document-title">文档标题(可选):</label>
|
||
<input type="text" id="document-title" placeholder="为文档添加标题...">
|
||
</div>
|
||
<button id="upload-btn" onclick="uploadDocument()">上传文档</button>
|
||
</div>
|
||
|
||
<!-- 知识库管理区 -->
|
||
<div class="section">
|
||
<h3>知识库管理</h3>
|
||
<button id="refresh-btn" onclick="loadDocuments()">刷新文档列表</button>
|
||
<div id="document-list">
|
||
<p>点击刷新按钮查看已上传的文档...</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 问答区 -->
|
||
<div class="section">
|
||
<h3>知识库问答</h3>
|
||
<div class="form-group">
|
||
<label for="question">请输入您的问题:</label>
|
||
<input type="text" id="question" placeholder="例如:什么是Python?">
|
||
</div>
|
||
<button id="ask-btn" onclick="askQuestion()">提问</button>
|
||
<div class="response-box" id="answer">
|
||
输入问题并点击提问按钮获取答案...
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// 页面加载时加载文档列表
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
loadDocuments();
|
||
});
|
||
|
||
// 上传文档
|
||
function uploadDocument() {
|
||
const fileInput = document.getElementById('file-upload');
|
||
const titleInput = document.getElementById('document-title');
|
||
const uploadBtn = document.getElementById('upload-btn');
|
||
const uploadMessage = document.getElementById('upload-message');
|
||
|
||
// 检查是否选择了文件
|
||
if (!fileInput.files || fileInput.files.length === 0) {
|
||
showMessage(uploadMessage, '请选择一个文件', 'error');
|
||
return;
|
||
}
|
||
|
||
const file = fileInput.files[0];
|
||
const title = titleInput.value;
|
||
|
||
// 重置消息
|
||
uploadMessage.innerHTML = '';
|
||
|
||
// 禁用按钮并显示加载状态
|
||
uploadBtn.disabled = true;
|
||
uploadBtn.innerHTML = '<span class="loading"></span>正在上传...';
|
||
|
||
// 创建表单数据
|
||
const formData = new FormData();
|
||
formData.append('file', file);
|
||
if (title) {
|
||
formData.append('title', title);
|
||
}
|
||
|
||
// 发送请求
|
||
fetch('/upload', {
|
||
method: 'POST',
|
||
body: formData
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
showMessage(uploadMessage, `文档上传成功!已处理 ${data.count} 个文档块`, 'success');
|
||
// 重置表单
|
||
fileInput.value = '';
|
||
titleInput.value = '';
|
||
// 更新文档列表
|
||
loadDocuments();
|
||
} else {
|
||
showMessage(uploadMessage, `上传失败:${data.error}`, 'error');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
showMessage(uploadMessage, `上传失败:${error.message}`, 'error');
|
||
})
|
||
.finally(() => {
|
||
// 恢复按钮状态
|
||
uploadBtn.disabled = false;
|
||
uploadBtn.innerHTML = '上传文档';
|
||
});
|
||
}
|
||
|
||
// 加载文档列表
|
||
function loadDocuments() {
|
||
const documentList = document.getElementById('document-list');
|
||
const refreshBtn = document.getElementById('refresh-btn');
|
||
|
||
// 禁用按钮并显示加载状态
|
||
refreshBtn.disabled = true;
|
||
refreshBtn.innerHTML = '<span class="loading"></span>正在加载...';
|
||
|
||
// 发送请求
|
||
fetch('/documents')
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
if (data.documents.length === 0) {
|
||
documentList.innerHTML = '<p>知识库中暂无文档,请先上传文档...</p>';
|
||
} else {
|
||
// 按文档分组显示
|
||
const documentsByFile = {};
|
||
data.documents.forEach(doc => {
|
||
const fileName = doc.parent_file || '未知文件';
|
||
if (!documentsByFile[fileName]) {
|
||
documentsByFile[fileName] = [];
|
||
}
|
||
documentsByFile[fileName].push(doc);
|
||
});
|
||
|
||
let html = '';
|
||
for (const fileName in documentsByFile) {
|
||
const docs = documentsByFile[fileName];
|
||
const firstDoc = docs[0];
|
||
html += `
|
||
<div class="document-item">
|
||
<div class="document-info">
|
||
<div class="document-title">${firstDoc.metadata?.title || fileName}</div>
|
||
<div class="document-meta">
|
||
文档块数量: ${docs.length} | 上传时间: ${new Date(firstDoc.timestamp).toLocaleString()}
|
||
</div>
|
||
</div>
|
||
<button class="delete-btn" onclick="deleteDocument('${firstDoc.id}')">删除</button>
|
||
</div>
|
||
`;
|
||
}
|
||
documentList.innerHTML = html;
|
||
}
|
||
} else {
|
||
documentList.innerHTML = `<p class="error-message">加载失败:${data.error}</p>`;
|
||
}
|
||
})
|
||
.catch(error => {
|
||
documentList.innerHTML = `<p class="error-message">加载失败:${error.message}</p>`;
|
||
})
|
||
.finally(() => {
|
||
// 恢复按钮状态
|
||
refreshBtn.disabled = false;
|
||
refreshBtn.innerHTML = '刷新文档列表';
|
||
});
|
||
}
|
||
|
||
// 删除文档
|
||
function deleteDocument(documentId) {
|
||
if (!confirm('确定要删除这个文档吗?')) {
|
||
return;
|
||
}
|
||
|
||
// 发送请求
|
||
fetch(`/documents/${documentId}`, {
|
||
method: 'DELETE'
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
// 更新文档列表
|
||
loadDocuments();
|
||
} else {
|
||
alert(`删除失败:${data.error}`);
|
||
}
|
||
})
|
||
.catch(error => {
|
||
alert(`删除失败:${error.message}`);
|
||
});
|
||
}
|
||
|
||
// 提问
|
||
function askQuestion() {
|
||
const questionInput = document.getElementById('question');
|
||
const askBtn = document.getElementById('ask-btn');
|
||
const answerBox = document.getElementById('answer');
|
||
|
||
const question = questionInput.value.trim();
|
||
if (!question) {
|
||
alert('请输入问题');
|
||
return;
|
||
}
|
||
|
||
// 重置内容
|
||
answerBox.textContent = '';
|
||
|
||
// 禁用按钮并显示加载状态
|
||
askBtn.disabled = true;
|
||
askBtn.innerHTML = '<span class="loading"></span>正在思考...';
|
||
|
||
// 发送请求
|
||
fetch('/ask', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({ query: question })
|
||
})
|
||
.then(response => {
|
||
if (!response.ok) {
|
||
throw new Error('问答失败');
|
||
}
|
||
|
||
// 处理流式响应
|
||
const reader = response.body.getReader();
|
||
const decoder = new TextDecoder('utf-8');
|
||
|
||
function read() {
|
||
return reader.read().then(({ done, value }) => {
|
||
if (done) {
|
||
return;
|
||
}
|
||
|
||
// 解码并显示内容
|
||
const chunk = decoder.decode(value, { stream: true });
|
||
answerBox.textContent += chunk;
|
||
|
||
// 继续读取
|
||
return read();
|
||
});
|
||
}
|
||
|
||
return read();
|
||
})
|
||
.catch(error => {
|
||
answerBox.textContent = `问答失败:${error.message}`;
|
||
})
|
||
.finally(() => {
|
||
// 恢复按钮状态
|
||
askBtn.disabled = false;
|
||
askBtn.innerHTML = '提问';
|
||
});
|
||
}
|
||
|
||
// 显示消息
|
||
function showMessage(element, message, type) {
|
||
const messageElement = document.createElement('div');
|
||
messageElement.className = type === 'success' ? 'success-message' : 'error-message';
|
||
messageElement.textContent = message;
|
||
element.innerHTML = '';
|
||
element.appendChild(messageElement);
|
||
|
||
// 3秒后自动隐藏
|
||
setTimeout(() => {
|
||
messageElement.remove();
|
||
}, 3000);
|
||
}
|
||
|
||
// 按下Enter键也可以提问
|
||
document.getElementById('question').addEventListener('keypress', function(e) {
|
||
if (e.key === 'Enter') {
|
||
askQuestion();
|
||
}
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|