feat: Tabbed Settings - implement support and move existing settings to 3 tab categories

Rafael Uzarowski committed Mar 22, 2025 at 18:09 UTC 1eb6e735a45b4452998fbc453c285fceadfc92c5
5 files changed +205 -13
python/helpers/settings.py
+10 -1
@@ -1,4 +1,3 @@
1 -import asyncio
1 import json
2 import os
3 import re
@@ -90,6 +89,7 @@ class SettingsSection(TypedDict, total=False):
89 title: str
90 description: str
91 fields: list[SettingsField]
92 + tab: str # Indicates which tab this section belongs to
93
94
95 class SettingsOutput(TypedDict):
@@ -206,6 +206,7 @@ def convert_out(settings: Settings) -> SettingsOutput:
206 "title": "Chat Model",
207 "description": "Selection and settings for main chat model used by Agent Zero",
208 "fields": chat_model_fields,
209 + "tab": "agent",
210 }
211
212 # main model section
@@ -275,6 +276,7 @@ def convert_out(settings: Settings) -> SettingsOutput:
276 "title": "Utility model",
277 "description": "Smaller, cheaper, faster model for handling utility tasks like organizing memory, preparing prompts, summarizing.",
278 "fields": util_model_fields,
279 + "tab": "agent",
280 }
281
282 # embedding model section
@@ -334,6 +336,7 @@ def convert_out(settings: Settings) -> SettingsOutput:
336 "title": "Embedding Model",
337 "description": "Settings for the embedding model used by Agent Zero.",
338 "fields": embed_model_fields,
339 + "tab": "agent",
340 }
341
342 # embedding model section
@@ -383,6 +386,7 @@ def convert_out(settings: Settings) -> SettingsOutput:
386 "title": "Web Browser Model",
387 "description": "Settings for the web browser model. Agent Zero uses <a href='https://github.com/browser-use/browser-use' target='_blank'>browser-use</a> agentic framework to handle web interactions.",
388 "fields": browser_model_fields,
389 + "tab": "agent",
390 }
391
392 # # Memory settings section
@@ -447,6 +451,7 @@ def convert_out(settings: Settings) -> SettingsOutput:
451 "title": "Authentication",
452 "description": "Settings for authentication to use Agent Zero Web UI.",
453 "fields": auth_fields,
454 + "tab": "external",
455 }
456
457 # api keys model section
@@ -476,6 +481,7 @@ def convert_out(settings: Settings) -> SettingsOutput:
481 "title": "API Keys",
482 "description": "API keys for model providers and services used by Agent Zero.",
483 "fields": api_keys_fields,
484 + "tab": "external",
485 }
486
487 # Agent config section
@@ -528,6 +534,7 @@ def convert_out(settings: Settings) -> SettingsOutput:
534 "title": "Agent Config",
535 "description": "Agent parameters.",
536 "fields": agent_fields,
537 + "tab": "agent",
538 }
539
540 dev_fields: list[SettingsField] = []
@@ -593,6 +600,7 @@ def convert_out(settings: Settings) -> SettingsOutput:
600 "title": "Development",
601 "description": "Parameters for A0 framework development. RFCs (remote function calls) are used to call functions on another A0 instance. You can develop and debug A0 natively on your local system while redirecting some functions to A0 instance in docker. This is crucial for development as A0 needs to run in standardized environment to support all features.",
602 "fields": dev_fields,
603 + "tab": "developer",
604 }
605
606 # Speech to text section
@@ -664,6 +672,7 @@ def convert_out(settings: Settings) -> SettingsOutput:
672 "title": "Speech to Text",
673 "description": "Voice transcription preferences and server turn detection settings.",
674 "fields": stt_fields,
675 + "tab": "agent",
676 }
677
678 # Add the section to the result
python/helpers/task_scheduler.py
+5 -5
@@ -320,15 +320,15 @@ class TaskScheduler:
320
321 async def _run_task_wrapper(task: Union[ScheduledTask, AdHocTask]):
322 if task.state != "idle":
323 - self._printer.print(f"Scheduler Task {task.name} state is '{task.state}', skipping")
323 + self._printer.print(f"Scheduler Task '{task.name}' state is '{task.state}', skipping")
324 return
325
326 if task.state == "running":
327 - self._printer.print(f"Scheduler Task {task.name} already running, skipping")
327 + self._printer.print(f"Scheduler Task '{task.name}' already running, skipping")
328 return
329
330 try:
331 - self._printer.print(f"Scheduler Task {task.name} started")
331 + self._printer.print(f"Scheduler Task '{task.name}' started")
332
333 task.update(state="running")
334 await self._tasks.save()
@@ -374,7 +374,7 @@ class TaskScheduler:
374 await self._persist_chat(task, context)
375
376 except Exception as e:
377 - self._printer.print(f"Scheduler Task {task.name} failed: {e}")
377 + self._printer.print(f"Scheduler Task '{task.name}' failed: {e}")
378 task.update(last_result=f"ERROR: {str(e)}")
379 if agent:
380 agent.handle_critical_exception(e)
@@ -387,7 +387,7 @@ class TaskScheduler:
387 )
388 await self._tasks.save()
389 except Exception as e:
390 - self._printer.print(f"Scheduler Task {task.name} failed to save: {e}")
390 + self._printer.print(f"Scheduler Task '{task.name}' failed to save: {e}")
391
392 deferred_task = DeferredTask(thread_name=self.__class__.__name__)
393 deferred_task.start_task(_run_task_wrapper, task)
webui/css/settings.css
+141 -3
@@ -214,7 +214,7 @@ nav ul li a img {
214 }
215 }
216
217 -@media (max-width: 640px) {
217 +@media (max-width: 640px) {
218 nav ul {
219 grid-template-columns: repeat(2, 1.2fr);
220 }
@@ -224,17 +224,155 @@ nav ul li a img {
224 nav ul {
225 grid-template-columns: 1fr;
226 }
227 -
227 +
228 nav ul li a {
229 flex-direction: row;
230 justify-content: flex-start;
231 gap: 1rem;
232 padding: 0.75rem 1rem;
233 }
234 -
234 +
235 nav ul li a img {
236 margin-bottom: 0;
237 width: 30px;
238 height: 30px;
239 }
240 }
241 +
242 +/* Settings Tab Styles */
243 +.settings-tabs-container {
244 + width: 100%;
245 + margin-bottom: 8px;
246 + padding: 0;
247 + margin-top: 20px;
248 + position: relative;
249 + overflow: visible;
250 +}
251 +
252 +.settings-tabs {
253 + display: flex;
254 + width: 100%;
255 + position: relative;
256 + gap: 5px;
257 + border-bottom: 3px solid var(--color-border);
258 + justify-content: flex-start;
259 + padding-left: 20px;
260 + padding-top: 8px;
261 + overflow-x: auto;
262 + overflow-y: hidden;
263 + scrollbar-width: none;
264 + -ms-overflow-style: none;
265 + white-space: nowrap;
266 + -webkit-overflow-scrolling: touch;
267 +}
268 +
269 +.settings-tabs::-webkit-scrollbar {
270 + display: none;
271 +}
272 +
273 +.settings-tabs::before,
274 +.settings-tabs::after {
275 + content: '';
276 + position: absolute;
277 + top: 2px;
278 + bottom: 3px;
279 + width: 20px;
280 + pointer-events: none;
281 + z-index: 2;
282 + opacity: 0.7;
283 +}
284 +
285 +.settings-tabs::before {
286 + left: 0;
287 + background: linear-gradient(to right, var(--color-panel), transparent);
288 +}
289 +
290 +.settings-tabs::after {
291 + right: 0;
292 + background: linear-gradient(to left, var(--color-panel), transparent);
293 +}
294 +
295 +.settings-tab {
296 + padding: 8px 16px;
297 + cursor: pointer;
298 + position: relative;
299 + color: var(--color-text);
300 + border: 2px solid var(--color-border);
301 + border-bottom: none;
302 + border-radius: 8px 8px 0 0;
303 + transition: all 0.3s ease;
304 + background-color: var(--color-panel);
305 + margin-bottom: -3px;
306 + z-index: 1;
307 + min-width: min-content;
308 + width: auto;
309 + max-width: 100px;
310 + overflow: hidden;
311 + text-overflow: ellipsis;
312 + white-space: nowrap;
313 + flex-shrink: 0;
314 +}
315 +
316 +.settings-tab:not(.active) {
317 + opacity: 0.8;
318 + border-bottom: 3px solid var(--color-border);
319 + background-color: rgba(255, 255, 255, 0.03);
320 +}
321 +
322 +.settings-tab.active {
323 + color: var(--color-border);
324 + border-color: var(--color-border);
325 + box-shadow:
326 + 0 -4px 8px -2px var(--color-border) 0.5,
327 + 4px 0 8px -2px var(--color-border) 0.5,
328 + -4px 0 8px -2px var(--color-border) 0.5;
329 + font-weight: bold;
330 + background-color: var(--color-panel);
331 +}
332 +
333 +/* Light mode overrides */
334 +.light-mode .settings-tab.active {
335 + color: var(--color-border);
336 + box-shadow:
337 + 0 -4px 8px -2px var(--color-border) 0.5,
338 + 4px 0 8px -2px var(--color-border) 0.5,
339 + -4px 0 8px -2px var(--color-border) 0.5;
340 +}
341 +
342 +.light-mode .settings-tab:not(.active) {
343 + background-color: rgba(0, 0, 0, 0.03);
344 +}
345 +
346 +.light-mode .settings-tabs::before {
347 + background: linear-gradient(to right, var(--color-panel-light), transparent);
348 +}
349 +
350 +.light-mode .settings-tabs::after {
351 + background: linear-gradient(to left, var(--color-panel-light), transparent);
352 +}
353 +
354 +/* Responsive Design for Settings Tabs */
355 +@media (max-width: 640px) {
356 + .settings-tabs {
357 + gap: 2px;
358 + padding-left: 10px;
359 + }
360 +
361 + .settings-tab {
362 + padding: 6px 12px;
363 + font-size: 0.9rem;
364 + }
365 +}
366 +
367 +@media (max-width: 480px) {
368 + .settings-tabs {
369 + padding-left: 5px;
370 + }
371 +
372 + .settings-tab {
373 + flex: 0 0 auto;
374 + text-align: center;
375 + min-width: 60px;
376 + max-width: 80px;
377 + }
378 +}
webui/index.html
+20 -2
@@ -458,10 +458,28 @@
458 </div>
459
460 <div class="modal-content">
461 + <!-- Tab Navigation -->
462 + <div class="settings-tabs-container">
463 + <div class="settings-tabs">
464 + <div class="settings-tab"
465 + :class="{'active': activeTab === 'agent'}"
466 + @click="switchTab('agent')"
467 + title="Agent Settings">Agent Settings</div>
468 + <div class="settings-tab"
469 + :class="{'active': activeTab === 'external'}"
470 + @click="switchTab('external')"
471 + title="External Services">External Services</div>
472 + <div class="settings-tab"
473 + :class="{'active': activeTab === 'developer'}"
474 + @click="switchTab('developer')"
475 + title="Developer">Developer</div>
476 + </div>
477 + </div>
478 +
479 <div id="settings-sections">
480 <nav>
481 <ul>
464 - <template x-for="(section, index) in settings.sections" :key="section.title">
482 + <template x-for="(section, index) in filteredSections" :key="section.title">
483 <li>
484 <a :href="'#section' + (index + 1)">
485 <img :src="'/public/' + section.id +'.svg'"
@@ -473,7 +491,7 @@
491 </ul>
492 </nav>
493 </div>
476 - <template x-for="(section, sectionIndex) in settings.sections" :key="sectionIndex">
494 + <template x-for="(section, sectionIndex) in filteredSections" :key="sectionIndex">
495 <div :id="'section' + (sectionIndex + 1)" class="section">
496 <div class="section-title" x-text="section.title"></div>
497 <div class="section-description" x-html="section.description"></div>
webui/js/settings.js
+29 -2
@@ -2,7 +2,34 @@ const settingsModalProxy = {
2 isOpen: false,
3 settings: {},
4 resolvePromise: null,
5 + activeTab: 'agent', // Default tab
6
7 + // Computed property for filtered sections
8 + get filteredSections() {
9 + if (!this.settings || !this.settings.sections) return [];
10 + const filteredSections = this.settings.sections.filter(section => section.tab === this.activeTab);
11 +
12 + // If no sections match the current tab (or all tabs are missing), show all sections
13 + if (filteredSections.length === 0) {
14 + return this.settings.sections;
15 + }
16 +
17 + return filteredSections;
18 + },
19 +
20 + // Switch tab method
21 + switchTab(tabName) {
22 + this.activeTab = tabName;
23 + localStorage.setItem('settingsActiveTab', tabName);
24 +
25 + // Auto-scroll active tab into view (added after a small delay to ensure DOM updates)
26 + setTimeout(() => {
27 + const activeTab = document.querySelector('.settings-tab.active');
28 + if (activeTab) {
29 + activeTab.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
30 + }
31 + }, 10);
32 + },
33
34 async openModal() {
35
@@ -13,6 +40,8 @@ const settingsModalProxy = {
40 try {
41 const set = await sendJsonData("/settings_get", null);
42
43 + // Restore active tab from localStorage or use default
44 + this.activeTab = localStorage.getItem('settingsActiveTab') || 'agent';
45
46 const settings = {
47 "title": "Settings",
@@ -35,8 +64,6 @@ const settingsModalProxy = {
64 modalAD.isOpen = true; // Update directly
65 modalAD.settings = settings; // Update directly
66
38 -
39 -
67 return new Promise(resolve => {
68 this.resolvePromise = resolve;
69 });