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
+1 -1
View File
@@ -217,7 +217,7 @@ func (s *Server) handleGetSupportedModels(c *gin.Context) {
{"id": "qwen", "name": "Qwen", "provider": "qwen", "defaultModel": "qwen3-max"}, {"id": "qwen", "name": "Qwen", "provider": "qwen", "defaultModel": "qwen3-max"},
{"id": "openai", "name": "OpenAI", "provider": "openai", "defaultModel": "gpt-5.1"}, {"id": "openai", "name": "OpenAI", "provider": "openai", "defaultModel": "gpt-5.1"},
{"id": "claude", "name": "Claude", "provider": "claude", "defaultModel": "claude-opus-4-6"}, {"id": "claude", "name": "Claude", "provider": "claude", "defaultModel": "claude-opus-4-6"},
{"id": "gemini", "name": "Google Gemini", "provider": "gemini", "defaultModel": "gemini-3-pro-preview"}, {"id": "gemini", "name": "Google Gemini", "provider": "gemini", "defaultModel": "gemini-3.1-pro"},
{"id": "grok", "name": "Grok (xAI)", "provider": "grok", "defaultModel": "grok-3-latest"}, {"id": "grok", "name": "Grok (xAI)", "provider": "grok", "defaultModel": "grok-3-latest"},
{"id": "kimi", "name": "Kimi (Moonshot)", "provider": "kimi", "defaultModel": "moonshot-v1-auto"}, {"id": "kimi", "name": "Kimi (Moonshot)", "provider": "kimi", "defaultModel": "moonshot-v1-auto"},
{"id": "minimax", "name": "MiniMax", "provider": "minimax", "defaultModel": "MiniMax-M2.7"}, {"id": "minimax", "name": "MiniMax", "provider": "minimax", "defaultModel": "MiniMax-M2.7"},
+31 -16
View File
@@ -45,8 +45,12 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
const [allModels, setAllModels] = useState<AIModel[]>([]) const [allModels, setAllModels] = useState<AIModel[]>([])
const [allExchanges, setAllExchanges] = useState<Exchange[]>([]) const [allExchanges, setAllExchanges] = useState<Exchange[]>([])
const [supportedModels, setSupportedModels] = useState<AIModel[]>([]) const [supportedModels, setSupportedModels] = useState<AIModel[]>([])
const [visibleTraderAddresses, setVisibleTraderAddresses] = useState<Set<string>>(new Set()) const [visibleTraderAddresses, setVisibleTraderAddresses] = useState<
const [visibleExchangeAddresses, setVisibleExchangeAddresses] = useState<Set<string>>(new Set()) Set<string>
>(new Set())
const [visibleExchangeAddresses, setVisibleExchangeAddresses] = useState<
Set<string>
>(new Set())
const [copiedId, setCopiedId] = useState<string | null>(null) const [copiedId, setCopiedId] = useState<string | null>(null)
const loadConfigs = async () => { const loadConfigs = async () => {
@@ -72,7 +76,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
// Toggle wallet address visibility for a trader // Toggle wallet address visibility for a trader
const toggleTraderAddressVisibility = (traderId: string) => { const toggleTraderAddressVisibility = (traderId: string) => {
setVisibleTraderAddresses(prev => { setVisibleTraderAddresses((prev) => {
const next = new Set(prev) const next = new Set(prev)
if (next.has(traderId)) { if (next.has(traderId)) {
next.delete(traderId) next.delete(traderId)
@@ -85,7 +89,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
// Toggle wallet address visibility for an exchange // Toggle wallet address visibility for an exchange
const toggleExchangeAddressVisibility = (exchangeId: string) => { const toggleExchangeAddressVisibility = (exchangeId: string) => {
setVisibleExchangeAddresses(prev => { setVisibleExchangeAddresses((prev) => {
const next = new Set(prev) const next = new Set(prev)
if (next.has(exchangeId)) { if (next.has(exchangeId)) {
next.delete(exchangeId) next.delete(exchangeId)
@@ -180,7 +184,8 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
} }
const getExchangeUsageInfo = (exchangeId: string) => { 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 runningCount = usingTraders.filter((tr) => tr.is_running).length
const totalCount = usingTraders.length const totalCount = usingTraders.length
return { runningCount, totalCount, usingTraders } 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 { try {
const newValue = !currentShowInCompetition const newValue = !currentShowInCompetition
await api.toggleCompetition(traderId, newValue) 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() await mutateTraders()
} catch (error) { } catch (error) {
@@ -452,12 +464,12 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
allModels?.map((m) => allModels?.map((m) =>
m.id === modelId m.id === modelId
? { ? {
...m, ...m,
apiKey, apiKey,
customApiUrl: customApiUrl || '', customApiUrl: customApiUrl || '',
customModelName: customModelName || '', customModelName: customModelName || '',
enabled: true, enabled: true,
} }
: m : m
) || [] ) || []
} else { } else {
@@ -572,7 +584,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
} }
await api.updateExchangeConfigsEncrypted(request) await api.updateExchangeConfigsEncrypted(request)
toast.success(t('aiTradersToast.exchangeConfigUpdated', language)) toast.success(t('aiTradersToast.exchangeConfigUpdated', language))
} else { } else {
const createRequest = { const createRequest = {
exchange_type: exchangeType, exchange_type: exchangeType,
@@ -593,7 +605,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
} }
await api.createExchangeEncrypted(createRequest) await api.createExchangeEncrypted(createRequest)
toast.success(t('aiTradersToast.exchangeCreated', language)) toast.success(t('aiTradersToast.exchangeCreated', language))
} }
const refreshedExchanges = await api.getExchangeConfigs() const refreshedExchanges = await api.getExchangeConfigs()
@@ -676,7 +688,10 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
<button <button
onClick={() => setShowCreateModal(true)} 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)]" 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"> <span className="relative z-10 flex items-center gap-2">
+128 -47
View File
@@ -1,6 +1,16 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { toast } from 'sonner' 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 { useAuth } from '../contexts/AuthContext'
import { useLanguage } from '../contexts/LanguageContext' import { useLanguage } from '../contexts/LanguageContext'
import { api } from '../lib/api' import { api } from '../lib/api'
@@ -107,7 +117,9 @@ export function SettingsPage() {
toast.success('Password updated successfully') toast.success('Password updated successfully')
setNewPassword('') setNewPassword('')
} catch (err) { } 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 { } finally {
setChangingPassword(false) setChangingPassword(false)
} }
@@ -123,33 +135,48 @@ export function SettingsPage() {
const existingModel = configuredModels.find((m) => m.id === modelId) const existingModel = configuredModels.find((m) => m.id === modelId)
const modelTemplate = supportedModels.find((m) => m.id === modelId) const modelTemplate = supportedModels.find((m) => m.id === modelId)
const modelToUpdate = existingModel || modelTemplate const modelToUpdate = existingModel || modelTemplate
if (!modelToUpdate) { toast.error('Model not found'); return } if (!modelToUpdate) {
toast.error('Model not found')
return
}
let updatedModels: AIModel[] let updatedModels: AIModel[]
if (existingModel) { if (existingModel) {
updatedModels = configuredModels.map((m) => updatedModels = configuredModels.map((m) =>
m.id === modelId m.id === modelId
? { ...m, apiKey, customApiUrl: customApiUrl || '', customModelName: customModelName || '', enabled: true } ? {
...m,
apiKey,
customApiUrl: customApiUrl || '',
customModelName: customModelName || '',
enabled: true,
}
: m : m
) )
} else { } else {
updatedModels = [...configuredModels, { updatedModels = [
...modelToUpdate, ...configuredModels,
apiKey, {
customApiUrl: customApiUrl || '', ...modelToUpdate,
customModelName: customModelName || '', apiKey,
enabled: true, customApiUrl: customApiUrl || '',
}] customModelName: customModelName || '',
enabled: true,
},
]
} }
const request = { const request = {
models: Object.fromEntries( models: Object.fromEntries(
updatedModels.map((m) => [m.provider, { updatedModels.map((m) => [
enabled: m.enabled, m.provider,
api_key: m.apiKey || '', {
custom_api_url: m.customApiUrl || '', enabled: m.enabled,
custom_model_name: m.customModelName || '', api_key: m.apiKey || '',
}]) custom_api_url: m.customApiUrl || '',
custom_model_name: m.customModelName || '',
},
])
), ),
} }
await api.updateModelConfigs(request) await api.updateModelConfigs(request)
@@ -165,16 +192,27 @@ export function SettingsPage() {
const handleDeleteModel = async (modelId: string) => { const handleDeleteModel = async (modelId: string) => {
try { try {
const updatedModels = configuredModels.map((m) => 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 = { const request = {
models: Object.fromEntries( models: Object.fromEntries(
updatedModels.map((m) => [m.provider, { updatedModels.map((m) => [
enabled: m.enabled, m.provider,
api_key: m.apiKey || '', {
custom_api_url: m.customApiUrl || '', enabled: m.enabled,
custom_model_name: m.customModelName || '', api_key: m.apiKey || '',
}]) custom_api_url: m.customApiUrl || '',
custom_model_name: m.customModelName || '',
},
])
), ),
} }
await api.updateModelConfigs(request) await api.updateModelConfigs(request)
@@ -226,7 +264,7 @@ export function SettingsPage() {
}, },
} }
await api.updateExchangeConfigsEncrypted(request) await api.updateExchangeConfigsEncrypted(request)
toast.success('Exchange config updated') toast.success('Exchange config updated')
} else { } else {
const createRequest = { const createRequest = {
exchange_type: exchangeType, exchange_type: exchangeType,
@@ -246,7 +284,7 @@ export function SettingsPage() {
lighter_api_key_index: lighterApiKeyIndex || 0, lighter_api_key_index: lighterApiKeyIndex || 0,
} }
await api.createExchangeEncrypted(createRequest) await api.createExchangeEncrypted(createRequest)
toast.success('Exchange account created') toast.success('Exchange account created')
} }
await refreshExchangeConfigs() await refreshExchangeConfigs()
setShowExchangeModal(false) setShowExchangeModal(false)
@@ -276,7 +314,10 @@ export function SettingsPage() {
] ]
return ( 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"> <div className="max-w-2xl mx-auto">
<h1 className="text-xl font-bold text-white mb-6">Settings</h1> <h1 className="text-xl font-bold text-white mb-6">Settings</h1>
@@ -287,9 +328,10 @@ export function SettingsPage() {
key={tab.key} key={tab.key}
onClick={() => setActiveTab(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 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' activeTab === tab.key
: 'text-zinc-400 hover:text-white' ? 'bg-nofx-gold text-black'
: 'text-zinc-400 hover:text-white'
}`} }`}
> >
{tab.icon} {tab.icon}
@@ -300,7 +342,6 @@ export function SettingsPage() {
{/* Tab Content */} {/* Tab Content */}
<div className="bg-zinc-900/60 backdrop-blur-xl border border-zinc-800/80 rounded-2xl p-6"> <div className="bg-zinc-900/60 backdrop-blur-xl border border-zinc-800/80 rounded-2xl p-6">
{/* Account Tab */} {/* Account Tab */}
{activeTab === 'account' && ( {activeTab === 'account' && (
<div className="space-y-6"> <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> <h3 className="text-sm font-semibold text-white mb-4">Change Password</h3>
<form onSubmit={handleChangePassword} className="space-y-4"> <form onSubmit={handleChangePassword} className="space-y-4">
<div> <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"> <div className="relative">
<input <input
type={showPassword ? 'text' : 'password'} type={showPassword ? 'text' : 'password'}
@@ -328,7 +371,11 @@ export function SettingsPage() {
onClick={() => setShowPassword(!showPassword)} onClick={() => setShowPassword(!showPassword)}
className="absolute right-3.5 top-1/2 -translate-y-1/2 text-zinc-500 hover:text-zinc-300 transition-colors" 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> </button>
</div> </div>
</div> </div>
@@ -349,10 +396,14 @@ export function SettingsPage() {
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<p className="text-sm text-zinc-400"> <p className="text-sm text-zinc-400">
{configuredModels.length} model{configuredModels.length !== 1 ? 's' : ''} configured {configuredModels.length} model
{configuredModels.length !== 1 ? 's' : ''} configured
</p> </p>
<button <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" 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} /> <Plus size={14} />
@@ -369,7 +420,10 @@ export function SettingsPage() {
{configuredModels.map((model) => ( {configuredModels.map((model) => (
<button <button
key={model.id} 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" 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="flex items-center gap-3">
@@ -387,10 +441,15 @@ export function SettingsPage() {
</div> </div>
</div> </div>
<div className="flex items-center gap-2"> <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'} {model.enabled ? 'Active' : 'Inactive'}
</span> </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> </div>
</button> </button>
))} ))}
@@ -404,10 +463,14 @@ export function SettingsPage() {
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<p className="text-sm text-zinc-400"> <p className="text-sm text-zinc-400">
{exchanges.length} account{exchanges.length !== 1 ? 's' : ''} connected {exchanges.length} account{exchanges.length !== 1 ? 's' : ''}{' '}
connected
</p> </p>
<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" 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} /> <Plus size={14} />
@@ -424,7 +487,10 @@ export function SettingsPage() {
{exchanges.map((exchange) => ( {exchanges.map((exchange) => (
<button <button
key={exchange.id} 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" 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="flex items-center gap-3">
@@ -444,7 +510,10 @@ export function SettingsPage() {
</div> </div>
</div> </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> </button>
))} ))}
</div> </div>
@@ -456,7 +525,8 @@ export function SettingsPage() {
{activeTab === 'telegram' && ( {activeTab === 'telegram' && (
<div className="space-y-4"> <div className="space-y-4">
<p className="text-sm text-zinc-400"> <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> </p>
<button <button
onClick={() => setShowTelegramModal(true)} 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"> <div className="w-8 h-8 rounded-lg bg-[#0088cc]/20 flex items-center justify-center">
<MessageCircle size={14} className="text-[#0088cc]" /> <MessageCircle size={14} className="text-[#0088cc]" />
</div> </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> </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> </button>
</div> </div>
)} )}
@@ -484,7 +559,10 @@ export function SettingsPage() {
editingModelId={editingModel} editingModelId={editingModel}
onSave={handleSaveModel} onSave={handleSaveModel}
onDelete={handleDeleteModel} onDelete={handleDeleteModel}
onClose={() => { setShowModelModal(false); setEditingModel(null) }} onClose={() => {
setShowModelModal(false)
setEditingModel(null)
}}
language={language} language={language}
/> />
</div> </div>
@@ -498,7 +576,10 @@ export function SettingsPage() {
editingExchangeId={editingExchange} editingExchangeId={editingExchange}
onSave={handleSaveExchange} onSave={handleSaveExchange}
onDelete={handleDeleteExchange} onDelete={handleDeleteExchange}
onClose={() => { setShowExchangeModal(false); setEditingExchange(null) }} onClose={() => {
setShowExchangeModal(false)
setEditingExchange(null)
}}
language={language} language={language}
/> />
</div> </div>