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';
|