diff --git a/app.py b/app.py index 761f0aa..0541fe8 100644 --- a/app.py +++ b/app.py @@ -114,6 +114,10 @@ def map_isolator_response_to_flask_response(response): def home(): return render_template("home.html", name='Try me', script=DEFAULT_SCRIPT, environment_info=DEFAULT_ENVIRONMENT) +@app.route("/documentation", methods=["GET"]) +def documentation(): + return render_template("documentation.html") + @ app.route("/dashboard", methods=["GET"]) @login_required def dashboard(): @@ -257,6 +261,115 @@ def get_http_function_history(function_id): return render_block(app.jinja_env, 'dashboard/http_functions/history.html', 'page', user_id=user_id, function_id=function_id, name=name, http_function=http_function, http_function_history=http_function_history, original_script=original_script) return render_template("dashboard/http_functions/history.html", user_id=user_id, name=name, function_id=function_id, http_function=http_function, http_function_history=http_function_history, original_script=original_script) +def _generate_script_from_natural_language(natural_query): + """Generates a Javascript function from natural language using Gemini REST API.""" + gemni_model = os.environ.get("GEMINI_MODEL", "gemini-1.5-flash") + api_key = os.environ.get("GEMINI_API_KEY") + if not api_key: + return None, "GEMINI_API_KEY environment variable not set." + + api_url = f"https://generativelanguage.googleapis.com/v1beta/models/{gemni_model}:generateContent?key={api_key}" + headers = {'Content-Type': 'application/json'} + + try: + prompt = f""" +You are an expert Javascript developer. Your task is to write a complete, production-ready Javascript async function based on the user's request. + +**Environment & Constraints:** +- The function will be executed in a simple, sandboxed Javascript environment. +- **DO NOT** use `require()`, `import`, or any other module loading system. The environment does not support it. +- **DO NOT** access the file system (`fs` module) or make network requests. +- The function must be a single `async` arrow function. +- You have access to a persistent JSON object called `environment`. You can read from and write to it. For example: `environment.my_variable = 'hello'`. +- You also have access to a `console.log()` function for logging. + +**Input:** +The function receives one argument: `req`, an object containing details about the incoming HTTP request. It has properties like `req.method`, `req.headers`, `req.body`, `req.query`, etc. + +**Output:** +You must use one of the following helper functions to return a response: +- `HtmlResponse(body)`: Returns an HTML response. +- `JsonResponse(body)`: Returns a JSON response. The `body` will be automatically stringified. +- `TextResponse(body)`: Returns a plain text response. +- `RedirectResponse(url)`: Redirects the user to a different URL. + +**Example:** +```javascript +async (req) => {{ + if (!environment.counter) {{ + environment.counter = 0; + }} + environment.counter++; + console.log("The counter is now " + environment.counter); + return HtmlResponse(`

Counter: ${{environment.counter}}

`); +}} +``` + +**User's request:** "{natural_query}" + +Return ONLY the complete Javascript function code, without any explanation, comments, or surrounding text/markdown. +""" + payload = json.dumps({ + "contents": [{"parts": [{"text": prompt}]}] + }) + + response = requests.post(api_url, headers=headers, data=payload) + response.raise_for_status() + + response_data = response.json() + + candidates = response_data.get('candidates', []) + if not candidates: + return None, "No candidates found in API response." + + content = candidates[0].get('content', {}) + parts = content.get('parts', []) + if not parts: + return None, "No parts found in API response content." + + generated_script = parts[0].get('text', '').strip() + + # More robustly extract from markdown + if generated_script.startswith("```javascript"): + generated_script = generated_script[12:] + if generated_script.endswith("```"): + generated_script = generated_script[:-3] + + generated_script = generated_script.strip() + + # Remove any leading non-code characters + if not generated_script.startswith('async ('): + async_start = generated_script.find('async (') + if async_start != -1: + generated_script = generated_script[async_start:] + + return generated_script.strip(), None + + except requests.exceptions.RequestException as e: + app.logger.error(f"Gemini API request error: {e}") + return None, f"Error communicating with API: {e}" + except (KeyError, IndexError, Exception) as e: + app.logger.error(f"Error processing Gemini API response: {e} - Response: {response_data if 'response_data' in locals() else 'N/A'}") + return None, f"Error processing API response: {e}" + +@app.route("/api/generate_script", methods=["POST"]) +@login_required +def generate_script(): + try: + natural_query = request.json.get('natural_query') + if not natural_query: + return jsonify({"error": "natural_query is required"}), 400 + + script_content, error = _generate_script_from_natural_language(natural_query) + + if error: + return jsonify({"error": error}), 500 + + return jsonify({"script_content": script_content}) + + except Exception as e: + return jsonify({'error': str(e)}), 500 + @app.route('/execute', methods=['POST']) def execute_code(): try: diff --git a/static/js/mithril/editor.js b/static/js/mithril/editor.js index 0c4a14c..ed35c5c 100644 --- a/static/js/mithril/editor.js +++ b/static/js/mithril/editor.js @@ -58,17 +58,22 @@ const Editor = { // New timer-specific props this.isTimer = vnode.attrs.isTimer || false; - this.triggerType = vnode.attrs.triggerType || 'interval'; // 'interval' or 'date' + this.triggerType = vnode.attrs.triggerType || "interval"; // 'interval' or 'date' this.frequencyMinutes = vnode.attrs.frequencyMinutes || 60; - this.runDate = vnode.attrs.runDate || ''; + this.runDate = vnode.attrs.runDate || ""; // Show timer settings panel this.showTimerSettings = vnode.attrs.showTimerSettings === true; // default false - this.cancelUrl = vnode.attrs.cancelUrl || '/dashboard/http_functions'; + this.cancelUrl = vnode.attrs.cancelUrl || "/dashboard/http_functions"; // Add enabled property for timer functions this.isEnabled = vnode.attrs.isEnabled !== false; // default true + + // New state for AI generation + this.naturalLanguageQuery = ""; + this.generateLoading = false; + this.showNaturalLanguageQuery = false; }, oncreate() { @@ -142,44 +147,52 @@ const Editor = { environment_info: this.jsonValue, is_public: this.isPublic, log_request: this.logRequest, - log_response: this.logResponse + log_response: this.logResponse, }; // Create payload based on whether this is a timer function - payload = this.isTimer ? { - name: this.name, - script_content: this.jsValue, - environment_info: this.jsonValue, - trigger_type: this.triggerType, - frequency_minutes: this.triggerType === 'interval' ? parseInt(this.frequencyMinutes) : null, - run_date: this.triggerType === 'date' ? this.runDate : null, - is_enabled: this.isEnabled // Add enabled status to payload - } : { - name: this.name, - script_content: this.jsValue, - environment_info: this.jsonValue - }; + payload = this.isTimer + ? { + name: this.name, + script_content: this.jsValue, + environment_info: this.jsonValue, + trigger_type: this.triggerType, + frequency_minutes: + this.triggerType === "interval" + ? parseInt(this.frequencyMinutes) + : null, + run_date: this.triggerType === "date" ? this.runDate : null, + is_enabled: this.isEnabled, // Add enabled status to payload + } + : { + name: this.name, + script_content: this.jsValue, + environment_info: this.jsonValue, + }; const response = await m.request({ - method: 'POST', + method: "POST", url: this.saveUrl, - body: payload + body: payload, }); - if (response.status === 'success') { + if (response.status === "success") { if (this.isAdd) { window.location.href = this.dashboardUrl; } else { // Increment version number after successful save this.versionNumber = (parseInt(this.versionNumber) + 1).toString(); } - Alert.show(response.message || 'Function saved successfully!', 'success'); + Alert.show( + response.message || "Function saved successfully!", + "success" + ); } else { - Alert.show(response.message || 'Error saving function', 'error'); + Alert.show(response.message || "Error saving function", "error"); this.error = new Error(response.message); } } catch (err) { - Alert.show(err?.response.message || 'Error saving function', 'error'); + Alert.show(err?.response.message || "Error saving function", "error"); this.error = err?.response; } finally { this.saveLoading = false; @@ -188,7 +201,7 @@ const Editor = { }, async delete() { - if (!confirm('Are you sure you want to delete this function?')) { + if (!confirm("Are you sure you want to delete this function?")) { return; } @@ -197,20 +210,23 @@ const Editor = { try { const response = await m.request({ - method: 'DELETE', - url: this.deleteUrl + method: "DELETE", + url: this.deleteUrl, }); - if (response.status === 'success') { - Alert.show(response.message || 'Function deleted successfully!', 'success'); + if (response.status === "success") { + Alert.show( + response.message || "Function deleted successfully!", + "success" + ); // Optionally redirect to a different page after deletion window.location.href = this.cancelUrl; } else { - Alert.show(response.message || 'Error deleting function', 'error'); + Alert.show(response.message || "Error deleting function", "error"); this.error = new Error(response.message); } } catch (err) { - Alert.show(err.message || 'Error deleting function', 'error'); + Alert.show(err.message || "Error deleting function", "error"); this.error = err; } finally { this.deleteLoading = false; @@ -218,6 +234,31 @@ const Editor = { } }, + async generateWithAI() { + this.generateLoading = true; + this.error = null; + m.redraw(); + + try { + const resp = await m.request({ + method: "POST", + url: "/api/generate_script", // Assuming this is the new endpoint + body: { natural_query: this.naturalLanguageQuery }, + }); + + if (resp.script_content) { + this.editorJS.setValue(resp.script_content, -1); + } else if (resp.error) { + throw new Error(resp.error); + } + } catch (err) { + this.error = err; + } finally { + this.generateLoading = false; + m.redraw(); + } + }, + view() { return m("div", { class: "" }, [ /* ───────────────────────────────────────────────────────────────── @@ -350,6 +391,49 @@ const Editor = { ] ), + /* ───────────────────────────────────────────────────────────────── + AI Generation + ─────────────────────────────────────────────────────────────────*/ + m("div", { class: "p-2 border-b border-gray-200 dark:border-gray-800" }, [ + m( + "button", + { + class: "text-sm text-blue-500 hover:underline", + onclick: () => + (this.showNaturalLanguageQuery = !this.showNaturalLanguageQuery), + }, + "Generate with AI" + ), + + this.showNaturalLanguageQuery && + m("div", { class: "mt-2" }, [ + m("textarea", { + class: + "w-full p-2 border rounded bg-gray-50 dark:bg-gray-700 dark:border-gray-600", + rows: 3, + placeholder: + "Enter a description of what you want this function to do...", + oninput: (e) => (this.naturalLanguageQuery = e.target.value), + value: this.naturalLanguageQuery, + }), + m( + "button", + { + class: + "mt-2 px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600 disabled:opacity-50", + onclick: () => this.generateWithAI(), + disabled: this.generateLoading, + }, + this.generateLoading + ? m("div", { + class: + "animate-spin h-4 w-4 border-2 border-white border-t-transparent rounded-full", + }) + : "Generate" + ), + ]), + ]), + /* ───────────────────────────────────────────────────────────────── JS Editor ─────────────────────────────────────────────────────────────────*/ @@ -419,170 +503,248 @@ const Editor = { ), // Function settings panel - this.showFunctionSettings && m("div", { class: "bg-gray-100 dark:bg-gray-800 p-4 border-b" }, [ - // Settings group - m("div", { class: "flex flex-col space-y-4" }, [ - // Toggles group - m("div", { class: "flex flex-wrap gap-6" }, [ - // Public/Private toggle - this.showPublicToggle && m("label", { - class: "flex items-center space-x-3 text-sm text-gray-600 dark:text-gray-300 cursor-pointer" - }, [ - m("div", { class: "relative" }, [ - m("input[type=checkbox]", { - class: "sr-only peer", - checked: this.isPublic, - onchange: (e) => this.isPublic = e.target.checked - }), - m("div", { - class: "w-11 h-6 bg-gray-200 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600" - }) - ]), - m("span", "Public Function") - ]), - - // Log Request toggle - this.showLogRequestToggle && m("label", { - class: "flex items-center space-x-3 text-sm text-gray-600 dark:text-gray-300 cursor-pointer" - }, [ - m("div", { class: "relative" }, [ - m("input[type=checkbox]", { - class: "sr-only peer", - checked: this.logRequest, - onchange: (e) => this.logRequest = e.target.checked - }), - m("div", { - class: "w-11 h-6 bg-gray-200 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600" - }) - ]), - m("span", "Log Requests") - ]), - - // Log Response toggle - this.showLogResponseToggle && m("label", { - class: "flex items-center space-x-3 text-sm text-gray-600 dark:text-gray-300 cursor-pointer" - }, [ - m("div", { class: "relative" }, [ - m("input[type=checkbox]", { - class: "sr-only peer", - checked: this.logResponse, - onchange: (e) => this.logResponse = e.target.checked - }), - m("div", { - class: "w-11 h-6 bg-gray-200 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600" - }) - ]), - m("span", "Log Responses") - ]) - ]), - - // Timer settings (shown only if isTimer is true) - this.isTimer && this.showTimerSettings && [ - // Enabled toggle - m("label", { - class: "flex items-center space-x-3 text-sm text-gray-600 dark:text-gray-300 cursor-pointer mt-4" - }, [ - m("div", { class: "relative" }, [ - m("input[type=checkbox]", { - class: "sr-only peer", - checked: this.isEnabled, - onchange: (e) => this.isEnabled = e.target.checked - }), - m("div", { - class: "w-11 h-6 bg-gray-200 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600" - }) - ]), - m("span", "Enabled") - ]), - - // Timer settings group - m("div", { class: "grid grid-cols-2 gap-4 mt-4" }, [ - // Trigger Type Selection - m("div", { class: "flex flex-col space-y-2" }, [ - m("label", { class: "text-sm font-medium text-gray-700 dark:text-gray-300" }, - "Trigger Type" + this.showFunctionSettings && + m("div", { class: "bg-gray-100 dark:bg-gray-800 p-4 border-b" }, [ + // Settings group + m("div", { class: "flex flex-col space-y-4" }, [ + // Toggles group + m("div", { class: "flex flex-wrap gap-6" }, [ + // Public/Private toggle + this.showPublicToggle && + m( + "label", + { + class: + "flex items-center space-x-3 text-sm text-gray-600 dark:text-gray-300 cursor-pointer", + }, + [ + m("div", { class: "relative" }, [ + m("input[type=checkbox]", { + class: "sr-only peer", + checked: this.isPublic, + onchange: (e) => (this.isPublic = e.target.checked), + }), + m("div", { + class: + "w-11 h-6 bg-gray-200 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600", + }), + ]), + m("span", "Public Function"), + ] ), - m("select", { - class: "bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg px-3 py-2", - value: this.triggerType, - onchange: (e) => this.triggerType = e.target.value - }, [ - m("option", { value: "interval" }, "Interval"), - m("option", { value: "date" }, "Specific Date") - ]) - ]), - // Interval Settings or Date Settings based on triggerType - this.triggerType === 'interval' ? - m("div", { class: "flex flex-col space-y-2" }, [ - m("label", { class: "text-sm font-medium text-gray-700 dark:text-gray-300" }, - "Frequency (minutes)" - ), - m("input[type=number]", { - class: "bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg px-3 py-2", - value: this.frequencyMinutes, - min: 1, - onchange: (e) => this.frequencyMinutes = e.target.value - }) - ]) : - m("div", { class: "flex flex-col space-y-2" }, [ - m("label", { class: "text-sm font-medium text-gray-700 dark:text-gray-300" }, - "Run Date" - ), - m("input[type=datetime-local]", { - class: "bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg px-3 py-2", - value: this.runDate, - onchange: (e) => this.runDate = e.target.value - }) - ]) - ]) - ], + // Log Request toggle + this.showLogRequestToggle && + m( + "label", + { + class: + "flex items-center space-x-3 text-sm text-gray-600 dark:text-gray-300 cursor-pointer", + }, + [ + m("div", { class: "relative" }, [ + m("input[type=checkbox]", { + class: "sr-only peer", + checked: this.logRequest, + onchange: (e) => (this.logRequest = e.target.checked), + }), + m("div", { + class: + "w-11 h-6 bg-gray-200 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600", + }), + ]), + m("span", "Log Requests"), + ] + ), - // Actions group - m("div", { class: "flex items-center justify-end space-x-3 pt-2" }, [ - // Save button - this.showSaveButton && m("button", { - class: "px-4 py-2 bg-blue-500 text-white text-sm font-medium rounded-lg hover:bg-blue-600 transition-colors flex items-center space-x-2 disabled:opacity-50", - onclick: () => this.save(), - disabled: this.saveLoading - }, [ - this.saveLoading && m("div", { - class: "animate-spin h-4 w-4 border-2 border-white border-t-transparent rounded-full" - }), - m("span", this.saveLoading ? "Saving..." : "Save Function") + // Log Response toggle + this.showLogResponseToggle && + m( + "label", + { + class: + "flex items-center space-x-3 text-sm text-gray-600 dark:text-gray-300 cursor-pointer", + }, + [ + m("div", { class: "relative" }, [ + m("input[type=checkbox]", { + class: "sr-only peer", + checked: this.logResponse, + onchange: (e) => (this.logResponse = e.target.checked), + }), + m("div", { + class: + "w-11 h-6 bg-gray-200 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600", + }), + ]), + m("span", "Log Responses"), + ] + ), ]), - // Delete button - this.showDeleteButton && m("button", { - class: "px-4 py-2 bg-white text-red-600 text-sm font-medium border border-red-200 rounded-lg hover:bg-red-50 transition-colors flex items-center space-x-2 disabled:opacity-50", - onclick: () => this.delete(), - disabled: this.deleteLoading - }, [ - this.deleteLoading && m("div", { - class: "animate-spin h-4 w-4 border-2 border-red-600 border-t-transparent rounded-full" - }), - m("span", this.deleteLoading ? "Deleting..." : "Delete Function") - ]) - ]) - ]) - ]), + // Timer settings (shown only if isTimer is true) + this.isTimer && + this.showTimerSettings && [ + // Enabled toggle + m( + "label", + { + class: + "flex items-center space-x-3 text-sm text-gray-600 dark:text-gray-300 cursor-pointer mt-4", + }, + [ + m("div", { class: "relative" }, [ + m("input[type=checkbox]", { + class: "sr-only peer", + checked: this.isEnabled, + onchange: (e) => (this.isEnabled = e.target.checked), + }), + m("div", { + class: + "w-11 h-6 bg-gray-200 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600", + }), + ]), + m("span", "Enabled"), + ] + ), + + // Timer settings group + m("div", { class: "grid grid-cols-2 gap-4 mt-4" }, [ + // Trigger Type Selection + m("div", { class: "flex flex-col space-y-2" }, [ + m( + "label", + { + class: + "text-sm font-medium text-gray-700 dark:text-gray-300", + }, + "Trigger Type" + ), + m( + "select", + { + class: + "bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg px-3 py-2", + value: this.triggerType, + onchange: (e) => (this.triggerType = e.target.value), + }, + [ + m("option", { value: "interval" }, "Interval"), + m("option", { value: "date" }, "Specific Date"), + ] + ), + ]), + + // Interval Settings or Date Settings based on triggerType + this.triggerType === "interval" + ? m("div", { class: "flex flex-col space-y-2" }, [ + m( + "label", + { + class: + "text-sm font-medium text-gray-700 dark:text-gray-300", + }, + "Frequency (minutes)" + ), + m("input[type=number]", { + class: + "bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg px-3 py-2", + value: this.frequencyMinutes, + min: 1, + onchange: (e) => + (this.frequencyMinutes = e.target.value), + }), + ]) + : m("div", { class: "flex flex-col space-y-2" }, [ + m( + "label", + { + class: + "text-sm font-medium text-gray-700 dark:text-gray-300", + }, + "Run Date" + ), + m("input[type=datetime-local]", { + class: + "bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg px-3 py-2", + value: this.runDate, + onchange: (e) => (this.runDate = e.target.value), + }), + ]), + ]), + ], + + // Actions group + m( + "div", + { class: "flex items-center justify-end space-x-3 pt-2" }, + [ + // Save button + this.showSaveButton && + m( + "button", + { + class: + "px-4 py-2 bg-blue-500 text-white text-sm font-medium rounded-lg hover:bg-blue-600 transition-colors flex items-center space-x-2 disabled:opacity-50", + onclick: () => this.save(), + disabled: this.saveLoading, + }, + [ + this.saveLoading && + m("div", { + class: + "animate-spin h-4 w-4 border-2 border-white border-t-transparent rounded-full", + }), + m( + "span", + this.saveLoading ? "Saving..." : "Save Function" + ), + ] + ), + + // Delete button + this.showDeleteButton && + m( + "button", + { + class: + "px-4 py-2 bg-white text-red-600 text-sm font-medium border border-red-200 rounded-lg hover:bg-red-50 transition-colors flex items-center space-x-2 disabled:opacity-50", + onclick: () => this.delete(), + disabled: this.deleteLoading, + }, + [ + this.deleteLoading && + m("div", { + class: + "animate-spin h-4 w-4 border-2 border-red-600 border-t-transparent rounded-full", + }), + m( + "span", + this.deleteLoading ? "Deleting..." : "Delete Function" + ), + ] + ), + ] + ), + ]), + ]), /* ───────────────────────────────────────────────────────────────── ResponseView (child) if needed ─────────────────────────────────────────────────────────────────*/ !this.executeLoading && - !this.error && - this.response && - m(ResponseView, { - response: this.response, - responseTime: this.responseTime, - responseSize: this.responseSize, - envEditorValue: this.jsonValue, - isTimer: this.isTimer, - onClose: () => { - this.response = null; - }, - }), + !this.error && + this.response && + m(ResponseView, { + response: this.response, + responseTime: this.responseTime, + responseSize: this.responseSize, + envEditorValue: this.jsonValue, + isTimer: this.isTimer, + onClose: () => { + this.response = null; + }, + }), ]); }, }; diff --git a/templates/documentation.html b/templates/documentation.html new file mode 100644 index 0000000..373cc76 --- /dev/null +++ b/templates/documentation.html @@ -0,0 +1,149 @@ + + + + + + + + Function - Documentation + + + + + + + + + +
+ + + +
+

Documentation

+

+ Everything you need to know to build powerful functions. +

+
+ +
+
+ + +
+
+

The `request` Object

+

Your function receives a `request` object containing all the + details of the incoming HTTP request. It has the following structure:

+
{
+    "method": "GET",
+    "headers": { ... },
+    "url": "http://...",
+    "path": "/sub/path",
+    "query": { "param": "value" },
+    "json": { "key": "value" },
+    "form": { "field": "value" },
+    "text": "plain text body"
+}
+
+
+ + +
+
+

The `environment` Object

+

The `environment` object is a mutable JSON object that persists + across function executions. You can read from it and write to it to maintain state.

+
// Example: A simple counter
+async (req) => {
+    if (!environment.counter) {
+        environment.counter = 0;
+    }
+    environment.counter++;
+    return JsonResponse({ count: environment.counter });
+}
+
+
+ + +
+
+

Response Helpers

+

Several helper functions are available globally to make creating + responses easier:

+
    +
  • `Response(body, headers, status)`: The base response function.
  • +
  • `JsonResponse(body, headers, status)`: Creates a response with `Content-Type: + application/json`.
  • +
  • `HtmlResponse(body, headers, status)`: Creates a response with `Content-Type: + text/html`.
  • +
  • `TextResponse(body, headers, status)`: Creates a response with `Content-Type: + text/plain`.
  • +
+
+
+ + +
+
+

Console Logging

+

You can use `console.log()` and `console.error()` within your + function. The output will be captured and displayed in the function's logs, which you can + view in your dashboard.

+
+
+ +
+
+
+ + + \ No newline at end of file diff --git a/templates/home.html b/templates/home.html index 9a33142..c2b8785 100644 --- a/templates/home.html +++ b/templates/home.html @@ -81,7 +81,7 @@
  • Documentation + href="{{ url_for('documentation') }}">Documentation
  • Sign Up - View documentation