jiaoxue/medical/medlit-agent.html
2026-02-28 15:05:39 +08:00

274 lines
10 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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>