node-red-contrib-knx-ultimate 6.3.16 → 6.3.18

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.
@@ -46,60 +46,99 @@
46
46
  background-color: #fffde7 !important;
47
47
  }
48
48
 
49
- #knx-ai-accordion {
50
- margin-top: 12px;
49
+ #knx-ai-tabs.ui-tabs {
50
+ background: transparent;
51
+ border: none;
51
52
  }
52
53
 
53
- #knx-ai-accordion>h3 {
54
- margin-top: 6px;
55
- font-size: 14px;
56
- font-weight: 600;
54
+ #knx-ai-tabs>.ui-tabs-nav {
55
+ display: flex;
56
+ flex-wrap: wrap;
57
+ align-items: flex-end;
58
+ margin: 0;
59
+ padding: 0;
60
+ background: transparent;
61
+ border: none;
62
+ border-bottom: 1px solid #c0c0c0;
63
+ border-radius: 0;
64
+ }
65
+
66
+ #knx-ai-tabs>.ui-tabs-nav>li.ui-state-default {
67
+ float: none;
68
+ margin: 0 2px -1px 0;
69
+ background: transparent;
70
+ border: none;
71
+ }
72
+
73
+ #knx-ai-tabs>.ui-tabs-nav>li.ui-state-default>a {
74
+ padding: 0.5em 1em;
75
+ color: #878787;
76
+ outline: none;
77
+ }
78
+
79
+ #knx-ai-tabs>.ui-tabs-nav>li.ui-state-active {
80
+ background: transparent no-repeat bottom center;
81
+ border: none;
82
+ }
83
+
84
+ #knx-ai-tabs>.ui-tabs-nav>li.ui-state-active>a {
85
+ color: #377e00;
86
+ border-bottom: 1px solid #377e00;
87
+ outline: none;
88
+ }
89
+
90
+ #knx-ai-tabs>.ui-tabs-nav>li>a>i {
91
+ color: inherit;
57
92
  }
58
93
 
59
- #knx-ai-accordion>.ui-accordion-content {
60
- padding: 14px 16px;
94
+ #knx-ai-tabs>.ui-tabs-panel {
95
+ padding: 1em 1.4em;
61
96
  overflow: visible;
97
+ background: transparent;
98
+ border: none;
99
+ border-radius: 0;
62
100
  }
63
101
 
64
- #knx-ai-accordion .knx-ai-accordion-subsection {
102
+ #knx-ai-tabs .knx-ai-accordion-subsection {
65
103
  padding: 0;
66
104
  }
67
105
 
68
- #knx-ai-accordion .knx-ai-accordion-subsection+.knx-ai-accordion-subsection {
106
+ #knx-ai-tabs .knx-ai-accordion-subsection+.knx-ai-accordion-subsection {
69
107
  margin-top: 18px;
70
108
  padding-top: 16px;
71
109
  border-top: 1px solid var(--red-ui-form-input-border-color, #ccc);
72
110
  }
73
111
 
74
- #knx-ai-accordion .knx-ai-accordion-subsection>h4 {
112
+ #knx-ai-tabs .knx-ai-accordion-subsection>h4 {
75
113
  margin: 0 0 14px;
76
114
  color: var(--red-ui-primary-text-color, #555);
77
115
  font-size: 14px;
78
116
  font-weight: 600;
79
117
  }
80
118
 
81
- #knx-ai-accordion .knx-ai-accordion-subsection>h4 i {
119
+ #knx-ai-tabs .knx-ai-accordion-subsection>h4 i {
82
120
  width: 18px;
83
121
  text-align: center;
84
122
  }
85
123
 
86
- #knx-ai-accordion .knx-ai-inline-subsection {
124
+ #knx-ai-tabs .knx-ai-inline-subsection {
87
125
  margin-top: 18px;
88
126
  padding-top: 14px;
89
127
  border-top: 1px dashed var(--red-ui-form-input-border-color, #ccc);
90
128
  }
91
129
 
92
- #knx-ai-accordion .knx-ai-inline-subsection>h5 {
130
+ #knx-ai-tabs .knx-ai-inline-subsection>h5 {
93
131
  margin: 0 0 12px;
94
132
  font-size: 13px;
95
133
  font-weight: 600;
96
134
  }
97
135
 
98
- #knx-ai-accordion .knx-ai-accordion-mount>div {
136
+ #knx-ai-tabs .knx-ai-accordion-mount>div {
99
137
  display: block;
100
138
  }
101
139
 
102
- #knx-ai-detected-adapters-panel {
140
+ #knx-ai-detected-adapters-panel,
141
+ #knx-ai-chat-context-panel {
103
142
  margin: 0 0 18px;
104
143
  padding: 12px 14px;
105
144
  border: 1px solid var(--red-ui-form-input-border-color, #ccc);
@@ -107,7 +146,16 @@
107
146
  background: var(--red-ui-secondary-background, #f7f7f7);
108
147
  }
109
148
 
110
- #knx-ai-detected-adapters-panel>h4 {
149
+ #knx-ai-tabs-chat-home>#knx-ai-detected-adapters-panel {
150
+ margin: 18px 0 0;
151
+ }
152
+
153
+ #knx-ai-tabs-chat-home>#knx-ai-detected-adapters-panel+#knx-ai-chat-context-panel {
154
+ margin: 18px 0 0;
155
+ }
156
+
157
+ #knx-ai-detected-adapters-panel>h4,
158
+ #knx-ai-chat-context-panel>h4 {
111
159
  margin: 0 0 9px;
112
160
  font-size: 14px;
113
161
  font-weight: 600;
@@ -131,11 +179,33 @@
131
179
 
132
180
  .knx-ai-detected-adapter-package,
133
181
  .knx-ai-detected-adapter-counts,
134
- #knx-ai-detected-adapters-status {
182
+ #knx-ai-detected-adapters-status,
183
+ #knx-ai-chat-context-status,
184
+ .knx-ai-chat-context-intro,
185
+ .knx-ai-chat-context-description,
186
+ .knx-ai-chat-context-pattern {
135
187
  color: var(--red-ui-secondary-text-color, #777);
136
188
  font-size: 12px;
137
189
  }
138
190
 
191
+ .knx-ai-chat-context-group-title {
192
+ margin: 12px 0 2px;
193
+ font-size: 12px;
194
+ font-weight: 600;
195
+ }
196
+
197
+ .knx-ai-chat-context-path {
198
+ margin-top: 2px;
199
+ color: var(--red-ui-secondary-text-color, #777);
200
+ font-family: monospace;
201
+ font-size: 11px;
202
+ overflow-wrap: anywhere;
203
+ }
204
+
205
+ .knx-ai-chat-context-pattern {
206
+ margin-top: 8px;
207
+ }
208
+
139
209
  .knx-ai-detected-adapter-badge {
140
210
  flex: 0 0 auto;
141
211
  padding: 2px 7px;
@@ -146,6 +216,29 @@
146
216
  font-weight: 600;
147
217
  }
148
218
 
219
+ #knx-ai-tts-ultimate-picker {
220
+ margin-top: 8px;
221
+ padding-top: 12px;
222
+ border-top: 1px solid var(--red-ui-form-input-border-color, #ddd);
223
+ }
224
+
225
+ #knx-ai-tts-ultimate-picker label {
226
+ display: block;
227
+ width: 100%;
228
+ margin-bottom: 6px;
229
+ font-weight: 600;
230
+ }
231
+
232
+ #node-input-ttsUltimateNodeId {
233
+ width: 100%;
234
+ }
235
+
236
+ .knx-ai-tts-ultimate-hint {
237
+ margin-top: 6px;
238
+ color: var(--red-ui-secondary-text-color, #777);
239
+ font-size: 12px;
240
+ }
241
+
149
242
  @media (max-width: 900px) {
150
243
  #knx-ai-ollama-install-row>label {
151
244
  width: 100% !important;
@@ -169,31 +262,17 @@
169
262
  name: { value: "KNX AI", required: false },
170
263
  topic: { value: "", required: false },
171
264
 
172
- analysisWindowSec: { value: 120 },
173
- historyWindowSec: { value: 600 },
174
- historyStoreToDisk: { value: true },
175
- historyStoreRetentionDays: { value: 10 },
176
- emitIntervalSec: { value: 0 },
177
- topN: { value: 10 },
178
- maxEvents: { value: 50000 },
179
-
180
265
  llmEnabled: { value: false },
181
266
  llmProvider: { value: "openai_compat" },
182
267
  llmBaseUrl: { value: "https://api.openai.com/v1/chat/completions" },
183
268
  llmModel: { value: "gpt-5.4" },
184
- llmIncludeFlowContext: { value: true },
185
- llmDocsLanguage: { value: "en" },
269
+ llmContextLength: { value: 0 },
186
270
  llmAllowKnxCommands: { value: false },
187
271
  llmRequireCommandConfirmation: { value: true },
188
272
  chatAdapterPreset: { value: "none" },
189
273
  chatInputCode: { value: "" },
190
274
  chatOutputCode: { value: "" },
191
- proactiveEnabled: { value: false },
192
- proactiveRecipient: { value: "" },
193
- proactiveOpenMinutes: { value: 120 },
194
- proactiveCooldownMinutes: { value: 360 },
195
- proactiveQuietStart: { value: "23:00" },
196
- proactiveQuietEnd: { value: "07:00" },
275
+ ttsUltimateNodeId: { value: "" },
197
276
  aiEducation: { value: "" }
198
277
  },
199
278
  credentials: {
@@ -220,43 +299,30 @@
220
299
  const OPENAI_COMPAT_DEFAULT_CHAT_URL = "https://api.openai.com/v1/chat/completions";
221
300
  const OLLAMA_DEFAULT_CHAT_URL = "http://localhost:11434/api/chat";
222
301
  const ANTHROPIC_DEFAULT_CHAT_URL = "https://api.anthropic.com/v1/messages";
302
+ const LMSTUDIO_DEFAULT_CHAT_URL = "http://localhost:1234/v1/chat/completions";
223
303
  const OLLAMA_LIBRARY_URL = "https://ollama.com/library";
224
304
  const OPENAI_COMPAT_DEFAULT_MODELS = ["gpt-5.4", "gpt-4o-mini"];
225
305
  const OLLAMA_DEFAULT_MODEL = "llama3.1";
226
306
  const ANTHROPIC_DEFAULT_MODEL = "claude-opus-4-8";
227
307
 
228
- const mountAccordionSection = (mountSelector, sectionSelector) => {
308
+ const mountTabSection = (mountSelector, sectionSelector) => {
229
309
  const $mount = $(mountSelector);
230
310
  const $section = $(sectionSelector);
231
311
  if ($mount.length && $section.length) $mount.append($section);
232
312
  };
233
- mountAccordionSection("#knx-ai-mount-assistant-setup", "#knx-ai-tab-llm-connection");
234
- mountAccordionSection("#knx-ai-mount-assistant-context", "#knx-ai-tab-llm-context");
235
- mountAccordionSection("#knx-ai-mount-assistant-advanced", "#knx-ai-tab-advanced");
236
- mountAccordionSection("#knx-ai-mount-chat-adapter", "#knx-ai-tab-chat-adapter");
237
- mountAccordionSection("#knx-ai-mount-home-intelligence", "#knx-ai-tab-home-intelligence");
238
- mountAccordionSection("#knx-ai-mount-knx-storage", "#knx-ai-tab-storage");
239
- $("#knx-ai-accordion").accordion({
240
- active: 0,
241
- animate: false,
242
- collapsible: true,
243
- heightStyle: "content"
244
- });
313
+ mountTabSection("#knx-ai-mount-assistant-setup", "#knx-ai-tab-llm-connection");
314
+ mountTabSection("#knx-ai-mount-assistant-advanced", "#knx-ai-tab-advanced");
315
+ mountTabSection("#knx-ai-mount-chat-adapter", "#knx-ai-tab-chat-adapter");
316
+ mountTabSection("#knx-ai-mount-home-intelligence", "#knx-ai-tab-home-intelligence");
317
+ $("#knx-ai-tabs").tabs({ active: 0 });
245
318
 
246
319
  const toggleLLM = () => {
247
320
  const enabled = $("#node-input-llmEnabled").is(":checked");
248
321
  $("#knx-ai-llm-connection-settings").toggle(enabled);
249
- $("#knx-ai-llm-context-settings").toggle(enabled);
250
322
  $("#knx-ai-llm-advanced-settings").toggle(enabled);
251
323
  };
252
324
  $("#node-input-llmEnabled").on("change", toggleLLM);
253
325
 
254
- const toggleHistoryArchive = () => {
255
- const enabled = $("#node-input-historyStoreToDisk").is(":checked");
256
- $("#knx-ai-history-retention-row").toggle(enabled);
257
- };
258
- $("#node-input-historyStoreToDisk").on("change", toggleHistoryArchive);
259
-
260
326
  const toggleCommandConfirmation = () => {
261
327
  const enabled = $("#node-input-llmAllowKnxCommands").is(":checked");
262
328
  $("#knx-ai-command-confirmation-row").toggle(enabled);
@@ -264,26 +330,8 @@
264
330
  $("#node-input-llmAllowKnxCommands").on("change", toggleCommandConfirmation);
265
331
 
266
332
  toggleLLM();
267
- toggleHistoryArchive();
268
333
  toggleCommandConfirmation();
269
334
 
270
- const applyNumericFallback = (selector, fallback) => {
271
- const $field = $(selector);
272
- if (!$field.length) return;
273
- const value = $field.val();
274
- if (value === undefined || value === null || String(value).trim() === "") {
275
- $field.val(String(fallback));
276
- }
277
- };
278
- applyNumericFallback("#node-input-analysisWindowSec", 120);
279
- applyNumericFallback("#node-input-historyWindowSec", 600);
280
- applyNumericFallback("#node-input-historyStoreRetentionDays", 10);
281
- applyNumericFallback("#node-input-emitIntervalSec", 0);
282
- applyNumericFallback("#node-input-topN", 10);
283
- applyNumericFallback("#node-input-maxEvents", 50000);
284
- applyNumericFallback("#node-input-proactiveOpenMinutes", 120);
285
- applyNumericFallback("#node-input-proactiveCooldownMinutes", 360);
286
-
287
335
  const nodeId = node.id;
288
336
  const resolveAdminRoot = () => {
289
337
  const raw = (RED.settings && typeof RED.settings.httpAdminRoot === "string") ? RED.settings.httpAdminRoot : "/";
@@ -402,18 +450,8 @@
402
450
  : [];
403
451
  const $picker = $("#node-input-chatAdapterPreset");
404
452
  if (!$picker.length) return;
405
- const updateAdapterFieldsVisibility = (preset) => {
406
- const visible = String(preset || "none") !== "none";
407
- $("#knx-ai-chat-adapter-fields").toggle(visible);
408
- if (visible) {
409
- const resizeEditors = function () {
410
- try {
411
- if (node.chatInputCodeEditor && typeof node.chatInputCodeEditor.resize === "function") node.chatInputCodeEditor.resize();
412
- if (node.chatOutputCodeEditor && typeof node.chatOutputCodeEditor.resize === "function") node.chatOutputCodeEditor.resize();
413
- } catch (error) { }
414
- };
415
- try { setTimeout(resizeEditors, 0); } catch (error) { resizeEditors(); }
416
- }
453
+ const hideAdapterMappingFields = () => {
454
+ $("#knx-ai-chat-adapter-fields").hide();
417
455
  };
418
456
  $picker.empty();
419
457
  $("<option></option>")
@@ -436,24 +474,99 @@
436
474
  .appendTo($picker);
437
475
  }
438
476
  $picker.val(configuredPreset);
439
- updateAdapterFieldsVisibility(configuredPreset);
477
+ hideAdapterMappingFields();
440
478
  $picker.on("change", function () {
441
479
  const selected = String($(this).val() || "none");
442
480
  if (selected === "none") {
443
481
  setChatEditorValue(node.chatInputCodeEditor, "");
444
482
  setChatEditorValue(node.chatOutputCodeEditor, "");
445
- updateAdapterFieldsVisibility(selected);
483
+ hideAdapterMappingFields();
446
484
  return;
447
485
  }
448
486
  const adapter = adapters.find(item => item && item.id === selected);
449
487
  if (!adapter) return;
450
488
  setChatEditorValue(node.chatInputCodeEditor, adapter.inputCode || "");
451
489
  setChatEditorValue(node.chatOutputCodeEditor, adapter.outputCode || "");
452
- updateAdapterFieldsVisibility(selected);
490
+ hideAdapterMappingFields();
453
491
  });
454
492
  };
455
493
  configureChatAdapterPicker.call(node);
456
494
 
495
+ const collectEditorTtsUltimateNodes = () => {
496
+ const tabById = new Map();
497
+ const entries = [];
498
+ try {
499
+ RED.nodes.eachNode(function (candidate) {
500
+ if (!candidate || String(candidate.type || "") !== "tab") return;
501
+ tabById.set(String(candidate.id || ""), String(candidate.label || candidate.name || ""));
502
+ });
503
+ RED.nodes.eachNode(function (candidate) {
504
+ if (!candidate || String(candidate.type || "") !== "ttsultimate") return;
505
+ const id = String(candidate.id || "").trim();
506
+ if (!id) return;
507
+ entries.push({
508
+ id: id,
509
+ name: String(candidate.name || "TTS Ultimate"),
510
+ flowId: String(candidate.z || ""),
511
+ flowName: tabById.get(String(candidate.z || "")) || "",
512
+ playerType: String(candidate.playertype || "sonos")
513
+ });
514
+ });
515
+ } catch (error) { }
516
+ return entries;
517
+ };
518
+
519
+ const populateTtsUltimatePicker = (runtimeSummary) => {
520
+ const $pickerContainer = $("#knx-ai-tts-ultimate-picker");
521
+ const $picker = $("#node-input-ttsUltimateNodeId");
522
+ if (!$picker.length) return;
523
+ const byId = new Map();
524
+ const runtimeNodes = runtimeSummary && Array.isArray(runtimeSummary.nodes) ? runtimeSummary.nodes : [];
525
+ runtimeNodes.forEach(function (item) {
526
+ if (item && item.id) byId.set(String(item.id), item);
527
+ });
528
+ collectEditorTtsUltimateNodes().forEach(function (item) {
529
+ byId.set(String(item.id), item);
530
+ });
531
+ const entries = Array.from(byId.values()).sort(function (left, right) {
532
+ return String((left.flowName || "") + "\u0000" + (left.name || "") + "\u0000" + left.id)
533
+ .localeCompare(String((right.flowName || "") + "\u0000" + (right.name || "") + "\u0000" + right.id));
534
+ });
535
+ const configuredId = String(node.ttsUltimateNodeId || "").trim();
536
+ $picker.empty();
537
+ $("<option></option>")
538
+ .attr("value", "")
539
+ .text(entries.length
540
+ ? t("knxUltimateAI.selectlists.ttsUltimate.select", "Select a TTS Ultimate node")
541
+ : t("knxUltimateAI.selectlists.ttsUltimate.none", "No TTS Ultimate node found"))
542
+ .appendTo($picker);
543
+ entries.forEach(function (item) {
544
+ const flowName = String(item.flowName || t("knxUltimateAI.messages.ttsUltimateUnknownFlow", "Flow without name"));
545
+ const playerType = String(item.playerType || "sonos");
546
+ $("<option></option>")
547
+ .attr("value", item.id)
548
+ .text(flowName + " · " + String(item.name || "TTS Ultimate") + " · " + playerType)
549
+ .appendTo($picker);
550
+ });
551
+ if (configuredId && !byId.has(configuredId)) {
552
+ $("<option></option>")
553
+ .attr("value", configuredId)
554
+ .text(t("knxUltimateAI.messages.ttsUltimateUnavailable", "Unavailable TTS Ultimate node") + " · " + configuredId)
555
+ .appendTo($picker);
556
+ }
557
+ if (configuredId) {
558
+ $picker.val(configuredId);
559
+ } else if (entries.length === 1) {
560
+ $picker.val(entries[0].id);
561
+ } else {
562
+ $picker.val("");
563
+ }
564
+ const detected = entries.length > 0 || !!(runtimeSummary && runtimeSummary.detected);
565
+ $picker.prop("disabled", entries.length === 0 && !configuredId);
566
+ $pickerContainer.toggle(detected || !!configuredId);
567
+ };
568
+ populateTtsUltimatePicker(null);
569
+
457
570
  const renderDetectedAdapters = (adapters) => {
458
571
  const $list = $("#knx-ai-detected-adapters-list");
459
572
  const $status = $("#knx-ai-detected-adapters-status");
@@ -469,6 +582,7 @@
469
582
  if (!adapter || !adapter.id) return;
470
583
  const providerCount = Number(adapter.providerCount) || 0;
471
584
  const cameraCount = Number(adapter.cameraCount) || 0;
585
+ const nodeCount = Number(adapter.nodeCount) || 0;
472
586
  const $row = $("<div></div>").addClass("knx-ai-detected-adapter");
473
587
  const $main = $("<div></div>").addClass("knx-ai-detected-adapter-main");
474
588
  $("<div></div>").text(adapter.title || adapter.id).appendTo($main);
@@ -477,7 +591,9 @@
477
591
  }
478
592
  $("<div></div>")
479
593
  .addClass("knx-ai-detected-adapter-counts")
480
- .text(t("knxUltimateAI.messages.detectedAdapterControllers", "Controllers") + ": " + providerCount + " · " + t("knxUltimateAI.messages.detectedAdapterCameras", "Cameras") + ": " + cameraCount)
594
+ .text(adapter.kind === "tts"
595
+ ? t("knxUltimateAI.messages.detectedAdapterNodes", "Nodes") + ": " + nodeCount
596
+ : t("knxUltimateAI.messages.detectedAdapterControllers", "Controllers") + ": " + providerCount + " · " + t("knxUltimateAI.messages.detectedAdapterCameras", "Cameras") + ": " + cameraCount)
481
597
  .appendTo($main);
482
598
  $main.appendTo($row);
483
599
  $("<span></span>")
@@ -487,6 +603,89 @@
487
603
  $row.appendTo($list);
488
604
  });
489
605
  };
606
+
607
+ const renderChatContextOverview = (overview) => {
608
+ const $status = $("#knx-ai-chat-context-status");
609
+ const $content = $("#knx-ai-chat-context-content");
610
+ const $sources = $("#knx-ai-chat-context-sources-list").empty();
611
+ const $files = $("#knx-ai-chat-context-files-list").empty();
612
+ const $directories = $("#knx-ai-chat-context-directories-list").empty();
613
+ if (!overview || typeof overview !== "object") {
614
+ $content.hide();
615
+ $status
616
+ .text(t("knxUltimateAI.messages.chatContextUnavailable", "The chat context summary is temporarily unavailable."))
617
+ .show();
618
+ return;
619
+ }
620
+
621
+ const addRow = function ($list, title, description, pathText, badge) {
622
+ const $row = $("<div></div>").addClass("knx-ai-detected-adapter");
623
+ const $main = $("<div></div>").addClass("knx-ai-detected-adapter-main");
624
+ $("<div></div>").text(title || "").appendTo($main);
625
+ if (description) {
626
+ $("<div></div>").addClass("knx-ai-chat-context-description").text(description).appendTo($main);
627
+ }
628
+ if (pathText) {
629
+ $("<div></div>").addClass("knx-ai-chat-context-path").text(pathText).appendTo($main);
630
+ }
631
+ $main.appendTo($row);
632
+ $("<span></span>").addClass("knx-ai-detected-adapter-badge").text(badge).appendTo($row);
633
+ $row.appendTo($list);
634
+ };
635
+
636
+ const sourceLabels = {
637
+ knxTraffic: t("knxUltimateAI.messages.chatContextSourceKnxTraffic", "Live KNX summary, anomalies, topology and selected telegrams."),
638
+ etsProject: t("knxUltimateAI.messages.chatContextSourceEtsProject", "ETS semantics and the full Node-RED project inventory."),
639
+ memoryEducation: t("knxUltimateAI.messages.chatContextSourceMemoryEducation", "Session context, AI Education and bounded home memory."),
640
+ camerasDocs: t("knxUltimateAI.messages.chatContextSourceCamerasDocs", "Detected cameras and relevant help, README and example snippets."),
641
+ ttsUltimate: t("knxUltimateAI.messages.chatContextSourceTtsUltimate", "Selected TTS Ultimate announcement target.")
642
+ };
643
+ (Array.isArray(overview.sources) ? overview.sources : []).forEach(function (sourceId) {
644
+ addRow(
645
+ $sources,
646
+ sourceLabels[sourceId] || String(sourceId),
647
+ "",
648
+ "",
649
+ t("knxUltimateAI.messages.chatContextSourceBadge", "Source")
650
+ );
651
+ });
652
+
653
+ const fileDescriptions = {
654
+ chatContext: t("knxUltimateAI.messages.chatContextFileChatContext", "Persistent conversation turns, instructions and camera notification rules."),
655
+ homeMemory: t("knxUltimateAI.messages.chatContextFileHomeMemory", "AI Education and bounded learned home memory."),
656
+ assistantConfig: t("knxUltimateAI.messages.chatContextFileAssistantConfig", "Persistent web Assistant configuration and semantic areas for this node.")
657
+ };
658
+ (Array.isArray(overview.files) ? overview.files : []).forEach(function (item) {
659
+ if (!item || !item.name || !item.path) return;
660
+ addRow(
661
+ $files,
662
+ item.name,
663
+ fileDescriptions[item.id] || "",
664
+ item.path,
665
+ t("knxUltimateAI.messages.chatContextFileBadge", "File")
666
+ );
667
+ });
668
+
669
+ const directoryLabels = {
670
+ archiveRoot: t("knxUltimateAI.messages.chatContextDirectoryRoot", "Telegram archive root"),
671
+ nodeArchive: t("knxUltimateAI.messages.chatContextDirectoryNode", "This node's telegram archive")
672
+ };
673
+ (Array.isArray(overview.telegramDirectories) ? overview.telegramDirectories : []).forEach(function (item) {
674
+ if (!item || !item.path) return;
675
+ addRow(
676
+ $directories,
677
+ directoryLabels[item.id] || String(item.id || ""),
678
+ "",
679
+ item.path,
680
+ t("knxUltimateAI.messages.chatContextDirectoryBadge", "KNX")
681
+ );
682
+ });
683
+
684
+ $("#knx-ai-chat-context-pattern")
685
+ .text(t("knxUltimateAI.messages.chatContextTelegramPattern", "Daily files") + ": " + String(overview.telegramFilePattern || "YYYY-MM-DD.jsonl"));
686
+ $status.hide();
687
+ $content.show();
688
+ };
490
689
  $.ajax({
491
690
  url: "knxUltimateAI/adapters",
492
691
  type: "GET",
@@ -494,20 +693,16 @@
494
693
  })
495
694
  .done(function (data) {
496
695
  renderDetectedAdapters(data && data.adapters);
696
+ populateTtsUltimatePicker(data && data.ttsUltimate);
697
+ renderChatContextOverview(data && data.chatContext);
497
698
  })
498
699
  .fail(function () {
499
700
  $("#knx-ai-detected-adapters-status")
500
701
  .text(t("knxUltimateAI.messages.detectedAdaptersUnavailable", "Automatic adapter detection is temporarily unavailable."))
501
702
  .show();
703
+ renderChatContextOverview(null);
502
704
  });
503
705
 
504
- const updateProactiveVisibility = () => {
505
- const enabled = $("#node-input-proactiveEnabled").is(":checked");
506
- $("#knx-ai-proactive-settings").toggle(enabled);
507
- };
508
- $("#node-input-proactiveEnabled").on("change", updateProactiveVisibility);
509
- updateProactiveVisibility();
510
-
511
706
  $("#knx-ai-open-web-page-vue").on("click", function (evt) {
512
707
  evt.preventDefault();
513
708
  const adminRoot = resolveAdminRoot();
@@ -532,12 +727,55 @@
532
727
  $("#knx-ai-downloadOllamaModel").prop("disabled", !!busy);
533
728
  };
534
729
 
535
- const populateModels = (models) => {
730
+ let localModelDetailsById = new Map();
731
+ let localContextRequestVersion = 0;
732
+
733
+ const formatContextTokens = (value) => {
734
+ const tokens = Math.max(0, Number(value) || 0);
735
+ return tokens ? tokens.toLocaleString() : "?";
736
+ };
737
+
738
+ const setLocalModelContextStatus = (detail, state, errorText) => {
739
+ const $status = $("#knx-ai-local-context-status");
740
+ const provider = $("#node-input-llmProvider").val();
741
+ if (!$status.length || (provider !== "lmstudio" && provider !== "ollama") || !detail) {
742
+ $status.hide();
743
+ return;
744
+ }
745
+ const maxTokens = formatContextTokens(detail.maxContextLength);
746
+ const loadedTokens = formatContextTokens(detail.loadedContextLength);
747
+ let text = t("knxUltimateAI.messages.lmStudioContextAvailable", "Maximum model context") + ": " + maxTokens + " token";
748
+ let color = "#555";
749
+ if (state === "loading") {
750
+ text = t("knxUltimateAI.messages.lmStudioContextLoading", "Loading the model with its maximum context") + ": " + maxTokens + " token…";
751
+ color = "#1565c0";
752
+ } else if (state === "configured") {
753
+ const configuredTokens = formatContextTokens(detail.configuredContextLength || detail.loadedContextLength || detail.maxContextLength);
754
+ text = t("knxUltimateAI.messages.lmStudioContextConfigured", "Maximum model context configured") + ": " + configuredTokens + " token";
755
+ color = "#2e7d32";
756
+ } else if (state === "error") {
757
+ text = String(errorText || t("knxUltimateAI.messages.lmStudioContextFailed", "Unable to configure the model context"));
758
+ color = "#c62828";
759
+ } else if (detail.loadedContextLength > 0) {
760
+ text += " · " + t("knxUltimateAI.messages.lmStudioContextCurrentlyLoaded", "currently loaded") + ": " + loadedTokens + " token";
761
+ }
762
+ $status.text(text).css("color", color).show();
763
+ };
764
+
765
+ const populateModels = (models, modelDetails) => {
536
766
  const $dl = $("#knx-ai-llmModels");
537
767
  if (!$dl || $dl.length === 0) return;
768
+ localModelDetailsById = new Map();
769
+ (modelDetails || []).forEach(function (detail) {
770
+ if (detail && detail.id) localModelDetailsById.set(String(detail.id), detail);
771
+ });
538
772
  $dl.empty();
539
773
  (models || []).forEach(function (m) {
540
- $("<option>").attr("value", m).appendTo($dl);
774
+ const detail = localModelDetailsById.get(String(m));
775
+ const label = detail
776
+ ? String(detail.displayName || detail.id) + " · " + formatContextTokens(detail.maxContextLength) + " token"
777
+ : String(m);
778
+ $("<option>").attr("value", m).attr("label", label).appendTo($dl);
541
779
  });
542
780
  };
543
781
 
@@ -545,7 +783,7 @@
545
783
  const isOpenAiDefaultUrl = (value) => normalizeUrl(value) === normalizeUrl(OPENAI_COMPAT_DEFAULT_CHAT_URL);
546
784
  const isOllamaDefaultUrl = (value) => normalizeUrl(value) === normalizeUrl(OLLAMA_DEFAULT_CHAT_URL);
547
785
  const isOpenAiDefaultModel = (value) => OPENAI_COMPAT_DEFAULT_MODELS.indexOf(String(value || "").trim().toLowerCase()) >= 0;
548
- const DEFAULT_CHAT_URLS = [OPENAI_COMPAT_DEFAULT_CHAT_URL, OLLAMA_DEFAULT_CHAT_URL, ANTHROPIC_DEFAULT_CHAT_URL];
786
+ const DEFAULT_CHAT_URLS = [OPENAI_COMPAT_DEFAULT_CHAT_URL, OLLAMA_DEFAULT_CHAT_URL, ANTHROPIC_DEFAULT_CHAT_URL, LMSTUDIO_DEFAULT_CHAT_URL];
549
787
  const DEFAULT_MODELS = OPENAI_COMPAT_DEFAULT_MODELS.concat([OLLAMA_DEFAULT_MODEL, ANTHROPIC_DEFAULT_MODEL]);
550
788
  const isAnyDefaultUrl = (value) => DEFAULT_CHAT_URLS.some((u) => normalizeUrl(u) === normalizeUrl(value));
551
789
  const isAnyDefaultModel = (value) => DEFAULT_MODELS.indexOf(String(value || "").trim().toLowerCase()) >= 0;
@@ -556,10 +794,12 @@
556
794
  const currentBaseUrl = String($baseUrl.val() || "").trim();
557
795
  const currentModel = String($model.val() || "").trim();
558
796
 
559
- const urlByProvider = { ollama: OLLAMA_DEFAULT_CHAT_URL, anthropic: ANTHROPIC_DEFAULT_CHAT_URL, openai_compat: OPENAI_COMPAT_DEFAULT_CHAT_URL };
560
- const modelByProvider = { ollama: OLLAMA_DEFAULT_MODEL, anthropic: ANTHROPIC_DEFAULT_MODEL, openai_compat: OPENAI_COMPAT_DEFAULT_MODELS[0] };
797
+ const urlByProvider = { ollama: OLLAMA_DEFAULT_CHAT_URL, anthropic: ANTHROPIC_DEFAULT_CHAT_URL, lmstudio: LMSTUDIO_DEFAULT_CHAT_URL, openai_compat: OPENAI_COMPAT_DEFAULT_CHAT_URL };
798
+ const modelByProvider = { ollama: OLLAMA_DEFAULT_MODEL, anthropic: ANTHROPIC_DEFAULT_MODEL, lmstudio: "", openai_compat: OPENAI_COMPAT_DEFAULT_MODELS[0] };
561
799
  const targetUrl = urlByProvider[provider] || OPENAI_COMPAT_DEFAULT_CHAT_URL;
562
- const targetModel = modelByProvider[provider] || OPENAI_COMPAT_DEFAULT_MODELS[0];
800
+ const targetModel = Object.prototype.hasOwnProperty.call(modelByProvider, provider)
801
+ ? modelByProvider[provider]
802
+ : OPENAI_COMPAT_DEFAULT_MODELS[0];
563
803
 
564
804
  // Only overwrite when the field is empty or still holds another provider's default,
565
805
  // so a user's custom base URL / model is never clobbered when switching providers.
@@ -567,6 +807,66 @@
567
807
  if (!currentModel || isAnyDefaultModel(currentModel)) $model.val(targetModel);
568
808
  };
569
809
 
810
+ const configureSelectedLocalModel = () => {
811
+ const provider = $("#node-input-llmProvider").val();
812
+ if (provider !== "lmstudio" && provider !== "ollama") return;
813
+ const model = String($("#node-input-llmModel").val() || "").trim();
814
+ const detail = localModelDetailsById.get(model);
815
+ if (!detail || !(Number(detail.maxContextLength) > 0)) {
816
+ $("#node-input-llmContextLength").val(0);
817
+ setLocalModelContextStatus(null);
818
+ return;
819
+ }
820
+
821
+ const requestVersion = ++localContextRequestVersion;
822
+ $("#node-input-llmContextLength").val(Number(detail.maxContextLength));
823
+ if (provider === "ollama") {
824
+ detail.configuredContextLength = Number(detail.maxContextLength);
825
+ setLocalModelContextStatus(detail, "configured");
826
+ return;
827
+ }
828
+
829
+ setLocalModelContextStatus(detail, "loading");
830
+ $("#node-input-llmModel").prop("disabled", true);
831
+ $("#knx-ai-refreshModels").prop("disabled", true);
832
+
833
+ const payload = {
834
+ nodeId: nodeId,
835
+ baseUrl: $("#node-input-llmBaseUrl").val() || LMSTUDIO_DEFAULT_CHAT_URL,
836
+ model: model
837
+ };
838
+ const apiKey = $("#node-input-llmApiKey").val() || "";
839
+ if (apiKey && apiKey !== "__PWRD__") payload.apiKey = apiKey;
840
+
841
+ $.ajax({
842
+ url: "knxUltimateAI/lmstudio/select-model",
843
+ type: "POST",
844
+ contentType: "application/json",
845
+ data: JSON.stringify(payload)
846
+ })
847
+ .done(function (data) {
848
+ if (requestVersion !== localContextRequestVersion) return;
849
+ detail.loadedContextLength = Math.max(0, Number(data && data.contextLength) || Number(detail.maxContextLength));
850
+ detail.configuredContextLength = detail.loadedContextLength;
851
+ $("#node-input-llmContextLength").val(detail.loadedContextLength);
852
+ setLocalModelContextStatus(detail, "configured");
853
+ })
854
+ .fail(function (xhr) {
855
+ if (requestVersion !== localContextRequestVersion) return;
856
+ let errorText = t("knxUltimateAI.messages.lmStudioContextFailed", "Unable to configure the model context");
857
+ try {
858
+ if (xhr && xhr.responseJSON && xhr.responseJSON.error) errorText = xhr.responseJSON.error;
859
+ } catch (e) { }
860
+ setLocalModelContextStatus(detail, "error", errorText);
861
+ try { RED.notify(errorText, "error"); } catch (e) { }
862
+ })
863
+ .always(function () {
864
+ if (requestVersion !== localContextRequestVersion) return;
865
+ $("#node-input-llmModel").prop("disabled", false);
866
+ $("#knx-ai-refreshModels").prop("disabled", false);
867
+ });
868
+ };
869
+
570
870
  const refreshModels = () => {
571
871
  const provider = $("#node-input-llmProvider").val();
572
872
  const baseUrl = $("#node-input-llmBaseUrl").val() || "";
@@ -586,7 +886,21 @@
586
886
  })
587
887
  .done(function (data) {
588
888
  const models = (data && data.models) ? data.models : [];
589
- populateModels(models);
889
+ populateModels(models, data && data.modelDetails);
890
+ let selectedAutomatically = false;
891
+ if (provider === "lmstudio" && models.length > 0 && !String($("#node-input-llmModel").val() || "").trim()) {
892
+ $("#node-input-llmModel").val(models[0]);
893
+ selectedAutomatically = true;
894
+ }
895
+ if (provider === "lmstudio" || provider === "ollama") {
896
+ const selectedDetail = localModelDetailsById.get(String($("#node-input-llmModel").val() || ""));
897
+ if (selectedDetail) {
898
+ setLocalModelContextStatus(selectedDetail);
899
+ configureSelectedLocalModel();
900
+ } else if (selectedAutomatically) {
901
+ configureSelectedLocalModel();
902
+ }
903
+ }
590
904
  if (provider === "ollama" && data && data.ollamaStarted) {
591
905
  try {
592
906
  const startedMsg = t('knxUltimateAI.messages.ollamaStartedAuto', "Ollama server started automatically.");
@@ -667,6 +981,11 @@
667
981
  } else {
668
982
  $("#knx-ai-apikey-row").show();
669
983
  $("#knx-ai-ollama-install-row").hide();
984
+ if (provider === "lmstudio" && autoLoadModels) refreshModels();
985
+ if (provider !== "lmstudio") {
986
+ $("#node-input-llmContextLength").val(0);
987
+ if (provider !== "ollama") $("#knx-ai-local-context-status").hide();
988
+ }
670
989
  }
671
990
  };
672
991
  $("#node-input-llmProvider").on("change", function () {
@@ -678,6 +997,9 @@
678
997
  evt.preventDefault();
679
998
  refreshModels();
680
999
  });
1000
+ $("#node-input-llmModel").on("change", function () {
1001
+ configureSelectedLocalModel();
1002
+ });
681
1003
  $("#knx-ai-downloadOllamaModel").on("click", function (evt) {
682
1004
  evt.preventDefault();
683
1005
  openOllamaLibrary();
@@ -753,30 +1075,23 @@
753
1075
  </div>
754
1076
  </div>
755
1077
 
756
- <div id="knx-ai-accordion">
757
- <h3><i class="fa fa-magic"></i> <span data-i18n="knxUltimateAI.sections.groupAssistant"></span></h3>
758
- <div>
1078
+ <div id="knx-ai-tabs">
1079
+ <ul>
1080
+ <li><a href="#knx-ai-tabs-assistant"><i class="fa fa-magic"></i> <span data-i18n="knxUltimateAI.sections.groupAssistant"></span></a></li>
1081
+ <li><a href="#knx-ai-tabs-chat-home"><i class="fa fa-comments"></i> <span data-i18n="knxUltimateAI.sections.groupChatHome"></span></a></li>
1082
+ </ul>
1083
+ <div id="knx-ai-tabs-assistant">
759
1084
  <div class="knx-ai-accordion-subsection">
760
1085
  <h4><i class="fa fa-cog"></i> <span data-i18n="knxUltimateAI.sections.quickSetup"></span></h4>
761
1086
  <div class="knx-ai-accordion-mount" id="knx-ai-mount-assistant-setup"></div>
762
1087
  </div>
763
- <div class="knx-ai-accordion-subsection">
764
- <h4><i class="fa fa-book"></i> <span data-i18n="knxUltimateAI.sections.llmContext"></span></h4>
765
- <div class="knx-ai-accordion-mount" id="knx-ai-mount-assistant-context"></div>
766
- </div>
767
1088
  <div class="knx-ai-accordion-subsection">
768
1089
  <h4><i class="fa fa-sliders"></i> <span data-i18n="knxUltimateAI.sections.advanced"></span></h4>
769
1090
  <div class="knx-ai-accordion-mount" id="knx-ai-mount-assistant-advanced"></div>
770
1091
  </div>
771
1092
  </div>
772
1093
 
773
- <h3><i class="fa fa-comments"></i> <span data-i18n="knxUltimateAI.sections.groupChatHome"></span></h3>
774
- <div>
775
- <div id="knx-ai-detected-adapters-panel">
776
- <h4><i class="fa fa-puzzle-piece"></i> <span data-i18n="knxUltimateAI.sections.detectedAdapters"></span></h4>
777
- <div id="knx-ai-detected-adapters-status" data-i18n="knxUltimateAI.messages.detectedAdaptersLoading"></div>
778
- <div id="knx-ai-detected-adapters-list"></div>
779
- </div>
1094
+ <div id="knx-ai-tabs-chat-home">
780
1095
  <div class="knx-ai-accordion-subsection">
781
1096
  <h4><i class="fa fa-plug"></i> <span data-i18n="knxUltimateAI.sections.chatAdapter"></span></h4>
782
1097
  <div class="knx-ai-accordion-mount" id="knx-ai-mount-chat-adapter"></div>
@@ -785,50 +1100,36 @@
785
1100
  <h4><i class="fa fa-home"></i> <span data-i18n="knxUltimateAI.sections.homeIntelligence"></span></h4>
786
1101
  <div class="knx-ai-accordion-mount" id="knx-ai-mount-home-intelligence"></div>
787
1102
  </div>
788
- </div>
789
-
790
- <h3><i class="fa fa-exchange"></i> <span data-i18n="knxUltimateAI.sections.groupKnxAnalysis"></span></h3>
791
- <div>
792
- <div class="knx-ai-accordion-subsection">
793
- <h4><i class="fa fa-database"></i> <span data-i18n="knxUltimateAI.sections.storage"></span></h4>
794
- <div class="knx-ai-accordion-mount" id="knx-ai-mount-knx-storage"></div>
1103
+ <div id="knx-ai-detected-adapters-panel">
1104
+ <h4><i class="fa fa-puzzle-piece"></i> <span data-i18n="knxUltimateAI.sections.detectedAdapters"></span></h4>
1105
+ <div id="knx-ai-detected-adapters-status" data-i18n="knxUltimateAI.messages.detectedAdaptersLoading"></div>
1106
+ <div id="knx-ai-detected-adapters-list"></div>
1107
+ <div id="knx-ai-tts-ultimate-picker" style="display:none;">
1108
+ <label for="node-input-ttsUltimateNodeId"><i class="fa fa-volume-up"></i> <span data-i18n="knxUltimateAI.properties.ttsUltimateNodeId"></span></label>
1109
+ <select id="node-input-ttsUltimateNodeId"></select>
1110
+ <div class="knx-ai-tts-ultimate-hint" data-i18n="knxUltimateAI.messages.ttsUltimateHint"></div>
1111
+ </div>
1112
+ </div>
1113
+ <div id="knx-ai-chat-context-panel">
1114
+ <h4><i class="fa fa-database"></i> <span data-i18n="knxUltimateAI.sections.chatContextOverview"></span></h4>
1115
+ <div id="knx-ai-chat-context-status" data-i18n="knxUltimateAI.messages.chatContextLoading"></div>
1116
+ <div id="knx-ai-chat-context-content" style="display:none;">
1117
+ <div class="knx-ai-chat-context-intro" data-i18n="knxUltimateAI.messages.chatContextIntro"></div>
1118
+ <div class="knx-ai-chat-context-group-title" data-i18n="knxUltimateAI.messages.chatContextSourcesTitle"></div>
1119
+ <div id="knx-ai-chat-context-sources-list"></div>
1120
+ <div class="knx-ai-chat-context-group-title" data-i18n="knxUltimateAI.messages.chatContextFilesTitle"></div>
1121
+ <div id="knx-ai-chat-context-files-list"></div>
1122
+ <div class="knx-ai-chat-context-group-title" data-i18n="knxUltimateAI.messages.chatContextDirectoriesTitle"></div>
1123
+ <div id="knx-ai-chat-context-directories-list"></div>
1124
+ <div id="knx-ai-chat-context-pattern" class="knx-ai-chat-context-pattern"></div>
1125
+ </div>
795
1126
  </div>
796
1127
  </div>
1128
+
797
1129
  </div>
798
1130
 
799
1131
  <div id="knx-ai-accordion-source" style="display:none;">
800
1132
 
801
- <div id="knx-ai-tab-storage">
802
- <div class="form-row">
803
- <label style="width:290px" for="node-input-historyWindowSec"><i class="fa fa-history"></i> <span data-i18n="knxUltimateAI.properties.historyWindowSec"></span></label>
804
- <input style="width:90px" type="number" id="node-input-historyWindowSec">
805
- </div>
806
- <div class="form-row">
807
- <input type="checkbox" id="node-input-historyStoreToDisk" style="display:inline-block; width:auto; vertical-align:top;">
808
- <label style="width:auto" for="node-input-historyStoreToDisk">&nbsp;&nbsp;<span data-i18n="knxUltimateAI.properties.historyStoreToDisk"></span></label>
809
- </div>
810
- <div class="form-row" id="knx-ai-history-retention-row">
811
- <label style="width:290px" for="node-input-historyStoreRetentionDays"><i class="fa fa-calendar"></i> <span data-i18n="knxUltimateAI.properties.historyStoreRetentionDays"></span></label>
812
- <input style="width:90px" type="number" id="node-input-historyStoreRetentionDays">
813
- </div>
814
- <div class="form-row">
815
- <label style="width:290px" for="node-input-emitIntervalSec"><i class="fa fa-send"></i> <span data-i18n="knxUltimateAI.properties.emitIntervalSec"></span></label>
816
- <input style="width:90px" type="number" id="node-input-emitIntervalSec">
817
- </div>
818
- <div class="form-row">
819
- <label style="width:290px" for="node-input-analysisWindowSec"><i class="fa fa-clock-o"></i> <span data-i18n="knxUltimateAI.properties.analysisWindowSec"></span></label>
820
- <input style="width:90px" type="number" id="node-input-analysisWindowSec">
821
- </div>
822
- <div class="form-row">
823
- <label style="width:290px" for="node-input-maxEvents"><i class="fa fa-bars"></i> <span data-i18n="knxUltimateAI.properties.maxEvents"></span></label>
824
- <input style="width:90px" type="number" id="node-input-maxEvents">
825
- </div>
826
- <div class="form-row">
827
- <label style="width:290px" for="node-input-topN"><i class="fa fa-list-ol"></i> <span data-i18n="knxUltimateAI.properties.topN"></span></label>
828
- <input style="width:90px" type="number" id="node-input-topN">
829
- </div>
830
- </div>
831
-
832
1133
  <div id="knx-ai-tab-llm-connection">
833
1134
  <div class="form-row">
834
1135
  <input type="checkbox" id="node-input-llmEnabled" style="display:inline-block; width:auto; vertical-align:top;">
@@ -842,6 +1143,7 @@
842
1143
  <option value="openai_compat" data-i18n="knxUltimateAI.selectlists.llmProvider.openai_compat"></option>
843
1144
  <option value="anthropic" data-i18n="knxUltimateAI.selectlists.llmProvider.anthropic"></option>
844
1145
  <option value="ollama" data-i18n="knxUltimateAI.selectlists.llmProvider.ollama"></option>
1146
+ <option value="lmstudio" data-i18n="knxUltimateAI.selectlists.llmProvider.lmstudio"></option>
845
1147
  </select>
846
1148
  </div>
847
1149
  <div class="form-row" id="knx-ai-apikey-row">
@@ -857,6 +1159,8 @@
857
1159
  </button>
858
1160
  </div>
859
1161
  <datalist id="knx-ai-llmModels"></datalist>
1162
+ <input type="hidden" id="node-input-llmContextLength">
1163
+ <div id="knx-ai-local-context-status" style="display:none; margin:-8px 0 12px 290px; font-size:12px;"></div>
860
1164
  <div class="form-row">
861
1165
  <input type="checkbox" id="node-input-llmAllowKnxCommands" style="display:inline-block; width:auto; vertical-align:top;">
862
1166
  <label style="width:auto" for="node-input-llmAllowKnxCommands">&nbsp;&nbsp;<span data-i18n="knxUltimateAI.properties.llmAllowKnxCommands"></span></label>
@@ -891,61 +1195,9 @@
891
1195
 
892
1196
  <div id="knx-ai-tab-home-intelligence">
893
1197
  <div class="form-row">
894
- <input type="checkbox" id="node-input-proactiveEnabled" style="display:inline-block; width:auto; vertical-align:top;">
895
- <label style="width:auto" for="node-input-proactiveEnabled">&nbsp;&nbsp;<span data-i18n="knxUltimateAI.properties.proactiveEnabled"></span></label>
896
- </div>
897
- <div id="knx-ai-proactive-settings">
898
- <div class="form-row">
899
- <label style="width:290px" for="node-input-proactiveRecipient"><i class="fa fa-user"></i> <span data-i18n="knxUltimateAI.properties.proactiveRecipient"></span></label>
900
- <input style="width:100%" type="text" id="node-input-proactiveRecipient" data-i18n="[placeholder]knxUltimateAI.placeholder.proactiveRecipient">
901
- </div>
902
- <div class="form-row">
903
- <label style="width:290px" for="node-input-proactiveOpenMinutes"><i class="fa fa-clock-o"></i> <span data-i18n="knxUltimateAI.properties.proactiveOpenMinutes"></span></label>
904
- <input style="width:100px" type="number" min="1" max="1440" id="node-input-proactiveOpenMinutes">
905
- </div>
906
- <div class="form-row">
907
- <label style="width:290px" for="node-input-proactiveQuietStart"><i class="fa fa-moon-o"></i> <span data-i18n="knxUltimateAI.properties.proactiveQuietStart"></span></label>
908
- <input style="width:120px" type="time" id="node-input-proactiveQuietStart">
909
- </div>
910
- <div class="form-row">
911
- <label style="width:290px" for="node-input-proactiveQuietEnd"><i class="fa fa-sun-o"></i> <span data-i18n="knxUltimateAI.properties.proactiveQuietEnd"></span></label>
912
- <input style="width:120px" type="time" id="node-input-proactiveQuietEnd">
913
- </div>
914
- </div>
915
- <div class="form-row" style="margin-top:14px;">
916
1198
  <label style="width:100%" for="node-input-aiEducation"><i class="fa fa-graduation-cap"></i> <span data-i18n="knxUltimateAI.properties.aiEducation"></span></label>
917
1199
  <textarea id="node-input-aiEducation" maxlength="16000" style="width:100%; min-height:180px; box-sizing:border-box;" data-i18n="[placeholder]knxUltimateAI.placeholder.aiEducation"></textarea>
918
1200
  </div>
919
- <div class="knx-ai-inline-subsection">
920
- <h5><i class="fa fa-sliders"></i> <span data-i18n="knxUltimateAI.sections.homeIntelligenceAdvanced"></span></h5>
921
- <div>
922
- <div class="form-row">
923
- <label style="width:290px" for="node-input-proactiveCooldownMinutes"><i class="fa fa-bell-slash-o"></i> <span data-i18n="knxUltimateAI.properties.proactiveCooldownMinutes"></span></label>
924
- <input style="width:100px" type="number" min="5" max="10080" id="node-input-proactiveCooldownMinutes">
925
- </div>
926
- </div>
927
- </div>
928
- </div>
929
-
930
- <div id="knx-ai-tab-llm-context">
931
- <div id="knx-ai-llm-context-settings">
932
- <div class="form-row">
933
- <input type="checkbox" id="node-input-llmIncludeFlowContext" style="display:inline-block; width:auto; vertical-align:top;">
934
- <label style="width:auto" for="node-input-llmIncludeFlowContext">&nbsp;&nbsp;<span data-i18n="knxUltimateAI.properties.llmIncludeFlowContext"></span></label>
935
- </div>
936
-
937
- <div class="form-row" id="knx-ai-docs-language-row">
938
- <label style="width:290px" for="node-input-llmDocsLanguage"><i class="fa fa-language"></i> <span data-i18n="knxUltimateAI.properties.llmDocsLanguage"></span></label>
939
- <select style="width:100%" id="node-input-llmDocsLanguage">
940
- <option value="it">Italiano</option>
941
- <option value="en">English</option>
942
- <option value="de">Deutsch</option>
943
- <option value="fr">Français</option>
944
- <option value="es">Español</option>
945
- <option value="zh-CN">简体中文</option>
946
- </select>
947
- </div>
948
- </div>
949
1201
  </div>
950
1202
 
951
1203
  <div id="knx-ai-tab-advanced">