| <!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/bootstrap@5.2.3/dist/css/bootstrap.min.css">
|
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/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> |