原始代码

This commit is contained in:
xxm
2025-12-05 07:11:25 +00:00
parent 045e777a11
commit dd5339de32
46 changed files with 5848 additions and 0 deletions

446
frontend/index.html Normal file
View File

@@ -0,0 +1,446 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OPRO Prompt Optimizer</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
body { margin: 0; overflow: hidden; }
.chat-container { height: 100vh; display: flex; }
.scrollbar-hide::-webkit-scrollbar { display: none; }
.scrollbar-hide { -ms-overflow-style: none; scrollbar-width: none; }
</style>
</head>
<body>
<div class="chat-container">
<div class="w-64 bg-white border-r border-gray-200 flex flex-col">
<div class="p-4 border-b border-gray-200">
<button onclick="createNewSession()" class="w-full flex items-center justify-center gap-2 px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors">
<span>+</span> 新建会话
</button>
</div>
<div id="sessionList" class="flex-1 overflow-y-auto scrollbar-hide p-2"></div>
<div class="p-4 border-t border-gray-200">
<div class="text-sm font-semibold text-gray-700 mb-1">选择模型</div>
<div class="flex gap-2">
<select id="modelSelector" class="flex-1 px-2 py-1 border border-gray-300 rounded"></select>
<button onclick="applyModelToSession()" class="px-3 py-1 bg-indigo-500 text-white rounded hover:bg-indigo-600 text-sm">应用</button>
</div>
</div>
</div>
<div class="flex-1 flex flex-col bg-white">
<div class="p-4 border-b border-gray-200 bg-gradient-to-r from-blue-500 to-purple-500">
<h1 class="text-xl font-bold text-white">OPRO Prompt Optimizer</h1>
</div>
<div id="chatArea" class="flex-1 overflow-y-auto scrollbar-hide p-4 space-y-4"></div>
<div class="p-4 border-t border-gray-200">
<div class="flex gap-2">
<input type="text" id="messageInput" placeholder="输入消息..."
class="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
onkeypress="handleKeyPress(event)">
<button onclick="sendMessage()" class="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors">
发送
</button>
</div>
</div>
</div>
<div class="w-80 bg-white border-l border-gray-200 flex flex-col">
<div class="p-4 border-b border-gray-200 bg-purple-50">
<h2 class="font-bold text-lg">会话信息</h2>
</div>
<div class="flex-1 overflow-y-auto scrollbar-hide p-4">
<div class="space-y-4">
<div>
<div class="text-sm font-semibold text-gray-700 mb-1">当前轮次</div>
<div id="roundInfo" class="text-2xl font-bold text-purple-600">Round 0</div>
</div>
<div>
<div class="text-sm font-semibold text-gray-700 mb-1">已选提示词</div>
<div id="selectedPromptInfo" class="text-sm text-gray-500 italic">暂未选择</div>
</div>
<div>
<div class="text-sm font-semibold text-gray-700 mb-2">操作提示</div>
<div class="text-xs text-gray-600 space-y-1">
<div>• 输入问题后会生成候选提示词</div>
<div>• 点击"选择"使用该提示词</div>
<div>• 点击"拒绝"生成新候选</div>
<div>• 点击"继续优化"获取更多选项</div>
</div>
</div>
</div>
</div>
<div id="optimizeBtn" class="p-4 border-t border-gray-200" style="display:none;">
<button onclick="continueOptimization()" class="w-full flex items-center justify-center gap-2 px-4 py-2 bg-purple-500 text-white rounded-lg hover:bg-purple-600 transition-colors">
🔄 继续优化
</button>
</div>
</div>
</div>
<div id="loadingMask" class="fixed inset-0 bg-black bg-opacity-30 flex items-center justify-center z-50" style="display:none;">
<div class="bg-white rounded-lg p-6 shadow-xl">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto"></div>
<div class="mt-4 text-gray-700">处理中...</div>
</div>
</div>
<script>
let currentSession = null;
let currentRound = 0;
let candidates = [];
let selectedPrompt = null;
let modelList = [];
let currentModel = '';
const API_BASE = '';
function handleKeyPress(event) {
if (event.key === 'Enter') {
sendMessage();
}
}
function showLoading(show) {
document.getElementById('loadingMask').style.display = show ? 'flex' : 'none';
}
function updateSessionInfo() {
document.getElementById('roundInfo').textContent = 'Round ' + currentRound;
const selectedInfo = document.getElementById('selectedPromptInfo');
if (selectedPrompt) {
selectedInfo.textContent = selectedPrompt.length > 100 ? selectedPrompt.substring(0, 100) + '...' : selectedPrompt;
selectedInfo.className = 'text-sm text-gray-700 bg-green-50 p-2 rounded border border-green-200';
} else {
selectedInfo.textContent = '暂未选择';
selectedInfo.className = 'text-sm text-gray-500 italic';
}
}
async function loadSessions() {
try {
const res = await fetch(API_BASE + '/sessions');
const data = await res.json();
const payload = data && data.data ? data.data : {};
const sessionList = document.getElementById('sessionList');
sessionList.innerHTML = '';
const sessions = payload.sessions || [];
for (let i = 0; i < sessions.length; i++) {
const session = sessions[i];
const div = document.createElement('div');
const isActive = currentSession === session.session_id;
div.className = 'p-3 mb-2 rounded-lg cursor-pointer transition-colors ' +
(isActive ? 'bg-blue-50 border-2 border-blue-300' : 'bg-gray-50 hover:bg-gray-100 border-2 border-transparent');
div.onclick = function() { loadSession(session.session_id); };
div.innerHTML = '<div class="font-medium text-sm truncate">' +
(session.original_query || '未命名会话') + '</div>' +
'<div class="text-xs text-gray-500 mt-1">Round ' + session.round + '</div>';
sessionList.appendChild(div);
}
} catch (err) {
console.error('加载会话失败:', err);
}
}
async function loadModels() {
try {
const res = await fetch(API_BASE + '/models');
const data = await res.json();
const payload = data && data.data ? data.data : {};
modelList = payload.models || [];
const sel = document.getElementById('modelSelector');
if (sel) {
sel.innerHTML = '';
for (let i = 0; i < modelList.length; i++) {
const opt = document.createElement('option');
opt.value = modelList[i];
opt.textContent = modelList[i];
sel.appendChild(opt);
}
if (!currentModel && modelList.length > 0) {
currentModel = modelList[0];
sel.value = currentModel;
}
}
} catch (err) { /* ignore */ }
}
async function applyModelToSession() {
try {
const sel = document.getElementById('modelSelector');
currentModel = sel ? sel.value : currentModel;
if (!currentSession || !currentModel) return;
await fetch(API_BASE + '/set_model', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ session_id: currentSession, model_name: currentModel })
});
} catch (err) {
alert('应用模型失败: ' + err.message);
}
}
async function createNewSession() {
currentSession = null;
currentRound = 0;
candidates = [];
selectedPrompt = null;
document.getElementById('chatArea').innerHTML = '';
document.getElementById('optimizeBtn').style.display = 'none';
updateSessionInfo();
document.getElementById('messageInput').focus();
}
async function loadSession(sessionId) {
showLoading(true);
try {
const res = await fetch(API_BASE + '/session/' + sessionId);
const data = await res.json();
const payload = data && data.data ? data.data : {};
currentSession = sessionId;
currentRound = payload.round;
candidates = payload.candidates || [];
selectedPrompt = payload.selected_prompt;
document.getElementById('chatArea').innerHTML = '';
appendMessage('user', payload.original_query);
if (payload.candidates && payload.candidates.length > 0) {
appendCandidateMessage('为你生成以下候选提示词,请选择:', payload.candidates);
}
if (payload.history) {
for (let i = 0; i < payload.history.length; i++) {
const msg = payload.history[i];
appendMessage(msg.role, msg.content);
}
}
updateSessionInfo();
document.getElementById('optimizeBtn').style.display = candidates.length > 0 ? 'block' : 'none';
await loadSessions();
await loadModels();
} catch (err) {
alert('加载会话失败: ' + err.message);
} finally {
showLoading(false);
}
}
async function sendMessage() {
const input = document.getElementById('messageInput');
const msg = input.value.trim();
if (!msg) return;
input.value = '';
showLoading(true);
try {
if (!currentSession) {
const res = await fetch(API_BASE + '/query', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: msg })
});
const data = await res.json();
const payload = data && data.data ? data.data : {};
currentSession = payload.session_id;
currentRound = payload.round;
candidates = payload.candidates || [];
if (currentModel) {
await fetch(API_BASE + '/set_model', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ session_id: currentSession, model_name: currentModel })
});
}
appendMessage('user', msg);
appendCandidateMessage('为你生成以下候选提示词,请选择:', payload.candidates);
updateSessionInfo();
document.getElementById('optimizeBtn').style.display = 'block';
await loadSessions();
} else {
appendMessage('user', msg);
const candRes = await fetch(API_BASE + '/query_from_message', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ session_id: currentSession })
});
const candData = await candRes.json();
const payload = candData && candData.data ? candData.data : {};
candidates = payload.candidates || [];
currentRound = payload.round;
appendCandidateMessage('为你生成以下候选提示词,请选择:', payload.candidates);
updateSessionInfo();
document.getElementById('optimizeBtn').style.display = 'block';
}
} catch (err) {
alert('发送失败: ' + err.message);
} finally {
showLoading(false);
}
}
function appendMessage(role, content) {
const chatArea = document.getElementById('chatArea');
const div = document.createElement('div');
if (role === 'user') {
div.className = 'flex justify-end';
div.innerHTML = '<div class="max-w-2xl bg-blue-500 text-white rounded-lg px-4 py-3">' +
'<div class="font-semibold mb-1 text-sm">👤 用户</div>' +
'<div>' + escapeHtml(content) + '</div>' +
'</div>';
} else if (role === 'assistant') {
div.className = 'flex justify-start';
div.innerHTML = '<div class="max-w-3xl bg-green-50 rounded-lg px-4 py-3 border border-green-200">' +
'<div class="font-semibold mb-2 text-sm text-green-700">🤖 AI 回答</div>' +
'<div class="whitespace-pre-wrap">' + escapeHtml(content) + '</div>' +
'</div>';
}
chatArea.appendChild(div);
chatArea.scrollTop = chatArea.scrollHeight;
}
function appendCandidateMessage(content, candidateList) {
const chatArea = document.getElementById('chatArea');
const div = document.createElement('div');
div.className = 'flex justify-start';
let candidatesHtml = '';
if (candidateList && candidateList.length > 0) {
candidatesHtml = '<div class="space-y-3 mt-3">';
for (let i = 0; i < candidateList.length; i++) {
const cand = candidateList[i];
candidatesHtml += '<div class="bg-white border-2 border-gray-200 rounded-lg p-3 hover:border-blue-300 transition-colors">' +
'<div class="flex items-start gap-3">' +
'<div class="flex-shrink-0 w-8 h-8 bg-purple-100 rounded-full flex items-center justify-center text-purple-600 font-bold text-sm">' + (i + 1) + '</div>' +
'<div class="flex-1">' +
'<div class="text-sm text-gray-700 mb-3">' + escapeHtml(cand) + '</div>' +
'<div class="flex gap-2">' +
'<button onclick="selectCandidate(' + i + ')" class="flex-1 px-3 py-1.5 bg-green-500 text-white rounded hover:bg-green-600 text-sm font-medium transition-colors">✓ 选择</button>' +
'<button onclick="rejectCandidate(' + i + ')" class="flex-1 px-3 py-1.5 bg-red-500 text-white rounded hover:bg-red-600 text-sm font-medium transition-colors">✗ 拒绝</button>' +
'</div>' +
'</div>' +
'</div>' +
'</div>';
}
candidatesHtml += '</div>';
}
div.innerHTML = '<div class="max-w-4xl bg-gradient-to-r from-purple-50 to-blue-50 rounded-lg px-4 py-3 border-2 border-purple-200">' +
'<div class="font-semibold mb-2 text-purple-700">🤖 系统推荐</div>' +
'<div class="mb-2 text-sm text-gray-600">' + escapeHtml(content) + '</div>' +
candidatesHtml +
'</div>';
chatArea.appendChild(div);
chatArea.scrollTop = chatArea.scrollHeight;
}
async function selectCandidate(idx) {
if (!currentSession) return;
const candidate = candidates[idx];
showLoading(true);
try {
const res = await fetch(API_BASE + '/select', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ session_id: currentSession, choice: candidate })
});
const data = await res.json();
const payload = data && data.data ? data.data : {};
selectedPrompt = candidate;
appendMessage('user', '✅ 确认选择 Prompt ' + (idx + 1));
appendMessage('assistant', payload.answer);
updateSessionInfo();
} catch (err) {
alert('选择失败: ' + err.message);
} finally {
showLoading(false);
}
}
async function rejectCandidate(idx) {
if (!currentSession) return;
const candidate = candidates[idx];
const reason = prompt('请说明拒绝理由(可选):');
showLoading(true);
try {
const res = await fetch(API_BASE + '/reject', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
session_id: currentSession,
candidate: candidate,
reason: reason
})
});
const data = await res.json();
const payload = data && data.data ? data.data : {};
candidates = payload.candidates || [];
currentRound = payload.round;
appendMessage('user', '❌ 拒绝 Prompt ' + (idx + 1) + (reason ? '' + reason : ''));
appendCandidateMessage('已生成新的候选提示词:', payload.candidates);
updateSessionInfo();
} catch (err) {
alert('拒绝失败: ' + err.message);
} finally {
showLoading(false);
}
}
async function continueOptimization() {
if (!currentSession) return;
showLoading(true);
try {
const res = await fetch(API_BASE + '/query_from_message', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ session_id: currentSession })
});
const data = await res.json();
const payload = data && data.data ? data.data : {};
candidates = payload.candidates || [];
currentRound = payload.round;
appendMessage('user', '🔄 继续优化提示词');
appendCandidateMessage('新一轮候选提示词:', payload.candidates);
updateSessionInfo();
} catch (err) {
alert('优化失败: ' + err.message);
} finally {
showLoading(false);
}
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
window.onload = function() {
loadSessions();
loadModels();
};
</script>
</body>
</html>

164
frontend/react-app.html Normal file
View File

@@ -0,0 +1,164 @@
<!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>

192
frontend/react/index.html Normal file
View File

@@ -0,0 +1,192 @@
<!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,
models: [],
currentModel: '',
candidatesPage: 0,
setSessions: (data) => set({ sessions: data }),
setCurrentSession: (sid) => set({ currentSessionId: sid }),
setDetail: (d) => set({ detail: d, candidatesPage: 0 }),
setModels: (models) => set({ models }),
setCurrentModel: (m) => set({ currentModel: m }),
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 models = useStore(s => s.models);
const currentModel = useStore(s => s.currentModel);
const setModels = useStore(s => s.setModels);
const setCurrentModel = useStore(s => s.setCurrentModel);
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 loadModels() { try {
const data = await api('/models');
setModels(data.models || []);
if (!useStore.getState().currentModel && data.models && data.models.length > 0) {
setCurrentModel(data.models[0]);
}
} catch(e) { /* ignore */ } }
async function applyModel() { try {
const sid = useStore.getState().currentSessionId;
if (!sid || !currentModel) return;
await api('/set_model', 'POST', { session_id: sid, model_name: currentModel });
} 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(); loadModels(); }, []);
return React.createElement('div', { className: 'sidebar' },
React.createElement('div', { className: 'topbar' },
React.createElement('button', { onClick: loadSessions }, '刷新'),
React.createElement('select', { value: currentModel, onChange: e => setCurrentModel(e.target.value) },
(models || []).map(m => React.createElement('option', { key: m, value: m }, m))
),
React.createElement('button', { onClick: applyModel }, '应用模型'),
),
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('');
await api('/query_from_message', 'POST', { session_id: sid });
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>

157
frontend/ui_offline.html Normal file
View File

@@ -0,0 +1,157 @@
<!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>