0
- ? '1px solid rgba(99, 102, 241, 0.1)'
- : 'none',
- }}
+ className={`transition-colors hover:bg-white/5 ${
+ idx > 0 ? 'border-t border-indigo-500/10' : ''
+ }`}
>
|
-
+
{stat.symbol}
|
-
+ |
{stat.total_trades}
|
= 50 ? '#10B981' : '#F87171',
- }}
+ className={`px-4 py-3 text-right mono text-sm font-semibold ${
+ (stat.win_rate || 0) >= 50
+ ? 'text-emerald-500'
+ : 'text-red-400'
+ }`}
>
{(stat.win_rate || 0).toFixed(1)}%
|
0 ? '#10B981' : '#F87171',
- }}
+ className={`px-4 py-3 text-right mono text-sm font-bold ${
+ (stat.total_pn_l || 0) > 0
+ ? 'text-emerald-500'
+ : 'text-red-400'
+ }`}
>
{(stat.total_pn_l || 0) > 0 ? '+' : ''}
{(stat.total_pn_l || 0).toFixed(2)}
|
0 ? '#10B981' : '#F87171',
- }}
+ className={`px-4 py-3 text-right mono text-sm ${
+ (stat.avg_pn_l || 0) > 0
+ ? 'text-emerald-500'
+ : 'text-red-400'
+ }`}
>
{(stat.avg_pn_l || 0) > 0 ? '+' : ''}
{(stat.avg_pn_l || 0).toFixed(2)}
@@ -810,28 +534,17 @@ export default function AILearning({ traderId }: AILearningProps) {
{/* 右侧:历史成交记录 */}
-
+
-
+
-
+
{t('tradeHistory', language)}
-
+
{performance?.recent_trades &&
performance.recent_trades.length > 0
? t('completedTrades', language, {
@@ -844,7 +557,7 @@ export default function AILearning({ traderId }: AILearningProps) {
{performance?.recent_trades &&
@@ -857,61 +570,38 @@ export default function AILearning({ traderId }: AILearningProps) {
return (
-
+
{trade.symbol}
{trade.side.toUpperCase()}
{isRecent && (
-
+
{t('latest', language)}
)}
{isProfitable ? '+' : ''}
{trade.pn_l_pct.toFixed(2)}%
@@ -920,24 +610,18 @@ export default function AILearning({ traderId }: AILearningProps) {
-
+
{t('entry', language)}
-
+
{trade.open_price.toFixed(4)}
-
+
{t('exit', language)}
-
+
{trade.close_price.toFixed(4)}
@@ -946,40 +630,36 @@ export default function AILearning({ traderId }: AILearningProps) {
{/* Position Details */}
- Quantity
-
+
+ Quantity
+
+
{trade.quantity ? trade.quantity.toFixed(4) : '-'}
- Leverage
-
+
+ Leverage
+
+
{trade.leverage ? `${trade.leverage}x` : '-'}
- Position Value
-
+
+ Position Value
+
+
{trade.position_value
? `$${trade.position_value.toFixed(2)}`
: '-'}
- Margin Used
-
+
+ Margin Used
+
+
{trade.margin_used
? `$${trade.margin_used.toFixed(2)}`
: '-'}
@@ -988,20 +668,22 @@ export default function AILearning({ traderId }: AILearningProps) {
- P&L
+
+ P&L
+
{isProfitable ? '+' : ''}
{trade.pn_l.toFixed(2)} USDT
@@ -1009,31 +691,16 @@ export default function AILearning({ traderId }: AILearningProps) {
-
+
⏱️ {formatDuration(trade.duration)}
{trade.was_stop_loss && (
-
+
{t('stopLoss', language)}
)}
-
+
{new Date(trade.close_time).toLocaleString('en-US', {
month: 'short',
day: '2-digit',
@@ -1048,12 +715,9 @@ export default function AILearning({ traderId }: AILearningProps) {
) : (
-
+
-
+
{t('noCompletedTrades', language)}
@@ -1063,54 +727,37 @@ export default function AILearning({ traderId }: AILearningProps) {
{/* AI学习说明 - 现代化设计 */}
-
+
-
-
+
+
-
+
{stripLeadingIcons(t('howAILearns', language))}
- •
-
+ •
+
{t('aiLearningPoint1', language)}
- •
-
+ •
+
{t('aiLearningPoint2', language)}
- •
-
+ •
+
{t('aiLearningPoint3', language)}
- •
-
+ •
+
{t('aiLearningPoint4', language)}
diff --git a/web/src/components/AITradersPage.tsx b/web/src/components/AITradersPage.tsx
deleted file mode 100644
index 3f05c5e18c..0000000000
--- a/web/src/components/AITradersPage.tsx
+++ /dev/null
@@ -1,2652 +0,0 @@
-import React, { useState, useEffect } from 'react'
-import { useNavigate } from 'react-router-dom'
-import useSWR from 'swr'
-import { api } from '../lib/api'
-import type {
- TraderInfo,
- CreateTraderRequest,
- AIModel,
- Exchange,
-} from '../types'
-import { useLanguage } from '../contexts/LanguageContext'
-import { t, type Language } from '../i18n/translations'
-import { useAuth } from '../contexts/AuthContext'
-import { getExchangeIcon } from './ExchangeIcons'
-import { getModelIcon } from './ModelIcons'
-import { TraderConfigModal } from './TraderConfigModal'
-import {
- TwoStageKeyModal,
- type TwoStageKeyModalResult,
-} from './TwoStageKeyModal'
-import {
- WebCryptoEnvironmentCheck,
- type WebCryptoCheckStatus,
-} from './WebCryptoEnvironmentCheck'
-import {
- Bot,
- Brain,
- Landmark,
- BarChart3,
- Trash2,
- Plus,
- Users,
- AlertTriangle,
- BookOpen,
- HelpCircle,
- Radio,
- Pencil,
-} from 'lucide-react'
-import { confirmToast } from '../lib/notify'
-import { toast } from 'sonner'
-
-// 获取友好的AI模型名称
-function getModelDisplayName(modelId: string): string {
- switch (modelId.toLowerCase()) {
- case 'deepseek':
- return 'DeepSeek'
- case 'qwen':
- return 'Qwen'
- case 'claude':
- return 'Claude'
- default:
- return modelId.toUpperCase()
- }
-}
-
-// 提取下划线后面的名称部分
-function getShortName(fullName: string): string {
- const parts = fullName.split('_')
- return parts.length > 1 ? parts[parts.length - 1] : fullName
-}
-
-interface AITradersPageProps {
- onTraderSelect?: (traderId: string) => void
-}
-
-export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
- const { language } = useLanguage()
- const { user, token } = useAuth()
- const navigate = useNavigate()
- const [showCreateModal, setShowCreateModal] = useState(false)
- const [showEditModal, setShowEditModal] = useState(false)
- const [showModelModal, setShowModelModal] = useState(false)
- const [showExchangeModal, setShowExchangeModal] = useState(false)
- const [showSignalSourceModal, setShowSignalSourceModal] = useState(false)
- const [editingModel, setEditingModel] = useState (null)
- const [editingExchange, setEditingExchange] = useState(null)
- const [editingTrader, setEditingTrader] = useState(null)
- const [allModels, setAllModels] = useState([])
- const [allExchanges, setAllExchanges] = useState([])
- const [supportedModels, setSupportedModels] = useState([])
- const [supportedExchanges, setSupportedExchanges] = useState([])
- const [userSignalSource, setUserSignalSource] = useState<{
- coinPoolUrl: string
- oiTopUrl: string
- }>({
- coinPoolUrl: '',
- oiTopUrl: '',
- })
-
- const { data: traders, mutate: mutateTraders } = useSWR(
- user && token ? 'traders' : null,
- api.getTraders,
- { refreshInterval: 5000 }
- )
-
- // 加载AI模型和交易所配置
- useEffect(() => {
- const loadConfigs = async () => {
- if (!user || !token) {
- // 未登录时只加载公开的支持模型和交易所
- try {
- const [supportedModels, supportedExchanges] = await Promise.all([
- api.getSupportedModels(),
- api.getSupportedExchanges(),
- ])
- setSupportedModels(supportedModels)
- setSupportedExchanges(supportedExchanges)
- } catch (err) {
- console.error('Failed to load supported configs:', err)
- }
- return
- }
-
- try {
- const [
- modelConfigs,
- exchangeConfigs,
- supportedModels,
- supportedExchanges,
- ] = await Promise.all([
- api.getModelConfigs(),
- api.getExchangeConfigs(),
- api.getSupportedModels(),
- api.getSupportedExchanges(),
- ])
- setAllModels(modelConfigs)
- setAllExchanges(exchangeConfigs)
- setSupportedModels(supportedModels)
- setSupportedExchanges(supportedExchanges)
-
- // 加载用户信号源配置
- try {
- const signalSource = await api.getUserSignalSource()
- setUserSignalSource({
- coinPoolUrl: signalSource.coin_pool_url || '',
- oiTopUrl: signalSource.oi_top_url || '',
- })
- } catch (error) {
- console.log('📡 用户信号源配置暂未设置')
- }
- } catch (error) {
- console.error('Failed to load configs:', error)
- }
- }
- loadConfigs()
- }, [user, token])
-
- // 只显示已配置的模型和交易所
- // 注意:后端返回的数据不包含敏感信息(apiKey等),所以通过其他字段判断是否已配置
- const configuredModels =
- allModels?.filter((m) => {
- // 如果模型已启用,说明已配置
- // 或者有自定义API URL,也说明已配置
- return m.enabled || (m.customApiUrl && m.customApiUrl.trim() !== '')
- }) || []
- const configuredExchanges =
- allExchanges?.filter((e) => {
- // Aster 交易所检查特殊字段
- if (e.id === 'aster') {
- return e.asterUser && e.asterUser.trim() !== ''
- }
- // Hyperliquid 需要检查钱包地址(后端会返回这个字段)
- if (e.id === 'hyperliquid') {
- return e.hyperliquidWalletAddr && e.hyperliquidWalletAddr.trim() !== ''
- }
- // 其他交易所:如果已启用,说明已配置(后端返回的已配置交易所会有 enabled: true)
- return e.enabled
- }) || []
-
- // 只在创建交易员时使用已启用且配置完整的
- // 注意:后端返回的数据不包含敏感信息,所以只检查 enabled 状态和必要的非敏感字段
- const enabledModels = allModels?.filter((m) => m.enabled) || []
- const enabledExchanges =
- allExchanges?.filter((e) => {
- if (!e.enabled) return false
-
- // Aster 交易所需要特殊字段(后端会返回这些非敏感字段)
- if (e.id === 'aster') {
- return (
- e.asterUser &&
- e.asterUser.trim() !== '' &&
- e.asterSigner &&
- e.asterSigner.trim() !== ''
- )
- }
-
- // Hyperliquid 需要钱包地址(后端会返回这个字段)
- if (e.id === 'hyperliquid') {
- return e.hyperliquidWalletAddr && e.hyperliquidWalletAddr.trim() !== ''
- }
-
- // 其他交易所:如果已启用,说明已配置完整(后端只返回已配置的交易所)
- return true
- }) || []
-
- // 检查模型是否正在被运行中的交易员使用(用于UI禁用)
- const isModelInUse = (modelId: string) => {
- return traders?.some((t) => t.ai_model === modelId && t.is_running)
- }
-
- // 检查交易所是否正在被运行中的交易员使用(用于UI禁用)
- const isExchangeInUse = (exchangeId: string) => {
- return traders?.some((t) => t.exchange_id === exchangeId && t.is_running)
- }
-
- // 检查模型是否被任何交易员使用(包括停止状态的)
- const isModelUsedByAnyTrader = (modelId: string) => {
- return traders?.some((t) => t.ai_model === modelId) || false
- }
-
- // 检查交易所是否被任何交易员使用(包括停止状态的)
- const isExchangeUsedByAnyTrader = (exchangeId: string) => {
- return traders?.some((t) => t.exchange_id === exchangeId) || false
- }
-
- // 获取使用特定模型的交易员列表
- const getTradersUsingModel = (modelId: string) => {
- return traders?.filter((t) => t.ai_model === modelId) || []
- }
-
- // 获取使用特定交易所的交易员列表
- const getTradersUsingExchange = (exchangeId: string) => {
- return traders?.filter((t) => t.exchange_id === exchangeId) || []
- }
-
- const handleCreateTrader = async (data: CreateTraderRequest) => {
- try {
- const model = allModels?.find((m) => m.id === data.ai_model_id)
- const exchange = allExchanges?.find((e) => e.id === data.exchange_id)
-
- if (!model?.enabled) {
- toast.error(t('modelNotConfigured', language))
- return
- }
-
- if (!exchange?.enabled) {
- toast.error(t('exchangeNotConfigured', language))
- return
- }
-
- await toast.promise(api.createTrader(data), {
- loading: '正在创建…',
- success: '创建成功',
- error: '创建失败',
- })
- setShowCreateModal(false)
- // Immediately refresh traders list for better UX
- await mutateTraders()
- } catch (error) {
- console.error('Failed to create trader:', error)
- toast.error(t('createTraderFailed', language))
- }
- }
-
- const handleEditTrader = async (traderId: string) => {
- try {
- const traderConfig = await api.getTraderConfig(traderId)
- setEditingTrader(traderConfig)
- setShowEditModal(true)
- } catch (error) {
- console.error('Failed to fetch trader config:', error)
- toast.error(t('getTraderConfigFailed', language))
- }
- }
-
- const handleSaveEditTrader = async (data: CreateTraderRequest) => {
- if (!editingTrader) return
-
- try {
- const model = enabledModels?.find((m) => m.id === data.ai_model_id)
- const exchange = enabledExchanges?.find((e) => e.id === data.exchange_id)
-
- if (!model) {
- toast.error(t('modelConfigNotExist', language))
- return
- }
-
- if (!exchange) {
- toast.error(t('exchangeConfigNotExist', language))
- return
- }
-
- const request = {
- name: data.name,
- ai_model_id: data.ai_model_id,
- exchange_id: data.exchange_id,
- initial_balance: data.initial_balance,
- scan_interval_minutes: data.scan_interval_minutes,
- btc_eth_leverage: data.btc_eth_leverage,
- altcoin_leverage: data.altcoin_leverage,
- trading_symbols: data.trading_symbols,
- custom_prompt: data.custom_prompt,
- override_base_prompt: data.override_base_prompt,
- system_prompt_template: data.system_prompt_template,
- is_cross_margin: data.is_cross_margin,
- use_coin_pool: data.use_coin_pool,
- use_oi_top: data.use_oi_top,
- }
-
- await toast.promise(api.updateTrader(editingTrader.trader_id, request), {
- loading: '正在保存…',
- success: '保存成功',
- error: '保存失败',
- })
- setShowEditModal(false)
- setEditingTrader(null)
- // Immediately refresh traders list for better UX
- await mutateTraders()
- } catch (error) {
- console.error('Failed to update trader:', error)
- toast.error(t('updateTraderFailed', language))
- }
- }
-
- const handleDeleteTrader = async (traderId: string) => {
- {
- const ok = await confirmToast(t('confirmDeleteTrader', language))
- if (!ok) return
- }
-
- try {
- await toast.promise(api.deleteTrader(traderId), {
- loading: '正在删除…',
- success: '删除成功',
- error: '删除失败',
- })
-
- // Immediately refresh traders list for better UX
- await mutateTraders()
- } catch (error) {
- console.error('Failed to delete trader:', error)
- toast.error(t('deleteTraderFailed', language))
- }
- }
-
- const handleToggleTrader = async (traderId: string, running: boolean) => {
- try {
- if (running) {
- await toast.promise(api.stopTrader(traderId), {
- loading: '正在停止…',
- success: '已停止',
- error: '停止失败',
- })
- } else {
- await toast.promise(api.startTrader(traderId), {
- loading: '正在启动…',
- success: '已启动',
- error: '启动失败',
- })
- }
-
- // Immediately refresh traders list to update running status
- await mutateTraders()
- } catch (error) {
- console.error('Failed to toggle trader:', error)
- toast.error(t('operationFailed', language))
- }
- }
-
- const handleModelClick = (modelId: string) => {
- if (!isModelInUse(modelId)) {
- setEditingModel(modelId)
- setShowModelModal(true)
- }
- }
-
- const handleExchangeClick = (exchangeId: string) => {
- if (!isExchangeInUse(exchangeId)) {
- setEditingExchange(exchangeId)
- setShowExchangeModal(true)
- }
- }
-
- // 通用删除配置处理函数
- const handleDeleteConfig = async (config: {
- id: string
- type: 'model' | 'exchange'
- checkInUse: (id: string) => boolean
- getUsingTraders: (id: string) => any[]
- cannotDeleteKey: string
- confirmDeleteKey: string
- allItems: T[] | undefined
- clearFields: (item: T) => T
- buildRequest: (items: T[]) => any
- updateApi: (request: any) => Promise
- refreshApi: () => Promise
- setItems: (items: T[]) => void
- closeModal: () => void
- errorKey: string
- }) => {
- // 检查是否有交易员正在使用
- if (config.checkInUse(config.id)) {
- const usingTraders = config.getUsingTraders(config.id)
- const traderNames = usingTraders.map((t) => t.trader_name).join(', ')
- toast.error(
- `${t(config.cannotDeleteKey, language)} · ${t('tradersUsing', language)}: ${traderNames} · ${t('pleaseDeleteTradersFirst', language)}`
- )
- return
- }
-
- {
- const ok = await confirmToast(t(config.confirmDeleteKey, language))
- if (!ok) return
- }
-
- try {
- const updatedItems =
- config.allItems?.map((item) =>
- item.id === config.id ? config.clearFields(item) : item
- ) || []
-
- const request = config.buildRequest(updatedItems)
- await toast.promise(config.updateApi(request), {
- loading: '正在更新配置…',
- success: '配置已更新',
- error: '更新配置失败',
- })
-
- // 重新获取用户配置以确保数据同步
- const refreshedItems = await config.refreshApi()
- config.setItems(refreshedItems)
-
- config.closeModal()
- } catch (error) {
- console.error(`Failed to delete ${config.type} config:`, error)
- toast.error(t(config.errorKey, language))
- }
- }
-
- const handleDeleteModelConfig = async (modelId: string) => {
- await handleDeleteConfig({
- id: modelId,
- type: 'model',
- checkInUse: isModelUsedByAnyTrader,
- getUsingTraders: getTradersUsingModel,
- cannotDeleteKey: 'cannotDeleteModelInUse',
- confirmDeleteKey: 'confirmDeleteModel',
- allItems: allModels,
- clearFields: (m) => ({
- ...m,
- apiKey: '',
- customApiUrl: '',
- customModelName: '',
- enabled: false,
- }),
- buildRequest: (models) => ({
- models: Object.fromEntries(
- models.map((model) => [
- model.provider,
- {
- enabled: model.enabled,
- api_key: model.apiKey || '',
- custom_api_url: model.customApiUrl || '',
- custom_model_name: model.customModelName || '',
- },
- ])
- ),
- }),
- updateApi: api.updateModelConfigs,
- refreshApi: api.getModelConfigs,
- setItems: (items) => {
- // 使用函数式更新确保状态正确更新
- setAllModels([...items])
- },
- closeModal: () => {
- setShowModelModal(false)
- setEditingModel(null)
- },
- errorKey: 'deleteConfigFailed',
- })
- }
-
- const handleSaveModelConfig = async (
- modelId: string,
- apiKey: string,
- customApiUrl?: string,
- customModelName?: string
- ) => {
- try {
- // 创建或更新用户的模型配置
- const existingModel = allModels?.find((m) => m.id === modelId)
- let updatedModels
-
- // 找到要配置的模型(优先从已配置列表,其次从支持列表)
- const modelToUpdate =
- existingModel || supportedModels?.find((m) => m.id === modelId)
- if (!modelToUpdate) {
- toast.error(t('modelNotExist', language))
- return
- }
-
- if (existingModel) {
- // 更新现有配置
- updatedModels =
- allModels?.map((m) =>
- m.id === modelId
- ? {
- ...m,
- apiKey,
- customApiUrl: customApiUrl || '',
- customModelName: customModelName || '',
- enabled: true,
- }
- : m
- ) || []
- } else {
- // 添加新配置
- const newModel = {
- ...modelToUpdate,
- apiKey,
- customApiUrl: customApiUrl || '',
- customModelName: customModelName || '',
- enabled: true,
- }
- updatedModels = [...(allModels || []), newModel]
- }
-
- const request = {
- models: Object.fromEntries(
- updatedModels.map((model) => [
- model.provider, // 使用 provider 而不是 id
- {
- enabled: model.enabled,
- api_key: model.apiKey || '',
- custom_api_url: model.customApiUrl || '',
- custom_model_name: model.customModelName || '',
- },
- ])
- ),
- }
-
- await toast.promise(api.updateModelConfigs(request), {
- loading: '正在更新模型配置…',
- success: '模型配置已更新',
- error: '更新模型配置失败',
- })
-
- // 重新获取用户配置以确保数据同步
- const refreshedModels = await api.getModelConfigs()
- setAllModels(refreshedModels)
-
- setShowModelModal(false)
- setEditingModel(null)
- } catch (error) {
- console.error('Failed to save model config:', error)
- toast.error(t('saveConfigFailed', language))
- }
- }
-
- const handleDeleteExchangeConfig = async (exchangeId: string) => {
- await handleDeleteConfig({
- id: exchangeId,
- type: 'exchange',
- checkInUse: isExchangeUsedByAnyTrader,
- getUsingTraders: getTradersUsingExchange,
- cannotDeleteKey: 'cannotDeleteExchangeInUse',
- confirmDeleteKey: 'confirmDeleteExchange',
- allItems: allExchanges,
- clearFields: (e) => ({
- ...e,
- apiKey: '',
- secretKey: '',
- hyperliquidWalletAddr: '',
- asterUser: '',
- asterSigner: '',
- asterPrivateKey: '',
- enabled: false,
- }),
- buildRequest: (exchanges) => ({
- exchanges: Object.fromEntries(
- exchanges.map((exchange) => [
- exchange.id,
- {
- enabled: exchange.enabled,
- api_key: exchange.apiKey || '',
- secret_key: exchange.secretKey || '',
- testnet: exchange.testnet || false,
- hyperliquid_wallet_addr: exchange.hyperliquidWalletAddr || '',
- aster_user: exchange.asterUser || '',
- aster_signer: exchange.asterSigner || '',
- aster_private_key: exchange.asterPrivateKey || '',
- },
- ])
- ),
- }),
- updateApi: api.updateExchangeConfigsEncrypted,
- refreshApi: api.getExchangeConfigs,
- setItems: (items) => {
- // 使用函数式更新确保状态正确更新
- setAllExchanges([...items])
- },
- closeModal: () => {
- setShowExchangeModal(false)
- setEditingExchange(null)
- },
- errorKey: 'deleteExchangeConfigFailed',
- })
- }
-
- const handleSaveExchangeConfig = async (
- exchangeId: string,
- apiKey: string,
- secretKey?: string,
- testnet?: boolean,
- hyperliquidWalletAddr?: string,
- asterUser?: string,
- asterSigner?: string,
- asterPrivateKey?: string
- ) => {
- try {
- // 找到要配置的交易所(从supportedExchanges中)
- const exchangeToUpdate = supportedExchanges?.find(
- (e) => e.id === exchangeId
- )
- if (!exchangeToUpdate) {
- toast.error(t('exchangeNotExist', language))
- return
- }
-
- // 创建或更新用户的交易所配置
- const existingExchange = allExchanges?.find((e) => e.id === exchangeId)
- let updatedExchanges
-
- if (existingExchange) {
- // 更新现有配置
- updatedExchanges =
- allExchanges?.map((e) =>
- e.id === exchangeId
- ? {
- ...e,
- apiKey,
- secretKey,
- testnet,
- hyperliquidWalletAddr,
- asterUser,
- asterSigner,
- asterPrivateKey,
- enabled: true,
- }
- : e
- ) || []
- } else {
- // 添加新配置
- const newExchange = {
- ...exchangeToUpdate,
- apiKey,
- secretKey,
- testnet,
- hyperliquidWalletAddr,
- asterUser,
- asterSigner,
- asterPrivateKey,
- enabled: true,
- }
- updatedExchanges = [...(allExchanges || []), newExchange]
- }
-
- const request = {
- exchanges: Object.fromEntries(
- updatedExchanges.map((exchange) => [
- exchange.id,
- {
- enabled: exchange.enabled,
- api_key: exchange.apiKey || '',
- secret_key: exchange.secretKey || '',
- testnet: exchange.testnet || false,
- hyperliquid_wallet_addr: exchange.hyperliquidWalletAddr || '',
- aster_user: exchange.asterUser || '',
- aster_signer: exchange.asterSigner || '',
- aster_private_key: exchange.asterPrivateKey || '',
- },
- ])
- ),
- }
-
- await toast.promise(api.updateExchangeConfigsEncrypted(request), {
- loading: '正在更新交易所配置…',
- success: '交易所配置已更新',
- error: '更新交易所配置失败',
- })
-
- // 重新获取用户配置以确保数据同步
- const refreshedExchanges = await api.getExchangeConfigs()
- setAllExchanges(refreshedExchanges)
-
- setShowExchangeModal(false)
- setEditingExchange(null)
- } catch (error) {
- console.error('Failed to save exchange config:', error)
- toast.error(t('saveConfigFailed', language))
- }
- }
-
- const handleAddModel = () => {
- setEditingModel(null)
- setShowModelModal(true)
- }
-
- const handleAddExchange = () => {
- setEditingExchange(null)
- setShowExchangeModal(true)
- }
-
- const handleSaveSignalSource = async (
- coinPoolUrl: string,
- oiTopUrl: string
- ) => {
- try {
- await toast.promise(api.saveUserSignalSource(coinPoolUrl, oiTopUrl), {
- loading: '正在保存…',
- success: '保存成功',
- error: '保存失败',
- })
- setUserSignalSource({ coinPoolUrl, oiTopUrl })
- setShowSignalSourceModal(false)
- } catch (error) {
- console.error('Failed to save signal source:', error)
- toast.error(t('saveSignalSourceFailed', language))
- }
- }
-
- return (
-
- {/* Header */}
-
-
-
-
-
-
-
- {t('aiTraders', language)}
-
- {traders?.length || 0} {t('active', language)}
-
-
-
- {t('manageAITraders', language)}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {/* 信号源配置警告 */}
- {traders &&
- traders.some((t) => t.use_coin_pool || t.use_oi_top) &&
- !userSignalSource.coinPoolUrl &&
- !userSignalSource.oiTopUrl && (
-
-
-
-
- ⚠️ {t('signalSourceNotConfigured', language)}
-
-
-
- {t('signalSourceWarningMessage', language)}
-
-
- {t('solutions', language)}
-
-
- - 点击"{t('signalSource', language)}"按钮配置API地址
- - 或在交易员配置中禁用"使用币种池"和"使用OI Top"
- - 或在交易员配置中设置自定义币种列表
-
-
-
-
-
- )}
-
- {/* Configuration Status */}
-
- {/* AI Models */}
-
-
-
- {t('aiModels', language)}
-
-
- {configuredModels.map((model) => {
- const inUse = isModelInUse(model.id)
- return (
- handleModelClick(model.id)}
- >
-
-
- {getModelIcon(model.provider || model.id, {
- width: 28,
- height: 28,
- }) || (
-
- {getShortName(model.name)[0]}
-
- )}
-
-
-
- {getShortName(model.name)}
-
-
- {inUse
- ? t('inUse', language)
- : model.enabled
- ? t('enabled', language)
- : t('configured', language)}
-
-
-
-
-
- )
- })}
- {configuredModels.length === 0 && (
-
-
-
- {t('noModelsConfigured', language)}
-
-
- )}
-
-
-
- {/* Exchanges */}
-
-
-
- {t('exchanges', language)}
-
-
- {configuredExchanges.map((exchange) => {
- const inUse = isExchangeInUse(exchange.id)
- return (
- handleExchangeClick(exchange.id)}
- >
-
-
- {getExchangeIcon(exchange.id, { width: 28, height: 28 })}
-
-
-
- {getShortName(exchange.name)}
-
-
- {exchange.type.toUpperCase()} •{' '}
- {inUse
- ? t('inUse', language)
- : exchange.enabled
- ? t('enabled', language)
- : t('configured', language)}
-
-
-
-
-
- )
- })}
- {configuredExchanges.length === 0 && (
-
-
-
- {t('noExchangesConfigured', language)}
-
-
- )}
-
-
-
-
- {/* Traders List */}
-
-
-
-
- {t('currentTraders', language)}
-
-
-
- {traders && traders.length > 0 ? (
-
- {traders.map((trader) => (
-
-
-
-
-
-
-
- {trader.trader_name}
-
-
- {getModelDisplayName(
- trader.ai_model.split('_').pop() || trader.ai_model
- )}{' '}
- Model • {trader.exchange_id?.toUpperCase()}
-
-
-
-
-
- {/* Status */}
-
- {/*
- {t('status', language)}
- */}
-
- {trader.is_running
- ? t('running', language)
- : t('stopped', language)}
-
-
-
- {/* Actions: 禁止换行,超出横向滚动 */}
-
-
-
-
-
-
-
-
-
-
-
- ))}
-
- ) : (
-
-
-
- {t('noTraders', language)}
-
-
- {t('createFirstTrader', language)}
-
- {(configuredModels.length === 0 ||
- configuredExchanges.length === 0) && (
-
- {configuredModels.length === 0 &&
- configuredExchanges.length === 0
- ? t('configureModelsAndExchangesFirst', language)
- : configuredModels.length === 0
- ? t('configureModelsFirst', language)
- : t('configureExchangesFirst', language)}
-
- )}
-
- )}
-
-
- {/* Create Trader Modal */}
- {showCreateModal && (
- setShowCreateModal(false)}
- />
- )}
-
- {/* Edit Trader Modal */}
- {showEditModal && editingTrader && (
- {
- setShowEditModal(false)
- setEditingTrader(null)
- }}
- />
- )}
-
- {/* Model Configuration Modal */}
- {showModelModal && (
- {
- setShowModelModal(false)
- setEditingModel(null)
- }}
- language={language}
- />
- )}
-
- {/* Exchange Configuration Modal */}
- {showExchangeModal && (
- {
- setShowExchangeModal(false)
- setEditingExchange(null)
- }}
- language={language}
- />
- )}
-
- {/* Signal Source Configuration Modal */}
- {showSignalSourceModal && (
- setShowSignalSourceModal(false)}
- language={language}
- />
- )}
-
- )
-}
-
-// Tooltip Helper Component
-function Tooltip({
- content,
- children,
-}: {
- content: string
- children: React.ReactNode
-}) {
- const [show, setShow] = useState(false)
-
- return (
-
- setShow(true)}
- onMouseLeave={() => setShow(false)}
- onClick={() => setShow(!show)}
- >
- {children}
-
- {show && (
-
- )}
-
- )
-}
-
-// Signal Source Configuration Modal Component
-function SignalSourceModal({
- coinPoolUrl,
- oiTopUrl,
- onSave,
- onClose,
- language,
-}: {
- coinPoolUrl: string
- oiTopUrl: string
- onSave: (coinPoolUrl: string, oiTopUrl: string) => void
- onClose: () => void
- language: Language
-}) {
- const [coinPool, setCoinPool] = useState(coinPoolUrl || '')
- const [oiTop, setOiTop] = useState(oiTopUrl || '')
-
- const handleSubmit = (e: React.FormEvent) => {
- e.preventDefault()
- onSave(coinPool.trim(), oiTop.trim())
- }
-
- return (
-
-
-
- {t('signalSourceConfig', language)}
-
-
-
-
-
- )
-}
-
-// Model Configuration Modal Component
-function ModelConfigModal({
- allModels,
- configuredModels,
- editingModelId,
- onSave,
- onDelete,
- onClose,
- language,
-}: {
- allModels: AIModel[]
- configuredModels: AIModel[]
- editingModelId: string | null
- onSave: (
- modelId: string,
- apiKey: string,
- baseUrl?: string,
- modelName?: string
- ) => void
- onDelete: (modelId: string) => void
- onClose: () => void
- language: Language
-}) {
- const [selectedModelId, setSelectedModelId] = useState(editingModelId || '')
- const [apiKey, setApiKey] = useState('')
- const [baseUrl, setBaseUrl] = useState('')
- const [modelName, setModelName] = useState('')
-
- // 获取当前编辑的模型信息 - 编辑时从已配置的模型中查找,新建时从所有支持的模型中查找
- const selectedModel = editingModelId
- ? configuredModels?.find((m) => m.id === selectedModelId)
- : allModels?.find((m) => m.id === selectedModelId)
-
- // 如果是编辑现有模型,初始化API Key、Base URL和Model Name
- useEffect(() => {
- if (editingModelId && selectedModel) {
- setApiKey(selectedModel.apiKey || '')
- setBaseUrl(selectedModel.customApiUrl || '')
- setModelName(selectedModel.customModelName || '')
- }
- }, [editingModelId, selectedModel])
-
- const handleSubmit = (e: React.FormEvent) => {
- e.preventDefault()
- if (!selectedModelId || !apiKey.trim()) return
-
- onSave(
- selectedModelId,
- apiKey.trim(),
- baseUrl.trim() || undefined,
- modelName.trim() || undefined
- )
- }
-
- // 可选择的模型列表(所有支持的模型)
- const availableModels = allModels || []
-
- return (
-
-
-
-
- {editingModelId
- ? t('editAIModel', language)
- : t('addAIModel', language)}
-
- {editingModelId && (
-
- )}
-
-
-
-
-
- )
-}
-
-// Exchange Configuration Modal Component
-function ExchangeConfigModal({
- allExchanges,
- editingExchangeId,
- onSave,
- onDelete,
- onClose,
- language,
-}: {
- allExchanges: Exchange[]
- editingExchangeId: string | null
- onSave: (
- exchangeId: string,
- apiKey: string,
- secretKey?: string,
- testnet?: boolean,
- hyperliquidWalletAddr?: string,
- asterUser?: string,
- asterSigner?: string,
- asterPrivateKey?: string
- ) => Promise
- onDelete: (exchangeId: string) => void
- onClose: () => void
- language: Language
-}) {
- const [selectedExchangeId, setSelectedExchangeId] = useState(
- editingExchangeId || ''
- )
- const [apiKey, setApiKey] = useState('')
- const [secretKey, setSecretKey] = useState('')
- const [passphrase, setPassphrase] = useState('')
- const [testnet, setTestnet] = useState(false)
- const [showGuide, setShowGuide] = useState(false)
- const [serverIP, setServerIP] = useState<{
- public_ip: string
- message: string
- } | null>(null)
- const [loadingIP, setLoadingIP] = useState(false)
- const [copiedIP, setCopiedIP] = useState(false)
- const [webCryptoStatus, setWebCryptoStatus] =
- useState('idle')
-
- // 币安配置指南展开状态
- const [showBinanceGuide, setShowBinanceGuide] = useState(false)
-
- // Aster 特定字段
- const [asterUser, setAsterUser] = useState('')
- const [asterSigner, setAsterSigner] = useState('')
- const [asterPrivateKey, setAsterPrivateKey] = useState('')
-
- // Hyperliquid 特定字段
- const [hyperliquidWalletAddr, setHyperliquidWalletAddr] = useState('')
-
- // 安全输入状态
- const [secureInputTarget, setSecureInputTarget] = useState<
- null | 'hyperliquid' | 'aster'
- >(null)
-
- // 获取当前编辑的交易所信息
- const selectedExchange = allExchanges?.find(
- (e) => e.id === selectedExchangeId
- )
-
- // 如果是编辑现有交易所,初始化表单数据
- useEffect(() => {
- if (editingExchangeId && selectedExchange) {
- setApiKey(selectedExchange.apiKey || '')
- setSecretKey(selectedExchange.secretKey || '')
- setPassphrase('') // Don't load existing passphrase for security
- setTestnet(selectedExchange.testnet || false)
-
- // Aster 字段
- setAsterUser(selectedExchange.asterUser || '')
- setAsterSigner(selectedExchange.asterSigner || '')
- setAsterPrivateKey('') // Don't load existing private key for security
-
- // Hyperliquid 字段
- setHyperliquidWalletAddr(selectedExchange.hyperliquidWalletAddr || '')
- }
- }, [editingExchangeId, selectedExchange])
-
- // 加载服务器IP(当选择binance时)
- useEffect(() => {
- if (selectedExchangeId === 'binance' && !serverIP) {
- setLoadingIP(true)
- api
- .getServerIP()
- .then((data) => {
- setServerIP(data)
- })
- .catch((err) => {
- console.error('Failed to load server IP:', err)
- })
- .finally(() => {
- setLoadingIP(false)
- })
- }
- }, [selectedExchangeId])
-
- const handleCopyIP = async (ip: string) => {
- try {
- // 优先使用现代 Clipboard API
- if (navigator.clipboard && navigator.clipboard.writeText) {
- await navigator.clipboard.writeText(ip)
- setCopiedIP(true)
- setTimeout(() => setCopiedIP(false), 2000)
- toast.success(t('ipCopied', language))
- } else {
- // 降级方案: 使用传统的 execCommand 方法
- const textArea = document.createElement('textarea')
- textArea.value = ip
- textArea.style.position = 'fixed'
- textArea.style.left = '-999999px'
- textArea.style.top = '-999999px'
- document.body.appendChild(textArea)
- textArea.focus()
- textArea.select()
-
- try {
- const successful = document.execCommand('copy')
- if (successful) {
- setCopiedIP(true)
- setTimeout(() => setCopiedIP(false), 2000)
- toast.success(t('ipCopied', language))
- } else {
- throw new Error('复制命令执行失败')
- }
- } finally {
- document.body.removeChild(textArea)
- }
- }
- } catch (err) {
- console.error('复制失败:', err)
- // 显示错误提示
- toast.error(
- t('copyIPFailed', language) || `复制失败: ${ip}\n请手动复制此IP地址`
- )
- }
- }
-
- // 安全输入处理函数
- const secureInputContextLabel =
- secureInputTarget === 'aster'
- ? t('asterExchangeName', language)
- : secureInputTarget === 'hyperliquid'
- ? t('hyperliquidExchangeName', language)
- : undefined
-
- const handleSecureInputCancel = () => {
- setSecureInputTarget(null)
- }
-
- const handleSecureInputComplete = ({
- value,
- obfuscationLog,
- }: TwoStageKeyModalResult) => {
- const trimmed = value.trim()
- if (secureInputTarget === 'hyperliquid') {
- setApiKey(trimmed)
- }
- if (secureInputTarget === 'aster') {
- setAsterPrivateKey(trimmed)
- }
- console.log('Secure input obfuscation log:', obfuscationLog)
- setSecureInputTarget(null)
- }
-
- // 掩盖敏感数据显示
- const maskSecret = (secret: string) => {
- if (!secret || secret.length === 0) return ''
- if (secret.length <= 8) return '*'.repeat(secret.length)
- return (
- secret.slice(0, 4) +
- '*'.repeat(Math.max(secret.length - 8, 4)) +
- secret.slice(-4)
- )
- }
-
- const handleSubmit = async (e: React.FormEvent) => {
- e.preventDefault()
- if (!selectedExchangeId) return
-
- // 根据交易所类型验证不同字段
- if (selectedExchange?.id === 'binance') {
- if (!apiKey.trim() || !secretKey.trim()) return
- await onSave(selectedExchangeId, apiKey.trim(), secretKey.trim(), testnet)
- } else if (selectedExchange?.id === 'hyperliquid') {
- if (!apiKey.trim() || !hyperliquidWalletAddr.trim()) return // 验证私钥和钱包地址
- await onSave(
- selectedExchangeId,
- apiKey.trim(),
- '',
- testnet,
- hyperliquidWalletAddr.trim()
- )
- } else if (selectedExchange?.id === 'aster') {
- if (!asterUser.trim() || !asterSigner.trim() || !asterPrivateKey.trim())
- return
- await onSave(
- selectedExchangeId,
- '',
- '',
- testnet,
- undefined,
- asterUser.trim(),
- asterSigner.trim(),
- asterPrivateKey.trim()
- )
- } else if (selectedExchange?.id === 'okx') {
- if (!apiKey.trim() || !secretKey.trim() || !passphrase.trim()) return
- await onSave(selectedExchangeId, apiKey.trim(), secretKey.trim(), testnet)
- } else {
- // 默认情况(其他CEX交易所)
- if (!apiKey.trim() || !secretKey.trim()) return
- await onSave(selectedExchangeId, apiKey.trim(), secretKey.trim(), testnet)
- }
- }
-
- // 可选择的交易所列表(所有支持的交易所)
- const availableExchanges = allExchanges || []
-
- return (
-
-
-
-
- {editingExchangeId
- ? t('editExchange', language)
- : t('addExchange', language)}
-
-
- {selectedExchange?.id === 'binance' && (
-
- )}
- {editingExchangeId && (
-
- )}
-
-
-
-
-
-
- {/* Binance Setup Guide Modal */}
- {showGuide && (
- setShowGuide(false)}
- >
- e.stopPropagation()}
- >
-
-
-
- {t('binanceSetupGuide', language)}
-
-
-
-
- 
-
-
-
- )}
-
- {/* Two Stage Key Modal */}
-
-
- )
-}
diff --git a/web/src/components/ComparisonChart.tsx b/web/src/components/ComparisonChart.tsx
index 554c9b5076..593eb079b7 100644
--- a/web/src/components/ComparisonChart.tsx
+++ b/web/src/components/ComparisonChart.tsx
@@ -141,7 +141,7 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
if (isLoading) {
return (
-
+
Loading comparison data...
@@ -150,7 +150,7 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
if (combinedData.length === 0) {
return (
-
+
{t('noHistoricalData', language)}
@@ -197,11 +197,8 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
if (active && payload && payload.length) {
const data = payload[0].payload
return (
-
-
+
+
{data.time} - #{data.index}
{traders.map((trader) => {
@@ -218,15 +215,13 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
{trader.trader_name}
= 0 ? '#0ECB81' : '#F6465D' }}
+ className={`text-sm mono font-bold ${
+ pnlPct >= 0 ? 'text-emerald-500' : 'text-red-500'
+ }`}
>
{pnlPct >= 0 ? '+' : ''}
{pnlPct.toFixed(2)}%
-
+
({equity?.toFixed(2)} USDT)
@@ -253,27 +248,9 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
return (
-
+
{/* NOFX Watermark */}
-
+
NOFX
@@ -305,13 +282,13 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
))}
-
+
`${value.toFixed(1)}%`}
width={60}
@@ -336,7 +313,7 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
strokeWidth={1.5}
label={{
value: 'Break Even',
- fill: '#848E9C',
+ fill: '#94A3B8',
fontSize: 11,
position: 'right',
}}
@@ -391,75 +368,40 @@ export function ComparisonChart({ traders }: ComparisonChartProps) {
{/* Stats */}
-
-
-
+
+
+
{t('comparisonMode', language)}
-
-
-
+
+
{t('dataPoints', language)}
-
+
{t('count', language, { count: combinedData.length })}
-
-
+
+
{t('currentGap', language)}
1 ? '#F0B90B' : '#EAECEF' }}
+ className={`text-sm md:text-base font-bold mono ${
+ currentGap > 1 ? 'text-darkmoon-gold' : 'text-darkmoon-text-primary'
+ }`}
>
{currentGap.toFixed(2)}%
-
-
+
+
{t('displayRange', language)}
-
+
{combinedData.length > MAX_DISPLAY_POINTS
? `${t('recent', language)} ${MAX_DISPLAY_POINTS}`
: t('allData', language)}
diff --git a/web/src/components/CompetitionPage.tsx b/web/src/components/CompetitionPage.tsx
index e74d119b9e..169bed93e8 100644
--- a/web/src/components/CompetitionPage.tsx
+++ b/web/src/components/CompetitionPage.tsx
@@ -44,20 +44,20 @@ export function CompetitionPage() {
if (!competition) {
return (
-
+
-
@@ -71,35 +71,17 @@ export function CompetitionPage() {
{/* Competition Header - 精简版 */}
-
-
+
+
-
+
{t('aiCompetition', language)}
-
+
0 {t('traders', language)}
-
+
{t('liveBattle', language)}
@@ -107,15 +89,12 @@ export function CompetitionPage() {
{/* Empty State */}
-
-
-
+
+
+
{t('noTraders', language)}
-
+
{t('createFirstTrader', language)}
@@ -136,54 +115,32 @@ export function CompetitionPage() {
{/* Competition Header - 精简版 */}
-
-
+
+
-
+
{t('aiCompetition', language)}
-
+
{competition.count} {t('traders', language)}
-
+
{t('liveBattle', language)}
-
+
{t('leader', language)}
-
+
{leader?.trader_name}
= 0 ? '#0ECB81' : '#F6465D',
- }}
+ className={`text-sm font-semibold ${
+ (leader?.total_pnl ?? 0) >= 0 ? 'text-emerald-500' : 'text-red-500'
+ }`}
>
{(leader?.total_pnl ?? 0) >= 0 ? '+' : ''}
{leader?.total_pnl_pct?.toFixed(2) || '0.00'}%
@@ -194,18 +151,12 @@ export function CompetitionPage() {
{/* Left/Right Split: Performance Chart + Leaderboard */}
{/* Left: Performance Comparison Chart */}
-
+
-
+
{t('performanceComparison', language)}
-
+
{t('realTimePnL', language)}
@@ -213,25 +164,12 @@ export function CompetitionPage() {
{/* Right: Leaderboard */}
-
+
-
+
{t('leaderboard', language)}
-
@@ -247,16 +185,11 @@ export function CompetitionPage() {
handleTraderClick(trader.trader_id)}
- className="rounded p-3 transition-all duration-300 hover:translate-y-[-1px] cursor-pointer hover:shadow-lg"
- style={{
- background: isLeader
- ? 'linear-gradient(135deg, rgba(240, 185, 11, 0.08) 0%, #0B0E11 100%)'
- : '#0B0E11',
- border: `1px solid ${isLeader ? 'rgba(240, 185, 11, 0.4)' : '#2B3139'}`,
- boxShadow: isLeader
- ? '0 3px 15px rgba(240, 185, 11, 0.12), 0 0 0 1px rgba(240, 185, 11, 0.15)'
- : '0 1px 4px rgba(0, 0, 0, 0.3)',
- }}
+ className={`rounded p-3 transition-all duration-300 hover:translate-y-[-1px] cursor-pointer hover:shadow-lg ${
+ isLeader
+ ? 'bg-gradient-to-br from-darkmoon-gold/10 to-darkmoon-surface border border-darkmoon-gold/40 shadow-[0_3px_15px_rgba(240,185,11,0.12),0_0_0_1px_rgba(240,185,11,0.15)]'
+ : 'bg-darkmoon-surface border border-darkmoon-border shadow-sm'
+ }`}
>
{/* Rank & Name */}
@@ -275,10 +208,7 @@ export function CompetitionPage() {
/>
-
+
{trader.trader_name}
{/* Total Equity */}
-
+
{t('equity', language)}
-
+
{trader.total_equity?.toFixed(2) || '0.00'}
{/* P&L */}
-
+
{t('pnl', language)}
= 0
- ? '#0ECB81'
- : '#F6465D',
- }}
+ className={`text-base md:text-lg font-bold mono ${
+ (trader.total_pnl ?? 0) >= 0
+ ? 'text-emerald-500'
+ : 'text-red-500'
+ }`}
>
{(trader.total_pnl ?? 0) >= 0 ? '+' : ''}
{trader.total_pnl_pct?.toFixed(2) || '0.00'}%
-
+
{(trader.total_pnl ?? 0) >= 0 ? '+' : ''}
{trader.total_pnl?.toFixed(2) || '0.00'}
@@ -334,16 +256,13 @@ export function CompetitionPage() {
{/* Positions */}
-
+
{t('pos', language)}
-
+
{trader.position_count}
-
+
{trader.margin_used_pct.toFixed(1)}%
@@ -351,18 +270,11 @@ export function CompetitionPage() {
{/* Status */}
{trader.is_running ? '●' : '○'}
@@ -378,14 +290,8 @@ export function CompetitionPage() {
{/* Head-to-Head Stats */}
{competition.traders.length === 2 && (
-
-
+
+
{t('headToHead', language)}
@@ -407,21 +313,11 @@ export function CompetitionPage() {
return (
= 0 ? '#0ECB81' : '#F6465D',
- }}
+ className={`text-lg md:text-2xl font-bold mono mb-1 ${
+ (trader.total_pnl ?? 0) >= 0
+ ? 'text-emerald-500'
+ : 'text-red-500'
+ }`}
>
{trader.total_pnl_pct != null &&
!isNaN(trader.total_pnl_pct)
@@ -445,28 +341,19 @@ export function CompetitionPage() {
: '—'}
{hasValidData && isWinning && gap > 0 && (
-
+
{t('leadingBy', language, { gap: gap.toFixed(2) })}
)}
{hasValidData && !isWinning && gap < 0 && (
-
+
{t('behindBy', language, {
gap: Math.abs(gap).toFixed(2),
})}
)}
{!hasValidData && (
-
+
—
)}
diff --git a/web/src/components/CryptoFeatureCard.tsx b/web/src/components/CryptoFeatureCard.tsx
index d206d8aee7..1b2e3dc4c6 100644
--- a/web/src/components/CryptoFeatureCard.tsx
+++ b/web/src/components/CryptoFeatureCard.tsx
@@ -33,7 +33,7 @@ export const CryptoFeatureCard = React.forwardRef<
className={cn(
'relative h-full overflow-hidden border-2 transition-all duration-300 rounded-xl',
'bg-gradient-to-br from-[#000000] to-[#0A0A0A]',
- 'border-[#1A1A1A] hover:border-[#F0B90B]/50',
+ 'border-[#1A1A1A] hover:border-darkmoon-gold/50',
isHovered && 'shadow-[0_0_20px_rgba(240,185,11,0.2)]',
className
)}
@@ -46,29 +46,20 @@ export const CryptoFeatureCard = React.forwardRef<
}}
transition={{ duration: 0.3 }}
>
-
+
{/* Background pattern */}
{/* Icon container */}
- {icon}
+ {icon}
{/* Title */}
{title}
{/* Description */}
{description}
@@ -109,18 +98,15 @@ export const CryptoFeatureCard = React.forwardRef<
>
{feature}
diff --git a/web/src/components/Header.tsx b/web/src/components/Header.tsx
index 02b7320f7a..22257c4fb6 100644
--- a/web/src/components/Header.tsx
+++ b/web/src/components/Header.tsx
@@ -10,20 +10,20 @@ export function Header({ simple = false }: HeaderProps) {
const { language, setLanguage } = useLanguage()
return (
-
+
{/* Left - Logo and Title */}
-
- 
+
+
-
- {t('appTitle', language)}
+
+ DarkMoon
{!simple && (
-
+
{t('subtitle', language)}
)}
@@ -31,29 +31,24 @@ export function Header({ simple = false }: HeaderProps) {
{/* Right - Language Toggle (always show) */}
-
+
diff --git a/web/src/components/HeaderBar.tsx b/web/src/components/HeaderBar.tsx
index 7d3bd1c6e0..6f599c8d18 100644
--- a/web/src/components/HeaderBar.tsx
+++ b/web/src/components/HeaderBar.tsx
@@ -60,279 +60,89 @@ export default function HeaderBar({
}
}, [])
+ const NavButton = ({ page, label, onClick }: { page: string, label: string, onClick: () => void }) => (
+
+ )
+
+ const NavLink = ({ page, label, href }: { page: string, label: string, href: string }) => (
+
+ {currentPage === page && (
+
+ )}
+ {label}
+
+ )
+
return (
- |