jiaoxue/medical/medlit-agent.html

274 lines
10 KiB
HTML
Raw Permalink Normal View History

2026-02-28 15:05:39 +08:00
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>医学文献检索总结 Agent</title>
<style>
:root { --bg:#0b1220; --panel:#111a2e; --muted:#8aa0c5; --text:#e6eeff; --border:#233152; --accent:#4f8cff; --danger:#ff5c7a; }
* { box-sizing: border-box; }
body { margin:0; font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "PingFang SC", "Microsoft YaHei"; background: radial-gradient(1200px 600px at 10% 0%, #182447, var(--bg)); color:var(--text); }
.wrap { max-width: 1200px; margin: 0 auto; padding: 20px; }
.header { display:flex; align-items:flex-end; justify-content:space-between; gap:12px; margin-bottom: 16px; }
.title { font-size: 18px; font-weight: 700; letter-spacing: .2px; }
.sub { font-size: 12px; color: var(--muted); margin-top: 4px; }
.panel { background: linear-gradient(180deg, rgba(255,255,255,.04), rgba(255,255,255,.02)); border:1px solid var(--border); border-radius: 14px; padding: 14px; }
label { display:block; font-size: 12px; color: var(--muted); margin-bottom: 6px; }
input[type="text"], input[type="number"], textarea { width: 100%; background: rgba(0,0,0,.18); color: var(--text); border:1px solid var(--border); border-radius: 10px; padding: 10px 10px; outline: none; }
textarea { min-height: 120px; resize: vertical; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 12px; line-height: 1.45; }
.grid { display:grid; grid-template-columns: 1fr 1fr; gap: 12px; }
@media (max-width: 980px) { .grid { grid-template-columns: 1fr; } }
.btns { display:flex; gap:10px; flex-wrap: wrap; align-items:center; }
button { border: 1px solid var(--border); background: rgba(79,140,255,.18); color: var(--text); border-radius: 10px; padding: 9px 12px; cursor:pointer; font-weight:700; }
button.secondary { background: rgba(255,255,255,.06); }
button.danger { background: rgba(255,92,122,.18); border-color: rgba(255,92,122,.4); }
button:disabled { opacity: .55; cursor:not-allowed; }
.pill { display:inline-flex; align-items:center; gap: 8px; padding: 6px 10px; border: 1px solid var(--border); border-radius: 999px; background: rgba(255,255,255,.04); }
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
.out { white-space: pre-wrap; word-break: break-word; min-height: 180px; background: rgba(0,0,0,.22); border:1px solid var(--border); border-radius: 12px; padding: 12px; font-size: 13px; line-height: 1.5; }
.muted { color: var(--muted); }
.err { color: #ffd0d8; border-color: rgba(255,92,122,.35); background: rgba(255,92,122,.08); }
.ok { border-color: rgba(79,140,255,.35); }
.small { font-size: 12px; }
.table { width: 100%; border-collapse: collapse; font-size: 12px; }
.table th, .table td { border-bottom: 1px solid rgba(35,49,82,.7); padding: 8px 6px; vertical-align: top; }
.table th { text-align: left; color: var(--muted); font-weight: 650; }
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
</style>
</head>
<body>
<div class="wrap">
<div class="header">
<div>
<div class="title">医疗行业:医学文献检索总结 Agent</div>
<div class="sub">输入关键词 → PubMed 检索 → 拉取摘要 → 百炼(兼容模式)生成总结报告(带引用)</div>
</div>
<div class="small muted">接口:<span class="mono">/api/medlit/report</span></div>
</div>
<div class="panel" style="margin-bottom: 12px;">
<div class="grid">
<div>
<label>后端 Base URL</label>
<input id="baseUrl" type="text" placeholder="http://localhost:8080" />
</div>
<div>
<label>模型(可选;留空则后端默认)</label>
<input id="model" type="text" placeholder="例如qwen-plus" />
</div>
</div>
<div class="grid" style="margin-top:10px;">
<div>
<label>关键词PubMed term</label>
<input id="keyword" type="text" placeholder="例如diabetes metformin cardiovascular outcomes" />
</div>
<div>
<label>最大返回篇数1-20</label>
<input id="maxResults" type="number" min="1" max="20" step="1" />
</div>
</div>
<div style="margin-top:10px;" class="btns">
<button id="runBtn">生成报告</button>
<button id="stopBtn" class="danger" disabled>停止</button>
<span id="status" class="muted small"></span>
</div>
</div>
<div class="grid">
<div class="panel">
<h3 style="margin:0 0 10px 0; font-size:14px;">请求体</h3>
<div id="reqBox" class="out mono muted" style="min-height:140px;"></div>
<h3 style="margin:12px 0 10px 0; font-size:14px;">检索到的文献</h3>
<div id="articlesBox" class="out" style="min-height:220px;"></div>
</div>
<div class="panel">
<h3 style="margin:0 0 10px 0; font-size:14px;">总结报告</h3>
<div id="reportBox" class="out ok"></div>
<div style="margin-top:10px;">
<h3 style="margin:0 0 10px 0; font-size:14px;">调试信息</h3>
<div id="debugBox" class="out mono muted" style="min-height:140px;"></div>
</div>
</div>
</div>
</div>
<script>
const $ = (id) => document.getElementById(id);
const state = {
aborter: null,
};
function setStatus(msg) { $('status').textContent = msg || ''; }
function setReqBox(objOrText) {
if (typeof objOrText === 'string') { $('reqBox').textContent = objOrText; return; }
$('reqBox').textContent = JSON.stringify(objOrText, null, 2);
}
function setDebug(objOrText) {
if (typeof objOrText === 'string') { $('debugBox').textContent = objOrText; return; }
$('debugBox').textContent = JSON.stringify(objOrText, null, 2);
}
function setReportBox(text, isError=false) {
const box = $('reportBox');
box.textContent = text ?? '';
box.classList.toggle('err', !!isError);
box.classList.toggle('ok', !isError);
}
function setArticles(articles) {
if (!articles || articles.length === 0) {
$('articlesBox').textContent = '未检索到文献';
return;
}
const rows = articles.map((a, i) => {
const title = a.title || '';
const journal = a.journal || '';
const year = a.pubYear || '';
const pmid = a.pmid || '';
const url = a.url || '';
const abs = a.abstractText || '';
return `
<tr>
<td class="mono">[${i+1}]</td>
<td>
<div style="font-weight:700; margin-bottom:4px;">${escapeHtml(title)}</div>
<div class="muted">${escapeHtml(journal)} ${year ? ('(' + escapeHtml(year) + ')') : ''}</div>
<div class="muted">PMID: <span class="mono">${escapeHtml(pmid)}</span> ${url ? ('- <a href="' + escapeAttr(url) + '" target="_blank">链接</a>') : ''}</div>
${abs ? ('<div style="margin-top:6px;" class="mono">' + escapeHtml(abs) + '</div>') : '<div class="muted" style="margin-top:6px;">(无摘要或摘要未获取到)</div>'}
</td>
</tr>
`;
}).join('');
$('articlesBox').innerHTML = `
<table class="table">
<thead>
<tr>
<th style="width:60px;">#</th>
<th>文献信息</th>
</tr>
</thead>
<tbody>
${rows}
</tbody>
</table>
`;
}
function baseUrl() {
const v = $('baseUrl').value.trim();
return v || 'http://localhost:8080';
}
function stopRunning() {
if (state.aborter) {
state.aborter.abort();
state.aborter = null;
}
$('stopBtn').disabled = true;
}
function buildRequest() {
const keyword = $('keyword').value.trim();
const maxResults = Number($('maxResults').value || 8);
const model = $('model').value.trim();
const body = {
keyword,
maxResults: isFinite(maxResults) ? maxResults : 8,
};
if (model) body.model = model;
return body;
}
async function run() {
stopRunning();
$('stopBtn').disabled = false;
state.aborter = new AbortController();
setReportBox('');
setDebug('');
$('articlesBox').textContent = '';
const body = buildRequest();
setReqBox(body);
if (!body.keyword) {
setReportBox('请输入关键词', true);
stopRunning();
return;
}
const url = baseUrl() + '/api/medlit/report';
setDebug({ url, method: 'POST', headers: { 'Content-Type': 'application/json' }, body });
const startedAt = Date.now();
setStatus('生成中...');
try {
const resp = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
signal: state.aborter.signal,
});
const text = await resp.text();
if (!resp.ok) {
throw new Error(`HTTP ${resp.status}: ${text}`);
}
const data = text ? JSON.parse(text) : {};
setArticles(data.articles || []);
setReportBox(data.report || '');
setStatus(`完成:${((Date.now() - startedAt)/1000).toFixed(2)}s`);
} catch (e) {
if (e.name === 'AbortError') {
setStatus('已中止');
return;
}
setReportBox(String(e?.message ?? e), true);
setStatus('失败');
} finally {
state.aborter = null;
$('stopBtn').disabled = true;
}
}
function escapeHtml(s) {
return String(s)
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;');
}
function escapeAttr(s) {
return escapeHtml(s);
}
// init
$('baseUrl').value = 'http://localhost:8080';
$('keyword').value = 'diabetes metformin cardiovascular outcomes';
$('maxResults').value = 8;
$('runBtn').addEventListener('click', run);
$('stopBtn').addEventListener('click', () => {
stopRunning();
setStatus('已停止');
});
</script>
</body>
</html>