1651 lines
71 KiB
HTML
1651 lines
71 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>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, '&')
|
||
.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(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> |