1236 lines
51 KiB
HTML
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, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
|
|
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> |