Files
opro_demo/frontend/ui_offline.html

158 lines
6.8 KiB
HTML
Raw Normal View History

2025-12-05 07:11:25 +00:00
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>OPRO 三栏界面(离线版)</title>
<style>
body { margin: 0; font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif; }
.layout { display: grid; grid-template-columns: 260px 1fr 360px; height: 100vh; }
.sidebar { border-right: 1px solid #eee; padding: 12px; overflow: auto; }
.center { border-right: 1px solid #eee; padding: 12px; overflow: auto; }
.right { padding: 12px; overflow: auto; }
.session-item { padding: 8px; border-radius: 6px; cursor: pointer; }
.session-item:hover { background: #f5f5f5; }
.chat-msg { margin: 8px 0; }
.chat-role { font-size: 12px; color: #888; }
.cand { border: 1px solid #ddd; border-radius: 8px; padding: 8px; margin-bottom: 8px; }
.cand-actions { display: flex; gap: 8px; margin-top: 8px; }
.topbar { display: flex; gap: 8px; padding: 8px; border-bottom: 1px solid #eee; }
textarea { width: 100%; min-height: 80px; }
</style>
</head>
<body>
<div class="layout">
<div class="sidebar">
<div class="topbar">
<button id="btnRefresh">刷新会话</button>
<button id="btnNew">新建会话</button>
</div>
<div id="sessions"></div>
</div>
<div class="center">
<div id="history"></div>
<div class="topbar">
<textarea id="msg" placeholder="继续提问..."></textarea>
<button id="btnSend">发送</button>
</div>
</div>
<div class="right">
<div class="topbar">
<button id="btnPrev">上一组</button>
<button id="btnNext">下一组</button>
</div>
<div id="cands"></div>
</div>
</div>
<script>
let currentSid = null;
let page = 0;
const pageSize = 5;
let modelList = [];
let currentModel = '';
async function get(url) {
const r = await fetch(url);
if (!r.ok) throw new Error('HTTP ' + r.status);
return await r.json();
}
async function post(url, body) {
const r = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
if (!r.ok) throw new Error('HTTP ' + r.status);
return await r.json();
}
async function loadSessions() {
const data = await get('/sessions');
const el = document.getElementById('sessions');
el.innerHTML = '';
data.sessions.forEach(s => {
const item = document.createElement('div');
item.className = 'session-item';
item.textContent = (s.original_query || '(空)') + ' / 轮次 ' + s.round;
item.onclick = () => openSession(s.session_id);
el.appendChild(item);
});
}
async function openSession(sid) {
currentSid = sid; page = 0;
const d = await get('/session/' + sid);
renderDetail(d);
}
function renderDetail(d) {
// history
const h = document.getElementById('history');
h.innerHTML = '';
(d.history || []).forEach(m => {
const row = document.createElement('div');
row.className = 'chat-msg';
const role = document.createElement('div'); role.className = 'chat-role'; role.textContent = m.role;
const content = document.createElement('div'); content.textContent = m.content;
row.appendChild(role); row.appendChild(content); h.appendChild(row);
});
// candidates
const elC = document.getElementById('cands'); elC.innerHTML = '';
const cands = d.candidates || [];
const start = page * pageSize; const group = cands.slice(start, start + pageSize);
group.forEach(c => {
const box = document.createElement('div'); box.className = 'cand';
const text = document.createElement('div'); text.textContent = c;
const actions = document.createElement('div'); actions.className = 'cand-actions';
const ok = document.createElement('button'); ok.textContent = '✅ 选择'; ok.onclick = () => selectCand(c);
const no = document.createElement('button'); no.textContent = '❌ 拒绝'; no.onclick = () => rejectCand(c);
actions.appendChild(ok); actions.appendChild(no);
box.appendChild(text); box.appendChild(actions);
elC.appendChild(box);
});
}
async function newSession() {
const q = prompt('输入问题'); if (!q) return;
const r = await post('/query', { query: q });
currentSid = r.session_id; page = 0;
if (currentModel) { try { await post('/set_model', { session_id: currentSid, model_name: currentModel }); } catch(e){} }
const d = await get('/session/' + currentSid);
renderDetail(d); loadSessions();
}
async function sendMsg() {
if (!currentSid) return alert('请先选择会话');
const msg = document.getElementById('msg').value.trim(); if (!msg) return;
await post('/message', { session_id: currentSid, message: msg });
await post('/query_from_message', { session_id: currentSid });
const d = await get('/session/' + currentSid); renderDetail(d);
}
async function selectCand(c) {
await post('/select', { session_id: currentSid, choice: c });
const d = await get('/session/' + currentSid); renderDetail(d);
}
async function rejectCand(c) {
await post('/reject', { session_id: currentSid, candidate: c });
const d = await get('/session/' + currentSid); renderDetail(d);
}
function prev() { if (page > 0) { page--; refreshDetail(); } }
function next() { page++; refreshDetail(); }
async function refreshDetail() { if (!currentSid) return; const d = await get('/session/' + currentSid); renderDetail(d); }
document.getElementById('btnRefresh').onclick = loadSessions;
document.getElementById('btnNew').onclick = newSession;
document.getElementById('btnSend').onclick = sendMsg;
document.getElementById('btnPrev').onclick = prev;
document.getElementById('btnNext').onclick = next;
async function loadModels() {
try {
const data = await get('/models');
modelList = data.models || [];
const sel = document.createElement('select'); sel.id = 'modelSel';
sel.onchange = () => { currentModel = sel.value; if (currentSid) post('/set_model', { session_id: currentSid, model_name: currentModel }).catch(()=>{}); };
sel.style.marginTop = '8px'; sel.style.width = '100%';
for (const m of modelList) { const opt = document.createElement('option'); opt.value = m; opt.textContent = m; sel.appendChild(opt); }
if (!currentModel && modelList.length > 0) { currentModel = modelList[0]; sel.value = currentModel; }
const sidebar = document.querySelector('.sidebar'); sidebar.appendChild(sel);
} catch(e) {}
}
loadSessions();
loadModels();
</script>
</body>
</html>