165 lines
7.2 KiB
HTML
165 lines
7.2 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 React 界面</title>
|
|
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
|
|
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
|
|
<script crossorigin src="https://unpkg.com/immer@10.0.3/dist/immer.umd.production.min.js"></script>
|
|
<script crossorigin src="https://unpkg.com/zustand@4.5.2/umd/zustand.min.js"></script>
|
|
<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 id="root"></div>
|
|
<script>
|
|
const { useState, useEffect } = React;
|
|
const createZustand = zustand.default;
|
|
|
|
const useStore = createZustand((set, get) => ({
|
|
sessions: [],
|
|
currentSessionId: null,
|
|
detail: null,
|
|
candidatesPage: 0,
|
|
setSessions: (data) => set({ sessions: data }),
|
|
setCurrentSession: (sid) => set({ currentSessionId: sid }),
|
|
setDetail: (d) => set({ detail: d, candidatesPage: 0 }),
|
|
nextCandidatesPage: () => set({ candidatesPage: get().candidatesPage + 1 }),
|
|
prevCandidatesPage: () => set({ candidatesPage: Math.max(0, get().candidatesPage - 1) }),
|
|
}));
|
|
|
|
async function api(path, method = 'GET', body) {
|
|
const opts = { method, headers: { 'Content-Type': 'application/json' } };
|
|
if (body) opts.body = JSON.stringify(body);
|
|
const r = await fetch(path, opts);
|
|
if (!r.ok) throw new Error('HTTP ' + r.status);
|
|
return await r.json();
|
|
}
|
|
|
|
function SidebarSessionList() {
|
|
const sessions = useStore(s => s.sessions);
|
|
const setCurrentSession = useStore(s => s.setCurrentSession);
|
|
const setDetail = useStore(s => s.setDetail);
|
|
const [error, setError] = useState('');
|
|
|
|
async function loadSessions() { try {
|
|
const data = await api('/sessions');
|
|
useStore.getState().setSessions(data.sessions);
|
|
} catch(e) { setError(e.message); } }
|
|
|
|
async function openSession(sid) { try {
|
|
const data = await api('/session/' + sid);
|
|
setCurrentSession(sid);
|
|
setDetail(data);
|
|
} catch(e) { setError(e.message); } }
|
|
|
|
useEffect(() => { loadSessions(); }, []);
|
|
return React.createElement('div', { className: 'sidebar' },
|
|
React.createElement('div', { className: 'topbar' },
|
|
React.createElement('button', { onClick: loadSessions }, '刷新'),
|
|
),
|
|
error && React.createElement('div', null, error),
|
|
sessions.map(s => React.createElement('div', { key: s.session_id, className: 'session-item', onClick: () => openSession(s.session_id) },
|
|
React.createElement('div', null, s.original_query || '(空)'),
|
|
React.createElement('div', { className: 'chat-role' }, `轮次 ${s.round}`)
|
|
))
|
|
);
|
|
}
|
|
|
|
function ChatHistoryPanel() {
|
|
const detail = useStore(s => s.detail);
|
|
const [msg, setMsg] = useState('');
|
|
const [error, setError] = useState('');
|
|
async function send() {
|
|
try {
|
|
const sid = useStore.getState().currentSessionId;
|
|
const data = await api('/message', 'POST', { session_id: sid, message: msg });
|
|
setMsg('');
|
|
const d = await api('/session/' + sid);
|
|
useStore.getState().setDetail(d);
|
|
} catch(e) { setError(e.message); }
|
|
}
|
|
if (!detail) return React.createElement('div', { className: 'center' }, '请选择左侧会话');
|
|
return React.createElement('div', { className: 'center' },
|
|
(detail.history || []).map((h, i) => React.createElement('div', { key: i, className: 'chat-msg' },
|
|
React.createElement('div', { className: 'chat-role' }, h.role),
|
|
React.createElement('div', null, h.content)
|
|
)),
|
|
React.createElement('div', { className: 'topbar' },
|
|
React.createElement('textarea', { value: msg, onChange: e => setMsg(e.target.value), placeholder: '继续提问...' }),
|
|
React.createElement('button', { onClick: send }, '发送')
|
|
),
|
|
error && React.createElement('div', null, error)
|
|
);
|
|
}
|
|
|
|
function CandidateSelectorPanel() {
|
|
const detail = useStore(s => s.detail);
|
|
const page = useStore(s => s.candidatesPage);
|
|
const nextPage = useStore(s => s.nextCandidatesPage);
|
|
const prevPage = useStore(s => s.prevCandidatesPage);
|
|
const [error, setError] = useState('');
|
|
async function selectCand(c) {
|
|
try {
|
|
const sid = useStore.getState().currentSessionId;
|
|
await api('/select', 'POST', { session_id: sid, choice: c });
|
|
const d = await api('/session/' + sid);
|
|
useStore.getState().setDetail(d);
|
|
} catch(e) { setError(e.message); }
|
|
}
|
|
async function rejectCand(c) {
|
|
try {
|
|
const sid = useStore.getState().currentSessionId;
|
|
await api('/reject', 'POST', { session_id: sid, candidate: c });
|
|
const d = await api('/session/' + sid);
|
|
useStore.getState().setDetail(d);
|
|
} catch(e) { setError(e.message); }
|
|
}
|
|
if (!detail) return React.createElement('div', { className: 'right' }, '无会话');
|
|
const cands = detail.candidates || [];
|
|
const pageSize = 5;
|
|
const start = page * pageSize;
|
|
const group = cands.slice(start, start + pageSize);
|
|
return React.createElement('div', { className: 'right' },
|
|
React.createElement('div', { className: 'topbar' },
|
|
React.createElement('button', { onClick: prevPage }, '上一组'),
|
|
React.createElement('button', { onClick: nextPage }, '下一组')
|
|
),
|
|
group.map((c, i) => React.createElement('div', { key: start + i, className: 'cand' },
|
|
React.createElement('div', null, c),
|
|
React.createElement('div', { className: 'cand-actions' },
|
|
React.createElement('button', { onClick: () => selectCand(c) }, '✅ 选择'),
|
|
React.createElement('button', { onClick: () => rejectCand(c) }, '❌ 拒绝')
|
|
)
|
|
)),
|
|
error && React.createElement('div', null, error)
|
|
);
|
|
}
|
|
|
|
function App() {
|
|
return React.createElement('div', { className: 'layout' },
|
|
React.createElement(SidebarSessionList),
|
|
React.createElement(ChatHistoryPanel),
|
|
React.createElement(CandidateSelectorPanel),
|
|
);
|
|
}
|
|
|
|
ReactDOM.createRoot(document.getElementById('root')).render(React.createElement(App));
|
|
</script>
|
|
</body>
|
|
</html>
|