big fixes

fixes feedback from thecodacus
This commit is contained in:
Stijnus
2025-01-30 17:17:36 +01:00
parent d9a380f28a
commit d1d23d80e7
68 changed files with 2449 additions and 1350 deletions

View File

@@ -7,7 +7,7 @@ import { useState } from 'react';
import { toast } from 'react-toastify';
import { LoadingOverlay } from '~/components/ui/LoadingOverlay';
import { RepositorySelectionDialog } from '~/components/settings/connections/components/RepositorySelectionDialog';
import { cn } from '~/lib/utils';
import { classNames } from '~/utils/classNames';
import { Button } from '~/components/ui/Button';
import type { IChatMetadata } from '~/lib/persistence/db';
@@ -158,7 +158,7 @@ ${escapeBoltTags(file.content)}
title="Clone a Git Repo"
variant="outline"
size="lg"
className={cn(
className={classNames(
'gap-2 bg-[#F5F5F5] dark:bg-[#252525]',
'text-bolt-elements-textPrimary dark:text-white',
'hover:bg-[#E5E5E5] dark:hover:bg-[#333333]',

View File

@@ -5,7 +5,7 @@ import { MAX_FILES, isBinaryFile, shouldIncludeFile } from '~/utils/fileUtils';
import { createChatFromFolder } from '~/utils/folderImport';
import { logStore } from '~/lib/stores/logs'; // Assuming logStore is imported from this location
import { Button } from '~/components/ui/Button';
import { cn } from '~/lib/utils';
import { classNames } from '~/utils/classNames';
interface ImportFolderButtonProps {
className?: string;
@@ -119,9 +119,10 @@ export const ImportFolderButton: React.FC<ImportFolderButtonProps> = ({ classNam
const input = document.getElementById('folder-import');
input?.click();
}}
title="Import Folder"
variant="outline"
size="lg"
className={cn(
className={classNames(
'gap-2 bg-[#F5F5F5] dark:bg-[#252525]',
'text-bolt-elements-textPrimary dark:text-white',
'hover:bg-[#E5E5E5] dark:hover:bg-[#333333]',

View File

@@ -11,15 +11,24 @@ const FrameworkLink: React.FC<FrameworkLinkProps> = ({ template }) => (
href={`/git?url=https://github.com/${template.githubRepo}.git`}
data-state="closed"
data-discover="true"
className="items-center justify-center "
className="items-center justify-center"
>
<div
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`}
title={template.label}
/>
</a>
);
const StarterTemplates: React.FC = () => {
// Debug: Log available templates and their icons
React.useEffect(() => {
console.log(
'Available templates:',
STARTER_TEMPLATES.map((t) => ({ name: t.name, icon: t.icon })),
);
}, []);
return (
<div className="flex flex-col items-center gap-4">
<span className="text-sm text-gray-500">or start a blank app with your favorite stack</span>

View File

@@ -2,7 +2,7 @@ import type { Message } from 'ai';
import { toast } from 'react-toastify';
import { ImportFolderButton } from '~/components/chat/ImportFolderButton';
import { Button } from '~/components/ui/Button';
import { cn } from '~/lib/utils';
import { classNames } from '~/utils/classNames';
type ChatData = {
messages?: Message[]; // Standard Bolt format
@@ -66,7 +66,7 @@ export function ImportButtons(importChat: ((description: string, messages: Messa
}}
variant="outline"
size="lg"
className={cn(
className={classNames(
'gap-2 bg-[#F5F5F5] dark:bg-[#252525]',
'text-bolt-elements-textPrimary dark:text-white',
'hover:bg-[#E5E5E5] dark:hover:bg-[#333333]',
@@ -80,7 +80,7 @@ export function ImportButtons(importChat: ((description: string, messages: Messa
</Button>
<ImportFolderButton
importChat={importChat}
className={cn(
className={classNames(
'gap-2 bg-[#F5F5F5] dark:bg-[#252525]',
'text-bolt-elements-textPrimary dark:text-white',
'hover:bg-[#E5E5E5] dark:hover:bg-[#333333]',

View File

@@ -2,7 +2,7 @@ import React, { useEffect } from 'react';
import { classNames } from '~/utils/classNames';
import type { GitHubAuthState } from '~/components/settings/connections/types/GitHub';
import Cookies from 'js-cookie';
import { getLocalStorage } from '~/utils/localStorage';
import { getLocalStorage } from '~/lib/persistence';
const GITHUB_TOKEN_KEY = 'github_token';

View File

@@ -2,7 +2,7 @@ import * as Dialog from '@radix-ui/react-dialog';
import { useState, useEffect } from 'react';
import { toast } from 'react-toastify';
import { motion } from 'framer-motion';
import { getLocalStorage } from '~/utils/localStorage';
import { getLocalStorage } from '~/lib/persistence';
import { classNames } from '~/utils/classNames';
import type { GitHubUserResponse } from '~/types/GitHub';
import { logStore } from '~/lib/stores/logs';

View File

@@ -2,11 +2,11 @@ import type { GitHubRepoInfo, GitHubContent, RepositoryStats } from '~/types/Git
import { useState, useEffect } from 'react';
import { toast } from 'react-toastify';
import * as Dialog from '@radix-ui/react-dialog';
import { cn } from '~/lib/utils';
import { getLocalStorage } from '~/utils/localStorage';
import { classNames as utilsClassNames } from '~/utils/classNames';
import { classNames } from '~/utils/classNames';
import { getLocalStorage } from '~/lib/persistence';
import { motion } from 'framer-motion';
import { formatSize } from '~/utils/formatSize';
import { Input } from '~/components/ui/Input';
interface GitHubTreeResponse {
tree: Array<{
@@ -445,7 +445,7 @@ export function RepositorySelectionDialog({ isOpen, onClose, onSelect }: Reposit
</Dialog.Title>
<Dialog.Close
onClick={handleClose}
className={cn(
className={classNames(
'p-2 rounded-lg transition-all duration-200 ease-in-out',
'text-bolt-elements-textTertiary hover:text-bolt-elements-textPrimary',
'dark:text-bolt-elements-textTertiary-dark dark:hover:text-bolt-elements-textPrimary-dark',
@@ -476,12 +476,13 @@ export function RepositorySelectionDialog({ isOpen, onClose, onSelect }: Reposit
{activeTab === 'url' ? (
<div className="space-y-4">
<input
type="text"
placeholder="Enter GitHub repository URL..."
<Input
placeholder="Enter repository URL"
value={customUrl}
onChange={(e) => setCustomUrl(e.target.value)}
className="w-full px-4 py-2 rounded-lg bg-[#F5F5F5] dark:bg-[#252525] border border-[#E5E5E5] dark:border-[#333333] text-bolt-elements-textPrimary"
className={classNames('w-full', {
'border-red-500': false,
})}
/>
<button
onClick={handleImport}
@@ -610,7 +611,7 @@ function TabButton({ active, onClick, children }: { active: boolean; onClick: ()
return (
<button
onClick={onClick}
className={utilsClassNames(
className={classNames(
'px-4 py-2 h-10 rounded-lg transition-all duration-200 flex items-center gap-2 min-w-[120px] justify-center',
active
? 'bg-purple-500 text-white hover:bg-purple-600'

View File

@@ -1,13 +1,12 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useState, useMemo } from 'react';
import { toast } from 'react-toastify';
import { classNames } from '~/utils/classNames';
import { logStore } from '~/lib/stores/logs';
import type { LogEntry } from '~/lib/stores/logs';
import { logStore, type LogEntry } from '~/lib/stores/logs';
import { useStore } from '@nanostores/react';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '~/components/ui/Collapsible';
import { Progress } from '~/components/ui/Progress';
import { ScrollArea } from '~/components/ui/ScrollArea';
import { Badge } from '~/components/ui/Badge';
import { cn } from '~/lib/utils';
interface SystemInfo {
os: string;
@@ -88,125 +87,134 @@ interface SystemInfo {
};
}
interface GitHubRepoInfo {
fullName: string;
defaultBranch: string;
stars: number;
forks: number;
openIssues?: number;
}
interface GitInfo {
local: {
commitHash: string;
branch: string;
commitTime: string;
author: string;
email: string;
remoteUrl: string;
repoName: string;
};
github?: {
currentRepo: GitHubRepoInfo;
upstream?: GitHubRepoInfo;
};
isForked?: boolean;
}
interface WebAppInfo {
// Local WebApp Info
name: string;
version: string;
description: string;
license: string;
nodeVersion: string;
dependencies: { [key: string]: string };
devDependencies: { [key: string]: string };
// Build Info
buildTime?: string;
buildNumber?: string;
environment?: string;
// Git Info
gitInfo?: {
branch: string;
commit: string;
commitTime: string;
author: string;
remoteUrl: string;
environment: string;
timestamp: string;
runtimeInfo: {
nodeVersion: string;
};
// GitHub Repository Info
repoInfo?: {
name: string;
fullName: string;
description: string;
stars: number;
forks: number;
openIssues: number;
defaultBranch: string;
lastUpdate: string;
owner: {
login: string;
avatarUrl: string;
};
dependencies: {
production: Array<{ name: string; version: string; type: string }>;
development: Array<{ name: string; version: string; type: string }>;
peer: Array<{ name: string; version: string; type: string }>;
optional: Array<{ name: string; version: string; type: string }>;
};
gitInfo: GitInfo;
}
// Add interface for GitHub API response
interface GitHubRepoResponse {
name: string;
full_name: string;
description: string | null;
stargazers_count: number;
forks_count: number;
open_issues_count: number;
default_branch: string;
updated_at: string;
owner: {
login: string;
avatar_url: string;
};
}
const DependencySection = ({
title,
deps,
}: {
title: string;
deps: Array<{ name: string; version: string; type: string }>;
}) => {
const [isOpen, setIsOpen] = useState(false);
// Add interface for Git info response
interface GitInfo {
branch: string;
commit: string;
commitTime: string;
author: string;
remoteUrl: string;
}
if (deps.length === 0) {
return null;
}
return (
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
<CollapsibleTrigger className="flex w-full items-center justify-between p-4 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg">
<div className="flex items-center gap-3">
<div className="i-ph:package text-bolt-elements-textSecondary w-4 h-4" />
<span className="text-base text-bolt-elements-textPrimary">
{title} Dependencies ({deps.length})
</span>
</div>
<div className="flex items-center gap-2">
<span className="text-sm text-bolt-elements-textSecondary">{isOpen ? 'Hide' : 'Show'}</span>
<div
className={classNames(
'i-ph:caret-down w-4 h-4 transform transition-transform duration-200',
isOpen ? 'rotate-180' : '',
)}
/>
</div>
</CollapsibleTrigger>
<CollapsibleContent>
<ScrollArea className="h-[200px] w-full p-4">
<div className="space-y-2 pl-7">
{deps.map((dep) => (
<div key={dep.name} className="flex items-center justify-between text-sm">
<span className="text-bolt-elements-textPrimary">{dep.name}</span>
<span className="text-bolt-elements-textSecondary">{dep.version}</span>
</div>
))}
</div>
</ScrollArea>
</CollapsibleContent>
</Collapsible>
);
};
export default function DebugTab() {
const [systemInfo, setSystemInfo] = useState<SystemInfo | null>(null);
const [webAppInfo, setWebAppInfo] = useState<WebAppInfo | null>(null);
const [loading, setLoading] = useState({
systemInfo: false,
performance: false,
errors: false,
webAppInfo: false,
errors: false,
performance: false,
});
const [errorLog, setErrorLog] = useState<{
errors: any[];
lastCheck: string | null;
}>({
errors: [],
lastCheck: null,
});
// Add section collapse state
const [openSections, setOpenSections] = useState({
system: true,
performance: true,
webapp: true,
errors: true,
system: false,
webapp: false,
errors: false,
performance: false,
});
// Fetch initial data
useEffect(() => {
getSystemInfo();
getWebAppInfo();
}, []);
// Subscribe to logStore updates
const logs = useStore(logStore.logs);
const errorLogs = useMemo(() => {
return Object.values(logs).filter(
(log): log is LogEntry => typeof log === 'object' && log !== null && 'level' in log && log.level === 'error',
);
}, [logs]);
// Set up error listeners when component mounts
useEffect(() => {
const errors: any[] = [];
const handleError = (event: ErrorEvent) => {
errors.push({
type: 'error',
message: event.message,
logStore.logError(event.message, event.error, {
filename: event.filename,
lineNumber: event.lineno,
columnNumber: event.colno,
error: event.error,
timestamp: new Date().toISOString(),
});
};
const handleRejection = (event: PromiseRejectionEvent) => {
errors.push({
type: 'unhandledRejection',
reason: event.reason,
timestamp: new Date().toISOString(),
});
logStore.logError('Unhandled Promise Rejection', event.reason);
};
window.addEventListener('error', handleError);
@@ -218,6 +226,66 @@ export default function DebugTab() {
};
}, []);
// Check for errors when the errors section is opened
useEffect(() => {
if (openSections.errors) {
checkErrors();
}
}, [openSections.errors]);
// Load initial data when component mounts
useEffect(() => {
const loadInitialData = async () => {
await Promise.all([getSystemInfo(), getWebAppInfo()]);
};
loadInitialData();
}, []);
// Refresh data when sections are opened
useEffect(() => {
if (openSections.system) {
getSystemInfo();
}
if (openSections.webapp) {
getWebAppInfo();
}
}, [openSections.system, openSections.webapp]);
// Add periodic refresh of git info
useEffect(() => {
if (!openSections.webapp) {
return undefined;
}
const interval = setInterval(async () => {
try {
const response = await fetch('/api/system/git-info');
const updatedGitInfo = (await response.json()) as GitInfo;
setWebAppInfo((prev) => {
if (!prev) {
return null;
}
return {
...prev,
gitInfo: updatedGitInfo,
};
});
} catch (error) {
console.error('Failed to refresh git info:', error);
}
}, 5000);
const cleanup = () => {
clearInterval(interval);
};
return cleanup;
}, [openSections.webapp]);
const getSystemInfo = async () => {
try {
setLoading((prev) => ({ ...prev, systemInfo: true }));
@@ -367,67 +435,32 @@ export default function DebugTab() {
try {
setLoading((prev) => ({ ...prev, webAppInfo: true }));
// Fetch local app info
const appInfoResponse = await fetch('/api/system/app-info');
const [appResponse, gitResponse] = await Promise.all([
fetch('/api/system/app-info'),
fetch('/api/system/git-info'),
]);
if (!appInfoResponse.ok) {
if (!appResponse.ok || !gitResponse.ok) {
throw new Error('Failed to fetch webapp info');
}
const appData = (await appInfoResponse.json()) as Record<string, unknown>;
// Fetch git info
const gitInfoResponse = await fetch('/api/system/git-info');
let gitInfo: GitInfo | undefined;
if (gitInfoResponse.ok) {
gitInfo = (await gitInfoResponse.json()) as GitInfo;
}
// Fetch GitHub repository info
const repoInfoResponse = await fetch('https://api.github.com/repos/stackblitz-labs/bolt.diy');
let repoInfo: WebAppInfo['repoInfo'] | undefined;
if (repoInfoResponse.ok) {
const repoData = (await repoInfoResponse.json()) as GitHubRepoResponse;
repoInfo = {
name: repoData.name,
fullName: repoData.full_name,
description: repoData.description ?? '',
stars: repoData.stargazers_count,
forks: repoData.forks_count,
openIssues: repoData.open_issues_count,
defaultBranch: repoData.default_branch,
lastUpdate: repoData.updated_at,
owner: {
login: repoData.owner.login,
avatarUrl: repoData.owner.avatar_url,
},
};
}
// Get build info from environment variables or config
const buildInfo = {
buildTime: process.env.NEXT_PUBLIC_BUILD_TIME || new Date().toISOString(),
buildNumber: process.env.NEXT_PUBLIC_BUILD_NUMBER || 'development',
environment: process.env.NEXT_PUBLIC_ENV || 'development',
};
const appData = (await appResponse.json()) as Omit<WebAppInfo, 'gitInfo'>;
const gitData = (await gitResponse.json()) as GitInfo;
setWebAppInfo({
name: appData.name as string,
version: appData.version as string,
description: appData.description as string,
license: appData.license as string,
nodeVersion: appData.nodeVersion as string,
dependencies: appData.dependencies as Record<string, string>,
devDependencies: appData.devDependencies as Record<string, string>,
...buildInfo,
gitInfo,
repoInfo,
...appData,
gitInfo: gitData,
});
toast.success('WebApp information updated');
return true;
} catch (error) {
console.error('Failed to fetch webapp info:', error);
toast.error('Failed to fetch webapp information');
setWebAppInfo(null);
return false;
} finally {
setLoading((prev) => ({ ...prev, webAppInfo: false }));
}
@@ -536,28 +569,12 @@ export default function DebugTab() {
setLoading((prev) => ({ ...prev, errors: true }));
// Get errors from log store
const storedErrors = logStore.getLogs().filter((log: LogEntry) => log.level === 'error');
const storedErrors = errorLogs;
// Combine with runtime errors
const allErrors = [
...errorLog.errors,
...storedErrors.map((error) => ({
type: 'stored',
message: error.message,
timestamp: error.timestamp,
details: error.details || {},
})),
];
setErrorLog({
errors: allErrors,
lastCheck: new Date().toISOString(),
});
if (allErrors.length === 0) {
if (storedErrors.length === 0) {
toast.success('No errors found');
} else {
toast.warning(`Found ${allErrors.length} error(s)`);
toast.warning(`Found ${storedErrors.length} error(s)`);
}
} catch (error) {
toast.error('Failed to check errors');
@@ -573,7 +590,7 @@ export default function DebugTab() {
timestamp: new Date().toISOString(),
system: systemInfo,
webApp: webAppInfo,
errors: errorLog.errors,
errors: logStore.getLogs().filter((log: LogEntry) => log.level === 'error'),
performance: {
memory: (performance as any).memory || {},
timing: performance.timing,
@@ -629,10 +646,7 @@ export default function DebugTab() {
<div className="p-4 rounded-xl bg-gradient-to-br from-red-500/10 to-red-500/5 border border-red-500/20">
<div className="text-sm text-bolt-elements-textSecondary">Errors</div>
<div className="text-2xl font-semibold text-bolt-elements-textPrimary mt-1">{errorLog.errors.length}</div>
<div className="text-xs text-bolt-elements-textSecondary mt-2">
Last Check: {errorLog.lastCheck ? new Date(errorLog.lastCheck).toLocaleTimeString() : 'Never'}
</div>
<div className="text-2xl font-semibold text-bolt-elements-textPrimary mt-1">{errorLogs.length}</div>
</div>
</div>
@@ -746,7 +760,7 @@ export default function DebugTab() {
<h3 className="text-base font-medium text-bolt-elements-textPrimary">System Information</h3>
</div>
<div
className={cn(
className={classNames(
'i-ph:caret-down w-4 h-4 transform transition-transform duration-200',
openSections.system ? 'rotate-180' : '',
)}
@@ -893,7 +907,7 @@ export default function DebugTab() {
<h3 className="text-base font-medium text-bolt-elements-textPrimary">Performance Metrics</h3>
</div>
<div
className={cn(
className={classNames(
'i-ph:caret-down w-4 h-4 transform transition-transform duration-200',
openSections.performance ? 'rotate-180' : '',
)}
@@ -973,7 +987,7 @@ export default function DebugTab() {
{/* WebApp Information */}
<Collapsible
open={openSections.webapp}
onOpenChange={(open: boolean) => setOpenSections((prev) => ({ ...prev, webapp: open }))}
onOpenChange={(open) => setOpenSections((prev) => ({ ...prev, webapp: open }))}
className="w-full"
>
<CollapsibleTrigger className="w-full">
@@ -981,9 +995,10 @@ export default function DebugTab() {
<div className="flex items-center gap-3">
<div className="i-ph:info text-blue-500 w-5 h-5" />
<h3 className="text-base font-medium text-bolt-elements-textPrimary">WebApp Information</h3>
{loading.webAppInfo && <span className="loading loading-spinner loading-sm" />}
</div>
<div
className={cn(
className={classNames(
'i-ph:caret-down w-4 h-4 transform transition-transform duration-200',
openSections.webapp ? 'rotate-180' : '',
)}
@@ -993,142 +1008,154 @@ export default function DebugTab() {
<CollapsibleContent>
<div className="p-6 mt-2 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
{webAppInfo ? (
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<div className="text-sm flex items-center gap-2">
<div className="i-ph:app-window text-bolt-elements-textSecondary w-4 h-4" />
<span className="text-bolt-elements-textSecondary">Name: </span>
<span className="text-bolt-elements-textPrimary">{webAppInfo.name}</span>
</div>
<div className="text-sm flex items-center gap-2">
<div className="i-ph:tag text-bolt-elements-textSecondary w-4 h-4" />
<span className="text-bolt-elements-textSecondary">Version: </span>
<span className="text-bolt-elements-textPrimary">{webAppInfo.version}</span>
</div>
<div className="text-sm flex items-center gap-2">
<div className="i-ph:file-text text-bolt-elements-textSecondary w-4 h-4" />
<span className="text-bolt-elements-textSecondary">Description: </span>
<span className="text-bolt-elements-textPrimary">{webAppInfo.description}</span>
</div>
<div className="text-sm flex items-center gap-2">
<div className="i-ph:certificate text-bolt-elements-textSecondary w-4 h-4" />
<span className="text-bolt-elements-textSecondary">License: </span>
<span className="text-bolt-elements-textPrimary">{webAppInfo.license}</span>
</div>
<div className="text-sm flex items-center gap-2">
<div className="i-ph:node text-bolt-elements-textSecondary w-4 h-4" />
<span className="text-bolt-elements-textSecondary">Node Version: </span>
<span className="text-bolt-elements-textPrimary">{webAppInfo.nodeVersion}</span>
</div>
{webAppInfo.buildTime && (
{loading.webAppInfo ? (
<div className="flex items-center justify-center p-8">
<span className="loading loading-spinner loading-lg" />
</div>
) : !webAppInfo ? (
<div className="flex flex-col items-center justify-center p-8 text-bolt-elements-textSecondary">
<div className="i-ph:warning-circle w-8 h-8 mb-2" />
<p>Failed to load WebApp information</p>
<button
onClick={() => getWebAppInfo()}
className="mt-4 px-4 py-2 text-sm bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
>
Retry
</button>
</div>
) : (
<div className="grid grid-cols-2 gap-6">
<div>
<h3 className="mb-4 text-base font-medium text-bolt-elements-textPrimary">Basic Information</h3>
<div className="space-y-3">
<div className="text-sm flex items-center gap-2">
<div className="i-ph:calendar text-bolt-elements-textSecondary w-4 h-4" />
<span className="text-bolt-elements-textSecondary">Build Time: </span>
<span className="text-bolt-elements-textPrimary">{webAppInfo.buildTime}</span>
<div className="i-ph:app-window text-bolt-elements-textSecondary w-4 h-4" />
<span className="text-bolt-elements-textSecondary">Name:</span>
<span className="text-bolt-elements-textPrimary">{webAppInfo.name}</span>
</div>
)}
{webAppInfo.buildNumber && (
<div className="text-sm flex items-center gap-2">
<div className="i-ph:hash text-bolt-elements-textSecondary w-4 h-4" />
<span className="text-bolt-elements-textSecondary">Build Number: </span>
<span className="text-bolt-elements-textPrimary">{webAppInfo.buildNumber}</span>
<div className="i-ph:tag text-bolt-elements-textSecondary w-4 h-4" />
<span className="text-bolt-elements-textSecondary">Version:</span>
<span className="text-bolt-elements-textPrimary">{webAppInfo.version}</span>
</div>
<div className="text-sm flex items-center gap-2">
<div className="i-ph:certificate text-bolt-elements-textSecondary w-4 h-4" />
<span className="text-bolt-elements-textSecondary">License:</span>
<span className="text-bolt-elements-textPrimary">{webAppInfo.license}</span>
</div>
)}
{webAppInfo.environment && (
<div className="text-sm flex items-center gap-2">
<div className="i-ph:cloud text-bolt-elements-textSecondary w-4 h-4" />
<span className="text-bolt-elements-textSecondary">Environment: </span>
<span className="text-bolt-elements-textSecondary">Environment:</span>
<span className="text-bolt-elements-textPrimary">{webAppInfo.environment}</span>
</div>
)}
</div>
<div className="space-y-2">
<div className="text-sm">
<div className="flex items-center gap-2 mb-2">
<div className="i-ph:package text-bolt-elements-textSecondary w-4 h-4" />
<span className="text-bolt-elements-textSecondary">Key Dependencies:</span>
</div>
<div className="pl-6 space-y-1">
{Object.entries(webAppInfo.dependencies)
.filter(([key]) => ['react', '@remix-run/react', 'next', 'typescript'].includes(key))
.map(([key, version]) => (
<div key={key} className="text-xs text-bolt-elements-textPrimary">
{key}: {version}
</div>
))}
<div className="text-sm flex items-center gap-2">
<div className="i-ph:node text-bolt-elements-textSecondary w-4 h-4" />
<span className="text-bolt-elements-textSecondary">Node Version:</span>
<span className="text-bolt-elements-textPrimary">{webAppInfo.runtimeInfo.nodeVersion}</span>
</div>
</div>
{webAppInfo.gitInfo && (
<div className="text-sm">
<div className="flex items-center gap-2 mb-2">
<div className="i-ph:git-branch text-bolt-elements-textSecondary w-4 h-4" />
<span className="text-bolt-elements-textSecondary">Git Info:</span>
</div>
<div className="pl-6 space-y-1">
<div className="text-xs text-bolt-elements-textPrimary">
Branch: {webAppInfo.gitInfo.branch}
</div>
<div className="text-xs text-bolt-elements-textPrimary">
Commit: {webAppInfo.gitInfo.commit}
</div>
<div className="text-xs text-bolt-elements-textPrimary">
Commit Time: {webAppInfo.gitInfo.commitTime}
</div>
<div className="text-xs text-bolt-elements-textPrimary">
Author: {webAppInfo.gitInfo.author}
</div>
<div className="text-xs text-bolt-elements-textPrimary">
Remote URL: {webAppInfo.gitInfo.remoteUrl}
</div>
</div>
</div>
<div>
<h3 className="mb-4 text-base font-medium text-bolt-elements-textPrimary">Git Information</h3>
<div className="space-y-3">
<div className="text-sm flex items-center gap-2">
<div className="i-ph:git-branch text-bolt-elements-textSecondary w-4 h-4" />
<span className="text-bolt-elements-textSecondary">Branch:</span>
<span className="text-bolt-elements-textPrimary">{webAppInfo.gitInfo.local.branch}</span>
</div>
)}
{webAppInfo.repoInfo && (
<div className="text-sm">
<div className="flex items-center gap-2 mb-2">
<div className="i-ph:github text-bolt-elements-textSecondary w-4 h-4" />
<span className="text-bolt-elements-textSecondary">GitHub Repository:</span>
</div>
<div className="pl-6 space-y-3">
<div className="flex items-center gap-3">
<img
src={webAppInfo.repoInfo.owner.avatarUrl}
alt={`${webAppInfo.repoInfo.owner.login}'s avatar`}
className="w-8 h-8 rounded-full border border-[#E5E5E5] dark:border-[#1A1A1A]"
/>
<div className="space-y-0.5">
<div className="text-xs text-bolt-elements-textPrimary font-medium">
Owner: {webAppInfo.repoInfo.owner.login}
<div className="text-sm flex items-center gap-2">
<div className="i-ph:git-commit text-bolt-elements-textSecondary w-4 h-4" />
<span className="text-bolt-elements-textSecondary">Commit:</span>
<span className="text-bolt-elements-textPrimary">{webAppInfo.gitInfo.local.commitHash}</span>
</div>
<div className="text-sm flex items-center gap-2">
<div className="i-ph:user text-bolt-elements-textSecondary w-4 h-4" />
<span className="text-bolt-elements-textSecondary">Author:</span>
<span className="text-bolt-elements-textPrimary">{webAppInfo.gitInfo.local.author}</span>
</div>
<div className="text-sm flex items-center gap-2">
<div className="i-ph:clock text-bolt-elements-textSecondary w-4 h-4" />
<span className="text-bolt-elements-textSecondary">Commit Time:</span>
<span className="text-bolt-elements-textPrimary">{webAppInfo.gitInfo.local.commitTime}</span>
</div>
{webAppInfo.gitInfo.github && (
<>
<div className="mt-4 pt-4 border-t border-gray-200 dark:border-gray-800">
<div className="text-sm flex items-center gap-2">
<div className="i-ph:git-fork text-bolt-elements-textSecondary w-4 h-4" />
<span className="text-bolt-elements-textSecondary">Repository:</span>
<span className="text-bolt-elements-textPrimary">
{webAppInfo.gitInfo.github.currentRepo.fullName}
{webAppInfo.gitInfo.isForked && ' (fork)'}
</span>
</div>
<div className="mt-2 flex items-center gap-4 text-sm">
<div className="flex items-center gap-1">
<div className="i-ph:star text-yellow-500 w-4 h-4" />
<span className="text-bolt-elements-textSecondary">
{webAppInfo.gitInfo.github.currentRepo.stars}
</span>
</div>
<div className="text-xs text-bolt-elements-textSecondary">
Last Update: {new Date(webAppInfo.repoInfo.lastUpdate).toLocaleDateString()}
<div className="flex items-center gap-1">
<div className="i-ph:git-fork text-blue-500 w-4 h-4" />
<span className="text-bolt-elements-textSecondary">
{webAppInfo.gitInfo.github.currentRepo.forks}
</span>
</div>
<div className="flex items-center gap-1">
<div className="i-ph:warning-circle text-red-500 w-4 h-4" />
<span className="text-bolt-elements-textSecondary">
{webAppInfo.gitInfo.github.currentRepo.openIssues}
</span>
</div>
</div>
</div>
<div className="grid grid-cols-3 gap-2 mt-2">
<div className="flex items-center gap-1 text-xs text-bolt-elements-textSecondary">
<div className="i-ph:star text-yellow-500 w-4 h-4" />
{webAppInfo.repoInfo.stars.toLocaleString()} stars
{webAppInfo.gitInfo.github.upstream && (
<div className="mt-2">
<div className="text-sm flex items-center gap-2">
<div className="i-ph:git-fork text-bolt-elements-textSecondary w-4 h-4" />
<span className="text-bolt-elements-textSecondary">Upstream:</span>
<span className="text-bolt-elements-textPrimary">
{webAppInfo.gitInfo.github.upstream.fullName}
</span>
</div>
<div className="mt-2 flex items-center gap-4 text-sm">
<div className="flex items-center gap-1">
<div className="i-ph:star text-yellow-500 w-4 h-4" />
<span className="text-bolt-elements-textSecondary">
{webAppInfo.gitInfo.github.upstream.stars}
</span>
</div>
<div className="flex items-center gap-1">
<div className="i-ph:git-fork text-blue-500 w-4 h-4" />
<span className="text-bolt-elements-textSecondary">
{webAppInfo.gitInfo.github.upstream.forks}
</span>
</div>
</div>
</div>
<div className="flex items-center gap-1 text-xs text-bolt-elements-textSecondary">
<div className="i-ph:git-fork text-blue-500 w-4 h-4" />
{webAppInfo.repoInfo.forks.toLocaleString()} forks
</div>
<div className="flex items-center gap-1 text-xs text-bolt-elements-textSecondary">
<div className="i-ph:warning-circle text-red-500 w-4 h-4" />
{webAppInfo.repoInfo.openIssues.toLocaleString()} issues
</div>
</div>
</div>
</div>
)}
)}
</>
)}
</div>
</div>
</div>
) : (
<div className="text-sm text-bolt-elements-textSecondary">
{loading.webAppInfo ? 'Loading webapp information...' : 'No webapp information available'}
)}
{webAppInfo && (
<div className="mt-6">
<h3 className="mb-4 text-base font-medium text-bolt-elements-textPrimary">Dependencies</h3>
<div className="space-y-2 bg-gray-50 dark:bg-[#1A1A1A] rounded-lg">
<DependencySection title="Production" deps={webAppInfo.dependencies.production} />
<DependencySection title="Development" deps={webAppInfo.dependencies.development} />
<DependencySection title="Peer" deps={webAppInfo.dependencies.peer} />
<DependencySection title="Optional" deps={webAppInfo.dependencies.optional} />
</div>
</div>
)}
</div>
@@ -1138,7 +1165,7 @@ export default function DebugTab() {
{/* Error Check */}
<Collapsible
open={openSections.errors}
onOpenChange={(open: boolean) => setOpenSections((prev) => ({ ...prev, errors: open }))}
onOpenChange={(open) => setOpenSections((prev) => ({ ...prev, errors: open }))}
className="w-full"
>
<CollapsibleTrigger className="w-full">
@@ -1146,14 +1173,14 @@ export default function DebugTab() {
<div className="flex items-center gap-3">
<div className="i-ph:warning text-red-500 w-5 h-5" />
<h3 className="text-base font-medium text-bolt-elements-textPrimary">Error Check</h3>
{errorLog.errors.length > 0 && (
{errorLogs.length > 0 && (
<Badge variant="destructive" className="ml-2">
{errorLog.errors.length} Errors
{errorLogs.length} Errors
</Badge>
)}
</div>
<div
className={cn(
className={classNames(
'i-ph:caret-down w-4 h-4 transform transition-transform duration-200',
openSections.errors ? 'rotate-180' : '',
)}
@@ -1175,31 +1202,33 @@ export default function DebugTab() {
</ul>
</div>
<div className="text-sm">
<span className="text-bolt-elements-textSecondary">Last Check: </span>
<span className="text-bolt-elements-textSecondary">Status: </span>
<span className="text-bolt-elements-textPrimary">
{loading.errors
? 'Checking...'
: errorLog.lastCheck
? `Last checked ${new Date(errorLog.lastCheck).toLocaleString()} (${errorLog.errors.length} errors found)`
: 'Click to check for errors'}
: errorLogs.length > 0
? `${errorLogs.length} errors found`
: 'No errors found'}
</span>
</div>
{errorLog.errors.length > 0 && (
{errorLogs.length > 0 && (
<div className="mt-4">
<div className="text-sm font-medium text-bolt-elements-textPrimary mb-2">Recent Errors:</div>
<div className="space-y-2">
{errorLog.errors.slice(0, 3).map((error, index) => (
<div key={index} className="text-sm text-red-500 dark:text-red-400">
{error.type === 'error' && `${error.message} (${error.filename}:${error.lineNumber})`}
{error.type === 'unhandledRejection' && `Unhandled Promise Rejection: ${error.reason}`}
{error.type === 'networkError' && `Network Error: Failed to load ${error.resource}`}
{errorLogs.map((error) => (
<div key={error.id} className="text-sm text-red-500 dark:text-red-400 p-2 rounded bg-red-500/5">
<div className="font-medium">{error.message}</div>
{error.source && (
<div className="text-xs mt-1 text-red-400">
Source: {error.source}
{error.details?.lineNumber && `:${error.details.lineNumber}`}
</div>
)}
{error.stack && (
<div className="text-xs mt-1 text-red-400 font-mono whitespace-pre-wrap">{error.stack}</div>
)}
</div>
))}
{errorLog.errors.length > 3 && (
<div className="text-sm text-bolt-elements-textSecondary">
And {errorLog.errors.length - 3} more errors...
</div>
)}
</div>
</div>
)}

View File

@@ -30,6 +30,7 @@ import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
import CloudProvidersTab from '~/components/settings/providers/CloudProvidersTab';
import LocalProvidersTab from '~/components/settings/providers/LocalProvidersTab';
import TaskManagerTab from '~/components/settings/task-manager/TaskManagerTab';
import ServiceStatusTab from '~/components/settings/providers/ServiceStatusTab';
import { Switch } from '~/components/ui/Switch';
interface DraggableTabTileProps {
@@ -57,7 +58,7 @@ const TAB_DESCRIPTIONS: Record<TabType, string> = {
'event-logs': 'View application event logs',
update: 'Check for updates',
'task-manager': 'Manage running tasks',
'service-status': 'View service health and status',
'service-status': 'Monitor provider service health and status',
};
const DraggableTabTile = ({
@@ -207,8 +208,6 @@ export const DeveloperWindow = ({ open, onClose }: DeveloperWindowProps) => {
// Only show tabs that are assigned to the developer window AND are visible
const visibleDeveloperTabs = useMemo(() => {
console.log('Filtering developer tabs with configuration:', tabConfiguration);
if (!tabConfiguration?.developerTabs || !Array.isArray(tabConfiguration.developerTabs)) {
console.warn('Invalid tab configuration, using empty array');
return [];
@@ -223,7 +222,6 @@ export const DeveloperWindow = ({ open, onClose }: DeveloperWindowProps) => {
// Hide notifications tab if notifications are disabled
if (tab.id === 'notifications' && !profile.notifications) {
console.log('Hiding notifications tab due to disabled notifications');
return false;
}
@@ -235,7 +233,6 @@ export const DeveloperWindow = ({ open, onClose }: DeveloperWindowProps) => {
// Only show tabs that are explicitly visible and assigned to the developer window
const isVisible = tab.visible && tab.window === 'developer';
console.log(`Tab ${tab.id} visibility:`, isVisible);
return isVisible;
})
@@ -247,8 +244,6 @@ export const DeveloperWindow = ({ open, onClose }: DeveloperWindowProps) => {
});
}, [tabConfiguration, profile.notifications]);
console.log('Filtered visible developer tabs:', visibleDeveloperTabs);
const moveTab = (dragIndex: number, hoverIndex: number) => {
const draggedTab = visibleDeveloperTabs[dragIndex];
const targetTab = visibleDeveloperTabs[hoverIndex];
@@ -324,6 +319,8 @@ export const DeveloperWindow = ({ open, onClose }: DeveloperWindowProps) => {
return <UpdateTab />;
case 'task-manager':
return <TaskManagerTab />;
case 'service-status':
return <ServiceStatusTab />;
default:
return null;
}

View File

@@ -177,7 +177,15 @@ export const TabManagement = () => {
const [searchQuery, setSearchQuery] = useState('');
// Define standard (visible by default) tabs for each window
const standardUserTabs: TabType[] = ['features', 'data', 'local-providers', 'cloud-providers', 'connection', 'debug'];
const standardUserTabs: TabType[] = [
'features',
'data',
'local-providers',
'cloud-providers',
'connection',
'debug',
'service-status',
];
const standardDeveloperTabs: TabType[] = [
'profile',
'settings',
@@ -190,6 +198,8 @@ export const TabManagement = () => {
'debug',
'event-logs',
'update',
'task-manager',
'service-status',
];
const handleVisibilityChange = (tabId: TabType, enabled: boolean, targetWindow: 'user' | 'developer') => {

View File

@@ -5,7 +5,6 @@ import { logStore, type LogEntry } from '~/lib/stores/logs';
import { useStore } from '@nanostores/react';
import { classNames } from '~/utils/classNames';
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
import { settingsStyles } from '~/components/settings/settings.styles';
interface SelectOption {
value: string;
@@ -231,7 +230,13 @@ export function EventLogsTab() {
const selectedCategoryOption = logCategoryOptions.find((opt) => opt.value === selectedCategory);
return (
<div className="flex flex-col h-full gap-4 p-6">
<div
className={classNames(
'rounded-lg border bg-bolt-elements-background text-bolt-elements-textPrimary shadow-sm p-4',
'hover:bg-bolt-elements-background-depth-2',
'transition-all duration-200',
)}
>
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
@@ -245,10 +250,11 @@ export function EventLogsTab() {
<button
onClick={handleRefresh}
className={classNames(
settingsStyles.button.base,
settingsStyles.button.secondary,
'hover:bg-purple-500/10 hover:text-purple-500',
'dark:bg-[#1A1A1A] dark:hover:bg-purple-500/20 dark:text-bolt-elements-textPrimary dark:hover:text-purple-500',
'rounded-md px-4 py-2 text-sm',
'bg-purple-500 text-white',
'hover:bg-purple-600',
'dark:bg-purple-500 dark:hover:bg-purple-600',
'transition-all duration-200',
)}
>
<div className="i-ph:arrows-clockwise text-lg" />
@@ -292,7 +298,13 @@ export function EventLogsTab() {
<motion.button
onClick={handleExportLogs}
className="flex items-center gap-2 px-4 py-1.5 rounded-lg text-sm bg-purple-500 hover:bg-purple-600 text-white transition-colors"
className={classNames(
'rounded-md px-4 py-2 text-sm',
'bg-purple-500 text-white',
'hover:bg-purple-600',
'dark:bg-purple-500 dark:hover:bg-purple-600',
'transition-all duration-200',
)}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
@@ -309,7 +321,6 @@ export function EventLogsTab() {
<DropdownMenu.Trigger asChild>
<button
className={classNames(
'flex items-center gap-2',
'rounded-lg px-3 py-1.5',
'text-sm text-gray-900 dark:text-white',
'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
@@ -360,7 +371,6 @@ export function EventLogsTab() {
<DropdownMenu.Trigger asChild>
<button
className={classNames(
'flex items-center gap-2',
'rounded-lg px-3 py-1.5',
'text-sm text-gray-900 dark:text-white',
'bg-[#FAFAFA] dark:bg-[#0A0A0A]',

View File

@@ -4,7 +4,6 @@ import { toast } from 'react-toastify';
import { classNames } from '~/utils/classNames';
import type { UserProfile } from '~/components/settings/settings.types';
import { motion } from 'framer-motion';
import { settingsStyles } from '~/components/settings/settings.styles';
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
const ALLOWED_FILE_TYPES = ['image/jpeg', 'image/png', 'image/gif'];
@@ -112,7 +111,13 @@ export default function ProfileTab() {
};
return (
<div className="space-y-4">
<div
className={classNames(
'rounded-lg border bg-bolt-elements-background text-bolt-elements-textPrimary shadow-sm p-4',
'hover:bg-bolt-elements-background-depth-2',
'transition-all duration-200',
)}
>
{/* Profile Information */}
<motion.div
className="bg-white dark:bg-[#0A0A0A] rounded-lg shadow-sm dark:shadow-none"
@@ -249,10 +254,11 @@ export default function ProfileTab() {
onClick={handleSave}
disabled={isLoading}
className={classNames(
settingsStyles.button.base,
settingsStyles.button.primary,
'rounded-md px-4 py-2 text-sm',
'bg-purple-500 text-white',
'hover:bg-purple-600',
'disabled:opacity-50 disabled:cursor-not-allowed',
'dark:bg-purple-500 dark:hover:bg-purple-600',
'transition-all duration-200',
)}
>
{isLoading ? (

View File

@@ -6,7 +6,6 @@ import type { IProviderConfig } from '~/types/model';
import { logStore } from '~/lib/stores/logs';
import { motion } from 'framer-motion';
import { classNames } from '~/utils/classNames';
import { settingsStyles } from '~/components/settings/settings.styles';
import { toast } from 'react-toastify';
import { providerBaseUrlEnvKeys } from '~/utils/constants';
import { SiAmazon, SiGoogle, SiHuggingface, SiPerplexity, SiOpenai } from 'react-icons/si';
@@ -168,7 +167,7 @@ const CloudProvidersTab = () => {
<motion.div
key={provider.name}
className={classNames(
settingsStyles.card,
'rounded-lg border bg-bolt-elements-background text-bolt-elements-textPrimary shadow-sm',
'bg-bolt-elements-background-depth-2',
'hover:bg-bolt-elements-background-depth-3',
'transition-all duration-200',

View File

@@ -6,7 +6,6 @@ import type { IProviderConfig } from '~/types/model';
import { logStore } from '~/lib/stores/logs';
import { motion } from 'framer-motion';
import { classNames } from '~/utils/classNames';
import { settingsStyles } from '~/components/settings/settings.styles';
import { BsRobot } from 'react-icons/bs';
import type { IconType } from 'react-icons';
import { BiChip } from 'react-icons/bi';
@@ -473,7 +472,13 @@ export function LocalProvidersTab() {
}, []);
return (
<div className="space-y-6">
<div
className={classNames(
'rounded-lg border bg-bolt-elements-background text-bolt-elements-textPrimary shadow-sm p-4',
'hover:bg-bolt-elements-background-depth-2',
'transition-all duration-200',
)}
>
{/* Service Status Indicator - Move to top */}
<div
className={classNames(
@@ -526,7 +531,6 @@ export function LocalProvidersTab() {
<motion.div
key={provider.name}
className={classNames(
settingsStyles.card,
'bg-bolt-elements-background-depth-2',
'hover:bg-bolt-elements-background-depth-3',
'transition-all duration-200',
@@ -728,9 +732,11 @@ export function LocalProvidersTab() {
onClick={() => handleUpdateOllamaModel(model.name)}
disabled={model.status === 'updating'}
className={classNames(
settingsStyles.button.base,
settingsStyles.button.secondary,
'hover:bg-purple-500/10 hover:text-purple-500',
'rounded-md px-4 py-2 text-sm',
'bg-purple-500 text-white',
'hover:bg-purple-600',
'dark:bg-purple-500 dark:hover:bg-purple-600',
'transition-all duration-200',
)}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
@@ -746,9 +752,11 @@ export function LocalProvidersTab() {
}}
disabled={model.status === 'updating'}
className={classNames(
settingsStyles.button.base,
settingsStyles.button.secondary,
'hover:bg-red-500/10 hover:text-red-500',
'rounded-md px-4 py-2 text-sm',
'bg-red-500 text-white',
'hover:bg-red-600',
'dark:bg-red-500 dark:hover:bg-red-600',
'transition-all duration-200',
)}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
@@ -839,10 +847,11 @@ export function LocalProvidersTab() {
onClick={() => handleManualInstall(manualInstall.modelString)}
disabled={!manualInstall.modelString || !!isInstallingModel}
className={classNames(
settingsStyles.button.base,
settingsStyles.button.primary,
'hover:bg-purple-500/10 hover:text-purple-500',
'min-w-[120px] justify-center',
'rounded-md px-4 py-2 text-sm',
'bg-purple-500 text-white',
'hover:bg-purple-600',
'dark:bg-purple-500 dark:hover:bg-purple-600',
'transition-all duration-200',
)}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
@@ -867,10 +876,11 @@ export function LocalProvidersTab() {
error('Installation cancelled');
}}
className={classNames(
settingsStyles.button.base,
settingsStyles.button.secondary,
'hover:bg-red-500/10 hover:text-red-500',
'min-w-[100px] justify-center',
'rounded-md px-4 py-2 text-sm',
'bg-red-500 text-white',
'hover:bg-red-600',
'dark:bg-red-500 dark:hover:bg-red-600',
'transition-all duration-200',
)}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}

View File

@@ -2,8 +2,6 @@ import React, { useEffect, useState } from 'react';
import { motion } from 'framer-motion';
import { toast } from 'react-toastify';
import { classNames } from '~/utils/classNames';
import { settingsStyles } from '~/components/settings/settings.styles';
import { DialogTitle, DialogDescription } from '~/components/ui/Dialog';
interface OllamaModel {
name: string;
@@ -197,108 +195,135 @@ export default function OllamaModelUpdater() {
if (isLoading) {
return (
<div className="flex items-center justify-center p-4">
<div className={settingsStyles['loading-spinner']} />
<div
className={classNames(
'rounded-full border-4 border-t-4 border-b-4 border-purple-500',
'h-16 w-16 animate-spin',
)}
/>
<span className="ml-2 text-bolt-elements-textSecondary">Loading models...</span>
</div>
);
}
return (
<div className="space-y-4">
<div className="space-y-2">
<DialogTitle>Ollama Model Manager</DialogTitle>
<DialogDescription>Update your local Ollama models to their latest versions</DialogDescription>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="i-ph:arrows-clockwise text-purple-500" />
<span className="text-sm text-bolt-elements-textPrimary">{models.length} models available</span>
<div
className={classNames(
'rounded-lg border bg-bolt-elements-background text-bolt-elements-textPrimary shadow-sm p-4',
'hover:bg-bolt-elements-background-depth-2',
'transition-all duration-200',
)}
>
<div className="space-y-4">
<div className="space-y-2">
<h2 className="text-2xl font-bold">Ollama Model Manager</h2>
<p>Update your local Ollama models to their latest versions</p>
</div>
<motion.button
onClick={handleBulkUpdate}
disabled={isBulkUpdating}
className={classNames(
settingsStyles.button.base,
settingsStyles.button.primary,
'hover:bg-purple-500/10 hover:text-purple-500',
'dark:hover:bg-purple-500/20 dark:hover:text-purple-500',
)}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
{isBulkUpdating ? (
<>
<div className={settingsStyles['loading-spinner']} />
Updating All...
</>
) : (
<>
<div className="i-ph:arrows-clockwise" />
Update All Models
</>
)}
</motion.button>
</div>
<div className="space-y-2">
{models.map((model) => (
<div
key={model.name}
className={classNames(
'flex items-center justify-between p-3 rounded-lg',
'bg-[#F8F8F8] dark:bg-[#1A1A1A]',
'border border-[#E5E5E5] dark:border-[#333333]',
)}
>
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<div className="i-ph:cube text-purple-500" />
<span className="text-sm text-bolt-elements-textPrimary">{model.name}</span>
{model.status === 'updating' && <div className={settingsStyles['loading-spinner']} />}
{model.status === 'updated' && <div className="i-ph:check-circle text-green-500" />}
{model.status === 'error' && <div className="i-ph:x-circle text-red-500" />}
</div>
<div className="flex items-center gap-2 text-xs text-bolt-elements-textSecondary">
<span>Version: {model.digest.substring(0, 7)}</span>
{model.status === 'updated' && model.newDigest && (
<>
<div className="i-ph:arrow-right w-3 h-3" />
<span className="text-green-500">{model.newDigest.substring(0, 7)}</span>
</>
)}
{model.progress && (
<span className="ml-2">
{model.progress.status}{' '}
{model.progress.total > 0 && (
<>({Math.round((model.progress.current / model.progress.total) * 100)}%)</>
)}
</span>
)}
{model.details && (
<span className="ml-2">
({model.details.parameter_size}, {model.details.quantization_level})
</span>
)}
</div>
</div>
<motion.button
onClick={() => handleSingleUpdate(model.name)}
disabled={model.status === 'updating'}
className={classNames(
settingsStyles.button.base,
settingsStyles.button.secondary,
'hover:bg-purple-500/10 hover:text-purple-500',
'dark:bg-[#1A1A1A] dark:hover:bg-purple-500/20 dark:text-bolt-elements-textPrimary dark:hover:text-purple-500',
)}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<div className="i-ph:arrows-clockwise" />
Update
</motion.button>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="i-ph:arrows-clockwise text-purple-500" />
<span className="text-sm text-bolt-elements-textPrimary">{models.length} models available</span>
</div>
))}
<motion.button
onClick={handleBulkUpdate}
disabled={isBulkUpdating}
className={classNames(
'rounded-md px-4 py-2 text-sm',
'bg-purple-500 text-white',
'hover:bg-purple-600',
'dark:bg-purple-500 dark:hover:bg-purple-600',
'transition-all duration-200',
)}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
{isBulkUpdating ? (
<>
<div
className={classNames(
'rounded-full border-4 border-t-4 border-b-4 border-purple-500',
'h-4 w-4 animate-spin mr-2',
)}
/>
Updating All...
</>
) : (
<>
<div className="i-ph:arrows-clockwise" />
Update All Models
</>
)}
</motion.button>
</div>
<div className="space-y-2">
{models.map((model) => (
<div
key={model.name}
className={classNames(
'flex items-center justify-between p-3 rounded-lg',
'bg-[#F8F8F8] dark:bg-[#1A1A1A]',
'border border-[#E5E5E5] dark:border-[#333333]',
)}
>
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<div className="i-ph:cube text-purple-500" />
<span className="text-sm text-bolt-elements-textPrimary">{model.name}</span>
{model.status === 'updating' && (
<div
className={classNames(
'rounded-full border-4 border-t-4 border-b-4 border-purple-500',
'h-4 w-4 animate-spin',
)}
/>
)}
{model.status === 'updated' && <div className="i-ph:check-circle text-green-500" />}
{model.status === 'error' && <div className="i-ph:x-circle text-red-500" />}
</div>
<div className="flex items-center gap-2 text-xs text-bolt-elements-textSecondary">
<span>Version: {model.digest.substring(0, 7)}</span>
{model.status === 'updated' && model.newDigest && (
<>
<div className="i-ph:arrow-right w-3 h-3" />
<span className="text-green-500">{model.newDigest.substring(0, 7)}</span>
</>
)}
{model.progress && (
<span className="ml-2">
{model.progress.status}{' '}
{model.progress.total > 0 && (
<>({Math.round((model.progress.current / model.progress.total) * 100)}%)</>
)}
</span>
)}
{model.details && (
<span className="ml-2">
({model.details.parameter_size}, {model.details.quantization_level})
</span>
)}
</div>
</div>
<motion.button
onClick={() => handleSingleUpdate(model.name)}
disabled={model.status === 'updating'}
className={classNames(
'rounded-md px-4 py-2 text-sm',
'bg-purple-500 text-white',
'hover:bg-purple-600',
'dark:bg-purple-500 dark:hover:bg-purple-600',
'transition-all duration-200',
)}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<div className="i-ph:arrows-clockwise" />
Update
</motion.button>
</div>
))}
</div>
</div>
</div>
);

View File

@@ -0,0 +1,135 @@
import { useState, useEffect } from 'react';
import type { ServiceStatus } from './types';
import { ProviderStatusCheckerFactory } from './provider-factory';
export default function ServiceStatusTab() {
const [serviceStatuses, setServiceStatuses] = useState<ServiceStatus[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const checkAllProviders = async () => {
try {
setLoading(true);
setError(null);
const providers = ProviderStatusCheckerFactory.getProviderNames();
const statuses: ServiceStatus[] = [];
for (const provider of providers) {
try {
const checker = ProviderStatusCheckerFactory.getChecker(provider);
const result = await checker.checkStatus();
statuses.push({
provider,
...result,
lastChecked: new Date().toISOString(),
});
} catch (err) {
console.error(`Error checking ${provider} status:`, err);
statuses.push({
provider,
status: 'degraded',
message: 'Unable to check service status',
incidents: ['Error checking service status'],
lastChecked: new Date().toISOString(),
});
}
}
setServiceStatuses(statuses);
} catch (err) {
console.error('Error checking provider statuses:', err);
setError('Failed to check service statuses');
} finally {
setLoading(false);
}
};
checkAllProviders();
// Set up periodic checks every 5 minutes
const interval = setInterval(checkAllProviders, 5 * 60 * 1000);
return () => clearInterval(interval);
}, []);
const getStatusColor = (status: ServiceStatus['status']) => {
switch (status) {
case 'operational':
return 'text-green-500 dark:text-green-400';
case 'degraded':
return 'text-yellow-500 dark:text-yellow-400';
case 'down':
return 'text-red-500 dark:text-red-400';
default:
return 'text-gray-500 dark:text-gray-400';
}
};
const getStatusIcon = (status: ServiceStatus['status']) => {
switch (status) {
case 'operational':
return 'i-ph:check-circle';
case 'degraded':
return 'i-ph:warning';
case 'down':
return 'i-ph:x-circle';
default:
return 'i-ph:question';
}
};
if (loading) {
return (
<div className="flex items-center justify-center h-full">
<div className="animate-spin i-ph:circle-notch w-8 h-8 text-purple-500" />
</div>
);
}
if (error) {
return (
<div className="flex flex-col items-center justify-center h-full text-red-500 dark:text-red-400">
<div className="i-ph:warning w-8 h-8 mb-2" />
<p>{error}</p>
</div>
);
}
return (
<div className="space-y-6">
<div className="grid grid-cols-1 gap-4">
{serviceStatuses.map((service) => (
<div
key={service.provider}
className="p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700"
>
<div className="flex items-center justify-between mb-2">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">{service.provider}</h3>
<div className={`flex items-center ${getStatusColor(service.status)}`}>
<div className={`${getStatusIcon(service.status)} w-5 h-5 mr-2`} />
<span className="capitalize">{service.status}</span>
</div>
</div>
<p className="text-gray-600 dark:text-gray-300 mb-2">{service.message}</p>
{service.incidents && service.incidents.length > 0 && (
<div className="mt-2">
<h4 className="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-1">Recent Incidents:</h4>
<ul className="text-sm text-gray-600 dark:text-gray-400 space-y-1">
{service.incidents.map((incident, index) => (
<li key={index}>{incident}</li>
))}
</ul>
</div>
)}
<div className="mt-2 text-xs text-gray-500 dark:text-gray-400">
Last checked: {new Date(service.lastChecked).toLocaleString()}
</div>
</div>
))}
</div>
</div>
);
}

View File

@@ -1,122 +1,91 @@
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
import { AmazonBedrockStatusChecker } from './providers/amazon-bedrock';
import { CohereStatusChecker } from './providers/cohere';
import { DeepseekStatusChecker } from './providers/deepseek';
import { GoogleStatusChecker } from './providers/google';
import { GroqStatusChecker } from './providers/groq';
import { HuggingFaceStatusChecker } from './providers/huggingface';
import { HyperbolicStatusChecker } from './providers/hyperbolic';
import { MistralStatusChecker } from './providers/mistral';
import { OpenRouterStatusChecker } from './providers/openrouter';
import { PerplexityStatusChecker } from './providers/perplexity';
import { TogetherStatusChecker } from './providers/together';
import { XAIStatusChecker } from './providers/xai';
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',
},
headers: {},
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',
},
headers: {},
testModel: 'command',
},
Deepseek: {
statusUrl: 'https://status.deepseek.com/',
apiUrl: 'https://api.deepseek.com/v1/models',
headers: {
Authorization: 'Bearer $DEEPSEEK_API_KEY',
},
headers: {},
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',
},
headers: {},
testModel: 'gemini-pro',
},
Groq: {
statusUrl: 'https://groqstatus.com/',
apiUrl: 'https://api.groq.com/v1/models',
headers: {
Authorization: 'Bearer $GROQ_API_KEY',
},
headers: {},
testModel: 'mixtral-8x7b-32768',
},
HuggingFace: {
statusUrl: 'https://status.huggingface.co/',
apiUrl: 'https://api-inference.huggingface.co/models',
headers: {
Authorization: 'Bearer $HUGGINGFACE_API_KEY',
},
headers: {},
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',
},
headers: {},
testModel: 'hyperbolic-1',
},
Mistral: {
statusUrl: 'https://status.mistral.ai/',
apiUrl: 'https://api.mistral.ai/v1/models',
headers: {
Authorization: 'Bearer $MISTRAL_API_KEY',
},
headers: {},
testModel: 'mistral-tiny',
},
OpenRouter: {
statusUrl: 'https://status.openrouter.ai/',
apiUrl: 'https://openrouter.ai/api/v1/models',
headers: {
Authorization: 'Bearer $OPEN_ROUTER_API_KEY',
},
headers: {},
testModel: 'anthropic/claude-3-sonnet',
},
Perplexity: {
statusUrl: 'https://status.perplexity.com/',
apiUrl: 'https://api.perplexity.ai/v1/models',
headers: {
Authorization: 'Bearer $PERPLEXITY_API_KEY',
},
headers: {},
testModel: 'pplx-7b-chat',
},
Together: {
statusUrl: 'https://status.together.ai/',
apiUrl: 'https://api.together.xyz/v1/models',
headers: {
Authorization: 'Bearer $TOGETHER_API_KEY',
},
headers: {},
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',
},
headers: {},
testModel: 'grok-1',
},
};
@@ -128,12 +97,31 @@ export class ProviderStatusCheckerFactory {
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
case 'AmazonBedrock':
return new AmazonBedrockStatusChecker(config);
case 'Cohere':
return new CohereStatusChecker(config);
case 'Deepseek':
return new DeepseekStatusChecker(config);
case 'Google':
return new GoogleStatusChecker(config);
case 'Groq':
return new GroqStatusChecker(config);
case 'HuggingFace':
return new HuggingFaceStatusChecker(config);
case 'Hyperbolic':
return new HyperbolicStatusChecker(config);
case 'Mistral':
return new MistralStatusChecker(config);
case 'OpenRouter':
return new OpenRouterStatusChecker(config);
case 'Perplexity':
return new PerplexityStatusChecker(config);
case 'Together':
return new TogetherStatusChecker(config);
case 'XAI':
return new XAIStatusChecker(config);
default:
return new (class extends BaseProviderChecker {
async checkStatus(): Promise<StatusCheckResult> {
@@ -154,7 +142,13 @@ export class ProviderStatusCheckerFactory {
return Object.keys(this._providerConfigs) as ProviderName[];
}
static getProviderConfig(provider: ProviderName): ProviderConfig | undefined {
return this._providerConfigs[provider];
static getProviderConfig(provider: ProviderName): ProviderConfig {
const config = this._providerConfigs[provider];
if (!config) {
throw new Error(`Unknown provider: ${provider}`);
}
return config;
}
}

View File

@@ -0,0 +1,76 @@
import { BaseProviderChecker } from '~/components/settings/providers/service-status/base-provider';
import type { StatusCheckResult } from '~/components/settings/providers/service-status/types';
export class AmazonBedrockStatusChecker extends BaseProviderChecker {
async checkStatus(): Promise<StatusCheckResult> {
try {
// Check AWS health status page
const statusPageResponse = await fetch('https://health.aws.amazon.com/health/status');
const text = await statusPageResponse.text();
// Check for Bedrock and general AWS status
const hasBedrockIssues =
text.includes('Amazon Bedrock') &&
(text.includes('Service is experiencing elevated error rates') ||
text.includes('Service disruption') ||
text.includes('Degraded Service'));
const hasGeneralIssues = text.includes('Service disruption') || text.includes('Multiple services affected');
// Extract incidents
const incidents: string[] = [];
const incidentMatches = text.matchAll(/(\d{4}-\d{2}-\d{2})\s+(.*?)\s+Impact:(.*?)(?=\n|$)/g);
for (const match of incidentMatches) {
const [, date, title, impact] = match;
if (title.includes('Bedrock') || title.includes('AWS')) {
incidents.push(`${date}: ${title.trim()} - Impact: ${impact.trim()}`);
}
}
let status: StatusCheckResult['status'] = 'operational';
let message = 'All services operational';
if (hasBedrockIssues) {
status = 'degraded';
message = 'Amazon Bedrock service issues reported';
} else if (hasGeneralIssues) {
status = 'degraded';
message = 'AWS experiencing general issues';
}
// If status page check fails, fallback to endpoint check
if (!statusPageResponse.ok) {
const endpointStatus = await this.checkEndpoint('https://health.aws.amazon.com/health/status');
const apiEndpoint = 'https://bedrock.us-east-1.amazonaws.com/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,
incidents: incidents.slice(0, 5),
};
} catch (error) {
console.error('Error checking Amazon Bedrock status:', error);
// Fallback to basic endpoint check
const endpointStatus = await this.checkEndpoint('https://health.aws.amazon.com/health/status');
const apiEndpoint = 'https://bedrock.us-east-1.amazonaws.com/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,80 @@
import { BaseProviderChecker } from '~/components/settings/providers/service-status/base-provider';
import type { StatusCheckResult } from '~/components/settings/providers/service-status/types';
export class AnthropicStatusChecker extends BaseProviderChecker {
async checkStatus(): Promise<StatusCheckResult> {
try {
// Check status page
const statusPageResponse = await fetch('https://status.anthropic.com/');
const text = await statusPageResponse.text();
// Check for specific Anthropic status indicators
const isOperational = text.includes('All Systems Operational');
const hasDegradedPerformance = text.includes('Degraded Performance');
const hasPartialOutage = text.includes('Partial Outage');
const hasMajorOutage = text.includes('Major Outage');
// Extract incidents
const incidents: string[] = [];
const incidentSection = text.match(/Past Incidents(.*?)(?=\n\n)/s);
if (incidentSection) {
const incidentLines = incidentSection[1]
.split('\n')
.map((line) => line.trim())
.filter((line) => line && line.includes('202')); // Only get dated incidents
incidents.push(...incidentLines.slice(0, 5));
}
let status: StatusCheckResult['status'] = 'operational';
let message = 'All systems operational';
if (hasMajorOutage) {
status = 'down';
message = 'Major service outage';
} else if (hasPartialOutage) {
status = 'down';
message = 'Partial service outage';
} else if (hasDegradedPerformance) {
status = 'degraded';
message = 'Service experiencing degraded performance';
} else if (!isOperational) {
status = 'degraded';
message = 'Service status unknown';
}
// If status page check fails, fallback to endpoint check
if (!statusPageResponse.ok) {
const endpointStatus = await this.checkEndpoint('https://status.anthropic.com/');
const apiEndpoint = 'https://api.anthropic.com/v1/messages';
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,
incidents,
};
} catch (error) {
console.error('Error checking Anthropic status:', error);
// Fallback to basic endpoint check
const endpointStatus = await this.checkEndpoint('https://status.anthropic.com/');
const apiEndpoint = 'https://api.anthropic.com/v1/messages';
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,91 @@
import { BaseProviderChecker } from '~/components/settings/providers/service-status/base-provider';
import type { StatusCheckResult } from '~/components/settings/providers/service-status/types';
export class CohereStatusChecker extends BaseProviderChecker {
async checkStatus(): Promise<StatusCheckResult> {
try {
// Check status page
const statusPageResponse = await fetch('https://status.cohere.com/');
const text = await statusPageResponse.text();
// Check for specific Cohere status indicators
const isOperational = text.includes('All Systems Operational');
const hasIncidents = text.includes('Active Incidents');
const hasDegradation = text.includes('Degraded Performance');
const hasOutage = text.includes('Service Outage');
// Extract incidents
const incidents: string[] = [];
const incidentSection = text.match(/Past Incidents(.*?)(?=\n\n)/s);
if (incidentSection) {
const incidentLines = incidentSection[1]
.split('\n')
.map((line) => line.trim())
.filter((line) => line && line.includes('202')); // Only get dated incidents
incidents.push(...incidentLines.slice(0, 5));
}
// Check specific services
const services = {
api: {
operational: text.includes('API Service') && text.includes('Operational'),
degraded: text.includes('API Service') && text.includes('Degraded Performance'),
outage: text.includes('API Service') && text.includes('Service Outage'),
},
generation: {
operational: text.includes('Generation Service') && text.includes('Operational'),
degraded: text.includes('Generation Service') && text.includes('Degraded Performance'),
outage: text.includes('Generation Service') && text.includes('Service Outage'),
},
};
let status: StatusCheckResult['status'] = 'operational';
let message = 'All systems operational';
if (services.api.outage || services.generation.outage || hasOutage) {
status = 'down';
message = 'Service outage detected';
} else if (services.api.degraded || services.generation.degraded || hasDegradation || hasIncidents) {
status = 'degraded';
message = 'Service experiencing issues';
} else if (!isOperational) {
status = 'degraded';
message = 'Service status unknown';
}
// If status page check fails, fallback to endpoint check
if (!statusPageResponse.ok) {
const endpointStatus = await this.checkEndpoint('https://status.cohere.com/');
const apiEndpoint = 'https://api.cohere.ai/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,
incidents,
};
} catch (error) {
console.error('Error checking Cohere status:', error);
// Fallback to basic endpoint check
const endpointStatus = await this.checkEndpoint('https://status.cohere.com/');
const apiEndpoint = 'https://api.cohere.ai/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,40 @@
import { BaseProviderChecker } from '~/components/settings/providers/service-status/base-provider';
import type { StatusCheckResult } from '~/components/settings/providers/service-status/types';
export class DeepseekStatusChecker extends BaseProviderChecker {
async checkStatus(): Promise<StatusCheckResult> {
try {
/*
* Check status page - Note: Deepseek doesn't have a public status page yet
* so we'll check their API endpoint directly
*/
const apiEndpoint = 'https://api.deepseek.com/v1/models';
const apiStatus = await this.checkEndpoint(apiEndpoint);
// Check their website as a secondary indicator
const websiteStatus = await this.checkEndpoint('https://deepseek.com');
let status: StatusCheckResult['status'] = 'operational';
let message = 'All systems operational';
if (apiStatus !== 'reachable' || websiteStatus !== 'reachable') {
status = apiStatus !== 'reachable' ? 'down' : 'degraded';
message = apiStatus !== 'reachable' ? 'API appears to be down' : 'Service may be experiencing issues';
}
return {
status,
message,
incidents: [], // No public incident tracking available yet
};
} catch (error) {
console.error('Error checking Deepseek status:', error);
return {
status: 'degraded',
message: 'Unable to determine service status',
incidents: ['Note: Limited status information available'],
};
}
}
}

View File

@@ -0,0 +1,77 @@
import { BaseProviderChecker } from '~/components/settings/providers/service-status/base-provider';
import type { StatusCheckResult } from '~/components/settings/providers/service-status/types';
export class GoogleStatusChecker extends BaseProviderChecker {
async checkStatus(): Promise<StatusCheckResult> {
try {
// Check status page
const statusPageResponse = await fetch('https://status.cloud.google.com/');
const text = await statusPageResponse.text();
// Check for Vertex AI and general cloud status
const hasVertexAIIssues =
text.includes('Vertex AI') &&
(text.includes('Incident') ||
text.includes('Disruption') ||
text.includes('Outage') ||
text.includes('degraded'));
const hasGeneralIssues = text.includes('Major Incidents') || text.includes('Service Disruption');
// Extract incidents
const incidents: string[] = [];
const incidentMatches = text.matchAll(/(\d{4}-\d{2}-\d{2})\s+(.*?)\s+Impact:(.*?)(?=\n|$)/g);
for (const match of incidentMatches) {
const [, date, title, impact] = match;
if (title.includes('Vertex AI') || title.includes('Cloud')) {
incidents.push(`${date}: ${title.trim()} - Impact: ${impact.trim()}`);
}
}
let status: StatusCheckResult['status'] = 'operational';
let message = 'All services operational';
if (hasVertexAIIssues) {
status = 'degraded';
message = 'Vertex AI service issues reported';
} else if (hasGeneralIssues) {
status = 'degraded';
message = 'Google Cloud experiencing issues';
}
// If status page check fails, fallback to endpoint check
if (!statusPageResponse.ok) {
const endpointStatus = await this.checkEndpoint('https://status.cloud.google.com/');
const apiEndpoint = 'https://generativelanguage.googleapis.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,
incidents: incidents.slice(0, 5),
};
} catch (error) {
console.error('Error checking Google status:', error);
// Fallback to basic endpoint check
const endpointStatus = await this.checkEndpoint('https://status.cloud.google.com/');
const apiEndpoint = 'https://generativelanguage.googleapis.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,72 @@
import { BaseProviderChecker } from '~/components/settings/providers/service-status/base-provider';
import type { StatusCheckResult } from '~/components/settings/providers/service-status/types';
export class GroqStatusChecker extends BaseProviderChecker {
async checkStatus(): Promise<StatusCheckResult> {
try {
// Check status page
const statusPageResponse = await fetch('https://groqstatus.com/');
const text = await statusPageResponse.text();
const isOperational = text.includes('All Systems Operational');
const hasIncidents = text.includes('Active Incidents');
const hasDegradation = text.includes('Degraded Performance');
const hasOutage = text.includes('Service Outage');
// Extract incidents
const incidents: string[] = [];
const incidentMatches = text.matchAll(/(\d{4}-\d{2}-\d{2})\s+(.*?)\s+Status:(.*?)(?=\n|$)/g);
for (const match of incidentMatches) {
const [, date, title, status] = match;
incidents.push(`${date}: ${title.trim()} - ${status.trim()}`);
}
let status: StatusCheckResult['status'] = 'operational';
let message = 'All systems operational';
if (hasOutage) {
status = 'down';
message = 'Service outage detected';
} else if (hasDegradation || hasIncidents) {
status = 'degraded';
message = 'Service experiencing issues';
} else if (!isOperational) {
status = 'degraded';
message = 'Service status unknown';
}
// If status page check fails, fallback to endpoint check
if (!statusPageResponse.ok) {
const endpointStatus = await this.checkEndpoint('https://groqstatus.com/');
const apiEndpoint = 'https://api.groq.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,
incidents: incidents.slice(0, 5),
};
} catch (error) {
console.error('Error checking Groq status:', error);
// Fallback to basic endpoint check
const endpointStatus = await this.checkEndpoint('https://groqstatus.com/');
const apiEndpoint = 'https://api.groq.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,98 @@
import { BaseProviderChecker } from '~/components/settings/providers/service-status/base-provider';
import type { StatusCheckResult } from '~/components/settings/providers/service-status/types';
export class HuggingFaceStatusChecker extends BaseProviderChecker {
async checkStatus(): Promise<StatusCheckResult> {
try {
// Check status page
const statusPageResponse = await fetch('https://status.huggingface.co/');
const text = await statusPageResponse.text();
// Check for "All services are online" message
const allServicesOnline = text.includes('All services are online');
// Get last update time
const lastUpdateMatch = text.match(/Last updated on (.*?)(EST|PST|GMT)/);
const lastUpdate = lastUpdateMatch ? `${lastUpdateMatch[1]}${lastUpdateMatch[2]}` : '';
// Check individual services and their uptime percentages
const services = {
'Huggingface Hub': {
operational: text.includes('Huggingface Hub') && text.includes('Operational'),
uptime: text.match(/Huggingface Hub[\s\S]*?(\d+\.\d+)%\s*uptime/)?.[1],
},
'Git Hosting and Serving': {
operational: text.includes('Git Hosting and Serving') && text.includes('Operational'),
uptime: text.match(/Git Hosting and Serving[\s\S]*?(\d+\.\d+)%\s*uptime/)?.[1],
},
'Inference API': {
operational: text.includes('Inference API') && text.includes('Operational'),
uptime: text.match(/Inference API[\s\S]*?(\d+\.\d+)%\s*uptime/)?.[1],
},
'HF Endpoints': {
operational: text.includes('HF Endpoints') && text.includes('Operational'),
uptime: text.match(/HF Endpoints[\s\S]*?(\d+\.\d+)%\s*uptime/)?.[1],
},
Spaces: {
operational: text.includes('Spaces') && text.includes('Operational'),
uptime: text.match(/Spaces[\s\S]*?(\d+\.\d+)%\s*uptime/)?.[1],
},
};
// Create service status messages with uptime
const serviceMessages = Object.entries(services).map(([name, info]) => {
if (info.uptime) {
return `${name}: ${info.uptime}% uptime`;
}
return `${name}: ${info.operational ? 'Operational' : 'Issues detected'}`;
});
// Determine overall status
let status: StatusCheckResult['status'] = 'operational';
let message = allServicesOnline
? `All services are online (Last updated on ${lastUpdate})`
: 'Checking individual services';
// Only mark as degraded if we explicitly detect issues
const hasIssues = Object.values(services).some((service) => !service.operational);
if (hasIssues) {
status = 'degraded';
message = `Service issues detected (Last updated on ${lastUpdate})`;
}
// If status page check fails, fallback to endpoint check
if (!statusPageResponse.ok) {
const endpointStatus = await this.checkEndpoint('https://status.huggingface.co/');
const apiEndpoint = 'https://api-inference.huggingface.co/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,
incidents: serviceMessages,
};
} catch (error) {
console.error('Error checking HuggingFace status:', error);
// Fallback to basic endpoint check
const endpointStatus = await this.checkEndpoint('https://status.huggingface.co/');
const apiEndpoint = 'https://api-inference.huggingface.co/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,40 @@
import { BaseProviderChecker } from '~/components/settings/providers/service-status/base-provider';
import type { StatusCheckResult } from '~/components/settings/providers/service-status/types';
export class HyperbolicStatusChecker extends BaseProviderChecker {
async checkStatus(): Promise<StatusCheckResult> {
try {
/*
* Check API endpoint directly since Hyperbolic is a newer provider
* and may not have a public status page yet
*/
const apiEndpoint = 'https://api.hyperbolic.ai/v1/models';
const apiStatus = await this.checkEndpoint(apiEndpoint);
// Check their website as a secondary indicator
const websiteStatus = await this.checkEndpoint('https://hyperbolic.ai');
let status: StatusCheckResult['status'] = 'operational';
let message = 'All systems operational';
if (apiStatus !== 'reachable' || websiteStatus !== 'reachable') {
status = apiStatus !== 'reachable' ? 'down' : 'degraded';
message = apiStatus !== 'reachable' ? 'API appears to be down' : 'Service may be experiencing issues';
}
return {
status,
message,
incidents: [], // No public incident tracking available yet
};
} catch (error) {
console.error('Error checking Hyperbolic status:', error);
return {
status: 'degraded',
message: 'Unable to determine service status',
incidents: ['Note: Limited status information available'],
};
}
}
}

View File

@@ -0,0 +1,76 @@
import { BaseProviderChecker } from '~/components/settings/providers/service-status/base-provider';
import type { StatusCheckResult } from '~/components/settings/providers/service-status/types';
export class MistralStatusChecker extends BaseProviderChecker {
async checkStatus(): Promise<StatusCheckResult> {
try {
// Check status page
const statusPageResponse = await fetch('https://status.mistral.ai/');
const text = await statusPageResponse.text();
const isOperational = text.includes('All Systems Operational');
const hasIncidents = text.includes('Active Incidents');
const hasDegradation = text.includes('Degraded Performance');
const hasOutage = text.includes('Service Outage');
// Extract incidents
const incidents: string[] = [];
const incidentSection = text.match(/Recent Events(.*?)(?=\n\n)/s);
if (incidentSection) {
const incidentLines = incidentSection[1]
.split('\n')
.map((line) => line.trim())
.filter((line) => line && !line.includes('No incidents'));
incidents.push(...incidentLines.slice(0, 5));
}
let status: StatusCheckResult['status'] = 'operational';
let message = 'All systems operational';
if (hasOutage) {
status = 'down';
message = 'Service outage detected';
} else if (hasDegradation || hasIncidents) {
status = 'degraded';
message = 'Service experiencing issues';
} else if (!isOperational) {
status = 'degraded';
message = 'Service status unknown';
}
// If status page check fails, fallback to endpoint check
if (!statusPageResponse.ok) {
const endpointStatus = await this.checkEndpoint('https://status.mistral.ai/');
const apiEndpoint = 'https://api.mistral.ai/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,
incidents,
};
} catch (error) {
console.error('Error checking Mistral status:', error);
// Fallback to basic endpoint check
const endpointStatus = await this.checkEndpoint('https://status.mistral.ai/');
const apiEndpoint = 'https://api.mistral.ai/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,91 @@
import { BaseProviderChecker } from '~/components/settings/providers/service-status/base-provider';
import type { StatusCheckResult } from '~/components/settings/providers/service-status/types';
export class OpenRouterStatusChecker extends BaseProviderChecker {
async checkStatus(): Promise<StatusCheckResult> {
try {
// Check status page
const statusPageResponse = await fetch('https://status.openrouter.ai/');
const text = await statusPageResponse.text();
// Check for specific OpenRouter status indicators
const isOperational = text.includes('All Systems Operational');
const hasIncidents = text.includes('Active Incidents');
const hasDegradation = text.includes('Degraded Performance');
const hasOutage = text.includes('Service Outage');
// Extract incidents
const incidents: string[] = [];
const incidentSection = text.match(/Past Incidents(.*?)(?=\n\n)/s);
if (incidentSection) {
const incidentLines = incidentSection[1]
.split('\n')
.map((line) => line.trim())
.filter((line) => line && line.includes('202')); // Only get dated incidents
incidents.push(...incidentLines.slice(0, 5));
}
// Check specific services
const services = {
api: {
operational: text.includes('API Service') && text.includes('Operational'),
degraded: text.includes('API Service') && text.includes('Degraded Performance'),
outage: text.includes('API Service') && text.includes('Service Outage'),
},
routing: {
operational: text.includes('Routing Service') && text.includes('Operational'),
degraded: text.includes('Routing Service') && text.includes('Degraded Performance'),
outage: text.includes('Routing Service') && text.includes('Service Outage'),
},
};
let status: StatusCheckResult['status'] = 'operational';
let message = 'All systems operational';
if (services.api.outage || services.routing.outage || hasOutage) {
status = 'down';
message = 'Service outage detected';
} else if (services.api.degraded || services.routing.degraded || hasDegradation || hasIncidents) {
status = 'degraded';
message = 'Service experiencing issues';
} else if (!isOperational) {
status = 'degraded';
message = 'Service status unknown';
}
// If status page check fails, fallback to endpoint check
if (!statusPageResponse.ok) {
const endpointStatus = await this.checkEndpoint('https://status.openrouter.ai/');
const apiEndpoint = 'https://openrouter.ai/api/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,
incidents,
};
} catch (error) {
console.error('Error checking OpenRouter status:', error);
// Fallback to basic endpoint check
const endpointStatus = await this.checkEndpoint('https://status.openrouter.ai/');
const apiEndpoint = 'https://openrouter.ai/api/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,91 @@
import { BaseProviderChecker } from '~/components/settings/providers/service-status/base-provider';
import type { StatusCheckResult } from '~/components/settings/providers/service-status/types';
export class PerplexityStatusChecker extends BaseProviderChecker {
async checkStatus(): Promise<StatusCheckResult> {
try {
// Check status page
const statusPageResponse = await fetch('https://status.perplexity.ai/');
const text = await statusPageResponse.text();
// Check for specific Perplexity status indicators
const isOperational = text.includes('All Systems Operational');
const hasIncidents = text.includes('Active Incidents');
const hasDegradation = text.includes('Degraded Performance');
const hasOutage = text.includes('Service Outage');
// Extract incidents
const incidents: string[] = [];
const incidentSection = text.match(/Past Incidents(.*?)(?=\n\n)/s);
if (incidentSection) {
const incidentLines = incidentSection[1]
.split('\n')
.map((line) => line.trim())
.filter((line) => line && line.includes('202')); // Only get dated incidents
incidents.push(...incidentLines.slice(0, 5));
}
// Check specific services
const services = {
api: {
operational: text.includes('API Service') && text.includes('Operational'),
degraded: text.includes('API Service') && text.includes('Degraded Performance'),
outage: text.includes('API Service') && text.includes('Service Outage'),
},
inference: {
operational: text.includes('Inference Service') && text.includes('Operational'),
degraded: text.includes('Inference Service') && text.includes('Degraded Performance'),
outage: text.includes('Inference Service') && text.includes('Service Outage'),
},
};
let status: StatusCheckResult['status'] = 'operational';
let message = 'All systems operational';
if (services.api.outage || services.inference.outage || hasOutage) {
status = 'down';
message = 'Service outage detected';
} else if (services.api.degraded || services.inference.degraded || hasDegradation || hasIncidents) {
status = 'degraded';
message = 'Service experiencing issues';
} else if (!isOperational) {
status = 'degraded';
message = 'Service status unknown';
}
// If status page check fails, fallback to endpoint check
if (!statusPageResponse.ok) {
const endpointStatus = await this.checkEndpoint('https://status.perplexity.ai/');
const apiEndpoint = 'https://api.perplexity.ai/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,
incidents,
};
} catch (error) {
console.error('Error checking Perplexity status:', error);
// Fallback to basic endpoint check
const endpointStatus = await this.checkEndpoint('https://status.perplexity.ai/');
const apiEndpoint = 'https://api.perplexity.ai/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,91 @@
import { BaseProviderChecker } from '~/components/settings/providers/service-status/base-provider';
import type { StatusCheckResult } from '~/components/settings/providers/service-status/types';
export class TogetherStatusChecker extends BaseProviderChecker {
async checkStatus(): Promise<StatusCheckResult> {
try {
// Check status page
const statusPageResponse = await fetch('https://status.together.ai/');
const text = await statusPageResponse.text();
// Check for specific Together status indicators
const isOperational = text.includes('All Systems Operational');
const hasIncidents = text.includes('Active Incidents');
const hasDegradation = text.includes('Degraded Performance');
const hasOutage = text.includes('Service Outage');
// Extract incidents
const incidents: string[] = [];
const incidentSection = text.match(/Past Incidents(.*?)(?=\n\n)/s);
if (incidentSection) {
const incidentLines = incidentSection[1]
.split('\n')
.map((line) => line.trim())
.filter((line) => line && line.includes('202')); // Only get dated incidents
incidents.push(...incidentLines.slice(0, 5));
}
// Check specific services
const services = {
api: {
operational: text.includes('API Service') && text.includes('Operational'),
degraded: text.includes('API Service') && text.includes('Degraded Performance'),
outage: text.includes('API Service') && text.includes('Service Outage'),
},
inference: {
operational: text.includes('Inference Service') && text.includes('Operational'),
degraded: text.includes('Inference Service') && text.includes('Degraded Performance'),
outage: text.includes('Inference Service') && text.includes('Service Outage'),
},
};
let status: StatusCheckResult['status'] = 'operational';
let message = 'All systems operational';
if (services.api.outage || services.inference.outage || hasOutage) {
status = 'down';
message = 'Service outage detected';
} else if (services.api.degraded || services.inference.degraded || hasDegradation || hasIncidents) {
status = 'degraded';
message = 'Service experiencing issues';
} else if (!isOperational) {
status = 'degraded';
message = 'Service status unknown';
}
// If status page check fails, fallback to endpoint check
if (!statusPageResponse.ok) {
const endpointStatus = await this.checkEndpoint('https://status.together.ai/');
const apiEndpoint = 'https://api.together.ai/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,
incidents,
};
} catch (error) {
console.error('Error checking Together status:', error);
// Fallback to basic endpoint check
const endpointStatus = await this.checkEndpoint('https://status.together.ai/');
const apiEndpoint = 'https://api.together.ai/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,40 @@
import { BaseProviderChecker } from '~/components/settings/providers/service-status/base-provider';
import type { StatusCheckResult } from '~/components/settings/providers/service-status/types';
export class XAIStatusChecker extends BaseProviderChecker {
async checkStatus(): Promise<StatusCheckResult> {
try {
/*
* Check API endpoint directly since XAI is a newer provider
* and may not have a public status page yet
*/
const apiEndpoint = 'https://api.xai.com/v1/models';
const apiStatus = await this.checkEndpoint(apiEndpoint);
// Check their website as a secondary indicator
const websiteStatus = await this.checkEndpoint('https://x.ai');
let status: StatusCheckResult['status'] = 'operational';
let message = 'All systems operational';
if (apiStatus !== 'reachable' || websiteStatus !== 'reachable') {
status = apiStatus !== 'reachable' ? 'down' : 'degraded';
message = apiStatus !== 'reachable' ? 'API appears to be down' : 'Service may be experiencing issues';
}
return {
status,
message,
incidents: [], // No public incident tracking available yet
};
} catch (error) {
console.error('Error checking XAI status:', error);
return {
status: 'degraded',
message: 'Unable to determine service status',
incidents: ['Note: Limited status information available'],
};
}
}
}

View File

@@ -2,7 +2,6 @@ import type { IconType } from 'react-icons';
export type ProviderName =
| 'AmazonBedrock'
| 'Anthropic'
| 'Cohere'
| 'Deepseek'
| 'Google'
@@ -10,7 +9,6 @@ export type ProviderName =
| 'HuggingFace'
| 'Hyperbolic'
| 'Mistral'
| 'OpenAI'
| 'OpenRouter'
| 'Perplexity'
| 'Together'
@@ -27,12 +25,12 @@ export type ServiceStatus = {
incidents?: string[];
};
export type ProviderConfig = {
export interface ProviderConfig {
statusUrl: string;
apiUrl: string;
headers: Record<string, string>;
testModel: string;
};
}
export type ApiResponse = {
error?: {
@@ -51,8 +49,7 @@ export type ApiResponse = {
};
export type StatusCheckResult = {
status: ServiceStatus['status'];
message?: string;
incidents?: string[];
responseTime?: number;
status: 'operational' | 'degraded' | 'down';
message: string;
incidents: string[];
};

View File

@@ -1,43 +0,0 @@
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export const settingsStyles = {
// Card styles
card: 'bg-bolt-elements-background dark:bg-bolt-elements-backgroundDark rounded-lg p-6 border border-bolt-elements-border dark:border-bolt-elements-borderDark',
// Button styles
button: {
base: 'inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm disabled:opacity-50 disabled:cursor-not-allowed',
primary: 'bg-purple-500 text-white hover:bg-purple-600',
secondary:
'bg-bolt-elements-hover dark:bg-bolt-elements-hoverDark text-bolt-elements-textSecondary dark:text-bolt-elements-textSecondaryDark hover:text-bolt-elements-textPrimary dark:hover:text-bolt-elements-textPrimaryDark',
danger: 'bg-red-50 text-red-500 hover:bg-red-100 dark:bg-red-500/10 dark:hover:bg-red-500/20',
warning: 'bg-yellow-50 text-yellow-600 hover:bg-yellow-100 dark:bg-yellow-500/10 dark:hover:bg-yellow-500/20',
success: 'bg-green-50 text-green-600 hover:bg-green-100 dark:bg-green-500/10 dark:hover:bg-green-500/20',
},
// Form styles
form: {
label: 'block text-sm text-bolt-elements-textSecondary dark:text-bolt-elements-textSecondaryDark mb-2',
input:
'w-full px-3 py-2 rounded-lg text-sm bg-bolt-elements-hover dark:bg-bolt-elements-hoverDark border border-bolt-elements-border dark:border-bolt-elements-borderDark text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimaryDark placeholder-bolt-elements-textTertiary focus:outline-none focus:ring-1 focus:ring-purple-500',
},
// Search container
search: {
input:
'w-full h-10 pl-10 pr-4 rounded-lg text-sm bg-bolt-elements-hover dark:bg-bolt-elements-hoverDark border border-bolt-elements-border dark:border-bolt-elements-borderDark text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimaryDark placeholder-bolt-elements-textTertiary focus:outline-none focus:ring-1 focus:ring-purple-500 transition-all',
},
// Scroll container styles
scroll: {
container: 'overflow-y-auto overscroll-y-contain',
content: 'min-h-full',
},
'loading-spinner': 'i-ph:spinner-gap-bold animate-spin w-4 h-4',
} as const;

View File

@@ -97,13 +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: '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 },
{ 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: 12 },
];
export const categoryLabels: Record<SettingCategory, string> = {

View File

@@ -5,7 +5,6 @@ import { classNames } from '~/utils/classNames';
import { Switch } from '~/components/ui/Switch';
import { themeStore, kTheme } from '~/lib/stores/theme';
import type { UserProfile } from '~/components/settings/settings.types';
import { settingsStyles } from '~/components/settings/settings.styles';
import { useStore } from '@nanostores/react';
import { shortcutsStore } from '~/lib/stores/settings';
@@ -97,11 +96,10 @@ export default function SettingsTab() {
}
}}
className={classNames(
settingsStyles.button.base,
settings.theme === theme ? settingsStyles.button.primary : settingsStyles.button.secondary,
'inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm disabled:opacity-50 disabled:cursor-not-allowed',
settings.theme === theme
? 'dark:bg-purple-500 dark:text-white dark:hover:bg-purple-600 dark:hover:text-white'
: 'hover:bg-purple-500/10 hover:text-purple-500 dark:bg-[#1A1A1A] dark:hover:bg-purple-500/20 dark:text-bolt-elements-textPrimary dark:hover:text-purple-500',
? 'bg-purple-500 text-white hover:bg-purple-600 dark:bg-purple-500 dark:text-white dark:hover:bg-purple-600'
: 'bg-bolt-elements-hover dark:bg-[#1A1A1A] text-bolt-elements-textSecondary hover:bg-purple-500/10 hover:text-purple-500 dark:hover:bg-purple-500/20 dark:text-bolt-elements-textPrimary dark:hover:text-purple-500',
)}
>
<div

View File

@@ -23,19 +23,6 @@ interface BatteryManager extends EventTarget {
level: number;
}
type ProcessStatus = 'active' | 'idle' | 'suspended';
type ProcessImpact = 'high' | 'medium' | 'low';
interface ProcessInfo {
name: string;
type: 'API' | 'Animation' | 'Background' | 'Network' | 'Storage';
cpuUsage: number;
memoryUsage: number;
status: ProcessStatus;
lastUpdate: string;
impact: ProcessImpact;
}
interface SystemMetrics {
cpu: number;
memory: {
@@ -43,7 +30,6 @@ interface SystemMetrics {
total: number;
percentage: number;
};
activeProcesses: number;
uptime: number;
battery?: {
level: number;
@@ -89,11 +75,9 @@ const BATTERY_THRESHOLD = 20; // Enable energy saver when battery below 20%
const UPDATE_INTERVALS = {
normal: {
metrics: 1000, // 1s
processes: 2000, // 2s
},
energySaver: {
metrics: 5000, // 5s
processes: 10000, // 10s
},
};
@@ -105,11 +89,9 @@ const ENERGY_COSTS = {
};
export default function TaskManagerTab() {
const [processes, setProcesses] = useState<ProcessInfo[]>([]);
const [metrics, setMetrics] = useState<SystemMetrics>({
cpu: 0,
memory: { used: 0, total: 0, percentage: 0 },
activeProcesses: 0,
uptime: 0,
network: { downlink: 0, latency: 0, type: 'unknown' },
});
@@ -120,10 +102,6 @@ export default function TaskManagerTab() {
battery: [],
network: [],
});
const [loading, setLoading] = useState({
metrics: false,
processes: false,
});
const [energySaverMode, setEnergySaverMode] = useState<boolean>(() => {
// Initialize from localStorage, default to false
const saved = localStorage.getItem('energySaverMode');
@@ -144,8 +122,6 @@ export default function TaskManagerTab() {
const saverModeStartTime = useRef<number | null>(null);
const [performanceObserver, setPerformanceObserver] = useState<PerformanceObserver | null>(null);
// Handle energy saver mode changes
const handleEnergySaverChange = (checked: boolean) => {
setEnergySaverMode(checked);
@@ -166,54 +142,6 @@ export default function TaskManagerTab() {
}
};
// Add this helper function at the top level of the component
function isNetworkRequest(entry: PerformanceEntry): boolean {
const resourceTiming = entry as PerformanceResourceTiming;
return resourceTiming.initiatorType === 'fetch' && entry.duration === 0;
}
// Update getActiveProcessCount
const getActiveProcessCount = async (): Promise<number> => {
try {
const networkCount = (navigator as any)?.connections?.length || 0;
const swCount = (await navigator.serviceWorker?.getRegistrations().then((regs) => regs.length)) || 0;
const animationCount = document.getAnimations().length;
const fetchCount = performance.getEntriesByType('resource').filter(isNetworkRequest).length;
return networkCount + swCount + animationCount + fetchCount;
} catch (error) {
console.error('Failed to get active process count:', error);
return 0;
}
};
// Update process cleanup
const cleanupOldProcesses = useCallback(() => {
const MAX_PROCESS_AGE = 30000; // 30 seconds
setProcesses((currentProcesses) => {
const now = Date.now();
return currentProcesses.filter((process) => {
const processTime = new Date(process.lastUpdate).getTime();
const age = now - processTime;
/*
* Keep processes that are:
* 1. Less than MAX_PROCESS_AGE old, or
* 2. Currently active, or
* 3. Service workers (they're managed separately)
*/
return age < MAX_PROCESS_AGE || process.status === 'active' || process.type === 'Background';
});
});
}, []);
// Add cleanup interval
useEffect(() => {
const interval = setInterval(cleanupOldProcesses, 5000);
return () => clearInterval(interval);
}, [cleanupOldProcesses]);
// Update energy savings calculation
const updateEnergySavings = useCallback(() => {
if (!energySaverMode) {
@@ -237,8 +165,7 @@ export default function TaskManagerTab() {
const saverUpdatesPerMinute = 60 / (UPDATE_INTERVALS.energySaver.metrics / 1000);
const updatesReduced = Math.floor((normalUpdatesPerMinute - saverUpdatesPerMinute) * (timeInSaverMode / 60));
const processCount = processes.length;
const energyPerUpdate = ENERGY_COSTS.update + processCount * ENERGY_COSTS.rendering;
const energyPerUpdate = ENERGY_COSTS.update;
const energySaved = (updatesReduced * energyPerUpdate) / 3600;
setEnergySavings({
@@ -246,7 +173,7 @@ export default function TaskManagerTab() {
timeInSaverMode,
estimatedEnergySaved: energySaved,
});
}, [energySaverMode, processes.length]);
}, [energySaverMode]);
// Add interval for energy savings updates
useEffect(() => {
@@ -254,153 +181,9 @@ export default function TaskManagerTab() {
return () => clearInterval(interval);
}, [updateEnergySavings]);
// Improve process monitoring by adding unique IDs and timestamps
const createProcess = (
name: string,
type: ProcessInfo['type'],
cpuUsage: number,
memoryUsage: number,
status: ProcessStatus,
impact: ProcessImpact,
): ProcessInfo => ({
name,
type,
cpuUsage,
memoryUsage,
status,
lastUpdate: new Date().toISOString(),
impact,
});
// Update animation monitoring to track changes better
const updateAnimations = useCallback(() => {
const animations = document.getAnimations();
setProcesses((currentProcesses) => {
const nonAnimationProcesses = currentProcesses.filter((p) => !p.name.startsWith('Animation:'));
const newAnimations = animations
.slice(0, 5)
.map((animation) =>
createProcess(
`Animation: ${animation.id || 'Unnamed'}`,
'Animation',
animation.playState === 'running' ? 2 : 0,
1,
animation.playState === 'running' ? 'active' : 'idle',
'low',
),
);
return [...nonAnimationProcesses, ...newAnimations];
});
}, []);
// Add animation monitoring interval
useEffect(() => {
const interval = setInterval(updateAnimations, energySaverMode ? 5000 : 1000);
return () => clearInterval(interval);
}, [updateAnimations, energySaverMode]);
useEffect((): (() => void) | undefined => {
if (!autoEnergySaver) {
// If auto mode is disabled, clear any forced energy saver state
setEnergySaverMode(false);
return undefined;
}
const checkBatteryStatus = async () => {
try {
const battery = await navigator.getBattery();
const shouldEnableSaver = !battery.charging && battery.level * 100 <= BATTERY_THRESHOLD;
setEnergySaverMode(shouldEnableSaver);
} catch {
console.log('Battery API not available');
}
};
checkBatteryStatus();
const batteryCheckInterval = setInterval(checkBatteryStatus, 60000);
return () => clearInterval(batteryCheckInterval);
}, [autoEnergySaver]);
const getUsageColor = (usage: number): string => {
if (usage > 80) {
return 'text-red-500';
}
if (usage > 50) {
return 'text-yellow-500';
}
return 'text-gray-500';
};
const getImpactColor = (impact: ProcessImpact): string => {
if (impact === 'high') {
return 'text-red-500';
}
if (impact === 'medium') {
return 'text-yellow-500';
}
return 'text-gray-500';
};
const renderUsageGraph = (data: number[], label: string, color: string) => {
const chartData = {
labels: metricsHistory.timestamps,
datasets: [
{
label,
data,
borderColor: color,
fill: false,
tension: 0.4,
},
],
};
const options = {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
max: 100,
grid: {
color: 'rgba(255, 255, 255, 0.1)',
},
},
x: {
grid: {
display: false,
},
},
},
plugins: {
legend: {
display: false,
},
},
animation: {
duration: 0,
} as const,
};
return (
<div className="h-32">
<Line data={chartData} options={options} />
</div>
);
};
// Update metrics
const updateMetrics = async () => {
try {
setLoading((prev) => ({ ...prev, metrics: true }));
// Get memory info using Performance API
const memory = performance.memory || {
jsHeapSizeLimit: 0,
@@ -444,7 +227,6 @@ export default function TaskManagerTab() {
total: Math.round(totalMem),
percentage: Math.round(memPercentage),
},
activeProcesses: await getActiveProcessCount(),
uptime: performance.now() / 1000,
battery: batteryInfo,
network: networkInfo,
@@ -465,8 +247,6 @@ export default function TaskManagerTab() {
});
} catch (error: unknown) {
console.error('Failed to update system metrics:', error);
} finally {
setLoading((prev) => ({ ...prev, metrics: false }));
}
};
@@ -530,206 +310,129 @@ export default function TaskManagerTab() {
return () => connection.removeEventListener('change', updateNetworkInfo);
}, []);
// Add this effect for live process monitoring
// Remove all animation and process monitoring
useEffect(() => {
// Clean up previous observer if exists
performanceObserver?.disconnect();
const metricsInterval = setInterval(
() => {
if (!energySaverMode) {
updateMetrics();
}
},
energySaverMode ? UPDATE_INTERVALS.energySaver.metrics : UPDATE_INTERVALS.normal.metrics,
);
// Create new performance observer for network requests
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
const newNetworkEntries = entries
.filter((entry: PerformanceEntry): boolean => {
const resourceTiming = entry as PerformanceResourceTiming;
return entry.entryType === 'resource' && resourceTiming.initiatorType === 'fetch';
})
.slice(-5);
if (newNetworkEntries.length > 0) {
setProcesses((currentProcesses) => {
// Remove old network processes
const filteredProcesses = currentProcesses.filter((p) => !p.name.startsWith('Network Request:'));
// Add new network processes
const newProcesses = newNetworkEntries.map((entry) => ({
name: `Network Request: ${new URL((entry as PerformanceResourceTiming).name).pathname}`,
type: 'Network' as const,
cpuUsage: entry.duration > 0 ? entry.duration / 100 : 0,
memoryUsage: (entry as PerformanceResourceTiming).encodedBodySize / (1024 * 1024),
status: (entry.duration === 0 ? 'active' : 'idle') as ProcessStatus,
lastUpdate: new Date().toISOString(),
impact: (entry.duration > 1000 ? 'high' : entry.duration > 500 ? 'medium' : 'low') as ProcessImpact,
})) as ProcessInfo[];
return [...filteredProcesses, ...newProcesses];
});
}
});
// Start observing resource timing entries
observer.observe({ entryTypes: ['resource'] });
setPerformanceObserver(observer);
// Set up animation observer
const animationObserver = new MutationObserver(() => {
const animations = document.getAnimations();
setProcesses((currentProcesses) => {
// Remove old animation processes
const filteredProcesses = currentProcesses.filter((p) => !p.name.startsWith('Animation:'));
// Add current animations
const animationProcesses = animations.slice(0, 5).map((animation) => ({
name: `Animation: ${animation.id || 'Unnamed'}`,
type: 'Animation' as const,
cpuUsage: animation.playState === 'running' ? 2 : 0,
memoryUsage: 1,
status: (animation.playState === 'running' ? 'active' : 'idle') as ProcessStatus,
lastUpdate: new Date().toISOString(),
impact: 'low' as ProcessImpact,
})) as ProcessInfo[];
return [...filteredProcesses, ...animationProcesses];
});
});
// Observe DOM changes that might trigger animations
animationObserver.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
});
// Set up service worker observer
const checkServiceWorkers = async () => {
const serviceWorkers = (await navigator.serviceWorker?.getRegistrations()) || [];
setProcesses((currentProcesses) => {
// Remove old service worker processes
const filteredProcesses = currentProcesses.filter((p) => !p.name.startsWith('Service Worker:'));
// Add current service workers
const swProcesses = serviceWorkers.map((sw) => ({
name: `Service Worker: ${sw.scope}`,
type: 'Background' as const,
cpuUsage: sw.active ? 1 : 0,
memoryUsage: 5,
status: (sw.active ? 'active' : 'idle') as ProcessStatus,
lastUpdate: new Date().toISOString(),
impact: 'low' as ProcessImpact,
})) as ProcessInfo[];
return [...filteredProcesses, ...swProcesses];
});
};
// Check service workers periodically
const swInterval = setInterval(checkServiceWorkers, 5000);
// Clean up
return () => {
performanceObserver?.disconnect();
animationObserver.disconnect();
clearInterval(swInterval);
clearInterval(metricsInterval);
};
}, []);
// Update the updateProcesses function
const updateProcesses = async () => {
try {
setLoading((prev) => ({ ...prev, processes: true }));
// Get initial process information
const processes: ProcessInfo[] = [];
// Add initial network processes
const networkEntries = performance
.getEntriesByType('resource')
.filter((entry: PerformanceEntry): boolean => {
const resourceTiming = entry as PerformanceResourceTiming;
return entry.entryType === 'resource' && resourceTiming.initiatorType === 'fetch';
})
.slice(-5);
networkEntries.forEach((entry) => {
processes.push({
name: `Network Request: ${new URL((entry as PerformanceResourceTiming).name).pathname}`,
type: 'Network',
cpuUsage: entry.duration > 0 ? entry.duration / 100 : 0,
memoryUsage: (entry as PerformanceResourceTiming).encodedBodySize / (1024 * 1024),
status: (entry.duration === 0 ? 'active' : 'idle') as ProcessStatus,
lastUpdate: new Date().toISOString(),
impact: (entry.duration > 1000 ? 'high' : entry.duration > 500 ? 'medium' : 'low') as ProcessImpact,
});
});
// Add initial animations
document
.getAnimations()
.slice(0, 5)
.forEach((animation) => {
processes.push({
name: `Animation: ${animation.id || 'Unnamed'}`,
type: 'Animation',
cpuUsage: animation.playState === 'running' ? 2 : 0,
memoryUsage: 1,
status: (animation.playState === 'running' ? 'active' : 'idle') as ProcessStatus,
lastUpdate: new Date().toISOString(),
impact: 'low' as ProcessImpact,
});
});
// Add initial service workers
const serviceWorkers = (await navigator.serviceWorker?.getRegistrations()) || [];
serviceWorkers.forEach((sw) => {
processes.push({
name: `Service Worker: ${sw.scope}`,
type: 'Background',
cpuUsage: sw.active ? 1 : 0,
memoryUsage: 5,
status: (sw.active ? 'active' : 'idle') as ProcessStatus,
lastUpdate: new Date().toISOString(),
impact: 'low' as ProcessImpact,
});
});
setProcesses(processes);
} catch (error) {
console.error('Failed to update process list:', error);
} finally {
setLoading((prev) => ({ ...prev, processes: false }));
}
};
}, [energySaverMode]);
// Initial update effect
useEffect((): (() => void) => {
// Initial update
updateMetrics();
updateProcesses();
// Set up intervals for live updates
const metricsInterval = setInterval(
updateMetrics,
energySaverMode ? UPDATE_INTERVALS.energySaver.metrics : UPDATE_INTERVALS.normal.metrics,
);
const processesInterval = setInterval(
updateProcesses,
energySaverMode ? UPDATE_INTERVALS.energySaver.processes : UPDATE_INTERVALS.normal.processes,
);
// Cleanup on unmount
return () => {
clearInterval(metricsInterval);
clearInterval(processesInterval);
};
}, [energySaverMode]); // Re-create intervals when energy saver mode changes
const getUsageColor = (usage: number): string => {
if (usage > 80) {
return 'text-red-500';
}
if (usage > 50) {
return 'text-yellow-500';
}
return 'text-gray-500';
};
const renderUsageGraph = (data: number[], label: string, color: string) => {
const chartData = {
labels: metricsHistory.timestamps,
datasets: [
{
label,
data,
borderColor: color,
fill: false,
tension: 0.4,
},
],
};
const options = {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
max: 100,
grid: {
color: 'rgba(255, 255, 255, 0.1)',
},
},
x: {
grid: {
display: false,
},
},
},
plugins: {
legend: {
display: false,
},
},
animation: {
duration: 0,
} as const,
};
return (
<div className="h-32">
<Line data={chartData} options={options} />
</div>
);
};
useEffect((): (() => void) | undefined => {
if (!autoEnergySaver) {
// If auto mode is disabled, clear any forced energy saver state
setEnergySaverMode(false);
return undefined;
}
const checkBatteryStatus = async () => {
try {
const battery = await navigator.getBattery();
const shouldEnableSaver = !battery.charging && battery.level * 100 <= BATTERY_THRESHOLD;
setEnergySaverMode(shouldEnableSaver);
} catch {
console.log('Battery API not available');
}
};
checkBatteryStatus();
const batteryCheckInterval = setInterval(checkBatteryStatus, 60000);
return () => clearInterval(batteryCheckInterval);
}, [autoEnergySaver]);
return (
<div className="space-y-6">
{/* System Overview */}
<div className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
<div className="flex justify-between items-center mb-4">
<h2 className="text-lg font-medium text-bolt-elements-textPrimary">System Overview</h2>
<div className="flex flex-col gap-6">
{/* System Metrics */}
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between">
<h3 className="text-base font-medium text-bolt-elements-textPrimary">System Metrics</h3>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<input
@@ -763,185 +466,86 @@ export default function TaskManagerTab() {
</div>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="p-4 rounded-lg bg-[#F8F8F8] dark:bg-[#141414]">
<div className="flex items-center gap-2 mb-2">
<div className="i-ph:cpu text-gray-500 dark:text-gray-400 w-4 h-4" />
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
{/* CPU Usage */}
<div className="flex flex-col gap-2 rounded-lg bg-[#F8F8F8] dark:bg-[#141414] p-4">
<div className="flex items-center justify-between">
<span className="text-sm text-bolt-elements-textSecondary">CPU Usage</span>
<span className={classNames('text-sm font-medium', getUsageColor(metrics.cpu))}>
{Math.round(metrics.cpu)}%
</span>
</div>
<p className={classNames('text-lg font-medium', getUsageColor(metrics.cpu))}>{Math.round(metrics.cpu)}%</p>
{renderUsageGraph(metricsHistory.cpu, 'CPU', '#9333ea')}
</div>
<div className="p-4 rounded-lg bg-[#F8F8F8] dark:bg-[#141414]">
<div className="flex items-center gap-2 mb-2">
<div className="i-ph:database text-gray-500 dark:text-gray-400 w-4 h-4" />
{/* Memory Usage */}
<div className="flex flex-col gap-2 rounded-lg bg-[#F8F8F8] dark:bg-[#141414] p-4">
<div className="flex items-center justify-between">
<span className="text-sm text-bolt-elements-textSecondary">Memory Usage</span>
<span className={classNames('text-sm font-medium', getUsageColor(metrics.memory.percentage))}>
{Math.round(metrics.memory.percentage)}%
</span>
</div>
<p className={classNames('text-lg font-medium', getUsageColor(metrics.memory.percentage))}>
{metrics.memory.used}MB / {metrics.memory.total}MB
</p>
{renderUsageGraph(metricsHistory.memory, 'Memory', '#2563eb')}
</div>
<div className="p-4 rounded-lg bg-[#F8F8F8] dark:bg-[#141414]">
<div className="flex items-center gap-2 mb-2">
<div className="i-ph:battery text-gray-500 dark:text-gray-400 w-4 h-4" />
<span className="text-sm text-bolt-elements-textSecondary">Battery</span>
</div>
{metrics.battery ? (
<div>
<p className="text-lg font-medium text-bolt-elements-textPrimary">
{Math.round(metrics.battery.level)}%
{metrics.battery.charging && (
<span className="ml-2 text-bolt-elements-textSecondary">
<div className="i-ph:lightning-fill w-4 h-4 inline-block" />
</span>
)}
</p>
{metrics.battery.timeRemaining && metrics.battery.timeRemaining !== Infinity && (
<p className="text-xs text-bolt-elements-textSecondary mt-1">
{metrics.battery.charging ? 'Full in: ' : 'Remaining: '}
{Math.round(metrics.battery.timeRemaining / 60)}m
</p>
)}
{renderUsageGraph(metricsHistory.battery, 'Battery', '#22c55e')}
{/* Battery */}
{metrics.battery && (
<div className="flex flex-col gap-2 rounded-lg bg-[#F8F8F8] dark:bg-[#141414] p-4">
<div className="flex items-center justify-between">
<span className="text-sm text-bolt-elements-textSecondary">Battery</span>
<div className="flex items-center gap-2">
{metrics.battery.charging && <div className="i-ph:lightning-fill w-4 h-4 text-bolt-action-primary" />}
<span
className={classNames(
'text-sm font-medium',
metrics.battery.level > 20 ? 'text-bolt-elements-textPrimary' : 'text-red-500',
)}
>
{Math.round(metrics.battery.level)}%
</span>
</div>
</div>
) : (
<p className="text-sm text-bolt-elements-textSecondary">Not available</p>
)}
</div>
<div className="p-4 rounded-lg bg-[#F8F8F8] dark:bg-[#141414]">
<div className="flex items-center gap-2 mb-2">
<div className="i-ph:wifi text-gray-500 dark:text-gray-400 w-4 h-4" />
<span className="text-sm text-bolt-elements-textSecondary">Network</span>
{renderUsageGraph(metricsHistory.battery, 'Battery', '#22c55e')}
</div>
)}
{/* Network */}
<div className="flex flex-col gap-2 rounded-lg bg-[#F8F8F8] dark:bg-[#141414] p-4">
<div className="flex items-center justify-between">
<span className="text-sm text-bolt-elements-textSecondary">Network</span>
<span className="text-sm font-medium text-bolt-elements-textPrimary">
{metrics.network.downlink.toFixed(1)} Mbps
</span>
</div>
<p className="text-lg font-medium text-bolt-elements-textPrimary">{metrics.network.downlink} Mbps</p>
<p className="text-xs text-bolt-elements-textSecondary mt-1">Latency: {metrics.network.latency}ms</p>
{renderUsageGraph(metricsHistory.network, 'Network', '#f59e0b')}
</div>
</div>
</div>
{/* Process List */}
<div className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<div className="i-ph:list-bullets text-purple-500 w-5 h-5" />
<h3 className="text-base font-medium text-bolt-elements-textPrimary">Active Processes</h3>
</div>
<button
onClick={updateProcesses}
className={classNames(
'flex items-center gap-2 px-3 py-2 rounded-lg text-sm',
'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
'hover:bg-purple-500/10 hover:text-purple-500',
'dark:hover:bg-purple-500/20 dark:hover:text-purple-500',
'text-bolt-elements-textPrimary',
'transition-colors duration-200',
{ 'opacity-50 cursor-not-allowed': loading.processes },
)}
disabled={loading.processes}
>
<div className={classNames('i-ph:arrows-clockwise w-4 h-4', loading.processes ? 'animate-spin' : '')} />
Refresh
</button>
</div>
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-[#E5E5E5] dark:border-[#1A1A1A]">
<th className="py-2 px-4 text-left text-sm font-medium text-bolt-elements-textSecondary">Process</th>
<th className="py-2 px-4 text-left text-sm font-medium text-bolt-elements-textSecondary">Type</th>
<th className="py-2 px-4 text-left text-sm font-medium text-bolt-elements-textSecondary">CPU</th>
<th className="py-2 px-4 text-left text-sm font-medium text-bolt-elements-textSecondary">Memory</th>
<th className="py-2 px-4 text-left text-sm font-medium text-bolt-elements-textSecondary">Status</th>
<th className="py-2 px-4 text-left text-sm font-medium text-bolt-elements-textSecondary">Impact</th>
<th className="py-2 px-4 text-left text-sm font-medium text-bolt-elements-textSecondary">
Last Update
</th>
</tr>
</thead>
<tbody>
{processes.map((process, index) => (
<tr
key={index}
data-process={process.name}
className="border-b border-[#E5E5E5] dark:border-[#1A1A1A] last:border-0"
>
<td className="py-3 px-4">
<div className="flex items-center gap-2">
<div className="i-ph:cube text-gray-500 dark:text-gray-400 w-4 h-4" />
<span className="text-sm text-bolt-elements-textPrimary">{process.name}</span>
</div>
</td>
<td className="py-3 px-4">
<span className="text-sm text-bolt-elements-textSecondary">{process.type}</span>
</td>
<td className="py-3 px-4">
<span className={classNames('text-sm', getUsageColor(process.cpuUsage))}>
{process.cpuUsage.toFixed(1)}%
</span>
</td>
<td className="py-3 px-4">
<span className={classNames('text-sm', getUsageColor(process.memoryUsage))}>
{process.memoryUsage.toFixed(1)} MB
</span>
</td>
<td className="py-3 px-4">
<span className={classNames('text-sm text-bolt-elements-textSecondary capitalize')}>
{process.status}
</span>
</td>
<td className="py-3 px-4">
<span className={classNames('text-sm', getImpactColor(process.impact))}>{process.impact}</span>
</td>
<td className="py-3 px-4">
<span className="text-sm text-bolt-elements-textSecondary">
{new Date(process.lastUpdate).toLocaleTimeString()}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Energy Savings */}
<div className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
<h3 className="text-base font-medium text-bolt-elements-textPrimary">Energy Savings</h3>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-4">
<div className="p-4 rounded-lg bg-[#F8F8F8] dark:bg-[#141414]">
<div className="flex items-center gap-2 mb-2">
<div className="i-ph:clock text-gray-500 dark:text-gray-400 w-4 h-4" />
<span className="text-sm text-bolt-elements-textSecondary">Time in Saver Mode</span>
{/* Energy Savings */}
{energySaverMode && (
<div className="flex flex-col gap-2 rounded-lg bg-[#F8F8F8] dark:bg-[#141414] p-4">
<h4 className="text-sm font-medium text-bolt-elements-textPrimary">Energy Savings</h4>
<div className="grid grid-cols-3 gap-4">
<div>
<span className="text-sm text-bolt-elements-textSecondary">Updates Reduced</span>
<p className="text-lg font-medium text-bolt-elements-textPrimary">{energySavings.updatesReduced}</p>
</div>
<div>
<span className="text-sm text-bolt-elements-textSecondary">Time in Saver Mode</span>
<p className="text-lg font-medium text-bolt-elements-textPrimary">
{Math.floor(energySavings.timeInSaverMode / 60)}m {Math.floor(energySavings.timeInSaverMode % 60)}s
</p>
</div>
<div>
<span className="text-sm text-bolt-elements-textSecondary">Energy Saved</span>
<p className="text-lg font-medium text-bolt-elements-textPrimary">
{energySavings.estimatedEnergySaved.toFixed(2)} mWh
</p>
</div>
</div>
<p className="text-lg font-medium text-bolt-elements-textPrimary">
{Math.floor(energySavings.timeInSaverMode / 60)}m {Math.floor(energySavings.timeInSaverMode % 60)}s
</p>
</div>
<div className="p-4 rounded-lg bg-[#F8F8F8] dark:bg-[#141414]">
<div className="flex items-center gap-2 mb-2">
<div className="i-ph:chart-line text-gray-500 dark:text-gray-400 w-4 h-4" />
<span className="text-sm text-bolt-elements-textSecondary">Updates Reduced</span>
</div>
<p className="text-lg font-medium text-bolt-elements-textPrimary">{energySavings.updatesReduced}</p>
</div>
<div className="p-4 rounded-lg bg-[#F8F8F8] dark:bg-[#141414]">
<div className="flex items-center gap-2 mb-2">
<div className="i-ph:battery text-gray-500 dark:text-gray-400 w-4 h-4" />
<span className="text-sm text-bolt-elements-textSecondary">Estimated Energy Saved</span>
</div>
<p className="text-lg font-medium text-bolt-elements-textPrimary">
{energySavings.estimatedEnergySaved.toFixed(2)} mWh
</p>
</div>
</div>
)}
</div>
</div>
);

View File

@@ -263,8 +263,6 @@ export const UsersWindow = ({ open, onClose }: UsersWindowProps) => {
});
}, [tabConfiguration, profile.notifications]);
console.log('Filtered visible user tabs:', visibleUserTabs);
const moveTab = (dragIndex: number, hoverIndex: number) => {
const draggedTab = visibleUserTabs[dragIndex];
const targetTab = visibleUserTabs[hoverIndex];

View File

@@ -2,17 +2,19 @@
import * as React from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '~/lib/utils';
import { classNames } from '~/utils/classNames';
const badgeVariants = cva(
'inline-flex items-center rounded-md px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-bolt-elements-ring focus:ring-offset-2',
{
variants: {
variant: {
default: 'border-transparent bg-primary text-primary-foreground',
secondary: 'border-transparent bg-secondary text-secondary-foreground',
destructive: 'border-transparent bg-red-500/10 text-red-500 dark:bg-red-900/30',
outline: 'text-foreground',
default:
'border-transparent bg-bolt-elements-background text-bolt-elements-textPrimary hover:bg-bolt-elements-background/80',
secondary:
'border-transparent bg-bolt-elements-background text-bolt-elements-textSecondary hover:bg-bolt-elements-background/80',
destructive: 'border-transparent bg-red-500/10 text-red-500 hover:bg-red-500/20',
outline: 'text-bolt-elements-textPrimary',
},
},
defaultVariants: {
@@ -21,13 +23,10 @@ const badgeVariants = cva(
},
);
interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {
variant?: 'default' | 'secondary' | 'destructive' | 'outline';
}
export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
function Badge({ className, variant = 'default', ...props }: BadgeProps) {
return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
function Badge({ className, variant, ...props }: BadgeProps) {
return <div className={classNames(badgeVariants({ variant }), className)} {...props} />;
}
export { Badge, badgeVariants };
export type { BadgeProps };

View File

@@ -1,6 +1,6 @@
import * as React from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '~/lib/utils';
import { classNames } from '~/utils/classNames';
const buttonVariants = cva(
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-bolt-elements-borderColor disabled:pointer-events-none disabled:opacity-50',
@@ -38,7 +38,7 @@ export interface ButtonProps
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, _asChild = false, ...props }, ref) => {
return <button className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />;
return <button className={classNames(buttonVariants({ variant, size }), className)} ref={ref} {...props} />;
},
);
Button.displayName = 'Button';

View File

@@ -1,5 +1,5 @@
import { forwardRef } from 'react';
import { cn } from '~/lib/utils';
import { classNames } from '~/utils/classNames';
export interface CardProps extends React.HTMLAttributes<HTMLDivElement> {}
@@ -7,7 +7,7 @@ const Card = forwardRef<HTMLDivElement, CardProps>(({ className, ...props }, ref
return (
<div
ref={ref}
className={cn(
className={classNames(
'rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary shadow-sm',
className,
)}
@@ -18,27 +18,38 @@ const Card = forwardRef<HTMLDivElement, CardProps>(({ className, ...props }, ref
Card.displayName = 'Card';
const CardHeader = forwardRef<HTMLDivElement, CardProps>(({ className, ...props }, ref) => {
return <div ref={ref} className={cn('flex flex-col space-y-1.5 p-6', className)} {...props} />;
return <div ref={ref} className={classNames('flex flex-col space-y-1.5 p-6', className)} {...props} />;
});
CardHeader.displayName = 'CardHeader';
const CardTitle = forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
({ className, ...props }, ref) => {
return <h3 ref={ref} className={cn('text-2xl font-semibold leading-none tracking-tight', className)} {...props} />;
return (
<h3
ref={ref}
className={classNames('text-2xl font-semibold leading-none tracking-tight', className)}
{...props}
/>
);
},
);
CardTitle.displayName = 'CardTitle';
const CardDescription = forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
({ className, ...props }, ref) => {
return <p ref={ref} className={cn('text-sm text-bolt-elements-textSecondary', className)} {...props} />;
return <p ref={ref} className={classNames('text-sm text-bolt-elements-textSecondary', className)} {...props} />;
},
);
CardDescription.displayName = 'CardDescription';
const CardContent = forwardRef<HTMLDivElement, CardProps>(({ className, ...props }, ref) => {
return <div ref={ref} className={cn('p-6 pt-0', className)} {...props} />;
return <div ref={ref} className={classNames('p-6 pt-0', className)} {...props} />;
});
CardContent.displayName = 'CardContent';
export { Card, CardHeader, CardTitle, CardDescription, CardContent };
const CardFooter = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
<div ref={ref} className={classNames('flex items-center p-6 pt-0', className)} {...props} />
));
CardFooter.displayName = 'CardFooter';
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };

View File

@@ -1,5 +1,5 @@
import { forwardRef } from 'react';
import { cn } from '~/lib/utils';
import { classNames } from '~/utils/classNames';
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
@@ -7,11 +7,8 @@ const Input = forwardRef<HTMLInputElement, InputProps>(({ className, type, ...pr
return (
<input
type={type}
className={cn(
'flex h-10 w-full rounded-md border border-bolt-elements-borderColor bg-bolt-elements-background-depth-1 px-3 py-2 text-sm',
'ring-offset-bolt-elements-background-depth-1 file:border-0 file:bg-transparent file:text-sm file:font-medium',
'placeholder:text-bolt-elements-textTertiary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-purple-500/30',
'focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className={classNames(
'flex h-10 w-full rounded-md border border-bolt-elements-border bg-bolt-elements-background px-3 py-2 text-sm ring-offset-bolt-elements-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-bolt-elements-textSecondary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-bolt-elements-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className,
)}
ref={ref}

View File

@@ -1,22 +1,20 @@
import { forwardRef } from 'react';
import { cn } from '~/lib/utils';
import * as React from 'react';
import * as LabelPrimitive from '@radix-ui/react-label';
import { classNames } from '~/utils/classNames';
export interface LabelProps extends React.LabelHTMLAttributes<HTMLLabelElement> {}
const Label = forwardRef<HTMLLabelElement, LabelProps>(({ className, ...props }, ref) => {
return (
<label
ref={ref}
className={cn(
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
'text-bolt-elements-textPrimary',
className,
)}
{...props}
/>
);
});
Label.displayName = 'Label';
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={classNames(
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
className,
)}
{...props}
/>
));
Label.displayName = LabelPrimitive.Root.displayName;
export { Label };

View File

@@ -1,22 +1,22 @@
import * as React from 'react';
import * as ProgressPrimitive from '@radix-ui/react-progress';
import { cn } from '~/lib/utils';
import { classNames } from '~/utils/classNames';
const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
>(({ className, value, ...props }, ref) => (
<ProgressPrimitive.Root
interface ProgressProps extends React.HTMLAttributes<HTMLDivElement> {
value?: number;
}
const Progress = React.forwardRef<HTMLDivElement, ProgressProps>(({ className, value, ...props }, ref) => (
<div
ref={ref}
className={cn('relative h-2 w-full overflow-hidden rounded-full bg-bolt-elements-background', className)}
className={classNames('relative h-2 w-full overflow-hidden rounded-full bg-bolt-elements-background', className)}
{...props}
>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-purple-500 transition-all"
<div
className="h-full w-full flex-1 bg-bolt-elements-textPrimary transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
</div>
));
Progress.displayName = ProgressPrimitive.Root.displayName;
Progress.displayName = 'Progress';
export { Progress };

View File

@@ -2,13 +2,13 @@
import * as React from 'react';
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
import { cn } from '~/lib/utils';
import { classNames } from '~/utils/classNames';
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root ref={ref} className={cn('relative overflow-hidden', className)} {...props}>
<ScrollAreaPrimitive.Root ref={ref} className={classNames('relative overflow-hidden', className)} {...props}>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">{children}</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
@@ -23,15 +23,17 @@ const ScrollBar = React.forwardRef<
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
className={classNames(
'flex touch-none select-none transition-colors',
orientation === 'vertical' && 'h-full w-2.5 border-l border-l-transparent p-[1px]',
orientation === 'horizontal' && 'h-2.5 flex-col border-t border-t-transparent p-[1px]',
{
'h-full w-2.5 border-l border-l-transparent p-[1px]': orientation === 'vertical',
'h-2.5 flex-col border-t border-t-transparent p-[1px]': orientation === 'horizontal',
},
className,
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-bolt-elements-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
));
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;

View File

@@ -1,18 +1,17 @@
import * as React from 'react';
import * as TabsPrimitive from '@radix-ui/react-tabs';
import { forwardRef } from 'react';
import { cn } from '~/lib/utils';
import { classNames } from '~/utils/classNames';
const Tabs = TabsPrimitive.Root;
const TabsList = forwardRef<
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
'inline-flex h-10 items-center justify-center rounded-md bg-bolt-elements-background-depth-2 p-1',
'text-bolt-elements-textSecondary',
className={classNames(
'inline-flex h-10 items-center justify-center rounded-md bg-bolt-elements-background p-1 text-bolt-elements-textSecondary',
className,
)}
{...props}
@@ -20,17 +19,14 @@ const TabsList = forwardRef<
));
TabsList.displayName = TabsPrimitive.List.displayName;
const TabsTrigger = forwardRef<
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
'inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-bolt-elements-background-depth-1',
'transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-purple-500/30 focus-visible:ring-offset-2',
'disabled:pointer-events-none disabled:opacity-50',
'data-[state=active]:bg-bolt-elements-background-depth-1 data-[state=active]:text-bolt-elements-textPrimary data-[state=active]:shadow-sm',
className={classNames(
'inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-bolt-elements-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-bolt-elements-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-bolt-elements-background data-[state=active]:text-bolt-elements-textPrimary data-[state=active]:shadow-sm',
className,
)}
{...props}
@@ -38,14 +34,14 @@ const TabsTrigger = forwardRef<
));
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
const TabsContent = forwardRef<
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
'mt-2 ring-offset-bolt-elements-background-depth-1 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-purple-500/30 focus-visible:ring-offset-2',
className={classNames(
'mt-2 ring-offset-bolt-elements-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-bolt-elements-ring focus-visible:ring-offset-2',
className,
)}
{...props}