x-components + mcp wip
frdel committed
Jun 2, 2025 at 09:48 UTC
599f8481e9e8981098a7ee956fca4eaca6b05049
18 files changed
+1189
-109
.vscode/settings.json
+11
@@ -2,4 +2,15 @@
2
"python.analysis.typeCheckingMode": "standard",
3
"windsurfPyright.analysis.diagnosticMode": "workspace",
4
"windsurfPyright.analysis.typeCheckingMode": "standard",
5
+ // Enable JavaScript linting
6
+ "eslint.enable": true,
7
+ "eslint.validate": ["javascript", "javascriptreact"],
8
+ // Set import root for JS/TS
9
+ "javascript.preferences.importModuleSpecifier": "relative",
10
+ "js/ts.implicitProjectConfig.checkJs": true,
11
+ "jsconfig.paths": {
12
+ "*": ["webui/*"]
13
+ },
14
+ // Optional: point VSCode to jsconfig.json if you add one
15
+ "jsconfig.json": "${workspaceFolder}/jsconfig.json"
16
}
\ No newline at end of file
jsconfig.json
new
+9
@@ -0,0 +1,9 @@
1
+{
2
+ "compilerOptions": {
3
+ "baseUrl": ".",
4
+ "paths": {
5
+ "*": ["webui/*"]
6
+ }
7
+ },
8
+ "include": ["webui/**/*.js"]
9
+ }
\ No newline at end of file
python/api/mcp_servers_apply.py
new
+17
@@ -0,0 +1,17 @@
1
+from python.helpers.api import ApiHandler
2
+from flask import Request, Response
3
+
4
+from typing import Any
5
+
6
+from python.helpers.mcp_handler import MCPConfig
7
+
8
+
9
+class McpServersApply(ApiHandler):
10
+ async def process(self, input: dict[Any, Any], request: Request) -> dict[Any, Any] | Response:
11
+ mcp_servers = input["mcp_servers"]
12
+ mcp_config = MCPConfig.get_instance()
13
+ try:
14
+ MCPConfig.update(mcp_servers)
15
+ except Exception as e:
16
+ return {"success": False, "error": str(e)}
17
+ return {"success": True}
python/helpers/settings.py
+54
-18
@@ -10,7 +10,6 @@ from . import files, dotenv
10
from python.helpers.print_style import PrintStyle
11
12
13
-
13
class Settings(TypedDict):
14
chat_model_provider: str
15
chat_model_name: str
@@ -66,7 +65,6 @@ class Settings(TypedDict):
65
stt_waiting_timeout: int
66
67
mcp_server_enabled: bool
69
-
68
69
70
class PartialSettings(Settings, total=False):
@@ -82,11 +80,14 @@ class SettingsField(TypedDict, total=False):
80
id: str
81
title: str
82
description: str
85
- type: Literal["text", "number", "select", "range", "textarea", "password", "switch"]
83
+ type: Literal[
84
+ "text", "number", "select", "range", "textarea", "password", "switch", "button"
85
+ ]
86
value: Any
87
min: float
88
max: float
89
step: float
90
+ hidden: bool
91
options: list[FieldOption]
92
93
@@ -465,9 +466,7 @@ def convert_out(settings: Settings) -> SettingsOutput:
466
api_keys_fields.append(
467
_get_api_key_field(settings, "anthropic", "Anthropic API Key")
468
)
468
- api_keys_fields.append(
469
- _get_api_key_field(settings, "chutes", "Chutes API Key")
470
- )
469
+ api_keys_fields.append(_get_api_key_field(settings, "chutes", "Chutes API Key"))
470
api_keys_fields.append(_get_api_key_field(settings, "deepseek", "DeepSeek API Key"))
471
api_keys_fields.append(_get_api_key_field(settings, "google", "Google API Key"))
472
api_keys_fields.append(_get_api_key_field(settings, "groq", "Groq API Key"))
@@ -683,10 +682,19 @@ def convert_out(settings: Settings) -> SettingsOutput:
682
"tab": "agent",
683
}
684
686
-
685
# MCP section
686
mcp_client_fields: list[SettingsField] = []
687
688
+ mcp_client_fields.append(
689
+ {
690
+ "id": "mcp_servers_config",
691
+ "title": "MCP Servers Configuration",
692
+ "description": "External MCP servers can be configured here.",
693
+ "type": "button",
694
+ "value": "Open",
695
+ }
696
+ )
697
+
698
mcp_client_fields.append(
699
{
700
"id": "mcp_servers",
@@ -694,6 +702,7 @@ def convert_out(settings: Settings) -> SettingsOutput:
702
"description": "(JSON list of) >> RemoteServer <<: [name, url, headers, timeout (opt), sse_read_timeout (opt), disabled (opt)] / >> Local Server <<: [name, command, args, env, encoding (opt), encoding_error_handler (opt), disabled (opt)]",
703
"type": "textarea",
704
"value": settings["mcp_servers"],
705
+ "hidden": True,
706
}
707
)
708
@@ -725,7 +734,6 @@ def convert_out(settings: Settings) -> SettingsOutput:
734
"tab": "mcp",
735
}
736
728
-
737
# Add the section to the result
738
result: SettingsOutput = {
739
"sections": [
@@ -789,9 +797,22 @@ def set_settings(settings: Settings):
797
_apply_settings(previous)
798
799
800
+def set_settings_delta(delta: dict):
801
+ current = get_settings()
802
+ new = {**current, **delta}
803
+ set_settings(new) # type: ignore
804
+
805
+
806
def normalize_settings(settings: Settings) -> Settings:
807
copy = settings.copy()
808
default = get_default_settings()
809
+
810
+ # remove keys that are not in default
811
+ keys_to_remove = [key for key in copy if key not in default]
812
+ for key in keys_to_remove:
813
+ del copy[key]
814
+
815
+ # add missing keys and normalize types
816
for key, value in default.items():
817
if key not in copy:
818
copy[key] = value
@@ -800,6 +821,7 @@ def normalize_settings(settings: Settings) -> Settings:
821
copy[key] = type(value)(copy[key]) # type: ignore
822
except (ValueError, TypeError):
823
copy[key] = value # make default instead
824
+
825
return copy
826
827
@@ -922,41 +944,55 @@ def _apply_settings(previous: Settings | None):
944
or _settings["embed_model_kwargs"] != previous["embed_model_kwargs"]
945
):
946
from python.helpers.memory import reload as memory_reload
947
+
948
memory_reload()
949
950
# update mcp settings if necessary
951
from python.helpers.mcp_handler import MCPConfig
952
953
async def update_mcp_settings(mcp_servers: str):
931
- PrintStyle(background_color="black", font_color="white", padding=True).print("Updating MCP config...")
954
+ PrintStyle(
955
+ background_color="black", font_color="white", padding=True
956
+ ).print("Updating MCP config...")
957
first_context = AgentContext.first()
958
if first_context:
934
- first_context.log.log(type="info", content="Updating MCP settings...", temp=True)
959
+ first_context.log.log(
960
+ type="info", content="Updating MCP settings...", temp=True
961
+ )
962
963
mcp_config = MCPConfig.get_instance()
964
try:
965
MCPConfig.update(mcp_servers)
966
except Exception as e:
967
if first_context:
941
- first_context.log.log(type="warning", content=f"Failed to update MCP settings: {e}", temp=False)
968
+ first_context.log.log(
969
+ type="warning",
970
+ content=f"Failed to update MCP settings: {e}",
971
+ temp=False,
972
+ )
973
(
943
- PrintStyle(background_color="red", font_color="black", padding=True)
944
- .print("Failed to update MCP settings")
974
+ PrintStyle(
975
+ background_color="red", font_color="black", padding=True
976
+ ).print("Failed to update MCP settings")
977
)
978
(
947
- PrintStyle(background_color="black", font_color="red", padding=True)
948
- .print(f"{e}")
979
+ PrintStyle(
980
+ background_color="black", font_color="red", padding=True
981
+ ).print(f"{e}")
982
)
983
984
PrintStyle(
985
background_color="#6734C3", font_color="white", padding=True
986
).print("Parsed MCP config:")
987
(
955
- PrintStyle(background_color="#334455", font_color="white", padding=False)
956
- .print(mcp_config.model_dump_json())
988
+ PrintStyle(
989
+ background_color="#334455", font_color="white", padding=False
990
+ ).print(mcp_config.model_dump_json())
991
)
992
if first_context:
959
- first_context.log.log(type="info", content="Finished updating MCP settings :)", temp=True)
993
+ first_context.log.log(
994
+ type="info", content="Finished updating MCP settings :)", temp=True
995
+ )
996
997
task2 = defer.DeferredTask().start_task(
998
update_mcp_settings, config.mcp_servers
webui/components/settings/mcp/client/mcp-servers-store.js
new
+75
@@ -0,0 +1,75 @@
1
+import { createStore } from "/js/AlpineStore.js";
2
+import { scrollModal } from "/js/modals.js";
3
+import sleep from "/js/sleep.js";
4
+
5
+const model = {
6
+ editor: null,
7
+ settingsJsonField: settingsModalProxy.settings.sections
8
+ .filter((x) => x.id == "mcp_client")[0]
9
+ .fields.filter((x) => x.id == "mcp_servers")[0],
10
+ servers: [
11
+ { connected: true, name: "Server 1", tools: 10 },
12
+ { connected: false, name: "Server 2", tools: 0, error: "Server is disabled in configuration" },
13
+ ],
14
+ loading: false,
15
+
16
+ async initialize() {
17
+ // Initialize the JSON Viewer after the modal is rendered
18
+ const container = document.getElementById("mcp-servers-config-json");
19
+ if (container) {
20
+ const editor = ace.edit("mcp-servers-config-json");
21
+
22
+ const dark = localStorage.getItem("darkMode");
23
+ if (dark != "false") {
24
+ editor.setTheme("ace/theme/github_dark");
25
+ } else {
26
+ editor.setTheme("ace/theme/tomorrow");
27
+ }
28
+
29
+ editor.session.setMode("ace/mode/json");
30
+ const json = this.settingsJsonField.value;
31
+ editor.setValue(json);
32
+ editor.clearSelection();
33
+ this.editor = editor;
34
+ }
35
+ },
36
+
37
+ formatJson() {
38
+ try {
39
+ // get current content
40
+ const currentContent = this.editor.getValue();
41
+
42
+ // parse and format with 2 spaces indentation
43
+ const parsed = JSON.parse(currentContent);
44
+ const formatted = JSON.stringify(parsed, null, 2);
45
+
46
+ // update editor content
47
+ this.editor.setValue(formatted);
48
+ this.editor.clearSelection();
49
+
50
+ // move cursor to start
51
+ this.editor.navigateFileStart();
52
+ } catch (error) {
53
+ console.error("Failed to format JSON:", error);
54
+ alert("Invalid JSON: " + error.message);
55
+ }
56
+ },
57
+
58
+ onClose() {
59
+ const val = this.editor.getValue();
60
+ this.settingsJsonField.value = val;
61
+ },
62
+
63
+ async applyNow() {
64
+ if (this.loading) return;
65
+ this.loading = true;
66
+ scrollModal("mcp-servers-status");
67
+ await sleep(1000);
68
+ scrollModal("mcp-servers-status");
69
+ this.loading = false;
70
+ },
71
+};
72
+
73
+const store = createStore("mcpServersStore", model);
74
+
75
+export { store };
webui/components/settings/mcp/client/mcp-servers.html
new
+111
@@ -0,0 +1,111 @@
1
+<html>
2
+
3
+<head>
4
+ <title>MCP Servers Configuration</title>
5
+
6
+ <script type="module">
7
+ import { store } from "/components/settings/mcp/client/mcp-servers-store.js";
8
+ </script>
9
+</head>
10
+
11
+<body>
12
+ <div x-data>
13
+ <template x-if="$store.mcpServersStore">
14
+ <div x-init="$store.mcpServersStore.initialize()" x-destroy="$store.mcpServersStore.onClose()">
15
+
16
+ <h3>MCP Servers Configuration JSON
17
+ <button class="btn slim" style="margin-left: 0.5em;"
18
+ @click="$store.mcpServersStore.formatJson()">Reformat</button>
19
+ <button class="btn slim primary" :disabled="$store.mcpServersStore.loading"
20
+ style="margin-left: 0.5em;" @click="$store.mcpServersStore.applyNow()">Apply now</button>
21
+ </h3>
22
+ <div id="mcp-servers-config-json"></div>
23
+
24
+ <h3 id="mcp-servers-status">Status</h3>
25
+
26
+ <div class="server-list">
27
+ <template x-for="server in $store.mcpServersStore.servers" :key="server.name">
28
+ <div class="server-item">
29
+ <span class="status-indicator"
30
+ :class="server.connected ? 'connected' : 'disconnected'"></span>
31
+ <span class="server-name" x-text="server.name"></span>
32
+ <span class="server-tools"
33
+ x-text="'- ' + (server.tools ? server.tools : 0) + ' tools'"></span>
34
+ <span class="server-error" x-show="server.error" x-text="server.error"></span>
35
+ </div>
36
+ </template>
37
+ <div x-show="$store.mcpServersStore.servers.length === 0" class="no-servers">
38
+ No servers connected
39
+ </div>
40
+ </div>
41
+
42
+
43
+ </div>
44
+ </template>
45
+ </div>
46
+
47
+ <style>
48
+ #mcp-servers-config-json {
49
+ width: 100%;
50
+ height: 40em;
51
+ }
52
+
53
+ .server-list {
54
+ margin-top: 0.5em;
55
+ margin-bottom: 1em;
56
+ }
57
+
58
+ .server-item {
59
+ display: flex;
60
+ align-items: center;
61
+ flex-wrap: wrap;
62
+ padding: 0.5em;
63
+ border-bottom: 1px solid var(--c-border);
64
+ }
65
+
66
+ .server-item:last-child {
67
+ border-bottom: none;
68
+ }
69
+
70
+ .status-indicator {
71
+ display: inline-block;
72
+ width: 0.8em;
73
+ height: 0.8em;
74
+ border-radius: 50%;
75
+ margin-right: 0.8em;
76
+ }
77
+
78
+ .status-indicator.connected {
79
+ background-color: #4CAF50;
80
+ }
81
+
82
+ .status-indicator.disconnected {
83
+ background-color: #F44336;
84
+ }
85
+
86
+ .server-name {
87
+ font-weight: 500;
88
+ margin-right: 0.5em;
89
+ }
90
+
91
+ .server-tools {
92
+ color: var(--c-fg2);
93
+ font-size: 0.9em;
94
+ }
95
+
96
+ .no-servers {
97
+ padding: 1em;
98
+ color: var(--c-fg2);
99
+ font-style: italic;
100
+ }
101
+
102
+ .server-error {
103
+ color: #F44336;
104
+ font-size: 0.9em;
105
+ margin-left: 0.8em;
106
+ }
107
+ </style>
108
+
109
+</body>
110
+
111
+</html>
\ No newline at end of file
webui/css/modals.css
+91
-76
@@ -129,30 +129,30 @@
129
130
/* Full Screen Input Modal Styles */
131
.full-screen-input-modal {
132
- width: 90%;
133
- max-width: 800px;
134
- max-height: 80vh;
135
- position: relative;
136
- padding: 0;
137
- background-color: rgb(20, 20, 20, 0.96);
138
- border: 1.5px solid var(--color-border);
132
+ width: 90%;
133
+ max-width: 800px;
134
+ max-height: 80vh;
135
+ position: relative;
136
+ padding: 0;
137
+ background-color: rgb(20, 20, 20, 0.96);
138
+ border: 1.5px solid var(--color-border);
139
}
140
141
.full-screen-input-modal h2 {
142
- margin: 0;
143
- padding: 0;
144
- font-size: 1.1rem;
145
- color: var(--color-text);
146
- opacity: 0.8;
142
+ margin: 0;
143
+ padding: 0;
144
+ font-size: 1.1rem;
145
+ color: var(--color-text);
146
+ opacity: 0.8;
147
}
148
149
.full-screen-input-modal .modal-close {
150
- position: absolute;
151
- top: 1.2rem;
152
- right: 1rem;
153
- font-size: 1.5rem;
154
- padding: 0 0.5rem;
155
- line-height: 0.8;
150
+ position: absolute;
151
+ top: 1.2rem;
152
+ right: 1rem;
153
+ font-size: 1.5rem;
154
+ padding: 0 0.5rem;
155
+ line-height: 0.8;
156
}
157
158
.full-screen-input-modal .btn-ok {
@@ -160,29 +160,29 @@
160
}
161
162
.full-screen-input-modal .modal-content {
163
- height: calc(80vh);
164
- padding: 0;
165
- margin: 0;
166
- overflow: hidden;
163
+ height: calc(80vh);
164
+ padding: 0;
165
+ margin: 0;
166
+ overflow: hidden;
167
}
168
169
.full-screen-input-modal .modal-footer {
170
- background: transparent;
170
+ background: transparent;
171
max-height: 50px;
172
}
173
174
#full-screen-input {
175
- width: 100%;
176
- height: calc(100% - 50px);
177
- border: none;
178
- background-color: transparent;
179
- color: var(--color-text);
180
- font-family: 'Roboto Mono', monospace;
181
- font-optical-sizing: auto;
182
- font-size: 0.955rem;
183
- padding: 1.2rem 1rem;
184
- resize: none;
185
- outline: none;
175
+ width: 100%;
176
+ height: calc(100% - 50px);
177
+ border: none;
178
+ background-color: transparent;
179
+ color: var(--color-text);
180
+ font-family: "Roboto Mono", monospace;
181
+ font-optical-sizing: auto;
182
+ font-size: 0.955rem;
183
+ padding: 1.2rem 1rem;
184
+ resize: none;
185
+ outline: none;
186
}
187
188
#full-screen-input::-webkit-scrollbar {
@@ -208,13 +208,13 @@
208
}
209
210
.light-mode .full-screen-input-modal {
211
- background-color: rgb(220, 220, 220, 0.86);
211
+ background-color: rgb(220, 220, 220, 0.86);
212
}
213
214
.full-screen-input-modal .modal-footer {
215
- padding: 1rem 0;
216
- border-top: none;
217
- background: transparent;
215
+ padding: 1rem 0;
216
+ border-top: none;
217
+ background: transparent;
218
}
219
220
/* Buttons Container */
@@ -265,6 +265,21 @@
265
font-family: "Rubik", Arial, Helvetica, sans-serif;
266
}
267
268
+.btn.slim {
269
+ padding: 0.2em 0.4em;
270
+}
271
+
272
+.btn.primary {
273
+ background: #2196f3;
274
+ color: white;
275
+ width: fit-content;
276
+}
277
+
278
+.btn:disabled {
279
+ background: #ccc;
280
+ cursor: not-allowed;
281
+}
282
+
283
.btn-ok {
284
background: #4248f1;
285
color: white;
@@ -391,72 +406,72 @@ h2 {
406
407
/* Editor Toolbar */
408
.editor-toolbar {
394
- display: flex;
395
- align-items: center;
396
- gap: 1rem;
397
- padding: 0.5rem 1rem;
398
- border-radius: 6px;
399
- background-color: rgba(30, 30, 30, 0.95);
400
- border-bottom: 1px solid var(--color-border);
409
+ display: flex;
410
+ align-items: center;
411
+ gap: 1rem;
412
+ padding: 0.5rem 1rem;
413
+ border-radius: 6px;
414
+ background-color: rgba(30, 30, 30, 0.95);
415
+ border-bottom: 1px solid var(--color-border);
416
}
417
418
.light-mode .editor-toolbar {
404
- background-color: rgba(240, 240, 240, 0.95);
419
+ background-color: rgba(240, 240, 240, 0.95);
420
}
421
422
.toolbar-group {
408
- display: flex;
409
- align-items: center;
410
- gap: 0.5rem;
411
- padding: 0 0.5rem;
412
- border-right: 1px solid var(--color-border);
423
+ display: flex;
424
+ align-items: center;
425
+ gap: 0.5rem;
426
+ padding: 0 0.5rem;
427
+ border-right: 1px solid var(--color-border);
428
}
429
430
.toolbar-group:last-child {
416
- border-right: none;
431
+ border-right: none;
432
}
433
434
.toolbar-button {
420
- display: flex;
421
- align-items: center;
422
- justify-content: center;
423
- width: 32px;
424
- height: 32px;
425
- padding: 0.4rem;
426
- background: transparent;
427
- border: 1px solid transparent;
428
- border-radius: 4px;
429
- color: var(--color-text);
430
- opacity: 0.7;
431
- cursor: pointer;
432
- transition: all 0.2s ease;
435
+ display: flex;
436
+ align-items: center;
437
+ justify-content: center;
438
+ width: 32px;
439
+ height: 32px;
440
+ padding: 0.4rem;
441
+ background: transparent;
442
+ border: 1px solid transparent;
443
+ border-radius: 4px;
444
+ color: var(--color-text);
445
+ opacity: 0.7;
446
+ cursor: pointer;
447
+ transition: all 0.2s ease;
448
}
449
450
.toolbar-button svg {
436
- width: 18px;
437
- height: 18px;
451
+ width: 18px;
452
+ height: 18px;
453
}
454
455
.toolbar-button:hover {
441
- opacity: 1;
442
- background-color: rgba(255, 255, 255, 0.1);
456
+ opacity: 1;
457
+ background-color: rgba(255, 255, 255, 0.1);
458
}
459
460
.toolbar-button:active {
446
- transform: translateY(1px);
461
+ transform: translateY(1px);
462
}
463
464
.toolbar-button.active {
450
- background-color: rgba(255, 255, 255, 0.15);
451
- border-color: var(--color-border);
452
- opacity: 1;
465
+ background-color: rgba(255, 255, 255, 0.15);
466
+ border-color: var(--color-border);
467
+ opacity: 1;
468
}
469
470
.toolbar-button:disabled {
456
- opacity: 0.3;
457
- cursor: not-allowed;
471
+ opacity: 0.3;
472
+ cursor: not-allowed;
473
}
474
475
.toolbar-button:disabled:hover {
461
- background-color: transparent;
476
+ background-color: transparent;
477
}
webui/css/modals2.css
new
+190
@@ -0,0 +1,190 @@
1
+/* Modal Styles */
2
+.modal {
3
+ display: none;
4
+ position: fixed;
5
+ top: 0;
6
+ left: 0;
7
+ width: 100%;
8
+ height: 100%;
9
+ z-index: 2000;
10
+}
11
+
12
+.modal.show {
13
+ display: block;
14
+}
15
+
16
+.modal-backdrop {
17
+ position: fixed;
18
+ top: 0;
19
+ left: 0;
20
+ width: 100%;
21
+ height: 100%;
22
+ background-color: rgba(0, 0, 0, 0.5);
23
+ z-index: 1;
24
+ cursor: pointer;
25
+}
26
+
27
+.modal-inner {
28
+display: flex;
29
+ flex-direction: column;
30
+ overflow: hidden;
31
+ position: fixed;
32
+ top: 50%;
33
+ left: 50%;
34
+ transform: translate(-50%, -50%);
35
+ background-color: var(--color-background-light);
36
+ width: 90%;
37
+ max-width: 800px;
38
+ max-height: 90vh;
39
+ border-radius: 8px;
40
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
41
+ z-index: 2;
42
+}
43
+
44
+.dark-mode .modal-inner {
45
+ background-color: var(--color-background-dark);
46
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
47
+}
48
+
49
+/* Mobile Viewport Behavior */
50
+@media (max-width: 1280px) {
51
+ .modal-inner {
52
+ width: 95%; /* Take up most of the screen on mobile */
53
+ min-width: unset; /* Remove min-width constraints */
54
+ max-width: 95%; /* Ensure consistent width */
55
+ }
56
+
57
+ /* Ensure section content can scroll horizontally */
58
+ .section {
59
+ overflow-x: auto;
60
+ }
61
+ }
62
+
63
+.modal-scroll {
64
+ max-height: 90vh;
65
+ overflow-y: auto;
66
+ padding: 0 1rem 1rem 1rem;
67
+}
68
+
69
+.modal-x {
70
+ position: absolute;
71
+ top: 1rem;
72
+ right: 1rem;
73
+ background: none;
74
+ border: none;
75
+ font-size: 1.5rem;
76
+ cursor: pointer;
77
+ color: var(--text-primary-light);
78
+ padding: 0.5rem;
79
+ line-height: 1;
80
+ z-index: 3;
81
+}
82
+
83
+.dark-mode .modal-x {
84
+ color: var(--text-primary-dark);
85
+}
86
+
87
+.modal-title {
88
+ /* margin: 0 0 1.5rem 0;
89
+ padding-right: 3rem;
90
+ color: var(--text-primary-light);
91
+ font-size: 1.75rem;
92
+ border-bottom: 1px solid rgba(0, 0, 0, 0.1);
93
+ padding-bottom: 1rem; */
94
+}
95
+
96
+.dark-mode .modal-title {
97
+ color: var(--text-primary-dark);
98
+ border-bottom-color: rgba(255, 255, 255, 0.1);
99
+}
100
+
101
+.modal-button-container {
102
+ display: flex;
103
+ justify-content: space-between;
104
+ gap: 10px;
105
+ width: 100%;
106
+ margin-top: 20px;
107
+}
108
+
109
+.modal-button-container .button {
110
+ flex: 1;
111
+ min-width: 0;
112
+ padding: 10px;
113
+ text-align: center;
114
+ white-space: nowrap;
115
+ font-size: 0.9em;
116
+ display: flex;
117
+ justify-content: center;
118
+ align-items: center;
119
+}
120
+
121
+.loading {
122
+ width: calc(100% - 4rem);
123
+ max-width: 1200px;
124
+ min-height: 50px;
125
+ border-radius: 12px;
126
+ position: relative;
127
+ overflow: hidden;
128
+ margin: 2rem auto;
129
+ opacity: 0;
130
+ animation: fadeIn 500ms ease-out 500ms forwards;
131
+}
132
+
133
+.light-mode .loading {
134
+ background: var(--color-border-light);
135
+}
136
+
137
+.dark-mode .loading {
138
+ background: var(--color-border-dark);
139
+}
140
+
141
+@keyframes fadeIn {
142
+ 0% {
143
+ opacity: 0;
144
+ }
145
+ 100% {
146
+ opacity: 1;
147
+ }
148
+}
149
+
150
+.loading::before {
151
+ content: "";
152
+ position: absolute;
153
+ top: 0;
154
+ left: 0;
155
+ width: 100%;
156
+ height: 100%;
157
+}
158
+
159
+.dark-mode .loading::before {
160
+ background: linear-gradient(
161
+ 90deg,
162
+ var(--color-background-dark),
163
+ var(--color-border-dark),
164
+ var(--color-background-dark)
165
+ );
166
+ animation: shimmer 2s infinite;
167
+ animation-delay: 250ms;
168
+ background-size: 200% 100%;
169
+}
170
+
171
+.light-mode .loading::before {
172
+ background: linear-gradient(
173
+ 90deg,
174
+ var(--color-background-light),
175
+ color-mix(in srgb, var(--color-border-light) 95%, black),
176
+ var(--color-background-light)
177
+ );
178
+ animation: shimmer 2s infinite;
179
+ animation-delay: 250ms;
180
+ background-size: 200% 100%;
181
+}
182
+
183
+@keyframes shimmer {
184
+ 0% {
185
+ background-position: 200% 0;
186
+ }
187
+ 100% {
188
+ background-position: -200% 0;
189
+ }
190
+}
webui/index.html
+5
-3
@@ -11,6 +11,7 @@
11
<link rel="stylesheet" href="css/settings.css">
12
<link rel="stylesheet" href="css/file_browser.css">
13
<link rel="stylesheet" href="css/modals.css">
14
+ <link rel="stylesheet" href="css/modals2.css">
15
<link rel="stylesheet" href="css/speech.css">
16
<link rel="stylesheet" href="css/history.css">
17
<link rel="stylesheet" href="css/scheduler-datepicker.css">
@@ -104,7 +105,8 @@
105
106
<!-- Then load Alpine.js -->
107
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/collapse@3.14.3/dist/cdn.min.js"></script>
107
- <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.3/dist/cdn.min.js"></script>
108
+ <!-- <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.3/dist/cdn.min.js"></script> -->
109
+ <script type="module" src="js/initFw.js"></script>
110
111
<script src="https://cdn.jsdelivr.net/npm/ace-builds@1.36.5/src-noconflict/ace.js"></script>
112
<link href="https://cdn.jsdelivr.net/npm/ace-builds@1.36.5/css/ace.min.css" rel="stylesheet">
@@ -123,7 +125,7 @@
125
<script type="text/javascript" src="js/tunnel.js"></script>
126
</head>
127
126
-<body>
128
+<body class="dark-mode">
129
<div class="container">
130
<div id="sidebar-overlay" class="sidebar-overlay hidden"></div>
131
<div class="icons-section" id="hide-button" x-data="{ connected: true }">
@@ -626,7 +628,7 @@
628
<div class="section-title" x-text="section.title"></div>
629
<div class="section-description" x-html="section.description"></div>
630
629
- <template x-for="(field, fieldIndex) in section.fields" :key="fieldIndex">
631
+ <template x-for="(field, fieldIndex) in section.fields.filter(f => !f.hidden)" :key="fieldIndex">
632
<div :class="{'field': true, 'field-full': field.type === 'textarea'}">
633
<div class="field-label">
634
<div class="field-title" x-text="field.title"></div>
webui/index.js
+3
-10
@@ -726,7 +726,9 @@ window.toggleUtils = async function (showUtils) {
726
window.toggleDarkMode = function (isDark) {
727
if (isDark) {
728
document.body.classList.remove('light-mode');
729
- } else {
729
+ document.body.classList.add('dark-mode');
730
+ } else {
731
+ document.body.classList.remove('dark-mode');
732
document.body.classList.add('light-mode');
733
}
734
console.log("Dark mode:", isDark);
@@ -791,15 +793,6 @@ document.addEventListener('DOMContentLoaded', () => {
793
toggleDarkMode(isDarkMode);
794
});
795
794
-window.toggleDarkMode = function (isDark) {
795
- if (isDark) {
796
- document.body.classList.remove('light-mode');
797
- } else {
798
- document.body.classList.add('light-mode');
799
- }
800
- console.log("Dark mode:", isDark);
801
- localStorage.setItem('darkMode', isDark);
802
-};
796
797
function toggleCssProperty(selector, property, value) {
798
// Get the stylesheet that contains the class
webui/js/AlpineStore.js
new
+45
@@ -0,0 +1,45 @@
1
+// Track all created stores
2
+const stores = new Map();
3
+
4
+/**
5
+ * Creates a store that can be used to share state between components.
6
+ * Uses initial state object and returns a proxy to it that uses Alpine when initialized
7
+ * @template T
8
+ * @param {string} name
9
+ * @param {T} initialState
10
+ * @returns {T}
11
+ */
12
+export function createStore(name, initialState) {
13
+ const proxy = new Proxy(initialState, {
14
+ set(target, prop, value) {
15
+ const store = globalThis.Alpine?.store(name);
16
+ if (store) store[prop] = value;
17
+ else target[prop] = value;
18
+ return true;
19
+ },
20
+ get(target, prop) {
21
+ return target[prop];
22
+ }
23
+ });
24
+
25
+ if (globalThis.Alpine) {
26
+ globalThis.Alpine.store(name, initialState);
27
+ } else {
28
+ document.addEventListener("alpine:init", () => Alpine.store(name, initialState));
29
+ }
30
+
31
+ // Store the proxy
32
+ stores.set(name, proxy);
33
+
34
+ return /** @type {T} */ (proxy); // explicitly cast for linter support
35
+}
36
+
37
+/**
38
+ * Get an existing store by name
39
+ * @template T
40
+ * @param {string} name
41
+ * @returns {T | undefined}
42
+ */
43
+export function getStore(name) {
44
+ return /** @type {T | undefined} */ (stores.get(name));
45
+}
\ No newline at end of file
webui/js/alpine.min.js
new
+6
@@ -0,0 +1,6 @@
1
+
2
+(()=>{var nt=!1,it=!1,W=[],ot=-1;function Ut(e){Rn(e)}function Rn(e){W.includes(e)||W.push(e),Mn()}function Wt(e){let t=W.indexOf(e);t!==-1&&t>ot&&W.splice(t,1)}function Mn(){!it&&!nt&&(nt=!0,queueMicrotask(Nn))}function Nn(){nt=!1,it=!0;for(let e=0;e<W.length;e++)W[e](),ot=e;W.length=0,ot=-1,it=!1}var T,N,$,at,st=!0;function Gt(e){st=!1,e(),st=!0}function Jt(e){T=e.reactive,$=e.release,N=t=>e.effect(t,{scheduler:r=>{st?Ut(r):r()}}),at=e.raw}function ct(e){N=e}function Yt(e){let t=()=>{};return[n=>{let i=N(n);return e._x_effects||(e._x_effects=new Set,e._x_runEffects=()=>{e._x_effects.forEach(o=>o())}),e._x_effects.add(i),t=()=>{i!==void 0&&(e._x_effects.delete(i),$(i))},i},()=>{t()}]}function ve(e,t){let r=!0,n,i=N(()=>{let o=e();JSON.stringify(o),r?n=o:queueMicrotask(()=>{t(o,n),n=o}),r=!1});return()=>$(i)}var Xt=[],Zt=[],Qt=[];function er(e){Qt.push(e)}function te(e,t){typeof t=="function"?(e._x_cleanups||(e._x_cleanups=[]),e._x_cleanups.push(t)):(t=e,Zt.push(t))}function Ae(e){Xt.push(e)}function Oe(e,t,r){e._x_attributeCleanups||(e._x_attributeCleanups={}),e._x_attributeCleanups[t]||(e._x_attributeCleanups[t]=[]),e._x_attributeCleanups[t].push(r)}function lt(e,t){e._x_attributeCleanups&&Object.entries(e._x_attributeCleanups).forEach(([r,n])=>{(t===void 0||t.includes(r))&&(n.forEach(i=>i()),delete e._x_attributeCleanups[r])})}function tr(e){for(e._x_effects?.forEach(Wt);e._x_cleanups?.length;)e._x_cleanups.pop()()}var ut=new MutationObserver(mt),ft=!1;function ue(){ut.observe(document,{subtree:!0,childList:!0,attributes:!0,attributeOldValue:!0}),ft=!0}function dt(){kn(),ut.disconnect(),ft=!1}var le=[];function kn(){let e=ut.takeRecords();le.push(()=>e.length>0&&mt(e));let t=le.length;queueMicrotask(()=>{if(le.length===t)for(;le.length>0;)le.shift()()})}function m(e){if(!ft)return e();dt();let t=e();return ue(),t}var pt=!1,Se=[];function rr(){pt=!0}function nr(){pt=!1,mt(Se),Se=[]}function mt(e){if(pt){Se=Se.concat(e);return}let t=[],r=new Set,n=new Map,i=new Map;for(let o=0;o<e.length;o++)if(!e[o].target._x_ignoreMutationObserver&&(e[o].type==="childList"&&(e[o].removedNodes.forEach(s=>{s.nodeType===1&&s._x_marker&&r.add(s)}),e[o].addedNodes.forEach(s=>{if(s.nodeType===1){if(r.has(s)){r.delete(s);return}s._x_marker||t.push(s)}})),e[o].type==="attributes")){let s=e[o].target,a=e[o].attributeName,c=e[o].oldValue,l=()=>{n.has(s)||n.set(s,[]),n.get(s).push({name:a,value:s.getAttribute(a)})},u=()=>{i.has(s)||i.set(s,[]),i.get(s).push(a)};s.hasAttribute(a)&&c===null?l():s.hasAttribute(a)?(u(),l()):u()}i.forEach((o,s)=>{lt(s,o)}),n.forEach((o,s)=>{Xt.forEach(a=>a(s,o))});for(let o of r)t.some(s=>s.contains(o))||Zt.forEach(s=>s(o));for(let o of t)o.isConnected&&Qt.forEach(s=>s(o));t=null,r=null,n=null,i=null}function Ce(e){return z(B(e))}function k(e,t,r){return e._x_dataStack=[t,...B(r||e)],()=>{e._x_dataStack=e._x_dataStack.filter(n=>n!==t)}}function B(e){return e._x_dataStack?e._x_dataStack:typeof ShadowRoot=="function"&&e instanceof ShadowRoot?B(e.host):e.parentNode?B(e.parentNode):[]}function z(e){return new Proxy({objects:e},Dn)}var Dn={ownKeys({objects:e}){return Array.from(new Set(e.flatMap(t=>Object.keys(t))))},has({objects:e},t){return t==Symbol.unscopables?!1:e.some(r=>Object.prototype.hasOwnProperty.call(r,t)||Reflect.has(r,t))},get({objects:e},t,r){return t=="toJSON"?Pn:Reflect.get(e.find(n=>Reflect.has(n,t))||{},t,r)},set({objects:e},t,r,n){let i=e.find(s=>Object.prototype.hasOwnProperty.call(s,t))||e[e.length-1],o=Object.getOwnPropertyDescriptor(i,t);return o?.set&&o?.get?o.set.call(n,r)||!0:Reflect.set(i,t,r)}};function Pn(){return Reflect.ownKeys(this).reduce((t,r)=>(t[r]=Reflect.get(this,r),t),{})}function Te(e){let t=n=>typeof n=="object"&&!Array.isArray(n)&&n!==null,r=(n,i="")=>{Object.entries(Object.getOwnPropertyDescriptors(n)).forEach(([o,{value:s,enumerable:a}])=>{if(a===!1||s===void 0||typeof s=="object"&&s!==null&&s.__v_skip)return;let c=i===""?o:`${i}.${o}`;typeof s=="object"&&s!==null&&s._x_interceptor?n[o]=s.initialize(e,c,o):t(s)&&s!==n&&!(s instanceof Element)&&r(s,c)})};return r(e)}function Re(e,t=()=>{}){let r={initialValue:void 0,_x_interceptor:!0,initialize(n,i,o){return e(this.initialValue,()=>In(n,i),s=>ht(n,i,s),i,o)}};return t(r),n=>{if(typeof n=="object"&&n!==null&&n._x_interceptor){let i=r.initialize.bind(r);r.initialize=(o,s,a)=>{let c=n.initialize(o,s,a);return r.initialValue=c,i(o,s,a)}}else r.initialValue=n;return r}}function In(e,t){return t.split(".").reduce((r,n)=>r[n],e)}function ht(e,t,r){if(typeof t=="string"&&(t=t.split(".")),t.length===1)e[t[0]]=r;else{if(t.length===0)throw error;return e[t[0]]||(e[t[0]]={}),ht(e[t[0]],t.slice(1),r)}}var ir={};function y(e,t){ir[e]=t}function fe(e,t){let r=Ln(t);return Object.entries(ir).forEach(([n,i])=>{Object.defineProperty(e,`$${n}`,{get(){return i(t,r)},enumerable:!1})}),e}function Ln(e){let[t,r]=_t(e),n={interceptor:Re,...t};return te(e,r),n}function or(e,t,r,...n){try{return r(...n)}catch(i){re(i,e,t)}}function re(e,t,r=void 0){e=Object.assign(e??{message:"No error message given."},{el:t,expression:r}),console.warn(`Alpine Expression Error: ${e.message}
3
+
4
+ ${r?'Expression: "'+r+`"
5
+
6
+ `:""}`,t),setTimeout(()=>{throw e},0)}var Me=!0;function ke(e){let t=Me;Me=!1;let r=e();return Me=t,r}function R(e,t,r={}){let n;return x(e,t)(i=>n=i,r),n}function x(...e){return sr(...e)}var sr=xt;function ar(e){sr=e}function xt(e,t){let r={};fe(r,e);let n=[r,...B(e)],i=typeof t=="function"?$n(n,t):Fn(n,t,e);return or.bind(null,e,t,i)}function $n(e,t){return(r=()=>{},{scope:n={},params:i=[]}={})=>{let o=t.apply(z([n,...e]),i);Ne(r,o)}}var gt={};function jn(e,t){if(gt[e])return gt[e];let r=Object.getPrototypeOf(async function(){}).constructor,n=/^[\n\s]*if.*\(.*\)/.test(e.trim())||/^(let|const)\s/.test(e.trim())?`(async()=>{ ${e} })()`:e,o=(()=>{try{let s=new r(["__self","scope"],`with (scope) { __self.result = ${n} }; __self.finished = true; return __self.result;`);return Object.defineProperty(s,"name",{value:`[Alpine] ${e}`}),s}catch(s){return re(s,t,e),Promise.resolve()}})();return gt[e]=o,o}function Fn(e,t,r){let n=jn(t,r);return(i=()=>{},{scope:o={},params:s=[]}={})=>{n.result=void 0,n.finished=!1;let a=z([o,...e]);if(typeof n=="function"){let c=n(n,a).catch(l=>re(l,r,t));n.finished?(Ne(i,n.result,a,s,r),n.result=void 0):c.then(l=>{Ne(i,l,a,s,r)}).catch(l=>re(l,r,t)).finally(()=>n.result=void 0)}}}function Ne(e,t,r,n,i){if(Me&&typeof t=="function"){let o=t.apply(r,n);o instanceof Promise?o.then(s=>Ne(e,s,r,n)).catch(s=>re(s,i,t)):e(o)}else typeof t=="object"&&t instanceof Promise?t.then(o=>e(o)):e(t)}var wt="x-";function C(e=""){return wt+e}function cr(e){wt=e}var De={};function d(e,t){return De[e]=t,{before(r){if(!De[r]){console.warn(String.raw`Cannot find directive \`${r}\`. \`${e}\` will use the default order of execution`);return}let n=G.indexOf(r);G.splice(n>=0?n:G.indexOf("DEFAULT"),0,e)}}}function lr(e){return Object.keys(De).includes(e)}function pe(e,t,r){if(t=Array.from(t),e._x_virtualDirectives){let o=Object.entries(e._x_virtualDirectives).map(([a,c])=>({name:a,value:c})),s=Et(o);o=o.map(a=>s.find(c=>c.name===a.name)?{name:`x-bind:${a.name}`,value:`"${a.value}"`}:a),t=t.concat(o)}let n={};return t.map(dr((o,s)=>n[o]=s)).filter(mr).map(zn(n,r)).sort(Kn).map(o=>Bn(e,o))}function Et(e){return Array.from(e).map(dr()).filter(t=>!mr(t))}var yt=!1,de=new Map,ur=Symbol();function fr(e){yt=!0;let t=Symbol();ur=t,de.set(t,[]);let r=()=>{for(;de.get(t).length;)de.get(t).shift()();de.delete(t)},n=()=>{yt=!1,r()};e(r),n()}function _t(e){let t=[],r=a=>t.push(a),[n,i]=Yt(e);return t.push(i),[{Alpine:K,effect:n,cleanup:r,evaluateLater:x.bind(x,e),evaluate:R.bind(R,e)},()=>t.forEach(a=>a())]}function Bn(e,t){let r=()=>{},n=De[t.type]||r,[i,o]=_t(e);Oe(e,t.original,o);let s=()=>{e._x_ignore||e._x_ignoreSelf||(n.inline&&n.inline(e,t,i),n=n.bind(n,e,t,i),yt?de.get(ur).push(n):n())};return s.runCleanups=o,s}var Pe=(e,t)=>({name:r,value:n})=>(r.startsWith(e)&&(r=r.replace(e,t)),{name:r,value:n}),Ie=e=>e;function dr(e=()=>{}){return({name:t,value:r})=>{let{name:n,value:i}=pr.reduce((o,s)=>s(o),{name:t,value:r});return n!==t&&e(n,t),{name:n,value:i}}}var pr=[];function ne(e){pr.push(e)}function mr({name:e}){return hr().test(e)}var hr=()=>new RegExp(`^${wt}([^:^.]+)\\b`);function zn(e,t){return({name:r,value:n})=>{let i=r.match(hr()),o=r.match(/:([a-zA-Z0-9\-_:]+)/),s=r.match(/\.[^.\]]+(?=[^\]]*$)/g)||[],a=t||e[r]||r;return{type:i?i[1]:null,value:o?o[1]:null,modifiers:s.map(c=>c.replace(".","")),expression:n,original:a}}}var bt="DEFAULT",G=["ignore","ref","data","id","anchor","bind","init","for","model","modelable","transition","show","if",bt,"teleport"];function Kn(e,t){let r=G.indexOf(e.type)===-1?bt:e.type,n=G.indexOf(t.type)===-1?bt:t.type;return G.indexOf(r)-G.indexOf(n)}function J(e,t,r={}){e.dispatchEvent(new CustomEvent(t,{detail:r,bubbles:!0,composed:!0,cancelable:!0}))}function D(e,t){if(typeof ShadowRoot=="function"&&e instanceof ShadowRoot){Array.from(e.children).forEach(i=>D(i,t));return}let r=!1;if(t(e,()=>r=!0),r)return;let n=e.firstElementChild;for(;n;)D(n,t,!1),n=n.nextElementSibling}function E(e,...t){console.warn(`Alpine Warning: ${e}`,...t)}var _r=!1;function gr(){_r&&E("Alpine has already been initialized on this page. Calling Alpine.start() more than once can cause problems."),_r=!0,document.body||E("Unable to initialize. Trying to load Alpine before `<body>` is available. Did you forget to add `defer` in Alpine's `<script>` tag?"),J(document,"alpine:init"),J(document,"alpine:initializing"),ue(),er(t=>S(t,D)),te(t=>P(t)),Ae((t,r)=>{pe(t,r).forEach(n=>n())});let e=t=>!Y(t.parentElement,!0);Array.from(document.querySelectorAll(br().join(","))).filter(e).forEach(t=>{S(t)}),J(document,"alpine:initialized"),setTimeout(()=>{Vn()})}var vt=[],xr=[];function yr(){return vt.map(e=>e())}function br(){return vt.concat(xr).map(e=>e())}function Le(e){vt.push(e)}function $e(e){xr.push(e)}function Y(e,t=!1){return j(e,r=>{if((t?br():yr()).some(i=>r.matches(i)))return!0})}function j(e,t){if(e){if(t(e))return e;if(e._x_teleportBack&&(e=e._x_teleportBack),!!e.parentElement)return j(e.parentElement,t)}}function wr(e){return yr().some(t=>e.matches(t))}var Er=[];function vr(e){Er.push(e)}var Hn=1;function S(e,t=D,r=()=>{}){j(e,n=>n._x_ignore)||fr(()=>{t(e,(n,i)=>{n._x_marker||(r(n,i),Er.forEach(o=>o(n,i)),pe(n,n.attributes).forEach(o=>o()),n._x_ignore||(n._x_marker=Hn++),n._x_ignore&&i())})})}function P(e,t=D){t(e,r=>{tr(r),lt(r),delete r._x_marker})}function Vn(){[["ui","dialog",["[x-dialog], [x-popover]"]],["anchor","anchor",["[x-anchor]"]],["sort","sort",["[x-sort]"]]].forEach(([t,r,n])=>{lr(r)||n.some(i=>{if(document.querySelector(i))return E(`found "${i}", but missing ${t} plugin`),!0})})}var St=[],At=!1;function ie(e=()=>{}){return queueMicrotask(()=>{At||setTimeout(()=>{je()})}),new Promise(t=>{St.push(()=>{e(),t()})})}function je(){for(At=!1;St.length;)St.shift()()}function Sr(){At=!0}function me(e,t){return Array.isArray(t)?Ar(e,t.join(" ")):typeof t=="object"&&t!==null?qn(e,t):typeof t=="function"?me(e,t()):Ar(e,t)}function Ar(e,t){let r=o=>o.split(" ").filter(Boolean),n=o=>o.split(" ").filter(s=>!e.classList.contains(s)).filter(Boolean),i=o=>(e.classList.add(...o),()=>{e.classList.remove(...o)});return t=t===!0?t="":t||"",i(n(t))}function qn(e,t){let r=a=>a.split(" ").filter(Boolean),n=Object.entries(t).flatMap(([a,c])=>c?r(a):!1).filter(Boolean),i=Object.entries(t).flatMap(([a,c])=>c?!1:r(a)).filter(Boolean),o=[],s=[];return i.forEach(a=>{e.classList.contains(a)&&(e.classList.remove(a),s.push(a))}),n.forEach(a=>{e.classList.contains(a)||(e.classList.add(a),o.push(a))}),()=>{s.forEach(a=>e.classList.add(a)),o.forEach(a=>e.classList.remove(a))}}function X(e,t){return typeof t=="object"&&t!==null?Un(e,t):Wn(e,t)}function Un(e,t){let r={};return Object.entries(t).forEach(([n,i])=>{r[n]=e.style[n],n.startsWith("--")||(n=Gn(n)),e.style.setProperty(n,i)}),setTimeout(()=>{e.style.length===0&&e.removeAttribute("style")}),()=>{X(e,r)}}function Wn(e,t){let r=e.getAttribute("style",t);return e.setAttribute("style",t),()=>{e.setAttribute("style",r||"")}}function Gn(e){return e.replace(/([a-z])([A-Z])/g,"$1-$2").toLowerCase()}function he(e,t=()=>{}){let r=!1;return function(){r?t.apply(this,arguments):(r=!0,e.apply(this,arguments))}}d("transition",(e,{value:t,modifiers:r,expression:n},{evaluate:i})=>{typeof n=="function"&&(n=i(n)),n!==!1&&(!n||typeof n=="boolean"?Yn(e,r,t):Jn(e,n,t))});function Jn(e,t,r){Or(e,me,""),{enter:i=>{e._x_transition.enter.during=i},"enter-start":i=>{e._x_transition.enter.start=i},"enter-end":i=>{e._x_transition.enter.end=i},leave:i=>{e._x_transition.leave.during=i},"leave-start":i=>{e._x_transition.leave.start=i},"leave-end":i=>{e._x_transition.leave.end=i}}[r](t)}function Yn(e,t,r){Or(e,X);let n=!t.includes("in")&&!t.includes("out")&&!r,i=n||t.includes("in")||["enter"].includes(r),o=n||t.includes("out")||["leave"].includes(r);t.includes("in")&&!n&&(t=t.filter((g,b)=>b<t.indexOf("out"))),t.includes("out")&&!n&&(t=t.filter((g,b)=>b>t.indexOf("out")));let s=!t.includes("opacity")&&!t.includes("scale"),a=s||t.includes("opacity"),c=s||t.includes("scale"),l=a?0:1,u=c?_e(t,"scale",95)/100:1,p=_e(t,"delay",0)/1e3,h=_e(t,"origin","center"),w="opacity, transform",F=_e(t,"duration",150)/1e3,Ee=_e(t,"duration",75)/1e3,f="cubic-bezier(0.4, 0.0, 0.2, 1)";i&&(e._x_transition.enter.during={transformOrigin:h,transitionDelay:`${p}s`,transitionProperty:w,transitionDuration:`${F}s`,transitionTimingFunction:f},e._x_transition.enter.start={opacity:l,transform:`scale(${u})`},e._x_transition.enter.end={opacity:1,transform:"scale(1)"}),o&&(e._x_transition.leave.during={transformOrigin:h,transitionDelay:`${p}s`,transitionProperty:w,transitionDuration:`${Ee}s`,transitionTimingFunction:f},e._x_transition.leave.start={opacity:1,transform:"scale(1)"},e._x_transition.leave.end={opacity:l,transform:`scale(${u})`})}function Or(e,t,r={}){e._x_transition||(e._x_transition={enter:{during:r,start:r,end:r},leave:{during:r,start:r,end:r},in(n=()=>{},i=()=>{}){Fe(e,t,{during:this.enter.during,start:this.enter.start,end:this.enter.end},n,i)},out(n=()=>{},i=()=>{}){Fe(e,t,{during:this.leave.during,start:this.leave.start,end:this.leave.end},n,i)}})}window.Element.prototype._x_toggleAndCascadeWithTransitions=function(e,t,r,n){let i=document.visibilityState==="visible"?requestAnimationFrame:setTimeout,o=()=>i(r);if(t){e._x_transition&&(e._x_transition.enter||e._x_transition.leave)?e._x_transition.enter&&(Object.entries(e._x_transition.enter.during).length||Object.entries(e._x_transition.enter.start).length||Object.entries(e._x_transition.enter.end).length)?e._x_transition.in(r):o():e._x_transition?e._x_transition.in(r):o();return}e._x_hidePromise=e._x_transition?new Promise((s,a)=>{e._x_transition.out(()=>{},()=>s(n)),e._x_transitioning&&e._x_transitioning.beforeCancel(()=>a({isFromCancelledTransition:!0}))}):Promise.resolve(n),queueMicrotask(()=>{let s=Cr(e);s?(s._x_hideChildren||(s._x_hideChildren=[]),s._x_hideChildren.push(e)):i(()=>{let a=c=>{let l=Promise.all([c._x_hidePromise,...(c._x_hideChildren||[]).map(a)]).then(([u])=>u?.());return delete c._x_hidePromise,delete c._x_hideChildren,l};a(e).catch(c=>{if(!c.isFromCancelledTransition)throw c})})})};function Cr(e){let t=e.parentNode;if(t)return t._x_hidePromise?t:Cr(t)}function Fe(e,t,{during:r,start:n,end:i}={},o=()=>{},s=()=>{}){if(e._x_transitioning&&e._x_transitioning.cancel(),Object.keys(r).length===0&&Object.keys(n).length===0&&Object.keys(i).length===0){o(),s();return}let a,c,l;Xn(e,{start(){a=t(e,n)},during(){c=t(e,r)},before:o,end(){a(),l=t(e,i)},after:s,cleanup(){c(),l()}})}function Xn(e,t){let r,n,i,o=he(()=>{m(()=>{r=!0,n||t.before(),i||(t.end(),je()),t.after(),e.isConnected&&t.cleanup(),delete e._x_transitioning})});e._x_transitioning={beforeCancels:[],beforeCancel(s){this.beforeCancels.push(s)},cancel:he(function(){for(;this.beforeCancels.length;)this.beforeCancels.shift()();o()}),finish:o},m(()=>{t.start(),t.during()}),Sr(),requestAnimationFrame(()=>{if(r)return;let s=Number(getComputedStyle(e).transitionDuration.replace(/,.*/,"").replace("s",""))*1e3,a=Number(getComputedStyle(e).transitionDelay.replace(/,.*/,"").replace("s",""))*1e3;s===0&&(s=Number(getComputedStyle(e).animationDuration.replace("s",""))*1e3),m(()=>{t.before()}),n=!0,requestAnimationFrame(()=>{r||(m(()=>{t.end()}),je(),setTimeout(e._x_transitioning.finish,s+a),i=!0)})})}function _e(e,t,r){if(e.indexOf(t)===-1)return r;let n=e[e.indexOf(t)+1];if(!n||t==="scale"&&isNaN(n))return r;if(t==="duration"||t==="delay"){let i=n.match(/([0-9]+)ms/);if(i)return i[1]}return t==="origin"&&["top","right","left","center","bottom"].includes(e[e.indexOf(t)+2])?[n,e[e.indexOf(t)+2]].join(" "):n}var I=!1;function A(e,t=()=>{}){return(...r)=>I?t(...r):e(...r)}function Tr(e){return(...t)=>I&&e(...t)}var Rr=[];function H(e){Rr.push(e)}function Mr(e,t){Rr.forEach(r=>r(e,t)),I=!0,kr(()=>{S(t,(r,n)=>{n(r,()=>{})})}),I=!1}var Be=!1;function Nr(e,t){t._x_dataStack||(t._x_dataStack=e._x_dataStack),I=!0,Be=!0,kr(()=>{Zn(t)}),I=!1,Be=!1}function Zn(e){let t=!1;S(e,(n,i)=>{D(n,(o,s)=>{if(t&&wr(o))return s();t=!0,i(o,s)})})}function kr(e){let t=N;ct((r,n)=>{let i=t(r);return $(i),()=>{}}),e(),ct(t)}function ge(e,t,r,n=[]){switch(e._x_bindings||(e._x_bindings=T({})),e._x_bindings[t]=r,t=n.includes("camel")?si(t):t,t){case"value":Qn(e,r);break;case"style":ti(e,r);break;case"class":ei(e,r);break;case"selected":case"checked":ri(e,t,r);break;default:Pr(e,t,r);break}}function Qn(e,t){if(Ot(e))e.attributes.value===void 0&&(e.value=t),window.fromModel&&(typeof t=="boolean"?e.checked=xe(e.value)===t:e.checked=Dr(e.value,t));else if(ze(e))Number.isInteger(t)?e.value=t:!Array.isArray(t)&&typeof t!="boolean"&&![null,void 0].includes(t)?e.value=String(t):Array.isArray(t)?e.checked=t.some(r=>Dr(r,e.value)):e.checked=!!t;else if(e.tagName==="SELECT")oi(e,t);else{if(e.value===t)return;e.value=t===void 0?"":t}}function ei(e,t){e._x_undoAddedClasses&&e._x_undoAddedClasses(),e._x_undoAddedClasses=me(e,t)}function ti(e,t){e._x_undoAddedStyles&&e._x_undoAddedStyles(),e._x_undoAddedStyles=X(e,t)}function ri(e,t,r){Pr(e,t,r),ii(e,t,r)}function Pr(e,t,r){[null,void 0,!1].includes(r)&&ci(t)?e.removeAttribute(t):(Ir(t)&&(r=t),ni(e,t,r))}function ni(e,t,r){e.getAttribute(t)!=r&&e.setAttribute(t,r)}function ii(e,t,r){e[t]!==r&&(e[t]=r)}function oi(e,t){let r=[].concat(t).map(n=>n+"");Array.from(e.options).forEach(n=>{n.selected=r.includes(n.value)})}function si(e){return e.toLowerCase().replace(/-(\w)/g,(t,r)=>r.toUpperCase())}function Dr(e,t){return e==t}function xe(e){return[1,"1","true","on","yes",!0].includes(e)?!0:[0,"0","false","off","no",!1].includes(e)?!1:e?Boolean(e):null}var ai=new Set(["allowfullscreen","async","autofocus","autoplay","checked","controls","default","defer","disabled","formnovalidate","inert","ismap","itemscope","loop","multiple","muted","nomodule","novalidate","open","playsinline","readonly","required","reversed","selected","shadowrootclonable","shadowrootdelegatesfocus","shadowrootserializable"]);function Ir(e){return ai.has(e)}function ci(e){return!["aria-pressed","aria-checked","aria-expanded","aria-selected"].includes(e)}function Lr(e,t,r){return e._x_bindings&&e._x_bindings[t]!==void 0?e._x_bindings[t]:jr(e,t,r)}function $r(e,t,r,n=!0){if(e._x_bindings&&e._x_bindings[t]!==void 0)return e._x_bindings[t];if(e._x_inlineBindings&&e._x_inlineBindings[t]!==void 0){let i=e._x_inlineBindings[t];return i.extract=n,ke(()=>R(e,i.expression))}return jr(e,t,r)}function jr(e,t,r){let n=e.getAttribute(t);return n===null?typeof r=="function"?r():r:n===""?!0:Ir(t)?!![t,"true"].includes(n):n}function ze(e){return e.type==="checkbox"||e.localName==="ui-checkbox"||e.localName==="ui-switch"}function Ot(e){return e.type==="radio"||e.localName==="ui-radio"}function Ke(e,t){var r;return function(){var n=this,i=arguments,o=function(){r=null,e.apply(n,i)};clearTimeout(r),r=setTimeout(o,t)}}function He(e,t){let r;return function(){let n=this,i=arguments;r||(e.apply(n,i),r=!0,setTimeout(()=>r=!1,t))}}function Ve({get:e,set:t},{get:r,set:n}){let i=!0,o,s,a=N(()=>{let c=e(),l=r();if(i)n(Ct(c)),i=!1;else{let u=JSON.stringify(c),p=JSON.stringify(l);u!==o?n(Ct(c)):u!==p&&t(Ct(l))}o=JSON.stringify(e()),s=JSON.stringify(r())});return()=>{$(a)}}function Ct(e){return typeof e=="object"?JSON.parse(JSON.stringify(e)):e}function Fr(e){(Array.isArray(e)?e:[e]).forEach(r=>r(K))}var Z={},Br=!1;function zr(e,t){if(Br||(Z=T(Z),Br=!0),t===void 0)return Z[e];Z[e]=t,Te(Z[e]),typeof t=="object"&&t!==null&&t.hasOwnProperty("init")&&typeof t.init=="function"&&Z[e].init()}function Kr(){return Z}var Hr={};function Vr(e,t){let r=typeof t!="function"?()=>t:t;return e instanceof Element?Tt(e,r()):(Hr[e]=r,()=>{})}function qr(e){return Object.entries(Hr).forEach(([t,r])=>{Object.defineProperty(e,t,{get(){return(...n)=>r(...n)}})}),e}function Tt(e,t,r){let n=[];for(;n.length;)n.pop()();let i=Object.entries(t).map(([s,a])=>({name:s,value:a})),o=Et(i);return i=i.map(s=>o.find(a=>a.name===s.name)?{name:`x-bind:${s.name}`,value:`"${s.value}"`}:s),pe(e,i,r).map(s=>{n.push(s.runCleanups),s()}),()=>{for(;n.length;)n.pop()()}}var Ur={};function Wr(e,t){Ur[e]=t}function Gr(e,t){return Object.entries(Ur).forEach(([r,n])=>{Object.defineProperty(e,r,{get(){return(...i)=>n.bind(t)(...i)},enumerable:!1})}),e}var li={get reactive(){return T},get release(){return $},get effect(){return N},get raw(){return at},version:"3.14.9",flushAndStopDeferringMutations:nr,dontAutoEvaluateFunctions:ke,disableEffectScheduling:Gt,startObservingMutations:ue,stopObservingMutations:dt,setReactivityEngine:Jt,onAttributeRemoved:Oe,onAttributesAdded:Ae,closestDataStack:B,skipDuringClone:A,onlyDuringClone:Tr,addRootSelector:Le,addInitSelector:$e,interceptClone:H,addScopeToNode:k,deferMutations:rr,mapAttributes:ne,evaluateLater:x,interceptInit:vr,setEvaluator:ar,mergeProxies:z,extractProp:$r,findClosest:j,onElRemoved:te,closestRoot:Y,destroyTree:P,interceptor:Re,transition:Fe,setStyles:X,mutateDom:m,directive:d,entangle:Ve,throttle:He,debounce:Ke,evaluate:R,initTree:S,nextTick:ie,prefixed:C,prefix:cr,plugin:Fr,magic:y,store:zr,start:gr,clone:Nr,cloneNode:Mr,bound:Lr,$data:Ce,watch:ve,walk:D,data:Wr,bind:Vr},K=li;function Rt(e,t){let r=Object.create(null),n=e.split(",");for(let i=0;i<n.length;i++)r[n[i]]=!0;return t?i=>!!r[i.toLowerCase()]:i=>!!r[i]}var ui="itemscope,allowfullscreen,formnovalidate,ismap,nomodule,novalidate,readonly";var Ls=Rt(ui+",async,autofocus,autoplay,controls,default,defer,disabled,hidden,loop,open,required,reversed,scoped,seamless,checked,muted,multiple,selected");var Jr=Object.freeze({}),$s=Object.freeze([]);var fi=Object.prototype.hasOwnProperty,ye=(e,t)=>fi.call(e,t),V=Array.isArray,oe=e=>Yr(e)==="[object Map]";var di=e=>typeof e=="string",qe=e=>typeof e=="symbol",be=e=>e!==null&&typeof e=="object";var pi=Object.prototype.toString,Yr=e=>pi.call(e),Mt=e=>Yr(e).slice(8,-1);var Ue=e=>di(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e;var We=e=>{let t=Object.create(null);return r=>t[r]||(t[r]=e(r))},mi=/-(\w)/g,js=We(e=>e.replace(mi,(t,r)=>r?r.toUpperCase():"")),hi=/\B([A-Z])/g,Fs=We(e=>e.replace(hi,"-$1").toLowerCase()),Nt=We(e=>e.charAt(0).toUpperCase()+e.slice(1)),Bs=We(e=>e?`on${Nt(e)}`:""),kt=(e,t)=>e!==t&&(e===e||t===t);var Dt=new WeakMap,we=[],L,Q=Symbol("iterate"),Pt=Symbol("Map key iterate");function _i(e){return e&&e._isEffect===!0}function rn(e,t=Jr){_i(e)&&(e=e.raw);let r=xi(e,t);return t.lazy||r(),r}function nn(e){e.active&&(on(e),e.options.onStop&&e.options.onStop(),e.active=!1)}var gi=0;function xi(e,t){let r=function(){if(!r.active)return e();if(!we.includes(r)){on(r);try{return bi(),we.push(r),L=r,e()}finally{we.pop(),sn(),L=we[we.length-1]}}};return r.id=gi++,r.allowRecurse=!!t.allowRecurse,r._isEffect=!0,r.active=!0,r.raw=e,r.deps=[],r.options=t,r}function on(e){let{deps:t}=e;if(t.length){for(let r=0;r<t.length;r++)t[r].delete(e);t.length=0}}var se=!0,Lt=[];function yi(){Lt.push(se),se=!1}function bi(){Lt.push(se),se=!0}function sn(){let e=Lt.pop();se=e===void 0?!0:e}function M(e,t,r){if(!se||L===void 0)return;let n=Dt.get(e);n||Dt.set(e,n=new Map);let i=n.get(r);i||n.set(r,i=new Set),i.has(L)||(i.add(L),L.deps.push(i),L.options.onTrack&&L.options.onTrack({effect:L,target:e,type:t,key:r}))}function U(e,t,r,n,i,o){let s=Dt.get(e);if(!s)return;let a=new Set,c=u=>{u&&u.forEach(p=>{(p!==L||p.allowRecurse)&&a.add(p)})};if(t==="clear")s.forEach(c);else if(r==="length"&&V(e))s.forEach((u,p)=>{(p==="length"||p>=n)&&c(u)});else switch(r!==void 0&&c(s.get(r)),t){case"add":V(e)?Ue(r)&&c(s.get("length")):(c(s.get(Q)),oe(e)&&c(s.get(Pt)));break;case"delete":V(e)||(c(s.get(Q)),oe(e)&&c(s.get(Pt)));break;case"set":oe(e)&&c(s.get(Q));break}let l=u=>{u.options.onTrigger&&u.options.onTrigger({effect:u,target:e,key:r,type:t,newValue:n,oldValue:i,oldTarget:o}),u.options.scheduler?u.options.scheduler(u):u()};a.forEach(l)}var wi=Rt("__proto__,__v_isRef,__isVue"),an=new Set(Object.getOwnPropertyNames(Symbol).map(e=>Symbol[e]).filter(qe)),Ei=cn();var vi=cn(!0);var Xr=Si();function Si(){let e={};return["includes","indexOf","lastIndexOf"].forEach(t=>{e[t]=function(...r){let n=_(this);for(let o=0,s=this.length;o<s;o++)M(n,"get",o+"");let i=n[t](...r);return i===-1||i===!1?n[t](...r.map(_)):i}}),["push","pop","shift","unshift","splice"].forEach(t=>{e[t]=function(...r){yi();let n=_(this)[t].apply(this,r);return sn(),n}}),e}function cn(e=!1,t=!1){return function(n,i,o){if(i==="__v_isReactive")return!e;if(i==="__v_isReadonly")return e;if(i==="__v_raw"&&o===(e?t?Bi:dn:t?Fi:fn).get(n))return n;let s=V(n);if(!e&&s&&ye(Xr,i))return Reflect.get(Xr,i,o);let a=Reflect.get(n,i,o);return(qe(i)?an.has(i):wi(i))||(e||M(n,"get",i),t)?a:It(a)?!s||!Ue(i)?a.value:a:be(a)?e?pn(a):et(a):a}}var Ai=Oi();function Oi(e=!1){return function(r,n,i,o){let s=r[n];if(!e&&(i=_(i),s=_(s),!V(r)&&It(s)&&!It(i)))return s.value=i,!0;let a=V(r)&&Ue(n)?Number(n)<r.length:ye(r,n),c=Reflect.set(r,n,i,o);return r===_(o)&&(a?kt(i,s)&&U(r,"set",n,i,s):U(r,"add",n,i)),c}}function Ci(e,t){let r=ye(e,t),n=e[t],i=Reflect.deleteProperty(e,t);return i&&r&&U(e,"delete",t,void 0,n),i}function Ti(e,t){let r=Reflect.has(e,t);return(!qe(t)||!an.has(t))&&M(e,"has",t),r}function Ri(e){return M(e,"iterate",V(e)?"length":Q),Reflect.ownKeys(e)}var Mi={get:Ei,set:Ai,deleteProperty:Ci,has:Ti,ownKeys:Ri},Ni={get:vi,set(e,t){return console.warn(`Set operation on key "${String(t)}" failed: target is readonly.`,e),!0},deleteProperty(e,t){return console.warn(`Delete operation on key "${String(t)}" failed: target is readonly.`,e),!0}};var $t=e=>be(e)?et(e):e,jt=e=>be(e)?pn(e):e,Ft=e=>e,Qe=e=>Reflect.getPrototypeOf(e);function Ge(e,t,r=!1,n=!1){e=e.__v_raw;let i=_(e),o=_(t);t!==o&&!r&&M(i,"get",t),!r&&M(i,"get",o);let{has:s}=Qe(i),a=n?Ft:r?jt:$t;if(s.call(i,t))return a(e.get(t));if(s.call(i,o))return a(e.get(o));e!==i&&e.get(t)}function Je(e,t=!1){let r=this.__v_raw,n=_(r),i=_(e);return e!==i&&!t&&M(n,"has",e),!t&&M(n,"has",i),e===i?r.has(e):r.has(e)||r.has(i)}function Ye(e,t=!1){return e=e.__v_raw,!t&&M(_(e),"iterate",Q),Reflect.get(e,"size",e)}function Zr(e){e=_(e);let t=_(this);return Qe(t).has.call(t,e)||(t.add(e),U(t,"add",e,e)),this}function Qr(e,t){t=_(t);let r=_(this),{has:n,get:i}=Qe(r),o=n.call(r,e);o?un(r,n,e):(e=_(e),o=n.call(r,e));let s=i.call(r,e);return r.set(e,t),o?kt(t,s)&&U(r,"set",e,t,s):U(r,"add",e,t),this}function en(e){let t=_(this),{has:r,get:n}=Qe(t),i=r.call(t,e);i?un(t,r,e):(e=_(e),i=r.call(t,e));let o=n?n.call(t,e):void 0,s=t.delete(e);return i&&U(t,"delete",e,void 0,o),s}function tn(){let e=_(this),t=e.size!==0,r=oe(e)?new Map(e):new Set(e),n=e.clear();return t&&U(e,"clear",void 0,void 0,r),n}function Xe(e,t){return function(n,i){let o=this,s=o.__v_raw,a=_(s),c=t?Ft:e?jt:$t;return!e&&M(a,"iterate",Q),s.forEach((l,u)=>n.call(i,c(l),c(u),o))}}function Ze(e,t,r){return function(...n){let i=this.__v_raw,o=_(i),s=oe(o),a=e==="entries"||e===Symbol.iterator&&s,c=e==="keys"&&s,l=i[e](...n),u=r?Ft:t?jt:$t;return!t&&M(o,"iterate",c?Pt:Q),{next(){let{value:p,done:h}=l.next();return h?{value:p,done:h}:{value:a?[u(p[0]),u(p[1])]:u(p),done:h}},[Symbol.iterator](){return this}}}}function q(e){return function(...t){{let r=t[0]?`on key "${t[0]}" `:"";console.warn(`${Nt(e)} operation ${r}failed: target is readonly.`,_(this))}return e==="delete"?!1:this}}function ki(){let e={get(o){return Ge(this,o)},get size(){return Ye(this)},has:Je,add:Zr,set:Qr,delete:en,clear:tn,forEach:Xe(!1,!1)},t={get(o){return Ge(this,o,!1,!0)},get size(){return Ye(this)},has:Je,add:Zr,set:Qr,delete:en,clear:tn,forEach:Xe(!1,!0)},r={get(o){return Ge(this,o,!0)},get size(){return Ye(this,!0)},has(o){return Je.call(this,o,!0)},add:q("add"),set:q("set"),delete:q("delete"),clear:q("clear"),forEach:Xe(!0,!1)},n={get(o){return Ge(this,o,!0,!0)},get size(){return Ye(this,!0)},has(o){return Je.call(this,o,!0)},add:q("add"),set:q("set"),delete:q("delete"),clear:q("clear"),forEach:Xe(!0,!0)};return["keys","values","entries",Symbol.iterator].forEach(o=>{e[o]=Ze(o,!1,!1),r[o]=Ze(o,!0,!1),t[o]=Ze(o,!1,!0),n[o]=Ze(o,!0,!0)}),[e,r,t,n]}var[Di,Pi,Ii,Li]=ki();function ln(e,t){let r=t?e?Li:Ii:e?Pi:Di;return(n,i,o)=>i==="__v_isReactive"?!e:i==="__v_isReadonly"?e:i==="__v_raw"?n:Reflect.get(ye(r,i)&&i in n?r:n,i,o)}var $i={get:ln(!1,!1)};var ji={get:ln(!0,!1)};function un(e,t,r){let n=_(r);if(n!==r&&t.call(e,n)){let i=Mt(e);console.warn(`Reactive ${i} contains both the raw and reactive versions of the same object${i==="Map"?" as keys":""}, which can lead to inconsistencies. Avoid differentiating between the raw and reactive versions of an object and only use the reactive version if possible.`)}}var fn=new WeakMap,Fi=new WeakMap,dn=new WeakMap,Bi=new WeakMap;function zi(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function Ki(e){return e.__v_skip||!Object.isExtensible(e)?0:zi(Mt(e))}function et(e){return e&&e.__v_isReadonly?e:mn(e,!1,Mi,$i,fn)}function pn(e){return mn(e,!0,Ni,ji,dn)}function mn(e,t,r,n,i){if(!be(e))return console.warn(`value cannot be made reactive: ${String(e)}`),e;if(e.__v_raw&&!(t&&e.__v_isReactive))return e;let o=i.get(e);if(o)return o;let s=Ki(e);if(s===0)return e;let a=new Proxy(e,s===2?n:r);return i.set(e,a),a}function _(e){return e&&_(e.__v_raw)||e}function It(e){return Boolean(e&&e.__v_isRef===!0)}y("nextTick",()=>ie);y("dispatch",e=>J.bind(J,e));y("watch",(e,{evaluateLater:t,cleanup:r})=>(n,i)=>{let o=t(n),a=ve(()=>{let c;return o(l=>c=l),c},i);r(a)});y("store",Kr);y("data",e=>Ce(e));y("root",e=>Y(e));y("refs",e=>(e._x_refs_proxy||(e._x_refs_proxy=z(Hi(e))),e._x_refs_proxy));function Hi(e){let t=[];return j(e,r=>{r._x_refs&&t.push(r._x_refs)}),t}var Bt={};function zt(e){return Bt[e]||(Bt[e]=0),++Bt[e]}function hn(e,t){return j(e,r=>{if(r._x_ids&&r._x_ids[t])return!0})}function _n(e,t){e._x_ids||(e._x_ids={}),e._x_ids[t]||(e._x_ids[t]=zt(t))}y("id",(e,{cleanup:t})=>(r,n=null)=>{let i=`${r}${n?`-${n}`:""}`;return Vi(e,i,t,()=>{let o=hn(e,r),s=o?o._x_ids[r]:zt(r);return n?`${r}-${s}-${n}`:`${r}-${s}`})});H((e,t)=>{e._x_id&&(t._x_id=e._x_id)});function Vi(e,t,r,n){if(e._x_id||(e._x_id={}),e._x_id[t])return e._x_id[t];let i=n();return e._x_id[t]=i,r(()=>{delete e._x_id[t]}),i}y("el",e=>e);gn("Focus","focus","focus");gn("Persist","persist","persist");function gn(e,t,r){y(t,n=>E(`You can't use [$${t}] without first installing the "${e}" plugin here: https://alpinejs.dev/plugins/${r}`,n))}d("modelable",(e,{expression:t},{effect:r,evaluateLater:n,cleanup:i})=>{let o=n(t),s=()=>{let u;return o(p=>u=p),u},a=n(`${t} = __placeholder`),c=u=>a(()=>{},{scope:{__placeholder:u}}),l=s();c(l),queueMicrotask(()=>{if(!e._x_model)return;e._x_removeModelListeners.default();let u=e._x_model.get,p=e._x_model.set,h=Ve({get(){return u()},set(w){p(w)}},{get(){return s()},set(w){c(w)}});i(h)})});d("teleport",(e,{modifiers:t,expression:r},{cleanup:n})=>{e.tagName.toLowerCase()!=="template"&&E("x-teleport can only be used on a <template> tag",e);let i=xn(r),o=e.content.cloneNode(!0).firstElementChild;e._x_teleport=o,o._x_teleportBack=e,e.setAttribute("data-teleport-template",!0),o.setAttribute("data-teleport-target",!0),e._x_forwardEvents&&e._x_forwardEvents.forEach(a=>{o.addEventListener(a,c=>{c.stopPropagation(),e.dispatchEvent(new c.constructor(c.type,c))})}),k(o,{},e);let s=(a,c,l)=>{l.includes("prepend")?c.parentNode.insertBefore(a,c):l.includes("append")?c.parentNode.insertBefore(a,c.nextSibling):c.appendChild(a)};m(()=>{s(o,i,t),A(()=>{S(o)})()}),e._x_teleportPutBack=()=>{let a=xn(r);m(()=>{s(e._x_teleport,a,t)})},n(()=>m(()=>{o.remove(),P(o)}))});var qi=document.createElement("div");function xn(e){let t=A(()=>document.querySelector(e),()=>qi)();return t||E(`Cannot find x-teleport element for selector: "${e}"`),t}var yn=()=>{};yn.inline=(e,{modifiers:t},{cleanup:r})=>{t.includes("self")?e._x_ignoreSelf=!0:e._x_ignore=!0,r(()=>{t.includes("self")?delete e._x_ignoreSelf:delete e._x_ignore})};d("ignore",yn);d("effect",A((e,{expression:t},{effect:r})=>{r(x(e,t))}));function ae(e,t,r,n){let i=e,o=c=>n(c),s={},a=(c,l)=>u=>l(c,u);if(r.includes("dot")&&(t=Ui(t)),r.includes("camel")&&(t=Wi(t)),r.includes("passive")&&(s.passive=!0),r.includes("capture")&&(s.capture=!0),r.includes("window")&&(i=window),r.includes("document")&&(i=document),r.includes("debounce")){let c=r[r.indexOf("debounce")+1]||"invalid-wait",l=tt(c.split("ms")[0])?Number(c.split("ms")[0]):250;o=Ke(o,l)}if(r.includes("throttle")){let c=r[r.indexOf("throttle")+1]||"invalid-wait",l=tt(c.split("ms")[0])?Number(c.split("ms")[0]):250;o=He(o,l)}return r.includes("prevent")&&(o=a(o,(c,l)=>{l.preventDefault(),c(l)})),r.includes("stop")&&(o=a(o,(c,l)=>{l.stopPropagation(),c(l)})),r.includes("once")&&(o=a(o,(c,l)=>{c(l),i.removeEventListener(t,o,s)})),(r.includes("away")||r.includes("outside"))&&(i=document,o=a(o,(c,l)=>{e.contains(l.target)||l.target.isConnected!==!1&&(e.offsetWidth<1&&e.offsetHeight<1||e._x_isShown!==!1&&c(l))})),r.includes("self")&&(o=a(o,(c,l)=>{l.target===e&&c(l)})),(Ji(t)||wn(t))&&(o=a(o,(c,l)=>{Yi(l,r)||c(l)})),i.addEventListener(t,o,s),()=>{i.removeEventListener(t,o,s)}}function Ui(e){return e.replace(/-/g,".")}function Wi(e){return e.toLowerCase().replace(/-(\w)/g,(t,r)=>r.toUpperCase())}function tt(e){return!Array.isArray(e)&&!isNaN(e)}function Gi(e){return[" ","_"].includes(e)?e:e.replace(/([a-z])([A-Z])/g,"$1-$2").replace(/[_\s]/,"-").toLowerCase()}function Ji(e){return["keydown","keyup"].includes(e)}function wn(e){return["contextmenu","click","mouse"].some(t=>e.includes(t))}function Yi(e,t){let r=t.filter(o=>!["window","document","prevent","stop","once","capture","self","away","outside","passive"].includes(o));if(r.includes("debounce")){let o=r.indexOf("debounce");r.splice(o,tt((r[o+1]||"invalid-wait").split("ms")[0])?2:1)}if(r.includes("throttle")){let o=r.indexOf("throttle");r.splice(o,tt((r[o+1]||"invalid-wait").split("ms")[0])?2:1)}if(r.length===0||r.length===1&&bn(e.key).includes(r[0]))return!1;let i=["ctrl","shift","alt","meta","cmd","super"].filter(o=>r.includes(o));return r=r.filter(o=>!i.includes(o)),!(i.length>0&&i.filter(s=>((s==="cmd"||s==="super")&&(s="meta"),e[`${s}Key`])).length===i.length&&(wn(e.type)||bn(e.key).includes(r[0])))}function bn(e){if(!e)return[];e=Gi(e);let t={ctrl:"control",slash:"/",space:" ",spacebar:" ",cmd:"meta",esc:"escape",up:"arrow-up",down:"arrow-down",left:"arrow-left",right:"arrow-right",period:".",comma:",",equal:"=",minus:"-",underscore:"_"};return t[e]=e,Object.keys(t).map(r=>{if(t[r]===e)return r}).filter(r=>r)}d("model",(e,{modifiers:t,expression:r},{effect:n,cleanup:i})=>{let o=e;t.includes("parent")&&(o=e.parentNode);let s=x(o,r),a;typeof r=="string"?a=x(o,`${r} = __placeholder`):typeof r=="function"&&typeof r()=="string"?a=x(o,`${r()} = __placeholder`):a=()=>{};let c=()=>{let h;return s(w=>h=w),En(h)?h.get():h},l=h=>{let w;s(F=>w=F),En(w)?w.set(h):a(()=>{},{scope:{__placeholder:h}})};typeof r=="string"&&e.type==="radio"&&m(()=>{e.hasAttribute("name")||e.setAttribute("name",r)});var u=e.tagName.toLowerCase()==="select"||["checkbox","radio"].includes(e.type)||t.includes("lazy")?"change":"input";let p=I?()=>{}:ae(e,u,t,h=>{l(Kt(e,t,h,c()))});if(t.includes("fill")&&([void 0,null,""].includes(c())||ze(e)&&Array.isArray(c())||e.tagName.toLowerCase()==="select"&&e.multiple)&&l(Kt(e,t,{target:e},c())),e._x_removeModelListeners||(e._x_removeModelListeners={}),e._x_removeModelListeners.default=p,i(()=>e._x_removeModelListeners.default()),e.form){let h=ae(e.form,"reset",[],w=>{ie(()=>e._x_model&&e._x_model.set(Kt(e,t,{target:e},c())))});i(()=>h())}e._x_model={get(){return c()},set(h){l(h)}},e._x_forceModelUpdate=h=>{h===void 0&&typeof r=="string"&&r.match(/\./)&&(h=""),window.fromModel=!0,m(()=>ge(e,"value",h)),delete window.fromModel},n(()=>{let h=c();t.includes("unintrusive")&&document.activeElement.isSameNode(e)||e._x_forceModelUpdate(h)})});function Kt(e,t,r,n){return m(()=>{if(r instanceof CustomEvent&&r.detail!==void 0)return r.detail!==null&&r.detail!==void 0?r.detail:r.target.value;if(ze(e))if(Array.isArray(n)){let i=null;return t.includes("number")?i=Ht(r.target.value):t.includes("boolean")?i=xe(r.target.value):i=r.target.value,r.target.checked?n.includes(i)?n:n.concat([i]):n.filter(o=>!Xi(o,i))}else return r.target.checked;else{if(e.tagName.toLowerCase()==="select"&&e.multiple)return t.includes("number")?Array.from(r.target.selectedOptions).map(i=>{let o=i.value||i.text;return Ht(o)}):t.includes("boolean")?Array.from(r.target.selectedOptions).map(i=>{let o=i.value||i.text;return xe(o)}):Array.from(r.target.selectedOptions).map(i=>i.value||i.text);{let i;return Ot(e)?r.target.checked?i=r.target.value:i=n:i=r.target.value,t.includes("number")?Ht(i):t.includes("boolean")?xe(i):t.includes("trim")?i.trim():i}}})}function Ht(e){let t=e?parseFloat(e):null;return Zi(t)?t:e}function Xi(e,t){return e==t}function Zi(e){return!Array.isArray(e)&&!isNaN(e)}function En(e){return e!==null&&typeof e=="object"&&typeof e.get=="function"&&typeof e.set=="function"}d("cloak",e=>queueMicrotask(()=>m(()=>e.removeAttribute(C("cloak")))));$e(()=>`[${C("init")}]`);d("init",A((e,{expression:t},{evaluate:r})=>typeof t=="string"?!!t.trim()&&r(t,{},!1):r(t,{},!1)));d("text",(e,{expression:t},{effect:r,evaluateLater:n})=>{let i=n(t);r(()=>{i(o=>{m(()=>{e.textContent=o})})})});d("html",(e,{expression:t},{effect:r,evaluateLater:n})=>{let i=n(t);r(()=>{i(o=>{m(()=>{e.innerHTML=o,e._x_ignoreSelf=!0,S(e),delete e._x_ignoreSelf})})})});ne(Pe(":",Ie(C("bind:"))));var vn=(e,{value:t,modifiers:r,expression:n,original:i},{effect:o,cleanup:s})=>{if(!t){let c={};qr(c),x(e,n)(u=>{Tt(e,u,i)},{scope:c});return}if(t==="key")return Qi(e,n);if(e._x_inlineBindings&&e._x_inlineBindings[t]&&e._x_inlineBindings[t].extract)return;let a=x(e,n);o(()=>a(c=>{c===void 0&&typeof n=="string"&&n.match(/\./)&&(c=""),m(()=>ge(e,t,c,r))})),s(()=>{e._x_undoAddedClasses&&e._x_undoAddedClasses(),e._x_undoAddedStyles&&e._x_undoAddedStyles()})};vn.inline=(e,{value:t,modifiers:r,expression:n})=>{t&&(e._x_inlineBindings||(e._x_inlineBindings={}),e._x_inlineBindings[t]={expression:n,extract:!1})};d("bind",vn);function Qi(e,t){e._x_keyExpression=t}Le(()=>`[${C("data")}]`);d("data",(e,{expression:t},{cleanup:r})=>{if(eo(e))return;t=t===""?"{}":t;let n={};fe(n,e);let i={};Gr(i,n);let o=R(e,t,{scope:i});(o===void 0||o===!0)&&(o={}),fe(o,e);let s=T(o);Te(s);let a=k(e,s);s.init&&R(e,s.init),r(()=>{s.destroy&&R(e,s.destroy),a()})});H((e,t)=>{e._x_dataStack&&(t._x_dataStack=e._x_dataStack,t.setAttribute("data-has-alpine-state",!0))});function eo(e){return I?Be?!0:e.hasAttribute("data-has-alpine-state"):!1}d("show",(e,{modifiers:t,expression:r},{effect:n})=>{let i=x(e,r);e._x_doHide||(e._x_doHide=()=>{m(()=>{e.style.setProperty("display","none",t.includes("important")?"important":void 0)})}),e._x_doShow||(e._x_doShow=()=>{m(()=>{e.style.length===1&&e.style.display==="none"?e.removeAttribute("style"):e.style.removeProperty("display")})});let o=()=>{e._x_doHide(),e._x_isShown=!1},s=()=>{e._x_doShow(),e._x_isShown=!0},a=()=>setTimeout(s),c=he(p=>p?s():o(),p=>{typeof e._x_toggleAndCascadeWithTransitions=="function"?e._x_toggleAndCascadeWithTransitions(e,p,s,o):p?a():o()}),l,u=!0;n(()=>i(p=>{!u&&p===l||(t.includes("immediate")&&(p?a():o()),c(p),l=p,u=!1)}))});d("for",(e,{expression:t},{effect:r,cleanup:n})=>{let i=ro(t),o=x(e,i.items),s=x(e,e._x_keyExpression||"index");e._x_prevKeys=[],e._x_lookup={},r(()=>to(e,i,o,s)),n(()=>{Object.values(e._x_lookup).forEach(a=>m(()=>{P(a),a.remove()})),delete e._x_prevKeys,delete e._x_lookup})});function to(e,t,r,n){let i=s=>typeof s=="object"&&!Array.isArray(s),o=e;r(s=>{no(s)&&s>=0&&(s=Array.from(Array(s).keys(),f=>f+1)),s===void 0&&(s=[]);let a=e._x_lookup,c=e._x_prevKeys,l=[],u=[];if(i(s))s=Object.entries(s).map(([f,g])=>{let b=Sn(t,g,f,s);n(v=>{u.includes(v)&&E("Duplicate key on x-for",e),u.push(v)},{scope:{index:f,...b}}),l.push(b)});else for(let f=0;f<s.length;f++){let g=Sn(t,s[f],f,s);n(b=>{u.includes(b)&&E("Duplicate key on x-for",e),u.push(b)},{scope:{index:f,...g}}),l.push(g)}let p=[],h=[],w=[],F=[];for(let f=0;f<c.length;f++){let g=c[f];u.indexOf(g)===-1&&w.push(g)}c=c.filter(f=>!w.includes(f));let Ee="template";for(let f=0;f<u.length;f++){let g=u[f],b=c.indexOf(g);if(b===-1)c.splice(f,0,g),p.push([Ee,f]);else if(b!==f){let v=c.splice(f,1)[0],O=c.splice(b-1,1)[0];c.splice(f,0,O),c.splice(b,0,v),h.push([v,O])}else F.push(g);Ee=g}for(let f=0;f<w.length;f++){let g=w[f];g in a&&(m(()=>{P(a[g]),a[g].remove()}),delete a[g])}for(let f=0;f<h.length;f++){let[g,b]=h[f],v=a[g],O=a[b],ee=document.createElement("div");m(()=>{O||E('x-for ":key" is undefined or invalid',o,b,a),O.after(ee),v.after(O),O._x_currentIfEl&&O.after(O._x_currentIfEl),ee.before(v),v._x_currentIfEl&&v.after(v._x_currentIfEl),ee.remove()}),O._x_refreshXForScope(l[u.indexOf(b)])}for(let f=0;f<p.length;f++){let[g,b]=p[f],v=g==="template"?o:a[g];v._x_currentIfEl&&(v=v._x_currentIfEl);let O=l[b],ee=u[b],ce=document.importNode(o.content,!0).firstElementChild,qt=T(O);k(ce,qt,o),ce._x_refreshXForScope=On=>{Object.entries(On).forEach(([Cn,Tn])=>{qt[Cn]=Tn})},m(()=>{v.after(ce),A(()=>S(ce))()}),typeof ee=="object"&&E("x-for key cannot be an object, it must be a string or an integer",o),a[ee]=ce}for(let f=0;f<F.length;f++)a[F[f]]._x_refreshXForScope(l[u.indexOf(F[f])]);o._x_prevKeys=u})}function ro(e){let t=/,([^,\}\]]*)(?:,([^,\}\]]*))?$/,r=/^\s*\(|\)\s*$/g,n=/([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/,i=e.match(n);if(!i)return;let o={};o.items=i[2].trim();let s=i[1].replace(r,"").trim(),a=s.match(t);return a?(o.item=s.replace(t,"").trim(),o.index=a[1].trim(),a[2]&&(o.collection=a[2].trim())):o.item=s,o}function Sn(e,t,r,n){let i={};return/^\[.*\]$/.test(e.item)&&Array.isArray(t)?e.item.replace("[","").replace("]","").split(",").map(s=>s.trim()).forEach((s,a)=>{i[s]=t[a]}):/^\{.*\}$/.test(e.item)&&!Array.isArray(t)&&typeof t=="object"?e.item.replace("{","").replace("}","").split(",").map(s=>s.trim()).forEach(s=>{i[s]=t[s]}):i[e.item]=t,e.index&&(i[e.index]=r),e.collection&&(i[e.collection]=n),i}function no(e){return!Array.isArray(e)&&!isNaN(e)}function An(){}An.inline=(e,{expression:t},{cleanup:r})=>{let n=Y(e);n._x_refs||(n._x_refs={}),n._x_refs[t]=e,r(()=>delete n._x_refs[t])};d("ref",An);d("if",(e,{expression:t},{effect:r,cleanup:n})=>{e.tagName.toLowerCase()!=="template"&&E("x-if can only be used on a <template> tag",e);let i=x(e,t),o=()=>{if(e._x_currentIfEl)return e._x_currentIfEl;let a=e.content.cloneNode(!0).firstElementChild;return k(a,{},e),m(()=>{e.after(a),A(()=>S(a))()}),e._x_currentIfEl=a,e._x_undoIf=()=>{m(()=>{P(a),a.remove()}),delete e._x_currentIfEl},a},s=()=>{e._x_undoIf&&(e._x_undoIf(),delete e._x_undoIf)};r(()=>i(a=>{a?o():s()})),n(()=>e._x_undoIf&&e._x_undoIf())});d("id",(e,{expression:t},{evaluate:r})=>{r(t).forEach(i=>_n(e,i))});H((e,t)=>{e._x_ids&&(t._x_ids=e._x_ids)});ne(Pe("@",Ie(C("on:"))));d("on",A((e,{value:t,modifiers:r,expression:n},{cleanup:i})=>{let o=n?x(e,n):()=>{};e.tagName.toLowerCase()==="template"&&(e._x_forwardEvents||(e._x_forwardEvents=[]),e._x_forwardEvents.includes(t)||e._x_forwardEvents.push(t));let s=ae(e,t,r,a=>{o(()=>{},{scope:{$event:a},params:[a]})});i(()=>s())}));rt("Collapse","collapse","collapse");rt("Intersect","intersect","intersect");rt("Focus","trap","focus");rt("Mask","mask","mask");function rt(e,t,r){d(t,n=>E(`You can't use [x-${t}] without first installing the "${e}" plugin here: https://alpinejs.dev/plugins/${r}`,n))}K.setEvaluator(xt);K.setReactivityEngine({reactive:et,effect:rn,release:nn,raw:_});var Vt=K;window.Alpine=Vt;queueMicrotask(()=>{Vt.start()});})();
\ No newline at end of file
webui/js/components.js
new
+245
@@ -0,0 +1,245 @@
1
+// Import a component into a target element
2
+// Import a component and recursively load its nested components
3
+// Returns the parsed document for additional processing
4
+
5
+// cache object to store loaded components
6
+const componentCache = {};
7
+
8
+export async function importComponent(path, targetElement) {
9
+ try {
10
+ if (!targetElement) {
11
+ throw new Error("Target element is required");
12
+ }
13
+
14
+ // Show loading indicator
15
+ targetElement.innerHTML = '<div class="loading"></div>';
16
+
17
+ // full component url
18
+ const componentUrl = "components/" + path;
19
+
20
+ // get html from cache or fetch it
21
+ let html;
22
+ if (componentCache[componentUrl]) {
23
+ html = componentCache[componentUrl];
24
+ } else {
25
+ const response = await fetch(componentUrl);
26
+ if (!response.ok) {
27
+ throw new Error(
28
+ `Error loading component ${path}: ${response.statusText}`
29
+ );
30
+ }
31
+ html = await response.text();
32
+ // store in cache
33
+ componentCache[componentUrl] = html;
34
+ }
35
+ const parser = new DOMParser();
36
+ const doc = parser.parseFromString(html, "text/html");
37
+
38
+ const allNodes = [
39
+ ...doc.querySelectorAll("style"),
40
+ ...doc.querySelectorAll("script"),
41
+ ...doc.body.childNodes,
42
+ ];
43
+
44
+ const loadPromises = [];
45
+ let blobCounter = 0;
46
+
47
+ for (const node of allNodes) {
48
+ if (node.nodeName === "SCRIPT") {
49
+ const isModule =
50
+ node.type === "module" || node.getAttribute("type") === "module";
51
+
52
+ if (isModule) {
53
+ if (node.src) {
54
+ // For <script type="module" src="..." use dynamic import
55
+ const resolvedUrl = new URL(
56
+ node.src,
57
+ globalThis.location.origin
58
+ ).toString();
59
+
60
+ // Check if module is already in cache
61
+ if (!componentCache[resolvedUrl]) {
62
+ const modulePromise = import(resolvedUrl);
63
+ componentCache[resolvedUrl] = modulePromise;
64
+ loadPromises.push(modulePromise);
65
+ }
66
+ } else {
67
+ const virtualUrl = `${componentUrl.replaceAll(
68
+ "/",
69
+ "_"
70
+ )}.${++blobCounter}.js`;
71
+
72
+ // For inline module scripts, use cache or create blob
73
+ if (!componentCache[virtualUrl]) {
74
+ // Transform relative import paths to absolute URLs
75
+ let content = node.textContent.replace(
76
+ /import\s+([^'"]+)\s+from\s+["']([^"']+)["']/g,
77
+ (match, bindings, importPath) => {
78
+ // Convert relative OR root-based (e.g. /src/...) to absolute URLs
79
+ if (!/^https?:\/\//.test(importPath)) {
80
+ const absoluteUrl = new URL(
81
+ importPath,
82
+ globalThis.location.origin
83
+ ).href;
84
+ return `import ${bindings} from "${absoluteUrl}"`;
85
+ }
86
+ return match;
87
+ }
88
+ );
89
+
90
+ // Add sourceURL to the content
91
+ content += `\n//# sourceURL=${virtualUrl}`;
92
+
93
+ // Create a Blob from the rewritten content
94
+ const blob = new Blob([content], {
95
+ type: "text/javascript",
96
+ });
97
+ const blobUrl = URL.createObjectURL(blob);
98
+
99
+ const modulePromise = import(blobUrl)
100
+ .catch((err) => {
101
+ console.error("Failed to load inline module", err);
102
+ throw err;
103
+ })
104
+ .finally(() => URL.revokeObjectURL(blobUrl));
105
+
106
+ componentCache[virtualUrl] = modulePromise;
107
+ loadPromises.push(modulePromise);
108
+ }
109
+ }
110
+ } else {
111
+ // Non-module script
112
+ const script = document.createElement("script");
113
+ Array.from(node.attributes || []).forEach((attr) => {
114
+ script.setAttribute(attr.name, attr.value);
115
+ });
116
+ script.textContent = node.textContent;
117
+
118
+ if (script.src) {
119
+ const promise = new Promise((resolve, reject) => {
120
+ script.onload = resolve;
121
+ script.onerror = reject;
122
+ });
123
+ loadPromises.push(promise);
124
+ }
125
+
126
+ targetElement.appendChild(script);
127
+ }
128
+ } else if (
129
+ node.nodeName === "STYLE" ||
130
+ (node.nodeName === "LINK" && node.rel === "stylesheet")
131
+ ) {
132
+ const clone = node.cloneNode(true);
133
+
134
+ if (clone.tagName === "LINK" && clone.rel === "stylesheet") {
135
+ const promise = new Promise((resolve, reject) => {
136
+ clone.onload = resolve;
137
+ clone.onerror = reject;
138
+ });
139
+ loadPromises.push(promise);
140
+ }
141
+
142
+ targetElement.appendChild(clone);
143
+ } else {
144
+ const clone = node.cloneNode(true);
145
+ targetElement.appendChild(clone);
146
+ }
147
+ }
148
+
149
+ // Wait for all tracked external scripts/styles to finish loading
150
+ await Promise.all(loadPromises);
151
+
152
+ // Remove loading indicator
153
+ const loadingEl = targetElement.querySelector(".loading");
154
+ if (loadingEl) {
155
+ targetElement.removeChild(loadingEl);
156
+ }
157
+
158
+ // // Load any nested components
159
+ // await loadComponents([targetElement]);
160
+
161
+ // Return parsed document
162
+ return doc;
163
+ } catch (error) {
164
+ console.error("Error importing component:", error);
165
+ throw error;
166
+ }
167
+}
168
+
169
+// Load all x-component tags starting from root elements
170
+export async function loadComponents(roots = [document.documentElement]) {
171
+ try {
172
+ // Convert single root to array if needed
173
+ const rootElements = Array.isArray(roots) ? roots : [roots];
174
+
175
+ // Find all top-level components and load them in parallel
176
+ const components = rootElements.flatMap((root) =>
177
+ Array.from(root.querySelectorAll("x-component"))
178
+ );
179
+
180
+ if (components.length === 0) return;
181
+
182
+ await Promise.all(
183
+ components.map(async (component) => {
184
+ const path = component.getAttribute("path");
185
+ if (!path) {
186
+ console.error("x-component missing path attribute:", component);
187
+ return;
188
+ }
189
+ await importComponent(path, component);
190
+ })
191
+ );
192
+ } catch (error) {
193
+ console.error("Error loading components:", error);
194
+ }
195
+}
196
+
197
+// Function to traverse parents and collect x-component attributes
198
+export function getParentAttributes(el) {
199
+ let element = el;
200
+ let attrs = {};
201
+
202
+ while (element) {
203
+ if (element.tagName.toLowerCase() === 'x-component') {
204
+ // Get all attributes
205
+ for (let attr of element.attributes) {
206
+ try {
207
+ // Try to parse as JSON first
208
+ attrs[attr.name] = JSON.parse(attr.value);
209
+ } catch(_e) {
210
+ // If not JSON, use raw value
211
+ attrs[attr.name] = attr.value;
212
+ }
213
+ }
214
+ }
215
+ element = element.parentElement;
216
+ }
217
+ return attrs;
218
+}
219
+// expose as global for x-components in Alpine
220
+globalThis.xAttrs = getParentAttributes;
221
+
222
+// Initialize when DOM is ready
223
+if (document.readyState === 'loading') {
224
+ document.addEventListener('DOMContentLoaded', () => loadComponents());
225
+} else {
226
+ loadComponents();
227
+}
228
+
229
+// Watch for DOM changes to dynamically load x-components
230
+const observer = new MutationObserver((mutations) => {
231
+ for (const mutation of mutations) {
232
+ for (const node of mutation.addedNodes) {
233
+ if (node.nodeType === 1) {
234
+ // ELEMENT_NODE
235
+ // Check if this node or its descendants contain x-component(s)
236
+ if (node.matches?.("x-component")) {
237
+ importComponent(node.getAttribute("path"), node);
238
+ } else if (node.querySelectorAll) {
239
+ loadComponents([node]);
240
+ }
241
+ }
242
+ }
243
+ }
244
+});
245
+observer.observe(document.body, { childList: true, subtree: true });
webui/js/initFw.js
new
+13
@@ -0,0 +1,13 @@
1
+import * as _modals from "./modals.js";
2
+import * as _components from "./components.js";
3
+
4
+await import("./alpine.min.js");
5
+
6
+// add x-destroy directive
7
+Alpine.directive(
8
+ "destroy",
9
+ (el, { expression }, { evaluateLater, cleanup }) => {
10
+ const onDestroy = evaluateLater(expression);
11
+ cleanup(() => onDestroy());
12
+ }
13
+);
webui/js/modals.js
new
+264
@@ -0,0 +1,264 @@
1
+// Import the component loader and page utilities
2
+import { importComponent } from "/js/components.js";
3
+
4
+
5
+// Modal functionality
6
+const modalStack = [];
7
+
8
+// Create a single backdrop for all modals
9
+const backdrop = document.createElement("div");
10
+backdrop.className = "modal-backdrop";
11
+backdrop.style.display = "none";
12
+backdrop.style.backdropFilter = "blur(5px)";
13
+
14
+// Make sure we only close when clicking directly on the backdrop, not its children
15
+backdrop.addEventListener("click", (event) => {
16
+ if (event.target === backdrop) {
17
+ closeModal();
18
+ }
19
+});
20
+document.body.appendChild(backdrop);
21
+
22
+// Function to update z-index for all modals and backdrop
23
+function updateModalZIndexes() {
24
+ // Base z-index for modals
25
+ const baseZIndex = 3000;
26
+
27
+ // Update z-index for all modals
28
+ modalStack.forEach((modal, index) => {
29
+ // For first modal, z-index is baseZIndex
30
+ // For second modal, z-index is baseZIndex + 20
31
+ // This leaves room for the backdrop between them
32
+ modal.element.style.zIndex = baseZIndex + index * 20;
33
+ });
34
+
35
+ // Always show backdrop
36
+ backdrop.style.display = "block";
37
+
38
+ if (modalStack.length > 1) {
39
+ // For multiple modals, position backdrop between the top two
40
+ const topModalIndex = modalStack.length - 1;
41
+ const previousModalZIndex = baseZIndex + (topModalIndex - 1) * 20;
42
+ backdrop.style.zIndex = previousModalZIndex + 10;
43
+ } else if (modalStack.length === 1) {
44
+ // For single modal, position backdrop below it
45
+ backdrop.style.zIndex = baseZIndex - 1;
46
+ } else {
47
+ // No modals, hide backdrop
48
+ backdrop.style.display = "none";
49
+ }
50
+}
51
+
52
+// Function to create a new modal element
53
+function createModalElement(name) {
54
+ // Create modal element
55
+ const newModal = document.createElement("div");
56
+ newModal.className = "modal";
57
+ newModal.modalName = name; // save name to the object
58
+
59
+ // Add click handler to the modal element to close when clicking outside content
60
+ newModal.addEventListener("click", (event) => {
61
+ // Only close if clicking directly on the modal container, not its content
62
+ if (event.target === newModal) {
63
+ closeModal();
64
+ }
65
+ });
66
+
67
+ // Create modal structure
68
+ newModal.innerHTML = `
69
+ <div class="modal-inner">
70
+ <div class="modal-header">
71
+ <h2 class="modal-title"></h2>
72
+ <button class="modal-close">×</button>
73
+ </div>
74
+ <div class="modal-scroll">
75
+ <div class="modal-bd"></div>
76
+ </div>
77
+ </div>
78
+ `;
79
+
80
+ // Add modal to DOM
81
+ document.body.appendChild(newModal);
82
+
83
+ // Show the modal
84
+ newModal.classList.add("show");
85
+
86
+ // Update modal z-indexes
87
+ updateModalZIndexes();
88
+
89
+ return {
90
+ element: newModal,
91
+ title: newModal.querySelector(".modal-title"),
92
+ body: newModal.querySelector(".modal-bd"),
93
+ close: newModal.querySelector(".modal-close"),
94
+ styles: [],
95
+ scripts: [],
96
+ };
97
+}
98
+
99
+// Function to open modal with content from URL
100
+export function openModal(modalPath) {
101
+ return new Promise((resolve) => {
102
+ try {
103
+ // Create new modal instance
104
+ const modal = createModalElement();
105
+
106
+ new MutationObserver(
107
+ (_, o) =>
108
+ !document.contains(modal.element) && (o.disconnect(), resolve())
109
+ ).observe(document.body, { childList: true, subtree: true });
110
+
111
+ // Set a loading state
112
+ modal.body.innerHTML = '<div class="loading">Loading...</div>';
113
+
114
+ // Already added to stack above
115
+
116
+ // Setup close button handler for this specific modal
117
+ modal.close.addEventListener("click", () => closeModal());
118
+
119
+ // Use importComponent to load the modal content
120
+ // This handles all HTML, styles, scripts and nested components
121
+ // Updated path to use the new folder structure with modal.html
122
+ const componentPath = modalPath; // `modals/${modalPath}/modal.html`;
123
+
124
+ // Use importComponent which now returns the parsed document
125
+ importComponent(componentPath, modal.body)
126
+ .then((doc) => {
127
+ // Set the title from the document
128
+ modal.title.innerHTML = doc.title || modalPath;
129
+ })
130
+ .catch((error) => {
131
+ console.error("Error loading modal content:", error);
132
+ modal.body.innerHTML = `<div class="error">Failed to load modal content: ${error.message}</div>`;
133
+ });
134
+
135
+ // Add modal to stack and show it
136
+ // Add modal to stack
137
+ modalStack.push(modal);
138
+ modal.element.classList.add("show");
139
+ document.body.style.overflow = "hidden";
140
+
141
+ // Update modal z-indexes
142
+ updateModalZIndexes();
143
+
144
+ // Setup close button handler for this specific modal
145
+ modal.close.addEventListener("click", () => closeModal());
146
+ } catch (error) {
147
+ console.error("Error loading modal content:", error);
148
+ resolve();
149
+ }
150
+ });
151
+}
152
+
153
+// Function to close modal
154
+export function closeModal(modalName=null) {
155
+ if (modalStack.length === 0) return;
156
+
157
+ let modalIndex = modalStack.length - 1; // Default to last modal
158
+ let modal;
159
+
160
+ if (modalName) {
161
+ // Find the modal with the specified name in the stack
162
+ modalIndex = modalStack.findIndex((modal) => modal.modalName === modalName);
163
+ if (modalIndex === -1) return; // Modal not found in stack
164
+
165
+ // Get the modal from stack at the found index
166
+ modal = modalStack[modalIndex];
167
+ // Remove the modal from stack
168
+ modalStack.splice(modalIndex, 1);
169
+ } else {
170
+ // Just remove the last modal
171
+ modal = modalStack.pop();
172
+ }
173
+
174
+ // Remove modal-specific styles and scripts immediately
175
+ modal.styles.forEach((styleId) => {
176
+ document.querySelector(`[data-modal-style="${styleId}"]`)?.remove();
177
+ });
178
+ modal.scripts.forEach((scriptId) => {
179
+ document.querySelector(`[data-modal-script="${scriptId}"]`)?.remove();
180
+ });
181
+
182
+ // First remove the show class to trigger the transition
183
+ modal.element.classList.remove("show");
184
+
185
+ // Remove the modal element from DOM after animation
186
+ modal.element.addEventListener(
187
+ "transitionend",
188
+ () => {
189
+ // Make sure the modal is completely removed from the DOM
190
+ if (modal.element.parentNode) {
191
+ modal.element.parentNode.removeChild(modal.element);
192
+ }
193
+ },
194
+ { once: true }
195
+ );
196
+
197
+ // Fallback in case the transition event doesn't fire
198
+ setTimeout(() => {
199
+ if (modal.element.parentNode) {
200
+ modal.element.parentNode.removeChild(modal.element);
201
+ }
202
+ }, 500); // 500ms should be enough for the transition to complete
203
+
204
+ // Handle backdrop visibility and body overflow
205
+ if (modalStack.length === 0) {
206
+ // Hide backdrop when no modals are left
207
+ backdrop.style.display = "none";
208
+ document.body.style.overflow = "";
209
+ } else {
210
+ // Update modal z-indexes
211
+ updateModalZIndexes();
212
+ }
213
+}
214
+
215
+// Function to scroll to element by ID within the last modal
216
+export function scrollModal(id) {
217
+ if (!id) return;
218
+
219
+ // Get the last modal in the stack
220
+ const lastModal = modalStack[modalStack.length - 1].element;
221
+ if (!lastModal) return;
222
+
223
+ // Find the modal container and target element
224
+ const modalContainer = lastModal.querySelector(".modal-scroll");
225
+ const targetElement = lastModal.querySelector(`#${id}`);
226
+
227
+ if (modalContainer && targetElement) {
228
+ modalContainer.scrollTo({
229
+ top: targetElement.offsetTop - 20, // 20px padding from top
230
+ behavior: "smooth",
231
+ });
232
+ }
233
+}
234
+
235
+// Make scrollModal globally available
236
+globalThis.scrollModal = scrollModal;
237
+
238
+// Handle modal content loading from clicks
239
+document.addEventListener("click", async (e) => {
240
+ const modalTrigger = e.target.closest("[data-modal-content]");
241
+ if (modalTrigger) {
242
+ e.preventDefault();
243
+ if (
244
+ modalTrigger.hasAttribute("disabled") ||
245
+ modalTrigger.classList.contains("disabled")
246
+ ) {
247
+ return;
248
+ }
249
+ const modalPath = modalTrigger.getAttribute("href");
250
+ await openModal(modalPath);
251
+ }
252
+});
253
+
254
+// Close modal on escape key (closes only the top modal)
255
+document.addEventListener("keydown", (e) => {
256
+ if (e.key === "Escape" && modalStack.length > 0) {
257
+ closeModal();
258
+ }
259
+});
260
+
261
+// also export as global function
262
+globalThis.openModal = openModal;
263
+globalThis.closeModal = closeModal;
264
+globalThis.scrollModal = scrollModal;
webui/js/settings.js
+7
-2
@@ -1,3 +1,4 @@
1
+
2
const settingsModalProxy = {
3
isOpen: false,
4
settings: {},
@@ -280,8 +281,12 @@ const settingsModalProxy = {
281
}
282
},
283
283
- handleFieldButton(field) {
284
- console.log(`Button clicked: ${field.action}`);
284
+ async handleFieldButton(field) {
285
+ console.log(`Button clicked: ${field.id}`);
286
+
287
+ if (field.id === "mcp_servers_config") {
288
+ openModal("settings/mcp/client/mcp-servers.html");
289
+ }
290
}
291
};
292
webui/js/sleep.js
new
+27
@@ -0,0 +1,27 @@
1
+/** Async function that waits for specified number of time units. */
2
+export async function sleep(miliseconds = 0, seconds = 0, minutes = 0, hours = 0, days = 0) {
3
+ hours += days * 24;
4
+ minutes += hours * 60;
5
+ seconds += minutes * 60;
6
+ miliseconds += seconds * 1000;
7
+ await new Promise((resolve) => setTimeout(resolve, miliseconds));
8
+ }
9
+ export default sleep;
10
+
11
+ /** Equals to Sleep(0), but can be used to yield break a coroutine after N interations. */
12
+ let yieldIterations = 0;
13
+ export async function Yield(afterIterations = 1) {
14
+ yieldIterations++;
15
+ if (yieldIterations >= afterIterations) {
16
+ await new Promise((resolve) => setTimeout(resolve, 0));
17
+ yieldIterations = 0;
18
+ }
19
+ }
20
+
21
+ /** Awaits equivalent of Sleep(0) N times which means it skips N-1 turns in the eventQueue. */
22
+ export async function Skip(turns = 1) {
23
+ while (turns > 0) {
24
+ await new Promise((resolve) => setTimeout(resolve, 0));
25
+ turns--;
26
+ }
27
+ }
\ No newline at end of file
webui/js/timeout.js
new
+16
@@ -0,0 +1,16 @@
1
+// function timeout(ms: number, errorMessage: string = "Operation timed out") {
2
+// let timeoutId: number;
3
+// const promise = new Promise<never>((_, reject) => {
4
+// timeoutId = setTimeout(() => {
5
+// reject(new Error(errorMessage));
6
+// }, ms);
7
+// });
8
+// return { promise, cancel: () => clearTimeout(timeoutId) };
9
+// }
10
+
11
+// export async function Timeout<T>(promise: Promise<T>, ms: number): Promise<T> {
12
+// const { promise: timeoutPromise, cancel: cancelTimeout } = timeout(ms);
13
+
14
+// // Race the timeout against the original promise
15
+// return await Promise.race([promise, timeoutPromise]).finally(cancelTimeout);
16
+// }