mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-07 04:50:57 +08:00
change v1
This commit is contained in:
@@ -1,9 +1,4 @@
|
||||
interface AgentStep {
|
||||
id: string
|
||||
label: string
|
||||
status: 'planning' | 'pending' | 'running' | 'completed' | 'replanned'
|
||||
detail?: string
|
||||
}
|
||||
import type { AgentStep } from '../../types/agent'
|
||||
|
||||
interface AgentStepPanelProps {
|
||||
steps?: AgentStep[]
|
||||
|
||||
@@ -2,22 +2,7 @@ import { forwardRef } from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
import { AgentStepPanel } from './AgentStepPanel'
|
||||
import { renderMessageContent } from './MessageRenderer'
|
||||
|
||||
interface AgentStep {
|
||||
id: string
|
||||
label: string
|
||||
status: 'planning' | 'pending' | 'running' | 'completed' | 'replanned'
|
||||
detail?: string
|
||||
}
|
||||
|
||||
interface Message {
|
||||
id: string
|
||||
role: 'user' | 'bot'
|
||||
text: string
|
||||
time: string
|
||||
streaming?: boolean
|
||||
steps?: AgentStep[]
|
||||
}
|
||||
import type { AgentMessage as Message, AgentStep } from '../../types/agent'
|
||||
|
||||
interface ChatMessagesProps {
|
||||
messages: Message[]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { httpClient } from '../../lib/httpClient'
|
||||
// icons reserved for future use
|
||||
|
||||
interface TickerData {
|
||||
@@ -25,8 +26,11 @@ export function MarketTicker() {
|
||||
const fetchTickers = async () => {
|
||||
try {
|
||||
// Batch fetch: single API call for all symbols
|
||||
const res = await fetch(`/api/agent/tickers?symbols=${SYMBOLS.join(',')}`)
|
||||
const data = await res.json()
|
||||
const res = await httpClient.request<TickerData[]>(
|
||||
`/api/agent/tickers?symbols=${SYMBOLS.join(',')}`,
|
||||
{ silent: true }
|
||||
)
|
||||
const data = res.data
|
||||
const map: Record<string, TickerData> = {}
|
||||
if (Array.isArray(data)) {
|
||||
data.forEach((r: TickerData) => {
|
||||
@@ -49,7 +53,11 @@ export function MarketTicker() {
|
||||
|
||||
const formatPrice = (price: string) => {
|
||||
const n = parseFloat(price)
|
||||
if (n >= 1000) return n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
if (n >= 1000)
|
||||
return n.toLocaleString('en-US', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
})
|
||||
if (n >= 1) return n.toFixed(2)
|
||||
return n.toFixed(4)
|
||||
}
|
||||
@@ -76,13 +84,15 @@ export function MarketTicker() {
|
||||
height: 56,
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
width: '60%',
|
||||
height: 10,
|
||||
background: 'rgba(255,255,255,0.04)',
|
||||
borderRadius: 4,
|
||||
animation: 'pulse 1.5s infinite',
|
||||
}} />
|
||||
<div
|
||||
style={{
|
||||
width: '60%',
|
||||
height: 10,
|
||||
background: 'rgba(255,255,255,0.04)',
|
||||
borderRadius: 4,
|
||||
animation: 'pulse 1.5s infinite',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<style>{`
|
||||
@@ -104,7 +114,11 @@ export function MarketTicker() {
|
||||
const isUp = pct > 0
|
||||
const isDown = pct < 0
|
||||
const color = isUp ? '#00e5a0' : isDown ? '#F6465D' : '#6c6c82'
|
||||
const bgColor = isUp ? 'rgba(0,229,160,0.06)' : isDown ? 'rgba(246,70,93,0.06)' : 'rgba(108,108,130,0.06)'
|
||||
const bgColor = isUp
|
||||
? 'rgba(0,229,160,0.06)'
|
||||
: isDown
|
||||
? 'rgba(246,70,93,0.06)'
|
||||
: 'rgba(108,108,130,0.06)'
|
||||
const label = sym.replace('USDT', '')
|
||||
const icon = SYMBOL_ICONS[label] || label[0]
|
||||
|
||||
@@ -149,7 +163,14 @@ export function MarketTicker() {
|
||||
{icon}
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 12.5, fontWeight: 600, color: '#e0e0ec', letterSpacing: '-0.01em' }}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 12.5,
|
||||
fontWeight: 600,
|
||||
color: '#e0e0ec',
|
||||
letterSpacing: '-0.01em',
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
<div style={{ fontSize: 10, color: '#4c4c62' }}>
|
||||
@@ -158,16 +179,27 @@ export function MarketTicker() {
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ textAlign: 'right' }}>
|
||||
<div style={{ fontSize: 12.5, fontWeight: 600, color: '#e0e0ec', fontFamily: '"IBM Plex Mono", monospace', letterSpacing: '-0.02em' }}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 12.5,
|
||||
fontWeight: 600,
|
||||
color: '#e0e0ec',
|
||||
fontFamily: '"IBM Plex Mono", monospace',
|
||||
letterSpacing: '-0.02em',
|
||||
}}
|
||||
>
|
||||
${formatPrice(t.lastPrice)}
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: 10.5,
|
||||
fontWeight: 600,
|
||||
color,
|
||||
fontFamily: '"IBM Plex Mono", monospace',
|
||||
}}>
|
||||
{isUp ? '+' : ''}{pct.toFixed(2)}%
|
||||
<div
|
||||
style={{
|
||||
fontSize: 10.5,
|
||||
fontWeight: 600,
|
||||
color,
|
||||
fontFamily: '"IBM Plex Mono", monospace',
|
||||
}}
|
||||
>
|
||||
{isUp ? '+' : ''}
|
||||
{pct.toFixed(2)}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { httpClient, ApiError } from '../../lib/httpClient'
|
||||
|
||||
interface Preference {
|
||||
id: string
|
||||
@@ -27,12 +28,11 @@ export function UserPreferencesPanel({ token, language }: Props) {
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await fetch('/api/agent/preferences', {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
if (!res.ok) throw new Error('Failed to load preferences')
|
||||
const data = await res.json()
|
||||
setPreferences(Array.isArray(data.preferences) ? data.preferences : [])
|
||||
const resp = await httpClient.get<{ preferences: Preference[] }>(
|
||||
'/api/agent/preferences'
|
||||
)
|
||||
const prefs = resp.data?.preferences
|
||||
setPreferences(Array.isArray(prefs) ? prefs : [])
|
||||
setError(null)
|
||||
} catch {
|
||||
setError(language === 'zh' ? '加载偏好失败' : 'Failed to load')
|
||||
@@ -70,21 +70,19 @@ export function UserPreferencesPanel({ token, language }: Props) {
|
||||
|
||||
setSaving(true)
|
||||
try {
|
||||
const res = await fetch('/api/agent/preferences', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({ text }),
|
||||
})
|
||||
const data = await res.json().catch(() => ({}))
|
||||
if (!res.ok) throw new Error(data.error || 'save failed')
|
||||
setPreferences(Array.isArray(data.preferences) ? data.preferences : [])
|
||||
const resp = await httpClient.post<{ preferences: Preference[] }>(
|
||||
'/api/agent/preferences',
|
||||
{ text }
|
||||
)
|
||||
const prefs = resp.data?.preferences
|
||||
setPreferences(Array.isArray(prefs) ? prefs : [])
|
||||
setDraft('')
|
||||
setError(null)
|
||||
} catch {
|
||||
setError(language === 'zh' ? '保存偏好失败' : 'Failed to save')
|
||||
} catch (err) {
|
||||
const message = err instanceof ApiError ? err.message : ''
|
||||
setError(
|
||||
message || (language === 'zh' ? '保存偏好失败' : 'Failed to save')
|
||||
)
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
@@ -94,13 +92,11 @@ export function UserPreferencesPanel({ token, language }: Props) {
|
||||
if (!token || saving) return
|
||||
setSaving(true)
|
||||
try {
|
||||
const res = await fetch(`/api/agent/preferences/${encodeURIComponent(id)}`, {
|
||||
method: 'DELETE',
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
const data = await res.json().catch(() => ({}))
|
||||
if (!res.ok) throw new Error(data.error || 'delete failed')
|
||||
setPreferences(Array.isArray(data.preferences) ? data.preferences : [])
|
||||
const resp = await httpClient.delete<{ preferences: Preference[] }>(
|
||||
`/api/agent/preferences/${encodeURIComponent(id)}`
|
||||
)
|
||||
const prefs = resp.data?.preferences
|
||||
setPreferences(Array.isArray(prefs) ? prefs : [])
|
||||
setError(null)
|
||||
} catch {
|
||||
setError(language === 'zh' ? '删除偏好失败' : 'Failed to delete')
|
||||
@@ -123,7 +119,14 @@ export function UserPreferencesPanel({ token, language }: Props) {
|
||||
<div style={{ color: '#d7d7e0', fontSize: 12, fontWeight: 600 }}>
|
||||
{language === 'zh' ? '长期偏好' : 'Persistent Preferences'}
|
||||
</div>
|
||||
<div style={{ color: '#77778d', fontSize: 11, lineHeight: 1.5, marginTop: 4 }}>
|
||||
<div
|
||||
style={{
|
||||
color: '#77778d',
|
||||
fontSize: 11,
|
||||
lineHeight: 1.5,
|
||||
marginTop: 4,
|
||||
}}
|
||||
>
|
||||
{language === 'zh'
|
||||
? '把长期偏好固定下来,比如“默认用中文回答”或“优先关注 BTC 和 ETH”。'
|
||||
: 'Pin durable preferences the agent should keep in mind, like answering in Chinese or focusing on BTC and ETH.'}
|
||||
@@ -138,7 +141,11 @@ export function UserPreferencesPanel({ token, language }: Props) {
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') void addPreference()
|
||||
}}
|
||||
placeholder={language === 'zh' ? '例如:默认用中文回答,优先关注 BTC、ETH' : 'Example: Answer in Chinese and focus on BTC, ETH'}
|
||||
placeholder={
|
||||
language === 'zh'
|
||||
? '例如:默认用中文回答,优先关注 BTC、ETH'
|
||||
: 'Example: Answer in Chinese and focus on BTC, ETH'
|
||||
}
|
||||
style={{
|
||||
flex: 1,
|
||||
background: 'rgba(255,255,255,0.03)',
|
||||
@@ -154,7 +161,10 @@ export function UserPreferencesPanel({ token, language }: Props) {
|
||||
onClick={() => void addPreference()}
|
||||
disabled={!draft.trim() || saving}
|
||||
style={{
|
||||
background: draft.trim() && !saving ? 'rgba(240,185,11,0.12)' : 'rgba(255,255,255,0.04)',
|
||||
background:
|
||||
draft.trim() && !saving
|
||||
? 'rgba(240,185,11,0.12)'
|
||||
: 'rgba(255,255,255,0.04)',
|
||||
color: draft.trim() && !saving ? '#F0B90B' : '#6d6d82',
|
||||
border: '1px solid rgba(240,185,11,0.14)',
|
||||
borderRadius: 8,
|
||||
@@ -168,7 +178,9 @@ export function UserPreferencesPanel({ token, language }: Props) {
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div style={{ color: '#f08a8a', fontSize: 11, marginBottom: 8 }}>{error}</div>
|
||||
<div style={{ color: '#f08a8a', fontSize: 11, marginBottom: 8 }}>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
@@ -196,7 +208,14 @@ export function UserPreferencesPanel({ token, language }: Props) {
|
||||
border: '1px solid rgba(255,255,255,0.04)',
|
||||
}}
|
||||
>
|
||||
<div style={{ flex: 1, color: '#d7d7e0', fontSize: 12, lineHeight: 1.5 }}>
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
color: '#d7d7e0',
|
||||
fontSize: 12,
|
||||
lineHeight: 1.5,
|
||||
}}
|
||||
>
|
||||
{pref.text}
|
||||
</div>
|
||||
<button
|
||||
|
||||
@@ -38,7 +38,7 @@ export function RiskControlEditor({
|
||||
<div className="grid grid-cols-1 gap-4 mb-4">
|
||||
<div
|
||||
className="p-4 rounded-lg"
|
||||
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
|
||||
style={{ background: '#0B0E11', border: '1px solid #0ECB81' }}
|
||||
>
|
||||
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
||||
{ts(riskControl.maxPositions, language)}
|
||||
@@ -46,22 +46,14 @@ export function RiskControlEditor({
|
||||
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
||||
{ts(riskControl.maxPositionsDesc, language)}
|
||||
</p>
|
||||
<input
|
||||
type="number"
|
||||
value={config.max_positions ?? 3}
|
||||
onChange={(e) =>
|
||||
updateField('max_positions', parseInt(e.target.value) || 3)
|
||||
}
|
||||
disabled={disabled}
|
||||
min={1}
|
||||
max={3}
|
||||
className="w-32 px-3 py-2 rounded"
|
||||
style={{
|
||||
background: '#1E2329',
|
||||
border: '1px solid #2B3139',
|
||||
color: '#EAECEF',
|
||||
}}
|
||||
/>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="font-mono text-lg" style={{ color: '#0ECB81' }}>
|
||||
{config.max_positions ?? 3}
|
||||
</span>
|
||||
<span className="text-xs" style={{ color: '#848E9C' }}>
|
||||
System enforced
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -156,24 +148,15 @@ export function RiskControlEditor({
|
||||
{ts(riskControl.btcEthPositionValueRatioDesc, language)}
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="range"
|
||||
value={config.btc_eth_max_position_value_ratio ?? 5}
|
||||
onChange={(e) =>
|
||||
updateField('btc_eth_max_position_value_ratio', parseFloat(e.target.value))
|
||||
}
|
||||
disabled={disabled}
|
||||
min={0.5}
|
||||
max={10}
|
||||
step={0.5}
|
||||
className="flex-1 accent-green-500"
|
||||
/>
|
||||
<span
|
||||
className="w-12 text-center font-mono"
|
||||
style={{ color: '#0ECB81' }}
|
||||
>
|
||||
{config.btc_eth_max_position_value_ratio ?? 5}x
|
||||
</span>
|
||||
<span className="text-xs" style={{ color: '#848E9C' }}>
|
||||
System enforced
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -188,24 +171,15 @@ export function RiskControlEditor({
|
||||
{ts(riskControl.altcoinPositionValueRatioDesc, language)}
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="range"
|
||||
value={config.altcoin_max_position_value_ratio ?? 1}
|
||||
onChange={(e) =>
|
||||
updateField('altcoin_max_position_value_ratio', parseFloat(e.target.value))
|
||||
}
|
||||
disabled={disabled}
|
||||
min={0.5}
|
||||
max={10}
|
||||
step={0.5}
|
||||
className="flex-1 accent-green-500"
|
||||
/>
|
||||
<span
|
||||
className="w-12 text-center font-mono"
|
||||
style={{ color: '#0ECB81' }}
|
||||
>
|
||||
{config.altcoin_max_position_value_ratio ?? 1}x
|
||||
</span>
|
||||
<span className="text-xs" style={{ color: '#848E9C' }}>
|
||||
System enforced
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -264,20 +238,12 @@ export function RiskControlEditor({
|
||||
{ts(riskControl.maxMarginUsageDesc, language)}
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="range"
|
||||
value={(config.max_margin_usage ?? 0.9) * 100}
|
||||
onChange={(e) =>
|
||||
updateField('max_margin_usage', parseInt(e.target.value) / 100)
|
||||
}
|
||||
disabled={disabled}
|
||||
min={10}
|
||||
max={100}
|
||||
className="flex-1 accent-green-500"
|
||||
/>
|
||||
<span className="w-12 text-center font-mono" style={{ color: '#0ECB81' }}>
|
||||
{Math.round((config.max_margin_usage ?? 0.9) * 100)}%
|
||||
</span>
|
||||
<span className="text-xs" style={{ color: '#848E9C' }}>
|
||||
System enforced
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -295,7 +261,7 @@ export function RiskControlEditor({
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div
|
||||
className="p-4 rounded-lg"
|
||||
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
|
||||
style={{ background: '#0B0E11', border: '1px solid #0ECB81' }}
|
||||
>
|
||||
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
||||
{ts(riskControl.minPositionSize, language)}
|
||||
@@ -303,26 +269,16 @@ export function RiskControlEditor({
|
||||
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
||||
{ts(riskControl.minPositionSizeDesc, language)}
|
||||
</p>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="number"
|
||||
value={config.min_position_size ?? 12}
|
||||
onChange={(e) =>
|
||||
updateField('min_position_size', parseFloat(e.target.value) || 12)
|
||||
}
|
||||
disabled={disabled}
|
||||
min={10}
|
||||
max={1000}
|
||||
className="w-24 px-3 py-2 rounded"
|
||||
style={{
|
||||
background: '#1E2329',
|
||||
border: '1px solid #2B3139',
|
||||
color: '#EAECEF',
|
||||
}}
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-mono text-lg" style={{ color: '#0ECB81' }}>
|
||||
{config.min_position_size ?? 12}
|
||||
</span>
|
||||
<span className="ml-2" style={{ color: '#848E9C' }}>
|
||||
USDT
|
||||
</span>
|
||||
<span className="text-xs" style={{ color: '#848E9C' }}>
|
||||
System enforced
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user