Files
opro_demo/frontend/opro.html
leehwui 1376d60ed5 feat: implement true OPRO with Gemini-style UI
- Add true OPRO system instruction optimization (vs query rewriting)
- Implement iterative optimization with performance trajectory
- Add new OPRO API endpoints (/opro/create, /opro/generate_and_evaluate, /opro/execute)
- Create modern Gemini-style chat UI (frontend/opro.html)
- Optimize performance: reduce candidates from 20 to 10 (2x faster)
- Add model selector in UI toolbar
- Add collapsible sidebar with session management
- Add copy button for instructions
- Ensure all generated prompts use simplified Chinese
- Update README with comprehensive documentation
- Add .gitignore for local_docs folder
2025-12-06 17:24:28 +08:00

508 lines
27 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">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>OPRO - System Instruction Optimizer</title>
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://cdn.tailwindcss.com"></script>
<style>
body {
margin: 0;
font-family: 'Google Sans', 'Segoe UI', Roboto, sans-serif;
background: #f8f9fa;
}
.chat-container { height: 100vh; display: flex; }
.scrollbar-hide::-webkit-scrollbar { display: none; }
.scrollbar-hide { -ms-overflow-style: none; scrollbar-width: none; }
.sidebar-collapsed { width: 60px; }
.sidebar-expanded { width: 260px; }
.instruction-card {
transition: all 0.15s ease;
border: 1px solid #e8eaed;
}
.instruction-card:hover {
border-color: #dadce0;
box-shadow: 0 1px 3px rgba(60,64,67,0.15);
}
.loading-dots::after {
content: '...';
animation: dots 1.5s steps(4, end) infinite;
}
@keyframes dots {
0%, 20% { content: '.'; }
40% { content: '..'; }
60%, 100% { content: '...'; }
}
</style>
</head>
<body>
<div id="root"></div>
<script>
const { useState, useEffect, useRef } = React;
const API_BASE = 'http://127.0.0.1:8010';
// Main App Component
function App() {
const [sidebarOpen, setSidebarOpen] = useState(false);
const [runs, setRuns] = useState([]);
const [currentRunId, setCurrentRunId] = useState(null);
const [messages, setMessages] = useState([]);
const [inputValue, setInputValue] = useState('');
const [loading, setLoading] = useState(false);
const [models, setModels] = useState([]);
const [selectedModel, setSelectedModel] = useState('');
const chatEndRef = useRef(null);
// Load runs and models on mount
useEffect(() => {
loadRuns();
loadModels();
}, []);
async function loadModels() {
try {
const res = await fetch(`${API_BASE}/models`);
const data = await res.json();
if (data.success && data.data.models) {
setModels(data.data.models);
if (data.data.models.length > 0) {
setSelectedModel(data.data.models[0]);
}
}
} catch (err) {
console.error('Failed to load models:', err);
}
}
// Auto-scroll chat
useEffect(() => {
chatEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
async function loadRuns() {
try {
const res = await fetch(`${API_BASE}/opro/runs`);
const data = await res.json();
if (data.success) {
setRuns(data.data.runs || []);
}
} catch (err) {
console.error('Failed to load runs:', err);
}
}
async function createNewRun(taskDescription) {
setLoading(true);
try {
// Create run
const res = await fetch(`${API_BASE}/opro/create`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
task_description: taskDescription,
test_cases: [],
model_name: selectedModel || undefined
})
});
const data = await res.json();
if (!data.success) {
throw new Error(data.error || 'Failed to create run');
}
const runId = data.data.run_id;
setCurrentRunId(runId);
// Add user message
setMessages([{ role: 'user', content: taskDescription }]);
// Generate and evaluate candidates
await generateCandidates(runId);
// Reload runs list
await loadRuns();
} catch (err) {
alert('创建任务失败: ' + err.message);
} finally {
setLoading(false);
}
}
async function generateCandidates(runId) {
setLoading(true);
try {
const res = await fetch(`${API_BASE}/opro/generate_and_evaluate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
run_id: runId,
top_k: 5,
auto_evaluate: false // Use diversity-based selection
})
});
const data = await res.json();
if (!data.success) {
throw new Error(data.error || 'Failed to generate candidates');
}
// Add assistant message with candidates
setMessages(prev => [...prev, {
role: 'assistant',
type: 'candidates',
candidates: data.data.candidates,
iteration: data.data.iteration
}]);
} catch (err) {
alert('生成候选指令失败: ' + err.message);
} finally {
setLoading(false);
}
}
async function executeInstruction(instruction, userInput) {
setLoading(true);
try {
const res = await fetch(`${API_BASE}/opro/execute`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
instruction: instruction,
user_input: userInput || '请执行任务',
model_name: selectedModel || undefined
})
});
const data = await res.json();
if (!data.success) {
throw new Error(data.error || 'Failed to execute');
}
// Add execution result
setMessages(prev => [...prev, {
role: 'assistant',
type: 'execution',
instruction: instruction,
response: data.data.response
}]);
} catch (err) {
alert('执行失败: ' + err.message);
} finally {
setLoading(false);
}
}
function handleSendMessage() {
const msg = inputValue.trim();
if (!msg || loading) return;
setInputValue('');
if (!currentRunId) {
// Create new run with task description
createNewRun(msg);
} else {
// Continue optimization or execute
// For now, just show message
setMessages(prev => [...prev, { role: 'user', content: msg }]);
}
}
function handleContinueOptimize() {
if (!currentRunId || loading) return;
generateCandidates(currentRunId);
}
function handleExecute(instruction) {
if (loading) return;
const userInput = prompt('请输入要处理的内容(可选):');
executeInstruction(instruction, userInput);
}
function handleCopyInstruction(instruction) {
navigator.clipboard.writeText(instruction).then(() => {
// Could add a toast notification here
console.log('Instruction copied to clipboard');
}).catch(err => {
console.error('Failed to copy:', err);
});
}
function handleNewTask() {
setCurrentRunId(null);
setMessages([]);
setInputValue('');
}
async function loadRun(runId) {
setLoading(true);
try {
const res = await fetch(`${API_BASE}/opro/run/${runId}`);
const data = await res.json();
if (!data.success) {
throw new Error(data.error || 'Failed to load run');
}
const run = data.data;
setCurrentRunId(runId);
// Reconstruct messages from run data
const msgs = [
{ role: 'user', content: run.task_description }
];
if (run.current_candidates && run.current_candidates.length > 0) {
msgs.push({
role: 'assistant',
type: 'candidates',
candidates: run.current_candidates.map(c => ({ instruction: c, score: null })),
iteration: run.iteration
});
}
setMessages(msgs);
} catch (err) {
alert('加载任务失败: ' + err.message);
} finally {
setLoading(false);
}
}
return React.createElement('div', { className: 'chat-container' },
// Sidebar
React.createElement('div', {
className: `bg-white border-r border-gray-200 transition-all duration-300 flex flex-col ${sidebarOpen ? 'sidebar-expanded' : 'sidebar-collapsed'}`
},
// Header area - Collapse button only
React.createElement('div', { className: 'p-3 border-b border-gray-200 flex items-center justify-between' },
sidebarOpen ? React.createElement('button', {
onClick: () => setSidebarOpen(false),
className: 'p-2 text-gray-600 hover:bg-gray-100 rounded-lg transition-colors'
},
React.createElement('svg', { width: '20', height: '20', viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: '2' },
React.createElement('path', { d: 'M15 18l-6-6 6-6' })
)
) : React.createElement('button', {
onClick: () => setSidebarOpen(true),
className: 'w-full p-2 text-gray-600 hover:bg-gray-100 rounded-lg transition-colors flex items-center justify-center'
},
React.createElement('svg', { width: '20', height: '20', viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: '2' },
React.createElement('path', { d: 'M3 12h18M3 6h18M3 18h18' })
)
)
),
// Content area
React.createElement('div', { className: 'flex-1 overflow-y-auto scrollbar-hide p-2 flex flex-col' },
sidebarOpen ? React.createElement(React.Fragment, null,
// New task button (expanded)
React.createElement('button', {
onClick: handleNewTask,
className: 'mb-3 px-4 py-2.5 bg-white border border-gray-300 hover:bg-gray-50 rounded-lg transition-colors flex items-center justify-center gap-2 text-gray-700 font-medium'
},
React.createElement('span', { className: 'text-lg' }, '+'),
React.createElement('span', null, '新建会话')
),
// Sessions list
runs.length > 0 && React.createElement('div', { className: 'text-xs text-gray-500 mb-2 px-2' }, '会话列表'),
runs.map(run =>
React.createElement('div', {
key: run.run_id,
onClick: () => loadRun(run.run_id),
className: `p-3 mb-1 rounded-lg cursor-pointer transition-colors flex items-center gap-2 ${
currentRunId === run.run_id ? 'bg-gray-100' : 'hover:bg-gray-50'
}`
},
React.createElement('svg', {
width: '16',
height: '16',
viewBox: '0 0 24 24',
fill: 'none',
stroke: 'currentColor',
strokeWidth: '2',
className: 'flex-shrink-0 text-gray-500'
},
React.createElement('path', { d: 'M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z' })
),
React.createElement('div', { className: 'text-sm text-gray-800 truncate flex-1' },
run.task_description
)
)
)
) : React.createElement('button', {
onClick: handleNewTask,
className: 'p-2 text-gray-600 hover:bg-gray-100 rounded-lg transition-colors flex items-center justify-center',
title: '新建会话'
},
React.createElement('svg', { width: '24', height: '24', viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: '2' },
React.createElement('path', { d: 'M12 5v14M5 12h14' })
)
)
)
),
// Main Chat Area
React.createElement('div', { className: 'flex-1 flex flex-col bg-white' },
// Header
React.createElement('div', { className: 'px-4 py-3 border-b border-gray-200 bg-white flex items-center gap-3' },
React.createElement('h1', { className: 'text-lg font-normal text-gray-800' },
'OPRO'
)
),
// Chat Messages
React.createElement('div', { className: 'flex-1 overflow-y-auto scrollbar-hide p-6 space-y-6 max-w-4xl mx-auto w-full' },
messages.map((msg, idx) => {
if (msg.role === 'user') {
return React.createElement('div', { key: idx, className: 'flex justify-end' },
React.createElement('div', { className: 'max-w-2xl bg-gray-100 text-gray-800 rounded-2xl px-5 py-3' },
msg.content
)
);
} else if (msg.type === 'candidates') {
return React.createElement('div', { key: idx, className: 'flex justify-start' },
React.createElement('div', { className: 'w-full' },
React.createElement('div', { className: 'mb-3' },
React.createElement('div', { className: 'text-sm text-gray-600' },
`优化后的提示词(第 ${msg.iteration} 轮)`
),
),
msg.candidates.map((cand, cidx) =>
React.createElement('div', {
key: cidx,
className: 'instruction-card bg-white rounded-xl p-5 mb-3'
},
React.createElement('div', { className: 'flex items-start gap-3' },
React.createElement('div', { className: 'flex-shrink-0 w-7 h-7 bg-gray-200 text-gray-700 rounded-full flex items-center justify-center text-sm font-medium' },
cidx + 1
),
React.createElement('div', { className: 'flex-1' },
React.createElement('div', { className: 'text-gray-800 mb-4 whitespace-pre-wrap leading-relaxed' },
cand.instruction
),
cand.score !== null && React.createElement('div', { className: 'text-xs text-gray-500 mb-3' },
`评分: ${cand.score.toFixed(4)}`
),
React.createElement('div', { className: 'flex gap-2' },
React.createElement('button', {
onClick: handleContinueOptimize,
disabled: loading,
className: 'px-4 py-2 bg-white border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 disabled:bg-gray-100 disabled:text-gray-400 disabled:cursor-not-allowed transition-colors text-sm font-medium'
}, '继续优化'),
React.createElement('button', {
onClick: () => handleCopyInstruction(cand.instruction),
className: 'px-4 py-2 bg-white border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors text-sm font-medium flex items-center gap-1'
},
React.createElement('svg', { width: '16', height: '16', viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: '2' },
React.createElement('rect', { x: '9', y: '9', width: '13', height: '13', rx: '2', ry: '2' }),
React.createElement('path', { d: 'M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1' })
),
'复制'
),
React.createElement('button', {
onClick: () => handleExecute(cand.instruction),
disabled: loading,
className: 'px-4 py-2 bg-gray-900 text-white rounded-lg hover:bg-gray-800 disabled:bg-gray-300 disabled:cursor-not-allowed transition-colors text-sm font-medium'
}, '执行此指令')
)
)
)
)
)
)
);
} else if (msg.type === 'execution') {
return React.createElement('div', { key: idx, className: 'flex justify-start' },
React.createElement('div', { className: 'max-w-2xl bg-gray-50 border border-gray-200 rounded-2xl p-5' },
React.createElement('div', { className: 'text-xs text-gray-600 mb-2 font-medium' },
'执行结果'
),
React.createElement('div', { className: 'text-gray-800 whitespace-pre-wrap leading-relaxed' },
msg.response
)
)
);
}
}),
loading && React.createElement('div', { className: 'flex justify-start' },
React.createElement('div', { className: 'bg-gray-100 rounded-2xl px-5 py-3 text-gray-600' },
React.createElement('span', { className: 'loading-dots' }, '思考中')
)
),
React.createElement('div', { ref: chatEndRef })
),
// Input Area
React.createElement('div', { className: 'p-6 bg-white max-w-4xl mx-auto w-full' },
React.createElement('div', { className: 'relative' },
React.createElement('div', { className: 'bg-white border border-gray-300 rounded-3xl shadow-sm hover:shadow-md transition-shadow focus-within:shadow-md focus-within:border-gray-400' },
// Textarea
React.createElement('textarea', {
value: inputValue,
onChange: (e) => setInputValue(e.target.value),
onKeyPress: (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSendMessage();
}
},
placeholder: currentRunId ? '输入消息...' : '在此输入提示词',
disabled: loading,
rows: 3,
className: 'w-full px-5 pt-4 pb-2 bg-transparent focus:outline-none disabled:bg-transparent text-gray-800 placeholder-gray-500 resize-none'
}),
// Toolbar
React.createElement('div', { className: 'flex items-center justify-between px-4 pb-3 pt-1 border-t border-gray-100' },
// Left side - Model selector
React.createElement('div', { className: 'flex items-center gap-2' },
React.createElement('label', { className: 'text-xs text-gray-600' }, '模型:'),
React.createElement('select', {
value: selectedModel,
onChange: (e) => setSelectedModel(e.target.value),
className: 'text-sm px-2 py-1 border border-gray-300 rounded-lg bg-white text-gray-700 focus:outline-none focus:border-gray-400 cursor-pointer'
},
models.map(model =>
React.createElement('option', { key: model, value: model }, model)
)
)
),
// Right side - Send button
React.createElement('button', {
onClick: handleSendMessage,
disabled: loading || !inputValue.trim(),
className: 'p-2.5 bg-gray-100 text-gray-700 rounded-full hover:bg-gray-200 disabled:bg-gray-50 disabled:text-gray-300 disabled:cursor-not-allowed transition-colors flex items-center justify-center'
},
React.createElement('svg', {
width: '20',
height: '20',
viewBox: '0 0 24 24',
fill: 'currentColor'
},
React.createElement('path', { d: 'M2.01 21L23 12 2.01 3 2 10l15 2-15 2z' })
)
)
)
),
!currentRunId && React.createElement('div', { className: 'text-xs text-gray-500 mt-3 px-4' },
'输入任务描述后AI 将为你生成优化的系统指令'
)
)
)
)
);
}
// Render App
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(React.createElement(App));
</script>
</body>
</html>