- 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
651 lines
33 KiB
HTML
651 lines
33 KiB
HTML
<!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>
|
||
|