// ======================================== // Models Tab Component // ======================================== // Model management tab with list, search, and download actions import { useState, useMemo } from 'react'; import { useIntl } from 'react-intl'; import { Search, RefreshCw, Package, Filter, AlertCircle, } from 'lucide-react'; import { Card } from '@/components/ui/Card'; import { Button } from '@/components/ui/Button'; import { Input } from '@/components/ui/Input'; import { Badge } from '@/components/ui/Badge'; import { ModelCard, CustomModelInput } from './ModelCard'; import { useCodexLensModels, useCodexLensMutations } from '@/hooks'; import type { CodexLensModel } from '@/lib/api'; import { cn } from '@/lib/utils'; // ========== Types ========== type FilterType = 'all' | 'embedding' | 'reranker' | 'downloaded' | 'available'; // ========== Helper Functions ========== function filterModels(models: CodexLensModel[], filter: FilterType, search: string): CodexLensModel[] { let filtered = models; // Apply type/status filter if (filter === 'embedding') { filtered = filtered.filter(m => m.type === 'embedding'); } else if (filter === 'reranker') { filtered = filtered.filter(m => m.type === 'reranker'); } else if (filter === 'downloaded') { filtered = filtered.filter(m => m.installed); } else if (filter === 'available') { filtered = filtered.filter(m => !m.installed); } // Apply search filter if (search.trim()) { const query = search.toLowerCase(); filtered = filtered.filter(m => m.name.toLowerCase().includes(query) || m.profile.toLowerCase().includes(query) || (m.description?.toLowerCase().includes(query) ?? false) ); } return filtered; } // ========== Component ========== export interface ModelsTabProps { installed?: boolean; } export function ModelsTab({ installed = false }: ModelsTabProps) { const { formatMessage } = useIntl(); const [searchQuery, setSearchQuery] = useState(''); const [filterType, setFilterType] = useState('all'); const [downloadingProfile, setDownloadingProfile] = useState(null); const [downloadProgress, setDownloadProgress] = useState(0); const { models, isLoading, error, refetch, } = useCodexLensModels({ enabled: installed, }); const { downloadModel, downloadCustomModel, deleteModel, isDownloading, isDeleting, } = useCodexLensMutations(); // Filter models based on search and filter const filteredModels = useMemo(() => { if (!models) return []; return filterModels(models, filterType, searchQuery); }, [models, filterType, searchQuery]); // Count models by type and status const stats = useMemo(() => { if (!models) return null; return { total: models.length, embedding: models.filter(m => m.type === 'embedding').length, reranker: models.filter(m => m.type === 'reranker').length, downloaded: models.filter(m => m.installed).length, available: models.filter(m => !m.installed).length, }; }, [models]); // Handle model download const handleDownload = async (profile: string) => { setDownloadingProfile(profile); setDownloadProgress(0); // Simulate progress for demo (in real implementation, use WebSocket or polling) const progressInterval = setInterval(() => { setDownloadProgress(prev => { if (prev >= 95) { clearInterval(progressInterval); return 95; } return prev + 5; }); }, 500); try { const result = await downloadModel(profile); if (result.success) { setDownloadProgress(100); setTimeout(() => { setDownloadingProfile(null); setDownloadProgress(0); refetch(); }, 500); } else { setDownloadingProfile(null); setDownloadProgress(0); } } catch (error) { setDownloadingProfile(null); setDownloadProgress(0); } finally { clearInterval(progressInterval); } }; // Handle custom model download const handleCustomDownload = async (modelName: string, modelType: 'embedding' | 'reranker') => { try { const result = await downloadCustomModel(modelName, modelType); if (result.success) { refetch(); } } catch (error) { console.error('Failed to download custom model:', error); } }; // Handle model delete const handleDelete = async (profile: string) => { const result = await deleteModel(profile); if (result.success) { refetch(); } }; // Filter buttons const filterButtons: Array<{ type: FilterType; label: string; count: number | undefined }> = [ { type: 'all', label: formatMessage({ id: 'codexlens.models.filters.all' }), count: stats?.total }, { type: 'embedding', label: formatMessage({ id: 'codexlens.models.types.embedding' }), count: stats?.embedding }, { type: 'reranker', label: formatMessage({ id: 'codexlens.models.types.reranker' }), count: stats?.reranker }, { type: 'downloaded', label: formatMessage({ id: 'codexlens.models.status.downloaded' }), count: stats?.downloaded }, { type: 'available', label: formatMessage({ id: 'codexlens.models.status.available' }), count: stats?.available }, ]; if (!installed) { return (

{formatMessage({ id: 'codexlens.models.notInstalled.title' })}

{formatMessage({ id: 'codexlens.models.notInstalled.description' })}

); } return (
{/* Header with Search and Actions */}
setSearchQuery(e.target.value)} className="pl-9" />
{/* Stats and Filters */}
{formatMessage({ id: 'codexlens.models.filters.label' })}
{filterButtons.map(({ type, label, count }) => ( ))}
{/* Custom Model Input */} {/* Model List */} {error ? (

{formatMessage({ id: 'codexlens.models.error.title' })}

{error.message || formatMessage({ id: 'codexlens.models.error.description' })}

) : isLoading ? (

{formatMessage({ id: 'common.actions.loading' })}

) : filteredModels.length === 0 ? (

{models && models.length > 0 ? formatMessage({ id: 'codexlens.models.empty.filtered' }) : formatMessage({ id: 'codexlens.models.empty.title' }) }

{models && models.length > 0 ? formatMessage({ id: 'codexlens.models.empty.filteredDesc' }) : formatMessage({ id: 'codexlens.models.empty.description' }) }

) : (
{filteredModels.map((model) => ( { setDownloadingProfile(null); setDownloadProgress(0); }} /> ))}
)}
); } export default ModelsTab;