|
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>AI代码助手 - Python执行环境</title>
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css">
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.css">
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/vs2015.min.css">
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/highlight.min.js"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/languages/python.min.js"></script>
|
|
<style>
|
|
|
|
:root {
|
|
--primary-color: #4361ee;
|
|
--secondary-color: #3f37c9;
|
|
--accent-color: #4cc9f0;
|
|
--success-color: #4caf50;
|
|
--warning-color: #ff9800;
|
|
--danger-color: #f44336;
|
|
--light-color: #f8f9fa;
|
|
--dark-color: #212529;
|
|
--border-color: #dee2e6;
|
|
--border-radius: 0.375rem;
|
|
}
|
|
|
|
body {
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
margin: 0;
|
|
padding: 0;
|
|
height: 100vh;
|
|
background-color: #f5f7fa;
|
|
color: #333;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
|
|
.workspace {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 16px;
|
|
flex: 1;
|
|
padding: 16px;
|
|
}
|
|
|
|
@media (max-width: 992px) {
|
|
.workspace {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
|
|
.section {
|
|
background: #fff;
|
|
border-radius: var(--border-radius);
|
|
overflow: hidden;
|
|
display: flex;
|
|
flex-direction: column;
|
|
border: 1px solid var(--border-color);
|
|
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
|
|
}
|
|
|
|
.section-header {
|
|
background: #f8f9fa;
|
|
padding: 12px 16px;
|
|
border-bottom: 1px solid var(--border-color);
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.section-title {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
font-size: 1rem;
|
|
font-weight: 500;
|
|
}
|
|
|
|
|
|
.editor-content {
|
|
flex: 1;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.code-area {
|
|
position: absolute;
|
|
left: 40px;
|
|
right: 0;
|
|
top: 0;
|
|
bottom: 0;
|
|
padding: 12px 16px;
|
|
color: #333;
|
|
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
|
font-size: 14px;
|
|
line-height: 1.6;
|
|
overflow: auto;
|
|
}
|
|
|
|
.code-area pre {
|
|
margin: 0;
|
|
padding: 0;
|
|
background: none;
|
|
border: none;
|
|
}
|
|
|
|
.code-area code {
|
|
display: block;
|
|
padding: 0;
|
|
tab-size: 4;
|
|
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
|
outline: none;
|
|
position: relative;
|
|
min-height: 100%;
|
|
white-space: pre !important;
|
|
word-wrap: normal !important;
|
|
}
|
|
|
|
.line-numbers {
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0;
|
|
bottom: 0;
|
|
width: 40px;
|
|
padding: 12px 0;
|
|
background: #f5f7fa;
|
|
border-right: 1px solid #e9ecef;
|
|
text-align: center;
|
|
color: #6c757d;
|
|
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
|
font-size: 14px;
|
|
line-height: 1.6;
|
|
user-select: none;
|
|
}
|
|
|
|
|
|
.output-content {
|
|
flex: 1;
|
|
background: #f8f9fa;
|
|
position: relative;
|
|
overflow: hidden;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.terminal-window {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
background: #212529;
|
|
color: #f8f9fa;
|
|
}
|
|
|
|
#output {
|
|
color: #f8f9fa;
|
|
margin: 0;
|
|
padding: 12px 16px;
|
|
background: transparent;
|
|
border: none;
|
|
white-space: pre-wrap;
|
|
word-wrap: break-word;
|
|
line-height: 1.6;
|
|
font-size: 14px;
|
|
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
|
}
|
|
|
|
.console-input-container {
|
|
display: flex;
|
|
align-items: center;
|
|
background: #343a40;
|
|
border-top: 1px solid #495057;
|
|
padding: 8px 12px;
|
|
}
|
|
|
|
.console-prompt {
|
|
color: #4caf50;
|
|
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
|
margin-right: 8px;
|
|
font-size: 14px;
|
|
user-select: none;
|
|
}
|
|
|
|
.console-input {
|
|
flex: 1;
|
|
background: transparent;
|
|
border: none;
|
|
color: #f8f9fa;
|
|
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
|
font-size: 14px;
|
|
line-height: 1.5;
|
|
padding: 4px 0;
|
|
}
|
|
|
|
.console-input:focus {
|
|
outline: none;
|
|
}
|
|
|
|
|
|
.chat-section {
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100%;
|
|
}
|
|
|
|
.chat-messages {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 16px;
|
|
}
|
|
|
|
.message {
|
|
margin-bottom: 16px;
|
|
padding: 12px;
|
|
border-radius: var(--border-radius);
|
|
max-width: 85%;
|
|
position: relative;
|
|
}
|
|
|
|
.message.user {
|
|
background-color: #e3f2fd;
|
|
color: #0d47a1;
|
|
align-self: flex-end;
|
|
margin-left: auto;
|
|
}
|
|
|
|
.message.bot {
|
|
background-color: #f5f5f5;
|
|
color: #333;
|
|
align-self: flex-start;
|
|
border-left: 3px solid var(--primary-color);
|
|
}
|
|
|
|
.chat-input-container {
|
|
padding: 16px;
|
|
border-top: 1px solid var(--border-color);
|
|
background-color: #f9f9f9;
|
|
}
|
|
|
|
.input-row {
|
|
display: flex;
|
|
gap: 8px;
|
|
}
|
|
|
|
.chat-input {
|
|
flex: 1;
|
|
padding: 12px;
|
|
border: 1px solid var(--border-color);
|
|
border-radius: var(--border-radius);
|
|
resize: none;
|
|
font-size: 14px;
|
|
height: 100px;
|
|
}
|
|
|
|
.chat-input:focus {
|
|
outline: none;
|
|
border-color: var(--primary-color);
|
|
box-shadow: 0 0 0 3px rgba(67, 97, 238, 0.1);
|
|
}
|
|
|
|
|
|
.btn {
|
|
padding: 8px 16px;
|
|
border: none;
|
|
border-radius: var(--border-radius);
|
|
cursor: pointer;
|
|
font-weight: 500;
|
|
font-size: 14px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.btn-primary {
|
|
background-color: var(--primary-color);
|
|
color: white;
|
|
}
|
|
|
|
.btn-primary:hover {
|
|
background-color: var(--secondary-color);
|
|
}
|
|
|
|
.btn-secondary {
|
|
background-color: #6c757d;
|
|
color: white;
|
|
}
|
|
|
|
.btn-secondary:hover {
|
|
background-color: #5a6268;
|
|
}
|
|
|
|
.btn-danger {
|
|
background-color: var(--danger-color);
|
|
color: white;
|
|
}
|
|
|
|
.btn-danger:hover {
|
|
background-color: #d32f2f;
|
|
}
|
|
|
|
|
|
.loading {
|
|
display: none;
|
|
align-items: center;
|
|
gap: 8px;
|
|
color: #6c757d;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.loading.active {
|
|
display: flex;
|
|
}
|
|
|
|
@keyframes spin {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
.loading i {
|
|
animation: spin 1s linear infinite;
|
|
}
|
|
|
|
|
|
::-webkit-scrollbar {
|
|
width: 8px;
|
|
height: 8px;
|
|
}
|
|
|
|
::-webkit-scrollbar-track {
|
|
background: #f1f1f1;
|
|
}
|
|
|
|
::-webkit-scrollbar-thumb {
|
|
background: #c1c1c1;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
::-webkit-scrollbar-thumb:hover {
|
|
background: #a8a8a8;
|
|
}
|
|
|
|
|
|
.term-input {
|
|
color: #4caf50;
|
|
}
|
|
|
|
.term-output {
|
|
color: #f8f9fa;
|
|
}
|
|
|
|
.term-error {
|
|
color: #f44336;
|
|
}
|
|
|
|
.term-warning {
|
|
color: #ff9800;
|
|
}
|
|
|
|
.term-system {
|
|
color: #2196f3;
|
|
}
|
|
|
|
|
|
.header {
|
|
background-color: #fff;
|
|
border-bottom: 1px solid var(--border-color);
|
|
padding: 1rem;
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
|
}
|
|
|
|
.header-content {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.header h1 {
|
|
margin: 0;
|
|
font-size: 1.5rem;
|
|
color: var(--primary-color);
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<header class="header">
|
|
<div class="header-content">
|
|
<h1>AI代码助手</h1>
|
|
<div>
|
|
<span class="badge bg-primary">Python编程环境</span>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="workspace">
|
|
<div class="section">
|
|
<div class="section-header">
|
|
<div class="section-title">
|
|
<i class="bi bi-code-square"></i>
|
|
代码编辑器
|
|
</div>
|
|
<div style="display: flex; gap: 8px;">
|
|
<button class="btn btn-primary" id="runCode">
|
|
<i class="bi bi-play-fill"></i>
|
|
运行
|
|
</button>
|
|
<button class="btn btn-danger" id="stopCode" style="display: none;">
|
|
<i class="bi bi-stop-fill"></i>
|
|
停止
|
|
</button>
|
|
<button class="btn btn-secondary" id="clearCode">
|
|
<i class="bi bi-trash"></i>
|
|
清除
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="editor-content">
|
|
<div class="line-numbers" id="lineNumbers">1</div>
|
|
<div class="code-area" id="codeArea">
|
|
<pre><code class="language-python" contenteditable="true" spellcheck="false" autocorrect="off" autocapitalize="off"># 您的代码将在这里显示</code></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<div class="section-header">
|
|
<div class="section-title">
|
|
<i class="bi bi-terminal"></i>
|
|
终端输出
|
|
</div>
|
|
<div style="display: flex; gap: 8px;">
|
|
<button class="btn btn-secondary" id="clearTerminal">
|
|
<i class="bi bi-eraser"></i>
|
|
清除
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="output-content">
|
|
<div class="terminal-window">
|
|
<pre id="output"></pre>
|
|
</div>
|
|
<div class="console-input-container" id="consoleInputContainer" style="display: none;">
|
|
<div class="console-prompt">>></div>
|
|
<input type="text" id="consoleInput" class="console-input" autocomplete="off" spellcheck="false" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
|
|
const terminalOutput = document.getElementById('output');
|
|
const consoleInputContainer = document.getElementById('consoleInputContainer');
|
|
const consoleInput = document.getElementById('consoleInput');
|
|
const clearTerminalBtn = document.getElementById('clearTerminal');
|
|
const stopCodeBtn = document.getElementById('stopCode');
|
|
|
|
|
|
const codeArea = document.querySelector('#codeArea code');
|
|
const lineNumbers = document.getElementById('lineNumbers');
|
|
const runButton = document.getElementById('runCode');
|
|
const clearButton = document.getElementById('clearCode');
|
|
|
|
|
|
let executionContext = null;
|
|
let isExecuting = false;
|
|
let executionStartTime = null;
|
|
|
|
|
|
initializeTerminal();
|
|
|
|
|
|
updateLineNumbers();
|
|
|
|
|
|
function updateLineNumbers() {
|
|
const lines = codeArea.textContent.split('\n').length;
|
|
lineNumbers.innerHTML = Array.from({length: lines}, (_, i) => i + 1).join('<br>');
|
|
}
|
|
|
|
|
|
function initializeTerminal() {
|
|
clearTerminalOutput();
|
|
appendToTerminal("欢迎使用Python交互式终端", "term-system");
|
|
appendToTerminal("使用'运行'按钮执行您的代码", "term-system");
|
|
appendToTerminal("", "term-system");
|
|
appendToTerminal(">>> 准备执行代码...", "term-system");
|
|
}
|
|
|
|
|
|
function appendToTerminal(text, type = null) {
|
|
const lines = text.split('\n');
|
|
let html = '';
|
|
|
|
for (const line of lines) {
|
|
if (type) {
|
|
html += `<span class="${type}">${escapeHtml(line)}</span>\n`;
|
|
} else {
|
|
html += escapeHtml(line) + '\n';
|
|
}
|
|
}
|
|
|
|
terminalOutput.innerHTML += html;
|
|
terminalOutput.scrollTop = terminalOutput.scrollHeight;
|
|
}
|
|
|
|
|
|
function clearTerminalOutput() {
|
|
terminalOutput.innerHTML = '';
|
|
}
|
|
|
|
|
|
function escapeHtml(text) {
|
|
return text
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'");
|
|
}
|
|
|
|
|
|
runButton.addEventListener('click', async () => {
|
|
if (isExecuting) return;
|
|
|
|
const code = codeArea.textContent;
|
|
|
|
|
|
clearTerminalOutput();
|
|
appendToTerminal("开始执行Python代码...", "term-system");
|
|
|
|
|
|
isExecuting = true;
|
|
executionStartTime = performance.now();
|
|
runButton.style.display = 'none';
|
|
stopCodeBtn.style.display = 'flex';
|
|
|
|
try {
|
|
const response = await fetch('/api/code/execute', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ code })
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
|
|
if (data.output && data.output.trim()) {
|
|
appendToTerminal(data.output, "term-output");
|
|
}
|
|
|
|
if (data.needsInput) {
|
|
|
|
executionContext = data.context_id;
|
|
consoleInputContainer.style.display = 'flex';
|
|
consoleInput.focus();
|
|
} else {
|
|
|
|
appendToTerminal("程序执行完成", "term-system");
|
|
finishExecution();
|
|
}
|
|
} else {
|
|
|
|
appendToTerminal(`错误: ${data.error}`, "term-error");
|
|
if (data.traceback) {
|
|
appendToTerminal(data.traceback, "term-error");
|
|
}
|
|
appendToTerminal("执行失败", "term-system");
|
|
finishExecution();
|
|
}
|
|
} catch (error) {
|
|
appendToTerminal(`系统错误: ${error.message}`, "term-error");
|
|
appendToTerminal("执行失败", "term-system");
|
|
finishExecution();
|
|
}
|
|
});
|
|
|
|
|
|
consoleInput.addEventListener('keydown', async (e) => {
|
|
if (e.key === 'Enter' && executionContext) {
|
|
const input = consoleInput.value;
|
|
consoleInput.value = '';
|
|
|
|
|
|
appendToTerminal(`>> ${input}`, "term-input");
|
|
|
|
try {
|
|
|
|
const response = await fetch('/api/code/input', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
input: input,
|
|
context_id: executionContext
|
|
})
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
|
|
if (data.output && data.output.trim()) {
|
|
appendToTerminal(data.output, "term-output");
|
|
}
|
|
|
|
if (data.needsInput) {
|
|
|
|
consoleInput.focus();
|
|
} else {
|
|
|
|
appendToTerminal(">>> 程序执行完成", "term-system");
|
|
finishExecution();
|
|
}
|
|
} else {
|
|
|
|
appendToTerminal(`错误: ${data.error}`, "term-error");
|
|
if (data.traceback) {
|
|
appendToTerminal(data.traceback, "term-error");
|
|
}
|
|
finishExecution();
|
|
}
|
|
} catch (error) {
|
|
appendToTerminal(`系统错误: ${error.message}`, "term-error");
|
|
finishExecution();
|
|
}
|
|
}
|
|
});
|
|
|
|
|
|
stopCodeBtn.addEventListener('click', async () => {
|
|
if (!executionContext) return;
|
|
|
|
try {
|
|
|
|
const response = await fetch('/api/code/stop', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ context_id: executionContext })
|
|
});
|
|
|
|
|
|
appendToTerminal("用户终止了执行", "term-warning");
|
|
finishExecution();
|
|
} catch (error) {
|
|
console.error('停止执行时出错:', error);
|
|
finishExecution();
|
|
}
|
|
});
|
|
|
|
|
|
clearButton.addEventListener('click', () => {
|
|
codeArea.textContent = '# 您的代码将在这里显示';
|
|
updateLineNumbers();
|
|
clearTerminalOutput();
|
|
initializeTerminal();
|
|
consoleInputContainer.style.display = 'none';
|
|
executionContext = null;
|
|
isExecuting = false;
|
|
runButton.style.display = 'flex';
|
|
stopCodeBtn.style.display = 'none';
|
|
});
|
|
|
|
|
|
clearTerminalBtn.addEventListener('click', () => {
|
|
if (!isExecuting) {
|
|
clearTerminalOutput();
|
|
initializeTerminal();
|
|
} else {
|
|
|
|
appendToTerminal("\n--- 已清除终端 ---\n", "term-system");
|
|
}
|
|
});
|
|
|
|
|
|
codeArea.addEventListener('input', updateLineNumbers);
|
|
|
|
|
|
codeArea.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Tab') {
|
|
e.preventDefault();
|
|
document.execCommand('insertText', false, ' ');
|
|
}
|
|
});
|
|
|
|
|
|
function finishExecution() {
|
|
consoleInputContainer.style.display = 'none';
|
|
executionContext = null;
|
|
isExecuting = false;
|
|
runButton.style.display = 'flex';
|
|
stopCodeBtn.style.display = 'none';
|
|
}
|
|
|
|
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
const initialCode = urlParams.get('code');
|
|
if (initialCode) {
|
|
try {
|
|
codeArea.textContent = decodeURIComponent(initialCode);
|
|
updateLineNumbers();
|
|
} catch (e) {
|
|
console.error('Failed to decode initial code:', e);
|
|
}
|
|
}
|
|
|
|
|
|
window.addEventListener('message', (event) => {
|
|
if (event.data && event.data.type === 'setCode') {
|
|
codeArea.textContent = event.data.code;
|
|
updateLineNumbers();
|
|
}
|
|
});
|
|
});
|
|
</script>
|
|
</body>
|
|
</html> |