|
<!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> |