import { useState, useEffect } from 'react' import { toast } from 'sonner' import { User, Cpu, Building2, MessageCircle, Eye, EyeOff, ChevronRight, Plus, Pencil, } from 'lucide-react' import { useAuth } from '../contexts/AuthContext' import { useLanguage } from '../contexts/LanguageContext' import { api } from '../lib/api' import { ExchangeConfigModal } from '../components/trader/ExchangeConfigModal' import { TelegramConfigModal } from '../components/trader/TelegramConfigModal' import { ModelConfigModal } from '../components/trader/ModelConfigModal' import type { Exchange, AIModel, ExchangeAccountState } from '../types' type Tab = 'account' | 'models' | 'exchanges' | 'telegram' function configBadge(label: string, active: boolean) { return ( {label} ) } export function SettingsPage() { const { user } = useAuth() const { language } = useLanguage() const [activeTab, setActiveTab] = useState('account') // Account state const [newPassword, setNewPassword] = useState('') const [showPassword, setShowPassword] = useState(false) const [changingPassword, setChangingPassword] = useState(false) // AI Models state const [configuredModels, setConfiguredModels] = useState([]) const [supportedModels, setSupportedModels] = useState([]) const [showModelModal, setShowModelModal] = useState(false) const [editingModel, setEditingModel] = useState(null) // Exchanges state const [exchanges, setExchanges] = useState([]) const [exchangeStates, setExchangeStates] = useState< Record >({}) const [exchangeStatesLoading, setExchangeStatesLoading] = useState(false) const [showExchangeModal, setShowExchangeModal] = useState(false) const [editingExchange, setEditingExchange] = useState(null) // Telegram state const [showTelegramModal, setShowTelegramModal] = useState(false) const refreshModelConfigs = async () => { const [configs, supported] = await Promise.all([ api.getModelConfigs(), api.getSupportedModels(), ]) setConfiguredModels(configs) setSupportedModels(supported) } const refreshExchangeConfigs = async () => { const [refreshed, accountStateResponse] = await Promise.all([ api.getExchangeConfigs(), api.getExchangeAccountState().catch(() => ({ states: {} })), ]) setExchanges(refreshed) setExchangeStates(accountStateResponse.states || {}) } const refreshExchangeStates = async () => { setExchangeStatesLoading(true) try { const response = await api.getExchangeAccountState() setExchangeStates(response.states || {}) } catch { toast.error('Failed to load exchange balances') } finally { setExchangeStatesLoading(false) } } // Fetch data when tabs are visited useEffect(() => { if (activeTab === 'models') { refreshModelConfigs().catch(() => toast.error('Failed to load AI models')) } if (activeTab === 'exchanges') { refreshExchangeConfigs().catch(() => toast.error('Failed to load exchanges') ) } }, [activeTab]) const handleChangePassword = async (e: React.FormEvent) => { e.preventDefault() if (newPassword.length < 8) { toast.error('Password must be at least 8 characters') return } setChangingPassword(true) try { const res = await fetch('/api/user/password', { method: 'PUT', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('auth_token') || ''}`, }, body: JSON.stringify({ new_password: newPassword }), }) if (!res.ok) { const data = await res.json().catch(() => ({})) throw new Error(data.error || 'Failed to update password') } toast.success('Password updated successfully') setNewPassword('') } catch (err) { toast.error( err instanceof Error ? err.message : 'Failed to update password' ) } finally { setChangingPassword(false) } } const handleSaveModel = async ( modelId: string, apiKey: string, customApiUrl?: string, customModelName?: string ) => { try { const existingModel = configuredModels.find((m) => m.id === modelId) const modelTemplate = supportedModels.find((m) => m.id === modelId) const modelToUpdate = existingModel || modelTemplate if (!modelToUpdate) { toast.error('Model not found') return } let updatedModels: AIModel[] if (existingModel) { updatedModels = configuredModels.map((m) => m.id === modelId ? { ...m, apiKey, customApiUrl: customApiUrl || '', customModelName: customModelName || '', enabled: true, } : m ) } else { updatedModels = [ ...configuredModels, { ...modelToUpdate, apiKey, customApiUrl: customApiUrl || '', customModelName: customModelName || '', enabled: true, }, ] } const request = { models: Object.fromEntries( updatedModels.map((m) => [ m.provider, { enabled: m.enabled, api_key: m.apiKey || '', custom_api_url: m.customApiUrl || '', custom_model_name: m.customModelName || '', }, ]) ), } await api.updateModelConfigs(request) toast.success('Model config saved') await refreshModelConfigs() setShowModelModal(false) setEditingModel(null) } catch { toast.error('Failed to save model config') } } const handleDeleteModel = async (modelId: string) => { try { const updatedModels = configuredModels.map((m) => m.id === modelId ? { ...m, apiKey: '', customApiUrl: '', customModelName: '', enabled: false, } : m ) const request = { models: Object.fromEntries( updatedModels.map((m) => [ m.provider, { enabled: m.enabled, api_key: m.apiKey || '', custom_api_url: m.customApiUrl || '', custom_model_name: m.customModelName || '', }, ]) ), } await api.updateModelConfigs(request) await refreshModelConfigs() setShowModelModal(false) setEditingModel(null) toast.success('Model config removed') } catch { toast.error('Failed to remove model config') } } const handleSaveExchange = async ( exchangeId: string | null, exchangeType: string, accountName: string, apiKey: string, secretKey?: string, passphrase?: string, testnet?: boolean, hyperliquidWalletAddr?: string, asterUser?: string, asterSigner?: string, asterPrivateKey?: string, lighterWalletAddr?: string, lighterPrivateKey?: string, lighterApiKeyPrivateKey?: string, lighterApiKeyIndex?: number ) => { try { if (exchangeType === 'hyperliquid') { toast.error( language === 'zh' ? 'Hyperliquid must be connected through wallet authorization, not manual keys.' : 'Hyperliquid must be connected through wallet authorization, not manual keys.' ) return } if (exchangeId) { const request = { exchanges: { [exchangeId]: { enabled: true, api_key: apiKey || '', secret_key: secretKey || '', passphrase: passphrase || '', testnet: testnet || false, hyperliquid_wallet_addr: hyperliquidWalletAddr || '', hyperliquid_unified_account: exchangeType === 'hyperliquid', aster_user: asterUser || '', aster_signer: asterSigner || '', aster_private_key: asterPrivateKey || '', lighter_wallet_addr: lighterWalletAddr || '', lighter_private_key: lighterPrivateKey || '', lighter_api_key_private_key: lighterApiKeyPrivateKey || '', lighter_api_key_index: lighterApiKeyIndex || 0, }, }, } await api.updateExchangeConfigsEncrypted(request) toast.success('Exchange config updated') } else { const createRequest = { exchange_type: exchangeType, account_name: accountName, enabled: true, api_key: apiKey || '', secret_key: secretKey || '', passphrase: passphrase || '', testnet: testnet || false, hyperliquid_wallet_addr: hyperliquidWalletAddr || '', hyperliquid_unified_account: exchangeType === 'hyperliquid', aster_user: asterUser || '', aster_signer: asterSigner || '', aster_private_key: asterPrivateKey || '', lighter_wallet_addr: lighterWalletAddr || '', lighter_private_key: lighterPrivateKey || '', lighter_api_key_private_key: lighterApiKeyPrivateKey || '', lighter_api_key_index: lighterApiKeyIndex || 0, } await api.createExchangeEncrypted(createRequest) toast.success('Exchange account created') } await refreshExchangeConfigs() setShowExchangeModal(false) setEditingExchange(null) } catch { toast.error('Failed to save exchange config') } } const handleDeleteExchange = async (exchangeId: string) => { try { await api.deleteExchange(exchangeId) toast.success('Exchange account deleted') await refreshExchangeConfigs() setShowExchangeModal(false) setEditingExchange(null) } catch { toast.error('Failed to delete exchange account') } } const tabs: { key: Tab; label: string; icon: React.ReactNode }[] = [ { key: 'account', label: 'Account', icon: }, { key: 'models', label: 'AI Models', icon: }, { key: 'exchanges', label: 'Exchanges', icon: }, { key: 'telegram', label: 'Telegram', icon: }, ] return (

Settings

{/* Tabs */}
{tabs.map((tab) => ( ))}
{/* Tab Content */}
{/* Account Tab */} {activeTab === 'account' && (

Email

{user?.email}

Change Password

setNewPassword(e.target.value)} className="w-full bg-nofx-bg-deeper border border-[rgba(26,24,19,0.14)] rounded-xl px-4 py-3 pr-11 text-sm text-nofx-text placeholder-nofx-text-muted focus:outline-none focus:border-nofx-gold/60 focus:ring-1 focus:ring-nofx-gold/30 transition-all" placeholder="At least 8 characters" required />
)} {/* AI Models Tab */} {activeTab === 'models' && (

{configuredModels.length} model {configuredModels.length !== 1 ? 's' : ''} configured

{configuredModels.length === 0 ? (
No AI models configured yet
) : (
{configuredModels.map((model) => ( ))}
)}
)} {/* Exchanges Tab */} {activeTab === 'exchanges' && (

{exchanges.length} account{exchanges.length !== 1 ? 's' : ''}{' '} connected

{exchanges.length === 0 ? (
No exchange accounts connected yet
) : (
{exchanges.map((exchange) => { const accountState = exchangeStates[exchange.id] return ( ) })}
)}
)} {/* Telegram Tab */} {activeTab === 'telegram' && (

Connect a Telegram bot to receive trading notifications and interact with your traders.

)}
{/* AI Model Modal */} {showModelModal && (
{ setShowModelModal(false) setEditingModel(null) }} language={language} />
)} {/* Exchange Modal */} {showExchangeModal && (
{ setShowExchangeModal(false) setEditingExchange(null) }} language={language} />
)} {/* Telegram Modal */} {showTelegramModal && (
setShowTelegramModal(false)} language={language} />
)}
) }