4305 lines
188 KiB
HTML
4305 lines
188 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>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, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
|
||
return content;
|
||
} catch (e) {
|
||
console.error('Error processing markdown content:', e);
|
||
return content;
|
||
}
|
||
}
|
||
</script>
|
||
</body>
|
||
</html> |