import React, { useState, useRef, useEffect, useCallback } from 'react'; import ReactDOM from 'react-dom'; import { X, User as UserIcon, Lock, Trash2, AlertTriangle, Activity, RefreshCw, Loader2, } from 'lucide-react'; const overlay = { position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000, }; const modal = { background: 'var(--bg-primary)', borderRadius: 16, padding: 0, width: 560, maxWidth: '95vw', maxHeight: '85vh', overflow: 'hidden', boxShadow: 'var(--shadow-xl)', display: 'flex', flexDirection: 'column', }; const header = { display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '20px 24px', borderBottom: '1px solid var(--border-primary)', }; const tabRow = { display: 'flex', gap: 4, padding: '12px 16px 0', borderBottom: '1px solid var(--border-primary)', flexWrap: 'wrap', }; const tabBtn = (active) => ({ display: 'flex', alignItems: 'center', gap: 8, padding: '10px 14px', background: 'transparent', border: 'none', borderBottom: active ? '2px solid var(--accent-primary)' : '2px solid transparent', color: active ? 'var(--accent-primary)' : 'var(--text-secondary)', cursor: 'pointer', fontSize: 13.5, fontWeight: 500, marginBottom: -1, }); const body = { padding: 24, overflowY: 'auto', flex: 1 }; const label = { display: 'block', fontSize: 13, fontWeight: 600, color: 'var(--text-secondary)', marginBottom: 6 }; const input = { width: '100%', padding: '10px 12px', borderRadius: 8, border: '1px solid var(--border-primary)', background: 'var(--bg-secondary)', color: 'var(--text-primary)', fontSize: 14, boxSizing: 'border-box', }; const primaryBtn = { padding: '10px 16px', background: 'var(--accent-fill, var(--accent-primary))', color: 'var(--accent-on-accent, #fff)', border: 'none', borderRadius: 8, cursor: 'pointer', fontSize: 14, fontWeight: 500, }; const dangerBtn = { padding: '10px 16px', background: '#dc2626', color: '#fff', border: 'none', borderRadius: 8, cursor: 'pointer', fontSize: 14, fontWeight: 500, }; const statusColors = { online: { bg: 'rgba(22,163,74,0.12)', color: '#16a34a', border: 'rgba(22,163,74,0.35)' }, unavailable: { bg: 'rgba(234,179,8,0.12)', color: '#ca8a04', border: 'rgba(234,179,8,0.35)' }, error: { bg: 'rgba(220,38,38,0.1)', color: '#dc2626', border: 'rgba(220,38,38,0.3)' }, }; const SettingsModal = ({ user, authToken, onUserUpdate, onSignOut, onClose, initialTab = 'profile', }) => { const [activeTab, setActiveTab] = useState(initialTab || 'profile'); const mouseDownOnOverlay = useRef(false); const handleOverlayMouseDown = (e) => { mouseDownOnOverlay.current = e.target === e.currentTarget; }; const handleOverlayMouseUp = (e) => { if (mouseDownOnOverlay.current && e.target === e.currentTarget) onClose(); mouseDownOnOverlay.current = false; }; const [firstName, setFirstName] = useState(user?.firstName || ''); const [lastName, setLastName] = useState(user?.lastName || ''); const [email, setEmail] = useState(user?.email || ''); const [currentPassword, setCurrentPassword] = useState(''); const [newPassword, setNewPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); const [deleteConfirmPassword, setDeleteConfirmPassword] = useState(''); const [deleteConfirmText, setDeleteConfirmText] = useState(''); const [message, setMessage] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); const [modelStatus, setModelStatus] = useState(null); const [statusLoading, setStatusLoading] = useState(false); const [statusError, setStatusError] = useState(null); const [currentProvider, setCurrentProvider] = useState(null); const [switchingProvider, setSwitchingProvider] = useState(null); const apiUrl = process.env.REACT_APP_API_URL; const extractError = (data, fallback) => { if (!data) return fallback; if (typeof data.detail === 'string') return data.detail; if (Array.isArray(data.detail) && data.detail[0]?.msg) return data.detail[0].msg; return fallback; }; const fetchModelStatus = useCallback(async (forceRefresh = false) => { setStatusLoading(true); setStatusError(null); try { const qs = forceRefresh ? '?refresh=true' : ''; const response = await fetch(`${apiUrl}/models/status${qs}`); if (!response.ok) { throw new Error(`Status request failed (${response.status})`); } const data = await response.json(); setModelStatus(data); } catch (err) { console.warn('Model status check failed; keeping unfiltered provider list.', err); setStatusError(err.message || 'Could not load model status.'); setModelStatus({ models: [], online_providers: null, check_failed: true, error: err.message || 'Network error', }); } finally { setStatusLoading(false); } }, [apiUrl]); const fetchCurrentProvider = useCallback(async () => { try { const response = await fetch(`${apiUrl}/current-provider`); if (response.ok) { const data = await response.json(); setCurrentProvider(data.current_provider); } } catch { /* provider display is best-effort */ } }, [apiUrl]); const handleProviderSwitch = async (providerId) => { if (providerId === currentProvider || switchingProvider) return; setSwitchingProvider(providerId); setMessage(null); try { const response = await fetch(`${apiUrl}/switch-provider`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ provider: providerId }), }); const data = await response.json().catch(() => null); if (!response.ok) { setMessage({ type: 'error', text: extractError(data, `Could not switch to ${providerId}.`) }); return; } setCurrentProvider(providerId); setMessage({ type: 'success', text: `Advisors now use the ${providerId} provider.` }); } catch { setMessage({ type: 'error', text: 'Network error while switching provider.' }); } finally { setSwitchingProvider(null); } }; useEffect(() => { if (activeTab === 'model-status') { fetchModelStatus(false); fetchCurrentProvider(); } }, [activeTab, fetchModelStatus, fetchCurrentProvider]); useEffect(() => { setActiveTab(initialTab || 'profile'); }, [initialTab]); const handleProfileSubmit = async (e) => { e.preventDefault(); setMessage(null); if (!firstName.trim()) { setMessage({ type: 'error', text: 'First name is required.' }); return; } if (!email.trim()) { setMessage({ type: 'error', text: 'Email is required.' }); return; } setIsSubmitting(true); try { const payload = { firstName: firstName.trim(), lastName: lastName.trim(), }; if (email.trim() !== (user?.email || '')) { payload.email = email.trim(); } const response = await fetch(`${apiUrl}/auth/me`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${authToken}`, }, body: JSON.stringify(payload), }); const data = await response.json().catch(() => null); if (!response.ok) { setMessage({ type: 'error', text: extractError(data, 'Could not update profile.') }); return; } onUserUpdate?.(data); setFirstName(data.firstName || ''); setLastName(data.lastName || ''); setEmail(data.email || ''); setMessage({ type: 'success', text: 'Profile updated.' }); } catch (err) { setMessage({ type: 'error', text: 'Network error. Please try again.' }); } finally { setIsSubmitting(false); } }; const handlePasswordSubmit = async (e) => { e.preventDefault(); setMessage(null); if (newPassword !== confirmPassword) { setMessage({ type: 'error', text: 'New passwords do not match.' }); return; } if (newPassword.length < 8) { setMessage({ type: 'error', text: 'New password must be at least 8 characters.' }); return; } setIsSubmitting(true); try { const response = await fetch(`${apiUrl}/auth/me/password`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${authToken}`, }, body: JSON.stringify({ current_password: currentPassword, new_password: newPassword, }), }); const data = await response.json().catch(() => null); if (!response.ok) { setMessage({ type: 'error', text: extractError(data, 'Could not change password.') }); return; } setCurrentPassword(''); setNewPassword(''); setConfirmPassword(''); setMessage({ type: 'success', text: 'Password changed.' }); } catch (err) { setMessage({ type: 'error', text: 'Network error. Please try again.' }); } finally { setIsSubmitting(false); } }; const handleDeleteAccount = async (e) => { e.preventDefault(); setMessage(null); if (deleteConfirmText !== 'DELETE') { setMessage({ type: 'error', text: 'Type DELETE to confirm.' }); return; } if (!deleteConfirmPassword) { setMessage({ type: 'error', text: 'Password required to delete account.' }); return; } setIsSubmitting(true); try { const response = await fetch(`${apiUrl}/auth/me`, { method: 'DELETE', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${authToken}`, }, body: JSON.stringify({ password: deleteConfirmPassword }), }); const data = await response.json().catch(() => null); if (!response.ok) { setMessage({ type: 'error', text: extractError(data, 'Could not delete account.') }); return; } onClose?.(); onSignOut?.(); } catch (err) { setMessage({ type: 'error', text: 'Network error. Please try again.' }); } finally { setIsSubmitting(false); } }; const messageStyle = (type) => ({ padding: '10px 12px', borderRadius: 8, marginBottom: 16, fontSize: 13, background: type === 'error' ? 'rgba(220,38,38,0.1)' : type === 'success' ? 'rgba(22,163,74,0.1)' : 'var(--bg-secondary)', color: type === 'error' ? '#dc2626' : type === 'success' ? '#16a34a' : 'var(--text-secondary)', border: `1px solid ${ type === 'error' ? 'rgba(220,38,38,0.3)' : type === 'success' ? 'rgba(22,163,74,0.3)' : 'var(--border-primary)' }`, }); const switchTab = (tab) => { setActiveTab(tab); setMessage(null); }; return ReactDOM.createPortal(