From dfcbd9263eaca787962ad6772de05106216aa472 Mon Sep 17 00:00:00 2001 From: Peter Stockings Date: Wed, 19 Nov 2025 22:28:00 +1100 Subject: [PATCH] Add explain, debug and optimise llm options, also make it runtime specific --- routes/llm.py | 171 ++++++++---------- static/js/mithril/editor.js | 336 +++++++++++++++++++----------------- 2 files changed, 249 insertions(+), 258 deletions(-) diff --git a/routes/llm.py b/routes/llm.py index c49c03b..df642e1 100644 --- a/routes/llm.py +++ b/routes/llm.py @@ -1,96 +1,69 @@ import os import json import requests -from flask import Blueprint, jsonify, request +from flask import Blueprint, request, jsonify from flask_login import login_required llm = Blueprint('llm', __name__) -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") +def _call_llm(action, natural_query, code, runtime, logs): api_key = os.environ.get("GEMINI_API_KEY") + model = os.environ.get("GEMINI_MODEL", "gemini-2.0-flash") + if not api_key: - return None, "GEMINI_API_KEY environment variable not set." + return None, "GEMINI_API_KEY not set." - api_url = f"https://generativelanguage.googleapis.com/v1beta/models/{gemni_model}:generateContent?key={api_key}" + api_url = f"https://generativelanguage.googleapis.com/v1beta/models/{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. + system_instruction = "" + user_prompt = "" + + 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:** -Your function MUST have the following signature: `async (req, environment) => {{ ... }}` -- `req`: An object containing details about the incoming HTTP request (e.g., `req.method`, `req.headers`, `req.body`, `req.query`). -- `environment`: A mutable JSON object that persists across executions. You can read and write to it to maintain state. +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. +- `environment`: A mutable JSON object that persists across executions. -**Environment & Constraints:** -- The function will be executed in a simple, sandboxed Javascript environment. -- **CRITICAL**: ALL helper functions or variables MUST be defined *inside* the main `async` function. Do not define anything in the global scope. -- **DO NOT** use `require()`, `import`, or any other module loading system. -- **DO NOT** access the file system (`fs` module) or make network requests. -- 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) => ``).join(""); - const message = environment.winner ? `Player ${{environment.winner}} wins!` : `Turn: ${{environment.turn}}`; - - return HtmlResponse(` - - Tic-Tac-Toe -

${{message}}

${{boardHTML}}
- - `); -}} -``` - -**User's request:** "{natural_query}" - -Return ONLY the complete Javascript function code, without any explanation, comments, or surrounding text/markdown. +**Constraints:** +- The function will be executed in a sandboxed environment. +- Define ALL helper functions inside the main function. +- DO NOT use external module loading (require/import) unless standard lib. DO NOT IMPORT! +- Return ONLY the code. """ + 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({ - "contents": [{"parts": [{"text": prompt}]}] + "contents": [{"parts": [{"text": system_instruction + "\n\n" + user_prompt}]}] }) 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: 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 - if generated_script.startswith("```javascript"): - generated_script = generated_script[12:] - if generated_script.endswith("```"): - generated_script = generated_script[:-3] - - generated_script = generated_script.strip() + # Clean up code blocks for generate/optimize actions + if action in ["generate", "optimize"]: + if generated_text.startswith("```"): + # Find first newline + 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() - # 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 + return generated_text, None except requests.exceptions.RequestException as 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 def generate_script(): try: - natural_query = request.json.get('natural_query') - if not natural_query: - return jsonify({"error": "natural_query is required"}), 400 + data = request.json + action = data.get('action', 'generate') # Default to generate for backward compatibility + 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: return jsonify({"error": error}), 500 - return jsonify({"script_content": script_content}) + return jsonify({"result": result}) except Exception as e: return jsonify({'error': str(e)}), 500 \ No newline at end of file diff --git a/static/js/mithril/editor.js b/static/js/mithril/editor.js index 2a08207..91654f1 100644 --- a/static/js/mithril/editor.js +++ b/static/js/mithril/editor.js @@ -6,82 +6,54 @@ const Editor = { this.isPublic = vnode.attrs.isPublic || false; this.logRequest = vnode.attrs.logRequest || false; this.logResponse = vnode.attrs.logResponse || false; - this.runtime = vnode.attrs.runtime || "node"; // Add runtime - - // Only controls whether the name/version is shown (left side of header), - // but we still always show the Execute button on the right side. - this.showHeader = vnode.attrs.showHeader !== false; // default true - - // New props for showing/hiding individual settings - 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.runtime = vnode.attrs.runtime || "node"; + this.showHeader = vnode.attrs.showHeader !== false; + this.showPublicToggle = vnode.attrs.showPublicToggle !== false; + this.showLogRequestToggle = vnode.attrs.showLogRequestToggle !== false; + this.showLogResponseToggle = vnode.attrs.showLogResponseToggle !== false; + this.showSaveButton = vnode.attrs.showSaveButton !== false; + this.showFunctionSettings = vnode.attrs.showFunctionSettings !== false; this.name = vnode.attrs.name || "foo"; this.path = vnode.attrs.path || ""; this.versionNumber = vnode.attrs.versionNumber || "1"; this.nameEditing = false; this.pathEditing = false; - - // Editor defaults this.jsValue = vnode.attrs.jsValue || ""; this.jsonValue = vnode.attrs.jsonValue || "{}"; - - // Execute endpoint this.executeUrl = vnode.attrs.executeUrl; - - // State for environment toggle, fetch results, etc. this.showEnvironment = false; this.loading = false; this.error = null; - this.response = null; // JSON from server - this.responseRaw = ""; // Raw text from server + this.response = null; + this.responseRaw = ""; this.responseTime = 0; this.responseSize = 0; - - // URL props this.saveUrl = vnode.attrs.saveUrl; - - // Separate loading states for each button this.executeLoading = false; this.saveLoading = false; - - // New prop for showing/hiding delete button - this.showDeleteButton = vnode.attrs.showDeleteButton !== false; // default true - - // Delete endpoint + this.showDeleteButton = vnode.attrs.showDeleteButton !== false; this.deleteUrl = vnode.attrs.deleteUrl; - this.dashboardUrl = vnode.attrs.dashboardUrl; - - // 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"; this.frequencyMinutes = vnode.attrs.frequencyMinutes || 60; this.runDate = vnode.attrs.runDate || ""; - - // Show timer settings panel - this.showTimerSettings = vnode.attrs.showTimerSettings === true; // default false - + this.showTimerSettings = vnode.attrs.showTimerSettings === true; 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; + this.isEnabled = vnode.attrs.isEnabled !== false; 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() { - // Initialize top JS editor this.editorJS = ace.edit("js-editor"); this.editorJS.setOptions({ maxLines: 100 }); this.editorJS.setTheme("ace/theme/github_dark"); @@ -95,7 +67,6 @@ const Editor = { m.redraw(); }); - // Initialize bottom JSON editor this.editorJSON = ace.edit("json-editor"); this.editorJSON.setOptions({ maxLines: 100 }); this.editorJSON.setTheme("ace/theme/github_dark"); @@ -162,7 +133,6 @@ const Editor = { runtime: this.runtime, }; - // Create payload based on whether this is a timer function payload = this.isTimer ? { name: this.name, @@ -174,7 +144,7 @@ const Editor = { ? parseInt(this.frequencyMinutes) : 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, @@ -197,7 +167,6 @@ const Editor = { if (this.isAdd) { window.location.href = this.dashboardUrl; } else { - // Increment version number after successful save this.versionNumber = (parseInt(this.versionNumber) + 1).toString(); } Alert.show( @@ -236,7 +205,6 @@ const Editor = { 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"); @@ -251,46 +219,72 @@ const Editor = { } }, - async generateWithAI() { - this.generateLoading = true; + async callAI(action, extraData = {}) { + this.aiLoading = true; this.error = null; m.redraw(); try { + const payload = { + action: action, + code: this.editorJS.getValue(), + runtime: this.runtime, + ...extraData + }; + const resp = await m.request({ method: "POST", url: this.generateUrl, - body: { natural_query: this.naturalLanguageQuery }, + body: payload, }); - if (resp.script_content) { - this.editorJS.setValue(resp.script_content, -1); + if (resp.result) { + 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) { throw new Error(resp.error); } } catch (err) { this.error = err; + Alert.show(err.message || "AI Error", "error"); } finally { - this.generateLoading = false; + this.aiLoading = false; 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() { return m("div", { class: "" }, [ - /* ───────────────────────────────────────────────────────────────── - HEADER BAR - ─────────────────────────────────────────────────────────────────*/ + /* Header */ m( "div", { class: "flex items-center justify-between pl-2", }, [ - // Left side: name/version OR add input (shown only if showHeader==true) this.showHeader ? m("div", { class: "flex space-x-2" }, [ - // If editing existing function this.isEdit ? m( "div", @@ -300,7 +294,6 @@ const Editor = { }, [ m("span", { class: "inline-flex items-center" }, [ - // Path !this.pathEditing ? m( "span", @@ -319,8 +312,6 @@ const Editor = { onblur: () => (this.pathEditing = false), autofocus: true, }), - - // Name !this.nameEditing ? m( "span", @@ -338,8 +329,6 @@ const Editor = { onblur: () => (this.nameEditing = false), autofocus: true, }), - - // Pencil icon m( "svg", { @@ -359,7 +348,6 @@ const Editor = { }) ), ]), - // Version m( "span", { @@ -371,8 +359,6 @@ const Editor = { ] ) : null, - - // If adding a new function this.isAdd ? m("div", { class: "flex space-x-2 w-full" }, [ m("div", { class: "w-1/3" }, [ @@ -407,16 +393,12 @@ const Editor = { ]) : null, ]) - : m("div"), // If header is hidden, left side is empty - - // Right side is now empty as controls are moved down + : m("div"), m("div"), ] ), - /* ───────────────────────────────────────────────────────────────── - AI Generation - ─────────────────────────────────────────────────────────────────*/ + /* AI Toolbar */ m( "div", { @@ -424,23 +406,51 @@ const Editor = { "flex items-center justify-between p-2 border-b border-gray-200 dark:border-gray-800", }, [ - // Left side: AI Generation - m("div", {}, [ + m("div", { class: "flex space-x-2" }, [ m( "button", { - class: "text-sm text-blue-500 hover:underline", + class: "text-sm text-blue-500 hover:underline flex items-center", 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" }, [ - // Runtime Dropdown m( "select", { @@ -510,7 +520,6 @@ const Editor = { ] ), - // AI Generation Text Area (conditionally shown) this.showNaturalLanguageQuery && m( "div", @@ -530,27 +539,16 @@ const Editor = { { 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, + onclick: () => this.callAI('generate', { natural_query: this.naturalLanguageQuery }), + disabled: this.aiLoading, }, - this.generateLoading - ? m("div", { - class: - "animate-spin h-4 w-4 border-2 border-white border-t-transparent rounded-full", - }) - : "Generate" + "Generate" ), ] ), - /* ───────────────────────────────────────────────────────────────── - JS Editor - ─────────────────────────────────────────────────────────────────*/ m("div", { id: "js-editor", class: "rounded shadow h-64" }), - /* ───────────────────────────────────────────────────────────────── - Environment Toggle - ─────────────────────────────────────────────────────────────────*/ m( "div", { 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" }), - /* ───────────────────────────────────────────────────────────────── - Hidden fields (if needed for forms) - ─────────────────────────────────────────────────────────────────*/ m("input", { type: "hidden", name: "script", value: this.jsValue }), m("input", { type: "hidden", @@ -601,9 +593,6 @@ const Editor = { value: this.jsonValue, }), - /* ───────────────────────────────────────────────────────────────── - Loading & Error - ─────────────────────────────────────────────────────────────────*/ this.error && m( "div", @@ -611,14 +600,10 @@ const Editor = { `Error: ${this.error.message}` ), - // 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", @@ -642,7 +627,6 @@ const Editor = { ] ), - // Log Request toggle this.showLogRequestToggle && m( "label", @@ -666,7 +650,6 @@ const Editor = { ] ), - // Log Response toggle this.showLogResponseToggle && m( "label", @@ -691,10 +674,8 @@ const Editor = { ), ]), - // Timer settings (shown only if isTimer is true) this.isTimer && this.showTimerSettings && [ - // Enabled toggle m( "label", { @@ -717,9 +698,7 @@ const Editor = { ] ), - // 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", @@ -744,7 +723,6 @@ const Editor = { ), ]), - // Interval Settings or Date Settings based on triggerType this.triggerType === "interval" ? m("div", { class: "flex flex-col space-y-2" }, [ m( @@ -783,12 +761,10 @@ const Editor = { ]), ], - // Actions group m( "div", { class: "flex items-center justify-end space-x-3 pt-2" }, [ - // Save button this.showSaveButton && m( "button", @@ -798,62 +774,98 @@ const Editor = { onclick: () => this.save(), disabled: this.saveLoading, }, - [ - this.saveLoading && - m("div", { + 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" - ), - ] + }) + : [ + m( + "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 && 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", + "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(), disabled: this.deleteLoading, }, - [ - this.deleteLoading && - m("div", { + 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" - ), - ] + "animate-spin h-4 w-4 border-2 border-white border-t-transparent rounded-full", + }) + : [ + m( + "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"), + ] ), ] ), ]), ]), - /* ───────────────────────────────────────────────────────────────── - 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; - }, - }), + /* AI Modal */ + this.aiModalOpen && m("div", { + class: "fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50", + onclick: () => this.aiModalOpen = false + }, [ + m("div", { + class: "bg-white dark:bg-gray-800 rounded-lg shadow-xl w-3/4 max-w-4xl max-h-[80vh] flex flex-col", + onclick: (e) => e.stopPropagation() + }, [ + m("div", {class: "p-4 border-b border-gray-200 dark:border-gray-700 flex justify-between items-center"}, [ + m("h3", {class: "text-lg font-semibold"}, this.aiModalTitle), + m("button", { + class: "text-gray-500 hover:text-gray-700", + 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") + ]) + ]) + ]) ]); }, };