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, KNX AI for diagnosticsand KNX routing between interfaces. Easy to use and highly configurable.
594 lines (536 loc) • 32.5 kB
HTML
<script type="text/javascript" src="resources/node-red-contrib-knx-ultimate/htmlUtils.js"></script>
<style type="text/css">
margin-top: 6px;
margin-bottom: 6px;
}
align-items: flex-start;
}
padding-top: 6px;
}
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
max-width: calc(100% - 300px);
}
margin: 0 !important;
}
margin-left: 290px;
margin-top: 6px;
}
@media (max-width: 900px) {
width: 100% !important;
padding-top: 0;
margin-bottom: 6px;
}
max-width: 100%;
}
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() },
historyStoreToDisk: { value: true },
historyStoreRetentionDays: { value: 10, 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: "" },
llmIncludeRaw: { value: false },
llmIncludeFlowContext: { value: true },
llmIncludeDocsSnippets: { value: true },
llmDocsLanguage: { value: "en" }
},
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";
$("#knx-ai-accordion").accordion({
header: "h3",
heightStyle: "content",
collapsible: true,
active: 0
});
const toggleLLM = () => {
const enabled = $("#node-input-llmEnabled").is(":checked");
$("#knx-ai-llm-connection-settings").toggle(enabled);
$("#knx-ai-llm-context-settings").toggle(enabled);
};
$("#node-input-llmEnabled").on("change", toggleLLM);
const toggleHistoryArchive = () => {
const enabled = $("#node-input-historyStoreToDisk").is(":checked");
$("#knx-ai-history-retention-row").toggle(enabled);
};
$("#node-input-historyStoreToDisk").on("change", toggleHistoryArchive);
const toggleDocsContext = () => {
const enabled = $("#node-input-llmIncludeDocsSnippets").is(":checked");
$("#knx-ai-docs-language-row").toggle(enabled);
};
$("#node-input-llmIncludeDocsSnippets").on("change", toggleDocsContext);
toggleLLM();
toggleHistoryArchive();
toggleDocsContext();
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>  <span style="color:#ff9800"     <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"> <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"> <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"> <span data-i18n="knxUltimateAI.properties.notifyreadrequest"></span></label>
</div>
</div>
<h3><span data-i18n="knxUltimateAI.sections.storage"></span></h3>
<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">
<input type="checkbox" id="node-input-historyStoreToDisk" style="display:inline-block; width:auto; vertical-align:top;">
<label style="width:auto" for="node-input-historyStoreToDisk"> <span data-i18n="knxUltimateAI.properties.historyStoreToDisk"></span></label>
</div>
<div class="form-row" id="knx-ai-history-retention-row">
<label style="width:290px" for="node-input-historyStoreRetentionDays"><i class="fa fa-calendar"></i> <span data-i18n="knxUltimateAI.properties.historyStoreRetentionDays"></span></label>
<input style="width:90px" type="number" id="node-input-historyStoreRetentionDays">
</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>
<h3><span data-i18n="knxUltimateAI.sections.detection"></span></h3>
<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"> <span data-i18n="knxUltimateAI.properties.enablePattern"></span></label>
</div>
</div>
<h3><span data-i18n="knxUltimateAI.sections.llmConnection"></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"> <span data-i18n="knxUltimateAI.properties.llmEnabled"></span></label>
</div>
<div id="knx-ai-llm-connection-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>
</div>
<h3><span data-i18n="knxUltimateAI.sections.llmContext"></span></h3>
<div>
<div id="knx-ai-llm-context-settings">
<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"> <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"> <span data-i18n="knxUltimateAI.properties.llmIncludeFlowContext"></span></label>
</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"> <span data-i18n="knxUltimateAI.properties.llmIncludeDocsSnippets"></span></label>
</div>
<div class="form-row" id="knx-ai-docs-language-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>
</div>
<h3><span data-i18n="knxUltimateAI.sections.advanced"></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-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-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">
<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 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 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>
</div>
</script>