merge: resolve conflicts from origin/main into dev

All conflicts were in frontend files where main had beginner-mode features
(BeginnerGuideCards, Claw402 balance alerts, mode switcher, actionable error
helpers) that dev intentionally simplified. Kept dev's version in every case.
Removed unused navigate import in SettingsPage after conflict resolution.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
shinchan-zhai
2026-04-26 00:13:31 +08:00
3 changed files with 160 additions and 64 deletions
+31 -16
View File
@@ -45,8 +45,12 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
const [allModels, setAllModels] = useState<AIModel[]>([])
const [allExchanges, setAllExchanges] = useState<Exchange[]>([])
const [supportedModels, setSupportedModels] = useState<AIModel[]>([])
const [visibleTraderAddresses, setVisibleTraderAddresses] = useState<Set<string>>(new Set())
const [visibleExchangeAddresses, setVisibleExchangeAddresses] = useState<Set<string>>(new Set())
const [visibleTraderAddresses, setVisibleTraderAddresses] = useState<
Set<string>
>(new Set())
const [visibleExchangeAddresses, setVisibleExchangeAddresses] = useState<
Set<string>
>(new Set())
const [copiedId, setCopiedId] = useState<string | null>(null)
const loadConfigs = async () => {
@@ -72,7 +76,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
// Toggle wallet address visibility for a trader
const toggleTraderAddressVisibility = (traderId: string) => {
setVisibleTraderAddresses(prev => {
setVisibleTraderAddresses((prev) => {
const next = new Set(prev)
if (next.has(traderId)) {
next.delete(traderId)
@@ -85,7 +89,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
// Toggle wallet address visibility for an exchange
const toggleExchangeAddressVisibility = (exchangeId: string) => {
setVisibleExchangeAddresses(prev => {
setVisibleExchangeAddresses((prev) => {
const next = new Set(prev)
if (next.has(exchangeId)) {
next.delete(exchangeId)
@@ -180,7 +184,8 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
}
const getExchangeUsageInfo = (exchangeId: string) => {
const usingTraders = traders?.filter((tr) => tr.exchange_id === exchangeId) || []
const usingTraders =
traders?.filter((tr) => tr.exchange_id === exchangeId) || []
const runningCount = usingTraders.filter((tr) => tr.is_running).length
const totalCount = usingTraders.length
return { runningCount, totalCount, usingTraders }
@@ -311,11 +316,18 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
}
}
const handleToggleCompetition = async (traderId: string, currentShowInCompetition: boolean) => {
const handleToggleCompetition = async (
traderId: string,
currentShowInCompetition: boolean
) => {
try {
const newValue = !currentShowInCompetition
await api.toggleCompetition(traderId, newValue)
toast.success(newValue ? t('aiTradersToast.showInCompetition', language) : t('aiTradersToast.hideInCompetition', language))
toast.success(
newValue
? t('aiTradersToast.showInCompetition', language)
: t('aiTradersToast.hideInCompetition', language)
)
await mutateTraders()
} catch (error) {
@@ -452,12 +464,12 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
allModels?.map((m) =>
m.id === modelId
? {
...m,
apiKey,
customApiUrl: customApiUrl || '',
customModelName: customModelName || '',
enabled: true,
}
...m,
apiKey,
customApiUrl: customApiUrl || '',
customModelName: customModelName || '',
enabled: true,
}
: m
) || []
} else {
@@ -572,7 +584,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
}
await api.updateExchangeConfigsEncrypted(request)
toast.success(t('aiTradersToast.exchangeConfigUpdated', language))
toast.success(t('aiTradersToast.exchangeConfigUpdated', language))
} else {
const createRequest = {
exchange_type: exchangeType,
@@ -593,7 +605,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
}
await api.createExchangeEncrypted(createRequest)
toast.success(t('aiTradersToast.exchangeCreated', language))
toast.success(t('aiTradersToast.exchangeCreated', language))
}
const refreshedExchanges = await api.getExchangeConfigs()
@@ -676,7 +688,10 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
<button
onClick={() => setShowCreateModal(true)}
disabled={configuredModels.length === 0 || configuredExchanges.length === 0}
disabled={
configuredModels.length === 0 ||
configuredExchanges.length === 0
}
className="group relative px-6 py-2 rounded text-xs font-bold font-mono uppercase tracking-wider transition-all disabled:opacity-50 disabled:cursor-not-allowed whitespace-nowrap overflow-hidden bg-nofx-gold text-black hover:bg-yellow-400 shadow-[0_0_20px_rgba(240,185,11,0.2)] hover:shadow-[0_0_30px_rgba(240,185,11,0.4)]"
>
<span className="relative z-10 flex items-center gap-2">
+128 -47
View File
@@ -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'
@@ -107,7 +117,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)
}
@@ -123,33 +135,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)
@@ -165,16 +192,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)
@@ -226,7 +264,7 @@ export function SettingsPage() {
},
}
await api.updateExchangeConfigsEncrypted(request)
toast.success('Exchange config updated')
toast.success('Exchange config updated')
} else {
const createRequest = {
exchange_type: exchangeType,
@@ -246,7 +284,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)
@@ -276,7 +314,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>
@@ -287,9 +328,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}
@@ -300,7 +342,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">
@@ -313,7 +354,9 @@ export function SettingsPage() {
<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'}
@@ -328,7 +371,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>
@@ -349,10 +396,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} />
@@ -369,7 +420,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">
@@ -387,10 +441,15 @@ export function SettingsPage() {
</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>
))}
@@ -404,10 +463,14 @@ 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>
<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} />
@@ -424,7 +487,10 @@ export function SettingsPage() {
{exchanges.map((exchange) => (
<button
key={exchange.id}
onClick={() => { setEditingExchange(exchange.id); setShowExchangeModal(true) }}
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">
@@ -444,7 +510,10 @@ export function SettingsPage() {
</div>
</div>
</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>
@@ -456,7 +525,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)}
@@ -466,9 +536,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>
)}
@@ -484,7 +559,10 @@ export function SettingsPage() {
editingModelId={editingModel}
onSave={handleSaveModel}
onDelete={handleDeleteModel}
onClose={() => { setShowModelModal(false); setEditingModel(null) }}
onClose={() => {
setShowModelModal(false)
setEditingModel(null)
}}
language={language}
/>
</div>
@@ -498,7 +576,10 @@ export function SettingsPage() {
editingExchangeId={editingExchange}
onSave={handleSaveExchange}
onDelete={handleDeleteExchange}
onClose={() => { setShowExchangeModal(false); setEditingExchange(null) }}
onClose={() => {
setShowExchangeModal(false)
setEditingExchange(null)
}}
language={language}
/>
</div>