2025-12-06 17:24:28 +08:00
|
|
|
|
<!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);
|
2025-12-06 21:26:24 +08:00
|
|
|
|
const [sessions, setSessions] = useState([]);
|
|
|
|
|
|
const [currentSessionId, setCurrentSessionId] = useState(null);
|
|
|
|
|
|
const [currentSessionRuns, setCurrentSessionRuns] = useState([]);
|
2025-12-06 17:24:28 +08:00
|
|
|
|
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);
|
|
|
|
|
|
|
2025-12-06 21:26:24 +08:00
|
|
|
|
// Load sessions and models on mount
|
2025-12-06 17:24:28 +08:00
|
|
|
|
useEffect(() => {
|
2025-12-06 21:26:24 +08:00
|
|
|
|
loadSessions();
|
2025-12-06 17:24:28 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-06 21:26:24 +08:00
|
|
|
|
|
2025-12-06 17:24:28 +08:00
|
|
|
|
// Auto-scroll chat
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
chatEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
|
|
|
|
}, [messages]);
|
2025-12-06 21:26:24 +08:00
|
|
|
|
|
|
|
|
|
|
async function loadSessions() {
|
2025-12-06 17:24:28 +08:00
|
|
|
|
try {
|
2025-12-06 21:26:24 +08:00
|
|
|
|
const res = await fetch(`${API_BASE}/opro/sessions`);
|
2025-12-06 17:24:28 +08:00
|
|
|
|
const data = await res.json();
|
|
|
|
|
|
if (data.success) {
|
2025-12-06 21:26:24 +08:00
|
|
|
|
setSessions(data.data.sessions || []);
|
2025-12-06 17:24:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
} catch (err) {
|
2025-12-06 21:26:24 +08:00
|
|
|
|
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);
|
2025-12-06 17:24:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-06 21:26:24 +08:00
|
|
|
|
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([]);
|
|
|
|
|
|
|
|
|
|
|
|
// Reload sessions list
|
|
|
|
|
|
await loadSessions();
|
|
|
|
|
|
|
|
|
|
|
|
return sessionId;
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
alert('创建会话失败: ' + err.message);
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-06 17:24:28 +08:00
|
|
|
|
async function createNewRun(taskDescription) {
|
|
|
|
|
|
setLoading(true);
|
|
|
|
|
|
try {
|
2025-12-06 21:26:24 +08:00
|
|
|
|
// Ensure we have a session
|
|
|
|
|
|
let sessionId = currentSessionId;
|
|
|
|
|
|
if (!sessionId) {
|
|
|
|
|
|
sessionId = await createNewSession();
|
|
|
|
|
|
if (!sessionId) return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Create run within session
|
2025-12-06 17:24:28 +08:00
|
|
|
|
const res = await fetch(`${API_BASE}/opro/create`, {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
task_description: taskDescription,
|
|
|
|
|
|
test_cases: [],
|
2025-12-06 21:26:24 +08:00
|
|
|
|
model_name: selectedModel || undefined,
|
|
|
|
|
|
session_id: sessionId
|
2025-12-06 17:24:28 +08:00
|
|
|
|
})
|
|
|
|
|
|
});
|
|
|
|
|
|
const data = await res.json();
|
2025-12-06 21:26:24 +08:00
|
|
|
|
|
2025-12-06 17:24:28 +08:00
|
|
|
|
if (!data.success) {
|
|
|
|
|
|
throw new Error(data.error || 'Failed to create run');
|
|
|
|
|
|
}
|
2025-12-06 21:26:24 +08:00
|
|
|
|
|
2025-12-06 17:24:28 +08:00
|
|
|
|
const runId = data.data.run_id;
|
|
|
|
|
|
setCurrentRunId(runId);
|
2025-12-06 21:26:24 +08:00
|
|
|
|
|
2025-12-06 17:24:28 +08:00
|
|
|
|
// Add user message
|
|
|
|
|
|
setMessages([{ role: 'user', content: taskDescription }]);
|
2025-12-06 21:26:24 +08:00
|
|
|
|
|
2025-12-06 17:24:28 +08:00
|
|
|
|
// Generate and evaluate candidates
|
|
|
|
|
|
await generateCandidates(runId);
|
2025-12-06 21:26:24 +08:00
|
|
|
|
|
|
|
|
|
|
// Reload sessions and session runs
|
|
|
|
|
|
await loadSessions();
|
|
|
|
|
|
await loadSessionRuns(sessionId);
|
2025-12-06 17:24:28 +08:00
|
|
|
|
} catch (err) {
|
|
|
|
|
|
alert('创建任务失败: ' + err.message);
|
2025-12-06 21:26:24 +08:00
|
|
|
|
console.error('Error creating run:', err);
|
2025-12-06 17:24:28 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function generateCandidates(runId) {
|
|
|
|
|
|
setLoading(true);
|
|
|
|
|
|
try {
|
2025-12-06 21:26:24 +08:00
|
|
|
|
console.log('Generating candidates for run:', runId);
|
2025-12-06 17:24:28 +08:00
|
|
|
|
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();
|
2025-12-06 21:26:24 +08:00
|
|
|
|
|
|
|
|
|
|
console.log('Generate candidates response:', data);
|
|
|
|
|
|
|
2025-12-06 17:24:28 +08:00
|
|
|
|
if (!data.success) {
|
|
|
|
|
|
throw new Error(data.error || 'Failed to generate candidates');
|
|
|
|
|
|
}
|
2025-12-06 21:26:24 +08:00
|
|
|
|
|
2025-12-06 17:24:28 +08:00
|
|
|
|
// 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);
|
2025-12-06 21:26:24 +08:00
|
|
|
|
console.error('Error generating candidates:', err);
|
2025-12-06 17:24:28 +08:00
|
|
|
|
} 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;
|
2025-12-06 21:26:24 +08:00
|
|
|
|
executeInstruction(instruction, '');
|
2025-12-06 17:24:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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() {
|
2025-12-06 21:26:24 +08:00
|
|
|
|
// Create new run within current session
|
2025-12-06 17:24:28 +08:00
|
|
|
|
setCurrentRunId(null);
|
|
|
|
|
|
setMessages([]);
|
|
|
|
|
|
setInputValue('');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-06 21:26:24 +08:00
|
|
|
|
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);
|
|
|
|
|
|
setCurrentRunId(null);
|
|
|
|
|
|
setMessages([]);
|
|
|
|
|
|
await loadSessionRuns(sessionId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-06 17:24:28 +08:00
|
|
|
|
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,
|
2025-12-06 21:26:24 +08:00
|
|
|
|
// New session button (expanded)
|
2025-12-06 17:24:28 +08:00
|
|
|
|
React.createElement('button', {
|
2025-12-06 21:26:24 +08:00
|
|
|
|
onClick: handleNewSession,
|
2025-12-06 17:24:28 +08:00
|
|
|
|
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
|
2025-12-06 21:26:24 +08:00
|
|
|
|
sessions.length > 0 && React.createElement('div', { className: 'text-xs text-gray-500 mb-2 px-2' }, '会话列表'),
|
|
|
|
|
|
sessions.map(session =>
|
2025-12-06 17:24:28 +08:00
|
|
|
|
React.createElement('div', {
|
2025-12-06 21:26:24 +08:00
|
|
|
|
key: session.session_id,
|
|
|
|
|
|
onClick: () => handleSelectSession(session.session_id),
|
2025-12-06 17:24:28 +08:00
|
|
|
|
className: `p-3 mb-1 rounded-lg cursor-pointer transition-colors flex items-center gap-2 ${
|
2025-12-06 21:26:24 +08:00
|
|
|
|
currentSessionId === session.session_id ? 'bg-gray-100' : 'hover:bg-gray-50'
|
2025-12-06 17:24:28 +08:00
|
|
|
|
}`
|
|
|
|
|
|
},
|
|
|
|
|
|
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' },
|
2025-12-06 21:26:24 +08:00
|
|
|
|
session.session_name
|
2025-12-06 17:24:28 +08:00
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
) : React.createElement('button', {
|
2025-12-06 21:26:24 +08:00
|
|
|
|
onClick: handleNewSession,
|
2025-12-06 17:24:28 +08:00
|
|
|
|
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
|
2025-12-06 21:26:24 +08:00
|
|
|
|
React.createElement('div', { className: 'px-4 py-3 border-b border-gray-200 bg-white flex items-center justify-between' },
|
|
|
|
|
|
React.createElement('div', { className: '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 || '当前会话'
|
|
|
|
|
|
)
|
|
|
|
|
|
),
|
|
|
|
|
|
currentSessionId && React.createElement('button', {
|
|
|
|
|
|
onClick: handleNewTask,
|
|
|
|
|
|
className: 'px-3 py-1.5 text-sm bg-white border border-gray-300 hover:bg-gray-50 rounded-lg transition-colors text-gray-700'
|
|
|
|
|
|
}, '+ 新建任务')
|
2025-12-06 17:24:28 +08:00
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
|
|
// 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' },
|
2025-12-06 21:26:24 +08:00
|
|
|
|
currentSessionId
|
|
|
|
|
|
? '输入任务描述后,AI 将为你生成优化的系统指令'
|
|
|
|
|
|
: '点击左侧"新建会话"开始,或输入任务描述自动创建会话'
|
2025-12-06 17:24:28 +08:00
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Render App
|
|
|
|
|
|
const root = ReactDOM.createRoot(document.getElementById('root'));
|
|
|
|
|
|
root.render(React.createElement(App));
|
|
|
|
|
|
</script>
|
|
|
|
|
|
</body>
|
|
|
|
|
|
</html>
|
|
|
|
|
|
|