274 lines
10 KiB
HTML
274 lines
10 KiB
HTML
<!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('&', '&')
|
||
.replaceAll('<', '<')
|
||
.replaceAll('>', '>')
|
||
.replaceAll('"', '"')
|
||
.replaceAll("'", ''');
|
||
}
|
||
|
||
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>
|