import { useEffect, useState } from 'react'; import { IconAlertCircle, IconDownload, IconPalette, IconRobot, IconTrash, IconUser, IconX, } from '@tabler/icons-react'; import { ActionIcon, Alert, Badge, Button, Card, ColorSwatch, Combobox, Divider, Group, Input, InputBase, Loader, Modal, NavLink, PasswordInput, rem, ScrollArea, Stack, Text, TextInput, Title, useCombobox, useMantineTheme, } from '@mantine/core'; import { deleteModel, getInstalledModels, pullModel, type OllamaModel } from '@/app/actions/ollama'; const POPULAR_MODELS = [ 'llama3.2', 'llama3.1', 'mistral', 'gemma2', 'qwen2.5', 'phi3.5', 'neural-chat', 'starling-lm', 'codellama', 'deepseek-coder', 'llava', ]; interface User { id: string; username: string; } interface SettingsModalProps { opened: boolean; close: () => void; primaryColor: string; setPrimaryColor: (color: string) => void; } export function SettingsModal({ opened, close, primaryColor, setPrimaryColor, }: SettingsModalProps) { const theme = useMantineTheme(); const [activeTab, setActiveTab] = useState<'appearance' | 'account' | 'models'>('appearance'); // Account State const [user, setUser] = useState(null); const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [isLoginMode, setIsLoginMode] = useState(true); const [error, setError] = useState(''); const [loading, setLoading] = useState(false); // Models State const [models, setModels] = useState([]); const [loadingModels, setLoadingModels] = useState(false); const [pullingModel, setPullingModel] = useState(null); const [newModelName, setNewModelName] = useState(''); // Combobox State const [search, setSearch] = useState(''); const combobox = useCombobox({ onDropdownClose: () => { combobox.resetSelectedOption(); combobox.focusTarget(); setSearch(''); }, onDropdownOpen: () => { combobox.focusSearchInput(); }, }); const [value, setValue] = useState(null); // Filter installed models based on search const options = models .filter((item) => item.name.toLowerCase().includes(search.toLowerCase().trim())) .map((item) => ( {item.name} )); // Check login status on mount useEffect(() => { if (opened) { fetchUser(); if (activeTab === 'models') { fetchModels(); } } }, [opened, activeTab]); const fetchUser = async () => { try { const res = await fetch('/api/auth/me'); const data = await res.json(); if (data.user) { setUser(data.user); } else { setUser(null); } } catch (e) { console.error(e); } }; const fetchModels = async () => { setLoadingModels(true); try { const list = await getInstalledModels(); setModels(list); } catch (e) { console.error(e); } finally { setLoadingModels(false); } }; const handlePullModel = async () => { if (!newModelName) return; setPullingModel(newModelName); try { const result = await pullModel(newModelName); if (result.success) { setNewModelName(''); await fetchModels(); } else { setError(result.message); } } catch (e) { console.error(e); } finally { setPullingModel(null); } }; const handleDeleteModel = async (name: string) => { if (!confirm(`Are you sure you want to delete ${name}?`)) return; try { await deleteModel(name); await fetchModels(); } catch (e) { console.error(e); } }; const handleAuth = async () => { setError(''); setLoading(true); const endpoint = isLoginMode ? '/api/auth/login' : '/api/auth/register'; try { const res = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }), }); const data = await res.json(); if (!res.ok) { throw new Error(data.error || 'Something went wrong'); } // Refresh user state await fetchUser(); setUsername(''); setPassword(''); } catch (err: any) { setError(err.message); } finally { setLoading(false); } }; const handleLogout = async () => { await fetch('/api/auth/logout', { method: 'POST' }); setUser(null); }; const colors = Object.keys(theme.colors).filter( (color) => color !== 'dark' && color !== 'gray' && color !== 'white' && color !== 'black' ); return ( {/* Left Sidebar */} } variant="light" color={primaryColor} onClick={() => setActiveTab('appearance')} style={{ borderRadius: 'var(--mantine-radius-lg)' }} /> } variant="light" color={primaryColor} onClick={() => setActiveTab('models')} style={{ borderRadius: 'var(--mantine-radius-lg)' }} /> } variant="light" color={primaryColor} onClick={() => setActiveTab('account')} style={{ borderRadius: 'var(--mantine-radius-lg)' }} /> {/* Right Content */} {activeTab === 'appearance' && ( <> Appearance Customize the look and feel of the application. Accent Color {colors.map((color) => ( setPrimaryColor(color)} style={{ color: '#fff', cursor: 'pointer' }} withShadow > {primaryColor === color && } ))} )} {activeTab === 'models' && ( <> Models Manage your local AI models via Ollama. { setNewModelName(val); combobox.closeDropdown(); // Optional: trigger pull immediately or let user click button? // User code sample sets value. I'll set newModelName. }} > } onClick={() => combobox.toggleDropdown()} rightSectionPointerEvents="none" label="Download Model" description="Select an installed model to update, or type a new model name (e.g. llama3)" style={{ flex: 1 }} > {newModelName || ( Pick or type model name )} { setSearch(event.currentTarget.value); setNewModelName(event.currentTarget.value); // Allow typing new names }} placeholder="Search installed models or type new one" /> {options.length > 0 ? ( options ) : ( No matching installed models )} {pullingModel && ( } title="Downloading..." color="blue" mt="md"> Pulling {pullingModel}. This may take a while depending on your connection. )} Installed Models {loadingModels ? ( ) : models.length === 0 ? ( No models found. Try pulling one! ) : ( {models.map((model) => (
{model.name} {(model.size / 1024 / 1024 / 1024).toFixed(2)} GB {model.details.parameter_size} {model.details.quantization_level}
handleDeleteModel(model.name)} >
))}
)} )} {activeTab === 'account' && ( <> Account Manage your account and chat history. {user ? ( Logged in as {user.username} ) : ( {error && ( } title="Error" color="red"> {error} )} setUsername(e.target.value)} /> setPassword(e.target.value)} /> setIsLoginMode(!isLoginMode)} > {isLoginMode ? 'Need an account? Register' : 'Have an account? Login'} )} )}
); }