添加了文献的ai问答
This commit is contained in:
+322
-17
@@ -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>
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user