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

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.
@@ -168,6 +168,7 @@
168
168
  display: block;
169
169
  }
170
170
 
171
+ #knx-ai-setup-doctor-panel,
171
172
  #knx-ai-detected-adapters-panel,
172
173
  #knx-ai-chat-context-panel,
173
174
  #knx-ai-chat-learning-link-panel {
@@ -186,6 +187,7 @@
186
187
  margin: 18px 0 0;
187
188
  }
188
189
 
190
+ #knx-ai-setup-doctor-panel>h4,
189
191
  #knx-ai-detected-adapters-panel>h4,
190
192
  #knx-ai-chat-context-panel>h4,
191
193
  #knx-ai-chat-learning-link-panel>h4 {
@@ -194,6 +196,148 @@
194
196
  font-weight: 600;
195
197
  }
196
198
 
199
+ #knx-ai-setup-doctor-panel {
200
+ margin: 16px 0 18px;
201
+ border-color: rgba(255, 152, 0, 0.55);
202
+ background: linear-gradient(135deg, rgba(255, 152, 0, 0.10), var(--red-ui-secondary-background, #f7f7f7));
203
+ }
204
+
205
+ #knx-ai-setup-doctor-panel.status-ready {
206
+ border-color: rgba(46, 125, 50, 0.55);
207
+ background: linear-gradient(135deg, rgba(46, 125, 50, 0.10), var(--red-ui-secondary-background, #f7f7f7));
208
+ }
209
+
210
+ #knx-ai-setup-doctor-panel.status-blocked {
211
+ border-color: rgba(198, 40, 40, 0.52);
212
+ background: linear-gradient(135deg, rgba(198, 40, 40, 0.09), var(--red-ui-secondary-background, #f7f7f7));
213
+ }
214
+
215
+ .knx-ai-setup-doctor-hero {
216
+ display: flex;
217
+ align-items: flex-start;
218
+ gap: 10px;
219
+ }
220
+
221
+ .knx-ai-setup-doctor-summary {
222
+ flex: 1 1 auto;
223
+ min-width: 0;
224
+ line-height: 1.45;
225
+ }
226
+
227
+ .knx-ai-setup-doctor-score {
228
+ flex: 0 0 auto;
229
+ min-width: 48px;
230
+ padding: 6px 8px;
231
+ border-radius: 16px;
232
+ color: #7a4200;
233
+ background: rgba(255, 152, 0, 0.18);
234
+ text-align: center;
235
+ font-size: 12px;
236
+ font-weight: 700;
237
+ }
238
+
239
+ #knx-ai-setup-doctor-panel.status-ready .knx-ai-setup-doctor-score {
240
+ color: #256029;
241
+ background: rgba(46, 125, 50, 0.16);
242
+ }
243
+
244
+ #knx-ai-setup-doctor-panel.status-blocked .knx-ai-setup-doctor-score {
245
+ color: #842029;
246
+ background: rgba(198, 40, 40, 0.14);
247
+ }
248
+
249
+ .knx-ai-setup-doctor-check {
250
+ display: flex;
251
+ align-items: flex-start;
252
+ gap: 8px;
253
+ padding: 7px 0;
254
+ }
255
+
256
+ .knx-ai-setup-doctor-check+.knx-ai-setup-doctor-check {
257
+ border-top: 1px solid var(--red-ui-form-input-border-color, #ddd);
258
+ }
259
+
260
+ .knx-ai-setup-doctor-check-copy {
261
+ flex: 1 1 auto;
262
+ min-width: 0;
263
+ }
264
+
265
+ .knx-ai-setup-doctor-check-detail,
266
+ #knx-ai-setup-doctor-status,
267
+ .knx-ai-setup-doctor-prompts-hint {
268
+ color: var(--red-ui-secondary-text-color, #777);
269
+ font-size: 12px;
270
+ line-height: 1.4;
271
+ }
272
+
273
+ .knx-ai-setup-doctor-badge {
274
+ flex: 0 0 auto;
275
+ min-width: 48px;
276
+ padding: 2px 7px;
277
+ border-radius: 10px;
278
+ text-align: center;
279
+ font-size: 11px;
280
+ font-weight: 700;
281
+ }
282
+
283
+ .knx-ai-setup-doctor-badge.pass {
284
+ color: #256029;
285
+ background: #dff0d8;
286
+ }
287
+
288
+ .knx-ai-setup-doctor-badge.warn {
289
+ color: #7a5200;
290
+ background: #fff3cd;
291
+ }
292
+
293
+ .knx-ai-setup-doctor-badge.fail {
294
+ color: #842029;
295
+ background: #f8d7da;
296
+ }
297
+
298
+ .knx-ai-setup-doctor-badge.info {
299
+ color: #245269;
300
+ background: #d9edf7;
301
+ }
302
+
303
+ .knx-ai-setup-doctor-prompts {
304
+ margin-top: 12px;
305
+ padding-top: 10px;
306
+ border-top: 1px dashed var(--red-ui-form-input-border-color, #ccc);
307
+ }
308
+
309
+ .knx-ai-setup-doctor-prompt-list {
310
+ display: flex;
311
+ flex-direction: column;
312
+ gap: 6px;
313
+ margin-top: 8px;
314
+ }
315
+
316
+ .knx-ai-setup-doctor-prompt {
317
+ width: 100%;
318
+ margin: 0 !important;
319
+ text-align: left;
320
+ white-space: normal;
321
+ }
322
+
323
+ .knx-ai-setup-doctor-footer {
324
+ display: flex;
325
+ align-items: center;
326
+ justify-content: space-between;
327
+ gap: 10px;
328
+ margin-top: 10px;
329
+ }
330
+
331
+ .knx-ai-setup-doctor-deploy-hint {
332
+ color: var(--red-ui-secondary-text-color, #777);
333
+ font-size: 11px;
334
+ line-height: 1.35;
335
+ }
336
+
337
+ #knx-ai-setup-doctor-refresh {
338
+ flex: 0 0 auto;
339
+ }
340
+
197
341
  .knx-ai-detected-adapter {
198
342
  display: flex;
199
343
  align-items: center;
@@ -266,29 +410,6 @@
266
410
  font-weight: 600;
267
411
  }
268
412
 
269
- #knx-ai-tts-ultimate-picker {
270
- margin-top: 8px;
271
- padding-top: 12px;
272
- border-top: 1px solid var(--red-ui-form-input-border-color, #ddd);
273
- }
274
-
275
- #knx-ai-tts-ultimate-picker label {
276
- display: block;
277
- width: 100%;
278
- margin-bottom: 6px;
279
- font-weight: 600;
280
- }
281
-
282
- #node-input-ttsUltimateNodeId {
283
- width: 100%;
284
- }
285
-
286
- .knx-ai-tts-ultimate-hint {
287
- margin-top: 6px;
288
- color: var(--red-ui-secondary-text-color, #777);
289
- font-size: 12px;
290
- }
291
-
292
413
  @media (max-width: 900px) {
293
414
  #knx-ai-ollama-install-row>label {
294
415
  width: 100% !important;
@@ -320,23 +441,27 @@
320
441
  llmPromptContextTokens: { value: 16384 },
321
442
  llmAllowKnxCommands: { value: false },
322
443
  llmRequireCommandConfirmation: { value: true },
444
+ webAccessEnabled: { value: false },
445
+ webProactiveEnabled: { value: false },
446
+ webProactiveIntervalMinutes: { value: 30 },
447
+ webMaxCallsPerHour: { value: 12 },
323
448
  chatAdapterPreset: { value: "none" },
324
449
  chatInputCode: { value: "" },
325
450
  chatOutputCode: { value: "" },
326
- ttsUltimateNodeId: { value: "" },
327
451
  aiEducation: { value: "" }
328
452
  },
329
453
  credentials: {
330
454
  llmApiKey: { type: "password" }
331
455
  },
332
456
  inputs: 1,
333
- outputs: 4,
457
+ outputs: 5,
334
458
  outputLabels: function (i) {
335
459
  switch (i) {
336
460
  case 0: return this._('knxUltimateAI.outputs.summary');
337
461
  case 1: return this._('knxUltimateAI.outputs.anomalies');
338
462
  case 2: return this._('knxUltimateAI.outputs.assistant');
339
463
  case 3: return this._('knxUltimateAI.outputs.knxCommands');
464
+ case 4: return this._('knxUltimateAI.outputs.ttsUltimate');
340
465
  }
341
466
  },
342
467
  icon: "node-eye-icon.svg",
@@ -550,81 +675,6 @@
550
675
  };
551
676
  configureChatAdapterPicker.call(node);
552
677
 
553
- const collectEditorTtsUltimateNodes = () => {
554
- const tabById = new Map();
555
- const entries = [];
556
- try {
557
- RED.nodes.eachNode(function (candidate) {
558
- if (!candidate || String(candidate.type || "") !== "tab") return;
559
- tabById.set(String(candidate.id || ""), String(candidate.label || candidate.name || ""));
560
- });
561
- RED.nodes.eachNode(function (candidate) {
562
- if (!candidate || String(candidate.type || "") !== "ttsultimate") return;
563
- const id = String(candidate.id || "").trim();
564
- if (!id) return;
565
- entries.push({
566
- id: id,
567
- name: String(candidate.name || "TTS Ultimate"),
568
- flowId: String(candidate.z || ""),
569
- flowName: tabById.get(String(candidate.z || "")) || "",
570
- playerType: String(candidate.playertype || "sonos")
571
- });
572
- });
573
- } catch (error) { }
574
- return entries;
575
- };
576
-
577
- const populateTtsUltimatePicker = (runtimeSummary) => {
578
- const $pickerContainer = $("#knx-ai-tts-ultimate-picker");
579
- const $picker = $("#node-input-ttsUltimateNodeId");
580
- if (!$picker.length) return;
581
- const byId = new Map();
582
- const runtimeNodes = runtimeSummary && Array.isArray(runtimeSummary.nodes) ? runtimeSummary.nodes : [];
583
- runtimeNodes.forEach(function (item) {
584
- if (item && item.id) byId.set(String(item.id), item);
585
- });
586
- collectEditorTtsUltimateNodes().forEach(function (item) {
587
- byId.set(String(item.id), item);
588
- });
589
- const entries = Array.from(byId.values()).sort(function (left, right) {
590
- return String((left.flowName || "") + "\u0000" + (left.name || "") + "\u0000" + left.id)
591
- .localeCompare(String((right.flowName || "") + "\u0000" + (right.name || "") + "\u0000" + right.id));
592
- });
593
- const configuredId = String(node.ttsUltimateNodeId || "").trim();
594
- $picker.empty();
595
- $("<option></option>")
596
- .attr("value", "")
597
- .text(entries.length
598
- ? t("knxUltimateAI.selectlists.ttsUltimate.select", "Select a TTS Ultimate node")
599
- : t("knxUltimateAI.selectlists.ttsUltimate.none", "No TTS Ultimate node found"))
600
- .appendTo($picker);
601
- entries.forEach(function (item) {
602
- const flowName = String(item.flowName || t("knxUltimateAI.messages.ttsUltimateUnknownFlow", "Flow without name"));
603
- const playerType = String(item.playerType || "sonos");
604
- $("<option></option>")
605
- .attr("value", item.id)
606
- .text(flowName + " · " + String(item.name || "TTS Ultimate") + " · " + playerType)
607
- .appendTo($picker);
608
- });
609
- if (configuredId && !byId.has(configuredId)) {
610
- $("<option></option>")
611
- .attr("value", configuredId)
612
- .text(t("knxUltimateAI.messages.ttsUltimateUnavailable", "Unavailable TTS Ultimate node") + " · " + configuredId)
613
- .appendTo($picker);
614
- }
615
- if (configuredId) {
616
- $picker.val(configuredId);
617
- } else if (entries.length === 1) {
618
- $picker.val(entries[0].id);
619
- } else {
620
- $picker.val("");
621
- }
622
- const detected = entries.length > 0 || !!(runtimeSummary && runtimeSummary.detected);
623
- $picker.prop("disabled", entries.length === 0 && !configuredId);
624
- $pickerContainer.toggle(detected || !!configuredId);
625
- };
626
- populateTtsUltimatePicker(null);
627
-
628
678
  const renderDetectedAdapters = (adapters) => {
629
679
  const $list = $("#knx-ai-detected-adapters-list");
630
680
  const $status = $("#knx-ai-detected-adapters-status");
@@ -632,7 +682,7 @@
632
682
  $list.empty();
633
683
  const entries = Array.isArray(adapters) ? adapters : [];
634
684
  if (!entries.length) {
635
- $status.text(t("knxUltimateAI.messages.detectedAdaptersNone", "No automatic adapter detected.")).show();
685
+ $status.text(t("knxUltimateAI.messages.detectedAdaptersNone", "No compatible node detected.")).show();
636
686
  return;
637
687
  }
638
688
  $status.hide();
@@ -662,6 +712,60 @@
662
712
  });
663
713
  };
664
714
 
715
+ const renderSetupDoctor = (snapshot) => {
716
+ const $panel = $("#knx-ai-setup-doctor-panel");
717
+ const $status = $("#knx-ai-setup-doctor-status");
718
+ const $content = $("#knx-ai-setup-doctor-content");
719
+ const $checks = $("#knx-ai-setup-doctor-checks").empty();
720
+ const $prompts = $("#knx-ai-setup-doctor-prompt-list").empty();
721
+ if (!snapshot || typeof snapshot !== "object") {
722
+ $panel.removeClass("status-ready status-attention status-blocked");
723
+ $content.hide();
724
+ $status.text(t("knxUltimateAI.messages.setupDoctorUnavailable", "Setup Doctor is temporarily unavailable.")).show();
725
+ return;
726
+ }
727
+ const badgeLabels = {
728
+ pass: t("knxUltimateAI.messages.setupDoctorPass", "OK"),
729
+ warn: t("knxUltimateAI.messages.setupDoctorWarn", "Check"),
730
+ fail: t("knxUltimateAI.messages.setupDoctorFail", "Fix"),
731
+ info: t("knxUltimateAI.messages.setupDoctorInfo", "Optional")
732
+ };
733
+ const snapshotStatus = ["ready", "attention", "blocked"].includes(String(snapshot.status)) ? String(snapshot.status) : "attention";
734
+ $panel.removeClass("status-ready status-attention status-blocked").addClass("status-" + snapshotStatus);
735
+ $("#knx-ai-setup-doctor-state").text(String(snapshot.statusLabel || snapshot.status || ""));
736
+ $("#knx-ai-setup-doctor-summary").text(String(snapshot.summary || ""));
737
+ $("#knx-ai-setup-doctor-score").text(String(Math.max(0, Number(snapshot.score) || 0)) + "%");
738
+ (Array.isArray(snapshot.checks) ? snapshot.checks : []).forEach(function (check) {
739
+ if (!check || !check.id) return;
740
+ const statusName = ["pass", "warn", "fail", "info"].includes(String(check.status)) ? String(check.status) : "info";
741
+ const $row = $("<div></div>").addClass("knx-ai-setup-doctor-check");
742
+ const $copy = $("<div></div>").addClass("knx-ai-setup-doctor-check-copy");
743
+ $("<strong></strong>").text(String(check.title || check.id)).appendTo($copy);
744
+ $("<div></div>").addClass("knx-ai-setup-doctor-check-detail").text(String(check.detail || "")).appendTo($copy);
745
+ $copy.appendTo($row);
746
+ $("<span></span>")
747
+ .addClass("knx-ai-setup-doctor-badge " + statusName)
748
+ .text(badgeLabels[statusName])
749
+ .appendTo($row);
750
+ $row.appendTo($checks);
751
+ });
752
+ const promptItems = snapshot.firstRun && Array.isArray(snapshot.firstRun.prompts) ? snapshot.firstRun.prompts : [];
753
+ promptItems.forEach(function (prompt) {
754
+ if (!prompt || !prompt.text) return;
755
+ $("<button type=\"button\"></button>")
756
+ .addClass("red-ui-button knx-ai-setup-doctor-prompt")
757
+ .text(String(prompt.text))
758
+ .on("click", function (evt) {
759
+ evt.preventDefault();
760
+ openKnxAiWebPage("assistant", String(prompt.text));
761
+ })
762
+ .appendTo($prompts);
763
+ });
764
+ $(".knx-ai-setup-doctor-prompts").toggle(promptItems.length > 0);
765
+ $status.hide();
766
+ $content.show();
767
+ };
768
+
665
769
  const renderChatContextOverview = (overview) => {
666
770
  const $status = $("#knx-ai-chat-context-status");
667
771
  const $content = $("#knx-ai-chat-context-content");
@@ -749,8 +853,7 @@
749
853
  adapterHistory: t("knxUltimateAI.messages.chatContextSourceAdapterHistory", "Persistent history of automatically detected adapter events."),
750
854
  etsProject: t("knxUltimateAI.messages.chatContextSourceEtsProject", "ETS semantics and the full Node-RED project inventory."),
751
855
  memoryEducation: t("knxUltimateAI.messages.chatContextSourceMemoryEducation", "Session context, AI Education and bounded home memory."),
752
- cameras: t("knxUltimateAI.messages.chatContextSourceCameras", "Detected cameras and their available capabilities."),
753
- ttsUltimate: t("knxUltimateAI.messages.chatContextSourceTtsUltimate", "Selected TTS Ultimate announcement target.")
856
+ cameras: t("knxUltimateAI.messages.chatContextSourceCameras", "Detected cameras and their available capabilities.")
754
857
  };
755
858
  (Array.isArray(overview.sources) ? overview.sources : []).forEach(function (sourceId) {
756
859
  addRow(
@@ -802,24 +905,41 @@
802
905
  $status.hide();
803
906
  $content.show();
804
907
  };
805
- $.ajax({
806
- url: "knxUltimateAI/adapters",
807
- type: "GET",
808
- data: nodeId ? { nodeId: nodeId } : {}
809
- })
810
- .done(function (data) {
811
- renderDetectedAdapters(data && data.adapters);
812
- populateTtsUltimatePicker(data && data.ttsUltimate);
813
- renderChatContextOverview(data && data.chatContext);
908
+ const editorLanguage = String((RED.settings && RED.settings.lang) || (window.navigator && window.navigator.language) || "en");
909
+ const loadKnxAiOverview = function (refreshSetup) {
910
+ const $refresh = $("#knx-ai-setup-doctor-refresh");
911
+ $refresh.prop("disabled", true).find("i.fa-refresh").addClass("fa-spin");
912
+ const requestData = { language: editorLanguage };
913
+ if (nodeId) requestData.nodeId = nodeId;
914
+ if (refreshSetup) requestData.refreshSetup = "1";
915
+ $.ajax({
916
+ url: "knxUltimateAI/adapters",
917
+ type: "GET",
918
+ data: requestData
814
919
  })
815
- .fail(function () {
816
- $("#knx-ai-detected-adapters-status")
817
- .text(t("knxUltimateAI.messages.detectedAdaptersUnavailable", "Automatic adapter detection is temporarily unavailable."))
818
- .show();
819
- renderChatContextOverview(null);
820
- });
920
+ .done(function (data) {
921
+ renderSetupDoctor(data && data.setupDoctor);
922
+ renderDetectedAdapters(data && data.adapters);
923
+ renderChatContextOverview(data && data.chatContext);
924
+ })
925
+ .fail(function () {
926
+ renderSetupDoctor(null);
927
+ $("#knx-ai-detected-adapters-status")
928
+ .text(t("knxUltimateAI.messages.detectedAdaptersUnavailable", "Compatible-node detection is temporarily unavailable."))
929
+ .show();
930
+ renderChatContextOverview(null);
931
+ })
932
+ .always(function () {
933
+ $refresh.prop("disabled", false).find("i.fa-refresh").removeClass("fa-spin");
934
+ });
935
+ };
936
+ loadKnxAiOverview(false);
937
+ $("#knx-ai-setup-doctor-refresh").on("click", function (evt) {
938
+ evt.preventDefault();
939
+ loadKnxAiOverview(true);
940
+ });
821
941
 
822
- const openKnxAiWebPage = function (view) {
942
+ const openKnxAiWebPage = function (view, prompt) {
823
943
  const adminRoot = resolveAdminRoot();
824
944
  const targetBase = adminRoot + "/knxUltimateAI/sidebar/page";
825
945
  const params = new URLSearchParams();
@@ -827,6 +947,9 @@
827
947
  if (view === "chat-learning") {
828
948
  params.set("tab", "settings");
829
949
  params.set("settingsTab", "learning");
950
+ } else if (view === "assistant") {
951
+ params.set("tab", "assistant");
952
+ if (prompt) params.set("prompt", String(prompt));
830
953
  }
831
954
  const accessToken = resolveAccessToken();
832
955
  if (accessToken) params.set("access_token", accessToken);
@@ -1167,6 +1290,14 @@
1167
1290
  }
1168
1291
  }
1169
1292
  };
1293
+ const toggleWebAccess = () => {
1294
+ const enabled = $("#node-input-webAccessEnabled").prop("checked") === true;
1295
+ const proactive = enabled && $("#node-input-webProactiveEnabled").prop("checked") === true;
1296
+ $("#knx-ai-web-access-settings").toggle(enabled);
1297
+ $("#knx-ai-web-proactive-interval-row").toggle(proactive);
1298
+ };
1299
+ $("#node-input-webAccessEnabled, #node-input-webProactiveEnabled").on("change", toggleWebAccess);
1300
+ toggleWebAccess();
1170
1301
  $("#node-input-llmProvider").on("change", function () {
1171
1302
  toggleProvider({ applyDefaults: true, autoLoadModels: true });
1172
1303
  });
@@ -1258,6 +1389,34 @@
1258
1389
  </div>
1259
1390
  </div>
1260
1391
 
1392
+ <div id="knx-ai-setup-doctor-panel">
1393
+ <h4><i class="fa fa-magic"></i> <span data-i18n="knxUltimateAI.sections.setupDoctor"></span></h4>
1394
+ <div id="knx-ai-setup-doctor-status" data-i18n="knxUltimateAI.messages.setupDoctorLoading"></div>
1395
+ <div id="knx-ai-setup-doctor-content" style="display:none;">
1396
+ <div class="knx-ai-setup-doctor-hero">
1397
+ <div class="knx-ai-setup-doctor-summary">
1398
+ <strong id="knx-ai-setup-doctor-state"></strong>
1399
+ <div id="knx-ai-setup-doctor-summary"></div>
1400
+ </div>
1401
+ <div id="knx-ai-setup-doctor-score" class="knx-ai-setup-doctor-score"></div>
1402
+ </div>
1403
+ <div id="knx-ai-setup-doctor-checks"></div>
1404
+ <div class="knx-ai-setup-doctor-prompts">
1405
+ <strong data-i18n="knxUltimateAI.messages.setupDoctorPromptsTitle"></strong>
1406
+ <div class="knx-ai-setup-doctor-prompts-hint" data-i18n="knxUltimateAI.messages.setupDoctorPromptsHint"></div>
1407
+ <div id="knx-ai-setup-doctor-prompt-list" class="knx-ai-setup-doctor-prompt-list"></div>
1408
+ </div>
1409
+ <div class="knx-ai-setup-doctor-footer">
1410
+ <div class="knx-ai-setup-doctor-deploy-hint">
1411
+ <i class="fa fa-info-circle"></i> <span data-i18n="knxUltimateAI.messages.setupDoctorDeployHint"></span>
1412
+ </div>
1413
+ <button type="button" class="red-ui-button" id="knx-ai-setup-doctor-refresh">
1414
+ <i class="fa fa-refresh"></i> <span data-i18n="knxUltimateAI.buttons.refreshSetupDoctor"></span>
1415
+ </button>
1416
+ </div>
1417
+ </div>
1418
+ </div>
1419
+
1261
1420
  <div id="knx-ai-tabs">
1262
1421
  <ul>
1263
1422
  <li><a href="#knx-ai-tabs-assistant"><i class="fa fa-magic"></i> <span data-i18n="knxUltimateAI.sections.groupAssistant"></span></a></li>
@@ -1294,11 +1453,6 @@
1294
1453
  <h4><i class="fa fa-puzzle-piece"></i> <span data-i18n="knxUltimateAI.sections.detectedAdapters"></span></h4>
1295
1454
  <div id="knx-ai-detected-adapters-status" data-i18n="knxUltimateAI.messages.detectedAdaptersLoading"></div>
1296
1455
  <div id="knx-ai-detected-adapters-list"></div>
1297
- <div id="knx-ai-tts-ultimate-picker" style="display:none;">
1298
- <label for="node-input-ttsUltimateNodeId"><i class="fa fa-volume-up"></i> <span data-i18n="knxUltimateAI.properties.ttsUltimateNodeId"></span></label>
1299
- <select id="node-input-ttsUltimateNodeId"></select>
1300
- <div class="knx-ai-tts-ultimate-hint" data-i18n="knxUltimateAI.messages.ttsUltimateHint"></div>
1301
- </div>
1302
1456
  </div>
1303
1457
  <div id="knx-ai-chat-context-panel">
1304
1458
  <h4><i class="fa fa-database"></i> <span data-i18n="knxUltimateAI.sections.chatContextOverview"></span></h4>
@@ -1394,6 +1548,38 @@
1394
1548
  </div>
1395
1549
 
1396
1550
  <div id="knx-ai-tab-home-intelligence">
1551
+ <div class="form-row">
1552
+ <strong><i class="fa fa-globe"></i> <span data-i18n="knxUltimateAI.sections.webIntelligence"></span></strong>
1553
+ </div>
1554
+ <div class="form-row">
1555
+ <input type="checkbox" id="node-input-webAccessEnabled" style="display:inline-block; width:auto; vertical-align:top;">
1556
+ <label style="width:auto" for="node-input-webAccessEnabled">&nbsp;&nbsp;<span data-i18n="knxUltimateAI.properties.webAccessEnabled"></span></label>
1557
+ <div class="knx-ai-prompt-context-hint" data-i18n="knxUltimateAI.messages.webAccessHint"></div>
1558
+ </div>
1559
+ <div id="knx-ai-web-access-settings">
1560
+ <div class="form-row">
1561
+ <label style="width:290px" for="node-input-webMaxCallsPerHour"><i class="fa fa-tachometer"></i> <span data-i18n="knxUltimateAI.properties.webMaxCallsPerHour"></span></label>
1562
+ <input type="number" id="node-input-webMaxCallsPerHour" min="1" max="60" step="1" style="width:100%;">
1563
+ <div class="knx-ai-prompt-context-hint" data-i18n="knxUltimateAI.messages.webBudgetHint"></div>
1564
+ </div>
1565
+ <div class="form-row">
1566
+ <input type="checkbox" id="node-input-webProactiveEnabled" style="display:inline-block; width:auto; vertical-align:top;">
1567
+ <label style="width:auto" for="node-input-webProactiveEnabled">&nbsp;&nbsp;<span data-i18n="knxUltimateAI.properties.webProactiveEnabled"></span></label>
1568
+ <div class="knx-ai-prompt-context-hint" data-i18n="knxUltimateAI.messages.webProactiveHint"></div>
1569
+ </div>
1570
+ <div class="form-row" id="knx-ai-web-proactive-interval-row">
1571
+ <label style="width:290px" for="node-input-webProactiveIntervalMinutes"><i class="fa fa-clock-o"></i> <span data-i18n="knxUltimateAI.properties.webProactiveIntervalMinutes"></span></label>
1572
+ <select style="width:100%" id="node-input-webProactiveIntervalMinutes">
1573
+ <option value="5" data-i18n="knxUltimateAI.selectlists.webProactiveInterval.5"></option>
1574
+ <option value="10" data-i18n="knxUltimateAI.selectlists.webProactiveInterval.10"></option>
1575
+ <option value="15" data-i18n="knxUltimateAI.selectlists.webProactiveInterval.15"></option>
1576
+ <option value="30" data-i18n="knxUltimateAI.selectlists.webProactiveInterval.30"></option>
1577
+ <option value="60" data-i18n="knxUltimateAI.selectlists.webProactiveInterval.60"></option>
1578
+ <option value="180" data-i18n="knxUltimateAI.selectlists.webProactiveInterval.180"></option>
1579
+ </select>
1580
+ </div>
1581
+ </div>
1582
+ <hr>
1397
1583
  <div class="form-row">
1398
1584
  <label style="width:100%" for="node-input-aiEducation"><i class="fa fa-graduation-cap"></i> <span data-i18n="knxUltimateAI.properties.aiEducation"></span></label>
1399
1585
  <textarea id="node-input-aiEducation" maxlength="16000" style="width:100%; min-height:180px; box-sizing:border-box;" data-i18n="[placeholder]knxUltimateAI.placeholder.aiEducation"></textarea>