main
js 249 lines 7.14 KB
Raw
1 import { createStore } from "/js/AlpineStore.js";
2 import { ttsService } from "/js/tts-service.js";
3 import { applyModeSteps } from "/components/messages/process-group/process-group-dom.js";
4
5 const UI_VISIBILITY_DEFAULTS = {
6 projectSelector: { mobile: true, desktop: true },
7 time: { mobile: false, desktop: true },
8 connectionStatus: { mobile: true, desktop: true },
9 rightCanvasRail: { mobile: true, desktop: true },
10 };
11
12 function normalizeUiVisibility(value = {}) {
13 return Object.fromEntries(
14 Object.entries(UI_VISIBILITY_DEFAULTS).map(([control, defaults]) => [
15 control,
16 {
17 mobile: typeof value?.[control]?.mobile === "boolean" ? value[control].mobile : defaults.mobile,
18 desktop: typeof value?.[control]?.desktop === "boolean" ? value[control].desktop : defaults.desktop,
19 },
20 ])
21 );
22 }
23
24 // Preferences store centralizes user preference toggles and side-effects
25 const model = {
26 _initialized: false,
27
28 // UI toggles (initialized with safe defaults, loaded from localStorage in init)
29 get autoScroll() {
30 return this._autoScroll;
31 },
32 set autoScroll(value) {
33 this._autoScroll = value;
34 this._applyAutoScroll(value);
35 },
36 _autoScroll: true,
37
38 get darkMode() {
39 return this._darkMode;
40 },
41 set darkMode(value) {
42 this._darkMode = value;
43 this._applyDarkMode(value);
44 },
45 _darkMode: true,
46
47 get speech() {
48 return this._speech;
49 },
50 set speech(value) {
51 this._speech = value;
52 this._applySpeech(value);
53 },
54 _speech: false,
55
56 get showUtils() {
57 return this._showUtils;
58 },
59 set showUtils(value) {
60 this._showUtils = value;
61 this._applyShowUtils(value);
62 },
63 _showUtils: false,
64
65 // Chat container width preference for HiDPI/large screens
66 get chatWidth() {
67 return this._chatWidth;
68 },
69 set chatWidth(value) {
70 this._chatWidth = value;
71 this._applyChatWidth(value);
72 },
73 _chatWidth: "55", // Default width in em (standard)
74
75 // Width presets: { label, value in em }
76 chatWidthOptions: [
77 { label: "MIN", value: "40" },
78 { label: "WIDE", value: "55" },
79 { label: "2X", value: "80" },
80 { label: "FULL", value: "full" },
81 ],
82
83 // Detail mode for process groups/steps expansion
84 get detailMode() {
85 return this._detailMode;
86 },
87 set detailMode(value) {
88 this._detailMode = value;
89 this._applyDetailMode(value);
90 },
91 _detailMode: "current", // Default: show current step only
92
93 _uiVisibility: normalizeUiVisibility(globalThis.runtimeInfo?.uiControlVisibility),
94 _isMobileViewport: false,
95
96 registerUiControlVisibility(control, defaults = {}) {
97 const id = String(control || "").trim();
98 if (!id) return;
99 UI_VISIBILITY_DEFAULTS[id] = {
100 mobile: defaults.mobile !== false,
101 desktop: defaults.desktop !== false,
102 };
103 this._uiVisibility = normalizeUiVisibility({
104 ...(globalThis.runtimeInfo?.uiControlVisibility || {}),
105 ...(this._uiVisibility || {}),
106 });
107 },
108
109 uiVisibilitySnapshot() {
110 return normalizeUiVisibility(this._uiVisibility);
111 },
112
113 setUiVisibility(value) {
114 this._uiVisibility = normalizeUiVisibility(value);
115 },
116
117 isUiControlVisible(control) {
118 const device = this._isMobileViewport ? "mobile" : "desktop";
119 return this._uiVisibility?.[control]?.[device] !== false;
120 },
121
122 // Detail mode options for UI sidebar
123 detailModeOptions: [
124 { label: "NO", value: "collapsed", title: "All collapsed" },
125 { label: "LIST", value: "list", title: "Steps collapsed" },
126 { label: "STEP", value: "current", title: "Current step only" },
127 { label: "ALL", value: "expanded", title: "All expanded" },
128 ],
129
130 // Initialize preferences and apply current state
131 init() {
132 if (this._initialized) return;
133 this._initialized = true;
134
135 try {
136 // Load persisted preferences with safe fallbacks
137 try {
138 const storedDarkMode = localStorage.getItem("darkMode");
139 this._darkMode = storedDarkMode !== "false";
140 } catch {
141 this._darkMode = true; // Default to dark mode if localStorage is unavailable
142 }
143
144 try {
145 const storedSpeech = localStorage.getItem("speech");
146 this._speech = storedSpeech === "true";
147 } catch {
148 this._speech = false; // Default to speech off if localStorage is unavailable
149 }
150
151 // Load chat width preference
152 try {
153 const storedChatWidth = localStorage.getItem("chatWidth");
154 if (storedChatWidth && this.chatWidthOptions.some(opt => opt.value === storedChatWidth)) {
155 this._chatWidth = storedChatWidth;
156 }
157 } catch {
158 this._chatWidth = "55"; // Default to standard
159 }
160
161 // Load detail mode preference
162 try {
163 const storedDetailMode = localStorage.getItem("detailMode");
164 if (storedDetailMode && this.detailModeOptions.some(opt => opt.value === storedDetailMode)) {
165 this._detailMode = storedDetailMode;
166 }
167 } catch {
168 this._detailMode = "current"; // Default
169 }
170
171 // load utility messages preference
172 try{
173 const storedShowUtils = localStorage.getItem("showUtils");
174 this._showUtils = storedShowUtils === "true";
175 } catch {
176 this._showUtils = false; // Default to speech off if localStorage is unavailable
177 }
178
179 this._isMobileViewport = globalThis.innerWidth <= 768;
180 globalThis.addEventListener("resize", () => {
181 this._isMobileViewport = globalThis.innerWidth <= 768;
182 });
183
184 // Apply all preferences
185 this._applyDarkMode(this._darkMode);
186 this._applyAutoScroll(this._autoScroll);
187 this._applySpeech(this._speech);
188 this._applyShowUtils(this._showUtils);
189 this._applyChatWidth(this._chatWidth);
190 this._applyDetailMode(this._detailMode);
191 } catch (e) {
192 console.error("Failed to initialize preferences store", e);
193 }
194 },
195
196 _applyAutoScroll(value) {
197 // nothing for now
198 },
199
200 _applyDarkMode(value) {
201 if (value) {
202 document.body.classList.remove("light-mode");
203 document.body.classList.add("dark-mode");
204 } else {
205 document.body.classList.remove("dark-mode");
206 document.body.classList.add("light-mode");
207 }
208 localStorage.setItem("darkMode", value);
209 },
210
211 _applySpeech(value) {
212 localStorage.setItem("speech", value);
213 if (!value) ttsService.stop();
214 },
215
216
217 _applyShowUtils(value) {
218 localStorage.setItem("showUtils", value);
219 document.documentElement.classList.toggle(
220 "show-utility-messages",
221 Boolean(value),
222 );
223 },
224
225 _applyChatWidth(value) {
226 localStorage.setItem("chatWidth", value);
227 // Set CSS custom property for chat max-width
228 const root = document.documentElement;
229 if (value === "full") {
230 root.style.setProperty("--chat-max-width", "100%");
231 } else {
232 root.style.setProperty("--chat-max-width", `${value}em`);
233 }
234 },
235
236 applyCurrentDetailMode(chatHistory = undefined) {
237 return applyModeSteps(this._detailMode, this._showUtils, chatHistory);
238 },
239
240 _applyDetailMode(value) {
241 localStorage.setItem("detailMode", value);
242 // Apply mode to all existing DOM elements
243 void this.applyCurrentDetailMode().catch((error) => {
244 console.error("Failed to apply process detail mode", error);
245 });
246 },
247 };
248
249 export const store = createStore("preferences", model);