From 92d7faa9ef512f599443f2267ba8e60e44e87a4c Mon Sep 17 00:00:00 2001 From: Grover Date: Thu, 19 Jun 2025 18:53:42 -0400 Subject: [PATCH 01/11] feature: agregando autocompletado al editor --- frontend/angular/angular.json | 5 +- .../app/shared/editor/editor.component.html | 1 + .../app/shared/editor/editor.component.scss | 31 +++ .../src/app/shared/editor/editor.component.ts | 209 +++++++++++++++++- 4 files changed, 235 insertions(+), 11 deletions(-) diff --git a/frontend/angular/angular.json b/frontend/angular/angular.json index 3eaf821..76a6498 100644 --- a/frontend/angular/angular.json +++ b/frontend/angular/angular.json @@ -34,12 +34,15 @@ "node_modules/codemirror/lib/codemirror.css", "node_modules/codemirror/theme/material.css", "node_modules/codemirror/addon/lint/lint.css", + "node_modules/codemirror/addon/hint/show-hint.css", "src/styles.scss" ], "scripts": [ "node_modules/codemirror/lib/codemirror.js", "node_modules/codemirror/mode/python/python.js", - "node_modules/codemirror/addon/display/placeholder.js" + "node_modules/codemirror/addon/display/placeholder.js", + "node_modules/codemirror/addon/hint/show-hint.js", + "node_modules/codemirror/addon/hint/anyword-hint.js" ] }, "configurations": { diff --git a/frontend/angular/src/app/shared/editor/editor.component.html b/frontend/angular/src/app/shared/editor/editor.component.html index 809d175..d47c939 100644 --- a/frontend/angular/src/app/shared/editor/editor.component.html +++ b/frontend/angular/src/app/shared/editor/editor.component.html @@ -3,6 +3,7 @@

Editor de Código Python

{ + CodeMirror.showHint(cm, (cm: any) => { + const cursor = cm.getCursor(); + const token = cm.getTokenAt(cursor); + const start = token.start; + const end = cursor.ch; + const line = cursor.line; + const currentWord = token.string; + + const list = [...PYTHON_KEYWORDS, ...PYTHON_BUILTINS].filter(word => + word.toLowerCase().startsWith(currentWord.toLowerCase()) && + word !== currentWord + ); + + // Obtener palabras del documento actual + const words = new Set(); + for (let i = 0; i < cm.lineCount(); i++) { + const lineText = cm.getLine(i); + const wordMatches = lineText.match(/[\w$]+/g); + if (wordMatches) { + wordMatches.forEach((word: string) => words.add(word)); + } + } + + // Agregar palabras del documento a las sugerencias + words.forEach(word => { + if (word.toLowerCase().startsWith(currentWord.toLowerCase()) && + word !== currentWord && + !list.includes(word)) { + list.push(word); + } + }); + + return { + list: list.sort(), + from: CodeMirror.Pos(line, start), + to: CodeMirror.Pos(line, end) + }; + }, { + completeSingle: false + }); + }, + 'Tab': (cm: any) => { + if (cm.somethingSelected()) { + cm.indentSelection('add'); + } else { + const spaces = Array(cm.getOption('indentUnit') + 1).join(' '); + cm.replaceSelection(spaces); + } + } } }; @@ -96,6 +168,62 @@ export class EditorComponent implements OnInit { pyodide: any; pyodideReady = false; codeMirrorInstance: any; + editorOptions: any; + codeEditor: any; + + constructor(private http: HttpClient) { + // Función de autocompletado personalizada + const pythonHint = (cm: any) => { + const cursor = cm.getCursor(); + const token = cm.getTokenAt(cursor); + + // No mostrar sugerencias dentro de strings o comentarios + if (token.type === 'string' || token.type === 'comment') { + return null; + } + + const words = new Set(); + + // Agregar palabras clave de Python + PYTHON_KEYWORDS.forEach(word => words.add(word)); + + // Agregar palabras del documento actual + for (let i = 0; i < cm.lineCount(); i++) { + const lineText = cm.getLine(i); + const wordMatches = lineText.match(/[\\w$]+/g); + if (wordMatches) { + wordMatches.forEach((word: string) => words.add(word)); + } + } + + return { + list: Array.from(words).filter(word => + word.toLowerCase().startsWith(token.string.toLowerCase()) + ).sort(), + from: {line: cursor.line, ch: token.start}, + to: {line: cursor.line, ch: token.end} + }; + }; + + this.editorOptions = { + mode: 'python', + theme: 'material', + lineNumbers: true, + lineWrapping: true, + styleActiveLine: true, + autoCloseBrackets: true, + matchBrackets: true, + lint: true, + extraKeys: { + 'Ctrl-Space': false, + 'Ctrl-B': false + }, + hintOptions: { + completeSingle: false, + hint: pythonHint + } + }; + } async ngOnInit() { // @ts-expect-error: loadPyodide no está definido en el contexto de TypeScript @@ -105,9 +233,75 @@ export class EditorComponent implements OnInit { console.log('[Editor] Pyodide cargado desde CDN'); } + ngAfterViewInit() { + // Esperar a que el editor esté listo + setTimeout(() => { + if (this.codeEditorComponent) { + this.codeEditor = this.codeEditorComponent.codeMirror; + + // Configurar el autocompletado automático + this.codeEditor.on('inputRead', (cm: any, change: any) => { + if (!change || !change.text) return; + + const cursor = cm.getCursor(); + const token = cm.getTokenAt(cursor); + + // No mostrar sugerencias para operadores o dentro de strings/comentarios + if (token.type === 'string' || token.type === 'comment') { + return; + } + + // Verificar si el último carácter ingresado es una letra o guión bajo + const lastChar = change.text[change.text.length - 1]; + if (lastChar && lastChar.match(/[a-zA-Z_]/)) { + cm.showHint({ + completeSingle: false, + closeOnUnfocus: false, + alignWithWord: true + }); + } + }); + + // También mostrar sugerencias al borrar + this.codeEditor.on('keyup', (cm: any, event: KeyboardEvent) => { + const cursor = cm.getCursor(); + const token = cm.getTokenAt(cursor); + + // No mostrar sugerencias para operadores o dentro de strings/comentarios + if (token.type === 'string' || token.type === 'comment') { + return; + } + + // Mostrar sugerencias al borrar si hay texto que completar + if (event.key === 'Backspace' && token.string.length > 0) { + cm.showHint({ + completeSingle: false, + closeOnUnfocus: false, + alignWithWord: true + }); + } + }); + } + }, 100); + } + // Vincular instancia de CodeMirror onEditorInit(instance: any) { this.codeMirrorInstance = instance; + + // Activar autocompletado al escribir + instance.on('inputRead', (cm: any, change: any) => { + if (!change || change.origin !== '+input') return; + + const cur = cm.getCursor(); + const token = cm.getTokenAt(cur); + + // Solo mostrar sugerencias si estamos escribiendo una palabra + if (token.type !== 'operator' && token.type !== 'string' && + token.string.length > 1 && !/^\d+$/.test(token.string)) { + CodeMirror.commands.autocomplete(cm); + } + }); } // Forzar el lint en cada cambio de código @@ -150,11 +344,6 @@ export class EditorComponent implements OnInit { } } - - - - constructor(private http: HttpClient) {} - chat(): void{ if (!this.inputChat.trim()) { this.outputChat = 'Por favor, escribe un mensaje.'; From 0f594b6bc11166027656285ac480cb2aa2d859fe Mon Sep 17 00:00:00 2001 From: Grover Date: Thu, 19 Jun 2025 19:22:08 -0400 Subject: [PATCH 02/11] feat: implement contextual code completion with LSP - Add WebSocket LSP endpoint in backend - Create Python LSP service for frontend - Unify autocompletion (keywords, builtins, contextual variables) - Enable real-time autocompletion on typing - Maintain Ctrl+Space functionality --- backend/main.go | 9 +- backend/routes/lsp.go | 10 +- frontend/angular/package-lock.json | 304 +++++++++++++++++- frontend/angular/package.json | 5 +- .../src/app/services/python-lsp.service.ts | 73 +++++ .../src/app/shared/editor/editor.component.ts | 171 ++++++---- 6 files changed, 491 insertions(+), 81 deletions(-) create mode 100644 frontend/angular/src/app/services/python-lsp.service.ts diff --git a/backend/main.go b/backend/main.go index d6816f3..372e263 100644 --- a/backend/main.go +++ b/backend/main.go @@ -2,6 +2,7 @@ package main import ( "log" + "net/http" "github.com/Frosmin/backend/db" "github.com/Frosmin/backend/routes" @@ -23,9 +24,11 @@ func main() { // Obtener el router configurado con CORS y rutas r := routes.SetupRouter() - // No es necesario configurar rutas aquí, ya están en SetupRouter() - // ¡Elimina estas líneas! + // Configurar rutas adicionales + http.HandleFunc("/", routes.Homehandler) + http.HandleFunc("/verify", routes.VerificarPythonHandler) + http.HandleFunc("/lsp", routes.LSPHandler) log.Println("Servidor escuchando en :8080") - log.Fatal(r.Run(":8080")) + log.Fatal(http.ListenAndServe(":8080", r)) } diff --git a/backend/routes/lsp.go b/backend/routes/lsp.go index 4d25847..a60e7e7 100644 --- a/backend/routes/lsp.go +++ b/backend/routes/lsp.go @@ -8,9 +8,13 @@ import ( "github.com/gorilla/websocket" ) -var upgrader = websocket.Upgrader{} +var upgrader = websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { + return true // Permitir todas las conexiones + }, +} -func LspHandler(w http.ResponseWriter, r *http.Request) { +func LSPHandler(w http.ResponseWriter, r *http.Request) { conn, err := upgrader.Upgrade(w, r, nil) if err != nil { log.Println("Error al actualizar a WebSocket:", err) @@ -50,7 +54,7 @@ func LspHandler(w http.ResponseWriter, r *http.Request) { } }() - buf := make([]byte, 1024) + buf := make([]byte, 4096) for { n, err := stdout.Read(buf) if err != nil { diff --git a/frontend/angular/package-lock.json b/frontend/angular/package-lock.json index 69e19df..5492e44 100644 --- a/frontend/angular/package-lock.json +++ b/frontend/angular/package-lock.json @@ -18,12 +18,14 @@ "@angular/platform-browser-dynamic": "^19.2.0", "@angular/router": "^19.2.0", "@ctrl/ngx-codemirror": "^7.0.0", + "@marimo-team/codemirror-languageserver": "^1.15.15", "@tsparticles/angular": "^3.0.0", "@tsparticles/engine": "^3.8.1", "@tsparticles/slim": "^3.8.1", "codemirror": "^5.65.19", "jspdf": "^3.0.1", "pyodide": "^0.27.5", + "pyright": "^1.1.402", "rxjs": "~7.8.0", "tslib": "^2.3.0", "zone.js": "~0.15.0" @@ -37,6 +39,7 @@ "@angular/cli": "^19.2.9", "@angular/compiler-cli": "^19.2.0", "@types/jasmine": "~5.1.0", + "@types/node": "^24.0.3", "angular-eslint": "19.5.0", "eslint": "^9.27.0", "jasmine-core": "~5.6.0", @@ -2529,6 +2532,64 @@ "node": ">=6.9.0" } }, + "node_modules/@codemirror/autocomplete": { + "version": "6.18.6", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.6.tgz", + "integrity": "sha512-PHHBXFomUs5DF+9tCOM/UoW6XQ4R44lLNNhRaW9PKPTU0D7lIjRg3ElxaJnTwsl/oHiR93WSXDBrekhoUGCPtg==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.11.1", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.1.tgz", + "integrity": "sha512-5kS1U7emOGV84vxC+ruBty5sUgcD0te6dyupyRVG2zaSjhTDM73LhVKUtVwiqSe6QwmEoA4SCiU8AKPFyumAWQ==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.1.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.8.5", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.5.tgz", + "integrity": "sha512-s3n3KisH7dx3vsoeGMxsbRAgKe4O1vbrnKBClm99PU0fWxmxsx5rR2PfqQgIt+2MMJBHbiJ5rfIdLYfB9NNvsA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.35.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz", + "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==", + "license": "MIT", + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.37.2", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.37.2.tgz", + "integrity": "sha512-XD3LdgQpxQs5jhOOZ2HRVT+Rj59O4Suc7g2ULvZ+Yi8eCkickrkZ5JFuoDhs2ST1mNI5zSsNYgR3NGa4OUrbnw==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.5.0", + "crelt": "^1.0.6", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -3773,6 +3834,30 @@ "dev": true, "license": "MIT" }, + "node_modules/@lezer/common": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz", + "integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==", + "license": "MIT" + }, + "node_modules/@lezer/highlight": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz", + "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz", + "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, "node_modules/@listr2/prompt-adapter-inquirer": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-2.0.18.tgz", @@ -3896,6 +3981,29 @@ "win32" ] }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", + "license": "MIT" + }, + "node_modules/@marimo-team/codemirror-languageserver": { + "version": "1.15.15", + "resolved": "https://registry.npmjs.org/@marimo-team/codemirror-languageserver/-/codemirror-languageserver-1.15.15.tgz", + "integrity": "sha512-dJCJ0qn6HDNs12Ps4kk67+/ShPKHla9LpbGIsCaIh9SWi7OnWfUTLyCHCMIxS5ROYsDbkzhKknfm4tucKQI9bQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@codemirror/autocomplete": "^6.18.4", + "@codemirror/lint": "^6.8.4", + "@open-rpc/client-js": "^1.8.1", + "marked": "^15.0.6", + "vscode-languageserver-protocol": "^3.17.5" + }, + "peerDependencies": { + "@codemirror/state": "^6", + "@codemirror/view": "^6" + } + }, "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", @@ -4616,6 +4724,39 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/@open-rpc/client-js": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@open-rpc/client-js/-/client-js-1.8.1.tgz", + "integrity": "sha512-vV+Hetl688nY/oWI9IFY0iKDrWuLdYhf7OIKI6U1DcnJV7r4gAgwRJjEr1QVYszUc0gjkHoQJzqevmXMGLyA0g==", + "license": "Apache-2.0", + "dependencies": { + "isomorphic-fetch": "^3.0.0", + "isomorphic-ws": "^5.0.0", + "strict-event-emitter-types": "^2.0.0", + "ws": "^7.0.0" + } + }, + "node_modules/@open-rpc/client-js/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/@parcel/watcher": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", @@ -6057,13 +6198,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.15.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.2.tgz", - "integrity": "sha512-uKXqKN9beGoMdBfcaTY1ecwz6ctxuJAcUlwE55938g0ZJ8lRxwAZqRz2AJ4pzpt5dHdTPMB863UZ0ESiFUcP7A==", + "version": "24.0.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.3.tgz", + "integrity": "sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.21.0" + "undici-types": "~7.8.0" } }, "node_modules/@types/node-forge": { @@ -8078,6 +8219,12 @@ } } }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -8504,7 +8651,6 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -8515,7 +8661,6 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -9674,7 +9819,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -10617,6 +10761,25 @@ "node": ">=0.10.0" } }, + "node_modules/isomorphic-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", + "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.6.1", + "whatwg-fetch": "^3.4.1" + } + }, + "node_modules/isomorphic-ws": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz", + "integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==", + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", @@ -11736,6 +11899,18 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/marked": { + "version": "15.0.12", + "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", + "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -12274,6 +12449,26 @@ "license": "MIT", "optional": true }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-forge": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", @@ -13405,6 +13600,22 @@ "node": ">=18.0.0" } }, + "node_modules/pyright": { + "version": "1.1.402", + "resolved": "https://registry.npmjs.org/pyright/-/pyright-1.1.402.tgz", + "integrity": "sha512-DwzfZFTlqg9j7VDvwSIORmTsYL8awHMce8DaNV7n+M3KjD0wFEWBQrxSUX6N0pv2BJcKe1PKx+8x9yfJRQvqKQ==", + "license": "MIT", + "bin": { + "pyright": "index.js", + "pyright-langserver": "langserver.index.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "node_modules/qjobs": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", @@ -13928,7 +14139,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/sass": { @@ -14805,6 +15016,12 @@ "node": ">=8.0" } }, + "node_modules/strict-event-emitter-types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-event-emitter-types/-/strict-event-emitter-types-2.0.0.tgz", + "integrity": "sha512-Nk/brWYpD85WlOgzw5h173aci0Teyv8YdIAEtV+N88nDB0dLlazZyJMIsN6eo1/AR61l+p6CJTG1JIyFaoNEEA==", + "license": "ISC" + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -14942,6 +15159,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/style-mod": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", + "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==", + "license": "MIT" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -15236,6 +15459,12 @@ "node": ">=0.6" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, "node_modules/tree-dump": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.0.2.tgz", @@ -15409,9 +15638,9 @@ } }, "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", "dev": true, "license": "MIT" }, @@ -16087,6 +16316,37 @@ "node": ">=0.10.0" } }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "license": "MIT" + }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" + }, "node_modules/watchpack": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", @@ -16129,6 +16389,12 @@ "license": "MIT", "optional": true }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, "node_modules/webpack": { "version": "5.98.0", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.98.0.tgz", @@ -16453,6 +16719,22 @@ "node": ">=0.8.0" } }, + "node_modules/whatwg-fetch": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", + "license": "MIT" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", diff --git a/frontend/angular/package.json b/frontend/angular/package.json index cd3d00e..1e8298d 100644 --- a/frontend/angular/package.json +++ b/frontend/angular/package.json @@ -21,12 +21,14 @@ "@angular/platform-browser-dynamic": "^19.2.0", "@angular/router": "^19.2.0", "@ctrl/ngx-codemirror": "^7.0.0", + "@marimo-team/codemirror-languageserver": "^1.15.15", "@tsparticles/angular": "^3.0.0", "@tsparticles/engine": "^3.8.1", "@tsparticles/slim": "^3.8.1", "codemirror": "^5.65.19", "jspdf": "^3.0.1", "pyodide": "^0.27.5", + "pyright": "^1.1.402", "rxjs": "~7.8.0", "tslib": "^2.3.0", "zone.js": "~0.15.0" @@ -40,6 +42,7 @@ "@angular/cli": "^19.2.9", "@angular/compiler-cli": "^19.2.0", "@types/jasmine": "~5.1.0", + "@types/node": "^24.0.3", "angular-eslint": "19.5.0", "eslint": "^9.27.0", "jasmine-core": "~5.6.0", @@ -51,4 +54,4 @@ "typescript": "~5.7.2", "typescript-eslint": "8.32.1" } -} \ No newline at end of file +} diff --git a/frontend/angular/src/app/services/python-lsp.service.ts b/frontend/angular/src/app/services/python-lsp.service.ts new file mode 100644 index 0000000..de03f4c --- /dev/null +++ b/frontend/angular/src/app/services/python-lsp.service.ts @@ -0,0 +1,73 @@ +import { Injectable } from '@angular/core'; +import { Subject } from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class PythonLSPService { + private socket: WebSocket; + private messageSubject = new Subject(); + private initialized = false; + + constructor() { + this.socket = new WebSocket('ws://localhost:8080/lsp'); + this.initializeLSPServer(); + } + + private initializeLSPServer() { + this.socket.onopen = () => { + if (this.initialized) return; + + const initializeParams = { + jsonrpc: '2.0', + id: 1, + method: 'initialize', + params: { + processId: null, // No es necesario en el cliente + rootUri: 'file:///', + capabilities: { + textDocument: { + completion: { + completionItem: { + snippetSupport: true + } + } + } + } + } + }; + + this.sendRequest(initializeParams); + this.initialized = true; + }; + + this.socket.onmessage = (event) => { + try { + const message = JSON.parse(event.data); + this.messageSubject.next(message); + } catch (error) { + console.error('Error parsing LSP message:', error); + } + }; + + this.socket.onerror = (error) => { + console.error('WebSocket Error:', error); + }; + } + + public sendRequest(params: any): void { + if (this.socket.readyState === WebSocket.OPEN) { + this.socket.send(JSON.stringify(params)); + } + } + + public onMessage() { + return this.messageSubject.asObservable(); + } + + public dispose() { + if (this.socket) { + this.socket.close(); + } + } +} \ No newline at end of file diff --git a/frontend/angular/src/app/shared/editor/editor.component.ts b/frontend/angular/src/app/shared/editor/editor.component.ts index 7752fce..2e45240 100644 --- a/frontend/angular/src/app/shared/editor/editor.component.ts +++ b/frontend/angular/src/app/shared/editor/editor.component.ts @@ -1,10 +1,12 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ // EditorComponent.ts -import { Component, OnInit, AfterViewInit, ViewChild } from '@angular/core'; +import { Component, OnInit, AfterViewInit, ViewChild, OnDestroy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { CodemirrorModule, CodemirrorComponent } from '@ctrl/ngx-codemirror'; import { HttpClient } from '@angular/common/http'; +import { PythonLSPService } from '../../services/python-lsp.service'; +import { Subscription } from 'rxjs'; // Importacion de lint y modo python para CodeMirror import * as CodeMirrorNS from 'codemirror'; @@ -80,7 +82,7 @@ function pythonLint(code: string) { templateUrl: './editor.component.html', styleUrls: ['./editor.component.scss'], }) -export class EditorComponent implements OnInit, AfterViewInit { +export class EditorComponent implements OnInit, AfterViewInit, OnDestroy { @ViewChild('codeEditor') private codeEditorComponent!: CodemirrorComponent; codigo = ''; inputs = ''; @@ -170,9 +172,14 @@ export class EditorComponent implements OnInit, AfterViewInit { codeMirrorInstance: any; editorOptions: any; codeEditor: any; + private lspSubscription: Subscription | null = null; + private completionItems: any[] = []; - constructor(private http: HttpClient) { - // Función de autocompletado personalizada + constructor( + private http: HttpClient, + private pythonLSP: PythonLSPService + ) { + // Función de autocompletado personalizada que combina LSP y palabras clave const pythonHint = (cm: any) => { const cursor = cm.getCursor(); const token = cm.getTokenAt(cursor); @@ -187,21 +194,31 @@ export class EditorComponent implements OnInit, AfterViewInit { // Agregar palabras clave de Python PYTHON_KEYWORDS.forEach(word => words.add(word)); + // Agregar funciones built-in de Python + PYTHON_BUILTINS.forEach(word => words.add(word)); + + // Agregar sugerencias del LSP + this.completionItems.forEach(item => words.add(item.label)); + // Agregar palabras del documento actual for (let i = 0; i < cm.lineCount(); i++) { const lineText = cm.getLine(i); - const wordMatches = lineText.match(/[\\w$]+/g); + const wordMatches = lineText.match(/[\w$]+/g); if (wordMatches) { wordMatches.forEach((word: string) => words.add(word)); } } + // Filtrar sugerencias basadas en el token actual + const filteredWords = Array.from(words).filter(word => + word.toLowerCase().startsWith(token.string.toLowerCase()) && + word !== token.string + ).sort(); + return { - list: Array.from(words).filter(word => - word.toLowerCase().startsWith(token.string.toLowerCase()) - ).sort(), - from: {line: cursor.line, ch: token.start}, - to: {line: cursor.line, ch: token.end} + list: filteredWords, + from: CodeMirror.Pos(cursor.line, token.start), + to: CodeMirror.Pos(cursor.line, token.end) }; }; @@ -215,12 +232,24 @@ export class EditorComponent implements OnInit, AfterViewInit { matchBrackets: true, lint: true, extraKeys: { - 'Ctrl-Space': false, + 'Ctrl-Space': (cm: any) => { + // Solicitar sugerencias al LSP antes de mostrar el hint + const cursor = cm.getCursor(); + this.requestCompletions(cm, cursor); + cm.showHint({ + hint: pythonHint, + completeSingle: false, + closeOnUnfocus: false, + alignWithWord: true + }); + }, 'Ctrl-B': false }, hintOptions: { + hint: pythonHint, completeSingle: false, - hint: pythonHint + closeOnUnfocus: false, + alignWithWord: true } }; } @@ -231,77 +260,93 @@ export class EditorComponent implements OnInit, AfterViewInit { this.pyodideReady = true; (window as any).pyodideInstance = this.pyodide; console.log('[Editor] Pyodide cargado desde CDN'); + + // Suscribirse a los mensajes del LSP + this.lspSubscription = this.pythonLSP.onMessage().subscribe(message => { + if (message.method === 'textDocument/completion') { + this.completionItems = message.result?.items || []; + } + }); } ngAfterViewInit() { - // Esperar a que el editor esté listo setTimeout(() => { if (this.codeEditorComponent) { this.codeEditor = this.codeEditorComponent.codeMirror; - - // Configurar el autocompletado automático + + // Autocompletado en tiempo real al escribir this.codeEditor.on('inputRead', (cm: any, change: any) => { if (!change || !change.text) return; - - const cursor = cm.getCursor(); - const token = cm.getTokenAt(cursor); - - // No mostrar sugerencias para operadores o dentro de strings/comentarios - if (token.type === 'string' || token.type === 'comment') { - return; - } - - // Verificar si el último carácter ingresado es una letra o guión bajo const lastChar = change.text[change.text.length - 1]; - if (lastChar && lastChar.match(/[a-zA-Z_]/)) { - cm.showHint({ - completeSingle: false, - closeOnUnfocus: false, - alignWithWord: true - }); + // Solo dispara si el usuario escribe letra, número, punto o guion bajo + if (lastChar && /[\w.]/.test(lastChar)) { + const cursor = cm.getCursor(); + const token = cm.getTokenAt(cursor); + if (token.type !== 'string' && token.type !== 'comment') { + // Solicita sugerencias al LSP y muestra el hint con la función unificada + this.requestCompletions(cm, cursor); + cm.showHint({ + hint: this.editorOptions.hintOptions.hint, + completeSingle: false, + closeOnUnfocus: false, + alignWithWord: true + }); + } } }); - // También mostrar sugerencias al borrar - this.codeEditor.on('keyup', (cm: any, event: KeyboardEvent) => { - const cursor = cm.getCursor(); - const token = cm.getTokenAt(cursor); - - // No mostrar sugerencias para operadores o dentro de strings/comentarios - if (token.type === 'string' || token.type === 'comment') { - return; - } - - // Mostrar sugerencias al borrar si hay texto que completar - if (event.key === 'Backspace' && token.string.length > 0) { - cm.showHint({ - completeSingle: false, - closeOnUnfocus: false, - alignWithWord: true - }); - } + // Notificar cambios al LSP + this.codeEditor.on('change', (cm: any) => { + this.notifyLSPChanges(cm); }); } }, 100); } + ngOnDestroy() { + if (this.lspSubscription) { + this.lspSubscription.unsubscribe(); + } + } + + private requestCompletions(cm: any, cursor: any) { + const documentUri = 'file:///workspace/main.py'; + const position = { + line: cursor.line, + character: cursor.ch + }; + + this.pythonLSP.sendRequest({ + jsonrpc: '2.0', + id: 2, + method: 'textDocument/completion', + params: { + textDocument: { uri: documentUri }, + position: position + } + }); + } + + private notifyLSPChanges(cm: any) { + const documentUri = 'file:///workspace/main.py'; + const content = cm.getValue(); + + this.pythonLSP.sendRequest({ + jsonrpc: '2.0', + method: 'textDocument/didChange', + params: { + textDocument: { + uri: documentUri, + version: Date.now() + }, + contentChanges: [{ text: content }] + } + }); + } + // Vincular instancia de CodeMirror onEditorInit(instance: any) { this.codeMirrorInstance = instance; - - // Activar autocompletado al escribir - instance.on('inputRead', (cm: any, change: any) => { - if (!change || change.origin !== '+input') return; - - const cur = cm.getCursor(); - const token = cm.getTokenAt(cur); - - // Solo mostrar sugerencias si estamos escribiendo una palabra - if (token.type !== 'operator' && token.type !== 'string' && - token.string.length > 1 && !/^\d+$/.test(token.string)) { - CodeMirror.commands.autocomplete(cm); - } - }); } // Forzar el lint en cada cambio de código From 44a6c3a770c4c161dbb1cc1725cf9438436175f9 Mon Sep 17 00:00:00 2001 From: Grover Date: Thu, 19 Jun 2025 20:53:49 -0400 Subject: [PATCH 03/11] Refactor: migrar backend a Gin, centralizar rutas y actualizar LSP - Eliminar uso de Gorilla en el backend - Centralizar todas las rutas en la carpeta routes/ usando Gin - Adaptar LSPHandler y VerificarPythonHandler a Gin - Limpiar main.go para usar solo Gin como router - Mantener compatibilidad con autocompletado contextual en frontend --- backend/main.go | 8 +------ backend/routes/cors.go | 6 ++++++ backend/routes/index.routes.go | 13 ++++++------ backend/routes/lsp.go | 10 +++++++-- .../src/app/services/python-lsp.service.ts | 21 ++++++++++++++++--- .../src/app/shared/editor/editor.component.ts | 12 ++++++----- 6 files changed, 47 insertions(+), 23 deletions(-) diff --git a/backend/main.go b/backend/main.go index 372e263..1b1861d 100644 --- a/backend/main.go +++ b/backend/main.go @@ -2,7 +2,6 @@ package main import ( "log" - "net/http" "github.com/Frosmin/backend/db" "github.com/Frosmin/backend/routes" @@ -24,11 +23,6 @@ func main() { // Obtener el router configurado con CORS y rutas r := routes.SetupRouter() - // Configurar rutas adicionales - http.HandleFunc("/", routes.Homehandler) - http.HandleFunc("/verify", routes.VerificarPythonHandler) - http.HandleFunc("/lsp", routes.LSPHandler) - log.Println("Servidor escuchando en :8080") - log.Fatal(http.ListenAndServe(":8080", r)) + log.Fatal(r.Run(":8080")) } diff --git a/backend/routes/cors.go b/backend/routes/cors.go index c856621..fd1742b 100644 --- a/backend/routes/cors.go +++ b/backend/routes/cors.go @@ -22,6 +22,12 @@ func SetupRouter() *gin.Engine { c.String(200, "Bienvenido a la API de Aprendizaje Python") }) + // Ruta LSP para WebSocket + r.GET("/lsp", LSPHandler) + + // Ruta para verificar Python + r.GET("/verify", VerificarPythonHandler) + // Configurar rutas de API api := r.Group("/api") diff --git a/backend/routes/index.routes.go b/backend/routes/index.routes.go index 03e5a4d..ef91ed1 100644 --- a/backend/routes/index.routes.go +++ b/backend/routes/index.routes.go @@ -2,23 +2,24 @@ package routes import ( "fmt" - "net/http" "os/exec" + + "github.com/gin-gonic/gin" ) -func Homehandler(w http.ResponseWriter, r *http.Request) { - w.Write([]byte("esto vien de la carpeta routesss")) +func Homehandler(c *gin.Context) { + c.String(200, "esto vien de la carpeta routesss") } -func VerificarPythonHandler(w http.ResponseWriter, r *http.Request) { +func VerificarPythonHandler(c *gin.Context) { rutaArchivo := "test.py" // En la práctica lo recibes por POST o desde un path cmd := exec.Command("pyright", rutaArchivo) salida, err := cmd.CombinedOutput() if err != nil { - http.Error(w, fmt.Sprintf("Errores detectados:\n%s", salida), http.StatusBadRequest) + c.String(400, fmt.Sprintf("Errores detectados:\n%s", salida)) return } - w.Write([]byte(fmt.Sprintf("Sin errores:\n%s", salida))) + c.String(200, fmt.Sprintf("Sin errores:\n%s", salida)) } diff --git a/backend/routes/lsp.go b/backend/routes/lsp.go index a60e7e7..3e8a119 100644 --- a/backend/routes/lsp.go +++ b/backend/routes/lsp.go @@ -5,6 +5,7 @@ import ( "net/http" "os/exec" + "github.com/gin-gonic/gin" "github.com/gorilla/websocket" ) @@ -14,14 +15,17 @@ var upgrader = websocket.Upgrader{ }, } -func LSPHandler(w http.ResponseWriter, r *http.Request) { - conn, err := upgrader.Upgrade(w, r, nil) +// LSPHandler maneja las conexiones WebSocket para el Language Server Protocol +func LSPHandler(c *gin.Context) { + // Obtener la conexión WebSocket desde Gin + conn, err := upgrader.Upgrade(c.Writer, c.Request, nil) if err != nil { log.Println("Error al actualizar a WebSocket:", err) return } defer conn.Close() + // Iniciar el servidor Pyright cmd := exec.Command("pyright-langserver", "--stdio") stdin, err := cmd.StdinPipe() if err != nil { @@ -39,6 +43,7 @@ func LSPHandler(w http.ResponseWriter, r *http.Request) { return } + // Goroutine para leer mensajes del WebSocket y enviarlos a Pyright go func() { for { _, message, err := conn.ReadMessage() @@ -54,6 +59,7 @@ func LSPHandler(w http.ResponseWriter, r *http.Request) { } }() + // Leer respuestas de Pyright y enviarlas al WebSocket buf := make([]byte, 4096) for { n, err := stdout.Read(buf) diff --git a/frontend/angular/src/app/services/python-lsp.service.ts b/frontend/angular/src/app/services/python-lsp.service.ts index de03f4c..4e691ec 100644 --- a/frontend/angular/src/app/services/python-lsp.service.ts +++ b/frontend/angular/src/app/services/python-lsp.service.ts @@ -1,12 +1,27 @@ import { Injectable } from '@angular/core'; import { Subject } from 'rxjs'; +// Interfaz para el resultado de una solicitud de autocompletado +export interface CompletionResponse { + items: { label: string }[]; +} + +// Interfaz para los mensajes LSP +export interface LSPMessage { + jsonrpc: string; + id?: number | string; + method?: string; + params?: unknown; + result?: CompletionResponse | unknown; + error?: unknown; +} + @Injectable({ providedIn: 'root' }) export class PythonLSPService { private socket: WebSocket; - private messageSubject = new Subject(); + private messageSubject = new Subject(); private initialized = false; constructor() { @@ -43,7 +58,7 @@ export class PythonLSPService { this.socket.onmessage = (event) => { try { - const message = JSON.parse(event.data); + const message: LSPMessage = JSON.parse(event.data); this.messageSubject.next(message); } catch (error) { console.error('Error parsing LSP message:', error); @@ -55,7 +70,7 @@ export class PythonLSPService { }; } - public sendRequest(params: any): void { + public sendRequest(params: LSPMessage): void { if (this.socket.readyState === WebSocket.OPEN) { this.socket.send(JSON.stringify(params)); } diff --git a/frontend/angular/src/app/shared/editor/editor.component.ts b/frontend/angular/src/app/shared/editor/editor.component.ts index 2e45240..7214a66 100644 --- a/frontend/angular/src/app/shared/editor/editor.component.ts +++ b/frontend/angular/src/app/shared/editor/editor.component.ts @@ -5,7 +5,7 @@ import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { CodemirrorModule, CodemirrorComponent } from '@ctrl/ngx-codemirror'; import { HttpClient } from '@angular/common/http'; -import { PythonLSPService } from '../../services/python-lsp.service'; +import { PythonLSPService, LSPMessage, CompletionResponse } from '../../services/python-lsp.service'; import { Subscription } from 'rxjs'; // Importacion de lint y modo python para CodeMirror @@ -173,7 +173,7 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy { editorOptions: any; codeEditor: any; private lspSubscription: Subscription | null = null; - private completionItems: any[] = []; + private completionItems: { label: string }[] = []; constructor( private http: HttpClient, @@ -262,9 +262,11 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy { console.log('[Editor] Pyodide cargado desde CDN'); // Suscribirse a los mensajes del LSP - this.lspSubscription = this.pythonLSP.onMessage().subscribe(message => { - if (message.method === 'textDocument/completion') { - this.completionItems = message.result?.items || []; + this.lspSubscription = this.pythonLSP.onMessage().subscribe((message: LSPMessage) => { + if (message.method === 'textDocument/completion' && message.result) { + // Asegurarse de que el resultado sea del tipo esperado + const completionResult = message.result as CompletionResponse; + this.completionItems = completionResult.items || []; } }); } From b60339303214656c72e4e03dd00be47d6d451a91 Mon Sep 17 00:00:00 2001 From: Isac Poly Bolivar Puma <119465061+polycyllo@users.noreply.github.com> Date: Thu, 19 Jun 2025 23:55:00 -0400 Subject: [PATCH 04/11] solucionado --- .../introduction/introduction.component.html | 30 ++- .../introduction/introduction.component.scss | 239 ++++++++++-------- .../introduction/introduction.component.ts | 55 +++- .../app/shared/editor/editor.component.html | 85 +++++-- .../app/shared/editor/editor.component.scss | 194 ++++++++++---- .../src/app/shared/editor/editor.component.ts | 90 ++++++- 6 files changed, 492 insertions(+), 201 deletions(-) diff --git a/frontend/angular/src/app/pages/introduction/introduction.component.html b/frontend/angular/src/app/pages/introduction/introduction.component.html index ea664ba..6ee10f4 100644 --- a/frontend/angular/src/app/pages/introduction/introduction.component.html +++ b/frontend/angular/src/app/pages/introduction/introduction.component.html @@ -50,22 +50,32 @@

Ejemplo

Siguiente
+

Instrucciones del ejercicio

{{ curso?.instrucciones || 'Realiza el ejercicio según lo aprendido en esta lección.' }}

- - + + + -
-

Salida

-
{{ salidaCodigo || 'Ejecuta el código para ver el resultado.' }}
-
+ + @if (resultadoVerificacion) { +
+

{{ resultadoVerificacion }}

+
+ }
- + \ No newline at end of file diff --git a/frontend/angular/src/app/pages/introduction/introduction.component.scss b/frontend/angular/src/app/pages/introduction/introduction.component.scss index ca280dc..7d7d3ea 100644 --- a/frontend/angular/src/app/pages/introduction/introduction.component.scss +++ b/frontend/angular/src/app/pages/introduction/introduction.component.scss @@ -1,119 +1,160 @@ +// Estilos adicionales para el componente de introducción + .intro-template { - // background-color: red; - height: 100%; - display: grid; - grid-template-columns: 1fr 1fr; - grid-template-areas: - "left right" - "left right"; - overflow: hidden; -} + display: flex; + gap: 2rem; + min-height: 100vh; + padding: 2rem; -.intro-left { - grid-area: left; - padding: 1rem; + @media (max-width: 768px) { + flex-direction: column; + padding: 1rem; + } } -.intro-left h1 { - color: #fff; - font-size: 2.5rem; - margin-bottom: 0.5rem; - border-bottom: 2px solid #333; - padding-bottom: 0.5rem; - background: linear-gradient(135deg, #ff6ec4, #7873f5); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} +.intro-left { + flex: 1; + + h1 { + color: #333; + margin-bottom: 1rem; + } -.intro-left h2 { - color: #ff6ec4; - font-size: 1.8rem; - margin: 0.5rem 0 1rem 0; -} + h2 { + color: #555; + margin: 1.5rem 0 0.5rem 0; + } -.intro-left h3 { - color: #7873f5; - font-size: 1.3rem; - margin-bottom: 0.1rem; -} + p { + line-height: 1.6; + margin-bottom: 1rem; + } -.intro-left p { - color: #ccc; - line-height: 1.2; - font-size: 1.1rem; -} + .example-box { + background: #f8f9fa; + border: 1px solid #e9ecef; + border-radius: 8px; + padding: 1rem; + margin: 1rem 0; -.intro-left .example-box { - background: linear-gradient(135deg, #1a1a1a, #2a2a2a); - border: 1px solid #444; - border-radius: 0.8rem; - padding-top: 0.5rem; - padding-left: 1rem; - padding-right: 1rem; - padding-bottom: 0.5rem; - margin: 1rem 0; - box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3); - border-left: 4px solid #ff6ec4; - - h3 { - color: #ff6ec4; - margin-top: 0; - margin-bottom: 0.5rem; - } -} + h3 { + margin-top: 0; + color: #495057; + } -.intro-left .code-block { - background: #0d1117; - border: 1px solid #30363d; - border-radius: 0.5rem; - padding: 1.2rem; - margin: 0.5rem 0; - overflow-x: auto; - position: relative; - - &::before { - content: ""; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 3px; - background: linear-gradient(90deg, #ff6ec4, #7873f5); - border-radius: 0.5rem 0.5rem 0 0; - } + .code-block { + background: #f1f3f4; + border-radius: 4px; + padding: 0.5rem; - pre { - color: #c9d1d9; - font-family: "Courier New", monospace; - margin: 0; - white-space: pre-wrap; - font-size: 0.95rem; - line-height: 1.5; + pre { + margin: 0; + font-family: 'Courier New', monospace; + font-size: 0.9rem; + color: #2d3748; + white-space: pre-wrap; + } + } } } .intro-right { - grid-area: right; - padding: 1rem; - overflow: hidden; -} - -.intro-right .buttons-field { + flex: 1; display: flex; + flex-direction: column; gap: 1rem; - justify-content: center; - align-items: center; -} -.editor-block { - background: #2c2c2c; - padding: 1rem; - border-radius: 8px; -} + .buttons-field { + display: flex; + gap: 1rem; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; + + .btn { + padding: 0.5rem 1rem; + border-radius: 4px; + border: 1px solid #007bff; + background: #007bff; + color: white; + cursor: pointer; + transition: all 0.3s; + + &:hover:not(:disabled) { + background: #0056b3; + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } + } + } + + .editor-field { + flex: 1; + display: flex; + flex-direction: column; + gap: 1rem; + + .editor-block { + background: #f8f9fa; + padding: 1rem; + border-radius: 8px; + border: 1px solid #e9ecef; + + h3 { + margin-top: 0; + color: #495057; + } -.btn { - border: 1px solid white; - color: white !important; + p { + margin-bottom: 0; + line-height: 1.5; + } + } + + .verification-result { + padding: 0.75rem; + border-radius: 4px; + text-align: center; + font-weight: bold; + + p { + margin: 0; + } + + // Estilos condicionales que puedes agregar con ngClass + &.correct { + background: #d4edda; + color: #155724; + border: 1px solid #c3e6cb; + } + + &.incorrect { + background: #f8d7da; + color: #721c24; + border: 1px solid #f5c6cb; + } + } + } } +// Responsive design +@media (max-width: 768px) { + .intro-template { + .intro-left, .intro-right { + flex: none; + width: 100%; + } + + .buttons-field { + flex-direction: column; + gap: 0.5rem; + + .btn { + width: 100%; + } + } + } +} \ No newline at end of file diff --git a/frontend/angular/src/app/pages/introduction/introduction.component.ts b/frontend/angular/src/app/pages/introduction/introduction.component.ts index 612ef2d..a027b30 100644 --- a/frontend/angular/src/app/pages/introduction/introduction.component.ts +++ b/frontend/angular/src/app/pages/introduction/introduction.component.ts @@ -1,19 +1,18 @@ import { Component } from '@angular/core'; import introductionMock from './introduccionMock.json'; import { MatButtonModule } from '@angular/material/button'; -import { EditorActividadComponent } from '../../components/editor-actividad/editor-actividad.component'; import { SearchComponent } from '../../components/search/search.component'; +import { EditorComponent } from '../../shared/editor/editor.component'; // Solo este import + interface Curso { instrucciones: string; codigo_incompleto: string; solucion_correcta: string; } - - @Component({ selector: 'app-introduction', - imports: [MatButtonModule, EditorActividadComponent, SearchComponent], + imports: [MatButtonModule, SearchComponent, EditorComponent], // Solo EditorComponent templateUrl: './introduction.component.html', styleUrl: './introduction.component.scss', }) @@ -25,12 +24,12 @@ export class IntroductionComponent { totalSubjects = this.intro.length - 1; salidaCodigo = ''; content = this.intro[this.initialSubject]; + resultadoVerificacion = ''; allCursos = this.intro.map((e) => { return e.title; }); - gonext() { this.initialSubject++; this.resetCnt++; @@ -45,10 +44,54 @@ export class IntroductionComponent { update() { this.content = this.intro[this.initialSubject]; + this.cargarEjercicio(); + } + + cargarEjercicio() { + // Simulación de carga de ejercicio específico + if (this.initialSubject === 0) { + this.curso = { + instrucciones: 'Crea un comentario y luego imprime "Hola mundo"', + codigo_incompleto: '# Escribe tu comentario aquí\n# Completa el código\n', + solucion_correcta: '# Este es mi comentario\nprint("Hola mundo")' + }; + } else if (this.initialSubject === 1) { + this.curso = { + instrucciones: 'Crea dos variables: una numérica (x) y una de texto (y), luego imprímelas', + codigo_incompleto: '# Crea las variables aquí\nx = \ny = \n# Imprime las variables\n', + solucion_correcta: 'x = 5\ny = "Juan"\nprint(x)\nprint(y)' + }; + } else { + this.curso = { + instrucciones: 'Realiza el ejercicio según lo aprendido en esta lección.', + codigo_incompleto: '# Escribe tu código aquí\n', + solucion_correcta: 'print("Ejercicio completado")' + }; + } } handleSelection(index: number) { this.initialSubject = index; this.update(); } -} + + // Manejar salida del código + onCodeOutput(output: string) { + this.salidaCodigo = output; + } + + // Manejar verificación de solución + onSolutionCheck(result: {correct: boolean, output: string}) { + if (result.correct) { + this.resultadoVerificacion = '¡Correcto! ✅'; + } else { + this.resultadoVerificacion = 'Incorrecto. Inténtalo de nuevo. ❌'; + } + this.salidaCodigo = result.output; + } + + // Inicializar el ejercicio al cargar el componente + ngOnInit() { + this.cargarEjercicio(); + } +} \ No newline at end of file diff --git a/frontend/angular/src/app/shared/editor/editor.component.html b/frontend/angular/src/app/shared/editor/editor.component.html index d47c939..5a80301 100644 --- a/frontend/angular/src/app/shared/editor/editor.component.html +++ b/frontend/angular/src/app/shared/editor/editor.component.html @@ -1,7 +1,10 @@ -
-

Editor de Código Python

+
+ + +

Editor de Código Python

-
+
+ Editor de Código Python [options]="cmOptions" (editorInitialized)="onEditorInit($event)"> -
+ + +

Input:

Output:
- + +
+ + - + + + + + + + +
- -
-

Input:

- - -
+ +
+

Output:

+
{{ output || 'Ejecuta el código para ver el resultado.' }}
+
-
-

Output:

-
{{ outputChat }}
-
-
+ +
+
+

Chat Input:

+ + +
+ +
+

Chat Output:

+
{{ outputChat }}
+
+
+
\ No newline at end of file diff --git a/frontend/angular/src/app/shared/editor/editor.component.scss b/frontend/angular/src/app/shared/editor/editor.component.scss index 9641e3c..e9094b9 100644 --- a/frontend/angular/src/app/shared/editor/editor.component.scss +++ b/frontend/angular/src/app/shared/editor/editor.component.scss @@ -2,12 +2,38 @@ flex-direction: column; gap: 10px; height: 100%; + + &.activity-mode { + background: #1e1e1e; + color: white; + padding: 1rem; + border-radius: 10px; + height: auto; + } + + .instructions { + font-size: 1rem; + color: #dddddd; + margin-bottom: 1rem; + padding: 0.5rem; + background: rgba(255, 255, 255, 0.05); + border-radius: 4px; + } + .editor { display: flex; flex-direction: row; gap: 1rem; min-width: 100%; height: 70vh; + + &.activity-layout { + height: 300px; + .cm-panel { + width: 100%; + } + } + .cm-panel { border: 1px solid #ffffff32; border-radius: 12px; @@ -15,20 +41,24 @@ width: 70%; height: 100%; } + .output-panel { flex: 1; display: flex; flex-direction: column; height: 100%; + .section { flex: 1; display: flex; flex-direction: column; margin-bottom: 1rem; min-height: 0; + &:last-child { margin-bottom: 0; } + h3 { font-size: 1.2rem; margin-bottom: 0.5rem; @@ -36,11 +66,13 @@ color: #dddddd; flex-shrink: 0; } + .cm-panel { flex: 1; width: 100%; min-height: 0; } + pre { background: #181335; border: 1px solid #ffffff32; @@ -55,6 +87,19 @@ } } } + + .botones { + display: flex; + gap: 1rem; + justify-content: center; + margin: 30px auto; + flex-shrink: 0; + + .activity-mode & { + margin: 1rem 0; + } + } + .btn-ejecutar { align-items: center; background-color: #df881d; @@ -72,35 +117,106 @@ white-space: nowrap; cursor: pointer; transition: all 0.3s; - margin: 30px auto; flex-shrink: 0; + + span { + background-color: #2e2143; + padding: 16px 24px; + border-radius: 6px; + width: 100%; + height: 100%; + transition: 300ms; + } + + &:hover { + outline: 0; + color: white; + + span { + background: none; + } + } + + &:active { + transform: scale(0.9); + } } - button:active, - button:hover { - outline: 0; - color: white; - } - button span { - background-color: #2e2143; - padding: 16px 24px; - border-radius: 6px; - width: 100%; - height: 100%; - transition: 300ms; + + .btn-actividad { + padding: 0.5rem 1rem; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 1rem; + transition: background-color 0.3s; + + &.btn-enviar { + background-color: #4CAF50; + color: white; + + &:hover { + background-color: #45a049; + } + } + + &.btn-restablecer { + background-color: #f44336; + color: white; + + &:hover { + background-color: #da190b; + } + } } - button:hover span { - background: none; + .resultado { + font-weight: bold; + padding: 0.5rem; + border-radius: 4px; + text-align: center; + margin-top: 1rem; } - button:active { - transform: scale(0.9); + .chat-section { + margin-top: 2rem; + + .section { + display: flex; + flex-direction: column; + margin-bottom: 1rem; + + h3 { + font-size: 1.2rem; + margin-bottom: 0.5rem; + margin-top: 0; + color: #dddddd; + } + + .cm-panel { + height: 150px; + width: 100%; + } + + pre { + background: #181335; + border: 1px solid #ffffff32; + border-radius: 4px; + color: #98c379; + padding: 1rem; + margin: 0; + overflow-y: auto; + min-height: 150px; + } + } } } + +// Estilos de CodeMirror .cm-s-material.CodeMirror { background: #181335; color: #abb2bf; } + ::ng-deep .CodeMirror { background-color: #181335 !important; color: #abb2bf !important; @@ -108,16 +224,20 @@ border-radius: 8px; height: 100%; } + ::ng-deep .CodeMirror-gutters { background-color: #181335 !important; border-right: 1px solid #ffffff32; } + ::ng-deep .CodeMirror-linenumber { color: #abb2bf !important; } + ::ng-deep .CodeMirror-cursor { border-left: 2px solid #df881d !important; } + ::ng-deep .CodeMirror-placeholder { color: #abb2bf !important; } @@ -137,42 +257,4 @@ max-height: 20em; overflow-y: auto; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); -} - -::ng-deep .CodeMirror-hint { - margin: 0; - padding: 4px 6px; - border-radius: 2px; - white-space: pre; - color: #abb2bf; - cursor: pointer; -} - -::ng-deep li.CodeMirror-hint-active { - background: #2e2143; - color: #df881d; -} - -@media screen and (max-width: 768px) { - .editor-container { - height: auto; - h2 { - font-size: 1.2rem; - } - .editor { - flex-direction: column; - height: auto; - .cm-panel { - width: 100%; - height: 300px; - } - .output-panel { - width: 100%; - height: auto; - .section { - margin-bottom: 1rem; - } - } - } - } -} +} \ No newline at end of file diff --git a/frontend/angular/src/app/shared/editor/editor.component.ts b/frontend/angular/src/app/shared/editor/editor.component.ts index 7214a66..2fa0772 100644 --- a/frontend/angular/src/app/shared/editor/editor.component.ts +++ b/frontend/angular/src/app/shared/editor/editor.component.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -// EditorComponent.ts -import { Component, OnInit, AfterViewInit, ViewChild, OnDestroy } from '@angular/core'; +// EditorComponent.ts - Versión Reutilizable +import { Component, OnInit, AfterViewInit, ViewChild, OnDestroy, Input, Output, EventEmitter, OnChanges, SimpleChanges } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { CodemirrorModule, CodemirrorComponent } from '@ctrl/ngx-codemirror'; @@ -82,8 +82,24 @@ function pythonLint(code: string) { templateUrl: './editor.component.html', styleUrls: ['./editor.component.scss'], }) -export class EditorComponent implements OnInit, AfterViewInit, OnDestroy { +export class EditorComponent implements OnInit, AfterViewInit, OnDestroy, OnChanges { @ViewChild('codeEditor') private codeEditorComponent!: CodemirrorComponent; + + // Inputs para hacer el componente reutilizable + @Input() mode: 'full' | 'activity' = 'full'; // Modo del editor + @Input() initialCode: string = ''; // Código inicial + @Input() correctSolution: string = ''; // Solución correcta (para modo actividad) + @Input() showChat: boolean = true; // Mostrar funcionalidad de chat + @Input() showInputOutput: boolean = true; // Mostrar paneles de input/output + @Input() height: string = '70vh'; // Altura del editor + @Input() placeholder: string = 'Escribe tu código…'; // Placeholder + + // Outputs para comunicación con el componente padre + @Output() codeChange = new EventEmitter(); + @Output() codeOutput = new EventEmitter(); + @Output() codeExecuted = new EventEmitter<{code: string, output: string}>(); + @Output() solutionCheck = new EventEmitter<{correct: boolean, output: string}>(); + codigo = ''; inputs = ''; output = ''; @@ -99,7 +115,7 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy { styleActiveLine: true, matchBrackets: true, viewportMargin: Infinity, - placeholder: 'Escribe tu código…', + placeholder: this.placeholder, gutters: ['CodeMirror-lint-markers'], lint: { getAnnotations: pythonLint, @@ -254,6 +270,15 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy { }; } + ngOnChanges(changes: SimpleChanges) { + if (changes['initialCode'] && changes['initialCode'].currentValue !== undefined) { + this.codigo = this.initialCode; + } + if (changes['placeholder']) { + this.cmOptions.placeholder = this.placeholder; + } + } + async ngOnInit() { // @ts-expect-error: loadPyodide no está definido en el contexto de TypeScript this.pyodide = await loadPyodide(); @@ -261,6 +286,11 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy { (window as any).pyodideInstance = this.pyodide; console.log('[Editor] Pyodide cargado desde CDN'); + // Inicializar código si se proporciona + if (this.initialCode) { + this.codigo = this.initialCode; + } + // Suscribirse a los mensajes del LSP this.lspSubscription = this.pythonLSP.onMessage().subscribe((message: LSPMessage) => { if (message.method === 'textDocument/completion' && message.result) { @@ -297,9 +327,11 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy { } }); - // Notificar cambios al LSP + // Notificar cambios al LSP y emitir evento this.codeEditor.on('change', (cm: any) => { this.notifyLSPChanges(cm); + this.codigo = cm.getValue(); + this.codeChange.emit(this.codigo); }); } }, 100); @@ -361,6 +393,7 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy { async ejecutarCodigo(): Promise { if (!this.pyodide) { this.output = 'Pyodide no está cargado correctamente.'; + this.codeOutput.emit(this.output); return; } @@ -386,12 +419,57 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy { }); await this.pyodide.runPythonAsync(this.codigo); + + // Emitir eventos + this.codeOutput.emit(this.output); + this.codeExecuted.emit({code: this.codigo, output: this.output}); + } catch (error) { this.output = `Error: ${String(error)}`; + this.codeOutput.emit(this.output); + this.codeExecuted.emit({code: this.codigo, output: this.output}); + } + } + + // Nueva función para verificar solución (modo actividad) + async verificarSolucion(): Promise { + if (!this.correctSolution) { + console.warn('No se ha proporcionado una solución correcta'); + return; } + + await this.ejecutarCodigo(); + + try { + // Ejecutar la solución correcta + let correctOutput = ''; + this.pyodide.setStdout({ + batched: (text: string) => { + correctOutput += text; + }, + }); + + await this.pyodide.runPythonAsync(this.correctSolution); + + // Comparar salidas + const isCorrect = this.output.trim() === correctOutput.trim(); + this.solutionCheck.emit({correct: isCorrect, output: this.output}); + + } catch (error) { + console.error('Error al verificar solución:', error); + this.solutionCheck.emit({correct: false, output: this.output}); + } + } + + // Función para restablecer el código + restablecerCodigo(): void { + this.codigo = this.initialCode; + this.output = ''; + this.codeChange.emit(this.codigo); + this.codeOutput.emit(''); } - chat(): void{ + chat(): void { if (!this.inputChat.trim()) { this.outputChat = 'Por favor, escribe un mensaje.'; return; From 9a0b13b6e642c6df910f800ecab87ac4e97ababf Mon Sep 17 00:00:00 2001 From: Isac Poly Bolivar Puma <119465061+polycyllo@users.noreply.github.com> Date: Fri, 20 Jun 2025 00:18:47 -0400 Subject: [PATCH 05/11] fix:texto ejemplo --- .../introduction/introduction.component.scss | 59 +++- .../app/shared/editor/editor.component.scss | 299 +++++++++++++++++- 2 files changed, 354 insertions(+), 4 deletions(-) diff --git a/frontend/angular/src/app/pages/introduction/introduction.component.scss b/frontend/angular/src/app/pages/introduction/introduction.component.scss index 7d7d3ea..aa6503e 100644 --- a/frontend/angular/src/app/pages/introduction/introduction.component.scss +++ b/frontend/angular/src/app/pages/introduction/introduction.component.scss @@ -43,16 +43,21 @@ } .code-block { - background: #f1f3f4; + background: #000000; // Cambiado a negro + border: 1px solid #333333; // Borde más oscuro border-radius: 4px; padding: 0.5rem; - + overflow: hidden; // Sin scroll horizontal + max-height: none; // Sin límite de altura + pre { margin: 0; font-family: 'Courier New', monospace; font-size: 0.9rem; - color: #2d3748; + color: #ffffff; // Texto blanco sobre fondo negro white-space: pre-wrap; + overflow: visible; // Sin scroll + word-wrap: break-word; // Ajustar palabras largas } } } @@ -140,6 +145,54 @@ } } +// Estilos específicos para la terminal/editor en la parte derecha +.intro-right { + .editor-field { + // Asegurar que el contenedor del editor tenga fondo negro + ::ng-deep .CodeMirror { + background-color: #000000 !important; // Fondo negro + color: #ffffff !important; // Texto blanco + border: 1px solid #333333; + border-radius: 8px; + overflow: hidden !important; // Sin scroll + max-height: none !important; // Sin límite de altura + } + + ::ng-deep .CodeMirror-gutters { + background-color: #000000 !important; // Gutter negro + border-right: 1px solid #333333; + } + + ::ng-deep .CodeMirror-linenumber { + color: #888888 !important; // Números de línea grises + } + + ::ng-deep .CodeMirror-cursor { + border-left: 2px solid #ffffff !important; // Cursor blanco + } + + ::ng-deep .CodeMirror-placeholder { + color: #888888 !important; // Placeholder gris + } + + // Eliminar scroll de la sección de output + .section { + pre { + background: #000000; // Fondo negro para output + border: 1px solid #333333; + border-radius: 4px; + color: #ffffff; // Texto blanco + padding: 1rem; + margin: 0; + overflow: visible !important; // Sin scroll + max-height: none !important; // Sin límite de altura + white-space: pre-wrap; // Ajustar texto + word-wrap: break-word; // Ajustar palabras largas + } + } + } +} + // Responsive design @media (max-width: 768px) { .intro-template { diff --git a/frontend/angular/src/app/shared/editor/editor.component.scss b/frontend/angular/src/app/shared/editor/editor.component.scss index e9094b9..42aea50 100644 --- a/frontend/angular/src/app/shared/editor/editor.component.scss +++ b/frontend/angular/src/app/shared/editor/editor.component.scss @@ -1,3 +1,6 @@ +// =============================== +// ESTILOS BASE DEL EDITOR +// =============================== .editor-container { flex-direction: column; gap: 10px; @@ -211,7 +214,196 @@ } } -// Estilos de CodeMirror +// =============================== +// ESTILOS DE ACTIVIDAD INTEGRADOS +// =============================== +.actividad-container { + display: flex; + flex-direction: column; + gap: 1rem; + background: #1e1e1e; + color: white; + padding: 1rem; + border-radius: 10px; +} + +.editor-panel { + border-radius: 8px; +} + +// =============================== +// ESTILOS DE INTRODUCCIÓN INTEGRADOS +// =============================== +.intro-template { + height: 100%; + display: grid; + grid-template-columns: 1fr 1fr; + grid-template-areas: + "left right" + "left right"; + gap: 1rem; + background: #000; + color: white; +} + +.intro-left { + grid-area: left; + padding: 1rem; + background: #000; + + h1 { + color: #fff; + font-size: 2.5rem; + margin-bottom: 0.5rem; + border-bottom: 2px solid #333; + padding-bottom: 0.5rem; + background: linear-gradient(135deg, #ff6ec4, #7873f5); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + } + + h2 { + color: #ff6ec4; + font-size: 1.8rem; + margin: 1.5rem 0 1rem 0; + } + + h3 { + color: #7873f5; + font-size: 1.3rem; + margin-bottom: 0.1rem; + } + + p { + color: #ccc; + line-height: 1.6; + font-size: 1.1rem; + margin-bottom: 0.8rem; + } + + .example-box { + background: linear-gradient(135deg, #1a1a1a, #2a2a2a); + border: 1px solid #444; + border-radius: 0.8rem; + padding: 1rem; + margin: 1rem 0; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3); + border-left: 4px solid #ff6ec4; + + h3 { + color: #ff6ec4; + margin-top: 0; + margin-bottom: 0.5rem; + } + } + + .code-block { + background: #0d1117; + border: 1px solid #30363d; + border-radius: 0.5rem; + padding: 1.2rem; + margin: 0.5rem 0; + overflow-x: auto; + position: relative; + + &::before { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: linear-gradient(90deg, #ff6ec4, #7873f5); + border-radius: 0.5rem 0.5rem 0 0; + } + + pre { + color: #c9d1d9; + font-family: "Courier New", monospace; + margin: 0; + white-space: pre-wrap; + font-size: 0.95rem; + line-height: 1.5; + } + } +} + +.intro-right { + grid-area: right; + padding: 1rem; + display: flex; + flex-direction: column; + gap: 1rem; + background: #000; + + .buttons-field { + display: flex; + gap: 1rem; + justify-content: center; + align-items: center; + flex-wrap: wrap; + margin-bottom: 1rem; + } + + .editor-field { + flex: 1; + display: flex; + flex-direction: column; + gap: 1rem; + min-height: 0; + } + + .verification-result { + padding: 0.8rem; + border-radius: 0.5rem; + border-left: 4px solid #ff6ec4; + background: linear-gradient(135deg, #1a1a1a, #2a2a2a); + color: #ccc; + font-weight: 500; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); + + p { + margin: 0; + font-size: 1rem; + } + } +} + +.editor-block { + background: linear-gradient(135deg, #2c2c2c, #3a3a3a); + padding: 1.5rem; + border-radius: 8px; + border: 1px solid #444; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3); + + h3 { + color: #ff6ec4; + margin-top: 0; + margin-bottom: 0.8rem; + font-size: 1.2rem; + background: linear-gradient(135deg, #ff6ec4, #7873f5); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + } + + p { + color: #ccc; + line-height: 1.6; + margin: 0; + font-size: 1rem; + } +} + +.btn { + border: 1px solid white; + color: white !important; +} + +// =============================== +// ESTILOS DE CODEMIRROR +// =============================== .cm-s-material.CodeMirror { background: #181335; color: #abb2bf; @@ -257,4 +449,109 @@ max-height: 20em; overflow-y: auto; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); +} + +// =============================== +// ESTILOS ADICIONALES Y UTILIDADES +// =============================== +// Clases de utilidad que pueden ser útiles +.gradient-text { + background: linear-gradient(135deg, #ff6ec4, #7873f5); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.dark-panel { + background: #1e1e1e; + color: white; + padding: 1rem; + border-radius: 10px; +} + +.highlight-border { + border-left: 4px solid #ff6ec4; +} + +// Responsive design +@media (max-width: 768px) { + .intro-template { + grid-template-columns: 1fr; + grid-template-areas: + "left" + "right"; + height: auto; + } + + .intro-left { + max-height: 50vh; + + h1 { + font-size: 2rem; + } + + h2 { + font-size: 1.5rem; + } + } + + .intro-right { + .buttons-field { + flex-direction: column; + gap: 0.5rem; + + button, app-search { + width: 100%; + } + } + } + + .editor { + flex-direction: column; + height: auto; + + .cm-panel { + width: 100%; + height: 300px; + } + } +} + +@media (max-width: 480px) { + .intro-left { + padding: 0.5rem; + + h1 { + font-size: 1.8rem; + } + + .example-box { + padding: 0.8rem; + margin: 0.8rem 0; + } + + .code-block { + padding: 1rem; + + pre { + font-size: 0.85rem; + } + } + } + + .intro-right { + padding: 0.5rem; + } + + .editor-block { + padding: 1rem; + + h3 { + font-size: 1.1rem; + } + + p { + font-size: 0.9rem; + } + } } \ No newline at end of file From dda7bd6a7ae9933a3cfeee1b11c936da25737ee6 Mon Sep 17 00:00:00 2001 From: Isac Poly Bolivar Puma <119465061+polycyllo@users.noreply.github.com> Date: Fri, 20 Jun 2025 00:38:38 -0400 Subject: [PATCH 06/11] m --- .../src/app/pages/introduction/introduction.component.scss | 2 +- frontend/angular/src/app/shared/editor/editor.component.scss | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/frontend/angular/src/app/pages/introduction/introduction.component.scss b/frontend/angular/src/app/pages/introduction/introduction.component.scss index aa6503e..84493f2 100644 --- a/frontend/angular/src/app/pages/introduction/introduction.component.scss +++ b/frontend/angular/src/app/pages/introduction/introduction.component.scss @@ -5,7 +5,7 @@ gap: 2rem; min-height: 100vh; padding: 2rem; - + overflow: hidden; @media (max-width: 768px) { flex-direction: column; padding: 1rem; diff --git a/frontend/angular/src/app/shared/editor/editor.component.scss b/frontend/angular/src/app/shared/editor/editor.component.scss index 42aea50..c836b63 100644 --- a/frontend/angular/src/app/shared/editor/editor.component.scss +++ b/frontend/angular/src/app/shared/editor/editor.component.scss @@ -1,6 +1,4 @@ -// =============================== -// ESTILOS BASE DEL EDITOR -// =============================== + .editor-container { flex-direction: column; gap: 10px; From 277ac33dce08ab6e6d947c5d88737488ab6d9440 Mon Sep 17 00:00:00 2001 From: Isac Poly Bolivar Puma <119465061+polycyllo@users.noreply.github.com> Date: Fri, 20 Jun 2025 00:41:22 -0400 Subject: [PATCH 07/11] z --- .../src/app/pages/introduction/introduction.component.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/angular/src/app/pages/introduction/introduction.component.scss b/frontend/angular/src/app/pages/introduction/introduction.component.scss index 84493f2..96ba9fb 100644 --- a/frontend/angular/src/app/pages/introduction/introduction.component.scss +++ b/frontend/angular/src/app/pages/introduction/introduction.component.scss @@ -31,7 +31,7 @@ } .example-box { - background: #f8f9fa; + background: #2c2c2c; border: 1px solid #e9ecef; border-radius: 8px; padding: 1rem; @@ -103,7 +103,7 @@ gap: 1rem; .editor-block { - background: #f8f9fa; + background: #2c2c2c; padding: 1rem; border-radius: 8px; border: 1px solid #e9ecef; From 75057de0eecb1b41ee35ec1dd595f58f9e567cdf Mon Sep 17 00:00:00 2001 From: Isac Poly Bolivar Puma <119465061+polycyllo@users.noreply.github.com> Date: Fri, 20 Jun 2025 00:49:40 -0400 Subject: [PATCH 08/11] fix editor activida --- .../introduction/introduction.component.scss | 136 +++++++++++------- 1 file changed, 85 insertions(+), 51 deletions(-) diff --git a/frontend/angular/src/app/pages/introduction/introduction.component.scss b/frontend/angular/src/app/pages/introduction/introduction.component.scss index 96ba9fb..23610b1 100644 --- a/frontend/angular/src/app/pages/introduction/introduction.component.scss +++ b/frontend/angular/src/app/pages/introduction/introduction.component.scss @@ -1,65 +1,97 @@ // Estilos adicionales para el componente de introducción .intro-template { - display: flex; - gap: 2rem; - min-height: 100vh; - padding: 2rem; - overflow: hidden; - @media (max-width: 768px) { - flex-direction: column; - padding: 1rem; - } + // background-color: red; + height: 100%; + display: grid; + grid-template-columns: 1fr 1fr; + grid-template-areas: + "left right" + "left right"; + overflow: hidden; } .intro-left { - flex: 1; - - h1 { - color: #333; - margin-bottom: 1rem; - } + grid-area: left; + padding: 1rem; +} - h2 { - color: #555; - margin: 1.5rem 0 0.5rem 0; - } +.intro-left h1 { + color: #fff; + font-size: 2.5rem; + margin-bottom: 0.5rem; + border-bottom: 2px solid #333; + padding-bottom: 0.5rem; + background: linear-gradient(135deg, #ff6ec4, #7873f5); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} - p { - line-height: 1.6; - margin-bottom: 1rem; - } +.intro-left h2 { + color: #ff6ec4; + font-size: 1.8rem; + margin: 0.5rem 0 1rem 0; +} + +.intro-left h3 { + color: #7873f5; + font-size: 1.3rem; + margin-bottom: 0.1rem; +} - .example-box { - background: #2c2c2c; - border: 1px solid #e9ecef; - border-radius: 8px; - padding: 1rem; - margin: 1rem 0; +.intro-left p { + color: #ccc; + line-height: 1.2; + font-size: 1.1rem; +} - h3 { - margin-top: 0; - color: #495057; - } +.intro-left .example-box { + background: linear-gradient(135deg, #1a1a1a, #2a2a2a); + border: 1px solid #444; + border-radius: 0.8rem; + padding-top: 0.5rem; + padding-left: 1rem; + padding-right: 1rem; + padding-bottom: 0.5rem; + margin: 1rem 0; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3); + border-left: 4px solid #ff6ec4; + + h3 { + color: #ff6ec4; + margin-top: 0; + margin-bottom: 0.5rem; + } +} - .code-block { - background: #000000; // Cambiado a negro - border: 1px solid #333333; // Borde más oscuro - border-radius: 4px; - padding: 0.5rem; - overflow: hidden; // Sin scroll horizontal - max-height: none; // Sin límite de altura - - pre { - margin: 0; - font-family: 'Courier New', monospace; - font-size: 0.9rem; - color: #ffffff; // Texto blanco sobre fondo negro - white-space: pre-wrap; - overflow: visible; // Sin scroll - word-wrap: break-word; // Ajustar palabras largas - } - } +.intro-left .code-block { + background: #0d1117; + border: 1px solid #30363d; + border-radius: 0.5rem; + padding: 1.2rem; + margin: 0.5rem 0; + overflow-x: auto; + position: relative; + + &::before { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: linear-gradient(90deg, #ff6ec4, #7873f5); + border-radius: 0.5rem 0.5rem 0 0; + } + + pre { + color: #c9d1d9; + font-family: "Courier New", monospace; + margin: 0; + white-space: pre-wrap; + font-size: 0.95rem; + line-height: 1.5; } } @@ -68,6 +100,8 @@ display: flex; flex-direction: column; gap: 1rem; + grid-area: right; + padding: 1rem; .buttons-field { display: flex; From cca03ba4570ada472edf1ca44630805923aa4fb7 Mon Sep 17 00:00:00 2001 From: Isac Poly Bolivar Puma <119465061+polycyllo@users.noreply.github.com> Date: Fri, 20 Jun 2025 01:19:43 -0400 Subject: [PATCH 09/11] fix: editor en actividades --- .../introduction/introduction.component.html | 3 +- .../introduction/introduction.component.scss | 74 +++++++------------ .../introduction/introduction.component.ts | 11 ++- .../app/shared/editor/editor.component.scss | 30 ++++---- .../src/app/shared/editor/editor.component.ts | 3 +- 5 files changed, 48 insertions(+), 73 deletions(-) diff --git a/frontend/angular/src/app/pages/introduction/introduction.component.html b/frontend/angular/src/app/pages/introduction/introduction.component.html index 6ee10f4..6f0f0b5 100644 --- a/frontend/angular/src/app/pages/introduction/introduction.component.html +++ b/frontend/angular/src/app/pages/introduction/introduction.component.html @@ -57,7 +57,6 @@

Instrucciones del ejercicio

{{ curso?.instrucciones || 'Realiza el ejercicio según lo aprendido en esta lección.' }}

- Instrucciones del ejercicio (solutionCheck)="onSolutionCheck($event)"> - + @if (resultadoVerificacion) {

{{ resultadoVerificacion }}

diff --git a/frontend/angular/src/app/pages/introduction/introduction.component.scss b/frontend/angular/src/app/pages/introduction/introduction.component.scss index 23610b1..4e045fd 100644 --- a/frontend/angular/src/app/pages/introduction/introduction.component.scss +++ b/frontend/angular/src/app/pages/introduction/introduction.component.scss @@ -1,16 +1,19 @@ -// Estilos adicionales para el componente de introducción .intro-template { // background-color: red; height: 100%; display: grid; + grid-template-columns: 1fr 1fr; grid-template-areas: "left right" "left right"; - overflow: hidden; + //overflow: hidden; +} +.intro-left, +.intro-right { + min-width: 0; } - .intro-left { grid-area: left; padding: 1rem; @@ -144,7 +147,7 @@ h3 { margin-top: 0; - color: #495057; + color: #ff6ec4; } p { @@ -163,7 +166,7 @@ margin: 0; } - // Estilos condicionales que puedes agregar con ngClass + &.correct { background: #d4edda; color: #155724; @@ -179,55 +182,30 @@ } } -// Estilos específicos para la terminal/editor en la parte derecha .intro-right { - .editor-field { - // Asegurar que el contenedor del editor tenga fondo negro - ::ng-deep .CodeMirror { - background-color: #000000 !important; // Fondo negro - color: #ffffff !important; // Texto blanco - border: 1px solid #333333; - border-radius: 8px; - overflow: hidden !important; // Sin scroll - max-height: none !important; // Sin límite de altura - } - - ::ng-deep .CodeMirror-gutters { - background-color: #000000 !important; // Gutter negro - border-right: 1px solid #333333; - } - - ::ng-deep .CodeMirror-linenumber { - color: #888888 !important; // Números de línea grises - } + grid-area: right; + padding: 1rem; +} - ::ng-deep .CodeMirror-cursor { - border-left: 2px solid #ffffff !important; // Cursor blanco - } +.intro-right .buttons-field { + display: flex; + gap: 1rem; + justify-content: center; + align-items: center; +} - ::ng-deep .CodeMirror-placeholder { - color: #888888 !important; // Placeholder gris - } +.editor-block { + background: #2c2c2c; + padding: 1rem; + border-radius: 8px; +} - // Eliminar scroll de la sección de output - .section { - pre { - background: #000000; // Fondo negro para output - border: 1px solid #333333; - border-radius: 4px; - color: #ffffff; // Texto blanco - padding: 1rem; - margin: 0; - overflow: visible !important; // Sin scroll - max-height: none !important; // Sin límite de altura - white-space: pre-wrap; // Ajustar texto - word-wrap: break-word; // Ajustar palabras largas - } - } - } +.btn { + border: 1px solid white; + color: white !important; } -// Responsive design + @media (max-width: 768px) { .intro-template { .intro-left, .intro-right { diff --git a/frontend/angular/src/app/pages/introduction/introduction.component.ts b/frontend/angular/src/app/pages/introduction/introduction.component.ts index a027b30..ac73b6c 100644 --- a/frontend/angular/src/app/pages/introduction/introduction.component.ts +++ b/frontend/angular/src/app/pages/introduction/introduction.component.ts @@ -2,7 +2,7 @@ import { Component } from '@angular/core'; import introductionMock from './introduccionMock.json'; import { MatButtonModule } from '@angular/material/button'; import { SearchComponent } from '../../components/search/search.component'; -import { EditorComponent } from '../../shared/editor/editor.component'; // Solo este import +import { EditorComponent } from '../../shared/editor/editor.component'; interface Curso { instrucciones: string; @@ -12,7 +12,7 @@ interface Curso { @Component({ selector: 'app-introduction', - imports: [MatButtonModule, SearchComponent, EditorComponent], // Solo EditorComponent + imports: [MatButtonModule, SearchComponent, EditorComponent], templateUrl: './introduction.component.html', styleUrl: './introduction.component.scss', }) @@ -48,7 +48,7 @@ export class IntroductionComponent { } cargarEjercicio() { - // Simulación de carga de ejercicio específico + if (this.initialSubject === 0) { this.curso = { instrucciones: 'Crea un comentario y luego imprime "Hola mundo"', @@ -75,12 +75,12 @@ export class IntroductionComponent { this.update(); } - // Manejar salida del código + onCodeOutput(output: string) { this.salidaCodigo = output; } - // Manejar verificación de solución + onSolutionCheck(result: {correct: boolean, output: string}) { if (result.correct) { this.resultadoVerificacion = '¡Correcto! ✅'; @@ -90,7 +90,6 @@ export class IntroductionComponent { this.salidaCodigo = result.output; } - // Inicializar el ejercicio al cargar el componente ngOnInit() { this.cargarEjercicio(); } diff --git a/frontend/angular/src/app/shared/editor/editor.component.scss b/frontend/angular/src/app/shared/editor/editor.component.scss index c836b63..269b1ee 100644 --- a/frontend/angular/src/app/shared/editor/editor.component.scss +++ b/frontend/angular/src/app/shared/editor/editor.component.scss @@ -1,4 +1,5 @@ + .editor-container { flex-direction: column; gap: 10px; @@ -21,13 +22,16 @@ border-radius: 4px; } + .editor { display: flex; flex-direction: row; gap: 1rem; - min-width: 100%; + width: 100%; height: 70vh; - + max-width: 100%; + overflow-x: hidden; + flex: 1 1 0%; &.activity-layout { height: 300px; .cm-panel { @@ -212,9 +216,7 @@ } } -// =============================== -// ESTILOS DE ACTIVIDAD INTEGRADOS -// =============================== + .actividad-container { display: flex; flex-direction: column; @@ -229,9 +231,7 @@ border-radius: 8px; } -// =============================== -// ESTILOS DE INTRODUCCIÓN INTEGRADOS -// =============================== + .intro-template { height: 100%; display: grid; @@ -399,9 +399,6 @@ color: white !important; } -// =============================== -// ESTILOS DE CODEMIRROR -// =============================== .cm-s-material.CodeMirror { background: #181335; color: #abb2bf; @@ -449,10 +446,6 @@ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); } -// =============================== -// ESTILOS ADICIONALES Y UTILIDADES -// =============================== -// Clases de utilidad que pueden ser útiles .gradient-text { background: linear-gradient(135deg, #ff6ec4, #7873f5); -webkit-background-clip: text; @@ -514,7 +507,12 @@ } } } - +.CodeMirror, +ngx-codemirror, +.cm-panel { + width: 100% !important; + max-width: 100% !important; +} @media (max-width: 480px) { .intro-left { padding: 0.5rem; diff --git a/frontend/angular/src/app/shared/editor/editor.component.ts b/frontend/angular/src/app/shared/editor/editor.component.ts index 2fa0772..e7b50cd 100644 --- a/frontend/angular/src/app/shared/editor/editor.component.ts +++ b/frontend/angular/src/app/shared/editor/editor.component.ts @@ -111,10 +111,11 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy, OnChan theme: 'material', lineNumbers: true, indentUnit: 4, + lineWrapping: true, + viewportMargin: Infinity, tabSize: 4, styleActiveLine: true, matchBrackets: true, - viewportMargin: Infinity, placeholder: this.placeholder, gutters: ['CodeMirror-lint-markers'], lint: { From 7dffbf92792188464fc5f20659e9c9b3cdbfa8d4 Mon Sep 17 00:00:00 2001 From: Isac Poly Bolivar Puma <119465061+polycyllo@users.noreply.github.com> Date: Mon, 23 Jun 2025 18:47:10 -0400 Subject: [PATCH 10/11] fix: lint --- .../src/app/shared/editor/editor.component.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/angular/src/app/shared/editor/editor.component.ts b/frontend/angular/src/app/shared/editor/editor.component.ts index 59ccc0f..23cae05 100644 --- a/frontend/angular/src/app/shared/editor/editor.component.ts +++ b/frontend/angular/src/app/shared/editor/editor.component.ts @@ -87,12 +87,12 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy, OnChan // Inputs para hacer el componente reutilizable @Input() mode: 'full' | 'activity' = 'full'; // Modo del editor - @Input() initialCode: string = ''; // Código inicial - @Input() correctSolution: string = ''; // Solución correcta (para modo actividad) - @Input() showChat: boolean = true; // Mostrar funcionalidad de chat - @Input() showInputOutput: boolean = true; // Mostrar paneles de input/output - @Input() height: string = '70vh'; // Altura del editor - @Input() placeholder: string = 'Escribe tu código…'; // Placeholder + @Input() initialCode = ''; // Código inicial + @Input() correctSolution= ''; // Solución correcta (para modo actividad) + @Input() showChat = true; // Mostrar funcionalidad de chat + @Input() showInputOutput = true; // Mostrar paneles de input/output + @Input() height = '70vh'; // Altura del editor + @Input() placeholder = 'Escribe tu código…'; // Placeholder // Outputs para comunicación con el componente padre @Output() codeChange = new EventEmitter(); From 0c9cfb140e9dbff6b1b28987ebf19a97eb74ceff Mon Sep 17 00:00:00 2001 From: Isac Poly Bolivar Puma <119465061+polycyllo@users.noreply.github.com> Date: Mon, 23 Jun 2025 19:23:20 -0400 Subject: [PATCH 11/11] feat: limitar tiempo y memoria --- .../tabs/submit/submit.component.html | 2 +- .../app/shared/editor/editor.component.scss | 2 +- .../src/app/shared/editor/editor.component.ts | 227 +++++++++++++++--- 3 files changed, 198 insertions(+), 33 deletions(-) diff --git a/frontend/angular/src/app/pages/see-exercise/tabs/submit/submit.component.html b/frontend/angular/src/app/pages/see-exercise/tabs/submit/submit.component.html index c13198e..c3c6324 100644 --- a/frontend/angular/src/app/pages/see-exercise/tabs/submit/submit.component.html +++ b/frontend/angular/src/app/pages/see-exercise/tabs/submit/submit.component.html @@ -1,3 +1,3 @@
- +
diff --git a/frontend/angular/src/app/shared/editor/editor.component.scss b/frontend/angular/src/app/shared/editor/editor.component.scss index 8c78847..05476e0 100644 --- a/frontend/angular/src/app/shared/editor/editor.component.scss +++ b/frontend/angular/src/app/shared/editor/editor.component.scss @@ -568,4 +568,4 @@ ngx-codemirror, font-size: 0.9rem; } } -} \ No newline at end of file +} diff --git a/frontend/angular/src/app/shared/editor/editor.component.ts b/frontend/angular/src/app/shared/editor/editor.component.ts index 23cae05..02e89f3 100644 --- a/frontend/angular/src/app/shared/editor/editor.component.ts +++ b/frontend/angular/src/app/shared/editor/editor.component.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -// EditorComponent.ts - Versión Reutilizable +// EditorComponent.ts - Versión con límites usando Web Workers import { Component, OnInit, AfterViewInit, ViewChild, OnDestroy, Input, Output, EventEmitter, OnChanges, SimpleChanges } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; @@ -88,23 +88,32 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy, OnChan // Inputs para hacer el componente reutilizable @Input() mode: 'full' | 'activity' = 'full'; // Modo del editor @Input() initialCode = ''; // Código inicial - @Input() correctSolution= ''; // Solución correcta (para modo actividad) + @Input() correctSolution = ''; // Solución correcta (para modo actividad) @Input() showChat = true; // Mostrar funcionalidad de chat @Input() showInputOutput = true; // Mostrar paneles de input/output @Input() height = '70vh'; // Altura del editor @Input() placeholder = 'Escribe tu código…'; // Placeholder - // Outputs para comunicación con el componente padre + // Nuevos inputs para límites de ejecución + @Input() timeoutSeconds = 5; // Límite de tiempo en segundos + @Input() memoryLimitMB = 50; // Límite de memoria en MB + @Input() maxOutputLines = 1000; // Límite de líneas de output + + // Outputs para comunicación con el componente padre - CORREGIDOS @Output() codeChange = new EventEmitter(); @Output() codeOutput = new EventEmitter(); @Output() codeExecuted = new EventEmitter<{code: string, output: string}>(); @Output() solutionCheck = new EventEmitter<{correct: boolean, output: string}>(); + @Output() executionTimeout = new EventEmitter(); + @Output() memoryExceeded = new EventEmitter(); + @Output() outputLimitExceeded = new EventEmitter(); codigo = ''; inputs = ''; output = ''; inputChat = ''; outputChat = ''; + isExecuting = false; cmOptions = { mode: 'python', @@ -191,6 +200,8 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy, OnChan codeEditor: any; private lspSubscription: Subscription | null = null; private completionItems: { label: string }[] = []; + private executionWorker: Worker | null = null; + private executionTimeoutHandle: any = null; constructor( private http: HttpClient, @@ -342,6 +353,103 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy, OnChan if (this.lspSubscription) { this.lspSubscription.unsubscribe(); } + if (this.executionWorker) { + this.executionWorker.terminate(); + } + if (this.executionTimeoutHandle) { + clearTimeout(this.executionTimeoutHandle); + } + } + + private createSecurePythonCode(code: string): string { + // Código de seguridad que se inyecta para limitar ejecución + const secureWrapper = ` +import sys +import time +import threading +from io import StringIO + +# Límites de seguridad +MAX_OUTPUT_LINES = ${this.maxOutputLines} +MAX_MEMORY_MB = ${this.memoryLimitMB} +TIMEOUT_SECONDS = ${this.timeoutSeconds} + +# Variables de control +_output_lines = 0 +_start_time = time.time() +_original_stdout = sys.stdout +_string_io = StringIO() +_execution_stopped = False + +class LimitedStringIO(StringIO): + def write(self, s): + global _output_lines, _execution_stopped + if _execution_stopped: + return + + # Contar líneas + _output_lines += s.count('\\n') + if _output_lines > MAX_OUTPUT_LINES: + _execution_stopped = True + super().write(f"\\n\\n Límite de output excedido ({MAX_OUTPUT_LINES} líneas)\\n") + raise Exception("Output limit exceeded") + + # Verificar tiempo + if time.time() - _start_time > TIMEOUT_SECONDS: + _execution_stopped = True + super().write(f"\\n\\n Tiempo de ejecución excedido ({TIMEOUT_SECONDS}s)\\n") + raise Exception("Timeout exceeded") + + return super().write(s) + +# Reemplazar stdout +sys.stdout = LimitedStringIO() + +# Función de trace para monitorear ejecución línea por línea +def trace_calls(frame, event, arg): + global _execution_stopped + if _execution_stopped: + raise Exception("Execution stopped") + + # Verificar timeout en cada línea + if time.time() - _start_time > TIMEOUT_SECONDS: + _execution_stopped = True + raise Exception("Timeout exceeded") + + return trace_calls + +# Activar trace +sys.settrace(trace_calls) + +try: + # Ejecutar código del usuario +${code.split('\n').map(line => ' ' + line).join('\n')} + +except KeyboardInterrupt: + print("\\n\\n Ejecución interrumpida") +except Exception as e: + if "Timeout exceeded" in str(e): + print(f"\\n\\n Tiempo de ejecución excedido ({TIMEOUT_SECONDS}s)") + elif "Output limit exceeded" in str(e): + print(f"\\n\\n Límite de output excedido ({MAX_OUTPUT_LINES} líneas)") + elif "Execution stopped" in str(e): + print("\\n\\n Ejecución detenida") + else: + print(f"\\n\\nError: {e}") +finally: + # Limpiar + sys.settrace(None) + + # Obtener output + output_content = sys.stdout.getvalue() if hasattr(sys.stdout, 'getvalue') else '' + + # Restaurar stdout + sys.stdout = _original_stdout + + # Imprimir resultado + print(output_content, end='') +`; + return secureWrapper; } private requestCompletions(cm: any, cursor: any) { @@ -415,26 +523,76 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy, OnChan this.pyodide.globals.set('input', inputFunction); } - // Método privado para ejecutar código Python - private async executeCode(code: string): Promise { - let output = ''; - - // Configurar captura de stdout - this.pyodide.setStdout({ - batched: (text: string) => { - output += text; - }, - }); - - // Configurar input si hay inputs disponibles - if (this.inputs.trim()) { - this.setupInput(); + // Método para detener ejecución + stopExecution(): void { + if (this.executionWorker) { + this.executionWorker.terminate(); + this.executionWorker = null; } + if (this.executionTimeoutHandle) { + clearTimeout(this.executionTimeoutHandle); + this.executionTimeoutHandle = null; + } + this.isExecuting = false; + this.output += '\n\n⏹️ Ejecución detenida por el usuario'; + this.codeOutput.emit(this.output); + } - // Ejecutar código - await this.pyodide.runPythonAsync(code); - - return output; + // Método privado para ejecutar código Python con límites estrictos + private async executeCodeWithLimits(code: string): Promise { + return new Promise((resolve, reject) => { + this.isExecuting = true; + let output = ''; + + // Configurar captura de stdout + this.pyodide.setStdout({ + batched: (text: string) => { + output += text; + }, + }); + + // Configurar input si hay inputs disponibles + if (this.inputs.trim()) { + this.setupInput(); + } + + // Crear código seguro + const secureCode = this.createSecurePythonCode(code); + + // Timeout de JavaScript como respaldo + this.executionTimeoutHandle = setTimeout(() => { + this.isExecuting = false; + this.executionTimeout.emit(); + reject(new Error(`⏱️ Timeout de JavaScript: excedió ${this.timeoutSeconds} segundos`)); + }, this.timeoutSeconds * 1000); + + // Ejecutar código + this.pyodide.runPythonAsync(secureCode) + .then(() => { + clearTimeout(this.executionTimeoutHandle); + this.isExecuting = false; + resolve(output); + }) + .catch((error: any) => { + clearTimeout(this.executionTimeoutHandle); + this.isExecuting = false; + + const errorMessage = error.message || error.toString(); + + if (errorMessage.includes('Timeout exceeded') || errorMessage.includes('timeout')) { + this.executionTimeout.emit(); + reject(new Error(`⏱️ Tiempo de ejecución excedido (${this.timeoutSeconds}s)`)); + } else if (errorMessage.includes('Output limit exceeded')) { + this.outputLimitExceeded.emit(); + reject(new Error(`⚠️ Límite de output excedido (${this.maxOutputLines} líneas)`)); + } else if (errorMessage.includes('Memory limit') || errorMessage.includes('MemoryError')) { + this.memoryExceeded.emit(); + reject(new Error(`💾 Límite de memoria excedido (${this.memoryLimitMB}MB)`)); + } else { + reject(error); + } + }); + }); } async ejecutarCodigo(): Promise { @@ -444,22 +602,28 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy, OnChan return; } + if (this.isExecuting) { + this.stopExecution(); + return; + } + try { this.output = ''; - this.output = await this.executeCode(this.codigo); + this.output = await this.executeCodeWithLimits(this.codigo); // Emitir eventos this.codeOutput.emit(this.output); this.codeExecuted.emit({code: this.codigo, output: this.output}); - } catch (error) { - this.output = `Error: ${String(error)}`; + } catch (error: any) { + const errorMessage = String(error.message || error); + this.output = `Error: ${errorMessage}`; this.codeOutput.emit(this.output); this.codeExecuted.emit({code: this.codigo, output: this.output}); } } - // Nueva función para verificar solución (modo actividad) + // Nueva función para verificar solución (modo actividad) con límites async verificarSolucion(): Promise { if (!this.correctSolution) { console.warn('No se ha proporcionado una solución correcta'); @@ -473,13 +637,13 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy, OnChan } try { - // Ejecutar el código del usuario + // Ejecutar el código del usuario con límites this.output = ''; - const userOutput = await this.executeCode(this.codigo); + const userOutput = await this.executeCodeWithLimits(this.codigo); this.output = userOutput; - // Ejecutar la solución correcta - const correctOutput = await this.executeCode(this.correctSolution); + // Ejecutar la solución correcta (también con límites por seguridad) + const correctOutput = await this.executeCodeWithLimits(this.correctSolution); // Comparar salidas (normalizar espacios en blanco) const normalizeOutput = (str: string) => str.trim().replace(/\s+/g, ' '); @@ -489,8 +653,9 @@ export class EditorComponent implements OnInit, AfterViewInit, OnDestroy, OnChan this.codeOutput.emit(this.output); this.solutionCheck.emit({correct: isCorrect, output: this.output}); - } catch (error) { - this.output = `Error: ${String(error)}`; + } catch (error: any) { + const errorMessage = String(error.message || error); + this.output = `Error: ${errorMessage}`; this.codeOutput.emit(this.output); this.solutionCheck.emit({correct: false, output: this.output}); }