Skip to content

Commit a44de8a

Browse files
Stijnusclaude
andauthored
feat: local providers refactor & enhancement (#1968)
* feat: improve local providers health monitoring and model management - Add automatic health monitoring initialization for enabled providers - Add LM Studio model management and display functionality - Fix endpoint status detection by setting default base URLs - Replace mixed icon libraries with consistent Lucide icons only - Fix button styling with transparent backgrounds - Add comprehensive setup guides with web-researched content - Add proper navigation with back buttons between views - Fix all TypeScript and linting issues in LocalProvidersTab 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * Remove Service Status tab and related code The Service Status tab and all associated files, components, and provider checkers have been deleted. References to 'service-status' have been removed from tab constants, types, and the control panel. This simplifies the settings UI and codebase by eliminating the service status monitoring feature. * Update LocalProvidersTab.tsx * Fix all linter and TypeScript errors in local providers components - Remove unused imports and fix import formatting - Fix type-only imports for OllamaModel and LMStudioModel - Fix Icon component usage in ProviderCard.tsx - Clean up unused imports across all local provider files - Ensure all TypeScript and ESLint checks pass --------- Co-authored-by: Claude <[email protected]>
1 parent 3ea9650 commit a44de8a

37 files changed

+2785
-3697
lines changed

app/components/@settings/core/ControlPanel.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import { DataTab } from '~/components/@settings/tabs/data/DataTab';
2323
import { EventLogsTab } from '~/components/@settings/tabs/event-logs/EventLogsTab';
2424
import ConnectionsTab from '~/components/@settings/tabs/connections/ConnectionsTab';
2525
import CloudProvidersTab from '~/components/@settings/tabs/providers/cloud/CloudProvidersTab';
26-
import ServiceStatusTab from '~/components/@settings/tabs/providers/status/ServiceStatusTab';
2726
import LocalProvidersTab from '~/components/@settings/tabs/providers/local/LocalProvidersTab';
2827
import McpTab from '~/components/@settings/tabs/mcp/McpTab';
2928

@@ -33,7 +32,7 @@ interface ControlPanelProps {
3332
}
3433

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

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

app/components/@settings/core/constants.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ export const TAB_ICONS: Record<TabType, string> = {
88
data: 'i-ph:database',
99
'cloud-providers': 'i-ph:cloud',
1010
'local-providers': 'i-ph:laptop',
11-
'service-status': 'i-ph:activity-bold',
1211
connection: 'i-ph:wifi-high',
1312
'event-logs': 'i-ph:list-bullets',
1413
mcp: 'i-ph:wrench',
@@ -22,7 +21,6 @@ export const TAB_LABELS: Record<TabType, string> = {
2221
data: 'Data Management',
2322
'cloud-providers': 'Cloud Providers',
2423
'local-providers': 'Local Providers',
25-
'service-status': 'Service Status',
2624
connection: 'Connection',
2725
'event-logs': 'Event Logs',
2826
mcp: 'MCP Servers',
@@ -36,7 +34,6 @@ export const TAB_DESCRIPTIONS: Record<TabType, string> = {
3634
data: 'Manage your data and storage',
3735
'cloud-providers': 'Configure cloud AI providers and models',
3836
'local-providers': 'Configure local AI providers and models',
39-
'service-status': 'Monitor cloud LLM service status',
4037
connection: 'Check connection status and settings',
4138
'event-logs': 'View system events and logs',
4239
mcp: 'Configure MCP (Model Context Protocol) servers',
@@ -54,8 +51,7 @@ export const DEFAULT_TAB_CONFIG = [
5451
{ id: 'mcp', visible: true, window: 'user' as const, order: 7 },
5552

5653
{ id: 'profile', visible: true, window: 'user' as const, order: 9 },
57-
{ id: 'service-status', visible: true, window: 'user' as const, order: 10 },
58-
{ id: 'settings', visible: true, window: 'user' as const, order: 11 },
54+
{ id: 'settings', visible: true, window: 'user' as const, order: 10 },
5955

6056
// User Window Tabs (In dropdown, initially hidden)
6157
];

app/components/@settings/core/types.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ export type TabType =
1010
| 'data'
1111
| 'cloud-providers'
1212
| 'local-providers'
13-
| 'service-status'
1413
| 'connection'
1514
| 'event-logs'
1615
| 'mcp';
@@ -70,7 +69,6 @@ export const TAB_LABELS: Record<TabType, string> = {
7069
data: 'Data Management',
7170
'cloud-providers': 'Cloud Providers',
7271
'local-providers': 'Local Providers',
73-
'service-status': 'Service Status',
7472
connection: 'Connections',
7573
'event-logs': 'Event Logs',
7674
mcp: 'MCP Servers',
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import React, { Component } from 'react';
2+
import type { ReactNode } from 'react';
3+
import { AlertCircle } from 'lucide-react';
4+
import { classNames } from '~/utils/classNames';
5+
6+
interface Props {
7+
children: ReactNode;
8+
fallback?: ReactNode;
9+
onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
10+
}
11+
12+
interface State {
13+
hasError: boolean;
14+
error?: Error;
15+
}
16+
17+
export default class ErrorBoundary extends Component<Props, State> {
18+
constructor(props: Props) {
19+
super(props);
20+
this.state = { hasError: false };
21+
}
22+
23+
static getDerivedStateFromError(error: Error): State {
24+
return { hasError: true, error };
25+
}
26+
27+
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
28+
console.error('Local Providers Error Boundary caught an error:', error, errorInfo);
29+
this.props.onError?.(error, errorInfo);
30+
}
31+
32+
render() {
33+
if (this.state.hasError) {
34+
if (this.props.fallback) {
35+
return this.props.fallback;
36+
}
37+
38+
return (
39+
<div className={classNames('p-6 rounded-lg border border-red-500/20', 'bg-red-500/5 text-center')}>
40+
<AlertCircle className="w-12 h-12 mx-auto text-red-500 mb-4" />
41+
<h3 className="text-lg font-medium text-red-500 mb-2">Something went wrong</h3>
42+
<p className="text-sm text-red-400 mb-4">There was an error loading the local providers section.</p>
43+
<button
44+
onClick={() => this.setState({ hasError: false, error: undefined })}
45+
className={classNames(
46+
'px-4 py-2 rounded-lg text-sm font-medium',
47+
'bg-red-500/10 text-red-500',
48+
'hover:bg-red-500/20',
49+
'transition-colors duration-200',
50+
)}
51+
>
52+
Try Again
53+
</button>
54+
{process.env.NODE_ENV === 'development' && this.state.error && (
55+
<details className="mt-4 text-left">
56+
<summary className="cursor-pointer text-sm text-red-400 hover:text-red-300">Error Details</summary>
57+
<pre className="mt-2 p-2 bg-red-500/10 rounded text-xs text-red-300 overflow-auto">
58+
{this.state.error.stack}
59+
</pre>
60+
</details>
61+
)}
62+
</div>
63+
);
64+
}
65+
66+
return this.props.children;
67+
}
68+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import React from 'react';
2+
import { CheckCircle, XCircle, Loader2, AlertCircle } from 'lucide-react';
3+
import { classNames } from '~/utils/classNames';
4+
5+
interface HealthStatusBadgeProps {
6+
status: 'healthy' | 'unhealthy' | 'checking' | 'unknown';
7+
responseTime?: number;
8+
className?: string;
9+
}
10+
11+
function HealthStatusBadge({ status, responseTime, className }: HealthStatusBadgeProps) {
12+
const getStatusConfig = () => {
13+
switch (status) {
14+
case 'healthy':
15+
return {
16+
color: 'text-green-500',
17+
bgColor: 'bg-green-500/10 border-green-500/20',
18+
Icon: CheckCircle,
19+
label: 'Healthy',
20+
};
21+
case 'unhealthy':
22+
return {
23+
color: 'text-red-500',
24+
bgColor: 'bg-red-500/10 border-red-500/20',
25+
Icon: XCircle,
26+
label: 'Unhealthy',
27+
};
28+
case 'checking':
29+
return {
30+
color: 'text-blue-500',
31+
bgColor: 'bg-blue-500/10 border-blue-500/20',
32+
Icon: Loader2,
33+
label: 'Checking',
34+
};
35+
default:
36+
return {
37+
color: 'text-bolt-elements-textTertiary',
38+
bgColor: 'bg-bolt-elements-background-depth-3 border-bolt-elements-borderColor',
39+
Icon: AlertCircle,
40+
label: 'Unknown',
41+
};
42+
}
43+
};
44+
45+
const config = getStatusConfig();
46+
const Icon = config.Icon;
47+
48+
return (
49+
<div
50+
className={classNames(
51+
'flex items-center gap-2 px-3 py-1.5 rounded-lg text-xs font-medium border transition-colors',
52+
config.bgColor,
53+
config.color,
54+
className,
55+
)}
56+
>
57+
<Icon className={classNames('w-3 h-3', { 'animate-spin': status === 'checking' })} />
58+
<span>{config.label}</span>
59+
{responseTime !== undefined && status === 'healthy' && <span className="opacity-75">({responseTime}ms)</span>}
60+
</div>
61+
);
62+
}
63+
64+
export default HealthStatusBadge;
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import React from 'react';
2+
import { classNames } from '~/utils/classNames';
3+
4+
interface LoadingSkeletonProps {
5+
className?: string;
6+
lines?: number;
7+
height?: string;
8+
}
9+
10+
export function LoadingSkeleton({ className, lines = 1, height = 'h-4' }: LoadingSkeletonProps) {
11+
return (
12+
<div className={classNames('space-y-2', className)}>
13+
{Array.from({ length: lines }).map((_, i) => (
14+
<div
15+
key={i}
16+
className={classNames('bg-bolt-elements-background-depth-3 rounded', height, 'animate-pulse')}
17+
style={{ animationDelay: `${i * 0.1}s` }}
18+
/>
19+
))}
20+
</div>
21+
);
22+
}
23+
24+
interface ModelCardSkeletonProps {
25+
className?: string;
26+
}
27+
28+
export function ModelCardSkeleton({ className }: ModelCardSkeletonProps) {
29+
return (
30+
<div
31+
className={classNames(
32+
'border rounded-lg p-4',
33+
'bg-bolt-elements-background-depth-2',
34+
'border-bolt-elements-borderColor',
35+
className,
36+
)}
37+
>
38+
<div className="flex items-center justify-between">
39+
<div className="flex items-center gap-3 flex-1">
40+
<div className="w-3 h-3 rounded-full bg-bolt-elements-textTertiary animate-pulse" />
41+
<div className="space-y-2 flex-1">
42+
<LoadingSkeleton height="h-5" lines={1} className="w-3/4" />
43+
<LoadingSkeleton height="h-3" lines={1} className="w-1/2" />
44+
</div>
45+
</div>
46+
<div className="w-4 h-4 bg-bolt-elements-textTertiary rounded animate-pulse" />
47+
</div>
48+
</div>
49+
);
50+
}
51+
52+
interface ProviderCardSkeletonProps {
53+
className?: string;
54+
}
55+
56+
export function ProviderCardSkeleton({ className }: ProviderCardSkeletonProps) {
57+
return (
58+
<div className={classNames('bg-bolt-elements-background-depth-2 rounded-xl p-5', className)}>
59+
<div className="flex items-start justify-between gap-4">
60+
<div className="flex items-start gap-4 flex-1">
61+
<div className="w-12 h-12 rounded-xl bg-bolt-elements-background-depth-3 animate-pulse" />
62+
<div className="space-y-3 flex-1">
63+
<div className="space-y-2">
64+
<LoadingSkeleton height="h-5" lines={1} className="w-1/3" />
65+
<LoadingSkeleton height="h-4" lines={1} className="w-2/3" />
66+
</div>
67+
<div className="space-y-2">
68+
<LoadingSkeleton height="h-3" lines={1} className="w-1/4" />
69+
<LoadingSkeleton height="h-8" lines={1} className="w-full" />
70+
</div>
71+
</div>
72+
</div>
73+
<div className="w-10 h-6 bg-bolt-elements-background-depth-3 rounded-full animate-pulse" />
74+
</div>
75+
</div>
76+
);
77+
}
78+
79+
interface ModelManagerSkeletonProps {
80+
className?: string;
81+
cardCount?: number;
82+
}
83+
84+
export function ModelManagerSkeleton({ className, cardCount = 3 }: ModelManagerSkeletonProps) {
85+
return (
86+
<div className={classNames('space-y-6', className)}>
87+
{/* Header */}
88+
<div className="flex items-center justify-between">
89+
<div className="space-y-2">
90+
<LoadingSkeleton height="h-6" lines={1} className="w-48" />
91+
<LoadingSkeleton height="h-4" lines={1} className="w-64" />
92+
</div>
93+
<div className="flex items-center gap-2">
94+
<div className="w-24 h-8 bg-bolt-elements-background-depth-3 rounded-lg animate-pulse" />
95+
<div className="w-16 h-8 bg-bolt-elements-background-depth-3 rounded-lg animate-pulse" />
96+
</div>
97+
</div>
98+
99+
{/* Model Cards */}
100+
<div className="space-y-4">
101+
{Array.from({ length: cardCount }).map((_, i) => (
102+
<ModelCardSkeleton key={i} />
103+
))}
104+
</div>
105+
</div>
106+
);
107+
}

0 commit comments

Comments
 (0)