mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-07 04:50:57 +08:00
Fix Claw402 autopilot launch and accounting
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom'
|
||||
import useSWR from 'swr'
|
||||
import { api } from '../../lib/api'
|
||||
import type {
|
||||
@@ -32,6 +32,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
const { language } = useLanguage()
|
||||
const { user, token } = useAuth()
|
||||
const navigate = useNavigate()
|
||||
const [searchParams, setSearchParams] = useSearchParams()
|
||||
const [showCreateModal, setShowCreateModal] = useState(false)
|
||||
const [showEditModal, setShowEditModal] = useState(false)
|
||||
const [showModelModal, setShowModelModal] = useState(false)
|
||||
@@ -39,6 +40,10 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
const [showTelegramModal, setShowTelegramModal] = useState(false)
|
||||
const [editingModel, setEditingModel] = useState<string | null>(null)
|
||||
const [editingExchange, setEditingExchange] = useState<string | null>(null)
|
||||
const [initialModelId, setInitialModelId] = useState<string | null>(null)
|
||||
const [initialExchangeType, setInitialExchangeType] = useState<string | null>(
|
||||
null
|
||||
)
|
||||
const [editingTrader, setEditingTrader] = useState<any>(null)
|
||||
const [allModels, setAllModels] = useState<AIModel[]>([])
|
||||
const [allExchanges, setAllExchanges] = useState<Exchange[]>([])
|
||||
@@ -166,10 +171,6 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
return true
|
||||
}) || []
|
||||
|
||||
const isModelInUse = (modelId: string) => {
|
||||
return traders?.some((tr) => tr.ai_model === modelId && tr.is_running)
|
||||
}
|
||||
|
||||
const getModelUsageInfo = (modelId: string) => {
|
||||
const usingTraders = traders?.filter((tr) => tr.ai_model === modelId) || []
|
||||
const runningCount = usingTraders.filter((tr) => tr.is_running).length
|
||||
@@ -177,10 +178,6 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
return { runningCount, totalCount, usingTraders }
|
||||
}
|
||||
|
||||
const isExchangeInUse = (exchangeId: string) => {
|
||||
return traders?.some((tr) => tr.exchange_id === exchangeId && tr.is_running)
|
||||
}
|
||||
|
||||
const getExchangeUsageInfo = (exchangeId: string) => {
|
||||
const usingTraders =
|
||||
traders?.filter((tr) => tr.exchange_id === exchangeId) || []
|
||||
@@ -334,17 +331,15 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
}
|
||||
|
||||
const handleModelClick = (modelId: string) => {
|
||||
if (!isModelInUse(modelId)) {
|
||||
setEditingModel(modelId)
|
||||
setShowModelModal(true)
|
||||
}
|
||||
setInitialModelId(null)
|
||||
setEditingModel(modelId)
|
||||
setShowModelModal(true)
|
||||
}
|
||||
|
||||
const handleExchangeClick = (exchangeId: string) => {
|
||||
if (!isExchangeInUse(exchangeId)) {
|
||||
setEditingExchange(exchangeId)
|
||||
setShowExchangeModal(true)
|
||||
}
|
||||
setInitialExchangeType(null)
|
||||
setEditingExchange(exchangeId)
|
||||
setShowExchangeModal(true)
|
||||
}
|
||||
|
||||
const handleDeleteConfig = async <T extends { id: string }>(config: {
|
||||
@@ -619,15 +614,63 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
}
|
||||
|
||||
const handleAddModel = () => {
|
||||
setInitialModelId(null)
|
||||
setEditingModel(null)
|
||||
setShowModelModal(true)
|
||||
}
|
||||
|
||||
const handleAddExchange = () => {
|
||||
setInitialExchangeType(null)
|
||||
setEditingExchange(null)
|
||||
setShowExchangeModal(true)
|
||||
}
|
||||
|
||||
const handleOpenClaw402Config = () => {
|
||||
const configuredClaw402 = allModels?.find(
|
||||
(model) => model.provider === 'claw402'
|
||||
)
|
||||
const supportedClaw402 = supportedModels?.find(
|
||||
(model) => model.provider === 'claw402'
|
||||
)
|
||||
const modelId = configuredClaw402?.id || supportedClaw402?.id || 'claw402'
|
||||
|
||||
setEditingModel(configuredClaw402?.id || null)
|
||||
setInitialModelId(modelId)
|
||||
setShowModelModal(true)
|
||||
}
|
||||
|
||||
const handleOpenHyperliquidConfig = () => {
|
||||
const existingHyperliquid = allExchanges?.find(
|
||||
(exchange) =>
|
||||
exchange.exchange_type === 'hyperliquid' ||
|
||||
exchange.id === 'hyperliquid'
|
||||
)
|
||||
|
||||
setEditingExchange(existingHyperliquid?.id || null)
|
||||
setInitialExchangeType(existingHyperliquid ? null : 'hyperliquid')
|
||||
setShowExchangeModal(true)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!user || !token) return
|
||||
|
||||
const setupTarget = searchParams.get('setup')
|
||||
if (!setupTarget) return
|
||||
|
||||
if (setupTarget === 'claw402') {
|
||||
if (supportedModels.length === 0 && allModels.length === 0) return
|
||||
handleOpenClaw402Config()
|
||||
} else if (setupTarget === 'hyperliquid') {
|
||||
handleOpenHyperliquidConfig()
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
const nextParams = new URLSearchParams(searchParams)
|
||||
nextParams.delete('setup')
|
||||
setSearchParams(nextParams, { replace: true })
|
||||
}, [allExchanges, allModels, searchParams, setSearchParams, supportedModels, token, user])
|
||||
|
||||
const refreshLaunchState = async () => {
|
||||
await Promise.all([loadConfigs(), mutateTraders()])
|
||||
}
|
||||
@@ -715,9 +758,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
visibleExchangeAddresses={visibleExchangeAddresses}
|
||||
copiedId={copiedId}
|
||||
language={language}
|
||||
isModelInUse={isModelInUse}
|
||||
getModelUsageInfo={getModelUsageInfo}
|
||||
isExchangeInUse={isExchangeInUse}
|
||||
getExchangeUsageInfo={getExchangeUsageInfo}
|
||||
onModelClick={handleModelClick}
|
||||
onExchangeClick={handleExchangeClick}
|
||||
@@ -733,6 +774,8 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
isLoggedIn={Boolean(user && token)}
|
||||
language={language}
|
||||
onRefresh={refreshLaunchState}
|
||||
onOpenClaw402Config={handleOpenClaw402Config}
|
||||
onOpenHyperliquidConfig={handleOpenHyperliquidConfig}
|
||||
/>
|
||||
|
||||
{/* Traders List */}
|
||||
@@ -789,11 +832,13 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
allModels={supportedModels}
|
||||
configuredModels={allModels}
|
||||
editingModelId={editingModel}
|
||||
initialModelId={initialModelId}
|
||||
onSave={handleSaveModelConfig}
|
||||
onDelete={handleDeleteModelConfig}
|
||||
onClose={() => {
|
||||
setShowModelModal(false)
|
||||
setEditingModel(null)
|
||||
setInitialModelId(null)
|
||||
}}
|
||||
language={language}
|
||||
/>
|
||||
@@ -804,11 +849,13 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
|
||||
<ExchangeConfigModal
|
||||
allExchanges={allExchanges}
|
||||
editingExchangeId={editingExchange}
|
||||
initialExchangeType={initialExchangeType}
|
||||
onSave={handleSaveExchangeConfig}
|
||||
onDelete={handleDeleteExchangeConfig}
|
||||
onClose={() => {
|
||||
setShowExchangeModal(false)
|
||||
setEditingExchange(null)
|
||||
setInitialExchangeType(null)
|
||||
}}
|
||||
language={language}
|
||||
/>
|
||||
|
||||
@@ -37,6 +37,8 @@ interface AutopilotLaunchPanelProps {
|
||||
isLoggedIn: boolean
|
||||
language: string
|
||||
onRefresh: () => Promise<void>
|
||||
onOpenClaw402Config?: () => void
|
||||
onOpenHyperliquidConfig?: () => void
|
||||
}
|
||||
|
||||
const MIN_AI_FEE_USDC = 1
|
||||
@@ -186,6 +188,8 @@ export function AutopilotLaunchPanel({
|
||||
isLoggedIn,
|
||||
language,
|
||||
onRefresh,
|
||||
onOpenClaw402Config,
|
||||
onOpenHyperliquidConfig,
|
||||
}: AutopilotLaunchPanelProps) {
|
||||
const navigate = useNavigate()
|
||||
const [wallet, setWallet] = useState<CurrentBeginnerWalletResponse | null>(
|
||||
@@ -371,15 +375,34 @@ export function AutopilotLaunchPanel({
|
||||
? `${shortAddress(feeWalletAddress)} · ${formatUSDC(feeWalletBalance)} USDC`
|
||||
: 'Base USDC wallet required',
|
||||
action: feeWalletAddress ? (
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onOpenClaw402Config?.()}
|
||||
className="inline-flex items-center gap-1.5 text-xs font-semibold text-nofx-gold hover:text-yellow-300"
|
||||
>
|
||||
<CircleDollarSign className="h-3.5 w-3.5" />
|
||||
Deposit
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void copyText(feeWalletAddress, 'AI fee wallet')}
|
||||
className="inline-flex items-center gap-1.5 text-xs font-semibold text-nofx-gold hover:text-yellow-300"
|
||||
>
|
||||
<Copy className="h-3.5 w-3.5" />
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void copyText(feeWalletAddress, 'AI fee wallet')}
|
||||
onClick={() => onOpenClaw402Config?.()}
|
||||
className="inline-flex items-center gap-1.5 text-xs font-semibold text-nofx-gold hover:text-yellow-300"
|
||||
>
|
||||
<Copy className="h-3.5 w-3.5" />
|
||||
Copy
|
||||
<ArrowRight className="h-3.5 w-3.5" />
|
||||
Open
|
||||
</button>
|
||||
) : undefined,
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Hyperliquid trading wallet',
|
||||
@@ -389,6 +412,16 @@ export function AutopilotLaunchPanel({
|
||||
meta: hyperliquidExchange?.hyperliquidWalletAddr
|
||||
? `${shortAddress(hyperliquidExchange.hyperliquidWalletAddr)} · authorized`
|
||||
: 'Agent and trading authorization required',
|
||||
action: (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onOpenHyperliquidConfig?.()}
|
||||
className="inline-flex items-center gap-1.5 text-xs font-semibold text-nofx-gold hover:text-yellow-300"
|
||||
>
|
||||
<Wallet className="h-3.5 w-3.5" />
|
||||
Open
|
||||
</button>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Trading balance',
|
||||
@@ -421,10 +454,16 @@ export function AutopilotLaunchPanel({
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate(ROUTES.welcome)}
|
||||
onClick={() => {
|
||||
if (onOpenClaw402Config) {
|
||||
onOpenClaw402Config()
|
||||
} else {
|
||||
navigate(ROUTES.welcome)
|
||||
}
|
||||
}}
|
||||
className="inline-flex items-center justify-center gap-2 rounded-lg bg-nofx-gold px-4 py-3 text-sm font-bold text-black hover:bg-yellow-400"
|
||||
>
|
||||
Prepare AI fee wallet
|
||||
Open Claw402 wallet
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
</button>
|
||||
)
|
||||
@@ -435,9 +474,13 @@ export function AutopilotLaunchPanel({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
document
|
||||
.getElementById('hyperliquid-quick-connect')
|
||||
?.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
if (onOpenHyperliquidConfig) {
|
||||
onOpenHyperliquidConfig()
|
||||
} else {
|
||||
document
|
||||
.getElementById('hyperliquid-quick-connect')
|
||||
?.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
}
|
||||
}}
|
||||
className="inline-flex items-center justify-center gap-2 rounded-lg bg-nofx-gold px-4 py-3 text-sm font-bold text-black hover:bg-yellow-400"
|
||||
>
|
||||
|
||||
@@ -30,9 +30,7 @@ interface ConfigStatusGridProps {
|
||||
visibleExchangeAddresses: Set<string>
|
||||
copiedId: string | null
|
||||
language: Language
|
||||
isModelInUse: (modelId: string) => boolean | undefined
|
||||
getModelUsageInfo: (modelId: string) => UsageInfo
|
||||
isExchangeInUse: (exchangeId: string) => boolean | undefined
|
||||
getExchangeUsageInfo: (exchangeId: string) => UsageInfo
|
||||
onModelClick: (modelId: string) => void
|
||||
onExchangeClick: (exchangeId: string) => void
|
||||
@@ -48,9 +46,7 @@ export function ConfigStatusGrid({
|
||||
visibleExchangeAddresses,
|
||||
copiedId,
|
||||
language,
|
||||
isModelInUse,
|
||||
getModelUsageInfo,
|
||||
isExchangeInUse,
|
||||
getExchangeUsageInfo,
|
||||
onModelClick,
|
||||
onExchangeClick,
|
||||
@@ -112,14 +108,20 @@ export function ConfigStatusGrid({
|
||||
|
||||
<div className="p-4 space-y-3">
|
||||
{configuredModels.map((model) => {
|
||||
const inUse = isModelInUse(model.id)
|
||||
const usageInfo = getModelUsageInfo(model.id)
|
||||
return (
|
||||
<div
|
||||
key={model.id}
|
||||
className={`group relative flex items-center justify-between p-3 rounded-md transition-all border border-transparent ${inUse ? 'opacity-80' : 'hover:bg-white/5 hover:border-white/10 cursor-pointer'
|
||||
} bg-black/20`}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="group relative flex cursor-pointer items-center justify-between rounded-md border border-transparent bg-black/20 p-3 transition-all hover:border-white/10 hover:bg-white/5"
|
||||
onClick={() => onModelClick(model.id)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault()
|
||||
onModelClick(model.id)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="relative">
|
||||
@@ -193,16 +195,22 @@ export function ConfigStatusGrid({
|
||||
|
||||
<div className="p-4 space-y-3">
|
||||
{configuredExchanges.map((exchange) => {
|
||||
const inUse = isExchangeInUse(exchange.id)
|
||||
const usageInfo = getExchangeUsageInfo(exchange.id)
|
||||
const state = exchangeAccountStates?.[exchange.id]
|
||||
const stateMeta = getExchangeStateMeta(state)
|
||||
return (
|
||||
<div
|
||||
key={exchange.id}
|
||||
className={`group relative flex items-center justify-between p-3 rounded-md transition-all border border-transparent ${inUse ? 'opacity-80' : 'hover:bg-white/5 hover:border-white/10 cursor-pointer'
|
||||
} bg-black/20`}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="group relative flex cursor-pointer items-center justify-between rounded-md border border-transparent bg-black/20 p-3 transition-all hover:border-white/10 hover:bg-white/5"
|
||||
onClick={() => onExchangeClick(exchange.id)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault()
|
||||
onExchangeClick(exchange.id)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-4 min-w-0">
|
||||
<div className="relative">
|
||||
|
||||
@@ -38,6 +38,7 @@ const SUPPORTED_EXCHANGE_TEMPLATES = [
|
||||
interface ExchangeConfigModalProps {
|
||||
allExchanges: Exchange[]
|
||||
editingExchangeId: string | null
|
||||
initialExchangeType?: string | null
|
||||
onSave: (
|
||||
exchangeId: string | null,
|
||||
exchangeType: string,
|
||||
@@ -148,6 +149,7 @@ function ExchangeCard({
|
||||
export function ExchangeConfigModal({
|
||||
allExchanges,
|
||||
editingExchangeId,
|
||||
initialExchangeType,
|
||||
onSave,
|
||||
onDelete,
|
||||
onClose,
|
||||
@@ -155,8 +157,12 @@ export function ExchangeConfigModal({
|
||||
}: ExchangeConfigModalProps) {
|
||||
const { user } = useAuth()
|
||||
// Step: 0 = select exchange, 1 = configure
|
||||
const [currentStep, setCurrentStep] = useState(editingExchangeId ? 1 : 0)
|
||||
const [selectedExchangeType, setSelectedExchangeType] = useState('')
|
||||
const [currentStep, setCurrentStep] = useState(
|
||||
editingExchangeId || initialExchangeType ? 1 : 0
|
||||
)
|
||||
const [selectedExchangeType, setSelectedExchangeType] = useState(
|
||||
initialExchangeType || ''
|
||||
)
|
||||
const [apiKey, setApiKey] = useState('')
|
||||
const [secretKey, setSecretKey] = useState('')
|
||||
const [passphrase, setPassphrase] = useState('')
|
||||
|
||||
@@ -18,6 +18,7 @@ interface ModelConfigModalProps {
|
||||
allModels: AIModel[]
|
||||
configuredModels: AIModel[]
|
||||
editingModelId: string | null
|
||||
initialModelId?: string | null
|
||||
onSave: (
|
||||
modelId: string,
|
||||
apiKey: string,
|
||||
@@ -33,13 +34,18 @@ export function ModelConfigModal({
|
||||
allModels,
|
||||
configuredModels,
|
||||
editingModelId,
|
||||
initialModelId,
|
||||
onSave,
|
||||
onDelete,
|
||||
onClose,
|
||||
language,
|
||||
}: ModelConfigModalProps) {
|
||||
const [currentStep, setCurrentStep] = useState(editingModelId ? 1 : 0)
|
||||
const [selectedModelId, setSelectedModelId] = useState(editingModelId || '')
|
||||
const [currentStep, setCurrentStep] = useState(
|
||||
editingModelId || initialModelId ? 1 : 0
|
||||
)
|
||||
const [selectedModelId, setSelectedModelId] = useState(
|
||||
editingModelId || initialModelId || ''
|
||||
)
|
||||
const [apiKey, setApiKey] = useState('')
|
||||
const [baseUrl, setBaseUrl] = useState('')
|
||||
const [modelName, setModelName] = useState('')
|
||||
@@ -75,12 +81,20 @@ export function ModelConfigModal({
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (!selectedModelId || !apiKey.trim()) return
|
||||
onSave(selectedModelId, apiKey.trim(), baseUrl.trim() || undefined, modelName.trim() || undefined)
|
||||
onSave(
|
||||
selectedModelId,
|
||||
apiKey.trim(),
|
||||
baseUrl.trim() || undefined,
|
||||
modelName.trim() || undefined
|
||||
)
|
||||
}
|
||||
|
||||
const availableModels = allModels || []
|
||||
const configuredIds = new Set(configuredModels?.map(m => m.id) || [])
|
||||
const stepLabels = [t('modelConfig.selectModel', language), t('modelConfig.configureApi', language)]
|
||||
const configuredIds = new Set(configuredModels?.map((m) => m.id) || [])
|
||||
const stepLabels = [
|
||||
t('modelConfig.selectModel', language),
|
||||
t('modelConfig.configureApi', language),
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 p-4 overflow-y-auto backdrop-blur-sm">
|
||||
@@ -168,18 +182,23 @@ export function ModelConfigModal({
|
||||
)}
|
||||
|
||||
{/* Step 1: Configure — Claw402 Dedicated UI */}
|
||||
{(currentStep === 1 || editingModelId) && selectedModel && (selectedModel.provider === 'claw402' || selectedModel.id === 'claw402') && (
|
||||
<Claw402ConfigForm
|
||||
apiKey={apiKey}
|
||||
modelName={modelName}
|
||||
editingModelId={editingModelId}
|
||||
onApiKeyChange={setApiKey}
|
||||
onModelNameChange={setModelName}
|
||||
onBack={handleBack}
|
||||
onSubmit={handleSubmit}
|
||||
language={language}
|
||||
/>
|
||||
)}
|
||||
{(currentStep === 1 || editingModelId) &&
|
||||
selectedModel &&
|
||||
(selectedModel.provider === 'claw402' ||
|
||||
selectedModel.id === 'claw402') && (
|
||||
<Claw402ConfigForm
|
||||
apiKey={apiKey}
|
||||
modelName={modelName}
|
||||
editingModelId={editingModelId}
|
||||
initialWalletAddress={selectedModel.walletAddress}
|
||||
initialBalanceUsdc={selectedModel.balanceUsdc}
|
||||
onApiKeyChange={setApiKey}
|
||||
onModelNameChange={setModelName}
|
||||
onBack={handleBack}
|
||||
onSubmit={handleSubmit}
|
||||
language={language}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Step 1: Configure — Standard Providers (non-claw402) */}
|
||||
{(currentStep === 1 || editingModelId) &&
|
||||
@@ -228,11 +247,11 @@ function ModelSelectionStep({
|
||||
</div>
|
||||
|
||||
{/* Claw402 Featured Card */}
|
||||
{availableModels.some(m => m.provider === 'claw402') && (
|
||||
{availableModels.some((m) => m.provider === 'claw402') && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const claw = availableModels.find(m => m.provider === 'claw402')
|
||||
const claw = availableModels.find((m) => m.provider === 'claw402')
|
||||
if (claw) onSelectModel(claw.id)
|
||||
}}
|
||||
className="w-full p-5 rounded-xl text-left transition-all hover:scale-[1.01]"
|
||||
@@ -278,8 +297,13 @@ function ModelSelectionStep({
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{configuredIds.has(availableModels.find(m => m.provider === 'claw402')?.id || '') && (
|
||||
<div className="w-2 h-2 rounded-full" style={{ background: '#00E096' }} />
|
||||
{configuredIds.has(
|
||||
availableModels.find((m) => m.provider === 'claw402')?.id || ''
|
||||
) && (
|
||||
<div
|
||||
className="w-2 h-2 rounded-full"
|
||||
style={{ background: '#00E096' }}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className="px-3 py-1.5 rounded-full text-xs font-bold"
|
||||
@@ -308,35 +332,45 @@ function ModelSelectionStep({
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-3 sm:grid-cols-4 gap-3">
|
||||
{availableModels.filter(m => !m.provider?.startsWith('blockrun') && m.provider !== 'claw402').map((model) => (
|
||||
<ModelCard
|
||||
key={model.id}
|
||||
model={model}
|
||||
selected={selectedModelId === model.id}
|
||||
onClick={() => onSelectModel(model.id)}
|
||||
configured={configuredIds.has(model.id)}
|
||||
/>
|
||||
))}
|
||||
{availableModels
|
||||
.filter(
|
||||
(m) =>
|
||||
!m.provider?.startsWith('blockrun') && m.provider !== 'claw402'
|
||||
)
|
||||
.map((model) => (
|
||||
<ModelCard
|
||||
key={model.id}
|
||||
model={model}
|
||||
selected={selectedModelId === model.id}
|
||||
onClick={() => onSelectModel(model.id)}
|
||||
configured={configuredIds.has(model.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{availableModels.some(m => m.provider?.startsWith('blockrun')) && (
|
||||
{availableModels.some((m) => m.provider?.startsWith('blockrun')) && (
|
||||
<>
|
||||
<div className="flex items-center gap-3 pt-2">
|
||||
<div className="flex-1 h-px" style={{ background: '#2B3139' }} />
|
||||
<span className="text-xs font-medium px-2" style={{ color: '#848E9C' }}>
|
||||
<span
|
||||
className="text-xs font-medium px-2"
|
||||
style={{ color: '#848E9C' }}
|
||||
>
|
||||
{t('modelConfig.viaBlockrunWallet', language)}
|
||||
</span>
|
||||
<div className="flex-1 h-px" style={{ background: '#2B3139' }} />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{availableModels.filter(m => m.provider?.startsWith('blockrun')).map((model) => (
|
||||
<ModelCard
|
||||
key={model.id}
|
||||
model={model}
|
||||
selected={selectedModelId === model.id}
|
||||
onClick={() => onSelectModel(model.id)}
|
||||
configured={configuredIds.has(model.id)}
|
||||
/>
|
||||
))}
|
||||
{availableModels
|
||||
.filter((m) => m.provider?.startsWith('blockrun'))
|
||||
.map((model) => (
|
||||
<ModelCard
|
||||
key={model.id}
|
||||
model={model}
|
||||
selected={selectedModelId === model.id}
|
||||
onClick={() => onSelectModel(model.id)}
|
||||
configured={configuredIds.has(model.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
@@ -351,6 +385,8 @@ function Claw402ConfigForm({
|
||||
apiKey,
|
||||
modelName,
|
||||
editingModelId,
|
||||
initialWalletAddress,
|
||||
initialBalanceUsdc,
|
||||
onApiKeyChange,
|
||||
onModelNameChange,
|
||||
onBack,
|
||||
@@ -360,18 +396,22 @@ function Claw402ConfigForm({
|
||||
apiKey: string
|
||||
modelName: string
|
||||
editingModelId: string | null
|
||||
initialWalletAddress?: string
|
||||
initialBalanceUsdc?: string
|
||||
onApiKeyChange: (value: string) => void
|
||||
onModelNameChange: (value: string) => void
|
||||
onBack: () => void
|
||||
onSubmit: (e: React.FormEvent) => void
|
||||
language: Language
|
||||
}) {
|
||||
const [walletAddress, setWalletAddress] = useState('')
|
||||
const [walletAddress, setWalletAddress] = useState(initialWalletAddress || '')
|
||||
const [copiedAddr, setCopiedAddr] = useState(false)
|
||||
const [showDeposit, setShowDeposit] = useState(false)
|
||||
const [showDeposit, setShowDeposit] = useState(Boolean(initialWalletAddress))
|
||||
const [showNewWalletBackup, setShowNewWalletBackup] = useState(false)
|
||||
const [newWalletKey, setNewWalletKey] = useState('')
|
||||
const [usdcBalance, setUsdcBalance] = useState<string | null>(null)
|
||||
const [usdcBalance, setUsdcBalance] = useState<string | null>(
|
||||
initialBalanceUsdc || null
|
||||
)
|
||||
const [keyError, setKeyError] = useState('')
|
||||
const [validating, setValidating] = useState(false)
|
||||
const [claw402Status, setClaw402Status] = useState<string | null>(null)
|
||||
@@ -400,17 +440,25 @@ function Claw402ConfigForm({
|
||||
|
||||
// Truncate address for display
|
||||
|
||||
|
||||
// Debounced validation when apiKey changes
|
||||
useEffect(() => {
|
||||
setWalletAddress('')
|
||||
setUsdcBalance(null)
|
||||
setClaw402Status(null)
|
||||
setTestResult(null)
|
||||
|
||||
const clientErr = getClientError(apiKey)
|
||||
setKeyError(clientErr)
|
||||
|
||||
if (!apiKey) {
|
||||
setWalletAddress(initialWalletAddress || '')
|
||||
setUsdcBalance(initialBalanceUsdc || null)
|
||||
setShowDeposit(Boolean(initialWalletAddress))
|
||||
setValidating(false)
|
||||
return
|
||||
}
|
||||
|
||||
setWalletAddress('')
|
||||
setUsdcBalance(null)
|
||||
|
||||
if (clientErr || !apiKey) {
|
||||
setValidating(false)
|
||||
return
|
||||
@@ -441,7 +489,7 @@ function Claw402ConfigForm({
|
||||
}, 500)
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
}, [apiKey])
|
||||
}, [apiKey, initialBalanceUsdc, initialWalletAddress, language])
|
||||
|
||||
const handleTestConnection = async () => {
|
||||
setTesting(true)
|
||||
@@ -666,24 +714,33 @@ function Claw402ConfigForm({
|
||||
: '1px solid #2B3139',
|
||||
color: '#EAECEF',
|
||||
}}
|
||||
required
|
||||
required={!walletAddress}
|
||||
/>
|
||||
{!apiKey && (
|
||||
{!apiKey && !walletAddress && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
try {
|
||||
const res = await fetch('/api/wallet/generate', { method: 'POST' })
|
||||
const res = await fetch('/api/wallet/generate', {
|
||||
method: 'POST',
|
||||
})
|
||||
const data = await res.json()
|
||||
if (data.private_key) {
|
||||
onApiKeyChange(data.private_key)
|
||||
setShowNewWalletBackup(true)
|
||||
setNewWalletKey(data.private_key)
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}}
|
||||
className="shrink-0 px-3 py-3 rounded-xl text-xs font-semibold transition-all hover:scale-[1.02]"
|
||||
style={{ background: 'linear-gradient(135deg, #2563EB, #7C3AED)', color: '#fff', border: 'none', cursor: 'pointer' }}
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #2563EB, #7C3AED)',
|
||||
color: '#fff',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
{language === 'zh' ? '🔑 创建钱包' : '🔑 Create Wallet'}
|
||||
</button>
|
||||
@@ -692,9 +749,21 @@ function Claw402ConfigForm({
|
||||
|
||||
{/* New wallet backup warning */}
|
||||
{showNewWalletBackup && newWalletKey && (
|
||||
<div className="p-3 rounded-xl" style={{ background: 'rgba(239, 68, 68, 0.08)', border: '1px solid rgba(239, 68, 68, 0.3)' }}>
|
||||
<div className="text-xs font-bold mb-2" style={{ color: '#EF4444' }}>
|
||||
🚨 {language === 'zh' ? '重要:请立即备份私钥!' : 'Important: Backup your private key NOW!'}
|
||||
<div
|
||||
className="p-3 rounded-xl"
|
||||
style={{
|
||||
background: 'rgba(239, 68, 68, 0.08)',
|
||||
border: '1px solid rgba(239, 68, 68, 0.3)',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="text-xs font-bold mb-2"
|
||||
style={{ color: '#EF4444' }}
|
||||
>
|
||||
🚨{' '}
|
||||
{language === 'zh'
|
||||
? '重要:请立即备份私钥!'
|
||||
: 'Important: Backup your private key NOW!'}
|
||||
</div>
|
||||
<div className="text-[11px] mb-2" style={{ color: '#F87171' }}>
|
||||
{language === 'zh'
|
||||
@@ -702,7 +771,10 @@ function Claw402ConfigForm({
|
||||
: 'This is your wallet private key. If lost, it cannot be recovered and all assets will be permanently lost. Copy and save it securely.'}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<code className="text-[10px] font-mono break-all select-all flex-1 p-2 rounded" style={{ background: '#0B0E11', color: '#F87171' }}>
|
||||
<code
|
||||
className="text-[10px] font-mono break-all select-all flex-1 p-2 rounded"
|
||||
style={{ background: '#0B0E11', color: '#F87171' }}
|
||||
>
|
||||
{newWalletKey}
|
||||
</code>
|
||||
<button
|
||||
@@ -713,15 +785,38 @@ function Claw402ConfigForm({
|
||||
setTimeout(() => setCopiedAddr(false), 2000)
|
||||
}}
|
||||
className="shrink-0 text-[10px] px-2 py-1 rounded"
|
||||
style={{ background: 'rgba(239,68,68,0.15)', color: '#F87171', border: 'none', cursor: 'pointer' }}
|
||||
style={{
|
||||
background: 'rgba(239,68,68,0.15)',
|
||||
color: '#F87171',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
{copiedAddr ? '✅ Copied' : '📋 Copy Key'}
|
||||
</button>
|
||||
</div>
|
||||
<div className="text-[10px] space-y-1" style={{ color: '#848E9C' }}>
|
||||
<div>✅ {language === 'zh' ? '建议保存到密码管理器(1Password / Bitwarden)' : 'Save to a password manager (1Password / Bitwarden)'}</div>
|
||||
<div>✅ {language === 'zh' ? '或抄在纸上放安全的地方' : 'Or write it down and store it safely'}</div>
|
||||
<div>❌ {language === 'zh' ? '不要截图发给别人' : 'Do NOT screenshot or share with anyone'}</div>
|
||||
<div
|
||||
className="text-[10px] space-y-1"
|
||||
style={{ color: '#848E9C' }}
|
||||
>
|
||||
<div>
|
||||
✅{' '}
|
||||
{language === 'zh'
|
||||
? '建议保存到密码管理器(1Password / Bitwarden)'
|
||||
: 'Save to a password manager (1Password / Bitwarden)'}
|
||||
</div>
|
||||
<div>
|
||||
✅{' '}
|
||||
{language === 'zh'
|
||||
? '或抄在纸上放安全的地方'
|
||||
: 'Or write it down and store it safely'}
|
||||
</div>
|
||||
<div>
|
||||
❌{' '}
|
||||
{language === 'zh'
|
||||
? '不要截图发给别人'
|
||||
: 'Do NOT screenshot or share with anyone'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -736,7 +831,7 @@ function Claw402ConfigForm({
|
||||
</div>
|
||||
|
||||
{/* Wallet Validation Results */}
|
||||
{apiKey && (
|
||||
{(apiKey || walletAddress) && (
|
||||
<div className="space-y-2 pl-1">
|
||||
{/* Validating spinner */}
|
||||
{validating && (
|
||||
@@ -792,15 +887,28 @@ function Claw402ConfigForm({
|
||||
{copiedAddr ? '✅' : '📋'}
|
||||
</button>
|
||||
</div>
|
||||
<code className="text-[11px] font-mono block select-all" style={{ color: '#60A5FA' }}>{walletAddress}</code>
|
||||
<div className="text-[10px] mt-1.5" style={{ color: '#F59E0B' }}>
|
||||
⚠️ {language === 'zh' ? '请确认这是你的钱包地址(可在 MetaMask 中核对)' : 'Please confirm this is your wallet address (verify in MetaMask)'}
|
||||
<code
|
||||
className="text-[11px] font-mono block select-all"
|
||||
style={{ color: '#60A5FA' }}
|
||||
>
|
||||
{walletAddress}
|
||||
</code>
|
||||
<div
|
||||
className="text-[10px] mt-1.5"
|
||||
style={{ color: '#F59E0B' }}
|
||||
>
|
||||
⚠️{' '}
|
||||
{language === 'zh'
|
||||
? '请确认这是你的钱包地址(可在 MetaMask 中核对)'
|
||||
: 'Please confirm this is your wallet address (verify in MetaMask)'}
|
||||
</div>
|
||||
</div>
|
||||
{usdcBalance !== null && (
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<span>💰</span>
|
||||
<span style={{ color: balanceNum > 0 ? '#00E096' : '#F59E0B' }}>
|
||||
<span
|
||||
style={{ color: balanceNum > 0 ? '#00E096' : '#F59E0B' }}
|
||||
>
|
||||
{t('modelConfig.usdcBalance', language)}: ${usdcBalance}
|
||||
</span>
|
||||
<button
|
||||
@@ -842,7 +950,10 @@ function Claw402ConfigForm({
|
||||
: 'Deposit USDC (Base Chain)'}
|
||||
</div>
|
||||
<div className="flex gap-3 items-start mb-3">
|
||||
<div className="shrink-0 p-1.5 rounded-lg" style={{ background: '#fff' }}>
|
||||
<div
|
||||
className="shrink-0 p-1.5 rounded-lg"
|
||||
style={{ background: '#fff' }}
|
||||
>
|
||||
<QRCodeSVG value={walletAddress} size={80} level="M" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
@@ -854,7 +965,12 @@ function Claw402ConfigForm({
|
||||
? '扫码或复制地址转账'
|
||||
: 'Scan QR or copy address to transfer'}
|
||||
</div>
|
||||
<code className="text-[10px] font-mono break-all select-all block mb-1.5" style={{ color: '#60A5FA' }}>{walletAddress}</code>
|
||||
<code
|
||||
className="text-[10px] font-mono break-all select-all block mb-1.5"
|
||||
style={{ color: '#60A5FA' }}
|
||||
>
|
||||
{walletAddress}
|
||||
</code>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
@@ -1015,7 +1131,12 @@ function Claw402ConfigForm({
|
||||
type="submit"
|
||||
disabled={!isKeyValid}
|
||||
className="flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-xl text-sm font-bold transition-all hover:scale-[1.02] disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
style={{ background: isKeyValid ? 'linear-gradient(135deg, #2563EB, #7C3AED)' : '#2B3139', color: '#fff' }}
|
||||
style={{
|
||||
background: isKeyValid
|
||||
? 'linear-gradient(135deg, #2563EB, #7C3AED)'
|
||||
: '#2B3139',
|
||||
color: '#fff',
|
||||
}}
|
||||
>
|
||||
{'🚀 ' + t('modelConfig.startTrading', language)}
|
||||
</button>
|
||||
@@ -1119,9 +1240,14 @@ function StandardProviderConfigForm({
|
||||
{editingModelId && selectedModel && 'has_api_key' in selectedModel && (
|
||||
<div
|
||||
className="p-3 rounded-xl text-xs"
|
||||
style={{ background: 'rgba(14, 203, 129, 0.08)', border: '1px solid rgba(14, 203, 129, 0.2)', color: '#9FE8C5' }}
|
||||
style={{
|
||||
background: 'rgba(14, 203, 129, 0.08)',
|
||||
border: '1px solid rgba(14, 203, 129, 0.2)',
|
||||
color: '#9FE8C5',
|
||||
}}
|
||||
>
|
||||
当前模型密钥状态:{selectedModel.has_api_key ? '已配置 API Key' : '未配置 API Key'}
|
||||
当前模型密钥状态:
|
||||
{selectedModel.has_api_key ? '已配置 API Key' : '未配置 API Key'}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1156,10 +1282,10 @@ function StandardProviderConfigForm({
|
||||
editingModelId && selectedModel.has_api_key
|
||||
? '已保存,如需更换请重新输入'
|
||||
: selectedModel.provider === 'blockrun-base'
|
||||
? '0x... (EVM private key)'
|
||||
: selectedModel.provider === 'blockrun-sol'
|
||||
? 'bs58 encoded key (Solana)'
|
||||
: t('enterAPIKey', language)
|
||||
? '0x... (EVM private key)'
|
||||
: selectedModel.provider === 'blockrun-sol'
|
||||
? 'bs58 encoded key (Solana)'
|
||||
: t('enterAPIKey', language)
|
||||
}
|
||||
className="w-full px-4 py-3 rounded-xl"
|
||||
style={{
|
||||
@@ -1174,9 +1300,23 @@ function StandardProviderConfigForm({
|
||||
{/* Custom Base URL (hidden for BlockRun) */}
|
||||
{!selectedModel.provider?.startsWith('blockrun') && (
|
||||
<div className="space-y-2">
|
||||
<label className="flex items-center gap-2 text-sm font-semibold" style={{ color: '#EAECEF' }}>
|
||||
<svg className="w-4 h-4" style={{ color: '#A78BFA' }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
|
||||
<label
|
||||
className="flex items-center gap-2 text-sm font-semibold"
|
||||
style={{ color: '#EAECEF' }}
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4"
|
||||
style={{ color: '#A78BFA' }}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"
|
||||
/>
|
||||
</svg>
|
||||
{t('customBaseURL', language)}
|
||||
</label>
|
||||
@@ -1186,7 +1326,11 @@ function StandardProviderConfigForm({
|
||||
onChange={(e) => onBaseUrlChange(e.target.value)}
|
||||
placeholder={t('customBaseURLPlaceholder', language)}
|
||||
className="w-full px-4 py-3 rounded-xl"
|
||||
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
|
||||
style={{
|
||||
background: '#0B0E11',
|
||||
border: '1px solid #2B3139',
|
||||
color: '#EAECEF',
|
||||
}}
|
||||
/>
|
||||
<div className="text-xs" style={{ color: '#848E9C' }}>
|
||||
{t('leaveBlankForDefault', language)}
|
||||
@@ -1197,9 +1341,23 @@ function StandardProviderConfigForm({
|
||||
{/* Custom Model Name (hidden for BlockRun) */}
|
||||
{!selectedModel.provider?.startsWith('blockrun') && (
|
||||
<div className="space-y-2">
|
||||
<label className="flex items-center gap-2 text-sm font-semibold" style={{ color: '#EAECEF' }}>
|
||||
<svg className="w-4 h-4" style={{ color: '#A78BFA' }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
|
||||
<label
|
||||
className="flex items-center gap-2 text-sm font-semibold"
|
||||
style={{ color: '#EAECEF' }}
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4"
|
||||
style={{ color: '#A78BFA' }}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"
|
||||
/>
|
||||
</svg>
|
||||
{t('customModelName', language)}
|
||||
</label>
|
||||
@@ -1209,7 +1367,11 @@ function StandardProviderConfigForm({
|
||||
onChange={(e) => onModelNameChange(e.target.value)}
|
||||
placeholder={t('customModelNamePlaceholder', language)}
|
||||
className="w-full px-4 py-3 rounded-xl"
|
||||
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
|
||||
style={{
|
||||
background: '#0B0E11',
|
||||
border: '1px solid #2B3139',
|
||||
color: '#EAECEF',
|
||||
}}
|
||||
/>
|
||||
<div className="text-xs" style={{ color: '#848E9C' }}>
|
||||
{t('leaveBlankForDefaultModel', language)}
|
||||
@@ -1220,9 +1382,23 @@ function StandardProviderConfigForm({
|
||||
{/* BlockRun Model Selector */}
|
||||
{selectedModel.provider?.startsWith('blockrun') && (
|
||||
<div className="space-y-2">
|
||||
<label className="flex items-center gap-2 text-sm font-semibold" style={{ color: '#EAECEF' }}>
|
||||
<svg className="w-4 h-4" style={{ color: '#A78BFA' }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
<label
|
||||
className="flex items-center gap-2 text-sm font-semibold"
|
||||
style={{ color: '#EAECEF' }}
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4"
|
||||
style={{ color: '#A78BFA' }}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
{t('modelConfig.selectModelLabel', language)}
|
||||
</label>
|
||||
@@ -1236,14 +1412,23 @@ function StandardProviderConfigForm({
|
||||
onClick={() => onModelNameChange(m.id)}
|
||||
className="flex flex-col items-start px-3 py-2 rounded-xl text-left transition-all"
|
||||
style={{
|
||||
background: isSelected ? 'rgba(37, 99, 235, 0.2)' : '#0B0E11',
|
||||
border: isSelected ? '1px solid #2563EB' : '1px solid #2B3139',
|
||||
background: isSelected
|
||||
? 'rgba(37, 99, 235, 0.2)'
|
||||
: '#0B0E11',
|
||||
border: isSelected
|
||||
? '1px solid #2563EB'
|
||||
: '1px solid #2B3139',
|
||||
}}
|
||||
>
|
||||
<span className="text-xs font-semibold" style={{ color: isSelected ? '#60A5FA' : '#EAECEF' }}>
|
||||
<span
|
||||
className="text-xs font-semibold"
|
||||
style={{ color: isSelected ? '#60A5FA' : '#EAECEF' }}
|
||||
>
|
||||
{m.name}
|
||||
</span>
|
||||
<span className="text-[10px]" style={{ color: '#848E9C' }}>{m.desc}</span>
|
||||
<span className="text-[10px]" style={{ color: '#848E9C' }}>
|
||||
{m.desc}
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -18,24 +18,34 @@ const setupSteps = [
|
||||
detail:
|
||||
'Your account keeps the Autopilot configuration, wallet authorization state, and trading dashboard in one place.',
|
||||
icon: KeyRound,
|
||||
action: 'Create account',
|
||||
to: ROUTES.register,
|
||||
},
|
||||
{
|
||||
title: 'Fund the AI fee wallet',
|
||||
detail:
|
||||
'NOFX prepares a Base USDC wallet for Claw402.ai data and model calls. This wallet is separate from trading collateral.',
|
||||
icon: CircleDollarSign,
|
||||
action: 'Open deposit QR',
|
||||
to: ROUTES.login,
|
||||
returnUrl: `${ROUTES.traders}?setup=claw402`,
|
||||
},
|
||||
{
|
||||
title: 'Authorize Hyperliquid',
|
||||
detail:
|
||||
'Connect your trading wallet, approve the NOFX Agent, and approve the builder fee. Funds remain in your Hyperliquid account.',
|
||||
icon: Wallet,
|
||||
action: 'Connect exchange',
|
||||
to: ROUTES.login,
|
||||
returnUrl: `${ROUTES.traders}?setup=hyperliquid`,
|
||||
},
|
||||
{
|
||||
title: 'Deposit trading USDC',
|
||||
detail:
|
||||
'Add USDC on Hyperliquid, then start NOFX Autopilot. The strategy is created and launched automatically.',
|
||||
icon: Zap,
|
||||
action: 'Open Hyperliquid',
|
||||
href: 'https://app.hyperliquid.xyz/',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -66,6 +76,12 @@ export function TraderLaunchGuestPage() {
|
||||
<div className="mt-7 flex flex-col gap-3 sm:flex-row">
|
||||
<Link
|
||||
to={ROUTES.login}
|
||||
onClick={() =>
|
||||
sessionStorage.setItem(
|
||||
'returnUrl',
|
||||
`${ROUTES.traders}?setup=claw402`
|
||||
)
|
||||
}
|
||||
className="inline-flex items-center justify-center gap-2 rounded-xl bg-nofx-gold px-5 py-3 text-sm font-bold text-black transition hover:bg-yellow-400"
|
||||
>
|
||||
Start setup
|
||||
@@ -83,11 +99,10 @@ export function TraderLaunchGuestPage() {
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
{setupSteps.map((step, index) => {
|
||||
const Icon = step.icon
|
||||
return (
|
||||
<div
|
||||
key={step.title}
|
||||
className="rounded-xl border border-white/10 bg-black/24 p-4"
|
||||
>
|
||||
const cardClass =
|
||||
'group rounded-xl border border-white/10 bg-black/24 p-4 text-left transition hover:border-nofx-gold/35 hover:bg-nofx-gold/[0.04]'
|
||||
const content = (
|
||||
<>
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-xl border border-nofx-gold/20 bg-nofx-gold/10 text-nofx-gold">
|
||||
<Icon className="h-4 w-4" />
|
||||
@@ -102,7 +117,44 @@ export function TraderLaunchGuestPage() {
|
||||
<p className="mt-2 text-sm leading-6 text-zinc-500">
|
||||
{step.detail}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-4 inline-flex items-center gap-2 text-xs font-bold text-nofx-gold transition group-hover:text-yellow-300">
|
||||
{step.action}
|
||||
{step.href ? (
|
||||
<ExternalLink className="h-3.5 w-3.5" />
|
||||
) : (
|
||||
<ArrowRight className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
if (step.href) {
|
||||
return (
|
||||
<a
|
||||
key={step.title}
|
||||
href={step.href}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={cardClass}
|
||||
>
|
||||
{content}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={step.title}
|
||||
to={step.to || ROUTES.login}
|
||||
onClick={() => {
|
||||
if (step.returnUrl) {
|
||||
sessionStorage.setItem('returnUrl', step.returnUrl)
|
||||
}
|
||||
}}
|
||||
className={cardClass}
|
||||
>
|
||||
{content}
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user