diff --git a/backend/main.go b/backend/main.go index 2207c26..9c03a43 100644 --- a/backend/main.go +++ b/backend/main.go @@ -29,9 +29,6 @@ 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! - log.Println("Servidor escuchando en :8080") log.Fatal(r.Run(":8080")) } diff --git a/backend/routes/cors.go b/backend/routes/cors.go index 7c9d1e9..905f72b 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 4d25847..3e8a119 100644 --- a/backend/routes/lsp.go +++ b/backend/routes/lsp.go @@ -5,19 +5,27 @@ import ( "net/http" "os/exec" + "github.com/gin-gonic/gin" "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) { - 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 { @@ -35,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() @@ -50,7 +59,8 @@ func LspHandler(w http.ResponseWriter, r *http.Request) { } }() - buf := make([]byte, 1024) + // Leer respuestas de Pyright y enviarlas al WebSocket + buf := make([]byte, 4096) for { n, err := stdout.Read(buf) if err != nil { 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/package-lock.json b/frontend/angular/package-lock.json index 590f4aa..f6ede42 100644 --- a/frontend/angular/package-lock.json +++ b/frontend/angular/package-lock.json @@ -18,6 +18,7 @@ "@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", "@ngx-translate/core": "^16.0.4", "@ngx-translate/http-loader": "^16.0.1", "@tsparticles/angular": "^3.0.0", @@ -26,6 +27,7 @@ "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" @@ -39,6 +41,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", @@ -2531,6 +2534,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", @@ -3775,6 +3836,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", @@ -3898,6 +3983,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", @@ -4644,6 +4752,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", @@ -6085,13 +6226,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": { @@ -8106,6 +8247,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", @@ -8532,7 +8679,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": { @@ -8543,7 +8689,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": { @@ -9702,7 +9847,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, @@ -10645,6 +10789,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", @@ -11764,6 +11927,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", @@ -12302,6 +12477,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", @@ -13433,6 +13628,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", @@ -13956,7 +14167,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": { @@ -14833,6 +15044,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", @@ -14970,6 +15187,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", @@ -15264,6 +15487,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", @@ -15437,9 +15666,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" }, @@ -16115,6 +16344,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", @@ -16157,6 +16417,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", @@ -16481,6 +16747,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 681d435..bbddccb 100644 --- a/frontend/angular/package.json +++ b/frontend/angular/package.json @@ -21,6 +21,7 @@ "@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", "@ngx-translate/core": "^16.0.4", "@ngx-translate/http-loader": "^16.0.1", "@tsparticles/angular": "^3.0.0", @@ -29,6 +30,7 @@ "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" @@ -42,6 +44,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", diff --git a/frontend/angular/src/app/pages/introduction/introduction.component.html b/frontend/angular/src/app/pages/introduction/introduction.component.html index 49f0eb2..f82e75b 100644 --- a/frontend/angular/src/app/pages/introduction/introduction.component.html +++ b/frontend/angular/src/app/pages/introduction/introduction.component.html @@ -1,81 +1,123 @@ - +
+ @for (curso of cursosDisponibles; track curso.id) { + } -
esto esta por si quieren mostrar los botones ahi arriba con mejor estilo claro--> + -
-
- @if (content) { -

{{ content.title }}

- @for (line of content.paragraph; track $index) { -

{{ line }}

- } @for (subcontent of content.subcontent; track $index) { -

{{ subcontent.subtitle }}

- @for (subpar of subcontent.subparagraph; track $index) { -

{{ subpar }}

- } @if (subcontent.example.length > 0) { -
-

Ejemplo

-
-
{{ subcontent.example[0] }}
-
-
- } } } + +@if (loading) { +
+

Cargando curso...

+} -
-
- + +@if (error && !loading) { +
+

{{ error }}

+ +
+} - + +@if (!loading && !error && content) { +
+
+

{{ content.title }}

+ + @for (line of content.paragraph; track $index) { +

{{ line }}

+ } - + @for (subcontent of content.subcontent; track $index) { +

{{ subcontent.subtitle }}

+ + @for (subpar of subcontent.subparagraph; track $index) { +

{{ subpar }}

+ } + + @if (subcontent.example && subcontent.example.length > 0) { +
+

Ejemplo

+
+
{{ subcontent.example[0] }}
+
+
+ } + }
-
-
-

Instrucciones del ejercicio

-

- {{ - content?.instrucciones || - "Realiza el ejercicio según lo aprendido en esta lección." - }} -

+
+
+ + + + +
+ +
+
+

Instrucciones del ejercicio

+

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

+
+ + + - + @if (resultadoVerificacion) { +
+

{{ resultadoVerificacion }}

+
+ } -
-

Salida

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

Salida del código:

+
{{ salidaCodigo }}
+
+ }
-
+} + + +@if (!loading && !error && !content) { +
+

No hay contenido disponible para mostrar.

+
+} diff --git a/frontend/angular/src/app/pages/introduction/introduction.component.scss b/frontend/angular/src/app/pages/introduction/introduction.component.scss index efe251c..b82b223 100644 --- a/frontend/angular/src/app/pages/introduction/introduction.component.scss +++ b/frontend/angular/src/app/pages/introduction/introduction.component.scss @@ -1,14 +1,17 @@ -// SCSS de intro optimizado para una sola pantalla .intro-template { height: 100vh; 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: 0.8rem; @@ -100,6 +103,90 @@ } } +.intro-right { + flex: 1; + display: flex; + flex-direction: column; + gap: 1rem; + grid-area: right; + padding: 1rem; + + .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: #2c2c2c; + padding: 1rem; + border-radius: 8px; + border: 1px solid #e9ecef; + + h3 { + margin-top: 0; + color: #ff6ec4; + } + + 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; + } + + + &.correct { + background: #d4edda; + color: #155724; + border: 1px solid #c3e6cb; + } + + &.incorrect { + background: #f8d7da; + color: #721c24; + border: 1px solid #f5c6cb; + } + } + } +} + .intro-right { grid-area: right; padding: 0.8rem; @@ -113,7 +200,6 @@ .intro-right { grid-area: right; padding: 1rem; - overflow: hidden; } .intro-right .buttons-field { @@ -133,3 +219,22 @@ border: 1px solid white; color: white !important; } + + +@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%; + } + } + } +} diff --git a/frontend/angular/src/app/pages/introduction/introduction.component.ts b/frontend/angular/src/app/pages/introduction/introduction.component.ts index 8d28e58..6cff385 100644 --- a/frontend/angular/src/app/pages/introduction/introduction.component.ts +++ b/frontend/angular/src/app/pages/introduction/introduction.component.ts @@ -1,8 +1,8 @@ import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; 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'; import { CommonModule } from '@angular/common'; import { CoursesService } from '../../services/courses.service'; @@ -42,7 +42,7 @@ interface CursoDisponible { @Component({ selector: 'app-introduction', - imports: [MatButtonModule, EditorActividadComponent, SearchComponent, CommonModule], + imports: [MatButtonModule, EditorComponent, SearchComponent, CommonModule], templateUrl: './introduction.component.html', styleUrl: './introduction.component.scss', }) @@ -50,6 +50,7 @@ export class IntroductionComponent implements OnInit { curso: CourseData | null = null; resetCnt = 0; salidaCodigo = ''; + resultadoVerificacion = ''; currentMock: ContenidoCurso[] = []; initialSubject = 0; totalSubjects = 0; @@ -156,6 +157,19 @@ export class IntroductionComponent implements OnInit { this.update(); } + onCodeOutput(output: string): void { + this.salidaCodigo = output; + } + + onSolutionCheck(result: {correct: boolean, output: string}): void { + if (result.correct) { + this.resultadoVerificacion = '¡Correcto! ✅'; + } else { + this.resultadoVerificacion = 'Incorrecto. Inténtalo de nuevo. ❌'; + } + this.salidaCodigo = result.output; + } + cambiarCurso(cursoDisponible: CursoDisponible): void { this.loading = true; this.error = null; 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/services/python-lsp.service.ts b/frontend/angular/src/app/services/python-lsp.service.ts new file mode 100644 index 0000000..4e691ec --- /dev/null +++ b/frontend/angular/src/app/services/python-lsp.service.ts @@ -0,0 +1,88 @@ +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 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: LSPMessage = 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: LSPMessage): 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.html b/frontend/angular/src/app/shared/editor/editor.component.html index 809d175..ee6e82d 100644 --- a/frontend/angular/src/app/shared/editor/editor.component.html +++ b/frontend/angular/src/app/shared/editor/editor.component.html @@ -1,14 +1,20 @@ -
-

Editor de Código Python

+
+ + +

Editor de Código Python

-
+
+ -
+ + +

Input:

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

Output:

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

Input:

- - -
+ +
+
+

Chat Input:

+ + +
-
-

Output:

-
{{ outputChat }}
-
-
+
+

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 7ff6b81..05476e0 100644 --- a/frontend/angular/src/app/shared/editor/editor.component.scss +++ b/frontend/angular/src/app/shared/editor/editor.component.scss @@ -1,13 +1,62 @@ +.editor-container:not(.activity-mode) { + .editor { + display: flex; + + max-width: 1000px; + margin: 0 auto; + } + + + .editor .cm-panel { + flex: 1 1 65%; + min-width: 0; + } + + + .editor .output-panel { + flex: 0 0 30%; + max-width: 30%; + display: flex; + flex-direction: column; + } +} .editor-container { 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; + min-width: 100%; + //overflow-x: hidden; + &.activity-layout { + height: 300px; + .cm-panel { + width: 100%; + } + } + .cm-panel { border: 1px solid #ffffff32; border-radius: 12px; @@ -15,20 +64,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 +89,13 @@ color: #dddddd; flex-shrink: 0; } + .cm-panel { flex: 1; width: 100%; min-height: 0; } + pre { background: #181335; border: 1px solid #ffffff32; @@ -55,6 +110,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 +140,288 @@ 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; + + .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 span { - background-color: #2e2143; - padding: 16px 24px; - border-radius: 6px; - width: 100%; - height: 100%; - transition: 300ms; + + .resultado { + font-weight: bold; + padding: 0.5rem; + border-radius: 4px; + text-align: center; + margin-top: 1rem; + } + + .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; + } + } + } +} + + +.actividad-container { + display: flex; + flex-direction: column; + gap: 1rem; + background: #1e1e1e; + color: white; + padding: 1rem; + border-radius: 10px; +} + +.editor-panel { + border-radius: 8px; +} + + +.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; } - button:hover span { - background: none; + h3 { + color: #7873f5; + font-size: 1.3rem; + margin-bottom: 0.1rem; } - button:active { - transform: scale(0.9); + 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; +} + .cm-s-material.CodeMirror { background: #181335; color: #abb2bf; } + ::ng-deep .CodeMirror { background-color: #181335 !important; color: #abb2bf !important; @@ -108,40 +429,143 @@ 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; } -@media screen and (max-width: 768px) { - .editor-container { +// Estilos para el autocompletado +::ng-deep .CodeMirror-hints { + position: absolute; + z-index: 10; + overflow: hidden; + list-style: none; + margin: 0; + padding: 2px; + border-radius: 4px; + border: 1px solid #ffffff32; + background: #181335; + font-size: 90%; + max-height: 20em; + overflow-y: auto; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); +} + +.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.2rem; + font-size: 1.5rem; } - .editor { + } + + .intro-right { + .buttons-field { flex-direction: column; - height: auto; - .cm-panel { + gap: 0.5rem; + + button, app-search { width: 100%; - height: 300px; } - .output-panel { - width: 100%; - height: auto; - .section { - margin-bottom: 1rem; - } + } + } + + .editor { + flex-direction: column; + height: auto; + + .cm-panel { + width: 100%; + height: 300px; + } + } +} +.CodeMirror, +ngx-codemirror, +.cm-panel { + width: 100% !important; + max-width: 100% !important; +} +@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; + } + } } diff --git a/frontend/angular/src/app/shared/editor/editor.component.ts b/frontend/angular/src/app/shared/editor/editor.component.ts index 2b1e71f..02e89f3 100644 --- a/frontend/angular/src/app/shared/editor/editor.component.ts +++ b/frontend/angular/src/app/shared/editor/editor.component.ts @@ -1,20 +1,44 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -// EditorComponent.ts -import { Component, OnInit } from '@angular/core'; +// 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'; -import { CodemirrorModule } from '@ctrl/ngx-codemirror'; +import { CodemirrorModule, CodemirrorComponent } from '@ctrl/ngx-codemirror'; import { HttpClient } from '@angular/common/http'; +import { PythonLSPService, LSPMessage, CompletionResponse } from '../../services/python-lsp.service'; +import { Subscription } from 'rxjs'; // Importacion de lint y modo python para CodeMirror import * as CodeMirrorNS from 'codemirror'; import 'codemirror/addon/lint/lint'; -// import 'codemirror/addon/lint/lint.css'; import 'codemirror/mode/python/python'; import 'codemirror/addon/selection/active-line'; +import 'codemirror/addon/hint/show-hint'; +import 'codemirror/addon/hint/anyword-hint'; (window as any).CodeMirror = CodeMirrorNS; declare const CodeMirror: any; +// Keywords de Python para autocompletado +const PYTHON_KEYWORDS = [ + 'False', 'None', 'True', 'and', 'as', 'assert', 'async', 'await', 'break', 'class', + 'continue', 'def', 'del', 'elif', 'else', 'except', 'finally', 'for', 'from', 'global', + 'if', 'import', 'in', 'is', 'lambda', 'nonlocal', 'not', 'or', 'pass', 'raise', + 'return', 'try', 'while', 'with', 'yield', 'print', 'range', 'len', 'str', 'int', 'float', + 'list', 'dict', 'set', 'tuple', 'input', 'sum', 'min', 'max' +]; + +// Funciones built-in de Python para autocompletado +const PYTHON_BUILTINS = [ + 'abs', 'all', 'any', 'ascii', 'bin', 'bool', 'bytearray', 'bytes', 'callable', 'chr', + 'classmethod', 'compile', 'complex', 'delattr', 'dict', 'dir', 'divmod', 'enumerate', + 'eval', 'exec', 'filter', 'float', 'format', 'frozenset', 'getattr', 'globals', + 'hasattr', 'hash', 'help', 'hex', 'id', 'input', 'int', 'isinstance', 'issubclass', + 'iter', 'len', 'list', 'locals', 'map', 'max', 'memoryview', 'min', 'next', 'object', + 'oct', 'open', 'ord', 'pow', 'print', 'property', 'range', 'repr', 'reversed', 'round', + 'set', 'setattr', 'slice', 'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple', + 'type', 'vars', 'zip' +]; + // Lint global para CodeMirror y Pyodide function pythonLint(code: string) { const pyodide = (window as any).pyodideInstance; @@ -58,30 +82,106 @@ function pythonLint(code: string) { templateUrl: './editor.component.html', styleUrls: ['./editor.component.scss'], }) -export class EditorComponent implements OnInit { +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 = ''; // 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 + + // 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', theme: 'material', lineNumbers: true, indentUnit: 4, + lineWrapping: true, + viewportMargin: Infinity, tabSize: 4, styleActiveLine: true, matchBrackets: true, - viewportMargin: Infinity, - placeholder: 'Escribe tu código…', + placeholder: this.placeholder, gutters: ['CodeMirror-lint-markers'], lint: { getAnnotations: pythonLint, async: false }, extraKeys: { - 'Ctrl-Space': 'autocomplete' + 'Ctrl-Space': (cm: any) => { + 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 +196,100 @@ export class EditorComponent implements OnInit { pyodide: any; pyodideReady = false; codeMirrorInstance: any; + editorOptions: any; + codeEditor: any; + private lspSubscription: Subscription | null = null; + private completionItems: { label: string }[] = []; + private executionWorker: Worker | null = null; + private executionTimeoutHandle: any = null; + + 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); + + // 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 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); + 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: filteredWords, + from: CodeMirror.Pos(cursor.line, token.start), + to: CodeMirror.Pos(cursor.line, token.end) + }; + }; + + this.editorOptions = { + mode: 'python', + theme: 'material', + lineNumbers: true, + lineWrapping: true, + styleActiveLine: true, + autoCloseBrackets: true, + matchBrackets: true, + lint: true, + extraKeys: { + '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, + closeOnUnfocus: false, + alignWithWord: true + } + }; + } + + 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 @@ -103,6 +297,194 @@ export class EditorComponent implements OnInit { this.pyodideReady = true; (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) { + // Asegurarse de que el resultado sea del tipo esperado + const completionResult = message.result as CompletionResponse; + this.completionItems = completionResult.items || []; + } + }); + } + + ngAfterViewInit() { + setTimeout(() => { + if (this.codeEditorComponent) { + this.codeEditor = this.codeEditorComponent.codeMirror; + + // Autocompletado en tiempo real al escribir + this.codeEditor.on('inputRead', (cm: any, change: any) => { + if (!change || !change.text) return; + const lastChar = change.text[change.text.length - 1]; + // 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 + }); + } + } + }); + + // 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); + } + + ngOnDestroy() { + 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) { + 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 @@ -117,45 +499,177 @@ export class EditorComponent implements OnInit { } } - async ejecutarCodigo(): Promise { - if (!this.pyodide) { - this.output = 'Pyodide no está cargado correctamente.'; - return; - } + // Método privado para configurar stdout de forma consistente + private setupStdout(): void { + this.pyodide.setStdout({ + batched: (text: string) => { + this.output += text; + }, + }); + } - try { - const inputLines = this.inputs.split('\n'); - let inputIndex = 0; + // Método privado para configurar input de forma consistente + private setupInput(): void { + const inputLines = this.inputs.split('\n').filter(line => line.trim() !== ''); + let inputIndex = 0; - const inputFunction = () => { - if (inputIndex >= inputLines.length) { - throw new Error('No hay más inputs disponibles'); - } - return inputLines[inputIndex++]; - }; + const inputFunction = () => { + if (inputIndex >= inputLines.length) { + throw new Error('No hay más inputs disponibles'); + } + return inputLines[inputIndex++]; + }; - this.pyodide.globals.set('input', inputFunction); + this.pyodide.globals.set('input', inputFunction); + } - this.output = ''; + // 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); + } + // 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) => { - this.output += text; + output += text; }, }); - await this.pyodide.runPythonAsync(this.codigo); - } catch (error) { - this.output = `Error: ${String(error)}`; + // 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 { + if (!this.pyodide) { + this.output = 'Pyodide no está cargado correctamente.'; + this.codeOutput.emit(this.output); + return; + } + + if (this.isExecuting) { + this.stopExecution(); + return; + } + + try { + this.output = ''; + 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: 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) con límites + async verificarSolucion(): Promise { + if (!this.correctSolution) { + console.warn('No se ha proporcionado una solución correcta'); + return; + } + + if (!this.pyodide) { + this.output = 'Pyodide no está cargado correctamente.'; + this.codeOutput.emit(this.output); + return; + } + try { + // Ejecutar el código del usuario con límites + this.output = ''; + const userOutput = await this.executeCodeWithLimits(this.codigo); + this.output = userOutput; + + // 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, ' '); + const isCorrect = normalizeOutput(userOutput) === normalizeOutput(correctOutput); + + // Emitir eventos + this.codeOutput.emit(this.output); + this.solutionCheck.emit({correct: isCorrect, output: this.output}); + + } 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}); + } + } - - constructor(private http: HttpClient) {} + // 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;