agent / frontend /src /components /plugins /CodeExecutor.jsx
samlax12's picture
Upload 139 files
ad74240 verified
/**
* 代码执行插件组件
* 使用 CodeMirror 编辑器 + 终端输出面板 + 交互式输入
*/
import { useState, useRef, useEffect } from 'react';
import { Button, Space, Input, Typography } from 'antd';
import {
PlayCircleOutlined,
StopOutlined,
ClearOutlined,
CodeOutlined
} from '@ant-design/icons';
import CodeMirror from '@uiw/react-codemirror';
import { python } from '@codemirror/lang-python';
import api from '../../services/api';
const { Text } = Typography;
const CodeExecutor = ({ initialCode = '' }) => {
const [code, setCode] = useState(initialCode);
const [output, setOutput] = useState([]);
const [isRunning, setIsRunning] = useState(false);
const [needsInput, setNeedsInput] = useState(false);
const [contextId, setContextId] = useState(null);
const [userInput, setUserInput] = useState('');
const outputEndRef = useRef(null);
useEffect(() => {
if (initialCode) setCode(initialCode);
}, [initialCode]);
useEffect(() => {
outputEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [output]);
const appendOutput = (type, text) => {
setOutput(prev => [...prev, { type, text }]);
};
const handleRun = async () => {
if (!code.trim()) return;
setIsRunning(true);
setNeedsInput(false);
setOutput([{ type: 'info', text: '>>> 运行中...' }]);
try {
const res = await api.post('/code/execute', { code });
if (!res.success) {
appendOutput('error', res.error || '执行失败');
if (res.traceback) appendOutput('error', res.traceback);
setIsRunning(false);
return;
}
// 显示初始输出
if (res.output) {
appendOutput('stdout', res.output);
}
if (res.needsInput) {
// 代码需要交互输入
setContextId(res.context_id);
setNeedsInput(true);
} else {
// 代码直接执行完了
appendOutput('info', '>>> 执行完成');
setIsRunning(false);
}
} catch (error) {
appendOutput('error', `错误: ${error.message}`);
setIsRunning(false);
}
};
const handleInput = async () => {
if (!contextId) return;
const inputText = userInput;
appendOutput('input', `<<< ${inputText}`);
setUserInput('');
setNeedsInput(false);
try {
const res = await api.post('/code/input', {
context_id: contextId,
input: inputText
});
if (!res.success) {
appendOutput('error', res.error || '输入处理失败');
if (res.traceback) appendOutput('error', res.traceback);
setIsRunning(false);
setContextId(null);
return;
}
if (res.output) {
appendOutput('stdout', res.output);
}
if (res.needsInput) {
// 还需要更多输入
setNeedsInput(true);
} else {
// 执行完成
appendOutput('info', '>>> 执行完成');
setIsRunning(false);
setContextId(null);
}
} catch (error) {
appendOutput('error', `错误: ${error.message}`);
setIsRunning(false);
setContextId(null);
}
};
const handleStop = async () => {
if (contextId) {
try {
const res = await api.post('/code/stop', { context_id: contextId });
if (res.output) {
appendOutput('stdout', res.output);
}
} catch (e) { /* ignore */ }
}
appendOutput('warning', '>>> 已终止');
setIsRunning(false);
setNeedsInput(false);
setContextId(null);
};
const handleClear = () => {
setOutput([]);
};
const getOutputColor = (type) => {
switch (type) {
case 'error': return '#ef4444';
case 'warning': return '#f59e0b';
case 'input': return '#10b981';
case 'info': return '#6b7280';
default: return '#e5e7eb';
}
};
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%', gap: '8px' }}>
{/* 工具栏 */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Text strong style={{ fontSize: '13px' }}>
<CodeOutlined /> Python 编辑器
</Text>
<Space size="small">
{!isRunning ? (
<Button
type="primary"
size="small"
icon={<PlayCircleOutlined />}
onClick={handleRun}
style={{ background: '#10b981', borderColor: '#10b981' }}
>
运行
</Button>
) : (
<Button
danger
size="small"
icon={<StopOutlined />}
onClick={handleStop}
>
停止
</Button>
)}
<Button
size="small"
icon={<ClearOutlined />}
onClick={handleClear}
>
清除
</Button>
</Space>
</div>
{/* 代码编辑器 */}
<div style={{
flex: '0 0 auto',
maxHeight: '45%',
overflow: 'auto',
borderRadius: '6px',
border: '1px solid rgba(0,0,0,0.1)'
}}>
<CodeMirror
value={code}
height="auto"
minHeight="120px"
maxHeight="300px"
extensions={[python()]}
onChange={(val) => setCode(val)}
theme="light"
basicSetup={{
lineNumbers: true,
highlightActiveLine: true,
foldGutter: true,
}}
/>
</div>
{/* 终端输出 */}
<div style={{
flex: 1,
minHeight: '120px',
background: '#1e1e1e',
borderRadius: '6px',
padding: '12px',
overflow: 'auto',
fontFamily: "'JetBrains Mono', 'Fira Code', monospace",
fontSize: '13px',
}}>
{output.map((line, i) => (
<div key={i} style={{ color: getOutputColor(line.type), lineHeight: '1.6', whiteSpace: 'pre-wrap' }}>
{line.text}
</div>
))}
{needsInput && (
<div style={{ display: 'flex', gap: '8px', marginTop: '8px', alignItems: 'center' }}>
<span style={{ color: '#10b981', flexShrink: 0 }}>{'>>>'}</span>
<Input
size="small"
value={userInput}
onChange={e => setUserInput(e.target.value)}
onPressEnter={handleInput}
placeholder="输入内容后按回车..."
style={{
flex: 1,
background: '#2d2d2d',
border: '1px solid #444',
color: '#e5e7eb',
fontFamily: 'monospace'
}}
autoFocus
/>
</div>
)}
<div ref={outputEndRef} />
</div>
</div>
);
};
export default CodeExecutor;