mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-18 01:44:38 +08:00
feat: simplify Claw402 autopilot trading flow
This commit is contained in:
@@ -1,6 +1,16 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { User, Cpu, Building2, MessageCircle, Eye, EyeOff, ChevronRight, Plus, Pencil } from 'lucide-react'
|
||||
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'
|
||||
@@ -43,7 +53,9 @@ export function SettingsPage() {
|
||||
|
||||
// Exchanges state
|
||||
const [exchanges, setExchanges] = useState<Exchange[]>([])
|
||||
const [exchangeStates, setExchangeStates] = useState<Record<string, ExchangeAccountState>>({})
|
||||
const [exchangeStates, setExchangeStates] = useState<
|
||||
Record<string, ExchangeAccountState>
|
||||
>({})
|
||||
const [exchangeStatesLoading, setExchangeStatesLoading] = useState(false)
|
||||
const [showExchangeModal, setShowExchangeModal] = useState(false)
|
||||
const [editingExchange, setEditingExchange] = useState<string | null>(null)
|
||||
@@ -84,24 +96,15 @@ export function SettingsPage() {
|
||||
// Fetch data when tabs are visited
|
||||
useEffect(() => {
|
||||
if (activeTab === 'models') {
|
||||
refreshModelConfigs()
|
||||
.catch(() => toast.error('Failed to load AI models'))
|
||||
refreshModelConfigs().catch(() => toast.error('Failed to load AI models'))
|
||||
}
|
||||
if (activeTab === 'exchanges') {
|
||||
refreshExchangeConfigs()
|
||||
.catch(() => toast.error('Failed to load exchanges'))
|
||||
refreshExchangeConfigs().catch(() =>
|
||||
toast.error('Failed to load exchanges')
|
||||
)
|
||||
}
|
||||
}, [activeTab])
|
||||
|
||||
useEffect(() => {
|
||||
const handleRefresh = () => {
|
||||
refreshModelConfigs().catch(() => {})
|
||||
refreshExchangeConfigs().catch(() => {})
|
||||
}
|
||||
window.addEventListener('agent-config-refresh', handleRefresh)
|
||||
return () => window.removeEventListener('agent-config-refresh', handleRefresh)
|
||||
}, [])
|
||||
|
||||
const handleChangePassword = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (newPassword.length < 8) {
|
||||
@@ -125,7 +128,9 @@ export function SettingsPage() {
|
||||
toast.success('Password updated successfully')
|
||||
setNewPassword('')
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : 'Failed to update password')
|
||||
toast.error(
|
||||
err instanceof Error ? err.message : 'Failed to update password'
|
||||
)
|
||||
} finally {
|
||||
setChangingPassword(false)
|
||||
}
|
||||
@@ -141,33 +146,48 @@ export function SettingsPage() {
|
||||
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 }
|
||||
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,
|
||||
apiKey,
|
||||
customApiUrl: customApiUrl || '',
|
||||
customModelName: customModelName || '',
|
||||
enabled: true,
|
||||
}
|
||||
: m
|
||||
)
|
||||
} else {
|
||||
updatedModels = [...configuredModels, {
|
||||
...modelToUpdate,
|
||||
apiKey,
|
||||
customApiUrl: customApiUrl || '',
|
||||
customModelName: customModelName || '',
|
||||
enabled: true,
|
||||
}]
|
||||
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 || '',
|
||||
}])
|
||||
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)
|
||||
@@ -183,16 +203,27 @@ export function SettingsPage() {
|
||||
const handleDeleteModel = async (modelId: string) => {
|
||||
try {
|
||||
const updatedModels = configuredModels.map((m) =>
|
||||
m.id === modelId ? { ...m, apiKey: '', customApiUrl: '', customModelName: '', enabled: false } : 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 || '',
|
||||
}])
|
||||
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)
|
||||
@@ -224,7 +255,11 @@ export function SettingsPage() {
|
||||
) => {
|
||||
try {
|
||||
if (exchangeType === 'hyperliquid') {
|
||||
toast.error(language === 'zh' ? 'Hyperliquid 必须通过钱包授权连接,不能手动填写密钥。' : 'Hyperliquid must be connected through wallet authorization, not manual keys.')
|
||||
toast.error(
|
||||
language === 'zh'
|
||||
? 'Hyperliquid 必须通过钱包授权连接,不能手动填写密钥。'
|
||||
: 'Hyperliquid must be connected through wallet authorization, not manual keys.'
|
||||
)
|
||||
return
|
||||
}
|
||||
if (exchangeId) {
|
||||
@@ -237,6 +272,7 @@ export function SettingsPage() {
|
||||
passphrase: passphrase || '',
|
||||
testnet: testnet || false,
|
||||
hyperliquid_wallet_addr: hyperliquidWalletAddr || '',
|
||||
hyperliquid_unified_account: exchangeType === 'hyperliquid',
|
||||
aster_user: asterUser || '',
|
||||
aster_signer: asterSigner || '',
|
||||
aster_private_key: asterPrivateKey || '',
|
||||
@@ -248,7 +284,7 @@ export function SettingsPage() {
|
||||
},
|
||||
}
|
||||
await api.updateExchangeConfigsEncrypted(request)
|
||||
toast.success('Exchange config updated')
|
||||
toast.success('Exchange config updated')
|
||||
} else {
|
||||
const createRequest = {
|
||||
exchange_type: exchangeType,
|
||||
@@ -259,6 +295,7 @@ export function SettingsPage() {
|
||||
passphrase: passphrase || '',
|
||||
testnet: testnet || false,
|
||||
hyperliquid_wallet_addr: hyperliquidWalletAddr || '',
|
||||
hyperliquid_unified_account: exchangeType === 'hyperliquid',
|
||||
aster_user: asterUser || '',
|
||||
aster_signer: asterSigner || '',
|
||||
aster_private_key: asterPrivateKey || '',
|
||||
@@ -268,7 +305,7 @@ export function SettingsPage() {
|
||||
lighter_api_key_index: lighterApiKeyIndex || 0,
|
||||
}
|
||||
await api.createExchangeEncrypted(createRequest)
|
||||
toast.success('Exchange account created')
|
||||
toast.success('Exchange account created')
|
||||
}
|
||||
await refreshExchangeConfigs()
|
||||
setShowExchangeModal(false)
|
||||
@@ -298,7 +335,10 @@ export function SettingsPage() {
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="min-h-screen pt-20 pb-12 px-4" style={{ background: '#0B0E11' }}>
|
||||
<div
|
||||
className="min-h-screen pt-20 pb-12 px-4"
|
||||
style={{ background: '#0B0E11' }}
|
||||
>
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<h1 className="text-xl font-bold text-white mb-6">Settings</h1>
|
||||
|
||||
@@ -309,9 +349,10 @@ export function SettingsPage() {
|
||||
key={tab.key}
|
||||
onClick={() => setActiveTab(tab.key)}
|
||||
className={`flex-1 flex items-center justify-center gap-2 px-3 py-2 rounded-lg text-sm font-medium transition-all
|
||||
${activeTab === tab.key
|
||||
? 'bg-nofx-gold text-black'
|
||||
: 'text-zinc-400 hover:text-white'
|
||||
${
|
||||
activeTab === tab.key
|
||||
? 'bg-nofx-gold text-black'
|
||||
: 'text-zinc-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
{tab.icon}
|
||||
@@ -322,7 +363,6 @@ export function SettingsPage() {
|
||||
|
||||
{/* Tab Content */}
|
||||
<div className="bg-zinc-900/60 backdrop-blur-xl border border-zinc-800/80 rounded-2xl p-6">
|
||||
|
||||
{/* Account Tab */}
|
||||
{activeTab === 'account' && (
|
||||
<div className="space-y-6">
|
||||
@@ -332,10 +372,14 @@ export function SettingsPage() {
|
||||
</div>
|
||||
|
||||
<div className="border-t border-zinc-800 pt-6">
|
||||
<h3 className="text-sm font-semibold text-white mb-4">Change Password</h3>
|
||||
<h3 className="text-sm font-semibold text-white mb-4">
|
||||
Change Password
|
||||
</h3>
|
||||
<form onSubmit={handleChangePassword} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-zinc-400 mb-2">New Password</label>
|
||||
<label className="block text-xs font-medium text-zinc-400 mb-2">
|
||||
New Password
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
@@ -350,7 +394,11 @@ export function SettingsPage() {
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-3.5 top-1/2 -translate-y-1/2 text-zinc-500 hover:text-zinc-300 transition-colors"
|
||||
>
|
||||
{showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||
{showPassword ? (
|
||||
<EyeOff size={16} />
|
||||
) : (
|
||||
<Eye size={16} />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -371,10 +419,14 @@ export function SettingsPage() {
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm text-zinc-400">
|
||||
{configuredModels.length} model{configuredModels.length !== 1 ? 's' : ''} configured
|
||||
{configuredModels.length} model
|
||||
{configuredModels.length !== 1 ? 's' : ''} configured
|
||||
</p>
|
||||
<button
|
||||
onClick={() => { setEditingModel(null); setShowModelModal(true) }}
|
||||
onClick={() => {
|
||||
setEditingModel(null)
|
||||
setShowModelModal(true)
|
||||
}}
|
||||
className="flex items-center gap-1.5 text-xs font-medium bg-nofx-gold/10 hover:bg-nofx-gold/20 text-nofx-gold px-3 py-1.5 rounded-lg transition-colors"
|
||||
>
|
||||
<Plus size={14} />
|
||||
@@ -391,7 +443,10 @@ export function SettingsPage() {
|
||||
{configuredModels.map((model) => (
|
||||
<button
|
||||
key={model.id}
|
||||
onClick={() => { setEditingModel(model.id); setShowModelModal(true) }}
|
||||
onClick={() => {
|
||||
setEditingModel(model.id)
|
||||
setShowModelModal(true)
|
||||
}}
|
||||
className="w-full flex items-center justify-between px-4 py-3 rounded-xl bg-zinc-800/50 hover:bg-zinc-800 border border-zinc-700/50 transition-colors group"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -399,20 +454,33 @@ export function SettingsPage() {
|
||||
<Cpu size={14} className="text-zinc-300" />
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<p className="text-sm font-medium text-white">{model.name}</p>
|
||||
<p className="text-sm font-medium text-white">
|
||||
{model.name}
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center gap-1.5 mt-1">
|
||||
<p className="text-xs text-zinc-500">{model.provider}</p>
|
||||
<p className="text-xs text-zinc-500">
|
||||
{model.provider}
|
||||
</p>
|
||||
{configBadge('API Key', !!model.has_api_key)}
|
||||
{model.customModelName ? configBadge('Custom Model', true) : null}
|
||||
{model.customApiUrl ? configBadge('Base URL', true) : null}
|
||||
{model.customModelName
|
||||
? configBadge('Custom Model', true)
|
||||
: null}
|
||||
{model.customApiUrl
|
||||
? configBadge('Base URL', true)
|
||||
: null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`text-xs px-2 py-0.5 rounded-full ${model.enabled ? 'bg-emerald-500/10 text-emerald-400' : 'bg-zinc-700 text-zinc-500'}`}>
|
||||
<span
|
||||
className={`text-xs px-2 py-0.5 rounded-full ${model.enabled ? 'bg-emerald-500/10 text-emerald-400' : 'bg-zinc-700 text-zinc-500'}`}
|
||||
>
|
||||
{model.enabled ? 'Active' : 'Inactive'}
|
||||
</span>
|
||||
<Pencil size={14} className="text-zinc-600 group-hover:text-zinc-400 transition-colors" />
|
||||
<Pencil
|
||||
size={14}
|
||||
className="text-zinc-600 group-hover:text-zinc-400 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
@@ -426,7 +494,8 @@ export function SettingsPage() {
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm text-zinc-400">
|
||||
{exchanges.length} account{exchanges.length !== 1 ? 's' : ''} connected
|
||||
{exchanges.length} account{exchanges.length !== 1 ? 's' : ''}{' '}
|
||||
connected
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
@@ -437,7 +506,10 @@ export function SettingsPage() {
|
||||
{exchangeStatesLoading ? 'Refreshing…' : 'Refresh Balances'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setEditingExchange(null); setShowExchangeModal(true) }}
|
||||
onClick={() => {
|
||||
setEditingExchange(null)
|
||||
setShowExchangeModal(true)
|
||||
}}
|
||||
className="flex items-center gap-1.5 text-xs font-medium bg-nofx-gold/10 hover:bg-nofx-gold/20 text-nofx-gold px-3 py-1.5 rounded-lg transition-colors"
|
||||
>
|
||||
<Plus size={14} />
|
||||
@@ -455,48 +527,78 @@ export function SettingsPage() {
|
||||
{exchanges.map((exchange) => {
|
||||
const accountState = exchangeStates[exchange.id]
|
||||
return (
|
||||
<button
|
||||
key={exchange.id}
|
||||
onClick={() => { setEditingExchange(exchange.id); setShowExchangeModal(true) }}
|
||||
className="w-full flex items-center justify-between px-4 py-3 rounded-xl bg-zinc-800/50 hover:bg-zinc-800 border border-zinc-700/50 transition-colors group"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-lg bg-zinc-700 flex items-center justify-center">
|
||||
<Building2 size={14} className="text-zinc-300" />
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<p className="text-sm font-medium text-white">{exchange.account_name || exchange.name}</p>
|
||||
<div className="flex flex-wrap items-center gap-1.5 mt-1">
|
||||
<p className="text-xs text-zinc-500 capitalize">{exchange.exchange_type || exchange.type}</p>
|
||||
{configBadge('API Key', !!exchange.has_api_key)}
|
||||
{configBadge('Secret', !!exchange.has_secret_key)}
|
||||
{exchange.has_passphrase ? configBadge('Passphrase', true) : null}
|
||||
{exchange.hyperliquidWalletAddr ? configBadge('Wallet', true) : null}
|
||||
{exchange.has_aster_private_key ? configBadge('Aster Key', true) : null}
|
||||
{exchange.has_lighter_private_key || exchange.has_lighter_api_key_private_key ? configBadge('Lighter Key', true) : null}
|
||||
<button
|
||||
key={exchange.id}
|
||||
onClick={() => {
|
||||
setEditingExchange(exchange.id)
|
||||
setShowExchangeModal(true)
|
||||
}}
|
||||
className="w-full flex items-center justify-between px-4 py-3 rounded-xl bg-zinc-800/50 hover:bg-zinc-800 border border-zinc-700/50 transition-colors group"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-lg bg-zinc-700 flex items-center justify-center">
|
||||
<Building2 size={14} className="text-zinc-300" />
|
||||
</div>
|
||||
{accountState && (
|
||||
<div className="flex flex-wrap items-center gap-2 mt-2 text-xs">
|
||||
{accountState.status === 'ok' ? (
|
||||
<>
|
||||
<span className="rounded-full bg-emerald-500/10 px-2 py-0.5 font-mono text-emerald-300">
|
||||
Balance {accountState.display_balance || `${accountState.total_equity?.toFixed(2) ?? '--'} ${accountState.asset || ''}`}
|
||||
</span>
|
||||
{typeof accountState.available_balance === 'number' && (
|
||||
<span className="text-zinc-500">Available {accountState.available_balance.toFixed(2)} {accountState.asset || ''}</span>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<span className="rounded-full bg-amber-500/10 px-2 py-0.5 text-amber-300">
|
||||
Balance unavailable: {accountState.error_message || accountState.status}
|
||||
</span>
|
||||
)}
|
||||
<div className="text-left">
|
||||
<p className="text-sm font-medium text-white">
|
||||
{exchange.account_name || exchange.name}
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center gap-1.5 mt-1">
|
||||
<p className="text-xs text-zinc-500 capitalize">
|
||||
{exchange.exchange_type || exchange.type}
|
||||
</p>
|
||||
{configBadge('API Key', !!exchange.has_api_key)}
|
||||
{configBadge('Secret', !!exchange.has_secret_key)}
|
||||
{exchange.has_passphrase
|
||||
? configBadge('Passphrase', true)
|
||||
: null}
|
||||
{exchange.hyperliquidWalletAddr
|
||||
? configBadge('Wallet', true)
|
||||
: null}
|
||||
{exchange.has_aster_private_key
|
||||
? configBadge('Aster Key', true)
|
||||
: null}
|
||||
{exchange.has_lighter_private_key ||
|
||||
exchange.has_lighter_api_key_private_key
|
||||
? configBadge('Lighter Key', true)
|
||||
: null}
|
||||
</div>
|
||||
)}
|
||||
{accountState && (
|
||||
<div className="flex flex-wrap items-center gap-2 mt-2 text-xs">
|
||||
{accountState.status === 'ok' ? (
|
||||
<>
|
||||
<span className="rounded-full bg-emerald-500/10 px-2 py-0.5 font-mono text-emerald-300">
|
||||
Balance{' '}
|
||||
{accountState.display_balance ||
|
||||
`${accountState.total_equity?.toFixed(2) ?? '--'} ${accountState.asset || ''}`}
|
||||
</span>
|
||||
{typeof accountState.available_balance ===
|
||||
'number' && (
|
||||
<span className="text-zinc-500">
|
||||
Available{' '}
|
||||
{accountState.available_balance.toFixed(
|
||||
2
|
||||
)}{' '}
|
||||
{accountState.asset || ''}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<span className="rounded-full bg-amber-500/10 px-2 py-0.5 text-amber-300">
|
||||
Balance unavailable:{' '}
|
||||
{accountState.error_message ||
|
||||
accountState.status}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronRight size={14} className="text-zinc-600 group-hover:text-zinc-400 transition-colors" />
|
||||
</button>
|
||||
<ChevronRight
|
||||
size={14}
|
||||
className="text-zinc-600 group-hover:text-zinc-400 transition-colors"
|
||||
/>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
@@ -508,7 +610,8 @@ export function SettingsPage() {
|
||||
{activeTab === 'telegram' && (
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-zinc-400">
|
||||
Connect a Telegram bot to receive trading notifications and interact with your traders.
|
||||
Connect a Telegram bot to receive trading notifications and
|
||||
interact with your traders.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => setShowTelegramModal(true)}
|
||||
@@ -518,9 +621,14 @@ export function SettingsPage() {
|
||||
<div className="w-8 h-8 rounded-lg bg-[#0088cc]/20 flex items-center justify-center">
|
||||
<MessageCircle size={14} className="text-[#0088cc]" />
|
||||
</div>
|
||||
<span className="text-sm font-medium text-white">Configure Telegram Bot</span>
|
||||
<span className="text-sm font-medium text-white">
|
||||
Configure Telegram Bot
|
||||
</span>
|
||||
</div>
|
||||
<ChevronRight size={14} className="text-zinc-600 group-hover:text-zinc-400 transition-colors" />
|
||||
<ChevronRight
|
||||
size={14}
|
||||
className="text-zinc-600 group-hover:text-zinc-400 transition-colors"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -536,7 +644,10 @@ export function SettingsPage() {
|
||||
editingModelId={editingModel}
|
||||
onSave={handleSaveModel}
|
||||
onDelete={handleDeleteModel}
|
||||
onClose={() => { setShowModelModal(false); setEditingModel(null) }}
|
||||
onClose={() => {
|
||||
setShowModelModal(false)
|
||||
setEditingModel(null)
|
||||
}}
|
||||
language={language}
|
||||
/>
|
||||
</div>
|
||||
@@ -550,7 +661,10 @@ export function SettingsPage() {
|
||||
editingExchangeId={editingExchange}
|
||||
onSave={handleSaveExchange}
|
||||
onDelete={handleDeleteExchange}
|
||||
onClose={() => { setShowExchangeModal(false); setEditingExchange(null) }}
|
||||
onClose={() => {
|
||||
setShowExchangeModal(false)
|
||||
setEditingExchange(null)
|
||||
}}
|
||||
language={language}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user