Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions app/components/@settings/core/ControlPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import { DataTab } from '~/components/@settings/tabs/data/DataTab';
import { EventLogsTab } from '~/components/@settings/tabs/event-logs/EventLogsTab';
import ConnectionsTab from '~/components/@settings/tabs/connections/ConnectionsTab';
import CloudProvidersTab from '~/components/@settings/tabs/providers/cloud/CloudProvidersTab';
import ServiceStatusTab from '~/components/@settings/tabs/providers/status/ServiceStatusTab';
import LocalProvidersTab from '~/components/@settings/tabs/providers/local/LocalProvidersTab';
import McpTab from '~/components/@settings/tabs/mcp/McpTab';

Expand All @@ -33,7 +32,7 @@ interface ControlPanelProps {
}

// Beta status for experimental features
const BETA_TABS = new Set<TabType>(['service-status', 'local-providers', 'mcp']);
const BETA_TABS = new Set<TabType>(['local-providers', 'mcp']);

const BetaLabel = () => (
<div className="absolute top-2 right-2 px-1.5 py-0.5 rounded-full bg-purple-500/10 dark:bg-purple-500/20">
Expand Down Expand Up @@ -138,8 +137,6 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
return <ConnectionsTab />;
case 'event-logs':
return <EventLogsTab />;
case 'service-status':
return <ServiceStatusTab />;
case 'mcp':
return <McpTab />;

Expand Down
6 changes: 1 addition & 5 deletions app/components/@settings/core/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ export const TAB_ICONS: Record<TabType, string> = {
data: 'i-ph:database',
'cloud-providers': 'i-ph:cloud',
'local-providers': 'i-ph:laptop',
'service-status': 'i-ph:activity-bold',
connection: 'i-ph:wifi-high',
'event-logs': 'i-ph:list-bullets',
mcp: 'i-ph:wrench',
Expand All @@ -22,7 +21,6 @@ export const TAB_LABELS: Record<TabType, string> = {
data: 'Data Management',
'cloud-providers': 'Cloud Providers',
'local-providers': 'Local Providers',
'service-status': 'Service Status',
connection: 'Connection',
'event-logs': 'Event Logs',
mcp: 'MCP Servers',
Expand All @@ -36,7 +34,6 @@ export const TAB_DESCRIPTIONS: Record<TabType, string> = {
data: 'Manage your data and storage',
'cloud-providers': 'Configure cloud AI providers and models',
'local-providers': 'Configure local AI providers and models',
'service-status': 'Monitor cloud LLM service status',
connection: 'Check connection status and settings',
'event-logs': 'View system events and logs',
mcp: 'Configure MCP (Model Context Protocol) servers',
Expand All @@ -54,8 +51,7 @@ export const DEFAULT_TAB_CONFIG = [
{ id: 'mcp', visible: true, window: 'user' as const, order: 7 },

{ id: 'profile', visible: true, window: 'user' as const, order: 9 },
{ id: 'service-status', visible: true, window: 'user' as const, order: 10 },
{ id: 'settings', visible: true, window: 'user' as const, order: 11 },
{ id: 'settings', visible: true, window: 'user' as const, order: 10 },

// User Window Tabs (In dropdown, initially hidden)
];
2 changes: 0 additions & 2 deletions app/components/@settings/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ export type TabType =
| 'data'
| 'cloud-providers'
| 'local-providers'
| 'service-status'
| 'connection'
| 'event-logs'
| 'mcp';
Expand Down Expand Up @@ -70,7 +69,6 @@ export const TAB_LABELS: Record<TabType, string> = {
data: 'Data Management',
'cloud-providers': 'Cloud Providers',
'local-providers': 'Local Providers',
'service-status': 'Service Status',
connection: 'Connections',
'event-logs': 'Event Logs',
mcp: 'MCP Servers',
Expand Down
68 changes: 68 additions & 0 deletions app/components/@settings/tabs/providers/local/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import React, { Component } from 'react';
import type { ReactNode } from 'react';
import { AlertCircle } from 'lucide-react';
import { classNames } from '~/utils/classNames';

interface Props {
children: ReactNode;
fallback?: ReactNode;
onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
}

interface State {
hasError: boolean;
error?: Error;
}

export default class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false };
}

static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}

componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('Local Providers Error Boundary caught an error:', error, errorInfo);
this.props.onError?.(error, errorInfo);
}

render() {
if (this.state.hasError) {
if (this.props.fallback) {
return this.props.fallback;
}

return (
<div className={classNames('p-6 rounded-lg border border-red-500/20', 'bg-red-500/5 text-center')}>
<AlertCircle className="w-12 h-12 mx-auto text-red-500 mb-4" />
<h3 className="text-lg font-medium text-red-500 mb-2">Something went wrong</h3>
<p className="text-sm text-red-400 mb-4">There was an error loading the local providers section.</p>
<button
onClick={() => this.setState({ hasError: false, error: undefined })}
className={classNames(
'px-4 py-2 rounded-lg text-sm font-medium',
'bg-red-500/10 text-red-500',
'hover:bg-red-500/20',
'transition-colors duration-200',
)}
>
Try Again
</button>
{process.env.NODE_ENV === 'development' && this.state.error && (
<details className="mt-4 text-left">
<summary className="cursor-pointer text-sm text-red-400 hover:text-red-300">Error Details</summary>
<pre className="mt-2 p-2 bg-red-500/10 rounded text-xs text-red-300 overflow-auto">
{this.state.error.stack}
</pre>
</details>
)}
</div>
);
}

return this.props.children;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React from 'react';
import { CheckCircle, XCircle, Loader2, AlertCircle } from 'lucide-react';
import { classNames } from '~/utils/classNames';

interface HealthStatusBadgeProps {
status: 'healthy' | 'unhealthy' | 'checking' | 'unknown';
responseTime?: number;
className?: string;
}

function HealthStatusBadge({ status, responseTime, className }: HealthStatusBadgeProps) {
const getStatusConfig = () => {
switch (status) {
case 'healthy':
return {
color: 'text-green-500',
bgColor: 'bg-green-500/10 border-green-500/20',
Icon: CheckCircle,
label: 'Healthy',
};
case 'unhealthy':
return {
color: 'text-red-500',
bgColor: 'bg-red-500/10 border-red-500/20',
Icon: XCircle,
label: 'Unhealthy',
};
case 'checking':
return {
color: 'text-blue-500',
bgColor: 'bg-blue-500/10 border-blue-500/20',
Icon: Loader2,
label: 'Checking',
};
default:
return {
color: 'text-bolt-elements-textTertiary',
bgColor: 'bg-bolt-elements-background-depth-3 border-bolt-elements-borderColor',
Icon: AlertCircle,
label: 'Unknown',
};
}
};

const config = getStatusConfig();
const Icon = config.Icon;

return (
<div
className={classNames(
'flex items-center gap-2 px-3 py-1.5 rounded-lg text-xs font-medium border transition-colors',
config.bgColor,
config.color,
className,
)}
>
<Icon className={classNames('w-3 h-3', { 'animate-spin': status === 'checking' })} />
<span>{config.label}</span>
{responseTime !== undefined && status === 'healthy' && <span className="opacity-75">({responseTime}ms)</span>}
</div>
);
}

export default HealthStatusBadge;
107 changes: 107 additions & 0 deletions app/components/@settings/tabs/providers/local/LoadingSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import React from 'react';
import { classNames } from '~/utils/classNames';

interface LoadingSkeletonProps {
className?: string;
lines?: number;
height?: string;
}

export function LoadingSkeleton({ className, lines = 1, height = 'h-4' }: LoadingSkeletonProps) {
return (
<div className={classNames('space-y-2', className)}>
{Array.from({ length: lines }).map((_, i) => (
<div
key={i}
className={classNames('bg-bolt-elements-background-depth-3 rounded', height, 'animate-pulse')}
style={{ animationDelay: `${i * 0.1}s` }}
/>
))}
</div>
);
}

interface ModelCardSkeletonProps {
className?: string;
}

export function ModelCardSkeleton({ className }: ModelCardSkeletonProps) {
return (
<div
className={classNames(
'border rounded-lg p-4',
'bg-bolt-elements-background-depth-2',
'border-bolt-elements-borderColor',
className,
)}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3 flex-1">
<div className="w-3 h-3 rounded-full bg-bolt-elements-textTertiary animate-pulse" />
<div className="space-y-2 flex-1">
<LoadingSkeleton height="h-5" lines={1} className="w-3/4" />
<LoadingSkeleton height="h-3" lines={1} className="w-1/2" />
</div>
</div>
<div className="w-4 h-4 bg-bolt-elements-textTertiary rounded animate-pulse" />
</div>
</div>
);
}

interface ProviderCardSkeletonProps {
className?: string;
}

export function ProviderCardSkeleton({ className }: ProviderCardSkeletonProps) {
return (
<div className={classNames('bg-bolt-elements-background-depth-2 rounded-xl p-5', className)}>
<div className="flex items-start justify-between gap-4">
<div className="flex items-start gap-4 flex-1">
<div className="w-12 h-12 rounded-xl bg-bolt-elements-background-depth-3 animate-pulse" />
<div className="space-y-3 flex-1">
<div className="space-y-2">
<LoadingSkeleton height="h-5" lines={1} className="w-1/3" />
<LoadingSkeleton height="h-4" lines={1} className="w-2/3" />
</div>
<div className="space-y-2">
<LoadingSkeleton height="h-3" lines={1} className="w-1/4" />
<LoadingSkeleton height="h-8" lines={1} className="w-full" />
</div>
</div>
</div>
<div className="w-10 h-6 bg-bolt-elements-background-depth-3 rounded-full animate-pulse" />
</div>
</div>
);
}

interface ModelManagerSkeletonProps {
className?: string;
cardCount?: number;
}

export function ModelManagerSkeleton({ className, cardCount = 3 }: ModelManagerSkeletonProps) {
return (
<div className={classNames('space-y-6', className)}>
{/* Header */}
<div className="flex items-center justify-between">
<div className="space-y-2">
<LoadingSkeleton height="h-6" lines={1} className="w-48" />
<LoadingSkeleton height="h-4" lines={1} className="w-64" />
</div>
<div className="flex items-center gap-2">
<div className="w-24 h-8 bg-bolt-elements-background-depth-3 rounded-lg animate-pulse" />
<div className="w-16 h-8 bg-bolt-elements-background-depth-3 rounded-lg animate-pulse" />
</div>
</div>

{/* Model Cards */}
<div className="space-y-4">
{Array.from({ length: cardCount }).map((_, i) => (
<ModelCardSkeleton key={i} />
))}
</div>
</div>
);
}
Loading
Loading