node-red-contrib-knx-ultimate 6.2.0 → 6.2.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.
@@ -1,11 +1,6 @@
1
1
  <script type="text/javascript" src="resources/node-red-contrib-knx-ultimate/htmlUtils.js"></script>
2
2
  <script type="text/javascript" src="resources/node-red-contrib-knx-ultimate/KNXAIChatAdapterMappings.js"></script>
3
3
  <style type="text/css">
4
- #knx-ai-models-status {
5
- margin-top: 6px;
6
- margin-bottom: 6px;
7
- }
8
-
9
4
  #knx-ai-ollama-install-row {
10
5
  align-items: flex-start;
11
6
  }
@@ -26,24 +21,82 @@
26
21
  margin: 0 !important;
27
22
  }
28
23
 
29
- .knx-ai-chat-adapter-code {
30
- display: block;
24
+ .knx-ai-chat-adapter-editor {
31
25
  width: 100% !important;
32
- min-width: 100%;
33
- max-width: none !important;
34
- min-height: 190px;
26
+ height: 220px;
27
+ min-height: 180px;
35
28
  box-sizing: border-box;
36
- clear: both;
37
- resize: vertical;
38
- font-family: var(--red-ui-monospace-font, monospace);
39
- font-size: 12px;
40
- line-height: 1.4;
41
29
  }
42
30
 
43
- #knx-ai-ollama-steps,
44
- #knx-ai-ollama-empty-help {
45
- margin-left: 290px;
31
+ #node-input-chatInputCode-editor .monaco-editor,
32
+ #node-input-chatInputCode-editor .monaco-editor-background,
33
+ #node-input-chatInputCode-editor .monaco-editor .margin,
34
+ #node-input-chatInputCode-editor .monaco-editor .overflow-guard,
35
+ #node-input-chatInputCode-editor .monaco-editor .lines-content,
36
+ #node-input-chatInputCode-editor .monaco-editor .editor-scrollable {
37
+ background-color: #e8f5e9 !important;
38
+ }
39
+
40
+ #node-input-chatOutputCode-editor .monaco-editor,
41
+ #node-input-chatOutputCode-editor .monaco-editor-background,
42
+ #node-input-chatOutputCode-editor .monaco-editor .margin,
43
+ #node-input-chatOutputCode-editor .monaco-editor .overflow-guard,
44
+ #node-input-chatOutputCode-editor .monaco-editor .lines-content,
45
+ #node-input-chatOutputCode-editor .monaco-editor .editor-scrollable {
46
+ background-color: #fffde7 !important;
47
+ }
48
+
49
+ #knx-ai-accordion {
50
+ margin-top: 12px;
51
+ }
52
+
53
+ #knx-ai-accordion>h3 {
46
54
  margin-top: 6px;
55
+ font-size: 14px;
56
+ font-weight: 600;
57
+ }
58
+
59
+ #knx-ai-accordion>.ui-accordion-content {
60
+ padding: 14px 16px;
61
+ overflow: visible;
62
+ }
63
+
64
+ #knx-ai-accordion .knx-ai-accordion-subsection {
65
+ padding: 0;
66
+ }
67
+
68
+ #knx-ai-accordion .knx-ai-accordion-subsection+.knx-ai-accordion-subsection {
69
+ margin-top: 18px;
70
+ padding-top: 16px;
71
+ border-top: 1px solid var(--red-ui-form-input-border-color, #ccc);
72
+ }
73
+
74
+ #knx-ai-accordion .knx-ai-accordion-subsection>h4 {
75
+ margin: 0 0 14px;
76
+ color: var(--red-ui-primary-text-color, #555);
77
+ font-size: 14px;
78
+ font-weight: 600;
79
+ }
80
+
81
+ #knx-ai-accordion .knx-ai-accordion-subsection>h4 i {
82
+ width: 18px;
83
+ text-align: center;
84
+ }
85
+
86
+ #knx-ai-accordion .knx-ai-inline-subsection {
87
+ margin-top: 18px;
88
+ padding-top: 14px;
89
+ border-top: 1px dashed var(--red-ui-form-input-border-color, #ccc);
90
+ }
91
+
92
+ #knx-ai-accordion .knx-ai-inline-subsection>h5 {
93
+ margin: 0 0 12px;
94
+ font-size: 13px;
95
+ font-weight: 600;
96
+ }
97
+
98
+ #knx-ai-accordion .knx-ai-accordion-mount>div {
99
+ display: block;
47
100
  }
48
101
 
49
102
  @media (max-width: 900px) {
@@ -57,10 +110,6 @@
57
110
  max-width: 100%;
58
111
  }
59
112
 
60
- #knx-ai-ollama-steps,
61
- #knx-ai-ollama-empty-help {
62
- margin-left: 0;
63
- }
64
113
  }
65
114
  </style>
66
115
 
@@ -73,43 +122,33 @@
73
122
  name: { value: "KNX AI", required: false },
74
123
  topic: { value: "", required: false },
75
124
 
76
- notifyreadrequest: { value: true },
77
- notifyresponse: { value: true },
78
- notifywrite: { value: true },
79
-
80
- analysisWindowSec: { value: 120, required: true, validate: RED.validators.number() },
81
- historyWindowSec: { value: 600, required: true, validate: RED.validators.number() },
125
+ analysisWindowSec: { value: 120 },
126
+ historyWindowSec: { value: 600 },
82
127
  historyStoreToDisk: { value: true },
83
- historyStoreRetentionDays: { value: 10, required: true, validate: RED.validators.number() },
84
- emitIntervalSec: { value: 0, required: true, validate: RED.validators.number() },
85
- topN: { value: 10, required: true, validate: RED.validators.number() },
86
- maxEvents: { value: 50000, required: true, validate: RED.validators.number() },
87
-
88
- rateWindowSec: { value: 10, required: true, validate: RED.validators.number() },
89
- maxTelegramPerSecOverall: { value: 0, required: true, validate: RED.validators.number() },
90
- maxTelegramPerSecPerGA: { value: 0, required: true, validate: RED.validators.number() },
91
-
92
- flapWindowSec: { value: 30, required: true, validate: RED.validators.number() },
93
- flapMaxChanges: { value: 0, required: true, validate: RED.validators.number() },
94
-
95
- enablePattern: { value: true },
96
- patternMaxLagMs: { value: 1500, required: true, validate: RED.validators.number() },
97
- patternMinCount: { value: 8, required: true, validate: RED.validators.number() },
128
+ historyStoreRetentionDays: { value: 10 },
129
+ emitIntervalSec: { value: 0 },
130
+ topN: { value: 10 },
131
+ maxEvents: { value: 50000 },
98
132
 
99
133
  llmEnabled: { value: false },
100
134
  llmProvider: { value: "openai_compat" },
101
135
  llmBaseUrl: { value: "https://api.openai.com/v1/chat/completions" },
102
136
  llmModel: { value: "gpt-5.4" },
103
- llmSystemPrompt: { value: "" },
104
- llmIncludeRaw: { value: false },
105
137
  llmIncludeFlowContext: { value: true },
106
- llmIncludeDocsSnippets: { value: true },
107
138
  llmDocsLanguage: { value: "en" },
108
139
  llmAllowKnxCommands: { value: false },
109
140
  llmRequireCommandConfirmation: { value: true },
110
141
  chatAdapterPreset: { value: "none" },
111
142
  chatInputCode: { value: "" },
112
- chatOutputCode: { value: "" }
143
+ chatOutputCode: { value: "" },
144
+ proactiveEnabled: { value: false },
145
+ proactiveRecipient: { value: "" },
146
+ proactiveOpenMinutes: { value: 120 },
147
+ proactiveCooldownMinutes: { value: 360 },
148
+ proactiveQuietStart: { value: "23:00" },
149
+ proactiveQuietEnd: { value: "07:00" },
150
+ homeMemoryMaxKb: { value: 256 },
151
+ aiEducation: { value: "" }
113
152
  },
114
153
  credentials: {
115
154
  llmApiKey: { type: "password" }
@@ -131,6 +170,7 @@
131
170
  paletteLabel: "KNX AI",
132
171
  oneditprepare: function () {
133
172
  try { RED.sidebar.show("help"); } catch (error) { }
173
+ const node = this;
134
174
  const OPENAI_COMPAT_DEFAULT_CHAT_URL = "https://api.openai.com/v1/chat/completions";
135
175
  const OLLAMA_DEFAULT_CHAT_URL = "http://localhost:11434/api/chat";
136
176
  const ANTHROPIC_DEFAULT_CHAT_URL = "https://api.anthropic.com/v1/messages";
@@ -139,71 +179,23 @@
139
179
  const OLLAMA_DEFAULT_MODEL = "llama3.1";
140
180
  const ANTHROPIC_DEFAULT_MODEL = "claude-opus-4-8";
141
181
 
142
- // Reuse the same left-hand vertical tabs as the Matter nodes.
143
- if ($("#knxUltimateMatterControllerDeviceVerticalTabs").length === 0) {
144
- $("head").append(`
145
- <style id="knxUltimateMatterControllerDeviceVerticalTabs">
146
- .hue-vertical-tabs.ui-tabs.ui-widget.ui-widget-content.ui-corner-all {
147
- display: flex;
148
- border: none;
149
- padding: 0;
150
- }
151
- .hue-vertical-tabs > ul.ui-tabs-nav {
152
- flex: 0 0 180px;
153
- border-right: 1px solid #ccc;
154
- border-left: none;
155
- border-top: none;
156
- border-bottom: none;
157
- padding: 0.5em 0.3em;
158
- }
159
- .hue-vertical-tabs > ul.ui-tabs-nav li {
160
- float: none;
161
- width: 100%;
162
- margin: 0 0 2px 0;
163
- }
164
- .hue-vertical-tabs > ul.ui-tabs-nav li a {
165
- display: block;
166
- width: 100%;
167
- white-space: normal;
168
- position: relative;
169
- border-bottom: none !important;
170
- }
171
- .hue-vertical-tabs > ul.ui-tabs-nav li.ui-tabs-active {
172
- border-bottom: none !important;
173
- }
174
- .hue-vertical-tabs > ul.ui-tabs-nav li.ui-tabs-active a::after {
175
- content: "";
176
- position: absolute;
177
- left: 0;
178
- bottom: 0;
179
- width: 50%;
180
- height: 3px;
181
- background: currentColor;
182
- }
183
- .hue-vertical-tabs .ui-tabs-panel {
184
- flex: 1;
185
- padding: 0.8em 1em;
186
- box-sizing: border-box;
187
- border: none;
188
- background: transparent;
189
- }
190
- .hue-vertical-tabs .form-row > dt {
191
- flex: 1 1 auto;
192
- margin: 0;
193
- }
194
- .hue-vertical-tabs hr {
195
- width: 100%;
196
- border: 0;
197
- border-top: 1px solid #ccc;
198
- margin: 8px 0;
199
- }
200
- </style>`);
201
- }
202
- const $tabs = $("#knx-ai-tabs");
203
- $tabs.addClass("hue-vertical-tabs");
204
- $tabs.tabs();
205
- $tabs.find("ul").addClass("ui-tabs-nav");
206
- $tabs.find("li").removeClass("ui-corner-top").addClass("ui-corner-left");
182
+ const mountAccordionSection = (mountSelector, sectionSelector) => {
183
+ const $mount = $(mountSelector);
184
+ const $section = $(sectionSelector);
185
+ if ($mount.length && $section.length) $mount.append($section);
186
+ };
187
+ mountAccordionSection("#knx-ai-mount-assistant-setup", "#knx-ai-tab-llm-connection");
188
+ mountAccordionSection("#knx-ai-mount-assistant-context", "#knx-ai-tab-llm-context");
189
+ mountAccordionSection("#knx-ai-mount-assistant-advanced", "#knx-ai-tab-advanced");
190
+ mountAccordionSection("#knx-ai-mount-chat-adapter", "#knx-ai-tab-chat-adapter");
191
+ mountAccordionSection("#knx-ai-mount-home-intelligence", "#knx-ai-tab-home-intelligence");
192
+ mountAccordionSection("#knx-ai-mount-knx-storage", "#knx-ai-tab-storage");
193
+ $("#knx-ai-accordion").accordion({
194
+ active: 0,
195
+ animate: false,
196
+ collapsible: true,
197
+ heightStyle: "content"
198
+ });
207
199
 
208
200
  const toggleLLM = () => {
209
201
  const enabled = $("#node-input-llmEnabled").is(":checked");
@@ -219,12 +211,6 @@
219
211
  };
220
212
  $("#node-input-historyStoreToDisk").on("change", toggleHistoryArchive);
221
213
 
222
- const toggleDocsContext = () => {
223
- const enabled = $("#node-input-llmIncludeDocsSnippets").is(":checked");
224
- $("#knx-ai-docs-language-row").toggle(enabled);
225
- };
226
- $("#node-input-llmIncludeDocsSnippets").on("change", toggleDocsContext);
227
-
228
214
  const toggleCommandConfirmation = () => {
229
215
  const enabled = $("#node-input-llmAllowKnxCommands").is(":checked");
230
216
  $("#knx-ai-command-confirmation-row").toggle(enabled);
@@ -233,10 +219,27 @@
233
219
 
234
220
  toggleLLM();
235
221
  toggleHistoryArchive();
236
- toggleDocsContext();
237
222
  toggleCommandConfirmation();
238
223
 
239
- const nodeId = this.id;
224
+ const applyNumericFallback = (selector, fallback) => {
225
+ const $field = $(selector);
226
+ if (!$field.length) return;
227
+ const value = $field.val();
228
+ if (value === undefined || value === null || String(value).trim() === "") {
229
+ $field.val(String(fallback));
230
+ }
231
+ };
232
+ applyNumericFallback("#node-input-analysisWindowSec", 120);
233
+ applyNumericFallback("#node-input-historyWindowSec", 600);
234
+ applyNumericFallback("#node-input-historyStoreRetentionDays", 10);
235
+ applyNumericFallback("#node-input-emitIntervalSec", 0);
236
+ applyNumericFallback("#node-input-topN", 10);
237
+ applyNumericFallback("#node-input-maxEvents", 50000);
238
+ applyNumericFallback("#node-input-proactiveOpenMinutes", 120);
239
+ applyNumericFallback("#node-input-proactiveCooldownMinutes", 360);
240
+ applyNumericFallback("#node-input-homeMemoryMaxKb", 256);
241
+
242
+ const nodeId = node.id;
240
243
  const resolveAdminRoot = () => {
241
244
  const raw = (RED.settings && typeof RED.settings.httpAdminRoot === "string") ? RED.settings.httpAdminRoot : "/";
242
245
  const trimmed = String(raw || "/").trim();
@@ -258,12 +261,115 @@
258
261
  return value;
259
262
  };
260
263
 
264
+ const applyChatEditorOptions = (editor) => {
265
+ try {
266
+ if (!editor) return;
267
+ if (typeof editor.updateOptions === "function") {
268
+ editor.updateOptions({
269
+ lineNumbers: "off",
270
+ minimap: { enabled: false },
271
+ scrollbar: { verticalScrollbarSize: 8, horizontalScrollbarSize: 8 }
272
+ });
273
+ } else if (editor.renderer && typeof editor.renderer.setShowGutter === "function") {
274
+ editor.renderer.setShowGutter(false);
275
+ }
276
+ if (typeof editor.setShowPrintMargin === "function") editor.setShowPrintMargin(false);
277
+ if (typeof editor.setOptions === "function") {
278
+ editor.setOptions({
279
+ enableBasicAutocompletion: true,
280
+ enableLiveAutocompletion: true
281
+ });
282
+ }
283
+ } catch (error) { }
284
+ };
285
+ const setChatEditorValue = (editor, value) => {
286
+ try {
287
+ if (!editor) return;
288
+ if (typeof editor.setValue === "function") {
289
+ editor.setValue(String(value || ""));
290
+ } else if (editor.session && typeof editor.session.setValue === "function") {
291
+ editor.session.setValue(String(value || ""));
292
+ }
293
+ } catch (error) { }
294
+ };
295
+ const destroyChatEditors = () => {
296
+ ["chatInputCodeEditor", "chatOutputCodeEditor"].forEach(function (key) {
297
+ try {
298
+ if (node[key] && typeof node[key].destroy === "function") node[key].destroy();
299
+ } catch (error) { }
300
+ delete node[key];
301
+ });
302
+ };
303
+
304
+ try {
305
+ node.chatInputCodeEditor = RED.editor.createEditor({
306
+ id: "node-input-chatInputCode-editor",
307
+ mode: "ace/mode/nrjavascript",
308
+ value: node.chatInputCode || ""
309
+ });
310
+ } catch (error) { }
311
+ try {
312
+ node.chatOutputCodeEditor = RED.editor.createEditor({
313
+ id: "node-input-chatOutputCode-editor",
314
+ mode: "ace/mode/nrjavascript",
315
+ value: node.chatOutputCode || ""
316
+ });
317
+ } catch (error) { }
318
+ applyChatEditorOptions(node.chatInputCodeEditor);
319
+ applyChatEditorOptions(node.chatOutputCodeEditor);
320
+ if (typeof monaco !== "undefined") {
321
+ try {
322
+ const globalScope = typeof window !== "undefined" ? window : globalThis;
323
+ if (!globalScope._knxAiChatEditorThemesDefined) {
324
+ monaco.editor.defineTheme("knx-ai-chat-input-theme", {
325
+ base: "vs",
326
+ inherit: true,
327
+ rules: [],
328
+ colors: {
329
+ "editor.background": "#e8f5e9",
330
+ "editorGutter.background": "#e8f5e9"
331
+ }
332
+ });
333
+ monaco.editor.defineTheme("knx-ai-chat-output-theme", {
334
+ base: "vs",
335
+ inherit: true,
336
+ rules: [],
337
+ colors: {
338
+ "editor.background": "#fffde7",
339
+ "editorGutter.background": "#fffde7"
340
+ }
341
+ });
342
+ globalScope._knxAiChatEditorThemesDefined = true;
343
+ }
344
+ if (node.chatInputCodeEditor && typeof node.chatInputCodeEditor.updateOptions === "function") {
345
+ node.chatInputCodeEditor.updateOptions({ theme: "knx-ai-chat-input-theme" });
346
+ }
347
+ if (node.chatOutputCodeEditor && typeof node.chatOutputCodeEditor.updateOptions === "function") {
348
+ node.chatOutputCodeEditor.updateOptions({ theme: "knx-ai-chat-output-theme" });
349
+ }
350
+ } catch (error) { }
351
+ }
352
+ node._destroyKnxAiChatEditors = destroyChatEditors;
353
+
261
354
  const configureChatAdapterPicker = () => {
262
355
  const adapters = Array.isArray(window.KNXAIChatAdapterMappings)
263
356
  ? window.KNXAIChatAdapterMappings.slice()
264
357
  : [];
265
358
  const $picker = $("#node-input-chatAdapterPreset");
266
359
  if (!$picker.length) return;
360
+ const updateAdapterFieldsVisibility = (preset) => {
361
+ const visible = String(preset || "none") !== "none";
362
+ $("#knx-ai-chat-adapter-fields").toggle(visible);
363
+ if (visible) {
364
+ const resizeEditors = function () {
365
+ try {
366
+ if (node.chatInputCodeEditor && typeof node.chatInputCodeEditor.resize === "function") node.chatInputCodeEditor.resize();
367
+ if (node.chatOutputCodeEditor && typeof node.chatOutputCodeEditor.resize === "function") node.chatOutputCodeEditor.resize();
368
+ } catch (error) { }
369
+ };
370
+ try { setTimeout(resizeEditors, 0); } catch (error) { resizeEditors(); }
371
+ }
372
+ };
267
373
  $picker.empty();
268
374
  $("<option></option>")
269
375
  .attr("value", "none")
@@ -285,20 +391,30 @@
285
391
  .appendTo($picker);
286
392
  }
287
393
  $picker.val(configuredPreset);
394
+ updateAdapterFieldsVisibility(configuredPreset);
288
395
  $picker.on("change", function () {
289
396
  const selected = String($(this).val() || "none");
290
397
  if (selected === "none") {
291
- $("#node-input-chatInputCode").val("");
292
- $("#node-input-chatOutputCode").val("");
398
+ setChatEditorValue(node.chatInputCodeEditor, "");
399
+ setChatEditorValue(node.chatOutputCodeEditor, "");
400
+ updateAdapterFieldsVisibility(selected);
293
401
  return;
294
402
  }
295
403
  const adapter = adapters.find(item => item && item.id === selected);
296
404
  if (!adapter) return;
297
- $("#node-input-chatInputCode").val(adapter.inputCode || "");
298
- $("#node-input-chatOutputCode").val(adapter.outputCode || "");
405
+ setChatEditorValue(node.chatInputCodeEditor, adapter.inputCode || "");
406
+ setChatEditorValue(node.chatOutputCodeEditor, adapter.outputCode || "");
407
+ updateAdapterFieldsVisibility(selected);
299
408
  });
300
409
  };
301
- configureChatAdapterPicker.call(this);
410
+ configureChatAdapterPicker.call(node);
411
+
412
+ const updateProactiveVisibility = () => {
413
+ const enabled = $("#node-input-proactiveEnabled").is(":checked");
414
+ $("#knx-ai-proactive-settings").toggle(enabled);
415
+ };
416
+ $("#node-input-proactiveEnabled").on("change", updateProactiveVisibility);
417
+ updateProactiveVisibility();
302
418
 
303
419
  $("#knx-ai-open-web-page-vue").on("click", function (evt) {
304
420
  evt.preventDefault();
@@ -313,19 +429,10 @@
313
429
  try { if (wnd && typeof wnd.focus === "function") wnd.focus(); } catch (e) { }
314
430
  });
315
431
 
316
- const setModelsStatus = (text, level) => {
317
- const $status = $("#knx-ai-models-status");
318
- if (!$status || $status.length === 0) return;
319
- $status.text(text || "");
320
- $status.removeClass("knx-ai-models-ok knx-ai-models-warn knx-ai-models-err");
321
- if (level === "ok") $status.addClass("knx-ai-models-ok");
322
- if (level === "warn") $status.addClass("knx-ai-models-warn");
323
- if (level === "err") $status.addClass("knx-ai-models-err");
324
- };
325
-
326
- const setOllamaNoModelsVisible = (visible) => {
327
- if (visible) $("#knx-ai-ollama-empty-help").show();
328
- else $("#knx-ai-ollama-empty-help").hide();
432
+ const setRefreshModelsBusy = (busy) => {
433
+ const $button = $("#knx-ai-refreshModels");
434
+ $button.prop("disabled", !!busy);
435
+ $button.find("i.fa-refresh").toggleClass("fa-spin", !!busy);
329
436
  };
330
437
 
331
438
  const setOllamaInstallUiState = (busy) => {
@@ -377,8 +484,7 @@
377
484
  // Node-RED uses "__PWRD__" as placeholder for stored credentials: don't send it.
378
485
  if (apiKey && apiKey !== "__PWRD__") payload.apiKey = apiKey;
379
486
 
380
- setModelsStatus(t('knxUltimateAI.messages.loadingModels', "Loading models..."), "warn");
381
- $("#knx-ai-refreshModels").prop("disabled", true);
487
+ setRefreshModelsBusy(true);
382
488
 
383
489
  $.ajax({
384
490
  url: "knxUltimateAI/models",
@@ -388,7 +494,6 @@
388
494
  })
389
495
  .done(function (data) {
390
496
  const models = (data && data.models) ? data.models : [];
391
- let msg = "";
392
497
  populateModels(models);
393
498
  if (provider === "ollama" && data && data.ollamaStarted) {
394
499
  try {
@@ -397,15 +502,9 @@
397
502
  } catch (e) { }
398
503
  }
399
504
  if (provider === "ollama" && models.length === 0) {
400
- msg = t('knxUltimateAI.messages.ollamaNoModels', "No local Ollama model found. Install one from https://ollama.com/library.");
401
- setModelsStatus(msg, "warn");
402
- setOllamaNoModelsVisible(true);
403
- } else {
404
- msg = t('knxUltimateAI.messages.loadedModels', "Models loaded") + ": " + models.length;
405
- setModelsStatus(msg, "ok");
406
- setOllamaNoModelsVisible(false);
505
+ const msg = t('knxUltimateAI.messages.ollamaNoModels', "No local Ollama model found. Install one from https://ollama.com/library.");
506
+ try { RED.notify(msg, "warning"); } catch (e) { }
407
507
  }
408
- try { RED.notify(msg, "success"); } catch (e) { }
409
508
  })
410
509
  .fail(function (xhr) {
411
510
  let err = "Failed to load models";
@@ -413,11 +512,10 @@
413
512
  const resp = xhr && xhr.responseJSON;
414
513
  if (resp && resp.error) err = resp.error;
415
514
  } catch (e) { }
416
- setModelsStatus(err, "err");
417
515
  try { RED.notify(err, "error"); } catch (e) { }
418
516
  })
419
517
  .always(function () {
420
- $("#knx-ai-refreshModels").prop("disabled", false);
518
+ setRefreshModelsBusy(false);
421
519
  });
422
520
  };
423
521
 
@@ -428,7 +526,6 @@
428
526
  const model = String($("#node-input-llmModel").val() || "").trim() || OLLAMA_DEFAULT_MODEL;
429
527
  const payload = { nodeId: nodeId, baseUrl: baseUrl, model: model };
430
528
 
431
- setModelsStatus(t('knxUltimateAI.messages.installingOllamaModel', "Installing Ollama model..."), "warn");
432
529
  $("#knx-ai-refreshModels").prop("disabled", true);
433
530
  setOllamaInstallUiState(true);
434
531
 
@@ -455,11 +552,10 @@
455
552
  const resp = xhr && xhr.responseJSON;
456
553
  if (resp && resp.error) err = resp.error;
457
554
  } catch (e) { }
458
- setModelsStatus(err, "err");
459
555
  try { RED.notify(err, "error"); } catch (e) { }
556
+ $("#knx-ai-refreshModels").prop("disabled", false);
460
557
  })
461
558
  .always(function () {
462
- $("#knx-ai-refreshModels").prop("disabled", false);
463
559
  setOllamaInstallUiState(false);
464
560
  });
465
561
  };
@@ -474,16 +570,11 @@
474
570
  if (applyDefaults) applyProviderDefaults(provider);
475
571
  if (provider === "ollama") {
476
572
  $("#knx-ai-apikey-row").hide();
477
- $("#knx-ai-ollama-warning").show();
478
573
  $("#knx-ai-ollama-install-row").show();
479
- $("#knx-ai-ollama-steps").show();
480
574
  if (autoLoadModels) refreshModels();
481
575
  } else {
482
576
  $("#knx-ai-apikey-row").show();
483
- $("#knx-ai-ollama-warning").hide();
484
577
  $("#knx-ai-ollama-install-row").hide();
485
- $("#knx-ai-ollama-steps").hide();
486
- setOllamaNoModelsVisible(false);
487
578
  }
488
579
  };
489
580
  $("#node-input-llmProvider").on("change", function () {
@@ -506,9 +597,35 @@
506
597
  },
507
598
  oneditsave: function () {
508
599
  try { RED.sidebar.show("info"); } catch (error) { }
600
+ try {
601
+ if (this.chatInputCodeEditor && typeof this.chatInputCodeEditor.getValue === "function") {
602
+ this.chatInputCode = this.chatInputCodeEditor.getValue();
603
+ $("#node-input-chatInputCode").val(this.chatInputCode);
604
+ }
605
+ } catch (error) { }
606
+ try {
607
+ if (this.chatOutputCodeEditor && typeof this.chatOutputCodeEditor.getValue === "function") {
608
+ this.chatOutputCode = this.chatOutputCodeEditor.getValue();
609
+ $("#node-input-chatOutputCode").val(this.chatOutputCode);
610
+ }
611
+ } catch (error) { }
612
+ try {
613
+ if (typeof this._destroyKnxAiChatEditors === "function") this._destroyKnxAiChatEditors();
614
+ } catch (error) { }
615
+ delete this._destroyKnxAiChatEditors;
509
616
  },
510
617
  oneditcancel: function () {
511
618
  try { RED.sidebar.show("info"); } catch (error) { }
619
+ try {
620
+ if (typeof this._destroyKnxAiChatEditors === "function") this._destroyKnxAiChatEditors();
621
+ } catch (error) { }
622
+ delete this._destroyKnxAiChatEditors;
623
+ },
624
+ oneditresize: function () {
625
+ try {
626
+ if (this.chatInputCodeEditor && typeof this.chatInputCodeEditor.resize === "function") this.chatInputCodeEditor.resize();
627
+ if (this.chatOutputCodeEditor && typeof this.chatOutputCodeEditor.resize === "function") this.chatOutputCodeEditor.resize();
628
+ } catch (error) { }
512
629
  }
513
630
  })
514
631
  </script>
@@ -544,31 +661,45 @@
544
661
  </div>
545
662
  </div>
546
663
 
547
- <div id="knx-ai-tabs" style="margin-top:12px;">
548
- <ul>
549
- <li><a href="#knx-ai-tab-llm-connection"><i class="fa fa-magic"></i> <span data-i18n="knxUltimateAI.sections.quickSetup"></span></a></li>
550
- <li><a href="#knx-ai-tab-capture"><i class="fa fa-exchange"></i> <span data-i18n="knxUltimateAI.sections.capture"></span></a></li>
551
- <li><a href="#knx-ai-tab-storage"><i class="fa fa-database"></i> <span data-i18n="knxUltimateAI.sections.storage"></span></a></li>
552
- <li><a href="#knx-ai-tab-detection"><i class="fa fa-search"></i> <span data-i18n="knxUltimateAI.sections.detection"></span></a></li>
553
- <li><a href="#knx-ai-tab-chat-adapter"><i class="fa fa-plug"></i> <span data-i18n="knxUltimateAI.sections.chatAdapter"></span></a></li>
554
- <li><a href="#knx-ai-tab-llm-context"><i class="fa fa-comments"></i> <span data-i18n="knxUltimateAI.sections.llmContext"></span></a></li>
555
- <li><a href="#knx-ai-tab-advanced"><i class="fa fa-sliders"></i> <span data-i18n="knxUltimateAI.sections.advanced"></span></a></li>
556
- </ul>
557
-
558
- <div id="knx-ai-tab-capture">
559
- <div class="form-row">
560
- <input type="checkbox" id="node-input-notifywrite" style="display:inline-block; width:auto; vertical-align:top;">
561
- <label style="width:auto" for="node-input-notifywrite">&nbsp;&nbsp;<span data-i18n="knxUltimateAI.properties.notifywrite"></span></label>
664
+ <div id="knx-ai-accordion">
665
+ <h3><i class="fa fa-magic"></i> <span data-i18n="knxUltimateAI.sections.groupAssistant"></span></h3>
666
+ <div>
667
+ <div class="knx-ai-accordion-subsection">
668
+ <h4><i class="fa fa-cog"></i> <span data-i18n="knxUltimateAI.sections.quickSetup"></span></h4>
669
+ <div class="knx-ai-accordion-mount" id="knx-ai-mount-assistant-setup"></div>
562
670
  </div>
563
- <div class="form-row">
564
- <input type="checkbox" id="node-input-notifyresponse" style="display:inline-block; width:auto; vertical-align:top;">
565
- <label style="width:auto" for="node-input-notifyresponse">&nbsp;&nbsp;<span data-i18n="knxUltimateAI.properties.notifyresponse"></span></label>
671
+ <div class="knx-ai-accordion-subsection">
672
+ <h4><i class="fa fa-book"></i> <span data-i18n="knxUltimateAI.sections.llmContext"></span></h4>
673
+ <div class="knx-ai-accordion-mount" id="knx-ai-mount-assistant-context"></div>
566
674
  </div>
567
- <div class="form-row">
568
- <input type="checkbox" id="node-input-notifyreadrequest" style="display:inline-block; width:auto; vertical-align:top;">
569
- <label style="width:auto" for="node-input-notifyreadrequest">&nbsp;&nbsp;<span data-i18n="knxUltimateAI.properties.notifyreadrequest"></span></label>
675
+ <div class="knx-ai-accordion-subsection">
676
+ <h4><i class="fa fa-sliders"></i> <span data-i18n="knxUltimateAI.sections.advanced"></span></h4>
677
+ <div class="knx-ai-accordion-mount" id="knx-ai-mount-assistant-advanced"></div>
678
+ </div>
679
+ </div>
680
+
681
+ <h3><i class="fa fa-comments"></i> <span data-i18n="knxUltimateAI.sections.groupChatHome"></span></h3>
682
+ <div>
683
+ <div class="knx-ai-accordion-subsection">
684
+ <h4><i class="fa fa-plug"></i> <span data-i18n="knxUltimateAI.sections.chatAdapter"></span></h4>
685
+ <div class="knx-ai-accordion-mount" id="knx-ai-mount-chat-adapter"></div>
686
+ </div>
687
+ <div class="knx-ai-accordion-subsection">
688
+ <h4><i class="fa fa-home"></i> <span data-i18n="knxUltimateAI.sections.homeIntelligence"></span></h4>
689
+ <div class="knx-ai-accordion-mount" id="knx-ai-mount-home-intelligence"></div>
690
+ </div>
691
+ </div>
692
+
693
+ <h3><i class="fa fa-exchange"></i> <span data-i18n="knxUltimateAI.sections.groupKnxAnalysis"></span></h3>
694
+ <div>
695
+ <div class="knx-ai-accordion-subsection">
696
+ <h4><i class="fa fa-database"></i> <span data-i18n="knxUltimateAI.sections.storage"></span></h4>
697
+ <div class="knx-ai-accordion-mount" id="knx-ai-mount-knx-storage"></div>
570
698
  </div>
571
699
  </div>
700
+ </div>
701
+
702
+ <div id="knx-ai-accordion-source" style="display:none;">
572
703
 
573
704
  <div id="knx-ai-tab-storage">
574
705
  <div class="form-row">
@@ -601,41 +732,6 @@
601
732
  </div>
602
733
  </div>
603
734
 
604
- <div id="knx-ai-tab-detection">
605
- <div class="form-row">
606
- <input type="checkbox" id="node-input-enablePattern" style="display:inline-block; width:auto; vertical-align:top;">
607
- <label style="width:auto" for="node-input-enablePattern">&nbsp;&nbsp;<span data-i18n="knxUltimateAI.properties.enablePattern"></span></label>
608
- </div>
609
- <div class="form-row">
610
- <label style="width:290px" for="node-input-patternMaxLagMs"><i class="fa fa-arrows-h"></i> <span data-i18n="knxUltimateAI.properties.patternMaxLagMs"></span></label>
611
- <input style="width:120px" type="number" id="node-input-patternMaxLagMs">
612
- </div>
613
- <div class="form-row">
614
- <label style="width:290px" for="node-input-patternMinCount"><i class="fa fa-filter"></i> <span data-i18n="knxUltimateAI.properties.patternMinCount"></span></label>
615
- <input style="width:90px" type="number" id="node-input-patternMinCount">
616
- </div>
617
- <div class="form-row">
618
- <label style="width:290px" for="node-input-rateWindowSec"><i class="fa fa-clock-o"></i> <span data-i18n="knxUltimateAI.properties.rateWindowSec"></span></label>
619
- <input style="width:90px" type="number" id="node-input-rateWindowSec">
620
- </div>
621
- <div class="form-row">
622
- <label style="width:290px" for="node-input-maxTelegramPerSecOverall"><i class="fa fa-tachometer"></i> <span data-i18n="knxUltimateAI.properties.maxTelegramPerSecOverall"></span></label>
623
- <input style="width:90px" type="number" id="node-input-maxTelegramPerSecOverall">
624
- </div>
625
- <div class="form-row">
626
- <label style="width:290px" for="node-input-maxTelegramPerSecPerGA"><i class="fa fa-bolt"></i> <span data-i18n="knxUltimateAI.properties.maxTelegramPerSecPerGA"></span></label>
627
- <input style="width:90px" type="number" id="node-input-maxTelegramPerSecPerGA">
628
- </div>
629
- <div class="form-row">
630
- <label style="width:290px" for="node-input-flapWindowSec"><i class="fa fa-exchange"></i> <span data-i18n="knxUltimateAI.properties.flapWindowSec"></span></label>
631
- <input style="width:90px" type="number" id="node-input-flapWindowSec">
632
- </div>
633
- <div class="form-row">
634
- <label style="width:290px" for="node-input-flapMaxChanges"><i class="fa fa-random"></i> <span data-i18n="knxUltimateAI.properties.flapMaxChanges"></span></label>
635
- <input style="width:90px" type="number" id="node-input-flapMaxChanges">
636
- </div>
637
- </div>
638
-
639
735
  <div id="knx-ai-tab-llm-connection">
640
736
  <div class="form-row">
641
737
  <input type="checkbox" id="node-input-llmEnabled" style="display:inline-block; width:auto; vertical-align:top;">
@@ -664,7 +760,6 @@
664
760
  </button>
665
761
  </div>
666
762
  <datalist id="knx-ai-llmModels"></datalist>
667
- <div class="form-tips" id="knx-ai-models-status"></div>
668
763
  <div class="form-row">
669
764
  <input type="checkbox" id="node-input-llmAllowKnxCommands" style="display:inline-block; width:auto; vertical-align:top;">
670
765
  <label style="width:auto" for="node-input-llmAllowKnxCommands">&nbsp;&nbsp;<span data-i18n="knxUltimateAI.properties.llmAllowKnxCommands"></span></label>
@@ -677,45 +772,75 @@
677
772
  </div>
678
773
 
679
774
  <div id="knx-ai-tab-chat-adapter">
680
- <div class="form-tips" style="margin-bottom:12px;">
681
- <span data-i18n="knxUltimateAI.messages.chatAdapterIntro"></span>
682
- </div>
683
775
  <div class="form-row">
684
776
  <label style="width:290px" for="node-input-chatAdapterPreset"><i class="fa fa-plug"></i> <span data-i18n="knxUltimateAI.properties.chatAdapterPreset"></span></label>
685
777
  <select style="width:100%" id="node-input-chatAdapterPreset">
686
778
  <option value="none" data-i18n="knxUltimateAI.selectlists.chatAdapter.none"></option>
687
779
  </select>
688
780
  </div>
689
- <div class="form-row">
690
- <label style="width:100%" for="node-input-chatInputCode"><i class="fa fa-sign-in"></i> <span data-i18n="knxUltimateAI.properties.chatInputCode"></span></label>
691
- <textarea id="node-input-chatInputCode" class="knx-ai-chat-adapter-code" spellcheck="false"></textarea>
781
+ <div id="knx-ai-chat-adapter-fields" style="display:none;">
782
+ <div class="form-row">
783
+ <label style="width:100%" for="node-input-chatInputCode"><i class="fa fa-sign-in"></i> <span data-i18n="knxUltimateAI.properties.chatInputCode"></span></label>
784
+ <input type="hidden" id="node-input-chatInputCode">
785
+ <div id="node-input-chatInputCode-editor" class="node-text-editor knx-ai-chat-adapter-editor"></div>
786
+ </div>
787
+ <div class="form-row">
788
+ <label style="width:100%" for="node-input-chatOutputCode"><i class="fa fa-sign-out"></i> <span data-i18n="knxUltimateAI.properties.chatOutputCode"></span></label>
789
+ <input type="hidden" id="node-input-chatOutputCode">
790
+ <div id="node-input-chatOutputCode-editor" class="node-text-editor knx-ai-chat-adapter-editor"></div>
791
+ </div>
692
792
  </div>
793
+ </div>
794
+
795
+ <div id="knx-ai-tab-home-intelligence">
693
796
  <div class="form-row">
694
- <label style="width:100%" for="node-input-chatOutputCode"><i class="fa fa-sign-out"></i> <span data-i18n="knxUltimateAI.properties.chatOutputCode"></span></label>
695
- <textarea id="node-input-chatOutputCode" class="knx-ai-chat-adapter-code" spellcheck="false"></textarea>
797
+ <input type="checkbox" id="node-input-proactiveEnabled" style="display:inline-block; width:auto; vertical-align:top;">
798
+ <label style="width:auto" for="node-input-proactiveEnabled">&nbsp;&nbsp;<span data-i18n="knxUltimateAI.properties.proactiveEnabled"></span></label>
696
799
  </div>
697
- <div class="form-tips">
698
- <span data-i18n="knxUltimateAI.messages.chatAdapterCodeHelp"></span>
800
+ <div id="knx-ai-proactive-settings">
801
+ <div class="form-row">
802
+ <label style="width:290px" for="node-input-proactiveRecipient"><i class="fa fa-user"></i> <span data-i18n="knxUltimateAI.properties.proactiveRecipient"></span></label>
803
+ <input style="width:100%" type="text" id="node-input-proactiveRecipient" data-i18n="[placeholder]knxUltimateAI.placeholder.proactiveRecipient">
804
+ </div>
805
+ <div class="form-row">
806
+ <label style="width:290px" for="node-input-proactiveOpenMinutes"><i class="fa fa-clock-o"></i> <span data-i18n="knxUltimateAI.properties.proactiveOpenMinutes"></span></label>
807
+ <input style="width:100px" type="number" min="1" max="1440" id="node-input-proactiveOpenMinutes">
808
+ </div>
809
+ <div class="form-row">
810
+ <label style="width:290px" for="node-input-proactiveQuietStart"><i class="fa fa-moon-o"></i> <span data-i18n="knxUltimateAI.properties.proactiveQuietStart"></span></label>
811
+ <input style="width:120px" type="time" id="node-input-proactiveQuietStart">
812
+ </div>
813
+ <div class="form-row">
814
+ <label style="width:290px" for="node-input-proactiveQuietEnd"><i class="fa fa-sun-o"></i> <span data-i18n="knxUltimateAI.properties.proactiveQuietEnd"></span></label>
815
+ <input style="width:120px" type="time" id="node-input-proactiveQuietEnd">
816
+ </div>
817
+ </div>
818
+ <div class="form-row" style="margin-top:14px;">
819
+ <label style="width:100%" for="node-input-aiEducation"><i class="fa fa-graduation-cap"></i> <span data-i18n="knxUltimateAI.properties.aiEducation"></span></label>
820
+ <textarea id="node-input-aiEducation" maxlength="16000" style="width:100%; min-height:180px; box-sizing:border-box;" data-i18n="[placeholder]knxUltimateAI.placeholder.aiEducation"></textarea>
821
+ </div>
822
+ <div class="knx-ai-inline-subsection">
823
+ <h5><i class="fa fa-sliders"></i> <span data-i18n="knxUltimateAI.sections.homeIntelligenceAdvanced"></span></h5>
824
+ <div>
825
+ <div class="form-row">
826
+ <label style="width:290px" for="node-input-proactiveCooldownMinutes"><i class="fa fa-bell-slash-o"></i> <span data-i18n="knxUltimateAI.properties.proactiveCooldownMinutes"></span></label>
827
+ <input style="width:100px" type="number" min="5" max="10080" id="node-input-proactiveCooldownMinutes">
828
+ </div>
829
+ <div class="form-row">
830
+ <label style="width:290px" for="node-input-homeMemoryMaxKb"><i class="fa fa-hdd-o"></i> <span data-i18n="knxUltimateAI.properties.homeMemoryMaxKb"></span></label>
831
+ <input style="width:100px" type="number" min="64" max="1024" id="node-input-homeMemoryMaxKb">
832
+ </div>
833
+ </div>
699
834
  </div>
700
835
  </div>
701
836
 
702
837
  <div id="knx-ai-tab-llm-context">
703
838
  <div id="knx-ai-llm-context-settings">
704
- <div class="form-row">
705
- <input type="checkbox" id="node-input-llmIncludeRaw" style="display:inline-block; width:auto; vertical-align:top;">
706
- <label style="width:auto" for="node-input-llmIncludeRaw">&nbsp;&nbsp;<span data-i18n="knxUltimateAI.properties.llmIncludeRaw"></span></label>
707
- </div>
708
-
709
839
  <div class="form-row">
710
840
  <input type="checkbox" id="node-input-llmIncludeFlowContext" style="display:inline-block; width:auto; vertical-align:top;">
711
841
  <label style="width:auto" for="node-input-llmIncludeFlowContext">&nbsp;&nbsp;<span data-i18n="knxUltimateAI.properties.llmIncludeFlowContext"></span></label>
712
842
  </div>
713
843
 
714
- <div class="form-row">
715
- <input type="checkbox" id="node-input-llmIncludeDocsSnippets" style="display:inline-block; width:auto; vertical-align:top;">
716
- <label style="width:auto" for="node-input-llmIncludeDocsSnippets">&nbsp;&nbsp;<span data-i18n="knxUltimateAI.properties.llmIncludeDocsSnippets"></span></label>
717
- </div>
718
-
719
844
  <div class="form-row" id="knx-ai-docs-language-row">
720
845
  <label style="width:290px" for="node-input-llmDocsLanguage"><i class="fa fa-language"></i> <span data-i18n="knxUltimateAI.properties.llmDocsLanguage"></span></label>
721
846
  <select style="width:100%" id="node-input-llmDocsLanguage">
@@ -727,10 +852,6 @@
727
852
  <option value="zh-CN">简体中文</option>
728
853
  </select>
729
854
  </div>
730
- <div class="form-row">
731
- <label style="width:290px" for="node-input-llmSystemPrompt"><i class="fa fa-commenting-o"></i> <span data-i18n="knxUltimateAI.properties.llmSystemPrompt"></span></label>
732
- <textarea style="width:100%; height:110px;" id="node-input-llmSystemPrompt" data-i18n="[placeholder]knxUltimateAI.placeholder.llmSystemPrompt"></textarea>
733
- </div>
734
855
  </div>
735
856
  </div>
736
857
 
@@ -740,9 +861,6 @@
740
861
  <label style="width:290px" for="node-input-llmBaseUrl"><i class="fa fa-link"></i> <span data-i18n="knxUltimateAI.properties.llmBaseUrl"></span></label>
741
862
  <input type="text" id="node-input-llmBaseUrl" data-i18n="[placeholder]knxUltimateAI.placeholder.llmBaseUrl">
742
863
  </div>
743
- <div class="form-tips" id="knx-ai-ollama-warning" style="display:none;">
744
- <span data-i18n="knxUltimateAI.messages.ollamaNotSupported"></span>
745
- </div>
746
864
  <div class="form-row" id="knx-ai-ollama-install-row" style="display:none;">
747
865
  <label style="width:290px"><i class="fa fa-download"></i> Ollama</label>
748
866
  <div class="knx-ai-ollama-actions">
@@ -754,14 +872,6 @@
754
872
  </button>
755
873
  </div>
756
874
  </div>
757
- <div class="form-tips" id="knx-ai-ollama-steps" style="display:none;">
758
- <span data-i18n="knxUltimateAI.messages.ollamaInstallSteps"></span>
759
- </div>
760
- <div class="form-tips" id="knx-ai-ollama-empty-help" style="display:none;">
761
- <span data-i18n="knxUltimateAI.messages.ollamaNoModels"></span>
762
- <br>
763
- <code>ollama pull llama3.1</code>
764
- </div>
765
875
  </div>
766
876
  </div>
767
877
  </div>