原始代码
This commit is contained in:
446
frontend/index.html
Normal file
446
frontend/index.html
Normal 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
164
frontend/react-app.html
Normal 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
192
frontend/react/index.html
Normal 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
157
frontend/ui_offline.html
Normal 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>
|
||||
Reference in New Issue
Block a user