'use client'
import React, { useState, useEffect, useCallback } from 'react'
import { Link } from 'react-router-dom'
import { ArrowLeft, Cpu, Loader2, Download, Trash2, Play, Square, RefreshCw, AlertCircle, CheckCircle2 } from 'lucide-react'
import { llmService, LLMModel } from '../../services/llmService'
import MCPStatusBanner from '../../components/MCPStatusBanner'
export default function AILLMPage() {
const [models, setModels] = useState<LLMModel[]>([])
const [ollamaModels, setOllamaModels] = useState<LLMModel[]>([])
const [lmstudioModels, setLMStudioModels] = useState<LLMModel[]>([])
const [activeModels, setActiveModels] = useState<Record<string, any>>({})
const [loading, setLoading] = useState(true)
const [selectedProvider, setSelectedProvider] = useState<string>('all')
const [loadingModel, setLoadingModel] = useState<string | null>(null)
const [unloadingModel, setUnloadingModel] = useState<string | null>(null)
const [pullingModel, setPullingModel] = useState<string | null>(null)
const [healthStatus, setHealthStatus] = useState<any>(null)
const [chatbotOpen, setChatbotOpen] = useState(false)
const [selectedModelForChat, setSelectedModelForChat] = useState<string>('')
const loadModels = useCallback(async () => {
setLoading(true)
try {
const [allModels, ollama, lmstudio, active, health] = await Promise.all([
llmService.listModels(),
llmService.listOllamaModels(),
llmService.listLMStudioModels(),
llmService.getActiveModels(),
llmService.getHealth()
])
setModels(allModels)
setOllamaModels(ollama)
setLMStudioModels(lmstudio)
setActiveModels(active)
setHealthStatus(health)
} catch (error) {
console.error('Failed to load models:', error)
} finally {
setLoading(false)
}
}, [])
useEffect(() => {
loadModels()
const interval = setInterval(loadModels, 30000) // Refresh every 30 seconds
return () => clearInterval(interval)
}, [loadModels])
const handleLoadModel = async (modelId: string, provider: string) => {
setLoadingModel(modelId)
try {
const result = await llmService.loadModel(modelId, provider)
if (result.success) {
await loadModels()
} else {
alert(`Failed to load model: ${result.error}`)
}
} catch (error) {
alert(`Error loading model: ${error}`)
} finally {
setLoadingModel(null)
}
}
const handleUnloadModel = async (modelId: string, provider: string) => {
setUnloadingModel(modelId)
try {
const result = await llmService.unloadModel(modelId, provider)
if (result.success) {
await loadModels()
} else {
alert(`Failed to unload model: ${result.error}`)
}
} catch (error) {
alert(`Error unloading model: ${error}`)
} finally {
setUnloadingModel(null)
}
}
const handlePullModel = async (modelId: string, provider: string) => {
setPullingModel(modelId)
try {
const result = await llmService.pullModel(modelId, provider)
if (result.success) {
await loadModels()
} else {
alert(`Failed to pull model: ${result.error}`)
}
} catch (error) {
alert(`Error pulling model: ${error}`)
} finally {
setPullingModel(null)
}
}
const displayModels = selectedProvider === 'all'
? models
: selectedProvider === 'ollama'
? ollamaModels
: lmstudioModels
const isModelActive = (modelId: string) => modelId in activeModels
return (
<div style={{ minHeight: '100vh', backgroundColor: '#f8fafc', padding: '24px' }}>
{/* Header */}
<div style={{ marginBottom: '24px' }}>
<Link
to="/"
style={{
display: 'inline-flex',
alignItems: 'center',
color: '#2563eb',
textDecoration: 'none',
marginBottom: '16px'
}}
>
<ArrowLeft style={{ width: '16px', height: '16px', marginRight: '8px' }} />
Back to Home
</Link>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '16px' }}>
<Cpu style={{ width: '32px', height: '32px', color: '#2563eb' }} />
<div>
<h1 style={{ fontSize: '32px', fontWeight: '700', margin: 0, color: '#1e293b' }}>
AI & LLM Management
</h1>
<p style={{ fontSize: '16px', color: '#64748b', margin: '4px 0 0 0' }}>
Manage local and cloud LLM models, Ollama, LM Studio, and more
</p>
</div>
</div>
<MCPStatusBanner serverName="local_llm" displayName="Local LLM MCP" />
{/* Chatbot Button */}
<div style={{ marginTop: '16px', display: 'flex', justifyContent: 'flex-end' }}>
<button
onClick={() => {
const firstActiveModel = Object.keys(activeModels)[0] || models[0]?.id || ''
setSelectedModelForChat(firstActiveModel)
setChatbotOpen(true)
}}
style={{
padding: '12px 24px',
borderRadius: '8px',
border: 'none',
backgroundColor: '#10b981',
color: 'white',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: '8px',
fontWeight: '500',
fontSize: '14px'
}}
>
<MessageSquare style={{ width: '18px', height: '18px' }} />
Open Chatbot
</button>
</div>
</div>
{/* Health Status */}
{healthStatus && (
<div style={{
backgroundColor: healthStatus.success ? '#dcfce7' : '#fef3c7',
border: `1px solid ${healthStatus.success ? '#86efac' : '#fde047'}`,
borderRadius: '8px',
padding: '16px',
marginBottom: '24px',
display: 'flex',
alignItems: 'center',
gap: '12px'
}}>
{healthStatus.success ? (
<CheckCircle2 style={{ width: '20px', height: '20px', color: '#16a34a' }} />
) : (
<AlertCircle style={{ width: '20px', height: '20px', color: '#ca8a04' }} />
)}
<div>
<div style={{ fontWeight: '600', color: '#1e293b' }}>
LLM Service Status: {healthStatus.success ? 'Healthy' : 'Warning'}
</div>
{healthStatus.data && (
<div style={{ fontSize: '14px', color: '#64748b', marginTop: '4px' }}>
{JSON.stringify(healthStatus.data, null, 2)}
</div>
)}
</div>
</div>
)}
{/* Provider Filter */}
<div style={{
backgroundColor: 'white',
borderRadius: '12px',
padding: '20px',
marginBottom: '24px',
boxShadow: '0 1px 3px rgba(0,0,0,0.1)'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '16px', marginBottom: '16px' }}>
<span style={{ fontWeight: '600', color: '#1e293b' }}>Provider Filter:</span>
<div style={{ display: 'flex', gap: '8px' }}>
{['all', 'ollama', 'lmstudio'].map(provider => (
<button
key={provider}
onClick={() => setSelectedProvider(provider)}
style={{
padding: '8px 16px',
borderRadius: '6px',
border: '1px solid #e2e8f0',
backgroundColor: selectedProvider === provider ? '#2563eb' : 'white',
color: selectedProvider === provider ? 'white' : '#64748b',
cursor: 'pointer',
fontWeight: selectedProvider === provider ? '600' : '400',
textTransform: 'capitalize'
}}
>
{provider}
</button>
))}
</div>
<button
onClick={loadModels}
disabled={loading}
style={{
marginLeft: 'auto',
padding: '8px 16px',
borderRadius: '6px',
border: '1px solid #e2e8f0',
backgroundColor: 'white',
color: '#64748b',
cursor: loading ? 'not-allowed' : 'pointer',
display: 'flex',
alignItems: 'center',
gap: '8px'
}}
>
<RefreshCw style={{ width: '16px', height: '16px' }} />
Refresh
</button>
</div>
</div>
{/* Models List */}
{loading ? (
<div style={{ textAlign: 'center', padding: '48px' }}>
<Loader2 style={{ width: '48px', height: '48px', color: '#2563eb', animation: 'spin 1s linear infinite', margin: '0 auto' }} />
<p style={{ marginTop: '16px', color: '#64748b' }}>Loading models...</p>
</div>
) : displayModels.length === 0 ? (
<div style={{
backgroundColor: 'white',
borderRadius: '12px',
padding: '48px',
textAlign: 'center',
boxShadow: '0 1px 3px rgba(0,0,0,0.1)'
}}>
<AlertCircle style={{ width: '48px', height: '48px', color: '#94a3b8', margin: '0 auto 16px' }} />
<h3 style={{ fontSize: '20px', fontWeight: '600', color: '#1e293b', marginBottom: '8px' }}>
No Models Found
</h3>
<p style={{ color: '#64748b', marginBottom: '24px' }}>
{selectedProvider === 'all'
? 'No models available. Make sure your LLM providers are running.'
: `No ${selectedProvider} models found. Make sure ${selectedProvider} is running and has models installed.`}
</p>
</div>
) : (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(400px, 1fr))', gap: '20px' }}>
{displayModels.map((model) => {
const isActive = isModelActive(model.id)
const isLoading = loadingModel === model.id
const isUnloading = unloadingModel === model.id
const isPulling = pullingModel === model.id
const provider = model.provider || (selectedProvider === 'all' ? 'ollama' : selectedProvider)
return (
<div
key={model.id}
style={{
backgroundColor: 'white',
borderRadius: '12px',
padding: '20px',
boxShadow: '0 1px 3px rgba(0,0,0,0.1)',
border: isActive ? '2px solid #10b981' : '1px solid #e2e8f0'
}}
>
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', marginBottom: '12px' }}>
<div style={{ flex: 1 }}>
<h3 style={{ fontSize: '18px', fontWeight: '600', color: '#1e293b', margin: '0 0 4px 0' }}>
{model.name || model.id}
</h3>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px' }}>
<span style={{
padding: '4px 8px',
borderRadius: '4px',
backgroundColor: '#eff6ff',
color: '#2563eb',
fontSize: '12px',
fontWeight: '500',
textTransform: 'uppercase'
}}>
{provider}
</span>
{isActive && (
<span style={{
padding: '4px 8px',
borderRadius: '4px',
backgroundColor: '#dcfce7',
color: '#16a34a',
fontSize: '12px',
fontWeight: '500',
display: 'flex',
alignItems: 'center',
gap: '4px'
}}>
<CheckCircle2 style={{ width: '12px', height: '12px' }} />
Active
</span>
)}
</div>
</div>
</div>
{model.description && (
<p style={{ fontSize: '14px', color: '#64748b', marginBottom: '12px' }}>
{model.description}
</p>
)}
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px', marginBottom: '16px' }}>
{model.context_length && (
<span style={{ fontSize: '12px', color: '#64748b' }}>
Context: {model.context_length.toLocaleString()} tokens
</span>
)}
{model.max_tokens && (
<span style={{ fontSize: '12px', color: '#64748b' }}>
Max: {model.max_tokens.toLocaleString()} tokens
</span>
)}
</div>
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
{!isActive ? (
<button
onClick={() => handleLoadModel(model.id, provider)}
disabled={isLoading || isUnloading || isPulling}
style={{
padding: '8px 16px',
borderRadius: '6px',
border: 'none',
backgroundColor: '#2563eb',
color: 'white',
cursor: (isLoading || isUnloading || isPulling) ? 'not-allowed' : 'pointer',
display: 'flex',
alignItems: 'center',
gap: '8px',
fontSize: '14px',
fontWeight: '500',
opacity: (isLoading || isUnloading || isPulling) ? 0.5 : 1
}}
>
{isLoading ? (
<>
<Loader2 style={{ width: '16px', height: '16px', animation: 'spin 1s linear infinite' }} />
Loading...
</>
) : (
<>
<Play style={{ width: '16px', height: '16px' }} />
Load Model
</>
)}
</button>
) : (
<button
onClick={() => handleUnloadModel(model.id, provider)}
disabled={isLoading || isUnloading || isPulling}
style={{
padding: '8px 16px',
borderRadius: '6px',
border: 'none',
backgroundColor: '#ef4444',
color: 'white',
cursor: (isLoading || isUnloading || isPulling) ? 'not-allowed' : 'pointer',
display: 'flex',
alignItems: 'center',
gap: '8px',
fontSize: '14px',
fontWeight: '500',
opacity: (isLoading || isUnloading || isPulling) ? 0.5 : 1
}}
>
{isUnloading ? (
<>
<Loader2 style={{ width: '16px', height: '16px', animation: 'spin 1s linear infinite' }} />
Unloading...
</>
) : (
<>
<Square style={{ width: '16px', height: '16px' }} />
Unload Model
</>
)}
</button>
)}
{provider === 'ollama' && (
<button
onClick={() => handlePullModel(model.id, provider)}
disabled={isLoading || isUnloading || isPulling}
style={{
padding: '8px 16px',
borderRadius: '6px',
border: '1px solid #e2e8f0',
backgroundColor: 'white',
color: '#64748b',
cursor: (isLoading || isUnloading || isPulling) ? 'not-allowed' : 'pointer',
display: 'flex',
alignItems: 'center',
gap: '8px',
fontSize: '14px',
fontWeight: '500',
opacity: (isLoading || isUnloading || isPulling) ? 0.5 : 1
}}
>
{isPulling ? (
<>
<Loader2 style={{ width: '16px', height: '16px', animation: 'spin 1s linear infinite' }} />
Pulling...
</>
) : (
<>
<Download style={{ width: '16px', height: '16px' }} />
Pull Model
</>
)}
</button>
)}
</div>
</div>
)
})}
</div>
)}
<ChatbotModal
isOpen={chatbotOpen}
onClose={() => setChatbotOpen(false)}
defaultModel={selectedModelForChat}
/>
<style>{`
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
`}</style>
</div>
)
}