-- config_gui.lua -- LLM API Configuration GUI (llm_root only) -- v0.8.1: Added timeout field for better control local core = core local M = {} local function has_priv(name, priv) local p = core.get_player_privs(name) or {} return p[priv] == true end local function get_llm_api() if not _G.llm_api then error("[config_gui] llm_api not available") end return _G.llm_api end function M.show(name) if not has_priv(name, "llm_root") then core.chat_send_player(name, "Missing privilege: llm_root") return end local llm_api = get_llm_api() local cfg = llm_api.config local W, H = 14.0, 14.5 local PAD = 0.3 local HEADER_H = 0.8 local FIELD_H = 0.8 local BTN_H = 0.9 local fs = { "formspec_version[6]", "size[" .. W .. "," .. H .. "]", "bgcolor[#0f0f0f;both]", "style_type[*;bgcolor=#1a1a1a;textcolor=#e0e0e0;font=mono]", } -- Header table.insert(fs, "box[0,0;" .. W .. "," .. HEADER_H .. ";#202020]") table.insert(fs, "label[" .. PAD .. "," .. (HEADER_H/2 - 0.2) .. ";LLM Configuration (llm_root only)]") table.insert(fs, "label[" .. (W - 4) .. "," .. (HEADER_H/2 - 0.2) .. ";" .. os.date("%H:%M") .. "]") local y = HEADER_H + PAD * 2 -- API Key table.insert(fs, "label[" .. PAD .. "," .. y .. ";API Key:]") y = y + 0.5 table.insert(fs, "field[" .. PAD .. "," .. y .. ";" .. (W - PAD*2) .. "," .. FIELD_H .. ";api_key;;" .. core.formspec_escape(cfg.api_key or "") .. "]") table.insert(fs, "style[api_key;bgcolor=#1e1e1e]") y = y + FIELD_H + PAD -- API URL table.insert(fs, "label[" .. PAD .. "," .. y .. ";API URL:]") y = y + 0.5 table.insert(fs, "field[" .. PAD .. "," .. y .. ";" .. (W - PAD*2) .. "," .. FIELD_H .. ";api_url;;" .. core.formspec_escape(cfg.api_url or "") .. "]") table.insert(fs, "style[api_url;bgcolor=#1e1e1e]") y = y + FIELD_H + PAD -- Model table.insert(fs, "label[" .. PAD .. "," .. y .. ";Model:]") y = y + 0.5 table.insert(fs, "field[" .. PAD .. "," .. y .. ";" .. (W - PAD*2) .. "," .. FIELD_H .. ";model;;" .. core.formspec_escape(cfg.model or "") .. "]") table.insert(fs, "style[model;bgcolor=#1e1e1e]") y = y + FIELD_H + PAD -- Max Tokens & Temperature (side by side) table.insert(fs, "label[" .. PAD .. "," .. y .. ";Max Tokens:]") table.insert(fs, "label[" .. (W/2 + PAD) .. "," .. y .. ";Temperature:]") y = y + 0.5 local half_w = (W - PAD*3) / 2 table.insert(fs, "field[" .. PAD .. "," .. y .. ";" .. half_w .. "," .. FIELD_H .. ";max_tokens;;" .. tostring(cfg.max_tokens or 4000) .. "]") table.insert(fs, "style[max_tokens;bgcolor=#1e1e1e]") table.insert(fs, "field[" .. (W/2 + PAD) .. "," .. y .. ";" .. half_w .. "," .. FIELD_H .. ";temperature;;" .. tostring(cfg.temperature or 0.7) .. "]") table.insert(fs, "style[temperature;bgcolor=#1e1e1e]") y = y + FIELD_H + PAD -- Timeout field (new in v0.8.1) table.insert(fs, "label[" .. PAD .. "," .. y .. ";Timeout (seconds):]") y = y + 0.5 table.insert(fs, "field[" .. PAD .. "," .. y .. ";" .. half_w .. "," .. FIELD_H .. ";timeout;;" .. tostring(cfg.timeout or 120) .. "]") table.insert(fs, "style[timeout;bgcolor=#1e1e1e]") table.insert(fs, "tooltip[timeout;Global fallback timeout (30-600s). Per-mode overrides below override this.]") y = y + FIELD_H + PAD -- Per-mode timeout overrides table.insert(fs, "label[" .. PAD .. "," .. y .. ";Per-mode timeout overrides (0 = use global):]") y = y + 0.5 local third_w = (W - PAD * 2 - 0.2 * 2) / 3 local function tx(i) return PAD + i * (third_w + 0.2) end table.insert(fs, "label[" .. tx(0) .. "," .. y .. ";Chat:]") table.insert(fs, "label[" .. tx(1) .. "," .. y .. ";IDE:]") table.insert(fs, "label[" .. tx(2) .. "," .. y .. ";WorldEdit:]") y = y + 0.45 table.insert(fs, "field[" .. string.format("%.2f", tx(0)) .. "," .. y .. ";" .. string.format("%.2f", third_w) .. "," .. FIELD_H .. ";timeout_chat;;" .. tostring(cfg.timeout_chat or 0) .. "]") table.insert(fs, "style[timeout_chat;bgcolor=#1e1e1e]") table.insert(fs, "tooltip[timeout_chat;Chat mode timeout (0 = global)]") table.insert(fs, "field[" .. string.format("%.2f", tx(1)) .. "," .. y .. ";" .. string.format("%.2f", third_w) .. "," .. FIELD_H .. ";timeout_ide;;" .. tostring(cfg.timeout_ide or 0) .. "]") table.insert(fs, "style[timeout_ide;bgcolor=#1e1e1e]") table.insert(fs, "tooltip[timeout_ide;IDE mode timeout (0 = global)]") table.insert(fs, "field[" .. string.format("%.2f", tx(2)) .. "," .. y .. ";" .. string.format("%.2f", third_w) .. "," .. FIELD_H .. ";timeout_we;;" .. tostring(cfg.timeout_we or 0) .. "]") table.insert(fs, "style[timeout_we;bgcolor=#1e1e1e]") table.insert(fs, "tooltip[timeout_we;WorldEdit mode timeout (0 = global)]") y = y + FIELD_H + PAD * 2 -- WEA toggle + separator table.insert(fs, "box[" .. PAD .. "," .. y .. ";" .. (W - PAD*2) .. ",0.02;#333333]") y = y + 0.18 local wea_val = core.settings:get_bool("llm_worldedit_additions", true) local wea_label = "Enable WorldEditAdditions tools (torus, ellipsoid, erode, convolve...)" local wea_is_installed = type(worldeditadditions) == "table" if not wea_is_installed then wea_label = wea_label .. " [WEA mod not detected]" end table.insert(fs, "checkbox[" .. PAD .. "," .. y .. ";wea_enabled;" .. core.formspec_escape(wea_label) .. ";" .. (wea_val and "true" or "false") .. "]") y = y + 0.55 + PAD -- 4 buttons evenly distributed: Save, Reload, Test, Close local btn_count = 4 local btn_spacing = 0.2 local btn_w = (W - PAD * 2 - btn_spacing * (btn_count - 1)) / btn_count local function bx(i) return PAD + i * (btn_w + btn_spacing) end table.insert(fs, "button[" .. string.format("%.2f", bx(0)) .. "," .. y .. ";" .. string.format("%.2f", btn_w) .. "," .. BTN_H .. ";save;Save Config]") table.insert(fs, "button[" .. string.format("%.2f", bx(1)) .. "," .. y .. ";" .. string.format("%.2f", btn_w) .. "," .. BTN_H .. ";reload;Reload]") table.insert(fs, "button[" .. string.format("%.2f", bx(2)) .. "," .. y .. ";" .. string.format("%.2f", btn_w) .. "," .. BTN_H .. ";test;Test Connection]") table.insert(fs, "style[close;bgcolor=#3a1a1a;textcolor=#ffaaaa]") table.insert(fs, "button[" .. string.format("%.2f", bx(3)) .. "," .. y .. ";" .. string.format("%.2f", btn_w) .. "," .. BTN_H .. ";close;✕ Close]") y = y + BTN_H + PAD -- Info label table.insert(fs, "label[" .. PAD .. "," .. y .. ";Note: Runtime changes. Edit minetest.conf for persistence.]") core.show_formspec(name, "llm_connect:config", table.concat(fs)) end function M.handle_fields(name, formname, fields) if not formname:match("^llm_connect:config") then return false end if not has_priv(name, "llm_root") then return true end local llm_api = get_llm_api() -- WEA checkbox: instant toggle (no Save needed) if fields.wea_enabled ~= nil then local val = fields.wea_enabled == "true" core.settings:set_bool("llm_worldedit_additions", val) core.chat_send_player(name, "[LLM] WorldEditAdditions tools: " .. (val and "enabled" or "disabled")) M.show(name) return true end if fields.save then -- Validation local max_tokens = tonumber(fields.max_tokens) local temperature = tonumber(fields.temperature) local timeout = tonumber(fields.timeout) if not max_tokens or max_tokens < 1 or max_tokens > 100000 then core.chat_send_player(name, "[LLM] Error: max_tokens must be between 1 and 100000") return true end if not temperature or temperature < 0 or temperature > 2 then core.chat_send_player(name, "[LLM] Error: temperature must be between 0 and 2") return true end if not timeout or timeout < 30 or timeout > 600 then core.chat_send_player(name, "[LLM] Error: timeout must be between 30 and 600 seconds") return true end local timeout_chat = tonumber(fields.timeout_chat) or 0 local timeout_ide = tonumber(fields.timeout_ide) or 0 local timeout_we = tonumber(fields.timeout_we) or 0 for _, t in ipairs({timeout_chat, timeout_ide, timeout_we}) do if t ~= 0 and (t < 30 or t > 600) then core.chat_send_player(name, "[LLM] Error: per-mode timeouts must be 0 or between 30-600") return true end end llm_api.set_config({ api_key = fields.api_key or "", api_url = fields.api_url or "", model = fields.model or "", max_tokens = max_tokens, temperature = temperature, timeout = timeout, timeout_chat = timeout_chat, timeout_ide = timeout_ide, timeout_we = timeout_we, }) core.chat_send_player(name, "[LLM] Configuration updated (runtime only)") core.log("action", "[llm_connect] Config updated by " .. name) M.show(name) return true elseif fields.reload then llm_api.reload_config() core.chat_send_player(name, "[LLM] Configuration reloaded from settings") core.log("action", "[llm_connect] Config reloaded by " .. name) M.show(name) return true elseif fields.test then -- Test LLM connection with a simple request core.chat_send_player(name, "[LLM] Testing connection...") local messages = { {role = "user", content = "Reply with just the word 'OK' if you can read this."} } llm_api.request(messages, function(result) if result.success then core.chat_send_player(name, "[LLM] ✓ Connection test successful!") core.chat_send_player(name, "[LLM] Response: " .. (result.content or "No content")) else core.chat_send_player(name, "[LLM] ✗ Connection test failed!") core.chat_send_player(name, "[LLM] Error: " .. (result.error or "Unknown error")) end end, {timeout = 30}) return true elseif fields.close or fields.quit then -- Return to chat_gui if _G.chat_gui then _G.chat_gui.show(name) else core.close_formspec(name, "llm_connect:config") end return true end return true end return M