diff --git a/configs/webpack/common.js b/configs/webpack/common.js index e22d7764..0567eb34 100644 --- a/configs/webpack/common.js +++ b/configs/webpack/common.js @@ -1,165 +1,223 @@ // shared config (dev and prod) -const { resolve, join } = require('path'); -const { readFileSync } = require('fs'); -const HtmlWebpackPlugin = require('html-webpack-plugin'); -const NpmDtsPlugin = require('npm-dts-webpack-plugin') -const { DefinePlugin, IgnorePlugin } = require('webpack'); -const process = require('process'); +const { resolve, join } = require("path"); +const { readFileSync } = require("fs"); +const HtmlWebpackPlugin = require("html-webpack-plugin"); +const NpmDtsPlugin = require("npm-dts-webpack-plugin"); +const { DefinePlugin, IgnorePlugin } = require("webpack"); +const process = require("process"); -const commitHash = require('child_process').execSync('git rev-parse --short=8 HEAD').toString().trim(); +const commitHash = require("child_process") + .execSync("git rev-parse --short=8 HEAD") + .toString() + .trim(); let dependencies = {}; try { - dependencies = JSON.parse(readFileSync(resolve(__dirname, '..', '..', 'dependencies', 'dependencies.json'))); + dependencies = JSON.parse( + readFileSync( + resolve(__dirname, "..", "..", "dependencies", "dependencies.json") + ) + ); } catch (e) { - console.log('Failed to read dependencies.json'); + console.log("Failed to read dependencies.json"); } -const modules = ['node_modules']; +const modules = ["node_modules"]; if (dependencies.cpython) modules.push(resolve(dependencies.cpython)); let libkiprCDocumentation = undefined; +let libkiprCCCommonDocumentation = undefined; if (dependencies.libkipr_c_documentation) { - libkiprCDocumentation = JSON.parse(readFileSync(resolve(dependencies.libkipr_c_documentation))); + libkiprCDocumentation = JSON.parse( + readFileSync(resolve(dependencies.libkipr_c_documentation)) + ); +} +if (dependencies.libkipr_c_common_documentation) { + libkiprCCCommonDocumentation = JSON.parse( + readFileSync(resolve(dependencies.libkipr_c_common_documentation)) + ); } let i18n = {}; try { - i18n = JSON.parse(readFileSync(resolve(__dirname, '..', '..', 'i18n', 'i18n.json'))); + i18n = JSON.parse( + readFileSync(resolve(__dirname, "..", "..", "i18n", "i18n.json")) + ); } catch (e) { - console.log('Failed to read i18n.json'); + console.log("Failed to read i18n.json"); console.log(`Please run 'yarn run build-i18n'`); process.exit(1); } - module.exports = { entry: { - app: './index.tsx', - login: './components/Login/index.tsx', - plugin: './lms/plugin/index.tsx', - parentalConsent: './components/ParentalConsent/index.tsx', - 'editor.worker': 'monaco-editor/esm/vs/editor/editor.worker.js', - 'ts.worker': 'monaco-editor/esm/vs/language/typescript/ts.worker.js', + app: "./index.tsx", + login: "./components/Login/index.tsx", + plugin: "./lms/plugin/index.tsx", + parentalConsent: "./components/ParentalConsent/index.tsx", + "editor.worker": "monaco-editor/esm/vs/editor/editor.worker.js", + "ts.worker": "monaco-editor/esm/vs/language/typescript/ts.worker.js", }, output: { filename: (pathData) => { - if (pathData.chunk.name === 'editor.worker') return 'editor.worker.bundle.js'; - if (pathData.chunk.name === 'ts.worker') return 'ts.worker.bundle.js'; - return 'js/[name].[contenthash].min.js'; + if (pathData.chunk.name === "editor.worker") + return "editor.worker.bundle.js"; + if (pathData.chunk.name === "ts.worker") return "ts.worker.bundle.js"; + return "js/[name].[contenthash].min.js"; }, - path: resolve(__dirname, '../../dist'), - publicPath: '/', + path: resolve(__dirname, "../../dist"), + publicPath: "/", clean: true, }, - externals: [ - 'child_process', - 'fs', - 'path', - 'crypto', - ], + watchOptions: { + ignored: /node_modules\/(?!ivygate)/, + }, + + externals: ["child_process", "fs", "path", "crypto"], + snapshot: { + managedPaths: [], // ensures node_modules/ivygate symlink is watched + }, resolve: { - extensions: ['.ts', '.tsx', '.js', '.jsx'], + extensions: [".ts", ".tsx", ".js", ".jsx"], fallback: { fs: false, path: false, }, alias: { - state: resolve(__dirname, '../../src/state'), - '@i18n': resolve(__dirname, '../../src/util/i18n'), + state: resolve(__dirname, "../../src/state"), + "@i18n": resolve(__dirname, "../../src/util/i18n"), + "@ivygate": resolve(__dirname, "../../node_modules/ivygate"), }, symlinks: false, - modules //: [resolve(__dirname, '../../src'), 'node_modules'] + modules, //: [resolve(__dirname, '../../src'), 'node_modules'] }, - context: resolve(__dirname, '../../src'), + watchOptions: { + followSymlinks: true, + }, + context: resolve(__dirname, "../../src"), module: { rules: [ // Apply class static block transform to monaco-editor ESM sources inside node_modules // because we normally exclude node_modules from Babel handling. { test: /\.js$/, - include: /node_modules[\/\\]monaco-editor[\/\\]esm/, use: [ { - loader: 'babel-loader', + loader: "babel-loader", options: { - plugins: [ - '@babel/plugin-transform-class-static-block' - ] - } - } - ] + plugins: ["@babel/plugin-transform-class-static-block"], + }, + }, + ], }, { test: /\.js$/, - use: ['babel-loader', 'source-map-loader'], + use: ["babel-loader", "source-map-loader"], exclude: /node_modules/, }, { test: /\.tsx?$/, use: [ { - loader: 'babel-loader', + loader: "babel-loader", options: { - plugins: ['@babel/plugin-syntax-import-meta'] - } + plugins: ["@babel/plugin-syntax-import-meta"], + }, }, { - loader: 'ts-loader', + loader: "ts-loader", options: { + transpileOnly: true, allowTsInNodeModules: true, - } - } + }, + }, + ], + // ✅ This allows both IvyGate and Itch from node_modules + exclude: /node_modules\/(?!ivygate|itch)/, + include: [ + resolve(__dirname, "../../src"), + resolve(__dirname, "../../ivygate/src"), + resolve(__dirname, "../../node_modules/ivygate/src"), + resolve(__dirname, "../../node_modules/itch/src"), ], }, { test: /\.css$/, use: [ - 'style-loader', + "style-loader", { - loader: 'css-loader', + loader: "css-loader", options: { - importLoaders: 1 - } - } + importLoaders: 1, + }, + }, ], }, { test: /\.scss$/, use: [ - 'style-loader', + "style-loader", { - loader: 'css-loader', + loader: "css-loader", options: { - importLoaders: 1 - } + importLoaders: 1, + }, }, - 'sass-loader', + "sass-loader", ], }, { test: /\.(jpe?g|png|gif|svg|PNG)$/i, use: [ - 'file-loader?hash=sha512&digest=hex&name=img/[hash].[ext]', - 'image-webpack-loader?bypassOnDebug&optipng.optimizationLevel=7&gifsicle.interlaced=false', + { + loader: "file-loader", + options: { + name: "img/[hash].[ext]", + }, + }, + { + loader: "image-webpack-loader", + options: { + mozjpeg: { progressive: true }, + optipng: { optimizationLevel: 7 }, + gifsicle: { interlaced: false }, + pngquant: { quality: [0.65, 0.9], speed: 4 }, + disable: process.env.NODE_ENV === "development", + }, + }, ], }, { test: /\.(woff|woff2|eot|ttf)$/, - loader: 'url-loader', + loader: "url-loader", options: { limit: 100000, }, - } + }, ], }, plugins: [ - new HtmlWebpackPlugin({ template: 'index.html.ejs', excludeChunks: ['login', 'plugin', 'parentalConsent'] }), - new HtmlWebpackPlugin({ template: 'components/Login/login.html.ejs', filename: 'login.html', chunks: ['login'] }), - new HtmlWebpackPlugin({ template: 'lms/plugin/plugin.html.ejs', filename: 'plugin.html', chunks: ['plugin'] }), - new HtmlWebpackPlugin({ template: 'components/ParentalConsent/parental-consent.html.ejs', filename: 'parental-consent.html', chunks: ['parentalConsent'] }), + new HtmlWebpackPlugin({ + template: "index.html.ejs", + excludeChunks: ["login", "plugin", "parentalConsent"], + }), + new HtmlWebpackPlugin({ + template: "components/Login/login.html.ejs", + filename: "login.html", + chunks: ["login"], + }), + new HtmlWebpackPlugin({ + template: "lms/plugin/plugin.html.ejs", + filename: "plugin.html", + chunks: ["plugin"], + }), + new HtmlWebpackPlugin({ + template: "components/ParentalConsent/parental-consent.html.ejs", + filename: "parental-consent.html", + chunks: ["parentalConsent"], + }), new DefinePlugin({ - SIMULATOR_VERSION: JSON.stringify(require('../../package.json').version), + SIMULATOR_VERSION: JSON.stringify(require("../../package.json").version), SIMULATOR_GIT_HASH: JSON.stringify(commitHash), SIMULATOR_HAS_CPYTHON: JSON.stringify(dependencies.cpython !== undefined), SIMULATOR_LIBKIPR_C_DOCUMENTATION: JSON.stringify(libkiprCDocumentation), @@ -167,17 +225,19 @@ module.exports = { // needed because ivygate relies on them being defined IDE_LIBKIPR_C_DOCUMENTATION: JSON.stringify(libkiprCDocumentation), - IDE_LIBKIPR_C_COMMON_DOCUMENTATION: null, + IDE_LIBKIPR_C_COMMON_DOCUMENTATION: JSON.stringify( + libkiprCCCommonDocumentation + ), IDE_I18N: JSON.stringify(i18n), }), new NpmDtsPlugin({ - root: resolve(__dirname, '../../'), - logLevel: 'error', + root: resolve(__dirname, "../../"), + logLevel: "error", force: true, - output: resolve(__dirname, '../../dist/simulator.d.ts'), - }) + output: resolve(__dirname, "../../dist/simulator.d.ts"), + }), ], performance: { hints: false, }, -}; \ No newline at end of file +}; diff --git a/dependencies/build.py b/dependencies/build.py index d300e4b3..e1741c17 100755 --- a/dependencies/build.py +++ b/dependencies/build.py @@ -221,9 +221,9 @@ def is_tool(name): print('Generating JSON documentation...') libkipr_c_documentation_json = f'{libkipr_build_c_dir}/documentation/json.json' +libkipr_c_common_documentation = f'{libkipr_build_c_dir}/documentation/json_common.json' subprocess.run( - [ python, 'generate_doxygen_json.py', f'{libkipr_build_c_dir}/documentation/xml', libkipr_c_documentation_json ], - # [ 'python3', 'generate_doxygen_json.py', f'{libkipr_build_c_dir}/documentation/xml', libkipr_c_documentation_json ], + [ python, 'generate_doxygen_json.py', f'{libkipr_build_c_dir}/documentation/xml', libkipr_c_documentation_json, libkipr_c_common_documentation], cwd = working_dir, check = True ) @@ -278,6 +278,7 @@ def is_tool(name): 'cpython': f'{cpython_emscripten_build_dir}', 'cpython_hash': hash_dir(cpython_dir), "libkipr_c_documentation": libkipr_c_documentation_json, + "libkipr_c_common_documentation": libkipr_c_common_documentation, 'scratch_rt': f'{scratch_runtime_path}.js', }) diff --git a/dependencies/generate_doxygen_json.py b/dependencies/generate_doxygen_json.py index 21333034..0672f1a3 100644 --- a/dependencies/generate_doxygen_json.py +++ b/dependencies/generate_doxygen_json.py @@ -20,8 +20,13 @@ ) parser.add_argument( - 'output_file', - help = 'Output file to write JSON to' + 'default_output_file', + help = 'Output file to write default JSON to' +) + +parser.add_argument( + 'common_output_file', + help = 'Output file to write common JSON to' ) args = parser.parse_args() @@ -32,9 +37,7 @@ def eprint(*args, **kwargs): # Get all XML files in a directory passed in as the first argument -# add create*.xml to the list of files to parse xml_file_matches = [ - 'create*.xml', 'analog*.xml', 'botball*.xml', 'button*.xml', @@ -57,12 +60,67 @@ def eprint(*args, **kwargs): 'struct*.xml', 'time*.xml', 'wait__for*.xml', + +] + +commonFileNames = [ + "analog.h", + "analog.hpp", + "botball.h", + "button_ids.hpp", + "button.h", + "button.hpp", + "color.hpp", + "colors.h", + "console.hpp", + "console.h", + "digital.h", + "digital.hpp", + "display.h", + "geometry.h", + "geometry.hpp", + "logic.hpp", + "log.hpp", + "motor.hpp", + "motor.h", + "sensor.hpp", + "servo.hpp", + "servo.h", + "time.h", + "wait_for.h" +] + +commonFunctionNames = [ + "analog", + "digital", + "get_motor_position_counter", + "gmpc", + "clear_motor_position_counter", + "cmpc", + "move_at_velocity", + "mav", + "motor", + "alloff" + "ao", + "enable_servo", + "disable_servo", + "enable_servos", + "disable_servos", + "set_servo_position", +] + +commonModuleNames = [ + "analog", + "digital", + "motor", + "servo", ] xml_files = [] for match in xml_file_matches: xml_files += glob.glob(args.input_dir + '/' + match) + @dataclass class File: id: str @@ -135,6 +193,11 @@ class Type: types: List[Type] = [] files: List[File] = [] + +commonFiles: List[File] = [] +commonFunctions: List[Function] = [] +commonModules: List[Module] = [] + def parse_text(node): if node is None: return None # Collect all subtext ignoring parameterlist and simplesect tags @@ -227,11 +290,12 @@ def parse_file(node): for section in sections: if section.get('kind') == 'func': - for member in section.findall('memberdef'): - if member.get('kind') == 'function': - functions.append(member.get('id')) - parse_function(member) + for memberdef in section.findall('memberdef'): + if memberdef.get('kind') == 'function': + functions.append(memberdef.get('id')) + parse_function(memberdef) + files.append(File(id, name, functions, [], [])) def parse_struct(node): @@ -299,10 +363,10 @@ def parse_group(node): for member in node.findall('sectiondef/memberdef'): if member.get('kind') == 'function': functions.append(member.get('id')) + parse_function(member) modules.append(Module(id, name, functions, [])) - def parse_compounddef(node): # Determine kind kind = node.get('kind') @@ -321,9 +385,27 @@ def parse_xml(tree): if child.tag == 'compounddef': parse_compounddef(child) +def parse_common(): + for f in files: + if f.name in commonFileNames: + commonFiles.append(f) + for func in functions: + if func.name in commonFunctionNames: + commonFunctions.append(func) + for mod in modules: + if mod.name in commonModuleNames: + #commonModules.append(mod) + # Filter mod functions to only those in commonFunctions + common_func_ids = {func.id for func in commonFunctions} + filtered_functions = [func_id for func_id in mod.functions if func_id in common_func_ids] + commonModules.append(Module(mod.id, mod.name, filtered_functions, [])) + + + for xml_file in xml_files: tree = ET.parse(xml_file) parse_xml(tree) +parse_common() # Convert lists to dictionaries by ID files_dict = {file.id: asdict(file) for file in files} @@ -333,8 +415,11 @@ def parse_xml(tree): enumerations_dict = {enumeration.name: asdict(enumeration) for enumeration in enumerations} types_dict = {type.id: asdict(type) for type in types} +common_files_dict = {file.id: asdict(file) for file in commonFiles} +common_functions_dict = {function.id: asdict(function) for function in commonFunctions} +common_modules_dict = {module.id: asdict(module) for module in commonModules} -with open(args.output_file, 'w') as f: +with open(args.default_output_file, 'w') as f: f.write(json.dumps({ 'files': files_dict, 'functions': functions_dict, @@ -342,4 +427,13 @@ def parse_xml(tree): 'structures': structures_dict, 'enumerations': enumerations_dict, 'types': types_dict - }, indent = 2)) \ No newline at end of file + }, indent = 2)) + + +with open(args.common_output_file, 'w') as f: + f.write(json.dumps({ + 'title': 'common', + 'files': common_files_dict, + 'functions': common_functions_dict, + 'modules': common_modules_dict + }, indent = 2)) \ No newline at end of file diff --git a/express.js b/express.js index 410a50cd..0ff94e2f 100644 --- a/express.js +++ b/express.js @@ -1,24 +1,24 @@ /* eslint-env node */ -const express = require('express'); -const bodyParser = require('body-parser'); -const morgan = require('morgan'); -const fs = require('fs'); -const uuid = require('uuid'); -const { exec } = require('child_process'); -const session = require('express-session'); -const csrf = require('lusca').csrf; +const express = require("express"); +const bodyParser = require("body-parser"); +const morgan = require("morgan"); +const fs = require("fs"); +const uuid = require("uuid"); +const { exec } = require("child_process"); +const session = require("express-session"); +const csrf = require("lusca").csrf; const app = express(); -const sourceDir = 'dist'; -const { get: getConfig } = require('./config'); -const { WebhookClient } = require('discord.js'); -const proxy = require('express-http-proxy'); -const path = require('path'); -const { FirebaseTokenManager } = require('./firebaseAuth'); -const formData = require('form-data'); -const Mailgun = require('mailgun.js'); -const createParentalConsentRouter = require('./parentalConsent'); -const createAiRouter = require('./ai'); +const sourceDir = "dist"; +const { get: getConfig } = require("./config"); +const { WebhookClient } = require("discord.js"); +const proxy = require("express-http-proxy"); +const path = require("path"); +const { FirebaseTokenManager } = require("./firebaseAuth"); +const formData = require("form-data"); +const Mailgun = require("mailgun.js"); +const createParentalConsentRouter = require("./parentalConsent"); +const createAiRouter = require("./ai"); let config; try { @@ -28,25 +28,27 @@ try { throw e; } -app.set('trust proxy', true); +app.set("trust proxy", false); // Session middleware for generating session IDs -app.use(session({ - secret: config.server.sessionSecret || 'kipr-simulator-session-secret', - resave: false, - saveUninitialized: true, - cookie: { - maxAge: 24 * 60 * 60 * 1000, // 24 hours - httpOnly: true, - secure: process.env.NODE_ENV === 'production' ? true : false // Enforce secure cookies in production - }, - name: 'kipr_session' -})); +app.use( + session({ + secret: config.server.sessionSecret || "kipr-simulator-session-secret", + resave: false, + saveUninitialized: true, + cookie: { + maxAge: 24 * 60 * 60 * 1000, // 24 hours + httpOnly: true, + secure: process.env.NODE_ENV === "production" ? true : false, // Enforce secure cookies in production + }, + name: "kipr_session", + }) +); // CSRF protection - skip for API routes that use Bearer token authentication app.use((req, res, next) => { // Skip CSRF for API routes that use Bearer tokens (CSRF-safe by design) - if (req.path.startsWith('/api/')) { + if (req.path.startsWith("/api/")) { return next(); } // Apply CSRF protection to other routes @@ -54,92 +56,101 @@ app.use((req, res, next) => { }); // Metrics collection -const metrics = require('./metrics'); +const metrics = require("./metrics"); app.use(metrics.metricsMiddleware); // Logging -const { logCompilation, logFeedback, logRateLimit } = require('./logger'); +const { logCompilation, logFeedback, logRateLimit } = require("./logger"); // set up rate limiter: maximum of 100 requests per 15 minute -var RateLimit = require('express-rate-limit'); +var RateLimit = require("express-rate-limit"); var limiter = RateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 1000, // max 100 requests per windowMs handler: (req, res) => { // Track rate limit hits metrics.rateLimit.hits.inc({ endpoint: req.path }); - + // Log rate limit hit logRateLimit({ endpoint: req.path, ip: req.ip, - userId: req.user?.uid + userId: req.user?.uid, }); - + res.status(429).json({ - error: 'Too many requests, please try again later.' + error: "Too many requests, please try again later.", }); - } + }, }); - - // apply rate limiter to all requests app.use(limiter); const mailgun = new Mailgun(formData); const mailgunClient = mailgun.client({ - username: 'api', + username: "api", key: config.mailgun.apiKey, }); -const firebaseTokenManager = new FirebaseTokenManager(config.firebase.serviceAccountKey, config.firebase.apiKey); +const firebaseTokenManager = new FirebaseTokenManager( + config.firebase.serviceAccountKey, + config.firebase.apiKey +); app.use((req, res, next) => { res.header("Access-Control-Allow-Origin", "*"); - res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); + res.header( + "Access-Control-Allow-Headers", + "Origin, X-Requested-With, Content-Type, Accept" + ); next(); }); app.use(bodyParser.json()); -app.use(morgan('combined')); +app.use(morgan("combined")); -app.use('/api/parental-consent', createParentalConsentRouter(firebaseTokenManager, mailgunClient, config)); +app.use( + "/api/parental-consent", + createParentalConsentRouter(firebaseTokenManager, mailgunClient, config) +); // Add AI router -app.use('/api/ai', createAiRouter(firebaseTokenManager, config)); +app.use("/api/ai", createAiRouter(firebaseTokenManager, config)); -app.use('/api', proxy(config.dbUrl)); +app.use("/api", proxy(config.dbUrl)); // If we have libkipr (C) artifacts and emsdk, we can compile. -if (config.server.dependencies.libkipr_c && config.server.dependencies.emsdk_env) { - - app.post('/compile', (req, res) => { +if ( + config.server.dependencies.libkipr_c && + config.server.dependencies.emsdk_env +) { + app.post("/compile", (req, res) => { const startTime = Date.now(); - const language = 'C'; + const language = "C"; const userId = req.user?.uid; - const sessionId = req.headers['x-session-id'] || req.sessionID || 'unknown'; - + const sessionId = req.headers["x-session-id"] || req.sessionID || "unknown"; + // Track session interaction - metrics.trackSessionInteraction(sessionId, 'compile'); - - if (!('code' in req.body)) { + metrics.trackSessionInteraction(sessionId, "compile"); + + if (!("code" in req.body)) { return res.status(400).json({ - error: "Expected code key in body" + error: "Expected code key in body", }); } - - if (typeof req.body.code !== 'string') { + + if (typeof req.body.code !== "string") { return res.status(400).json({ - error: "Expected code key in body to be a string" + error: "Expected code key in body to be a string", }); } - + const code = req.body.code; - + // Track code size metrics.compilation.codeSize.observe({ language }, code.length); - + // Wrap user's main() in our own "main()" that exits properly // Required because Asyncify keeps emscripten runtime alive, which would prevent cleanup code from running const augmentedCode = `${code} @@ -156,15 +167,14 @@ if (config.server.dependencies.libkipr_c && config.server.dependencies.emsdk_env emscripten_force_exit(0); } `; - - + const id = uuid.v4(); const path = `/tmp/${id}.c`; - fs.writeFile(path, augmentedCode, err => { + fs.writeFile(path, augmentedCode, (err) => { if (err) { const durationMs = Date.now() - startTime; const duration = durationMs / 1000; - + // Log failed compilation logCompilation({ userId, @@ -172,140 +182,164 @@ if (config.server.dependencies.libkipr_c && config.server.dependencies.emsdk_env language, code, duration: durationMs, - status: 'error', + status: "error", stdout: null, - stderr: err.message + stderr: err.message, }); - - metrics.compilation.counter.inc({ status: 'error', language }); - metrics.compilation.duration.observe({ status: 'error', language }, duration); - + + metrics.compilation.counter.inc({ status: "error", language }); + metrics.compilation.duration.observe( + { status: "error", language }, + duration + ); + return res.status(500).json({ - error: "Failed to write ${}" + error: "Failed to write ${}", }); } // ...process.env causes a linter error for some reason. // We work around this by doing it manually. - + const env = {}; for (const key of Object.keys(process.env)) { env[key] = process.env[key]; } - - env['PATH'] = `${config.server.dependencies.emsdk_env.PATH}:${process.env.PATH}`; - env['EMSDK'] = config.server.dependencies.emsdk_env.EMSDK; - env['EM_CONFIG'] = config.server.dependencies.emsdk_env.EM_CONFIG; - - exec(`emcc -s WASM=0 -s INVOKE_RUN=0 -s ASYNCIFY -s EXIT_RUNTIME=1 -s "EXPORTED_FUNCTIONS=['_main', '_simMainWrapper']" -I${config.server.dependencies.libkipr_c}/include -L${config.server.dependencies.libkipr_c}/lib -lkipr -o ${path}.js ${path}`, { - env - }, (err, stdout, stderr) => { - const durationMs = Date.now() - startTime; - const duration = durationMs / 1000; - - if (err) { - console.log(stderr); - - // Log failed compilation - logCompilation({ - userId, - sessionId, - language, - code, - duration: durationMs, - status: 'error', - stdout, - stderr - }); - - metrics.compilation.counter.inc({ status: 'error', language }); - metrics.compilation.duration.observe({ status: 'error', language }, duration); - - return res.status(200).json({ - stdout, - stderr - }); - } - - fs.readFile(`${path}.js`, (err, data) => { + + env[ + "PATH" + ] = `${config.server.dependencies.emsdk_env.PATH}:${process.env.PATH}`; + env["EMSDK"] = config.server.dependencies.emsdk_env.EMSDK; + env["EM_CONFIG"] = config.server.dependencies.emsdk_env.EM_CONFIG; + + exec( + `emcc -s WASM=0 -s INVOKE_RUN=0 -s ASYNCIFY -s EXIT_RUNTIME=1 -s "EXPORTED_FUNCTIONS=['_main', '_simMainWrapper']" -I${config.server.dependencies.libkipr_c}/include -L${config.server.dependencies.libkipr_c}/lib -lkipr -o ${path}.js ${path}`, + { + env, + }, + (err, stdout, stderr) => { + const durationMs = Date.now() - startTime; + const duration = durationMs / 1000; + if (err) { - return res.status(400).json({ - error: `Failed to open ${path}.js for reading` + console.log(stderr); + + // Log failed compilation + logCompilation({ + userId, + sessionId, + language, + code, + duration: durationMs, + status: "error", + stdout, + stderr, + }); + + metrics.compilation.counter.inc({ status: "error", language }); + metrics.compilation.duration.observe( + { status: "error", language }, + duration + ); + + return res.status(200).json({ + stdout, + stderr, }); } - - fs.unlink(`${path}.js`, err => { + + fs.readFile(`${path}.js`, (err, data) => { if (err) { - return res.status(500).json({ - error: `Failed to delete ${path}.js` + return res.status(400).json({ + error: `Failed to open ${path}.js for reading`, }); } - fs.unlink(`${path}`, err => { + + fs.unlink(`${path}.js`, (err) => { if (err) { return res.status(500).json({ - error: `Failed to delete ${path}` + error: `Failed to delete ${path}.js`, }); } - - // Log successful compilation - logCompilation({ - userId, - sessionId, - language, - code, - duration: durationMs, - status: 'success', - stdout, - stderr: stderr || null - }); - - // Success! Track metrics - metrics.compilation.counter.inc({ status: 'success', language }); - metrics.compilation.duration.observe({ status: 'success', language }, duration); - - res.status(200).json({ - result: data.toString(), - stdout, - stderr, + fs.unlink(`${path}`, (err) => { + if (err) { + return res.status(500).json({ + error: `Failed to delete ${path}`, + }); + } + + // Log successful compilation + logCompilation({ + userId, + sessionId, + language, + code, + duration: durationMs, + status: "success", + stdout, + stderr: stderr || null, + }); + + // Success! Track metrics + metrics.compilation.counter.inc({ + status: "success", + language, + }); + metrics.compilation.duration.observe( + { status: "success", language }, + duration + ); + + res.status(200).json({ + result: data.toString(), + stdout, + stderr, + }); }); }); }); - }); - }); + } + ); }); - - }); } - -app.post('/feedback', (req, res) => { +app.post("/feedback", (req, res) => { const hookURL = config.server.feedbackWebhookURL; if (!hookURL) { res.status(500).json({ - message: 'The feedback URL is not set on the server. If this is a developoment environment, make sure the feedback URL environment variable is set.' + message: + "The feedback URL is not set on the server. If this is a developoment environment, make sure the feedback URL environment variable is set.", }); return; } const body = req.body; - const sessionId = req.headers['x-session-id'] || req.sessionID || 'unknown'; - + const sessionId = req.headers["x-session-id"] || req.sessionID || "unknown"; + // Track session interaction - metrics.trackSessionInteraction(sessionId, 'feedback'); + metrics.trackSessionInteraction(sessionId, "feedback"); let content = `User Feedback Recieved:\n\`\`\`${body.feedback} \`\`\``; - + content += `Sentiment: `; switch (body.sentiment) { - case 0: content += 'No sentiment! This is probably a bug'; break; - case 1: content += ':frowning2:'; break; - case 2: content += ':expressionless:'; break; - case 3: content += ':smile:'; break; + case 0: + content += "No sentiment! This is probably a bug"; + break; + case 1: + content += ":frowning2:"; + break; + case 2: + content += ":expressionless:"; + break; + case 3: + content += ":smile:"; + break; } - content += '\n'; + content += "\n"; - if (body.email !== null && body.email !== '') { + if (body.email !== null && body.email !== "") { content += `User Email: ${body.email}\n`; } @@ -313,10 +347,12 @@ app.post('/feedback', (req, res) => { if (body.includeAnonData) { content += `Browser User-Agent: ${body.userAgent}\n`; - files = [{ - attachment: Buffer.from(JSON.stringify(body.state, undefined, 2)), - name: 'userdata.json' - }]; + files = [ + { + attachment: Buffer.from(JSON.stringify(body.state, undefined, 2)), + name: "userdata.json", + }, + ]; } let webhook; @@ -325,87 +361,121 @@ app.post('/feedback', (req, res) => { } catch (error) { console.log(error); res.status(500).json({ - message: 'An error occured on the server. If you are a developer, your webhook url is likely wrong.' + message: + "An error occured on the server. If you are a developer, your webhook url is likely wrong.", }); // TODO: write the feedback to a file if an error occurs? return; } - webhook.send({ - content: content, - username: 'KIPR Simulator Feedback', - avatarURL: 'https://www.kipr.org/wp-content/uploads/2018/08/botguy-copy.jpg', - files: files - }) + webhook + .send({ + content: content, + username: "KIPR Simulator Feedback", + avatarURL: + "https://www.kipr.org/wp-content/uploads/2018/08/botguy-copy.jpg", + files: files, + }) .then(() => { // Log feedback submission logFeedback({ - userId: body.userId || 'anonymous', + userId: body.userId || "anonymous", sentiment: body.sentiment, feedback: body.feedback, email: body.email, - includeAnonData: body.includeAnonData + includeAnonData: body.includeAnonData, }); - + // Track feedback submission - const sentimentLabel = body.sentiment === 1 ? 'negative' - : body.sentiment === 2 ? 'neutral' - : body.sentiment === 3 ? 'positive' - : 'unknown'; + const sentimentLabel = + body.sentiment === 1 + ? "negative" + : body.sentiment === 2 + ? "neutral" + : body.sentiment === 3 + ? "positive" + : "unknown"; metrics.feedback.counter.inc({ sentiment: sentimentLabel }); - + res.status(200).json({ - message: 'Feedback submitted! Thank you!' + message: "Feedback submitted! Thank you!", }); }) .catch(() => { res.status(500).json({ - message: 'An error occured on the server while sending feedback.' + message: "An error occured on the server while sending feedback.", }); // TODO: write the feedback to a file if an error occurs? }); }); -app.use('/static', express.static(`${__dirname}/static`, { - maxAge: config.caching.staticMaxAge, -})); +// ✅ Add this line +app.use((req, res, next) => { + res.setHeader("Cross-Origin-Opener-Policy", "same-origin"); + res.setHeader("Cross-Origin-Embedder-Policy", "require-corp"); + next(); +}); +app.use( + "/static", + express.static(`${__dirname}/static`, { + maxAge: config.caching.staticMaxAge, + }) +); if (config.server.dependencies.scratch_rt) { - console.log('Scratch Runtime is enabled.'); - app.use('/scratch/rt.js', express.static(`${config.server.dependencies.scratch_rt}`, { - maxAge: config.caching.staticMaxAge, - })); + console.log("Scratch Runtime is enabled."); + app.use( + "/scratch/rt.js", + express.static(`${config.server.dependencies.scratch_rt}`, { + maxAge: config.caching.staticMaxAge, + }) + ); } -app.use('/scratch', express.static(path.resolve(__dirname, 'node_modules', 'kipr-scratch'), { - maxAge: config.caching.staticMaxAge, -})); - -app.use('/media', express.static(path.resolve(__dirname, 'node_modules', 'kipr-scratch', 'media'), { - maxAge: config.caching.staticMaxAge, -})); +app.use( + "/scratch", + express.static(path.resolve(__dirname, "node_modules", "kipr-scratch"), { + maxAge: config.caching.staticMaxAge, + }) +); + +app.use( + "/media", + express.static( + path.resolve(__dirname, "node_modules", "kipr-scratch", "media"), + { + maxAge: config.caching.staticMaxAge, + } + ) +); // Expose cpython artifacts if (config.server.dependencies.cpython) { - console.log('CPython artifacts are enabled.'); - app.use('/cpython', express.static(`${config.server.dependencies.cpython}`, { - maxAge: config.caching.staticMaxAge, - })); + console.log("CPython artifacts are enabled."); + app.use( + "/cpython", + express.static(`${config.server.dependencies.cpython}`, { + maxAge: config.caching.staticMaxAge, + }) + ); } // Expose libkipr (Python) artifacts if (config.server.dependencies.libkipr_python) { - console.log('libkipr (Python) artifacts are enabled.'); - app.use('/libkipr/python', express.static(`${config.server.dependencies.libkipr_python}`, { - maxAge: config.caching.staticMaxAge, - })); + console.log("libkipr (Python) artifacts are enabled."); + app.use( + "/libkipr/python", + express.static(`${config.server.dependencies.libkipr_python}`, { + maxAge: config.caching.staticMaxAge, + }) + ); } // Expose metrics endpoint -app.get('/metrics', async (req, res) => { +app.get("/metrics", async (req, res) => { try { - res.set('Content-Type', metrics.register.contentType); + res.set("Content-Type", metrics.register.contentType); res.end(await metrics.register.metrics()); } catch (err) { console.error("Error in /metrics endpoint:", err); @@ -413,36 +483,40 @@ app.get('/metrics', async (req, res) => { } }); -app.use('/dist', express.static(`${__dirname}/dist`, { - setHeaders: setCrossOriginIsolationHeaders, -})); +app.use( + "/dist", + express.static(`${__dirname}/dist`, { + setHeaders: setCrossOriginIsolationHeaders, + }) +); -app.use(express.static(sourceDir, { - maxAge: config.caching.staticMaxAge, - setHeaders: setCrossOriginIsolationHeaders, -})); +app.use( + express.static(sourceDir, { + maxAge: config.caching.staticMaxAge, + setHeaders: setCrossOriginIsolationHeaders, + }) +); -app.get('/login', (req, res) => { +app.get("/login", (req, res) => { res.sendFile(`${__dirname}/${sourceDir}/login.html`); }); -app.get('/lms/plugin', (req, res) => { +app.get("/lms/plugin", (req, res) => { res.sendFile(`${__dirname}/${sourceDir}/plugin.html`); }); -app.get('/parental-consent/*', (req, res) => { +app.use(/^\/parental-consent(?:\/.*)?$/, (req, res) => { res.sendFile(`${__dirname}/${sourceDir}/parental-consent.html`); }); -app.use('*', (req, res) => { - setCrossOriginIsolationHeaders(res); +app.use(/.*/, (req, res) => { res.sendFile(`${__dirname}/${sourceDir}/index.html`); }); - - app.listen(config.server.port, () => { - console.log(`Express web server started: http://localhost:${config.server.port}`); + console.log( + `Express web server started: http://localhost:${config.server.port}` + ); console.log(`Serving content from /${sourceDir}/`); }); @@ -450,4 +524,4 @@ app.listen(config.server.port, () => { function setCrossOriginIsolationHeaders(res) { res.header("Cross-Origin-Opener-Policy", "same-origin"); res.header("Cross-Origin-Embedder-Policy", "require-corp"); -} \ No newline at end of file +} diff --git a/package.json b/package.json index bdbdc085..10260520 100644 --- a/package.json +++ b/package.json @@ -19,9 +19,12 @@ "build-i18n": "ts-node i18n/build.ts" }, "devDependencies": { - "@babel/core": "^7.9.0", + "@babel/core": "^7.28.5", "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-transform-class-static-block": "^7.28.3", + "@babel/preset-env": "^7.28.5", + "@babel/preset-react": "^7.28.5", + "@babel/preset-typescript": "^7.28.5", "@types/chai": "^4.3.3", "@types/gapi": "^0.0.44", "@types/gettext-parser": "^4.0.2", @@ -38,7 +41,7 @@ "@types/xmldom": "^0.1.31", "@typescript-eslint/eslint-plugin": "^4.18.0", "@typescript-eslint/parser": "^4.18.0", - "babel-loader": "^8.1.0", + "babel-loader": "^10.0.0", "eslint": "^7.22.0", "file-loader": "^6.0.0", "gettext-parser": "^6.0.0", @@ -82,11 +85,13 @@ "image-webpack-loader": "^8.1.0", "immer": "^9.0.15", "itch": "https://github.com/chrismbirmingham/itch#36", - "ivygate": "https://github.com/kipr/ivygate#47796742720abad4f00c19a3af3421bad073046d", + "ivygate": "https://github.com/kipr/ivygate#038fc28b9fc4210be38785f2462ef8c9a8caedbd", "jspdf": "^3.0.2", "kipr-scratch": "file:dependencies/kipr-scratch/kipr-scratch", "lusca": "^1.7.0", "mailgun.js": "^10.2.1", + "minimatch": "^10.1.1", + "monaco-editor": "^0.54.0", "morgan": "^1.10.0", "pdf-lib": "^1.17.1", "pdfjs-dist": "^4.4.168", @@ -97,7 +102,7 @@ "react-dom": "^18.3.1", "react-loading": "^2.0.3", "react-markdown": "^8.0.1", - "react-redux": "^7.2.9", + "react-redux": "9.2.0", "react-reverse-portal": "^2.0.1", "react-router-dom": "^6.26.2", "redux": "^4.1.0", @@ -114,6 +119,8 @@ }, "resolutions": { "@types/react": "18.3.24", - "@types/d3-dispatch": "3.0.6" + "@types/d3-dispatch": "3.0.6", + "styletron-react": "6.1.1", + "styletron-engine-atomic": "1.5.0" } -} \ No newline at end of file +} diff --git a/src/App.tsx b/src/App.tsx index 8742c635..0903936a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -13,9 +13,10 @@ import Leaderboard from './pages/Leaderboard'; import Loading from './components/Loading'; import Root from './pages/Root'; import ChallengeRoot from './pages/ChallengeRoot'; -import DocumentationWindow from './components/documentation/DocumentationWindow'; +//import DocumentationWindow from './components/documentation/DocumentationWindow'; +import { DocumentationWindow } from 'ivygate'; import AiWindow from './components/Ai/AiWindow'; -import { DARK } from './components/constants/theme'; +import { DARK, LIGHT } from './components/constants/theme'; import CurriculumPage from './lms/CurriculumPage'; import { UsersAction, I18nAction } from './state/reducer'; import db from './db'; @@ -67,6 +68,7 @@ type State = AppState; * Note: This component also maintains a private field `onAuthStateChangedSubscription_` for managing * the subscription to the authentication state changes. */ + class App extends React.Component { constructor(props: Props) { super(props); @@ -181,7 +183,9 @@ class App extends React.Component { } /> } /> - + + + {/* */} ); } diff --git a/src/components/Editor/Editor.tsx b/src/components/Editor/Editor.tsx index 73db3da8..9f03dd62 100644 --- a/src/components/Editor/Editor.tsx +++ b/src/components/Editor/Editor.tsx @@ -8,7 +8,7 @@ import { FontAwesome } from '../FontAwesome'; import { Button } from '../interface/Button'; import { BarComponent } from '../interface/Widget'; import { WarningCharm, ErrorCharm } from './'; - +import type { Ivygate as IvygateType } from 'ivygate'; import { Ivygate, Message } from 'ivygate'; import LanguageSelectCharm from './LanguageSelectCharm'; import ProgrammingLanguage from '../../programming/compiler/ProgrammingLanguage'; @@ -38,7 +38,7 @@ export interface EditorPublicProps extends StyleProps, ThemeProps { autocomplete: boolean; onDocumentationGoToFuzzy?: (query: string, language: 'c' | 'python' | 'scratch') => void; - + onCommonDocumentationGoToFuzzy?: (query: string, language: 'c' | 'python' | 'scratch') => void; mini?: boolean; } @@ -91,14 +91,14 @@ export const createEditorBarComponents = ({ target, locale }: { - theme: Theme, + theme: Theme, target: EditorBarTarget, locale: LocalizedString.Language }) => { - + // eslint-disable-next-line @typescript-eslint/ban-types const editorBar: BarComponent[] = []; - + switch (target.type) { case EditorBarTarget.Type.Robot: { let errors = 0; @@ -187,6 +187,10 @@ export const createEditorBarComponents = ({ export const IVYGATE_LANGUAGE_MAPPING: Dict = { 'ecmascript': 'javascript', + 'python': 'customPython', + 'c': 'customCpp', + 'cpp': 'customCpp', + 'plaintext': 'plaintext', }; const DOCUMENTATION_LANGUAGE_MAPPING: { [key in ProgrammingLanguage | Script.Language]?: 'c' | 'python' | 'scratch' | undefined } = { @@ -201,15 +205,33 @@ class Editor extends React.PureComponent { } private openDocumentation_ = () => { + console.log("Opening documentation from Editor"); const { word } = this.ivygate_.editor.getModel().getWordAtPosition(this.ivygate_.editor.getPosition()); const language = DOCUMENTATION_LANGUAGE_MAPPING[this.props.language]; if (!language) return; + console.log("word:", word, "language:", language); this.props.onDocumentationGoToFuzzy?.(word, language); - + + }; + + private openCommonDocumentation_ = () => { + console.log("Opening common documentation from Editor"); + const { word } = this.ivygate_.editor.getModel().getWordAtPosition(this.ivygate_.editor.getPosition()); + const language = DOCUMENTATION_LANGUAGE_MAPPING[this.props.language]; + if (!language) return; + console.log("word:", word, "language:", language); + this.props.onCommonDocumentationGoToFuzzy?.(word, language); }; + componentDidUpdate(prevProps: Readonly, prevState: Readonly, snapshot?: any): void { + console.log("Editor componentDidUpdate - props:", this.props, "prevProps:", prevProps); + + } private openDocumentationAction_?: monaco.IDisposable; + private openCommonDocumentationAction_?: monaco.IDisposable; private setupCodeEditor_ = (editor: monaco.editor.IStandaloneCodeEditor) => { + console.log("Setting up code editor actions in Editor"); + console.log("this.props.onDocumentationGoToFuzzy:", this.props.onDocumentationGoToFuzzy); if (this.props.onDocumentationGoToFuzzy) this.openDocumentationAction_ = editor.addAction({ id: 'open-documentation', label: 'Open Documentation', @@ -218,21 +240,35 @@ class Editor extends React.PureComponent { keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter], run: this.openDocumentation_, }); + + console.log("this.props.onCommonDocumentationGoToFuzzy:", this.props.onCommonDocumentationGoToFuzzy); + if (this.props.onCommonDocumentationGoToFuzzy) { + console.log("Setting up openCommonDocumentationAction_"); + this.openCommonDocumentationAction_ = editor.addAction({ + id: 'open-common-documentation', + label: 'Open Common Documentation', + contextMenuOrder: 1, + contextMenuGroupId: "operation", + keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter], + run: this.openCommonDocumentation_, + }); + } }; private disposeCodeEditor_ = (editor: monaco.editor.IStandaloneCodeEditor) => { if (this.openDocumentationAction_) this.openDocumentationAction_.dispose(); + if (this.openCommonDocumentationAction_) this.openCommonDocumentationAction_.dispose(); }; - private ivygate_: Ivygate; - private bindIvygate_ = (ivygate: Ivygate) => { + private ivygate_: IvygateType | null = null; + private bindIvygate_ = (ivygate: IvygateType) => { if (this.ivygate_ === ivygate) return; const old = this.ivygate_; this.ivygate_ = ivygate; if (this.ivygate_ && this.ivygate_.editor) { - this.setupCodeEditor_(this.ivygate_.editor as monaco.editor.IStandaloneCodeEditor); + this.setupCodeEditor_(this.ivygate_.editor as unknown as monaco.editor.IStandaloneCodeEditor); } else { - this.disposeCodeEditor_(old.editor as monaco.editor.IStandaloneCodeEditor); + this.disposeCodeEditor_(old.editor as unknown as monaco.editor.IStandaloneCodeEditor); } }; @@ -264,11 +300,14 @@ class Editor extends React.PureComponent { /> ); } else { + window.console.log("Rendering Ivygate with code:", code); + window.console.log("Rendering Ivygate with language:", language); + window.console.log("IVYGATE_LANGUAGE_MAPPING[language]:", IVYGATE_LANGUAGE_MAPPING[language]); component = ( { {component} - + ); } } diff --git a/src/components/Layout/Layout.ts b/src/components/Layout/Layout.ts index 7f223fa5..92e27068 100644 --- a/src/components/Layout/Layout.ts +++ b/src/components/Layout/Layout.ts @@ -65,6 +65,7 @@ export interface LayoutProps extends StyleProps, ThemeProps { challengeState?: ChallengeState; worldCapabilities?: Capabilities; onDocumentationGoToFuzzy?: (query: string, language: 'c' | 'python') => void; + onCommonDocumentationGoToFuzzy?: (query: string, language: 'c' | 'python') => void; } export enum Layout { diff --git a/src/components/Layout/OverlayLayout.tsx b/src/components/Layout/OverlayLayout.tsx index 9ae275f1..86b26396 100644 --- a/src/components/Layout/OverlayLayout.tsx +++ b/src/components/Layout/OverlayLayout.tsx @@ -24,7 +24,7 @@ import LocalizedString from '../../util/LocalizedString'; import tr from '@i18n'; export interface OverlayLayoutProps extends LayoutProps { - + } interface ReduxOverlayLayoutProps { @@ -76,7 +76,7 @@ const transparentStyling = (theme: Theme): React.CSSProperties => ({ backdropFilter: 'blur(16px)' }); -const ConsoleWidget = styled(Widget, (props: WidgetProps & { $challenge?: boolean; }) => { +const ConsoleWidget = styled(Widget, (props: WidgetProps & { $challenge?: boolean; }): any => { const size = props.sizes[props.size]; switch (size.type) { case Size.Type.Minimized: return { @@ -98,10 +98,10 @@ const ConsoleWidget = styled(Widget, (props: WidgetProps & { $challenge?: boolea gridRow: props.$challenge ? 3 : 2, ...transparentStyling(props.theme) }; - } + } }); -const EditorWidget = styled(Widget, (props: WidgetProps & { $challenge?: boolean; }) => { +const EditorWidget = styled(Widget, (props: WidgetProps & { $challenge?: boolean; }): any => { const size = props.sizes[props.size]; switch (size.type) { case Size.Type.Minimized: return { @@ -126,7 +126,7 @@ const EditorWidget = styled(Widget, (props: WidgetProps & { $challenge?: boolean } }); -const InfoWidget = styled(Widget, (props: WidgetProps & { $challenge?: boolean; }) => { +const InfoWidget = styled(Widget, (props: WidgetProps & { $challenge?: boolean; }): any => { const size = props.sizes[props.size]; switch (size.type) { case Size.Type.Minimized: return { @@ -141,22 +141,22 @@ const InfoWidget = styled(Widget, (props: WidgetProps & { $challenge?: boolean; } }); -const ChallengeWidget = styled(Widget, (props: WidgetProps) => { +const ChallengeWidget = styled(Widget, (props: WidgetProps & { $challenge?: boolean; }): any => { const size = props.sizes[props.size]; switch (size.type) { case Size.Type.Minimized: return { display: 'none' - }; + } as any; default: case Size.Type.Partial: return { gridColumn: 3, gridRow: 2, ...transparentStyling(props.theme) - }; + } as any; } }); -const WorldWidget = styled(Widget, (props: WidgetProps & { $challenge?: boolean; }) => { +const WorldWidget = styled(Widget, (props: WidgetProps & { $challenge?: boolean; }): any => { const size = props.sizes[props.size]; switch (size.type) { case Size.Type.Minimized: return { @@ -179,7 +179,7 @@ const CONSOLE_SIZES: Size[] = [Size.MINIATURE_LEFT, Size.PARTIAL_DOWN, Size.MAXI const sizeDict = (sizes: Size[]) => { const forward: { [type: number]: number } = {}; - + for (let i = 0; i < sizes.length; ++i) { const size = sizes[i]; forward[size.type] = i; @@ -216,7 +216,7 @@ export class OverlayLayout extends React.PureComponent { const { scene, onNodeChange } = this.props; - + const latestScene = Async.latestValue(scene); if (!latestScene) return; @@ -333,7 +333,7 @@ export class OverlayLayout extends React.PureComponent ); break; @@ -526,7 +530,7 @@ export const OverlayLayoutRedux = connect((state: ReduxState, { sceneId }: Layou const scene = Async.latestValue(asyncScene); let robots: Dict = {}; if (scene) robots = Scene.robots(scene); - + return { robots, locale: state.i18n.locale, diff --git a/src/components/Layout/SideLayout.tsx b/src/components/Layout/SideLayout.tsx index 9e9d6891..60758b15 100644 --- a/src/components/Layout/SideLayout.tsx +++ b/src/components/Layout/SideLayout.tsx @@ -197,6 +197,7 @@ export class SideLayout extends React.PureComponent ); break; diff --git a/src/components/constants/theme.ts b/src/components/constants/theme.ts index a98d2d47..381ca66d 100644 --- a/src/components/constants/theme.ts +++ b/src/components/constants/theme.ts @@ -2,6 +2,9 @@ export interface ButtonColor { disabled: string; standard: string; hover: string; + border?: string; + textColor?: string; + textShadow?: string; } export const GREEN: ButtonColor = Object.freeze({ @@ -10,6 +13,12 @@ export const GREEN: ButtonColor = Object.freeze({ hover: '#4aad52' }); +export const LIGHTMODE_GREEN: ButtonColor = Object.freeze({ + disabled: '#507255', + standard: '#89c28a', + hover: '#4aad52' +}); + export const RED: ButtonColor = Object.freeze({ disabled: '#735350', standard: '#8C494C', @@ -28,11 +37,78 @@ export const BROWN: ButtonColor = Object.freeze({ hover: '#ab8c49', }); +export const LIGHTMODE_YES: ButtonColor = Object.freeze({ + disabled: '#808080', + border: '#1f7a72', + standard: "#41af3c", + hover: "#51d94b", + textColor: 'white', + textShadow: '2px 2px 4px rgba(0,0,0,0.9)', +}); + +export const LIGHTMODE_NO: ButtonColor = Object.freeze({ + disabled: '#507255', + border: '#800000', + standard: "#cc0000", + hover: "#ff1a1a", + textColor: 'white', + textShadow: '2px 2px 4px rgba(0,0,0,0.9)', +}); + +export const DARKMODE_YES: ButtonColor = Object.freeze({ + disabled: '#5c665e', + standard: '#488b49', + hover: '#4aad52', + textColor: 'white', + textShadow: '2px 2px 4px rgba(0,0,0,0.9)', +}); + +export const DARKMODE_NO: ButtonColor = Object.freeze({ + disabled: '#735350', + standard: '#8C494C', + hover: '#AD4C4B', + textColor: 'white', + textShadow: '2px 2px 4px rgba(0,0,0,0.9)', + +}); + export interface Theme { + themeName: string; + foreground: 'white' | 'black'; - backgroundColor: string; - transparentBackgroundColor: (a: number) => string; color: string; + backgroundColor: string; + iconColor: string; + whiteText: string; + textColor: string; + cursorColor: string; + verticalLineColor: string; + titleBarBackground: string; + fileContainerBackground: string; + leftBarContainerBackground: string; + editorPageBackground: string; + startContainerBackground: string; + editorConsoleBackground: string; + mobileEditorBarBackground?: string; + editorBackground: string; + homeStartContainerBackground: string; + selectedUserBackground: string; + selectedProjectBackground: string; + selectedFileBackground: string; + hoverFileBackground: string; + hoverOptionBackground: string; + confirmMessageBackground: string; + successMessageBackground: string; + compileWarningColor: string; + dialogBoxTitleBackground: string; + unselectedBackground: string; + contextMenuBackground: string; + boxShadow: string; + + runButtonColor: ButtonColor; + yesButtonColor: ButtonColor; + noButtonColor: ButtonColor; + borderColor: string; borderRadius: number; widget: { @@ -49,16 +125,47 @@ export interface Theme { secondary: string; } }; + + transparentBackgroundColor: (a: number) => string; lighten: (frac: number) => string; darken: (frac: number) => string; } export const COMMON: Theme = { + themeName: 'common', foreground: undefined, backgroundColor: undefined, transparentBackgroundColor: undefined, color: undefined, + textColor: undefined, + cursorColor: undefined, + verticalLineColor: undefined, + titleBarBackground: undefined, + startContainerBackground: undefined, + homeStartContainerBackground: undefined, + selectedUserBackground: undefined, + selectedProjectBackground: undefined, + selectedFileBackground: undefined, + hoverFileBackground: undefined, + fileContainerBackground: undefined, + leftBarContainerBackground: undefined, + editorPageBackground: undefined, + editorConsoleBackground: undefined, + mobileEditorBarBackground: undefined, + confirmMessageBackground: undefined, + successMessageBackground: undefined, + compileWarningColor: undefined, + editorBackground: undefined, + yesButtonColor: undefined, + noButtonColor: undefined, + hoverOptionBackground: undefined, + dialogBoxTitleBackground: undefined, + whiteText: undefined, + unselectedBackground: undefined, borderColor: undefined, + runButtonColor: undefined, + contextMenuBackground: undefined, + boxShadow: undefined, borderRadius: 10, widget: { padding: 10 @@ -75,49 +182,139 @@ export const COMMON: Theme = { } }, lighten: undefined, - darken: undefined + darken: undefined, + + iconColor: undefined }; + +export const GRAPHICAL_LIGHT = { + toolbox: '#fbfbfb', + toolboxSelected: '#dadada', + toolboxText: "#212121", + toolboxHover: '#4C97FF', + flyout: '#fbfbfb', + workspace: '#fbfbfb', +} export const LIGHT: Theme = { ...COMMON, + themeName: 'LIGHT', + whiteText: 'white', + textColor: '#000000', + color: '#403f53', + cursorColor: '#000000', + borderColor: '#ede0e0', + iconColor: '#f5ebeb', foreground: 'white', - color: '#000', - backgroundColor: '#efefef', - borderColor: '#c0c0c0', - transparentBackgroundColor: a => `rgba(255, 255, 255, ${a})`, + verticalLineColor: 'black', + backgroundColor: '#ffffff', + titleBarBackground: '#f4ecec', + startContainerBackground: '#ebdbdc', + dialogBoxTitleBackground: '#e3cece', + editorPageBackground: '#FBFBFB', + editorConsoleBackground: '#fff6f7', + mobileEditorBarBackground: '#e6ddde', + editorBackground: '#fbfbfb', + contextMenuBackground: '#ffffff', + boxShadow: '0px 10px 13px -6px rgba(255, 105, 180, 0.1), 0px 1px 31px 0px rgba(135, 206, 250, 0.08), 0px 8px 38px 7px rgba(144, 238, 144, 0.1)', + + unselectedBackground: '#f4ebec', + fileContainerBackground: '#f4ecec', + leftBarContainerBackground: '#f4ecec', + homeStartContainerBackground: '#f4ebec', + confirmMessageBackground: '#ff4d4d', + successMessageBackground: '#5dd5cb', + compileWarningColor: '#c3c30f', + + selectedUserBackground: '#dadada', + selectedProjectBackground: '#dadada', + selectedFileBackground: '#d3e8f9', + + hoverFileBackground: '#e4f1fb', + hoverOptionBackground: '#e4f1fb', + + yesButtonColor: LIGHTMODE_YES, + noButtonColor: LIGHTMODE_NO, + runButtonColor: LIGHTMODE_GREEN, + + transparentBackgroundColor: (a) => `rgba(255, 255, 255, ${a})`, switch: { on: { - primary: 'rgb(63, 63, 63)', - secondary: 'rgb(72, 200, 73)' + primary: 'rgb(0, 0, 0)', + secondary: 'rgb(72, 139, 73)', }, off: { primary: 'rgb(127, 127, 127)', - secondary: 'rgba(0, 0, 0, 0.1)' - } + secondary: 'rgba(0, 0, 0, 0.1)', + }, }, - lighten: (frac: number) => `rgba(0, 0, 0, ${frac})`, - darken: (frac: number) => `rgba(255, 255, 255, ${frac})`, + lighten: (frac) => `rgba(0, 0, 0, ${frac})`, + darken: (frac) => `rgba(255, 255, 255, ${frac})`, }; +export const GRAPHICAL_DARK = { + toolbox: '#212121', + toolboxSelected: '#313131', + toolboxText: "#EEEEEE", + toolbBoxHover: '#4C97FF', + flyout: '#212121', + workspace: '#212121', + + +} + export const DARK: Theme = { ...COMMON, + themeName: 'DARK', + color: '#ffffff', + textColor: '#ffffff', + borderColor: '#323232', foreground: 'black', - color: '#fff', backgroundColor: '#212121', - transparentBackgroundColor: a => `rgba(${0x21}, ${0x21}, ${0x21}, ${a})`, - borderColor: '#323232', + verticalLineColor: 'white', + titleBarBackground: '#212121', + cursorColor: '#ffffff', + dialogBoxTitleBackground: '#212121', + editorPageBackground: '#212121', + editorConsoleBackground: '#212121', + mobileEditorBarBackground: "#1e1e1e", + editorBackground: '#212121', + contextMenuBackground: '#212121', + + fileContainerBackground: '#343436', + leftBarContainerBackground: '#212121', + homeStartContainerBackground: '#333333', + startContainerBackground: '#404040', + unselectedBackground: '#343436', + confirmMessageBackground: '#ff1a1a', + successMessageBackground: '#488b49', + compileWarningColor: '#fbfc6e', + boxShadow: '0px 10px 13px -6px rgba(100, 100, 120, 0.2), 0px 1px 31px 0px rgba(120, 120, 150, 0.12), 0px 8px 38px 1px rgba(160, 160, 180, 0.1)', + selectedUserBackground: '#3f3f3f', + selectedProjectBackground: '#3f3f3f', + selectedFileBackground: '#3f3f3f', + + hoverFileBackground: `rgba(255, 255, 255, 0.1)`, + hoverOptionBackground: `rgba(255, 255, 255, 0.1)`, + + + yesButtonColor: DARKMODE_YES, + noButtonColor: DARKMODE_NO, + runButtonColor: GREEN, + + transparentBackgroundColor: (a) => `rgba(${0x21}, ${0x21}, ${0x21}, ${a})`, switch: { on: { primary: 'rgb(255, 255, 255)', - secondary: 'rgb(72, 139, 73)' + secondary: 'rgb(72, 139, 73)', }, off: { primary: 'rgb(127, 127, 127)', - secondary: 'rgba(255, 255, 255, 0.1)' - } + secondary: 'rgba(255, 255, 255, 0.1)', + }, }, - lighten: (frac: number) => `rgba(255, 255, 255, ${frac})`, - darken: (frac: number) => `rgba(0, 0, 0, ${frac})`, + lighten: (frac) => `rgba(255, 255, 255, ${frac})`, + darken: (frac) => `rgba(0, 0, 0, ${frac})`, }; export interface ThemeProps { diff --git a/src/index.tsx b/src/index.tsx index 7a284460..2ecc59a7 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { createRoot } from 'react-dom/client'; import { Provider as ReduxProvider } from 'react-redux'; +import { SimReduxContext } from './state/context'; import { BrowserRouter } from 'react-router-dom'; import { Provider as StyletronProvider, DebugEngine } from "styletron-react"; @@ -22,9 +23,11 @@ if (!reactRoot) { } const root = createRoot(reactRoot); +console.log("IvyGate path:", require.resolve("ivygate")); + root.render( - + diff --git a/src/pages/LoginPage.tsx b/src/pages/LoginPage.tsx index a0d917dd..dc3e0819 100644 --- a/src/pages/LoginPage.tsx +++ b/src/pages/LoginPage.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { DARK, ThemeProps } from '../components/constants/theme'; +import { DARK, GREEN, ThemeProps } from '../components/constants/theme'; import { StyleProps } from '../util/style'; import { styled } from 'styletron-react'; import { auth, Providers } from '../firebase/firebase'; @@ -82,6 +82,21 @@ const StyledForm = styled(Form, (props: ThemeProps) => ({ paddingRight: `${props.theme.itemPadding * 2}px`, })); +const ResetInfoButton = styled('div', (props: ThemeProps & { disabled?: boolean }) => ({ + flex: '1 1', + borderRadius: `${props.theme.itemPadding * 2}px`, + padding: `${props.theme.itemPadding * 2}px`, + backgroundColor: props.disabled ? GREEN.disabled : GREEN.standard, + ':hover': props.disabled ? {} : { + backgroundColor: GREEN.hover, + }, + marginBottom: `${props.theme.itemPadding * 2}px`, + fontWeight: 400, + fontSize: '1.1em', + textAlign: 'center', + cursor: props.disabled ? 'auto' : 'pointer', +})); + const TABS: TabBar.TabDescription[] = [{ name: 'Sign In', icon: faSignInAlt, @@ -144,10 +159,10 @@ class LoginPage extends React.Component { console.log('onAuthStateChanged with user; getting user from db'); const handleUnexpectedConsentError_ = (error: unknown) => { - if (typeof(error) !== 'undefined') { + if (typeof (error) !== 'undefined') { console.log('Unknown login failure:', error); } - + this.setState({ loggedIn: false, initialAuthLoaded: true, authenticating: false, userConsent: undefined, logInFailedMessage: 'Something went wrong' }); }; @@ -237,7 +252,7 @@ class LoginPage extends React.Component { authenticating: true, logInFailedMessage: null, }); - + let newUserCredential: UserCredential; try { newUserCredential = await createUserWithEmail(email, password); @@ -372,7 +387,7 @@ class LoginPage extends React.Component { return; } - + const userId = auth.currentUser.uid; const dob = this.state.userConsent.dateOfBirth; @@ -418,6 +433,35 @@ class LoginPage extends React.Component { }); }; + private resetParentalConsentInfo_ = () => { + const userId = auth.currentUser.uid; + + const userConsentPatch: Partial = { + legalAcceptance: { + state: LegalAcceptance.State.NotStarted, + version: 1, + expiresAt: Date.now() + 1000 * 60 * 60 * 24 * 2, + }, + }; + + this.setState({ authenticating: true }, () => { + db.set>(Selector.user(userId), userConsentPatch, true) + .then(() => { + this.setState((prevState) => ({ + authenticating: false, + userConsent: { + ...prevState.userConsent, + ...userConsentPatch, + }, + })); + }) + .catch((error) => { + console.error('Resetting parental consent info failed', error); + this.setState({ authenticating: false }); + }); + }); + } + private getAge = (dob: Date) => { const today = new Date(); let age = today.getUTCFullYear() - dob.getUTCFullYear(); @@ -491,6 +535,28 @@ class LoginPage extends React.Component { marginBottom: '8px', } })} /> + + + + + this.resetParentalConsentInfo_()} + > + Reset Parental/Guardian Consent Information + + ); diff --git a/src/pages/Root.tsx b/src/pages/Root.tsx index 23149021..23d0d563 100644 --- a/src/pages/Root.tsx +++ b/src/pages/Root.tsx @@ -24,7 +24,7 @@ import AboutDialog from '../components/Dialog/AboutDialog'; import SceneSettingsDialog from '../components/Dialog/SceneSettingsDialog'; import { FeedbackDialog, DEFAULT_FEEDBACK, Feedback, FeedbackSuccessDialog, sendFeedback, FeedbackResponse } from '../components/Feedback'; -import { Layout, LayoutProps, LayoutEditorTarget, OverlayLayout, OverlayLayoutRedux, SideLayoutRedux } from '../components/Layout'; +import { Layout, LayoutProps, LayoutEditorTarget, OverlayLayout, OverlayLayoutRedux, SideLayoutRedux } from '../components/Layout'; import { SceneErrorDialog, OpenSceneDialog, NewSceneDialog, DeleteDialog, SaveAsSceneDialog } from '../components/Dialog'; import Loading from '../components/Loading'; @@ -32,7 +32,8 @@ import { Editor } from '../components/Editor'; import { State as ReduxState } from '../state'; -import { DocumentationAction, ScenesAction, ChallengeCompletionsAction, AiAction } from '../state/reducer'; +import { ScenesAction, ChallengeCompletionsAction, AiAction } from '../state/reducer'; +import { DocumentationAction } from 'ivygate/dist/state/reducer/documentation'; import { sendMessage, SendMessageParams } from '../util/ai'; import Scene, { AsyncScene } from '../state/State/Scene'; @@ -104,6 +105,7 @@ interface RootPrivateProps { onDocumentationPush: (location: DocumentationLocation) => void; onDocumentationSetLanguage: (language: 'c' | 'python') => void; onDocumentationGoToFuzzy: (query: string, language: 'c' | 'python') => void; + onCommonDocumentationGoToFuzzy: (query: string, language: 'c' | 'python') => void; onCreateScene: (id: string, scene: Scene) => void; onSaveScene: (id: string) => void; @@ -150,7 +152,7 @@ interface RootState { windowInnerHeight: number; miniEditor: boolean; - + } type Props = RootPublicProps & RootPrivateProps & WithNavigateProps; @@ -180,9 +182,9 @@ const STDERR_STYLE = (theme: Theme) => ({ class Root extends React.Component { private editorRef: React.MutableRefObject; - private overlayLayoutRef: React.MutableRefObject; - - + private overlayLayoutRef: React.MutableRefObject; + + constructor(props: Props) { super(props); @@ -204,12 +206,12 @@ class Root extends React.Component { feedback: DEFAULT_FEEDBACK, windowInnerHeight: window.innerHeight, miniEditor: true - + }; this.editorRef = React.createRef(); this.overlayLayoutRef = React.createRef(); - + console.log("Initial state:", this.state); Space.getInstance().scene = Async.latestValue(props.scene) || Scene.EMPTY; } @@ -234,12 +236,12 @@ class Root extends React.Component { componentWillUnmount() { window.removeEventListener('resize', this.onWindowResize_); cancelAnimationFrame(this.updateConsoleHandle_); - + Space.getInstance().onSelectNodeId = undefined; Space.getInstance().onSetNodeBatch = undefined; } - componentDidUpdate(prevProps: Readonly, prevState: Readonly): void { + componentDidUpdate(prevProps: Readonly, prevState: Readonly): void { if (this.props.scene !== prevProps.scene) { Space.getInstance().scene = Async.latestValue(this.props.scene) || Scene.EMPTY; } @@ -267,14 +269,20 @@ class Root extends React.Component { }; private onActiveLanguageChange_ = (language: ProgrammingLanguage) => { + + console.log("Changing active language to:", language); + console.log("State before language change:", this.state); this.setState({ activeLanguage: language }, () => { + console.log("State after language change:", this.state); this.props.onDocumentationSetLanguage(language === 'python' ? 'python' : 'c'); }); }; private onCodeChange_ = (code: string) => { + + console.log("Code changed in editor:", code); const { activeLanguage } = this.state; this.setState({ code: { @@ -282,6 +290,7 @@ class Root extends React.Component { [activeLanguage]: code, } }, () => { + console.log("Updated code state:", this.state.code); window.localStorage.setItem(`code-${activeLanguage}`, code); }); }; @@ -303,7 +312,7 @@ class Root extends React.Component { private onModalClick_ = (modal: Modal) => () => this.setState({ modal }); private onModalClose_ = () => this.setState({ modal: Modal.NONE }); - + private updateConsole_ = () => { const text = WorkerInstance.sharedConsole.popString(); if (text.length > 0) { @@ -314,7 +323,7 @@ class Root extends React.Component { }), 300) }); } - + this.scheduleUpdateConsole_(); }; @@ -340,7 +349,7 @@ class Root extends React.Component { text: LocalizedString.lookup(tr('Compiling...\n'), locale), style: STDOUT_STYLE(this.state.theme) })); - + this.setState({ simulatorState: SimulatorState.COMPILING, console: nextConsole @@ -350,7 +359,7 @@ class Root extends React.Component { nextConsole = this.state.console; const messages = sort(parseMessages(compileResult.stderr)); const compileSucceeded = compileResult.result && compileResult.result.length > 0; - + // Show all errors/warnings in console for (const message of messages) { nextConsole = StyledText.extend(nextConsole, toStyledText(message, { @@ -359,7 +368,7 @@ class Root extends React.Component { : undefined })); } - + if (compileSucceeded) { // Show success in console and start running the program const haveWarnings = hasWarnings(messages); @@ -369,7 +378,7 @@ class Root extends React.Component { : LocalizedString.lookup(tr('Compilation succeeded.\n'), locale), style: STDOUT_STYLE(this.state.theme) })); - + WorkerInstance.start({ language: activeLanguage, code: compileResult.result @@ -383,13 +392,13 @@ class Root extends React.Component { style: STDERR_STYLE(this.state.theme) })); } - + nextConsole = StyledText.extend(nextConsole, StyledText.text({ text: LocalizedString.lookup(tr('Compilation failed.\n'), locale), style: STDERR_STYLE(this.state.theme) })); } - + this.setState({ simulatorState: compileSucceeded ? SimulatorState.RUNNING : SimulatorState.STOPPED, messages, @@ -402,7 +411,7 @@ class Root extends React.Component { text: LocalizedString.lookup(tr('Something went wrong during compilation.\n'), locale), style: STDERR_STYLE(this.state.theme) })); - + this.setState({ simulatorState: SimulatorState.STOPPED, messages: [], @@ -436,7 +445,7 @@ class Root extends React.Component { } } - + }; private onStopClick_ = () => { @@ -462,7 +471,7 @@ class Root extends React.Component { private onStartChallengeClick_ = () => { window.location.href = `/challenge/${this.props.params.sceneId}`; }; - + private onClearConsole_ = () => { this.setState({ console: StyledText.compose({ items: [] }) @@ -473,16 +482,17 @@ class Root extends React.Component { const { activeLanguage, code, console } = this.state; const currentCode = code[activeLanguage]; const consoleText = StyledText.toString(console); - + // Create a message for the tutor with the code and errors const message = `I'm having trouble with my ${activeLanguage} program. Here's my code:\n\n\`\`\`${activeLanguage}\n${currentCode}\n\`\`\`\n\nAnd here are the errors I'm seeing:\n\n${consoleText}`; - + // Add the message to the tutor chat this.props.onAiClick(); this.props.onAddUserMessage(message); }; private onIndentCode_ = () => { + console.log("Indenting code in editor"); if (this.editorRef.current) this.editorRef.current.ivygate.formatCode(); }; @@ -502,7 +512,7 @@ class Root extends React.Component { modal: Modal.NONE, }); }; - + onDocumentationClick_ = () => { this.props.onDocumentationClick(); }; @@ -541,7 +551,7 @@ class Root extends React.Component { if ('simulationRealisticSensors' in changedSettings) { Space.getInstance().realisticSensors = changedSettings.simulationRealisticSensors; } - + if ('simulationSensorNoise' in changedSettings) { Space.getInstance().noisySensors = changedSettings.simulationSensorNoise; } @@ -632,7 +642,7 @@ class Root extends React.Component { render() { const { props, state } = this; - + const { params: { sceneId, challengeId }, scene, @@ -650,6 +660,7 @@ class Root extends React.Component { selectedScriptId, onDocumentationClick, onDocumentationGoToFuzzy, + onCommonDocumentationGoToFuzzy, } = props; const { @@ -664,11 +675,13 @@ class Root extends React.Component { feedback, windowInnerHeight, miniEditor - + } = state; const theme = DARK; + //window.console.log("code[activelanguage]:", state.code[state.activeLanguage]); + const editorTarget: LayoutEditorTarget = { type: LayoutEditorTarget.Type.Robot, code: code[activeLanguage], @@ -708,6 +721,7 @@ class Root extends React.Component { challengeCompletion: challengeCompletion || Async.unloaded({ brief: {} }), } : undefined, onDocumentationGoToFuzzy, + onCommonDocumentationGoToFuzzy, }; let impl: JSX.Element; @@ -891,7 +905,7 @@ const ConnectedRoot = connect((state: ReduxState, { params: { sceneId, challenge challengeCompletion: Dict.unique(builder.challengeCompletions), sceneHasChallenge, locale: state.i18n.locale, - robots: Dict.map(state.robots.robots, Async.latestValue), + robots: Dict.map(state.robots.robots, Async.latestValue), }; }, (dispatch, { params: { sceneId } }: RootPublicProps) => ({ onNodeAdd: (nodeId: string, node: Node) => dispatch(ScenesAction.setNode({ sceneId, nodeId, node })), @@ -948,6 +962,7 @@ const ConnectedRoot = connect((state: ReduxState, { params: { sceneId, challenge onDocumentationPush: (location: DocumentationLocation) => dispatch(DocumentationAction.pushLocation({ location })), onDocumentationSetLanguage: (language: 'c' | 'python') => dispatch(DocumentationAction.setLanguage({ language })), onDocumentationGoToFuzzy: (query: string, language: 'c' | 'python') => dispatch(DocumentationAction.goToFuzzy({ query, language })), + onCommonDocumentationGoToFuzzy: (query: string, language: 'c' | 'python') => dispatch(DocumentationAction.goToFuzzyCommon({ query, language })), onSaveScene: (sceneId: string) => dispatch(ScenesAction.saveScene({ sceneId })), onSetScenePartial: (partialScene: Partial) => dispatch(ScenesAction.setScenePartial({ sceneId, partialScene })), unfailScene: (sceneId: string) => dispatch(ScenesAction.unfailScene({ sceneId })), diff --git a/src/state/context.ts b/src/state/context.ts new file mode 100644 index 00000000..2b8d4f68 --- /dev/null +++ b/src/state/context.ts @@ -0,0 +1,2 @@ +import { ReactReduxContext } from 'react-redux'; +export { ReactReduxContext as SimReduxContext }; diff --git a/src/state/index.ts b/src/state/index.ts index a9b43590..ba8aba4b 100644 --- a/src/state/index.ts +++ b/src/state/index.ts @@ -8,7 +8,8 @@ import { AsyncScene } from './State/Scene'; import { CHALLENGE_COLLECTION, CHALLENGE_COMPLETION_COLLECTION, SCENE_COLLECTION, ASSIGNMENT_COLLECTION, USER_COLLECTION } from '../db/constants'; import Record from '../db/Record'; import Selector from '../db/Selector'; - +//import { reduceDocumentation, reduceDocumentationCommon } from 'ivygate/src/state/reducer/documentation'; +import { reduceDocumentation, reduceDocumentationCommon } from 'ivygate/dist/state/reducer/documentation'; // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; @@ -17,7 +18,9 @@ const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || export default createStore(combineReducers({ scenes: reducer.reduceScenes, robots: reducer.reduceRobots, - documentation: reducer.reduceDocumentation, + documentation: reduceDocumentation, + documentationDefault: reduceDocumentation, + documentationCommon: reduceDocumentationCommon, challenges: reducer.reduceChallenges, challengeCompletions: reducer.reduceChallengeCompletions, i18n: reducer.reduceI18n, @@ -37,6 +40,8 @@ export interface State { challengeCompletions: ChallengeCompletions; robots: Robots; documentation: DocumentationState; + documentationDefault: DocumentationState; + documentationCommon: DocumentationState; i18n: I18n; assignments: Assignments; users: Users; diff --git a/src/state/reducer/index.ts b/src/state/reducer/index.ts index 9dc0a604..60bd86d8 100644 --- a/src/state/reducer/index.ts +++ b/src/state/reducer/index.ts @@ -2,7 +2,7 @@ export * from './scenes'; export * from './robots'; export * from './challenges'; export * from './challengeCompletions'; -export * from './documentation'; +//export * from './documentation'; export * from './i18n'; export * from './assignments'; export * from './users';