Service console check providers

This commit is contained in:
Stijnus 2025-01-30 01:58:47 +01:00
parent 9e8d05cb54
commit d9a380f28a
22 changed files with 1476 additions and 104 deletions

View File

@ -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>
);

View File

@ -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);

View File

@ -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 = ({

View File

@ -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 {

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

View File

@ -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>;
}

View File

@ -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];
}
}

View File

@ -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'],
};
}
}
}

View 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;
};

View File

@ -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> = {

View File

@ -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,

View File

@ -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 {

View File

@ -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':

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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,
};

View File

@ -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: {

View File

@ -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
View 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
View 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
View 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

View File

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