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
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() },
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>  <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.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"> <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"> <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"> <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">
<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"> <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>