UNPKG

node-red-contrib-knx-ultimate

Version:

Control your KNX and KNX Secure intallation via Node-Red! A bunch of KNX nodes, with integrated Philips HUE control, ETS group address importer, and KNX routing between interfaces. Easy to use and highly configurable.

613 lines (551 loc) 34.2 kB
<script type="text/javascript" src="resources/node-red-contrib-knx-ultimate/htmlUtils.js"></script> <style type="text/css"> #knx-ai-models-status { margin-top: 6px; margin-bottom: 6px; } #knx-ai-ollama-install-row { align-items: flex-start; } #knx-ai-ollama-install-row > label { padding-top: 6px; } #knx-ai-ollama-install-row .knx-ai-ollama-actions { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; max-width: calc(100% - 300px); } #knx-ai-ollama-install-row .knx-ai-ollama-actions .red-ui-button { margin: 0 !important; } #knx-ai-ollama-steps, #knx-ai-ollama-empty-help { margin-left: 290px; margin-top: 6px; } @media (max-width: 900px) { #knx-ai-ollama-install-row > label { width: 100% !important; padding-top: 0; margin-bottom: 6px; } #knx-ai-ollama-install-row .knx-ai-ollama-actions { max-width: 100%; } #knx-ai-ollama-steps, #knx-ai-ollama-empty-help { margin-left: 0; } } </style> <script type="text/javascript"> RED.nodes.registerType('knxUltimateAI', { category: "KNX Ultimate", color: '#FF9800', defaults: { server: { type: "knxUltimate-config", required: true }, name: { value: "KNX AI", required: false }, topic: { value: "", required: false }, notifyreadrequest: { value: true }, notifyresponse: { value: true }, notifywrite: { value: true }, analysisWindowSec: { value: 120, required: true, validate: RED.validators.number() }, historyWindowSec: { value: 600, required: true, validate: RED.validators.number() }, emitIntervalSec: { value: 0, required: true, validate: RED.validators.number() }, topN: { value: 10, required: true, validate: RED.validators.number() }, maxEvents: { value: 50000, required: true, validate: RED.validators.number() }, rateWindowSec: { value: 10, required: true, validate: RED.validators.number() }, maxTelegramPerSecOverall: { value: 0, required: true, validate: RED.validators.number() }, maxTelegramPerSecPerGA: { value: 0, required: true, validate: RED.validators.number() }, flapWindowSec: { value: 30, required: true, validate: RED.validators.number() }, flapMaxChanges: { value: 0, required: true, validate: RED.validators.number() }, enablePattern: { value: true }, patternMaxLagMs: { value: 1500, required: true, validate: RED.validators.number() }, patternMinCount: { value: 8, required: true, validate: RED.validators.number() }, llmEnabled: { value: false }, llmProvider: { value: "openai_compat" }, llmBaseUrl: { value: "https://api.openai.com/v1/chat/completions" }, llmModel: { value: "gpt-5.4" }, llmSystemPrompt: { value: "" }, llmTemperature: { value: 0.2, required: false, validate: RED.validators.number() }, llmMaxTokens: { value: 50000, required: false, validate: RED.validators.number() }, llmTimeoutMs: { value: 120000, required: false, validate: RED.validators.number() }, llmMaxEventsInPrompt: { value: 120, required: false, validate: RED.validators.number() }, llmIncludeRaw: { value: false }, llmIncludeFlowContext: { value: true }, llmMaxFlowNodesInPrompt: { value: 400, required: false, validate: RED.validators.number() }, llmIncludeDocsSnippets: { value: true }, llmDocsLanguage: { value: "en" }, llmDocsMaxSnippets: { value: 5, required: false, validate: RED.validators.number() }, llmDocsMaxChars: { value: 60000, required: false, validate: RED.validators.number() } }, credentials: { llmApiKey: { type: "password" } }, inputs: 1, outputs: 3, outputLabels: function (i) { switch (i) { case 0: return RED._('knxUltimateAI.outputs.summary'); case 1: return RED._('knxUltimateAI.outputs.anomalies'); case 2: return RED._('knxUltimateAI.outputs.assistant'); } }, icon: "node-eye-icon.svg", label: function () { return (this.name || "KNX AI"); }, paletteLabel: "KNX AI", oneditprepare: function () { try { RED.sidebar.show("help"); } catch (error) { } const OPENAI_COMPAT_DEFAULT_CHAT_URL = "https://api.openai.com/v1/chat/completions"; const OLLAMA_DEFAULT_CHAT_URL = "http://localhost:11434/api/chat"; const OLLAMA_LIBRARY_URL = "https://ollama.com/library"; const OPENAI_COMPAT_DEFAULT_MODELS = ["gpt-5.4", "gpt-4o-mini"]; const OLLAMA_DEFAULT_MODEL = "llama3.1"; const $accordion = $("#knx-ai-accordion"); const $llmContent = $accordion.find("#node-input-llmEnabled").closest("#knx-ai-accordion > div").first(); const $llmHeader = $llmContent.prev("h3"); if ($llmHeader.length && $llmContent.length) { $llmContent.prependTo($accordion); $llmHeader.prependTo($accordion); } $("#knx-ai-accordion").accordion({ header: "h3", heightStyle: "content", collapsible: true, active: 0 }); const toggleLLM = () => { const enabled = $("#node-input-llmEnabled").is(":checked"); if (enabled) { $("#knx-ai-llm-settings").show(); } else { $("#knx-ai-llm-settings").hide(); } }; $("#node-input-llmEnabled").on("change", toggleLLM); toggleLLM(); const nodeId = this.id; const resolveAdminRoot = () => { const raw = (RED.settings && typeof RED.settings.httpAdminRoot === "string") ? RED.settings.httpAdminRoot : "/"; const trimmed = String(raw || "/").trim(); if (trimmed === "" || trimmed === "/") return ""; return "/" + trimmed.replace(/^\/+|\/+$/g, ""); }; const resolveAccessToken = () => { try { const tokens = (RED.settings && typeof RED.settings.get === "function") ? RED.settings.get("auth-tokens") : null; const token = tokens && typeof tokens.access_token === "string" ? tokens.access_token.trim() : ""; return token; } catch (error) { return ""; } }; const t = (key, fallback) => { const value = RED._(key); if (!value || value === key) return fallback || ""; return value; }; $("#knx-ai-open-web-page-vue").on("click", function (evt) { evt.preventDefault(); const adminRoot = resolveAdminRoot(); const targetBase = adminRoot + "/knxUltimateAI/sidebar/page"; const params = new URLSearchParams(); if (nodeId) params.set("nodeId", nodeId); const accessToken = resolveAccessToken(); if (accessToken) params.set("access_token", accessToken); const target = targetBase + (params.toString() ? ("?" + params.toString()) : ""); const wnd = window.open(target, "_blank", "noopener,noreferrer"); try { if (wnd && typeof wnd.focus === "function") wnd.focus(); } catch (e) { } }); const setModelsStatus = (text, level) => { const $status = $("#knx-ai-models-status"); if (!$status || $status.length === 0) return; $status.text(text || ""); $status.removeClass("knx-ai-models-ok knx-ai-models-warn knx-ai-models-err"); if (level === "ok") $status.addClass("knx-ai-models-ok"); if (level === "warn") $status.addClass("knx-ai-models-warn"); if (level === "err") $status.addClass("knx-ai-models-err"); }; const setOllamaNoModelsVisible = (visible) => { if (visible) $("#knx-ai-ollama-empty-help").show(); else $("#knx-ai-ollama-empty-help").hide(); }; const setOllamaInstallUiState = (busy) => { $("#knx-ai-installOllamaModel").prop("disabled", !!busy); $("#knx-ai-downloadOllamaModel").prop("disabled", !!busy); }; const populateModels = (models) => { const $dl = $("#knx-ai-llmModels"); if (!$dl || $dl.length === 0) return; $dl.empty(); (models || []).forEach(function (m) { $("<option>").attr("value", m).appendTo($dl); }); }; const normalizeUrl = (value) => String(value || "").trim().replace(/\/+$/, "").toLowerCase(); const isOpenAiDefaultUrl = (value) => normalizeUrl(value) === normalizeUrl(OPENAI_COMPAT_DEFAULT_CHAT_URL); const isOllamaDefaultUrl = (value) => normalizeUrl(value) === normalizeUrl(OLLAMA_DEFAULT_CHAT_URL); const isOpenAiDefaultModel = (value) => OPENAI_COMPAT_DEFAULT_MODELS.indexOf(String(value || "").trim().toLowerCase()) >= 0; const applyProviderDefaults = (provider) => { const $baseUrl = $("#node-input-llmBaseUrl"); const $model = $("#node-input-llmModel"); const currentBaseUrl = String($baseUrl.val() || "").trim(); const currentModel = String($model.val() || "").trim(); if (provider === "ollama") { if (!currentBaseUrl || isOpenAiDefaultUrl(currentBaseUrl)) $baseUrl.val(OLLAMA_DEFAULT_CHAT_URL); if (!currentModel || isOpenAiDefaultModel(currentModel)) $model.val(OLLAMA_DEFAULT_MODEL); return; } if (!currentBaseUrl || isOllamaDefaultUrl(currentBaseUrl)) $baseUrl.val(OPENAI_COMPAT_DEFAULT_CHAT_URL); if (!currentModel || String(currentModel).trim().toLowerCase() === OLLAMA_DEFAULT_MODEL) $model.val(OPENAI_COMPAT_DEFAULT_MODELS[0]); }; const refreshModels = () => { const provider = $("#node-input-llmProvider").val(); const baseUrl = $("#node-input-llmBaseUrl").val() || ""; const apiKey = $("#node-input-llmApiKey").val() || ""; const payload = { nodeId: nodeId, provider: provider, baseUrl: baseUrl }; if (provider === "ollama") payload.autoStart = true; // Node-RED uses "__PWRD__" as placeholder for stored credentials: don't send it. if (apiKey && apiKey !== "__PWRD__") payload.apiKey = apiKey; setModelsStatus(t('knxUltimateAI.messages.loadingModels', "Loading models..."), "warn"); $("#knx-ai-refreshModels").prop("disabled", true); $.ajax({ url: "knxUltimateAI/models", type: "POST", contentType: "application/json", data: JSON.stringify(payload) }) .done(function (data) { const models = (data && data.models) ? data.models : []; let msg = ""; populateModels(models); if (provider === "ollama" && data && data.ollamaStarted) { try { const startedMsg = t('knxUltimateAI.messages.ollamaStartedAuto', "Ollama server started automatically."); RED.notify(startedMsg, "success"); } catch (e) { } } if (provider === "ollama" && models.length === 0) { msg = t('knxUltimateAI.messages.ollamaNoModels', "No local Ollama model found. Install one from https://ollama.com/library."); setModelsStatus(msg, "warn"); setOllamaNoModelsVisible(true); } else { msg = t('knxUltimateAI.messages.loadedModels', "Models loaded") + ": " + models.length; setModelsStatus(msg, "ok"); setOllamaNoModelsVisible(false); } try { RED.notify(msg, "success"); } catch (e) { } }) .fail(function (xhr) { let err = "Failed to load models"; try { const resp = xhr && xhr.responseJSON; if (resp && resp.error) err = resp.error; } catch (e) { } setModelsStatus(err, "err"); try { RED.notify(err, "error"); } catch (e) { } }) .always(function () { $("#knx-ai-refreshModels").prop("disabled", false); }); }; const installOllamaModel = () => { const provider = $("#node-input-llmProvider").val(); if (provider !== "ollama") return; const baseUrl = $("#node-input-llmBaseUrl").val() || ""; const model = String($("#node-input-llmModel").val() || "").trim() || OLLAMA_DEFAULT_MODEL; const payload = { nodeId: nodeId, baseUrl: baseUrl, model: model }; setModelsStatus(t('knxUltimateAI.messages.installingOllamaModel', "Installing Ollama model..."), "warn"); $("#knx-ai-refreshModels").prop("disabled", true); setOllamaInstallUiState(true); $.ajax({ url: "knxUltimateAI/ollama/pull", type: "POST", contentType: "application/json", data: JSON.stringify(payload) }) .done(function (data) { try { const okMsg = t('knxUltimateAI.messages.installedOllamaModel', "Ollama model installed") + ": " + model; RED.notify(okMsg, "success"); if (data && data.ollamaStarted) { const startedMsg = t('knxUltimateAI.messages.ollamaStartedAuto', "Ollama server started automatically."); RED.notify(startedMsg, "success"); } } catch (e) { } refreshModels(); }) .fail(function (xhr) { let err = t('knxUltimateAI.messages.installOllamaModelFailed', "Failed to install Ollama model"); try { const resp = xhr && xhr.responseJSON; if (resp && resp.error) err = resp.error; } catch (e) { } setModelsStatus(err, "err"); try { RED.notify(err, "error"); } catch (e) { } }) .always(function () { $("#knx-ai-refreshModels").prop("disabled", false); setOllamaInstallUiState(false); }); }; const openOllamaLibrary = () => { const wnd = window.open(OLLAMA_LIBRARY_URL, "_blank", "noopener,noreferrer"); try { if (wnd && typeof wnd.focus === "function") wnd.focus(); } catch (e) { } }; const toggleProvider = ({ applyDefaults = false, autoLoadModels = false } = {}) => { const provider = $("#node-input-llmProvider").val(); if (applyDefaults) applyProviderDefaults(provider); if (provider === "ollama") { $("#knx-ai-apikey-row").hide(); $("#knx-ai-ollama-warning").show(); $("#knx-ai-ollama-install-row").show(); $("#knx-ai-ollama-steps").show(); if (autoLoadModels) refreshModels(); } else { $("#knx-ai-apikey-row").show(); $("#knx-ai-ollama-warning").hide(); $("#knx-ai-ollama-install-row").hide(); $("#knx-ai-ollama-steps").hide(); setOllamaNoModelsVisible(false); } }; $("#node-input-llmProvider").on("change", function () { toggleProvider({ applyDefaults: true, autoLoadModels: true }); }); toggleProvider({ applyDefaults: true }); $("#knx-ai-refreshModels").on("click", function (evt) { evt.preventDefault(); refreshModels(); }); $("#knx-ai-downloadOllamaModel").on("click", function (evt) { evt.preventDefault(); openOllamaLibrary(); }); $("#knx-ai-installOllamaModel").on("click", function (evt) { evt.preventDefault(); installOllamaModel(); }); }, oneditsave: function () { try { RED.sidebar.show("info"); } catch (error) { } }, oneditcancel: function () { try { RED.sidebar.show("info"); } catch (error) { } } }) </script> <script type="text/html" data-template-name="knxUltimateAI"> <b><span data-i18n="knxUltimateAI.title"></span></b>&nbsp&nbsp<span style="color:#ff9800" &nbsp &nbsp <i class="fa fa-youtube"></i></span><a target="_blank" href="https://www.youtube.com/watch?v=qw7kjQ_mvdg&t=10s">See sample video</a> <br /><br /> <div class="form-row"> <label for="node-input-server"><i class="fa fa-tag"></i> <span data-i18n="knxUltimateAI.properties.server"></span></label> <input type="text" id="node-input-server"> </div> <div class="form-row"> <label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="knxUltimateAI.properties.name"></span></label> <input type="text" id="node-input-name" data-i18n="[placeholder]knxUltimateAI.properties.name" style="flex:1 1 240px; min-width:240px; max-width:240px;"> </div> <div class="form-row"> <label for="node-input-topic"><i class="fa fa-tasks"></i> <span data-i18n="knxUltimateAI.properties.topic"></span></label> <input type="text" id="node-input-topic" data-i18n="[placeholder]knxUltimateAI.properties.topic"> </div> <div class="form-row"> <label><i class="fa fa-external-link"></i> Web UI</label> <div style="display:flex; gap:8px; flex-wrap:wrap;"> <button type="button" class="red-ui-button" id="knx-ai-open-web-page-vue" style="background-color:#ff9800; border-color:#ff9800; color:#ffffff !important; -webkit-text-fill-color:#ffffff;"> <i class="fa fa-external-link" style="color:#ffffff !important;"></i> <span style="color:#ffffff !important;">Open KNX AI Web Page</span> </button> </div> </div> <div id="knx-ai-accordion"> <h3><span data-i18n="knxUltimateAI.sections.capture"></span></h3> <div> <div class="form-row"> <input type="checkbox" id="node-input-notifywrite" style="display:inline-block; width:auto; vertical-align:top;"> <label style="width:auto" for="node-input-notifywrite">&nbsp;&nbsp;<span data-i18n="knxUltimateAI.properties.notifywrite"></span></label> </div> <div class="form-row"> <input type="checkbox" id="node-input-notifyresponse" style="display:inline-block; width:auto; vertical-align:top;"> <label style="width:auto" for="node-input-notifyresponse">&nbsp;&nbsp;<span data-i18n="knxUltimateAI.properties.notifyresponse"></span></label> </div> <div class="form-row"> <input type="checkbox" id="node-input-notifyreadrequest" style="display:inline-block; width:auto; vertical-align:top;"> <label style="width:auto" for="node-input-notifyreadrequest">&nbsp;&nbsp;<span data-i18n="knxUltimateAI.properties.notifyreadrequest"></span></label> </div> </div> <h3><span data-i18n="knxUltimateAI.sections.analysis"></span></h3> <div> <div class="form-row"> <label style="width:290px" for="node-input-analysisWindowSec"><i class="fa fa-clock-o"></i> <span data-i18n="knxUltimateAI.properties.analysisWindowSec"></span></label> <input style="width:90px" type="number" id="node-input-analysisWindowSec"> </div> <div class="form-row"> <label style="width:290px" for="node-input-historyWindowSec"><i class="fa fa-history"></i> <span data-i18n="knxUltimateAI.properties.historyWindowSec"></span></label> <input style="width:90px" type="number" id="node-input-historyWindowSec"> </div> <div class="form-row"> <label style="width:290px" for="node-input-maxEvents"><i class="fa fa-bars"></i> <span data-i18n="knxUltimateAI.properties.maxEvents"></span></label> <input style="width:90px" type="number" id="node-input-maxEvents"> </div> <div class="form-row"> <label style="width:290px" for="node-input-emitIntervalSec"><i class="fa fa-send"></i> <span data-i18n="knxUltimateAI.properties.emitIntervalSec"></span></label> <input style="width:90px" type="number" id="node-input-emitIntervalSec"> </div> <div class="form-row"> <label style="width:290px" for="node-input-topN"><i class="fa fa-list-ol"></i> <span data-i18n="knxUltimateAI.properties.topN"></span></label> <input style="width:90px" type="number" id="node-input-topN"> </div> <div class="form-row"> <input type="checkbox" id="node-input-enablePattern" style="display:inline-block; width:auto; vertical-align:top;"> <label style="width:auto" for="node-input-enablePattern">&nbsp;&nbsp;<span data-i18n="knxUltimateAI.properties.enablePattern"></span></label> </div> <div class="form-row"> <label style="width:290px" for="node-input-patternMaxLagMs"><i class="fa fa-arrows-h"></i> <span data-i18n="knxUltimateAI.properties.patternMaxLagMs"></span></label> <input style="width:120px" type="number" id="node-input-patternMaxLagMs"> </div> <div class="form-row"> <label style="width:290px" for="node-input-patternMinCount"><i class="fa fa-filter"></i> <span data-i18n="knxUltimateAI.properties.patternMinCount"></span></label> <input style="width:90px" type="number" id="node-input-patternMinCount"> </div> </div> <h3><span data-i18n="knxUltimateAI.sections.anomalies"></span></h3> <div> <div class="form-row"> <label style="width:290px" for="node-input-rateWindowSec"><i class="fa fa-clock-o"></i> <span data-i18n="knxUltimateAI.properties.rateWindowSec"></span></label> <input style="width:90px" type="number" id="node-input-rateWindowSec"> </div> <div class="form-row"> <label style="width:290px" for="node-input-maxTelegramPerSecOverall"><i class="fa fa-tachometer"></i> <span data-i18n="knxUltimateAI.properties.maxTelegramPerSecOverall"></span></label> <input style="width:90px" type="number" id="node-input-maxTelegramPerSecOverall"> </div> <div class="form-row"> <label style="width:290px" for="node-input-maxTelegramPerSecPerGA"><i class="fa fa-bolt"></i> <span data-i18n="knxUltimateAI.properties.maxTelegramPerSecPerGA"></span></label> <input style="width:90px" type="number" id="node-input-maxTelegramPerSecPerGA"> </div> <div class="form-row"> <label style="width:290px" for="node-input-flapWindowSec"><i class="fa fa-exchange"></i> <span data-i18n="knxUltimateAI.properties.flapWindowSec"></span></label> <input style="width:90px" type="number" id="node-input-flapWindowSec"> </div> <div class="form-row"> <label style="width:290px" for="node-input-flapMaxChanges"><i class="fa fa-random"></i> <span data-i18n="knxUltimateAI.properties.flapMaxChanges"></span></label> <input style="width:90px" type="number" id="node-input-flapMaxChanges"> </div> </div> <h3><span data-i18n="knxUltimateAI.sections.llm"></span></h3> <div> <div class="form-row"> <input type="checkbox" id="node-input-llmEnabled" style="display:inline-block; width:auto; vertical-align:top;"> <label style="width:auto" for="node-input-llmEnabled">&nbsp;&nbsp;<span data-i18n="knxUltimateAI.properties.llmEnabled"></span></label> </div> <div id="knx-ai-llm-settings"> <div class="form-row"> <label style="width:290px" for="node-input-llmProvider"><i class="fa fa-cog"></i> <span data-i18n="knxUltimateAI.properties.llmProvider"></span></label> <select style="width:100%" id="node-input-llmProvider"> <option value="openai_compat" data-i18n="knxUltimateAI.selectlists.llmProvider.openai_compat"></option> <option value="ollama" data-i18n="knxUltimateAI.selectlists.llmProvider.ollama"></option> </select> </div> <div class="form-tips" id="knx-ai-ollama-warning" style="display:none;"> <span data-i18n="knxUltimateAI.messages.ollamaNotSupported"></span> </div> <div class="form-row"> <label style="width:290px" for="node-input-llmBaseUrl"><i class="fa fa-link"></i> <span data-i18n="knxUltimateAI.properties.llmBaseUrl"></span></label> <input type="text" id="node-input-llmBaseUrl" data-i18n="[placeholder]knxUltimateAI.placeholder.llmBaseUrl"> </div> <div class="form-row" id="knx-ai-apikey-row"> <label style="width:290px" for="node-input-llmApiKey"><i class="fa fa-key"></i> <span data-i18n="knxUltimateAI.properties.llmApiKey"></span></label> <input type="password" id="node-input-llmApiKey" data-i18n="[placeholder]knxUltimateAI.placeholder.llmApiKey"> </div> <div class="form-row"> <label style="width:290px" for="node-input-llmModel"><i class="fa fa-cube"></i> <span data-i18n="knxUltimateAI.properties.llmModel"></span></label> <input style="width:calc(100% - 110px)" type="text" id="node-input-llmModel" list="knx-ai-llmModels" data-i18n="[placeholder]knxUltimateAI.placeholder.llmModel"> <button type="button" class="red-ui-button" id="knx-ai-refreshModels" style="margin-left:6px;" title="Refresh models"> <i class="fa fa-refresh"></i> <span data-i18n="knxUltimateAI.buttons.refreshModels"></span> </button> </div> <datalist id="knx-ai-llmModels"></datalist> <div class="form-tips" id="knx-ai-models-status"></div> <div class="form-row" id="knx-ai-ollama-install-row" style="display:none;"> <label style="width:290px"><i class="fa fa-download"></i> Ollama</label> <div class="knx-ai-ollama-actions"> <button type="button" class="red-ui-button" id="knx-ai-downloadOllamaModel"> <i class="fa fa-cloud-download"></i> <span data-i18n="knxUltimateAI.buttons.downloadOllamaModel"></span> </button> <button type="button" class="red-ui-button" id="knx-ai-installOllamaModel"> <i class="fa fa-download"></i> <span data-i18n="knxUltimateAI.buttons.installOllamaModel"></span> </button> </div> </div> <div class="form-tips" id="knx-ai-ollama-steps" style="display:none;"> <span data-i18n="knxUltimateAI.messages.ollamaInstallSteps"></span> </div> <div class="form-tips" id="knx-ai-ollama-empty-help" style="display:none;"> <span data-i18n="knxUltimateAI.messages.ollamaNoModels"></span> <br> <code>ollama pull llama3.1</code> </div> <div class="form-row"> <label style="width:290px" for="node-input-llmSystemPrompt"><i class="fa fa-commenting-o"></i> <span data-i18n="knxUltimateAI.properties.llmSystemPrompt"></span></label> <textarea style="width:100%; height:110px;" id="node-input-llmSystemPrompt" data-i18n="[placeholder]knxUltimateAI.placeholder.llmSystemPrompt"></textarea> </div> <div class="form-row"> <label style="width:290px" for="node-input-llmTemperature"><i class="fa fa-sliders"></i> <span data-i18n="knxUltimateAI.properties.llmTemperature"></span></label> <input style="width:90px" type="number" id="node-input-llmTemperature"> </div> <div class="form-row"> <label style="width:290px" for="node-input-llmMaxTokens"><i class="fa fa-align-left"></i> <span data-i18n="knxUltimateAI.properties.llmMaxTokens"></span></label> <input style="width:90px" type="number" id="node-input-llmMaxTokens"> </div> <div class="form-row"> <label style="width:290px" for="node-input-llmTimeoutMs"><i class="fa fa-hourglass-half"></i> <span data-i18n="knxUltimateAI.properties.llmTimeoutMs"></span></label> <input style="width:120px" type="number" id="node-input-llmTimeoutMs"> </div> <div class="form-row"> <label style="width:290px" for="node-input-llmMaxEventsInPrompt"><i class="fa fa-list"></i> <span data-i18n="knxUltimateAI.properties.llmMaxEventsInPrompt"></span></label> <input style="width:90px" type="number" id="node-input-llmMaxEventsInPrompt"> </div> <div class="form-row"> <input type="checkbox" id="node-input-llmIncludeRaw" style="display:inline-block; width:auto; vertical-align:top;"> <label style="width:auto" for="node-input-llmIncludeRaw">&nbsp;&nbsp;<span data-i18n="knxUltimateAI.properties.llmIncludeRaw"></span></label> </div> <div class="form-row"> <input type="checkbox" id="node-input-llmIncludeFlowContext" style="display:inline-block; width:auto; vertical-align:top;"> <label style="width:auto" for="node-input-llmIncludeFlowContext">&nbsp;&nbsp;<span data-i18n="knxUltimateAI.properties.llmIncludeFlowContext"></span></label> </div> <div class="form-row"> <label style="width:290px" for="node-input-llmMaxFlowNodesInPrompt"><i class="fa fa-sitemap"></i> <span data-i18n="knxUltimateAI.properties.llmMaxFlowNodesInPrompt"></span></label> <input style="width:90px" type="number" id="node-input-llmMaxFlowNodesInPrompt"> </div> <div class="form-row"> <input type="checkbox" id="node-input-llmIncludeDocsSnippets" style="display:inline-block; width:auto; vertical-align:top;"> <label style="width:auto" for="node-input-llmIncludeDocsSnippets">&nbsp;&nbsp;<span data-i18n="knxUltimateAI.properties.llmIncludeDocsSnippets"></span></label> </div> <div class="form-row"> <label style="width:290px" for="node-input-llmDocsLanguage"><i class="fa fa-language"></i> <span data-i18n="knxUltimateAI.properties.llmDocsLanguage"></span></label> <select style="width:100%" id="node-input-llmDocsLanguage"> <option value="it">Italiano</option> <option value="en">English</option> <option value="de">Deutsch</option> <option value="fr">Français</option> <option value="es">Español</option> <option value="zh-CN">简体中文</option> </select> </div> <div class="form-row"> <label style="width:290px" for="node-input-llmDocsMaxSnippets"><i class="fa fa-files-o"></i> <span data-i18n="knxUltimateAI.properties.llmDocsMaxSnippets"></span></label> <input style="width:90px" type="number" id="node-input-llmDocsMaxSnippets"> </div> <div class="form-row"> <label style="width:290px" for="node-input-llmDocsMaxChars"><i class="fa fa-text-width"></i> <span data-i18n="knxUltimateAI.properties.llmDocsMaxChars"></span></label> <input style="width:110px" type="number" id="node-input-llmDocsMaxChars"> </div> </div> </div> </div> </script>