Stijnus commited on
Commit
999d87b
·
1 Parent(s): af620d0

beta New control panel

Browse files

# Tab Management System Implementation

## What's Been Implemented
1. Complete Tab Management System with:
- Drag and drop functionality for reordering tabs
- Visual feedback during drag operations
- Smooth animations and transitions
- Dark mode support
- Search functionality for tabs
- Reset to defaults option

2. Developer Mode Features:
- Shows ALL available tabs in developer mode
- Maintains tab order across modes
- Proper visibility toggles
- Automatic inclusion of developer-specific tabs

3. User Mode Features:
- Shows only user-configured tabs
- Maintains separate tab configurations
- Proper visibility management

## Key Components
- `TabManagement.tsx`: Main management interface
- `ControlPanel.tsx`: Main panel with tab display
- Integration with tab configuration store
- Proper type definitions and interfaces

## Technical Features
- React DnD for drag and drop
- Framer Motion for animations
- TypeScript for type safety
- UnoCSS for styling
- Toast notifications for user feedback

## Next Steps
1. Testing:
- Test tab visibility in both modes
- Verify drag and drop persistence
- Check dark mode compatibility
- Verify search functionality
- Test reset functionality

2. Potential Improvements:
- Add tab grouping functionality
- Implement tab pinning
- Add keyboard shortcuts
- Improve accessibility
- Add tab descriptions
- Add tab icons customization

3. Documentation:
- Add inline code comments
- Create user documentation
- Document API interfaces
- Add setup instructions

4. Future Features:
- Tab export/import
- Custom tab creation
- Tab templates
- User preferences sync
- Tab statistics

## Known Issues to Address
1. Ensure all tabs are visible in developer mode
2. Improve drag and drop performance
3. Better state persistence
4. Enhanced error handling
5. Improved type safety

## Usage Instructions
1. Switch to developer mode to see all available tabs
2. Use drag and drop to reorder tabs
3. Toggle visibility using switches
4. Use search to filter tabs
5. Reset to defaults if needed

## Technical Debt
1. Refactor tab configuration store
2. Improve type definitions
3. Add proper error boundaries
4. Implement proper loading states
5. Add comprehensive testing

## Security Considerations
1. Validate tab configurations
2. Sanitize user input
3. Implement proper access control
4. Add audit logging
5. Secure state management

app/components/settings/ControlPanel.tsx ADDED
@@ -0,0 +1,607 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect, useMemo } from 'react';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
+ import { useStore } from '@nanostores/react';
4
+ import { Switch } from '@radix-ui/react-switch';
5
+ import * as RadixDialog from '@radix-ui/react-dialog';
6
+ import { DndProvider } from 'react-dnd';
7
+ import { HTML5Backend } from 'react-dnd-html5-backend';
8
+ import { classNames } from '~/utils/classNames';
9
+ import { TabManagement } from './developer/TabManagement';
10
+ import { TabTile } from './shared/TabTile';
11
+ import { useUpdateCheck } from '~/lib/hooks/useUpdateCheck';
12
+ import { useFeatures } from '~/lib/hooks/useFeatures';
13
+ import { useNotifications } from '~/lib/hooks/useNotifications';
14
+ import { useConnectionStatus } from '~/lib/hooks/useConnectionStatus';
15
+ import { useDebugStatus } from '~/lib/hooks/useDebugStatus';
16
+ import { tabConfigurationStore, developerModeStore, setDeveloperMode } from '~/lib/stores/settings';
17
+ import type { TabType, TabVisibilityConfig } from './settings.types';
18
+ import { TAB_LABELS, DEFAULT_TAB_CONFIG } from './settings.types';
19
+ import { resetTabConfiguration } from '~/lib/stores/settings';
20
+ import { DialogTitle } from '~/components/ui/Dialog';
21
+ import { useDrag, useDrop } from 'react-dnd';
22
+
23
+ // Import all tab components
24
+ import ProfileTab from './profile/ProfileTab';
25
+ import SettingsTab from './settings/SettingsTab';
26
+ import NotificationsTab from './notifications/NotificationsTab';
27
+ import FeaturesTab from './features/FeaturesTab';
28
+ import DataTab from './data/DataTab';
29
+ import DebugTab from './debug/DebugTab';
30
+ import { EventLogsTab } from './event-logs/EventLogsTab';
31
+ import UpdateTab from './update/UpdateTab';
32
+ import ConnectionsTab from './connections/ConnectionsTab';
33
+ import CloudProvidersTab from './providers/CloudProvidersTab';
34
+ import ServiceStatusTab from './providers/ServiceStatusTab';
35
+ import LocalProvidersTab from './providers/LocalProvidersTab';
36
+ import TaskManagerTab from './task-manager/TaskManagerTab';
37
+
38
+ interface ControlPanelProps {
39
+ open: boolean;
40
+ onClose: () => void;
41
+ }
42
+
43
+ interface TabWithDevType extends TabVisibilityConfig {
44
+ isExtraDevTab?: boolean;
45
+ }
46
+
47
+ const TAB_DESCRIPTIONS: Record<TabType, string> = {
48
+ profile: 'Manage your profile and account settings',
49
+ settings: 'Configure application preferences',
50
+ notifications: 'View and manage your notifications',
51
+ features: 'Explore new and upcoming features',
52
+ data: 'Manage your data and storage',
53
+ 'cloud-providers': 'Configure cloud AI providers and models',
54
+ 'local-providers': 'Configure local AI providers and models',
55
+ 'service-status': 'Monitor cloud LLM service status',
56
+ connection: 'Check connection status and settings',
57
+ debug: 'Debug tools and system information',
58
+ 'event-logs': 'View system events and logs',
59
+ update: 'Check for updates and release notes',
60
+ 'task-manager': 'Monitor system resources and processes',
61
+ };
62
+
63
+ // Add DraggableTabTile component before the ControlPanel component
64
+ const DraggableTabTile = ({
65
+ tab,
66
+ index,
67
+ moveTab,
68
+ ...props
69
+ }: {
70
+ tab: TabWithDevType;
71
+ index: number;
72
+ moveTab: (dragIndex: number, hoverIndex: number) => void;
73
+ onClick: () => void;
74
+ isActive: boolean;
75
+ hasUpdate: boolean;
76
+ statusMessage: string;
77
+ description: string;
78
+ isLoading?: boolean;
79
+ }) => {
80
+ const [{ isDragging }, drag] = useDrag({
81
+ type: 'tab',
82
+ item: { index, id: tab.id },
83
+ collect: (monitor) => ({
84
+ isDragging: monitor.isDragging(),
85
+ }),
86
+ });
87
+
88
+ const [{ isOver, canDrop }, drop] = useDrop({
89
+ accept: 'tab',
90
+ hover: (item: { index: number; id: string }, monitor) => {
91
+ if (!monitor.isOver({ shallow: true })) {
92
+ return;
93
+ }
94
+
95
+ if (item.id === tab.id) {
96
+ return;
97
+ }
98
+
99
+ if (item.index === index) {
100
+ return;
101
+ }
102
+
103
+ // Only move when hovering over the middle section
104
+ const hoverBoundingRect = monitor.getSourceClientOffset();
105
+ const clientOffset = monitor.getClientOffset();
106
+
107
+ if (!hoverBoundingRect || !clientOffset) {
108
+ return;
109
+ }
110
+
111
+ const hoverMiddleX = hoverBoundingRect.x + 150; // Half of typical card width
112
+ const hoverClientX = clientOffset.x;
113
+
114
+ // Only perform the move when the mouse has crossed half of the items width
115
+ if (item.index < index && hoverClientX < hoverMiddleX) {
116
+ return;
117
+ }
118
+
119
+ if (item.index > index && hoverClientX > hoverMiddleX) {
120
+ return;
121
+ }
122
+
123
+ moveTab(item.index, index);
124
+ item.index = index;
125
+ },
126
+ collect: (monitor) => ({
127
+ isOver: monitor.isOver({ shallow: true }),
128
+ canDrop: monitor.canDrop(),
129
+ }),
130
+ });
131
+
132
+ const dropIndicatorClasses = classNames('rounded-xl border-2 border-transparent transition-all duration-200', {
133
+ 'ring-2 ring-purple-500 ring-opacity-50 bg-purple-50 dark:bg-purple-900/20': isOver,
134
+ 'hover:ring-2 hover:ring-purple-500/30': canDrop && !isOver,
135
+ });
136
+
137
+ return (
138
+ <motion.div
139
+ ref={(node) => drag(drop(node))}
140
+ style={{
141
+ opacity: isDragging ? 0.5 : 1,
142
+ cursor: 'move',
143
+ position: 'relative',
144
+ zIndex: isDragging ? 100 : isOver ? 50 : 1,
145
+ }}
146
+ animate={{
147
+ scale: isDragging ? 1.02 : isOver ? 1.05 : 1,
148
+ boxShadow: isDragging
149
+ ? '0 8px 24px rgba(0, 0, 0, 0.15)'
150
+ : isOver
151
+ ? '0 4px 12px rgba(147, 51, 234, 0.3)'
152
+ : '0 0 0 rgba(0, 0, 0, 0)',
153
+ borderColor: isOver ? 'rgb(147, 51, 234)' : isDragging ? 'rgba(147, 51, 234, 0.5)' : 'transparent',
154
+ y: isOver ? -2 : 0,
155
+ }}
156
+ transition={{
157
+ type: 'spring',
158
+ stiffness: 500,
159
+ damping: 30,
160
+ mass: 0.8,
161
+ }}
162
+ className={dropIndicatorClasses}
163
+ >
164
+ <TabTile {...props} tab={tab} />
165
+ {isOver && (
166
+ <motion.div
167
+ className="absolute inset-0 rounded-xl pointer-events-none"
168
+ initial={{ opacity: 0 }}
169
+ animate={{ opacity: 1 }}
170
+ exit={{ opacity: 0 }}
171
+ transition={{ duration: 0.2 }}
172
+ >
173
+ <div className="absolute inset-0 bg-gradient-to-r from-purple-500/10 to-purple-500/20 rounded-xl" />
174
+ <div className="absolute inset-0 border-2 border-purple-500/50 rounded-xl" />
175
+ </motion.div>
176
+ )}
177
+ </motion.div>
178
+ );
179
+ };
180
+
181
+ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
182
+ // State
183
+ const [activeTab, setActiveTab] = useState<TabType | null>(null);
184
+ const [loadingTab, setLoadingTab] = useState<TabType | null>(null);
185
+ const [showTabManagement, setShowTabManagement] = useState(false);
186
+ const [profile, setProfile] = useState({ avatar: null, notifications: true });
187
+
188
+ // Store values
189
+ const tabConfiguration = useStore(tabConfigurationStore);
190
+ const developerMode = useStore(developerModeStore);
191
+
192
+ // Status hooks
193
+ const { hasUpdate, currentVersion, acknowledgeUpdate } = useUpdateCheck();
194
+ const { hasNewFeatures, unviewedFeatures, acknowledgeAllFeatures } = useFeatures();
195
+ const { hasUnreadNotifications, unreadNotifications, markAllAsRead } = useNotifications();
196
+ const { hasConnectionIssues, currentIssue, acknowledgeIssue } = useConnectionStatus();
197
+ const { hasActiveWarnings, activeIssues, acknowledgeAllIssues } = useDebugStatus();
198
+
199
+ // Initialize profile from localStorage on mount
200
+ useEffect(() => {
201
+ if (typeof window === 'undefined') {
202
+ return;
203
+ }
204
+
205
+ const saved = localStorage.getItem('bolt_user_profile');
206
+
207
+ if (saved) {
208
+ try {
209
+ const parsedProfile = JSON.parse(saved);
210
+ setProfile(parsedProfile);
211
+ } catch (error) {
212
+ console.warn('Failed to parse profile from localStorage:', error);
213
+ }
214
+ }
215
+ }, []);
216
+
217
+ // Add visibleTabs logic using useMemo
218
+ const visibleTabs = useMemo(() => {
219
+ if (!tabConfiguration?.userTabs || !Array.isArray(tabConfiguration.userTabs)) {
220
+ console.warn('Invalid tab configuration, resetting to defaults');
221
+ resetTabConfiguration();
222
+
223
+ return [];
224
+ }
225
+
226
+ // In developer mode, show ALL tabs without restrictions
227
+ if (developerMode) {
228
+ // Combine all unique tabs from both user and developer configurations
229
+ const allTabs = new Set([
230
+ ...DEFAULT_TAB_CONFIG.map((tab) => tab.id),
231
+ ...tabConfiguration.userTabs.map((tab) => tab.id),
232
+ ...(tabConfiguration.developerTabs || []).map((tab) => tab.id),
233
+ ]);
234
+
235
+ // Create a complete tab list with all tabs visible
236
+ const devTabs = Array.from(allTabs).map((tabId) => {
237
+ // Try to find existing configuration for this tab
238
+ const existingTab =
239
+ tabConfiguration.developerTabs?.find((t) => t.id === tabId) ||
240
+ tabConfiguration.userTabs?.find((t) => t.id === tabId) ||
241
+ DEFAULT_TAB_CONFIG.find((t) => t.id === tabId);
242
+
243
+ return {
244
+ id: tabId,
245
+ visible: true,
246
+ window: 'developer' as const,
247
+ order: existingTab?.order || DEFAULT_TAB_CONFIG.findIndex((t) => t.id === tabId),
248
+ };
249
+ });
250
+
251
+ return devTabs.sort((a, b) => a.order - b.order);
252
+ }
253
+
254
+ // In user mode, only show visible user tabs
255
+ return tabConfiguration.userTabs
256
+ .filter((tab) => {
257
+ if (!tab || typeof tab.id !== 'string') {
258
+ console.warn('Invalid tab entry:', tab);
259
+ return false;
260
+ }
261
+
262
+ // Hide notifications tab if notifications are disabled
263
+ if (tab.id === 'notifications' && !profile.notifications) {
264
+ return false;
265
+ }
266
+
267
+ // Only show tabs that are explicitly visible and assigned to the user window
268
+ return tab.visible && tab.window === 'user';
269
+ })
270
+ .sort((a, b) => a.order - b.order);
271
+ }, [tabConfiguration, profile.notifications, developerMode]);
272
+
273
+ // Add moveTab handler
274
+ const moveTab = (dragIndex: number, hoverIndex: number) => {
275
+ const newTabs = [...visibleTabs];
276
+ const dragTab = newTabs[dragIndex];
277
+ newTabs.splice(dragIndex, 1);
278
+ newTabs.splice(hoverIndex, 0, dragTab);
279
+
280
+ // Update the order of the tabs
281
+ const updatedTabs = newTabs.map((tab, index) => ({
282
+ ...tab,
283
+ order: index,
284
+ window: 'developer' as const,
285
+ visible: true,
286
+ }));
287
+
288
+ // Update the tab configuration store directly
289
+ if (developerMode) {
290
+ // In developer mode, update developerTabs while preserving configuration
291
+ tabConfigurationStore.set({
292
+ ...tabConfiguration,
293
+ developerTabs: updatedTabs,
294
+ });
295
+ } else {
296
+ // In user mode, update userTabs
297
+ tabConfigurationStore.set({
298
+ ...tabConfiguration,
299
+ userTabs: updatedTabs.map((tab) => ({ ...tab, window: 'user' as const })),
300
+ });
301
+ }
302
+ };
303
+
304
+ // Handlers
305
+ const handleBack = () => {
306
+ if (showTabManagement) {
307
+ setShowTabManagement(false);
308
+ } else if (activeTab) {
309
+ setActiveTab(null);
310
+ }
311
+ };
312
+
313
+ const handleDeveloperModeChange = (checked: boolean) => {
314
+ console.log('Developer mode changed:', checked);
315
+ setDeveloperMode(checked);
316
+ };
317
+
318
+ // Add effect to log developer mode changes
319
+ useEffect(() => {
320
+ console.log('Current developer mode:', developerMode);
321
+ }, [developerMode]);
322
+
323
+ const getTabComponent = () => {
324
+ switch (activeTab) {
325
+ case 'profile':
326
+ return <ProfileTab />;
327
+ case 'settings':
328
+ return <SettingsTab />;
329
+ case 'notifications':
330
+ return <NotificationsTab />;
331
+ case 'features':
332
+ return <FeaturesTab />;
333
+ case 'data':
334
+ return <DataTab />;
335
+ case 'cloud-providers':
336
+ return <CloudProvidersTab />;
337
+ case 'local-providers':
338
+ return <LocalProvidersTab />;
339
+ case 'connection':
340
+ return <ConnectionsTab />;
341
+ case 'debug':
342
+ return <DebugTab />;
343
+ case 'event-logs':
344
+ return <EventLogsTab />;
345
+ case 'update':
346
+ return <UpdateTab />;
347
+ case 'task-manager':
348
+ return <TaskManagerTab />;
349
+ case 'service-status':
350
+ return <ServiceStatusTab />;
351
+ default:
352
+ return null;
353
+ }
354
+ };
355
+
356
+ const getTabUpdateStatus = (tabId: TabType): boolean => {
357
+ switch (tabId) {
358
+ case 'update':
359
+ return hasUpdate;
360
+ case 'features':
361
+ return hasNewFeatures;
362
+ case 'notifications':
363
+ return hasUnreadNotifications;
364
+ case 'connection':
365
+ return hasConnectionIssues;
366
+ case 'debug':
367
+ return hasActiveWarnings;
368
+ default:
369
+ return false;
370
+ }
371
+ };
372
+
373
+ const getStatusMessage = (tabId: TabType): string => {
374
+ switch (tabId) {
375
+ case 'update':
376
+ return `New update available (v${currentVersion})`;
377
+ case 'features':
378
+ return `${unviewedFeatures.length} new feature${unviewedFeatures.length === 1 ? '' : 's'} to explore`;
379
+ case 'notifications':
380
+ return `${unreadNotifications.length} unread notification${unreadNotifications.length === 1 ? '' : 's'}`;
381
+ case 'connection':
382
+ return currentIssue === 'disconnected'
383
+ ? 'Connection lost'
384
+ : currentIssue === 'high-latency'
385
+ ? 'High latency detected'
386
+ : 'Connection issues detected';
387
+ case 'debug': {
388
+ const warnings = activeIssues.filter((i) => i.type === 'warning').length;
389
+ const errors = activeIssues.filter((i) => i.type === 'error').length;
390
+
391
+ return `${warnings} warning${warnings === 1 ? '' : 's'}, ${errors} error${errors === 1 ? '' : 's'}`;
392
+ }
393
+ default:
394
+ return '';
395
+ }
396
+ };
397
+
398
+ const handleTabClick = (tabId: TabType) => {
399
+ setLoadingTab(tabId);
400
+ setActiveTab(tabId);
401
+
402
+ // Acknowledge notifications based on tab
403
+ switch (tabId) {
404
+ case 'update':
405
+ acknowledgeUpdate();
406
+ break;
407
+ case 'features':
408
+ acknowledgeAllFeatures();
409
+ break;
410
+ case 'notifications':
411
+ markAllAsRead();
412
+ break;
413
+ case 'connection':
414
+ acknowledgeIssue();
415
+ break;
416
+ case 'debug':
417
+ acknowledgeAllIssues();
418
+ break;
419
+ }
420
+
421
+ // Clear loading state after a delay
422
+ setTimeout(() => setLoadingTab(null), 500);
423
+ };
424
+
425
+ return (
426
+ <DndProvider backend={HTML5Backend}>
427
+ <RadixDialog.Root open={open}>
428
+ <RadixDialog.Portal>
429
+ <div className="fixed inset-0 flex items-center justify-center z-[100]">
430
+ <RadixDialog.Overlay asChild>
431
+ <motion.div
432
+ className="absolute inset-0 bg-black/50 backdrop-blur-sm"
433
+ initial={{ opacity: 0 }}
434
+ animate={{ opacity: 1 }}
435
+ exit={{ opacity: 0 }}
436
+ transition={{ duration: 0.2 }}
437
+ />
438
+ </RadixDialog.Overlay>
439
+
440
+ <RadixDialog.Content
441
+ aria-describedby={undefined}
442
+ onEscapeKeyDown={onClose}
443
+ onPointerDownOutside={onClose}
444
+ className="relative z-[101]"
445
+ >
446
+ <motion.div
447
+ className={classNames(
448
+ 'w-[1200px] h-[90vh]',
449
+ 'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
450
+ 'rounded-2xl shadow-2xl',
451
+ 'border border-[#E5E5E5] dark:border-[#1A1A1A]',
452
+ 'flex flex-col overflow-hidden',
453
+ )}
454
+ initial={{ opacity: 0, scale: 0.95, y: 20 }}
455
+ animate={{ opacity: 1, scale: 1, y: 0 }}
456
+ exit={{ opacity: 0, scale: 0.95, y: 20 }}
457
+ transition={{ duration: 0.2 }}
458
+ >
459
+ {/* Header */}
460
+ <div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700">
461
+ <div className="flex items-center space-x-4">
462
+ {activeTab || showTabManagement ? (
463
+ <button
464
+ onClick={handleBack}
465
+ className="flex items-center justify-center w-8 h-8 rounded-lg bg-gray-100 dark:bg-gray-800 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 group transition-all duration-200"
466
+ >
467
+ <div className="i-ph:arrow-left w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
468
+ </button>
469
+ ) : (
470
+ <motion.div
471
+ className="i-ph:lightning-fill w-5 h-5 text-purple-500"
472
+ initial={{ rotate: -10 }}
473
+ animate={{ rotate: 10 }}
474
+ transition={{
475
+ repeat: Infinity,
476
+ repeatType: 'reverse',
477
+ duration: 2,
478
+ ease: 'easeInOut',
479
+ }}
480
+ />
481
+ )}
482
+ <DialogTitle className="text-xl font-semibold text-gray-900 dark:text-white">
483
+ {showTabManagement ? 'Tab Management' : activeTab ? TAB_LABELS[activeTab] : 'Control Panel'}
484
+ </DialogTitle>
485
+ </div>
486
+
487
+ <div className="flex items-center space-x-4">
488
+ {/* Only show Manage Tabs button in developer mode */}
489
+ {!activeTab && !showTabManagement && developerMode && (
490
+ <motion.button
491
+ onClick={() => setShowTabManagement(true)}
492
+ className="flex items-center space-x-2 px-3 py-1.5 rounded-lg bg-gray-100 dark:bg-gray-800 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 group transition-all duration-200"
493
+ whileHover={{ scale: 1.05 }}
494
+ whileTap={{ scale: 0.95 }}
495
+ >
496
+ <div className="i-ph:sliders-horizontal w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
497
+ <span className="text-sm text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors">
498
+ Manage Tabs
499
+ </span>
500
+ </motion.button>
501
+ )}
502
+
503
+ <div className="flex items-center gap-2">
504
+ <Switch
505
+ id="developer-mode"
506
+ checked={developerMode}
507
+ onCheckedChange={handleDeveloperModeChange}
508
+ className={classNames(
509
+ 'relative inline-flex h-6 w-11 items-center rounded-full',
510
+ 'bg-gray-200 dark:bg-gray-700',
511
+ 'data-[state=checked]:bg-purple-500',
512
+ 'transition-colors duration-200',
513
+ )}
514
+ >
515
+ <span className="sr-only">Toggle developer mode</span>
516
+ <span
517
+ className={classNames(
518
+ 'inline-block h-4 w-4 transform rounded-full bg-white',
519
+ 'transition duration-200',
520
+ 'translate-x-1 data-[state=checked]:translate-x-6',
521
+ )}
522
+ />
523
+ </Switch>
524
+ <label
525
+ htmlFor="developer-mode"
526
+ className="text-sm text-gray-500 dark:text-gray-400 select-none cursor-pointer"
527
+ >
528
+ {developerMode ? 'Developer Mode' : 'User Mode'}
529
+ </label>
530
+ </div>
531
+
532
+ <button
533
+ onClick={onClose}
534
+ className="flex items-center justify-center w-8 h-8 rounded-lg bg-gray-100 dark:bg-gray-800 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 group transition-all duration-200"
535
+ >
536
+ <div className="i-ph:x w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
537
+ </button>
538
+ </div>
539
+ </div>
540
+
541
+ {/* Content */}
542
+ <div
543
+ className={classNames(
544
+ 'flex-1',
545
+ 'overflow-y-auto',
546
+ 'hover:overflow-y-auto',
547
+ 'scrollbar scrollbar-w-2',
548
+ 'scrollbar-track-transparent',
549
+ 'scrollbar-thumb-[#E5E5E5] hover:scrollbar-thumb-[#CCCCCC]',
550
+ 'dark:scrollbar-thumb-[#333333] dark:hover:scrollbar-thumb-[#444444]',
551
+ 'will-change-scroll',
552
+ 'touch-auto',
553
+ )}
554
+ >
555
+ <motion.div
556
+ key={activeTab || 'home'}
557
+ initial={{ opacity: 0 }}
558
+ animate={{ opacity: 1 }}
559
+ exit={{ opacity: 0 }}
560
+ transition={{ duration: 0.2 }}
561
+ className="p-6"
562
+ >
563
+ {showTabManagement ? (
564
+ <TabManagement />
565
+ ) : activeTab ? (
566
+ getTabComponent()
567
+ ) : (
568
+ <motion.div className="grid grid-cols-4 gap-4">
569
+ <AnimatePresence mode="popLayout">
570
+ {visibleTabs.map((tab: TabWithDevType, index: number) => (
571
+ <motion.div
572
+ key={tab.id}
573
+ layout
574
+ initial={{ opacity: 0, scale: 0.8, y: 20 }}
575
+ animate={{ opacity: 1, scale: 1, y: 0 }}
576
+ exit={{ opacity: 0, scale: 0.8, y: -20 }}
577
+ transition={{
578
+ duration: 0.2,
579
+ delay: index * 0.05,
580
+ }}
581
+ >
582
+ <DraggableTabTile
583
+ tab={tab}
584
+ index={index}
585
+ moveTab={moveTab}
586
+ onClick={() => handleTabClick(tab.id)}
587
+ isActive={activeTab === tab.id}
588
+ hasUpdate={getTabUpdateStatus(tab.id)}
589
+ statusMessage={getStatusMessage(tab.id)}
590
+ description={TAB_DESCRIPTIONS[tab.id]}
591
+ isLoading={loadingTab === tab.id}
592
+ />
593
+ </motion.div>
594
+ ))}
595
+ </AnimatePresence>
596
+ </motion.div>
597
+ )}
598
+ </motion.div>
599
+ </div>
600
+ </motion.div>
601
+ </RadixDialog.Content>
602
+ </div>
603
+ </RadixDialog.Portal>
604
+ </RadixDialog.Root>
605
+ </DndProvider>
606
+ );
607
+ };
app/components/settings/developer/DeveloperWindow.tsx CHANGED
@@ -1,5 +1,5 @@
1
  import * as RadixDialog from '@radix-ui/react-dialog';
2
- import { motion } from 'framer-motion';
3
  import { useState, useEffect, useMemo } from 'react';
4
  import { classNames } from '~/utils/classNames';
5
  import { TabManagement } from './TabManagement';
@@ -481,14 +481,9 @@ export const DeveloperWindow = ({ open, onClose }: DeveloperWindowProps) => {
481
  'border border-[#E5E5E5] dark:border-[#1A1A1A]',
482
  'flex flex-col overflow-hidden',
483
  )}
484
- initial={{ opacity: 0, scale: 0.95, y: 20 }}
485
- animate={{
486
- opacity: developerMode ? 1 : 0,
487
- scale: developerMode ? 1 : 0.95,
488
- y: developerMode ? 0 : 20,
489
- }}
490
- exit={{ opacity: 0, scale: 0.95, y: 20 }}
491
- transition={{ duration: 0.2 }}
492
  >
493
  {/* Header */}
494
  <div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700">
@@ -592,28 +587,54 @@ export const DeveloperWindow = ({ open, onClose }: DeveloperWindowProps) => {
592
  'touch-auto',
593
  )}
594
  >
595
- <motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="p-6">
 
 
 
 
 
 
 
596
  {showTabManagement ? (
597
  <TabManagement />
598
  ) : activeTab ? (
599
  getTabComponent()
600
  ) : (
601
- <div className="grid grid-cols-4 gap-4">
602
- {visibleDeveloperTabs.map((tab: TabVisibilityConfig, index: number) => (
603
- <DraggableTabTile
604
- key={tab.id}
605
- tab={tab}
606
- index={index}
607
- moveTab={moveTab}
608
- onClick={() => handleTabClick(tab.id)}
609
- isActive={activeTab === tab.id}
610
- hasUpdate={getTabUpdateStatus(tab.id)}
611
- statusMessage={getStatusMessage(tab.id)}
612
- description={TAB_DESCRIPTIONS[tab.id]}
613
- isLoading={loadingTab === tab.id}
614
- />
615
- ))}
616
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
617
  )}
618
  </motion.div>
619
  </div>
 
1
  import * as RadixDialog from '@radix-ui/react-dialog';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
  import { useState, useEffect, useMemo } from 'react';
4
  import { classNames } from '~/utils/classNames';
5
  import { TabManagement } from './TabManagement';
 
481
  'border border-[#E5E5E5] dark:border-[#1A1A1A]',
482
  'flex flex-col overflow-hidden',
483
  )}
484
+ initial={{ opacity: 1 }}
485
+ animate={{ opacity: 1 }}
486
+ transition={{ duration: 0.15 }}
 
 
 
 
 
487
  >
488
  {/* Header */}
489
  <div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700">
 
587
  'touch-auto',
588
  )}
589
  >
590
+ <motion.div
591
+ key={activeTab || 'home'}
592
+ initial={{ opacity: 0 }}
593
+ animate={{ opacity: 1 }}
594
+ exit={{ opacity: 0 }}
595
+ transition={{ duration: 0.2 }}
596
+ className="p-6"
597
+ >
598
  {showTabManagement ? (
599
  <TabManagement />
600
  ) : activeTab ? (
601
  getTabComponent()
602
  ) : (
603
+ <motion.div
604
+ className="grid grid-cols-4 gap-4"
605
+ initial={{ opacity: 0 }}
606
+ animate={{ opacity: 1 }}
607
+ exit={{ opacity: 0 }}
608
+ transition={{ duration: 0.2 }}
609
+ >
610
+ <AnimatePresence mode="popLayout">
611
+ {visibleDeveloperTabs.map((tab: TabVisibilityConfig, index: number) => (
612
+ <motion.div
613
+ key={tab.id}
614
+ layout
615
+ initial={{ opacity: 0, scale: 0.8, y: 20 }}
616
+ animate={{ opacity: 1, scale: 1, y: 0 }}
617
+ exit={{ opacity: 0, scale: 0.8, y: -20 }}
618
+ transition={{
619
+ duration: 0.2,
620
+ delay: index * 0.05,
621
+ }}
622
+ >
623
+ <DraggableTabTile
624
+ tab={tab}
625
+ index={index}
626
+ moveTab={moveTab}
627
+ onClick={() => handleTabClick(tab.id)}
628
+ isActive={activeTab === tab.id}
629
+ hasUpdate={getTabUpdateStatus(tab.id)}
630
+ statusMessage={getStatusMessage(tab.id)}
631
+ description={TAB_DESCRIPTIONS[tab.id]}
632
+ isLoading={loadingTab === tab.id}
633
+ />
634
+ </motion.div>
635
+ ))}
636
+ </AnimatePresence>
637
+ </motion.div>
638
  )}
639
  </motion.div>
640
  </div>
app/components/settings/developer/TabManagement.tsx CHANGED
@@ -1,9 +1,16 @@
1
- import { motion } from 'framer-motion';
2
- import { useState } from 'react';
3
- import { classNames } from '~/utils/classNames';
4
- import { tabConfigurationStore, updateTabConfiguration, resetTabConfiguration } from '~/lib/stores/settings';
5
  import { useStore } from '@nanostores/react';
6
- import { TAB_LABELS, type TabType, type TabVisibilityConfig } from '~/components/settings/settings.types';
 
 
 
 
 
 
 
 
 
7
  import { toast } from 'react-toastify';
8
 
9
  // Define icons for each tab type
@@ -23,152 +30,88 @@ const TAB_ICONS: Record<TabType, string> = {
23
  'service-status': 'i-ph:heartbeat-fill',
24
  };
25
 
26
- interface TabGroupProps {
27
- title: string;
28
- description?: string;
29
- tabs: TabVisibilityConfig[];
30
- onVisibilityChange: (tabId: TabType, enabled: boolean) => void;
31
- targetWindow: 'user' | 'developer';
32
- standardTabs: TabType[];
33
  }
34
 
35
- const TabGroup = ({ title, description, tabs, onVisibilityChange, targetWindow }: TabGroupProps) => {
36
- // Split tabs into visible and hidden
37
- const visibleTabs = tabs.filter((tab) => tab.visible).sort((a, b) => (a.order || 0) - (b.order || 0));
38
- const hiddenTabs = tabs.filter((tab) => !tab.visible).sort((a, b) => (a.order || 0) - (b.order || 0));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
 
40
  return (
41
- <div className="mb-8 rounded-xl bg-white/5 p-6 dark:bg-gray-800/30">
42
- <div className="mb-6">
43
- <h3 className="flex items-center gap-2 text-lg font-medium text-gray-900 dark:text-white">
44
- <span className="i-ph:layout-fill h-5 w-5 text-purple-500" />
45
- {title}
46
- </h3>
47
- {description && <p className="mt-1.5 text-sm text-gray-600 dark:text-gray-400">{description}</p>}
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  </div>
49
-
50
- <div className="space-y-6">
51
- <motion.div layout className="space-y-2">
52
- {visibleTabs.map((tab) => (
53
- <motion.div
54
- key={tab.id}
55
- layout
56
- initial={{ opacity: 0, y: 20 }}
57
- animate={{ opacity: 1, y: 0 }}
58
- exit={{ opacity: 0, y: -20 }}
59
- transition={{ duration: 0.2 }}
60
- className="group relative flex items-center justify-between rounded-lg border border-gray-200 bg-white px-4 py-3 shadow-sm transition-all hover:border-purple-200 hover:shadow-md dark:border-gray-700 dark:bg-gray-800 dark:hover:border-purple-500/30"
61
- >
62
- <div className="flex items-center space-x-3">
63
- <div
64
- className={classNames(
65
- TAB_ICONS[tab.id],
66
- 'h-5 w-5 transition-colors',
67
- tab.id === 'profile'
68
- ? 'text-purple-500 dark:text-purple-400'
69
- : 'text-gray-500 group-hover:text-purple-500 dark:text-gray-400 dark:group-hover:text-purple-400',
70
- )}
71
- />
72
- <span
73
- className={classNames(
74
- 'text-sm font-medium transition-colors',
75
- tab.id === 'profile'
76
- ? 'text-gray-900 dark:text-white'
77
- : 'text-gray-700 group-hover:text-gray-900 dark:text-gray-300 dark:group-hover:text-white',
78
- )}
79
- >
80
- {TAB_LABELS[tab.id]}
81
- </span>
82
- {tab.id === 'profile' && targetWindow === 'user' && (
83
- <span className="rounded-full bg-purple-50 px-2 py-0.5 text-xs font-medium text-purple-600 dark:bg-purple-500/10 dark:text-purple-400">
84
- Standard
85
- </span>
86
- )}
87
- </div>
88
- <div className="flex items-center space-x-4">
89
- {targetWindow === 'user' ? (
90
- <label className="relative inline-flex cursor-pointer items-center">
91
- <input
92
- type="checkbox"
93
- checked={tab.visible}
94
- onChange={(e) => onVisibilityChange(tab.id, e.target.checked)}
95
- className="peer sr-only"
96
- />
97
- <div
98
- className={classNames(
99
- 'h-6 w-11 rounded-full bg-gray-200 transition-colors dark:bg-gray-700',
100
- 'after:absolute after:left-[2px] after:top-[2px]',
101
- 'after:h-5 after:w-5 after:rounded-full after:bg-white after:shadow-sm',
102
- 'after:transition-all after:content-[""]',
103
- 'peer-checked:bg-purple-500 peer-checked:after:translate-x-full',
104
- 'peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-purple-500/20',
105
- )}
106
- />
107
- </label>
108
- ) : (
109
- <div className="text-sm text-gray-500 dark:text-gray-400">Always visible</div>
110
- )}
111
- </div>
112
- </motion.div>
113
- ))}
114
- </motion.div>
115
-
116
- {hiddenTabs.length > 0 && (
117
- <motion.div layout className="space-y-2">
118
- <div className="flex items-center gap-2 text-sm font-medium text-gray-500 dark:text-gray-400">
119
- <span className="i-ph:eye-slash-fill h-4 w-4" />
120
- Hidden Tabs
121
- </div>
122
- {hiddenTabs.map((tab) => (
123
- <motion.div
124
- key={tab.id}
125
- layout
126
- initial={{ opacity: 0, y: 20 }}
127
- animate={{ opacity: 1, y: 0 }}
128
- exit={{ opacity: 0, y: -20 }}
129
- transition={{ duration: 0.2 }}
130
- className="group relative flex items-center justify-between rounded-lg border border-gray-200 bg-white/50 px-4 py-3 transition-all hover:border-purple-200 dark:border-gray-700 dark:bg-gray-800/50 dark:hover:border-purple-500/30"
131
- >
132
- <div className="flex items-center space-x-3">
133
- <div
134
- className={classNames(
135
- TAB_ICONS[tab.id],
136
- 'h-5 w-5 transition-colors',
137
- 'text-gray-400 group-hover:text-purple-500 dark:text-gray-500 dark:group-hover:text-purple-400',
138
- )}
139
- />
140
- <span className="text-sm font-medium text-gray-500 transition-colors group-hover:text-gray-900 dark:text-gray-400 dark:group-hover:text-white">
141
- {TAB_LABELS[tab.id]}
142
- </span>
143
- </div>
144
- <div className="flex items-center space-x-4">
145
- {targetWindow === 'user' && (
146
- <label className="relative inline-flex cursor-pointer items-center">
147
- <input
148
- type="checkbox"
149
- checked={tab.visible}
150
- onChange={(e) => onVisibilityChange(tab.id, e.target.checked)}
151
- className="peer sr-only"
152
- />
153
- <div
154
- className={classNames(
155
- 'h-6 w-11 rounded-full bg-gray-200 transition-colors dark:bg-gray-700',
156
- 'after:absolute after:left-[2px] after:top-[2px]',
157
- 'after:h-5 after:w-5 after:rounded-full after:bg-white after:shadow-sm',
158
- 'after:transition-all after:content-[""]',
159
- 'peer-checked:bg-purple-500 peer-checked:after:translate-x-full',
160
- 'peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-purple-500/20',
161
- )}
162
- />
163
- </label>
164
- )}
165
- </div>
166
- </motion.div>
167
- ))}
168
- </motion.div>
169
- )}
170
  </div>
171
- </div>
172
  );
173
  };
174
 
@@ -176,53 +119,64 @@ export const TabManagement = () => {
176
  const config = useStore(tabConfigurationStore);
177
  const [searchQuery, setSearchQuery] = useState('');
178
 
179
- // Define standard (visible by default) tabs for each window
180
- const standardUserTabs: TabType[] = [
181
- 'features',
182
- 'data',
183
- 'local-providers',
184
- 'cloud-providers',
185
- 'connection',
186
- 'debug',
187
- 'service-status',
188
- ];
189
- const standardDeveloperTabs: TabType[] = [
190
- 'profile',
191
- 'settings',
192
- 'notifications',
193
- 'features',
194
- 'data',
195
- 'local-providers',
196
- 'cloud-providers',
197
- 'connection',
198
- 'debug',
199
- 'event-logs',
200
- 'update',
201
- 'task-manager',
202
- 'service-status',
203
- ];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
 
205
- const handleVisibilityChange = (tabId: TabType, enabled: boolean, targetWindow: 'user' | 'developer') => {
206
- const tabs = targetWindow === 'user' ? config.userTabs : config.developerTabs;
207
- const existingTab = tabs.find((tab) => tab.id === tabId);
208
 
209
- const updatedTab: TabVisibilityConfig = existingTab
210
- ? {
211
- ...existingTab,
212
- visible: enabled,
213
- }
214
- : {
215
- id: tabId,
216
- visible: enabled,
217
- window: targetWindow,
218
- order: tabs.length,
219
- };
220
 
221
- // Update the store
222
- updateTabConfiguration(updatedTab);
 
 
223
 
224
- // Show toast notification
225
- toast.success(`${TAB_LABELS[tabId]} ${enabled ? 'enabled' : 'disabled'} in ${targetWindow} window`);
 
 
226
  };
227
 
228
  const handleResetToDefaults = () => {
@@ -230,38 +184,14 @@ export const TabManagement = () => {
230
  toast.success('Tab settings reset to defaults');
231
  };
232
 
233
- // Filter tabs based on search and window
234
- const userTabs = (config.userTabs || []).filter(
235
- (tab) => tab && TAB_LABELS[tab.id]?.toLowerCase().includes((searchQuery || '').toLowerCase()),
236
- );
237
-
238
- const developerTabs = (config.developerTabs || []).filter(
239
- (tab) => tab && TAB_LABELS[tab.id]?.toLowerCase().includes((searchQuery || '').toLowerCase()),
240
- );
241
 
242
  return (
243
- <div className="h-full overflow-y-auto px-6 py-6">
244
- <div className="mb-8">
245
  <div className="flex items-center justify-between">
246
- <div>
247
- <h2 className="flex items-center gap-2 text-xl font-semibold text-gray-900 dark:text-white">
248
- <span className="i-ph:squares-four-fill h-6 w-6 text-purple-500" />
249
- Tab Management
250
- </h2>
251
- <p className="mt-1.5 text-sm text-gray-600 dark:text-gray-400">
252
- Configure which tabs are visible in the user and developer windows
253
- </p>
254
- </div>
255
- <button
256
- onClick={handleResetToDefaults}
257
- className="inline-flex items-center gap-1.5 rounded-lg bg-purple-50 px-4 py-2 text-sm font-medium text-purple-600 transition-colors hover:bg-purple-100 focus:outline-none focus:ring-4 focus:ring-purple-500/20 dark:bg-purple-500/10 dark:text-purple-400 dark:hover:bg-purple-500/20"
258
- >
259
- <span className="i-ph:arrow-counter-clockwise-fill h-4 w-4" />
260
- Reset to Defaults
261
- </button>
262
- </div>
263
-
264
- <div className="mt-6 flex items-center gap-4">
265
  <div className="relative flex-1">
266
  <div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
267
  <span className="i-ph:magnifying-glass h-5 w-5 text-gray-400" />
@@ -274,56 +204,31 @@ export const TabManagement = () => {
274
  className="block w-full rounded-lg border border-gray-200 bg-white py-2.5 pl-10 pr-4 text-sm text-gray-900 placeholder:text-gray-500 focus:border-purple-500 focus:outline-none focus:ring-4 focus:ring-purple-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-white dark:placeholder:text-gray-400 dark:focus:border-purple-400"
275
  />
276
  </div>
277
- </div>
278
- </div>
279
-
280
- <div className="space-y-8">
281
- {/* User Window Section */}
282
- <div className="rounded-xl border border-purple-100 bg-purple-50/50 p-1 dark:border-purple-500/10 dark:bg-purple-500/5">
283
- <div className="rounded-lg bg-white p-6 dark:bg-gray-800">
284
- <div className="mb-6 flex items-center gap-3">
285
- <div className="rounded-lg bg-purple-100 p-2 dark:bg-purple-500/10">
286
- <span className="i-ph:user-circle-fill h-5 w-5 text-purple-500 dark:text-purple-400" />
287
- </div>
288
- <div>
289
- <h3 className="text-base font-medium text-gray-900 dark:text-white">User Window</h3>
290
- <p className="text-sm text-gray-600 dark:text-gray-400">Configure tabs visible to regular users</p>
291
- </div>
292
- </div>
293
- <TabGroup
294
- title="User Interface"
295
- description="Manage which tabs are visible in the user window"
296
- tabs={userTabs}
297
- onVisibilityChange={(tabId, enabled) => handleVisibilityChange(tabId, enabled, 'user')}
298
- targetWindow="user"
299
- standardTabs={standardUserTabs}
300
- />
301
- </div>
302
  </div>
303
 
304
- {/* Developer Window Section */}
305
- <div className="rounded-xl border border-blue-100 bg-blue-50/50 p-1 dark:border-blue-500/10 dark:bg-blue-500/5">
306
- <div className="rounded-lg bg-white p-6 dark:bg-gray-800">
307
- <div className="mb-6 flex items-center gap-3">
308
- <div className="rounded-lg bg-blue-100 p-2 dark:bg-blue-500/10">
309
- <span className="i-ph:code-fill h-5 w-5 text-blue-500 dark:text-blue-400" />
310
- </div>
311
- <div>
312
- <h3 className="text-base font-medium text-gray-900 dark:text-white">Developer Window</h3>
313
- <p className="text-sm text-gray-600 dark:text-gray-400">Configure tabs visible to developers</p>
314
- </div>
 
315
  </div>
316
- <TabGroup
317
- title="Developer Interface"
318
- description="Manage which tabs are visible in the developer window"
319
- tabs={developerTabs}
320
- onVisibilityChange={(tabId, enabled) => handleVisibilityChange(tabId, enabled, 'developer')}
321
- targetWindow="developer"
322
- standardTabs={standardDeveloperTabs}
323
- />
324
- </div>
325
  </div>
326
  </div>
327
- </div>
328
  );
329
  };
 
1
+ import { motion, AnimatePresence } from 'framer-motion';
2
+ import { useState, useMemo } from 'react';
 
 
3
  import { useStore } from '@nanostores/react';
4
+ import { DndProvider, useDrag, useDrop } from 'react-dnd';
5
+ import { HTML5Backend } from 'react-dnd-html5-backend';
6
+ import { classNames } from '~/utils/classNames';
7
+ import { tabConfigurationStore, resetTabConfiguration } from '~/lib/stores/settings';
8
+ import {
9
+ TAB_LABELS,
10
+ DEFAULT_TAB_CONFIG,
11
+ type TabType,
12
+ type TabVisibilityConfig,
13
+ } from '~/components/settings/settings.types';
14
  import { toast } from 'react-toastify';
15
 
16
  // Define icons for each tab type
 
30
  'service-status': 'i-ph:heartbeat-fill',
31
  };
32
 
33
+ interface DraggableTabProps {
34
+ tab: TabVisibilityConfig;
35
+ index: number;
36
+ moveTab: (dragIndex: number, hoverIndex: number) => void;
37
+ onVisibilityChange: (enabled: boolean) => void;
 
 
38
  }
39
 
40
+ const DraggableTab = ({ tab, index, moveTab, onVisibilityChange }: DraggableTabProps) => {
41
+ const [{ isDragging }, drag] = useDrag({
42
+ type: 'tab-management',
43
+ item: { index, id: tab.id },
44
+ collect: (monitor) => ({
45
+ isDragging: monitor.isDragging(),
46
+ }),
47
+ });
48
+
49
+ const [{ isOver }, drop] = useDrop({
50
+ accept: 'tab-management',
51
+ hover: (item: { index: number; id: string }, monitor) => {
52
+ if (!monitor.isOver({ shallow: true })) {
53
+ return;
54
+ }
55
+
56
+ if (item.id === tab.id) {
57
+ return;
58
+ }
59
+
60
+ if (item.index === index) {
61
+ return;
62
+ }
63
+
64
+ moveTab(item.index, index);
65
+ item.index = index;
66
+ },
67
+ collect: (monitor) => ({
68
+ isOver: monitor.isOver({ shallow: true }),
69
+ }),
70
+ });
71
 
72
  return (
73
+ <motion.div
74
+ ref={(node) => drag(drop(node))}
75
+ layout
76
+ initial={{ opacity: 0, y: 20 }}
77
+ animate={{ opacity: 1, y: 0 }}
78
+ exit={{ opacity: 0, y: -20 }}
79
+ style={{
80
+ opacity: isDragging ? 0.5 : 1,
81
+ cursor: 'move',
82
+ }}
83
+ className={classNames(
84
+ 'group relative flex items-center justify-between rounded-lg border px-4 py-3 transition-all',
85
+ isOver
86
+ ? 'border-purple-500 bg-purple-50/50 dark:border-purple-500/50 dark:bg-purple-500/10'
87
+ : 'border-gray-200 bg-white hover:border-purple-200 dark:border-gray-700 dark:bg-gray-800 dark:hover:border-purple-500/30',
88
+ )}
89
+ >
90
+ <div className="flex items-center space-x-3">
91
+ <div className={classNames(TAB_ICONS[tab.id], 'h-5 w-5 text-purple-500 dark:text-purple-400')} />
92
+ <span className="text-sm font-medium text-gray-900 dark:text-white">{TAB_LABELS[tab.id]}</span>
93
  </div>
94
+ <div className="flex items-center space-x-4">
95
+ <label className="relative inline-flex cursor-pointer items-center">
96
+ <input
97
+ type="checkbox"
98
+ checked={tab.visible}
99
+ onChange={(e) => onVisibilityChange(e.target.checked)}
100
+ className="peer sr-only"
101
+ />
102
+ <div
103
+ className={classNames(
104
+ 'h-6 w-11 rounded-full bg-gray-200 transition-colors dark:bg-gray-700',
105
+ 'after:absolute after:left-[2px] after:top-[2px]',
106
+ 'after:h-5 after:w-5 after:rounded-full after:bg-white after:shadow-sm',
107
+ 'after:transition-all after:content-[""]',
108
+ 'peer-checked:bg-purple-500 peer-checked:after:translate-x-full',
109
+ 'peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-purple-500/20',
110
+ )}
111
+ />
112
+ </label>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  </div>
114
+ </motion.div>
115
  );
116
  };
117
 
 
119
  const config = useStore(tabConfigurationStore);
120
  const [searchQuery, setSearchQuery] = useState('');
121
 
122
+ // Get ALL possible tabs for developer mode
123
+ const allTabs = useMemo(() => {
124
+ const uniqueTabs = new Set([
125
+ ...DEFAULT_TAB_CONFIG.map((tab) => tab.id),
126
+ ...(config.userTabs || []).map((tab) => tab.id),
127
+ ...(config.developerTabs || []).map((tab) => tab.id),
128
+ 'event-logs', // Ensure these are always included
129
+ 'task-manager',
130
+ ]);
131
+
132
+ return Array.from(uniqueTabs).map((tabId) => {
133
+ const existingTab =
134
+ config.developerTabs?.find((t) => t.id === tabId) ||
135
+ config.userTabs?.find((t) => t.id === tabId) ||
136
+ DEFAULT_TAB_CONFIG.find((t) => t.id === tabId);
137
+
138
+ return {
139
+ id: tabId as TabType,
140
+ visible: true,
141
+ window: 'developer' as const,
142
+ order: existingTab?.order || DEFAULT_TAB_CONFIG.findIndex((t) => t.id === tabId),
143
+ };
144
+ });
145
+ }, [config]);
146
+
147
+ const handleVisibilityChange = (tabId: TabType, enabled: boolean) => {
148
+ const updatedDevTabs = allTabs.map((tab) => {
149
+ if (tab.id === tabId) {
150
+ return { ...tab, visible: enabled };
151
+ }
152
+
153
+ return tab;
154
+ });
155
+
156
+ tabConfigurationStore.set({
157
+ ...config,
158
+ developerTabs: updatedDevTabs,
159
+ });
160
+
161
+ toast.success(`${TAB_LABELS[tabId]} ${enabled ? 'enabled' : 'disabled'}`);
162
+ };
163
 
164
+ const moveTab = (dragIndex: number, hoverIndex: number) => {
165
+ const newTabs = [...allTabs];
166
+ const dragTab = newTabs[dragIndex];
167
 
168
+ newTabs.splice(dragIndex, 1);
169
+ newTabs.splice(hoverIndex, 0, dragTab);
 
 
 
 
 
 
 
 
 
170
 
171
+ const updatedTabs = newTabs.map((tab, index) => ({
172
+ ...tab,
173
+ order: index,
174
+ }));
175
 
176
+ tabConfigurationStore.set({
177
+ ...config,
178
+ developerTabs: updatedTabs,
179
+ });
180
  };
181
 
182
  const handleResetToDefaults = () => {
 
184
  toast.success('Tab settings reset to defaults');
185
  };
186
 
187
+ const filteredTabs = allTabs
188
+ .filter((tab) => tab && TAB_LABELS[tab.id]?.toLowerCase().includes((searchQuery || '').toLowerCase()))
189
+ .sort((a, b) => a.order - b.order);
 
 
 
 
 
190
 
191
  return (
192
+ <DndProvider backend={HTML5Backend}>
193
+ <div className="space-y-6">
194
  <div className="flex items-center justify-between">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
195
  <div className="relative flex-1">
196
  <div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
197
  <span className="i-ph:magnifying-glass h-5 w-5 text-gray-400" />
 
204
  className="block w-full rounded-lg border border-gray-200 bg-white py-2.5 pl-10 pr-4 text-sm text-gray-900 placeholder:text-gray-500 focus:border-purple-500 focus:outline-none focus:ring-4 focus:ring-purple-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-white dark:placeholder:text-gray-400 dark:focus:border-purple-400"
205
  />
206
  </div>
207
+ <button
208
+ onClick={handleResetToDefaults}
209
+ className="ml-4 inline-flex items-center gap-1.5 rounded-lg bg-purple-50 px-4 py-2 text-sm font-medium text-purple-600 transition-colors hover:bg-purple-100 focus:outline-none focus:ring-4 focus:ring-purple-500/20 dark:bg-purple-500/10 dark:text-purple-400 dark:hover:bg-purple-500/20"
210
+ >
211
+ <span className="i-ph:arrow-counter-clockwise-fill h-4 w-4" />
212
+ Reset to Defaults
213
+ </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
  </div>
215
 
216
+ <div className="rounded-xl border border-purple-100 bg-purple-50/50 p-6 dark:border-purple-500/10 dark:bg-purple-500/5">
217
+ <AnimatePresence mode="popLayout">
218
+ <div className="space-y-2">
219
+ {filteredTabs.map((tab, index) => (
220
+ <DraggableTab
221
+ key={tab.id}
222
+ tab={tab}
223
+ index={index}
224
+ moveTab={moveTab}
225
+ onVisibilityChange={(enabled) => handleVisibilityChange(tab.id, enabled)}
226
+ />
227
+ ))}
228
  </div>
229
+ </AnimatePresence>
 
 
 
 
 
 
 
 
230
  </div>
231
  </div>
232
+ </DndProvider>
233
  );
234
  };
app/components/settings/settings.types.ts CHANGED
@@ -77,33 +77,29 @@ export const DEFAULT_TAB_CONFIG: TabVisibilityConfig[] = [
77
  { id: 'features', visible: true, window: 'user', order: 0 },
78
  { id: 'data', visible: true, window: 'user', order: 1 },
79
  { id: 'cloud-providers', visible: true, window: 'user', order: 2 },
80
- { id: 'service-status', visible: true, window: 'user', order: 3 },
81
- { id: 'local-providers', visible: true, window: 'user', order: 4 },
82
- { id: 'connection', visible: true, window: 'user', order: 5 },
83
- { id: 'debug', visible: true, window: 'user', order: 6 },
84
 
85
  // User Window Tabs (Hidden by default)
86
- { id: 'profile', visible: false, window: 'user', order: 7 },
87
- { id: 'settings', visible: false, window: 'user', order: 8 },
88
- { id: 'notifications', visible: false, window: 'user', order: 9 },
89
- { id: 'event-logs', visible: false, window: 'user', order: 10 },
90
- { id: 'update', visible: false, window: 'user', order: 11 },
91
- { id: 'task-manager', visible: false, window: 'user', order: 12 },
92
 
93
  // Developer Window Tabs (All visible by default)
94
- { id: 'profile', visible: true, window: 'developer', order: 0 },
95
- { id: 'settings', visible: true, window: 'developer', order: 1 },
96
- { id: 'notifications', visible: true, window: 'developer', order: 2 },
97
- { id: 'features', visible: true, window: 'developer', order: 3 },
98
- { id: 'data', visible: true, window: 'developer', order: 4 },
99
- { id: 'cloud-providers', visible: true, window: 'developer', order: 5 },
100
- { id: 'local-providers', visible: true, window: 'developer', order: 6 },
101
- { id: 'connection', visible: true, window: 'developer', order: 7 },
102
- { id: 'debug', visible: true, window: 'developer', order: 8 },
103
- { id: 'event-logs', visible: true, window: 'developer', order: 9 },
104
- { id: 'update', visible: true, window: 'developer', order: 10 },
105
- { id: 'task-manager', visible: true, window: 'developer', order: 11 },
106
- { id: 'service-status', visible: true, window: 'developer', order: 12 },
107
  ];
108
 
109
  export const categoryLabels: Record<SettingCategory, string> = {
 
77
  { id: 'features', visible: true, window: 'user', order: 0 },
78
  { id: 'data', visible: true, window: 'user', order: 1 },
79
  { id: 'cloud-providers', visible: true, window: 'user', order: 2 },
80
+ { id: 'local-providers', visible: true, window: 'user', order: 3 },
81
+ { id: 'connection', visible: true, window: 'user', order: 4 },
82
+ { id: 'debug', visible: true, window: 'user', order: 5 },
 
83
 
84
  // User Window Tabs (Hidden by default)
85
+ { id: 'profile', visible: false, window: 'user', order: 6 },
86
+ { id: 'settings', visible: false, window: 'user', order: 7 },
87
+ { id: 'notifications', visible: false, window: 'user', order: 8 },
88
+ { id: 'event-logs', visible: false, window: 'user', order: 9 },
89
+ { id: 'update', visible: false, window: 'user', order: 10 },
90
+ { id: 'service-status', visible: false, window: 'user', order: 11 },
91
 
92
  // Developer Window Tabs (All visible by default)
93
+ { id: 'features', visible: true, window: 'developer', order: 0 },
94
+ { id: 'data', visible: true, window: 'developer', order: 1 },
95
+ { id: 'cloud-providers', visible: true, window: 'developer', order: 2 },
96
+ { id: 'local-providers', visible: true, window: 'developer', order: 3 },
97
+ { id: 'connection', visible: true, window: 'developer', order: 4 },
98
+ { id: 'debug', visible: true, window: 'developer', order: 5 },
99
+ { id: 'task-manager', visible: true, window: 'developer', order: 6 },
100
+ { id: 'settings', visible: true, window: 'developer', order: 7 },
101
+ { id: 'notifications', visible: true, window: 'developer', order: 8 },
102
+ { id: 'service-status', visible: true, window: 'developer', order: 9 },
 
 
 
103
  ];
104
 
105
  export const categoryLabels: Record<SettingCategory, string> = {
app/components/settings/user/UsersWindow.tsx CHANGED
@@ -1,6 +1,6 @@
1
  import * as RadixDialog from '@radix-ui/react-dialog';
2
  import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
3
- import { motion } from 'framer-motion';
4
  import React, { useState, useEffect, useMemo } from 'react';
5
  import { classNames } from '~/utils/classNames';
6
  import { DialogTitle } from '~/components/ui/Dialog';
@@ -37,6 +37,7 @@ import {
37
  developerModeStore,
38
  setDeveloperMode,
39
  } from '~/lib/stores/settings';
 
40
 
41
  interface DraggableTabTileProps {
42
  tab: TabVisibilityConfig;
@@ -123,6 +124,10 @@ interface UsersWindowProps {
123
  onClose: () => void;
124
  }
125
 
 
 
 
 
126
  export const UsersWindow = ({ open, onClose }: UsersWindowProps) => {
127
  const [activeTab, setActiveTab] = useState<TabType | null>(null);
128
  const [loadingTab, setLoadingTab] = useState<TabType | null>(null);
@@ -223,45 +228,48 @@ export const UsersWindow = ({ open, onClose }: UsersWindowProps) => {
223
 
224
  // Only show tabs that are assigned to the user window AND are visible
225
  const visibleUserTabs = useMemo(() => {
226
- console.log('Filtering user tabs with configuration:', tabConfiguration);
227
-
228
  if (!tabConfiguration?.userTabs || !Array.isArray(tabConfiguration.userTabs)) {
229
  console.warn('Invalid tab configuration, using empty array');
230
  return [];
231
  }
232
 
233
- return tabConfiguration.userTabs
234
- .filter((tab) => {
235
- if (!tab || typeof tab.id !== 'string') {
236
- console.warn('Invalid tab entry:', tab);
237
- return false;
238
- }
239
-
240
- // Hide notifications tab if notifications are disabled
241
- if (tab.id === 'notifications' && !profile.notifications) {
242
- console.log('Hiding notifications tab due to disabled notifications');
243
- return false;
244
- }
245
-
246
- // Ensure the tab has the required properties
247
- if (typeof tab.visible !== 'boolean' || typeof tab.window !== 'string' || typeof tab.order !== 'number') {
248
- console.warn('Tab missing required properties:', tab);
249
- return false;
250
- }
251
-
252
- // Only show tabs that are explicitly visible and assigned to the user window
253
- const isVisible = tab.visible && tab.window === 'user';
254
- console.log(`Tab ${tab.id} visibility:`, isVisible);
255
-
256
- return isVisible;
257
- })
258
- .sort((a: TabVisibilityConfig, b: TabVisibilityConfig) => {
259
- const orderA = typeof a.order === 'number' ? a.order : 0;
260
- const orderB = typeof b.order === 'number' ? b.order : 0;
261
-
262
- return orderA - orderB;
263
- });
264
- }, [tabConfiguration, profile.notifications]);
 
 
 
 
 
265
 
266
  const moveTab = (dragIndex: number, hoverIndex: number) => {
267
  const draggedTab = visibleUserTabs[dragIndex];
@@ -569,29 +577,50 @@ export const UsersWindow = ({ open, onClose }: UsersWindowProps) => {
569
  >
570
  <motion.div
571
  key={activeTab || 'home'}
572
- initial={{ opacity: 0, y: 20 }}
573
- animate={{ opacity: 1, y: 0 }}
 
 
574
  className="p-6"
575
  >
576
  {activeTab ? (
577
  getTabComponent()
578
  ) : (
579
- <div className="grid grid-cols-4 gap-4">
580
- {visibleUserTabs.map((tab: TabVisibilityConfig, index: number) => (
581
- <DraggableTabTile
582
- key={tab.id}
583
- tab={tab}
584
- index={index}
585
- moveTab={moveTab}
586
- onClick={() => handleTabClick(tab.id)}
587
- isActive={activeTab === tab.id}
588
- hasUpdate={getTabUpdateStatus(tab.id)}
589
- statusMessage={getStatusMessage(tab.id)}
590
- description={TAB_DESCRIPTIONS[tab.id]}
591
- isLoading={loadingTab === tab.id}
592
- />
593
- ))}
594
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
595
  )}
596
  </motion.div>
597
  </div>
 
1
  import * as RadixDialog from '@radix-ui/react-dialog';
2
  import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
3
+ import { motion, AnimatePresence } from 'framer-motion';
4
  import React, { useState, useEffect, useMemo } from 'react';
5
  import { classNames } from '~/utils/classNames';
6
  import { DialogTitle } from '~/components/ui/Dialog';
 
37
  developerModeStore,
38
  setDeveloperMode,
39
  } from '~/lib/stores/settings';
40
+ import { DEFAULT_TAB_CONFIG } from '~/components/settings/settings.types';
41
 
42
  interface DraggableTabTileProps {
43
  tab: TabVisibilityConfig;
 
124
  onClose: () => void;
125
  }
126
 
127
+ interface TabWithType extends TabVisibilityConfig {
128
+ isExtraDevTab?: boolean;
129
+ }
130
+
131
  export const UsersWindow = ({ open, onClose }: UsersWindowProps) => {
132
  const [activeTab, setActiveTab] = useState<TabType | null>(null);
133
  const [loadingTab, setLoadingTab] = useState<TabType | null>(null);
 
228
 
229
  // Only show tabs that are assigned to the user window AND are visible
230
  const visibleUserTabs = useMemo(() => {
 
 
231
  if (!tabConfiguration?.userTabs || !Array.isArray(tabConfiguration.userTabs)) {
232
  console.warn('Invalid tab configuration, using empty array');
233
  return [];
234
  }
235
 
236
+ // Get the base user tabs that are visible
237
+ const baseTabs = tabConfiguration.userTabs.filter((tab) => {
238
+ if (!tab || typeof tab.id !== 'string') {
239
+ console.warn('Invalid tab entry:', tab);
240
+ return false;
241
+ }
242
+
243
+ // Hide notifications tab if notifications are disabled
244
+ if (tab.id === 'notifications' && !profile.notifications) {
245
+ return false;
246
+ }
247
+
248
+ // Only show tabs that are explicitly visible and assigned to the user window
249
+ return tab.visible && tab.window === 'user';
250
+ });
251
+
252
+ // If in developer mode, add the developer-only tabs
253
+ if (developerMode) {
254
+ const developerOnlyTabs = DEFAULT_TAB_CONFIG.filter((tab) => {
255
+ /*
256
+ * Only include tabs that are:
257
+ * 1. Assigned to developer window
258
+ * 2. Not already in user tabs
259
+ * 3. Marked as visible in developer window
260
+ */
261
+ return tab.window === 'developer' && tab.visible && !baseTabs.some((baseTab) => baseTab.id === tab.id);
262
+ }).map((tab) => ({
263
+ ...tab,
264
+ isExtraDevTab: true,
265
+ order: baseTabs.length + tab.order, // Place after user tabs
266
+ }));
267
+
268
+ return [...baseTabs, ...developerOnlyTabs].sort((a, b) => a.order - b.order);
269
+ }
270
+
271
+ return baseTabs.sort((a, b) => a.order - b.order);
272
+ }, [tabConfiguration, profile.notifications, developerMode]);
273
 
274
  const moveTab = (dragIndex: number, hoverIndex: number) => {
275
  const draggedTab = visibleUserTabs[dragIndex];
 
577
  >
578
  <motion.div
579
  key={activeTab || 'home'}
580
+ initial={{ opacity: 0 }}
581
+ animate={{ opacity: 1 }}
582
+ exit={{ opacity: 0 }}
583
+ transition={{ duration: 0.2 }}
584
  className="p-6"
585
  >
586
  {activeTab ? (
587
  getTabComponent()
588
  ) : (
589
+ <motion.div
590
+ className="grid grid-cols-4 gap-4"
591
+ initial={{ opacity: 0 }}
592
+ animate={{ opacity: 1 }}
593
+ exit={{ opacity: 0 }}
594
+ transition={{ duration: 0.2 }}
595
+ >
596
+ <AnimatePresence mode="popLayout">
597
+ {visibleUserTabs.map((tab: TabWithType, index: number) => (
598
+ <motion.div
599
+ key={tab.id}
600
+ layout
601
+ initial={{ opacity: 0, scale: 0.8, y: 20 }}
602
+ animate={{ opacity: 1, scale: 1, y: 0 }}
603
+ exit={{ opacity: 0, scale: 0.8, y: -20 }}
604
+ transition={{
605
+ duration: 0.2,
606
+ delay: index * 0.05,
607
+ }}
608
+ >
609
+ <DraggableTabTile
610
+ tab={tab}
611
+ index={index}
612
+ moveTab={moveTab}
613
+ onClick={() => handleTabClick(tab.id)}
614
+ isActive={activeTab === tab.id}
615
+ hasUpdate={getTabUpdateStatus(tab.id)}
616
+ statusMessage={getStatusMessage(tab.id)}
617
+ description={TAB_DESCRIPTIONS[tab.id]}
618
+ isLoading={loadingTab === tab.id}
619
+ />
620
+ </motion.div>
621
+ ))}
622
+ </AnimatePresence>
623
+ </motion.div>
624
  )}
625
  </motion.div>
626
  </div>
app/lib/stores/settings.ts CHANGED
@@ -62,9 +62,11 @@ export const shortcutsStore = map<Shortcuts>({
62
  // Create a single key for provider settings
63
  const PROVIDER_SETTINGS_KEY = 'provider_settings';
64
 
 
 
 
65
  // Initialize provider settings from both localStorage and defaults
66
  const getInitialProviderSettings = (): ProviderSetting => {
67
- const savedSettings = localStorage.getItem(PROVIDER_SETTINGS_KEY);
68
  const initialSettings: ProviderSetting = {};
69
 
70
  // Start with default settings
@@ -77,17 +79,21 @@ const getInitialProviderSettings = (): ProviderSetting => {
77
  };
78
  });
79
 
80
- // Override with saved settings if they exist
81
- if (savedSettings) {
82
- try {
83
- const parsed = JSON.parse(savedSettings);
84
- Object.entries(parsed).forEach(([key, value]) => {
85
- if (initialSettings[key]) {
86
- initialSettings[key].settings = (value as IProviderConfig).settings;
87
- }
88
- });
89
- } catch (error) {
90
- console.error('Error parsing saved provider settings:', error);
 
 
 
 
91
  }
92
  }
93
 
@@ -127,11 +133,16 @@ const SETTINGS_KEYS = {
127
  EVENT_LOGS: 'isEventLogsEnabled',
128
  LOCAL_MODELS: 'isLocalModelsEnabled',
129
  PROMPT_ID: 'promptId',
 
130
  } as const;
131
 
132
  // Initialize settings from localStorage or defaults
133
  const getInitialSettings = () => {
134
  const getStoredBoolean = (key: string, defaultValue: boolean): boolean => {
 
 
 
 
135
  const stored = localStorage.getItem(key);
136
 
137
  if (stored === null) {
@@ -151,7 +162,8 @@ const getInitialSettings = () => {
151
  contextOptimization: getStoredBoolean(SETTINGS_KEYS.CONTEXT_OPTIMIZATION, false),
152
  eventLogs: getStoredBoolean(SETTINGS_KEYS.EVENT_LOGS, true),
153
  localModels: getStoredBoolean(SETTINGS_KEYS.LOCAL_MODELS, true),
154
- promptId: localStorage.getItem(SETTINGS_KEYS.PROMPT_ID) || 'default',
 
155
  };
156
  };
157
 
@@ -196,65 +208,40 @@ export const updatePromptId = (id: string) => {
196
  localStorage.setItem(SETTINGS_KEYS.PROMPT_ID, id);
197
  };
198
 
199
- // Initialize tab configuration from cookie or default
200
- const savedTabConfig = Cookies.get('tabConfiguration');
201
- console.log('Saved tab configuration:', savedTabConfig);
202
-
203
- let initialTabConfig: TabWindowConfig;
204
-
205
- try {
206
- if (savedTabConfig) {
207
- const parsedConfig = JSON.parse(savedTabConfig);
208
-
209
- // Validate the parsed configuration
210
- if (
211
- parsedConfig &&
212
- Array.isArray(parsedConfig.userTabs) &&
213
- Array.isArray(parsedConfig.developerTabs) &&
214
- parsedConfig.userTabs.every(
215
- (tab: any) =>
216
- tab &&
217
- typeof tab.id === 'string' &&
218
- typeof tab.visible === 'boolean' &&
219
- typeof tab.window === 'string' &&
220
- typeof tab.order === 'number',
221
- ) &&
222
- parsedConfig.developerTabs.every(
223
- (tab: any) =>
224
- tab &&
225
- typeof tab.id === 'string' &&
226
- typeof tab.visible === 'boolean' &&
227
- typeof tab.window === 'string' &&
228
- typeof tab.order === 'number',
229
- )
230
- ) {
231
- initialTabConfig = parsedConfig;
232
- console.log('Using saved tab configuration');
233
- } else {
234
- console.warn('Invalid saved tab configuration, using defaults');
235
- initialTabConfig = {
236
- userTabs: DEFAULT_TAB_CONFIG.filter((tab) => tab.window === 'user'),
237
- developerTabs: DEFAULT_TAB_CONFIG.filter((tab) => tab.window === 'developer'),
238
- };
239
- }
240
- } else {
241
- console.log('No saved tab configuration found, using defaults');
242
- initialTabConfig = {
243
- userTabs: DEFAULT_TAB_CONFIG.filter((tab) => tab.window === 'user'),
244
- developerTabs: DEFAULT_TAB_CONFIG.filter((tab) => tab.window === 'developer'),
245
- };
246
- }
247
- } catch (error) {
248
- console.error('Error loading tab configuration:', error);
249
- initialTabConfig = {
250
  userTabs: DEFAULT_TAB_CONFIG.filter((tab) => tab.window === 'user'),
251
  developerTabs: DEFAULT_TAB_CONFIG.filter((tab) => tab.window === 'developer'),
252
  };
253
- }
254
 
255
- console.log('Initial tab configuration:', initialTabConfig);
 
 
256
 
257
- export const tabConfigurationStore = map<TabWindowConfig>(initialTabConfig);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
 
259
  // Helper function to update tab configuration
260
  export const updateTabConfiguration = (config: TabVisibilityConfig) => {
@@ -307,9 +294,13 @@ export const resetTabConfiguration = () => {
307
  });
308
  };
309
 
310
- // Developer mode store
311
- export const developerModeStore = atom<boolean>(false);
312
 
313
  export const setDeveloperMode = (value: boolean) => {
314
  developerModeStore.set(value);
 
 
 
 
315
  };
 
62
  // Create a single key for provider settings
63
  const PROVIDER_SETTINGS_KEY = 'provider_settings';
64
 
65
+ // Add this helper function at the top of the file
66
+ const isBrowser = typeof window !== 'undefined';
67
+
68
  // Initialize provider settings from both localStorage and defaults
69
  const getInitialProviderSettings = (): ProviderSetting => {
 
70
  const initialSettings: ProviderSetting = {};
71
 
72
  // Start with default settings
 
79
  };
80
  });
81
 
82
+ // Only try to load from localStorage in the browser
83
+ if (isBrowser) {
84
+ const savedSettings = localStorage.getItem(PROVIDER_SETTINGS_KEY);
85
+
86
+ if (savedSettings) {
87
+ try {
88
+ const parsed = JSON.parse(savedSettings);
89
+ Object.entries(parsed).forEach(([key, value]) => {
90
+ if (initialSettings[key]) {
91
+ initialSettings[key].settings = (value as IProviderConfig).settings;
92
+ }
93
+ });
94
+ } catch (error) {
95
+ console.error('Error parsing saved provider settings:', error);
96
+ }
97
  }
98
  }
99
 
 
133
  EVENT_LOGS: 'isEventLogsEnabled',
134
  LOCAL_MODELS: 'isLocalModelsEnabled',
135
  PROMPT_ID: 'promptId',
136
+ DEVELOPER_MODE: 'isDeveloperMode',
137
  } as const;
138
 
139
  // Initialize settings from localStorage or defaults
140
  const getInitialSettings = () => {
141
  const getStoredBoolean = (key: string, defaultValue: boolean): boolean => {
142
+ if (!isBrowser) {
143
+ return defaultValue;
144
+ }
145
+
146
  const stored = localStorage.getItem(key);
147
 
148
  if (stored === null) {
 
162
  contextOptimization: getStoredBoolean(SETTINGS_KEYS.CONTEXT_OPTIMIZATION, false),
163
  eventLogs: getStoredBoolean(SETTINGS_KEYS.EVENT_LOGS, true),
164
  localModels: getStoredBoolean(SETTINGS_KEYS.LOCAL_MODELS, true),
165
+ promptId: isBrowser ? localStorage.getItem(SETTINGS_KEYS.PROMPT_ID) || 'default' : 'default',
166
+ developerMode: getStoredBoolean(SETTINGS_KEYS.DEVELOPER_MODE, false),
167
  };
168
  };
169
 
 
208
  localStorage.setItem(SETTINGS_KEYS.PROMPT_ID, id);
209
  };
210
 
211
+ // Initialize tab configuration from localStorage or defaults
212
+ const getInitialTabConfiguration = (): TabWindowConfig => {
213
+ const defaultConfig: TabWindowConfig = {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
  userTabs: DEFAULT_TAB_CONFIG.filter((tab) => tab.window === 'user'),
215
  developerTabs: DEFAULT_TAB_CONFIG.filter((tab) => tab.window === 'developer'),
216
  };
 
217
 
218
+ if (!isBrowser) {
219
+ return defaultConfig;
220
+ }
221
 
222
+ try {
223
+ const saved = localStorage.getItem('bolt_tab_configuration');
224
+
225
+ if (!saved) {
226
+ return defaultConfig;
227
+ }
228
+
229
+ const parsed = JSON.parse(saved);
230
+
231
+ if (!parsed?.userTabs || !parsed?.developerTabs) {
232
+ return defaultConfig;
233
+ }
234
+
235
+ return parsed;
236
+ } catch (error) {
237
+ console.warn('Failed to parse tab configuration:', error);
238
+ return defaultConfig;
239
+ }
240
+ };
241
+
242
+ console.log('Initial tab configuration:', getInitialTabConfiguration());
243
+
244
+ export const tabConfigurationStore = map<TabWindowConfig>(getInitialTabConfiguration());
245
 
246
  // Helper function to update tab configuration
247
  export const updateTabConfiguration = (config: TabVisibilityConfig) => {
 
294
  });
295
  };
296
 
297
+ // Developer mode store with persistence
298
+ export const developerModeStore = atom<boolean>(initialSettings.developerMode);
299
 
300
  export const setDeveloperMode = (value: boolean) => {
301
  developerModeStore.set(value);
302
+
303
+ if (isBrowser) {
304
+ localStorage.setItem(SETTINGS_KEYS.DEVELOPER_MODE, JSON.stringify(value));
305
+ }
306
  };
app/routes/_index.tsx CHANGED
@@ -4,6 +4,8 @@ import { BaseChat } from '~/components/chat/BaseChat';
4
  import { Chat } from '~/components/chat/Chat.client';
5
  import { Header } from '~/components/header/Header';
6
  import BackgroundRays from '~/components/ui/BackgroundRays';
 
 
7
 
8
  export const meta: MetaFunction = () => {
9
  return [{ title: 'Bolt' }, { name: 'description', content: 'Talk with Bolt, an AI assistant from StackBlitz' }];
@@ -12,11 +14,23 @@ export const meta: MetaFunction = () => {
12
  export const loader = () => json({});
13
 
14
  export default function Index() {
 
 
15
  return (
16
  <div className="flex flex-col h-full w-full bg-bolt-elements-background-depth-1">
17
  <BackgroundRays />
18
  <Header />
19
  <ClientOnly fallback={<BaseChat />}>{() => <Chat />}</ClientOnly>
 
 
 
 
 
 
 
 
 
 
20
  </div>
21
  );
22
  }
 
4
  import { Chat } from '~/components/chat/Chat.client';
5
  import { Header } from '~/components/header/Header';
6
  import BackgroundRays from '~/components/ui/BackgroundRays';
7
+ import { ControlPanel } from '~/components/settings/ControlPanel';
8
+ import { useState } from 'react';
9
 
10
  export const meta: MetaFunction = () => {
11
  return [{ title: 'Bolt' }, { name: 'description', content: 'Talk with Bolt, an AI assistant from StackBlitz' }];
 
14
  export const loader = () => json({});
15
 
16
  export default function Index() {
17
+ const [showControlPanel, setShowControlPanel] = useState(false);
18
+
19
  return (
20
  <div className="flex flex-col h-full w-full bg-bolt-elements-background-depth-1">
21
  <BackgroundRays />
22
  <Header />
23
  <ClientOnly fallback={<BaseChat />}>{() => <Chat />}</ClientOnly>
24
+ <button
25
+ onClick={() => setShowControlPanel(true)}
26
+ className="fixed bottom-4 right-4 flex items-center space-x-2 px-4 py-2 rounded-lg bg-purple-500 text-white hover:bg-purple-600 transition-colors"
27
+ >
28
+ <span className="i-ph:gear w-5 h-5" />
29
+ <span>Open Control Panel</span>
30
+ </button>
31
+ <ClientOnly>
32
+ {() => <ControlPanel open={showControlPanel} onClose={() => setShowControlPanel(false)} />}
33
+ </ClientOnly>
34
  </div>
35
  );
36
  }
package.json CHANGED
@@ -78,6 +78,7 @@
78
  "@remix-run/cloudflare-pages": "^2.15.2",
79
  "@remix-run/node": "^2.15.2",
80
  "@remix-run/react": "^2.15.2",
 
81
  "@uiw/codemirror-theme-vscode": "^4.23.6",
82
  "@unocss/reset": "^0.61.9",
83
  "@webcontainer/api": "1.3.0-internal.10",
@@ -106,6 +107,7 @@
106
  "ollama-ai-provider": "^0.15.2",
107
  "path-browserify": "^1.0.1",
108
  "react": "^18.3.1",
 
109
  "react-chartjs-2": "^5.3.0",
110
  "react-dnd": "^16.0.1",
111
  "react-dnd-html5-backend": "^16.0.1",
 
78
  "@remix-run/cloudflare-pages": "^2.15.2",
79
  "@remix-run/node": "^2.15.2",
80
  "@remix-run/react": "^2.15.2",
81
+ "@types/react-beautiful-dnd": "^13.1.8",
82
  "@uiw/codemirror-theme-vscode": "^4.23.6",
83
  "@unocss/reset": "^0.61.9",
84
  "@webcontainer/api": "1.3.0-internal.10",
 
107
  "ollama-ai-provider": "^0.15.2",
108
  "path-browserify": "^1.0.1",
109
  "react": "^18.3.1",
110
+ "react-beautiful-dnd": "^13.1.1",
111
  "react-chartjs-2": "^5.3.0",
112
  "react-dnd": "^16.0.1",
113
  "react-dnd-html5-backend": "^16.0.1",
pnpm-lock.yaml CHANGED
@@ -152,6 +152,9 @@ importers:
152
  '@remix-run/react':
153
  specifier: ^2.15.2
154
 
 
 
155
  '@uiw/codemirror-theme-vscode':
156
  specifier: ^4.23.6
157
  version: 4.23.7(@codemirror/[email protected])(@codemirror/[email protected])(@codemirror/[email protected])
@@ -236,12 +239,15 @@ importers:
236
  react:
237
  specifier: ^18.3.1
238
  version: 18.3.1
 
 
 
239
  react-chartjs-2:
240
  specifier: ^5.3.0
241
242
  react-dnd:
243
  specifier: ^16.0.1
244
- version: 16.0.1(@types/[email protected])(@types/[email protected])([email protected])
245
  react-dnd-html5-backend:
246
  specifier: ^16.0.1
247
  version: 16.0.1
@@ -2802,6 +2808,9 @@ packages:
2802
  '@types/[email protected]':
2803
  resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
2804
 
 
 
 
2805
  '@types/[email protected]':
2806
  resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==}
2807
 
@@ -2829,11 +2838,17 @@ packages:
2829
  '@types/[email protected]':
2830
  resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==}
2831
 
 
 
 
2832
  '@types/[email protected]':
2833
  resolution: {integrity: sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==}
2834
  peerDependencies:
2835
  '@types/react': ^18.0.0
2836
 
 
 
 
2837
  '@types/[email protected]':
2838
  resolution: {integrity: sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==}
2839
 
@@ -3482,6 +3497,9 @@ packages:
3482
  resolution: {integrity: sha512-r4ESw/IlusD17lgQi1O20Fa3qNnsckR126TdUuBgAu7GBYSIPvdNyONd3Zrxh0xCwA4+6w/TDArBPsMvhur+KQ==}
3483
  engines: {node: '>= 0.10'}
3484
 
 
 
 
3485
3486
  resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==}
3487
  engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
@@ -4623,6 +4641,9 @@ packages:
4623
  resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
4624
  engines: {node: '>= 0.6'}
4625
 
 
 
 
4626
4627
  resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==}
4628
 
@@ -5029,6 +5050,10 @@ packages:
5029
  resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==}
5030
  engines: {node: '>=8'}
5031
 
 
 
 
 
5032
5033
  resolution: {integrity: sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==}
5034
  engines: {node: '>= 0.4'}
@@ -5330,6 +5355,9 @@ packages:
5330
  resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==}
5331
  engines: {node: '>=10'}
5332
 
 
 
 
5333
5334
  resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==}
5335
 
@@ -5371,6 +5399,9 @@ packages:
5371
5372
  resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
5373
 
 
 
 
5374
5375
  resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
5376
 
@@ -5385,6 +5416,13 @@ packages:
5385
  resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==}
5386
  engines: {node: '>= 0.8'}
5387
 
 
 
 
 
 
 
 
5388
5389
  resolution: {integrity: sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==}
5390
  peerDependencies:
@@ -5428,12 +5466,27 @@ packages:
5428
5429
  resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
5430
 
 
 
 
5431
5432
  resolution: {integrity: sha512-Yk7Z94dbgYTOrdk41Z74GoKA7rThnsbbqBTRYuxoe08qvfQ9tJVhmAKw6BJS/ZORG7kTy/s1QvYzSuaoBA1qfw==}
5433
  peerDependencies:
5434
  '@types/react': '>=18'
5435
  react: '>=18'
5436
 
 
 
 
 
 
 
 
 
 
 
 
 
5437
5438
  resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==}
5439
  engines: {node: '>=0.10.0'}
@@ -6090,6 +6143,9 @@ packages:
6090
  resolution: {integrity: sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==}
6091
  engines: {node: '>=0.6.0'}
6092
 
 
 
 
6093
6094
  resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
6095
 
@@ -6306,6 +6362,11 @@ packages:
6306
  '@types/react':
6307
  optional: true
6308
 
 
 
 
 
 
6309
6310
  resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==}
6311
  engines: {node: '>=10'}
@@ -9306,6 +9367,11 @@ snapshots:
9306
  dependencies:
9307
  '@types/unist': 3.0.3
9308
 
 
 
 
 
 
9309
  '@types/[email protected]': {}
9310
 
9311
  '@types/[email protected]': {}
@@ -9330,10 +9396,21 @@ snapshots:
9330
 
9331
  '@types/[email protected]': {}
9332
 
 
 
 
 
9333
  '@types/[email protected](@types/[email protected])':
9334
  dependencies:
9335
  '@types/react': 18.3.18
9336
 
 
 
 
 
 
 
 
9337
  '@types/[email protected]':
9338
  dependencies:
9339
  '@types/prop-types': 15.7.14
@@ -10171,6 +10248,10 @@ snapshots:
10171
  randombytes: 2.1.0
10172
  randomfill: 1.0.4
10173
 
 
 
 
 
10174
10175
  dependencies:
10176
  mdn-data: 2.0.30
@@ -11633,6 +11714,8 @@ snapshots:
11633
 
11634
11635
 
 
 
11636
11637
 
11638
@@ -12275,6 +12358,8 @@ snapshots:
12275
  dependencies:
12276
  path-key: 3.1.1
12277
 
 
 
12278
12279
 
12280
@@ -12566,6 +12651,12 @@ snapshots:
12566
  err-code: 2.0.3
12567
  retry: 0.12.0
12568
 
 
 
 
 
 
 
12569
12570
 
12571
@@ -12614,6 +12705,8 @@ snapshots:
12614
 
12615
12616
 
 
 
12617
12618
  dependencies:
12619
  safe-buffer: 5.2.1
@@ -12632,6 +12725,20 @@ snapshots:
12632
  iconv-lite: 0.4.24
12633
  unpipe: 1.0.0
12634
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12635
12636
  dependencies:
12637
  chart.js: 4.4.7
@@ -12641,7 +12748,7 @@ snapshots:
12641
  dependencies:
12642
  dnd-core: 16.0.1
12643
 
12644
12645
  dependencies:
12646
  '@react-dnd/invariant': 4.0.2
12647
  '@react-dnd/shallowequal': 4.0.2
@@ -12650,6 +12757,7 @@ snapshots:
12650
  hoist-non-react-statics: 3.3.2
12651
  react: 18.3.1
12652
  optionalDependencies:
 
12653
  '@types/node': 22.10.10
12654
  '@types/react': 18.3.18
12655
 
@@ -12670,6 +12778,8 @@ snapshots:
12670
 
12671
12672
 
 
 
12673
12674
  dependencies:
12675
  '@types/hast': 3.0.4
@@ -12687,6 +12797,18 @@ snapshots:
12687
  transitivePeerDependencies:
12688
  - supports-color
12689
 
 
 
 
 
 
 
 
 
 
 
 
 
12690
12691
 
12692
@@ -13414,6 +13536,8 @@ snapshots:
13414
  dependencies:
13415
  setimmediate: 1.0.5
13416
 
 
 
13417
13418
 
13419
@@ -13656,6 +13780,10 @@ snapshots:
13656
  optionalDependencies:
13657
  '@types/react': 18.3.18
13658
 
 
 
 
 
13659
13660
  dependencies:
13661
  detect-node-es: 1.1.0
 
152
  '@remix-run/react':
153
  specifier: ^2.15.2
154
155
+ '@types/react-beautiful-dnd':
156
+ specifier: ^13.1.8
157
+ version: 13.1.8
158
  '@uiw/codemirror-theme-vscode':
159
  specifier: ^4.23.6
160
  version: 4.23.7(@codemirror/[email protected])(@codemirror/[email protected])(@codemirror/[email protected])
 
239
  react:
240
  specifier: ^18.3.1
241
  version: 18.3.1
242
+ react-beautiful-dnd:
243
+ specifier: ^13.1.1
244
245
  react-chartjs-2:
246
  specifier: ^5.3.0
247
248
  react-dnd:
249
  specifier: ^16.0.1
250
+ version: 16.0.1(@types/[email protected])(@types/[email protected])(@types/[email protected])([email protected])
251
  react-dnd-html5-backend:
252
  specifier: ^16.0.1
253
  version: 16.0.1
 
2808
  '@types/[email protected]':
2809
  resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
2810
 
2811
+ '@types/[email protected]':
2812
+ resolution: {integrity: sha512-lPByRJUer/iN/xa4qpyL0qmL11DqNW81iU/IG1S3uvRUq4oKagz8VCxZjiWkumgt66YT3vOdDgZ0o32sGKtCEw==}
2813
+
2814
  '@types/[email protected]':
2815
  resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==}
2816
 
 
2838
  '@types/[email protected]':
2839
  resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==}
2840
 
2841
+ '@types/[email protected]':
2842
+ resolution: {integrity: sha512-E3TyFsro9pQuK4r8S/OL6G99eq7p8v29sX0PM7oT8Z+PJfZvSQTx4zTQbUJ+QZXioAF0e7TGBEcA1XhYhCweyQ==}
2843
+
2844
  '@types/[email protected]':
2845
  resolution: {integrity: sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==}
2846
  peerDependencies:
2847
  '@types/react': ^18.0.0
2848
 
2849
+ '@types/[email protected]':
2850
+ resolution: {integrity: sha512-GdFaVjEbYv4Fthm2ZLvj1VSCedV7TqE5y1kNwnjSdBOTXuRSgowux6J8TAct15T3CKBr63UMk+2CO7ilRhyrAQ==}
2851
+
2852
  '@types/[email protected]':
2853
  resolution: {integrity: sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==}
2854
 
 
3497
  resolution: {integrity: sha512-r4ESw/IlusD17lgQi1O20Fa3qNnsckR126TdUuBgAu7GBYSIPvdNyONd3Zrxh0xCwA4+6w/TDArBPsMvhur+KQ==}
3498
  engines: {node: '>= 0.10'}
3499
 
3500
3501
+ resolution: {integrity: sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==}
3502
+
3503
3504
  resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==}
3505
  engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
 
4641
  resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
4642
  engines: {node: '>= 0.6'}
4643
 
4644
4645
+ resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==}
4646
+
4647
4648
  resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==}
4649
 
 
5050
  resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==}
5051
  engines: {node: '>=8'}
5052
 
5053
5054
+ resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
5055
+ engines: {node: '>=0.10.0'}
5056
+
5057
5058
  resolution: {integrity: sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==}
5059
  engines: {node: '>= 0.4'}
 
5355
  resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==}
5356
  engines: {node: '>=10'}
5357
 
5358
5359
+ resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
5360
+
5361
5362
  resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==}
5363
 
 
5399
5400
  resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
5401
 
5402
5403
+ resolution: {integrity: sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==}
5404
+
5405
5406
  resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
5407
 
 
5416
  resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==}
5417
  engines: {node: '>= 0.8'}
5418
 
5419
5420
+ resolution: {integrity: sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==}
5421
+ deprecated: 'react-beautiful-dnd is now deprecated. Context and options: https://github.com/atlassian/react-beautiful-dnd/issues/2672'
5422
+ peerDependencies:
5423
+ react: ^16.8.5 || ^17.0.0 || ^18.0.0
5424
+ react-dom: ^16.8.5 || ^17.0.0 || ^18.0.0
5425
+
5426
5427
  resolution: {integrity: sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==}
5428
  peerDependencies:
 
5466
5467
  resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
5468
 
5469
5470
+ resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
5471
+
5472
5473
  resolution: {integrity: sha512-Yk7Z94dbgYTOrdk41Z74GoKA7rThnsbbqBTRYuxoe08qvfQ9tJVhmAKw6BJS/ZORG7kTy/s1QvYzSuaoBA1qfw==}
5474
  peerDependencies:
5475
  '@types/react': '>=18'
5476
  react: '>=18'
5477
 
5478
5479
+ resolution: {integrity: sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==}
5480
+ peerDependencies:
5481
+ react: ^16.8.3 || ^17 || ^18
5482
+ react-dom: '*'
5483
+ react-native: '*'
5484
+ peerDependenciesMeta:
5485
+ react-dom:
5486
+ optional: true
5487
+ react-native:
5488
+ optional: true
5489
+
5490
5491
  resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==}
5492
  engines: {node: '>=0.10.0'}
 
6143
  resolution: {integrity: sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==}
6144
  engines: {node: '>=0.6.0'}
6145
 
6146
6147
+ resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
6148
+
6149
6150
  resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
6151
 
 
6362
  '@types/react':
6363
  optional: true
6364
 
6365
6366
+ resolution: {integrity: sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==}
6367
+ peerDependencies:
6368
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0
6369
+
6370
6371
  resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==}
6372
  engines: {node: '>=10'}
 
9367
  dependencies:
9368
  '@types/unist': 3.0.3
9369
 
9370
+ '@types/[email protected]':
9371
+ dependencies:
9372
+ '@types/react': 18.3.18
9373
+ hoist-non-react-statics: 3.3.2
9374
+
9375
  '@types/[email protected]': {}
9376
 
9377
  '@types/[email protected]': {}
 
9396
 
9397
  '@types/[email protected]': {}
9398
 
9399
+ '@types/[email protected]':
9400
+ dependencies:
9401
+ '@types/react': 18.3.18
9402
+
9403
  '@types/[email protected](@types/[email protected])':
9404
  dependencies:
9405
  '@types/react': 18.3.18
9406
 
9407
+ '@types/[email protected]':
9408
+ dependencies:
9409
+ '@types/hoist-non-react-statics': 3.3.6
9410
+ '@types/react': 18.3.18
9411
+ hoist-non-react-statics: 3.3.2
9412
+ redux: 4.2.1
9413
+
9414
  '@types/[email protected]':
9415
  dependencies:
9416
  '@types/prop-types': 15.7.14
 
10248
  randombytes: 2.1.0
10249
  randomfill: 1.0.4
10250
 
10251
10252
+ dependencies:
10253
+ tiny-invariant: 1.3.3
10254
+
10255
10256
  dependencies:
10257
  mdn-data: 2.0.30
 
11714
 
11715
11716
 
11717
11718
+
11719
11720
 
11721
 
12358
  dependencies:
12359
  path-key: 3.1.1
12360
 
12361
12362
+
12363
12364
 
12365
 
12651
  err-code: 2.0.3
12652
  retry: 0.12.0
12653
 
12654
12655
+ dependencies:
12656
+ loose-envify: 1.4.0
12657
+ object-assign: 4.1.1
12658
+ react-is: 16.13.1
12659
+
12660
12661
 
12662
 
12705
 
12706
12707
 
12708
12709
+
12710
12711
  dependencies:
12712
  safe-buffer: 5.2.1
 
12725
  iconv-lite: 0.4.24
12726
  unpipe: 1.0.0
12727
 
12728
12729
+ dependencies:
12730
+ '@babel/runtime': 7.26.7
12731
+ css-box-model: 1.2.1
12732
+ memoize-one: 5.2.1
12733
+ raf-schd: 4.0.3
12734
+ react: 18.3.1
12735
+ react-dom: 18.3.1([email protected])
12736
12737
+ redux: 4.2.1
12738
+ use-memo-one: 1.1.3([email protected])
12739
+ transitivePeerDependencies:
12740
+ - react-native
12741
+
12742
12743
  dependencies:
12744
  chart.js: 4.4.7
 
12748
  dependencies:
12749
  dnd-core: 16.0.1
12750
 
12751
12752
  dependencies:
12753
  '@react-dnd/invariant': 4.0.2
12754
  '@react-dnd/shallowequal': 4.0.2
 
12757
  hoist-non-react-statics: 3.3.2
12758
  react: 18.3.1
12759
  optionalDependencies:
12760
+ '@types/hoist-non-react-statics': 3.3.6
12761
  '@types/node': 22.10.10
12762
  '@types/react': 18.3.18
12763
 
 
12778
 
12779
12780
 
12781
12782
+
12783
12784
  dependencies:
12785
  '@types/hast': 3.0.4
 
12797
  transitivePeerDependencies:
12798
  - supports-color
12799
 
12800
12801
+ dependencies:
12802
+ '@babel/runtime': 7.26.7
12803
+ '@types/react-redux': 7.1.34
12804
+ hoist-non-react-statics: 3.3.2
12805
+ loose-envify: 1.4.0
12806
+ prop-types: 15.8.1
12807
+ react: 18.3.1
12808
+ react-is: 17.0.2
12809
+ optionalDependencies:
12810
+ react-dom: 18.3.1([email protected])
12811
+
12812
12813
 
12814
 
13536
  dependencies:
13537
  setimmediate: 1.0.5
13538
 
13539
13540
+
13541
13542
 
13543
 
13780
  optionalDependencies:
13781
  '@types/react': 18.3.18
13782
 
13783
13784
+ dependencies:
13785
+ react: 18.3.1
13786
+
13787
13788
  dependencies:
13789
  detect-node-es: 1.1.0