node-red-contrib-knx-ultimate 6.3.30 → 6.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/nodes/knxUltimateAI.html +210 -82
- package/nodes/knxUltimateAI.js +2554 -1660
- package/nodes/knxUltimateAIHomeAssistant.html +38 -0
- package/nodes/knxUltimateAIHomeAssistant.js +152 -0
- package/nodes/locales/de/knxUltimateAI.html +19 -16
- package/nodes/locales/de/knxUltimateAI.json +40 -31
- package/nodes/locales/en/knxUltimateAI.html +27 -18
- package/nodes/locales/en/knxUltimateAI.json +40 -31
- package/nodes/locales/es/knxUltimateAI.html +19 -16
- package/nodes/locales/es/knxUltimateAI.json +40 -31
- package/nodes/locales/fr/knxUltimateAI.html +19 -16
- package/nodes/locales/fr/knxUltimateAI.json +40 -31
- package/nodes/locales/it/knxUltimateAI.html +27 -18
- package/nodes/locales/it/knxUltimateAI.json +40 -31
- package/nodes/locales/zh-CN/knxUltimateAI.html +19 -16
- package/nodes/locales/zh-CN/knxUltimateAI.json +40 -31
- package/nodes/plugins/knxUltimate-cerebrum-runtime-plugin.js +79 -0
- package/nodes/plugins/knxUltimateAI-vue/assets/app.css +1 -1
- package/nodes/plugins/knxUltimateAI-vue/assets/app.js +13 -4
- package/nodes/utils/knxAiCamera.js +4 -4
- package/nodes/utils/knxAiCatalogRetrieval.js +349 -0
- package/nodes/utils/knxAiCerebrum.js +406 -0
- package/nodes/utils/knxAiChatContext.js +43 -1
- package/nodes/utils/knxAiHomeMemory.js +555 -13
- package/nodes/utils/knxAiScheduler.js +4 -1
- package/nodes/utils/knxAiSemanticContext.js +662 -0
- package/package.json +5 -3
- package/resources/KNXAIChatAdapterMappings.js +31 -5
- package/resources/hueControllerProfiles.js +7614 -7622
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,27 @@
|
|
|
6
6
|
|
|
7
7
|
# CHANGELOG
|
|
8
8
|
|
|
9
|
+
**Version 6.4.1** - September 2026<br/>
|
|
10
|
+
|
|
11
|
+
- **KNX AI — safe Cerebrum startup delivery**: corrected the RedBot Telegram envelope used by startup and other proactive messages after a Node-RED restart. Its synthetic chat context now fulfils RedBot's synchronous context contract instead of returning `undefined`, preventing `chat-platform` from raising an uncaught `sourceCode` error and terminating Node-RED. Saved RedBot adapter code is hardened at runtime as well. Schedule persistence now passes the generated Markdown filename to the atomic writer under the correct `filePath` key, removing the accompanying undefined-path warning.<br/>
|
|
12
|
+
- **KNX AI — Cerebrum (BETA)**: renamed the Web Assistant area to Cerebrum and introduced a passive home-automation intelligence layer that studies KNX telegrams, useful Node-RED flow events and the registered HUE, Matter and Home Assistant adapters. Cerebrum maintains bounded current-state and observation context locally, learns only repeated patterns spanning distinct days and asks the occupant for confirmation or correction through the Assistant output before a habit can become predictive. Confirmed habits remain subject to the exact ETS/DPT permissions: group addresses marked read-only are never written.<br/>
|
|
13
|
+
- **Cerebrum — Home Assistant adapter**: added the `Cerebrum Home Assistant` node and the shared runtime registry. When Node-RED is running as a Home Assistant add-on, Setup Doctor detects whether the required `ha-api` round trip is present and tells the user to add it when missing. Entity states, events and service capabilities are exposed to Cerebrum through bounded, vendor-neutral records without retaining raw messages, secrets or binary payloads.<br/>
|
|
14
|
+
- **Cerebrum — supervised startup**: every KNX AI node now asks its configured model to generate a short startup reassurance on the Assistant output. The message carries `msg.boot = true` for simple flow filtering and falls back to a localized deterministic notice if the provider is unavailable.<br/>
|
|
15
|
+
- **KNX AI Web — Cerebrum workspace and readable memory**: reorganized the Web navigation into Cerebrum submenus, removed the separate KNX AI node selector and bound the workspace directly to the node that opened it. Cerebrum Memory now offers an editable authoritative JSON view and a read-only simplified view; AI Chat Learning offers the equivalent editable native `.knxctx` view and localized simplified view. Copy follows the visible representation, while save, restore, download and concurrency checks continue to protect the complete authoritative files.<br/>
|
|
16
|
+
- **Cerebrum — complete import/export**: the Settings page now contains only import/export and its versioned backup includes AI configuration, chat learning, home memory, schedules and the readable schedules companion file, with validation and rollback-safe restoration.<br/>
|
|
17
|
+
|
|
18
|
+
**Version 6.3.32** - August 2026<br/>
|
|
19
|
+
|
|
20
|
+
- **KNX AI**: AI-first chat with complete cloud ETS context, selectable local windows and guided retrieval; selected ETS access is now the sole read/write authority, with no neutral role.<br/>
|
|
21
|
+
|
|
22
|
+
**Version 6.3.31** - August 2026<br/>
|
|
23
|
+
|
|
24
|
+
- **KNX AI — adaptive prompts for 8K local models**: the conversational prompt now reserves response space from the active model window before every request. For an 8K model it uses a compact tool contract and automatically bounds the newest conversation turns, KNX/adapter events, home memory, Web results, schedules, Function source, retrieved ETS objects and camera metadata. The complete selected ETS catalog remains local and available through deterministic retrieval; it is not copied wholesale into the prompt. The prompt-debug file records the final bounded system/user text actually sent to the model.<br/>
|
|
25
|
+
- **KNX AI — model-guided local ETS retrieval**: the conversational model now receives `catalogActions` for deterministic searches over the complete selected ETS catalog held inside the node instead of receiving the full catalog in every prompt. Bounded multi-pass retrieval supports ranked search across exact GAs, ETS names, aliases, hierarchy, areas, semantics, DPTs and value labels; accent-insensitive and typo-tolerant matches; exact-address lookup; area discovery/browsing; and related command/status discovery. No socket or second classifier is used: LM Studio returns structured requests, KNX AI executes them locally and calls the same model again with a deduplicated subset of at most 24 objects. Commands and GA-role learning are accepted only for retrieved objects and still pass the complete local ETS/DPT/access validation. The final prompt debug file exposes the retrieval summaries and exact retrieved subset.<br/>
|
|
26
|
+
- **KNX AI — complete ETS with deduplicated operational context**: removed the 4K/8K/16K/unlimited context-size selector and its persisted runtime property while keeping the complete selected ETS catalog locally searchable from every conversation. The implicit KNX and adapter archive range is now 20 minutes instead of 24 hours; an explicit user time range still takes precedence. Packaged help, README, wiki, examples and changelog text are no longer embedded in prompts and can instead be retrieved from public GitHub through the request-driven Web tool. Retrieved ETS metadata, archive rows and the current request each have one canonical prompt location; duplicated archive summaries, ETS-derived area lists, per-GA live snapshots and the always-on Node-RED project inventory were removed. Complete Function source is supplied only for explicit Function-code review requests. A node-specific, locally protected `knxai-last-chat-prompt-<node-id>.txt` debug file is overwritten before every conversational model call with the exact system/user prompt text and its estimated size, excluding API keys and HTTP headers; its path appears in the editor context card. Ollama receives the model's full declared window as `num_ctx`; KNX AI never retries an oversized request by compacting its input.<br/>
|
|
27
|
+
- **KNX AI — explicit ETS catalog access and read-only policy**: added the same filterable group-address selector used by IoT Bridge’s MQTT profile, with Select all/none and bulk or per-row Read only controls. Every selected ETS object is available to conversational retrieval, while Flow Builder keeps its complete selected design inventory. Excluded objects are absent from AI tools, while read-only objects remain searchable and readable but are rejected locally for every write even if their inferred or learned role is command. Complete ETS names, hierarchy paths and duplicate-address aliases are preserved in the local catalog and retrieved object records. There is intentionally no backward-compatibility migration: an existing node without a newly saved explicit ETS selection exposes no group addresses until it is opened, configured, saved and deployed.<br/>
|
|
28
|
+
- **KNX AI — request-driven Web access with clarification-first tool use**: removed the fixed proactive Web polling option, interval selector, background timer, synthetic recipient routing and related Setup Doctor state. During a normal conversation the model now decides semantically whether the current request needs fresh public information; when a missing goal, scope, place, time window or desired outcome would materially change the answer or query, it must ask one concise clarification and emit no tool action until the user answers. Explicit one-time or recurring Web checks remain available through the user-authorized natural-language scheduler and share the existing hourly budget, bounded research loop, verified citations and security controls. Editor strings, help, wiki documentation and tests were aligned in EN, IT, DE, FR, ES and zh-CN.<br/>
|
|
29
|
+
|
|
9
30
|
**Version 6.3.30** - August 2026<br/>
|
|
10
31
|
|
|
11
32
|
- **KNX AI — two-way RedBot Telegram voice chat**: the `RedBot / node-red-contrib-chatbot (Telegram)` preset now accepts RedBot's native inbound `audio` payload and its already-downloaded OGG/Opus `Buffer`, applies the same 20 MB and five-minute safeguards without downloading the file a second time, and transcribes it through the selected OpenAI-compatible provider. Successful requests receive a native RedBot `audio` reply with the localized AI-generated-voice disclosure and text caption; KNX confirmations deliberately remain RedBot `inline-buttons` text messages because its Telegram voice sender cannot attach that keyboard to `sendVoice`. Existing saved RedBot input/output mappings are upgraded at runtime. The separate `windkh/node-red-contrib-telegrambot` voice contract and its tests remain unchanged.<br/>
|
package/nodes/knxUltimateAI.html
CHANGED
|
@@ -480,12 +480,13 @@
|
|
|
480
480
|
llmModel: { value: "gpt-5.4" },
|
|
481
481
|
llmReasoningEffort: { value: "default" },
|
|
482
482
|
llmContextLength: { value: 0 },
|
|
483
|
-
|
|
483
|
+
llmLocalContextTokens: { value: 0 },
|
|
484
484
|
llmAllowKnxCommands: { value: false },
|
|
485
485
|
llmRequireCommandConfirmation: { value: true },
|
|
486
|
+
etsExposedGAs: { value: [] },
|
|
487
|
+
etsReadOnlyGAs: { value: [] },
|
|
488
|
+
etsExposeConfigured: { value: false },
|
|
486
489
|
webAccessEnabled: { value: true },
|
|
487
|
-
webProactiveEnabled: { value: true },
|
|
488
|
-
webProactiveIntervalMinutes: { value: 5 },
|
|
489
490
|
webMaxCallsPerHour: { value: 40 },
|
|
490
491
|
chatAdapterPreset: { value: "none" },
|
|
491
492
|
chatInputCode: { value: "" },
|
|
@@ -529,6 +530,7 @@
|
|
|
529
530
|
if ($mount.length && $section.length) $mount.append($section);
|
|
530
531
|
};
|
|
531
532
|
mountTabSection("#knx-ai-mount-assistant-setup", "#knx-ai-tab-llm-connection");
|
|
533
|
+
mountTabSection("#knx-ai-mount-ets-access", "#knx-ai-tab-ets-access");
|
|
532
534
|
mountTabSection("#knx-ai-mount-assistant-advanced", "#knx-ai-tab-advanced");
|
|
533
535
|
mountTabSection("#knx-ai-mount-chat-adapter", "#knx-ai-tab-chat-adapter");
|
|
534
536
|
mountTabSection("#knx-ai-mount-home-intelligence", "#knx-ai-tab-home-intelligence");
|
|
@@ -572,6 +574,78 @@
|
|
|
572
574
|
return value;
|
|
573
575
|
};
|
|
574
576
|
|
|
577
|
+
// Same ETS access selector used by the MQTT profile of IoT Bridge.
|
|
578
|
+
try {
|
|
579
|
+
const savedSet = new Set(Array.isArray(node.etsExposedGAs) ? node.etsExposedGAs : []);
|
|
580
|
+
const readOnlySet = new Set(Array.isArray(node.etsReadOnlyGAs) ? node.etsReadOnlyGAs : []);
|
|
581
|
+
const exposeConfigured = node.etsExposeConfigured === true;
|
|
582
|
+
|
|
583
|
+
const updateEtsCount = () => {
|
|
584
|
+
const total = $("#knx-ai-ets-ga-list input.knx-ai-ets-ga-cb").length;
|
|
585
|
+
const selected = $("#knx-ai-ets-ga-list input.knx-ai-ets-ga-cb:checked").length;
|
|
586
|
+
$("#knx-ai-ets-ga-count").text(selected + " / " + total + " " + t("knxUltimateAI.messages.etsSelected", "selected"));
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
const buildEtsGaList = () => {
|
|
590
|
+
const list = $("#knx-ai-ets-ga-list").empty();
|
|
591
|
+
const serverId = String($("#node-input-server").val() || "").trim();
|
|
592
|
+
if (!serverId) {
|
|
593
|
+
list.append($("<div/>").css("color", "#999").text(t("knxUltimateAI.messages.etsNoGateway", "Select a KNX gateway.")));
|
|
594
|
+
updateEtsCount();
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
$.getJSON("knxUltimatecsv?nodeID=" + encodeURIComponent(serverId), function (data) {
|
|
598
|
+
list.empty();
|
|
599
|
+
if (!Array.isArray(data) || data.length === 0) {
|
|
600
|
+
list.append($("<div/>").css("color", "#999").text(t("knxUltimateAI.messages.etsNoGa", "No group addresses found. Import the ETS list in the KNX gateway.")));
|
|
601
|
+
updateEtsCount();
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
data.forEach(function (item) {
|
|
605
|
+
if (!item || !item.ga) return;
|
|
606
|
+
const row = $("<div/>").addClass("knx-ai-ets-ga-row").css({
|
|
607
|
+
display: "flex", gap: "8px", alignItems: "center", padding: "3px 2px",
|
|
608
|
+
width: "100%", whiteSpace: "nowrap", cursor: "pointer", borderBottom: "1px solid #f3f3f3"
|
|
609
|
+
}).appendTo(list);
|
|
610
|
+
const include = $("<input/>", { type: "checkbox", class: "knx-ai-ets-ga-cb", value: item.ga, style: "width:auto; margin:0; flex:0 0 auto;" }).appendTo(row);
|
|
611
|
+
include.prop("checked", exposeConfigured ? savedSet.has(item.ga) : true);
|
|
612
|
+
$("<span/>").css({ flex: "0 0 auto", fontFamily: "monospace", minWidth: "64px" }).text(item.ga).appendTo(row);
|
|
613
|
+
const name = String(item.devicename || item.ga);
|
|
614
|
+
$("<span/>").addClass("knx-ai-ets-ga-name").css({ flex: "1 1 auto", minWidth: "0", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", color: "#444" }).text(name).attr("title", name).appendTo(row);
|
|
615
|
+
$("<span/>").css({ flex: "0 0 auto", color: "#888", fontSize: "11px" }).text("DPT" + (item.dpt || "")).appendTo(row);
|
|
616
|
+
const readOnlyLabel = $("<label/>").css({ flex: "0 0 auto", display: "flex", alignItems: "center", gap: "3px", margin: "0", fontSize: "11px", color: "#888", cursor: "pointer" }).appendTo(row);
|
|
617
|
+
const readOnly = $("<input/>", { type: "checkbox", class: "knx-ai-ets-ga-ro", value: item.ga, style: "width:auto; margin:0;" }).appendTo(readOnlyLabel);
|
|
618
|
+
readOnly.prop("checked", readOnlySet.has(item.ga));
|
|
619
|
+
$("<span/>").text(t("knxUltimateAI.messages.etsReadOnly", "Read only")).appendTo(readOnlyLabel);
|
|
620
|
+
readOnlyLabel.on("click", function (event) { event.stopPropagation(); });
|
|
621
|
+
row.on("click", function (event) {
|
|
622
|
+
if (event.target === include[0] || event.target === readOnly[0]) return;
|
|
623
|
+
include.prop("checked", !include.prop("checked"));
|
|
624
|
+
updateEtsCount();
|
|
625
|
+
});
|
|
626
|
+
});
|
|
627
|
+
$("#knx-ai-ets-ga-list input.knx-ai-ets-ga-cb").on("change", updateEtsCount);
|
|
628
|
+
updateEtsCount();
|
|
629
|
+
}).fail(function () {
|
|
630
|
+
list.empty().append($("<div/>").css("color", "#c0392b").text(t("knxUltimateAI.messages.etsCsvError", "Unable to load the group address list from the gateway.")));
|
|
631
|
+
updateEtsCount();
|
|
632
|
+
});
|
|
633
|
+
};
|
|
634
|
+
|
|
635
|
+
$("#knx-ai-ets-ga-all").on("click", function (event) { event.preventDefault(); $("#knx-ai-ets-ga-list .knx-ai-ets-ga-row:visible").find("input.knx-ai-ets-ga-cb").prop("checked", true); updateEtsCount(); });
|
|
636
|
+
$("#knx-ai-ets-ga-none").on("click", function (event) { event.preventDefault(); $("#knx-ai-ets-ga-list .knx-ai-ets-ga-row:visible").find("input.knx-ai-ets-ga-cb").prop("checked", false); updateEtsCount(); });
|
|
637
|
+
$("#knx-ai-ets-ga-ro-all").on("click", function (event) { event.preventDefault(); $("#knx-ai-ets-ga-list .knx-ai-ets-ga-row:visible").find("input.knx-ai-ets-ga-ro").prop("checked", true); });
|
|
638
|
+
$("#knx-ai-ets-ga-ro-none").on("click", function (event) { event.preventDefault(); $("#knx-ai-ets-ga-list .knx-ai-ets-ga-row:visible").find("input.knx-ai-ets-ga-ro").prop("checked", false); });
|
|
639
|
+
$("#knx-ai-ets-ga-filter").on("input", function () {
|
|
640
|
+
const term = String($(this).val() || "").toLowerCase();
|
|
641
|
+
$("#knx-ai-ets-ga-list .knx-ai-ets-ga-row").each(function () {
|
|
642
|
+
$(this).toggle($(this).text().toLowerCase().indexOf(term) !== -1);
|
|
643
|
+
});
|
|
644
|
+
});
|
|
645
|
+
$("#node-input-server").on("change.knxAiEtsAccess", buildEtsGaList);
|
|
646
|
+
buildEtsGaList();
|
|
647
|
+
} catch (error) { }
|
|
648
|
+
|
|
575
649
|
const applyChatEditorOptions = (editor) => {
|
|
576
650
|
try {
|
|
577
651
|
if (!editor) return;
|
|
@@ -799,7 +873,7 @@
|
|
|
799
873
|
.text(String(prompt.text))
|
|
800
874
|
.on("click", function (evt) {
|
|
801
875
|
evt.preventDefault();
|
|
802
|
-
openKnxAiWebPage("
|
|
876
|
+
openKnxAiWebPage("cerebrum", String(prompt.text));
|
|
803
877
|
})
|
|
804
878
|
.appendTo($prompts);
|
|
805
879
|
});
|
|
@@ -808,6 +882,21 @@
|
|
|
808
882
|
$content.show();
|
|
809
883
|
};
|
|
810
884
|
|
|
885
|
+
let lastChatContextOverview = null;
|
|
886
|
+
const selectedLocalContextTokens = () => Math.max(0, Number($("#node-input-llmLocalContextTokens").val()) || 0);
|
|
887
|
+
const formatCompactContextTokens = (value) => {
|
|
888
|
+
const tokens = Math.max(0, Number(value) || 0);
|
|
889
|
+
if (!tokens) return "?";
|
|
890
|
+
if (tokens >= 1024 && tokens % 1024 === 0) return (tokens / 1024) + "K";
|
|
891
|
+
return tokens.toLocaleString();
|
|
892
|
+
};
|
|
893
|
+
const selectedLocalContextLabel = () => {
|
|
894
|
+
const selectedTokens = selectedLocalContextTokens();
|
|
895
|
+
return selectedTokens
|
|
896
|
+
? formatCompactContextTokens(selectedTokens)
|
|
897
|
+
: t("knxUltimateAI.selectlists.localContext.maximum", "Maximum");
|
|
898
|
+
};
|
|
899
|
+
|
|
811
900
|
const renderChatContextOverview = (overview) => {
|
|
812
901
|
const $status = $("#knx-ai-chat-context-status");
|
|
813
902
|
const $content = $("#knx-ai-chat-context-content");
|
|
@@ -817,12 +906,14 @@
|
|
|
817
906
|
const $contextLimit = $("#knx-ai-chat-context-limit");
|
|
818
907
|
const $lastPromptUsage = $("#knx-ai-chat-context-last-prompt");
|
|
819
908
|
if (!overview || typeof overview !== "object") {
|
|
909
|
+
lastChatContextOverview = null;
|
|
820
910
|
$content.hide();
|
|
821
911
|
$status
|
|
822
912
|
.text(t("knxUltimateAI.messages.chatContextUnavailable", "The chat context summary is temporarily unavailable."))
|
|
823
913
|
.show();
|
|
824
914
|
return;
|
|
825
915
|
}
|
|
916
|
+
lastChatContextOverview = overview;
|
|
826
917
|
|
|
827
918
|
const addRow = function ($list, title, description, pathText, badge) {
|
|
828
919
|
const $row = $("<div></div>").addClass("knx-ai-detected-adapter");
|
|
@@ -839,12 +930,6 @@
|
|
|
839
930
|
$row.appendTo($list);
|
|
840
931
|
};
|
|
841
932
|
|
|
842
|
-
const formatCompactTokens = function (value) {
|
|
843
|
-
const tokens = Math.max(0, Number(value) || 0);
|
|
844
|
-
if (!tokens) return "?";
|
|
845
|
-
if (tokens >= 1024 && tokens % 1024 === 0) return (tokens / 1024) + "K";
|
|
846
|
-
return tokens.toLocaleString();
|
|
847
|
-
};
|
|
848
933
|
const formatBytes = function (value) {
|
|
849
934
|
const bytes = Math.max(0, Number(value) || 0);
|
|
850
935
|
if (bytes >= (1024 * 1024)) return (bytes / (1024 * 1024)).toLocaleString(undefined, { maximumFractionDigits: 1 }) + " MB";
|
|
@@ -853,24 +938,27 @@
|
|
|
853
938
|
};
|
|
854
939
|
|
|
855
940
|
const contextLimit = overview.contextLimit && typeof overview.contextLimit === "object" ? overview.contextLimit : {};
|
|
856
|
-
const
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
contextLimitTokens =
|
|
866
|
-
? modelContextTokens
|
|
867
|
-
:
|
|
941
|
+
const deployedProvider = String(contextLimit.provider || "").toLowerCase();
|
|
942
|
+
const currentProvider = String($("#node-input-llmProvider").val() || deployedProvider).toLowerCase();
|
|
943
|
+
let contextLimitTokens = currentProvider === deployedProvider
|
|
944
|
+
? Math.max(0, Number(contextLimit.tokens) || 0)
|
|
945
|
+
: 0;
|
|
946
|
+
const localProvider = currentProvider === "lmstudio" || currentProvider === "ollama";
|
|
947
|
+
if (localProvider) {
|
|
948
|
+
const modelContextTokens = contextLimitTokens || Math.max(0, Number($("#node-input-llmContextLength").val()) || 0);
|
|
949
|
+
const selectedTokens = selectedLocalContextTokens();
|
|
950
|
+
contextLimitTokens = selectedTokens > 0
|
|
951
|
+
? (modelContextTokens > 0 ? Math.min(modelContextTokens, selectedTokens) : selectedTokens)
|
|
952
|
+
: modelContextTokens;
|
|
868
953
|
}
|
|
869
954
|
const contextLimitValue = contextLimitTokens
|
|
870
|
-
?
|
|
955
|
+
? formatCompactContextTokens(contextLimitTokens) + " " + t("knxUltimateAI.messages.chatContextTokens", "tokens")
|
|
871
956
|
: t("knxUltimateAI.messages.chatContextProviderManaged", "managed by the selected provider/model");
|
|
957
|
+
const localBudgetValue = localProvider
|
|
958
|
+
? " · " + t("knxUltimateAI.messages.localContextBudget", "Local chat context") + ": " + selectedLocalContextLabel()
|
|
959
|
+
: "";
|
|
872
960
|
$contextLimit
|
|
873
|
-
.text(t("knxUltimateAI.messages.chatContextLimitLabel", "Maximum operational context") + ": " + contextLimitValue)
|
|
961
|
+
.text(t("knxUltimateAI.messages.chatContextLimitLabel", "Maximum operational context") + ": " + contextLimitValue + localBudgetValue)
|
|
874
962
|
.attr("title", contextLimitTokens ? contextLimitTokens.toLocaleString() + " tokens" : contextLimitValue);
|
|
875
963
|
|
|
876
964
|
const promptUsage = overview.lastPromptUsage && typeof overview.lastPromptUsage === "object" ? overview.lastPromptUsage : null;
|
|
@@ -878,8 +966,8 @@
|
|
|
878
966
|
const exactInputTokens = Math.max(0, Number(promptUsage.exactInputTokens) || 0);
|
|
879
967
|
const estimatedInputTokens = Math.max(0, Number(promptUsage.estimatedInputTokens) || 0);
|
|
880
968
|
const tokenText = exactInputTokens
|
|
881
|
-
?
|
|
882
|
-
: "~" +
|
|
969
|
+
? formatCompactContextTokens(exactInputTokens) + " " + t("knxUltimateAI.messages.chatContextExactInputTokens", "input tokens measured by the provider")
|
|
970
|
+
: "~" + formatCompactContextTokens(estimatedInputTokens) + " " + t("knxUltimateAI.messages.chatContextEstimatedInputTokens", "estimated input tokens");
|
|
883
971
|
const imageText = Number(promptUsage.imageCount) > 0
|
|
884
972
|
? " · " + Number(promptUsage.imageCount).toLocaleString() + " " + t("knxUltimateAI.messages.chatContextImages", "images")
|
|
885
973
|
: "";
|
|
@@ -895,9 +983,9 @@
|
|
|
895
983
|
}
|
|
896
984
|
|
|
897
985
|
const sourceLabels = {
|
|
898
|
-
knxTraffic: t("knxUltimateAI.messages.chatContextSourceKnxTraffic", "
|
|
899
|
-
adapterHistory: t("knxUltimateAI.messages.chatContextSourceAdapterHistory", "
|
|
900
|
-
etsProject: t("knxUltimateAI.messages.chatContextSourceEtsProject", "ETS
|
|
986
|
+
knxTraffic: t("knxUltimateAI.messages.chatContextSourceKnxTraffic", "Derived KNX analysis plus the latest exact events from the default 20-minute or explicit interval, automatically bounded for the active local-model window."),
|
|
987
|
+
adapterHistory: t("knxUltimateAI.messages.chatContextSourceAdapterHistory", "Latest exact adapter events from the same interval, automatically bounded for the active local-model window."),
|
|
988
|
+
etsProject: t("knxUltimateAI.messages.chatContextSourceEtsProject", "Complete selected semantic ETS catalog for cloud models. Local models receive the full catalog when it fits; otherwise they receive a window-sized manifest and exact model-requested details. Function source only for explicit code review."),
|
|
901
989
|
memoryEducation: t("knxUltimateAI.messages.chatContextSourceMemoryEducation", "Session context, AI Education, bounded home memory and active plans."),
|
|
902
990
|
cameras: t("knxUltimateAI.messages.chatContextSourceCameras", "Detected cameras and their available capabilities.")
|
|
903
991
|
};
|
|
@@ -916,7 +1004,8 @@
|
|
|
916
1004
|
homeMemory: t("knxUltimateAI.messages.chatContextFileHomeMemory", "Bounded learned home memory only; AI Education remains in the node property."),
|
|
917
1005
|
schedules: t("knxUltimateAI.messages.chatContextFileSchedules", "Authoritative structured plans and reminders for this node."),
|
|
918
1006
|
schedulesReadable: t("knxUltimateAI.messages.chatContextFileSchedulesReadable", "Human-readable plans and reminders generated from the scheduler state."),
|
|
919
|
-
assistantConfig: t("knxUltimateAI.messages.chatContextFileAssistantConfig", "Persistent
|
|
1007
|
+
assistantConfig: t("knxUltimateAI.messages.chatContextFileAssistantConfig", "Persistent Cerebrum configuration and semantic areas for this node."),
|
|
1008
|
+
lastChatPrompt: t("knxUltimateAI.messages.chatContextFileLastChatPrompt", "Temporary local copy of the latest system and user messages sent to the chat model; overwritten on every chat request.")
|
|
920
1009
|
};
|
|
921
1010
|
(Array.isArray(overview.files) ? overview.files : []).forEach(function (item) {
|
|
922
1011
|
if (!item || !item.name || !item.path) return;
|
|
@@ -993,10 +1082,14 @@
|
|
|
993
1082
|
const params = new URLSearchParams();
|
|
994
1083
|
if (nodeId) params.set("nodeId", nodeId);
|
|
995
1084
|
if (view === "chat-learning") {
|
|
996
|
-
params.set("tab", "
|
|
997
|
-
params.set("
|
|
998
|
-
} else if (view === "
|
|
999
|
-
params.set("tab", "
|
|
1085
|
+
params.set("tab", "cerebrum");
|
|
1086
|
+
params.set("cerebrumTab", "learning");
|
|
1087
|
+
} else if (view === "cerebrum-memory") {
|
|
1088
|
+
params.set("tab", "cerebrum");
|
|
1089
|
+
params.set("cerebrumTab", "memory");
|
|
1090
|
+
} else if (view === "cerebrum") {
|
|
1091
|
+
params.set("tab", "cerebrum");
|
|
1092
|
+
params.set("cerebrumTab", "conversation");
|
|
1000
1093
|
if (prompt) params.set("prompt", String(prompt));
|
|
1001
1094
|
}
|
|
1002
1095
|
const accessToken = resolveAccessToken();
|
|
@@ -1016,6 +1109,11 @@
|
|
|
1016
1109
|
openKnxAiWebPage("chat-learning");
|
|
1017
1110
|
});
|
|
1018
1111
|
|
|
1112
|
+
$("#knx-ai-open-cerebrum-memory").on("click", function (evt) {
|
|
1113
|
+
evt.preventDefault();
|
|
1114
|
+
openKnxAiWebPage("cerebrum-memory");
|
|
1115
|
+
});
|
|
1116
|
+
|
|
1019
1117
|
const setRefreshModelsBusy = (busy) => {
|
|
1020
1118
|
const $button = $("#knx-ai-refreshModels");
|
|
1021
1119
|
$button.prop("disabled", !!busy);
|
|
@@ -1037,9 +1135,25 @@
|
|
|
1037
1135
|
return tokens ? tokens.toLocaleString() : "?";
|
|
1038
1136
|
};
|
|
1039
1137
|
|
|
1138
|
+
const updateLocalContextTokenOptions = (detail) => {
|
|
1139
|
+
const $select = $("#node-input-llmLocalContextTokens");
|
|
1140
|
+
if (!$select.length) return;
|
|
1141
|
+
const maxContextLength = Math.max(0, Number(detail && detail.maxContextLength) || 0);
|
|
1142
|
+
$select.find("option[data-context-tokens]").each(function () {
|
|
1143
|
+
const optionTokens = Math.max(0, Number($(this).attr("data-context-tokens")) || 0);
|
|
1144
|
+
const unavailable = maxContextLength > 0 && optionTokens > maxContextLength;
|
|
1145
|
+
$(this).prop("disabled", unavailable).prop("hidden", unavailable).toggle(!unavailable);
|
|
1146
|
+
});
|
|
1147
|
+
if (maxContextLength > 0 && selectedLocalContextTokens() > maxContextLength) {
|
|
1148
|
+
$select.val("0");
|
|
1149
|
+
}
|
|
1150
|
+
if (lastChatContextOverview) renderChatContextOverview(lastChatContextOverview);
|
|
1151
|
+
};
|
|
1152
|
+
|
|
1040
1153
|
const setLocalModelContextStatus = (detail, state, errorText) => {
|
|
1041
1154
|
const $status = $("#knx-ai-local-context-status");
|
|
1042
1155
|
const provider = $("#node-input-llmProvider").val();
|
|
1156
|
+
updateLocalContextTokenOptions(detail);
|
|
1043
1157
|
if (!$status.length || (provider !== "lmstudio" && provider !== "ollama") || !detail) {
|
|
1044
1158
|
$status.hide();
|
|
1045
1159
|
return;
|
|
@@ -1064,13 +1178,7 @@
|
|
|
1064
1178
|
} else if (detail.loadedContextLength > 0) {
|
|
1065
1179
|
text += " · " + t("knxUltimateAI.messages.lmStudioContextCurrentlyLoaded", "currently loaded") + ": " + loadedTokens + " token";
|
|
1066
1180
|
}
|
|
1067
|
-
|
|
1068
|
-
const selectedContextValue = String($("#node-input-llmPromptContextTokens").val() || "16384");
|
|
1069
|
-
const selectedBudget = selectedContextValue === "0"
|
|
1070
|
-
? t("knxUltimateAI.selectlists.promptContext.unlimited", "No KNX AI limit (model context)")
|
|
1071
|
-
: (Math.max(4096, Number(selectedContextValue) || 16384) / 1024) + "K";
|
|
1072
|
-
text += " · " + t("knxUltimateAI.messages.localContextBudget", "KNX AI context budget") + ": " + selectedBudget;
|
|
1073
|
-
}
|
|
1181
|
+
text += " · " + t("knxUltimateAI.messages.localContextBudget", "Local chat context") + ": " + selectedLocalContextLabel();
|
|
1074
1182
|
$status.text(text).css("color", color).show();
|
|
1075
1183
|
};
|
|
1076
1184
|
|
|
@@ -1160,6 +1268,7 @@
|
|
|
1160
1268
|
const detail = localModelDetailsById.get(model);
|
|
1161
1269
|
if (!detail || !(Number(detail.maxContextLength) > 0)) {
|
|
1162
1270
|
$("#node-input-llmContextLength").val(0);
|
|
1271
|
+
updateLocalContextTokenOptions(null);
|
|
1163
1272
|
setLocalModelContextStatus(null);
|
|
1164
1273
|
return;
|
|
1165
1274
|
}
|
|
@@ -1170,11 +1279,7 @@
|
|
|
1170
1279
|
: Number(detail.maxContextLength);
|
|
1171
1280
|
$("#node-input-llmContextLength").val(activeContextLength);
|
|
1172
1281
|
if (provider === "ollama") {
|
|
1173
|
-
|
|
1174
|
-
const selectedContextTokens = selectedContextValue === "0"
|
|
1175
|
-
? Number(detail.maxContextLength)
|
|
1176
|
-
: Math.min(16384, Math.max(4096, Number(selectedContextValue) || 16384));
|
|
1177
|
-
detail.configuredContextLength = Math.min(Number(detail.maxContextLength), selectedContextTokens);
|
|
1282
|
+
detail.configuredContextLength = Number(detail.maxContextLength);
|
|
1178
1283
|
$("#node-input-llmContextLength").val(detail.configuredContextLength);
|
|
1179
1284
|
setLocalModelContextStatus(detail);
|
|
1180
1285
|
return;
|
|
@@ -1187,7 +1292,8 @@
|
|
|
1187
1292
|
const payload = {
|
|
1188
1293
|
nodeId: nodeId,
|
|
1189
1294
|
baseUrl: $("#node-input-llmBaseUrl").val() || LMSTUDIO_DEFAULT_CHAT_URL,
|
|
1190
|
-
model: model
|
|
1295
|
+
model: model,
|
|
1296
|
+
localContextTokens: selectedLocalContextTokens()
|
|
1191
1297
|
};
|
|
1192
1298
|
const apiKey = $("#node-input-llmApiKey").val() || "";
|
|
1193
1299
|
if (apiKey && apiKey !== "__PWRD__") payload.apiKey = apiKey;
|
|
@@ -1330,7 +1436,10 @@
|
|
|
1330
1436
|
const provider = $("#node-input-llmProvider").val();
|
|
1331
1437
|
const localProvider = provider === "lmstudio" || provider === "ollama";
|
|
1332
1438
|
if (applyDefaults) applyProviderDefaults(provider);
|
|
1333
|
-
$("#knx-ai-
|
|
1439
|
+
$("#knx-ai-local-context-row").toggle(localProvider);
|
|
1440
|
+
if (localProvider) {
|
|
1441
|
+
updateLocalContextTokenOptions(localModelDetailsById.get(String($("#node-input-llmModel").val() || "").trim()));
|
|
1442
|
+
}
|
|
1334
1443
|
if (provider === "ollama") {
|
|
1335
1444
|
$("#knx-ai-apikey-row").hide();
|
|
1336
1445
|
$("#knx-ai-ollama-install-row").show();
|
|
@@ -1347,11 +1456,9 @@
|
|
|
1347
1456
|
};
|
|
1348
1457
|
const toggleWebAccess = () => {
|
|
1349
1458
|
const enabled = $("#node-input-webAccessEnabled").prop("checked") === true;
|
|
1350
|
-
const proactive = enabled && $("#node-input-webProactiveEnabled").prop("checked") === true;
|
|
1351
1459
|
$("#knx-ai-web-access-settings").toggle(enabled);
|
|
1352
|
-
$("#knx-ai-web-proactive-interval-row").toggle(proactive);
|
|
1353
1460
|
};
|
|
1354
|
-
$("#node-input-webAccessEnabled
|
|
1461
|
+
$("#node-input-webAccessEnabled").on("change", toggleWebAccess);
|
|
1355
1462
|
toggleWebAccess();
|
|
1356
1463
|
$("#node-input-llmProvider").on("change", function () {
|
|
1357
1464
|
toggleProvider({ applyDefaults: true, autoLoadModels: true });
|
|
@@ -1365,14 +1472,10 @@
|
|
|
1365
1472
|
$("#node-input-llmModel").on("change", function () {
|
|
1366
1473
|
configureSelectedLocalModel();
|
|
1367
1474
|
});
|
|
1368
|
-
$("#node-input-
|
|
1475
|
+
$("#node-input-llmLocalContextTokens").on("change", function () {
|
|
1369
1476
|
const detail = localModelDetailsById.get(String($("#node-input-llmModel").val() || "").trim());
|
|
1370
|
-
|
|
1371
|
-
if (
|
|
1372
|
-
configureSelectedLocalModel();
|
|
1373
|
-
return;
|
|
1374
|
-
}
|
|
1375
|
-
setLocalModelContextStatus(detail, detail.loadedContextLength > 0 ? "configured" : "inactive");
|
|
1477
|
+
updateLocalContextTokenOptions(detail);
|
|
1478
|
+
if (detail) setLocalModelContextStatus(detail, detail.loadedContextLength > 0 ? "configured" : undefined);
|
|
1376
1479
|
});
|
|
1377
1480
|
$("#knx-ai-downloadOllamaModel").on("click", function (evt) {
|
|
1378
1481
|
evt.preventDefault();
|
|
@@ -1385,6 +1488,18 @@
|
|
|
1385
1488
|
},
|
|
1386
1489
|
oneditsave: function () {
|
|
1387
1490
|
try { RED.sidebar.show("info"); } catch (error) { }
|
|
1491
|
+
try {
|
|
1492
|
+
const checkboxes = $("#knx-ai-ets-ga-list input.knx-ai-ets-ga-cb");
|
|
1493
|
+
if (checkboxes.length > 0) {
|
|
1494
|
+
const exposed = [];
|
|
1495
|
+
const readOnly = [];
|
|
1496
|
+
checkboxes.each(function () { if ($(this).is(":checked")) exposed.push($(this).val()); });
|
|
1497
|
+
$("#knx-ai-ets-ga-list input.knx-ai-ets-ga-ro").each(function () { if ($(this).is(":checked")) readOnly.push($(this).val()); });
|
|
1498
|
+
this.etsExposedGAs = exposed;
|
|
1499
|
+
this.etsReadOnlyGAs = readOnly;
|
|
1500
|
+
this.etsExposeConfigured = true;
|
|
1501
|
+
}
|
|
1502
|
+
} catch (error) { }
|
|
1388
1503
|
try {
|
|
1389
1504
|
if (this.chatInputCodeEditor && typeof this.chatInputCodeEditor.getValue === "function") {
|
|
1390
1505
|
this.chatInputCode = this.chatInputCodeEditor.getValue();
|
|
@@ -1459,6 +1574,10 @@
|
|
|
1459
1574
|
<h4><i class="fa fa-cog"></i> <span data-i18n="knxUltimateAI.sections.quickSetup"></span></h4>
|
|
1460
1575
|
<div class="knx-ai-accordion-mount" id="knx-ai-mount-assistant-setup"></div>
|
|
1461
1576
|
</div>
|
|
1577
|
+
<div class="knx-ai-accordion-subsection">
|
|
1578
|
+
<h4><i class="fa fa-list"></i> <span data-i18n="knxUltimateAI.sections.etsAccess"></span></h4>
|
|
1579
|
+
<div class="knx-ai-accordion-mount" id="knx-ai-mount-ets-access"></div>
|
|
1580
|
+
</div>
|
|
1462
1581
|
<div class="knx-ai-accordion-subsection">
|
|
1463
1582
|
<h4><i class="fa fa-sliders"></i> <span data-i18n="knxUltimateAI.sections.advanced"></span></h4>
|
|
1464
1583
|
<div class="knx-ai-accordion-mount" id="knx-ai-mount-assistant-advanced"></div>
|
|
@@ -1480,6 +1599,11 @@
|
|
|
1480
1599
|
<button type="button" class="red-ui-button" id="knx-ai-open-chat-learning">
|
|
1481
1600
|
<i class="fa fa-external-link"></i> <span data-i18n="knxUltimateAI.buttons.openChatLearning"></span>
|
|
1482
1601
|
</button>
|
|
1602
|
+
<h4 style="margin-top:14px;"><i class="fa fa-database"></i> <span data-i18n="knxUltimateAI.sections.cerebrumMemory"></span></h4>
|
|
1603
|
+
<p class="knx-ai-chat-learning-link-copy" data-i18n="knxUltimateAI.messages.cerebrumMemoryOpenHint"></p>
|
|
1604
|
+
<button type="button" class="red-ui-button" id="knx-ai-open-cerebrum-memory">
|
|
1605
|
+
<i class="fa fa-external-link"></i> <span data-i18n="knxUltimateAI.buttons.openCerebrumMemory"></span>
|
|
1606
|
+
</button>
|
|
1483
1607
|
</div>
|
|
1484
1608
|
<div id="knx-ai-detected-adapters-panel">
|
|
1485
1609
|
<h4><i class="fa fa-puzzle-piece"></i> <span data-i18n="knxUltimateAI.sections.detectedAdapters"></span></h4>
|
|
@@ -1584,15 +1708,19 @@
|
|
|
1584
1708
|
</div>
|
|
1585
1709
|
<input type="hidden" id="node-input-llmContextLength">
|
|
1586
1710
|
<div id="knx-ai-local-context-status" style="display:none;"></div>
|
|
1587
|
-
<div class="form-row" id="knx-ai-
|
|
1588
|
-
<label style="width:290px" for="node-input-
|
|
1589
|
-
<select style="width:100%" id="node-input-
|
|
1590
|
-
<option value="
|
|
1591
|
-
<option value="
|
|
1592
|
-
<option value="
|
|
1593
|
-
<option value="
|
|
1711
|
+
<div class="form-row" id="knx-ai-local-context-row" style="display:none;">
|
|
1712
|
+
<label style="width:290px" for="node-input-llmLocalContextTokens"><i class="fa fa-database"></i> <span data-i18n="knxUltimateAI.properties.llmLocalContextTokens"></span></label>
|
|
1713
|
+
<select style="width:100%" id="node-input-llmLocalContextTokens">
|
|
1714
|
+
<option value="0" data-i18n="knxUltimateAI.selectlists.localContext.maximum"></option>
|
|
1715
|
+
<option value="4096" data-context-tokens="4096" data-i18n="knxUltimateAI.selectlists.localContext.k4"></option>
|
|
1716
|
+
<option value="8192" data-context-tokens="8192" data-i18n="knxUltimateAI.selectlists.localContext.k8"></option>
|
|
1717
|
+
<option value="16384" data-context-tokens="16384" data-i18n="knxUltimateAI.selectlists.localContext.k16"></option>
|
|
1718
|
+
<option value="32768" data-context-tokens="32768" data-i18n="knxUltimateAI.selectlists.localContext.k32"></option>
|
|
1719
|
+
<option value="65536" data-context-tokens="65536" data-i18n="knxUltimateAI.selectlists.localContext.k64"></option>
|
|
1720
|
+
<option value="131072" data-context-tokens="131072" data-i18n="knxUltimateAI.selectlists.localContext.k128"></option>
|
|
1721
|
+
<option value="262144" data-context-tokens="262144" data-i18n="knxUltimateAI.selectlists.localContext.k256"></option>
|
|
1594
1722
|
</select>
|
|
1595
|
-
<div class="knx-ai-prompt-context-hint" data-i18n="knxUltimateAI.messages.
|
|
1723
|
+
<div class="knx-ai-prompt-context-hint" data-i18n="knxUltimateAI.messages.localContextHint"></div>
|
|
1596
1724
|
</div>
|
|
1597
1725
|
<div class="form-row">
|
|
1598
1726
|
<input type="checkbox" id="node-input-llmAllowKnxCommands" style="display:inline-block; width:auto; vertical-align:top;">
|
|
@@ -1641,22 +1769,6 @@
|
|
|
1641
1769
|
<input type="number" id="node-input-webMaxCallsPerHour" min="1" max="60" step="1" style="width:100%;">
|
|
1642
1770
|
<!-- <div class="knx-ai-prompt-context-hint" data-i18n="knxUltimateAI.messages.webBudgetHint"></div> -->
|
|
1643
1771
|
</div>
|
|
1644
|
-
<div class="form-row">
|
|
1645
|
-
<input type="checkbox" id="node-input-webProactiveEnabled" style="display:inline-block; width:auto; vertical-align:top;">
|
|
1646
|
-
<label style="width:auto" for="node-input-webProactiveEnabled"> <span data-i18n="knxUltimateAI.properties.webProactiveEnabled"></span></label>
|
|
1647
|
-
<div class="knx-ai-prompt-context-hint" data-i18n="knxUltimateAI.messages.webProactiveHint"></div>
|
|
1648
|
-
</div>
|
|
1649
|
-
<div class="form-row" id="knx-ai-web-proactive-interval-row">
|
|
1650
|
-
<label style="width:290px" for="node-input-webProactiveIntervalMinutes"><i class="fa fa-clock-o"></i> <span data-i18n="knxUltimateAI.properties.webProactiveIntervalMinutes"></span></label>
|
|
1651
|
-
<select style="width:100%" id="node-input-webProactiveIntervalMinutes">
|
|
1652
|
-
<option value="5" data-i18n="knxUltimateAI.selectlists.webProactiveInterval.5"></option>
|
|
1653
|
-
<option value="10" data-i18n="knxUltimateAI.selectlists.webProactiveInterval.10"></option>
|
|
1654
|
-
<option value="15" data-i18n="knxUltimateAI.selectlists.webProactiveInterval.15"></option>
|
|
1655
|
-
<option value="30" data-i18n="knxUltimateAI.selectlists.webProactiveInterval.30"></option>
|
|
1656
|
-
<option value="60" data-i18n="knxUltimateAI.selectlists.webProactiveInterval.60"></option>
|
|
1657
|
-
<option value="180" data-i18n="knxUltimateAI.selectlists.webProactiveInterval.180"></option>
|
|
1658
|
-
</select>
|
|
1659
|
-
</div>
|
|
1660
1772
|
</div>
|
|
1661
1773
|
<hr>
|
|
1662
1774
|
<div class="form-row">
|
|
@@ -1665,6 +1777,22 @@
|
|
|
1665
1777
|
</div>
|
|
1666
1778
|
</div>
|
|
1667
1779
|
|
|
1780
|
+
<div id="knx-ai-tab-ets-access">
|
|
1781
|
+
<div class="form-row">
|
|
1782
|
+
<div class="knx-ai-prompt-context-hint" data-i18n="knxUltimateAI.messages.etsAccessHint"></div>
|
|
1783
|
+
<div style="margin:8px 0 4px; display:flex; gap:6px; align-items:center;">
|
|
1784
|
+
<input type="text" id="knx-ai-ets-ga-filter" data-i18n="[placeholder]knxUltimateAI.messages.etsFilterPlaceholder" style="flex:1 1 auto;">
|
|
1785
|
+
<button type="button" id="knx-ai-ets-ga-all" class="ui-button ui-corner-all ui-widget" style="width:auto;" data-i18n="knxUltimateAI.buttons.etsSelectAll"></button>
|
|
1786
|
+
<button type="button" id="knx-ai-ets-ga-none" class="ui-button ui-corner-all ui-widget" style="width:auto;" data-i18n="knxUltimateAI.buttons.etsSelectNone"></button>
|
|
1787
|
+
<span style="border-left:1px solid #ccc; align-self:stretch;"></span>
|
|
1788
|
+
<button type="button" id="knx-ai-ets-ga-ro-all" class="ui-button ui-corner-all ui-widget" style="width:auto;" data-i18n="[title]knxUltimateAI.messages.etsReadOnlyBulk;knxUltimateAI.buttons.etsReadOnlyAll"></button>
|
|
1789
|
+
<button type="button" id="knx-ai-ets-ga-ro-none" class="ui-button ui-corner-all ui-widget" style="width:auto;" data-i18n="[title]knxUltimateAI.messages.etsReadOnlyBulk;knxUltimateAI.buttons.etsReadOnlyNone"></button>
|
|
1790
|
+
</div>
|
|
1791
|
+
<div id="knx-ai-ets-ga-count" style="font-size:11px; color:#666; margin:2px 0;"></div>
|
|
1792
|
+
<div id="knx-ai-ets-ga-list" style="max-height:220px; overflow:auto; border:1px solid #ccc; padding:6px; border-radius:4px; background:#fff;"></div>
|
|
1793
|
+
</div>
|
|
1794
|
+
</div>
|
|
1795
|
+
|
|
1668
1796
|
<div id="knx-ai-tab-advanced">
|
|
1669
1797
|
<div id="knx-ai-llm-advanced-settings">
|
|
1670
1798
|
<div class="form-row">
|