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
-