360 lines
12 KiB
HTML
360 lines
12 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>Predict Result</title>
|
|
<link rel="icon" href="/public/assets/images/logo.png" type="image/png">
|
|
<link rel="stylesheet" href="/public/css/Tools.css">
|
|
<style>
|
|
/* 统一的基础样式 */
|
|
* {
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.status-wrapper {
|
|
max-width: 1000px;
|
|
margin: 30px auto;
|
|
background: #fff;
|
|
border-radius: 10px;
|
|
border: 1px solid #e6e6e6;
|
|
}
|
|
|
|
.status-header {
|
|
padding: 16px 20px;
|
|
border-bottom: 1px solid #e6e6e6;
|
|
background: linear-gradient(135deg, #f8f9ff 0%, #eef3ff 100%);
|
|
}
|
|
|
|
.status-body {
|
|
padding: 20px;
|
|
}
|
|
|
|
.meta {
|
|
display: flex;
|
|
gap: 16px;
|
|
flex-wrap: wrap;
|
|
color: #555;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.meta-item {
|
|
padding: 6px 10px;
|
|
background: #f7f7f7;
|
|
border-radius: 6px;
|
|
border: 1px solid #eee;
|
|
}
|
|
|
|
.estimate {
|
|
color: #2E5A61;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.result-area {
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.actions {
|
|
margin-top: 16px;
|
|
display: flex;
|
|
gap: 10px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.btn {
|
|
padding: 8px 14px;
|
|
border-radius: 8px;
|
|
border: 1px solid #e6e6e6;
|
|
background: #f8f9fa;
|
|
color: #333;
|
|
cursor: pointer;
|
|
text-decoration: none;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.btn-primary {
|
|
background: linear-gradient(135deg, #4A90A4, #6BC8D1);
|
|
color: #fff;
|
|
border: none;
|
|
}
|
|
|
|
/* 统一的自适应设计 */
|
|
|
|
/* 大屏幕 (1400px+) */
|
|
@media (min-width: 1400px) {
|
|
.status-wrapper {
|
|
max-width: 1200px;
|
|
}
|
|
}
|
|
|
|
/* 中等屏幕 (992px - 1399px) */
|
|
@media (max-width: 1399px) {
|
|
.status-wrapper {
|
|
max-width: 95%;
|
|
}
|
|
}
|
|
|
|
/* 平板 (768px - 991px) */
|
|
@media (max-width: 991px) {
|
|
.status-wrapper {
|
|
max-width: 98%;
|
|
margin: 20px auto;
|
|
}
|
|
|
|
.status-header {
|
|
padding: 14px 18px;
|
|
}
|
|
|
|
.status-body {
|
|
padding: 18px;
|
|
}
|
|
}
|
|
|
|
/* 移动设备 (最大768px) */
|
|
@media (max-width: 768px) {
|
|
.status-wrapper {
|
|
max-width: 100%;
|
|
margin: 15px 10px;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.status-header {
|
|
padding: 12px 15px;
|
|
}
|
|
|
|
.status-body {
|
|
padding: 15px;
|
|
}
|
|
|
|
.meta {
|
|
gap: 10px;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.meta-item {
|
|
padding: 5px 8px;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.actions {
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
|
|
.btn {
|
|
width: 100%;
|
|
text-align: center;
|
|
padding: 10px;
|
|
}
|
|
}
|
|
|
|
/* 小屏幕移动设备 (最大480px) */
|
|
@media (max-width: 480px) {
|
|
.status-wrapper {
|
|
margin: 10px 5px;
|
|
}
|
|
|
|
.status-header {
|
|
padding: 10px 12px;
|
|
}
|
|
|
|
.status-body {
|
|
padding: 12px;
|
|
}
|
|
|
|
.meta {
|
|
gap: 8px;
|
|
}
|
|
|
|
.meta-item {
|
|
padding: 4px 6px;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.btn {
|
|
font-size: 14px;
|
|
padding: 8px;
|
|
}
|
|
}
|
|
|
|
/* 超小屏幕 (最大360px) */
|
|
@media (max-width: 360px) {
|
|
.status-header {
|
|
padding: 8px 10px;
|
|
}
|
|
|
|
.status-body {
|
|
padding: 10px;
|
|
}
|
|
|
|
.meta-item {
|
|
font-size: 11px;
|
|
}
|
|
|
|
.btn {
|
|
font-size: 13px;
|
|
}
|
|
}
|
|
</style>
|
|
<script>
|
|
const API_BASE_URL = 'http://localhost:4000';
|
|
let pollTimer = null;
|
|
|
|
function qs(name){ const url = new URL(location.href); return url.searchParams.get(name) || ''; }
|
|
|
|
function setText(id, text){ const el = document.getElementById(id); if(el) el.textContent = text; }
|
|
|
|
async function fetchStatus(jobId){
|
|
const res = await fetch(`${API_BASE_URL}/api/status/${encodeURIComponent(jobId)}`);
|
|
if (!res.ok) throw new Error('Status interface error');
|
|
return res.json();
|
|
}
|
|
|
|
function updateEstimate(status){
|
|
const { eta_seconds = null, progress = 0, status: st = 'queued', queue_position } = status || {};
|
|
if (eta_seconds != null) {
|
|
const mins = Math.max(0, Math.round(eta_seconds / 60));
|
|
setText('eta', `${mins} minutes`);
|
|
} else {
|
|
setText('eta', 'Calculating...');
|
|
}
|
|
const detail = st === 'queued' ? `Waiting (queue position: ${queue_position ?? 'unknown'})` : (st === 'analyzing' ? 'Analyzing' : st);
|
|
setText('status-text', detail);
|
|
const fill = document.getElementById('progress-fill');
|
|
if (fill) fill.style.width = `${Math.max(0, Math.min(100, progress))}%`;
|
|
}
|
|
|
|
function renderResult(data){
|
|
// 与 Tools.html 的展示结构保持一致的简化填充
|
|
setText('result-species', data.species_name || '-');
|
|
setText('result-taxonomy', data.taxonomy || '-');
|
|
setText('result-ph', data.ph || '-');
|
|
setText('result-temperature', data.temperature || '-');
|
|
setText('result-respiratory', data.respiratory_type || '-');
|
|
setText('result-medium', data.culture_medium || '-');
|
|
setText('result-growth', data.max_growth_rate || '-');
|
|
document.getElementById('download-btn').style.display = 'inline-block';
|
|
}
|
|
|
|
async function startPolling(jobId){
|
|
// 首次拉取
|
|
try {
|
|
const first = await fetchStatus(jobId);
|
|
updateEstimate(first);
|
|
if (first.status === 'completed') {
|
|
renderResult(first);
|
|
return;
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
|
|
pollTimer = setInterval(async () => {
|
|
try {
|
|
const data = await fetchStatus(jobId);
|
|
updateEstimate(data);
|
|
if (data.status === 'completed') {
|
|
clearInterval(pollTimer);
|
|
pollTimer = null;
|
|
renderResult(data);
|
|
} else if (data.status === 'failed') {
|
|
clearInterval(pollTimer);
|
|
pollTimer = null;
|
|
alert('Mission failed:' + (data.error || 'Unknown error'));
|
|
}
|
|
} catch (err) {
|
|
console.error('Polling failed', err);
|
|
}
|
|
}, 2000);
|
|
}
|
|
|
|
async function downloadResult(){
|
|
const jobId = qs('job');
|
|
if (!jobId) return alert('No job number');
|
|
const res = await fetch(`${API_BASE_URL}/api/download/${encodeURIComponent(jobId)}`);
|
|
if (!res.ok) { try { const e = await res.json(); alert(e.error || 'Download failed'); } catch { alert('Download failed'); } return; }
|
|
const blob = await res.blob();
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `analysis_result_${jobId}.csv`;
|
|
document.body.appendChild(a); a.click();
|
|
URL.revokeObjectURL(url); document.body.removeChild(a);
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const jobId = qs('job');
|
|
const type = qs('type');
|
|
if (jobId) setText('job-id', jobId);
|
|
if (type) setText('job-type', type);
|
|
if (!jobId) { alert('Missing job number'); return; }
|
|
startPolling(jobId);
|
|
});
|
|
</script>
|
|
</head>
|
|
<body>
|
|
<div class="status-wrapper">
|
|
<div class="status-header">
|
|
<strong>Job status and results</strong>
|
|
</div>
|
|
<div class="status-body">
|
|
<div class="meta">
|
|
<span class="meta-item">Job Number:<span id="job-id">-</span></span>
|
|
<span class="meta-item">Tool Type:<span id="job-type">-</span></span>
|
|
<span class="meta-item estimate">Estimated time:<span id="eta">Calculating...</span></span>
|
|
<span class="meta-item">state:<span id="status-text">In preparation</span></span>
|
|
</div>
|
|
|
|
<div class="progress-container">
|
|
<div class="progress-bar">
|
|
<div class="progress-fill" id="progress-fill" style="width:0%"></div>
|
|
</div>
|
|
<div class="progress-text" id="progress-text">Real-time progress</div>
|
|
<div class="progress-details" id="progress-details">The system is estimating the time and queue</div>
|
|
</div>
|
|
|
|
<div class="result-area">
|
|
<div class="result-content">
|
|
<ul>
|
|
<li>
|
|
<p class="field-name">species name</p>
|
|
<p class="field-value" id="result-species">-</p>
|
|
</li>
|
|
<li>
|
|
<p class="field-name">taxonomy</p>
|
|
<p class="field-value" id="result-taxonomy">-</p>
|
|
</li>
|
|
<li>
|
|
<p class="field-name">pH</p>
|
|
<p class="field-value" id="result-ph">-</p>
|
|
</li>
|
|
<li>
|
|
<p class="field-name">temperature</p>
|
|
<p class="field-value" id="result-temperature">-</p>
|
|
</li>
|
|
<li>
|
|
<p class="field-name">respiratory type</p>
|
|
<p class="field-value" id="result-respiratory">-</p>
|
|
</li>
|
|
<li>
|
|
<p class="field-name">culture medium</p>
|
|
<p class="field-value" id="result-medium">-</p>
|
|
</li>
|
|
<li>
|
|
<p class="field-name">Max growth rate</p>
|
|
<p class="field-value" id="result-growth">-</p>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="actions">
|
|
<a class="btn" href="/Tools.html">return Tools</a>
|
|
<a class="btn btn-primary" href="javascript:void(0)" onclick="downloadResult()">Download</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
|