Stijnus commited on
Commit
f33ba63
Β·
1 Parent(s): 41bb909

V1 : Release of the new Settings Dashboard

Browse files

# πŸš€ Release v1.0.0

## What's Changed 🌟

### 🎨 UI/UX Improvements
- **Dark Mode Support**
- Implemented comprehensive dark theme across all components
- Enhanced contrast and readability in dark mode
- Added smooth theme transitions
- Optimized dialog overlays and backdrops

### πŸ› οΈ Settings Panel
- **Data Management**
- Added chat history export/import functionality
- Implemented settings backup and restore
- Added secure data deletion with confirmations
- Added profile customization options

- **Provider Management**
- Added comprehensive provider configuration
- Implemented URL-configurable providers
- Added local model support (Ollama, LMStudio)
- Added provider health checks
- Added provider status indicators

- **Ollama Integration**
- Added Ollama Model Manager with real-time updates
- Implemented model version tracking
- Added bulk update capability
- Added progress tracking for model updates
- Displays model details (parameter size, quantization)

- **GitHub Integration**
- Added GitHub connection management
- Implemented secure token storage
- Added connection state persistence
- Real-time connection status updates
- Proper error handling and user feedback

### πŸ“Š Event Logging
- **System Monitoring**
- Added real-time event logging system
- Implemented log filtering by type (info, warning, error, debug)
- Added log export functionality
- Added auto-scroll and search capabilities
- Enhanced log visualization with color coding

### πŸ’« Animations & Interactions
- Added smooth page transitions
- Implemented loading states with spinners
- Added micro-interactions for better feedback
- Enhanced button hover and active states
- Added motion effects for UI elements

### πŸ” Security Features
- Secure token storage
- Added confirmation dialogs for destructive actions
- Implemented data validation
- Added file size and type validation
- Secure connection management

### ♿️ Accessibility
- Improved keyboard navigation
- Enhanced screen reader support
- Added ARIA labels and descriptions
- Implemented focus management
- Added proper dialog accessibility

### 🎯 Developer Experience
- Added comprehensive debug information
- Implemented system status monitoring
- Added version control integration
- Enhanced error handling and reporting
- Added detailed logging system

---

## πŸ”§ Technical Details
- **Frontend Stack**
- React 18 with TypeScript
- Framer Motion for animations
- TailwindCSS for styling
- Radix UI for accessible components

- **State Management**
- Local storage for persistence
- React hooks for state
- Custom stores for global state

- **API Integration**
- GitHub API integration
- Ollama API integration
- Provider API management
- Error boundary implementation

## πŸ“ Notes
- Initial release focusing on core functionality and user experience
- Enhanced dark mode support across all components
- Improved accessibility and keyboard navigation
- Added comprehensive logging and debugging tools
- Implemented robust error handling and user feedback

.windsurf/config.json ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "enabled": true,
3
+ "rulesPath": ".windsurf/rules.json",
4
+ "integration": {
5
+ "ide": {
6
+ "cursor": true,
7
+ "vscode": true
8
+ },
9
+ "autoApply": true,
10
+ "notifications": true,
11
+ "autoFix": {
12
+ "enabled": true,
13
+ "onSave": true,
14
+ "formatOnSave": true,
15
+ "suggestImports": true,
16
+ "suggestComponents": true
17
+ },
18
+ "suggestions": {
19
+ "inline": true,
20
+ "quickFix": true,
21
+ "codeActions": true,
22
+ "snippets": true
23
+ }
24
+ },
25
+ "features": {
26
+ "codeCompletion": true,
27
+ "linting": true,
28
+ "formatting": true,
29
+ "importValidation": true,
30
+ "dependencyChecks": true,
31
+ "uiStandardization": true
32
+ },
33
+ "hooks": {
34
+ "preCommit": true,
35
+ "prePush": true,
36
+ "onFileCreate": true,
37
+ "onImportAdd": true
38
+ }
39
+ }
.windsurf/rules.json ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "version": "1.0",
3
+ "rules": {
4
+ "fileTypes": {
5
+ "typescript": ["ts", "tsx"],
6
+ "javascript": ["js", "jsx", "mjs", "cjs"],
7
+ "json": ["json"],
8
+ "markdown": ["md"],
9
+ "css": ["css"],
10
+ "dockerfile": ["Dockerfile"]
11
+ },
12
+ "formatting": {
13
+ "typescript": {
14
+ "indentSize": 2,
15
+ "useTabs": false,
16
+ "maxLineLength": 100,
17
+ "semicolons": true,
18
+ "quotes": "single",
19
+ "trailingComma": "es5"
20
+ },
21
+ "javascript": {
22
+ "indentSize": 2,
23
+ "useTabs": false,
24
+ "maxLineLength": 100,
25
+ "semicolons": true,
26
+ "quotes": "single",
27
+ "trailingComma": "es5"
28
+ }
29
+ },
30
+ "linting": {
31
+ "typescript": {
32
+ "noUnusedVariables": true,
33
+ "noImplicitAny": true,
34
+ "strictNullChecks": true,
35
+ "noConsole": "warn"
36
+ }
37
+ },
38
+ "dependencies": {
39
+ "nodeVersion": ">=18.18.0",
40
+ "packageManager": "pnpm",
41
+ "requiredFiles": ["package.json", "tsconfig.json", ".env.example"]
42
+ },
43
+ "git": {
44
+ "ignoredPaths": ["node_modules", "build", ".env", ".env.local"],
45
+ "protectedBranches": ["main", "master"]
46
+ },
47
+ "testing": {
48
+ "framework": "vitest",
49
+ "coverage": {
50
+ "statements": 70,
51
+ "branches": 70,
52
+ "functions": 70,
53
+ "lines": 70
54
+ }
55
+ },
56
+ "security": {
57
+ "secrets": {
58
+ "patterns": ["API_KEY", "SECRET", "PASSWORD", "TOKEN"],
59
+ "locations": [".env", ".env.local"]
60
+ }
61
+ },
62
+ "commands": {
63
+ "dev": "pnpm dev",
64
+ "build": "pnpm build",
65
+ "test": "pnpm test",
66
+ "lint": "pnpm lint",
67
+ "typecheck": "pnpm typecheck"
68
+ },
69
+ "codeQuality": {
70
+ "imports": {
71
+ "validateImports": true,
72
+ "checkPackageAvailability": true,
73
+ "requireExactVersions": true,
74
+ "preventUnusedImports": true
75
+ },
76
+ "fileManagement": {
77
+ "preventUnnecessaryFiles": true,
78
+ "requireFileJustification": true,
79
+ "checkExistingImplementations": true
80
+ },
81
+ "dependencies": {
82
+ "autoInstallMissing": false,
83
+ "validateVersionCompatibility": true,
84
+ "checkPackageJson": true
85
+ }
86
+ },
87
+ "uiStandards": {
88
+ "styling": {
89
+ "framework": "tailwind",
90
+ "preferredIconSets": ["@iconify-json/ph", "@iconify-json/svg-spinners"],
91
+ "colorScheme": {
92
+ "useSystemPreference": true,
93
+ "supportDarkMode": true
94
+ },
95
+ "components": {
96
+ "preferModern": true,
97
+ "accessibility": true,
98
+ "responsive": true
99
+ }
100
+ }
101
+ }
102
+ }
103
+ }
app/components/settings/Settings.module.scss DELETED
@@ -1,63 +0,0 @@
1
- .settings-tabs {
2
- button {
3
- width: 100%;
4
- display: flex;
5
- align-items: center;
6
- gap: 0.5rem;
7
- padding: 0.75rem 1rem;
8
- border-radius: 0.5rem;
9
- text-align: left;
10
- font-size: 0.875rem;
11
- transition: all 0.2s;
12
- margin-bottom: 0.5rem;
13
-
14
- &.active {
15
- background: var(--bolt-elements-button-primary-background);
16
- color: var(--bolt-elements-textPrimary);
17
- }
18
-
19
- &:not(.active) {
20
- background: var(--bolt-elements-bg-depth-3);
21
- color: var(--bolt-elements-textPrimary);
22
-
23
- &:hover {
24
- background: var(--bolt-elements-button-primary-backgroundHover);
25
- }
26
- }
27
- }
28
- }
29
-
30
- .settings-button {
31
- background-color: var(--bolt-elements-button-primary-background);
32
- color: var(--bolt-elements-textPrimary);
33
- border-radius: 0.5rem;
34
- padding: 0.5rem 1rem;
35
- transition: background-color 0.2s;
36
-
37
- &:hover {
38
- background-color: var(--bolt-elements-button-primary-backgroundHover);
39
- }
40
- }
41
-
42
- .settings-danger-area {
43
- background-color: transparent;
44
- color: var(--bolt-elements-textPrimary);
45
- border-radius: 0.5rem;
46
- padding: 1rem;
47
- margin-bottom: 1rem;
48
- border-style: solid;
49
- border-color: var(--bolt-elements-button-danger-backgroundHover);
50
- border-width: thin;
51
-
52
- button {
53
- background-color: var(--bolt-elements-button-danger-background);
54
- color: var(--bolt-elements-button-danger-text);
55
- border-radius: 0.5rem;
56
- padding: 0.5rem 1rem;
57
- transition: background-color 0.2s;
58
-
59
- &:hover {
60
- background-color: var(--bolt-elements-button-danger-backgroundHover);
61
- }
62
- }
63
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/components/settings/SettingsWindow.tsx CHANGED
@@ -1,10 +1,11 @@
1
  import * as RadixDialog from '@radix-ui/react-dialog';
2
- import { motion } from 'framer-motion';
3
- import { useState, type ReactElement } from 'react';
4
  import { classNames } from '~/utils/classNames';
5
- import { DialogTitle, dialogVariants, dialogBackdropVariants } from '~/components/ui/Dialog';
6
- import { IconButton } from '~/components/ui/IconButton';
7
- import styles from './Settings.module.scss';
 
8
  import ProvidersTab from './providers/ProvidersTab';
9
  import { useSettings } from '~/lib/hooks/useSettings';
10
  import FeaturesTab from './features/FeaturesTab';
@@ -18,110 +19,281 @@ interface SettingsProps {
18
  onClose: () => void;
19
  }
20
 
21
- type TabType = 'data' | 'providers' | 'features' | 'debug' | 'event-logs' | 'connection';
22
-
23
  export const SettingsWindow = ({ open, onClose }: SettingsProps) => {
24
  const { debug, eventLogs } = useSettings();
25
- const [activeTab, setActiveTab] = useState<TabType>('data');
26
-
27
- const tabs: { id: TabType; label: string; icon: string; component?: ReactElement }[] = [
28
- { id: 'data', label: 'Data', icon: 'i-ph:database', component: <DataTab /> },
29
- { id: 'providers', label: 'Providers', icon: 'i-ph:key', component: <ProvidersTab /> },
30
- { id: 'connection', label: 'Connection', icon: 'i-ph:link', component: <ConnectionsTab /> },
31
- { id: 'features', label: 'Features', icon: 'i-ph:star', component: <FeaturesTab /> },
32
- ...(debug
33
- ? [
34
- {
35
- id: 'debug' as TabType,
36
- label: 'Debug Tab',
37
- icon: 'i-ph:bug',
38
- component: <DebugTab />,
39
- },
40
- ]
41
- : []),
42
- ...(eventLogs
43
- ? [
44
- {
45
- id: 'event-logs' as TabType,
46
- label: 'Event Logs',
47
- icon: 'i-ph:list-bullets',
48
- component: <EventLogsTab />,
49
- },
50
- ]
51
- : []),
52
- ];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
 
54
  return (
55
  <RadixDialog.Root open={open}>
56
  <RadixDialog.Portal>
57
- <RadixDialog.Overlay asChild onClick={onClose}>
58
- <motion.div
59
- className="bg-black/50 fixed inset-0 z-max backdrop-blur-sm"
60
- initial="closed"
61
- animate="open"
62
- exit="closed"
63
- variants={dialogBackdropVariants}
64
- />
65
- </RadixDialog.Overlay>
66
- <RadixDialog.Content aria-describedby={undefined} asChild>
67
- <motion.div
68
- className="fixed top-[50%] left-[50%] z-max h-[85vh] w-[90vw] max-w-[900px] translate-x-[-50%] translate-y-[-50%] border border-bolt-elements-borderColor rounded-lg shadow-lg focus:outline-none overflow-hidden"
69
- initial="closed"
70
- animate="open"
71
- exit="closed"
72
- variants={dialogVariants}
73
- >
74
- <div className="flex h-full">
75
- <div
76
- className={classNames(
77
- 'w-48 border-r border-bolt-elements-borderColor bg-bolt-elements-background-depth-1 p-4 flex flex-col justify-between',
78
- styles['settings-tabs'],
79
- )}
80
- >
81
- <DialogTitle className="flex-shrink-0 text-lg font-semibold text-bolt-elements-textPrimary mb-2">
82
- Settings
83
- </DialogTitle>
84
- {tabs.map((tab) => (
85
- <button
86
- key={tab.id}
87
- onClick={() => setActiveTab(tab.id)}
88
- className={classNames(activeTab === tab.id ? styles.active : '')}
89
- >
90
- <div className={tab.icon} />
91
- {tab.label}
92
- </button>
93
- ))}
94
- <div className="mt-auto flex flex-col gap-2">
95
- <a
96
- href="https://github.com/stackblitz-labs/bolt.diy"
97
- target="_blank"
98
- rel="noopener noreferrer"
99
- className={classNames(styles['settings-button'], 'flex items-center gap-2')}
100
  >
101
- <div className="i-ph:github-logo" />
102
- GitHub
103
- </a>
104
- <a
105
- href="https://stackblitz-labs.github.io/bolt.diy/"
106
- target="_blank"
107
- rel="noopener noreferrer"
108
- className={classNames(styles['settings-button'], 'flex items-center gap-2')}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  >
110
- <div className="i-ph:book" />
111
- Docs
112
- </a>
113
- </div>
114
- </div>
115
-
116
- <div className="flex-1 flex flex-col p-8 pt-10 bg-bolt-elements-background-depth-2">
117
- <div className="flex-1 overflow-y-auto">{tabs.find((tab) => tab.id === activeTab)?.component}</div>
118
- </div>
119
- </div>
120
- <RadixDialog.Close asChild onClick={onClose}>
121
- <IconButton icon="i-ph:x" className="absolute top-[10px] right-[10px]" />
122
- </RadixDialog.Close>
123
- </motion.div>
124
- </RadixDialog.Content>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
  </RadixDialog.Portal>
126
  </RadixDialog.Root>
127
  );
 
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';
 
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
  );
app/components/settings/connections/ConnectionsTab.tsx CHANGED
@@ -1,150 +1,207 @@
1
  import React, { useState, useEffect } from 'react';
2
- import { toast } from 'react-toastify';
3
- import Cookies from 'js-cookie';
4
  import { logStore } from '~/lib/stores/logs';
 
 
 
5
 
6
  interface GitHubUserResponse {
7
  login: string;
8
- id: number;
9
- [key: string]: any; // for other properties we don't explicitly need
10
  }
11
 
12
- export default function ConnectionsTab() {
13
- const [githubUsername, setGithubUsername] = useState(Cookies.get('githubUsername') || '');
14
- const [githubToken, setGithubToken] = useState(Cookies.get('githubToken') || '');
15
- const [isConnected, setIsConnected] = useState(false);
16
- const [isVerifying, setIsVerifying] = useState(false);
17
 
 
 
 
 
 
 
 
 
 
18
  useEffect(() => {
19
- // Check if credentials exist and verify them
20
- if (githubUsername && githubToken) {
21
- verifyGitHubCredentials();
 
22
  }
23
- }, []);
24
 
25
- const verifyGitHubCredentials = async () => {
26
- setIsVerifying(true);
27
 
 
28
  try {
 
 
29
  const response = await fetch('https://api.github.com/user', {
30
  headers: {
31
- Authorization: `Bearer ${githubToken}`,
32
  },
33
  });
34
 
35
- if (response.ok) {
36
- const data = (await response.json()) as GitHubUserResponse;
37
-
38
- if (data.login === githubUsername) {
39
- setIsConnected(true);
40
- return true;
41
- }
42
  }
43
 
44
- setIsConnected(false);
 
45
 
46
- return false;
 
 
 
47
  } catch (error) {
48
- console.error('Error verifying GitHub credentials:', error);
49
- setIsConnected(false);
50
-
51
- return false;
52
  } finally {
53
- setIsVerifying(false);
54
  }
55
  };
56
 
57
- const handleSaveConnection = async () => {
58
- if (!githubUsername || !githubToken) {
59
- toast.error('Please provide both GitHub username and token');
60
- return;
61
- }
62
-
63
- setIsVerifying(true);
64
-
65
- const isValid = await verifyGitHubCredentials();
66
-
67
- if (isValid) {
68
- Cookies.set('githubUsername', githubUsername);
69
- Cookies.set('githubToken', githubToken);
70
- logStore.logSystem('GitHub connection settings updated', {
71
- username: githubUsername,
72
- hasToken: !!githubToken,
73
- });
74
- toast.success('GitHub credentials verified and saved successfully!');
75
- Cookies.set('git:github.com', JSON.stringify({ username: githubToken, password: 'x-oauth-basic' }));
76
- setIsConnected(true);
77
- } else {
78
- toast.error('Invalid GitHub credentials. Please check your username and token.');
79
- }
80
  };
81
 
82
  const handleDisconnect = () => {
83
- Cookies.remove('githubUsername');
84
- Cookies.remove('githubToken');
85
- Cookies.remove('git:github.com');
86
- setGithubUsername('');
87
- setGithubToken('');
88
- setIsConnected(false);
89
- logStore.logSystem('GitHub connection removed');
90
- toast.success('GitHub connection removed successfully!');
91
  };
92
 
93
- return (
94
- <div className="p-4 mb-4 border border-bolt-elements-borderColor rounded-lg bg-bolt-elements-background-depth-3">
95
- <h3 className="text-lg font-medium text-bolt-elements-textPrimary mb-4">GitHub Connection</h3>
96
- <div className="flex mb-4">
97
- <div className="flex-1 mr-2">
98
- <label className="block text-sm text-bolt-elements-textSecondary mb-1">GitHub Username:</label>
99
- <input
100
- type="text"
101
- value={githubUsername}
102
- onChange={(e) => setGithubUsername(e.target.value)}
103
- disabled={isVerifying}
104
- className="w-full bg-white dark:bg-bolt-elements-background-depth-4 relative px-2 py-1.5 rounded-md focus:outline-none placeholder-bolt-elements-textTertiary text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary border border-bolt-elements-borderColor disabled:opacity-50"
105
- />
106
- </div>
107
- <div className="flex-1">
108
- <label className="block text-sm text-bolt-elements-textSecondary mb-1">Personal Access Token:</label>
109
- <input
110
- type="password"
111
- value={githubToken}
112
- onChange={(e) => setGithubToken(e.target.value)}
113
- disabled={isVerifying}
114
- className="w-full bg-white dark:bg-bolt-elements-background-depth-4 relative px-2 py-1.5 rounded-md focus:outline-none placeholder-bolt-elements-textTertiary text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary border border-bolt-elements-borderColor disabled:opacity-50"
115
- />
116
  </div>
117
  </div>
118
- <div className="flex mb-4 items-center">
119
- {!isConnected ? (
120
- <button
121
- onClick={handleSaveConnection}
122
- disabled={isVerifying || !githubUsername || !githubToken}
123
- className="bg-bolt-elements-button-primary-background rounded-lg px-4 py-2 mr-2 transition-colors duration-200 hover:bg-bolt-elements-button-primary-backgroundHover text-bolt-elements-button-primary-text disabled:opacity-50 disabled:cursor-not-allowed flex items-center"
124
- >
125
- {isVerifying ? (
126
- <>
127
- <div className="i-ph:spinner animate-spin mr-2" />
128
- Verifying...
129
- </>
130
- ) : (
131
- 'Connect'
132
- )}
133
- </button>
134
- ) : (
135
- <button
136
- onClick={handleDisconnect}
137
- className="bg-bolt-elements-button-danger-background rounded-lg px-4 py-2 mr-2 transition-colors duration-200 hover:bg-bolt-elements-button-danger-backgroundHover text-bolt-elements-button-danger-text"
138
- >
139
- Disconnect
140
- </button>
141
- )}
142
- {isConnected && (
143
- <span className="text-sm text-green-600 flex items-center">
144
- <div className="i-ph:check-circle mr-1" />
145
- Connected to GitHub
146
- </span>
147
- )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  </div>
149
  </div>
150
  );
 
1
  import React, { useState, useEffect } from 'react';
 
 
2
  import { logStore } from '~/lib/stores/logs';
3
+ import { classNames } from '~/utils/classNames';
4
+ import { motion } from 'framer-motion';
5
+ import { toast } from 'react-toastify';
6
 
7
  interface GitHubUserResponse {
8
  login: string;
9
+ avatar_url: string;
10
+ html_url: string;
11
  }
12
 
13
+ interface GitHubConnection {
14
+ user: GitHubUserResponse | null;
15
+ token: string;
16
+ }
 
17
 
18
+ export default function ConnectionsTab() {
19
+ const [connection, setConnection] = useState<GitHubConnection>({
20
+ user: null,
21
+ token: '',
22
+ });
23
+ const [isLoading, setIsLoading] = useState(true);
24
+ const [isConnecting, setIsConnecting] = useState(false);
25
+
26
+ // Load saved connection on mount
27
  useEffect(() => {
28
+ const savedConnection = localStorage.getItem('github_connection');
29
+
30
+ if (savedConnection) {
31
+ setConnection(JSON.parse(savedConnection));
32
  }
 
33
 
34
+ setIsLoading(false);
35
+ }, []);
36
 
37
+ const fetchGithubUser = async (token: string) => {
38
  try {
39
+ setIsConnecting(true);
40
+
41
  const response = await fetch('https://api.github.com/user', {
42
  headers: {
43
+ Authorization: `Bearer ${token}`,
44
  },
45
  });
46
 
47
+ if (!response.ok) {
48
+ throw new Error('Invalid token or unauthorized');
 
 
 
 
 
49
  }
50
 
51
+ const data = (await response.json()) as GitHubUserResponse;
52
+ const newConnection = { user: data, token };
53
 
54
+ // Save connection
55
+ localStorage.setItem('github_connection', JSON.stringify(newConnection));
56
+ setConnection(newConnection);
57
+ toast.success('Successfully connected to GitHub');
58
  } catch (error) {
59
+ logStore.logError('Failed to authenticate with GitHub', { error });
60
+ toast.error('Failed to connect to GitHub');
61
+ setConnection({ user: null, token: '' });
 
62
  } finally {
63
+ setIsConnecting(false);
64
  }
65
  };
66
 
67
+ const handleConnect = async (event: React.FormEvent) => {
68
+ event.preventDefault();
69
+ await fetchGithubUser(connection.token);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  };
71
 
72
  const handleDisconnect = () => {
73
+ localStorage.removeItem('github_connection');
74
+ setConnection({ user: null, token: '' });
75
+ toast.success('Disconnected from GitHub');
 
 
 
 
 
76
  };
77
 
78
+ if (isLoading) {
79
+ return (
80
+ <div className="flex items-center justify-center p-4">
81
+ <div className="flex items-center gap-2">
82
+ <div className="i-ph:spinner-gap-bold animate-spin w-4 h-4" />
83
+ <span className="text-bolt-elements-textSecondary">Loading...</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  </div>
85
  </div>
86
+ );
87
+ }
88
+
89
+ return (
90
+ <div className="space-y-4">
91
+ {/* Header */}
92
+ <motion.div
93
+ className="flex items-center gap-2 mb-2"
94
+ initial={{ opacity: 0, y: 20 }}
95
+ animate={{ opacity: 1, y: 0 }}
96
+ transition={{ delay: 0.1 }}
97
+ >
98
+ <div className="i-ph:plugs-connected w-5 h-5 text-purple-500" />
99
+ <h2 className="text-lg font-medium text-bolt-elements-textPrimary">Connection Settings</h2>
100
+ </motion.div>
101
+ <p className="text-sm text-bolt-elements-textSecondary mb-6">
102
+ Manage your external service connections and integrations
103
+ </p>
104
+
105
+ <div className="grid grid-cols-1 gap-4">
106
+ {/* GitHub Connection */}
107
+ <motion.div
108
+ className="bg-[#FFFFFF] dark:bg-[#0A0A0A] rounded-lg border border-[#E5E5E5] dark:border-[#1A1A1A]"
109
+ initial={{ opacity: 0, y: 20 }}
110
+ animate={{ opacity: 1, y: 0 }}
111
+ transition={{ delay: 0.2 }}
112
+ >
113
+ <div className="p-6 space-y-6">
114
+ <div className="flex items-center gap-2">
115
+ <div className="i-ph:github-logo w-5 h-5 text-bolt-elements-textPrimary" />
116
+ <h3 className="text-base font-medium text-bolt-elements-textPrimary">GitHub Connection</h3>
117
+ </div>
118
+
119
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
120
+ <div>
121
+ <label className="block text-sm text-bolt-elements-textSecondary mb-2">GitHub Username</label>
122
+ <input
123
+ type="text"
124
+ value={connection.user?.login || ''}
125
+ disabled={true}
126
+ placeholder="Not connected"
127
+ className={classNames(
128
+ 'w-full px-3 py-2 rounded-lg text-sm',
129
+ 'bg-[#F8F8F8] dark:bg-[#1A1A1A]',
130
+ 'border border-[#E5E5E5] dark:border-[#333333]',
131
+ 'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
132
+ 'focus:outline-none focus:ring-1 focus:ring-purple-500',
133
+ 'disabled:opacity-50',
134
+ )}
135
+ />
136
+ </div>
137
+
138
+ <div>
139
+ <label className="block text-sm text-bolt-elements-textSecondary mb-2">Personal Access Token</label>
140
+ <input
141
+ type="password"
142
+ value={connection.token}
143
+ onChange={(e) => setConnection((prev) => ({ ...prev, token: e.target.value }))}
144
+ disabled={isConnecting || !!connection.user}
145
+ placeholder="Enter your GitHub token"
146
+ className={classNames(
147
+ 'w-full px-3 py-2 rounded-lg text-sm',
148
+ 'bg-[#F8F8F8] dark:bg-[#1A1A1A]',
149
+ 'border border-[#E5E5E5] dark:border-[#333333]',
150
+ 'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
151
+ 'focus:outline-none focus:ring-1 focus:ring-purple-500',
152
+ 'disabled:opacity-50',
153
+ )}
154
+ />
155
+ </div>
156
+ </div>
157
+
158
+ <div className="flex items-center gap-3">
159
+ {!connection.user ? (
160
+ <button
161
+ onClick={handleConnect}
162
+ disabled={isConnecting || !connection.token}
163
+ className={classNames(
164
+ 'px-4 py-2 rounded-lg text-sm flex items-center gap-2',
165
+ 'bg-purple-500 text-white',
166
+ 'hover:bg-purple-600',
167
+ 'disabled:opacity-50 disabled:cursor-not-allowed',
168
+ )}
169
+ >
170
+ {isConnecting ? (
171
+ <>
172
+ <div className="i-ph:spinner-gap animate-spin" />
173
+ Connecting...
174
+ </>
175
+ ) : (
176
+ <>
177
+ <div className="i-ph:plug-charging w-4 h-4" />
178
+ Connect
179
+ </>
180
+ )}
181
+ </button>
182
+ ) : (
183
+ <button
184
+ onClick={handleDisconnect}
185
+ className={classNames(
186
+ 'px-4 py-2 rounded-lg text-sm flex items-center gap-2',
187
+ 'bg-red-500 text-white',
188
+ 'hover:bg-red-600',
189
+ )}
190
+ >
191
+ <div className="i-ph:plug-x w-4 h-4" />
192
+ Disconnect
193
+ </button>
194
+ )}
195
+
196
+ {connection.user && (
197
+ <span className="text-sm text-green-500 flex items-center gap-1">
198
+ <div className="i-ph:check-circle w-4 h-4" />
199
+ Connected to GitHub
200
+ </span>
201
+ )}
202
+ </div>
203
+ </div>
204
+ </motion.div>
205
  </div>
206
  </div>
207
  );
app/components/settings/data/DataTab.tsx CHANGED
@@ -1,388 +1,422 @@
1
- import React, { useState } from 'react';
2
- import { useNavigate } from '@remix-run/react';
3
- import Cookies from 'js-cookie';
4
  import { toast } from 'react-toastify';
5
- import { db, deleteById, getAll, setMessages } from '~/lib/persistence';
6
- import { logStore } from '~/lib/stores/logs';
7
- import { classNames } from '~/utils/classNames';
8
- import type { Message } from 'ai';
9
-
10
- // List of supported providers that can have API keys
11
- const API_KEY_PROVIDERS = [
12
- 'Anthropic',
13
- 'OpenAI',
14
- 'Google',
15
- 'Groq',
16
- 'HuggingFace',
17
- 'OpenRouter',
18
- 'Deepseek',
19
- 'Mistral',
20
- 'OpenAILike',
21
- 'Together',
22
- 'xAI',
23
- 'Perplexity',
24
- 'Cohere',
25
- 'AzureOpenAI',
26
- 'AmazonBedrock',
27
- ] as const;
28
-
29
- interface ApiKeys {
30
- [key: string]: string;
31
- }
32
 
33
  export default function DataTab() {
34
- const navigate = useNavigate();
 
 
35
  const [isDeleting, setIsDeleting] = useState(false);
36
-
37
- const downloadAsJson = (data: any, filename: string) => {
38
- const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
39
- const url = URL.createObjectURL(blob);
40
- const link = document.createElement('a');
41
- link.href = url;
42
- link.download = filename;
43
- document.body.appendChild(link);
44
- link.click();
45
- document.body.removeChild(link);
46
- URL.revokeObjectURL(url);
47
- };
48
 
49
  const handleExportAllChats = async () => {
50
- if (!db) {
51
- const error = new Error('Database is not available');
52
- logStore.logError('Failed to export chats - DB unavailable', error);
53
- toast.error('Database is not available');
54
-
55
- return;
56
- }
57
-
58
  try {
 
 
 
 
 
59
  const allChats = await getAll(db);
60
  const exportData = {
61
  chats: allChats,
62
  exportDate: new Date().toISOString(),
63
  };
64
 
65
- downloadAsJson(exportData, `all-chats-${new Date().toISOString()}.json`);
66
- logStore.logSystem('Chats exported successfully', { count: allChats.length });
 
 
 
 
 
 
 
 
 
67
  toast.success('Chats exported successfully');
68
  } catch (error) {
69
- logStore.logError('Failed to export chats', error);
70
  toast.error('Failed to export chats');
71
- console.error(error);
72
  }
73
  };
74
 
75
- const handleDeleteAllChats = async () => {
76
- const confirmDelete = window.confirm('Are you sure you want to delete all chats? This action cannot be undone.');
77
-
78
- if (!confirmDelete) {
79
- return;
80
- }
81
-
82
- if (!db) {
83
- const error = new Error('Database is not available');
84
- logStore.logError('Failed to delete chats - DB unavailable', error);
85
- toast.error('Database is not available');
86
-
87
- return;
88
- }
89
-
90
  try {
91
- setIsDeleting(true);
 
 
 
 
92
 
93
- const allChats = await getAll(db);
94
- await Promise.all(allChats.map((chat) => deleteById(db!, chat.id)));
95
- logStore.logSystem('All chats deleted successfully', { count: allChats.length });
96
- toast.success('All chats deleted successfully');
97
- navigate('/', { replace: true });
 
 
 
 
 
 
98
  } catch (error) {
99
- logStore.logError('Failed to delete chats', error);
100
- toast.error('Failed to delete chats');
101
- console.error(error);
102
- } finally {
103
- setIsDeleting(false);
104
  }
105
  };
106
 
107
- const handleExportSettings = () => {
108
- const settings = {
109
- providers: Cookies.get('providers'),
110
- isDebugEnabled: Cookies.get('isDebugEnabled'),
111
- isEventLogsEnabled: Cookies.get('isEventLogsEnabled'),
112
- isLocalModelsEnabled: Cookies.get('isLocalModelsEnabled'),
113
- promptId: Cookies.get('promptId'),
114
- isLatestBranch: Cookies.get('isLatestBranch'),
115
- commitHash: Cookies.get('commitHash'),
116
- eventLogs: Cookies.get('eventLogs'),
117
- selectedModel: Cookies.get('selectedModel'),
118
- selectedProvider: Cookies.get('selectedProvider'),
119
- githubUsername: Cookies.get('githubUsername'),
120
- githubToken: Cookies.get('githubToken'),
121
- bolt_theme: localStorage.getItem('bolt_theme'),
122
- };
123
-
124
- downloadAsJson(settings, 'bolt-settings.json');
125
- toast.success('Settings exported successfully');
126
- };
127
-
128
- const handleImportSettings = (event: React.ChangeEvent<HTMLInputElement>) => {
129
  const file = event.target.files?.[0];
130
 
131
  if (!file) {
132
  return;
133
  }
134
 
135
- const reader = new FileReader();
136
-
137
- reader.onload = (e) => {
138
- try {
139
- const settings = JSON.parse(e.target?.result as string);
140
-
141
- Object.entries(settings).forEach(([key, value]) => {
142
- if (key === 'bolt_theme') {
143
- if (value) {
144
- localStorage.setItem(key, value as string);
145
- }
146
- } else if (value) {
147
- Cookies.set(key, value as string);
148
- }
149
- });
150
-
151
- toast.success('Settings imported successfully. Please refresh the page for changes to take effect.');
152
- } catch (error) {
153
- toast.error('Failed to import settings. Make sure the file is a valid JSON file.');
154
- console.error('Failed to import settings:', error);
155
- }
156
- };
157
- reader.readAsText(file);
158
- event.target.value = '';
159
- };
160
 
161
- const handleExportApiKeyTemplate = () => {
162
- const template: ApiKeys = {};
163
- API_KEY_PROVIDERS.forEach((provider) => {
164
- template[`${provider}_API_KEY`] = '';
165
- });
166
 
167
- template.OPENAI_LIKE_API_BASE_URL = '';
168
- template.LMSTUDIO_API_BASE_URL = '';
169
- template.OLLAMA_API_BASE_URL = '';
170
- template.TOGETHER_API_BASE_URL = '';
171
 
172
- downloadAsJson(template, 'api-keys-template.json');
173
- toast.success('API keys template exported successfully');
 
 
 
 
174
  };
175
 
176
- const handleImportApiKeys = (event: React.ChangeEvent<HTMLInputElement>) => {
177
  const file = event.target.files?.[0];
178
 
179
  if (!file) {
180
  return;
181
  }
182
 
183
- const reader = new FileReader();
184
-
185
- reader.onload = (e) => {
186
- try {
187
- const apiKeys = JSON.parse(e.target?.result as string);
188
- let importedCount = 0;
189
- const consolidatedKeys: Record<string, string> = {};
190
 
191
- API_KEY_PROVIDERS.forEach((provider) => {
192
- const keyName = `${provider}_API_KEY`;
193
-
194
- if (apiKeys[keyName]) {
195
- consolidatedKeys[provider] = apiKeys[keyName];
196
- importedCount++;
197
- }
198
- });
199
-
200
- if (importedCount > 0) {
201
- // Store all API keys in a single cookie as JSON
202
- Cookies.set('apiKeys', JSON.stringify(consolidatedKeys));
203
-
204
- // Also set individual cookies for backward compatibility
205
- Object.entries(consolidatedKeys).forEach(([provider, key]) => {
206
- Cookies.set(`${provider}_API_KEY`, key);
207
- });
208
-
209
- toast.success(`Successfully imported ${importedCount} API keys/URLs. Refreshing page to apply changes...`);
210
 
211
- // Reload the page after a short delay to allow the toast to be seen
212
- setTimeout(() => {
213
- window.location.reload();
214
- }, 1500);
215
- } else {
216
- toast.warn('No valid API keys found in the file');
217
  }
218
 
219
- // Set base URLs if they exist
220
- ['OPENAI_LIKE_API_BASE_URL', 'LMSTUDIO_API_BASE_URL', 'OLLAMA_API_BASE_URL', 'TOGETHER_API_BASE_URL'].forEach(
221
- (baseUrl) => {
222
- if (apiKeys[baseUrl]) {
223
- Cookies.set(baseUrl, apiKeys[baseUrl]);
224
- }
225
- },
226
- );
227
- } catch (error) {
228
- toast.error('Failed to import API keys. Make sure the file is a valid JSON file.');
229
- console.error('Failed to import API keys:', error);
230
- }
231
- };
232
- reader.readAsText(file);
233
- event.target.value = '';
234
- };
235
 
236
- const processChatData = (
237
- data: any,
238
- ): Array<{
239
- id: string;
240
- messages: Message[];
241
- description: string;
242
- urlId?: string;
243
- }> => {
244
- // Handle Bolt standard format (single chat)
245
- if (data.messages && Array.isArray(data.messages)) {
246
- const chatId = crypto.randomUUID();
247
- return [
248
- {
249
- id: chatId,
250
- messages: data.messages,
251
- description: data.description || 'Imported Chat',
252
- urlId: chatId,
253
- },
254
- ];
255
- }
256
 
257
- // Handle Bolt export format (multiple chats)
258
- if (data.chats && Array.isArray(data.chats)) {
259
- return data.chats.map((chat: { id?: string; messages: Message[]; description?: string; urlId?: string }) => ({
260
- id: chat.id || crypto.randomUUID(),
261
- messages: chat.messages,
262
- description: chat.description || 'Imported Chat',
263
- urlId: chat.urlId,
264
- }));
265
  }
266
-
267
- console.error('No matching format found for:', data);
268
- throw new Error('Unsupported chat format');
269
  };
270
 
271
- const handleImportChats = () => {
272
- const input = document.createElement('input');
273
- input.type = 'file';
274
- input.accept = '.json';
275
-
276
- input.onchange = async (e) => {
277
- const file = (e.target as HTMLInputElement).files?.[0];
278
 
279
- if (!file || !db) {
280
- toast.error('Something went wrong');
281
- return;
282
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
283
 
284
- try {
285
- const content = await file.text();
286
- const data = JSON.parse(content);
287
- const chatsToImport = processChatData(data);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
288
 
289
- for (const chat of chatsToImport) {
290
- await setMessages(db, chat.id, chat.messages, chat.urlId, chat.description);
291
- }
292
 
293
- logStore.logSystem('Chats imported successfully', { count: chatsToImport.length });
294
- toast.success(`Successfully imported ${chatsToImport.length} chat${chatsToImport.length > 1 ? 's' : ''}`);
295
- window.location.reload();
296
- } catch (error) {
297
- if (error instanceof Error) {
298
- logStore.logError('Failed to import chats:', error);
299
- toast.error('Failed to import chats: ' + error.message);
300
- } else {
301
- toast.error('Failed to import chats');
302
- }
 
 
 
 
 
 
303
 
304
- console.error(error);
305
- }
306
- };
307
 
308
- input.click();
 
 
 
 
 
 
 
 
 
309
  };
310
 
311
  return (
312
- <div className="p-4 bg-bolt-elements-bg-depth-2 border border-bolt-elements-borderColor rounded-lg mb-4">
313
- <div className="mb-6">
314
- <h3 className="text-lg font-medium text-bolt-elements-textPrimary mb-4">Data Management</h3>
315
- <div className="space-y-8">
316
- <div className="flex flex-col gap-4">
317
- <div>
318
- <h4 className="text-bolt-elements-textPrimary mb-2">Chat History</h4>
319
- <p className="text-sm text-bolt-elements-textSecondary mb-4">Export or delete all your chat history.</p>
320
- <div className="flex gap-4">
321
- <button
322
- onClick={handleExportAllChats}
323
- className="px-4 py-2 bg-bolt-elements-button-primary-background hover:bg-bolt-elements-button-primary-backgroundHover text-bolt-elements-textPrimary rounded-lg transition-colors"
324
- >
325
- Export All Chats
326
- </button>
327
- <button
328
- onClick={handleImportChats}
329
- className="px-4 py-2 bg-bolt-elements-button-primary-background hover:bg-bolt-elements-button-primary-backgroundHover text-bolt-elements-textPrimary rounded-lg transition-colors"
330
- >
331
- Import Chats
332
- </button>
333
- <button
334
- onClick={handleDeleteAllChats}
335
- disabled={isDeleting}
336
- className={classNames(
337
- 'px-4 py-2 bg-bolt-elements-button-danger-background hover:bg-bolt-elements-button-danger-backgroundHover text-bolt-elements-button-danger-text rounded-lg transition-colors',
338
- isDeleting ? 'opacity-50 cursor-not-allowed' : '',
339
- )}
340
- >
341
- {isDeleting ? 'Deleting...' : 'Delete All Chats'}
342
- </button>
343
- </div>
344
  </div>
345
-
346
- <div>
347
- <h4 className="text-bolt-elements-textPrimary mb-2">Settings Backup</h4>
348
- <p className="text-sm text-bolt-elements-textSecondary mb-4">
349
- Export your settings to a JSON file or import settings from a previously exported file.
350
- </p>
351
- <div className="flex gap-4">
352
- <button
353
- onClick={handleExportSettings}
354
- className="px-4 py-2 bg-bolt-elements-button-primary-background hover:bg-bolt-elements-button-primary-backgroundHover text-bolt-elements-textPrimary rounded-lg transition-colors"
355
- >
356
- Export Settings
357
  </button>
358
- <label className="px-4 py-2 bg-bolt-elements-button-primary-background hover:bg-bolt-elements-button-primary-backgroundHover text-bolt-elements-textPrimary rounded-lg transition-colors cursor-pointer">
359
- Import Settings
360
- <input type="file" accept=".json" onChange={handleImportSettings} className="hidden" />
361
- </label>
362
- </div>
 
 
 
 
 
 
 
 
 
 
363
  </div>
364
-
365
- <div>
366
- <h4 className="text-bolt-elements-textPrimary mb-2">API Keys Management</h4>
367
- <p className="text-sm text-bolt-elements-textSecondary mb-4">
368
- Import API keys from a JSON file or download a template to fill in your keys.
369
- </p>
370
- <div className="flex gap-4">
371
- <button
372
- onClick={handleExportApiKeyTemplate}
373
- className="px-4 py-2 bg-bolt-elements-button-primary-background hover:bg-bolt-elements-button-primary-backgroundHover text-bolt-elements-textPrimary rounded-lg transition-colors"
374
- >
375
- Download Template
 
 
 
 
 
 
 
376
  </button>
377
- <label className="px-4 py-2 bg-bolt-elements-button-primary-background hover:bg-bolt-elements-button-primary-backgroundHover text-bolt-elements-textPrimary rounded-lg transition-colors cursor-pointer">
378
- Import API Keys
379
- <input type="file" accept=".json" onChange={handleImportApiKeys} className="hidden" />
380
- </label>
381
- </div>
 
 
 
 
 
 
 
 
 
 
382
  </div>
383
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
384
  </div>
385
- </div>
386
  </div>
387
  );
388
  }
 
1
+ import { useState, useRef } from 'react';
2
+ import { motion } from 'framer-motion';
 
3
  import { toast } from 'react-toastify';
4
+ import { DialogRoot, DialogClose, Dialog, DialogTitle } from '~/components/ui/Dialog';
5
+ import { db, getAll } from '~/lib/persistence';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
 
7
  export default function DataTab() {
8
+ const [isDownloadingTemplate, setIsDownloadingTemplate] = useState(false);
9
+ const [isImportingKeys, setIsImportingKeys] = useState(false);
10
+ const [isResetting, setIsResetting] = useState(false);
11
  const [isDeleting, setIsDeleting] = useState(false);
12
+ const [showResetInlineConfirm, setShowResetInlineConfirm] = useState(false);
13
+ const [showDeleteInlineConfirm, setShowDeleteInlineConfirm] = useState(false);
14
+ const fileInputRef = useRef<HTMLInputElement>(null);
15
+ const apiKeyFileInputRef = useRef<HTMLInputElement>(null);
 
 
 
 
 
 
 
 
16
 
17
  const handleExportAllChats = async () => {
 
 
 
 
 
 
 
 
18
  try {
19
+ if (!db) {
20
+ throw new Error('Database not initialized');
21
+ }
22
+
23
+ // Get all chats from IndexedDB
24
  const allChats = await getAll(db);
25
  const exportData = {
26
  chats: allChats,
27
  exportDate: new Date().toISOString(),
28
  };
29
 
30
+ // Download as JSON
31
+ const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
32
+ const url = URL.createObjectURL(blob);
33
+ const a = document.createElement('a');
34
+ a.href = url;
35
+ a.download = `bolt-chats-${new Date().toISOString()}.json`;
36
+ document.body.appendChild(a);
37
+ a.click();
38
+ document.body.removeChild(a);
39
+ URL.revokeObjectURL(url);
40
+
41
  toast.success('Chats exported successfully');
42
  } catch (error) {
43
+ console.error('Export error:', error);
44
  toast.error('Failed to export chats');
 
45
  }
46
  };
47
 
48
+ const handleExportSettings = () => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  try {
50
+ const settings = {
51
+ userProfile: localStorage.getItem('bolt_user_profile'),
52
+ settings: localStorage.getItem('bolt_settings'),
53
+ exportDate: new Date().toISOString(),
54
+ };
55
 
56
+ const blob = new Blob([JSON.stringify(settings, null, 2)], { type: 'application/json' });
57
+ const url = URL.createObjectURL(blob);
58
+ const a = document.createElement('a');
59
+ a.href = url;
60
+ a.download = `bolt-settings-${new Date().toISOString()}.json`;
61
+ document.body.appendChild(a);
62
+ a.click();
63
+ document.body.removeChild(a);
64
+ URL.revokeObjectURL(url);
65
+
66
+ toast.success('Settings exported successfully');
67
  } catch (error) {
68
+ console.error('Export error:', error);
69
+ toast.error('Failed to export settings');
 
 
 
70
  }
71
  };
72
 
73
+ const handleImportSettings = async (event: React.ChangeEvent<HTMLInputElement>) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  const file = event.target.files?.[0];
75
 
76
  if (!file) {
77
  return;
78
  }
79
 
80
+ try {
81
+ const content = await file.text();
82
+ const settings = JSON.parse(content);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
 
84
+ if (settings.userProfile) {
85
+ localStorage.setItem('bolt_user_profile', settings.userProfile);
86
+ }
 
 
87
 
88
+ if (settings.settings) {
89
+ localStorage.setItem('bolt_settings', settings.settings);
90
+ }
 
91
 
92
+ window.location.reload(); // Reload to apply settings
93
+ toast.success('Settings imported successfully');
94
+ } catch (error) {
95
+ console.error('Import error:', error);
96
+ toast.error('Failed to import settings');
97
+ }
98
  };
99
 
100
+ const handleImportAPIKeys = async (event: React.ChangeEvent<HTMLInputElement>) => {
101
  const file = event.target.files?.[0];
102
 
103
  if (!file) {
104
  return;
105
  }
106
 
107
+ setIsImportingKeys(true);
 
 
 
 
 
 
108
 
109
+ try {
110
+ const content = await file.text();
111
+ const keys = JSON.parse(content);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
 
113
+ // Validate and save each key
114
+ Object.entries(keys).forEach(([key, value]) => {
115
+ if (typeof value !== 'string') {
116
+ throw new Error(`Invalid value for key: ${key}`);
 
 
117
  }
118
 
119
+ localStorage.setItem(`bolt_${key.toLowerCase()}`, value);
120
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
 
122
+ toast.success('API keys imported successfully');
123
+ } catch (error) {
124
+ console.error('Error importing API keys:', error);
125
+ toast.error('Failed to import API keys');
126
+ } finally {
127
+ setIsImportingKeys(false);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
 
129
+ if (apiKeyFileInputRef.current) {
130
+ apiKeyFileInputRef.current.value = '';
131
+ }
 
 
 
 
 
132
  }
 
 
 
133
  };
134
 
135
+ const handleDownloadTemplate = () => {
136
+ setIsDownloadingTemplate(true);
 
 
 
 
 
137
 
138
+ try {
139
+ const template = {
140
+ Anthropic_API_KEY: '',
141
+ OpenAI_API_KEY: '',
142
+ Google_API_KEY: '',
143
+ Groq_API_KEY: '',
144
+ HuggingFace_API_KEY: '',
145
+ OpenRouter_API_KEY: '',
146
+ Deepseek_API_KEY: '',
147
+ Mistral_API_KEY: '',
148
+ OpenAILike_API_KEY: '',
149
+ Together_API_KEY: '',
150
+ xAI_API_KEY: '',
151
+ Perplexity_API_KEY: '',
152
+ Cohere_API_KEY: '',
153
+ AzureOpenAI_API_KEY: '',
154
+ OPENAI_LIKE_API_BASE_URL: '',
155
+ LMSTUDIO_API_BASE_URL: '',
156
+ OLLAMA_API_BASE_URL: '',
157
+ TOGETHER_API_BASE_URL: '',
158
+ };
159
 
160
+ const blob = new Blob([JSON.stringify(template, null, 2)], { type: 'application/json' });
161
+ const url = URL.createObjectURL(blob);
162
+ const a = document.createElement('a');
163
+ a.href = url;
164
+ a.download = 'bolt-api-keys-template.json';
165
+ document.body.appendChild(a);
166
+ a.click();
167
+ document.body.removeChild(a);
168
+ URL.revokeObjectURL(url);
169
+
170
+ toast.success('Template downloaded successfully');
171
+ } catch (error) {
172
+ console.error('Error downloading template:', error);
173
+ toast.error('Failed to download template');
174
+ } finally {
175
+ setIsDownloadingTemplate(false);
176
+ }
177
+ };
178
 
179
+ const handleResetSettings = async () => {
180
+ setIsResetting(true);
 
181
 
182
+ try {
183
+ // Clear all stored settings
184
+ localStorage.removeItem('bolt_user_profile');
185
+ localStorage.removeItem('bolt_settings');
186
+ localStorage.removeItem('bolt_chat_history');
187
+
188
+ // Reload the page to apply reset
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);
196
+ }
197
+ };
198
 
199
+ const handleDeleteAllChats = async () => {
200
+ setIsDeleting(true);
 
201
 
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);
211
+ }
212
  };
213
 
214
  return (
215
+ <div className="space-y-6">
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" />
223
+ <DialogTitle>Reset All Settings?</DialogTitle>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
224
  </div>
225
+ <p className="text-sm text-bolt-elements-textSecondary mt-2">
226
+ This will reset all your settings to their default values. This action cannot be undone.
227
+ </p>
228
+ <div className="flex justify-end items-center gap-3 mt-6">
229
+ <DialogClose asChild>
230
+ <button className="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">
231
+ Cancel
 
 
 
 
 
232
  </button>
233
+ </DialogClose>
234
+ <motion.button
235
+ className="inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm bg-white dark:bg-[#1A1A1A] text-yellow-600 dark:text-yellow-500 hover:bg-yellow-50 dark:hover:bg-yellow-500/10 border border-transparent hover:border-yellow-500/10 dark:hover:border-yellow-500/20"
236
+ onClick={handleResetSettings}
237
+ disabled={isResetting}
238
+ whileHover={{ scale: 1.02 }}
239
+ whileTap={{ scale: 0.98 }}
240
+ >
241
+ {isResetting ? (
242
+ <div className="i-ph:spinner-gap-bold animate-spin w-4 h-4" />
243
+ ) : (
244
+ <div className="i-ph:arrow-counter-clockwise w-4 h-4" />
245
+ )}
246
+ Reset Settings
247
+ </motion.button>
248
  </div>
249
+ </div>
250
+ </Dialog>
251
+ </DialogRoot>
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" />
259
+ <DialogTitle>Delete All Chats?</DialogTitle>
260
+ </div>
261
+ <p className="text-sm text-bolt-elements-textSecondary mt-2">
262
+ This will permanently delete all your chat history. This action cannot be undone.
263
+ </p>
264
+ <div className="flex justify-end items-center gap-3 mt-6">
265
+ <DialogClose asChild>
266
+ <button className="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">
267
+ Cancel
268
  </button>
269
+ </DialogClose>
270
+ <motion.button
271
+ className="inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm bg-white dark:bg-[#1A1A1A] text-red-500 dark:text-red-500 hover:bg-red-50 dark:hover:bg-red-500/10 border border-transparent hover:border-red-500/10 dark:hover:border-red-500/20"
272
+ onClick={handleDeleteAllChats}
273
+ disabled={isDeleting}
274
+ whileHover={{ scale: 1.02 }}
275
+ whileTap={{ scale: 0.98 }}
276
+ >
277
+ {isDeleting ? (
278
+ <div className="i-ph:spinner-gap-bold animate-spin w-4 h-4" />
279
+ ) : (
280
+ <div className="i-ph:trash w-4 h-4" />
281
+ )}
282
+ Delete All
283
+ </motion.button>
284
  </div>
285
  </div>
286
+ </Dialog>
287
+ </DialogRoot>
288
+
289
+ {/* Chat History Section */}
290
+ <motion.div
291
+ className="bg-white dark:bg-[#0A0A0A] rounded-lg p-6 border border-[#E5E5E5] dark:border-[#1A1A1A]"
292
+ initial={{ opacity: 0, y: 20 }}
293
+ animate={{ opacity: 1, y: 0 }}
294
+ transition={{ delay: 0.1 }}
295
+ >
296
+ <div className="flex items-center gap-2 mb-2">
297
+ <div className="i-ph:chat-circle-duotone w-5 h-5 text-purple-500" />
298
+ <h3 className="text-lg font-medium">Chat History</h3>
299
+ </div>
300
+ <p className="text-sm text-bolt-elements-textSecondary mb-4">Export or delete all your chat history.</p>
301
+ <div className="flex gap-4">
302
+ <motion.button
303
+ className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-purple-500 text-white text-sm hover:bg-purple-600"
304
+ whileHover={{ scale: 1.02 }}
305
+ whileTap={{ scale: 0.98 }}
306
+ onClick={handleExportAllChats}
307
+ >
308
+ <div className="i-ph:download-simple w-4 h-4" />
309
+ Export All Chats
310
+ </motion.button>
311
+ <motion.button
312
+ className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-red-50 text-red-500 text-sm hover:bg-red-100 dark:bg-red-500/10 dark:hover:bg-red-500/20"
313
+ whileHover={{ scale: 1.02 }}
314
+ whileTap={{ scale: 0.98 }}
315
+ onClick={() => setShowDeleteInlineConfirm(true)}
316
+ >
317
+ <div className="i-ph:trash w-4 h-4" />
318
+ Delete All Chats
319
+ </motion.button>
320
+ </div>
321
+ </motion.div>
322
+
323
+ {/* Settings Backup Section */}
324
+ <motion.div
325
+ className="bg-white dark:bg-[#0A0A0A] rounded-lg p-6 border border-[#E5E5E5] dark:border-[#1A1A1A]"
326
+ initial={{ opacity: 0, y: 20 }}
327
+ animate={{ opacity: 1, y: 0 }}
328
+ transition={{ delay: 0.2 }}
329
+ >
330
+ <div className="flex items-center gap-2 mb-2">
331
+ <div className="i-ph:gear-duotone w-5 h-5 text-purple-500" />
332
+ <h3 className="text-lg font-medium">Settings Backup</h3>
333
+ </div>
334
+ <p className="text-sm text-bolt-elements-textSecondary mb-4">
335
+ Export your settings to a JSON file or import settings from a previously exported file.
336
+ </p>
337
+ <div className="flex gap-4">
338
+ <motion.button
339
+ className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-purple-500 text-white text-sm hover:bg-purple-600"
340
+ whileHover={{ scale: 1.02 }}
341
+ whileTap={{ scale: 0.98 }}
342
+ onClick={handleExportSettings}
343
+ >
344
+ <div className="i-ph:download-simple w-4 h-4" />
345
+ Export Settings
346
+ </motion.button>
347
+ <motion.button
348
+ className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-purple-500 text-white text-sm hover:bg-purple-600"
349
+ whileHover={{ scale: 1.02 }}
350
+ whileTap={{ scale: 0.98 }}
351
+ onClick={() => fileInputRef.current?.click()}
352
+ >
353
+ <div className="i-ph:upload-simple w-4 h-4" />
354
+ Import Settings
355
+ </motion.button>
356
+ <motion.button
357
+ className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-yellow-50 text-yellow-600 text-sm hover:bg-yellow-100 dark:bg-yellow-500/10 dark:hover:bg-yellow-500/20"
358
+ whileHover={{ scale: 1.02 }}
359
+ whileTap={{ scale: 0.98 }}
360
+ onClick={() => setShowResetInlineConfirm(true)}
361
+ >
362
+ <div className="i-ph:arrow-counter-clockwise w-4 h-4" />
363
+ Reset Settings
364
+ </motion.button>
365
+ </div>
366
+ </motion.div>
367
+
368
+ {/* API Keys Management Section */}
369
+ <motion.div
370
+ className="bg-white dark:bg-[#0A0A0A] rounded-lg p-6 border border-[#E5E5E5] dark:border-[#1A1A1A]"
371
+ initial={{ opacity: 0, y: 20 }}
372
+ animate={{ opacity: 1, y: 0 }}
373
+ transition={{ delay: 0.3 }}
374
+ >
375
+ <div className="flex items-center gap-2 mb-2">
376
+ <div className="i-ph:key-duotone w-5 h-5 text-purple-500" />
377
+ <h3 className="text-lg font-medium">API Keys Management</h3>
378
+ </div>
379
+ <p className="text-sm text-bolt-elements-textSecondary mb-4">
380
+ Import API keys from a JSON file or download a template to fill in your keys.
381
+ </p>
382
+ <div className="flex gap-4">
383
+ <input
384
+ ref={apiKeyFileInputRef}
385
+ type="file"
386
+ accept=".json"
387
+ onChange={handleImportAPIKeys}
388
+ className="hidden"
389
+ />
390
+ <motion.button
391
+ className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-purple-500 text-white text-sm hover:bg-purple-600"
392
+ whileHover={{ scale: 1.02 }}
393
+ whileTap={{ scale: 0.98 }}
394
+ onClick={handleDownloadTemplate}
395
+ disabled={isDownloadingTemplate}
396
+ >
397
+ {isDownloadingTemplate ? (
398
+ <div className="i-ph:spinner-gap-bold animate-spin" />
399
+ ) : (
400
+ <div className="i-ph:download-simple w-4 h-4" />
401
+ )}
402
+ Download Template
403
+ </motion.button>
404
+ <motion.button
405
+ className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-purple-500 text-white text-sm hover:bg-purple-600"
406
+ whileHover={{ scale: 1.02 }}
407
+ whileTap={{ scale: 0.98 }}
408
+ onClick={() => apiKeyFileInputRef.current?.click()}
409
+ disabled={isImportingKeys}
410
+ >
411
+ {isImportingKeys ? (
412
+ <div className="i-ph:spinner-gap-bold animate-spin w-4 h-4" />
413
+ ) : (
414
+ <div className="i-ph:upload-simple w-4 h-4" />
415
+ )}
416
+ Import API Keys
417
+ </motion.button>
418
  </div>
419
+ </motion.div>
420
  </div>
421
  );
422
  }
app/components/settings/debug/DebugTab.tsx CHANGED
@@ -2,6 +2,9 @@ import React, { useCallback, useEffect, useState } from 'react';
2
  import { useSettings } from '~/lib/hooks/useSettings';
3
  import { toast } from 'react-toastify';
4
  import { providerBaseUrlEnvKeys } from '~/utils/constants';
 
 
 
5
 
6
  interface ProviderStatus {
7
  name: string;
@@ -438,107 +441,182 @@ export default function DebugTab() {
438
  }, [activeProviders, systemInfo, isLatestBranch]);
439
 
440
  return (
441
- <div className="p-4 space-y-6">
442
  <div className="flex items-center justify-between">
443
- <h3 className="text-lg font-medium text-bolt-elements-textPrimary">Debug Information</h3>
 
 
 
444
  <div className="flex gap-2">
445
- <button
446
  onClick={handleCopyToClipboard}
447
- className="bg-bolt-elements-button-primary-background rounded-lg px-4 py-2 transition-colors duration-200 hover:bg-bolt-elements-button-primary-backgroundHover text-bolt-elements-button-primary-text"
 
 
448
  >
 
449
  Copy Debug Info
450
- </button>
451
- <button
452
  onClick={handleCheckForUpdate}
453
  disabled={isCheckingUpdate}
454
- className={`bg-bolt-elements-button-primary-background rounded-lg px-4 py-2 transition-colors duration-200
455
- ${!isCheckingUpdate ? 'hover:bg-bolt-elements-button-primary-backgroundHover' : 'opacity-75 cursor-not-allowed'}
456
- text-bolt-elements-button-primary-text`}
457
  >
458
- {isCheckingUpdate ? 'Checking...' : 'Check for Updates'}
459
- </button>
 
 
 
 
 
 
 
 
 
 
460
  </div>
461
  </div>
462
 
463
  {updateMessage && (
464
- <div
465
- className={`bg-bolt-elements-surface rounded-lg p-3 ${
466
- updateMessage.includes('Update available') ? 'border-l-4 border-yellow-400' : ''
467
- }`}
 
 
 
 
468
  >
469
- <p className="text-bolt-elements-textSecondary whitespace-pre-line">{updateMessage}</p>
470
- {updateMessage.includes('Update available') && (
471
- <div className="mt-3 text-sm">
472
- <p className="font-medium text-bolt-elements-textPrimary">To update:</p>
473
- <ol className="list-decimal ml-4 mt-1 text-bolt-elements-textSecondary">
474
- <li>
475
- Pull the latest changes:{' '}
476
- <code className="bg-bolt-elements-surface-hover px-1 rounded">git pull upstream main</code>
477
- </li>
478
- <li>
479
- Install any new dependencies:{' '}
480
- <code className="bg-bolt-elements-surface-hover px-1 rounded">pnpm install</code>
481
- </li>
482
- <li>Restart the application</li>
483
- </ol>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
484
  </div>
485
- )}
486
- </div>
487
  )}
488
 
489
  <section className="space-y-4">
490
- <div>
491
- <h4 className="text-md font-medium text-bolt-elements-textPrimary mb-2">System Information</h4>
492
- <div className="bg-bolt-elements-surface rounded-lg p-4">
 
 
 
493
  <div className="grid grid-cols-2 md:grid-cols-3 gap-4">
494
  <div>
495
- <p className="text-xs text-bolt-elements-textSecondary">Operating System</p>
 
 
 
496
  <p className="text-sm font-medium text-bolt-elements-textPrimary">{systemInfo.os}</p>
497
  </div>
498
  <div>
499
- <p className="text-xs text-bolt-elements-textSecondary">Device Type</p>
 
 
 
500
  <p className="text-sm font-medium text-bolt-elements-textPrimary">{systemInfo.deviceType}</p>
501
  </div>
502
  <div>
503
- <p className="text-xs text-bolt-elements-textSecondary">Browser</p>
 
 
 
504
  <p className="text-sm font-medium text-bolt-elements-textPrimary">{systemInfo.browser}</p>
505
  </div>
506
  <div>
507
- <p className="text-xs text-bolt-elements-textSecondary">Display</p>
 
 
 
508
  <p className="text-sm font-medium text-bolt-elements-textPrimary">
509
  {systemInfo.screen} ({systemInfo.colorDepth}) @{systemInfo.pixelRatio}x
510
  </p>
511
  </div>
512
  <div>
513
- <p className="text-xs text-bolt-elements-textSecondary">Connection</p>
514
- <p className="text-sm font-medium flex items-center gap-2">
 
 
 
515
  <span
516
- className={`inline-block w-2 h-2 rounded-full ${systemInfo.online ? 'bg-green-500' : 'bg-red-500'}`}
517
  />
518
- <span className={`${systemInfo.online ? 'text-green-600' : 'text-red-600'}`}>
 
 
519
  {systemInfo.online ? 'Online' : 'Offline'}
520
  </span>
521
- </p>
522
- </div>
523
- <div>
524
- <p className="text-xs text-bolt-elements-textSecondary">Screen Resolution</p>
525
- <p className="text-sm font-medium text-bolt-elements-textPrimary">{systemInfo.screen}</p>
526
  </div>
527
  <div>
528
- <p className="text-xs text-bolt-elements-textSecondary">Language</p>
 
 
 
529
  <p className="text-sm font-medium text-bolt-elements-textPrimary">{systemInfo.language}</p>
530
  </div>
531
  <div>
532
- <p className="text-xs text-bolt-elements-textSecondary">Timezone</p>
 
 
 
533
  <p className="text-sm font-medium text-bolt-elements-textPrimary">{systemInfo.timezone}</p>
534
  </div>
535
  <div>
536
- <p className="text-xs text-bolt-elements-textSecondary">CPU Cores</p>
 
 
 
537
  <p className="text-sm font-medium text-bolt-elements-textPrimary">{systemInfo.cores}</p>
538
  </div>
539
  </div>
540
- <div className="mt-3 pt-3 border-t border-bolt-elements-surface-hover">
541
- <p className="text-xs text-bolt-elements-textSecondary">Version</p>
 
 
 
542
  <p className="text-sm font-medium text-bolt-elements-textPrimary font-mono">
543
  {connitJson.commit.slice(0, 7)}
544
  <span className="ml-2 text-xs text-bolt-elements-textSecondary">
@@ -546,22 +624,31 @@ export default function DebugTab() {
546
  </span>
547
  </p>
548
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
549
  </div>
550
- </div>
551
-
552
- <div>
553
- <h4 className="text-md font-medium text-bolt-elements-textPrimary mb-2">Local LLM Status</h4>
554
- <div className="bg-bolt-elements-surface rounded-lg">
555
- <div className="grid grid-cols-1 divide-y">
556
  {activeProviders.map((provider) => (
557
- <div key={provider.name} className="p-3 flex flex-col space-y-2">
558
  <div className="flex items-center justify-between">
559
  <div className="flex items-center gap-3">
560
  <div className="flex-shrink-0">
561
  <div
562
- className={`w-2 h-2 rounded-full ${
563
- !provider.enabled ? 'bg-gray-300' : provider.isRunning ? 'bg-green-400' : 'bg-red-400'
564
- }`}
 
565
  />
566
  </div>
567
  <div>
@@ -575,17 +662,21 @@ export default function DebugTab() {
575
  </div>
576
  <div className="flex items-center gap-2">
577
  <span
578
- className={`px-2 py-0.5 text-xs rounded-full ${
579
- provider.enabled ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'
580
- }`}
 
 
 
581
  >
582
  {provider.enabled ? 'Enabled' : 'Disabled'}
583
  </span>
584
  {provider.enabled && (
585
  <span
586
- className={`px-2 py-0.5 text-xs rounded-full ${
587
- provider.isRunning ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
588
- }`}
 
589
  >
590
  {provider.isRunning ? 'Running' : 'Not Running'}
591
  </span>
@@ -593,31 +684,28 @@ export default function DebugTab() {
593
  </div>
594
  </div>
595
 
596
- <div className="pl-5 flex flex-col space-y-1 text-xs">
597
- {/* Status Details */}
598
  <div className="flex flex-wrap gap-2">
599
- <span className="text-bolt-elements-textSecondary">
600
  Last checked: {new Date(provider.lastChecked).toLocaleTimeString()}
601
  </span>
602
  {provider.responseTime && (
603
- <span className="text-bolt-elements-textSecondary">
604
  Response time: {Math.round(provider.responseTime)}ms
605
  </span>
606
  )}
607
  </div>
608
 
609
- {/* Error Message */}
610
  {provider.error && (
611
- <div className="mt-1 text-red-600 bg-red-50 rounded-md p-2">
612
  <span className="font-medium">Error:</span> {provider.error}
613
  </div>
614
  )}
615
 
616
- {/* Connection Info */}
617
  {provider.url && (
618
- <div className="text-bolt-elements-textSecondary">
619
  <span className="font-medium">Endpoints checked:</span>
620
- <ul className="list-disc list-inside pl-2 mt-1">
621
  <li>{provider.url} (root)</li>
622
  <li>{provider.url}/api/health</li>
623
  <li>{provider.url}/v1/models</li>
@@ -631,8 +719,8 @@ export default function DebugTab() {
631
  <div className="p-4 text-center text-bolt-elements-textSecondary">No local LLMs configured</div>
632
  )}
633
  </div>
634
- </div>
635
- </div>
636
  </section>
637
  </div>
638
  );
 
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 { settingsStyles } from '~/components/settings/settings.styles';
8
 
9
  interface ProviderStatus {
10
  name: string;
 
441
  }, [activeProviders, systemInfo, isLatestBranch]);
442
 
443
  return (
444
+ <div className="space-y-6">
445
  <div className="flex items-center justify-between">
446
+ <div className="flex items-center gap-2">
447
+ <div className="i-ph:bug-fill text-xl text-purple-500" />
448
+ <h3 className="text-lg font-medium text-bolt-elements-textPrimary">Debug Information</h3>
449
+ </div>
450
  <div className="flex gap-2">
451
+ <motion.button
452
  onClick={handleCopyToClipboard}
453
+ className={classNames(settingsStyles.button.base, settingsStyles.button.primary)}
454
+ whileHover={{ scale: 1.02 }}
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
+ updateMessage.includes('Update available')
496
+ ? 'i-ph:warning-fill text-yellow-500'
497
+ : 'i-ph:info text-bolt-elements-textSecondary',
498
+ 'text-xl flex-shrink-0',
499
+ )}
500
+ />
501
+ <div className="flex-1">
502
+ <p className="text-bolt-elements-textSecondary whitespace-pre-line">{updateMessage}</p>
503
+ {updateMessage.includes('Update available') && (
504
+ <div className="mt-3">
505
+ <p className="font-medium text-bolt-elements-textPrimary">To update:</p>
506
+ <ol className="list-decimal ml-4 mt-1 space-y-2">
507
+ <li className="text-bolt-elements-textSecondary">
508
+ <div className="flex items-center gap-2">
509
+ <div className="i-ph:git-branch text-purple-500" />
510
+ Pull the latest changes:{' '}
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
  </div>
535
+ </div>
536
+ </motion.div>
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
+ <motion.div className={classNames(settingsStyles.card, 'bg-bolt-elements-background-depth-2')}>
546
  <div className="grid grid-cols-2 md:grid-cols-3 gap-4">
547
  <div>
548
+ <div className="flex items-center gap-2 mb-1">
549
+ <div className="i-ph:computer-tower text-bolt-elements-textSecondary" />
550
+ <p className="text-xs text-bolt-elements-textSecondary">Operating System</p>
551
+ </div>
552
  <p className="text-sm font-medium text-bolt-elements-textPrimary">{systemInfo.os}</p>
553
  </div>
554
  <div>
555
+ <div className="flex items-center gap-2 mb-1">
556
+ <div className="i-ph:device-mobile text-bolt-elements-textSecondary" />
557
+ <p className="text-xs text-bolt-elements-textSecondary">Device Type</p>
558
+ </div>
559
  <p className="text-sm font-medium text-bolt-elements-textPrimary">{systemInfo.deviceType}</p>
560
  </div>
561
  <div>
562
+ <div className="flex items-center gap-2 mb-1">
563
+ <div className="i-ph:browser text-bolt-elements-textSecondary" />
564
+ <p className="text-xs text-bolt-elements-textSecondary">Browser</p>
565
+ </div>
566
  <p className="text-sm font-medium text-bolt-elements-textPrimary">{systemInfo.browser}</p>
567
  </div>
568
  <div>
569
+ <div className="flex items-center gap-2 mb-1">
570
+ <div className="i-ph:monitor text-bolt-elements-textSecondary" />
571
+ <p className="text-xs text-bolt-elements-textSecondary">Display</p>
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="flex items-center gap-2 mb-1">
579
+ <div className="i-ph:wifi-high text-bolt-elements-textSecondary" />
580
+ <p className="text-xs text-bolt-elements-textSecondary">Connection</p>
581
+ </div>
582
+ <div className="flex items-center gap-2 mt-1">
583
  <span
584
+ className={classNames('w-2 h-2 rounded-full', systemInfo.online ? 'bg-green-500' : 'bg-red-500')}
585
  />
586
+ <span
587
+ className={classNames('text-sm font-medium', systemInfo.online ? 'text-green-500' : 'text-red-500')}
588
+ >
589
  {systemInfo.online ? 'Online' : 'Offline'}
590
  </span>
591
+ </div>
 
 
 
 
592
  </div>
593
  <div>
594
+ <div className="flex items-center gap-2 mb-1">
595
+ <div className="i-ph:translate text-bolt-elements-textSecondary" />
596
+ <p className="text-xs text-bolt-elements-textSecondary">Language</p>
597
+ </div>
598
  <p className="text-sm font-medium text-bolt-elements-textPrimary">{systemInfo.language}</p>
599
  </div>
600
  <div>
601
+ <div className="flex items-center gap-2 mb-1">
602
+ <div className="i-ph:clock text-bolt-elements-textSecondary" />
603
+ <p className="text-xs text-bolt-elements-textSecondary">Timezone</p>
604
+ </div>
605
  <p className="text-sm font-medium text-bolt-elements-textPrimary">{systemInfo.timezone}</p>
606
  </div>
607
  <div>
608
+ <div className="flex items-center gap-2 mb-1">
609
+ <div className="i-ph:cpu text-bolt-elements-textSecondary" />
610
+ <p className="text-xs text-bolt-elements-textSecondary">CPU Cores</p>
611
+ </div>
612
  <p className="text-sm font-medium text-bolt-elements-textPrimary">{systemInfo.cores}</p>
613
  </div>
614
  </div>
615
+ <div className="mt-3 pt-3 border-t border-bolt-elements-borderColor">
616
+ <div className="flex items-center gap-2 mb-1">
617
+ <div className="i-ph:git-commit text-bolt-elements-textSecondary" />
618
+ <p className="text-xs text-bolt-elements-textSecondary">Version</p>
619
+ </div>
620
  <p className="text-sm font-medium text-bolt-elements-textPrimary font-mono">
621
  {connitJson.commit.slice(0, 7)}
622
  <span className="ml-2 text-xs text-bolt-elements-textSecondary">
 
624
  </span>
625
  </p>
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
+ <motion.div className={classNames(settingsStyles.card, 'bg-bolt-elements-background-depth-2')}>
641
+ <div className="divide-y divide-bolt-elements-borderColor">
 
 
 
 
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>
 
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>
 
684
  </div>
685
  </div>
686
 
687
+ <div className="pl-5 mt-2 space-y-2">
 
688
  <div className="flex flex-wrap gap-2">
689
+ <span className="text-xs text-bolt-elements-textSecondary">
690
  Last checked: {new Date(provider.lastChecked).toLocaleTimeString()}
691
  </span>
692
  {provider.responseTime && (
693
+ <span className="text-xs text-bolt-elements-textSecondary">
694
  Response time: {Math.round(provider.responseTime)}ms
695
  </span>
696
  )}
697
  </div>
698
 
 
699
  {provider.error && (
700
+ <div className="mt-2 text-xs text-red-500 bg-red-500/10 rounded-md p-2">
701
  <span className="font-medium">Error:</span> {provider.error}
702
  </div>
703
  )}
704
 
 
705
  {provider.url && (
706
+ <div className="text-xs text-bolt-elements-textSecondary mt-2">
707
  <span className="font-medium">Endpoints checked:</span>
708
+ <ul className="list-disc list-inside pl-2 mt-1 space-y-1">
709
  <li>{provider.url} (root)</li>
710
  <li>{provider.url}/api/health</li>
711
  <li>{provider.url}/v1/models</li>
 
719
  <div className="p-4 text-center text-bolt-elements-textSecondary">No local LLMs configured</div>
720
  )}
721
  </div>
722
+ </motion.div>
723
+ </motion.div>
724
  </section>
725
  </div>
726
  );
app/components/settings/event-logs/EventLogsTab.tsx CHANGED
@@ -1,22 +1,27 @@
1
- import React, { useCallback, useEffect, useState, useMemo } 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
 
9
  export default function EventLogsTab() {
10
  const {} = useSettings();
11
  const showLogs = useStore(logStore.showLogs);
 
12
  const [logLevel, setLogLevel] = useState<LogEntry['level'] | 'all'>('info');
13
  const [autoScroll, setAutoScroll] = useState(true);
14
  const [searchQuery, setSearchQuery] = useState('');
15
  const [, forceUpdate] = useState({});
 
 
16
 
17
  const filteredLogs = useMemo(() => {
18
- const logs = logStore.getLogs();
19
- return logs.filter((log) => {
20
  const matchesLevel = !logLevel || log.level === logLevel || logLevel === 'all';
21
  const matchesSearch =
22
  !searchQuery ||
@@ -25,7 +30,9 @@ export default function EventLogsTab() {
25
 
26
  return matchesLevel && matchesSearch;
27
  });
28
- }, [logLevel, searchQuery]);
 
 
29
 
30
  // Effect to initialize showLogs
31
  useEffect(() => {
@@ -37,18 +44,51 @@ export default function EventLogsTab() {
37
  logStore.logSystem('Application initialized', {
38
  version: process.env.NEXT_PUBLIC_APP_VERSION,
39
  environment: process.env.NODE_ENV,
 
 
40
  });
41
 
42
  // Debug logs for system state
43
  logStore.logDebug('System configuration loaded', {
44
  runtime: 'Next.js',
45
- features: ['AI Chat', 'Event Logging'],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  });
47
 
48
  // Warning logs for potential issues
49
  logStore.logWarning('Resource usage threshold approaching', {
50
  memoryUsage: '75%',
51
  cpuLoad: '60%',
 
 
 
 
 
 
 
 
52
  });
53
 
54
  // Error logs with detailed context
@@ -56,16 +96,50 @@ export default function EventLogsTab() {
56
  endpoint: '/api/chat',
57
  retryCount: 3,
58
  lastAttempt: new Date().toISOString(),
 
59
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  }, []);
61
 
 
62
  useEffect(() => {
63
- const container = document.querySelector('.logs-container');
64
 
65
- if (container && autoScroll) {
66
- container.scrollTop = container.scrollHeight;
67
  }
68
- }, [filteredLogs, autoScroll]);
69
 
70
  const handleClearLogs = useCallback(() => {
71
  if (confirm('Are you sure you want to clear all logs?')) {
@@ -103,33 +177,56 @@ export default function EventLogsTab() {
103
  }
104
  }, []);
105
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  const getLevelColor = (level: LogEntry['level']) => {
107
  switch (level) {
108
  case 'info':
109
- return 'text-blue-500';
110
  case 'warning':
111
- return 'text-yellow-500';
112
  case 'error':
113
- return 'text-red-500';
114
  case 'debug':
115
- return 'text-gray-500';
116
  default:
117
  return 'text-bolt-elements-textPrimary';
118
  }
119
  };
120
 
121
  return (
122
- <div className="p-4 h-full flex flex-col">
123
- <div className="flex flex-col space-y-4 mb-4">
124
  {/* Title and Toggles Row */}
125
  <div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
126
- <h3 className="text-lg font-medium text-bolt-elements-textPrimary">Event Logs</h3>
 
 
 
 
 
 
127
  <div className="flex flex-wrap items-center gap-4">
128
- <div className="flex items-center space-x-2">
 
129
  <span className="text-sm text-bolt-elements-textSecondary whitespace-nowrap">Show Actions</span>
130
  <Switch checked={showLogs} onCheckedChange={(checked) => logStore.showLogs.set(checked)} />
131
  </div>
132
- <div className="flex items-center space-x-2">
 
133
  <span className="text-sm text-bolt-elements-textSecondary whitespace-nowrap">Auto-scroll</span>
134
  <Switch checked={autoScroll} onCheckedChange={setAutoScroll} />
135
  </div>
@@ -137,83 +234,166 @@ export default function EventLogsTab() {
137
  </div>
138
 
139
  {/* Controls Row */}
140
- <div className="flex flex-wrap items-center gap-2">
141
- <select
142
- value={logLevel}
143
- onChange={(e) => setLogLevel(e.target.value as LogEntry['level'])}
144
- className="flex-1 p-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus transition-all lg:max-w-[20%] text-sm min-w-[100px]"
145
- >
146
- <option value="all">All</option>
147
- <option value="info">Info</option>
148
- <option value="warning">Warning</option>
149
- <option value="error">Error</option>
150
- <option value="debug">Debug</option>
151
- </select>
 
 
 
 
 
 
 
 
 
 
 
 
152
  <div className="flex-1 min-w-[200px]">
153
- <input
154
- type="text"
155
- placeholder="Search logs..."
156
- value={searchQuery}
157
- onChange={(e) => setSearchQuery(e.target.value)}
158
- className="w-full bg-white dark:bg-bolt-elements-background-depth-4 relative px-2 py-1.5 rounded-md focus:outline-none placeholder-bolt-elements-textTertiary text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary border border-bolt-elements-borderColor"
159
- />
 
 
 
 
 
 
 
 
 
 
160
  </div>
161
  {showLogs && (
162
  <div className="flex items-center gap-2 flex-nowrap">
163
- <button
164
  onClick={handleExportLogs}
165
- className={classNames(
166
- 'bg-bolt-elements-button-primary-background',
167
- 'rounded-lg px-4 py-2 transition-colors duration-200',
168
- 'hover:bg-bolt-elements-button-primary-backgroundHover',
169
- 'text-bolt-elements-button-primary-text',
170
- )}
171
  >
 
172
  Export Logs
173
- </button>
174
- <button
175
  onClick={handleClearLogs}
176
- className={classNames(
177
- 'bg-bolt-elements-button-danger-background',
178
- 'rounded-lg px-4 py-2 transition-colors duration-200',
179
- 'hover:bg-bolt-elements-button-danger-backgroundHover',
180
- 'text-bolt-elements-button-danger-text',
181
- )}
182
  >
 
183
  Clear Logs
184
- </button>
185
  </div>
186
  )}
187
  </div>
188
  </div>
189
 
190
- <div className="bg-bolt-elements-bg-depth-1 rounded-lg p-4 h-[calc(100vh - 250px)] min-h-[400px] overflow-y-auto logs-container overflow-y-auto">
 
 
 
 
 
 
 
 
 
191
  {filteredLogs.length === 0 ? (
192
- <div className="text-center text-bolt-elements-textSecondary py-8">No logs found</div>
193
- ) : (
194
- filteredLogs.map((log, index) => (
195
- <div
196
- key={index}
197
- className="text-sm mb-3 font-mono border-b border-bolt-elements-borderColor pb-2 last:border-0"
 
 
 
 
 
 
198
  >
199
- <div className="flex items-start space-x-2 flex-wrap">
200
- <span className={`font-bold ${getLevelColor(log.level)} whitespace-nowrap`}>
201
- [{log.level.toUpperCase()}]
202
- </span>
203
- <span className="text-bolt-elements-textSecondary whitespace-nowrap">
204
- {new Date(log.timestamp).toLocaleString()}
205
- </span>
206
- <span className="text-bolt-elements-textPrimary break-all">{log.message}</span>
207
- </div>
208
- {log.details && (
209
- <pre className="mt-2 text-xs text-bolt-elements-textSecondary overflow-x-auto whitespace-pre-wrap break-all">
210
- {JSON.stringify(log.details, null, 2)}
211
- </pre>
212
- )}
213
- </div>
214
- ))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
  )}
216
- </div>
217
  </div>
218
  );
219
  }
 
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
  export default function EventLogsTab() {
12
  const {} = useSettings();
13
  const showLogs = useStore(logStore.showLogs);
14
+ const logs = useStore(logStore.logs);
15
  const [logLevel, setLogLevel] = useState<LogEntry['level'] | 'all'>('info');
16
  const [autoScroll, setAutoScroll] = useState(true);
17
  const [searchQuery, setSearchQuery] = useState('');
18
  const [, forceUpdate] = useState({});
19
+ const logsContainerRef = useRef<HTMLDivElement>(null);
20
+ const [isScrolledToBottom, setIsScrolledToBottom] = useState(true);
21
 
22
  const filteredLogs = useMemo(() => {
23
+ const allLogs = Object.values(logs);
24
+ const filtered = allLogs.filter((log) => {
25
  const matchesLevel = !logLevel || log.level === logLevel || logLevel === 'all';
26
  const matchesSearch =
27
  !searchQuery ||
 
30
 
31
  return matchesLevel && matchesSearch;
32
  });
33
+
34
+ return filtered.reverse();
35
+ }, [logs, logLevel, searchQuery]);
36
 
37
  // Effect to initialize showLogs
38
  useEffect(() => {
 
44
  logStore.logSystem('Application initialized', {
45
  version: process.env.NEXT_PUBLIC_APP_VERSION,
46
  environment: process.env.NODE_ENV,
47
+ timestamp: new Date().toISOString(),
48
+ userAgent: navigator.userAgent,
49
  });
50
 
51
  // Debug logs for system state
52
  logStore.logDebug('System configuration loaded', {
53
  runtime: 'Next.js',
54
+ features: ['AI Chat', 'Event Logging', 'Provider Management', 'Theme Support'],
55
+ locale: navigator.language,
56
+ timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
57
+ });
58
+
59
+ // Performance metrics
60
+ logStore.logSystem('Performance metrics', {
61
+ deviceMemory: (navigator as any).deviceMemory || 'unknown',
62
+ hardwareConcurrency: navigator.hardwareConcurrency,
63
+ connectionType: (navigator as any).connection?.effectiveType || 'unknown',
64
+ });
65
+
66
+ // Provider status
67
+ logStore.logProvider('Provider status check', {
68
+ availableProviders: ['OpenAI', 'Anthropic', 'Mistral', 'Ollama'],
69
+ defaultProvider: 'OpenAI',
70
+ status: 'operational',
71
+ });
72
+
73
+ // Theme and accessibility
74
+ logStore.logSystem('User preferences loaded', {
75
+ theme: document.documentElement.dataset.theme || 'system',
76
+ prefersReducedMotion: window.matchMedia('(prefers-reduced-motion: reduce)').matches,
77
+ prefersDarkMode: window.matchMedia('(prefers-color-scheme: dark)').matches,
78
  });
79
 
80
  // Warning logs for potential issues
81
  logStore.logWarning('Resource usage threshold approaching', {
82
  memoryUsage: '75%',
83
  cpuLoad: '60%',
84
+ timestamp: new Date().toISOString(),
85
+ });
86
+
87
+ // Security checks
88
+ logStore.logSystem('Security status', {
89
+ httpsEnabled: window.location.protocol === 'https:',
90
+ cookiesEnabled: navigator.cookieEnabled,
91
+ storageQuota: 'checking...',
92
  });
93
 
94
  // Error logs with detailed context
 
96
  endpoint: '/api/chat',
97
  retryCount: 3,
98
  lastAttempt: new Date().toISOString(),
99
+ statusCode: 408,
100
  });
101
+
102
+ // Debug logs for development
103
+ if (process.env.NODE_ENV === 'development') {
104
+ logStore.logDebug('Development mode active', {
105
+ debugFlags: true,
106
+ mockServices: false,
107
+ apiEndpoint: 'local',
108
+ });
109
+ }
110
+ }, []);
111
+
112
+ // Scroll handling
113
+ useEffect(() => {
114
+ const container = logsContainerRef.current;
115
+
116
+ if (!container) {
117
+ return undefined;
118
+ }
119
+
120
+ const handleScroll = () => {
121
+ const { scrollTop, scrollHeight, clientHeight } = container;
122
+ const isBottom = Math.abs(scrollHeight - clientHeight - scrollTop) < 10;
123
+ setIsScrolledToBottom(isBottom);
124
+ };
125
+
126
+ container.addEventListener('scroll', handleScroll);
127
+
128
+ const cleanup = () => {
129
+ container.removeEventListener('scroll', handleScroll);
130
+ };
131
+
132
+ return cleanup;
133
  }, []);
134
 
135
+ // Auto-scroll effect
136
  useEffect(() => {
137
+ const container = logsContainerRef.current;
138
 
139
+ if (container && (autoScroll || isScrolledToBottom)) {
140
+ container.scrollTop = 0;
141
  }
142
+ }, [filteredLogs, autoScroll, isScrolledToBottom]);
143
 
144
  const handleClearLogs = useCallback(() => {
145
  if (confirm('Are you sure you want to clear all logs?')) {
 
177
  }
178
  }, []);
179
 
180
+ const getLevelIcon = (level: LogEntry['level']): string => {
181
+ switch (level) {
182
+ case 'info':
183
+ return 'i-ph:info';
184
+ case 'warning':
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
  const getLevelColor = (level: LogEntry['level']) => {
196
  switch (level) {
197
  case 'info':
198
+ return 'text-[#1389FD] dark:text-[#1389FD]';
199
  case 'warning':
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="space-y-4">
212
+ <div className="flex flex-col space-y-4">
213
  {/* Title and Toggles Row */}
214
  <div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
215
+ <div className="flex items-center gap-2">
216
+ <div className="i-ph:list-bullets text-xl text-purple-500" />
217
+ <div>
218
+ <h3 className="text-lg font-medium text-bolt-elements-textPrimary">Event Logs</h3>
219
+ <p className="text-sm text-bolt-elements-textSecondary">Track system events and debug information</p>
220
+ </div>
221
+ </div>
222
  <div className="flex flex-wrap items-center gap-4">
223
+ <div className="flex items-center gap-2">
224
+ <div className="i-ph:eye text-bolt-elements-textSecondary" />
225
  <span className="text-sm text-bolt-elements-textSecondary whitespace-nowrap">Show Actions</span>
226
  <Switch checked={showLogs} onCheckedChange={(checked) => logStore.showLogs.set(checked)} />
227
  </div>
228
+ <div className="flex items-center gap-2">
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>
 
234
  </div>
235
 
236
  {/* Controls Row */}
237
+ <div className="flex flex-wrap items-center gap-4">
238
+ <div className="flex-1 min-w-[150px] max-w-[200px]">
239
+ <div className="relative group">
240
+ <select
241
+ value={logLevel}
242
+ onChange={(e) => setLogLevel(e.target.value as LogEntry['level'])}
243
+ className={classNames(
244
+ 'w-full pl-9 pr-3 py-2 rounded-lg',
245
+ 'bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor',
246
+ 'text-sm text-bolt-elements-textPrimary',
247
+ 'focus:outline-none focus:ring-2 focus:ring-purple-500/30',
248
+ 'group-hover:border-purple-500/30',
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
+ <div className="i-ph:magnifying-glass absolute left-3 top-1/2 -translate-y-1/2 text-bolt-elements-textSecondary group-hover:text-purple-500 transition-colors" />
278
+ </div>
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
+ <motion.div
306
+ ref={logsContainerRef}
307
+ className={classNames(
308
+ settingsStyles.card,
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
  {filteredLogs.length === 0 ? (
316
+ <div className="flex flex-col items-center justify-center h-full text-center p-8">
317
+ <motion.div
318
+ initial={{ scale: 0.8, opacity: 0 }}
319
+ animate={{ scale: 1, opacity: 1 }}
320
+ transition={{ type: 'spring', duration: 0.5 }}
321
+ className="i-ph:clipboard-text text-6xl text-bolt-elements-textSecondary mb-4"
322
+ />
323
+ <motion.p
324
+ initial={{ y: 10, opacity: 0 }}
325
+ animate={{ y: 0, opacity: 1 }}
326
+ transition={{ delay: 0.2 }}
327
+ className="text-bolt-elements-textSecondary"
328
  >
329
+ No logs found
330
+ </motion.p>
331
+ </div>
332
+ ) : (
333
+ <div className="divide-y divide-bolt-elements-borderColor">
334
+ {filteredLogs.map((log, index) => (
335
+ <motion.div
336
+ key={index}
337
+ className={classNames(
338
+ 'p-4 font-mono hover:bg-bolt-elements-background-depth-3 transition-colors duration-200',
339
+ { 'border-t border-bolt-elements-borderColor': index === 0 },
340
+ )}
341
+ initial={{ opacity: 0, y: 20 }}
342
+ animate={{ opacity: 1, y: 0 }}
343
+ transition={{ delay: index * 0.03 }}
344
+ >
345
+ <div className="flex items-start gap-3">
346
+ <div
347
+ className={classNames(
348
+ getLevelIcon(log.level),
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
  }
app/components/settings/features/FeaturesTab.tsx CHANGED
@@ -1,7 +1,22 @@
1
- import React from 'react';
2
  import { Switch } from '~/components/ui/Switch';
3
  import { PromptLibrary } from '~/lib/common/prompt-library';
4
  import { useSettings } from '~/lib/hooks/useSettings';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
  export default function FeaturesTab() {
7
  const {
@@ -20,88 +35,266 @@ export default function FeaturesTab() {
20
  contextOptimizationEnabled,
21
  } = useSettings();
22
 
 
 
 
23
  const handleToggle = (enabled: boolean) => {
24
  enableDebugMode(enabled);
25
  enableEventLogs(enabled);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  };
27
 
28
  return (
29
- <div className="p-4 bg-bolt-elements-bg-depth-2 border border-bolt-elements-borderColor rounded-lg mb-4">
30
- <div className="mb-6">
31
- <h3 className="text-lg font-medium text-bolt-elements-textPrimary mb-4">Optional Features</h3>
32
- <div className="space-y-4">
33
- <div className="flex items-center justify-between">
34
- <span className="text-bolt-elements-textPrimary">Debug Features</span>
35
- <Switch className="ml-auto" checked={debug} onCheckedChange={handleToggle} />
36
- </div>
37
- <div className="flex items-center justify-between">
38
- <div>
39
- <span className="text-bolt-elements-textPrimary">Use Main Branch</span>
40
- <p className="text-xs text-bolt-elements-textTertiary">
41
- Check for updates against the main branch instead of stable
42
- </p>
43
- </div>
44
- <Switch className="ml-auto" checked={isLatestBranch} onCheckedChange={enableLatestBranch} />
45
- </div>
46
- <div className="flex items-center justify-between">
47
- <div>
48
- <span className="text-bolt-elements-textPrimary">Auto Select Code Template</span>
49
- <p className="text-xs text-bolt-elements-textTertiary">
50
- Let Bolt select the best starter template for your project.
51
- </p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  </div>
53
- <Switch className="ml-auto" checked={autoSelectTemplate} onCheckedChange={setAutoSelectTemplate} />
54
- </div>
55
- <div className="flex items-center justify-between">
56
- <div>
57
- <span className="text-bolt-elements-textPrimary">Use Context Optimization</span>
58
- <p className="text-sm text-bolt-elements-textSecondary">
59
- redact file contents form chat and puts the latest file contents on the system prompt
60
- </p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  </div>
62
- <Switch
63
- className="ml-auto"
64
- checked={contextOptimizationEnabled}
65
- onCheckedChange={enableContextOptimization}
 
 
 
 
66
  />
67
- </div>
68
- </div>
69
- </div>
70
 
71
- <div className="mb-6 border-t border-bolt-elements-borderColor pt-4">
72
- <h3 className="text-lg font-medium text-bolt-elements-textPrimary mb-4">Experimental Features</h3>
73
- <p className="text-sm text-bolt-elements-textSecondary mb-10">
74
- Disclaimer: Experimental features may be unstable and are subject to change.
75
- </p>
76
- <div className="flex flex-col">
77
- <div className="flex items-center justify-between mb-2">
78
- <span className="text-bolt-elements-textPrimary">Experimental Providers</span>
79
- <Switch className="ml-auto" checked={isLocalModel} onCheckedChange={enableLocalModels} />
80
- </div>
81
- <p className="text-xs text-bolt-elements-textTertiary mb-4">
82
- Enable experimental providers such as Ollama, LMStudio, and OpenAILike.
83
- </p>
84
- </div>
85
- <div className="flex items-start justify-between pt-4 mb-2 gap-2">
86
- <div className="flex-1 max-w-[200px]">
87
- <span className="text-bolt-elements-textPrimary">Prompt Library</span>
88
- <p className="text-xs text-bolt-elements-textTertiary mb-4">
89
- Choose a prompt from the library to use as the system prompt.
90
- </p>
91
- </div>
92
- <select
93
- value={promptId}
94
- onChange={(e) => setPromptId(e.target.value)}
95
- className="flex-1 p-2 ml-auto rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus transition-all text-sm min-w-[100px]"
96
  >
97
- {PromptLibrary.getList().map((x) => (
98
- <option key={x.id} value={x.id}>
99
- {x.label}
100
- </option>
101
- ))}
102
- </select>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  </div>
104
- </div>
105
  </div>
106
  );
107
  }
 
1
+ import React, { useState } from '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;
12
+ title: string;
13
+ description: string;
14
+ icon: string;
15
+ enabled: boolean;
16
+ beta?: boolean;
17
+ experimental?: boolean;
18
+ tooltip?: string;
19
+ }
20
 
21
  export default function FeaturesTab() {
22
  const {
 
35
  contextOptimizationEnabled,
36
  } = useSettings();
37
 
38
+ const [hoveredFeature, setHoveredFeature] = useState<string | null>(null);
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',
60
+ description: 'Check for updates against the main branch instead of stable',
61
+ icon: 'i-ph:git-branch',
62
+ enabled: isLatestBranch,
63
+ beta: true,
64
+ tooltip: 'Get the latest features and improvements before they are officially released',
65
+ },
66
+ {
67
+ id: 'autoTemplate',
68
+ title: 'Auto Select Code Template',
69
+ description: 'Let Bolt select the best starter template for your project',
70
+ icon: 'i-ph:magic-wand',
71
+ enabled: autoSelectTemplate,
72
+ tooltip: 'Automatically choose the most suitable template based on your project type',
73
+ },
74
+ {
75
+ id: 'contextOptimization',
76
+ title: 'Context Optimization',
77
+ description: 'Optimize chat context by redacting file contents and using system prompts',
78
+ icon: 'i-ph:arrows-in',
79
+ enabled: contextOptimizationEnabled,
80
+ tooltip: 'Improve AI responses by optimizing the context window and system prompts',
81
+ },
82
+ {
83
+ id: 'experimentalProviders',
84
+ title: 'Experimental Providers',
85
+ description: 'Enable experimental providers like Ollama, LMStudio, and OpenAILike',
86
+ icon: 'i-ph:robot',
87
+ enabled: isLocalModel,
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'}`);
101
+ break;
102
+ case 'autoTemplate':
103
+ setAutoSelectTemplate(enabled);
104
+ toast.success(`Auto template selection ${enabled ? 'enabled' : 'disabled'}`);
105
+ break;
106
+ case 'contextOptimization':
107
+ enableContextOptimization(enabled);
108
+ toast.success(`Context optimization ${enabled ? 'enabled' : 'disabled'}`);
109
+ break;
110
+ case 'experimentalProviders':
111
+ enableLocalModels(enabled);
112
+ toast.success(`Experimental providers ${enabled ? 'enabled' : 'disabled'}`);
113
+ break;
114
+ }
115
  };
116
 
117
  return (
118
+ <div className="space-y-6">
119
+ <motion.div
120
+ className="flex items-center gap-2"
121
+ initial={{ opacity: 0, y: -20 }}
122
+ animate={{ opacity: 1, y: 0 }}
123
+ transition={{ duration: 0.3 }}
124
+ >
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">Customize your Bolt experience</p>
129
+ </div>
130
+ </motion.div>
131
+
132
+ <motion.div
133
+ className="grid grid-cols-1 md:grid-cols-2 gap-4"
134
+ initial={{ opacity: 0, y: 20 }}
135
+ animate={{ opacity: 1, y: 0 }}
136
+ transition={{ duration: 0.3 }}
137
+ >
138
+ {features.map((feature, index) => (
139
+ <motion.div
140
+ key={feature.id}
141
+ className={classNames(
142
+ settingsStyles.card,
143
+ 'bg-bolt-elements-background-depth-2',
144
+ 'hover:bg-bolt-elements-background-depth-3',
145
+ 'transition-colors duration-200',
146
+ 'relative overflow-hidden group cursor-pointer',
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
178
+ className="px-2 py-0.5 text-xs rounded-full bg-blue-500/10 text-blue-500 font-medium"
179
+ whileHover={{ scale: 1.05 }}
180
+ whileTap={{ scale: 0.95 }}
181
+ >
182
+ Beta
183
+ </motion.span>
184
+ )}
185
+ {feature.experimental && (
186
+ <motion.span
187
+ className="px-2 py-0.5 text-xs rounded-full bg-purple-500/10 text-purple-500 font-medium"
188
+ whileHover={{ scale: 1.05 }}
189
+ whileTap={{ scale: 0.95 }}
190
+ >
191
+ Experimental
192
+ </motion.span>
193
+ )}
194
  </div>
195
+
196
+ <div className="flex items-start gap-4 p-4">
197
+ <motion.div
198
+ className={classNames(
199
+ 'p-2 rounded-lg text-xl',
200
+ 'bg-bolt-elements-background-depth-3 group-hover:bg-bolt-elements-background-depth-4',
201
+ 'transition-colors duration-200',
202
+ )}
203
+ whileHover={{ scale: 1.1 }}
204
+ whileTap={{ scale: 0.9 }}
205
+ >
206
+ <div className={classNames(feature.icon, 'text-purple-500')} />
207
+ </motion.div>
208
+
209
+ <div className="flex-1 min-w-0">
210
+ <div className="flex items-center justify-between gap-4">
211
+ <div>
212
+ <h4 className="text-sm font-medium text-bolt-elements-textPrimary group-hover:text-purple-500 transition-colors">
213
+ {feature.title}
214
+ </h4>
215
+ <p className="text-xs text-bolt-elements-textSecondary mt-0.5">{feature.description}</p>
216
+ </div>
217
+ <Switch
218
+ checked={feature.enabled}
219
+ onCheckedChange={(checked) => handleToggleFeature(feature.id, checked)}
220
+ />
221
+ </div>
222
+ </div>
223
  </div>
224
+
225
+ <motion.div
226
+ className="absolute inset-0 border-2 border-purple-500/0 rounded-lg pointer-events-none"
227
+ animate={{
228
+ borderColor: feature.enabled ? 'rgba(168, 85, 247, 0.2)' : 'rgba(168, 85, 247, 0)',
229
+ scale: feature.enabled ? 1 : 0.98,
230
+ }}
231
+ transition={{ duration: 0.2 }}
232
  />
233
+ </motion.div>
234
+ ))}
235
+ </motion.div>
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 }}
246
+ animate={{ opacity: 1, y: 0 }}
247
+ transition={{ delay: 0.6 }}
248
+ whileHover={{ scale: 1.01 }}
249
+ >
250
+ <div className="flex items-start gap-4 p-4">
251
+ <motion.div
252
+ className={classNames(
253
+ 'p-2 rounded-lg text-xl',
254
+ 'bg-bolt-elements-background-depth-3 group-hover:bg-bolt-elements-background-depth-4',
255
+ 'transition-colors duration-200',
256
+ )}
257
+ whileHover={{ scale: 1.1 }}
258
+ whileTap={{ scale: 0.9 }}
 
 
 
259
  >
260
+ <div className="i-ph:book text-purple-500" />
261
+ </motion.div>
262
+
263
+ <div className="flex-1 min-w-0">
264
+ <div className="flex items-center justify-between gap-4">
265
+ <div>
266
+ <h4 className="text-sm font-medium text-bolt-elements-textPrimary group-hover:text-purple-500 transition-colors">
267
+ Prompt Library
268
+ </h4>
269
+ <p className="text-xs text-bolt-elements-textSecondary mt-0.5">
270
+ Choose a prompt from the library to use as the system prompt
271
+ </p>
272
+ </div>
273
+ <select
274
+ value={promptId}
275
+ onChange={(e) => {
276
+ setPromptId(e.target.value);
277
+ toast.success('Prompt template updated');
278
+ }}
279
+ className={classNames(
280
+ 'p-2 rounded-lg text-sm min-w-[200px]',
281
+ 'bg-bolt-elements-background-depth-3 border border-bolt-elements-borderColor',
282
+ 'text-bolt-elements-textPrimary',
283
+ 'focus:outline-none focus:ring-2 focus:ring-purple-500/30',
284
+ 'group-hover:border-purple-500/30',
285
+ 'transition-all duration-200',
286
+ )}
287
+ >
288
+ {PromptLibrary.getList().map((x) => (
289
+ <option key={x.id} value={x.id}>
290
+ {x.label}
291
+ </option>
292
+ ))}
293
+ </select>
294
+ </div>
295
+ </div>
296
  </div>
297
+ </motion.div>
298
  </div>
299
  );
300
  }
app/components/settings/profile/ProfileTab.tsx ADDED
@@ -0,0 +1,399 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useRef, useEffect } from 'react';
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
11
+ const ALLOWED_FILE_TYPES = ['image/jpeg', 'image/png', 'image/gif'];
12
+ const MIN_PASSWORD_LENGTH = 8;
13
+
14
+ 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
22
+ ? JSON.parse(saved)
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
+
58
+ if (!file) {
59
+ return;
60
+ }
61
+
62
+ if (!ALLOWED_FILE_TYPES.includes(file.type)) {
63
+ toast.error('Please upload a valid image file (JPEG, PNG, or GIF)');
64
+ return;
65
+ }
66
+
67
+ if (file.size > MAX_FILE_SIZE) {
68
+ toast.error('File size must be less than 5MB');
69
+ return;
70
+ }
71
+
72
+ setIsLoading(true);
73
+
74
+ try {
75
+ const reader = new FileReader();
76
+
77
+ reader.onloadend = () => {
78
+ setProfile((prev) => ({ ...prev, avatar: reader.result as string }));
79
+ setIsLoading(false);
80
+ };
81
+ reader.readAsDataURL(file);
82
+ } catch (error) {
83
+ console.error('Error uploading avatar:', error);
84
+ toast.error('Failed to upload avatar');
85
+ setIsLoading(false);
86
+ }
87
+ };
88
+
89
+ const handleSave = async () => {
90
+ if (!profile.name.trim()) {
91
+ toast.error('Name is required');
92
+ return;
93
+ }
94
+
95
+ if (!profile.email.trim() || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(profile.email)) {
96
+ toast.error('Please enter a valid email address');
97
+ return;
98
+ }
99
+
100
+ if (profile.password && profile.password.length < MIN_PASSWORD_LENGTH) {
101
+ toast.error(`Password must be at least ${MIN_PASSWORD_LENGTH} characters long`);
102
+ return;
103
+ }
104
+
105
+ setIsLoading(true);
106
+
107
+ try {
108
+ localStorage.setItem('bolt_user_profile', JSON.stringify(profile));
109
+ toast.success('Profile settings saved successfully');
110
+ } catch (error) {
111
+ console.error('Error saving profile:', error);
112
+ toast.error('Failed to save profile settings');
113
+ } finally {
114
+ setIsLoading(false);
115
+ }
116
+ };
117
+
118
+ return (
119
+ <div className="space-y-4">
120
+ <div className="grid grid-cols-1 gap-4">
121
+ {/* Profile Information */}
122
+ <motion.div
123
+ className="bg-white dark:bg-[#0A0A0A] rounded-lg shadow-sm dark:shadow-none"
124
+ initial={{ opacity: 0, y: 20 }}
125
+ animate={{ opacity: 1, y: 0 }}
126
+ transition={{ delay: 0.1 }}
127
+ >
128
+ <div className="flex items-center gap-2 px-4 pt-4 pb-2">
129
+ <div className="i-ph:user-circle-fill w-4 h-4 text-purple-500" />
130
+ <span className="text-sm font-medium text-bolt-elements-textPrimary">Personal Information</span>
131
+ </div>
132
+ <div className="flex items-start gap-4 p-4">
133
+ {/* Avatar */}
134
+ <div className="relative group">
135
+ <div className="w-12 h-12 rounded-lg bg-[#F5F5F5] dark:bg-[#1A1A1A] flex items-center justify-center overflow-hidden">
136
+ <AnimatePresence mode="wait">
137
+ {isLoading ? (
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
+ ref={fileInputRef}
155
+ type="file"
156
+ accept={ALLOWED_FILE_TYPES.join(',')}
157
+ onChange={handleAvatarUpload}
158
+ className="hidden"
159
+ />
160
+ </div>
161
+
162
+ {/* Profile Fields */}
163
+ <div className="flex-1 space-y-3">
164
+ <div className="relative">
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
+ <div className="relative">
184
+ <div className="absolute left-3 top-1/2 -translate-y-1/2">
185
+ <div className="i-ph:envelope-fill w-4 h-4 text-bolt-elements-textTertiary" />
186
+ </div>
187
+ <input
188
+ type="email"
189
+ value={profile.email}
190
+ onChange={(e) => setProfile((prev) => ({ ...prev, email: e.target.value }))}
191
+ placeholder="Enter your email"
192
+ className={classNames(
193
+ 'w-full px-3 py-1.5 rounded-lg text-sm',
194
+ 'pl-10',
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
+ <div>
280
+ <div className="flex items-center gap-2 mb-2">
281
+ <div className="i-ph:translate-fill w-4 h-4 text-bolt-elements-textSecondary" />
282
+ <label className="block text-sm text-bolt-elements-textSecondary">Language</label>
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
+ <div>
308
+ <div className="flex items-center gap-2 mb-2">
309
+ <div className="i-ph:bell-fill w-4 h-4 text-bolt-elements-textSecondary" />
310
+ <label className="block text-sm text-bolt-elements-textSecondary">Notifications</label>
311
+ </div>
312
+ <div className="flex items-center justify-between">
313
+ <span className="text-sm text-bolt-elements-textSecondary">
314
+ {profile.notifications ? 'Notifications are enabled' : 'Notifications are disabled'}
315
+ </span>
316
+ <Switch
317
+ checked={profile.notifications}
318
+ onCheckedChange={(checked) => setProfile((prev) => ({ ...prev, notifications: checked }))}
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.3 }}
373
+ >
374
+ <button
375
+ onClick={handleSave}
376
+ disabled={isLoading}
377
+ className={classNames(
378
+ 'px-4 py-2 rounded-lg text-sm flex items-center gap-2',
379
+ 'bg-purple-500 text-white',
380
+ 'hover:bg-purple-600',
381
+ 'disabled:opacity-50 disabled:cursor-not-allowed',
382
+ )}
383
+ >
384
+ {isLoading ? (
385
+ <>
386
+ <div className="i-ph:spinner-gap-bold animate-spin" />
387
+ Saving...
388
+ </>
389
+ ) : (
390
+ <>
391
+ <div className="i-ph:check-circle-fill" />
392
+ Save Changes
393
+ </>
394
+ )}
395
+ </button>
396
+ </motion.div>
397
+ </div>
398
+ );
399
+ }
app/components/settings/providers/OllamaModelUpdater.tsx ADDED
@@ -0,0 +1,295 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect, useState } from 'react';
2
+ import { motion } from 'framer-motion';
3
+ import { toast } from 'react-toastify';
4
+ import { classNames } from '~/utils/classNames';
5
+ import { settingsStyles } from '~/components/settings/settings.styles';
6
+ import { DialogTitle, DialogDescription } from '~/components/ui/Dialog';
7
+
8
+ interface OllamaModel {
9
+ name: string;
10
+ digest: string;
11
+ size: number;
12
+ modified_at: string;
13
+ details?: {
14
+ family: string;
15
+ parameter_size: string;
16
+ quantization_level: string;
17
+ };
18
+ status?: 'idle' | 'updating' | 'updated' | 'error' | 'checking';
19
+ error?: string;
20
+ newDigest?: string;
21
+ progress?: {
22
+ current: number;
23
+ total: number;
24
+ status: string;
25
+ };
26
+ }
27
+
28
+ interface OllamaTagResponse {
29
+ models: Array<{
30
+ name: string;
31
+ digest: string;
32
+ size: number;
33
+ modified_at: string;
34
+ details?: {
35
+ family: string;
36
+ parameter_size: string;
37
+ quantization_level: string;
38
+ };
39
+ }>;
40
+ }
41
+
42
+ interface OllamaPullResponse {
43
+ status: string;
44
+ digest?: string;
45
+ total?: number;
46
+ completed?: number;
47
+ }
48
+
49
+ export default function OllamaModelUpdater() {
50
+ const [models, setModels] = useState<OllamaModel[]>([]);
51
+ const [isLoading, setIsLoading] = useState(true);
52
+ const [isBulkUpdating, setIsBulkUpdating] = useState(false);
53
+
54
+ useEffect(() => {
55
+ fetchModels();
56
+ }, []);
57
+
58
+ const fetchModels = async () => {
59
+ try {
60
+ setIsLoading(true);
61
+
62
+ const response = await fetch('http://localhost:11434/api/tags');
63
+ const data = (await response.json()) as OllamaTagResponse;
64
+ setModels(
65
+ data.models.map((model) => ({
66
+ name: model.name,
67
+ digest: model.digest,
68
+ size: model.size,
69
+ modified_at: model.modified_at,
70
+ details: model.details,
71
+ status: 'idle' as const,
72
+ })),
73
+ );
74
+ } catch (error) {
75
+ toast.error('Failed to fetch Ollama models');
76
+ console.error('Error fetching models:', error);
77
+ } finally {
78
+ setIsLoading(false);
79
+ }
80
+ };
81
+
82
+ const updateModel = async (modelName: string): Promise<{ success: boolean; newDigest?: string }> => {
83
+ try {
84
+ const response = await fetch('http://localhost:11434/api/pull', {
85
+ method: 'POST',
86
+ headers: {
87
+ 'Content-Type': 'application/json',
88
+ },
89
+ body: JSON.stringify({ name: modelName }),
90
+ });
91
+
92
+ if (!response.ok) {
93
+ throw new Error(`Failed to update ${modelName}`);
94
+ }
95
+
96
+ const reader = response.body?.getReader();
97
+
98
+ if (!reader) {
99
+ throw new Error('No response reader available');
100
+ }
101
+
102
+ while (true) {
103
+ const { done, value } = await reader.read();
104
+
105
+ if (done) {
106
+ break;
107
+ }
108
+
109
+ const text = new TextDecoder().decode(value);
110
+ const lines = text.split('\n').filter(Boolean);
111
+
112
+ for (const line of lines) {
113
+ const data = JSON.parse(line) as OllamaPullResponse;
114
+
115
+ setModels((current) =>
116
+ current.map((m) =>
117
+ m.name === modelName
118
+ ? {
119
+ ...m,
120
+ progress: {
121
+ current: data.completed || 0,
122
+ total: data.total || 0,
123
+ status: data.status,
124
+ },
125
+ newDigest: data.digest,
126
+ }
127
+ : m,
128
+ ),
129
+ );
130
+ }
131
+ }
132
+
133
+ setModels((current) => current.map((m) => (m.name === modelName ? { ...m, status: 'checking' } : m)));
134
+
135
+ const updatedResponse = await fetch('http://localhost:11434/api/tags');
136
+ const data = (await updatedResponse.json()) as OllamaTagResponse;
137
+ const updatedModel = data.models.find((m) => m.name === modelName);
138
+
139
+ return { success: true, newDigest: updatedModel?.digest };
140
+ } catch (error) {
141
+ console.error(`Error updating ${modelName}:`, error);
142
+ return { success: false };
143
+ }
144
+ };
145
+
146
+ const handleBulkUpdate = async () => {
147
+ setIsBulkUpdating(true);
148
+
149
+ for (const model of models) {
150
+ setModels((current) => current.map((m) => (m.name === model.name ? { ...m, status: 'updating' } : m)));
151
+
152
+ const { success, newDigest } = await updateModel(model.name);
153
+
154
+ setModels((current) =>
155
+ current.map((m) =>
156
+ m.name === model.name
157
+ ? {
158
+ ...m,
159
+ status: success ? 'updated' : 'error',
160
+ error: success ? undefined : 'Update failed',
161
+ newDigest,
162
+ }
163
+ : m,
164
+ ),
165
+ );
166
+ }
167
+
168
+ setIsBulkUpdating(false);
169
+ toast.success('Bulk update completed');
170
+ };
171
+
172
+ const handleSingleUpdate = async (modelName: string) => {
173
+ setModels((current) => current.map((m) => (m.name === modelName ? { ...m, status: 'updating' } : m)));
174
+
175
+ const { success, newDigest } = await updateModel(modelName);
176
+
177
+ setModels((current) =>
178
+ current.map((m) =>
179
+ m.name === modelName
180
+ ? {
181
+ ...m,
182
+ status: success ? 'updated' : 'error',
183
+ error: success ? undefined : 'Update failed',
184
+ newDigest,
185
+ }
186
+ : m,
187
+ ),
188
+ );
189
+
190
+ if (success) {
191
+ toast.success(`Updated ${modelName}`);
192
+ } else {
193
+ toast.error(`Failed to update ${modelName}`);
194
+ }
195
+ };
196
+
197
+ if (isLoading) {
198
+ return (
199
+ <div className="flex items-center justify-center p-4">
200
+ <div className={settingsStyles['loading-spinner']} />
201
+ <span className="ml-2 text-bolt-elements-textSecondary">Loading models...</span>
202
+ </div>
203
+ );
204
+ }
205
+
206
+ return (
207
+ <div className="space-y-4">
208
+ <div className="space-y-2">
209
+ <DialogTitle>Ollama Model Manager</DialogTitle>
210
+ <DialogDescription>Update your local Ollama models to their latest versions</DialogDescription>
211
+ </div>
212
+
213
+ <div className="flex items-center justify-between">
214
+ <div className="flex items-center gap-2">
215
+ <div className="i-ph:arrows-clockwise text-purple-500" />
216
+ <span className="text-sm text-bolt-elements-textPrimary">{models.length} models available</span>
217
+ </div>
218
+ <motion.button
219
+ onClick={handleBulkUpdate}
220
+ disabled={isBulkUpdating}
221
+ className={classNames(settingsStyles.button.base, settingsStyles.button.primary)}
222
+ whileHover={{ scale: 1.02 }}
223
+ whileTap={{ scale: 0.98 }}
224
+ >
225
+ {isBulkUpdating ? (
226
+ <>
227
+ <div className={settingsStyles['loading-spinner']} />
228
+ Updating All...
229
+ </>
230
+ ) : (
231
+ <>
232
+ <div className="i-ph:arrows-clockwise" />
233
+ Update All Models
234
+ </>
235
+ )}
236
+ </motion.button>
237
+ </div>
238
+
239
+ <div className="space-y-2">
240
+ {models.map((model) => (
241
+ <div
242
+ key={model.name}
243
+ className={classNames(
244
+ 'flex items-center justify-between p-3 rounded-lg',
245
+ 'bg-[#F8F8F8] dark:bg-[#1A1A1A]',
246
+ 'border border-[#E5E5E5] dark:border-[#333333]',
247
+ )}
248
+ >
249
+ <div className="flex flex-col gap-1">
250
+ <div className="flex items-center gap-2">
251
+ <div className="i-ph:cube text-purple-500" />
252
+ <span className="text-sm text-bolt-elements-textPrimary">{model.name}</span>
253
+ {model.status === 'updating' && <div className={settingsStyles['loading-spinner']} />}
254
+ {model.status === 'updated' && <div className="i-ph:check-circle text-green-500" />}
255
+ {model.status === 'error' && <div className="i-ph:x-circle text-red-500" />}
256
+ </div>
257
+ <div className="flex items-center gap-2 text-xs text-bolt-elements-textSecondary">
258
+ <span>Version: {model.digest.substring(0, 7)}</span>
259
+ {model.status === 'updated' && model.newDigest && (
260
+ <>
261
+ <div className="i-ph:arrow-right w-3 h-3" />
262
+ <span className="text-green-500">{model.newDigest.substring(0, 7)}</span>
263
+ </>
264
+ )}
265
+ {model.progress && (
266
+ <span className="ml-2">
267
+ {model.progress.status}{' '}
268
+ {model.progress.total > 0 && (
269
+ <>({Math.round((model.progress.current / model.progress.total) * 100)}%)</>
270
+ )}
271
+ </span>
272
+ )}
273
+ {model.details && (
274
+ <span className="ml-2">
275
+ ({model.details.parameter_size}, {model.details.quantization_level})
276
+ </span>
277
+ )}
278
+ </div>
279
+ </div>
280
+ <motion.button
281
+ onClick={() => handleSingleUpdate(model.name)}
282
+ disabled={model.status === 'updating'}
283
+ className={classNames(settingsStyles.button.base, settingsStyles.button.secondary)}
284
+ whileHover={{ scale: 1.02 }}
285
+ whileTap={{ scale: 0.98 }}
286
+ >
287
+ <div className="i-ph:arrows-clockwise" />
288
+ Update
289
+ </motion.button>
290
+ </div>
291
+ ))}
292
+ </div>
293
+ </div>
294
+ );
295
+ }
app/components/settings/providers/ProvidersTab.tsx CHANGED
@@ -1,34 +1,157 @@
1
- import React, { useEffect, useState } from 'react';
2
  import { Switch } from '~/components/ui/Switch';
 
3
  import { useSettings } from '~/lib/hooks/useSettings';
4
  import { LOCAL_PROVIDERS, URL_CONFIGURABLE_PROVIDERS } from '~/lib/stores/settings';
5
  import type { IProviderConfig } from '~/types/model';
6
  import { logStore } from '~/lib/stores/logs';
7
-
8
- // Import a default fallback icon
 
 
9
  import { providerBaseUrlEnvKeys } from '~/utils/constants';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
- const DefaultIcon = '/icons/Default.svg'; // Adjust the path as necessary
 
 
 
 
 
 
 
 
 
 
 
12
 
13
  export default function ProvidersTab() {
14
  const { providers, updateProviderSettings, isLocalModel } = useSettings();
 
15
  const [filteredProviders, setFilteredProviders] = useState<IProviderConfig[]>([]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
- // Load base URLs from cookies
18
- const [searchTerm, setSearchTerm] = useState('');
19
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  useEffect(() => {
21
  let newFilteredProviders: IProviderConfig[] = Object.entries(providers).map(([key, value]) => ({
22
  ...value,
23
  name: key,
24
  }));
25
 
26
- if (searchTerm && searchTerm.length > 0) {
27
- newFilteredProviders = newFilteredProviders.filter((provider) =>
28
- provider.name.toLowerCase().includes(searchTerm.toLowerCase()),
29
- );
30
- }
31
-
32
  if (!isLocalModel) {
33
  newFilteredProviders = newFilteredProviders.filter((provider) => !LOCAL_PROVIDERS.includes(provider.name));
34
  }
@@ -40,108 +163,245 @@ export default function ProvidersTab() {
40
  const urlConfigurable = newFilteredProviders.filter((p) => URL_CONFIGURABLE_PROVIDERS.includes(p.name));
41
 
42
  setFilteredProviders([...regular, ...urlConfigurable]);
43
- }, [providers, searchTerm, isLocalModel]);
44
-
45
- const renderProviderCard = (provider: IProviderConfig) => {
46
- const envBaseUrlKey = providerBaseUrlEnvKeys[provider.name].baseUrlKey;
47
- const envBaseUrl = envBaseUrlKey ? import.meta.env[envBaseUrlKey] : undefined;
48
- const isUrlConfigurable = URL_CONFIGURABLE_PROVIDERS.includes(provider.name);
49
-
50
- return (
51
- <div
52
- key={provider.name}
53
- className="flex flex-col provider-item hover:bg-bolt-elements-bg-depth-3 p-4 rounded-lg border border-bolt-elements-borderColor"
54
- >
55
- <div className="flex items-center justify-between mb-2">
56
- <div className="flex items-center gap-2">
57
- <img
58
- src={`/icons/${provider.name}.svg`}
59
- onError={(e) => {
60
- e.currentTarget.src = DefaultIcon;
61
- }}
62
- alt={`${provider.name} icon`}
63
- className="w-6 h-6 dark:invert"
64
- />
65
- <span className="text-bolt-elements-textPrimary">{provider.name}</span>
66
- </div>
67
- <Switch
68
- className="ml-auto"
69
- checked={provider.settings.enabled}
70
- onCheckedChange={(enabled) => {
71
- updateProviderSettings(provider.name, { ...provider.settings, enabled });
72
-
73
- if (enabled) {
74
- logStore.logProvider(`Provider ${provider.name} enabled`, { provider: provider.name });
75
- } else {
76
- logStore.logProvider(`Provider ${provider.name} disabled`, { provider: provider.name });
77
- }
78
- }}
79
- />
80
- </div>
81
- {isUrlConfigurable && provider.settings.enabled && (
82
- <div className="mt-2">
83
- {envBaseUrl && (
84
- <label className="block text-xs text-bolt-elements-textSecondary text-green-300 mb-2">
85
- Set On (.env) : {envBaseUrl}
86
- </label>
87
- )}
88
- <label className="block text-sm text-bolt-elements-textSecondary mb-2">
89
- {envBaseUrl ? 'Override Base Url' : 'Base URL '}:{' '}
90
- </label>
91
- <input
92
- type="text"
93
- value={provider.settings.baseUrl || ''}
94
- onChange={(e) => {
95
- let newBaseUrl: string | undefined = e.target.value;
96
-
97
- if (newBaseUrl && newBaseUrl.trim().length === 0) {
98
- newBaseUrl = undefined;
99
- }
100
-
101
- updateProviderSettings(provider.name, { ...provider.settings, baseUrl: newBaseUrl });
102
- logStore.logProvider(`Base URL updated for ${provider.name}`, {
103
- provider: provider.name,
104
- baseUrl: newBaseUrl,
105
- });
106
- }}
107
- placeholder={`Enter ${provider.name} base URL`}
108
- className="w-full bg-white dark:bg-bolt-elements-background-depth-4 relative px-2 py-1.5 rounded-md focus:outline-none placeholder-bolt-elements-textTertiary text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary border border-bolt-elements-borderColor"
109
- />
110
- </div>
111
- )}
112
- </div>
113
- );
114
  };
115
 
116
- const regularProviders = filteredProviders.filter((p) => !URL_CONFIGURABLE_PROVIDERS.includes(p.name));
117
- const urlConfigurableProviders = filteredProviders.filter((p) => URL_CONFIGURABLE_PROVIDERS.includes(p.name));
 
 
 
 
 
 
 
 
 
 
 
 
 
118
 
119
  return (
120
- <div className="p-4">
121
- <div className="flex mb-4">
122
- <input
123
- type="text"
124
- placeholder="Search providers..."
125
- value={searchTerm}
126
- onChange={(e) => setSearchTerm(e.target.value)}
127
- className="w-full bg-white dark:bg-bolt-elements-background-depth-4 relative px-2 py-1.5 rounded-md focus:outline-none placeholder-bolt-elements-textTertiary text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary border border-bolt-elements-borderColor"
128
- />
129
- </div>
130
-
131
- {/* Regular Providers Grid */}
132
- <div className="grid grid-cols-2 gap-4 mb-8">{regularProviders.map(renderProviderCard)}</div>
133
-
134
- {/* URL Configurable Providers Section */}
135
- {urlConfigurableProviders.length > 0 && (
136
- <div className="mt-8">
137
- <h3 className="text-lg font-semibold mb-2 text-bolt-elements-textPrimary">Experimental Providers</h3>
138
- <p className="text-sm text-bolt-elements-textSecondary mb-4">
139
- These providers are experimental and allow you to run AI models locally or connect to your own
140
- infrastructure. They require additional setup but offer more flexibility.
141
- </p>
142
- <div className="space-y-4">{urlConfigurableProviders.map(renderProviderCard)}</div>
143
- </div>
144
- )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
  </div>
146
  );
147
  }
 
1
+ import React, { useEffect, useState, useMemo, useCallback } from 'react';
2
  import { Switch } from '~/components/ui/Switch';
3
+ import Separator from '~/components/ui/Separator';
4
  import { useSettings } from '~/lib/hooks/useSettings';
5
  import { LOCAL_PROVIDERS, URL_CONFIGURABLE_PROVIDERS } from '~/lib/stores/settings';
6
  import type { IProviderConfig } from '~/types/model';
7
  import { logStore } from '~/lib/stores/logs';
8
+ import { motion } from 'framer-motion';
9
+ import { classNames } from '~/utils/classNames';
10
+ import { settingsStyles } from '~/components/settings/settings.styles';
11
+ import { toast } from 'react-toastify';
12
  import { providerBaseUrlEnvKeys } from '~/utils/constants';
13
+ import { SiAmazon, SiOpenai, SiGoogle, SiHuggingface, SiPerplexity } from 'react-icons/si';
14
+ import { BsRobot, BsCloud, BsCodeSquare, BsCpu, BsBox } from 'react-icons/bs';
15
+ import { TbBrandOpenai, TbBrain, TbCloudComputing } from 'react-icons/tb';
16
+ import { BiCodeBlock, BiChip } from 'react-icons/bi';
17
+ import { FaCloud, FaBrain } from 'react-icons/fa';
18
+ import type { IconType } from 'react-icons';
19
+ import OllamaModelUpdater from './OllamaModelUpdater';
20
+ import { DialogRoot, Dialog } from '~/components/ui/Dialog';
21
+
22
+ // Add type for provider names to ensure type safety
23
+ type ProviderName =
24
+ | 'AmazonBedrock'
25
+ | 'Anthropic'
26
+ | 'Cohere'
27
+ | 'Deepseek'
28
+ | 'Google'
29
+ | 'Groq'
30
+ | 'HuggingFace'
31
+ | 'Hyperbolic'
32
+ | 'LMStudio'
33
+ | 'Mistral'
34
+ | 'Ollama'
35
+ | 'OpenAI'
36
+ | 'OpenAILike'
37
+ | 'OpenRouter'
38
+ | 'Perplexity'
39
+ | 'Together'
40
+ | 'XAI';
41
+
42
+ // Update the PROVIDER_ICONS type to use the ProviderName type
43
+ const PROVIDER_ICONS: Record<ProviderName, IconType> = {
44
+ AmazonBedrock: SiAmazon,
45
+ Anthropic: FaBrain,
46
+ Cohere: BiChip,
47
+ Deepseek: BiCodeBlock,
48
+ Google: SiGoogle,
49
+ Groq: BsCpu,
50
+ HuggingFace: SiHuggingface,
51
+ Hyperbolic: TbCloudComputing,
52
+ LMStudio: BsCodeSquare,
53
+ Mistral: TbBrain,
54
+ Ollama: BsBox,
55
+ OpenAI: SiOpenai,
56
+ OpenAILike: TbBrandOpenai,
57
+ OpenRouter: FaCloud,
58
+ Perplexity: SiPerplexity,
59
+ Together: BsCloud,
60
+ XAI: BsRobot,
61
+ };
62
+
63
+ // Update PROVIDER_DESCRIPTIONS to use the same type
64
+ const PROVIDER_DESCRIPTIONS: Partial<Record<ProviderName, string>> = {
65
+ OpenAI: 'Use GPT-4, GPT-3.5, and other OpenAI models',
66
+ Anthropic: 'Access Claude and other Anthropic models',
67
+ Ollama: 'Run open-source models locally on your machine',
68
+ LMStudio: 'Local model inference with LM Studio',
69
+ OpenAILike: 'Connect to OpenAI-compatible API endpoints',
70
+ };
71
+
72
+ // Add these types and helper functions
73
+ type ProviderCategory = 'cloud' | 'local';
74
 
75
+ interface ProviderGroup {
76
+ title: string;
77
+ description: string;
78
+ icon: string;
79
+ providers: IProviderConfig[];
80
+ }
81
+
82
+ // Add this type
83
+ interface CategoryToggleState {
84
+ cloud: boolean;
85
+ local: boolean;
86
+ }
87
 
88
  export default function ProvidersTab() {
89
  const { providers, updateProviderSettings, isLocalModel } = useSettings();
90
+ const [editingProvider, setEditingProvider] = useState<string | null>(null);
91
  const [filteredProviders, setFilteredProviders] = useState<IProviderConfig[]>([]);
92
+ const [categoryEnabled, setCategoryEnabled] = useState<CategoryToggleState>({
93
+ cloud: false,
94
+ local: false,
95
+ });
96
+ const [showOllamaUpdater, setShowOllamaUpdater] = useState(false);
97
+
98
+ // Group providers by category
99
+ const groupedProviders = useMemo(() => {
100
+ const groups: Record<ProviderCategory, ProviderGroup> = {
101
+ cloud: {
102
+ title: 'Cloud Providers',
103
+ description: 'AI models hosted on cloud platforms',
104
+ icon: 'i-ph:cloud-duotone',
105
+ providers: [],
106
+ },
107
+ local: {
108
+ title: 'Local Providers',
109
+ description: 'Run models locally on your machine',
110
+ icon: 'i-ph:desktop-duotone',
111
+ providers: [],
112
+ },
113
+ };
114
+
115
+ filteredProviders.forEach((provider) => {
116
+ const category: ProviderCategory = LOCAL_PROVIDERS.includes(provider.name) ? 'local' : 'cloud';
117
+ groups[category].providers.push(provider);
118
+ });
119
 
120
+ return groups;
121
+ }, [filteredProviders]);
122
 
123
+ // Update the toggle handler
124
+ const handleToggleCategory = useCallback(
125
+ (category: ProviderCategory, enabled: boolean) => {
126
+ setCategoryEnabled((prev) => ({ ...prev, [category]: enabled }));
127
+
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
140
+ useEffect(() => {
141
+ const newCategoryState = {
142
+ cloud: groupedProviders.cloud.providers.every((p) => p.settings.enabled),
143
+ local: groupedProviders.local.providers.every((p) => p.settings.enabled),
144
+ };
145
+ setCategoryEnabled(newCategoryState);
146
+ }, [groupedProviders]);
147
+
148
+ // Effect to filter and sort providers
149
  useEffect(() => {
150
  let newFilteredProviders: IProviderConfig[] = Object.entries(providers).map(([key, value]) => ({
151
  ...value,
152
  name: key,
153
  }));
154
 
 
 
 
 
 
 
155
  if (!isLocalModel) {
156
  newFilteredProviders = newFilteredProviders.filter((provider) => !LOCAL_PROVIDERS.includes(provider.name));
157
  }
 
163
  const urlConfigurable = newFilteredProviders.filter((p) => URL_CONFIGURABLE_PROVIDERS.includes(p.name));
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 });
173
+ toast.success(`${provider.name} enabled`);
174
+ } else {
175
+ logStore.logProvider(`Provider ${provider.name} disabled`, { provider: provider.name });
176
+ toast.success(`${provider.name} disabled`);
177
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
  };
179
 
180
+ const handleUpdateBaseUrl = (provider: IProviderConfig, baseUrl: string) => {
181
+ let newBaseUrl: string | undefined = baseUrl;
182
+
183
+ if (newBaseUrl && newBaseUrl.trim().length === 0) {
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,
191
+ });
192
+ toast.success(`${provider.name} base URL updated`);
193
+ setEditingProvider(null);
194
+ };
195
 
196
  return (
197
+ <div className="space-y-6">
198
+ {Object.entries(groupedProviders).map(([category, group]) => (
199
+ <motion.div
200
+ key={category}
201
+ className="space-y-4"
202
+ initial={{ opacity: 0, y: 20 }}
203
+ animate={{ opacity: 1, y: 0 }}
204
+ transition={{ duration: 0.3 }}
205
+ >
206
+ <div className="flex items-center justify-between gap-4 mt-8 mb-4">
207
+ <div className="flex items-center gap-2">
208
+ <div
209
+ className={classNames(
210
+ 'w-8 h-8 flex items-center justify-center rounded-lg',
211
+ 'bg-bolt-elements-background-depth-3',
212
+ 'text-purple-500',
213
+ )}
214
+ >
215
+ <div className={group.icon} />
216
+ </div>
217
+ <div>
218
+ <h4 className="text-md font-medium text-bolt-elements-textPrimary">{group.title}</h4>
219
+ <p className="text-sm text-bolt-elements-textSecondary">{group.description}</p>
220
+ </div>
221
+ </div>
222
+
223
+ <div className="flex items-center gap-2">
224
+ <span className="text-sm text-bolt-elements-textSecondary">
225
+ Enable All {category === 'cloud' ? 'Cloud' : 'Local'}
226
+ </span>
227
+ <Switch
228
+ checked={categoryEnabled[category as ProviderCategory]}
229
+ onCheckedChange={(checked) => handleToggleCategory(category as ProviderCategory, checked)}
230
+ />
231
+ </div>
232
+ </div>
233
+
234
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
235
+ {group.providers.map((provider, index) => (
236
+ <motion.div
237
+ key={provider.name}
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
+ 'relative overflow-hidden group',
244
+ 'flex flex-col',
245
+ )}
246
+ initial={{ opacity: 0, y: 20 }}
247
+ animate={{ opacity: 1, y: 0 }}
248
+ transition={{ delay: index * 0.1 }}
249
+ whileHover={{ scale: 1.02 }}
250
+ >
251
+ <div className="absolute top-0 right-0 p-2 flex gap-1">
252
+ {LOCAL_PROVIDERS.includes(provider.name) && (
253
+ <motion.span
254
+ className="px-2 py-0.5 text-xs rounded-full bg-green-500/10 text-green-500 font-medium"
255
+ whileHover={{ scale: 1.05 }}
256
+ whileTap={{ scale: 0.95 }}
257
+ >
258
+ Local
259
+ </motion.span>
260
+ )}
261
+ {URL_CONFIGURABLE_PROVIDERS.includes(provider.name) && (
262
+ <motion.span
263
+ className="px-2 py-0.5 text-xs rounded-full bg-purple-500/10 text-purple-500 font-medium"
264
+ whileHover={{ scale: 1.05 }}
265
+ whileTap={{ scale: 0.95 }}
266
+ >
267
+ Configurable
268
+ </motion.span>
269
+ )}
270
+ </div>
271
+
272
+ <div className="flex items-start gap-4 p-4">
273
+ <motion.div
274
+ className={classNames(
275
+ 'w-10 h-10 flex items-center justify-center rounded-xl',
276
+ 'bg-bolt-elements-background-depth-3 group-hover:bg-bolt-elements-background-depth-4',
277
+ 'transition-all duration-200',
278
+ provider.settings.enabled ? 'text-purple-500' : 'text-bolt-elements-textSecondary',
279
+ )}
280
+ whileHover={{ scale: 1.1 }}
281
+ whileTap={{ scale: 0.9 }}
282
+ >
283
+ <div
284
+ className={classNames('w-6 h-6', 'transition-transform duration-200', 'group-hover:rotate-12')}
285
+ >
286
+ {React.createElement(PROVIDER_ICONS[provider.name as ProviderName] || BsRobot, {
287
+ className: 'w-full h-full',
288
+ 'aria-label': `${provider.name} logo`,
289
+ })}
290
+ </div>
291
+ </motion.div>
292
+
293
+ <div className="flex-1 min-w-0">
294
+ <div className="flex items-center justify-between gap-4 mb-2">
295
+ <div>
296
+ <h4 className="text-sm font-medium text-bolt-elements-textPrimary group-hover:text-purple-500 transition-colors">
297
+ {provider.name}
298
+ </h4>
299
+ <p className="text-xs text-bolt-elements-textSecondary mt-0.5">
300
+ {PROVIDER_DESCRIPTIONS[provider.name as keyof typeof PROVIDER_DESCRIPTIONS] ||
301
+ (URL_CONFIGURABLE_PROVIDERS.includes(provider.name)
302
+ ? 'Configure custom endpoint for this provider'
303
+ : 'Standard AI provider integration')}
304
+ </p>
305
+ </div>
306
+ <Switch
307
+ checked={provider.settings.enabled}
308
+ onCheckedChange={(checked) => handleToggleProvider(provider, checked)}
309
+ />
310
+ </div>
311
+
312
+ {provider.settings.enabled && URL_CONFIGURABLE_PROVIDERS.includes(provider.name) && (
313
+ <motion.div
314
+ initial={{ opacity: 0, height: 0 }}
315
+ animate={{ opacity: 1, height: 'auto' }}
316
+ exit={{ opacity: 0, height: 0 }}
317
+ transition={{ duration: 0.2 }}
318
+ >
319
+ <div className="flex items-center gap-2 mt-4">
320
+ {editingProvider === provider.name ? (
321
+ <input
322
+ type="text"
323
+ defaultValue={provider.settings.baseUrl}
324
+ placeholder={`Enter ${provider.name} base URL`}
325
+ className={classNames(
326
+ 'flex-1 px-3 py-1.5 rounded-lg text-sm',
327
+ 'bg-bolt-elements-background-depth-3 border border-bolt-elements-borderColor',
328
+ 'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
329
+ 'focus:outline-none focus:ring-2 focus:ring-purple-500/30',
330
+ 'transition-all duration-200',
331
+ )}
332
+ onKeyDown={(e) => {
333
+ if (e.key === 'Enter') {
334
+ handleUpdateBaseUrl(provider, e.currentTarget.value);
335
+ } else if (e.key === 'Escape') {
336
+ setEditingProvider(null);
337
+ }
338
+ }}
339
+ onBlur={(e) => handleUpdateBaseUrl(provider, e.target.value)}
340
+ autoFocus
341
+ />
342
+ ) : (
343
+ <div
344
+ className="flex-1 px-3 py-1.5 rounded-lg text-sm cursor-pointer group/url"
345
+ onClick={() => setEditingProvider(provider.name)}
346
+ >
347
+ <div className="flex items-center gap-2 text-bolt-elements-textSecondary">
348
+ <div className="i-ph:link text-sm" />
349
+ <span className="group-hover/url:text-purple-500 transition-colors">
350
+ {provider.settings.baseUrl || 'Click to set base URL'}
351
+ </span>
352
+ </div>
353
+ </div>
354
+ )}
355
+ </div>
356
+
357
+ {providerBaseUrlEnvKeys[provider.name]?.baseUrlKey && (
358
+ <div className="mt-2 text-xs text-green-500">
359
+ <div className="flex items-center gap-1">
360
+ <div className="i-ph:info" />
361
+ <span>Environment URL set in .env file</span>
362
+ </div>
363
+ </div>
364
+ )}
365
+ </motion.div>
366
+ )}
367
+ </div>
368
+ </div>
369
+
370
+ <motion.div
371
+ className="absolute inset-0 border-2 border-purple-500/0 rounded-lg pointer-events-none"
372
+ animate={{
373
+ borderColor: provider.settings.enabled ? 'rgba(168, 85, 247, 0.2)' : 'rgba(168, 85, 247, 0)',
374
+ scale: provider.settings.enabled ? 1 : 0.98,
375
+ }}
376
+ transition={{ duration: 0.2 }}
377
+ />
378
+
379
+ {provider.name === 'Ollama' && provider.settings.enabled && (
380
+ <motion.button
381
+ onClick={() => setShowOllamaUpdater(true)}
382
+ className={classNames(settingsStyles.button.base, settingsStyles.button.secondary, 'ml-2')}
383
+ whileHover={{ scale: 1.02 }}
384
+ whileTap={{ scale: 0.98 }}
385
+ >
386
+ <div className="i-ph:arrows-clockwise" />
387
+ Update Models
388
+ </motion.button>
389
+ )}
390
+
391
+ <DialogRoot open={showOllamaUpdater} onOpenChange={setShowOllamaUpdater}>
392
+ <Dialog>
393
+ <div className="p-6">
394
+ <OllamaModelUpdater />
395
+ </div>
396
+ </Dialog>
397
+ </DialogRoot>
398
+ </motion.div>
399
+ ))}
400
+ </div>
401
+
402
+ {category === 'cloud' && <Separator className="my-8" />}
403
+ </motion.div>
404
+ ))}
405
  </div>
406
  );
407
  }
app/components/settings/settings.styles.ts ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { type ClassValue, clsx } from 'clsx';
2
+ import { twMerge } from 'tailwind-merge';
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
7
+
8
+ export const settingsStyles = {
9
+ // Card styles
10
+ card: 'bg-white dark:bg-[#0A0A0A] rounded-lg p-6 border border-[#E5E5E5] dark:border-[#1A1A1A]',
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-[#F5F5F5] dark:bg-[#1A1A1A] text-[#666666] dark:text-[#999999] hover:text-[#333333] dark:hover:text-white',
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',
21
+ },
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-[#F8F8F8] dark:bg-[#1A1A1A] border border-[#E5E5E5] dark:border-[#333333] text-bolt-elements-textPrimary 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-[#F8F8F8] dark:bg-[#1A1A1A] border border-[#E5E5E5] dark:border-[#333333] text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary focus:outline-none focus:ring-1 focus:ring-purple-500 transition-all',
34
+ },
35
+
36
+ 'loading-spinner': 'i-ph:spinner-gap-bold animate-spin w-4 h-4',
37
+ } as const;
app/components/settings/settings.types.ts ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ | 'features'
9
+ | 'debug'
10
+ | 'event-logs'
11
+ | 'connection'
12
+ | 'preferences';
13
+
14
+ export interface UserProfile {
15
+ name: string;
16
+ email: string;
17
+ avatar?: string;
18
+ theme: 'light' | 'dark' | 'system';
19
+ notifications: boolean;
20
+ password?: string;
21
+ bio?: string;
22
+ language: string;
23
+ timezone: string;
24
+ }
25
+
26
+ export interface SettingItem {
27
+ id: TabType;
28
+ label: string;
29
+ icon: string;
30
+ category: SettingCategory;
31
+ description?: string;
32
+ component: () => ReactNode;
33
+ badge?: string;
34
+ keywords?: string[];
35
+ }
36
+
37
+ export const categoryLabels: Record<SettingCategory, string> = {
38
+ profile: 'Profile & Account',
39
+ file_sharing: 'File Sharing',
40
+ connectivity: 'Connectivity',
41
+ system: 'System',
42
+ services: 'Services',
43
+ preferences: 'Preferences',
44
+ };
45
+
46
+ export const categoryIcons: Record<SettingCategory, string> = {
47
+ profile: 'i-ph:user-circle',
48
+ file_sharing: 'i-ph:folder-simple',
49
+ connectivity: 'i-ph:wifi-high',
50
+ system: 'i-ph:gear',
51
+ services: 'i-ph:cube',
52
+ preferences: 'i-ph:sliders',
53
+ };
app/components/ui/Dialog.tsx CHANGED
@@ -7,60 +7,23 @@ import { IconButton } from './IconButton';
7
 
8
  export { Close as DialogClose, Root as DialogRoot } from '@radix-ui/react-dialog';
9
 
10
- const transition = {
11
- duration: 0.15,
12
- ease: cubicEasingFn,
13
- };
14
-
15
- export const dialogBackdropVariants = {
16
- closed: {
17
- opacity: 0,
18
- transition,
19
- },
20
- open: {
21
- opacity: 1,
22
- transition,
23
- },
24
- } satisfies Variants;
25
-
26
- export const dialogVariants = {
27
- closed: {
28
- x: '-50%',
29
- y: '-40%',
30
- scale: 0.96,
31
- opacity: 0,
32
- transition,
33
- },
34
- open: {
35
- x: '-50%',
36
- y: '-50%',
37
- scale: 1,
38
- opacity: 1,
39
- transition,
40
- },
41
- } satisfies Variants;
42
-
43
  interface DialogButtonProps {
44
  type: 'primary' | 'secondary' | 'danger';
45
  children: ReactNode;
46
- onClick?: (event: React.UIEvent) => void;
 
47
  }
48
 
49
- export const DialogButton = memo(({ type, children, onClick }: DialogButtonProps) => {
50
  return (
51
  <button
52
- className={classNames(
53
- 'inline-flex h-[35px] items-center justify-center rounded-lg px-4 text-sm leading-none focus:outline-none',
54
- {
55
- 'bg-bolt-elements-button-primary-background text-bolt-elements-button-primary-text hover:bg-bolt-elements-button-primary-backgroundHover':
56
- type === 'primary',
57
- 'bg-bolt-elements-button-secondary-background text-bolt-elements-button-secondary-text hover:bg-bolt-elements-button-secondary-backgroundHover':
58
- type === 'secondary',
59
- 'bg-bolt-elements-button-danger-background text-bolt-elements-button-danger-text hover:bg-bolt-elements-button-danger-backgroundHover':
60
- type === 'danger',
61
- },
62
- )}
63
  onClick={onClick}
 
64
  >
65
  {children}
66
  </button>
@@ -70,10 +33,7 @@ export const DialogButton = memo(({ type, children, onClick }: DialogButtonProps
70
  export const DialogTitle = memo(({ className, children, ...props }: RadixDialog.DialogTitleProps) => {
71
  return (
72
  <RadixDialog.Title
73
- className={classNames(
74
- 'px-5 py-4 flex items-center justify-between border-b border-bolt-elements-borderColor text-lg font-semibold leading-6 text-bolt-elements-textPrimary',
75
- className,
76
- )}
77
  {...props}
78
  >
79
  {children}
@@ -84,7 +44,7 @@ export const DialogTitle = memo(({ className, children, ...props }: RadixDialog.
84
  export const DialogDescription = memo(({ className, children, ...props }: RadixDialog.DialogDescriptionProps) => {
85
  return (
86
  <RadixDialog.Description
87
- className={classNames('px-5 py-4 text-bolt-elements-textPrimary text-md', className)}
88
  {...props}
89
  >
90
  {children}
@@ -92,29 +52,72 @@ export const DialogDescription = memo(({ className, children, ...props }: RadixD
92
  );
93
  });
94
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  interface DialogProps {
96
- children: ReactNode | ReactNode[];
97
  className?: string;
98
- onBackdrop?: (event: React.UIEvent) => void;
99
- onClose?: (event: React.UIEvent) => void;
 
100
  }
101
 
102
- export const Dialog = memo(({ className, children, onBackdrop, onClose }: DialogProps) => {
103
  return (
104
  <RadixDialog.Portal>
105
- <RadixDialog.Overlay onClick={onBackdrop} asChild>
106
  <motion.div
107
- className="bg-black/50 fixed inset-0 z-max"
 
 
 
 
108
  initial="closed"
109
  animate="open"
110
  exit="closed"
111
  variants={dialogBackdropVariants}
 
112
  />
113
  </RadixDialog.Overlay>
114
  <RadixDialog.Content asChild>
115
  <motion.div
116
  className={classNames(
117
- 'fixed top-[50%] left-[50%] z-max max-h-[85vh] w-[90vw] max-w-[450px] translate-x-[-50%] translate-y-[-50%] border border-bolt-elements-borderColor rounded-lg bg-bolt-elements-background-depth-2 shadow-lg focus:outline-none overflow-hidden',
 
 
 
 
118
  className,
119
  )}
120
  initial="closed"
@@ -122,10 +125,17 @@ export const Dialog = memo(({ className, children, onBackdrop, onClose }: Dialog
122
  exit="closed"
123
  variants={dialogVariants}
124
  >
125
- {children}
126
- <RadixDialog.Close asChild onClick={onClose}>
127
- <IconButton icon="i-ph:x" className="absolute top-[10px] right-[10px]" />
128
- </RadixDialog.Close>
 
 
 
 
 
 
 
129
  </motion.div>
130
  </RadixDialog.Content>
131
  </RadixDialog.Portal>
 
7
 
8
  export { Close as DialogClose, Root as DialogRoot } from '@radix-ui/react-dialog';
9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  interface DialogButtonProps {
11
  type: 'primary' | 'secondary' | 'danger';
12
  children: ReactNode;
13
+ onClick?: (event: React.MouseEvent) => void;
14
+ disabled?: boolean;
15
  }
16
 
17
+ export const DialogButton = memo(({ type, children, onClick, disabled }: DialogButtonProps) => {
18
  return (
19
  <button
20
+ className={classNames('inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm', {
21
+ 'bg-purple-500 text-white hover:bg-purple-600': type === 'primary',
22
+ 'text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary': type === 'secondary',
23
+ 'text-red-500 hover:bg-red-50 dark:hover:bg-red-500/10': type === 'danger',
24
+ })}
 
 
 
 
 
 
25
  onClick={onClick}
26
+ disabled={disabled}
27
  >
28
  {children}
29
  </button>
 
33
  export const DialogTitle = memo(({ className, children, ...props }: RadixDialog.DialogTitleProps) => {
34
  return (
35
  <RadixDialog.Title
36
+ className={classNames('text-lg font-medium text-bolt-elements-textPrimary', 'flex items-center gap-2', className)}
 
 
 
37
  {...props}
38
  >
39
  {children}
 
44
  export const DialogDescription = memo(({ className, children, ...props }: RadixDialog.DialogDescriptionProps) => {
45
  return (
46
  <RadixDialog.Description
47
+ className={classNames('text-sm text-bolt-elements-textSecondary', 'mt-1', className)}
48
  {...props}
49
  >
50
  {children}
 
52
  );
53
  });
54
 
55
+ const transition = {
56
+ duration: 0.15,
57
+ ease: cubicEasingFn,
58
+ };
59
+
60
+ export const dialogBackdropVariants = {
61
+ closed: {
62
+ opacity: 0,
63
+ transition,
64
+ },
65
+ open: {
66
+ opacity: 1,
67
+ transition,
68
+ },
69
+ } satisfies Variants;
70
+
71
+ export const dialogVariants = {
72
+ closed: {
73
+ x: '-50%',
74
+ y: '-40%',
75
+ scale: 0.96,
76
+ opacity: 0,
77
+ transition,
78
+ },
79
+ open: {
80
+ x: '-50%',
81
+ y: '-50%',
82
+ scale: 1,
83
+ opacity: 1,
84
+ transition,
85
+ },
86
+ } satisfies Variants;
87
+
88
  interface DialogProps {
89
+ children: ReactNode;
90
  className?: string;
91
+ showCloseButton?: boolean;
92
+ onClose?: () => void;
93
+ onBackdrop?: () => void;
94
  }
95
 
96
+ export const Dialog = memo(({ children, className, showCloseButton = true, onClose, onBackdrop }: DialogProps) => {
97
  return (
98
  <RadixDialog.Portal>
99
+ <RadixDialog.Overlay asChild>
100
  <motion.div
101
+ className={classNames(
102
+ 'fixed inset-0 z-[9999]',
103
+ 'bg-[#FAFAFA]/80 dark:bg-[#0A0A0A]/80',
104
+ 'backdrop-blur-[2px]',
105
+ )}
106
  initial="closed"
107
  animate="open"
108
  exit="closed"
109
  variants={dialogBackdropVariants}
110
+ onClick={onBackdrop}
111
  />
112
  </RadixDialog.Overlay>
113
  <RadixDialog.Content asChild>
114
  <motion.div
115
  className={classNames(
116
+ 'fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2',
117
+ 'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
118
+ 'rounded-lg shadow-lg',
119
+ 'border border-[#E5E5E5] dark:border-[#1A1A1A]',
120
+ 'z-[9999] w-[520px]',
121
  className,
122
  )}
123
  initial="closed"
 
125
  exit="closed"
126
  variants={dialogVariants}
127
  >
128
+ <div className="flex flex-col">
129
+ {children}
130
+ {showCloseButton && (
131
+ <RadixDialog.Close asChild onClick={onClose}>
132
+ <IconButton
133
+ icon="i-ph:x"
134
+ className="absolute top-3 right-3 text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary"
135
+ />
136
+ </RadixDialog.Close>
137
+ )}
138
+ </div>
139
  </motion.div>
140
  </RadixDialog.Content>
141
  </RadixDialog.Portal>
app/components/ui/Separator.tsx ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as SeparatorPrimitive from '@radix-ui/react-separator';
2
+ import { classNames } from '~/utils/classNames';
3
+
4
+ interface SeparatorProps {
5
+ className?: string;
6
+ orientation?: 'horizontal' | 'vertical';
7
+ }
8
+
9
+ export const Separator = ({ className, orientation = 'horizontal' }: SeparatorProps) => {
10
+ return (
11
+ <SeparatorPrimitive.Root
12
+ className={classNames(
13
+ 'bg-bolt-elements-borderColor',
14
+ orientation === 'horizontal' ? 'h-px w-full' : 'h-full w-px',
15
+ className,
16
+ )}
17
+ orientation={orientation}
18
+ />
19
+ );
20
+ };
21
+
22
+ export default Separator;
app/lib/stores/logs.ts CHANGED
@@ -24,6 +24,11 @@ class LogStore {
24
  this._loadLogs();
25
  }
26
 
 
 
 
 
 
27
  private _loadLogs() {
28
  const savedLogs = Cookies.get('eventLogs');
29
 
 
24
  this._loadLogs();
25
  }
26
 
27
+ // Expose the logs store for subscription
28
+ get logs() {
29
+ return this._logs;
30
+ }
31
+
32
  private _loadLogs() {
33
  const savedLogs = Cookies.get('eventLogs');
34
 
package.json CHANGED
@@ -30,12 +30,12 @@
30
  "node": ">=18.18.0"
31
  },
32
  "dependencies": {
 
33
  "@ai-sdk/anthropic": "^0.0.39",
34
  "@ai-sdk/cohere": "^1.0.3",
35
  "@ai-sdk/google": "^0.0.52",
36
  "@ai-sdk/mistral": "^0.0.43",
37
  "@ai-sdk/openai": "^0.0.66",
38
- "@ai-sdk/amazon-bedrock": "1.0.6",
39
  "@codemirror/autocomplete": "^6.18.3",
40
  "@codemirror/commands": "^6.7.1",
41
  "@codemirror/lang-cpp": "^6.0.2",
@@ -52,7 +52,7 @@
52
  "@codemirror/search": "^6.5.8",
53
  "@codemirror/state": "^6.4.1",
54
  "@codemirror/view": "^6.35.0",
55
- "@iconify-json/ph": "^1.2.1",
56
  "@iconify-json/svg-spinners": "^1.2.1",
57
  "@lezer/highlight": "^1.2.1",
58
  "@nanostores/react": "^0.7.3",
@@ -76,6 +76,7 @@
76
  "@xterm/xterm": "^5.5.0",
77
  "ai": "^4.0.13",
78
  "chalk": "^5.4.1",
 
79
  "date-fns": "^3.6.0",
80
  "diff": "^5.2.0",
81
  "dotenv": "^16.4.7",
@@ -93,6 +94,7 @@
93
  "react": "^18.3.1",
94
  "react-dom": "^18.3.1",
95
  "react-hotkeys-hook": "^4.6.1",
 
96
  "react-markdown": "^9.0.1",
97
  "react-resizable-panels": "^2.1.7",
98
  "react-toastify": "^10.0.6",
@@ -102,11 +104,14 @@
102
  "remix-island": "^0.2.0",
103
  "remix-utils": "^7.7.0",
104
  "shiki": "^1.24.0",
 
105
  "unist-util-visit": "^5.0.0"
106
  },
107
  "devDependencies": {
108
  "@blitz/eslint-plugin": "0.1.0",
109
  "@cloudflare/workers-types": "^4.20241127.0",
 
 
110
  "@remix-run/dev": "^2.15.0",
111
  "@types/diff": "^5.2.3",
112
  "@types/dom-speech-recognition": "^0.0.4",
 
30
  "node": ">=18.18.0"
31
  },
32
  "dependencies": {
33
+ "@ai-sdk/amazon-bedrock": "1.0.6",
34
  "@ai-sdk/anthropic": "^0.0.39",
35
  "@ai-sdk/cohere": "^1.0.3",
36
  "@ai-sdk/google": "^0.0.52",
37
  "@ai-sdk/mistral": "^0.0.43",
38
  "@ai-sdk/openai": "^0.0.66",
 
39
  "@codemirror/autocomplete": "^6.18.3",
40
  "@codemirror/commands": "^6.7.1",
41
  "@codemirror/lang-cpp": "^6.0.2",
 
52
  "@codemirror/search": "^6.5.8",
53
  "@codemirror/state": "^6.4.1",
54
  "@codemirror/view": "^6.35.0",
55
+ "@headlessui/react": "^2.2.0",
56
  "@iconify-json/svg-spinners": "^1.2.1",
57
  "@lezer/highlight": "^1.2.1",
58
  "@nanostores/react": "^0.7.3",
 
76
  "@xterm/xterm": "^5.5.0",
77
  "ai": "^4.0.13",
78
  "chalk": "^5.4.1",
79
+ "clsx": "^2.1.1",
80
  "date-fns": "^3.6.0",
81
  "diff": "^5.2.0",
82
  "dotenv": "^16.4.7",
 
94
  "react": "^18.3.1",
95
  "react-dom": "^18.3.1",
96
  "react-hotkeys-hook": "^4.6.1",
97
+ "react-icons": "^5.4.0",
98
  "react-markdown": "^9.0.1",
99
  "react-resizable-panels": "^2.1.7",
100
  "react-toastify": "^10.0.6",
 
104
  "remix-island": "^0.2.0",
105
  "remix-utils": "^7.7.0",
106
  "shiki": "^1.24.0",
107
+ "tailwind-merge": "^2.6.0",
108
  "unist-util-visit": "^5.0.0"
109
  },
110
  "devDependencies": {
111
  "@blitz/eslint-plugin": "0.1.0",
112
  "@cloudflare/workers-types": "^4.20241127.0",
113
+ "@iconify-json/ph": "^1.2.1",
114
+ "@iconify/types": "^2.0.0",
115
  "@remix-run/dev": "^2.15.0",
116
  "@types/diff": "^5.2.3",
117
  "@types/dom-speech-recognition": "^0.0.4",
pnpm-lock.yaml CHANGED
@@ -77,9 +77,9 @@ importers:
77
  '@codemirror/view':
78
  specifier: ^6.35.0
79
  version: 6.35.0
80
- '@iconify-json/ph':
81
- specifier: ^1.2.1
82
- version: 1.2.1
83
  '@iconify-json/svg-spinners':
84
  specifier: ^1.2.1
85
  version: 1.2.1
@@ -149,6 +149,9 @@ importers:
149
  chalk:
150
  specifier: ^5.4.1
151
  version: 5.4.1
 
 
 
152
  date-fns:
153
  specifier: ^3.6.0
154
  version: 3.6.0
@@ -200,6 +203,9 @@ importers:
200
  react-hotkeys-hook:
201
  specifier: ^4.6.1
202
 
 
 
203
  react-markdown:
204
  specifier: ^9.0.1
205
  version: 9.0.1(@types/[email protected])([email protected])
@@ -227,6 +233,9 @@ importers:
227
  shiki:
228
  specifier: ^1.24.0
229
  version: 1.24.0
 
 
 
230
  unist-util-visit:
231
  specifier: ^5.0.0
232
  version: 5.0.0
@@ -237,6 +246,12 @@ importers:
237
  '@cloudflare/workers-types':
238
  specifier: ^4.20241127.0
239
  version: 4.20241127.0
 
 
 
 
 
 
240
  '@remix-run/dev':
241
  specifier: ^2.15.0
242
@@ -1437,9 +1452,22 @@ packages:
1437
  react: '>=16.8.0'
1438
  react-dom: '>=16.8.0'
1439
 
 
 
 
 
 
 
1440
  '@floating-ui/[email protected]':
1441
  resolution: {integrity: sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==}
1442
 
 
 
 
 
 
 
 
1443
  '@humanfs/[email protected]':
1444
  resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
1445
  engines: {node: '>=18.18.0'}
@@ -1997,6 +2025,40 @@ packages:
1997
  '@radix-ui/[email protected]':
1998
  resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==}
1999
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2000
  '@remix-run/[email protected]':
2001
  resolution: {integrity: sha512-3FjiON0BmEH3fwGdmP6eEf9TL5BejCt9LOMnszefDGdwY7kgXCodJNr8TAYseor6m7LlC4xgSkgkgj/YRIZTGA==}
2002
  engines: {node: '>=18.0.0'}
@@ -2398,6 +2460,18 @@ packages:
2398
  peerDependencies:
2399
  eslint: '>=8.40.0'
2400
 
 
 
 
 
 
 
 
 
 
 
 
 
2401
  '@types/[email protected]':
2402
  resolution: {integrity: sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==}
2403
 
@@ -4965,6 +5039,11 @@ packages:
4965
  react: '>=16.8.1'
4966
  react-dom: '>=16.8.1'
4967
 
 
 
 
 
 
4968
4969
  resolution: {integrity: sha512-186Gw/vF1uRkydbsOIkcGXw7aHq0sZOCRFFjGrr7b9+nVZg4UfA4enXCaxm4fUzecU38sWfrNDitGhshuU7rdg==}
4970
  peerDependencies:
@@ -5559,6 +5638,12 @@ packages:
5559
  resolution: {integrity: sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==}
5560
  engines: {node: ^14.18.0 || >=16.0.0}
5561
 
 
 
 
 
 
 
5562
5563
  resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==}
5564
 
@@ -7372,8 +7457,25 @@ snapshots:
7372
  react: 18.3.1
7373
  react-dom: 18.3.1([email protected])
7374
 
 
 
 
 
 
 
 
 
7375
  '@floating-ui/[email protected]': {}
7376
 
 
 
 
 
 
 
 
 
 
7377
  '@humanfs/[email protected]': {}
7378
 
7379
  '@humanfs/[email protected]':
@@ -7980,6 +8082,49 @@ snapshots:
7980
 
7981
  '@radix-ui/[email protected]': {}
7982
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7983
7984
  dependencies:
7985
  '@cloudflare/workers-types': 4.20241127.0
@@ -8543,6 +8688,18 @@ snapshots:
8543
  - supports-color
8544
  - typescript
8545
 
 
 
 
 
 
 
 
 
 
 
 
 
8546
  '@types/[email protected]':
8547
  dependencies:
8548
  '@types/estree': 1.0.6
@@ -11823,6 +11980,10 @@ snapshots:
11823
  react: 18.3.1
11824
  react-dom: 18.3.1([email protected])
11825
 
 
 
 
 
11826
11827
  dependencies:
11828
  '@types/hast': 3.0.4
@@ -12456,6 +12617,10 @@ snapshots:
12456
  '@pkgr/core': 0.1.1
12457
  tslib: 2.8.1
12458
 
 
 
 
 
12459
12460
  dependencies:
12461
  chownr: 1.1.4
 
77
  '@codemirror/view':
78
  specifier: ^6.35.0
79
  version: 6.35.0
80
+ '@headlessui/react':
81
+ specifier: ^2.2.0
82
83
  '@iconify-json/svg-spinners':
84
  specifier: ^1.2.1
85
  version: 1.2.1
 
149
  chalk:
150
  specifier: ^5.4.1
151
  version: 5.4.1
152
+ clsx:
153
+ specifier: ^2.1.1
154
+ version: 2.1.1
155
  date-fns:
156
  specifier: ^3.6.0
157
  version: 3.6.0
 
203
  react-hotkeys-hook:
204
  specifier: ^4.6.1
205
206
+ react-icons:
207
+ specifier: ^5.4.0
208
+ version: 5.4.0([email protected])
209
  react-markdown:
210
  specifier: ^9.0.1
211
  version: 9.0.1(@types/[email protected])([email protected])
 
233
  shiki:
234
  specifier: ^1.24.0
235
  version: 1.24.0
236
+ tailwind-merge:
237
+ specifier: ^2.6.0
238
+ version: 2.6.0
239
  unist-util-visit:
240
  specifier: ^5.0.0
241
  version: 5.0.0
 
246
  '@cloudflare/workers-types':
247
  specifier: ^4.20241127.0
248
  version: 4.20241127.0
249
+ '@iconify-json/ph':
250
+ specifier: ^1.2.1
251
+ version: 1.2.1
252
+ '@iconify/types':
253
+ specifier: ^2.0.0
254
+ version: 2.0.0
255
  '@remix-run/dev':
256
  specifier: ^2.15.0
257
 
1452
  react: '>=16.8.0'
1453
  react-dom: '>=16.8.0'
1454
 
1455
+ '@floating-ui/[email protected]':
1456
+ resolution: {integrity: sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==}
1457
+ peerDependencies:
1458
+ react: '>=16.8.0'
1459
+ react-dom: '>=16.8.0'
1460
+
1461
  '@floating-ui/[email protected]':
1462
  resolution: {integrity: sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==}
1463
 
1464
+ '@headlessui/[email protected]':
1465
+ resolution: {integrity: sha512-RzCEg+LXsuI7mHiSomsu/gBJSjpupm6A1qIZ5sWjd7JhARNlMiSA4kKfJpCKwU9tE+zMRterhhrP74PvfJrpXQ==}
1466
+ engines: {node: '>=10'}
1467
+ peerDependencies:
1468
+ react: ^18 || ^19 || ^19.0.0-rc
1469
+ react-dom: ^18 || ^19 || ^19.0.0-rc
1470
+
1471
  '@humanfs/[email protected]':
1472
  resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
1473
  engines: {node: '>=18.18.0'}
 
2025
  '@radix-ui/[email protected]':
2026
  resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==}
2027
 
2028
+ '@react-aria/[email protected]':
2029
+ resolution: {integrity: sha512-bix9Bu1Ue7RPcYmjwcjhB14BMu2qzfJ3tMQLqDc9pweJA66nOw8DThy3IfVr8Z7j2PHktOLf9kcbiZpydKHqzg==}
2030
+ peerDependencies:
2031
+ react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
2032
+ react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
2033
+
2034
+ '@react-aria/[email protected]':
2035
+ resolution: {integrity: sha512-0qR1atBIWrb7FzQ+Tmr3s8uH5mQdyRH78n0krYaG8tng9+u1JlSi8DGRSaC9ezKyNB84m7vHT207xnHXGeJ3Fg==}
2036
+ peerDependencies:
2037
+ react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
2038
+ react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
2039
+
2040
+ '@react-aria/[email protected]':
2041
+ resolution: {integrity: sha512-GQygZaGlmYjmYM+tiNBA5C6acmiDWF52Nqd40bBp0Znk4M4hP+LTmI0lpI1BuKMw45T8RIhrAsICIfKwZvi2Gg==}
2042
+ engines: {node: '>= 12'}
2043
+ peerDependencies:
2044
+ react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
2045
+
2046
+ '@react-aria/[email protected]':
2047
+ resolution: {integrity: sha512-p681OtApnKOdbeN8ITfnnYqfdHS0z7GE+4l8EXlfLnr70Rp/9xicBO6d2rU+V/B3JujDw2gPWxYKEnEeh0CGCw==}
2048
+ peerDependencies:
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:
2055
+ react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
2056
+
2057
+ '@react-types/[email protected]':
2058
+ resolution: {integrity: sha512-gvznmLhi6JPEf0bsq7SwRYTHAKKq/wcmKqFez9sRdbED+SPMUmK5omfZ6w3EwUFQHbYUa4zPBYedQ7Knv70RMw==}
2059
+ peerDependencies:
2060
+ react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
2061
+
2062
  '@remix-run/[email protected]':
2063
  resolution: {integrity: sha512-3FjiON0BmEH3fwGdmP6eEf9TL5BejCt9LOMnszefDGdwY7kgXCodJNr8TAYseor6m7LlC4xgSkgkgj/YRIZTGA==}
2064
  engines: {node: '>=18.0.0'}
 
2460
  peerDependencies:
2461
  eslint: '>=8.40.0'
2462
 
2463
+ '@swc/[email protected]':
2464
+ resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
2465
+
2466
+ '@tanstack/[email protected]':
2467
+ resolution: {integrity: sha512-OuFzMXPF4+xZgx8UzJha0AieuMihhhaWG0tCqpp6tDzlFwOmNBPYMuLOtMJ1Tr4pXLHmgjcWhG6RlknY2oNTdQ==}
2468
+ peerDependencies:
2469
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
2470
+ react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
2471
+
2472
+ '@tanstack/[email protected]':
2473
+ resolution: {integrity: sha512-vTtpNt7mKCiZ1pwU9hfKPhpdVO2sVzFQsxoVBGtOSHxlrRRzYr8iQ2TlwbAcRYCcEiZ9ECAM8kBzH0v2+VzfKw==}
2474
+
2475
  '@types/[email protected]':
2476
  resolution: {integrity: sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==}
2477
 
 
5039
  react: '>=16.8.1'
5040
  react-dom: '>=16.8.1'
5041
 
5042
5043
+ resolution: {integrity: sha512-7eltJxgVt7X64oHh6wSWNwwbKTCtMfK35hcjvJS0yxEAhPM8oUKdS3+kqaW1vicIltw+kR2unHaa12S9pPALoQ==}
5044
+ peerDependencies:
5045
+ react: '*'
5046
+
5047
5048
  resolution: {integrity: sha512-186Gw/vF1uRkydbsOIkcGXw7aHq0sZOCRFFjGrr7b9+nVZg4UfA4enXCaxm4fUzecU38sWfrNDitGhshuU7rdg==}
5049
  peerDependencies:
 
5638
  resolution: {integrity: sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==}
5639
  engines: {node: ^14.18.0 || >=16.0.0}
5640
 
5641
5642
+ resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==}
5643
+
5644
5645
+ resolution: {integrity: sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==}
5646
+
5647
5648
  resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==}
5649
 
 
7457
  react: 18.3.1
7458
  react-dom: 18.3.1([email protected])
7459
 
7460
7461
+ dependencies:
7462
+ '@floating-ui/react-dom': 2.1.2([email protected]([email protected]))([email protected])
7463
+ '@floating-ui/utils': 0.2.8
7464
+ react: 18.3.1
7465
+ react-dom: 18.3.1([email protected])
7466
+ tabbable: 6.2.0
7467
+
7468
  '@floating-ui/[email protected]': {}
7469
 
7470
7471
+ dependencies:
7472
+ '@floating-ui/react': 0.26.28([email protected]([email protected]))([email protected])
7473
+ '@react-aria/focus': 3.19.1([email protected]([email protected]))([email protected])
7474
+ '@react-aria/interactions': 3.23.0([email protected]([email protected]))([email protected])
7475
+ '@tanstack/react-virtual': 3.11.2([email protected]([email protected]))([email protected])
7476
+ react: 18.3.1
7477
+ react-dom: 18.3.1([email protected])
7478
+
7479
  '@humanfs/[email protected]': {}
7480
 
7481
  '@humanfs/[email protected]':
 
8082
 
8083
  '@radix-ui/[email protected]': {}
8084
 
8085
8086
+ dependencies:
8087
+ '@react-aria/interactions': 3.23.0([email protected]([email protected]))([email protected])
8088
+ '@react-aria/utils': 3.27.0([email protected]([email protected]))([email protected])
8089
+ '@react-types/shared': 3.27.0([email protected])
8090
+ '@swc/helpers': 0.5.15
8091
+ clsx: 2.1.1
8092
+ react: 18.3.1
8093
+ react-dom: 18.3.1([email protected])
8094
+
8095
8096
+ dependencies:
8097
+ '@react-aria/ssr': 3.9.7([email protected])
8098
+ '@react-aria/utils': 3.27.0([email protected]([email protected]))([email protected])
8099
+ '@react-types/shared': 3.27.0([email protected])
8100
+ '@swc/helpers': 0.5.15
8101
+ react: 18.3.1
8102
+ react-dom: 18.3.1([email protected])
8103
+
8104
8105
+ dependencies:
8106
+ '@swc/helpers': 0.5.15
8107
+ react: 18.3.1
8108
+
8109
8110
+ dependencies:
8111
+ '@react-aria/ssr': 3.9.7([email protected])
8112
+ '@react-stately/utils': 3.10.5([email protected])
8113
+ '@react-types/shared': 3.27.0([email protected])
8114
+ '@swc/helpers': 0.5.15
8115
+ clsx: 2.1.1
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
8122
+ react: 18.3.1
8123
+
8124
8125
+ dependencies:
8126
+ react: 18.3.1
8127
+
8128
8129
  dependencies:
8130
  '@cloudflare/workers-types': 4.20241127.0
 
8688
  - supports-color
8689
  - typescript
8690
 
8691
+ '@swc/[email protected]':
8692
+ dependencies:
8693
+ tslib: 2.8.1
8694
+
8695
8696
+ dependencies:
8697
+ '@tanstack/virtual-core': 3.11.2
8698
+ react: 18.3.1
8699
+ react-dom: 18.3.1([email protected])
8700
+
8701
+ '@tanstack/[email protected]': {}
8702
+
8703
  '@types/[email protected]':
8704
  dependencies:
8705
  '@types/estree': 1.0.6
 
11980
  react: 18.3.1
11981
  react-dom: 18.3.1([email protected])
11982
 
11983
11984
+ dependencies:
11985
+ react: 18.3.1
11986
+
11987
11988
  dependencies:
11989
  '@types/hast': 3.0.4
 
12617
  '@pkgr/core': 0.1.1
12618
  tslib: 2.8.1
12619
 
12620
12621
+
12622
12623
+
12624
12625
  dependencies:
12626
  chownr: 1.1.4
uno.config.ts CHANGED
@@ -1,23 +1,43 @@
1
  import { globSync } from 'fast-glob';
2
  import fs from 'node:fs/promises';
3
- import { basename } from 'node:path';
4
  import { defineConfig, presetIcons, presetUno, transformerDirectives } from 'unocss';
 
5
 
6
- const iconPaths = globSync('./icons/*.svg');
 
 
 
 
7
 
8
  const collectionName = 'bolt';
9
 
10
- const customIconCollection = iconPaths.reduce(
11
- (acc, iconPath) => {
12
- const [iconName] = basename(iconPath).split('.');
 
13
 
14
- acc[collectionName] ??= {};
15
- acc[collectionName][iconName] = async () => fs.readFile(iconPath, 'utf8');
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
- return acc;
18
- },
19
- {} as Record<string, Record<string, () => Promise<string>>>,
20
- );
 
21
 
22
  const BASE_COLORS = {
23
  white: '#FFFFFF',
@@ -98,9 +118,7 @@ const COLOR_PRIMITIVES = {
98
  };
99
 
100
  export default defineConfig({
101
- safelist: [
102
- ...Object.keys(customIconCollection[collectionName]||{}).map(x=>`i-bolt:${x}`)
103
- ],
104
  shortcuts: {
105
  'bolt-ease-cubic-bezier': 'ease-[cubic-bezier(0.4,0,0.2,1)]',
106
  'transition-theme': 'transition-[background-color,border-color,color] duration-150 bolt-ease-cubic-bezier',
@@ -242,9 +260,27 @@ export default defineConfig({
242
  presetIcons({
243
  warn: true,
244
  collections: {
245
- ...customIconCollection,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
246
  },
247
- unit: 'em',
248
  }),
249
  ],
250
  });
 
1
  import { globSync } from 'fast-glob';
2
  import fs from 'node:fs/promises';
3
+ import { basename, join } from 'node:path';
4
  import { defineConfig, presetIcons, presetUno, transformerDirectives } from 'unocss';
5
+ import type { IconifyJSON } from '@iconify/types';
6
 
7
+ // Debug: Log the current working directory and icon paths
8
+ console.log('CWD:', process.cwd());
9
+
10
+ const iconPaths = globSync(join(process.cwd(), 'public/icons/*.svg'));
11
+ console.log('Found icons:', iconPaths);
12
 
13
  const collectionName = 'bolt';
14
 
15
+ const customIconCollection = {
16
+ [collectionName]: iconPaths.reduce(
17
+ (acc, iconPath) => {
18
+ const [iconName] = basename(iconPath).split('.');
19
 
20
+ acc[iconName] = async () => {
21
+ try {
22
+ const content = await fs.readFile(iconPath, 'utf8');
23
+ return content
24
+ .replace(/fill="[^"]*"/g, '')
25
+ .replace(/fill='[^']*'/g, '')
26
+ .replace(/width="[^"]*"/g, '')
27
+ .replace(/height="[^"]*"/g, '')
28
+ .replace(/viewBox="[^"]*"/g, 'viewBox="0 0 24 24"')
29
+ .replace(/<svg([^>]*)>/, '<svg $1 fill="currentColor">');
30
+ } catch (error) {
31
+ console.error(`Error loading icon ${iconName}:`, error);
32
+ return '';
33
+ }
34
+ };
35
 
36
+ return acc;
37
+ },
38
+ {} as Record<string, () => Promise<string>>,
39
+ ),
40
+ };
41
 
42
  const BASE_COLORS = {
43
  white: '#FFFFFF',
 
118
  };
119
 
120
  export default defineConfig({
121
+ safelist: [...Object.keys(customIconCollection[collectionName] || {}).map((x) => `i-bolt:${x}`)],
 
 
122
  shortcuts: {
123
  'bolt-ease-cubic-bezier': 'ease-[cubic-bezier(0.4,0,0.2,1)]',
124
  'transition-theme': 'transition-[background-color,border-color,color] duration-150 bolt-ease-cubic-bezier',
 
260
  presetIcons({
261
  warn: true,
262
  collections: {
263
+ bolt: customIconCollection.bolt,
264
+ ph: async () => {
265
+ const icons = await import('@iconify-json/ph/icons.json');
266
+ return icons.default as IconifyJSON;
267
+ },
268
+ },
269
+ extraProperties: {
270
+ display: 'inline-block',
271
+ 'vertical-align': 'middle',
272
+ width: '24px',
273
+ height: '24px',
274
+ },
275
+ customizations: {
276
+ customize(props) {
277
+ return {
278
+ ...props,
279
+ width: '24px',
280
+ height: '24px',
281
+ };
282
+ },
283
  },
 
284
  }),
285
  ],
286
  });