big fixes
fixes feedback from thecodacus
This commit is contained in:
@@ -154,3 +154,4 @@ bolt.diy (previously oTToDev) is an open-source AI-powered full-stack web develo
|
||||
- Don't use white background for dark mode
|
||||
- Don't use white text on white background for dark mode
|
||||
- Match the style of the existing codebase
|
||||
- Use consistent naming conventions for components and variables
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -42,3 +42,5 @@ site
|
||||
app/commit.json
|
||||
changelogUI.md
|
||||
docs/instructions/Roadmap.md
|
||||
.cursorrules
|
||||
.cursorrules
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -18,7 +18,7 @@ import Cookies from 'js-cookie';
|
||||
import type { IProviderSetting, ProviderInfo, IProviderConfig } from '~/types/model';
|
||||
import type { TabWindowConfig, TabVisibilityConfig } from '~/components/settings/settings.types';
|
||||
import { logStore } from '~/lib/stores/logs';
|
||||
import { getLocalStorage, setLocalStorage } from '~/utils/localStorage';
|
||||
import { getLocalStorage, setLocalStorage } from '~/lib/persistence';
|
||||
|
||||
export interface Settings {
|
||||
theme: 'light' | 'dark' | 'system';
|
||||
|
||||
53
app/lib/modules/llm/providers/github.ts
Normal file
53
app/lib/modules/llm/providers/github.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { BaseProvider } from '~/lib/modules/llm/base-provider';
|
||||
import type { ModelInfo } from '~/lib/modules/llm/types';
|
||||
import type { IProviderSetting } from '~/types/model';
|
||||
import type { LanguageModelV1 } from 'ai';
|
||||
import { createOpenAI } from '@ai-sdk/openai';
|
||||
|
||||
export default class GithubProvider extends BaseProvider {
|
||||
name = 'Github';
|
||||
getApiKeyLink = 'https://github.com/settings/personal-access-tokens';
|
||||
|
||||
config = {
|
||||
apiTokenKey: 'GITHUB_API_KEY',
|
||||
};
|
||||
|
||||
// find more in https://github.com/marketplace?type=models
|
||||
staticModels: ModelInfo[] = [
|
||||
{ name: 'gpt-4o', label: 'GPT-4o', provider: 'Github', maxTokenAllowed: 8000 },
|
||||
{ name: 'o1', label: 'o1-preview', provider: 'Github', maxTokenAllowed: 100000 },
|
||||
{ name: 'o1-mini', label: 'o1-mini', provider: 'Github', maxTokenAllowed: 8000 },
|
||||
{ name: 'gpt-4o-mini', label: 'GPT-4o Mini', provider: 'Github', maxTokenAllowed: 8000 },
|
||||
{ name: 'gpt-4-turbo', label: 'GPT-4 Turbo', provider: 'Github', maxTokenAllowed: 8000 },
|
||||
{ name: 'gpt-4', label: 'GPT-4', provider: 'Github', maxTokenAllowed: 8000 },
|
||||
{ name: 'gpt-3.5-turbo', label: 'GPT-3.5 Turbo', provider: 'Github', maxTokenAllowed: 8000 },
|
||||
];
|
||||
|
||||
getModelInstance(options: {
|
||||
model: string;
|
||||
serverEnv: Env;
|
||||
apiKeys?: Record<string, string>;
|
||||
providerSettings?: Record<string, IProviderSetting>;
|
||||
}): LanguageModelV1 {
|
||||
const { model, serverEnv, apiKeys, providerSettings } = options;
|
||||
|
||||
const { apiKey } = this.getProviderBaseUrlAndKey({
|
||||
apiKeys,
|
||||
providerSettings: providerSettings?.[this.name],
|
||||
serverEnv: serverEnv as any,
|
||||
defaultBaseUrlKey: '',
|
||||
defaultApiTokenKey: 'GITHUB_API_KEY',
|
||||
});
|
||||
|
||||
if (!apiKey) {
|
||||
throw new Error(`Missing API key for ${this.name} provider`);
|
||||
}
|
||||
|
||||
const openai = createOpenAI({
|
||||
baseURL: 'https://models.inference.ai.azure.com',
|
||||
apiKey,
|
||||
});
|
||||
|
||||
return openai(model);
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import TogetherProvider from './providers/together';
|
||||
import XAIProvider from './providers/xai';
|
||||
import HyperbolicProvider from './providers/hyperbolic';
|
||||
import AmazonBedrockProvider from './providers/amazon-bedrock';
|
||||
import GithubProvider from './providers/github';
|
||||
|
||||
export {
|
||||
AnthropicProvider,
|
||||
@@ -34,4 +35,5 @@ export {
|
||||
TogetherProvider,
|
||||
LMStudioProvider,
|
||||
AmazonBedrockProvider,
|
||||
GithubProvider,
|
||||
};
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './localStorage';
|
||||
export * from './db';
|
||||
export * from './useChatHistory';
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
export function getLocalStorage(key: string) {
|
||||
// Client-side storage utilities
|
||||
const isClient = typeof window !== 'undefined' && typeof localStorage !== 'undefined';
|
||||
|
||||
export function getLocalStorage(key: string): any | null {
|
||||
if (!isClient) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const item = localStorage.getItem(key);
|
||||
return item ? JSON.parse(item) : null;
|
||||
@@ -8,7 +15,11 @@ export function getLocalStorage(key: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export function setLocalStorage(key: string, value: any) {
|
||||
export function setLocalStorage(key: string, value: any): void {
|
||||
if (!isClient) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
localStorage.setItem(key, JSON.stringify(value));
|
||||
} catch (error) {
|
||||
@@ -1,6 +1,6 @@
|
||||
import { WebContainer } from '@webcontainer/api';
|
||||
import type { WebContainer } from '@webcontainer/api';
|
||||
import { path } from '~/utils/path';
|
||||
import { atom, map, type MapStore } from 'nanostores';
|
||||
import * as nodePath from 'node:path';
|
||||
import type { ActionAlert, BoltAction } from '~/types/actions';
|
||||
import { createScopedLogger } from '~/utils/logger';
|
||||
import { unreachable } from '~/utils/unreachable';
|
||||
@@ -276,9 +276,9 @@ export class ActionRunner {
|
||||
}
|
||||
|
||||
const webcontainer = await this.#webcontainer;
|
||||
const relativePath = nodePath.relative(webcontainer.workdir, action.filePath);
|
||||
const relativePath = path.relative(webcontainer.workdir, action.filePath);
|
||||
|
||||
let folder = nodePath.dirname(relativePath);
|
||||
let folder = path.dirname(relativePath);
|
||||
|
||||
// remove trailing slashes
|
||||
folder = folder.replace(/\/+$/g, '');
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { PathWatcherEvent, WebContainer } from '@webcontainer/api';
|
||||
import { getEncoding } from 'istextorbinary';
|
||||
import { map, type MapStore } from 'nanostores';
|
||||
import { Buffer } from 'node:buffer';
|
||||
import * as nodePath from 'node:path';
|
||||
import { path } from '~/utils/path';
|
||||
import { bufferWatchEvents } from '~/utils/buffer';
|
||||
import { WORK_DIR } from '~/utils/constants';
|
||||
import { computeFileModifications } from '~/utils/diff';
|
||||
@@ -84,7 +84,7 @@ export class FilesStore {
|
||||
const webcontainer = await this.#webcontainer;
|
||||
|
||||
try {
|
||||
const relativePath = nodePath.relative(webcontainer.workdir, filePath);
|
||||
const relativePath = path.relative(webcontainer.workdir, filePath);
|
||||
|
||||
if (!relativePath) {
|
||||
throw new Error(`EINVAL: invalid file path, write '${relativePath}'`);
|
||||
|
||||
@@ -10,15 +10,18 @@ import { FilesStore, type FileMap } from './files';
|
||||
import { PreviewsStore } from './previews';
|
||||
import { TerminalStore } from './terminal';
|
||||
import JSZip from 'jszip';
|
||||
import { saveAs } from 'file-saver';
|
||||
import fileSaver from 'file-saver';
|
||||
import { Octokit, type RestEndpointMethodTypes } from '@octokit/rest';
|
||||
import * as nodePath from 'node:path';
|
||||
import { path } from '~/utils/path';
|
||||
import { extractRelativePath } from '~/utils/diff';
|
||||
import { description } from '~/lib/persistence';
|
||||
import Cookies from 'js-cookie';
|
||||
import { createSampler } from '~/utils/sampler';
|
||||
import type { ActionAlert } from '~/types/actions';
|
||||
|
||||
// Destructure saveAs from the CommonJS module
|
||||
const { saveAs } = fileSaver;
|
||||
|
||||
export interface ArtifactState {
|
||||
id: string;
|
||||
title: string;
|
||||
@@ -329,7 +332,7 @@ export class WorkbenchStore {
|
||||
|
||||
if (data.action.type === 'file') {
|
||||
const wc = await webcontainer;
|
||||
const fullPath = nodePath.join(wc.workdir, data.action.filePath);
|
||||
const fullPath = path.join(wc.workdir, data.action.filePath);
|
||||
|
||||
if (this.selectedFile.value !== fullPath) {
|
||||
this.setSelectedFile(fullPath);
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
import { type ClassValue, clsx } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
@@ -1,62 +1,146 @@
|
||||
import type { ActionFunctionArgs, LoaderFunction } from '@remix-run/cloudflare';
|
||||
import { json } from '@remix-run/cloudflare';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
interface PackageJson {
|
||||
name: string;
|
||||
version: string;
|
||||
description: string;
|
||||
license: string;
|
||||
dependencies: Record<string, string>;
|
||||
devDependencies: Record<string, string>;
|
||||
}
|
||||
// These are injected by Vite at build time
|
||||
declare const __APP_VERSION: string;
|
||||
declare const __PKG_NAME: string;
|
||||
declare const __PKG_DESCRIPTION: string;
|
||||
declare const __PKG_LICENSE: string;
|
||||
declare const __PKG_DEPENDENCIES: Record<string, string>;
|
||||
declare const __PKG_DEV_DEPENDENCIES: Record<string, string>;
|
||||
declare const __PKG_PEER_DEPENDENCIES: Record<string, string>;
|
||||
declare const __PKG_OPTIONAL_DEPENDENCIES: Record<string, string>;
|
||||
|
||||
const packageJson = {
|
||||
name: 'bolt.diy',
|
||||
version: '0.1.0',
|
||||
description: 'A DIY LLM interface',
|
||||
license: 'MIT',
|
||||
dependencies: {
|
||||
'@remix-run/cloudflare': '^2.0.0',
|
||||
react: '^18.0.0',
|
||||
'react-dom': '^18.0.0',
|
||||
typescript: '^5.0.0',
|
||||
},
|
||||
devDependencies: {
|
||||
'@types/react': '^18.0.0',
|
||||
'@types/react-dom': '^18.0.0',
|
||||
},
|
||||
} as PackageJson;
|
||||
const getGitInfo = () => {
|
||||
try {
|
||||
return {
|
||||
commitHash: execSync('git rev-parse --short HEAD').toString().trim(),
|
||||
branch: execSync('git rev-parse --abbrev-ref HEAD').toString().trim(),
|
||||
commitTime: execSync('git log -1 --format=%cd').toString().trim(),
|
||||
author: execSync('git log -1 --format=%an').toString().trim(),
|
||||
email: execSync('git log -1 --format=%ae').toString().trim(),
|
||||
remoteUrl: execSync('git config --get remote.origin.url').toString().trim(),
|
||||
repoName: execSync('git config --get remote.origin.url')
|
||||
.toString()
|
||||
.trim()
|
||||
.replace(/^.*github.com[:/]/, '')
|
||||
.replace(/\.git$/, ''),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to get git info:', error);
|
||||
return {
|
||||
commitHash: 'unknown',
|
||||
branch: 'unknown',
|
||||
commitTime: 'unknown',
|
||||
author: 'unknown',
|
||||
email: 'unknown',
|
||||
remoteUrl: 'unknown',
|
||||
repoName: 'unknown',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const formatDependencies = (
|
||||
deps: Record<string, string>,
|
||||
type: 'production' | 'development' | 'peer' | 'optional',
|
||||
): Array<{ name: string; version: string; type: string }> => {
|
||||
return Object.entries(deps || {}).map(([name, version]) => ({
|
||||
name,
|
||||
version: version.replace(/^\^|~/, ''),
|
||||
type,
|
||||
}));
|
||||
};
|
||||
|
||||
const getAppResponse = () => {
|
||||
const gitInfo = getGitInfo();
|
||||
|
||||
return {
|
||||
name: __PKG_NAME || 'bolt.diy',
|
||||
version: __APP_VERSION || '0.1.0',
|
||||
description: __PKG_DESCRIPTION || 'A DIY LLM interface',
|
||||
license: __PKG_LICENSE || 'MIT',
|
||||
environment: process.env.NODE_ENV || 'development',
|
||||
gitInfo,
|
||||
timestamp: new Date().toISOString(),
|
||||
runtimeInfo: {
|
||||
nodeVersion: process.version || 'unknown',
|
||||
},
|
||||
dependencies: {
|
||||
production: formatDependencies(__PKG_DEPENDENCIES, 'production'),
|
||||
development: formatDependencies(__PKG_DEV_DEPENDENCIES, 'development'),
|
||||
peer: formatDependencies(__PKG_PEER_DEPENDENCIES, 'peer'),
|
||||
optional: formatDependencies(__PKG_OPTIONAL_DEPENDENCIES, 'optional'),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const loader: LoaderFunction = async ({ request: _request }) => {
|
||||
try {
|
||||
return json({
|
||||
name: packageJson.name,
|
||||
version: packageJson.version,
|
||||
description: packageJson.description,
|
||||
license: packageJson.license,
|
||||
nodeVersion: process.version,
|
||||
dependencies: packageJson.dependencies,
|
||||
devDependencies: packageJson.devDependencies,
|
||||
});
|
||||
return json(getAppResponse());
|
||||
} catch (error) {
|
||||
console.error('Failed to get webapp info:', error);
|
||||
return json({ error: 'Failed to get webapp information' }, { status: 500 });
|
||||
return json(
|
||||
{
|
||||
name: 'bolt.diy',
|
||||
version: '0.0.0',
|
||||
description: 'Error fetching app info',
|
||||
license: 'MIT',
|
||||
environment: 'error',
|
||||
gitInfo: {
|
||||
commitHash: 'error',
|
||||
branch: 'unknown',
|
||||
commitTime: 'unknown',
|
||||
author: 'unknown',
|
||||
email: 'unknown',
|
||||
remoteUrl: 'unknown',
|
||||
repoName: 'unknown',
|
||||
},
|
||||
timestamp: new Date().toISOString(),
|
||||
runtimeInfo: { nodeVersion: 'unknown' },
|
||||
dependencies: {
|
||||
production: [],
|
||||
development: [],
|
||||
peer: [],
|
||||
optional: [],
|
||||
},
|
||||
},
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const action = async ({ request: _request }: ActionFunctionArgs) => {
|
||||
try {
|
||||
return json({
|
||||
name: packageJson.name,
|
||||
version: packageJson.version,
|
||||
description: packageJson.description,
|
||||
license: packageJson.license,
|
||||
nodeVersion: process.version,
|
||||
dependencies: packageJson.dependencies,
|
||||
devDependencies: packageJson.devDependencies,
|
||||
});
|
||||
return json(getAppResponse());
|
||||
} catch (error) {
|
||||
console.error('Failed to get webapp info:', error);
|
||||
return json({ error: 'Failed to get webapp information' }, { status: 500 });
|
||||
return json(
|
||||
{
|
||||
name: 'bolt.diy',
|
||||
version: '0.0.0',
|
||||
description: 'Error fetching app info',
|
||||
license: 'MIT',
|
||||
environment: 'error',
|
||||
gitInfo: {
|
||||
commitHash: 'error',
|
||||
branch: 'unknown',
|
||||
commitTime: 'unknown',
|
||||
author: 'unknown',
|
||||
email: 'unknown',
|
||||
remoteUrl: 'unknown',
|
||||
repoName: 'unknown',
|
||||
},
|
||||
timestamp: new Date().toISOString(),
|
||||
runtimeInfo: { nodeVersion: 'unknown' },
|
||||
dependencies: {
|
||||
production: [],
|
||||
development: [],
|
||||
peer: [],
|
||||
optional: [],
|
||||
},
|
||||
},
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,26 +1,103 @@
|
||||
import { json } from '@remix-run/node';
|
||||
import type { LoaderFunctionArgs } from '@remix-run/node';
|
||||
import type { LoaderFunction } from '@remix-run/cloudflare';
|
||||
import { json } from '@remix-run/cloudflare';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
export async function loader({ request: _request }: LoaderFunctionArgs) {
|
||||
try {
|
||||
const branch = execSync('git rev-parse --abbrev-ref HEAD').toString().trim();
|
||||
const commit = execSync('git rev-parse --short HEAD').toString().trim();
|
||||
const lastCommitMessage = execSync('git log -1 --pretty=%B').toString().trim();
|
||||
|
||||
return json({
|
||||
branch,
|
||||
commit,
|
||||
lastCommitMessage,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
} catch (error) {
|
||||
return json(
|
||||
{
|
||||
error: 'Failed to fetch git information',
|
||||
details: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
interface GitHubRepoInfo {
|
||||
name: string;
|
||||
full_name: string;
|
||||
default_branch: string;
|
||||
stargazers_count: number;
|
||||
forks_count: number;
|
||||
open_issues_count: number;
|
||||
parent?: {
|
||||
full_name: string;
|
||||
default_branch: string;
|
||||
stargazers_count: number;
|
||||
forks_count: number;
|
||||
};
|
||||
}
|
||||
|
||||
const getLocalGitInfo = () => {
|
||||
try {
|
||||
return {
|
||||
commitHash: execSync('git rev-parse --short HEAD').toString().trim(),
|
||||
branch: execSync('git rev-parse --abbrev-ref HEAD').toString().trim(),
|
||||
commitTime: execSync('git log -1 --format=%cd').toString().trim(),
|
||||
author: execSync('git log -1 --format=%an').toString().trim(),
|
||||
email: execSync('git log -1 --format=%ae').toString().trim(),
|
||||
remoteUrl: execSync('git config --get remote.origin.url').toString().trim(),
|
||||
repoName: execSync('git config --get remote.origin.url')
|
||||
.toString()
|
||||
.trim()
|
||||
.replace(/^.*github.com[:/]/, '')
|
||||
.replace(/\.git$/, ''),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to get local git info:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const getGitHubInfo = async (repoFullName: string) => {
|
||||
try {
|
||||
const response = await fetch(`https://api.github.com/repos/${repoFullName}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`GitHub API error: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return (await response.json()) as GitHubRepoInfo;
|
||||
} catch (error) {
|
||||
console.error('Failed to get GitHub info:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const loader: LoaderFunction = async ({ request: _request }) => {
|
||||
const localInfo = getLocalGitInfo();
|
||||
|
||||
// If we have local info, try to get GitHub info for both our fork and upstream
|
||||
let githubInfo = null;
|
||||
|
||||
if (localInfo?.repoName) {
|
||||
githubInfo = await getGitHubInfo(localInfo.repoName);
|
||||
}
|
||||
|
||||
// If no local info or GitHub info, try the main repo
|
||||
if (!githubInfo) {
|
||||
githubInfo = await getGitHubInfo('stackblitz-labs/bolt.diy');
|
||||
}
|
||||
|
||||
return json({
|
||||
local: localInfo || {
|
||||
commitHash: 'unknown',
|
||||
branch: 'unknown',
|
||||
commitTime: 'unknown',
|
||||
author: 'unknown',
|
||||
email: 'unknown',
|
||||
remoteUrl: 'unknown',
|
||||
repoName: 'unknown',
|
||||
},
|
||||
github: githubInfo
|
||||
? {
|
||||
currentRepo: {
|
||||
fullName: githubInfo.full_name,
|
||||
defaultBranch: githubInfo.default_branch,
|
||||
stars: githubInfo.stargazers_count,
|
||||
forks: githubInfo.forks_count,
|
||||
openIssues: githubInfo.open_issues_count,
|
||||
},
|
||||
upstream: githubInfo.parent
|
||||
? {
|
||||
fullName: githubInfo.parent.full_name,
|
||||
defaultBranch: githubInfo.parent.default_branch,
|
||||
stars: githubInfo.parent.stargazers_count,
|
||||
forks: githubInfo.parent.forks_count,
|
||||
}
|
||||
: null,
|
||||
}
|
||||
: null,
|
||||
isForked: Boolean(githubInfo?.parent),
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -63,7 +63,7 @@ export const STARTER_TEMPLATES: Template[] = [
|
||||
description: 'Slidev starter template for creating developer-friendly presentations using Markdown',
|
||||
githubRepo: 'thecodacus/bolt-slidev-template',
|
||||
tags: ['slidev', 'presentation', 'markdown'],
|
||||
icon: 'i-bolt:slidev',
|
||||
icon: 'i-bolt-slidev',
|
||||
},
|
||||
{
|
||||
name: 'bolt-sveltekit',
|
||||
|
||||
19
app/utils/path.ts
Normal file
19
app/utils/path.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
// Browser-compatible path utilities
|
||||
import type { ParsedPath } from 'path';
|
||||
import pathBrowserify from 'path-browserify';
|
||||
|
||||
/**
|
||||
* A browser-compatible path utility that mimics Node's path module
|
||||
* Using path-browserify for consistent behavior in browser environments
|
||||
*/
|
||||
export const path = {
|
||||
join: (...paths: string[]): string => pathBrowserify.join(...paths),
|
||||
dirname: (path: string): string => pathBrowserify.dirname(path),
|
||||
basename: (path: string, ext?: string): string => pathBrowserify.basename(path, ext),
|
||||
extname: (path: string): string => pathBrowserify.extname(path),
|
||||
relative: (from: string, to: string): string => pathBrowserify.relative(from, to),
|
||||
isAbsolute: (path: string): boolean => pathBrowserify.isAbsolute(path),
|
||||
normalize: (path: string): string => pathBrowserify.normalize(path),
|
||||
parse: (path: string): ParsedPath => pathBrowserify.parse(path),
|
||||
format: (pathObject: ParsedPath): string => pathBrowserify.format(pathObject),
|
||||
} as const;
|
||||
@@ -1 +1,3 @@
|
||||
<svg xmlns='http://www.w3.org/2000/svg' width='29' height='28' fill='none'><g clip-path='url(#a)'><mask id='b' width='29' height='29' x='0' y='-1' maskUnits='userSpaceOnUse' style='mask-type:luminance'><path fill='#fff' d='M28.573-.002h-28v28h28v-28Z'/></mask><g mask='url(#b)'><path fill='#4B4B4B' d='M22.243 3.408H7.634A3.652 3.652 0 0 0 3.982 7.06v14.61a3.652 3.652 0 0 0 3.652 3.651h14.609a3.652 3.652 0 0 0 3.652-3.652V7.06a3.652 3.652 0 0 0-3.652-3.652Z'/><path fill='#7B7B7B' d='M22.486 10.955c0-6.052-4.905-10.957-10.956-10.957C5.479-.002.573 4.903.573 10.955c0 6.05 4.906 10.956 10.957 10.956 6.05 0 10.956-4.905 10.956-10.956Z'/><path fill='#fff' d='M14.239 15.563c-.287-1.07-.43-1.604-.288-1.974.123-.322.378-.576.7-.7.37-.141.904.002 1.974.288l5.315 1.425c1.07.286 1.604.43 1.853.737.217.268.31.616.256.956-.062.391-.453.782-1.236 1.565l-3.891 3.892c-.783.782-1.174 1.174-1.565 1.236-.34.054-.688-.04-.957-.257-.307-.248-.45-.783-.737-1.853l-1.424-5.315Z'/></g></g><defs><clipPath id='a'><path fill='#fff' d='M.574 0h28v28h-28z'/></clipPath></defs></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V5h14v14zm-5-7l-3 3.72L9 13l-3 4h12l-4-5z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 249 B |
@@ -65,6 +65,7 @@
|
||||
"@radix-ui/react-context-menu": "^2.2.2",
|
||||
"@radix-ui/react-dialog": "^1.1.5",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.2",
|
||||
"@radix-ui/react-label": "^2.1.1",
|
||||
"@radix-ui/react-popover": "^1.1.5",
|
||||
"@radix-ui/react-progress": "^1.0.3",
|
||||
"@radix-ui/react-scroll-area": "^1.2.2",
|
||||
@@ -102,6 +103,7 @@
|
||||
"nanostores": "^0.10.3",
|
||||
"next": "^15.1.5",
|
||||
"ollama-ai-provider": "^0.15.2",
|
||||
"path-browserify": "^1.0.1",
|
||||
"react": "^18.3.1",
|
||||
"react-chartjs-2": "^5.3.0",
|
||||
"react-dnd": "^16.0.1",
|
||||
@@ -131,6 +133,7 @@
|
||||
"@types/dom-speech-recognition": "^0.0.4",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/path-browserify": "^1.0.3",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"fast-glob": "^3.3.2",
|
||||
|
||||
@@ -9,22 +9,30 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
try {
|
||||
// Get git information using git commands
|
||||
const branch = execSync('git rev-parse --abbrev-ref HEAD').toString().trim();
|
||||
const commit = execSync('git rev-parse HEAD').toString().trim();
|
||||
const commitHash = execSync('git rev-parse HEAD').toString().trim();
|
||||
const commitTime = execSync('git log -1 --format=%cd').toString().trim();
|
||||
const author = execSync('git log -1 --format=%an').toString().trim();
|
||||
const email = execSync('git log -1 --format=%ae').toString().trim();
|
||||
const remoteUrl = execSync('git config --get remote.origin.url').toString().trim();
|
||||
|
||||
// Extract repo name from remote URL
|
||||
const repoName = remoteUrl.split('/').pop()?.replace('.git', '') || '';
|
||||
|
||||
const gitInfo = {
|
||||
branch,
|
||||
commit,
|
||||
commitTime,
|
||||
author,
|
||||
remoteUrl,
|
||||
local: {
|
||||
commitHash,
|
||||
branch,
|
||||
commitTime,
|
||||
author,
|
||||
email,
|
||||
remoteUrl,
|
||||
repoName,
|
||||
},
|
||||
};
|
||||
|
||||
res.status(200).json(gitInfo);
|
||||
return res.status(200).json(gitInfo);
|
||||
} catch (error) {
|
||||
console.error('Failed to get git information:', error);
|
||||
res.status(500).json({ message: 'Failed to get git information' });
|
||||
return res.status(500).json({ message: 'Failed to get git information' });
|
||||
}
|
||||
}
|
||||
|
||||
36
pnpm-lock.yaml
generated
36
pnpm-lock.yaml
generated
@@ -116,6 +116,9 @@ importers:
|
||||
'@radix-ui/react-dropdown-menu':
|
||||
specifier: ^2.1.2
|
||||
version: 2.1.5(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-label':
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-popover':
|
||||
specifier: ^1.1.5
|
||||
version: 1.1.5(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
@@ -227,6 +230,9 @@ importers:
|
||||
ollama-ai-provider:
|
||||
specifier: ^0.15.2
|
||||
version: 0.15.2(zod@3.24.1)
|
||||
path-browserify:
|
||||
specifier: ^1.0.1
|
||||
version: 1.0.1
|
||||
react:
|
||||
specifier: ^18.3.1
|
||||
version: 18.3.1
|
||||
@@ -309,6 +315,9 @@ importers:
|
||||
'@types/js-cookie':
|
||||
specifier: ^3.0.6
|
||||
version: 3.0.6
|
||||
'@types/path-browserify':
|
||||
specifier: ^1.0.3
|
||||
version: 1.0.3
|
||||
'@types/react':
|
||||
specifier: ^18.3.12
|
||||
version: 18.3.18
|
||||
@@ -2015,6 +2024,19 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-label@2.1.1':
|
||||
resolution: {integrity: sha512-UUw5E4e/2+4kFMH7+YxORXGWggtY6sM8WIwh5RZchhLuUg2H1hc98Py+pr8HMz6rdaYrK2t296ZEjYLOCO5uUw==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-menu@2.1.5':
|
||||
resolution: {integrity: sha512-uH+3w5heoMJtqVCgYOtYVMECk1TOrkUn0OG0p5MqXC0W2ppcuVeESbou8PTHoqAjbdTEK19AGXBWcEtR5WpEQg==}
|
||||
peerDependencies:
|
||||
@@ -2801,6 +2823,9 @@ packages:
|
||||
'@types/node@22.10.10':
|
||||
resolution: {integrity: sha512-X47y/mPNzxviAGY5TcYPtYL8JsY3kAq2n8fMmKoRCxq/c4v4pyGNCzM2R6+M5/umG4ZfHuT+sgqDYqWc9rJ6ww==}
|
||||
|
||||
'@types/path-browserify@1.0.3':
|
||||
resolution: {integrity: sha512-ZmHivEbNCBtAfcrFeBCiTjdIc2dey0l7oCGNGpSuRTy8jP6UVND7oUowlvDujBy8r2Hoa8bfFUOCiPWfmtkfxw==}
|
||||
|
||||
'@types/prop-types@15.7.14':
|
||||
resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==}
|
||||
|
||||
@@ -8339,6 +8364,15 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 18.3.18
|
||||
|
||||
'@radix-ui/react-label@2.1.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
optionalDependencies:
|
||||
'@types/react': 18.3.18
|
||||
'@types/react-dom': 18.3.5(@types/react@18.3.18)
|
||||
|
||||
'@radix-ui/react-menu@2.1.5(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.1
|
||||
@@ -9292,6 +9326,8 @@ snapshots:
|
||||
dependencies:
|
||||
undici-types: 6.20.0
|
||||
|
||||
'@types/path-browserify@1.0.3': {}
|
||||
|
||||
'@types/prop-types@15.7.14': {}
|
||||
|
||||
'@types/react-dom@18.3.5(@types/react@18.3.18)':
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.074 16.86c-.72.616-2.157 1.035-3.812 1.035-2.032 0-3.735-.632-4.187-1.483-.161.488-.198 1.046-.198 1.402 0 0-.106 1.75 1.111 2.968 0-.632.438-1.149.98-1.149.963 0 .963.964.957 1.743v.069c0 1.398.866 2.557 2.083 2.557 1.014 0 1.807-.817 2.025-1.93.105.113.2.235.284.364 1.009 1.369 1.51 2.563 1.51 2.563s.201-.981.201-2.203c0-1.806-.957-2.916-1.438-3.489 1.105-.321 1.828-.809 2.09-1.119-2.135.413-4.156.413-6.314-.413 2.517-.706 3.848-1.778 4.662-2.632a8.224 8.224 0 0 0 1.019-1.597c.105-.224.201-.452.284-.686.099.099.189.21.27.329.045.064.086.131.124.201.146.259.247.547.247.854 0 .642-.321 1.219-.828 1.616z" fill="currentColor"/>
|
||||
<path d="M16.8 7.2c-.488 2.486-3.257 5.143-6.171 6.245-.043.016-.086.034-.128.051.099-.099.189-.21.27-.329.045-.064.086-.131.124-.201.146-.259.247-.547.247-.854 0-.642-.321-1.219-.828-1.616-.72.616-2.157 1.035-3.812 1.035-2.032 0-3.735-.632-4.187-1.483-.161.488-.198 1.046-.198 1.402 0 0-.106 1.75 1.111 2.968 0-.632.438-1.149.98-1.149.963 0 .963.964.957 1.743v.069c0 1.398.866 2.557 2.083 2.557 1.014 0 1.807-.817 2.025-1.93.105.113.2.235.284.364 1.009 1.369 1.51 2.563 1.51 2.563s.201-.981.201-2.203c0-1.806-.957-2.916-1.438-3.489 1.105-.321 1.828-.809 2.09-1.119-2.135.413-4.156.413-6.314-.413 2.517-.706 3.848-1.778 4.662-2.632a8.224 8.224 0 0 0 1.019-1.597c1.41-2.847 1.457-5.367 1.457-5.367s2.532 3.257 2.156 6.386z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.214 0.00500488C11.0731 0.0159347 10.9364 0.0459471 10.809 0.0940049L1.159 3.9C0.769 4.067 0.5 4.443 0.5 4.857V19.143C0.5 19.557 0.769 19.933 1.159 20.1L10.809 23.906C11.191 24.07 11.632 24.07 12.014 23.906L21.664 20.1C22.054 19.933 22.323 19.557 22.323 19.143V4.857C22.323 4.443 22.054 4.067 21.664 3.9L12.014 0.0940049C11.7573 0.00589139 11.4829 -0.0181383 11.214 0.00500488ZM11.411 2.813L19.411 6.036V17.964L11.411 21.188L3.411 17.964V6.036L11.411 2.813Z" fill="currentColor"/>
|
||||
<path d="M15.859 16.839L9.359 7.839C9.278 7.721 9.161 7.631 9.026 7.583C8.891 7.535 8.745 7.531 8.608 7.573C8.471 7.615 8.349 7.7 8.259 7.816C8.169 7.932 8.115 8.073 8.105 8.221L8.105 15.779C8.115 15.927 8.169 16.068 8.259 16.184C8.349 16.3 8.471 16.385 8.608 16.427C8.745 16.469 8.891 16.465 9.026 16.417C9.161 16.369 9.278 16.279 9.359 16.161L15.859 7.161C15.94 7.045 15.985 6.904 15.985 6.759C15.985 6.614 15.94 6.473 15.859 6.357L15.859 16.839Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 2L3 7V17L12 22L21 17V7L12 2ZM12 4.311L18.754 8.156L16.664 9.311L9.91 5.467L12 4.311ZM5.246 8.156L12 4.311L14.09 5.467L7.336 9.311L5.246 8.156ZM5 9.467L7.09 10.622L7.09 15.378L5 14.222V9.467ZM8.09 16.533L5 15.378V14.222L8.09 15.378V16.533ZM9.09 16.533V15.378L12.18 14.222V15.378L9.09 16.533ZM13.18 15.378V14.222L16.27 15.378V16.533L13.18 15.378ZM17.27 16.533L14.18 15.378V14.222L17.27 15.378V16.533ZM18.27 15.378L15.18 14.222V9.467L18.27 10.622V15.378ZM14.18 8.311L17.27 9.467L14.18 10.622L11.09 9.467L14.18 8.311ZM10.09 9.467L13.18 8.311L16.27 9.467L13.18 10.622L10.09 9.467ZM6.09 9.467L9.18 8.311L12.27 9.467L9.18 10.622L6.09 9.467Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 814 B |
@@ -16,17 +16,19 @@ const customIconCollection = {
|
||||
[collectionName]: iconPaths.reduce(
|
||||
(acc, iconPath) => {
|
||||
const [iconName] = basename(iconPath).split('.');
|
||||
console.log(`Loading icon: ${iconName} from ${iconPath}`); // Debug log
|
||||
|
||||
acc[iconName] = async () => {
|
||||
try {
|
||||
const content = await fs.readFile(iconPath, 'utf8');
|
||||
|
||||
// Simplified SVG processing
|
||||
return content
|
||||
.replace(/fill="[^"]*"/g, '')
|
||||
.replace(/fill='[^']*'/g, '')
|
||||
.replace(/width="[^"]*"/g, '')
|
||||
.replace(/height="[^"]*"/g, '')
|
||||
.replace(/viewBox="[^"]*"/g, 'viewBox="0 0 24 24"')
|
||||
.replace(/<svg([^>]*)>/, '<svg $1 fill="currentColor">');
|
||||
.replace(/fill="[^"]*"/g, 'fill="currentColor"')
|
||||
.replace(/fill='[^']*'/g, "fill='currentColor'")
|
||||
.replace(/width="[^"]*"/g, 'width="24"')
|
||||
.replace(/height="[^"]*"/g, 'height="24"')
|
||||
.replace(/viewBox="[^"]*"/g, 'viewBox="0 0 24 24"');
|
||||
} catch (error) {
|
||||
console.error(`Error loading icon ${iconName}:`, error);
|
||||
return '';
|
||||
@@ -118,7 +120,11 @@ const COLOR_PRIMITIVES = {
|
||||
};
|
||||
|
||||
export default defineConfig({
|
||||
safelist: [...Object.keys(customIconCollection[collectionName] || {}).map((x) => `i-bolt:${x}`)],
|
||||
safelist: [
|
||||
// Explicitly safelist all icon combinations
|
||||
...Object.keys(customIconCollection[collectionName] || {}).map((x) => `i-${collectionName}-${x}`),
|
||||
...Object.keys(customIconCollection[collectionName] || {}).map((x) => `i-${collectionName}:${x}`),
|
||||
],
|
||||
shortcuts: {
|
||||
'bolt-ease-cubic-bezier': 'ease-[cubic-bezier(0.4,0,0.2,1)]',
|
||||
'transition-theme': 'transition-[background-color,border-color,color] duration-150 bolt-ease-cubic-bezier',
|
||||
|
||||
@@ -6,24 +6,89 @@ import { optimizeCssModules } from 'vite-plugin-optimize-css-modules';
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
import * as dotenv from 'dotenv';
|
||||
import { execSync } from 'child_process';
|
||||
import { readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
// Get git hash with fallback
|
||||
const getGitHash = () => {
|
||||
// Get detailed git info with fallbacks
|
||||
const getGitInfo = () => {
|
||||
try {
|
||||
return execSync('git rev-parse --short HEAD').toString().trim();
|
||||
return {
|
||||
commitHash: execSync('git rev-parse --short HEAD').toString().trim(),
|
||||
branch: execSync('git rev-parse --abbrev-ref HEAD').toString().trim(),
|
||||
commitTime: execSync('git log -1 --format=%cd').toString().trim(),
|
||||
author: execSync('git log -1 --format=%an').toString().trim(),
|
||||
email: execSync('git log -1 --format=%ae').toString().trim(),
|
||||
remoteUrl: execSync('git config --get remote.origin.url').toString().trim(),
|
||||
repoName: execSync('git config --get remote.origin.url')
|
||||
.toString()
|
||||
.trim()
|
||||
.replace(/^.*github.com[:/]/, '')
|
||||
.replace(/\.git$/, ''),
|
||||
};
|
||||
} catch {
|
||||
return 'no-git-info';
|
||||
return {
|
||||
commitHash: 'no-git-info',
|
||||
branch: 'unknown',
|
||||
commitTime: 'unknown',
|
||||
author: 'unknown',
|
||||
email: 'unknown',
|
||||
remoteUrl: 'unknown',
|
||||
repoName: 'unknown',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Read package.json with detailed dependency info
|
||||
const getPackageJson = () => {
|
||||
try {
|
||||
const pkgPath = join(process.cwd(), 'package.json');
|
||||
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
||||
|
||||
return {
|
||||
name: pkg.name,
|
||||
description: pkg.description,
|
||||
license: pkg.license,
|
||||
dependencies: pkg.dependencies || {},
|
||||
devDependencies: pkg.devDependencies || {},
|
||||
peerDependencies: pkg.peerDependencies || {},
|
||||
optionalDependencies: pkg.optionalDependencies || {},
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
name: 'bolt.diy',
|
||||
description: 'A DIY LLM interface',
|
||||
license: 'MIT',
|
||||
dependencies: {},
|
||||
devDependencies: {},
|
||||
peerDependencies: {},
|
||||
optionalDependencies: {},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const pkg = getPackageJson();
|
||||
const gitInfo = getGitInfo();
|
||||
|
||||
export default defineConfig((config) => {
|
||||
return {
|
||||
define: {
|
||||
__COMMIT_HASH: JSON.stringify(getGitHash()),
|
||||
__COMMIT_HASH: JSON.stringify(gitInfo.commitHash),
|
||||
__GIT_BRANCH: JSON.stringify(gitInfo.branch),
|
||||
__GIT_COMMIT_TIME: JSON.stringify(gitInfo.commitTime),
|
||||
__GIT_AUTHOR: JSON.stringify(gitInfo.author),
|
||||
__GIT_EMAIL: JSON.stringify(gitInfo.email),
|
||||
__GIT_REMOTE_URL: JSON.stringify(gitInfo.remoteUrl),
|
||||
__GIT_REPO_NAME: JSON.stringify(gitInfo.repoName),
|
||||
__APP_VERSION: JSON.stringify(process.env.npm_package_version),
|
||||
// 'process.env': JSON.stringify(process.env)
|
||||
__PKG_NAME: JSON.stringify(pkg.name),
|
||||
__PKG_DESCRIPTION: JSON.stringify(pkg.description),
|
||||
__PKG_LICENSE: JSON.stringify(pkg.license),
|
||||
__PKG_DEPENDENCIES: JSON.stringify(pkg.dependencies),
|
||||
__PKG_DEV_DEPENDENCIES: JSON.stringify(pkg.devDependencies),
|
||||
__PKG_PEER_DEPENDENCIES: JSON.stringify(pkg.peerDependencies),
|
||||
__PKG_OPTIONAL_DEPENDENCIES: JSON.stringify(pkg.optionalDependencies),
|
||||
},
|
||||
build: {
|
||||
target: 'esnext',
|
||||
|
||||
Reference in New Issue
Block a user