Stijnus
commited on
Commit
·
d9a380f
1
Parent(s):
9e8d05c
Service console check providers
Browse files- app/components/chat/StarterTemplates.tsx +1 -1
- app/components/settings/data/DataTab.tsx +23 -3
- app/components/settings/developer/DeveloperWindow.tsx +9 -8
- app/components/settings/developer/TabManagement.tsx +2 -1
- app/components/settings/providers/ServiceStatusTab.tsx +886 -0
- app/components/settings/providers/service-status/base-provider.ts +121 -0
- app/components/settings/providers/service-status/provider-factory.ts +160 -0
- app/components/settings/providers/service-status/providers/openai.ts +99 -0
- app/components/settings/providers/service-status/types.ts +58 -0
- app/components/settings/settings.types.ts +20 -16
- app/components/settings/settings/SettingsTab.tsx +9 -3
- app/components/settings/shared/TabTile.tsx +1 -0
- app/components/settings/user/UsersWindow.tsx +4 -0
- app/lib/hooks/useShortcuts.ts +44 -13
- app/lib/modules/llm/providers/github.ts +0 -53
- app/lib/modules/llm/registry.ts +0 -2
- app/lib/stores/settings.ts +3 -2
- app/lib/stores/theme.ts +21 -1
- public/icons/astro.svg +5 -0
- public/icons/nextjs.svg +5 -0
- public/icons/qwik.svg +4 -0
- uno.config.ts +1 -1
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-
|
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: '
|
52 |
data: 'Manage your data and storage',
|
53 |
-
'cloud-providers': 'Configure cloud AI providers
|
54 |
-
'local-providers': 'Configure local AI providers
|
55 |
-
connection: '
|
56 |
-
debug: 'Debug
|
57 |
-
'event-logs': 'View
|
58 |
-
update: 'Check for updates
|
59 |
-
'task-manager': '
|
|
|
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:
|
|
|
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: '
|
79 |
-
{ id: '
|
80 |
-
{ id: '
|
|
|
81 |
|
82 |
// User Window Tabs (Hidden by default)
|
83 |
-
{ id: 'profile', visible: false, window: 'user', order:
|
84 |
-
{ id: 'settings', visible: false, window: 'user', order:
|
85 |
-
{ id: 'notifications', visible: false, window: 'user', order:
|
86 |
-
{ id: 'event-logs', visible: false, window: 'user', order:
|
87 |
-
{ id: 'update', visible: false, window: 'user', order:
|
88 |
-
{ id: 'task-manager', visible: false, window: 'user', order:
|
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: '
|
98 |
-
{ id: '
|
99 |
-
{ id: '
|
100 |
-
{ id: '
|
101 |
-
{ id: '
|
102 |
-
{ id: '
|
|
|
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 |
-
|
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={() =>
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
29 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
30 |
for (const name in shortcuts) {
|
31 |
const shortcut = shortcuts[name as keyof Shortcuts];
|
32 |
|
33 |
-
if (
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
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 |
-
|
|
|
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(), '
|
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';
|