系统状态
运行中
数据更新
--
活跃服务
--
健康评分
--%
PubMed 文献总数
--
实时数据
平台访客总数
--
累计统计
API 调用次数
--
本会话
在线服务端口
--
健康运行
知识库实时指标
正在获取...
--
本地文献
--
篇 (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 联动
👋 你好,我已加载 KG 知识图谱(7 类关系 · 1300+ 边)。问我关于 sepsis、ARDS、创伤 等问题
后端服务状态(via /ai/api/l1l9 探活)
数据集卡片网格
L1L9 端点实时探活(30s 自动刷新)
KG v2 · L1-L9 生命链路 + 因果方向
运行中的代码文件
大模型清单
实时服务状态
服务响应时间 (ms)
API 调用分布
实时流量监控
服务健康状态
实时活动日志
⚡ Neural Processing Pipeline
${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 = '| version | change_type | diff_summary | deployed_at |
|---|---|---|---|
| ' + escapeHtml(r.version) + ' | ' + escapeHtml(r.change_type) + ' | ' + escapeHtml(r.diff_summary) + ' | ' + escapeHtml(r.deployed_at) + ' |
暂无系统进化记录
';
}
// Calibration table
const calTable = document.getElementById('w2CalibrationTable');
if (cal && cal.rows && cal.rows.length) {
calTable.innerHTML = '| predicted_conf | actual_outcome | count |
|---|---|---|
| ' + (r.pred_bucket || 0) + ' | ' + escapeHtml(r.actual_outcome) + ' | ' + r.cnt + ' |
暂无置信度校准数据 (需要累积反馈后才有)
';
}
} 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` : ''}
${a.text}
${a.time}
${code.name}
${code.path}
${code.desc ? `${code.desc}
` : ''}
运行中
${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 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 '' +
'' + e.label + '' +
'' +
'
' +
'' + KG_V2_BASE + '/' + e.ep + '
' +
'' +
'
' +
'' +
'' +
'延迟' +
'--' +
'
' +
'' +
'状态' +
'--' +
'
' +
'' +
'
';
}).join("");
}
function renderEndpoints() {
var g = $id("daoEpGrid"); if (!g || g.children.length) return;
g.innerHTML = ENDPOINTS.map(function(e) {
return '' +
'' + d.code + '' +
(d.badge ? '' + d.badge + '' : '') +
'
' +
'' + d.name + '
' +
'' + d.file + '
' +
'' + d.rows + '行
' +
'' +
'
';
}).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 = '' +
'' + e.label + '' +
'' +
'
' +
'' + DAO_BASE + '/' + e.ep + '
' +
'' +
'
' +
'' +
'延迟' +
'--' +
'
' +
'' +
'条数' +
'--' +
'
' +
'无命中
';
} 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);