添加了文献的ai问答

This commit is contained in:
2025-01-13 07:09:34 +00:00
parent 5a556fd34a
commit 7cacc541b1
2 changed files with 555 additions and 17 deletions
+322 -17
View File
@@ -248,6 +248,72 @@
transform: none !important;
box-shadow: 0 2px 4px rgba(0,0,0,0.1) !important;
}
/* 对话框标题样式 */
.modal-title {
font-size: 1.1rem;
font-weight: 600;
color: #2c2c2c;
}
/* 现代化滚动条样式 */
.custom-scrollbar::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: rgba(99, 102, 241, 0.05);
border-radius: 10px;
margin: 4px 0;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: linear-gradient(
180deg,
var(--primary-color) 0%,
var(--primary-light) 100%
);
border-radius: 10px;
border: 2px solid transparent;
background-clip: padding-box;
transition: all 0.2s ease;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: linear-gradient(
180deg,
var(--primary-light) 0%,
var(--primary-color) 100%
);
border: 1.5px solid transparent;
}
.custom-scrollbar::-webkit-scrollbar-corner {
background: transparent;
}
/* Firefox 滚动条样式 */
.custom-scrollbar {
scrollbar-width: thin;
scrollbar-color: var(--primary-color) transparent;
}
/* 当滚动条悬停在内容上时隐藏 */
.custom-scrollbar {
overflow: auto;
}
/* 仅在hover时显示滚动条 */
@media (hover: hover) {
.custom-scrollbar::-webkit-scrollbar-thumb {
opacity: 0;
}
.custom-scrollbar:hover::-webkit-scrollbar-thumb {
opacity: 1;
}
}
</style>
</head>
<body>
@@ -519,23 +585,31 @@
}, 0);
return `
<div class="card mb-3" id="${cardId}" style="cursor: pointer;"
onclick="viewReferenceDetailFromCard(this)">
<div class="card-body">
<h5 class="card-title">${basicInfo.title}</h5>
<p class="card-text">
Author: ${basicInfo.author}<br>
Publication Date: ${basicInfo.publication_date}<br>
Journal/Publisher: ${basicInfo.journal_publisher}
</p>
<div class="btn-group" onclick="event.stopPropagation();">
<button class="btn btn-warning btn-sm"
onclick="viewReferenceDetailFromCard(this.closest('.card'))">
View Details
</button>
<button class="btn btn-danger btn-sm"
onclick="deleteReference('${projectId}', '${reference._id}')">
Delete
<div class="card mb-3" id="${cardId}">
<div class="card-body d-flex position-relative">
<div class="flex-grow-1" style="cursor: pointer;" onclick="viewReferenceDetailFromCard(this.closest('.card'))">
<h5 class="card-title">${basicInfo.title}</h5>
<p class="card-text">
Author: ${basicInfo.author}<br>
Publication Date: ${basicInfo.publication_date}<br>
Journal/Publisher: ${basicInfo.journal_publisher}
</p>
<div class="btn-group">
<button class="btn btn-warning btn-sm"
onclick="event.stopPropagation(); viewReferenceDetailFromCard(this.closest('.card'))">
View Details
</button>
<button class="btn btn-danger btn-sm"
onclick="event.stopPropagation(); deleteReference('${projectId}', '${reference._id}')">
Delete
</button>
</div>
</div>
<div class="position-absolute" style="right: 1rem; top: 50%; transform: translateY(-50%);">
<button class="btn rounded-circle d-flex align-items-center justify-content-center"
style="width: 48px; height: 48px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); background: var(--primary-color); color: white;"
onclick="event.stopPropagation(); openChatDialog('${reference._id}', '${basicInfo.title}')">
<i class="bi bi-chat-dots" style="font-size: 1.2rem;"></i>
</button>
</div>
</div>
@@ -938,6 +1012,237 @@
function goBack() {
window.location.href = 'https://beta.obscura.work/lab/lab.html';
}
// Add chat dialog function
async function openChatDialog(referenceId, title) {
const modal = document.createElement('div');
modal.className = 'modal fade';
modal.id = 'chatModal';
modal.setAttribute('data-bs-backdrop', 'static');
modal.setAttribute('data-bs-keyboard', 'false');
modal.innerHTML = `
<div class="modal-dialog modal-lg modal-dialog-centered" style="max-width: 800px;">
<div class="modal-content" style="min-height: 60vh;">
<div class="modal-header">
<h5 class="modal-title">Chat with Literature: ${title}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body d-flex flex-column" style="height: calc(60vh - 76px);">
<div id="chatHistory" class="flex-grow-1" style="overflow-y: auto; margin-bottom: 15px; padding: 10px;">
<!-- Chat history will be loaded here -->
</div>
<div class="input-group">
<input type="text" id="questionInput" class="form-control"
placeholder="Type your question here...">
<button class="btn"
style="background: var(--primary-color); color: white;"
onclick="sendQuestion('${referenceId}')">
Send
</button>
</div>
</div>
</div>
</div>
`;
document.body.appendChild(modal);
const modalInstance = new bootstrap.Modal(modal);
modalInstance.show();
// Load chat history
await loadChatHistory(referenceId);
// Add event listener for Enter key
const input = document.getElementById('questionInput');
input.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
sendQuestion(referenceId);
}
});
modal.addEventListener('hidden.bs.modal', () => {
document.body.removeChild(modal);
});
}
// Load chat history function
async function loadChatHistory(referenceId) {
const chatHistory = document.getElementById('chatHistory');
try {
const response = await fetch(`${API_BASE_URL}/references/${referenceId}/qa/history`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) {
throw new Error('Failed to load chat history');
}
const history = await response.json();
if (history.length === 0) {
chatHistory.innerHTML = '<div class="text-center text-muted">No chat history yet. Start a conversation!</div>';
return;
}
chatHistory.innerHTML = history.map(chat => `
<div class="mb-3">
<div class="d-flex align-items-start mb-2">
<div class="me-2">👤</div>
<div class="bg-light rounded p-2 flex-grow-1">
${chat.question}
</div>
</div>
<div class="d-flex align-items-start">
<div class="me-2">🤖</div>
<div class="bg-primary bg-opacity-10 rounded p-2 flex-grow-1">
${chat.answer}
</div>
</div>
<div class="text-end text-muted small mt-1">
${new Date(chat.timestamp).toLocaleString()}
</div>
</div>
`).join('');
// Scroll to bottom
chatHistory.scrollTop = chatHistory.scrollHeight;
} catch (error) {
console.error('Failed to load chat history:', error);
chatHistory.innerHTML = '<div class="alert alert-danger">Failed to load chat history</div>';
}
}
// Send question function
async function sendQuestion(referenceId) {
const input = document.getElementById('questionInput');
const question = input.value.trim();
if (!question) {
return;
}
const chatHistory = document.getElementById('chatHistory');
try {
// Clear input and disable it
input.value = '';
input.disabled = true;
// First, add the user's question to chat history
const questionDiv = document.createElement('div');
questionDiv.className = 'mb-3';
questionDiv.innerHTML = `
<div class="d-flex align-items-start mb-2">
<div class="me-2">👤</div>
<div class="bg-light rounded p-2 flex-grow-1">
${question}
</div>
</div>
`;
chatHistory.appendChild(questionDiv);
// Then add the loading indicator
const loadingDiv = document.createElement('div');
loadingDiv.className = 'd-flex align-items-start mb-3';
loadingDiv.innerHTML = `
<div class="me-2">🤖</div>
<div class="bg-primary bg-opacity-10 rounded p-2 flex-grow-1">
<div class="d-flex align-items-center">
<div class="spinner-border spinner-border-sm text-primary me-2" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<span>Thinking...</span>
</div>
</div>
`;
chatHistory.appendChild(loadingDiv);
chatHistory.scrollTop = chatHistory.scrollHeight;
// 发送问题并获取任务ID
const response = await fetch(`${API_BASE_URL}/references/${referenceId}/qa`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ question })
});
if (!response.ok) {
throw new Error('Failed to send question');
}
const { task_id } = await response.json();
// 轮询获取任务结果
let result;
while (true) {
const taskResponse = await fetch(`${API_BASE_URL}/task/${task_id}`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!taskResponse.ok) {
throw new Error('Failed to get task status');
}
const taskResult = await taskResponse.json();
if (taskResult.status === 'completed') {
result = taskResult;
break;
} else if (taskResult.status === 'failed') {
throw new Error(taskResult.error || 'Task failed');
}
// 等待1秒后再次查询
await new Promise(resolve => setTimeout(resolve, 1000));
}
// Remove loading indicator
loadingDiv.remove();
// Add AI's answer
const answerDiv = document.createElement('div');
answerDiv.className = 'd-flex align-items-start mb-3';
answerDiv.innerHTML = `
<div class="me-2">🤖</div>
<div class="bg-primary bg-opacity-10 rounded p-2 flex-grow-1">
${result.answer}
</div>
`;
// Add timestamp
const timestampDiv = document.createElement('div');
timestampDiv.className = 'text-end text-muted small mt-1 mb-3';
timestampDiv.textContent = new Date().toLocaleString();
chatHistory.appendChild(answerDiv);
chatHistory.appendChild(timestampDiv);
chatHistory.scrollTop = chatHistory.scrollHeight;
} catch (error) {
console.error('Failed to send question:', error);
const errorDiv = document.createElement('div');
errorDiv.className = 'd-flex align-items-start mb-3';
errorDiv.innerHTML = `
<div class="me-2">🤖</div>
<div class="bg-danger text-white rounded p-2 flex-grow-1">
Failed to get answer: ${error.message}
</div>
`;
chatHistory.appendChild(errorDiv);
chatHistory.scrollTop = chatHistory.scrollHeight;
} finally {
input.disabled = false;
input.focus();
}
}
</script>
</body>
</html>
+233
View File
@@ -2601,6 +2601,239 @@ async def add_sensor_formula(
print(f"Error adding formula: {str(e)}")
raise HTTPException(status_code=500, detail=f"Failed to add formula: {str(e)}")
# 添加问答相关的模型
class QuestionModel(BaseModel):
"""问题模型"""
question: str
class TaskStatus:
PENDING = "pending"
PROCESSING = "processing"
COMPLETED = "completed"
FAILED = "failed"
@app.post("/lab/references/{reference_id}/qa")
async def ask_reference_question(
reference_id: str,
question: QuestionModel,
current_user: UserModel = Depends(get_current_user)
):
"""向文献提问(异步)"""
db = await get_database()
redis = await get_redis()
try:
# 验证文献是否存在
reference = await db.references.find_one({"_id": ObjectId(reference_id)})
if not reference:
raise HTTPException(status_code=404, detail="文献不存在")
# 生成任务ID
task_id = str(ObjectId())
# 创建后台任务
asyncio.create_task(process_reference_question(
task_id=task_id,
reference_id=reference_id,
question=question.question,
reference=reference
))
return {
"task_id": task_id,
"status": TaskStatus.PENDING,
"message": "问题已提交,正在处理中"
}
except Exception as e:
print(f"Error in ask_reference_question: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
finally:
try:
await redis.aclose()
except Exception as e:
print(f"Error closing Redis connection: {e}")
async def process_reference_question(task_id: str, reference_id: str, question: str, reference: dict):
"""处理文献问答的后台任务"""
redis = await get_redis()
try:
# 更新任务状态为处理中
await redis.select(208) # 使用db208存储任务状态
await redis.hset(
f"task:{task_id}",
mapping={
"status": TaskStatus.PROCESSING,
"reference_id": reference_id,
"question": question
}
)
# 从Redis获取分析报告
await redis.select(203)
report_key = f"reference_report:{reference_id}"
report_data = await redis.get(report_key)
if not report_data:
raise Exception("文献分析报告不存在,请先进行分析")
# 读取PDF文件内容
file_path = reference.get("reference_link")
if not file_path or not os.path.exists(file_path):
raise Exception("文献文件不存在")
# 读取PDF内容
pdf_content = ""
with open(file_path, 'rb') as file:
pdf_reader = PyPDF2.PdfReader(file)
for page in pdf_reader.pages:
content = page.extract_text()
pdf_content += content
# 限制内容长度
MAX_CHARS = 180000
pdf_content = pdf_content[:MAX_CHARS]
# 构建系统提示和用户提示
system_prompt = """
你是一个专业的学术助手,负责回答关于学术文献的问题。
你应该基于文献内容和分析报告提供准确、专业的回答。
回答应当简洁明了,并尽可能引用文献中的具体内容。
"""
# 构建上下文
context = {
"文献内容": pdf_content,
"分析报告": json.loads(report_data)
}
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": f"基于以下文献内容和分析报告回答问题:\n\n文献信息:{json.dumps(context, ensure_ascii=False)}\n\n问题:{question}"}
]
# 调用DeepSeek API获取回答
response = client.chat.completions.create(
model="deepseek-chat",
messages=messages
)
answer = response.choices[0].message.content
# 保存对话历史到Redis db207
await redis.select(207)
chat_history_key = f"chat_history:{reference_id}"
# 获取现有历史记录
existing_history = await redis.get(chat_history_key)
history = json.loads(existing_history) if existing_history else []
# 添加新的对话
history.append({
"question": question,
"answer": answer,
"timestamp": datetime.now(timezone.utc).isoformat()
})
# 保存更新后的历史记录
await redis.set(chat_history_key, json.dumps(history))
# 更新任务状态为完成
await redis.select(208)
await redis.hset(
f"task:{task_id}",
mapping={
"status": TaskStatus.COMPLETED,
"answer": answer,
"reference_title": reference.get("reference_title")
}
)
except Exception as e:
print(f"Error processing question: {e}")
# 更新任务状态为失败
await redis.select(208)
await redis.hset(
f"task:{task_id}",
mapping={
"status": TaskStatus.FAILED,
"error": str(e)
}
)
finally:
try:
await redis.aclose()
except Exception as e:
print(f"Error closing Redis connection: {e}")
@app.get("/lab/task/{task_id}")
async def get_task_status(task_id: str):
"""获取任务状态和结果"""
redis = await get_redis()
try:
await redis.select(208)
task_data = await redis.hgetall(f"task:{task_id}")
if not task_data:
raise HTTPException(status_code=404, detail="任务不存在")
response = {
"task_id": task_id,
"status": task_data.get("status", TaskStatus.PENDING)
}
# 如果任务完成,添加结果
if task_data.get("status") == TaskStatus.COMPLETED:
response.update({
"answer": task_data.get("answer"),
"reference_title": task_data.get("reference_title")
})
# 如果任务失败,添加错误信息
elif task_data.get("status") == TaskStatus.FAILED:
response.update({
"error": task_data.get("error")
})
return response
except Exception as e:
print(f"Error getting task status: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
finally:
try:
await redis.aclose()
except Exception as e:
print(f"Error closing Redis connection: {e}")
@app.get("/lab/references/{reference_id}/qa/history")
async def get_reference_qa_history(
reference_id: str,
current_user: UserModel = Depends(get_current_user)
):
"""获取文献问答历史记录"""
redis = await get_redis()
try:
# 从Redis db207获取对话历史
await redis.select(207)
chat_history_key = f"chat_history:{reference_id}"
history_data = await redis.get(chat_history_key)
if not history_data:
return []
return json.loads(history_data)
except Exception as e:
print(f"Error getting QA history: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
finally:
try:
await redis.aclose()
except Exception as e:
print(f"Error closing Redis connection: {e}")
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=6000)