learning_ai_invt_trdg/web/src/App.tsx

272 lines
9.9 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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, useEffect, useCallback, useRef } from 'react';
import { BrowserRouter } from 'react-router-dom';
import { useWebSocket } from './hooks/useWebSocket';
import { useAuth } from './components/AuthContext';
import { Login } from './components/Login';
import { ResetPassword } from './components/ResetPassword';
import { ChatControl } from './components/ChatControl';
import { AppContext } from './context/AppContext';
import { AppShell } from './components/layout/AppShell';
import { useBacktestFeatureGate } from './backtest/useBacktestFeatureGate';
import { useTabFeatureFlags } from './hooks/useTabFeatureFlags';
import { tradingRuntime, tradingTelemetry } from './lib/runtime';
import { createTradeProfile, fetchTradeProfiles, updateTradeProfile } from './lib/profileApi';
// ─── Helpers (preserved from original App.tsx) ───────────────────────────────
export const resolveProfileNameForAction = (
action: string,
requestedName: string | undefined,
chatProfiles: Array<{ name?: string }>,
suffixProvider: () => string = () =>
new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }).replace(/:/g, '')
) => {
let profileName = requestedName || 'AI Profile';
if (action === 'create_profile') {
const existing = chatProfiles.find(p => p.name === profileName);
if (existing) profileName = `${profileName} (${suffixProvider()})`;
}
return profileName;
};
export const buildChatApplyPayload = (
profileData: any,
currentUserId: string,
profileName: string
) => ({
name: profileName,
user_id: currentUserId,
allocated_capital: Number(profileData.allocated_capital || 1000),
risk_per_trade_percent: Number(profileData.risk_per_trade_percent || 1),
symbols: profileData.symbols || 'BTC/USDT',
is_active: profileData.is_active ?? true,
strategy_config: profileData.strategy_config,
});
// ─── App ─────────────────────────────────────────────────────────────────────
function App() {
const { user, profile, loading, signOut } = useAuth();
const { socket, botState, connected } = useWebSocket(tradingRuntime.tradingApiUrl);
const [activeSymbol, setActiveSymbol] = useState('');
const [chatProfiles, setChatProfiles] = useState<any[]>([]);
const [previewAsCustomer] = useState(false);
const [simpleToasts, setSimpleToasts] = useState<Array<{
id: string;
message: string;
severity: 'INFO' | 'WARN' | 'ERROR';
}>>([]);
const seenSimpleEventIdsRef = useRef<Set<string>>(new Set());
const { enabled: backtestEnabledForView, loading: backtestGateLoading } =
useBacktestFeatureGate({ previewAsCustomer });
const { flags: tabFlags } = useTabFeatureFlags();
// Feature gates
const isAdminAccount = profile?.role === 'admin';
const isAdmin = isAdminAccount && !previewAsCustomer;
const showBacktestTab = isAdmin || (!backtestGateLoading && backtestEnabledForView);
const showMarketplaceTab = isAdmin || tabFlags.marketplace;
// Critical system events (for the alert banner)
const recentCriticalEvents = (botState.operationalEvents ?? []).filter((e): e is NonNullable<typeof e> =>
Boolean(e)
&& (e.severity === 'ERROR' || e.severity === 'WARN')
&& Date.now() - e.timestamp < 600_000
);
const hasCriticalEvents = recentCriticalEvents.length > 0;
useEffect(() => {
const events = botState.operationalEvents ?? [];
const now = Date.now();
const nextToasts: Array<{ id: string; message: string; severity: 'INFO' | 'WARN' | 'ERROR' }> = [];
for (const event of events) {
if (!event || event.type !== 'SIMPLE_SETUP_UPDATE') continue;
if (seenSimpleEventIdsRef.current.has(event.id)) continue;
seenSimpleEventIdsRef.current.add(event.id);
if (now - event.timestamp > 120_000) continue;
nextToasts.push({
id: event.id,
message: event.message,
severity: (event.severity as 'INFO' | 'WARN' | 'ERROR') || 'INFO',
});
}
if (nextToasts.length === 0) return;
setSimpleToasts((prev) => [...prev, ...nextToasts].slice(-4));
for (const toast of nextToasts) {
window.setTimeout(() => {
setSimpleToasts((prev) => prev.filter((entry) => entry.id !== toast.id));
}, 4500);
}
}, [botState.operationalEvents]);
// Chat profile management
const fetchChatProfiles = useCallback(async () => {
const data = await fetchTradeProfiles();
setChatProfiles(data ?? []);
}, []);
useEffect(() => {
if (user) {
fetchChatProfiles();
const id = setInterval(fetchChatProfiles, 30_000);
return () => clearInterval(id);
}
}, [user, fetchChatProfiles]);
const handleChatApply = async (
action: string,
profileData: any
): Promise<{ success: boolean; error?: string }> => {
const currentUserId = user?.id;
if (!currentUserId) return { success: false, error: 'Not authenticated' };
const profileName = resolveProfileNameForAction(action, profileData.name, chatProfiles);
const payload = buildChatApplyPayload(profileData, currentUserId, profileName);
if (action === 'create_profile') {
try {
await createTradeProfile(payload);
fetchChatProfiles();
return { success: true };
} catch (err: any) {
return { success: false, error: err.message };
}
}
if (action === 'update_profile' && profileData.id) {
try {
await updateTradeProfile(profileData.id, payload);
fetchChatProfiles();
return { success: true };
} catch (err: any) {
return { success: false, error: err.message };
}
}
return { success: false, error: 'Unknown action' };
};
const handleSignOut = async () => {
tradingTelemetry.client.trackEvent('info', 'auth', 'trading_web_sign_out', {
userId: user?.id ?? 'anonymous',
feature: 'sign_out',
tags: { surface: 'web' },
});
await signOut();
};
// ── Auth gates ──────────────────────────────────────────────────────────────
if (window.location.pathname === '/reset-callback') return <ResetPassword />;
if (loading) {
return (
<div style={{
display: 'flex', justifyContent: 'center', alignItems: 'center',
height: '100vh', background: 'var(--background)', color: 'var(--foreground)',
fontSize: 15, fontFamily: 'Inter, system-ui, sans-serif',
}}>
Loading
</div>
);
}
if (!user) return <Login />;
// ── Render ──────────────────────────────────────────────────────────────────
return (
<BrowserRouter>
<AppContext.Provider value={{
botState,
socket,
connected,
activeSymbol,
setActiveSymbol,
isAdmin,
user,
profile,
showBacktestTab,
showMarketplaceTab,
handleSignOut,
}}>
{/* Critical system alert banner */}
{hasCriticalEvents && (
<div style={{
position: 'fixed', top: 0, left: 0, right: 0, zIndex: 9999,
background: 'linear-gradient(90deg, #7f1d1d 0%, #dc2626 50%, #7f1d1d 100%)',
color: '#fff',
padding: '6px 20px',
textAlign: 'center',
fontSize: 11,
fontWeight: 900,
textTransform: 'uppercase',
letterSpacing: '0.1em',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 10,
}}>
<span></span>
<span>
SYSTEM ALERT: {recentCriticalEvents.length} CRITICAL ISSUES DETECTED GO TO SETTINGS ADMIN PANEL
</span>
<span></span>
</div>
)}
<div style={{ paddingTop: hasCriticalEvents ? 32 : 0 }}>
<AppShell />
</div>
{simpleToasts.length > 0 && (
<div style={{
position: 'fixed',
right: 16,
bottom: 16,
zIndex: 9998,
display: 'flex',
flexDirection: 'column',
gap: 10,
maxWidth: 420,
}}>
{simpleToasts.map((toast) => {
const palette = toast.severity === 'ERROR'
? { border: 'rgba(239,68,68,0.35)', bg: 'rgba(127,29,29,0.95)', fg: '#fff' }
: toast.severity === 'WARN'
? { border: 'rgba(245,158,11,0.35)', bg: 'rgba(120,53,15,0.95)', fg: '#fff' }
: { border: 'rgba(16,185,129,0.35)', bg: 'rgba(6,78,59,0.95)', fg: '#fff' };
return (
<div
key={toast.id}
style={{
border: `1px solid ${palette.border}`,
background: palette.bg,
color: palette.fg,
borderRadius: 16,
padding: '12px 14px',
boxShadow: '0 12px 32px rgba(0,0,0,0.28)',
fontSize: 13,
lineHeight: 1.45,
backdropFilter: 'blur(8px)',
}}
>
<div style={{ fontSize: 10, fontWeight: 900, letterSpacing: '0.16em', textTransform: 'uppercase', opacity: 0.8, marginBottom: 4 }}>
Simple update
</div>
<div>{toast.message}</div>
</div>
);
})}
</div>
)}
{/* Floating AI strategy assistant */}
<ChatControl profiles={chatProfiles} onApplyProfile={handleChatApply} />
</AppContext.Provider>
</BrowserRouter>
);
}
export default App;