frontend components refactor cleanup
frdel committed
Oct 24, 2025 at 11:42 UTC
8c6c1e78d54eb3e95a922e1a7a4efddd551b0d6c
7 files changed
+190
-162
webui/components/messages/action-buttons/simple-action-buttons.js
+2
-2
@@ -2,7 +2,7 @@
2
import { store as speechStore } from "/components/chat/speech/speech-store.js";
3
4
// Extract text content from different message types
5
-function getTextContent(element) {
5
+function getTextContent(element,html=false) {
6
// Get all children except action buttons
7
const textParts = [];
8
// Loop through all child elements
@@ -15,7 +15,7 @@ function getTextContent(element) {
15
continue;
16
}
17
// Get text content from the child
18
- const text = child.innerText || "";
18
+ const text = (html ? child.innerHTML : child.innerText) || "";
19
if (text.trim()) {
20
textParts.push(text.trim());
21
}
webui/components/projects/project-selector.html
+2
-2
@@ -7,7 +7,7 @@
7
</head>
8
9
<body>
10
- <div x-data>
10
+ <!-- <div x-data>
11
<template x-if="$store.projects">
12
<div>
13
<template x-if="$store.selectedProject">
@@ -18,7 +18,7 @@
18
</template>
19
</div>
20
</template>
21
- </div>
21
+ </div> -->
22
</body>
23
24
</html>
\ No newline at end of file
webui/components/sidebar/bottom/preferences/preferences-panel.html
+6
-6
@@ -16,42 +16,42 @@
16
</svg>
17
</h3>
18
<ul class="config-list collapse show" id="pref-list">
19
- <li x-data x-init="$watch('$store.preferences.autoScroll', v => $store.preferences.applyAutoScroll(v))">
19
+ <li x-data>
20
<span>Autoscroll</span>
21
<label class="switch">
22
<input id="auto-scroll-switch" type="checkbox" x-model="$store.preferences.autoScroll">
23
<span class="slider"></span>
24
</label>
25
</li>
26
- <li x-data x-init="$watch('$store.preferences.darkMode', v => $store.preferences.applyDarkMode(v))">
26
+ <li x-data>
27
<span class="switch-label">Dark mode</span>
28
<label class="switch">
29
<input type="checkbox" x-model="$store.preferences.darkMode">
30
<span class="slider"></span>
31
</label>
32
</li>
33
- <li x-data x-init="$watch('$store.preferences.speech', v => $store.preferences.applySpeech(v))">
33
+ <li x-data>
34
<span class="switch-label">Speech</span>
35
<label class="switch">
36
<input type="checkbox" x-model="$store.preferences.speech">
37
<span class="slider"></span>
38
</label>
39
</li>
40
- <li x-data x-init="$watch('$store.preferences.showThoughts', v => $store.preferences.applyShowThoughts(v))">
40
+ <li x-data>
41
<span>Show thoughts</span>
42
<label class="switch">
43
<input type="checkbox" x-model="$store.preferences.showThoughts">
44
<span class="slider"></span>
45
</label>
46
</li>
47
- <li x-data x-init="$watch('$store.preferences.showJson', v => $store.preferences.applyShowJson(v))">
47
+ <li x-data>
48
<span>Show JSON</span>
49
<label class="switch">
50
<input type="checkbox" x-model="$store.preferences.showJson">
51
<span class="slider"></span>
52
</label>
53
</li>
54
- <li x-data x-init="$watch('$store.preferences.showUtils', v => $store.preferences.applyShowUtils(v))">
54
+ <li x-data>
55
<span>Show utility messages</span>
56
<label class="switch">
57
<input type="checkbox" x-model="$store.preferences.showUtils">
webui/components/sidebar/bottom/preferences/preferences-store.js
+92
-42
@@ -1,14 +1,63 @@
1
import { createStore } from "/js/AlpineStore.js";
2
+import * as css from "/js/css.js";
3
+import { store as speechStore } from "/components/chat/speech/speech-store.js";
4
5
// Preferences store centralizes user preference toggles and side-effects
6
const model = {
7
// UI toggles (initialized with safe defaults, loaded from localStorage in init)
6
- autoScroll: true,
7
- darkMode: true,
8
- speech: false,
9
- showThoughts: true,
10
- showJson: false,
11
- showUtils: false,
8
+ get autoScroll() {
9
+ return this._autoScroll;
10
+ },
11
+ set autoScroll(value) {
12
+ this._autoScroll = value;
13
+ this._applyAutoScroll(value);
14
+ },
15
+ _autoScroll: true,
16
+
17
+ get darkMode() {
18
+ return this._darkMode;
19
+ },
20
+ set darkMode(value) {
21
+ this._darkMode = value;
22
+ this._applyDarkMode(value);
23
+ },
24
+ _darkMode: true,
25
+
26
+ get speech() {
27
+ return this._speech;
28
+ },
29
+ set speech(value) {
30
+ this._speech = value;
31
+ this._applySpeech(value);
32
+ },
33
+ _speech: false,
34
+
35
+ get showThoughts() {
36
+ return this._showThoughts;
37
+ },
38
+ set showThoughts(value) {
39
+ this._showThoughts = value;
40
+ this._applyShowThoughts(value);
41
+ },
42
+ _showThoughts: true,
43
+
44
+ get showJson() {
45
+ return this._showJson;
46
+ },
47
+ set showJson(value) {
48
+ this._showJson = value;
49
+ this._applyShowJson(value);
50
+ },
51
+ _showJson: false,
52
+
53
+ get showUtils() {
54
+ return this._showUtils;
55
+ },
56
+ set showUtils(value) {
57
+ this._showUtils = value;
58
+ this._applyShowUtils(value);
59
+ },
60
+ _showUtils: false,
61
62
// Initialize preferences and apply current state
63
init() {
@@ -16,68 +65,69 @@ const model = {
65
// Load persisted preferences with safe fallbacks
66
try {
67
const storedDarkMode = localStorage.getItem("darkMode");
19
- this.darkMode = storedDarkMode !== "false";
68
+ this._darkMode = storedDarkMode !== "false";
69
} catch {
21
- this.darkMode = true; // Default to dark mode if localStorage is unavailable
70
+ this._darkMode = true; // Default to dark mode if localStorage is unavailable
71
}
72
73
try {
74
const storedSpeech = localStorage.getItem("speech");
26
- this.speech = storedSpeech === "true";
75
+ this._speech = storedSpeech === "true";
76
} catch {
28
- this.speech = false; // Default to speech off if localStorage is unavailable
77
+ this._speech = false; // Default to speech off if localStorage is unavailable
78
}
79
80
// Apply all preferences
32
- this.applyDarkMode(this.darkMode);
33
- this.applyAutoScroll(this.autoScroll);
34
- this.applySpeech(this.speech);
35
- this.applyShowThoughts(this.showThoughts);
36
- this.applyShowJson(this.showJson);
37
- this.applyShowUtils(this.showUtils);
81
+ this._applyDarkMode(this._darkMode);
82
+ this._applyAutoScroll(this._autoScroll);
83
+ this._applySpeech(this._speech);
84
+ this._applyShowThoughts(this._showThoughts);
85
+ this._applyShowJson(this._showJson);
86
+ this._applyShowUtils(this._showUtils);
87
} catch (e) {
88
console.error("Failed to initialize preferences store", e);
89
}
90
},
91
43
- // Side-effect appliers delegate to existing global handlers for parity
44
- applyAutoScroll(value = this.autoScroll) {
45
- if (typeof globalThis.toggleAutoScroll === "function") {
46
- globalThis.toggleAutoScroll(value);
47
- }
92
+ _applyAutoScroll(value) {
93
+ // nothing for now
94
},
95
50
- applyDarkMode(value = this.darkMode) {
51
- if (typeof globalThis.toggleDarkMode === "function") {
52
- globalThis.toggleDarkMode(value);
96
+ _applyDarkMode(value) {
97
+ if (value) {
98
+ document.body.classList.remove("light-mode");
99
+ document.body.classList.add("dark-mode");
100
+ } else {
101
+ document.body.classList.remove("dark-mode");
102
+ document.body.classList.add("light-mode");
103
}
104
+ localStorage.setItem("darkMode", value);
105
},
106
56
- applySpeech(value = this.speech) {
57
- if (typeof globalThis.toggleSpeech === "function") {
58
- globalThis.toggleSpeech(value);
59
- }
107
+ _applySpeech(value) {
108
+ localStorage.setItem("speech", value);
109
+ if (!value) speechStore.stopAudio();
110
},
111
62
- applyShowThoughts(value = this.showThoughts) {
63
- if (typeof globalThis.toggleThoughts === "function") {
64
- globalThis.toggleThoughts(value);
65
- }
112
+ _applyShowThoughts(value) {
113
+ css.toggleCssProperty(
114
+ ".msg-thoughts",
115
+ "display",
116
+ value ? undefined : "none"
117
+ );
118
},
119
68
- applyShowJson(value = this.showJson) {
69
- if (typeof globalThis.toggleJson === "function") {
70
- globalThis.toggleJson(value);
71
- }
120
+ _applyShowJson(value) {
121
+ css.toggleCssProperty(".msg-json", "display", value ? "block" : "none");
122
},
123
74
- applyShowUtils(value = this.showUtils) {
75
- if (typeof globalThis.toggleUtils === "function") {
76
- globalThis.toggleUtils(value);
77
- }
124
+ _applyShowUtils(value) {
125
+ css.toggleCssProperty(
126
+ ".message-util",
127
+ "display",
128
+ value ? undefined : "none"
129
+ );
130
},
131
};
132
133
export const store = createStore("preferences", model);
82
-
83
-
webui/components/sidebar/left-sidebar.html
+76
-46
@@ -1,30 +1,36 @@
1
<html>
2
+
3
<head>
4
<script type="module">
5
import { store as sidebarStore } from "/components/sidebar/sidebar-store.js";
6
</script>
7
<!-- Wrapper composes existing sidebar components -->
8
</head>
9
+
10
<body>
9
- <div id="left-panel" class="panel" x-data :class="{'hidden': !$store.sidebar.isOpen}">
10
- <!--Sidebar upper elements-->
11
- <div class="left-panel-top">
12
- <!-- Top Section: Header Icons + Quick Actions -->
13
- <x-component path="sidebar/top-section/sidebar-top.html"></x-component>
14
- <!-- Chats List -->
15
- <div class="config-section" id="chats-section">
16
- <x-component path="sidebar/chats/chats-list.html"></x-component>
17
- </div>
11
+ <div x-data>
12
+ <template x-if="$store.sidebar">
13
+ <div id="left-panel" class="panel" x-data :class="{'hidden': !$store.sidebar.isOpen}">
14
+ <!--Sidebar upper elements-->
15
+ <div class="left-panel-top">
16
+ <!-- Top Section: Header Icons + Quick Actions -->
17
+ <x-component path="sidebar/top-section/sidebar-top.html"></x-component>
18
+ <!-- Chats List -->
19
+ <div class="config-section" id="chats-section">
20
+ <x-component path="sidebar/chats/chats-list.html"></x-component>
21
+ </div>
22
19
- <!-- Tasks List -->
20
- <div class="config-section" id="tasks-section">
21
- <x-component path="sidebar/tasks/tasks-list.html"></x-component>
23
+ <!-- Tasks List -->
24
+ <div class="config-section" id="tasks-section">
25
+ <x-component path="sidebar/tasks/tasks-list.html"></x-component>
26
+ </div>
27
+ </div>
28
+ <!--Sidebar lower elements-->
29
+ <div class="left-panel-bottom">
30
+ <x-component path="sidebar/bottom/sidebar-bottom.html"></x-component>
31
+ </div>
32
</div>
23
- </div>
24
- <!--Sidebar lower elements-->
25
- <div class="left-panel-bottom">
26
- <x-component path="sidebar/bottom/sidebar-bottom.html"></x-component>
27
- </div>
33
+ </template>
34
</div>
35
36
<style>
@@ -42,21 +48,25 @@
48
color: var(--color-text);
49
box-shadow: 1px 0 5px rgba(0, 0, 0, 0.3);
50
user-select: none;
45
- -webkit-user-select: none; /* Safari compatibility */
46
- height: 100vh; /* Ensure full height */
47
- overflow: hidden; /* Prevent overall panel overflow */
51
+ -webkit-user-select: none;
52
+ /* Safari compatibility */
53
+ height: 100vh;
54
+ /* Ensure full height */
55
+ overflow: hidden;
56
+ /* Prevent overall panel overflow */
57
}
58
59
/* Ensure component host behaves like the original direct children for flex layout */
51
- #left-panel > .left-panel-top,
52
- #left-panel > .left-panel-bottom {
53
- display: flex;
54
- flex-direction: column;
60
+ #left-panel>.left-panel-top,
61
+ #left-panel>.left-panel-bottom {
62
+ display: flex;
63
+ flex-direction: column;
64
}
65
57
- #left-panel > .left-panel-top {
58
- flex: 1;
59
- min-height: 0; /* allow inner flex children to shrink properly */
66
+ #left-panel>.left-panel-top {
67
+ flex: 1;
68
+ min-height: 0;
69
+ /* allow inner flex children to shrink properly */
70
}
71
72
#left-panel.hidden {
@@ -64,11 +74,13 @@
74
}
75
76
.left-panel-top {
67
- flex: 1 1 auto; /* Take available space */
77
+ flex: 1 1 auto;
78
+ /* Take available space */
79
display: -webkit-flex;
80
display: flex;
81
flex-direction: column;
71
- min-height: 0; /* Critical for allowing flex children to shrink */
82
+ min-height: 0;
83
+ /* Critical for allowing flex children to shrink */
84
overflow: hidden;
85
justify-content: space-between;
86
margin-top: 3.5rem;
@@ -76,13 +88,16 @@
88
scrollbar-width: none;
89
-ms-overflow-style: none;
90
}
91
+
92
.left-panel-top::-webkit-scrollbar {
93
width: 0px;
94
}
95
+
96
.left-panel-bottom {
97
position: relative;
98
flex-shrink: 0;
85
- flex-grow: 0; /* Prevent bottom from growing */
99
+ flex-grow: 0;
100
+ /* Prevent bottom from growing */
101
}
102
103
/* Chats section container inside sidebar */
@@ -91,7 +106,8 @@
106
display: flex;
107
flex-direction: column;
108
min-height: 0;
94
- flex: 1 1 auto; /* Take all available space */
109
+ flex: 1 1 auto;
110
+ /* Take all available space */
111
max-height: 100%;
112
overflow: hidden;
113
}
@@ -102,19 +118,21 @@
118
display: flex;
119
flex-direction: column;
120
min-height: 0;
105
- flex: 0 0 auto; /* Shrink to content, no reserved space */
106
- max-height: 40%; /* Limit to 40% of viewport height when expanded */
121
+ flex: 0 0 auto;
122
+ /* Shrink to content, no reserved space */
123
+ max-height: 40%;
124
+ /* Limit to 40% of viewport height when expanded */
125
margin-top: 0;
126
padding-top: var(--spacing-md);
127
overflow: hidden;
128
}
111
-
129
+
130
/* Flatten wrapper elements to maintain flex chain for inner scroll containers */
113
- #chats-section > x-component,
114
- #chats-section > x-component > div[x-data],
115
- #tasks-section > x-component,
116
- #tasks-section > x-component > div[x-data],
117
- #tasks-section > x-component > div[x-data] > div[x-data] {
131
+ #chats-section>x-component,
132
+ #chats-section>x-component>div[x-data],
133
+ #tasks-section>x-component,
134
+ #tasks-section>x-component>div[x-data],
135
+ #tasks-section>x-component>div[x-data]>div[x-data] {
136
display: contents;
137
}
138
@@ -125,10 +143,12 @@
143
flex: 1;
144
min-height: 0;
145
}
146
+
147
/* During the collapsing transition, allow height animation to control size */
148
#tasks-section .collapsing {
149
flex: 0 0 auto !important;
150
}
151
+
152
/* Common section header styling (Chats, Tasks) - matching Preferences style */
153
.section-header {
154
display: flex;
@@ -137,23 +157,25 @@
157
gap: 6px;
158
margin: 0 0 0.5rem 0;
159
padding: 0;
140
- -webkit-user-select: none; /* Safari compatibility */
160
+ -webkit-user-select: none;
161
+ /* Safari compatibility */
162
user-select: none;
163
font-size: var(--font-size-normal);
164
}
144
-
165
+
166
/* Collapsible section headers (Tasks) */
167
.section-header-collapsible {
168
cursor: pointer;
169
}
149
-
170
+
171
@media (max-width: 768px) {
172
#left-panel {
173
position: fixed;
174
left: 0;
175
top: 0;
176
bottom: 0;
156
- width: 250px !important; /* Force width */
177
+ width: 250px !important;
178
+ /* Force width */
179
min-width: 250px;
180
z-index: 1003;
181
transition: all var(--transition-speed) ease-in-out;
@@ -165,9 +187,17 @@
187
}
188
189
@media (max-height: 600px) {
168
- #chats-section { min-height: 100%; }
169
- .left-panel-top { overflow-y: auto; -webkit-scroll-behavior: smooth; scroll-behavior: smooth; }
190
+ #chats-section {
191
+ min-height: 100%;
192
+ }
193
+
194
+ .left-panel-top {
195
+ overflow-y: auto;
196
+ -webkit-scroll-behavior: smooth;
197
+ scroll-behavior: smooth;
198
+ }
199
}
200
</style>
201
</body>
173
-</html>
202
+
203
+</html>
\ No newline at end of file
webui/index.html
+11
-5
@@ -99,7 +99,9 @@
99
<script type="module" src="index.js"></script>
100
101
<!-- Bootstrap JS (only for logic, importing bundled CSS => UI conflicts) -->
102
- <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
102
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"
103
+ integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"
104
+ crossorigin="anonymous"></script>
105
106
<!-- Then load Alpine.js -->
107
<!-- <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.3/dist/cdn.min.js"></script> -->
@@ -133,15 +135,19 @@
135
</script>
136
</head>
137
136
-<body class="dark-mode device-pointer">
138
+<body class="dark-mode device-pointer" x-data>
139
<div class="container">
140
<!-- Sidebar Overlay -->
139
- <div id="sidebar-overlay" class="sidebar-overlay" x-data :class="{'visible': $store.sidebar.isOpen && $store.sidebar.isMobile()}" @click="$store.sidebar.close()"></div>
141
+ <template x-if="$store.sidebar">
142
+ <div id="sidebar-overlay" class="sidebar-overlay" x-data
143
+ :class="{'visible': $store.sidebar.isOpen && $store.sidebar.isMobile()}"
144
+ @click="$store.sidebar.close()"></div>
145
+ </template>
146
<!-- Left Sidebar (Header Icons, Quick Actions, Tabs, Chats, Tasks) -->
147
<x-component path="sidebar/left-sidebar.html"></x-component>
142
-
148
+
149
<!-- Right Panel (Message History and Input Section) -->
144
- <div id="right-panel" class="panel" :class="{'expanded': !$store.sidebar.isOpen}">
150
+ <div id="right-panel" class="panel" >
151
<!-- Time and Date -->
152
<div id="time-date-container">
153
<div id="time-date"></div>
webui/index.js
+1
-59
@@ -21,9 +21,6 @@ globalThis.resetCounter = 0; // Used by stores and getChatBasedId
21
let skipOneSpeech = false;
22
let connectionStatus = undefined; // undefined = not checked yet, true = connected, false = disconnected
23
24
-export function getAutoScroll() {
25
- return autoScroll;
26
-}
24
25
// Sidebar toggle logic is now handled by sidebar-store.js
26
@@ -175,7 +172,7 @@ setInterval(updateUserTime, 1000);
172
function setMessage(id, type, heading, content, temp, kvps = null) {
173
const result = msgs.setMessage(id, type, heading, content, temp, kvps);
174
const chatHistoryEl = document.getElementById("chat-history");
178
- if (autoScroll && chatHistoryEl) {
175
+ if (preferencesStore.autoScroll && chatHistoryEl) {
176
chatHistoryEl.scrollTop = chatHistoryEl.scrollHeight;
177
}
178
return result;
@@ -480,61 +477,6 @@ export const getChatBasedId = function (id) {
477
return context + "-" + globalThis.resetCounter + "-" + id;
478
};
479
483
-globalThis.toggleAutoScroll = async function (_autoScroll) {
484
- autoScroll = _autoScroll;
485
-};
486
-
487
-globalThis.toggleJson = async function (showJson) {
488
- css.toggleCssProperty(".msg-json", "display", showJson ? "block" : "none");
489
-};
490
-
491
-globalThis.toggleThoughts = async function (showThoughts) {
492
- css.toggleCssProperty(
493
- ".msg-thoughts",
494
- "display",
495
- showThoughts ? undefined : "none"
496
- );
497
-};
498
-
499
-globalThis.toggleUtils = async function (showUtils) {
500
- css.toggleCssProperty(
501
- ".message-util",
502
- "display",
503
- showUtils ? undefined : "none"
504
- );
505
-};
506
-
507
-globalThis.toggleDarkMode = function (isDark) {
508
- if (isDark) {
509
- document.body.classList.remove("light-mode");
510
- document.body.classList.add("dark-mode");
511
- } else {
512
- document.body.classList.remove("dark-mode");
513
- document.body.classList.add("light-mode");
514
- }
515
- console.log("Dark mode:", isDark);
516
- localStorage.setItem("darkMode", isDark);
517
-};
518
-
519
-globalThis.toggleSpeech = function (isOn) {
520
- console.log("Speech:", isOn);
521
- localStorage.setItem("speech", isOn);
522
- if (!isOn) speechStore.stopAudio();
523
-};
524
-
525
-globalThis.nudge = async function () {
526
- await inputStore.nudge();
527
-};
528
-
529
-globalThis.restart = async function () {
530
- await chatsStore.restart();
531
-};
532
-
533
-// Modify this part
534
-document.addEventListener("DOMContentLoaded", () => {
535
- const isDarkMode = localStorage.getItem("darkMode") !== "false";
536
- toggleDarkMode(isDarkMode);
537
-});
480
481
globalThis.loadChats = async function () {
482
await chatsStore.loadChats();