learning_ai_invt_trdg/web/src/App.tsx

259 lines
9.7 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 { Suspense, lazy, 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 { 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';
const ChatControl = lazy(() => import('./components/ChatControl').then((mod) => ({ default: mod.ChatControl })));
// ─── 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,
}}>
<div className={`app-runtime${hasCriticalEvents ? ' has-critical-alert' : ''}`}>
{hasCriticalEvents && (
<a className="critical-alert-banner" href="/settings">
<span aria-hidden="true"></span>
<span>
System alert: {recentCriticalEvents.length} critical issues detected go to Settings Admin Panel
</span>
<span aria-hidden="true"></span>
</a>
)}
<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: 'var(--bl-danger-border, var(--bl-danger))', bg: 'var(--bl-danger-muted)', fg: 'var(--foreground)' }
: toast.severity === 'WARN'
? { border: 'var(--bl-warning-border, var(--bl-warning))', bg: 'var(--bl-warning-muted)', fg: 'var(--foreground)' }
: { border: 'var(--bl-success-border, var(--bl-success))', bg: 'var(--bl-success-muted)', fg: 'var(--foreground)' };
return (
<div
key={toast.id}
style={{
border: `1px solid ${palette.border}`,
background: palette.bg,
color: palette.fg,
borderRadius: 16,
padding: '12px 14px',
boxShadow: 'var(--card-shadow)',
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 */}
<Suspense fallback={null}>
<ChatControl profiles={chatProfiles} botState={botState} onApplyProfile={handleChatApply} />
</Suspense>
</AppContext.Provider>
</BrowserRouter>
);
}
export default App;