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();