Stijnus commited on
Commit
d9a380f
·
1 Parent(s): 9e8d05c

Service console check providers

Browse files
app/components/chat/StarterTemplates.tsx CHANGED
@@ -14,7 +14,7 @@ const FrameworkLink: React.FC<FrameworkLinkProps> = ({ template }) => (
14
  className="items-center justify-center "
15
  >
16
  <div
17
- className={`inline-block ${template.icon} w-8 h-8 text-4xl transition-theme opacity-25 hover:opacity-75 transition-all`}
18
  />
19
  </a>
20
  );
 
14
  className="items-center justify-center "
15
  >
16
  <div
17
+ className={`inline-block ${template.icon} w-8 h-8 text-4xl transition-theme opacity-25 hover:opacity-100 hover:text-purple-500 dark:text-white dark:opacity-50 dark:hover:opacity-100 dark:hover:text-purple-400 transition-all`}
18
  />
19
  </a>
20
  );
app/components/settings/data/DataTab.tsx CHANGED
@@ -2,7 +2,7 @@ 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);
@@ -180,11 +180,21 @@ export default function DataTab() {
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
  // Close the dialog first
189
  setShowResetInlineConfirm(false);
190
 
@@ -204,9 +214,19 @@ export default function DataTab() {
204
  setIsDeleting(true);
205
 
206
  try {
207
- // Clear chat history
208
  localStorage.removeItem('bolt_chat_history');
209
 
 
 
 
 
 
 
 
 
 
 
210
  // Close the dialog first
211
  setShowDeleteInlineConfirm(false);
212
 
 
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, deleteById } from '~/lib/persistence';
6
 
7
  export default function DataTab() {
8
  const [isDownloadingTemplate, setIsDownloadingTemplate] = useState(false);
 
180
  setIsResetting(true);
181
 
182
  try {
183
+ // Clear all stored settings from localStorage
184
  localStorage.removeItem('bolt_user_profile');
185
  localStorage.removeItem('bolt_settings');
186
  localStorage.removeItem('bolt_chat_history');
187
 
188
+ // Clear all data from IndexedDB
189
+ if (!db) {
190
+ throw new Error('Database not initialized');
191
+ }
192
+
193
+ // Get all chats and delete them
194
+ const chats = await getAll(db as IDBDatabase);
195
+ const deletePromises = chats.map((chat) => deleteById(db as IDBDatabase, chat.id));
196
+ await Promise.all(deletePromises);
197
+
198
  // Close the dialog first
199
  setShowResetInlineConfirm(false);
200
 
 
214
  setIsDeleting(true);
215
 
216
  try {
217
+ // Clear chat history from localStorage
218
  localStorage.removeItem('bolt_chat_history');
219
 
220
+ // Clear chats from IndexedDB
221
+ if (!db) {
222
+ throw new Error('Database not initialized');
223
+ }
224
+
225
+ // Get all chats and delete them one by one
226
+ const chats = await getAll(db as IDBDatabase);
227
+ const deletePromises = chats.map((chat) => deleteById(db as IDBDatabase, chat.id));
228
+ await Promise.all(deletePromises);
229
+
230
  // Close the dialog first
231
  setShowDeleteInlineConfirm(false);
232
 
app/components/settings/developer/DeveloperWindow.tsx CHANGED
@@ -48,15 +48,16 @@ const TAB_DESCRIPTIONS: Record<TabType, string> = {
48
  profile: 'Manage your profile and account settings',
49
  settings: 'Configure application preferences',
50
  notifications: 'View and manage your notifications',
51
- features: 'Explore new and upcoming features',
52
  data: 'Manage your data and storage',
53
- 'cloud-providers': 'Configure cloud AI providers and models',
54
- 'local-providers': 'Configure local AI providers and models',
55
- connection: 'Check connection status and settings',
56
- debug: 'Debug tools and system information',
57
- 'event-logs': 'View system events and logs',
58
- update: 'Check for updates and release notes',
59
- 'task-manager': 'Monitor system resources and processes',
 
60
  };
61
 
62
  const DraggableTabTile = ({
 
48
  profile: 'Manage your profile and account settings',
49
  settings: 'Configure application preferences',
50
  notifications: 'View and manage your notifications',
51
+ features: 'Manage application features',
52
  data: 'Manage your data and storage',
53
+ 'cloud-providers': 'Configure cloud AI providers',
54
+ 'local-providers': 'Configure local AI providers',
55
+ connection: 'View and manage connections',
56
+ debug: 'Debug application issues',
57
+ 'event-logs': 'View application event logs',
58
+ update: 'Check for updates',
59
+ 'task-manager': 'Manage running tasks',
60
+ 'service-status': 'View service health and status',
61
  };
62
 
63
  const DraggableTabTile = ({
app/components/settings/developer/TabManagement.tsx CHANGED
@@ -19,7 +19,8 @@ const TAB_ICONS: Record<TabType, string> = {
19
  debug: 'i-ph:bug-fill',
20
  'event-logs': 'i-ph:list-bullets-fill',
21
  update: 'i-ph:arrow-clockwise-fill',
22
- 'task-manager': 'i-ph:gauge-fill',
 
23
  };
24
 
25
  interface TabGroupProps {
 
19
  debug: 'i-ph:bug-fill',
20
  'event-logs': 'i-ph:list-bullets-fill',
21
  update: 'i-ph:arrow-clockwise-fill',
22
+ 'task-manager': 'i-ph:activity-fill',
23
+ 'service-status': 'i-ph:heartbeat-fill',
24
  };
25
 
26
  interface TabGroupProps {
app/components/settings/providers/ServiceStatusTab.tsx ADDED
@@ -0,0 +1,886 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect, useState, useCallback } from 'react';
2
+ import { motion } from 'framer-motion';
3
+ import { classNames } from '~/utils/classNames';
4
+ import { TbActivityHeartbeat } from 'react-icons/tb';
5
+ import { BsCheckCircleFill, BsXCircleFill, BsExclamationCircleFill } from 'react-icons/bs';
6
+ import { SiAmazon, SiGoogle, SiHuggingface, SiPerplexity, SiOpenai } from 'react-icons/si';
7
+ import { BsRobot, BsCloud } from 'react-icons/bs';
8
+ import { TbBrain } from 'react-icons/tb';
9
+ import { BiChip, BiCodeBlock } from 'react-icons/bi';
10
+ import { FaCloud, FaBrain } from 'react-icons/fa';
11
+ import type { IconType } from 'react-icons';
12
+ import { useSettings } from '~/lib/hooks/useSettings';
13
+ import { useToast } from '~/components/ui/use-toast';
14
+
15
+ // Types
16
+ type ProviderName =
17
+ | 'AmazonBedrock'
18
+ | 'Anthropic'
19
+ | 'Cohere'
20
+ | 'Deepseek'
21
+ | 'Google'
22
+ | 'Groq'
23
+ | 'HuggingFace'
24
+ | 'Mistral'
25
+ | 'OpenAI'
26
+ | 'OpenRouter'
27
+ | 'Perplexity'
28
+ | 'Together'
29
+ | 'XAI';
30
+
31
+ type ServiceStatus = {
32
+ provider: ProviderName;
33
+ status: 'operational' | 'degraded' | 'down';
34
+ lastChecked: string;
35
+ statusUrl?: string;
36
+ icon?: IconType;
37
+ message?: string;
38
+ responseTime?: number;
39
+ incidents?: string[];
40
+ };
41
+
42
+ type ProviderConfig = {
43
+ statusUrl: string;
44
+ apiUrl: string;
45
+ headers: Record<string, string>;
46
+ testModel: string;
47
+ };
48
+
49
+ // Types for API responses
50
+ type ApiResponse = {
51
+ error?: {
52
+ message: string;
53
+ };
54
+ message?: string;
55
+ model?: string;
56
+ models?: Array<{
57
+ id?: string;
58
+ name?: string;
59
+ }>;
60
+ data?: Array<{
61
+ id?: string;
62
+ name?: string;
63
+ }>;
64
+ };
65
+
66
+ // Constants
67
+ const PROVIDER_STATUS_URLS: Record<ProviderName, ProviderConfig> = {
68
+ OpenAI: {
69
+ statusUrl: 'https://status.openai.com/',
70
+ apiUrl: 'https://api.openai.com/v1/models',
71
+ headers: {
72
+ Authorization: 'Bearer $OPENAI_API_KEY',
73
+ },
74
+ testModel: 'gpt-3.5-turbo',
75
+ },
76
+ Anthropic: {
77
+ statusUrl: 'https://status.anthropic.com/',
78
+ apiUrl: 'https://api.anthropic.com/v1/messages',
79
+ headers: {
80
+ 'x-api-key': '$ANTHROPIC_API_KEY',
81
+ 'anthropic-version': '2024-02-29',
82
+ },
83
+ testModel: 'claude-3-sonnet-20240229',
84
+ },
85
+ Cohere: {
86
+ statusUrl: 'https://status.cohere.com/',
87
+ apiUrl: 'https://api.cohere.ai/v1/models',
88
+ headers: {
89
+ Authorization: 'Bearer $COHERE_API_KEY',
90
+ },
91
+ testModel: 'command',
92
+ },
93
+ Google: {
94
+ statusUrl: 'https://status.cloud.google.com/',
95
+ apiUrl: 'https://generativelanguage.googleapis.com/v1/models',
96
+ headers: {
97
+ 'x-goog-api-key': '$GOOGLE_API_KEY',
98
+ },
99
+ testModel: 'gemini-pro',
100
+ },
101
+ HuggingFace: {
102
+ statusUrl: 'https://status.huggingface.co/',
103
+ apiUrl: 'https://api-inference.huggingface.co/models',
104
+ headers: {
105
+ Authorization: 'Bearer $HUGGINGFACE_API_KEY',
106
+ },
107
+ testModel: 'mistralai/Mixtral-8x7B-Instruct-v0.1',
108
+ },
109
+ Mistral: {
110
+ statusUrl: 'https://status.mistral.ai/',
111
+ apiUrl: 'https://api.mistral.ai/v1/models',
112
+ headers: {
113
+ Authorization: 'Bearer $MISTRAL_API_KEY',
114
+ },
115
+ testModel: 'mistral-tiny',
116
+ },
117
+ Perplexity: {
118
+ statusUrl: 'https://status.perplexity.com/',
119
+ apiUrl: 'https://api.perplexity.ai/v1/models',
120
+ headers: {
121
+ Authorization: 'Bearer $PERPLEXITY_API_KEY',
122
+ },
123
+ testModel: 'pplx-7b-chat',
124
+ },
125
+ Together: {
126
+ statusUrl: 'https://status.together.ai/',
127
+ apiUrl: 'https://api.together.xyz/v1/models',
128
+ headers: {
129
+ Authorization: 'Bearer $TOGETHER_API_KEY',
130
+ },
131
+ testModel: 'mistralai/Mixtral-8x7B-Instruct-v0.1',
132
+ },
133
+ AmazonBedrock: {
134
+ statusUrl: 'https://health.aws.amazon.com/health/status',
135
+ apiUrl: 'https://bedrock.us-east-1.amazonaws.com/models',
136
+ headers: {
137
+ Authorization: 'Bearer $AWS_BEDROCK_CONFIG',
138
+ },
139
+ testModel: 'anthropic.claude-3-sonnet-20240229-v1:0',
140
+ },
141
+ Groq: {
142
+ statusUrl: 'https://groqstatus.com/',
143
+ apiUrl: 'https://api.groq.com/v1/models',
144
+ headers: {
145
+ Authorization: 'Bearer $GROQ_API_KEY',
146
+ },
147
+ testModel: 'mixtral-8x7b-32768',
148
+ },
149
+ OpenRouter: {
150
+ statusUrl: 'https://status.openrouter.ai/',
151
+ apiUrl: 'https://openrouter.ai/api/v1/models',
152
+ headers: {
153
+ Authorization: 'Bearer $OPEN_ROUTER_API_KEY',
154
+ },
155
+ testModel: 'anthropic/claude-3-sonnet',
156
+ },
157
+ XAI: {
158
+ statusUrl: 'https://status.x.ai/',
159
+ apiUrl: 'https://api.x.ai/v1/models',
160
+ headers: {
161
+ Authorization: 'Bearer $XAI_API_KEY',
162
+ },
163
+ testModel: 'grok-1',
164
+ },
165
+ Deepseek: {
166
+ statusUrl: 'https://status.deepseek.com/',
167
+ apiUrl: 'https://api.deepseek.com/v1/models',
168
+ headers: {
169
+ Authorization: 'Bearer $DEEPSEEK_API_KEY',
170
+ },
171
+ testModel: 'deepseek-chat',
172
+ },
173
+ };
174
+
175
+ const PROVIDER_ICONS: Record<ProviderName, IconType> = {
176
+ AmazonBedrock: SiAmazon,
177
+ Anthropic: FaBrain,
178
+ Cohere: BiChip,
179
+ Google: SiGoogle,
180
+ Groq: BsCloud,
181
+ HuggingFace: SiHuggingface,
182
+ Mistral: TbBrain,
183
+ OpenAI: SiOpenai,
184
+ OpenRouter: FaCloud,
185
+ Perplexity: SiPerplexity,
186
+ Together: BsCloud,
187
+ XAI: BsRobot,
188
+ Deepseek: BiCodeBlock,
189
+ };
190
+
191
+ const ServiceStatusTab = () => {
192
+ const [serviceStatuses, setServiceStatuses] = useState<ServiceStatus[]>([]);
193
+ const [loading, setLoading] = useState(true);
194
+ const [lastRefresh, setLastRefresh] = useState<Date>(new Date());
195
+ const [testApiKey, setTestApiKey] = useState<string>('');
196
+ const [testProvider, setTestProvider] = useState<ProviderName | ''>('');
197
+ const [testingStatus, setTestingStatus] = useState<'idle' | 'testing' | 'success' | 'error'>('idle');
198
+ const settings = useSettings();
199
+ const { success, error } = useToast();
200
+
201
+ // Function to get the API key for a provider from environment variables
202
+ const getApiKey = useCallback(
203
+ (provider: ProviderName): string | null => {
204
+ if (!settings.providers) {
205
+ return null;
206
+ }
207
+
208
+ // Map provider names to environment variable names
209
+ const envKeyMap: Record<ProviderName, string> = {
210
+ OpenAI: 'OPENAI_API_KEY',
211
+ Anthropic: 'ANTHROPIC_API_KEY',
212
+ Cohere: 'COHERE_API_KEY',
213
+ Google: 'GOOGLE_GENERATIVE_AI_API_KEY',
214
+ HuggingFace: 'HuggingFace_API_KEY',
215
+ Mistral: 'MISTRAL_API_KEY',
216
+ Perplexity: 'PERPLEXITY_API_KEY',
217
+ Together: 'TOGETHER_API_KEY',
218
+ AmazonBedrock: 'AWS_BEDROCK_CONFIG',
219
+ Groq: 'GROQ_API_KEY',
220
+ OpenRouter: 'OPEN_ROUTER_API_KEY',
221
+ XAI: 'XAI_API_KEY',
222
+ Deepseek: 'DEEPSEEK_API_KEY',
223
+ };
224
+
225
+ const envKey = envKeyMap[provider];
226
+
227
+ if (!envKey) {
228
+ return null;
229
+ }
230
+
231
+ // Get the API key from environment variables
232
+ const apiKey = (import.meta.env[envKey] as string) || null;
233
+
234
+ // Special handling for providers with base URLs
235
+ if (provider === 'Together' && apiKey) {
236
+ const baseUrl = import.meta.env.TOGETHER_API_BASE_URL;
237
+
238
+ if (!baseUrl) {
239
+ return null;
240
+ }
241
+ }
242
+
243
+ return apiKey;
244
+ },
245
+ [settings.providers],
246
+ );
247
+
248
+ // Update provider configurations based on available API keys
249
+ const getProviderConfig = useCallback((provider: ProviderName): ProviderConfig | null => {
250
+ const config = PROVIDER_STATUS_URLS[provider];
251
+
252
+ if (!config) {
253
+ return null;
254
+ }
255
+
256
+ // Handle special cases for providers with base URLs
257
+ let updatedConfig = { ...config };
258
+ const togetherBaseUrl = import.meta.env.TOGETHER_API_BASE_URL;
259
+
260
+ if (provider === 'Together' && togetherBaseUrl) {
261
+ updatedConfig = {
262
+ ...config,
263
+ apiUrl: `${togetherBaseUrl}/models`,
264
+ };
265
+ }
266
+
267
+ return updatedConfig;
268
+ }, []);
269
+
270
+ // Function to check if an API endpoint is accessible with model verification
271
+ const checkApiEndpoint = useCallback(
272
+ async (
273
+ url: string,
274
+ headers?: Record<string, string>,
275
+ testModel?: string,
276
+ ): Promise<{ ok: boolean; status: number | string; message?: string; responseTime: number }> => {
277
+ try {
278
+ const controller = new AbortController();
279
+ const timeoutId = setTimeout(() => controller.abort(), 10000);
280
+
281
+ const startTime = performance.now();
282
+
283
+ // Add common headers
284
+ const processedHeaders = {
285
+ 'Content-Type': 'application/json',
286
+ ...headers,
287
+ };
288
+
289
+ // First check if the API is accessible
290
+ const response = await fetch(url, {
291
+ method: 'GET',
292
+ headers: processedHeaders,
293
+ signal: controller.signal,
294
+ });
295
+
296
+ const endTime = performance.now();
297
+ const responseTime = endTime - startTime;
298
+
299
+ clearTimeout(timeoutId);
300
+
301
+ // Get response data
302
+ const data = (await response.json()) as ApiResponse;
303
+
304
+ // Special handling for different provider responses
305
+ if (!response.ok) {
306
+ let errorMessage = `API returned status: ${response.status}`;
307
+
308
+ // Handle provider-specific error messages
309
+ if (data.error?.message) {
310
+ errorMessage = data.error.message;
311
+ } else if (data.message) {
312
+ errorMessage = data.message;
313
+ }
314
+
315
+ return {
316
+ ok: false,
317
+ status: response.status,
318
+ message: errorMessage,
319
+ responseTime,
320
+ };
321
+ }
322
+
323
+ // Different providers have different model list formats
324
+ let models: string[] = [];
325
+
326
+ if (Array.isArray(data)) {
327
+ models = data.map((model: { id?: string; name?: string }) => model.id || model.name || '');
328
+ } else if (data.data && Array.isArray(data.data)) {
329
+ models = data.data.map((model) => model.id || model.name || '');
330
+ } else if (data.models && Array.isArray(data.models)) {
331
+ models = data.models.map((model) => model.id || model.name || '');
332
+ } else if (data.model) {
333
+ // Some providers return single model info
334
+ models = [data.model];
335
+ }
336
+
337
+ // For some providers, just having a successful response is enough
338
+ if (!testModel || models.length > 0) {
339
+ return {
340
+ ok: true,
341
+ status: response.status,
342
+ responseTime,
343
+ message: 'API key is valid',
344
+ };
345
+ }
346
+
347
+ // If a specific model was requested, verify it exists
348
+ if (testModel && !models.includes(testModel)) {
349
+ return {
350
+ ok: true, // Still mark as ok since API works
351
+ status: 'model_not_found',
352
+ message: `API key is valid (test model ${testModel} not found in ${models.length} available models)`,
353
+ responseTime,
354
+ };
355
+ }
356
+
357
+ return {
358
+ ok: true,
359
+ status: response.status,
360
+ message: 'API key is valid',
361
+ responseTime,
362
+ };
363
+ } catch (error) {
364
+ console.error(`Error checking API endpoint ${url}:`, error);
365
+ return {
366
+ ok: false,
367
+ status: error instanceof Error ? error.message : 'Unknown error',
368
+ message: error instanceof Error ? `Connection failed: ${error.message}` : 'Connection failed',
369
+ responseTime: 0,
370
+ };
371
+ }
372
+ },
373
+ [getApiKey],
374
+ );
375
+
376
+ // Function to fetch real status from provider status pages
377
+ const fetchPublicStatus = useCallback(
378
+ async (
379
+ provider: ProviderName,
380
+ ): Promise<{
381
+ status: ServiceStatus['status'];
382
+ message?: string;
383
+ incidents?: string[];
384
+ }> => {
385
+ try {
386
+ // Due to CORS restrictions, we can only check if the endpoints are reachable
387
+ const checkEndpoint = async (url: string) => {
388
+ try {
389
+ const response = await fetch(url, {
390
+ mode: 'no-cors',
391
+ headers: {
392
+ Accept: 'text/html',
393
+ },
394
+ });
395
+
396
+ // With no-cors, we can only know if the request succeeded
397
+ return response.type === 'opaque' ? 'reachable' : 'unreachable';
398
+ } catch (error) {
399
+ console.error(`Error checking ${url}:`, error);
400
+ return 'unreachable';
401
+ }
402
+ };
403
+
404
+ switch (provider) {
405
+ case 'HuggingFace': {
406
+ const endpointStatus = await checkEndpoint('https://status.huggingface.co/');
407
+
408
+ // Check API endpoint as fallback
409
+ const apiEndpoint = 'https://api-inference.huggingface.co/models';
410
+ const apiStatus = await checkEndpoint(apiEndpoint);
411
+
412
+ return {
413
+ status: endpointStatus === 'reachable' && apiStatus === 'reachable' ? 'operational' : 'degraded',
414
+ message: `Status page: ${endpointStatus}, API: ${apiStatus}`,
415
+ incidents: ['Note: Limited status information due to CORS restrictions'],
416
+ };
417
+ }
418
+
419
+ case 'OpenAI': {
420
+ const endpointStatus = await checkEndpoint('https://status.openai.com/');
421
+ const apiEndpoint = 'https://api.openai.com/v1/models';
422
+ const apiStatus = await checkEndpoint(apiEndpoint);
423
+
424
+ return {
425
+ status: endpointStatus === 'reachable' && apiStatus === 'reachable' ? 'operational' : 'degraded',
426
+ message: `Status page: ${endpointStatus}, API: ${apiStatus}`,
427
+ incidents: ['Note: Limited status information due to CORS restrictions'],
428
+ };
429
+ }
430
+
431
+ case 'Google': {
432
+ const endpointStatus = await checkEndpoint('https://status.cloud.google.com/');
433
+ const apiEndpoint = 'https://generativelanguage.googleapis.com/v1/models';
434
+ const apiStatus = await checkEndpoint(apiEndpoint);
435
+
436
+ return {
437
+ status: endpointStatus === 'reachable' && apiStatus === 'reachable' ? 'operational' : 'degraded',
438
+ message: `Status page: ${endpointStatus}, API: ${apiStatus}`,
439
+ incidents: ['Note: Limited status information due to CORS restrictions'],
440
+ };
441
+ }
442
+
443
+ // Similar pattern for other providers...
444
+ default:
445
+ return {
446
+ status: 'operational',
447
+ message: 'Basic reachability check only',
448
+ incidents: ['Note: Limited status information due to CORS restrictions'],
449
+ };
450
+ }
451
+ } catch (error) {
452
+ console.error(`Error fetching status for ${provider}:`, error);
453
+ return {
454
+ status: 'degraded',
455
+ message: 'Unable to fetch status due to CORS restrictions',
456
+ incidents: ['Error: Unable to check service status'],
457
+ };
458
+ }
459
+ },
460
+ [],
461
+ );
462
+
463
+ // Function to fetch status for a provider with retries
464
+ const fetchProviderStatus = useCallback(
465
+ async (provider: ProviderName, config: ProviderConfig): Promise<ServiceStatus> => {
466
+ const MAX_RETRIES = 2;
467
+ const RETRY_DELAY = 2000; // 2 seconds
468
+
469
+ const attemptCheck = async (attempt: number): Promise<ServiceStatus> => {
470
+ try {
471
+ // First check the public status page if available
472
+ const hasPublicStatus = [
473
+ 'Anthropic',
474
+ 'OpenAI',
475
+ 'Google',
476
+ 'HuggingFace',
477
+ 'Mistral',
478
+ 'Groq',
479
+ 'Perplexity',
480
+ 'Together',
481
+ ].includes(provider);
482
+
483
+ if (hasPublicStatus) {
484
+ const publicStatus = await fetchPublicStatus(provider);
485
+
486
+ return {
487
+ provider,
488
+ status: publicStatus.status,
489
+ lastChecked: new Date().toISOString(),
490
+ statusUrl: config.statusUrl,
491
+ icon: PROVIDER_ICONS[provider],
492
+ message: publicStatus.message,
493
+ incidents: publicStatus.incidents,
494
+ };
495
+ }
496
+
497
+ // For other providers, we'll show status but mark API check as separate
498
+ const apiKey = getApiKey(provider);
499
+ const providerConfig = getProviderConfig(provider);
500
+
501
+ if (!apiKey || !providerConfig) {
502
+ return {
503
+ provider,
504
+ status: 'operational',
505
+ lastChecked: new Date().toISOString(),
506
+ statusUrl: config.statusUrl,
507
+ icon: PROVIDER_ICONS[provider],
508
+ message: !apiKey
509
+ ? 'Status operational (API key needed for usage)'
510
+ : 'Status operational (configuration needed for usage)',
511
+ incidents: [],
512
+ };
513
+ }
514
+
515
+ // If we have API access, let's verify that too
516
+ const { ok, status, message, responseTime } = await checkApiEndpoint(
517
+ providerConfig.apiUrl,
518
+ providerConfig.headers,
519
+ providerConfig.testModel,
520
+ );
521
+
522
+ if (!ok && attempt < MAX_RETRIES) {
523
+ await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY));
524
+ return attemptCheck(attempt + 1);
525
+ }
526
+
527
+ return {
528
+ provider,
529
+ status: ok ? 'operational' : 'degraded',
530
+ lastChecked: new Date().toISOString(),
531
+ statusUrl: providerConfig.statusUrl,
532
+ icon: PROVIDER_ICONS[provider],
533
+ message: ok ? 'Service and API operational' : `Service operational (API: ${message || status})`,
534
+ responseTime,
535
+ incidents: [],
536
+ };
537
+ } catch (error) {
538
+ console.error(`Error fetching status for ${provider} (attempt ${attempt}):`, error);
539
+
540
+ if (attempt < MAX_RETRIES) {
541
+ await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY));
542
+ return attemptCheck(attempt + 1);
543
+ }
544
+
545
+ return {
546
+ provider,
547
+ status: 'degraded',
548
+ lastChecked: new Date().toISOString(),
549
+ statusUrl: config.statusUrl,
550
+ icon: PROVIDER_ICONS[provider],
551
+ message: 'Service operational (Status check error)',
552
+ responseTime: 0,
553
+ incidents: [],
554
+ };
555
+ }
556
+ };
557
+
558
+ return attemptCheck(1);
559
+ },
560
+ [checkApiEndpoint, getApiKey, getProviderConfig, fetchPublicStatus],
561
+ );
562
+
563
+ // Memoize the fetchAllStatuses function
564
+ const fetchAllStatuses = useCallback(async () => {
565
+ try {
566
+ setLoading(true);
567
+
568
+ const statuses = await Promise.all(
569
+ Object.entries(PROVIDER_STATUS_URLS).map(([provider, config]) =>
570
+ fetchProviderStatus(provider as ProviderName, config),
571
+ ),
572
+ );
573
+
574
+ setServiceStatuses(statuses.sort((a, b) => a.provider.localeCompare(b.provider)));
575
+ setLastRefresh(new Date());
576
+ success('Service statuses updated successfully');
577
+ } catch (err) {
578
+ console.error('Error fetching all statuses:', err);
579
+ error('Failed to update service statuses');
580
+ } finally {
581
+ setLoading(false);
582
+ }
583
+ }, [fetchProviderStatus, success, error]);
584
+
585
+ useEffect(() => {
586
+ fetchAllStatuses();
587
+
588
+ // Refresh status every 2 minutes
589
+ const interval = setInterval(fetchAllStatuses, 2 * 60 * 1000);
590
+
591
+ return () => clearInterval(interval);
592
+ }, [fetchAllStatuses]);
593
+
594
+ // Function to test an API key
595
+ const testApiKeyForProvider = useCallback(
596
+ async (provider: ProviderName, apiKey: string) => {
597
+ try {
598
+ setTestingStatus('testing');
599
+
600
+ const config = PROVIDER_STATUS_URLS[provider];
601
+
602
+ if (!config) {
603
+ throw new Error('Provider configuration not found');
604
+ }
605
+
606
+ const headers = { ...config.headers };
607
+
608
+ // Replace the placeholder API key with the test key
609
+ Object.keys(headers).forEach((key) => {
610
+ if (headers[key].startsWith('$')) {
611
+ headers[key] = headers[key].replace(/\$.*/, apiKey);
612
+ }
613
+ });
614
+
615
+ // Special handling for certain providers
616
+ switch (provider) {
617
+ case 'Anthropic':
618
+ headers['anthropic-version'] = '2024-02-29';
619
+ break;
620
+ case 'OpenAI':
621
+ if (!headers.Authorization?.startsWith('Bearer ')) {
622
+ headers.Authorization = `Bearer ${apiKey}`;
623
+ }
624
+
625
+ break;
626
+ case 'Google': {
627
+ // Google uses the API key directly in the URL
628
+ const googleUrl = `${config.apiUrl}?key=${apiKey}`;
629
+ const result = await checkApiEndpoint(googleUrl, {}, config.testModel);
630
+
631
+ if (result.ok) {
632
+ setTestingStatus('success');
633
+ success('API key is valid!');
634
+ } else {
635
+ setTestingStatus('error');
636
+ error(`API key test failed: ${result.message}`);
637
+ }
638
+
639
+ return;
640
+ }
641
+ }
642
+
643
+ const { ok, message } = await checkApiEndpoint(config.apiUrl, headers, config.testModel);
644
+
645
+ if (ok) {
646
+ setTestingStatus('success');
647
+ success('API key is valid!');
648
+ } else {
649
+ setTestingStatus('error');
650
+ error(`API key test failed: ${message}`);
651
+ }
652
+ } catch (err: unknown) {
653
+ setTestingStatus('error');
654
+ error('Failed to test API key: ' + (err instanceof Error ? err.message : 'Unknown error'));
655
+ } finally {
656
+ // Reset testing status after a delay
657
+ setTimeout(() => setTestingStatus('idle'), 3000);
658
+ }
659
+ },
660
+ [checkApiEndpoint, success, error],
661
+ );
662
+
663
+ const getStatusColor = (status: ServiceStatus['status']) => {
664
+ switch (status) {
665
+ case 'operational':
666
+ return 'text-green-500';
667
+ case 'degraded':
668
+ return 'text-yellow-500';
669
+ case 'down':
670
+ return 'text-red-500';
671
+ default:
672
+ return 'text-gray-500';
673
+ }
674
+ };
675
+
676
+ const getStatusIcon = (status: ServiceStatus['status']) => {
677
+ switch (status) {
678
+ case 'operational':
679
+ return <BsCheckCircleFill className="w-4 h-4" />;
680
+ case 'degraded':
681
+ return <BsExclamationCircleFill className="w-4 h-4" />;
682
+ case 'down':
683
+ return <BsXCircleFill className="w-4 h-4" />;
684
+ default:
685
+ return <BsXCircleFill className="w-4 h-4" />;
686
+ }
687
+ };
688
+
689
+ return (
690
+ <div className="space-y-6">
691
+ <motion.div
692
+ className="space-y-4"
693
+ initial={{ opacity: 0, y: 20 }}
694
+ animate={{ opacity: 1, y: 0 }}
695
+ transition={{ duration: 0.3 }}
696
+ >
697
+ <div className="flex items-center justify-between gap-2 mt-8 mb-4">
698
+ <div className="flex items-center gap-2">
699
+ <div
700
+ className={classNames(
701
+ 'w-8 h-8 flex items-center justify-center rounded-lg',
702
+ 'bg-bolt-elements-background-depth-3',
703
+ 'text-purple-500',
704
+ )}
705
+ >
706
+ <TbActivityHeartbeat className="w-5 h-5" />
707
+ </div>
708
+ <div>
709
+ <h4 className="text-md font-medium text-bolt-elements-textPrimary">Service Status</h4>
710
+ <p className="text-sm text-bolt-elements-textSecondary">
711
+ Monitor and test the operational status of cloud LLM providers
712
+ </p>
713
+ </div>
714
+ </div>
715
+ <div className="flex items-center gap-2">
716
+ <span className="text-sm text-bolt-elements-textSecondary">
717
+ Last updated: {lastRefresh.toLocaleTimeString()}
718
+ </span>
719
+ <button
720
+ onClick={() => fetchAllStatuses()}
721
+ className={classNames(
722
+ 'px-3 py-1.5 rounded-lg text-sm',
723
+ 'bg-bolt-elements-background-depth-3 hover:bg-bolt-elements-background-depth-4',
724
+ 'text-bolt-elements-textPrimary',
725
+ 'transition-all duration-200',
726
+ 'flex items-center gap-2',
727
+ loading ? 'opacity-50 cursor-not-allowed' : '',
728
+ )}
729
+ disabled={loading}
730
+ >
731
+ <div className={`i-ph:arrows-clockwise w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
732
+ <span>{loading ? 'Refreshing...' : 'Refresh'}</span>
733
+ </button>
734
+ </div>
735
+ </div>
736
+
737
+ {/* API Key Test Section */}
738
+ <div className="p-4 bg-bolt-elements-background-depth-2 rounded-lg">
739
+ <h5 className="text-sm font-medium text-bolt-elements-textPrimary mb-2">Test API Key</h5>
740
+ <div className="flex gap-2">
741
+ <select
742
+ value={testProvider}
743
+ onChange={(e) => setTestProvider(e.target.value as ProviderName)}
744
+ className={classNames(
745
+ 'flex-1 px-3 py-1.5 rounded-lg text-sm max-w-[200px]',
746
+ 'bg-bolt-elements-background-depth-3 border border-bolt-elements-borderColor',
747
+ 'text-bolt-elements-textPrimary',
748
+ 'focus:outline-none focus:ring-2 focus:ring-purple-500/30',
749
+ )}
750
+ >
751
+ <option value="">Select Provider</option>
752
+ {Object.keys(PROVIDER_STATUS_URLS).map((provider) => (
753
+ <option key={provider} value={provider}>
754
+ {provider}
755
+ </option>
756
+ ))}
757
+ </select>
758
+ <input
759
+ type="password"
760
+ value={testApiKey}
761
+ onChange={(e) => setTestApiKey(e.target.value)}
762
+ placeholder="Enter API key to test"
763
+ className={classNames(
764
+ 'flex-1 px-3 py-1.5 rounded-lg text-sm',
765
+ 'bg-bolt-elements-background-depth-3 border border-bolt-elements-borderColor',
766
+ 'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
767
+ 'focus:outline-none focus:ring-2 focus:ring-purple-500/30',
768
+ )}
769
+ />
770
+ <button
771
+ onClick={() =>
772
+ testProvider && testApiKey && testApiKeyForProvider(testProvider as ProviderName, testApiKey)
773
+ }
774
+ disabled={!testProvider || !testApiKey || testingStatus === 'testing'}
775
+ className={classNames(
776
+ 'px-4 py-1.5 rounded-lg text-sm',
777
+ 'bg-purple-500 hover:bg-purple-600',
778
+ 'text-white',
779
+ 'transition-all duration-200',
780
+ 'flex items-center gap-2',
781
+ !testProvider || !testApiKey || testingStatus === 'testing' ? 'opacity-50 cursor-not-allowed' : '',
782
+ )}
783
+ >
784
+ {testingStatus === 'testing' ? (
785
+ <>
786
+ <div className="i-ph:spinner-gap w-4 h-4 animate-spin" />
787
+ <span>Testing...</span>
788
+ </>
789
+ ) : (
790
+ <>
791
+ <div className="i-ph:key w-4 h-4" />
792
+ <span>Test Key</span>
793
+ </>
794
+ )}
795
+ </button>
796
+ </div>
797
+ </div>
798
+
799
+ {/* Status Grid */}
800
+ {loading && serviceStatuses.length === 0 ? (
801
+ <div className="text-center py-8 text-bolt-elements-textSecondary">Loading service statuses...</div>
802
+ ) : (
803
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
804
+ {serviceStatuses.map((service, index) => (
805
+ <motion.div
806
+ key={service.provider}
807
+ className={classNames(
808
+ 'bg-bolt-elements-background-depth-2',
809
+ 'hover:bg-bolt-elements-background-depth-3',
810
+ 'transition-all duration-200',
811
+ 'relative overflow-hidden rounded-lg',
812
+ )}
813
+ initial={{ opacity: 0, y: 20 }}
814
+ animate={{ opacity: 1, y: 0 }}
815
+ transition={{ delay: index * 0.1 }}
816
+ whileHover={{ scale: 1.02 }}
817
+ >
818
+ <div
819
+ className={classNames('block p-4', service.statusUrl ? 'cursor-pointer' : '')}
820
+ onClick={() => service.statusUrl && window.open(service.statusUrl, '_blank')}
821
+ >
822
+ <div className="flex items-center justify-between gap-4">
823
+ <div className="flex items-center gap-3">
824
+ {service.icon && (
825
+ <div
826
+ className={classNames(
827
+ 'w-8 h-8 flex items-center justify-center rounded-lg',
828
+ 'bg-bolt-elements-background-depth-3',
829
+ getStatusColor(service.status),
830
+ )}
831
+ >
832
+ {React.createElement(service.icon, {
833
+ className: 'w-5 h-5',
834
+ })}
835
+ </div>
836
+ )}
837
+ <div>
838
+ <h4 className="text-sm font-medium text-bolt-elements-textPrimary">{service.provider}</h4>
839
+ <div className="space-y-1">
840
+ <p className="text-xs text-bolt-elements-textSecondary">
841
+ Last checked: {new Date(service.lastChecked).toLocaleTimeString()}
842
+ </p>
843
+ {service.responseTime && (
844
+ <p className="text-xs text-bolt-elements-textTertiary">
845
+ Response time: {Math.round(service.responseTime)}ms
846
+ </p>
847
+ )}
848
+ {service.message && (
849
+ <p className="text-xs text-bolt-elements-textTertiary">{service.message}</p>
850
+ )}
851
+ </div>
852
+ </div>
853
+ </div>
854
+ <div className={classNames('flex items-center gap-2', getStatusColor(service.status))}>
855
+ <span className="text-sm capitalize">{service.status}</span>
856
+ {getStatusIcon(service.status)}
857
+ </div>
858
+ </div>
859
+ {service.incidents && service.incidents.length > 0 && (
860
+ <div className="mt-2 border-t border-bolt-elements-borderColor pt-2">
861
+ <p className="text-xs font-medium text-bolt-elements-textSecondary mb-1">Recent Incidents:</p>
862
+ <ul className="text-xs text-bolt-elements-textTertiary space-y-1">
863
+ {service.incidents.map((incident, i) => (
864
+ <li key={i}>{incident}</li>
865
+ ))}
866
+ </ul>
867
+ </div>
868
+ )}
869
+ </div>
870
+ </motion.div>
871
+ ))}
872
+ </div>
873
+ )}
874
+ </motion.div>
875
+ </div>
876
+ );
877
+ };
878
+
879
+ // Add tab metadata
880
+ ServiceStatusTab.tabMetadata = {
881
+ icon: 'i-ph:activity-bold',
882
+ description: 'Monitor and test LLM provider service status',
883
+ category: 'services',
884
+ };
885
+
886
+ export default ServiceStatusTab;
app/components/settings/providers/service-status/base-provider.ts ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { ProviderConfig, StatusCheckResult, ApiResponse } from './types';
2
+
3
+ export abstract class BaseProviderChecker {
4
+ protected config: ProviderConfig;
5
+
6
+ constructor(config: ProviderConfig) {
7
+ this.config = config;
8
+ }
9
+
10
+ protected async checkApiEndpoint(
11
+ url: string,
12
+ headers?: Record<string, string>,
13
+ testModel?: string,
14
+ ): Promise<{ ok: boolean; status: number | string; message?: string; responseTime: number }> {
15
+ try {
16
+ const controller = new AbortController();
17
+ const timeoutId = setTimeout(() => controller.abort(), 10000);
18
+
19
+ const startTime = performance.now();
20
+
21
+ // Add common headers
22
+ const processedHeaders = {
23
+ 'Content-Type': 'application/json',
24
+ ...headers,
25
+ };
26
+
27
+ const response = await fetch(url, {
28
+ method: 'GET',
29
+ headers: processedHeaders,
30
+ signal: controller.signal,
31
+ });
32
+
33
+ const endTime = performance.now();
34
+ const responseTime = endTime - startTime;
35
+
36
+ clearTimeout(timeoutId);
37
+
38
+ const data = (await response.json()) as ApiResponse;
39
+
40
+ if (!response.ok) {
41
+ let errorMessage = `API returned status: ${response.status}`;
42
+
43
+ if (data.error?.message) {
44
+ errorMessage = data.error.message;
45
+ } else if (data.message) {
46
+ errorMessage = data.message;
47
+ }
48
+
49
+ return {
50
+ ok: false,
51
+ status: response.status,
52
+ message: errorMessage,
53
+ responseTime,
54
+ };
55
+ }
56
+
57
+ // Different providers have different model list formats
58
+ let models: string[] = [];
59
+
60
+ if (Array.isArray(data)) {
61
+ models = data.map((model: { id?: string; name?: string }) => model.id || model.name || '');
62
+ } else if (data.data && Array.isArray(data.data)) {
63
+ models = data.data.map((model) => model.id || model.name || '');
64
+ } else if (data.models && Array.isArray(data.models)) {
65
+ models = data.models.map((model) => model.id || model.name || '');
66
+ } else if (data.model) {
67
+ models = [data.model];
68
+ }
69
+
70
+ if (!testModel || models.length > 0) {
71
+ return {
72
+ ok: true,
73
+ status: response.status,
74
+ responseTime,
75
+ message: 'API key is valid',
76
+ };
77
+ }
78
+
79
+ if (testModel && !models.includes(testModel)) {
80
+ return {
81
+ ok: true,
82
+ status: 'model_not_found',
83
+ message: `API key is valid (test model ${testModel} not found in ${models.length} available models)`,
84
+ responseTime,
85
+ };
86
+ }
87
+
88
+ return {
89
+ ok: true,
90
+ status: response.status,
91
+ message: 'API key is valid',
92
+ responseTime,
93
+ };
94
+ } catch (error) {
95
+ console.error(`Error checking API endpoint ${url}:`, error);
96
+ return {
97
+ ok: false,
98
+ status: error instanceof Error ? error.message : 'Unknown error',
99
+ message: error instanceof Error ? `Connection failed: ${error.message}` : 'Connection failed',
100
+ responseTime: 0,
101
+ };
102
+ }
103
+ }
104
+
105
+ protected async checkEndpoint(url: string): Promise<'reachable' | 'unreachable'> {
106
+ try {
107
+ const response = await fetch(url, {
108
+ mode: 'no-cors',
109
+ headers: {
110
+ Accept: 'text/html',
111
+ },
112
+ });
113
+ return response.type === 'opaque' ? 'reachable' : 'unreachable';
114
+ } catch (error) {
115
+ console.error(`Error checking ${url}:`, error);
116
+ return 'unreachable';
117
+ }
118
+ }
119
+
120
+ abstract checkStatus(): Promise<StatusCheckResult>;
121
+ }
app/components/settings/providers/service-status/provider-factory.ts ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { ProviderName, ProviderConfig, StatusCheckResult } from './types';
2
+ import { OpenAIStatusChecker } from './providers/openai';
3
+ import { BaseProviderChecker } from './base-provider';
4
+
5
+ // Import other provider implementations as they are created
6
+
7
+ export class ProviderStatusCheckerFactory {
8
+ private static _providerConfigs: Record<ProviderName, ProviderConfig> = {
9
+ OpenAI: {
10
+ statusUrl: 'https://status.openai.com/',
11
+ apiUrl: 'https://api.openai.com/v1/models',
12
+ headers: {
13
+ Authorization: 'Bearer $OPENAI_API_KEY',
14
+ },
15
+ testModel: 'gpt-3.5-turbo',
16
+ },
17
+ Anthropic: {
18
+ statusUrl: 'https://status.anthropic.com/',
19
+ apiUrl: 'https://api.anthropic.com/v1/messages',
20
+ headers: {
21
+ 'x-api-key': '$ANTHROPIC_API_KEY',
22
+ 'anthropic-version': '2024-02-29',
23
+ },
24
+ testModel: 'claude-3-sonnet-20240229',
25
+ },
26
+ AmazonBedrock: {
27
+ statusUrl: 'https://health.aws.amazon.com/health/status',
28
+ apiUrl: 'https://bedrock.us-east-1.amazonaws.com/models',
29
+ headers: {
30
+ Authorization: 'Bearer $AWS_BEDROCK_CONFIG',
31
+ },
32
+ testModel: 'anthropic.claude-3-sonnet-20240229-v1:0',
33
+ },
34
+ Cohere: {
35
+ statusUrl: 'https://status.cohere.com/',
36
+ apiUrl: 'https://api.cohere.ai/v1/models',
37
+ headers: {
38
+ Authorization: 'Bearer $COHERE_API_KEY',
39
+ },
40
+ testModel: 'command',
41
+ },
42
+ Deepseek: {
43
+ statusUrl: 'https://status.deepseek.com/',
44
+ apiUrl: 'https://api.deepseek.com/v1/models',
45
+ headers: {
46
+ Authorization: 'Bearer $DEEPSEEK_API_KEY',
47
+ },
48
+ testModel: 'deepseek-chat',
49
+ },
50
+ Google: {
51
+ statusUrl: 'https://status.cloud.google.com/',
52
+ apiUrl: 'https://generativelanguage.googleapis.com/v1/models',
53
+ headers: {
54
+ 'x-goog-api-key': '$GOOGLE_API_KEY',
55
+ },
56
+ testModel: 'gemini-pro',
57
+ },
58
+ Groq: {
59
+ statusUrl: 'https://groqstatus.com/',
60
+ apiUrl: 'https://api.groq.com/v1/models',
61
+ headers: {
62
+ Authorization: 'Bearer $GROQ_API_KEY',
63
+ },
64
+ testModel: 'mixtral-8x7b-32768',
65
+ },
66
+ HuggingFace: {
67
+ statusUrl: 'https://status.huggingface.co/',
68
+ apiUrl: 'https://api-inference.huggingface.co/models',
69
+ headers: {
70
+ Authorization: 'Bearer $HUGGINGFACE_API_KEY',
71
+ },
72
+ testModel: 'mistralai/Mixtral-8x7B-Instruct-v0.1',
73
+ },
74
+ Hyperbolic: {
75
+ statusUrl: 'https://status.hyperbolic.ai/',
76
+ apiUrl: 'https://api.hyperbolic.ai/v1/models',
77
+ headers: {
78
+ Authorization: 'Bearer $HYPERBOLIC_API_KEY',
79
+ },
80
+ testModel: 'hyperbolic-1',
81
+ },
82
+ Mistral: {
83
+ statusUrl: 'https://status.mistral.ai/',
84
+ apiUrl: 'https://api.mistral.ai/v1/models',
85
+ headers: {
86
+ Authorization: 'Bearer $MISTRAL_API_KEY',
87
+ },
88
+ testModel: 'mistral-tiny',
89
+ },
90
+ OpenRouter: {
91
+ statusUrl: 'https://status.openrouter.ai/',
92
+ apiUrl: 'https://openrouter.ai/api/v1/models',
93
+ headers: {
94
+ Authorization: 'Bearer $OPEN_ROUTER_API_KEY',
95
+ },
96
+ testModel: 'anthropic/claude-3-sonnet',
97
+ },
98
+ Perplexity: {
99
+ statusUrl: 'https://status.perplexity.com/',
100
+ apiUrl: 'https://api.perplexity.ai/v1/models',
101
+ headers: {
102
+ Authorization: 'Bearer $PERPLEXITY_API_KEY',
103
+ },
104
+ testModel: 'pplx-7b-chat',
105
+ },
106
+ Together: {
107
+ statusUrl: 'https://status.together.ai/',
108
+ apiUrl: 'https://api.together.xyz/v1/models',
109
+ headers: {
110
+ Authorization: 'Bearer $TOGETHER_API_KEY',
111
+ },
112
+ testModel: 'mistralai/Mixtral-8x7B-Instruct-v0.1',
113
+ },
114
+ XAI: {
115
+ statusUrl: 'https://status.x.ai/',
116
+ apiUrl: 'https://api.x.ai/v1/models',
117
+ headers: {
118
+ Authorization: 'Bearer $XAI_API_KEY',
119
+ },
120
+ testModel: 'grok-1',
121
+ },
122
+ };
123
+
124
+ static getChecker(provider: ProviderName): BaseProviderChecker {
125
+ const config = this._providerConfigs[provider];
126
+
127
+ if (!config) {
128
+ throw new Error(`No configuration found for provider: ${provider}`);
129
+ }
130
+
131
+ // Return specific provider implementation or fallback to base implementation
132
+ switch (provider) {
133
+ case 'OpenAI':
134
+ return new OpenAIStatusChecker(config);
135
+
136
+ // Add other provider implementations as they are created
137
+ default:
138
+ return new (class extends BaseProviderChecker {
139
+ async checkStatus(): Promise<StatusCheckResult> {
140
+ const endpointStatus = await this.checkEndpoint(this.config.statusUrl);
141
+ const apiStatus = await this.checkEndpoint(this.config.apiUrl);
142
+
143
+ return {
144
+ status: endpointStatus === 'reachable' && apiStatus === 'reachable' ? 'operational' : 'degraded',
145
+ message: `Status page: ${endpointStatus}, API: ${apiStatus}`,
146
+ incidents: ['Note: Limited status information due to CORS restrictions'],
147
+ };
148
+ }
149
+ })(config);
150
+ }
151
+ }
152
+
153
+ static getProviderNames(): ProviderName[] {
154
+ return Object.keys(this._providerConfigs) as ProviderName[];
155
+ }
156
+
157
+ static getProviderConfig(provider: ProviderName): ProviderConfig | undefined {
158
+ return this._providerConfigs[provider];
159
+ }
160
+ }
app/components/settings/providers/service-status/providers/openai.ts ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { BaseProviderChecker } from '~/components/settings/providers/service-status/base-provider';
2
+ import type { StatusCheckResult } from '~/components/settings/providers/service-status/types';
3
+
4
+ export class OpenAIStatusChecker extends BaseProviderChecker {
5
+ async checkStatus(): Promise<StatusCheckResult> {
6
+ try {
7
+ // Check status page
8
+ const statusPageResponse = await fetch('https://status.openai.com/');
9
+ const text = await statusPageResponse.text();
10
+
11
+ // Check individual services
12
+ const services = {
13
+ api: {
14
+ operational: text.includes('API ? Operational'),
15
+ degraded: text.includes('API ? Degraded Performance'),
16
+ outage: text.includes('API ? Major Outage') || text.includes('API ? Partial Outage'),
17
+ },
18
+ chat: {
19
+ operational: text.includes('ChatGPT ? Operational'),
20
+ degraded: text.includes('ChatGPT ? Degraded Performance'),
21
+ outage: text.includes('ChatGPT ? Major Outage') || text.includes('ChatGPT ? Partial Outage'),
22
+ },
23
+ };
24
+
25
+ // Extract recent incidents
26
+ const incidents: string[] = [];
27
+ const incidentMatches = text.match(/Past Incidents(.*?)(?=\w+ \d+, \d{4})/s);
28
+
29
+ if (incidentMatches) {
30
+ const recentIncidents = incidentMatches[1]
31
+ .split('\n')
32
+ .map((line) => line.trim())
33
+ .filter((line) => line && line.includes('202')); // Get only dated incidents
34
+
35
+ incidents.push(...recentIncidents.slice(0, 5));
36
+ }
37
+
38
+ // Determine overall status
39
+ let status: StatusCheckResult['status'] = 'operational';
40
+ const messages: string[] = [];
41
+
42
+ if (services.api.outage || services.chat.outage) {
43
+ status = 'down';
44
+
45
+ if (services.api.outage) {
46
+ messages.push('API: Major Outage');
47
+ }
48
+
49
+ if (services.chat.outage) {
50
+ messages.push('ChatGPT: Major Outage');
51
+ }
52
+ } else if (services.api.degraded || services.chat.degraded) {
53
+ status = 'degraded';
54
+
55
+ if (services.api.degraded) {
56
+ messages.push('API: Degraded Performance');
57
+ }
58
+
59
+ if (services.chat.degraded) {
60
+ messages.push('ChatGPT: Degraded Performance');
61
+ }
62
+ } else if (services.api.operational) {
63
+ messages.push('API: Operational');
64
+ }
65
+
66
+ // If status page check fails, fallback to endpoint check
67
+ if (!statusPageResponse.ok) {
68
+ const endpointStatus = await this.checkEndpoint('https://status.openai.com/');
69
+ const apiEndpoint = 'https://api.openai.com/v1/models';
70
+ const apiStatus = await this.checkEndpoint(apiEndpoint);
71
+
72
+ return {
73
+ status: endpointStatus === 'reachable' && apiStatus === 'reachable' ? 'operational' : 'degraded',
74
+ message: `Status page: ${endpointStatus}, API: ${apiStatus}`,
75
+ incidents: ['Note: Limited status information due to CORS restrictions'],
76
+ };
77
+ }
78
+
79
+ return {
80
+ status,
81
+ message: messages.join(', ') || 'Status unknown',
82
+ incidents,
83
+ };
84
+ } catch (error) {
85
+ console.error('Error checking OpenAI status:', error);
86
+
87
+ // Fallback to basic endpoint check
88
+ const endpointStatus = await this.checkEndpoint('https://status.openai.com/');
89
+ const apiEndpoint = 'https://api.openai.com/v1/models';
90
+ const apiStatus = await this.checkEndpoint(apiEndpoint);
91
+
92
+ return {
93
+ status: endpointStatus === 'reachable' && apiStatus === 'reachable' ? 'operational' : 'degraded',
94
+ message: `Status page: ${endpointStatus}, API: ${apiStatus}`,
95
+ incidents: ['Note: Limited status information due to CORS restrictions'],
96
+ };
97
+ }
98
+ }
99
+ }
app/components/settings/providers/service-status/types.ts ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { IconType } from 'react-icons';
2
+
3
+ export type ProviderName =
4
+ | 'AmazonBedrock'
5
+ | 'Anthropic'
6
+ | 'Cohere'
7
+ | 'Deepseek'
8
+ | 'Google'
9
+ | 'Groq'
10
+ | 'HuggingFace'
11
+ | 'Hyperbolic'
12
+ | 'Mistral'
13
+ | 'OpenAI'
14
+ | 'OpenRouter'
15
+ | 'Perplexity'
16
+ | 'Together'
17
+ | 'XAI';
18
+
19
+ export type ServiceStatus = {
20
+ provider: ProviderName;
21
+ status: 'operational' | 'degraded' | 'down';
22
+ lastChecked: string;
23
+ statusUrl?: string;
24
+ icon?: IconType;
25
+ message?: string;
26
+ responseTime?: number;
27
+ incidents?: string[];
28
+ };
29
+
30
+ export type ProviderConfig = {
31
+ statusUrl: string;
32
+ apiUrl: string;
33
+ headers: Record<string, string>;
34
+ testModel: string;
35
+ };
36
+
37
+ export type ApiResponse = {
38
+ error?: {
39
+ message: string;
40
+ };
41
+ message?: string;
42
+ model?: string;
43
+ models?: Array<{
44
+ id?: string;
45
+ name?: string;
46
+ }>;
47
+ data?: Array<{
48
+ id?: string;
49
+ name?: string;
50
+ }>;
51
+ };
52
+
53
+ export type StatusCheckResult = {
54
+ status: ServiceStatus['status'];
55
+ message?: string;
56
+ incidents?: string[];
57
+ responseTime?: number;
58
+ };
app/components/settings/settings.types.ts CHANGED
@@ -14,7 +14,8 @@ export type TabType =
14
  | 'debug'
15
  | 'event-logs'
16
  | 'update'
17
- | 'task-manager';
 
18
 
19
  export type WindowType = 'user' | 'developer';
20
 
@@ -68,6 +69,7 @@ export const TAB_LABELS: Record<TabType, string> = {
68
  'event-logs': 'Event Logs',
69
  update: 'Update',
70
  'task-manager': 'Task Manager',
 
71
  };
72
 
73
  export const DEFAULT_TAB_CONFIG: TabVisibilityConfig[] = [
@@ -75,17 +77,18 @@ export const DEFAULT_TAB_CONFIG: TabVisibilityConfig[] = [
75
  { id: 'features', visible: true, window: 'user', order: 0 },
76
  { id: 'data', visible: true, window: 'user', order: 1 },
77
  { id: 'cloud-providers', visible: true, window: 'user', order: 2 },
78
- { id: 'local-providers', visible: true, window: 'user', order: 3 },
79
- { id: 'connection', visible: true, window: 'user', order: 4 },
80
- { id: 'debug', visible: true, window: 'user', order: 5 },
 
81
 
82
  // User Window Tabs (Hidden by default)
83
- { id: 'profile', visible: false, window: 'user', order: 6 },
84
- { id: 'settings', visible: false, window: 'user', order: 7 },
85
- { id: 'notifications', visible: false, window: 'user', order: 8 },
86
- { id: 'event-logs', visible: false, window: 'user', order: 9 },
87
- { id: 'update', visible: false, window: 'user', order: 10 },
88
- { id: 'task-manager', visible: false, window: 'user', order: 11 },
89
 
90
  // Developer Window Tabs (All visible by default)
91
  { id: 'profile', visible: true, window: 'developer', order: 0 },
@@ -94,12 +97,13 @@ export const DEFAULT_TAB_CONFIG: TabVisibilityConfig[] = [
94
  { id: 'features', visible: true, window: 'developer', order: 3 },
95
  { id: 'data', visible: true, window: 'developer', order: 4 },
96
  { id: 'cloud-providers', visible: true, window: 'developer', order: 5 },
97
- { id: 'local-providers', visible: true, window: 'developer', order: 6 },
98
- { id: 'connection', visible: true, window: 'developer', order: 7 },
99
- { id: 'debug', visible: true, window: 'developer', order: 8 },
100
- { id: 'event-logs', visible: true, window: 'developer', order: 9 },
101
- { id: 'update', visible: true, window: 'developer', order: 10 },
102
- { id: 'task-manager', visible: true, window: 'developer', order: 11 },
 
103
  ];
104
 
105
  export const categoryLabels: Record<SettingCategory, string> = {
 
14
  | 'debug'
15
  | 'event-logs'
16
  | 'update'
17
+ | 'task-manager'
18
+ | 'service-status';
19
 
20
  export type WindowType = 'user' | 'developer';
21
 
 
69
  'event-logs': 'Event Logs',
70
  update: 'Update',
71
  'task-manager': 'Task Manager',
72
+ 'service-status': 'Service Status',
73
  };
74
 
75
  export const DEFAULT_TAB_CONFIG: TabVisibilityConfig[] = [
 
77
  { id: 'features', visible: true, window: 'user', order: 0 },
78
  { id: 'data', visible: true, window: 'user', order: 1 },
79
  { id: 'cloud-providers', visible: true, window: 'user', order: 2 },
80
+ { id: 'service-status', visible: true, window: 'user', order: 3 },
81
+ { id: 'local-providers', visible: true, window: 'user', order: 4 },
82
+ { id: 'connection', visible: true, window: 'user', order: 5 },
83
+ { id: 'debug', visible: true, window: 'user', order: 6 },
84
 
85
  // User Window Tabs (Hidden by default)
86
+ { id: 'profile', visible: false, window: 'user', order: 7 },
87
+ { id: 'settings', visible: false, window: 'user', order: 8 },
88
+ { id: 'notifications', visible: false, window: 'user', order: 9 },
89
+ { id: 'event-logs', visible: false, window: 'user', order: 10 },
90
+ { id: 'update', visible: false, window: 'user', order: 11 },
91
+ { id: 'task-manager', visible: false, window: 'user', order: 12 },
92
 
93
  // Developer Window Tabs (All visible by default)
94
  { id: 'profile', visible: true, window: 'developer', order: 0 },
 
97
  { id: 'features', visible: true, window: 'developer', order: 3 },
98
  { id: 'data', visible: true, window: 'developer', order: 4 },
99
  { id: 'cloud-providers', visible: true, window: 'developer', order: 5 },
100
+ { id: 'service-status', visible: true, window: 'developer', order: 6 },
101
+ { id: 'local-providers', visible: true, window: 'developer', order: 7 },
102
+ { id: 'connection', visible: true, window: 'developer', order: 8 },
103
+ { id: 'debug', visible: true, window: 'developer', order: 9 },
104
+ { id: 'event-logs', visible: true, window: 'developer', order: 10 },
105
+ { id: 'update', visible: true, window: 'developer', order: 11 },
106
+ { id: 'task-manager', visible: true, window: 'developer', order: 12 },
107
  ];
108
 
109
  export const categoryLabels: Record<SettingCategory, string> = {
app/components/settings/settings/SettingsTab.tsx CHANGED
@@ -35,11 +35,11 @@ export default function SettingsTab() {
35
 
36
  const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
37
  document.querySelector('html')?.setAttribute('data-theme', prefersDark ? 'dark' : 'light');
 
38
  } else {
39
- // Set specific theme
40
  localStorage.setItem(kTheme, settings.theme);
41
  document.querySelector('html')?.setAttribute('data-theme', settings.theme);
42
- themeStore.set(settings.theme);
43
  }
44
  }, [settings.theme]);
45
 
@@ -89,7 +89,13 @@ export default function SettingsTab() {
89
  {(['light', 'dark', 'system'] as const).map((theme) => (
90
  <button
91
  key={theme}
92
- onClick={() => setSettings((prev) => ({ ...prev, theme }))}
 
 
 
 
 
 
93
  className={classNames(
94
  settingsStyles.button.base,
95
  settings.theme === theme ? settingsStyles.button.primary : settingsStyles.button.secondary,
 
35
 
36
  const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
37
  document.querySelector('html')?.setAttribute('data-theme', prefersDark ? 'dark' : 'light');
38
+ themeStore.set(prefersDark ? 'dark' : 'light');
39
  } else {
40
+ themeStore.set(settings.theme);
41
  localStorage.setItem(kTheme, settings.theme);
42
  document.querySelector('html')?.setAttribute('data-theme', settings.theme);
 
43
  }
44
  }, [settings.theme]);
45
 
 
89
  {(['light', 'dark', 'system'] as const).map((theme) => (
90
  <button
91
  key={theme}
92
+ onClick={() => {
93
+ setSettings((prev) => ({ ...prev, theme }));
94
+
95
+ if (theme !== 'system') {
96
+ themeStore.set(theme);
97
+ }
98
+ }}
99
  className={classNames(
100
  settingsStyles.button.base,
101
  settings.theme === theme ? settingsStyles.button.primary : settingsStyles.button.secondary,
app/components/settings/shared/TabTile.tsx CHANGED
@@ -18,6 +18,7 @@ const TAB_ICONS = {
18
  'task-manager': 'i-ph:activity',
19
  'cloud-providers': 'i-ph:cloud',
20
  'local-providers': 'i-ph:desktop',
 
21
  };
22
 
23
  interface TabTileProps {
 
18
  'task-manager': 'i-ph:activity',
19
  'cloud-providers': 'i-ph:cloud',
20
  'local-providers': 'i-ph:desktop',
21
+ 'service-status': 'i-ph:activity-bold',
22
  };
23
 
24
  interface TabTileProps {
app/components/settings/user/UsersWindow.tsx CHANGED
@@ -27,6 +27,7 @@ import { useNotifications } from '~/lib/hooks/useNotifications';
27
  import { useConnectionStatus } from '~/lib/hooks/useConnectionStatus';
28
  import { useDebugStatus } from '~/lib/hooks/useDebugStatus';
29
  import CloudProvidersTab from '~/components/settings/providers/CloudProvidersTab';
 
30
  import LocalProvidersTab from '~/components/settings/providers/LocalProvidersTab';
31
  import TaskManagerTab from '~/components/settings/task-manager/TaskManagerTab';
32
  import {
@@ -57,6 +58,7 @@ const TAB_DESCRIPTIONS: Record<TabType, string> = {
57
  data: 'Manage your data and storage',
58
  'cloud-providers': 'Configure cloud AI providers and models',
59
  'local-providers': 'Configure local AI providers and models',
 
60
  connection: 'Check connection status and settings',
61
  debug: 'Debug tools and system information',
62
  'event-logs': 'View system events and logs',
@@ -320,6 +322,8 @@ export const UsersWindow = ({ open, onClose }: UsersWindowProps) => {
320
  return <DataTab />;
321
  case 'cloud-providers':
322
  return <CloudProvidersTab />;
 
 
323
  case 'local-providers':
324
  return <LocalProvidersTab />;
325
  case 'connection':
 
27
  import { useConnectionStatus } from '~/lib/hooks/useConnectionStatus';
28
  import { useDebugStatus } from '~/lib/hooks/useDebugStatus';
29
  import CloudProvidersTab from '~/components/settings/providers/CloudProvidersTab';
30
+ import ServiceStatusTab from '~/components/settings/providers/ServiceStatusTab';
31
  import LocalProvidersTab from '~/components/settings/providers/LocalProvidersTab';
32
  import TaskManagerTab from '~/components/settings/task-manager/TaskManagerTab';
33
  import {
 
58
  data: 'Manage your data and storage',
59
  'cloud-providers': 'Configure cloud AI providers and models',
60
  'local-providers': 'Configure local AI providers and models',
61
+ 'service-status': 'Monitor cloud LLM service status',
62
  connection: 'Check connection status and settings',
63
  debug: 'Debug tools and system information',
64
  'event-logs': 'View system events and logs',
 
322
  return <DataTab />;
323
  case 'cloud-providers':
324
  return <CloudProvidersTab />;
325
+ case 'service-status':
326
+ return <ServiceStatusTab />;
327
  case 'local-providers':
328
  return <LocalProvidersTab />;
329
  case 'connection':
app/lib/hooks/useShortcuts.ts CHANGED
@@ -25,26 +25,57 @@ export function useShortcuts(): void {
25
 
26
  useEffect(() => {
27
  const handleKeyDown = (event: KeyboardEvent): void => {
28
- const { key, ctrlKey, shiftKey, altKey, metaKey } = event;
 
 
 
 
 
 
 
 
29
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  for (const name in shortcuts) {
31
  const shortcut = shortcuts[name as keyof Shortcuts];
32
 
33
- if (
34
- shortcut.key.toLowerCase() === key.toLowerCase() &&
35
- (shortcut.ctrlOrMetaKey
36
- ? ctrlKey || metaKey
37
- : (shortcut.ctrlKey === undefined || shortcut.ctrlKey === ctrlKey) &&
38
- (shortcut.metaKey === undefined || shortcut.metaKey === metaKey)) &&
39
- (shortcut.shiftKey === undefined || shortcut.shiftKey === shiftKey) &&
40
- (shortcut.altKey === undefined || shortcut.altKey === altKey)
41
- ) {
42
- shortcutEventEmitter.dispatch(name as keyof Shortcuts);
 
 
 
 
 
43
  event.preventDefault();
44
  event.stopPropagation();
45
-
46
  shortcut.action();
47
-
48
  break;
49
  }
50
  }
 
25
 
26
  useEffect(() => {
27
  const handleKeyDown = (event: KeyboardEvent): void => {
28
+ // Debug logging
29
+ console.log('Key pressed:', {
30
+ key: event.key,
31
+ code: event.code, // This gives us the physical key regardless of modifiers
32
+ ctrlKey: event.ctrlKey,
33
+ shiftKey: event.shiftKey,
34
+ altKey: event.altKey,
35
+ metaKey: event.metaKey,
36
+ });
37
 
38
+ /*
39
+ * Check for theme toggle shortcut first (Option + Command + Shift + D)
40
+ * Use event.code to check for the physical D key regardless of the character produced
41
+ */
42
+ if (
43
+ event.code === 'KeyD' &&
44
+ event.metaKey && // Command (Mac) or Windows key
45
+ event.altKey && // Option (Mac) or Alt (Windows)
46
+ event.shiftKey &&
47
+ !event.ctrlKey
48
+ ) {
49
+ event.preventDefault();
50
+ event.stopPropagation();
51
+ shortcuts.toggleTheme.action();
52
+
53
+ return;
54
+ }
55
+
56
+ // Handle other shortcuts
57
  for (const name in shortcuts) {
58
  const shortcut = shortcuts[name as keyof Shortcuts];
59
 
60
+ if (name === 'toggleTheme') {
61
+ continue;
62
+ } // Skip theme toggle as it's handled above
63
+
64
+ // For other shortcuts, check both key and code
65
+ const keyMatches =
66
+ shortcut.key.toLowerCase() === event.key.toLowerCase() || `Key${shortcut.key.toUpperCase()}` === event.code;
67
+
68
+ const modifiersMatch =
69
+ (shortcut.ctrlKey === undefined || shortcut.ctrlKey === event.ctrlKey) &&
70
+ (shortcut.metaKey === undefined || shortcut.metaKey === event.metaKey) &&
71
+ (shortcut.shiftKey === undefined || shortcut.shiftKey === event.shiftKey) &&
72
+ (shortcut.altKey === undefined || shortcut.altKey === event.altKey);
73
+
74
+ if (keyMatches && modifiersMatch) {
75
  event.preventDefault();
76
  event.stopPropagation();
77
+ shortcutEventEmitter.dispatch(name as keyof Shortcuts);
78
  shortcut.action();
 
79
  break;
80
  }
81
  }
app/lib/modules/llm/providers/github.ts DELETED
@@ -1,53 +0,0 @@
1
- import { BaseProvider } from '~/lib/modules/llm/base-provider';
2
- import type { ModelInfo } from '~/lib/modules/llm/types';
3
- import type { IProviderSetting } from '~/types/model';
4
- import type { LanguageModelV1 } from 'ai';
5
- import { createOpenAI } from '@ai-sdk/openai';
6
-
7
- export default class GithubProvider extends BaseProvider {
8
- name = 'Github';
9
- getApiKeyLink = 'https://github.com/settings/personal-access-tokens';
10
-
11
- config = {
12
- apiTokenKey: 'GITHUB_API_KEY',
13
- };
14
-
15
- // find more in https://github.com/marketplace?type=models
16
- staticModels: ModelInfo[] = [
17
- { name: 'gpt-4o', label: 'GPT-4o', provider: 'Github', maxTokenAllowed: 8000 },
18
- { name: 'o1', label: 'o1-preview', provider: 'Github', maxTokenAllowed: 100000 },
19
- { name: 'o1-mini', label: 'o1-mini', provider: 'Github', maxTokenAllowed: 8000 },
20
- { name: 'gpt-4o-mini', label: 'GPT-4o Mini', provider: 'Github', maxTokenAllowed: 8000 },
21
- { name: 'gpt-4-turbo', label: 'GPT-4 Turbo', provider: 'Github', maxTokenAllowed: 8000 },
22
- { name: 'gpt-4', label: 'GPT-4', provider: 'Github', maxTokenAllowed: 8000 },
23
- { name: 'gpt-3.5-turbo', label: 'GPT-3.5 Turbo', provider: 'Github', maxTokenAllowed: 8000 },
24
- ];
25
-
26
- getModelInstance(options: {
27
- model: string;
28
- serverEnv: Env;
29
- apiKeys?: Record<string, string>;
30
- providerSettings?: Record<string, IProviderSetting>;
31
- }): LanguageModelV1 {
32
- const { model, serverEnv, apiKeys, providerSettings } = options;
33
-
34
- const { apiKey } = this.getProviderBaseUrlAndKey({
35
- apiKeys,
36
- providerSettings: providerSettings?.[this.name],
37
- serverEnv: serverEnv as any,
38
- defaultBaseUrlKey: '',
39
- defaultApiTokenKey: 'GITHUB_API_KEY',
40
- });
41
-
42
- if (!apiKey) {
43
- throw new Error(`Missing API key for ${this.name} provider`);
44
- }
45
-
46
- const openai = createOpenAI({
47
- baseURL: 'https://models.inference.ai.azure.com',
48
- apiKey,
49
- });
50
-
51
- return openai(model);
52
- }
53
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/lib/modules/llm/registry.ts CHANGED
@@ -15,7 +15,6 @@ import TogetherProvider from './providers/together';
15
  import XAIProvider from './providers/xai';
16
  import HyperbolicProvider from './providers/hyperbolic';
17
  import AmazonBedrockProvider from './providers/amazon-bedrock';
18
- import GithubProvider from './providers/github';
19
 
20
  export {
21
  AnthropicProvider,
@@ -35,5 +34,4 @@ export {
35
  TogetherProvider,
36
  LMStudioProvider,
37
  AmazonBedrockProvider,
38
- GithubProvider,
39
  };
 
15
  import XAIProvider from './providers/xai';
16
  import HyperbolicProvider from './providers/hyperbolic';
17
  import AmazonBedrockProvider from './providers/amazon-bedrock';
 
18
 
19
  export {
20
  AnthropicProvider,
 
34
  TogetherProvider,
35
  LMStudioProvider,
36
  AmazonBedrockProvider,
 
37
  };
app/lib/stores/settings.ts CHANGED
@@ -38,8 +38,9 @@ export const shortcutsStore = map<Shortcuts>({
38
  },
39
  toggleTheme: {
40
  key: 'd',
41
- ctrlOrMetaKey: true,
42
- altKey: true,
 
43
  action: () => toggleTheme(),
44
  },
45
  toggleChat: {
 
38
  },
39
  toggleTheme: {
40
  key: 'd',
41
+ metaKey: true, // Command key on Mac, Windows key on Windows
42
+ altKey: true, // Option key on Mac, Alt key on Windows
43
+ shiftKey: true,
44
  action: () => toggleTheme(),
45
  },
46
  toggleChat: {
app/lib/stores/theme.ts CHANGED
@@ -27,8 +27,28 @@ function initStore() {
27
  export function toggleTheme() {
28
  const currentTheme = themeStore.get();
29
  const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
 
 
30
  themeStore.set(newTheme);
31
- logStore.logSystem(`Theme changed to ${newTheme} mode`);
 
32
  localStorage.setItem(kTheme, newTheme);
 
 
33
  document.querySelector('html')?.setAttribute('data-theme', newTheme);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  }
 
27
  export function toggleTheme() {
28
  const currentTheme = themeStore.get();
29
  const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
30
+
31
+ // Update the theme store
32
  themeStore.set(newTheme);
33
+
34
+ // Update localStorage
35
  localStorage.setItem(kTheme, newTheme);
36
+
37
+ // Update the HTML attribute
38
  document.querySelector('html')?.setAttribute('data-theme', newTheme);
39
+
40
+ // Update user profile if it exists
41
+ try {
42
+ const userProfile = localStorage.getItem('bolt_user_profile');
43
+
44
+ if (userProfile) {
45
+ const profile = JSON.parse(userProfile);
46
+ profile.theme = newTheme;
47
+ localStorage.setItem('bolt_user_profile', JSON.stringify(profile));
48
+ }
49
+ } catch (error) {
50
+ console.error('Error updating user profile theme:', error);
51
+ }
52
+
53
+ logStore.logSystem(`Theme changed to ${newTheme} mode`);
54
  }
public/icons/astro.svg ADDED
public/icons/nextjs.svg ADDED
public/icons/qwik.svg ADDED
uno.config.ts CHANGED
@@ -7,7 +7,7 @@ import type { IconifyJSON } from '@iconify/types';
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';
 
7
  // Debug: Log the current working directory and icon paths
8
  console.log('CWD:', process.cwd());
9
 
10
+ const iconPaths = globSync(join(process.cwd(), 'icons/*.svg'));
11
  console.log('Found icons:', iconPaths);
12
 
13
  const collectionName = 'bolt';