文献批量上传,api调用改为异步,优化显示
This commit is contained in:
+272
-181
@@ -31,6 +31,23 @@
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
margin-bottom: 24px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.card-text {
|
||||
font-size: 14px;
|
||||
color: #64748B;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
@@ -320,10 +337,10 @@
|
||||
<nav class="navbar">
|
||||
<div class="navbar-brand">Lab</div>
|
||||
<div class="navbar-menu">
|
||||
<div class="navbar-menu-item" onclick="window.open('lab.html', '_blank')">Project</div>
|
||||
<div class="navbar-menu-item" onclick="window.open('device.html', '_blank')">Device</div>
|
||||
<div class="navbar-menu-item" onclick="window.open('reports.html', '_blank')">Reports</div>
|
||||
<div class="navbar-menu-item active" onclick="window.open('literature.html', '_blank')">Literature</div>
|
||||
<div class="navbar-menu-item" onclick="window.location.href='lab.html'">Project</div>
|
||||
<div class="navbar-menu-item" onclick="window.location.href='device.html'">Device</div>
|
||||
<div class="navbar-menu-item" onclick="window.location.href='reports.html'">Reports</div>
|
||||
<div class="navbar-menu-item active" onclick="window.location.href='literature.html'">Literature</div>
|
||||
</div>
|
||||
<div class="navbar-right">
|
||||
<a href="https://beta.obscura.work/lab/device.html" target="_blank" class="btn btn-primary">
|
||||
@@ -553,47 +570,23 @@
|
||||
}
|
||||
|
||||
// Literature card rendering function
|
||||
function renderReferenceCard(reference, projectId, analysisResult = null) {
|
||||
let basicInfo = {
|
||||
title: reference.reference_title,
|
||||
author: 'Unknown Author',
|
||||
publication_date: 'Unknown Date',
|
||||
journal_publisher: 'Unknown Publisher'
|
||||
};
|
||||
if (analysisResult && analysisResult['1. Basic Information']) {
|
||||
const analyzeBasicInfo = analysisResult['1. Basic Information'];
|
||||
basicInfo = {
|
||||
title: analyzeBasicInfo.title || reference.reference_title,
|
||||
author: analyzeBasicInfo.author || 'Unknown Author',
|
||||
publication_date: analyzeBasicInfo.publication_date || 'Unknown Date',
|
||||
journal_publisher: analyzeBasicInfo.journal_publisher || 'Unknown Publisher',
|
||||
document_type: analyzeBasicInfo.document_type || 'Unknown Type'
|
||||
};
|
||||
}
|
||||
|
||||
function renderReferenceCard(reference, projectId) {
|
||||
const uploadTime = reference.upload_time;
|
||||
|
||||
const cardId = `reference-${reference._id}`;
|
||||
const encodedAnalysis = analysisResult ?
|
||||
encodeURIComponent(JSON.stringify(analysisResult)) : '';
|
||||
|
||||
setTimeout(() => {
|
||||
const card = document.getElementById(cardId);
|
||||
if (card) {
|
||||
card.setAttribute('data-analysis', encodedAnalysis);
|
||||
}
|
||||
}, 0);
|
||||
|
||||
return `
|
||||
<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>
|
||||
<h5 class="card-title">${reference.reference_title}</h5>
|
||||
<div class="card-text analysis-content-${reference._id}">
|
||||
<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>Analyzing literature content...</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-warning btn-sm"
|
||||
onclick="event.stopPropagation(); viewReferenceDetailFromCard(this.closest('.card'))">
|
||||
@@ -608,13 +601,14 @@
|
||||
<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}')">
|
||||
onclick="event.stopPropagation(); openChatDialog('${reference._id}', '${reference.reference_title}')">
|
||||
<i class="bi bi-chat-dots" style="font-size: 1.2rem;"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer text-muted">
|
||||
Upload Time: ${uploadTime}
|
||||
<span class="ms-2 analysis-status-${reference._id}">Analysis in progress...</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -625,6 +619,7 @@
|
||||
const referencesList = document.getElementById('referenceAnalysis');
|
||||
|
||||
try {
|
||||
// Get literature list
|
||||
const response = await fetch(`${API_BASE_URL}/projects/${projectId}/references`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
@@ -632,7 +627,6 @@
|
||||
});
|
||||
|
||||
if (response.status === 401) {
|
||||
// Token 无效或过期
|
||||
window.location.href = 'https://beta.obscura.work/lab/login.html';
|
||||
return;
|
||||
}
|
||||
@@ -652,27 +646,93 @@
|
||||
referencesList.innerHTML = '<div class="alert alert-info">No literature data, please upload literature</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const referencesWithAnalysis = await Promise.all(references.map(async (ref) => {
|
||||
|
||||
// Render literature cards
|
||||
const referenceCards = references.map(ref => renderReferenceCard(ref, projectId)).join('');
|
||||
referencesList.innerHTML = referenceCards;
|
||||
|
||||
// Get analysis status and results for each literature
|
||||
references.forEach(async (ref) => {
|
||||
try {
|
||||
const analysisResponse = await fetch(`${API_BASE_URL}/references/${ref._id}/report`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (analysisResponse.ok) {
|
||||
const analysis = await analysisResponse.json();
|
||||
return renderReferenceCard(ref, projectId, analysis);
|
||||
|
||||
if (!analysisResponse.ok) {
|
||||
throw new Error('Failed to fetch analysis');
|
||||
}
|
||||
|
||||
const analysisResult = await analysisResponse.json();
|
||||
const contentElement = document.querySelector(`.analysis-content-${ref._id}`);
|
||||
const statusElement = document.querySelector(`.analysis-status-${ref._id}`);
|
||||
|
||||
if (analysisResult.status === 'processing') {
|
||||
// Processing status
|
||||
contentElement.innerHTML = `
|
||||
<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>Analyzing literature content...</span>
|
||||
</div>
|
||||
`;
|
||||
statusElement.innerHTML = '<span class="text-primary">Analysis in progress...</span>';
|
||||
} else if (analysisResult.status === 'completed') {
|
||||
// Completed status
|
||||
let author = 'N/A';
|
||||
let publicationDate = 'N/A';
|
||||
let journal = 'N/A';
|
||||
|
||||
try {
|
||||
if (analysisResult['1. Basic Information']) {
|
||||
const basicInfo = analysisResult['1. Basic Information'];
|
||||
author = basicInfo.author || 'N/A';
|
||||
publicationDate = basicInfo.publication_date || 'N/A';
|
||||
journal = basicInfo.journal_publisher || 'N/A';
|
||||
|
||||
// 更新卡片标题为分析结果中的标题
|
||||
const titleElement = document.querySelector(`#reference-${ref._id} .card-title`);
|
||||
if (titleElement && basicInfo.title) {
|
||||
titleElement.textContent = basicInfo.title;
|
||||
}
|
||||
}
|
||||
|
||||
contentElement.innerHTML = `
|
||||
<p class="mb-1">Author: ${author}</p>
|
||||
<p class="mb-1">Publication Date: ${publicationDate}</p>
|
||||
<p class="mb-1">Journal/Publisher: ${journal}</p>
|
||||
`;
|
||||
statusElement.innerHTML = '<span class="text-success">Analysis completed</span>';
|
||||
} catch (parseError) {
|
||||
console.error('Failed to parse analysis result:', parseError);
|
||||
}
|
||||
} else if (analysisResult.status === 'failed') {
|
||||
// Failed status
|
||||
contentElement.innerHTML = `
|
||||
<div class="text-danger">
|
||||
<i class="bi bi-exclamation-triangle me-2"></i>
|
||||
Analysis failed
|
||||
</div>
|
||||
`;
|
||||
statusElement.innerHTML = `<span class="text-danger">Analysis failed: ${analysisResult.message || 'Unknown error'}</span>`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to get analysis report for literature ${ref._id}:`, error);
|
||||
console.error(`Failed to fetch analysis for reference ${ref._id}:`, error);
|
||||
const contentElement = document.querySelector(`.analysis-content-${ref._id}`);
|
||||
const statusElement = document.querySelector(`.analysis-status-${ref._id}`);
|
||||
|
||||
contentElement.innerHTML = `
|
||||
<div class="text-danger">
|
||||
<i class="bi bi-exclamation-triangle me-2"></i>
|
||||
Failed to get analysis status
|
||||
</div>
|
||||
`;
|
||||
statusElement.innerHTML = '<span class="text-danger">Failed to get analysis status</span>';
|
||||
}
|
||||
return renderReferenceCard(ref, projectId);
|
||||
}));
|
||||
|
||||
referencesList.innerHTML = referencesWithAnalysis.join('');
|
||||
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to load literature list:', error);
|
||||
referencesList.innerHTML = `
|
||||
@@ -686,7 +746,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Upload literature function
|
||||
async function uploadLiterature() {
|
||||
if (!currentProjectId) {
|
||||
alert('Please select a project first');
|
||||
@@ -702,12 +761,10 @@
|
||||
const files = Array.from(e.target.files);
|
||||
if (!files.length) return;
|
||||
|
||||
// 创建进度条卡片
|
||||
// 创建进度条
|
||||
const progressCard = document.createElement('div');
|
||||
progressCard.className = 'card w-100 mb-4';
|
||||
progressCard.style.cssText = `
|
||||
transition: all 0.3s ease;
|
||||
`;
|
||||
progressCard.style.cssText = `transition: all 0.3s ease;`;
|
||||
|
||||
const progressCardBody = document.createElement('div');
|
||||
progressCardBody.className = 'card-body';
|
||||
@@ -746,80 +803,56 @@
|
||||
const titleElement = document.getElementById('referenceAnalysisTitle');
|
||||
titleElement.parentNode.parentNode.insertBefore(progressCard, titleElement.parentNode);
|
||||
|
||||
const updateProgress = (current, total, status) => {
|
||||
const progress = (current / total) * 100;
|
||||
progressFill.style.width = `${progress}%`;
|
||||
progressText.textContent = `${status} (${current}/${total})`;
|
||||
};
|
||||
|
||||
const removeProgressBar = () => {
|
||||
progressCard.style.opacity = '0';
|
||||
progressCard.style.transform = 'translateY(-10px)';
|
||||
setTimeout(() => {
|
||||
progressCard.remove();
|
||||
}, 300);
|
||||
};
|
||||
|
||||
try {
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('reference_title', file.name);
|
||||
const formData = new FormData();
|
||||
files.forEach(file => {
|
||||
formData.append('files', file);
|
||||
});
|
||||
|
||||
try {
|
||||
updateProgress(i + 1, files.length, `Uploading: ${file.name}`);
|
||||
const uploadResponse = await fetch(`${API_BASE_URL}/projects/${currentProjectId}/references`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!uploadResponse.ok) {
|
||||
throw new Error((await uploadResponse.json()).detail || 'Upload failed');
|
||||
}
|
||||
|
||||
const uploadResult = await uploadResponse.json();
|
||||
const referenceId = uploadResult.reference_id;
|
||||
|
||||
updateProgress(i + 1, files.length, `Analyzing: ${file.name}`);
|
||||
const analyzeResponse = await fetch(`${API_BASE_URL}/references/${referenceId}/analyze`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!analyzeResponse.ok) {
|
||||
throw new Error((await analyzeResponse.json()).detail || 'Analysis failed');
|
||||
}
|
||||
|
||||
const reportResponse = await fetch(`${API_BASE_URL}/references/${referenceId}/report`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!reportResponse.ok) {
|
||||
throw new Error('Failed to get analysis report');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Error processing ${file.name}:`, error);
|
||||
// 使用 XMLHttpRequest 来获取上传进度
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.upload.onprogress = function(e) {
|
||||
if (e.lengthComputable) {
|
||||
const percentComplete = (e.loaded / e.total) * 100;
|
||||
progressFill.style.width = percentComplete + '%';
|
||||
progressText.textContent = `Uploading: ${Math.round(percentComplete)}%`;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const uploadPromise = new Promise((resolve, reject) => {
|
||||
xhr.onload = function() {
|
||||
if (xhr.status === 200) {
|
||||
resolve(JSON.parse(xhr.response));
|
||||
} else {
|
||||
reject(new Error('Upload failed'));
|
||||
}
|
||||
};
|
||||
xhr.onerror = () => reject(new Error('Upload failed'));
|
||||
});
|
||||
|
||||
xhr.open('POST', `${API_BASE_URL}/projects/${currentProjectId}/references/batch`);
|
||||
xhr.setRequestHeader('Authorization', `Bearer ${token}`);
|
||||
xhr.send(formData);
|
||||
|
||||
await uploadPromise;
|
||||
|
||||
// 上传完成后移除进度条
|
||||
setTimeout(() => {
|
||||
removeProgressBar();
|
||||
}, 1000);
|
||||
progressCard.style.opacity = '0';
|
||||
progressCard.style.transform = 'translateY(-10px)';
|
||||
setTimeout(() => {
|
||||
progressCard.remove();
|
||||
}, 300);
|
||||
}, 500);
|
||||
|
||||
// 刷新文献列表
|
||||
await loadReferences(currentProjectId);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Upload process failed:', error);
|
||||
console.error('Upload failed:', error);
|
||||
progressCard.remove();
|
||||
progressText.remove();
|
||||
alert(`Upload failed: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -827,79 +860,104 @@
|
||||
}
|
||||
|
||||
// View literature details function
|
||||
async function viewReferenceDetail(referenceId, encodedAnalysis = '') {
|
||||
async function viewReferenceDetail(referenceId) {
|
||||
try {
|
||||
let analysisResult;
|
||||
|
||||
if (encodedAnalysis) {
|
||||
try {
|
||||
analysisResult = JSON.parse(decodeURIComponent(encodedAnalysis));
|
||||
} catch (e) {
|
||||
console.error('Failed to parse encoded analysis:', e);
|
||||
analysisResult = null;
|
||||
const response = await fetch(`${API_BASE_URL}/references/${referenceId}/report`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch analysis report');
|
||||
}
|
||||
|
||||
if (!analysisResult) {
|
||||
const response = await fetch(`${API_BASE_URL}/references/${referenceId}/report`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
const analysisResult = await response.json();
|
||||
let modalContent;
|
||||
|
||||
if (analysisResult.status === 'processing') {
|
||||
modalContent = `
|
||||
<div class="d-flex align-items-center justify-content-center p-5">
|
||||
<div class="spinner-border text-primary me-3" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<h5 class="mb-0">Analyzing literature content, please wait...</h5>
|
||||
</div>
|
||||
`;
|
||||
} else if (analysisResult.status === 'completed') {
|
||||
const formattedReport = JSON.stringify(analysisResult, null, 2)
|
||||
.replace(/[{}"]/g, '')
|
||||
.replace(/},/g, '')
|
||||
.replace(/{ },/g, '')
|
||||
.replace(/^\s*,/gm, '')
|
||||
.replace(/Literature Analysis Report:/, '<div style="text-align: center"><strong>Literature Analysis Report</strong></div>')
|
||||
.replace(/^(\s*\d+\.[^:\n]+:)/gm, '<strong>$1</strong>')
|
||||
.split('\n')
|
||||
.map(line => line.trimEnd())
|
||||
.join('\n');
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to get analysis report');
|
||||
}
|
||||
|
||||
analysisResult = await response.json();
|
||||
modalContent = `
|
||||
<pre class="report-content" style="white-space: pre-wrap; word-wrap: break-word;">${formattedReport}</pre>
|
||||
`;
|
||||
} else if (analysisResult.status === 'failed') {
|
||||
modalContent = `
|
||||
<div class="alert alert-danger">
|
||||
<i class="bi bi-exclamation-triangle me-2"></i>
|
||||
Analysis failed: ${analysisResult.message || 'Unknown error'}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
const formattedReport = JSON.stringify(analysisResult, null, 2)
|
||||
.replace(/[{}"]/g, '')
|
||||
.replace(/},/g, '')
|
||||
.replace(/{ },/g, '')
|
||||
.replace(/^\s*,/gm, '')
|
||||
.replace(/Literature Analysis Report:/, '<div style="text-align: center"><strong>Literature Analysis Report</strong></div>')
|
||||
.replace(/^(\s*\d+\.[^:\n]+:)/gm, '<strong>$1</strong>')
|
||||
.split('\n')
|
||||
.map(line => line.trimEnd())
|
||||
.join('\n');
|
||||
|
||||
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'modal fade';
|
||||
modal.id = 'referenceDetailModal';
|
||||
modal.setAttribute('role', 'dialog');
|
||||
modal.setAttribute('aria-modal', 'true');
|
||||
modal.setAttribute('aria-labelledby', 'referenceDetailModalTitle');
|
||||
|
||||
modal.innerHTML = `
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-dialog modal-lg" style="max-width: 80%; margin: 1.75rem auto;">
|
||||
<div class="modal-content" style="min-height: 80vh;">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Literature Analysis Report</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
<h5 class="modal-title" id="referenceDetailModalTitle">Literature Analysis Report</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<pre class="report-content" style="white-space: pre-wrap; word-wrap: break-word;">${formattedReport}</pre>
|
||||
<div class="modal-body" style="max-height: calc(80vh - 60px); overflow-y: auto;">
|
||||
${modalContent}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(modal);
|
||||
const modalInstance = new bootstrap.Modal(modal);
|
||||
const modalInstance = new bootstrap.Modal(modal, {
|
||||
keyboard: true,
|
||||
focus: true
|
||||
});
|
||||
|
||||
modalInstance.show();
|
||||
|
||||
modal.addEventListener('hidden.bs.modal', () => {
|
||||
document.body.removeChild(modal);
|
||||
modal.addEventListener('hide.bs.modal', () => {
|
||||
const focusedElement = document.activeElement;
|
||||
if (modal.contains(focusedElement)) {
|
||||
focusedElement.blur();
|
||||
}
|
||||
});
|
||||
|
||||
modal.addEventListener('hidden.bs.modal', () => {
|
||||
modal.remove();
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to view details:', error);
|
||||
alert('Failed to get analysis report, please try again');
|
||||
alert('Failed to get literature analysis report, please try again later');
|
||||
}
|
||||
}
|
||||
|
||||
// View literature details from card
|
||||
function viewReferenceDetailFromCard(card) {
|
||||
const referenceId = card.id.replace('reference-', '');
|
||||
const encodedAnalysis = card.getAttribute('data-analysis') || '';
|
||||
viewReferenceDetail(referenceId, encodedAnalysis);
|
||||
viewReferenceDetail(referenceId);
|
||||
}
|
||||
|
||||
// Download literature analysis report function
|
||||
@@ -1015,25 +1073,29 @@
|
||||
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.setAttribute('role', 'dialog');
|
||||
modal.setAttribute('aria-modal', 'true');
|
||||
modal.setAttribute('aria-labelledby', 'chatModalTitle');
|
||||
|
||||
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>
|
||||
<h5 class="modal-title" id="chatModalTitle">Chat with Literature: ${title}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></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;">
|
||||
<div id="chatHistory" class="flex-grow-1 custom-scrollbar" 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...">
|
||||
placeholder="Type your question here..."
|
||||
aria-label="Question input">
|
||||
<button class="btn"
|
||||
style="background: var(--primary-color); color: white;"
|
||||
onclick="sendQuestion('${referenceId}')">
|
||||
onclick="sendQuestion('${referenceId}')"
|
||||
aria-label="Send question">
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
@@ -1043,23 +1105,45 @@
|
||||
`;
|
||||
|
||||
document.body.appendChild(modal);
|
||||
const modalInstance = new bootstrap.Modal(modal);
|
||||
|
||||
// 使用更多的 Modal 选项来控制行为
|
||||
const modalInstance = new bootstrap.Modal(modal, {
|
||||
backdrop: 'static', // 静态背景
|
||||
keyboard: false, // 禁用键盘关闭
|
||||
focus: true // 自动聚焦
|
||||
});
|
||||
|
||||
// 在显示模态框之前添加事件监听器
|
||||
modal.addEventListener('show.bs.modal', () => {
|
||||
// 确保模态框可以正确接收焦点
|
||||
modal.removeAttribute('aria-hidden');
|
||||
});
|
||||
|
||||
// 在隐藏模态框之前清除焦点
|
||||
modal.addEventListener('hide.bs.modal', () => {
|
||||
const focusedElement = document.activeElement;
|
||||
if (modal.contains(focusedElement)) {
|
||||
focusedElement.blur();
|
||||
}
|
||||
});
|
||||
|
||||
// 在模态框完全隐藏后移除它
|
||||
modal.addEventListener('hidden.bs.modal', () => {
|
||||
modal.remove();
|
||||
});
|
||||
|
||||
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
|
||||
@@ -1122,11 +1206,13 @@
|
||||
}
|
||||
|
||||
const chatHistory = document.getElementById('chatHistory');
|
||||
const sendButton = input.nextElementSibling;
|
||||
|
||||
try {
|
||||
// Clear input and disable it
|
||||
// 清空输入并禁用输入框和发送按钮
|
||||
input.value = '';
|
||||
input.disabled = true;
|
||||
sendButton.disabled = true;
|
||||
|
||||
// First, add the user's question to chat history
|
||||
const questionDiv = document.createElement('div');
|
||||
@@ -1236,8 +1322,13 @@
|
||||
chatHistory.appendChild(errorDiv);
|
||||
chatHistory.scrollTop = chatHistory.scrollHeight;
|
||||
} finally {
|
||||
// 重新启用输入框和发送按钮
|
||||
input.disabled = false;
|
||||
input.focus();
|
||||
sendButton.disabled = false;
|
||||
// 将焦点返回到输入框,但不强制聚焦
|
||||
if (document.activeElement === document.body) {
|
||||
input.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -24,6 +24,9 @@ import PyPDF2
|
||||
import numpy as np
|
||||
import zipfile
|
||||
from io import BytesIO, StringIO
|
||||
import aiohttp
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from functools import partial
|
||||
|
||||
# Database Configuration
|
||||
MONGODB_URL = "mongodb://lab:[email protected]:27017/lab"
|
||||
@@ -1603,7 +1606,7 @@ async def analyze_project_data(project_data):
|
||||
print(f"Error calling DeepSeek API: {e}")
|
||||
return None
|
||||
|
||||
async def analyze_reference_document(content: str):
|
||||
async def analyze_reference_document_async(content: str):
|
||||
"""分析文献的基本信息和内容"""
|
||||
system_prompt = """
|
||||
You are an AI assistant tasked with analyzing academic literature.
|
||||
@@ -1635,23 +1638,12 @@ async def analyze_reference_document(content: str):
|
||||
}}
|
||||
"""
|
||||
|
||||
messages = [
|
||||
return await call_deepseek_api_async([
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": user_prompt}
|
||||
]
|
||||
|
||||
try:
|
||||
response = client.chat.completions.create(
|
||||
model="deepseek-chat",
|
||||
messages=messages,
|
||||
response_format={'type': 'json_object'}
|
||||
)
|
||||
return json.loads(response.choices[0].message.content)
|
||||
except Exception as e:
|
||||
print(f"Error calling DeepSeek API for document analysis: {e}")
|
||||
return None
|
||||
])
|
||||
|
||||
async def analyze_reference_value(content_analysis: dict):
|
||||
async def analyze_reference_value_async(content_analysis: dict):
|
||||
"""基于内容分析结果评估文献的价值"""
|
||||
system_prompt = """
|
||||
You are an AI assistant tasked with evaluating the value of academic literature based on its content analysis.
|
||||
@@ -1674,21 +1666,10 @@ async def analyze_reference_value(content_analysis: dict):
|
||||
}}
|
||||
"""
|
||||
|
||||
messages = [
|
||||
return await call_deepseek_api_async([
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": user_prompt}
|
||||
]
|
||||
|
||||
try:
|
||||
response = client.chat.completions.create(
|
||||
model="deepseek-chat",
|
||||
messages=messages,
|
||||
response_format={'type': 'json_object'}
|
||||
)
|
||||
return json.loads(response.choices[0].message.content)
|
||||
except Exception as e:
|
||||
print(f"Error calling DeepSeek API for value evaluation: {e}")
|
||||
return None
|
||||
])
|
||||
|
||||
# 修改函数定义为异步函数
|
||||
async def analyze_reference_summary(reference_data):
|
||||
@@ -2059,70 +2040,6 @@ class ReferenceModel(BaseModel):
|
||||
arbitrary_types_allowed = True
|
||||
json_encoders = {ObjectId: str}
|
||||
|
||||
# 添加文献上传路由
|
||||
@app.post("/lab/projects/{project_id}/references")
|
||||
async def upload_reference(
|
||||
project_id: str,
|
||||
file: UploadFile = File(...),
|
||||
reference_title: str = Form(...),
|
||||
current_user: UserModel = Depends(get_current_user)
|
||||
):
|
||||
"""上传项目相关文献"""
|
||||
db = await get_database()
|
||||
|
||||
try:
|
||||
# 验证项目是否存在且属于当前用户
|
||||
project = await db.projects.find_one({
|
||||
"_id": ObjectId(project_id),
|
||||
"user_id": current_user.id
|
||||
})
|
||||
if not project:
|
||||
raise HTTPException(status_code=404, detail="Project not found or unauthorized access")
|
||||
|
||||
# 验证文件类型(可选)
|
||||
allowed_types = ["application/pdf", "application/msword",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"]
|
||||
if file.content_type not in allowed_types:
|
||||
raise HTTPException(status_code=400, detail="Unsupported file type")
|
||||
|
||||
# 确保上传目录存在
|
||||
os.makedirs(upload_path, exist_ok=True)
|
||||
|
||||
# 生成安全的文件名
|
||||
file_extension = os.path.splitext(file.filename)[1]
|
||||
safe_filename = f"{project_id}_{datetime.now().strftime('%Y%m%d_%H%M%S')}{file_extension}"
|
||||
file_path = os.path.join(upload_path, safe_filename)
|
||||
|
||||
# 保存文件
|
||||
with open(file_path, "wb") as buffer:
|
||||
content = await file.read()
|
||||
buffer.write(content)
|
||||
|
||||
# 创建引用记录
|
||||
reference = {
|
||||
"project_id": ObjectId(project_id),
|
||||
"reference_link": file_path,
|
||||
"reference_title": reference_title or file.filename, # 使用提供的标题或文件名
|
||||
"upload_time": datetime.now(timezone.utc)
|
||||
}
|
||||
|
||||
result = await db.references.insert_one(reference)
|
||||
|
||||
return {
|
||||
"message": "Reference uploaded successfully",
|
||||
"reference_id": str(result.inserted_id),
|
||||
"file_path": file_path,
|
||||
"reference_title": reference["reference_title"]
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f"Detailed error: {str(e)}") # 添加详细错误日志
|
||||
if 'file_path' in locals() and os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Failed to upload reference: {str(e)}"
|
||||
)
|
||||
|
||||
# 删除文献
|
||||
@app.delete("/lab/projects/{project_id}/references/{reference_id}")
|
||||
@@ -2221,89 +2138,6 @@ async def get_reference_report(
|
||||
except Exception as e:
|
||||
print(f"Error closing Redis connection: {e}")
|
||||
|
||||
@app.get("/lab/references/{reference_id}/analyze")
|
||||
async def analyze_reference_data(
|
||||
reference_id: str,
|
||||
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="Reference record not found")
|
||||
|
||||
file_path = reference.get("reference_link")
|
||||
if not file_path or not os.path.exists(file_path):
|
||||
raise HTTPException(status_code=404, detail="Reference file not found")
|
||||
|
||||
# 读取PDF文件内容
|
||||
pdf_content = ""
|
||||
try:
|
||||
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
|
||||
except Exception as e:
|
||||
print(f"Error reading PDF file: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Failed to read PDF file")
|
||||
|
||||
# 限制内容长度
|
||||
MAX_CHARS = 180000
|
||||
pdf_content = pdf_content[:MAX_CHARS]
|
||||
# 分析文档内容
|
||||
document_analysis = await analyze_reference_document(pdf_content)
|
||||
if not document_analysis:
|
||||
raise HTTPException(status_code=500, detail="Failed to analyze document")
|
||||
|
||||
# 分析文献价值
|
||||
value_evaluation = await analyze_reference_value(document_analysis)
|
||||
if not value_evaluation:
|
||||
raise HTTPException(status_code=500, detail="Failed to evaluate value")
|
||||
|
||||
# 合并所有分析结果
|
||||
analysis_result = {
|
||||
**document_analysis,
|
||||
**value_evaluation
|
||||
}
|
||||
|
||||
# 将分析结果保存到db203
|
||||
await redis.select(203)
|
||||
report_key = f"reference_report:{reference_id}"
|
||||
await redis.set(report_key, json.dumps(analysis_result))
|
||||
|
||||
# 更新数据库中的分析状态
|
||||
await db.references.update_one(
|
||||
{"_id": ObjectId(reference_id)},
|
||||
{"$set": {
|
||||
"analysis_status": "completed"
|
||||
}}
|
||||
)
|
||||
|
||||
return analysis_result
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error analyzing reference data: {str(e)}")
|
||||
# 更新数据库中的分析状态为失败
|
||||
if 'reference' in locals():
|
||||
await db.references.update_one(
|
||||
{"_id": ObjectId(reference_id)},
|
||||
{"$set": {
|
||||
"analysis_status": "failed",
|
||||
"analysis_error": 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/{project_id}/analyze_report")
|
||||
async def analyze_reference_summary_report(
|
||||
project_id: str,
|
||||
@@ -2834,6 +2668,258 @@ async def get_reference_qa_history(
|
||||
except Exception as e:
|
||||
print(f"Error closing Redis connection: {e}")
|
||||
|
||||
@app.post("/lab/projects/{project_id}/references/batch")
|
||||
async def batch_upload_references(
|
||||
project_id: str,
|
||||
files: List[UploadFile] = File(...),
|
||||
current_user: UserModel = Depends(get_current_user)
|
||||
):
|
||||
"""批量上传项目相关文献"""
|
||||
db = await get_database()
|
||||
|
||||
try:
|
||||
# 验证项目是否存在且属于当前用户
|
||||
project = await db.projects.find_one({
|
||||
"_id": ObjectId(project_id),
|
||||
"user_id": current_user.id
|
||||
})
|
||||
if not project:
|
||||
raise HTTPException(status_code=404, detail="Project not found or unauthorized access")
|
||||
|
||||
uploaded_references = []
|
||||
|
||||
# 批量上传文件
|
||||
for file in files:
|
||||
# 验证文件类型
|
||||
allowed_types = ["application/pdf", "application/msword",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"]
|
||||
if file.content_type not in allowed_types:
|
||||
continue # 跳过不支持的文件类型
|
||||
|
||||
# 确保上传目录存在
|
||||
os.makedirs(upload_path, exist_ok=True)
|
||||
|
||||
# 生成安全的文件名
|
||||
file_extension = os.path.splitext(file.filename)[1]
|
||||
safe_filename = f"{project_id}_{datetime.now().strftime('%Y%m%d_%H%M%S')}{file_extension}"
|
||||
file_path = os.path.join(upload_path, safe_filename)
|
||||
|
||||
# 保存文件
|
||||
with open(file_path, "wb") as buffer:
|
||||
content = await file.read()
|
||||
buffer.write(content)
|
||||
|
||||
# 创建引用记录
|
||||
reference = {
|
||||
"project_id": ObjectId(project_id),
|
||||
"reference_link": file_path,
|
||||
"reference_title": file.filename,
|
||||
"upload_time": datetime.now(timezone.utc)
|
||||
}
|
||||
|
||||
result = await db.references.insert_one(reference)
|
||||
reference_info = {
|
||||
"reference_id": str(result.inserted_id),
|
||||
"file_path": file_path,
|
||||
"reference_title": reference["reference_title"]
|
||||
}
|
||||
uploaded_references.append(reference_info)
|
||||
|
||||
# 为每个文献创建初始状态
|
||||
redis = await get_redis()
|
||||
try:
|
||||
await redis.select(203)
|
||||
report_key = f"reference_report:{str(result.inserted_id)}"
|
||||
initial_status = {
|
||||
"status": "processing",
|
||||
"message": "Analysis in progress"
|
||||
}
|
||||
await redis.set(report_key, json.dumps(initial_status))
|
||||
finally:
|
||||
await redis.aclose()
|
||||
|
||||
# 在后台启动分析任务
|
||||
if uploaded_references:
|
||||
asyncio.create_task(process_batch_analysis(uploaded_references))
|
||||
|
||||
return {
|
||||
"message": f"Successfully uploaded {len(uploaded_references)} files",
|
||||
"uploaded_files": uploaded_references
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f"Batch upload error: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
async def process_batch_analysis(references: List[dict]):
|
||||
"""批量处理文献分析的后台任务"""
|
||||
redis = await get_redis()
|
||||
|
||||
# 限制并发数量
|
||||
semaphore = asyncio.Semaphore(3)
|
||||
|
||||
async def process_single_reference(ref: dict):
|
||||
async with semaphore:
|
||||
try:
|
||||
reference_id = ref["reference_id"]
|
||||
file_path = ref["file_path"]
|
||||
|
||||
if not os.path.exists(file_path):
|
||||
print(f"Reference file not found: {file_path}")
|
||||
return
|
||||
|
||||
# 异步读取PDF
|
||||
pdf_content = await read_pdf_async(file_path)
|
||||
if not pdf_content:
|
||||
raise Exception("Failed to read PDF content")
|
||||
|
||||
# 异步分析文档
|
||||
document_analysis = await analyze_reference_document_async(pdf_content)
|
||||
if not document_analysis:
|
||||
raise Exception("Failed to analyze document")
|
||||
|
||||
# 等待一小段时间避免API限制
|
||||
await asyncio.sleep(1)
|
||||
|
||||
# 异步分析价值
|
||||
value_evaluation = await analyze_reference_value_async(document_analysis)
|
||||
if not value_evaluation:
|
||||
raise Exception("Failed to evaluate value")
|
||||
|
||||
# 合并结果
|
||||
analysis_result = {
|
||||
**document_analysis,
|
||||
**value_evaluation,
|
||||
"status": "completed"
|
||||
}
|
||||
|
||||
# 保存结果
|
||||
await redis.select(203)
|
||||
report_key = f"reference_report:{reference_id}"
|
||||
await redis.set(report_key, json.dumps(analysis_result))
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error processing reference {ref['reference_id']}: {str(e)}")
|
||||
try:
|
||||
await redis.select(203)
|
||||
report_key = f"reference_report:{ref['reference_id']}"
|
||||
error_status = {
|
||||
"status": "failed",
|
||||
"message": str(e)
|
||||
}
|
||||
await redis.set(report_key, json.dumps(error_status))
|
||||
except Exception as redis_error:
|
||||
print(f"Error updating Redis status: {redis_error}")
|
||||
|
||||
try:
|
||||
# 并发处理所有引用
|
||||
await asyncio.gather(
|
||||
*(process_single_reference(ref) for ref in references)
|
||||
)
|
||||
finally:
|
||||
try:
|
||||
await redis.aclose()
|
||||
except Exception as e:
|
||||
print(f"Error closing Redis connection: {e}")
|
||||
|
||||
# 在全局范围创建线程池
|
||||
pdf_thread_pool = ThreadPoolExecutor(max_workers=3) # 限制并发PDF处理数量
|
||||
|
||||
# 创建异步HTTP客户端会话
|
||||
async def get_aiohttp_session():
|
||||
return aiohttp.ClientSession(
|
||||
base_url="https://api.deepseek.com/v1/", # 添加了末尾的斜杠
|
||||
headers={"Authorization": f"Bearer sk-3027fb3c810b4e17985fa397d41250b9"}
|
||||
)
|
||||
|
||||
async def read_pdf_async(file_path: str) -> str:
|
||||
"""在线程池中异步读取PDF"""
|
||||
def read_pdf():
|
||||
try:
|
||||
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
|
||||
return pdf_content[:180000] # 限制内容长度
|
||||
except Exception as e:
|
||||
print(f"Error reading PDF: {e}")
|
||||
return ""
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
return await loop.run_in_executor(pdf_thread_pool, read_pdf)
|
||||
|
||||
async def call_deepseek_api_async(messages: list) -> dict:
|
||||
"""异步调用DeepSeek API"""
|
||||
async with await get_aiohttp_session() as session:
|
||||
async with session.post("/chat/completions", json={
|
||||
"model": "deepseek-chat",
|
||||
"messages": messages,
|
||||
"response_format": {"type": "json_object"}
|
||||
}) as response:
|
||||
if response.status == 200:
|
||||
data = await response.json()
|
||||
return json.loads(data["choices"][0]["message"]["content"])
|
||||
else:
|
||||
raise Exception(f"API调用失败: {await response.text()}")
|
||||
|
||||
async def process_reference_analysis(reference_id: str, reference: dict):
|
||||
"""后台处理文献分析"""
|
||||
redis = await get_redis()
|
||||
|
||||
try:
|
||||
file_path = reference.get("reference_link")
|
||||
if not file_path or not os.path.exists(file_path):
|
||||
print(f"Reference file not found: {file_path}")
|
||||
return
|
||||
|
||||
# 异步读取PDF
|
||||
pdf_content = await read_pdf_async(file_path)
|
||||
if not pdf_content:
|
||||
raise Exception("Failed to read PDF content")
|
||||
|
||||
# 异步分析文档内容
|
||||
document_analysis = await analyze_reference_document_async(pdf_content)
|
||||
if not document_analysis:
|
||||
print("Failed to analyze document")
|
||||
return
|
||||
|
||||
# 异步分析文献价值
|
||||
value_evaluation = await analyze_reference_value_async(document_analysis)
|
||||
if not value_evaluation:
|
||||
print("Failed to evaluate value")
|
||||
return
|
||||
|
||||
# 合并分析结果
|
||||
analysis_result = {
|
||||
**document_analysis,
|
||||
**value_evaluation
|
||||
}
|
||||
|
||||
# 保存分析结果到db203
|
||||
await redis.select(203)
|
||||
report_key = f"reference_report:{reference_id}"
|
||||
await redis.set(report_key, json.dumps(analysis_result))
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error analyzing reference data: {str(e)}")
|
||||
try:
|
||||
await redis.select(203)
|
||||
report_key = f"reference_report:{reference_id}"
|
||||
error_status = {
|
||||
"status": "failed",
|
||||
"message": str(e)
|
||||
}
|
||||
await redis.set(report_key, json.dumps(error_status))
|
||||
except Exception as redis_error:
|
||||
print(f"Error updating Redis status: {redis_error}")
|
||||
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