diff --git a/literature.html b/literature.html index eb1e309..59a3c34 100644 --- a/literature.html +++ b/literature.html @@ -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; + } + } @@ -519,23 +585,31 @@ }, 0); return ` -
-
-
${basicInfo.title}
-

- Author: ${basicInfo.author}
- Publication Date: ${basicInfo.publication_date}
- Journal/Publisher: ${basicInfo.journal_publisher} -

-
- - + +
+
+
+
@@ -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 = ` + + `; + + 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 = '
No chat history yet. Start a conversation!
'; + return; + } + + chatHistory.innerHTML = history.map(chat => ` +
+
+
👤
+
+ ${chat.question} +
+
+
+
🤖
+
+ ${chat.answer} +
+
+
+ ${new Date(chat.timestamp).toLocaleString()} +
+
+ `).join(''); + + // Scroll to bottom + chatHistory.scrollTop = chatHistory.scrollHeight; + + } catch (error) { + console.error('Failed to load chat history:', error); + chatHistory.innerHTML = '
Failed to load chat history
'; + } + } + + // 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 = ` +
+
👤
+
+ ${question} +
+
+ `; + chatHistory.appendChild(questionDiv); + + // Then add the loading indicator + const loadingDiv = document.createElement('div'); + loadingDiv.className = 'd-flex align-items-start mb-3'; + loadingDiv.innerHTML = ` +
🤖
+
+
+
+ Loading... +
+ Thinking... +
+
+ `; + + 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 = ` +
🤖
+
+ ${result.answer} +
+ `; + + // 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 = ` +
🤖
+
+ Failed to get answer: ${error.message} +
+ `; + chatHistory.appendChild(errorDiv); + chatHistory.scrollTop = chatHistory.scrollHeight; + } finally { + input.disabled = false; + input.focus(); + } + } \ No newline at end of file diff --git a/main.py b/main.py index 0b69b10..2793060 100644 --- a/main.py +++ b/main.py @@ -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) \ No newline at end of file