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

4305 lines
188 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Lab</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">
<style>
/* 基础样式优化 */
body {
background-color: #f8f9fa;
color: #212529;
line-height: 1.6;
}
/* 卡片样式现代化 */
.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;
}
.card-title {
font-size: 1.25rem; /* 20px */
font-weight: 600; /* 稍微加粗 */
margin-bottom: 0.75rem;
color: #2c3e50; /* 深色系,提高对比度 */
}
.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; /* 14px */
transition: all 0.2s;
line-height: 1; /* 优化行高 */
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
backdrop-filter: blur(8px);
transition: all 0.3s ease;
}
.btn-sm { /* 更小尺寸的按钮 */
padding: 4px 10px;
font-size: 0.8rem; /* 13px */
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-success {
background: linear-gradient(45deg, #1fbff4, #61caf7); /* 明亮蓝色 */
border: none;
color: white;
}
.btn-success:hover {
background: linear-gradient(45deg, #61caf7, #1fbff4);
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);
}
.btn-secondary {
background: linear-gradient(45deg, #a5a4a4, #7b8389);
border: none;
}
.btn-secondary:hover {
background: linear-gradient(45deg, #7b8389, #a5a4a4);
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);
}
/* 调整主内容区域的上边距,以适应固定导航栏 */
#mainContent {
margin-top: 64px;
}
/* 图表容器优化 */
.charts-container {
display: flex;
flex-direction: column; /* 垂直排列 */
gap: 20px; /* 图表之间的间距 */
width: 100%;
padding: 20px;
}
.chart-wrapper {
width: 100%;
background: white;
border-radius: 8px;
padding: 15px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.chart-container {
height: 300px; /* 每个图表的固定高度 */
width: 100%;
}
.chart-wrapper h5 {
margin: 0 0 10px 0;
color: #333;
font-size: 16px;
}
/* 数据显示区域优化 */
.data-container {
background-color: white;
padding: 20px;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
}
/* 表单控件美化 */
.form-control {
border-radius: 8px;
border: 1px solid #dee2e6;
padding: 10px 16px;
transition: all 0.2s;
}
.form-control:focus {
border-color: #4a6bff;
box-shadow: 0 0 0 3px rgba(74,107,255,0.1);
}
/* 模态框优化 */
.modal-content {
border: none;
border-radius: 16px;
}
.modal-header {
border-bottom: 1px solid #eee;
padding: 20px 24px;
}
.modal-body {
padding: 24px;
}
/* 实验数据显示区域 */
#realTimeData {
height: 300px;
border-radius: 12px;
background: white;
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
}
/* 加载动画优化 */
.spinner-border {
width: 1.5rem;
height: 1.5rem;
border-width: 0.2em;
}
/* 警告提示框美化 */
.alert {
border: none;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
}
/* 现代化表格容器样式 */
.table-responsive {
flex: 1;
overflow-y: auto;
margin-bottom: 16px;
max-height: 250px;
border-radius: 12px;
background: #ffffff;
padding: 8px;
}
/* 现代化表格样式 */
.table-sm {
width: 100%;
border-collapse: separate;
border-spacing: 0;
}
.table-sm th {
padding: 12px 16px;
font-size: 0.8rem;
font-weight: 600;
color: #475569;
background: #fafafa;
text-transform: uppercase;
letter-spacing: 0.5px;
border-bottom: 2px solid #fafafa;
}
.table-sm td {
padding: 12px 16px;
font-size: 0.75rem;
color: #334155;
border-bottom: 1px solid #ffffff;
transition: background-color 0.2s ease;
}
.table-sm tr:hover td {
background-color: #ffffff;
}
/* 设备操作按钮容器样式 */
.device-actions {
margin-top: auto;
padding-top: 16px;
border-top: 1px solid #ffffff;
}
/* 列布局样式 */
.col-md-4 {
margin-bottom: 24px;
}
/* 自定义滚动条样式 */
.table-responsive::-webkit-scrollbar {
width: 6px;
height: 6px;
}
.table-responsive::-webkit-scrollbar-track {
background: #ffffff;
border-radius: 8px;
}
.table-responsive::-webkit-scrollbar-thumb {
background: #e8ebf0;
border-radius: 8px;
transition: background 0.2s ease;
}
.table-responsive::-webkit-scrollbar-thumb:hover {
background: #e8ebf0;
}
/* 表格空状态样式 */
.table-sm tr:last-child td {
border-bottom: none;
}
/* 添加表格斑马纹 */
.table-sm tr:nth-child(even) {
background-color: #fafafa;
}
/* 添加表格单元格动画效果 */
.table-sm td {
position: relative;
overflow: hidden;
}
.table-sm td::after {
content: '';
position: absolute;
left: 0;
bottom: 0;
width: 100%;
height: 1px;
background: #3b82f6;
transform: scaleX(0);
transition: transform 0.3s ease;
}
.table-sm tr:hover td::after {
transform: scaleX(1);
}
.hidden {
display: none !important;
}
.page-content {
display: none;
}
.page-content.active {
display: block;
}
.charts-container {
display: flex;
flex-wrap: wrap;
gap: 20px;
margin: 20px 0;
}
.chart-wrapper {
background: white;
padding: 15px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
width: calc(50% - 10px);
min-width: 300px;
}
.chart-container {
position: relative;
height: 250px;
width: 100%;
}
/* 按钮禁用状态样式 */
.btn:disabled,
.btn[disabled] {
background-color: #b8b8b8 !important; /* 使用 !important 确保覆盖其他样式 */
color: white !important;
border: none;
opacity: 0.65 !important; /* 防止 Bootstrap 默认的透明度 */
cursor: not-allowed;
}
/* 确保禁用状态下的渐变背景也变为灰色 */
.btn-warning:disabled,
.btn-primary:disabled,
.btn-info:disabled {
background: #b8b8b8 !important;
background-image: none !important;
}
/* 禁用状态下取消悬停效果 */
.btn:disabled:hover {
transform: none !important;
box-shadow: 0 2px 4px rgba(0,0,0,0.1) !important;
}
/* 禁用状态下的按钮样式 */
.btn-success:disabled,
.btn-success[disabled] {
background: #adb0b4 !important; /* 使用纯色背景 */
background-image: none !important; /* 移除渐变效果 */
background-color: #adb0b4 !important;
border-color: #adb0b4 !important;
color: #fff !important;
opacity: 0.65 !important;
pointer-events: none;
transform: none !important;
}
/* 确保禁用状态下不会显示悬停效果 */
.btn-success:disabled:hover,
.btn-success[disabled]:hover {
background: #adb0b4 !important;
background-image: none !important;
background-color: #adb0b4 !important;
border-color: #adb0b4 !important;
transform: none !important;
}
/* 侧边栏样式 */
.sidebar {
position: fixed;
top: var(--header-height);
left: 0;
bottom: 0;
width: var(--sidebar-width);
background: var(--surface-color);
border-right: 1px solid var(--border-color);
}
.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: var(--text-secondary);
font-weight: 500;
}
.sidebar-item:hover {
background: linear-gradient(145deg, var(--primary-color), var(--primary-light));
color: white;
}
.sidebar-item.active {
background: linear-gradient(145deg, var(--primary-color), var(--primary-light));
color: white;
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.2);
}
.sidebar-item .bi {
font-size: 1.2rem;
}
/* 添加CSS变量 */
:root {
--header-height: 64px;
--sidebar-width: 240px;
--surface-color: #FFFFFF;
--border-color: #E2E8F0;
--primary-color: #6366F1;
--primary-light: #818CF8;
--text-secondary: #64748B;
}
/* 移除卡片样式 */
.sidebar.card {
border: none;
box-shadow: none;
background: none;
}
.sidebar .card-body {
padding: 0;
}
/* 主要内容区域 */
.main-content {
margin-left: var(--sidebar-width);
margin-top: var(--header-height);
padding: 24px 32px;
}
.page-header {
margin-bottom: 24px;
display: flex;
justify-content: space-between;
align-items: center;
}
.page-title {
font-size: 24px;
font-weight: 600;
background: linear-gradient(45deg, var(--primary-color), var(--primary-light));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
/* 更新卡片样式 */
.card {
background: var(--surface-color);
border-radius: 16px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
transition: transform 0.3s;
border: none;
}
.card:hover {
transform: translateY(-2px);
}
.card-header {
padding: 20px 24px;
border-bottom: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
background: transparent;
}
.card-title {
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
}
.card-body {
padding: 24px;
}
/* 更新按钮样式 */
.btn {
padding: 10px 20px;
border-radius: 8px;
border: none;
cursor: pointer;
transition: all 0.3s;
font-weight: 500;
display: inline-flex;
align-items: center;
gap: 8px;
}
.btn-primary {
background: linear-gradient(145deg, var(--primary-color), var(--primary-light));
color: white;
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.2);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(99, 102, 241, 0.3);
}
.btn-secondary {
background: var(--background-color);
color: var(--text-secondary);
}
.btn-secondary:hover {
background: var(--border-color);
}
/* 更新表单控件样式 */
.form-control {
padding: 10px 16px;
border: 1px solid var(--border-color);
border-radius: 8px;
font-size: 14px;
transition: all 0.3s;
background: var(--background-color);
color: var(--text-primary);
}
.form-control:focus {
border-color: var(--primary-color);
outline: none;
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
}
/* 告部分切换样式 */
.report-section {
display: none;
}
.report-section.active {
display: block;
}
#chartsContainer {
display: flex;
flex-direction: column; /* 垂直排列 */
gap: 20px; /* 图表之间的间距 */
width: 100%;
padding: 20px;
}
.chart-wrapper {
width: 100%;
background: white;
border-radius: 8px;
padding: 15px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.chart-container {
height: 300px; /* 每个图表的固定高度 */
width: 100%;
}
.chart-wrapper h5 {
margin: 0 0 10px 0;
color: #333;
font-size: 16px;
}
.chart-data-panel {
background: #f8f9fa;
border-radius: 12px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
height: 100%;
min-height: 300px;
}
.custom-data-list {
display: flex;
flex-direction: column;
gap: 15px;
}
.custom-data-item {
background: white;
padding: 15px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
transition: transform 0.2s;
}
.custom-data-item:hover {
transform: translateY(-2px);
}
.custom-data-name {
font-size: 0.9rem;
color: #666;
margin-bottom: 5px;
}
.custom-data-value {
font-size: 1.5rem;
font-weight: 600;
color: #2c3e50;
display: flex;
align-items: baseline;
}
.custom-data-unit {
font-size: 0.9rem;
color: #666;
margin-left: 5px;
}
/* 响应式布局优化 */
@media (max-width: 991.98px) {
.chart-data-panel {
margin-top: 20px;
min-height: auto;
}
}
#mainContent {
display: block;
}
#mainContent.hidden {
display: none !important;
}
</style>
</head>
<body>
<!-- 替换原有的导航栏 -->
<nav class="navbar">
<div class="navbar-brand">Lab</div>
<div class="navbar-menu">
<div class="navbar-menu-item active" onclick="window.open('lab.html')">Project</div>
<div class="navbar-menu-item" onclick="window.open('device.html', '_blank')">Device</div>
<div class="navbar-menu-item" onclick="window.open('reports.html', '_blank')">Reports</div>
<div class="navbar-menu-item" onclick="window.open('paper.html', '_blank')">Paper</div>
</div>
<div class="navbar-right">
<a href="https://beta.obscura.work/lab/device-register.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 id="mainContent" class="hidden main-content">
<!-- Project Management Page -->
<div id="projectsPage" class="page-content">
<div class="row">
<!-- 左侧侧边栏 -->
<div class="col-auto">
<div class="sidebar">
<div class="sidebar-menu">
<div class="sidebar-item" onclick="showCreateProjectModal()">
<i class="bi bi-plus-circle"></i>
<span>Add Project</span>
</div>
<div class="sidebar-item" onclick="showProjectsFilter()">
<i class="bi bi-funnel"></i>
<span>Filter</span>
</div>
<div class="sidebar-item" onclick="showProjectsSort()">
<i class="bi bi-sort-down"></i>
<span>Sort</span>
</div>
</div>
</div>
</div>
<!-- 右侧主要内容区 -->
<div class="col ps-0 pe-4">
<div class="card w-100">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-4">
<h3 class="page-title">Projects</h3>
</div>
<div id="projectsList">
<!-- 项目列表将通过 JavaScript 动态加载 -->
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Experiment Page -->
<div id="experimentPage" class="container-fluid mt-4 page-content">
<div class="row">
<!-- 左侧侧边栏 -->
<div class="col-auto">
<div class="sidebar">
<div class="sidebar-menu">
<div class="sidebar-item" onclick="showExperimentMemo(currentExperimentId)">
<i class="bi bi-journal-text"></i>
<span>Memo</span>
</div>
<div class="sidebar-item" onclick="viewProjectDetail(currentProjectId)">
<i class="bi bi-arrow-left"></i>
<span>Back</span>
</div>
<!-- 添加分割线 -->
<hr class="my-3 mx-3" style="border-top: 1px solid rgba(0,0,0,0.1);">
<!-- 实验历史记录部分 -->
<div class="history-section mt-4">
<h6 class="sidebar-title px-3 mb-3">Experiment History</h6>
<div id="experimentHistory" class="experiment-history px-3">
<!-- 实验历史记录将通过 JavaScript 动态加载 -->
</div>
</div>
</div>
</div>
</div>
<!-- 右侧主要内容区 -->
<div class="col ps-0 pe-4"> <!-- 添加右侧内边距 pe-4 -->
<div class="card w-100">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-4">
<h3 id="experimentTitle"></h3>
<div class="d-flex gap-2 align-items-center">
<button id="experimentChatBtn" 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; display: none;"
onclick="openExperimentChatDialog(currentExperimentId)">
<i class="bi bi-chat-dots" style="font-size: 1.2rem;"></i>
</button>
</div>
</div>
<p id="experimentDescription" class="mb-4"></p>
<div id="experimentDevices" class="mb-4">
<!-- Device list will be displayed here dynamically -->
</div>
<div class="btn-group" role="group">
<button id="startExperimentBtn" class="btn btn-info" onclick="startExperiment()">
<i class="bi bi-play-fill"></i>
Start
</button>
<button id="stopExperimentBtn" class="btn btn-info" onclick="stopExperiment()" disabled>
<i class="bi bi-stop-fill"></i>
Stop
</button>
<button id="completeExperimentBtn" class="btn btn-warning" onclick="completeExperiment()">
<i class="bi bi-check2"></i>
Complete
</button>
</div>
<!-- Real-time Data Display Section -->
<div id="realTimeDataSection" class="mt-4" style="display: none;">
<h4>Real-time Data</h4>
<div class="row">
<!-- 图表容器 -->
<div class="col" style="flex: 1; min-width: 0;">
<div id="chartsContainer" class="charts-container">
<!-- Charts will be displayed here dynamically -->
</div>
</div>
<!-- 自定义数据面板 -->
<div class="col-auto" style="min-width: 200px; max-width: 300px; width: auto;">
<div id="customDataPanel" class="chart-data-panel">
<h5 class="mb-3">Data</h5>
<div class="custom-data-list">
<!-- 自定义数据将在这里动态显示 -->
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Project Detail Page -->
<div id="projectDetailPage" class="container-fluid mt-4 page-content">
<div class="row">
<!-- 左侧边栏 -->
<div class="col-auto">
<div class="sidebar">
<div class="sidebar-menu">
<div class="sidebar-item" onclick="showExperimentContent()">
<i class="bi bi-list-ul"></i>
<span>Experiment</span>
</div>
<div class="sidebar-item" onclick="displayProjectReport(currentProjectId)">
<i class="bi bi-file-earmark-text"></i>
<span>Report</span>
</div>
<div class="sidebar-item" onclick="showProjectMemo(currentProjectId)">
<i class="bi bi-journal-text"></i>
<span>Memo</span>
</div>
<div class="sidebar-item" onclick="event.preventDefault(); window.open('https://beta.obscura.work/lab/paper.html?projectId=' + currentProjectId, '_blank')">
<i class="bi bi-book"></i>
<span>Paper</span>
</div>
<div class="sidebar-item" onclick="showPage('projects')">
<i class="bi bi-arrow-left"></i>
<span>Back</span>
</div>
</div>
</div>
</div>
<!-- 主要内容区 -->
<div class="col ps-0 pe-4">
<!-- 实验内容区域 -->
<div id="experimentContent" class="card w-100">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-4">
<h3 id="projectDetailTitle">Project Details</h3>
<button class="btn btn-primary" onclick="showCreateExperimentModal(currentProjectId)">
<i class="bi bi-plus-circle"></i>
Add
</button>
</div>
<div id="projectExperiments">
<!-- Experiment list will be displayed here dynamically -->
</div>
</div>
</div>
<!-- 报告内容区域 -->
<div id="reportContent" class="card w-100 hidden">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-4">
<h3>Project Report</h3>
<div class="d-flex gap-2 align-items-center">
<button class="btn btn-info d-inline-flex align-items-center" onclick="regenerateReport()" style="font-size: 0.85rem; height: 40px;">
<i class="bi bi-arrow-clockwise me-2"></i>
Generate
</button>
<button class="btn btn-primary d-inline-flex align-items-center" onclick="downloadReport()" style="font-size: 0.85rem; height: 40px;">
<i class="bi bi-download me-2"></i>
Download
</button>
<button id="reportChatBtn" class="btn rounded-circle d-flex align-items-center justify-content-center"
style="width: 45px; height: 45px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); background: var(--primary-color); color: white; display: none;"
onclick="openReportChatDialog(currentProjectId, 'Project Report Chat')">
<i class="bi bi-chat-dots" style="font-size: 1.2rem;"></i>
</button>
</div>
</div>
<div id="projectReport">
<!-- Report content will be displayed here -->
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Create Project Modal -->
<div class="modal fade" id="createProjectModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Create New Project</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="createProjectForm">
<div class="mb-3">
<label class="form-label">Project Name</label>
<input type="text" class="form-control" id="projectName" required>
</div>
<div class="mb-3">
<label class="form-label">Project Description</label>
<textarea class="form-control" id="projectDescription" required></textarea>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" onclick="createProject()">Create</button>
</div>
</div>
</div>
</div>
<!-- Create Experiment Modal -->
<div class="modal fade" id="createExperimentModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Create New Experiment</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="createExperimentForm">
<input type="hidden" id="experimentProjectId">
<div class="mb-3">
<label class="form-label">Experiment Name</label>
<input type="text" class="form-control" id="experimentName" required>
</div>
<div class="mb-3">
<label class="form-label">Experiment Description</label>
<textarea class="form-control" id="experimentDescription" required></textarea>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" onclick="createExperiment()">Create</button>
</div>
</div>
</div>
</div>
<!-- Add Device to Experiment Modal -->
<div class="modal fade" id="addDeviceToExperimentModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Add Device to Experiment</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div id="availableDevicesList">
<!-- Available devices list will be displayed here dynamically -->
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
</div>
</div>
</div>
</div>
<!-- 修改备忘录模态框的样式 -->
<div class="modal fade" id="projectMemoModal" tabindex="-1">
<div class="modal-dialog modal-lg modal-dialog-centered"> <!-- 添加 modal-lg 和 modal-dialog-centered -->
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Project Memo</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="projectMemoForm">
<div class="mb-3">
<label class="form-label">Memo Content</label>
<textarea class="form-control" id="memoContent" rows="15" style="resize: vertical; min-height: 300px;" required></textarea>
</div>
<div class="text-muted small" id="memoCreateTime"></div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" onclick="saveMemo()">Save</button>
</div>
</div>
</div>
</div>
<!-- 修改备忘录模态框的样式 -->
<div class="modal fade" id="experimentMemoModal" tabindex="-1">
<div class="modal-dialog modal-lg modal-dialog-centered"> <!-- 添加 modal-lg 和 modal-dialog-centered -->
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Experiment Memo</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="experimentMemoForm">
<div class="mb-3">
<label class="form-label">Memo Content</label>
<textarea class="form-control" id="experimentMemoContent" rows="15" style="resize: vertical; min-height: 300px;" required></textarea>
</div>
<div class="text-muted small" id="experimentMemoCreateTime"></div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" onclick="saveExperimentMemo()">Save</button>
</div>
</div>
</div>
</div>
<!-- Projects Filter Modal -->
<div class="modal fade" id="projectsFilterModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Filter Projects</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="projectFilterForm">
<div class="mb-3">
<label class="form-label">Project Name</label>
<input type="text" class="form-control" id="filterProjectName">
</div>
<div class="mb-3">
<label class="form-label">Date Range</label>
<div class="row">
<div class="col">
<input type="date" class="form-control" id="filterStartDate">
</div>
<div class="col">
<input type="date" class="form-control" id="filterEndDate">
</div>
</div>
</div>
<div class="mb-3">
<label class="form-label">Status</label>
<select class="form-select" id="filterStatus">
<option value="">All</option>
<option value="active">Active</option>
<option value="completed">Completed</option>
</select>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="resetProjectFilters()">Reset</button>
<button type="button" class="btn btn-primary" onclick="applyProjectFilters()">Apply</button>
</div>
</div>
</div>
</div>
<!-- Projects Sort Modal -->
<div class="modal fade" id="projectsSortModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Sort Projects</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="projectSortForm">
<div class="mb-3">
<label class="form-label">Sort By</label>
<select class="form-select" id="sortField">
<option value="create_time">Creation Date</option>
<option value="project_name">Project Name</option>
<option value="status">Status</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">Order</label>
<select class="form-select" id="sortOrder">
<option value="desc">Descending</option>
<option value="asc">Ascending</option>
</select>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" onclick="applyProjectSort()">Apply</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="formulaModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Add Formula</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="formulaForm">
<!-- 添加隐藏字段存储传感器名称和设备ID -->
<input type="hidden" id="formulaSensorName">
<input type="hidden" id="formulaDeviceId">
<input type="hidden" id="formulaExperimentId">
<div class="mb-3">
<label for="dataName" class="form-label">Data Name</label>
<input type="text" class="form-control" id="dataName" required>
</div>
<div class="mb-3">
<label for="dataUnit" class="form-label">Data Unit</label>
<input type="text" class="form-control" id="dataUnit">
</div>
<div class="mb-3">
<label for="formula" class="form-label">Formula</label>
<input type="text" class="form-control" id="formula" required>
<small class="text-muted">Use value to represent sensor value, e.g.: value * 2</small>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" onclick="addFormula()">Add</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
// API base URL
const API_BASE_URL = 'https://dev.obscura.work/lab';
let token = localStorage.getItem('token');
let ws = null;
// 检查登录状态
document.addEventListener('DOMContentLoaded', () => {
// 检查登录状态
if (!token) {
window.location.href = 'login.html';
return;
}
// 显示主内容
const mainContent = document.getElementById('mainContent');
if (mainContent) {
mainContent.classList.remove('hidden');
}
// 默认显示项目页面
showPage('projects');
// 加载项目列表
loadProjects();
});
// 修改 showMainContent 函数
function showMainContent() {
document.getElementById('mainContent').classList.remove('hidden');
// 默认显示项目管理页面
showPage('projects');
}
// 修改 logout 函数
function logout() {
token = null;
localStorage.removeItem('token');
if (ws) {
ws.close();
}
window.location.href = 'login.html';
}
// Show/hide form functions
function showLoginForm() {
document.getElementById('loginForm').classList.remove('hidden');
document.getElementById('registerForm').classList.add('hidden');
document.getElementById('mainContent').classList.add('hidden');
}
function showRegisterForm() {
document.getElementById('loginForm').classList.add('hidden');
document.getElementById('registerForm').classList.remove('hidden');
document.getElementById('mainContent').classList.add('hidden');
}
// User registration
async function register(event) {
event.preventDefault();
const userData = {
username: document.getElementById('regUsername').value,
password: document.getElementById('regPassword').value,
email: document.getElementById('regEmail').value,
name: document.getElementById('regName').value,
institution: document.getElementById('regInstitution').value
};
try {
const response = await fetch(`${API_BASE_URL}/register`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(userData)
});
if (response.ok) {
alert('Registration successful!');
showLoginForm();
} else {
const error = await response.json();
alert(`Registration failed: ${error.detail}`);
}
} catch (error) {
alert('Registration failed: ' + error.message);
}
}
// User login
async function login(event) {
event.preventDefault();
const formData = new FormData();
formData.append('username', document.getElementById('loginUsername').value);
formData.append('password', document.getElementById('loginPassword').value);
try {
const response = await fetch(`${API_BASE_URL}/token`, {
method: 'POST',
body: formData
});
if (response.ok) {
const data = await response.json();
token = data.access_token;
localStorage.setItem('token', token);
showMainContent();
loadProjects();
} else {
const error = await response.json();
alert(`Login failed: ${error.detail}`);
}
} catch (error) {
alert('Login failed: ' + error.message);
}
}
// Add global variable
let currentProjectId = null;
// 添加全局变量来存储过滤和排序状态
let projectFilters = {
name: '',
startDate: '',
endDate: '',
status: ''
};
let projectSort = {
field: 'create_time',
order: 'desc'
};
// 显示过滤模态框
function showProjectsFilter() {
// 设置当前过滤值
document.getElementById('filterProjectName').value = projectFilters.name;
document.getElementById('filterStartDate').value = projectFilters.startDate;
document.getElementById('filterEndDate').value = projectFilters.endDate;
document.getElementById('filterStatus').value = projectFilters.status;
// 显示模态框
new bootstrap.Modal(document.getElementById('projectsFilterModal')).show();
}
// 显示排序模态框
function showProjectsSort() {
// 设置当前排序值
document.getElementById('sortField').value = projectSort.field;
document.getElementById('sortOrder').value = projectSort.order;
// 显示模态框
new bootstrap.Modal(document.getElementById('projectsSortModal')).show();
}
// 重置过滤条件
function resetProjectFilters() {
projectFilters = {
name: '',
startDate: '',
endDate: '',
status: ''
};
// 重置表单
document.getElementById('projectFilterForm').reset();
// 重新加载项目列表
loadProjects();
// 关闭模态框
bootstrap.Modal.getInstance(document.getElementById('projectsFilterModal')).hide();
}
// 应用过滤条件
function applyProjectFilters() {
projectFilters = {
name: document.getElementById('filterProjectName').value,
startDate: document.getElementById('filterStartDate').value,
endDate: document.getElementById('filterEndDate').value,
status: document.getElementById('filterStatus').value
};
// 重新加载项目表
loadProjects();
// 关闭模态框
bootstrap.Modal.getInstance(document.getElementById('projectsFilterModal')).hide();
}
// 应用排序
function applyProjectSort() {
projectSort = {
field: document.getElementById('sortField').value,
order: document.getElementById('sortOrder').value
};
// 重新加载项目列表
loadProjects();
// 关闭模态框
bootstrap.Modal.getInstance(document.getElementById('projectsSortModal')).hide();
}
// Modify load projects function
async function loadProjects() {
try {
const response = await fetch(`${API_BASE_URL}/projects`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
let projects = await response.json();
// 应用过滤
if (projectFilters.name) {
projects = projects.filter(project =>
project.project_name.toLowerCase().includes(projectFilters.name.toLowerCase())
);
}
if (projectFilters.startDate) {
projects = projects.filter(project =>
new Date(project.create_time) >= new Date(projectFilters.startDate)
);
}
if (projectFilters.endDate) {
projects = projects.filter(project =>
new Date(project.create_time) <= new Date(projectFilters.endDate)
);
}
if (projectFilters.status) {
projects = projects.filter(project =>
project.status === projectFilters.status
);
}
// 应用排序
projects.sort((a, b) => {
const aValue = a[projectSort.field];
const bValue = b[projectSort.field];
if (projectSort.order === 'asc') {
return aValue > bValue ? 1 : -1;
} else {
return aValue < bValue ? 1 : -1;
}
});
// 渲染项目列表
const projectsList = document.getElementById('projectsList');
const projectsHtml = projects.map(project => `
<div class="card mb-3" style="cursor: pointer;" onclick="viewProjectDetail('${project._id}', '${project.project_name}')">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start">
<div>
<h5 class="card-title">${project.project_name}</h5>
<p class="card-text">${project.description}</p>
</div>
<div class="btn-group">
<button class="btn btn-danger btn-sm" onclick="event.stopPropagation(); deleteProject('${project._id}')">
Delete
</button>
</div>
</div>
<div class="mt-2 text-muted small">
<span>Created: ${new Date(project.create_time).toLocaleString()}</span>
${project.status ? `<span class="ms-2">Status: ${project.status}</span>` : ''}
</div>
</div>
</div>
`).join('');
projectsList.innerHTML = projectsHtml;
}
} catch (error) {
console.error('Failed to load projects:', error);
alert('Failed to load projects, please try again');
}
}
// Add view project details function
async function viewProjectDetail(projectId, projectName = null) {
currentProjectId = projectId;
// If project name is not provided, fetch it from the server
if (!projectName) {
try {
const response = await fetch(`${API_BASE_URL}/projects/${projectId}`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const projectData = await response.json();
projectName = projectData.project_name;
}
} catch (error) {
console.error('获取项目详情失败:', error);
}
}
document.getElementById('projectDetailTitle').textContent = projectName;
// Load project experiments
const experimentsHtml = await loadExperiments(projectId);
document.getElementById('projectExperiments').innerHTML = experimentsHtml;
// Show project details page
document.querySelectorAll('.page-content').forEach(page => {
page.classList.remove('active');
});
document.getElementById('projectDetailPage').classList.add('active');
// 始终显示实验列表
document.getElementById('experimentContent').classList.remove('hidden');
document.getElementById('reportContent').classList.add('hidden');
}
// Add load single experiment device function
async function loadExperimentDevices(experimentId) {
try {
const response = await fetch(`${API_BASE_URL}/experiments/${experimentId}/devices`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const devices = await response.json();
const devicesHtml = devices.map(device => `
<div class="card mb-3">
<div class="card-body">
<h5 class="card-title">${device.device_name}</h5>
<p class="text-muted">Type: ${device.device_type}</p>
<div class="table-responsive">
<table class="table table-bordered">
<thead>
<tr>
<th>Sensor Name</th>
<th>Function</th>
<th>Unit</th>
</tr>
</thead>
<tbody>
${device.sensors.map(sensor => `
<tr>
<td>${sensor.name}</td>
<td>${sensor.function}</td>
<td>${sensor.unit}</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
</div>
</div>
`).join('');
document.getElementById('experimentDevices').innerHTML = devicesHtml;
}
} catch (error) {
console.error('Failed to load experiment devices:', error);
}
}
async function loadExperiments(projectId) {
try {
const response = await fetch(`${API_BASE_URL}/experiments?project_id=${projectId}`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const experiments = await response.json();
experiments.sort((a, b) => {
if (a.status !== b.status) {
return a.status === 'completed' ? 1 : -1;
}
return new Date(b.create_time) - new Date(a.create_time);
});
const experimentsHtml = await Promise.all(experiments.map(async exp => {
const devicesResponse = await fetch(`${API_BASE_URL}/experiments/${exp._id}/devices`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
let devicesHtml = '';
if (devicesResponse.ok) {
const devices = await devicesResponse.json();
devicesHtml = `
<div id="experiment-devices-${exp._id}" class="mt-4">
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-4 g-4">
${devices.map(device => `
<div class="col device-card" id="device-${device.user_device_id}">
<div class="card h-100">
<div class="card-body">
<h6 class="card-title">${device.device_name}</h6>
<p class="card-text small">
Type: ${device.device_type}<br>
Channel Number: ${device.device_number}
</p>
<button class="btn btn-danger btn-sm"
onclick="event.stopPropagation(); removeDevice('${device.user_device_id}', '${exp._id}')">
Remove
</button>
</div>
</div>
</div>
`).join('')}
</div>
</div>
`;
}
const buttonsHtml = exp.status === 'completed' ? `
<div class="btn-group" role="group" onclick="event.stopPropagation();">
<button class="btn btn-sm btn-info " onclick="downloadExperimentData('${exp._id}')">
Download Data
</button>
<button class="btn btn-sm btn-secondary " onclick="downloadExperimentanalyze('${exp._id}')">
Download Report
</button>
</div>
` : `
<div class="btn-group" role="group" onclick="event.stopPropagation();">
<button class="btn btn-sm btn-primary " onclick="showAddDeviceToExperiment('${exp._id}')">
Add Device
</button>
</div>
`;
return `
<div class="card mb-2" style="cursor: pointer;" onclick="enterExperiment('${exp._id}')">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start">
<div>
<h6 class="card-title" data-experiment-id="${exp._id}">${exp.experiment_name}</h6>
<p class="card-text">${exp.description}</p>
</div>
<button class="btn btn-danger btn-sm" onclick="event.stopPropagation(); deleteExperiment('${exp._id}')">
Delete
</button>
</div>
${buttonsHtml}
${devicesHtml}
</div>
</div>
`;
}));
return experimentsHtml.join('');
}
console.error('Failed to load experiments:', response.statusText);
return '';
} catch (error) {
console.error('Failed to load experiments:', error);
return '';
}
}
async function removeDevice(deviceId, experimentId = null) {
let message = 'Are you sure you want to remove this device from the current experiment?';
if (!confirm(message)) {
return;
}
try {
const url = `${API_BASE_URL}/experiments/${experimentId}/devices/${deviceId}`;
const response = await fetch(url, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const result = await response.json();
// 直接移除设备卡片元素
const deviceCard = document.getElementById(`device-${deviceId}`);
if (deviceCard) {
deviceCard.remove();
}
alert(result.message);
} else {
const error = await response.json();
alert(`Failed to remove device: ${error.detail}`);
}
} catch (error) {
console.error('Failed to remove device:', error);
alert('Failed to remove device, please try again');
}
}
// Create new project
async function createProject() {
const projectData = {
project_name: document.getElementById('projectName').value,
description: document.getElementById('projectDescription').value
};
try {
const response = await fetch(`${API_BASE_URL}/projects`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(projectData)
});
if (response.ok) {
bootstrap.Modal.getInstance(document.getElementById('createProjectModal')).hide();
loadProjects();
} else {
const error = await response.json();
alert(`Failed to create project: ${error.detail}`);
}
} catch (error) {
alert('Failed to create project: ' + error.message);
}
}
// Helper function to show create project modal
function showCreateProjectModal() {
new bootstrap.Modal(document.getElementById('createProjectModal')).show();
}
function showCreateDeviceModal() {
new bootstrap.Modal(document.getElementById('createDeviceModal')).show();
}
// Show specific page
function showPage(pageId, clickedElement = null) {
// Hide all pages
document.querySelectorAll('.page-content').forEach(page => {
page.classList.remove('active');
});
// Remove active class from all menu items
document.querySelectorAll('.navbar-menu-item').forEach(item => {
item.classList.remove('active');
});
// Show selected page
const selectedPage = document.getElementById(pageId + 'Page');
if (selectedPage) {
selectedPage.classList.add('active');
}
// Set clicked menu item as active
if (clickedElement) {
clickedElement.classList.add('active');
} else {
// 如果没有传入点击元素,根据 pageId 找到对应菜单项
const menuItem = document.querySelector(`.navbar-menu-item[onclick*="showPage('${pageId}'"]`);
if (menuItem) {
menuItem.classList.add('active');
}
}
}
// Show create experiment modal
function showCreateExperimentModal(projectId) {
document.getElementById('experimentProjectId').value = projectId;
new bootstrap.Modal(document.getElementById('createExperimentModal')).show();
}
// Modify createExperiment function
async function createExperiment() {
const projectId = document.getElementById('experimentProjectId').value;
const experimentData = {
experiment_name: document.getElementById('experimentName').value,
description: document.getElementById('experimentDescription').value,
project_id: projectId
};
try {
const response = await fetch(`${API_BASE_URL}/experiments`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(experimentData)
});
if (response.ok) {
// Close modal
bootstrap.Modal.getInstance(document.getElementById('createExperimentModal')).hide();
document.getElementById('createExperimentForm').reset();
// Refresh project details page
await viewProjectDetail(projectId, document.getElementById('projectDetailTitle').textContent);
// Optional: Display success message
alert('Experiment created successfully!');
} else {
const error = await response.json();
console.error('Failed to create experiment:', error.detail);
alert(`Failed to create experiment: ${error.detail}`);
}
} catch (error) {
console.error('Failed to create experiment:', error.message);
alert('Failed to create experiment: ' + error.message);
}
}
// Modify add device to experiment function
async function addDeviceToExperiment(experimentId, userDeviceId) {
try {
const response = await fetch(`${API_BASE_URL}/experiments/${experimentId}/devices/${userDeviceId}`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
// 关闭模态框
const modal = bootstrap.Modal.getInstance(document.getElementById('addDeviceToExperimentModal'));
modal.hide();
// 立即重新加载该实验的设备列表
// 重新加载实验列表以更新设备显示
const experimentsList = document.getElementById('projectExperiments');
const experimentsHtml = await loadExperiments(currentProjectId);
experimentsList.innerHTML = experimentsHtml;
alert('Device added successfully!');
} else {
const error = await response.json();
throw new Error(error.detail || 'Failed to add device');
}
} catch (error) {
console.error('Failed to add device to experiment:', error);
alert('Failed to add device to experiment: ' + error.message);
}
}
// Modify show add device to experiment modal function
async function showAddDeviceToExperiment(experimentId) {
try {
const response = await fetch(`${API_BASE_URL}/userdevices`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const devices = await response.json();
// 只显示 active 状态的设备
const activeDevices = devices.filter(device => device.status === 'active');
const devicesList = document.getElementById('availableDevicesList');
// 如果没有活动设备,显示提示信息
if (activeDevices.length === 0) {
devicesList.innerHTML = `
<div class="text-center p-3">
<p class="text-muted">No active devices available</p>
</div>
`;
} else {
devicesList.innerHTML = activeDevices.map(device => `
<div class="card mb-2">
<div class="card-body">
<h5 class="card-title">${device.device_name}</h5>
<p class="card-text">
Serial Number: ${device.serial_number}<br>
Type: ${device.device_type}<br>
Channel Number: ${device.device_number}
</p>
<button class="btn btn-primary btn-sm"
onclick="addDeviceToExperiment('${experimentId}', '${device.user_device_id}')">
Add
</button>
</div>
</div>
`).join('');
}
// 显示模态框
const modal = new bootstrap.Modal(document.getElementById('addDeviceToExperimentModal'));
modal.show();
} else {
throw new Error('Failed to fetch devices');
}
} catch (error) {
console.error('Failed to load available devices:', error);
alert('Failed to load available devices, please try again');
}
}
let currentExperimentId = null;
// 添加全局变量来存储公式配置
let formulaConfigs = {};
async function enterExperiment(experimentId) {
try {
currentExperimentId = experimentId;
// 获取实验设备和传感器信息
const response = await fetch(`${API_BASE_URL}/experiments/${experimentId}/devices`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) {
throw new Error('Failed to get experiment devices');
}
const devices = await response.json();
// 重构公式配置,将所有设备的传感器公式整合到一起
formulaConfigs = devices.reduce((acc, device) => {
device.sensors.forEach(sensor => {
if (sensor.formulas) {
sensor.formulas.forEach(formula => {
acc.push({
...formula,
deviceName: device.device_name,
sensorName: sensor.sensor_name
});
});
}
});
return acc;
}, []);
// Get experiment details and status
const experimentResponse = await fetch(`${API_BASE_URL}/experiments/${experimentId}`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!experimentResponse.ok) {
throw new Error('Failed to get experiment details');
}
const experimentData = await experimentResponse.json();
// Update experiment title and description
document.getElementById('experimentTitle').textContent = experimentData.experiment_name;
document.getElementById('experimentDescription').textContent = experimentData.description;
// Load experiment devices
await loadExperimentDevicesForPage(experimentId);
// Show experiment page
showPage('experiment');
// Load experiment session history
await loadExperimentSessions(experimentId);
// Control UI based on experiment status
if (experimentData.status === 'completed') {
// Hide all experiment control buttons
document.getElementById('startExperimentBtn').style.display = 'none';
document.getElementById('stopExperimentBtn').style.display = 'none';
document.getElementById('completeExperimentBtn').style.display = 'none';
// Try to get saved report
try {
const reportResponse = await fetch(`${API_BASE_URL}/experiments/${experimentId}/report`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (reportResponse.ok) {
const reportData = await reportResponse.json();
const report = reportData["Experiment Analysis Report"];
// Parse and format the report data
const formattedReport = `
<div class="report-container">
<h4 class="mb-4 text-center">Experiment Analysis Report</h4>
<div class="section mb-4">
<h5 class="mb-3">1. Basic Information</h5>
<div class="ps-3">
<div class="row">
<div class="col-md-4">
<p><strong>Total Sessions:</strong> ${report["1. Basic Information"]["Total Sessions"]}</p>
</div>
<div class="col-md-4">
<p><strong>Total Duration:</strong> ${report["1. Basic Information"]["Total Duration"]}</p>
</div>
<div class="col-md-4">
<p><strong>Data Points:</strong> ${report["1. Basic Information"]["Data Points"]}</p>
</div>
<div class="col-md-4">
<p><strong>Devices Number:</strong> ${report["1. Basic Information"]["Devices number"]}</p>
</div>
<div class="col-md-4">
<p><strong>Sensors Number:</strong> ${report["1. Basic Information"]["Sensors number"]}</p>
</div>
</div>
</div>
</div>
<div class="section mb-4">
<h5 class="mb-3">2. Session Data Analysis</h5>
<div class="row">
${Object.entries(report["2. Session Data Analysis"]).map(([sessionId, data]) => `
<div class="col-12 mb-3">
<div class="card">
<div class="card-body">
<h6 class="card-title">Session ID: ${sessionId}</h6>
<div class="ps-2">
<p class="mb-1"><strong>Duration:</strong> ${data["Duration"]}</p>
<p class="mb-1"><strong>Data Points:</strong> ${data["Data Points"]}</p>
</div>
</div>
</div>
</div>
`).join('')}
</div>
</div>
<div class="section mb-4">
<h5 class="mb-3">3. Key Findings</h5>
<ul class="list-unstyled">
${report["3. Key Findings"].map(finding => `
<li class="mb-2">${finding}</li>
`).join('')}
</ul>
</div>
<div class="section mb-4">
<h5 class="mb-3">4. Recommendations</h5>
<ul class="list-unstyled">
${report["4. Recommendations"].map(recommendation => `
<li class="mb-2">${recommendation}</li>
`).join('')}
</ul>
</div>
</div>
`;
// Create report display area
const reportSection = document.createElement('div');
reportSection.className = 'experiment-report mt-4';
reportSection.innerHTML = formattedReport;
// Create button group
const buttonGroup = document.createElement('div');
buttonGroup.className = 'btn-group mt-3';
const downloadDataBtn = document.createElement('button');
downloadDataBtn.className = 'btn btn-info ';
downloadDataBtn.innerHTML = 'Download Data';
downloadDataBtn.onclick = () => downloadExperimentData(experimentId);
const downloadReportBtn = document.createElement('button');
downloadReportBtn.className = 'btn btn-secondary';
downloadReportBtn.innerHTML = 'Download Report';
downloadReportBtn.onclick = () => downloadExperimentanalyze(experimentId);
buttonGroup.appendChild(downloadDataBtn);
buttonGroup.appendChild(downloadReportBtn);
// Add to page
const experimentPage = document.getElementById('experimentPage');
const cardBody = experimentPage.querySelector('.card-body');
// Remove existing report and buttons
const existingReport = cardBody.querySelector('.experiment-report');
const existingButtons = cardBody.querySelector('.btn-group.mt-3');
if (existingReport) existingReport.remove();
if (existingButtons) existingButtons.remove();
// 先添加按钮组,再添加报告内容
cardBody.appendChild(buttonGroup);
cardBody.appendChild(reportSection);
}
} catch (error) {
console.error('Failed to get experiment report:', error);
}
} else {
// Experiment status is active, show control buttons
document.getElementById('startExperimentBtn').style.display = 'block';
document.getElementById('stopExperimentBtn').style.display = 'block';
document.getElementById('completeExperimentBtn').style.display = 'block';
// Set initial button states
document.getElementById('startExperimentBtn').disabled = false;
document.getElementById('stopExperimentBtn').disabled = true;
// Remove existing report and buttons
const experimentPage = document.getElementById('experimentPage');
const cardBody = experimentPage.querySelector('.card-body');
const existingReport = cardBody.querySelector('.experiment-report');
const existingButtons = cardBody.querySelector('.btn-group.mt-3');
if (existingReport) existingReport.remove();
if (existingButtons) existingButtons.remove();
}
} catch (error) {
console.error('Failed to enter experiment:', error);
alert('Failed to enter experiment, please try again');
}
}
async function loadExperimentDevicesForPage(experimentId) {
try {
const response = await fetch(`${API_BASE_URL}/experiments/${experimentId}/devices`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const devices = await response.json();
const devicesContainer = document.getElementById('experimentDevices');
devicesContainer.innerHTML = `
<div class="d-flex justify-content-between align-items-center mb-3">
</div>
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4">
${devices.map(device => `
<div class="col">
<div class="card h-100">
<div class="card-body">
<h6 class="card-title">${device.device_name}</h6>
<p class="card-text">
Type: ${device.device_type}<br>
Channel Number: ${device.device_number}
</p>
<!-- 添加复制链接按钮 -->
<button class="btn btn-primary btn-sm copy-link-btn"
onclick="copyDeviceMonitorLink('${experimentId}', '${device.serial_number}')"
style="display: none;">
Copy Link
</button>
<!-- 显示传感器信息 -->
<div class="mt-3">
<h6 class="mb-2">Sensors:</h6>
${device.sensors && device.sensors.length > 0 ? `
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr class="text-center">
<th>Index</th>
<th>Name</th>
<th>Type</th>
<th>Unit</th>
<th>Formulas</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
${device.sensors.map(sensor => `
<tr class="text-center">
<td>${sensor.index}</td>
<td>${sensor.sensor_name}</td>
<td>${sensor.sensor_type}</td>
<td>${sensor.unit}</td>
<td>
${sensor.formulas ?
sensor.formulas.map(f =>
`<span class="badge bg-info">${f.data_name}</span>`
).join(' ') :
''
}
</td>
<td>
<button class="btn btn-secondary btn-sm"
onclick="showFormulaModal('${sensor.sensor_name}', '${device.user_device_id}', '${currentExperimentId}')">
<i class="bi bi-plus-circle"></i> Add Formula
</button>
</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
` : '<p class="text-muted">No sensors available</p>'}
</div>
</div>
</div>
</div>
`).join('')}
</div>
`;
updateCopyLinkButtonsVisibility();
}
} catch (error) {
console.error('Failed to load experiment devices:', error);
alert('Failed to load experiment devices, please try again');
}
}
const maxDataPoints = 50;
let charts = {};
let chartDatasets = {};
// 统一的图表配置
function getChartConfig(deviceName, sensorName, dataset) {
return {
type: 'line',
data: {
labels: dataset.labels,
datasets: [{
label: `${deviceName} - ${sensorName}`,
data: dataset.values,
borderColor: '#6366F1',
borderWidth: 2,
fill: false,
tension: 0.3,
pointRadius: 0
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: false
},
x: {
grid: {
display: false
}
}
},
animation: {
duration: 0
},
plugins: {
legend: {
display: false
},
tooltip: {
enabled: false
}
}
}
};
}
// 统一的数据集初始化
function initChartDataset(value) {
return {
labels: Array(maxDataPoints).fill(''),
values: Array(maxDataPoints).fill(null),
historicalMin: value,
historicalMax: value
};
}
// 创建图表容器
function createChartContainer(deviceName, sensorName, chartId) {
const chartWrapper = document.createElement('div');
chartWrapper.className = 'chart-wrapper mb-4';
chartWrapper.innerHTML = `
<div class="card">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<h5 class="card-title mb-0">Device: ${deviceName}</h5>
<small class="text-muted">Sensor: ${sensorName}</small>
</div>
</div>
<div class="chart-container">
<canvas id="chart_${chartId}"></canvas>
</div>
</div>
</div>
`;
return chartWrapper;
}
// 更新图表数据
function updateChartData(chartId, value) {
const dataset = chartDatasets[chartId];
dataset.values.shift();
dataset.values.push(value);
// 更新历史极值
if (dataset.historicalMin === undefined || dataset.historicalMax === undefined) {
dataset.historicalMin = value;
dataset.historicalMax = value;
} else {
dataset.historicalMin = Math.min(dataset.historicalMin, value);
dataset.historicalMax = Math.max(dataset.historicalMax, value);
}
// 更新图表显示
const range = dataset.historicalMax - dataset.historicalMin;
const padding = range * 0.1;
const chart = charts[chartId];
chart.data.datasets[0].data = dataset.values;
chart.options.scales.y.min = dataset.historicalMin - padding;
chart.options.scales.y.max = dataset.historicalMax + padding;
chart.update('none');
}
// 处理自定义数据显示
function updateCustomData(deviceName, sensorName, value, formula) {
try {
const result = eval(`const value = ${value}; ${formula.formula}`);
const elementId = `formula-${deviceName}-${sensorName}-${formula.data_name}`;
const customDataList = document.querySelector('#customDataPanel .custom-data-list');
let resultElement = document.getElementById(elementId);
if (!resultElement) {
resultElement = document.createElement('div');
resultElement.className = 'custom-data-item';
resultElement.id = elementId;
resultElement.style.width = '180px'; // 设置固定宽度
resultElement.innerHTML = `
<div class="custom-data-name" style="font-size: 0.9em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
${deviceName} - ${formula.data_name}
</div>
<div class="custom-data-value" style="text-align: center;">
${Number(result).toFixed(2)}
<span class="custom-data-unit" style="font-size: 0.8em;">${formula.data_unit || ''}</span>
</div>
`;
customDataList.appendChild(resultElement);
} else {
const valueElement = resultElement.querySelector('.custom-data-value');
valueElement.innerHTML = `
${Number(result).toFixed(2)}
<span class="custom-data-unit" style="font-size: 0.8em;">${formula.data_unit || ''}</span>
`;
}
} catch (error) {
console.error(`Calculation formula error ${formula.data_name}:`, error);
}
}
let deviceNameMap = {};
let wsConnections = {};
async function connectWebSocket(experimentId) {
try {
const response = await fetch(`${API_BASE_URL}/experiments/${experimentId}/devices`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const devices = await response.json();
// 关闭现有连接
Object.values(wsConnections).forEach(ws => {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.close();
}
});
wsConnections = {};
// 清空图表
document.getElementById('chartsContainer').innerHTML = '';
charts = {};
chartDatasets = {};
// 创建设备映射
deviceNameMap = devices.reduce((acc, device) => {
acc[device.serial_number] = device.device_name;
return acc;
}, {});
// 重构公式配置
formulaConfigs = devices.reduce((acc, device) => {
device.sensors.forEach(sensor => {
if (sensor.formulas) {
sensor.formulas.forEach(formula => {
acc.push({
...formula,
sensorName: sensor.sensor_name
});
});
}
});
return acc;
}, []);
// 为每个设备创建WebSocket连接
devices.forEach(device => {
const serialNumber = device.serial_number;
const wsUrl = `wss://dev.obscura.work/lab/ws/${experimentId}/${serialNumber}`;
const ws = new WebSocket(wsUrl);
wsConnections[serialNumber] = ws;
ws.onopen = () => {
console.log(`WebSocket Connected: ${serialNumber}`);
document.getElementById('realTimeDataSection').style.display = 'block';
document.getElementById('startExperimentBtn').disabled = true;
document.getElementById('stopExperimentBtn').disabled = false;
};
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
// 处理心跳
if (data.type === "ping") {
ws.send(JSON.stringify({
type: "pong",
timestamp: Date.now()
}));
return;
}
// 处理数据批次
if (data.type === "data_batch" && data.data_points) {
const deviceId = data.serial_number;
const deviceName = deviceNameMap[deviceId] || deviceId;
// 按时间戳排序
const sortedDataPoints = data.data_points.sort((a, b) => a.timestamp - b.timestamp);
// 获取所有传感器名称
const sensorNames = new Set();
sortedDataPoints.forEach(point => {
Object.keys(point.sensor_data).forEach(name => sensorNames.add(name));
});
// 处理每个传感器数据
sensorNames.forEach(sensorName => {
const chartId = `${deviceId}_${sensorName}`;
// 创建或获取图表
if (!charts[chartId]) {
const container = createChartContainer(deviceName, sensorName, chartId);
document.getElementById('chartsContainer').appendChild(container);
const firstValue = sortedDataPoints[0].sensor_data[sensorName];
chartDatasets[chartId] = initChartDataset(firstValue);
const canvas = document.getElementById(`chart_${chartId}`);
charts[chartId] = new Chart(
canvas.getContext('2d'),
getChartConfig(deviceName, sensorName, chartDatasets[chartId])
);
}
// 更新数据
sortedDataPoints.forEach(point => {
const value = point.sensor_data[sensorName];
if (value !== undefined) {
updateChartData(chartId, value);
// 更新自定义数据
formulaConfigs.forEach(formula => {
if (formula.sensorName === sensorName) {
updateCustomData(deviceName, sensorName, value, formula);
}
});
}
});
});
}
} catch (error) {
console.error('Error processing WebSocket message:', error);
}
};
ws.onclose = () => {
console.log(`WebSocket disconnected: ${serialNumber}`);
delete wsConnections[serialNumber];
};
ws.onerror = (error) => {
console.error(`WebSocket Error ${serialNumber}:`, error);
};
});
}
} catch (error) {
console.error('Failed to load formula:', error);
}
}
// Add session management related variables
let currentSessionId = null;
// Modify start experiment function
async function startExperiment() {
if (!currentExperimentId) return;
try {
const response = await fetch(`${API_BASE_URL}/experiments/${currentExperimentId}/start`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) {
throw new Error('Failed to start experiment session');
}
const result = await response.json();
currentSessionId = result.session_id;
// 更新按钮状态
document.getElementById('startExperimentBtn').disabled = true;
document.getElementById('stopExperimentBtn').disabled = false;
document.getElementById('realTimeDataSection').style.display = 'block';
// Clear existing charts
document.getElementById('chartsContainer').innerHTML = '';
charts = {};
chartDatasets = {};
// Connect WebSocket
connectWebSocket(currentExperimentId);
} catch (error) {
console.error('Failed to start experiment:', error);
alert('Failed to start experiment, please try again');
}
}
// Modify stop experiment function
async function stopExperiment() {
if (!currentExperimentId || !currentSessionId) return;
try {
const requestBody = JSON.stringify({ session_id: currentSessionId });
console.log('Request Body:', requestBody); // Check request body
const response = await fetch(`${API_BASE_URL}/experiments/${currentExperimentId}/stop`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: requestBody
});
if (!response.ok) {
throw new Error('Failed to stop experiment session');
}
const result = await response.json();
// Close all WebSocket connections
closeAllWebSockets();
// Update button status
document.getElementById('startExperimentBtn').disabled = false;
document.getElementById('stopExperimentBtn').disabled = true;
document.getElementById('realTimeDataSection').style.display = 'none';
// Display experiment duration
alert(`Experiment stopped, duration: ${result.duration.toFixed(2)} seconds`);
// Reset session ID
currentSessionId = null;
// Refresh experiment session history
await loadExperimentSessions(currentExperimentId);
} catch (error) {
console.error('Failed to stop experiment:', error);
alert('Failed to stop experiment, please try again');
}
}
async function loadExperimentSessions(experimentId) {
try {
const response = await fetch(`${API_BASE_URL}/experiments/${experimentId}/sessions`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const sessions = await response.json();
const historyContainer = document.getElementById('experimentHistory');
if (sessions.length === 0) {
historyContainer.innerHTML = '<div class="text-muted">No experiment sessions yet</div>';
return;
}
historyContainer.innerHTML = sessions.map(session => {
const isOngoing = !session.end_time;
return `
<div class="history-item mb-3 ${isOngoing ? 'ongoing-session' : ''}"
${isOngoing ? `onclick="resumeSession('${experimentId}', '${session.session_id}')"` : ''}
style="${isOngoing ? 'cursor: pointer;' : ''}">
<div class="small text-muted">
<div style="${isOngoing ? 'color: var(--primary-color)' : ''}"><strong>Start:</strong> ${new Date(session.start_time).toLocaleString()}</div>
${session.end_time ? `
<div><strong>End:</strong> ${new Date(session.end_time).toLocaleString()}</div>
<div><strong>Duration:</strong> ${session.duration.toFixed(2)} seconds</div>
` : '<div style="color: #ffc107; font-weight: bold">(Ongoing - Click to enter)</div>'}
</div>
</div>
`;
}).join('');
// 如果有正在进行的会话,自动恢复
const ongoingSession = sessions.find(session => !session.end_time);
if (ongoingSession) {
await resumeSession(experimentId, ongoingSession.session_id);
}
}
} catch (error) {
console.error('Failed to load experiment session history:', error);
// 如果恢复失败,重置按钮状态
document.getElementById('startExperimentBtn').disabled = false;
document.getElementById('stopExperimentBtn').disabled = true;
document.getElementById('completeExperimentBtn').disabled = true;
}
}
async function resumeSession(experimentId, sessionId) {
try {
// 保存会话ID用于stop和complete操作
currentSessionId = sessionId;
// 更新按钮状态
document.getElementById('startExperimentBtn').disabled = true;
document.getElementById('stopExperimentBtn').disabled = false;
document.getElementById('completeExperimentBtn').disabled = false;
// 连接WebSocket显示实时数据
await connectWebSocket(experimentId);
} catch (error) {
console.error('Error resuming session:', error);
alert('恢复实验会话失败,请重试');
}
}
// Modify completeExperiment function
async function completeExperiment() {
if (!currentExperimentId) return;
if (confirm('Are you sure you want to complete this experiment?')) {
try {
// 1. Update experiment status
const completeResponse = await fetch(`${API_BASE_URL}/experiments/${currentExperimentId}/complete`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!completeResponse.ok) {
throw new Error('Failed to update experiment status');
}
// 2. Update UI status
document.getElementById('startExperimentBtn').disabled = true;
document.getElementById('stopExperimentBtn').disabled = true;
// 3. If there are ongoing sessions, stop them first
if (currentSessionId) {
await stopExperiment();
}
// 4. Create temporary UI display
const experimentPage = document.getElementById('experimentPage');
const cardBody = experimentPage.querySelector('.card-body');
// Remove existing report and buttons
const existingReport = cardBody.querySelector('.experiment-report');
const existingButtons = cardBody.querySelector('.btn-group.mt-3');
if (existingReport) existingReport.remove();
if (existingButtons) existingButtons.remove();
// Add download data button and generating prompt
const tempSection = document.createElement('div');
tempSection.className = 'experiment-report mt-4';
tempSection.innerHTML = `
<div class="alert alert-info">Experiment completed, generating report...</div>
<div class="btn-group mt-3">
<button class="btn btn-info download-btn" onclick="downloadExperimentData('${currentExperimentId}')">
Download Data
</button>
</div>
`;
cardBody.appendChild(tempSection);
// 5. Call analysis interface to generate report
const analyzeResponse = await fetch(`${API_BASE_URL}/experiments/${currentExperimentId}/analyze`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!analyzeResponse.ok) {
throw new Error('Failed to generate experiment report');
}
// 6. Start polling for analysis status
let attempts = 0;
const maxAttempts = 12; // 5 minutes maximum (with 5-second intervals)
const showLoading = (message) => {
const loadingAlert = cardBody.querySelector('.alert');
if (loadingAlert) {
loadingAlert.textContent = message;
}
};
const statusCheck = setInterval(async () => {
try {
const statusResponse = await fetch(`${API_BASE_URL}/experiments/${currentExperimentId}/analysis_status`, {
headers: { 'Authorization': `Bearer ${token}` }
});
const status = await statusResponse.json();
attempts++;
if (status.status === 'completed') {
clearInterval(statusCheck);
// Call enterExperiment to display the report
await enterExperiment(currentExperimentId);
}
else if (status.status === 'failed') {
clearInterval(statusCheck);
tempSection.innerHTML = `
<div class="alert alert-danger">
Failed to generate report: ${status.error || 'Unknown error'}
<button class="btn btn-primary mt-2" onclick="completeExperiment()">
Retry
</button>
</div>
<div class="btn-group mt-3">
<button class="btn btn-info download-btn" onclick="downloadExperimentData('${currentExperimentId}')">
Download Data
</button>
</div>
`;
}
else if (attempts >= maxAttempts) {
clearInterval(statusCheck);
tempSection.innerHTML = `
<div class="alert alert-warning">
Report generation is taking longer than expected. Please try again later.
<button class="btn btn-primary mt-2" onclick="completeExperiment()">
Retry
</button>
</div>
<div class="btn-group mt-3">
<button class="btn btn-info download-btn" onclick="downloadExperimentData('${currentExperimentId}')">
Download Data
</button>
</div>
`;
}
else {
showLoading(`${status.status === 'processing' ? 'Generating report' : 'Waiting'}... (${attempts}/${maxAttempts})`);
}
} catch (error) {
clearInterval(statusCheck);
tempSection.innerHTML = `
<div class="alert alert-danger">
Failed to check report status: ${error.message}
<button class="btn btn-primary mt-2" onclick="completeExperiment()">
Retry
</button>
</div>
<div class="btn-group mt-3">
<button class="btn btn-info download-btn" onclick="downloadExperimentData('${currentExperimentId}')">
Download Data
</button>
</div>
`;
}
}, 5000);
} catch (error) {
console.error('Failed to complete experiment:', error);
const experimentPage = document.getElementById('experimentPage');
const cardBody = experimentPage.querySelector('.card-body');
const tempSection = cardBody.querySelector('.experiment-report');
if (tempSection) {
tempSection.innerHTML = `
<div class="alert alert-danger">
Operation failed: ${error.message}
<button class="btn btn-primary mt-2" onclick="completeExperiment()">
Retry
</button>
</div>
<div class="btn-group mt-3">
<button class="btn btn-info download-btn" onclick="downloadExperimentData('${currentExperimentId}')">
Download Data
</button>
</div>
`;
}
}
}
}
async function downloadExperimentData(experimentId) {
try {
const response = await fetch(`${API_BASE_URL}/experiments/${experimentId}/export`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) {
throw new Error('Download failed');
}
// 获取文件名
const contentDisposition = response.headers.get('content-disposition');
let filename = 'experiment_data.zip';
if (contentDisposition) {
const matches = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/.exec(contentDisposition);
if (matches != null && matches[1]) {
filename = matches[1].replace(/['"]/g, '');
}
}
// 获取二进制数据并创建下载
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
} catch (error) {
console.error('Failed to download experiment data:', error);
alert('Failed to download experiment data, please try again');
}
}
async function downloadExperimentanalyze(experimentId) {
try {
// First try to get saved report from Redis db201
const reportResponse = await fetch(`${API_BASE_URL}/experiments/${experimentId}/report`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
let result;
if (reportResponse.status === 404) {
// If report does not exist, call analysis interface to generate new report
const analyzeResponse = await fetch(`${API_BASE_URL}/experiments/${experimentId}/analyze`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!analyzeResponse.ok) {
throw new Error('Failed to generate experiment report');
}
result = await analyzeResponse.json();
} else if (reportResponse.ok) {
// If report exists, use it directly
result = await reportResponse.json();
} else {
throw new Error('Failed to get experiment report');
}
// Display report content on page
const experimentPage = document.getElementById('experimentPage');
const cardBody = experimentPage.querySelector('.card-body');
// Remove existing report (if any)
const existingReport = cardBody.querySelector('.experiment-report');
if (existingReport) {
existingReport.remove();
}
// Create new report display area with formatted content
const reportSection = document.createElement('div');
reportSection.className = 'experiment-report mt-4';
const report = result["Experiment Analysis Report"];
reportSection.innerHTML = `
<div class="report-container">
<h4 class="mb-4 text-center">Experiment Analysis Report</h4>
<div class="section mb-4">
<h5 class="mb-3">1. Basic Information</h5>
<div class="ps-3">
<div class="row">
<div class="col-md-4">
<p><strong>Total Sessions:</strong> ${report["1. Basic Information"]["Total Sessions"]}</p>
</div>
<div class="col-md-4">
<p><strong>Total Duration:</strong> ${report["1. Basic Information"]["Total Duration"]}</p>
</div>
<div class="col-md-4">
<p><strong>Data Points:</strong> ${report["1. Basic Information"]["Data Points"]}</p>
</div>
<div class="col-md-4">
<p><strong>Devices Number:</strong> ${report["1. Basic Information"]["Devices number"]}</p>
</div>
<div class="col-md-4">
<p><strong>Sensors Number:</strong> ${report["1. Basic Information"]["Sensors number"]}</p>
</div>
</div>
</div>
</div>
<div class="section mb-4">
<h5 class="mb-3">2. Session Data Analysis</h5>
<div class="row">
${Object.entries(report["2. Session Data Analysis"]).map(([sessionId, data]) => `
<div class="col-12 mb-3">
<div class="card">
<div class="card-body">
<h6 class="card-title">Session ID: ${sessionId}</h6>
<div class="ps-2">
<p class="mb-1"><strong>Duration:</strong> ${data["Duration"]}</p>
<p class="mb-1"><strong>Data Points:</strong> ${data["Data Points"]}</p>
</div>
</div>
</div>
</div>
`).join('')}
</div>
</div>
<div class="section mb-4">
<h5 class="mb-3">3. Key Findings</h5>
<ul class="list-unstyled">
${report["3. Key Findings"].map(finding => `
<li class="mb-2">${finding}</li>
`).join('')}
</ul>
</div>
<div class="section mb-4">
<h5 class="mb-3">4. Recommendations</h5>
<ul class="list-unstyled">
${report["4. Recommendations"].map(recommendation => `
<li class="mb-2">${recommendation}</li>
`).join('')}
</ul>
</div>
</div>
`;
// Add report to page
cardBody.appendChild(reportSection);
// Convert report to downloadable text file
const reportText = JSON.stringify(result, null, 2);
const blob = new Blob([reportText], { type: 'application/json' });
const url = window.URL.createObjectURL(blob);
// Create download link
const a = document.createElement('a');
a.href = url;
a.download = `experiment_report_${experimentId}.json`;
document.body.appendChild(a);
a.click();
// Clean up
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
} catch (error) {
console.error('Failed to download experiment report:', error);
alert('Failed to download experiment report, please try again');
}
}
// Add download project report function
async function downloadProjectReport(projectId) {
try {
// Display loading prompt
const loadingToast = document.createElement('div');
loadingToast.className = 'alert alert-info position-fixed top-50 start-50 translate-middle';
loadingToast.innerHTML = `
<div class="d-flex align-items-center">
<div class="spinner-border spinner-border-sm " role="status">
<span class="visually-hidden">Generating...</span>
</div>
<span>Generating project report, please wait...</span>
</div>
`;
document.body.appendChild(loadingToast);
let projectResult, paperResult;
// 3. Generate new project report
const analyzeResponse = await fetch(`${API_BASE_URL}/projects/${projectId}/analyze`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!analyzeResponse.ok) {
throw new Error('Failed to generate project report');
}
projectResult = await analyzeResponse.json();
// 2. Get paper analysis report
const paperReportResponse = await fetch(`${API_BASE_URL}/references/${projectId}/summary_report`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (paperReportResponse.status === 404 || !paperReportResponse.ok) {
console.log('No saved paper analysis report found, generating new report...');
const paperAnalyzeResponse = await fetch(`${API_BASE_URL}/references/${projectId}/analyze_report`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!paperAnalyzeResponse.ok) {
console.log('Failed to generate paper analysis report, will only include project report');
paperResult = null;
} else {
// Re-fetch generated report
const newReportResponse = await fetch(`${API_BASE_URL}/references/${projectId}/summary_report`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (newReportResponse.ok) {
paperResult = await newReportResponse.json();
}
}
} else {
paperResult = await paperReportResponse.json();
}
// 3. Merge reports
const combinedReport = {
project_report: projectResult,
paper_analysis: paperResult || "No analysis report found"
};
// 4. Remove loading prompt
document.body.removeChild(loadingToast);
// 5. Convert report to downloadable text file
const reportText = JSON.stringify(combinedReport, null, 2);
const blob = new Blob([reportText], { type: 'application/json' });
const url = window.URL.createObjectURL(blob);
// 6. Create download link
const a = document.createElement('a');
a.href = url;
a.download = `project_complete_report_${projectId}.json`;
document.body.appendChild(a);
a.click();
// 7. Clean up
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
alert('Project report generated and downloaded');
} catch (error) {
// Remove loading prompt (if any)
const loadingToast = document.querySelector('.alert-info');
if (loadingToast) {
document.body.removeChild(loadingToast);
}
console.error('Failed to download project report:', error);
alert('Failed to download project report, please try again');
}
}
async function displayProjectReport(projectId) {
// 1. 切换显示区域
document.getElementById('experimentContent').classList.add('hidden');
document.getElementById('reportContent').classList.remove('hidden');
const projectReport = document.getElementById('projectReport');
const chatBtn = document.getElementById('reportChatBtn');
try {
// 显示加载状态
projectReport.innerHTML = `
<div class="text-center">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-3">Loading report...</p>
</div>
`;
// 隐藏聊天按钮
if (chatBtn) {
chatBtn.style.display = 'none';
}
// 2. 获取报告数据
const response = await fetch(`${API_BASE_URL}/projects/${projectId}/report`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const reportData = await response.json();
// 使用 formatReportContent 格式化报告内容
projectReport.innerHTML = formatReportContent(reportData);
// 显示聊天按钮
if (chatBtn) {
chatBtn.style.display = 'flex';
}
} else {
projectReport.innerHTML = '<div class="alert alert-info">No report available for this project.</div>';
}
} catch (error) {
console.error('Failed to load project report:', error);
projectReport.innerHTML = '<div class="alert alert-danger">Failed to load project report. Please try again.</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(/Project Analysis Report:/, '<div style="text-align: center"><strong style="font-size: 24px;">Project Analysis Report</strong></div>')
.replace(/^(\s*\d+\.[^:\n]+:)/gm, '<strong>$1</strong>')
.split('\n')
.map(line => line.trimEnd())
.join('\n')
}</pre>
</div>
</div>`;
}
// Delete experiment
async function deleteExperiment(experimentId) {
if (!confirm('Are you sure you want to delete this experiment? This action cannot be undone.')) {
return;
}
try {
const response = await fetch(`${API_BASE_URL}/experiments/${experimentId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
alert('Experiment deleted successfully');
// Refresh project details page
await viewProjectDetail(currentProjectId, document.getElementById('projectDetailTitle').textContent);
} else {
const error = await response.json();
alert(`Failed to delete experiment: ${error.detail}`);
}
} catch (error) {
console.error('Failed to delete experiment:', error);
alert('Failed to delete experiment, please try again');
}
}
// Delete project
async function deleteProject(projectId) {
if (!confirm('Are you sure you want to delete this project? This will delete all experiment data and cannot be undone.')) {
return;
}
try {
const response = await fetch(`${API_BASE_URL}/projects/${projectId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
alert('Project deleted successfully');
// Refresh project list
loadProjects();
} else {
const error = await response.json();
alert(`Failed to delete project: ${error.detail}`);
}
} catch (error) {
console.error('Failed to delete project:', error);
alert('Failed to delete project, please try again');
}
}
// Show project memo modal
function showProjectMemo(projectId) {
new bootstrap.Modal(document.getElementById('projectMemoModal')).show();
}
// 显示备忘录模态框
async function showProjectMemo(projectId) {
try {
const response = await fetch(`${API_BASE_URL}/projects/${projectId}/memo`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const memoData = await response.json();
document.getElementById('memoContent').value = memoData.content || '';
// 显示创建时间
const createTimeElement = document.getElementById('memoCreateTime');
if (memoData.create_time) {
const createTime = new Date(memoData.create_time);
createTimeElement.textContent = `Last modified: ${createTime.toLocaleString()}`;
} else {
createTimeElement.textContent = '';
}
// 显示模态框
new bootstrap.Modal(document.getElementById('projectMemoModal')).show();
}
} catch (error) {
console.error('Failed to get memo:', error);
alert('Failed to get memo, please try again');
}
}
// 保存备忘录
async function saveMemo() {
try {
const content = document.getElementById('memoContent').value;
const response = await fetch(`${API_BASE_URL}/projects/${currentProjectId}/memo`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
content: content
})
});
if (response.ok) {
bootstrap.Modal.getInstance(document.getElementById('projectMemoModal')).hide();
alert('Memo saved successfully');
} else {
throw new Error('Save failed');
}
} catch (error) {
console.error('Failed to save memo:', error);
alert('Failed to save memo, please try again');
}
}
// 显示实验备忘录模态框
async function showExperimentMemo(experimentId) {
try {
const response = await fetch(`${API_BASE_URL}/experiments/${experimentId}/memo`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const memoData = await response.json();
document.getElementById('experimentMemoContent').value = memoData.content || '';
// 显示创建时间
const createTimeElement = document.getElementById('experimentMemoCreateTime');
if (memoData.create_time) {
const createTime = new Date(memoData.create_time);
createTimeElement.textContent = `Last modified: ${createTime.toLocaleString()}`;
} else {
createTimeElement.textContent = '';
}
// 显示模态框
new bootstrap.Modal(document.getElementById('experimentMemoModal')).show();
}
} catch (error) {
console.error('Failed to get experiment memo:', error);
alert('Failed to get experiment memo, please try again');
}
}
// 保存实验备忘录
async function saveExperimentMemo() {
try {
const content = document.getElementById('experimentMemoContent').value;
const response = await fetch(`${API_BASE_URL}/experiments/${currentExperimentId}/memo`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
content: content
})
});
if (response.ok) {
bootstrap.Modal.getInstance(document.getElementById('experimentMemoModal')).hide();
alert('Experiment memo saved successfully');
} else {
throw new Error('Save failed');
}
} catch (error) {
console.error('Failed to save experiment memo:', error);
alert('Failed to save experiment memo, please try again');
}
}
let formulas = [];
// 修改 showFormulaModal 函数,接收必要的参数
function showFormulaModal(sensorName, deviceId, experimentId) {
// 保存所有必要的参数到表单中的隐藏字段
document.getElementById('formulaSensorName').value = sensorName;
document.getElementById('formulaDeviceId').value = deviceId;
document.getElementById('formulaExperimentId').value = experimentId;
// 显示模态框
new bootstrap.Modal(document.getElementById('formulaModal')).show();
}
// 修改 addFormula 函数,从隐藏字段获取 experimentId
async function addFormula() {
try {
const experimentId = document.getElementById('formulaExperimentId').value;
const deviceId = document.getElementById('formulaDeviceId').value;
const sensorName = document.getElementById('formulaSensorName').value;
const dataName = document.getElementById('dataName').value;
const dataUnit = document.getElementById('dataUnit').value;
const formula = document.getElementById('formula').value;
if (!experimentId || !deviceId || !sensorName || !dataName || !formula) {
alert('All required fields must be filled');
return;
}
const response = await fetch(`${API_BASE_URL}/experiments/${experimentId}/devices/${deviceId}/formulas`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
sensor_name: sensorName,
data_name: dataName,
data_unit: dataUnit,
formula: formula
})
});
if (response.ok) {
// 关闭模态框
bootstrap.Modal.getInstance(document.getElementById('formulaModal')).hide();
// 清空表单
document.getElementById('formulaForm').reset();
// 重新加载设备列表
await loadExperimentDevicesForPage(experimentId);
alert('Formula added successfully');
} else {
const error = await response.json();
throw new Error(error.detail || 'Failed to add formula');
}
} catch (error) {
console.error('Failed to add formula:', error);
alert('Failed to add formula: ' + error.message);
}
}
// 添加公式数据更新函数
function updateFormulaData(formulaKey, sensorValue) {
const formulas = formulaConfigs[formulaKey];
const customDataList = document.querySelector('#customDataPanel .custom-data-list');
formulas.forEach(formula => {
const formulaId = `formula_${formulaKey}_${formula.data_name}`;
let formulaElement = document.getElementById(formulaId);
try {
// 计算公式结果
const result = evaluateFormula(formula.formula, sensorValue);
if (!formulaElement) {
// 创建新的公式显示元素
formulaElement = document.createElement('div');
formulaElement.id = formulaId;
formulaElement.className = 'custom-data-item';
formulaElement.innerHTML = `
<div class="custom-data-name">${formula.data_name}</div>
<div class="custom-data-value">
<span class="value">${result.toFixed(2)}</span>
<span class="custom-data-unit">${formula.data_unit || ''}</span>
</div>
`;
customDataList.appendChild(formulaElement);
} else {
// 更新现有元素的值
formulaElement.querySelector('.value').textContent = result.toFixed(2);
}
} catch (error) {
console.error('Failed to calculate formula:', error);
}
});
}
function evaluateFormula(formula, value) {
try {
// 使用 Function 构造器创建安全的计算函数
const calculateResult = new Function('value', `return ${formula}`);
return calculateResult(value);
} catch (error) {
console.error('Formula calculation error:', error);
return 0;
}
}
// 添加复制链接功能
function copyDeviceMonitorLink(experimentId, serialNumber) {
const monitorUrl = `${window.location.origin}/lab/device_monitor.html?experimentId=${experimentId}&serialNumber=${serialNumber}`;
navigator.clipboard.writeText(monitorUrl).then(() => {
alert('Monitoring link copied to clipboard');
}).catch(err => {
console.error('Copy Failure:', err);
alert('Copy Failure');
});
}
// 更新复制链接按钮的显示状态
function updateCopyLinkButtonsVisibility() {
const copyLinkBtns = document.querySelectorAll('.copy-link-btn');
const isExperimentActive = !document.getElementById('startExperimentBtn').disabled;
copyLinkBtns.forEach(btn => {
btn.style.display = isExperimentActive ? 'inline-block' : 'none';
});
}
// 添加关闭所有WebSocket连接的函数
function closeAllWebSockets() {
Object.entries(wsConnections).forEach(([serialNumber, ws]) => {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.close();
console.log(`Closed WebSocket connection for device ${serialNumber}`);
}
});
wsConnections = {};
}
// 新增函数,从卡片元素获取数据并显示详情
function viewReferenceDetailFromCard(cardElement) {
const referenceId = cardElement.id.replace('reference-', '');
const encodedAnalysis = cardElement.getAttribute('data-analysis') || '';
viewReferenceDetail(referenceId, encodedAnalysis);
}
// 显示实验内容
function showExperimentContent() {
document.getElementById('experimentContent').classList.remove('hidden');
document.getElementById('reportContent').classList.add('hidden');
}
// 显示报告内容
function showReportContent() {
document.getElementById('experimentContent').classList.add('hidden');
document.getElementById('reportContent').classList.remove('hidden');
}
// 下载报告
async function downloadReport() {
try {
const response = await fetch(`${API_BASE_URL}/projects/${currentProjectId}/report`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) {
throw new Error('Failed to download report');
}
const report = await response.json();
const blob = new Blob([JSON.stringify(report, null, 2)], { type: 'application/json' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `project_report_${currentProjectId}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
} catch (error) {
console.error('Failed to download report:', error);
alert('Failed to download report: ' + error.message);
}
}
// 添加重新生成报告功能
async function regenerateReport() {
if (!currentProjectId) {
alert('Please select a project first');
return;
}
try {
// 显示加载状态
const showLoading = (message) => {
document.getElementById('projectReport').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}/projects/${currentProjectId}/analyze?force=true`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!generateResponse.ok) {
throw new Error((await generateResponse.json()).detail || 'Failed to generate report');
}
// 监控状态
let attempts = 0;
const maxAttempts = 24; // 2 minutes (5 seconds × 24)
const statusCheck = setInterval(async () => {
try {
const statusResponse = await fetch(`${API_BASE_URL}/projects/${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}/projects/${currentProjectId}/report`, {
headers: { 'Authorization': `Bearer ${token}` }
});
const report = await finalReport.json();
document.getElementById('projectReport').innerHTML = `
<div class="report-content">
<pre class="bg-light p-3 rounded">${JSON.stringify(report, null, 2)}</pre>
</div>
`;
}
else if (status.status === 'failed') {
clearInterval(statusCheck);
throw new Error(status.error || 'Report regeneration failed');
}
else if (attempts >= maxAttempts) {
clearInterval(statusCheck);
document.getElementById('projectReport').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'}... (${attempts}/${maxAttempts})`);
}
} catch (error) {
clearInterval(statusCheck);
document.getElementById('projectReport').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('projectReport').innerHTML = `
<div class="alert alert-danger">
Operation failed: ${error.message}
<button class="btn btn-primary mt-2" onclick="regenerateReport()">
Retry
</button>
</div>
`;
}
}
// 打开项目报告AI问答对话框
async function openReportChatDialog(projectId, title) {
try {
// 先检查是否有项目报告
const response = await fetch(`${API_BASE_URL}/projects/${projectId}/report`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) {
alert('Please generate a project report before using the AI Q&A');
return;
}
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">${title}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body d-flex flex-column" style="height: calc(60vh - 76px);">
<div id="chatHistory" class="flex-grow-1 custom-scrollbar" style="overflow-y: auto; margin-bottom: 15px; padding: 10px;">
<!-- Chat history will be loaded here -->
</div>
<form class="input-group" onsubmit="event.preventDefault(); sendReportQuestion('${projectId}');">
<input type="text" id="questionInput" class="form-control"
placeholder="Type your question here..."
aria-label="Question input">
<button type="submit" class="btn"
style="background: var(--primary-color); color: white;"
aria-label="Send question">
Send
</button>
</form>
</div>
</div>
</div>
`;
document.body.appendChild(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 loadReportChatHistory(projectId);
const input = document.getElementById('questionInput');
input.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
sendReportQuestion(projectId);
}
});
} catch (error) {
console.error('Failed to open chat dialog:', error);
}
}
// 打开实验AI问答对话框
async function openExperimentChatDialog(experimentId) {
try {
// 先检查实验状态
const response = await fetch(`${API_BASE_URL}/experiments/${experimentId}`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) {
throw new Error('Failed to fetch experiment data');
}
const experimentData = await response.json();
if (experimentData.status !== 'completed') {
alert('AI Q&A is only available after the experiment is completed');
return;
}
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">${experimentData.experiment_name}</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>
<form class="input-group" onsubmit="event.preventDefault(); sendExperimentQuestion('${experimentId}');">
<input type="text" id="questionInput" class="form-control"
placeholder="Type your question here..."
aria-label="Question input">
<button type="submit" class="btn"
style="background: var(--primary-color); color: white;"
aria-label="Send question">
Send
</button>
</form>
</div>
</div>
</div>
`;
document.body.appendChild(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 loadExperimentChatHistory(experimentId);
const input = document.getElementById('questionInput');
input.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
sendExperimentQuestion(experimentId);
}
});
} catch (error) {
console.error('Failed to open chat dialog:', error);
}
}
// 加载项目报告聊天历史
async function loadReportChatHistory(projectId) {
const chatHistory = document.getElementById('chatHistory');
try {
const response = await fetch(`${API_BASE_URL}/projects/${projectId}/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">
<div class="d-flex align-items-start mb-2">
<div class="me-2">👤</div>
<div class="bg-light rounded p-2 flex-grow-1">
${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>${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>';
}
}
// 加载实验聊天历史
async function loadExperimentChatHistory(experimentId) {
const chatHistory = document.getElementById('chatHistory');
try {
const response = await fetch(`${API_BASE_URL}/experiments/${experimentId}/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">
<div class="d-flex align-items-start mb-2">
<div class="me-2">👤</div>
<div class="bg-light rounded p-2 flex-grow-1">
${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>${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>';
}
}
// 发送项目报告问题
async function sendReportQuestion(projectId) {
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">
${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}/projects/${projectId}/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">🤖</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>${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">
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();
}
}
}
// 发送实验问题
async function sendExperimentQuestion(experimentId) {
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">
${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}/experiments/${experimentId}/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">🤖</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>${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">
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();
}
}
}
async function viewExperiment(experimentId) {
try {
const response = await fetch(`${API_BASE_URL}/experiments/${experimentId}`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) {
throw new Error('Failed to fetch experiment data');
}
const experimentData = await response.json();
// 更新当前实验ID
currentExperimentId = experimentId;
// 更新页面标题
document.getElementById('experimentTitle').textContent = experimentData.experiment_name;
// 更新AI问答按钮显示状态
const chatBtn = document.getElementById('experimentChatBtn');
if (chatBtn) {
chatBtn.style.display = experimentData.status === 'completed' ? 'flex' : 'none';
}
// 更新实验描述
document.getElementById('experimentDescription').textContent = experimentData.description || 'No description';
// 更新设备列表
const devicesContainer = document.getElementById('experimentDevices');
if (experimentData.devices && experimentData.devices.length > 0) {
devicesContainer.innerHTML = `
<h4 class="mb-3">Devices</h4>
<div class="row">
${experimentData.devices.map(device => `
<div class="col-md-4 mb-3">
<div class="card">
<div class="card-body">
<h5 class="card-title">${device.device_name}</h5>
<p class="card-text">Status: ${device.status}</p>
</div>
</div>
</div>
`).join('')}
</div>
`;
} else {
devicesContainer.innerHTML = '<p class="text-muted">No devices assigned to this experiment</p>';
}
// 更新按钮状态
const startBtn = document.getElementById('startExperimentBtn');
const stopBtn = document.getElementById('stopExperimentBtn');
const completeBtn = document.getElementById('completeExperimentBtn');
if (startBtn && stopBtn && completeBtn) {
switch (experimentData.status) {
case 'not_started':
startBtn.disabled = false;
stopBtn.disabled = true;
completeBtn.disabled = true;
break;
case 'running':
startBtn.disabled = true;
stopBtn.disabled = false;
completeBtn.disabled = false;
break;
case 'completed':
startBtn.disabled = true;
stopBtn.disabled = true;
completeBtn.disabled = true;
break;
default:
startBtn.disabled = true;
stopBtn.disabled = true;
completeBtn.disabled = true;
}
}
// 如果实验正在运行,启动实时数据更新
if (experimentData.status === 'running') {
startRealtimeDataUpdate(experimentId);
}
} catch (error) {
console.error('Failed to view experiment:', error);
alert('Failed to load experiment details');
}
}
function processMarkdownContent(content) {
try {
// 如果内容是JSON字符串,先解析它
if (typeof content === 'string' && content.startsWith('"') && content.endsWith('"')) {
content = JSON.parse(content);
}
// 处理可能的嵌套代码块缩进,但不添加最外层的markdown包装
content = content.replace(/```(\w+)\n([\s\S]*?)```/g, (match, lang, code) => {
// 为代码块添加缩进
const indentedCode = code.split('\n').map(line => line).join('\n');
return '```' + lang + '\n' + indentedCode + '\n```';
});
// 转义HTML特殊字符以正确显示
content = content
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
return content;
} catch (e) {
console.error('Error processing markdown content:', e);
return content;
}
}
</script>
</body>
</html>