Files
opro_demo/frontend/index.html
2025-12-05 07:11:25 +00:00

447 lines
20 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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>