Files
nofx/web/src/components/strategy/PromptSectionsEditor.tsx
2026-03-12 16:14:56 +08:00

179 lines
6.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState } from 'react'
import { ChevronDown, ChevronRight, RotateCcw, FileText } from 'lucide-react'
import type { PromptSectionsConfig } from '../../types'
import { promptSections as promptSectionsI18n, ts } from '../../i18n/strategy-translations'
interface PromptSectionsEditorProps {
config: PromptSectionsConfig | undefined
onChange: (config: PromptSectionsConfig) => void
disabled?: boolean
language: string
}
// Default prompt sections (same as backend defaults)
const defaultSections: PromptSectionsConfig = {
role_definition: `# 你是专业的加密货币交易AI
你专注于技术分析和风险管理,基于市场数据做出理性的交易决策。
你的目标是在控制风险的前提下,捕捉高概率的交易机会。`,
trading_frequency: `# ⏱️ 交易频率认知
- 优秀交易员每天2-4笔 ≈ 每小时0.1-0.2笔
- 每小时>2笔 = 过度交易
- 单笔持仓时间≥30-60分钟
如果你发现自己每个周期都在交易 → 标准过低;若持仓<30分钟就平仓 → 过于急躁。`,
entry_standards: `# 🎯 开仓标准(严格)
只在多重信号共振时开仓:
- 趋势方向明确EMA排列、价格位置
- 动量确认MACD、RSI协同
- 波动率适中ATR合理范围
- 量价配合(成交量支持方向)
避免:单一指标、信号矛盾、横盘震荡、刚平仓即重启。`,
decision_process: `# 📋 决策流程
1. 检查持仓 → 是否该止盈/止损
2. 扫描候选币 + 多时间框 → 是否存在强信号
3. 评估风险回报比 → 是否满足最小要求
4. 先写思维链再输出结构化JSON`,
}
export function PromptSectionsEditor({
config,
onChange,
disabled,
language,
}: PromptSectionsEditorProps) {
const [expandedSections, setExpandedSections] = useState<Record<string, boolean>>({
role_definition: false,
trading_frequency: false,
entry_standards: false,
decision_process: false,
})
const sections = [
{ key: 'role_definition', label: ts(promptSectionsI18n.roleDefinition, language), desc: ts(promptSectionsI18n.roleDefinitionDesc, language) },
{ key: 'trading_frequency', label: ts(promptSectionsI18n.tradingFrequency, language), desc: ts(promptSectionsI18n.tradingFrequencyDesc, language) },
{ key: 'entry_standards', label: ts(promptSectionsI18n.entryStandards, language), desc: ts(promptSectionsI18n.entryStandardsDesc, language) },
{ key: 'decision_process', label: ts(promptSectionsI18n.decisionProcess, language), desc: ts(promptSectionsI18n.decisionProcessDesc, language) },
]
const currentConfig = config || {}
const updateSection = (key: keyof PromptSectionsConfig, value: string) => {
if (!disabled) {
onChange({ ...currentConfig, [key]: value })
}
}
const resetSection = (key: keyof PromptSectionsConfig) => {
if (!disabled) {
onChange({ ...currentConfig, [key]: defaultSections[key] })
}
}
const toggleSection = (key: string) => {
setExpandedSections((prev) => ({ ...prev, [key]: !prev[key] }))
}
const getValue = (key: keyof PromptSectionsConfig): string => {
return currentConfig[key] || defaultSections[key] || ''
}
return (
<div className="space-y-4">
<div className="flex items-start gap-2 mb-4">
<FileText className="w-5 h-5 mt-0.5" style={{ color: '#a855f7' }} />
<div>
<h3 className="font-medium" style={{ color: '#EAECEF' }}>
{ts(promptSectionsI18n.promptSections, language)}
</h3>
<p className="text-xs mt-1" style={{ color: '#848E9C' }}>
{ts(promptSectionsI18n.promptSectionsDesc, language)}
</p>
</div>
</div>
<div className="space-y-2">
{sections.map(({ key, label, desc }) => {
const sectionKey = key as keyof PromptSectionsConfig
const isExpanded = expandedSections[key]
const value = getValue(sectionKey)
const isModified = currentConfig[sectionKey] !== undefined && currentConfig[sectionKey] !== defaultSections[sectionKey]
return (
<div
key={key}
className="rounded-lg overflow-hidden"
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
>
<button
onClick={() => toggleSection(key)}
className="w-full flex items-center justify-between px-3 py-2.5 hover:bg-white/5 transition-colors text-left"
>
<div className="flex items-center gap-2">
{isExpanded ? (
<ChevronDown className="w-4 h-4" style={{ color: '#848E9C' }} />
) : (
<ChevronRight className="w-4 h-4" style={{ color: '#848E9C' }} />
)}
<span className="text-sm font-medium" style={{ color: '#EAECEF' }}>
{label}
</span>
{isModified && (
<span
className="px-1.5 py-0.5 text-[10px] rounded"
style={{ background: 'rgba(168, 85, 247, 0.15)', color: '#a855f7' }}
>
{ts(promptSectionsI18n.modified, language)}
</span>
)}
</div>
<span className="text-[10px]" style={{ color: '#848E9C' }}>
{value.length} {ts(promptSectionsI18n.chars, language)}
</span>
</button>
{isExpanded && (
<div className="px-3 pb-3">
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{desc}
</p>
<textarea
value={value}
onChange={(e) => updateSection(sectionKey, e.target.value)}
disabled={disabled}
rows={6}
className="w-full px-3 py-2 rounded-lg resize-y font-mono text-xs"
style={{
background: '#1E2329',
border: '1px solid #2B3139',
color: '#EAECEF',
minHeight: '120px',
}}
/>
<div className="flex justify-end mt-2">
<button
onClick={() => resetSection(sectionKey)}
disabled={disabled || !isModified}
className="flex items-center gap-1 px-2 py-1 rounded text-xs transition-colors hover:bg-white/5 disabled:opacity-30"
style={{ color: '#848E9C' }}
>
<RotateCcw className="w-3 h-3" />
{ts(promptSectionsI18n.resetToDefault, language)}
</button>
</div>
</div>
)}
</div>
)
})}
</div>
</div>
)
}