node-red-contrib-knx-ultimate 6.3.22 → 6.3.25

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.
Files changed (38) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/examples/IoT Bridge - Modbus Flex Adapter.json +361 -0
  3. package/examples/KNX AI - Telegrambot Direct Chat.json +1 -17
  4. package/nodes/knxUltimate-config.js +13 -4
  5. package/nodes/knxUltimateAI.html +142 -15
  6. package/nodes/knxUltimateAI.js +734 -163
  7. package/nodes/knxUltimateIoTBridge.html +349 -20
  8. package/nodes/knxUltimateIoTBridge.js +474 -44
  9. package/nodes/locales/de/knxUltimateAI.html +26 -8
  10. package/nodes/locales/de/knxUltimateAI.json +11 -1
  11. package/nodes/locales/de/knxUltimateIoTBridge.html +28 -2
  12. package/nodes/locales/de/knxUltimateIoTBridge.json +20 -2
  13. package/nodes/locales/en/knxUltimateAI.html +26 -8
  14. package/nodes/locales/en/knxUltimateAI.json +11 -1
  15. package/nodes/locales/en/knxUltimateIoTBridge.html +28 -2
  16. package/nodes/locales/en/knxUltimateIoTBridge.json +20 -2
  17. package/nodes/locales/es/knxUltimateAI.html +26 -8
  18. package/nodes/locales/es/knxUltimateAI.json +11 -1
  19. package/nodes/locales/es/knxUltimateIoTBridge.html +28 -2
  20. package/nodes/locales/es/knxUltimateIoTBridge.json +20 -2
  21. package/nodes/locales/fr/knxUltimateAI.html +26 -8
  22. package/nodes/locales/fr/knxUltimateAI.json +11 -1
  23. package/nodes/locales/fr/knxUltimateIoTBridge.html +28 -2
  24. package/nodes/locales/fr/knxUltimateIoTBridge.json +20 -2
  25. package/nodes/locales/it/knxUltimateAI.html +26 -8
  26. package/nodes/locales/it/knxUltimateAI.json +11 -1
  27. package/nodes/locales/it/knxUltimateIoTBridge.html +28 -2
  28. package/nodes/locales/it/knxUltimateIoTBridge.json +20 -2
  29. package/nodes/locales/zh-CN/knxUltimateAI.html +26 -8
  30. package/nodes/locales/zh-CN/knxUltimateAI.json +11 -1
  31. package/nodes/locales/zh-CN/knxUltimateIoTBridge.html +28 -2
  32. package/nodes/locales/zh-CN/knxUltimateIoTBridge.json +20 -2
  33. package/nodes/plugins/knxUltimateAI-vue/assets/app.css +1 -1
  34. package/nodes/plugins/knxUltimateAI-vue/assets/app.js +4 -4
  35. package/nodes/utils/knxAiChatContext.js +181 -84
  36. package/nodes/utils/knxAiEventHistory.js +275 -0
  37. package/package.json +3 -3
  38. package/resources/KNXAIChatAdapterMappings.js +14 -12
@@ -5,6 +5,37 @@
5
5
  align-items: flex-start;
6
6
  }
7
7
 
8
+ #knx-ai-llm-connection-settings {
9
+ width: 100%;
10
+ max-width: 720px;
11
+ min-width: 0;
12
+ }
13
+
14
+ #knx-ai-llm-connection-settings .form-row {
15
+ min-width: 0;
16
+ }
17
+
18
+ #knx-ai-local-context-status,
19
+ .knx-ai-prompt-context-hint {
20
+ width: 100%;
21
+ max-width: 720px;
22
+ min-width: 0;
23
+ box-sizing: border-box;
24
+ color: var(--red-ui-secondary-text-color, #777);
25
+ font-size: 12px;
26
+ line-height: 1.4;
27
+ white-space: normal;
28
+ overflow-wrap: break-word;
29
+ }
30
+
31
+ #knx-ai-local-context-status {
32
+ margin: -8px 0 12px;
33
+ }
34
+
35
+ .knx-ai-prompt-context-hint {
36
+ margin: 6px 0 0;
37
+ }
38
+
8
39
  #knx-ai-ollama-install-row>label {
9
40
  padding-top: 6px;
10
41
  }
@@ -138,7 +169,8 @@
138
169
  }
139
170
 
140
171
  #knx-ai-detected-adapters-panel,
141
- #knx-ai-chat-context-panel {
172
+ #knx-ai-chat-context-panel,
173
+ #knx-ai-chat-learning-link-panel {
142
174
  margin: 0 0 18px;
143
175
  padding: 12px 14px;
144
176
  border: 1px solid var(--red-ui-form-input-border-color, #ccc);
@@ -155,7 +187,8 @@
155
187
  }
156
188
 
157
189
  #knx-ai-detected-adapters-panel>h4,
158
- #knx-ai-chat-context-panel>h4 {
190
+ #knx-ai-chat-context-panel>h4,
191
+ #knx-ai-chat-learning-link-panel>h4 {
159
192
  margin: 0 0 9px;
160
193
  font-size: 14px;
161
194
  font-weight: 600;
@@ -189,6 +222,17 @@
189
222
  font-size: 12px;
190
223
  }
191
224
 
225
+ #knx-ai-chat-learning-link-panel {
226
+ margin: 18px 0 0;
227
+ }
228
+
229
+ .knx-ai-chat-learning-link-copy {
230
+ margin: 0 0 10px;
231
+ color: var(--red-ui-secondary-text-color, #777);
232
+ font-size: 12px;
233
+ line-height: 1.45;
234
+ }
235
+
192
236
  .knx-ai-chat-context-metric {
193
237
  margin-top: 6px;
194
238
  font-weight: 600;
@@ -273,6 +317,7 @@
273
317
  llmBaseUrl: { value: "https://api.openai.com/v1/chat/completions" },
274
318
  llmModel: { value: "gpt-5.4" },
275
319
  llmContextLength: { value: 0 },
320
+ llmPromptContextTokens: { value: 16384 },
276
321
  llmAllowKnxCommands: { value: false },
277
322
  llmRequireCommandConfirmation: { value: true },
278
323
  chatAdapterPreset: { value: "none" },
@@ -398,6 +443,13 @@
398
443
  } catch (error) { }
399
444
  delete node[key];
400
445
  });
446
+ try {
447
+ const $model = $("#node-input-llmModel");
448
+ $model.off(".knxUltimateAiModelPicker");
449
+ if (typeof $model.autocomplete === "function" && $model.data("ui-autocomplete")) {
450
+ $model.autocomplete("destroy");
451
+ }
452
+ } catch (error) { }
401
453
  };
402
454
 
403
455
  try {
@@ -658,7 +710,11 @@
658
710
  const currentProvider = String(contextLimit.provider || $("#node-input-llmProvider").val() || "").toLowerCase();
659
711
  let contextLimitTokens = Math.max(0, Number(contextLimit.tokens) || 0);
660
712
  if (!contextLimitTokens && (currentProvider === "lmstudio" || currentProvider === "ollama")) {
661
- contextLimitTokens = Math.min(Math.max(0, Number($("#node-input-llmContextLength").val()) || 16384), 16384);
713
+ contextLimitTokens = Math.min(
714
+ Math.max(0, Number($("#node-input-llmContextLength").val()) || 16384),
715
+ Math.max(0, Number($("#node-input-llmPromptContextTokens").val()) || 16384),
716
+ 16384
717
+ );
662
718
  }
663
719
  const contextLimitValue = contextLimitTokens
664
720
  ? formatCompactTokens(contextLimitTokens) + " " + t("knxUltimateAI.messages.chatContextTokens", "tokens")
@@ -742,7 +798,7 @@
742
798
  });
743
799
 
744
800
  $("#knx-ai-chat-context-pattern")
745
- .text(t("knxUltimateAI.messages.chatContextTelegramPattern", "Daily files") + ": " + String(overview.telegramFilePattern || "YYYY-MM-DD.jsonl"));
801
+ .text(t("knxUltimateAI.messages.chatContextTelegramPattern", "Daily files") + ": " + String(overview.telegramFilePattern || "YYYY-MM-DD.knxctx"));
746
802
  $status.hide();
747
803
  $content.show();
748
804
  };
@@ -763,17 +819,30 @@
763
819
  renderChatContextOverview(null);
764
820
  });
765
821
 
766
- $("#knx-ai-open-web-page-vue").on("click", function (evt) {
767
- evt.preventDefault();
822
+ const openKnxAiWebPage = function (view) {
768
823
  const adminRoot = resolveAdminRoot();
769
824
  const targetBase = adminRoot + "/knxUltimateAI/sidebar/page";
770
825
  const params = new URLSearchParams();
771
826
  if (nodeId) params.set("nodeId", nodeId);
827
+ if (view === "chat-learning") {
828
+ params.set("tab", "settings");
829
+ params.set("settingsTab", "learning");
830
+ }
772
831
  const accessToken = resolveAccessToken();
773
832
  if (accessToken) params.set("access_token", accessToken);
774
833
  const target = targetBase + (params.toString() ? ("?" + params.toString()) : "");
775
834
  const wnd = window.open(target, "_blank", "noopener,noreferrer");
776
835
  try { if (wnd && typeof wnd.focus === "function") wnd.focus(); } catch (e) { }
836
+ };
837
+
838
+ $("#knx-ai-open-web-page-vue").on("click", function (evt) {
839
+ evt.preventDefault();
840
+ openKnxAiWebPage();
841
+ });
842
+
843
+ $("#knx-ai-open-chat-learning").on("click", function (evt) {
844
+ evt.preventDefault();
845
+ openKnxAiWebPage("chat-learning");
777
846
  });
778
847
 
779
848
  const setRefreshModelsBusy = (busy) => {
@@ -788,6 +857,8 @@
788
857
  };
789
858
 
790
859
  let localModelDetailsById = new Map();
860
+ let availableModelItems = [];
861
+ let modelPickerMouseDown = false;
791
862
  let localContextRequestVersion = 0;
792
863
 
793
864
  const formatContextTokens = (value) => {
@@ -823,28 +894,63 @@
823
894
  text += " · " + t("knxUltimateAI.messages.lmStudioContextCurrentlyLoaded", "currently loaded") + ": " + loadedTokens + " token";
824
895
  }
825
896
  if (provider === "lmstudio" || provider === "ollama") {
826
- text += " · " + t("knxUltimateAI.messages.localContextBudget", "KNX AI context budget") + ": 16K";
897
+ const selectedBudget = Math.max(4096, Number($("#node-input-llmPromptContextTokens").val()) || 16384);
898
+ text += " · " + t("knxUltimateAI.messages.localContextBudget", "KNX AI context budget") + ": " + (selectedBudget / 1024) + "K";
827
899
  }
828
900
  $status.text(text).css("color", color).show();
829
901
  };
830
902
 
831
903
  const populateModels = (models, modelDetails) => {
832
- const $dl = $("#knx-ai-llmModels");
833
- if (!$dl || $dl.length === 0) return;
834
904
  localModelDetailsById = new Map();
835
905
  (modelDetails || []).forEach(function (detail) {
836
906
  if (detail && detail.id) localModelDetailsById.set(String(detail.id), detail);
837
907
  });
838
- $dl.empty();
839
- (models || []).forEach(function (m) {
908
+ availableModelItems = (models || []).map(function (m) {
840
909
  const detail = localModelDetailsById.get(String(m));
841
910
  const label = detail
842
911
  ? String(detail.displayName || detail.id) + " · " + formatContextTokens(detail.maxContextLength) + " token"
843
912
  : String(m);
844
- $("<option>").attr("value", m).attr("label", label).appendTo($dl);
913
+ return { label: label, value: String(m) };
845
914
  });
846
915
  };
847
916
 
917
+ const configureModelPicker = () => {
918
+ const $model = $("#node-input-llmModel");
919
+ if (!$model.length || typeof $model.autocomplete !== "function") return;
920
+ $model.autocomplete({
921
+ minLength: 0,
922
+ source: function (request, response) {
923
+ const term = String(request && request.term ? request.term : "").trim().toLowerCase();
924
+ if (!term) {
925
+ response(availableModelItems.slice());
926
+ return;
927
+ }
928
+ response(availableModelItems.filter(function (item) {
929
+ return item.value.toLowerCase().includes(term) || item.label.toLowerCase().includes(term);
930
+ }));
931
+ },
932
+ select: function (event, ui) {
933
+ $(this).val(ui.item.value).trigger("change");
934
+ return false;
935
+ }
936
+ });
937
+ $model.on("mousedown.knxUltimateAiModelPicker", function () {
938
+ modelPickerMouseDown = true;
939
+ }).on("focus.knxUltimateAiModelPicker", function () {
940
+ if (!modelPickerMouseDown) $(this).autocomplete("search", "");
941
+ }).on("click.knxUltimateAiModelPicker", function () {
942
+ modelPickerMouseDown = false;
943
+ const input = this;
944
+ setTimeout(function () {
945
+ try { $(input).autocomplete("search", ""); } catch (error) { }
946
+ }, 0);
947
+ }).on("blur.knxUltimateAiModelPicker", function () {
948
+ modelPickerMouseDown = false;
949
+ });
950
+ };
951
+
952
+ configureModelPicker();
953
+
848
954
  const normalizeUrl = (value) => String(value || "").trim().replace(/\/+$/, "").toLowerCase();
849
955
  const isOpenAiDefaultUrl = (value) => normalizeUrl(value) === normalizeUrl(OPENAI_COMPAT_DEFAULT_CHAT_URL);
850
956
  const isOllamaDefaultUrl = (value) => normalizeUrl(value) === normalizeUrl(OLLAMA_DEFAULT_CHAT_URL);
@@ -1044,7 +1150,9 @@
1044
1150
 
1045
1151
  const toggleProvider = ({ applyDefaults = false, autoLoadModels = false } = {}) => {
1046
1152
  const provider = $("#node-input-llmProvider").val();
1153
+ const localProvider = provider === "lmstudio" || provider === "ollama";
1047
1154
  if (applyDefaults) applyProviderDefaults(provider);
1155
+ $("#knx-ai-prompt-context-row").toggle(localProvider);
1048
1156
  if (provider === "ollama") {
1049
1157
  $("#knx-ai-apikey-row").hide();
1050
1158
  $("#knx-ai-ollama-install-row").show();
@@ -1071,6 +1179,10 @@
1071
1179
  $("#node-input-llmModel").on("change", function () {
1072
1180
  configureSelectedLocalModel();
1073
1181
  });
1182
+ $("#node-input-llmPromptContextTokens").on("change", function () {
1183
+ const detail = localModelDetailsById.get(String($("#node-input-llmModel").val() || "").trim());
1184
+ if (detail) setLocalModelContextStatus(detail, detail.loadedContextLength > 0 ? "configured" : "inactive");
1185
+ });
1074
1186
  $("#knx-ai-downloadOllamaModel").on("click", function (evt) {
1075
1187
  evt.preventDefault();
1076
1188
  openOllamaLibrary();
@@ -1171,6 +1283,13 @@
1171
1283
  <h4><i class="fa fa-home"></i> <span data-i18n="knxUltimateAI.sections.homeIntelligence"></span></h4>
1172
1284
  <div class="knx-ai-accordion-mount" id="knx-ai-mount-home-intelligence"></div>
1173
1285
  </div>
1286
+ <div id="knx-ai-chat-learning-link-panel">
1287
+ <h4><i class="fa fa-graduation-cap"></i> <span data-i18n="knxUltimateAI.sections.chatLearning"></span></h4>
1288
+ <p class="knx-ai-chat-learning-link-copy" data-i18n="knxUltimateAI.messages.chatLearningOpenHint"></p>
1289
+ <button type="button" class="red-ui-button" id="knx-ai-open-chat-learning">
1290
+ <i class="fa fa-external-link"></i> <span data-i18n="knxUltimateAI.buttons.openChatLearning"></span>
1291
+ </button>
1292
+ </div>
1174
1293
  <div id="knx-ai-detected-adapters-panel">
1175
1294
  <h4><i class="fa fa-puzzle-piece"></i> <span data-i18n="knxUltimateAI.sections.detectedAdapters"></span></h4>
1176
1295
  <div id="knx-ai-detected-adapters-status" data-i18n="knxUltimateAI.messages.detectedAdaptersLoading"></div>
@@ -1226,14 +1345,22 @@
1226
1345
 
1227
1346
  <div class="form-row">
1228
1347
  <label style="width:290px" for="node-input-llmModel"><i class="fa fa-cube"></i> <span data-i18n="knxUltimateAI.properties.llmModel"></span></label>
1229
- <input style="width:calc(100% - 110px)" type="text" id="node-input-llmModel" list="knx-ai-llmModels" data-i18n="[placeholder]knxUltimateAI.placeholder.llmModel">
1348
+ <input style="width:calc(100% - 110px)" type="text" id="node-input-llmModel" autocomplete="off" data-i18n="[placeholder]knxUltimateAI.placeholder.llmModel">
1230
1349
  <button type="button" class="red-ui-button" id="knx-ai-refreshModels" style="margin-left:6px;" title="Refresh models">
1231
1350
  <i class="fa fa-refresh"></i> <span data-i18n="knxUltimateAI.buttons.refreshModels"></span>
1232
1351
  </button>
1233
1352
  </div>
1234
- <datalist id="knx-ai-llmModels"></datalist>
1235
1353
  <input type="hidden" id="node-input-llmContextLength">
1236
- <div id="knx-ai-local-context-status" style="display:none; margin:-8px 0 12px 290px; font-size:12px;"></div>
1354
+ <div id="knx-ai-local-context-status" style="display:none;"></div>
1355
+ <div class="form-row" id="knx-ai-prompt-context-row" style="display:none;">
1356
+ <label style="width:290px" for="node-input-llmPromptContextTokens"><i class="fa fa-database"></i> <span data-i18n="knxUltimateAI.properties.llmPromptContextTokens"></span></label>
1357
+ <select style="width:100%" id="node-input-llmPromptContextTokens">
1358
+ <option value="4096" data-i18n="knxUltimateAI.selectlists.promptContext.small"></option>
1359
+ <option value="8192" data-i18n="knxUltimateAI.selectlists.promptContext.medium"></option>
1360
+ <option value="16384" data-i18n="knxUltimateAI.selectlists.promptContext.full"></option>
1361
+ </select>
1362
+ <div class="knx-ai-prompt-context-hint" data-i18n="knxUltimateAI.messages.promptContextHint"></div>
1363
+ </div>
1237
1364
  <div class="form-row">
1238
1365
  <input type="checkbox" id="node-input-llmAllowKnxCommands" style="display:inline-block; width:auto; vertical-align:top;">
1239
1366
  <label style="width:auto" for="node-input-llmAllowKnxCommands">&nbsp;&nbsp;<span data-i18n="knxUltimateAI.properties.llmAllowKnxCommands"></span></label>