big fixes
fixes feedback from thecodacus
This commit is contained in:
@@ -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]',
|
||||
|
||||
@@ -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]',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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]',
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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') => {
|
||||
|
||||
@@ -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]',
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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'],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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'],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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'],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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'],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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'],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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'],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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'],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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'],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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'],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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'],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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'],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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'],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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[];
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
@@ -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> = {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user