node-red-contrib-knx-ultimate 4.3.0 → 4.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,18 @@
6
6
 
7
7
  # CHANGELOG
8
8
 
9
+ **Version 4.3.2** - April 2026<br/>
10
+
11
+ - Docs/help/wiki: updated **KNX AI** help HTML and wiki pages in all supported languages (EN/IT/DE/FR/ES/zh-CN) to reflect the latest LLM/Ollama UX changes.<br/>
12
+ - Docs/help/wiki: documented the new **Ollama quick setup** flow (**Download model** -> **Install it**).<br/>
13
+ - Docs/help/wiki: clarified that the **LLM Assistant** tab is shown first in the KNX AI editor for faster setup.<br/>
14
+
15
+ **Version 4.3.1** - April 2026<br/>
16
+
17
+ - Bumped KNXEngine to 5.5.2<br/>
18
+ - UI: **KNX AI Web** now always opens on the first menu item (**Overview**).<br/>
19
+ - FIX: **KNX AI Web Assistant** preset prompts now send the exact current button text to the AI request, so localized UI labels are used directly (no forced English fallback).<br/>
20
+
9
21
  **Version 4.3.0** - April 2026<br/>
10
22
 
11
23
  - Bumped KNXEngine to 5.5.1<br/>
@@ -982,4 +982,4 @@ module.exports = function (RED) {
982
982
  startPeriodicSendTimer()
983
983
  }
984
984
  RED.nodes.registerType('knxUltimate', knxUltimate)
985
- }
985
+ }
@@ -1,4 +1,53 @@
1
1
  <script type="text/javascript" src="resources/node-red-contrib-knx-ultimate/htmlUtils.js"></script>
2
+ <style type="text/css">
3
+ #knx-ai-models-status {
4
+ margin-top: 6px;
5
+ margin-bottom: 6px;
6
+ }
7
+
8
+ #knx-ai-ollama-install-row {
9
+ align-items: flex-start;
10
+ }
11
+
12
+ #knx-ai-ollama-install-row > label {
13
+ padding-top: 6px;
14
+ }
15
+
16
+ #knx-ai-ollama-install-row .knx-ai-ollama-actions {
17
+ display: flex;
18
+ flex-wrap: wrap;
19
+ gap: 8px;
20
+ align-items: center;
21
+ max-width: calc(100% - 300px);
22
+ }
23
+
24
+ #knx-ai-ollama-install-row .knx-ai-ollama-actions .red-ui-button {
25
+ margin: 0 !important;
26
+ }
27
+
28
+ #knx-ai-ollama-steps,
29
+ #knx-ai-ollama-empty-help {
30
+ margin-left: 290px;
31
+ margin-top: 6px;
32
+ }
33
+
34
+ @media (max-width: 900px) {
35
+ #knx-ai-ollama-install-row > label {
36
+ width: 100% !important;
37
+ padding-top: 0;
38
+ margin-bottom: 6px;
39
+ }
40
+
41
+ #knx-ai-ollama-install-row .knx-ai-ollama-actions {
42
+ max-width: 100%;
43
+ }
44
+
45
+ #knx-ai-ollama-steps,
46
+ #knx-ai-ollama-empty-help {
47
+ margin-left: 0;
48
+ }
49
+ }
50
+ </style>
2
51
 
3
52
  <script type="text/javascript">
4
53
  RED.nodes.registerType('knxUltimateAI', {
@@ -41,11 +90,11 @@
41
90
  llmMaxEventsInPrompt: { value: 120, required: false, validate: RED.validators.number() },
42
91
  llmIncludeRaw: { value: false },
43
92
  llmIncludeFlowContext: { value: true },
44
- llmMaxFlowNodesInPrompt: { value: 80, required: false, validate: RED.validators.number() },
93
+ llmMaxFlowNodesInPrompt: { value: 400, required: false, validate: RED.validators.number() },
45
94
  llmIncludeDocsSnippets: { value: true },
46
95
  llmDocsLanguage: { value: "en" },
47
96
  llmDocsMaxSnippets: { value: 5, required: false, validate: RED.validators.number() },
48
- llmDocsMaxChars: { value: 3000, required: false, validate: RED.validators.number() }
97
+ llmDocsMaxChars: { value: 60000, required: false, validate: RED.validators.number() }
49
98
  },
50
99
  credentials: {
51
100
  llmApiKey: { type: "password" }
@@ -66,6 +115,19 @@
66
115
  paletteLabel: "KNX AI",
67
116
  oneditprepare: function () {
68
117
  try { RED.sidebar.show("help"); } catch (error) { }
118
+ const OPENAI_COMPAT_DEFAULT_CHAT_URL = "https://api.openai.com/v1/chat/completions";
119
+ const OLLAMA_DEFAULT_CHAT_URL = "http://localhost:11434/api/chat";
120
+ const OLLAMA_LIBRARY_URL = "https://ollama.com/library";
121
+ const OPENAI_COMPAT_DEFAULT_MODELS = ["gpt-5.4", "gpt-4o-mini"];
122
+ const OLLAMA_DEFAULT_MODEL = "llama3.1";
123
+
124
+ const $accordion = $("#knx-ai-accordion");
125
+ const $llmContent = $accordion.find("#node-input-llmEnabled").closest("#knx-ai-accordion > div").first();
126
+ const $llmHeader = $llmContent.prev("h3");
127
+ if ($llmHeader.length && $llmContent.length) {
128
+ $llmContent.prependTo($accordion);
129
+ $llmHeader.prependTo($accordion);
130
+ }
69
131
 
70
132
  $("#knx-ai-accordion").accordion({
71
133
  header: "h3",
@@ -85,19 +147,6 @@
85
147
  $("#node-input-llmEnabled").on("change", toggleLLM);
86
148
  toggleLLM();
87
149
 
88
- const toggleProvider = () => {
89
- const provider = $("#node-input-llmProvider").val();
90
- if (provider === "ollama") {
91
- $("#knx-ai-apikey-row").hide();
92
- $("#knx-ai-ollama-warning").show();
93
- } else {
94
- $("#knx-ai-apikey-row").show();
95
- $("#knx-ai-ollama-warning").hide();
96
- }
97
- };
98
- $("#node-input-llmProvider").on("change", toggleProvider);
99
- toggleProvider();
100
-
101
150
  const nodeId = this.id;
102
151
  const resolveAdminRoot = () => {
103
152
  const raw = (RED.settings && typeof RED.settings.httpAdminRoot === "string") ? RED.settings.httpAdminRoot : "/";
@@ -114,6 +163,11 @@
114
163
  return "";
115
164
  }
116
165
  };
166
+ const t = (key, fallback) => {
167
+ const value = RED._(key);
168
+ if (!value || value === key) return fallback || "";
169
+ return value;
170
+ };
117
171
 
118
172
  $("#knx-ai-open-web-page-vue").on("click", function (evt) {
119
173
  evt.preventDefault();
@@ -138,6 +192,16 @@
138
192
  if (level === "err") $status.addClass("knx-ai-models-err");
139
193
  };
140
194
 
195
+ const setOllamaNoModelsVisible = (visible) => {
196
+ if (visible) $("#knx-ai-ollama-empty-help").show();
197
+ else $("#knx-ai-ollama-empty-help").hide();
198
+ };
199
+
200
+ const setOllamaInstallUiState = (busy) => {
201
+ $("#knx-ai-installOllamaModel").prop("disabled", !!busy);
202
+ $("#knx-ai-downloadOllamaModel").prop("disabled", !!busy);
203
+ };
204
+
141
205
  const populateModels = (models) => {
142
206
  const $dl = $("#knx-ai-llmModels");
143
207
  if (!$dl || $dl.length === 0) return;
@@ -147,15 +211,37 @@
147
211
  });
148
212
  };
149
213
 
214
+ const normalizeUrl = (value) => String(value || "").trim().replace(/\/+$/, "").toLowerCase();
215
+ const isOpenAiDefaultUrl = (value) => normalizeUrl(value) === normalizeUrl(OPENAI_COMPAT_DEFAULT_CHAT_URL);
216
+ const isOllamaDefaultUrl = (value) => normalizeUrl(value) === normalizeUrl(OLLAMA_DEFAULT_CHAT_URL);
217
+ const isOpenAiDefaultModel = (value) => OPENAI_COMPAT_DEFAULT_MODELS.indexOf(String(value || "").trim().toLowerCase()) >= 0;
218
+
219
+ const applyProviderDefaults = (provider) => {
220
+ const $baseUrl = $("#node-input-llmBaseUrl");
221
+ const $model = $("#node-input-llmModel");
222
+ const currentBaseUrl = String($baseUrl.val() || "").trim();
223
+ const currentModel = String($model.val() || "").trim();
224
+
225
+ if (provider === "ollama") {
226
+ if (!currentBaseUrl || isOpenAiDefaultUrl(currentBaseUrl)) $baseUrl.val(OLLAMA_DEFAULT_CHAT_URL);
227
+ if (!currentModel || isOpenAiDefaultModel(currentModel)) $model.val(OLLAMA_DEFAULT_MODEL);
228
+ return;
229
+ }
230
+
231
+ if (!currentBaseUrl || isOllamaDefaultUrl(currentBaseUrl)) $baseUrl.val(OPENAI_COMPAT_DEFAULT_CHAT_URL);
232
+ if (!currentModel || String(currentModel).trim().toLowerCase() === OLLAMA_DEFAULT_MODEL) $model.val(OPENAI_COMPAT_DEFAULT_MODELS[0]);
233
+ };
234
+
150
235
  const refreshModels = () => {
151
236
  const provider = $("#node-input-llmProvider").val();
152
237
  const baseUrl = $("#node-input-llmBaseUrl").val() || "";
153
238
  const apiKey = $("#node-input-llmApiKey").val() || "";
154
239
  const payload = { nodeId: nodeId, provider: provider, baseUrl: baseUrl };
240
+ if (provider === "ollama") payload.autoStart = true;
155
241
  // Node-RED uses "__PWRD__" as placeholder for stored credentials: don't send it.
156
242
  if (apiKey && apiKey !== "__PWRD__") payload.apiKey = apiKey;
157
243
 
158
- setModelsStatus(RED._('knxUltimateAI.messages.loadingModels') || "Loading models...", "warn");
244
+ setModelsStatus(t('knxUltimateAI.messages.loadingModels', "Loading models..."), "warn");
159
245
  $("#knx-ai-refreshModels").prop("disabled", true);
160
246
 
161
247
  $.ajax({
@@ -166,9 +252,23 @@
166
252
  })
167
253
  .done(function (data) {
168
254
  const models = (data && data.models) ? data.models : [];
255
+ let msg = "";
169
256
  populateModels(models);
170
- const msg = (RED._('knxUltimateAI.messages.loadedModels') || "Models loaded") + ": " + models.length;
171
- setModelsStatus(msg, "ok");
257
+ if (provider === "ollama" && data && data.ollamaStarted) {
258
+ try {
259
+ const startedMsg = t('knxUltimateAI.messages.ollamaStartedAuto', "Ollama server started automatically.");
260
+ RED.notify(startedMsg, "success");
261
+ } catch (e) { }
262
+ }
263
+ if (provider === "ollama" && models.length === 0) {
264
+ msg = t('knxUltimateAI.messages.ollamaNoModels', "No local Ollama model found. Install one from https://ollama.com/library.");
265
+ setModelsStatus(msg, "warn");
266
+ setOllamaNoModelsVisible(true);
267
+ } else {
268
+ msg = t('knxUltimateAI.messages.loadedModels', "Models loaded") + ": " + models.length;
269
+ setModelsStatus(msg, "ok");
270
+ setOllamaNoModelsVisible(false);
271
+ }
172
272
  try { RED.notify(msg, "success"); } catch (e) { }
173
273
  })
174
274
  .fail(function (xhr) {
@@ -185,10 +285,88 @@
185
285
  });
186
286
  };
187
287
 
288
+ const installOllamaModel = () => {
289
+ const provider = $("#node-input-llmProvider").val();
290
+ if (provider !== "ollama") return;
291
+ const baseUrl = $("#node-input-llmBaseUrl").val() || "";
292
+ const model = String($("#node-input-llmModel").val() || "").trim() || OLLAMA_DEFAULT_MODEL;
293
+ const payload = { nodeId: nodeId, baseUrl: baseUrl, model: model };
294
+
295
+ setModelsStatus(t('knxUltimateAI.messages.installingOllamaModel', "Installing Ollama model..."), "warn");
296
+ $("#knx-ai-refreshModels").prop("disabled", true);
297
+ setOllamaInstallUiState(true);
298
+
299
+ $.ajax({
300
+ url: "knxUltimateAI/ollama/pull",
301
+ type: "POST",
302
+ contentType: "application/json",
303
+ data: JSON.stringify(payload)
304
+ })
305
+ .done(function (data) {
306
+ try {
307
+ const okMsg = t('knxUltimateAI.messages.installedOllamaModel', "Ollama model installed") + ": " + model;
308
+ RED.notify(okMsg, "success");
309
+ if (data && data.ollamaStarted) {
310
+ const startedMsg = t('knxUltimateAI.messages.ollamaStartedAuto', "Ollama server started automatically.");
311
+ RED.notify(startedMsg, "success");
312
+ }
313
+ } catch (e) { }
314
+ refreshModels();
315
+ })
316
+ .fail(function (xhr) {
317
+ let err = t('knxUltimateAI.messages.installOllamaModelFailed', "Failed to install Ollama model");
318
+ try {
319
+ const resp = xhr && xhr.responseJSON;
320
+ if (resp && resp.error) err = resp.error;
321
+ } catch (e) { }
322
+ setModelsStatus(err, "err");
323
+ try { RED.notify(err, "error"); } catch (e) { }
324
+ })
325
+ .always(function () {
326
+ $("#knx-ai-refreshModels").prop("disabled", false);
327
+ setOllamaInstallUiState(false);
328
+ });
329
+ };
330
+
331
+ const openOllamaLibrary = () => {
332
+ const wnd = window.open(OLLAMA_LIBRARY_URL, "_blank", "noopener,noreferrer");
333
+ try { if (wnd && typeof wnd.focus === "function") wnd.focus(); } catch (e) { }
334
+ };
335
+
336
+ const toggleProvider = ({ applyDefaults = false, autoLoadModels = false } = {}) => {
337
+ const provider = $("#node-input-llmProvider").val();
338
+ if (applyDefaults) applyProviderDefaults(provider);
339
+ if (provider === "ollama") {
340
+ $("#knx-ai-apikey-row").hide();
341
+ $("#knx-ai-ollama-warning").show();
342
+ $("#knx-ai-ollama-install-row").show();
343
+ $("#knx-ai-ollama-steps").show();
344
+ if (autoLoadModels) refreshModels();
345
+ } else {
346
+ $("#knx-ai-apikey-row").show();
347
+ $("#knx-ai-ollama-warning").hide();
348
+ $("#knx-ai-ollama-install-row").hide();
349
+ $("#knx-ai-ollama-steps").hide();
350
+ setOllamaNoModelsVisible(false);
351
+ }
352
+ };
353
+ $("#node-input-llmProvider").on("change", function () {
354
+ toggleProvider({ applyDefaults: true, autoLoadModels: true });
355
+ });
356
+ toggleProvider({ applyDefaults: true });
357
+
188
358
  $("#knx-ai-refreshModels").on("click", function (evt) {
189
359
  evt.preventDefault();
190
360
  refreshModels();
191
361
  });
362
+ $("#knx-ai-downloadOllamaModel").on("click", function (evt) {
363
+ evt.preventDefault();
364
+ openOllamaLibrary();
365
+ });
366
+ $("#knx-ai-installOllamaModel").on("click", function (evt) {
367
+ evt.preventDefault();
368
+ installOllamaModel();
369
+ });
192
370
  },
193
371
  oneditsave: function () {
194
372
  try { RED.sidebar.show("info"); } catch (error) { }
@@ -342,6 +520,25 @@
342
520
  </div>
343
521
  <datalist id="knx-ai-llmModels"></datalist>
344
522
  <div class="form-tips" id="knx-ai-models-status"></div>
523
+ <div class="form-row" id="knx-ai-ollama-install-row" style="display:none;">
524
+ <label style="width:290px"><i class="fa fa-download"></i> Ollama</label>
525
+ <div class="knx-ai-ollama-actions">
526
+ <button type="button" class="red-ui-button" id="knx-ai-downloadOllamaModel">
527
+ <i class="fa fa-cloud-download"></i> <span data-i18n="knxUltimateAI.buttons.downloadOllamaModel"></span>
528
+ </button>
529
+ <button type="button" class="red-ui-button" id="knx-ai-installOllamaModel">
530
+ <i class="fa fa-download"></i> <span data-i18n="knxUltimateAI.buttons.installOllamaModel"></span>
531
+ </button>
532
+ </div>
533
+ </div>
534
+ <div class="form-tips" id="knx-ai-ollama-steps" style="display:none;">
535
+ <span data-i18n="knxUltimateAI.messages.ollamaInstallSteps"></span>
536
+ </div>
537
+ <div class="form-tips" id="knx-ai-ollama-empty-help" style="display:none;">
538
+ <span data-i18n="knxUltimateAI.messages.ollamaNoModels"></span>
539
+ <br>
540
+ <code>ollama pull llama3.1</code>
541
+ </div>
345
542
 
346
543
  <div class="form-row">
347
544
  <label style="width:290px" for="node-input-llmSystemPrompt"><i class="fa fa-commenting-o"></i> <span data-i18n="knxUltimateAI.properties.llmSystemPrompt"></span></label>