Files
zydi-paper/paper.html
T
2025-01-17 08:19:30 +00:00

1236 lines
51 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Papers</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/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">Papers</div>
</nav>
<div class="container-fluid">
<div class="row">
<!-- 左侧侧边栏 -->
<div class="col-auto">
<div class="sidebar">
<!-- 文献页面的侧边栏 -->
<div class="sidebar-menu hidden" id="DetailSidebar">
<div class="sidebar-item" onclick="showPaperList()">
<i class="bi bi-list-ul"></i>
<span>Paper List</span>
</div>
</div>
</div>
</div>
<!-- 右侧主要内容区 -->
<div class="col">
<div class="content-wrapper">
<!-- 文献详情页面 -->
<div id="DetailPage" class="content-body hidden">
<div class="content-header d-flex justify-content-between align-items-center">
<h3 class="content-title" id="paperTitle">Paper List</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="paper" class="mt-4">
<!-- 文献分析内容将在这里动态加载 -->
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
// 全局变量
const API_BASE_URL = 'https://dev.obscura.work/paper'; // 修改为正确的API地址
let currentReportData = null; // 添加存储当前报告数据的变量
// 添加一个全局变量来存储论文报告数据
const paperReports = new Map();
// Add show paper analysis page function
async function showpaper() {
try {
// Hide other pages
document.querySelectorAll('.page-content').forEach(page => {
page.classList.remove('active');
});
// Show paper analysis page
document.getElementById('paperPage').classList.add('active');
// Set title
document.getElementById('paperTitle').textContent =
`Paper Analysis`;
// Load paper list
await loadPapers();
} 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 renderPaperCard(paper) {
const uploadTime = paper.upload_time;
const cardId = `paper-${paper._id}`;
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="DetailFromCard(this.closest('.card'))">
<h5 class="card-title">${paper.paper_title}</h5>
<div class="card-text analysis-content-${paper._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(); DetailFromCard(this.closest('.card'))">
View Details
</button>
<button class="btn btn-danger btn-sm"
onclick="event.stopPropagation(); deletepaper('${paper._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('${paper._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-${paper._id}">Analysis in progress...</span>
</div>
</div>
`;
}
// Load paper list function
async function loadPapers() {
const papersList = document.getElementById('paper');
try {
// Get paper list
const response = await fetch(`${API_BASE_URL}/papers`);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail || 'Failed to fetch papers');
}
const papers = await response.json();
if (!Array.isArray(papers)) {
throw new Error('Invalid data format: expected an array');
}
if (papers.length === 0) {
papersList.innerHTML = '<div class="alert alert-info">No paper, please upload paper</div>';
return;
}
// Render paper cards
const paperCards = papers.map(ref => renderPaperCard(ref)).join('');
papersList.innerHTML = paperCards;
// Get analysis status and results for each paper
papers.forEach(async (ref) => {
try {
const checkAnalysisStatus = async () => {
const analysisResponse = await fetch(`${API_BASE_URL}/${ref._id}/report`);
if (!analysisResponse.ok) {
throw new Error('Failed to fetch analysis');
}
const analysisResult = await analysisResponse.json();
// 存储报告数据
paperReports.set(ref._id, analysisResult);
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(`#paper-${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 paper ${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);
papersList.innerHTML = `
<div class="alert alert-danger">
Failed to load Paper list: ${error.message}
<br>
<button class="btn btn-primary mt-2" onclick="loadpapers()">
Retry
</button>
</div>`;
}
}
async function uploadPaper() {
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('paperTitle');
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}/upload`);
xhr.send(formData);
await uploadPromise;
// 上传完成后移除进度条
setTimeout(() => {
progressCard.style.opacity = '0';
progressCard.style.transform = 'translateY(-10px)';
setTimeout(() => {
progressCard.remove();
}, 300);
}, 500);
// 刷新文献列表
await loadPapers();
} catch (error) {
console.error('Upload failed:', error);
progressCard.remove();
alert(`Upload failed: ${error.message}`);
}
};
input.click();
}
// View paper details function
async function viewDetail(paperId) {
try {
const response = await fetch(`${API_BASE_URL}/${paperId}/report`);
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 = 'paperDetailModal';
modal.setAttribute('role', 'dialog');
modal.setAttribute('aria-modal', 'true');
modal.setAttribute('aria-labelledby', 'paperDetailModalTitle');
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="paperDetailModalTitle">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 DetailFromCard(card) {
const paperId = card.id.replace('paper-', '');
viewDetail(paperId);
}
// Delete paper function
async function deletePaper(paperId) {
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}/delete/${paperId}`, {
method: 'DELETE'
});
if (response.ok) {
const card = document.getElementById(`paper-${paperId}`);
if (card) {
card.remove();
}
const papersList = document.getElementById('paper');
if (!papersList.children.length) {
papersList.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');
}
}
// Add chat dialog function
async function openChatDialog(paperId) {
try {
// 使用已获取的报告数据
const report = paperReports.get(paperId);
let paperTitle = 'Chat';
// 从报告中获取标题
if (report && report.status === 'completed' && report['1. Basic Information'] && report['1. Basic Information'].title) {
paperTitle = report['1. Basic Information'].title;
}
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;">
<!-- 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('${paperId}')"
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(paperId);
// 添加回车键事件监听器
const input = document.getElementById('questionInput');
input.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
sendQuestion(paperId);
}
});
} catch (error) {
console.error('Failed to open chat dialog:', error);
alert('Failed to open chat dialog');
}
}
// Load chat history function
async function loadChatHistory(paperId) {
const chatHistory = document.getElementById('chatHistory');
try {
const response = await fetch(`${API_BASE_URL}/${paperId}/qa/history`);
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" style="word-break: break-word;">
${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">
<pre style="white-space: pre-wrap; word-wrap: break-word; margin: 0;"><code style="word-break: break-word;">${MarkdownContent(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 MarkdownContent(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(paperId) {
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">👤</div>
<div class="bg-light rounded p-2 flex-grow-1" style="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}/${paperId}/qa`, {
method: 'POST',
headers: {
'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}`);
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">🤖</div>
<div class="bg-primary bg-opacity-10 rounded p-2 flex-grow-1">
<pre style="white-space: pre-wrap; word-wrap: break-word; margin: 0;"><code style="word-break: break-word;">${MarkdownContent(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">
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('DetailPage').classList.remove('hidden');
}
// 添加下载报告功能
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');
}
}
// 在页面加载完成后自动显示文献列表
document.addEventListener('DOMContentLoaded', function() {
showPaperList();
loadPapers();
});
</script>
</body>
</html>