Files
2025-01-21 03:09:10 +00:00

1651 lines
71 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Paper Analysis</title>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.min.css">
<script type="module">
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
window.mermaid = mermaid;
mermaid.initialize({
startOnLoad: true,
theme: 'default',
flowchart: {
useMaxWidth: true,
htmlLabels: true,
curve: 'basis'
}
});
</script>
<style>
/* 基础样式优化 */
body {
background-color: #f8f9fa;
color: #212529;
line-height: 1.6;
}
/* 添加CSS变量 */
:root {
--header-height: 64px;
--sidebar-width: 240px;
--surface-color: #FFFFFF;
--border-color: #E2E8F0;
--primary-color: #6366F1;
--primary-light: #818CF8;
--text-secondary: #64748B;
}
/* 卡片样式现代化 */
.card {
border: none;
border-radius: 12px;
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 {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0,0,0,0.12);
}
.card-body {
padding: 24px;
}
/* 按钮样式优化 */
.btn {
border-radius: 8px;
padding: 6px 10px;
font-weight: 500;
font-size: 0.8rem;
transition: all 0.2s;
line-height: 1;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.btn-sm {
padding: 4px 10px;
font-size: 0.8rem;
border-radius: 8px;
line-height: 1;
}
.btn-warning {
background: linear-gradient(45deg, #6897fe, #3979e0);
border: none;
color: white;
}
.btn-warning:hover {
background: linear-gradient(45deg, #3979e0, #6897fe);
transform: translateY(-1px);
color: white;
}
.btn-info {
background: linear-gradient(45deg, #818CF8, #6366F1);
border: none;
color: white !important;
outline: none;
}
.btn-info:hover,
.btn-info:active,
.btn-info:focus {
background: linear-gradient(45deg, #6366F1, #818CF8);
transform: translateY(-1px);
color: white !important;
outline: none;
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.2);
}
.btn-danger {
background-color: #d3d3d3;
border: none;
color: white;
min-width: 40px;
}
.btn-danger:hover {
background-color: #b8b8b8;
transform: translateY(-1px);
}
/* 导航栏样式 */
.navbar {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 64px;
background: var(--surface-color, #FFFFFF);
display: flex;
align-items: center;
padding: 0 24px;
z-index: 1000;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
}
.navbar-brand {
font-size: 20px;
font-weight: 600;
color: var(--primary-color, #6366F1);
margin-right: 48px;
}
.navbar-menu {
display: flex;
gap: 32px;
}
/* 修改导航栏菜单项样式 */
.navbar-menu-item {
padding: 8px 16px;
cursor: pointer;
border-radius: 8px;
transition: all 0.3s;
color: var(--text-secondary, #64748B);
font-weight: 500;
background: transparent; /* 默认透明背景 */
}
/* 修改悬停和激活状态样式 */
.navbar-menu-item:hover {
background: linear-gradient(145deg, var(--primary-color, #6366F1), var(--primary-light, #818CF8));
color: white;
}
.navbar-menu-item.active {
background: linear-gradient(145deg, var(--primary-color, #6366F1), var(--primary-light, #818CF8));
color: white;
}
.navbar-right {
margin-left: auto;
display: flex;
align-items: center;
gap: 24px;
color: var(--text-secondary, #64748B);
}
/* 更新按钮样式 */
.navbar .btn {
padding: 8px 16px;
border-radius: 8px;
display: flex;
align-items: center;
gap: 8px;
font-weight: 500;
transition: all 0.3s;
}
.navbar .btn-primary {
background: linear-gradient(145deg, var(--primary-color, #6366F1), var(--primary-light, #818CF8));
color: white;
border: none;
}
.navbar .btn-secondary {
background: var(--background-color, #F8FAFC);
color: var(--text-secondary, #64748B);
border: none;
}
.navbar .btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.2);
}
/* 侧边栏样式 */
.sidebar {
position: fixed;
top: 64px;
left: 0;
bottom: 0;
width: 240px;
background: #FFFFFF;
border-right: 1px solid #E2E8F0;
}
.sidebar-menu {
list-style: none;
padding: 16px 0;
}
.sidebar-item {
padding: 12px 20px;
display: flex;
align-items: center;
gap: 12px;
cursor: pointer;
transition: all 0.3s;
margin: 4px 8px;
border-radius: 8px;
color: #64748B;
font-weight: 500;
}
.sidebar-item:hover {
background: linear-gradient(145deg, #6366F1, #818CF8);
color: white;
}
/* 主要内容区域 */
.content-wrapper {
margin-left: 230px;
margin-top: 75px;
padding: 16px 20px;
background-color: #f8f9fa;
min-height: calc(100vh - 64px);
}
.content-header {
margin-bottom: 24px;
}
.content-title {
font-size: 24px;
font-weight: 600;
color: #2c3e50;
margin: 0;
}
.content-body {
background: #fff;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
padding: 24px;
}
.hidden {
display: none;
}
/* 按钮禁用状态样式 */
.btn:disabled,
.btn[disabled] {
background-color: #b8b8b8 !important;
color: white !important;
border: none;
opacity: 0.65 !important;
cursor: not-allowed;
}
.btn:disabled:hover {
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>
<nav class="navbar">
<div class="navbar-brand">Lab</div>
<div class="navbar-menu">
<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='paper.html'">Paper</div>
</div>
<div class="navbar-right">
<a href="https://beta.obscura.work/lab/device.html" target="_blank" class="btn btn-primary">
<i class="bi bi-plus-circle"></i>
Device Registration
</a>
<span class="nav-item" id="userInfo"></span>
<button class="btn btn-secondary" id="logoutBtn" onclick="logout()">
<i class="bi bi-box-arrow-right"></i>
Logout
</button>
</div>
</nav>
<div class="container-fluid">
<div class="row">
<!-- 左侧侧边栏 -->
<div class="col-auto">
<div class="sidebar">
<!-- 项目列表页面的侧边栏 -->
<div class="sidebar-menu" id="projectListSidebar">
<div class="sidebar-item" onclick="showProjectsList()">
<i class="bi bi-list"></i>
<span>Projects List</span>
</div>
<div class="sidebar-item" onclick="goBack()">
<i class="bi bi-arrow-left"></i>
<span>Back</span>
</div>
</div>
<!-- 项目文献页面的侧边栏 -->
<div class="sidebar-menu hidden" id="projectDetailSidebar">
<div class="sidebar-item" onclick="showPaperList()">
<i class="bi bi-list-ul"></i>
<span>Paper List</span>
</div>
<div class="sidebar-item" onclick="downloadReferenceReport()">
<i class="bi bi-file-earmark-text"></i>
<span>Summary</span>
</div>
<div class="sidebar-item" onclick="showProjectsList()">
<i class="bi bi-arrow-left"></i>
<span>Back</span>
</div>
</div>
</div>
</div>
<!-- 右侧主要内容区 -->
<div class="col">
<div class="content-wrapper">
<!-- 项目列表页面 -->
<div id="projectsListPage" class="content-body">
<div class="content-header">
<h3 class="content-title">Projects List</h3>
</div>
<div id="projectsList" class="mt-4">
<!-- 项目列表将在这里动态加载 -->
</div>
</div>
<!-- 项目文献详情页面 -->
<div id="projectDetailPage" class="content-body hidden">
<div class="content-header d-flex justify-content-between align-items-center">
<h3 class="content-title" id="referenceAnalysisTitle">Paper Analysis</h3>
<button class="btn btn-info" onclick="uploadPaper()" style="padding: 10px 20px;">
<i class="bi bi-upload me-2"></i>
Upload
</button>
</div>
<div id="referenceAnalysis" class="mt-4">
<!-- 文献分析内容将在这里动态加载 -->
</div>
</div>
<!-- 文献分析报告页面 -->
<div id="paperReportPage" class="content-body hidden">
<div class="content-header d-flex justify-content-between align-items-center">
<h3 class="content-title" id="paperReportTitle">Paper Summary Report</h3>
<div class="d-flex gap-2">
<button class="btn btn-info" onclick="regenerateReport()">
<i class="bi bi-arrow-clockwise me-2"></i>
Generate
</button>
<button class="btn btn-info" onclick="downloadReport()">
<i class="bi bi-download me-2"></i>
Download
</button>
</div>
</div>
<div id="paperReportContent" class="mt-4">
<!-- 报告内容将在这里动态加载 -->
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
<script>
// 全局变量
const API_BASE_URL = 'https://dev.obscura.work/lab'; // 修改为正确的API地址
let token = localStorage.getItem('token');
let currentProjectId = null;
let currentReportData = null; // 添加存储当前报告数据的变量
// 检查登录状态
if (!token) {
window.location.href = 'https://beta.obscura.work/lab/login.html';
}
// 页面加载时加载项目列表
document.addEventListener('DOMContentLoaded', async () => {
const urlParams = new URLSearchParams(window.location.search);
const projectId = urlParams.get('projectId');
if (projectId) {
// 如果URL中有projectId参数,直接显示该项目的详情
try {
const response = await fetch(`${API_BASE_URL}/projects/${projectId}`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const project = await response.json();
await showProjectDetail(projectId, project.project_name);
} else {
await loadProjectsList();
}
} catch (error) {
console.error('Failed to load project:', error);
await loadProjectsList();
}
} else {
// 如果没有projectId参数,显示项目列表
await loadProjectsList();
}
});
// 加载项目列表
async function loadProjectsList() {
const projectsList = document.getElementById('projectsList');
try {
const response = await fetch(`${API_BASE_URL}/projects`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.status === 401) {
window.location.href = 'https://beta.obscura.work/lab/login.html';
return;
}
if (!response.ok) {
throw new Error('Failed to fetch projects');
}
const projects = await response.json();
if (projects.length === 0) {
projectsList.innerHTML = '<div class="alert alert-info">No projects found</div>';
return;
}
const projectCards = projects.map(project => `
<div class="card" onclick="showProjectDetail('${project._id}', '${project.project_name}')">
<div class="card-body">
<h5 class="card-title">${project.project_name}</h5>
<p class="card-text">
${project.description || 'No description'}
</p>
<div class="text-muted">
Created: ${new Date(project.create_time).toLocaleDateString()}
</div>
</div>
</div>
`).join('');
projectsList.innerHTML = projectCards;
} catch (error) {
console.error('Failed to load projects:', error);
projectsList.innerHTML = `
<div class="alert alert-danger">
Failed to load projects: ${error.message}
<br>
<button class="btn btn-primary mt-2" onclick="loadProjectsList()">
Retry
</button>
</div>`;
}
}
// 显示项目列表页面
function showProjectsList() {
// 显示项目列表页面和对应的侧边栏
document.getElementById('projectsListPage').classList.remove('hidden');
document.getElementById('projectListSidebar').classList.remove('hidden');
// 隐藏其他页面和侧边栏
document.getElementById('projectDetailPage').classList.add('hidden');
document.getElementById('projectDetailSidebar').classList.add('hidden');
document.getElementById('paperReportPage').classList.add('hidden');
currentProjectId = null;
// 重新加载项目列表数据
loadProjectsList();
}
// 显示项目详情页面
async function showProjectDetail(projectId, projectName) {
currentProjectId = projectId;
// 隐藏项目列表页面和对应的侧边栏
document.getElementById('projectsListPage').classList.add('hidden');
document.getElementById('projectListSidebar').classList.add('hidden');
// 显示项目文献页面和对应的侧边栏
document.getElementById('projectDetailPage').classList.remove('hidden');
document.getElementById('projectDetailSidebar').classList.remove('hidden');
// 隐藏报告页面
document.getElementById('paperReportPage').classList.add('hidden');
document.getElementById('referenceAnalysisTitle').textContent = `Paper Analysis - ${projectName}`;
await loadReferences(projectId);
}
// Add show paper analysis page function
async function showReferenceAnalysis() {
try {
// Hide other pages
document.querySelectorAll('.page-content').forEach(page => {
page.classList.remove('active');
});
// Show paper analysis page
document.getElementById('referenceAnalysisPage').classList.add('active');
// Set title
document.getElementById('referenceAnalysisTitle').textContent =
`Paper Analysis - ${document.getElementById('projectDetailTitle').textContent}`;
// Load paper list
await loadReferences(currentProjectId);
} catch (error) {
console.error('Failed to show Paper analysis page:', error);
alert('Failed to show Paper analysis page, please try again');
}
}
// paper card rendering function
function renderReferenceCard(reference, projectId) {
const uploadTime = reference.upload_time;
const cardId = `reference-${reference._id}`;
return `
<div class="card mb-3" id="${cardId}" data-paper-title="${reference.reference_title}">
<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">${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 Paper content...</span>
</div>
</div>
<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}')">
<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>
`;
}
// Load paper list function
async function loadReferences(projectId) {
const referencesList = document.getElementById('referenceAnalysis');
try {
// Get paper list
const response = await fetch(`${API_BASE_URL}/projects/${projectId}/references`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.status === 401) {
window.location.href = 'https://beta.obscura.work/lab/login.html';
return;
}
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail || 'Failed to fetch references');
}
const references = await response.json();
if (!Array.isArray(references)) {
throw new Error('Invalid data format: expected an array');
}
if (references.length === 0) {
referencesList.innerHTML = '<div class="alert alert-info">No paper data, please upload paper</div>';
return;
}
// Render paper cards
const referenceCards = references.map(ref => renderReferenceCard(ref, projectId)).join('');
referencesList.innerHTML = referenceCards;
// Get analysis status and results for each paper
references.forEach(async (ref) => {
try {
const checkAnalysisStatus = async () => {
const analysisResponse = await fetch(`${API_BASE_URL}/references/${ref._id}/report`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
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 Paper content...</span>
</div>
`;
statusElement.innerHTML = '<span class="text-primary">Analysis in progress...</span>';
// 如果还在处理中,5秒后再次检查
setTimeout(checkAnalysisStatus, 10000);
} 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>`;
}
};
// 开始首次检查
await checkAnalysisStatus();
} catch (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>';
}
});
} catch (error) {
console.error('Failed to load Paper list:', error);
referencesList.innerHTML = `
<div class="alert alert-danger">
Failed to load Paper list: ${error.message}
<br>
<button class="btn btn-primary mt-2" onclick="loadReferences('${projectId}')">
Retry
</button>
</div>`;
}
}
async function uploadPaper() {
if (!currentProjectId) {
alert('Please select a project first');
return;
}
const input = document.createElement('input');
input.type = 'file';
input.accept = '.pdf,.doc,.docx';
input.multiple = true;
input.onchange = async function(e) {
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;`;
const progressCardBody = document.createElement('div');
progressCardBody.className = 'card-body';
const progressInner = document.createElement('div');
progressInner.style.cssText = `
width: 100%;
height: 6px;
background: rgba(99, 102, 241, 0.1);
border-radius: 3px;
margin-top: 8px;
overflow: hidden;
`;
const progressFill = document.createElement('div');
progressFill.style.cssText = `
height: 100%;
width: 0;
background: linear-gradient(45deg, var(--primary-color), var(--primary-light));
border-radius: 3px;
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
`;
const progressText = document.createElement('div');
progressText.style.cssText = `
font-size: 0.875rem;
font-weight: 500;
color: var(--text-secondary);
`;
progressInner.appendChild(progressFill);
progressCardBody.appendChild(progressText);
progressCardBody.appendChild(progressInner);
progressCard.appendChild(progressCardBody);
const titleElement = document.getElementById('referenceAnalysisTitle');
titleElement.parentNode.parentNode.insertBefore(progressCard, titleElement.parentNode);
try {
const formData = new FormData();
files.forEach(file => {
formData.append('files', file);
});
// 使用 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(() => {
progressCard.style.opacity = '0';
progressCard.style.transform = 'translateY(-10px)';
setTimeout(() => {
progressCard.remove();
}, 300);
}, 500);
// 刷新文献列表
await loadReferences(currentProjectId);
} catch (error) {
console.error('Upload failed:', error);
progressCard.remove();
alert(`Upload failed: ${error.message}`);
}
};
input.click();
}
// View paper details function
async function viewReferenceDetail(referenceId) {
try {
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');
}
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 Paper 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(/Paper Analysis Report:/, '<div style="text-align: center; margin-bottom: 20px;"><strong style="font-size: 1.5rem;">Paper Analysis</strong></div>')
.replace(/^(\s*\d+\.[^:\n]+:)/gm, '<strong style="color: #6366F1;font-size: 1.2rem;">$1</strong>')
.replace(/(author|publication_date|title|journal_publisher|document_type):/g, '<span style="font-weight: bold; font-size: 1rem;">$1:</span>')
.replace(/(abstract|research_purpose|methodology|main_arguments|conclusions|innovations|flowchart):/g, '<span style="font-weight: bold; font-size: 1rem;">$1:</span>')
.replace(/(academic_contribution|practical_significance|limitations|implications):/g, '<span style="font-weight: bold; font-size: 1rem;">$1:</span>')
.split('\n')
.map(line => `<p style="margin: 0 0 10px;">${line.trimEnd()}</p>`)
.join('');
// 检查是否有流程图数据
let flowchartData = analysisResult.flowchart || 'No flowchart data available';
modalContent = `
<div class="d-flex">
<div class="flex-grow-1 pe-3" style="width: 70%;">
<h5 class="mb-2" style="font-size: 1.2rem; font-weight: 600;">Report</h5>
<div style="background-color: white; padding: 10px 35px 10px 35px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); height: calc(80vh - 30px);">
<pre class="report-content custom-scrollbar" style="white-space: pre-wrap; word-wrap: break-word; height: 100%; overflow-y: auto; scrollbar-width: 4px; scrollbar-color: #d3d3d3 transparent; scrollbar-gutter: stable; margin: 0;padding: 10px 20px 10px 20px;">
${formattedReport}
</pre>
</div>
</div>
<div class="flex-grow-1 ps-3" style="width: 30%;">
<h5 class="mb-2" style="font-size: 1.2rem; font-weight: 600;">FlowChart</h5>
<div class="mermaid-wrapper custom-scrollbar" style="background-color: white; padding: 10px 35px 10px 35px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); height: calc(80vh - 30px); overflow: auto; scrollbar-width: 4px; scrollbar-color: #d3d3d3 transparent; scrollbar-gutter: stable;padding: 10px 20px 10px;">
<div class="mermaid">
${flowchartData}
</div>
</div>
</div>
</div>
`;
} 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 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" style="max-width: 70%; margin: 1.75rem auto;">
<div class="modal-content" style="min-height: 90vh;">
<div class="modal-header">
<h5 class="modal-title" id="referenceDetailModalTitle">Paper Analysis</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" style="outline: none; box-shadow: none;"></button>
</div>
<div class="modal-body" style="padding: 20px;">
${modalContent}
</div>
</div>
</div>
`;
document.body.appendChild(modal);
const modalInstance = new bootstrap.Modal(modal, {
backdrop: 'static', // 禁用点击空白处关闭
keyboard: false // 禁用 ESC 键关闭
});
modalInstance.show();
// 初始化 Mermaid 图表
if (analysisResult.status === 'completed' && analysisResult.flowchart) {
// 在模态框显示后重新初始化 Mermaid
modal.addEventListener('shown.bs.modal', async () => {
try {
// 清除之前的图表
const mermaidDiv = modal.querySelector('.mermaid');
if (mermaidDiv) {
mermaidDiv.innerHTML = analysisResult.flowchart;
// 重新初始化 mermaid
await window.mermaid.init();
// 如果初始化失败,尝试重新渲染
if (!mermaidDiv.querySelector('svg')) {
window.mermaid.contentLoaded();
}
}
} catch (error) {
console.error('Failed to initialize mermaid:', error);
const mermaidDiv = modal.querySelector('.mermaid');
if (mermaidDiv) {
mermaidDiv.innerHTML = `
<div class="alert alert-warning">
<i class="bi bi-exclamation-triangle me-2"></i>
Failed to render flowchart. Please try closing and reopening this dialog.
</div>
`;
}
}
});
}
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 later');
}
}
// View paper details from card
function viewReferenceDetailFromCard(card) {
const referenceId = card.id.replace('reference-', '');
viewReferenceDetail(referenceId);
}
// Download paper analysis report function
async function downloadReferenceReport() {
if (!currentProjectId) {
alert('Please select a project first');
return;
}
try {
// 1. 检查是否有文献
const referencesList = document.getElementById('referenceAnalysis');
if (referencesList && referencesList.innerHTML.includes('No Paper data')) {
alert('Please upload Paper first!');
return;
}
// 显示报告页面
document.getElementById('projectsListPage').classList.add('hidden');
document.getElementById('projectDetailPage').classList.add('hidden');
const reportPage = document.getElementById('paperReportPage');
reportPage.classList.remove('hidden');
// Show loading status
const showLoading = (message) => {
document.getElementById('paperReportContent').innerHTML = `
<div class="d-flex align-items-center justify-content-center" style="min-height: 200px;">
<div class="text-center">
<div class="spinner-border text-primary mb-3" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<h5 class="mb-0">${message}</h5>
<p class="text-muted mt-2">This may take a few minutes</p>
</div>
</div>
`;
};
showLoading('Getting paper analysis report...');
// 2. Check if report already exists
const existingReport = await fetch(`${API_BASE_URL}/references/${currentProjectId}/summary_report`, {
headers: { 'Authorization': `Bearer ${token}` }
});
if (existingReport.ok) {
const report = await existingReport.json();
currentReportData = report;
document.getElementById('paperReportContent').innerHTML = formatReportContent(report);
return;
}
// If no report exists, show generate button
document.getElementById('paperReportContent').innerHTML = `
<div class="text-center text-muted">
<i class="bi bi-file-text display-4"></i>
<p class="mt-3">No report available. Please click the "Generate" button to create a new report.</p>
</div>
`;
} catch (error) {
document.getElementById('paperReportContent').innerHTML = `
<div class="alert alert-danger">
Operation failed: ${error.message}
<button class="btn btn-primary mt-2" onclick="downloadReferenceReport()">
Retry
</button>
</div>
`;
}
}
// 添加格式化报告内容的函数
function formatReportContent(jsonData) {
if (!jsonData) return '<div class="alert alert-info">No report data available</div>';
return `<div class="card">
<div class="card-body">
<pre style="white-space: pre-wrap; word-wrap: break-word;">${
JSON.stringify(jsonData, null, 2)
.replace(/[{}"]/g, '')
.replace(/},/g, '')
.replace(/{ },/g, '')
.replace(/^\s*,/gm, '')
.replace(/Paper Summary Report:/, '<div style="text-align: center"><strong style="font-size: 24px;">Paper Summary Report</strong></div>')
.replace(/^(\s*\d+\.[^:\n]+:)/gm, '<strong>$1</strong>')
.split('\n')
.map(line => line.trimEnd())
.join('\n')
}</pre>
</div>
</div>`;
}
// Delete paper function
async function deleteReference(projectId, referenceId) {
if (!confirm('Are you sure you want to delete this Paper? This action cannot be undone.')) {
return;
}
try {
const response = await fetch(`${API_BASE_URL}/projects/${projectId}/references/${referenceId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const card = document.getElementById(`reference-${referenceId}`);
if (card) {
card.remove();
}
const referencesList = document.getElementById('referenceAnalysis');
if (!referencesList.children.length) {
referencesList.innerHTML = '<div class="alert alert-info">No Paper data, please upload</div>';
}
alert('Deleted successfully');
} else {
const error = await response.json();
alert(`Failed to delete Paper: ${error.detail}`);
}
} catch (error) {
console.error('Failed to delete Paper:', error);
alert('Failed to delete Paper, please try again');
}
}
// 返回上一页
function goBack() {
window.location.href = 'https://beta.obscura.work/lab/lab.html';
}
// Add chat dialog function
async function openChatDialog(referenceId) {
const card = document.getElementById(`reference-${referenceId}`);
if (!card) {
console.error('Card not found');
return;
}
const paperTitle = card.querySelector('.card-title').textContent;
const modal = document.createElement('div');
modal.className = 'modal fade';
modal.id = 'chatModal';
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" id="chatModalTitle">${paperTitle}</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 custom-scrollbar" style="overflow-y: auto; margin-bottom: 15px; padding: 10px; word-wrap: break-word; word-break: break-word;">
<!-- Chat history will be loaded here -->
</div>
<div class="input-group">
<input type="text" id="questionInput" class="form-control"
placeholder="Type your question here..."
aria-label="Question input">
<button class="btn"
style="background: var(--primary-color); color: white;"
onclick="sendQuestion('${referenceId}')"
aria-label="Send question">
Send
</button>
</div>
</div>
</div>
</div>
`;
document.body.appendChild(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();
// 加载聊天历史
await loadChatHistory(referenceId);
// 添加回车键事件监听器
const input = document.getElementById('questionInput');
input.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
sendQuestion(referenceId);
}
});
}
// 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" style="width: 100%;">
<div class="d-flex align-items-start mb-2">
<div class="me-2" style="flex-shrink: 0;">👤</div>
<div class="bg-light rounded p-2" style="flex: 1; min-width: 0; overflow-wrap: break-word; word-break: break-word;">
${chat.question}
</div>
</div>
<div class="d-flex align-items-start">
<div class="me-2" style="flex-shrink: 0;">🤖</div>
<div class="bg-primary bg-opacity-10 rounded p-2" style="flex: 1; min-width: 0;">
<pre style="margin: 0; white-space: pre-wrap; word-wrap: break-word; word-break: break-word; overflow-wrap: break-word; font-family: inherit;"><code style="white-space: pre-wrap; word-wrap: break-word; word-break: break-word; overflow-wrap: break-word; font-family: inherit; background: none; padding: 0;">${processMarkdownContent(chat.answer)}</code></pre>
</div>
</div>
<div class="text-end text-muted small mt-1">
${new Date(chat.timestamp).toLocaleString()}
</div>
</div>
`).join('');
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>';
}
}
function processMarkdownContent(content) {
try {
// 如果内容是JSON字符串,先解析它
if (typeof content === 'string' && content.startsWith('"') && content.endsWith('"')) {
content = JSON.parse(content);
}
// 处理可能的嵌套代码块缩进,但不添加最外层的markdown包装
content = content.replace(/```(\w+)\n([\s\S]*?)```/g, (match, lang, code) => {
// 为代码块添加缩进
const indentedCode = code.split('\n').map(line => line).join('\n');
return '```' + lang + '\n' + indentedCode + '\n```';
});
// 转义HTML特殊字符以正确显示
content = content
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
return content;
} catch (e) {
console.error('Error processing markdown content:', e);
return content;
}
}
// 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');
const sendButton = input.nextElementSibling;
try {
input.value = '';
input.disabled = true;
sendButton.disabled = true;
const questionDiv = document.createElement('div');
questionDiv.className = 'mb-3';
questionDiv.innerHTML = `
<div class="d-flex align-items-start mb-2">
<div class="me-2" style="flex-shrink: 0;">👤</div>
<div class="bg-light rounded p-2" style="flex: 1; min-width: 0; overflow-wrap: break-word; word-break: break-word;">
${question}
</div>
</div>
`;
chatHistory.appendChild(questionDiv);
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;
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');
}
await new Promise(resolve => setTimeout(resolve, 1000));
}
loadingDiv.remove();
const answerDiv = document.createElement('div');
answerDiv.className = 'd-flex align-items-start mb-3';
answerDiv.innerHTML = `
<div class="me-2" style="flex-shrink: 0;">🤖</div>
<div class="bg-primary bg-opacity-10 rounded p-2" style="flex: 1; min-width: 0;">
<pre style="margin: 0; white-space: pre-wrap; word-wrap: break-word; word-break: break-word; overflow-wrap: break-word; font-family: inherit;"><code style="white-space: pre-wrap; word-wrap: break-word; word-break: break-word; overflow-wrap: break-word; font-family: inherit; background: none; padding: 0;">${processMarkdownContent(result.answer)}</code></pre>
</div>
`;
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" style="word-wrap: break-word; word-break: break-word;">
Failed to get answer: ${error.message}
</div>
`;
chatHistory.appendChild(errorDiv);
chatHistory.scrollTop = chatHistory.scrollHeight;
} finally {
input.disabled = false;
sendButton.disabled = false;
if (document.activeElement === document.body) {
input.focus();
}
}
}
// 添加显示文献列表的函数
function showPaperList() {
// 隐藏报告页面
document.getElementById('paperReportPage').classList.add('hidden');
// 显示文献列表页面
document.getElementById('projectDetailPage').classList.remove('hidden');
// 如果有当前项目ID,重新加载文献列表
if (currentProjectId) {
loadReferences(currentProjectId);
}
}
// 添加下载报告功能
function downloadReport() {
if (!currentReportData) {
alert('No report available to download');
return;
}
try {
// 使用原始的JSON数据创建下载内容
const downloadContent = JSON.stringify(currentReportData, null, 2);
// 创建Blob对象
const blob = new Blob([downloadContent], { type: 'application/json' });
const url = window.URL.createObjectURL(blob);
// 创建临时下载链接
const a = document.createElement('a');
a.href = url;
a.download = `paper_analysis_report_${new Date().toISOString().split('T')[0]}.json`;
// 触发下载
document.body.appendChild(a);
a.click();
// 清理
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
} catch (error) {
console.error('Failed to download report:', error);
alert('Failed to download report');
}
}
// 添加重新生成报告功能
async function regenerateReport() {
if (!currentProjectId) {
alert('Please select a project first');
return;
}
try {
// 显示加载状态
const showLoading = (message) => {
document.getElementById('paperReportContent').innerHTML = `
<div class="d-flex align-items-center justify-content-center" style="min-height: 200px;">
<div class="text-center">
<div class="spinner-border text-primary mb-3" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<h5 class="mb-0">${message}</h5>
<p class="text-muted mt-2">This may take a few minutes</p>
</div>
</div>
`;
};
showLoading('Regenerating report...');
// 直接调用重新生成API
const generateResponse = await fetch(`${API_BASE_URL}/references/${currentProjectId}/analyze_report?force=true`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!generateResponse.ok) {
throw new Error((await generateResponse.json()).detail || 'Failed to regenerate report');
}
// 监控状态
let attempts = 0;
const maxAttempts = 24; // 2 minutes (5 seconds × 24)
const statusCheck = setInterval(async () => {
try {
const statusResponse = await fetch(`${API_BASE_URL}/references/${currentProjectId}/analysis_status`, {
headers: { 'Authorization': `Bearer ${token}` }
});
const status = await statusResponse.json();
attempts++;
if (status.status === 'completed') {
clearInterval(statusCheck);
const finalReport = await fetch(`${API_BASE_URL}/references/${currentProjectId}/summary_report`, {
headers: { 'Authorization': `Bearer ${token}` }
});
const report = await finalReport.json();
currentReportData = report;
document.getElementById('paperReportContent').innerHTML = formatReportContent(report);
}
else if (status.status === 'failed') {
clearInterval(statusCheck);
throw new Error(status.error || 'Report regeneration failed');
}
else if (attempts >= maxAttempts) {
clearInterval(statusCheck);
document.getElementById('paperReportContent').innerHTML = `
<div class="alert alert-warning">
Report regeneration is taking longer than expected. Please refresh and try again later.
<button class="btn btn-primary mt-2" onclick="regenerateReport()">
Retry
</button>
</div>
`;
}
else {
showLoading(`${status.status === 'processing' ? 'Regenerating report' : 'Waiting'}...`);
}
} catch (error) {
clearInterval(statusCheck);
document.getElementById('paperReportContent').innerHTML = `
<div class="alert alert-danger">
Failed to regenerate report: ${error.message}
<button class="btn btn-primary mt-2" onclick="regenerateReport()">
Retry
</button>
</div>
`;
}
}, 5000);
} catch (error) {
document.getElementById('paperReportContent').innerHTML = `
<div class="alert alert-danger">
Operation failed: ${error.message}
<button class="btn btn-primary mt-2" onclick="regenerateReport()">
Retry
</button>
</div>
`;
}
}
</script>
</body>
</html>