mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-09 22:10:57 +08:00
feat: add Grok, OpenAI, Claude, Gemini, Kimi AI providers
- Add new MCP clients for Grok (xAI), OpenAI, Claude, Gemini, Kimi - Update auto_trader, backtest, and strategy to support all providers - Add provider icons and fix SVG gradient conflicts - Add API application links and hints in model config modal - Show model version in AI model list cards - Add Chinese/English translations for provider hints - Remove deprecated traders component files
This commit is contained in:
@@ -27,6 +27,7 @@ import {
|
||||
Pencil,
|
||||
Eye,
|
||||
EyeOff,
|
||||
ExternalLink,
|
||||
} from 'lucide-react'
|
||||
import { confirmToast } from '../lib/notify'
|
||||
import { toast } from 'sonner'
|
||||
@@ -51,6 +52,49 @@ function getShortName(fullName: string): string {
|
||||
return parts.length > 1 ? parts[parts.length - 1] : fullName
|
||||
}
|
||||
|
||||
// AI Provider configuration - default models and API links
|
||||
const AI_PROVIDER_CONFIG: Record<string, {
|
||||
defaultModel: string
|
||||
apiUrl: string
|
||||
apiName: string
|
||||
}> = {
|
||||
deepseek: {
|
||||
defaultModel: 'deepseek-chat',
|
||||
apiUrl: 'https://platform.deepseek.com/api_keys',
|
||||
apiName: 'DeepSeek',
|
||||
},
|
||||
qwen: {
|
||||
defaultModel: 'qwen3-max',
|
||||
apiUrl: 'https://dashscope.console.aliyun.com/apiKey',
|
||||
apiName: 'Alibaba Cloud',
|
||||
},
|
||||
openai: {
|
||||
defaultModel: 'gpt-5.1',
|
||||
apiUrl: 'https://platform.openai.com/api-keys',
|
||||
apiName: 'OpenAI',
|
||||
},
|
||||
claude: {
|
||||
defaultModel: 'claude-opus-4-5-20251101',
|
||||
apiUrl: 'https://console.anthropic.com/settings/keys',
|
||||
apiName: 'Anthropic',
|
||||
},
|
||||
gemini: {
|
||||
defaultModel: 'gemini-3-pro-preview',
|
||||
apiUrl: 'https://aistudio.google.com/app/apikey',
|
||||
apiName: 'Google AI Studio',
|
||||
},
|
||||
grok: {
|
||||
defaultModel: 'grok-3-latest',
|
||||
apiUrl: 'https://console.x.ai/',
|
||||
apiName: 'xAI',
|
||||
},
|
||||
kimi: {
|
||||
defaultModel: 'moonshot-v1-auto',
|
||||
apiUrl: 'https://platform.moonshot.ai/console/api-keys',
|
||||
apiName: 'Moonshot',
|
||||
},
|
||||
}
|
||||
|
||||
interface AITradersPageProps {
|
||||
onTraderSelect?: (traderId: string) => void
|
||||
}
|
||||
@@ -815,6 +859,9 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
>
|
||||
{getShortName(model.name)}
|
||||
</div>
|
||||
<div className="text-xs" style={{ color: '#F0B90B' }}>
|
||||
{model.customModelName || AI_PROVIDER_CONFIG[model.provider]?.defaultModel || ''}
|
||||
</div>
|
||||
<div className="text-xs" style={{ color: '#848E9C' }}>
|
||||
{inUse
|
||||
? t('inUse', language)
|
||||
@@ -1362,7 +1409,7 @@ function ModelConfigModal({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex-1">
|
||||
<div className="font-semibold" style={{ color: '#EAECEF' }}>
|
||||
{getShortName(selectedModel.name)}
|
||||
</div>
|
||||
@@ -1371,6 +1418,29 @@ function ModelConfigModal({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Default model info and API link */}
|
||||
{AI_PROVIDER_CONFIG[selectedModel.provider] && (
|
||||
<div className="mt-3 pt-3" style={{ borderTop: '1px solid #2B3139' }}>
|
||||
<div className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
||||
{t('defaultModel', language)}: <span style={{ color: '#F0B90B' }}>{AI_PROVIDER_CONFIG[selectedModel.provider].defaultModel}</span>
|
||||
</div>
|
||||
<a
|
||||
href={AI_PROVIDER_CONFIG[selectedModel.provider].apiUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1.5 text-xs hover:underline"
|
||||
style={{ color: '#F0B90B' }}
|
||||
>
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
{t('applyApiKey', language)} → {AI_PROVIDER_CONFIG[selectedModel.provider].apiName}
|
||||
</a>
|
||||
{selectedModel.provider === 'kimi' && (
|
||||
<div className="mt-2 text-xs p-2 rounded" style={{ background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }}>
|
||||
⚠️ {t('kimiApiNote', language)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1427,13 +1497,13 @@ function ModelConfigModal({
|
||||
className="block text-sm font-semibold mb-2"
|
||||
style={{ color: '#EAECEF' }}
|
||||
>
|
||||
Model Name (可选)
|
||||
{t('customModelName', language)}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={modelName}
|
||||
onChange={(e) => setModelName(e.target.value)}
|
||||
placeholder="例如: deepseek-chat, qwen3-max, gpt-5"
|
||||
placeholder={t('customModelNamePlaceholder', language)}
|
||||
className="w-full px-3 py-2 rounded"
|
||||
style={{
|
||||
background: '#0B0E11',
|
||||
@@ -1442,7 +1512,7 @@ function ModelConfigModal({
|
||||
}}
|
||||
/>
|
||||
<div className="text-xs mt-1" style={{ color: '#848E9C' }}>
|
||||
留空使用默认模型名称
|
||||
{t('leaveBlankForDefaultModel', language)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -4,6 +4,17 @@ interface IconProps {
|
||||
className?: string
|
||||
}
|
||||
|
||||
// AI model colors for fallback display
|
||||
const MODEL_COLORS: Record<string, string> = {
|
||||
deepseek: '#4A90E2',
|
||||
qwen: '#9B59B6',
|
||||
claude: '#D97757',
|
||||
kimi: '#6366F1',
|
||||
gemini: '#4285F4',
|
||||
grok: '#000000',
|
||||
openai: '#10A37F',
|
||||
}
|
||||
|
||||
// 获取AI模型图标的函数
|
||||
export const getModelIcon = (modelType: string, props: IconProps = {}) => {
|
||||
// 支持完整ID或类型名
|
||||
@@ -18,6 +29,21 @@ export const getModelIcon = (modelType: string, props: IconProps = {}) => {
|
||||
case 'qwen':
|
||||
iconPath = '/icons/qwen.svg'
|
||||
break
|
||||
case 'claude':
|
||||
iconPath = '/icons/claude.svg'
|
||||
break
|
||||
case 'kimi':
|
||||
iconPath = '/icons/kimi.svg'
|
||||
break
|
||||
case 'gemini':
|
||||
iconPath = '/icons/gemini.svg'
|
||||
break
|
||||
case 'grok':
|
||||
iconPath = '/icons/grok.svg'
|
||||
break
|
||||
case 'openai':
|
||||
iconPath = '/icons/openai.svg'
|
||||
break
|
||||
default:
|
||||
return null
|
||||
}
|
||||
@@ -29,7 +55,12 @@ export const getModelIcon = (modelType: string, props: IconProps = {}) => {
|
||||
width={props.width || 24}
|
||||
height={props.height || 24}
|
||||
className={props.className}
|
||||
style={{ borderRadius: '50%' }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// 获取模型颜色(用于没有图标时的fallback)
|
||||
export const getModelColor = (modelType: string): string => {
|
||||
const type = modelType.includes('_') ? modelType.split('_').pop() : modelType
|
||||
return MODEL_COLORS[type || ''] || '#60a5fa'
|
||||
}
|
||||
|
||||
@@ -1,298 +0,0 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Trash2 } from 'lucide-react'
|
||||
import { t, type Language } from '../../i18n/translations'
|
||||
import type { AIModel } from '../../types'
|
||||
import { getModelIcon } from '../ModelIcons'
|
||||
import { getShortName } from './utils'
|
||||
|
||||
interface ModelConfigModalProps {
|
||||
allModels: AIModel[]
|
||||
configuredModels: AIModel[]
|
||||
editingModelId: string | null
|
||||
onSave: (
|
||||
modelId: string,
|
||||
apiKey: string,
|
||||
baseUrl?: string,
|
||||
modelName?: string
|
||||
) => Promise<void>
|
||||
onDelete: (modelId: string) => Promise<void>
|
||||
onClose: () => void
|
||||
language: Language
|
||||
}
|
||||
|
||||
export function ModelConfigModal({
|
||||
allModels,
|
||||
configuredModels,
|
||||
editingModelId,
|
||||
onSave,
|
||||
onDelete,
|
||||
onClose,
|
||||
language,
|
||||
}: ModelConfigModalProps) {
|
||||
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 [isSaving, setIsSaving] = useState(false)
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (!selectedModelId || !apiKey.trim() || isSaving) return
|
||||
|
||||
setIsSaving(true)
|
||||
try {
|
||||
await onSave(
|
||||
selectedModelId,
|
||||
apiKey.trim(),
|
||||
baseUrl.trim() || undefined,
|
||||
modelName.trim() || undefined
|
||||
)
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 可选择的模型列表(所有支持的模型)
|
||||
const availableModels = allModels || []
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4 overflow-y-auto">
|
||||
<div
|
||||
className="bg-gray-800 rounded-lg w-full max-w-lg relative my-8"
|
||||
style={{
|
||||
background: '#1E2329',
|
||||
maxHeight: 'calc(100vh - 4rem)',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="flex items-center justify-between p-6 pb-4 sticky top-0 z-10"
|
||||
style={{ background: '#1E2329' }}
|
||||
>
|
||||
<h3 className="text-xl font-bold" style={{ color: '#EAECEF' }}>
|
||||
{editingModelId
|
||||
? t('editAIModel', language)
|
||||
: t('addAIModel', language)}
|
||||
</h3>
|
||||
{editingModelId && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onDelete(editingModelId)}
|
||||
className="p-2 rounded hover:bg-red-100 transition-colors"
|
||||
style={{ background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D' }}
|
||||
title={t('delete', language)}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="px-6 pb-6">
|
||||
<div
|
||||
className="space-y-4 overflow-y-auto"
|
||||
style={{ maxHeight: 'calc(100vh - 16rem)' }}
|
||||
>
|
||||
{!editingModelId && (
|
||||
<div>
|
||||
<label
|
||||
className="block text-sm font-semibold mb-2"
|
||||
style={{ color: '#EAECEF' }}
|
||||
>
|
||||
{t('selectModel', language)}
|
||||
</label>
|
||||
<select
|
||||
value={selectedModelId}
|
||||
onChange={(e) => setSelectedModelId(e.target.value)}
|
||||
className="w-full px-3 py-2 rounded"
|
||||
style={{
|
||||
background: '#0B0E11',
|
||||
border: '1px solid #2B3139',
|
||||
color: '#EAECEF',
|
||||
}}
|
||||
required
|
||||
>
|
||||
<option value="">{t('pleaseSelectModel', language)}</option>
|
||||
{availableModels.map((model) => (
|
||||
<option key={model.id} value={model.id}>
|
||||
{getShortName(model.name)} ({model.provider})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedModel && (
|
||||
<div
|
||||
className="p-4 rounded"
|
||||
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
|
||||
>
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className="w-8 h-8 flex items-center justify-center">
|
||||
{getModelIcon(selectedModel.provider || selectedModel.id, {
|
||||
width: 32,
|
||||
height: 32,
|
||||
}) || (
|
||||
<div
|
||||
className="w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold"
|
||||
style={{
|
||||
background:
|
||||
selectedModel.id === 'deepseek'
|
||||
? '#60a5fa'
|
||||
: '#c084fc',
|
||||
color: '#fff',
|
||||
}}
|
||||
>
|
||||
{selectedModel.name[0]}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-semibold" style={{ color: '#EAECEF' }}>
|
||||
{getShortName(selectedModel.name)}
|
||||
</div>
|
||||
<div className="text-xs" style={{ color: '#848E9C' }}>
|
||||
{selectedModel.provider} • {selectedModel.id}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedModel && (
|
||||
<>
|
||||
<div>
|
||||
<label
|
||||
className="block text-sm font-semibold mb-2"
|
||||
style={{ color: '#EAECEF' }}
|
||||
>
|
||||
API Key
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={apiKey}
|
||||
onChange={(e) => setApiKey(e.target.value)}
|
||||
placeholder={t('enterAPIKey', language)}
|
||||
className="w-full px-3 py-2 rounded"
|
||||
style={{
|
||||
background: '#0B0E11',
|
||||
border: '1px solid #2B3139',
|
||||
color: '#EAECEF',
|
||||
}}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
className="block text-sm font-semibold mb-2"
|
||||
style={{ color: '#EAECEF' }}
|
||||
>
|
||||
{t('customBaseURL', language)}
|
||||
</label>
|
||||
<input
|
||||
type="url"
|
||||
value={baseUrl}
|
||||
onChange={(e) => setBaseUrl(e.target.value)}
|
||||
placeholder={t('customBaseURLPlaceholder', language)}
|
||||
className="w-full px-3 py-2 rounded"
|
||||
style={{
|
||||
background: '#0B0E11',
|
||||
border: '1px solid #2B3139',
|
||||
color: '#EAECEF',
|
||||
}}
|
||||
/>
|
||||
<div className="text-xs mt-1" style={{ color: '#848E9C' }}>
|
||||
{t('leaveBlankForDefault', language)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
className="block text-sm font-semibold mb-2"
|
||||
style={{ color: '#EAECEF' }}
|
||||
>
|
||||
Model Name (可选)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={modelName}
|
||||
onChange={(e) => setModelName(e.target.value)}
|
||||
placeholder="例如: deepseek-chat, qwen3-max, gpt-5"
|
||||
className="w-full px-3 py-2 rounded"
|
||||
style={{
|
||||
background: '#0B0E11',
|
||||
border: '1px solid #2B3139',
|
||||
color: '#EAECEF',
|
||||
}}
|
||||
/>
|
||||
<div className="text-xs mt-1" style={{ color: '#848E9C' }}>
|
||||
留空使用默认模型名称
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="p-4 rounded"
|
||||
style={{
|
||||
background: 'rgba(240, 185, 11, 0.1)',
|
||||
border: '1px solid rgba(240, 185, 11, 0.2)',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="text-sm font-semibold mb-2"
|
||||
style={{ color: '#F0B90B' }}
|
||||
>
|
||||
ℹ️ {t('information', language)}
|
||||
</div>
|
||||
<div
|
||||
className="text-xs space-y-1"
|
||||
style={{ color: '#848E9C' }}
|
||||
>
|
||||
<div>{t('modelConfigInfo1', language)}</div>
|
||||
<div>{t('modelConfigInfo2', language)}</div>
|
||||
<div>{t('modelConfigInfo3', language)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="flex gap-3 mt-6 pt-4 sticky bottom-0"
|
||||
style={{ background: '#1E2329' }}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="flex-1 px-4 py-2 rounded text-sm font-semibold"
|
||||
style={{ background: '#2B3139', color: '#848E9C' }}
|
||||
>
|
||||
{t('cancel', language)}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!selectedModel || !apiKey.trim() || isSaving}
|
||||
className="flex-1 px-4 py-2 rounded text-sm font-semibold disabled:opacity-50"
|
||||
style={{ background: '#F0B90B', color: '#000' }}
|
||||
>
|
||||
{isSaving ? t('saving', language) || '保存中...' : t('saveConfig', language)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export { Tooltip } from './Tooltip'
|
||||
export { ModelConfigModal } from './ModelConfigModal'
|
||||
export { ExchangeConfigModal } from './ExchangeConfigModal'
|
||||
export { getModelDisplayName, getShortName } from './utils'
|
||||
@@ -1,97 +0,0 @@
|
||||
import { Brain } from 'lucide-react'
|
||||
import { t, Language } from '../../../i18n/translations'
|
||||
import { getModelIcon } from '../../ModelIcons'
|
||||
import { getShortName } from '../utils'
|
||||
import type { AIModel } from '../../../types'
|
||||
|
||||
interface AIModelsSectionProps {
|
||||
language: Language
|
||||
configuredModels: AIModel[]
|
||||
isModelInUse: (modelId: string) => boolean
|
||||
onModelClick: (modelId: string) => void
|
||||
}
|
||||
|
||||
export function AIModelsSection({
|
||||
language,
|
||||
configuredModels,
|
||||
isModelInUse,
|
||||
onModelClick,
|
||||
}: AIModelsSectionProps) {
|
||||
return (
|
||||
<div className="binance-card p-3 md:p-4">
|
||||
<h3
|
||||
className="text-base md:text-lg font-semibold mb-3 flex items-center gap-2"
|
||||
style={{ color: '#EAECEF' }}
|
||||
>
|
||||
<Brain className="w-4 h-4 md:w-5 md:h-5" style={{ color: '#60a5fa' }} />
|
||||
{t('aiModels', language)}
|
||||
</h3>
|
||||
<div className="space-y-2 md:space-y-3">
|
||||
{configuredModels.map((model) => {
|
||||
const inUse = isModelInUse(model.id)
|
||||
return (
|
||||
<div
|
||||
key={model.id}
|
||||
className={`flex items-center justify-between p-2 md:p-3 rounded transition-all ${
|
||||
inUse
|
||||
? 'cursor-not-allowed'
|
||||
: 'cursor-pointer hover:bg-gray-700'
|
||||
}`}
|
||||
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
|
||||
onClick={() => onModelClick(model.id)}
|
||||
>
|
||||
<div className="flex items-center gap-2 md:gap-3">
|
||||
<div className="w-7 h-7 md:w-8 md:h-8 flex items-center justify-center flex-shrink-0">
|
||||
{getModelIcon(model.provider || model.id, {
|
||||
width: 28,
|
||||
height: 28,
|
||||
}) || (
|
||||
<div
|
||||
className="w-7 h-7 md:w-8 md:h-8 rounded-full flex items-center justify-center text-xs md:text-sm font-bold"
|
||||
style={{
|
||||
background:
|
||||
model.id === 'deepseek' ? '#60a5fa' : '#c084fc',
|
||||
color: '#fff',
|
||||
}}
|
||||
>
|
||||
{getShortName(model.name)[0]}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div
|
||||
className="font-semibold text-sm md:text-base truncate"
|
||||
style={{ color: '#EAECEF' }}
|
||||
>
|
||||
{getShortName(model.name)}
|
||||
</div>
|
||||
<div className="text-xs" style={{ color: '#848E9C' }}>
|
||||
{inUse
|
||||
? t('inUse', language)
|
||||
: model.enabled
|
||||
? t('enabled', language)
|
||||
: t('configured', language)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`w-2.5 h-2.5 md:w-3 md:h-3 rounded-full flex-shrink-0 ${model.enabled ? 'bg-green-400' : 'bg-gray-500'}`}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{configuredModels.length === 0 && (
|
||||
<div
|
||||
className="text-center py-6 md:py-8"
|
||||
style={{ color: '#848E9C' }}
|
||||
>
|
||||
<Brain className="w-10 h-10 md:w-12 md:h-12 mx-auto mb-2 opacity-50" />
|
||||
<div className="text-xs md:text-sm">
|
||||
{t('noModelsConfigured', language)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
import { Landmark } from 'lucide-react'
|
||||
import { t, type Language } from '../../../i18n/translations'
|
||||
import { getExchangeIcon } from '../../ExchangeIcons'
|
||||
import { getShortName } from '../index'
|
||||
import type { Exchange } from '../../../types'
|
||||
|
||||
interface ExchangesSectionProps {
|
||||
language: Language
|
||||
configuredExchanges: Exchange[]
|
||||
isExchangeInUse: (exchangeId: string) => boolean
|
||||
onExchangeClick: (exchangeId: string) => void
|
||||
}
|
||||
|
||||
export function ExchangesSection({
|
||||
language,
|
||||
configuredExchanges,
|
||||
isExchangeInUse,
|
||||
onExchangeClick,
|
||||
}: ExchangesSectionProps) {
|
||||
return (
|
||||
<div className="binance-card p-3 md:p-4">
|
||||
<h3
|
||||
className="text-base md:text-lg font-semibold mb-3 flex items-center gap-2"
|
||||
style={{ color: '#EAECEF' }}
|
||||
>
|
||||
<Landmark
|
||||
className="w-4 h-4 md:w-5 md:h-5"
|
||||
style={{ color: '#F0B90B' }}
|
||||
/>
|
||||
{t('exchanges', language)}
|
||||
</h3>
|
||||
<div className="space-y-2 md:space-y-3">
|
||||
{configuredExchanges.map((exchange) => {
|
||||
const inUse = isExchangeInUse(exchange.id)
|
||||
return (
|
||||
<div
|
||||
key={exchange.id}
|
||||
className={`flex items-center justify-between p-2 md:p-3 rounded transition-all ${
|
||||
inUse
|
||||
? 'cursor-not-allowed'
|
||||
: 'cursor-pointer hover:bg-gray-700'
|
||||
}`}
|
||||
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
|
||||
onClick={() => onExchangeClick(exchange.id)}
|
||||
>
|
||||
<div className="flex items-center gap-2 md:gap-3">
|
||||
<div className="w-7 h-7 md:w-8 md:h-8 flex items-center justify-center flex-shrink-0">
|
||||
{getExchangeIcon(exchange.exchange_type, { width: 28, height: 28 })}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div
|
||||
className="font-semibold text-sm md:text-base truncate"
|
||||
style={{ color: '#EAECEF' }}
|
||||
>
|
||||
{exchange.exchange_type?.toUpperCase() || getShortName(exchange.name)}
|
||||
<span className="text-xs font-normal ml-1.5" style={{ color: '#F0B90B' }}>
|
||||
- {exchange.account_name || 'Default'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xs" style={{ color: '#848E9C' }}>
|
||||
{exchange.type?.toUpperCase() || 'CEX'} •{' '}
|
||||
{inUse
|
||||
? t('inUse', language)
|
||||
: exchange.enabled
|
||||
? t('enabled', language)
|
||||
: t('configured', language)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`w-2.5 h-2.5 md:w-3 md:h-3 rounded-full flex-shrink-0 ${exchange.enabled ? 'bg-green-400' : 'bg-gray-500'}`}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{configuredExchanges.length === 0 && (
|
||||
<div
|
||||
className="text-center py-6 md:py-8"
|
||||
style={{ color: '#848E9C' }}
|
||||
>
|
||||
<Landmark className="w-10 h-10 md:w-12 md:h-12 mx-auto mb-2 opacity-50" />
|
||||
<div className="text-xs md:text-sm">
|
||||
{t('noExchangesConfigured', language)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
import { Bot, Plus } from 'lucide-react'
|
||||
import { t, type Language } from '../../../i18n/translations'
|
||||
|
||||
interface PageHeaderProps {
|
||||
language: Language
|
||||
tradersCount: number
|
||||
configuredModelsCount: number
|
||||
configuredExchangesCount: number
|
||||
onAddModel: () => void
|
||||
onAddExchange: () => void
|
||||
onCreateTrader: () => void
|
||||
}
|
||||
|
||||
export function PageHeader({
|
||||
language,
|
||||
tradersCount,
|
||||
configuredModelsCount,
|
||||
configuredExchangesCount,
|
||||
onAddModel,
|
||||
onAddExchange,
|
||||
onCreateTrader,
|
||||
}: PageHeaderProps) {
|
||||
const canCreateTrader =
|
||||
configuredModelsCount > 0 && configuredExchangesCount > 0
|
||||
|
||||
return (
|
||||
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-3 md:gap-0">
|
||||
<div className="flex items-center gap-3 md:gap-4">
|
||||
<div
|
||||
className="w-10 h-10 md:w-12 md:h-12 rounded-xl flex items-center justify-center"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)',
|
||||
boxShadow: '0 4px 14px rgba(240, 185, 11, 0.4)',
|
||||
}}
|
||||
>
|
||||
<Bot className="w-5 h-5 md:w-6 md:h-6" style={{ color: '#000' }} />
|
||||
</div>
|
||||
<div>
|
||||
<h1
|
||||
className="text-xl md:text-2xl font-bold flex items-center gap-2"
|
||||
style={{ color: '#EAECEF' }}
|
||||
>
|
||||
{t('aiTraders', language)}
|
||||
<span
|
||||
className="text-xs font-normal px-2 py-1 rounded"
|
||||
style={{
|
||||
background: 'rgba(240, 185, 11, 0.15)',
|
||||
color: '#F0B90B',
|
||||
}}
|
||||
>
|
||||
{tradersCount} {t('active', language)}
|
||||
</span>
|
||||
</h1>
|
||||
<p className="text-xs" style={{ color: '#848E9C' }}>
|
||||
{t('manageAITraders', language)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 md:gap-3 w-full md:w-auto overflow-hidden flex-wrap md:flex-nowrap">
|
||||
<button
|
||||
onClick={onAddModel}
|
||||
className="px-3 md:px-4 py-2 rounded text-xs md:text-sm font-semibold transition-all hover:scale-105 flex items-center gap-1 md:gap-2 whitespace-nowrap"
|
||||
style={{
|
||||
background: '#2B3139',
|
||||
color: '#EAECEF',
|
||||
border: '1px solid #474D57',
|
||||
}}
|
||||
>
|
||||
<Plus className="w-3 h-3 md:w-4 md:h-4" />
|
||||
{t('aiModels', language)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={onAddExchange}
|
||||
className="px-3 md:px-4 py-2 rounded text-xs md:text-sm font-semibold transition-all hover:scale-105 flex items-center gap-1 md:gap-2 whitespace-nowrap"
|
||||
style={{
|
||||
background: '#2B3139',
|
||||
color: '#EAECEF',
|
||||
border: '1px solid #474D57',
|
||||
}}
|
||||
>
|
||||
<Plus className="w-3 h-3 md:w-4 md:h-4" />
|
||||
{t('exchanges', language)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={onCreateTrader}
|
||||
disabled={!canCreateTrader}
|
||||
className="px-3 md:px-4 py-2 rounded text-xs md:text-sm font-semibold transition-all hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1 md:gap-2 whitespace-nowrap"
|
||||
style={{
|
||||
background: canCreateTrader ? '#F0B90B' : '#2B3139',
|
||||
color: canCreateTrader ? '#000' : '#848E9C',
|
||||
}}
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
{t('createTrader', language)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,212 +0,0 @@
|
||||
import { Bot, BarChart3, Trash2, Pencil, Eye, EyeOff } from 'lucide-react'
|
||||
import { t, type Language } from '../../../i18n/translations'
|
||||
import { getModelDisplayName } from '../index'
|
||||
import type { TraderInfo, Exchange } from '../../../types'
|
||||
import { PunkAvatar, getTraderAvatar } from '../../PunkAvatar'
|
||||
|
||||
interface TradersGridProps {
|
||||
language: Language
|
||||
traders: TraderInfo[] | undefined
|
||||
exchanges?: Exchange[]
|
||||
onTraderSelect: (traderId: string) => void
|
||||
onEditTrader: (traderId: string) => void
|
||||
onDeleteTrader: (traderId: string) => void
|
||||
onToggleTrader: (traderId: string, running: boolean) => void
|
||||
onToggleCompetition?: (traderId: string, showInCompetition: boolean) => void
|
||||
}
|
||||
|
||||
export function TradersGrid({
|
||||
language,
|
||||
traders,
|
||||
exchanges = [],
|
||||
onTraderSelect,
|
||||
onEditTrader,
|
||||
onDeleteTrader,
|
||||
onToggleTrader,
|
||||
onToggleCompetition,
|
||||
}: TradersGridProps) {
|
||||
// Helper function to get exchange display name
|
||||
const getExchangeDisplayName = (exchangeId: string | undefined) => {
|
||||
if (!exchangeId) return 'Unknown'
|
||||
const exchange = exchanges.find(e => e.id === exchangeId)
|
||||
if (!exchange) return exchangeId.toUpperCase()
|
||||
const typeName = exchange.exchange_type?.toUpperCase() || exchange.name
|
||||
return exchange.account_name ? `${typeName} - ${exchange.account_name}` : typeName
|
||||
}
|
||||
if (!traders || traders.length === 0) {
|
||||
return (
|
||||
<div className="text-center py-12 md:py-16" style={{ color: '#848E9C' }}>
|
||||
<Bot className="w-16 h-16 md:w-24 md:h-24 mx-auto mb-3 md:mb-4 opacity-50" />
|
||||
<div className="text-base md:text-lg font-semibold mb-2">
|
||||
{t('noTraders', language)}
|
||||
</div>
|
||||
<div className="text-xs md:text-sm mb-3 md:mb-4">
|
||||
{t('createFirstTrader', language)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3 md:space-y-4">
|
||||
{traders.map((trader) => (
|
||||
<div
|
||||
key={trader.trader_id}
|
||||
className="flex flex-col md:flex-row md:items-center justify-between p-3 md:p-4 rounded transition-all hover:translate-y-[-1px] gap-3 md:gap-4"
|
||||
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
|
||||
>
|
||||
<div className="flex items-center gap-3 md:gap-4">
|
||||
<div className="flex-shrink-0">
|
||||
<PunkAvatar
|
||||
seed={getTraderAvatar(trader.trader_id, trader.trader_name)}
|
||||
size={48}
|
||||
className="rounded-lg hidden md:block"
|
||||
/>
|
||||
<PunkAvatar
|
||||
seed={getTraderAvatar(trader.trader_id, trader.trader_name)}
|
||||
size={40}
|
||||
className="rounded-lg md:hidden"
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div
|
||||
className="font-bold text-base md:text-lg truncate"
|
||||
style={{ color: '#EAECEF' }}
|
||||
>
|
||||
{trader.trader_name}
|
||||
</div>
|
||||
<div
|
||||
className="text-xs md:text-sm truncate"
|
||||
style={{
|
||||
color: trader.ai_model.includes('deepseek')
|
||||
? '#60a5fa'
|
||||
: '#c084fc',
|
||||
}}
|
||||
>
|
||||
{getModelDisplayName(
|
||||
trader.ai_model.split('_').pop() || trader.ai_model
|
||||
)}{' '}
|
||||
Model • {getExchangeDisplayName(trader.exchange_id)}
|
||||
<span style={{ color: '#F0B90B' }}> • {trader.strategy_name || 'No Strategy'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 md:gap-4 flex-wrap md:flex-nowrap">
|
||||
{/* Status */}
|
||||
<div className="text-center">
|
||||
<div
|
||||
className={`px-2 md:px-3 py-1 rounded text-xs font-bold ${
|
||||
trader.is_running
|
||||
? 'bg-green-100 text-green-800'
|
||||
: 'bg-red-100 text-red-800'
|
||||
}`}
|
||||
style={
|
||||
trader.is_running
|
||||
? {
|
||||
background: 'rgba(14, 203, 129, 0.1)',
|
||||
color: '#0ECB81',
|
||||
}
|
||||
: {
|
||||
background: 'rgba(246, 70, 93, 0.1)',
|
||||
color: '#F6465D',
|
||||
}
|
||||
}
|
||||
>
|
||||
{trader.is_running
|
||||
? t('running', language)
|
||||
: t('stopped', language)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions: 禁止换行,超出横向滚动 */}
|
||||
<div className="flex gap-1.5 md:gap-2 flex-nowrap overflow-x-auto items-center">
|
||||
<button
|
||||
onClick={() => onTraderSelect(trader.trader_id)}
|
||||
className="px-2 md:px-3 py-1.5 md:py-2 rounded text-xs md:text-sm font-semibold transition-all hover:scale-105 flex items-center gap-1 whitespace-nowrap"
|
||||
style={{
|
||||
background: 'rgba(99, 102, 241, 0.1)',
|
||||
color: '#6366F1',
|
||||
}}
|
||||
>
|
||||
<BarChart3 className="w-3 h-3 md:w-4 md:h-4" />
|
||||
{t('view', language)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => onEditTrader(trader.trader_id)}
|
||||
disabled={trader.is_running}
|
||||
className="px-2 md:px-3 py-1.5 md:py-2 rounded text-xs md:text-sm font-semibold transition-all hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed whitespace-nowrap flex items-center gap-1"
|
||||
style={{
|
||||
background: trader.is_running
|
||||
? 'rgba(132, 142, 156, 0.1)'
|
||||
: 'rgba(255, 193, 7, 0.1)',
|
||||
color: trader.is_running ? '#848E9C' : '#FFC107',
|
||||
}}
|
||||
>
|
||||
<Pencil className="w-3 h-3 md:w-4 md:h-4" />
|
||||
{t('edit', language)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() =>
|
||||
onToggleTrader(trader.trader_id, trader.is_running || false)
|
||||
}
|
||||
className="px-2 md:px-3 py-1.5 md:py-2 rounded text-xs md:text-sm font-semibold transition-all hover:scale-105 whitespace-nowrap"
|
||||
style={
|
||||
trader.is_running
|
||||
? {
|
||||
background: 'rgba(246, 70, 93, 0.1)',
|
||||
color: '#F6465D',
|
||||
}
|
||||
: {
|
||||
background: 'rgba(14, 203, 129, 0.1)',
|
||||
color: '#0ECB81',
|
||||
}
|
||||
}
|
||||
>
|
||||
{trader.is_running ? t('stop', language) : t('start', language)}
|
||||
</button>
|
||||
|
||||
{onToggleCompetition && (
|
||||
<button
|
||||
onClick={() => onToggleCompetition(trader.trader_id, trader.show_in_competition ?? true)}
|
||||
className="px-2 md:px-3 py-1.5 md:py-2 rounded text-xs md:text-sm font-semibold transition-all hover:scale-105 whitespace-nowrap flex items-center gap-1"
|
||||
style={
|
||||
trader.show_in_competition !== false
|
||||
? {
|
||||
background: 'rgba(14, 203, 129, 0.1)',
|
||||
color: '#0ECB81',
|
||||
}
|
||||
: {
|
||||
background: 'rgba(132, 142, 156, 0.1)',
|
||||
color: '#848E9C',
|
||||
}
|
||||
}
|
||||
title={trader.show_in_competition !== false ? '在竞技场显示' : '在竞技场隐藏'}
|
||||
>
|
||||
{trader.show_in_competition !== false ? (
|
||||
<Eye className="w-3 h-3 md:w-4 md:h-4" />
|
||||
) : (
|
||||
<EyeOff className="w-3 h-3 md:w-4 md:h-4" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => onDeleteTrader(trader.trader_id)}
|
||||
className="px-2 md:px-3 py-1.5 md:py-2 rounded text-xs md:text-sm font-semibold transition-all hover:scale-105"
|
||||
style={{
|
||||
background: 'rgba(246, 70, 93, 0.1)',
|
||||
color: '#F6465D',
|
||||
}}
|
||||
>
|
||||
<Trash2 className="w-3 h-3 md:w-4 md:h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -518,10 +518,16 @@ export const translations = {
|
||||
'Custom API base URL, e.g.: https://api.openai.com/v1',
|
||||
leaveBlankForDefault: 'Leave blank to use default API address',
|
||||
modelConfigInfo1:
|
||||
'• API Key will be encrypted and stored, please ensure it is valid',
|
||||
modelConfigInfo2: '• Base URL is used for custom API server address',
|
||||
'• For official API, only API Key is required, leave other fields blank',
|
||||
modelConfigInfo2: '• Custom Base URL and Model Name only needed for third-party proxies',
|
||||
modelConfigInfo3:
|
||||
'• After deleting configuration, traders using this model will not work properly',
|
||||
'• API Key is encrypted and stored securely',
|
||||
defaultModel: 'Default model',
|
||||
applyApiKey: 'Apply API Key',
|
||||
kimiApiNote: 'Kimi requires API Key from international site (moonshot.ai), China region keys are not compatible',
|
||||
leaveBlankForDefaultModel: 'Leave blank to use default model',
|
||||
customModelName: 'Model Name (Optional)',
|
||||
customModelNamePlaceholder: 'e.g.: deepseek-chat, qwen3-max, gpt-4o',
|
||||
saveConfig: 'Save Configuration',
|
||||
editExchange: 'Edit Exchange',
|
||||
addExchange: 'Add Exchange',
|
||||
@@ -1506,9 +1512,15 @@ export const translations = {
|
||||
customBaseURL: 'Base URL (可选)',
|
||||
customBaseURLPlaceholder: '自定义API基础URL,如: https://api.openai.com/v1',
|
||||
leaveBlankForDefault: '留空则使用默认API地址',
|
||||
modelConfigInfo1: '• API Key将被加密存储,请确保密钥有效',
|
||||
modelConfigInfo2: '• Base URL用于自定义API服务器地址',
|
||||
modelConfigInfo3: '• 删除配置后,使用此模型的交易员将无法正常工作',
|
||||
modelConfigInfo1: '• 使用官方 API 时,只需填写 API Key,其他字段留空即可',
|
||||
modelConfigInfo2: '• 自定义 Base URL 和 Model Name 仅在使用第三方代理时需要填写',
|
||||
modelConfigInfo3: '• API Key 加密存储,不会明文展示',
|
||||
defaultModel: '默认模型',
|
||||
applyApiKey: '申请 API Key',
|
||||
kimiApiNote: 'Kimi 需要从国际站申请 API Key (moonshot.ai),中国区 Key 不通用',
|
||||
leaveBlankForDefaultModel: '留空使用默认模型名称',
|
||||
customModelName: 'Model Name (可选)',
|
||||
customModelNamePlaceholder: '例如: deepseek-chat, qwen3-max, gpt-4o',
|
||||
saveConfig: '保存配置',
|
||||
editExchange: '编辑交易所',
|
||||
addExchange: '添加交易所',
|
||||
|
||||
Reference in New Issue
Block a user