Stijnus
commited on
Commit
·
436a8e5
1
Parent(s):
9230ef3
ui refactor
Browse files- app/components/settings/SettingsWindow.tsx +0 -300
- app/components/settings/data/DataTab.tsx +13 -3
- app/components/settings/debug/DebugTab.tsx +775 -633
- app/components/settings/developer/DeveloperWindow.tsx +378 -0
- app/components/settings/developer/TabManagement.tsx +315 -0
- app/components/settings/event-logs/EventLogsTab.tsx +425 -307
- app/components/settings/features/FeaturesTab.tsx +27 -56
- app/components/settings/notifications/NotificationsTab.tsx +143 -0
- app/components/settings/profile/ProfileTab.tsx +130 -261
- app/components/settings/providers/ProvidersTab.tsx +26 -20
- app/components/settings/settings.styles.ts +11 -5
- app/components/settings/settings.types.ts +62 -3
- app/components/settings/settings/SettingsTab.tsx +215 -0
- app/components/settings/shared/DraggableTabList.tsx +158 -0
- app/components/settings/shared/TabTile.tsx +230 -0
- app/components/settings/update/UpdateTab.tsx +217 -0
- app/components/settings/user/ProfileHeader.tsx +25 -0
- app/components/settings/user/UsersWindow.tsx +385 -0
- app/components/sidebar/Menu.client.tsx +2 -2
- app/components/ui/Dropdown.tsx +63 -0
- app/components/workbench/Preview.tsx +89 -71
- app/lib/api/connection.ts +18 -0
- app/lib/api/debug.ts +65 -0
- app/lib/api/features.ts +35 -0
- app/lib/api/notifications.ts +40 -0
- app/lib/api/updates.ts +28 -0
- app/lib/hooks/index.ts +5 -0
- app/lib/hooks/useConnectionStatus.ts +61 -0
- app/lib/hooks/useDebugStatus.ts +89 -0
- app/lib/hooks/useFeatures.ts +72 -0
- app/lib/hooks/useNotifications.ts +77 -0
- app/lib/hooks/{useSettings.tsx → useSettings.ts} +117 -124
- app/lib/hooks/useUpdateCheck.ts +58 -0
- app/lib/modules/llm/providers/github.ts +2 -1
- app/lib/stores/logs.ts +72 -11
- app/lib/stores/settings.ts +51 -1
- app/root.tsx +4 -2
- app/utils/localStorage.ts +17 -0
- package.json +2 -0
- pnpm-lock.yaml +83 -0
app/components/settings/SettingsWindow.tsx
DELETED
@@ -1,300 +0,0 @@
|
|
1 |
-
import * as RadixDialog from '@radix-ui/react-dialog';
|
2 |
-
import { motion, AnimatePresence } from 'framer-motion';
|
3 |
-
import { useState } from 'react';
|
4 |
-
import { classNames } from '~/utils/classNames';
|
5 |
-
import { DialogTitle } from '~/components/ui/Dialog';
|
6 |
-
import type { SettingCategory, TabType } from './settings.types';
|
7 |
-
import { categoryLabels, categoryIcons } from './settings.types';
|
8 |
-
import ProfileTab from './profile/ProfileTab';
|
9 |
-
import ProvidersTab from './providers/ProvidersTab';
|
10 |
-
import { useSettings } from '~/lib/hooks/useSettings';
|
11 |
-
import FeaturesTab from './features/FeaturesTab';
|
12 |
-
import DebugTab from './debug/DebugTab';
|
13 |
-
import EventLogsTab from './event-logs/EventLogsTab';
|
14 |
-
import ConnectionsTab from './connections/ConnectionsTab';
|
15 |
-
import DataTab from './data/DataTab';
|
16 |
-
|
17 |
-
interface SettingsProps {
|
18 |
-
open: boolean;
|
19 |
-
onClose: () => void;
|
20 |
-
}
|
21 |
-
|
22 |
-
export const SettingsWindow = ({ open, onClose }: SettingsProps) => {
|
23 |
-
const { debug, eventLogs } = useSettings();
|
24 |
-
const [searchQuery, setSearchQuery] = useState('');
|
25 |
-
const [activeTab, setActiveTab] = useState<TabType | null>(null);
|
26 |
-
|
27 |
-
const settingItems = [
|
28 |
-
{
|
29 |
-
id: 'profile' as const,
|
30 |
-
label: 'Profile Settings',
|
31 |
-
icon: 'i-ph:user-circle',
|
32 |
-
category: 'profile' as const,
|
33 |
-
description: 'Manage your personal information and preferences',
|
34 |
-
component: () => <ProfileTab />,
|
35 |
-
keywords: ['profile', 'account', 'avatar', 'email', 'name', 'theme', 'notifications'],
|
36 |
-
},
|
37 |
-
|
38 |
-
{
|
39 |
-
id: 'data' as const,
|
40 |
-
label: 'Data Management',
|
41 |
-
icon: 'i-ph:database',
|
42 |
-
category: 'file_sharing' as const,
|
43 |
-
description: 'Manage your chat history and application data',
|
44 |
-
component: () => <DataTab />,
|
45 |
-
keywords: ['data', 'export', 'import', 'backup', 'delete'],
|
46 |
-
},
|
47 |
-
|
48 |
-
{
|
49 |
-
id: 'providers' as const,
|
50 |
-
label: 'Providers',
|
51 |
-
icon: 'i-ph:key',
|
52 |
-
category: 'file_sharing' as const,
|
53 |
-
description: 'Configure AI providers and API keys',
|
54 |
-
component: () => <ProvidersTab />,
|
55 |
-
keywords: ['api', 'keys', 'providers', 'configuration'],
|
56 |
-
},
|
57 |
-
|
58 |
-
{
|
59 |
-
id: 'connection' as const,
|
60 |
-
label: 'Connection',
|
61 |
-
icon: 'i-ph:link',
|
62 |
-
category: 'connectivity' as const,
|
63 |
-
description: 'Manage network and connection settings',
|
64 |
-
component: () => <ConnectionsTab />,
|
65 |
-
keywords: ['network', 'connection', 'proxy', 'ssl'],
|
66 |
-
},
|
67 |
-
|
68 |
-
{
|
69 |
-
id: 'features' as const,
|
70 |
-
label: 'Features',
|
71 |
-
icon: 'i-ph:star',
|
72 |
-
category: 'system' as const,
|
73 |
-
description: 'Configure application features and preferences',
|
74 |
-
component: () => <FeaturesTab />,
|
75 |
-
keywords: ['features', 'settings', 'options'],
|
76 |
-
},
|
77 |
-
] as const;
|
78 |
-
|
79 |
-
const debugItems = debug
|
80 |
-
? [
|
81 |
-
{
|
82 |
-
id: 'debug' as const,
|
83 |
-
label: 'Debug',
|
84 |
-
icon: 'i-ph:bug',
|
85 |
-
category: 'system' as const,
|
86 |
-
description: 'Advanced debugging tools and options',
|
87 |
-
component: () => <DebugTab />,
|
88 |
-
keywords: ['debug', 'logs', 'developer'],
|
89 |
-
},
|
90 |
-
]
|
91 |
-
: [];
|
92 |
-
|
93 |
-
const eventLogItems = eventLogs
|
94 |
-
? [
|
95 |
-
{
|
96 |
-
id: 'event-logs' as const,
|
97 |
-
label: 'Event Logs',
|
98 |
-
icon: 'i-ph:list-bullets',
|
99 |
-
category: 'system' as const,
|
100 |
-
description: 'View system events and application logs',
|
101 |
-
component: () => <EventLogsTab />,
|
102 |
-
keywords: ['logs', 'events', 'history'],
|
103 |
-
},
|
104 |
-
]
|
105 |
-
: [];
|
106 |
-
|
107 |
-
const allSettingItems = [...settingItems, ...debugItems, ...eventLogItems];
|
108 |
-
|
109 |
-
const filteredItems = allSettingItems.filter(
|
110 |
-
(item) =>
|
111 |
-
item.label.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
112 |
-
item.description?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
113 |
-
item.keywords?.some((keyword) => keyword.toLowerCase().includes(searchQuery.toLowerCase())),
|
114 |
-
);
|
115 |
-
|
116 |
-
const groupedItems = filteredItems.reduce(
|
117 |
-
(acc, item) => {
|
118 |
-
if (!acc[item.category]) {
|
119 |
-
acc[item.category] = allSettingItems.filter((i) => i.category === item.category);
|
120 |
-
}
|
121 |
-
|
122 |
-
return acc;
|
123 |
-
},
|
124 |
-
{} as Record<SettingCategory, typeof allSettingItems>,
|
125 |
-
);
|
126 |
-
|
127 |
-
const handleBackToDashboard = () => {
|
128 |
-
setActiveTab(null);
|
129 |
-
onClose();
|
130 |
-
};
|
131 |
-
|
132 |
-
const activeTabItem = allSettingItems.find((item) => item.id === activeTab);
|
133 |
-
|
134 |
-
return (
|
135 |
-
<RadixDialog.Root open={open}>
|
136 |
-
<RadixDialog.Portal>
|
137 |
-
<div className="fixed inset-0 flex items-center justify-center z-[9999]">
|
138 |
-
<RadixDialog.Overlay asChild>
|
139 |
-
<motion.div
|
140 |
-
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
|
141 |
-
initial={{ opacity: 0 }}
|
142 |
-
animate={{ opacity: 1 }}
|
143 |
-
exit={{ opacity: 0 }}
|
144 |
-
transition={{ duration: 0.2 }}
|
145 |
-
/>
|
146 |
-
</RadixDialog.Overlay>
|
147 |
-
<RadixDialog.Content aria-describedby={undefined} asChild>
|
148 |
-
<motion.div
|
149 |
-
className={classNames(
|
150 |
-
'relative',
|
151 |
-
'w-[1000px] max-h-[90vh] min-h-[700px]',
|
152 |
-
'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
|
153 |
-
'rounded-2xl overflow-hidden shadow-2xl',
|
154 |
-
'border border-[#E5E5E5] dark:border-[#1A1A1A]',
|
155 |
-
'overflow-y-auto scrollbar-thin scrollbar-thumb-gray-300 dark:scrollbar-thumb-gray-700 scrollbar-track-transparent',
|
156 |
-
)}
|
157 |
-
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
158 |
-
animate={{ opacity: 1, scale: 1, y: 0 }}
|
159 |
-
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
160 |
-
transition={{ duration: 0.2 }}
|
161 |
-
>
|
162 |
-
<AnimatePresence mode="wait">
|
163 |
-
{activeTab ? (
|
164 |
-
<motion.div
|
165 |
-
className="flex flex-col h-full"
|
166 |
-
initial={{ opacity: 0, y: 20 }}
|
167 |
-
animate={{ opacity: 1, y: 0 }}
|
168 |
-
exit={{ opacity: 0, y: -20 }}
|
169 |
-
transition={{ duration: 0.2 }}
|
170 |
-
>
|
171 |
-
<div className="flex items-center justify-between p-6 border-b border-[#E5E5E5] dark:border-[#1A1A1A] sticky top-0 bg-[#FAFAFA] dark:bg-[#0A0A0A] z-10">
|
172 |
-
<div className="flex items-center">
|
173 |
-
<button
|
174 |
-
onClick={() => setActiveTab(null)}
|
175 |
-
className="flex items-center gap-2 px-4 py-2 rounded-lg text-sm bg-[#F5F5F5] dark:bg-[#1A1A1A] text-[#666666] dark:text-[#999999] hover:text-[#333333] dark:hover:text-white"
|
176 |
-
>
|
177 |
-
<div className="i-ph:arrow-left w-4 h-4" />
|
178 |
-
Back to Settings
|
179 |
-
</button>
|
180 |
-
|
181 |
-
<div className="text-bolt-elements-textTertiary mx-6 select-none">|</div>
|
182 |
-
|
183 |
-
{activeTabItem && (
|
184 |
-
<div className="flex items-center gap-4">
|
185 |
-
<div className={classNames(activeTabItem.icon, 'w-6 h-6 text-purple-500')} />
|
186 |
-
<div>
|
187 |
-
<h2 className="text-lg font-medium text-bolt-elements-textPrimary">
|
188 |
-
{activeTabItem.label}
|
189 |
-
</h2>
|
190 |
-
<p className="text-sm text-bolt-elements-textSecondary">{activeTabItem.description}</p>
|
191 |
-
</div>
|
192 |
-
</div>
|
193 |
-
)}
|
194 |
-
</div>
|
195 |
-
|
196 |
-
<button
|
197 |
-
onClick={handleBackToDashboard}
|
198 |
-
className="flex items-center gap-2 px-4 py-2 rounded-lg text-sm bg-[#F5F5F5] dark:bg-[#1A1A1A] text-[#666666] dark:text-[#999999] hover:text-[#333333] dark:hover:text-white"
|
199 |
-
>
|
200 |
-
<div className="i-ph:house w-4 h-4" />
|
201 |
-
Back to Bolt DIY
|
202 |
-
</button>
|
203 |
-
</div>
|
204 |
-
<div className="flex-1 p-6 overflow-y-auto scrollbar-thin scrollbar-thumb-gray-300 dark:scrollbar-thumb-gray-700 scrollbar-track-transparent">
|
205 |
-
{allSettingItems.find((item) => item.id === activeTab)?.component()}
|
206 |
-
</div>
|
207 |
-
</motion.div>
|
208 |
-
) : (
|
209 |
-
<motion.div
|
210 |
-
className="flex flex-col h-full"
|
211 |
-
initial={{ opacity: 0, y: 20 }}
|
212 |
-
animate={{ opacity: 1, y: 0 }}
|
213 |
-
exit={{ opacity: 0, y: -20 }}
|
214 |
-
transition={{ duration: 0.2 }}
|
215 |
-
>
|
216 |
-
<div className="flex items-center justify-between p-6 border-b border-[#E5E5E5] dark:border-[#1A1A1A] sticky top-0 bg-[#FAFAFA] dark:bg-[#0A0A0A] z-10">
|
217 |
-
<div className="flex items-center gap-3">
|
218 |
-
<div className="i-ph:lightning-fill w-5 h-5 text-purple-500" />
|
219 |
-
<DialogTitle className="text-lg font-medium text-bolt-elements-textPrimary">
|
220 |
-
Bolt Control Panel
|
221 |
-
</DialogTitle>
|
222 |
-
</div>
|
223 |
-
<div className="flex items-center gap-4">
|
224 |
-
<div className="relative w-[320px]">
|
225 |
-
<input
|
226 |
-
type="text"
|
227 |
-
placeholder="Search settings..."
|
228 |
-
value={searchQuery}
|
229 |
-
onChange={(e) => setSearchQuery(e.target.value)}
|
230 |
-
className={classNames(
|
231 |
-
'w-full h-10 pl-10 pr-4 rounded-lg text-sm',
|
232 |
-
'bg-[#F8F8F8] dark:bg-[#1A1A1A]',
|
233 |
-
'border border-[#E5E5E5] dark:border-[#333333]',
|
234 |
-
'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
|
235 |
-
'focus:outline-none focus:ring-1 focus:ring-purple-500 transition-all',
|
236 |
-
)}
|
237 |
-
/>
|
238 |
-
<div className="absolute left-3.5 top-1/2 -translate-y-1/2">
|
239 |
-
<div className="i-ph:magnifying-glass w-4 h-4 text-bolt-elements-textTertiary" />
|
240 |
-
</div>
|
241 |
-
</div>
|
242 |
-
<button
|
243 |
-
onClick={handleBackToDashboard}
|
244 |
-
className="flex items-center gap-2 px-4 py-2 rounded-lg text-sm bg-[#F5F5F5] dark:bg-[#1A1A1A] text-[#666666] dark:text-[#999999] hover:text-[#333333] dark:hover:text-white"
|
245 |
-
>
|
246 |
-
<div className="i-ph:house w-4 h-4" />
|
247 |
-
Back to Bolt DIY
|
248 |
-
</button>
|
249 |
-
</div>
|
250 |
-
</div>
|
251 |
-
|
252 |
-
<div className="flex-1 p-6 overflow-y-auto scrollbar-thin scrollbar-thumb-gray-300 dark:scrollbar-thumb-gray-700 scrollbar-track-transparent">
|
253 |
-
<div className="space-y-8">
|
254 |
-
{(Object.keys(groupedItems) as SettingCategory[]).map((category) => (
|
255 |
-
<div key={category} className="space-y-4">
|
256 |
-
<div className="flex items-center gap-3">
|
257 |
-
<div className={classNames(categoryIcons[category], 'w-5 h-5 text-purple-500')} />
|
258 |
-
<h2 className="text-base font-medium text-bolt-elements-textPrimary">
|
259 |
-
{categoryLabels[category]}
|
260 |
-
</h2>
|
261 |
-
</div>
|
262 |
-
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
263 |
-
{groupedItems[category].map((item) => (
|
264 |
-
<button
|
265 |
-
key={item.id}
|
266 |
-
onClick={() => setActiveTab(item.id)}
|
267 |
-
className={classNames(
|
268 |
-
'flex flex-col gap-2 p-4 rounded-lg text-left',
|
269 |
-
'bg-white dark:bg-[#0A0A0A]',
|
270 |
-
'border border-[#E5E5E5] dark:border-[#1A1A1A]',
|
271 |
-
'hover:bg-[#F8F8F8] dark:hover:bg-[#1A1A1A]',
|
272 |
-
'transition-all duration-200',
|
273 |
-
)}
|
274 |
-
>
|
275 |
-
<div className="flex items-center gap-3">
|
276 |
-
<div className={classNames(item.icon, 'w-5 h-5 text-purple-500')} />
|
277 |
-
<span className="text-sm font-medium text-bolt-elements-textPrimary">
|
278 |
-
{item.label}
|
279 |
-
</span>
|
280 |
-
</div>
|
281 |
-
{item.description && (
|
282 |
-
<p className="text-sm text-bolt-elements-textSecondary">{item.description}</p>
|
283 |
-
)}
|
284 |
-
</button>
|
285 |
-
))}
|
286 |
-
</div>
|
287 |
-
</div>
|
288 |
-
))}
|
289 |
-
</div>
|
290 |
-
</div>
|
291 |
-
</motion.div>
|
292 |
-
)}
|
293 |
-
</AnimatePresence>
|
294 |
-
</motion.div>
|
295 |
-
</RadixDialog.Content>
|
296 |
-
</div>
|
297 |
-
</RadixDialog.Portal>
|
298 |
-
</RadixDialog.Root>
|
299 |
-
);
|
300 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/components/settings/data/DataTab.tsx
CHANGED
@@ -185,11 +185,15 @@ export default function DataTab() {
|
|
185 |
localStorage.removeItem('bolt_settings');
|
186 |
localStorage.removeItem('bolt_chat_history');
|
187 |
|
188 |
-
//
|
|
|
|
|
|
|
189 |
window.location.reload();
|
190 |
toast.success('Settings reset successfully');
|
191 |
} catch (error) {
|
192 |
console.error('Reset error:', error);
|
|
|
193 |
toast.error('Failed to reset settings');
|
194 |
} finally {
|
195 |
setIsResetting(false);
|
@@ -202,9 +206,15 @@ export default function DataTab() {
|
|
202 |
try {
|
203 |
// Clear chat history
|
204 |
localStorage.removeItem('bolt_chat_history');
|
|
|
|
|
|
|
|
|
|
|
205 |
toast.success('Chat history deleted successfully');
|
206 |
} catch (error) {
|
207 |
console.error('Delete error:', error);
|
|
|
208 |
toast.error('Failed to delete chat history');
|
209 |
} finally {
|
210 |
setIsDeleting(false);
|
@@ -216,7 +226,7 @@ export default function DataTab() {
|
|
216 |
<input ref={fileInputRef} type="file" accept=".json" onChange={handleImportSettings} className="hidden" />
|
217 |
{/* Reset Settings Dialog */}
|
218 |
<DialogRoot open={showResetInlineConfirm} onOpenChange={setShowResetInlineConfirm}>
|
219 |
-
<Dialog showCloseButton={false}>
|
220 |
<div className="p-6">
|
221 |
<div className="flex items-center gap-3">
|
222 |
<div className="i-ph:warning-circle-fill w-5 h-5 text-yellow-500" />
|
@@ -252,7 +262,7 @@ export default function DataTab() {
|
|
252 |
|
253 |
{/* Delete Confirmation Dialog */}
|
254 |
<DialogRoot open={showDeleteInlineConfirm} onOpenChange={setShowDeleteInlineConfirm}>
|
255 |
-
<Dialog showCloseButton={false}>
|
256 |
<div className="p-6">
|
257 |
<div className="flex items-center gap-3">
|
258 |
<div className="i-ph:warning-circle-fill w-5 h-5 text-red-500" />
|
|
|
185 |
localStorage.removeItem('bolt_settings');
|
186 |
localStorage.removeItem('bolt_chat_history');
|
187 |
|
188 |
+
// Close the dialog first
|
189 |
+
setShowResetInlineConfirm(false);
|
190 |
+
|
191 |
+
// Then reload and show success message
|
192 |
window.location.reload();
|
193 |
toast.success('Settings reset successfully');
|
194 |
} catch (error) {
|
195 |
console.error('Reset error:', error);
|
196 |
+
setShowResetInlineConfirm(false);
|
197 |
toast.error('Failed to reset settings');
|
198 |
} finally {
|
199 |
setIsResetting(false);
|
|
|
206 |
try {
|
207 |
// Clear chat history
|
208 |
localStorage.removeItem('bolt_chat_history');
|
209 |
+
|
210 |
+
// Close the dialog first
|
211 |
+
setShowDeleteInlineConfirm(false);
|
212 |
+
|
213 |
+
// Then show the success message
|
214 |
toast.success('Chat history deleted successfully');
|
215 |
} catch (error) {
|
216 |
console.error('Delete error:', error);
|
217 |
+
setShowDeleteInlineConfirm(false);
|
218 |
toast.error('Failed to delete chat history');
|
219 |
} finally {
|
220 |
setIsDeleting(false);
|
|
|
226 |
<input ref={fileInputRef} type="file" accept=".json" onChange={handleImportSettings} className="hidden" />
|
227 |
{/* Reset Settings Dialog */}
|
228 |
<DialogRoot open={showResetInlineConfirm} onOpenChange={setShowResetInlineConfirm}>
|
229 |
+
<Dialog showCloseButton={false} className="z-[1000]">
|
230 |
<div className="p-6">
|
231 |
<div className="flex items-center gap-3">
|
232 |
<div className="i-ph:warning-circle-fill w-5 h-5 text-yellow-500" />
|
|
|
262 |
|
263 |
{/* Delete Confirmation Dialog */}
|
264 |
<DialogRoot open={showDeleteInlineConfirm} onOpenChange={setShowDeleteInlineConfirm}>
|
265 |
+
<Dialog showCloseButton={false} className="z-[1000]">
|
266 |
<div className="p-6">
|
267 |
<div className="flex items-center gap-3">
|
268 |
<div className="i-ph:warning-circle-fill w-5 h-5 text-red-500" />
|
app/components/settings/debug/DebugTab.tsx
CHANGED
@@ -1,727 +1,869 @@
|
|
1 |
-
import React, {
|
2 |
-
import { useSettings } from '~/lib/hooks/useSettings';
|
3 |
import { toast } from 'react-toastify';
|
4 |
-
import { providerBaseUrlEnvKeys } from '~/utils/constants';
|
5 |
-
import { motion } from 'framer-motion';
|
6 |
import { classNames } from '~/utils/classNames';
|
7 |
-
import {
|
8 |
|
9 |
interface ProviderStatus {
|
|
|
10 |
name: string;
|
11 |
-
|
12 |
-
isLocal: boolean;
|
13 |
-
isRunning: boolean | null;
|
14 |
error?: string;
|
15 |
-
lastChecked: Date;
|
16 |
-
responseTime?: number;
|
17 |
-
url: string | null;
|
18 |
}
|
19 |
|
20 |
interface SystemInfo {
|
21 |
os: string;
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
41 |
};
|
42 |
}
|
43 |
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
};
|
53 |
-
|
54 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
55 |
|
56 |
-
|
57 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
58 |
|
59 |
-
const
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
|
67 |
-
|
68 |
-
|
69 |
-
);
|
70 |
-
const packageJson: { version: string } = await packageJsonResp.json();
|
71 |
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
console.log('Failed to fetch local commit info:', error);
|
78 |
-
throw new Error('Failed to fetch local commit info');
|
79 |
-
}
|
80 |
-
},
|
81 |
-
};
|
82 |
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
return '0 Bytes';
|
87 |
-
}
|
88 |
|
89 |
-
|
90 |
-
|
91 |
-
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
92 |
|
93 |
-
|
94 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
95 |
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
}
|
106 |
-
|
107 |
-
} else {
|
108 |
-
browser = 'Chrome';
|
109 |
}
|
110 |
-
|
111 |
-
|
112 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
113 |
}
|
114 |
-
}
|
115 |
|
116 |
-
|
117 |
-
|
118 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
119 |
|
120 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
121 |
};
|
122 |
|
123 |
-
const
|
124 |
-
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
130 |
|
131 |
-
|
132 |
-
|
133 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
134 |
}
|
135 |
|
136 |
-
|
137 |
-
|
|
|
|
|
|
|
|
|
|
|
138 |
|
139 |
-
|
140 |
-
|
141 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
142 |
|
143 |
-
|
144 |
-
|
145 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
146 |
|
147 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
148 |
};
|
149 |
|
150 |
-
|
151 |
-
|
|
|
|
|
|
|
152 |
|
153 |
-
|
154 |
-
|
|
|
155 |
}
|
156 |
|
157 |
-
|
158 |
-
return 'Tablet';
|
159 |
-
}
|
160 |
-
|
161 |
-
return 'Desktop';
|
162 |
};
|
163 |
|
164 |
-
|
165 |
-
|
166 |
-
|
167 |
-
const memory = (performance as any).memory;
|
168 |
-
return `${formatBytes(memory.jsHeapSizeLimit)} (Used: ${formatBytes(memory.usedJSHeapSize)})`;
|
169 |
}
|
170 |
|
171 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
172 |
};
|
173 |
|
174 |
-
|
175 |
-
|
176 |
-
|
177 |
-
screen: `${window.screen.width}x${window.screen.height}`,
|
178 |
-
language: navigator.language,
|
179 |
-
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
180 |
-
memory: getMemoryInfo(),
|
181 |
-
cores: navigator.hardwareConcurrency || 0,
|
182 |
-
deviceType: getDeviceType(),
|
183 |
-
|
184 |
-
// Add new fields
|
185 |
-
colorDepth: `${window.screen.colorDepth}-bit`,
|
186 |
-
pixelRatio: window.devicePixelRatio,
|
187 |
-
online: navigator.onLine,
|
188 |
-
cookiesEnabled: navigator.cookieEnabled,
|
189 |
-
doNotTrack: navigator.doNotTrack === '1',
|
190 |
-
};
|
191 |
-
}
|
192 |
|
193 |
-
|
194 |
-
|
195 |
-
|
196 |
-
return {
|
197 |
-
name: providerName,
|
198 |
-
enabled: false,
|
199 |
-
isLocal: true,
|
200 |
-
isRunning: false,
|
201 |
-
error: 'No URL configured',
|
202 |
-
lastChecked: new Date(),
|
203 |
-
url: null,
|
204 |
-
};
|
205 |
-
}
|
206 |
|
207 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
208 |
|
209 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
210 |
|
211 |
-
|
212 |
-
|
213 |
-
|
214 |
-
|
215 |
-
|
|
|
|
|
|
|
|
|
216 |
|
217 |
-
|
218 |
-
|
219 |
|
220 |
-
|
221 |
-
|
222 |
-
headers: {
|
223 |
-
Accept: 'text/plain,application/json',
|
224 |
-
},
|
225 |
-
});
|
226 |
-
clearTimeout(timeoutId);
|
227 |
-
|
228 |
-
const text = await response.text();
|
229 |
-
console.log(`[Debug] Ollama root response:`, text);
|
230 |
-
|
231 |
-
if (text.includes('Ollama is running')) {
|
232 |
-
console.log(`[Debug] Ollama running confirmed via root endpoint`);
|
233 |
-
return {
|
234 |
-
name: providerName,
|
235 |
-
enabled: false,
|
236 |
-
isLocal: true,
|
237 |
-
isRunning: true,
|
238 |
-
lastChecked: new Date(),
|
239 |
-
responseTime: performance.now() - startTime,
|
240 |
-
url,
|
241 |
-
};
|
242 |
-
}
|
243 |
-
} catch (error) {
|
244 |
-
console.log(`[Debug] Ollama root check failed:`, error);
|
245 |
-
|
246 |
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
247 |
-
|
248 |
-
if (errorMessage.includes('aborted')) {
|
249 |
-
return {
|
250 |
-
name: providerName,
|
251 |
-
enabled: false,
|
252 |
-
isLocal: true,
|
253 |
-
isRunning: false,
|
254 |
-
error: 'Connection timeout',
|
255 |
-
lastChecked: new Date(),
|
256 |
-
responseTime: performance.now() - startTime,
|
257 |
-
url,
|
258 |
-
};
|
259 |
-
}
|
260 |
-
}
|
261 |
-
}
|
262 |
|
263 |
-
|
264 |
-
|
265 |
-
|
266 |
-
|
267 |
-
|
268 |
-
|
269 |
-
|
270 |
-
|
271 |
-
|
272 |
-
const controller = new AbortController();
|
273 |
-
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
274 |
-
|
275 |
-
const response = await fetch(checkUrl, {
|
276 |
-
signal: controller.signal,
|
277 |
-
headers: {
|
278 |
-
Accept: 'application/json',
|
279 |
-
},
|
280 |
-
});
|
281 |
-
clearTimeout(timeoutId);
|
282 |
-
|
283 |
-
const ok = response.ok;
|
284 |
-
console.log(`[Debug] Endpoint ${checkUrl} response:`, ok);
|
285 |
-
|
286 |
-
if (ok) {
|
287 |
-
try {
|
288 |
-
const data = await response.json();
|
289 |
-
console.log(`[Debug] Endpoint ${checkUrl} data:`, data);
|
290 |
-
} catch {
|
291 |
-
console.log(`[Debug] Could not parse JSON from ${checkUrl}`);
|
292 |
-
}
|
293 |
}
|
294 |
|
295 |
-
|
296 |
-
|
297 |
-
|
298 |
-
return false;
|
299 |
}
|
300 |
-
}),
|
301 |
-
);
|
302 |
-
|
303 |
-
const isRunning = results.some((result) => result);
|
304 |
-
console.log(`[Debug] Final status for ${providerName}:`, isRunning);
|
305 |
-
|
306 |
-
return {
|
307 |
-
name: providerName,
|
308 |
-
enabled: false,
|
309 |
-
isLocal: true,
|
310 |
-
isRunning,
|
311 |
-
lastChecked: new Date(),
|
312 |
-
responseTime: performance.now() - startTime,
|
313 |
-
url,
|
314 |
-
};
|
315 |
-
} catch (error) {
|
316 |
-
console.log(`[Debug] Provider check failed for ${providerName}:`, error);
|
317 |
-
return {
|
318 |
-
name: providerName,
|
319 |
-
enabled: false,
|
320 |
-
isLocal: true,
|
321 |
-
isRunning: false,
|
322 |
-
error: error instanceof Error ? error.message : 'Unknown error',
|
323 |
-
lastChecked: new Date(),
|
324 |
-
responseTime: performance.now() - startTime,
|
325 |
-
url,
|
326 |
-
};
|
327 |
-
}
|
328 |
-
};
|
329 |
|
330 |
-
|
331 |
-
|
332 |
-
const [activeProviders, setActiveProviders] = useState<ProviderStatus[]>([]);
|
333 |
-
const [updateMessage, setUpdateMessage] = useState<string>('');
|
334 |
-
const [systemInfo] = useState<SystemInfo>(getSystemInfo());
|
335 |
-
const [isCheckingUpdate, setIsCheckingUpdate] = useState(false);
|
336 |
-
|
337 |
-
const updateProviderStatuses = async () => {
|
338 |
-
if (!providers) {
|
339 |
-
return;
|
340 |
-
}
|
341 |
|
342 |
-
|
343 |
-
|
344 |
-
|
345 |
-
|
346 |
-
|
347 |
-
|
348 |
-
|
349 |
-
|
350 |
-
|
351 |
-
|
352 |
-
|
353 |
-
|
354 |
-
|
355 |
-
|
356 |
-
}
|
357 |
-
|
358 |
-
const url = settingsUrl || import.meta.env[envVarName] || null; // Ensure baseUrl is used
|
359 |
-
console.log(`[Debug] Using URL for ${provider.name}:`, url, `(from ${envVarName})`);
|
360 |
-
|
361 |
-
const status = await checkProviderStatus(url, provider.name);
|
362 |
-
|
363 |
-
return {
|
364 |
-
...status,
|
365 |
-
enabled: provider.settings.enabled ?? false,
|
366 |
-
};
|
367 |
-
}),
|
368 |
-
);
|
369 |
-
|
370 |
-
setActiveProviders(statuses);
|
371 |
} catch (error) {
|
372 |
-
|
|
|
|
|
|
|
373 |
}
|
374 |
};
|
375 |
|
376 |
-
|
377 |
-
updateProviderStatuses();
|
378 |
-
|
379 |
-
const interval = setInterval(updateProviderStatuses, 30000);
|
380 |
-
|
381 |
-
return () => clearInterval(interval);
|
382 |
-
}, [providers]);
|
383 |
-
|
384 |
-
const handleCheckForUpdate = useCallback(async () => {
|
385 |
-
if (isCheckingUpdate) {
|
386 |
-
return;
|
387 |
-
}
|
388 |
-
|
389 |
try {
|
390 |
-
|
391 |
-
|
392 |
-
|
393 |
-
const
|
394 |
-
|
395 |
-
|
396 |
-
|
397 |
-
|
398 |
-
|
399 |
-
|
400 |
-
|
401 |
-
|
402 |
-
|
403 |
-
|
404 |
-
|
405 |
-
|
406 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
407 |
} else {
|
408 |
-
|
409 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
410 |
} catch (error) {
|
411 |
-
|
412 |
-
console.error('
|
413 |
} finally {
|
414 |
-
|
415 |
}
|
416 |
-
}
|
417 |
-
|
418 |
-
const handleCopyToClipboard = useCallback(() => {
|
419 |
-
const debugInfo = {
|
420 |
-
System: systemInfo,
|
421 |
-
Providers: activeProviders.map((provider) => ({
|
422 |
-
name: provider.name,
|
423 |
-
enabled: provider.enabled,
|
424 |
-
isLocal: provider.isLocal,
|
425 |
-
running: provider.isRunning,
|
426 |
-
error: provider.error,
|
427 |
-
lastChecked: provider.lastChecked,
|
428 |
-
responseTime: provider.responseTime,
|
429 |
-
url: provider.url,
|
430 |
-
})),
|
431 |
-
Version: {
|
432 |
-
hash: versionHash.slice(0, 7),
|
433 |
-
branch: isLatestBranch ? 'main' : 'stable',
|
434 |
-
},
|
435 |
-
Timestamp: new Date().toISOString(),
|
436 |
-
};
|
437 |
-
|
438 |
-
navigator.clipboard.writeText(JSON.stringify(debugInfo, null, 2)).then(() => {
|
439 |
-
toast.success('Debug information copied to clipboard!');
|
440 |
-
});
|
441 |
-
}, [activeProviders, systemInfo, isLatestBranch]);
|
442 |
|
443 |
return (
|
444 |
<div className="space-y-6">
|
445 |
-
|
446 |
-
|
447 |
-
|
448 |
-
<
|
449 |
-
|
450 |
-
|
451 |
-
|
452 |
-
|
453 |
-
|
454 |
-
|
455 |
-
whileTap={{ scale: 0.98 }}
|
456 |
-
>
|
457 |
-
<div className="i-ph:copy" />
|
458 |
-
Copy Debug Info
|
459 |
-
</motion.button>
|
460 |
-
<motion.button
|
461 |
-
onClick={handleCheckForUpdate}
|
462 |
-
disabled={isCheckingUpdate}
|
463 |
-
className={classNames(settingsStyles.button.base, settingsStyles.button.primary)}
|
464 |
-
whileHover={!isCheckingUpdate ? { scale: 1.02 } : undefined}
|
465 |
-
whileTap={!isCheckingUpdate ? { scale: 0.98 } : undefined}
|
466 |
-
>
|
467 |
-
{isCheckingUpdate ? (
|
468 |
-
<>
|
469 |
-
<div className={settingsStyles['loading-spinner']} />
|
470 |
-
Checking...
|
471 |
-
</>
|
472 |
-
) : (
|
473 |
-
<>
|
474 |
-
<div className="i-ph:arrow-clockwise" />
|
475 |
-
Check for Updates
|
476 |
-
</>
|
477 |
-
)}
|
478 |
-
</motion.button>
|
479 |
-
</div>
|
480 |
-
</div>
|
481 |
-
|
482 |
-
{updateMessage && (
|
483 |
-
<motion.div
|
484 |
-
className={classNames(
|
485 |
-
settingsStyles.card,
|
486 |
-
'bg-bolt-elements-background-depth-2',
|
487 |
-
updateMessage.includes('Update available') ? 'border-l-4 border-yellow-500' : '',
|
488 |
-
)}
|
489 |
-
initial={{ opacity: 0, y: -20 }}
|
490 |
-
animate={{ opacity: 1, y: 0 }}
|
491 |
-
>
|
492 |
-
<div className="flex items-start gap-3">
|
493 |
-
<div
|
494 |
className={classNames(
|
495 |
-
|
496 |
-
|
497 |
-
|
498 |
-
'
|
499 |
)}
|
500 |
-
|
501 |
-
|
502 |
-
|
503 |
-
|
504 |
-
|
505 |
-
|
506 |
-
|
507 |
-
|
508 |
-
|
509 |
-
|
510 |
-
|
511 |
-
<code className="px-1.5 py-0.5 rounded bg-bolt-elements-background-depth-3 text-bolt-elements-textPrimary">
|
512 |
-
git pull upstream main
|
513 |
-
</code>
|
514 |
-
</div>
|
515 |
-
</li>
|
516 |
-
<li className="text-bolt-elements-textSecondary">
|
517 |
-
<div className="flex items-center gap-2">
|
518 |
-
<div className="i-ph:package text-purple-500" />
|
519 |
-
Install any new dependencies:{' '}
|
520 |
-
<code className="px-1.5 py-0.5 rounded bg-bolt-elements-background-depth-3 text-bolt-elements-textPrimary">
|
521 |
-
pnpm install
|
522 |
-
</code>
|
523 |
-
</div>
|
524 |
-
</li>
|
525 |
-
<li className="text-bolt-elements-textSecondary">
|
526 |
-
<div className="flex items-center gap-2">
|
527 |
-
<div className="i-ph:arrows-clockwise text-purple-500" />
|
528 |
-
Restart the application
|
529 |
-
</div>
|
530 |
-
</li>
|
531 |
-
</ol>
|
532 |
-
</div>
|
533 |
)}
|
534 |
-
|
535 |
-
|
536 |
-
|
537 |
-
|
538 |
-
|
539 |
-
<section className="space-y-4">
|
540 |
-
<motion.div className="space-y-4">
|
541 |
-
<div className="flex items-center gap-2 mb-4">
|
542 |
-
<div className="i-ph:desktop text-xl text-purple-500" />
|
543 |
-
<h4 className="text-md font-medium text-bolt-elements-textPrimary">System Information</h4>
|
544 |
</div>
|
545 |
-
|
546 |
-
|
547 |
-
|
548 |
-
|
549 |
-
|
550 |
-
|
551 |
-
</
|
552 |
-
<
|
553 |
</div>
|
554 |
-
<div>
|
555 |
-
<div className="
|
556 |
-
|
557 |
-
|
558 |
-
</div>
|
559 |
-
<p className="text-sm font-medium text-bolt-elements-textPrimary">{systemInfo.deviceType}</p>
|
560 |
</div>
|
561 |
-
<div>
|
562 |
-
<div className="
|
563 |
-
|
564 |
-
|
565 |
-
</div>
|
566 |
-
<p className="text-sm font-medium text-bolt-elements-textPrimary">{systemInfo.browser}</p>
|
567 |
</div>
|
568 |
-
<div>
|
569 |
-
<div className="
|
570 |
-
|
571 |
-
|
572 |
-
</div>
|
573 |
-
<p className="text-sm font-medium text-bolt-elements-textPrimary">
|
574 |
-
{systemInfo.screen} ({systemInfo.colorDepth}) @{systemInfo.pixelRatio}x
|
575 |
-
</p>
|
576 |
</div>
|
577 |
-
<div>
|
578 |
-
<div className="
|
579 |
-
|
580 |
-
|
581 |
-
|
582 |
-
|
583 |
-
|
584 |
-
|
585 |
-
|
586 |
-
|
587 |
-
|
588 |
-
|
589 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
590 |
</span>
|
591 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
592 |
</div>
|
593 |
-
|
594 |
-
|
595 |
-
|
596 |
-
|
597 |
-
</
|
598 |
-
<
|
|
|
|
|
599 |
</div>
|
600 |
-
<div>
|
601 |
-
<div className="
|
602 |
-
|
603 |
-
|
604 |
-
|
605 |
-
|
606 |
</div>
|
607 |
-
<div>
|
608 |
-
<div className="
|
609 |
-
|
610 |
-
|
611 |
-
|
612 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
613 |
</div>
|
614 |
</div>
|
615 |
-
|
616 |
-
|
617 |
-
|
618 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
619 |
</div>
|
620 |
-
<
|
621 |
-
|
622 |
-
|
623 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
624 |
</span>
|
625 |
-
</
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
626 |
</div>
|
627 |
-
</motion.div>
|
628 |
-
</motion.div>
|
629 |
-
|
630 |
-
<motion.div
|
631 |
-
className="space-y-4"
|
632 |
-
initial={{ opacity: 0, y: 20 }}
|
633 |
-
animate={{ opacity: 1, y: 0 }}
|
634 |
-
transition={{ delay: 0.2 }}
|
635 |
-
>
|
636 |
-
<div className="flex items-center gap-2 mb-4">
|
637 |
-
<div className="i-ph:robot text-xl text-purple-500" />
|
638 |
-
<h4 className="text-md font-medium text-bolt-elements-textPrimary">Local LLM Status</h4>
|
639 |
</div>
|
640 |
-
|
641 |
-
|
642 |
-
{activeProviders.map((provider) => (
|
643 |
-
<div key={provider.name} className="p-4 first:pt-0 last:pb-0">
|
644 |
-
<div className="flex items-center justify-between">
|
645 |
-
<div className="flex items-center gap-3">
|
646 |
-
<div className="flex-shrink-0">
|
647 |
-
<div
|
648 |
-
className={classNames(
|
649 |
-
'w-2 h-2 rounded-full',
|
650 |
-
!provider.enabled ? 'bg-gray-400' : provider.isRunning ? 'bg-green-500' : 'bg-red-500',
|
651 |
-
)}
|
652 |
-
/>
|
653 |
-
</div>
|
654 |
-
<div>
|
655 |
-
<p className="text-sm font-medium text-bolt-elements-textPrimary">{provider.name}</p>
|
656 |
-
{provider.url && (
|
657 |
-
<p className="text-xs text-bolt-elements-textSecondary truncate max-w-[300px]">
|
658 |
-
{provider.url}
|
659 |
-
</p>
|
660 |
-
)}
|
661 |
-
</div>
|
662 |
-
</div>
|
663 |
-
<div className="flex items-center gap-2">
|
664 |
-
<span
|
665 |
-
className={classNames(
|
666 |
-
'px-2 py-0.5 text-xs rounded-full',
|
667 |
-
provider.enabled
|
668 |
-
? 'bg-green-500/10 text-green-500'
|
669 |
-
: 'bg-gray-500/10 text-bolt-elements-textSecondary',
|
670 |
-
)}
|
671 |
-
>
|
672 |
-
{provider.enabled ? 'Enabled' : 'Disabled'}
|
673 |
-
</span>
|
674 |
-
{provider.enabled && (
|
675 |
-
<span
|
676 |
-
className={classNames(
|
677 |
-
'px-2 py-0.5 text-xs rounded-full',
|
678 |
-
provider.isRunning ? 'bg-green-500/10 text-green-500' : 'bg-red-500/10 text-red-500',
|
679 |
-
)}
|
680 |
-
>
|
681 |
-
{provider.isRunning ? 'Running' : 'Not Running'}
|
682 |
-
</span>
|
683 |
-
)}
|
684 |
-
</div>
|
685 |
-
</div>
|
686 |
|
687 |
-
|
688 |
-
|
689 |
-
|
690 |
-
|
691 |
-
|
692 |
-
|
693 |
-
|
694 |
-
|
695 |
-
|
696 |
-
|
697 |
-
|
698 |
-
|
699 |
-
|
700 |
-
|
701 |
-
|
702 |
-
|
703 |
-
|
704 |
-
|
705 |
-
|
706 |
-
|
707 |
-
|
708 |
-
|
709 |
-
|
710 |
-
|
711 |
-
|
712 |
-
|
713 |
-
|
714 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
715 |
</div>
|
716 |
-
|
717 |
-
|
718 |
-
|
719 |
-
|
720 |
-
|
|
|
|
|
721 |
</div>
|
722 |
-
|
723 |
-
</
|
724 |
-
</
|
725 |
</div>
|
726 |
);
|
727 |
}
|
|
|
1 |
+
import React, { useEffect, useState } from 'react';
|
|
|
2 |
import { toast } from 'react-toastify';
|
|
|
|
|
3 |
import { classNames } from '~/utils/classNames';
|
4 |
+
import { logStore } from '~/lib/stores/logs';
|
5 |
|
6 |
interface ProviderStatus {
|
7 |
+
id: string;
|
8 |
name: string;
|
9 |
+
status: 'online' | 'offline' | 'error';
|
|
|
|
|
10 |
error?: string;
|
|
|
|
|
|
|
11 |
}
|
12 |
|
13 |
interface SystemInfo {
|
14 |
os: string;
|
15 |
+
arch: string;
|
16 |
+
platform: string;
|
17 |
+
cpus: string;
|
18 |
+
memory: {
|
19 |
+
total: string;
|
20 |
+
free: string;
|
21 |
+
used: string;
|
22 |
+
percentage: number;
|
23 |
+
};
|
24 |
+
node: string;
|
25 |
+
browser: {
|
26 |
+
name: string;
|
27 |
+
version: string;
|
28 |
+
language: string;
|
29 |
+
userAgent: string;
|
30 |
+
cookiesEnabled: boolean;
|
31 |
+
online: boolean;
|
32 |
+
platform: string;
|
33 |
+
cores: number;
|
34 |
+
};
|
35 |
+
screen: {
|
36 |
+
width: number;
|
37 |
+
height: number;
|
38 |
+
colorDepth: number;
|
39 |
+
pixelRatio: number;
|
40 |
+
};
|
41 |
+
time: {
|
42 |
+
timezone: string;
|
43 |
+
offset: number;
|
44 |
+
locale: string;
|
45 |
+
};
|
46 |
+
performance: {
|
47 |
+
memory: {
|
48 |
+
jsHeapSizeLimit: number;
|
49 |
+
totalJSHeapSize: number;
|
50 |
+
usedJSHeapSize: number;
|
51 |
+
usagePercentage: number;
|
52 |
+
};
|
53 |
+
timing: {
|
54 |
+
loadTime: number;
|
55 |
+
domReadyTime: number;
|
56 |
+
readyStart: number;
|
57 |
+
redirectTime: number;
|
58 |
+
appcacheTime: number;
|
59 |
+
unloadEventTime: number;
|
60 |
+
lookupDomainTime: number;
|
61 |
+
connectTime: number;
|
62 |
+
requestTime: number;
|
63 |
+
initDomTreeTime: number;
|
64 |
+
loadEventTime: number;
|
65 |
+
};
|
66 |
+
navigation: {
|
67 |
+
type: number;
|
68 |
+
redirectCount: number;
|
69 |
+
};
|
70 |
+
};
|
71 |
+
network: {
|
72 |
+
downlink: number;
|
73 |
+
effectiveType: string;
|
74 |
+
rtt: number;
|
75 |
+
saveData: boolean;
|
76 |
+
type: string;
|
77 |
+
};
|
78 |
+
battery?: {
|
79 |
+
charging: boolean;
|
80 |
+
chargingTime: number;
|
81 |
+
dischargingTime: number;
|
82 |
+
level: number;
|
83 |
+
};
|
84 |
+
storage: {
|
85 |
+
quota: number;
|
86 |
+
usage: number;
|
87 |
+
persistent: boolean;
|
88 |
+
temporary: boolean;
|
89 |
};
|
90 |
}
|
91 |
|
92 |
+
export default function DebugTab() {
|
93 |
+
const [providerStatuses, setProviderStatuses] = useState<ProviderStatus[]>([]);
|
94 |
+
const [systemInfo, setSystemInfo] = useState<SystemInfo | null>(null);
|
95 |
+
const [loading, setLoading] = useState({
|
96 |
+
systemInfo: false,
|
97 |
+
providers: false,
|
98 |
+
performance: false,
|
99 |
+
errors: false,
|
100 |
+
});
|
101 |
+
const [errorLog, setErrorLog] = useState<{
|
102 |
+
errors: any[];
|
103 |
+
lastCheck: string | null;
|
104 |
+
}>({
|
105 |
+
errors: [],
|
106 |
+
lastCheck: null,
|
107 |
+
});
|
108 |
+
|
109 |
+
// Fetch initial data
|
110 |
+
useEffect(() => {
|
111 |
+
checkProviderStatus();
|
112 |
+
getSystemInfo();
|
113 |
+
}, []);
|
114 |
|
115 |
+
// Set up error listeners when component mounts
|
116 |
+
useEffect(() => {
|
117 |
+
const errors: any[] = [];
|
118 |
+
|
119 |
+
const handleError = (event: ErrorEvent) => {
|
120 |
+
errors.push({
|
121 |
+
type: 'error',
|
122 |
+
message: event.message,
|
123 |
+
filename: event.filename,
|
124 |
+
lineNumber: event.lineno,
|
125 |
+
columnNumber: event.colno,
|
126 |
+
error: event.error,
|
127 |
+
timestamp: new Date().toISOString(),
|
128 |
+
});
|
129 |
+
};
|
130 |
|
131 |
+
const handleRejection = (event: PromiseRejectionEvent) => {
|
132 |
+
errors.push({
|
133 |
+
type: 'unhandledRejection',
|
134 |
+
reason: event.reason,
|
135 |
+
timestamp: new Date().toISOString(),
|
136 |
+
});
|
137 |
+
};
|
138 |
|
139 |
+
window.addEventListener('error', handleError);
|
140 |
+
window.addEventListener('unhandledrejection', handleRejection);
|
|
|
|
|
141 |
|
142 |
+
return () => {
|
143 |
+
window.removeEventListener('error', handleError);
|
144 |
+
window.removeEventListener('unhandledrejection', handleRejection);
|
145 |
+
};
|
146 |
+
}, []);
|
|
|
|
|
|
|
|
|
|
|
147 |
|
148 |
+
const checkProviderStatus = async () => {
|
149 |
+
try {
|
150 |
+
setLoading((prev) => ({ ...prev, providers: true }));
|
|
|
|
|
151 |
|
152 |
+
// Fetch real provider statuses
|
153 |
+
const providers: ProviderStatus[] = [];
|
|
|
154 |
|
155 |
+
// Check OpenAI status
|
156 |
+
try {
|
157 |
+
const openaiResponse = await fetch('/api/providers/openai/status');
|
158 |
+
providers.push({
|
159 |
+
id: 'openai',
|
160 |
+
name: 'OpenAI',
|
161 |
+
status: openaiResponse.ok ? 'online' : 'error',
|
162 |
+
error: !openaiResponse.ok ? 'API Error' : undefined,
|
163 |
+
});
|
164 |
+
} catch {
|
165 |
+
providers.push({ id: 'openai', name: 'OpenAI', status: 'offline' });
|
166 |
+
}
|
167 |
|
168 |
+
// Check Anthropic status
|
169 |
+
try {
|
170 |
+
const anthropicResponse = await fetch('/api/providers/anthropic/status');
|
171 |
+
providers.push({
|
172 |
+
id: 'anthropic',
|
173 |
+
name: 'Anthropic',
|
174 |
+
status: anthropicResponse.ok ? 'online' : 'error',
|
175 |
+
error: !anthropicResponse.ok ? 'API Error' : undefined,
|
176 |
+
});
|
177 |
+
} catch {
|
178 |
+
providers.push({ id: 'anthropic', name: 'Anthropic', status: 'offline' });
|
|
|
|
|
179 |
}
|
180 |
+
|
181 |
+
// Check Local Models status
|
182 |
+
try {
|
183 |
+
const localResponse = await fetch('/api/providers/local/status');
|
184 |
+
providers.push({
|
185 |
+
id: 'local',
|
186 |
+
name: 'Local Models',
|
187 |
+
status: localResponse.ok ? 'online' : 'error',
|
188 |
+
error: !localResponse.ok ? 'API Error' : undefined,
|
189 |
+
});
|
190 |
+
} catch {
|
191 |
+
providers.push({ id: 'local', name: 'Local Models', status: 'offline' });
|
192 |
}
|
|
|
193 |
|
194 |
+
// Check Ollama status
|
195 |
+
try {
|
196 |
+
const ollamaResponse = await fetch('/api/providers/ollama/status');
|
197 |
+
providers.push({
|
198 |
+
id: 'ollama',
|
199 |
+
name: 'Ollama',
|
200 |
+
status: ollamaResponse.ok ? 'online' : 'error',
|
201 |
+
error: !ollamaResponse.ok ? 'API Error' : undefined,
|
202 |
+
});
|
203 |
+
} catch {
|
204 |
+
providers.push({ id: 'ollama', name: 'Ollama', status: 'offline' });
|
205 |
+
}
|
206 |
|
207 |
+
setProviderStatuses(providers);
|
208 |
+
toast.success('Provider status updated');
|
209 |
+
} catch (error) {
|
210 |
+
toast.error('Failed to check provider status');
|
211 |
+
console.error('Failed to check provider status:', error);
|
212 |
+
} finally {
|
213 |
+
setLoading((prev) => ({ ...prev, providers: false }));
|
214 |
+
}
|
215 |
};
|
216 |
|
217 |
+
const getSystemInfo = async () => {
|
218 |
+
try {
|
219 |
+
setLoading((prev) => ({ ...prev, systemInfo: true }));
|
220 |
+
|
221 |
+
// Get browser info
|
222 |
+
const ua = navigator.userAgent;
|
223 |
+
const browserName = ua.includes('Firefox')
|
224 |
+
? 'Firefox'
|
225 |
+
: ua.includes('Chrome')
|
226 |
+
? 'Chrome'
|
227 |
+
: ua.includes('Safari')
|
228 |
+
? 'Safari'
|
229 |
+
: ua.includes('Edge')
|
230 |
+
? 'Edge'
|
231 |
+
: 'Unknown';
|
232 |
+
const browserVersion = ua.match(/(Firefox|Chrome|Safari|Edge)\/([0-9.]+)/)?.[2] || 'Unknown';
|
233 |
+
|
234 |
+
// Get performance metrics
|
235 |
+
const memory = (performance as any).memory || {};
|
236 |
+
const timing = performance.timing;
|
237 |
+
const navigation = performance.navigation;
|
238 |
+
const connection = (navigator as any).connection;
|
239 |
+
|
240 |
+
// Get battery info
|
241 |
+
let batteryInfo;
|
242 |
|
243 |
+
try {
|
244 |
+
const battery = await (navigator as any).getBattery();
|
245 |
+
batteryInfo = {
|
246 |
+
charging: battery.charging,
|
247 |
+
chargingTime: battery.chargingTime,
|
248 |
+
dischargingTime: battery.dischargingTime,
|
249 |
+
level: battery.level * 100,
|
250 |
+
};
|
251 |
+
} catch {
|
252 |
+
console.log('Battery API not supported');
|
253 |
}
|
254 |
|
255 |
+
// Get storage info
|
256 |
+
let storageInfo = {
|
257 |
+
quota: 0,
|
258 |
+
usage: 0,
|
259 |
+
persistent: false,
|
260 |
+
temporary: false,
|
261 |
+
};
|
262 |
|
263 |
+
try {
|
264 |
+
const storage = await navigator.storage.estimate();
|
265 |
+
const persistent = await navigator.storage.persist();
|
266 |
+
storageInfo = {
|
267 |
+
quota: storage.quota || 0,
|
268 |
+
usage: storage.usage || 0,
|
269 |
+
persistent,
|
270 |
+
temporary: !persistent,
|
271 |
+
};
|
272 |
+
} catch {
|
273 |
+
console.log('Storage API not supported');
|
274 |
+
}
|
275 |
|
276 |
+
// Get memory info from browser performance API
|
277 |
+
const performanceMemory = (performance as any).memory || {};
|
278 |
+
const totalMemory = performanceMemory.jsHeapSizeLimit || 0;
|
279 |
+
const usedMemory = performanceMemory.usedJSHeapSize || 0;
|
280 |
+
const freeMemory = totalMemory - usedMemory;
|
281 |
+
const memoryPercentage = totalMemory ? (usedMemory / totalMemory) * 100 : 0;
|
282 |
+
|
283 |
+
const systemInfo: SystemInfo = {
|
284 |
+
os: navigator.platform,
|
285 |
+
arch: navigator.userAgent.includes('x64') ? 'x64' : navigator.userAgent.includes('arm') ? 'arm' : 'unknown',
|
286 |
+
platform: navigator.platform,
|
287 |
+
cpus: navigator.hardwareConcurrency + ' cores',
|
288 |
+
memory: {
|
289 |
+
total: formatBytes(totalMemory),
|
290 |
+
free: formatBytes(freeMemory),
|
291 |
+
used: formatBytes(usedMemory),
|
292 |
+
percentage: Math.round(memoryPercentage),
|
293 |
+
},
|
294 |
+
node: 'browser',
|
295 |
+
browser: {
|
296 |
+
name: browserName,
|
297 |
+
version: browserVersion,
|
298 |
+
language: navigator.language,
|
299 |
+
userAgent: navigator.userAgent,
|
300 |
+
cookiesEnabled: navigator.cookieEnabled,
|
301 |
+
online: navigator.onLine,
|
302 |
+
platform: navigator.platform,
|
303 |
+
cores: navigator.hardwareConcurrency,
|
304 |
+
},
|
305 |
+
screen: {
|
306 |
+
width: window.screen.width,
|
307 |
+
height: window.screen.height,
|
308 |
+
colorDepth: window.screen.colorDepth,
|
309 |
+
pixelRatio: window.devicePixelRatio,
|
310 |
+
},
|
311 |
+
time: {
|
312 |
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
313 |
+
offset: new Date().getTimezoneOffset(),
|
314 |
+
locale: navigator.language,
|
315 |
+
},
|
316 |
+
performance: {
|
317 |
+
memory: {
|
318 |
+
jsHeapSizeLimit: memory.jsHeapSizeLimit || 0,
|
319 |
+
totalJSHeapSize: memory.totalJSHeapSize || 0,
|
320 |
+
usedJSHeapSize: memory.usedJSHeapSize || 0,
|
321 |
+
usagePercentage: memory.totalJSHeapSize ? (memory.usedJSHeapSize / memory.totalJSHeapSize) * 100 : 0,
|
322 |
+
},
|
323 |
+
timing: {
|
324 |
+
loadTime: timing.loadEventEnd - timing.navigationStart,
|
325 |
+
domReadyTime: timing.domContentLoadedEventEnd - timing.navigationStart,
|
326 |
+
readyStart: timing.fetchStart - timing.navigationStart,
|
327 |
+
redirectTime: timing.redirectEnd - timing.redirectStart,
|
328 |
+
appcacheTime: timing.domainLookupStart - timing.fetchStart,
|
329 |
+
unloadEventTime: timing.unloadEventEnd - timing.unloadEventStart,
|
330 |
+
lookupDomainTime: timing.domainLookupEnd - timing.domainLookupStart,
|
331 |
+
connectTime: timing.connectEnd - timing.connectStart,
|
332 |
+
requestTime: timing.responseEnd - timing.requestStart,
|
333 |
+
initDomTreeTime: timing.domInteractive - timing.responseEnd,
|
334 |
+
loadEventTime: timing.loadEventEnd - timing.loadEventStart,
|
335 |
+
},
|
336 |
+
navigation: {
|
337 |
+
type: navigation.type,
|
338 |
+
redirectCount: navigation.redirectCount,
|
339 |
+
},
|
340 |
+
},
|
341 |
+
network: {
|
342 |
+
downlink: connection?.downlink || 0,
|
343 |
+
effectiveType: connection?.effectiveType || 'unknown',
|
344 |
+
rtt: connection?.rtt || 0,
|
345 |
+
saveData: connection?.saveData || false,
|
346 |
+
type: connection?.type || 'unknown',
|
347 |
+
},
|
348 |
+
battery: batteryInfo,
|
349 |
+
storage: storageInfo,
|
350 |
+
};
|
351 |
|
352 |
+
setSystemInfo(systemInfo);
|
353 |
+
toast.success('System information updated');
|
354 |
+
} catch (error) {
|
355 |
+
toast.error('Failed to get system information');
|
356 |
+
console.error('Failed to get system information:', error);
|
357 |
+
} finally {
|
358 |
+
setLoading((prev) => ({ ...prev, systemInfo: false }));
|
359 |
+
}
|
360 |
};
|
361 |
|
362 |
+
// Helper function to format bytes to human readable format
|
363 |
+
const formatBytes = (bytes: number) => {
|
364 |
+
const units = ['B', 'KB', 'MB', 'GB'];
|
365 |
+
let size = bytes;
|
366 |
+
let unitIndex = 0;
|
367 |
|
368 |
+
while (size >= 1024 && unitIndex < units.length - 1) {
|
369 |
+
size /= 1024;
|
370 |
+
unitIndex++;
|
371 |
}
|
372 |
|
373 |
+
return `${Math.round(size)} ${units[unitIndex]}`;
|
|
|
|
|
|
|
|
|
374 |
};
|
375 |
|
376 |
+
const handleLogSystemInfo = () => {
|
377 |
+
if (!systemInfo) {
|
378 |
+
return;
|
|
|
|
|
379 |
}
|
380 |
|
381 |
+
logStore.logSystem('System Information', {
|
382 |
+
os: systemInfo.os,
|
383 |
+
arch: systemInfo.arch,
|
384 |
+
cpus: systemInfo.cpus,
|
385 |
+
memory: systemInfo.memory,
|
386 |
+
node: systemInfo.node,
|
387 |
+
});
|
388 |
+
toast.success('System information logged');
|
389 |
};
|
390 |
|
391 |
+
const handleLogPerformance = () => {
|
392 |
+
try {
|
393 |
+
setLoading((prev) => ({ ...prev, performance: true }));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
394 |
|
395 |
+
// Get performance metrics using modern Performance API
|
396 |
+
const performanceEntries = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
|
397 |
+
const memory = (performance as any).memory;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
398 |
|
399 |
+
// Calculate timing metrics
|
400 |
+
const timingMetrics = {
|
401 |
+
loadTime: performanceEntries.loadEventEnd - performanceEntries.startTime,
|
402 |
+
domReadyTime: performanceEntries.domContentLoadedEventEnd - performanceEntries.startTime,
|
403 |
+
fetchTime: performanceEntries.responseEnd - performanceEntries.fetchStart,
|
404 |
+
redirectTime: performanceEntries.redirectEnd - performanceEntries.redirectStart,
|
405 |
+
dnsTime: performanceEntries.domainLookupEnd - performanceEntries.domainLookupStart,
|
406 |
+
tcpTime: performanceEntries.connectEnd - performanceEntries.connectStart,
|
407 |
+
ttfb: performanceEntries.responseStart - performanceEntries.requestStart,
|
408 |
+
processingTime: performanceEntries.loadEventEnd - performanceEntries.responseEnd,
|
409 |
+
};
|
410 |
|
411 |
+
// Get resource timing data
|
412 |
+
const resourceEntries = performance.getEntriesByType('resource');
|
413 |
+
const resourceStats = {
|
414 |
+
totalResources: resourceEntries.length,
|
415 |
+
totalSize: resourceEntries.reduce((total, entry) => total + (entry as any).transferSize || 0, 0),
|
416 |
+
totalTime: Math.max(...resourceEntries.map((entry) => entry.duration)),
|
417 |
+
};
|
418 |
|
419 |
+
// Get memory metrics
|
420 |
+
const memoryMetrics = memory
|
421 |
+
? {
|
422 |
+
jsHeapSizeLimit: memory.jsHeapSizeLimit,
|
423 |
+
totalJSHeapSize: memory.totalJSHeapSize,
|
424 |
+
usedJSHeapSize: memory.usedJSHeapSize,
|
425 |
+
heapUtilization: (memory.usedJSHeapSize / memory.totalJSHeapSize) * 100,
|
426 |
+
}
|
427 |
+
: null;
|
428 |
|
429 |
+
// Get frame rate metrics
|
430 |
+
let fps = 0;
|
431 |
|
432 |
+
if ('requestAnimationFrame' in window) {
|
433 |
+
const times: number[] = [];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
434 |
|
435 |
+
function calculateFPS(now: number) {
|
436 |
+
times.push(now);
|
437 |
+
|
438 |
+
if (times.length > 10) {
|
439 |
+
const fps = Math.round((1000 * 10) / (now - times[0]));
|
440 |
+
times.shift();
|
441 |
+
|
442 |
+
return fps;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
443 |
}
|
444 |
|
445 |
+
requestAnimationFrame(calculateFPS);
|
446 |
+
|
447 |
+
return 0;
|
|
|
448 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
449 |
|
450 |
+
fps = calculateFPS(performance.now());
|
451 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
452 |
|
453 |
+
// Log all performance metrics
|
454 |
+
logStore.logSystem('Performance Metrics', {
|
455 |
+
timing: timingMetrics,
|
456 |
+
resources: resourceStats,
|
457 |
+
memory: memoryMetrics,
|
458 |
+
fps,
|
459 |
+
timestamp: new Date().toISOString(),
|
460 |
+
navigationEntry: {
|
461 |
+
type: performanceEntries.type,
|
462 |
+
redirectCount: performanceEntries.redirectCount,
|
463 |
+
},
|
464 |
+
});
|
465 |
+
|
466 |
+
toast.success('Performance metrics logged');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
467 |
} catch (error) {
|
468 |
+
toast.error('Failed to log performance metrics');
|
469 |
+
console.error('Failed to log performance metrics:', error);
|
470 |
+
} finally {
|
471 |
+
setLoading((prev) => ({ ...prev, performance: false }));
|
472 |
}
|
473 |
};
|
474 |
|
475 |
+
const handleCheckErrors = () => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
476 |
try {
|
477 |
+
setLoading((prev) => ({ ...prev, errors: true }));
|
478 |
+
|
479 |
+
// Get any errors from the performance entries
|
480 |
+
const resourceErrors = performance
|
481 |
+
.getEntriesByType('resource')
|
482 |
+
.filter((entry) => {
|
483 |
+
const failedEntry = entry as PerformanceResourceTiming;
|
484 |
+
return failedEntry.responseEnd - failedEntry.startTime === 0;
|
485 |
+
})
|
486 |
+
.map((entry) => ({
|
487 |
+
type: 'networkError',
|
488 |
+
resource: entry.name,
|
489 |
+
timestamp: new Date().toISOString(),
|
490 |
+
}));
|
491 |
+
|
492 |
+
// Combine collected errors with resource errors
|
493 |
+
const allErrors = [...errorLog.errors, ...resourceErrors];
|
494 |
+
|
495 |
+
if (allErrors.length > 0) {
|
496 |
+
logStore.logError('JavaScript Errors Found', {
|
497 |
+
errors: allErrors,
|
498 |
+
timestamp: new Date().toISOString(),
|
499 |
+
});
|
500 |
+
toast.error(`Found ${allErrors.length} error(s)`);
|
501 |
} else {
|
502 |
+
toast.success('No errors found');
|
503 |
}
|
504 |
+
|
505 |
+
// Update error log
|
506 |
+
setErrorLog({
|
507 |
+
errors: allErrors,
|
508 |
+
lastCheck: new Date().toISOString(),
|
509 |
+
});
|
510 |
} catch (error) {
|
511 |
+
toast.error('Failed to check for errors');
|
512 |
+
console.error('Failed to check for errors:', error);
|
513 |
} finally {
|
514 |
+
setLoading((prev) => ({ ...prev, errors: false }));
|
515 |
}
|
516 |
+
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
517 |
|
518 |
return (
|
519 |
<div className="space-y-6">
|
520 |
+
{/* System Information */}
|
521 |
+
<div className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
|
522 |
+
<div className="flex items-center justify-between mb-4">
|
523 |
+
<div className="flex items-center gap-3">
|
524 |
+
<div className="i-ph:cpu text-purple-500 w-5 h-5" />
|
525 |
+
<h3 className="text-base font-medium text-bolt-elements-textPrimary">System Information</h3>
|
526 |
+
</div>
|
527 |
+
<div className="flex items-center gap-2">
|
528 |
+
<button
|
529 |
+
onClick={handleLogSystemInfo}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
530 |
className={classNames(
|
531 |
+
'flex items-center gap-2 px-3 py-2 rounded-lg text-sm',
|
532 |
+
'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
|
533 |
+
'hover:bg-[#E5E5E5] dark:hover:bg-[#252525]',
|
534 |
+
'transition-colors',
|
535 |
)}
|
536 |
+
>
|
537 |
+
<div className="i-ph:note text-bolt-elements-textSecondary w-4 h-4" />
|
538 |
+
Log
|
539 |
+
</button>
|
540 |
+
<button
|
541 |
+
onClick={getSystemInfo}
|
542 |
+
className={classNames(
|
543 |
+
'flex items-center gap-2 px-3 py-2 rounded-lg text-sm',
|
544 |
+
'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
|
545 |
+
'hover:bg-[#E5E5E5] dark:hover:bg-[#252525]',
|
546 |
+
'transition-colors',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
547 |
)}
|
548 |
+
>
|
549 |
+
<div className={classNames('i-ph:arrows-clockwise w-4 h-4', loading.systemInfo ? 'animate-spin' : '')} />
|
550 |
+
Refresh
|
551 |
+
</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
552 |
</div>
|
553 |
+
</div>
|
554 |
+
{systemInfo ? (
|
555 |
+
<div className="grid grid-cols-2 gap-6">
|
556 |
+
<div className="space-y-2">
|
557 |
+
<div className="text-sm flex items-center gap-2">
|
558 |
+
<div className="i-ph:desktop text-bolt-elements-textSecondary w-4 h-4" />
|
559 |
+
<span className="text-bolt-elements-textSecondary">OS: </span>
|
560 |
+
<span className="text-bolt-elements-textPrimary">{systemInfo.os}</span>
|
561 |
</div>
|
562 |
+
<div className="text-sm flex items-center gap-2">
|
563 |
+
<div className="i-ph:device-mobile text-bolt-elements-textSecondary w-4 h-4" />
|
564 |
+
<span className="text-bolt-elements-textSecondary">Platform: </span>
|
565 |
+
<span className="text-bolt-elements-textPrimary">{systemInfo.platform}</span>
|
|
|
|
|
566 |
</div>
|
567 |
+
<div className="text-sm flex items-center gap-2">
|
568 |
+
<div className="i-ph:microchip text-bolt-elements-textSecondary w-4 h-4" />
|
569 |
+
<span className="text-bolt-elements-textSecondary">Architecture: </span>
|
570 |
+
<span className="text-bolt-elements-textPrimary">{systemInfo.arch}</span>
|
|
|
|
|
571 |
</div>
|
572 |
+
<div className="text-sm flex items-center gap-2">
|
573 |
+
<div className="i-ph:cpu text-bolt-elements-textSecondary w-4 h-4" />
|
574 |
+
<span className="text-bolt-elements-textSecondary">CPU Cores: </span>
|
575 |
+
<span className="text-bolt-elements-textPrimary">{systemInfo.cpus}</span>
|
|
|
|
|
|
|
|
|
576 |
</div>
|
577 |
+
<div className="text-sm flex items-center gap-2">
|
578 |
+
<div className="i-ph:node text-bolt-elements-textSecondary w-4 h-4" />
|
579 |
+
<span className="text-bolt-elements-textSecondary">Node Version: </span>
|
580 |
+
<span className="text-bolt-elements-textPrimary">{systemInfo.node}</span>
|
581 |
+
</div>
|
582 |
+
<div className="text-sm flex items-center gap-2">
|
583 |
+
<div className="i-ph:wifi-high text-bolt-elements-textSecondary w-4 h-4" />
|
584 |
+
<span className="text-bolt-elements-textSecondary">Network Type: </span>
|
585 |
+
<span className="text-bolt-elements-textPrimary">
|
586 |
+
{systemInfo.network.type} ({systemInfo.network.effectiveType})
|
587 |
+
</span>
|
588 |
+
</div>
|
589 |
+
<div className="text-sm flex items-center gap-2">
|
590 |
+
<div className="i-ph:gauge text-bolt-elements-textSecondary w-4 h-4" />
|
591 |
+
<span className="text-bolt-elements-textSecondary">Network Speed: </span>
|
592 |
+
<span className="text-bolt-elements-textPrimary">
|
593 |
+
{systemInfo.network.downlink}Mbps (RTT: {systemInfo.network.rtt}ms)
|
594 |
+
</span>
|
595 |
+
</div>
|
596 |
+
{systemInfo.battery && (
|
597 |
+
<div className="text-sm flex items-center gap-2">
|
598 |
+
<div className="i-ph:battery-charging text-bolt-elements-textSecondary w-4 h-4" />
|
599 |
+
<span className="text-bolt-elements-textSecondary">Battery: </span>
|
600 |
+
<span className="text-bolt-elements-textPrimary">
|
601 |
+
{systemInfo.battery.level.toFixed(1)}% {systemInfo.battery.charging ? '(Charging)' : ''}
|
602 |
</span>
|
603 |
</div>
|
604 |
+
)}
|
605 |
+
<div className="text-sm flex items-center gap-2">
|
606 |
+
<div className="i-ph:hard-drive text-bolt-elements-textSecondary w-4 h-4" />
|
607 |
+
<span className="text-bolt-elements-textSecondary">Storage: </span>
|
608 |
+
<span className="text-bolt-elements-textPrimary">
|
609 |
+
{(systemInfo.storage.usage / (1024 * 1024 * 1024)).toFixed(2)}GB /{' '}
|
610 |
+
{(systemInfo.storage.quota / (1024 * 1024 * 1024)).toFixed(2)}GB
|
611 |
+
</span>
|
612 |
</div>
|
613 |
+
</div>
|
614 |
+
<div className="space-y-2">
|
615 |
+
<div className="text-sm flex items-center gap-2">
|
616 |
+
<div className="i-ph:database text-bolt-elements-textSecondary w-4 h-4" />
|
617 |
+
<span className="text-bolt-elements-textSecondary">Memory Usage: </span>
|
618 |
+
<span className="text-bolt-elements-textPrimary">
|
619 |
+
{systemInfo.memory.used} / {systemInfo.memory.total} ({systemInfo.memory.percentage}%)
|
620 |
+
</span>
|
621 |
</div>
|
622 |
+
<div className="text-sm flex items-center gap-2">
|
623 |
+
<div className="i-ph:browser text-bolt-elements-textSecondary w-4 h-4" />
|
624 |
+
<span className="text-bolt-elements-textSecondary">Browser: </span>
|
625 |
+
<span className="text-bolt-elements-textPrimary">
|
626 |
+
{systemInfo.browser.name} {systemInfo.browser.version}
|
627 |
+
</span>
|
628 |
</div>
|
629 |
+
<div className="text-sm flex items-center gap-2">
|
630 |
+
<div className="i-ph:monitor text-bolt-elements-textSecondary w-4 h-4" />
|
631 |
+
<span className="text-bolt-elements-textSecondary">Screen: </span>
|
632 |
+
<span className="text-bolt-elements-textPrimary">
|
633 |
+
{systemInfo.screen.width}x{systemInfo.screen.height} ({systemInfo.screen.pixelRatio}x)
|
634 |
+
</span>
|
635 |
+
</div>
|
636 |
+
<div className="text-sm flex items-center gap-2">
|
637 |
+
<div className="i-ph:clock text-bolt-elements-textSecondary w-4 h-4" />
|
638 |
+
<span className="text-bolt-elements-textSecondary">Timezone: </span>
|
639 |
+
<span className="text-bolt-elements-textPrimary">{systemInfo.time.timezone}</span>
|
640 |
+
</div>
|
641 |
+
<div className="text-sm flex items-center gap-2">
|
642 |
+
<div className="i-ph:translate text-bolt-elements-textSecondary w-4 h-4" />
|
643 |
+
<span className="text-bolt-elements-textSecondary">Language: </span>
|
644 |
+
<span className="text-bolt-elements-textPrimary">{systemInfo.browser.language}</span>
|
645 |
+
</div>
|
646 |
+
<div className="text-sm flex items-center gap-2">
|
647 |
+
<div className="i-ph:chart-pie text-bolt-elements-textSecondary w-4 h-4" />
|
648 |
+
<span className="text-bolt-elements-textSecondary">JS Heap: </span>
|
649 |
+
<span className="text-bolt-elements-textPrimary">
|
650 |
+
{(systemInfo.performance.memory.usedJSHeapSize / (1024 * 1024)).toFixed(1)}MB /{' '}
|
651 |
+
{(systemInfo.performance.memory.totalJSHeapSize / (1024 * 1024)).toFixed(1)}MB (
|
652 |
+
{systemInfo.performance.memory.usagePercentage.toFixed(1)}%)
|
653 |
+
</span>
|
654 |
+
</div>
|
655 |
+
<div className="text-sm flex items-center gap-2">
|
656 |
+
<div className="i-ph:timer text-bolt-elements-textSecondary w-4 h-4" />
|
657 |
+
<span className="text-bolt-elements-textSecondary">Page Load: </span>
|
658 |
+
<span className="text-bolt-elements-textPrimary">
|
659 |
+
{(systemInfo.performance.timing.loadTime / 1000).toFixed(2)}s
|
660 |
+
</span>
|
661 |
+
</div>
|
662 |
+
<div className="text-sm flex items-center gap-2">
|
663 |
+
<div className="i-ph:code text-bolt-elements-textSecondary w-4 h-4" />
|
664 |
+
<span className="text-bolt-elements-textSecondary">DOM Ready: </span>
|
665 |
+
<span className="text-bolt-elements-textPrimary">
|
666 |
+
{(systemInfo.performance.timing.domReadyTime / 1000).toFixed(2)}s
|
667 |
+
</span>
|
668 |
</div>
|
669 |
</div>
|
670 |
+
</div>
|
671 |
+
) : (
|
672 |
+
<div className="text-sm text-bolt-elements-textSecondary">Loading system information...</div>
|
673 |
+
)}
|
674 |
+
</div>
|
675 |
+
|
676 |
+
{/* Provider Status */}
|
677 |
+
<div className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
|
678 |
+
<div className="flex items-center justify-between mb-4">
|
679 |
+
<div className="flex items-center gap-3">
|
680 |
+
<div className="i-ph:robot text-purple-500 w-5 h-5" />
|
681 |
+
<h3 className="text-base font-medium text-bolt-elements-textPrimary">Provider Status</h3>
|
682 |
+
</div>
|
683 |
+
<button
|
684 |
+
onClick={checkProviderStatus}
|
685 |
+
className={classNames(
|
686 |
+
'flex items-center gap-2 px-3 py-2 rounded-lg text-sm',
|
687 |
+
'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
|
688 |
+
'hover:bg-[#E5E5E5] dark:hover:bg-[#252525]',
|
689 |
+
'transition-colors',
|
690 |
+
)}
|
691 |
+
>
|
692 |
+
<div className={classNames('i-ph:arrows-clockwise w-4 h-4', loading.providers ? 'animate-spin' : '')} />
|
693 |
+
Refresh
|
694 |
+
</button>
|
695 |
+
</div>
|
696 |
+
<div className="grid grid-cols-2 gap-4">
|
697 |
+
{providerStatuses.map((provider) => (
|
698 |
+
<div
|
699 |
+
key={provider.id}
|
700 |
+
className="flex items-center justify-between p-3 rounded-lg bg-[#F8F8F8] dark:bg-[#141414]"
|
701 |
+
>
|
702 |
+
<div className="flex items-center gap-3">
|
703 |
+
<div
|
704 |
+
className={classNames(
|
705 |
+
'w-2 h-2 rounded-full',
|
706 |
+
provider.status === 'online'
|
707 |
+
? 'bg-green-500'
|
708 |
+
: provider.status === 'offline'
|
709 |
+
? 'bg-red-500'
|
710 |
+
: 'bg-yellow-500',
|
711 |
+
)}
|
712 |
+
/>
|
713 |
+
<span className="text-sm text-bolt-elements-textPrimary">{provider.name}</span>
|
714 |
</div>
|
715 |
+
<span className="text-xs text-bolt-elements-textSecondary capitalize">{provider.status}</span>
|
716 |
+
</div>
|
717 |
+
))}
|
718 |
+
</div>
|
719 |
+
</div>
|
720 |
+
|
721 |
+
{/* Performance Metrics */}
|
722 |
+
<div className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
|
723 |
+
<div className="flex items-center justify-between mb-4">
|
724 |
+
<div className="flex items-center gap-3">
|
725 |
+
<div className="i-ph:chart-line text-purple-500 w-5 h-5" />
|
726 |
+
<h3 className="text-base font-medium text-bolt-elements-textPrimary">Performance Metrics</h3>
|
727 |
+
</div>
|
728 |
+
<button
|
729 |
+
onClick={handleLogPerformance}
|
730 |
+
className={classNames(
|
731 |
+
'flex items-center gap-2 px-3 py-2 rounded-lg text-sm',
|
732 |
+
'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
|
733 |
+
'hover:bg-[#E5E5E5] dark:hover:bg-[#252525]',
|
734 |
+
'transition-colors',
|
735 |
+
)}
|
736 |
+
>
|
737 |
+
<div className={classNames('i-ph:note w-4 h-4', loading.performance ? 'animate-spin' : '')} />
|
738 |
+
Log Performance
|
739 |
+
</button>
|
740 |
+
</div>
|
741 |
+
{systemInfo && (
|
742 |
+
<div className="grid grid-cols-2 gap-4">
|
743 |
+
<div className="space-y-2">
|
744 |
+
<div className="text-sm">
|
745 |
+
<span className="text-bolt-elements-textSecondary">Page Load Time: </span>
|
746 |
+
<span className="text-bolt-elements-textPrimary">
|
747 |
+
{(systemInfo.performance.timing.loadTime / 1000).toFixed(2)}s
|
748 |
+
</span>
|
749 |
+
</div>
|
750 |
+
<div className="text-sm">
|
751 |
+
<span className="text-bolt-elements-textSecondary">DOM Ready Time: </span>
|
752 |
+
<span className="text-bolt-elements-textPrimary">
|
753 |
+
{(systemInfo.performance.timing.domReadyTime / 1000).toFixed(2)}s
|
754 |
+
</span>
|
755 |
+
</div>
|
756 |
+
<div className="text-sm">
|
757 |
+
<span className="text-bolt-elements-textSecondary">Request Time: </span>
|
758 |
+
<span className="text-bolt-elements-textPrimary">
|
759 |
+
{(systemInfo.performance.timing.requestTime / 1000).toFixed(2)}s
|
760 |
</span>
|
761 |
+
</div>
|
762 |
+
<div className="text-sm">
|
763 |
+
<span className="text-bolt-elements-textSecondary">Redirect Time: </span>
|
764 |
+
<span className="text-bolt-elements-textPrimary">
|
765 |
+
{(systemInfo.performance.timing.redirectTime / 1000).toFixed(2)}s
|
766 |
+
</span>
|
767 |
+
</div>
|
768 |
+
</div>
|
769 |
+
<div className="space-y-2">
|
770 |
+
<div className="text-sm">
|
771 |
+
<span className="text-bolt-elements-textSecondary">JS Heap Usage: </span>
|
772 |
+
<span className="text-bolt-elements-textPrimary">
|
773 |
+
{(systemInfo.performance.memory.usedJSHeapSize / (1024 * 1024)).toFixed(1)}MB /{' '}
|
774 |
+
{(systemInfo.performance.memory.totalJSHeapSize / (1024 * 1024)).toFixed(1)}MB
|
775 |
+
</span>
|
776 |
+
</div>
|
777 |
+
<div className="text-sm">
|
778 |
+
<span className="text-bolt-elements-textSecondary">Heap Utilization: </span>
|
779 |
+
<span className="text-bolt-elements-textPrimary">
|
780 |
+
{systemInfo.performance.memory.usagePercentage.toFixed(1)}%
|
781 |
+
</span>
|
782 |
+
</div>
|
783 |
+
<div className="text-sm">
|
784 |
+
<span className="text-bolt-elements-textSecondary">Navigation Type: </span>
|
785 |
+
<span className="text-bolt-elements-textPrimary">
|
786 |
+
{systemInfo.performance.navigation.type === 0
|
787 |
+
? 'Navigate'
|
788 |
+
: systemInfo.performance.navigation.type === 1
|
789 |
+
? 'Reload'
|
790 |
+
: systemInfo.performance.navigation.type === 2
|
791 |
+
? 'Back/Forward'
|
792 |
+
: 'Other'}
|
793 |
+
</span>
|
794 |
+
</div>
|
795 |
+
<div className="text-sm">
|
796 |
+
<span className="text-bolt-elements-textSecondary">Redirects: </span>
|
797 |
+
<span className="text-bolt-elements-textPrimary">
|
798 |
+
{systemInfo.performance.navigation.redirectCount}
|
799 |
+
</span>
|
800 |
+
</div>
|
801 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
802 |
</div>
|
803 |
+
)}
|
804 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
805 |
|
806 |
+
{/* Error Check */}
|
807 |
+
<div className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
|
808 |
+
<div className="flex items-center justify-between mb-4">
|
809 |
+
<div className="flex items-center gap-3">
|
810 |
+
<div className="i-ph:warning text-purple-500 w-5 h-5" />
|
811 |
+
<h3 className="text-base font-medium text-bolt-elements-textPrimary">Error Check</h3>
|
812 |
+
</div>
|
813 |
+
<button
|
814 |
+
onClick={handleCheckErrors}
|
815 |
+
className={classNames(
|
816 |
+
'flex items-center gap-2 px-3 py-2 rounded-lg text-sm',
|
817 |
+
'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
|
818 |
+
'hover:bg-[#E5E5E5] dark:hover:bg-[#252525]',
|
819 |
+
'transition-colors',
|
820 |
+
)}
|
821 |
+
>
|
822 |
+
<div className={classNames('i-ph:magnifying-glass w-4 h-4', loading.errors ? 'animate-spin' : '')} />
|
823 |
+
Check for Errors
|
824 |
+
</button>
|
825 |
+
</div>
|
826 |
+
<div className="space-y-4">
|
827 |
+
<div className="text-sm text-bolt-elements-textSecondary">
|
828 |
+
Checks for:
|
829 |
+
<ul className="list-disc list-inside mt-2 space-y-1">
|
830 |
+
<li>Unhandled JavaScript errors</li>
|
831 |
+
<li>Unhandled Promise rejections</li>
|
832 |
+
<li>Runtime exceptions</li>
|
833 |
+
<li>Network errors</li>
|
834 |
+
</ul>
|
835 |
+
</div>
|
836 |
+
<div className="text-sm">
|
837 |
+
<span className="text-bolt-elements-textSecondary">Last Check: </span>
|
838 |
+
<span className="text-bolt-elements-textPrimary">
|
839 |
+
{loading.errors
|
840 |
+
? 'Checking...'
|
841 |
+
: errorLog.lastCheck
|
842 |
+
? `Last checked ${new Date(errorLog.lastCheck).toLocaleString()} (${errorLog.errors.length} errors found)`
|
843 |
+
: 'Click to check for errors'}
|
844 |
+
</span>
|
845 |
+
</div>
|
846 |
+
{errorLog.errors.length > 0 && (
|
847 |
+
<div className="mt-4">
|
848 |
+
<div className="text-sm font-medium text-bolt-elements-textPrimary mb-2">Recent Errors:</div>
|
849 |
+
<div className="space-y-2">
|
850 |
+
{errorLog.errors.slice(0, 3).map((error, index) => (
|
851 |
+
<div key={index} className="text-sm text-red-500 dark:text-red-400">
|
852 |
+
{error.type === 'error' && `${error.message} (${error.filename}:${error.lineNumber})`}
|
853 |
+
{error.type === 'unhandledRejection' && `Unhandled Promise Rejection: ${error.reason}`}
|
854 |
+
{error.type === 'networkError' && `Network Error: Failed to load ${error.resource}`}
|
855 |
</div>
|
856 |
+
))}
|
857 |
+
{errorLog.errors.length > 3 && (
|
858 |
+
<div className="text-sm text-bolt-elements-textSecondary">
|
859 |
+
And {errorLog.errors.length - 3} more errors...
|
860 |
+
</div>
|
861 |
+
)}
|
862 |
+
</div>
|
863 |
</div>
|
864 |
+
)}
|
865 |
+
</div>
|
866 |
+
</div>
|
867 |
</div>
|
868 |
);
|
869 |
}
|
app/components/settings/developer/DeveloperWindow.tsx
ADDED
@@ -0,0 +1,378 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import * as RadixDialog from '@radix-ui/react-dialog';
|
2 |
+
import { motion } from 'framer-motion';
|
3 |
+
import { useState } from 'react';
|
4 |
+
import { classNames } from '~/utils/classNames';
|
5 |
+
import { TabManagement } from './TabManagement';
|
6 |
+
import { TabTile } from '~/components/settings/shared/TabTile';
|
7 |
+
import type { TabType, TabVisibilityConfig } from '~/components/settings/settings.types';
|
8 |
+
import { tabConfigurationStore, updateTabConfiguration } from '~/lib/stores/settings';
|
9 |
+
import { useStore } from '@nanostores/react';
|
10 |
+
import { DndProvider, useDrag, useDrop } from 'react-dnd';
|
11 |
+
import { HTML5Backend } from 'react-dnd-html5-backend';
|
12 |
+
import DebugTab from '~/components/settings/debug/DebugTab';
|
13 |
+
import { EventLogsTab } from '~/components/settings/event-logs/EventLogsTab';
|
14 |
+
import UpdateTab from '~/components/settings/update/UpdateTab';
|
15 |
+
import { ProvidersTab } from '~/components/settings/providers/ProvidersTab';
|
16 |
+
import DataTab from '~/components/settings/data/DataTab';
|
17 |
+
import FeaturesTab from '~/components/settings/features/FeaturesTab';
|
18 |
+
import NotificationsTab from '~/components/settings/notifications/NotificationsTab';
|
19 |
+
import SettingsTab from '~/components/settings/settings/SettingsTab';
|
20 |
+
import ProfileTab from '~/components/settings/profile/ProfileTab';
|
21 |
+
import ConnectionsTab from '~/components/settings/connections/ConnectionsTab';
|
22 |
+
import { useUpdateCheck, useFeatures, useNotifications, useConnectionStatus, useDebugStatus } from '~/lib/hooks';
|
23 |
+
|
24 |
+
interface DraggableTabTileProps {
|
25 |
+
tab: TabVisibilityConfig;
|
26 |
+
index: number;
|
27 |
+
moveTab: (dragIndex: number, hoverIndex: number) => void;
|
28 |
+
onClick: () => void;
|
29 |
+
isActive: boolean;
|
30 |
+
hasUpdate: boolean;
|
31 |
+
statusMessage: string;
|
32 |
+
description: string;
|
33 |
+
isLoading?: boolean;
|
34 |
+
}
|
35 |
+
|
36 |
+
const TAB_DESCRIPTIONS: Record<TabType, string> = {
|
37 |
+
profile: 'Manage your profile and account settings',
|
38 |
+
settings: 'Configure application preferences',
|
39 |
+
notifications: 'View and manage your notifications',
|
40 |
+
features: 'Explore new and upcoming features',
|
41 |
+
data: 'Manage your data and storage',
|
42 |
+
providers: 'Configure AI providers and models',
|
43 |
+
connection: 'Check connection status and settings',
|
44 |
+
debug: 'Debug tools and system information',
|
45 |
+
'event-logs': 'View system events and logs',
|
46 |
+
update: 'Check for updates and release notes',
|
47 |
+
};
|
48 |
+
|
49 |
+
const DraggableTabTile = ({
|
50 |
+
tab,
|
51 |
+
index,
|
52 |
+
moveTab,
|
53 |
+
onClick,
|
54 |
+
isActive,
|
55 |
+
hasUpdate,
|
56 |
+
statusMessage,
|
57 |
+
description,
|
58 |
+
isLoading,
|
59 |
+
}: DraggableTabTileProps) => {
|
60 |
+
const [{ isDragging }, drag] = useDrag({
|
61 |
+
type: 'tab',
|
62 |
+
item: { index },
|
63 |
+
collect: (monitor) => ({
|
64 |
+
isDragging: monitor.isDragging(),
|
65 |
+
}),
|
66 |
+
});
|
67 |
+
|
68 |
+
const [, drop] = useDrop({
|
69 |
+
accept: 'tab',
|
70 |
+
hover: (item: { index: number }) => {
|
71 |
+
if (item.index === index) {
|
72 |
+
return;
|
73 |
+
}
|
74 |
+
|
75 |
+
moveTab(item.index, index);
|
76 |
+
item.index = index;
|
77 |
+
},
|
78 |
+
});
|
79 |
+
|
80 |
+
return (
|
81 |
+
<div ref={(node) => drag(drop(node))} style={{ opacity: isDragging ? 0.5 : 1 }}>
|
82 |
+
<TabTile
|
83 |
+
tab={tab}
|
84 |
+
onClick={onClick}
|
85 |
+
isActive={isActive}
|
86 |
+
hasUpdate={hasUpdate}
|
87 |
+
statusMessage={statusMessage}
|
88 |
+
description={description}
|
89 |
+
isLoading={isLoading}
|
90 |
+
/>
|
91 |
+
</div>
|
92 |
+
);
|
93 |
+
};
|
94 |
+
|
95 |
+
interface DeveloperWindowProps {
|
96 |
+
open: boolean;
|
97 |
+
onClose: () => void;
|
98 |
+
}
|
99 |
+
|
100 |
+
export const DeveloperWindow = ({ open, onClose }: DeveloperWindowProps) => {
|
101 |
+
const tabConfiguration = useStore(tabConfigurationStore);
|
102 |
+
const [activeTab, setActiveTab] = useState<TabType | null>(null);
|
103 |
+
const [showTabManagement, setShowTabManagement] = useState(false);
|
104 |
+
const [loadingTab, setLoadingTab] = useState<TabType | null>(null);
|
105 |
+
|
106 |
+
// Status hooks
|
107 |
+
const { hasUpdate, currentVersion, acknowledgeUpdate } = useUpdateCheck();
|
108 |
+
const { hasNewFeatures, unviewedFeatures, acknowledgeAllFeatures } = useFeatures();
|
109 |
+
const { hasUnreadNotifications, unreadNotifications, markAllAsRead } = useNotifications();
|
110 |
+
const { hasConnectionIssues, currentIssue, acknowledgeIssue } = useConnectionStatus();
|
111 |
+
const { hasActiveWarnings, activeIssues, acknowledgeAllIssues } = useDebugStatus();
|
112 |
+
|
113 |
+
const handleBack = () => {
|
114 |
+
if (showTabManagement) {
|
115 |
+
setShowTabManagement(false);
|
116 |
+
} else if (activeTab) {
|
117 |
+
setActiveTab(null);
|
118 |
+
}
|
119 |
+
};
|
120 |
+
|
121 |
+
// Only show tabs that are assigned to the developer window AND are visible
|
122 |
+
const visibleDeveloperTabs = tabConfiguration.developerTabs
|
123 |
+
.filter((tab: TabVisibilityConfig) => tab.window === 'developer' && tab.visible)
|
124 |
+
.sort((a: TabVisibilityConfig, b: TabVisibilityConfig) => (a.order || 0) - (b.order || 0));
|
125 |
+
|
126 |
+
const moveTab = (dragIndex: number, hoverIndex: number) => {
|
127 |
+
const draggedTab = visibleDeveloperTabs[dragIndex];
|
128 |
+
const targetTab = visibleDeveloperTabs[hoverIndex];
|
129 |
+
|
130 |
+
// Update the order of the dragged and target tabs
|
131 |
+
const updatedDraggedTab = { ...draggedTab, order: targetTab.order };
|
132 |
+
const updatedTargetTab = { ...targetTab, order: draggedTab.order };
|
133 |
+
|
134 |
+
// Update both tabs in the store
|
135 |
+
updateTabConfiguration(updatedDraggedTab);
|
136 |
+
updateTabConfiguration(updatedTargetTab);
|
137 |
+
};
|
138 |
+
|
139 |
+
const handleTabClick = async (tabId: TabType) => {
|
140 |
+
setLoadingTab(tabId);
|
141 |
+
setActiveTab(tabId);
|
142 |
+
|
143 |
+
// Acknowledge the status based on tab type
|
144 |
+
switch (tabId) {
|
145 |
+
case 'update':
|
146 |
+
await acknowledgeUpdate();
|
147 |
+
break;
|
148 |
+
case 'features':
|
149 |
+
await acknowledgeAllFeatures();
|
150 |
+
break;
|
151 |
+
case 'notifications':
|
152 |
+
await markAllAsRead();
|
153 |
+
break;
|
154 |
+
case 'connection':
|
155 |
+
acknowledgeIssue();
|
156 |
+
break;
|
157 |
+
case 'debug':
|
158 |
+
await acknowledgeAllIssues();
|
159 |
+
break;
|
160 |
+
}
|
161 |
+
|
162 |
+
// Simulate loading time (remove this in production)
|
163 |
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
164 |
+
setLoadingTab(null);
|
165 |
+
};
|
166 |
+
|
167 |
+
const getTabComponent = () => {
|
168 |
+
switch (activeTab) {
|
169 |
+
case 'profile':
|
170 |
+
return <ProfileTab />;
|
171 |
+
case 'settings':
|
172 |
+
return <SettingsTab />;
|
173 |
+
case 'notifications':
|
174 |
+
return <NotificationsTab />;
|
175 |
+
case 'features':
|
176 |
+
return <FeaturesTab />;
|
177 |
+
case 'data':
|
178 |
+
return <DataTab />;
|
179 |
+
case 'providers':
|
180 |
+
return <ProvidersTab />;
|
181 |
+
case 'connection':
|
182 |
+
return <ConnectionsTab />;
|
183 |
+
case 'debug':
|
184 |
+
return <DebugTab />;
|
185 |
+
case 'event-logs':
|
186 |
+
return <EventLogsTab />;
|
187 |
+
case 'update':
|
188 |
+
return <UpdateTab />;
|
189 |
+
default:
|
190 |
+
return null;
|
191 |
+
}
|
192 |
+
};
|
193 |
+
|
194 |
+
const getTabUpdateStatus = (tabId: TabType): boolean => {
|
195 |
+
switch (tabId) {
|
196 |
+
case 'update':
|
197 |
+
return hasUpdate;
|
198 |
+
case 'features':
|
199 |
+
return hasNewFeatures;
|
200 |
+
case 'notifications':
|
201 |
+
return hasUnreadNotifications;
|
202 |
+
case 'connection':
|
203 |
+
return hasConnectionIssues;
|
204 |
+
case 'debug':
|
205 |
+
return hasActiveWarnings;
|
206 |
+
default:
|
207 |
+
return false;
|
208 |
+
}
|
209 |
+
};
|
210 |
+
|
211 |
+
const getStatusMessage = (tabId: TabType): string => {
|
212 |
+
switch (tabId) {
|
213 |
+
case 'update':
|
214 |
+
return `New update available (v${currentVersion})`;
|
215 |
+
case 'features':
|
216 |
+
return `${unviewedFeatures.length} new feature${unviewedFeatures.length === 1 ? '' : 's'} to explore`;
|
217 |
+
case 'notifications':
|
218 |
+
return `${unreadNotifications.length} unread notification${unreadNotifications.length === 1 ? '' : 's'}`;
|
219 |
+
case 'connection':
|
220 |
+
return currentIssue === 'disconnected'
|
221 |
+
? 'Connection lost'
|
222 |
+
: currentIssue === 'high-latency'
|
223 |
+
? 'High latency detected'
|
224 |
+
: 'Connection issues detected';
|
225 |
+
case 'debug': {
|
226 |
+
const warnings = activeIssues.filter((i) => i.type === 'warning').length;
|
227 |
+
const errors = activeIssues.filter((i) => i.type === 'error').length;
|
228 |
+
|
229 |
+
return `${warnings} warning${warnings === 1 ? '' : 's'}, ${errors} error${errors === 1 ? '' : 's'}`;
|
230 |
+
}
|
231 |
+
default:
|
232 |
+
return '';
|
233 |
+
}
|
234 |
+
};
|
235 |
+
|
236 |
+
return (
|
237 |
+
<DndProvider backend={HTML5Backend}>
|
238 |
+
<RadixDialog.Root open={open}>
|
239 |
+
<RadixDialog.Portal>
|
240 |
+
<div className="fixed inset-0 flex items-center justify-center z-[60]">
|
241 |
+
<RadixDialog.Overlay asChild>
|
242 |
+
<motion.div
|
243 |
+
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
|
244 |
+
initial={{ opacity: 0 }}
|
245 |
+
animate={{ opacity: 1 }}
|
246 |
+
exit={{ opacity: 0 }}
|
247 |
+
transition={{ duration: 0.2 }}
|
248 |
+
/>
|
249 |
+
</RadixDialog.Overlay>
|
250 |
+
<RadixDialog.Content aria-describedby={undefined} asChild>
|
251 |
+
<motion.div
|
252 |
+
className={classNames(
|
253 |
+
'relative',
|
254 |
+
'w-[1200px] h-[90vh]',
|
255 |
+
'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
|
256 |
+
'rounded-2xl shadow-2xl',
|
257 |
+
'border border-[#E5E5E5] dark:border-[#1A1A1A]',
|
258 |
+
'flex flex-col overflow-hidden',
|
259 |
+
'z-[61]',
|
260 |
+
)}
|
261 |
+
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
262 |
+
animate={{ opacity: 1, scale: 1, y: 0 }}
|
263 |
+
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
264 |
+
transition={{ duration: 0.2 }}
|
265 |
+
>
|
266 |
+
{/* Header */}
|
267 |
+
<div className="flex-none flex items-center justify-between px-6 py-4 border-b border-[#E5E5E5] dark:border-[#1A1A1A]">
|
268 |
+
<div className="flex items-center gap-4">
|
269 |
+
{(activeTab || showTabManagement) && (
|
270 |
+
<motion.button
|
271 |
+
onClick={handleBack}
|
272 |
+
className={classNames(
|
273 |
+
'flex items-center justify-center w-8 h-8 rounded-lg',
|
274 |
+
'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
|
275 |
+
'hover:bg-purple-500/10 dark:hover:bg-purple-500/20',
|
276 |
+
'group transition-all duration-200',
|
277 |
+
)}
|
278 |
+
whileHover={{ scale: 1.05 }}
|
279 |
+
whileTap={{ scale: 0.95 }}
|
280 |
+
>
|
281 |
+
<div className="i-ph:arrow-left w-4 h-4 text-bolt-elements-textSecondary group-hover:text-purple-500 transition-colors" />
|
282 |
+
</motion.button>
|
283 |
+
)}
|
284 |
+
<div className="flex items-center gap-3">
|
285 |
+
<motion.div
|
286 |
+
className="i-ph:code-fill w-5 h-5 text-purple-500"
|
287 |
+
initial={{ rotate: 0 }}
|
288 |
+
animate={{ rotate: 360 }}
|
289 |
+
transition={{
|
290 |
+
repeat: Infinity,
|
291 |
+
duration: 8,
|
292 |
+
ease: 'linear',
|
293 |
+
}}
|
294 |
+
/>
|
295 |
+
<h2 className="text-lg font-medium text-bolt-elements-textPrimary">
|
296 |
+
{showTabManagement ? 'Tab Management' : activeTab ? 'Developer Tools' : 'Developer Dashboard'}
|
297 |
+
</h2>
|
298 |
+
</div>
|
299 |
+
</div>
|
300 |
+
<div className="flex items-center gap-3">
|
301 |
+
{!showTabManagement && !activeTab && (
|
302 |
+
<motion.button
|
303 |
+
onClick={() => setShowTabManagement(true)}
|
304 |
+
className={classNames(
|
305 |
+
'px-3 py-1.5 rounded-lg text-sm',
|
306 |
+
'bg-purple-500/10 text-purple-500',
|
307 |
+
'hover:bg-purple-500/20',
|
308 |
+
'transition-colors duration-200',
|
309 |
+
)}
|
310 |
+
whileHover={{ scale: 1.02 }}
|
311 |
+
whileTap={{ scale: 0.98 }}
|
312 |
+
>
|
313 |
+
Manage Tabs
|
314 |
+
</motion.button>
|
315 |
+
)}
|
316 |
+
<motion.button
|
317 |
+
onClick={onClose}
|
318 |
+
className={classNames(
|
319 |
+
'flex items-center justify-center w-8 h-8 rounded-lg',
|
320 |
+
'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
|
321 |
+
'hover:bg-purple-500/10 dark:hover:bg-purple-500/20',
|
322 |
+
'group transition-all duration-200',
|
323 |
+
)}
|
324 |
+
whileHover={{ scale: 1.05 }}
|
325 |
+
whileTap={{ scale: 0.95 }}
|
326 |
+
>
|
327 |
+
<div className="i-ph:x w-4 h-4 text-bolt-elements-textSecondary group-hover:text-purple-500 transition-colors" />
|
328 |
+
</motion.button>
|
329 |
+
</div>
|
330 |
+
</div>
|
331 |
+
|
332 |
+
{/* Content */}
|
333 |
+
<div
|
334 |
+
className={classNames(
|
335 |
+
'flex-1',
|
336 |
+
'overflow-y-auto',
|
337 |
+
'hover:overflow-y-auto',
|
338 |
+
'scrollbar scrollbar-w-2',
|
339 |
+
'scrollbar-track-transparent',
|
340 |
+
'scrollbar-thumb-[#E5E5E5] hover:scrollbar-thumb-[#CCCCCC]',
|
341 |
+
'dark:scrollbar-thumb-[#333333] dark:hover:scrollbar-thumb-[#444444]',
|
342 |
+
'will-change-scroll',
|
343 |
+
'touch-auto',
|
344 |
+
)}
|
345 |
+
>
|
346 |
+
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="p-6">
|
347 |
+
{showTabManagement ? (
|
348 |
+
<TabManagement />
|
349 |
+
) : activeTab ? (
|
350 |
+
getTabComponent()
|
351 |
+
) : (
|
352 |
+
<div className="grid grid-cols-4 gap-4">
|
353 |
+
{visibleDeveloperTabs.map((tab: TabVisibilityConfig, index: number) => (
|
354 |
+
<DraggableTabTile
|
355 |
+
key={tab.id}
|
356 |
+
tab={tab}
|
357 |
+
index={index}
|
358 |
+
moveTab={moveTab}
|
359 |
+
onClick={() => handleTabClick(tab.id)}
|
360 |
+
isActive={activeTab === tab.id}
|
361 |
+
hasUpdate={getTabUpdateStatus(tab.id)}
|
362 |
+
statusMessage={getStatusMessage(tab.id)}
|
363 |
+
description={TAB_DESCRIPTIONS[tab.id]}
|
364 |
+
isLoading={loadingTab === tab.id}
|
365 |
+
/>
|
366 |
+
))}
|
367 |
+
</div>
|
368 |
+
)}
|
369 |
+
</motion.div>
|
370 |
+
</div>
|
371 |
+
</motion.div>
|
372 |
+
</RadixDialog.Content>
|
373 |
+
</div>
|
374 |
+
</RadixDialog.Portal>
|
375 |
+
</RadixDialog.Root>
|
376 |
+
</DndProvider>
|
377 |
+
);
|
378 |
+
};
|
app/components/settings/developer/TabManagement.tsx
ADDED
@@ -0,0 +1,315 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
10 |
+
const TAB_ICONS: Record<TabType, string> = {
|
11 |
+
profile: 'i-ph:user-circle-fill',
|
12 |
+
settings: 'i-ph:gear-six-fill',
|
13 |
+
notifications: 'i-ph:bell-fill',
|
14 |
+
features: 'i-ph:sparkle-fill',
|
15 |
+
data: 'i-ph:database-fill',
|
16 |
+
providers: 'i-ph:robot-fill',
|
17 |
+
connection: 'i-ph:plug-fill',
|
18 |
+
debug: 'i-ph:bug-fill',
|
19 |
+
'event-logs': 'i-ph:list-bullets-fill',
|
20 |
+
update: 'i-ph:arrow-clockwise-fill',
|
21 |
+
};
|
22 |
+
|
23 |
+
interface TabGroupProps {
|
24 |
+
title: string;
|
25 |
+
description?: string;
|
26 |
+
tabs: TabVisibilityConfig[];
|
27 |
+
onVisibilityChange: (tabId: TabType, enabled: boolean) => void;
|
28 |
+
targetWindow: 'user' | 'developer';
|
29 |
+
standardTabs: TabType[];
|
30 |
+
}
|
31 |
+
|
32 |
+
const TabGroup = ({ title, description, tabs, onVisibilityChange, targetWindow }: TabGroupProps) => {
|
33 |
+
// Split tabs into visible and hidden
|
34 |
+
const visibleTabs = tabs.filter((tab) => tab.visible).sort((a, b) => (a.order || 0) - (b.order || 0));
|
35 |
+
const hiddenTabs = tabs.filter((tab) => !tab.visible).sort((a, b) => (a.order || 0) - (b.order || 0));
|
36 |
+
|
37 |
+
return (
|
38 |
+
<div className="mb-8 rounded-xl bg-white/5 p-6 backdrop-blur-sm dark:bg-gray-800/30">
|
39 |
+
<div className="mb-6">
|
40 |
+
<h3 className="flex items-center gap-2 text-lg font-medium text-gray-900 dark:text-white">
|
41 |
+
<span className="i-ph:layout-fill h-5 w-5 text-purple-500" />
|
42 |
+
{title}
|
43 |
+
</h3>
|
44 |
+
{description && <p className="mt-1.5 text-sm text-gray-600 dark:text-gray-400">{description}</p>}
|
45 |
+
</div>
|
46 |
+
|
47 |
+
<div className="space-y-6">
|
48 |
+
<motion.div layout className="space-y-2">
|
49 |
+
{visibleTabs.map((tab) => (
|
50 |
+
<motion.div
|
51 |
+
key={tab.id}
|
52 |
+
layout
|
53 |
+
initial={{ opacity: 0, y: 20 }}
|
54 |
+
animate={{ opacity: 1, y: 0 }}
|
55 |
+
exit={{ opacity: 0, y: -20 }}
|
56 |
+
transition={{ duration: 0.2 }}
|
57 |
+
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"
|
58 |
+
>
|
59 |
+
<div className="flex items-center space-x-3">
|
60 |
+
<div
|
61 |
+
className={classNames(
|
62 |
+
TAB_ICONS[tab.id],
|
63 |
+
'h-5 w-5 transition-colors',
|
64 |
+
tab.id === 'profile'
|
65 |
+
? 'text-purple-500 dark:text-purple-400'
|
66 |
+
: 'text-gray-500 group-hover:text-purple-500 dark:text-gray-400 dark:group-hover:text-purple-400',
|
67 |
+
)}
|
68 |
+
/>
|
69 |
+
<span
|
70 |
+
className={classNames(
|
71 |
+
'text-sm font-medium transition-colors',
|
72 |
+
tab.id === 'profile'
|
73 |
+
? 'text-gray-900 dark:text-white'
|
74 |
+
: 'text-gray-700 group-hover:text-gray-900 dark:text-gray-300 dark:group-hover:text-white',
|
75 |
+
)}
|
76 |
+
>
|
77 |
+
{TAB_LABELS[tab.id]}
|
78 |
+
</span>
|
79 |
+
{tab.id === 'profile' && targetWindow === 'user' && (
|
80 |
+
<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">
|
81 |
+
Standard
|
82 |
+
</span>
|
83 |
+
)}
|
84 |
+
</div>
|
85 |
+
<div className="flex items-center space-x-4">
|
86 |
+
{targetWindow === 'user' ? (
|
87 |
+
<label className="relative inline-flex cursor-pointer items-center">
|
88 |
+
<input
|
89 |
+
type="checkbox"
|
90 |
+
checked={tab.visible}
|
91 |
+
onChange={(e) => onVisibilityChange(tab.id, e.target.checked)}
|
92 |
+
className="peer sr-only"
|
93 |
+
/>
|
94 |
+
<div
|
95 |
+
className={classNames(
|
96 |
+
'h-6 w-11 rounded-full bg-gray-200 transition-colors dark:bg-gray-700',
|
97 |
+
'after:absolute after:left-[2px] after:top-[2px]',
|
98 |
+
'after:h-5 after:w-5 after:rounded-full after:bg-white after:shadow-sm',
|
99 |
+
'after:transition-all after:content-[""]',
|
100 |
+
'peer-checked:bg-purple-500 peer-checked:after:translate-x-full',
|
101 |
+
'peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-purple-500/20',
|
102 |
+
)}
|
103 |
+
/>
|
104 |
+
</label>
|
105 |
+
) : (
|
106 |
+
<div className="text-sm text-gray-500 dark:text-gray-400">Always visible</div>
|
107 |
+
)}
|
108 |
+
</div>
|
109 |
+
</motion.div>
|
110 |
+
))}
|
111 |
+
</motion.div>
|
112 |
+
|
113 |
+
{hiddenTabs.length > 0 && (
|
114 |
+
<motion.div layout className="space-y-2">
|
115 |
+
<div className="flex items-center gap-2 text-sm font-medium text-gray-500 dark:text-gray-400">
|
116 |
+
<span className="i-ph:eye-slash-fill h-4 w-4" />
|
117 |
+
Hidden Tabs
|
118 |
+
</div>
|
119 |
+
{hiddenTabs.map((tab) => (
|
120 |
+
<motion.div
|
121 |
+
key={tab.id}
|
122 |
+
layout
|
123 |
+
initial={{ opacity: 0, y: 20 }}
|
124 |
+
animate={{ opacity: 1, y: 0 }}
|
125 |
+
exit={{ opacity: 0, y: -20 }}
|
126 |
+
transition={{ duration: 0.2 }}
|
127 |
+
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"
|
128 |
+
>
|
129 |
+
<div className="flex items-center space-x-3">
|
130 |
+
<div
|
131 |
+
className={classNames(
|
132 |
+
TAB_ICONS[tab.id],
|
133 |
+
'h-5 w-5 transition-colors',
|
134 |
+
'text-gray-400 group-hover:text-purple-500 dark:text-gray-500 dark:group-hover:text-purple-400',
|
135 |
+
)}
|
136 |
+
/>
|
137 |
+
<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">
|
138 |
+
{TAB_LABELS[tab.id]}
|
139 |
+
</span>
|
140 |
+
</div>
|
141 |
+
<div className="flex items-center space-x-4">
|
142 |
+
{targetWindow === 'user' && (
|
143 |
+
<label className="relative inline-flex cursor-pointer items-center">
|
144 |
+
<input
|
145 |
+
type="checkbox"
|
146 |
+
checked={tab.visible}
|
147 |
+
onChange={(e) => onVisibilityChange(tab.id, e.target.checked)}
|
148 |
+
className="peer sr-only"
|
149 |
+
/>
|
150 |
+
<div
|
151 |
+
className={classNames(
|
152 |
+
'h-6 w-11 rounded-full bg-gray-200 transition-colors dark:bg-gray-700',
|
153 |
+
'after:absolute after:left-[2px] after:top-[2px]',
|
154 |
+
'after:h-5 after:w-5 after:rounded-full after:bg-white after:shadow-sm',
|
155 |
+
'after:transition-all after:content-[""]',
|
156 |
+
'peer-checked:bg-purple-500 peer-checked:after:translate-x-full',
|
157 |
+
'peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-purple-500/20',
|
158 |
+
)}
|
159 |
+
/>
|
160 |
+
</label>
|
161 |
+
)}
|
162 |
+
</div>
|
163 |
+
</motion.div>
|
164 |
+
))}
|
165 |
+
</motion.div>
|
166 |
+
)}
|
167 |
+
</div>
|
168 |
+
</div>
|
169 |
+
);
|
170 |
+
};
|
171 |
+
|
172 |
+
export const TabManagement = () => {
|
173 |
+
const config = useStore(tabConfigurationStore);
|
174 |
+
const [searchQuery, setSearchQuery] = useState('');
|
175 |
+
|
176 |
+
// Define standard (visible by default) tabs for each window
|
177 |
+
const standardUserTabs: TabType[] = ['features', 'data', 'providers', 'connection', 'debug'];
|
178 |
+
const standardDeveloperTabs: TabType[] = [
|
179 |
+
'profile',
|
180 |
+
'settings',
|
181 |
+
'notifications',
|
182 |
+
'features',
|
183 |
+
'data',
|
184 |
+
'providers',
|
185 |
+
'connection',
|
186 |
+
'debug',
|
187 |
+
'event-logs',
|
188 |
+
'update',
|
189 |
+
];
|
190 |
+
|
191 |
+
const handleVisibilityChange = (tabId: TabType, enabled: boolean, targetWindow: 'user' | 'developer') => {
|
192 |
+
const tabs = targetWindow === 'user' ? config.userTabs : config.developerTabs;
|
193 |
+
const existingTab = tabs.find((tab) => tab.id === tabId);
|
194 |
+
|
195 |
+
const updatedTab: TabVisibilityConfig = existingTab
|
196 |
+
? {
|
197 |
+
...existingTab,
|
198 |
+
visible: enabled,
|
199 |
+
}
|
200 |
+
: {
|
201 |
+
id: tabId,
|
202 |
+
visible: enabled,
|
203 |
+
window: targetWindow,
|
204 |
+
order: tabs.length,
|
205 |
+
};
|
206 |
+
|
207 |
+
// Update the store
|
208 |
+
updateTabConfiguration(updatedTab);
|
209 |
+
|
210 |
+
// Show toast notification
|
211 |
+
toast.success(`${TAB_LABELS[tabId]} ${enabled ? 'enabled' : 'disabled'} in ${targetWindow} window`);
|
212 |
+
};
|
213 |
+
|
214 |
+
const handleResetToDefaults = () => {
|
215 |
+
resetTabConfiguration();
|
216 |
+
toast.success('Tab settings reset to defaults');
|
217 |
+
};
|
218 |
+
|
219 |
+
// Filter tabs based on search and window
|
220 |
+
const userTabs = config.userTabs.filter((tab) =>
|
221 |
+
TAB_LABELS[tab.id].toLowerCase().includes(searchQuery.toLowerCase()),
|
222 |
+
);
|
223 |
+
|
224 |
+
const developerTabs = config.developerTabs.filter((tab) =>
|
225 |
+
TAB_LABELS[tab.id].toLowerCase().includes(searchQuery.toLowerCase()),
|
226 |
+
);
|
227 |
+
|
228 |
+
return (
|
229 |
+
<div className="h-full overflow-y-auto px-6 py-6">
|
230 |
+
<div className="mb-8">
|
231 |
+
<div className="flex items-center justify-between">
|
232 |
+
<div>
|
233 |
+
<h2 className="flex items-center gap-2 text-xl font-semibold text-gray-900 dark:text-white">
|
234 |
+
<span className="i-ph:squares-four-fill h-6 w-6 text-purple-500" />
|
235 |
+
Tab Management
|
236 |
+
</h2>
|
237 |
+
<p className="mt-1.5 text-sm text-gray-600 dark:text-gray-400">
|
238 |
+
Configure which tabs are visible in the user and developer windows
|
239 |
+
</p>
|
240 |
+
</div>
|
241 |
+
<button
|
242 |
+
onClick={handleResetToDefaults}
|
243 |
+
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"
|
244 |
+
>
|
245 |
+
<span className="i-ph:arrow-counter-clockwise-fill h-4 w-4" />
|
246 |
+
Reset to Defaults
|
247 |
+
</button>
|
248 |
+
</div>
|
249 |
+
|
250 |
+
<div className="mt-6 flex items-center gap-4">
|
251 |
+
<div className="relative flex-1">
|
252 |
+
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
253 |
+
<span className="i-ph:magnifying-glass h-5 w-5 text-gray-400" />
|
254 |
+
</div>
|
255 |
+
<input
|
256 |
+
type="text"
|
257 |
+
value={searchQuery}
|
258 |
+
onChange={(e) => setSearchQuery(e.target.value)}
|
259 |
+
placeholder="Search tabs..."
|
260 |
+
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"
|
261 |
+
/>
|
262 |
+
</div>
|
263 |
+
</div>
|
264 |
+
</div>
|
265 |
+
|
266 |
+
<div className="space-y-8">
|
267 |
+
{/* User Window Section */}
|
268 |
+
<div className="rounded-xl border border-purple-100 bg-purple-50/50 p-1 dark:border-purple-500/10 dark:bg-purple-500/5">
|
269 |
+
<div className="rounded-lg bg-white p-6 dark:bg-gray-800">
|
270 |
+
<div className="mb-6 flex items-center gap-3">
|
271 |
+
<div className="rounded-lg bg-purple-100 p-2 dark:bg-purple-500/10">
|
272 |
+
<span className="i-ph:user-circle-fill h-5 w-5 text-purple-500 dark:text-purple-400" />
|
273 |
+
</div>
|
274 |
+
<div>
|
275 |
+
<h3 className="text-base font-medium text-gray-900 dark:text-white">User Window</h3>
|
276 |
+
<p className="text-sm text-gray-600 dark:text-gray-400">Configure tabs visible to regular users</p>
|
277 |
+
</div>
|
278 |
+
</div>
|
279 |
+
<TabGroup
|
280 |
+
title="User Interface"
|
281 |
+
description="Manage which tabs are visible in the user window"
|
282 |
+
tabs={userTabs}
|
283 |
+
onVisibilityChange={(tabId, enabled) => handleVisibilityChange(tabId, enabled, 'user')}
|
284 |
+
targetWindow="user"
|
285 |
+
standardTabs={standardUserTabs}
|
286 |
+
/>
|
287 |
+
</div>
|
288 |
+
</div>
|
289 |
+
|
290 |
+
{/* Developer Window Section */}
|
291 |
+
<div className="rounded-xl border border-blue-100 bg-blue-50/50 p-1 dark:border-blue-500/10 dark:bg-blue-500/5">
|
292 |
+
<div className="rounded-lg bg-white p-6 dark:bg-gray-800">
|
293 |
+
<div className="mb-6 flex items-center gap-3">
|
294 |
+
<div className="rounded-lg bg-blue-100 p-2 dark:bg-blue-500/10">
|
295 |
+
<span className="i-ph:code-fill h-5 w-5 text-blue-500 dark:text-blue-400" />
|
296 |
+
</div>
|
297 |
+
<div>
|
298 |
+
<h3 className="text-base font-medium text-gray-900 dark:text-white">Developer Window</h3>
|
299 |
+
<p className="text-sm text-gray-600 dark:text-gray-400">Configure tabs visible to developers</p>
|
300 |
+
</div>
|
301 |
+
</div>
|
302 |
+
<TabGroup
|
303 |
+
title="Developer Interface"
|
304 |
+
description="Manage which tabs are visible in the developer window"
|
305 |
+
tabs={developerTabs}
|
306 |
+
onVisibilityChange={(tabId, enabled) => handleVisibilityChange(tabId, enabled, 'developer')}
|
307 |
+
targetWindow="developer"
|
308 |
+
standardTabs={standardDeveloperTabs}
|
309 |
+
/>
|
310 |
+
</div>
|
311 |
+
</div>
|
312 |
+
</div>
|
313 |
+
</div>
|
314 |
+
);
|
315 |
+
};
|
app/components/settings/event-logs/EventLogsTab.tsx
CHANGED
@@ -1,151 +1,304 @@
|
|
1 |
import React, { useCallback, useEffect, useState, useMemo, useRef } from 'react';
|
2 |
-
import { useSettings } from '~/lib/hooks/useSettings';
|
3 |
import { toast } from 'react-toastify';
|
4 |
import { Switch } from '~/components/ui/Switch';
|
5 |
import { logStore, type LogEntry } from '~/lib/stores/logs';
|
6 |
import { useStore } from '@nanostores/react';
|
7 |
import { classNames } from '~/utils/classNames';
|
8 |
import { motion } from 'framer-motion';
|
9 |
-
import { settingsStyles } from '~/components/settings/settings.styles';
|
10 |
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
const [searchQuery, setSearchQuery] = useState('');
|
18 |
-
const [, forceUpdate] = useState({});
|
19 |
-
const logsContainerRef = useRef<HTMLDivElement>(null);
|
20 |
-
const [isScrolledToBottom, setIsScrolledToBottom] = useState(true);
|
21 |
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
JSON.stringify(log.details)?.toLowerCase()?.includes(searchQuery?.toLowerCase());
|
30 |
|
31 |
-
|
32 |
-
|
|
|
|
|
|
|
|
|
|
|
33 |
|
34 |
-
|
35 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
36 |
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
50 |
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
58 |
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
|
|
|
|
|
|
|
|
|
|
65 |
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
defaultProvider: 'OpenAI',
|
70 |
-
status: 'operational',
|
71 |
-
});
|
72 |
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
prefersDarkMode: window.matchMedia('(prefers-color-scheme: dark)').matches,
|
78 |
-
});
|
79 |
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
timestamp: new Date().toISOString(),
|
85 |
});
|
|
|
86 |
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
});
|
93 |
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
lastAttempt: new Date().toISOString(),
|
99 |
-
statusCode: 408,
|
100 |
});
|
101 |
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
108 |
});
|
|
|
|
|
|
|
|
|
109 |
}
|
110 |
-
}, []);
|
111 |
|
112 |
-
|
113 |
-
|
114 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
115 |
|
116 |
-
|
117 |
-
|
118 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
119 |
|
120 |
-
|
121 |
-
|
122 |
-
|
123 |
-
|
124 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
125 |
|
126 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
127 |
|
128 |
-
|
129 |
-
|
130 |
-
|
131 |
|
132 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
133 |
}, []);
|
134 |
|
135 |
-
|
136 |
-
|
137 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
138 |
|
139 |
-
|
140 |
-
|
141 |
-
|
142 |
-
|
|
|
143 |
|
144 |
const handleClearLogs = useCallback(() => {
|
145 |
if (confirm('Are you sure you want to clear all logs?')) {
|
146 |
logStore.clearLogs();
|
147 |
toast.success('Logs cleared successfully');
|
148 |
-
forceUpdate({}); // Force a re-render after clearing logs
|
149 |
}
|
150 |
}, []);
|
151 |
|
@@ -177,223 +330,188 @@ export default function EventLogsTab() {
|
|
177 |
}
|
178 |
}, []);
|
179 |
|
180 |
-
const
|
181 |
-
|
182 |
-
|
183 |
-
|
184 |
-
|
185 |
-
return 'i-ph:warning';
|
186 |
-
case 'error':
|
187 |
-
return 'i-ph:x-circle';
|
188 |
-
case 'debug':
|
189 |
-
return 'i-ph:bug';
|
190 |
-
default:
|
191 |
-
return 'i-ph:circle';
|
192 |
}
|
|
|
|
|
|
|
|
|
193 |
};
|
194 |
|
195 |
-
|
196 |
-
|
197 |
-
|
198 |
-
|
199 |
-
|
200 |
-
return 'text-[#FFDB6C] dark:text-[#FFDB6C]';
|
201 |
-
case 'error':
|
202 |
-
return 'text-[#EE4744] dark:text-[#EE4744]';
|
203 |
-
case 'debug':
|
204 |
-
return 'text-[#77828D] dark:text-[#77828D]';
|
205 |
-
default:
|
206 |
-
return 'text-bolt-elements-textPrimary';
|
207 |
}
|
208 |
-
};
|
209 |
|
210 |
return (
|
211 |
-
<div className="
|
212 |
-
|
213 |
-
|
214 |
-
|
215 |
-
|
216 |
-
|
|
|
217 |
<div>
|
218 |
-
<
|
219 |
<p className="text-sm text-bolt-elements-textSecondary">Track system events and debug information</p>
|
220 |
</div>
|
221 |
</div>
|
222 |
-
<
|
223 |
-
|
224 |
-
|
225 |
-
|
226 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
227 |
</div>
|
228 |
-
|
229 |
-
<div className="i-ph:arrow-clockwise text-bolt-elements-textSecondary" />
|
230 |
-
<span className="text-sm text-bolt-elements-textSecondary whitespace-nowrap">Auto-scroll</span>
|
231 |
-
<Switch checked={autoScroll} onCheckedChange={setAutoScroll} />
|
232 |
-
</div>
|
233 |
-
</div>
|
234 |
</div>
|
235 |
|
236 |
-
{/* Controls
|
237 |
-
<div className="flex
|
238 |
-
<div className="flex-1
|
239 |
-
<div
|
240 |
-
|
241 |
-
|
242 |
-
|
243 |
-
|
244 |
-
|
245 |
-
|
246 |
-
|
247 |
-
|
248 |
-
|
249 |
-
'transition-all duration-200',
|
250 |
-
)}
|
251 |
-
>
|
252 |
-
<option value="all">All Levels</option>
|
253 |
-
<option value="info">Info</option>
|
254 |
-
<option value="warning">Warning</option>
|
255 |
-
<option value="error">Error</option>
|
256 |
-
<option value="debug">Debug</option>
|
257 |
-
</select>
|
258 |
-
<div className="i-ph:funnel absolute left-3 top-1/2 -translate-y-1/2 text-bolt-elements-textSecondary group-hover:text-purple-500 transition-colors" />
|
259 |
-
</div>
|
260 |
-
</div>
|
261 |
-
<div className="flex-1 min-w-[200px]">
|
262 |
-
<div className="relative group">
|
263 |
-
<input
|
264 |
-
type="text"
|
265 |
-
placeholder="Search logs..."
|
266 |
-
value={searchQuery}
|
267 |
-
onChange={(e) => setSearchQuery(e.target.value)}
|
268 |
-
className={classNames(
|
269 |
-
'w-full pl-9 pr-3 py-2 rounded-lg',
|
270 |
-
'bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor',
|
271 |
-
'text-sm text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
|
272 |
-
'focus:outline-none focus:ring-2 focus:ring-purple-500/30',
|
273 |
-
'group-hover:border-purple-500/30',
|
274 |
-
'transition-all duration-200',
|
275 |
-
)}
|
276 |
/>
|
277 |
-
|
278 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
279 |
</div>
|
280 |
-
{showLogs && (
|
281 |
-
<div className="flex items-center gap-2 flex-nowrap">
|
282 |
-
<motion.button
|
283 |
-
onClick={handleExportLogs}
|
284 |
-
className={classNames(settingsStyles.button.base, settingsStyles.button.primary, 'group')}
|
285 |
-
whileHover={{ scale: 1.02 }}
|
286 |
-
whileTap={{ scale: 0.98 }}
|
287 |
-
>
|
288 |
-
<div className="i-ph:download-simple group-hover:scale-110 transition-transform" />
|
289 |
-
Export Logs
|
290 |
-
</motion.button>
|
291 |
-
<motion.button
|
292 |
-
onClick={handleClearLogs}
|
293 |
-
className={classNames(settingsStyles.button.base, settingsStyles.button.danger, 'group')}
|
294 |
-
whileHover={{ scale: 1.02 }}
|
295 |
-
whileTap={{ scale: 0.98 }}
|
296 |
-
>
|
297 |
-
<div className="i-ph:trash group-hover:scale-110 transition-transform" />
|
298 |
-
Clear Logs
|
299 |
-
</motion.button>
|
300 |
-
</div>
|
301 |
-
)}
|
302 |
</div>
|
303 |
</div>
|
304 |
|
305 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
306 |
ref={logsContainerRef}
|
307 |
-
className=
|
308 |
-
|
309 |
-
'h-[calc(100vh-250px)] min-h-[400px] overflow-y-auto logs-container',
|
310 |
-
'scrollbar-thin scrollbar-thumb-bolt-elements-borderColor scrollbar-track-transparent hover:scrollbar-thumb-purple-500/30',
|
311 |
-
)}
|
312 |
-
initial={{ opacity: 0, y: 20 }}
|
313 |
-
animate={{ opacity: 1, y: 0 }}
|
314 |
>
|
315 |
-
|
316 |
-
|
317 |
-
<
|
318 |
-
|
319 |
-
|
320 |
-
|
321 |
-
|
322 |
-
|
323 |
-
|
324 |
-
|
325 |
-
|
326 |
-
|
327 |
-
|
328 |
-
|
329 |
-
|
330 |
-
|
331 |
-
|
332 |
-
|
333 |
-
|
334 |
-
|
335 |
-
|
336 |
-
|
337 |
-
|
338 |
-
|
339 |
-
|
340 |
-
|
341 |
-
|
342 |
-
|
343 |
-
|
344 |
-
|
345 |
-
|
346 |
-
|
347 |
-
|
348 |
-
|
349 |
-
getLevelColor(log.level),
|
350 |
-
'mt-1 flex-shrink-0 text-lg',
|
351 |
-
)}
|
352 |
-
/>
|
353 |
-
<div className="flex-1 min-w-0">
|
354 |
-
<div className="flex items-center gap-2 flex-wrap">
|
355 |
-
<span
|
356 |
-
className={classNames(
|
357 |
-
'font-bold whitespace-nowrap px-2 py-0.5 rounded-full text-xs',
|
358 |
-
{
|
359 |
-
'bg-blue-500/10': log.level === 'info',
|
360 |
-
'bg-yellow-500/10': log.level === 'warning',
|
361 |
-
'bg-red-500/10': log.level === 'error',
|
362 |
-
'bg-bolt-elements-textSecondary/10': log.level === 'debug',
|
363 |
-
},
|
364 |
-
getLevelColor(log.level),
|
365 |
-
)}
|
366 |
-
>
|
367 |
-
{log.level.toUpperCase()}
|
368 |
-
</span>
|
369 |
-
<span className="text-bolt-elements-textSecondary whitespace-nowrap text-xs">
|
370 |
-
{new Date(log.timestamp).toLocaleString()}
|
371 |
-
</span>
|
372 |
-
<span className="text-bolt-elements-textPrimary break-all">{log.message}</span>
|
373 |
-
</div>
|
374 |
-
{log.details && (
|
375 |
-
<motion.pre
|
376 |
-
initial={{ opacity: 0, height: 0 }}
|
377 |
-
animate={{ opacity: 1, height: 'auto' }}
|
378 |
-
transition={{ duration: 0.2 }}
|
379 |
-
className={classNames(
|
380 |
-
'mt-2 text-xs',
|
381 |
-
'overflow-x-auto whitespace-pre-wrap break-all',
|
382 |
-
'bg-[#1A1A1A] dark:bg-[#0A0A0A] rounded-md p-3',
|
383 |
-
'border border-[#333333] dark:border-[#1A1A1A]',
|
384 |
-
'text-[#666666] dark:text-[#999999]',
|
385 |
-
)}
|
386 |
-
>
|
387 |
-
{JSON.stringify(log.details, null, 2)}
|
388 |
-
</motion.pre>
|
389 |
-
)}
|
390 |
-
</div>
|
391 |
-
</div>
|
392 |
-
</motion.div>
|
393 |
-
))}
|
394 |
-
</div>
|
395 |
-
)}
|
396 |
-
</motion.div>
|
397 |
</div>
|
398 |
);
|
399 |
}
|
|
|
1 |
import React, { useCallback, useEffect, useState, useMemo, useRef } from 'react';
|
|
|
2 |
import { toast } from 'react-toastify';
|
3 |
import { Switch } from '~/components/ui/Switch';
|
4 |
import { logStore, type LogEntry } from '~/lib/stores/logs';
|
5 |
import { useStore } from '@nanostores/react';
|
6 |
import { classNames } from '~/utils/classNames';
|
7 |
import { motion } from 'framer-motion';
|
|
|
8 |
|
9 |
+
interface SelectOption {
|
10 |
+
value: string;
|
11 |
+
label: string;
|
12 |
+
icon: string;
|
13 |
+
color?: string;
|
14 |
+
}
|
|
|
|
|
|
|
|
|
15 |
|
16 |
+
const logLevelOptions: SelectOption[] = [
|
17 |
+
{ value: 'all', label: 'All Levels', icon: 'i-ph:funnel' },
|
18 |
+
{ value: 'info', label: 'Info', icon: 'i-ph:info', color: 'text-blue-500' },
|
19 |
+
{ value: 'warning', label: 'Warning', icon: 'i-ph:warning', color: 'text-yellow-500' },
|
20 |
+
{ value: 'error', label: 'Error', icon: 'i-ph:x-circle', color: 'text-red-500' },
|
21 |
+
{ value: 'debug', label: 'Debug', icon: 'i-ph:bug', color: 'text-gray-500' },
|
22 |
+
];
|
|
|
23 |
|
24 |
+
const logCategoryOptions: SelectOption[] = [
|
25 |
+
{ value: 'all', label: 'All Categories', icon: 'i-ph:squares-four' },
|
26 |
+
{ value: 'system', label: 'System', icon: 'i-ph:desktop' },
|
27 |
+
{ value: 'provider', label: 'Provider', icon: 'i-ph:plug' },
|
28 |
+
{ value: 'user', label: 'User', icon: 'i-ph:user' },
|
29 |
+
{ value: 'error', label: 'Error', icon: 'i-ph:warning-octagon' },
|
30 |
+
];
|
31 |
|
32 |
+
const SegmentedGroup = ({
|
33 |
+
value,
|
34 |
+
onChange,
|
35 |
+
options,
|
36 |
+
className,
|
37 |
+
}: {
|
38 |
+
value: string;
|
39 |
+
onChange: (value: string) => void;
|
40 |
+
options: SelectOption[];
|
41 |
+
className?: string;
|
42 |
+
}) => {
|
43 |
+
const [isExpanded, setIsExpanded] = useState(false);
|
44 |
+
const selectedOption = options.find((opt) => opt.value === value);
|
45 |
|
46 |
+
if (!isExpanded) {
|
47 |
+
return (
|
48 |
+
<button
|
49 |
+
type="button"
|
50 |
+
onClick={() => setIsExpanded(true)}
|
51 |
+
className={classNames(
|
52 |
+
'flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm',
|
53 |
+
'bg-white/50 dark:bg-gray-800/30',
|
54 |
+
'hover:bg-gray-50 dark:hover:bg-gray-800/50',
|
55 |
+
'border border-gray-200/50 dark:border-gray-700/50',
|
56 |
+
'text-bolt-elements-textPrimary',
|
57 |
+
className,
|
58 |
+
)}
|
59 |
+
>
|
60 |
+
<div className={classNames(selectedOption?.icon, 'text-base text-purple-500')} />
|
61 |
+
<span className="text-sm">{selectedOption?.label}</span>
|
62 |
+
<div className="i-ph:caret-right text-sm text-bolt-elements-textTertiary" />
|
63 |
+
</button>
|
64 |
+
);
|
65 |
+
}
|
66 |
|
67 |
+
return (
|
68 |
+
<div className="flex items-center gap-0.5 p-0.5 rounded-lg bg-white/50 dark:bg-gray-800/30 border border-gray-200/50 dark:border-gray-700/50">
|
69 |
+
{options.map((option) => (
|
70 |
+
<button
|
71 |
+
key={option.value}
|
72 |
+
type="button"
|
73 |
+
onClick={() => {
|
74 |
+
onChange(option.value);
|
75 |
+
setIsExpanded(false);
|
76 |
+
}}
|
77 |
+
className={classNames(
|
78 |
+
'flex items-center gap-2 px-3 py-1.5 rounded-md text-sm transition-colors',
|
79 |
+
option.value === value
|
80 |
+
? 'bg-purple-100 dark:bg-purple-800/40 text-purple-900 dark:text-purple-200'
|
81 |
+
: 'text-bolt-elements-textSecondary hover:bg-gray-50 dark:hover:bg-gray-800/50',
|
82 |
+
)}
|
83 |
+
>
|
84 |
+
<div className={classNames(option.icon, 'text-base', option.value === value ? option.color : '')} />
|
85 |
+
<span className="truncate">{option.label}</span>
|
86 |
+
</button>
|
87 |
+
))}
|
88 |
+
</div>
|
89 |
+
);
|
90 |
+
};
|
91 |
|
92 |
+
const LogEntryItem = ({
|
93 |
+
log,
|
94 |
+
isExpanded: forceExpanded,
|
95 |
+
use24Hour,
|
96 |
+
}: {
|
97 |
+
log: LogEntry;
|
98 |
+
isExpanded: boolean;
|
99 |
+
use24Hour: boolean;
|
100 |
+
}) => {
|
101 |
+
const [isExpanded, setIsExpanded] = useState(forceExpanded);
|
102 |
+
const [isCopied, setIsCopied] = useState(false);
|
103 |
|
104 |
+
useEffect(() => {
|
105 |
+
setIsExpanded(forceExpanded);
|
106 |
+
}, [forceExpanded]);
|
|
|
|
|
|
|
107 |
|
108 |
+
const handleCopy = useCallback(() => {
|
109 |
+
const logText = `[${log.level.toUpperCase()}] ${log.message}\nTimestamp: ${new Date(
|
110 |
+
log.timestamp,
|
111 |
+
).toLocaleString()}\nCategory: ${log.category}\nDetails: ${JSON.stringify(log.details, null, 2)}`;
|
|
|
|
|
112 |
|
113 |
+
navigator.clipboard.writeText(logText).then(() => {
|
114 |
+
setIsCopied(true);
|
115 |
+
toast.success('Log copied to clipboard');
|
116 |
+
setTimeout(() => setIsCopied(false), 2000);
|
|
|
117 |
});
|
118 |
+
}, [log]);
|
119 |
|
120 |
+
const formattedTime = useMemo(() => {
|
121 |
+
const date = new Date(log.timestamp);
|
122 |
+
const now = new Date();
|
123 |
+
const isToday = date.toDateString() === now.toDateString();
|
124 |
+
const isYesterday = new Date(now.setDate(now.getDate() - 1)).toDateString() === date.toDateString();
|
|
|
125 |
|
126 |
+
const timeStr = date.toLocaleTimeString(undefined, {
|
127 |
+
hour: '2-digit',
|
128 |
+
minute: '2-digit',
|
129 |
+
hour12: !use24Hour,
|
|
|
|
|
130 |
});
|
131 |
|
132 |
+
if (isToday) {
|
133 |
+
return {
|
134 |
+
primary: timeStr,
|
135 |
+
secondary: 'Today',
|
136 |
+
};
|
137 |
+
} else if (isYesterday) {
|
138 |
+
return {
|
139 |
+
primary: timeStr,
|
140 |
+
secondary: 'Yesterday',
|
141 |
+
};
|
142 |
+
} else {
|
143 |
+
const dateStr = date.toLocaleDateString(undefined, {
|
144 |
+
month: 'short',
|
145 |
+
day: 'numeric',
|
146 |
+
year: date.getFullYear() !== now.getFullYear() ? 'numeric' : undefined,
|
147 |
});
|
148 |
+
return {
|
149 |
+
primary: dateStr,
|
150 |
+
secondary: timeStr,
|
151 |
+
};
|
152 |
}
|
153 |
+
}, [log.timestamp, use24Hour]);
|
154 |
|
155 |
+
return (
|
156 |
+
<div
|
157 |
+
className={classNames('group transition-colors', 'hover:bg-gray-50 dark:hover:bg-gray-800/50', 'py-3', {
|
158 |
+
'bg-red-50/20 dark:bg-red-900/5': log.level === 'error',
|
159 |
+
'bg-yellow-50/20 dark:bg-yellow-900/5': log.level === 'warning',
|
160 |
+
'bg-blue-50/20 dark:bg-blue-900/5': log.level === 'info',
|
161 |
+
'bg-gray-50/20 dark:bg-gray-800/5': log.level === 'debug',
|
162 |
+
})}
|
163 |
+
>
|
164 |
+
<div className="px-3">
|
165 |
+
<div className="flex items-center gap-3">
|
166 |
+
<span
|
167 |
+
className={classNames('px-2 py-0.5 text-xs font-medium rounded-full', {
|
168 |
+
'bg-red-100/80 text-red-800 dark:bg-red-500/10 dark:text-red-400': log.level === 'error',
|
169 |
+
'bg-yellow-100/80 text-yellow-800 dark:bg-yellow-500/10 dark:text-yellow-400': log.level === 'warning',
|
170 |
+
'bg-blue-100/80 text-blue-800 dark:bg-blue-500/10 dark:text-blue-400': log.level === 'info',
|
171 |
+
'bg-gray-100/80 text-gray-800 dark:bg-gray-500/10 dark:text-gray-400': log.level === 'debug',
|
172 |
+
})}
|
173 |
+
>
|
174 |
+
{log.level}
|
175 |
+
</span>
|
176 |
+
<p className="flex-1 text-sm text-bolt-elements-textPrimary">{log.message}</p>
|
177 |
+
<div className="flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
178 |
+
<button onClick={handleCopy} className="p-1 transition-colors rounded focus:outline-none" title="Copy log">
|
179 |
+
<div
|
180 |
+
className={classNames(
|
181 |
+
'text-base transition-colors',
|
182 |
+
isCopied
|
183 |
+
? 'i-ph:check text-green-500'
|
184 |
+
: 'i-ph:copy text-bolt-elements-textTertiary hover:text-bolt-elements-textSecondary',
|
185 |
+
)}
|
186 |
+
/>
|
187 |
+
</button>
|
188 |
+
{log.details && (
|
189 |
+
<button
|
190 |
+
onClick={() => setIsExpanded(!isExpanded)}
|
191 |
+
className="p-1 text-bolt-elements-textTertiary hover:text-bolt-elements-textSecondary transition-colors rounded focus:outline-none"
|
192 |
+
title="Toggle details"
|
193 |
+
>
|
194 |
+
<div
|
195 |
+
className={classNames('text-base transition-transform', {
|
196 |
+
'i-ph:caret-down rotate-180': isExpanded,
|
197 |
+
'i-ph:caret-down': !isExpanded,
|
198 |
+
})}
|
199 |
+
/>
|
200 |
+
</button>
|
201 |
+
)}
|
202 |
+
</div>
|
203 |
+
</div>
|
204 |
+
<div className="flex items-center gap-2 mt-1 text-xs">
|
205 |
+
<div className="flex items-center gap-1">
|
206 |
+
<div className="i-ph:clock text-bolt-elements-textTertiary" />
|
207 |
+
<span className="text-bolt-elements-textSecondary">{formattedTime.primary}</span>
|
208 |
+
<span className="text-bolt-elements-textTertiary">·</span>
|
209 |
+
<span className="text-bolt-elements-textTertiary">{formattedTime.secondary}</span>
|
210 |
+
</div>
|
211 |
+
<span className="px-2 py-0.5 rounded-full bg-bolt-elements-background-depth-2 dark:bg-bolt-elements-background-depth-3">
|
212 |
+
{log.category}
|
213 |
+
</span>
|
214 |
+
</div>
|
215 |
+
</div>
|
216 |
|
217 |
+
{isExpanded && log.details && (
|
218 |
+
<motion.div
|
219 |
+
initial={{ height: 0, opacity: 0 }}
|
220 |
+
animate={{ height: 'auto', opacity: 1 }}
|
221 |
+
exit={{ height: 0, opacity: 0 }}
|
222 |
+
transition={{ duration: 0.2 }}
|
223 |
+
className="mt-2 px-3"
|
224 |
+
>
|
225 |
+
<pre className="p-2 text-sm rounded-md overflow-auto bg-bolt-elements-background-depth-2/50 dark:bg-bolt-elements-background-depth-3/50 text-bolt-elements-textSecondary">
|
226 |
+
{JSON.stringify(log.details, null, 2)}
|
227 |
+
</pre>
|
228 |
+
</motion.div>
|
229 |
+
)}
|
230 |
+
</div>
|
231 |
+
);
|
232 |
+
};
|
233 |
|
234 |
+
/**
|
235 |
+
* TODO: Future Enhancements
|
236 |
+
*
|
237 |
+
* 1. Advanced Features:
|
238 |
+
* - Add export to JSON/CSV functionality
|
239 |
+
* - Implement log retention policies
|
240 |
+
* - Add custom alert rules and notifications
|
241 |
+
* - Add pattern detection and highlighting
|
242 |
+
*
|
243 |
+
* 2. Visual Improvements:
|
244 |
+
* - Add dark/light mode specific styling
|
245 |
+
* - Implement collapsible JSON viewer
|
246 |
+
* - Add timeline view with zoom capabilities
|
247 |
+
*
|
248 |
+
* 3. Performance Optimizations:
|
249 |
+
* - Implement virtualized scrolling for large logs
|
250 |
+
* - Add lazy loading for log details
|
251 |
+
* - Optimize search with indexing
|
252 |
+
*/
|
253 |
|
254 |
+
export function EventLogsTab() {
|
255 |
+
const logs = useStore(logStore.logs);
|
256 |
+
const [logLevel, setLogLevel] = useState<LogEntry['level'] | 'all'>('all');
|
257 |
+
const [logCategory, setLogCategory] = useState<LogEntry['category'] | 'all'>('all');
|
258 |
+
const [autoScroll, setAutoScroll] = useState(true);
|
259 |
+
const [searchQuery, setSearchQuery] = useState('');
|
260 |
+
const [expandAll, setExpandAll] = useState(false);
|
261 |
+
const [use24Hour, setUse24Hour] = useState(true);
|
262 |
+
const [isRefreshing, setIsRefreshing] = useState(false);
|
263 |
+
const logsContainerRef = useRef<HTMLDivElement>(null);
|
264 |
+
const [isScrolledToBottom, setIsScrolledToBottom] = useState(true);
|
265 |
|
266 |
+
// Add refresh function
|
267 |
+
const handleRefresh = useCallback(async () => {
|
268 |
+
setIsRefreshing(true);
|
269 |
|
270 |
+
try {
|
271 |
+
// Since logStore doesn't have refresh, we'll re-fetch logs
|
272 |
+
await new Promise((resolve) => setTimeout(resolve, 500)); // Simulate refresh
|
273 |
+
toast.success('Logs refreshed');
|
274 |
+
} catch (err) {
|
275 |
+
console.error('Failed to refresh logs:', err);
|
276 |
+
toast.error('Failed to refresh logs');
|
277 |
+
} finally {
|
278 |
+
setIsRefreshing(false);
|
279 |
+
}
|
280 |
}, []);
|
281 |
|
282 |
+
const filteredLogs = useMemo(() => {
|
283 |
+
const allLogs = Object.values(logs);
|
284 |
+
const filtered = allLogs.filter((log) => {
|
285 |
+
const matchesLevel = logLevel === 'all' || log.level === logLevel;
|
286 |
+
const matchesCategory = logCategory === 'all' || log.category === logCategory;
|
287 |
+
const matchesSearch =
|
288 |
+
!searchQuery ||
|
289 |
+
log.message?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
290 |
+
JSON.stringify(log.details)?.toLowerCase()?.includes(searchQuery?.toLowerCase());
|
291 |
|
292 |
+
return matchesLevel && matchesCategory && matchesSearch;
|
293 |
+
});
|
294 |
+
|
295 |
+
return filtered.reverse();
|
296 |
+
}, [logs, logLevel, logCategory, searchQuery]);
|
297 |
|
298 |
const handleClearLogs = useCallback(() => {
|
299 |
if (confirm('Are you sure you want to clear all logs?')) {
|
300 |
logStore.clearLogs();
|
301 |
toast.success('Logs cleared successfully');
|
|
|
302 |
}
|
303 |
}, []);
|
304 |
|
|
|
330 |
}
|
331 |
}, []);
|
332 |
|
333 |
+
const handleScroll = () => {
|
334 |
+
const container = logsContainerRef.current;
|
335 |
+
|
336 |
+
if (!container) {
|
337 |
+
return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
338 |
}
|
339 |
+
|
340 |
+
const { scrollTop, scrollHeight, clientHeight } = container;
|
341 |
+
const isBottom = Math.abs(scrollHeight - clientHeight - scrollTop) < 10;
|
342 |
+
setIsScrolledToBottom(isBottom);
|
343 |
};
|
344 |
|
345 |
+
useEffect(() => {
|
346 |
+
const container = logsContainerRef.current;
|
347 |
+
|
348 |
+
if (container && (autoScroll || isScrolledToBottom)) {
|
349 |
+
container.scrollTop = container.scrollHeight;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
350 |
}
|
351 |
+
}, [filteredLogs, autoScroll, isScrolledToBottom]);
|
352 |
|
353 |
return (
|
354 |
+
<div className="flex flex-col h-full gap-4 p-6">
|
355 |
+
{/* Header Section */}
|
356 |
+
<div className="flex flex-col gap-4 pb-4">
|
357 |
+
{/* Title and Refresh */}
|
358 |
+
<div className="flex items-center justify-between">
|
359 |
+
<div className="flex items-center gap-3">
|
360 |
+
<div className="i-ph:list text-xl text-purple-500" />
|
361 |
<div>
|
362 |
+
<h2 className="text-lg font-semibold text-bolt-elements-textPrimary">Event Logs</h2>
|
363 |
<p className="text-sm text-bolt-elements-textSecondary">Track system events and debug information</p>
|
364 |
</div>
|
365 |
</div>
|
366 |
+
<motion.button
|
367 |
+
onClick={handleRefresh}
|
368 |
+
disabled={isRefreshing}
|
369 |
+
className={classNames(
|
370 |
+
'p-2.5 rounded-lg',
|
371 |
+
'bg-purple-50/50 dark:bg-purple-900/10',
|
372 |
+
'text-purple-500 hover:text-purple-600',
|
373 |
+
'hover:bg-purple-100/50 dark:hover:bg-purple-900/20',
|
374 |
+
'focus:outline-none focus:ring-2 focus:ring-purple-500/20',
|
375 |
+
'transition-all duration-200 ease-in-out',
|
376 |
+
{ 'opacity-50 cursor-not-allowed': isRefreshing },
|
377 |
+
)}
|
378 |
+
whileHover={{ scale: 1.05 }}
|
379 |
+
whileTap={{ scale: 0.95 }}
|
380 |
+
title="Refresh logs"
|
381 |
+
>
|
382 |
+
<div className={classNames('text-lg transition-all duration-300', { 'animate-spin': isRefreshing })}>
|
383 |
+
<div className="i-ph:arrows-clockwise" />
|
384 |
</div>
|
385 |
+
</motion.button>
|
|
|
|
|
|
|
|
|
|
|
386 |
</div>
|
387 |
|
388 |
+
{/* Controls Section */}
|
389 |
+
<div className="flex items-center justify-end gap-2 px-1">
|
390 |
+
<div className="flex items-center gap-6 p-1.5 rounded-lg bg-white/50 dark:bg-gray-800/30 border border-gray-200/50 dark:border-gray-700/50">
|
391 |
+
<motion.div
|
392 |
+
className="flex items-center gap-3"
|
393 |
+
whileHover={{ scale: 1.02 }}
|
394 |
+
transition={{ type: 'spring', stiffness: 400, damping: 20 }}
|
395 |
+
>
|
396 |
+
<span className="text-sm font-medium text-bolt-elements-textSecondary">Auto-scroll</span>
|
397 |
+
<Switch
|
398 |
+
checked={autoScroll}
|
399 |
+
onCheckedChange={setAutoScroll}
|
400 |
+
className="data-[state=checked]:bg-purple-500"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
401 |
/>
|
402 |
+
</motion.div>
|
403 |
+
|
404 |
+
<div className="h-4 w-px bg-bolt-elements-borderColor" />
|
405 |
+
|
406 |
+
<motion.div
|
407 |
+
className="flex items-center gap-3"
|
408 |
+
whileHover={{ scale: 1.02 }}
|
409 |
+
transition={{ type: 'spring', stiffness: 400, damping: 20 }}
|
410 |
+
>
|
411 |
+
<span className="text-sm font-medium text-bolt-elements-textSecondary">24h Time</span>
|
412 |
+
<Switch
|
413 |
+
checked={use24Hour}
|
414 |
+
onCheckedChange={setUse24Hour}
|
415 |
+
className="data-[state=checked]:bg-purple-500"
|
416 |
+
/>
|
417 |
+
</motion.div>
|
418 |
+
|
419 |
+
<div className="h-4 w-px bg-bolt-elements-borderColor" />
|
420 |
+
|
421 |
+
<motion.div
|
422 |
+
className="flex items-center gap-3"
|
423 |
+
whileHover={{ scale: 1.02 }}
|
424 |
+
transition={{ type: 'spring', stiffness: 400, damping: 20 }}
|
425 |
+
>
|
426 |
+
<span className="text-sm font-medium text-bolt-elements-textSecondary">Expand All</span>
|
427 |
+
<Switch
|
428 |
+
checked={expandAll}
|
429 |
+
onCheckedChange={setExpandAll}
|
430 |
+
className="data-[state=checked]:bg-purple-500"
|
431 |
+
/>
|
432 |
+
</motion.div>
|
433 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
434 |
</div>
|
435 |
</div>
|
436 |
|
437 |
+
{/* Header with Search */}
|
438 |
+
<div className="flex flex-col gap-4">
|
439 |
+
<div className="relative w-72">
|
440 |
+
<div className="absolute left-2.5 top-1/2 -translate-y-1/2 text-bolt-elements-textTertiary">
|
441 |
+
<div className="i-ph:magnifying-glass text-base" />
|
442 |
+
</div>
|
443 |
+
<input
|
444 |
+
type="text"
|
445 |
+
placeholder="Search logs..."
|
446 |
+
className={classNames(
|
447 |
+
'w-full pl-8 pr-3 py-1.5 rounded-md text-sm',
|
448 |
+
'bg-bolt-elements-background-depth-1 dark:bg-bolt-elements-background-depth-2',
|
449 |
+
'border border-bolt-elements-borderColor',
|
450 |
+
'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
|
451 |
+
'focus:outline-none focus:ring-1 focus:ring-purple-500/30 focus:border-purple-500/30',
|
452 |
+
'transition-all duration-200',
|
453 |
+
)}
|
454 |
+
value={searchQuery}
|
455 |
+
onChange={(e) => setSearchQuery(e.target.value)}
|
456 |
+
/>
|
457 |
+
</div>
|
458 |
+
|
459 |
+
{/* Filters Row */}
|
460 |
+
<div className="flex items-center -ml-1">
|
461 |
+
<SegmentedGroup
|
462 |
+
value={logLevel}
|
463 |
+
onChange={(value) => setLogLevel(value as LogEntry['level'] | 'all')}
|
464 |
+
options={logLevelOptions}
|
465 |
+
/>
|
466 |
+
<div className="mx-2 w-px h-4 bg-bolt-elements-borderColor" />
|
467 |
+
<SegmentedGroup
|
468 |
+
value={logCategory}
|
469 |
+
onChange={(value) => setLogCategory(value as LogEntry['category'] | 'all')}
|
470 |
+
options={logCategoryOptions}
|
471 |
+
/>
|
472 |
+
</div>
|
473 |
+
</div>
|
474 |
+
|
475 |
+
{/* Logs Display */}
|
476 |
+
<div
|
477 |
ref={logsContainerRef}
|
478 |
+
className="flex-1 overflow-auto rounded-lg bg-bolt-elements-background-depth-1 dark:bg-bolt-elements-background-depth-2"
|
479 |
+
onScroll={handleScroll}
|
|
|
|
|
|
|
|
|
|
|
480 |
>
|
481 |
+
<div className="divide-y divide-bolt-elements-borderColor">
|
482 |
+
{filteredLogs.map((log) => (
|
483 |
+
<LogEntryItem key={log.id} log={log} isExpanded={expandAll} use24Hour={use24Hour} />
|
484 |
+
))}
|
485 |
+
</div>
|
486 |
+
</div>
|
487 |
+
|
488 |
+
{/* Status Bar */}
|
489 |
+
<div className="flex items-center justify-between py-2 px-4 text-sm text-bolt-elements-textSecondary">
|
490 |
+
<div className="flex items-center gap-6">
|
491 |
+
<span>{filteredLogs.length} logs displayed</span>
|
492 |
+
<span>{isScrolledToBottom ? 'Watching for new logs...' : 'Scroll to bottom to watch new logs'}</span>
|
493 |
+
</div>
|
494 |
+
<div className="flex items-center gap-2">
|
495 |
+
<motion.button
|
496 |
+
onClick={handleExportLogs}
|
497 |
+
className="flex items-center gap-2 px-3 py-1.5 text-sm bg-purple-500/10 text-purple-600 dark:text-purple-400 hover:bg-purple-500/20 rounded-md transition-colors"
|
498 |
+
whileHover={{ scale: 1.02 }}
|
499 |
+
whileTap={{ scale: 0.98 }}
|
500 |
+
>
|
501 |
+
<div className="i-ph:download-simple" />
|
502 |
+
Export
|
503 |
+
</motion.button>
|
504 |
+
<motion.button
|
505 |
+
onClick={handleClearLogs}
|
506 |
+
className="flex items-center gap-2 px-3 py-1.5 text-sm bg-red-500/10 text-red-600 dark:text-red-400 hover:bg-red-500/20 rounded-md transition-colors"
|
507 |
+
whileHover={{ scale: 1.02 }}
|
508 |
+
whileTap={{ scale: 0.98 }}
|
509 |
+
>
|
510 |
+
<div className="i-ph:trash" />
|
511 |
+
Clear
|
512 |
+
</motion.button>
|
513 |
+
</div>
|
514 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
515 |
</div>
|
516 |
);
|
517 |
}
|
app/components/settings/features/FeaturesTab.tsx
CHANGED
@@ -1,11 +1,12 @@
|
|
1 |
-
import React
|
|
|
2 |
import { Switch } from '~/components/ui/Switch';
|
3 |
-
import { PromptLibrary } from '~/lib/common/prompt-library';
|
4 |
import { useSettings } from '~/lib/hooks/useSettings';
|
5 |
-
import { motion, AnimatePresence } from 'framer-motion';
|
6 |
import { classNames } from '~/utils/classNames';
|
7 |
-
import { settingsStyles } from '~/components/settings/settings.styles';
|
8 |
import { toast } from 'react-toastify';
|
|
|
|
|
|
|
9 |
|
10 |
interface FeatureToggle {
|
11 |
id: string;
|
@@ -20,11 +21,9 @@ interface FeatureToggle {
|
|
20 |
|
21 |
export default function FeaturesTab() {
|
22 |
const {
|
23 |
-
|
24 |
-
enableDebugMode,
|
25 |
isLocalModel,
|
26 |
enableLocalModels,
|
27 |
-
enableEventLogs,
|
28 |
isLatestBranch,
|
29 |
enableLatestBranch,
|
30 |
promptId,
|
@@ -35,25 +34,9 @@ export default function FeaturesTab() {
|
|
35 |
contextOptimizationEnabled,
|
36 |
} = useSettings();
|
37 |
|
38 |
-
const
|
39 |
-
const [expandedFeature, setExpandedFeature] = useState<string | null>(null);
|
40 |
-
|
41 |
-
const handleToggle = (enabled: boolean) => {
|
42 |
-
enableDebugMode(enabled);
|
43 |
-
enableEventLogs(enabled);
|
44 |
-
toast.success(`Debug features ${enabled ? 'enabled' : 'disabled'}`);
|
45 |
-
};
|
46 |
|
47 |
const features: FeatureToggle[] = [
|
48 |
-
{
|
49 |
-
id: 'debug',
|
50 |
-
title: 'Debug Features',
|
51 |
-
description: 'Enable debugging tools and detailed logging',
|
52 |
-
icon: 'i-ph:bug',
|
53 |
-
enabled: debug,
|
54 |
-
experimental: true,
|
55 |
-
tooltip: 'Access advanced debugging tools and view detailed system logs',
|
56 |
-
},
|
57 |
{
|
58 |
id: 'latestBranch',
|
59 |
title: 'Use Main Branch',
|
@@ -88,13 +71,18 @@ export default function FeaturesTab() {
|
|
88 |
experimental: true,
|
89 |
tooltip: 'Try out new AI providers and models in development',
|
90 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
91 |
];
|
92 |
|
93 |
const handleToggleFeature = (featureId: string, enabled: boolean) => {
|
94 |
switch (featureId) {
|
95 |
-
case 'debug':
|
96 |
-
handleToggle(enabled);
|
97 |
-
break;
|
98 |
case 'latestBranch':
|
99 |
enableLatestBranch(enabled);
|
100 |
toast.success(`Main branch updates ${enabled ? 'enabled' : 'disabled'}`);
|
@@ -111,13 +99,17 @@ export default function FeaturesTab() {
|
|
111 |
enableLocalModels(enabled);
|
112 |
toast.success(`Experimental providers ${enabled ? 'enabled' : 'disabled'}`);
|
113 |
break;
|
|
|
|
|
|
|
|
|
114 |
}
|
115 |
};
|
116 |
|
117 |
return (
|
118 |
-
<div className="
|
119 |
<motion.div
|
120 |
-
className="flex items-center gap-
|
121 |
initial={{ opacity: 0, y: -20 }}
|
122 |
animate={{ opacity: 1, y: 0 }}
|
123 |
transition={{ duration: 0.3 }}
|
@@ -125,7 +117,9 @@ export default function FeaturesTab() {
|
|
125 |
<div className="i-ph:puzzle-piece text-xl text-purple-500" />
|
126 |
<div>
|
127 |
<h3 className="text-lg font-medium text-bolt-elements-textPrimary">Features</h3>
|
128 |
-
<p className="text-sm text-bolt-elements-textSecondary">
|
|
|
|
|
129 |
</div>
|
130 |
</motion.div>
|
131 |
|
@@ -139,39 +133,16 @@ export default function FeaturesTab() {
|
|
139 |
<motion.div
|
140 |
key={feature.id}
|
141 |
className={classNames(
|
142 |
-
|
143 |
'bg-bolt-elements-background-depth-2',
|
144 |
'hover:bg-bolt-elements-background-depth-3',
|
145 |
'transition-colors duration-200',
|
146 |
-
'
|
147 |
)}
|
148 |
initial={{ opacity: 0, y: 20 }}
|
149 |
animate={{ opacity: 1, y: 0 }}
|
150 |
transition={{ delay: index * 0.1 }}
|
151 |
-
onHoverStart={() => setHoveredFeature(feature.id)}
|
152 |
-
onHoverEnd={() => setHoveredFeature(null)}
|
153 |
-
onClick={() => setExpandedFeature(expandedFeature === feature.id ? null : feature.id)}
|
154 |
>
|
155 |
-
<AnimatePresence>
|
156 |
-
{hoveredFeature === feature.id && feature.tooltip && (
|
157 |
-
<motion.div
|
158 |
-
className={classNames(
|
159 |
-
'absolute -top-12 left-1/2 transform -translate-x-1/2',
|
160 |
-
'px-3 py-2 rounded-lg text-xs',
|
161 |
-
'bg-bolt-elements-background-depth-4 text-bolt-elements-textPrimary',
|
162 |
-
'border border-bolt-elements-borderColor',
|
163 |
-
'whitespace-nowrap z-10',
|
164 |
-
)}
|
165 |
-
initial={{ opacity: 0, y: 10 }}
|
166 |
-
animate={{ opacity: 1, y: 0 }}
|
167 |
-
exit={{ opacity: 0, y: 10 }}
|
168 |
-
>
|
169 |
-
{feature.tooltip}
|
170 |
-
<div className="absolute -bottom-1 left-1/2 transform -translate-x-1/2 rotate-45 w-2 h-2 bg-bolt-elements-background-depth-4 border-r border-b border-bolt-elements-borderColor" />
|
171 |
-
</motion.div>
|
172 |
-
)}
|
173 |
-
</AnimatePresence>
|
174 |
-
|
175 |
<div className="absolute top-0 right-0 p-2 flex gap-1">
|
176 |
{feature.beta && (
|
177 |
<motion.span
|
@@ -236,10 +207,10 @@ export default function FeaturesTab() {
|
|
236 |
|
237 |
<motion.div
|
238 |
className={classNames(
|
239 |
-
settingsStyles.card,
|
240 |
'bg-bolt-elements-background-depth-2',
|
241 |
'hover:bg-bolt-elements-background-depth-3',
|
242 |
'transition-all duration-200',
|
|
|
243 |
'group',
|
244 |
)}
|
245 |
initial={{ opacity: 0, y: 20 }}
|
|
|
1 |
+
import React from 'react';
|
2 |
+
import { motion } from 'framer-motion';
|
3 |
import { Switch } from '~/components/ui/Switch';
|
|
|
4 |
import { useSettings } from '~/lib/hooks/useSettings';
|
|
|
5 |
import { classNames } from '~/utils/classNames';
|
|
|
6 |
import { toast } from 'react-toastify';
|
7 |
+
import { PromptLibrary } from '~/lib/common/prompt-library';
|
8 |
+
import { useStore } from '@nanostores/react';
|
9 |
+
import { isEventLogsEnabled } from '~/lib/stores/settings';
|
10 |
|
11 |
interface FeatureToggle {
|
12 |
id: string;
|
|
|
21 |
|
22 |
export default function FeaturesTab() {
|
23 |
const {
|
24 |
+
setEventLogs,
|
|
|
25 |
isLocalModel,
|
26 |
enableLocalModels,
|
|
|
27 |
isLatestBranch,
|
28 |
enableLatestBranch,
|
29 |
promptId,
|
|
|
34 |
contextOptimizationEnabled,
|
35 |
} = useSettings();
|
36 |
|
37 |
+
const eventLogs = useStore(isEventLogsEnabled);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
38 |
|
39 |
const features: FeatureToggle[] = [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
40 |
{
|
41 |
id: 'latestBranch',
|
42 |
title: 'Use Main Branch',
|
|
|
71 |
experimental: true,
|
72 |
tooltip: 'Try out new AI providers and models in development',
|
73 |
},
|
74 |
+
{
|
75 |
+
id: 'eventLogs',
|
76 |
+
title: 'Event Logging',
|
77 |
+
description: 'Enable detailed event logging and history',
|
78 |
+
icon: 'i-ph:list-bullets',
|
79 |
+
enabled: eventLogs,
|
80 |
+
tooltip: 'Record detailed logs of system events and user actions',
|
81 |
+
},
|
82 |
];
|
83 |
|
84 |
const handleToggleFeature = (featureId: string, enabled: boolean) => {
|
85 |
switch (featureId) {
|
|
|
|
|
|
|
86 |
case 'latestBranch':
|
87 |
enableLatestBranch(enabled);
|
88 |
toast.success(`Main branch updates ${enabled ? 'enabled' : 'disabled'}`);
|
|
|
99 |
enableLocalModels(enabled);
|
100 |
toast.success(`Experimental providers ${enabled ? 'enabled' : 'disabled'}`);
|
101 |
break;
|
102 |
+
case 'eventLogs':
|
103 |
+
setEventLogs(enabled);
|
104 |
+
toast.success(`Event logging ${enabled ? 'enabled' : 'disabled'}`);
|
105 |
+
break;
|
106 |
}
|
107 |
};
|
108 |
|
109 |
return (
|
110 |
+
<div className="flex flex-col gap-6">
|
111 |
<motion.div
|
112 |
+
className="flex items-center gap-3"
|
113 |
initial={{ opacity: 0, y: -20 }}
|
114 |
animate={{ opacity: 1, y: 0 }}
|
115 |
transition={{ duration: 0.3 }}
|
|
|
117 |
<div className="i-ph:puzzle-piece text-xl text-purple-500" />
|
118 |
<div>
|
119 |
<h3 className="text-lg font-medium text-bolt-elements-textPrimary">Features</h3>
|
120 |
+
<p className="text-sm text-bolt-elements-textSecondary">
|
121 |
+
Customize your Bolt experience with experimental features
|
122 |
+
</p>
|
123 |
</div>
|
124 |
</motion.div>
|
125 |
|
|
|
133 |
<motion.div
|
134 |
key={feature.id}
|
135 |
className={classNames(
|
136 |
+
'relative group cursor-pointer',
|
137 |
'bg-bolt-elements-background-depth-2',
|
138 |
'hover:bg-bolt-elements-background-depth-3',
|
139 |
'transition-colors duration-200',
|
140 |
+
'rounded-lg overflow-hidden',
|
141 |
)}
|
142 |
initial={{ opacity: 0, y: 20 }}
|
143 |
animate={{ opacity: 1, y: 0 }}
|
144 |
transition={{ delay: index * 0.1 }}
|
|
|
|
|
|
|
145 |
>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
146 |
<div className="absolute top-0 right-0 p-2 flex gap-1">
|
147 |
{feature.beta && (
|
148 |
<motion.span
|
|
|
207 |
|
208 |
<motion.div
|
209 |
className={classNames(
|
|
|
210 |
'bg-bolt-elements-background-depth-2',
|
211 |
'hover:bg-bolt-elements-background-depth-3',
|
212 |
'transition-all duration-200',
|
213 |
+
'rounded-lg',
|
214 |
'group',
|
215 |
)}
|
216 |
initial={{ opacity: 0, y: 20 }}
|
app/components/settings/notifications/NotificationsTab.tsx
ADDED
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React, { useState } from 'react';
|
2 |
+
import { motion } from 'framer-motion';
|
3 |
+
import { logStore } from '~/lib/stores/logs';
|
4 |
+
import { useStore } from '@nanostores/react';
|
5 |
+
import { formatDistanceToNow } from 'date-fns';
|
6 |
+
import { classNames } from '~/utils/classNames';
|
7 |
+
|
8 |
+
interface NotificationDetails {
|
9 |
+
type?: string;
|
10 |
+
message?: string;
|
11 |
+
currentVersion?: string;
|
12 |
+
latestVersion?: string;
|
13 |
+
branch?: string;
|
14 |
+
updateUrl?: string;
|
15 |
+
}
|
16 |
+
|
17 |
+
const NotificationsTab = () => {
|
18 |
+
const [filter, setFilter] = useState<'all' | 'error' | 'warning'>('all');
|
19 |
+
const logs = useStore(logStore.logs);
|
20 |
+
|
21 |
+
const handleClearNotifications = () => {
|
22 |
+
logStore.clearLogs();
|
23 |
+
};
|
24 |
+
|
25 |
+
const handleUpdateAction = (updateUrl: string) => {
|
26 |
+
window.open(updateUrl, '_blank');
|
27 |
+
};
|
28 |
+
|
29 |
+
const filteredLogs = Object.values(logs)
|
30 |
+
.filter((log) => {
|
31 |
+
if (filter === 'all') {
|
32 |
+
return log.level === 'error' || log.level === 'warning';
|
33 |
+
}
|
34 |
+
|
35 |
+
return log.level === filter;
|
36 |
+
})
|
37 |
+
.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
38 |
+
|
39 |
+
const renderNotificationDetails = (details: NotificationDetails) => {
|
40 |
+
if (details.type === 'update') {
|
41 |
+
return (
|
42 |
+
<div className="flex flex-col gap-2">
|
43 |
+
<p className="text-sm text-gray-600 dark:text-gray-400">{details.message}</p>
|
44 |
+
<div className="flex flex-col gap-1 text-xs text-gray-500 dark:text-gray-500">
|
45 |
+
<p>Current Version: {details.currentVersion}</p>
|
46 |
+
<p>Latest Version: {details.latestVersion}</p>
|
47 |
+
<p>Branch: {details.branch}</p>
|
48 |
+
</div>
|
49 |
+
<button
|
50 |
+
onClick={() => details.updateUrl && handleUpdateAction(details.updateUrl)}
|
51 |
+
className="mt-2 inline-flex items-center gap-2 rounded-md bg-blue-50 px-3 py-1.5 text-sm font-medium text-blue-600 hover:bg-blue-100 dark:bg-blue-900/20 dark:text-blue-400 dark:hover:bg-blue-900/30"
|
52 |
+
>
|
53 |
+
<span className="i-ph:git-branch text-lg" />
|
54 |
+
View Changes
|
55 |
+
</button>
|
56 |
+
</div>
|
57 |
+
);
|
58 |
+
}
|
59 |
+
|
60 |
+
return details.message ? <p className="text-sm text-gray-600 dark:text-gray-400">{details.message}</p> : null;
|
61 |
+
};
|
62 |
+
|
63 |
+
return (
|
64 |
+
<div className="flex h-full flex-col gap-6">
|
65 |
+
<div className="flex items-center justify-between">
|
66 |
+
<div className="flex items-center gap-2">
|
67 |
+
<select
|
68 |
+
value={filter}
|
69 |
+
onChange={(e) => setFilter(e.target.value as 'all' | 'error' | 'warning')}
|
70 |
+
className="rounded-md border border-gray-300 bg-white px-3 py-1.5 text-sm shadow-sm dark:border-gray-700 dark:bg-gray-800"
|
71 |
+
>
|
72 |
+
<option value="all">All Notifications</option>
|
73 |
+
<option value="error">Errors</option>
|
74 |
+
<option value="warning">Warnings</option>
|
75 |
+
</select>
|
76 |
+
</div>
|
77 |
+
<button
|
78 |
+
onClick={handleClearNotifications}
|
79 |
+
className="rounded-md bg-gray-50 px-3 py-1.5 text-sm font-medium text-gray-600 hover:bg-gray-100 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700"
|
80 |
+
>
|
81 |
+
Clear All
|
82 |
+
</button>
|
83 |
+
</div>
|
84 |
+
|
85 |
+
<div className="flex flex-col gap-4">
|
86 |
+
{filteredLogs.length === 0 ? (
|
87 |
+
<div className="flex flex-col items-center justify-center gap-4 rounded-lg border border-gray-200 p-8 text-center dark:border-gray-700">
|
88 |
+
<span className="i-ph:bell-slash text-4xl text-gray-400 dark:text-gray-600" />
|
89 |
+
<div className="flex flex-col gap-1">
|
90 |
+
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100">No Notifications</h3>
|
91 |
+
<p className="text-sm text-gray-500 dark:text-gray-400">You're all caught up!</p>
|
92 |
+
</div>
|
93 |
+
</div>
|
94 |
+
) : (
|
95 |
+
filteredLogs.map((log) => (
|
96 |
+
<motion.div
|
97 |
+
key={log.id}
|
98 |
+
initial={{ opacity: 0, y: 20 }}
|
99 |
+
animate={{ opacity: 1, y: 0 }}
|
100 |
+
className={classNames(
|
101 |
+
'flex flex-col gap-2 rounded-lg border p-4',
|
102 |
+
log.level === 'error'
|
103 |
+
? 'border-red-200 bg-red-50 dark:border-red-900/50 dark:bg-red-900/20'
|
104 |
+
: 'border-yellow-200 bg-yellow-50 dark:border-yellow-900/50 dark:bg-yellow-900/20',
|
105 |
+
)}
|
106 |
+
>
|
107 |
+
<div className="flex items-start justify-between gap-4">
|
108 |
+
<div className="flex items-center gap-3">
|
109 |
+
<span
|
110 |
+
className={classNames(
|
111 |
+
'text-lg',
|
112 |
+
log.level === 'error'
|
113 |
+
? 'i-ph:warning-circle text-red-600 dark:text-red-400'
|
114 |
+
: 'i-ph:warning text-yellow-600 dark:text-yellow-400',
|
115 |
+
)}
|
116 |
+
/>
|
117 |
+
<div>
|
118 |
+
<h3
|
119 |
+
className={classNames(
|
120 |
+
'text-sm font-medium',
|
121 |
+
log.level === 'error'
|
122 |
+
? 'text-red-900 dark:text-red-300'
|
123 |
+
: 'text-yellow-900 dark:text-yellow-300',
|
124 |
+
)}
|
125 |
+
>
|
126 |
+
{log.message}
|
127 |
+
</h3>
|
128 |
+
{log.details && renderNotificationDetails(log.details as NotificationDetails)}
|
129 |
+
</div>
|
130 |
+
</div>
|
131 |
+
<time className="shrink-0 text-xs text-gray-500 dark:text-gray-400">
|
132 |
+
{formatDistanceToNow(new Date(log.timestamp), { addSuffix: true })}
|
133 |
+
</time>
|
134 |
+
</div>
|
135 |
+
</motion.div>
|
136 |
+
))
|
137 |
+
)}
|
138 |
+
</div>
|
139 |
+
</div>
|
140 |
+
);
|
141 |
+
};
|
142 |
+
|
143 |
+
export default NotificationsTab;
|
app/components/settings/profile/ProfileTab.tsx
CHANGED
@@ -1,10 +1,8 @@
|
|
1 |
-
import React, { useState, useRef
|
2 |
import { AnimatePresence } from 'framer-motion';
|
3 |
import { toast } from 'react-toastify';
|
4 |
import { classNames } from '~/utils/classNames';
|
5 |
-
import { Switch } from '~/components/ui/Switch';
|
6 |
import type { UserProfile } from '~/components/settings/settings.types';
|
7 |
-
import { themeStore, kTheme } from '~/lib/stores/theme';
|
8 |
import { motion } from 'framer-motion';
|
9 |
|
10 |
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
|
@@ -15,7 +13,6 @@ export default function ProfileTab() {
|
|
15 |
const fileInputRef = useRef<HTMLInputElement>(null);
|
16 |
const [isLoading, setIsLoading] = useState(false);
|
17 |
const [showPassword, setShowPassword] = useState(false);
|
18 |
-
const [currentTimezone, setCurrentTimezone] = useState('');
|
19 |
const [profile, setProfile] = useState<UserProfile>(() => {
|
20 |
const saved = localStorage.getItem('bolt_user_profile');
|
21 |
return saved
|
@@ -23,35 +20,11 @@ export default function ProfileTab() {
|
|
23 |
: {
|
24 |
name: '',
|
25 |
email: '',
|
26 |
-
theme: 'system',
|
27 |
-
notifications: true,
|
28 |
-
language: 'en',
|
29 |
-
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
30 |
password: '',
|
31 |
bio: '',
|
32 |
};
|
33 |
});
|
34 |
|
35 |
-
useEffect(() => {
|
36 |
-
setCurrentTimezone(Intl.DateTimeFormat().resolvedOptions().timeZone);
|
37 |
-
}, []);
|
38 |
-
|
39 |
-
// Apply theme when profile changes
|
40 |
-
useEffect(() => {
|
41 |
-
if (profile.theme === 'system') {
|
42 |
-
// Remove theme override
|
43 |
-
localStorage.removeItem(kTheme);
|
44 |
-
|
45 |
-
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
46 |
-
document.querySelector('html')?.setAttribute('data-theme', prefersDark ? 'dark' : 'light');
|
47 |
-
} else {
|
48 |
-
// Set specific theme
|
49 |
-
localStorage.setItem(kTheme, profile.theme);
|
50 |
-
document.querySelector('html')?.setAttribute('data-theme', profile.theme);
|
51 |
-
themeStore.set(profile.theme);
|
52 |
-
}
|
53 |
-
}, [profile.theme]);
|
54 |
-
|
55 |
const handleAvatarUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
56 |
const file = event.target.files?.[0];
|
57 |
|
@@ -105,7 +78,20 @@ export default function ProfileTab() {
|
|
105 |
setIsLoading(true);
|
106 |
|
107 |
try {
|
108 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
109 |
toast.success('Profile settings saved successfully');
|
110 |
} catch (error) {
|
111 |
console.error('Error saving profile:', error);
|
@@ -117,259 +103,142 @@ export default function ProfileTab() {
|
|
117 |
|
118 |
return (
|
119 |
<div className="space-y-4">
|
120 |
-
|
121 |
-
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
|
127 |
-
>
|
128 |
-
<div className="
|
129 |
-
|
130 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
131 |
</div>
|
132 |
-
|
133 |
-
|
134 |
-
|
135 |
-
|
136 |
-
|
137 |
-
|
138 |
-
<div className="i-ph:spinner-gap-bold animate-spin text-purple-500" />
|
139 |
-
) : profile.avatar ? (
|
140 |
-
<img src={profile.avatar} alt="Profile" className="w-full h-full object-cover" />
|
141 |
-
) : (
|
142 |
-
<div className="i-ph:user-circle-fill text-bolt-elements-textSecondary" />
|
143 |
-
)}
|
144 |
-
</AnimatePresence>
|
145 |
</div>
|
146 |
-
<button
|
147 |
-
onClick={() => fileInputRef.current?.click()}
|
148 |
-
disabled={isLoading}
|
149 |
-
className="absolute inset-0 flex items-center justify-center bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity rounded-lg"
|
150 |
-
>
|
151 |
-
<div className="i-ph:camera-fill text-white" />
|
152 |
-
</button>
|
153 |
<input
|
154 |
-
|
155 |
-
|
156 |
-
|
157 |
-
|
158 |
-
className=
|
|
|
|
|
|
|
|
|
|
|
|
|
159 |
/>
|
160 |
</div>
|
161 |
|
162 |
-
|
163 |
-
|
164 |
-
|
165 |
-
<div className="absolute left-3 top-1/2 -translate-y-1/2">
|
166 |
-
<div className="i-ph:user-fill w-4 h-4 text-bolt-elements-textTertiary" />
|
167 |
-
</div>
|
168 |
-
<input
|
169 |
-
type="text"
|
170 |
-
value={profile.name}
|
171 |
-
onChange={(e) => setProfile((prev) => ({ ...prev, name: e.target.value }))}
|
172 |
-
placeholder="Enter your name"
|
173 |
-
className={classNames(
|
174 |
-
'w-full px-3 py-1.5 rounded-lg text-sm',
|
175 |
-
'pl-10',
|
176 |
-
'bg-[#F5F5F5] dark:bg-[#1A1A1A] border border-[#E5E5E5] dark:border-[#333333]',
|
177 |
-
'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
|
178 |
-
'focus:outline-none focus:ring-1 focus:ring-purple-500',
|
179 |
-
)}
|
180 |
-
/>
|
181 |
</div>
|
182 |
-
|
183 |
-
|
184 |
-
|
185 |
-
|
186 |
-
|
187 |
-
|
188 |
-
|
189 |
-
|
190 |
-
|
191 |
-
placeholder
|
192 |
-
|
193 |
-
|
194 |
-
|
195 |
-
'bg-[#F5F5F5] dark:bg-[#1A1A1A] border border-[#E5E5E5] dark:border-[#333333]',
|
196 |
-
'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
|
197 |
-
'focus:outline-none focus:ring-1 focus:ring-purple-500',
|
198 |
-
)}
|
199 |
-
/>
|
200 |
-
</div>
|
201 |
-
|
202 |
-
<div className="relative">
|
203 |
-
<input
|
204 |
-
type={showPassword ? 'text' : 'password'}
|
205 |
-
value={profile.password}
|
206 |
-
onChange={(e) => setProfile((prev) => ({ ...prev, password: e.target.value }))}
|
207 |
-
placeholder="Enter new password"
|
208 |
-
className={classNames(
|
209 |
-
'w-full px-3 py-1.5 rounded-lg text-sm',
|
210 |
-
'bg-[#F5F5F5] dark:bg-[#1A1A1A] border border-[#E5E5E5] dark:border-[#333333]',
|
211 |
-
'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
|
212 |
-
'focus:outline-none focus:ring-1 focus:ring-purple-500',
|
213 |
-
)}
|
214 |
-
/>
|
215 |
-
<button
|
216 |
-
type="button"
|
217 |
-
onClick={() => setShowPassword(!showPassword)}
|
218 |
-
className={classNames(
|
219 |
-
'absolute right-3 top-1/2 -translate-y-1/2',
|
220 |
-
'flex items-center justify-center',
|
221 |
-
'w-6 h-6 rounded-md',
|
222 |
-
'text-bolt-elements-textSecondary',
|
223 |
-
'hover:text-bolt-elements-item-contentActive',
|
224 |
-
'hover:bg-bolt-elements-item-backgroundActive',
|
225 |
-
'transition-colors',
|
226 |
-
)}
|
227 |
-
>
|
228 |
-
<div className={classNames(showPassword ? 'i-ph:eye-slash-fill' : 'i-ph:eye-fill', 'w-4 h-4')} />
|
229 |
-
</button>
|
230 |
-
</div>
|
231 |
-
</div>
|
232 |
-
</div>
|
233 |
-
</motion.div>
|
234 |
-
|
235 |
-
{/* Theme & Language */}
|
236 |
-
<motion.div
|
237 |
-
className="bg-white dark:bg-[#0A0A0A] rounded-lg shadow-sm dark:shadow-none p-4 space-y-4"
|
238 |
-
initial={{ opacity: 0, y: 20 }}
|
239 |
-
animate={{ opacity: 1, y: 0 }}
|
240 |
-
transition={{ delay: 0.2 }}
|
241 |
-
>
|
242 |
-
<div className="flex items-center gap-2 mb-4">
|
243 |
-
<div className="i-ph:palette-fill w-4 h-4 text-purple-500" />
|
244 |
-
<span className="text-sm font-medium text-bolt-elements-textPrimary">Appearance</span>
|
245 |
-
</div>
|
246 |
-
|
247 |
-
<div>
|
248 |
-
<div className="flex items-center gap-2 mb-2">
|
249 |
-
<div className="i-ph:paint-brush-fill w-4 h-4 text-bolt-elements-textSecondary" />
|
250 |
-
<label className="block text-sm text-bolt-elements-textSecondary">Theme</label>
|
251 |
-
</div>
|
252 |
-
<div className="flex gap-2">
|
253 |
-
{(['light', 'dark', 'system'] as const).map((theme) => (
|
254 |
-
<button
|
255 |
-
key={theme}
|
256 |
-
onClick={() => setProfile((prev) => ({ ...prev, theme }))}
|
257 |
-
className={classNames(
|
258 |
-
'px-3 py-1.5 rounded-lg text-sm flex items-center gap-2 transition-colors',
|
259 |
-
profile.theme === theme
|
260 |
-
? 'bg-purple-500 text-white hover:bg-purple-600'
|
261 |
-
: 'bg-[#F5F5F5] dark:bg-[#1A1A1A] text-bolt-elements-textSecondary hover:bg-[#E5E5E5] dark:hover:bg-[#252525] hover:text-bolt-elements-textPrimary',
|
262 |
-
)}
|
263 |
-
>
|
264 |
-
<div
|
265 |
-
className={`w-4 h-4 ${
|
266 |
-
theme === 'light'
|
267 |
-
? 'i-ph:sun-fill'
|
268 |
-
: theme === 'dark'
|
269 |
-
? 'i-ph:moon-stars-fill'
|
270 |
-
: 'i-ph:monitor-fill'
|
271 |
-
}`}
|
272 |
-
/>
|
273 |
-
<span className="capitalize">{theme}</span>
|
274 |
-
</button>
|
275 |
-
))}
|
276 |
</div>
|
277 |
-
</div>
|
278 |
|
279 |
-
|
280 |
-
|
281 |
-
|
282 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
283 |
</div>
|
284 |
-
<select
|
285 |
-
value={profile.language}
|
286 |
-
onChange={(e) => setProfile((prev) => ({ ...prev, language: e.target.value }))}
|
287 |
-
className={classNames(
|
288 |
-
'w-full px-3 py-1.5 rounded-lg text-sm',
|
289 |
-
'bg-[#F5F5F5] dark:bg-[#1A1A1A] border border-[#E5E5E5] dark:border-[#333333]',
|
290 |
-
'text-bolt-elements-textPrimary',
|
291 |
-
'focus:outline-none focus:ring-1 focus:ring-purple-500',
|
292 |
-
)}
|
293 |
-
>
|
294 |
-
<option value="en">English</option>
|
295 |
-
<option value="es">Español</option>
|
296 |
-
<option value="fr">Français</option>
|
297 |
-
<option value="de">Deutsch</option>
|
298 |
-
<option value="it">Italiano</option>
|
299 |
-
<option value="pt">Português</option>
|
300 |
-
<option value="ru">Русский</option>
|
301 |
-
<option value="zh">中文</option>
|
302 |
-
<option value="ja">日本語</option>
|
303 |
-
<option value="ko">한국어</option>
|
304 |
-
</select>
|
305 |
-
</div>
|
306 |
|
307 |
-
|
308 |
-
|
309 |
-
|
310 |
-
|
311 |
-
|
312 |
-
|
313 |
-
|
314 |
-
|
315 |
-
|
316 |
-
|
317 |
-
|
318 |
-
|
|
|
319 |
/>
|
320 |
</div>
|
321 |
</div>
|
322 |
-
</motion.div>
|
323 |
-
|
324 |
-
{/* Timezone */}
|
325 |
-
<div className="bg-white dark:bg-[#0A0A0A] rounded-lg shadow-sm dark:shadow-none p-4">
|
326 |
-
<div className="flex items-center gap-2 mb-4">
|
327 |
-
<div className="i-ph:clock-fill w-4 h-4 text-purple-500" />
|
328 |
-
<span className="text-sm font-medium text-bolt-elements-textPrimary">Time Settings</span>
|
329 |
-
</div>
|
330 |
-
|
331 |
-
<div className="flex items-center gap-2 mb-2">
|
332 |
-
<div className="i-ph:globe-fill w-4 h-4 text-bolt-elements-textSecondary" />
|
333 |
-
<label className="block text-sm text-bolt-elements-textSecondary">Timezone</label>
|
334 |
-
</div>
|
335 |
-
<div className="flex gap-2">
|
336 |
-
<select
|
337 |
-
value={profile.timezone}
|
338 |
-
onChange={(e) => setProfile((prev) => ({ ...prev, timezone: e.target.value }))}
|
339 |
-
className={classNames(
|
340 |
-
'flex-1 px-3 py-1.5 rounded-lg text-sm',
|
341 |
-
'bg-[#F5F5F5] dark:bg-[#1A1A1A] border border-[#E5E5E5] dark:border-[#333333]',
|
342 |
-
'text-bolt-elements-textPrimary',
|
343 |
-
'focus:outline-none focus:ring-1 focus:ring-purple-500',
|
344 |
-
)}
|
345 |
-
>
|
346 |
-
{Intl.supportedValuesOf('timeZone').map((tz) => (
|
347 |
-
<option key={tz} value={tz}>
|
348 |
-
{tz.replace(/_/g, ' ')}
|
349 |
-
</option>
|
350 |
-
))}
|
351 |
-
</select>
|
352 |
-
<button
|
353 |
-
onClick={() => setProfile((prev) => ({ ...prev, timezone: currentTimezone }))}
|
354 |
-
className={classNames(
|
355 |
-
'px-3 py-1.5 rounded-lg text-sm flex items-center gap-2',
|
356 |
-
'bg-[#F5F5F5] dark:bg-[#1A1A1A] text-bolt-elements-textSecondary',
|
357 |
-
'hover:text-bolt-elements-textPrimary',
|
358 |
-
)}
|
359 |
-
>
|
360 |
-
<div className="i-ph:crosshair-simple-fill" />
|
361 |
-
Auto-detect
|
362 |
-
</button>
|
363 |
-
</div>
|
364 |
</div>
|
365 |
-
</div>
|
366 |
|
367 |
{/* Save Button */}
|
368 |
<motion.div
|
369 |
className="flex justify-end mt-6"
|
370 |
initial={{ opacity: 0, y: 20 }}
|
371 |
animate={{ opacity: 1, y: 0 }}
|
372 |
-
transition={{ delay: 0.
|
373 |
>
|
374 |
<button
|
375 |
onClick={handleSave}
|
|
|
1 |
+
import React, { useState, useRef } from 'react';
|
2 |
import { AnimatePresence } from 'framer-motion';
|
3 |
import { toast } from 'react-toastify';
|
4 |
import { classNames } from '~/utils/classNames';
|
|
|
5 |
import type { UserProfile } from '~/components/settings/settings.types';
|
|
|
6 |
import { motion } from 'framer-motion';
|
7 |
|
8 |
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
|
|
|
13 |
const fileInputRef = useRef<HTMLInputElement>(null);
|
14 |
const [isLoading, setIsLoading] = useState(false);
|
15 |
const [showPassword, setShowPassword] = useState(false);
|
|
|
16 |
const [profile, setProfile] = useState<UserProfile>(() => {
|
17 |
const saved = localStorage.getItem('bolt_user_profile');
|
18 |
return saved
|
|
|
20 |
: {
|
21 |
name: '',
|
22 |
email: '',
|
|
|
|
|
|
|
|
|
23 |
password: '',
|
24 |
bio: '',
|
25 |
};
|
26 |
});
|
27 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
28 |
const handleAvatarUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
29 |
const file = event.target.files?.[0];
|
30 |
|
|
|
78 |
setIsLoading(true);
|
79 |
|
80 |
try {
|
81 |
+
// Get existing profile data to preserve settings
|
82 |
+
const existingProfile = JSON.parse(localStorage.getItem('bolt_user_profile') || '{}');
|
83 |
+
|
84 |
+
// Merge with new profile data
|
85 |
+
const updatedProfile = {
|
86 |
+
...existingProfile,
|
87 |
+
name: profile.name,
|
88 |
+
email: profile.email,
|
89 |
+
password: profile.password,
|
90 |
+
bio: profile.bio,
|
91 |
+
avatar: profile.avatar,
|
92 |
+
};
|
93 |
+
|
94 |
+
localStorage.setItem('bolt_user_profile', JSON.stringify(updatedProfile));
|
95 |
toast.success('Profile settings saved successfully');
|
96 |
} catch (error) {
|
97 |
console.error('Error saving profile:', error);
|
|
|
103 |
|
104 |
return (
|
105 |
<div className="space-y-4">
|
106 |
+
{/* Profile Information */}
|
107 |
+
<motion.div
|
108 |
+
className="bg-white dark:bg-[#0A0A0A] rounded-lg shadow-sm dark:shadow-none"
|
109 |
+
initial={{ opacity: 0, y: 20 }}
|
110 |
+
animate={{ opacity: 1, y: 0 }}
|
111 |
+
transition={{ delay: 0.1 }}
|
112 |
+
>
|
113 |
+
<div className="flex items-center gap-2 px-4 pt-4 pb-2">
|
114 |
+
<div className="i-ph:user-circle-fill w-4 h-4 text-purple-500" />
|
115 |
+
<span className="text-sm font-medium text-bolt-elements-textPrimary">Personal Information</span>
|
116 |
+
</div>
|
117 |
+
<div className="flex items-start gap-4 p-4">
|
118 |
+
{/* Avatar */}
|
119 |
+
<div className="relative group">
|
120 |
+
<div className="w-12 h-12 rounded-lg bg-[#F5F5F5] dark:bg-[#1A1A1A] flex items-center justify-center overflow-hidden">
|
121 |
+
<AnimatePresence mode="wait">
|
122 |
+
{isLoading ? (
|
123 |
+
<div className="i-ph:spinner-gap-bold animate-spin text-purple-500" />
|
124 |
+
) : profile.avatar ? (
|
125 |
+
<img src={profile.avatar} alt="Profile" className="w-full h-full object-cover" />
|
126 |
+
) : (
|
127 |
+
<div className="i-ph:user-circle-fill text-bolt-elements-textSecondary" />
|
128 |
+
)}
|
129 |
+
</AnimatePresence>
|
130 |
+
</div>
|
131 |
+
<button
|
132 |
+
onClick={() => fileInputRef.current?.click()}
|
133 |
+
disabled={isLoading}
|
134 |
+
className="absolute inset-0 flex items-center justify-center bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity rounded-lg"
|
135 |
+
>
|
136 |
+
<div className="i-ph:camera-fill text-white" />
|
137 |
+
</button>
|
138 |
+
<input
|
139 |
+
ref={fileInputRef}
|
140 |
+
type="file"
|
141 |
+
accept={ALLOWED_FILE_TYPES.join(',')}
|
142 |
+
onChange={handleAvatarUpload}
|
143 |
+
className="hidden"
|
144 |
+
/>
|
145 |
</div>
|
146 |
+
|
147 |
+
{/* Profile Fields */}
|
148 |
+
<div className="flex-1 space-y-3">
|
149 |
+
<div className="relative">
|
150 |
+
<div className="absolute left-3 top-1/2 -translate-y-1/2">
|
151 |
+
<div className="i-ph:user-fill w-4 h-4 text-bolt-elements-textTertiary" />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
152 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
153 |
<input
|
154 |
+
type="text"
|
155 |
+
value={profile.name}
|
156 |
+
onChange={(e) => setProfile((prev) => ({ ...prev, name: e.target.value }))}
|
157 |
+
placeholder="Enter your name"
|
158 |
+
className={classNames(
|
159 |
+
'w-full px-3 py-1.5 rounded-lg text-sm',
|
160 |
+
'pl-10',
|
161 |
+
'bg-[#F5F5F5] dark:bg-[#1A1A1A] border border-[#E5E5E5] dark:border-[#333333]',
|
162 |
+
'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
|
163 |
+
'focus:outline-none focus:ring-1 focus:ring-purple-500',
|
164 |
+
)}
|
165 |
/>
|
166 |
</div>
|
167 |
|
168 |
+
<div className="relative">
|
169 |
+
<div className="absolute left-3 top-1/2 -translate-y-1/2">
|
170 |
+
<div className="i-ph:envelope-fill w-4 h-4 text-bolt-elements-textTertiary" />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
171 |
</div>
|
172 |
+
<input
|
173 |
+
type="email"
|
174 |
+
value={profile.email}
|
175 |
+
onChange={(e) => setProfile((prev) => ({ ...prev, email: e.target.value }))}
|
176 |
+
placeholder="Enter your email"
|
177 |
+
className={classNames(
|
178 |
+
'w-full px-3 py-1.5 rounded-lg text-sm',
|
179 |
+
'pl-10',
|
180 |
+
'bg-[#F5F5F5] dark:bg-[#1A1A1A] border border-[#E5E5E5] dark:border-[#333333]',
|
181 |
+
'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
|
182 |
+
'focus:outline-none focus:ring-1 focus:ring-purple-500',
|
183 |
+
)}
|
184 |
+
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
185 |
</div>
|
|
|
186 |
|
187 |
+
<div className="relative">
|
188 |
+
<input
|
189 |
+
type={showPassword ? 'text' : 'password'}
|
190 |
+
value={profile.password}
|
191 |
+
onChange={(e) => setProfile((prev) => ({ ...prev, password: e.target.value }))}
|
192 |
+
placeholder="Enter new password"
|
193 |
+
className={classNames(
|
194 |
+
'w-full px-3 py-1.5 rounded-lg text-sm',
|
195 |
+
'bg-[#F5F5F5] dark:bg-[#1A1A1A] border border-[#E5E5E5] dark:border-[#333333]',
|
196 |
+
'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
|
197 |
+
'focus:outline-none focus:ring-1 focus:ring-purple-500',
|
198 |
+
)}
|
199 |
+
/>
|
200 |
+
<button
|
201 |
+
type="button"
|
202 |
+
onClick={() => setShowPassword(!showPassword)}
|
203 |
+
className={classNames(
|
204 |
+
'absolute right-3 top-1/2 -translate-y-1/2',
|
205 |
+
'flex items-center justify-center',
|
206 |
+
'w-6 h-6 rounded-md',
|
207 |
+
'text-bolt-elements-textSecondary',
|
208 |
+
'hover:text-bolt-elements-item-contentActive',
|
209 |
+
'hover:bg-bolt-elements-item-backgroundActive',
|
210 |
+
'transition-colors',
|
211 |
+
)}
|
212 |
+
>
|
213 |
+
<div className={classNames(showPassword ? 'i-ph:eye-slash-fill' : 'i-ph:eye-fill', 'w-4 h-4')} />
|
214 |
+
</button>
|
215 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
216 |
|
217 |
+
<div className="relative">
|
218 |
+
<textarea
|
219 |
+
value={profile.bio}
|
220 |
+
onChange={(e) => setProfile((prev) => ({ ...prev, bio: e.target.value }))}
|
221 |
+
placeholder="Tell us about yourself"
|
222 |
+
rows={3}
|
223 |
+
className={classNames(
|
224 |
+
'w-full px-3 py-2 rounded-lg text-sm',
|
225 |
+
'bg-[#F5F5F5] dark:bg-[#1A1A1A] border border-[#E5E5E5] dark:border-[#333333]',
|
226 |
+
'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
|
227 |
+
'focus:outline-none focus:ring-1 focus:ring-purple-500',
|
228 |
+
'resize-none',
|
229 |
+
)}
|
230 |
/>
|
231 |
</div>
|
232 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
233 |
</div>
|
234 |
+
</motion.div>
|
235 |
|
236 |
{/* Save Button */}
|
237 |
<motion.div
|
238 |
className="flex justify-end mt-6"
|
239 |
initial={{ opacity: 0, y: 20 }}
|
240 |
animate={{ opacity: 1, y: 0 }}
|
241 |
+
transition={{ delay: 0.2 }}
|
242 |
>
|
243 |
<button
|
244 |
onClick={handleSave}
|
app/components/settings/providers/ProvidersTab.tsx
CHANGED
@@ -85,8 +85,8 @@ interface CategoryToggleState {
|
|
85 |
local: boolean;
|
86 |
}
|
87 |
|
88 |
-
export
|
89 |
-
const
|
90 |
const [editingProvider, setEditingProvider] = useState<string | null>(null);
|
91 |
const [filteredProviders, setFilteredProviders] = useState<IProviderConfig[]>([]);
|
92 |
const [categoryEnabled, setCategoryEnabled] = useState<CategoryToggleState>({
|
@@ -128,12 +128,12 @@ export default function ProvidersTab() {
|
|
128 |
// Get providers for this category
|
129 |
const categoryProviders = groupedProviders[category].providers;
|
130 |
categoryProviders.forEach((provider) => {
|
131 |
-
updateProviderSettings(provider.name, { ...provider.settings, enabled });
|
132 |
});
|
133 |
|
134 |
toast.success(enabled ? `All ${category} providers enabled` : `All ${category} providers disabled`);
|
135 |
},
|
136 |
-
[groupedProviders, updateProviderSettings],
|
137 |
);
|
138 |
|
139 |
// Add effect to update category toggle states based on provider states
|
@@ -147,26 +147,32 @@ export default function ProvidersTab() {
|
|
147 |
|
148 |
// Effect to filter and sort providers
|
149 |
useEffect(() => {
|
150 |
-
|
151 |
-
|
152 |
-
|
153 |
-
|
154 |
-
|
155 |
-
|
156 |
-
|
157 |
-
|
|
|
|
|
|
|
|
|
158 |
|
159 |
-
|
|
|
|
|
160 |
|
161 |
-
|
162 |
-
const regular =
|
163 |
-
const urlConfigurable =
|
164 |
|
165 |
setFilteredProviders([...regular, ...urlConfigurable]);
|
166 |
-
}, [providers, isLocalModel]);
|
167 |
|
168 |
const handleToggleProvider = (provider: IProviderConfig, enabled: boolean) => {
|
169 |
-
updateProviderSettings(provider.name, { ...provider.settings, enabled });
|
170 |
|
171 |
if (enabled) {
|
172 |
logStore.logProvider(`Provider ${provider.name} enabled`, { provider: provider.name });
|
@@ -184,7 +190,7 @@ export default function ProvidersTab() {
|
|
184 |
newBaseUrl = undefined;
|
185 |
}
|
186 |
|
187 |
-
updateProviderSettings(provider.name, { ...provider.settings, baseUrl: newBaseUrl });
|
188 |
logStore.logProvider(`Base URL updated for ${provider.name}`, {
|
189 |
provider: provider.name,
|
190 |
baseUrl: newBaseUrl,
|
@@ -404,4 +410,4 @@ export default function ProvidersTab() {
|
|
404 |
))}
|
405 |
</div>
|
406 |
);
|
407 |
-
}
|
|
|
85 |
local: boolean;
|
86 |
}
|
87 |
|
88 |
+
export const ProvidersTab = () => {
|
89 |
+
const settings = useSettings();
|
90 |
const [editingProvider, setEditingProvider] = useState<string | null>(null);
|
91 |
const [filteredProviders, setFilteredProviders] = useState<IProviderConfig[]>([]);
|
92 |
const [categoryEnabled, setCategoryEnabled] = useState<CategoryToggleState>({
|
|
|
128 |
// Get providers for this category
|
129 |
const categoryProviders = groupedProviders[category].providers;
|
130 |
categoryProviders.forEach((provider) => {
|
131 |
+
settings.updateProviderSettings(provider.name, { ...provider.settings, enabled });
|
132 |
});
|
133 |
|
134 |
toast.success(enabled ? `All ${category} providers enabled` : `All ${category} providers disabled`);
|
135 |
},
|
136 |
+
[groupedProviders, settings.updateProviderSettings],
|
137 |
);
|
138 |
|
139 |
// Add effect to update category toggle states based on provider states
|
|
|
147 |
|
148 |
// Effect to filter and sort providers
|
149 |
useEffect(() => {
|
150 |
+
const newFilteredProviders = Object.entries(settings.providers || {}).map(([key, value]) => {
|
151 |
+
const provider = value as IProviderConfig;
|
152 |
+
return {
|
153 |
+
name: key,
|
154 |
+
settings: provider.settings,
|
155 |
+
staticModels: provider.staticModels || [],
|
156 |
+
getDynamicModels: provider.getDynamicModels,
|
157 |
+
getApiKeyLink: provider.getApiKeyLink,
|
158 |
+
labelForGetApiKey: provider.labelForGetApiKey,
|
159 |
+
icon: provider.icon,
|
160 |
+
} as IProviderConfig;
|
161 |
+
});
|
162 |
|
163 |
+
const filtered = !settings.isLocalModel
|
164 |
+
? newFilteredProviders.filter((provider) => !LOCAL_PROVIDERS.includes(provider.name))
|
165 |
+
: newFilteredProviders;
|
166 |
|
167 |
+
const sorted = filtered.sort((a, b) => a.name.localeCompare(b.name));
|
168 |
+
const regular = sorted.filter((p) => !URL_CONFIGURABLE_PROVIDERS.includes(p.name));
|
169 |
+
const urlConfigurable = sorted.filter((p) => URL_CONFIGURABLE_PROVIDERS.includes(p.name));
|
170 |
|
171 |
setFilteredProviders([...regular, ...urlConfigurable]);
|
172 |
+
}, [settings.providers, settings.isLocalModel]);
|
173 |
|
174 |
const handleToggleProvider = (provider: IProviderConfig, enabled: boolean) => {
|
175 |
+
settings.updateProviderSettings(provider.name, { ...provider.settings, enabled });
|
176 |
|
177 |
if (enabled) {
|
178 |
logStore.logProvider(`Provider ${provider.name} enabled`, { provider: provider.name });
|
|
|
190 |
newBaseUrl = undefined;
|
191 |
}
|
192 |
|
193 |
+
settings.updateProviderSettings(provider.name, { ...provider.settings, baseUrl: newBaseUrl });
|
194 |
logStore.logProvider(`Base URL updated for ${provider.name}`, {
|
195 |
provider: provider.name,
|
196 |
baseUrl: newBaseUrl,
|
|
|
410 |
))}
|
411 |
</div>
|
412 |
);
|
413 |
+
};
|
app/components/settings/settings.styles.ts
CHANGED
@@ -7,14 +7,14 @@ export function cn(...inputs: ClassValue[]) {
|
|
7 |
|
8 |
export const settingsStyles = {
|
9 |
// Card styles
|
10 |
-
card: 'bg-
|
11 |
|
12 |
// Button styles
|
13 |
button: {
|
14 |
base: 'inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm disabled:opacity-50 disabled:cursor-not-allowed',
|
15 |
primary: 'bg-purple-500 text-white hover:bg-purple-600',
|
16 |
secondary:
|
17 |
-
'bg-
|
18 |
danger: 'bg-red-50 text-red-500 hover:bg-red-100 dark:bg-red-500/10 dark:hover:bg-red-500/20',
|
19 |
warning: 'bg-yellow-50 text-yellow-600 hover:bg-yellow-100 dark:bg-yellow-500/10 dark:hover:bg-yellow-500/20',
|
20 |
success: 'bg-green-50 text-green-600 hover:bg-green-100 dark:bg-green-500/10 dark:hover:bg-green-500/20',
|
@@ -22,15 +22,21 @@ export const settingsStyles = {
|
|
22 |
|
23 |
// Form styles
|
24 |
form: {
|
25 |
-
label: 'block text-sm text-bolt-elements-textSecondary mb-2',
|
26 |
input:
|
27 |
-
'w-full px-3 py-2 rounded-lg text-sm bg-
|
28 |
},
|
29 |
|
30 |
// Search container
|
31 |
search: {
|
32 |
input:
|
33 |
-
'w-full h-10 pl-10 pr-4 rounded-lg text-sm bg-
|
|
|
|
|
|
|
|
|
|
|
|
|
34 |
},
|
35 |
|
36 |
'loading-spinner': 'i-ph:spinner-gap-bold animate-spin w-4 h-4',
|
|
|
7 |
|
8 |
export const settingsStyles = {
|
9 |
// Card styles
|
10 |
+
card: 'bg-bolt-elements-background dark:bg-bolt-elements-backgroundDark rounded-lg p-6 border border-bolt-elements-border dark:border-bolt-elements-borderDark',
|
11 |
|
12 |
// Button styles
|
13 |
button: {
|
14 |
base: 'inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm disabled:opacity-50 disabled:cursor-not-allowed',
|
15 |
primary: 'bg-purple-500 text-white hover:bg-purple-600',
|
16 |
secondary:
|
17 |
+
'bg-bolt-elements-hover dark:bg-bolt-elements-hoverDark text-bolt-elements-textSecondary dark:text-bolt-elements-textSecondaryDark hover:text-bolt-elements-textPrimary dark:hover:text-bolt-elements-textPrimaryDark',
|
18 |
danger: 'bg-red-50 text-red-500 hover:bg-red-100 dark:bg-red-500/10 dark:hover:bg-red-500/20',
|
19 |
warning: 'bg-yellow-50 text-yellow-600 hover:bg-yellow-100 dark:bg-yellow-500/10 dark:hover:bg-yellow-500/20',
|
20 |
success: 'bg-green-50 text-green-600 hover:bg-green-100 dark:bg-green-500/10 dark:hover:bg-green-500/20',
|
|
|
22 |
|
23 |
// Form styles
|
24 |
form: {
|
25 |
+
label: 'block text-sm text-bolt-elements-textSecondary dark:text-bolt-elements-textSecondaryDark mb-2',
|
26 |
input:
|
27 |
+
'w-full px-3 py-2 rounded-lg text-sm bg-bolt-elements-hover dark:bg-bolt-elements-hoverDark border border-bolt-elements-border dark:border-bolt-elements-borderDark text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimaryDark placeholder-bolt-elements-textTertiary focus:outline-none focus:ring-1 focus:ring-purple-500',
|
28 |
},
|
29 |
|
30 |
// Search container
|
31 |
search: {
|
32 |
input:
|
33 |
+
'w-full h-10 pl-10 pr-4 rounded-lg text-sm bg-bolt-elements-hover dark:bg-bolt-elements-hoverDark border border-bolt-elements-border dark:border-bolt-elements-borderDark text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimaryDark placeholder-bolt-elements-textTertiary focus:outline-none focus:ring-1 focus:ring-purple-500 transition-all',
|
34 |
+
},
|
35 |
+
|
36 |
+
// Scroll container styles
|
37 |
+
scroll: {
|
38 |
+
container: 'overflow-y-auto overscroll-y-contain',
|
39 |
+
content: 'min-h-full',
|
40 |
},
|
41 |
|
42 |
'loading-spinner': 'i-ph:spinner-gap-bold animate-spin w-4 h-4',
|
app/components/settings/settings.types.ts
CHANGED
@@ -1,15 +1,20 @@
|
|
1 |
import type { ReactNode } from 'react';
|
2 |
|
3 |
export type SettingCategory = 'profile' | 'file_sharing' | 'connectivity' | 'system' | 'services' | 'preferences';
|
|
|
4 |
export type TabType =
|
5 |
| 'profile'
|
|
|
|
|
|
|
6 |
| 'data'
|
7 |
| 'providers'
|
8 |
-
| '
|
9 |
| 'debug'
|
10 |
| 'event-logs'
|
11 |
-
| '
|
12 |
-
|
|
|
13 |
|
14 |
export interface UserProfile {
|
15 |
name: string;
|
@@ -34,6 +39,60 @@ export interface SettingItem {
|
|
34 |
keywords?: string[];
|
35 |
}
|
36 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
37 |
export const categoryLabels: Record<SettingCategory, string> = {
|
38 |
profile: 'Profile & Account',
|
39 |
file_sharing: 'File Sharing',
|
|
|
1 |
import type { ReactNode } from 'react';
|
2 |
|
3 |
export type SettingCategory = 'profile' | 'file_sharing' | 'connectivity' | 'system' | 'services' | 'preferences';
|
4 |
+
|
5 |
export type TabType =
|
6 |
| 'profile'
|
7 |
+
| 'settings'
|
8 |
+
| 'notifications'
|
9 |
+
| 'features'
|
10 |
| 'data'
|
11 |
| 'providers'
|
12 |
+
| 'connection'
|
13 |
| 'debug'
|
14 |
| 'event-logs'
|
15 |
+
| 'update';
|
16 |
+
|
17 |
+
export type WindowType = 'user' | 'developer';
|
18 |
|
19 |
export interface UserProfile {
|
20 |
name: string;
|
|
|
39 |
keywords?: string[];
|
40 |
}
|
41 |
|
42 |
+
export interface TabVisibilityConfig {
|
43 |
+
id: TabType;
|
44 |
+
visible: boolean;
|
45 |
+
window: 'user' | 'developer';
|
46 |
+
order: number;
|
47 |
+
locked?: boolean;
|
48 |
+
}
|
49 |
+
|
50 |
+
export interface TabWindowConfig {
|
51 |
+
userTabs: TabVisibilityConfig[];
|
52 |
+
developerTabs: TabVisibilityConfig[];
|
53 |
+
}
|
54 |
+
|
55 |
+
export const TAB_LABELS: Record<TabType, string> = {
|
56 |
+
profile: 'Profile',
|
57 |
+
settings: 'Settings',
|
58 |
+
notifications: 'Notifications',
|
59 |
+
features: 'Features',
|
60 |
+
data: 'Data',
|
61 |
+
providers: 'Providers',
|
62 |
+
connection: 'Connection',
|
63 |
+
debug: 'Debug',
|
64 |
+
'event-logs': 'Event Logs',
|
65 |
+
update: 'Update',
|
66 |
+
};
|
67 |
+
|
68 |
+
export const DEFAULT_TAB_CONFIG: TabVisibilityConfig[] = [
|
69 |
+
// User Window Tabs (Visible by default)
|
70 |
+
{ id: 'features', visible: true, window: 'user', order: 0 },
|
71 |
+
{ id: 'data', visible: true, window: 'user', order: 1 },
|
72 |
+
{ id: 'providers', visible: true, window: 'user', order: 2 },
|
73 |
+
{ id: 'connection', visible: true, window: 'user', order: 3 },
|
74 |
+
{ id: 'debug', visible: true, window: 'user', order: 4 },
|
75 |
+
|
76 |
+
// User Window Tabs (Hidden by default)
|
77 |
+
{ id: 'profile', visible: false, window: 'user', order: 5 },
|
78 |
+
{ id: 'settings', visible: false, window: 'user', order: 6 },
|
79 |
+
{ id: 'notifications', visible: false, window: 'user', order: 7 },
|
80 |
+
{ id: 'event-logs', visible: false, window: 'user', order: 8 },
|
81 |
+
{ id: 'update', visible: false, window: 'user', order: 9 },
|
82 |
+
|
83 |
+
// Developer Window Tabs (All visible by default)
|
84 |
+
{ id: 'profile', visible: true, window: 'developer', order: 0 },
|
85 |
+
{ id: 'settings', visible: true, window: 'developer', order: 1 },
|
86 |
+
{ id: 'notifications', visible: true, window: 'developer', order: 2 },
|
87 |
+
{ id: 'features', visible: true, window: 'developer', order: 3 },
|
88 |
+
{ id: 'data', visible: true, window: 'developer', order: 4 },
|
89 |
+
{ id: 'providers', visible: true, window: 'developer', order: 5 },
|
90 |
+
{ id: 'connection', visible: true, window: 'developer', order: 6 },
|
91 |
+
{ id: 'debug', visible: true, window: 'developer', order: 7 },
|
92 |
+
{ id: 'event-logs', visible: true, window: 'developer', order: 8 },
|
93 |
+
{ id: 'update', visible: true, window: 'developer', order: 9 },
|
94 |
+
];
|
95 |
+
|
96 |
export const categoryLabels: Record<SettingCategory, string> = {
|
97 |
profile: 'Profile & Account',
|
98 |
file_sharing: 'File Sharing',
|
app/components/settings/settings/SettingsTab.tsx
ADDED
@@ -0,0 +1,215 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React, { useState, useEffect } from 'react';
|
2 |
+
import { motion } from 'framer-motion';
|
3 |
+
import { toast } from 'react-toastify';
|
4 |
+
import { classNames } from '~/utils/classNames';
|
5 |
+
import { Switch } from '~/components/ui/Switch';
|
6 |
+
import { themeStore, kTheme } from '~/lib/stores/theme';
|
7 |
+
import type { UserProfile } from '~/components/settings/settings.types';
|
8 |
+
|
9 |
+
export default function SettingsTab() {
|
10 |
+
const [currentTimezone, setCurrentTimezone] = useState('');
|
11 |
+
const [settings, setSettings] = useState<UserProfile>(() => {
|
12 |
+
const saved = localStorage.getItem('bolt_user_profile');
|
13 |
+
return saved
|
14 |
+
? JSON.parse(saved)
|
15 |
+
: {
|
16 |
+
theme: 'system',
|
17 |
+
notifications: true,
|
18 |
+
language: 'en',
|
19 |
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
20 |
+
};
|
21 |
+
});
|
22 |
+
|
23 |
+
useEffect(() => {
|
24 |
+
setCurrentTimezone(Intl.DateTimeFormat().resolvedOptions().timeZone);
|
25 |
+
}, []);
|
26 |
+
|
27 |
+
// Apply theme when settings changes
|
28 |
+
useEffect(() => {
|
29 |
+
if (settings.theme === 'system') {
|
30 |
+
// Remove theme override
|
31 |
+
localStorage.removeItem(kTheme);
|
32 |
+
|
33 |
+
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
34 |
+
document.querySelector('html')?.setAttribute('data-theme', prefersDark ? 'dark' : 'light');
|
35 |
+
} else {
|
36 |
+
// Set specific theme
|
37 |
+
localStorage.setItem(kTheme, settings.theme);
|
38 |
+
document.querySelector('html')?.setAttribute('data-theme', settings.theme);
|
39 |
+
themeStore.set(settings.theme);
|
40 |
+
}
|
41 |
+
}, [settings.theme]);
|
42 |
+
|
43 |
+
// Save settings automatically when they change
|
44 |
+
useEffect(() => {
|
45 |
+
try {
|
46 |
+
// Get existing profile data
|
47 |
+
const existingProfile = JSON.parse(localStorage.getItem('bolt_user_profile') || '{}');
|
48 |
+
|
49 |
+
// Merge with new settings
|
50 |
+
const updatedProfile = {
|
51 |
+
...existingProfile,
|
52 |
+
theme: settings.theme,
|
53 |
+
notifications: settings.notifications,
|
54 |
+
language: settings.language,
|
55 |
+
timezone: settings.timezone,
|
56 |
+
};
|
57 |
+
|
58 |
+
localStorage.setItem('bolt_user_profile', JSON.stringify(updatedProfile));
|
59 |
+
toast.success('Settings updated');
|
60 |
+
} catch (error) {
|
61 |
+
console.error('Error saving settings:', error);
|
62 |
+
toast.error('Failed to update settings');
|
63 |
+
}
|
64 |
+
}, [settings]);
|
65 |
+
|
66 |
+
return (
|
67 |
+
<div className="space-y-4">
|
68 |
+
{/* Theme & Language */}
|
69 |
+
<motion.div
|
70 |
+
className="bg-white dark:bg-[#0A0A0A] rounded-lg shadow-sm dark:shadow-none p-4 space-y-4"
|
71 |
+
initial={{ opacity: 0, y: 20 }}
|
72 |
+
animate={{ opacity: 1, y: 0 }}
|
73 |
+
transition={{ delay: 0.1 }}
|
74 |
+
>
|
75 |
+
<div className="flex items-center gap-2 mb-4">
|
76 |
+
<div className="i-ph:palette-fill w-4 h-4 text-purple-500" />
|
77 |
+
<span className="text-sm font-medium text-bolt-elements-textPrimary">Appearance</span>
|
78 |
+
</div>
|
79 |
+
|
80 |
+
<div>
|
81 |
+
<div className="flex items-center gap-2 mb-2">
|
82 |
+
<div className="i-ph:paint-brush-fill w-4 h-4 text-bolt-elements-textSecondary" />
|
83 |
+
<label className="block text-sm text-bolt-elements-textSecondary">Theme</label>
|
84 |
+
</div>
|
85 |
+
<div className="flex gap-2">
|
86 |
+
{(['light', 'dark', 'system'] as const).map((theme) => (
|
87 |
+
<button
|
88 |
+
key={theme}
|
89 |
+
onClick={() => setSettings((prev) => ({ ...prev, theme }))}
|
90 |
+
className={classNames(
|
91 |
+
'px-3 py-1.5 rounded-lg text-sm flex items-center gap-2 transition-colors',
|
92 |
+
settings.theme === theme
|
93 |
+
? 'bg-purple-500 text-white hover:bg-purple-600'
|
94 |
+
: 'bg-[#F5F5F5] dark:bg-[#1A1A1A] text-bolt-elements-textSecondary hover:bg-[#E5E5E5] dark:hover:bg-[#252525] hover:text-bolt-elements-textPrimary',
|
95 |
+
)}
|
96 |
+
>
|
97 |
+
<div
|
98 |
+
className={`w-4 h-4 ${
|
99 |
+
theme === 'light'
|
100 |
+
? 'i-ph:sun-fill'
|
101 |
+
: theme === 'dark'
|
102 |
+
? 'i-ph:moon-stars-fill'
|
103 |
+
: 'i-ph:monitor-fill'
|
104 |
+
}`}
|
105 |
+
/>
|
106 |
+
<span className="capitalize">{theme}</span>
|
107 |
+
</button>
|
108 |
+
))}
|
109 |
+
</div>
|
110 |
+
</div>
|
111 |
+
|
112 |
+
<div>
|
113 |
+
<div className="flex items-center gap-2 mb-2">
|
114 |
+
<div className="i-ph:translate-fill w-4 h-4 text-bolt-elements-textSecondary" />
|
115 |
+
<label className="block text-sm text-bolt-elements-textSecondary">Language</label>
|
116 |
+
</div>
|
117 |
+
<select
|
118 |
+
value={settings.language}
|
119 |
+
onChange={(e) => setSettings((prev) => ({ ...prev, language: e.target.value }))}
|
120 |
+
className={classNames(
|
121 |
+
'w-full px-3 py-2 rounded-lg text-sm',
|
122 |
+
'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
|
123 |
+
'border border-[#E5E5E5] dark:border-[#1A1A1A]',
|
124 |
+
'text-bolt-elements-textPrimary',
|
125 |
+
'focus:outline-none focus:ring-2 focus:ring-purple-500/30',
|
126 |
+
'transition-all duration-200',
|
127 |
+
)}
|
128 |
+
>
|
129 |
+
<option value="en">English</option>
|
130 |
+
<option value="es">Español</option>
|
131 |
+
<option value="fr">Français</option>
|
132 |
+
<option value="de">Deutsch</option>
|
133 |
+
<option value="it">Italiano</option>
|
134 |
+
<option value="pt">Português</option>
|
135 |
+
<option value="ru">Русский</option>
|
136 |
+
<option value="zh">中文</option>
|
137 |
+
<option value="ja">日本語</option>
|
138 |
+
<option value="ko">한국어</option>
|
139 |
+
</select>
|
140 |
+
</div>
|
141 |
+
|
142 |
+
<div>
|
143 |
+
<div className="flex items-center gap-2 mb-2">
|
144 |
+
<div className="i-ph:bell-fill w-4 h-4 text-bolt-elements-textSecondary" />
|
145 |
+
<label className="block text-sm text-bolt-elements-textSecondary">Notifications</label>
|
146 |
+
</div>
|
147 |
+
<div className="flex items-center justify-between">
|
148 |
+
<span className="text-sm text-bolt-elements-textSecondary">
|
149 |
+
{settings.notifications ? 'Notifications are enabled' : 'Notifications are disabled'}
|
150 |
+
</span>
|
151 |
+
<Switch
|
152 |
+
checked={settings.notifications}
|
153 |
+
onCheckedChange={(checked) => {
|
154 |
+
// Update local state
|
155 |
+
setSettings((prev) => ({ ...prev, notifications: checked }));
|
156 |
+
|
157 |
+
// Update localStorage immediately
|
158 |
+
const existingProfile = JSON.parse(localStorage.getItem('bolt_user_profile') || '{}');
|
159 |
+
const updatedProfile = {
|
160 |
+
...existingProfile,
|
161 |
+
notifications: checked,
|
162 |
+
};
|
163 |
+
localStorage.setItem('bolt_user_profile', JSON.stringify(updatedProfile));
|
164 |
+
|
165 |
+
// Dispatch storage event for other components
|
166 |
+
window.dispatchEvent(
|
167 |
+
new StorageEvent('storage', {
|
168 |
+
key: 'bolt_user_profile',
|
169 |
+
newValue: JSON.stringify(updatedProfile),
|
170 |
+
}),
|
171 |
+
);
|
172 |
+
|
173 |
+
toast.success(`Notifications ${checked ? 'enabled' : 'disabled'}`);
|
174 |
+
}}
|
175 |
+
/>
|
176 |
+
</div>
|
177 |
+
</div>
|
178 |
+
</motion.div>
|
179 |
+
|
180 |
+
{/* Timezone */}
|
181 |
+
<motion.div
|
182 |
+
className="bg-white dark:bg-[#0A0A0A] rounded-lg shadow-sm dark:shadow-none p-4"
|
183 |
+
initial={{ opacity: 0, y: 20 }}
|
184 |
+
animate={{ opacity: 1, y: 0 }}
|
185 |
+
transition={{ delay: 0.2 }}
|
186 |
+
>
|
187 |
+
<div className="flex items-center gap-2 mb-4">
|
188 |
+
<div className="i-ph:clock-fill w-4 h-4 text-purple-500" />
|
189 |
+
<span className="text-sm font-medium text-bolt-elements-textPrimary">Time Settings</span>
|
190 |
+
</div>
|
191 |
+
|
192 |
+
<div>
|
193 |
+
<div className="flex items-center gap-2 mb-2">
|
194 |
+
<div className="i-ph:globe-fill w-4 h-4 text-bolt-elements-textSecondary" />
|
195 |
+
<label className="block text-sm text-bolt-elements-textSecondary">Timezone</label>
|
196 |
+
</div>
|
197 |
+
<select
|
198 |
+
value={settings.timezone}
|
199 |
+
onChange={(e) => setSettings((prev) => ({ ...prev, timezone: e.target.value }))}
|
200 |
+
className={classNames(
|
201 |
+
'w-full px-3 py-2 rounded-lg text-sm',
|
202 |
+
'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
|
203 |
+
'border border-[#E5E5E5] dark:border-[#1A1A1A]',
|
204 |
+
'text-bolt-elements-textPrimary',
|
205 |
+
'focus:outline-none focus:ring-2 focus:ring-purple-500/30',
|
206 |
+
'transition-all duration-200',
|
207 |
+
)}
|
208 |
+
>
|
209 |
+
<option value={currentTimezone}>{currentTimezone}</option>
|
210 |
+
</select>
|
211 |
+
</div>
|
212 |
+
</motion.div>
|
213 |
+
</div>
|
214 |
+
);
|
215 |
+
}
|
app/components/settings/shared/DraggableTabList.tsx
ADDED
@@ -0,0 +1,158 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useDrag, useDrop } from 'react-dnd';
|
2 |
+
import { motion } from 'framer-motion';
|
3 |
+
import { classNames } from '~/utils/classNames';
|
4 |
+
import type { TabVisibilityConfig } from '~/components/settings/settings.types';
|
5 |
+
import { TAB_LABELS } from '~/components/settings/settings.types';
|
6 |
+
import { Switch } from '~/components/ui/Switch';
|
7 |
+
|
8 |
+
interface DraggableTabListProps {
|
9 |
+
tabs: TabVisibilityConfig[];
|
10 |
+
onReorder: (tabs: TabVisibilityConfig[]) => void;
|
11 |
+
onWindowChange?: (tab: TabVisibilityConfig, window: 'user' | 'developer') => void;
|
12 |
+
onVisibilityChange?: (tab: TabVisibilityConfig, visible: boolean) => void;
|
13 |
+
showControls?: boolean;
|
14 |
+
}
|
15 |
+
|
16 |
+
interface DraggableTabItemProps {
|
17 |
+
tab: TabVisibilityConfig;
|
18 |
+
index: number;
|
19 |
+
moveTab: (dragIndex: number, hoverIndex: number) => void;
|
20 |
+
showControls?: boolean;
|
21 |
+
onWindowChange?: (tab: TabVisibilityConfig, window: 'user' | 'developer') => void;
|
22 |
+
onVisibilityChange?: (tab: TabVisibilityConfig, visible: boolean) => void;
|
23 |
+
}
|
24 |
+
|
25 |
+
interface DragItem {
|
26 |
+
type: string;
|
27 |
+
index: number;
|
28 |
+
id: string;
|
29 |
+
}
|
30 |
+
|
31 |
+
const DraggableTabItem = ({
|
32 |
+
tab,
|
33 |
+
index,
|
34 |
+
moveTab,
|
35 |
+
showControls,
|
36 |
+
onWindowChange,
|
37 |
+
onVisibilityChange,
|
38 |
+
}: DraggableTabItemProps) => {
|
39 |
+
const [{ isDragging }, drag] = useDrag({
|
40 |
+
type: 'tab',
|
41 |
+
item: { type: 'tab', index, id: tab.id },
|
42 |
+
collect: (monitor) => ({
|
43 |
+
isDragging: monitor.isDragging(),
|
44 |
+
}),
|
45 |
+
});
|
46 |
+
|
47 |
+
const [, drop] = useDrop({
|
48 |
+
accept: 'tab',
|
49 |
+
hover: (item: DragItem, monitor) => {
|
50 |
+
if (!monitor.isOver({ shallow: true })) {
|
51 |
+
return;
|
52 |
+
}
|
53 |
+
|
54 |
+
if (item.index === index) {
|
55 |
+
return;
|
56 |
+
}
|
57 |
+
|
58 |
+
if (item.id === tab.id) {
|
59 |
+
return;
|
60 |
+
}
|
61 |
+
|
62 |
+
moveTab(item.index, index);
|
63 |
+
item.index = index;
|
64 |
+
},
|
65 |
+
});
|
66 |
+
|
67 |
+
return (
|
68 |
+
<motion.div
|
69 |
+
ref={(node) => drag(drop(node))}
|
70 |
+
initial={false}
|
71 |
+
animate={{
|
72 |
+
scale: isDragging ? 1.02 : 1,
|
73 |
+
boxShadow: isDragging ? '0 8px 16px rgba(0,0,0,0.1)' : 'none',
|
74 |
+
}}
|
75 |
+
className={classNames(
|
76 |
+
'flex items-center justify-between p-4 rounded-lg',
|
77 |
+
'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
|
78 |
+
'border border-[#E5E5E5] dark:border-[#333333]',
|
79 |
+
isDragging ? 'z-50' : '',
|
80 |
+
)}
|
81 |
+
>
|
82 |
+
<div className="flex items-center gap-4">
|
83 |
+
<div className="cursor-grab">
|
84 |
+
<div className="i-ph:dots-six-vertical w-4 h-4 text-bolt-elements-textSecondary" />
|
85 |
+
</div>
|
86 |
+
<div>
|
87 |
+
<div className="font-medium text-bolt-elements-textPrimary">{TAB_LABELS[tab.id]}</div>
|
88 |
+
{showControls && (
|
89 |
+
<div className="text-xs text-bolt-elements-textSecondary">
|
90 |
+
Order: {tab.order}, Window: {tab.window}
|
91 |
+
</div>
|
92 |
+
)}
|
93 |
+
</div>
|
94 |
+
</div>
|
95 |
+
{showControls && !tab.locked && (
|
96 |
+
<div className="flex items-center gap-4">
|
97 |
+
<div className="flex items-center gap-2">
|
98 |
+
<Switch
|
99 |
+
checked={tab.visible}
|
100 |
+
onCheckedChange={(checked: boolean) => onVisibilityChange?.(tab, checked)}
|
101 |
+
className="data-[state=checked]:bg-purple-500"
|
102 |
+
aria-label={`Toggle ${TAB_LABELS[tab.id]} visibility`}
|
103 |
+
/>
|
104 |
+
<label className="text-sm text-bolt-elements-textSecondary">Visible</label>
|
105 |
+
</div>
|
106 |
+
<div className="flex items-center gap-2">
|
107 |
+
<label className="text-sm text-bolt-elements-textSecondary">User</label>
|
108 |
+
<Switch
|
109 |
+
checked={tab.window === 'developer'}
|
110 |
+
onCheckedChange={(checked: boolean) => onWindowChange?.(tab, checked ? 'developer' : 'user')}
|
111 |
+
className="data-[state=checked]:bg-purple-500"
|
112 |
+
aria-label={`Toggle ${TAB_LABELS[tab.id]} window assignment`}
|
113 |
+
/>
|
114 |
+
<label className="text-sm text-bolt-elements-textSecondary">Dev</label>
|
115 |
+
</div>
|
116 |
+
</div>
|
117 |
+
)}
|
118 |
+
</motion.div>
|
119 |
+
);
|
120 |
+
};
|
121 |
+
|
122 |
+
export const DraggableTabList = ({
|
123 |
+
tabs,
|
124 |
+
onReorder,
|
125 |
+
onWindowChange,
|
126 |
+
onVisibilityChange,
|
127 |
+
showControls = false,
|
128 |
+
}: DraggableTabListProps) => {
|
129 |
+
const moveTab = (dragIndex: number, hoverIndex: number) => {
|
130 |
+
const items = Array.from(tabs);
|
131 |
+
const [reorderedItem] = items.splice(dragIndex, 1);
|
132 |
+
items.splice(hoverIndex, 0, reorderedItem);
|
133 |
+
|
134 |
+
// Update order numbers based on position
|
135 |
+
const reorderedTabs = items.map((tab, index) => ({
|
136 |
+
...tab,
|
137 |
+
order: index + 1,
|
138 |
+
}));
|
139 |
+
|
140 |
+
onReorder(reorderedTabs);
|
141 |
+
};
|
142 |
+
|
143 |
+
return (
|
144 |
+
<div className="space-y-2">
|
145 |
+
{tabs.map((tab, index) => (
|
146 |
+
<DraggableTabItem
|
147 |
+
key={tab.id}
|
148 |
+
tab={tab}
|
149 |
+
index={index}
|
150 |
+
moveTab={moveTab}
|
151 |
+
showControls={showControls}
|
152 |
+
onWindowChange={onWindowChange}
|
153 |
+
onVisibilityChange={onVisibilityChange}
|
154 |
+
/>
|
155 |
+
))}
|
156 |
+
</div>
|
157 |
+
);
|
158 |
+
};
|
app/components/settings/shared/TabTile.tsx
ADDED
@@ -0,0 +1,230 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { motion } from 'framer-motion';
|
2 |
+
import * as Tooltip from '@radix-ui/react-tooltip';
|
3 |
+
import { classNames } from '~/utils/classNames';
|
4 |
+
import type { TabVisibilityConfig } from '~/components/settings/settings.types';
|
5 |
+
import { TAB_LABELS } from '~/components/settings/settings.types';
|
6 |
+
|
7 |
+
const TAB_ICONS = {
|
8 |
+
profile: 'i-ph:user',
|
9 |
+
settings: 'i-ph:gear',
|
10 |
+
notifications: 'i-ph:bell',
|
11 |
+
features: 'i-ph:star',
|
12 |
+
data: 'i-ph:database',
|
13 |
+
providers: 'i-ph:plug',
|
14 |
+
connection: 'i-ph:wifi-high',
|
15 |
+
debug: 'i-ph:bug',
|
16 |
+
'event-logs': 'i-ph:list-bullets',
|
17 |
+
update: 'i-ph:arrow-clockwise',
|
18 |
+
};
|
19 |
+
|
20 |
+
interface TabTileProps {
|
21 |
+
tab: TabVisibilityConfig;
|
22 |
+
onClick: () => void;
|
23 |
+
isActive?: boolean;
|
24 |
+
hasUpdate?: boolean;
|
25 |
+
statusMessage?: string;
|
26 |
+
description?: string;
|
27 |
+
isLoading?: boolean;
|
28 |
+
}
|
29 |
+
|
30 |
+
export const TabTile = ({
|
31 |
+
tab,
|
32 |
+
onClick,
|
33 |
+
isActive = false,
|
34 |
+
hasUpdate = false,
|
35 |
+
statusMessage,
|
36 |
+
description,
|
37 |
+
isLoading = false,
|
38 |
+
}: TabTileProps) => {
|
39 |
+
return (
|
40 |
+
<Tooltip.Provider delayDuration={200}>
|
41 |
+
<Tooltip.Root>
|
42 |
+
<Tooltip.Trigger asChild>
|
43 |
+
<motion.button
|
44 |
+
onClick={onClick}
|
45 |
+
disabled={isLoading}
|
46 |
+
className={classNames(
|
47 |
+
'relative flex flex-col items-center justify-center gap-3 p-6 rounded-xl',
|
48 |
+
'w-full h-full min-h-[160px]',
|
49 |
+
|
50 |
+
// Background and border styles
|
51 |
+
'bg-white dark:bg-[#141414]',
|
52 |
+
'border border-[#E5E5E5]/50 dark:border-[#333333]/50',
|
53 |
+
|
54 |
+
// Shadow and glass effect
|
55 |
+
'shadow-sm backdrop-blur-sm',
|
56 |
+
'dark:shadow-[0_0_15px_rgba(0,0,0,0.1)]',
|
57 |
+
'dark:bg-opacity-50',
|
58 |
+
|
59 |
+
// Hover effects
|
60 |
+
'hover:border-purple-500/30 dark:hover:border-purple-500/30',
|
61 |
+
'hover:bg-gradient-to-br hover:from-purple-50/50 hover:to-white dark:hover:from-purple-500/5 dark:hover:to-[#141414]',
|
62 |
+
'hover:shadow-md hover:shadow-purple-500/5',
|
63 |
+
'dark:hover:shadow-purple-500/10',
|
64 |
+
|
65 |
+
// Focus states for keyboard navigation
|
66 |
+
'focus:outline-none',
|
67 |
+
'focus:ring-2 focus:ring-purple-500/50 focus:ring-offset-2',
|
68 |
+
'dark:focus:ring-offset-[#141414]',
|
69 |
+
'focus:border-purple-500/30',
|
70 |
+
|
71 |
+
// Active state
|
72 |
+
isActive
|
73 |
+
? [
|
74 |
+
'border-purple-500/50 dark:border-purple-500/50',
|
75 |
+
'bg-gradient-to-br from-purple-50 to-white dark:from-purple-500/10 dark:to-[#141414]',
|
76 |
+
'shadow-md shadow-purple-500/10',
|
77 |
+
]
|
78 |
+
: '',
|
79 |
+
|
80 |
+
// Loading state
|
81 |
+
isLoading ? 'cursor-wait opacity-70' : '',
|
82 |
+
|
83 |
+
// Transitions
|
84 |
+
'transition-all duration-300 ease-out',
|
85 |
+
'group',
|
86 |
+
)}
|
87 |
+
whileHover={
|
88 |
+
!isLoading
|
89 |
+
? {
|
90 |
+
scale: 1.02,
|
91 |
+
transition: { duration: 0.2, ease: 'easeOut' },
|
92 |
+
}
|
93 |
+
: {}
|
94 |
+
}
|
95 |
+
whileTap={
|
96 |
+
!isLoading
|
97 |
+
? {
|
98 |
+
scale: 0.98,
|
99 |
+
transition: { duration: 0.1, ease: 'easeIn' },
|
100 |
+
}
|
101 |
+
: {}
|
102 |
+
}
|
103 |
+
>
|
104 |
+
{/* Loading Overlay */}
|
105 |
+
{isLoading && (
|
106 |
+
<motion.div
|
107 |
+
className={classNames(
|
108 |
+
'absolute inset-0 rounded-xl z-10',
|
109 |
+
'bg-white/50 dark:bg-black/50',
|
110 |
+
'backdrop-blur-sm',
|
111 |
+
'flex items-center justify-center',
|
112 |
+
)}
|
113 |
+
initial={{ opacity: 0 }}
|
114 |
+
animate={{ opacity: 1 }}
|
115 |
+
transition={{ duration: 0.2 }}
|
116 |
+
>
|
117 |
+
<motion.div
|
118 |
+
className={classNames('w-8 h-8 rounded-full', 'border-2 border-purple-500/30', 'border-t-purple-500')}
|
119 |
+
animate={{ rotate: 360 }}
|
120 |
+
transition={{
|
121 |
+
duration: 1,
|
122 |
+
repeat: Infinity,
|
123 |
+
ease: 'linear',
|
124 |
+
}}
|
125 |
+
/>
|
126 |
+
</motion.div>
|
127 |
+
)}
|
128 |
+
|
129 |
+
{/* Status Indicator */}
|
130 |
+
{hasUpdate && (
|
131 |
+
<motion.div
|
132 |
+
className={classNames(
|
133 |
+
'absolute top-3 right-3',
|
134 |
+
'w-2.5 h-2.5 rounded-full',
|
135 |
+
'bg-green-500',
|
136 |
+
'shadow-lg shadow-green-500/20',
|
137 |
+
'ring-4 ring-green-500/20',
|
138 |
+
)}
|
139 |
+
initial={{ scale: 0 }}
|
140 |
+
animate={{ scale: 1 }}
|
141 |
+
transition={{ type: 'spring', bounce: 0.5 }}
|
142 |
+
/>
|
143 |
+
)}
|
144 |
+
|
145 |
+
{/* Background glow effect */}
|
146 |
+
<div
|
147 |
+
className={classNames(
|
148 |
+
'absolute inset-0 rounded-xl opacity-0 group-hover:opacity-100',
|
149 |
+
'bg-gradient-to-br from-purple-500/5 to-transparent dark:from-purple-500/10',
|
150 |
+
'transition-opacity duration-300',
|
151 |
+
isActive ? 'opacity-100' : '',
|
152 |
+
)}
|
153 |
+
/>
|
154 |
+
|
155 |
+
{/* Icon */}
|
156 |
+
<div
|
157 |
+
className={classNames(
|
158 |
+
TAB_ICONS[tab.id],
|
159 |
+
'w-12 h-12',
|
160 |
+
'relative',
|
161 |
+
'text-gray-600 dark:text-gray-300',
|
162 |
+
'group-hover:text-purple-500 dark:group-hover:text-purple-400',
|
163 |
+
'transition-all duration-300',
|
164 |
+
isActive ? 'text-purple-500 dark:text-purple-400 scale-110' : '',
|
165 |
+
)}
|
166 |
+
/>
|
167 |
+
|
168 |
+
{/* Label and Description */}
|
169 |
+
<div className="relative flex flex-col items-center text-center">
|
170 |
+
<div
|
171 |
+
className={classNames(
|
172 |
+
'text-base font-medium',
|
173 |
+
'text-gray-700 dark:text-gray-200',
|
174 |
+
'group-hover:text-purple-500 dark:group-hover:text-purple-400',
|
175 |
+
'transition-colors duration-300',
|
176 |
+
isActive ? 'text-purple-500 dark:text-purple-400' : '',
|
177 |
+
)}
|
178 |
+
>
|
179 |
+
{TAB_LABELS[tab.id]}
|
180 |
+
</div>
|
181 |
+
{description && (
|
182 |
+
<div
|
183 |
+
className={classNames(
|
184 |
+
'text-xs mt-1',
|
185 |
+
'text-gray-500 dark:text-gray-400',
|
186 |
+
'group-hover:text-purple-400/70 dark:group-hover:text-purple-300/70',
|
187 |
+
'transition-colors duration-300',
|
188 |
+
'max-w-[180px]',
|
189 |
+
isActive ? 'text-purple-400/70 dark:text-purple-300/70' : '',
|
190 |
+
)}
|
191 |
+
>
|
192 |
+
{description}
|
193 |
+
</div>
|
194 |
+
)}
|
195 |
+
</div>
|
196 |
+
|
197 |
+
{/* Bottom indicator line */}
|
198 |
+
<div
|
199 |
+
className={classNames(
|
200 |
+
'absolute bottom-0 left-1/2 -translate-x-1/2',
|
201 |
+
'w-12 h-0.5 rounded-full',
|
202 |
+
'bg-purple-500/0 group-hover:bg-purple-500/50',
|
203 |
+
'transition-all duration-300 ease-out',
|
204 |
+
'transform scale-x-0 group-hover:scale-x-100',
|
205 |
+
isActive ? 'bg-purple-500 scale-x-100' : '',
|
206 |
+
)}
|
207 |
+
/>
|
208 |
+
</motion.button>
|
209 |
+
</Tooltip.Trigger>
|
210 |
+
<Tooltip.Portal>
|
211 |
+
<Tooltip.Content
|
212 |
+
className={classNames(
|
213 |
+
'px-3 py-1.5 rounded-lg',
|
214 |
+
'bg-[#18181B] text-white',
|
215 |
+
'text-sm font-medium',
|
216 |
+
'shadow-xl',
|
217 |
+
'select-none',
|
218 |
+
'z-[100]',
|
219 |
+
)}
|
220 |
+
side="top"
|
221 |
+
sideOffset={5}
|
222 |
+
>
|
223 |
+
{statusMessage || TAB_LABELS[tab.id]}
|
224 |
+
<Tooltip.Arrow className="fill-[#18181B]" />
|
225 |
+
</Tooltip.Content>
|
226 |
+
</Tooltip.Portal>
|
227 |
+
</Tooltip.Root>
|
228 |
+
</Tooltip.Provider>
|
229 |
+
);
|
230 |
+
};
|
app/components/settings/update/UpdateTab.tsx
ADDED
@@ -0,0 +1,217 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React, { useState, useEffect } from 'react';
|
2 |
+
import { motion } from 'framer-motion';
|
3 |
+
import { useSettings } from '~/lib/hooks/useSettings';
|
4 |
+
import { logStore } from '~/lib/stores/logs';
|
5 |
+
import { classNames } from '~/utils/classNames';
|
6 |
+
|
7 |
+
interface GitHubCommitResponse {
|
8 |
+
sha: string;
|
9 |
+
}
|
10 |
+
|
11 |
+
interface UpdateInfo {
|
12 |
+
currentVersion: string;
|
13 |
+
latestVersion: string;
|
14 |
+
branch: string;
|
15 |
+
hasUpdate: boolean;
|
16 |
+
}
|
17 |
+
|
18 |
+
const GITHUB_URLS = {
|
19 |
+
commitJson: async (branch: string): Promise<UpdateInfo> => {
|
20 |
+
try {
|
21 |
+
const response = await fetch(`https://api.github.com/repos/stackblitz-labs/bolt.diy/commits/${branch}`);
|
22 |
+
const data = (await response.json()) as GitHubCommitResponse;
|
23 |
+
|
24 |
+
const currentCommitHash = __COMMIT_HASH;
|
25 |
+
const remoteCommitHash = data.sha.slice(0, 7);
|
26 |
+
|
27 |
+
return {
|
28 |
+
currentVersion: currentCommitHash,
|
29 |
+
latestVersion: remoteCommitHash,
|
30 |
+
branch,
|
31 |
+
hasUpdate: remoteCommitHash !== currentCommitHash,
|
32 |
+
};
|
33 |
+
} catch (error) {
|
34 |
+
console.error('Failed to fetch commit info:', error);
|
35 |
+
throw new Error('Failed to fetch commit info');
|
36 |
+
}
|
37 |
+
},
|
38 |
+
};
|
39 |
+
|
40 |
+
const UpdateTab = () => {
|
41 |
+
const { isLatestBranch } = useSettings();
|
42 |
+
const [updateInfo, setUpdateInfo] = useState<UpdateInfo | null>(null);
|
43 |
+
const [isChecking, setIsChecking] = useState(false);
|
44 |
+
const [error, setError] = useState<string | null>(null);
|
45 |
+
|
46 |
+
const checkForUpdates = async () => {
|
47 |
+
setIsChecking(true);
|
48 |
+
setError(null);
|
49 |
+
|
50 |
+
try {
|
51 |
+
const branchToCheck = isLatestBranch ? 'main' : 'stable';
|
52 |
+
const info = await GITHUB_URLS.commitJson(branchToCheck);
|
53 |
+
setUpdateInfo(info);
|
54 |
+
|
55 |
+
if (info.hasUpdate) {
|
56 |
+
// Add update notification only if it doesn't already exist
|
57 |
+
const existingLogs = Object.values(logStore.logs.get());
|
58 |
+
const hasUpdateNotification = existingLogs.some(
|
59 |
+
(log) =>
|
60 |
+
log.level === 'warning' &&
|
61 |
+
log.details?.type === 'update' &&
|
62 |
+
log.details.latestVersion === info.latestVersion,
|
63 |
+
);
|
64 |
+
|
65 |
+
if (!hasUpdateNotification) {
|
66 |
+
logStore.logWarning('Update Available', {
|
67 |
+
currentVersion: info.currentVersion,
|
68 |
+
latestVersion: info.latestVersion,
|
69 |
+
branch: branchToCheck,
|
70 |
+
type: 'update',
|
71 |
+
message: `A new version is available on the ${branchToCheck} branch`,
|
72 |
+
updateUrl: `https://github.com/stackblitz-labs/bolt.diy/compare/${info.currentVersion}...${info.latestVersion}`,
|
73 |
+
});
|
74 |
+
}
|
75 |
+
}
|
76 |
+
} catch (err) {
|
77 |
+
setError('Failed to check for updates. Please try again later.');
|
78 |
+
console.error('Update check failed:', err);
|
79 |
+
} finally {
|
80 |
+
setIsChecking(false);
|
81 |
+
}
|
82 |
+
};
|
83 |
+
|
84 |
+
useEffect(() => {
|
85 |
+
checkForUpdates();
|
86 |
+
}, [isLatestBranch]);
|
87 |
+
|
88 |
+
const handleViewChanges = () => {
|
89 |
+
if (updateInfo) {
|
90 |
+
window.open(
|
91 |
+
`https://github.com/stackblitz-labs/bolt.diy/compare/${updateInfo.currentVersion}...${updateInfo.latestVersion}`,
|
92 |
+
'_blank',
|
93 |
+
);
|
94 |
+
}
|
95 |
+
};
|
96 |
+
|
97 |
+
return (
|
98 |
+
<div className="flex flex-col gap-6">
|
99 |
+
<motion.div
|
100 |
+
className="flex items-center gap-3"
|
101 |
+
initial={{ opacity: 0, y: -20 }}
|
102 |
+
animate={{ opacity: 1, y: 0 }}
|
103 |
+
transition={{ duration: 0.3 }}
|
104 |
+
>
|
105 |
+
<div className="i-ph:arrow-circle-up text-xl text-purple-500" />
|
106 |
+
<div>
|
107 |
+
<h3 className="text-lg font-medium text-bolt-elements-textPrimary">Updates</h3>
|
108 |
+
<p className="text-sm text-bolt-elements-textSecondary">Check for and manage application updates</p>
|
109 |
+
</div>
|
110 |
+
</motion.div>
|
111 |
+
|
112 |
+
<motion.div
|
113 |
+
className="flex flex-col gap-4"
|
114 |
+
initial={{ opacity: 0, y: 20 }}
|
115 |
+
animate={{ opacity: 1, y: 0 }}
|
116 |
+
transition={{ duration: 0.3, delay: 0.1 }}
|
117 |
+
>
|
118 |
+
<div className="flex items-center justify-between">
|
119 |
+
<div className="flex items-center gap-4">
|
120 |
+
<span className="text-sm text-bolt-elements-textSecondary">
|
121 |
+
Currently on {isLatestBranch ? 'main' : 'stable'} branch
|
122 |
+
</span>
|
123 |
+
{updateInfo && (
|
124 |
+
<span className="text-xs text-bolt-elements-textTertiary">Version: {updateInfo.currentVersion}</span>
|
125 |
+
)}
|
126 |
+
</div>
|
127 |
+
<button
|
128 |
+
onClick={checkForUpdates}
|
129 |
+
disabled={isChecking}
|
130 |
+
className={classNames(
|
131 |
+
'px-3 py-2 rounded-lg text-sm',
|
132 |
+
'bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor',
|
133 |
+
'text-bolt-elements-textPrimary',
|
134 |
+
'hover:bg-bolt-elements-background-depth-3',
|
135 |
+
'focus:outline-none focus:ring-2 focus:ring-purple-500/30',
|
136 |
+
'transition-all duration-200',
|
137 |
+
'disabled:opacity-50 disabled:cursor-not-allowed',
|
138 |
+
)}
|
139 |
+
>
|
140 |
+
<div className="flex items-center gap-2">
|
141 |
+
<div className={classNames('i-ph:arrows-clockwise', isChecking ? 'animate-spin' : '')} />
|
142 |
+
{isChecking ? 'Checking...' : 'Check for Updates'}
|
143 |
+
</div>
|
144 |
+
</button>
|
145 |
+
</div>
|
146 |
+
|
147 |
+
{error && (
|
148 |
+
<div className="p-4 rounded-lg bg-red-50 border border-red-200 text-red-700 dark:bg-red-900/20 dark:border-red-900/50 dark:text-red-400">
|
149 |
+
<div className="flex items-center gap-2">
|
150 |
+
<div className="i-ph:warning-circle" />
|
151 |
+
{error}
|
152 |
+
</div>
|
153 |
+
</div>
|
154 |
+
)}
|
155 |
+
|
156 |
+
{updateInfo && (
|
157 |
+
<div
|
158 |
+
className={classNames(
|
159 |
+
'p-4 rounded-lg border',
|
160 |
+
updateInfo.hasUpdate
|
161 |
+
? 'bg-yellow-50 border-yellow-200 dark:bg-yellow-900/20 dark:border-yellow-900/50'
|
162 |
+
: 'bg-green-50 border-green-200 dark:bg-green-900/20 dark:border-green-900/50',
|
163 |
+
)}
|
164 |
+
>
|
165 |
+
<div className="flex items-start justify-between gap-4">
|
166 |
+
<div className="flex items-center gap-3">
|
167 |
+
<span
|
168 |
+
className={classNames(
|
169 |
+
'text-lg',
|
170 |
+
updateInfo.hasUpdate
|
171 |
+
? 'i-ph:warning text-yellow-600 dark:text-yellow-400'
|
172 |
+
: 'i-ph:check-circle text-green-600 dark:text-green-400',
|
173 |
+
)}
|
174 |
+
/>
|
175 |
+
<div>
|
176 |
+
<h3
|
177 |
+
className={classNames(
|
178 |
+
'text-sm font-medium',
|
179 |
+
updateInfo.hasUpdate
|
180 |
+
? 'text-yellow-900 dark:text-yellow-300'
|
181 |
+
: 'text-green-900 dark:text-green-300',
|
182 |
+
)}
|
183 |
+
>
|
184 |
+
{updateInfo.hasUpdate ? 'Update Available' : 'Up to Date'}
|
185 |
+
</h3>
|
186 |
+
<p className="text-sm text-bolt-elements-textSecondary mt-1">
|
187 |
+
{updateInfo.hasUpdate
|
188 |
+
? `A new version is available on the ${updateInfo.branch} branch`
|
189 |
+
: 'You are running the latest version'}
|
190 |
+
</p>
|
191 |
+
{updateInfo.hasUpdate && (
|
192 |
+
<div className="mt-2 flex flex-col gap-1 text-xs text-bolt-elements-textTertiary">
|
193 |
+
<p>Current Version: {updateInfo.currentVersion}</p>
|
194 |
+
<p>Latest Version: {updateInfo.latestVersion}</p>
|
195 |
+
<p>Branch: {updateInfo.branch}</p>
|
196 |
+
</div>
|
197 |
+
)}
|
198 |
+
</div>
|
199 |
+
</div>
|
200 |
+
{updateInfo.hasUpdate && (
|
201 |
+
<button
|
202 |
+
onClick={handleViewChanges}
|
203 |
+
className="shrink-0 inline-flex items-center gap-2 rounded-md bg-blue-50 px-3 py-1.5 text-sm font-medium text-blue-600 hover:bg-blue-100 dark:bg-blue-900/20 dark:text-blue-400 dark:hover:bg-blue-900/30"
|
204 |
+
>
|
205 |
+
<span className="i-ph:git-branch text-lg" />
|
206 |
+
View Changes
|
207 |
+
</button>
|
208 |
+
)}
|
209 |
+
</div>
|
210 |
+
</div>
|
211 |
+
)}
|
212 |
+
</motion.div>
|
213 |
+
</div>
|
214 |
+
);
|
215 |
+
};
|
216 |
+
|
217 |
+
export default UpdateTab;
|
app/components/settings/user/ProfileHeader.tsx
ADDED
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import type { Dispatch, SetStateAction } from 'react';
|
2 |
+
import type { TabType, TabVisibilityConfig } from '~/components/settings/settings.types';
|
3 |
+
|
4 |
+
export interface ProfileHeaderProps {
|
5 |
+
onNavigate: Dispatch<SetStateAction<TabType | null>>;
|
6 |
+
visibleTabs: TabVisibilityConfig[];
|
7 |
+
}
|
8 |
+
|
9 |
+
export { type TabType };
|
10 |
+
|
11 |
+
export const ProfileHeader = ({ onNavigate, visibleTabs }: ProfileHeaderProps) => {
|
12 |
+
return (
|
13 |
+
<div className="flex items-center gap-2">
|
14 |
+
{visibleTabs.map((tab) => (
|
15 |
+
<button
|
16 |
+
key={tab.id}
|
17 |
+
onClick={() => onNavigate(tab.id)}
|
18 |
+
className="text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary transition-colors"
|
19 |
+
>
|
20 |
+
{tab.id}
|
21 |
+
</button>
|
22 |
+
))}
|
23 |
+
</div>
|
24 |
+
);
|
25 |
+
};
|
app/components/settings/user/UsersWindow.tsx
ADDED
@@ -0,0 +1,385 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import * as RadixDialog from '@radix-ui/react-dialog';
|
2 |
+
import { motion } from 'framer-motion';
|
3 |
+
import { useState } from 'react';
|
4 |
+
import { classNames } from '~/utils/classNames';
|
5 |
+
import { DialogTitle } from '~/components/ui/Dialog';
|
6 |
+
import { Switch } from '~/components/ui/Switch';
|
7 |
+
import type { TabType, TabVisibilityConfig } from '~/components/settings/settings.types';
|
8 |
+
import { TAB_LABELS } from '~/components/settings/settings.types';
|
9 |
+
import { DeveloperWindow } from '~/components/settings/developer/DeveloperWindow';
|
10 |
+
import { TabTile } from '~/components/settings/shared/TabTile';
|
11 |
+
import { tabConfigurationStore, updateTabConfiguration } from '~/lib/stores/settings';
|
12 |
+
import { useStore } from '@nanostores/react';
|
13 |
+
import { DndProvider, useDrag, useDrop } from 'react-dnd';
|
14 |
+
import { HTML5Backend } from 'react-dnd-html5-backend';
|
15 |
+
import ProfileTab from '~/components/settings/profile/ProfileTab';
|
16 |
+
import SettingsTab from '~/components/settings/settings/SettingsTab';
|
17 |
+
import NotificationsTab from '~/components/settings/notifications/NotificationsTab';
|
18 |
+
import FeaturesTab from '~/components/settings/features/FeaturesTab';
|
19 |
+
import DataTab from '~/components/settings/data/DataTab';
|
20 |
+
import { ProvidersTab } from '~/components/settings/providers/ProvidersTab';
|
21 |
+
import DebugTab from '~/components/settings/debug/DebugTab';
|
22 |
+
import { EventLogsTab } from '~/components/settings/event-logs/EventLogsTab';
|
23 |
+
import UpdateTab from '~/components/settings/update/UpdateTab';
|
24 |
+
import ConnectionsTab from '~/components/settings/connections/ConnectionsTab';
|
25 |
+
import { useUpdateCheck } from '~/lib/hooks/useUpdateCheck';
|
26 |
+
import { useFeatures } from '~/lib/hooks/useFeatures';
|
27 |
+
import { useNotifications } from '~/lib/hooks/useNotifications';
|
28 |
+
import { useConnectionStatus } from '~/lib/hooks/useConnectionStatus';
|
29 |
+
import { useDebugStatus } from '~/lib/hooks/useDebugStatus';
|
30 |
+
|
31 |
+
interface DraggableTabTileProps {
|
32 |
+
tab: TabVisibilityConfig;
|
33 |
+
index: number;
|
34 |
+
moveTab: (dragIndex: number, hoverIndex: number) => void;
|
35 |
+
onClick: () => void;
|
36 |
+
isActive: boolean;
|
37 |
+
hasUpdate: boolean;
|
38 |
+
statusMessage: string;
|
39 |
+
description: string;
|
40 |
+
isLoading?: boolean;
|
41 |
+
}
|
42 |
+
|
43 |
+
const TAB_DESCRIPTIONS: Record<TabType, string> = {
|
44 |
+
profile: 'Manage your profile and account settings',
|
45 |
+
settings: 'Configure application preferences',
|
46 |
+
notifications: 'View and manage your notifications',
|
47 |
+
features: 'Explore new and upcoming features',
|
48 |
+
data: 'Manage your data and storage',
|
49 |
+
providers: 'Configure AI providers and models',
|
50 |
+
connection: 'Check connection status and settings',
|
51 |
+
debug: 'Debug tools and system information',
|
52 |
+
'event-logs': 'View system events and logs',
|
53 |
+
update: 'Check for updates and release notes',
|
54 |
+
};
|
55 |
+
|
56 |
+
const DraggableTabTile = ({
|
57 |
+
tab,
|
58 |
+
index,
|
59 |
+
moveTab,
|
60 |
+
onClick,
|
61 |
+
isActive,
|
62 |
+
hasUpdate,
|
63 |
+
statusMessage,
|
64 |
+
description,
|
65 |
+
isLoading,
|
66 |
+
}: DraggableTabTileProps) => {
|
67 |
+
const [{ isDragging }, drag] = useDrag({
|
68 |
+
type: 'tab',
|
69 |
+
item: { index },
|
70 |
+
collect: (monitor) => ({
|
71 |
+
isDragging: monitor.isDragging(),
|
72 |
+
}),
|
73 |
+
});
|
74 |
+
|
75 |
+
const [, drop] = useDrop({
|
76 |
+
accept: 'tab',
|
77 |
+
hover: (item: { index: number }) => {
|
78 |
+
if (item.index === index) {
|
79 |
+
return;
|
80 |
+
}
|
81 |
+
|
82 |
+
moveTab(item.index, index);
|
83 |
+
item.index = index;
|
84 |
+
},
|
85 |
+
});
|
86 |
+
|
87 |
+
return (
|
88 |
+
<div ref={(node) => drag(drop(node))} style={{ opacity: isDragging ? 0.5 : 1 }}>
|
89 |
+
<TabTile
|
90 |
+
tab={tab}
|
91 |
+
onClick={onClick}
|
92 |
+
isActive={isActive}
|
93 |
+
hasUpdate={hasUpdate}
|
94 |
+
statusMessage={statusMessage}
|
95 |
+
description={description}
|
96 |
+
isLoading={isLoading}
|
97 |
+
/>
|
98 |
+
</div>
|
99 |
+
);
|
100 |
+
};
|
101 |
+
|
102 |
+
interface UsersWindowProps {
|
103 |
+
open: boolean;
|
104 |
+
onClose: () => void;
|
105 |
+
}
|
106 |
+
|
107 |
+
export const UsersWindow = ({ open, onClose }: UsersWindowProps) => {
|
108 |
+
const [developerMode, setDeveloperMode] = useState(false);
|
109 |
+
const [activeTab, setActiveTab] = useState<TabType | null>(null);
|
110 |
+
const [loadingTab, setLoadingTab] = useState<TabType | null>(null);
|
111 |
+
const tabConfiguration = useStore(tabConfigurationStore);
|
112 |
+
|
113 |
+
// Status hooks
|
114 |
+
const { hasUpdate, currentVersion, acknowledgeUpdate } = useUpdateCheck();
|
115 |
+
const { hasNewFeatures, unviewedFeatures, acknowledgeAllFeatures } = useFeatures();
|
116 |
+
const { hasUnreadNotifications, unreadNotifications, markAllAsRead } = useNotifications();
|
117 |
+
const { hasConnectionIssues, currentIssue, acknowledgeIssue } = useConnectionStatus();
|
118 |
+
const { hasActiveWarnings, activeIssues, acknowledgeAllIssues } = useDebugStatus();
|
119 |
+
|
120 |
+
const handleDeveloperModeChange = (checked: boolean) => {
|
121 |
+
setDeveloperMode(checked);
|
122 |
+
};
|
123 |
+
|
124 |
+
const handleBack = () => {
|
125 |
+
setActiveTab(null);
|
126 |
+
};
|
127 |
+
|
128 |
+
// Only show tabs that are assigned to the user window AND are visible
|
129 |
+
const visibleUserTabs = tabConfiguration.userTabs
|
130 |
+
.filter((tab: TabVisibilityConfig) => tab.window === 'user' && tab.visible)
|
131 |
+
.sort((a: TabVisibilityConfig, b: TabVisibilityConfig) => (a.order || 0) - (b.order || 0));
|
132 |
+
|
133 |
+
const moveTab = (dragIndex: number, hoverIndex: number) => {
|
134 |
+
const draggedTab = visibleUserTabs[dragIndex];
|
135 |
+
const targetTab = visibleUserTabs[hoverIndex];
|
136 |
+
|
137 |
+
// Update the order of the dragged and target tabs
|
138 |
+
const updatedDraggedTab = { ...draggedTab, order: targetTab.order };
|
139 |
+
const updatedTargetTab = { ...targetTab, order: draggedTab.order };
|
140 |
+
|
141 |
+
// Update both tabs in the store
|
142 |
+
updateTabConfiguration(updatedDraggedTab);
|
143 |
+
updateTabConfiguration(updatedTargetTab);
|
144 |
+
};
|
145 |
+
|
146 |
+
const handleTabClick = async (tabId: TabType) => {
|
147 |
+
setLoadingTab(tabId);
|
148 |
+
setActiveTab(tabId);
|
149 |
+
|
150 |
+
// Acknowledge the status based on tab type
|
151 |
+
switch (tabId) {
|
152 |
+
case 'update':
|
153 |
+
await acknowledgeUpdate();
|
154 |
+
break;
|
155 |
+
case 'features':
|
156 |
+
await acknowledgeAllFeatures();
|
157 |
+
break;
|
158 |
+
case 'notifications':
|
159 |
+
await markAllAsRead();
|
160 |
+
break;
|
161 |
+
case 'connection':
|
162 |
+
acknowledgeIssue();
|
163 |
+
break;
|
164 |
+
case 'debug':
|
165 |
+
await acknowledgeAllIssues();
|
166 |
+
break;
|
167 |
+
}
|
168 |
+
|
169 |
+
// Simulate loading time (remove this in production)
|
170 |
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
171 |
+
setLoadingTab(null);
|
172 |
+
};
|
173 |
+
|
174 |
+
const getTabComponent = () => {
|
175 |
+
switch (activeTab) {
|
176 |
+
case 'profile':
|
177 |
+
return <ProfileTab />;
|
178 |
+
case 'settings':
|
179 |
+
return <SettingsTab />;
|
180 |
+
case 'notifications':
|
181 |
+
return <NotificationsTab />;
|
182 |
+
case 'features':
|
183 |
+
return <FeaturesTab />;
|
184 |
+
case 'data':
|
185 |
+
return <DataTab />;
|
186 |
+
case 'providers':
|
187 |
+
return <ProvidersTab />;
|
188 |
+
case 'connection':
|
189 |
+
return <ConnectionsTab />;
|
190 |
+
case 'debug':
|
191 |
+
return <DebugTab />;
|
192 |
+
case 'event-logs':
|
193 |
+
return <EventLogsTab />;
|
194 |
+
case 'update':
|
195 |
+
return <UpdateTab />;
|
196 |
+
default:
|
197 |
+
return null;
|
198 |
+
}
|
199 |
+
};
|
200 |
+
|
201 |
+
const getTabUpdateStatus = (tabId: TabType): boolean => {
|
202 |
+
switch (tabId) {
|
203 |
+
case 'update':
|
204 |
+
return hasUpdate;
|
205 |
+
case 'features':
|
206 |
+
return hasNewFeatures;
|
207 |
+
case 'notifications':
|
208 |
+
return hasUnreadNotifications;
|
209 |
+
case 'connection':
|
210 |
+
return hasConnectionIssues;
|
211 |
+
case 'debug':
|
212 |
+
return hasActiveWarnings;
|
213 |
+
default:
|
214 |
+
return false;
|
215 |
+
}
|
216 |
+
};
|
217 |
+
|
218 |
+
const getStatusMessage = (tabId: TabType): string => {
|
219 |
+
switch (tabId) {
|
220 |
+
case 'update':
|
221 |
+
return `New update available (v${currentVersion})`;
|
222 |
+
case 'features':
|
223 |
+
return `${unviewedFeatures.length} new feature${unviewedFeatures.length === 1 ? '' : 's'} to explore`;
|
224 |
+
case 'notifications':
|
225 |
+
return `${unreadNotifications.length} unread notification${unreadNotifications.length === 1 ? '' : 's'}`;
|
226 |
+
case 'connection':
|
227 |
+
return currentIssue === 'disconnected'
|
228 |
+
? 'Connection lost'
|
229 |
+
: currentIssue === 'high-latency'
|
230 |
+
? 'High latency detected'
|
231 |
+
: 'Connection issues detected';
|
232 |
+
case 'debug': {
|
233 |
+
const warnings = activeIssues.filter((i) => i.type === 'warning').length;
|
234 |
+
const errors = activeIssues.filter((i) => i.type === 'error').length;
|
235 |
+
|
236 |
+
return `${warnings} warning${warnings === 1 ? '' : 's'}, ${errors} error${errors === 1 ? '' : 's'}`;
|
237 |
+
}
|
238 |
+
default:
|
239 |
+
return '';
|
240 |
+
}
|
241 |
+
};
|
242 |
+
|
243 |
+
return (
|
244 |
+
<>
|
245 |
+
<DeveloperWindow open={developerMode} onClose={() => setDeveloperMode(false)} />
|
246 |
+
<DndProvider backend={HTML5Backend}>
|
247 |
+
<RadixDialog.Root open={open}>
|
248 |
+
<RadixDialog.Portal>
|
249 |
+
<div className="fixed inset-0 flex items-center justify-center z-[50]">
|
250 |
+
<RadixDialog.Overlay asChild>
|
251 |
+
<motion.div
|
252 |
+
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
|
253 |
+
initial={{ opacity: 0 }}
|
254 |
+
animate={{ opacity: 1 }}
|
255 |
+
exit={{ opacity: 0 }}
|
256 |
+
transition={{ duration: 0.2 }}
|
257 |
+
/>
|
258 |
+
</RadixDialog.Overlay>
|
259 |
+
<RadixDialog.Content aria-describedby={undefined} asChild>
|
260 |
+
<motion.div
|
261 |
+
className={classNames(
|
262 |
+
'relative',
|
263 |
+
'w-[1200px] h-[90vh]',
|
264 |
+
'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
|
265 |
+
'rounded-2xl shadow-2xl',
|
266 |
+
'border border-[#E5E5E5] dark:border-[#1A1A1A]',
|
267 |
+
'flex flex-col overflow-hidden',
|
268 |
+
'z-[51]',
|
269 |
+
)}
|
270 |
+
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
271 |
+
animate={{ opacity: 1, scale: 1, y: 0 }}
|
272 |
+
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
273 |
+
transition={{ duration: 0.2 }}
|
274 |
+
>
|
275 |
+
{/* Header */}
|
276 |
+
<div className="flex-none flex items-center justify-between px-6 py-4 border-b border-[#E5E5E5] dark:border-[#1A1A1A]">
|
277 |
+
<div className="flex items-center gap-3">
|
278 |
+
{activeTab ? (
|
279 |
+
<motion.button
|
280 |
+
onClick={handleBack}
|
281 |
+
className={classNames(
|
282 |
+
'flex items-center justify-center w-8 h-8 rounded-lg',
|
283 |
+
'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
|
284 |
+
'hover:bg-purple-500/10 dark:hover:bg-purple-500/20',
|
285 |
+
'group transition-all duration-200',
|
286 |
+
)}
|
287 |
+
whileHover={{ scale: 1.05 }}
|
288 |
+
whileTap={{ scale: 0.95 }}
|
289 |
+
>
|
290 |
+
<div className="i-ph:arrow-left w-4 h-4 text-bolt-elements-textSecondary group-hover:text-purple-500 transition-colors" />
|
291 |
+
</motion.button>
|
292 |
+
) : (
|
293 |
+
<motion.div
|
294 |
+
className="i-ph:lightning-fill w-5 h-5 text-purple-500"
|
295 |
+
initial={{ rotate: -10 }}
|
296 |
+
animate={{ rotate: 10 }}
|
297 |
+
transition={{
|
298 |
+
repeat: Infinity,
|
299 |
+
repeatType: 'reverse',
|
300 |
+
duration: 2,
|
301 |
+
ease: 'easeInOut',
|
302 |
+
}}
|
303 |
+
/>
|
304 |
+
)}
|
305 |
+
<DialogTitle className="text-lg font-medium text-bolt-elements-textPrimary">
|
306 |
+
{activeTab ? TAB_LABELS[activeTab] : 'Bolt Control Panel'}
|
307 |
+
</DialogTitle>
|
308 |
+
</div>
|
309 |
+
<div className="flex items-center gap-3">
|
310 |
+
<div className="flex items-center gap-2">
|
311 |
+
<Switch
|
312 |
+
checked={developerMode}
|
313 |
+
onCheckedChange={handleDeveloperModeChange}
|
314 |
+
className="data-[state=checked]:bg-purple-500"
|
315 |
+
aria-label="Toggle developer mode"
|
316 |
+
/>
|
317 |
+
<label className="text-sm text-bolt-elements-textSecondary">Developer Mode</label>
|
318 |
+
</div>
|
319 |
+
<motion.button
|
320 |
+
onClick={onClose}
|
321 |
+
className={classNames(
|
322 |
+
'flex items-center justify-center w-8 h-8 rounded-lg',
|
323 |
+
'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
|
324 |
+
'hover:bg-purple-500/10 dark:hover:bg-purple-500/20',
|
325 |
+
'group transition-all duration-200',
|
326 |
+
)}
|
327 |
+
whileHover={{ scale: 1.05 }}
|
328 |
+
whileTap={{ scale: 0.95 }}
|
329 |
+
>
|
330 |
+
<div className="i-ph:x w-4 h-4 text-bolt-elements-textSecondary group-hover:text-purple-500 transition-colors" />
|
331 |
+
</motion.button>
|
332 |
+
</div>
|
333 |
+
</div>
|
334 |
+
|
335 |
+
{/* Content */}
|
336 |
+
<div
|
337 |
+
className={classNames(
|
338 |
+
'flex-1',
|
339 |
+
'overflow-y-auto',
|
340 |
+
'hover:overflow-y-auto',
|
341 |
+
'scrollbar scrollbar-w-2',
|
342 |
+
'scrollbar-track-transparent',
|
343 |
+
'scrollbar-thumb-[#E5E5E5] hover:scrollbar-thumb-[#CCCCCC]',
|
344 |
+
'dark:scrollbar-thumb-[#333333] dark:hover:scrollbar-thumb-[#444444]',
|
345 |
+
'will-change-scroll',
|
346 |
+
'touch-auto',
|
347 |
+
)}
|
348 |
+
>
|
349 |
+
<motion.div
|
350 |
+
key={activeTab || 'home'}
|
351 |
+
initial={{ opacity: 0, y: 20 }}
|
352 |
+
animate={{ opacity: 1, y: 0 }}
|
353 |
+
className="p-6"
|
354 |
+
>
|
355 |
+
{activeTab ? (
|
356 |
+
getTabComponent()
|
357 |
+
) : (
|
358 |
+
<div className="grid grid-cols-4 gap-4">
|
359 |
+
{visibleUserTabs.map((tab: TabVisibilityConfig, index: number) => (
|
360 |
+
<DraggableTabTile
|
361 |
+
key={tab.id}
|
362 |
+
tab={tab}
|
363 |
+
index={index}
|
364 |
+
moveTab={moveTab}
|
365 |
+
onClick={() => handleTabClick(tab.id)}
|
366 |
+
isActive={activeTab === tab.id}
|
367 |
+
hasUpdate={getTabUpdateStatus(tab.id)}
|
368 |
+
statusMessage={getStatusMessage(tab.id)}
|
369 |
+
description={TAB_DESCRIPTIONS[tab.id]}
|
370 |
+
isLoading={loadingTab === tab.id}
|
371 |
+
/>
|
372 |
+
))}
|
373 |
+
</div>
|
374 |
+
)}
|
375 |
+
</motion.div>
|
376 |
+
</div>
|
377 |
+
</motion.div>
|
378 |
+
</RadixDialog.Content>
|
379 |
+
</div>
|
380 |
+
</RadixDialog.Portal>
|
381 |
+
</RadixDialog.Root>
|
382 |
+
</DndProvider>
|
383 |
+
</>
|
384 |
+
);
|
385 |
+
};
|
app/components/sidebar/Menu.client.tsx
CHANGED
@@ -3,7 +3,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
|
|
3 |
import { toast } from 'react-toastify';
|
4 |
import { Dialog, DialogButton, DialogDescription, DialogRoot, DialogTitle } from '~/components/ui/Dialog';
|
5 |
import { ThemeSwitch } from '~/components/ui/ThemeSwitch';
|
6 |
-
import {
|
7 |
import { SettingsButton } from '~/components/ui/SettingsButton';
|
8 |
import { db, deleteById, getAll, chatId, type ChatHistoryItem, useChatHistory } from '~/lib/persistence';
|
9 |
import { cubicEasingFn } from '~/utils/easings';
|
@@ -226,7 +226,7 @@ export const Menu = () => {
|
|
226 |
<ThemeSwitch />
|
227 |
</div>
|
228 |
</div>
|
229 |
-
<
|
230 |
</motion.div>
|
231 |
);
|
232 |
};
|
|
|
3 |
import { toast } from 'react-toastify';
|
4 |
import { Dialog, DialogButton, DialogDescription, DialogRoot, DialogTitle } from '~/components/ui/Dialog';
|
5 |
import { ThemeSwitch } from '~/components/ui/ThemeSwitch';
|
6 |
+
import { UsersWindow } from '~/components/settings/user/UsersWindow';
|
7 |
import { SettingsButton } from '~/components/ui/SettingsButton';
|
8 |
import { db, deleteById, getAll, chatId, type ChatHistoryItem, useChatHistory } from '~/lib/persistence';
|
9 |
import { cubicEasingFn } from '~/utils/easings';
|
|
|
226 |
<ThemeSwitch />
|
227 |
</div>
|
228 |
</div>
|
229 |
+
<UsersWindow open={isSettingsOpen} onClose={() => setIsSettingsOpen(false)} />
|
230 |
</motion.div>
|
231 |
);
|
232 |
};
|
app/components/ui/Dropdown.tsx
ADDED
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
2 |
+
import { type ReactNode } from 'react';
|
3 |
+
import { classNames } from '~/utils/classNames';
|
4 |
+
|
5 |
+
interface DropdownProps {
|
6 |
+
trigger: ReactNode;
|
7 |
+
children: ReactNode;
|
8 |
+
align?: 'start' | 'center' | 'end';
|
9 |
+
sideOffset?: number;
|
10 |
+
}
|
11 |
+
|
12 |
+
interface DropdownItemProps {
|
13 |
+
children: ReactNode;
|
14 |
+
onSelect?: () => void;
|
15 |
+
className?: string;
|
16 |
+
}
|
17 |
+
|
18 |
+
export const DropdownItem = ({ children, onSelect, className }: DropdownItemProps) => (
|
19 |
+
<DropdownMenu.Item
|
20 |
+
className={classNames(
|
21 |
+
'relative flex items-center gap-2 px-3 py-2 rounded-lg text-sm',
|
22 |
+
'text-bolt-elements-textPrimary hover:text-bolt-elements-textPrimary',
|
23 |
+
'hover:bg-bolt-elements-background-depth-3',
|
24 |
+
'transition-colors cursor-pointer',
|
25 |
+
'outline-none',
|
26 |
+
className,
|
27 |
+
)}
|
28 |
+
onSelect={onSelect}
|
29 |
+
>
|
30 |
+
{children}
|
31 |
+
</DropdownMenu.Item>
|
32 |
+
);
|
33 |
+
|
34 |
+
export const DropdownSeparator = () => <DropdownMenu.Separator className="h-px bg-bolt-elements-borderColor my-1" />;
|
35 |
+
|
36 |
+
export const Dropdown = ({ trigger, children, align = 'end', sideOffset = 5 }: DropdownProps) => {
|
37 |
+
return (
|
38 |
+
<DropdownMenu.Root>
|
39 |
+
<DropdownMenu.Trigger asChild>{trigger}</DropdownMenu.Trigger>
|
40 |
+
|
41 |
+
<DropdownMenu.Portal>
|
42 |
+
<DropdownMenu.Content
|
43 |
+
className={classNames(
|
44 |
+
'min-w-[220px] rounded-lg p-2',
|
45 |
+
'bg-bolt-elements-background-depth-2',
|
46 |
+
'border border-bolt-elements-borderColor',
|
47 |
+
'shadow-lg',
|
48 |
+
'animate-in fade-in-80 zoom-in-95',
|
49 |
+
'data-[side=bottom]:slide-in-from-top-2',
|
50 |
+
'data-[side=left]:slide-in-from-right-2',
|
51 |
+
'data-[side=right]:slide-in-from-left-2',
|
52 |
+
'data-[side=top]:slide-in-from-bottom-2',
|
53 |
+
'z-[1000]',
|
54 |
+
)}
|
55 |
+
sideOffset={sideOffset}
|
56 |
+
align={align}
|
57 |
+
>
|
58 |
+
{children}
|
59 |
+
</DropdownMenu.Content>
|
60 |
+
</DropdownMenu.Portal>
|
61 |
+
</DropdownMenu.Root>
|
62 |
+
);
|
63 |
+
};
|
app/components/workbench/Preview.tsx
CHANGED
@@ -11,13 +11,14 @@ interface WindowSize {
|
|
11 |
name: string;
|
12 |
width: number;
|
13 |
height: number;
|
|
|
14 |
}
|
15 |
|
16 |
const WINDOW_SIZES: WindowSize[] = [
|
17 |
-
{ name: 'Mobile
|
18 |
-
{ name: 'Tablet
|
19 |
-
{ name: 'Laptop
|
20 |
-
{ name: 'Desktop
|
21 |
];
|
22 |
|
23 |
export const Preview = memo(() => {
|
@@ -249,14 +250,17 @@ export const Preview = memo(() => {
|
|
249 |
{isPortDropdownOpen && (
|
250 |
<div className="z-iframe-overlay w-full h-full absolute" onClick={() => setIsPortDropdownOpen(false)} />
|
251 |
)}
|
252 |
-
<div className="bg-bolt-elements-background-depth-2 p-2 flex items-center gap-
|
253 |
-
<
|
254 |
-
|
255 |
-
|
256 |
-
|
257 |
-
|
258 |
-
|
259 |
-
|
|
|
|
|
|
|
260 |
<input
|
261 |
title="URL"
|
262 |
ref={inputRef}
|
@@ -278,68 +282,80 @@ export const Preview = memo(() => {
|
|
278 |
/>
|
279 |
</div>
|
280 |
|
281 |
-
|
282 |
-
|
283 |
-
|
284 |
-
|
285 |
-
|
286 |
-
|
287 |
-
|
288 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
289 |
/>
|
290 |
-
|
291 |
-
|
292 |
-
<IconButton
|
293 |
-
icon="i-ph:devices"
|
294 |
-
onClick={toggleDeviceMode}
|
295 |
-
title={isDeviceModeOn ? 'Switch to Responsive Mode' : 'Switch to Device Mode'}
|
296 |
-
/>
|
297 |
-
|
298 |
-
<IconButton
|
299 |
-
icon="i-ph:layout-light"
|
300 |
-
onClick={() => setIsPreviewOnly(!isPreviewOnly)}
|
301 |
-
title={isPreviewOnly ? 'Show Full Interface' : 'Show Preview Only'}
|
302 |
-
/>
|
303 |
-
|
304 |
-
<IconButton
|
305 |
-
icon={isFullscreen ? 'i-ph:arrows-in' : 'i-ph:arrows-out'}
|
306 |
-
onClick={toggleFullscreen}
|
307 |
-
title={isFullscreen ? 'Exit Full Screen' : 'Full Screen'}
|
308 |
-
/>
|
309 |
-
|
310 |
-
<div className="relative">
|
311 |
<IconButton
|
312 |
-
icon="i-ph:
|
313 |
-
onClick={() =>
|
314 |
-
title={
|
315 |
/>
|
|
|
316 |
<IconButton
|
317 |
-
icon=
|
318 |
-
onClick={
|
319 |
-
|
320 |
-
title="Select Window Size"
|
321 |
/>
|
322 |
|
323 |
-
|
324 |
-
|
325 |
-
|
326 |
-
|
327 |
-
|
328 |
-
|
329 |
-
|
330 |
-
|
331 |
-
|
332 |
-
|
333 |
-
|
334 |
-
|
335 |
-
|
336 |
-
|
337 |
-
|
338 |
-
|
339 |
-
)
|
340 |
-
|
341 |
-
|
342 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
343 |
</div>
|
344 |
</div>
|
345 |
|
@@ -349,7 +365,7 @@ export const Preview = memo(() => {
|
|
349 |
width: isDeviceModeOn ? `${widthPercent}%` : '100%',
|
350 |
height: '100%',
|
351 |
overflow: 'visible',
|
352 |
-
background: '
|
353 |
position: 'relative',
|
354 |
display: 'flex',
|
355 |
}}
|
@@ -359,7 +375,7 @@ export const Preview = memo(() => {
|
|
359 |
<iframe
|
360 |
ref={iframeRef}
|
361 |
title="preview"
|
362 |
-
className="border-none w-full h-full bg-
|
363 |
src={iframeUrl}
|
364 |
sandbox="allow-scripts allow-forms allow-popups allow-modals allow-storage-access-by-user-activation allow-same-origin"
|
365 |
allow="cross-origin-isolated"
|
@@ -371,7 +387,9 @@ export const Preview = memo(() => {
|
|
371 |
/>
|
372 |
</>
|
373 |
) : (
|
374 |
-
<div className="flex w-full h-full justify-center items-center bg-
|
|
|
|
|
375 |
)}
|
376 |
|
377 |
{isDeviceModeOn && (
|
|
|
11 |
name: string;
|
12 |
width: number;
|
13 |
height: number;
|
14 |
+
icon: string;
|
15 |
}
|
16 |
|
17 |
const WINDOW_SIZES: WindowSize[] = [
|
18 |
+
{ name: 'Mobile', width: 375, height: 667, icon: 'i-ph:device-mobile' },
|
19 |
+
{ name: 'Tablet', width: 768, height: 1024, icon: 'i-ph:device-tablet' },
|
20 |
+
{ name: 'Laptop', width: 1366, height: 768, icon: 'i-ph:laptop' },
|
21 |
+
{ name: 'Desktop', width: 1920, height: 1080, icon: 'i-ph:monitor' },
|
22 |
];
|
23 |
|
24 |
export const Preview = memo(() => {
|
|
|
250 |
{isPortDropdownOpen && (
|
251 |
<div className="z-iframe-overlay w-full h-full absolute" onClick={() => setIsPortDropdownOpen(false)} />
|
252 |
)}
|
253 |
+
<div className="bg-bolt-elements-background-depth-2 p-2 flex items-center gap-2">
|
254 |
+
<div className="flex items-center gap-2">
|
255 |
+
<IconButton icon="i-ph:arrow-clockwise" onClick={reloadPreview} />
|
256 |
+
<IconButton
|
257 |
+
icon="i-ph:selection"
|
258 |
+
onClick={() => setIsSelectionMode(!isSelectionMode)}
|
259 |
+
className={isSelectionMode ? 'bg-bolt-elements-background-depth-3' : ''}
|
260 |
+
/>
|
261 |
+
</div>
|
262 |
+
|
263 |
+
<div className="flex-grow flex items-center gap-1 bg-bolt-elements-preview-addressBar-background border border-bolt-elements-borderColor text-bolt-elements-preview-addressBar-text rounded-full px-3 py-1 text-sm hover:bg-bolt-elements-preview-addressBar-backgroundHover hover:focus-within:bg-bolt-elements-preview-addressBar-backgroundActive focus-within:bg-bolt-elements-preview-addressBar-backgroundActive focus-within-border-bolt-elements-borderColorActive focus-within:text-bolt-elements-preview-addressBar-textActive">
|
264 |
<input
|
265 |
title="URL"
|
266 |
ref={inputRef}
|
|
|
282 |
/>
|
283 |
</div>
|
284 |
|
285 |
+
<div className="flex items-center gap-2">
|
286 |
+
{previews.length > 1 && (
|
287 |
+
<PortDropdown
|
288 |
+
activePreviewIndex={activePreviewIndex}
|
289 |
+
setActivePreviewIndex={setActivePreviewIndex}
|
290 |
+
isDropdownOpen={isPortDropdownOpen}
|
291 |
+
setHasSelectedPreview={(value) => (hasSelectedPreview.current = value)}
|
292 |
+
setIsDropdownOpen={setIsPortDropdownOpen}
|
293 |
+
previews={previews}
|
294 |
+
/>
|
295 |
+
)}
|
296 |
+
|
297 |
+
<IconButton
|
298 |
+
icon="i-ph:devices"
|
299 |
+
onClick={toggleDeviceMode}
|
300 |
+
title={isDeviceModeOn ? 'Switch to Responsive Mode' : 'Switch to Device Mode'}
|
301 |
/>
|
302 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
303 |
<IconButton
|
304 |
+
icon="i-ph:layout-light"
|
305 |
+
onClick={() => setIsPreviewOnly(!isPreviewOnly)}
|
306 |
+
title={isPreviewOnly ? 'Show Full Interface' : 'Show Preview Only'}
|
307 |
/>
|
308 |
+
|
309 |
<IconButton
|
310 |
+
icon={isFullscreen ? 'i-ph:arrows-in' : 'i-ph:arrows-out'}
|
311 |
+
onClick={toggleFullscreen}
|
312 |
+
title={isFullscreen ? 'Exit Full Screen' : 'Full Screen'}
|
|
|
313 |
/>
|
314 |
|
315 |
+
<div className="flex items-center relative">
|
316 |
+
<IconButton
|
317 |
+
icon="i-ph:arrow-square-out"
|
318 |
+
onClick={() => openInNewWindow(selectedWindowSize)}
|
319 |
+
title={`Open Preview in ${selectedWindowSize.name} Window`}
|
320 |
+
/>
|
321 |
+
<IconButton
|
322 |
+
icon="i-ph:caret-down"
|
323 |
+
onClick={() => setIsWindowSizeDropdownOpen(!isWindowSizeDropdownOpen)}
|
324 |
+
className="ml-1"
|
325 |
+
title="Select Window Size"
|
326 |
+
/>
|
327 |
+
|
328 |
+
{isWindowSizeDropdownOpen && (
|
329 |
+
<>
|
330 |
+
<div className="fixed inset-0 z-50" onClick={() => setIsWindowSizeDropdownOpen(false)} />
|
331 |
+
<div className="absolute right-0 top-full mt-2 z-50 min-w-[240px] bg-white dark:bg-black rounded-xl shadow-2xl border border-[#E5E7EB] dark:border-[rgba(255,255,255,0.1)] overflow-hidden">
|
332 |
+
{WINDOW_SIZES.map((size) => (
|
333 |
+
<button
|
334 |
+
key={size.name}
|
335 |
+
className="w-full px-4 py-3.5 text-left text-[#111827] dark:text-gray-300 text-sm whitespace-nowrap flex items-center gap-3 group hover:bg-[#F5EEFF] dark:hover:bg-gray-900 bg-white dark:bg-black"
|
336 |
+
onClick={() => {
|
337 |
+
setSelectedWindowSize(size);
|
338 |
+
setIsWindowSizeDropdownOpen(false);
|
339 |
+
openInNewWindow(size);
|
340 |
+
}}
|
341 |
+
>
|
342 |
+
<div
|
343 |
+
className={`${size.icon} w-5 h-5 text-[#6B7280] dark:text-gray-400 group-hover:text-[#6D28D9] dark:group-hover:text-[#6D28D9] transition-colors duration-200`}
|
344 |
+
/>
|
345 |
+
<div className="flex flex-col">
|
346 |
+
<span className="font-medium group-hover:text-[#6D28D9] dark:group-hover:text-[#6D28D9] transition-colors duration-200">
|
347 |
+
{size.name}
|
348 |
+
</span>
|
349 |
+
<span className="text-xs text-[#6B7280] dark:text-gray-400 group-hover:text-[#6D28D9] dark:group-hover:text-[#6D28D9] transition-colors duration-200">
|
350 |
+
{size.width} × {size.height}
|
351 |
+
</span>
|
352 |
+
</div>
|
353 |
+
</button>
|
354 |
+
))}
|
355 |
+
</div>
|
356 |
+
</>
|
357 |
+
)}
|
358 |
+
</div>
|
359 |
</div>
|
360 |
</div>
|
361 |
|
|
|
365 |
width: isDeviceModeOn ? `${widthPercent}%` : '100%',
|
366 |
height: '100%',
|
367 |
overflow: 'visible',
|
368 |
+
background: 'var(--bolt-elements-background-depth-1)',
|
369 |
position: 'relative',
|
370 |
display: 'flex',
|
371 |
}}
|
|
|
375 |
<iframe
|
376 |
ref={iframeRef}
|
377 |
title="preview"
|
378 |
+
className="border-none w-full h-full bg-bolt-elements-background-depth-1"
|
379 |
src={iframeUrl}
|
380 |
sandbox="allow-scripts allow-forms allow-popups allow-modals allow-storage-access-by-user-activation allow-same-origin"
|
381 |
allow="cross-origin-isolated"
|
|
|
387 |
/>
|
388 |
</>
|
389 |
) : (
|
390 |
+
<div className="flex w-full h-full justify-center items-center bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary">
|
391 |
+
No preview available
|
392 |
+
</div>
|
393 |
)}
|
394 |
|
395 |
{isDeviceModeOn && (
|
app/lib/api/connection.ts
ADDED
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export interface ConnectionStatus {
|
2 |
+
connected: boolean;
|
3 |
+
latency: number;
|
4 |
+
lastChecked: string;
|
5 |
+
}
|
6 |
+
|
7 |
+
export const checkConnection = async (): Promise<ConnectionStatus> => {
|
8 |
+
/*
|
9 |
+
* TODO: Implement actual connection check logic
|
10 |
+
* This is a mock implementation
|
11 |
+
*/
|
12 |
+
const connected = Math.random() > 0.1; // 90% chance of being connected
|
13 |
+
return {
|
14 |
+
connected,
|
15 |
+
latency: connected ? Math.floor(Math.random() * 1500) : 0, // Random latency between 0-1500ms
|
16 |
+
lastChecked: new Date().toISOString(),
|
17 |
+
};
|
18 |
+
};
|
app/lib/api/debug.ts
ADDED
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export interface DebugWarning {
|
2 |
+
id: string;
|
3 |
+
message: string;
|
4 |
+
timestamp: string;
|
5 |
+
code: string;
|
6 |
+
}
|
7 |
+
|
8 |
+
export interface DebugError {
|
9 |
+
id: string;
|
10 |
+
message: string;
|
11 |
+
timestamp: string;
|
12 |
+
stack?: string;
|
13 |
+
}
|
14 |
+
|
15 |
+
export interface DebugStatus {
|
16 |
+
warnings: DebugWarning[];
|
17 |
+
errors: DebugError[];
|
18 |
+
lastChecked: string;
|
19 |
+
}
|
20 |
+
|
21 |
+
export interface DebugIssue {
|
22 |
+
id: string;
|
23 |
+
type: 'warning' | 'error';
|
24 |
+
message: string;
|
25 |
+
}
|
26 |
+
|
27 |
+
export const getDebugStatus = async (): Promise<DebugStatus> => {
|
28 |
+
/*
|
29 |
+
* TODO: Implement actual debug status logic
|
30 |
+
* This is a mock implementation
|
31 |
+
*/
|
32 |
+
return {
|
33 |
+
warnings: [
|
34 |
+
{
|
35 |
+
id: 'warn-1',
|
36 |
+
message: 'High memory usage detected',
|
37 |
+
timestamp: new Date().toISOString(),
|
38 |
+
code: 'MEM_HIGH',
|
39 |
+
},
|
40 |
+
],
|
41 |
+
errors: [
|
42 |
+
{
|
43 |
+
id: 'err-1',
|
44 |
+
message: 'Failed to connect to database',
|
45 |
+
timestamp: new Date().toISOString(),
|
46 |
+
stack: 'Error: Connection timeout',
|
47 |
+
},
|
48 |
+
],
|
49 |
+
lastChecked: new Date().toISOString(),
|
50 |
+
};
|
51 |
+
};
|
52 |
+
|
53 |
+
export const acknowledgeWarning = async (warningId: string): Promise<void> => {
|
54 |
+
/*
|
55 |
+
* TODO: Implement actual warning acknowledgment logic
|
56 |
+
*/
|
57 |
+
console.log(`Acknowledging warning ${warningId}`);
|
58 |
+
};
|
59 |
+
|
60 |
+
export const acknowledgeError = async (errorId: string): Promise<void> => {
|
61 |
+
/*
|
62 |
+
* TODO: Implement actual error acknowledgment logic
|
63 |
+
*/
|
64 |
+
console.log(`Acknowledging error ${errorId}`);
|
65 |
+
};
|
app/lib/api/features.ts
ADDED
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export interface Feature {
|
2 |
+
id: string;
|
3 |
+
name: string;
|
4 |
+
description: string;
|
5 |
+
viewed: boolean;
|
6 |
+
releaseDate: string;
|
7 |
+
}
|
8 |
+
|
9 |
+
export const getFeatureFlags = async (): Promise<Feature[]> => {
|
10 |
+
/*
|
11 |
+
* TODO: Implement actual feature flags logic
|
12 |
+
* This is a mock implementation
|
13 |
+
*/
|
14 |
+
return [
|
15 |
+
{
|
16 |
+
id: 'feature-1',
|
17 |
+
name: 'Dark Mode',
|
18 |
+
description: 'Enable dark mode for better night viewing',
|
19 |
+
viewed: true,
|
20 |
+
releaseDate: '2024-03-15',
|
21 |
+
},
|
22 |
+
{
|
23 |
+
id: 'feature-2',
|
24 |
+
name: 'Tab Management',
|
25 |
+
description: 'Customize your tab layout',
|
26 |
+
viewed: false,
|
27 |
+
releaseDate: '2024-03-20',
|
28 |
+
},
|
29 |
+
];
|
30 |
+
};
|
31 |
+
|
32 |
+
export const markFeatureViewed = async (featureId: string): Promise<void> => {
|
33 |
+
/* TODO: Implement actual feature viewed logic */
|
34 |
+
console.log(`Marking feature ${featureId} as viewed`);
|
35 |
+
};
|
app/lib/api/notifications.ts
ADDED
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export interface Notification {
|
2 |
+
id: string;
|
3 |
+
title: string;
|
4 |
+
message: string;
|
5 |
+
type: 'info' | 'warning' | 'error' | 'success';
|
6 |
+
read: boolean;
|
7 |
+
timestamp: string;
|
8 |
+
}
|
9 |
+
|
10 |
+
export const getNotifications = async (): Promise<Notification[]> => {
|
11 |
+
/*
|
12 |
+
* TODO: Implement actual notifications logic
|
13 |
+
* This is a mock implementation
|
14 |
+
*/
|
15 |
+
return [
|
16 |
+
{
|
17 |
+
id: 'notif-1',
|
18 |
+
title: 'Welcome to Bolt',
|
19 |
+
message: 'Get started by exploring the features',
|
20 |
+
type: 'info',
|
21 |
+
read: true,
|
22 |
+
timestamp: new Date().toISOString(),
|
23 |
+
},
|
24 |
+
{
|
25 |
+
id: 'notif-2',
|
26 |
+
title: 'New Update Available',
|
27 |
+
message: 'Version 1.0.1 is now available',
|
28 |
+
type: 'info',
|
29 |
+
read: false,
|
30 |
+
timestamp: new Date().toISOString(),
|
31 |
+
},
|
32 |
+
];
|
33 |
+
};
|
34 |
+
|
35 |
+
export const markNotificationRead = async (notificationId: string): Promise<void> => {
|
36 |
+
/*
|
37 |
+
* TODO: Implement actual notification read logic
|
38 |
+
*/
|
39 |
+
console.log(`Marking notification ${notificationId} as read`);
|
40 |
+
};
|
app/lib/api/updates.ts
ADDED
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export interface UpdateCheckResult {
|
2 |
+
available: boolean;
|
3 |
+
version: string;
|
4 |
+
releaseNotes?: string;
|
5 |
+
}
|
6 |
+
|
7 |
+
export const checkForUpdates = async (): Promise<UpdateCheckResult> => {
|
8 |
+
/*
|
9 |
+
* TODO: Implement actual update check logic
|
10 |
+
* This is a mock implementation
|
11 |
+
*/
|
12 |
+
return {
|
13 |
+
available: Math.random() > 0.7, // 30% chance of update
|
14 |
+
version: '1.0.1',
|
15 |
+
releaseNotes: 'Bug fixes and performance improvements',
|
16 |
+
};
|
17 |
+
};
|
18 |
+
|
19 |
+
export const acknowledgeUpdate = async (version: string): Promise<void> => {
|
20 |
+
/*
|
21 |
+
* TODO: Implement actual update acknowledgment logic
|
22 |
+
* This is a mock implementation that would typically:
|
23 |
+
* 1. Store the acknowledged version in a persistent store
|
24 |
+
* 2. Update the UI state
|
25 |
+
* 3. Potentially send analytics
|
26 |
+
*/
|
27 |
+
console.log(`Acknowledging update version ${version}`);
|
28 |
+
};
|
app/lib/hooks/index.ts
CHANGED
@@ -4,3 +4,8 @@ export * from './useShortcuts';
|
|
4 |
export * from './useSnapScroll';
|
5 |
export * from './useEditChatDescription';
|
6 |
export { default } from './useViewport';
|
|
|
|
|
|
|
|
|
|
|
|
4 |
export * from './useSnapScroll';
|
5 |
export * from './useEditChatDescription';
|
6 |
export { default } from './useViewport';
|
7 |
+
export { useUpdateCheck } from './useUpdateCheck';
|
8 |
+
export { useFeatures } from './useFeatures';
|
9 |
+
export { useNotifications } from './useNotifications';
|
10 |
+
export { useConnectionStatus } from './useConnectionStatus';
|
11 |
+
export { useDebugStatus } from './useDebugStatus';
|
app/lib/hooks/useConnectionStatus.ts
ADDED
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useState, useEffect } from 'react';
|
2 |
+
import { checkConnection } from '~/lib/api/connection';
|
3 |
+
|
4 |
+
const ACKNOWLEDGED_CONNECTION_ISSUE_KEY = 'bolt_acknowledged_connection_issue';
|
5 |
+
|
6 |
+
type ConnectionIssueType = 'disconnected' | 'high-latency' | null;
|
7 |
+
|
8 |
+
const getAcknowledgedIssue = (): string | null => {
|
9 |
+
try {
|
10 |
+
return localStorage.getItem(ACKNOWLEDGED_CONNECTION_ISSUE_KEY);
|
11 |
+
} catch {
|
12 |
+
return null;
|
13 |
+
}
|
14 |
+
};
|
15 |
+
|
16 |
+
export const useConnectionStatus = () => {
|
17 |
+
const [hasConnectionIssues, setHasConnectionIssues] = useState(false);
|
18 |
+
const [currentIssue, setCurrentIssue] = useState<ConnectionIssueType>(null);
|
19 |
+
const [acknowledgedIssue, setAcknowledgedIssue] = useState<string | null>(() => getAcknowledgedIssue());
|
20 |
+
|
21 |
+
const checkStatus = async () => {
|
22 |
+
try {
|
23 |
+
const status = await checkConnection();
|
24 |
+
const issue = !status.connected ? 'disconnected' : status.latency > 1000 ? 'high-latency' : null;
|
25 |
+
|
26 |
+
setCurrentIssue(issue);
|
27 |
+
|
28 |
+
// Only show issues if they're new or different from the acknowledged one
|
29 |
+
setHasConnectionIssues(issue !== null && issue !== acknowledgedIssue);
|
30 |
+
} catch (error) {
|
31 |
+
console.error('Failed to check connection:', error);
|
32 |
+
|
33 |
+
// Show connection issues if we can't even check the status
|
34 |
+
setCurrentIssue('disconnected');
|
35 |
+
setHasConnectionIssues(true);
|
36 |
+
}
|
37 |
+
};
|
38 |
+
|
39 |
+
useEffect(() => {
|
40 |
+
// Check immediately and then every 10 seconds
|
41 |
+
checkStatus();
|
42 |
+
|
43 |
+
const interval = setInterval(checkStatus, 10 * 1000);
|
44 |
+
|
45 |
+
return () => clearInterval(interval);
|
46 |
+
}, [acknowledgedIssue]);
|
47 |
+
|
48 |
+
const acknowledgeIssue = () => {
|
49 |
+
setAcknowledgedIssue(currentIssue);
|
50 |
+
setAcknowledgedIssue(currentIssue);
|
51 |
+
setHasConnectionIssues(false);
|
52 |
+
};
|
53 |
+
|
54 |
+
const resetAcknowledgment = () => {
|
55 |
+
setAcknowledgedIssue(null);
|
56 |
+
setAcknowledgedIssue(null);
|
57 |
+
checkStatus();
|
58 |
+
};
|
59 |
+
|
60 |
+
return { hasConnectionIssues, currentIssue, acknowledgeIssue, resetAcknowledgment };
|
61 |
+
};
|
app/lib/hooks/useDebugStatus.ts
ADDED
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useState, useEffect } from 'react';
|
2 |
+
import { getDebugStatus, acknowledgeWarning, acknowledgeError, type DebugIssue } from '~/lib/api/debug';
|
3 |
+
|
4 |
+
const ACKNOWLEDGED_DEBUG_ISSUES_KEY = 'bolt_acknowledged_debug_issues';
|
5 |
+
|
6 |
+
const getAcknowledgedIssues = (): string[] => {
|
7 |
+
try {
|
8 |
+
const stored = localStorage.getItem(ACKNOWLEDGED_DEBUG_ISSUES_KEY);
|
9 |
+
return stored ? JSON.parse(stored) : [];
|
10 |
+
} catch {
|
11 |
+
return [];
|
12 |
+
}
|
13 |
+
};
|
14 |
+
|
15 |
+
const setAcknowledgedIssues = (issueIds: string[]) => {
|
16 |
+
try {
|
17 |
+
localStorage.setItem(ACKNOWLEDGED_DEBUG_ISSUES_KEY, JSON.stringify(issueIds));
|
18 |
+
} catch (error) {
|
19 |
+
console.error('Failed to persist acknowledged debug issues:', error);
|
20 |
+
}
|
21 |
+
};
|
22 |
+
|
23 |
+
export const useDebugStatus = () => {
|
24 |
+
const [hasActiveWarnings, setHasActiveWarnings] = useState(false);
|
25 |
+
const [activeIssues, setActiveIssues] = useState<DebugIssue[]>([]);
|
26 |
+
const [acknowledgedIssueIds, setAcknowledgedIssueIds] = useState<string[]>(() => getAcknowledgedIssues());
|
27 |
+
|
28 |
+
const checkDebugStatus = async () => {
|
29 |
+
try {
|
30 |
+
const status = await getDebugStatus();
|
31 |
+
const issues: DebugIssue[] = [
|
32 |
+
...status.warnings.map((w) => ({ ...w, type: 'warning' as const })),
|
33 |
+
...status.errors.map((e) => ({ ...e, type: 'error' as const })),
|
34 |
+
].filter((issue) => !acknowledgedIssueIds.includes(issue.id));
|
35 |
+
|
36 |
+
setActiveIssues(issues);
|
37 |
+
setHasActiveWarnings(issues.length > 0);
|
38 |
+
} catch (error) {
|
39 |
+
console.error('Failed to check debug status:', error);
|
40 |
+
}
|
41 |
+
};
|
42 |
+
|
43 |
+
useEffect(() => {
|
44 |
+
// Check immediately and then every 5 seconds
|
45 |
+
checkDebugStatus();
|
46 |
+
|
47 |
+
const interval = setInterval(checkDebugStatus, 5 * 1000);
|
48 |
+
|
49 |
+
return () => clearInterval(interval);
|
50 |
+
}, [acknowledgedIssueIds]);
|
51 |
+
|
52 |
+
const acknowledgeIssue = async (issue: DebugIssue) => {
|
53 |
+
try {
|
54 |
+
if (issue.type === 'warning') {
|
55 |
+
await acknowledgeWarning(issue.id);
|
56 |
+
} else {
|
57 |
+
await acknowledgeError(issue.id);
|
58 |
+
}
|
59 |
+
|
60 |
+
const newAcknowledgedIds = [...acknowledgedIssueIds, issue.id];
|
61 |
+
setAcknowledgedIssueIds(newAcknowledgedIds);
|
62 |
+
setAcknowledgedIssues(newAcknowledgedIds);
|
63 |
+
setActiveIssues((prev) => prev.filter((i) => i.id !== issue.id));
|
64 |
+
setHasActiveWarnings(activeIssues.length > 1);
|
65 |
+
} catch (error) {
|
66 |
+
console.error('Failed to acknowledge issue:', error);
|
67 |
+
}
|
68 |
+
};
|
69 |
+
|
70 |
+
const acknowledgeAllIssues = async () => {
|
71 |
+
try {
|
72 |
+
await Promise.all(
|
73 |
+
activeIssues.map((issue) =>
|
74 |
+
issue.type === 'warning' ? acknowledgeWarning(issue.id) : acknowledgeError(issue.id),
|
75 |
+
),
|
76 |
+
);
|
77 |
+
|
78 |
+
const newAcknowledgedIds = [...acknowledgedIssueIds, ...activeIssues.map((i) => i.id)];
|
79 |
+
setAcknowledgedIssueIds(newAcknowledgedIds);
|
80 |
+
setAcknowledgedIssues(newAcknowledgedIds);
|
81 |
+
setActiveIssues([]);
|
82 |
+
setHasActiveWarnings(false);
|
83 |
+
} catch (error) {
|
84 |
+
console.error('Failed to acknowledge all issues:', error);
|
85 |
+
}
|
86 |
+
};
|
87 |
+
|
88 |
+
return { hasActiveWarnings, activeIssues, acknowledgeIssue, acknowledgeAllIssues };
|
89 |
+
};
|
app/lib/hooks/useFeatures.ts
ADDED
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useState, useEffect } from 'react';
|
2 |
+
import { getFeatureFlags, markFeatureViewed, type Feature } from '~/lib/api/features';
|
3 |
+
|
4 |
+
const VIEWED_FEATURES_KEY = 'bolt_viewed_features';
|
5 |
+
|
6 |
+
const getViewedFeatures = (): string[] => {
|
7 |
+
try {
|
8 |
+
const stored = localStorage.getItem(VIEWED_FEATURES_KEY);
|
9 |
+
return stored ? JSON.parse(stored) : [];
|
10 |
+
} catch {
|
11 |
+
return [];
|
12 |
+
}
|
13 |
+
};
|
14 |
+
|
15 |
+
const setViewedFeatures = (featureIds: string[]) => {
|
16 |
+
try {
|
17 |
+
localStorage.setItem(VIEWED_FEATURES_KEY, JSON.stringify(featureIds));
|
18 |
+
} catch (error) {
|
19 |
+
console.error('Failed to persist viewed features:', error);
|
20 |
+
}
|
21 |
+
};
|
22 |
+
|
23 |
+
export const useFeatures = () => {
|
24 |
+
const [hasNewFeatures, setHasNewFeatures] = useState(false);
|
25 |
+
const [unviewedFeatures, setUnviewedFeatures] = useState<Feature[]>([]);
|
26 |
+
const [viewedFeatureIds, setViewedFeatureIds] = useState<string[]>(() => getViewedFeatures());
|
27 |
+
|
28 |
+
useEffect(() => {
|
29 |
+
const checkNewFeatures = async () => {
|
30 |
+
try {
|
31 |
+
const features = await getFeatureFlags();
|
32 |
+
const unviewed = features.filter((feature) => !viewedFeatureIds.includes(feature.id));
|
33 |
+
setUnviewedFeatures(unviewed);
|
34 |
+
setHasNewFeatures(unviewed.length > 0);
|
35 |
+
} catch (error) {
|
36 |
+
console.error('Failed to check for new features:', error);
|
37 |
+
}
|
38 |
+
};
|
39 |
+
|
40 |
+
checkNewFeatures();
|
41 |
+
}, [viewedFeatureIds]);
|
42 |
+
|
43 |
+
const acknowledgeFeature = async (featureId: string) => {
|
44 |
+
try {
|
45 |
+
await markFeatureViewed(featureId);
|
46 |
+
|
47 |
+
const newViewedIds = [...viewedFeatureIds, featureId];
|
48 |
+
setViewedFeatureIds(newViewedIds);
|
49 |
+
setViewedFeatures(newViewedIds);
|
50 |
+
setUnviewedFeatures((prev) => prev.filter((feature) => feature.id !== featureId));
|
51 |
+
setHasNewFeatures(unviewedFeatures.length > 1);
|
52 |
+
} catch (error) {
|
53 |
+
console.error('Failed to acknowledge feature:', error);
|
54 |
+
}
|
55 |
+
};
|
56 |
+
|
57 |
+
const acknowledgeAllFeatures = async () => {
|
58 |
+
try {
|
59 |
+
await Promise.all(unviewedFeatures.map((feature) => markFeatureViewed(feature.id)));
|
60 |
+
|
61 |
+
const newViewedIds = [...viewedFeatureIds, ...unviewedFeatures.map((f) => f.id)];
|
62 |
+
setViewedFeatureIds(newViewedIds);
|
63 |
+
setViewedFeatures(newViewedIds);
|
64 |
+
setUnviewedFeatures([]);
|
65 |
+
setHasNewFeatures(false);
|
66 |
+
} catch (error) {
|
67 |
+
console.error('Failed to acknowledge all features:', error);
|
68 |
+
}
|
69 |
+
};
|
70 |
+
|
71 |
+
return { hasNewFeatures, unviewedFeatures, acknowledgeFeature, acknowledgeAllFeatures };
|
72 |
+
};
|
app/lib/hooks/useNotifications.ts
ADDED
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useState, useEffect } from 'react';
|
2 |
+
import { getNotifications, markNotificationRead, type Notification } from '~/lib/api/notifications';
|
3 |
+
|
4 |
+
const READ_NOTIFICATIONS_KEY = 'bolt_read_notifications';
|
5 |
+
|
6 |
+
const getReadNotifications = (): string[] => {
|
7 |
+
try {
|
8 |
+
const stored = localStorage.getItem(READ_NOTIFICATIONS_KEY);
|
9 |
+
return stored ? JSON.parse(stored) : [];
|
10 |
+
} catch {
|
11 |
+
return [];
|
12 |
+
}
|
13 |
+
};
|
14 |
+
|
15 |
+
const setReadNotifications = (notificationIds: string[]) => {
|
16 |
+
try {
|
17 |
+
localStorage.setItem(READ_NOTIFICATIONS_KEY, JSON.stringify(notificationIds));
|
18 |
+
} catch (error) {
|
19 |
+
console.error('Failed to persist read notifications:', error);
|
20 |
+
}
|
21 |
+
};
|
22 |
+
|
23 |
+
export const useNotifications = () => {
|
24 |
+
const [hasUnreadNotifications, setHasUnreadNotifications] = useState(false);
|
25 |
+
const [unreadNotifications, setUnreadNotifications] = useState<Notification[]>([]);
|
26 |
+
const [readNotificationIds, setReadNotificationIds] = useState<string[]>(() => getReadNotifications());
|
27 |
+
|
28 |
+
const checkNotifications = async () => {
|
29 |
+
try {
|
30 |
+
const notifications = await getNotifications();
|
31 |
+
const unread = notifications.filter((n) => !readNotificationIds.includes(n.id));
|
32 |
+
setUnreadNotifications(unread);
|
33 |
+
setHasUnreadNotifications(unread.length > 0);
|
34 |
+
} catch (error) {
|
35 |
+
console.error('Failed to check notifications:', error);
|
36 |
+
}
|
37 |
+
};
|
38 |
+
|
39 |
+
useEffect(() => {
|
40 |
+
// Check immediately and then every minute
|
41 |
+
checkNotifications();
|
42 |
+
|
43 |
+
const interval = setInterval(checkNotifications, 60 * 1000);
|
44 |
+
|
45 |
+
return () => clearInterval(interval);
|
46 |
+
}, [readNotificationIds]);
|
47 |
+
|
48 |
+
const markAsRead = async (notificationId: string) => {
|
49 |
+
try {
|
50 |
+
await markNotificationRead(notificationId);
|
51 |
+
|
52 |
+
const newReadIds = [...readNotificationIds, notificationId];
|
53 |
+
setReadNotificationIds(newReadIds);
|
54 |
+
setReadNotifications(newReadIds);
|
55 |
+
setUnreadNotifications((prev) => prev.filter((n) => n.id !== notificationId));
|
56 |
+
setHasUnreadNotifications(unreadNotifications.length > 1);
|
57 |
+
} catch (error) {
|
58 |
+
console.error('Failed to mark notification as read:', error);
|
59 |
+
}
|
60 |
+
};
|
61 |
+
|
62 |
+
const markAllAsRead = async () => {
|
63 |
+
try {
|
64 |
+
await Promise.all(unreadNotifications.map((n) => markNotificationRead(n.id)));
|
65 |
+
|
66 |
+
const newReadIds = [...readNotificationIds, ...unreadNotifications.map((n) => n.id)];
|
67 |
+
setReadNotificationIds(newReadIds);
|
68 |
+
setReadNotifications(newReadIds);
|
69 |
+
setUnreadNotifications([]);
|
70 |
+
setHasUnreadNotifications(false);
|
71 |
+
} catch (error) {
|
72 |
+
console.error('Failed to mark all notifications as read:', error);
|
73 |
+
}
|
74 |
+
};
|
75 |
+
|
76 |
+
return { hasUnreadNotifications, unreadNotifications, markAsRead, markAllAsRead };
|
77 |
+
};
|
app/lib/hooks/{useSettings.tsx → useSettings.ts}
RENAMED
@@ -9,24 +9,63 @@ import {
|
|
9 |
latestBranchStore,
|
10 |
autoSelectStarterTemplate,
|
11 |
enableContextOptimizationStore,
|
|
|
|
|
|
|
12 |
} from '~/lib/stores/settings';
|
13 |
import { useCallback, useEffect, useState } from 'react';
|
14 |
import Cookies from 'js-cookie';
|
15 |
-
import type { IProviderSetting, ProviderInfo } from '~/types/model';
|
16 |
-
import {
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
21 |
}
|
22 |
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
27 |
|
28 |
-
export function useSettings() {
|
29 |
-
const providers = useStore(providersStore)
|
30 |
const debug = useStore(isDebugMode);
|
31 |
const eventLogs = useStore(isEventLogsEnabled);
|
32 |
const promptId = useStore(promptStore);
|
@@ -35,106 +74,18 @@ export function useSettings() {
|
|
35 |
const autoSelectTemplate = useStore(autoSelectStarterTemplate);
|
36 |
const [activeProviders, setActiveProviders] = useState<ProviderInfo[]>([]);
|
37 |
const contextOptimizationEnabled = useStore(enableContextOptimizationStore);
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
}
|
49 |
-
|
50 |
-
return false;
|
51 |
-
}
|
52 |
-
};
|
53 |
-
|
54 |
-
// reading values from cookies on mount
|
55 |
-
useEffect(() => {
|
56 |
-
const savedProviders = Cookies.get('providers');
|
57 |
-
|
58 |
-
if (savedProviders) {
|
59 |
-
try {
|
60 |
-
const parsedProviders: Record<string, IProviderSetting> = JSON.parse(savedProviders);
|
61 |
-
Object.keys(providers).forEach((provider) => {
|
62 |
-
const currentProviderSettings = parsedProviders[provider];
|
63 |
-
|
64 |
-
if (currentProviderSettings) {
|
65 |
-
providersStore.setKey(provider, {
|
66 |
-
...providers[provider],
|
67 |
-
settings: {
|
68 |
-
...currentProviderSettings,
|
69 |
-
enabled: currentProviderSettings.enabled ?? true,
|
70 |
-
},
|
71 |
-
});
|
72 |
-
}
|
73 |
-
});
|
74 |
-
} catch (error) {
|
75 |
-
console.error('Failed to parse providers from cookies:', error);
|
76 |
-
}
|
77 |
-
}
|
78 |
-
|
79 |
-
// load debug mode from cookies
|
80 |
-
const savedDebugMode = Cookies.get('isDebugEnabled');
|
81 |
-
|
82 |
-
if (savedDebugMode) {
|
83 |
-
isDebugMode.set(savedDebugMode === 'true');
|
84 |
-
}
|
85 |
-
|
86 |
-
// load event logs from cookies
|
87 |
-
const savedEventLogs = Cookies.get('isEventLogsEnabled');
|
88 |
-
|
89 |
-
if (savedEventLogs) {
|
90 |
-
isEventLogsEnabled.set(savedEventLogs === 'true');
|
91 |
-
}
|
92 |
-
|
93 |
-
// load local models from cookies
|
94 |
-
const savedLocalModels = Cookies.get('isLocalModelsEnabled');
|
95 |
-
|
96 |
-
if (savedLocalModels) {
|
97 |
-
isLocalModelsEnabled.set(savedLocalModels === 'true');
|
98 |
-
}
|
99 |
-
|
100 |
-
const promptId = Cookies.get('promptId');
|
101 |
-
|
102 |
-
if (promptId) {
|
103 |
-
promptStore.set(promptId);
|
104 |
-
}
|
105 |
-
|
106 |
-
// load latest branch setting from cookies or determine based on version
|
107 |
-
const savedLatestBranch = Cookies.get('isLatestBranch');
|
108 |
-
let checkCommit = Cookies.get('commitHash');
|
109 |
-
|
110 |
-
if (checkCommit === undefined) {
|
111 |
-
checkCommit = versionData.commit;
|
112 |
-
}
|
113 |
-
|
114 |
-
if (savedLatestBranch === undefined || checkCommit !== versionData.commit) {
|
115 |
-
// If setting hasn't been set by user, check version
|
116 |
-
checkIsStableVersion().then((isStable) => {
|
117 |
-
const shouldUseLatest = !isStable;
|
118 |
-
latestBranchStore.set(shouldUseLatest);
|
119 |
-
Cookies.set('isLatestBranch', String(shouldUseLatest));
|
120 |
-
Cookies.set('commitHash', String(versionData.commit));
|
121 |
-
});
|
122 |
-
} else {
|
123 |
-
latestBranchStore.set(savedLatestBranch === 'true');
|
124 |
-
}
|
125 |
-
|
126 |
-
const autoSelectTemplate = Cookies.get('autoSelectTemplate');
|
127 |
-
|
128 |
-
if (autoSelectTemplate) {
|
129 |
-
autoSelectStarterTemplate.set(autoSelectTemplate === 'true');
|
130 |
-
}
|
131 |
-
|
132 |
-
const savedContextOptimizationEnabled = Cookies.get('contextOptimizationEnabled');
|
133 |
-
|
134 |
-
if (savedContextOptimizationEnabled) {
|
135 |
-
enableContextOptimizationStore.set(savedContextOptimizationEnabled === 'true');
|
136 |
-
}
|
137 |
-
}, []);
|
138 |
|
139 |
// writing values to cookies on change
|
140 |
useEffect(() => {
|
@@ -158,14 +109,18 @@ export function useSettings() {
|
|
158 |
setActiveProviders(active);
|
159 |
}, [providers, isLocalModel]);
|
160 |
|
161 |
-
|
162 |
-
|
163 |
-
|
164 |
-
|
165 |
-
|
166 |
-
|
167 |
-
|
168 |
-
);
|
|
|
|
|
|
|
|
|
169 |
|
170 |
const enableDebugMode = useCallback((enabled: boolean) => {
|
171 |
isDebugMode.set(enabled);
|
@@ -173,7 +128,7 @@ export function useSettings() {
|
|
173 |
Cookies.set('isDebugEnabled', String(enabled));
|
174 |
}, []);
|
175 |
|
176 |
-
const
|
177 |
isEventLogsEnabled.set(enabled);
|
178 |
logStore.logSystem(`Event logs ${enabled ? 'enabled' : 'disabled'}`);
|
179 |
Cookies.set('isEventLogsEnabled', String(enabled));
|
@@ -189,6 +144,7 @@ export function useSettings() {
|
|
189 |
promptStore.set(promptId);
|
190 |
Cookies.set('promptId', promptId);
|
191 |
}, []);
|
|
|
192 |
const enableLatestBranch = useCallback((enabled: boolean) => {
|
193 |
latestBranchStore.set(enabled);
|
194 |
logStore.logSystem(`Main branch updates ${enabled ? 'enabled' : 'disabled'}`);
|
@@ -207,16 +163,45 @@ export function useSettings() {
|
|
207 |
Cookies.set('contextOptimizationEnabled', String(enabled));
|
208 |
}, []);
|
209 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
210 |
return {
|
|
|
211 |
providers,
|
212 |
activeProviders,
|
213 |
updateProviderSettings,
|
|
|
|
|
214 |
debug,
|
215 |
enableDebugMode,
|
216 |
eventLogs,
|
217 |
-
|
218 |
-
isLocalModel,
|
219 |
-
enableLocalModels,
|
220 |
promptId,
|
221 |
setPromptId,
|
222 |
isLatestBranch,
|
@@ -225,5 +210,13 @@ export function useSettings() {
|
|
225 |
setAutoSelectTemplate,
|
226 |
contextOptimizationEnabled,
|
227 |
enableContextOptimization,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
228 |
};
|
229 |
}
|
|
|
9 |
latestBranchStore,
|
10 |
autoSelectStarterTemplate,
|
11 |
enableContextOptimizationStore,
|
12 |
+
tabConfigurationStore,
|
13 |
+
updateTabConfiguration as updateTabConfig,
|
14 |
+
resetTabConfiguration as resetTabConfig,
|
15 |
} from '~/lib/stores/settings';
|
16 |
import { useCallback, useEffect, useState } from 'react';
|
17 |
import Cookies from 'js-cookie';
|
18 |
+
import type { IProviderSetting, ProviderInfo, IProviderConfig } from '~/types/model';
|
19 |
+
import type { TabWindowConfig, TabVisibilityConfig } from '~/components/settings/settings.types';
|
20 |
+
import { logStore } from '~/lib/stores/logs';
|
21 |
+
import { getLocalStorage, setLocalStorage } from '~/utils/localStorage';
|
22 |
+
|
23 |
+
export interface Settings {
|
24 |
+
theme: 'light' | 'dark' | 'system';
|
25 |
+
language: string;
|
26 |
+
notifications: boolean;
|
27 |
+
eventLogs: boolean;
|
28 |
+
timezone: string;
|
29 |
+
tabConfiguration: TabWindowConfig;
|
30 |
}
|
31 |
|
32 |
+
export interface UseSettingsReturn {
|
33 |
+
// Theme and UI settings
|
34 |
+
setTheme: (theme: Settings['theme']) => void;
|
35 |
+
setLanguage: (language: string) => void;
|
36 |
+
setNotifications: (enabled: boolean) => void;
|
37 |
+
setEventLogs: (enabled: boolean) => void;
|
38 |
+
setTimezone: (timezone: string) => void;
|
39 |
+
settings: Settings;
|
40 |
+
|
41 |
+
// Provider settings
|
42 |
+
providers: Record<string, IProviderConfig>;
|
43 |
+
activeProviders: ProviderInfo[];
|
44 |
+
updateProviderSettings: (provider: string, config: IProviderSetting) => void;
|
45 |
+
isLocalModel: boolean;
|
46 |
+
enableLocalModels: (enabled: boolean) => void;
|
47 |
+
|
48 |
+
// Debug and development settings
|
49 |
+
debug: boolean;
|
50 |
+
enableDebugMode: (enabled: boolean) => void;
|
51 |
+
eventLogs: boolean;
|
52 |
+
promptId: string;
|
53 |
+
setPromptId: (promptId: string) => void;
|
54 |
+
isLatestBranch: boolean;
|
55 |
+
enableLatestBranch: (enabled: boolean) => void;
|
56 |
+
autoSelectTemplate: boolean;
|
57 |
+
setAutoSelectTemplate: (enabled: boolean) => void;
|
58 |
+
contextOptimizationEnabled: boolean;
|
59 |
+
enableContextOptimization: (enabled: boolean) => void;
|
60 |
+
|
61 |
+
// Tab configuration
|
62 |
+
tabConfiguration: TabWindowConfig;
|
63 |
+
updateTabConfiguration: (config: TabVisibilityConfig) => void;
|
64 |
+
resetTabConfiguration: () => void;
|
65 |
+
}
|
66 |
|
67 |
+
export function useSettings(): UseSettingsReturn {
|
68 |
+
const providers = useStore(providersStore) as Record<string, IProviderConfig>;
|
69 |
const debug = useStore(isDebugMode);
|
70 |
const eventLogs = useStore(isEventLogsEnabled);
|
71 |
const promptId = useStore(promptStore);
|
|
|
74 |
const autoSelectTemplate = useStore(autoSelectStarterTemplate);
|
75 |
const [activeProviders, setActiveProviders] = useState<ProviderInfo[]>([]);
|
76 |
const contextOptimizationEnabled = useStore(enableContextOptimizationStore);
|
77 |
+
const tabConfiguration = useStore(tabConfigurationStore);
|
78 |
+
const [settings, setSettings] = useState<Settings>(() => {
|
79 |
+
const storedSettings = getLocalStorage('settings');
|
80 |
+
return {
|
81 |
+
theme: storedSettings?.theme || 'system',
|
82 |
+
language: storedSettings?.language || 'en',
|
83 |
+
notifications: storedSettings?.notifications ?? true,
|
84 |
+
eventLogs: storedSettings?.eventLogs ?? true,
|
85 |
+
timezone: storedSettings?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone,
|
86 |
+
tabConfiguration,
|
87 |
+
};
|
88 |
+
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
89 |
|
90 |
// writing values to cookies on change
|
91 |
useEffect(() => {
|
|
|
109 |
setActiveProviders(active);
|
110 |
}, [providers, isLocalModel]);
|
111 |
|
112 |
+
const saveSettings = useCallback((newSettings: Partial<Settings>) => {
|
113 |
+
setSettings((prev) => {
|
114 |
+
const updated = { ...prev, ...newSettings };
|
115 |
+
setLocalStorage('settings', updated);
|
116 |
+
|
117 |
+
return updated;
|
118 |
+
});
|
119 |
+
}, []);
|
120 |
+
|
121 |
+
const updateProviderSettings = useCallback((provider: string, config: IProviderSetting) => {
|
122 |
+
providersStore.setKey(provider, { settings: config } as IProviderConfig);
|
123 |
+
}, []);
|
124 |
|
125 |
const enableDebugMode = useCallback((enabled: boolean) => {
|
126 |
isDebugMode.set(enabled);
|
|
|
128 |
Cookies.set('isDebugEnabled', String(enabled));
|
129 |
}, []);
|
130 |
|
131 |
+
const setEventLogs = useCallback((enabled: boolean) => {
|
132 |
isEventLogsEnabled.set(enabled);
|
133 |
logStore.logSystem(`Event logs ${enabled ? 'enabled' : 'disabled'}`);
|
134 |
Cookies.set('isEventLogsEnabled', String(enabled));
|
|
|
144 |
promptStore.set(promptId);
|
145 |
Cookies.set('promptId', promptId);
|
146 |
}, []);
|
147 |
+
|
148 |
const enableLatestBranch = useCallback((enabled: boolean) => {
|
149 |
latestBranchStore.set(enabled);
|
150 |
logStore.logSystem(`Main branch updates ${enabled ? 'enabled' : 'disabled'}`);
|
|
|
163 |
Cookies.set('contextOptimizationEnabled', String(enabled));
|
164 |
}, []);
|
165 |
|
166 |
+
const setTheme = useCallback(
|
167 |
+
(theme: Settings['theme']) => {
|
168 |
+
saveSettings({ theme });
|
169 |
+
},
|
170 |
+
[saveSettings],
|
171 |
+
);
|
172 |
+
|
173 |
+
const setLanguage = useCallback(
|
174 |
+
(language: string) => {
|
175 |
+
saveSettings({ language });
|
176 |
+
},
|
177 |
+
[saveSettings],
|
178 |
+
);
|
179 |
+
|
180 |
+
const setNotifications = useCallback(
|
181 |
+
(enabled: boolean) => {
|
182 |
+
saveSettings({ notifications: enabled });
|
183 |
+
},
|
184 |
+
[saveSettings],
|
185 |
+
);
|
186 |
+
|
187 |
+
const setTimezone = useCallback(
|
188 |
+
(timezone: string) => {
|
189 |
+
saveSettings({ timezone });
|
190 |
+
},
|
191 |
+
[saveSettings],
|
192 |
+
);
|
193 |
+
|
194 |
return {
|
195 |
+
...settings,
|
196 |
providers,
|
197 |
activeProviders,
|
198 |
updateProviderSettings,
|
199 |
+
isLocalModel,
|
200 |
+
enableLocalModels,
|
201 |
debug,
|
202 |
enableDebugMode,
|
203 |
eventLogs,
|
204 |
+
setEventLogs,
|
|
|
|
|
205 |
promptId,
|
206 |
setPromptId,
|
207 |
isLatestBranch,
|
|
|
210 |
setAutoSelectTemplate,
|
211 |
contextOptimizationEnabled,
|
212 |
enableContextOptimization,
|
213 |
+
setTheme,
|
214 |
+
setLanguage,
|
215 |
+
setNotifications,
|
216 |
+
setTimezone,
|
217 |
+
settings,
|
218 |
+
tabConfiguration,
|
219 |
+
updateTabConfiguration: updateTabConfig,
|
220 |
+
resetTabConfiguration: resetTabConfig,
|
221 |
};
|
222 |
}
|
app/lib/hooks/useUpdateCheck.ts
ADDED
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useState, useEffect } from 'react';
|
2 |
+
import { checkForUpdates, acknowledgeUpdate } from '~/lib/api/updates';
|
3 |
+
|
4 |
+
const LAST_ACKNOWLEDGED_VERSION_KEY = 'bolt_last_acknowledged_version';
|
5 |
+
|
6 |
+
export const useUpdateCheck = () => {
|
7 |
+
const [hasUpdate, setHasUpdate] = useState(false);
|
8 |
+
const [currentVersion, setCurrentVersion] = useState<string>('');
|
9 |
+
const [lastAcknowledgedVersion, setLastAcknowledgedVersion] = useState<string | null>(() => {
|
10 |
+
try {
|
11 |
+
return localStorage.getItem(LAST_ACKNOWLEDGED_VERSION_KEY);
|
12 |
+
} catch {
|
13 |
+
return null;
|
14 |
+
}
|
15 |
+
});
|
16 |
+
|
17 |
+
useEffect(() => {
|
18 |
+
const checkUpdate = async () => {
|
19 |
+
try {
|
20 |
+
const { available, version } = await checkForUpdates();
|
21 |
+
setCurrentVersion(version);
|
22 |
+
|
23 |
+
// Only show update if it's a new version and hasn't been acknowledged
|
24 |
+
setHasUpdate(available && version !== lastAcknowledgedVersion);
|
25 |
+
} catch (error) {
|
26 |
+
console.error('Failed to check for updates:', error);
|
27 |
+
}
|
28 |
+
};
|
29 |
+
|
30 |
+
// Check immediately and then every 30 minutes
|
31 |
+
checkUpdate();
|
32 |
+
|
33 |
+
const interval = setInterval(checkUpdate, 30 * 60 * 1000);
|
34 |
+
|
35 |
+
return () => clearInterval(interval);
|
36 |
+
}, [lastAcknowledgedVersion]);
|
37 |
+
|
38 |
+
const handleAcknowledgeUpdate = async () => {
|
39 |
+
try {
|
40 |
+
const { version } = await checkForUpdates();
|
41 |
+
await acknowledgeUpdate(version);
|
42 |
+
|
43 |
+
// Store in localStorage
|
44 |
+
try {
|
45 |
+
localStorage.setItem(LAST_ACKNOWLEDGED_VERSION_KEY, version);
|
46 |
+
} catch (error) {
|
47 |
+
console.error('Failed to persist acknowledged version:', error);
|
48 |
+
}
|
49 |
+
|
50 |
+
setLastAcknowledgedVersion(version);
|
51 |
+
setHasUpdate(false);
|
52 |
+
} catch (error) {
|
53 |
+
console.error('Failed to acknowledge update:', error);
|
54 |
+
}
|
55 |
+
};
|
56 |
+
|
57 |
+
return { hasUpdate, currentVersion, acknowledgeUpdate: handleAcknowledgeUpdate };
|
58 |
+
};
|
app/lib/modules/llm/providers/github.ts
CHANGED
@@ -11,7 +11,8 @@ export default class GithubProvider extends BaseProvider {
|
|
11 |
config = {
|
12 |
apiTokenKey: 'GITHUB_API_KEY',
|
13 |
};
|
14 |
-
|
|
|
15 |
staticModels: ModelInfo[] = [
|
16 |
{ name: 'gpt-4o', label: 'GPT-4o', provider: 'Github', maxTokenAllowed: 8000 },
|
17 |
{ name: 'o1', label: 'o1-preview', provider: 'Github', maxTokenAllowed: 100000 },
|
|
|
11 |
config = {
|
12 |
apiTokenKey: 'GITHUB_API_KEY',
|
13 |
};
|
14 |
+
|
15 |
+
// find more in https://github.com/marketplace?type=models
|
16 |
staticModels: ModelInfo[] = [
|
17 |
{ name: 'gpt-4o', label: 'GPT-4o', provider: 'Github', maxTokenAllowed: 8000 },
|
18 |
{ name: 'o1', label: 'o1-preview', provider: 'Github', maxTokenAllowed: 100000 },
|
app/lib/stores/logs.ts
CHANGED
@@ -10,7 +10,10 @@ export interface LogEntry {
|
|
10 |
level: 'info' | 'warning' | 'error' | 'debug';
|
11 |
message: string;
|
12 |
details?: Record<string, any>;
|
13 |
-
category: 'system' | 'provider' | 'user' | 'error';
|
|
|
|
|
|
|
14 |
}
|
15 |
|
16 |
const MAX_LOGS = 1000; // Maximum number of logs to keep in memory
|
@@ -101,18 +104,76 @@ class LogStore {
|
|
101 |
return this.addLog(message, 'info', 'user', details);
|
102 |
}
|
103 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
104 |
// Error events
|
105 |
logError(message: string, error?: Error | unknown, details?: Record<string, any>) {
|
106 |
-
const errorDetails =
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
return this.addLog(message, 'error', 'error', errorDetails);
|
117 |
}
|
118 |
|
|
|
10 |
level: 'info' | 'warning' | 'error' | 'debug';
|
11 |
message: string;
|
12 |
details?: Record<string, any>;
|
13 |
+
category: 'system' | 'provider' | 'user' | 'error' | 'api' | 'auth' | 'database' | 'network';
|
14 |
+
subCategory?: string;
|
15 |
+
duration?: number;
|
16 |
+
statusCode?: number;
|
17 |
}
|
18 |
|
19 |
const MAX_LOGS = 1000; // Maximum number of logs to keep in memory
|
|
|
104 |
return this.addLog(message, 'info', 'user', details);
|
105 |
}
|
106 |
|
107 |
+
// API Connection Logging
|
108 |
+
logAPIRequest(endpoint: string, method: string, duration: number, statusCode: number, details?: Record<string, any>) {
|
109 |
+
const message = `${method} ${endpoint} - ${statusCode} (${duration}ms)`;
|
110 |
+
const level = statusCode >= 400 ? 'error' : statusCode >= 300 ? 'warning' : 'info';
|
111 |
+
|
112 |
+
return this.addLog(message, level, 'api', {
|
113 |
+
...details,
|
114 |
+
endpoint,
|
115 |
+
method,
|
116 |
+
duration,
|
117 |
+
statusCode,
|
118 |
+
timestamp: new Date().toISOString(),
|
119 |
+
});
|
120 |
+
}
|
121 |
+
|
122 |
+
// Authentication Logging
|
123 |
+
logAuth(
|
124 |
+
action: 'login' | 'logout' | 'token_refresh' | 'key_validation',
|
125 |
+
success: boolean,
|
126 |
+
details?: Record<string, any>,
|
127 |
+
) {
|
128 |
+
const message = `Auth ${action} - ${success ? 'Success' : 'Failed'}`;
|
129 |
+
const level = success ? 'info' : 'error';
|
130 |
+
|
131 |
+
return this.addLog(message, level, 'auth', {
|
132 |
+
...details,
|
133 |
+
action,
|
134 |
+
success,
|
135 |
+
timestamp: new Date().toISOString(),
|
136 |
+
});
|
137 |
+
}
|
138 |
+
|
139 |
+
// Network Status Logging
|
140 |
+
logNetworkStatus(status: 'online' | 'offline' | 'reconnecting' | 'connected', details?: Record<string, any>) {
|
141 |
+
const message = `Network ${status}`;
|
142 |
+
const level = status === 'offline' ? 'error' : status === 'reconnecting' ? 'warning' : 'info';
|
143 |
+
|
144 |
+
return this.addLog(message, level, 'network', {
|
145 |
+
...details,
|
146 |
+
status,
|
147 |
+
timestamp: new Date().toISOString(),
|
148 |
+
});
|
149 |
+
}
|
150 |
+
|
151 |
+
// Database Operations Logging
|
152 |
+
logDatabase(operation: string, success: boolean, duration: number, details?: Record<string, any>) {
|
153 |
+
const message = `DB ${operation} - ${success ? 'Success' : 'Failed'} (${duration}ms)`;
|
154 |
+
const level = success ? 'info' : 'error';
|
155 |
+
|
156 |
+
return this.addLog(message, level, 'database', {
|
157 |
+
...details,
|
158 |
+
operation,
|
159 |
+
success,
|
160 |
+
duration,
|
161 |
+
timestamp: new Date().toISOString(),
|
162 |
+
});
|
163 |
+
}
|
164 |
+
|
165 |
// Error events
|
166 |
logError(message: string, error?: Error | unknown, details?: Record<string, any>) {
|
167 |
+
const errorDetails =
|
168 |
+
error instanceof Error
|
169 |
+
? {
|
170 |
+
name: error.name,
|
171 |
+
message: error.message,
|
172 |
+
stack: error.stack,
|
173 |
+
...details,
|
174 |
+
}
|
175 |
+
: { error, ...details };
|
176 |
+
|
177 |
return this.addLog(message, 'error', 'error', errorDetails);
|
178 |
}
|
179 |
|
app/lib/stores/settings.ts
CHANGED
@@ -2,6 +2,9 @@ import { atom, map } from 'nanostores';
|
|
2 |
import { workbenchStore } from './workbench';
|
3 |
import { PROVIDER_LIST } from '~/utils/constants';
|
4 |
import type { IProviderConfig } from '~/types/model';
|
|
|
|
|
|
|
5 |
|
6 |
export interface Shortcut {
|
7 |
key: string;
|
@@ -46,7 +49,9 @@ export const providersStore = map<ProviderSetting>(initialProviderSettings);
|
|
46 |
|
47 |
export const isDebugMode = atom(false);
|
48 |
|
49 |
-
|
|
|
|
|
50 |
|
51 |
export const isLocalModelsEnabled = atom(true);
|
52 |
|
@@ -56,3 +61,48 @@ export const latestBranchStore = atom(false);
|
|
56 |
|
57 |
export const autoSelectStarterTemplate = atom(false);
|
58 |
export const enableContextOptimizationStore = atom(false);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2 |
import { workbenchStore } from './workbench';
|
3 |
import { PROVIDER_LIST } from '~/utils/constants';
|
4 |
import type { IProviderConfig } from '~/types/model';
|
5 |
+
import type { TabVisibilityConfig, TabWindowConfig } from '~/components/settings/settings.types';
|
6 |
+
import { DEFAULT_TAB_CONFIG } from '~/components/settings/settings.types';
|
7 |
+
import Cookies from 'js-cookie';
|
8 |
|
9 |
export interface Shortcut {
|
10 |
key: string;
|
|
|
49 |
|
50 |
export const isDebugMode = atom(false);
|
51 |
|
52 |
+
// Initialize event logs from cookie or default to false
|
53 |
+
const savedEventLogs = Cookies.get('isEventLogsEnabled');
|
54 |
+
export const isEventLogsEnabled = atom(savedEventLogs === 'true');
|
55 |
|
56 |
export const isLocalModelsEnabled = atom(true);
|
57 |
|
|
|
61 |
|
62 |
export const autoSelectStarterTemplate = atom(false);
|
63 |
export const enableContextOptimizationStore = atom(false);
|
64 |
+
|
65 |
+
// Initialize tab configuration from cookie or default
|
66 |
+
const savedTabConfig = Cookies.get('tabConfiguration');
|
67 |
+
const initialTabConfig: TabWindowConfig = savedTabConfig
|
68 |
+
? JSON.parse(savedTabConfig)
|
69 |
+
: {
|
70 |
+
userTabs: DEFAULT_TAB_CONFIG.filter((tab) => tab.window === 'user'),
|
71 |
+
developerTabs: DEFAULT_TAB_CONFIG.filter((tab) => tab.window === 'developer'),
|
72 |
+
};
|
73 |
+
|
74 |
+
export const tabConfigurationStore = map<TabWindowConfig>(initialTabConfig);
|
75 |
+
|
76 |
+
// Helper function to update tab configuration
|
77 |
+
export const updateTabConfiguration = (config: TabVisibilityConfig) => {
|
78 |
+
const currentConfig = tabConfigurationStore.get();
|
79 |
+
const isUserTab = config.window === 'user';
|
80 |
+
const targetArray = isUserTab ? 'userTabs' : 'developerTabs';
|
81 |
+
|
82 |
+
// Only update the tab in its respective window
|
83 |
+
const updatedTabs = currentConfig[targetArray].map((tab) => (tab.id === config.id ? { ...config } : tab));
|
84 |
+
|
85 |
+
// If tab doesn't exist in this window yet, add it
|
86 |
+
if (!updatedTabs.find((tab) => tab.id === config.id)) {
|
87 |
+
updatedTabs.push(config);
|
88 |
+
}
|
89 |
+
|
90 |
+
// Create new config, only updating the target window's tabs
|
91 |
+
const newConfig: TabWindowConfig = {
|
92 |
+
...currentConfig,
|
93 |
+
[targetArray]: updatedTabs,
|
94 |
+
};
|
95 |
+
|
96 |
+
tabConfigurationStore.set(newConfig);
|
97 |
+
Cookies.set('tabConfiguration', JSON.stringify(newConfig));
|
98 |
+
};
|
99 |
+
|
100 |
+
// Helper function to reset tab configuration
|
101 |
+
export const resetTabConfiguration = () => {
|
102 |
+
const defaultConfig: TabWindowConfig = {
|
103 |
+
userTabs: DEFAULT_TAB_CONFIG.filter((tab) => tab.window === 'user'),
|
104 |
+
developerTabs: DEFAULT_TAB_CONFIG.filter((tab) => tab.window === 'developer'),
|
105 |
+
};
|
106 |
+
tabConfigurationStore.set(defaultConfig);
|
107 |
+
Cookies.set('tabConfiguration', JSON.stringify(defaultConfig));
|
108 |
+
};
|
app/root.tsx
CHANGED
@@ -6,6 +6,8 @@ import { themeStore } from './lib/stores/theme';
|
|
6 |
import { stripIndents } from './utils/stripIndent';
|
7 |
import { createHead } from 'remix-island';
|
8 |
import { useEffect } from 'react';
|
|
|
|
|
9 |
|
10 |
import reactToastifyStyles from 'react-toastify/dist/ReactToastify.css?url';
|
11 |
import globalStyles from './styles/index.scss?url';
|
@@ -70,11 +72,11 @@ export function Layout({ children }: { children: React.ReactNode }) {
|
|
70 |
}, [theme]);
|
71 |
|
72 |
return (
|
73 |
-
|
74 |
{children}
|
75 |
<ScrollRestoration />
|
76 |
<Scripts />
|
77 |
-
|
78 |
);
|
79 |
}
|
80 |
|
|
|
6 |
import { stripIndents } from './utils/stripIndent';
|
7 |
import { createHead } from 'remix-island';
|
8 |
import { useEffect } from 'react';
|
9 |
+
import { DndProvider } from 'react-dnd';
|
10 |
+
import { HTML5Backend } from 'react-dnd-html5-backend';
|
11 |
|
12 |
import reactToastifyStyles from 'react-toastify/dist/ReactToastify.css?url';
|
13 |
import globalStyles from './styles/index.scss?url';
|
|
|
72 |
}, [theme]);
|
73 |
|
74 |
return (
|
75 |
+
<DndProvider backend={HTML5Backend}>
|
76 |
{children}
|
77 |
<ScrollRestoration />
|
78 |
<Scripts />
|
79 |
+
</DndProvider>
|
80 |
);
|
81 |
}
|
82 |
|
app/utils/localStorage.ts
ADDED
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export function getLocalStorage(key: string) {
|
2 |
+
try {
|
3 |
+
const item = localStorage.getItem(key);
|
4 |
+
return item ? JSON.parse(item) : null;
|
5 |
+
} catch (error) {
|
6 |
+
console.error(`Error reading from localStorage key "${key}":`, error);
|
7 |
+
return null;
|
8 |
+
}
|
9 |
+
}
|
10 |
+
|
11 |
+
export function setLocalStorage(key: string, value: any) {
|
12 |
+
try {
|
13 |
+
localStorage.setItem(key, JSON.stringify(value));
|
14 |
+
} catch (error) {
|
15 |
+
console.error(`Error writing to localStorage key "${key}":`, error);
|
16 |
+
}
|
17 |
+
}
|
package.json
CHANGED
@@ -92,6 +92,8 @@
|
|
92 |
"nanostores": "^0.10.3",
|
93 |
"ollama-ai-provider": "^0.15.2",
|
94 |
"react": "^18.3.1",
|
|
|
|
|
95 |
"react-dom": "^18.3.1",
|
96 |
"react-hotkeys-hook": "^4.6.1",
|
97 |
"react-icons": "^5.4.0",
|
|
|
92 |
"nanostores": "^0.10.3",
|
93 |
"ollama-ai-provider": "^0.15.2",
|
94 |
"react": "^18.3.1",
|
95 |
+
"react-dnd": "^16.0.1",
|
96 |
+
"react-dnd-html5-backend": "^16.0.1",
|
97 |
"react-dom": "^18.3.1",
|
98 |
"react-hotkeys-hook": "^4.6.1",
|
99 |
"react-icons": "^5.4.0",
|
pnpm-lock.yaml
CHANGED
@@ -197,6 +197,12 @@ importers:
|
|
197 |
react:
|
198 |
specifier: ^18.3.1
|
199 |
version: 18.3.1
|
|
|
|
|
|
|
|
|
|
|
|
|
200 |
react-dom:
|
201 |
specifier: ^18.3.1
|
202 |
version: 18.3.1([email protected])
|
@@ -2049,6 +2055,15 @@ packages:
|
|
2049 |
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
|
2050 |
react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
|
2051 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2052 |
'@react-stately/[email protected]':
|
2053 |
resolution: {integrity: sha512-iMQSGcpaecghDIh3mZEpZfoFH3ExBwTtuBEcvZ2XnGzCgQjeYXcMdIUwAfVQLXFTdHUHGF6Gu6/dFrYsCzySBQ==}
|
2054 |
peerDependencies:
|
@@ -3309,6 +3324,9 @@ packages:
|
|
3309 | |
3310 |
resolution: {integrity: sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==}
|
3311 |
|
|
|
|
|
|
|
3312 | |
3313 |
resolution: {integrity: sha512-IGBwjF7tNk3cwypFNH/7bfzBcgSCbaMOD3GsaY1AU/JRrnHnYgEM0+9kQt52iZxjNsjBtJYtao146V+f8jFZNw==}
|
3314 |
engines: {node: '>=10'}
|
@@ -3846,6 +3864,9 @@ packages:
|
|
3846 | |
3847 |
resolution: {integrity: sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==}
|
3848 |
|
|
|
|
|
|
|
3849 | |
3850 |
resolution: {integrity: sha512-HVJyzUrLIL1c0QmviVh5E8VGyUS7xCFPS6yydaVd1UegW+ibV/CohqTH9MkOLDp5o+rb82DMo77PTuc9F/8GKw==}
|
3851 |
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
@@ -5028,6 +5049,24 @@ packages:
|
|
5028 |
resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==}
|
5029 |
engines: {node: '>= 0.8'}
|
5030 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
5031 | |
5032 |
resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==}
|
5033 |
peerDependencies:
|
@@ -5044,6 +5083,9 @@ packages:
|
|
5044 |
peerDependencies:
|
5045 |
react: '*'
|
5046 |
|
|
|
|
|
|
|
5047 | |
5048 |
resolution: {integrity: sha512-186Gw/vF1uRkydbsOIkcGXw7aHq0sZOCRFFjGrr7b9+nVZg4UfA4enXCaxm4fUzecU38sWfrNDitGhshuU7rdg==}
|
5049 |
peerDependencies:
|
@@ -5128,6 +5170,9 @@ packages:
|
|
5128 |
resolution: {integrity: sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==}
|
5129 |
engines: {node: '>= 14.16.0'}
|
5130 |
|
|
|
|
|
|
|
5131 | |
5132 |
resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
|
5133 |
|
@@ -8116,6 +8161,12 @@ snapshots:
|
|
8116 |
react: 18.3.1
|
8117 |
react-dom: 18.3.1([email protected])
|
8118 |
|
|
|
|
|
|
|
|
|
|
|
|
|
8119 |
'@react-stately/[email protected]([email protected])':
|
8120 |
dependencies:
|
8121 |
'@swc/helpers': 0.5.15
|
@@ -9678,6 +9729,12 @@ snapshots:
|
|
9678 |
miller-rabin: 4.0.1
|
9679 |
randombytes: 2.1.0
|
9680 |
|
|
|
|
|
|
|
|
|
|
|
|
|
9681 | |
9682 |
|
9683 | |
@@ -10423,6 +10480,10 @@ snapshots:
|
|
10423 |
minimalistic-assert: 1.0.1
|
10424 |
minimalistic-crypto-utils: 1.0.1
|
10425 |
|
|
|
|
|
|
|
|
|
10426 | |
10427 |
dependencies:
|
10428 |
lru-cache: 7.18.3
|
@@ -11969,6 +12030,22 @@ snapshots:
|
|
11969 |
iconv-lite: 0.4.24
|
11970 |
unpipe: 1.0.0
|
11971 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
11972 | |
11973 |
dependencies:
|
11974 |
loose-envify: 1.4.0
|
@@ -11984,6 +12061,8 @@ snapshots:
|
|
11984 |
dependencies:
|
11985 |
react: 18.3.1
|
11986 |
|
|
|
|
|
11987 | |
11988 |
dependencies:
|
11989 |
'@types/hast': 3.0.4
|
@@ -12080,6 +12159,10 @@ snapshots:
|
|
12080 |
|
12081 | |
12082 |
|
|
|
|
|
|
|
|
|
12083 | |
12084 |
|
12085 |
|
|
197 |
react:
|
198 |
specifier: ^18.3.1
|
199 |
version: 18.3.1
|
200 |
+
react-dnd:
|
201 |
+
specifier: ^16.0.1
|
202 |
+
version: 16.0.1(@types/[email protected])(@types/[email protected])([email protected])
|
203 |
+
react-dnd-html5-backend:
|
204 |
+
specifier: ^16.0.1
|
205 |
+
version: 16.0.1
|
206 |
react-dom:
|
207 |
specifier: ^18.3.1
|
208 |
version: 18.3.1([email protected])
|
|
|
2055 |
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
|
2056 |
react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
|
2057 |
|
2058 |
+
'@react-dnd/[email protected]':
|
2059 |
+
resolution: {integrity: sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A==}
|
2060 |
+
|
2061 |
+
'@react-dnd/[email protected]':
|
2062 |
+
resolution: {integrity: sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw==}
|
2063 |
+
|
2064 |
+
'@react-dnd/[email protected]':
|
2065 |
+
resolution: {integrity: sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==}
|
2066 |
+
|
2067 |
'@react-stately/[email protected]':
|
2068 |
resolution: {integrity: sha512-iMQSGcpaecghDIh3mZEpZfoFH3ExBwTtuBEcvZ2XnGzCgQjeYXcMdIUwAfVQLXFTdHUHGF6Gu6/dFrYsCzySBQ==}
|
2069 |
peerDependencies:
|
|
|
3324 | |
3325 |
resolution: {integrity: sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==}
|
3326 |
|
3327 | |
3328 |
+
resolution: {integrity: sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==}
|
3329 |
+
|
3330 | |
3331 |
resolution: {integrity: sha512-IGBwjF7tNk3cwypFNH/7bfzBcgSCbaMOD3GsaY1AU/JRrnHnYgEM0+9kQt52iZxjNsjBtJYtao146V+f8jFZNw==}
|
3332 |
engines: {node: '>=10'}
|
|
|
3864 | |
3865 |
resolution: {integrity: sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==}
|
3866 |
|
3867 | |
3868 |
+
resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
|
3869 |
+
|
3870 | |
3871 |
resolution: {integrity: sha512-HVJyzUrLIL1c0QmviVh5E8VGyUS7xCFPS6yydaVd1UegW+ibV/CohqTH9MkOLDp5o+rb82DMo77PTuc9F/8GKw==}
|
3872 |
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
|
|
5049 |
resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==}
|
5050 |
engines: {node: '>= 0.8'}
|
5051 |
|
5052 | |
5053 |
+
resolution: {integrity: sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==}
|
5054 |
+
|
5055 | |
5056 |
+
resolution: {integrity: sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==}
|
5057 |
+
peerDependencies:
|
5058 |
+
'@types/hoist-non-react-statics': '>= 3.3.1'
|
5059 |
+
'@types/node': '>= 12'
|
5060 |
+
'@types/react': '>= 16'
|
5061 |
+
react: '>= 16.14'
|
5062 |
+
peerDependenciesMeta:
|
5063 |
+
'@types/hoist-non-react-statics':
|
5064 |
+
optional: true
|
5065 |
+
'@types/node':
|
5066 |
+
optional: true
|
5067 |
+
'@types/react':
|
5068 |
+
optional: true
|
5069 |
+
|
5070 | |
5071 |
resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==}
|
5072 |
peerDependencies:
|
|
|
5083 |
peerDependencies:
|
5084 |
react: '*'
|
5085 |
|
5086 | |
5087 |
+
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
5088 |
+
|
5089 | |
5090 |
resolution: {integrity: sha512-186Gw/vF1uRkydbsOIkcGXw7aHq0sZOCRFFjGrr7b9+nVZg4UfA4enXCaxm4fUzecU38sWfrNDitGhshuU7rdg==}
|
5091 |
peerDependencies:
|
|
|
5170 |
resolution: {integrity: sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==}
|
5171 |
engines: {node: '>= 14.16.0'}
|
5172 |
|
5173 | |
5174 |
+
resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==}
|
5175 |
+
|
5176 | |
5177 |
resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
|
5178 |
|
|
|
8161 |
react: 18.3.1
|
8162 |
react-dom: 18.3.1([email protected])
|
8163 |
|
8164 |
+
'@react-dnd/[email protected]': {}
|
8165 |
+
|
8166 |
+
'@react-dnd/[email protected]': {}
|
8167 |
+
|
8168 |
+
'@react-dnd/[email protected]': {}
|
8169 |
+
|
8170 |
'@react-stately/[email protected]([email protected])':
|
8171 |
dependencies:
|
8172 |
'@swc/helpers': 0.5.15
|
|
|
9729 |
miller-rabin: 4.0.1
|
9730 |
randombytes: 2.1.0
|
9731 |
|
9732 | |
9733 |
+
dependencies:
|
9734 |
+
'@react-dnd/asap': 5.0.2
|
9735 |
+
'@react-dnd/invariant': 4.0.2
|
9736 |
+
redux: 4.2.1
|
9737 |
+
|
9738 | |
9739 |
|
9740 | |
|
|
10480 |
minimalistic-assert: 1.0.1
|
10481 |
minimalistic-crypto-utils: 1.0.1
|
10482 |
|
10483 | |
10484 |
+
dependencies:
|
10485 |
+
react-is: 16.13.1
|
10486 |
+
|
10487 | |
10488 |
dependencies:
|
10489 |
lru-cache: 7.18.3
|
|
|
12030 |
iconv-lite: 0.4.24
|
12031 |
unpipe: 1.0.0
|
12032 |
|
12033 | |
12034 |
+
dependencies:
|
12035 |
+
dnd-core: 16.0.1
|
12036 |
+
|
12037 |
+
[email protected](@types/[email protected])(@types/[email protected])([email protected]):
|
12038 |
+
dependencies:
|
12039 |
+
'@react-dnd/invariant': 4.0.2
|
12040 |
+
'@react-dnd/shallowequal': 4.0.2
|
12041 |
+
dnd-core: 16.0.1
|
12042 |
+
fast-deep-equal: 3.1.3
|
12043 |
+
hoist-non-react-statics: 3.3.2
|
12044 |
+
react: 18.3.1
|
12045 |
+
optionalDependencies:
|
12046 |
+
'@types/node': 22.10.1
|
12047 |
+
'@types/react': 18.3.12
|
12048 |
+
|
12049 | |
12050 |
dependencies:
|
12051 |
loose-envify: 1.4.0
|
|
|
12061 |
dependencies:
|
12062 |
react: 18.3.1
|
12063 |
|
12064 |
+
[email protected]: {}
|
12065 |
+
|
12066 | |
12067 |
dependencies:
|
12068 |
'@types/hast': 3.0.4
|
|
|
12159 |
|
12160 | |
12161 |
|
12162 | |
12163 |
+
dependencies:
|
12164 |
+
'@babel/runtime': 7.26.0
|
12165 |
+
|
12166 | |
12167 |
|
12168 |