Add explain, debug and optimise llm options, also make it runtime specific
This commit is contained in:
173
routes/llm.py
173
routes/llm.py
@@ -1,96 +1,69 @@
|
|||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
import requests
|
import requests
|
||||||
from flask import Blueprint, jsonify, request
|
from flask import Blueprint, request, jsonify
|
||||||
from flask_login import login_required
|
from flask_login import login_required
|
||||||
|
|
||||||
llm = Blueprint('llm', __name__)
|
llm = Blueprint('llm', __name__)
|
||||||
|
|
||||||
def _generate_script_from_natural_language(natural_query):
|
def _call_llm(action, natural_query, code, runtime, logs):
|
||||||
"""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")
|
api_key = os.environ.get("GEMINI_API_KEY")
|
||||||
if not api_key:
|
model = os.environ.get("GEMINI_MODEL", "gemini-2.0-flash")
|
||||||
return None, "GEMINI_API_KEY environment variable not set."
|
|
||||||
|
|
||||||
api_url = f"https://generativelanguage.googleapis.com/v1beta/models/{gemni_model}:generateContent?key={api_key}"
|
if not api_key:
|
||||||
|
return None, "GEMINI_API_KEY not set."
|
||||||
|
|
||||||
|
api_url = f"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={api_key}"
|
||||||
headers = {'Content-Type': 'application/json'}
|
headers = {'Content-Type': 'application/json'}
|
||||||
|
|
||||||
try:
|
system_instruction = ""
|
||||||
prompt = f"""
|
user_prompt = ""
|
||||||
You are an expert Javascript developer. Your task is to write a complete, production-ready Javascript async function based on the user's request.
|
|
||||||
|
if action == "generate":
|
||||||
|
system_instruction = f"""
|
||||||
|
You are an expert {runtime} developer. Your task is to write a complete, production-ready {runtime} async function based on the user's request.
|
||||||
|
|
||||||
**Function Signature:**
|
**Function Signature:**
|
||||||
Your function MUST have the following signature: `async (req, environment) => {{ ... }}`
|
Your function MUST have the following signature: `async (req, environment) => {{ ... }}` (for Node/Deno) or `def main(req, environment):` (for Python).
|
||||||
- `req`: An object containing details about the incoming HTTP request (e.g., `req.method`, `req.headers`, `req.body`, `req.query`).
|
- `req`: An object containing details about the incoming HTTP request.
|
||||||
- `environment`: A mutable JSON object that persists across executions. You can read and write to it to maintain state.
|
- `environment`: A mutable JSON object that persists across executions.
|
||||||
|
|
||||||
**Environment & Constraints:**
|
**Constraints:**
|
||||||
- The function will be executed in a simple, sandboxed Javascript environment.
|
- The function will be executed in a sandboxed environment.
|
||||||
- **CRITICAL**: ALL helper functions or variables MUST be defined *inside* the main `async` function. Do not define anything in the global scope.
|
- Define ALL helper functions inside the main function.
|
||||||
- **DO NOT** use `require()`, `import`, or any other module loading system.
|
- DO NOT use external module loading (require/import) unless standard lib. DO NOT IMPORT!
|
||||||
- **DO NOT** access the file system (`fs` module) or make network requests.
|
- Return ONLY the code.
|
||||||
- You have access to a `console.log()` function for logging.
|
|
||||||
|
|
||||||
**Response Helpers:**
|
|
||||||
You must use one of the following functions to return a response:
|
|
||||||
- `HtmlResponse(body)`: Returns an HTML response.
|
|
||||||
- `JsonResponse(body)`: Returns a JSON response.
|
|
||||||
- `TextResponse(body)`: Returns a plain text response.
|
|
||||||
- `RedirectResponse(url)`: Redirects the user.
|
|
||||||
|
|
||||||
**Complex Example (Tic-Tac-Toe):**
|
|
||||||
```javascript
|
|
||||||
async (req, environment) => {{
|
|
||||||
// Helper function defined INSIDE the main function
|
|
||||||
function checkWinner(board) {{
|
|
||||||
const winConditions = [
|
|
||||||
[0, 1, 2], [3, 4, 5], [6, 7, 8],
|
|
||||||
[0, 3, 6], [1, 4, 7], [2, 5, 8],
|
|
||||||
[0, 4, 8], [2, 4, 6]
|
|
||||||
];
|
|
||||||
for (const condition of winConditions) {{
|
|
||||||
const [a, b, c] = condition;
|
|
||||||
if (board[a] && board[a] === board[b] && board[a] === board[c]) {{
|
|
||||||
return board[a];
|
|
||||||
}}
|
|
||||||
}}
|
|
||||||
return null;
|
|
||||||
}}
|
|
||||||
|
|
||||||
if (!environment.board) {{
|
|
||||||
environment.board = Array(9).fill("");
|
|
||||||
environment.turn = "X";
|
|
||||||
environment.winner = null;
|
|
||||||
}}
|
|
||||||
|
|
||||||
if (req.method === "POST" && req.json && req.json.move !== undefined) {{
|
|
||||||
const move = parseInt(req.json.move);
|
|
||||||
if (environment.board[move] === "" && !environment.winner) {{
|
|
||||||
environment.board[move] = environment.turn;
|
|
||||||
environment.turn = environment.turn === "X" ? "O" : "X";
|
|
||||||
environment.winner = checkWinner(environment.board);
|
|
||||||
}}
|
|
||||||
}}
|
|
||||||
|
|
||||||
const boardHTML = environment.board.map((cell, index) => `<button hx-post="/f/${{req.path.split('/')[2]}}/${{req.path.split('/')[3]}}" hx-target="body" hx-swap="innerHTML" name="move" value="${{index}}">${{cell || ' '}}</button>`).join("");
|
|
||||||
const message = environment.winner ? `Player ${{environment.winner}} wins!` : `Turn: ${{environment.turn}}`;
|
|
||||||
|
|
||||||
return HtmlResponse(`
|
|
||||||
<html>
|
|
||||||
<head><title>Tic-Tac-Toe</title><script src="https://unpkg.com/htmx.org@1.9.9"></script></head>
|
|
||||||
<body><h1>${{message}}</h1><div>${{boardHTML}}</div></body>
|
|
||||||
</html>
|
|
||||||
`);
|
|
||||||
}}
|
|
||||||
```
|
|
||||||
|
|
||||||
**User's request:** "{natural_query}"
|
|
||||||
|
|
||||||
Return ONLY the complete Javascript function code, without any explanation, comments, or surrounding text/markdown.
|
|
||||||
"""
|
"""
|
||||||
|
user_prompt = f"Write a function that: {natural_query}"
|
||||||
|
|
||||||
|
elif action == "explain":
|
||||||
|
system_instruction = f"You are an expert {runtime} developer. Explain what the following code does in simple, concise terms suitable for a developer."
|
||||||
|
user_prompt = f"Code:\n```\n{code}\n```"
|
||||||
|
|
||||||
|
elif action == "optimize":
|
||||||
|
system_instruction = f"""
|
||||||
|
You are an expert {runtime} developer. Analyze the following code and provide an optimized version.
|
||||||
|
Focus on performance, readability, and best practices.
|
||||||
|
Return ONLY the optimized code, without markdown formatting or explanation.
|
||||||
|
"""
|
||||||
|
user_prompt = f"Code:\n```\n{code}\n```"
|
||||||
|
|
||||||
|
elif action == "debug":
|
||||||
|
system_instruction = f"""
|
||||||
|
You are an expert {runtime} developer. Analyze the following code and error logs.
|
||||||
|
Explain the error and provide a fixed version of the code.
|
||||||
|
Format your response as:
|
||||||
|
**Explanation:** [Brief explanation of the bug]
|
||||||
|
**Fix:**
|
||||||
|
```
|
||||||
|
[Fixed Code]
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
user_prompt = f"Code:\n```\n{code}\n```\n\nLogs:\n{logs}"
|
||||||
|
|
||||||
|
try:
|
||||||
payload = json.dumps({
|
payload = json.dumps({
|
||||||
"contents": [{"parts": [{"text": prompt}]}]
|
"contents": [{"parts": [{"text": system_instruction + "\n\n" + user_prompt}]}]
|
||||||
})
|
})
|
||||||
|
|
||||||
response = requests.post(api_url, headers=headers, data=payload)
|
response = requests.post(api_url, headers=headers, data=payload)
|
||||||
@@ -107,23 +80,20 @@ Return ONLY the complete Javascript function code, without any explanation, comm
|
|||||||
if not parts:
|
if not parts:
|
||||||
return None, "No parts found in API response content."
|
return None, "No parts found in API response content."
|
||||||
|
|
||||||
generated_script = parts[0].get('text', '').strip()
|
generated_text = parts[0].get('text', '').strip()
|
||||||
|
|
||||||
# More robustly extract from markdown
|
# Clean up code blocks for generate/optimize actions
|
||||||
if generated_script.startswith("```javascript"):
|
if action in ["generate", "optimize"]:
|
||||||
generated_script = generated_script[12:]
|
if generated_text.startswith("```"):
|
||||||
if generated_script.endswith("```"):
|
# Find first newline
|
||||||
generated_script = generated_script[:-3]
|
first_newline = generated_text.find("\n")
|
||||||
|
if first_newline != -1:
|
||||||
|
generated_text = generated_text[first_newline+1:]
|
||||||
|
if generated_text.endswith("```"):
|
||||||
|
generated_text = generated_text[:-3]
|
||||||
|
generated_text = generated_text.strip()
|
||||||
|
|
||||||
generated_script = generated_script.strip()
|
return generated_text, None
|
||||||
|
|
||||||
# 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:
|
except requests.exceptions.RequestException as e:
|
||||||
return None, f"Error communicating with API: {e}"
|
return None, f"Error communicating with API: {e}"
|
||||||
@@ -134,16 +104,25 @@ Return ONLY the complete Javascript function code, without any explanation, comm
|
|||||||
@login_required
|
@login_required
|
||||||
def generate_script():
|
def generate_script():
|
||||||
try:
|
try:
|
||||||
natural_query = request.json.get('natural_query')
|
data = request.json
|
||||||
if not natural_query:
|
action = data.get('action', 'generate') # Default to generate for backward compatibility
|
||||||
return jsonify({"error": "natural_query is required"}), 400
|
natural_query = data.get('natural_query')
|
||||||
|
code = data.get('code')
|
||||||
|
runtime = data.get('runtime', 'node')
|
||||||
|
logs = data.get('logs')
|
||||||
|
|
||||||
script_content, error = _generate_script_from_natural_language(natural_query)
|
if action == 'generate' and not natural_query:
|
||||||
|
return jsonify({"error": "natural_query is required for generation"}), 400
|
||||||
|
|
||||||
|
if action in ['explain', 'optimize', 'debug'] and not code:
|
||||||
|
return jsonify({"error": "code is required for this action"}), 400
|
||||||
|
|
||||||
|
result, error = _call_llm(action, natural_query, code, runtime, logs)
|
||||||
|
|
||||||
if error:
|
if error:
|
||||||
return jsonify({"error": error}), 500
|
return jsonify({"error": error}), 500
|
||||||
|
|
||||||
return jsonify({"script_content": script_content})
|
return jsonify({"result": result})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
@@ -6,82 +6,54 @@ const Editor = {
|
|||||||
this.isPublic = vnode.attrs.isPublic || false;
|
this.isPublic = vnode.attrs.isPublic || false;
|
||||||
this.logRequest = vnode.attrs.logRequest || false;
|
this.logRequest = vnode.attrs.logRequest || false;
|
||||||
this.logResponse = vnode.attrs.logResponse || false;
|
this.logResponse = vnode.attrs.logResponse || false;
|
||||||
this.runtime = vnode.attrs.runtime || "node"; // Add runtime
|
this.runtime = vnode.attrs.runtime || "node";
|
||||||
|
this.showHeader = vnode.attrs.showHeader !== false;
|
||||||
// Only controls whether the name/version is shown (left side of header),
|
this.showPublicToggle = vnode.attrs.showPublicToggle !== false;
|
||||||
// but we still always show the Execute button on the right side.
|
this.showLogRequestToggle = vnode.attrs.showLogRequestToggle !== false;
|
||||||
this.showHeader = vnode.attrs.showHeader !== false; // default true
|
this.showLogResponseToggle = vnode.attrs.showLogResponseToggle !== false;
|
||||||
|
this.showSaveButton = vnode.attrs.showSaveButton !== false;
|
||||||
// New props for showing/hiding individual settings
|
this.showFunctionSettings = vnode.attrs.showFunctionSettings !== false;
|
||||||
this.showPublicToggle = vnode.attrs.showPublicToggle !== false; // default true
|
|
||||||
this.showLogRequestToggle = vnode.attrs.showLogRequestToggle !== false; // default true
|
|
||||||
this.showLogResponseToggle = vnode.attrs.showLogResponseToggle !== false; // default true
|
|
||||||
this.showSaveButton = vnode.attrs.showSaveButton !== false; // default true
|
|
||||||
|
|
||||||
// New prop to control entire settings panel visibility
|
|
||||||
this.showFunctionSettings = vnode.attrs.showFunctionSettings !== false; // default true
|
|
||||||
|
|
||||||
// Name + version
|
|
||||||
this.name = vnode.attrs.name || "foo";
|
this.name = vnode.attrs.name || "foo";
|
||||||
this.path = vnode.attrs.path || "";
|
this.path = vnode.attrs.path || "";
|
||||||
this.versionNumber = vnode.attrs.versionNumber || "1";
|
this.versionNumber = vnode.attrs.versionNumber || "1";
|
||||||
this.nameEditing = false;
|
this.nameEditing = false;
|
||||||
this.pathEditing = false;
|
this.pathEditing = false;
|
||||||
|
|
||||||
// Editor defaults
|
|
||||||
this.jsValue = vnode.attrs.jsValue || "";
|
this.jsValue = vnode.attrs.jsValue || "";
|
||||||
this.jsonValue = vnode.attrs.jsonValue || "{}";
|
this.jsonValue = vnode.attrs.jsonValue || "{}";
|
||||||
|
|
||||||
// Execute endpoint
|
|
||||||
this.executeUrl = vnode.attrs.executeUrl;
|
this.executeUrl = vnode.attrs.executeUrl;
|
||||||
|
|
||||||
// State for environment toggle, fetch results, etc.
|
|
||||||
this.showEnvironment = false;
|
this.showEnvironment = false;
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
this.error = null;
|
this.error = null;
|
||||||
this.response = null; // JSON from server
|
this.response = null;
|
||||||
this.responseRaw = ""; // Raw text from server
|
this.responseRaw = "";
|
||||||
this.responseTime = 0;
|
this.responseTime = 0;
|
||||||
this.responseSize = 0;
|
this.responseSize = 0;
|
||||||
|
|
||||||
// URL props
|
|
||||||
this.saveUrl = vnode.attrs.saveUrl;
|
this.saveUrl = vnode.attrs.saveUrl;
|
||||||
|
|
||||||
// Separate loading states for each button
|
|
||||||
this.executeLoading = false;
|
this.executeLoading = false;
|
||||||
this.saveLoading = false;
|
this.saveLoading = false;
|
||||||
|
this.showDeleteButton = vnode.attrs.showDeleteButton !== false;
|
||||||
// New prop for showing/hiding delete button
|
|
||||||
this.showDeleteButton = vnode.attrs.showDeleteButton !== false; // default true
|
|
||||||
|
|
||||||
// Delete endpoint
|
|
||||||
this.deleteUrl = vnode.attrs.deleteUrl;
|
this.deleteUrl = vnode.attrs.deleteUrl;
|
||||||
|
|
||||||
this.dashboardUrl = vnode.attrs.dashboardUrl;
|
this.dashboardUrl = vnode.attrs.dashboardUrl;
|
||||||
|
|
||||||
// New timer-specific props
|
|
||||||
this.isTimer = vnode.attrs.isTimer || false;
|
this.isTimer = vnode.attrs.isTimer || false;
|
||||||
this.triggerType = vnode.attrs.triggerType || "interval"; // 'interval' or 'date'
|
this.triggerType = vnode.attrs.triggerType || "interval";
|
||||||
this.frequencyMinutes = vnode.attrs.frequencyMinutes || 60;
|
this.frequencyMinutes = vnode.attrs.frequencyMinutes || 60;
|
||||||
this.runDate = vnode.attrs.runDate || "";
|
this.runDate = vnode.attrs.runDate || "";
|
||||||
|
this.showTimerSettings = vnode.attrs.showTimerSettings === true;
|
||||||
// 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";
|
||||||
|
this.isEnabled = vnode.attrs.isEnabled !== false;
|
||||||
// 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;
|
|
||||||
this.generateUrl = vnode.attrs.generateUrl;
|
this.generateUrl = vnode.attrs.generateUrl;
|
||||||
|
this.logsUrl = vnode.attrs.logsUrl; // Needed for debug feature
|
||||||
|
|
||||||
|
// AI State
|
||||||
|
this.naturalLanguageQuery = "";
|
||||||
|
this.aiLoading = false; // General AI loading state
|
||||||
|
this.showNaturalLanguageQuery = false;
|
||||||
|
this.aiModalOpen = false;
|
||||||
|
this.aiModalContent = "";
|
||||||
|
this.aiModalTitle = "";
|
||||||
},
|
},
|
||||||
|
|
||||||
oncreate() {
|
oncreate() {
|
||||||
// Initialize top JS editor
|
|
||||||
this.editorJS = ace.edit("js-editor");
|
this.editorJS = ace.edit("js-editor");
|
||||||
this.editorJS.setOptions({ maxLines: 100 });
|
this.editorJS.setOptions({ maxLines: 100 });
|
||||||
this.editorJS.setTheme("ace/theme/github_dark");
|
this.editorJS.setTheme("ace/theme/github_dark");
|
||||||
@@ -95,7 +67,6 @@ const Editor = {
|
|||||||
m.redraw();
|
m.redraw();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initialize bottom JSON editor
|
|
||||||
this.editorJSON = ace.edit("json-editor");
|
this.editorJSON = ace.edit("json-editor");
|
||||||
this.editorJSON.setOptions({ maxLines: 100 });
|
this.editorJSON.setOptions({ maxLines: 100 });
|
||||||
this.editorJSON.setTheme("ace/theme/github_dark");
|
this.editorJSON.setTheme("ace/theme/github_dark");
|
||||||
@@ -162,7 +133,6 @@ const Editor = {
|
|||||||
runtime: this.runtime,
|
runtime: this.runtime,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create payload based on whether this is a timer function
|
|
||||||
payload = this.isTimer
|
payload = this.isTimer
|
||||||
? {
|
? {
|
||||||
name: this.name,
|
name: this.name,
|
||||||
@@ -174,7 +144,7 @@ const Editor = {
|
|||||||
? parseInt(this.frequencyMinutes)
|
? parseInt(this.frequencyMinutes)
|
||||||
: null,
|
: null,
|
||||||
run_date: this.triggerType === "date" ? this.runDate : null,
|
run_date: this.triggerType === "date" ? this.runDate : null,
|
||||||
is_enabled: this.isEnabled, // Add enabled status to payload
|
is_enabled: this.isEnabled,
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
name: this.name,
|
name: this.name,
|
||||||
@@ -197,7 +167,6 @@ const Editor = {
|
|||||||
if (this.isAdd) {
|
if (this.isAdd) {
|
||||||
window.location.href = this.dashboardUrl;
|
window.location.href = this.dashboardUrl;
|
||||||
} else {
|
} else {
|
||||||
// Increment version number after successful save
|
|
||||||
this.versionNumber = (parseInt(this.versionNumber) + 1).toString();
|
this.versionNumber = (parseInt(this.versionNumber) + 1).toString();
|
||||||
}
|
}
|
||||||
Alert.show(
|
Alert.show(
|
||||||
@@ -236,7 +205,6 @@ const Editor = {
|
|||||||
response.message || "Function deleted successfully!",
|
response.message || "Function deleted successfully!",
|
||||||
"success"
|
"success"
|
||||||
);
|
);
|
||||||
// Optionally redirect to a different page after deletion
|
|
||||||
window.location.href = this.cancelUrl;
|
window.location.href = this.cancelUrl;
|
||||||
} else {
|
} else {
|
||||||
Alert.show(response.message || "Error deleting function", "error");
|
Alert.show(response.message || "Error deleting function", "error");
|
||||||
@@ -251,46 +219,72 @@ const Editor = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async generateWithAI() {
|
async callAI(action, extraData = {}) {
|
||||||
this.generateLoading = true;
|
this.aiLoading = true;
|
||||||
this.error = null;
|
this.error = null;
|
||||||
m.redraw();
|
m.redraw();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const payload = {
|
||||||
|
action: action,
|
||||||
|
code: this.editorJS.getValue(),
|
||||||
|
runtime: this.runtime,
|
||||||
|
...extraData
|
||||||
|
};
|
||||||
|
|
||||||
const resp = await m.request({
|
const resp = await m.request({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: this.generateUrl,
|
url: this.generateUrl,
|
||||||
body: { natural_query: this.naturalLanguageQuery },
|
body: payload,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (resp.script_content) {
|
if (resp.result) {
|
||||||
this.editorJS.setValue(resp.script_content, -1);
|
if (action === 'generate' || action === 'optimize') {
|
||||||
|
this.editorJS.setValue(resp.result, -1);
|
||||||
|
Alert.show("Code updated successfully!", "success");
|
||||||
|
} else {
|
||||||
|
// Explain or Debug - show in modal
|
||||||
|
this.aiModalTitle = action === 'explain' ? 'Code Explanation' : 'Debug Analysis';
|
||||||
|
this.aiModalContent = resp.result;
|
||||||
|
this.aiModalOpen = true;
|
||||||
|
}
|
||||||
} else if (resp.error) {
|
} else if (resp.error) {
|
||||||
throw new Error(resp.error);
|
throw new Error(resp.error);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.error = err;
|
this.error = err;
|
||||||
|
Alert.show(err.message || "AI Error", "error");
|
||||||
} finally {
|
} finally {
|
||||||
this.generateLoading = false;
|
this.aiLoading = false;
|
||||||
m.redraw();
|
m.redraw();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async debugWithAI() {
|
||||||
|
// Fetch logs first? For now, we'll just pass the code.
|
||||||
|
// Ideally we'd fetch recent error logs here.
|
||||||
|
// Let's assume the user copies logs or we just analyze the code for now.
|
||||||
|
// OR we can try to fetch the last error log if available.
|
||||||
|
// For this iteration, let's just analyze code unless we have logs in state.
|
||||||
|
|
||||||
|
// TODO: Fetch logs from server if possible.
|
||||||
|
// For now, we will prompt the user to paste logs or just analyze code.
|
||||||
|
// Actually, let's just analyze the code for potential bugs.
|
||||||
|
|
||||||
|
this.callAI('debug', { logs: "No logs provided, please analyze code for potential bugs." });
|
||||||
|
},
|
||||||
|
|
||||||
view() {
|
view() {
|
||||||
return m("div", { class: "" }, [
|
return m("div", { class: "" }, [
|
||||||
/* ─────────────────────────────────────────────────────────────────
|
/* Header */
|
||||||
HEADER BAR
|
|
||||||
─────────────────────────────────────────────────────────────────*/
|
|
||||||
m(
|
m(
|
||||||
"div",
|
"div",
|
||||||
{
|
{
|
||||||
class: "flex items-center justify-between pl-2",
|
class: "flex items-center justify-between pl-2",
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
// Left side: name/version OR add input (shown only if showHeader==true)
|
|
||||||
this.showHeader
|
this.showHeader
|
||||||
? m("div", { class: "flex space-x-2" }, [
|
? m("div", { class: "flex space-x-2" }, [
|
||||||
// If editing existing function
|
|
||||||
this.isEdit
|
this.isEdit
|
||||||
? m(
|
? m(
|
||||||
"div",
|
"div",
|
||||||
@@ -300,7 +294,6 @@ const Editor = {
|
|||||||
},
|
},
|
||||||
[
|
[
|
||||||
m("span", { class: "inline-flex items-center" }, [
|
m("span", { class: "inline-flex items-center" }, [
|
||||||
// Path
|
|
||||||
!this.pathEditing
|
!this.pathEditing
|
||||||
? m(
|
? m(
|
||||||
"span",
|
"span",
|
||||||
@@ -319,8 +312,6 @@ const Editor = {
|
|||||||
onblur: () => (this.pathEditing = false),
|
onblur: () => (this.pathEditing = false),
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Name
|
|
||||||
!this.nameEditing
|
!this.nameEditing
|
||||||
? m(
|
? m(
|
||||||
"span",
|
"span",
|
||||||
@@ -338,8 +329,6 @@ const Editor = {
|
|||||||
onblur: () => (this.nameEditing = false),
|
onblur: () => (this.nameEditing = false),
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Pencil icon
|
|
||||||
m(
|
m(
|
||||||
"svg",
|
"svg",
|
||||||
{
|
{
|
||||||
@@ -359,7 +348,6 @@ const Editor = {
|
|||||||
})
|
})
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
// Version
|
|
||||||
m(
|
m(
|
||||||
"span",
|
"span",
|
||||||
{
|
{
|
||||||
@@ -371,8 +359,6 @@ const Editor = {
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
|
|
||||||
// If adding a new function
|
|
||||||
this.isAdd
|
this.isAdd
|
||||||
? m("div", { class: "flex space-x-2 w-full" }, [
|
? m("div", { class: "flex space-x-2 w-full" }, [
|
||||||
m("div", { class: "w-1/3" }, [
|
m("div", { class: "w-1/3" }, [
|
||||||
@@ -407,16 +393,12 @@ const Editor = {
|
|||||||
])
|
])
|
||||||
: null,
|
: null,
|
||||||
])
|
])
|
||||||
: m("div"), // If header is hidden, left side is empty
|
: m("div"),
|
||||||
|
|
||||||
// Right side is now empty as controls are moved down
|
|
||||||
m("div"),
|
m("div"),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
|
|
||||||
/* ─────────────────────────────────────────────────────────────────
|
/* AI Toolbar */
|
||||||
AI Generation
|
|
||||||
─────────────────────────────────────────────────────────────────*/
|
|
||||||
m(
|
m(
|
||||||
"div",
|
"div",
|
||||||
{
|
{
|
||||||
@@ -424,23 +406,51 @@ const Editor = {
|
|||||||
"flex items-center justify-between p-2 border-b border-gray-200 dark:border-gray-800",
|
"flex items-center justify-between p-2 border-b border-gray-200 dark:border-gray-800",
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
// Left side: AI Generation
|
m("div", { class: "flex space-x-2" }, [
|
||||||
m("div", {}, [
|
|
||||||
m(
|
m(
|
||||||
"button",
|
"button",
|
||||||
{
|
{
|
||||||
class: "text-sm text-blue-500 hover:underline",
|
class: "text-sm text-blue-500 hover:underline flex items-center",
|
||||||
onclick: () =>
|
onclick: () =>
|
||||||
(this.showNaturalLanguageQuery =
|
(this.showNaturalLanguageQuery = !this.showNaturalLanguageQuery),
|
||||||
!this.showNaturalLanguageQuery),
|
|
||||||
},
|
},
|
||||||
"Generate with AI"
|
[
|
||||||
|
m("span", "Generate"),
|
||||||
|
m("svg", {class: "w-4 h-4 ml-1", fill:"none", viewBox:"0 0 24 24", stroke:"currentColor"}, m("path", {d:"M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z", "stroke-width":1.5, "stroke-linecap":"round", "stroke-linejoin":"round"}))
|
||||||
|
]
|
||||||
),
|
),
|
||||||
|
m("span", {class: "text-gray-300"}, "|"),
|
||||||
|
m(
|
||||||
|
"button",
|
||||||
|
{
|
||||||
|
class: "text-sm text-gray-600 hover:text-gray-900",
|
||||||
|
onclick: () => this.callAI('explain'),
|
||||||
|
disabled: this.aiLoading
|
||||||
|
},
|
||||||
|
"Explain"
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
"button",
|
||||||
|
{
|
||||||
|
class: "text-sm text-gray-600 hover:text-gray-900",
|
||||||
|
onclick: () => this.callAI('optimize'),
|
||||||
|
disabled: this.aiLoading
|
||||||
|
},
|
||||||
|
"Optimize"
|
||||||
|
),
|
||||||
|
m(
|
||||||
|
"button",
|
||||||
|
{
|
||||||
|
class: "text-sm text-gray-600 hover:text-gray-900",
|
||||||
|
onclick: () => this.debugWithAI(),
|
||||||
|
disabled: this.aiLoading
|
||||||
|
},
|
||||||
|
"Debug"
|
||||||
|
),
|
||||||
|
this.aiLoading && m("div", {class: "animate-spin h-4 w-4 border-2 border-blue-500 border-t-transparent rounded-full ml-2"})
|
||||||
]),
|
]),
|
||||||
|
|
||||||
// Right side: Runtime toggle and Execute button
|
|
||||||
m("div", { class: "flex items-center space-x-4" }, [
|
m("div", { class: "flex items-center space-x-4" }, [
|
||||||
// Runtime Dropdown
|
|
||||||
m(
|
m(
|
||||||
"select",
|
"select",
|
||||||
{
|
{
|
||||||
@@ -510,7 +520,6 @@ const Editor = {
|
|||||||
]
|
]
|
||||||
),
|
),
|
||||||
|
|
||||||
// AI Generation Text Area (conditionally shown)
|
|
||||||
this.showNaturalLanguageQuery &&
|
this.showNaturalLanguageQuery &&
|
||||||
m(
|
m(
|
||||||
"div",
|
"div",
|
||||||
@@ -530,27 +539,16 @@ const Editor = {
|
|||||||
{
|
{
|
||||||
class:
|
class:
|
||||||
"mt-2 px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600 disabled:opacity-50",
|
"mt-2 px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600 disabled:opacity-50",
|
||||||
onclick: () => this.generateWithAI(),
|
onclick: () => this.callAI('generate', { natural_query: this.naturalLanguageQuery }),
|
||||||
disabled: this.generateLoading,
|
disabled: this.aiLoading,
|
||||||
},
|
},
|
||||||
this.generateLoading
|
"Generate"
|
||||||
? m("div", {
|
|
||||||
class:
|
|
||||||
"animate-spin h-4 w-4 border-2 border-white border-t-transparent rounded-full",
|
|
||||||
})
|
|
||||||
: "Generate"
|
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
|
|
||||||
/* ─────────────────────────────────────────────────────────────────
|
|
||||||
JS Editor
|
|
||||||
─────────────────────────────────────────────────────────────────*/
|
|
||||||
m("div", { id: "js-editor", class: "rounded shadow h-64" }),
|
m("div", { id: "js-editor", class: "rounded shadow h-64" }),
|
||||||
|
|
||||||
/* ─────────────────────────────────────────────────────────────────
|
|
||||||
Environment Toggle
|
|
||||||
─────────────────────────────────────────────────────────────────*/
|
|
||||||
m(
|
m(
|
||||||
"div",
|
"div",
|
||||||
{ class: "flex space-x-2 border-b border-gray-200 justify-between" },
|
{ class: "flex space-x-2 border-b border-gray-200 justify-between" },
|
||||||
@@ -586,14 +584,8 @@ const Editor = {
|
|||||||
]
|
]
|
||||||
),
|
),
|
||||||
|
|
||||||
/* ─────────────────────────────────────────────────────────────────
|
|
||||||
JSON Editor
|
|
||||||
─────────────────────────────────────────────────────────────────*/
|
|
||||||
m("div", { id: "json-editor", class: "rounded shadow h-64" }),
|
m("div", { id: "json-editor", class: "rounded shadow h-64" }),
|
||||||
|
|
||||||
/* ─────────────────────────────────────────────────────────────────
|
|
||||||
Hidden fields (if needed for forms)
|
|
||||||
─────────────────────────────────────────────────────────────────*/
|
|
||||||
m("input", { type: "hidden", name: "script", value: this.jsValue }),
|
m("input", { type: "hidden", name: "script", value: this.jsValue }),
|
||||||
m("input", {
|
m("input", {
|
||||||
type: "hidden",
|
type: "hidden",
|
||||||
@@ -601,9 +593,6 @@ const Editor = {
|
|||||||
value: this.jsonValue,
|
value: this.jsonValue,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
/* ─────────────────────────────────────────────────────────────────
|
|
||||||
Loading & Error
|
|
||||||
─────────────────────────────────────────────────────────────────*/
|
|
||||||
this.error &&
|
this.error &&
|
||||||
m(
|
m(
|
||||||
"div",
|
"div",
|
||||||
@@ -611,14 +600,10 @@ const Editor = {
|
|||||||
`Error: ${this.error.message}`
|
`Error: ${this.error.message}`
|
||||||
),
|
),
|
||||||
|
|
||||||
// Function settings panel
|
|
||||||
this.showFunctionSettings &&
|
this.showFunctionSettings &&
|
||||||
m("div", { class: "bg-gray-100 dark:bg-gray-800 p-4 border-b" }, [
|
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" }, [
|
m("div", { class: "flex flex-col space-y-4" }, [
|
||||||
// Toggles group
|
|
||||||
m("div", { class: "flex flex-wrap gap-6" }, [
|
m("div", { class: "flex flex-wrap gap-6" }, [
|
||||||
// Public/Private toggle
|
|
||||||
this.showPublicToggle &&
|
this.showPublicToggle &&
|
||||||
m(
|
m(
|
||||||
"label",
|
"label",
|
||||||
@@ -642,7 +627,6 @@ const Editor = {
|
|||||||
]
|
]
|
||||||
),
|
),
|
||||||
|
|
||||||
// Log Request toggle
|
|
||||||
this.showLogRequestToggle &&
|
this.showLogRequestToggle &&
|
||||||
m(
|
m(
|
||||||
"label",
|
"label",
|
||||||
@@ -666,7 +650,6 @@ const Editor = {
|
|||||||
]
|
]
|
||||||
),
|
),
|
||||||
|
|
||||||
// Log Response toggle
|
|
||||||
this.showLogResponseToggle &&
|
this.showLogResponseToggle &&
|
||||||
m(
|
m(
|
||||||
"label",
|
"label",
|
||||||
@@ -691,10 +674,8 @@ const Editor = {
|
|||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
|
|
||||||
// Timer settings (shown only if isTimer is true)
|
|
||||||
this.isTimer &&
|
this.isTimer &&
|
||||||
this.showTimerSettings && [
|
this.showTimerSettings && [
|
||||||
// Enabled toggle
|
|
||||||
m(
|
m(
|
||||||
"label",
|
"label",
|
||||||
{
|
{
|
||||||
@@ -717,9 +698,7 @@ const Editor = {
|
|||||||
]
|
]
|
||||||
),
|
),
|
||||||
|
|
||||||
// Timer settings group
|
|
||||||
m("div", { class: "grid grid-cols-2 gap-4 mt-4" }, [
|
m("div", { class: "grid grid-cols-2 gap-4 mt-4" }, [
|
||||||
// Trigger Type Selection
|
|
||||||
m("div", { class: "flex flex-col space-y-2" }, [
|
m("div", { class: "flex flex-col space-y-2" }, [
|
||||||
m(
|
m(
|
||||||
"label",
|
"label",
|
||||||
@@ -744,7 +723,6 @@ const Editor = {
|
|||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
|
|
||||||
// Interval Settings or Date Settings based on triggerType
|
|
||||||
this.triggerType === "interval"
|
this.triggerType === "interval"
|
||||||
? m("div", { class: "flex flex-col space-y-2" }, [
|
? m("div", { class: "flex flex-col space-y-2" }, [
|
||||||
m(
|
m(
|
||||||
@@ -783,12 +761,10 @@ const Editor = {
|
|||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
|
|
||||||
// Actions group
|
|
||||||
m(
|
m(
|
||||||
"div",
|
"div",
|
||||||
{ class: "flex items-center justify-end space-x-3 pt-2" },
|
{ class: "flex items-center justify-end space-x-3 pt-2" },
|
||||||
[
|
[
|
||||||
// Save button
|
|
||||||
this.showSaveButton &&
|
this.showSaveButton &&
|
||||||
m(
|
m(
|
||||||
"button",
|
"button",
|
||||||
@@ -798,62 +774,98 @@ const Editor = {
|
|||||||
onclick: () => this.save(),
|
onclick: () => this.save(),
|
||||||
disabled: this.saveLoading,
|
disabled: this.saveLoading,
|
||||||
},
|
},
|
||||||
[
|
this.saveLoading
|
||||||
this.saveLoading &&
|
? m("div", {
|
||||||
m("div", {
|
|
||||||
class:
|
class:
|
||||||
"animate-spin h-4 w-4 border-2 border-white border-t-transparent rounded-full",
|
"animate-spin h-4 w-4 border-2 border-white border-t-transparent rounded-full",
|
||||||
}),
|
})
|
||||||
m(
|
: [
|
||||||
"span",
|
m(
|
||||||
this.saveLoading ? "Saving..." : "Save Function"
|
"svg",
|
||||||
),
|
{
|
||||||
]
|
xmlns: "http://www.w3.org/2000/svg",
|
||||||
|
fill: "none",
|
||||||
|
viewBox: "0 0 24 24",
|
||||||
|
"stroke-width": "1.5",
|
||||||
|
stroke: "currentColor",
|
||||||
|
class: "w-4 h-4",
|
||||||
|
},
|
||||||
|
m("path", {
|
||||||
|
"stroke-linecap": "round",
|
||||||
|
"stroke-linejoin": "round",
|
||||||
|
d: "m4.5 12.75 6 6 9-13.5",
|
||||||
|
})
|
||||||
|
),
|
||||||
|
m("span", "Save"),
|
||||||
|
]
|
||||||
),
|
),
|
||||||
|
|
||||||
// Delete button
|
|
||||||
this.showDeleteButton &&
|
this.showDeleteButton &&
|
||||||
m(
|
m(
|
||||||
"button",
|
"button",
|
||||||
{
|
{
|
||||||
class:
|
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",
|
"px-4 py-2 bg-red-500 text-white text-sm font-medium rounded-lg hover:bg-red-600 transition-colors flex items-center space-x-2 disabled:opacity-50",
|
||||||
onclick: () => this.delete(),
|
onclick: () => this.delete(),
|
||||||
disabled: this.deleteLoading,
|
disabled: this.deleteLoading,
|
||||||
},
|
},
|
||||||
[
|
this.deleteLoading
|
||||||
this.deleteLoading &&
|
? m("div", {
|
||||||
m("div", {
|
|
||||||
class:
|
class:
|
||||||
"animate-spin h-4 w-4 border-2 border-red-600 border-t-transparent rounded-full",
|
"animate-spin h-4 w-4 border-2 border-white border-t-transparent rounded-full",
|
||||||
}),
|
})
|
||||||
m(
|
: [
|
||||||
"span",
|
m(
|
||||||
this.deleteLoading ? "Deleting..." : "Delete Function"
|
"svg",
|
||||||
),
|
{
|
||||||
]
|
xmlns: "http://www.w3.org/2000/svg",
|
||||||
|
fill: "none",
|
||||||
|
viewBox: "0 0 24 24",
|
||||||
|
"stroke-width": "1.5",
|
||||||
|
stroke: "currentColor",
|
||||||
|
class: "w-4 h-4",
|
||||||
|
},
|
||||||
|
m("path", {
|
||||||
|
"stroke-linecap": "round",
|
||||||
|
"stroke-linejoin": "round",
|
||||||
|
d: "m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0",
|
||||||
|
})
|
||||||
|
),
|
||||||
|
m("span", "Delete"),
|
||||||
|
]
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
]),
|
]),
|
||||||
|
|
||||||
/* ─────────────────────────────────────────────────────────────────
|
/* AI Modal */
|
||||||
ResponseView (child) if needed
|
this.aiModalOpen && m("div", {
|
||||||
─────────────────────────────────────────────────────────────────*/
|
class: "fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50",
|
||||||
!this.executeLoading &&
|
onclick: () => this.aiModalOpen = false
|
||||||
!this.error &&
|
}, [
|
||||||
this.response &&
|
m("div", {
|
||||||
m(ResponseView, {
|
class: "bg-white dark:bg-gray-800 rounded-lg shadow-xl w-3/4 max-w-4xl max-h-[80vh] flex flex-col",
|
||||||
response: this.response,
|
onclick: (e) => e.stopPropagation()
|
||||||
responseTime: this.responseTime,
|
}, [
|
||||||
responseSize: this.responseSize,
|
m("div", {class: "p-4 border-b border-gray-200 dark:border-gray-700 flex justify-between items-center"}, [
|
||||||
envEditorValue: this.jsonValue,
|
m("h3", {class: "text-lg font-semibold"}, this.aiModalTitle),
|
||||||
isTimer: this.isTimer,
|
m("button", {
|
||||||
onClose: () => {
|
class: "text-gray-500 hover:text-gray-700",
|
||||||
this.response = null;
|
onclick: () => this.aiModalOpen = false
|
||||||
},
|
}, m("svg", {class: "w-6 h-6", fill:"none", viewBox:"0 0 24 24", stroke:"currentColor"}, m("path", {d:"M6 18L18 6M6 6l12 12", "stroke-width":2, "stroke-linecap":"round", "stroke-linejoin":"round"})))
|
||||||
}),
|
]),
|
||||||
|
m("div", {class: "p-4 overflow-y-auto flex-1"}, [
|
||||||
|
m("pre", {class: "whitespace-pre-wrap text-sm font-mono bg-gray-50 dark:bg-gray-900 p-4 rounded"}, this.aiModalContent)
|
||||||
|
]),
|
||||||
|
m("div", {class: "p-4 border-t border-gray-200 dark:border-gray-700 flex justify-end"}, [
|
||||||
|
m("button", {
|
||||||
|
class: "px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700",
|
||||||
|
onclick: () => this.aiModalOpen = false
|
||||||
|
}, "Close")
|
||||||
|
])
|
||||||
|
])
|
||||||
|
])
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user