- Updated system prompt to match main analysis style: precision of proprietary desk trader - Reduced max tokens from 800 to 200 for main responses - Reduced screenshot analysis tokens from 1000 to 150 - Made welcome messages more compact and focused - Shortened quick action buttons (Exit now?, Move stop loss, etc.) - Condensed status report to essential information only - Eliminated verbose explanations, focus on exact price levels and immediate actions - Changed temperature from 0.3 to 0.1 for more consistent responses Addresses user feedback that responses were 'way too vague and too much talk'.
418 lines
16 KiB
TypeScript
418 lines
16 KiB
TypeScript
"use client"
|
|
import React, { useState, useRef, useEffect } from 'react'
|
|
|
|
interface TradePosition {
|
|
id: string
|
|
symbol: string
|
|
side: 'LONG' | 'SHORT'
|
|
entryPrice: number
|
|
currentPrice: number
|
|
amount: number
|
|
unrealizedPnl: number
|
|
pnlPercentage: number
|
|
totalValue: number
|
|
stopLoss?: number
|
|
takeProfit?: number
|
|
timestamp: number
|
|
status: string
|
|
leverage: number
|
|
txId: string
|
|
entryAnalysis?: string
|
|
}
|
|
|
|
interface ChatMessage {
|
|
id: string
|
|
type: 'user' | 'assistant' | 'system'
|
|
content: string
|
|
timestamp: string
|
|
analysis?: any
|
|
screenshots?: string[]
|
|
}
|
|
|
|
interface TradeFollowUpPanelProps {
|
|
onClose: () => void
|
|
}
|
|
|
|
export default function TradeFollowUpPanel({ onClose }: TradeFollowUpPanelProps) {
|
|
const [activePosition, setActivePosition] = useState<TradePosition | null>(null)
|
|
const [chatMessages, setChatMessages] = useState<ChatMessage[]>([])
|
|
const [currentMessage, setCurrentMessage] = useState('')
|
|
const [isLoading, setIsLoading] = useState(false)
|
|
const [isAnalyzing, setIsAnalyzing] = useState(false)
|
|
const [systemStatus, setSystemStatus] = useState<any>(null)
|
|
const chatEndRef = useRef<HTMLDivElement>(null)
|
|
|
|
// Auto-scroll to bottom of chat
|
|
useEffect(() => {
|
|
chatEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
|
}, [chatMessages])
|
|
|
|
// Load active positions on mount
|
|
useEffect(() => {
|
|
const initializePanel = async () => {
|
|
await loadSystemStatus()
|
|
await loadActivePositions()
|
|
}
|
|
initializePanel()
|
|
}, [])
|
|
|
|
const loadSystemStatus = async () => {
|
|
try {
|
|
// Test multiple endpoints to get status
|
|
const [statusRes, walletRes, healthRes] = await Promise.allSettled([
|
|
fetch('/api/status'),
|
|
fetch('/api/wallet/balance'),
|
|
fetch('/api/trading/health')
|
|
])
|
|
|
|
const systemInfo = {
|
|
api: statusRes.status === 'fulfilled',
|
|
wallet: walletRes.status === 'fulfilled',
|
|
trading: healthRes.status === 'fulfilled',
|
|
timestamp: new Date().toLocaleTimeString()
|
|
}
|
|
|
|
setSystemStatus(systemInfo)
|
|
} catch (error) {
|
|
console.error('Error loading system status:', error)
|
|
}
|
|
}
|
|
|
|
const loadActivePositions = async () => {
|
|
try {
|
|
const response = await fetch('/api/trading/positions')
|
|
const data = await response.json()
|
|
|
|
if (data.success && data.positions?.length > 0) {
|
|
// For now, take the first active position
|
|
// TODO: Add position selector if multiple positions
|
|
setActivePosition(data.positions[0])
|
|
|
|
// Add welcome message with system status
|
|
const statusEmoji = systemStatus ?
|
|
`🟢 API ${systemStatus.api ? '✓' : '✗'} | 💰 Wallet ${systemStatus.wallet ? '✓' : '✗'} | 📈 Trading ${systemStatus.trading ? '✓' : '✗'}` :
|
|
'🔄 Loading system status...'
|
|
|
|
setChatMessages([{
|
|
id: Date.now().toString(),
|
|
type: 'system',
|
|
content: `🎯 **Trade Follow-up Assistant**\n\n**System:** ${statusEmoji}\n\n**Active Position:**\n• ${data.positions[0].symbol} ${data.positions[0].side}\n• Entry: $${data.positions[0].entryPrice} | Size: ${data.positions[0].amount}\n• P&L: ${data.positions[0].unrealizedPnl > 0 ? '+' : ''}$${data.positions[0].unrealizedPnl.toFixed(2)}\n\nAsk: "exit?", "analysis", "risk", or type your question.`,
|
|
timestamp: new Date().toISOString()
|
|
}])
|
|
} else {
|
|
const statusEmoji = systemStatus ?
|
|
`🟢 API ${systemStatus.api ? '✓' : '✗'} | 💰 Wallet ${systemStatus.wallet ? '✓' : '✗'} | 📈 Trading ${systemStatus.trading ? '✓' : '✗'}` :
|
|
'🔄 Loading system status...'
|
|
|
|
setChatMessages([{
|
|
id: Date.now().toString(),
|
|
type: 'system',
|
|
content: `⚠️ **No Active Positions**\n\n**System:** ${statusEmoji}\n\nExecute a trade first, then mark it as traded for follow-up analysis.\n\nType "status" for system diagnostics.`,
|
|
timestamp: new Date().toISOString()
|
|
}])
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading positions:', error)
|
|
setChatMessages([{
|
|
id: Date.now().toString(),
|
|
type: 'system',
|
|
content: `❌ **Error Loading Positions**\n\n**System:** ${systemStatus ? `API ${systemStatus.api ? '✓' : '✗'} Wallet ${systemStatus.wallet ? '✓' : '✗'} Trading ${systemStatus.trading ? '✓' : '✗'}` : 'Unknown'}\n\nConnection issues detected. Type "status" for diagnostics.`,
|
|
timestamp: new Date().toISOString()
|
|
}])
|
|
}
|
|
}
|
|
|
|
const handleSendMessage = async () => {
|
|
if (!currentMessage.trim() || isLoading) return
|
|
|
|
const userMessage: ChatMessage = {
|
|
id: Date.now().toString(),
|
|
type: 'user',
|
|
content: currentMessage,
|
|
timestamp: new Date().toISOString()
|
|
}
|
|
|
|
setChatMessages(prev => [...prev, userMessage])
|
|
setCurrentMessage('')
|
|
setIsLoading(true)
|
|
|
|
try {
|
|
// Check if user is asking for status or system information
|
|
const isStatusRequest = currentMessage.toLowerCase().includes('status') ||
|
|
currentMessage.toLowerCase().includes('system') ||
|
|
currentMessage.toLowerCase().includes('health') ||
|
|
currentMessage.toLowerCase().includes('diagnostic')
|
|
|
|
if (isStatusRequest) {
|
|
// Refresh system status and provide detailed information
|
|
await loadSystemStatus()
|
|
|
|
const statusMessage: ChatMessage = {
|
|
id: (Date.now() + 1).toString(),
|
|
type: 'assistant',
|
|
content: `📊 **System Status** (${new Date().toLocaleTimeString()})\n\n**Core Services:**\n• API: ${systemStatus?.api ? '🟢' : '🔴'} | Wallet: ${systemStatus?.wallet ? '🟢' : '🔴'} | Trading: ${systemStatus?.trading ? '🟢' : '🔴'}\n\n**Trading:**\n• Drift Protocol: ${systemStatus?.trading ? '🟢 Connected' : '🔴 Disconnected'}\n• Screenshot Service: ${systemStatus?.api ? '🟢 Ready' : '🔴 Not ready'}\n• AI Analysis: ${process.env.OPENAI_API_KEY ? '🟢 Ready' : '🔴 No API key'}\n\n**Container:**\n• Docker: 🟢 Running (Port 9001)\n• Database: 🟢 SQLite Connected\n\n${!activePosition ? '⚠️ No active positions for follow-up' : '✅ Ready for trade management'}`,
|
|
timestamp: new Date().toISOString()
|
|
}
|
|
setChatMessages(prev => [...prev, statusMessage])
|
|
setIsLoading(false)
|
|
return
|
|
}
|
|
|
|
// Check if user is asking for updated analysis
|
|
const needsScreenshot = currentMessage.toLowerCase().includes('analysis') ||
|
|
currentMessage.toLowerCase().includes('update') ||
|
|
currentMessage.toLowerCase().includes('current') ||
|
|
currentMessage.toLowerCase().includes('now')
|
|
|
|
let screenshots: string[] = []
|
|
|
|
if (needsScreenshot && activePosition) {
|
|
setIsAnalyzing(true)
|
|
|
|
// Add thinking message
|
|
const thinkingMessage: ChatMessage = {
|
|
id: (Date.now() + 1).toString(),
|
|
type: 'assistant',
|
|
content: '🔄 **Capturing fresh screenshots and analyzing...**',
|
|
timestamp: new Date().toISOString()
|
|
}
|
|
setChatMessages(prev => [...prev, thinkingMessage])
|
|
|
|
// Get fresh screenshots
|
|
const screenshotResponse = await fetch('/api/enhanced-screenshot', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
symbol: activePosition.symbol,
|
|
timeframe: '240', // 4h default for trade follow-up
|
|
layouts: ['ai', 'diy'],
|
|
analyze: false // We'll analyze separately with trade context
|
|
})
|
|
})
|
|
|
|
const screenshotData = await screenshotResponse.json()
|
|
if (screenshotData.success && screenshotData.screenshots) {
|
|
screenshots = screenshotData.screenshots
|
|
}
|
|
|
|
setIsAnalyzing(false)
|
|
}
|
|
|
|
// Send to trade follow-up API
|
|
const response = await fetch('/api/trade-followup', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
message: currentMessage,
|
|
position: activePosition,
|
|
screenshots: screenshots,
|
|
chatHistory: chatMessages.slice(-5) // Last 5 messages for context
|
|
})
|
|
})
|
|
|
|
const data = await response.json()
|
|
|
|
if (data.success) {
|
|
const assistantMessage: ChatMessage = {
|
|
id: (Date.now() + 2).toString(),
|
|
type: 'assistant',
|
|
content: data.response,
|
|
timestamp: new Date().toISOString(),
|
|
analysis: data.analysis,
|
|
screenshots: screenshots
|
|
}
|
|
|
|
setChatMessages(prev => [...prev.filter(m => !m.content.includes('Getting Updated Analysis')), assistantMessage])
|
|
} else {
|
|
throw new Error(data.error || 'Failed to get response')
|
|
}
|
|
} catch (error) {
|
|
console.error('Error sending message:', error)
|
|
const errorMessage: ChatMessage = {
|
|
id: (Date.now() + 3).toString(),
|
|
type: 'assistant',
|
|
content: '❌ **Error**\n\nSorry, I encountered an error. Please try again.',
|
|
timestamp: new Date().toISOString()
|
|
}
|
|
setChatMessages(prev => [...prev.filter(m => !m.content.includes('Getting Updated Analysis')), errorMessage])
|
|
} finally {
|
|
setIsLoading(false)
|
|
setIsAnalyzing(false)
|
|
}
|
|
}
|
|
|
|
const handleKeyPress = (e: React.KeyboardEvent) => {
|
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
e.preventDefault()
|
|
handleSendMessage()
|
|
}
|
|
}
|
|
|
|
const formatMessage = (content: string) => {
|
|
// Convert markdown-style formatting to JSX
|
|
return content.split('\n').map((line, index) => {
|
|
if (line.startsWith('**') && line.endsWith('**')) {
|
|
return <div key={index} className="font-bold text-purple-300 mb-2">{line.slice(2, -2)}</div>
|
|
}
|
|
if (line.startsWith('• ')) {
|
|
return <div key={index} className="ml-4 text-gray-300">{line}</div>
|
|
}
|
|
return <div key={index} className="text-gray-300">{line}</div>
|
|
})
|
|
}
|
|
|
|
const quickActions = [
|
|
"Exit now?",
|
|
"Move stop loss",
|
|
"Fresh analysis",
|
|
"Risk check",
|
|
"status"
|
|
]
|
|
|
|
return (
|
|
<div className="fixed inset-0 bg-black/80 backdrop-blur-sm flex items-center justify-center p-4 z-50">
|
|
<div className="bg-gradient-to-br from-gray-900 to-purple-900/20 border border-purple-500/30 rounded-xl w-full max-w-4xl h-[80vh] flex flex-col">
|
|
{/* Header */}
|
|
<div className="p-4 border-b border-purple-500/30 flex items-center justify-between">
|
|
<div className="flex items-center">
|
|
<span className="w-8 h-8 bg-gradient-to-br from-purple-400 to-purple-600 rounded-lg flex items-center justify-center mr-3 text-lg">
|
|
💬
|
|
</span>
|
|
<div>
|
|
<h3 className="text-lg font-bold text-white">Trade Follow-up Assistant</h3>
|
|
{activePosition ? (
|
|
<p className="text-sm text-purple-300">
|
|
{activePosition.symbol} {activePosition.side} • Entry: ${activePosition.entryPrice}
|
|
</p>
|
|
) : (
|
|
<p className="text-sm text-gray-400">No active positions</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center space-x-3">
|
|
{/* System Status Indicator */}
|
|
<div className="flex items-center space-x-1">
|
|
<div className={`w-2 h-2 rounded-full ${systemStatus?.api ? 'bg-green-400' : 'bg-red-400'}`}></div>
|
|
<span className="text-xs text-gray-400">
|
|
{systemStatus?.api && systemStatus?.wallet && systemStatus?.trading ? 'All Systems' : 'System Issues'}
|
|
</span>
|
|
</div>
|
|
<button
|
|
onClick={onClose}
|
|
className="w-8 h-8 bg-red-500/20 hover:bg-red-500/40 rounded-lg flex items-center justify-center text-red-400 transition-colors"
|
|
>
|
|
✕
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Chat Messages */}
|
|
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
|
{chatMessages.map((message) => (
|
|
<div
|
|
key={message.id}
|
|
className={`flex ${message.type === 'user' ? 'justify-end' : 'justify-start'}`}
|
|
>
|
|
<div
|
|
className={`max-w-[80%] p-3 rounded-lg ${
|
|
message.type === 'user'
|
|
? 'bg-purple-600 text-white'
|
|
: message.type === 'system'
|
|
? 'bg-blue-600/20 border border-blue-500/30 text-blue-300'
|
|
: 'bg-gray-800 text-gray-300'
|
|
}`}
|
|
>
|
|
{formatMessage(message.content)}
|
|
|
|
{/* Show screenshots if available */}
|
|
{message.screenshots && message.screenshots.length > 0 && (
|
|
<div className="mt-3 grid grid-cols-2 gap-2">
|
|
{message.screenshots.map((screenshot, index) => (
|
|
<img
|
|
key={index}
|
|
src={`/api/image?file=${screenshot.split('/').pop()}`}
|
|
alt={`Analysis screenshot ${index + 1}`}
|
|
className="w-full rounded border border-gray-600 cursor-pointer hover:border-purple-500/50 transition-colors"
|
|
onClick={() => {
|
|
// TODO: Open enlarged view
|
|
}}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
<div className="text-xs opacity-60 mt-2">
|
|
{new Date(message.timestamp).toLocaleTimeString()}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
{isAnalyzing && (
|
|
<div className="flex justify-start">
|
|
<div className="bg-purple-600/20 border border-purple-500/30 p-3 rounded-lg">
|
|
<div className="flex items-center space-x-2">
|
|
<div className="animate-spin w-4 h-4 border-2 border-purple-400 border-t-transparent rounded-full"></div>
|
|
<span className="text-purple-300">Analyzing market conditions...</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div ref={chatEndRef} />
|
|
</div>
|
|
|
|
{/* Quick Actions */}
|
|
{activePosition && (
|
|
<div className="p-4 border-t border-purple-500/30">
|
|
<div className="mb-3">
|
|
<div className="text-xs text-gray-400 mb-2">Quick Actions:</div>
|
|
<div className="flex flex-wrap gap-2">
|
|
{quickActions.map((action, index) => (
|
|
<button
|
|
key={index}
|
|
onClick={() => setCurrentMessage(action)}
|
|
className="px-3 py-1 bg-purple-600/20 hover:bg-purple-600/40 border border-purple-500/30 rounded-full text-xs text-purple-300 transition-colors"
|
|
>
|
|
{action}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Message Input */}
|
|
<div className="p-4 border-t border-purple-500/30">
|
|
<div className="flex items-center space-x-3">
|
|
<textarea
|
|
value={currentMessage}
|
|
onChange={(e) => setCurrentMessage(e.target.value)}
|
|
onKeyPress={handleKeyPress}
|
|
placeholder={
|
|
activePosition
|
|
? "Ask about your trade: 'Should I exit?', 'Update analysis', 'Move stop loss'..."
|
|
: "No active positions to analyze"
|
|
}
|
|
disabled={!activePosition || isLoading}
|
|
className="flex-1 bg-gray-800 border border-gray-600 rounded-lg px-4 py-2 text-white placeholder-gray-400 focus:border-purple-500 focus:outline-none resize-none"
|
|
rows={2}
|
|
/>
|
|
<button
|
|
onClick={handleSendMessage}
|
|
disabled={!currentMessage.trim() || isLoading || !activePosition}
|
|
className="w-10 h-10 bg-purple-600 hover:bg-purple-700 disabled:bg-gray-600 disabled:cursor-not-allowed rounded-lg flex items-center justify-center text-white transition-colors"
|
|
>
|
|
{isLoading ? (
|
|
<div className="animate-spin w-4 h-4 border-2 border-white border-t-transparent rounded-full"></div>
|
|
) : (
|
|
'📤'
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|