Files
opro_demo/frontend/index.html

447 lines
20 KiB
HTML
Raw Normal View History

2025-12-05 07:11:25 +00:00
<!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, message: msg })
2025-12-05 07:11:25 +00:00
});
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>