node-red-contrib-knx-ultimate 7.1.0-beta.2 → 7.1.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.
- package/CHANGELOG.md +9 -0
- package/README.md +5 -1
- package/nodes/knxUltimateAI.html +1820 -0
- package/nodes/knxUltimateAI.js +16797 -0
- package/nodes/knxUltimateAIHomeAssistant.html +40 -0
- package/nodes/knxUltimateAIHomeAssistant.js +152 -0
- package/nodes/knxUltimateHueController.html +1 -1
- package/nodes/knxUltimateMatterBridge.html +2 -2
- package/nodes/knxUltimateMatterControllerDevice.html +2 -2
- package/nodes/locales/de/knxUltimateAI.html +5 -0
- package/nodes/locales/de/knxUltimateAI.json +231 -0
- package/nodes/locales/de/knxUltimateUtility.json +8 -0
- package/nodes/locales/en/knxUltimateAI.html +5 -0
- package/nodes/locales/en/knxUltimateAI.json +234 -0
- package/nodes/locales/en/knxUltimateUtility.json +8 -0
- package/nodes/locales/es/knxUltimateAI.html +5 -0
- package/nodes/locales/es/knxUltimateAI.json +231 -0
- package/nodes/locales/es/knxUltimateUtility.json +9 -1
- package/nodes/locales/fr/knxUltimateAI.html +5 -0
- package/nodes/locales/fr/knxUltimateAI.json +231 -0
- package/nodes/locales/fr/knxUltimateUtility.json +8 -0
- package/nodes/locales/it/knxUltimateAI.html +5 -0
- package/nodes/locales/it/knxUltimateAI.json +234 -0
- package/nodes/locales/it/knxUltimateUtility.json +8 -0
- package/nodes/locales/zh-CN/knxUltimateAI.html +5 -0
- package/nodes/locales/zh-CN/knxUltimateAI.json +231 -0
- package/nodes/locales/zh-CN/knxUltimateUtility.json +8 -0
- package/nodes/plugins/knxUltimate-cerebrum-runtime-plugin.js +79 -0
- package/nodes/plugins/knxUltimate-migration-notice-plugin.html +19 -0
- package/nodes/plugins/knxUltimateAI-vue/assets/app.css +1 -0
- package/nodes/plugins/knxUltimateAI-vue/assets/app.js +13 -0
- package/nodes/plugins/knxUltimateAI-vue/assets/chunk-html2canvas.esm.js +5 -0
- package/nodes/plugins/knxUltimateAI-vue/assets/chunk-index.es.js +5 -0
- package/nodes/plugins/knxUltimateAI-vue/assets/chunk-jspdf.es.min.js +79 -0
- package/nodes/plugins/knxUltimateAI-vue/assets/chunk-purify.es.js +3 -0
- package/nodes/plugins/knxUltimateAI-vue/index.html +13 -0
- package/nodes/plugins/knxUltimateViewer-vue/assets/app.js +2 -2
- package/nodes/utils/knxAiCamera.js +302 -0
- package/nodes/utils/knxAiCatalogRetrieval.js +349 -0
- package/nodes/utils/knxAiCerebrum.js +406 -0
- package/nodes/utils/knxAiChatContext.js +496 -0
- package/nodes/utils/knxAiEventHistory.js +521 -0
- package/nodes/utils/knxAiHomeMemory.js +1021 -0
- package/nodes/utils/knxAiScheduler.js +445 -0
- package/nodes/utils/knxAiSemanticContext.js +662 -0
- package/nodes/utils/knxAiTelegramVoice.js +545 -0
- package/nodes/utils/knxAiWebAccess.js +631 -0
- package/package.json +17 -9
- package/resources/KNXAIChatAdapterMappings.js +416 -0
- package/resources/hueControllerMigrationDialog.js +3 -0
- package/resources/knxUtilityMigration.js +3 -0
- package/resources/legacyMigrationNotice.js +143 -0
|
@@ -0,0 +1,1820 @@
|
|
|
1
|
+
<script type="text/javascript" src="resources/node-red-contrib-knx-ultimate/htmlUtils.js"></script>
|
|
2
|
+
<script type="text/javascript" src="resources/node-red-contrib-knx-ultimate/KNXAIChatAdapterMappings.js"></script>
|
|
3
|
+
<style type="text/css">
|
|
4
|
+
#knx-ai-ollama-install-row {
|
|
5
|
+
align-items: flex-start;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
#knx-ai-llm-connection-settings {
|
|
9
|
+
width: 100%;
|
|
10
|
+
max-width: 720px;
|
|
11
|
+
min-width: 0;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
#knx-ai-llm-connection-settings .form-row {
|
|
15
|
+
min-width: 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
#knx-ai-local-context-status,
|
|
19
|
+
.knx-ai-prompt-context-hint {
|
|
20
|
+
width: 100%;
|
|
21
|
+
max-width: 720px;
|
|
22
|
+
min-width: 0;
|
|
23
|
+
box-sizing: border-box;
|
|
24
|
+
color: var(--red-ui-secondary-text-color, #777);
|
|
25
|
+
font-size: 12px;
|
|
26
|
+
line-height: 1.4;
|
|
27
|
+
white-space: normal;
|
|
28
|
+
overflow-wrap: break-word;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
#knx-ai-local-context-status {
|
|
32
|
+
margin: -8px 0 12px;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.knx-ai-prompt-context-hint {
|
|
36
|
+
margin: 6px 0 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
#knx-ai-ollama-install-row>label {
|
|
40
|
+
padding-top: 6px;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
#knx-ai-ollama-install-row .knx-ai-ollama-actions {
|
|
44
|
+
display: flex;
|
|
45
|
+
flex-wrap: wrap;
|
|
46
|
+
gap: 8px;
|
|
47
|
+
align-items: center;
|
|
48
|
+
max-width: calc(100% - 300px);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
#knx-ai-ollama-install-row .knx-ai-ollama-actions .red-ui-button {
|
|
52
|
+
margin: 0 !important;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.knx-ai-chat-adapter-editor {
|
|
56
|
+
width: 100% !important;
|
|
57
|
+
height: 220px;
|
|
58
|
+
min-height: 180px;
|
|
59
|
+
box-sizing: border-box;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
#node-input-chatInputCode-editor .monaco-editor,
|
|
63
|
+
#node-input-chatInputCode-editor .monaco-editor-background,
|
|
64
|
+
#node-input-chatInputCode-editor .monaco-editor .margin,
|
|
65
|
+
#node-input-chatInputCode-editor .monaco-editor .overflow-guard,
|
|
66
|
+
#node-input-chatInputCode-editor .monaco-editor .lines-content,
|
|
67
|
+
#node-input-chatInputCode-editor .monaco-editor .editor-scrollable {
|
|
68
|
+
background-color: #e8f5e9 !important;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
#node-input-chatOutputCode-editor .monaco-editor,
|
|
72
|
+
#node-input-chatOutputCode-editor .monaco-editor-background,
|
|
73
|
+
#node-input-chatOutputCode-editor .monaco-editor .margin,
|
|
74
|
+
#node-input-chatOutputCode-editor .monaco-editor .overflow-guard,
|
|
75
|
+
#node-input-chatOutputCode-editor .monaco-editor .lines-content,
|
|
76
|
+
#node-input-chatOutputCode-editor .monaco-editor .editor-scrollable {
|
|
77
|
+
background-color: #fffde7 !important;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
#knx-ai-tabs.ui-tabs {
|
|
81
|
+
background: transparent;
|
|
82
|
+
border: none;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
#knx-ai-tabs>.ui-tabs-nav {
|
|
86
|
+
display: flex;
|
|
87
|
+
flex-wrap: wrap;
|
|
88
|
+
align-items: flex-end;
|
|
89
|
+
margin: 0;
|
|
90
|
+
padding: 0;
|
|
91
|
+
background: transparent;
|
|
92
|
+
border: none;
|
|
93
|
+
border-bottom: 1px solid #c0c0c0;
|
|
94
|
+
border-radius: 0;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
#knx-ai-tabs>.ui-tabs-nav>li.ui-state-default {
|
|
98
|
+
float: none;
|
|
99
|
+
margin: 0 2px -1px 0;
|
|
100
|
+
background: transparent;
|
|
101
|
+
border: none;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
#knx-ai-tabs>.ui-tabs-nav>li.ui-state-default>a {
|
|
105
|
+
padding: 0.5em 1em;
|
|
106
|
+
color: #878787;
|
|
107
|
+
outline: none;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
#knx-ai-tabs>.ui-tabs-nav>li.ui-state-active {
|
|
111
|
+
background: transparent no-repeat bottom center;
|
|
112
|
+
border: none;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
#knx-ai-tabs>.ui-tabs-nav>li.ui-state-active>a {
|
|
116
|
+
color: #377e00;
|
|
117
|
+
border-bottom: 1px solid #377e00;
|
|
118
|
+
outline: none;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
#knx-ai-tabs>.ui-tabs-nav>li>a>i {
|
|
122
|
+
color: inherit;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
#knx-ai-tabs>.ui-tabs-panel {
|
|
126
|
+
padding: 1em 1.4em;
|
|
127
|
+
overflow: visible;
|
|
128
|
+
background: transparent;
|
|
129
|
+
border: none;
|
|
130
|
+
border-radius: 0;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
#knx-ai-tabs .knx-ai-accordion-subsection {
|
|
134
|
+
padding: 0;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
#knx-ai-tabs .knx-ai-accordion-subsection+.knx-ai-accordion-subsection {
|
|
138
|
+
margin-top: 18px;
|
|
139
|
+
padding-top: 16px;
|
|
140
|
+
border-top: 1px solid var(--red-ui-form-input-border-color, #ccc);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
#knx-ai-tabs .knx-ai-accordion-subsection>h4 {
|
|
144
|
+
margin: 0 0 14px;
|
|
145
|
+
color: var(--red-ui-primary-text-color, #555);
|
|
146
|
+
font-size: 14px;
|
|
147
|
+
font-weight: 600;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
#knx-ai-tabs .knx-ai-accordion-subsection>h4 i {
|
|
151
|
+
width: 18px;
|
|
152
|
+
text-align: center;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
#knx-ai-tabs .knx-ai-inline-subsection {
|
|
156
|
+
margin-top: 18px;
|
|
157
|
+
padding-top: 14px;
|
|
158
|
+
border-top: 1px dashed var(--red-ui-form-input-border-color, #ccc);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
#knx-ai-tabs .knx-ai-inline-subsection>h5 {
|
|
162
|
+
margin: 0 0 12px;
|
|
163
|
+
font-size: 13px;
|
|
164
|
+
font-weight: 600;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
#knx-ai-tabs .knx-ai-accordion-mount>div {
|
|
168
|
+
display: block;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
#knx-ai-setup-doctor-panel,
|
|
172
|
+
#knx-ai-detected-adapters-panel,
|
|
173
|
+
#knx-ai-chat-context-panel,
|
|
174
|
+
#knx-ai-chat-learning-link-panel {
|
|
175
|
+
margin: 0 0 18px;
|
|
176
|
+
padding: 12px 14px;
|
|
177
|
+
border: 1px solid var(--red-ui-form-input-border-color, #ccc);
|
|
178
|
+
border-radius: 4px;
|
|
179
|
+
background: var(--red-ui-secondary-background, #f7f7f7);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
#knx-ai-tabs-chat-home>#knx-ai-detected-adapters-panel {
|
|
183
|
+
margin: 18px 0 0;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
#knx-ai-tabs-chat-home>#knx-ai-detected-adapters-panel+#knx-ai-chat-context-panel {
|
|
187
|
+
margin: 18px 0 0;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
#knx-ai-detected-adapters-panel>h4,
|
|
191
|
+
#knx-ai-chat-context-panel>h4,
|
|
192
|
+
#knx-ai-chat-learning-link-panel>h4 {
|
|
193
|
+
margin: 0 0 9px;
|
|
194
|
+
font-size: 14px;
|
|
195
|
+
font-weight: 600;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
#knx-ai-setup-doctor-panel {
|
|
199
|
+
margin: 24px 0 0;
|
|
200
|
+
border-color: rgba(255, 152, 0, 0.55);
|
|
201
|
+
background: linear-gradient(135deg, rgba(255, 152, 0, 0.10), var(--red-ui-secondary-background, #f7f7f7));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.knx-ai-setup-doctor-toggle {
|
|
205
|
+
display: flex;
|
|
206
|
+
align-items: center;
|
|
207
|
+
justify-content: space-between;
|
|
208
|
+
gap: 12px;
|
|
209
|
+
margin: 0;
|
|
210
|
+
color: var(--red-ui-primary-text-color, #555);
|
|
211
|
+
font-size: 14px;
|
|
212
|
+
font-weight: 600;
|
|
213
|
+
cursor: pointer;
|
|
214
|
+
list-style: none;
|
|
215
|
+
user-select: none;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.knx-ai-setup-doctor-toggle::-webkit-details-marker {
|
|
219
|
+
display: none;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.knx-ai-setup-doctor-toggle>span>i {
|
|
223
|
+
width: 18px;
|
|
224
|
+
text-align: center;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.knx-ai-setup-doctor-toggle:focus-visible {
|
|
228
|
+
outline: 2px solid var(--red-ui-primary-color, #377e00);
|
|
229
|
+
outline-offset: 3px;
|
|
230
|
+
border-radius: 2px;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.knx-ai-setup-doctor-chevron {
|
|
234
|
+
flex: 0 0 auto;
|
|
235
|
+
transition: transform 0.16s ease;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
#knx-ai-setup-doctor-panel[open] .knx-ai-setup-doctor-chevron {
|
|
239
|
+
transform: rotate(90deg);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.knx-ai-setup-doctor-body {
|
|
243
|
+
margin-top: 11px;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
#knx-ai-setup-doctor-panel.status-ready {
|
|
247
|
+
border-color: rgba(46, 125, 50, 0.55);
|
|
248
|
+
background: linear-gradient(135deg, rgba(46, 125, 50, 0.10), var(--red-ui-secondary-background, #f7f7f7));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
#knx-ai-setup-doctor-panel.status-blocked {
|
|
252
|
+
border-color: rgba(198, 40, 40, 0.52);
|
|
253
|
+
background: linear-gradient(135deg, rgba(198, 40, 40, 0.09), var(--red-ui-secondary-background, #f7f7f7));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.knx-ai-setup-doctor-hero {
|
|
257
|
+
display: flex;
|
|
258
|
+
align-items: flex-start;
|
|
259
|
+
gap: 10px;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.knx-ai-setup-doctor-summary {
|
|
263
|
+
flex: 1 1 auto;
|
|
264
|
+
min-width: 0;
|
|
265
|
+
line-height: 1.45;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.knx-ai-setup-doctor-score {
|
|
269
|
+
flex: 0 0 auto;
|
|
270
|
+
min-width: 48px;
|
|
271
|
+
padding: 6px 8px;
|
|
272
|
+
border-radius: 16px;
|
|
273
|
+
color: #7a4200;
|
|
274
|
+
background: rgba(255, 152, 0, 0.18);
|
|
275
|
+
text-align: center;
|
|
276
|
+
font-size: 12px;
|
|
277
|
+
font-weight: 700;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
#knx-ai-setup-doctor-panel.status-ready .knx-ai-setup-doctor-score {
|
|
281
|
+
color: #256029;
|
|
282
|
+
background: rgba(46, 125, 50, 0.16);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
#knx-ai-setup-doctor-panel.status-blocked .knx-ai-setup-doctor-score {
|
|
286
|
+
color: #842029;
|
|
287
|
+
background: rgba(198, 40, 40, 0.14);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.knx-ai-setup-doctor-check {
|
|
291
|
+
display: flex;
|
|
292
|
+
align-items: flex-start;
|
|
293
|
+
gap: 8px;
|
|
294
|
+
padding: 7px 0;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.knx-ai-setup-doctor-check+.knx-ai-setup-doctor-check {
|
|
298
|
+
border-top: 1px solid var(--red-ui-form-input-border-color, #ddd);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.knx-ai-setup-doctor-check-copy {
|
|
302
|
+
flex: 1 1 auto;
|
|
303
|
+
min-width: 0;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.knx-ai-setup-doctor-check-detail,
|
|
307
|
+
#knx-ai-setup-doctor-status,
|
|
308
|
+
.knx-ai-setup-doctor-prompts-hint {
|
|
309
|
+
color: var(--red-ui-secondary-text-color, #777);
|
|
310
|
+
font-size: 12px;
|
|
311
|
+
line-height: 1.4;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.knx-ai-setup-doctor-badge {
|
|
315
|
+
flex: 0 0 auto;
|
|
316
|
+
min-width: 48px;
|
|
317
|
+
padding: 2px 7px;
|
|
318
|
+
border-radius: 10px;
|
|
319
|
+
text-align: center;
|
|
320
|
+
font-size: 11px;
|
|
321
|
+
font-weight: 700;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.knx-ai-setup-doctor-badge.pass {
|
|
325
|
+
color: #256029;
|
|
326
|
+
background: #dff0d8;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
.knx-ai-setup-doctor-badge.warn {
|
|
330
|
+
color: #7a5200;
|
|
331
|
+
background: #fff3cd;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.knx-ai-setup-doctor-badge.fail {
|
|
335
|
+
color: #842029;
|
|
336
|
+
background: #f8d7da;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.knx-ai-setup-doctor-badge.info {
|
|
340
|
+
color: #245269;
|
|
341
|
+
background: #d9edf7;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
.knx-ai-setup-doctor-prompts {
|
|
345
|
+
margin-top: 12px;
|
|
346
|
+
padding-top: 10px;
|
|
347
|
+
border-top: 1px dashed var(--red-ui-form-input-border-color, #ccc);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
.knx-ai-setup-doctor-prompt-list {
|
|
351
|
+
display: flex;
|
|
352
|
+
flex-direction: column;
|
|
353
|
+
gap: 6px;
|
|
354
|
+
margin-top: 8px;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.knx-ai-setup-doctor-prompt {
|
|
358
|
+
width: 100%;
|
|
359
|
+
margin: 0 !important;
|
|
360
|
+
text-align: left;
|
|
361
|
+
white-space: normal;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.knx-ai-setup-doctor-footer {
|
|
365
|
+
display: flex;
|
|
366
|
+
align-items: center;
|
|
367
|
+
justify-content: space-between;
|
|
368
|
+
gap: 10px;
|
|
369
|
+
margin-top: 10px;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
.knx-ai-setup-doctor-deploy-hint {
|
|
373
|
+
color: var(--red-ui-secondary-text-color, #777);
|
|
374
|
+
font-size: 11px;
|
|
375
|
+
line-height: 1.35;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
#knx-ai-setup-doctor-refresh {
|
|
379
|
+
flex: 0 0 auto;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
.knx-ai-detected-adapter {
|
|
383
|
+
display: flex;
|
|
384
|
+
align-items: center;
|
|
385
|
+
gap: 8px;
|
|
386
|
+
padding: 7px 0;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
.knx-ai-detected-adapter+.knx-ai-detected-adapter {
|
|
390
|
+
border-top: 1px solid var(--red-ui-form-input-border-color, #ddd);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
.knx-ai-detected-adapter-main {
|
|
394
|
+
flex: 1 1 auto;
|
|
395
|
+
min-width: 0;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
.knx-ai-detected-adapter-package,
|
|
399
|
+
.knx-ai-detected-adapter-counts,
|
|
400
|
+
#knx-ai-detected-adapters-status,
|
|
401
|
+
#knx-ai-chat-context-status,
|
|
402
|
+
.knx-ai-chat-context-intro,
|
|
403
|
+
.knx-ai-chat-context-metric,
|
|
404
|
+
.knx-ai-chat-context-description,
|
|
405
|
+
.knx-ai-chat-context-pattern {
|
|
406
|
+
color: var(--red-ui-secondary-text-color, #777);
|
|
407
|
+
font-size: 12px;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
#knx-ai-chat-learning-link-panel {
|
|
411
|
+
margin: 18px 0 0;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
.knx-ai-chat-learning-link-copy {
|
|
415
|
+
margin: 0 0 10px;
|
|
416
|
+
color: var(--red-ui-secondary-text-color, #777);
|
|
417
|
+
font-size: 12px;
|
|
418
|
+
line-height: 1.45;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
.knx-ai-chat-context-metric {
|
|
422
|
+
margin-top: 6px;
|
|
423
|
+
font-weight: 600;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
.knx-ai-chat-context-group-title {
|
|
427
|
+
margin: 12px 0 2px;
|
|
428
|
+
font-size: 12px;
|
|
429
|
+
font-weight: 600;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
.knx-ai-chat-context-path {
|
|
433
|
+
margin-top: 2px;
|
|
434
|
+
color: var(--red-ui-secondary-text-color, #777);
|
|
435
|
+
font-family: monospace;
|
|
436
|
+
font-size: 11px;
|
|
437
|
+
overflow-wrap: anywhere;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
.knx-ai-chat-context-pattern {
|
|
441
|
+
margin-top: 8px;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
.knx-ai-detected-adapter-badge {
|
|
445
|
+
flex: 0 0 auto;
|
|
446
|
+
padding: 2px 7px;
|
|
447
|
+
border-radius: 10px;
|
|
448
|
+
color: #256029;
|
|
449
|
+
background: #dff0d8;
|
|
450
|
+
font-size: 11px;
|
|
451
|
+
font-weight: 600;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
@media (max-width: 900px) {
|
|
455
|
+
#knx-ai-ollama-install-row>label {
|
|
456
|
+
width: 100% !important;
|
|
457
|
+
padding-top: 0;
|
|
458
|
+
margin-bottom: 6px;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
#knx-ai-ollama-install-row .knx-ai-ollama-actions {
|
|
462
|
+
max-width: 100%;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
}
|
|
466
|
+
</style>
|
|
467
|
+
|
|
468
|
+
<script type="text/javascript">
|
|
469
|
+
RED.nodes.registerType('knxUltimateAI', {
|
|
470
|
+
category: 'deprecated',
|
|
471
|
+
color: '#FF9800',
|
|
472
|
+
defaults: {
|
|
473
|
+
server: { type: "knxUltimate-config", required: true },
|
|
474
|
+
name: { value: "Legacy AI node", required: false },
|
|
475
|
+
topic: { value: "", required: false },
|
|
476
|
+
|
|
477
|
+
llmEnabled: { value: false },
|
|
478
|
+
llmProvider: { value: "openai_compat" },
|
|
479
|
+
llmBaseUrl: { value: "https://api.openai.com/v1/chat/completions" },
|
|
480
|
+
llmModel: { value: "gpt-5.4" },
|
|
481
|
+
llmReasoningEffort: { value: "default" },
|
|
482
|
+
llmContextLength: { value: 0 },
|
|
483
|
+
llmLocalContextTokens: { value: 0 },
|
|
484
|
+
llmAllowKnxCommands: { value: false },
|
|
485
|
+
llmRequireCommandConfirmation: { value: true },
|
|
486
|
+
etsExposedGAs: { value: [] },
|
|
487
|
+
etsReadOnlyGAs: { value: [] },
|
|
488
|
+
etsExposeConfigured: { value: false },
|
|
489
|
+
webAccessEnabled: { value: true },
|
|
490
|
+
webMaxCallsPerHour: { value: 40 },
|
|
491
|
+
chatAdapterPreset: { value: "none" },
|
|
492
|
+
chatInputCode: { value: "" },
|
|
493
|
+
chatOutputCode: { value: "" },
|
|
494
|
+
aiEducation: { value: "" }
|
|
495
|
+
},
|
|
496
|
+
credentials: {
|
|
497
|
+
llmApiKey: { type: "password" }
|
|
498
|
+
},
|
|
499
|
+
inputs: 1,
|
|
500
|
+
outputs: 5,
|
|
501
|
+
outputLabels: function (i) {
|
|
502
|
+
switch (i) {
|
|
503
|
+
case 0: return this._('knxUltimateAI.outputs.summary');
|
|
504
|
+
case 1: return this._('knxUltimateAI.outputs.anomalies');
|
|
505
|
+
case 2: return this._('knxUltimateAI.outputs.assistant');
|
|
506
|
+
case 3: return this._('knxUltimateAI.outputs.knxCommands');
|
|
507
|
+
case 4: return this._('knxUltimateAI.outputs.ttsUltimate');
|
|
508
|
+
}
|
|
509
|
+
},
|
|
510
|
+
icon: "node-eye-icon.svg",
|
|
511
|
+
label: function () {
|
|
512
|
+
return (this.name || "Legacy AI node");
|
|
513
|
+
},
|
|
514
|
+
paletteLabel: "Legacy AI node",
|
|
515
|
+
oneditprepare: function () {
|
|
516
|
+
try { RED.sidebar.show("help"); } catch (error) { }
|
|
517
|
+
const node = this;
|
|
518
|
+
const OPENAI_COMPAT_DEFAULT_CHAT_URL = "https://api.openai.com/v1/chat/completions";
|
|
519
|
+
const OLLAMA_DEFAULT_CHAT_URL = "http://localhost:11434/api/chat";
|
|
520
|
+
const ANTHROPIC_DEFAULT_CHAT_URL = "https://api.anthropic.com/v1/messages";
|
|
521
|
+
const LMSTUDIO_DEFAULT_CHAT_URL = "http://localhost:1234/v1/chat/completions";
|
|
522
|
+
const OLLAMA_LIBRARY_URL = "https://ollama.com/library";
|
|
523
|
+
const OPENAI_COMPAT_DEFAULT_MODELS = ["gpt-5.4", "gpt-4o-mini"];
|
|
524
|
+
const OLLAMA_DEFAULT_MODEL = "llama3.1";
|
|
525
|
+
const ANTHROPIC_DEFAULT_MODEL = "claude-opus-4-8";
|
|
526
|
+
|
|
527
|
+
const mountTabSection = (mountSelector, sectionSelector) => {
|
|
528
|
+
const $mount = $(mountSelector);
|
|
529
|
+
const $section = $(sectionSelector);
|
|
530
|
+
if ($mount.length && $section.length) $mount.append($section);
|
|
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");
|
|
534
|
+
mountTabSection("#knx-ai-mount-assistant-advanced", "#knx-ai-tab-advanced");
|
|
535
|
+
mountTabSection("#knx-ai-mount-chat-adapter", "#knx-ai-tab-chat-adapter");
|
|
536
|
+
mountTabSection("#knx-ai-mount-home-intelligence", "#knx-ai-tab-home-intelligence");
|
|
537
|
+
$("#knx-ai-tabs").tabs({ active: 0 });
|
|
538
|
+
|
|
539
|
+
const toggleLLM = () => {
|
|
540
|
+
const enabled = $("#node-input-llmEnabled").is(":checked");
|
|
541
|
+
$("#knx-ai-llm-connection-settings").toggle(enabled);
|
|
542
|
+
$("#knx-ai-llm-advanced-settings").toggle(enabled);
|
|
543
|
+
};
|
|
544
|
+
$("#node-input-llmEnabled").on("change", toggleLLM);
|
|
545
|
+
|
|
546
|
+
const toggleCommandConfirmation = () => {
|
|
547
|
+
const enabled = $("#node-input-llmAllowKnxCommands").is(":checked");
|
|
548
|
+
$("#knx-ai-command-confirmation-row").toggle(enabled);
|
|
549
|
+
};
|
|
550
|
+
$("#node-input-llmAllowKnxCommands").on("change", toggleCommandConfirmation);
|
|
551
|
+
|
|
552
|
+
toggleLLM();
|
|
553
|
+
toggleCommandConfirmation();
|
|
554
|
+
|
|
555
|
+
const nodeId = node.id;
|
|
556
|
+
const resolveAdminRoot = () => {
|
|
557
|
+
const raw = (RED.settings && typeof RED.settings.httpAdminRoot === "string") ? RED.settings.httpAdminRoot : "/";
|
|
558
|
+
const trimmed = String(raw || "/").trim();
|
|
559
|
+
if (trimmed === "" || trimmed === "/") return "";
|
|
560
|
+
return "/" + trimmed.replace(/^\/+|\/+$/g, "");
|
|
561
|
+
};
|
|
562
|
+
const resolveAccessToken = () => {
|
|
563
|
+
try {
|
|
564
|
+
const tokens = (RED.settings && typeof RED.settings.get === "function") ? RED.settings.get("auth-tokens") : null;
|
|
565
|
+
const token = tokens && typeof tokens.access_token === "string" ? tokens.access_token.trim() : "";
|
|
566
|
+
return token;
|
|
567
|
+
} catch (error) {
|
|
568
|
+
return "";
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
const t = (key, fallback) => {
|
|
572
|
+
const value = RED._(key);
|
|
573
|
+
if (!value || value === key) return fallback || "";
|
|
574
|
+
return value;
|
|
575
|
+
};
|
|
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
|
+
|
|
649
|
+
const applyChatEditorOptions = (editor) => {
|
|
650
|
+
try {
|
|
651
|
+
if (!editor) return;
|
|
652
|
+
if (typeof editor.updateOptions === "function") {
|
|
653
|
+
editor.updateOptions({
|
|
654
|
+
lineNumbers: "off",
|
|
655
|
+
minimap: { enabled: false },
|
|
656
|
+
scrollbar: { verticalScrollbarSize: 8, horizontalScrollbarSize: 8 }
|
|
657
|
+
});
|
|
658
|
+
} else if (editor.renderer && typeof editor.renderer.setShowGutter === "function") {
|
|
659
|
+
editor.renderer.setShowGutter(false);
|
|
660
|
+
}
|
|
661
|
+
if (typeof editor.setShowPrintMargin === "function") editor.setShowPrintMargin(false);
|
|
662
|
+
if (typeof editor.setOptions === "function") {
|
|
663
|
+
editor.setOptions({
|
|
664
|
+
enableBasicAutocompletion: true,
|
|
665
|
+
enableLiveAutocompletion: true
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
} catch (error) { }
|
|
669
|
+
};
|
|
670
|
+
const setChatEditorValue = (editor, value) => {
|
|
671
|
+
try {
|
|
672
|
+
if (!editor) return;
|
|
673
|
+
if (typeof editor.setValue === "function") {
|
|
674
|
+
editor.setValue(String(value || ""));
|
|
675
|
+
} else if (editor.session && typeof editor.session.setValue === "function") {
|
|
676
|
+
editor.session.setValue(String(value || ""));
|
|
677
|
+
}
|
|
678
|
+
} catch (error) { }
|
|
679
|
+
};
|
|
680
|
+
const destroyChatEditors = () => {
|
|
681
|
+
["chatInputCodeEditor", "chatOutputCodeEditor"].forEach(function (key) {
|
|
682
|
+
try {
|
|
683
|
+
if (node[key] && typeof node[key].destroy === "function") node[key].destroy();
|
|
684
|
+
} catch (error) { }
|
|
685
|
+
delete node[key];
|
|
686
|
+
});
|
|
687
|
+
try {
|
|
688
|
+
const $model = $("#node-input-llmModel");
|
|
689
|
+
$model.off(".knxUltimateAiModelPicker");
|
|
690
|
+
if (typeof $model.autocomplete === "function" && $model.data("ui-autocomplete")) {
|
|
691
|
+
$model.autocomplete("destroy");
|
|
692
|
+
}
|
|
693
|
+
} catch (error) { }
|
|
694
|
+
};
|
|
695
|
+
|
|
696
|
+
try {
|
|
697
|
+
node.chatInputCodeEditor = RED.editor.createEditor({
|
|
698
|
+
id: "node-input-chatInputCode-editor",
|
|
699
|
+
mode: "ace/mode/nrjavascript",
|
|
700
|
+
value: node.chatInputCode || ""
|
|
701
|
+
});
|
|
702
|
+
} catch (error) { }
|
|
703
|
+
try {
|
|
704
|
+
node.chatOutputCodeEditor = RED.editor.createEditor({
|
|
705
|
+
id: "node-input-chatOutputCode-editor",
|
|
706
|
+
mode: "ace/mode/nrjavascript",
|
|
707
|
+
value: node.chatOutputCode || ""
|
|
708
|
+
});
|
|
709
|
+
} catch (error) { }
|
|
710
|
+
applyChatEditorOptions(node.chatInputCodeEditor);
|
|
711
|
+
applyChatEditorOptions(node.chatOutputCodeEditor);
|
|
712
|
+
if (typeof monaco !== "undefined") {
|
|
713
|
+
try {
|
|
714
|
+
const globalScope = typeof window !== "undefined" ? window : globalThis;
|
|
715
|
+
if (!globalScope._knxAiChatEditorThemesDefined) {
|
|
716
|
+
monaco.editor.defineTheme("knx-ai-chat-input-theme", {
|
|
717
|
+
base: "vs",
|
|
718
|
+
inherit: true,
|
|
719
|
+
rules: [],
|
|
720
|
+
colors: {
|
|
721
|
+
"editor.background": "#e8f5e9",
|
|
722
|
+
"editorGutter.background": "#e8f5e9"
|
|
723
|
+
}
|
|
724
|
+
});
|
|
725
|
+
monaco.editor.defineTheme("knx-ai-chat-output-theme", {
|
|
726
|
+
base: "vs",
|
|
727
|
+
inherit: true,
|
|
728
|
+
rules: [],
|
|
729
|
+
colors: {
|
|
730
|
+
"editor.background": "#fffde7",
|
|
731
|
+
"editorGutter.background": "#fffde7"
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
globalScope._knxAiChatEditorThemesDefined = true;
|
|
735
|
+
}
|
|
736
|
+
if (node.chatInputCodeEditor && typeof node.chatInputCodeEditor.updateOptions === "function") {
|
|
737
|
+
node.chatInputCodeEditor.updateOptions({ theme: "knx-ai-chat-input-theme" });
|
|
738
|
+
}
|
|
739
|
+
if (node.chatOutputCodeEditor && typeof node.chatOutputCodeEditor.updateOptions === "function") {
|
|
740
|
+
node.chatOutputCodeEditor.updateOptions({ theme: "knx-ai-chat-output-theme" });
|
|
741
|
+
}
|
|
742
|
+
} catch (error) { }
|
|
743
|
+
}
|
|
744
|
+
node._destroyKnxAiChatEditors = destroyChatEditors;
|
|
745
|
+
|
|
746
|
+
const configureChatAdapterPicker = () => {
|
|
747
|
+
const adapters = Array.isArray(window.KNXAIChatAdapterMappings)
|
|
748
|
+
? window.KNXAIChatAdapterMappings.slice()
|
|
749
|
+
: [];
|
|
750
|
+
const $picker = $("#node-input-chatAdapterPreset");
|
|
751
|
+
if (!$picker.length) return;
|
|
752
|
+
const hideAdapterMappingFields = () => {
|
|
753
|
+
$("#knx-ai-chat-adapter-fields").hide();
|
|
754
|
+
};
|
|
755
|
+
$picker.empty();
|
|
756
|
+
$("<option></option>")
|
|
757
|
+
.attr("value", "none")
|
|
758
|
+
.text(t("knxUltimateAI.selectlists.chatAdapter.none", "No adapter"))
|
|
759
|
+
.appendTo($picker);
|
|
760
|
+
adapters.forEach(function (adapter) {
|
|
761
|
+
if (!adapter || !adapter.id) return;
|
|
762
|
+
$("<option></option>")
|
|
763
|
+
.attr("value", adapter.id)
|
|
764
|
+
.text(adapter.title || adapter.id)
|
|
765
|
+
.appendTo($picker);
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
const configuredPreset = String(this.chatAdapterPreset || "none");
|
|
769
|
+
if (configuredPreset !== "none" && !adapters.some(adapter => adapter && adapter.id === configuredPreset)) {
|
|
770
|
+
$("<option></option>")
|
|
771
|
+
.attr("value", configuredPreset)
|
|
772
|
+
.text(configuredPreset)
|
|
773
|
+
.appendTo($picker);
|
|
774
|
+
}
|
|
775
|
+
$picker.val(configuredPreset);
|
|
776
|
+
hideAdapterMappingFields();
|
|
777
|
+
$picker.on("change", function () {
|
|
778
|
+
const selected = String($(this).val() || "none");
|
|
779
|
+
if (selected === "none") {
|
|
780
|
+
setChatEditorValue(node.chatInputCodeEditor, "");
|
|
781
|
+
setChatEditorValue(node.chatOutputCodeEditor, "");
|
|
782
|
+
hideAdapterMappingFields();
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
const adapter = adapters.find(item => item && item.id === selected);
|
|
786
|
+
if (!adapter) return;
|
|
787
|
+
setChatEditorValue(node.chatInputCodeEditor, adapter.inputCode || "");
|
|
788
|
+
setChatEditorValue(node.chatOutputCodeEditor, adapter.outputCode || "");
|
|
789
|
+
hideAdapterMappingFields();
|
|
790
|
+
});
|
|
791
|
+
};
|
|
792
|
+
configureChatAdapterPicker.call(node);
|
|
793
|
+
|
|
794
|
+
const renderDetectedAdapters = (adapters) => {
|
|
795
|
+
const $list = $("#knx-ai-detected-adapters-list");
|
|
796
|
+
const $status = $("#knx-ai-detected-adapters-status");
|
|
797
|
+
if (!$list.length) return;
|
|
798
|
+
$list.empty();
|
|
799
|
+
const entries = Array.isArray(adapters) ? adapters : [];
|
|
800
|
+
if (!entries.length) {
|
|
801
|
+
$status.text(t("knxUltimateAI.messages.detectedAdaptersNone", "No compatible node detected.")).show();
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
$status.hide();
|
|
805
|
+
entries.forEach(function (adapter) {
|
|
806
|
+
if (!adapter || !adapter.id) return;
|
|
807
|
+
const providerCount = Number(adapter.providerCount) || 0;
|
|
808
|
+
const cameraCount = Number(adapter.cameraCount) || 0;
|
|
809
|
+
const nodeCount = Number(adapter.nodeCount) || 0;
|
|
810
|
+
const $row = $("<div></div>").addClass("knx-ai-detected-adapter");
|
|
811
|
+
const $main = $("<div></div>").addClass("knx-ai-detected-adapter-main");
|
|
812
|
+
$("<div></div>").text(adapter.title || adapter.id).appendTo($main);
|
|
813
|
+
if (adapter.packageName) {
|
|
814
|
+
$("<div></div>").addClass("knx-ai-detected-adapter-package").text(adapter.packageName).appendTo($main);
|
|
815
|
+
}
|
|
816
|
+
$("<div></div>")
|
|
817
|
+
.addClass("knx-ai-detected-adapter-counts")
|
|
818
|
+
.text(adapter.kind === "tts"
|
|
819
|
+
? t("knxUltimateAI.messages.detectedAdapterNodes", "Nodes") + ": " + nodeCount
|
|
820
|
+
: t("knxUltimateAI.messages.detectedAdapterControllers", "Controllers") + ": " + providerCount + " · " + t("knxUltimateAI.messages.detectedAdapterCameras", "Cameras") + ": " + cameraCount)
|
|
821
|
+
.appendTo($main);
|
|
822
|
+
$main.appendTo($row);
|
|
823
|
+
$("<span></span>")
|
|
824
|
+
.addClass("knx-ai-detected-adapter-badge")
|
|
825
|
+
.text(t("knxUltimateAI.messages.detectedAdapterDetected", "Detected"))
|
|
826
|
+
.appendTo($row);
|
|
827
|
+
$row.appendTo($list);
|
|
828
|
+
});
|
|
829
|
+
};
|
|
830
|
+
|
|
831
|
+
const renderSetupDoctor = (snapshot) => {
|
|
832
|
+
const $panel = $("#knx-ai-setup-doctor-panel");
|
|
833
|
+
const $status = $("#knx-ai-setup-doctor-status");
|
|
834
|
+
const $content = $("#knx-ai-setup-doctor-content");
|
|
835
|
+
const $checks = $("#knx-ai-setup-doctor-checks").empty();
|
|
836
|
+
const $prompts = $("#knx-ai-setup-doctor-prompt-list").empty();
|
|
837
|
+
if (!snapshot || typeof snapshot !== "object") {
|
|
838
|
+
$panel.removeClass("status-ready status-attention status-blocked");
|
|
839
|
+
$content.hide();
|
|
840
|
+
$status.text(t("knxUltimateAI.messages.setupDoctorUnavailable", "Setup Doctor is temporarily unavailable.")).show();
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
const badgeLabels = {
|
|
844
|
+
pass: t("knxUltimateAI.messages.setupDoctorPass", "OK"),
|
|
845
|
+
warn: t("knxUltimateAI.messages.setupDoctorWarn", "Check"),
|
|
846
|
+
fail: t("knxUltimateAI.messages.setupDoctorFail", "Fix"),
|
|
847
|
+
info: t("knxUltimateAI.messages.setupDoctorInfo", "Optional")
|
|
848
|
+
};
|
|
849
|
+
const snapshotStatus = ["ready", "attention", "blocked"].includes(String(snapshot.status)) ? String(snapshot.status) : "attention";
|
|
850
|
+
$panel.removeClass("status-ready status-attention status-blocked").addClass("status-" + snapshotStatus);
|
|
851
|
+
$("#knx-ai-setup-doctor-state").text(String(snapshot.statusLabel || snapshot.status || ""));
|
|
852
|
+
$("#knx-ai-setup-doctor-summary").text(String(snapshot.summary || ""));
|
|
853
|
+
$("#knx-ai-setup-doctor-score").text(String(Math.max(0, Number(snapshot.score) || 0)) + "%");
|
|
854
|
+
(Array.isArray(snapshot.checks) ? snapshot.checks : []).forEach(function (check) {
|
|
855
|
+
if (!check || !check.id) return;
|
|
856
|
+
const statusName = ["pass", "warn", "fail", "info"].includes(String(check.status)) ? String(check.status) : "info";
|
|
857
|
+
const $row = $("<div></div>").addClass("knx-ai-setup-doctor-check");
|
|
858
|
+
const $copy = $("<div></div>").addClass("knx-ai-setup-doctor-check-copy");
|
|
859
|
+
$("<strong></strong>").text(String(check.title || check.id)).appendTo($copy);
|
|
860
|
+
$("<div></div>").addClass("knx-ai-setup-doctor-check-detail").text(String(check.detail || "")).appendTo($copy);
|
|
861
|
+
$copy.appendTo($row);
|
|
862
|
+
$("<span></span>")
|
|
863
|
+
.addClass("knx-ai-setup-doctor-badge " + statusName)
|
|
864
|
+
.text(badgeLabels[statusName])
|
|
865
|
+
.appendTo($row);
|
|
866
|
+
$row.appendTo($checks);
|
|
867
|
+
});
|
|
868
|
+
const promptItems = snapshot.firstRun && Array.isArray(snapshot.firstRun.prompts) ? snapshot.firstRun.prompts : [];
|
|
869
|
+
promptItems.forEach(function (prompt) {
|
|
870
|
+
if (!prompt || !prompt.text) return;
|
|
871
|
+
$("<button type=\"button\"></button>")
|
|
872
|
+
.addClass("red-ui-button knx-ai-setup-doctor-prompt")
|
|
873
|
+
.text(String(prompt.text))
|
|
874
|
+
.on("click", function (evt) {
|
|
875
|
+
evt.preventDefault();
|
|
876
|
+
openKnxAiWebPage("cerebrum", String(prompt.text));
|
|
877
|
+
})
|
|
878
|
+
.appendTo($prompts);
|
|
879
|
+
});
|
|
880
|
+
$(".knx-ai-setup-doctor-prompts").toggle(promptItems.length > 0);
|
|
881
|
+
$status.hide();
|
|
882
|
+
$content.show();
|
|
883
|
+
};
|
|
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
|
+
|
|
900
|
+
const renderChatContextOverview = (overview) => {
|
|
901
|
+
const $status = $("#knx-ai-chat-context-status");
|
|
902
|
+
const $content = $("#knx-ai-chat-context-content");
|
|
903
|
+
const $sources = $("#knx-ai-chat-context-sources-list").empty();
|
|
904
|
+
const $files = $("#knx-ai-chat-context-files-list").empty();
|
|
905
|
+
const $directories = $("#knx-ai-chat-context-directories-list").empty();
|
|
906
|
+
const $contextLimit = $("#knx-ai-chat-context-limit");
|
|
907
|
+
const $lastPromptUsage = $("#knx-ai-chat-context-last-prompt");
|
|
908
|
+
if (!overview || typeof overview !== "object") {
|
|
909
|
+
lastChatContextOverview = null;
|
|
910
|
+
$content.hide();
|
|
911
|
+
$status
|
|
912
|
+
.text(t("knxUltimateAI.messages.chatContextUnavailable", "The chat context summary is temporarily unavailable."))
|
|
913
|
+
.show();
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
lastChatContextOverview = overview;
|
|
917
|
+
|
|
918
|
+
const addRow = function ($list, title, description, pathText, badge) {
|
|
919
|
+
const $row = $("<div></div>").addClass("knx-ai-detected-adapter");
|
|
920
|
+
const $main = $("<div></div>").addClass("knx-ai-detected-adapter-main");
|
|
921
|
+
$("<div></div>").text(title || "").appendTo($main);
|
|
922
|
+
if (description) {
|
|
923
|
+
$("<div></div>").addClass("knx-ai-chat-context-description").text(description).appendTo($main);
|
|
924
|
+
}
|
|
925
|
+
if (pathText) {
|
|
926
|
+
$("<div></div>").addClass("knx-ai-chat-context-path").text(pathText).appendTo($main);
|
|
927
|
+
}
|
|
928
|
+
$main.appendTo($row);
|
|
929
|
+
$("<span></span>").addClass("knx-ai-detected-adapter-badge").text(badge).appendTo($row);
|
|
930
|
+
$row.appendTo($list);
|
|
931
|
+
};
|
|
932
|
+
|
|
933
|
+
const formatBytes = function (value) {
|
|
934
|
+
const bytes = Math.max(0, Number(value) || 0);
|
|
935
|
+
if (bytes >= (1024 * 1024)) return (bytes / (1024 * 1024)).toLocaleString(undefined, { maximumFractionDigits: 1 }) + " MB";
|
|
936
|
+
if (bytes >= 1024) return (bytes / 1024).toLocaleString(undefined, { maximumFractionDigits: 1 }) + " KB";
|
|
937
|
+
return bytes.toLocaleString() + " B";
|
|
938
|
+
};
|
|
939
|
+
|
|
940
|
+
const contextLimit = overview.contextLimit && typeof overview.contextLimit === "object" ? overview.contextLimit : {};
|
|
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;
|
|
953
|
+
}
|
|
954
|
+
const contextLimitValue = contextLimitTokens
|
|
955
|
+
? formatCompactContextTokens(contextLimitTokens) + " " + t("knxUltimateAI.messages.chatContextTokens", "tokens")
|
|
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
|
+
: "";
|
|
960
|
+
$contextLimit
|
|
961
|
+
.text(t("knxUltimateAI.messages.chatContextLimitLabel", "Maximum operational context") + ": " + contextLimitValue + localBudgetValue)
|
|
962
|
+
.attr("title", contextLimitTokens ? contextLimitTokens.toLocaleString() + " tokens" : contextLimitValue);
|
|
963
|
+
|
|
964
|
+
const promptUsage = overview.lastPromptUsage && typeof overview.lastPromptUsage === "object" ? overview.lastPromptUsage : null;
|
|
965
|
+
if (promptUsage && Number(promptUsage.bytes) > 0) {
|
|
966
|
+
const exactInputTokens = Math.max(0, Number(promptUsage.exactInputTokens) || 0);
|
|
967
|
+
const estimatedInputTokens = Math.max(0, Number(promptUsage.estimatedInputTokens) || 0);
|
|
968
|
+
const tokenText = exactInputTokens
|
|
969
|
+
? formatCompactContextTokens(exactInputTokens) + " " + t("knxUltimateAI.messages.chatContextExactInputTokens", "input tokens measured by the provider")
|
|
970
|
+
: "~" + formatCompactContextTokens(estimatedInputTokens) + " " + t("knxUltimateAI.messages.chatContextEstimatedInputTokens", "estimated input tokens");
|
|
971
|
+
const imageText = Number(promptUsage.imageCount) > 0
|
|
972
|
+
? " · " + Number(promptUsage.imageCount).toLocaleString() + " " + t("knxUltimateAI.messages.chatContextImages", "images")
|
|
973
|
+
: "";
|
|
974
|
+
$lastPromptUsage.text(
|
|
975
|
+
t("knxUltimateAI.messages.chatContextLastPromptLabel", "Last chat prompt actual size") + ": " +
|
|
976
|
+
formatBytes(promptUsage.bytes) + " · " + tokenText + imageText
|
|
977
|
+
);
|
|
978
|
+
} else {
|
|
979
|
+
$lastPromptUsage.text(
|
|
980
|
+
t("knxUltimateAI.messages.chatContextLastPromptLabel", "Last chat prompt actual size") + ": " +
|
|
981
|
+
t("knxUltimateAI.messages.chatContextLastPromptUnavailable", "not available until the first chat request")
|
|
982
|
+
);
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
const sourceLabels = {
|
|
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."),
|
|
989
|
+
memoryEducation: t("knxUltimateAI.messages.chatContextSourceMemoryEducation", "Session context, AI Education, bounded home memory and active plans."),
|
|
990
|
+
cameras: t("knxUltimateAI.messages.chatContextSourceCameras", "Detected cameras and their available capabilities.")
|
|
991
|
+
};
|
|
992
|
+
(Array.isArray(overview.sources) ? overview.sources : []).forEach(function (sourceId) {
|
|
993
|
+
addRow(
|
|
994
|
+
$sources,
|
|
995
|
+
sourceLabels[sourceId] || String(sourceId),
|
|
996
|
+
"",
|
|
997
|
+
"",
|
|
998
|
+
t("knxUltimateAI.messages.chatContextSourceBadge", "Source")
|
|
999
|
+
);
|
|
1000
|
+
});
|
|
1001
|
+
|
|
1002
|
+
const fileDescriptions = {
|
|
1003
|
+
chatContext: t("knxUltimateAI.messages.chatContextFileChatContext", "Persistent conversation turns, instructions and camera notification rules."),
|
|
1004
|
+
homeMemory: t("knxUltimateAI.messages.chatContextFileHomeMemory", "Bounded learned home memory only; AI Education remains in the node property."),
|
|
1005
|
+
schedules: t("knxUltimateAI.messages.chatContextFileSchedules", "Authoritative structured plans and reminders for this node."),
|
|
1006
|
+
schedulesReadable: t("knxUltimateAI.messages.chatContextFileSchedulesReadable", "Human-readable plans and reminders generated from the scheduler state."),
|
|
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.")
|
|
1009
|
+
};
|
|
1010
|
+
(Array.isArray(overview.files) ? overview.files : []).forEach(function (item) {
|
|
1011
|
+
if (!item || !item.name || !item.path) return;
|
|
1012
|
+
addRow(
|
|
1013
|
+
$files,
|
|
1014
|
+
item.name,
|
|
1015
|
+
fileDescriptions[item.id] || "",
|
|
1016
|
+
item.path,
|
|
1017
|
+
t("knxUltimateAI.messages.chatContextFileBadge", "File")
|
|
1018
|
+
);
|
|
1019
|
+
});
|
|
1020
|
+
|
|
1021
|
+
const directoryLabels = {
|
|
1022
|
+
archiveRoot: t("knxUltimateAI.messages.chatContextDirectoryRoot", "Telegram archive root"),
|
|
1023
|
+
nodeArchive: t("knxUltimateAI.messages.chatContextDirectoryNode", "This node's telegram archive"),
|
|
1024
|
+
adapterArchiveRoot: t("knxUltimateAI.messages.chatContextDirectoryAdapterRoot", "Adapter event archive root"),
|
|
1025
|
+
adapterNodeArchive: t("knxUltimateAI.messages.chatContextDirectoryAdapterNode", "This node's adapter event archive")
|
|
1026
|
+
};
|
|
1027
|
+
(Array.isArray(overview.telegramDirectories) ? overview.telegramDirectories : []).forEach(function (item) {
|
|
1028
|
+
if (!item || !item.path) return;
|
|
1029
|
+
addRow(
|
|
1030
|
+
$directories,
|
|
1031
|
+
directoryLabels[item.id] || String(item.id || ""),
|
|
1032
|
+
"",
|
|
1033
|
+
item.path,
|
|
1034
|
+
/^adapter/.test(String(item.id || ""))
|
|
1035
|
+
? t("knxUltimateAI.messages.chatContextDirectoryAdapterBadge", "Adapter")
|
|
1036
|
+
: t("knxUltimateAI.messages.chatContextDirectoryBadge", "KNX")
|
|
1037
|
+
);
|
|
1038
|
+
});
|
|
1039
|
+
|
|
1040
|
+
$("#knx-ai-chat-context-pattern")
|
|
1041
|
+
.text(t("knxUltimateAI.messages.chatContextTelegramPattern", "Daily files") + ": " + String(overview.telegramFilePattern || "YYYY-MM-DD.knxctx"));
|
|
1042
|
+
$status.hide();
|
|
1043
|
+
$content.show();
|
|
1044
|
+
};
|
|
1045
|
+
const editorLanguage = String((RED.settings && RED.settings.lang) || (window.navigator && window.navigator.language) || "en");
|
|
1046
|
+
const loadKnxAiOverview = function (refreshSetup) {
|
|
1047
|
+
const $refresh = $("#knx-ai-setup-doctor-refresh");
|
|
1048
|
+
$refresh.prop("disabled", true).find("i.fa-refresh").addClass("fa-spin");
|
|
1049
|
+
const requestData = { language: editorLanguage };
|
|
1050
|
+
if (nodeId) requestData.nodeId = nodeId;
|
|
1051
|
+
if (refreshSetup) requestData.refreshSetup = "1";
|
|
1052
|
+
$.ajax({
|
|
1053
|
+
url: "knxUltimateAI/adapters",
|
|
1054
|
+
type: "GET",
|
|
1055
|
+
data: requestData
|
|
1056
|
+
})
|
|
1057
|
+
.done(function (data) {
|
|
1058
|
+
renderSetupDoctor(data && data.setupDoctor);
|
|
1059
|
+
renderDetectedAdapters(data && data.adapters);
|
|
1060
|
+
renderChatContextOverview(data && data.chatContext);
|
|
1061
|
+
})
|
|
1062
|
+
.fail(function () {
|
|
1063
|
+
renderSetupDoctor(null);
|
|
1064
|
+
$("#knx-ai-detected-adapters-status")
|
|
1065
|
+
.text(t("knxUltimateAI.messages.detectedAdaptersUnavailable", "Compatible-node detection is temporarily unavailable."))
|
|
1066
|
+
.show();
|
|
1067
|
+
renderChatContextOverview(null);
|
|
1068
|
+
})
|
|
1069
|
+
.always(function () {
|
|
1070
|
+
$refresh.prop("disabled", false).find("i.fa-refresh").removeClass("fa-spin");
|
|
1071
|
+
});
|
|
1072
|
+
};
|
|
1073
|
+
loadKnxAiOverview(false);
|
|
1074
|
+
$("#knx-ai-setup-doctor-refresh").on("click", function (evt) {
|
|
1075
|
+
evt.preventDefault();
|
|
1076
|
+
loadKnxAiOverview(true);
|
|
1077
|
+
});
|
|
1078
|
+
|
|
1079
|
+
const openKnxAiWebPage = function (view, prompt) {
|
|
1080
|
+
const adminRoot = resolveAdminRoot();
|
|
1081
|
+
const targetBase = adminRoot + "/knxUltimateAI/sidebar/page";
|
|
1082
|
+
const params = new URLSearchParams();
|
|
1083
|
+
if (nodeId) params.set("nodeId", nodeId);
|
|
1084
|
+
if (view === "chat-learning") {
|
|
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");
|
|
1093
|
+
if (prompt) params.set("prompt", String(prompt));
|
|
1094
|
+
}
|
|
1095
|
+
const accessToken = resolveAccessToken();
|
|
1096
|
+
if (accessToken) params.set("access_token", accessToken);
|
|
1097
|
+
const target = targetBase + (params.toString() ? ("?" + params.toString()) : "");
|
|
1098
|
+
const wnd = window.open(target, "_blank", "noopener,noreferrer");
|
|
1099
|
+
try { if (wnd && typeof wnd.focus === "function") wnd.focus(); } catch (e) { }
|
|
1100
|
+
};
|
|
1101
|
+
|
|
1102
|
+
$("#knx-ai-open-web-page-vue").on("click", function (evt) {
|
|
1103
|
+
evt.preventDefault();
|
|
1104
|
+
openKnxAiWebPage();
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1107
|
+
$("#knx-ai-open-chat-learning").on("click", function (evt) {
|
|
1108
|
+
evt.preventDefault();
|
|
1109
|
+
openKnxAiWebPage("chat-learning");
|
|
1110
|
+
});
|
|
1111
|
+
|
|
1112
|
+
$("#knx-ai-open-cerebrum-memory").on("click", function (evt) {
|
|
1113
|
+
evt.preventDefault();
|
|
1114
|
+
openKnxAiWebPage("cerebrum-memory");
|
|
1115
|
+
});
|
|
1116
|
+
|
|
1117
|
+
const setRefreshModelsBusy = (busy) => {
|
|
1118
|
+
const $button = $("#knx-ai-refreshModels");
|
|
1119
|
+
$button.prop("disabled", !!busy);
|
|
1120
|
+
$button.find("i.fa-refresh").toggleClass("fa-spin", !!busy);
|
|
1121
|
+
};
|
|
1122
|
+
|
|
1123
|
+
const setOllamaInstallUiState = (busy) => {
|
|
1124
|
+
$("#knx-ai-installOllamaModel").prop("disabled", !!busy);
|
|
1125
|
+
$("#knx-ai-downloadOllamaModel").prop("disabled", !!busy);
|
|
1126
|
+
};
|
|
1127
|
+
|
|
1128
|
+
let localModelDetailsById = new Map();
|
|
1129
|
+
let availableModelItems = [];
|
|
1130
|
+
let modelPickerMouseDown = false;
|
|
1131
|
+
let localContextRequestVersion = 0;
|
|
1132
|
+
|
|
1133
|
+
const formatContextTokens = (value) => {
|
|
1134
|
+
const tokens = Math.max(0, Number(value) || 0);
|
|
1135
|
+
return tokens ? tokens.toLocaleString() : "?";
|
|
1136
|
+
};
|
|
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
|
+
|
|
1153
|
+
const setLocalModelContextStatus = (detail, state, errorText) => {
|
|
1154
|
+
const $status = $("#knx-ai-local-context-status");
|
|
1155
|
+
const provider = $("#node-input-llmProvider").val();
|
|
1156
|
+
updateLocalContextTokenOptions(detail);
|
|
1157
|
+
if (!$status.length || (provider !== "lmstudio" && provider !== "ollama") || !detail) {
|
|
1158
|
+
$status.hide();
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
const maxTokens = formatContextTokens(detail.maxContextLength);
|
|
1162
|
+
const loadedTokens = formatContextTokens(detail.loadedContextLength);
|
|
1163
|
+
let text = t("knxUltimateAI.messages.lmStudioContextAvailable", "Maximum model context") + ": " + maxTokens + " token";
|
|
1164
|
+
let color = "#555";
|
|
1165
|
+
if (state === "loading") {
|
|
1166
|
+
text = t("knxUltimateAI.messages.lmStudioContextLoading", "Checking the active model context") + "…";
|
|
1167
|
+
color = "#1565c0";
|
|
1168
|
+
} else if (state === "inactive") {
|
|
1169
|
+
text = t("knxUltimateAI.messages.lmStudioContextInactive", "Model inactive; Bionic defaults will be used on the first request");
|
|
1170
|
+
color = "#555";
|
|
1171
|
+
} else if (state === "configured") {
|
|
1172
|
+
const configuredTokens = formatContextTokens(detail.configuredContextLength || detail.loadedContextLength || detail.maxContextLength);
|
|
1173
|
+
text = t("knxUltimateAI.messages.lmStudioContextConfigured", "Active model context") + ": " + configuredTokens + " token";
|
|
1174
|
+
color = "#2e7d32";
|
|
1175
|
+
} else if (state === "error") {
|
|
1176
|
+
text = String(errorText || t("knxUltimateAI.messages.lmStudioContextFailed", "Unable to configure the model context"));
|
|
1177
|
+
color = "#c62828";
|
|
1178
|
+
} else if (detail.loadedContextLength > 0) {
|
|
1179
|
+
text += " · " + t("knxUltimateAI.messages.lmStudioContextCurrentlyLoaded", "currently loaded") + ": " + loadedTokens + " token";
|
|
1180
|
+
}
|
|
1181
|
+
text += " · " + t("knxUltimateAI.messages.localContextBudget", "Local chat context") + ": " + selectedLocalContextLabel();
|
|
1182
|
+
$status.text(text).css("color", color).show();
|
|
1183
|
+
};
|
|
1184
|
+
|
|
1185
|
+
const populateModels = (models, modelDetails) => {
|
|
1186
|
+
localModelDetailsById = new Map();
|
|
1187
|
+
(modelDetails || []).forEach(function (detail) {
|
|
1188
|
+
if (detail && detail.id) localModelDetailsById.set(String(detail.id), detail);
|
|
1189
|
+
});
|
|
1190
|
+
availableModelItems = (models || []).map(function (m) {
|
|
1191
|
+
const detail = localModelDetailsById.get(String(m));
|
|
1192
|
+
const label = detail
|
|
1193
|
+
? String(detail.displayName || detail.id) + " · " + formatContextTokens(detail.maxContextLength) + " token"
|
|
1194
|
+
: String(m);
|
|
1195
|
+
return { label: label, value: String(m) };
|
|
1196
|
+
});
|
|
1197
|
+
};
|
|
1198
|
+
|
|
1199
|
+
const configureModelPicker = () => {
|
|
1200
|
+
const $model = $("#node-input-llmModel");
|
|
1201
|
+
if (!$model.length || typeof $model.autocomplete !== "function") return;
|
|
1202
|
+
$model.autocomplete({
|
|
1203
|
+
minLength: 0,
|
|
1204
|
+
source: function (request, response) {
|
|
1205
|
+
const term = String(request && request.term ? request.term : "").trim().toLowerCase();
|
|
1206
|
+
if (!term) {
|
|
1207
|
+
response(availableModelItems.slice());
|
|
1208
|
+
return;
|
|
1209
|
+
}
|
|
1210
|
+
response(availableModelItems.filter(function (item) {
|
|
1211
|
+
return item.value.toLowerCase().includes(term) || item.label.toLowerCase().includes(term);
|
|
1212
|
+
}));
|
|
1213
|
+
},
|
|
1214
|
+
select: function (event, ui) {
|
|
1215
|
+
$(this).val(ui.item.value).trigger("change");
|
|
1216
|
+
return false;
|
|
1217
|
+
}
|
|
1218
|
+
});
|
|
1219
|
+
$model.on("mousedown.knxUltimateAiModelPicker", function () {
|
|
1220
|
+
modelPickerMouseDown = true;
|
|
1221
|
+
}).on("focus.knxUltimateAiModelPicker", function () {
|
|
1222
|
+
if (!modelPickerMouseDown) $(this).autocomplete("search", "");
|
|
1223
|
+
}).on("click.knxUltimateAiModelPicker", function () {
|
|
1224
|
+
modelPickerMouseDown = false;
|
|
1225
|
+
const input = this;
|
|
1226
|
+
setTimeout(function () {
|
|
1227
|
+
try { $(input).autocomplete("search", ""); } catch (error) { }
|
|
1228
|
+
}, 0);
|
|
1229
|
+
}).on("blur.knxUltimateAiModelPicker", function () {
|
|
1230
|
+
modelPickerMouseDown = false;
|
|
1231
|
+
});
|
|
1232
|
+
};
|
|
1233
|
+
|
|
1234
|
+
configureModelPicker();
|
|
1235
|
+
|
|
1236
|
+
const normalizeUrl = (value) => String(value || "").trim().replace(/\/+$/, "").toLowerCase();
|
|
1237
|
+
const isOpenAiDefaultUrl = (value) => normalizeUrl(value) === normalizeUrl(OPENAI_COMPAT_DEFAULT_CHAT_URL);
|
|
1238
|
+
const isOllamaDefaultUrl = (value) => normalizeUrl(value) === normalizeUrl(OLLAMA_DEFAULT_CHAT_URL);
|
|
1239
|
+
const isOpenAiDefaultModel = (value) => OPENAI_COMPAT_DEFAULT_MODELS.indexOf(String(value || "").trim().toLowerCase()) >= 0;
|
|
1240
|
+
const DEFAULT_CHAT_URLS = [OPENAI_COMPAT_DEFAULT_CHAT_URL, OLLAMA_DEFAULT_CHAT_URL, ANTHROPIC_DEFAULT_CHAT_URL, LMSTUDIO_DEFAULT_CHAT_URL];
|
|
1241
|
+
const DEFAULT_MODELS = OPENAI_COMPAT_DEFAULT_MODELS.concat([OLLAMA_DEFAULT_MODEL, ANTHROPIC_DEFAULT_MODEL]);
|
|
1242
|
+
const isAnyDefaultUrl = (value) => DEFAULT_CHAT_URLS.some((u) => normalizeUrl(u) === normalizeUrl(value));
|
|
1243
|
+
const isAnyDefaultModel = (value) => DEFAULT_MODELS.indexOf(String(value || "").trim().toLowerCase()) >= 0;
|
|
1244
|
+
|
|
1245
|
+
const applyProviderDefaults = (provider) => {
|
|
1246
|
+
const $baseUrl = $("#node-input-llmBaseUrl");
|
|
1247
|
+
const $model = $("#node-input-llmModel");
|
|
1248
|
+
const currentBaseUrl = String($baseUrl.val() || "").trim();
|
|
1249
|
+
const currentModel = String($model.val() || "").trim();
|
|
1250
|
+
|
|
1251
|
+
const urlByProvider = { ollama: OLLAMA_DEFAULT_CHAT_URL, anthropic: ANTHROPIC_DEFAULT_CHAT_URL, lmstudio: LMSTUDIO_DEFAULT_CHAT_URL, openai_compat: OPENAI_COMPAT_DEFAULT_CHAT_URL };
|
|
1252
|
+
const modelByProvider = { ollama: OLLAMA_DEFAULT_MODEL, anthropic: ANTHROPIC_DEFAULT_MODEL, lmstudio: "", openai_compat: OPENAI_COMPAT_DEFAULT_MODELS[0] };
|
|
1253
|
+
const targetUrl = urlByProvider[provider] || OPENAI_COMPAT_DEFAULT_CHAT_URL;
|
|
1254
|
+
const targetModel = Object.prototype.hasOwnProperty.call(modelByProvider, provider)
|
|
1255
|
+
? modelByProvider[provider]
|
|
1256
|
+
: OPENAI_COMPAT_DEFAULT_MODELS[0];
|
|
1257
|
+
|
|
1258
|
+
// Only overwrite when the field is empty or still holds another provider's default,
|
|
1259
|
+
// so a user's custom base URL / model is never clobbered when switching providers.
|
|
1260
|
+
if (!currentBaseUrl || isAnyDefaultUrl(currentBaseUrl)) $baseUrl.val(targetUrl);
|
|
1261
|
+
if (!currentModel || isAnyDefaultModel(currentModel)) $model.val(targetModel);
|
|
1262
|
+
};
|
|
1263
|
+
|
|
1264
|
+
const configureSelectedLocalModel = () => {
|
|
1265
|
+
const provider = $("#node-input-llmProvider").val();
|
|
1266
|
+
if (provider !== "lmstudio" && provider !== "ollama") return;
|
|
1267
|
+
const model = String($("#node-input-llmModel").val() || "").trim();
|
|
1268
|
+
const detail = localModelDetailsById.get(model);
|
|
1269
|
+
if (!detail || !(Number(detail.maxContextLength) > 0)) {
|
|
1270
|
+
$("#node-input-llmContextLength").val(0);
|
|
1271
|
+
updateLocalContextTokenOptions(null);
|
|
1272
|
+
setLocalModelContextStatus(null);
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
const requestVersion = ++localContextRequestVersion;
|
|
1277
|
+
const activeContextLength = Number(detail.loadedContextLength) > 0
|
|
1278
|
+
? Number(detail.loadedContextLength)
|
|
1279
|
+
: Number(detail.maxContextLength);
|
|
1280
|
+
$("#node-input-llmContextLength").val(activeContextLength);
|
|
1281
|
+
if (provider === "ollama") {
|
|
1282
|
+
detail.configuredContextLength = Number(detail.maxContextLength);
|
|
1283
|
+
$("#node-input-llmContextLength").val(detail.configuredContextLength);
|
|
1284
|
+
setLocalModelContextStatus(detail);
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
setLocalModelContextStatus(detail, "loading");
|
|
1289
|
+
$("#node-input-llmModel").prop("disabled", true);
|
|
1290
|
+
$("#knx-ai-refreshModels").prop("disabled", true);
|
|
1291
|
+
|
|
1292
|
+
const payload = {
|
|
1293
|
+
nodeId: nodeId,
|
|
1294
|
+
baseUrl: $("#node-input-llmBaseUrl").val() || LMSTUDIO_DEFAULT_CHAT_URL,
|
|
1295
|
+
model: model,
|
|
1296
|
+
localContextTokens: selectedLocalContextTokens()
|
|
1297
|
+
};
|
|
1298
|
+
const apiKey = $("#node-input-llmApiKey").val() || "";
|
|
1299
|
+
if (apiKey && apiKey !== "__PWRD__") payload.apiKey = apiKey;
|
|
1300
|
+
|
|
1301
|
+
$.ajax({
|
|
1302
|
+
url: "knxUltimateAI/lmstudio/select-model",
|
|
1303
|
+
type: "POST",
|
|
1304
|
+
contentType: "application/json",
|
|
1305
|
+
data: JSON.stringify(payload)
|
|
1306
|
+
})
|
|
1307
|
+
.done(function (data) {
|
|
1308
|
+
if (requestVersion !== localContextRequestVersion) return;
|
|
1309
|
+
const resolvedContextLength = Math.max(0, Number(data && data.contextLength) || 0);
|
|
1310
|
+
detail.loadedContextLength = data && data.active === true ? resolvedContextLength : 0;
|
|
1311
|
+
detail.configuredContextLength = resolvedContextLength;
|
|
1312
|
+
$("#node-input-llmContextLength").val(resolvedContextLength);
|
|
1313
|
+
setLocalModelContextStatus(detail, data && data.active === true ? "configured" : "inactive");
|
|
1314
|
+
})
|
|
1315
|
+
.fail(function (xhr) {
|
|
1316
|
+
if (requestVersion !== localContextRequestVersion) return;
|
|
1317
|
+
let errorText = t("knxUltimateAI.messages.lmStudioContextFailed", "Unable to configure the model context");
|
|
1318
|
+
try {
|
|
1319
|
+
if (xhr && xhr.responseJSON && xhr.responseJSON.error) errorText = xhr.responseJSON.error;
|
|
1320
|
+
} catch (e) { }
|
|
1321
|
+
setLocalModelContextStatus(detail, "error", errorText);
|
|
1322
|
+
try { RED.notify(errorText, "error"); } catch (e) { }
|
|
1323
|
+
})
|
|
1324
|
+
.always(function () {
|
|
1325
|
+
if (requestVersion !== localContextRequestVersion) return;
|
|
1326
|
+
$("#node-input-llmModel").prop("disabled", false);
|
|
1327
|
+
$("#knx-ai-refreshModels").prop("disabled", false);
|
|
1328
|
+
});
|
|
1329
|
+
};
|
|
1330
|
+
|
|
1331
|
+
const refreshModels = () => {
|
|
1332
|
+
const provider = $("#node-input-llmProvider").val();
|
|
1333
|
+
const baseUrl = $("#node-input-llmBaseUrl").val() || "";
|
|
1334
|
+
const apiKey = $("#node-input-llmApiKey").val() || "";
|
|
1335
|
+
const payload = { nodeId: nodeId, provider: provider, baseUrl: baseUrl };
|
|
1336
|
+
if (provider === "ollama") payload.autoStart = true;
|
|
1337
|
+
// Node-RED uses "__PWRD__" as placeholder for stored credentials: don't send it.
|
|
1338
|
+
if (apiKey && apiKey !== "__PWRD__") payload.apiKey = apiKey;
|
|
1339
|
+
|
|
1340
|
+
setRefreshModelsBusy(true);
|
|
1341
|
+
|
|
1342
|
+
$.ajax({
|
|
1343
|
+
url: "knxUltimateAI/models",
|
|
1344
|
+
type: "POST",
|
|
1345
|
+
contentType: "application/json",
|
|
1346
|
+
data: JSON.stringify(payload)
|
|
1347
|
+
})
|
|
1348
|
+
.done(function (data) {
|
|
1349
|
+
const models = (data && data.models) ? data.models : [];
|
|
1350
|
+
populateModels(models, data && data.modelDetails);
|
|
1351
|
+
let selectedAutomatically = false;
|
|
1352
|
+
if (provider === "lmstudio" && models.length > 0 && !String($("#node-input-llmModel").val() || "").trim()) {
|
|
1353
|
+
$("#node-input-llmModel").val(models[0]);
|
|
1354
|
+
selectedAutomatically = true;
|
|
1355
|
+
}
|
|
1356
|
+
if (provider === "lmstudio" || provider === "ollama") {
|
|
1357
|
+
const selectedDetail = localModelDetailsById.get(String($("#node-input-llmModel").val() || ""));
|
|
1358
|
+
if (selectedDetail) {
|
|
1359
|
+
setLocalModelContextStatus(selectedDetail);
|
|
1360
|
+
configureSelectedLocalModel();
|
|
1361
|
+
} else if (selectedAutomatically) {
|
|
1362
|
+
configureSelectedLocalModel();
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
if (provider === "ollama" && data && data.ollamaStarted) {
|
|
1366
|
+
try {
|
|
1367
|
+
const startedMsg = t('knxUltimateAI.messages.ollamaStartedAuto', "Ollama server started automatically.");
|
|
1368
|
+
RED.notify(startedMsg, "success");
|
|
1369
|
+
} catch (e) { }
|
|
1370
|
+
}
|
|
1371
|
+
if (provider === "ollama" && models.length === 0) {
|
|
1372
|
+
const msg = t('knxUltimateAI.messages.ollamaNoModels', "No local Ollama model found. Install one from https://ollama.com/library.");
|
|
1373
|
+
try { RED.notify(msg, "warning"); } catch (e) { }
|
|
1374
|
+
}
|
|
1375
|
+
})
|
|
1376
|
+
.fail(function (xhr) {
|
|
1377
|
+
let err = "Failed to load models";
|
|
1378
|
+
try {
|
|
1379
|
+
const resp = xhr && xhr.responseJSON;
|
|
1380
|
+
if (resp && resp.error) err = resp.error;
|
|
1381
|
+
} catch (e) { }
|
|
1382
|
+
try { RED.notify(err, "error"); } catch (e) { }
|
|
1383
|
+
})
|
|
1384
|
+
.always(function () {
|
|
1385
|
+
setRefreshModelsBusy(false);
|
|
1386
|
+
});
|
|
1387
|
+
};
|
|
1388
|
+
|
|
1389
|
+
const installOllamaModel = () => {
|
|
1390
|
+
const provider = $("#node-input-llmProvider").val();
|
|
1391
|
+
if (provider !== "ollama") return;
|
|
1392
|
+
const baseUrl = $("#node-input-llmBaseUrl").val() || "";
|
|
1393
|
+
const model = String($("#node-input-llmModel").val() || "").trim() || OLLAMA_DEFAULT_MODEL;
|
|
1394
|
+
const payload = { nodeId: nodeId, baseUrl: baseUrl, model: model };
|
|
1395
|
+
|
|
1396
|
+
$("#knx-ai-refreshModels").prop("disabled", true);
|
|
1397
|
+
setOllamaInstallUiState(true);
|
|
1398
|
+
|
|
1399
|
+
$.ajax({
|
|
1400
|
+
url: "knxUltimateAI/ollama/pull",
|
|
1401
|
+
type: "POST",
|
|
1402
|
+
contentType: "application/json",
|
|
1403
|
+
data: JSON.stringify(payload)
|
|
1404
|
+
})
|
|
1405
|
+
.done(function (data) {
|
|
1406
|
+
try {
|
|
1407
|
+
const okMsg = t('knxUltimateAI.messages.installedOllamaModel', "Ollama model installed") + ": " + model;
|
|
1408
|
+
RED.notify(okMsg, "success");
|
|
1409
|
+
if (data && data.ollamaStarted) {
|
|
1410
|
+
const startedMsg = t('knxUltimateAI.messages.ollamaStartedAuto', "Ollama server started automatically.");
|
|
1411
|
+
RED.notify(startedMsg, "success");
|
|
1412
|
+
}
|
|
1413
|
+
} catch (e) { }
|
|
1414
|
+
refreshModels();
|
|
1415
|
+
})
|
|
1416
|
+
.fail(function (xhr) {
|
|
1417
|
+
let err = t('knxUltimateAI.messages.installOllamaModelFailed', "Failed to install Ollama model");
|
|
1418
|
+
try {
|
|
1419
|
+
const resp = xhr && xhr.responseJSON;
|
|
1420
|
+
if (resp && resp.error) err = resp.error;
|
|
1421
|
+
} catch (e) { }
|
|
1422
|
+
try { RED.notify(err, "error"); } catch (e) { }
|
|
1423
|
+
$("#knx-ai-refreshModels").prop("disabled", false);
|
|
1424
|
+
})
|
|
1425
|
+
.always(function () {
|
|
1426
|
+
setOllamaInstallUiState(false);
|
|
1427
|
+
});
|
|
1428
|
+
};
|
|
1429
|
+
|
|
1430
|
+
const openOllamaLibrary = () => {
|
|
1431
|
+
const wnd = window.open(OLLAMA_LIBRARY_URL, "_blank", "noopener,noreferrer");
|
|
1432
|
+
try { if (wnd && typeof wnd.focus === "function") wnd.focus(); } catch (e) { }
|
|
1433
|
+
};
|
|
1434
|
+
|
|
1435
|
+
const toggleProvider = ({ applyDefaults = false, autoLoadModels = false } = {}) => {
|
|
1436
|
+
const provider = $("#node-input-llmProvider").val();
|
|
1437
|
+
const localProvider = provider === "lmstudio" || provider === "ollama";
|
|
1438
|
+
if (applyDefaults) applyProviderDefaults(provider);
|
|
1439
|
+
$("#knx-ai-local-context-row").toggle(localProvider);
|
|
1440
|
+
if (localProvider) {
|
|
1441
|
+
updateLocalContextTokenOptions(localModelDetailsById.get(String($("#node-input-llmModel").val() || "").trim()));
|
|
1442
|
+
}
|
|
1443
|
+
if (provider === "ollama") {
|
|
1444
|
+
$("#knx-ai-apikey-row").hide();
|
|
1445
|
+
$("#knx-ai-ollama-install-row").show();
|
|
1446
|
+
if (autoLoadModels) refreshModels();
|
|
1447
|
+
} else {
|
|
1448
|
+
$("#knx-ai-apikey-row").show();
|
|
1449
|
+
$("#knx-ai-ollama-install-row").hide();
|
|
1450
|
+
if (provider === "lmstudio" && autoLoadModels) refreshModels();
|
|
1451
|
+
if (provider !== "lmstudio") {
|
|
1452
|
+
$("#node-input-llmContextLength").val(0);
|
|
1453
|
+
if (provider !== "ollama") $("#knx-ai-local-context-status").hide();
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
};
|
|
1457
|
+
const toggleWebAccess = () => {
|
|
1458
|
+
const enabled = $("#node-input-webAccessEnabled").prop("checked") === true;
|
|
1459
|
+
$("#knx-ai-web-access-settings").toggle(enabled);
|
|
1460
|
+
};
|
|
1461
|
+
$("#node-input-webAccessEnabled").on("change", toggleWebAccess);
|
|
1462
|
+
toggleWebAccess();
|
|
1463
|
+
$("#node-input-llmProvider").on("change", function () {
|
|
1464
|
+
toggleProvider({ applyDefaults: true, autoLoadModels: true });
|
|
1465
|
+
});
|
|
1466
|
+
toggleProvider({ applyDefaults: true });
|
|
1467
|
+
|
|
1468
|
+
$("#knx-ai-refreshModels").on("click", function (evt) {
|
|
1469
|
+
evt.preventDefault();
|
|
1470
|
+
refreshModels();
|
|
1471
|
+
});
|
|
1472
|
+
$("#node-input-llmModel").on("change", function () {
|
|
1473
|
+
configureSelectedLocalModel();
|
|
1474
|
+
});
|
|
1475
|
+
$("#node-input-llmLocalContextTokens").on("change", function () {
|
|
1476
|
+
const detail = localModelDetailsById.get(String($("#node-input-llmModel").val() || "").trim());
|
|
1477
|
+
updateLocalContextTokenOptions(detail);
|
|
1478
|
+
if (detail) setLocalModelContextStatus(detail, detail.loadedContextLength > 0 ? "configured" : undefined);
|
|
1479
|
+
});
|
|
1480
|
+
$("#knx-ai-downloadOllamaModel").on("click", function (evt) {
|
|
1481
|
+
evt.preventDefault();
|
|
1482
|
+
openOllamaLibrary();
|
|
1483
|
+
});
|
|
1484
|
+
$("#knx-ai-installOllamaModel").on("click", function (evt) {
|
|
1485
|
+
evt.preventDefault();
|
|
1486
|
+
installOllamaModel();
|
|
1487
|
+
});
|
|
1488
|
+
},
|
|
1489
|
+
oneditsave: function () {
|
|
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) { }
|
|
1503
|
+
try {
|
|
1504
|
+
if (this.chatInputCodeEditor && typeof this.chatInputCodeEditor.getValue === "function") {
|
|
1505
|
+
this.chatInputCode = this.chatInputCodeEditor.getValue();
|
|
1506
|
+
$("#node-input-chatInputCode").val(this.chatInputCode);
|
|
1507
|
+
}
|
|
1508
|
+
} catch (error) { }
|
|
1509
|
+
try {
|
|
1510
|
+
if (this.chatOutputCodeEditor && typeof this.chatOutputCodeEditor.getValue === "function") {
|
|
1511
|
+
this.chatOutputCode = this.chatOutputCodeEditor.getValue();
|
|
1512
|
+
$("#node-input-chatOutputCode").val(this.chatOutputCode);
|
|
1513
|
+
}
|
|
1514
|
+
} catch (error) { }
|
|
1515
|
+
try {
|
|
1516
|
+
if (typeof this._destroyKnxAiChatEditors === "function") this._destroyKnxAiChatEditors();
|
|
1517
|
+
} catch (error) { }
|
|
1518
|
+
delete this._destroyKnxAiChatEditors;
|
|
1519
|
+
},
|
|
1520
|
+
oneditcancel: function () {
|
|
1521
|
+
try { RED.sidebar.show("info"); } catch (error) { }
|
|
1522
|
+
try {
|
|
1523
|
+
if (typeof this._destroyKnxAiChatEditors === "function") this._destroyKnxAiChatEditors();
|
|
1524
|
+
} catch (error) { }
|
|
1525
|
+
delete this._destroyKnxAiChatEditors;
|
|
1526
|
+
},
|
|
1527
|
+
oneditresize: function () {
|
|
1528
|
+
try {
|
|
1529
|
+
if (this.chatInputCodeEditor && typeof this.chatInputCodeEditor.resize === "function") this.chatInputCodeEditor.resize();
|
|
1530
|
+
if (this.chatOutputCodeEditor && typeof this.chatOutputCodeEditor.resize === "function") this.chatOutputCodeEditor.resize();
|
|
1531
|
+
} catch (error) { }
|
|
1532
|
+
}
|
|
1533
|
+
})
|
|
1534
|
+
// The historical type remains registered so deployed flows keep loading,
|
|
1535
|
+
// but new Cerebrum nodes belong to node-red-contrib-cerebrum-ultimate.
|
|
1536
|
+
try { RED.palette.remove('knxUltimateAI'); } catch (error) { }
|
|
1537
|
+
</script>
|
|
1538
|
+
|
|
1539
|
+
<script type="text/html" data-template-name="knxUltimateAI">
|
|
1540
|
+
<div class="form-tips" style="margin-bottom:10px;"><b>Legacy node:</b> existing flow instances remain supported. For new installations use <a href="https://github.com/Supergiovane/node-red-contrib-cerebrum-ultimate#readme" target="_blank"><b>Cerebrum Ultimate</b></a>.</div>
|
|
1541
|
+
<div class="form-row" style="margin-bottom:10px;">
|
|
1542
|
+
<span style="color:#ff0000"><i class="fa fa-youtube"></i></span> <a target="_blank" href="https://www.youtube.com/playlist?list=PL9Yh1bjbLAYrU8PsVhW4xzEug2WtVFv3E"><b>KNX-Ultimate video tutorials (YouTube playlist)</b></a>
|
|
1543
|
+
</div>
|
|
1544
|
+
<b><span data-i18n="knxUltimateAI.title"></span></b>  <span style="color:#ff9800"     <i class="fa fa-youtube"></i></span><a
|
|
1545
|
+
target="_blank" href="https://www.youtube.com/watch?v=qw7kjQ_mvdg&t=10s">See sample video</a>
|
|
1546
|
+
<br /><br />
|
|
1547
|
+
<div class="form-row">
|
|
1548
|
+
|
|
1549
|
+
<label for="node-input-server"><i class="fa fa-tag"></i> <span data-i18n="knxUltimateAI.properties.server"></span></label>
|
|
1550
|
+
<input type="text" id="node-input-server">
|
|
1551
|
+
</div>
|
|
1552
|
+
|
|
1553
|
+
<div class="form-row">
|
|
1554
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="knxUltimateAI.properties.name"></span></label>
|
|
1555
|
+
<input type="text" id="node-input-name" data-i18n="[placeholder]knxUltimateAI.properties.name" style="flex:1 1 240px; min-width:240px; max-width:240px;">
|
|
1556
|
+
</div>
|
|
1557
|
+
|
|
1558
|
+
<div class="form-row">
|
|
1559
|
+
<label for="node-input-topic"><i class="fa fa-tasks"></i> <span data-i18n="knxUltimateAI.properties.topic"></span></label>
|
|
1560
|
+
<input type="text" id="node-input-topic" data-i18n="[placeholder]knxUltimateAI.properties.topic">
|
|
1561
|
+
</div>
|
|
1562
|
+
<div class="form-row">
|
|
1563
|
+
<label><i class="fa fa-external-link"></i> Web UI</label>
|
|
1564
|
+
<div style="display:flex; gap:8px; flex-wrap:wrap;">
|
|
1565
|
+
<button type="button" class="red-ui-button" id="knx-ai-open-web-page-vue" style="background-color:#ff9800; border-color:#ff9800; color:#ffffff !important; -webkit-text-fill-color:#ffffff;">
|
|
1566
|
+
<i class="fa fa-external-link" style="color:#ffffff !important;"></i> <span style="color:#ffffff !important;">Open Cerebrum Ultimate Web Page</span>
|
|
1567
|
+
</button>
|
|
1568
|
+
</div>
|
|
1569
|
+
</div>
|
|
1570
|
+
|
|
1571
|
+
<div id="knx-ai-tabs">
|
|
1572
|
+
<ul>
|
|
1573
|
+
<li><a href="#knx-ai-tabs-assistant"><i class="fa fa-magic"></i> <span data-i18n="knxUltimateAI.sections.groupAssistant"></span></a></li>
|
|
1574
|
+
<li><a href="#knx-ai-tabs-chat-home"><i class="fa fa-comments"></i> <span data-i18n="knxUltimateAI.sections.groupChatHome"></span></a></li>
|
|
1575
|
+
</ul>
|
|
1576
|
+
<div id="knx-ai-tabs-assistant">
|
|
1577
|
+
<div class="knx-ai-accordion-subsection">
|
|
1578
|
+
<h4><i class="fa fa-cog"></i> <span data-i18n="knxUltimateAI.sections.quickSetup"></span></h4>
|
|
1579
|
+
<div class="knx-ai-accordion-mount" id="knx-ai-mount-assistant-setup"></div>
|
|
1580
|
+
</div>
|
|
1581
|
+
<div class="knx-ai-accordion-subsection">
|
|
1582
|
+
<h4><i class="fa fa-list"></i> <span data-i18n="knxUltimateAI.sections.etsAccess"></span></h4>
|
|
1583
|
+
<div class="knx-ai-accordion-mount" id="knx-ai-mount-ets-access"></div>
|
|
1584
|
+
</div>
|
|
1585
|
+
<div class="knx-ai-accordion-subsection">
|
|
1586
|
+
<h4><i class="fa fa-sliders"></i> <span data-i18n="knxUltimateAI.sections.advanced"></span></h4>
|
|
1587
|
+
<div class="knx-ai-accordion-mount" id="knx-ai-mount-assistant-advanced"></div>
|
|
1588
|
+
</div>
|
|
1589
|
+
</div>
|
|
1590
|
+
|
|
1591
|
+
<div id="knx-ai-tabs-chat-home">
|
|
1592
|
+
<div class="knx-ai-accordion-subsection">
|
|
1593
|
+
<h4><i class="fa fa-plug"></i> <span data-i18n="knxUltimateAI.sections.chatAdapter"></span></h4>
|
|
1594
|
+
<div class="knx-ai-accordion-mount" id="knx-ai-mount-chat-adapter"></div>
|
|
1595
|
+
</div>
|
|
1596
|
+
<div class="knx-ai-accordion-subsection">
|
|
1597
|
+
<h4><i class="fa fa-home"></i> <span data-i18n="knxUltimateAI.sections.homeIntelligence"></span></h4>
|
|
1598
|
+
<div class="knx-ai-accordion-mount" id="knx-ai-mount-home-intelligence"></div>
|
|
1599
|
+
</div>
|
|
1600
|
+
<div id="knx-ai-chat-learning-link-panel">
|
|
1601
|
+
<h4><i class="fa fa-graduation-cap"></i> <span data-i18n="knxUltimateAI.sections.chatLearning"></span></h4>
|
|
1602
|
+
<p class="knx-ai-chat-learning-link-copy" data-i18n="knxUltimateAI.messages.chatLearningOpenHint"></p>
|
|
1603
|
+
<button type="button" class="red-ui-button" id="knx-ai-open-chat-learning">
|
|
1604
|
+
<i class="fa fa-external-link"></i> <span data-i18n="knxUltimateAI.buttons.openChatLearning"></span>
|
|
1605
|
+
</button>
|
|
1606
|
+
<h4 style="margin-top:14px;"><i class="fa fa-database"></i> <span data-i18n="knxUltimateAI.sections.cerebrumMemory"></span></h4>
|
|
1607
|
+
<p class="knx-ai-chat-learning-link-copy" data-i18n="knxUltimateAI.messages.cerebrumMemoryOpenHint"></p>
|
|
1608
|
+
<button type="button" class="red-ui-button" id="knx-ai-open-cerebrum-memory">
|
|
1609
|
+
<i class="fa fa-external-link"></i> <span data-i18n="knxUltimateAI.buttons.openCerebrumMemory"></span>
|
|
1610
|
+
</button>
|
|
1611
|
+
</div>
|
|
1612
|
+
<div id="knx-ai-detected-adapters-panel">
|
|
1613
|
+
<h4><i class="fa fa-puzzle-piece"></i> <span data-i18n="knxUltimateAI.sections.detectedAdapters"></span></h4>
|
|
1614
|
+
<div id="knx-ai-detected-adapters-status" data-i18n="knxUltimateAI.messages.detectedAdaptersLoading"></div>
|
|
1615
|
+
<div id="knx-ai-detected-adapters-list"></div>
|
|
1616
|
+
</div>
|
|
1617
|
+
<div id="knx-ai-chat-context-panel">
|
|
1618
|
+
<h4><i class="fa fa-database"></i> <span data-i18n="knxUltimateAI.sections.chatContextOverview"></span></h4>
|
|
1619
|
+
<div id="knx-ai-chat-context-status" data-i18n="knxUltimateAI.messages.chatContextLoading"></div>
|
|
1620
|
+
<div id="knx-ai-chat-context-content" style="display:none;">
|
|
1621
|
+
<div class="knx-ai-chat-context-intro" data-i18n="knxUltimateAI.messages.chatContextIntro"></div>
|
|
1622
|
+
<div id="knx-ai-chat-context-limit" class="knx-ai-chat-context-metric"></div>
|
|
1623
|
+
<div id="knx-ai-chat-context-last-prompt" class="knx-ai-chat-context-metric"></div>
|
|
1624
|
+
<div class="knx-ai-chat-context-group-title" data-i18n="knxUltimateAI.messages.chatContextSourcesTitle"></div>
|
|
1625
|
+
<div id="knx-ai-chat-context-sources-list"></div>
|
|
1626
|
+
<div class="knx-ai-chat-context-group-title" data-i18n="knxUltimateAI.messages.chatContextFilesTitle"></div>
|
|
1627
|
+
<div id="knx-ai-chat-context-files-list"></div>
|
|
1628
|
+
<div class="knx-ai-chat-context-group-title" data-i18n="knxUltimateAI.messages.chatContextDirectoriesTitle"></div>
|
|
1629
|
+
<div id="knx-ai-chat-context-directories-list"></div>
|
|
1630
|
+
<div id="knx-ai-chat-context-pattern" class="knx-ai-chat-context-pattern"></div>
|
|
1631
|
+
</div>
|
|
1632
|
+
</div>
|
|
1633
|
+
<details id="knx-ai-setup-doctor-panel">
|
|
1634
|
+
<summary class="knx-ai-setup-doctor-toggle">
|
|
1635
|
+
<span><i class="fa fa-magic"></i> <span data-i18n="knxUltimateAI.sections.setupDoctor"></span></span>
|
|
1636
|
+
<i class="fa fa-chevron-right knx-ai-setup-doctor-chevron" aria-hidden="true"></i>
|
|
1637
|
+
</summary>
|
|
1638
|
+
<div class="knx-ai-setup-doctor-body">
|
|
1639
|
+
<div id="knx-ai-setup-doctor-status" data-i18n="knxUltimateAI.messages.setupDoctorLoading"></div>
|
|
1640
|
+
<div id="knx-ai-setup-doctor-content" style="display:none;">
|
|
1641
|
+
<div class="knx-ai-setup-doctor-hero">
|
|
1642
|
+
<div class="knx-ai-setup-doctor-summary">
|
|
1643
|
+
<strong id="knx-ai-setup-doctor-state"></strong>
|
|
1644
|
+
<div id="knx-ai-setup-doctor-summary"></div>
|
|
1645
|
+
</div>
|
|
1646
|
+
<div id="knx-ai-setup-doctor-score" class="knx-ai-setup-doctor-score"></div>
|
|
1647
|
+
</div>
|
|
1648
|
+
<div id="knx-ai-setup-doctor-checks"></div>
|
|
1649
|
+
<div class="knx-ai-setup-doctor-prompts">
|
|
1650
|
+
<strong data-i18n="knxUltimateAI.messages.setupDoctorPromptsTitle"></strong>
|
|
1651
|
+
<div class="knx-ai-setup-doctor-prompts-hint" data-i18n="knxUltimateAI.messages.setupDoctorPromptsHint"></div>
|
|
1652
|
+
<div id="knx-ai-setup-doctor-prompt-list" class="knx-ai-setup-doctor-prompt-list"></div>
|
|
1653
|
+
</div>
|
|
1654
|
+
<div class="knx-ai-setup-doctor-footer">
|
|
1655
|
+
<div class="knx-ai-setup-doctor-deploy-hint">
|
|
1656
|
+
<i class="fa fa-info-circle"></i> <span data-i18n="knxUltimateAI.messages.setupDoctorDeployHint"></span>
|
|
1657
|
+
</div>
|
|
1658
|
+
<button type="button" class="red-ui-button" id="knx-ai-setup-doctor-refresh">
|
|
1659
|
+
<i class="fa fa-refresh"></i> <span data-i18n="knxUltimateAI.buttons.refreshSetupDoctor"></span>
|
|
1660
|
+
</button>
|
|
1661
|
+
</div>
|
|
1662
|
+
</div>
|
|
1663
|
+
</div>
|
|
1664
|
+
</details>
|
|
1665
|
+
</div>
|
|
1666
|
+
|
|
1667
|
+
</div>
|
|
1668
|
+
|
|
1669
|
+
<div id="knx-ai-accordion-source" style="display:none;">
|
|
1670
|
+
|
|
1671
|
+
<div id="knx-ai-tab-llm-connection">
|
|
1672
|
+
<div class="form-row">
|
|
1673
|
+
<input type="checkbox" id="node-input-llmEnabled" style="display:inline-block; width:auto; vertical-align:top;">
|
|
1674
|
+
<label style="width:auto" for="node-input-llmEnabled"> <span data-i18n="knxUltimateAI.properties.llmEnabled"></span></label>
|
|
1675
|
+
</div>
|
|
1676
|
+
|
|
1677
|
+
<div id="knx-ai-llm-connection-settings">
|
|
1678
|
+
<div class="form-row">
|
|
1679
|
+
<label style="width:290px" for="node-input-llmProvider"><i class="fa fa-cog"></i> <span data-i18n="knxUltimateAI.properties.llmProvider"></span></label>
|
|
1680
|
+
<select style="width:100%" id="node-input-llmProvider">
|
|
1681
|
+
<option value="openai_compat" data-i18n="knxUltimateAI.selectlists.llmProvider.openai_compat"></option>
|
|
1682
|
+
<option value="anthropic" data-i18n="knxUltimateAI.selectlists.llmProvider.anthropic"></option>
|
|
1683
|
+
<option value="ollama" data-i18n="knxUltimateAI.selectlists.llmProvider.ollama"></option>
|
|
1684
|
+
<option value="lmstudio" data-i18n="knxUltimateAI.selectlists.llmProvider.lmstudio"></option>
|
|
1685
|
+
</select>
|
|
1686
|
+
</div>
|
|
1687
|
+
<div class="form-row" id="knx-ai-apikey-row">
|
|
1688
|
+
<label style="width:290px" for="node-input-llmApiKey"><i class="fa fa-key"></i> <span data-i18n="knxUltimateAI.properties.llmApiKey"></span></label>
|
|
1689
|
+
<input type="password" id="node-input-llmApiKey" data-i18n="[placeholder]knxUltimateAI.placeholder.llmApiKey">
|
|
1690
|
+
</div>
|
|
1691
|
+
|
|
1692
|
+
<div class="form-row">
|
|
1693
|
+
<label style="width:290px" for="node-input-llmModel"><i class="fa fa-cube"></i> <span data-i18n="knxUltimateAI.properties.llmModel"></span></label>
|
|
1694
|
+
<input style="width:calc(100% - 110px)" type="text" id="node-input-llmModel" autocomplete="off" data-i18n="[placeholder]knxUltimateAI.placeholder.llmModel">
|
|
1695
|
+
<button type="button" class="red-ui-button" id="knx-ai-refreshModels" style="margin-left:6px;" title="Refresh models">
|
|
1696
|
+
<i class="fa fa-refresh"></i> <span data-i18n="knxUltimateAI.buttons.refreshModels"></span>
|
|
1697
|
+
</button>
|
|
1698
|
+
</div>
|
|
1699
|
+
<div class="form-row">
|
|
1700
|
+
<label style="width:290px" for="node-input-llmReasoningEffort"><i class="fa fa-tachometer"></i> <span data-i18n="knxUltimateAI.properties.llmReasoningEffort"></span></label>
|
|
1701
|
+
<select style="width:100%" id="node-input-llmReasoningEffort">
|
|
1702
|
+
<option value="default" data-i18n="knxUltimateAI.selectlists.reasoningEffort.default"></option>
|
|
1703
|
+
<option value="none" data-i18n="knxUltimateAI.selectlists.reasoningEffort.none"></option>
|
|
1704
|
+
<option value="minimal" data-i18n="knxUltimateAI.selectlists.reasoningEffort.minimal"></option>
|
|
1705
|
+
<option value="low" data-i18n="knxUltimateAI.selectlists.reasoningEffort.low"></option>
|
|
1706
|
+
<option value="medium" data-i18n="knxUltimateAI.selectlists.reasoningEffort.medium"></option>
|
|
1707
|
+
<option value="high" data-i18n="knxUltimateAI.selectlists.reasoningEffort.high"></option>
|
|
1708
|
+
<option value="xhigh" data-i18n="knxUltimateAI.selectlists.reasoningEffort.xhigh"></option>
|
|
1709
|
+
<option value="max" data-i18n="knxUltimateAI.selectlists.reasoningEffort.max"></option>
|
|
1710
|
+
</select>
|
|
1711
|
+
<div class="knx-ai-prompt-context-hint" data-i18n="knxUltimateAI.messages.reasoningEffortHint"></div>
|
|
1712
|
+
</div>
|
|
1713
|
+
<input type="hidden" id="node-input-llmContextLength">
|
|
1714
|
+
<div id="knx-ai-local-context-status" style="display:none;"></div>
|
|
1715
|
+
<div class="form-row" id="knx-ai-local-context-row" style="display:none;">
|
|
1716
|
+
<label style="width:290px" for="node-input-llmLocalContextTokens"><i class="fa fa-database"></i> <span data-i18n="knxUltimateAI.properties.llmLocalContextTokens"></span></label>
|
|
1717
|
+
<select style="width:100%" id="node-input-llmLocalContextTokens">
|
|
1718
|
+
<option value="0" data-i18n="knxUltimateAI.selectlists.localContext.maximum"></option>
|
|
1719
|
+
<option value="4096" data-context-tokens="4096" data-i18n="knxUltimateAI.selectlists.localContext.k4"></option>
|
|
1720
|
+
<option value="8192" data-context-tokens="8192" data-i18n="knxUltimateAI.selectlists.localContext.k8"></option>
|
|
1721
|
+
<option value="16384" data-context-tokens="16384" data-i18n="knxUltimateAI.selectlists.localContext.k16"></option>
|
|
1722
|
+
<option value="32768" data-context-tokens="32768" data-i18n="knxUltimateAI.selectlists.localContext.k32"></option>
|
|
1723
|
+
<option value="65536" data-context-tokens="65536" data-i18n="knxUltimateAI.selectlists.localContext.k64"></option>
|
|
1724
|
+
<option value="131072" data-context-tokens="131072" data-i18n="knxUltimateAI.selectlists.localContext.k128"></option>
|
|
1725
|
+
<option value="262144" data-context-tokens="262144" data-i18n="knxUltimateAI.selectlists.localContext.k256"></option>
|
|
1726
|
+
</select>
|
|
1727
|
+
<div class="knx-ai-prompt-context-hint" data-i18n="knxUltimateAI.messages.localContextHint"></div>
|
|
1728
|
+
</div>
|
|
1729
|
+
<div class="form-row">
|
|
1730
|
+
<input type="checkbox" id="node-input-llmAllowKnxCommands" style="display:inline-block; width:auto; vertical-align:top;">
|
|
1731
|
+
<label style="width:auto" for="node-input-llmAllowKnxCommands"> <span data-i18n="knxUltimateAI.properties.llmAllowKnxCommands"></span></label>
|
|
1732
|
+
</div>
|
|
1733
|
+
<div class="form-row" id="knx-ai-command-confirmation-row">
|
|
1734
|
+
<input type="checkbox" id="node-input-llmRequireCommandConfirmation" style="display:inline-block; width:auto; vertical-align:top;">
|
|
1735
|
+
<label style="width:auto" for="node-input-llmRequireCommandConfirmation"> <span data-i18n="knxUltimateAI.properties.llmRequireCommandConfirmation"></span></label>
|
|
1736
|
+
</div>
|
|
1737
|
+
</div>
|
|
1738
|
+
</div>
|
|
1739
|
+
|
|
1740
|
+
<div id="knx-ai-tab-chat-adapter">
|
|
1741
|
+
<div class="form-row">
|
|
1742
|
+
<label style="width:290px" for="node-input-chatAdapterPreset"><i class="fa fa-plug"></i> <span data-i18n="knxUltimateAI.properties.chatAdapterPreset"></span></label>
|
|
1743
|
+
<select style="width:100%" id="node-input-chatAdapterPreset">
|
|
1744
|
+
<option value="none" data-i18n="knxUltimateAI.selectlists.chatAdapter.none"></option>
|
|
1745
|
+
</select>
|
|
1746
|
+
</div>
|
|
1747
|
+
<div id="knx-ai-chat-adapter-fields" style="display:none;">
|
|
1748
|
+
<div class="form-row">
|
|
1749
|
+
<label style="width:100%" for="node-input-chatInputCode"><i class="fa fa-sign-in"></i> <span data-i18n="knxUltimateAI.properties.chatInputCode"></span></label>
|
|
1750
|
+
<input type="hidden" id="node-input-chatInputCode">
|
|
1751
|
+
<div id="node-input-chatInputCode-editor" class="node-text-editor knx-ai-chat-adapter-editor"></div>
|
|
1752
|
+
</div>
|
|
1753
|
+
<div class="form-row">
|
|
1754
|
+
<label style="width:100%" for="node-input-chatOutputCode"><i class="fa fa-sign-out"></i> <span data-i18n="knxUltimateAI.properties.chatOutputCode"></span></label>
|
|
1755
|
+
<input type="hidden" id="node-input-chatOutputCode">
|
|
1756
|
+
<div id="node-input-chatOutputCode-editor" class="node-text-editor knx-ai-chat-adapter-editor"></div>
|
|
1757
|
+
</div>
|
|
1758
|
+
</div>
|
|
1759
|
+
</div>
|
|
1760
|
+
|
|
1761
|
+
<div id="knx-ai-tab-home-intelligence">
|
|
1762
|
+
<div class="form-row">
|
|
1763
|
+
<strong><i class="fa fa-globe"></i> <span data-i18n="knxUltimateAI.sections.webIntelligence"></span></strong>
|
|
1764
|
+
</div>
|
|
1765
|
+
<div class="form-row">
|
|
1766
|
+
<input type="checkbox" id="node-input-webAccessEnabled" style="display:inline-block; width:auto; vertical-align:top;">
|
|
1767
|
+
<label style="width:auto" for="node-input-webAccessEnabled"> <span data-i18n="knxUltimateAI.properties.webAccessEnabled"></span></label>
|
|
1768
|
+
<!-- <div class="knx-ai-prompt-context-hint" data-i18n="knxUltimateAI.messages.webAccessHint"></div> -->
|
|
1769
|
+
</div>
|
|
1770
|
+
<div id="knx-ai-web-access-settings">
|
|
1771
|
+
<div class="form-row">
|
|
1772
|
+
<label style="width:290px" for="node-input-webMaxCallsPerHour"><i class="fa fa-tachometer"></i> <span data-i18n="knxUltimateAI.properties.webMaxCallsPerHour"></span></label>
|
|
1773
|
+
<input type="number" id="node-input-webMaxCallsPerHour" min="1" max="60" step="1" style="width:100%;">
|
|
1774
|
+
<!-- <div class="knx-ai-prompt-context-hint" data-i18n="knxUltimateAI.messages.webBudgetHint"></div> -->
|
|
1775
|
+
</div>
|
|
1776
|
+
</div>
|
|
1777
|
+
<hr>
|
|
1778
|
+
<div class="form-row">
|
|
1779
|
+
<label style="width:100%" for="node-input-aiEducation"><i class="fa fa-graduation-cap"></i> <span data-i18n="knxUltimateAI.properties.aiEducation"></span></label>
|
|
1780
|
+
<textarea id="node-input-aiEducation" maxlength="16000" style="width:100%; min-height:180px; box-sizing:border-box;" data-i18n="[placeholder]knxUltimateAI.placeholder.aiEducation"></textarea>
|
|
1781
|
+
</div>
|
|
1782
|
+
</div>
|
|
1783
|
+
|
|
1784
|
+
<div id="knx-ai-tab-ets-access">
|
|
1785
|
+
<div class="form-row">
|
|
1786
|
+
<div class="knx-ai-prompt-context-hint" data-i18n="knxUltimateAI.messages.etsAccessHint"></div>
|
|
1787
|
+
<div style="margin:8px 0 4px; display:flex; gap:6px; align-items:center;">
|
|
1788
|
+
<input type="text" id="knx-ai-ets-ga-filter" data-i18n="[placeholder]knxUltimateAI.messages.etsFilterPlaceholder" style="flex:1 1 auto;">
|
|
1789
|
+
<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>
|
|
1790
|
+
<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>
|
|
1791
|
+
<span style="border-left:1px solid #ccc; align-self:stretch;"></span>
|
|
1792
|
+
<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>
|
|
1793
|
+
<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>
|
|
1794
|
+
</div>
|
|
1795
|
+
<div id="knx-ai-ets-ga-count" style="font-size:11px; color:#666; margin:2px 0;"></div>
|
|
1796
|
+
<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>
|
|
1797
|
+
</div>
|
|
1798
|
+
</div>
|
|
1799
|
+
|
|
1800
|
+
<div id="knx-ai-tab-advanced">
|
|
1801
|
+
<div id="knx-ai-llm-advanced-settings">
|
|
1802
|
+
<div class="form-row">
|
|
1803
|
+
<label style="width:290px" for="node-input-llmBaseUrl"><i class="fa fa-link"></i> <span data-i18n="knxUltimateAI.properties.llmBaseUrl"></span></label>
|
|
1804
|
+
<input type="text" id="node-input-llmBaseUrl" data-i18n="[placeholder]knxUltimateAI.placeholder.llmBaseUrl">
|
|
1805
|
+
</div>
|
|
1806
|
+
<div class="form-row" id="knx-ai-ollama-install-row" style="display:none;">
|
|
1807
|
+
<label style="width:290px"><i class="fa fa-download"></i> Ollama</label>
|
|
1808
|
+
<div class="knx-ai-ollama-actions">
|
|
1809
|
+
<button type="button" class="red-ui-button" id="knx-ai-downloadOllamaModel">
|
|
1810
|
+
<i class="fa fa-cloud-download"></i> <span data-i18n="knxUltimateAI.buttons.downloadOllamaModel"></span>
|
|
1811
|
+
</button>
|
|
1812
|
+
<button type="button" class="red-ui-button" id="knx-ai-installOllamaModel">
|
|
1813
|
+
<i class="fa fa-download"></i> <span data-i18n="knxUltimateAI.buttons.installOllamaModel"></span>
|
|
1814
|
+
</button>
|
|
1815
|
+
</div>
|
|
1816
|
+
</div>
|
|
1817
|
+
</div>
|
|
1818
|
+
</div>
|
|
1819
|
+
</div>
|
|
1820
|
+
</script>
|