Stijnus commited on
Commit
723c6a4
·
1 Parent(s): a94330e
app/components/settings/features/FeaturesTab.tsx CHANGED
@@ -1,12 +1,18 @@
1
- import React, { memo } from 'react';
2
  import { motion } from 'framer-motion';
3
  import { Switch } from '~/components/ui/Switch';
4
  import { useSettings } from '~/lib/hooks/useSettings';
5
  import { classNames } from '~/utils/classNames';
6
  import { toast } from 'react-toastify';
7
  import { PromptLibrary } from '~/lib/common/prompt-library';
8
- import { useStore } from '@nanostores/react';
9
- import { isEventLogsEnabled } from '~/lib/stores/settings';
 
 
 
 
 
 
10
 
11
  interface FeatureToggle {
12
  id: string;
@@ -107,21 +113,102 @@ const FeatureSection = memo(
107
  );
108
 
109
  export default function FeaturesTab() {
110
- const {
111
- setEventLogs,
112
- isLocalModel,
113
- enableLocalModels,
114
- isLatestBranch,
115
- enableLatestBranch,
116
- promptId,
117
- setPromptId,
118
- autoSelectTemplate,
119
- setAutoSelectTemplate,
120
- enableContextOptimization,
121
- contextOptimizationEnabled,
122
- } = useSettings();
123
 
124
- const eventLogs = useStore(isEventLogsEnabled);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
 
126
  const features: Record<'stable' | 'beta' | 'experimental', FeatureToggle[]> = {
127
  stable: [
@@ -130,7 +217,7 @@ export default function FeaturesTab() {
130
  title: 'Auto Select Code Template',
131
  description: 'Let Bolt select the best starter template for your project',
132
  icon: 'i-ph:magic-wand',
133
- enabled: autoSelectTemplate,
134
  tooltip: 'Automatically choose the most suitable template based on your project type',
135
  },
136
  {
@@ -138,7 +225,7 @@ export default function FeaturesTab() {
138
  title: 'Context Optimization',
139
  description: 'Optimize chat context by redacting file contents and using system prompts',
140
  icon: 'i-ph:arrows-in',
141
- enabled: contextOptimizationEnabled,
142
  tooltip: 'Improve AI responses by optimizing the context window and system prompts',
143
  },
144
  {
@@ -146,9 +233,17 @@ export default function FeaturesTab() {
146
  title: 'Event Logging',
147
  description: 'Enable detailed event logging and history',
148
  icon: 'i-ph:list-bullets',
149
- enabled: eventLogs,
150
  tooltip: 'Record detailed logs of system events and user actions',
151
  },
 
 
 
 
 
 
 
 
152
  ],
153
  beta: [
154
  {
@@ -156,7 +251,7 @@ export default function FeaturesTab() {
156
  title: 'Use Main Branch',
157
  description: 'Check for updates against the main branch instead of stable',
158
  icon: 'i-ph:git-branch',
159
- enabled: isLatestBranch,
160
  beta: true,
161
  tooltip: 'Get the latest features and improvements before they are officially released',
162
  },
@@ -167,38 +262,13 @@ export default function FeaturesTab() {
167
  title: 'Experimental Providers',
168
  description: 'Enable experimental providers like Ollama, LMStudio, and OpenAILike',
169
  icon: 'i-ph:robot',
170
- enabled: isLocalModel,
171
  experimental: true,
172
  tooltip: 'Try out new AI providers and models in development',
173
  },
174
  ],
175
  };
176
 
177
- const handleToggleFeature = (featureId: string, enabled: boolean) => {
178
- switch (featureId) {
179
- case 'latestBranch':
180
- enableLatestBranch(enabled);
181
- toast.success(`Main branch updates ${enabled ? 'enabled' : 'disabled'}`);
182
- break;
183
- case 'autoTemplate':
184
- setAutoSelectTemplate(enabled);
185
- toast.success(`Auto template selection ${enabled ? 'enabled' : 'disabled'}`);
186
- break;
187
- case 'contextOptimization':
188
- enableContextOptimization(enabled);
189
- toast.success(`Context optimization ${enabled ? 'enabled' : 'disabled'}`);
190
- break;
191
- case 'experimentalProviders':
192
- enableLocalModels(enabled);
193
- toast.success(`Experimental providers ${enabled ? 'enabled' : 'disabled'}`);
194
- break;
195
- case 'eventLogs':
196
- setEventLogs(enabled);
197
- toast.success(`Event logging ${enabled ? 'enabled' : 'disabled'}`);
198
- break;
199
- }
200
- };
201
-
202
  return (
203
  <div className="flex flex-col gap-8">
204
  <FeatureSection
@@ -262,9 +332,9 @@ export default function FeaturesTab() {
262
  </p>
263
  </div>
264
  <select
265
- value={promptId}
266
  onChange={(e) => {
267
- setPromptId(e.target.value);
268
  toast.success('Prompt template updated');
269
  }}
270
  className={classNames(
 
1
+ import React, { memo, useEffect, useState } from 'react';
2
  import { motion } from 'framer-motion';
3
  import { Switch } from '~/components/ui/Switch';
4
  import { useSettings } from '~/lib/hooks/useSettings';
5
  import { classNames } from '~/utils/classNames';
6
  import { toast } from 'react-toastify';
7
  import { PromptLibrary } from '~/lib/common/prompt-library';
8
+ import {
9
+ isEventLogsEnabled,
10
+ isLocalModelsEnabled,
11
+ latestBranchStore as latestBranchAtom,
12
+ promptStore as promptAtom,
13
+ autoSelectStarterTemplate as autoSelectTemplateAtom,
14
+ enableContextOptimizationStore as contextOptimizationAtom,
15
+ } from '~/lib/stores/settings';
16
 
17
  interface FeatureToggle {
18
  id: string;
 
113
  );
114
 
115
  export default function FeaturesTab() {
116
+ const { autoSelectTemplate, isLatestBranch, contextOptimizationEnabled, eventLogs, isLocalModel } = useSettings();
 
 
 
 
 
 
 
 
 
 
 
 
117
 
118
+ // Setup store setters
119
+ const setEventLogs = (value: boolean) => isEventLogsEnabled.set(value);
120
+ const setLocalModels = (value: boolean) => isLocalModelsEnabled.set(value);
121
+ const setLatestBranch = (value: boolean) => latestBranchAtom.set(value);
122
+ const setPromptId = (value: string) => promptAtom.set(value);
123
+ const setAutoSelectTemplate = (value: boolean) => autoSelectTemplateAtom.set(value);
124
+ const setContextOptimization = (value: boolean) => contextOptimizationAtom.set(value);
125
+
126
+ const getLocalStorageBoolean = (key: string, defaultValue: boolean): boolean => {
127
+ const value = localStorage.getItem(key);
128
+
129
+ if (value === null) {
130
+ return defaultValue;
131
+ }
132
+
133
+ try {
134
+ return JSON.parse(value);
135
+ } catch {
136
+ return defaultValue;
137
+ }
138
+ };
139
+
140
+ // Initialize state with proper type handling
141
+ const autoSelectTemplateState = getLocalStorageBoolean('autoSelectTemplate', autoSelectTemplate);
142
+ const enableLatestBranchState = getLocalStorageBoolean('enableLatestBranch', isLatestBranch);
143
+ const contextOptimizationState = getLocalStorageBoolean('contextOptimization', contextOptimizationEnabled);
144
+ const eventLogsState = getLocalStorageBoolean('eventLogs', eventLogs);
145
+ const experimentalProvidersState = getLocalStorageBoolean('experimentalProviders', isLocalModel);
146
+ const promptLibraryState = getLocalStorageBoolean('promptLibrary', false);
147
+ const promptIdState = localStorage.getItem('promptId') ?? '';
148
+
149
+ const [autoSelectTemplateLocal, setAutoSelectTemplateLocal] = useState(autoSelectTemplateState);
150
+ const [enableLatestBranchLocal, setEnableLatestBranchLocal] = useState(enableLatestBranchState);
151
+ const [contextOptimizationLocal, setContextOptimizationLocal] = useState(contextOptimizationState);
152
+ const [eventLogsLocal, setEventLogsLocal] = useState(eventLogsState);
153
+ const [experimentalProvidersLocal, setExperimentalProvidersLocal] = useState(experimentalProvidersState);
154
+ const [promptLibraryLocal, setPromptLibraryLocal] = useState(promptLibraryState);
155
+ const [promptIdLocal, setPromptIdLocal] = useState(promptIdState);
156
+
157
+ useEffect(() => {
158
+ // Update localStorage
159
+ localStorage.setItem('autoSelectTemplate', JSON.stringify(autoSelectTemplateLocal));
160
+ localStorage.setItem('enableLatestBranch', JSON.stringify(enableLatestBranchLocal));
161
+ localStorage.setItem('contextOptimization', JSON.stringify(contextOptimizationLocal));
162
+ localStorage.setItem('eventLogs', JSON.stringify(eventLogsLocal));
163
+ localStorage.setItem('experimentalProviders', JSON.stringify(experimentalProvidersLocal));
164
+ localStorage.setItem('promptLibrary', JSON.stringify(promptLibraryLocal));
165
+ localStorage.setItem('promptId', promptIdLocal);
166
+
167
+ // Update global state
168
+ setEventLogs(eventLogsLocal);
169
+ setLocalModels(experimentalProvidersLocal);
170
+ setLatestBranch(enableLatestBranchLocal);
171
+ setPromptId(promptIdLocal);
172
+ setAutoSelectTemplate(autoSelectTemplateLocal);
173
+ setContextOptimization(contextOptimizationLocal);
174
+ }, [
175
+ autoSelectTemplateLocal,
176
+ enableLatestBranchLocal,
177
+ contextOptimizationLocal,
178
+ eventLogsLocal,
179
+ experimentalProvidersLocal,
180
+ promptLibraryLocal,
181
+ promptIdLocal,
182
+ ]);
183
+
184
+ const handleToggleFeature = (featureId: string, enabled: boolean) => {
185
+ switch (featureId) {
186
+ case 'latestBranch':
187
+ setEnableLatestBranchLocal(enabled);
188
+ toast.success(`Main branch updates ${enabled ? 'enabled' : 'disabled'}`);
189
+ break;
190
+ case 'autoTemplate':
191
+ setAutoSelectTemplateLocal(enabled);
192
+ toast.success(`Auto template selection ${enabled ? 'enabled' : 'disabled'}`);
193
+ break;
194
+ case 'contextOptimization':
195
+ setContextOptimizationLocal(enabled);
196
+ toast.success(`Context optimization ${enabled ? 'enabled' : 'disabled'}`);
197
+ break;
198
+ case 'eventLogs':
199
+ setEventLogsLocal(enabled);
200
+ toast.success(`Event logging ${enabled ? 'enabled' : 'disabled'}`);
201
+ break;
202
+ case 'experimentalProviders':
203
+ setExperimentalProvidersLocal(enabled);
204
+ toast.success(`Experimental providers ${enabled ? 'enabled' : 'disabled'}`);
205
+ break;
206
+ case 'promptLibrary':
207
+ setPromptLibraryLocal(enabled);
208
+ toast.success(`Prompt Library ${enabled ? 'enabled' : 'disabled'}`);
209
+ break;
210
+ }
211
+ };
212
 
213
  const features: Record<'stable' | 'beta' | 'experimental', FeatureToggle[]> = {
214
  stable: [
 
217
  title: 'Auto Select Code Template',
218
  description: 'Let Bolt select the best starter template for your project',
219
  icon: 'i-ph:magic-wand',
220
+ enabled: autoSelectTemplateLocal,
221
  tooltip: 'Automatically choose the most suitable template based on your project type',
222
  },
223
  {
 
225
  title: 'Context Optimization',
226
  description: 'Optimize chat context by redacting file contents and using system prompts',
227
  icon: 'i-ph:arrows-in',
228
+ enabled: contextOptimizationLocal,
229
  tooltip: 'Improve AI responses by optimizing the context window and system prompts',
230
  },
231
  {
 
233
  title: 'Event Logging',
234
  description: 'Enable detailed event logging and history',
235
  icon: 'i-ph:list-bullets',
236
+ enabled: eventLogsLocal,
237
  tooltip: 'Record detailed logs of system events and user actions',
238
  },
239
+ {
240
+ id: 'promptLibrary',
241
+ title: 'Prompt Library',
242
+ description: 'Manage your prompt library settings',
243
+ icon: 'i-ph:library',
244
+ enabled: promptLibraryLocal,
245
+ tooltip: 'Enable or disable the prompt library',
246
+ },
247
  ],
248
  beta: [
249
  {
 
251
  title: 'Use Main Branch',
252
  description: 'Check for updates against the main branch instead of stable',
253
  icon: 'i-ph:git-branch',
254
+ enabled: enableLatestBranchLocal,
255
  beta: true,
256
  tooltip: 'Get the latest features and improvements before they are officially released',
257
  },
 
262
  title: 'Experimental Providers',
263
  description: 'Enable experimental providers like Ollama, LMStudio, and OpenAILike',
264
  icon: 'i-ph:robot',
265
+ enabled: experimentalProvidersLocal,
266
  experimental: true,
267
  tooltip: 'Try out new AI providers and models in development',
268
  },
269
  ],
270
  };
271
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
272
  return (
273
  <div className="flex flex-col gap-8">
274
  <FeatureSection
 
332
  </p>
333
  </div>
334
  <select
335
+ value={promptIdLocal}
336
  onChange={(e) => {
337
+ setPromptIdLocal(e.target.value);
338
  toast.success('Prompt template updated');
339
  }}
340
  className={classNames(
app/components/settings/notifications/NotificationsTab.tsx CHANGED
@@ -4,6 +4,7 @@ import { logStore } from '~/lib/stores/logs';
4
  import { useStore } from '@nanostores/react';
5
  import { formatDistanceToNow } from 'date-fns';
6
  import { classNames } from '~/utils/classNames';
 
7
 
8
  interface NotificationDetails {
9
  type?: string;
@@ -14,8 +15,10 @@ interface NotificationDetails {
14
  updateUrl?: string;
15
  }
16
 
 
 
17
  const NotificationsTab = () => {
18
- const [filter, setFilter] = useState<'all' | 'error' | 'warning' | 'update'>('all');
19
  const logs = useStore(logStore.logs);
20
 
21
  const handleClearNotifications = () => {
@@ -29,42 +32,64 @@ const NotificationsTab = () => {
29
  const filteredLogs = Object.values(logs)
30
  .filter((log) => {
31
  if (filter === 'all') {
32
- return log.level === 'error' || log.level === 'warning' || log.details?.type === 'update';
33
  }
34
 
35
  if (filter === 'update') {
36
  return log.details?.type === 'update';
37
  }
38
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  return log.level === filter;
40
  })
41
  .sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
42
 
43
- const getNotificationStyle = (log: (typeof filteredLogs)[0]) => {
44
- if (log.details?.type === 'update') {
45
  return {
46
- border: 'border-purple-200 dark:border-purple-900/50',
47
- bg: 'bg-purple-50 dark:bg-purple-900/20',
48
- icon: 'i-ph:arrow-circle-up text-purple-600 dark:text-purple-400',
49
- text: 'text-purple-900 dark:text-purple-300',
50
  };
51
  }
52
 
53
- if (log.level === 'error') {
54
- return {
55
- border: 'border-red-200 dark:border-red-900/50',
56
- bg: 'bg-red-50 dark:bg-red-900/20',
57
- icon: 'i-ph:warning-circle text-red-600 dark:text-red-400',
58
- text: 'text-red-900 dark:text-red-300',
59
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  }
61
-
62
- return {
63
- border: 'border-yellow-200 dark:border-yellow-900/50',
64
- bg: 'bg-yellow-50 dark:bg-yellow-900/20',
65
- icon: 'i-ph:warning text-yellow-600 dark:text-yellow-400',
66
- text: 'text-yellow-900 dark:text-yellow-300',
67
- };
68
  };
69
 
70
  const renderNotificationDetails = (details: NotificationDetails) => {
@@ -79,7 +104,16 @@ const NotificationsTab = () => {
79
  </div>
80
  <button
81
  onClick={() => details.updateUrl && handleUpdateAction(details.updateUrl)}
82
- className="mt-2 inline-flex items-center gap-2 rounded-md bg-purple-50 px-3 py-1.5 text-sm font-medium text-purple-600 hover:bg-purple-100 dark:bg-purple-900/20 dark:text-purple-400 dark:hover:bg-purple-900/30"
 
 
 
 
 
 
 
 
 
83
  >
84
  <span className="i-ph:git-branch text-lg" />
85
  View Changes
@@ -91,54 +125,134 @@ const NotificationsTab = () => {
91
  return details.message ? <p className="text-sm text-gray-600 dark:text-gray-400">{details.message}</p> : null;
92
  };
93
 
 
 
 
 
 
 
 
 
 
 
 
94
  return (
95
  <div className="flex h-full flex-col gap-6">
96
  <div className="flex items-center justify-between">
97
- <div className="flex items-center gap-2">
98
- <select
99
- value={filter}
100
- onChange={(e) => setFilter(e.target.value as 'all' | 'error' | 'warning' | 'update')}
101
- className="rounded-md border border-gray-300 bg-white px-3 py-1.5 text-sm shadow-sm dark:border-gray-700 dark:bg-gray-800"
102
- >
103
- <option value="all">All Notifications</option>
104
- <option value="update">Updates</option>
105
- <option value="error">Errors</option>
106
- <option value="warning">Warnings</option>
107
- </select>
108
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  <button
110
  onClick={handleClearNotifications}
111
- className="rounded-md bg-gray-50 px-3 py-1.5 text-sm font-medium text-gray-600 hover:bg-gray-100 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700"
 
 
 
 
 
 
 
 
112
  >
 
113
  Clear All
114
  </button>
115
  </div>
116
 
117
  <div className="flex flex-col gap-4">
118
  {filteredLogs.length === 0 ? (
119
- <div className="flex flex-col items-center justify-center gap-4 rounded-lg border border-gray-200 p-8 text-center dark:border-gray-700">
 
 
 
 
 
 
 
 
 
120
  <span className="i-ph:bell-slash text-4xl text-gray-400 dark:text-gray-600" />
121
  <div className="flex flex-col gap-1">
122
- <h3 className="text-sm font-medium text-gray-900 dark:text-gray-100">No Notifications</h3>
123
  <p className="text-sm text-gray-500 dark:text-gray-400">You're all caught up!</p>
124
  </div>
125
- </div>
126
  ) : (
127
  filteredLogs.map((log) => {
128
- const style = getNotificationStyle(log);
129
  return (
130
  <motion.div
131
  key={log.id}
132
  initial={{ opacity: 0, y: 20 }}
133
  animate={{ opacity: 1, y: 0 }}
134
- className={classNames('flex flex-col gap-2 rounded-lg border p-4', style.border, style.bg)}
 
 
 
 
 
 
 
135
  >
136
  <div className="flex items-start justify-between gap-4">
137
- <div className="flex items-center gap-3">
138
- <span className={classNames('text-lg', style.icon)} />
139
- <div>
140
- <h3 className={classNames('text-sm font-medium', style.text)}>{log.message}</h3>
141
  {log.details && renderNotificationDetails(log.details as NotificationDetails)}
 
 
 
 
142
  </div>
143
  </div>
144
  <time className="shrink-0 text-xs text-gray-500 dark:text-gray-400">
 
4
  import { useStore } from '@nanostores/react';
5
  import { formatDistanceToNow } from 'date-fns';
6
  import { classNames } from '~/utils/classNames';
7
+ import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
8
 
9
  interface NotificationDetails {
10
  type?: string;
 
15
  updateUrl?: string;
16
  }
17
 
18
+ type FilterType = 'all' | 'system' | 'error' | 'warning' | 'update' | 'info' | 'provider' | 'network';
19
+
20
  const NotificationsTab = () => {
21
+ const [filter, setFilter] = useState<FilterType>('all');
22
  const logs = useStore(logStore.logs);
23
 
24
  const handleClearNotifications = () => {
 
32
  const filteredLogs = Object.values(logs)
33
  .filter((log) => {
34
  if (filter === 'all') {
35
+ return true;
36
  }
37
 
38
  if (filter === 'update') {
39
  return log.details?.type === 'update';
40
  }
41
 
42
+ if (filter === 'system') {
43
+ return log.category === 'system';
44
+ }
45
+
46
+ if (filter === 'provider') {
47
+ return log.category === 'provider';
48
+ }
49
+
50
+ if (filter === 'network') {
51
+ return log.category === 'network';
52
+ }
53
+
54
  return log.level === filter;
55
  })
56
  .sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
57
 
58
+ const getNotificationStyle = (level: string, type?: string) => {
59
+ if (type === 'update') {
60
  return {
61
+ icon: 'i-ph:arrow-circle-up',
62
+ color: 'text-purple-500 dark:text-purple-400',
63
+ bg: 'hover:bg-purple-500/10 dark:hover:bg-purple-500/20',
 
64
  };
65
  }
66
 
67
+ switch (level) {
68
+ case 'error':
69
+ return {
70
+ icon: 'i-ph:warning-circle',
71
+ color: 'text-red-500 dark:text-red-400',
72
+ bg: 'hover:bg-red-500/10 dark:hover:bg-red-500/20',
73
+ };
74
+ case 'warning':
75
+ return {
76
+ icon: 'i-ph:warning',
77
+ color: 'text-yellow-500 dark:text-yellow-400',
78
+ bg: 'hover:bg-yellow-500/10 dark:hover:bg-yellow-500/20',
79
+ };
80
+ case 'info':
81
+ return {
82
+ icon: 'i-ph:info',
83
+ color: 'text-blue-500 dark:text-blue-400',
84
+ bg: 'hover:bg-blue-500/10 dark:hover:bg-blue-500/20',
85
+ };
86
+ default:
87
+ return {
88
+ icon: 'i-ph:bell',
89
+ color: 'text-gray-500 dark:text-gray-400',
90
+ bg: 'hover:bg-gray-500/10 dark:hover:bg-gray-500/20',
91
+ };
92
  }
 
 
 
 
 
 
 
93
  };
94
 
95
  const renderNotificationDetails = (details: NotificationDetails) => {
 
104
  </div>
105
  <button
106
  onClick={() => details.updateUrl && handleUpdateAction(details.updateUrl)}
107
+ className={classNames(
108
+ 'mt-2 inline-flex items-center gap-2',
109
+ 'rounded-lg px-3 py-1.5',
110
+ 'text-sm font-medium',
111
+ 'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
112
+ 'border border-[#E5E5E5] dark:border-[#1A1A1A]',
113
+ 'text-gray-900 dark:text-white',
114
+ 'hover:bg-purple-500/10 dark:hover:bg-purple-500/20',
115
+ 'transition-all duration-200',
116
+ )}
117
  >
118
  <span className="i-ph:git-branch text-lg" />
119
  View Changes
 
125
  return details.message ? <p className="text-sm text-gray-600 dark:text-gray-400">{details.message}</p> : null;
126
  };
127
 
128
+ const filterOptions: { id: FilterType; label: string; icon: string }[] = [
129
+ { id: 'all', label: 'All Notifications', icon: 'i-ph:bell' },
130
+ { id: 'system', label: 'System', icon: 'i-ph:gear' },
131
+ { id: 'update', label: 'Updates', icon: 'i-ph:arrow-circle-up' },
132
+ { id: 'error', label: 'Errors', icon: 'i-ph:warning-circle' },
133
+ { id: 'warning', label: 'Warnings', icon: 'i-ph:warning' },
134
+ { id: 'info', label: 'Information', icon: 'i-ph:info' },
135
+ { id: 'provider', label: 'Providers', icon: 'i-ph:robot' },
136
+ { id: 'network', label: 'Network', icon: 'i-ph:wifi-high' },
137
+ ];
138
+
139
  return (
140
  <div className="flex h-full flex-col gap-6">
141
  <div className="flex items-center justify-between">
142
+ <DropdownMenu.Root>
143
+ <DropdownMenu.Trigger asChild>
144
+ <button
145
+ className={classNames(
146
+ 'flex items-center gap-2',
147
+ 'rounded-lg px-3 py-1.5',
148
+ 'text-sm text-gray-900 dark:text-white',
149
+ 'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
150
+ 'border border-[#E5E5E5] dark:border-[#1A1A1A]',
151
+ 'hover:bg-purple-500/10 dark:hover:bg-purple-500/20',
152
+ 'transition-all duration-200',
153
+ )}
154
+ >
155
+ <span
156
+ className={classNames(
157
+ filterOptions.find((opt) => opt.id === filter)?.icon || 'i-ph:funnel',
158
+ 'text-lg text-gray-500 dark:text-gray-400',
159
+ )}
160
+ />
161
+ {filterOptions.find((opt) => opt.id === filter)?.label || 'Filter Notifications'}
162
+ <span className="i-ph:caret-down text-lg text-gray-500 dark:text-gray-400" />
163
+ </button>
164
+ </DropdownMenu.Trigger>
165
+
166
+ <DropdownMenu.Portal>
167
+ <DropdownMenu.Content
168
+ className="min-w-[200px] bg-white dark:bg-gray-800 rounded-lg shadow-lg py-1 z-[250] animate-in fade-in-0 zoom-in-95"
169
+ sideOffset={5}
170
+ align="start"
171
+ side="bottom"
172
+ >
173
+ {filterOptions.map((option) => (
174
+ <DropdownMenu.Item
175
+ key={option.id}
176
+ className="group flex items-center px-4 py-2.5 text-sm text-gray-700 dark:text-gray-200 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 cursor-pointer transition-colors"
177
+ onClick={() => setFilter(option.id)}
178
+ >
179
+ <div className="mr-3 flex h-5 w-5 items-center justify-center">
180
+ <div
181
+ className={classNames(
182
+ option.icon,
183
+ 'text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors',
184
+ )}
185
+ />
186
+ </div>
187
+ <span className="group-hover:text-purple-500 transition-colors">{option.label}</span>
188
+ </DropdownMenu.Item>
189
+ ))}
190
+ </DropdownMenu.Content>
191
+ </DropdownMenu.Portal>
192
+ </DropdownMenu.Root>
193
+
194
  <button
195
  onClick={handleClearNotifications}
196
+ className={classNames(
197
+ 'group flex items-center gap-2',
198
+ 'rounded-lg px-3 py-1.5',
199
+ 'text-sm text-gray-900 dark:text-white',
200
+ 'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
201
+ 'border border-[#E5E5E5] dark:border-[#1A1A1A]',
202
+ 'hover:bg-purple-500/10 dark:hover:bg-purple-500/20',
203
+ 'transition-all duration-200',
204
+ )}
205
  >
206
+ <span className="i-ph:trash text-lg text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
207
  Clear All
208
  </button>
209
  </div>
210
 
211
  <div className="flex flex-col gap-4">
212
  {filteredLogs.length === 0 ? (
213
+ <motion.div
214
+ initial={{ opacity: 0, y: 20 }}
215
+ animate={{ opacity: 1, y: 0 }}
216
+ className={classNames(
217
+ 'flex flex-col items-center justify-center gap-4',
218
+ 'rounded-lg p-8 text-center',
219
+ 'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
220
+ 'border border-[#E5E5E5] dark:border-[#1A1A1A]',
221
+ )}
222
+ >
223
  <span className="i-ph:bell-slash text-4xl text-gray-400 dark:text-gray-600" />
224
  <div className="flex flex-col gap-1">
225
+ <h3 className="text-sm font-medium text-gray-900 dark:text-white">No Notifications</h3>
226
  <p className="text-sm text-gray-500 dark:text-gray-400">You're all caught up!</p>
227
  </div>
228
+ </motion.div>
229
  ) : (
230
  filteredLogs.map((log) => {
231
+ const style = getNotificationStyle(log.level, log.details?.type);
232
  return (
233
  <motion.div
234
  key={log.id}
235
  initial={{ opacity: 0, y: 20 }}
236
  animate={{ opacity: 1, y: 0 }}
237
+ className={classNames(
238
+ 'flex flex-col gap-2',
239
+ 'rounded-lg p-4',
240
+ 'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
241
+ 'border border-[#E5E5E5] dark:border-[#1A1A1A]',
242
+ style.bg,
243
+ 'transition-all duration-200',
244
+ )}
245
  >
246
  <div className="flex items-start justify-between gap-4">
247
+ <div className="flex items-start gap-3">
248
+ <span className={classNames('text-lg', style.icon, style.color)} />
249
+ <div className="flex flex-col gap-1">
250
+ <h3 className="text-sm font-medium text-gray-900 dark:text-white">{log.message}</h3>
251
  {log.details && renderNotificationDetails(log.details as NotificationDetails)}
252
+ <p className="text-xs text-gray-500 dark:text-gray-400">
253
+ Category: {log.category}
254
+ {log.subCategory ? ` > ${log.subCategory}` : ''}
255
+ </p>
256
  </div>
257
  </div>
258
  <time className="shrink-0 text-xs text-gray-500 dark:text-gray-400">
app/lib/stores/settings.ts CHANGED
@@ -53,13 +53,19 @@ export const isDebugMode = atom(false);
53
  const savedEventLogs = Cookies.get('isEventLogsEnabled');
54
  export const isEventLogsEnabled = atom(savedEventLogs === 'true');
55
 
 
56
  export const isLocalModelsEnabled = atom(true);
57
 
 
58
  export const promptStore = atom<string>('default');
59
 
 
60
  export const latestBranchStore = atom(false);
61
 
 
62
  export const autoSelectStarterTemplate = atom(false);
 
 
63
  export const enableContextOptimizationStore = atom(false);
64
 
65
  // Initialize tab configuration from cookie or default
 
53
  const savedEventLogs = Cookies.get('isEventLogsEnabled');
54
  export const isEventLogsEnabled = atom(savedEventLogs === 'true');
55
 
56
+ // Local models settings
57
  export const isLocalModelsEnabled = atom(true);
58
 
59
+ // Prompt settings
60
  export const promptStore = atom<string>('default');
61
 
62
+ // Branch settings
63
  export const latestBranchStore = atom(false);
64
 
65
+ // Template settings
66
  export const autoSelectStarterTemplate = atom(false);
67
+
68
+ // Context optimization settings
69
  export const enableContextOptimizationStore = atom(false);
70
 
71
  // Initialize tab configuration from cookie or default
app/routes/api.system.app-info.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ActionFunctionArgs } from '@remix-run/cloudflare';
2
  import { json } from '@remix-run/cloudflare';
3
 
4
  interface PackageJson {
@@ -27,6 +27,23 @@ const packageJson = {
27
  },
28
  } as PackageJson;
29
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  export const action = async ({ request: _request }: ActionFunctionArgs) => {
31
  try {
32
  return json({
 
1
+ import type { ActionFunctionArgs, LoaderFunction } from '@remix-run/cloudflare';
2
  import { json } from '@remix-run/cloudflare';
3
 
4
  interface PackageJson {
 
27
  },
28
  } as PackageJson;
29
 
30
+ export const loader: LoaderFunction = async ({ request: _request }) => {
31
+ try {
32
+ return json({
33
+ name: packageJson.name,
34
+ version: packageJson.version,
35
+ description: packageJson.description,
36
+ license: packageJson.license,
37
+ nodeVersion: process.version,
38
+ dependencies: packageJson.dependencies,
39
+ devDependencies: packageJson.devDependencies,
40
+ });
41
+ } catch (error) {
42
+ console.error('Failed to get webapp info:', error);
43
+ return json({ error: 'Failed to get webapp information' }, { status: 500 });
44
+ }
45
+ };
46
+
47
  export const action = async ({ request: _request }: ActionFunctionArgs) => {
48
  try {
49
  return json({