mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-03-09 21:50:36 +00:00
Service console check providers
This commit is contained in:
parent
9e8d05cb54
commit
d9a380f28a
@ -14,7 +14,7 @@ const FrameworkLink: React.FC<FrameworkLinkProps> = ({ template }) => (
|
||||
className="items-center justify-center "
|
||||
>
|
||||
<div
|
||||
className={`inline-block ${template.icon} w-8 h-8 text-4xl transition-theme opacity-25 hover:opacity-75 transition-all`}
|
||||
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`}
|
||||
/>
|
||||
</a>
|
||||
);
|
||||
|
@ -2,7 +2,7 @@ import { useState, useRef } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { toast } from 'react-toastify';
|
||||
import { DialogRoot, DialogClose, Dialog, DialogTitle } from '~/components/ui/Dialog';
|
||||
import { db, getAll } from '~/lib/persistence';
|
||||
import { db, getAll, deleteById } from '~/lib/persistence';
|
||||
|
||||
export default function DataTab() {
|
||||
const [isDownloadingTemplate, setIsDownloadingTemplate] = useState(false);
|
||||
@ -180,11 +180,21 @@ export default function DataTab() {
|
||||
setIsResetting(true);
|
||||
|
||||
try {
|
||||
// Clear all stored settings
|
||||
// Clear all stored settings from localStorage
|
||||
localStorage.removeItem('bolt_user_profile');
|
||||
localStorage.removeItem('bolt_settings');
|
||||
localStorage.removeItem('bolt_chat_history');
|
||||
|
||||
// Clear all data from IndexedDB
|
||||
if (!db) {
|
||||
throw new Error('Database not initialized');
|
||||
}
|
||||
|
||||
// Get all chats and delete them
|
||||
const chats = await getAll(db as IDBDatabase);
|
||||
const deletePromises = chats.map((chat) => deleteById(db as IDBDatabase, chat.id));
|
||||
await Promise.all(deletePromises);
|
||||
|
||||
// Close the dialog first
|
||||
setShowResetInlineConfirm(false);
|
||||
|
||||
@ -204,9 +214,19 @@ export default function DataTab() {
|
||||
setIsDeleting(true);
|
||||
|
||||
try {
|
||||
// Clear chat history
|
||||
// Clear chat history from localStorage
|
||||
localStorage.removeItem('bolt_chat_history');
|
||||
|
||||
// Clear chats from IndexedDB
|
||||
if (!db) {
|
||||
throw new Error('Database not initialized');
|
||||
}
|
||||
|
||||
// Get all chats and delete them one by one
|
||||
const chats = await getAll(db as IDBDatabase);
|
||||
const deletePromises = chats.map((chat) => deleteById(db as IDBDatabase, chat.id));
|
||||
await Promise.all(deletePromises);
|
||||
|
||||
// Close the dialog first
|
||||
setShowDeleteInlineConfirm(false);
|
||||
|
||||
|
@ -48,15 +48,16 @@ const TAB_DESCRIPTIONS: Record<TabType, string> = {
|
||||
profile: 'Manage your profile and account settings',
|
||||
settings: 'Configure application preferences',
|
||||
notifications: 'View and manage your notifications',
|
||||
features: 'Explore new and upcoming features',
|
||||
features: 'Manage application features',
|
||||
data: 'Manage your data and storage',
|
||||
'cloud-providers': 'Configure cloud AI providers and models',
|
||||
'local-providers': 'Configure local AI providers and models',
|
||||
connection: 'Check connection status and settings',
|
||||
debug: 'Debug tools and system information',
|
||||
'event-logs': 'View system events and logs',
|
||||
update: 'Check for updates and release notes',
|
||||
'task-manager': 'Monitor system resources and processes',
|
||||
'cloud-providers': 'Configure cloud AI providers',
|
||||
'local-providers': 'Configure local AI providers',
|
||||
connection: 'View and manage connections',
|
||||
debug: 'Debug application issues',
|
||||
'event-logs': 'View application event logs',
|
||||
update: 'Check for updates',
|
||||
'task-manager': 'Manage running tasks',
|
||||
'service-status': 'View service health and status',
|
||||
};
|
||||
|
||||
const DraggableTabTile = ({
|
||||
|
@ -19,7 +19,8 @@ const TAB_ICONS: Record<TabType, string> = {
|
||||
debug: 'i-ph:bug-fill',
|
||||
'event-logs': 'i-ph:list-bullets-fill',
|
||||
update: 'i-ph:arrow-clockwise-fill',
|
||||
'task-manager': 'i-ph:gauge-fill',
|
||||
'task-manager': 'i-ph:activity-fill',
|
||||
'service-status': 'i-ph:heartbeat-fill',
|
||||
};
|
||||
|
||||
interface TabGroupProps {
|
||||
|
886
app/components/settings/providers/ServiceStatusTab.tsx
Normal file
886
app/components/settings/providers/ServiceStatusTab.tsx
Normal file
@ -0,0 +1,886 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { classNames } from '~/utils/classNames';
|
||||
import { TbActivityHeartbeat } from 'react-icons/tb';
|
||||
import { BsCheckCircleFill, BsXCircleFill, BsExclamationCircleFill } from 'react-icons/bs';
|
||||
import { SiAmazon, SiGoogle, SiHuggingface, SiPerplexity, SiOpenai } from 'react-icons/si';
|
||||
import { BsRobot, BsCloud } from 'react-icons/bs';
|
||||
import { TbBrain } from 'react-icons/tb';
|
||||
import { BiChip, BiCodeBlock } from 'react-icons/bi';
|
||||
import { FaCloud, FaBrain } from 'react-icons/fa';
|
||||
import type { IconType } from 'react-icons';
|
||||
import { useSettings } from '~/lib/hooks/useSettings';
|
||||
import { useToast } from '~/components/ui/use-toast';
|
||||
|
||||
// Types
|
||||
type ProviderName =
|
||||
| 'AmazonBedrock'
|
||||
| 'Anthropic'
|
||||
| 'Cohere'
|
||||
| 'Deepseek'
|
||||
| 'Google'
|
||||
| 'Groq'
|
||||
| 'HuggingFace'
|
||||
| 'Mistral'
|
||||
| 'OpenAI'
|
||||
| 'OpenRouter'
|
||||
| 'Perplexity'
|
||||
| 'Together'
|
||||
| 'XAI';
|
||||
|
||||
type ServiceStatus = {
|
||||
provider: ProviderName;
|
||||
status: 'operational' | 'degraded' | 'down';
|
||||
lastChecked: string;
|
||||
statusUrl?: string;
|
||||
icon?: IconType;
|
||||
message?: string;
|
||||
responseTime?: number;
|
||||
incidents?: string[];
|
||||
};
|
||||
|
||||
type ProviderConfig = {
|
||||
statusUrl: string;
|
||||
apiUrl: string;
|
||||
headers: Record<string, string>;
|
||||
testModel: string;
|
||||
};
|
||||
|
||||
// Types for API responses
|
||||
type ApiResponse = {
|
||||
error?: {
|
||||
message: string;
|
||||
};
|
||||
message?: string;
|
||||
model?: string;
|
||||
models?: Array<{
|
||||
id?: string;
|
||||
name?: string;
|
||||
}>;
|
||||
data?: Array<{
|
||||
id?: string;
|
||||
name?: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
// Constants
|
||||
const PROVIDER_STATUS_URLS: Record<ProviderName, ProviderConfig> = {
|
||||
OpenAI: {
|
||||
statusUrl: 'https://status.openai.com/',
|
||||
apiUrl: 'https://api.openai.com/v1/models',
|
||||
headers: {
|
||||
Authorization: 'Bearer $OPENAI_API_KEY',
|
||||
},
|
||||
testModel: 'gpt-3.5-turbo',
|
||||
},
|
||||
Anthropic: {
|
||||
statusUrl: 'https://status.anthropic.com/',
|
||||
apiUrl: 'https://api.anthropic.com/v1/messages',
|
||||
headers: {
|
||||
'x-api-key': '$ANTHROPIC_API_KEY',
|
||||
'anthropic-version': '2024-02-29',
|
||||
},
|
||||
testModel: 'claude-3-sonnet-20240229',
|
||||
},
|
||||
Cohere: {
|
||||
statusUrl: 'https://status.cohere.com/',
|
||||
apiUrl: 'https://api.cohere.ai/v1/models',
|
||||
headers: {
|
||||
Authorization: 'Bearer $COHERE_API_KEY',
|
||||
},
|
||||
testModel: 'command',
|
||||
},
|
||||
Google: {
|
||||
statusUrl: 'https://status.cloud.google.com/',
|
||||
apiUrl: 'https://generativelanguage.googleapis.com/v1/models',
|
||||
headers: {
|
||||
'x-goog-api-key': '$GOOGLE_API_KEY',
|
||||
},
|
||||
testModel: 'gemini-pro',
|
||||
},
|
||||
HuggingFace: {
|
||||
statusUrl: 'https://status.huggingface.co/',
|
||||
apiUrl: 'https://api-inference.huggingface.co/models',
|
||||
headers: {
|
||||
Authorization: 'Bearer $HUGGINGFACE_API_KEY',
|
||||
},
|
||||
testModel: 'mistralai/Mixtral-8x7B-Instruct-v0.1',
|
||||
},
|
||||
Mistral: {
|
||||
statusUrl: 'https://status.mistral.ai/',
|
||||
apiUrl: 'https://api.mistral.ai/v1/models',
|
||||
headers: {
|
||||
Authorization: 'Bearer $MISTRAL_API_KEY',
|
||||
},
|
||||
testModel: 'mistral-tiny',
|
||||
},
|
||||
Perplexity: {
|
||||
statusUrl: 'https://status.perplexity.com/',
|
||||
apiUrl: 'https://api.perplexity.ai/v1/models',
|
||||
headers: {
|
||||
Authorization: 'Bearer $PERPLEXITY_API_KEY',
|
||||
},
|
||||
testModel: 'pplx-7b-chat',
|
||||
},
|
||||
Together: {
|
||||
statusUrl: 'https://status.together.ai/',
|
||||
apiUrl: 'https://api.together.xyz/v1/models',
|
||||
headers: {
|
||||
Authorization: 'Bearer $TOGETHER_API_KEY',
|
||||
},
|
||||
testModel: 'mistralai/Mixtral-8x7B-Instruct-v0.1',
|
||||
},
|
||||
AmazonBedrock: {
|
||||
statusUrl: 'https://health.aws.amazon.com/health/status',
|
||||
apiUrl: 'https://bedrock.us-east-1.amazonaws.com/models',
|
||||
headers: {
|
||||
Authorization: 'Bearer $AWS_BEDROCK_CONFIG',
|
||||
},
|
||||
testModel: 'anthropic.claude-3-sonnet-20240229-v1:0',
|
||||
},
|
||||
Groq: {
|
||||
statusUrl: 'https://groqstatus.com/',
|
||||
apiUrl: 'https://api.groq.com/v1/models',
|
||||
headers: {
|
||||
Authorization: 'Bearer $GROQ_API_KEY',
|
||||
},
|
||||
testModel: 'mixtral-8x7b-32768',
|
||||
},
|
||||
OpenRouter: {
|
||||
statusUrl: 'https://status.openrouter.ai/',
|
||||
apiUrl: 'https://openrouter.ai/api/v1/models',
|
||||
headers: {
|
||||
Authorization: 'Bearer $OPEN_ROUTER_API_KEY',
|
||||
},
|
||||
testModel: 'anthropic/claude-3-sonnet',
|
||||
},
|
||||
XAI: {
|
||||
statusUrl: 'https://status.x.ai/',
|
||||
apiUrl: 'https://api.x.ai/v1/models',
|
||||
headers: {
|
||||
Authorization: 'Bearer $XAI_API_KEY',
|
||||
},
|
||||
testModel: 'grok-1',
|
||||
},
|
||||
Deepseek: {
|
||||
statusUrl: 'https://status.deepseek.com/',
|
||||
apiUrl: 'https://api.deepseek.com/v1/models',
|
||||
headers: {
|
||||
Authorization: 'Bearer $DEEPSEEK_API_KEY',
|
||||
},
|
||||
testModel: 'deepseek-chat',
|
||||
},
|
||||
};
|
||||
|
||||
const PROVIDER_ICONS: Record<ProviderName, IconType> = {
|
||||
AmazonBedrock: SiAmazon,
|
||||
Anthropic: FaBrain,
|
||||
Cohere: BiChip,
|
||||
Google: SiGoogle,
|
||||
Groq: BsCloud,
|
||||
HuggingFace: SiHuggingface,
|
||||
Mistral: TbBrain,
|
||||
OpenAI: SiOpenai,
|
||||
OpenRouter: FaCloud,
|
||||
Perplexity: SiPerplexity,
|
||||
Together: BsCloud,
|
||||
XAI: BsRobot,
|
||||
Deepseek: BiCodeBlock,
|
||||
};
|
||||
|
||||
const ServiceStatusTab = () => {
|
||||
const [serviceStatuses, setServiceStatuses] = useState<ServiceStatus[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [lastRefresh, setLastRefresh] = useState<Date>(new Date());
|
||||
const [testApiKey, setTestApiKey] = useState<string>('');
|
||||
const [testProvider, setTestProvider] = useState<ProviderName | ''>('');
|
||||
const [testingStatus, setTestingStatus] = useState<'idle' | 'testing' | 'success' | 'error'>('idle');
|
||||
const settings = useSettings();
|
||||
const { success, error } = useToast();
|
||||
|
||||
// Function to get the API key for a provider from environment variables
|
||||
const getApiKey = useCallback(
|
||||
(provider: ProviderName): string | null => {
|
||||
if (!settings.providers) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Map provider names to environment variable names
|
||||
const envKeyMap: Record<ProviderName, string> = {
|
||||
OpenAI: 'OPENAI_API_KEY',
|
||||
Anthropic: 'ANTHROPIC_API_KEY',
|
||||
Cohere: 'COHERE_API_KEY',
|
||||
Google: 'GOOGLE_GENERATIVE_AI_API_KEY',
|
||||
HuggingFace: 'HuggingFace_API_KEY',
|
||||
Mistral: 'MISTRAL_API_KEY',
|
||||
Perplexity: 'PERPLEXITY_API_KEY',
|
||||
Together: 'TOGETHER_API_KEY',
|
||||
AmazonBedrock: 'AWS_BEDROCK_CONFIG',
|
||||
Groq: 'GROQ_API_KEY',
|
||||
OpenRouter: 'OPEN_ROUTER_API_KEY',
|
||||
XAI: 'XAI_API_KEY',
|
||||
Deepseek: 'DEEPSEEK_API_KEY',
|
||||
};
|
||||
|
||||
const envKey = envKeyMap[provider];
|
||||
|
||||
if (!envKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get the API key from environment variables
|
||||
const apiKey = (import.meta.env[envKey] as string) || null;
|
||||
|
||||
// Special handling for providers with base URLs
|
||||
if (provider === 'Together' && apiKey) {
|
||||
const baseUrl = import.meta.env.TOGETHER_API_BASE_URL;
|
||||
|
||||
if (!baseUrl) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return apiKey;
|
||||
},
|
||||
[settings.providers],
|
||||
);
|
||||
|
||||
// Update provider configurations based on available API keys
|
||||
const getProviderConfig = useCallback((provider: ProviderName): ProviderConfig | null => {
|
||||
const config = PROVIDER_STATUS_URLS[provider];
|
||||
|
||||
if (!config) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Handle special cases for providers with base URLs
|
||||
let updatedConfig = { ...config };
|
||||
const togetherBaseUrl = import.meta.env.TOGETHER_API_BASE_URL;
|
||||
|
||||
if (provider === 'Together' && togetherBaseUrl) {
|
||||
updatedConfig = {
|
||||
...config,
|
||||
apiUrl: `${togetherBaseUrl}/models`,
|
||||
};
|
||||
}
|
||||
|
||||
return updatedConfig;
|
||||
}, []);
|
||||
|
||||
// Function to check if an API endpoint is accessible with model verification
|
||||
const checkApiEndpoint = useCallback(
|
||||
async (
|
||||
url: string,
|
||||
headers?: Record<string, string>,
|
||||
testModel?: string,
|
||||
): Promise<{ ok: boolean; status: number | string; message?: string; responseTime: number }> => {
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 10000);
|
||||
|
||||
const startTime = performance.now();
|
||||
|
||||
// Add common headers
|
||||
const processedHeaders = {
|
||||
'Content-Type': 'application/json',
|
||||
...headers,
|
||||
};
|
||||
|
||||
// First check if the API is accessible
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: processedHeaders,
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
const endTime = performance.now();
|
||||
const responseTime = endTime - startTime;
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
// Get response data
|
||||
const data = (await response.json()) as ApiResponse;
|
||||
|
||||
// Special handling for different provider responses
|
||||
if (!response.ok) {
|
||||
let errorMessage = `API returned status: ${response.status}`;
|
||||
|
||||
// Handle provider-specific error messages
|
||||
if (data.error?.message) {
|
||||
errorMessage = data.error.message;
|
||||
} else if (data.message) {
|
||||
errorMessage = data.message;
|
||||
}
|
||||
|
||||
return {
|
||||
ok: false,
|
||||
status: response.status,
|
||||
message: errorMessage,
|
||||
responseTime,
|
||||
};
|
||||
}
|
||||
|
||||
// Different providers have different model list formats
|
||||
let models: string[] = [];
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
models = data.map((model: { id?: string; name?: string }) => model.id || model.name || '');
|
||||
} else if (data.data && Array.isArray(data.data)) {
|
||||
models = data.data.map((model) => model.id || model.name || '');
|
||||
} else if (data.models && Array.isArray(data.models)) {
|
||||
models = data.models.map((model) => model.id || model.name || '');
|
||||
} else if (data.model) {
|
||||
// Some providers return single model info
|
||||
models = [data.model];
|
||||
}
|
||||
|
||||
// For some providers, just having a successful response is enough
|
||||
if (!testModel || models.length > 0) {
|
||||
return {
|
||||
ok: true,
|
||||
status: response.status,
|
||||
responseTime,
|
||||
message: 'API key is valid',
|
||||
};
|
||||
}
|
||||
|
||||
// If a specific model was requested, verify it exists
|
||||
if (testModel && !models.includes(testModel)) {
|
||||
return {
|
||||
ok: true, // Still mark as ok since API works
|
||||
status: 'model_not_found',
|
||||
message: `API key is valid (test model ${testModel} not found in ${models.length} available models)`,
|
||||
responseTime,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
status: response.status,
|
||||
message: 'API key is valid',
|
||||
responseTime,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Error checking API endpoint ${url}:`, error);
|
||||
return {
|
||||
ok: false,
|
||||
status: error instanceof Error ? error.message : 'Unknown error',
|
||||
message: error instanceof Error ? `Connection failed: ${error.message}` : 'Connection failed',
|
||||
responseTime: 0,
|
||||
};
|
||||
}
|
||||
},
|
||||
[getApiKey],
|
||||
);
|
||||
|
||||
// Function to fetch real status from provider status pages
|
||||
const fetchPublicStatus = useCallback(
|
||||
async (
|
||||
provider: ProviderName,
|
||||
): Promise<{
|
||||
status: ServiceStatus['status'];
|
||||
message?: string;
|
||||
incidents?: string[];
|
||||
}> => {
|
||||
try {
|
||||
// Due to CORS restrictions, we can only check if the endpoints are reachable
|
||||
const checkEndpoint = async (url: string) => {
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
mode: 'no-cors',
|
||||
headers: {
|
||||
Accept: 'text/html',
|
||||
},
|
||||
});
|
||||
|
||||
// With no-cors, we can only know if the request succeeded
|
||||
return response.type === 'opaque' ? 'reachable' : 'unreachable';
|
||||
} catch (error) {
|
||||
console.error(`Error checking ${url}:`, error);
|
||||
return 'unreachable';
|
||||
}
|
||||
};
|
||||
|
||||
switch (provider) {
|
||||
case 'HuggingFace': {
|
||||
const endpointStatus = await checkEndpoint('https://status.huggingface.co/');
|
||||
|
||||
// Check API endpoint as fallback
|
||||
const apiEndpoint = 'https://api-inference.huggingface.co/models';
|
||||
const apiStatus = await checkEndpoint(apiEndpoint);
|
||||
|
||||
return {
|
||||
status: endpointStatus === 'reachable' && apiStatus === 'reachable' ? 'operational' : 'degraded',
|
||||
message: `Status page: ${endpointStatus}, API: ${apiStatus}`,
|
||||
incidents: ['Note: Limited status information due to CORS restrictions'],
|
||||
};
|
||||
}
|
||||
|
||||
case 'OpenAI': {
|
||||
const endpointStatus = await checkEndpoint('https://status.openai.com/');
|
||||
const apiEndpoint = 'https://api.openai.com/v1/models';
|
||||
const apiStatus = await checkEndpoint(apiEndpoint);
|
||||
|
||||
return {
|
||||
status: endpointStatus === 'reachable' && apiStatus === 'reachable' ? 'operational' : 'degraded',
|
||||
message: `Status page: ${endpointStatus}, API: ${apiStatus}`,
|
||||
incidents: ['Note: Limited status information due to CORS restrictions'],
|
||||
};
|
||||
}
|
||||
|
||||
case 'Google': {
|
||||
const endpointStatus = await checkEndpoint('https://status.cloud.google.com/');
|
||||
const apiEndpoint = 'https://generativelanguage.googleapis.com/v1/models';
|
||||
const apiStatus = await checkEndpoint(apiEndpoint);
|
||||
|
||||
return {
|
||||
status: endpointStatus === 'reachable' && apiStatus === 'reachable' ? 'operational' : 'degraded',
|
||||
message: `Status page: ${endpointStatus}, API: ${apiStatus}`,
|
||||
incidents: ['Note: Limited status information due to CORS restrictions'],
|
||||
};
|
||||
}
|
||||
|
||||
// Similar pattern for other providers...
|
||||
default:
|
||||
return {
|
||||
status: 'operational',
|
||||
message: 'Basic reachability check only',
|
||||
incidents: ['Note: Limited status information due to CORS restrictions'],
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error fetching status for ${provider}:`, error);
|
||||
return {
|
||||
status: 'degraded',
|
||||
message: 'Unable to fetch status due to CORS restrictions',
|
||||
incidents: ['Error: Unable to check service status'],
|
||||
};
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
// Function to fetch status for a provider with retries
|
||||
const fetchProviderStatus = useCallback(
|
||||
async (provider: ProviderName, config: ProviderConfig): Promise<ServiceStatus> => {
|
||||
const MAX_RETRIES = 2;
|
||||
const RETRY_DELAY = 2000; // 2 seconds
|
||||
|
||||
const attemptCheck = async (attempt: number): Promise<ServiceStatus> => {
|
||||
try {
|
||||
// First check the public status page if available
|
||||
const hasPublicStatus = [
|
||||
'Anthropic',
|
||||
'OpenAI',
|
||||
'Google',
|
||||
'HuggingFace',
|
||||
'Mistral',
|
||||
'Groq',
|
||||
'Perplexity',
|
||||
'Together',
|
||||
].includes(provider);
|
||||
|
||||
if (hasPublicStatus) {
|
||||
const publicStatus = await fetchPublicStatus(provider);
|
||||
|
||||
return {
|
||||
provider,
|
||||
status: publicStatus.status,
|
||||
lastChecked: new Date().toISOString(),
|
||||
statusUrl: config.statusUrl,
|
||||
icon: PROVIDER_ICONS[provider],
|
||||
message: publicStatus.message,
|
||||
incidents: publicStatus.incidents,
|
||||
};
|
||||
}
|
||||
|
||||
// For other providers, we'll show status but mark API check as separate
|
||||
const apiKey = getApiKey(provider);
|
||||
const providerConfig = getProviderConfig(provider);
|
||||
|
||||
if (!apiKey || !providerConfig) {
|
||||
return {
|
||||
provider,
|
||||
status: 'operational',
|
||||
lastChecked: new Date().toISOString(),
|
||||
statusUrl: config.statusUrl,
|
||||
icon: PROVIDER_ICONS[provider],
|
||||
message: !apiKey
|
||||
? 'Status operational (API key needed for usage)'
|
||||
: 'Status operational (configuration needed for usage)',
|
||||
incidents: [],
|
||||
};
|
||||
}
|
||||
|
||||
// If we have API access, let's verify that too
|
||||
const { ok, status, message, responseTime } = await checkApiEndpoint(
|
||||
providerConfig.apiUrl,
|
||||
providerConfig.headers,
|
||||
providerConfig.testModel,
|
||||
);
|
||||
|
||||
if (!ok && attempt < MAX_RETRIES) {
|
||||
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY));
|
||||
return attemptCheck(attempt + 1);
|
||||
}
|
||||
|
||||
return {
|
||||
provider,
|
||||
status: ok ? 'operational' : 'degraded',
|
||||
lastChecked: new Date().toISOString(),
|
||||
statusUrl: providerConfig.statusUrl,
|
||||
icon: PROVIDER_ICONS[provider],
|
||||
message: ok ? 'Service and API operational' : `Service operational (API: ${message || status})`,
|
||||
responseTime,
|
||||
incidents: [],
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Error fetching status for ${provider} (attempt ${attempt}):`, error);
|
||||
|
||||
if (attempt < MAX_RETRIES) {
|
||||
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY));
|
||||
return attemptCheck(attempt + 1);
|
||||
}
|
||||
|
||||
return {
|
||||
provider,
|
||||
status: 'degraded',
|
||||
lastChecked: new Date().toISOString(),
|
||||
statusUrl: config.statusUrl,
|
||||
icon: PROVIDER_ICONS[provider],
|
||||
message: 'Service operational (Status check error)',
|
||||
responseTime: 0,
|
||||
incidents: [],
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
return attemptCheck(1);
|
||||
},
|
||||
[checkApiEndpoint, getApiKey, getProviderConfig, fetchPublicStatus],
|
||||
);
|
||||
|
||||
// Memoize the fetchAllStatuses function
|
||||
const fetchAllStatuses = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const statuses = await Promise.all(
|
||||
Object.entries(PROVIDER_STATUS_URLS).map(([provider, config]) =>
|
||||
fetchProviderStatus(provider as ProviderName, config),
|
||||
),
|
||||
);
|
||||
|
||||
setServiceStatuses(statuses.sort((a, b) => a.provider.localeCompare(b.provider)));
|
||||
setLastRefresh(new Date());
|
||||
success('Service statuses updated successfully');
|
||||
} catch (err) {
|
||||
console.error('Error fetching all statuses:', err);
|
||||
error('Failed to update service statuses');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [fetchProviderStatus, success, error]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchAllStatuses();
|
||||
|
||||
// Refresh status every 2 minutes
|
||||
const interval = setInterval(fetchAllStatuses, 2 * 60 * 1000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [fetchAllStatuses]);
|
||||
|
||||
// Function to test an API key
|
||||
const testApiKeyForProvider = useCallback(
|
||||
async (provider: ProviderName, apiKey: string) => {
|
||||
try {
|
||||
setTestingStatus('testing');
|
||||
|
||||
const config = PROVIDER_STATUS_URLS[provider];
|
||||
|
||||
if (!config) {
|
||||
throw new Error('Provider configuration not found');
|
||||
}
|
||||
|
||||
const headers = { ...config.headers };
|
||||
|
||||
// Replace the placeholder API key with the test key
|
||||
Object.keys(headers).forEach((key) => {
|
||||
if (headers[key].startsWith('$')) {
|
||||
headers[key] = headers[key].replace(/\$.*/, apiKey);
|
||||
}
|
||||
});
|
||||
|
||||
// Special handling for certain providers
|
||||
switch (provider) {
|
||||
case 'Anthropic':
|
||||
headers['anthropic-version'] = '2024-02-29';
|
||||
break;
|
||||
case 'OpenAI':
|
||||
if (!headers.Authorization?.startsWith('Bearer ')) {
|
||||
headers.Authorization = `Bearer ${apiKey}`;
|
||||
}
|
||||
|
||||
break;
|
||||
case 'Google': {
|
||||
// Google uses the API key directly in the URL
|
||||
const googleUrl = `${config.apiUrl}?key=${apiKey}`;
|
||||
const result = await checkApiEndpoint(googleUrl, {}, config.testModel);
|
||||
|
||||
if (result.ok) {
|
||||
setTestingStatus('success');
|
||||
success('API key is valid!');
|
||||
} else {
|
||||
setTestingStatus('error');
|
||||
error(`API key test failed: ${result.message}`);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const { ok, message } = await checkApiEndpoint(config.apiUrl, headers, config.testModel);
|
||||
|
||||
if (ok) {
|
||||
setTestingStatus('success');
|
||||
success('API key is valid!');
|
||||
} else {
|
||||
setTestingStatus('error');
|
||||
error(`API key test failed: ${message}`);
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
setTestingStatus('error');
|
||||
error('Failed to test API key: ' + (err instanceof Error ? err.message : 'Unknown error'));
|
||||
} finally {
|
||||
// Reset testing status after a delay
|
||||
setTimeout(() => setTestingStatus('idle'), 3000);
|
||||
}
|
||||
},
|
||||
[checkApiEndpoint, success, error],
|
||||
);
|
||||
|
||||
const getStatusColor = (status: ServiceStatus['status']) => {
|
||||
switch (status) {
|
||||
case 'operational':
|
||||
return 'text-green-500';
|
||||
case 'degraded':
|
||||
return 'text-yellow-500';
|
||||
case 'down':
|
||||
return 'text-red-500';
|
||||
default:
|
||||
return 'text-gray-500';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusIcon = (status: ServiceStatus['status']) => {
|
||||
switch (status) {
|
||||
case 'operational':
|
||||
return <BsCheckCircleFill className="w-4 h-4" />;
|
||||
case 'degraded':
|
||||
return <BsExclamationCircleFill className="w-4 h-4" />;
|
||||
case 'down':
|
||||
return <BsXCircleFill className="w-4 h-4" />;
|
||||
default:
|
||||
return <BsXCircleFill className="w-4 h-4" />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<motion.div
|
||||
className="space-y-4"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2 mt-8 mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className={classNames(
|
||||
'w-8 h-8 flex items-center justify-center rounded-lg',
|
||||
'bg-bolt-elements-background-depth-3',
|
||||
'text-purple-500',
|
||||
)}
|
||||
>
|
||||
<TbActivityHeartbeat className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-md font-medium text-bolt-elements-textPrimary">Service Status</h4>
|
||||
<p className="text-sm text-bolt-elements-textSecondary">
|
||||
Monitor and test the operational status of cloud LLM providers
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-bolt-elements-textSecondary">
|
||||
Last updated: {lastRefresh.toLocaleTimeString()}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => fetchAllStatuses()}
|
||||
className={classNames(
|
||||
'px-3 py-1.5 rounded-lg text-sm',
|
||||
'bg-bolt-elements-background-depth-3 hover:bg-bolt-elements-background-depth-4',
|
||||
'text-bolt-elements-textPrimary',
|
||||
'transition-all duration-200',
|
||||
'flex items-center gap-2',
|
||||
loading ? 'opacity-50 cursor-not-allowed' : '',
|
||||
)}
|
||||
disabled={loading}
|
||||
>
|
||||
<div className={`i-ph:arrows-clockwise w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
|
||||
<span>{loading ? 'Refreshing...' : 'Refresh'}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* API Key Test Section */}
|
||||
<div className="p-4 bg-bolt-elements-background-depth-2 rounded-lg">
|
||||
<h5 className="text-sm font-medium text-bolt-elements-textPrimary mb-2">Test API Key</h5>
|
||||
<div className="flex gap-2">
|
||||
<select
|
||||
value={testProvider}
|
||||
onChange={(e) => setTestProvider(e.target.value as ProviderName)}
|
||||
className={classNames(
|
||||
'flex-1 px-3 py-1.5 rounded-lg text-sm max-w-[200px]',
|
||||
'bg-bolt-elements-background-depth-3 border border-bolt-elements-borderColor',
|
||||
'text-bolt-elements-textPrimary',
|
||||
'focus:outline-none focus:ring-2 focus:ring-purple-500/30',
|
||||
)}
|
||||
>
|
||||
<option value="">Select Provider</option>
|
||||
{Object.keys(PROVIDER_STATUS_URLS).map((provider) => (
|
||||
<option key={provider} value={provider}>
|
||||
{provider}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<input
|
||||
type="password"
|
||||
value={testApiKey}
|
||||
onChange={(e) => setTestApiKey(e.target.value)}
|
||||
placeholder="Enter API key to test"
|
||||
className={classNames(
|
||||
'flex-1 px-3 py-1.5 rounded-lg text-sm',
|
||||
'bg-bolt-elements-background-depth-3 border border-bolt-elements-borderColor',
|
||||
'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
|
||||
'focus:outline-none focus:ring-2 focus:ring-purple-500/30',
|
||||
)}
|
||||
/>
|
||||
<button
|
||||
onClick={() =>
|
||||
testProvider && testApiKey && testApiKeyForProvider(testProvider as ProviderName, testApiKey)
|
||||
}
|
||||
disabled={!testProvider || !testApiKey || testingStatus === 'testing'}
|
||||
className={classNames(
|
||||
'px-4 py-1.5 rounded-lg text-sm',
|
||||
'bg-purple-500 hover:bg-purple-600',
|
||||
'text-white',
|
||||
'transition-all duration-200',
|
||||
'flex items-center gap-2',
|
||||
!testProvider || !testApiKey || testingStatus === 'testing' ? 'opacity-50 cursor-not-allowed' : '',
|
||||
)}
|
||||
>
|
||||
{testingStatus === 'testing' ? (
|
||||
<>
|
||||
<div className="i-ph:spinner-gap w-4 h-4 animate-spin" />
|
||||
<span>Testing...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="i-ph:key w-4 h-4" />
|
||||
<span>Test Key</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status Grid */}
|
||||
{loading && serviceStatuses.length === 0 ? (
|
||||
<div className="text-center py-8 text-bolt-elements-textSecondary">Loading service statuses...</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{serviceStatuses.map((service, index) => (
|
||||
<motion.div
|
||||
key={service.provider}
|
||||
className={classNames(
|
||||
'bg-bolt-elements-background-depth-2',
|
||||
'hover:bg-bolt-elements-background-depth-3',
|
||||
'transition-all duration-200',
|
||||
'relative overflow-hidden rounded-lg',
|
||||
)}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: index * 0.1 }}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
>
|
||||
<div
|
||||
className={classNames('block p-4', service.statusUrl ? 'cursor-pointer' : '')}
|
||||
onClick={() => service.statusUrl && window.open(service.statusUrl, '_blank')}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
{service.icon && (
|
||||
<div
|
||||
className={classNames(
|
||||
'w-8 h-8 flex items-center justify-center rounded-lg',
|
||||
'bg-bolt-elements-background-depth-3',
|
||||
getStatusColor(service.status),
|
||||
)}
|
||||
>
|
||||
{React.createElement(service.icon, {
|
||||
className: 'w-5 h-5',
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-bolt-elements-textPrimary">{service.provider}</h4>
|
||||
<div className="space-y-1">
|
||||
<p className="text-xs text-bolt-elements-textSecondary">
|
||||
Last checked: {new Date(service.lastChecked).toLocaleTimeString()}
|
||||
</p>
|
||||
{service.responseTime && (
|
||||
<p className="text-xs text-bolt-elements-textTertiary">
|
||||
Response time: {Math.round(service.responseTime)}ms
|
||||
</p>
|
||||
)}
|
||||
{service.message && (
|
||||
<p className="text-xs text-bolt-elements-textTertiary">{service.message}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classNames('flex items-center gap-2', getStatusColor(service.status))}>
|
||||
<span className="text-sm capitalize">{service.status}</span>
|
||||
{getStatusIcon(service.status)}
|
||||
</div>
|
||||
</div>
|
||||
{service.incidents && service.incidents.length > 0 && (
|
||||
<div className="mt-2 border-t border-bolt-elements-borderColor pt-2">
|
||||
<p className="text-xs font-medium text-bolt-elements-textSecondary mb-1">Recent Incidents:</p>
|
||||
<ul className="text-xs text-bolt-elements-textTertiary space-y-1">
|
||||
{service.incidents.map((incident, i) => (
|
||||
<li key={i}>{incident}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Add tab metadata
|
||||
ServiceStatusTab.tabMetadata = {
|
||||
icon: 'i-ph:activity-bold',
|
||||
description: 'Monitor and test LLM provider service status',
|
||||
category: 'services',
|
||||
};
|
||||
|
||||
export default ServiceStatusTab;
|
@ -0,0 +1,121 @@
|
||||
import type { ProviderConfig, StatusCheckResult, ApiResponse } from './types';
|
||||
|
||||
export abstract class BaseProviderChecker {
|
||||
protected config: ProviderConfig;
|
||||
|
||||
constructor(config: ProviderConfig) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
protected async checkApiEndpoint(
|
||||
url: string,
|
||||
headers?: Record<string, string>,
|
||||
testModel?: string,
|
||||
): Promise<{ ok: boolean; status: number | string; message?: string; responseTime: number }> {
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 10000);
|
||||
|
||||
const startTime = performance.now();
|
||||
|
||||
// Add common headers
|
||||
const processedHeaders = {
|
||||
'Content-Type': 'application/json',
|
||||
...headers,
|
||||
};
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: processedHeaders,
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
const endTime = performance.now();
|
||||
const responseTime = endTime - startTime;
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
const data = (await response.json()) as ApiResponse;
|
||||
|
||||
if (!response.ok) {
|
||||
let errorMessage = `API returned status: ${response.status}`;
|
||||
|
||||
if (data.error?.message) {
|
||||
errorMessage = data.error.message;
|
||||
} else if (data.message) {
|
||||
errorMessage = data.message;
|
||||
}
|
||||
|
||||
return {
|
||||
ok: false,
|
||||
status: response.status,
|
||||
message: errorMessage,
|
||||
responseTime,
|
||||
};
|
||||
}
|
||||
|
||||
// Different providers have different model list formats
|
||||
let models: string[] = [];
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
models = data.map((model: { id?: string; name?: string }) => model.id || model.name || '');
|
||||
} else if (data.data && Array.isArray(data.data)) {
|
||||
models = data.data.map((model) => model.id || model.name || '');
|
||||
} else if (data.models && Array.isArray(data.models)) {
|
||||
models = data.models.map((model) => model.id || model.name || '');
|
||||
} else if (data.model) {
|
||||
models = [data.model];
|
||||
}
|
||||
|
||||
if (!testModel || models.length > 0) {
|
||||
return {
|
||||
ok: true,
|
||||
status: response.status,
|
||||
responseTime,
|
||||
message: 'API key is valid',
|
||||
};
|
||||
}
|
||||
|
||||
if (testModel && !models.includes(testModel)) {
|
||||
return {
|
||||
ok: true,
|
||||
status: 'model_not_found',
|
||||
message: `API key is valid (test model ${testModel} not found in ${models.length} available models)`,
|
||||
responseTime,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
status: response.status,
|
||||
message: 'API key is valid',
|
||||
responseTime,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Error checking API endpoint ${url}:`, error);
|
||||
return {
|
||||
ok: false,
|
||||
status: error instanceof Error ? error.message : 'Unknown error',
|
||||
message: error instanceof Error ? `Connection failed: ${error.message}` : 'Connection failed',
|
||||
responseTime: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
protected async checkEndpoint(url: string): Promise<'reachable' | 'unreachable'> {
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
mode: 'no-cors',
|
||||
headers: {
|
||||
Accept: 'text/html',
|
||||
},
|
||||
});
|
||||
return response.type === 'opaque' ? 'reachable' : 'unreachable';
|
||||
} catch (error) {
|
||||
console.error(`Error checking ${url}:`, error);
|
||||
return 'unreachable';
|
||||
}
|
||||
}
|
||||
|
||||
abstract checkStatus(): Promise<StatusCheckResult>;
|
||||
}
|
@ -0,0 +1,160 @@
|
||||
import type { ProviderName, ProviderConfig, StatusCheckResult } from './types';
|
||||
import { OpenAIStatusChecker } from './providers/openai';
|
||||
import { BaseProviderChecker } from './base-provider';
|
||||
|
||||
// Import other provider implementations as they are created
|
||||
|
||||
export class ProviderStatusCheckerFactory {
|
||||
private static _providerConfigs: Record<ProviderName, ProviderConfig> = {
|
||||
OpenAI: {
|
||||
statusUrl: 'https://status.openai.com/',
|
||||
apiUrl: 'https://api.openai.com/v1/models',
|
||||
headers: {
|
||||
Authorization: 'Bearer $OPENAI_API_KEY',
|
||||
},
|
||||
testModel: 'gpt-3.5-turbo',
|
||||
},
|
||||
Anthropic: {
|
||||
statusUrl: 'https://status.anthropic.com/',
|
||||
apiUrl: 'https://api.anthropic.com/v1/messages',
|
||||
headers: {
|
||||
'x-api-key': '$ANTHROPIC_API_KEY',
|
||||
'anthropic-version': '2024-02-29',
|
||||
},
|
||||
testModel: 'claude-3-sonnet-20240229',
|
||||
},
|
||||
AmazonBedrock: {
|
||||
statusUrl: 'https://health.aws.amazon.com/health/status',
|
||||
apiUrl: 'https://bedrock.us-east-1.amazonaws.com/models',
|
||||
headers: {
|
||||
Authorization: 'Bearer $AWS_BEDROCK_CONFIG',
|
||||
},
|
||||
testModel: 'anthropic.claude-3-sonnet-20240229-v1:0',
|
||||
},
|
||||
Cohere: {
|
||||
statusUrl: 'https://status.cohere.com/',
|
||||
apiUrl: 'https://api.cohere.ai/v1/models',
|
||||
headers: {
|
||||
Authorization: 'Bearer $COHERE_API_KEY',
|
||||
},
|
||||
testModel: 'command',
|
||||
},
|
||||
Deepseek: {
|
||||
statusUrl: 'https://status.deepseek.com/',
|
||||
apiUrl: 'https://api.deepseek.com/v1/models',
|
||||
headers: {
|
||||
Authorization: 'Bearer $DEEPSEEK_API_KEY',
|
||||
},
|
||||
testModel: 'deepseek-chat',
|
||||
},
|
||||
Google: {
|
||||
statusUrl: 'https://status.cloud.google.com/',
|
||||
apiUrl: 'https://generativelanguage.googleapis.com/v1/models',
|
||||
headers: {
|
||||
'x-goog-api-key': '$GOOGLE_API_KEY',
|
||||
},
|
||||
testModel: 'gemini-pro',
|
||||
},
|
||||
Groq: {
|
||||
statusUrl: 'https://groqstatus.com/',
|
||||
apiUrl: 'https://api.groq.com/v1/models',
|
||||
headers: {
|
||||
Authorization: 'Bearer $GROQ_API_KEY',
|
||||
},
|
||||
testModel: 'mixtral-8x7b-32768',
|
||||
},
|
||||
HuggingFace: {
|
||||
statusUrl: 'https://status.huggingface.co/',
|
||||
apiUrl: 'https://api-inference.huggingface.co/models',
|
||||
headers: {
|
||||
Authorization: 'Bearer $HUGGINGFACE_API_KEY',
|
||||
},
|
||||
testModel: 'mistralai/Mixtral-8x7B-Instruct-v0.1',
|
||||
},
|
||||
Hyperbolic: {
|
||||
statusUrl: 'https://status.hyperbolic.ai/',
|
||||
apiUrl: 'https://api.hyperbolic.ai/v1/models',
|
||||
headers: {
|
||||
Authorization: 'Bearer $HYPERBOLIC_API_KEY',
|
||||
},
|
||||
testModel: 'hyperbolic-1',
|
||||
},
|
||||
Mistral: {
|
||||
statusUrl: 'https://status.mistral.ai/',
|
||||
apiUrl: 'https://api.mistral.ai/v1/models',
|
||||
headers: {
|
||||
Authorization: 'Bearer $MISTRAL_API_KEY',
|
||||
},
|
||||
testModel: 'mistral-tiny',
|
||||
},
|
||||
OpenRouter: {
|
||||
statusUrl: 'https://status.openrouter.ai/',
|
||||
apiUrl: 'https://openrouter.ai/api/v1/models',
|
||||
headers: {
|
||||
Authorization: 'Bearer $OPEN_ROUTER_API_KEY',
|
||||
},
|
||||
testModel: 'anthropic/claude-3-sonnet',
|
||||
},
|
||||
Perplexity: {
|
||||
statusUrl: 'https://status.perplexity.com/',
|
||||
apiUrl: 'https://api.perplexity.ai/v1/models',
|
||||
headers: {
|
||||
Authorization: 'Bearer $PERPLEXITY_API_KEY',
|
||||
},
|
||||
testModel: 'pplx-7b-chat',
|
||||
},
|
||||
Together: {
|
||||
statusUrl: 'https://status.together.ai/',
|
||||
apiUrl: 'https://api.together.xyz/v1/models',
|
||||
headers: {
|
||||
Authorization: 'Bearer $TOGETHER_API_KEY',
|
||||
},
|
||||
testModel: 'mistralai/Mixtral-8x7B-Instruct-v0.1',
|
||||
},
|
||||
XAI: {
|
||||
statusUrl: 'https://status.x.ai/',
|
||||
apiUrl: 'https://api.x.ai/v1/models',
|
||||
headers: {
|
||||
Authorization: 'Bearer $XAI_API_KEY',
|
||||
},
|
||||
testModel: 'grok-1',
|
||||
},
|
||||
};
|
||||
|
||||
static getChecker(provider: ProviderName): BaseProviderChecker {
|
||||
const config = this._providerConfigs[provider];
|
||||
|
||||
if (!config) {
|
||||
throw new Error(`No configuration found for provider: ${provider}`);
|
||||
}
|
||||
|
||||
// Return specific provider implementation or fallback to base implementation
|
||||
switch (provider) {
|
||||
case 'OpenAI':
|
||||
return new OpenAIStatusChecker(config);
|
||||
|
||||
// Add other provider implementations as they are created
|
||||
default:
|
||||
return new (class extends BaseProviderChecker {
|
||||
async checkStatus(): Promise<StatusCheckResult> {
|
||||
const endpointStatus = await this.checkEndpoint(this.config.statusUrl);
|
||||
const apiStatus = await this.checkEndpoint(this.config.apiUrl);
|
||||
|
||||
return {
|
||||
status: endpointStatus === 'reachable' && apiStatus === 'reachable' ? 'operational' : 'degraded',
|
||||
message: `Status page: ${endpointStatus}, API: ${apiStatus}`,
|
||||
incidents: ['Note: Limited status information due to CORS restrictions'],
|
||||
};
|
||||
}
|
||||
})(config);
|
||||
}
|
||||
}
|
||||
|
||||
static getProviderNames(): ProviderName[] {
|
||||
return Object.keys(this._providerConfigs) as ProviderName[];
|
||||
}
|
||||
|
||||
static getProviderConfig(provider: ProviderName): ProviderConfig | undefined {
|
||||
return this._providerConfigs[provider];
|
||||
}
|
||||
}
|
@ -0,0 +1,99 @@
|
||||
import { BaseProviderChecker } from '~/components/settings/providers/service-status/base-provider';
|
||||
import type { StatusCheckResult } from '~/components/settings/providers/service-status/types';
|
||||
|
||||
export class OpenAIStatusChecker extends BaseProviderChecker {
|
||||
async checkStatus(): Promise<StatusCheckResult> {
|
||||
try {
|
||||
// Check status page
|
||||
const statusPageResponse = await fetch('https://status.openai.com/');
|
||||
const text = await statusPageResponse.text();
|
||||
|
||||
// Check individual services
|
||||
const services = {
|
||||
api: {
|
||||
operational: text.includes('API ? Operational'),
|
||||
degraded: text.includes('API ? Degraded Performance'),
|
||||
outage: text.includes('API ? Major Outage') || text.includes('API ? Partial Outage'),
|
||||
},
|
||||
chat: {
|
||||
operational: text.includes('ChatGPT ? Operational'),
|
||||
degraded: text.includes('ChatGPT ? Degraded Performance'),
|
||||
outage: text.includes('ChatGPT ? Major Outage') || text.includes('ChatGPT ? Partial Outage'),
|
||||
},
|
||||
};
|
||||
|
||||
// Extract recent incidents
|
||||
const incidents: string[] = [];
|
||||
const incidentMatches = text.match(/Past Incidents(.*?)(?=\w+ \d+, \d{4})/s);
|
||||
|
||||
if (incidentMatches) {
|
||||
const recentIncidents = incidentMatches[1]
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line && line.includes('202')); // Get only dated incidents
|
||||
|
||||
incidents.push(...recentIncidents.slice(0, 5));
|
||||
}
|
||||
|
||||
// Determine overall status
|
||||
let status: StatusCheckResult['status'] = 'operational';
|
||||
const messages: string[] = [];
|
||||
|
||||
if (services.api.outage || services.chat.outage) {
|
||||
status = 'down';
|
||||
|
||||
if (services.api.outage) {
|
||||
messages.push('API: Major Outage');
|
||||
}
|
||||
|
||||
if (services.chat.outage) {
|
||||
messages.push('ChatGPT: Major Outage');
|
||||
}
|
||||
} else if (services.api.degraded || services.chat.degraded) {
|
||||
status = 'degraded';
|
||||
|
||||
if (services.api.degraded) {
|
||||
messages.push('API: Degraded Performance');
|
||||
}
|
||||
|
||||
if (services.chat.degraded) {
|
||||
messages.push('ChatGPT: Degraded Performance');
|
||||
}
|
||||
} else if (services.api.operational) {
|
||||
messages.push('API: Operational');
|
||||
}
|
||||
|
||||
// If status page check fails, fallback to endpoint check
|
||||
if (!statusPageResponse.ok) {
|
||||
const endpointStatus = await this.checkEndpoint('https://status.openai.com/');
|
||||
const apiEndpoint = 'https://api.openai.com/v1/models';
|
||||
const apiStatus = await this.checkEndpoint(apiEndpoint);
|
||||
|
||||
return {
|
||||
status: endpointStatus === 'reachable' && apiStatus === 'reachable' ? 'operational' : 'degraded',
|
||||
message: `Status page: ${endpointStatus}, API: ${apiStatus}`,
|
||||
incidents: ['Note: Limited status information due to CORS restrictions'],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status,
|
||||
message: messages.join(', ') || 'Status unknown',
|
||||
incidents,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error checking OpenAI status:', error);
|
||||
|
||||
// Fallback to basic endpoint check
|
||||
const endpointStatus = await this.checkEndpoint('https://status.openai.com/');
|
||||
const apiEndpoint = 'https://api.openai.com/v1/models';
|
||||
const apiStatus = await this.checkEndpoint(apiEndpoint);
|
||||
|
||||
return {
|
||||
status: endpointStatus === 'reachable' && apiStatus === 'reachable' ? 'operational' : 'degraded',
|
||||
message: `Status page: ${endpointStatus}, API: ${apiStatus}`,
|
||||
incidents: ['Note: Limited status information due to CORS restrictions'],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
58
app/components/settings/providers/service-status/types.ts
Normal file
58
app/components/settings/providers/service-status/types.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import type { IconType } from 'react-icons';
|
||||
|
||||
export type ProviderName =
|
||||
| 'AmazonBedrock'
|
||||
| 'Anthropic'
|
||||
| 'Cohere'
|
||||
| 'Deepseek'
|
||||
| 'Google'
|
||||
| 'Groq'
|
||||
| 'HuggingFace'
|
||||
| 'Hyperbolic'
|
||||
| 'Mistral'
|
||||
| 'OpenAI'
|
||||
| 'OpenRouter'
|
||||
| 'Perplexity'
|
||||
| 'Together'
|
||||
| 'XAI';
|
||||
|
||||
export type ServiceStatus = {
|
||||
provider: ProviderName;
|
||||
status: 'operational' | 'degraded' | 'down';
|
||||
lastChecked: string;
|
||||
statusUrl?: string;
|
||||
icon?: IconType;
|
||||
message?: string;
|
||||
responseTime?: number;
|
||||
incidents?: string[];
|
||||
};
|
||||
|
||||
export type ProviderConfig = {
|
||||
statusUrl: string;
|
||||
apiUrl: string;
|
||||
headers: Record<string, string>;
|
||||
testModel: string;
|
||||
};
|
||||
|
||||
export type ApiResponse = {
|
||||
error?: {
|
||||
message: string;
|
||||
};
|
||||
message?: string;
|
||||
model?: string;
|
||||
models?: Array<{
|
||||
id?: string;
|
||||
name?: string;
|
||||
}>;
|
||||
data?: Array<{
|
||||
id?: string;
|
||||
name?: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
export type StatusCheckResult = {
|
||||
status: ServiceStatus['status'];
|
||||
message?: string;
|
||||
incidents?: string[];
|
||||
responseTime?: number;
|
||||
};
|
@ -14,7 +14,8 @@ export type TabType =
|
||||
| 'debug'
|
||||
| 'event-logs'
|
||||
| 'update'
|
||||
| 'task-manager';
|
||||
| 'task-manager'
|
||||
| 'service-status';
|
||||
|
||||
export type WindowType = 'user' | 'developer';
|
||||
|
||||
@ -68,6 +69,7 @@ export const TAB_LABELS: Record<TabType, string> = {
|
||||
'event-logs': 'Event Logs',
|
||||
update: 'Update',
|
||||
'task-manager': 'Task Manager',
|
||||
'service-status': 'Service Status',
|
||||
};
|
||||
|
||||
export const DEFAULT_TAB_CONFIG: TabVisibilityConfig[] = [
|
||||
@ -75,17 +77,18 @@ export const DEFAULT_TAB_CONFIG: TabVisibilityConfig[] = [
|
||||
{ id: 'features', visible: true, window: 'user', order: 0 },
|
||||
{ id: 'data', visible: true, window: 'user', order: 1 },
|
||||
{ id: 'cloud-providers', visible: true, window: 'user', order: 2 },
|
||||
{ id: 'local-providers', visible: true, window: 'user', order: 3 },
|
||||
{ id: 'connection', visible: true, window: 'user', order: 4 },
|
||||
{ id: 'debug', visible: true, window: 'user', order: 5 },
|
||||
{ id: 'service-status', visible: true, window: 'user', order: 3 },
|
||||
{ id: 'local-providers', visible: true, window: 'user', order: 4 },
|
||||
{ id: 'connection', visible: true, window: 'user', order: 5 },
|
||||
{ id: 'debug', visible: true, window: 'user', order: 6 },
|
||||
|
||||
// User Window Tabs (Hidden by default)
|
||||
{ id: 'profile', visible: false, window: 'user', order: 6 },
|
||||
{ id: 'settings', visible: false, window: 'user', order: 7 },
|
||||
{ id: 'notifications', visible: false, window: 'user', order: 8 },
|
||||
{ id: 'event-logs', visible: false, window: 'user', order: 9 },
|
||||
{ id: 'update', visible: false, window: 'user', order: 10 },
|
||||
{ id: 'task-manager', visible: false, window: 'user', order: 11 },
|
||||
{ id: 'profile', visible: false, window: 'user', order: 7 },
|
||||
{ id: 'settings', visible: false, window: 'user', order: 8 },
|
||||
{ id: 'notifications', visible: false, window: 'user', order: 9 },
|
||||
{ id: 'event-logs', visible: false, window: 'user', order: 10 },
|
||||
{ id: 'update', visible: false, window: 'user', order: 11 },
|
||||
{ id: 'task-manager', visible: false, window: 'user', order: 12 },
|
||||
|
||||
// Developer Window Tabs (All visible by default)
|
||||
{ id: 'profile', visible: true, window: 'developer', order: 0 },
|
||||
@ -94,12 +97,13 @@ export const DEFAULT_TAB_CONFIG: TabVisibilityConfig[] = [
|
||||
{ id: 'features', visible: true, window: 'developer', order: 3 },
|
||||
{ id: 'data', visible: true, window: 'developer', order: 4 },
|
||||
{ id: 'cloud-providers', visible: true, window: 'developer', order: 5 },
|
||||
{ id: 'local-providers', visible: true, window: 'developer', order: 6 },
|
||||
{ id: 'connection', visible: true, window: 'developer', order: 7 },
|
||||
{ id: 'debug', visible: true, window: 'developer', order: 8 },
|
||||
{ id: 'event-logs', visible: true, window: 'developer', order: 9 },
|
||||
{ id: 'update', visible: true, window: 'developer', order: 10 },
|
||||
{ id: 'task-manager', visible: true, window: 'developer', order: 11 },
|
||||
{ id: 'service-status', visible: true, window: 'developer', order: 6 },
|
||||
{ id: 'local-providers', visible: true, window: 'developer', order: 7 },
|
||||
{ id: 'connection', visible: true, window: 'developer', order: 8 },
|
||||
{ id: 'debug', visible: true, window: 'developer', order: 9 },
|
||||
{ id: 'event-logs', visible: true, window: 'developer', order: 10 },
|
||||
{ id: 'update', visible: true, window: 'developer', order: 11 },
|
||||
{ id: 'task-manager', visible: true, window: 'developer', order: 12 },
|
||||
];
|
||||
|
||||
export const categoryLabels: Record<SettingCategory, string> = {
|
||||
|
@ -35,11 +35,11 @@ export default function SettingsTab() {
|
||||
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
document.querySelector('html')?.setAttribute('data-theme', prefersDark ? 'dark' : 'light');
|
||||
themeStore.set(prefersDark ? 'dark' : 'light');
|
||||
} else {
|
||||
// Set specific theme
|
||||
themeStore.set(settings.theme);
|
||||
localStorage.setItem(kTheme, settings.theme);
|
||||
document.querySelector('html')?.setAttribute('data-theme', settings.theme);
|
||||
themeStore.set(settings.theme);
|
||||
}
|
||||
}, [settings.theme]);
|
||||
|
||||
@ -89,7 +89,13 @@ export default function SettingsTab() {
|
||||
{(['light', 'dark', 'system'] as const).map((theme) => (
|
||||
<button
|
||||
key={theme}
|
||||
onClick={() => setSettings((prev) => ({ ...prev, theme }))}
|
||||
onClick={() => {
|
||||
setSettings((prev) => ({ ...prev, theme }));
|
||||
|
||||
if (theme !== 'system') {
|
||||
themeStore.set(theme);
|
||||
}
|
||||
}}
|
||||
className={classNames(
|
||||
settingsStyles.button.base,
|
||||
settings.theme === theme ? settingsStyles.button.primary : settingsStyles.button.secondary,
|
||||
|
@ -18,6 +18,7 @@ const TAB_ICONS = {
|
||||
'task-manager': 'i-ph:activity',
|
||||
'cloud-providers': 'i-ph:cloud',
|
||||
'local-providers': 'i-ph:desktop',
|
||||
'service-status': 'i-ph:activity-bold',
|
||||
};
|
||||
|
||||
interface TabTileProps {
|
||||
|
@ -27,6 +27,7 @@ import { useNotifications } from '~/lib/hooks/useNotifications';
|
||||
import { useConnectionStatus } from '~/lib/hooks/useConnectionStatus';
|
||||
import { useDebugStatus } from '~/lib/hooks/useDebugStatus';
|
||||
import CloudProvidersTab from '~/components/settings/providers/CloudProvidersTab';
|
||||
import ServiceStatusTab from '~/components/settings/providers/ServiceStatusTab';
|
||||
import LocalProvidersTab from '~/components/settings/providers/LocalProvidersTab';
|
||||
import TaskManagerTab from '~/components/settings/task-manager/TaskManagerTab';
|
||||
import {
|
||||
@ -57,6 +58,7 @@ const TAB_DESCRIPTIONS: Record<TabType, string> = {
|
||||
data: 'Manage your data and storage',
|
||||
'cloud-providers': 'Configure cloud AI providers and models',
|
||||
'local-providers': 'Configure local AI providers and models',
|
||||
'service-status': 'Monitor cloud LLM service status',
|
||||
connection: 'Check connection status and settings',
|
||||
debug: 'Debug tools and system information',
|
||||
'event-logs': 'View system events and logs',
|
||||
@ -320,6 +322,8 @@ export const UsersWindow = ({ open, onClose }: UsersWindowProps) => {
|
||||
return <DataTab />;
|
||||
case 'cloud-providers':
|
||||
return <CloudProvidersTab />;
|
||||
case 'service-status':
|
||||
return <ServiceStatusTab />;
|
||||
case 'local-providers':
|
||||
return <LocalProvidersTab />;
|
||||
case 'connection':
|
||||
|
@ -25,26 +25,57 @@ export function useShortcuts(): void {
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent): void => {
|
||||
const { key, ctrlKey, shiftKey, altKey, metaKey } = event;
|
||||
// Debug logging
|
||||
console.log('Key pressed:', {
|
||||
key: event.key,
|
||||
code: event.code, // This gives us the physical key regardless of modifiers
|
||||
ctrlKey: event.ctrlKey,
|
||||
shiftKey: event.shiftKey,
|
||||
altKey: event.altKey,
|
||||
metaKey: event.metaKey,
|
||||
});
|
||||
|
||||
/*
|
||||
* Check for theme toggle shortcut first (Option + Command + Shift + D)
|
||||
* Use event.code to check for the physical D key regardless of the character produced
|
||||
*/
|
||||
if (
|
||||
event.code === 'KeyD' &&
|
||||
event.metaKey && // Command (Mac) or Windows key
|
||||
event.altKey && // Option (Mac) or Alt (Windows)
|
||||
event.shiftKey &&
|
||||
!event.ctrlKey
|
||||
) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
shortcuts.toggleTheme.action();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle other shortcuts
|
||||
for (const name in shortcuts) {
|
||||
const shortcut = shortcuts[name as keyof Shortcuts];
|
||||
|
||||
if (
|
||||
shortcut.key.toLowerCase() === key.toLowerCase() &&
|
||||
(shortcut.ctrlOrMetaKey
|
||||
? ctrlKey || metaKey
|
||||
: (shortcut.ctrlKey === undefined || shortcut.ctrlKey === ctrlKey) &&
|
||||
(shortcut.metaKey === undefined || shortcut.metaKey === metaKey)) &&
|
||||
(shortcut.shiftKey === undefined || shortcut.shiftKey === shiftKey) &&
|
||||
(shortcut.altKey === undefined || shortcut.altKey === altKey)
|
||||
) {
|
||||
shortcutEventEmitter.dispatch(name as keyof Shortcuts);
|
||||
if (name === 'toggleTheme') {
|
||||
continue;
|
||||
} // Skip theme toggle as it's handled above
|
||||
|
||||
// For other shortcuts, check both key and code
|
||||
const keyMatches =
|
||||
shortcut.key.toLowerCase() === event.key.toLowerCase() || `Key${shortcut.key.toUpperCase()}` === event.code;
|
||||
|
||||
const modifiersMatch =
|
||||
(shortcut.ctrlKey === undefined || shortcut.ctrlKey === event.ctrlKey) &&
|
||||
(shortcut.metaKey === undefined || shortcut.metaKey === event.metaKey) &&
|
||||
(shortcut.shiftKey === undefined || shortcut.shiftKey === event.shiftKey) &&
|
||||
(shortcut.altKey === undefined || shortcut.altKey === event.altKey);
|
||||
|
||||
if (keyMatches && modifiersMatch) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
shortcutEventEmitter.dispatch(name as keyof Shortcuts);
|
||||
shortcut.action();
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -1,53 +0,0 @@
|
||||
import { BaseProvider } from '~/lib/modules/llm/base-provider';
|
||||
import type { ModelInfo } from '~/lib/modules/llm/types';
|
||||
import type { IProviderSetting } from '~/types/model';
|
||||
import type { LanguageModelV1 } from 'ai';
|
||||
import { createOpenAI } from '@ai-sdk/openai';
|
||||
|
||||
export default class GithubProvider extends BaseProvider {
|
||||
name = 'Github';
|
||||
getApiKeyLink = 'https://github.com/settings/personal-access-tokens';
|
||||
|
||||
config = {
|
||||
apiTokenKey: 'GITHUB_API_KEY',
|
||||
};
|
||||
|
||||
// find more in https://github.com/marketplace?type=models
|
||||
staticModels: ModelInfo[] = [
|
||||
{ name: 'gpt-4o', label: 'GPT-4o', provider: 'Github', maxTokenAllowed: 8000 },
|
||||
{ name: 'o1', label: 'o1-preview', provider: 'Github', maxTokenAllowed: 100000 },
|
||||
{ name: 'o1-mini', label: 'o1-mini', provider: 'Github', maxTokenAllowed: 8000 },
|
||||
{ name: 'gpt-4o-mini', label: 'GPT-4o Mini', provider: 'Github', maxTokenAllowed: 8000 },
|
||||
{ name: 'gpt-4-turbo', label: 'GPT-4 Turbo', provider: 'Github', maxTokenAllowed: 8000 },
|
||||
{ name: 'gpt-4', label: 'GPT-4', provider: 'Github', maxTokenAllowed: 8000 },
|
||||
{ name: 'gpt-3.5-turbo', label: 'GPT-3.5 Turbo', provider: 'Github', maxTokenAllowed: 8000 },
|
||||
];
|
||||
|
||||
getModelInstance(options: {
|
||||
model: string;
|
||||
serverEnv: Env;
|
||||
apiKeys?: Record<string, string>;
|
||||
providerSettings?: Record<string, IProviderSetting>;
|
||||
}): LanguageModelV1 {
|
||||
const { model, serverEnv, apiKeys, providerSettings } = options;
|
||||
|
||||
const { apiKey } = this.getProviderBaseUrlAndKey({
|
||||
apiKeys,
|
||||
providerSettings: providerSettings?.[this.name],
|
||||
serverEnv: serverEnv as any,
|
||||
defaultBaseUrlKey: '',
|
||||
defaultApiTokenKey: 'GITHUB_API_KEY',
|
||||
});
|
||||
|
||||
if (!apiKey) {
|
||||
throw new Error(`Missing API key for ${this.name} provider`);
|
||||
}
|
||||
|
||||
const openai = createOpenAI({
|
||||
baseURL: 'https://models.inference.ai.azure.com',
|
||||
apiKey,
|
||||
});
|
||||
|
||||
return openai(model);
|
||||
}
|
||||
}
|
@ -15,7 +15,6 @@ import TogetherProvider from './providers/together';
|
||||
import XAIProvider from './providers/xai';
|
||||
import HyperbolicProvider from './providers/hyperbolic';
|
||||
import AmazonBedrockProvider from './providers/amazon-bedrock';
|
||||
import GithubProvider from './providers/github';
|
||||
|
||||
export {
|
||||
AnthropicProvider,
|
||||
@ -35,5 +34,4 @@ export {
|
||||
TogetherProvider,
|
||||
LMStudioProvider,
|
||||
AmazonBedrockProvider,
|
||||
GithubProvider,
|
||||
};
|
||||
|
@ -38,8 +38,9 @@ export const shortcutsStore = map<Shortcuts>({
|
||||
},
|
||||
toggleTheme: {
|
||||
key: 'd',
|
||||
ctrlOrMetaKey: true,
|
||||
altKey: true,
|
||||
metaKey: true, // Command key on Mac, Windows key on Windows
|
||||
altKey: true, // Option key on Mac, Alt key on Windows
|
||||
shiftKey: true,
|
||||
action: () => toggleTheme(),
|
||||
},
|
||||
toggleChat: {
|
||||
|
@ -27,8 +27,28 @@ function initStore() {
|
||||
export function toggleTheme() {
|
||||
const currentTheme = themeStore.get();
|
||||
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
||||
|
||||
// Update the theme store
|
||||
themeStore.set(newTheme);
|
||||
logStore.logSystem(`Theme changed to ${newTheme} mode`);
|
||||
|
||||
// Update localStorage
|
||||
localStorage.setItem(kTheme, newTheme);
|
||||
|
||||
// Update the HTML attribute
|
||||
document.querySelector('html')?.setAttribute('data-theme', newTheme);
|
||||
|
||||
// Update user profile if it exists
|
||||
try {
|
||||
const userProfile = localStorage.getItem('bolt_user_profile');
|
||||
|
||||
if (userProfile) {
|
||||
const profile = JSON.parse(userProfile);
|
||||
profile.theme = newTheme;
|
||||
localStorage.setItem('bolt_user_profile', JSON.stringify(profile));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating user profile theme:', error);
|
||||
}
|
||||
|
||||
logStore.logSystem(`Theme changed to ${newTheme} mode`);
|
||||
}
|
||||
|
5
public/icons/astro.svg
Normal file
5
public/icons/astro.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.074 16.86c-.72.616-2.157 1.035-3.812 1.035-2.032 0-3.735-.632-4.187-1.483-.161.488-.198 1.046-.198 1.402 0 0-.106 1.75 1.111 2.968 0-.632.438-1.149.98-1.149.963 0 .963.964.957 1.743v.069c0 1.398.866 2.557 2.083 2.557 1.014 0 1.807-.817 2.025-1.93.105.113.2.235.284.364 1.009 1.369 1.51 2.563 1.51 2.563s.201-.981.201-2.203c0-1.806-.957-2.916-1.438-3.489 1.105-.321 1.828-.809 2.09-1.119-2.135.413-4.156.413-6.314-.413 2.517-.706 3.848-1.778 4.662-2.632a8.224 8.224 0 0 0 1.019-1.597c.105-.224.201-.452.284-.686.099.099.189.21.27.329.045.064.086.131.124.201.146.259.247.547.247.854 0 .642-.321 1.219-.828 1.616z" fill="currentColor"/>
|
||||
<path d="M16.8 7.2c-.488 2.486-3.257 5.143-6.171 6.245-.043.016-.086.034-.128.051.099-.099.189-.21.27-.329.045-.064.086-.131.124-.201.146-.259.247-.547.247-.854 0-.642-.321-1.219-.828-1.616-.72.616-2.157 1.035-3.812 1.035-2.032 0-3.735-.632-4.187-1.483-.161.488-.198 1.046-.198 1.402 0 0-.106 1.75 1.111 2.968 0-.632.438-1.149.98-1.149.963 0 .963.964.957 1.743v.069c0 1.398.866 2.557 2.083 2.557 1.014 0 1.807-.817 2.025-1.93.105.113.2.235.284.364 1.009 1.369 1.51 2.563 1.51 2.563s.201-.981.201-2.203c0-1.806-.957-2.916-1.438-3.489 1.105-.321 1.828-.809 2.09-1.119-2.135.413-4.156.413-6.314-.413 2.517-.706 3.848-1.778 4.662-2.632a8.224 8.224 0 0 0 1.019-1.597c1.41-2.847 1.457-5.367 1.457-5.367s2.532 3.257 2.156 6.386z" fill="currentColor"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
5
public/icons/nextjs.svg
Normal file
5
public/icons/nextjs.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.214 0.00500488C11.0731 0.0159347 10.9364 0.0459471 10.809 0.0940049L1.159 3.9C0.769 4.067 0.5 4.443 0.5 4.857V19.143C0.5 19.557 0.769 19.933 1.159 20.1L10.809 23.906C11.191 24.07 11.632 24.07 12.014 23.906L21.664 20.1C22.054 19.933 22.323 19.557 22.323 19.143V4.857C22.323 4.443 22.054 4.067 21.664 3.9L12.014 0.0940049C11.7573 0.00589139 11.4829 -0.0181383 11.214 0.00500488ZM11.411 2.813L19.411 6.036V17.964L11.411 21.188L3.411 17.964V6.036L11.411 2.813Z" fill="currentColor"/>
|
||||
<path d="M15.859 16.839L9.359 7.839C9.278 7.721 9.161 7.631 9.026 7.583C8.891 7.535 8.745 7.531 8.608 7.573C8.471 7.615 8.349 7.7 8.259 7.816C8.169 7.932 8.115 8.073 8.105 8.221L8.105 15.779C8.115 15.927 8.169 16.068 8.259 16.184C8.349 16.3 8.471 16.385 8.608 16.427C8.745 16.469 8.891 16.465 9.026 16.417C9.161 16.369 9.278 16.279 9.359 16.161L15.859 7.161C15.94 7.045 15.985 6.904 15.985 6.759C15.985 6.614 15.94 6.473 15.859 6.357L15.859 16.839Z" fill="currentColor"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
4
public/icons/qwik.svg
Normal file
4
public/icons/qwik.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 2L3 7V17L12 22L21 17V7L12 2ZM12 4.311L18.754 8.156L16.664 9.311L9.91 5.467L12 4.311ZM5.246 8.156L12 4.311L14.09 5.467L7.336 9.311L5.246 8.156ZM5 9.467L7.09 10.622L7.09 15.378L5 14.222V9.467ZM8.09 16.533L5 15.378V14.222L8.09 15.378V16.533ZM9.09 16.533V15.378L12.18 14.222V15.378L9.09 16.533ZM13.18 15.378V14.222L16.27 15.378V16.533L13.18 15.378ZM17.27 16.533L14.18 15.378V14.222L17.27 15.378V16.533ZM18.27 15.378L15.18 14.222V9.467L18.27 10.622V15.378ZM14.18 8.311L17.27 9.467L14.18 10.622L11.09 9.467L14.18 8.311ZM10.09 9.467L13.18 8.311L16.27 9.467L13.18 10.622L10.09 9.467ZM6.09 9.467L9.18 8.311L12.27 9.467L9.18 10.622L6.09 9.467Z" fill="currentColor"/>
|
||||
</svg>
|
After Width: | Height: | Size: 814 B |
@ -7,7 +7,7 @@ import type { IconifyJSON } from '@iconify/types';
|
||||
// Debug: Log the current working directory and icon paths
|
||||
console.log('CWD:', process.cwd());
|
||||
|
||||
const iconPaths = globSync(join(process.cwd(), 'public/icons/*.svg'));
|
||||
const iconPaths = globSync(join(process.cwd(), 'icons/*.svg'));
|
||||
console.log('Found icons:', iconPaths);
|
||||
|
||||
const collectionName = 'bolt';
|
||||
|
Loading…
Reference in New Issue
Block a user