158 lines
6.8 KiB
HTML
158 lines
6.8 KiB
HTML
|
|
<!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>
|