Files
opro_demo/frontend/opro.html
leehwui 602875b08c refactor: remove execute instruction button to simplify UX
- Removed '执行此指令' button from candidate cards
- Prevents confusion between execution interactions and new task input
- Cleaner workflow: input box for new tasks, 继续优化 for iteration, 复制 for copying
- Each candidate now only has two actions: continue optimizing or copy
2025-12-06 22:41:05 +08:00

651 lines
33 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 [sessions, setSessions] = useState([]);
const [currentSessionId, setCurrentSessionId] = useState(null);
const [currentSessionRuns, setCurrentSessionRuns] = useState([]);
const [currentRunId, setCurrentRunId] = useState(null);
const [messages, setMessages] = useState([]);
const [sessionMessages, setSessionMessages] = useState({}); // Store messages per session
const [sessionLastRunId, setSessionLastRunId] = useState({}); // Store last run ID per session
const [inputValue, setInputValue] = useState('');
const [loading, setLoading] = useState(false);
const [models, setModels] = useState([]);
const [selectedModel, setSelectedModel] = useState('');
const chatEndRef = useRef(null);
// Load sessions and models on mount
useEffect(() => {
loadSessions();
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 loadSessions() {
try {
const res = await fetch(`${API_BASE}/opro/sessions`);
const data = await res.json();
if (data.success) {
setSessions(data.data.sessions || []);
}
} catch (err) {
console.error('Failed to load sessions:', err);
}
}
async function loadSessionRuns(sessionId) {
try {
const res = await fetch(`${API_BASE}/opro/session/${sessionId}`);
const data = await res.json();
if (data.success) {
setCurrentSessionRuns(data.data.runs || []);
}
} catch (err) {
console.error('Failed to load session runs:', err);
}
}
async function createNewSession() {
try {
const res = await fetch(`${API_BASE}/opro/session/create`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
});
const data = await res.json();
if (!data.success) {
throw new Error(data.error || 'Failed to create session');
}
const sessionId = data.data.session_id;
setCurrentSessionId(sessionId);
setCurrentSessionRuns([]);
setCurrentRunId(null);
setMessages([]);
setSessionMessages(prev => ({ ...prev, [sessionId]: [] })); // Initialize empty messages for new session
// Reload sessions list
await loadSessions();
return sessionId;
} catch (err) {
alert('创建会话失败: ' + err.message);
return null;
}
}
async function createNewRun(taskDescription) {
setLoading(true);
try {
// Ensure we have a session
let sessionId = currentSessionId;
if (!sessionId) {
sessionId = await createNewSession();
if (!sessionId) return;
}
// Create run within session
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,
session_id: sessionId
})
});
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);
// Save this as the last run for this session
setSessionLastRunId(prev => ({
...prev,
[sessionId]: runId
}));
// Add user message to existing messages (keep chat history)
const newUserMessage = { role: 'user', content: taskDescription };
setMessages(prev => {
const updated = [...prev, newUserMessage];
// Save to session messages
setSessionMessages(prevSessions => ({
...prevSessions,
[sessionId]: updated
}));
return updated;
});
// Generate and evaluate candidates
await generateCandidates(runId);
// Reload sessions and session runs
await loadSessions();
await loadSessionRuns(sessionId);
} catch (err) {
alert('创建任务失败: ' + err.message);
console.error('Error creating run:', err);
} finally {
setLoading(false);
}
}
async function generateCandidates(runId) {
setLoading(true);
try {
console.log('Generating candidates for run:', runId);
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();
console.log('Generate candidates response:', data);
if (!data.success) {
throw new Error(data.error || 'Failed to generate candidates');
}
// Add assistant message with candidates
const newAssistantMessage = {
role: 'assistant',
type: 'candidates',
candidates: data.data.candidates,
iteration: data.data.iteration
};
setMessages(prev => {
const updated = [...prev, newAssistantMessage];
// Save to session messages
if (currentSessionId) {
setSessionMessages(prevSessions => ({
...prevSessions,
[currentSessionId]: updated
}));
}
return updated;
});
} catch (err) {
alert('生成候选指令失败: ' + err.message);
console.error('Error generating candidates:', err);
} 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
const newExecutionMessage = {
role: 'assistant',
type: 'execution',
instruction: instruction,
response: data.data.response
};
setMessages(prev => {
const updated = [...prev, newExecutionMessage];
// Save to session messages
if (currentSessionId) {
setSessionMessages(prevSessions => ({
...prevSessions,
[currentSessionId]: updated
}));
}
return updated;
});
} catch (err) {
alert('执行失败: ' + err.message);
} finally {
setLoading(false);
}
}
function handleSendMessage() {
const msg = inputValue.trim();
if (!msg || loading) return;
setInputValue('');
// Always create a new run with the message as task description
createNewRun(msg);
}
async function handleContinueOptimize(selectedInstruction, selectedScore) {
if (!currentRunId || loading) return;
// First, evaluate the selected instruction to add it to trajectory
if (selectedInstruction) {
setLoading(true);
try {
// Add the selected instruction to trajectory
const res = await fetch(`${API_BASE}/opro/evaluate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
run_id: currentRunId,
instruction: selectedInstruction
})
});
const data = await res.json();
if (!data.success) {
throw new Error(data.error || 'Failed to evaluate instruction');
}
console.log('Evaluated instruction, score:', data.data.score);
} catch (err) {
alert('评估指令失败: ' + err.message);
console.error('Error evaluating instruction:', err);
setLoading(false);
return;
} finally {
setLoading(false);
}
}
// Then generate new candidates based on updated trajectory
await generateCandidates(currentRunId);
}
function handleExecute(instruction) {
if (loading) return;
executeInstruction(instruction, '');
}
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() {
// Create new run within current session
setCurrentRunId(null);
setMessages([]);
setInputValue('');
}
async function handleNewSession() {
// Create completely new session
const sessionId = await createNewSession();
if (sessionId) {
setCurrentSessionId(sessionId);
setCurrentSessionRuns([]);
setCurrentRunId(null);
setMessages([]);
setInputValue('');
}
}
async function handleSelectSession(sessionId) {
setCurrentSessionId(sessionId);
// Restore the last run ID for this session
setCurrentRunId(sessionLastRunId[sessionId] || null);
// Load messages from session storage
setMessages(sessionMessages[sessionId] || []);
await loadSessionRuns(sessionId);
}
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 session button (expanded)
React.createElement('button', {
onClick: handleNewSession,
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
sessions.length > 0 && React.createElement('div', { className: 'text-xs text-gray-500 mb-2 px-2' }, '会话列表'),
sessions.map(session =>
React.createElement('div', {
key: session.session_id,
onClick: () => handleSelectSession(session.session_id),
className: `p-3 mb-1 rounded-lg cursor-pointer transition-colors flex items-center gap-2 ${
currentSessionId === session.session_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' },
session.session_name
)
)
)
) : React.createElement('button', {
onClick: handleNewSession,
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'
),
currentSessionId && React.createElement('div', { className: 'text-sm text-gray-500' },
sessions.find(s => s.session_id === currentSessionId)?.session_name || '当前会话'
)
),
// 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(cand.instruction, cand.score),
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' })
),
'复制'
)
)
)
)
)
)
)
);
} 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: '输入任务描述,创建新的优化任务...',
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' })
)
)
)
),
React.createElement('div', { className: 'text-xs text-gray-500 mt-3 px-4' },
currentSessionId
? '输入任务描述AI 将为你生成优化的系统指令'
: '点击左侧"新建会话"开始,或直接输入任务描述自动创建会话'
)
)
)
)
);
}
// Render App
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(React.createElement(App));
</script>
</body>
</html>