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