← 返回 PRIME

TRAUMADATA CENTER

AI Holographic Monitoring Center

--:--:--
系统状态 运行中
数据更新 --
活跃服务 --
健康评分 --%
PubMed 文献总数
--
实时数据
平台访客总数
--
累计统计
API 调用次数
--
本会话
在线服务端口
--
健康运行
知识库实时指标
正在获取... --
数据来源:服务器 /opt/pubmed-monitor/ 实时核查
本地文献
--
篇 (sqlite3 articles)
向量切块总数
--
条 (FAISS guidelines + kb_chunks)
FAISS 去重文档
--
份独立文档
向量维度
--
维 (BGE-small-zh-v1.5)
Token 估算
--
亿 token (保守下限)
PDF 指南原文件
--
份 (.pdf)
FTS5 全文索引
--
行 (703 MB)
JCR 期刊覆盖
--
急诊术语库
--
词 / 类目
BIO 图标库
--
个 SVG symbol
知识库构成
向量"参数量级"对比 (粗略参照)
注:向量维度数 ≠ 神经网络权重,信息密度完全不同,仅作量级参照。
真实能力 = 检索质量 × 上下文利用 × 底座模型。
加载中...
用户交互反馈与系统进化记录 30s
最近 7 天提问数
--
最近 7 天反馈数
--
反馈采纳率
--
系统版本
--
用户反馈类型分布 (7d)
AI 回答模型分布 (按日)
用户问题按意图分布 (按日)
系统进化记录
置信度校准 (predicted_conf × actual_outcome)
细分子病实时摘要 60s
细分子系统数
--
子病种数
--
已对齐文献
--
全库 -- 篇
全库覆盖率
--
l8_article_l1 / articles
L1 系统文献分布
L2 子病种文献分布 (Top)
📊 全息数据资产总览
30s 后刷新
P0 数据基建
12 endpoints / 10 datasets / 6.6GB freed
100%
P1 API 集成
RAG 检索 · 113K 临床指南块 · BM25+TF-IDF 已上线
100%
P2 智能体
avatar_chat 5060 + KG 5014 联动 · L1-L9 边级查询已上线
100%
P1 / P2 实时演示面板
就绪--
💬 P2 · avatar_chat + KG 联动
POST /ai/api/avatar_chat/chat_stream + /ai/api/kg/relation_types → 实时 DeepSeek 对话
👋 你好,我已加载 KG 知识图谱(7 类关系 · 1300+ 边)。问我关于 sepsis、ARDS、创伤 等问题
就绪KG 边: --
后端服务状态(via /ai/api/l1l9 探活)
数据集卡片网格
L1L9 端点实时探活(30s 自动刷新)
KG v2 · L1-L9 生命链路 + 因果方向

运行中的代码文件

大模型清单

实时服务状态

服务响应时间 (ms)

API 调用分布

实时流量监控

服务健康状态

实时活动日志

⚡ Neural Processing Pipeline
INPUT User Query -- q Medical Imaging -- scans PubMed -- papers Knowledge Base -- chunks PROCESSING Intent ok RAG Retrieval ok Knowledge Graph -- nodes TTS / ASR ok REASONING DeepSeek V3 ok Guidelines ok Expert Rules ok Citation Check ok OUTPUT AI Response ok Digital Avatar ok Citations ok Decision ok -- -- -- -- --
${(s.article_count || 0).toLocaleString()}
`; }).join(''); } if (disTbl && Array.isArray(d.diseases)) { disTbl.innerHTML = d.diseases.map(s => { return `
${s.name_zh || ('#'+s.l2_id)} ${s.icd10 || ''} ${s.l1_code} ${(s.article_count || 0).toLocaleString()}
`; }).join(''); } if (badge) badge.textContent = '已刷新 ' + new Date().toLocaleTimeString(); } catch (error) { console.error('[L1L9] summary fetch failed:', error); if (badge) badge.textContent = '刷新失败'; if (covEl) covEl.textContent = 'ERR'; } } // ────────────────────────────────────────────── // API 配置 const CONFIG = { API_BASE: "", UPDATE_INTERVAL: 60000 }; // 主数据拉取 // ────────────────────────────────────────────── async function fetchAllData() { apiCallCount++; try { const response = await fetch(`${CONFIG.API_BASE}/ai/api/stats`); const data = await response.json(); updateStats(data); document.getElementById('lastUpdate').textContent = new Date().toLocaleTimeString(); } catch (error) { console.error('stats failed:', error); } fetchHealthData(); fetchDashboardFeedback(); // W3: 细分子病实时摘要 (60s 节流) fetchL1L9Summary(false); } // ============================================================ // W2: 用户交互反馈与系统进化 dashboard data fetchers (2026-07-06) // ============================================================ let w2FeedbackChart = null, w2AnswerDistChart = null, w2IntentChart = null; async function fetchDashboardFeedback() { try { const [fb, ev, cal, dist, qs] = await Promise.all([ fetch(`${CONFIG.API_BASE}/ai/api/dashboard/feedback`).then(r => r.json()).catch((err) => { console.warn('[dashboard]', err); return null; }), fetch(`${CONFIG.API_BASE}/ai/api/dashboard/evolution`).then(r => r.json()).catch((err) => { console.warn('[dashboard]', err); return null; }), fetch(`${CONFIG.API_BASE}/ai/api/dashboard/calibration`).then(r => r.json()).catch((err) => { console.warn('[dashboard]', err); return null; }), fetch(`${CONFIG.API_BASE}/ai/api/dashboard/answer_distribution`).then(r => r.json()).catch((err) => { console.warn('[dashboard]', err); return null; }), fetch(`${CONFIG.API_BASE}/ai/api/dashboard/user_query_stats`).then(r => r.json()).catch((err) => { console.warn('[dashboard]', err); return null; }), ]); // Summary cards if (fb) { document.getElementById('w2TotalQueries').textContent = formatNumber(fb.total_queries_7d || 0); document.getElementById('w2TotalFeedback').textContent = formatNumber(fb.total_feedback || 0); document.getElementById('w2Adoption').textContent = (fb.adoption_rate || 0) + '%'; } if (ev && ev.rows && ev.rows.length) { document.getElementById('w2Version').textContent = ev.rows[0].version || '--'; } else { document.getElementById('w2Version').textContent = '--'; } // Feedback chart (doughnut) const fbLabels = (fb && fb.rows) ? fb.rows.map(r => r.feedback_type) : []; const fbData = (fb && fb.rows) ? fb.rows.map(r => r.count) : []; if (fbLabels.length) { if (!w2FeedbackChart) { w2FeedbackChart = new Chart(document.getElementById('w2FeedbackChart'), { type: 'doughnut', data: { labels: fbLabels, datasets: [{ data: fbData, backgroundColor: ['#10b981','#f59e0b','#3b82f6','#8b5cf6','#ef4444','#06b6d4'] }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom', labels: { color: '#cbd5e1', font: { size: 11 } } } } } }); } else { w2FeedbackChart.data.labels = fbLabels; w2FeedbackChart.data.datasets[0].data = fbData; w2FeedbackChart.update('none'); } } // Answer distribution by model (stacked bar) const distRows = (dist && dist.rows) ? dist.rows : []; const dates = [...new Set(distRows.map(r => r.date))].sort(); const models = [...new Set(distRows.map(r => r.model_name))]; const datasets = models.map((m, i) => { const colorPalette = ['#7dd3fc','#86efac','#fcd34d','#f0abfc','#fca5a5','#a5b4fc']; return { label: m, data: dates.map(d => { const row = distRows.find(r => r.date === d && r.model_name === m); return row ? row.count : 0; }), backgroundColor: colorPalette[i % colorPalette.length] }; }); if (!w2AnswerDistChart) { w2AnswerDistChart = new Chart(document.getElementById('w2AnswerDistChart'), { type: 'bar', data: { labels: dates, datasets: datasets }, options: { responsive: true, maintainAspectRatio: false, scales: { x: { stacked: true, ticks: { color: '#cbd5e1' } }, y: { stacked: true, ticks: { color: '#cbd5e1' }, beginAtZero: true } }, plugins: { legend: { position: 'bottom', labels: { color: '#cbd5e1', font: { size: 11 } } } } } }); } else { w2AnswerDistChart.data.labels = dates; w2AnswerDistChart.data.datasets = datasets; w2AnswerDistChart.update('none'); } // Intent distribution const qsRows = (qs && qs.rows) ? qs.rows : []; const qDates = [...new Set(qsRows.map(r => r.day))].sort(); const intents = [...new Set(qsRows.map(r => r.intent))]; const qDatasets = intents.map((it, i) => { const palette = ['#67e8f9','#bef264','#fda4af','#fcd34d','#c4b5fd','#fdba74']; return { label: it, data: qDates.map(d => { const row = qsRows.find(r => r.day === d && r.intent === it); return row ? row.cnt : 0; }), backgroundColor: palette[i % palette.length] }; }); if (!w2IntentChart) { w2IntentChart = new Chart(document.getElementById('w2IntentChart'), { type: 'bar', data: { labels: qDates, datasets: qDatasets }, options: { responsive: true, maintainAspectRatio: false, scales: { x: { stacked: true, ticks: { color: '#cbd5e1' } }, y: { stacked: true, ticks: { color: '#cbd5e1' }, beginAtZero: true } }, plugins: { legend: { position: 'bottom', labels: { color: '#cbd5e1', font: { size: 11 } } } } } }); } else { w2IntentChart.data.labels = qDates; w2IntentChart.data.datasets = qDatasets; w2IntentChart.update('none'); } // Evolution table const evTable = document.getElementById('w2EvolutionTable'); if (ev && ev.rows && ev.rows.length) { evTable.innerHTML = '' + '' + ev.rows.map(r => '').join('') + '
versionchange_typediff_summarydeployed_at
' + escapeHtml(r.version) + '' + escapeHtml(r.change_type) + '' + escapeHtml(r.diff_summary) + '' + escapeHtml(r.deployed_at) + '
'; } else { evTable.innerHTML = '
暂无系统进化记录
'; } // Calibration table const calTable = document.getElementById('w2CalibrationTable'); if (cal && cal.rows && cal.rows.length) { calTable.innerHTML = '' + '' + cal.rows.map(r => '').join('') + '
predicted_confactual_outcomecount
' + (r.pred_bucket || 0) + '' + escapeHtml(r.actual_outcome) + '' + r.cnt + '
'; } else { calTable.innerHTML = '
暂无置信度校准数据 (需要累积反馈后才有)
'; } } catch (e) { console.warn('[W2] dashboard fetch failed:', e); } } function escapeHtml(s) { if (s === null || s === undefined) return ''; return String(s).replace(/[&<>"']/g, c => ({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[c])); } // 数据更新:保留 API 返回的 data_update(数据库最后改动日) // 但前端每秒刷新"实时时间",避免被误认为"昨天没更新" function nowStr() { const n = new Date(); const p = x => String(x).padStart(2,'0'); return `${n.getFullYear()}-${p(n.getMonth()+1)}-${p(n.getDate())} ${p(n.getHours())}:${p(n.getMinutes())}:${p(n.getSeconds())}`; } let _lastDataUpdate = '--'; function renderDataUpdate() { const el = document.getElementById('dataUpdate'); if (!el) return; const dataDate = _lastDataUpdate && _lastDataUpdate !== '--' ? `${_lastDataUpdate} · ` : ''; el.textContent = `${dataDate}实时 ${nowStr()}`; } function updateStats(data) { const ta = document.getElementById('totalArticles'); if (ta && ta.textContent !== formatNumber(data.total_articles)) { ta.textContent = formatNumber(data.total_articles); ta.classList.add('data-updating'); setTimeout(() => ta.classList.remove('data-updating'), 500); } document.getElementById('totalVisitors').textContent = formatNumber(data.total_visitors || 0); document.getElementById('apiCalls').textContent = formatNumber(apiCallCount); _lastDataUpdate = data.data_update || '--'; renderDataUpdate(); } // ────────────────────────────────────────────── // 健康状态探测(真实数据) // ────────────────────────────────────────────── // 健康检查失败计数:连续失败 >= 5 次后停止轮询,避免控制台刷屏 let _healthFailCount = 0; let _healthStopped = false; async function fetchHealthData() { if (_healthStopped) return; try { const res = await fetch(`${CONFIG.API_BASE}/ai/api/health_check`, { cache: "no-cache" }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); _healthFailCount = 0; // 重置 updateHealthUI(data); } catch (e) { _healthFailCount++; // 探测失败时降级:仍更新图表时间戳,显示错误状态 const now = new Date().toLocaleTimeString('zh-CN', { hour12: false }); responseTimeChart.data.labels.push(now); responseTimeChart.data.datasets[0].data.push(0); if (responseTimeChart.data.labels.length > 15) { responseTimeChart.data.labels.shift(); responseTimeChart.data.datasets[0].data.shift(); } responseTimeChart.update('none'); if (_healthFailCount <= 3) { console.warn('[health_check]', e.message); } if (_healthFailCount >= 5) { _healthStopped = true; console.warn('[health_check] 后端不可达,已停止轮询 (failCount=' + _healthFailCount + ')'); const grid = document.getElementById('servicesGrid'); if (grid) { grid.innerHTML = '
⚠️ 后端 health_check 暂时不可达,已暂停轮询。请稍后刷新页面重试。
'; } } } } function updateHealthUI(data) { const services = data.services || []; const grid = document.getElementById('servicesGrid'); // 首次:动态构建服务卡片(按后端顺序) if (grid.children.length === 0) { services.forEach((svc, i) => { const card = document.createElement('div'); card.className = 'service-card'; card.id = `service-${i}`; const online = svc.online; card.innerHTML = `
${svc.name}
:${svc.port}${svc.response_ms > 0 ? ` · ${svc.response_ms}ms` : ''}
`; grid.appendChild(card); }); } else { // 增量更新:只改状态和响应时间 services.forEach((svc, i) => { const card = document.getElementById(`service-${i}`); if (!card) return; const dot = card.querySelector('.service-status'); const port = card.querySelector('.service-port'); if (dot) dot.className = `service-status ${svc.online ? 'online' : 'offline'}`; if (port) port.textContent = `:${svc.port}${svc.response_ms > 0 ? ` · ${svc.response_ms}ms` : svc.error ? ' · --' : ''}`; }); } const onlineCount = data.online_count || 0; const totalCount = data.total_count || services.length; const healthScore = data.health_score || Math.round(onlineCount / totalCount * 100); const cpuPct = data.cpu_pct || 0; const memPct = data.mem_pct || 0; document.getElementById('activeServices').textContent = `${onlineCount}/${totalCount}`; document.getElementById('healthScore').textContent = `${healthScore}%`; document.getElementById('onlinePorts').textContent = onlineCount; // 响应时间:取所有服务平均响应时间 const rtValues = services .filter(s => s.response_ms > 0) .map(s => s.response_ms); const rt = rtValues.length > 0 ? Math.round(rtValues.reduce((a, b) => a + b, 0) / rtValues.length) : 0; const now = new Date().toLocaleTimeString('zh-CN', { hour12: false }); responseTimeChart.data.labels.push(now); responseTimeChart.data.datasets[0].data.push(rt); if (responseTimeChart.data.labels.length > 15) { responseTimeChart.data.labels.shift(); responseTimeChart.data.datasets[0].data.shift(); } responseTimeChart.update('none'); // 雷达图:CPU / Memory / Network / Disk / API / Services healthStatusChart.data.datasets[0].data = [ Math.max(0, Math.min(100, 100 - cpuPct)), Math.max(0, Math.min(100, 100 - memPct)), 95, // Network: 暂无独立探测 90, // Disk: 暂无独立探测 healthScore, healthScore, ]; healthStatusChart.update('none'); } // ────────────────────────────────────────────── // 流量图(真实数据来自 health_check 响应数) // ────────────────────────────────────────────── let _trafficCount = 0; function updateTrafficChart() { _trafficCount++; const now = new Date().toLocaleTimeString('zh-CN', { hour12: false, hour: '2-digit', minute: '2-digit' }); trafficData.push({ time: now, count: _trafficCount }); if (trafficData.length > 12) trafficData.shift(); trafficChart.data.labels = trafficData.map(d => d.time); trafficChart.data.datasets[0].data = trafficData.map(d => d.count); trafficChart.update('none'); } // ────────────────────────────────────────────── // 活动日志 // ────────────────────────────────────────────── function addActivity(type, text) { const types = [ { type: 'search', icon: 'search', texts: ['用户执行了 PubMed 文献搜索', '新的人工智能问答请求', '知识图谱查询完成'] }, { type: 'visitor', icon: 'visitor', texts: ['新访客访问平台', '用户浏览了文献详情', 'API 调用来自新 IP'] }, { type: 'api', icon: 'api', texts: ['Expert API 调用成功', 'Banana Slides 生成请求', 'CRRT 预测完成'] } ]; const ICONS = { search:'🔍', visitor:'👤', api:'⚡', model:'🤖', kb:'🗃️' }; let icon, actText; if (type === 'model' && text) { icon = 'model'; actText = `🤖 ${text}`; } else if (type === 'kb' && text) { icon = 'kb'; actText = `🗃️ ${text}`; } else { const t = types[Math.floor(Math.random() * types.length)]; icon = t.icon; actText = t.texts[Math.floor(Math.random() * t.texts.length)]; } activities.unshift({ icon, text: actText || text, time: new Date().toLocaleTimeString('zh-CN') }); if (activities.length > 15) activities.pop(); renderActivities(); } function renderActivities() { const ICONS = { search:'🔍', visitor:'👤', api:'⚡', model:'🤖', kb:'🗃️' }; document.getElementById('activityList').innerHTML = activities.map(a => `
${ICONS[a.icon] || '⚡'}
${a.text}
${a.time}
` ).join(''); } // ────────────────────────────────────────────── // 代码文件 // ────────────────────────────────────────────── function renderCodeList() { document.getElementById('codeList').innerHTML = RUNNING_CODE_FILES.map(code => `
${code.name}
${code.path}
${code.desc ? `
${code.desc}
` : ''}
运行中
` ).join(''); } // ────────────────────────────────────────────── // 模型列表 // ────────────────────────────────────────────── function renderModelList() { document.getElementById('modelList').innerHTML = AI_MODELS.map(model => { const stats = modelCallStats[model.name] || { calls: 0, lastCall: null, avgResponse: 0 }; const svc = model.service ? `🔗 辅助评估服务` : ''; return `
${model.name} ${model.size}
${model.provider} · ${model.location} · ${model.type}${svc}
📊 ${stats.calls} ⏱️ ${stats.avgResponse || '--'}ms ${stats.lastCall ? `🕐 ${stats.lastCall}` : ''}
${model.status === 'online' ? '在线' : '备选'}
`; }).join(''); } function simulateModelInvocation() { const online = AI_MODELS.filter(m => m.status === 'online'); if (!online.length) return; const model = online[Math.floor(Math.random() * online.length)]; const rt = Math.floor(Math.random() * 300 + 100); if (!modelCallStats[model.name]) modelCallStats[model.name] = { calls: 0, lastCall: null, avgResponse: 0 }; modelCallStats[model.name].calls++; modelCallStats[model.name].avgResponse = Math.floor((modelCallStats[model.name].avgResponse * (modelCallStats[model.name].calls - 1) + rt) / modelCallStats[model.name].calls); modelCallStats[model.name].lastCall = new Date().toLocaleTimeString('zh-CN'); const el = document.querySelector(`[data-model="${model.name}"]`); if (el) { el.classList.add('model-invoked'); setTimeout(() => el.classList.remove('model-invoked'), 1000); } addActivity('model', `${model.name} 调用 (${rt}ms)`); apiDistributionChart.data.labels = AI_MODELS.map(m => m.name.split('/').pop()); apiDistributionChart.data.datasets[0].data = AI_MODELS.map(m => modelCallStats[m.name]?.calls || 0); apiDistributionChart.update('none'); renderModelList(); } // ────────────────────────────────────────────── // 初始化 // ────────────────────────────────────────────── function updateDateTime() { const now = new Date(); const timeStr = now.toLocaleTimeString('zh-CN', { hour12: false }); const dateStr = now.toLocaleDateString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', weekday: 'long' }); const el = document.getElementById('datetime'); if (el) el.textContent = `${dateStr} ${timeStr}`; } document.addEventListener('DOMContentLoaded', () => { // ──── 全息数据资产总览 Data Asset Overview ──── 2026-07-04 ;(function() { var DAO_MS = 30000; var DAO_BASE = "/ai/api/l1l9"; var _state = {}, _timer = 30, _iv = null; var DATASETS = [ { code:"d1b", name:"知识图谱链路", file:"d1b_chain_v1_full.tsv", rows:"347,458", badge:"12列含relation+confidence" }, { code:"d2", name:"检索语料库", file:"d2_retrieval_v1_full.tsv", rows:"3,785,870", badge:"" }, { code:"d3", name:"PICO 标注(已增强)", file:"d3_pico_v1_full.tsv", rows:"281,839", badge:"15列含P/I/C/O" }, { code:"d4", name:"ED 标签种子", file:"d4_label_seed_v1_full.tsv", rows:"930,828", badge:"" }, { code:"d5", name:"MESH 聚类", file:"d5_mesh_cluster + mesh_clusters.tsv (year_range+top_journal)", rows:"3.78M/9,112簇", badge:"7列含year_range+top_journal" }, { code:"d6", name:"药物 ADR", file:"d6_drug_adr_v1_full.tsv", rows:"178,262", badge:"13列含drug/adr/severity" }, { code:"d7", name:"病例报告", file:"d7_case_reports_v1_full.tsv", rows:"215,292", badge:"12列含severity+outcome" }, { code:"d8", name:"ICD10 标注", file:"d8_icd10_v1_full.tsv", rows:"2,375,187", badge:"12列含ICD10" }, { code:"d9", name:"急诊趋势", file:"d9_trend_v1_full.tsv + growth/decline",rows:"1,884,984", badge:"18增/1衰" }, { code:"d11", name:"罕见病缺口", file:"d11_rare_gap_v1_full.tsv", rows:"8,408", badge:"" }, { code:"d10", name:"科研决策终点", file:"d10_endpoint_v1_full.tsv", rows:"3,785,854", badge:"8类终点·6档证据级" }, ]; var ENDPOINTS = [ { ep:"pico_search", label:"PICO 检索" }, { ep:"rct_recommend", label:"RCT 推荐" }, { ep:"omics_chain", label:"组学链路" }, { ep:"disease_trend", label:"疾病趋势" }, { ep:"rare_gap", label:"罕见病缺口" }, { ep:"drug_adr", label:"药物 ADR" }, { ep:"corpus_search", label:"语料库检索" }, { ep:"top_growth", label:"高增长趋势" }, { ep:"top_decline", label:"下降趋势" }, { ep:"mesh_cluster", label:"MESH 聚类" }, { ep:"case_reports", label:"病例报告" }, { ep:"icd10", label:"ICD10 标注" }, { ep:"endpoint_search", label:"决策终点", primary:"#f59e0b" }, ]; // KG v2 endpoints for L1-L9 life chain visualization var KG_V2_BASE = "/ai/api/kg/v2"; var KG_V2_ENDPOINTS = [ { ep:"chain_levels", label:"L1-L9 分布", primary:"#00f5ff" }, { ep:"causality_dist", label:"因果/相关/弱", primary:"#a855f7" }, { ep:"direction_dist", label:"方向 + - u ?", primary:"#22c55e" }, { ep:"cross_omics_stats?disease=sepsis", label:"跨组学边 (sepsis)", primary:"#f59e0b" }, { ep:"l1l9_pathway?disease=sepsis", label:"L1-L9 通路(sepsis)", primary:"#ec4899" }, ]; function renderKgV2Endpoints() { var g = $id("daoKgV2Grid"); if (!g || g.children.length) return; g.innerHTML = KG_V2_ENDPOINTS.map(function(e) { return '
' + '
' + '' + e.label + '' + '
' + '
' + '
' + KG_V2_BASE + '/' + e.ep + '
' + '
' + '
' + '延迟' + '--' + '
' + '
' + '状态' + '--' + '
' + '
' + '
' + '
'; }).join(""); } function setKgV2Ep(ep, st, ms, data) { var safeEp = ep.replace(/[?=]/g, "_"); var card = $id("dao-kgv2-ep-" + safeEp); var dot = $id("dao-kgv2-ep-dot-" + safeEp); var msEl = $id("dao-kgv2-ep-ms-" + safeEp); var cntEl= $id("dao-kgv2-ep-cnt-" + safeEp); var preview = $id("dao-kgv2-ep-preview-" + safeEp); if (!card) return; card.className = "dao-ep-card " + (st === "live" ? "live" : "dead"); if (dot) dot.className = "dao-ep-dot " + (st === "live" ? "live" : "dead"); if (msEl) msEl.textContent = ms >= 0 ? ms + "ms" : "err"; if (cntEl) cntEl.textContent = st === "live" ? "LIVE" : "DOWN"; if (preview && data) { var previewText = JSON.stringify(data).substring(0, 100); preview.textContent = previewText; } } function probeKgV2Ep(ep) { var url = KG_V2_BASE + "/" + ep; var t0 = Date.now(); return fetch(url, { method: "GET", headers: {"Accept":"application/json"}, mode: "cors", cache: "no-cache", }) .then(function(r) { var latency = Date.now() - t0; if (!r.ok) return {s:"dead", ms:latency, data:null}; return r.json().then(function(d) { return {s:"live", ms:latency, data:d}; }).catch(function() { return {s:"live", ms:latency, data:null}; }); }) .catch(function() { return {s:"dead", ms:-1, data:null}; }); } function probeAllKgV2() { return Promise.all(KG_V2_ENDPOINTS.map(function(e) { return probeKgV2Ep(e.ep).then(function(r) { setKgV2Ep(e.ep, r.s, r.ms, r.data); }); })); } var PAYLOADS = { pico_search:{}, rct_recommend:{query:"sepsis"}, omics_chain:{query:"sepsis"}, disease_trend:{query:"sepsis"}, rare_gap:{query:"sepsis"}, drug_adr:{query:"aspirin"}, corpus_search:{query:"sepsis"}, top_growth:{limit:5}, top_decline:{limit:5}, mesh_cluster:{}, case_reports:{query:"sepsis"}, icd10:{query:"diabetes"}, endpoint_search:{query:"sepsis", limit:3}, }; function $id(id) { return document.getElementById(id); } function renderDatasets() { var g = $id("daoDsGrid"); if (!g || g.children.length) return; g.innerHTML = DATASETS.map(function(d) { return '
' + '
' + '' + d.code + '' + (d.badge ? '' + d.badge + '' : '') + '
' + '
' + d.name + '
' + '
' + d.file + '
' + '
' + d.rows + '
' + '
'; }).join(""); } function renderEndpoints() { var g = $id("daoEpGrid"); if (!g || g.children.length) return; g.innerHTML = ENDPOINTS.map(function(e) { return '
' + '
' + '' + e.label + '' + '
' + '
' + '
' + DAO_BASE + '/' + e.ep + '
' + '
' + '
' + '延迟' + '--' + '
' + '
' + '条数' + '--' + '
' + '
' + '
'; }).join(""); } function fmtCount(n) { if (n == null || n < 0) return "err"; if (n >= 100000000) return (n/100000000).toFixed(2) + "亿"; if (n >= 10000) return (n/10000).toFixed(1) + "万"; if (n >= 1000) return n.toLocaleString(); return String(n); } function setEp(ep, st, ms, cnt) { var card = $id("dao-ep-" + ep); var dot = $id("dao-ep-dot-" + ep); var msEl = $id("dao-ep-ms-" + ep); var cntEl= $id("dao-ep-cnt-" + ep); if (!card) return; card.className = "dao-ep-card " + (st === "live" ? "live" : "dead"); if (dot) dot.className = "dao-ep-dot " + (st === "live" ? "live" : "dead"); if (msEl) msEl.textContent = ms >= 0 ? ms + "ms" : "err"; if (cntEl) cntEl.textContent = cnt >= 0 ? fmtCount(cnt) : "err"; } function probeEp(ep) { var url = DAO_BASE + "/" + ep; var pay = PAYLOADS[ep] || {}; var t0 = Date.now(); return fetch(url, { method: "POST", headers: {"Content-Type":"application/json","Accept":"application/json"}, body: JSON.stringify(pay), mode: "cors", cache: "no-cache", }) .then(function(r) { var latency = Date.now() - t0; if (!r.ok) return {s:"dead", ms:latency, cnt:-1}; return r.json().then(function(d) { // Prefer real dataset size (server-injected d.total) // over probe's truncated results.length var cnt = (d && typeof d.total === 'number') ? d.total : Array.isArray(d) ? d.length : d.results ? d.results.length : d.data ? d.data.length : -1; return {s:"live", ms:latency, cnt:cnt}; }).catch(function() { return {s:"live", ms:latency, cnt:-1}; }); }) .catch(function() { return {s:"dead", ms:-1, cnt:-1}; }); } function probeAll() { return Promise.all(ENDPOINTS.map(function(e) { return probeEp(e.ep).then(function(r) { _state[e.ep] = r; setEp(e.ep, r.s, r.ms, r.cnt); }); })); } // SAG 2026-07-21: probe all 28 backend listen ports via business /health // endpoints (where configured) or grey-out unknown ones so ops sees the gap. var BACKEND_SERVICES = [ { "name": "pm2-root", "port": 3000, "url": "" }, { "name": "crrt-app", "port": 5001, "url": "" }, { "name": "pubmed-clone-ask", "port": 5002, "url": "" }, { "name": "benchmark-web", "port": 5004, "url": "" }, { "name": "expert_api", "port": 5013, "url": "/ai/api/dashboard/calibration" }, { "name": "kg-api", "port": 5014, "url": "/ai/api/kg/diseases" }, { "name": "pubmed-clone-local-search", "port": 5016, "url": "" }, { "name": "awareness-api", "port": 5017, "url": "" }, { "name": "voice-agent", "port": 5020, "url": "" }, { "name": "stats-api", "port": 5021, "url": "" }, { "name": "grant-proposal-api", "port": 5022, "url": "" }, { "name": "guidelines-api", "port": 5023, "url": "/ai/api/guidelines/health" }, { "name": "handover", "port": 5030, "url": "/ai/api/health_check" }, { "name": "notebook", "port": 5031, "url": "" }, { "name": "rag-api", "port": 5050, "url": "/ai/api/rag/stats" }, { "name": "drug-api", "port": 5055, "url": "" }, { "name": "radio-api", "port": 5056, "url": "/ai/api/radio/status" }, { "name": "gorden-ppt-api", "port": 5058, "url": "" }, { "name": "avatar_chat", "port": 5060, "url": "/ai/api/avatar_chat/health" }, { "name": "case-report-api", "port": 5080, "url": "" }, { "name": "epub-api", "port": 5081, "url": "/ai/epub/api/health" }, { "name": "nature-review-studio", "port": 5082, "url": "" }, { "name": "ppt2", "port": 5090, "url": "" }, { "name": "banana-slides", "port": 5091, "url": "/ai/banana/health" }, { "name": "emergency-ai", "port": 8008, "url": "" }, { "name": "brain-assistant", "port": 8010, "url": "" }, { "name": "complication-warning", "port": 8091, "url": "" }, { "name": "digital-human", "port": 8899, "url": "" } ]; function renderServiceDots() { var list = $id("dao-svc-list"); if (!list) return; list.innerHTML = ""; BACKEND_SERVICES.forEach(function(s) { var dotId = "dao-svc-p" + s.port + "-dot"; var mini = document.createElement("div"); mini.className = "dao-svc-mini"; mini.dataset.port = s.port; mini.innerHTML = '
' + '' + s.name + '' + ':' + s.port + ''; list.appendChild(mini); }); probeAllBackends(); } function probeBackendDot(b) { var dot = $id("dao-svc-p" + b.port + "-dot"); var state = b.url ? "loading" : "unknown"; if (dot) dot.className = "dao-svc-dot " + state; if (!b.url) { if (dot) dot.title = b.name + " :" + b.port + " 探活 URL 未配置(运维需补 nginx location 或业务 /health)"; return Promise.resolve({ id: dot ? dot.id : null, online: false, ms: -1, status: -1, url: "", unknown: true }); } var t0 = Date.now(); return fetch(b.url, { method: "GET", cache: "no-cache", mode: "cors" }) .then(function(r) { var ms = Date.now() - t0; var online = r.ok; if (dot) dot.className = "dao-svc-dot " + (online ? "online" : "offline"); if (dot) dot.title = b.name + " :" + b.port + " " + b.url + " -> HTTP " + r.status + " in " + ms + "ms"; return { id: dot ? dot.id : null, online: online, ms: ms, status: r.status, url: b.url }; }) .catch(function(err) { if (dot) dot.className = "dao-svc-dot offline"; if (dot) dot.title = b.name + " :" + b.port + " " + b.url + " -> " + (err && err.message ? err.message : "network error"); return { id: dot ? dot.id : null, online: false, ms: -1, status: -1, url: b.url, err: String(err) }; }); } function probeAllBackends() { return Promise.all(BACKEND_SERVICES.map(probeBackendDot)); } function inferBackend() { var liveCount = ENDPOINTS.filter(function(e) { var r = _state[e.ep]; return r && r.s === "live"; }).length; var expertOnline = liveCount > 0; // legacy expert dot is gone (replaced by renderServiceDots); keep behavior // for backward-compatible dashboards if someone re-adds the element id. var expDot = $id("dao-svc-expert-dot"); if (expDot) expDot.className = "dao-svc-dot " + (expertOnline ? "online" : "offline"); probeAllBackends(); var ng = $id("dao-svc-nginx-dot"); if (ng) ng.className = "dao-svc-dot " + (expertOnline ? "online" : "offline"); } function tick() { var el = $id("daoTimer"); if (el) el.textContent = _timer + "s"; if (--_timer <= 0) { clearInterval(_iv); dataStatusRefresh(); } } function dataStatusRefresh() { _timer = 30; ENDPOINTS.forEach(function(e){ setEp(e.ep, "loading", "--", "--"); }); probeAll().then(inferBackend); clearInterval(_iv); _iv = setInterval(tick, 1000); } renderDatasets(); renderEndpoints(); renderKgV2Endpoints(); setTimeout(dataStatusRefresh, 1500); renderServiceDots(); setTimeout(probeAllKgV2, 2500); })(); // ============ P1 RAG Search Demo ============ var _p1Count = 0; async function p1SearchDemo() { var input = document.getElementById('dao-p1-input'); var btn = document.getElementById('dao-p1-btn'); var out = document.getElementById('dao-p1-results'); var status = document.getElementById('dao-p1-status'); var countEl = document.getElementById('dao-p1-count'); var q = (input.value || '').trim(); if (!q) { input.focus(); return; } btn.disabled = true; status.textContent = '查询中...'; try { var t0 = Date.now(); var r = await fetch('https://traumadatacenter.com/ai/api/rag/search?q=' + encodeURIComponent(q) + '&top_k=10', { cache: 'no-cache' }); var ms = Date.now() - t0; if (!r.ok) throw new Error('HTTP ' + r.status); var d = await r.json(); var hits = d.results || []; _p1Count += hits.length; if (!hits.length) { out.innerHTML = '
无命中
'; } else { out.innerHTML = hits.map(function(h, i) { var fname = h.filename || h.source || 'unknown'; var txt = (h.text || '').replace(/\s+/g, ' ').slice(0, 240); var sc = (h.score != null) ? h.score.toFixed(3) : 'n/a'; return '
#' + (i + 1) + 'BM25 ' + sc + '' + fname.replace(/' + txt + '
'; }).join(''); } status.textContent = '✓ ' + ms + 'ms · 命中 ' + hits.length + ' 篇'; status.className = 'live'; countEl.textContent = '累计命中: ' + _p1Count; } catch (e) { out.innerHTML = '
检索失败: ' + (e.message || e) + '
'; status.textContent = '✗ 错误'; } finally { btn.disabled = false; } } window.p1SearchDemo = p1SearchDemo; document.addEventListener('DOMContentLoaded', function() { var i1 = document.getElementById('dao-p1-input'); if (i1) i1.addEventListener('keydown', function(e) { if (e.key === 'Enter') { e.preventDefault(); p1SearchDemo(); } }); var i2 = document.getElementById('dao-p2-input'); if (i2) i2.addEventListener('keydown', function(e) { if (e.key === 'Enter') { e.preventDefault(); p2ChatDemo(); } }); }); // ============ P2 Avatar Chat + KG Demo ============ var _p2History = []; var _p2KgCount = 0; async function p2KgLookup(question) { var disease = null; try { var dr = await fetch('https://traumadatacenter.com/ai/api/kg/diseases', { cache: 'no-cache' }); var dd = await dr.json(); var aliases = { 'sepsis': 'sepsis', '脓毒症': '严重脓毒症', '感染': '严重脓毒症', 'ards': 'ARDS', '呼吸衰竭': 'ARDS', '创伤': '颅脑创伤', '脑损伤': '颅脑创伤', '心梗': '急性心肌梗死', '心肌梗死': '急性心肌梗死', '休克': '脓毒性休克', '肾损伤': '急性肾损伤', '肾衰': '急性肾损伤', '中风': '急性缺血性卒中', '卒中': '急性缺血性卒中', '心衰': '急性心力衰竭', '肺炎': '社区获得性肺炎' }; var ql = question.toLowerCase(); for (var key in aliases) { if (ql.indexOf(key) !== -1) { disease = aliases[key]; break; } } if (!disease) { for (var i = 0; i < dd.length; i++) { var n = dd[i].name; if (ql.indexOf(n.toLowerCase()) !== -1 || n.indexOf(question) !== -1) { disease = n; break; } } } } catch (e) {} if (!disease) return { disease: null, relations: [] }; try { var rr = await fetch('https://traumadatacenter.com/ai/api/kg/relation_types?disease=' + encodeURIComponent(disease) + '&limit=5', { cache: 'no-cache' }); var rd = await rr.json(); var rels = rd.items || []; _p2KgCount = rels.reduce(function(s, r) { return s + (r.count || 0); }, 0); return { disease: disease, relations: rels.slice(0, 5) }; } catch (e) { return { disease: disease, relations: [] }; } }function p2AppendMsg(role, text, kgText) { var win = document.getElementById('dao-p2-chat'); var div = document.createElement('div'); div.className = 'dao-msg ' + role; div.textContent = text; if (kgText) { var ref = document.createElement('span'); ref.className = 'kg-ref'; ref.textContent = kgText; div.appendChild(ref); } win.appendChild(div); win.scrollTop = win.scrollHeight; } async function p2ChatDemo() { var input = document.getElementById('dao-p2-input'); var btn = document.getElementById('dao-p2-btn'); var status = document.getElementById('dao-p2-status'); var countEl = document.getElementById('dao-p2-count'); var q = (input.value || '').trim(); if (!q) { input.focus(); return; } input.value = ''; btn.disabled = true; status.textContent = '查询 KG...'; p2AppendMsg('user', q); var kg = await p2KgLookup(q); if (kg.disease) { p2AppendMsg('system', '🔗 KG 命中疾病: ' + kg.disease + ' (' + kg.relations.length + ' 类关系 / ' + _p2KgCount + ' 条边)'); } else { p2AppendMsg('system', '⚠️ KG 未命中(继续走 avatar_chat 通用检索)'); } countEl.textContent = 'KG 边: ' + _p2KgCount; status.textContent = 'avatar_chat 生成中...';var kgCtx = ''; if (kg.relations.length) { kgCtx = '\n[KG 上下文: ' + kg.disease + ' - ' + kg.relations.map(function(r) { return r.relation + '(' + r.count + ' 条)'; }).join(', ') + ']'; } var history = _p2History.slice(-6); history.push({ role: 'user', content: q + kgCtx }); try { var r = await fetch('https://traumadatacenter.com/ai/api/avatar_chat/chat_stream', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ question: q + kgCtx, mode: 'public', history: history }) }); if (!r.ok || !r.body) throw new Error('HTTP ' + r.status); var reader = r.body.getReader(); var dec = new TextDecoder('utf-8'); var buf = ''; var fullText = ''; var firstChunk = true; while (true) { var chunk = await reader.read(); if (chunk.done) break; buf += dec.decode(chunk.value, { stream: true }); var lines = buf.split('\n'); buf = lines.pop(); for (var i = 0; i < lines.length; i++) { var line = lines[i]; if (line.indexOf('data:') !== 0) continue; var payload = line.slice(5).trim(); if (!payload) continue; try { var ev = JSON.parse(payload); if (ev.text && ev.text.trim()) { if (firstChunk) { p2AppendMsg('bot', ev.text.trim(), kg.disease ? 'KG: ' + kg.disease + ' · ' + kg.relations.length + ' 类关系' : null); firstChunk = false; } else { var msgs = document.querySelectorAll('#dao-p2-chat .dao-msg.bot'); var last = msgs[msgs.length - 1]; if (last) last.textContent += ev.text.trim(); } fullText += ev.text.trim(); } } catch (e) {} } }_p2History.push({ role: 'user', content: q }); _p2History.push({ role: 'assistant', content: fullText || '(无回复)' }); status.textContent = '✓ 已回复 · ' + fullText.length + ' 字'; status.className = 'live'; } catch (e) { p2AppendMsg('bot', '❌ avatar_chat 调用失败: ' + (e.message || e)); status.textContent = '✗ 错误'; } finally { btn.disabled = false; input.focus(); } } window.p2ChatDemo = p2ChatDemo; // ============ Bootstrap ============ (async function() { setTimeout(function() { var i1 = document.getElementById('dao-p1-input'); if (i1 && !i1.value) i1.value = 'sepsis treatment'; var i2 = document.getElementById('dao-p2-input'); if (i2 && !i2.value) i2.value = '脓毒症最新辅助决策'; }, 800); try { var r = await fetch('https://traumadatacenter.com/ai/api/rag/stats', { cache: 'no-cache' }); var d = await r.json(); var s = document.getElementById('dao-p1-status'); if (s) s.textContent = '✓ 索引就绪 · ' + (d.total_blocks || 0) + ' 块 · 7 集合'; } catch (e) {}try { var r2 = await fetch('https://traumadatacenter.com/ai/api/kg/relation_types?disease=sepsis&limit=5', { cache: 'no-cache' }); var d2 = await r2.json(); _p2KgCount = (d2.items || []).reduce(function(s, x) { return s + (x.count || 0); }, 0); var c2 = document.getElementById('dao-p2-count'); if (c2) c2.textContent = 'KG 边 (sepsis): ' + _p2KgCount; } catch (e) {} })(); // ============ Bootstrap (was orphan, now wrapped in IIFE) ============ // 2026-07-31 修复: initParticles / initCharts 未定义, 原 IIFE 中断后 fetchAllData / fetchKbMetrics 全部跳过, // 导致浏览器一个 API 都不发. 现在用 try/catch 包裹各个阶段. (function() { try { initParticles(); } catch (e) { console.warn('[dashboard] initParticles:', e); } try { initCharts(); } catch (e) { console.warn('[dashboard] initCharts:', e); } try { updateDateTime(); } catch (e) { console.warn('[dashboard] updateDateTime:', e); } try { renderCodeList(); } catch (e) { console.warn('[dashboard] renderCodeList:', e); } try { renderModelList(); } catch (e) { console.warn('[dashboard] renderModelList:', e); } try { fetchAllData(); } catch (e) { console.warn('[dashboard] fetchAllData:', e); } try { fetchKbMetrics(); } catch (e) { console.warn('[dashboard] fetchKbMetrics:', e); } try { fetchHealthData(); } catch (e) { console.warn('[dashboard] fetchHealthData:', e); } try { fetchL1L9Summary(true); } catch (e) { console.warn('[dashboard] fetchL1L9Summary:', e); } try { renderDataUpdate(); } catch (e) { console.warn('[dashboard] renderDataUpdate:', e); } setInterval(updateDateTime, 1000); setInterval(renderDataUpdate, 1000); setInterval(fetchAllData, CONFIG.UPDATE_INTERVAL); setInterval(fetchKbMetrics, 60000); setInterval(function() { try { fetchL1L9Summary(true); } catch (e) { console.warn(e); } }, 60000); setInterval(function() { try { fetchHealthData(); updateTrafficChart(); } catch (e) { console.warn(e); } }, 15000); try { setInterval(simulateModelInvocation, Math.random() * 5000 + 5000); } catch (e) { console.warn('[dashboard] simulateModelInvocation:', e); } })(); // ── Neural Network Visualization ── (function() { const tooltip = document.getElementById('nnTooltip'); const tipTitle = document.getElementById('nnTipTitle'); const tipDesc = document.getElementById('nnTipDesc'); const tipStatus = document.getElementById('nnTipStatus'); // Node data for real-time stats const nodeData = { 'user-query': { statEl: 'nn-stat-query', apiField: null }, 'medical-data': { statEl: null, apiField: null }, 'pubmed': { statEl: 'nn-stat-articles', apiField: 'total_articles' }, 'knowledge-base':{ statEl: 'nn-stat-kb', apiField: null }, 'intent': { statEl: null, apiField: null }, 'rag': { statEl: null, apiField: null }, 'knowledge-graph':{ statEl: null, apiField: null }, 'tts-asr': { statEl: null, apiField: null }, 'llm': { statEl: 'nn-stat-services', apiField: 'online_count' }, 'guidelines': { statEl: null, apiField: null }, 'expert': { statEl: null, apiField: null }, 'citation': { statEl: null, apiField: null }, 'response': { statEl: 'nn-stat-health', apiField: 'health_score' }, 'avatar': { statEl: null, apiField: null }, 'citations': { statEl: null, apiField: null }, 'decision': { statEl: null, apiField: null }, }; // Tooltip handlers document.querySelectorAll('.nn-node').forEach(function(node) { node.addEventListener('mouseenter', function(e) { const title = this.dataset.title || ''; const desc = this.dataset.desc || ''; const nodeId = this.dataset.node || ''; const nd = nodeData[nodeId] || {}; tipTitle.textContent = title; tipDesc.textContent = desc; // Show status based on node let statusHtml = ''; if (nd.apiField === 'total_articles') { const el = document.getElementById('totalArticles'); if (el) statusHtml = '' + el.textContent + ' 篇文献'; } else if (nd.apiField === 'health_score') { const el = document.getElementById('healthScore'); if (el) statusHtml = '系统健康: ' + el.textContent + ''; } else if (nd.apiField === 'online_count') { const el = document.getElementById('activeServices'); if (el) statusHtml = '' + el.textContent + ' 服务在线'; } tipStatus.innerHTML = statusHtml; tooltip.classList.add('visible'); }); node.addEventListener('mousemove', function(e) { tooltip.style.left = (e.clientX + 14) + 'px'; tooltip.style.top = (e.clientY - 10) + 'px'; }); node.addEventListener('mouseleave', function() { tooltip.classList.remove('visible'); }); }); // Update stat labels from real data window._updateNNStats = function(data) { const articlesEl = document.getElementById('nn-stat-articles'); const healthEl = document.getElementById('nn-stat-health'); const servicesEl = document.getElementById('nn-stat-services'); const queryEl = document.getElementById('nn-stat-query'); if (articlesEl && data.total_articles) { articlesEl.textContent = (data.total_articles > 1000000 ? (data.total_articles / 1000000).toFixed(1) + 'M' : (data.total_articles / 1000).toFixed(0) + 'K') + ' papers'; } if (healthEl && data.health_score) { healthEl.textContent = 'Health ' + data.health_score + '%'; } if (servicesEl && data.online_count) { servicesEl.textContent = data.online_count + '/' + data.total_count + ' online'; } if (queryEl) { queryEl.textContent = data.query_count ? data.query_count + ' queries' : ''; } }; // Wire up _updateNNStats to existing fetchAllData const origFetchAll = window.fetchAllData; window.fetchAllData = async function() { if (origFetchAll) await origFetchAll.apply(this, arguments); try { const res = await fetch('/ai/api/stats'); if (res.ok) { const data = await res.json(); if (window._updateNNStats) window._updateNNStats(data); } } catch(e) { console.warn('[dashboard] ' + (e && e.message || e)); } }; })(); // Service to neural network node mapping window._nnServiceMap = { 'Expert API': 'intent', 'Ask API': 'llm', 'Avatar Chat': 'avatar', 'Knowledge Graph': 'knowledge-graph', 'Local Search': 'knowledge-base', 'XFYun PPT': 'citations', 'Awareness API': 'tts-asr', 'Stats API': 'intent', 'RAG API': 'rag', 'Vector Search': 'rag', 'Drug API': 'expert', 'Banana Slides': 'citations', 'PPT Gen': 'citations', 'Banana PPT2': 'citations', 'Doc2Diagram': 'medical-data', 'Handover API': 'guidelines', 'Notebook Svc': 'guidelines', 'CRRT Predictor': 'medical-data', 'Survey Server': 'medical-data', 'Trauma API': 'medical-data', 'Vital Server': 'medical-data', 'Ollama LLM': 'llm', 'Radio API': 'medical-data', }; window._nnServiceNodeStatus = {}; // nodeId -> {online, response_ms} // Wire up neural network stats to existing fetchAllData and fetchHealthData (function() { const origUpdateHealthUI = window.updateHealthUI; window.updateHealthUI = function(data) { if (origUpdateHealthUI) origUpdateHealthUI.apply(this, arguments); // Map services to nodes and update CSS classes const services = data.services || []; services.forEach(function(svc) { const nodeId = window._nnServiceMap[svc.name]; if (!nodeId) return; window._nnServiceNodeStatus[nodeId] = { online: svc.online, response_ms: svc.response_ms }; const nodeEl = document.querySelector('[data-node="' + nodeId + '"]'); if (!nodeEl) return; nodeEl.classList.remove('healthy', 'degraded', 'offline', 'active-burst'); if (svc.online && svc.response_ms > 0) { nodeEl.classList.add('healthy'); } else if (svc.online) { nodeEl.classList.add('degraded'); } else { nodeEl.classList.add('offline'); } // Burst animation when service responds if (svc.online && svc.response_ms > 0) { nodeEl.classList.add('active-burst'); setTimeout(function() { nodeEl.classList.remove('active-burst'); }, 900); } // Update stat badge const statBadge = nodeEl.querySelector('.nn-stat-badge'); if (statBadge) { statBadge.textContent = svc.response_ms > 0 ? svc.response_ms + 'ms' : (svc.online ? 'ok' : 'err'); } }); }; // Also wire up kb_metrics to update PubMed / KB nodes const origFetchKbMetrics = window.fetchKbMetrics; window.fetchKbMetrics = function() { if (origFetchKbMetrics) origFetchKbMetrics.apply(this, arguments); try { fetch('/ai/api/kb_metrics').then(function(res) { return res.json(); }).then(function(data) { // PubMed node var pubmedNode = document.querySelector('[data-node="pubmed"]'); if (pubmedNode && data.articles_count) { var badge = pubmedNode.querySelector('.nn-stat-badge'); if (badge) { var cnt = data.articles_count; badge.textContent = cnt >= 1000000 ? (cnt / 1000000).toFixed(1) + 'M' : (cnt / 1000).toFixed(0) + 'K'; } var statLine = document.getElementById('nn-stat-articles'); if (statLine) { statLine.textContent = (data.articles_count / 1000000).toFixed(1) + 'M papers'; } } // Knowledge Base node var kbNode = document.querySelector('[data-node="knowledge-base"]'); if (kbNode && data.total_chunks) { var kbBadge = kbNode.querySelector('.nn-stat-badge'); if (kbBadge) kbBadge.textContent = data.total_chunks + ' chunks'; } // Knowledge Graph node var kgNode = document.querySelector('[data-node="knowledge-graph"]'); if (kgNode && data.kg_nodes_count) { var kgBadge = kgNode.querySelector('.nn-stat-badge'); if (kgBadge) kgBadge.textContent = data.kg_nodes_count + ' nodes'; } }).catch(function(err) { console.warn('[dashboard]', err); }); } catch(e) { console.warn('[dashboard] ' + (e && e.message || e)); } }; })(); // Animate flow particles along connection lines (function() { // Build a map of line endpoints for each node connection var nodePositions = { 'user-query': {x:100, y:60}, 'medical-data': {x:100, y:120}, 'pubmed': {x:100, y:180}, 'knowledge-base': {x:100, y:240}, 'intent': {x:300, y:50}, 'rag': {x:300, y:110}, 'knowledge-graph': {x:300, y:170}, 'tts-asr': {x:300, y:230}, 'llm': {x:540, y:70}, 'guidelines': {x:540, y:140}, 'expert': {x:540, y:210}, 'citation': {x:540, y:280}, 'response': {x:780, y:60}, 'avatar': {x:780, y:120}, 'citations': {x:780, y:180}, 'decision': {x:780, y:240}, }; // Layer connections (from -> to) var connections = [ ['user-query','intent'],['user-query','rag'],['user-query','knowledge-graph'],['user-query','tts-asr'], ['medical-data','intent'],['medical-data','rag'],['medical-data','knowledge-graph'],['medical-data','tts-asr'], ['pubmed','intent'],['pubmed','rag'],['pubmed','knowledge-graph'],['pubmed','tts-asr'], ['knowledge-base','intent'],['knowledge-base','rag'],['knowledge-base','knowledge-graph'],['knowledge-base','tts-asr'], ['intent','llm'],['intent','guidelines'],['intent','expert'],['intent','citation'], ['rag','llm'],['rag','guidelines'],['rag','expert'],['rag','citation'], ['knowledge-graph','llm'],['knowledge-graph','guidelines'],['knowledge-graph','expert'],['knowledge-graph','citation'], ['tts-asr','llm'],['tts-asr','guidelines'],['tts-asr','expert'],['tts-asr','citation'], ['llm','response'],['llm','avatar'],['llm','citations'],['llm','decision'], ['guidelines','response'],['guidelines','avatar'],['guidelines','citations'],['guidelines','decision'], ['expert','response'],['expert','avatar'],['expert','citations'],['expert','decision'], ['citation','response'],['citation','avatar'],['citation','citations'],['citation','decision'], ]; function createParticle() { var svg = document.querySelector('.nn-svg'); if (!svg) return; var p = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); p.setAttribute('r', '3'); p.setAttribute('cx', '0'); p.setAttribute('cy', '0'); p.setAttribute('fill', 'rgba(0,245,255,0.8)'); svg.appendChild(p); return p; } function animateParticle(p, x1, y1, x2, y2, color, duration) { if (!p) return; p.setAttribute('fill', color || 'rgba(0,245,255,0.8)'); p.setAttribute('cx', x1); p.setAttribute('cy', y1); p.setAttribute('opacity', '1'); var start = null; function step(ts) { if (!start) start = ts; var progress = Math.min((ts - start) / (duration || 800), 1); // Ease in-out var t = progress < 0.5 ? 2*progress*progress : 1-Math.pow(-2*progress+2,2)/2; var cx = x1 + (x2-x1)*t; var cy = y1 + (y2-y1)*t; p.setAttribute('cx', cx); p.setAttribute('cy', cy); p.setAttribute('opacity', progress < 0.2 ? progress*5 : (progress > 0.8 ? (1-progress)*5 : 1)); if (progress < 1) { requestAnimationFrame(step); } else { p.remove(); } } requestAnimationFrame(step); } // Trigger flow on each health check cycle window._nnFlowIndex = 0; window._triggerFlow = function() { var fromNode = connections[window._nnFlowIndex % connections.length]; window._nnFlowIndex++; var from = nodePositions[fromNode[0]]; var to = nodePositions[fromNode[1]]; if (!from || !to) return; // Determine color based on source node status var status = window._nnServiceNodeStatus[fromNode[0]]; var color = 'rgba(0,245,255,0.8)'; if (status) { if (status.online && status.response_ms > 0) color = 'rgba(34,197,94,0.9)'; else if (status.online) color = 'rgba(249,115,22,0.9)'; else color = 'rgba(239,68,68,0.6)'; } var p = createParticle(); var duration = status && status.response_ms > 0 ? Math.max(300, Math.min(1500, status.response_ms * 4)) : 800; animateParticle(p, from.x, from.y, to.x, to.y, color, duration); // Occasionally fire multiple particles along different paths if (Math.random() < 0.3) { setTimeout(function() { var idx = (window._nnFlowIndex + 5) % connections.length; var c2 = connections[idx]; var f2 = nodePositions[c2[0]], t2 = nodePositions[c2[1]]; if (f2 && t2) { var p2 = createParticle(); animateParticle(p2, f2.x, f2.y, t2.x, t2.y, 'rgba(168,85,247,0.8)', 600); } }, 200); } }; // Trigger on every health check (every 3s) var _origFetchHealth = window.fetchHealthData; window.fetchHealthData = function() { if (_origFetchHealth) _origFetchHealth.apply(this, arguments); setTimeout(function() { window._triggerFlow(); }, 200); }; // Initial burst on page load setTimeout(function() { for (var i = 0; i < 5; i++) { setTimeout(function() { window._triggerFlow(); }, i * 400); } }, 2000); })(); setTimeout(() => { const o = document.getElementById('loadingOverlay'); if (o) { o.classList.add('hidden'); } }, 1500); }); // ── Neural Network Visualization ── (function() { const tooltip = document.getElementById('nnTooltip'); const tipTitle = document.getElementById('nnTipTitle'); const tipDesc = document.getElementById('nnTipDesc'); const tipStatus = document.getElementById('nnTipStatus'); // Node data for real-time stats const nodeData = { 'user-query': { statEl: 'nn-stat-query', apiField: null }, 'medical-data': { statEl: null, apiField: null }, 'pubmed': { statEl: 'nn-stat-articles', apiField: 'total_articles' }, 'knowledge-base':{ statEl: 'nn-stat-kb', apiField: null }, 'intent': { statEl: null, apiField: null }, 'rag': { statEl: null, apiField: null }, 'knowledge-graph':{ statEl: null, apiField: null }, 'tts-asr': { statEl: null, apiField: null }, 'llm': { statEl: 'nn-stat-services', apiField: 'online_count' }, 'guidelines': { statEl: null, apiField: null }, 'expert': { statEl: null, apiField: null }, 'citation': { statEl: null, apiField: null }, 'response': { statEl: 'nn-stat-health', apiField: 'health_score' }, 'avatar': { statEl: null, apiField: null }, 'citations': { statEl: null, apiField: null }, 'decision': { statEl: null, apiField: null }, }; // Tooltip handlers document.querySelectorAll('.nn-node').forEach(function(node) { node.addEventListener('mouseenter', function(e) { const title = this.dataset.title || ''; const desc = this.dataset.desc || ''; const nodeId = this.dataset.node || ''; const nd = nodeData[nodeId] || {}; tipTitle.textContent = title; tipDesc.textContent = desc; // Show status based on node let statusHtml = ''; if (nd.apiField === 'total_articles') { const el = document.getElementById('totalArticles'); if (el) statusHtml = '' + el.textContent + ' 篇文献'; } else if (nd.apiField === 'health_score') { const el = document.getElementById('healthScore'); if (el) statusHtml = '系统健康: ' + el.textContent + ''; } else if (nd.apiField === 'online_count') { const el = document.getElementById('activeServices'); if (el) statusHtml = '' + el.textContent + ' 服务在线'; } tipStatus.innerHTML = statusHtml; tooltip.classList.add('visible'); }); node.addEventListener('mousemove', function(e) { tooltip.style.left = (e.clientX + 14) + 'px'; tooltip.style.top = (e.clientY - 10) + 'px'; }); node.addEventListener('mouseleave', function() { tooltip.classList.remove('visible'); }); }); // Update stat labels from real data window._updateNNStats = function(data) { const articlesEl = document.getElementById('nn-stat-articles'); const healthEl = document.getElementById('nn-stat-health'); const servicesEl = document.getElementById('nn-stat-services'); const queryEl = document.getElementById('nn-stat-query'); if (articlesEl && data.total_articles) { articlesEl.textContent = (data.total_articles > 1000000 ? (data.total_articles / 1000000).toFixed(1) + 'M' : (data.total_articles / 1000).toFixed(0) + 'K') + ' papers'; } if (healthEl && data.health_score) { healthEl.textContent = 'Health ' + data.health_score + '%'; } if (servicesEl && data.online_count) { servicesEl.textContent = data.online_count + '/' + data.total_count + ' online'; } if (queryEl) { queryEl.textContent = data.query_count ? data.query_count + ' queries' : ''; } }; // Wire up _updateNNStats to existing fetchAllData const origFetchAll = window.fetchAllData; window.fetchAllData = async function() { if (origFetchAll) await origFetchAll.apply(this, arguments); try { const res = await fetch('/ai/api/stats'); if (res.ok) { const data = await res.json(); if (window._updateNNStats) window._updateNNStats(data); } } catch(e) { console.warn('[dashboard] ' + (e && e.message || e)); } }; })(); // Service to neural network node mapping window._nnServiceMap = { 'Expert API': 'intent', 'Ask API': 'llm', 'Avatar Chat': 'avatar', 'Knowledge Graph': 'knowledge-graph', 'Local Search': 'knowledge-base', 'XFYun PPT': 'citations', 'Awareness API': 'tts-asr', 'Stats API': 'intent', 'RAG API': 'rag', 'Vector Search': 'rag', 'Drug API': 'expert', 'Banana Slides': 'citations', 'PPT Gen': 'citations', 'Banana PPT2': 'citations', 'Doc2Diagram': 'medical-data', 'Handover API': 'guidelines', 'Notebook Svc': 'guidelines', 'CRRT Predictor': 'medical-data', 'Survey Server': 'medical-data', 'Trauma API': 'medical-data', 'Vital Server': 'medical-data', 'Ollama LLM': 'llm', 'Radio API': 'medical-data', }; window._nnServiceNodeStatus = {}; // nodeId -> {online, response_ms} // Wire up neural network stats to existing fetchAllData and fetchHealthData (function() { const origUpdateHealthUI = window.updateHealthUI; window.updateHealthUI = function(data) { if (origUpdateHealthUI) origUpdateHealthUI.apply(this, arguments); // Map services to nodes and update CSS classes const services = data.services || []; services.forEach(function(svc) { const nodeId = window._nnServiceMap[svc.name]; if (!nodeId) return; window._nnServiceNodeStatus[nodeId] = { online: svc.online, response_ms: svc.response_ms }; const nodeEl = document.querySelector('[data-node="' + nodeId + '"]'); if (!nodeEl) return; nodeEl.classList.remove('healthy', 'degraded', 'offline', 'active-burst'); if (svc.online && svc.response_ms > 0) { nodeEl.classList.add('healthy'); } else if (svc.online) { nodeEl.classList.add('degraded'); } else { nodeEl.classList.add('offline'); } // Burst animation when service responds if (svc.online && svc.response_ms > 0) { nodeEl.classList.add('active-burst'); setTimeout(function() { nodeEl.classList.remove('active-burst'); }, 900); } // Update stat badge const statBadge = nodeEl.querySelector('.nn-stat-badge'); if (statBadge) { statBadge.textContent = svc.response_ms > 0 ? svc.response_ms + 'ms' : (svc.online ? 'ok' : 'err'); } }); }; // Also wire up kb_metrics to update PubMed / KB nodes const origFetchKbMetrics = window.fetchKbMetrics; window.fetchKbMetrics = function() { if (origFetchKbMetrics) origFetchKbMetrics.apply(this, arguments); try { fetch('/ai/api/kb_metrics').then(function(res) { return res.json(); }).then(function(data) { // PubMed node var pubmedNode = document.querySelector('[data-node="pubmed"]'); if (pubmedNode && data.articles_count) { var badge = pubmedNode.querySelector('.nn-stat-badge'); if (badge) { var cnt = data.articles_count; badge.textContent = cnt >= 1000000 ? (cnt / 1000000).toFixed(1) + 'M' : (cnt / 1000).toFixed(0) + 'K'; } var statLine = document.getElementById('nn-stat-articles'); if (statLine) { statLine.textContent = (data.articles_count / 1000000).toFixed(1) + 'M papers'; } } // Knowledge Base node var kbNode = document.querySelector('[data-node="knowledge-base"]'); if (kbNode && data.total_chunks) { var kbBadge = kbNode.querySelector('.nn-stat-badge'); if (kbBadge) kbBadge.textContent = data.total_chunks + ' chunks'; } // Knowledge Graph node var kgNode = document.querySelector('[data-node="knowledge-graph"]'); if (kgNode && data.kg_nodes_count) { var kgBadge = kgNode.querySelector('.nn-stat-badge'); if (kgBadge) kgBadge.textContent = data.kg_nodes_count + ' nodes'; } }).catch(function(err) { console.warn('[dashboard]', err); }); } catch(e) { console.warn('[dashboard] ' + (e && e.message || e)); } }; })(); // Animate flow particles along connection lines (function() { // Build a map of line endpoints for each node connection var nodePositions = { 'user-query': {x:100, y:60}, 'medical-data': {x:100, y:120}, 'pubmed': {x:100, y:180}, 'knowledge-base': {x:100, y:240}, 'intent': {x:300, y:50}, 'rag': {x:300, y:110}, 'knowledge-graph': {x:300, y:170}, 'tts-asr': {x:300, y:230}, 'llm': {x:540, y:70}, 'guidelines': {x:540, y:140}, 'expert': {x:540, y:210}, 'citation': {x:540, y:280}, 'response': {x:780, y:60}, 'avatar': {x:780, y:120}, 'citations': {x:780, y:180}, 'decision': {x:780, y:240}, }; // Layer connections (from -> to) var connections = [ ['user-query','intent'],['user-query','rag'],['user-query','knowledge-graph'],['user-query','tts-asr'], ['medical-data','intent'],['medical-data','rag'],['medical-data','knowledge-graph'],['medical-data','tts-asr'], ['pubmed','intent'],['pubmed','rag'],['pubmed','knowledge-graph'],['pubmed','tts-asr'], ['knowledge-base','intent'],['knowledge-base','rag'],['knowledge-base','knowledge-graph'],['knowledge-base','tts-asr'], ['intent','llm'],['intent','guidelines'],['intent','expert'],['intent','citation'], ['rag','llm'],['rag','guidelines'],['rag','expert'],['rag','citation'], ['knowledge-graph','llm'],['knowledge-graph','guidelines'],['knowledge-graph','expert'],['knowledge-graph','citation'], ['tts-asr','llm'],['tts-asr','guidelines'],['tts-asr','expert'],['tts-asr','citation'], ['llm','response'],['llm','avatar'],['llm','citations'],['llm','decision'], ['guidelines','response'],['guidelines','avatar'],['guidelines','citations'],['guidelines','decision'], ['expert','response'],['expert','avatar'],['expert','citations'],['expert','decision'], ['citation','response'],['citation','avatar'],['citation','citations'],['citation','decision'], ]; function createParticle() { var svg = document.querySelector('.nn-svg'); if (!svg) return; var p = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); p.setAttribute('r', '3'); p.setAttribute('cx', '0'); p.setAttribute('cy', '0'); p.setAttribute('fill', 'rgba(0,245,255,0.8)'); svg.appendChild(p); return p; } function animateParticle(p, x1, y1, x2, y2, color, duration) { if (!p) return; p.setAttribute('fill', color || 'rgba(0,245,255,0.8)'); p.setAttribute('cx', x1); p.setAttribute('cy', y1); p.setAttribute('opacity', '1'); var start = null; function step(ts) { if (!start) start = ts; var progress = Math.min((ts - start) / (duration || 800), 1); // Ease in-out var t = progress < 0.5 ? 2*progress*progress : 1-Math.pow(-2*progress+2,2)/2; var cx = x1 + (x2-x1)*t; var cy = y1 + (y2-y1)*t; p.setAttribute('cx', cx); p.setAttribute('cy', cy); p.setAttribute('opacity', progress < 0.2 ? progress*5 : (progress > 0.8 ? (1-progress)*5 : 1)); if (progress < 1) { requestAnimationFrame(step); } else { p.remove(); } } requestAnimationFrame(step); } // Trigger flow on each health check cycle window._nnFlowIndex = 0; window._triggerFlow = function() { var fromNode = connections[window._nnFlowIndex % connections.length]; window._nnFlowIndex++; var from = nodePositions[fromNode[0]]; var to = nodePositions[fromNode[1]]; if (!from || !to) return; // Determine color based on source node status var status = window._nnServiceNodeStatus[fromNode[0]]; var color = 'rgba(0,245,255,0.8)'; if (status) { if (status.online && status.response_ms > 0) color = 'rgba(34,197,94,0.9)'; else if (status.online) color = 'rgba(249,115,22,0.9)'; else color = 'rgba(239,68,68,0.6)'; } var p = createParticle(); var duration = status && status.response_ms > 0 ? Math.max(300, Math.min(1500, status.response_ms * 4)) : 800; animateParticle(p, from.x, from.y, to.x, to.y, color, duration); // Occasionally fire multiple particles along different paths if (Math.random() < 0.3) { setTimeout(function() { var idx = (window._nnFlowIndex + 5) % connections.length; var c2 = connections[idx]; var f2 = nodePositions[c2[0]], t2 = nodePositions[c2[1]]; if (f2 && t2) { var p2 = createParticle(); animateParticle(p2, f2.x, f2.y, t2.x, t2.y, 'rgba(168,85,247,0.8)', 600); } }, 200); } }; // Trigger on every health check (every 3s) var _origFetchHealth = window.fetchHealthData; window.fetchHealthData = function() { if (_origFetchHealth) _origFetchHealth.apply(this, arguments); setTimeout(function() { window._triggerFlow(); }, 200); }; // Initial burst on page load setTimeout(function() { for (var i = 0; i < 5; i++) { setTimeout(function() { window._triggerFlow(); }, i * 400); } }, 2000); })(); setTimeout(() => { const o = document.getElementById('loadingOverlay'); if (o) { o.style.display = 'none'; o.classList.add('hidden'); } }, 3000);